Limpieza general
This commit is contained in:
parent
aaddfbc3fa
commit
9ba73a9db6
|
@ -0,0 +1,18 @@
|
|||
|
||||
ex = (t * 8) / w
|
||||
|
||||
|
||||
var1 = 2
|
||||
var2 = 4
|
||||
vatt1 = 4
|
||||
|
||||
IP4
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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.**
|
|
@ -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
|
|
@ -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! 🎉**
|
|
@ -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.
|
|
@ -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!** 🚀
|
|
@ -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):
|
|
@ -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
|
|
@ -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
|
|
@ -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
319
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()
|
|
@ -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=?
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
|
||||
a=4/7
|
|
@ -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
|
||||
]
|
||||
}
|
160
latex_debug.html
160
latex_debug.html
|
@ -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>
|
||||
|
|
@ -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()
|
4247
main_calc_app.py
4247
main_calc_app.py
File diff suppressed because it is too large
Load Diff
|
@ -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")
|
669
tl_popup.py
669
tl_popup.py
|
@ -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
|
Loading…
Reference in New Issue