Calc/main_calc_app.py

3159 lines
131 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
Calculadora MAV CAS Híbrida - Aplicación principal
VERSIÓN ADAPTADA AL NUEVO SISTEMA DE TIPOS
"""
import tkinter as tk
from tkinter import scrolledtext, messagebox, Menu, filedialog
import tkinter.font as tkFont
import json
import logging # <--- AÑADIDO
import os
from pathlib import Path
import threading
from typing import List, Dict, Any, Optional
import re
import time
# ========== IMPORTS PARA SISTEMA DE AYUDA ==========
# Para la ayuda en HTML
MARKDOWN_AVAILABLE = False
HTML_VIEWER_TYPE = None
try:
import markdown
MARKDOWN_AVAILABLE = True
except ImportError:
# markdown not available, MARKDOWN_AVAILABLE remains False
pass
# Intentar importar visores HTML
try:
import tkinterweb
HTML_VIEWER_TYPE = "tkinterweb"
except ImportError:
try:
from tkhtmlview import HTMLScrolledText
HTML_VIEWER_TYPE = "tkhtmlview"
except ImportError:
HTML_VIEWER_TYPE = None
# Usar logging para estas advertencias iniciales
module_logger = logging.getLogger(__name__)
if not MARKDOWN_AVAILABLE:
module_logger.warning("La librería 'markdown' no está instalada. La ayuda se mostrará en texto plano.")
if MARKDOWN_AVAILABLE and HTML_VIEWER_TYPE is None:
module_logger.warning("'markdown' está disponible, pero no se encontró un visor HTML (tkinterweb/tkhtmlview). La ayuda se mostrará en texto plano.")
# ========== IMPORTS ADAPTADOS AL NUEVO SISTEMA ==========
# Importar componentes del CAS híbrido con nuevo sistema de tipos
from main_evaluation_puro import PureAlgebraicEngine, EvaluationResult
from tl_popup import InteractiveResultManager, PlotResult
from type_registry import get_registered_helper_functions, get_registered_base_context
import sympy
from sympy_helper import SympyTools as SympyHelper
class HybridCalculatorApp:
"""Aplicación principal del CAS híbrido - ADAPTADA AL NUEVO SISTEMA"""
SETTINGS_FILE = "hybrid_calc_settings.json"
HISTORY_FILE = "hybrid_calc_history.txt"
HELP_FILE = "readme.md" # ========== NUEVO: Archivo de ayuda externo ==========
def __init__(self, root: tk.Tk):
self.root = root
# Configurar logging DEBUG para ver qué pasa
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.DEBUG)
# ========== INSTANCIAS DEL SISTEMA ==========
# Motor de evaluación (instancia única)
self.engine = PureAlgebraicEngine()
# Manager de contenido interactivo
self.interactive_manager = None # Se inicializará en setup_interactive_manager()
# ========== CONFIGURACIÓN DE INTERFAZ ==========
self.settings = self._load_settings()
self.debug = self.settings.get("debug_mode", False)
# ========== VARIABLES DE AUTOCOMPLETADO ==========
self._autocomplete_popup = None
self._autocomplete_listbox = None
self._autocomplete_active = False
self._autocomplete_suggestions = []
self._autocomplete_filter_text = ""
self._autocomplete_trigger_pos = ""
self._popup_disabled_until_next_dot = False
self._variable_popup_active = False
self._last_navigation_time = 0
# ========== VARIABLES PANEL LATEX ==========
self.latex_panel_visible = False
self._latex_equations = []
self.latex_renderer = None
self._webview_available = False
self._webview_type = None
# ========== 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"""
try:
# Frame para el panel LaTeX (crear pero no agregar al PanedWindow todavía)
self.latex_panel = tk.Frame(self.root, bg="#1a1a1a", bd=1, relief=tk.SOLID)
# Título del panel
title_frame = tk.Frame(self.latex_panel, bg="#1a1a1a", height=25)
title_frame.pack(fill=tk.X, pady=(2, 0))
title_frame.pack_propagate(False)
title_label = tk.Label(
title_frame,
text="📐 Ecuaciones & Asignaciones",
bg="#1a1a1a",
fg="#80c7f7",
font=("Consolas", 9, "bold"),
anchor=tk.W
)
title_label.pack(side=tk.LEFT, padx=5, pady=2)
# Botón de cerrar en el título
close_button = tk.Button(
title_frame,
text="",
font=("Arial", 8),
bg="#1a1a1a",
fg="#808080",
activebackground="#ff6b6b",
activeforeground="white",
bd=0,
relief=tk.FLAT,
cursor="hand2",
command=self._hide_latex_panel
)
close_button.pack(side=tk.RIGHT, padx=5)
# Intentar diferentes opciones de webview en orden de preferencia
self._webview_available = False
self._webview_type = None
self.latex_webview = None
self.latex_fallback_text = None
# Opción 1: tkinterweb (más compatible con tkinter)
try:
import tkinterweb
# Crear el visor HTML con tkinterweb
html_frame = tk.Frame(self.latex_panel, bg="#1a1a1a")
html_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
self.latex_webview = tkinterweb.HtmlFrame(
html_frame,
messages_enabled=False
)
self.latex_webview.pack(fill=tk.BOTH, expand=True)
# Cargar HTML base
base_html = self._generate_base_html()
self.latex_webview.load_html(base_html)
self._webview_available = True
self._webview_type = "tkinterweb"
except ImportError:
# Opción 2: pywebview (menos integrado pero más completo)
try:
import webview
# Crear el contenedor para el webview
webview_frame = tk.Frame(self.latex_panel, bg="#1a1a1a")
webview_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
# Inicializar contenido HTML base
self._current_html = self._generate_base_html()
# El webview se creará de forma lazy cuando sea necesario
self.latex_webview_frame = webview_frame
self.latex_webview = None
self._webview_available = True
self._webview_type = "pywebview"
except ImportError:
# Opción 3: Fallback con Text widget
self._webview_available = False
self._webview_type = "fallback"
fallback_text = scrolledtext.ScrolledText(
self.latex_panel,
font=("Consolas", 10),
bg="#1a1a1a",
fg="#d4d4d4",
state="disabled",
wrap=tk.WORD,
borderwidth=0,
highlightthickness=0,
relief=tk.FLAT
)
fallback_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
self.latex_fallback_text = fallback_text
# Mostrar mensaje informativo
fallback_text.config(state="normal")
fallback_text.insert("1.0", "Panel de Ecuaciones LaTeX\n\n"
"Para ver ecuaciones renderizadas, instala:\n"
"pip install tkinterweb\n"
"o pip install pywebview\n\n"
"Las ecuaciones aparecerán aquí en formato texto:")
fallback_text.config(state="disabled")
# Configurar tamaño del panel
self._min_latex_pane_width = 300
# Mostrar información sobre el tipo de visor usado
if self.debug:
self.logger.info(f"Panel LaTeX expandible configurado con: {self._webview_type}")
except Exception as e:
self.logger.error(f"Error configurando panel LaTeX expandible: {e}")
# Si falla completamente, no agregar el tercer panel
pass
def _refresh_latex_content(self):
"""Refresca el contenido del panel LaTeX cuando se muestra"""
if not hasattr(self, 'latex_panel') or not self.latex_panel_visible:
return
try:
if self._webview_available:
if self._webview_type == "tkinterweb":
self._update_tkinterweb()
elif self._webview_type == "pywebview":
self._update_pywebview()
else:
# Refrescar contenido de fallback
if hasattr(self, 'latex_fallback_text'):
self.latex_fallback_text.config(state="normal")
self.latex_fallback_text.delete("1.0", tk.END)
for eq in self._latex_equations:
if eq['type'] == 'spacer':
self.latex_fallback_text.insert(tk.END, "\n")
else:
type_text = f"[{eq['type'].upper()}] "
self.latex_fallback_text.insert(tk.END, type_text)
self.latex_fallback_text.insert(tk.END, f"{eq['original']}\n\n")
self.latex_fallback_text.config(state="disabled")
except Exception as e:
self.logger.debug(f"Error refrescando contenido LaTeX: {e}")
def _generate_base_html(self):
"""Genera el HTML base con soporte fallback para renderizar ecuaciones"""
return r"""
<!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>
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Consolas', monospace;
font-size: 13px;
margin: 0;
padding: 4px;
line-height: 1.2;
}
.equation-block {
margin: 1px 0;
padding: 4px 6px;
background-color: #1e1e1e;
border-left: 2px solid #80c7f7;
border-radius: 2px;
word-wrap: break-word;
min-height: auto;
}
.equation-content {
font-size: 14px;
color: #ffffff;
line-height: 1.3;
font-family: 'Times New Roman', serif;
margin: 0;
padding: 0;
}
.math-display {
font-size: 15px;
text-align: left;
margin: 2px 0;
padding: 2px 4px;
background-color: #252525;
border-radius: 2px;
min-height: auto;
}
.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 optimizado */
.info-message {
text-align: center;
color: #80c7f7;
font-style: italic;
margin: 10px;
padding: 8px;
border: 1px dashed #80c7f7;
border-radius: 4px;
font-size: 12px;
}
/* Mejorar representación matemática */
.frac {
display: inline-block;
vertical-align: middle;
text-align: center;
font-size: 1em;
margin: 0 2px;
}
.frac .num {
display: block;
border-bottom: 1px solid #fff;
padding-bottom: 1px;
line-height: 1;
}
.frac .den {
display: block;
padding-top: 1px;
line-height: 1;
}
.sqrt {
position: relative;
padding-left: 16px;
border-top: 1px solid #fff;
}
.sqrt::before {
content: "";
position: absolute;
left: 0;
top: -2px;
font-size: 16px;
font-weight: bold;
}
/* Mejorar subíndices y superíndices */
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;
}
</style>
</head>
<body>
<div id="equations-container">
<div class="info-message">
📐 Panel de Ecuaciones<br/>
Las ecuaciones se mostrarán aquí automáticamente
</div>
</div>
<script>
// Función mejorada para formatear expresiones matemáticas
function formatMathExpression(latex) {
// 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
latex = latex.replace(/\\alpha/g, 'α');
latex = latex.replace(/\\beta/g, 'β');
latex = latex.replace(/\\gamma/g, 'γ');
latex = latex.replace(/\\delta/g, 'δ');
latex = latex.replace(/\\pi/g, 'π');
latex = latex.replace(/\\theta/g, 'θ');
latex = latex.replace(/\\lambda/g, 'λ');
return latex;
}
// Función para limpiar ecuaciones
function clearEquations() {
const container = document.getElementById('equations-container');
container.innerHTML = '';
}
</script>
</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)
# ========== 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]}...")
# Usar la función _add_to_latex_panel
self._add_to_latex_panel(equation_type, latex_content)
self.logger.debug(f"✅ AÑADIDO al panel LaTeX: {equation_type} -> {latex_content[:50]}...")
else:
self.logger.debug(" -> NO añadido al panel LaTeX")
except Exception as e:
self.logger.error(f"Error procesando resultado para panel LaTeX: {e}")
import traceback
self.logger.error(traceback.format_exc())
def _get_result_tag_dynamic(self, result: Any) -> str:
"""Determina el tag de color para un resultado - SIMPLIFICADO"""
# Determinar tag basado en tipo
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_tkinterweb(self):
"""Actualiza el contenido usando tkinterweb"""
try:
if not hasattr(self, 'latex_webview') or not self.latex_webview:
return
# Generar HTML completo con todas las ecuaciones
html_content = self._generate_equations_html()
# Cargar el HTML actualizado
self.latex_webview.load_html(html_content)
except Exception as e:
self.logger.debug(f"Error actualizando tkinterweb: {e}")
def _update_pywebview(self):
"""Actualiza el contenido usando pywebview (implementación futura)"""
try:
# Para pywebview, necesitaríamos usar la API de JavaScript
# Por ahora, usar un enfoque más simple
pass
except Exception as e:
self.logger.debug(f"Error actualizando pywebview: {e}")
def _generate_equations_html(self):
"""Genera HTML completo con todas las ecuaciones"""
base_html = self._generate_base_html()
# Generar contenido de ecuaciones
equations_content = ""
for eq in self._latex_equations:
eq_type = eq['type']
content = eq['content']
# Manejar espaciadores especiales
if eq_type == 'spacer':
equations_content += '<div class="spacer"></div>'
continue
equations_content += f"""
<div class="equation-block {eq_type}">
<div class="equation-type">{eq_type}</div>
<div class="equation-content">{content}</div>
</div>
"""
# Reemplazar el contenido del container en el HTML base
if equations_content:
placeholder = '<div class="equation-block comment">'
if placeholder in base_html:
# Reemplazar el contenido de ejemplo
start_idx = base_html.find('<div id="equations-container">') + len('<div id="equations-container">')
end_idx = base_html.find('</div>', start_idx)
new_html = (base_html[:start_idx] +
equations_content +
base_html[end_idx:])
return new_html
return base_html
def _update_latex_panel(self):
"""Actualiza el contenido del panel LaTeX"""
if not self.latex_panel_visible or not hasattr(self, '_latex_equations'):
self.logger.debug("❌ Panel no visible o sin ecuaciones - no actualizar")
return
try:
self.logger.debug(f"🔄 Actualizando panel LaTeX con {len(self._latex_equations)} ecuaciones")
if not self._latex_equations:
# Si no hay ecuaciones, usar el HTML base
html_content = self._generate_base_html()
self.logger.debug("📄 Generando HTML base (sin ecuaciones)")
else:
# Generar HTML con las ecuaciones
html_blocks = []
for i, eq_data in enumerate(self._latex_equations):
eq_type = eq_data.get('type', 'symbolic')
latex_content = eq_data.get('latex', '')
self.logger.debug(f" 📝 Ecuación {i+1}: {eq_type} -> {latex_content[:50]}...")
# Crear bloque HTML para esta ecuación
# Aplicar formateo matemático mejorado
formatted_math = latex_content
# Convertir LaTeX básico a HTML mejorado con mejores símbolos
import re
# 1. Fracciones LaTeX: \frac{num}{den} → HTML
def replace_fractions(text):
def replace_frac(match):
num = match.group(1)
den = match.group(2)
return f'<span class="frac"><span class="num">{num}</span><span class="den">{den}</span></span>'
return re.sub(r'\\frac\{([^{}]*)\}\{([^{}]*)\}', replace_frac, text)
# 2. Raíces cuadradas: \sqrt{expr} → HTML
def replace_sqrt(text):
def replace_sqrt_match(match):
expr = match.group(1)
return f'<span class="sqrt">{expr}</span>'
return re.sub(r'\\sqrt\{([^{}]*)\}', replace_sqrt_match, text)
# 3. Exponentes: ^{expr} y ^num → superíndices
def replace_superscripts(text):
# Exponentes con llaves: ^{...}
text = re.sub(r'\^\{([^{}]*)\}', r'<sup>\1</sup>', text)
# Exponentes simples: ^2, ^3, etc.
text = re.sub(r'\^([0-9]+)', r'<sup>\1</sup>', text)
# ** para exponentes
text = text.replace('**2', '²').replace('**3', '³')
text = re.sub(r'\*\*([0-9]+)', r'<sup>\1</sup>', text)
return text
# 4. Subíndices: _{expr} → subíndices
def replace_subscripts(text):
text = re.sub(r'_\{([^{}]*)\}', r'<sub>\1</sub>', text)
text = re.sub(r'_([0-9]+)', r'<sub>\1</sub>', text)
return text
# 5. Símbolos matemáticos
def replace_symbols(text):
replacements = {
'pi': 'π', 'Pi': 'π', '\\pi': 'π',
'alpha': 'α', '\\alpha': 'α',
'beta': 'β', '\\beta': 'β',
'gamma': 'γ', '\\gamma': 'γ',
'delta': 'δ', '\\delta': 'δ',
'theta': 'θ', '\\theta': 'θ',
'lambda': 'λ', '\\lambda': 'λ',
'sigma': 'σ', '\\sigma': 'σ',
'omega': 'ω', '\\omega': 'ω'
}
for latex_sym, unicode_sym in replacements.items():
text = text.replace(latex_sym, unicode_sym)
return text
# Aplicar todas las transformaciones
formatted_math = replace_fractions(formatted_math)
formatted_math = replace_sqrt(formatted_math)
formatted_math = replace_superscripts(formatted_math)
formatted_math = replace_subscripts(formatted_math)
formatted_math = replace_symbols(formatted_math)
block_html = f"""
<div class="equation-block {eq_type}">
<div class="equation-content">
<div class="math-display">{formatted_math}</div>
</div>
</div>"""
html_blocks.append(block_html)
# Generar HTML completo
equations_html = '\n'.join(html_blocks)
html_content = self._generate_base_html().replace(
'<div id="equations-container">\n <div class="info-message">\n 📐 Panel de Ecuaciones<br/>\n Las ecuaciones se mostrarán aquí automáticamente\n </div>\n </div>',
f'<div id="equations-container">\n{equations_html}\n </div>'
)
self.logger.debug(f"📄 HTML generado: {len(html_content)} caracteres")
self.logger.debug(f"📄 Ecuaciones HTML: {len(equations_html)} caracteres")
# Actualizar el widget de renderizado
# Método 1: tkinterweb
if hasattr(self, 'latex_webview') and hasattr(self.latex_webview, 'load_html'):
try:
self.logger.debug("🌐 Intentando actualizar con tkinterweb...")
self.latex_webview.load_html(html_content)
# tkinterweb auto-renderiza KaTeX si está configurado correctamente en el HTML
self.logger.debug("✅ LaTeX actualizado via tkinterweb")
return
except Exception as e:
self.logger.error(f"❌ Error actualizando tkinterweb: {e}")
# Método 2: pywebview
elif hasattr(self, 'latex_webview') and self._webview_type == "pywebview":
try:
self.logger.debug("🌐 Intentando actualizar con pywebview...")
if self.latex_webview:
self.latex_webview.load_html(html_content)
self.logger.debug("✅ LaTeX actualizado via pywebview")
return
else:
self.logger.debug("⚠️ latex_webview no disponible para pywebview")
except Exception as e:
self.logger.error(f"❌ Error actualizando pywebview: {e}")
# Método 3: Fallback a texto plano
if hasattr(self, 'latex_fallback_text'):
try:
self.logger.debug("📝 Usando fallback a texto plano...")
# Limpiar y mostrar contenido como texto
self.latex_fallback_text.config(state="normal")
self.latex_fallback_text.delete('1.0', tk.END)
if self._latex_equations:
content_lines = ["📐 Ecuaciones LaTeX:", ""]
for eq_data in self._latex_equations:
eq_type = eq_data.get('type', 'symbolic')
latex_content = eq_data.get('latex', '')
original = eq_data.get('original', '')
content_lines.append(f"[{eq_type.upper()}]")
content_lines.append(f"LaTeX: {latex_content}")
if original:
content_lines.append(f"Original: {original}")
content_lines.append("-" * 50)
content_lines.append("")
self.latex_fallback_text.insert('1.0', '\n'.join(content_lines))
else:
self.latex_fallback_text.insert('1.0', "📐 Panel de Ecuaciones LaTeX\n\nLas ecuaciones aparecerán aquí...")
self.latex_fallback_text.config(state="disabled")
self.logger.debug("✅ LaTeX actualizado via texto plano")
return
except Exception as e:
self.logger.error(f"❌ Error actualizando texto plano: {e}")
# Si llegamos aquí, no hay widget disponible
self.logger.error("❌ No hay widget de renderizado disponible (tkinterweb, pywebview o fallback)")
except Exception as e:
self.logger.error(f"❌ Error crítico actualizando panel LaTeX: {e}")
import traceback
self.logger.error(traceback.format_exc())
def _add_to_latex_panel(self, content_type: str, latex_content: str):
"""Añade una ecuación al panel LaTeX"""
if not hasattr(self, '_latex_equations'):
self._latex_equations = []
# Crear datos de la ecuación
equation_data = {
'type': content_type,
'latex': latex_content,
'original': latex_content, # Guardar contenido original también
'timestamp': time.time()
}
self._latex_equations.append(equation_data)
# Limitar número de ecuaciones (opcional)
max_equations = 50
if len(self._latex_equations) > max_equations:
self._latex_equations = self._latex_equations[-max_equations:]
self.logger.debug(f" Añadida ecuación {content_type}: {latex_content[:50]}... (Total: {len(self._latex_equations)})")
# Actualizar indicador visual
self._update_content_indicator()
# IMPORTANTE: Actualizar panel si está visible
if self.latex_panel_visible:
self.logger.debug("🔄 Panel visible - actualizando contenido...")
self._update_latex_panel()
else:
self.logger.debug("👁️ Panel no visible - saltando actualización")
self.logger.debug(f"✅ Proceso de añadir ecuación completado")
def _update_content_indicator(self):
"""Actualiza el indicador visual de contenido disponible"""
try:
if hasattr(self, '_latex_equations') and self._latex_equations:
# Mostrar indicador si no está visible
if not hasattr(self, '_indicator_visible') or not self._indicator_visible:
self.latex_indicator.pack(side=tk.BOTTOM, pady=2)
self._indicator_visible = True
else:
# Ocultar indicador si no hay contenido
if hasattr(self, '_indicator_visible') and self._indicator_visible:
self.latex_indicator.pack_forget()
self._indicator_visible = False
except Exception as e:
self.logger.debug(f"Error actualizando indicador: {e}")
def main():
"""Función principal"""
root = tk.Tk()
app = HybridCalculatorApp(root)
try:
root.iconname("Calculadora MAV CAS")
except tk.TclError:
pass
root.mainloop()
if __name__ == "__main__":
main()