789 lines
29 KiB
Python
789 lines
29 KiB
Python
"""
|
|
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() |