""" Sistema de Menús y Diálogos para la Calculadora MAV CAS Híbrida """ import os import time from pathlib import Path from PySide6.QtWidgets import QMenuBar, QMenu, QMessageBox, QFileDialog from PySide6.QtGui import QAction, QKeySequence from PySide6.QtCore import QTimer from PySide6.QtWidgets import QApplication class MenuManager: """Gestor del sistema de menús""" def __init__(self, main_window): self.main_window = main_window self.logger = main_window.logger def setup_menu(self): """Configura el menú completo""" menubar = self.main_window.menuBar() # Menú Archivo file_menu = menubar.addMenu("Archivo") new_action = QAction("Nuevo", self.main_window) new_action.setShortcut(QKeySequence.New) new_action.triggered.connect(self.new_session) file_menu.addAction(new_action) file_menu.addSeparator() load_action = QAction("Cargar...", self.main_window) load_action.setShortcut(QKeySequence.Open) load_action.triggered.connect(self.load_file) file_menu.addAction(load_action) save_action = QAction("Guardar como...", self.main_window) save_action.setShortcut(QKeySequence.Save) save_action.triggered.connect(self.save_file) file_menu.addAction(save_action) # Menú de archivos recientes self.recent_files_menu = file_menu.addMenu("Archivos recientes") self._update_recent_files_menu() file_menu.addSeparator() exit_action = QAction("Salir", self.main_window) exit_action.triggered.connect(self.main_window.close) file_menu.addAction(exit_action) # Menú Editar edit_menu = menubar.addMenu("Editar") clear_input_action = QAction("Limpiar entrada", self.main_window) clear_input_action.triggered.connect(self.clear_input) edit_menu.addAction(clear_input_action) clear_output_action = QAction("Limpiar salida", self.main_window) clear_output_action.triggered.connect(self.clear_output) edit_menu.addAction(clear_output_action) edit_menu.addSeparator() clear_history_action = QAction("Limpiar historial", self.main_window) clear_history_action.triggered.connect(self.clear_history) edit_menu.addAction(clear_history_action) # Menú Ver view_menu = menubar.addMenu("Ver") toggle_latex_action = QAction("📐 Panel LaTeX", self.main_window) toggle_latex_action.setShortcut(QKeySequence("F12")) toggle_latex_action.triggered.connect(self.main_window._toggle_latex_panel) view_menu.addAction(toggle_latex_action) view_menu.addSeparator() # Submenu de fuentes font_menu = view_menu.addMenu("Tamaño de fuente") font_small_action = QAction("Pequeña (11px)", self.main_window) font_small_action.setCheckable(True) font_small_action.triggered.connect(lambda: self.set_font_size(False)) font_menu.addAction(font_small_action) font_large_action = QAction("Grande (14px)", self.main_window) font_large_action.setCheckable(True) font_large_action.triggered.connect(lambda: self.set_font_size(True)) font_menu.addAction(font_large_action) # Marcar el tamaño actual is_large = self.main_window.settings_manager.get_setting( "font_size_large", False ) if is_large: font_large_action.setChecked(True) else: font_small_action.setChecked(True) self.font_small_action = font_small_action self.font_large_action = font_large_action view_menu.addSeparator() system_info_action = QAction("Información del sistema", self.main_window) system_info_action.triggered.connect(self.show_types_info) view_menu.addAction(system_info_action) # Menú Herramientas tools_menu = menubar.addMenu("Herramientas") reload_types_action = QAction("Recargar Tipos Personalizados", self.main_window) reload_types_action.triggered.connect(self.reload_types) tools_menu.addAction(reload_types_action) tools_menu.addSeparator() # Menú de diagnóstico diag_menu = tools_menu.addMenu("Diagnóstico") mathjax_diag_action = QAction("🔍 Diagnóstico MathJax", self.main_window) mathjax_diag_action.triggered.connect(self._diagnose_mathjax) diag_menu.addAction(mathjax_diag_action) latex_status_action = QAction("📊 Estado Panel LaTeX", self.main_window) latex_status_action.triggered.connect(self._show_latex_panel_status) diag_menu.addAction(latex_status_action) save_html_action = QAction("💾 Guardar HTML del Panel LaTeX", self.main_window) save_html_action.triggered.connect(self._save_latex_html) diag_menu.addAction(save_html_action) diag_menu.addSeparator() copy_debug_action = QAction("📋 Copiar Debug al Portapapeles", self.main_window) copy_debug_action.setShortcut(QKeySequence("Ctrl+Shift+C")) copy_debug_action.triggered.connect(self._copy_debug_to_clipboard) diag_menu.addAction(copy_debug_action) # Menú Tipos types_menu = menubar.addMenu("Tipos") types_info_action = QAction("Información de tipos", self.main_window) types_info_action.triggered.connect(self.show_types_info) types_menu.addAction(types_info_action) types_menu.addSeparator() types_syntax_action = QAction("Sintaxis de tipos", self.main_window) types_syntax_action.triggered.connect(self.show_types_syntax) types_menu.addAction(types_syntax_action) # Menú Ayuda help_menu = menubar.addMenu("Ayuda") quick_guide_action = QAction("Guía rápida", self.main_window) quick_guide_action.triggered.connect(self.show_quick_guide) help_menu.addAction(quick_guide_action) syntax_help_action = QAction("Sintaxis", self.main_window) syntax_help_action.triggered.connect(self.show_syntax_help) help_menu.addAction(syntax_help_action) sympy_funcs_action = QAction("Funciones SymPy", self.main_window) sympy_funcs_action.triggered.connect(self.show_sympy_functions) help_menu.addAction(sympy_funcs_action) help_menu.addSeparator() about_action = QAction("Acerca de", self.main_window) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) # ========== FUNCIONES DE MENÚ ARCHIVO ========== def new_session(self): """Inicia una nueva sesión""" self.clear_input() self.clear_output() self.main_window.latex_panel.clear_equations() if hasattr(self.main_window, "_latex_equations"): self.main_window._latex_equations.clear() self.main_window._update_status("✨ Nueva sesión iniciada") def load_file(self): """Carga archivo en el editor""" filepath, _ = QFileDialog.getOpenFileName( self.main_window, "Cargar archivo", "", "Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)", ) if filepath: self._load_file_content(filepath) def _load_file_content(self, filepath: str): """Carga el contenido de un archivo en el editor.""" try: with open(filepath, "r", encoding="utf-8") as f: content = f.read() self.main_window.input_text.setPlainText(content) self.main_window._evaluate_and_update() self.main_window._update_status( f"📁 Archivo cargado: {Path(filepath).name}" ) # Añadir a recientes y actualizar menú self.main_window.settings_manager.add_recent_file(filepath) self._update_recent_files_menu() except Exception as e: QMessageBox.critical( self.main_window, "Error", f"No se pudo cargar el archivo:\n{e}" ) def save_file(self): """Guarda contenido del editor""" filepath, _ = QFileDialog.getSaveFileName( self.main_window, "Guardar archivo", "", "Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)", ) if filepath: try: content = self.main_window.input_text.toPlainText() with open(filepath, "w", encoding="utf-8") as f: f.write(content) self.main_window._update_status( f"💾 Archivo guardado: {Path(filepath).name}" ) # Añadir a recientes y actualizar menú self.main_window.settings_manager.add_recent_file(filepath) self._update_recent_files_menu() except Exception as e: QMessageBox.critical( self.main_window, "Error", f"No se pudo guardar el archivo:\n{e}" ) def _update_recent_files_menu(self): """Actualiza el menú de archivos recientes""" self.recent_files_menu.clear() recent_files = self.main_window.settings_manager.get_setting("recent_files", []) if not recent_files: empty_action = QAction("(Vacío)", self.main_window) empty_action.setEnabled(False) self.recent_files_menu.addAction(empty_action) return for filepath in recent_files: action = QAction(filepath, self.main_window) action.triggered.connect( lambda checked=False, path=filepath: self._load_file_content(path) ) self.recent_files_menu.addAction(action) # ========== FUNCIONES DE MENÚ EDITAR ========== def clear_input(self): """Limpia panel de entrada""" self.main_window.input_text.clear() self.main_window._clear_output() def clear_output(self): """Limpia panel de salida y LaTeX""" self.main_window._clear_output() self.main_window.latex_panel.clear_equations() if hasattr(self.main_window, "_latex_equations"): self.main_window._latex_equations.clear() def clear_history(self): """Limpia el archivo de historial""" try: if os.path.exists(self.main_window.HISTORY_FILE): os.remove(self.main_window.HISTORY_FILE) self.main_window._update_status("✓ Historial limpiado") except Exception as e: QMessageBox.critical( self.main_window, "Error", f"No se pudo limpiar el historial:\n{e}" ) def set_font_size(self, is_large: bool): """Cambia el tamaño de fuente de los paneles""" # Guardar configuración self.main_window.settings_manager.set_setting("font_size_large", is_large) # Actualizar checkboxes self.font_small_action.setChecked(not is_large) self.font_large_action.setChecked(is_large) # Aplicar nuevo tamaño font_size = 14 if is_large else 11 self._apply_font_size_to_panels(font_size) status_text = f"🔤 Fuente cambiada a {'grande' if is_large else 'pequeña'} ({font_size}px)" self.main_window._update_status(status_text) def _apply_font_size_to_panels(self, font_size: int): """Aplica el tamaño de fuente a los paneles de texto""" style_update = f""" QPlainTextEdit, QTextEdit {{ font-size: {font_size}px; }} """ # Actualizar estilo de los paneles principales current_style = self.main_window.styleSheet() # Reemplazar solo la parte de font-size en QPlainTextEdit y QTextEdit import re # Patrón para encontrar y reemplazar font-size en QPlainTextEdit y QTextEdit pattern = r"(QPlainTextEdit, QTextEdit\s*\{[^}]*?)font-size:\s*\d+px;([^}]*\})" replacement = rf"\1font-size: {font_size}px;\2" new_style = re.sub(pattern, replacement, current_style) # Si no se encontró el patrón, agregar el estilo if new_style == current_style: new_style += style_update self.main_window.setStyleSheet(new_style) # ========== FUNCIONES DE MENÚ HERRAMIENTAS ========== def reload_types(self): """Recarga el sistema de tipos""" try: self.logger.info("Recargando sistema de tipos...") self.main_window._setup_dynamic_helpers() self.main_window._evaluate_and_update() self.main_window._update_status("✓ Sistema de tipos recargado") except Exception as e: self.logger.error(f"Error recargando tipos: {e}") QMessageBox.critical( self.main_window, "Error", f"Error recargando tipos:\n{e}" ) def show_types_info(self): """Muestra información sobre tipos disponibles""" try: context_info = self.main_window.engine.get_context_info() info_text = f"""INFORMACIÓN DEL SISTEMA ALGEBRAICO PURO Ecuaciones en el sistema: {context_info.get('equations', 0)} Variables definidas: {context_info.get('variables', 0)} Variables activas: {', '.join(context_info.get('variable_names', []))} CARACTERÍSTICAS: • Sistema de ecuaciones puras con SymPy • Todas las asignaciones son ecuaciones • Resolución automática de sistemas • Evaluación numérica inteligente • Atajo x=? equivale a solve(x) """ self._show_info_dialog("Información del Sistema", info_text) except Exception as e: QMessageBox.critical( self.main_window, "Error", f"Error obteniendo información:\n{e}" ) def show_types_syntax(self): """Muestra sintaxis de tipos disponibles""" try: types_info = self.main_window.engine.get_available_types() syntax_text = "SINTAXIS DE TIPOS DISPONIBLES\n\n" # Aquí iría el código para mostrar sintaxis # Similar al original pero adaptado para PySide6 self._show_info_dialog("Sintaxis de Tipos", syntax_text) except Exception as e: QMessageBox.critical( self.main_window, "Error", f"Error obteniendo sintaxis:\n{e}" ) # ========== FUNCIONES DE DIAGNÓSTICO ========== def _diagnose_mathjax(self): """Ejecuta diagnóstico de MathJax""" if ( not hasattr(self.main_window.latex_panel, "_webview_available") or not self.main_window.latex_panel._webview_available ): QMessageBox.warning( self.main_window, "Diagnóstico", "Panel LaTeX no usa WebEngine (usando fallback)", ) return # Aquí iría el código de diagnóstico # Por ahora solo mostrar estado status = ( "WebEngine disponible" if self.main_window.latex_panel._webview_available else "Usando fallback HTML" ) equations = ( len(self.main_window._latex_equations) if hasattr(self.main_window, "_latex_equations") else 0 ) info = f"""DIAGNÓSTICO MATHJAX Estado: {status} Ecuaciones en memoria: {equations} Panel visible: {self.main_window.latex_panel_visible} Para depuración completa, revise la consola del navegador en el WebEngineView. """ self._show_info_dialog("Diagnóstico MathJax", info) def _show_latex_panel_status(self): """Muestra estado del panel LaTeX""" panel_exists = hasattr(self.main_window, "latex_panel") panel_visible = self.main_window.latex_panel_visible if panel_exists else False webview_available = ( self.main_window.latex_panel._webview_available if panel_exists else False ) equations_count = ( len(self.main_window._latex_equations) if hasattr(self.main_window, "_latex_equations") else 0 ) status_message = f"""ESTADO DEL PANEL LATEX COMPONENTES: • Panel creado: {'✓' if panel_exists else '✗'} • Panel visible: {'✓' if panel_visible else '✗'} • WebEngine disponible: {'✓' if webview_available else '✗'} CONTENIDO: • Ecuaciones en memoria: {equations_count} PARA SOLUCIONAR: 1. Si las ecuaciones están en memoria pero no se ven: → Cerrar y reabrir el panel LaTeX 2. Si WebEngine no está disponible: → Instalar con: pip install PySide6-WebEngine """ self._show_info_dialog("Estado Panel LaTeX", status_message) def _save_latex_html(self): """Guarda el HTML del panel LaTeX para diagnóstico""" try: # Obtener HTML del panel LaTeX latex_panel = self.main_window.latex_panel if ( hasattr(latex_panel, "_webview_available") and latex_panel._webview_available ): # Panel con WebEngine if hasattr(latex_panel, "webview") and latex_panel._webview_initialized: # Obtener HTML actual del WebView def on_html_received(html_content): self._save_html_to_file(html_content, "webview_current") latex_panel.webview.page().toHtml(on_html_received) # También guardar el HTML base generado base_html = latex_panel._generate_mathjax_html() self._save_html_to_file(base_html, "webview_base") else: # WebEngine no inicializado, solo HTML base base_html = latex_panel._generate_mathjax_html() self._save_html_to_file(base_html, "webview_not_initialized") else: # Panel con fallback text browser if hasattr(latex_panel, "text_browser"): fallback_html = latex_panel.text_browser.toHtml() self._save_html_to_file(fallback_html, "fallback_textbrowser") else: self._save_html_to_file( "No hay contenido HTML disponible", "no_content" ) QMessageBox.information( self.main_window, "HTML Guardado", "El HTML del panel LaTeX ha sido guardado en ./debug_html/ para diagnóstico.", ) except Exception as e: self.logger.error(f"Error guardando HTML del panel LaTeX: {e}") QMessageBox.critical( self.main_window, "Error", f"Error guardando HTML: {e}" ) def _save_html_to_file(self, html_content, file_suffix): """Guarda contenido HTML a un archivo""" # Crear directorio de debug si no existe debug_dir = Path("./debug_html") debug_dir.mkdir(exist_ok=True) # Generar nombre de archivo con timestamp timestamp = time.strftime("%Y%m%d_%H%M%S") filename = f"latex_panel_{file_suffix}_{timestamp}.html" filepath = debug_dir / filename try: with open(filepath, "w", encoding="utf-8") as f: f.write(html_content) self.logger.info(f"HTML guardado en: {filepath}") except Exception as e: self.logger.error(f"Error escribiendo archivo HTML: {e}") def _copy_debug_to_clipboard(self): """Copia información de debug completa al portapapeles""" try: # Obtener contenido de entrada input_content = self.main_window.input_text.toPlainText() # Obtener contenido de salida (texto plano) output_content = self.main_window.output_text.toPlainText() # Obtener información del sistema context_info = self.main_window.engine.get_context_info() # Obtener ecuaciones LaTeX si están disponibles latex_equations = "" if ( hasattr(self.main_window, "_latex_equations") and self.main_window._latex_equations ): latex_equations = "\\n".join( [ f"[{eq['type']}] {eq['content']}" for eq in self.main_window._latex_equations ] ) # Crear reporte de debug completo debug_report = f"""=== REPORTE DEBUG CALCULADORA MAV === Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S')} === ENTRADA === {input_content} === SALIDA === {output_content} === INFORMACIÓN DEL SISTEMA === Ecuaciones en sistema: {context_info.get('equations', 0)} Variables definidas: {context_info.get('variables', 0)} Variables activas: {', '.join(context_info.get('variable_names', []))} === PANEL LATEX === Ecuaciones LaTeX: {len(self.main_window._latex_equations) if hasattr(self.main_window, '_latex_equations') else 0} {latex_equations} === CONFIGURACIÓN === WebEngine disponible: {self.main_window.latex_panel._webview_available} MathJax listo: {getattr(self.main_window.latex_panel, '_mathjax_ready', False)} Panel LaTeX visible: {self.main_window.latex_panel_visible} === FIN REPORTE ===""" # Copiar al portapapeles clipboard = QApplication.clipboard() clipboard.setText(debug_report) # Mostrar confirmación self.main_window._update_status( "📋 Información de debug copiada al portapapeles", 3000 ) except Exception as e: self.logger.error(f"Error copiando debug: {e}") QMessageBox.critical( self.main_window, "Error", f"Error copiando debug al portapapeles:\\n{e}", ) # ========== FUNCIONES DE MENÚ AYUDA ========== def show_quick_guide(self): """Muestra guía rápida""" guide = """# Calculadora MAV - CAS Híbrido ## Sistema de Tipos Dinámico El sistema detecta automáticamente tipos disponibles en custom_types/ ## Sintaxis Nueva con Corchetes - Sintaxis: Tipo[valor] en lugar de Tipo("valor") - Ejemplos: Hex[FF], Bin[1010], Dec[10.5], Chr[A] - Use menú Tipos → Información de tipos para ver tipos disponibles ## Ecuaciones Automáticas - x**2 + 2*x = 8 (detectado automáticamente) - a + b = 10 (agregado al sistema) - variable=? (atajo para solve(variable)) ## Funciones SymPy Disponibles - solve(), diff(), integrate(), limit(), series() - sin(), cos(), tan(), exp(), log(), sqrt() - Matrix(), plot(), plot3d() ## Resultados Interactivos - 📊 Ver Plot (click para ventana matplotlib) - 📋 Ver Matriz (click para vista expandida) - 📋 Ver Lista (click para contenido completo) ## Variables Automáticas - Todas las variables son símbolos SymPy - x = 5 crea Symbol('x') con valor 5 - Evaluación simbólica + numérica automática ## Autocompletado Dinámico - Escriba "." después de cualquier objeto para ver métodos - El sistema usa los tipos registrados automáticamente """ self._show_info_dialog("Guía Rápida", guide) def show_syntax_help(self): """Muestra ayuda de sintaxis""" syntax = """# Sintaxis del CAS Híbrido ## Sistema de Tipos Dinámico Los tipos se detectan automáticamente desde custom_types/ Use menú Tipos → Información de tipos para ver tipos disponibles ## Sintaxis con Corchetes (Dinámica) Tipo[valor] # Sintaxis general Tipo[arg1; arg2] # Múltiples argumentos ## Métodos Disponibles (Dinámicos) Tipo[...].método() # Métodos específicos del tipo objeto.método[] # Método sin argumentos ## Ecuaciones (detección automática) expresión = expresión # Ecuación simple expresión == expresión # Igualdad SymPy expresión > expresión # Desigualdad SymPy ## Resolver solve(ecuación, variable) variable=? # Atajo para solve(variable) ## Variables SymPy Puras x = valor # Crea Symbol('x') expresión # Evaluación simbólica automática """ self._show_info_dialog("Sintaxis", syntax) def show_sympy_functions(self): """Muestra funciones SymPy disponibles""" functions = """# Funciones SymPy Disponibles ## Matemáticas Básicas sin(x), cos(x), tan(x) asin(x), acos(x), atan(x) sinh(x), cosh(x), tanh(x) exp(x), log(x), sqrt(x) abs(x), sign(x), factorial(x) ## Cálculo diff(expr, var) # Derivada integrate(expr, var) # Integral indefinida integrate(expr, (var, a, b)) # Integral definida limit(expr, var, punto) # Límite series(expr, var, punto, n) # Serie de Taylor ## Álgebra solve(ecuación, variable) simplify(expr), expand(expr) factor(expr), collect(expr, var) cancel(expr), apart(expr, var) ## Álgebra Lineal Matrix([[a, b], [c, d]]) det(matrix), inv(matrix) ## Plotting plot(expr, (var, inicio, fin)) plot3d(expr, (x, x1, x2), (y, y1, y2)) ## Constantes pi, E, I (imaginario), oo (infinito) """ self._show_info_dialog("Funciones SymPy", functions) def show_about(self): """Muestra información sobre la aplicación""" about = """Calculadora MAV - CAS Híbrido Versión: 2.1 PySide6 (Sistema de Tipos Dinámico) Motor: SymPy + Auto-descubrimiento de Tipos Características: • Motor algebraico completo (SymPy) • Sistema de tipos dinámico y extensible • Sintaxis simplificada con corchetes • Detección automática de ecuaciones • Resultados interactivos clickeables • Auto-descubrimiento de tipos en custom_types/ • Variables SymPy puras • Plotting integrado • Autocompletado dinámico Desarrollado para cálculo matemático avanzado con soporte especializado para redes, programación y análisis numérico. """ QMessageBox.about(self.main_window, "Acerca de", about) def _show_info_dialog(self, title: str, content: str): """Muestra diálogo de información con scroll""" dialog = QMessageBox(self.main_window) dialog.setWindowTitle(title) dialog.setIcon(QMessageBox.Information) dialog.setText(content[:200] + "..." if len(content) > 200 else content) dialog.setDetailedText(content) dialog.exec()