Calc/app/gui_main.py

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()