Limpieza general

This commit is contained in:
Miguel 2025-06-11 18:37:57 +02:00
parent aaddfbc3fa
commit 9ba73a9db6
25 changed files with 399 additions and 6357 deletions

18
.data/history.txt Normal file
View File

@ -0,0 +1,18 @@
ex = (t * 8) / w
var1 = 2
var2 = 4
vatt1 = 4
IP4

View File

@ -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.**

View File

@ -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

View File

@ -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! 🎉**

View File

@ -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.

View File

@ -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!** 🚀

0
app/__init__.py Normal file
View File

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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):

319
calc.py
View File

@ -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()

View File

@ -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=?

View File

@ -1,2 +0,0 @@
a=4/7

View File

@ -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
]
}

View File

@ -1,160 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ecuaciones LaTeX - PyWebView</title>
<!-- MathJax 3 configuración optimizada para pywebview -->
<script>
window.MathJax = {
tex: {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']],
processEscapes: true
},
options: {
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
},
startup: {
ready: function () {
console.log('🚀 [pywebview] Iniciando MathJax...');
MathJax.startup.defaultReady();
console.log('✅ [pywebview] MathJax listo');
// Auto-renderizar después de carga
setTimeout(function() {
if (MathJax.typesetPromise) {
MathJax.typesetPromise().then(function() {
console.log('🎉 [pywebview] Renderizado automático completado');
}).catch(function(err) {
console.log('❌ [pywebview] Error en renderizado:', err);
});
}
}, 500);
}
}
};
</script>
<!-- MathJax 3 CDN -->
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 13px;
margin: 0;
padding: 8px;
line-height: 1.4;
}
.equation-block {
margin: 4px 0;
padding: 8px 10px;
background-color: #2d2d2d;
border-left: 3px solid #80c7f7;
border-radius: 4px;
word-wrap: break-word;
transition: background-color 0.2s ease;
}
.equation-block:hover {
background-color: #3a3a3a;
}
.equation-content {
font-size: 14px;
color: #ffffff;
line-height: 1.5;
}
.math-display {
font-size: 15px;
text-align: left;
margin: 2px 0;
padding: 4px;
background-color: #252525;
border-radius: 3px;
min-height: 20px;
}
/* Tipos de ecuaciones */
.assignment { border-left-color: #dcdcaa; }
.equation { border-left-color: #c586c0; }
.comment { border-left-color: #6a9955; font-style: italic; }
.symbolic { border-left-color: #9cdcfe; }
.info-message {
text-align: center;
color: #80c7f7;
font-style: italic;
margin: 20px;
padding: 15px;
border: 1px dashed #80c7f7;
border-radius: 8px;
font-size: 14px;
background-color: #2d2d2d;
}
.status {
font-size: 11px;
color: #808080;
text-align: center;
margin-top: 10px;
padding: 5px;
}
/* Animación suave */
.equation-block {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<div id="equations-container">\n
<div class="equation-block assignment">
<div class="equation-content">
<div class="math-display">$$72$$</div>
</div>
</div>
<div class="equation-block assignment">
<div class="equation-content">
<div class="math-display">$$36$$</div>
</div>
</div>
<div class="equation-block symbolic">
<div class="equation-content">
<div class="math-display">$$2592$$</div>
</div>
</div>
<div class="equation-block assignment">
<div class="equation-content">
<div class="math-display">$$\frac{\sqrt{8 y - 25}}{e}$$</div>
</div>
</div>
<div class="equation-block symbolic">
<div class="equation-content">
<div class="math-display">$$t = \frac{\sqrt{8 y - 25}}{e}$$</div>
</div>
</div>\n</div>
</div>
<div class="status" id="status">
✓ PyWebView activo - MathJax cargándose...
</div>
</body>
</html>

View File

@ -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()

File diff suppressed because it is too large Load Diff

View File

@ -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")

View File

@ -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,
"<Button-1>",
lambda e, r=result: self._handle_interactive_click(r)
)
text_widget.tag_bind(
tag_name,
"<Enter>",
lambda e: text_widget.config(cursor="hand2")
)
text_widget.tag_bind(
tag_name,
"<Leave>",
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