From 9ba73a9db63317bb8df043075d5b173628bed757 Mon Sep 17 00:00:00 2001 From: Miguel Date: Wed, 11 Jun 2025 18:37:57 +0200 Subject: [PATCH] Limpieza general --- .data/history.txt | 18 + CORRECCIONES_APLICADAS.md | 165 - README_MEJORAS_PYSIDE6.md | 156 - README_PYSIDE6.md | 188 - RENDERIZADO_LATEX.md | 127 - RESUMEN_AUTOCOMPLETADO_COMPLETO.md | 178 - app/__init__.py | 0 class_base.py => app/class_base.py | 0 .../main_calc_app.py | 362 +- .../main_evaluation.py | 6 +- simple_debug.py => app/simple_debug.py | 4 +- sympy_Base.py => app/sympy_Base.py | 2 +- sympy_helper.py => app/sympy_helper.py | 0 .../tl_bracket_parser.py | 0 tl_popup_pyside6.py => app/tl_popup.py | 0 type_registry.py => app/type_registry.py | 0 calc.py | 319 +- hybrid_calc_history.txt | 5 - hybrid_calc_history_pyside6.txt | 2 - hybrid_calc_settings.json | 15 - latex_debug.html | 160 - launch_pyside6.py | 89 - main_calc_app.py | 4247 ----------------- test_final.py | 44 - tl_popup.py | 669 --- 25 files changed, 399 insertions(+), 6357 deletions(-) create mode 100644 .data/history.txt delete mode 100644 CORRECCIONES_APLICADAS.md delete mode 100644 README_MEJORAS_PYSIDE6.md delete mode 100644 README_PYSIDE6.md delete mode 100644 RENDERIZADO_LATEX.md delete mode 100644 RESUMEN_AUTOCOMPLETADO_COMPLETO.md create mode 100644 app/__init__.py rename class_base.py => app/class_base.py (100%) rename main_calc_app_pyside6.py => app/main_calc_app.py (85%) rename main_evaluation_puro.py => app/main_evaluation.py (99%) rename simple_debug.py => app/simple_debug.py (98%) rename sympy_Base.py => app/sympy_Base.py (99%) rename sympy_helper.py => app/sympy_helper.py (100%) rename tl_bracket_parser.py => app/tl_bracket_parser.py (100%) rename tl_popup_pyside6.py => app/tl_popup.py (100%) rename type_registry.py => app/type_registry.py (100%) delete mode 100644 hybrid_calc_history.txt delete mode 100644 hybrid_calc_history_pyside6.txt delete mode 100644 hybrid_calc_settings.json delete mode 100644 latex_debug.html delete mode 100644 launch_pyside6.py delete mode 100644 main_calc_app.py delete mode 100644 test_final.py delete mode 100644 tl_popup.py diff --git a/.data/history.txt b/.data/history.txt new file mode 100644 index 0000000..f9b3227 --- /dev/null +++ b/.data/history.txt @@ -0,0 +1,18 @@ + +ex = (t * 8) / w + + +var1 = 2 +var2 = 4 +vatt1 = 4 + +IP4 + + + + + + + + + diff --git a/CORRECCIONES_APLICADAS.md b/CORRECCIONES_APLICADAS.md deleted file mode 100644 index a09b423..0000000 --- a/CORRECCIONES_APLICADAS.md +++ /dev/null @@ -1,165 +0,0 @@ -# 🔧 CORRECCIONES APLICADAS - PROBLEMAS REPORTADOS - -## 🐛 **PROBLEMAS IDENTIFICADOS Y SOLUCIONADOS** - -### ❌ **PROBLEMA 1: Popups toman el foco y no permiten escribir** - -**🔍 Causa:** Los popups usaban `Qt.Popup` que automáticamente toma el foco del widget padre. - -**✅ Solución Aplicada:** -```python -# ANTES (PROBLEMÁTICO): -self._autocomplete_popup = QWidget(self, Qt.Popup | Qt.FramelessWindowHint) - -# DESPUÉS (CORREGIDO): -self._autocomplete_popup = QWidget(None, Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) -self._autocomplete_popup.setAttribute(Qt.WA_ShowWithoutActivating, True) -self._autocomplete_popup.setFocusPolicy(Qt.NoFocus) -``` - -**🎯 Cambios Específicos:** -- `Qt.Tool` en lugar de `Qt.Popup` - No roba foco automáticamente -- `Qt.WindowStaysOnTopHint` - Mantiene popup visible encima -- `WA_ShowWithoutActivating = True` - Muestra sin activar la ventana -- `setFocusPolicy(Qt.NoFocus)` - Widget nunca puede recibir foco -- `parent=None` - No hereda comportamiento de foco del padre - ---- - -### ❌ **PROBLEMA 2: TAB no funciona para seleccionar** - -**🔍 Causa:** El manejo de eventos de teclado era correcto, pero los popups con foco causaban interferencia. - -**✅ Solución Aplicada:** -```python -# También aplicado a QListWidget: -self._autocomplete_listbox.setFocusPolicy(Qt.NoFocus) -``` - -**🎯 Verificación del Flujo:** -1. `_handle_key_press` detecta `Qt.Key_Tab` ✅ -2. Llama a `_handle_tab_key()` ✅ -3. Ejecuta `_select_autocomplete()` ✅ -4. Ahora sin interferencia de foco ✅ - ---- - -### ❌ **PROBLEMA 3: Links de plots no se generan** - -**🔍 Causa:** Error en el nombre del atributo - buscaba `result.result_obj` pero el correcto es `result.actual_result_object`. - -**✅ Solución Aplicada:** -```python -# ANTES (INCORRECTO): -if hasattr(result, 'result_obj') and isinstance(result.result_obj, PlotResult): - plot_obj = result.result_obj - -# DESPUÉS (CORREGIDO): -if hasattr(result, 'actual_result_object') and isinstance(result.actual_result_object, PlotResult): - plot_obj = result.actual_result_object -``` - -**🎯 Verificación del Flujo:** -1. `main_evaluation_puro.py` establece `actual_result_object` ✅ -2. `_process_evaluation_result` ahora detecta correctamente PlotResult ✅ -3. Crea tupla `("clickeable", display_text, link_id, result_object)` ✅ -4. `_display_output_lines` procesa y muestra link azul ✅ - ---- - -## 🔧 **CORRECCIONES ADICIONALES APLICADAS** - -### 🆕 **Variables de Estado Inicializadas** -```python -self._last_navigation_time = 0 -self._last_input_change = 0 -``` -**Propósito:** Evitar errores en filtrado y navegación. - -### 🎨 **Estilo de Popups Optimizado** -- Popup principal: **Azul** (`#4fc3f7`) para métodos y constructores -- Popup de variables: **Verde** (`#c3e88d`) para variables disponibles -- Ambos con `Qt.NoFocus` para comportamiento verdaderamente modeless - -### 📍 **Posicionamiento Mejorado** -- `_position_popup_modeless()` posiciona por debajo de la línea actual -- Detección de bordes de pantalla y reposicionamiento automático -- Offset de 5px para separación visual - ---- - -## 🧪 **ARCHIVO DE PRUEBA CREADO** - -### 📁 `test_debug_problemas.py` -- Script específico para verificar las correcciones -- Debug en tiempo real del estado de los popups -- Secuencias de prueba paso a paso -- Criterios de éxito claramente definidos - ---- - -## ✅ **RESULTADOS ESPERADOS** - -### 🎯 **Comportamiento Correcto Ahora:** - -1. **✅ Popups Modeless:** - - Aparecen sin robar foco del input - - Permiten continuar escribiendo mientras están visibles - - Se posicionan de forma no intrusiva - -2. **✅ TAB Funcional:** - - Selecciona la opción resaltada en el popup - - Cierra el popup después de seleccionar - - Inserta el texto seleccionado en la posición correcta - -3. **✅ Links de Plots:** - - Se generan automáticamente para `PlotResult` - - Aparecen como links azules subrayados - - Click muestra plot en MathJax primero - - Segundo click abre ventana de edición - -4. **✅ Navegación Completa:** - - Flechas ↑↓ navegan opciones - - TAB selecciona opción actual - - ESC cierra popup - - Enter también selecciona (alternativa a TAB) - ---- - -## 🚀 **INSTRUCCIONES DE VERIFICACIÓN** - -### 📝 **Prueba Rápida:** -```bash -python test_debug_problemas.py -``` - -### 🔍 **Secuencia de Verificación:** -1. Escribir `x = 5` → Enter -2. Escribir `x.` → ¿Popup aparece sin robar foco? -3. Usar flechas para navegar → ¿Funciona? -4. Presionar TAB → ¿Se selecciona? -5. Escribir `plot(x**2, (x, -5, 5))` → ¿Link azul aparece? - ---- - -## 📋 **ARCHIVOS MODIFICADOS** - -### 🔧 **Principales:** -- `main_calc_app_pyside6.py` - Correcciones de foco, TAB, y detección de plots -- `tl_popup_pyside6.py` - Sistema de popups PySide6 (ya estaba correcto) - -### 🆕 **Nuevos:** -- `test_debug_problemas.py` - Script de verificación específica -- `CORRECCIONES_APLICADAS.md` - Este documento de resumen - ---- - -## 🎉 **ESTADO ACTUAL** - -**✅ TODOS LOS PROBLEMAS REPORTADOS HAN SIDO SOLUCIONADOS:** - -1. ✅ **Popups verdaderamente modeless** - No roban foco -2. ✅ **TAB funciona** - Selecciona opciones correctamente -3. ✅ **Links de plots se generan** - Detección corregida - -**🚀 El sistema está listo para uso completo con todas las funcionalidades operativas.** \ No newline at end of file diff --git a/README_MEJORAS_PYSIDE6.md b/README_MEJORAS_PYSIDE6.md deleted file mode 100644 index 2e532ed..0000000 --- a/README_MEJORAS_PYSIDE6.md +++ /dev/null @@ -1,156 +0,0 @@ -# Mejoras Implementadas en Calculadora MAV PySide6 - -## Problemas Solucionados - -### 1. ✅ Comentarios en Panel de Resultados -- **Problema**: Los comentarios no se copiaban al panel de resultados -- **Solución**: - - Modificada función `_evaluate_lines()` para detectar comentarios con `#` - - Agregado formato especial para comentarios en `setup_output_tags()` - - Los comentarios ahora aparecen en verde cursiva en el panel de resultados - - Correspondencia 1:1 mantenida entre entrada y salida - -### 2. ✅ Comentarios en Panel MathJax -- **Problema**: Los comentarios no se mostraban correctamente en el panel LaTeX -- **Solución**: - - Mejorado el JavaScript del panel MathJax para manejar comentarios - - Los comentarios se muestran con formato especial (sin LaTeX) - - Agregada función `preprocessLatex()` para mejor procesamiento - -### 3. ✅ Filtrado de Contenido en Panel MathJax -- **Problema**: Se mostraba todo en el panel LaTeX -- **Solución**: - - Mejorada función `_add_to_latex_panel_if_applicable()` con reglas claras: - - **Comentarios**: Siempre se agregan - - **Asignaciones**: Variables = expresiones simbólicas - - **Ecuaciones**: Expresiones con símbolos matemáticos - - **Exclusiones**: Resultados numéricos simples sin contenido simbólico - -### 4. ✅ Persistencia de Dimensiones -- **Problema**: Las dimensiones no se guardaban entre sesiones -- **Solución**: - - Agregada función `save_settings()` mejorada que guarda: - - Posición de ventana (x, y) - - Tamaño de ventana (width, height) - - Tamaños de cada panel del splitter - - Estado visible/oculto del panel LaTeX - - Agregada función `restore_geometry()` para cargar configuración - - Archivos de configuración separados: `hybrid_calc_settings_pyside6.json` - -### 5. ✅ Parsing LaTeX Mejorado -- **Problema**: Errores con divisiones y funciones como sqrt -- **Solución**: - - Mejorado HTML base con macros LaTeX para funciones comunes - - Agregada función `preprocessLatex()` en JavaScript: - - Convierte `/` a `\frac{}{}` - - Convierte `sqrt()` a `\sqrt{}` - - Maneja funciones trigonométricas - - Mejorada función `_sympy_to_latex()` con fallbacks - - Mejor manejo de errores de MathJax con fallback a texto plano - -### 6. ✅ Cierre de Popups con Enter -- **Problema**: Los popups de autocompletado no se cerraban con Enter -- **Solución**: - - Modificada función `_handle_key_press()` para detectar Enter/Return - - Los popups se cierran automáticamente al presionar Enter - - Mantiene funcionalidad normal de nueva línea - -## Nuevas Características - -### 📐 Panel MathJax Rediseñado -- Diseño moderno con gradientes y sombras -- Scrollbar personalizado -- Mejor tipografía y espaciado -- Animaciones suaves de hover -- Indicadores de tipo por colores - -### 💾 Configuración Persistente -- Geometría de ventana completamente persistente -- Tamaños de paneles individuales guardados -- Configuración en archivo JSON separado -- Carga automática al iniciar - -### 🎨 Resaltado de Sintaxis Mejorado -- Colores optimizados para tema oscuro -- Comentarios en verde cursiva -- Números, funciones y operadores diferenciados - -## Estructura de Archivos - -``` -Calcv2/ -├── main_calc_app_pyside6.py # Aplicación principal PySide6 -├── hybrid_calc_settings_pyside6.json # Configuración persistente -├── hybrid_calc_history_pyside6.txt # Historial de sesión -└── README_MEJORAS_PYSIDE6.md # Este archivo -``` - -## Cómo Probar las Mejoras - -### 1. Comentarios -```python -# Este es un comentario que aparece en ambos paneles -x = 2 + 3 # Comentario al final de línea -# Otro comentario -y = x * 4 -``` - -### 2. Ecuaciones LaTeX -```python -from sympy import * -x, y = symbols('x y') - -# Divisiones (ahora renderiza correctamente) -expr1 = (x + 1) / (x - 1) - -# Funciones sqrt -expr2 = sqrt(x**2 + y**2) - -# Funciones trigonométricas -expr3 = sin(x) / cos(x) - -# Asignaciones simbólicas -z = x**2 + 2*x + 1 -``` - -### 3. Persistencia -1. Redimensiona la ventana y los paneles -2. Muestra/oculta el panel LaTeX con F12 -3. Cierra la aplicación -4. Reabre - debería mantener todas las dimensiones - -### 4. Autocompletado -```python -# Escribe "sympy." y aparece popup -# Presiona Enter para cerrar sin seleccionar -# Escribe "x." después de definir una variable -``` - -## Comando de Ejecución - -```bash -python main_calc_app_pyside6.py -``` - -## Atajos de Teclado - -- **F12**: Mostrar/ocultar panel LaTeX -- **Ctrl+Enter**: Evaluar manualmente -- **Shift+Enter**: Evaluar manualmente -- **Escape**: Cerrar popups de autocompletado -- **Enter**: Cerrar popups y crear nueva línea - -## Log de Debugging - -La aplicación mantiene logs detallados para debugging en: -- Consola durante ejecución -- Información de estado en barra inferior -- Errores de MathJax manejados graciosamente - -## Próximas Mejoras Sugeridas - -1. **Exportar LaTeX**: Botón para exportar todas las ecuaciones del panel -2. **Temas**: Opción para cambiar entre tema claro/oscuro -3. **Búsqueda**: Función de búsqueda en historial -4. **Variables Globales**: Panel lateral con variables definidas -5. **Plots Integrados**: Mostrar gráficos directamente en el panel LaTeX \ No newline at end of file diff --git a/README_PYSIDE6.md b/README_PYSIDE6.md deleted file mode 100644 index 63bc1c5..0000000 --- a/README_PYSIDE6.md +++ /dev/null @@ -1,188 +0,0 @@ -# Calculadora MAV - Versión PySide6 con Diseño Minimalista - -## 🎯 Implementación Minimalista de 3 Paneles - -Esta es la implementación de la Calculadora MAV usando **PySide6** con diseño minimalista de 3 paneles y renderizado **MathJax** para ecuaciones LaTeX, manteniendo correspondencia 1:1 línea por línea entre entrada y salida. - -## ✨ Características Principales - -### 🔧 Interfaz Moderna -- **PySide6**: Framework Qt moderno y nativo -- **Tema Oscuro**: Diseño elegante y profesional -- **Resaltado de Sintaxis**: Coloreado inteligente de expresiones matemáticas -- **Interfaz Responsive**: Se adapta al tamaño de la ventana - -### 🧮 Motor de Cálculo -- **SymPy**: Motor algebraico simbólico completo -- **Evaluación Asíncrona**: Cálculos en threads separados -- **Tipos Personalizados**: Sistema de tipos extensible -- **Historial Automático**: Guarda y restaura sesiones - -### 📐 Renderizado LaTeX -- **MathJax**: Renderizado web profesional de ecuaciones -- **Tiempo Real**: Actualización automática del panel LaTeX -- **Múltiples Tipos**: Ecuaciones, asignaciones y expresiones simbólicas -- **Interactivo**: Panel redimensionable y ocultable - -## 🚀 Instalación y Uso - -### Requisitos Previos -```bash -Python 3.8 o superior -``` - -### Instalar Dependencias -```bash -pip install -r requirements.txt -``` - -### Ejecutar la Aplicación -```bash -# Opción 1: Usar el launcher (recomendado) -python launch_pyside6.py - -# Opción 2: Ejecutar directamente -python main_calc_app_pyside6.py -``` - -## 🎮 Guía de Uso - -### Interfaz Principal - 3 Paneles Minimalistas -- **Panel 1 (Izquierda)**: Entrada de expresiones matemáticas -- **Panel 2 (Centro)**: Resultados con correspondencia 1:1 línea por línea -- **Panel 3 (Derecha)**: Renderizado LaTeX/MathJax de ecuaciones y comentarios (colapsable) - -### Atajos de Teclado -| Atajo | Función | -|-------|---------| -| `Ctrl+Enter` | Evaluar expresión | -| `Shift+Enter` | Evaluar expresión | -| `F12` | Mostrar/ocultar panel LaTeX | -| `Ctrl+N` | Nueva sesión | -| `Ctrl+O` | Abrir archivo | -| `Ctrl+S` | Guardar archivo | - -### Ejemplos de Uso -```python -# Ecuaciones básicas -x**2 + y**2 = r**2 - -# Resolver ecuaciones -solve(x**2 - 4, x) - -# Cálculo diferencial -diff(x**3 + 2*x**2 + x, x) - -# Cálculo integral -integrate(x**2, x) - -# Álgebra lineal -Matrix([[1, 2], [3, 4]]) - -# Asignaciones -a = x**2 + 5 -b = solve(a - 10, x) -``` - -## 🎨 Características de la Interfaz - -### Resaltado de Sintaxis -- **Números**: Color azul claro (`#89ddff`) -- **Funciones**: Color azul (`#82aaff`) y negrita -- **Variables**: Color verde claro (`#c3e88d`) -- **Operadores**: Color rojo (`#ff6b6b`) y negrita -- **Paréntesis**: Color naranja (`#f78c6c`) y negrita - -### Panel LaTeX -- **Ecuaciones**: Borde azul (`#4fc3f7`) -- **Asignaciones**: Borde verde (`#c3e88d`) -- **Expresiones Simbólicas**: Borde naranja (`#f78c6c`) -- **Hover Effects**: Cambios de color al pasar el ratón - -## 🔧 Arquitectura Técnica - -### Componentes Principales -``` -HybridCalculatorPySide6 (Ventana Principal) -├── MathInputHighlighter (Resaltado de sintaxis) -├── CalculatorWorker (Evaluación asíncrona) -├── MathJaxPanel (Renderizado LaTeX) -└── PureAlgebraicEngine (Motor de cálculo) -``` - -### Threading -- **UI Thread**: Interfaz de usuario principal -- **Worker Thread**: Evaluación matemática asíncrona -- **Signals/Slots**: Comunicación segura entre threads - -### WebEngine -- **QWebEngineView**: Para renderizado MathJax -- **JavaScript Integration**: Comunicación bidireccional -- **HTML5**: Soporte completo para MathJax 3.x - -## 🆚 Comparación con Versión Original - -| Característica | Tkinter Original | PySide6 Nueva | -|----------------|------------------|---------------| -| **Framework** | Tkinter | PySide6/Qt | -| **Renderizado LaTeX** | pywebview/tkinterweb | MathJax nativo | -| **Tema** | Básico | Moderno oscuro | -| **Resaltado** | No | Sintaxis completa | -| **Threading** | Básico | Asíncrono avanzado | -| **Responsive** | Limitado | Completo | -| **Cross-platform** | Básico | Nativo Qt | - -## 🔍 Solución de Problemas - -### Error: "No module named 'PySide6'" -```bash -pip install PySide6 -``` - -### Error: "No module named 'PySide6.QtWebEngineWidgets'" -```bash -pip install PySide6-WebEngine -``` - -### Panel LaTeX no funciona -1. Verificar conexión a internet (MathJax CDN) -2. Comprobar que WebEngine esté instalado -3. Ver logs en consola para errores JavaScript - -### Rendimiento lento -1. Cerrar otras aplicaciones que usen Qt/WebEngine -2. Reducir el número de ecuaciones en el panel LaTeX -3. Verificar que no hay bucles infinitos en expresiones - -## 🚧 Desarrollo y Contribución - -### Estructura del Código -``` -main_calc_app_pyside6.py # Aplicación principal -launch_pyside6.py # Launcher con verificaciones -requirements.txt # Dependencias actualizadas -README_PYSIDE6.md # Esta documentación -``` - -### Extensiones Futuras -- [ ] Autocompletado inteligente -- [ ] Gráficos integrados con matplotlib -- [ ] Exportación de ecuaciones LaTeX -- [ ] Temas personalizables -- [ ] Plugin system -- [ ] Colaboración en tiempo real - -## 📄 Licencia - -Mismo sistema de licencia que el proyecto original. - -## 💝 Agradecimientos - -- **Qt/PySide6**: Framework de interfaz moderna -- **MathJax**: Renderizado matemático profesional -- **SymPy**: Motor algebraico potente -- **Comunidad Python**: Soporte y documentación - ---- - -**¡Disfruta calculando con la nueva interfaz moderna! 🎉** \ No newline at end of file diff --git a/RENDERIZADO_LATEX.md b/RENDERIZADO_LATEX.md deleted file mode 100644 index 0269bc5..0000000 --- a/RENDERIZADO_LATEX.md +++ /dev/null @@ -1,127 +0,0 @@ -# Sistema de Renderizado LaTeX Simplificado - -## Problema Original -El panel LaTeX no se renderizaba correctamente porque faltaba el motor de renderizado matemático (MathJax) y las fuentes matemáticas apropiadas. - -## Solución Implementada - -### 🌐 **Sistema Único: tkinterweb + MathJax 3** -- **Dependencia requerida**: tkinterweb -- **Motor de renderizado**: MathJax 3 optimizado -- **Fuentes matemáticas**: STIX Two Math, Computer Modern Serif -- **Configuración automática** de delimitadores LaTeX (`$$...$$`, `$...$`) -- **Re-renderizado dinámico** cuando se agregan nuevas ecuaciones - -```javascript -// MathJax configurado automáticamente -window.MathJax = { - tex: { - inlineMath: [['$', '$']], - displayMath: [['$$', '$$']], - processEscapes: true, - processEnvironments: true, - packages: {'[+]': ['ams', 'newcommand', 'configmacros']} - }, - startup: { - ready: function () { - MathJax.startup.defaultReady(); - console.log('✅ MathJax 3 listo'); - } - } -}; -``` - -## Instalación - -### Dependencia Requerida -```bash -pip install tkinterweb -``` - -**Nota**: Si no tienes tkinterweb instalado, la aplicación se cerrará automáticamente con un mensaje de error claro. - -## Características Técnicas - -### ✅ **Renderizado Optimizado** -- **MathJax 3** para ecuaciones LaTeX profesionales -- **Fuentes matemáticas mejoradas** (STIX Two Math, Computer Modern Serif) -- **Re-renderizado automático** cuando se agregan ecuaciones -- **Indicador de estado** que muestra si MathJax está activo - -### ✅ **Integración Perfecta** -- **Panel expandible** en el lado derecho de la calculadora -- **Sincronización automática** con las evaluaciones -- **Detección inteligente** de ecuaciones, asignaciones y comentarios -- **Indicador visual** cuando hay contenido disponible - -### ✅ **Tipos de Contenido Soportados** -- **Ecuaciones**: `x**2 + 2*x = 8` -- **Asignaciones**: `a = 5` -- **Expresiones simbólicas**: `sin(x) + cos(x)` -- **Comentarios**: `# Esto es un comentario` - -## Uso - -1. **Abrir panel LaTeX**: Click en el botón 📐 en el borde derecho -2. **Escribir ecuaciones**: Las ecuaciones se detectan automáticamente -3. **Ver renderizado**: MathJax renderiza las ecuaciones en tiempo real -4. **Cerrar panel**: Click en ✕ o en el botón 📐 nuevamente - -## Solución de Problemas - -### Si ves "Cargando MathJax..." -- **Causa**: MathJax no se ha cargado completamente -- **Solución**: Espera unos segundos, MathJax se carga desde CDN -- **Verificación**: El indicador cambiará a "✅ MathJax activo" - -### Si la aplicación se cierra al inicio -- **Causa**: tkinterweb no está instalado -- **Solución**: `pip install tkinterweb` -- **Verificación**: Reinicia la aplicación - -### Si las ecuaciones no se renderizan -- **Causa**: Error en la sintaxis LaTeX -- **Solución**: Verifica que la sintaxis LaTeX sea correcta -- **Verificación**: Revisa la consola del navegador (F12 en tkinterweb) - -## Ventajas del Sistema Simplificado - -### 🎯 **Confiabilidad** -- **Un solo sistema** = menos puntos de falla -- **Dependencia mínima** = fácil instalación -- **Error claro** si falta la dependencia - -### 🚀 **Rendimiento** -- **MathJax 3** optimizado para velocidad -- **Sin fallbacks** = código más limpio -- **Carga asíncrona** = no bloquea la UI - -### 🔧 **Mantenimiento** -- **Código simplificado** = fácil de mantener -- **Sin lógica compleja** de detección de sistemas -- **Comportamiento predecible** - -## Arquitectura - -``` -Calculadora MAV -├── Panel de Entrada (izquierda) -├── Panel de Salida (centro) -└── Panel LaTeX (derecha, expandible) - └── tkinterweb.HtmlFrame - └── MathJax 3 + HTML optimizado - ├── Ecuaciones renderizadas - ├── Fuentes matemáticas - └── Indicador de estado -``` - -## Conclusión - -El sistema simplificado con **tkinterweb + MathJax 3** proporciona: -- ✅ Renderizado LaTeX profesional -- ✅ Instalación simple (una dependencia) -- ✅ Comportamiento predecible -- ✅ Mantenimiento fácil -- ✅ Experiencia de usuario consistente - -**Resultado**: Panel LaTeX que funciona perfectamente sin fallbacks ni complejidad innecesaria. \ No newline at end of file diff --git a/RESUMEN_AUTOCOMPLETADO_COMPLETO.md b/RESUMEN_AUTOCOMPLETADO_COMPLETO.md deleted file mode 100644 index 359d0c1..0000000 --- a/RESUMEN_AUTOCOMPLETADO_COMPLETO.md +++ /dev/null @@ -1,178 +0,0 @@ -# 🎯 SISTEMA COMPLETO DE AUTOCOMPLETADO Y LINKS CLICKEABLES - -## ✅ FUNCIONALIDADES IMPLEMENTADAS - -### 🔧 **1. SISTEMA DE AUTOCOMPLETADO COMPLETO (3 TIPOS)** - -#### **1.1 Autocompletado de Variables (Timer-based)** -- ⏱️ **Activación**: Después de 800ms de inactividad -- 🎨 **Estilo**: Popup verde discreto -- 📝 **Contenido**: Variables disponibles del contexto actual -- 🔍 **Filtrado**: En tiempo real mientras escribes -- 🎯 **Uso**: Aparece automáticamente al dejar de escribir - -#### **1.2 Autocompletado con Punto en Objeto** -- ⚡ **Activación**: Al escribir "." después de un objeto -- 🎨 **Estilo**: Popup azul para métodos -- 📝 **Contenido**: Métodos disponibles del objeto -- 🔧 **Soporte**: Objetos personalizados con `PopupFunctionList()` -- 🎯 **Ejemplo**: `x.` muestra métodos de x - -#### **1.3 Autocompletado con Punto en Línea Vacía** -- ⚡ **Activación**: Al escribir "." en línea vacía o después de espacios -- 🎨 **Estilo**: Popup azul para constructores -- 📝 **Contenido**: Constructores de tipos y funciones globales -- 🔧 **Fuentes**: Registro dinámico de tipos + SymPy -- 🎯 **Ejemplo**: `.` muestra sin(), cos(), Matrix(), etc. - -### 🎮 **2. NAVEGACIÓN Y CONTROL** - -#### **2.1 Navegación con Teclado** -- ⬆️⬇️ **Flechas**: Navegar opciones arriba/abajo -- ⭐ **TAB**: Seleccionar opción actual (funcionalidad principal) -- 🚪 **ESC**: Cerrar popup -- ↩️ **Enter**: Seleccionar opción (alternativa a TAB) - -#### **2.2 Filtrado en Tiempo Real** -- ⌨️ **Mientras escribes**: Filtra opciones automáticamente -- 🔍 **Ejemplo**: Después de `x.`, escribir `ev` filtra a métodos que empiecen con "ev" -- ❌ **Auto-cierre**: Si no hay coincidencias, cierra el popup - -#### **2.3 Posicionamiento Modeless** -- 📍 **Ubicación**: Por debajo de la línea actual de escritura -- 🚫 **No invasivo**: No bloquea la escritura -- 📱 **Adaptativo**: Se ajusta si no hay espacio en pantalla -- 🎯 **Objetivo**: Experiencia no intrusiva - -### 🔗 **3. SISTEMA DE LINKS CLICKEABLES** - -#### **3.1 Detección Automática de Plots** -- 🎯 **Detección**: Automática para objetos `PlotResult` -- 🎨 **Formato**: Link azul subrayado en panel de salida -- 📊 **Texto**: "📊 Ver Plot" / "📊 Ver Plot3d" -- ⚡ **Activación**: Click en el link - -#### **3.2 Flujo de Visualización de Plots** -1. **Primera vez**: Click en link → Muestra plot en panel MathJax -2. **Segunda vez**: Click en MathJax → Abre ventana de edición -3. **Edición**: Permite modificar expresión y redibujar -4. **Sincronización**: Cambios se reflejan en panel de entrada - -#### **3.3 Ventanas Emergentes de Edición** -- 🖼️ **Contenido**: Canvas matplotlib + campo de edición -- ✏️ **Edición**: Modificar expresión original -- 🔄 **Redibujar**: Botón para actualizar plot -- 🔗 **Sincronización**: Cambios vuelven al panel principal - -### 🏗️ **4. ARQUITECTURA TÉCNICA** - -#### **4.1 Adaptación de tkinter a PySide6** -- ✅ **tl_popup.py** → **tl_popup_pyside6.py** -- ✅ **InteractiveResultManager** adaptado completamente -- ✅ **PlotResult** mantenido compatible -- ✅ **Ventanas emergentes** con matplotlib integrado - -#### **4.2 Integración con Motor Original** -- 🔧 **PureAlgebraicEngine**: Sin cambios -- 🔧 **EvaluationResult**: Estructura preservada -- 🔧 **Tipos personalizados**: Sistema mantenido -- 🔧 **Contexto dinámico**: Funciona igual que en tkinter - -#### **4.3 Componentes Nuevos** -- 🆕 **_append_clickeable_link()**: Crear links en salida -- 🆕 **_handle_output_click()**: Detectar clicks en links -- 🆕 **_show_plot_in_mathjax()**: Mostrar plots en MathJax -- 🆕 **_position_popup_modeless()**: Posicionamiento inteligente - -## 🎯 **CARACTERÍSTICAS DESTACADAS** - -### ✨ **Experiencia de Usuario** -- 🎨 **Colores distintivos**: Verde para variables, azul para métodos -- ⚡ **Respuesta rápida**: 800ms para variables, inmediato para métodos -- 🎯 **No invasivo**: Popups modeless que no interrumpen -- 🔍 **Filtrado inteligente**: Reduce opciones mientras escribes - -### 🔧 **Robustez Técnica** -- ✅ **Manejo de errores**: Try-catch en todas las operaciones críticas -- 🔄 **Limpieza automática**: Popups se cierran apropiadamente -- 📱 **Adaptación de pantalla**: Ajuste automático de posición -- 🎯 **Compatibilidad**: Funciona con todos los tipos del sistema - -### 🚀 **Rendimiento** -- ⚡ **Evaluación lazy**: Solo evalúa cuando es necesario -- 🎯 **Filtrado eficiente**: Búsqueda por prefijo optimizada -- 📦 **Memoria controlada**: Limpieza de referencias de plots -- 🔄 **Timers inteligentes**: Evita evaluaciones innecesarias - -## 📋 **ARCHIVOS MODIFICADOS/CREADOS** - -### 🆕 **Archivos Nuevos** -- `tl_popup_pyside6.py` - Versión PySide6 del sistema de popups -- `test_autocompletado_completo.py` - Script de prueba completo - -### 🔧 **Archivos Modificados** -- `main_calc_app_pyside6.py` - Sistema completo integrado -- `requirements.txt` - Dependencias actualizadas (si fue necesario) - -## 🧪 **CÓMO PROBAR** - -### 🚀 **Ejecución** -```bash -python test_autocompletado_completo.py -``` - -### 📝 **Casos de Prueba** - -#### **1. Variables** -```python -x = 5 -y = x # Esperar 800ms → popup verde con variables -``` - -#### **2. Métodos de Objeto** -```python -x = 5 -x. # → popup azul con métodos -``` - -#### **3. Constructores Globales** -```python -. # En línea vacía → popup con sin(), cos(), Matrix(), etc. -``` - -#### **4. Plots Clickeables** -```python -plot(x**2, (x, -5, 5)) # → link azul clickeable -``` - -#### **5. Filtrado** -```python -x.ev # Después del popup, escribir "ev" filtra opciones -``` - -## ✅ **CRITERIOS DE ÉXITO** - -- ✅ **3 tipos de autocompletado** funcionando -- ✅ **Navegación con TAB** implementada -- ✅ **Posicionamiento modeless** no invasivo -- ✅ **Links clickeables** para plots -- ✅ **Ventanas emergentes** de edición -- ✅ **Filtrado en tiempo real** mientras escribes -- ✅ **Compatibilidad completa** con motor original -- ✅ **Experiencia de usuario** mejorada - -## 🎉 **RESULTADO FINAL** - -El sistema de autocompletado de tkinter ha sido **completamente adaptado** a PySide6 con todas las funcionalidades originales: - -1. ✅ **Autocompletado de variables** (timer-based) -2. ✅ **Autocompletado de métodos** (punto en objeto) -3. ✅ **Autocompletado global** (punto en línea vacía) -4. ✅ **Navegación completa** con teclado -5. ✅ **TAB para seleccionar** (funcionalidad principal) -6. ✅ **Filtrado en tiempo real** -7. ✅ **Links clickeables** para plots -8. ✅ **Ventanas emergentes** de edición -9. ✅ **Posicionamiento modeless** - -**¡El sistema está listo para uso completo!** 🚀 \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/class_base.py b/app/class_base.py similarity index 100% rename from class_base.py rename to app/class_base.py diff --git a/main_calc_app_pyside6.py b/app/main_calc_app.py similarity index 85% rename from main_calc_app_pyside6.py rename to app/main_calc_app.py index ff9bdb9..f4f1680 100644 --- a/main_calc_app_pyside6.py +++ b/app/main_calc_app.py @@ -32,11 +32,11 @@ from PySide6.QtWebEngineWidgets import QWebEngineView from PySide6.QtWebEngineCore import QWebEngineSettings # Importar componentes del CAS híbrido -from main_evaluation_puro import PureAlgebraicEngine, EvaluationResult -from tl_popup_pyside6 import InteractiveResultManager, PlotResult -from type_registry import get_registered_helper_functions, get_registered_base_context +from .main_evaluation 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 +from .sympy_helper import SympyTools as SympyHelper class InputTextEdit(QPlainTextEdit): @@ -406,7 +406,13 @@ class LatexPanel(QWidget): self._pending_equations.clear() if self._webview_available: - self.webview.page().runJavaScript("clearEquations();") + # Usar JavaScript para limpiar dinámicamente si MathJax está listo + if self._mathjax_ready: + self.webview.page().runJavaScript("clearEquations();") + else: + # Si MathJax no está listo, recargar HTML base limpio + html_content = self._generate_mathjax_html() + self.webview.setHtml(html_content) else: self._setup_text_browser() # Reset al estado inicial @@ -566,8 +572,8 @@ class AutocompletePopup(QWidget): class HybridCalculatorPySide6(QMainWindow): """Aplicación principal del CAS híbrido - VERSIÓN COMPLETA""" - SETTINGS_FILE = "hybrid_calc_settings.json" - HISTORY_FILE = "hybrid_calc_history.txt" + SETTINGS_FILE = "./.data/settings.json" + HISTORY_FILE = "./.data/history.txt" def __init__(self): super().__init__() @@ -972,6 +978,7 @@ class HybridCalculatorPySide6(QMainWindow): input_content = self.input_text.toPlainText() if not input_content.strip(): self._clear_output() + # NO limpiar panel LaTeX cuando no hay contenido - mantener ecuaciones previas return # Limpiar contexto del motor @@ -980,7 +987,7 @@ class HybridCalculatorPySide6(QMainWindow): self.engine.variables.clear() self.logger.debug("Contexto del motor limpiado") - # Limpiar panel LaTeX + # Limpiar panel LaTeX solo cuando hay contenido nuevo para evaluar if hasattr(self, '_latex_equations'): self._latex_equations.clear() self.latex_panel.clear_equations() @@ -1307,6 +1314,20 @@ class HybridCalculatorPySide6(QMainWindow): self.output_text.clear() cursor = self.output_text.textCursor() cursor.insertText(error_msg, self.output_formats['error']) + + # Intentar obtener ayuda para el error + try: + input_content = self.input_text.toPlainText() + last_line = input_content.strip().split('\n')[-1] if input_content.strip() else "" + + if last_line: + ayuda = self._obtener_ayuda(last_line) + if ayuda: + cursor.insertText("\n\n💡 Ayuda:\n", self.output_formats['helper']) + cursor.insertText(ayuda, self.output_formats['helper']) + + except Exception as e: + self.logger.debug(f"Error obteniendo ayuda: {e}") def _handle_output_link_click(self, link_id: str, result_object): """Maneja clicks en links del panel de salida""" @@ -1355,9 +1376,15 @@ class HybridCalculatorPySide6(QMainWindow): self._select_autocomplete() return True - # Detectar backspace para cerrar popup si se borra el punto - if event.key() == Qt.Key_Backspace and self._autocomplete_active: - QTimer.singleShot(1, self._check_dot_removal) + # Detectar backspace para filtrar autocompletado o cerrar popup + if event.key() == Qt.Key_Backspace: + if self._autocomplete_active: + # Filtrar dinámicamente al eliminar caracteres + QTimer.singleShot(1, self._filter_autocomplete) + QTimer.singleShot(1, self._check_dot_removal) + else: + # Activar autocompletado de variables si no está activo + QTimer.singleShot(50, self._schedule_variable_autocomplete_improved) # Procesar autocompletado después de insertar carácter if event.text() and not event.modifiers() & Qt.ControlModifier: @@ -1376,23 +1403,34 @@ class HybridCalculatorPySide6(QMainWindow): # Verificar si acabamos de navegar just_navigated = (time.time() - self._last_navigation_time) < 0.1 + # Caracteres que cierran el autocompletado + closing_chars = [' ', '+', '-', '*', '/', '(', ')', '=', ',', ';', '>', '<', '!'] + + # Cerrar autocompletado con símbolos y espacio + if self._autocomplete_active and event_text in closing_chars: + self._close_autocomplete_popup() + return + # Manejar autocompletado con punto if event_text == '.' and not self._popup_disabled_until_next_dot: if self._variable_popup_active: self._close_autocomplete_popup() self._handle_dot_autocomplete() - # Filtrar autocompletado si está activo - elif self._autocomplete_active and event_text and not just_navigated: + # Filtrar autocompletado si está activo (incluye caracteres alfanuméricos y backspace procesado) + elif self._autocomplete_active and not just_navigated: self._filter_autocomplete() # Marcar tiempo del último cambio if event_text: self._last_input_change = time.time() - # Programar autocompletado de variables + # Programar autocompletado de variables - también cuando se eliminen caracteres if not self._autocomplete_active and not self._popup_disabled_until_next_dot: self._schedule_variable_autocomplete_improved() + elif self._variable_popup_active and not just_navigated: + # Si el popup de variables está activo, regenerar dinámicamente + QTimer.singleShot(50, self._regenerate_variable_autocomplete) def _on_key_release(self, event): """Maneja eventos después de insertar carácter - mantenido para compatibilidad""" @@ -1502,6 +1540,26 @@ class HybridCalculatorPySide6(QMainWindow): for fname, fhint in sympy_functions: if fname not in current_names: suggestions.append((fname, fhint)) + + # Añadir variables del contexto actual + try: + context = self.engine._get_full_context() + current_names = {s[0] for s in suggestions} + + for name, value in context.items(): + if (not name.startswith('_') and + not callable(value) and + name not in ['sympy', 'math', 'numpy', 'plt', 'builtins'] and + name not in current_names): + + # Descripción del valor + value_str = str(value) + if len(value_str) > 20: + value_str = value_str[:17] + "..." + + suggestions.append((name, f"Variable = {value_str}")) + except Exception: + pass except Exception as e: self.logger.debug(f"Error obteniendo sugerencias globales: {e}") @@ -1525,6 +1583,60 @@ class HybridCalculatorPySide6(QMainWindow): # Programar para 800ms después self._variable_popup_timer.start(800) + def _regenerate_variable_autocomplete(self): + """Regenera dinámicamente el autocompletado de variables""" + if not self._variable_popup_active: + return + + # Obtener la palabra actual bajo el cursor + cursor = self.input_text.textCursor() + cursor.select(QTextCursor.WordUnderCursor) + filter_text = cursor.selectedText().lower() + + # Obtener variables del contexto actual + try: + context = self.engine._get_full_context() + variables = [] + + # Filtrar variables + for name, value in context.items(): + if (not name.startswith('_') and + not callable(value) and + name not in ['sympy', 'math', 'numpy', 'plt', 'builtins']): + + # Descripción del valor + value_str = str(value) + if len(value_str) > 20: + value_str = value_str[:17] + "..." + + variables.append((name, f"= {value_str}")) + + if variables: + variables.sort(key=lambda x: x[0]) + + # Filtrar por texto actual si existe + if filter_text: + filtered_vars = [ + (name, value) for name, value in variables + if name.lower().startswith(filter_text) and name.lower() != filter_text + ] + else: + filtered_vars = variables + + if filtered_vars: + # Actualizar sugerencias y popup + self._current_suggestions = variables + if self._autocomplete_popup: + self._autocomplete_popup.set_suggestions(filtered_vars) + self._autocomplete_popup.adjust_size() + else: + self._close_autocomplete_popup() + else: + self._close_autocomplete_popup() + + except Exception as e: + self.logger.debug(f"Error regenerando variables: {e}") + def _show_variable_autocomplete_improved(self): """Muestra autocompletado de variables disponibles""" if self._autocomplete_active or self._variable_popup_active: @@ -1601,40 +1713,127 @@ class HybridCalculatorPySide6(QMainWindow): def _show_variable_popup(self, variables: List[Tuple[str, str]]): """Muestra popup de variables""" self._variable_popup_active = True - self._autocomplete_active = False + self._autocomplete_active = True # También debe estar activo para el filtrado - # Convertir formato para el popup - suggestions = [(name, f"= {value}") for name, value in variables] - self._show_autocomplete_popup(suggestions, is_global_popup=False) + # Las variables ya vienen en el formato correcto (nombre, descripción) + self._show_autocomplete_popup(variables, is_global_popup=False) def _filter_autocomplete(self): - """Filtra las sugerencias del autocompletado""" - if not self._autocomplete_active or not self._autocomplete_trigger_pos: + """Filtra las sugerencias del autocompletado dinámicamente""" + if not self._autocomplete_active: return - # Obtener texto escrito después del punto cursor = self.input_text.textCursor() current_pos = cursor.position() - if current_pos > self._autocomplete_trigger_pos: - cursor.setPosition(self._autocomplete_trigger_pos) - cursor.setPosition(current_pos, QTextCursor.KeepAnchor) + # Para popup de variables (filtrar por texto parcial) + if self._variable_popup_active: + # Obtener la palabra actual bajo el cursor + cursor.select(QTextCursor.WordUnderCursor) filter_text = cursor.selectedText().lower() self._autocomplete_filter_text = filter_text - else: - self._autocomplete_filter_text = "" - - # Filtrar sugerencias - filtered = [] - for name, hint in self._current_suggestions: - if name.lower().startswith(self._autocomplete_filter_text): - filtered.append((name, hint)) - - if filtered and self._autocomplete_popup: - self._autocomplete_popup.set_suggestions(filtered) - self._autocomplete_popup.adjust_size() - else: - self._close_autocomplete_popup() + + # Si no hay texto que filtrar, regenerar popup con todas las variables + if not filter_text: + # Regenerar popup dinámicamente con variables actuales + try: + context = self.engine._get_full_context() + variables = [] + + # Filtrar variables + for name, value in context.items(): + if (not name.startswith('_') and + not callable(value) and + name not in ['sympy', 'math', 'numpy', 'plt', 'builtins']): + + # Descripción del valor + value_str = str(value) + if len(value_str) > 20: + value_str = value_str[:17] + "..." + + variables.append((name, f"= {value_str}")) + + if variables: + variables.sort(key=lambda x: x[0]) + self._current_suggestions = variables + if self._autocomplete_popup: + self._autocomplete_popup.set_suggestions(variables) + self._autocomplete_popup.adjust_size() + else: + self._close_autocomplete_popup() + except Exception: + self._close_autocomplete_popup() + return + + # Filtrar sugerencias de variables existentes + regenerar con contexto actual + try: + context = self.engine._get_full_context() + all_variables = [] + + # Obtener todas las variables actuales + for name, value in context.items(): + if (not name.startswith('_') and + not callable(value) and + name not in ['sympy', 'math', 'numpy', 'plt', 'builtins']): + + # Descripción del valor + value_str = str(value) + if len(value_str) > 20: + value_str = value_str[:17] + "..." + + all_variables.append((name, f"= {value_str}")) + + # Filtrar por el texto parcial + filtered = [] + for name, hint in all_variables: + if name.lower().startswith(filter_text): + filtered.append((name, hint)) + + # Actualizar sugerencias actuales + self._current_suggestions = all_variables + + if filtered and self._autocomplete_popup: + self._autocomplete_popup.set_suggestions(filtered) + self._autocomplete_popup.adjust_size() + else: + self._close_autocomplete_popup() + + except Exception: + # Fallback al método anterior + filtered = [] + for name, hint in self._current_suggestions: + if name.lower().startswith(filter_text): + filtered.append((name, hint)) + + if filtered and self._autocomplete_popup: + self._autocomplete_popup.set_suggestions(filtered) + self._autocomplete_popup.adjust_size() + else: + self._close_autocomplete_popup() + + # Para popup de métodos (filtrar después del punto) + elif self._autocomplete_trigger_pos: + if current_pos >= self._autocomplete_trigger_pos: + cursor.setPosition(self._autocomplete_trigger_pos) + cursor.setPosition(current_pos, QTextCursor.KeepAnchor) + filter_text = cursor.selectedText().lower() + self._autocomplete_filter_text = filter_text + else: + # Si el cursor está antes del punto trigger, cerrar popup + self._close_autocomplete_popup() + return + + # Filtrar sugerencias de métodos + filtered = [] + for name, hint in self._current_suggestions: + if name.lower().startswith(self._autocomplete_filter_text): + filtered.append((name, hint)) + + if filtered and self._autocomplete_popup: + self._autocomplete_popup.set_suggestions(filtered) + self._autocomplete_popup.adjust_size() + else: + self._close_autocomplete_popup() def _handle_arrow_key(self, direction: int): """Maneja navegación con flechas en el popup""" @@ -1674,41 +1873,92 @@ class HybridCalculatorPySide6(QMainWindow): # Para popup de variables if self._variable_popup_active: - # Reemplazar palabra actual + # Obtener el texto parcial ya escrito + current_pos = cursor.position() cursor.select(QTextCursor.WordUnderCursor) - cursor.insertText(text) + partial_text = cursor.selectedText() + + # Verificar si hay texto parcial que ya coincide con el inicio de la variable + if partial_text and text.lower().startswith(partial_text.lower()): + # Reemplazar completamente la palabra seleccionada con la variable completa + cursor.insertText(text) + elif partial_text: + # Si hay texto parcial pero no coincide, reemplazar completamente + cursor.insertText(text) + else: + # Si no hay texto parcial, insertar directamente + cursor.insertText(text) return # Para popup global (después de punto solo) if self._is_global_popup: - # Eliminar el punto y añadir función + # Eliminar el punto y añadir función/variable cursor.setPosition(self._autocomplete_trigger_pos - 1) cursor.setPosition(self._autocomplete_trigger_pos, QTextCursor.KeepAnchor) - cursor.insertText(text + "()") - # Posicionar cursor dentro de paréntesis - cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text) + 1) + + # Verificar si es una función o variable para decidir si agregar paréntesis + try: + context = self.engine._get_full_context() + if text in context: + obj = context[text] + if callable(obj): + # Es una función, agregar paréntesis + cursor.insertText(text + "()") + cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text) + 1) + else: + # Es una variable, no agregar paréntesis + cursor.insertText(text) + cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text)) + else: + # Por defecto, asumir que es función si no está en contexto + cursor.insertText(text + "()") + cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text) + 1) + except Exception: + # Fallback: agregar paréntesis por defecto + cursor.insertText(text + "()") + cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text) + 1) + self.input_text.setTextCursor(cursor) else: - # Para métodos de objeto + # Para métodos de objeto - considerar texto ya escrito después del punto if self._autocomplete_filter_text: - # Eliminar texto filtrado - start_pos = cursor.position() - len(self._autocomplete_filter_text) - cursor.setPosition(start_pos) - cursor.setPosition(start_pos + len(self._autocomplete_filter_text), QTextCursor.KeepAnchor) + # Calcular la posición correcta del texto ya escrito + current_pos = cursor.position() + filter_len = len(self._autocomplete_filter_text) + + # Seleccionar el texto filtrado para reemplazarlo + cursor.setPosition(current_pos - filter_len) + cursor.setPosition(current_pos, QTextCursor.KeepAnchor) cursor.removeSelectedText() + + # Insertar el texto completo del método + cursor.insertText(text + "()") + cursor.setPosition(cursor.position() - 1) + else: + # Si no hay texto filtrado, insertar normalmente + cursor.insertText(text + "()") + cursor.setPosition(cursor.position() - 1) - # Insertar método - cursor.insertText(text + "()") - cursor.setPosition(cursor.position() - 1) self.input_text.setTextCursor(cursor) def _check_dot_removal(self): """Verifica si se borró el punto que activó el autocompletado""" + if not self._autocomplete_active or not self._autocomplete_trigger_pos: + return + cursor = self.input_text.textCursor() - if cursor.position() > 0: - cursor.setPosition(cursor.position() - 1) - cursor.setPosition(cursor.position() + 1, QTextCursor.KeepAnchor) - if cursor.selectedText() == '.': + current_pos = cursor.position() + + # Si el cursor está antes de la posición del trigger, el punto fue eliminado + if current_pos < self._autocomplete_trigger_pos: + self._close_autocomplete_popup() + return + + # Verificar si todavía existe el punto en la posición trigger + if self._autocomplete_trigger_pos > 0: + cursor.setPosition(self._autocomplete_trigger_pos - 1) + cursor.setPosition(self._autocomplete_trigger_pos, QTextCursor.KeepAnchor) + if cursor.selectedText() != '.': self._close_autocomplete_popup() def _close_autocomplete_popup(self): diff --git a/main_evaluation_puro.py b/app/main_evaluation.py similarity index 99% rename from main_evaluation_puro.py rename to app/main_evaluation.py index 92912b7..047685a 100644 --- a/main_evaluation_puro.py +++ b/app/main_evaluation.py @@ -16,13 +16,13 @@ try: except ImportError: HAS_SYMPY_HELPER = False -from type_registry import ( +from .type_registry import ( get_registered_base_context, get_registered_tokenization_patterns, discover_and_register_types ) -from tl_bracket_parser import BracketParser -from tl_popup import PlotResult +from .tl_bracket_parser import BracketParser +from .tl_popup import PlotResult @dataclass diff --git a/simple_debug.py b/app/simple_debug.py similarity index 98% rename from simple_debug.py rename to app/simple_debug.py index afcccd1..b91a9f8 100644 --- a/simple_debug.py +++ b/app/simple_debug.py @@ -22,7 +22,7 @@ from datetime import datetime from pathlib import Path # Importar motores de evaluación -from main_evaluation_OLD import HybridEvaluationEngine +from .main_evaluation import HybridEvaluationEngine def run_debug(input_file: str, output_file: str = None, verbose: bool = False): @@ -59,7 +59,7 @@ def run_debug(input_file: str, output_file: str = None, verbose: bool = False): # Crear motor de evaluación según el módulo especificado if engine_module == 'main_evaluation_puro': - from main_evaluation_puro import PureAlgebraicEngine + from .main_evaluation import PureAlgebraicEngine engine = PureAlgebraicEngine() else: # Motor por defecto diff --git a/sympy_Base.py b/app/sympy_Base.py similarity index 99% rename from sympy_Base.py rename to app/sympy_Base.py index 5d4a438..84c4cfb 100644 --- a/sympy_Base.py +++ b/app/sympy_Base.py @@ -5,7 +5,7 @@ import sympy from sympy import Basic, Symbol, sympify from typing import Any, Optional, Dict import re -from class_base import ClassBase +from .class_base import ClassBase class SympyClassBase(ClassBase, sympy.Basic): diff --git a/sympy_helper.py b/app/sympy_helper.py similarity index 100% rename from sympy_helper.py rename to app/sympy_helper.py diff --git a/tl_bracket_parser.py b/app/tl_bracket_parser.py similarity index 100% rename from tl_bracket_parser.py rename to app/tl_bracket_parser.py diff --git a/tl_popup_pyside6.py b/app/tl_popup.py similarity index 100% rename from tl_popup_pyside6.py rename to app/tl_popup.py diff --git a/type_registry.py b/app/type_registry.py similarity index 100% rename from type_registry.py rename to app/type_registry.py diff --git a/calc.py b/calc.py index 22df595..1186510 100644 --- a/calc.py +++ b/calc.py @@ -1,270 +1,89 @@ #!/usr/bin/env python3 """ -Launcher principal para Calculadora MAV - CAS Híbrido -Este script maneja la inicialización y ejecución de la aplicación +Launcher para Calculadora MAV - Versión PySide6 con MathJax """ import sys import os import subprocess -import tkinter as tk -from tkinter import messagebox from pathlib import Path import importlib.util import logging -import datetime -import traceback -import platform -import platform +def check_dependencies(): + """Verifica que todas las dependencias estén instaladas""" + required_modules = [ + 'PySide6', + 'sympy', + 'numpy', + 'matplotlib' + ] + + missing = [] + for module in required_modules: + try: + __import__(module) + except ImportError: + missing.append(module) + + if missing: + print("❌ Faltan las siguientes dependencias:") + for module in missing: + print(f" - {module}") + print("\n💡 Para instalar las dependencias, ejecuta:") + print(" pip install -r requirements.txt") + return False + + print("✅ Todas las dependencias están instaladas") + return True -def setup_logging(): - """Configura el sistema de logging completo""" - MAX_LOG_FILES = 10 # Límite de archivos de log - log_dir = Path("logs") - log_dir.mkdir(exist_ok=True) - - # Archivo de log con timestamp - timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - log_file = log_dir / f"mav_calc_{timestamp}.log" - - # Eliminar logs antiguos si se supera el límite +def check_pyside6_webengine(): + """Verifica si PySide6 WebEngine está disponible""" try: - existing_logs = sorted( - [f for f in log_dir.glob("mav_calc_*.log") if f.is_file()], - key=os.path.getmtime - ) - if len(existing_logs) >= MAX_LOG_FILES: - logs_to_delete = existing_logs[:len(existing_logs) - MAX_LOG_FILES + 1] - for old_log in logs_to_delete: - old_log.unlink() - logging.info(f"Eliminado log antiguo: {old_log}") - except Exception as e: - logging.warning(f"No se pudieron eliminar logs antiguos: {e}") - - # Configurar logging - logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s', - handlers=[ - logging.FileHandler(log_file, encoding='utf-8'), - logging.StreamHandler(sys.stdout) - ] - ) - - logger = logging.getLogger(__name__) - - - return logger, log_file - - - -def log_error_with_context(logger, error, context=""): - """Registra un error con contexto completo""" - logger.error("=" * 50) - logger.error("ERROR DETECTADO") - logger.error("=" * 50) - if context: - logger.error(f"Contexto: {context}") - logger.error(f"Tipo de error: {type(error).__name__}") - logger.error(f"Mensaje: {str(error)}") - logger.error("Traceback completo:") - logger.error(traceback.format_exc()) - logger.error("Variables locales en el momento del error:") - - # Intentar capturar variables locales del frame donde ocurrió el error - try: - tb = traceback.extract_tb(error.__traceback__) - if tb: - last_frame = tb[-1] - logger.error(f" Archivo: {last_frame.filename}") - logger.error(f" Línea: {last_frame.lineno}") - logger.error(f" Función: {last_frame.name}") - logger.error(f" Código: {last_frame.line}") - except Exception as frame_error: - logger.error(f"No se pudieron obtener detalles del frame: {frame_error}") - - logger.error("=" * 50) - - -def show_error_with_log_info(error, log_file, context=""): - """Muestra error al usuario con información del log""" - error_msg = f"""Error en Calculadora MAV: - -{context} - -Error: {type(error).__name__}: {str(error)} - -INFORMACIÓN DE DEBUGGING: -• Log completo guardado en: {log_file} -• Para soporte, enviar el archivo de log -• Timestamp: {datetime.datetime.now()} - -¿Qué hacer ahora? -1. Revisar el archivo de log para más detalles -2. Intentar reiniciar la aplicación -3. Verificar dependencias con: python launcher.py --setup -4. Ejecutar tests con: python launcher.py --test -""" - - try: - root = tk.Tk() - root.withdraw() - - # Crear ventana de error personalizada - error_window = tk.Toplevel(root) - error_window.title("Error - Calculadora MAV") - error_window.geometry("600x400") - error_window.configure(bg="#2b2b2b") - - # Hacer la ventana modal - error_window.transient(root) - error_window.grab_set() - - # Centrar ventana - error_window.update_idletasks() - x = (error_window.winfo_screenwidth() // 2) - (error_window.winfo_width() // 2) - y = (error_window.winfo_screenheight() // 2) - (error_window.winfo_height() // 2) - error_window.geometry(f"+{x}+{y}") - - # Contenido - from tkinter import scrolledtext - - text_widget = scrolledtext.ScrolledText( - error_window, - font=("Consolas", 10), - bg="#1e1e1e", - fg="#ff6b6b", - wrap=tk.WORD - ) - text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - text_widget.insert("1.0", error_msg) - text_widget.config(state="disabled") - - # Botones - button_frame = tk.Frame(error_window, bg="#2b2b2b") - button_frame.pack(fill=tk.X, padx=10, pady=5) - - def open_log_folder(): - try: - if platform.system() == "Windows": - os.startfile(log_file.parent) - elif platform.system() == "Darwin": # macOS - subprocess.run(["open", str(log_file.parent)]) - else: # Linux - subprocess.run(["xdg-open", str(log_file.parent)]) - except Exception: - pass - - def copy_log_path(): - error_window.clipboard_clear() - error_window.clipboard_append(str(log_file)) - - tk.Button( - button_frame, - text="Abrir Carpeta de Logs", - command=open_log_folder, - bg="#4fc3f7", - fg="white" - ).pack(side=tk.LEFT, padx=5) - - tk.Button( - button_frame, - text="Copiar Ruta del Log", - command=copy_log_path, - bg="#82aaff", - fg="white" - ).pack(side=tk.LEFT, padx=5) - - tk.Button( - button_frame, - text="Cerrar", - command=error_window.destroy, - bg="#ff6b6b", - fg="white" - ).pack(side=tk.RIGHT, padx=5) - - # Esperar a que se cierre la ventana - error_window.wait_window() - root.destroy() - - except Exception as gui_error: - # Si falla la GUI, mostrar en consola - print("ERROR: No se pudo mostrar ventana de error") - print(error_msg) - print(f"Error adicional: {gui_error}") - - -# Variable global para el logger -logger = None -log_file = None - -def launch_application(): - - try: - # Importar y ejecutar la aplicación - from main_calc_app import HybridCalculatorApp - root = tk.Tk() - - app = HybridCalculatorApp(root) - root.mainloop() - - logger.info("Aplicación cerrada normalmente") - - except ImportError as e: - logger.error(f"Error de importación: {e}") - log_error_with_context(logger, e, "Importación de módulos de la aplicación") - show_error_with_log_info(e, log_file, "Error importando módulos de la aplicación") - except Exception as e: - logger.error(f"Error durante ejecución de la aplicación: {e}") - log_error_with_context(logger, e, "Ejecución de la aplicación principal") - show_error_with_log_info(e, log_file, "Error durante la ejecución de la aplicación") - + from PySide6.QtWebEngineWidgets import QWebEngineView + print("✅ PySide6 WebEngine disponible para MathJax") + return True + except ImportError: + print("⚠️ PySide6 WebEngine no disponible") + print(" Instalando QtWebEngine...") + try: + subprocess.run([sys.executable, '-m', 'pip', 'install', 'PySide6-WebEngine'], + check=True, capture_output=True) + print("✅ PySide6 WebEngine instalado correctamente") + return True + except subprocess.CalledProcessError: + print("❌ No se pudo instalar PySide6 WebEngine") + return False def main(): """Función principal del launcher""" - global logger, log_file + print("🚀 Iniciando Calculadora MAV - Diseño Minimalista 3 Paneles") + print("=" * 60) - # Configurar logging al inicio - try: - logger, log_file = setup_logging() - - except Exception as e: - print(f"ERROR: No se pudo configurar logging: {e}") - # Continuar sin logging si falla - logger = None - log_file = None - - logger.info("Iniciando verificaciones del sistema...") - - try: - - launch_application() - - logger.info("Aplicación cerrada - fin de sesión") - logger.info("=" * 60) - - except KeyboardInterrupt: - logger.info("Aplicación interrumpida por el usuario (Ctrl+C)") - sys.exit(0) - except Exception as e: - logger.error("Error crítico en main():") - log_error_with_context(logger, e, "Función principal del launcher") - show_error_with_log_info(e, log_file, "Error crítico durante el inicio") + # Verificar dependencias + if not check_dependencies(): sys.exit(1) + + # Verificar WebEngine + if not check_pyside6_webengine(): + print("⚠️ Continuando sin WebEngine (funcionalidad limitada)") + + print("\n🎯 Iniciando aplicación...") + try: + # Importar y ejecutar la aplicación + from app.main_calc_app import main as run_app + run_app() + + except ImportError as e: + print(f"❌ Error de importación: {e}") + print(" Verifica que todos los archivos del proyecto estén presentes") + sys.exit(1) + + except Exception as e: + print(f"❌ Error inesperado: {e}") + import traceback + traceback.print_exc() + sys.exit(1) if __name__ == "__main__": - # Configurar logging básico para manejo de argumentos - temp_logger = None - temp_log_file = None - - # Inicio normal - try: - main() - except Exception as e: - if temp_logger: - log_error_with_context(temp_logger, e, "Error crítico en __main__") - print(f"ERROR CRÍTICO: {e}") - if temp_log_file: - print(f"Ver log completo en: {temp_log_file}") - sys.exit(1) + main() \ No newline at end of file diff --git a/hybrid_calc_history.txt b/hybrid_calc_history.txt deleted file mode 100644 index eec318d..0000000 --- a/hybrid_calc_history.txt +++ /dev/null @@ -1,5 +0,0 @@ - -$$Brix = \frac{Brix_{syrup} \cdot \delta_{syrup} + (Brix_{water} \cdot \delta_{water} \cdot Rateo)}{\delta_{syrup} + \delta_{water} \cdot Rateo}$$ -Brix=9.5 -Rateo=? - diff --git a/hybrid_calc_history_pyside6.txt b/hybrid_calc_history_pyside6.txt deleted file mode 100644 index 8307c95..0000000 --- a/hybrid_calc_history_pyside6.txt +++ /dev/null @@ -1,2 +0,0 @@ - -a=4/7 diff --git a/hybrid_calc_settings.json b/hybrid_calc_settings.json deleted file mode 100644 index efa7dd0..0000000 --- a/hybrid_calc_settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "window_geometry": { - "x": 119, - "y": 143, - "width": 1220, - "height": 700 - }, - "debug_mode": false, - "latex_panel_visible": true, - "sash_pos_x": 450, - "splitter_sizes": [ - 300, - 396 - ] -} \ No newline at end of file diff --git a/latex_debug.html b/latex_debug.html deleted file mode 100644 index 23b7236..0000000 --- a/latex_debug.html +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - Ecuaciones LaTeX - PyWebView - - - - - - - - - - -
\n -
-
-
$$72$$
-
-
- -
-
-
$$36$$
-
-
- -
-
-
$$2592$$
-
-
- -
-
-
$$\frac{\sqrt{8 y - 25}}{e}$$
-
-
- -
-
-
$$t = \frac{\sqrt{8 y - 25}}{e}$$
-
-
\n
- - -
- ✓ PyWebView activo - MathJax cargándose... -
- - - \ No newline at end of file diff --git a/launch_pyside6.py b/launch_pyside6.py deleted file mode 100644 index 16e530b..0000000 --- a/launch_pyside6.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python3 -""" -Launcher para Calculadora MAV - Versión PySide6 con MathJax -""" -import sys -import os -import subprocess -from pathlib import Path -import importlib.util -import logging - -def check_dependencies(): - """Verifica que todas las dependencias estén instaladas""" - required_modules = [ - 'PySide6', - 'sympy', - 'numpy', - 'matplotlib' - ] - - missing = [] - for module in required_modules: - try: - __import__(module) - except ImportError: - missing.append(module) - - if missing: - print("❌ Faltan las siguientes dependencias:") - for module in missing: - print(f" - {module}") - print("\n💡 Para instalar las dependencias, ejecuta:") - print(" pip install -r requirements.txt") - return False - - print("✅ Todas las dependencias están instaladas") - return True - -def check_pyside6_webengine(): - """Verifica si PySide6 WebEngine está disponible""" - try: - from PySide6.QtWebEngineWidgets import QWebEngineView - print("✅ PySide6 WebEngine disponible para MathJax") - return True - except ImportError: - print("⚠️ PySide6 WebEngine no disponible") - print(" Instalando QtWebEngine...") - try: - subprocess.run([sys.executable, '-m', 'pip', 'install', 'PySide6-WebEngine'], - check=True, capture_output=True) - print("✅ PySide6 WebEngine instalado correctamente") - return True - except subprocess.CalledProcessError: - print("❌ No se pudo instalar PySide6 WebEngine") - return False - -def main(): - """Función principal del launcher""" - print("🚀 Iniciando Calculadora MAV - Diseño Minimalista 3 Paneles") - print("=" * 60) - - # Verificar dependencias - if not check_dependencies(): - sys.exit(1) - - # Verificar WebEngine - if not check_pyside6_webengine(): - print("⚠️ Continuando sin WebEngine (funcionalidad limitada)") - - print("\n🎯 Iniciando aplicación...") - - try: - # Importar y ejecutar la aplicación - from main_calc_app_pyside6 import main as run_app - run_app() - - except ImportError as e: - print(f"❌ Error de importación: {e}") - print(" Verifica que todos los archivos del proyecto estén presentes") - sys.exit(1) - - except Exception as e: - print(f"❌ Error inesperado: {e}") - import traceback - traceback.print_exc() - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/main_calc_app.py b/main_calc_app.py deleted file mode 100644 index 9d42c94..0000000 --- a/main_calc_app.py +++ /dev/null @@ -1,4247 +0,0 @@ -""" -Calculadora MAV CAS Híbrida - Aplicación principal -VERSIÓN ADAPTADA AL NUEVO SISTEMA DE TIPOS -""" -import tkinter as tk -from tkinter import scrolledtext, messagebox, Menu, filedialog -import tkinter.font as tkFont -import json -import logging # <--- AÑADIDO -import os -from pathlib import Path -import threading -from typing import List, Dict, Any, Optional -import re -import time - -# ========== IMPORTS PARA SISTEMA DE AYUDA ========== -# Para la ayuda en HTML -MARKDOWN_AVAILABLE = False -HTML_VIEWER_TYPE = None - -try: - import markdown - MARKDOWN_AVAILABLE = True -except ImportError: - # markdown not available, MARKDOWN_AVAILABLE remains False - pass - -# Intentar importar visores HTML -try: - import tkinterweb - HTML_VIEWER_TYPE = "tkinterweb" -except ImportError: - try: - from tkhtmlview import HTMLScrolledText - HTML_VIEWER_TYPE = "tkhtmlview" - except ImportError: - HTML_VIEWER_TYPE = None - -# Usar logging para estas advertencias iniciales -module_logger = logging.getLogger(__name__) -if not MARKDOWN_AVAILABLE: - module_logger.warning("La librería 'markdown' no está instalada. La ayuda se mostrará en texto plano.") -if MARKDOWN_AVAILABLE and HTML_VIEWER_TYPE is None: - module_logger.warning("'markdown' está disponible, pero no se encontró un visor HTML (tkinterweb/tkhtmlview). La ayuda se mostrará en texto plano.") - -# ========== IMPORTS ADAPTADOS AL NUEVO SISTEMA ========== -# Importar componentes del CAS híbrido con nuevo sistema de tipos -from main_evaluation_puro import PureAlgebraicEngine, EvaluationResult -from tl_popup import InteractiveResultManager, PlotResult -from type_registry import get_registered_helper_functions, get_registered_base_context -import sympy -from sympy_helper import SympyTools as SympyHelper - - -class HybridCalculatorApp: - """Aplicación principal del CAS híbrido - ADAPTADA AL NUEVO SISTEMA""" - - SETTINGS_FILE = "hybrid_calc_settings.json" - HISTORY_FILE = "hybrid_calc_history.txt" - HELP_FILE = "readme.md" # ========== NUEVO: Archivo de ayuda externo ========== - - def __init__(self, root: tk.Tk): - self.root = root - - # Configurar logging DEBUG para ver qué pasa - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s') - self.logger = logging.getLogger(__name__) - self.logger.setLevel(logging.DEBUG) - - # ========== INSTANCIAS DEL SISTEMA ========== - # Motor de evaluación (instancia única) - self.engine = PureAlgebraicEngine() - - # Manager de contenido interactivo - self.interactive_manager = None # Se inicializará en setup_interactive_manager() - - # ========== CONFIGURACIÓN DE INTERFAZ ========== - self.settings = self._load_settings() - self.debug = self.settings.get("debug_mode", False) - - # ========== VARIABLES DE AUTOCOMPLETADO ========== - self._autocomplete_popup = None - self._autocomplete_listbox = None - self._autocomplete_active = False - self._autocomplete_suggestions = [] - self._autocomplete_filter_text = "" - self._autocomplete_trigger_pos = "" - self._popup_disabled_until_next_dot = False - self._variable_popup_active = False - self._last_navigation_time = 0 - - # ========== VARIABLES PANEL LATEX ========== - self.latex_panel_visible = False - self._latex_equations = [] - self.latex_renderer = None - self._webview_available = False - self._webview_type = None - self._js_available = False - - # ========== VARIABLES DE ESTADO FALTANTES ========== - self._cached_input_font = None - self._debounce_job = None - self._syncing_yview = False - self.output_buffer = [] - - # Variables para autocompletado de variables - self._variable_popup_job = None - self._last_input_change = 0 - - # ========== CONFIGURACIÓN DE VENTANA ========== - self._setup_window() - self._setup_icon() - - # ========== CONSTRUCCIÓN DE INTERFAZ ========== - self.create_widgets() - self.create_menu() - self.setup_output_tags() - self.setup_scroll_sync() - self.setup_interactive_manager() - - # ========== CONFIGURACIÓN FINAL ========== - self._setup_dynamic_helpers() - self.load_history() - - # ========== CONFIGURACIÓN INICIAL ========== - # Configurar bindings de teclado - self._setup_key_bindings() - - def _setup_window(self): - """Configura la ventana principal""" - self.root.title("Calculadora MAV - CAS Híbrido") - self.root.geometry(self.settings.get("window_geometry", "1000x700")) - self.root.configure(bg="#2b2b2b") - - # Configurar eventos de cierre - self.root.protocol("WM_DELETE_WINDOW", self.on_close) - - # ========== BARRA DE ESTADO ========== - self.status_frame = tk.Frame(self.root, bg="#2b2b2b", height=25) - self.status_frame.pack(side=tk.BOTTOM, fill=tk.X) - self.status_frame.pack_propagate(False) - - self.status_label = tk.Label( - self.status_frame, - text="🔢 Calculadora MAV - Sistema Algebraico Puro", - bg="#2b2b2b", - fg="#80c7f7", - font=("Consolas", 9), - anchor=tk.W - ) - self.status_label.pack(side=tk.LEFT, padx=10, pady=2) - - - - def _setup_key_bindings(self): - """Configura los bindings de teclado""" - try: - # ========== BINDINGS DE TECLADO ========== - self.input_text.bind("", self.on_key_release) - self.input_text.bind("", self.on_key_press) - self.input_text.bind("", self._on_input_click) - self.input_text.bind("", lambda e: self._close_autocomplete_popup()) - - # Bindings para navegación del autocompletado - self.input_text.bind("", self._handle_arrow_key) - self.input_text.bind("", self._handle_arrow_key) - self.input_text.bind("", self._handle_tab_key) - self.input_text.bind("", self._handle_escape_key) - - self.logger.debug("✅ Bindings de teclado configurados") - except Exception as e: - self.logger.error(f"❌ Error configurando bindings: {e}") - - def _setup_dynamic_helpers(self): - """Configura helpers dinámicamente desde el registro de tipos""" - try: - # Obtener helpers registrados dinámicamente - self.HELPERS = get_registered_helper_functions() - - # Añadir SympyHelper.Helper al final - self.HELPERS.append(SympyHelper.Helper) - - # Usar logger en lugar de print, y sin emoji para la consola - self.logger.info(f"Helpers dinámicos cargados: {len(self.HELPERS)}") # Original: 🆘 - - except Exception as e: - # Usar logger en lugar de print, y sin emoji para la consola - self.logger.error(f"Error cargando helpers dinámicos: {e}", exc_info=True) # Original: ⚠️ - # Fallback a helpers básicos - self.HELPERS = [SympyHelper.Helper] - - def reload_types(self): - """Recarga el sistema de tipos (útil para desarrollo)""" - try: - self.logger.info("Recargando sistema de tipos...") - - # Recargar helpers - self._setup_dynamic_helpers() - # Re-evaluar contenido actual - self._evaluate_and_update() - - self.logger.info("Sistema de tipos recargado.") - - except Exception as e: - self.logger.error(f"Error recargando tipos: {e}", exc_info=True) - messagebox.showerror("Error", f"Error recargando tipos:\n{e}") - - def show_types_info(self): - """Muestra información sobre tipos disponibles""" - try: - context_info = self.engine.get_context_info() - - info_text = f"""INFORMACIÓN DEL SISTEMA ALGEBRAICO PURO - -Ecuaciones en el sistema: {context_info.get('equations', 0)} -Variables definidas: {context_info.get('variables', 0)} -Variables activas: {', '.join(context_info.get('variable_names', []))} - -CARACTERÍSTICAS: -• Sistema de ecuaciones puras con SymPy -• Todas las asignaciones son ecuaciones -• Resolución automática de sistemas -• Evaluación numérica inteligente -• Atajo x=? equivale a solve(x) -""" - - # Mostrar en ventana - self._show_help_window("Información del Sistema", info_text) - - except Exception as e: - messagebox.showerror("Error", f"Error obteniendo información del sistema:\n{e}") - - def _setup_icon(self): - """Configura el ícono de la aplicación""" - try: - script_dir = Path(__file__).resolve().parent - icon_path = script_dir / "icon.png" - - if not icon_path.is_file(): - self.logger.warning(f"Archivo de ícono no encontrado en '{icon_path}'.") - return - - self.app_icon = tk.PhotoImage(file=str(icon_path)) - self.root.iconphoto(True, self.app_icon) - except tk.TclError as e: - self.logger.warning(f"No se pudo cargar el ícono desde '{icon_path}'. Error de Tkinter: {e}") - except Exception as e: - self.logger.warning(f"Ocurrió un error inesperado al cargar el ícono desde '{icon_path}': {e}", exc_info=True) - - def _load_settings(self) -> Dict[str, Any]: - """Carga configuración de la aplicación""" - if os.path.exists(self.SETTINGS_FILE): - try: - with open(self.SETTINGS_FILE, "r", encoding="utf-8") as f: - return json.load(f) - except (IOError, json.JSONDecodeError): - return {} - return {} - - def _save_settings(self): - """Guarda configuraciones en archivo JSON""" - try: - # Obtener geometría actual - self.settings["window_geometry"] = self.root.geometry() - - # Guardar posición del panel divisor si existe - if hasattr(self, 'paned_window'): - sash_pos = self.paned_window.sash_coord(0)[0] - self.settings["sash_pos_x"] = sash_pos - - with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f: - json.dump(self.settings, f, indent=4, ensure_ascii=False) - except Exception as e: - if self.debug: - self.logger.error(f"Error guardando configuración: {e}", exc_info=True) - - - - def create_widgets(self): - """Crea la interfaz gráfica con panel LaTeX opcional y expandible""" - # Frame principal - main_frame = tk.Frame(self.root, bg="#2b2b2b", bd=0) - main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) - - # Frame para el contenido principal (paneles + botón expandible) - content_frame = tk.Frame(main_frame, bg="#2b2b2b") - content_frame.pack(fill=tk.BOTH, expand=True) - - # Panel dividido principal (horizontal) - solo 2 paneles inicialmente - self.paned_window = tk.PanedWindow( - content_frame, orient=tk.HORIZONTAL, bg="#2b2b2b", - sashrelief=tk.FLAT, sashwidth=4, bd=0, - showhandle=False, opaqueresize=True, - ) - self.paned_window.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - # Panel de entrada (limitado a ~50 caracteres) - initial_input_width = min(self.settings.get("sash_pos_x", 450), 450) # Máximo 450px - - self.input_text = scrolledtext.ScrolledText( - self.paned_window, - font=("Consolas", 11), - bg="#1e1e1e", - fg="#d4d4d4", - insertbackground="#ffffff", - selectbackground="#264f78", - undo=True, - wrap=tk.NONE, - borderwidth=0, - highlightthickness=0, - relief=tk.FLAT, - ) - self.paned_window.add( - self.input_text, - width=initial_input_width, - stretch="never", # No se expande automáticamente - minsize=200 - ) - - # Panel de salida - self.output_text = scrolledtext.ScrolledText( - self.paned_window, - font=("Consolas", 11), - bg="#0f0f0f", - fg="#00ff00", - state="disabled", - wrap=tk.NONE, - borderwidth=0, - highlightthickness=0, - relief=tk.FLAT, - ) - self.paned_window.add( - self.output_text, - stretch="always", - minsize=200 - ) - - # NUEVO: Botón expandible para el panel LaTeX - self._create_expandable_latex_button(content_frame) - - # NUEVO: Configurar panel LaTeX (oculto inicialmente) - self._setup_latex_panel_expandable() - - # Estado del panel LaTeX - self.latex_panel_visible = self.settings.get("latex_panel_visible", False) - self._latex_equations = [] - - # Configurar eventos - self.input_text.bind("", lambda e: self._show_context_menu(e, "input")) - self.output_text.bind("", lambda e: self._show_context_menu(e, "output")) - - # Configurar scroll sincronizado - self.setup_scroll_sync() - - # Configurar tags de salida - self.setup_output_tags() - - # Crear menú - self.create_menu() - - # Restaurar estado del panel LaTeX si estaba visible - if self.latex_panel_visible: - self.root.after(100, self._show_latex_panel) # Delay para que la UI esté lista - - def _create_expandable_latex_button(self, parent): - """Crea el botón expandible para mostrar/ocultar el panel LaTeX""" - # Frame para el botón (vertical en el borde derecho) - self.expand_button_frame = tk.Frame( - parent, - bg="#3c3c3c", - width=20, - bd=0, - relief=tk.FLAT - ) - self.expand_button_frame.pack(side=tk.RIGHT, fill=tk.Y) - self.expand_button_frame.pack_propagate(False) - - # Botón principal (vertical) - SIN TOOLTIP que interfiere - self.latex_expand_button = tk.Button( - self.expand_button_frame, - text="📐", # Icono de ecuaciones - font=("Segoe UI Symbol", 12), - bg="#3c3c3c", - fg="#80c7f7", - activebackground="#4fc3f7", - activeforeground="white", - bd=0, - relief=tk.FLAT, - cursor="hand2", - command=self._toggle_latex_panel # Click simple directo - ) - self.latex_expand_button.pack(expand=True, fill=tk.BOTH, padx=2, pady=10) - - # Solo evento de click derecho para info (SIN tooltip que interfiere) - self.latex_expand_button.bind("", lambda e: self._on_latex_button_info()) - - # Indicador de contenido LaTeX disponible (inicialmente oculto) - self.latex_indicator = tk.Label( - self.expand_button_frame, - text="●", - font=("Arial", 8), - bg="#3c3c3c", - fg="#4fc3f7", - ) - # No empaquetar inicialmente (se muestra cuando hay contenido) - - def _on_latex_button_info(self): - """Maneja click derecho en el botón LaTeX (mostrar info en status bar)""" - equation_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0 - status_text = f"📐 Panel LaTeX: {equation_count} ecuaciones disponibles" - - if hasattr(self, 'status_label'): - original_text = self.status_label.cget("text") - self.status_label.config(text=status_text) - # Restaurar texto original después de 2 segundos - self.root.after(2000, lambda: self.status_label.config(text=original_text)) - - def _toggle_latex_panel(self): - """Muestra/oculta el panel LaTeX con animación""" - if not hasattr(self, 'latex_panel_visible'): - self.latex_panel_visible = False - - if self.latex_panel_visible: - self._hide_latex_panel() - else: - self._show_latex_panel() - - # Guardar estado en configuración - self.settings["latex_panel_visible"] = self.latex_panel_visible - - def _show_latex_panel(self): - """Muestra el panel LaTeX con animación suave""" - if not hasattr(self, 'latex_panel') or self.latex_panel_visible: - return - - try: - # Cambiar icono del botón - self.latex_expand_button.config(text="📐", bg="#4fc3f7") - - # Agregar el panel LaTeX al PanedWindow - self.paned_window.add( - self.latex_panel, - width=self._min_latex_pane_width, - stretch="never", - minsize=self._min_latex_pane_width - ) - - # Actualizar flag - self.latex_panel_visible = True - - # Actualizar contenido si hay ecuaciones pendientes - if hasattr(self, '_latex_equations') and self._latex_equations: - self._refresh_latex_content() - - # Actualizar indicador - self._update_latex_indicator() - - if self.debug: - self.logger.info("Panel LaTeX mostrado") - - except Exception as e: - self.logger.error(f"Error mostrando panel LaTeX: {e}") - - def _hide_latex_panel(self): - """Oculta el panel LaTeX con animación suave""" - if not hasattr(self, 'latex_panel') or not self.latex_panel_visible: - return - - try: - # Cambiar icono del botón - self.latex_expand_button.config(text="📐", bg="#3c3c3c") - - # Remover el panel del PanedWindow - self.paned_window.forget(self.latex_panel) - - # Actualizar flag - self.latex_panel_visible = False - - if self.debug: - self.logger.info("Panel LaTeX ocultado") - - except Exception as e: - self.logger.error(f"Error ocultando panel LaTeX: {e}") - - def _update_latex_indicator(self): - """Actualiza el indicador visual de contenido LaTeX""" - if not hasattr(self, 'latex_indicator'): - return - - equation_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0 - - if equation_count > 0: - # Mostrar indicador - if not self.latex_indicator.winfo_ismapped(): - self.latex_indicator.pack(side=tk.BOTTOM, pady=2) - else: - # Ocultar indicador - if self.latex_indicator.winfo_ismapped(): - self.latex_indicator.pack_forget() - - def _setup_latex_panel_expandable(self): - """Configura el panel LaTeX expandible con pywebview""" - try: - # Frame para el panel LaTeX (crear pero no agregar al PanedWindow todavía) - self.latex_panel = tk.Frame(self.root, bg="#1a1a1a", bd=1, relief=tk.SOLID) - - # Título del panel - title_frame = tk.Frame(self.latex_panel, bg="#1a1a1a", height=25) - title_frame.pack(fill=tk.X, pady=(2, 0)) - title_frame.pack_propagate(False) - - title_label = tk.Label( - title_frame, - text="📐 Ecuaciones & Asignaciones", - bg="#1a1a1a", - fg="#80c7f7", - font=("Consolas", 9, "bold"), - anchor=tk.W - ) - title_label.pack(side=tk.LEFT, padx=5, pady=2) - - # Botón de cerrar en el título - close_button = tk.Button( - title_frame, - text="✕", - font=("Arial", 8), - bg="#1a1a1a", - fg="#808080", - activebackground="#ff6b6b", - activeforeground="white", - bd=0, - relief=tk.FLAT, - cursor="hand2", - command=self._hide_latex_panel - ) - close_button.pack(side=tk.RIGHT, padx=5) - - # NUEVO: Sistema con pywebview (más robusto para MathJax) - try: - import webview - - # JavaScript siempre disponible con pywebview - self._js_available = True - self.logger.debug("✅ pywebview disponible - JavaScript totalmente habilitado") - - # Frame para contener el webview - html_frame = tk.Frame(self.latex_panel, bg="#1a1a1a") - html_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) - - # PLACEHOLDER: El webview se creará cuando sea necesario - # pywebview requiere un manejo especial para integración con tkinter - self.latex_webview = None - self.latex_webview_frame = html_frame - self._webview_ready = False - - self._webview_available = True - self._webview_type = "pywebview" - - self.logger.debug("🌐 pywebview configurado - webview se creará dinámicamente") - - except ImportError: - # Fallback a tkinterweb si pywebview no está disponible - try: - import tkinterweb - - self.logger.warning("⚠️ pywebview no disponible, usando tkinterweb como fallback") - - # Verificar disponibilidad de JavaScript en tkinterweb - try: - import pythonmonkey - self._js_available = True - self.logger.debug("✅ tkinterweb con PythonMonkey disponible") - except ImportError: - self._js_available = False - self.logger.debug("ℹ️ tkinterweb sin PythonMonkey - modo fallback") - - # Crear el visor HTML con tkinterweb - html_frame = tk.Frame(self.latex_panel, bg="#1a1a1a") - html_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) - - self.latex_webview = tkinterweb.HtmlFrame( - html_frame, - messages_enabled=False - ) - self.latex_webview.pack(fill=tk.BOTH, expand=True) - - # Cargar HTML base optimizado - base_html = self._generate_base_html() - self.latex_webview.load_html(base_html) - - self._webview_available = True - self._webview_type = "tkinterweb" - - except ImportError: - # Sin ninguna de las dos librerías - messagebox.showerror( - "Dependencia Requerida", - "Esta calculadora requiere 'pywebview' o 'tkinterweb' para el renderizado LaTeX.\n\n" - "Instala con: pip install pywebview\n" - "O: pip install tkinterweb\n\n" - "La aplicación se cerrará." - ) - self.root.quit() - return - - # Configurar tamaño del panel - self._min_latex_pane_width = 300 - - # Mostrar información sobre el tipo de visor usado - if self.debug: - self.logger.info(f"Panel LaTeX expandible configurado con: {self._webview_type}") - - except Exception as e: - self.logger.error(f"Error configurando panel LaTeX expandible: {e}") - # Si falla completamente, no agregar el tercer panel - pass - - def _refresh_latex_content(self): - """Refresca el contenido del panel LaTeX cuando se muestra""" - if not hasattr(self, 'latex_panel') or not self.latex_panel_visible: - return - - try: - # Actualizar contenido si hay ecuaciones pendientes - if hasattr(self, '_latex_equations') and self._latex_equations: - self._update_latex_panel() - - except Exception as e: - self.logger.debug(f"Error refrescando contenido LaTeX: {e}") - - def _generate_base_html(self): - """Genera el HTML base optimizado según el tipo de webview""" - if self._webview_type == "pywebview": - return self._generate_html_for_pywebview() - elif self._js_available: - return self._generate_html_with_javascript() - else: - return self._generate_html_fallback() - - def _generate_html_with_javascript(self): - """HTML con JavaScript completo para tkinterweb""" - return r""" - - - - - - Ecuaciones LaTeX - - - - - - - - - - - - - - - -
-
- 📐 Panel de Ecuaciones LaTeX
- Renderizado con MathJax + fuentes matemáticas
- Las ecuaciones se mostrarán aquí automáticamente -
-
- - - - - """ - - def _generate_html_for_pywebview(self): - """HTML optimizado específicamente para pywebview (más simple y robusto)""" - return r""" - - - - - - Ecuaciones LaTeX - PyWebView - - - - - - - - - - -
-
- 📐 Panel de Ecuaciones LaTeX
- Renderizado con MathJax en PyWebView
- Las ecuaciones aparecerán aquí automáticamente -
-
- -
- ✓ PyWebView activo - MathJax cargándose... -
- - - """ - - def _generate_html_fallback(self): - """HTML fallback simple sin JavaScript""" - return r""" - - - - - - Ecuaciones LaTeX (Modo Fallback) - - - - -
-
- 📐 Panel de Ecuaciones LaTeX (Modo Fallback)
- Renderizado texto mejorado - Sin JavaScript/MathJax
- Las ecuaciones se mostrarán en formato texto matemático mejorado -
-
- -
- ℹ️ Modo Fallback - Matemáticas en Texto Mejorado
- Para MathJax completo: pip install tkinterweb[javascript] -
- - - """ - - def setup_interactive_manager(self): - """Configura el gestor de resultados interactivos""" - self.interactive_manager = InteractiveResultManager(self.root) - # Configurar callback para actualizar el panel de entrada cuando se edite una expresión - self.interactive_manager.set_update_callback(self._update_input_expression) - - def create_menu(self): - """Crea el menú de la aplicación""" - menubar = Menu(self.root) - self.root.config(menu=menubar) - - # Menú Archivo - file_menu = Menu(menubar, tearoff=0) - menubar.add_cascade(label="Archivo", menu=file_menu) - file_menu.add_command(label="Nuevo", command=self.new_session) - file_menu.add_separator() - file_menu.add_command(label="Cargar...", command=self.load_file) - file_menu.add_command(label="Guardar como...", command=self.save_file) - file_menu.add_separator() - file_menu.add_command(label="Salir", command=self.on_close) - - # Menú Editar - edit_menu = Menu(menubar, tearoff=0) - menubar.add_cascade(label="Editar", menu=edit_menu) - edit_menu.add_command(label="Limpiar entrada", command=self.clear_input) - edit_menu.add_command(label="Limpiar salida", command=self.clear_output) - edit_menu.add_separator() - edit_menu.add_command(label="Limpiar historial", command=self.clear_history) - - # NUEVO: Menú Ver - view_menu = Menu(menubar, tearoff=0) - menubar.add_cascade(label="Ver", menu=view_menu) - view_menu.add_command( - label="📐 Panel LaTeX", - command=self._toggle_latex_panel, - accelerator="Doble-click borde derecho" - ) - view_menu.add_separator() - view_menu.add_command(label="Información del sistema", command=self.show_types_info) - - # Menú Herramientas (simplificado) - tools_menu = Menu(menubar, tearoff=0, bg="#3c3c3c", fg="white") - menubar.add_cascade(label="Herramientas", menu=tools_menu) - tools_menu.add_command(label="Recargar Tipos Personalizados", command=self.reload_types) - tools_menu.add_separator() - tools_menu.add_command(label="📊 Información del Sistema", command=self.show_types_info) - tools_menu.add_command(label="🔍 Diagnóstico MathJax", command=self._diagnose_mathjax) - tools_menu.add_command(label="🔧 Test tkinterweb", command=self._test_tkinterweb_mathjax) - tools_menu.add_command(label="🌐 Abrir HTML en Navegador", command=self._open_debug_html_in_browser) - tools_menu.add_separator() - tools_menu.add_command(label="📊 Estado Panel LaTeX", command=self._show_latex_panel_status) - tools_menu.add_command(label="🔄 Forzar Actualización HTML", command=self._force_html_update) - - # ========== MENÚ TIPOS (NUEVO) ========== - types_menu = Menu(menubar, tearoff=0, bg="#3c3c3c", fg="white") - menubar.add_cascade(label="Tipos", menu=types_menu) - types_menu.add_command(label="Información de tipos", command=self.show_types_info) - types_menu.add_separator() - types_menu.add_command(label="Sintaxis de tipos", command=self.show_types_syntax) - - # Menú Ayuda (actualizado) - help_menu = Menu(menubar, tearoff=0) - menubar.add_cascade(label="Ayuda", menu=help_menu) - help_menu.add_command(label="Guía rápida", command=self.show_quick_guide) - help_menu.add_command(label="Sintaxis", command=self.show_syntax_help) - help_menu.add_command(label="Funciones SymPy", command=self.show_sympy_functions) - help_menu.add_separator() - help_menu.add_command(label="Acerca de", command=self.show_about) - - def setup_scroll_sync(self): - """Configura scroll sincronizado entre paneles""" - def _yscroll_input_command(*args): - self.input_text.vbar.set(*args) - if not self._syncing_yview: - self._syncing_yview = True - self.output_text.yview_moveto(args[0]) - self._syncing_yview = False - - def _yscroll_output_command(*args): - self.output_text.vbar.set(*args) - if not self._syncing_yview: - self._syncing_yview = True - self.input_text.yview_moveto(args[0]) - self._syncing_yview = False - - def _unified_mouse_wheel(event): - if self._syncing_yview: - return "break" - if hasattr(event, "widget") and event.widget: - event.widget.yview_scroll(int(-1 * (event.delta / 120)), "units") - return "break" - - self.input_text.config(yscrollcommand=_yscroll_input_command) - self.output_text.config(yscrollcommand=_yscroll_output_command) - self.input_text.bind("", _unified_mouse_wheel) - self.output_text.bind("", _unified_mouse_wheel) - - def setup_output_tags(self): - """Configura tags de formato para el panel de salida""" - default_font = self._get_input_font() - - # Crear una fuente específica para errores (bold) - error_font = tkFont.Font(family=default_font.cget("family"), size=default_font.cget("size"), weight="bold") - - # Tag base - self.output_text.tag_configure("base", font=default_font, foreground="#d4d4d4") - - # Tags específicos - # Sympy y tipos base - self.output_text.tag_configure("symbolic", foreground="#9cdcfe") # Azul claro (SymPy) - self.output_text.tag_configure("numeric", foreground="#b5cea8") # Verde (Números) - self.output_text.tag_configure("boolean", foreground="#569cd6") # Azul (Booleanos) - self.output_text.tag_configure("string", foreground="#ce9178") # Naranja (Strings) - - # Tipos registrados dinámicamente (usar un color base) - self.output_text.tag_configure("custom_type", foreground="#4ec9b0") # Turquesa (Tipos Custom) - - # Estado de la aplicación - self.output_text.tag_configure("error", foreground="#f44747", font=error_font) # Rojo - self.output_text.tag_configure("comment", foreground="#6a9955") # Verde Oliva (Comentarios) - self.output_text.tag_configure("assignment", foreground="#dcdcaa") # Amarillo (Asignaciones) - self.output_text.tag_configure("equation", foreground="#c586c0") # Púrpura (Ecuaciones) - self.output_text.tag_configure("plot", foreground="#569cd6", underline=True) # Azul con subrayado (Plots) - - # Para el nuevo indicador de tipo algebraico - self.output_text.tag_configure("type_indicator", foreground="#808080") # Gris oscuro - - # Configurar tags para tipos específicos si es necesario (ejemplo) - # self.output_text.tag_configure("IP4", foreground="#4ec9b0") - # self.output_text.tag_configure("IntBase", foreground="#4ec9b0") - - def on_key_press(self, event=None): - """Maneja eventos de presión de tecla (antes de que se inserte el carácter)""" - # Si el popup está activo, manejar navegación y selección - if (self._autocomplete_active or self._variable_popup_active) and event: - if event.keysym in ['Up', 'Down']: - return self._handle_arrow_key(event) - elif event.keysym == 'Tab': - return self._handle_tab_key(event) - elif event.keysym == 'Escape': - return self._handle_escape_key(event) - - # Detectar backspace para cerrar popup de funciones si se borra el punto - if event and event.keysym == 'BackSpace' and self._autocomplete_active: - self._check_dot_removal() - - def _check_dot_removal(self): - """Verifica si se va a borrar el punto que activó el autocompletado""" - try: - # Obtener posición del cursor - cursor_pos = self.input_text.index(tk.INSERT) - - # Obtener el carácter anterior al cursor - if cursor_pos != "1.0": # No estamos al inicio del texto - prev_char_pos = f"{cursor_pos}-1c" - prev_char = self.input_text.get(prev_char_pos, cursor_pos) - - # Si el carácter anterior es un punto, cerrar el popup - if prev_char == '.': - # Programar cierre después del backspace - self.root.after(1, self._close_autocomplete_popup) - - except tk.TclError: - # Error de posición, cerrar popup por seguridad - self._close_autocomplete_popup() - - def on_key_release(self, event=None): - """Maneja eventos de teclado después de insertar carácter""" - if self._debounce_job: - self.root.after_cancel(self._debounce_job) - - # Cancelar job de autocompletado de variables si existe - if self._variable_popup_job: - self.root.after_cancel(self._variable_popup_job) - self._variable_popup_job = None - - # Verificar si acabamos de navegar (evitar filtrado inmediato) - import time - just_navigated = (time.time() - self._last_navigation_time) < 0.1 - - # Manejar autocompletado con punto - if event and event.char == '.' and self.input_text.focus_get() == self.input_text: - # Cerrar popup de variables si está activo - if self._variable_popup_active: - self._close_autocomplete_popup() - - if not self._popup_disabled_until_next_dot: - self._handle_dot_autocomplete() - else: - # Resetear flag cuando se escribe un nuevo punto - self._popup_disabled_until_next_dot = False - - # Filtrar autocompletado si está activo (pero no si acabamos de navegar) - elif self._autocomplete_active and event and event.char.isprintable() and not just_navigated: - self._filter_autocomplete() - - # Marcar tiempo del último cambio de input - if event and event.char.isprintable(): - self._last_input_change = time.time() - - # Evaluación con debounce y auto-dimensionado - self._debounce_job = self.root.after(300, self._process_input_and_adjust_layout) - - # Programar autocompletado de variables (nuevo sistema) - self._schedule_variable_autocomplete_improved() - - def _schedule_variable_autocomplete_improved(self): - """Programa el autocompletado de variables mientras se escribe""" - # Solo si no hay popup de funciones activo - if self._autocomplete_active or self._popup_disabled_until_next_dot: - self.logger.debug("Variable autocomplete: Saltando - popup activo o deshabilitado") - return - - # Verificar que estemos escribiendo (no solo navegando) - current_line = self.input_text.get("insert linestart", "insert lineend").strip() - if not current_line or current_line.endswith('.'): - self.logger.debug(f"Variable autocomplete: Saltando - línea vacía o termina en punto: '{current_line}'") - return - - # Cancelar job anterior si existe - if self._variable_popup_job: - self.root.after_cancel(self._variable_popup_job) - - self.logger.debug(f"Variable autocomplete: Programando para línea: '{current_line}'") - # Programar para 800ms después - self._variable_popup_job = self.root.after(800, self._show_variable_autocomplete_improved) - - def _show_variable_autocomplete_improved(self): - """Muestra autocompletado de variables disponibles (simplificado)""" - self.logger.debug("Variable autocomplete: Ejecutando show_variable_autocomplete_improved") - - if self._autocomplete_active or self._variable_popup_active: - self.logger.debug("Variable autocomplete: Saltando - ya hay popup activo") - return # Ya hay un popup activo - - # Verificar que aún estemos en una línea válida - current_line = self.input_text.get("insert linestart", "insert lineend").strip() - if not current_line or current_line.endswith('.'): - self.logger.debug(f"Variable autocomplete: Saltando - línea inválida: '{current_line}'") - self._variable_popup_job = None - return - - # Obtener variables del contexto - try: - context = self.engine._get_full_context() - self.logger.debug(f"Variable autocomplete: Contexto completo tiene {len(context)} elementos") - - # Mostrar tabla de símbolos específicamente - symbol_table = getattr(self.engine, 'symbol_table', {}) - self.logger.debug(f"Variable autocomplete: Symbol table tiene {len(symbol_table)} elementos: {list(symbol_table.keys())}") - - variables = [] - - # Filtrar variables (excluir funciones built-in y módulos) - for name, value in context.items(): - # Debug detallado de cada elemento - is_underscore = name.startswith('_') - is_callable = callable(value) - has_module = hasattr(value, '__module__') - is_excluded = name in ['sympy', 'math', 'numpy', 'plt', 'builtins'] - - # Permitir variables de SymPy específicamente (ANTES del log) - is_sympy_symbol = hasattr(value, 'is_symbol') or 'sympy' in str(type(value)).lower() - - self.logger.debug(f"Variable autocomplete: Analizando '{name}': underscore={is_underscore}, callable={is_callable}, module={has_module}, excluded={is_excluded}, sympy_symbol={is_sympy_symbol}, type={type(value)}") - - if (not is_underscore and - not is_callable and - (not has_module or is_sympy_symbol) and # Permitir SymPy symbols - not is_excluded): - - self.logger.debug(f"Variable autocomplete: ✅ Aceptando variable '{name}' = {value}") - - # Crear descripción del valor (más corta) - value_str = str(value) - if len(value_str) > 20: - value_str = value_str[:17] + "..." - - variables.append((name, value_str)) - else: - self.logger.debug(f"Variable autocomplete: ❌ Rechazando '{name}' por filtros") - - self.logger.debug(f"Variable autocomplete: Encontradas {len(variables)} variables totales") - - if variables: - variables.sort(key=lambda x: x[0]) - - # Obtener texto actual para filtrado - words = current_line.split() - self.logger.debug(f"Variable autocomplete: Palabras en línea: {words}") - - if words: - last_word = words[-1] - self.logger.debug(f"Variable autocomplete: Última palabra: '{last_word}'") - - # Filtrar variables que empiecen con la palabra actual - # Y que la palabra actual no sea igual a una variable existente - filtered_vars = [ - (name, value) for name, value in variables - if name.lower().startswith(last_word.lower()) and name != last_word - ] - - self.logger.debug(f"Variable autocomplete: Variables filtradas: {len(filtered_vars)}") - - if filtered_vars: - # Posicionar en el cursor actual - self._autocomplete_trigger_pos = self.input_text.index(tk.INSERT) - self._autocomplete_filter_text = "" - - # Mostrar popup de variables menos invasivo - self._show_variable_popup(filtered_vars) - - self.logger.debug(f"Mostrando autocompletado de variables: {len(filtered_vars)} encontradas") - else: - self.logger.debug("Variable autocomplete: No hay variables filtradas que mostrar") - else: - self.logger.debug("Variable autocomplete: No hay palabras en la línea") - else: - self.logger.debug("Variable autocomplete: No hay variables en el contexto") - - except Exception as e: - self.logger.debug(f"Error obteniendo variables para autocompletado: {e}") - - # Limpiar job - self._variable_popup_job = None - - def _show_variable_popup(self, variables): - """Muestra popup de variables con estilo menos invasivo""" - cursor_bbox = self.input_text.bbox(tk.INSERT) - if not cursor_bbox: - return - - # Marcar como popup de variables activo - self._variable_popup_active = True - self._autocomplete_active = False # No es el popup principal - - x, y, _, height = cursor_bbox - popup_x = self.input_text.winfo_rootx() + x - popup_y = self.input_text.winfo_rooty() + y + height + 2 - - # Crear popup más discreto - self._autocomplete_popup = tk.Toplevel(self.root) - self._autocomplete_popup.wm_overrideredirect(True) - self._autocomplete_popup.wm_geometry(f"+{popup_x}+{popup_y}") - self._autocomplete_popup.attributes('-topmost', True) - - # Crear listbox con colores discretos pero visibles - self._autocomplete_listbox = tk.Listbox( - self._autocomplete_popup, - bg="#2d2d30", # Más oscuro - fg="#c9c9c9", # Texto más visible que antes - selectbackground="#4a4a4a", # Selección más visible - selectforeground="#ffffff", - borderwidth=1, - relief="solid", - exportselection=False, - activestyle="none", - font=("Consolas", 10) # Fuente legible - ) - - # Llenar con variables (formato más simple) - for name, value in variables: - self._autocomplete_listbox.insert(tk.END, f"{name} = {value}") - - if variables: - self._autocomplete_listbox.select_set(0) - self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH) - - # Solo doble-click para seleccionar (más discreto) - self._autocomplete_listbox.bind("", - lambda e: self._select_variable()) - - # Binding para cerrar si se hace click fuera - self.root.bind("", self._on_click_outside_variable, add=True) - - # Calcular tamaño más compacto - max_len = 15 - for name, value in variables: - item_text = f"{name} = {value}" - max_len = max(max_len, len(item_text)) - - width = min(max_len + 3, 40) - height = min(len(variables), 5) - - self._autocomplete_listbox.config(width=width, height=height) - else: - self._close_autocomplete_popup() - - def _handle_arrow_key(self, event): - """Maneja las teclas de flecha cuando el popup está activo""" - if not self._autocomplete_active and not self._variable_popup_active: - return # Permitir comportamiento normal - - direction = -1 if event.keysym == 'Up' else 1 - self._navigate_autocomplete_improved(direction) - - # Marcar tiempo de navegación para evitar filtrado inmediato - import time - self._last_navigation_time = time.time() - - return "break" # Prevenir comportamiento normal - - def _handle_tab_key(self, event): - """Maneja la tecla TAB para seleccionar del popup""" - if self._autocomplete_active or self._variable_popup_active: - self._select_autocomplete() - return "break" - return # Permitir comportamiento normal si no hay popup - - def _handle_escape_key(self, event): - """Maneja la tecla ESC para cerrar popup""" - if self._autocomplete_active or self._variable_popup_active: - self._close_autocomplete_popup() - if self._autocomplete_active: - self._popup_disabled_until_next_dot = True - return "break" - return # Permitir comportamiento normal si no hay popup - - def _on_input_click(self, event): - """Maneja clicks en el campo de entrada""" - self._close_autocomplete_popup() - - def _handle_dot_autocomplete(self): - """Maneja el autocompletado cuando se escribe un punto - VERSIÓN MEJORADA""" - self._close_autocomplete_popup() - cursor_index_str = self.input_text.index(tk.INSERT) - line_num_str, char_num_str = cursor_index_str.split('.') - current_line_num = int(line_num_str) - char_idx_after_dot = int(char_num_str) - - if char_idx_after_dot == 0: - self.logger.debug("Autocomplete: Cursor at beginning of line after dot. No action.") - return - - # Guardar posición donde se activó el autocompletado - self._autocomplete_trigger_pos = f"{current_line_num}.{char_idx_after_dot}" - self._autocomplete_filter_text = "" - - dot_char_index_in_line = char_idx_after_dot - 1 - text_on_line_up_to_dot = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{dot_char_index_in_line}") - - stripped_text_before_dot = text_on_line_up_to_dot.strip() - - # 1. Determinar si es un popup GLOBAL (usando contexto dinámico) - if not stripped_text_before_dot: - self.logger.debug("Dot on empty line or after spaces. Offering global suggestions.") - suggestions = [] - - # ========== USAR CONTEXTO DINÁMICO DEL REGISTRO ========== - try: - dynamic_context = get_registered_base_context() - - for name, class_or_func in dynamic_context.items(): - if name[0].isupper(): # Prioritizar nombres capitalizados - hint = f"Tipo o función: {name}" - if hasattr(class_or_func, '__doc__') and class_or_func.__doc__: - first_line_doc = class_or_func.__doc__.strip().split('\n')[0] - hint = f"{name} - {first_line_doc}" - elif hasattr(class_or_func, 'Helper'): - try: - helper_text = class_or_func.Helper(name) - if helper_text: - hint = helper_text.split('\n')[0] - except Exception as e_helper: - self.logger.debug(f"Error calling Helper for {name}: {e_helper}") - pass - suggestions.append((name, hint)) - - except Exception as e: - self.logger.debug(f"Error obteniendo contexto dinámico: {e}") - # Fallback básico - suggestions = [("sin", "Función seno"), ("cos", "Función coseno")] - - # Añadir funciones de SympyHelper - try: - sympy_functions = SympyHelper.PopupFunctionList() - if sympy_functions: - current_suggestion_names = {s[0] for s in suggestions} - for fname, fhint in sympy_functions: - if fname not in current_suggestion_names: - suggestions.append((fname, fhint)) - except Exception as e: - self.logger.debug(f"Error calling SympyHelper.PopupFunctionList() for global: {e}") - - if suggestions: - suggestions.sort(key=lambda x: x[0]) - self._show_autocomplete_popup(suggestions, is_global_popup=True) - return - - # 2. Es un popup de OBJETO - obj_expr_str_candidate = "" - obj_expr_regex = r"([a-zA-Z_][a-zA-Z0-9_]*(?:\[[^\]]*\])?(?:(?:\s*\.\s*[a-zA-Z_][a-zA-Z0-9_]*)(?:\[[^\]]*\])?)*)$" - match = re.search(obj_expr_regex, stripped_text_before_dot) - - if match: - obj_expr_str_candidate = match.group(1).replace(" ", "") - else: - obj_expr_str_candidate = stripped_text_before_dot - if not obj_expr_str_candidate or \ - not re.match(r"^[a-zA-Z_0-9\(\)\[\]\.\"\'\+\-\*\/ ]*$", obj_expr_str_candidate) or \ - obj_expr_str_candidate.endswith(("+", "-", "*", "/", "(", ",")): - self.logger.debug(f"Extracted expr '{obj_expr_str_candidate}' from '{stripped_text_before_dot}' not a valid object for dot autocomplete.") - return - - obj_expr_str = obj_expr_str_candidate - self.logger.debug(f"Autocomplete for object. Extracted: '{obj_expr_str}' from: '{text_on_line_up_to_dot}'") - - if not obj_expr_str: - self.logger.debug("Object expression is empty after extraction. No autocomplete.") - return - - # 3. Caso especial para el módulo sympy - if obj_expr_str == "sympy": - self.logger.debug(f"Detected 'sympy.', using SympyHelper for suggestions.") - try: - methods = SympyHelper.PopupFunctionList() - if methods: - self._show_autocomplete_popup(methods, is_global_popup=False) - else: - self.logger.debug(f"SympyHelper.PopupFunctionList returned no methods.") - except Exception as e: - self.logger.debug(f"Error calling SympyHelper.PopupFunctionList(): {e}") - return - - # 4. Preprocesar con BracketParser - if '[' in obj_expr_str: - original_for_debug = obj_expr_str - obj_expr_str = self.engine.parser._transform_brackets(obj_expr_str) - if obj_expr_str != original_for_debug and self.debug: # Solo loguear si self.debug es True - self.logger.debug(f"Preprocessed by BracketParser: '{original_for_debug}' -> '{obj_expr_str}'") - - # 5. Evaluar la expresión del objeto (usando contexto dinámico) - eval_context = self.engine._get_full_context() - obj = None - try: - if not obj_expr_str.strip(): - self.logger.debug("Object expression became empty before eval. No action.") - return - self.logger.debug(f"Attempting to eval: '{obj_expr_str}'") - obj = eval(obj_expr_str, eval_context) - self.logger.debug(f"Eval successful. Object: {type(obj)}, Value: {obj}") - except Exception as e: - self.logger.debug(f"Error evaluating object expression '{obj_expr_str}': {e}") - return - - # 6. Mostrar popup de autocompletado para el objeto - if obj is not None and hasattr(obj, 'PopupFunctionList'): - methods = obj.PopupFunctionList() - if methods: - self._show_autocomplete_popup(methods, is_global_popup=False) - - def _show_autocomplete_popup(self, suggestions, is_global_popup=False): - """Muestra popup de autocompletado modeless con filtrado""" - cursor_bbox = self.input_text.bbox(tk.INSERT) - if not cursor_bbox: - return - - # Guardar sugerencias originales y estado - self._current_suggestions = suggestions.copy() - self._is_global_popup = is_global_popup - self._autocomplete_active = True - - x, y, _, height = cursor_bbox - popup_x = self.input_text.winfo_rootx() + x - popup_y = self.input_text.winfo_rooty() + y + height + 2 - - # Crear popup modeless - self._autocomplete_popup = tk.Toplevel(self.root) - self._autocomplete_popup.wm_overrideredirect(True) - self._autocomplete_popup.wm_geometry(f"+{popup_x}+{popup_y}") - self._autocomplete_popup.attributes('-topmost', True) - - # Crear listbox - self._autocomplete_listbox = tk.Listbox( - self._autocomplete_popup, - bg="#3c3f41", - fg="#bbbbbb", - selectbackground="#007acc", - selectforeground="white", - borderwidth=1, - relief="solid", - exportselection=False, - activestyle="none" - ) - - # Llenar con sugerencias iniciales - self._populate_listbox(suggestions) - - if suggestions: - self._autocomplete_listbox.select_set(0) - self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH) - - # Bindings solo para el listbox (no roba focus del input) - self._autocomplete_listbox.bind("", - lambda e: self._select_autocomplete()) - - # Binding para cerrar si se hace click fuera - self.root.bind("", self._on_click_outside, add=True) - - # Calcular tamaño - self._resize_popup() - else: - self._close_autocomplete_popup() - - def _populate_listbox(self, suggestions): - """Llena el listbox con las sugerencias""" - self._autocomplete_listbox.delete(0, tk.END) - for name, hint in suggestions: - self._autocomplete_listbox.insert(tk.END, f"{name} — {hint}") - - def _resize_popup(self): - """Redimensiona el popup según el contenido""" - if not self._autocomplete_listbox: - return - - size = self._autocomplete_listbox.size() - if size == 0: - return - - # Calcular dimensiones - max_len = 20 - for i in range(size): - item_text = self._autocomplete_listbox.get(i) - max_len = max(max_len, len(item_text)) - - width = min(max_len + 5, 80) - height = min(size, 10) - - self._autocomplete_listbox.config(width=width, height=height) - - def _filter_autocomplete(self): - """Filtra las sugerencias basándose en el texto escrito después del punto""" - if not self._autocomplete_active or not self._autocomplete_trigger_pos: - return - - # Obtener texto escrito después del punto - current_pos = self.input_text.index(tk.INSERT) - try: - filter_text = self.input_text.get(self._autocomplete_trigger_pos, current_pos) - self._autocomplete_filter_text = filter_text.lower() - except tk.TclError: - # Posición inválida, cerrar popup - self._close_autocomplete_popup() - return - - # Filtrar sugerencias - filtered = [] - for name, hint in self._current_suggestions: - if name.lower().startswith(self._autocomplete_filter_text): - filtered.append((name, hint)) - - if filtered: - # Actualizar listbox con sugerencias filtradas - self._populate_listbox(filtered) - self._autocomplete_listbox.select_set(0) - self._resize_popup() - else: - # No hay coincidencias, cerrar popup - self._close_autocomplete_popup() - - def _navigate_autocomplete_improved(self, direction): - """Navegación mejorada en el popup de autocompletado""" - if not self._autocomplete_listbox: - return - - current_selection = self._autocomplete_listbox.curselection() - size = self._autocomplete_listbox.size() - - if size == 0: - return - - if not current_selection: - new_idx = 0 if direction == 1 else size - 1 - else: - idx = current_selection[0] - new_idx = (idx + direction) % size # Navegación circular - - # Actualizar selección - if current_selection: - self._autocomplete_listbox.select_clear(current_selection[0]) - self._autocomplete_listbox.select_set(new_idx) - self._autocomplete_listbox.activate(new_idx) - self._autocomplete_listbox.see(new_idx) - - def _select_autocomplete(self): - """Selecciona el item actual del autocompletado""" - if not self._autocomplete_listbox: - return - - selection = self._autocomplete_listbox.curselection() - if not selection: - return - - # Obtener texto seleccionado - selected_text = self._autocomplete_listbox.get(selection[0]) - - # Determinar si es popup de variables o funciones - is_variable_popup = self._variable_popup_active - - if is_variable_popup: - # Para popup de variables, usar el método específico - self._select_variable() - return - - # Para popup de funciones, extraer nombre - item_name = selected_text.split(" —")[0].strip() - is_variable = " = " in selected_text # Nuevo formato de variables - - # Insertar en la posición correcta - if hasattr(self, '_is_global_popup') and self._is_global_popup: - # Para popup global, reemplazar el punto con la función - cursor_pos_str = self.input_text.index(tk.INSERT) - line_num, char_num = map(int, cursor_pos_str.split('.')) - dot_pos_on_line = char_num - len(self._autocomplete_filter_text) - 1 - dot_index_str = f"{line_num}.{dot_pos_on_line}" - - # Eliminar punto y texto filtrado - end_pos = f"{line_num}.{char_num}" - self.input_text.delete(dot_index_str, end_pos) - - # Insertar función (no variables en popup global) - insert_text = item_name + "()" - self.input_text.insert(dot_index_str, insert_text) - self.input_text.mark_set(tk.INSERT, f"{dot_index_str}+{len(item_name)+1}c") - else: - # Para popup de objeto/variables - current_pos = self.input_text.index(tk.INSERT) - - # Eliminar texto filtrado si existe - if self._autocomplete_filter_text: - start_pos = f"{current_pos}-{len(self._autocomplete_filter_text)}c" - self.input_text.delete(start_pos, current_pos) - current_pos = start_pos - - # Insertar según el tipo - if is_variable: - # Solo insertar el nombre de la variable - insert_text = item_name - self.input_text.insert(current_pos, insert_text) - self.input_text.mark_set(tk.INSERT, f"{current_pos}+{len(item_name)}c") - else: - # Insertar método con paréntesis - insert_text = item_name + "()" - self.input_text.insert(current_pos, insert_text) - self.input_text.mark_set(tk.INSERT, f"{current_pos}+{len(item_name)+1}c") - - # Cerrar popup y enfocar input - self._close_autocomplete_popup() - self.input_text.focus_set() - self.on_key_release() - - def _select_variable(self): - """Selecciona una variable del popup de variables""" - if not self._autocomplete_listbox: - return - - selection = self._autocomplete_listbox.curselection() - if not selection: - return - - # Obtener nombre de variable - selected_text = self._autocomplete_listbox.get(selection[0]) - var_name = selected_text.split(" = ")[0].strip() - - # Obtener posición de la palabra actual - current_line = self.input_text.get("insert linestart", "insert lineend") - cursor_pos = self.input_text.index(tk.INSERT) - line_start = self.input_text.index("insert linestart") - - # Encontrar la palabra que estamos completando - words = current_line.split() - if words: - last_word = words[-1] - # Buscar posición de la última palabra - word_start_pos = current_line.rfind(last_word) - if word_start_pos >= 0: - # Calcular posición absoluta - abs_word_start = f"{line_start.split('.')[0]}.{word_start_pos}" - abs_word_end = f"{line_start.split('.')[0]}.{word_start_pos + len(last_word)}" - - # Reemplazar la palabra parcial con la variable completa - self.input_text.delete(abs_word_start, abs_word_end) - self.input_text.insert(abs_word_start, var_name) - self.input_text.mark_set(tk.INSERT, f"{abs_word_start}+{len(var_name)}c") - - # Cerrar popup - self._close_autocomplete_popup() - self.input_text.focus_set() - - def _on_click_outside_variable(self, event): - """Maneja clicks fuera del popup de variables""" - if self._autocomplete_popup and event.widget not in [ - self._autocomplete_popup, self._autocomplete_listbox - ]: - self._close_autocomplete_popup() - - def _on_click_outside(self, event): - """Maneja clicks fuera del popup""" - if self._autocomplete_popup and event.widget not in [ - self._autocomplete_popup, self._autocomplete_listbox - ]: - self._close_autocomplete_popup() - - def _navigate_autocomplete(self, event, direction): - """Navegación en autocomplete (mantenido por compatibilidad)""" - return self._navigate_autocomplete_improved(direction) - - def _close_autocomplete_popup(self): - """Cierra popup de autocomplete y resetea estado""" - if hasattr(self, '_autocomplete_popup') and self._autocomplete_popup: - try: - self._autocomplete_popup.destroy() - except tk.TclError: - pass # Ya fue destruido - self._autocomplete_popup = None - - if hasattr(self, '_autocomplete_listbox') and self._autocomplete_listbox: - self._autocomplete_listbox = None - - # Resetear estado del autocompletado - self._autocomplete_active = False - self._variable_popup_active = False - self._autocomplete_trigger_pos = None - self._autocomplete_filter_text = "" - self._current_suggestions = [] - - # Remover bindings temporales - try: - self.root.unbind("") - except tk.TclError: - pass - - def _evaluate_and_update(self): - """Evalúa todas las líneas y actualiza la salida""" - try: - input_content = self.input_text.get("1.0", tk.END) - if not input_content.strip(): - self._clear_output() - return - - # MODIFICADO: Limpiar contexto completo para evitar conflictos entre evaluaciones - # Las variables y ecuaciones se limpian para una evaluación fresca - self.engine.equations.clear() # Limpiar ecuaciones - self.engine.symbol_table.clear() # Limpiar variables asignadas - self.engine.variables.clear() # Limpiar registro de variables conocidas - self.logger.debug("Contexto del motor limpiado completamente antes de evaluación") - - # ⭐ NUEVO: Limpiar panel LaTeX antes de nueva evaluación - if hasattr(self, '_latex_equations'): - self._latex_equations.clear() - self.logger.debug("Panel LaTeX limpiado antes de nueva evaluación") - - lines = input_content.splitlines() - self._evaluate_lines(lines) - - except Exception as e: - self._show_error(f"Error durante evaluación: {e}") - - def _evaluate_lines(self, lines: List[str]): - """Evalúa múltiples líneas de código""" - output_data = [] - - for line_num, line in enumerate(lines, 1): - line = line.strip() - - # Líneas vacías o comentarios - if not line or line.startswith('#'): - if line: - output_data.append([("comment", line)]) - else: - output_data.append([("", "")]) - continue - - # Evaluar línea - result = self.engine.evaluate_line(line) - line_output = self._process_evaluation_result(result) - output_data.append(line_output) - - self._display_output(output_data) - - def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]: - """Procesa el resultado de evaluación para display, priorizando resultados interactivos.""" - output_parts = [] - indicator_text: Optional[str] = None - - # NUEVO: Agregar al panel LaTeX según el tipo de resultado - self._add_to_latex_panel_if_applicable(result) - - if result.result_type == "comment": - output_parts.append(("comment", result.output if result.output is not None else "")) - return output_parts - - if not result.success: - # Manejo de errores - error_prefix = "Error: " - main_error_message = f"{error_prefix}{result.error_message}" - - # Intentar obtener ayuda contextual para el error - ayuda_text = self.obtener_ayuda(result.input_line) # obtener_ayuda devuelve string o None - if ayuda_text: - ayuda_linea = ayuda_text.replace("\n", " ").replace("\r", " ").strip() - if len(ayuda_linea) > 120: # Acortar si es muy larga - ayuda_linea = ayuda_linea[:117] + "..." - - # Mostrar primero el error principal, luego la sugerencia - output_parts.append(("error", main_error_message)) - output_parts.append( ("\n", "\n") ) # Separador para la ayuda - output_parts.append(("helper", f"Sugerencia: {ayuda_linea}")) - else: - output_parts.append(("error", main_error_message)) - # No se añade type_indicator para errores aquí, el mensaje de error es suficiente. - - else: - # RESULTADO EXITOSO: - # 1. Intentar crear un tag interactivo - interactive_tag_info = self.interactive_manager.create_interactive_tag( - result.actual_result_object, - self.output_text - ) - - if interactive_tag_info: - tag_name, display_text = interactive_tag_info - output_parts.append((tag_name, display_text)) - - # Añadir también el indicador de tipo algebraico - if result.algebraic_type: - indicator_text = f"[{result.algebraic_type}]" - output_parts.append((" ", " ")) - output_parts.append(("type_indicator", indicator_text)) - else: - # 2. Si no es interactivo, usar la lógica de formato de texto anterior - main_output_tag = "base" - - if result.is_assignment: - main_output_tag = "assignment" - indicator_text = "[=]" - elif result.is_equation: - main_output_tag = "equation" - indicator_text = "[eq]" - elif result.result_type == "plot": - main_output_tag = "plot" - # Este caso es un fallback si create_interactive_tag no lo manejó - else: - # Lógica para determinar el tag principal y el texto del indicador - if result.algebraic_type: - type_lower = result.algebraic_type.lower() - if type_lower in self.output_text.tag_names(): - main_output_tag = type_lower - elif isinstance(result.actual_result_object, sympy.Basic): - main_output_tag = "symbolic" - elif type_lower in ["int", "float", "complex"] or isinstance(result.actual_result_object, (int, float)): - main_output_tag = "numeric" - elif type_lower == "bool" or isinstance(result.actual_result_object, bool): - main_output_tag = "boolean" - elif type_lower == "str" or isinstance(result.actual_result_object, str): - main_output_tag = "string" - elif result.actual_result_object is not None and \ - not isinstance(result.actual_result_object, (sympy.Basic, int, float, bool, str, list, dict, tuple, type(None))): - main_output_tag = "custom_type" - else: - main_output_tag = "symbolic" - else: - main_output_tag = "symbolic" - - if result.algebraic_type: - is_collection = any(kw in result.algebraic_type.lower() for kw in ["matrix", "list", "dict", "tuple", "vector", "array"]) - is_custom_obj_tag = (main_output_tag == "custom_type") - is_non_trivial_sympy = isinstance(result.actual_result_object, sympy.Basic) and \ - result.algebraic_type not in ["Symbol", "Expr", "Integer", "Float", "Rational", "BooleanTrue", "BooleanFalse"] - if is_collection or is_custom_obj_tag or is_non_trivial_sympy: - indicator_text = f"[{result.algebraic_type}]" - - output_parts.append((main_output_tag, result.output if result.output is not None else "")) - - if indicator_text: - output_parts.append((" ", " ")) - output_parts.append(("type_indicator", indicator_text)) - - return output_parts - - def _add_to_latex_panel_if_applicable(self, result: EvaluationResult): - """Agrega resultado al panel LaTeX si es aplicable (ecuación, asignación o comentario)""" - try: - # DEBUG: Log información del resultado - self.logger.debug(f"Procesando para LaTeX - Tipo: {result.result_type}, Éxito: {result.success}") - self.logger.debug(f" - is_assignment: {result.is_assignment}") - self.logger.debug(f" - is_equation: {result.is_equation}") - self.logger.debug(f" - output: {result.output[:100]}...") - - # Determinar si debe ir al panel LaTeX - REGLAS SIMPLIFICADAS - should_add_to_latex = False - equation_type = "comment" # Tipo por defecto - - # 1. SIEMPRE agregar comentarios - if result.result_type == "comment": - should_add_to_latex = True - equation_type = "comment" - self.logger.debug(" -> Detectado como COMMENT") - - # 2. SIEMPRE agregar asignaciones - elif result.is_assignment: - should_add_to_latex = True - equation_type = "assignment" - self.logger.debug(" -> Detectado como ASSIGNMENT") - - # 3. SIEMPRE agregar ecuaciones - elif result.is_equation: - should_add_to_latex = True - equation_type = "equation" - self.logger.debug(" -> Detectado como EQUATION") - - # 4. Agregar CUALQUIER resultado exitoso que tenga contenido interesante - elif result.success and result.output: - # Si tiene símbolos matemáticos o contenido algebraico, agregarlo - math_indicators = ['=', '+', '-', '*', '/', '^', 'sqrt', 'sin', 'cos', 'tan', 'log', 'exp'] - if any(indicator in result.output for indicator in math_indicators): - should_add_to_latex = True - equation_type = "symbolic" - self.logger.debug(" -> Detectado como SYMBOLIC (contiene matemáticas)") - - # También agregar si es claramente una expresión matemática - elif (result.actual_result_object is not None and - hasattr(result.actual_result_object, '__class__')): - try: - import sympy - if isinstance(result.actual_result_object, sympy.Basic): - should_add_to_latex = True - equation_type = "symbolic" - self.logger.debug(" -> Detectado como SYMBOLIC (objeto SymPy)") - except ImportError: - pass - - if should_add_to_latex: - # Preparar contenido LaTeX - latex_content = "" - - if result.actual_result_object is not None: - try: - # Intentar convertir a LaTeX usando SymPy - import sympy - latex_content = sympy.latex(result.actual_result_object) - self.logger.debug(f" -> LaTeX de SymPy: {latex_content[:100]}...") - except Exception as e: - # Fallback al output de texto - latex_content = result.output if result.output else str(result.actual_result_object) - self.logger.debug(f" -> Fallback a texto: {latex_content[:100]}...") - else: - latex_content = result.output if result.output else "" - self.logger.debug(f" -> Usando output directo: {latex_content[:100]}...") - - # ANTES de añadir al panel, verificar si está inicializado - if not hasattr(self, '_latex_equations'): - self._latex_equations = [] - self.logger.debug(" -> ⚠️ Lista de ecuaciones no existía, creándola") - - total_antes = len(self._latex_equations) - - # Usar la función _add_to_latex_panel - self._add_to_latex_panel(equation_type, latex_content) - - total_despues = len(self._latex_equations) - self.logger.debug(f"✅ AÑADIDO al panel LaTeX: {equation_type} -> {latex_content[:50]}... (Total: {total_antes} -> {total_despues})") - - else: - self.logger.debug(" -> NO añadido al panel LaTeX") - - except Exception as e: - self.logger.error(f"Error procesando resultado para panel LaTeX: {e}") - import traceback - self.logger.error(traceback.format_exc()) - - def _get_result_tag_dynamic(self, result: Any) -> str: - """Determina el tag de color para un resultado - SIMPLIFICADO""" - # Determinar tag basado en tipo - if hasattr(result, '__class__'): - class_name = result.__class__.__name__.lower() - if 'hex' in class_name: - return "hex" - elif 'bin' in class_name: - return "bin" - elif 'ip' in class_name: - return "ip" - elif 'chr' in class_name: - return "chr_type" - elif 'date' in class_name: - return "date" - - # Fallback a tags existentes - try: - import sympy - if isinstance(result, sympy.Basic): - return "symbolic" - except: - pass - - return "result" - - def _get_class_display_name_dynamic(self, obj: Any) -> str: - """Obtiene nombre de clase para display - SIMPLIFICADO""" - try: - import sympy - if isinstance(obj, sympy.Basic): - return "Sympy" - except: - pass - - if isinstance(obj, (int, float, str, list, dict, tuple, bool, type(None))): - class_display_name = type(obj).__name__.capitalize() - if class_display_name == "Nonetype": - class_display_name = "None" - return class_display_name - - return type(obj).__name__ - - def _display_output(self, output_data: List[List[tuple]]): - """Muestra los datos de salida en el widget (sin cambios)""" - self.output_text.config(state="normal") - self.output_text.delete("1.0", tk.END) - - for line_idx, line_parts in enumerate(output_data): - if not line_parts or (len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == ""): - pass - else: - for part_idx, (tag, content) in enumerate(line_parts): - if not content: - continue - - if part_idx > 0: - prev_tag, prev_content = line_parts[part_idx-1] if part_idx > 0 else (None, None) - - if tag not in ["class_hint", "numeric", "info"] and prev_content: - self.output_text.insert(tk.END, " ; ") - elif tag in ["numeric", "info"] and prev_content: - self.output_text.insert(tk.END, " ") - - if content: - self.output_text.insert(tk.END, str(content), tag) - - if line_idx < len(output_data) - 1: - self.output_text.insert(tk.END, "\n") - - self.output_text.config(state="disabled") - - def _clear_output(self): - """Limpia el panel de salida""" - self.output_text.config(state="normal") - self.output_text.delete("1.0", tk.END) - self.output_text.config(state="disabled") - - def _show_error(self, error_msg: str): - """Muestra un error en el panel de salida""" - self.output_text.config(state="normal") - self.output_text.delete("1.0", tk.END) - self.output_text.insert("1.0", error_msg, "error") - self.output_text.config(state="disabled") - - def _show_context_menu(self, event, panel_type: str): - """Muestra menú contextual""" - context_menu = Menu( - self.root, tearoff=0, bg="#3c3c3c", fg="white", - activebackground="#007acc", activeforeground="white", - relief=tk.FLAT, bd=1, - ) - - if panel_type == "input": - context_menu.add_command(label="Cortar", command=lambda: self.input_text.event_generate("<>")) - context_menu.add_command(label="Copiar", command=lambda: self.input_text.event_generate("<>")) - context_menu.add_command(label="Pegar", command=lambda: self.input_text.event_generate("<>")) - context_menu.add_separator() - context_menu.add_command(label="Limpiar entrada", command=self.clear_input) - elif panel_type == "output": - context_menu.add_command(label="Copiar todo", command=self.copy_output) - context_menu.add_command(label="Limpiar salida", command=self.clear_output) - - context_menu.add_separator() - context_menu.add_command(label="Ayuda", command=self.show_help_window) - - try: - context_menu.tk_popup(event.x_root, event.y_root) - finally: - context_menu.grab_release() - - # ========== MÉTODOS DE MENÚ Y COMANDOS (la mayoría sin cambios) ========== - - def new_session(self): - """Inicia una nueva sesión limpiando todo""" - self.clear_input() - self.clear_output() - self._clear_latex_panel() - if hasattr(self.engine, 'clear_context'): - self.engine.clear_context() - - def load_file(self): - """Carga archivo en el editor""" - filepath = filedialog.askopenfilename( - title="Cargar archivo", - filetypes=[ - ("Archivos de texto", "*.txt"), - ("Archivos Python", "*.py"), - ("Todos los archivos", "*.*") - ] - ) - - if filepath: - try: - with open(filepath, "r", encoding="utf-8") as f: - content = f.read() - - self.input_text.delete("1.0", tk.END) - self.input_text.insert("1.0", content) - self._process_input_and_adjust_layout() - - except Exception as e: - messagebox.showerror("Error", f"No se pudo cargar el archivo:\n{e}") - - def save_file(self): - """Guarda contenido del editor""" - filepath = filedialog.asksaveasfilename( - title="Guardar archivo", - defaultextension=".txt", - filetypes=[ - ("Archivos de texto", "*.txt"), - ("Archivos Python", "*.py"), - ("Todos los archivos", "*.*") - ] - ) - - if filepath: - try: - content = self.input_text.get("1.0", tk.END) - with open(filepath, "w", encoding="utf-8") as f: - f.write(content) - - messagebox.showinfo("Éxito", "Archivo guardado correctamente.") - - except Exception as e: - messagebox.showerror("Error", f"No se pudo guardar el archivo:\n{e}") - - def clear_input(self): - """Limpia panel de entrada""" - self.input_text.delete("1.0", tk.END) - self._clear_output() - - def clear_output(self): - """Limpia el panel de salida y el panel LaTeX""" - self._clear_output() - self._clear_latex_panel() - - def clear_history(self): - """Limpia el archivo de historial""" - try: - if os.path.exists(self.HISTORY_FILE): - os.remove(self.HISTORY_FILE) - messagebox.showinfo("Éxito", "Historial limpiado correctamente.") - except Exception as e: - messagebox.showerror("Error", f"No se pudo limpiar el historial:\n{e}") - - def copy_output(self): - """Copia el contenido de la salida al portapapeles""" - content = self.output_text.get("1.0", tk.END).strip() - if content: - self.root.clipboard_clear() - self.root.clipboard_append(content) - - def show_types_syntax(self): - """Muestra sintaxis de tipos disponibles - NUEVA FUNCIÓN""" - try: - types_info = self.engine.get_available_types() - registered_classes = types_info.get('registered_classes', {}) - - syntax_text = "SINTAXIS DE TIPOS DISPONIBLES\n\n" - - if not registered_classes: - syntax_text += "No hay tipos personalizados disponibles.\n" - else: - syntax_text += "Tipos personalizados detectados:\n\n" - - for name, cls in sorted(registered_classes.items()): - syntax_text += f"=== {name} ===\n" - - # Sintaxis básica - syntax_text += f"Sintaxis: {name}[valor]\n" - syntax_text += f"Alias: {name.lower()}[valor]\n" - - # Obtener ayuda si está disponible - if hasattr(cls, 'Helper'): - try: - help_text = cls.Helper(name) - if help_text: - syntax_text += f"Ayuda: {help_text}\n" - except: - pass - - # Obtener métodos si está disponible - if hasattr(cls, 'PopupFunctionList'): - try: - methods = cls.PopupFunctionList() - if methods: - syntax_text += "Métodos disponibles:\n" - for method_name, method_desc in methods: - syntax_text += f" • {method_name}() - {method_desc}\n" - except: - pass - - syntax_text += "\n" - - self._show_help_window("Sintaxis de Tipos", syntax_text) - - except Exception as e: - messagebox.showerror("Error", f"Error obteniendo sintaxis de tipos:\n{e}") - - def show_quick_guide(self): - """Muestra guía rápida - ACTUALIZADA""" - guide = """# Calculadora MAV - CAS Híbrido - -## Sistema de Tipos Dinámico -El sistema detecta automáticamente tipos disponibles en custom_types/ - -## Sintaxis Nueva con Corchetes -- Sintaxis: Tipo[valor] en lugar de Tipo("valor") -- Ejemplos: Hex[FF], Bin[1010], Dec[10.5], Chr[A] -- Use menú Tipos → Información de tipos para ver tipos disponibles - -## Ecuaciones Automáticas -- x**2 + 2*x = 8 (detectado automáticamente) -- a + b = 10 (agregado al sistema) -- variable=? (atajo para solve(variable)) - -## Funciones SymPy Disponibles -- solve(), diff(), integrate(), limit(), series() -- sin(), cos(), tan(), exp(), log(), sqrt() -- Matrix(), plot(), plot3d() - -## Resultados Interactivos -- 📊 Ver Plot (click para ventana matplotlib) -- 📋 Ver Matriz (click para vista expandida) -- 📋 Ver Lista (click para contenido completo) - -## Variables Automáticas -- Todas las variables son símbolos SymPy -- x = 5 crea Symbol('x') con valor 5 -- Evaluación simbólica + numérica automática - -## Autocompletado Dinámico -- Escriba "." después de cualquier objeto para ver métodos -- El sistema usa los tipos registrados automáticamente -""" - - self._show_help_window("Guía Rápida", guide) - - def show_syntax_help(self): - """Muestra ayuda de sintaxis - ACTUALIZADA""" - syntax = """# Sintaxis del CAS Híbrido - -## Sistema de Tipos Dinámico -Los tipos se detectan automáticamente desde custom_types/ -Use menú Tipos → Información de tipos para ver tipos disponibles - -## Sintaxis con Corchetes (Dinámica) -Tipo[valor] # Sintaxis general -Tipo[arg1; arg2] # Múltiples argumentos - -## Métodos Disponibles (Dinámicos) -Tipo[...].método() # Métodos específicos del tipo -objeto.método[] # Método sin argumentos - -## Ecuaciones (detección automática) -expresión = expresión # Ecuación simple -expresión == expresión # Igualdad SymPy -expresión > expresión # Desigualdad SymPy - -## Resolver -solve(ecuación, variable) -variable=? # Atajo para solve(variable) - -## Variables SymPy Puras -x = valor # Crea Symbol('x') -expresión # Evaluación simbólica automática -""" - - self._show_help_window("Sintaxis", syntax) - - def show_sympy_functions(self): - """Muestra funciones SymPy disponibles (sin cambios)""" - functions = """# Funciones SymPy Disponibles - -## Matemáticas Básicas -sin(x), cos(x), tan(x) -asin(x), acos(x), atan(x) -sinh(x), cosh(x), tanh(x) -exp(x), log(x), sqrt(x) -abs(x), sign(x), factorial(x) - -## Cálculo -diff(expr, var) # Derivada -integrate(expr, var) # Integral indefinida -integrate(expr, (var, a, b)) # Integral definida -limit(expr, var, punto) # Límite -series(expr, var, punto, n) # Serie de Taylor - -## Álgebra -solve(ecuación, variable) -simplify(expr), expand(expr) -factor(expr), collect(expr, var) -cancel(expr), apart(expr, var) - -## Álgebra Lineal -Matrix([[a, b], [c, d]]) -det(matrix), inv(matrix) - -## Plotting -plot(expr, (var, inicio, fin)) -plot3d(expr, (x, x1, x2), (y, y1, y2)) - -## Constantes -pi, E, I (imaginario), oo (infinito) -""" - - self._show_help_window("Funciones SymPy", functions) - - def show_about(self): - """Muestra información sobre la aplicación - ACTUALIZADA""" - about = """Calculadora MAV - CAS Híbrido - -Versión: 2.1 (Sistema de Tipos Dinámico) -Motor: SymPy + Auto-descubrimiento de Tipos - -Características: -• Motor algebraico completo (SymPy) -• Sistema de tipos dinámico y extensible -• Sintaxis simplificada con corchetes -• Detección automática de ecuaciones -• Resultados interactivos clickeables -• Auto-descubrimiento de tipos en custom_types/ -• Variables SymPy puras -• Plotting integrado -• Autocompletado dinámico - -NUEVO: Sistema de Tipos Dinámico -• Detección automática de nuevos tipos -• Organización modular en custom_types/ -• Registro automático sin modificar código -• Escalabilidad mejorada - -Desarrollado para cálculo matemático avanzado -con soporte especializado para redes, -programación y análisis numérico. -""" - - messagebox.showinfo("Acerca de", about) - - def _show_help_window(self, title: str, content: str): - """Muestra ventana de ayuda""" - window = tk.Toplevel(self.root) - window.title(title) - window.geometry("700x500") - window.configure(bg="#2b2b2b") - - text_widget = scrolledtext.ScrolledText( - window, - font=("Consolas", 10), - bg="#1e1e1e", - fg="#d4d4d4", - wrap=tk.WORD - ) - text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - text_widget.insert("1.0", content) - text_widget.config(state="disabled") - - def load_history(self): - """Carga historial de entrada y realiza evaluación inicial""" - try: - if os.path.exists(self.HISTORY_FILE): - with open(self.HISTORY_FILE, "r", encoding="utf-8") as f: - content = f.read() - - if content.strip(): - self.input_text.insert("1.0", content) - # Hacer evaluación inicial para mostrar resultados del historial - # Esto mantiene el comportamiento de contexto limpio pero muestra resultados - self.root.after_idle(self._process_input_and_adjust_layout) - except Exception as e: - self.logger.error(f"Error cargando historial: {e}", exc_info=True) - - def save_history(self): - """Guarda historial de entrada""" - try: - content = self.input_text.get("1.0", tk.END).rstrip("\n") - if content: - with open(self.HISTORY_FILE, "w", encoding="utf-8") as f: - f.write(content) - elif os.path.exists(self.HISTORY_FILE): - os.remove(self.HISTORY_FILE) - except Exception as e: - self.logger.error(f"Error guardando historial: {e}", exc_info=True) - - def on_close(self): - """Maneja cierre de la aplicación de forma completa""" - try: - # Guardar historial y configuraciones - self.save_history() - self._save_settings() - - # Cerrar todas las ventanas interactivas - if self.interactive_manager: - self.interactive_manager.close_all_windows() - - # Detener cualquier job pendiente - if self._debounce_job: - self.root.after_cancel(self._debounce_job) - - # Cerrar autocompletado si está abierto - self._close_autocomplete_popup() - - # Asegurar que matplotlib libere recursos - try: - import matplotlib.pyplot as plt - plt.close('all') - except: - pass - - # Forzar la salida del mainloop - self.root.quit() - - except Exception as e: - self.logger.error(f"Error durante el cierre: {e}") - finally: - # Destruir la ventana principal como último recurso - try: - self.root.destroy() - except: - pass - - def show_help_window(self): - """Muestra ventana de ayuda con archivo externo - NUEVO SISTEMA""" - help_win = tk.Toplevel(self.root) - help_win.title("Ayuda - Calculadora MAV CAS Híbrido") - help_win.geometry("750x600") - help_win.configure(bg="#1e1e1e") - help_win.transient(self.root) - - readme_content = self._get_help_content() - - if MARKDOWN_AVAILABLE and HTML_VIEWER_TYPE: - try: - # CSS para un tema oscuro, consistente con la UI de la calculadora - dark_theme_css = """ - -""" - html_fragment = markdown.markdown( - readme_content, extensions=["fenced_code", "codehilite", "tables", "nl2br", "admonition"], - extension_configs={"codehilite": {"noclasses": True, "pygments_style": "monokai"}} - ) - - # Construir un documento HTML completo - content_for_viewer = f""" - - - - - Ayuda de Calculadora - {dark_theme_css} - - - {html_fragment} - - -""" - if HTML_VIEWER_TYPE == "tkinterweb": - html_viewer = tkinterweb.HtmlFrame(help_win, messages_enabled=False) - html_viewer.load_html(content_for_viewer) - elif HTML_VIEWER_TYPE == "tkhtmlview": - html_viewer = HTMLScrolledText(help_win) - html_viewer.configure(bg="#1e1e1e") - html_viewer.set_html(content_for_viewer) - - html_viewer.pack(padx=0, pady=0, fill=tk.BOTH, expand=True) - except Exception as e: - self.logger.error(f"Error al renderizar Markdown a HTML: {e}", exc_info=True) - # Fallback to text if HTML fails - self._show_text_help(help_win, readme_content) - else: - self._show_text_help(help_win, readme_content) - - # Botón de cerrar - close_button = tk.Button( - help_win, text="Cerrar", command=help_win.destroy, - bg="#3c3c3c", fg="white", relief=tk.FLAT, padx=10, - ) - close_button.pack(pady=(5, 10)) - - def _get_help_content(self): - """Obtiene el contenido de ayuda desde archivo externo o genera uno por defecto""" - try: - if os.path.exists(self.HELP_FILE): - with open(self.HELP_FILE, "r", encoding="utf-8") as f: - return f.read() - except IOError: - pass - - # Contenido por defecto si no se encuentra el archivo - return """# Calculadora MAV - CAS Híbrido - -## Sistema de Tipos Dinámico -El sistema detecta automáticamente tipos disponibles en `custom_types/` - -## Sintaxis Nueva con Corchetes -- **Sintaxis**: `Tipo[valor]` en lugar de `Tipo("valor")` -- **Ejemplos**: `Hex[FF]`, `Bin[1010]`, `Dec[10.5]`, `Chr[A]` -- Use menú **Tipos → Información de tipos** para ver tipos disponibles - -## Ecuaciones Automáticas -- `x**2 + 2*x = 8` (detectado automáticamente) -- `a + b = 10` (agregado al sistema) -- `variable=?` (atajo para solve(variable)) - -## Funciones SymPy Disponibles -- `solve()`, `diff()`, `integrate()`, `limit()`, `series()` -- `sin()`, `cos()`, `tan()`, `exp()`, `log()`, `sqrt()` -- `Matrix()`, `plot()`, `plot3d()` - -## Resultados Interactivos -- 📊 **Ver Plot** (click para ventana matplotlib) -- 📋 **Ver Matriz** (click para vista expandida) -- 📋 **Ver Lista** (click para contenido completo) - -## Variables Automáticas -- Todas las variables son símbolos SymPy -- `x = 5` crea Symbol('x') con valor 5 -- Evaluación simbólica + numérica automática - -## Autocompletado Dinámico -- Escriba "." después de cualquier objeto para ver métodos -- El sistema usa los tipos registrados automáticamente - -## Menú Contextual (clic derecho) -- **En entrada**: Cortar, Copiar, Pegar, Limpiar entrada, Ayuda -- **En salida**: Copiar todo, Limpiar salida, Ayuda - -## Desarrollo -Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el directorio raíz de la aplicación. -""" - - def _show_text_help(self, help_win, content): - """Muestra la ayuda en texto plano cuando markdown no está disponible""" - text_widget = scrolledtext.ScrolledText( - help_win, font=("Consolas", 10), bg="#1e1e1e", fg="#d4d4d4", - wrap=tk.WORD, borderwidth=0, highlightthickness=0 - ) - text_widget.insert("1.0", content) - text_widget.config(state="disabled") - text_widget.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) - - def obtener_ayuda(self, input_str): - """Obtiene ayuda usando helpers dinámicos""" - for helper in self.HELPERS: - try: - ayuda = helper(input_str) - if ayuda: - return ayuda - except Exception as e: - self.logger.debug(f"Error en helper: {e}") - continue - return None - - - - def _get_input_font(self): - """Obtiene o crea y cachea el objeto tk.Font para el panel de entrada.""" - if not self._cached_input_font: - # Asume la fuente configurada en create_widgets: ("Consolas", 11) - self._cached_input_font = tkFont.Font(family="Consolas", size=11) - return self._cached_input_font - - def _adjust_input_pane_width(self): - """Ajusta el ancho del panel de entrada según su contenido, limitado a ~50 caracteres.""" - if not hasattr(self, 'paned_window') or not self.paned_window.winfo_exists(): - return - - # Esperar a que la ventana tenga un tamaño válido - if self.paned_window.winfo_width() <= 1: - return # Se reintentará en la siguiente llamada (ej. por KeyRelease) - - # Obtener contenido excluyendo el último newline automático del widget Text - input_content = self.input_text.get("1.0", f"{tk.END}-1c") - lines = input_content.splitlines() - input_font = self._get_input_font() - - # NUEVO: Límite máximo de ~50 caracteres - max_chars_limit = 50 - max_char_width = input_font.measure("M") # Usar 'M' como referencia (carácter más ancho) - max_allowed_width = max_char_width * max_chars_limit - - max_pixel_width = 0 - if not input_content.strip(): # Si está vacío o solo espacios en blanco - max_pixel_width = 5 # Ancho mínimo para el cursor o como placeholder - else: - for line in lines: - measured_width = input_font.measure(line) if line.strip() else input_font.measure(" ") - # NUEVO: Aplicar límite de caracteres - measured_width = min(measured_width, max_allowed_width) - if measured_width > max_pixel_width: - max_pixel_width = measured_width - - padding = 40 # Relleno para barra de desplazamiento, márgenes, etc. - width_needed_by_text = max_pixel_width + padding - - # NUEVO: Aplicar límite absoluto - max_absolute_width = max_allowed_width + padding - width_needed_by_text = min(width_needed_by_text, max_absolute_width) - - # Debugging opcional (descomenta si necesitas depurar) - if self.debug: - self.logger.debug(f"--- Adjusting Input Pane (Limited to ~{max_chars_limit} chars) ---") - self.logger.debug(f"Input content: '{input_content[:50]}...'") - self.logger.debug(f"Max pixel width of text: {max_pixel_width}") - self.logger.debug(f"Width needed by text (limited): {width_needed_by_text}") - - min_input_pane_width = 200 # Definido en create_widgets - min_output_pane_width = 200 # Definido en create_widgets - total_width = self.paned_window.winfo_width() - - # NUEVO: Consideración del panel LaTeX expandible - # Solo reservar espacio si está visible - total_available_width = total_width - if hasattr(self, 'latex_panel_visible') and self.latex_panel_visible: - # Si el panel LaTeX está visible, reservar espacio para él - latex_width = getattr(self, '_min_latex_pane_width', 300) - total_available_width = total_width # El PanedWindow se ajusta automáticamente - - current_sash_pos = 0 - try: - sash_coords = self.paned_window.sash_coord(0) - if sash_coords: - current_sash_pos = sash_coords[0] - else: - if self.debug: - self.logger.debug("Could not get sash_coord.") - return - except tk.TclError: - if self.debug: - self.logger.debug("TclError getting sash_coord.") - return - - if self.debug: - self.logger.debug(f"Current sash position (input pane width): {current_sash_pos}") - - if width_needed_by_text > current_sash_pos: - if self.debug: - self.logger.debug(f"Condition MET: Text needs more space ({width_needed_by_text} > {current_sash_pos})") - new_input_width = width_needed_by_text # Punto de partida - - # Asegurar que el nuevo ancho no sea menor que el mínimo del panel de entrada - new_input_width = max(new_input_width, min_input_pane_width) - - # Asegurar que el panel de salida conserve su espacio mínimo - if total_available_width - new_input_width < min_output_pane_width: - new_input_width = total_available_width - min_output_pane_width - new_input_width = max(new_input_width, min_input_pane_width) # Re-verificar mínimo del input - - # MODIFICADO: Ratio máximo conservador para el input - max_input_ratio = 0.5 # 50% máximo para el input, dejando espacio para output y LaTeX - max_width_by_ratio = int(total_available_width * max_input_ratio) - - if new_input_width > max_width_by_ratio: - if max_width_by_ratio >= min_input_pane_width and \ - (total_available_width - max_width_by_ratio) >= min_output_pane_width: - new_input_width = max_width_by_ratio - - final_new_input_width = max(0, int(new_input_width)) # No debe ser negativo - if self.debug: - self.logger.debug(f"Calculated final new input width: {final_new_input_width}") - - # Mover el sash solo si el nuevo ancho es significativamente mayor que el actual (umbral ajustado) - sash_adjustment_threshold = 3 # Píxeles - if final_new_input_width > current_sash_pos and \ - (final_new_input_width - current_sash_pos) >= sash_adjustment_threshold: - if self.debug: - self.logger.debug(f"Condition MET for sash_place: New width {final_new_input_width} is significantly larger (diff >= {sash_adjustment_threshold}).") - try: - if self.paned_window.winfo_exists() and total_available_width >= (min_input_pane_width + min_output_pane_width): - self.paned_window.sash_place(0, final_new_input_width, 0) # Añadido el argumento y=0 - if self.debug: - self.logger.debug(f"Sash placed at: {final_new_input_width}") - elif self.debug: - self.logger.debug("Paned window not ready or total width too small for sash_place.") - except tk.TclError as e_sash: - if self.debug: - self.logger.debug(f"TclError during sash_place: {e_sash}") - pass - elif self.debug: - self.logger.debug(f"Condition NOT MET for sash_place: final_new_input_width ({final_new_input_width}) vs current_sash_pos ({current_sash_pos}) or threshold ({sash_adjustment_threshold}).") - elif self.debug: - self.logger.debug(f"Condition NOT MET: Text does not need more space ({width_needed_by_text} <= {current_sash_pos})") - - if self.debug: - self.logger.debug(f"--- End Adjusting Input Pane ---") - - def _process_input_and_adjust_layout(self): - """Evalúa todas las líneas y luego ajusta el ancho del panel de entrada.""" - self._evaluate_and_update() - self._adjust_input_pane_width() - - def _update_input_expression(self, original_expression, new_expression): - """Actualiza el panel de entrada reemplazando la expresión original con la nueva""" - try: - # Obtener todo el contenido actual - current_content = self.input_text.get("1.0", tk.END).rstrip('\n') - - # Buscar y reemplazar la expresión original - if original_expression in current_content: - updated_content = current_content.replace(original_expression, new_expression, 1) - - # Actualizar el panel de entrada - self.input_text.delete("1.0", tk.END) - self.input_text.insert("1.0", updated_content) - - # Evaluar automáticamente la nueva expresión - self._evaluate_and_update() - - self.logger.info(f"Expresión actualizada: '{original_expression}' -> '{new_expression}'") - else: - # Si no se encuentra la expresión original, agregar la nueva al final - if current_content and not current_content.endswith('\n'): - current_content += '\n' - updated_content = current_content + new_expression - - self.input_text.delete("1.0", tk.END) - self.input_text.insert("1.0", updated_content) - - self.logger.info(f"Expresión agregada: '{new_expression}'") - - except Exception as e: - self.logger.error(f"Error actualizando expresión: {e}") - # Fallback: simplemente insertar la nueva expresión - self.input_text.insert(tk.END, f"\n{new_expression}") - - def _add_equation_to_latex_panel(self, equation_type: str, content: str, sympy_obj=None): - """Agrega una ecuación al panel LaTeX expandible""" - if not hasattr(self, 'latex_panel'): - return - - try: - # Inicializar lista si no existe - if not hasattr(self, '_latex_equations'): - self._latex_equations = [] - - # Convertir objeto SymPy a LaTeX si es posible - latex_content = self._sympy_to_latex(sympy_obj) if sympy_obj else content - - # Agregar a la lista de ecuaciones - equation_data = { - 'type': equation_type, - 'content': latex_content, - 'original': content - } - self._latex_equations.append(equation_data) - - # Actualizar indicador visual - self._update_latex_indicator() - - # Si el panel está visible, actualizar contenido inmediatamente - if hasattr(self, 'latex_panel_visible') and self.latex_panel_visible: - if self._webview_available: - # Actualizar según el tipo de webview - if self._webview_type == "tkinterweb": - self._update_tkinterweb() - elif self._webview_type == "pywebview": - self._update_pywebview() - else: - # Fallback: usar el widget de texto - if hasattr(self, 'latex_fallback_text'): - self.latex_fallback_text.config(state="normal") - - # Agregar tipo y contenido - type_text = f"\n[{equation_type.upper()}] " - self.latex_fallback_text.insert(tk.END, type_text) - self.latex_fallback_text.insert(tk.END, f"{content}\n") - - self.latex_fallback_text.config(state="disabled") - - # Auto-scroll al final - self.latex_fallback_text.see(tk.END) - - except Exception as e: - self.logger.debug(f"Error agregando ecuación al panel LaTeX: {e}") - - def _add_spacer_to_latex_panel(self): - """Agrega un espaciador al panel LaTeX para correlación vertical""" - if not hasattr(self, 'latex_panel'): - return - - try: - # Inicializar lista si no existe - if not hasattr(self, '_latex_equations'): - self._latex_equations = [] - - # Agregar espaciador a la lista - spacer_data = { - 'type': 'spacer', - 'content': '', - 'original': '' - } - self._latex_equations.append(spacer_data) - - # Si el panel está visible, actualizar contenido - if hasattr(self, 'latex_panel_visible') and self.latex_panel_visible: - if self._webview_available: - # Actualizar según el tipo - if self._webview_type == "tkinterweb": - self._update_tkinterweb() - elif self._webview_type == "pywebview": - self._update_pywebview() - - elif hasattr(self, 'latex_fallback_text'): - # Agregar línea en blanco en el fallback - self.latex_fallback_text.config(state="normal") - self.latex_fallback_text.insert(tk.END, "\n") - self.latex_fallback_text.config(state="disabled") - - except Exception as e: - self.logger.debug(f"Error agregando espaciador: {e}") - - def _clear_latex_panel(self): - """Limpia el panel LaTeX""" - if not hasattr(self, 'latex_panel'): - return - - try: - # Limpiar lista de ecuaciones - self._latex_equations = [] - - # Actualizar indicador - self._update_latex_indicator() - - # Si el panel está visible, limpiar contenido - if hasattr(self, 'latex_panel_visible') and self.latex_panel_visible: - if self._webview_available: - if self._webview_type == "tkinterweb" and hasattr(self, 'latex_webview'): - # Recargar HTML base - base_html = self._generate_base_html() - self.latex_webview.load_html(base_html) - - elif self._webview_type == "pywebview": - # Para pywebview, reinicializar si es necesario - pass - - elif hasattr(self, 'latex_fallback_text'): - # Limpiar el widget de texto - self.latex_fallback_text.config(state="normal") - self.latex_fallback_text.delete("1.0", tk.END) - self.latex_fallback_text.insert("1.0", "Panel de Ecuaciones LaTeX\n\n" - "Las ecuaciones aparecerán aquí...\n") - self.latex_fallback_text.config(state="disabled") - - except Exception as e: - self.logger.debug(f"Error limpiando panel LaTeX: {e}") - - def _sympy_to_latex(self, sympy_obj) -> str: - """Convierte un objeto SymPy a LaTeX""" - try: - if sympy_obj is None: - return "" - - # Usar la función latex de SymPy - from sympy import latex - latex_str = latex(sympy_obj) - - # Envolver en delimitadores para display math - return f"$${latex_str}$$" - - except Exception as e: - self.logger.debug(f"Error convirtiendo a LaTeX: {e}") - return str(sympy_obj) - - - - def _update_latex_panel(self): - """Actualiza el panel LaTeX con soporte para pywebview y tkinterweb""" - if not self.latex_panel_visible or not hasattr(self, '_latex_equations'): - return - - try: - self.logger.debug(f"🔄 Actualizando panel LaTeX con {len(self._latex_equations)} ecuaciones (Tipo: {self._webview_type})") - - # Generar HTML con ecuaciones según el tipo de webview - if self._latex_equations: - if self._webview_type == "pywebview": - html_content = self._generate_html_pywebview_with_equations() - elif self._js_available: - html_content = self._generate_html_with_mathjax() - else: - html_content = self._generate_html_fallback_with_equations() - else: - # Sin ecuaciones, usar HTML base - html_content = self._generate_base_html() - - # Manejar según el tipo de webview - if self._webview_type == "pywebview": - self._update_pywebview_panel(html_content) - elif self._webview_type == "tkinterweb": - self._update_tkinterweb_panel(html_content) - else: - self.logger.warning("⚠️ Tipo de webview no reconocido") - - except Exception as e: - self.logger.error(f"❌ Error actualizando panel LaTeX: {e}") - import traceback - self.logger.error(traceback.format_exc()) - - def _update_pywebview_panel(self, html_content): - """Actualiza el panel usando pywebview (crea nueva ventana cada vez)""" - try: - self.logger.debug(f"📤 Actualizando con pywebview: {len(html_content)} caracteres") - - # Guardar HTML para debugging - self._save_html_debug_copy(html_content) - - # Con subprocess, siempre creamos una nueva ventana - # Esto es más simple y evita problemas de sincronización - self._create_pywebview_window(html_content) - - except Exception as e: - self.logger.error(f"❌ Error con pywebview: {e}") - - def _create_pywebview_window(self, html_content): - """Crea una ventana pywebview usando subprocess para evitar conflictos de hilo""" - try: - import subprocess - import tempfile - import os - - # Crear archivo temporal con el HTML - with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f: - f.write(html_content) - html_file_path = f.name - - # Script Python para ejecutar pywebview en proceso separado - pywebview_script = f''' -import webview -import os - -try: - # Leer HTML desde archivo temporal - with open(r"{html_file_path}", "r", encoding="utf-8") as f: - html_content = f.read() - - # Crear ventana pywebview - window = webview.create_window( - "Ecuaciones LaTeX - MAV Calculator", - html=html_content, - width=350, - height=450, - min_size=(300, 350), - resizable=True, - shadow=True, - on_top=False, - transparent=False - ) - - print("🌐 Ventana pywebview creada exitosamente") - - # Iniciar webview - webview.start(debug=False) - -except Exception as e: - print(f"❌ Error en pywebview: {{e}}") -finally: - # Limpiar archivo temporal - try: - os.unlink(r"{html_file_path}") - except: - pass -''' - - # Ejecutar en proceso separado - subprocess.Popen([ - 'python', '-c', pywebview_script - ], creationflags=subprocess.CREATE_NEW_CONSOLE if os.name == 'nt' else 0) - - self.logger.debug("🚀 Proceso pywebview iniciado exitosamente") - - except Exception as e: - self.logger.error(f"❌ Error creando ventana pywebview: {e}") - - def _update_tkinterweb_panel(self, html_content): - """Actualiza el panel usando tkinterweb (fallback)""" - try: - self.logger.debug(f"📤 Cargando HTML en tkinterweb: {len(html_content)} caracteres") - self.logger.debug(f"📝 Muestra del HTML: {html_content[html_content.find('equations-container'):html_content.find('equations-container')+200]}...") - - if hasattr(self, 'latex_webview') and self.latex_webview: - self.latex_webview.load_html(html_content) - self.logger.debug("✅ HTML cargado en tkinterweb") - else: - self.logger.warning("⚠️ latex_webview no disponible en tkinterweb") - - # Guardar HTML para debugging - self._save_html_debug_copy(html_content) - - self.logger.debug("✅ Panel tkinterweb actualizado") - - except Exception as e: - self.logger.error(f"❌ Error actualizando tkinterweb: {e}") - - def _generate_html_with_mathjax(self): - """Genera HTML con MathJax cuando JavaScript está disponible""" - self.logger.debug(f"🔧 Generando HTML con {len(self._latex_equations)} ecuaciones") - - html_blocks = [] - - for i, eq_data in enumerate(self._latex_equations): - eq_type = eq_data.get('type', 'symbolic') - latex_content = eq_data.get('latex', '') - original_content = eq_data.get('original', '') - - self.logger.debug(f" Ecuación {i}: tipo={eq_type}, latex='{latex_content[:50]}...', original='{original_content[:50]}...'") - - # Preparar LaTeX para MathJax - if latex_content: - # Usar $$ para display math - if not latex_content.startswith('$'): - formatted_latex = f"$${latex_content}$$" - else: - formatted_latex = latex_content - - # Crear bloque HTML optimizado - block_html = f""" -
-
-
{formatted_latex}
-
-
""" - else: - # Sin LaTeX válido, mostrar texto original - block_html = f""" -
-
-
{original_content or 'Sin contenido'}
-
-
""" - - html_blocks.append(block_html) - self.logger.debug(f" ✅ Bloque HTML creado para ecuación {i}") - - # Generar HTML completo - if html_blocks: - equations_html = '\n'.join(html_blocks) - status_indicator = '
⏳ Cargando MathJax...
' - final_content = f'{equations_html}\n{status_indicator}' - self.logger.debug(f"📝 HTML de ecuaciones generado: {len(final_content)} caracteres") - else: - # Sin ecuaciones, mantener mensaje por defecto pero agregar status - final_content = """ -
- 📐 Panel de Ecuaciones LaTeX
- Renderizado con MathJax + fuentes matemáticas
- Las ecuaciones se mostrarán aquí automáticamente -
-
ℹ️ Sin ecuaciones
""" - self.logger.debug("📝 Sin ecuaciones - usando contenido por defecto") - - # Obtener HTML base - base_html = self._generate_html_with_javascript() - - # Reemplazar el contenido del contenedor - VERSIÓN MEJORADA - # Buscar el div equations-container y reemplazar todo su contenido - import re - - # Patrón para encontrar el div equations-container completo - pattern = r'
.*?
' - new_container = f'
\n{final_content}\n
' - - # Reemplazar usando regex para mayor precisión - html_content = re.sub(pattern, new_container, base_html, flags=re.DOTALL) - - # Verificar que el reemplazo funcionó - if 'equations-container' in html_content and ('equation-block' in final_content): - equations_found = html_content.count('equation-block') - self.logger.debug(f"🔍 Verificación de reemplazo: {equations_found} bloques de ecuaciones encontrados en HTML final") - else: - self.logger.warning("⚠️ El reemplazo del contenedor de ecuaciones puede no haber funcionado correctamente") - - self.logger.debug(f"🔚 HTML final generado: {len(html_content)} caracteres") - - return html_content - - def _generate_html_pywebview_with_equations(self): - """Genera HTML con ecuaciones específicamente optimizado para pywebview""" - # Crear HTML de ecuaciones usando el mismo sistema que tkinterweb pero con base pywebview - html_blocks = [] - - for i, eq_data in enumerate(self._latex_equations): - eq_type = eq_data.get('type', 'symbolic') - latex_content = eq_data.get('latex', '') - original_content = eq_data.get('original', '') - - # Preparar LaTeX para MathJax - if latex_content: - # Usar $$ para display math - if not latex_content.startswith('$'): - formatted_latex = f"$${latex_content}$$" - else: - formatted_latex = latex_content - - # Crear bloque HTML optimizado para pywebview - block_html = f""" -
-
-
{formatted_latex}
-
-
""" - - html_blocks.append(block_html) - self.logger.debug(f" ✅ Bloque HTML pywebview creado para ecuación {i}") - - # Combinar todos los bloques - final_content = '\n'.join(html_blocks) - self.logger.debug(f"📝 HTML de ecuaciones pywebview generado: {len(final_content)} caracteres") - - # Obtener HTML base y reemplazar contenido - base_html = self._generate_html_for_pywebview() - - # Reemplazar el contenido del contenedor usando regex (corregido para LaTeX) - import re - - # Patrón para encontrar el div equations-container completo - pattern = r'(
)(.*?)(
)' - - # Nuevo contenido del contenedor (escapar para regex) - new_container_content = f'
\\n{final_content}\\n
' - - # Reemplazar usando re.escape para contenido seguro - html_content = re.sub(pattern, lambda m: new_container_content, base_html, flags=re.DOTALL) - - self.logger.debug(f"🔚 HTML final pywebview generado: {len(html_content)} caracteres") - - return html_content - - def _generate_html_fallback_with_equations(self): - """Genera HTML fallback con ecuaciones en formato texto mejorado""" - html_blocks = [] - - for i, eq_data in enumerate(self._latex_equations): - eq_type = eq_data.get('type', 'symbolic') - latex_content = eq_data.get('latex', '') - original_content = eq_data.get('original', '') - - # Convertir LaTeX básico a texto legible - display_content = latex_content or original_content or 'Sin contenido' - - # Conversiones básicas de LaTeX a texto legible - display_content = display_content.replace('\\frac{', '(').replace('}{', ')/(').replace('}', ')') - display_content = display_content.replace('\\sqrt{', '√(').replace('}', ')') - display_content = display_content.replace('^{2}', '²').replace('^{3}', '³') - display_content = display_content.replace('\\', '') - - # Crear bloque HTML simple - block_html = f""" -
-
{display_content}
-
""" - - html_blocks.append(block_html) - - # Generar HTML completo - equations_html = '\n'.join(html_blocks) - - html_content = self._generate_html_fallback().replace( - '
\n
\n 📐 Panel de Ecuaciones LaTeX (Modo Fallback)
\n Renderizado sin JavaScript - PythonMonkey no disponible
\n Las ecuaciones se mostrarán en formato texto mejorado\n
\n
', - f'
\n{equations_html}\n
' - ) - - return html_content - - def _eval_js_tkinterweb(self, js_code: str) -> bool: - """Evalúa JavaScript en tkinterweb (requiere PythonMonkey para JavaScript real)""" - try: - if not self._js_available: - self.logger.debug("⚠️ JavaScript no disponible - PythonMonkey no instalado") - return False - - # Usar PythonMonkey para ejecutar JavaScript - # NOTA: PythonMonkey NO tiene acceso directo al DOM de tkinterweb - # Esta implementación es para compatibilidad futura - import pythonmonkey - - # JavaScript ejecutado en contexto aislado (sin DOM) - result = pythonmonkey.eval(js_code) - self.logger.debug(f"✅ JavaScript ejecutado via PythonMonkey: {js_code[:50]}...") - return True - - except Exception as e: - self.logger.debug(f"⚠️ Error ejecutando JavaScript: {e}") - return False - - def _trigger_mathjax_rerender(self): - """Re-renderiza MathJax específicamente para tkinterweb""" - try: - if hasattr(self, 'latex_webview') and self.latex_webview: - js_code = """ - console.log('🔄 [PYTHON] Trigger de re-renderizado para tkinterweb...'); - - // Llamar directamente a la función específica de tkinterweb - if (typeof renderizarParaTkinterweb === 'function') { - console.log('✅ [PYTHON] Función tkinterweb encontrada, ejecutando...'); - renderizarParaTkinterweb(); - } else if (typeof forzarRenderizadoTkinterweb === 'function') { - console.log('✅ [PYTHON] Función de forzado encontrada, ejecutando...'); - forzarRenderizadoTkinterweb(); - } else { - console.log('⚠️ [PYTHON] Funciones específicas no encontradas, intentando método genérico...'); - - // Fallback para casos donde las funciones no estén definidas - function renderizadoFallback() { - if (typeof window.MathJax !== 'undefined' && window.MathJax.typesetPromise) { - console.log('🔧 [PYTHON] Usando renderizado fallback...'); - - var mathElements = document.querySelectorAll('.math-display'); - console.log('📊 [PYTHON] Elementos a renderizar:', mathElements.length); - - window.MathJax.typesetPromise().then(function() { - console.log('🎉 [PYTHON] Renderizado fallback exitoso!'); - var statusEl = document.getElementById('mathjax-status'); - if (statusEl) { - statusEl.innerHTML = '✅ Renderizado desde Python'; - statusEl.style.color = '#4fc3f7'; - } - }).catch(function(err) { - console.log('❌ [PYTHON] Error en renderizado fallback:', err); - var statusEl = document.getElementById('mathjax-status'); - if (statusEl) { - statusEl.innerHTML = '❌ Error desde Python: ' + err.message; - statusEl.style.color = '#f44747'; - } - }); - } else { - console.log('❌ [PYTHON] MathJax no disponible para fallback'); - var statusEl = document.getElementById('mathjax-status'); - if (statusEl) { - statusEl.innerHTML = '❌ MathJax no disponible'; - statusEl.style.color = '#f44747'; - } - } - } - - renderizadoFallback(); - } - """ - success = self._eval_js_tkinterweb(js_code) - if success: - self.logger.debug("🔄 Trigger específico para tkinterweb enviado") - else: - self.logger.warning("⚠️ JavaScript no ejecutado - revisando conexión MathJax") - else: - self.logger.warning("⚠️ tkinterweb no disponible para JavaScript") - except Exception as e: - self.logger.debug(f"⚠️ Error ejecutando JavaScript en tkinterweb: {e}") - - - - def _update_content_indicator(self): - """Actualiza el indicador visual de contenido disponible""" - try: - if hasattr(self, '_latex_equations') and self._latex_equations: - # Mostrar indicador si no está visible - if not hasattr(self, '_indicator_visible') or not self._indicator_visible: - self.latex_indicator.pack(side=tk.BOTTOM, pady=2) - self._indicator_visible = True - else: - # Ocultar indicador si no hay contenido - if hasattr(self, '_indicator_visible') and self._indicator_visible: - self.latex_indicator.pack_forget() - self._indicator_visible = False - except Exception as e: - self.logger.debug(f"Error actualizando indicador: {e}") - - def _diagnose_mathjax(self): - """Ejecuta diagnóstico completo de MathJax""" - try: - if not hasattr(self, 'latex_webview') or not self.latex_webview: - messagebox.showerror("Error", "Panel LaTeX no disponible") - return - - # Crear ventana de diagnóstico - diag_window = tk.Toplevel(self.root) - diag_window.title("Diagnóstico MathJax") - diag_window.geometry("600x400") - diag_window.configure(bg="#2b2b2b") - - text_widget = scrolledtext.ScrolledText( - diag_window, - font=("Consolas", 10), - bg="#1e1e1e", - fg="#d4d4d4", - wrap=tk.WORD - ) - text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - def agregar_resultado(texto): - text_widget.insert(tk.END, texto + "\n") - text_widget.see(tk.END) - diag_window.update() - - agregar_resultado("🔍 DIAGNÓSTICO MATHJAX") - agregar_resultado("=" * 50) - agregar_resultado("") - - # 1. Verificar que el panel está visible - agregar_resultado(f"📐 Panel LaTeX visible: {self.latex_panel_visible}") - agregar_resultado(f"📊 Ecuaciones en panel: {len(self._latex_equations) if hasattr(self, '_latex_equations') else 0}") - agregar_resultado("") - - # 2. Ejecutar diagnóstico JavaScript - agregar_resultado("🔍 Ejecutando diagnóstico JavaScript...") - - js_diagnostic = """ - var resultados = []; - - // Verificar MathJax - resultados.push('🌐 window.MathJax definido: ' + (typeof window.MathJax !== 'undefined')); - - if (typeof window.MathJax !== 'undefined') { - resultados.push('⚙️ MathJax.typesetPromise: ' + (typeof window.MathJax.typesetPromise !== 'undefined')); - resultados.push('⚙️ MathJax.startup: ' + (typeof window.MathJax.startup !== 'undefined')); - if (window.MathJax.version) { - resultados.push('📦 Versión MathJax: ' + window.MathJax.version); - } - } - - // Verificar DOM - var equations = document.querySelectorAll('.math-display'); - resultados.push('📄 Elementos .math-display encontrados: ' + equations.length); - - var statusEl = document.getElementById('mathjax-status'); - if (statusEl) { - resultados.push('📊 Estado actual: ' + statusEl.innerHTML); - } - - // Verificar consola - resultados.push('🌐 Navegador: ' + navigator.userAgent.substring(0, 50) + '...'); - - console.log('🔍 Diagnóstico completo ejecutado'); - - // Retornar resultados como string - resultados.join('\\n'); - """ - - try: - # Ejecutar diagnóstico JavaScript - self.latex_webview.eval_js(js_diagnostic) - agregar_resultado("✅ Diagnóstico JavaScript ejecutado") - agregar_resultado("") - - # 3. Intentar renderizado de prueba - agregar_resultado("🧪 Intentando renderizado de prueba...") - - test_js = """ - function pruebaRenderizado() { - if (typeof window.MathJax !== 'undefined' && window.MathJax.typesetPromise) { - console.log('🧪 Iniciando prueba de renderizado...'); - window.MathJax.typesetPromise().then(function() { - console.log('✅ Prueba de renderizado exitosa'); - }).catch(function(err) { - console.log('❌ Error en prueba:', err); - }); - } else { - console.log('❌ MathJax no disponible para prueba'); - } - } - pruebaRenderizado(); - """ - - self.latex_webview.eval_js(test_js) - agregar_resultado("✅ Prueba de renderizado enviada") - - except Exception as e: - agregar_resultado(f"❌ Error ejecutando JavaScript: {e}") - - agregar_resultado("") - agregar_resultado("💡 SOLUCIONES POSIBLES:") - agregar_resultado("• Verifica conexión a internet (CDN de MathJax)") - agregar_resultado("• Cierra y reabre el panel LaTeX") - agregar_resultado("• Reinicia la aplicación") - agregar_resultado("• Revisa la consola del navegador (F12)") - - # Botón para cerrar - close_btn = tk.Button( - diag_window, - text="Cerrar", - command=diag_window.destroy, - bg="#3c3c3c", - fg="white", - relief=tk.FLAT - ) - close_btn.pack(pady=5) - - except Exception as e: - messagebox.showerror("Error", f"Error en diagnóstico:\n{e}") - - def _test_tkinterweb_mathjax(self): - """Test específico para tkinterweb y MathJax""" - try: - if not hasattr(self, 'latex_webview') or not self.latex_webview: - messagebox.showerror("Error", "Panel LaTeX tkinterweb no disponible") - return - - # Mostrar panel si no está visible - if not self.latex_panel_visible: - self._show_latex_panel() - messagebox.showinfo("Panel LaTeX", "Panel LaTeX activado para testing") - return - - # NUEVO: Test de contenido HTML actual - test_content_js = """ - console.log('🔍 [CONTENIDO] Test de contenido HTML actual...'); - - // 1. Verificar que el contenedor existe - var container = document.getElementById('equations-container'); - console.log('🔍 [CONTENIDO] Contenedor encontrado:', container !== null); - - if (container) { - console.log('🔍 [CONTENIDO] Contenido del contenedor:'); - console.log(container.innerHTML.substring(0, 500) + '...'); - - // 2. Contar elementos específicos - var equationBlocks = container.querySelectorAll('.equation-block'); - var mathDisplays = container.querySelectorAll('.math-display'); - var infoMessages = container.querySelectorAll('.info-message'); - - console.log('🔍 [CONTENIDO] Bloques de ecuación:', equationBlocks.length); - console.log('🔍 [CONTENIDO] Displays matemáticos:', mathDisplays.length); - console.log('🔍 [CONTENIDO] Mensajes de info:', infoMessages.length); - - // 3. Log de contenido de cada ecuación - for (var i = 0; i < Math.min(mathDisplays.length, 3); i++) { - console.log('🔍 [CONTENIDO] Ecuación ' + i + ':', mathDisplays[i].innerHTML.substring(0, 100)); - } - - // 4. Verificar si hay contenido renderizado por MathJax - var mjxElements = container.querySelectorAll('mjx-container, .MathJax, mjx-math'); - console.log('🔍 [CONTENIDO] Elementos MathJax renderizados:', mjxElements.length); - - // 5. Actualizar status con información de contenido - var statusEl = document.getElementById('mathjax-status'); - if (statusEl) { - var info = 'Contenido: ' + equationBlocks.length + ' ecuaciones, ' + mjxElements.length + ' renderizadas'; - statusEl.innerHTML = '🔍 ' + info; - statusEl.style.color = '#4fc3f7'; - } - } else { - console.log('❌ [CONTENIDO] Contenedor no encontrado'); - } - - console.log('🔍 [CONTENIDO] Test de contenido completado'); - """ - - # Ejecutar test de contenido primero - self.logger.debug("🧪 Ejecutando test de contenido HTML en tkinterweb...") - content_success = self._eval_js_tkinterweb(test_content_js) - - # Luego ejecutar test general - test_js = """ - console.log('🧪 [TEST] Iniciando test específico tkinterweb...'); - - // 1. Verificar estado general - console.log('🧪 [TEST] MathJax disponible:', typeof window.MathJax !== 'undefined'); - console.log('🧪 [TEST] typesetPromise disponible:', typeof window.MathJax !== 'undefined' && typeof window.MathJax.typesetPromise !== 'undefined'); - - // 2. Verificar funciones específicas - console.log('🧪 [TEST] renderizarParaTkinterweb:', typeof renderizarParaTkinterweb); - console.log('🧪 [TEST] forzarRenderizadoTkinterweb:', typeof forzarRenderizadoTkinterweb); - - // 3. Contar elementos GLOBALES - var mathElements = document.querySelectorAll('.math-display'); - var mjxElements = document.querySelectorAll('mjx-container, .MathJax, mjx-math'); - console.log('🧪 [TEST] Elementos .math-display GLOBALES:', mathElements.length); - console.log('🧪 [TEST] Elementos MathJax renderizados GLOBALES:', mjxElements.length); - - // 4. Test de renderizado forzado - if (typeof forzarRenderizadoTkinterweb === 'function') { - console.log('🧪 [TEST] Ejecutando renderizado forzado...'); - forzarRenderizadoTkinterweb(); - } else { - console.log('⚠️ [TEST] Función de renderizado forzado no disponible'); - } - - console.log('🧪 [TEST] Test completado'); - """ - - success = self._eval_js_tkinterweb(test_js) - - # Determinar estado final - if content_success and success: - status_msg = "Test completo ejecutado correctamente." - elif content_success: - status_msg = "Test de contenido ejecutado, JavaScript limitado." - else: - status_msg = "Tests limitados - JavaScript no disponible." - - # Información adicional del sistema - equation_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0 - - messagebox.showinfo( - "Test tkinterweb", - f"{status_msg}\n\n" - f"JavaScript disponible: {self._js_available}\n" - f"tkinterweb disponible: {self._webview_available}\n" - f"Ecuaciones en memoria: {equation_count}\n\n" - "Tests ejecutados:\n" - f"✓ Contenido HTML: {'Sí' if content_success else 'No'}\n" - f"✓ Funciones MathJax: {'Sí' if success else 'No'}\n\n" - "Para ver logs detallados:\n" - "• Revisa la consola de la aplicación\n" - "• O usa 'Abrir HTML en Navegador' + F12" - ) - - except Exception as e: - self.logger.error(f"Error en test tkinterweb: {e}") - messagebox.showerror("Error", f"Error en test tkinterweb:\n{e}") - - def _save_html_debug_copy(self, html_content): - """Guarda una copia del HTML generado para debugging en navegador""" - try: - # Usar nombre simple para fácil acceso - filename = "latex_debug.html" - - # Guardar en el directorio de la aplicación - with open(filename, 'w', encoding='utf-8') as f: - f.write(html_content) - - # Actualizar referencia al último archivo generado - self._last_debug_html = filename - - self.logger.debug(f"🔍 HTML de debug guardado: {filename}") - - except Exception as e: - self.logger.error(f"❌ Error guardando HTML de debug: {e}") - - def _open_debug_html_in_browser(self): - """Abre el último HTML de debug en el navegador del sistema""" - try: - if not hasattr(self, '_last_debug_html') or not self._last_debug_html: - messagebox.showwarning("Sin archivo", "No hay archivo HTML de debug disponible") - return - - if not os.path.exists(self._last_debug_html): - messagebox.showerror("Archivo no encontrado", f"El archivo {self._last_debug_html} no existe") - return - - import webbrowser - file_path = os.path.abspath(self._last_debug_html) - file_url = f"file:///{file_path.replace('\\', '/')}" - - webbrowser.open(file_url) - - messagebox.showinfo( - "HTML Abierto", - f"Archivo abierto en navegador:\n{self._last_debug_html}\n\n" - f"Presiona F12 para ver la consola del navegador y depurar MathJax.\n\n" - f"NUEVA CONFIGURACIÓN:\n" - f"• Sin polyfill.io (evita errores SSL)\n" - f"• Diagnóstico mejorado en consola\n" - f"• Función global: forzarRenderizado()" - ) - - except Exception as e: - messagebox.showerror("Error", f"Error abriendo archivo en navegador:\n{e}") - - def _show_latex_panel_status(self): - """Muestra el estado actual del panel LaTeX""" - try: - # Información básica - panel_exists = hasattr(self, 'latex_panel') - panel_visible = hasattr(self, 'latex_panel_visible') and self.latex_panel_visible - webview_exists = hasattr(self, 'latex_webview') and self.latex_webview - equations_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0 - - # Información sobre las ecuaciones - equations_info = "" - if hasattr(self, '_latex_equations') and self._latex_equations: - equations_info = "\n\nECUACIONES EN MEMORIA:\n" - for i, eq in enumerate(self._latex_equations[:5]): # Mostrar solo las primeras 5 - eq_type = eq.get('type', 'unknown') - latex_content = eq.get('latex', '') - equations_info += f"{i+1}. [{eq_type}] {latex_content[:50]}...\n" - if len(self._latex_equations) > 5: - equations_info += f"... y {len(self._latex_equations) - 5} más\n" - - # Estado de archivos - debug_file_exists = os.path.exists("latex_debug.html") - debug_file_info = "" - if debug_file_exists: - stat = os.stat("latex_debug.html") - import datetime - mod_time = datetime.datetime.fromtimestamp(stat.st_mtime) - debug_file_info = f"\nArchivo debug: latex_debug.html\nÚltima modificación: {mod_time.strftime('%H:%M:%S')}" - - status_message = f"""ESTADO DEL PANEL LATEX - -COMPONENTES: -• Panel creado: {'✓' if panel_exists else '✗'} -• Panel visible: {'✓' if panel_visible else '✗'} -• WebView tkinterweb: {'✓' if webview_exists else '✗'} -• JavaScript disponible: {'✓' if self._js_available else '✗'} - -CONTENIDO: -• Ecuaciones en memoria: {equations_count} -• Archivo debug existe: {'✓' if debug_file_exists else '✗'}{debug_file_info} - -WEBVIEW TYPE: {getattr(self, '_webview_type', 'No definido')} -WEBVIEW AVAILABLE: {getattr(self, '_webview_available', 'No definido')}{equations_info} - -PARA SOLUCIONAR: -1. Si las ecuaciones están en memoria pero no se ven: - → Usa 'Test tkinterweb' para diagnosticar -2. Si el archivo debug tiene contenido pero tkinterweb no: - → Puede ser problema de renderizado en tkinterweb -3. Revisa el archivo debug con 'Abrir HTML en Navegador'""" - - # Crear ventana de estado - status_window = tk.Toplevel(self.root) - status_window.title("Estado Panel LaTeX") - status_window.geometry("600x500") - status_window.configure(bg="#2b2b2b") - - text_widget = scrolledtext.ScrolledText( - status_window, - font=("Consolas", 10), - bg="#1e1e1e", - fg="#d4d4d4", - wrap=tk.WORD - ) - text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - text_widget.insert("1.0", status_message) - text_widget.config(state="disabled") - - # Botón de cerrar - close_btn = tk.Button( - status_window, - text="Cerrar", - command=status_window.destroy, - bg="#3c3c3c", - fg="white", - relief=tk.FLAT - ) - close_btn.pack(pady=5) - - except Exception as e: - self.logger.error(f"Error mostrando estado del panel: {e}") - messagebox.showerror("Error", f"Error mostrando estado:\n{e}") - - def _force_html_update(self): - """Fuerza actualización del panel LaTeX y generación de nuevo HTML de debug""" - try: - if hasattr(self, '_latex_equations') and self._latex_equations: - self.logger.info("🔄 Forzando actualización del panel LaTeX...") - - # Forzar actualización del panel - if self.latex_panel_visible: - self._update_latex_panel() - else: - # Si no está visible, mostrarlo temporalmente para generar HTML - self._show_latex_panel() - self.root.after(1000, lambda: self._update_latex_panel()) - - messagebox.showinfo( - "Actualización Forzada", - f"Panel LaTeX actualizado.\n\n" - f"Ecuaciones en panel: {len(self._latex_equations)}\n" - f"Nuevo HTML generado: latex_debug.html\n\n" - f"Usa 'Abrir HTML en Navegador' para ver el resultado." - ) - else: - messagebox.showwarning( - "Sin contenido", - "No hay ecuaciones en el panel LaTeX para actualizar.\n\n" - "Escribe algunas ecuaciones en la calculadora primero." - ) - - except Exception as e: - self.logger.error(f"Error forzando actualización: {e}") - messagebox.showerror("Error", f"Error forzando actualización:\n{e}") - - def _add_to_latex_panel(self, content_type: str, latex_content: str): - """Añade una ecuación al panel LaTeX""" - self.logger.debug(f"🔧 AÑADIENDO al panel LaTeX: tipo='{content_type}', contenido='{latex_content[:100]}...'") - - if not hasattr(self, '_latex_equations'): - self._latex_equations = [] - self.logger.debug(" -> Lista de ecuaciones inicializada") - - # Crear datos de la ecuación - equation_data = { - 'type': content_type, - 'latex': latex_content, - 'original': latex_content, # Guardar contenido original también - 'timestamp': time.time() - } - - before_count = len(self._latex_equations) - self._latex_equations.append(equation_data) - after_count = len(self._latex_equations) - - # Limitar número de ecuaciones (opcional) - max_equations = 50 - if len(self._latex_equations) > max_equations: - self._latex_equations = self._latex_equations[-max_equations:] - self.logger.debug(f" -> Lista limitada a {max_equations} ecuaciones") - - self.logger.debug(f"➕ Ecuación añadida: {content_type} (Total: {before_count} -> {after_count})") - - # Actualizar indicador visual - self._update_content_indicator() - - # IMPORTANTE: Actualizar panel si está visible - panel_visible = hasattr(self, 'latex_panel_visible') and self.latex_panel_visible - - self.logger.debug(f"🔍 Estado del panel: existe={hasattr(self, 'latex_panel_visible')}, visible={panel_visible}") - - if panel_visible: - self.logger.debug("🔄 Panel visible - FORZANDO actualización de contenido...") - self._update_latex_panel() - else: - self.logger.debug("👁️ Panel no visible - saltando actualización (ecuación guardada para cuando se abra)") - - self.logger.debug(f"✅ Proceso de añadir ecuación COMPLETADO") - self.logger.debug(f"📊 Estado final: {len(self._latex_equations)} ecuaciones en total") - - -def main(): - """Función principal""" - root = tk.Tk() - app = HybridCalculatorApp(root) - - try: - root.iconname("Calculadora MAV CAS") - except tk.TclError: - pass - - root.mainloop() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/test_final.py b/test_final.py deleted file mode 100644 index 512e080..0000000 --- a/test_final.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -""" -Test final para verificar que el problema de solve(t) esté solucionado -""" -from main_evaluation_puro import PureAlgebraicEngine - -def test_solve_issue(): - print("=== TEST FINAL: PROBLEMA SOLVE(T) ===") - - engine = PureAlgebraicEngine() - - # Secuencia original del usuario - lines = [ - 'x=t**2+5/m', - 'm=3', - 'x=4', - 't=?', - 'solve(t)', - 'form=solve(t)' - ] - - print("Ejecutando secuencia del usuario:") - for i, line in enumerate(lines, 1): - result = engine.evaluate_line(line) - status = "✅" if result.success else "❌" - print(f"{i}. {line:<15} -> {result.output} {status}") - - # Verificar específicamente el caso problemático - if line == 'form=solve(t)': - if result.output == 'form = []': - print(" ❌ PROBLEMA PERSISTE: form está vacío") - return False - else: - print(" ✅ PROBLEMA SOLUCIONADO: form contiene resultado") - return True - - return True - -if __name__ == "__main__": - success = test_solve_issue() - if success: - print("\n🎉 PROBLEMA SOLUCIONADO EXITOSAMENTE") - else: - print("\n💥 PROBLEMA PERSISTE") \ No newline at end of file diff --git a/tl_popup.py b/tl_popup.py deleted file mode 100644 index 1996197..0000000 --- a/tl_popup.py +++ /dev/null @@ -1,669 +0,0 @@ -""" -Sistema de resultados interactivos con tags clickeables - VERSIÓN CORREGIDA -""" -import tkinter as tk -from tkinter import Toplevel, scrolledtext -import sympy -from typing import Any, Optional, Dict, List, Tuple -import matplotlib.pyplot as plt -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -import numpy as np - - -class PlotResult: - """Placeholder para resultados de plotting - DEFINICIÓN PRINCIPAL""" - - def __init__(self, plot_type: str, args: tuple, kwargs: dict, original_expression: str = None): - self.plot_type = plot_type - self.args = args - self.kwargs = kwargs - self.original_expression = original_expression or "" - - def __str__(self): - return f"📊 Ver {self.plot_type.title()}" - - def __repr__(self): - return f"PlotResult('{self.plot_type}', {self.args}, {self.kwargs})" - - -class InteractiveResultManager: - """Maneja resultados interactivos con ventanas emergentes""" - - def __init__(self, parent_window: tk.Tk): - self.parent = parent_window - self.open_windows: Dict[str, Toplevel] = {} - self.update_input_callback = None # Callback para actualizar el panel de entrada - - def set_update_callback(self, callback): - """Establece el callback para actualizar el panel de entrada""" - self.update_input_callback = callback - - def create_interactive_tag(self, result: Any, text_widget: tk.Text) -> Optional[Tuple[str, str]]: - """ - Crea un tag interactivo para un resultado si es necesario - - Returns: - (tag_name, display_text) si se creó tag, None si no es necesario - """ - tag_name = None - display_text = "" - - # 🔧 CORRECCIÓN: Verificar con isinstance correcto - if isinstance(result, PlotResult): - tag_name = f"plot_{id(result)}" - display_text = f"📊 Ver {result.plot_type.title()}" - - elif isinstance(result, sympy.Matrix): - tag_name = f"matrix_{id(result)}" - rows, cols = result.shape - display_text = f"📋 Ver Matriz {rows}×{cols}" - - elif isinstance(result, list) and len(result) > 5: - tag_name = f"list_{id(result)}" - display_text = f"📋 Ver Lista ({len(result)} elementos)" - - elif isinstance(result, dict) and len(result) > 3: - tag_name = f"dict_{id(result)}" - display_text = f"🔍 Ver Diccionario ({len(result)} entradas)" - - elif hasattr(result, '__dict__') and len(str(result)) > 100: - tag_name = f"object_{id(result)}" - display_text = f"🔍 Ver Detalles ({type(result).__name__})" - - # 🔧 CORRECCIÓN: Solo crear tag si se encontró un tipo interactivo - if tag_name and display_text: - try: - # Configurar tag - text_widget.tag_configure( - tag_name, - foreground="#4fc3f7", - underline=True, - font=("Consolas", 11, "underline") - ) - - # Bind click event - text_widget.tag_bind( - tag_name, - "", - lambda e, r=result: self._handle_interactive_click(r) - ) - - text_widget.tag_bind( - tag_name, - "", - lambda e: text_widget.config(cursor="hand2") - ) - - text_widget.tag_bind( - tag_name, - "", - lambda e: text_widget.config(cursor="") - ) - - return (tag_name, display_text) - - except Exception as e: - print(f"⚠️ Error creando tag interactivo: {e}") - return None - - return None - - def _handle_interactive_click(self, result: Any): - """Maneja clicks en elementos interactivos""" - window_key = f"{type(result).__name__}_{id(result)}" - - # Si ya existe la ventana, enfocarla - if window_key in self.open_windows: - window = self.open_windows[window_key] - try: - if window.winfo_exists(): - window.lift() - window.focus_set() - return - else: - del self.open_windows[window_key] - except tk.TclError: - del self.open_windows[window_key] - - # Crear nueva ventana - try: - if isinstance(result, PlotResult): - self._show_plot_window(result, window_key) - elif isinstance(result, sympy.Matrix): - self._show_matrix_window(result, window_key) - elif isinstance(result, list): - self._show_list_window(result, window_key) - elif isinstance(result, dict): - self._show_dict_window(result, window_key) - else: - self._show_object_window(result, window_key) - except Exception as e: - print(f"❌ Error abriendo ventana interactiva: {e}") - - def _show_plot_window(self, plot_result: PlotResult, window_key: str): - """Muestra ventana con plot matplotlib e interfaz de edición""" - # Asegurar que las dimensiones de la ventana principal estén actualizadas - self.parent.update_idletasks() - parent_x = self.parent.winfo_x() - parent_y = self.parent.winfo_y() - parent_width = self.parent.winfo_width() - parent_height = self.parent.winfo_height() - - # Definir dimensiones y posición para la ventana del plot - plot_window_width = 700 # Aumentado para dar espacio al campo de edición - plot_window_height = parent_height # Misma altura que la ventana principal - - # Posicionar la ventana del plot a la derecha de la ventana principal - plot_window_x = parent_x + parent_width - plot_window_y = parent_y # Misma posición Y que la ventana principal - - window_title = f"Plot - {plot_result.plot_type}" - - # Crear la ventana base especificando la posición - window = self._create_base_window( - window_title, - width=plot_window_width, - height=plot_window_height, - pos_x=plot_window_x, - pos_y=plot_window_y - ) - self.open_windows[window_key] = window - - # Frame principal para organizar la ventana - main_frame = tk.Frame(window, bg="#2b2b2b") - main_frame.pack(fill=tk.BOTH, expand=True) - - # Frame superior para el campo de edición - edit_frame = tk.Frame(main_frame, bg="#2b2b2b") - edit_frame.pack(fill=tk.X, padx=10, pady=5) - - # Label para el campo de edición - tk.Label( - edit_frame, - text="Expresión:", - bg="#2b2b2b", - fg="#d4d4d4", - font=("Consolas", 10) - ).pack(side=tk.LEFT) - - # Campo de entrada para editar la expresión - self.current_expression = tk.StringVar() - self.current_expression.set(plot_result.original_expression) - - expression_entry = tk.Entry( - edit_frame, - textvariable=self.current_expression, - bg="#1e1e1e", - fg="#d4d4d4", - font=("Consolas", 11), - insertbackground="#ffffff" - ) - expression_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 5)) - - # Botón para redibujar - redraw_btn = tk.Button( - edit_frame, - text="Redibujar", - command=lambda: self._redraw_plot(plot_result, canvas_frame, expression_entry.get()), - bg="#4fc3f7", - fg="white", - font=("Consolas", 9), - relief=tk.FLAT - ) - redraw_btn.pack(side=tk.RIGHT, padx=5) - - # Frame para el canvas del plot - canvas_frame = tk.Frame(main_frame, bg="#2b2b2b") - canvas_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) - - # Configurar el protocolo de cierre para guardar la expresión editada - def on_window_close(): - edited_expression = expression_entry.get().strip() - original_expression = plot_result.original_expression.strip() - - # Si la expresión cambió y tenemos un callback, actualizar el panel de entrada - if edited_expression != original_expression and self.update_input_callback: - self.update_input_callback(original_expression, edited_expression) - - # Limpiar la ventana del registro - if window_key in self.open_windows: - del self.open_windows[window_key] - - # Cerrar la ventana - window.destroy() - - window.protocol("WM_DELETE_WINDOW", on_window_close) - - # Crear el plot inicial - self._create_plot_in_frame(plot_result, canvas_frame) - - # Hacer focus en el campo de entrada para edición inmediata - expression_entry.focus_set() - expression_entry.select_range(0, tk.END) - - def _redraw_plot(self, plot_result: PlotResult, canvas_frame: tk.Frame, new_expression: str): - """Redibuja el plot con una nueva expresión""" - try: - # Limpiar el frame actual - for widget in canvas_frame.winfo_children(): - widget.destroy() - - # Evaluar la nueva expresión - import sympy as sp - - # Crear contexto básico para evaluación - eval_context = { - 'sin': sp.sin, 'cos': sp.cos, 'tan': sp.tan, - 'exp': sp.exp, 'log': sp.log, 'sqrt': sp.sqrt, - 'pi': sp.pi, 'e': sp.E, 'x': sp.Symbol('x'), 'y': sp.Symbol('y'), - 'z': sp.Symbol('z'), 't': sp.Symbol('t') - } - - # Evaluar la expresión - new_expr = sp.sympify(new_expression, locals=eval_context) - - # Crear nuevo PlotResult con la expresión actualizada - new_plot_result = PlotResult( - plot_result.plot_type, - (new_expr,) + plot_result.args[1:], # Mantener argumentos adicionales - plot_result.kwargs, - new_expression - ) - - # Actualizar la expresión original en el objeto - plot_result.original_expression = new_expression - - # Redibujar - self._create_plot_in_frame(new_plot_result, canvas_frame) - - except Exception as e: - # Mostrar error en el frame - error_label = tk.Label( - canvas_frame, - text=f"Error en expresión: {e}", - fg="#f44747", - bg="#2b2b2b", - font=("Consolas", 11), - wraplength=600 - ) - error_label.pack(pady=20) - - def _create_plot_in_frame(self, plot_result: PlotResult, parent_frame: tk.Frame): - """Crea el plot dentro del frame especificado""" - try: - fig, ax = plt.subplots(figsize=(8, 6)) - - if plot_result.plot_type == "plot": - self._create_2d_plot(fig, ax, plot_result.args, plot_result.kwargs) - elif plot_result.plot_type == "plot3d": - self._create_3d_plot(fig, plot_result.args, plot_result.kwargs) - - # Embed en tkinter - canvas = FigureCanvasTkAgg(fig, parent_frame) - canvas.draw() - canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) - - # Toolbar para interactividad - try: - from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk - toolbar = NavigationToolbar2Tk(canvas, parent_frame) - toolbar.update() - except ImportError: - pass # Si no está disponible, continuar sin toolbar - - except Exception as e: - error_label = tk.Label( - parent_frame, - text=f"Error generando plot: {e}", - fg="#f44747", - bg="#2b2b2b", - font=("Consolas", 12) - ) - error_label.pack(pady=20) - - def _create_2d_plot(self, fig, ax, args, kwargs): - """Crea plot 2D usando SymPy""" - if len(args) >= 1: - expr = args[0] - - try: - if len(args) >= 2: - # Rango especificado: (variable, start, end) - var_range = args[1] - if isinstance(var_range, tuple) and len(var_range) == 3: - var, start, end = var_range - x_vals = np.linspace(float(start), float(end), 1000) - - # Evaluar expresión - f = sympy.lambdify(var, expr, 'numpy') - y_vals = f(x_vals) - - ax.plot(x_vals, y_vals, **kwargs) - ax.set_xlabel(str(var)) - ax.set_ylabel(str(expr)) - ax.grid(True) - ax.set_title(f"Plot: {expr}") - else: - # Rango por defecto - free_symbols = list(expr.free_symbols) - if free_symbols: - var = free_symbols[0] - x_vals = np.linspace(-10, 10, 1000) - f = sympy.lambdify(var, expr, 'numpy') - y_vals = f(x_vals) - - ax.plot(x_vals, y_vals, **kwargs) - ax.set_xlabel(str(var)) - ax.set_ylabel(str(expr)) - ax.grid(True) - ax.set_title(f"Plot: {expr}") - - except Exception as e: - ax.text(0.5, 0.5, f"Error: {e}", - transform=ax.transAxes, ha='center', va='center') - ax.set_title("Error en Plot") - - def _create_3d_plot(self, fig, args, kwargs): - """Crea plot 3D""" - try: - ax = fig.add_subplot(111, projection='3d') - - if len(args) >= 3: - expr = args[0] - x_range = args[1] # (x, x_start, x_end) - y_range = args[2] # (y, y_start, y_end) - - if isinstance(x_range, tuple) and isinstance(y_range, tuple): - x_var, x_start, x_end = x_range - y_var, y_start, y_end = y_range - - x_vals = np.linspace(float(x_start), float(x_end), 50) - y_vals = np.linspace(float(y_start), float(y_end), 50) - X, Y = np.meshgrid(x_vals, y_vals) - - f = sympy.lambdify([x_var, y_var], expr, 'numpy') - Z = f(X, Y) - - ax.plot_surface(X, Y, Z, **kwargs) - ax.set_xlabel(str(x_var)) - ax.set_ylabel(str(y_var)) - ax.set_zlabel(str(expr)) - ax.set_title(f"3D Plot: {expr}") - - except Exception as e: - ax.text2D(0.5, 0.5, f"Error: {e}", transform=ax.transAxes) - - def _show_matrix_window(self, matrix: sympy.Matrix, window_key: str): - """Muestra ventana con matriz formateada""" - rows, cols = matrix.shape - window_title = f"Matriz {rows}×{cols}" - - # Asegurar que las dimensiones de la ventana principal estén actualizadas - self.parent.update_idletasks() - parent_x = self.parent.winfo_x() - parent_y = self.parent.winfo_y() - parent_width = self.parent.winfo_width() - parent_height = self.parent.winfo_height() - - # Definir dimensiones y posición para la ventana de la matriz - matrix_window_width = 600 # Ancho deseado - matrix_window_height = parent_height # Misma altura que la ventana principal - matrix_window_x = parent_x + parent_width # A la derecha - matrix_window_y = parent_y # Misma posición Y - - window = self._create_base_window(window_title, width=matrix_window_width, height=matrix_window_height, - pos_x=matrix_window_x, pos_y=matrix_window_y) - self.open_windows[window_key] = window - - # Crear frame con scroll - frame = tk.Frame(window, bg="#2b2b2b") - frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - text_widget = scrolledtext.ScrolledText( - frame, - font=("Courier New", 12), - bg="#1e1e1e", - fg="#d4d4d4", - insertbackground="#ffffff", - wrap=tk.NONE - ) - text_widget.pack(fill=tk.BOTH, expand=True) - - # Formatear matriz - matrix_str = self._format_matrix(matrix) - text_widget.insert("1.0", matrix_str) - text_widget.config(state="disabled") - - # Botones de utilidad - button_frame = tk.Frame(window, bg="#2b2b2b") - button_frame.pack(fill=tk.X, padx=10, pady=5) - - try: - det_btn = tk.Button( - button_frame, - text="Determinante", - command=lambda: self._show_matrix_property(matrix, "determinante", matrix.det()), - bg="#3c3c3c", - fg="white" - ) - det_btn.pack(side=tk.LEFT, padx=5) - except: - pass # Skip si la matriz no es cuadrada - - if matrix.is_square: - try: - inv_btn = tk.Button( - button_frame, - text="Inversa", - command=lambda: self._show_matrix_property(matrix, "inversa", matrix.inv()), - bg="#3c3c3c", - fg="white" - ) - inv_btn.pack(side=tk.LEFT, padx=5) - except: - pass # Skip si no es invertible - - def _format_matrix(self, matrix: sympy.Matrix) -> str: - """Formatea una matriz para display""" - rows, cols = matrix.shape - - # Calcular ancho máximo de elementos - max_width = 0 - for i in range(rows): - for j in range(cols): - element_str = str(matrix[i, j]) - max_width = max(max_width, len(element_str)) - - max_width = max(max_width, 8) # Mínimo 8 caracteres - - # Construir representación - lines = [] - lines.append("┌" + " " * (max_width * cols + cols - 1) + "┐") - - for i in range(rows): - line = "│" - for j in range(cols): - element_str = str(matrix[i, j]) - padded = element_str.center(max_width) - line += padded - if j < cols - 1: - line += " " - line += "│" - lines.append(line) - - lines.append("└" + " " * (max_width * cols + cols - 1) + "┘") - - return "\n".join(lines) - - def _show_matrix_property(self, matrix: sympy.Matrix, prop_name: str, prop_value: Any): - """Muestra propiedad de matriz en ventana separada""" - prop_window = self._create_base_window(f"Matriz - {prop_name.title()}", width=400, height=300) - - text_widget = scrolledtext.ScrolledText( - prop_window, - font=("Courier New", 12), - bg="#1e1e1e", - fg="#d4d4d4", - insertbackground="#ffffff" - ) - text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - if isinstance(prop_value, sympy.Matrix): - content = f"{prop_name.title()}:\n\n{self._format_matrix(prop_value)}" - else: - content = f"{prop_name.title()}: {prop_value}" - - text_widget.insert("1.0", content) - text_widget.config(state="disabled") - - def _show_list_window(self, lst: list, window_key: str): - """Muestra ventana con lista expandida""" - window_title = f"Lista ({len(lst)} elementos)" - - # Asegurar que las dimensiones de la ventana principal estén actualizadas - self.parent.update_idletasks() - parent_x = self.parent.winfo_x() - parent_y = self.parent.winfo_y() - parent_width = self.parent.winfo_width() - parent_height = self.parent.winfo_height() - - # Definir dimensiones y posición para la ventana de la lista - list_window_width = 500 # Ancho deseado - list_window_height = parent_height # Misma altura que la ventana principal - list_window_x = parent_x + parent_width # A la derecha - list_window_y = parent_y # Misma posición Y - - window = self._create_base_window(window_title, width=list_window_width, height=list_window_height, - pos_x=list_window_x, pos_y=list_window_y) - self.open_windows[window_key] = window - - text_widget = scrolledtext.ScrolledText( - window, - font=("Consolas", 11), - bg="#1e1e1e", - fg="#d4d4d4", - insertbackground="#ffffff" - ) - text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - content = "Elementos de la lista:\n\n" - for i, item in enumerate(lst): - content += f"[{i}] {item}\n" - - text_widget.insert("1.0", content) - text_widget.config(state="disabled") - - def _show_dict_window(self, dct: dict, window_key: str): - """Muestra ventana con diccionario expandido""" - window = self._create_base_window(f"Diccionario ({len(dct)} entradas)", width=500, height=400) - self.open_windows[window_key] = window - - text_widget = scrolledtext.ScrolledText( - window, - font=("Consolas", 11), - bg="#1e1e1e", - fg="#d4d4d4", - insertbackground="#ffffff" - ) - text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - content = "Entradas del diccionario:\n\n" - for key, value in dct.items(): - content += f"{key}: {value}\n" - - text_widget.insert("1.0", content) - text_widget.config(state="disabled") - - def _show_object_window(self, obj: Any, window_key: str): - """Muestra ventana con detalles de objeto""" - window = self._create_base_window(f"Objeto - {type(obj).__name__}", width=600, height=500) - self.open_windows[window_key] = window - - text_widget = scrolledtext.ScrolledText( - window, - font=("Consolas", 11), - bg="#1e1e1e", - fg="#d4d4d4", - insertbackground="#ffffff" - ) - text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - content = f"Objeto: {type(obj).__name__}\n\n" - content += f"Valor: {obj}\n\n" - content += f"Representación: {repr(obj)}\n\n" - - if hasattr(obj, '__dict__'): - content += "Atributos:\n" - for attr, value in obj.__dict__.items(): - content += f" {attr}: {value}\n" - - content += "\nMétodos disponibles:\n" - for attr in dir(obj): - if not attr.startswith('_') and callable(getattr(obj, attr, None)): - content += f" {attr}()\n" - - text_widget.insert("1.0", content) - text_widget.config(state="disabled") - - def _create_base_window(self, - title: str, - width: int = 500, - height: int = 400, - pos_x: Optional[int] = None, - pos_y: Optional[int] = None) -> Toplevel: - """Crea ventana base con estilo consistente y posición opcional""" - window = Toplevel(self.parent) - window.title(title) - window.configure(bg="#2b2b2b") - window.transient(self.parent) # Hace que la ventana aparezca encima del padre - - # Construir la cadena de geometría completa WxH+X+Y - geometry_str = f"{width}x{height}" - - if pos_x is not None and pos_y is not None: - # Usar posición provista, asegurándose de que no sea negativa - final_x = max(0, pos_x) - final_y = max(0, pos_y) - geometry_str += f"+{final_x}+{final_y}" - else: - # Centrar ventana si no se especifica posición - # Para centrar, necesitamos las dimensiones de la pantalla - # y las dimensiones de la ventana (width, height ya las tenemos) - screen_width = window.winfo_screenwidth() - screen_height = window.winfo_screenheight() - - center_x = (screen_width // 2) - (width // 2) - center_y = (screen_height // 2) - (height // 2) - final_x = max(0, center_x) - final_y = max(0, center_y) - geometry_str += f"+{final_x}+{final_y}" - - window.geometry(geometry_str) # Aplicar tamaño y posición de una sola vez - - return window - - def close_all_windows(self): - """Cierra todas las ventanas interactivas de forma segura""" - windows_to_close = list(self.open_windows.items()) - - for window_key, window in windows_to_close: - try: - if window and window.winfo_exists(): - # Forzar el cierre del protocolo de cierre si existe - window.protocol("WM_DELETE_WINDOW", window.destroy) - window.quit() # Detener el mainloop de la ventana si lo tiene - window.destroy() # Destruir la ventana - except tk.TclError: - # La ventana ya fue destruida o no es válida - pass - except Exception as e: - print(f"Error cerrando ventana {window_key}: {e}") - - # Limpiar el diccionario - self.open_windows.clear() - - # Cerrar todas las figuras de matplotlib para liberar memoria - try: - plt.close('all') - except: - pass