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
|
from PySide6.QtWebEngineCore import QWebEngineSettings
|
||||||
|
|
||||||
# Importar componentes del CAS híbrido
|
# Importar componentes del CAS híbrido
|
||||||
from main_evaluation_puro import PureAlgebraicEngine, EvaluationResult
|
from .main_evaluation import PureAlgebraicEngine, EvaluationResult
|
||||||
from tl_popup_pyside6 import InteractiveResultManager, PlotResult
|
from .tl_popup import InteractiveResultManager, PlotResult
|
||||||
from type_registry import get_registered_helper_functions, get_registered_base_context
|
from .type_registry import get_registered_helper_functions, get_registered_base_context
|
||||||
import sympy
|
import sympy
|
||||||
from sympy_helper import SympyTools as SympyHelper
|
from .sympy_helper import SympyTools as SympyHelper
|
||||||
|
|
||||||
|
|
||||||
class InputTextEdit(QPlainTextEdit):
|
class InputTextEdit(QPlainTextEdit):
|
||||||
|
@ -406,7 +406,13 @@ class LatexPanel(QWidget):
|
||||||
self._pending_equations.clear()
|
self._pending_equations.clear()
|
||||||
|
|
||||||
if self._webview_available:
|
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:
|
else:
|
||||||
self._setup_text_browser() # Reset al estado inicial
|
self._setup_text_browser() # Reset al estado inicial
|
||||||
|
|
||||||
|
@ -566,8 +572,8 @@ class AutocompletePopup(QWidget):
|
||||||
class HybridCalculatorPySide6(QMainWindow):
|
class HybridCalculatorPySide6(QMainWindow):
|
||||||
"""Aplicación principal del CAS híbrido - VERSIÓN COMPLETA"""
|
"""Aplicación principal del CAS híbrido - VERSIÓN COMPLETA"""
|
||||||
|
|
||||||
SETTINGS_FILE = "hybrid_calc_settings.json"
|
SETTINGS_FILE = "./.data/settings.json"
|
||||||
HISTORY_FILE = "hybrid_calc_history.txt"
|
HISTORY_FILE = "./.data/history.txt"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -972,6 +978,7 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
input_content = self.input_text.toPlainText()
|
input_content = self.input_text.toPlainText()
|
||||||
if not input_content.strip():
|
if not input_content.strip():
|
||||||
self._clear_output()
|
self._clear_output()
|
||||||
|
# NO limpiar panel LaTeX cuando no hay contenido - mantener ecuaciones previas
|
||||||
return
|
return
|
||||||
|
|
||||||
# Limpiar contexto del motor
|
# Limpiar contexto del motor
|
||||||
|
@ -980,7 +987,7 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
self.engine.variables.clear()
|
self.engine.variables.clear()
|
||||||
self.logger.debug("Contexto del motor limpiado")
|
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'):
|
if hasattr(self, '_latex_equations'):
|
||||||
self._latex_equations.clear()
|
self._latex_equations.clear()
|
||||||
self.latex_panel.clear_equations()
|
self.latex_panel.clear_equations()
|
||||||
|
@ -1307,6 +1314,20 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
self.output_text.clear()
|
self.output_text.clear()
|
||||||
cursor = self.output_text.textCursor()
|
cursor = self.output_text.textCursor()
|
||||||
cursor.insertText(error_msg, self.output_formats['error'])
|
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):
|
def _handle_output_link_click(self, link_id: str, result_object):
|
||||||
"""Maneja clicks en links del panel de salida"""
|
"""Maneja clicks en links del panel de salida"""
|
||||||
|
@ -1355,9 +1376,15 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
self._select_autocomplete()
|
self._select_autocomplete()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Detectar backspace para cerrar popup si se borra el punto
|
# Detectar backspace para filtrar autocompletado o cerrar popup
|
||||||
if event.key() == Qt.Key_Backspace and self._autocomplete_active:
|
if event.key() == Qt.Key_Backspace:
|
||||||
QTimer.singleShot(1, self._check_dot_removal)
|
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
|
# Procesar autocompletado después de insertar carácter
|
||||||
if event.text() and not event.modifiers() & Qt.ControlModifier:
|
if event.text() and not event.modifiers() & Qt.ControlModifier:
|
||||||
|
@ -1376,23 +1403,34 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
# Verificar si acabamos de navegar
|
# Verificar si acabamos de navegar
|
||||||
just_navigated = (time.time() - self._last_navigation_time) < 0.1
|
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
|
# Manejar autocompletado con punto
|
||||||
if event_text == '.' and not self._popup_disabled_until_next_dot:
|
if event_text == '.' and not self._popup_disabled_until_next_dot:
|
||||||
if self._variable_popup_active:
|
if self._variable_popup_active:
|
||||||
self._close_autocomplete_popup()
|
self._close_autocomplete_popup()
|
||||||
self._handle_dot_autocomplete()
|
self._handle_dot_autocomplete()
|
||||||
|
|
||||||
# Filtrar autocompletado si está activo
|
# Filtrar autocompletado si está activo (incluye caracteres alfanuméricos y backspace procesado)
|
||||||
elif self._autocomplete_active and event_text and not just_navigated:
|
elif self._autocomplete_active and not just_navigated:
|
||||||
self._filter_autocomplete()
|
self._filter_autocomplete()
|
||||||
|
|
||||||
# Marcar tiempo del último cambio
|
# Marcar tiempo del último cambio
|
||||||
if event_text:
|
if event_text:
|
||||||
self._last_input_change = time.time()
|
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:
|
if not self._autocomplete_active and not self._popup_disabled_until_next_dot:
|
||||||
self._schedule_variable_autocomplete_improved()
|
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):
|
def _on_key_release(self, event):
|
||||||
"""Maneja eventos después de insertar carácter - mantenido para compatibilidad"""
|
"""Maneja eventos después de insertar carácter - mantenido para compatibilidad"""
|
||||||
|
@ -1502,6 +1540,26 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
for fname, fhint in sympy_functions:
|
for fname, fhint in sympy_functions:
|
||||||
if fname not in current_names:
|
if fname not in current_names:
|
||||||
suggestions.append((fname, fhint))
|
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:
|
except Exception as e:
|
||||||
self.logger.debug(f"Error obteniendo sugerencias globales: {e}")
|
self.logger.debug(f"Error obteniendo sugerencias globales: {e}")
|
||||||
|
@ -1525,6 +1583,60 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
# Programar para 800ms después
|
# Programar para 800ms después
|
||||||
self._variable_popup_timer.start(800)
|
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):
|
def _show_variable_autocomplete_improved(self):
|
||||||
"""Muestra autocompletado de variables disponibles"""
|
"""Muestra autocompletado de variables disponibles"""
|
||||||
if self._autocomplete_active or self._variable_popup_active:
|
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]]):
|
def _show_variable_popup(self, variables: List[Tuple[str, str]]):
|
||||||
"""Muestra popup de variables"""
|
"""Muestra popup de variables"""
|
||||||
self._variable_popup_active = True
|
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
|
# Las variables ya vienen en el formato correcto (nombre, descripción)
|
||||||
suggestions = [(name, f"= {value}") for name, value in variables]
|
self._show_autocomplete_popup(variables, is_global_popup=False)
|
||||||
self._show_autocomplete_popup(suggestions, is_global_popup=False)
|
|
||||||
|
|
||||||
def _filter_autocomplete(self):
|
def _filter_autocomplete(self):
|
||||||
"""Filtra las sugerencias del autocompletado"""
|
"""Filtra las sugerencias del autocompletado dinámicamente"""
|
||||||
if not self._autocomplete_active or not self._autocomplete_trigger_pos:
|
if not self._autocomplete_active:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Obtener texto escrito después del punto
|
|
||||||
cursor = self.input_text.textCursor()
|
cursor = self.input_text.textCursor()
|
||||||
current_pos = cursor.position()
|
current_pos = cursor.position()
|
||||||
|
|
||||||
if current_pos > self._autocomplete_trigger_pos:
|
# Para popup de variables (filtrar por texto parcial)
|
||||||
cursor.setPosition(self._autocomplete_trigger_pos)
|
if self._variable_popup_active:
|
||||||
cursor.setPosition(current_pos, QTextCursor.KeepAnchor)
|
# Obtener la palabra actual bajo el cursor
|
||||||
|
cursor.select(QTextCursor.WordUnderCursor)
|
||||||
filter_text = cursor.selectedText().lower()
|
filter_text = cursor.selectedText().lower()
|
||||||
self._autocomplete_filter_text = filter_text
|
self._autocomplete_filter_text = filter_text
|
||||||
else:
|
|
||||||
self._autocomplete_filter_text = ""
|
# Si no hay texto que filtrar, regenerar popup con todas las variables
|
||||||
|
if not filter_text:
|
||||||
# Filtrar sugerencias
|
# Regenerar popup dinámicamente con variables actuales
|
||||||
filtered = []
|
try:
|
||||||
for name, hint in self._current_suggestions:
|
context = self.engine._get_full_context()
|
||||||
if name.lower().startswith(self._autocomplete_filter_text):
|
variables = []
|
||||||
filtered.append((name, hint))
|
|
||||||
|
# Filtrar variables
|
||||||
if filtered and self._autocomplete_popup:
|
for name, value in context.items():
|
||||||
self._autocomplete_popup.set_suggestions(filtered)
|
if (not name.startswith('_') and
|
||||||
self._autocomplete_popup.adjust_size()
|
not callable(value) and
|
||||||
else:
|
name not in ['sympy', 'math', 'numpy', 'plt', 'builtins']):
|
||||||
self._close_autocomplete_popup()
|
|
||||||
|
# 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):
|
def _handle_arrow_key(self, direction: int):
|
||||||
"""Maneja navegación con flechas en el popup"""
|
"""Maneja navegación con flechas en el popup"""
|
||||||
|
@ -1674,41 +1873,92 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
|
|
||||||
# Para popup de variables
|
# Para popup de variables
|
||||||
if self._variable_popup_active:
|
if self._variable_popup_active:
|
||||||
# Reemplazar palabra actual
|
# Obtener el texto parcial ya escrito
|
||||||
|
current_pos = cursor.position()
|
||||||
cursor.select(QTextCursor.WordUnderCursor)
|
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
|
return
|
||||||
|
|
||||||
# Para popup global (después de punto solo)
|
# Para popup global (después de punto solo)
|
||||||
if self._is_global_popup:
|
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 - 1)
|
||||||
cursor.setPosition(self._autocomplete_trigger_pos, QTextCursor.KeepAnchor)
|
cursor.setPosition(self._autocomplete_trigger_pos, QTextCursor.KeepAnchor)
|
||||||
cursor.insertText(text + "()")
|
|
||||||
# Posicionar cursor dentro de paréntesis
|
# Verificar si es una función o variable para decidir si agregar paréntesis
|
||||||
cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text) + 1)
|
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)
|
self.input_text.setTextCursor(cursor)
|
||||||
else:
|
else:
|
||||||
# Para métodos de objeto
|
# Para métodos de objeto - considerar texto ya escrito después del punto
|
||||||
if self._autocomplete_filter_text:
|
if self._autocomplete_filter_text:
|
||||||
# Eliminar texto filtrado
|
# Calcular la posición correcta del texto ya escrito
|
||||||
start_pos = cursor.position() - len(self._autocomplete_filter_text)
|
current_pos = cursor.position()
|
||||||
cursor.setPosition(start_pos)
|
filter_len = len(self._autocomplete_filter_text)
|
||||||
cursor.setPosition(start_pos + len(self._autocomplete_filter_text), QTextCursor.KeepAnchor)
|
|
||||||
|
# Seleccionar el texto filtrado para reemplazarlo
|
||||||
|
cursor.setPosition(current_pos - filter_len)
|
||||||
|
cursor.setPosition(current_pos, QTextCursor.KeepAnchor)
|
||||||
cursor.removeSelectedText()
|
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)
|
self.input_text.setTextCursor(cursor)
|
||||||
|
|
||||||
def _check_dot_removal(self):
|
def _check_dot_removal(self):
|
||||||
"""Verifica si se borró el punto que activó el autocompletado"""
|
"""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()
|
cursor = self.input_text.textCursor()
|
||||||
if cursor.position() > 0:
|
current_pos = cursor.position()
|
||||||
cursor.setPosition(cursor.position() - 1)
|
|
||||||
cursor.setPosition(cursor.position() + 1, QTextCursor.KeepAnchor)
|
# Si el cursor está antes de la posición del trigger, el punto fue eliminado
|
||||||
if cursor.selectedText() == '.':
|
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()
|
self._close_autocomplete_popup()
|
||||||
|
|
||||||
def _close_autocomplete_popup(self):
|
def _close_autocomplete_popup(self):
|
|
@ -16,13 +16,13 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_SYMPY_HELPER = False
|
HAS_SYMPY_HELPER = False
|
||||||
|
|
||||||
from type_registry import (
|
from .type_registry import (
|
||||||
get_registered_base_context,
|
get_registered_base_context,
|
||||||
get_registered_tokenization_patterns,
|
get_registered_tokenization_patterns,
|
||||||
discover_and_register_types
|
discover_and_register_types
|
||||||
)
|
)
|
||||||
from tl_bracket_parser import BracketParser
|
from .tl_bracket_parser import BracketParser
|
||||||
from tl_popup import PlotResult
|
from .tl_popup import PlotResult
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
|
@ -22,7 +22,7 @@ from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Importar motores de evaluación
|
# 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):
|
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
|
# Crear motor de evaluación según el módulo especificado
|
||||||
if engine_module == 'main_evaluation_puro':
|
if engine_module == 'main_evaluation_puro':
|
||||||
from main_evaluation_puro import PureAlgebraicEngine
|
from .main_evaluation import PureAlgebraicEngine
|
||||||
engine = PureAlgebraicEngine()
|
engine = PureAlgebraicEngine()
|
||||||
else:
|
else:
|
||||||
# Motor por defecto
|
# Motor por defecto
|
|
@ -5,7 +5,7 @@ import sympy
|
||||||
from sympy import Basic, Symbol, sympify
|
from sympy import Basic, Symbol, sympify
|
||||||
from typing import Any, Optional, Dict
|
from typing import Any, Optional, Dict
|
||||||
import re
|
import re
|
||||||
from class_base import ClassBase
|
from .class_base import ClassBase
|
||||||
|
|
||||||
|
|
||||||
class SympyClassBase(ClassBase, sympy.Basic):
|
class SympyClassBase(ClassBase, sympy.Basic):
|
319
calc.py
319
calc.py
|
@ -1,270 +1,89 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Launcher principal para Calculadora MAV - CAS Híbrido
|
Launcher para Calculadora MAV - Versión PySide6 con MathJax
|
||||||
Este script maneja la inicialización y ejecución de la aplicación
|
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import messagebox
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import logging
|
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():
|
def check_pyside6_webengine():
|
||||||
"""Configura el sistema de logging completo"""
|
"""Verifica si PySide6 WebEngine está disponible"""
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
existing_logs = sorted(
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||||
[f for f in log_dir.glob("mav_calc_*.log") if f.is_file()],
|
print("✅ PySide6 WebEngine disponible para MathJax")
|
||||||
key=os.path.getmtime
|
return True
|
||||||
)
|
except ImportError:
|
||||||
if len(existing_logs) >= MAX_LOG_FILES:
|
print("⚠️ PySide6 WebEngine no disponible")
|
||||||
logs_to_delete = existing_logs[:len(existing_logs) - MAX_LOG_FILES + 1]
|
print(" Instalando QtWebEngine...")
|
||||||
for old_log in logs_to_delete:
|
try:
|
||||||
old_log.unlink()
|
subprocess.run([sys.executable, '-m', 'pip', 'install', 'PySide6-WebEngine'],
|
||||||
logging.info(f"Eliminado log antiguo: {old_log}")
|
check=True, capture_output=True)
|
||||||
except Exception as e:
|
print("✅ PySide6 WebEngine instalado correctamente")
|
||||||
logging.warning(f"No se pudieron eliminar logs antiguos: {e}")
|
return True
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
# Configurar logging
|
print("❌ No se pudo instalar PySide6 WebEngine")
|
||||||
logging.basicConfig(
|
return False
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Función principal del launcher"""
|
"""Función principal del launcher"""
|
||||||
global logger, log_file
|
print("🚀 Iniciando Calculadora MAV - Diseño Minimalista 3 Paneles")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
# Configurar logging al inicio
|
# Verificar dependencias
|
||||||
try:
|
if not check_dependencies():
|
||||||
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")
|
|
||||||
sys.exit(1)
|
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__":
|
if __name__ == "__main__":
|
||||||
# Configurar logging básico para manejo de argumentos
|
main()
|
||||||
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)
|
|
|
@ -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