341 lines
12 KiB
Python
341 lines
12 KiB
Python
"""
|
|
Calculadora MAV CAS Híbrida - Aplicación principal PySide6 (Refactorizada)
|
|
VERSIÓN MODULAR: Código organizado en módulos especializados
|
|
"""
|
|
import sys
|
|
import os
|
|
import logging
|
|
from typing import Optional, Dict, Any, List
|
|
|
|
from PySide6.QtWidgets import (
|
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QSplitter, QStatusBar
|
|
)
|
|
from PySide6.QtCore import Qt, QTimer
|
|
from PySide6.QtGui import QKeySequence, QShortcut, QPixmap, QIcon
|
|
|
|
# Importar componentes del CAS híbrido
|
|
from .evaluation import PureAlgebraicEngine
|
|
from .gui_popup import InteractiveResultManager
|
|
from .type_registry import get_registered_helper_functions
|
|
from .sympy_Helper import SympyTools as SympyHelper
|
|
|
|
# Importar módulos refactorizados
|
|
from .gui_widgets import InputTextEdit, OutputTextEdit, ExpandableLatexButton, AutocompletePopup
|
|
from .gui_latex import LatexPanel
|
|
from .gui_autocomplete import AutocompleteManager
|
|
from .gui_evaluation import EvaluationManager
|
|
from .gui_menus import MenuManager
|
|
from .gui_settings import SettingsManager
|
|
|
|
|
|
class HybridCalculatorPySide6(QMainWindow):
|
|
"""Aplicación principal del CAS híbrido - VERSIÓN COMPLETA"""
|
|
|
|
SETTINGS_FILE = "./.data/settings.json"
|
|
HISTORY_FILE = "./.data/history.txt"
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
# Configurar logging
|
|
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
# Configurar icono de la aplicación
|
|
self._setup_application_icon()
|
|
|
|
# Motor y managers
|
|
self.engine = PureAlgebraicEngine()
|
|
self.interactive_manager = None
|
|
|
|
# Inicializar managers
|
|
self.settings_manager = SettingsManager(self)
|
|
self.menu_manager = MenuManager(self)
|
|
self.autocomplete_manager = AutocompleteManager(self)
|
|
self.evaluation_manager = EvaluationManager(self)
|
|
|
|
# Configuración
|
|
self.settings = self.settings_manager.settings
|
|
self.debug = self.settings.get("debug_mode", False)
|
|
|
|
# Estado del panel LaTeX
|
|
self.latex_panel_visible = self.settings.get("latex_panel_visible", False)
|
|
self._latex_equations = []
|
|
|
|
# Configurar helpers
|
|
self._setup_dynamic_helpers()
|
|
|
|
# Configurar UI
|
|
self._setup_ui()
|
|
|
|
# Configurar componentes usando managers
|
|
self.menu_manager.setup_menu()
|
|
self._setup_interactive_manager()
|
|
|
|
# Cargar historial y configuración
|
|
self.settings_manager.initialize()
|
|
|
|
self.logger.info("✅ Calculadora MAV PySide6 (versión modular) inicializada")
|
|
|
|
def _setup_dynamic_helpers(self):
|
|
"""Configura helpers dinámicamente desde el registro de tipos"""
|
|
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_ui(self):
|
|
"""Configura la interfaz de usuario completa"""
|
|
self.setWindowTitle("Calculadora MAV - CAS Híbrido")
|
|
self.setGeometry(100, 100, 1000, 700)
|
|
|
|
# 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(0)
|
|
|
|
# Splitter principal para entrada y salida
|
|
self.main_splitter = QSplitter(Qt.Horizontal)
|
|
|
|
# Panel de entrada
|
|
self.input_text = InputTextEdit(self)
|
|
self.input_text.setPlaceholderText("Introduce expresiones matemáticas...")
|
|
self.input_text.textChanged.connect(self._on_input_changed)
|
|
|
|
# Panel de salida
|
|
self.output_text = OutputTextEdit()
|
|
self.output_text.link_clicked.connect(self._handle_output_link_click)
|
|
|
|
# Añadir al splitter
|
|
self.main_splitter.addWidget(self.input_text)
|
|
self.main_splitter.addWidget(self.output_text)
|
|
|
|
# Configurar tamaños iniciales
|
|
self.main_splitter.setSizes([450, 450])
|
|
|
|
# Sincronizar scroll
|
|
self._setup_scroll_sync()
|
|
|
|
# Añadir splitter al layout
|
|
main_layout.addWidget(self.main_splitter)
|
|
|
|
# Botón expandible para LaTeX
|
|
self.latex_button = ExpandableLatexButton()
|
|
self.latex_button.clicked.connect(self._toggle_latex_panel)
|
|
main_layout.addWidget(self.latex_button)
|
|
|
|
# Panel LaTeX (inicialmente oculto)
|
|
self.latex_panel = LatexPanel(self)
|
|
self.latex_panel.setMinimumWidth(300)
|
|
self.latex_panel.setMaximumWidth(500)
|
|
|
|
if self.latex_panel_visible:
|
|
main_layout.addWidget(self.latex_panel)
|
|
self.latex_button.setChecked(True)
|
|
else:
|
|
# Asegurar que el panel está oculto por defecto
|
|
self.latex_panel.hide()
|
|
self.latex_button.setChecked(False)
|
|
|
|
# Los formatos de salida se configuran automáticamente en EvaluationManager
|
|
|
|
# Barra de estado
|
|
self.status_bar = QStatusBar()
|
|
self.setStatusBar(self.status_bar)
|
|
self._update_status("🔢 Calculadora MAV - Sistema Algebraico Puro")
|
|
|
|
# Aplicar tema oscuro
|
|
self._apply_dark_theme()
|
|
|
|
def _setup_scroll_sync(self):
|
|
"""Sincroniza el scroll entre entrada y salida"""
|
|
def sync_input_to_output():
|
|
if hasattr(self, '_syncing'):
|
|
return
|
|
self._syncing = True
|
|
self.output_text.verticalScrollBar().setValue(
|
|
self.input_text.verticalScrollBar().value()
|
|
)
|
|
self._syncing = False
|
|
|
|
def sync_output_to_input():
|
|
if hasattr(self, '_syncing'):
|
|
return
|
|
self._syncing = True
|
|
self.input_text.verticalScrollBar().setValue(
|
|
self.output_text.verticalScrollBar().value()
|
|
)
|
|
self._syncing = False
|
|
|
|
self.input_text.verticalScrollBar().valueChanged.connect(sync_input_to_output)
|
|
self.output_text.verticalScrollBar().valueChanged.connect(sync_output_to_input)
|
|
|
|
def _setup_interactive_manager(self):
|
|
"""Configura el manager de interactividad"""
|
|
self.interactive_manager = InteractiveResultManager()
|
|
|
|
def _apply_dark_theme(self):
|
|
"""Aplica el tema oscuro completo"""
|
|
dark_style = """
|
|
QMainWindow {
|
|
background-color: #1a1a1a;
|
|
color: #d4d4d4;
|
|
}
|
|
QPlainTextEdit, QTextEdit {
|
|
background-color: #1e1e1e;
|
|
color: #d4d4d4;
|
|
border: 1px solid #3c3c3c;
|
|
font-family: 'Consolas', 'Courier New', monospace;
|
|
font-size: 11px;
|
|
padding: 5px;
|
|
selection-background-color: #264f78;
|
|
}
|
|
QSplitter {
|
|
background-color: #1a1a1a;
|
|
}
|
|
QSplitter::handle {
|
|
background-color: #3c3c3c;
|
|
width: 2px;
|
|
height: 2px;
|
|
}
|
|
QSplitter::handle:hover {
|
|
background-color: #4fc3f7;
|
|
}
|
|
QMenuBar {
|
|
background-color: #2d2d30;
|
|
color: #cccccc;
|
|
border-bottom: 1px solid #3c3c3c;
|
|
}
|
|
QMenuBar::item {
|
|
background-color: transparent;
|
|
padding: 4px 8px;
|
|
}
|
|
QMenuBar::item:selected {
|
|
background-color: #4fc3f7;
|
|
color: white;
|
|
}
|
|
QMenu {
|
|
background-color: #2d2d30;
|
|
color: #cccccc;
|
|
border: 1px solid #3c3c3c;
|
|
}
|
|
QMenu::item {
|
|
padding: 4px 20px;
|
|
}
|
|
QMenu::item:selected {
|
|
background-color: #4fc3f7;
|
|
color: white;
|
|
}
|
|
QStatusBar {
|
|
background-color: #007acc;
|
|
color: white;
|
|
border-top: 1px solid #3c3c3c;
|
|
}
|
|
"""
|
|
self.setStyleSheet(dark_style)
|
|
|
|
def _on_input_changed(self):
|
|
"""Maneja cambios en el texto de entrada"""
|
|
# Delegar al autocomplete manager
|
|
self.autocomplete_manager.on_input_changed()
|
|
|
|
# Programar evaluación con debounce
|
|
self.evaluation_manager.schedule_evaluation()
|
|
|
|
def _evaluate_and_update(self):
|
|
"""Evalúa y actualiza la salida - método requerido por EvaluationManager"""
|
|
self.evaluation_manager.evaluate_and_update()
|
|
|
|
def _handle_key_press(self, event) -> bool:
|
|
"""Maneja eventos de teclado para autocompletado"""
|
|
return self.autocomplete_manager.handle_key_press(event)
|
|
|
|
def _handle_output_link_click(self, link_id: str, result_object):
|
|
"""Maneja clicks en links del output"""
|
|
# Delegar al evaluation manager
|
|
self.evaluation_manager.handle_output_link_click(link_id, result_object)
|
|
|
|
def _toggle_latex_panel(self):
|
|
"""Togglea la visibilidad del panel LaTeX"""
|
|
main_layout = self.centralWidget().layout()
|
|
|
|
if self.latex_panel_visible:
|
|
# Ocultar panel
|
|
main_layout.removeWidget(self.latex_panel)
|
|
self.latex_panel.hide()
|
|
self.latex_button.setChecked(False)
|
|
self.latex_panel_visible = False
|
|
else:
|
|
# Mostrar panel
|
|
main_layout.addWidget(self.latex_panel)
|
|
self.latex_panel.show()
|
|
self.latex_button.setChecked(True)
|
|
self.latex_panel_visible = True
|
|
|
|
# Actualizar panel con ecuaciones pendientes
|
|
self._sync_latex_equations_on_show()
|
|
|
|
# Guardar configuración
|
|
self.settings_manager.set_setting("latex_panel_visible", self.latex_panel_visible)
|
|
|
|
def _sync_latex_equations_on_show(self):
|
|
"""Sincroniza las ecuaciones LaTeX cuando se muestra el panel"""
|
|
try:
|
|
# Limpiar panel
|
|
self.latex_panel.clear_equations()
|
|
|
|
# Añadir todas las ecuaciones pendientes
|
|
for eq in self.evaluation_manager.get_latex_equations():
|
|
self.latex_panel.add_equation(eq['type'], eq['content'])
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error sincronizando ecuaciones LaTeX: {e}")
|
|
|
|
def _update_status(self, message: str, timeout: int = 0):
|
|
"""Actualiza la barra de estado"""
|
|
self.status_bar.showMessage(message, timeout)
|
|
|
|
def closeEvent(self, event):
|
|
"""Maneja el cierre de la aplicación"""
|
|
# Delegar al settings manager
|
|
self.settings_manager.save_all()
|
|
event.accept()
|
|
|
|
def _setup_application_icon(self):
|
|
"""Configura el icono de la aplicación"""
|
|
# Buscar el icono en el directorio raíz del proyecto
|
|
icon_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), ".res", "icon.png")
|
|
if os.path.exists(icon_path):
|
|
self.setWindowIcon(QIcon(icon_path))
|
|
self.logger.debug(f"✅ Icono cargado desde: {icon_path}")
|
|
else:
|
|
self.logger.warning(f"⚠️ Icono no encontrado en: {icon_path}")
|
|
|
|
|
|
def main():
|
|
"""Función principal para ejecutar la aplicación"""
|
|
app = QApplication(sys.argv)
|
|
|
|
# Configurar la aplicación
|
|
app.setApplicationName("Calculadora MAV")
|
|
app.setApplicationVersion("3.0")
|
|
app.setOrganizationName("MAV")
|
|
|
|
# Crear y mostrar la ventana principal
|
|
calculator = HybridCalculatorPySide6()
|
|
calculator.show()
|
|
|
|
# Ejecutar el loop principal
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |