Primera version con pyside

This commit is contained in:
Miguel 2025-06-07 23:48:14 +02:00
parent 488f78b409
commit 0cbf9dbf79
11 changed files with 1777 additions and 46 deletions

View File

@ -3,7 +3,7 @@
## Overview ## Overview
The MAV Calculator is a hybrid Computer Algebra System (CAS) built around a pure algebraic engine that treats all assignments as equations. The application follows a clean pipeline from user input to result display, with sophisticated tokenization and symbolic computation capabilities. The MAV Calculator is a hybrid Computer Algebra System (CAS) built around a pure algebraic engine that treats all assignments as equations. The application follows a clean pipeline from user input to result display, with sophisticated tokenization and symbolic computation capabilities. The GUI design must be minimalist and dark themed.
### The application has this main components: ### The application has this main components:
@ -21,6 +21,7 @@ The MAV Calculator is a hybrid Computer Algebra System (CAS) built around a pure
* Cycle: every time the user change the input panel start a cycle that ends when all lines are evaluated and produced an output * Cycle: every time the user change the input panel start a cycle that ends when all lines are evaluated and produced an output
* Input panel: tk text area where the user can write or modify text. * Input panel: tk text area where the user can write or modify text.
* Output panel: read only tk area text correlated 1:1 to every line on the input panel . Every input line must correspond to only 1 line on the output. The output lines can have colors and binds to click for opening the plots or to show matrix or lists. * Output panel: read only tk area text correlated 1:1 to every line on the input panel . Every input line must correspond to only 1 line on the output. The output lines can have colors and binds to click for opening the plots or to show matrix or lists.
* Mathjax panel: read only panel atached to the output panel on the right with the comments, equations and asignments. The formulas must be rendered using mathjax. This pannel must be collapsable.
* Persistence: The app maintain the state of all configurated setup and dimension of the window and all the text on the input panel. * Persistence: The app maintain the state of all configurated setup and dimension of the window and all the text on the input panel.
## Application Architecture ## Application Architecture

188
README_PYSIDE6.md Normal file
View File

@ -0,0 +1,188 @@
# 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! 🎉**

87
demo_minimalista.py Normal file
View File

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
Demostración del diseño minimalista de 3 paneles
Calculadora MAV - PySide6
"""
import sys
import time
def show_demo_instructions():
"""Muestra las instrucciones de la demostración"""
print("🎯 DEMOSTRACIÓN: Calculadora MAV - Diseño Minimalista")
print("=" * 60)
print()
print("📋 DISEÑO DE 3 PANELES:")
print(" ┌─────────────┬─────────────┬─────────────┐")
print(" │ PANEL 1 │ PANEL 2 │ PANEL 3 │")
print(" │ ENTRADA │ RESULTADOS │ LATEX │")
print(" │ │ (1:1) │ (colapsable)│")
print(" └─────────────┴─────────────┴─────────────┘")
print()
print("✨ CARACTERÍSTICAS PRINCIPALES:")
print(" • Correspondencia 1:1 entre líneas de entrada y salida")
print(" • Tema oscuro minimalista")
print(" • Resaltado de sintaxis en tiempo real")
print(" • Panel LaTeX con MathJax para ecuaciones")
print(" • Motor algebraico SymPy sin cambios")
print()
print("🎮 EJEMPLOS PARA PROBAR:")
print(" # Comentario - aparece en panel LaTeX")
print(" x**2 + y**2 = r**2")
print(" solve(x**2 - 4, x)")
print(" a = 5*x + 3")
print(" diff(x**3, x)")
print(" Matrix([[1, 2], [3, 4]])")
print()
print("⌨️ ATAJOS DE TECLADO:")
print(" • Ctrl+Enter / Shift+Enter: Evaluar")
print(" • F12: Mostrar/ocultar panel LaTeX")
print(" • Ctrl+N: Nueva sesión")
print()
def run_demo():
"""Ejecuta la demostración"""
show_demo_instructions()
try:
response = input("¿Ejecutar la aplicación? (s/N): ").strip().lower()
if response in ['s', '', 'si', 'y', 'yes']:
print("\n🚀 Iniciando Calculadora MAV...")
# Verificar dependencias primero
try:
from PySide6.QtWidgets import QApplication
print("✅ PySide6 disponible")
except ImportError:
print("❌ PySide6 no está instalado")
print(" Ejecuta: pip install -r requirements.txt")
return 1
# Iniciar aplicación
from main_calc_app_pyside6 import main as run_app
# Precarga con contenido de demostración
print("📝 Precargando contenido de demostración...")
demo_content = """# Demostración Calculadora MAV
x**2 + y**2 = r**2
solve(x**2 - 4, x)
a = 5*x + 3
diff(a, x)"""
# Ejecutar aplicación
run_app()
else:
print("👋 ¡Hasta luego!")
except KeyboardInterrupt:
print("\n\n🚪 Demostración cancelada")
return 0
except Exception as e:
print(f"\n❌ Error: {e}")
return 1
return 0
if __name__ == "__main__":
sys.exit(run_demo())

View File

@ -1,6 +1,8 @@
x**2 + y**2 = r**2 x**2 + y**2 = r**2
r=? r=?
a=r*5+5 a=r*5+5

View File

@ -1,9 +1,5 @@
{ {
"window_geometry": "1272x700+331+1194", "window_geometry": "1400x800",
"sash_pos_x": 440, "debug_mode": false,
"symbolic_mode": true, "latex_panel_visible": true
"show_numeric_approximation": true,
"keep_symbolic_fractions": true,
"auto_simplify": false,
"latex_panel_visible": true
} }

89
launch_pyside6.py Normal file
View File

@ -0,0 +1,89 @@
#!/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()

789
main_calc_app_pyside6.py Normal file
View File

@ -0,0 +1,789 @@
"""
Calculadora MAV CAS Híbrida - Aplicación PySide6 con MathJax
Diseño minimalista de 3 paneles con correspondencia 1:1 línea por línea
"""
import sys
import json
import logging
import os
import re
import threading
import time
from pathlib import Path
from typing import List, Dict, Any, Optional
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTextEdit, QPlainTextEdit, QSplitter, QPushButton, QLabel,
QFrame, QMenuBar, QStatusBar, QMessageBox, QFileDialog,
QScrollArea, QSizePolicy
)
from PySide6.QtCore import (
Qt, QTimer, QThread, QObject, Signal, QUrl, QSize
)
from PySide6.QtGui import (
QFont, QTextCursor, QTextCharFormat, QColor, QIcon,
QTextDocument, QSyntaxHighlighter, QTextFormat,
QKeySequence, QShortcut, QFontMetrics
)
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWebEngineCore import QWebEngineSettings
# Importar componentes del CAS híbrido
from main_evaluation_puro import PureAlgebraicEngine, EvaluationResult
from tl_popup import InteractiveResultManager, PlotResult
from type_registry import get_registered_helper_functions, get_registered_base_context
import sympy
from sympy_helper import SympyTools as SympyHelper
class MathInputHighlighter(QSyntaxHighlighter):
"""Resaltador de sintaxis para expresiones matemáticas"""
def __init__(self, parent=None):
super().__init__(parent)
self.setup_highlighting_rules()
def setup_highlighting_rules(self):
"""Configura las reglas de resaltado"""
self.highlighting_rules = []
# Números
number_format = QTextCharFormat()
number_format.setForeground(QColor("#89ddff"))
self.highlighting_rules.append((r'\b\d+\.?\d*\b', number_format))
# Funciones matemáticas
function_format = QTextCharFormat()
function_format.setForeground(QColor("#82aaff"))
function_format.setFontWeight(QFont.Bold)
functions = [
'sin', 'cos', 'tan', 'log', 'ln', 'exp', 'sqrt', 'abs',
'solve', 'diff', 'integrate', 'limit', 'series', 'factor',
'expand', 'simplify', 'Matrix', 'det', 'inv'
]
for func in functions:
pattern = rf'\b{func}\b'
self.highlighting_rules.append((pattern, function_format))
# Variables
variable_format = QTextCharFormat()
variable_format.setForeground(QColor("#c3e88d"))
self.highlighting_rules.append((r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', variable_format))
# Operadores
operator_format = QTextCharFormat()
operator_format.setForeground(QColor("#ff6b6b"))
operator_format.setFontWeight(QFont.Bold)
self.highlighting_rules.append((r'[+\-*/=<>!&|^]', operator_format))
# Paréntesis y corchetes
bracket_format = QTextCharFormat()
bracket_format.setForeground(QColor("#f78c6c"))
bracket_format.setFontWeight(QFont.Bold)
self.highlighting_rules.append((r'[\[\](){}]', bracket_format))
def highlightBlock(self, text):
"""Aplica el resaltado al bloque de texto"""
for pattern, format_obj in self.highlighting_rules:
expression = re.compile(pattern)
for match in expression.finditer(text):
start, end = match.span()
self.setFormat(start, end - start, format_obj)
class SynchronizedTextEdit(QTextEdit):
"""Editor de texto que mantiene sincronización línea por línea"""
def __init__(self, parent=None):
super().__init__(parent)
self.setLineWrapMode(QTextEdit.NoWrap)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
def sync_scroll_with(self, other_widget):
"""Sincroniza el scroll con otro widget"""
self.verticalScrollBar().valueChanged.connect(
other_widget.verticalScrollBar().setValue
)
class LineNumberedPlainTextEdit(QPlainTextEdit):
"""Editor de texto plano con numeración de líneas implícita"""
def __init__(self, parent=None):
super().__init__(parent)
self.setLineWrapMode(QPlainTextEdit.NoWrap)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
class MathJaxPanel(QWebEngineView):
"""Panel web para renderizado de LaTeX con MathJax - Panel colapsable a la derecha"""
def __init__(self, parent=None):
super().__init__(parent)
self.equations = []
self.setup_webview()
self.load_mathjax_base()
def setup_webview(self):
"""Configura el webview"""
self.setMinimumWidth(300)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
self.settings().setAttribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls, True)
self.settings().setAttribute(QWebEngineSettings.LocalContentCanAccessFileUrls, True)
def load_mathjax_base(self):
"""Carga el HTML base con MathJax"""
html_content = self.generate_base_html()
self.setHtml(html_content)
def generate_base_html(self):
"""Genera el HTML base con MathJax configurado"""
return """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MathJax Panel</title>
<style>
body {
font-family: 'Consolas', 'Courier New', monospace;
background-color: #1e1e1e;
color: #d4d4d4;
margin: 8px;
padding: 0;
line-height: 1.4;
font-size: 14px;
}
.equation {
margin: 6px 0;
padding: 8px;
background-color: #2d2d30;
border-left: 3px solid #4fc3f7;
border-radius: 3px;
transition: all 0.2s ease;
}
.equation:hover {
background-color: #363636;
border-left-color: #82aaff;
}
.equation-type {
font-size: 0.75em;
color: #4fc3f7;
margin-bottom: 4px;
text-transform: uppercase;
font-weight: bold;
opacity: 0.8;
}
.math-content {
font-size: 1.0em;
}
.assignment { border-left-color: #c3e88d; }
.assignment .equation-type { color: #c3e88d; }
.symbolic { border-left-color: #f78c6c; }
.symbolic .equation-type { color: #f78c6c; }
.comment {
border-left-color: #696969;
background-color: #252526;
}
.comment .equation-type { color: #696969; }
</style>
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
processEscapes: true
},
chtml: {
scale: 1.0,
minScale: 0.7
},
startup: {
ready: function () {
MathJax.startup.defaultReady();
}
}
};
function addEquation(type, content) {
const container = document.getElementById('equations-container');
if (!container) {
document.body.innerHTML = '<div id="equations-container"></div>';
}
const equationDiv = document.createElement('div');
equationDiv.className = 'equation ' + type;
equationDiv.innerHTML = `
<div class="equation-type">${type}</div>
<div class="math-content">$$${content}$$</div>
`;
document.getElementById('equations-container').appendChild(equationDiv);
if (window.MathJax && MathJax.typesetPromise) {
MathJax.typesetPromise([equationDiv]).catch(function (err) {
console.log('MathJax error:', err.message);
});
}
}
function clearEquations() {
const container = document.getElementById('equations-container');
if (container) {
container.innerHTML = '';
}
}
</script>
</head>
<body>
<div id="equations-container"></div>
</body>
</html>
"""
def add_equation(self, equation_type: str, latex_content: str):
"""Añade una ecuación al panel"""
self.equations.append({'type': equation_type, 'content': latex_content})
# Escapar backticks en el contenido LaTeX
escaped_content = latex_content.replace('`', '\\`')
js_code = f"addEquation('{equation_type}', `{escaped_content}`);"
self.page().runJavaScript(js_code)
def clear_equations(self):
"""Limpia todas las ecuaciones"""
self.equations.clear()
self.page().runJavaScript("clearEquations();")
class HybridCalculatorPySide6(QMainWindow):
"""Aplicación principal del CAS híbrido - Diseño minimalista de 3 paneles"""
SETTINGS_FILE = "hybrid_calc_settings.json"
HISTORY_FILE = "hybrid_calc_history.txt"
def __init__(self):
super().__init__()
# Configurar logging
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
self.logger = logging.getLogger(__name__)
# ========== USAR EL MOTOR ORIGINAL SIN CAMBIOS ==========
self.engine = PureAlgebraicEngine()
self.interactive_manager = None
# Variables de configuración
self.settings = self.load_settings()
self.debug = self.settings.get("debug_mode", False)
# Variables de estado UI
self.latex_panel_visible = True
self._debounce_timer = QTimer()
self._debounce_timer.setSingleShot(True)
self._debounce_timer.timeout.connect(self._evaluate_and_update)
# ========== CONFIGURAR HELPERS DINÁMICOS (COMO EN ORIGINAL) ==========
self._setup_dynamic_helpers()
# Configurar interfaz
self.setup_ui()
self.setup_shortcuts()
self.load_history()
# Configurar manager interactivo
self.setup_interactive_manager()
self.logger.info("✅ Calculadora MAV PySide6 inicializada")
def _setup_dynamic_helpers(self):
"""Configura helpers dinámicamente desde el registro de tipos - COMO EN ORIGINAL"""
try:
self.HELPERS = get_registered_helper_functions()
self.HELPERS.append(SympyHelper.Helper)
self.logger.info(f"Helpers dinámicos cargados: {len(self.HELPERS)}")
except Exception as e:
self.logger.error(f"Error cargando helpers dinámicos: {e}")
self.HELPERS = [SympyHelper.Helper]
def setup_interactive_manager(self):
"""Configura el manager de contenido interactivo - COMO EN ORIGINAL"""
try:
self.interactive_manager = InteractiveResultManager(self)
except Exception as e:
self.logger.error(f"Error configurando interactive manager: {e}")
self.interactive_manager = None
def setup_ui(self):
"""Configura la interfaz de usuario - DISEÑO MINIMALISTA DE 3 PANELES"""
self.setWindowTitle("Calculadora MAV - CAS Híbrido")
self.setGeometry(100, 100, 1400, 800)
self.setStyleSheet(self.get_minimal_dark_theme())
# Widget central
central_widget = QWidget()
self.setCentralWidget(central_widget)
# Layout principal horizontal
main_layout = QHBoxLayout(central_widget)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(1)
# ========== PANEL 1: ENTRADA ==========
self.input_text = LineNumberedPlainTextEdit()
self.input_text.setFont(QFont("Consolas", 11))
self.input_text.setPlaceholderText("Introduce expresiones matemáticas...")
self.input_text.textChanged.connect(self._on_input_changed)
# Configurar highlighter
self.highlighter = MathInputHighlighter(self.input_text.document())
# ========== PANEL 2: SALIDA (SINCRONIZADO 1:1) ==========
self.output_text = SynchronizedTextEdit()
self.output_text.setFont(QFont("Consolas", 11))
self.output_text.setReadOnly(True)
# Sincronizar scroll entre entrada y salida
self.input_text.verticalScrollBar().valueChanged.connect(
self.output_text.verticalScrollBar().setValue
)
self.output_text.verticalScrollBar().valueChanged.connect(
self.input_text.verticalScrollBar().setValue
)
# ========== PANEL 3: MATHJAX (COLAPSABLE) ==========
self.latex_panel = MathJaxPanel()
# ========== LAYOUT DE 3 PANELES ==========
# Paneles 1 y 2 tienen mismo ancho, panel 3 es más estrecho
main_layout.addWidget(self.input_text, 1)
main_layout.addWidget(self.output_text, 1)
main_layout.addWidget(self.latex_panel, 0)
# ========== CONFIGURAR TAGS DE SALIDA ==========
self.setup_output_tags()
# ========== MENÚ Y BARRA DE ESTADO ==========
self.create_menu_bar()
self.create_status_bar()
def setup_output_tags(self):
"""Configura los tags de formato para la salida - COMO EN ORIGINAL"""
doc = self.output_text.document()
# Crear formatos de texto
self.tag_formats = {}
# Error
error_format = QTextCharFormat()
error_format.setForeground(QColor("#ff6b6b"))
self.tag_formats['error'] = error_format
# Resultado simbólico
symbolic_format = QTextCharFormat()
symbolic_format.setForeground(QColor("#c3e88d"))
self.tag_formats['symbolic'] = symbolic_format
# Resultado numérico
numeric_format = QTextCharFormat()
numeric_format.setForeground(QColor("#89ddff"))
self.tag_formats['numeric'] = numeric_format
# Tipo personalizado
custom_format = QTextCharFormat()
custom_format.setForeground(QColor("#f78c6c"))
self.tag_formats['custom'] = custom_format
def _on_input_changed(self):
"""Maneja cambios en la entrada con debounce - COMO EN ORIGINAL"""
self._debounce_timer.start(300) # 300ms delay
def _evaluate_and_update(self):
"""Evalúa la entrada y actualiza la salida - USANDO MOTOR ORIGINAL"""
try:
# Obtener líneas de entrada
input_text = self.input_text.toPlainText()
lines = input_text.split('\n')
# Evaluar usando el motor original
self._evaluate_lines(lines)
except Exception as e:
self.logger.error(f"Error en evaluación: {e}")
self._show_error(str(e))
def _evaluate_lines(self, lines: List[str]):
"""Evalúa líneas usando el motor original - SIN CAMBIOS EN LÓGICA"""
try:
# Limpiar panel LaTeX
self.latex_panel.clear_equations()
# Limpiar salida
self.output_text.clear()
# Evaluar cada línea
output_lines = []
for i, line in enumerate(lines):
line = line.strip()
if not line or line.startswith('#'):
# Línea vacía o comentario
output_lines.append("")
if line.startswith('#'):
# Añadir comentario al panel LaTeX
comment_text = line[1:].strip()
if comment_text:
self.latex_panel.add_equation("comment", comment_text)
else:
try:
# Evaluar usando el motor original
result = self.engine.evaluate_line(line)
# Procesar resultado
output_data = self._process_evaluation_result(result)
if output_data:
output_lines.append(output_data[0][1]) # Tomar el texto del resultado
# Añadir al panel LaTeX si es aplicable
self._add_to_latex_panel_if_applicable(result)
else:
output_lines.append("")
except Exception as e:
error_msg = f"{str(e)}"
output_lines.append(error_msg)
self.logger.error(f"Error evaluando línea '{line}': {e}")
# Mostrar salida manteniendo correspondencia 1:1
self._display_output_lines(output_lines)
except Exception as e:
self.logger.error(f"Error en _evaluate_lines: {e}")
self._show_error(str(e))
def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]:
"""Procesa resultado de evaluación - USANDO ESTRUCTURA ORIGINAL"""
if not result.success:
return [("error", f"{result.error_message}")]
output_data = []
# El resultado principal está en result.output
if result.output:
# Determinar el tipo de formato basado en result_type
if result.result_type == "error":
output_data.append(("error", f"{result.output}"))
elif result.result_type == "plot":
output_data.append(("plot", f"📈 {result.output}"))
elif result.result_type == "symbolic":
output_data.append(("symbolic", f"📊 {result.output}"))
elif result.result_type == "numeric":
output_data.append(("numeric", f"🔢 {result.output}"))
else:
# Por defecto, mostrar como simbólico
output_data.append(("symbolic", result.output))
# Si no hay resultados, mostrar algo
if not output_data:
output_data.append(("symbolic", ""))
return output_data
def _display_output_lines(self, output_lines: List[str]):
"""Muestra líneas de salida manteniendo correspondencia 1:1"""
self.output_text.clear()
for i, line in enumerate(output_lines):
if i > 0:
self.output_text.append("") # Nueva línea
# Determinar formato basado en contenido
if line.startswith(""):
self._append_formatted_text(line, self.tag_formats['error'])
elif line.startswith("📊"):
self._append_formatted_text(line, self.tag_formats['symbolic'])
elif line.startswith("🔢"):
self._append_formatted_text(line, self.tag_formats['numeric'])
else:
self._append_formatted_text(line, self.tag_formats.get('custom', None))
def _append_formatted_text(self, text: str, format_obj: QTextCharFormat = None):
"""Añade texto formateado al panel de salida"""
cursor = self.output_text.textCursor()
cursor.movePosition(QTextCursor.End)
if format_obj:
cursor.insertText(text, format_obj)
else:
cursor.insertText(text)
def _add_to_latex_panel_if_applicable(self, result: EvaluationResult):
"""Añade resultado al panel LaTeX - USANDO ESTRUCTURA ORIGINAL"""
if not result.success:
return
try:
# Determinar tipo de ecuación basado en flags del result
if result.is_assignment:
equation_type = "assignment"
elif result.is_equation:
equation_type = "equation"
else:
equation_type = "symbolic"
# Generar LaTeX del objeto resultado actual
if result.actual_result_object is not None:
latex_content = self._sympy_to_latex(result.actual_result_object)
if latex_content:
self.latex_panel.add_equation(equation_type, latex_content)
except Exception as e:
self.logger.error(f"Error añadiendo al panel LaTeX: {e}")
def _sympy_to_latex(self, sympy_obj) -> str:
"""Convierte objeto SymPy a LaTeX - COMO EN ORIGINAL"""
try:
if hasattr(sympy_obj, 'latex'):
return sympy_obj.latex()
elif hasattr(sympy, 'latex'):
return sympy.latex(sympy_obj)
else:
return str(sympy_obj)
except Exception as e:
self.logger.error(f"Error convirtiendo a LaTeX: {e}")
return str(sympy_obj)
def _show_error(self, error_msg: str):
"""Muestra mensaje de error"""
self.output_text.append(f"❌ Error: {error_msg}")
self.statusBar().showMessage(f"{error_msg}", 5000)
def get_minimal_dark_theme(self):
"""Tema oscuro minimalista"""
return """
QMainWindow {
background-color: #1e1e1e;
color: #d4d4d4;
}
QPlainTextEdit, QTextEdit {
background-color: #252526;
color: #d4d4d4;
border: 1px solid #3c3c3c;
selection-background-color: #264f78;
padding: 8px;
}
QMenuBar {
background-color: #2d2d30;
color: #d4d4d4;
border-bottom: 1px solid #3c3c3c;
padding: 2px;
}
QMenuBar::item:selected {
background-color: #094771;
}
QMenu {
background-color: #2d2d30;
color: #d4d4d4;
border: 1px solid #3c3c3c;
}
QMenu::item:selected {
background-color: #094771;
}
QStatusBar {
background-color: #007acc;
color: white;
border: none;
}
"""
def setup_shortcuts(self):
"""Configura atajos de teclado"""
# Evaluar manual (forzar evaluación inmediata)
eval_shortcut = QShortcut(QKeySequence("Ctrl+Return"), self)
eval_shortcut.activated.connect(self._evaluate_and_update)
eval_shortcut2 = QShortcut(QKeySequence("Shift+Return"), self)
eval_shortcut2.activated.connect(self._evaluate_and_update)
# Toggle LaTeX panel
latex_shortcut = QShortcut(QKeySequence("F12"), self)
latex_shortcut.activated.connect(self.toggle_latex_panel)
def create_menu_bar(self):
"""Crea la barra de menú minimalista"""
menubar = self.menuBar()
# Menú Archivo
file_menu = menubar.addMenu("Archivo")
new_action = file_menu.addAction("Nuevo")
new_action.setShortcut(QKeySequence.New)
new_action.triggered.connect(self.new_session)
open_action = file_menu.addAction("Abrir...")
open_action.setShortcut(QKeySequence.Open)
open_action.triggered.connect(self.load_file)
save_action = file_menu.addAction("Guardar...")
save_action.setShortcut(QKeySequence.Save)
save_action.triggered.connect(self.save_file)
file_menu.addSeparator()
exit_action = file_menu.addAction("Salir")
exit_action.triggered.connect(self.close)
# Menú Ver
view_menu = menubar.addMenu("Ver")
toggle_latex_action = view_menu.addAction("Mostrar/Ocultar LaTeX")
toggle_latex_action.setShortcut(QKeySequence("F12"))
toggle_latex_action.triggered.connect(self.toggle_latex_panel)
# Menú Ayuda
help_menu = menubar.addMenu("Ayuda")
about_action = help_menu.addAction("Acerca de...")
about_action.triggered.connect(self.show_about)
def create_status_bar(self):
"""Crea la barra de estado"""
self.statusBar().showMessage("🔢 Calculadora MAV - Sistema Algebraico Híbrido")
def toggle_latex_panel(self):
"""Muestra/oculta el panel LaTeX"""
self.latex_panel_visible = not self.latex_panel_visible
if self.latex_panel_visible:
self.latex_panel.show()
else:
self.latex_panel.hide()
def new_session(self):
"""Inicia una nueva sesión"""
self.input_text.clear()
self.output_text.clear()
self.latex_panel.clear_equations()
self.statusBar().showMessage("✨ Nueva sesión iniciada", 2000)
def load_file(self):
"""Carga un archivo"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Abrir archivo", "", "Archivos de texto (*.txt);;Todos los archivos (*)"
)
if file_path:
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
self.input_text.setPlainText(content)
self.statusBar().showMessage(f"📁 Archivo cargado: {Path(file_path).name}", 3000)
except Exception as e:
QMessageBox.warning(self, "Error", f"No se pudo cargar el archivo:\n{e}")
def save_file(self):
"""Guarda el contenido actual"""
file_path, _ = QFileDialog.getSaveFileName(
self, "Guardar archivo", "", "Archivos de texto (*.txt);;Todos los archivos (*)"
)
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.input_text.toPlainText())
self.statusBar().showMessage(f"💾 Archivo guardado: {Path(file_path).name}", 3000)
except Exception as e:
QMessageBox.warning(self, "Error", f"No se pudo guardar el archivo:\n{e}")
def show_about(self):
"""Muestra información sobre la aplicación"""
QMessageBox.about(
self,
"Acerca de Calculadora MAV",
"""
<h2>Calculadora MAV - CAS Híbrido</h2>
<p><b>Versión PySide6 con diseño minimalista</b></p>
<p>Sistema algebraico computacional híbrido con 3 paneles:</p>
<ul>
<li>Panel de entrada (izquierda)</li>
<li>Panel de resultados 1:1 (centro)</li>
<li>Panel LaTeX/MathJax (derecha)</li>
</ul>
<p>Motor algebraico: <b>SymPy</b> con tipos personalizados</p>
"""
)
def load_settings(self) -> Dict[str, Any]:
"""Carga configuración desde archivo"""
try:
if Path(self.SETTINGS_FILE).exists():
with open(self.SETTINGS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
self.logger.warning(f"No se pudo cargar configuración: {e}")
return {
"window_geometry": "1400x800",
"debug_mode": False,
"latex_panel_visible": True
}
def save_settings(self):
"""Guarda configuración a archivo"""
try:
settings = {
"window_geometry": f"{self.width()}x{self.height()}",
"debug_mode": self.debug,
"latex_panel_visible": self.latex_panel_visible
}
with open(self.SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(settings, f, indent=2, ensure_ascii=False)
except Exception as e:
self.logger.error(f"Error guardando configuración: {e}")
def load_history(self):
"""Carga historial desde archivo"""
try:
if Path(self.HISTORY_FILE).exists():
with open(self.HISTORY_FILE, 'r', encoding='utf-8') as f:
history = f.read().strip()
if history:
self.input_text.setPlainText(history)
except Exception as e:
self.logger.warning(f"No se pudo cargar historial: {e}")
def save_history(self):
"""Guarda historial a archivo"""
try:
with open(self.HISTORY_FILE, 'w', encoding='utf-8') as f:
f.write(self.input_text.toPlainText())
except Exception as e:
self.logger.error(f"Error guardando historial: {e}")
def closeEvent(self, event):
"""Maneja el evento de cierre de la aplicación"""
self.save_settings()
self.save_history()
event.accept()
def main():
"""Función principal"""
app = QApplication(sys.argv)
app.setApplicationName("Calculadora MAV")
app.setApplicationVersion("2.0.0")
# Configurar estilo
app.setStyle('Fusion')
# Crear y mostrar ventana principal
window = HybridCalculatorPySide6()
window.show()
# Ejecutar aplicación
sys.exit(app.exec())
if __name__ == "__main__":
main()

View File

@ -1,12 +1,12 @@
# requirements.txt # requirements.txt
# Calculadora MAV - CAS Híbrido # Calculadora MAV - CAS Híbrido con PySide6 y MathJax
# Dependencias requeridas # Dependencias requeridas
# Motor algebraico principal # Motor algebraico principal
sympy>=1.12 sympy>=1.12
# Interfaz gráfica (generalmente incluido con Python) # Interfaz gráfica moderna
# tkinter - incluido con Python estándar PySide6>=6.6.0
# Plotting y visualización # Plotting y visualización
matplotlib>=3.7.0 matplotlib>=3.7.0

359
test.py
View File

@ -1,37 +1,332 @@
import webview # OPCIÓN 1: PySide6 (RECOMENDADA)
# pip install PySide6
def create_webview_app(): import sys
html_content = """ from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QHBoxLayout
<!DOCTYPE html> from PySide6.QtWebEngineWidgets import QWebEngineView
<html> from PySide6.QtCore import QUrl
<head>
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> class MathJaxPySide6(QMainWindow):
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script> def __init__(self):
<script> super().__init__()
MathJax = { self.setWindowTitle("🧮 MathJax con PySide6")
tex: { inlineMath: [['$', '$']], displayMath: [['$$', '$$']] } self.setGeometry(100, 100, 1200, 800)
};
</script> # Widget central
<style> central_widget = QWidget()
body { font-family: Arial, sans-serif; margin: 20px; } self.setCentralWidget(central_widget)
.demo { background: #f0f0f0; padding: 10px; margin: 10px 0; }
</style> # Layout principal
</head> layout = QVBoxLayout(central_widget)
<body>
<h1>MathJax con webview</h1> # Botones de control
<div class="demo"> button_layout = QHBoxLayout()
<p>Ecuación simple: $x^2 + y^2 = z^2$</p>
<p>Ecuación compleja:</p> btn_reload = QPushButton("🔄 Recargar")
$$\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}$$ btn_zoom_in = QPushButton("🔍+ Zoom In")
</div> btn_zoom_out = QPushButton("🔍- Zoom Out")
</body> btn_fullscreen = QPushButton("📺 Pantalla Completa")
</html>
""" btn_reload.clicked.connect(self.reload_page)
btn_zoom_in.clicked.connect(self.zoom_in)
btn_zoom_out.clicked.connect(self.zoom_out)
btn_fullscreen.clicked.connect(self.toggle_fullscreen)
button_layout.addWidget(btn_reload)
button_layout.addWidget(btn_zoom_in)
button_layout.addWidget(btn_zoom_out)
button_layout.addWidget(btn_fullscreen)
button_layout.addStretch()
layout.addLayout(button_layout)
# Vista web
self.web_view = QWebEngineView()
layout.addWidget(self.web_view)
# HTML con MathJax avanzado
self.html_content = """
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>MathJax Avanzado</title>
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
<script>
MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],
displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],
packages: {'[+]': ['ams', 'color', 'physics']}
},
chtml: {
scale: 1.3,
minScale: 0.5,
matchFontHeight: false
}
};
</script>
<style>
body {
font-family: 'Segoe UI', sans-serif;
margin: 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
background: white;
border-radius: 15px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
max-width: 1000px;
margin: 0 auto;
}
h1 { color: #2c3e50; text-align: center; margin-bottom: 40px; }
.section {
background: #f8f9fa;
border-left: 5px solid #007bff;
padding: 25px;
margin: 25px 0;
border-radius: 0 10px 10px 0;
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
}
.section h3 {
color: #495057;
margin-top: 0;
margin-bottom: 15px;
}
.interactive {
background: #e8f5e8;
border-left-color: #28a745;
}
.physics {
background: #fff3cd;
border-left-color: #ffc107;
}
.advanced {
background: #f8d7da;
border-left-color: #dc3545;
}
button {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
margin: 8px;
font-size: 14px;
transition: all 0.3s;
}
button:hover {
background: #0056b3;
transform: translateY(-2px);
}
#dynamic-content {
margin-top: 20px;
min-height: 100px;
}
</style>
</head>
<body>
<div class="container">
<h1>🚀 Laboratorio Matemático Avanzado</h1>
<div class="section">
<h3>📊 Álgebra Lineal</h3>
<p>Determinante de matriz 3×3:</p>
$$\\det(A) = \\begin{vmatrix}
a_{11} & a_{12} & a_{13} \\\\
a_{21} & a_{22} & a_{23} \\\\
a_{31} & a_{32} & a_{33}
\\end{vmatrix}$$
<p>Eigenvalores: $\\det(A - \\lambda I) = 0$</p>
</div>
<div class="section physics">
<h3> Física Cuántica</h3>
<p>Operador Hamiltoniano:</p>
$$\\hat{H} = \\frac{-\\hbar^2}{2m}\\nabla^2 + V(\\mathbf{r})$$
<p>Función de onda normalizada:</p>
$$\\int_{-\\infty}^{\\infty} |\\Psi(x,t)|^2 dx = 1$$
</div>
<div class="section advanced">
<h3>🔬 Matemáticas Avanzadas</h3>
<p>Transformada de Fourier:</p>
$$\\mathcal{F}[f(t)] = \\int_{-\\infty}^{\\infty} f(t) e^{-2\\pi i \\xi t} dt$$
<p>Función Gamma:</p>
$$\\Gamma(z) = \\int_0^\\infty t^{z-1} e^{-t} dt$$
</div>
<div class="section interactive">
<h3>🎮 Generador Interactivo</h3>
<button onclick="addCalculusExample()">📈 Cálculo</button>
<button onclick="addStatisticsExample()">📊 Estadística</button>
<button onclick="addGeometryExample()">📐 Geometría</button>
<button onclick="addPhysicsExample()"> Física</button>
<button onclick="clearContent()">🧹 Limpiar</button>
<div id="dynamic-content"></div>
</div>
</div>
<script>
const examples = {
calculus: [
{
title: "Regla de L'Hôpital",
eq: "$$\\\\lim_{x \\\\to c} \\\\frac{f(x)}{g(x)} = \\\\lim_{x \\\\to c} \\\\frac{f'(x)}{g'(x)}$$"
},
{
title: "Integración por partes",
eq: "$$\\\\int u \\\\, dv = uv - \\\\int v \\\\, du$$"
}
],
statistics: [
{
title: "Distribución Normal",
eq: "$$f(x) = \\\\frac{1}{\\\\sigma\\\\sqrt{2\\\\pi}} e^{-\\\\frac{(x-\\\\mu)^2}{2\\\\sigma^2}}$$"
},
{
title: "Teorema Central del Límite",
eq: "$$\\\\bar{X}_n \\\\xrightarrow{d} \\\\mathcal{N}\\\\left(\\\\mu, \\\\frac{\\\\sigma^2}{n}\\\\right)$$"
}
],
geometry: [
{
title: "Volumen de esfera",
eq: "$$V = \\\\frac{4}{3}\\\\pi r^3$$"
},
{
title: "Ley de cosenos",
eq: "$$c^2 = a^2 + b^2 - 2ab\\\\cos(C)$$"
}
],
physics: [
{
title: "Ecuaciones de Maxwell",
eq: "$$\\\\nabla \\\\cdot \\\\mathbf{E} = \\\\frac{\\\\rho}{\\\\epsilon_0}$$"
},
{
title: "Relatividad Especial",
eq: "$$t' = \\\\gamma \\\\left(t - \\\\frac{vx}{c^2}\\\\right)$$"
}
]
};
function addExample(category) {
const categoryExamples = examples[category];
const randomExample = categoryExamples[Math.floor(Math.random() * categoryExamples.length)];
const content = document.getElementById('dynamic-content');
const div = document.createElement('div');
div.className = 'section';
div.innerHTML = `
<h4> ${randomExample.title}</h4>
${randomExample.eq}
`;
content.appendChild(div);
MathJax.typesetPromise([div]);
}
function addCalculusExample() { addExample('calculus'); }
function addStatisticsExample() { addExample('statistics'); }
function addGeometryExample() { addExample('geometry'); }
function addPhysicsExample() { addExample('physics'); }
function clearContent() {
document.getElementById('dynamic-content').innerHTML = '';
}
MathJax.startup.promise.then(() => {
console.log('✅ MathJax cargado en PySide6!');
});
</script>
</body>
</html>
"""
# Cargar contenido
self.web_view.setHtml(self.html_content)
webview.create_window('MathJax App', html=html_content, width=800, height=600) def reload_page(self):
webview.start() self.web_view.setHtml(self.html_content)
def zoom_in(self):
current_zoom = self.web_view.zoomFactor()
self.web_view.setZoomFactor(current_zoom * 1.2)
def zoom_out(self):
current_zoom = self.web_view.zoomFactor()
self.web_view.setZoomFactor(current_zoom / 1.2)
def toggle_fullscreen(self):
if self.isFullScreen():
self.showNormal()
else:
self.showFullScreen()
# OPCIÓN 2: PyQt6 (Alternativa)
# pip install PyQt6 PyQt6-WebEngine
"""
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from PyQt6.QtWebEngineWidgets import QWebEngineView
class MathJaxPyQt6(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("MathJax con PyQt6")
self.setGeometry(100, 100, 1000, 700)
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
self.web_view = QWebEngineView()
layout.addWidget(self.web_view)
# Mismo HTML que PySide6
html_content = "[MISMO HTML DE ARRIBA]"
self.web_view.setHtml(html_content)
def run_pyqt6():
app = QApplication(sys.argv)
window = MathJaxPyQt6()
window.show()
sys.exit(app.exec())
"""
def run_pyside6():
app = QApplication(sys.argv)
window = MathJaxPySide6()
window.show()
sys.exit(app.exec())
# Función principal para elegir método
if __name__ == "__main__": if __name__ == "__main__":
create_webview_app() print("🧮 Iniciando aplicación MathJax con PySide6...")
print("✨ Funciones disponibles:")
print(" - Zoom in/out")
print(" - Pantalla completa")
print(" - Generador interactivo de ecuaciones")
print(" - MathJax completamente funcional")
run_pyside6()
# RESUMEN DE INSTALACIÓN:
#
# Para PySide6 (RECOMENDADO):
# pip install PySide6
#
# Para PyQt6:
# pip install PyQt6 PyQt6-WebEngine
#
# Ambos funcionan igual de bien para MathJax,
# pero PySide6 tiene licencia más permisiva.

212
test_pyside6.py Normal file
View File

@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""
Script de prueba para la versión PySide6 de Calculadora MAV
"""
import sys
import traceback
from pathlib import Path
def test_imports():
"""Prueba todas las importaciones necesarias"""
print("🧪 Probando importaciones...")
tests = [
("PySide6.QtWidgets", "QApplication"),
("PySide6.QtCore", "QThread"),
("PySide6.QtGui", "QFont"),
("PySide6.QtWebEngineWidgets", "QWebEngineView"),
("sympy", None),
("numpy", None),
("matplotlib", None),
]
success_count = 0
for module, attr in tests:
try:
if attr:
mod = __import__(module, fromlist=[attr])
getattr(mod, attr)
else:
__import__(module)
print(f"{module}" + (f".{attr}" if attr else ""))
success_count += 1
except ImportError as e:
print(f"{module}" + (f".{attr}" if attr else "") + f" - {e}")
except Exception as e:
print(f" ⚠️ {module}" + (f".{attr}" if attr else "") + f" - {e}")
print(f"\n📊 Resultado: {success_count}/{len(tests)} módulos disponibles")
return success_count == len(tests)
def test_application_creation():
"""Prueba la creación básica de la aplicación"""
print("\n🏗️ Probando creación de aplicación...")
try:
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QCoreApplication
# Crear aplicación mínima
if not QCoreApplication.instance():
app = QApplication([])
else:
app = QCoreApplication.instance()
print(" ✅ QApplication creada correctamente")
# Probar importación de nuestra aplicación
from main_calc_app_pyside6 import HybridCalculatorPySide6
print(" ✅ Clase HybridCalculatorPySide6 importada")
# Probar creación de ventana (sin mostrar)
window = HybridCalculatorPySide6()
print(" ✅ Ventana principal creada")
# Verificar componentes principales
assert hasattr(window, 'input_text'), "input_text no encontrado"
assert hasattr(window, 'output_text'), "output_text no encontrado"
assert hasattr(window, 'latex_panel'), "latex_panel no encontrado"
assert hasattr(window, 'engine'), "engine no encontrado"
print(" ✅ Componentes principales verificados")
return True
except Exception as e:
print(f" ❌ Error: {e}")
traceback.print_exc()
return False
def test_engine_functionality():
"""Prueba la funcionalidad del motor de cálculo"""
print("\n⚙️ Probando motor de cálculo...")
try:
from main_evaluation_puro import PureAlgebraicEngine
engine = PureAlgebraicEngine()
print(" ✅ Motor PureAlgebraicEngine creado")
# Prueba básica
result = engine.evaluate_line("2 + 2")
print(f" ✅ Evaluación básica: 2 + 2 = {result.output if result.success else result.error_message}")
# Prueba simbólica
result = engine.evaluate_line("x**2 + y**2")
print(f" ✅ Evaluación simbólica: x**2 + y**2 = {result.output if result.success else result.error_message}")
return True
except Exception as e:
print(f" ❌ Error: {e}")
traceback.print_exc()
return False
def test_mathjax_html():
"""Prueba la generación de HTML MathJax"""
print("\n📐 Probando generación MathJax...")
try:
from main_calc_app_pyside6 import MathJaxPanel
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QCoreApplication
if not QCoreApplication.instance():
app = QApplication([])
panel = MathJaxPanel()
print(" ✅ Panel MathJax creado")
# Verificar HTML base
html = panel.generate_base_html()
assert "MathJax" in html, "MathJax no encontrado en HTML"
assert "equation" in html, "Estilos de ecuación no encontrados"
print(" ✅ HTML base generado correctamente")
return True
except Exception as e:
print(f" ❌ Error: {e}")
traceback.print_exc()
return False
def run_minimal_app():
"""Ejecuta una versión mínima de la aplicación para prueba visual"""
print("\n🖥️ Iniciando prueba visual (cierra la ventana para continuar)...")
try:
from PySide6.QtWidgets import QApplication
from main_calc_app_pyside6 import HybridCalculatorPySide6
app = QApplication(sys.argv)
window = HybridCalculatorPySide6()
# Precargar algunos datos de prueba
window.input_text.setPlainText("# Prueba PySide6\nx**2 + y**2 = r**2\nsolve(x**2 - 4, x)")
window.show()
print(" ✅ Aplicación mostrada - cierra la ventana para continuar")
# No ejecutar el loop principal, solo mostrar
return True
except Exception as e:
print(f" ❌ Error: {e}")
traceback.print_exc()
return False
def main():
"""Función principal de pruebas"""
print("🚀 Iniciando pruebas de Calculadora MAV PySide6")
print("=" * 60)
tests = [
("Importaciones", test_imports),
("Creación de aplicación", test_application_creation),
("Motor de cálculo", test_engine_functionality),
("HTML MathJax", test_mathjax_html),
]
results = []
for test_name, test_func in tests:
try:
result = test_func()
results.append((test_name, result))
except Exception as e:
print(f"💥 Error inesperado en {test_name}: {e}")
results.append((test_name, False))
# Resumen de resultados
print("\n" + "=" * 60)
print("📋 RESUMEN DE PRUEBAS")
print("=" * 60)
passed = 0
total = len(results)
for test_name, success in results:
status = "✅ PASÓ" if success else "❌ FALLÓ"
print(f" {status:<10} {test_name}")
if success:
passed += 1
print(f"\n📊 Resultado final: {passed}/{total} pruebas pasaron")
if passed == total:
print("🎉 ¡Todas las pruebas pasaron! La aplicación está lista.")
# Ofrecer ejecutar prueba visual
try:
response = input("\n¿Quieres ejecutar una prueba visual? (s/N): ").strip().lower()
if response in ['s', '', 'si', 'y', 'yes']:
run_minimal_app()
except KeyboardInterrupt:
print("\n🚪 Prueba cancelada por el usuario")
else:
print("⚠️ Algunas pruebas fallaron. Revisa los errores antes de ejecutar la aplicación.")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

72
test_simple_engine.py Normal file
View File

@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
Prueba simple del motor PureAlgebraicEngine para verificar la API
"""
import sys
from pathlib import Path
def test_engine():
"""Prueba básica del motor"""
print("🧪 Probando PureAlgebraicEngine...")
try:
# Importar el motor
from main_evaluation_puro import PureAlgebraicEngine
print(" ✅ Import exitoso")
# Crear instancia
engine = PureAlgebraicEngine()
print(" ✅ Instancia creada")
# Pruebas básicas
test_cases = [
"2 + 2",
"x**2 + y**2",
"solve(x**2 - 4, x)",
"# Esto es un comentario",
"a = 5*x + 3",
"x**2 + y**2 = r**2"
]
print("\n📝 Ejecutando casos de prueba:")
for i, test_case in enumerate(test_cases, 1):
try:
result = engine.evaluate_line(test_case)
status = "" if result.success else ""
output = result.output if result.success else result.error_message
print(f" {i}. {status} '{test_case}'{output}")
# Información adicional del resultado
if result.success:
print(f" Tipo: {result.result_type}, Asignación: {result.is_assignment}, Ecuación: {result.is_equation}")
except Exception as e:
print(f" {i}. ❌ '{test_case}' → ERROR: {e}")
print("\n✅ Pruebas del motor completadas")
return True
except Exception as e:
print(f"❌ Error en prueba del motor: {e}")
import traceback
traceback.print_exc()
return False
def main():
"""Función principal"""
print("🚀 Prueba Simple del Motor Algebraico")
print("=" * 50)
success = test_engine()
if success:
print("\n🎉 ¡Motor funcionando correctamente!")
print(" Puedes proceder a usar la aplicación PySide6")
else:
print("\n⚠️ Problemas detectados en el motor")
print(" Revisa los errores antes de usar la aplicación")
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())