Calc/main_calc_app.py

4247 lines
174 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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("<KeyRelease>", self.on_key_release)
self.input_text.bind("<KeyPress>", self.on_key_press)
self.input_text.bind("<Button-1>", self._on_input_click)
self.input_text.bind("<FocusIn>", lambda e: self._close_autocomplete_popup())
# Bindings para navegación del autocompletado
self.input_text.bind("<Up>", self._handle_arrow_key)
self.input_text.bind("<Down>", self._handle_arrow_key)
self.input_text.bind("<Tab>", self._handle_tab_key)
self.input_text.bind("<Escape>", 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("<Button-3>", lambda e: self._show_context_menu(e, "input"))
self.output_text.bind("<Button-3>", 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("<Button-3>", 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"""
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ecuaciones LaTeX</title>
<!-- MathJax 3 configuración ESPECÍFICA para tkinterweb -->
<script>
// Configuración MathJax simplificada y compatible con tkinterweb
window.MathJax = {
tex: {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']],
processEscapes: true
},
options: {
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
},
startup: {
document: document,
ready: function () {
console.log('🚀 [tkinterweb] Iniciando MathJax...');
// Para tkinterweb: configuración más directa
MathJax.startup.defaultReady();
console.log('✅ [tkinterweb] MathJax configurado');
// Para tkinterweb: renderizado inmediato y sincronizado
var statusEl = document.getElementById('mathjax-status');
if (statusEl) {
statusEl.innerHTML = '⏳ Preparando renderizado...';
statusEl.style.color = '#ffcc02';
}
// Renderizado específico para tkinterweb
setTimeout(function() {
console.log('🔄 [tkinterweb] Iniciando renderizado...');
renderizarParaTkinterweb();
}, 500);
}
}
};
// Función específica para renderizado en tkinterweb
function renderizarParaTkinterweb() {
console.log('🎯 [tkinterweb] Función de renderizado específica');
if (typeof MathJax === 'undefined') {
console.log('❌ [tkinterweb] MathJax no disponible');
actualizarEstado('❌ MathJax no cargado', '#f44747');
return;
}
if (typeof MathJax.typesetPromise === 'undefined') {
console.log('❌ [tkinterweb] typesetPromise no disponible');
actualizarEstado('❌ Funciones MathJax incompletas', '#f44747');
return;
}
// Verificar elementos a renderizar
var mathElements = document.querySelectorAll('.math-display');
console.log('📄 [tkinterweb] Elementos encontrados:', mathElements.length);
if (mathElements.length === 0) {
console.log(' [tkinterweb] No hay elementos para renderizar');
actualizarEstado(' Sin ecuaciones', '#808080');
return;
}
// Log de contenido para debug
for (var i = 0; i < Math.min(mathElements.length, 3); i++) {
console.log('📝 [tkinterweb] Elemento ' + i + ':', mathElements[i].innerHTML.substring(0, 50) + '...');
}
// Renderizar con manejo específico para tkinterweb
console.log('🚀 [tkinterweb] Ejecutando typesetPromise...');
try {
MathJax.typesetPromise().then(function() {
console.log('🎉 [tkinterweb] ¡Renderizado exitoso!');
actualizarEstado('' + mathElements.length + ' ecuaciones renderizadas', '#4fc3f7');
// Verificación post-renderizado específica para tkinterweb
setTimeout(function() {
verificarRenderizadoTkinterweb();
}, 1000);
}).catch(function(err) {
console.log('❌ [tkinterweb] Error en renderizado:', err);
actualizarEstado('❌ Error: ' + err.message, '#f44747');
});
} catch (e) {
console.log('❌ [tkinterweb] Excepción en typesetPromise:', e);
actualizarEstado('❌ Excepción: ' + e.message, '#f44747');
}
}
// Verificación específica para tkinterweb
function verificarRenderizadoTkinterweb() {
console.log('🔍 [tkinterweb] Verificando renderizado...');
var mjElements = document.querySelectorAll('mjx-container, .MathJax, mjx-math');
console.log('🧮 [tkinterweb] Elementos MathJax encontrados:', mjElements.length);
if (mjElements.length > 0) {
console.log('✅ [tkinterweb] Renderizado confirmado');
actualizarEstado('✅ Confirmado: ' + mjElements.length + ' elementos', '#4fc3f7');
} else {
console.log('⚠️ [tkinterweb] Sin elementos renderizados detectados');
actualizarEstado('⚠️ Renderizado no detectado', '#ffcc02');
}
}
// Función auxiliar para actualizar estado
function actualizarEstado(mensaje, color) {
var statusEl = document.getElementById('mathjax-status');
if (statusEl) {
statusEl.innerHTML = mensaje;
statusEl.style.color = color;
}
}
// Función para debug manual específica para tkinterweb
function forzarRenderizadoTkinterweb() {
console.log('🔧 [tkinterweb] Forzando re-renderizado...');
actualizarEstado('🔄 Forzando renderizado...', '#ffcc02');
renderizarParaTkinterweb();
}
// Exponer función global
window.forzarRenderizadoTkinterweb = forzarRenderizadoTkinterweb;
window.forzarRenderizado = forzarRenderizadoTkinterweb; // Alias
// Timeout específico para tkinterweb (más largo)
var timeoutId = setTimeout(function() {
console.log('⚠️ [tkinterweb] Timeout de carga (20 segundos)');
actualizarEstado('⚠️ Timeout - Posible problema de red', '#ffcc02');
}, 20000);
// Event listener para tkinterweb
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
console.log('🌐 [tkinterweb] DOM cargado');
clearTimeout(timeoutId);
});
} else {
console.log('🌐 [tkinterweb] DOM ya cargado');
clearTimeout(timeoutId);
}
</script>
<!-- MathJax 3 carga optimizada para tkinterweb -->
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"
onload="console.log('📦 [tkinterweb] MathJax script cargado')"
onerror="console.log('❌ [tkinterweb] Error cargando MathJax script')"></script>
<!-- Fuentes matemáticas mejoradas -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=STIX+Two+Math&family=Computer+Modern+Serif:wght@400;700&display=swap" rel="stylesheet">
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Computer Modern Serif', 'STIX Two Math', 'Times New Roman', serif;
font-size: 13px;
margin: 0;
padding: 4px;
line-height: 1.4;
}
.equation-block {
margin: 2px 0;
padding: 6px 8px;
background-color: #1e1e1e;
border-left: 3px solid #80c7f7;
border-radius: 3px;
word-wrap: break-word;
min-height: auto;
transition: background-color 0.2s ease;
}
.equation-block:hover {
background-color: #252525;
}
.equation-content {
font-size: 15px;
color: #ffffff;
line-height: 1.5;
margin: 0;
padding: 0;
}
.math-display {
font-size: 16px;
text-align: left;
margin: 4px 0;
padding: 4px 6px;
background-color: #252525;
border-radius: 3px;
min-height: auto;
font-family: 'STIX Two Math', 'Computer Modern Serif', serif;
}
/* Tipos de ecuaciones con colores específicos */
.assignment {
border-left-color: #dcdcaa;
}
.equation {
border-left-color: #c586c0;
}
.comment {
border-left-color: #6a9955;
font-style: italic;
}
.symbolic {
border-left-color: #9cdcfe;
}
/* Mensaje de información mejorado */
.info-message {
text-align: center;
color: #80c7f7;
font-style: italic;
margin: 15px;
padding: 12px;
border: 1px dashed #80c7f7;
border-radius: 6px;
font-size: 13px;
background-color: #1e1e1e;
}
/* Fallback mejorado para navegadores sin MathJax */
.fallback-math {
font-family: 'STIX Two Math', 'Computer Modern Serif', 'Times New Roman', serif;
font-size: 16px;
line-height: 1.6;
}
/* Representación matemática mejorada para fallback */
.frac {
display: inline-block;
vertical-align: middle;
text-align: center;
font-size: 1.1em;
margin: 0 3px;
}
.frac .num {
display: block;
border-bottom: 1px solid #fff;
padding-bottom: 2px;
line-height: 1.1;
}
.frac .den {
display: block;
padding-top: 2px;
line-height: 1.1;
}
.sqrt {
position: relative;
padding-left: 18px;
border-top: 1px solid #fff;
}
.sqrt::before {
content: "";
position: absolute;
left: 0;
top: -3px;
font-size: 18px;
font-weight: bold;
}
/* Subíndices y superíndices mejorados */
sub {
font-size: 0.75em;
line-height: 0;
position: relative;
vertical-align: baseline;
bottom: -0.25em;
}
sup {
font-size: 0.75em;
line-height: 0;
position: relative;
vertical-align: baseline;
top: -0.5em;
}
/* Símbolos matemáticos especiales */
.math-symbol {
font-family: 'STIX Two Math', serif;
font-size: 1.2em;
}
/* Indicador de renderizado */
.render-status {
font-size: 11px;
color: #808080;
margin-top: 5px;
text-align: right;
}
/* Animación para ecuaciones nuevas */
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.equation-block.new {
animation: slideIn 0.3s ease-out;
}
/* Espaciador */
.spacer {
height: 8px;
}
</style>
</head>
<body>
<div id="equations-container">
<div class="info-message">
📐 Panel de Ecuaciones LaTeX<br/>
<small>Renderizado con MathJax + fuentes matemáticas</small><br/>
Las ecuaciones se mostrarán aquí automáticamente
</div>
</div>
<script>
// Función mejorada para formatear expresiones matemáticas (fallback si MathJax falla)
function formatMathExpression(latex) {
// Si MathJax está disponible, usarlo
if (window.MathJax && window.MathJax.tex2chtmlPromise) {
return latex; // MathJax se encargará
}
// Fallback: conversiones básicas para HTML
// Convertir fracciones LaTeX a HTML mejorado
latex = latex.replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g,
'<span class="frac"><span class="num">$1</span><span class="den">$2</span></span>');
// Convertir raíces cuadradas
latex = latex.replace(/\\sqrt\{([^}]+)\}/g,
'<span class="sqrt">$1</span>');
// Exponentes
latex = latex.replace(/\^2/g, '²');
latex = latex.replace(/\^3/g, '³');
latex = latex.replace(/\^([0-9]+)/g, '<sup>$1</sup>');
latex = latex.replace(/\^\{([^}]+)\}/g, '<sup>$1</sup>');
// Subíndices
latex = latex.replace(/_([0-9]+)/g, '<sub>$1</sub>');
latex = latex.replace(/_{([^}]+)}/g, '<sub>$1</sub>');
// Símbolos griegos comunes
const greekSymbols = {
'\\alpha': 'α', '\\beta': 'β', '\\gamma': 'γ', '\\delta': 'δ',
'\\epsilon': 'ε', '\\zeta': 'ζ', '\\eta': 'η', '\\theta': 'θ',
'\\iota': 'ι', '\\kappa': 'κ', '\\lambda': 'λ', '\\mu': 'μ',
'\\nu': 'ν', '\\xi': 'ξ', '\\pi': 'π', '\\rho': 'ρ',
'\\sigma': 'σ', '\\tau': 'τ', '\\upsilon': 'υ', '\\phi': 'φ',
'\\chi': 'χ', '\\psi': 'ψ', '\\omega': 'ω'
};
for (const [latexSym, unicodeSym] of Object.entries(greekSymbols)) {
latex = latex.replace(new RegExp(latexSym, 'g'), unicodeSym);
}
// Operadores matemáticos
latex = latex.replace(/\\cdot/g, '·');
latex = latex.replace(/\\times/g, '×');
latex = latex.replace(/\\div/g, '÷');
latex = latex.replace(/\\pm/g, '±');
latex = latex.replace(/\\mp/g, '');
latex = latex.replace(/\\infty/g, '');
return latex;
}
// Función para limpiar ecuaciones
function clearEquations() {
const container = document.getElementById('equations-container');
container.innerHTML = '';
}
// Función para re-renderizar MathJax cuando se agrega contenido
function rerenderMath() {
if (window.MathJax && window.MathJax.typesetPromise) {
window.MathJax.typesetPromise().catch(function (err) {
console.log('Error renderizando matemáticas:', err.message);
});
}
}
// Detectar si MathJax se cargó correctamente
window.addEventListener('load', function() {
setTimeout(function() {
const status = document.querySelector('.render-status');
if (window.MathJax) {
if (status) status.textContent = '✓ MathJax activo';
} else {
if (status) status.textContent = '⚠ Fallback HTML';
}
}, 1000);
});
</script>
</body>
</html>
"""
def _generate_html_for_pywebview(self):
"""HTML optimizado específicamente para pywebview (más simple y robusto)"""
return r"""
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ecuaciones LaTeX - PyWebView</title>
<!-- MathJax 3 configuración optimizada para pywebview -->
<script>
window.MathJax = {
tex: {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']],
processEscapes: true
},
options: {
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
},
startup: {
ready: function () {
console.log('🚀 [pywebview] Iniciando MathJax...');
MathJax.startup.defaultReady();
console.log('✅ [pywebview] MathJax listo');
// Auto-renderizar después de carga
setTimeout(function() {
if (MathJax.typesetPromise) {
MathJax.typesetPromise().then(function() {
console.log('🎉 [pywebview] Renderizado automático completado');
}).catch(function(err) {
console.log('❌ [pywebview] Error en renderizado:', err);
});
}
}, 500);
}
}
};
</script>
<!-- MathJax 3 CDN -->
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 13px;
margin: 0;
padding: 8px;
line-height: 1.4;
}
.equation-block {
margin: 4px 0;
padding: 8px 10px;
background-color: #2d2d2d;
border-left: 3px solid #80c7f7;
border-radius: 4px;
word-wrap: break-word;
transition: background-color 0.2s ease;
}
.equation-block:hover {
background-color: #3a3a3a;
}
.equation-content {
font-size: 14px;
color: #ffffff;
line-height: 1.5;
}
.math-display {
font-size: 15px;
text-align: left;
margin: 2px 0;
padding: 4px;
background-color: #252525;
border-radius: 3px;
min-height: 20px;
}
/* Tipos de ecuaciones */
.assignment { border-left-color: #dcdcaa; }
.equation { border-left-color: #c586c0; }
.comment { border-left-color: #6a9955; font-style: italic; }
.symbolic { border-left-color: #9cdcfe; }
.info-message {
text-align: center;
color: #80c7f7;
font-style: italic;
margin: 20px;
padding: 15px;
border: 1px dashed #80c7f7;
border-radius: 8px;
font-size: 14px;
background-color: #2d2d2d;
}
.status {
font-size: 11px;
color: #808080;
text-align: center;
margin-top: 10px;
padding: 5px;
}
/* Animación suave */
.equation-block {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<div id="equations-container">
<div class="info-message">
📐 Panel de Ecuaciones LaTeX<br/>
<small>Renderizado con MathJax en PyWebView</small><br/>
Las ecuaciones aparecerán aquí automáticamente
</div>
</div>
<div class="status" id="status">
✓ PyWebView activo - MathJax cargándose...
</div>
</body>
</html>
"""
def _generate_html_fallback(self):
"""HTML fallback simple sin JavaScript"""
return r"""
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ecuaciones LaTeX (Modo Fallback)</title>
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Times New Roman', serif;
font-size: 14px;
margin: 0;
padding: 8px;
line-height: 1.5;
}
.equation-block {
margin: 4px 0;
padding: 8px 10px;
background-color: #1e1e1e;
border-left: 3px solid #80c7f7;
border-radius: 4px;
word-wrap: break-word;
font-family: 'Times New Roman', serif;
}
.assignment {
border-left-color: #dcdcaa;
}
.equation {
border-left-color: #c586c0;
}
.symbolic {
border-left-color: #9cdcfe;
}
.comment {
border-left-color: #6a9955;
font-style: italic;
}
.math-text {
font-family: 'Times New Roman', serif;
font-size: 16px;
color: #ffffff;
line-height: 1.6;
}
/* Formateo matemático básico sin MathJax */
.superscript {
font-size: 0.8em;
vertical-align: super;
}
.subscript {
font-size: 0.8em;
vertical-align: sub;
}
.fraction {
display: inline-block;
text-align: center;
vertical-align: middle;
margin: 0 2px;
}
.fraction .numerator {
display: block;
border-bottom: 1px solid #fff;
padding-bottom: 1px;
}
.fraction .denominator {
display: block;
padding-top: 1px;
}
.sqrt-symbol {
font-size: 1.2em;
margin-right: 2px;
}
.info-message {
text-align: center;
color: #ffcc02;
font-style: italic;
margin: 15px;
padding: 12px;
border: 1px dashed #ffcc02;
border-radius: 6px;
font-size: 13px;
background-color: #2d2d2d;
}
.fallback-status {
font-size: 11px;
color: #808080;
text-align: right;
margin-top: 10px;
padding: 5px;
border-top: 1px solid #3a3a3a;
}
</style>
</head>
<body>
<div id="equations-container">
<div class="info-message">
📐 Panel de Ecuaciones LaTeX (Modo Fallback)<br/>
<small>Renderizado texto mejorado - Sin JavaScript/MathJax</small><br/>
Las ecuaciones se mostrarán en formato texto matemático mejorado
</div>
</div>
<div class="fallback-status">
Modo Fallback - Matemáticas en Texto Mejorado<br/>
Para MathJax completo: pip install tkinterweb[javascript]
</div>
</body>
</html>
"""
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("<MouseWheel>", _unified_mouse_wheel)
self.output_text.bind("<MouseWheel>", _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("<Double-Button-1>",
lambda e: self._select_variable())
# Binding para cerrar si se hace click fuera
self.root.bind("<Button-1>", 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("<Double-Button-1>",
lambda e: self._select_autocomplete())
# Binding para cerrar si se hace click fuera
self.root.bind("<Button-1>", 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("<Button-1>")
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("<<Cut>>"))
context_menu.add_command(label="Copiar", command=lambda: self.input_text.event_generate("<<Copy>>"))
context_menu.add_command(label="Pegar", command=lambda: self.input_text.event_generate("<<Paste>>"))
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 = """
<style>
h1, h2, h3, h4, h5, h6 {
color: #569cd6;
margin-top: 1.2em;
margin-bottom: 0.6em;
border-bottom: 1px solid #3a3a3a;
padding-bottom: 0.3em;
}
h1 { font-size: 1.8em; }
h2 { font-size: 1.5em; }
h3 { font-size: 1.3em; }
p {
line-height: 1.65;
margin-bottom: 0.8em;
}
a {
color: #4fc3f7;
text-decoration: none;
}
a:hover {
color: #80dfff;
text-decoration: underline;
}
code {
font-family: "Consolas", "Courier New", monospace;
background-color: #2d2d2d;
color: #ce9178;
padding: 3px 6px;
border-radius: 4px;
border: 1px solid #3c3c3c;
font-size: 0.95em;
}
pre {
background-color: #1e1e1e;
border: 1px solid #3c3c3c;
border-radius: 4px;
padding: 12px;
overflow-x: auto;
margin: 1em 0;
}
pre > code {
background-color: transparent !important;
color: inherit !important;
padding: 0 !important;
border-radius: 0 !important;
border: none !important;
font-size: 1em !important;
}
ul, ol { padding-left: 25px; color: #c8c8c8; }
li { margin-bottom: 0.4em; }
hr { border: 0; height: 1px; background: #3a3a3a; margin: 1.5em 0; }
table { border-collapse: collapse; width: 90%; margin: 1em auto; }
th, td { border: 1px solid #4a4a4a; padding: 8px; text-align: left; }
th { background-color: #2d2d2d; color: #9cdcfe; font-weight: bold; }
td { background-color: #1e1e1e; }
blockquote {
border-left: 4px solid #569cd6;
margin: 1em 0;
padding: 0.5em 1em;
background-color: #2d2d30;
font-style: italic;
}
</style>
"""
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"""
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Ayuda de Calculadora</title>
{dark_theme_css}
</head>
<body style="background-color: #1e1e1e; color: #d4d4d4; font-family: 'Segoe UI', sans-serif; font-size: 10pt; margin:0; padding:12px;">
{html_fragment}
</body>
</html>
"""
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"""
<div class="equation-block {eq_type}">
<div class="equation-content">
<div class="math-display">{formatted_latex}</div>
</div>
</div>"""
else:
# Sin LaTeX válido, mostrar texto original
block_html = f"""
<div class="equation-block {eq_type}">
<div class="equation-content">
<div class="math-display">{original_content or 'Sin contenido'}</div>
</div>
</div>"""
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 = '<div id="mathjax-status" class="render-status">⏳ Cargando MathJax...</div>'
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 = """
<div class="info-message">
📐 Panel de Ecuaciones LaTeX<br/>
<small>Renderizado con MathJax + fuentes matemáticas</small><br/>
Las ecuaciones se mostrarán aquí automáticamente
</div>
<div id="mathjax-status" class="render-status"> Sin ecuaciones</div>"""
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'<div id="equations-container">.*?</div>'
new_container = f'<div id="equations-container">\n{final_content}\n </div>'
# 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"""
<div class="equation-block {eq_type}">
<div class="equation-content">
<div class="math-display">{formatted_latex}</div>
</div>
</div>"""
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'(<div id="equations-container">)(.*?)(</div>)'
# Nuevo contenido del contenedor (escapar para regex)
new_container_content = f'<div id="equations-container">\\n{final_content}\\n</div>'
# 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"""
<div class="equation-block {eq_type}">
<div class="math-text">{display_content}</div>
</div>"""
html_blocks.append(block_html)
# Generar HTML completo
equations_html = '\n'.join(html_blocks)
html_content = self._generate_html_fallback().replace(
'<div id="equations-container">\n <div class="info-message">\n 📐 Panel de Ecuaciones LaTeX (Modo Fallback)<br/>\n <small>Renderizado sin JavaScript - PythonMonkey no disponible</small><br/>\n Las ecuaciones se mostrarán en formato texto mejorado\n </div>\n </div>',
f'<div id="equations-container">\n{equations_html}\n </div>'
)
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: {'' if content_success else 'No'}\n"
f"✓ Funciones MathJax: {'' 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()