From 6a533e5bd6bc83d68010c1154518a68833dd25a1 Mon Sep 17 00:00:00 2001 From: Miguel Date: Sat, 7 Jun 2025 19:38:57 +0200 Subject: [PATCH] =?UTF-8?q?Implementaci=C3=B3n=20de=20un=20panel=20LaTeX?= =?UTF-8?q?=20expandible=20en=20la=20interfaz=20de=20la=20calculadora,=20p?= =?UTF-8?q?ermitiendo=20la=20visualizaci=C3=B3n=20de=20ecuaciones=20y=20as?= =?UTF-8?q?ignaciones.=20Se=20a=C3=B1aden=20mejoras=20en=20la=20gesti?= =?UTF-8?q?=C3=B3n=20de=20resultados=20interactivos,=20incluyendo=20la=20l?= =?UTF-8?q?impieza=20del=20contexto=20antes=20de=20evaluaciones=20y=20la?= =?UTF-8?q?=20actualizaci=C3=B3n=20din=C3=A1mica=20del=20panel=20LaTeX.=20?= =?UTF-8?q?Se=20ajustan=20configuraciones=20de=20la=20ventana=20y=20se=20o?= =?UTF-8?q?ptimizan=20los=20bindings=20de=20teclado.=20Se=20actualizan=20l?= =?UTF-8?q?as=20dependencias=20para=20incluir=20soporte=20para=20renderiza?= =?UTF-8?q?do=20de=20LaTeX.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hybrid_calc_history.txt | 13 +- hybrid_calc_settings.json | 7 +- main_calc_app.py | 1208 ++++++++++++++++++++++++++++++++++--- main_evaluation_puro.py | 11 +- requirements.txt | 7 +- 5 files changed, 1166 insertions(+), 80 deletions(-) diff --git a/hybrid_calc_history.txt b/hybrid_calc_history.txt index 7522a12..aeed779 100644 --- a/hybrid_calc_history.txt +++ b/hybrid_calc_history.txt @@ -1,5 +1,12 @@ -x = 5 -resultado = x * 2 -valor_test = 100 \ No newline at end of file +x**2 + y**2 = r**2 +r=? +x=3 +y=6 +r +r=? + +$$Brix = \frac{Brix_{syrup} \cdot \delta_{syrup} + (Brix_{water} \cdot \delta_{water} \cdot Rateo)}{\delta_{syrup} + \delta_{water} \cdot Rateo}$$ + +$$Brix_{Bev} = \frac{Brix_{syr} + Brix_{H_2O} \cdot R_M}{R_M + 1}$$ \ No newline at end of file diff --git a/hybrid_calc_settings.json b/hybrid_calc_settings.json index da27a0b..0288774 100644 --- a/hybrid_calc_settings.json +++ b/hybrid_calc_settings.json @@ -1,8 +1,9 @@ { - "window_geometry": "1383x700+203+1261", - "sash_pos_x": 721, + "window_geometry": "1272x700+331+1194", + "sash_pos_x": 440, "symbolic_mode": true, "show_numeric_approximation": true, "keep_symbolic_fractions": true, - "auto_simplify": false + "auto_simplify": false, + "latex_panel_visible": true } \ No newline at end of file diff --git a/main_calc_app.py b/main_calc_app.py index f287aad..1a3a21f 100644 --- a/main_calc_app.py +++ b/main_calc_app.py @@ -12,6 +12,7 @@ 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 @@ -61,53 +62,78 @@ class HybridCalculatorApp: def __init__(self, root: tk.Tk): self.root = root - self.logger = logging.getLogger(__name__) # <--- AÑADIDO: Logger para la instancia - self.root.title("Calculadora MAV - CAS Híbrido") - # Configuración y estado - self.settings = self._load_settings() - self.root.geometry(self.settings.get("window_geometry", "1000x700")) - self.root.configure(bg="#2b2b2b") + # 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) - # Debug desde configuración (definir antes del motor) - self.debug = self.settings.get("debug", False) - - # Motor de evaluación - SISTEMA ALGEBRAICO PURO + # ========== INSTANCIAS DEL SISTEMA ========== + # Motor de evaluación (instancia única) self.engine = PureAlgebraicEngine() - # Configurar motor - if hasattr(self.engine, 'logger'): - self.engine.logger.setLevel(logging.DEBUG if self.debug else logging.INFO) + # Manager de contenido interactivo + self.interactive_manager = None # Se inicializará en setup_interactive_manager() - # ========== SISTEMA DE AUTOCOMPLETADO MEJORADO ========== + # ========== 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._current_suggestions = [] self._autocomplete_active = False - self._autocomplete_trigger_pos = None + self._autocomplete_suggestions = [] self._autocomplete_filter_text = "" + self._autocomplete_trigger_pos = "" self._popup_disabled_until_next_dot = False - self._last_navigation_time = 0 # Para evitar filtrado tras navegación + 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 + + # ========== 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._variable_popup_active = False self._last_input_change = 0 - # Configurar ícono + # ========== CONFIGURACIÓN DE VENTANA ========== + self._setup_window() self._setup_icon() - # ========== COMPONENTES PRINCIPALES CON NUEVO SISTEMA ========== - self.interactive_manager = None # Se inicializa después de crear widgets + # ========== CONSTRUCCIÓN DE INTERFAZ ========== + self.create_widgets() + self.create_menu() + self.setup_output_tags() + self.setup_scroll_sync() + self.setup_interactive_manager() - # ========== HELPERS DINÁMICOS DEL REGISTRO ========== + # ========== CONFIGURACIÓN FINAL ========== self._setup_dynamic_helpers() + self.load_history() - # Estado de la aplicación - self._debounce_job = None - self._syncing_yview = False - self._cached_input_font = None - self.output_buffer = [] + # ========== 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) @@ -123,26 +149,27 @@ class HybridCalculatorApp: anchor=tk.W ) self.status_label.pack(side=tk.LEFT, padx=10, pady=2) - - # ========== PANEL PRINCIPAL ========== - self.create_widgets() - self.setup_interactive_manager() - self.load_history() - - # Configurar eventos de cierre - self.root.protocol("WM_DELETE_WINDOW", self.on_close) - - # ========== BINDINGS DE TECLADO MEJORADOS ========== - 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) + + + + 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""" @@ -250,21 +277,25 @@ CARACTERÍSTICAS: def create_widgets(self): - """Crea la interfaz gráfica""" + """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) - # Panel dividido + # 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( - main_frame, orient=tk.HORIZONTAL, bg="#2b2b2b", + content_frame, orient=tk.HORIZONTAL, bg="#2b2b2b", sashrelief=tk.FLAT, sashwidth=4, bd=0, showhandle=False, opaqueresize=True, ) - self.paned_window.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) + self.paned_window.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - # Panel de entrada - initial_input_width = self.settings.get("sash_pos_x", 450) + # 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, @@ -282,7 +313,7 @@ CARACTERÍSTICAS: self.paned_window.add( self.input_text, width=initial_input_width, - stretch="always", + stretch="never", # No se expande automáticamente minsize=200 ) @@ -304,6 +335,16 @@ CARACTERÍSTICAS: 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")) @@ -316,6 +357,484 @@ CARACTERÍSTICAS: # 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""" + 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) + + # Intentar diferentes opciones de webview en orden de preferencia + self._webview_available = False + self._webview_type = None + self.latex_webview = None + self.latex_fallback_text = None + + # Opción 1: tkinterweb (más compatible con tkinter) + try: + import tkinterweb + + # 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 + base_html = self._generate_base_html() + self.latex_webview.load_html(base_html) + + self._webview_available = True + self._webview_type = "tkinterweb" + + except ImportError: + # Opción 2: pywebview (menos integrado pero más completo) + try: + import webview + + # Crear el contenedor para el webview + webview_frame = tk.Frame(self.latex_panel, bg="#1a1a1a") + webview_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) + + # Inicializar contenido HTML base + self._current_html = self._generate_base_html() + + # El webview se creará de forma lazy cuando sea necesario + self.latex_webview_frame = webview_frame + self.latex_webview = None + + self._webview_available = True + self._webview_type = "pywebview" + + except ImportError: + # Opción 3: Fallback con Text widget + self._webview_available = False + self._webview_type = "fallback" + + fallback_text = scrolledtext.ScrolledText( + self.latex_panel, + font=("Consolas", 10), + bg="#1a1a1a", + fg="#d4d4d4", + state="disabled", + wrap=tk.WORD, + borderwidth=0, + highlightthickness=0, + relief=tk.FLAT + ) + fallback_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) + self.latex_fallback_text = fallback_text + + # Mostrar mensaje informativo + fallback_text.config(state="normal") + fallback_text.insert("1.0", "Panel de Ecuaciones LaTeX\n\n" + "Para ver ecuaciones renderizadas, instala:\n" + "pip install tkinterweb\n" + "o pip install pywebview\n\n" + "Las ecuaciones aparecerán aquí en formato texto:") + fallback_text.config(state="disabled") + + # 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: + if self._webview_available: + if self._webview_type == "tkinterweb": + self._update_tkinterweb() + elif self._webview_type == "pywebview": + self._update_pywebview() + else: + # Refrescar contenido de fallback + if hasattr(self, 'latex_fallback_text'): + self.latex_fallback_text.config(state="normal") + self.latex_fallback_text.delete("1.0", tk.END) + + for eq in self._latex_equations: + if eq['type'] == 'spacer': + self.latex_fallback_text.insert(tk.END, "\n") + else: + type_text = f"[{eq['type'].upper()}] " + self.latex_fallback_text.insert(tk.END, type_text) + self.latex_fallback_text.insert(tk.END, f"{eq['original']}\n\n") + + self.latex_fallback_text.config(state="disabled") + + except Exception as e: + self.logger.debug(f"Error refrescando contenido LaTeX: {e}") + + def _generate_base_html(self): + """Genera el HTML base con soporte fallback para renderizar ecuaciones""" + return r""" + + + + + + Ecuaciones LaTeX + + + + +
+
+ 📐 Panel de Ecuaciones
+ Las ecuaciones se mostrarán aquí automáticamente +
+
+ + + + + """ def setup_interactive_manager(self): """Configura el gestor de resultados interactivos""" @@ -346,6 +865,17 @@ CARACTERÍSTICAS: 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) @@ -1138,10 +1668,17 @@ CARACTERÍSTICAS: self._clear_output() return - # MODIFICADO: Solo limpiar ecuaciones, NO las variables del usuario - # Las variables deben persistir para el autocompletado - self.engine.equations.clear() # Solo limpiar ecuaciones - self.logger.debug("Ecuaciones del motor limpiadas antes de evaluación (variables mantenidas)") + # 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) @@ -1176,6 +1713,9 @@ CARACTERÍSTICAS: 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 @@ -1268,6 +1808,89 @@ CARACTERÍSTICAS: 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]}...") + + # Usar la función _add_to_latex_panel + self._add_to_latex_panel(equation_type, latex_content) + + self.logger.debug(f"✅ AÑADIDO al panel LaTeX: {equation_type} -> {latex_content[:50]}...") + + 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 @@ -1382,10 +2005,12 @@ CARACTERÍSTICAS: # ========== MÉTODOS DE MENÚ Y COMANDOS (la mayoría sin cambios) ========== def new_session(self): - """Inicia nueva sesión""" + """Inicia una nueva sesión limpiando todo""" self.clear_input() self.clear_output() - self.engine.clear_context() # Aquí sí limpiamos todo porque es nueva sesión + self._clear_latex_panel() + if hasattr(self.engine, 'clear_context'): + self.engine.clear_context() def load_file(self): """Carga archivo en el editor""" @@ -1439,8 +2064,9 @@ CARACTERÍSTICAS: self._clear_output() def clear_output(self): - """Limpia panel de salida""" + """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""" @@ -1932,7 +2558,7 @@ Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el return self._cached_input_font def _adjust_input_pane_width(self): - """Ajusta el ancho del panel de entrada según su contenido.""" + """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 @@ -1945,29 +2571,48 @@ Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el 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 ---") + 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 (max_pixel_width + padding): {width_needed_by_text}") + 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) @@ -1994,17 +2639,17 @@ Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el new_input_width = max(new_input_width, min_input_pane_width) # Asegurar que el panel de salida conserve su espacio mínimo - if total_width - new_input_width < min_output_pane_width: - new_input_width = total_width - min_output_pane_width + 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 - # Aplicar un ratio máximo para el panel de entrada - max_input_ratio = 0.75 # Podría ser una constante de clase - max_width_by_ratio = int(total_width * max_input_ratio) + # 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_width - max_width_by_ratio) >= min_output_pane_width: + (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 @@ -2018,7 +2663,7 @@ Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el 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_width >= (min_input_pane_width + min_output_pane_width): + 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}") @@ -2075,6 +2720,427 @@ Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el # 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_tkinterweb(self): + """Actualiza el contenido usando tkinterweb""" + try: + if not hasattr(self, 'latex_webview') or not self.latex_webview: + return + + # Generar HTML completo con todas las ecuaciones + html_content = self._generate_equations_html() + + # Cargar el HTML actualizado + self.latex_webview.load_html(html_content) + + except Exception as e: + self.logger.debug(f"Error actualizando tkinterweb: {e}") + + def _update_pywebview(self): + """Actualiza el contenido usando pywebview (implementación futura)""" + try: + # Para pywebview, necesitaríamos usar la API de JavaScript + # Por ahora, usar un enfoque más simple + pass + + except Exception as e: + self.logger.debug(f"Error actualizando pywebview: {e}") + + def _generate_equations_html(self): + """Genera HTML completo con todas las ecuaciones""" + base_html = self._generate_base_html() + + # Generar contenido de ecuaciones + equations_content = "" + + for eq in self._latex_equations: + eq_type = eq['type'] + content = eq['content'] + + # Manejar espaciadores especiales + if eq_type == 'spacer': + equations_content += '
' + continue + + equations_content += f""" +
+
{eq_type}
+
{content}
+
+ """ + + # Reemplazar el contenido del container en el HTML base + if equations_content: + placeholder = '
' + if placeholder in base_html: + # Reemplazar el contenido de ejemplo + start_idx = base_html.find('
') + len('
') + end_idx = base_html.find('
', start_idx) + + new_html = (base_html[:start_idx] + + equations_content + + base_html[end_idx:]) + return new_html + + return base_html + + def _update_latex_panel(self): + """Actualiza el contenido del panel LaTeX""" + if not self.latex_panel_visible or not hasattr(self, '_latex_equations'): + self.logger.debug("❌ Panel no visible o sin ecuaciones - no actualizar") + return + + try: + self.logger.debug(f"🔄 Actualizando panel LaTeX con {len(self._latex_equations)} ecuaciones") + + if not self._latex_equations: + # Si no hay ecuaciones, usar el HTML base + html_content = self._generate_base_html() + self.logger.debug("📄 Generando HTML base (sin ecuaciones)") + else: + # Generar HTML con las 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', '') + + self.logger.debug(f" 📝 Ecuación {i+1}: {eq_type} -> {latex_content[:50]}...") + + # Crear bloque HTML para esta ecuación + # Aplicar formateo matemático mejorado + formatted_math = latex_content + + # Convertir LaTeX básico a HTML mejorado con mejores símbolos + import re + + # 1. Fracciones LaTeX: \frac{num}{den} → HTML + def replace_fractions(text): + def replace_frac(match): + num = match.group(1) + den = match.group(2) + return f'{num}{den}' + return re.sub(r'\\frac\{([^{}]*)\}\{([^{}]*)\}', replace_frac, text) + + # 2. Raíces cuadradas: \sqrt{expr} → HTML + def replace_sqrt(text): + def replace_sqrt_match(match): + expr = match.group(1) + return f'{expr}' + return re.sub(r'\\sqrt\{([^{}]*)\}', replace_sqrt_match, text) + + # 3. Exponentes: ^{expr} y ^num → superíndices + def replace_superscripts(text): + # Exponentes con llaves: ^{...} + text = re.sub(r'\^\{([^{}]*)\}', r'\1', text) + # Exponentes simples: ^2, ^3, etc. + text = re.sub(r'\^([0-9]+)', r'\1', text) + # ** para exponentes + text = text.replace('**2', '²').replace('**3', '³') + text = re.sub(r'\*\*([0-9]+)', r'\1', text) + return text + + # 4. Subíndices: _{expr} → subíndices + def replace_subscripts(text): + text = re.sub(r'_\{([^{}]*)\}', r'\1', text) + text = re.sub(r'_([0-9]+)', r'\1', text) + return text + + # 5. Símbolos matemáticos + def replace_symbols(text): + replacements = { + 'pi': 'π', 'Pi': 'π', '\\pi': 'π', + 'alpha': 'α', '\\alpha': 'α', + 'beta': 'β', '\\beta': 'β', + 'gamma': 'γ', '\\gamma': 'γ', + 'delta': 'δ', '\\delta': 'δ', + 'theta': 'θ', '\\theta': 'θ', + 'lambda': 'λ', '\\lambda': 'λ', + 'sigma': 'σ', '\\sigma': 'σ', + 'omega': 'ω', '\\omega': 'ω' + } + for latex_sym, unicode_sym in replacements.items(): + text = text.replace(latex_sym, unicode_sym) + return text + + # Aplicar todas las transformaciones + formatted_math = replace_fractions(formatted_math) + formatted_math = replace_sqrt(formatted_math) + formatted_math = replace_superscripts(formatted_math) + formatted_math = replace_subscripts(formatted_math) + formatted_math = replace_symbols(formatted_math) + + block_html = f""" +
+
+
{formatted_math}
+
+
""" + html_blocks.append(block_html) + + # Generar HTML completo + equations_html = '\n'.join(html_blocks) + html_content = self._generate_base_html().replace( + '
\n
\n 📐 Panel de Ecuaciones
\n Las ecuaciones se mostrarán aquí automáticamente\n
\n
', + f'
\n{equations_html}\n
' + ) + + self.logger.debug(f"📄 HTML generado: {len(html_content)} caracteres") + self.logger.debug(f"📄 Ecuaciones HTML: {len(equations_html)} caracteres") + + # Actualizar el widget de renderizado + # Método 1: tkinterweb + if hasattr(self, 'latex_webview') and hasattr(self.latex_webview, 'load_html'): + try: + self.logger.debug("🌐 Intentando actualizar con tkinterweb...") + self.latex_webview.load_html(html_content) + # tkinterweb auto-renderiza KaTeX si está configurado correctamente en el HTML + self.logger.debug("✅ LaTeX actualizado via tkinterweb") + return + except Exception as e: + self.logger.error(f"❌ Error actualizando tkinterweb: {e}") + + # Método 2: pywebview + elif hasattr(self, 'latex_webview') and self._webview_type == "pywebview": + try: + self.logger.debug("🌐 Intentando actualizar con pywebview...") + if self.latex_webview: + self.latex_webview.load_html(html_content) + self.logger.debug("✅ LaTeX actualizado via pywebview") + return + else: + self.logger.debug("⚠️ latex_webview no disponible para pywebview") + except Exception as e: + self.logger.error(f"❌ Error actualizando pywebview: {e}") + + # Método 3: Fallback a texto plano + if hasattr(self, 'latex_fallback_text'): + try: + self.logger.debug("📝 Usando fallback a texto plano...") + # Limpiar y mostrar contenido como texto + self.latex_fallback_text.config(state="normal") + self.latex_fallback_text.delete('1.0', tk.END) + + if self._latex_equations: + content_lines = ["📐 Ecuaciones LaTeX:", ""] + + for eq_data in self._latex_equations: + eq_type = eq_data.get('type', 'symbolic') + latex_content = eq_data.get('latex', '') + original = eq_data.get('original', '') + + content_lines.append(f"[{eq_type.upper()}]") + content_lines.append(f"LaTeX: {latex_content}") + if original: + content_lines.append(f"Original: {original}") + content_lines.append("-" * 50) + content_lines.append("") + + self.latex_fallback_text.insert('1.0', '\n'.join(content_lines)) + else: + self.latex_fallback_text.insert('1.0', "📐 Panel de Ecuaciones LaTeX\n\nLas ecuaciones aparecerán aquí...") + + self.latex_fallback_text.config(state="disabled") + self.logger.debug("✅ LaTeX actualizado via texto plano") + return + except Exception as e: + self.logger.error(f"❌ Error actualizando texto plano: {e}") + + # Si llegamos aquí, no hay widget disponible + self.logger.error("❌ No hay widget de renderizado disponible (tkinterweb, pywebview o fallback)") + + except Exception as e: + self.logger.error(f"❌ Error crítico actualizando panel LaTeX: {e}") + import traceback + self.logger.error(traceback.format_exc()) + + def _add_to_latex_panel(self, content_type: str, latex_content: str): + """Añade una ecuación al panel LaTeX""" + if not hasattr(self, '_latex_equations'): + self._latex_equations = [] + + # 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() + } + + self._latex_equations.append(equation_data) + + # 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"➕ Añadida ecuación {content_type}: {latex_content[:50]}... (Total: {len(self._latex_equations)})") + + # Actualizar indicador visual + self._update_content_indicator() + + # IMPORTANTE: Actualizar panel si está visible + if self.latex_panel_visible: + self.logger.debug("🔄 Panel visible - actualizando contenido...") + self._update_latex_panel() + else: + self.logger.debug("👁️ Panel no visible - saltando actualización") + + self.logger.debug(f"✅ Proceso de añadir ecuación completado") + + 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 main(): """Función principal""" diff --git a/main_evaluation_puro.py b/main_evaluation_puro.py index d928dcd..6e20822 100644 --- a/main_evaluation_puro.py +++ b/main_evaluation_puro.py @@ -396,7 +396,16 @@ class PureAlgebraicEngine: # Intentar convertir a objeto Ecuación de sympy # Usar _get_complete_context para incluir 'last' y otras variables eval_context = self._get_complete_context() - equation_obj = sympify(line, locals=eval_context, parse_function=lambda s: Eq(*s.split('=',1))) + + # Dividir la ecuación manualmente para evitar problemas de parsing + parts = line.split('=', 1) + if len(parts) != 2: + raise ValueError("Ecuación debe tener exactamente un signo '='") + + left_str, right_str = parts[0].strip(), parts[1].strip() + left_expr = sympify(left_str, locals=eval_context) + right_expr = sympify(right_str, locals=eval_context) + equation_obj = Eq(left_expr, right_expr) if not isinstance(equation_obj, sp.Equality): # Si no se pudo parsear como Eq(LHS,RHS), tratar como expresión que contiene un igual (posible error o comparación) diff --git a/requirements.txt b/requirements.txt index 150af77..66cd75f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,11 +12,14 @@ sympy>=1.12 matplotlib>=3.7.0 numpy>=1.24.0 +# Panel de ecuaciones LaTeX renderizadas +pywebview>=4.0.0 + # Opcional: Para ayuda mejorada con Markdown markdown>=3.4.0 -# Opcional: Para visor HTML en ayuda -# tkinterweb>=3.24.0 # Descomenta si quieres soporte HTML completo +# Opcional: Para visor HTML/LaTeX en el tercer panel (recomendado) +tkinterweb>=3.24.0 # Para renderizado LaTeX con KaTeX # tkhtmlview>=0.2.0 # Alternativa para HTML # Testing (opcional)