4247 lines
174 KiB
Python
4247 lines
174 KiB
Python
"""
|
||
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: {'Sí' if content_success else 'No'}\n"
|
||
f"✓ Funciones MathJax: {'Sí' if success else 'No'}\n\n"
|
||
"Para ver logs detallados:\n"
|
||
"• Revisa la consola de la aplicación\n"
|
||
"• O usa 'Abrir HTML en Navegador' + F12"
|
||
)
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"Error en test tkinterweb: {e}")
|
||
messagebox.showerror("Error", f"Error en test tkinterweb:\n{e}")
|
||
|
||
def _save_html_debug_copy(self, html_content):
|
||
"""Guarda una copia del HTML generado para debugging en navegador"""
|
||
try:
|
||
# Usar nombre simple para fácil acceso
|
||
filename = "latex_debug.html"
|
||
|
||
# Guardar en el directorio de la aplicación
|
||
with open(filename, 'w', encoding='utf-8') as f:
|
||
f.write(html_content)
|
||
|
||
# Actualizar referencia al último archivo generado
|
||
self._last_debug_html = filename
|
||
|
||
self.logger.debug(f"🔍 HTML de debug guardado: {filename}")
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"❌ Error guardando HTML de debug: {e}")
|
||
|
||
def _open_debug_html_in_browser(self):
|
||
"""Abre el último HTML de debug en el navegador del sistema"""
|
||
try:
|
||
if not hasattr(self, '_last_debug_html') or not self._last_debug_html:
|
||
messagebox.showwarning("Sin archivo", "No hay archivo HTML de debug disponible")
|
||
return
|
||
|
||
if not os.path.exists(self._last_debug_html):
|
||
messagebox.showerror("Archivo no encontrado", f"El archivo {self._last_debug_html} no existe")
|
||
return
|
||
|
||
import webbrowser
|
||
file_path = os.path.abspath(self._last_debug_html)
|
||
file_url = f"file:///{file_path.replace('\\', '/')}"
|
||
|
||
webbrowser.open(file_url)
|
||
|
||
messagebox.showinfo(
|
||
"HTML Abierto",
|
||
f"Archivo abierto en navegador:\n{self._last_debug_html}\n\n"
|
||
f"Presiona F12 para ver la consola del navegador y depurar MathJax.\n\n"
|
||
f"NUEVA CONFIGURACIÓN:\n"
|
||
f"• Sin polyfill.io (evita errores SSL)\n"
|
||
f"• Diagnóstico mejorado en consola\n"
|
||
f"• Función global: forzarRenderizado()"
|
||
)
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("Error", f"Error abriendo archivo en navegador:\n{e}")
|
||
|
||
def _show_latex_panel_status(self):
|
||
"""Muestra el estado actual del panel LaTeX"""
|
||
try:
|
||
# Información básica
|
||
panel_exists = hasattr(self, 'latex_panel')
|
||
panel_visible = hasattr(self, 'latex_panel_visible') and self.latex_panel_visible
|
||
webview_exists = hasattr(self, 'latex_webview') and self.latex_webview
|
||
equations_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0
|
||
|
||
# Información sobre las ecuaciones
|
||
equations_info = ""
|
||
if hasattr(self, '_latex_equations') and self._latex_equations:
|
||
equations_info = "\n\nECUACIONES EN MEMORIA:\n"
|
||
for i, eq in enumerate(self._latex_equations[:5]): # Mostrar solo las primeras 5
|
||
eq_type = eq.get('type', 'unknown')
|
||
latex_content = eq.get('latex', '')
|
||
equations_info += f"{i+1}. [{eq_type}] {latex_content[:50]}...\n"
|
||
if len(self._latex_equations) > 5:
|
||
equations_info += f"... y {len(self._latex_equations) - 5} más\n"
|
||
|
||
# Estado de archivos
|
||
debug_file_exists = os.path.exists("latex_debug.html")
|
||
debug_file_info = ""
|
||
if debug_file_exists:
|
||
stat = os.stat("latex_debug.html")
|
||
import datetime
|
||
mod_time = datetime.datetime.fromtimestamp(stat.st_mtime)
|
||
debug_file_info = f"\nArchivo debug: latex_debug.html\nÚltima modificación: {mod_time.strftime('%H:%M:%S')}"
|
||
|
||
status_message = f"""ESTADO DEL PANEL LATEX
|
||
|
||
COMPONENTES:
|
||
• Panel creado: {'✓' if panel_exists else '✗'}
|
||
• Panel visible: {'✓' if panel_visible else '✗'}
|
||
• WebView tkinterweb: {'✓' if webview_exists else '✗'}
|
||
• JavaScript disponible: {'✓' if self._js_available else '✗'}
|
||
|
||
CONTENIDO:
|
||
• Ecuaciones en memoria: {equations_count}
|
||
• Archivo debug existe: {'✓' if debug_file_exists else '✗'}{debug_file_info}
|
||
|
||
WEBVIEW TYPE: {getattr(self, '_webview_type', 'No definido')}
|
||
WEBVIEW AVAILABLE: {getattr(self, '_webview_available', 'No definido')}{equations_info}
|
||
|
||
PARA SOLUCIONAR:
|
||
1. Si las ecuaciones están en memoria pero no se ven:
|
||
→ Usa 'Test tkinterweb' para diagnosticar
|
||
2. Si el archivo debug tiene contenido pero tkinterweb no:
|
||
→ Puede ser problema de renderizado en tkinterweb
|
||
3. Revisa el archivo debug con 'Abrir HTML en Navegador'"""
|
||
|
||
# Crear ventana de estado
|
||
status_window = tk.Toplevel(self.root)
|
||
status_window.title("Estado Panel LaTeX")
|
||
status_window.geometry("600x500")
|
||
status_window.configure(bg="#2b2b2b")
|
||
|
||
text_widget = scrolledtext.ScrolledText(
|
||
status_window,
|
||
font=("Consolas", 10),
|
||
bg="#1e1e1e",
|
||
fg="#d4d4d4",
|
||
wrap=tk.WORD
|
||
)
|
||
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
text_widget.insert("1.0", status_message)
|
||
text_widget.config(state="disabled")
|
||
|
||
# Botón de cerrar
|
||
close_btn = tk.Button(
|
||
status_window,
|
||
text="Cerrar",
|
||
command=status_window.destroy,
|
||
bg="#3c3c3c",
|
||
fg="white",
|
||
relief=tk.FLAT
|
||
)
|
||
close_btn.pack(pady=5)
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"Error mostrando estado del panel: {e}")
|
||
messagebox.showerror("Error", f"Error mostrando estado:\n{e}")
|
||
|
||
def _force_html_update(self):
|
||
"""Fuerza actualización del panel LaTeX y generación de nuevo HTML de debug"""
|
||
try:
|
||
if hasattr(self, '_latex_equations') and self._latex_equations:
|
||
self.logger.info("🔄 Forzando actualización del panel LaTeX...")
|
||
|
||
# Forzar actualización del panel
|
||
if self.latex_panel_visible:
|
||
self._update_latex_panel()
|
||
else:
|
||
# Si no está visible, mostrarlo temporalmente para generar HTML
|
||
self._show_latex_panel()
|
||
self.root.after(1000, lambda: self._update_latex_panel())
|
||
|
||
messagebox.showinfo(
|
||
"Actualización Forzada",
|
||
f"Panel LaTeX actualizado.\n\n"
|
||
f"Ecuaciones en panel: {len(self._latex_equations)}\n"
|
||
f"Nuevo HTML generado: latex_debug.html\n\n"
|
||
f"Usa 'Abrir HTML en Navegador' para ver el resultado."
|
||
)
|
||
else:
|
||
messagebox.showwarning(
|
||
"Sin contenido",
|
||
"No hay ecuaciones en el panel LaTeX para actualizar.\n\n"
|
||
"Escribe algunas ecuaciones en la calculadora primero."
|
||
)
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"Error forzando actualización: {e}")
|
||
messagebox.showerror("Error", f"Error forzando actualización:\n{e}")
|
||
|
||
def _add_to_latex_panel(self, content_type: str, latex_content: str):
|
||
"""Añade una ecuación al panel LaTeX"""
|
||
self.logger.debug(f"🔧 AÑADIENDO al panel LaTeX: tipo='{content_type}', contenido='{latex_content[:100]}...'")
|
||
|
||
if not hasattr(self, '_latex_equations'):
|
||
self._latex_equations = []
|
||
self.logger.debug(" -> Lista de ecuaciones inicializada")
|
||
|
||
# Crear datos de la ecuación
|
||
equation_data = {
|
||
'type': content_type,
|
||
'latex': latex_content,
|
||
'original': latex_content, # Guardar contenido original también
|
||
'timestamp': time.time()
|
||
}
|
||
|
||
before_count = len(self._latex_equations)
|
||
self._latex_equations.append(equation_data)
|
||
after_count = len(self._latex_equations)
|
||
|
||
# Limitar número de ecuaciones (opcional)
|
||
max_equations = 50
|
||
if len(self._latex_equations) > max_equations:
|
||
self._latex_equations = self._latex_equations[-max_equations:]
|
||
self.logger.debug(f" -> Lista limitada a {max_equations} ecuaciones")
|
||
|
||
self.logger.debug(f"➕ Ecuación añadida: {content_type} (Total: {before_count} -> {after_count})")
|
||
|
||
# Actualizar indicador visual
|
||
self._update_content_indicator()
|
||
|
||
# IMPORTANTE: Actualizar panel si está visible
|
||
panel_visible = hasattr(self, 'latex_panel_visible') and self.latex_panel_visible
|
||
|
||
self.logger.debug(f"🔍 Estado del panel: existe={hasattr(self, 'latex_panel_visible')}, visible={panel_visible}")
|
||
|
||
if panel_visible:
|
||
self.logger.debug("🔄 Panel visible - FORZANDO actualización de contenido...")
|
||
self._update_latex_panel()
|
||
else:
|
||
self.logger.debug("👁️ Panel no visible - saltando actualización (ecuación guardada para cuando se abra)")
|
||
|
||
self.logger.debug(f"✅ Proceso de añadir ecuación COMPLETADO")
|
||
self.logger.debug(f"📊 Estado final: {len(self._latex_equations)} ecuaciones en total")
|
||
|
||
|
||
def main():
|
||
"""Función principal"""
|
||
root = tk.Tk()
|
||
app = HybridCalculatorApp(root)
|
||
|
||
try:
|
||
root.iconname("Calculadora MAV CAS")
|
||
except tk.TclError:
|
||
pass
|
||
|
||
root.mainloop()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |