""" Calculadora MAV CAS Híbrida - Aplicación principal VERSIÓN ADAPTADA AL NUEVO SISTEMA DE TIPOS """ import tkinter as tk from tkinter import scrolledtext, messagebox, Menu, filedialog import tkinter.font as tkFont import json import logging # <--- AÑADIDO import os from pathlib import Path import threading from typing import List, Dict, Any, Optional import re import time # ========== IMPORTS PARA SISTEMA DE AYUDA ========== # Para la ayuda en HTML MARKDOWN_AVAILABLE = False HTML_VIEWER_TYPE = None try: import markdown MARKDOWN_AVAILABLE = True except ImportError: # markdown not available, MARKDOWN_AVAILABLE remains False pass # Intentar importar visores HTML try: import tkinterweb HTML_VIEWER_TYPE = "tkinterweb" except ImportError: try: from tkhtmlview import HTMLScrolledText HTML_VIEWER_TYPE = "tkhtmlview" except ImportError: HTML_VIEWER_TYPE = None # Usar logging para estas advertencias iniciales module_logger = logging.getLogger(__name__) if not MARKDOWN_AVAILABLE: module_logger.warning("La librería 'markdown' no está instalada. La ayuda se mostrará en texto plano.") if MARKDOWN_AVAILABLE and HTML_VIEWER_TYPE is None: module_logger.warning("'markdown' está disponible, pero no se encontró un visor HTML (tkinterweb/tkhtmlview). La ayuda se mostrará en texto plano.") # ========== IMPORTS ADAPTADOS AL NUEVO SISTEMA ========== # Importar componentes del CAS híbrido con nuevo sistema de tipos from main_evaluation_puro import PureAlgebraicEngine, EvaluationResult from tl_popup import InteractiveResultManager, PlotResult from type_registry import get_registered_helper_functions, get_registered_base_context import sympy from sympy_helper import SympyTools as SympyHelper class HybridCalculatorApp: """Aplicación principal del CAS híbrido - ADAPTADA AL NUEVO SISTEMA""" SETTINGS_FILE = "hybrid_calc_settings.json" HISTORY_FILE = "hybrid_calc_history.txt" HELP_FILE = "readme.md" # ========== NUEVO: Archivo de ayuda externo ========== def __init__(self, root: tk.Tk): self.root = root # Configurar logging DEBUG para ver qué pasa logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s') self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.DEBUG) # ========== INSTANCIAS DEL SISTEMA ========== # Motor de evaluación (instancia única) self.engine = PureAlgebraicEngine() # Manager de contenido interactivo self.interactive_manager = None # Se inicializará en setup_interactive_manager() # ========== CONFIGURACIÓN DE INTERFAZ ========== self.settings = self._load_settings() self.debug = self.settings.get("debug_mode", False) # ========== VARIABLES DE AUTOCOMPLETADO ========== self._autocomplete_popup = None self._autocomplete_listbox = None self._autocomplete_active = False self._autocomplete_suggestions = [] self._autocomplete_filter_text = "" self._autocomplete_trigger_pos = "" self._popup_disabled_until_next_dot = False self._variable_popup_active = False self._last_navigation_time = 0 # ========== VARIABLES PANEL LATEX ========== self.latex_panel_visible = False self._latex_equations = [] self.latex_renderer = None self._webview_available = False self._webview_type = None self._js_available = False # ========== VARIABLES DE ESTADO FALTANTES ========== self._cached_input_font = None self._debounce_job = None self._syncing_yview = False self.output_buffer = [] # Variables para autocompletado de variables self._variable_popup_job = None self._last_input_change = 0 # ========== CONFIGURACIÓN DE VENTANA ========== self._setup_window() self._setup_icon() # ========== CONSTRUCCIÓN DE INTERFAZ ========== self.create_widgets() self.create_menu() self.setup_output_tags() self.setup_scroll_sync() self.setup_interactive_manager() # ========== CONFIGURACIÓN FINAL ========== self._setup_dynamic_helpers() self.load_history() # ========== CONFIGURACIÓN INICIAL ========== # Configurar bindings de teclado self._setup_key_bindings() def _setup_window(self): """Configura la ventana principal""" self.root.title("Calculadora MAV - CAS Híbrido") self.root.geometry(self.settings.get("window_geometry", "1000x700")) self.root.configure(bg="#2b2b2b") # Configurar eventos de cierre self.root.protocol("WM_DELETE_WINDOW", self.on_close) # ========== BARRA DE ESTADO ========== self.status_frame = tk.Frame(self.root, bg="#2b2b2b", height=25) self.status_frame.pack(side=tk.BOTTOM, fill=tk.X) self.status_frame.pack_propagate(False) self.status_label = tk.Label( self.status_frame, text="🔢 Calculadora MAV - Sistema Algebraico Puro", bg="#2b2b2b", fg="#80c7f7", font=("Consolas", 9), anchor=tk.W ) self.status_label.pack(side=tk.LEFT, padx=10, pady=2) def _setup_key_bindings(self): """Configura los bindings de teclado""" try: # ========== BINDINGS DE TECLADO ========== self.input_text.bind("", self.on_key_release) self.input_text.bind("", self.on_key_press) self.input_text.bind("", self._on_input_click) self.input_text.bind("", lambda e: self._close_autocomplete_popup()) # Bindings para navegación del autocompletado self.input_text.bind("", self._handle_arrow_key) self.input_text.bind("", self._handle_arrow_key) self.input_text.bind("", self._handle_tab_key) self.input_text.bind("", self._handle_escape_key) self.logger.debug("✅ Bindings de teclado configurados") except Exception as e: self.logger.error(f"❌ Error configurando bindings: {e}") def _setup_dynamic_helpers(self): """Configura helpers dinámicamente desde el registro de tipos""" try: # Obtener helpers registrados dinámicamente self.HELPERS = get_registered_helper_functions() # Añadir SympyHelper.Helper al final self.HELPERS.append(SympyHelper.Helper) # Usar logger en lugar de print, y sin emoji para la consola self.logger.info(f"Helpers dinámicos cargados: {len(self.HELPERS)}") # Original: 🆘 except Exception as e: # Usar logger en lugar de print, y sin emoji para la consola self.logger.error(f"Error cargando helpers dinámicos: {e}", exc_info=True) # Original: ⚠️ # Fallback a helpers básicos self.HELPERS = [SympyHelper.Helper] def reload_types(self): """Recarga el sistema de tipos (útil para desarrollo)""" try: self.logger.info("Recargando sistema de tipos...") # Recargar helpers self._setup_dynamic_helpers() # Re-evaluar contenido actual self._evaluate_and_update() self.logger.info("Sistema de tipos recargado.") except Exception as e: self.logger.error(f"Error recargando tipos: {e}", exc_info=True) messagebox.showerror("Error", f"Error recargando tipos:\n{e}") def show_types_info(self): """Muestra información sobre tipos disponibles""" try: context_info = self.engine.get_context_info() info_text = f"""INFORMACIÓN DEL SISTEMA ALGEBRAICO PURO Ecuaciones en el sistema: {context_info.get('equations', 0)} Variables definidas: {context_info.get('variables', 0)} Variables activas: {', '.join(context_info.get('variable_names', []))} CARACTERÍSTICAS: • Sistema de ecuaciones puras con SymPy • Todas las asignaciones son ecuaciones • Resolución automática de sistemas • Evaluación numérica inteligente • Atajo x=? equivale a solve(x) """ # Mostrar en ventana self._show_help_window("Información del Sistema", info_text) except Exception as e: messagebox.showerror("Error", f"Error obteniendo información del sistema:\n{e}") def _setup_icon(self): """Configura el ícono de la aplicación""" try: script_dir = Path(__file__).resolve().parent icon_path = script_dir / "icon.png" if not icon_path.is_file(): self.logger.warning(f"Archivo de ícono no encontrado en '{icon_path}'.") return self.app_icon = tk.PhotoImage(file=str(icon_path)) self.root.iconphoto(True, self.app_icon) except tk.TclError as e: self.logger.warning(f"No se pudo cargar el ícono desde '{icon_path}'. Error de Tkinter: {e}") except Exception as e: self.logger.warning(f"Ocurrió un error inesperado al cargar el ícono desde '{icon_path}': {e}", exc_info=True) def _load_settings(self) -> Dict[str, Any]: """Carga configuración de la aplicación""" if os.path.exists(self.SETTINGS_FILE): try: with open(self.SETTINGS_FILE, "r", encoding="utf-8") as f: return json.load(f) except (IOError, json.JSONDecodeError): return {} return {} def _save_settings(self): """Guarda configuraciones en archivo JSON""" try: # Obtener geometría actual self.settings["window_geometry"] = self.root.geometry() # Guardar posición del panel divisor si existe if hasattr(self, 'paned_window'): sash_pos = self.paned_window.sash_coord(0)[0] self.settings["sash_pos_x"] = sash_pos with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f: json.dump(self.settings, f, indent=4, ensure_ascii=False) except Exception as e: if self.debug: self.logger.error(f"Error guardando configuración: {e}", exc_info=True) def create_widgets(self): """Crea la interfaz gráfica con panel LaTeX opcional y expandible""" # Frame principal main_frame = tk.Frame(self.root, bg="#2b2b2b", bd=0) main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # Frame para el contenido principal (paneles + botón expandible) content_frame = tk.Frame(main_frame, bg="#2b2b2b") content_frame.pack(fill=tk.BOTH, expand=True) # Panel dividido principal (horizontal) - solo 2 paneles inicialmente self.paned_window = tk.PanedWindow( content_frame, orient=tk.HORIZONTAL, bg="#2b2b2b", sashrelief=tk.FLAT, sashwidth=4, bd=0, showhandle=False, opaqueresize=True, ) self.paned_window.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # Panel de entrada (limitado a ~50 caracteres) initial_input_width = min(self.settings.get("sash_pos_x", 450), 450) # Máximo 450px self.input_text = scrolledtext.ScrolledText( self.paned_window, font=("Consolas", 11), bg="#1e1e1e", fg="#d4d4d4", insertbackground="#ffffff", selectbackground="#264f78", undo=True, wrap=tk.NONE, borderwidth=0, highlightthickness=0, relief=tk.FLAT, ) self.paned_window.add( self.input_text, width=initial_input_width, stretch="never", # No se expande automáticamente minsize=200 ) # Panel de salida self.output_text = scrolledtext.ScrolledText( self.paned_window, font=("Consolas", 11), bg="#0f0f0f", fg="#00ff00", state="disabled", wrap=tk.NONE, borderwidth=0, highlightthickness=0, relief=tk.FLAT, ) self.paned_window.add( self.output_text, stretch="always", minsize=200 ) # NUEVO: Botón expandible para el panel LaTeX self._create_expandable_latex_button(content_frame) # NUEVO: Configurar panel LaTeX (oculto inicialmente) self._setup_latex_panel_expandable() # Estado del panel LaTeX self.latex_panel_visible = self.settings.get("latex_panel_visible", False) self._latex_equations = [] # Configurar eventos self.input_text.bind("", lambda e: self._show_context_menu(e, "input")) self.output_text.bind("", lambda e: self._show_context_menu(e, "output")) # Configurar scroll sincronizado self.setup_scroll_sync() # Configurar tags de salida self.setup_output_tags() # Crear menú self.create_menu() # Restaurar estado del panel LaTeX si estaba visible if self.latex_panel_visible: self.root.after(100, self._show_latex_panel) # Delay para que la UI esté lista def _create_expandable_latex_button(self, parent): """Crea el botón expandible para mostrar/ocultar el panel LaTeX""" # Frame para el botón (vertical en el borde derecho) self.expand_button_frame = tk.Frame( parent, bg="#3c3c3c", width=20, bd=0, relief=tk.FLAT ) self.expand_button_frame.pack(side=tk.RIGHT, fill=tk.Y) self.expand_button_frame.pack_propagate(False) # Botón principal (vertical) - SIN TOOLTIP que interfiere self.latex_expand_button = tk.Button( self.expand_button_frame, text="📐", # Icono de ecuaciones font=("Segoe UI Symbol", 12), bg="#3c3c3c", fg="#80c7f7", activebackground="#4fc3f7", activeforeground="white", bd=0, relief=tk.FLAT, cursor="hand2", command=self._toggle_latex_panel # Click simple directo ) self.latex_expand_button.pack(expand=True, fill=tk.BOTH, padx=2, pady=10) # Solo evento de click derecho para info (SIN tooltip que interfiere) self.latex_expand_button.bind("", lambda e: self._on_latex_button_info()) # Indicador de contenido LaTeX disponible (inicialmente oculto) self.latex_indicator = tk.Label( self.expand_button_frame, text="●", font=("Arial", 8), bg="#3c3c3c", fg="#4fc3f7", ) # No empaquetar inicialmente (se muestra cuando hay contenido) def _on_latex_button_info(self): """Maneja click derecho en el botón LaTeX (mostrar info en status bar)""" equation_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0 status_text = f"📐 Panel LaTeX: {equation_count} ecuaciones disponibles" if hasattr(self, 'status_label'): original_text = self.status_label.cget("text") self.status_label.config(text=status_text) # Restaurar texto original después de 2 segundos self.root.after(2000, lambda: self.status_label.config(text=original_text)) def _toggle_latex_panel(self): """Muestra/oculta el panel LaTeX con animación""" if not hasattr(self, 'latex_panel_visible'): self.latex_panel_visible = False if self.latex_panel_visible: self._hide_latex_panel() else: self._show_latex_panel() # Guardar estado en configuración self.settings["latex_panel_visible"] = self.latex_panel_visible def _show_latex_panel(self): """Muestra el panel LaTeX con animación suave""" if not hasattr(self, 'latex_panel') or self.latex_panel_visible: return try: # Cambiar icono del botón self.latex_expand_button.config(text="📐", bg="#4fc3f7") # Agregar el panel LaTeX al PanedWindow self.paned_window.add( self.latex_panel, width=self._min_latex_pane_width, stretch="never", minsize=self._min_latex_pane_width ) # Actualizar flag self.latex_panel_visible = True # Actualizar contenido si hay ecuaciones pendientes if hasattr(self, '_latex_equations') and self._latex_equations: self._refresh_latex_content() # Actualizar indicador self._update_latex_indicator() if self.debug: self.logger.info("Panel LaTeX mostrado") except Exception as e: self.logger.error(f"Error mostrando panel LaTeX: {e}") def _hide_latex_panel(self): """Oculta el panel LaTeX con animación suave""" if not hasattr(self, 'latex_panel') or not self.latex_panel_visible: return try: # Cambiar icono del botón self.latex_expand_button.config(text="📐", bg="#3c3c3c") # Remover el panel del PanedWindow self.paned_window.forget(self.latex_panel) # Actualizar flag self.latex_panel_visible = False if self.debug: self.logger.info("Panel LaTeX ocultado") except Exception as e: self.logger.error(f"Error ocultando panel LaTeX: {e}") def _update_latex_indicator(self): """Actualiza el indicador visual de contenido LaTeX""" if not hasattr(self, 'latex_indicator'): return equation_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0 if equation_count > 0: # Mostrar indicador if not self.latex_indicator.winfo_ismapped(): self.latex_indicator.pack(side=tk.BOTTOM, pady=2) else: # Ocultar indicador if self.latex_indicator.winfo_ismapped(): self.latex_indicator.pack_forget() def _setup_latex_panel_expandable(self): """Configura el panel LaTeX expandible con pywebview""" try: # Frame para el panel LaTeX (crear pero no agregar al PanedWindow todavía) self.latex_panel = tk.Frame(self.root, bg="#1a1a1a", bd=1, relief=tk.SOLID) # Título del panel title_frame = tk.Frame(self.latex_panel, bg="#1a1a1a", height=25) title_frame.pack(fill=tk.X, pady=(2, 0)) title_frame.pack_propagate(False) title_label = tk.Label( title_frame, text="📐 Ecuaciones & Asignaciones", bg="#1a1a1a", fg="#80c7f7", font=("Consolas", 9, "bold"), anchor=tk.W ) title_label.pack(side=tk.LEFT, padx=5, pady=2) # Botón de cerrar en el título close_button = tk.Button( title_frame, text="✕", font=("Arial", 8), bg="#1a1a1a", fg="#808080", activebackground="#ff6b6b", activeforeground="white", bd=0, relief=tk.FLAT, cursor="hand2", command=self._hide_latex_panel ) close_button.pack(side=tk.RIGHT, padx=5) # NUEVO: Sistema con pywebview (más robusto para MathJax) try: import webview # JavaScript siempre disponible con pywebview self._js_available = True self.logger.debug("✅ pywebview disponible - JavaScript totalmente habilitado") # Frame para contener el webview html_frame = tk.Frame(self.latex_panel, bg="#1a1a1a") html_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) # PLACEHOLDER: El webview se creará cuando sea necesario # pywebview requiere un manejo especial para integración con tkinter self.latex_webview = None self.latex_webview_frame = html_frame self._webview_ready = False self._webview_available = True self._webview_type = "pywebview" self.logger.debug("🌐 pywebview configurado - webview se creará dinámicamente") except ImportError: # Fallback a tkinterweb si pywebview no está disponible try: import tkinterweb self.logger.warning("⚠️ pywebview no disponible, usando tkinterweb como fallback") # Verificar disponibilidad de JavaScript en tkinterweb try: import pythonmonkey self._js_available = True self.logger.debug("✅ tkinterweb con PythonMonkey disponible") except ImportError: self._js_available = False self.logger.debug("ℹ️ tkinterweb sin PythonMonkey - modo fallback") # Crear el visor HTML con tkinterweb html_frame = tk.Frame(self.latex_panel, bg="#1a1a1a") html_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) self.latex_webview = tkinterweb.HtmlFrame( html_frame, messages_enabled=False ) self.latex_webview.pack(fill=tk.BOTH, expand=True) # Cargar HTML base optimizado base_html = self._generate_base_html() self.latex_webview.load_html(base_html) self._webview_available = True self._webview_type = "tkinterweb" except ImportError: # Sin ninguna de las dos librerías messagebox.showerror( "Dependencia Requerida", "Esta calculadora requiere 'pywebview' o 'tkinterweb' para el renderizado LaTeX.\n\n" "Instala con: pip install pywebview\n" "O: pip install tkinterweb\n\n" "La aplicación se cerrará." ) self.root.quit() return # Configurar tamaño del panel self._min_latex_pane_width = 300 # Mostrar información sobre el tipo de visor usado if self.debug: self.logger.info(f"Panel LaTeX expandible configurado con: {self._webview_type}") except Exception as e: self.logger.error(f"Error configurando panel LaTeX expandible: {e}") # Si falla completamente, no agregar el tercer panel pass def _refresh_latex_content(self): """Refresca el contenido del panel LaTeX cuando se muestra""" if not hasattr(self, 'latex_panel') or not self.latex_panel_visible: return try: # Actualizar contenido si hay ecuaciones pendientes if hasattr(self, '_latex_equations') and self._latex_equations: self._update_latex_panel() except Exception as e: self.logger.debug(f"Error refrescando contenido LaTeX: {e}") def _generate_base_html(self): """Genera el HTML base optimizado según el tipo de webview""" if self._webview_type == "pywebview": return self._generate_html_for_pywebview() elif self._js_available: return self._generate_html_with_javascript() else: return self._generate_html_fallback() def _generate_html_with_javascript(self): """HTML con JavaScript completo para tkinterweb""" return r""" Ecuaciones LaTeX
📐 Panel de Ecuaciones LaTeX
Renderizado con MathJax + fuentes matemáticas
Las ecuaciones se mostrarán aquí automáticamente
""" def _generate_html_for_pywebview(self): """HTML optimizado específicamente para pywebview (más simple y robusto)""" return r""" Ecuaciones LaTeX - PyWebView
📐 Panel de Ecuaciones LaTeX
Renderizado con MathJax en PyWebView
Las ecuaciones aparecerán aquí automáticamente
✓ PyWebView activo - MathJax cargándose...
""" def _generate_html_fallback(self): """HTML fallback simple sin JavaScript""" return r""" Ecuaciones LaTeX (Modo Fallback)
📐 Panel de Ecuaciones LaTeX (Modo Fallback)
Renderizado texto mejorado - Sin JavaScript/MathJax
Las ecuaciones se mostrarán en formato texto matemático mejorado
ℹ️ Modo Fallback - Matemáticas en Texto Mejorado
Para MathJax completo: pip install tkinterweb[javascript]
""" def setup_interactive_manager(self): """Configura el gestor de resultados interactivos""" self.interactive_manager = InteractiveResultManager(self.root) # Configurar callback para actualizar el panel de entrada cuando se edite una expresión self.interactive_manager.set_update_callback(self._update_input_expression) def create_menu(self): """Crea el menú de la aplicación""" menubar = Menu(self.root) self.root.config(menu=menubar) # Menú Archivo file_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Archivo", menu=file_menu) file_menu.add_command(label="Nuevo", command=self.new_session) file_menu.add_separator() file_menu.add_command(label="Cargar...", command=self.load_file) file_menu.add_command(label="Guardar como...", command=self.save_file) file_menu.add_separator() file_menu.add_command(label="Salir", command=self.on_close) # Menú Editar edit_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Editar", menu=edit_menu) edit_menu.add_command(label="Limpiar entrada", command=self.clear_input) edit_menu.add_command(label="Limpiar salida", command=self.clear_output) edit_menu.add_separator() edit_menu.add_command(label="Limpiar historial", command=self.clear_history) # NUEVO: Menú Ver view_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Ver", menu=view_menu) view_menu.add_command( label="📐 Panel LaTeX", command=self._toggle_latex_panel, accelerator="Doble-click borde derecho" ) view_menu.add_separator() view_menu.add_command(label="Información del sistema", command=self.show_types_info) # Menú Herramientas (simplificado) tools_menu = Menu(menubar, tearoff=0, bg="#3c3c3c", fg="white") menubar.add_cascade(label="Herramientas", menu=tools_menu) tools_menu.add_command(label="Recargar Tipos Personalizados", command=self.reload_types) tools_menu.add_separator() tools_menu.add_command(label="📊 Información del Sistema", command=self.show_types_info) tools_menu.add_command(label="🔍 Diagnóstico MathJax", command=self._diagnose_mathjax) tools_menu.add_command(label="🔧 Test tkinterweb", command=self._test_tkinterweb_mathjax) tools_menu.add_command(label="🌐 Abrir HTML en Navegador", command=self._open_debug_html_in_browser) tools_menu.add_separator() tools_menu.add_command(label="📊 Estado Panel LaTeX", command=self._show_latex_panel_status) tools_menu.add_command(label="🔄 Forzar Actualización HTML", command=self._force_html_update) # ========== MENÚ TIPOS (NUEVO) ========== types_menu = Menu(menubar, tearoff=0, bg="#3c3c3c", fg="white") menubar.add_cascade(label="Tipos", menu=types_menu) types_menu.add_command(label="Información de tipos", command=self.show_types_info) types_menu.add_separator() types_menu.add_command(label="Sintaxis de tipos", command=self.show_types_syntax) # Menú Ayuda (actualizado) help_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Ayuda", menu=help_menu) help_menu.add_command(label="Guía rápida", command=self.show_quick_guide) help_menu.add_command(label="Sintaxis", command=self.show_syntax_help) help_menu.add_command(label="Funciones SymPy", command=self.show_sympy_functions) help_menu.add_separator() help_menu.add_command(label="Acerca de", command=self.show_about) def setup_scroll_sync(self): """Configura scroll sincronizado entre paneles""" def _yscroll_input_command(*args): self.input_text.vbar.set(*args) if not self._syncing_yview: self._syncing_yview = True self.output_text.yview_moveto(args[0]) self._syncing_yview = False def _yscroll_output_command(*args): self.output_text.vbar.set(*args) if not self._syncing_yview: self._syncing_yview = True self.input_text.yview_moveto(args[0]) self._syncing_yview = False def _unified_mouse_wheel(event): if self._syncing_yview: return "break" if hasattr(event, "widget") and event.widget: event.widget.yview_scroll(int(-1 * (event.delta / 120)), "units") return "break" self.input_text.config(yscrollcommand=_yscroll_input_command) self.output_text.config(yscrollcommand=_yscroll_output_command) self.input_text.bind("", _unified_mouse_wheel) self.output_text.bind("", _unified_mouse_wheel) def setup_output_tags(self): """Configura tags de formato para el panel de salida""" default_font = self._get_input_font() # Crear una fuente específica para errores (bold) error_font = tkFont.Font(family=default_font.cget("family"), size=default_font.cget("size"), weight="bold") # Tag base self.output_text.tag_configure("base", font=default_font, foreground="#d4d4d4") # Tags específicos # Sympy y tipos base self.output_text.tag_configure("symbolic", foreground="#9cdcfe") # Azul claro (SymPy) self.output_text.tag_configure("numeric", foreground="#b5cea8") # Verde (Números) self.output_text.tag_configure("boolean", foreground="#569cd6") # Azul (Booleanos) self.output_text.tag_configure("string", foreground="#ce9178") # Naranja (Strings) # Tipos registrados dinámicamente (usar un color base) self.output_text.tag_configure("custom_type", foreground="#4ec9b0") # Turquesa (Tipos Custom) # Estado de la aplicación self.output_text.tag_configure("error", foreground="#f44747", font=error_font) # Rojo self.output_text.tag_configure("comment", foreground="#6a9955") # Verde Oliva (Comentarios) self.output_text.tag_configure("assignment", foreground="#dcdcaa") # Amarillo (Asignaciones) self.output_text.tag_configure("equation", foreground="#c586c0") # Púrpura (Ecuaciones) self.output_text.tag_configure("plot", foreground="#569cd6", underline=True) # Azul con subrayado (Plots) # Para el nuevo indicador de tipo algebraico self.output_text.tag_configure("type_indicator", foreground="#808080") # Gris oscuro # Configurar tags para tipos específicos si es necesario (ejemplo) # self.output_text.tag_configure("IP4", foreground="#4ec9b0") # self.output_text.tag_configure("IntBase", foreground="#4ec9b0") def on_key_press(self, event=None): """Maneja eventos de presión de tecla (antes de que se inserte el carácter)""" # Si el popup está activo, manejar navegación y selección if (self._autocomplete_active or self._variable_popup_active) and event: if event.keysym in ['Up', 'Down']: return self._handle_arrow_key(event) elif event.keysym == 'Tab': return self._handle_tab_key(event) elif event.keysym == 'Escape': return self._handle_escape_key(event) # Detectar backspace para cerrar popup de funciones si se borra el punto if event and event.keysym == 'BackSpace' and self._autocomplete_active: self._check_dot_removal() def _check_dot_removal(self): """Verifica si se va a borrar el punto que activó el autocompletado""" try: # Obtener posición del cursor cursor_pos = self.input_text.index(tk.INSERT) # Obtener el carácter anterior al cursor if cursor_pos != "1.0": # No estamos al inicio del texto prev_char_pos = f"{cursor_pos}-1c" prev_char = self.input_text.get(prev_char_pos, cursor_pos) # Si el carácter anterior es un punto, cerrar el popup if prev_char == '.': # Programar cierre después del backspace self.root.after(1, self._close_autocomplete_popup) except tk.TclError: # Error de posición, cerrar popup por seguridad self._close_autocomplete_popup() def on_key_release(self, event=None): """Maneja eventos de teclado después de insertar carácter""" if self._debounce_job: self.root.after_cancel(self._debounce_job) # Cancelar job de autocompletado de variables si existe if self._variable_popup_job: self.root.after_cancel(self._variable_popup_job) self._variable_popup_job = None # Verificar si acabamos de navegar (evitar filtrado inmediato) import time just_navigated = (time.time() - self._last_navigation_time) < 0.1 # Manejar autocompletado con punto if event and event.char == '.' and self.input_text.focus_get() == self.input_text: # Cerrar popup de variables si está activo if self._variable_popup_active: self._close_autocomplete_popup() if not self._popup_disabled_until_next_dot: self._handle_dot_autocomplete() else: # Resetear flag cuando se escribe un nuevo punto self._popup_disabled_until_next_dot = False # Filtrar autocompletado si está activo (pero no si acabamos de navegar) elif self._autocomplete_active and event and event.char.isprintable() and not just_navigated: self._filter_autocomplete() # Marcar tiempo del último cambio de input if event and event.char.isprintable(): self._last_input_change = time.time() # Evaluación con debounce y auto-dimensionado self._debounce_job = self.root.after(300, self._process_input_and_adjust_layout) # Programar autocompletado de variables (nuevo sistema) self._schedule_variable_autocomplete_improved() def _schedule_variable_autocomplete_improved(self): """Programa el autocompletado de variables mientras se escribe""" # Solo si no hay popup de funciones activo if self._autocomplete_active or self._popup_disabled_until_next_dot: self.logger.debug("Variable autocomplete: Saltando - popup activo o deshabilitado") return # Verificar que estemos escribiendo (no solo navegando) current_line = self.input_text.get("insert linestart", "insert lineend").strip() if not current_line or current_line.endswith('.'): self.logger.debug(f"Variable autocomplete: Saltando - línea vacía o termina en punto: '{current_line}'") return # Cancelar job anterior si existe if self._variable_popup_job: self.root.after_cancel(self._variable_popup_job) self.logger.debug(f"Variable autocomplete: Programando para línea: '{current_line}'") # Programar para 800ms después self._variable_popup_job = self.root.after(800, self._show_variable_autocomplete_improved) def _show_variable_autocomplete_improved(self): """Muestra autocompletado de variables disponibles (simplificado)""" self.logger.debug("Variable autocomplete: Ejecutando show_variable_autocomplete_improved") if self._autocomplete_active or self._variable_popup_active: self.logger.debug("Variable autocomplete: Saltando - ya hay popup activo") return # Ya hay un popup activo # Verificar que aún estemos en una línea válida current_line = self.input_text.get("insert linestart", "insert lineend").strip() if not current_line or current_line.endswith('.'): self.logger.debug(f"Variable autocomplete: Saltando - línea inválida: '{current_line}'") self._variable_popup_job = None return # Obtener variables del contexto try: context = self.engine._get_full_context() self.logger.debug(f"Variable autocomplete: Contexto completo tiene {len(context)} elementos") # Mostrar tabla de símbolos específicamente symbol_table = getattr(self.engine, 'symbol_table', {}) self.logger.debug(f"Variable autocomplete: Symbol table tiene {len(symbol_table)} elementos: {list(symbol_table.keys())}") variables = [] # Filtrar variables (excluir funciones built-in y módulos) for name, value in context.items(): # Debug detallado de cada elemento is_underscore = name.startswith('_') is_callable = callable(value) has_module = hasattr(value, '__module__') is_excluded = name in ['sympy', 'math', 'numpy', 'plt', 'builtins'] # Permitir variables de SymPy específicamente (ANTES del log) is_sympy_symbol = hasattr(value, 'is_symbol') or 'sympy' in str(type(value)).lower() self.logger.debug(f"Variable autocomplete: Analizando '{name}': underscore={is_underscore}, callable={is_callable}, module={has_module}, excluded={is_excluded}, sympy_symbol={is_sympy_symbol}, type={type(value)}") if (not is_underscore and not is_callable and (not has_module or is_sympy_symbol) and # Permitir SymPy symbols not is_excluded): self.logger.debug(f"Variable autocomplete: ✅ Aceptando variable '{name}' = {value}") # Crear descripción del valor (más corta) value_str = str(value) if len(value_str) > 20: value_str = value_str[:17] + "..." variables.append((name, value_str)) else: self.logger.debug(f"Variable autocomplete: ❌ Rechazando '{name}' por filtros") self.logger.debug(f"Variable autocomplete: Encontradas {len(variables)} variables totales") if variables: variables.sort(key=lambda x: x[0]) # Obtener texto actual para filtrado words = current_line.split() self.logger.debug(f"Variable autocomplete: Palabras en línea: {words}") if words: last_word = words[-1] self.logger.debug(f"Variable autocomplete: Última palabra: '{last_word}'") # Filtrar variables que empiecen con la palabra actual # Y que la palabra actual no sea igual a una variable existente filtered_vars = [ (name, value) for name, value in variables if name.lower().startswith(last_word.lower()) and name != last_word ] self.logger.debug(f"Variable autocomplete: Variables filtradas: {len(filtered_vars)}") if filtered_vars: # Posicionar en el cursor actual self._autocomplete_trigger_pos = self.input_text.index(tk.INSERT) self._autocomplete_filter_text = "" # Mostrar popup de variables menos invasivo self._show_variable_popup(filtered_vars) self.logger.debug(f"Mostrando autocompletado de variables: {len(filtered_vars)} encontradas") else: self.logger.debug("Variable autocomplete: No hay variables filtradas que mostrar") else: self.logger.debug("Variable autocomplete: No hay palabras en la línea") else: self.logger.debug("Variable autocomplete: No hay variables en el contexto") except Exception as e: self.logger.debug(f"Error obteniendo variables para autocompletado: {e}") # Limpiar job self._variable_popup_job = None def _show_variable_popup(self, variables): """Muestra popup de variables con estilo menos invasivo""" cursor_bbox = self.input_text.bbox(tk.INSERT) if not cursor_bbox: return # Marcar como popup de variables activo self._variable_popup_active = True self._autocomplete_active = False # No es el popup principal x, y, _, height = cursor_bbox popup_x = self.input_text.winfo_rootx() + x popup_y = self.input_text.winfo_rooty() + y + height + 2 # Crear popup más discreto self._autocomplete_popup = tk.Toplevel(self.root) self._autocomplete_popup.wm_overrideredirect(True) self._autocomplete_popup.wm_geometry(f"+{popup_x}+{popup_y}") self._autocomplete_popup.attributes('-topmost', True) # Crear listbox con colores discretos pero visibles self._autocomplete_listbox = tk.Listbox( self._autocomplete_popup, bg="#2d2d30", # Más oscuro fg="#c9c9c9", # Texto más visible que antes selectbackground="#4a4a4a", # Selección más visible selectforeground="#ffffff", borderwidth=1, relief="solid", exportselection=False, activestyle="none", font=("Consolas", 10) # Fuente legible ) # Llenar con variables (formato más simple) for name, value in variables: self._autocomplete_listbox.insert(tk.END, f"{name} = {value}") if variables: self._autocomplete_listbox.select_set(0) self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH) # Solo doble-click para seleccionar (más discreto) self._autocomplete_listbox.bind("", lambda e: self._select_variable()) # Binding para cerrar si se hace click fuera self.root.bind("", self._on_click_outside_variable, add=True) # Calcular tamaño más compacto max_len = 15 for name, value in variables: item_text = f"{name} = {value}" max_len = max(max_len, len(item_text)) width = min(max_len + 3, 40) height = min(len(variables), 5) self._autocomplete_listbox.config(width=width, height=height) else: self._close_autocomplete_popup() def _handle_arrow_key(self, event): """Maneja las teclas de flecha cuando el popup está activo""" if not self._autocomplete_active and not self._variable_popup_active: return # Permitir comportamiento normal direction = -1 if event.keysym == 'Up' else 1 self._navigate_autocomplete_improved(direction) # Marcar tiempo de navegación para evitar filtrado inmediato import time self._last_navigation_time = time.time() return "break" # Prevenir comportamiento normal def _handle_tab_key(self, event): """Maneja la tecla TAB para seleccionar del popup""" if self._autocomplete_active or self._variable_popup_active: self._select_autocomplete() return "break" return # Permitir comportamiento normal si no hay popup def _handle_escape_key(self, event): """Maneja la tecla ESC para cerrar popup""" if self._autocomplete_active or self._variable_popup_active: self._close_autocomplete_popup() if self._autocomplete_active: self._popup_disabled_until_next_dot = True return "break" return # Permitir comportamiento normal si no hay popup def _on_input_click(self, event): """Maneja clicks en el campo de entrada""" self._close_autocomplete_popup() def _handle_dot_autocomplete(self): """Maneja el autocompletado cuando se escribe un punto - VERSIÓN MEJORADA""" self._close_autocomplete_popup() cursor_index_str = self.input_text.index(tk.INSERT) line_num_str, char_num_str = cursor_index_str.split('.') current_line_num = int(line_num_str) char_idx_after_dot = int(char_num_str) if char_idx_after_dot == 0: self.logger.debug("Autocomplete: Cursor at beginning of line after dot. No action.") return # Guardar posición donde se activó el autocompletado self._autocomplete_trigger_pos = f"{current_line_num}.{char_idx_after_dot}" self._autocomplete_filter_text = "" dot_char_index_in_line = char_idx_after_dot - 1 text_on_line_up_to_dot = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{dot_char_index_in_line}") stripped_text_before_dot = text_on_line_up_to_dot.strip() # 1. Determinar si es un popup GLOBAL (usando contexto dinámico) if not stripped_text_before_dot: self.logger.debug("Dot on empty line or after spaces. Offering global suggestions.") suggestions = [] # ========== USAR CONTEXTO DINÁMICO DEL REGISTRO ========== try: dynamic_context = get_registered_base_context() for name, class_or_func in dynamic_context.items(): if name[0].isupper(): # Prioritizar nombres capitalizados hint = f"Tipo o función: {name}" if hasattr(class_or_func, '__doc__') and class_or_func.__doc__: first_line_doc = class_or_func.__doc__.strip().split('\n')[0] hint = f"{name} - {first_line_doc}" elif hasattr(class_or_func, 'Helper'): try: helper_text = class_or_func.Helper(name) if helper_text: hint = helper_text.split('\n')[0] except Exception as e_helper: self.logger.debug(f"Error calling Helper for {name}: {e_helper}") pass suggestions.append((name, hint)) except Exception as e: self.logger.debug(f"Error obteniendo contexto dinámico: {e}") # Fallback básico suggestions = [("sin", "Función seno"), ("cos", "Función coseno")] # Añadir funciones de SympyHelper try: sympy_functions = SympyHelper.PopupFunctionList() if sympy_functions: current_suggestion_names = {s[0] for s in suggestions} for fname, fhint in sympy_functions: if fname not in current_suggestion_names: suggestions.append((fname, fhint)) except Exception as e: self.logger.debug(f"Error calling SympyHelper.PopupFunctionList() for global: {e}") if suggestions: suggestions.sort(key=lambda x: x[0]) self._show_autocomplete_popup(suggestions, is_global_popup=True) return # 2. Es un popup de OBJETO obj_expr_str_candidate = "" obj_expr_regex = r"([a-zA-Z_][a-zA-Z0-9_]*(?:\[[^\]]*\])?(?:(?:\s*\.\s*[a-zA-Z_][a-zA-Z0-9_]*)(?:\[[^\]]*\])?)*)$" match = re.search(obj_expr_regex, stripped_text_before_dot) if match: obj_expr_str_candidate = match.group(1).replace(" ", "") else: obj_expr_str_candidate = stripped_text_before_dot if not obj_expr_str_candidate or \ not re.match(r"^[a-zA-Z_0-9\(\)\[\]\.\"\'\+\-\*\/ ]*$", obj_expr_str_candidate) or \ obj_expr_str_candidate.endswith(("+", "-", "*", "/", "(", ",")): self.logger.debug(f"Extracted expr '{obj_expr_str_candidate}' from '{stripped_text_before_dot}' not a valid object for dot autocomplete.") return obj_expr_str = obj_expr_str_candidate self.logger.debug(f"Autocomplete for object. Extracted: '{obj_expr_str}' from: '{text_on_line_up_to_dot}'") if not obj_expr_str: self.logger.debug("Object expression is empty after extraction. No autocomplete.") return # 3. Caso especial para el módulo sympy if obj_expr_str == "sympy": self.logger.debug(f"Detected 'sympy.', using SympyHelper for suggestions.") try: methods = SympyHelper.PopupFunctionList() if methods: self._show_autocomplete_popup(methods, is_global_popup=False) else: self.logger.debug(f"SympyHelper.PopupFunctionList returned no methods.") except Exception as e: self.logger.debug(f"Error calling SympyHelper.PopupFunctionList(): {e}") return # 4. Preprocesar con BracketParser if '[' in obj_expr_str: original_for_debug = obj_expr_str obj_expr_str = self.engine.parser._transform_brackets(obj_expr_str) if obj_expr_str != original_for_debug and self.debug: # Solo loguear si self.debug es True self.logger.debug(f"Preprocessed by BracketParser: '{original_for_debug}' -> '{obj_expr_str}'") # 5. Evaluar la expresión del objeto (usando contexto dinámico) eval_context = self.engine._get_full_context() obj = None try: if not obj_expr_str.strip(): self.logger.debug("Object expression became empty before eval. No action.") return self.logger.debug(f"Attempting to eval: '{obj_expr_str}'") obj = eval(obj_expr_str, eval_context) self.logger.debug(f"Eval successful. Object: {type(obj)}, Value: {obj}") except Exception as e: self.logger.debug(f"Error evaluating object expression '{obj_expr_str}': {e}") return # 6. Mostrar popup de autocompletado para el objeto if obj is not None and hasattr(obj, 'PopupFunctionList'): methods = obj.PopupFunctionList() if methods: self._show_autocomplete_popup(methods, is_global_popup=False) def _show_autocomplete_popup(self, suggestions, is_global_popup=False): """Muestra popup de autocompletado modeless con filtrado""" cursor_bbox = self.input_text.bbox(tk.INSERT) if not cursor_bbox: return # Guardar sugerencias originales y estado self._current_suggestions = suggestions.copy() self._is_global_popup = is_global_popup self._autocomplete_active = True x, y, _, height = cursor_bbox popup_x = self.input_text.winfo_rootx() + x popup_y = self.input_text.winfo_rooty() + y + height + 2 # Crear popup modeless self._autocomplete_popup = tk.Toplevel(self.root) self._autocomplete_popup.wm_overrideredirect(True) self._autocomplete_popup.wm_geometry(f"+{popup_x}+{popup_y}") self._autocomplete_popup.attributes('-topmost', True) # Crear listbox self._autocomplete_listbox = tk.Listbox( self._autocomplete_popup, bg="#3c3f41", fg="#bbbbbb", selectbackground="#007acc", selectforeground="white", borderwidth=1, relief="solid", exportselection=False, activestyle="none" ) # Llenar con sugerencias iniciales self._populate_listbox(suggestions) if suggestions: self._autocomplete_listbox.select_set(0) self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH) # Bindings solo para el listbox (no roba focus del input) self._autocomplete_listbox.bind("", lambda e: self._select_autocomplete()) # Binding para cerrar si se hace click fuera self.root.bind("", self._on_click_outside, add=True) # Calcular tamaño self._resize_popup() else: self._close_autocomplete_popup() def _populate_listbox(self, suggestions): """Llena el listbox con las sugerencias""" self._autocomplete_listbox.delete(0, tk.END) for name, hint in suggestions: self._autocomplete_listbox.insert(tk.END, f"{name} — {hint}") def _resize_popup(self): """Redimensiona el popup según el contenido""" if not self._autocomplete_listbox: return size = self._autocomplete_listbox.size() if size == 0: return # Calcular dimensiones max_len = 20 for i in range(size): item_text = self._autocomplete_listbox.get(i) max_len = max(max_len, len(item_text)) width = min(max_len + 5, 80) height = min(size, 10) self._autocomplete_listbox.config(width=width, height=height) def _filter_autocomplete(self): """Filtra las sugerencias basándose en el texto escrito después del punto""" if not self._autocomplete_active or not self._autocomplete_trigger_pos: return # Obtener texto escrito después del punto current_pos = self.input_text.index(tk.INSERT) try: filter_text = self.input_text.get(self._autocomplete_trigger_pos, current_pos) self._autocomplete_filter_text = filter_text.lower() except tk.TclError: # Posición inválida, cerrar popup self._close_autocomplete_popup() return # Filtrar sugerencias filtered = [] for name, hint in self._current_suggestions: if name.lower().startswith(self._autocomplete_filter_text): filtered.append((name, hint)) if filtered: # Actualizar listbox con sugerencias filtradas self._populate_listbox(filtered) self._autocomplete_listbox.select_set(0) self._resize_popup() else: # No hay coincidencias, cerrar popup self._close_autocomplete_popup() def _navigate_autocomplete_improved(self, direction): """Navegación mejorada en el popup de autocompletado""" if not self._autocomplete_listbox: return current_selection = self._autocomplete_listbox.curselection() size = self._autocomplete_listbox.size() if size == 0: return if not current_selection: new_idx = 0 if direction == 1 else size - 1 else: idx = current_selection[0] new_idx = (idx + direction) % size # Navegación circular # Actualizar selección if current_selection: self._autocomplete_listbox.select_clear(current_selection[0]) self._autocomplete_listbox.select_set(new_idx) self._autocomplete_listbox.activate(new_idx) self._autocomplete_listbox.see(new_idx) def _select_autocomplete(self): """Selecciona el item actual del autocompletado""" if not self._autocomplete_listbox: return selection = self._autocomplete_listbox.curselection() if not selection: return # Obtener texto seleccionado selected_text = self._autocomplete_listbox.get(selection[0]) # Determinar si es popup de variables o funciones is_variable_popup = self._variable_popup_active if is_variable_popup: # Para popup de variables, usar el método específico self._select_variable() return # Para popup de funciones, extraer nombre item_name = selected_text.split(" —")[0].strip() is_variable = " = " in selected_text # Nuevo formato de variables # Insertar en la posición correcta if hasattr(self, '_is_global_popup') and self._is_global_popup: # Para popup global, reemplazar el punto con la función cursor_pos_str = self.input_text.index(tk.INSERT) line_num, char_num = map(int, cursor_pos_str.split('.')) dot_pos_on_line = char_num - len(self._autocomplete_filter_text) - 1 dot_index_str = f"{line_num}.{dot_pos_on_line}" # Eliminar punto y texto filtrado end_pos = f"{line_num}.{char_num}" self.input_text.delete(dot_index_str, end_pos) # Insertar función (no variables en popup global) insert_text = item_name + "()" self.input_text.insert(dot_index_str, insert_text) self.input_text.mark_set(tk.INSERT, f"{dot_index_str}+{len(item_name)+1}c") else: # Para popup de objeto/variables current_pos = self.input_text.index(tk.INSERT) # Eliminar texto filtrado si existe if self._autocomplete_filter_text: start_pos = f"{current_pos}-{len(self._autocomplete_filter_text)}c" self.input_text.delete(start_pos, current_pos) current_pos = start_pos # Insertar según el tipo if is_variable: # Solo insertar el nombre de la variable insert_text = item_name self.input_text.insert(current_pos, insert_text) self.input_text.mark_set(tk.INSERT, f"{current_pos}+{len(item_name)}c") else: # Insertar método con paréntesis insert_text = item_name + "()" self.input_text.insert(current_pos, insert_text) self.input_text.mark_set(tk.INSERT, f"{current_pos}+{len(item_name)+1}c") # Cerrar popup y enfocar input self._close_autocomplete_popup() self.input_text.focus_set() self.on_key_release() def _select_variable(self): """Selecciona una variable del popup de variables""" if not self._autocomplete_listbox: return selection = self._autocomplete_listbox.curselection() if not selection: return # Obtener nombre de variable selected_text = self._autocomplete_listbox.get(selection[0]) var_name = selected_text.split(" = ")[0].strip() # Obtener posición de la palabra actual current_line = self.input_text.get("insert linestart", "insert lineend") cursor_pos = self.input_text.index(tk.INSERT) line_start = self.input_text.index("insert linestart") # Encontrar la palabra que estamos completando words = current_line.split() if words: last_word = words[-1] # Buscar posición de la última palabra word_start_pos = current_line.rfind(last_word) if word_start_pos >= 0: # Calcular posición absoluta abs_word_start = f"{line_start.split('.')[0]}.{word_start_pos}" abs_word_end = f"{line_start.split('.')[0]}.{word_start_pos + len(last_word)}" # Reemplazar la palabra parcial con la variable completa self.input_text.delete(abs_word_start, abs_word_end) self.input_text.insert(abs_word_start, var_name) self.input_text.mark_set(tk.INSERT, f"{abs_word_start}+{len(var_name)}c") # Cerrar popup self._close_autocomplete_popup() self.input_text.focus_set() def _on_click_outside_variable(self, event): """Maneja clicks fuera del popup de variables""" if self._autocomplete_popup and event.widget not in [ self._autocomplete_popup, self._autocomplete_listbox ]: self._close_autocomplete_popup() def _on_click_outside(self, event): """Maneja clicks fuera del popup""" if self._autocomplete_popup and event.widget not in [ self._autocomplete_popup, self._autocomplete_listbox ]: self._close_autocomplete_popup() def _navigate_autocomplete(self, event, direction): """Navegación en autocomplete (mantenido por compatibilidad)""" return self._navigate_autocomplete_improved(direction) def _close_autocomplete_popup(self): """Cierra popup de autocomplete y resetea estado""" if hasattr(self, '_autocomplete_popup') and self._autocomplete_popup: try: self._autocomplete_popup.destroy() except tk.TclError: pass # Ya fue destruido self._autocomplete_popup = None if hasattr(self, '_autocomplete_listbox') and self._autocomplete_listbox: self._autocomplete_listbox = None # Resetear estado del autocompletado self._autocomplete_active = False self._variable_popup_active = False self._autocomplete_trigger_pos = None self._autocomplete_filter_text = "" self._current_suggestions = [] # Remover bindings temporales try: self.root.unbind("") except tk.TclError: pass def _evaluate_and_update(self): """Evalúa todas las líneas y actualiza la salida""" try: input_content = self.input_text.get("1.0", tk.END) if not input_content.strip(): self._clear_output() return # MODIFICADO: Limpiar contexto completo para evitar conflictos entre evaluaciones # Las variables y ecuaciones se limpian para una evaluación fresca self.engine.equations.clear() # Limpiar ecuaciones self.engine.symbol_table.clear() # Limpiar variables asignadas self.engine.variables.clear() # Limpiar registro de variables conocidas self.logger.debug("Contexto del motor limpiado completamente antes de evaluación") # ⭐ NUEVO: Limpiar panel LaTeX antes de nueva evaluación if hasattr(self, '_latex_equations'): self._latex_equations.clear() self.logger.debug("Panel LaTeX limpiado antes de nueva evaluación") lines = input_content.splitlines() self._evaluate_lines(lines) except Exception as e: self._show_error(f"Error durante evaluación: {e}") def _evaluate_lines(self, lines: List[str]): """Evalúa múltiples líneas de código""" output_data = [] for line_num, line in enumerate(lines, 1): line = line.strip() # Líneas vacías o comentarios if not line or line.startswith('#'): if line: output_data.append([("comment", line)]) else: output_data.append([("", "")]) continue # Evaluar línea result = self.engine.evaluate_line(line) line_output = self._process_evaluation_result(result) output_data.append(line_output) self._display_output(output_data) def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]: """Procesa el resultado de evaluación para display, priorizando resultados interactivos.""" output_parts = [] indicator_text: Optional[str] = None # NUEVO: Agregar al panel LaTeX según el tipo de resultado self._add_to_latex_panel_if_applicable(result) if result.result_type == "comment": output_parts.append(("comment", result.output if result.output is not None else "")) return output_parts if not result.success: # Manejo de errores error_prefix = "Error: " main_error_message = f"{error_prefix}{result.error_message}" # Intentar obtener ayuda contextual para el error ayuda_text = self.obtener_ayuda(result.input_line) # obtener_ayuda devuelve string o None if ayuda_text: ayuda_linea = ayuda_text.replace("\n", " ").replace("\r", " ").strip() if len(ayuda_linea) > 120: # Acortar si es muy larga ayuda_linea = ayuda_linea[:117] + "..." # Mostrar primero el error principal, luego la sugerencia output_parts.append(("error", main_error_message)) output_parts.append( ("\n", "\n") ) # Separador para la ayuda output_parts.append(("helper", f"Sugerencia: {ayuda_linea}")) else: output_parts.append(("error", main_error_message)) # No se añade type_indicator para errores aquí, el mensaje de error es suficiente. else: # RESULTADO EXITOSO: # 1. Intentar crear un tag interactivo interactive_tag_info = self.interactive_manager.create_interactive_tag( result.actual_result_object, self.output_text ) if interactive_tag_info: tag_name, display_text = interactive_tag_info output_parts.append((tag_name, display_text)) # Añadir también el indicador de tipo algebraico if result.algebraic_type: indicator_text = f"[{result.algebraic_type}]" output_parts.append((" ", " ")) output_parts.append(("type_indicator", indicator_text)) else: # 2. Si no es interactivo, usar la lógica de formato de texto anterior main_output_tag = "base" if result.is_assignment: main_output_tag = "assignment" indicator_text = "[=]" elif result.is_equation: main_output_tag = "equation" indicator_text = "[eq]" elif result.result_type == "plot": main_output_tag = "plot" # Este caso es un fallback si create_interactive_tag no lo manejó else: # Lógica para determinar el tag principal y el texto del indicador if result.algebraic_type: type_lower = result.algebraic_type.lower() if type_lower in self.output_text.tag_names(): main_output_tag = type_lower elif isinstance(result.actual_result_object, sympy.Basic): main_output_tag = "symbolic" elif type_lower in ["int", "float", "complex"] or isinstance(result.actual_result_object, (int, float)): main_output_tag = "numeric" elif type_lower == "bool" or isinstance(result.actual_result_object, bool): main_output_tag = "boolean" elif type_lower == "str" or isinstance(result.actual_result_object, str): main_output_tag = "string" elif result.actual_result_object is not None and \ not isinstance(result.actual_result_object, (sympy.Basic, int, float, bool, str, list, dict, tuple, type(None))): main_output_tag = "custom_type" else: main_output_tag = "symbolic" else: main_output_tag = "symbolic" if result.algebraic_type: is_collection = any(kw in result.algebraic_type.lower() for kw in ["matrix", "list", "dict", "tuple", "vector", "array"]) is_custom_obj_tag = (main_output_tag == "custom_type") is_non_trivial_sympy = isinstance(result.actual_result_object, sympy.Basic) and \ result.algebraic_type not in ["Symbol", "Expr", "Integer", "Float", "Rational", "BooleanTrue", "BooleanFalse"] if is_collection or is_custom_obj_tag or is_non_trivial_sympy: indicator_text = f"[{result.algebraic_type}]" output_parts.append((main_output_tag, result.output if result.output is not None else "")) if indicator_text: output_parts.append((" ", " ")) output_parts.append(("type_indicator", indicator_text)) return output_parts def _add_to_latex_panel_if_applicable(self, result: EvaluationResult): """Agrega resultado al panel LaTeX si es aplicable (ecuación, asignación o comentario)""" try: # DEBUG: Log información del resultado self.logger.debug(f"Procesando para LaTeX - Tipo: {result.result_type}, Éxito: {result.success}") self.logger.debug(f" - is_assignment: {result.is_assignment}") self.logger.debug(f" - is_equation: {result.is_equation}") self.logger.debug(f" - output: {result.output[:100]}...") # Determinar si debe ir al panel LaTeX - REGLAS SIMPLIFICADAS should_add_to_latex = False equation_type = "comment" # Tipo por defecto # 1. SIEMPRE agregar comentarios if result.result_type == "comment": should_add_to_latex = True equation_type = "comment" self.logger.debug(" -> Detectado como COMMENT") # 2. SIEMPRE agregar asignaciones elif result.is_assignment: should_add_to_latex = True equation_type = "assignment" self.logger.debug(" -> Detectado como ASSIGNMENT") # 3. SIEMPRE agregar ecuaciones elif result.is_equation: should_add_to_latex = True equation_type = "equation" self.logger.debug(" -> Detectado como EQUATION") # 4. Agregar CUALQUIER resultado exitoso que tenga contenido interesante elif result.success and result.output: # Si tiene símbolos matemáticos o contenido algebraico, agregarlo math_indicators = ['=', '+', '-', '*', '/', '^', 'sqrt', 'sin', 'cos', 'tan', 'log', 'exp'] if any(indicator in result.output for indicator in math_indicators): should_add_to_latex = True equation_type = "symbolic" self.logger.debug(" -> Detectado como SYMBOLIC (contiene matemáticas)") # También agregar si es claramente una expresión matemática elif (result.actual_result_object is not None and hasattr(result.actual_result_object, '__class__')): try: import sympy if isinstance(result.actual_result_object, sympy.Basic): should_add_to_latex = True equation_type = "symbolic" self.logger.debug(" -> Detectado como SYMBOLIC (objeto SymPy)") except ImportError: pass if should_add_to_latex: # Preparar contenido LaTeX latex_content = "" if result.actual_result_object is not None: try: # Intentar convertir a LaTeX usando SymPy import sympy latex_content = sympy.latex(result.actual_result_object) self.logger.debug(f" -> LaTeX de SymPy: {latex_content[:100]}...") except Exception as e: # Fallback al output de texto latex_content = result.output if result.output else str(result.actual_result_object) self.logger.debug(f" -> Fallback a texto: {latex_content[:100]}...") else: latex_content = result.output if result.output else "" self.logger.debug(f" -> Usando output directo: {latex_content[:100]}...") # ANTES de añadir al panel, verificar si está inicializado if not hasattr(self, '_latex_equations'): self._latex_equations = [] self.logger.debug(" -> ⚠️ Lista de ecuaciones no existía, creándola") total_antes = len(self._latex_equations) # Usar la función _add_to_latex_panel self._add_to_latex_panel(equation_type, latex_content) total_despues = len(self._latex_equations) self.logger.debug(f"✅ AÑADIDO al panel LaTeX: {equation_type} -> {latex_content[:50]}... (Total: {total_antes} -> {total_despues})") else: self.logger.debug(" -> NO añadido al panel LaTeX") except Exception as e: self.logger.error(f"Error procesando resultado para panel LaTeX: {e}") import traceback self.logger.error(traceback.format_exc()) def _get_result_tag_dynamic(self, result: Any) -> str: """Determina el tag de color para un resultado - SIMPLIFICADO""" # Determinar tag basado en tipo if hasattr(result, '__class__'): class_name = result.__class__.__name__.lower() if 'hex' in class_name: return "hex" elif 'bin' in class_name: return "bin" elif 'ip' in class_name: return "ip" elif 'chr' in class_name: return "chr_type" elif 'date' in class_name: return "date" # Fallback a tags existentes try: import sympy if isinstance(result, sympy.Basic): return "symbolic" except: pass return "result" def _get_class_display_name_dynamic(self, obj: Any) -> str: """Obtiene nombre de clase para display - SIMPLIFICADO""" try: import sympy if isinstance(obj, sympy.Basic): return "Sympy" except: pass if isinstance(obj, (int, float, str, list, dict, tuple, bool, type(None))): class_display_name = type(obj).__name__.capitalize() if class_display_name == "Nonetype": class_display_name = "None" return class_display_name return type(obj).__name__ def _display_output(self, output_data: List[List[tuple]]): """Muestra los datos de salida en el widget (sin cambios)""" self.output_text.config(state="normal") self.output_text.delete("1.0", tk.END) for line_idx, line_parts in enumerate(output_data): if not line_parts or (len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == ""): pass else: for part_idx, (tag, content) in enumerate(line_parts): if not content: continue if part_idx > 0: prev_tag, prev_content = line_parts[part_idx-1] if part_idx > 0 else (None, None) if tag not in ["class_hint", "numeric", "info"] and prev_content: self.output_text.insert(tk.END, " ; ") elif tag in ["numeric", "info"] and prev_content: self.output_text.insert(tk.END, " ") if content: self.output_text.insert(tk.END, str(content), tag) if line_idx < len(output_data) - 1: self.output_text.insert(tk.END, "\n") self.output_text.config(state="disabled") def _clear_output(self): """Limpia el panel de salida""" self.output_text.config(state="normal") self.output_text.delete("1.0", tk.END) self.output_text.config(state="disabled") def _show_error(self, error_msg: str): """Muestra un error en el panel de salida""" self.output_text.config(state="normal") self.output_text.delete("1.0", tk.END) self.output_text.insert("1.0", error_msg, "error") self.output_text.config(state="disabled") def _show_context_menu(self, event, panel_type: str): """Muestra menú contextual""" context_menu = Menu( self.root, tearoff=0, bg="#3c3c3c", fg="white", activebackground="#007acc", activeforeground="white", relief=tk.FLAT, bd=1, ) if panel_type == "input": context_menu.add_command(label="Cortar", command=lambda: self.input_text.event_generate("<>")) context_menu.add_command(label="Copiar", command=lambda: self.input_text.event_generate("<>")) context_menu.add_command(label="Pegar", command=lambda: self.input_text.event_generate("<>")) context_menu.add_separator() context_menu.add_command(label="Limpiar entrada", command=self.clear_input) elif panel_type == "output": context_menu.add_command(label="Copiar todo", command=self.copy_output) context_menu.add_command(label="Limpiar salida", command=self.clear_output) context_menu.add_separator() context_menu.add_command(label="Ayuda", command=self.show_help_window) try: context_menu.tk_popup(event.x_root, event.y_root) finally: context_menu.grab_release() # ========== MÉTODOS DE MENÚ Y COMANDOS (la mayoría sin cambios) ========== def new_session(self): """Inicia una nueva sesión limpiando todo""" self.clear_input() self.clear_output() self._clear_latex_panel() if hasattr(self.engine, 'clear_context'): self.engine.clear_context() def load_file(self): """Carga archivo en el editor""" filepath = filedialog.askopenfilename( title="Cargar archivo", filetypes=[ ("Archivos de texto", "*.txt"), ("Archivos Python", "*.py"), ("Todos los archivos", "*.*") ] ) if filepath: try: with open(filepath, "r", encoding="utf-8") as f: content = f.read() self.input_text.delete("1.0", tk.END) self.input_text.insert("1.0", content) self._process_input_and_adjust_layout() except Exception as e: messagebox.showerror("Error", f"No se pudo cargar el archivo:\n{e}") def save_file(self): """Guarda contenido del editor""" filepath = filedialog.asksaveasfilename( title="Guardar archivo", defaultextension=".txt", filetypes=[ ("Archivos de texto", "*.txt"), ("Archivos Python", "*.py"), ("Todos los archivos", "*.*") ] ) if filepath: try: content = self.input_text.get("1.0", tk.END) with open(filepath, "w", encoding="utf-8") as f: f.write(content) messagebox.showinfo("Éxito", "Archivo guardado correctamente.") except Exception as e: messagebox.showerror("Error", f"No se pudo guardar el archivo:\n{e}") def clear_input(self): """Limpia panel de entrada""" self.input_text.delete("1.0", tk.END) self._clear_output() def clear_output(self): """Limpia el panel de salida y el panel LaTeX""" self._clear_output() self._clear_latex_panel() def clear_history(self): """Limpia el archivo de historial""" try: if os.path.exists(self.HISTORY_FILE): os.remove(self.HISTORY_FILE) messagebox.showinfo("Éxito", "Historial limpiado correctamente.") except Exception as e: messagebox.showerror("Error", f"No se pudo limpiar el historial:\n{e}") def copy_output(self): """Copia el contenido de la salida al portapapeles""" content = self.output_text.get("1.0", tk.END).strip() if content: self.root.clipboard_clear() self.root.clipboard_append(content) def show_types_syntax(self): """Muestra sintaxis de tipos disponibles - NUEVA FUNCIÓN""" try: types_info = self.engine.get_available_types() registered_classes = types_info.get('registered_classes', {}) syntax_text = "SINTAXIS DE TIPOS DISPONIBLES\n\n" if not registered_classes: syntax_text += "No hay tipos personalizados disponibles.\n" else: syntax_text += "Tipos personalizados detectados:\n\n" for name, cls in sorted(registered_classes.items()): syntax_text += f"=== {name} ===\n" # Sintaxis básica syntax_text += f"Sintaxis: {name}[valor]\n" syntax_text += f"Alias: {name.lower()}[valor]\n" # Obtener ayuda si está disponible if hasattr(cls, 'Helper'): try: help_text = cls.Helper(name) if help_text: syntax_text += f"Ayuda: {help_text}\n" except: pass # Obtener métodos si está disponible if hasattr(cls, 'PopupFunctionList'): try: methods = cls.PopupFunctionList() if methods: syntax_text += "Métodos disponibles:\n" for method_name, method_desc in methods: syntax_text += f" • {method_name}() - {method_desc}\n" except: pass syntax_text += "\n" self._show_help_window("Sintaxis de Tipos", syntax_text) except Exception as e: messagebox.showerror("Error", f"Error obteniendo sintaxis de tipos:\n{e}") def show_quick_guide(self): """Muestra guía rápida - ACTUALIZADA""" guide = """# Calculadora MAV - CAS Híbrido ## Sistema de Tipos Dinámico El sistema detecta automáticamente tipos disponibles en custom_types/ ## Sintaxis Nueva con Corchetes - Sintaxis: Tipo[valor] en lugar de Tipo("valor") - Ejemplos: Hex[FF], Bin[1010], Dec[10.5], Chr[A] - Use menú Tipos → Información de tipos para ver tipos disponibles ## Ecuaciones Automáticas - x**2 + 2*x = 8 (detectado automáticamente) - a + b = 10 (agregado al sistema) - variable=? (atajo para solve(variable)) ## Funciones SymPy Disponibles - solve(), diff(), integrate(), limit(), series() - sin(), cos(), tan(), exp(), log(), sqrt() - Matrix(), plot(), plot3d() ## Resultados Interactivos - 📊 Ver Plot (click para ventana matplotlib) - 📋 Ver Matriz (click para vista expandida) - 📋 Ver Lista (click para contenido completo) ## Variables Automáticas - Todas las variables son símbolos SymPy - x = 5 crea Symbol('x') con valor 5 - Evaluación simbólica + numérica automática ## Autocompletado Dinámico - Escriba "." después de cualquier objeto para ver métodos - El sistema usa los tipos registrados automáticamente """ self._show_help_window("Guía Rápida", guide) def show_syntax_help(self): """Muestra ayuda de sintaxis - ACTUALIZADA""" syntax = """# Sintaxis del CAS Híbrido ## Sistema de Tipos Dinámico Los tipos se detectan automáticamente desde custom_types/ Use menú Tipos → Información de tipos para ver tipos disponibles ## Sintaxis con Corchetes (Dinámica) Tipo[valor] # Sintaxis general Tipo[arg1; arg2] # Múltiples argumentos ## Métodos Disponibles (Dinámicos) Tipo[...].método() # Métodos específicos del tipo objeto.método[] # Método sin argumentos ## Ecuaciones (detección automática) expresión = expresión # Ecuación simple expresión == expresión # Igualdad SymPy expresión > expresión # Desigualdad SymPy ## Resolver solve(ecuación, variable) variable=? # Atajo para solve(variable) ## Variables SymPy Puras x = valor # Crea Symbol('x') expresión # Evaluación simbólica automática """ self._show_help_window("Sintaxis", syntax) def show_sympy_functions(self): """Muestra funciones SymPy disponibles (sin cambios)""" functions = """# Funciones SymPy Disponibles ## Matemáticas Básicas sin(x), cos(x), tan(x) asin(x), acos(x), atan(x) sinh(x), cosh(x), tanh(x) exp(x), log(x), sqrt(x) abs(x), sign(x), factorial(x) ## Cálculo diff(expr, var) # Derivada integrate(expr, var) # Integral indefinida integrate(expr, (var, a, b)) # Integral definida limit(expr, var, punto) # Límite series(expr, var, punto, n) # Serie de Taylor ## Álgebra solve(ecuación, variable) simplify(expr), expand(expr) factor(expr), collect(expr, var) cancel(expr), apart(expr, var) ## Álgebra Lineal Matrix([[a, b], [c, d]]) det(matrix), inv(matrix) ## Plotting plot(expr, (var, inicio, fin)) plot3d(expr, (x, x1, x2), (y, y1, y2)) ## Constantes pi, E, I (imaginario), oo (infinito) """ self._show_help_window("Funciones SymPy", functions) def show_about(self): """Muestra información sobre la aplicación - ACTUALIZADA""" about = """Calculadora MAV - CAS Híbrido Versión: 2.1 (Sistema de Tipos Dinámico) Motor: SymPy + Auto-descubrimiento de Tipos Características: • Motor algebraico completo (SymPy) • Sistema de tipos dinámico y extensible • Sintaxis simplificada con corchetes • Detección automática de ecuaciones • Resultados interactivos clickeables • Auto-descubrimiento de tipos en custom_types/ • Variables SymPy puras • Plotting integrado • Autocompletado dinámico NUEVO: Sistema de Tipos Dinámico • Detección automática de nuevos tipos • Organización modular en custom_types/ • Registro automático sin modificar código • Escalabilidad mejorada Desarrollado para cálculo matemático avanzado con soporte especializado para redes, programación y análisis numérico. """ messagebox.showinfo("Acerca de", about) def _show_help_window(self, title: str, content: str): """Muestra ventana de ayuda""" window = tk.Toplevel(self.root) window.title(title) window.geometry("700x500") window.configure(bg="#2b2b2b") text_widget = scrolledtext.ScrolledText( window, font=("Consolas", 10), bg="#1e1e1e", fg="#d4d4d4", wrap=tk.WORD ) text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) text_widget.insert("1.0", content) text_widget.config(state="disabled") def load_history(self): """Carga historial de entrada y realiza evaluación inicial""" try: if os.path.exists(self.HISTORY_FILE): with open(self.HISTORY_FILE, "r", encoding="utf-8") as f: content = f.read() if content.strip(): self.input_text.insert("1.0", content) # Hacer evaluación inicial para mostrar resultados del historial # Esto mantiene el comportamiento de contexto limpio pero muestra resultados self.root.after_idle(self._process_input_and_adjust_layout) except Exception as e: self.logger.error(f"Error cargando historial: {e}", exc_info=True) def save_history(self): """Guarda historial de entrada""" try: content = self.input_text.get("1.0", tk.END).rstrip("\n") if content: with open(self.HISTORY_FILE, "w", encoding="utf-8") as f: f.write(content) elif os.path.exists(self.HISTORY_FILE): os.remove(self.HISTORY_FILE) except Exception as e: self.logger.error(f"Error guardando historial: {e}", exc_info=True) def on_close(self): """Maneja cierre de la aplicación de forma completa""" try: # Guardar historial y configuraciones self.save_history() self._save_settings() # Cerrar todas las ventanas interactivas if self.interactive_manager: self.interactive_manager.close_all_windows() # Detener cualquier job pendiente if self._debounce_job: self.root.after_cancel(self._debounce_job) # Cerrar autocompletado si está abierto self._close_autocomplete_popup() # Asegurar que matplotlib libere recursos try: import matplotlib.pyplot as plt plt.close('all') except: pass # Forzar la salida del mainloop self.root.quit() except Exception as e: self.logger.error(f"Error durante el cierre: {e}") finally: # Destruir la ventana principal como último recurso try: self.root.destroy() except: pass def show_help_window(self): """Muestra ventana de ayuda con archivo externo - NUEVO SISTEMA""" help_win = tk.Toplevel(self.root) help_win.title("Ayuda - Calculadora MAV CAS Híbrido") help_win.geometry("750x600") help_win.configure(bg="#1e1e1e") help_win.transient(self.root) readme_content = self._get_help_content() if MARKDOWN_AVAILABLE and HTML_VIEWER_TYPE: try: # CSS para un tema oscuro, consistente con la UI de la calculadora dark_theme_css = """ """ html_fragment = markdown.markdown( readme_content, extensions=["fenced_code", "codehilite", "tables", "nl2br", "admonition"], extension_configs={"codehilite": {"noclasses": True, "pygments_style": "monokai"}} ) # Construir un documento HTML completo content_for_viewer = f""" Ayuda de Calculadora {dark_theme_css} {html_fragment} """ if HTML_VIEWER_TYPE == "tkinterweb": html_viewer = tkinterweb.HtmlFrame(help_win, messages_enabled=False) html_viewer.load_html(content_for_viewer) elif HTML_VIEWER_TYPE == "tkhtmlview": html_viewer = HTMLScrolledText(help_win) html_viewer.configure(bg="#1e1e1e") html_viewer.set_html(content_for_viewer) html_viewer.pack(padx=0, pady=0, fill=tk.BOTH, expand=True) except Exception as e: self.logger.error(f"Error al renderizar Markdown a HTML: {e}", exc_info=True) # Fallback to text if HTML fails self._show_text_help(help_win, readme_content) else: self._show_text_help(help_win, readme_content) # Botón de cerrar close_button = tk.Button( help_win, text="Cerrar", command=help_win.destroy, bg="#3c3c3c", fg="white", relief=tk.FLAT, padx=10, ) close_button.pack(pady=(5, 10)) def _get_help_content(self): """Obtiene el contenido de ayuda desde archivo externo o genera uno por defecto""" try: if os.path.exists(self.HELP_FILE): with open(self.HELP_FILE, "r", encoding="utf-8") as f: return f.read() except IOError: pass # Contenido por defecto si no se encuentra el archivo return """# Calculadora MAV - CAS Híbrido ## Sistema de Tipos Dinámico El sistema detecta automáticamente tipos disponibles en `custom_types/` ## Sintaxis Nueva con Corchetes - **Sintaxis**: `Tipo[valor]` en lugar de `Tipo("valor")` - **Ejemplos**: `Hex[FF]`, `Bin[1010]`, `Dec[10.5]`, `Chr[A]` - Use menú **Tipos → Información de tipos** para ver tipos disponibles ## Ecuaciones Automáticas - `x**2 + 2*x = 8` (detectado automáticamente) - `a + b = 10` (agregado al sistema) - `variable=?` (atajo para solve(variable)) ## Funciones SymPy Disponibles - `solve()`, `diff()`, `integrate()`, `limit()`, `series()` - `sin()`, `cos()`, `tan()`, `exp()`, `log()`, `sqrt()` - `Matrix()`, `plot()`, `plot3d()` ## Resultados Interactivos - 📊 **Ver Plot** (click para ventana matplotlib) - 📋 **Ver Matriz** (click para vista expandida) - 📋 **Ver Lista** (click para contenido completo) ## Variables Automáticas - Todas las variables son símbolos SymPy - `x = 5` crea Symbol('x') con valor 5 - Evaluación simbólica + numérica automática ## Autocompletado Dinámico - Escriba "." después de cualquier objeto para ver métodos - El sistema usa los tipos registrados automáticamente ## Menú Contextual (clic derecho) - **En entrada**: Cortar, Copiar, Pegar, Limpiar entrada, Ayuda - **En salida**: Copiar todo, Limpiar salida, Ayuda ## Desarrollo Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el directorio raíz de la aplicación. """ def _show_text_help(self, help_win, content): """Muestra la ayuda en texto plano cuando markdown no está disponible""" text_widget = scrolledtext.ScrolledText( help_win, font=("Consolas", 10), bg="#1e1e1e", fg="#d4d4d4", wrap=tk.WORD, borderwidth=0, highlightthickness=0 ) text_widget.insert("1.0", content) text_widget.config(state="disabled") text_widget.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) def obtener_ayuda(self, input_str): """Obtiene ayuda usando helpers dinámicos""" for helper in self.HELPERS: try: ayuda = helper(input_str) if ayuda: return ayuda except Exception as e: self.logger.debug(f"Error en helper: {e}") continue return None def _get_input_font(self): """Obtiene o crea y cachea el objeto tk.Font para el panel de entrada.""" if not self._cached_input_font: # Asume la fuente configurada en create_widgets: ("Consolas", 11) self._cached_input_font = tkFont.Font(family="Consolas", size=11) return self._cached_input_font def _adjust_input_pane_width(self): """Ajusta el ancho del panel de entrada según su contenido, limitado a ~50 caracteres.""" if not hasattr(self, 'paned_window') or not self.paned_window.winfo_exists(): return # Esperar a que la ventana tenga un tamaño válido if self.paned_window.winfo_width() <= 1: return # Se reintentará en la siguiente llamada (ej. por KeyRelease) # Obtener contenido excluyendo el último newline automático del widget Text input_content = self.input_text.get("1.0", f"{tk.END}-1c") lines = input_content.splitlines() input_font = self._get_input_font() # NUEVO: Límite máximo de ~50 caracteres max_chars_limit = 50 max_char_width = input_font.measure("M") # Usar 'M' como referencia (carácter más ancho) max_allowed_width = max_char_width * max_chars_limit max_pixel_width = 0 if not input_content.strip(): # Si está vacío o solo espacios en blanco max_pixel_width = 5 # Ancho mínimo para el cursor o como placeholder else: for line in lines: measured_width = input_font.measure(line) if line.strip() else input_font.measure(" ") # NUEVO: Aplicar límite de caracteres measured_width = min(measured_width, max_allowed_width) if measured_width > max_pixel_width: max_pixel_width = measured_width padding = 40 # Relleno para barra de desplazamiento, márgenes, etc. width_needed_by_text = max_pixel_width + padding # NUEVO: Aplicar límite absoluto max_absolute_width = max_allowed_width + padding width_needed_by_text = min(width_needed_by_text, max_absolute_width) # Debugging opcional (descomenta si necesitas depurar) if self.debug: self.logger.debug(f"--- Adjusting Input Pane (Limited to ~{max_chars_limit} chars) ---") self.logger.debug(f"Input content: '{input_content[:50]}...'") self.logger.debug(f"Max pixel width of text: {max_pixel_width}") self.logger.debug(f"Width needed by text (limited): {width_needed_by_text}") min_input_pane_width = 200 # Definido en create_widgets min_output_pane_width = 200 # Definido en create_widgets total_width = self.paned_window.winfo_width() # NUEVO: Consideración del panel LaTeX expandible # Solo reservar espacio si está visible total_available_width = total_width if hasattr(self, 'latex_panel_visible') and self.latex_panel_visible: # Si el panel LaTeX está visible, reservar espacio para él latex_width = getattr(self, '_min_latex_pane_width', 300) total_available_width = total_width # El PanedWindow se ajusta automáticamente current_sash_pos = 0 try: sash_coords = self.paned_window.sash_coord(0) if sash_coords: current_sash_pos = sash_coords[0] else: if self.debug: self.logger.debug("Could not get sash_coord.") return except tk.TclError: if self.debug: self.logger.debug("TclError getting sash_coord.") return if self.debug: self.logger.debug(f"Current sash position (input pane width): {current_sash_pos}") if width_needed_by_text > current_sash_pos: if self.debug: self.logger.debug(f"Condition MET: Text needs more space ({width_needed_by_text} > {current_sash_pos})") new_input_width = width_needed_by_text # Punto de partida # Asegurar que el nuevo ancho no sea menor que el mínimo del panel de entrada new_input_width = max(new_input_width, min_input_pane_width) # Asegurar que el panel de salida conserve su espacio mínimo if total_available_width - new_input_width < min_output_pane_width: new_input_width = total_available_width - min_output_pane_width new_input_width = max(new_input_width, min_input_pane_width) # Re-verificar mínimo del input # MODIFICADO: Ratio máximo conservador para el input max_input_ratio = 0.5 # 50% máximo para el input, dejando espacio para output y LaTeX max_width_by_ratio = int(total_available_width * max_input_ratio) if new_input_width > max_width_by_ratio: if max_width_by_ratio >= min_input_pane_width and \ (total_available_width - max_width_by_ratio) >= min_output_pane_width: new_input_width = max_width_by_ratio final_new_input_width = max(0, int(new_input_width)) # No debe ser negativo if self.debug: self.logger.debug(f"Calculated final new input width: {final_new_input_width}") # Mover el sash solo si el nuevo ancho es significativamente mayor que el actual (umbral ajustado) sash_adjustment_threshold = 3 # Píxeles if final_new_input_width > current_sash_pos and \ (final_new_input_width - current_sash_pos) >= sash_adjustment_threshold: if self.debug: self.logger.debug(f"Condition MET for sash_place: New width {final_new_input_width} is significantly larger (diff >= {sash_adjustment_threshold}).") try: if self.paned_window.winfo_exists() and total_available_width >= (min_input_pane_width + min_output_pane_width): self.paned_window.sash_place(0, final_new_input_width, 0) # Añadido el argumento y=0 if self.debug: self.logger.debug(f"Sash placed at: {final_new_input_width}") elif self.debug: self.logger.debug("Paned window not ready or total width too small for sash_place.") except tk.TclError as e_sash: if self.debug: self.logger.debug(f"TclError during sash_place: {e_sash}") pass elif self.debug: self.logger.debug(f"Condition NOT MET for sash_place: final_new_input_width ({final_new_input_width}) vs current_sash_pos ({current_sash_pos}) or threshold ({sash_adjustment_threshold}).") elif self.debug: self.logger.debug(f"Condition NOT MET: Text does not need more space ({width_needed_by_text} <= {current_sash_pos})") if self.debug: self.logger.debug(f"--- End Adjusting Input Pane ---") def _process_input_and_adjust_layout(self): """Evalúa todas las líneas y luego ajusta el ancho del panel de entrada.""" self._evaluate_and_update() self._adjust_input_pane_width() def _update_input_expression(self, original_expression, new_expression): """Actualiza el panel de entrada reemplazando la expresión original con la nueva""" try: # Obtener todo el contenido actual current_content = self.input_text.get("1.0", tk.END).rstrip('\n') # Buscar y reemplazar la expresión original if original_expression in current_content: updated_content = current_content.replace(original_expression, new_expression, 1) # Actualizar el panel de entrada self.input_text.delete("1.0", tk.END) self.input_text.insert("1.0", updated_content) # Evaluar automáticamente la nueva expresión self._evaluate_and_update() self.logger.info(f"Expresión actualizada: '{original_expression}' -> '{new_expression}'") else: # Si no se encuentra la expresión original, agregar la nueva al final if current_content and not current_content.endswith('\n'): current_content += '\n' updated_content = current_content + new_expression self.input_text.delete("1.0", tk.END) self.input_text.insert("1.0", updated_content) self.logger.info(f"Expresión agregada: '{new_expression}'") except Exception as e: self.logger.error(f"Error actualizando expresión: {e}") # Fallback: simplemente insertar la nueva expresión self.input_text.insert(tk.END, f"\n{new_expression}") def _add_equation_to_latex_panel(self, equation_type: str, content: str, sympy_obj=None): """Agrega una ecuación al panel LaTeX expandible""" if not hasattr(self, 'latex_panel'): return try: # Inicializar lista si no existe if not hasattr(self, '_latex_equations'): self._latex_equations = [] # Convertir objeto SymPy a LaTeX si es posible latex_content = self._sympy_to_latex(sympy_obj) if sympy_obj else content # Agregar a la lista de ecuaciones equation_data = { 'type': equation_type, 'content': latex_content, 'original': content } self._latex_equations.append(equation_data) # Actualizar indicador visual self._update_latex_indicator() # Si el panel está visible, actualizar contenido inmediatamente if hasattr(self, 'latex_panel_visible') and self.latex_panel_visible: if self._webview_available: # Actualizar según el tipo de webview if self._webview_type == "tkinterweb": self._update_tkinterweb() elif self._webview_type == "pywebview": self._update_pywebview() else: # Fallback: usar el widget de texto if hasattr(self, 'latex_fallback_text'): self.latex_fallback_text.config(state="normal") # Agregar tipo y contenido type_text = f"\n[{equation_type.upper()}] " self.latex_fallback_text.insert(tk.END, type_text) self.latex_fallback_text.insert(tk.END, f"{content}\n") self.latex_fallback_text.config(state="disabled") # Auto-scroll al final self.latex_fallback_text.see(tk.END) except Exception as e: self.logger.debug(f"Error agregando ecuación al panel LaTeX: {e}") def _add_spacer_to_latex_panel(self): """Agrega un espaciador al panel LaTeX para correlación vertical""" if not hasattr(self, 'latex_panel'): return try: # Inicializar lista si no existe if not hasattr(self, '_latex_equations'): self._latex_equations = [] # Agregar espaciador a la lista spacer_data = { 'type': 'spacer', 'content': '', 'original': '' } self._latex_equations.append(spacer_data) # Si el panel está visible, actualizar contenido if hasattr(self, 'latex_panel_visible') and self.latex_panel_visible: if self._webview_available: # Actualizar según el tipo if self._webview_type == "tkinterweb": self._update_tkinterweb() elif self._webview_type == "pywebview": self._update_pywebview() elif hasattr(self, 'latex_fallback_text'): # Agregar línea en blanco en el fallback self.latex_fallback_text.config(state="normal") self.latex_fallback_text.insert(tk.END, "\n") self.latex_fallback_text.config(state="disabled") except Exception as e: self.logger.debug(f"Error agregando espaciador: {e}") def _clear_latex_panel(self): """Limpia el panel LaTeX""" if not hasattr(self, 'latex_panel'): return try: # Limpiar lista de ecuaciones self._latex_equations = [] # Actualizar indicador self._update_latex_indicator() # Si el panel está visible, limpiar contenido if hasattr(self, 'latex_panel_visible') and self.latex_panel_visible: if self._webview_available: if self._webview_type == "tkinterweb" and hasattr(self, 'latex_webview'): # Recargar HTML base base_html = self._generate_base_html() self.latex_webview.load_html(base_html) elif self._webview_type == "pywebview": # Para pywebview, reinicializar si es necesario pass elif hasattr(self, 'latex_fallback_text'): # Limpiar el widget de texto self.latex_fallback_text.config(state="normal") self.latex_fallback_text.delete("1.0", tk.END) self.latex_fallback_text.insert("1.0", "Panel de Ecuaciones LaTeX\n\n" "Las ecuaciones aparecerán aquí...\n") self.latex_fallback_text.config(state="disabled") except Exception as e: self.logger.debug(f"Error limpiando panel LaTeX: {e}") def _sympy_to_latex(self, sympy_obj) -> str: """Convierte un objeto SymPy a LaTeX""" try: if sympy_obj is None: return "" # Usar la función latex de SymPy from sympy import latex latex_str = latex(sympy_obj) # Envolver en delimitadores para display math return f"$${latex_str}$$" except Exception as e: self.logger.debug(f"Error convirtiendo a LaTeX: {e}") return str(sympy_obj) def _update_latex_panel(self): """Actualiza el panel LaTeX con soporte para pywebview y tkinterweb""" if not self.latex_panel_visible or not hasattr(self, '_latex_equations'): return try: self.logger.debug(f"🔄 Actualizando panel LaTeX con {len(self._latex_equations)} ecuaciones (Tipo: {self._webview_type})") # Generar HTML con ecuaciones según el tipo de webview if self._latex_equations: if self._webview_type == "pywebview": html_content = self._generate_html_pywebview_with_equations() elif self._js_available: html_content = self._generate_html_with_mathjax() else: html_content = self._generate_html_fallback_with_equations() else: # Sin ecuaciones, usar HTML base html_content = self._generate_base_html() # Manejar según el tipo de webview if self._webview_type == "pywebview": self._update_pywebview_panel(html_content) elif self._webview_type == "tkinterweb": self._update_tkinterweb_panel(html_content) else: self.logger.warning("⚠️ Tipo de webview no reconocido") except Exception as e: self.logger.error(f"❌ Error actualizando panel LaTeX: {e}") import traceback self.logger.error(traceback.format_exc()) def _update_pywebview_panel(self, html_content): """Actualiza el panel usando pywebview (crea nueva ventana cada vez)""" try: self.logger.debug(f"📤 Actualizando con pywebview: {len(html_content)} caracteres") # Guardar HTML para debugging self._save_html_debug_copy(html_content) # Con subprocess, siempre creamos una nueva ventana # Esto es más simple y evita problemas de sincronización self._create_pywebview_window(html_content) except Exception as e: self.logger.error(f"❌ Error con pywebview: {e}") def _create_pywebview_window(self, html_content): """Crea una ventana pywebview usando subprocess para evitar conflictos de hilo""" try: import subprocess import tempfile import os # Crear archivo temporal con el HTML with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f: f.write(html_content) html_file_path = f.name # Script Python para ejecutar pywebview en proceso separado pywebview_script = f''' import webview import os try: # Leer HTML desde archivo temporal with open(r"{html_file_path}", "r", encoding="utf-8") as f: html_content = f.read() # Crear ventana pywebview window = webview.create_window( "Ecuaciones LaTeX - MAV Calculator", html=html_content, width=350, height=450, min_size=(300, 350), resizable=True, shadow=True, on_top=False, transparent=False ) print("🌐 Ventana pywebview creada exitosamente") # Iniciar webview webview.start(debug=False) except Exception as e: print(f"❌ Error en pywebview: {{e}}") finally: # Limpiar archivo temporal try: os.unlink(r"{html_file_path}") except: pass ''' # Ejecutar en proceso separado subprocess.Popen([ 'python', '-c', pywebview_script ], creationflags=subprocess.CREATE_NEW_CONSOLE if os.name == 'nt' else 0) self.logger.debug("🚀 Proceso pywebview iniciado exitosamente") except Exception as e: self.logger.error(f"❌ Error creando ventana pywebview: {e}") def _update_tkinterweb_panel(self, html_content): """Actualiza el panel usando tkinterweb (fallback)""" try: self.logger.debug(f"📤 Cargando HTML en tkinterweb: {len(html_content)} caracteres") self.logger.debug(f"📝 Muestra del HTML: {html_content[html_content.find('equations-container'):html_content.find('equations-container')+200]}...") if hasattr(self, 'latex_webview') and self.latex_webview: self.latex_webview.load_html(html_content) self.logger.debug("✅ HTML cargado en tkinterweb") else: self.logger.warning("⚠️ latex_webview no disponible en tkinterweb") # Guardar HTML para debugging self._save_html_debug_copy(html_content) self.logger.debug("✅ Panel tkinterweb actualizado") except Exception as e: self.logger.error(f"❌ Error actualizando tkinterweb: {e}") def _generate_html_with_mathjax(self): """Genera HTML con MathJax cuando JavaScript está disponible""" self.logger.debug(f"🔧 Generando HTML con {len(self._latex_equations)} ecuaciones") html_blocks = [] for i, eq_data in enumerate(self._latex_equations): eq_type = eq_data.get('type', 'symbolic') latex_content = eq_data.get('latex', '') original_content = eq_data.get('original', '') self.logger.debug(f" Ecuación {i}: tipo={eq_type}, latex='{latex_content[:50]}...', original='{original_content[:50]}...'") # Preparar LaTeX para MathJax if latex_content: # Usar $$ para display math if not latex_content.startswith('$'): formatted_latex = f"$${latex_content}$$" else: formatted_latex = latex_content # Crear bloque HTML optimizado block_html = f"""
{formatted_latex}
""" else: # Sin LaTeX válido, mostrar texto original block_html = f"""
{original_content or 'Sin contenido'}
""" html_blocks.append(block_html) self.logger.debug(f" ✅ Bloque HTML creado para ecuación {i}") # Generar HTML completo if html_blocks: equations_html = '\n'.join(html_blocks) status_indicator = '
⏳ Cargando MathJax...
' final_content = f'{equations_html}\n{status_indicator}' self.logger.debug(f"📝 HTML de ecuaciones generado: {len(final_content)} caracteres") else: # Sin ecuaciones, mantener mensaje por defecto pero agregar status final_content = """
📐 Panel de Ecuaciones LaTeX
Renderizado con MathJax + fuentes matemáticas
Las ecuaciones se mostrarán aquí automáticamente
ℹ️ Sin ecuaciones
""" self.logger.debug("📝 Sin ecuaciones - usando contenido por defecto") # Obtener HTML base base_html = self._generate_html_with_javascript() # Reemplazar el contenido del contenedor - VERSIÓN MEJORADA # Buscar el div equations-container y reemplazar todo su contenido import re # Patrón para encontrar el div equations-container completo pattern = r'
.*?
' new_container = f'
\n{final_content}\n
' # Reemplazar usando regex para mayor precisión html_content = re.sub(pattern, new_container, base_html, flags=re.DOTALL) # Verificar que el reemplazo funcionó if 'equations-container' in html_content and ('equation-block' in final_content): equations_found = html_content.count('equation-block') self.logger.debug(f"🔍 Verificación de reemplazo: {equations_found} bloques de ecuaciones encontrados en HTML final") else: self.logger.warning("⚠️ El reemplazo del contenedor de ecuaciones puede no haber funcionado correctamente") self.logger.debug(f"🔚 HTML final generado: {len(html_content)} caracteres") return html_content def _generate_html_pywebview_with_equations(self): """Genera HTML con ecuaciones específicamente optimizado para pywebview""" # Crear HTML de ecuaciones usando el mismo sistema que tkinterweb pero con base pywebview html_blocks = [] for i, eq_data in enumerate(self._latex_equations): eq_type = eq_data.get('type', 'symbolic') latex_content = eq_data.get('latex', '') original_content = eq_data.get('original', '') # Preparar LaTeX para MathJax if latex_content: # Usar $$ para display math if not latex_content.startswith('$'): formatted_latex = f"$${latex_content}$$" else: formatted_latex = latex_content # Crear bloque HTML optimizado para pywebview block_html = f"""
{formatted_latex}
""" html_blocks.append(block_html) self.logger.debug(f" ✅ Bloque HTML pywebview creado para ecuación {i}") # Combinar todos los bloques final_content = '\n'.join(html_blocks) self.logger.debug(f"📝 HTML de ecuaciones pywebview generado: {len(final_content)} caracteres") # Obtener HTML base y reemplazar contenido base_html = self._generate_html_for_pywebview() # Reemplazar el contenido del contenedor usando regex (corregido para LaTeX) import re # Patrón para encontrar el div equations-container completo pattern = r'(
)(.*?)(
)' # Nuevo contenido del contenedor (escapar para regex) new_container_content = f'
\\n{final_content}\\n
' # Reemplazar usando re.escape para contenido seguro html_content = re.sub(pattern, lambda m: new_container_content, base_html, flags=re.DOTALL) self.logger.debug(f"🔚 HTML final pywebview generado: {len(html_content)} caracteres") return html_content def _generate_html_fallback_with_equations(self): """Genera HTML fallback con ecuaciones en formato texto mejorado""" html_blocks = [] for i, eq_data in enumerate(self._latex_equations): eq_type = eq_data.get('type', 'symbolic') latex_content = eq_data.get('latex', '') original_content = eq_data.get('original', '') # Convertir LaTeX básico a texto legible display_content = latex_content or original_content or 'Sin contenido' # Conversiones básicas de LaTeX a texto legible display_content = display_content.replace('\\frac{', '(').replace('}{', ')/(').replace('}', ')') display_content = display_content.replace('\\sqrt{', '√(').replace('}', ')') display_content = display_content.replace('^{2}', '²').replace('^{3}', '³') display_content = display_content.replace('\\', '') # Crear bloque HTML simple block_html = f"""
{display_content}
""" html_blocks.append(block_html) # Generar HTML completo equations_html = '\n'.join(html_blocks) html_content = self._generate_html_fallback().replace( '
\n
\n 📐 Panel de Ecuaciones LaTeX (Modo Fallback)
\n Renderizado sin JavaScript - PythonMonkey no disponible
\n Las ecuaciones se mostrarán en formato texto mejorado\n
\n
', f'
\n{equations_html}\n
' ) return html_content def _eval_js_tkinterweb(self, js_code: str) -> bool: """Evalúa JavaScript en tkinterweb (requiere PythonMonkey para JavaScript real)""" try: if not self._js_available: self.logger.debug("⚠️ JavaScript no disponible - PythonMonkey no instalado") return False # Usar PythonMonkey para ejecutar JavaScript # NOTA: PythonMonkey NO tiene acceso directo al DOM de tkinterweb # Esta implementación es para compatibilidad futura import pythonmonkey # JavaScript ejecutado en contexto aislado (sin DOM) result = pythonmonkey.eval(js_code) self.logger.debug(f"✅ JavaScript ejecutado via PythonMonkey: {js_code[:50]}...") return True except Exception as e: self.logger.debug(f"⚠️ Error ejecutando JavaScript: {e}") return False def _trigger_mathjax_rerender(self): """Re-renderiza MathJax específicamente para tkinterweb""" try: if hasattr(self, 'latex_webview') and self.latex_webview: js_code = """ console.log('🔄 [PYTHON] Trigger de re-renderizado para tkinterweb...'); // Llamar directamente a la función específica de tkinterweb if (typeof renderizarParaTkinterweb === 'function') { console.log('✅ [PYTHON] Función tkinterweb encontrada, ejecutando...'); renderizarParaTkinterweb(); } else if (typeof forzarRenderizadoTkinterweb === 'function') { console.log('✅ [PYTHON] Función de forzado encontrada, ejecutando...'); forzarRenderizadoTkinterweb(); } else { console.log('⚠️ [PYTHON] Funciones específicas no encontradas, intentando método genérico...'); // Fallback para casos donde las funciones no estén definidas function renderizadoFallback() { if (typeof window.MathJax !== 'undefined' && window.MathJax.typesetPromise) { console.log('🔧 [PYTHON] Usando renderizado fallback...'); var mathElements = document.querySelectorAll('.math-display'); console.log('📊 [PYTHON] Elementos a renderizar:', mathElements.length); window.MathJax.typesetPromise().then(function() { console.log('🎉 [PYTHON] Renderizado fallback exitoso!'); var statusEl = document.getElementById('mathjax-status'); if (statusEl) { statusEl.innerHTML = '✅ Renderizado desde Python'; statusEl.style.color = '#4fc3f7'; } }).catch(function(err) { console.log('❌ [PYTHON] Error en renderizado fallback:', err); var statusEl = document.getElementById('mathjax-status'); if (statusEl) { statusEl.innerHTML = '❌ Error desde Python: ' + err.message; statusEl.style.color = '#f44747'; } }); } else { console.log('❌ [PYTHON] MathJax no disponible para fallback'); var statusEl = document.getElementById('mathjax-status'); if (statusEl) { statusEl.innerHTML = '❌ MathJax no disponible'; statusEl.style.color = '#f44747'; } } } renderizadoFallback(); } """ success = self._eval_js_tkinterweb(js_code) if success: self.logger.debug("🔄 Trigger específico para tkinterweb enviado") else: self.logger.warning("⚠️ JavaScript no ejecutado - revisando conexión MathJax") else: self.logger.warning("⚠️ tkinterweb no disponible para JavaScript") except Exception as e: self.logger.debug(f"⚠️ Error ejecutando JavaScript en tkinterweb: {e}") def _update_content_indicator(self): """Actualiza el indicador visual de contenido disponible""" try: if hasattr(self, '_latex_equations') and self._latex_equations: # Mostrar indicador si no está visible if not hasattr(self, '_indicator_visible') or not self._indicator_visible: self.latex_indicator.pack(side=tk.BOTTOM, pady=2) self._indicator_visible = True else: # Ocultar indicador si no hay contenido if hasattr(self, '_indicator_visible') and self._indicator_visible: self.latex_indicator.pack_forget() self._indicator_visible = False except Exception as e: self.logger.debug(f"Error actualizando indicador: {e}") def _diagnose_mathjax(self): """Ejecuta diagnóstico completo de MathJax""" try: if not hasattr(self, 'latex_webview') or not self.latex_webview: messagebox.showerror("Error", "Panel LaTeX no disponible") return # Crear ventana de diagnóstico diag_window = tk.Toplevel(self.root) diag_window.title("Diagnóstico MathJax") diag_window.geometry("600x400") diag_window.configure(bg="#2b2b2b") text_widget = scrolledtext.ScrolledText( diag_window, font=("Consolas", 10), bg="#1e1e1e", fg="#d4d4d4", wrap=tk.WORD ) text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) def agregar_resultado(texto): text_widget.insert(tk.END, texto + "\n") text_widget.see(tk.END) diag_window.update() agregar_resultado("🔍 DIAGNÓSTICO MATHJAX") agregar_resultado("=" * 50) agregar_resultado("") # 1. Verificar que el panel está visible agregar_resultado(f"📐 Panel LaTeX visible: {self.latex_panel_visible}") agregar_resultado(f"📊 Ecuaciones en panel: {len(self._latex_equations) if hasattr(self, '_latex_equations') else 0}") agregar_resultado("") # 2. Ejecutar diagnóstico JavaScript agregar_resultado("🔍 Ejecutando diagnóstico JavaScript...") js_diagnostic = """ var resultados = []; // Verificar MathJax resultados.push('🌐 window.MathJax definido: ' + (typeof window.MathJax !== 'undefined')); if (typeof window.MathJax !== 'undefined') { resultados.push('⚙️ MathJax.typesetPromise: ' + (typeof window.MathJax.typesetPromise !== 'undefined')); resultados.push('⚙️ MathJax.startup: ' + (typeof window.MathJax.startup !== 'undefined')); if (window.MathJax.version) { resultados.push('📦 Versión MathJax: ' + window.MathJax.version); } } // Verificar DOM var equations = document.querySelectorAll('.math-display'); resultados.push('📄 Elementos .math-display encontrados: ' + equations.length); var statusEl = document.getElementById('mathjax-status'); if (statusEl) { resultados.push('📊 Estado actual: ' + statusEl.innerHTML); } // Verificar consola resultados.push('🌐 Navegador: ' + navigator.userAgent.substring(0, 50) + '...'); console.log('🔍 Diagnóstico completo ejecutado'); // Retornar resultados como string resultados.join('\\n'); """ try: # Ejecutar diagnóstico JavaScript self.latex_webview.eval_js(js_diagnostic) agregar_resultado("✅ Diagnóstico JavaScript ejecutado") agregar_resultado("") # 3. Intentar renderizado de prueba agregar_resultado("🧪 Intentando renderizado de prueba...") test_js = """ function pruebaRenderizado() { if (typeof window.MathJax !== 'undefined' && window.MathJax.typesetPromise) { console.log('🧪 Iniciando prueba de renderizado...'); window.MathJax.typesetPromise().then(function() { console.log('✅ Prueba de renderizado exitosa'); }).catch(function(err) { console.log('❌ Error en prueba:', err); }); } else { console.log('❌ MathJax no disponible para prueba'); } } pruebaRenderizado(); """ self.latex_webview.eval_js(test_js) agregar_resultado("✅ Prueba de renderizado enviada") except Exception as e: agregar_resultado(f"❌ Error ejecutando JavaScript: {e}") agregar_resultado("") agregar_resultado("💡 SOLUCIONES POSIBLES:") agregar_resultado("• Verifica conexión a internet (CDN de MathJax)") agregar_resultado("• Cierra y reabre el panel LaTeX") agregar_resultado("• Reinicia la aplicación") agregar_resultado("• Revisa la consola del navegador (F12)") # Botón para cerrar close_btn = tk.Button( diag_window, text="Cerrar", command=diag_window.destroy, bg="#3c3c3c", fg="white", relief=tk.FLAT ) close_btn.pack(pady=5) except Exception as e: messagebox.showerror("Error", f"Error en diagnóstico:\n{e}") def _test_tkinterweb_mathjax(self): """Test específico para tkinterweb y MathJax""" try: if not hasattr(self, 'latex_webview') or not self.latex_webview: messagebox.showerror("Error", "Panel LaTeX tkinterweb no disponible") return # Mostrar panel si no está visible if not self.latex_panel_visible: self._show_latex_panel() messagebox.showinfo("Panel LaTeX", "Panel LaTeX activado para testing") return # NUEVO: Test de contenido HTML actual test_content_js = """ console.log('🔍 [CONTENIDO] Test de contenido HTML actual...'); // 1. Verificar que el contenedor existe var container = document.getElementById('equations-container'); console.log('🔍 [CONTENIDO] Contenedor encontrado:', container !== null); if (container) { console.log('🔍 [CONTENIDO] Contenido del contenedor:'); console.log(container.innerHTML.substring(0, 500) + '...'); // 2. Contar elementos específicos var equationBlocks = container.querySelectorAll('.equation-block'); var mathDisplays = container.querySelectorAll('.math-display'); var infoMessages = container.querySelectorAll('.info-message'); console.log('🔍 [CONTENIDO] Bloques de ecuación:', equationBlocks.length); console.log('🔍 [CONTENIDO] Displays matemáticos:', mathDisplays.length); console.log('🔍 [CONTENIDO] Mensajes de info:', infoMessages.length); // 3. Log de contenido de cada ecuación for (var i = 0; i < Math.min(mathDisplays.length, 3); i++) { console.log('🔍 [CONTENIDO] Ecuación ' + i + ':', mathDisplays[i].innerHTML.substring(0, 100)); } // 4. Verificar si hay contenido renderizado por MathJax var mjxElements = container.querySelectorAll('mjx-container, .MathJax, mjx-math'); console.log('🔍 [CONTENIDO] Elementos MathJax renderizados:', mjxElements.length); // 5. Actualizar status con información de contenido var statusEl = document.getElementById('mathjax-status'); if (statusEl) { var info = 'Contenido: ' + equationBlocks.length + ' ecuaciones, ' + mjxElements.length + ' renderizadas'; statusEl.innerHTML = '🔍 ' + info; statusEl.style.color = '#4fc3f7'; } } else { console.log('❌ [CONTENIDO] Contenedor no encontrado'); } console.log('🔍 [CONTENIDO] Test de contenido completado'); """ # Ejecutar test de contenido primero self.logger.debug("🧪 Ejecutando test de contenido HTML en tkinterweb...") content_success = self._eval_js_tkinterweb(test_content_js) # Luego ejecutar test general test_js = """ console.log('🧪 [TEST] Iniciando test específico tkinterweb...'); // 1. Verificar estado general console.log('🧪 [TEST] MathJax disponible:', typeof window.MathJax !== 'undefined'); console.log('🧪 [TEST] typesetPromise disponible:', typeof window.MathJax !== 'undefined' && typeof window.MathJax.typesetPromise !== 'undefined'); // 2. Verificar funciones específicas console.log('🧪 [TEST] renderizarParaTkinterweb:', typeof renderizarParaTkinterweb); console.log('🧪 [TEST] forzarRenderizadoTkinterweb:', typeof forzarRenderizadoTkinterweb); // 3. Contar elementos GLOBALES var mathElements = document.querySelectorAll('.math-display'); var mjxElements = document.querySelectorAll('mjx-container, .MathJax, mjx-math'); console.log('🧪 [TEST] Elementos .math-display GLOBALES:', mathElements.length); console.log('🧪 [TEST] Elementos MathJax renderizados GLOBALES:', mjxElements.length); // 4. Test de renderizado forzado if (typeof forzarRenderizadoTkinterweb === 'function') { console.log('🧪 [TEST] Ejecutando renderizado forzado...'); forzarRenderizadoTkinterweb(); } else { console.log('⚠️ [TEST] Función de renderizado forzado no disponible'); } console.log('🧪 [TEST] Test completado'); """ success = self._eval_js_tkinterweb(test_js) # Determinar estado final if content_success and success: status_msg = "Test completo ejecutado correctamente." elif content_success: status_msg = "Test de contenido ejecutado, JavaScript limitado." else: status_msg = "Tests limitados - JavaScript no disponible." # Información adicional del sistema equation_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0 messagebox.showinfo( "Test tkinterweb", f"{status_msg}\n\n" f"JavaScript disponible: {self._js_available}\n" f"tkinterweb disponible: {self._webview_available}\n" f"Ecuaciones en memoria: {equation_count}\n\n" "Tests ejecutados:\n" f"✓ Contenido HTML: {'Sí' if content_success else 'No'}\n" f"✓ Funciones MathJax: {'Sí' if success else 'No'}\n\n" "Para ver logs detallados:\n" "• Revisa la consola de la aplicación\n" "• O usa 'Abrir HTML en Navegador' + F12" ) except Exception as e: self.logger.error(f"Error en test tkinterweb: {e}") messagebox.showerror("Error", f"Error en test tkinterweb:\n{e}") def _save_html_debug_copy(self, html_content): """Guarda una copia del HTML generado para debugging en navegador""" try: # Usar nombre simple para fácil acceso filename = "latex_debug.html" # Guardar en el directorio de la aplicación with open(filename, 'w', encoding='utf-8') as f: f.write(html_content) # Actualizar referencia al último archivo generado self._last_debug_html = filename self.logger.debug(f"🔍 HTML de debug guardado: {filename}") except Exception as e: self.logger.error(f"❌ Error guardando HTML de debug: {e}") def _open_debug_html_in_browser(self): """Abre el último HTML de debug en el navegador del sistema""" try: if not hasattr(self, '_last_debug_html') or not self._last_debug_html: messagebox.showwarning("Sin archivo", "No hay archivo HTML de debug disponible") return if not os.path.exists(self._last_debug_html): messagebox.showerror("Archivo no encontrado", f"El archivo {self._last_debug_html} no existe") return import webbrowser file_path = os.path.abspath(self._last_debug_html) file_url = f"file:///{file_path.replace('\\', '/')}" webbrowser.open(file_url) messagebox.showinfo( "HTML Abierto", f"Archivo abierto en navegador:\n{self._last_debug_html}\n\n" f"Presiona F12 para ver la consola del navegador y depurar MathJax.\n\n" f"NUEVA CONFIGURACIÓN:\n" f"• Sin polyfill.io (evita errores SSL)\n" f"• Diagnóstico mejorado en consola\n" f"• Función global: forzarRenderizado()" ) except Exception as e: messagebox.showerror("Error", f"Error abriendo archivo en navegador:\n{e}") def _show_latex_panel_status(self): """Muestra el estado actual del panel LaTeX""" try: # Información básica panel_exists = hasattr(self, 'latex_panel') panel_visible = hasattr(self, 'latex_panel_visible') and self.latex_panel_visible webview_exists = hasattr(self, 'latex_webview') and self.latex_webview equations_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0 # Información sobre las ecuaciones equations_info = "" if hasattr(self, '_latex_equations') and self._latex_equations: equations_info = "\n\nECUACIONES EN MEMORIA:\n" for i, eq in enumerate(self._latex_equations[:5]): # Mostrar solo las primeras 5 eq_type = eq.get('type', 'unknown') latex_content = eq.get('latex', '') equations_info += f"{i+1}. [{eq_type}] {latex_content[:50]}...\n" if len(self._latex_equations) > 5: equations_info += f"... y {len(self._latex_equations) - 5} más\n" # Estado de archivos debug_file_exists = os.path.exists("latex_debug.html") debug_file_info = "" if debug_file_exists: stat = os.stat("latex_debug.html") import datetime mod_time = datetime.datetime.fromtimestamp(stat.st_mtime) debug_file_info = f"\nArchivo debug: latex_debug.html\nÚltima modificación: {mod_time.strftime('%H:%M:%S')}" status_message = f"""ESTADO DEL PANEL LATEX COMPONENTES: • Panel creado: {'✓' if panel_exists else '✗'} • Panel visible: {'✓' if panel_visible else '✗'} • WebView tkinterweb: {'✓' if webview_exists else '✗'} • JavaScript disponible: {'✓' if self._js_available else '✗'} CONTENIDO: • Ecuaciones en memoria: {equations_count} • Archivo debug existe: {'✓' if debug_file_exists else '✗'}{debug_file_info} WEBVIEW TYPE: {getattr(self, '_webview_type', 'No definido')} WEBVIEW AVAILABLE: {getattr(self, '_webview_available', 'No definido')}{equations_info} PARA SOLUCIONAR: 1. Si las ecuaciones están en memoria pero no se ven: → Usa 'Test tkinterweb' para diagnosticar 2. Si el archivo debug tiene contenido pero tkinterweb no: → Puede ser problema de renderizado en tkinterweb 3. Revisa el archivo debug con 'Abrir HTML en Navegador'""" # Crear ventana de estado status_window = tk.Toplevel(self.root) status_window.title("Estado Panel LaTeX") status_window.geometry("600x500") status_window.configure(bg="#2b2b2b") text_widget = scrolledtext.ScrolledText( status_window, font=("Consolas", 10), bg="#1e1e1e", fg="#d4d4d4", wrap=tk.WORD ) text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) text_widget.insert("1.0", status_message) text_widget.config(state="disabled") # Botón de cerrar close_btn = tk.Button( status_window, text="Cerrar", command=status_window.destroy, bg="#3c3c3c", fg="white", relief=tk.FLAT ) close_btn.pack(pady=5) except Exception as e: self.logger.error(f"Error mostrando estado del panel: {e}") messagebox.showerror("Error", f"Error mostrando estado:\n{e}") def _force_html_update(self): """Fuerza actualización del panel LaTeX y generación de nuevo HTML de debug""" try: if hasattr(self, '_latex_equations') and self._latex_equations: self.logger.info("🔄 Forzando actualización del panel LaTeX...") # Forzar actualización del panel if self.latex_panel_visible: self._update_latex_panel() else: # Si no está visible, mostrarlo temporalmente para generar HTML self._show_latex_panel() self.root.after(1000, lambda: self._update_latex_panel()) messagebox.showinfo( "Actualización Forzada", f"Panel LaTeX actualizado.\n\n" f"Ecuaciones en panel: {len(self._latex_equations)}\n" f"Nuevo HTML generado: latex_debug.html\n\n" f"Usa 'Abrir HTML en Navegador' para ver el resultado." ) else: messagebox.showwarning( "Sin contenido", "No hay ecuaciones en el panel LaTeX para actualizar.\n\n" "Escribe algunas ecuaciones en la calculadora primero." ) except Exception as e: self.logger.error(f"Error forzando actualización: {e}") messagebox.showerror("Error", f"Error forzando actualización:\n{e}") def _add_to_latex_panel(self, content_type: str, latex_content: str): """Añade una ecuación al panel LaTeX""" self.logger.debug(f"🔧 AÑADIENDO al panel LaTeX: tipo='{content_type}', contenido='{latex_content[:100]}...'") if not hasattr(self, '_latex_equations'): self._latex_equations = [] self.logger.debug(" -> Lista de ecuaciones inicializada") # Crear datos de la ecuación equation_data = { 'type': content_type, 'latex': latex_content, 'original': latex_content, # Guardar contenido original también 'timestamp': time.time() } before_count = len(self._latex_equations) self._latex_equations.append(equation_data) after_count = len(self._latex_equations) # Limitar número de ecuaciones (opcional) max_equations = 50 if len(self._latex_equations) > max_equations: self._latex_equations = self._latex_equations[-max_equations:] self.logger.debug(f" -> Lista limitada a {max_equations} ecuaciones") self.logger.debug(f"➕ Ecuación añadida: {content_type} (Total: {before_count} -> {after_count})") # Actualizar indicador visual self._update_content_indicator() # IMPORTANTE: Actualizar panel si está visible panel_visible = hasattr(self, 'latex_panel_visible') and self.latex_panel_visible self.logger.debug(f"🔍 Estado del panel: existe={hasattr(self, 'latex_panel_visible')}, visible={panel_visible}") if panel_visible: self.logger.debug("🔄 Panel visible - FORZANDO actualización de contenido...") self._update_latex_panel() else: self.logger.debug("👁️ Panel no visible - saltando actualización (ecuación guardada para cuando se abra)") self.logger.debug(f"✅ Proceso de añadir ecuación COMPLETADO") self.logger.debug(f"📊 Estado final: {len(self._latex_equations)} ecuaciones en total") def main(): """Función principal""" root = tk.Tk() app = HybridCalculatorApp(root) try: root.iconname("Calculadora MAV CAS") except tk.TclError: pass root.mainloop() if __name__ == "__main__": main()