""" Calculadora MAV CAS Híbrida - Aplicación principal PySide6 VERSIÓN COMPLETA: Preserva TODA la funcionalidad de la versión tkinter """ import sys import json import logging import os import re import time from pathlib import Path from typing import List, Dict, Any, Optional, Tuple from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QPlainTextEdit, QSplitter, QPushButton, QLabel, QFrame, QMenuBar, QMenu, QStatusBar, QMessageBox, QFileDialog, QListWidget, QListWidgetItem, QTextBrowser, QScrollArea, QDockWidget, QToolBar ) from PySide6.QtCore import ( Qt, QTimer, Signal, QUrl, QSize, QRect, QPoint, Slot, QPropertyAnimation, QEasingCurve, QEvent ) from PySide6.QtGui import ( QFont, QTextCursor, QTextCharFormat, QColor, QIcon, QSyntaxHighlighter, QTextDocument, QKeySequence, QShortcut, QFontMetrics, QPalette, QTextOption, QAction ) 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_pyside6 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 InputTextEdit(QPlainTextEdit): """Editor de texto personalizado con eventos mejorados""" def __init__(self, parent=None): super().__init__(parent) self.parent_app = parent self.setLineWrapMode(QPlainTextEdit.NoWrap) self.setFont(QFont("Consolas", 11)) def keyPressEvent(self, event): """Override para manejar autocompletado""" if hasattr(self.parent_app, '_handle_key_press'): # Dejar que el parent maneje primero para autocompletado if self.parent_app._handle_key_press(event): return super().keyPressEvent(event) class OutputTextEdit(QTextEdit): """Editor de salida con soporte para links clickeables""" link_clicked = Signal(str, object) # link_id, result_object def __init__(self, parent=None): super().__init__(parent) self.setReadOnly(True) self.setFont(QFont("Consolas", 11)) self.clickable_links = {} # {(start, end): (link_id, object)} def mousePressEvent(self, event): """Detecta clicks en links""" if event.button() == Qt.LeftButton: cursor = self.cursorForPosition(event.pos()) pos = cursor.position() # Buscar si el click fue en un link for (start, end), (link_id, obj) in self.clickable_links.items(): if start <= pos <= end: self.link_clicked.emit(link_id, obj) return super().mousePressEvent(event) class ExpandableLatexButton(QPushButton): """Botón expandible para mostrar/ocultar panel LaTeX""" def __init__(self, parent=None): super().__init__("📐", parent) self.setFixedWidth(25) self.setToolTip("Mostrar/ocultar panel LaTeX (F12)") self.setStyleSheet(""" QPushButton { background-color: #3c3c3c; color: #80c7f7; border: none; font-size: 14px; padding: 5px; } QPushButton:hover { background-color: #4fc3f7; color: white; } QPushButton:checked { background-color: #4fc3f7; color: white; } """) self.setCheckable(True) class LatexPanel(QWidget): """Panel LaTeX con WebEngine o fallback a HTML""" def __init__(self, parent=None): super().__init__(parent) self.equations = [] self._webview_available = False self._mathjax_ready = False self._pending_equations = [] self._parent_calculator = parent self._setup_ui() # Timer para verificar si MathJax está listo self._mathjax_check_timer = QTimer() self._mathjax_check_timer.timeout.connect(self._check_mathjax_ready) self._mathjax_check_timer.start(500) # Verificar cada 500ms def _setup_ui(self): """Configura la UI del panel""" layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) # Header header = QFrame() header.setFixedHeight(30) header.setStyleSheet("background-color: #1a1a1a; border-bottom: 1px solid #3c3c3c;") header_layout = QHBoxLayout(header) header_layout.setContentsMargins(10, 0, 10, 0) title = QLabel("📐 Ecuaciones LaTeX") title.setStyleSheet("color: #80c7f7; font-weight: bold;") header_layout.addWidget(title) header_layout.addStretch() layout.addWidget(header) # Intentar crear WebEngineView try: self.webview = QWebEngineView() self.webview.setContextMenuPolicy(Qt.NoContextMenu) self._setup_webview() layout.addWidget(self.webview) self._webview_available = True logging.debug("✅ WebEngineView disponible para LaTeX") except Exception as e: logging.warning(f"⚠️ WebEngineView no disponible: {e}") # Fallback a QTextBrowser self.text_browser = QTextBrowser() self.text_browser.setOpenExternalLinks(False) self._setup_text_browser() layout.addWidget(self.text_browser) self._webview_available = False def _setup_webview(self): """Configura WebEngineView con MathJax""" html_content = self._generate_mathjax_html() self.webview.setHtml(html_content) def _setup_text_browser(self): """Configura el browser de texto como fallback""" self.text_browser.setStyleSheet(""" QTextBrowser { background-color: #1a1a1a; color: #d4d4d4; border: none; font-family: 'Consolas'; font-size: 11px; padding: 10px; } """) self.text_browser.setHtml("""
📐 Panel de Ecuaciones
Las ecuaciones aparecerán aquí
""") def _generate_mathjax_html(self): """Genera HTML base con MathJax""" return """
Panel de Ecuaciones LaTeX
""" def _check_mathjax_ready(self): """Verifica si MathJax está listo y renderiza ecuaciones pendientes""" if not self._webview_available: return # Verificar si MathJax está listo self.webview.page().runJavaScript( "window.mathJaxReady || false;", self._on_mathjax_ready_check ) def _on_mathjax_ready_check(self, ready): """Callback cuando se verifica el estado de MathJax""" if ready and not self._mathjax_ready: self._mathjax_ready = True self._mathjax_check_timer.stop() logging.debug("✅ MathJax listo, procesando ecuaciones pendientes") # Renderizar ecuaciones pendientes for eq in self._pending_equations: self._add_equation_to_webview(eq['type'], eq['content']) self._pending_equations.clear() # Trigger initial render si hay un calculador padre if self._parent_calculator and hasattr(self._parent_calculator, '_trigger_initial_latex_render'): self._parent_calculator._trigger_initial_latex_render() def _add_equation_to_webview(self, eq_type: str, content: str): """Añade una ecuación directamente al webview""" if self._webview_available: escaped_content = content.replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"') js_code = f"addEquation('{eq_type}', '{escaped_content}');" self.webview.page().runJavaScript(js_code) def add_equation(self, eq_type: str, content: str): """Añade una ecuación al panel""" self.equations.append({'type': eq_type, 'content': content}) if self._webview_available: if self._mathjax_ready: # MathJax está listo, renderizar inmediatamente self._add_equation_to_webview(eq_type, content) else: # MathJax no está listo, guardar para después self._pending_equations.append({'type': eq_type, 'content': content}) else: # Actualizar HTML en text browser self._update_text_browser() def clear_equations(self): """Limpia todas las ecuaciones""" self.equations.clear() self._pending_equations.clear() if self._webview_available: self.webview.page().runJavaScript("clearEquations();") else: self._setup_text_browser() # Reset al estado inicial def _update_text_browser(self): """Actualiza el contenido del text browser (fallback)""" html_parts = [""" """] for eq in self.equations: eq_type = eq['type'] content = eq['content'] css_class = eq_type if eq_type == 'comment': html_parts.append(f'
{content}
') else: # Para ecuaciones matemáticas, mostrar en formato de código html_parts.append(f'
{content}
') self.text_browser.setHtml(''.join(html_parts)) class AutocompletePopup(QWidget): """Popup de autocompletado modeless""" item_selected = Signal(str) # Emite el texto seleccionado def __init__(self, parent=None): super().__init__(None, Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setAttribute(Qt.WA_ShowWithoutActivating, True) self.setFocusPolicy(Qt.NoFocus) # Lista de sugerencias self.listbox = QListWidget(self) self.listbox.setFocusPolicy(Qt.NoFocus) self.listbox.itemDoubleClicked.connect(self._on_item_double_clicked) # Layout layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.listbox) # Estilo self.setStyleSheet(""" QWidget { background-color: #3c3f41; border: 1px solid #4fc3f7; border-radius: 4px; } QListWidget { background-color: #3c3f41; color: #bbbbbb; border: none; font-family: 'Consolas'; font-size: 10px; outline: none; } QListWidget::item { padding: 3px 8px; border: none; } QListWidget::item:selected { background-color: #007acc; color: white; } QListWidget::item:hover { background-color: #094771; color: #e0e0e0; } """) self._suggestions = [] self._selected_index = 0 def set_suggestions(self, suggestions: List[Tuple[str, str]]): """Establece las sugerencias [(nombre, descripción), ...]""" self._suggestions = suggestions self.listbox.clear() for name, desc in suggestions: self.listbox.addItem(f"{name} — {desc}") if self.listbox.count() > 0: self.listbox.setCurrentRow(0) self._selected_index = 0 def navigate(self, direction: int): """Navega por las sugerencias (direction: -1=arriba, 1=abajo)""" if self.listbox.count() == 0: return new_index = (self._selected_index + direction) % self.listbox.count() self._selected_index = new_index self.listbox.setCurrentRow(new_index) def get_selected_text(self) -> str: """Obtiene el texto de la sugerencia seleccionada""" if 0 <= self._selected_index < len(self._suggestions): return self._suggestions[self._selected_index][0] return "" def _on_item_double_clicked(self, item): """Maneja doble click en un item""" text = item.text().split(" —")[0].strip() self.item_selected.emit(text) def adjust_size(self): """Ajusta el tamaño del popup según el contenido""" if self.listbox.count() == 0: return # Calcular tamaño necesario max_width = 300 for i in range(self.listbox.count()): item = self.listbox.item(i) width = self.listbox.fontMetrics().horizontalAdvance(item.text()) + 20 max_width = max(max_width, width) max_width = min(max_width, 600) height = min(self.listbox.count() * 20 + 10, 200) self.setFixedSize(max_width, height) class HybridCalculatorPySide6(QMainWindow): """Aplicación principal del CAS híbrido - VERSIÓN COMPLETA""" 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__) # Motor y managers self.engine = PureAlgebraicEngine() self.interactive_manager = None # Configuración self.settings = self._load_settings() self.debug = self.settings.get("debug_mode", False) # ========== VARIABLES DE AUTOCOMPLETADO (COMPLETO COMO EN TKINTER) ========== self._autocomplete_popup = None self._autocomplete_active = False self._autocomplete_suggestions = [] self._autocomplete_filter_text = "" self._autocomplete_trigger_pos = None self._popup_disabled_until_next_dot = False self._variable_popup_active = False self._last_navigation_time = 0 self._last_input_change = 0 self._current_suggestions = [] self._is_global_popup = False # Timers self._debounce_timer = QTimer() self._debounce_timer.setSingleShot(True) self._debounce_timer.timeout.connect(self._evaluate_and_update) self._variable_popup_timer = QTimer() self._variable_popup_timer.setSingleShot(True) self._variable_popup_timer.timeout.connect(self._show_variable_autocomplete_improved) # Estado del panel LaTeX self.latex_panel_visible = self.settings.get("latex_panel_visible", True) self._latex_equations = [] # Configurar helpers self._setup_dynamic_helpers() # Configurar UI self._setup_ui() self._setup_menu() self._setup_shortcuts() self._setup_interactive_manager() # Cargar historial y configuración self._load_history() self._restore_geometry() self.logger.info("✅ Calculadora MAV PySide6 (versión completa) 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: self.latex_panel.hide() # Configurar tags de salida self._setup_output_tags() # 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_output_tags(self): """Configura los formatos de texto para la salida""" self.output_formats = { 'error': self._create_format("#f44747", bold=True), 'comment': self._create_format("#6a9955", italic=True), 'assignment': self._create_format("#dcdcaa"), 'equation': self._create_format("#c586c0"), 'symbolic': self._create_format("#9cdcfe"), 'numeric': self._create_format("#b5cea8"), 'boolean': self._create_format("#569cd6"), 'string': self._create_format("#ce9178"), 'custom_type': self._create_format("#4ec9b0"), 'plot': self._create_format("#569cd6", underline=True), 'type_indicator': self._create_format("#808080"), 'clickable': self._create_format("#4fc3f7", underline=True), 'helper': self._create_format("#ffd700", italic=True) } def _create_format(self, color: str, bold: bool = False, italic: bool = False, underline: bool = False) -> QTextCharFormat: """Crea un formato de texto""" fmt = QTextCharFormat() fmt.setForeground(QColor(color)) if bold: fmt.setFontWeight(QFont.Bold) if italic: fmt.setFontItalic(True) if underline: fmt.setFontUnderline(True) return fmt def _setup_menu(self): """Configura el menú completo""" menubar = self.menuBar() # Menú Archivo file_menu = menubar.addMenu("Archivo") new_action = QAction("Nuevo", self) 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) load_action.setShortcut(QKeySequence.Open) load_action.triggered.connect(self.load_file) file_menu.addAction(load_action) save_action = QAction("Guardar como...", self) save_action.setShortcut(QKeySequence.Save) save_action.triggered.connect(self.save_file) file_menu.addAction(save_action) file_menu.addSeparator() exit_action = QAction("Salir", self) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # Menú Editar edit_menu = menubar.addMenu("Editar") clear_input_action = QAction("Limpiar entrada", self) clear_input_action.triggered.connect(self.clear_input) edit_menu.addAction(clear_input_action) clear_output_action = QAction("Limpiar salida", self) clear_output_action.triggered.connect(self.clear_output) edit_menu.addAction(clear_output_action) edit_menu.addSeparator() clear_history_action = QAction("Limpiar historial", self) 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) toggle_latex_action.setShortcut(QKeySequence("F12")) toggle_latex_action.triggered.connect(self._toggle_latex_panel) view_menu.addAction(toggle_latex_action) view_menu.addSeparator() system_info_action = QAction("Información del sistema", self) 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) 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) mathjax_diag_action.triggered.connect(self._diagnose_mathjax) diag_menu.addAction(mathjax_diag_action) latex_status_action = QAction("📊 Estado Panel LaTeX", self) latex_status_action.triggered.connect(self._show_latex_panel_status) diag_menu.addAction(latex_status_action) # Menú Tipos types_menu = menubar.addMenu("Tipos") types_info_action = QAction("Información de tipos", self) 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) 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) quick_guide_action.triggered.connect(self.show_quick_guide) help_menu.addAction(quick_guide_action) syntax_help_action = QAction("Sintaxis", self) syntax_help_action.triggered.connect(self.show_syntax_help) help_menu.addAction(syntax_help_action) sympy_funcs_action = QAction("Funciones SymPy", self) sympy_funcs_action.triggered.connect(self.show_sympy_functions) help_menu.addAction(sympy_funcs_action) help_menu.addSeparator() about_action = QAction("Acerca de", self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def _setup_shortcuts(self): """Configura atajos de teclado""" # Evaluación manual eval_shortcut = QShortcut(QKeySequence("Ctrl+Return"), self) eval_shortcut.activated.connect(self._evaluate_and_update) # Toggle LaTeX panel latex_shortcut = QShortcut(QKeySequence("F12"), self) latex_shortcut.activated.connect(self._toggle_latex_panel) # Nuevo new_shortcut = QShortcut(QKeySequence.New, self) new_shortcut.activated.connect(self.new_session) def _setup_interactive_manager(self): """Configura el gestor de resultados interactivos""" self.interactive_manager = InteractiveResultManager(self) self.interactive_manager.set_update_callback(self._update_input_expression) def _apply_dark_theme(self): """Aplica tema oscuro a la aplicación""" dark_theme = """ QMainWindow { background-color: #2b2b2b; color: #d4d4d4; } QPlainTextEdit, QTextEdit { background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #3c3c3c; selection-background-color: #264f78; } QMenuBar { background-color: #2d2d30; color: #d4d4d4; border-bottom: 1px solid #3c3c3c; } 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: #2b2b2b; color: #80c7f7; border-top: 1px solid #3c3c3c; } QSplitter::handle { background-color: #3c3c3c; width: 4px; } QSplitter::handle:hover { background-color: #007acc; } QScrollBar:vertical { background-color: #1e1e1e; width: 12px; border: none; } QScrollBar::handle:vertical { background-color: #3c3c3c; min-height: 20px; border-radius: 6px; } QScrollBar::handle:vertical:hover { background-color: #4c4c4c; } """ self.setStyleSheet(dark_theme) # ========== SISTEMA DE EVALUACIÓN ========== def _on_input_changed(self): """Maneja cambios en la entrada con debounce""" self._debounce_timer.stop() self._debounce_timer.start(300) # Cancelar popup de variables si existe self._variable_popup_timer.stop() # Programar autocompletado de variables if not self._autocomplete_active and not self._popup_disabled_until_next_dot: self._variable_popup_timer.start(800) def _evaluate_and_update(self): """Evalúa todas las líneas y actualiza la salida""" try: input_content = self.input_text.toPlainText() if not input_content.strip(): self._clear_output() return # Limpiar contexto del motor self.engine.equations.clear() self.engine.symbol_table.clear() self.engine.variables.clear() self.logger.debug("Contexto del motor limpiado") # Limpiar panel LaTeX if hasattr(self, '_latex_equations'): self._latex_equations.clear() self.latex_panel.clear_equations() lines = input_content.splitlines() self._evaluate_lines(lines) except Exception as e: self._show_error(f"Error durante evaluación: {e}") def _trigger_initial_latex_render(self): """Activa el renderizado inicial del panel LaTeX cuando MathJax está listo""" try: # Solo hacer evaluación inicial si hay contenido en el input input_content = self.input_text.toPlainText() if input_content.strip(): logging.debug("🎯 Activando renderizado inicial de LaTeX") self._evaluate_and_update() except Exception as e: logging.error(f"Error en renderizado inicial de LaTeX: {e}") def _evaluate_lines(self, lines: List[str]): """Evalúa múltiples líneas de código""" output_data = [] for line_num, line in enumerate(lines, 1): line_stripped = line.strip() # Líneas vacías o comentarios if not line_stripped or line_stripped.startswith('#'): if line_stripped: output_data.append([("comment", line_stripped)]) # Añadir comentario al panel LaTeX if line_stripped.startswith('#'): comment_text = line_stripped[1:].strip() self._add_to_latex_panel("comment", comment_text) else: output_data.append([("", "")]) continue # Evaluar línea result = self.engine.evaluate_line(line_stripped) line_output = self._process_evaluation_result(result) output_data.append(line_output) self._display_output(output_data) def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]: """Procesa el resultado de evaluación para display""" output_parts = [] indicator_text: Optional[str] = None # Añadir al panel LaTeX si es aplicable self._add_to_latex_panel_if_applicable(result) if result.result_type == "comment": output_parts.append(("comment", result.output if result.output is not None else "")) return output_parts if not result.success: # Manejo de errores con ayuda contextual error_msg = f"Error: {result.error_message}" # Intentar obtener ayuda ayuda_text = self._obtener_ayuda(result.input_line) if ayuda_text: ayuda_linea = ayuda_text.replace("\n", " ").strip() if len(ayuda_linea) > 120: ayuda_linea = ayuda_linea[:117] + "..." output_parts.append(("error", error_msg)) output_parts.append(("\n", "\n")) output_parts.append(("helper", f"Sugerencia: {ayuda_linea}")) else: output_parts.append(("error", error_msg)) else: # Intentar crear tag interactivo if self.interactive_manager: interactive_info = self.interactive_manager.create_interactive_link( result.actual_result_object ) if interactive_info: link_id, display_text, result_object = interactive_info output_parts.append(("clickable", display_text, link_id, result_object)) # Añadir indicador de tipo algebraico if result.algebraic_type: indicator_text = f"[{result.algebraic_type}]" output_parts.append((" ", " ")) output_parts.append(("type_indicator", indicator_text)) return output_parts # Si no es interactivo, usar formato normal main_output_tag = "base" if result.is_assignment: main_output_tag = "assignment" indicator_text = "[=]" elif result.is_equation: main_output_tag = "equation" indicator_text = "[eq]" elif result.result_type == "plot": main_output_tag = "plot" else: # Determinar tag según tipo algebraico if result.algebraic_type: type_lower = result.algebraic_type.lower() if isinstance(result.actual_result_object, sympy.Basic): main_output_tag = "symbolic" elif type_lower in ["int", "float", "complex"]: main_output_tag = "numeric" elif type_lower == "bool": main_output_tag = "boolean" elif type_lower == "str": main_output_tag = "string" else: main_output_tag = "custom_type" if result.algebraic_type: is_collection = any(kw in result.algebraic_type.lower() for kw in ["matrix", "list", "dict", "tuple", "vector", "array"]) if is_collection or isinstance(result.actual_result_object, sympy.Basic): indicator_text = f"[{result.algebraic_type}]" output_parts.append((main_output_tag, result.output if result.output is not None else "")) if indicator_text: output_parts.append((" ", " ")) output_parts.append(("type_indicator", indicator_text)) return output_parts def _add_to_latex_panel_if_applicable(self, result: EvaluationResult): """Agrega resultado al panel LaTeX si es aplicable""" try: should_add_to_latex = False equation_type = "comment" if result.result_type == "comment": should_add_to_latex = True equation_type = "comment" elif result.is_assignment: should_add_to_latex = True equation_type = "assignment" elif result.is_equation: should_add_to_latex = True equation_type = "equation" elif result.success and result.output: # Agregar si tiene contenido matemático math_indicators = ['=', '+', '-', '*', '/', '^', 'sqrt', 'sin', 'cos', 'tan', 'log', 'exp'] if any(indicator in result.output for indicator in math_indicators): should_add_to_latex = True equation_type = "symbolic" elif result.actual_result_object is not None and isinstance(result.actual_result_object, sympy.Basic): should_add_to_latex = True equation_type = "symbolic" if should_add_to_latex: # Generar contenido LaTeX que incluya toda la información del output latex_content = self._generate_complete_latex_content(result) self._add_to_latex_panel(equation_type, latex_content) except Exception as e: self.logger.error(f"Error procesando para panel LaTeX: {e}") def _generate_complete_latex_content(self, result: EvaluationResult) -> str: """Genera contenido LaTeX completo incluyendo toda la información del output""" try: # Para comentarios, usar el texto directamente if result.result_type == "comment": return result.output or "" latex_parts = [] # PARTE 1: Contenido principal (con LaTeX de SymPy si es posible) main_content = "" if result.actual_result_object is not None: try: # Intentar generar LaTeX de SymPy para el objeto matemático import sympy main_content = sympy.latex(result.actual_result_object) # Para asignaciones, necesitamos agregar el lado izquierdo if result.is_assignment and result.input_line: # Extraer la variable del lado izquierdo if '=' in result.input_line: left_side = result.input_line.split('=')[0].strip() # Limpiar posibles símbolos LaTeX del lado izquierdo left_side = left_side.replace('$$', '').strip() main_content = f"{left_side} = {main_content}" except Exception: # Si falla el LaTeX de SymPy, usar el output textual main_content = result.output or "" else: main_content = result.output or "" latex_parts.append(main_content) # PARTE 2: Aproximación numérica (si está disponible en el output) if result.output and "≈" in result.output: approx_parts = result.output.split("≈", 1) if len(approx_parts) == 2: approx_value = approx_parts[1].strip() # Extraer solo la parte numérica antes del indicador de tipo if ";" in approx_value: approx_value = approx_value.split(";")[0].strip() # Intentar convertir la aproximación a LaTeX si es una ecuación try: import sympy if "Eq(" in approx_value: # Es una ecuación, intentar parserarla para LaTeX approx_obj = eval(approx_value, {'Eq': sympy.Eq, 'sqrt': sympy.sqrt}) approx_latex = sympy.latex(approx_obj) latex_parts.append(f"\\approx {approx_latex}") else: # Es un valor numérico simple latex_parts.append(f"\\approx {approx_value}") except: # Si falla, usar la aproximación como texto latex_parts.append(f"\\approx {approx_value}") # PARTE 3: Indicador de tipo (si está en el output) if result.output and "[" in result.output and "]" in result.output: # Extraer el indicador de tipo (ej: [=], [Equality], etc.) parts = result.output.split("[") if len(parts) >= 2: type_part = "[" + parts[-1] # Tomar el último indicador if "]" in type_part: type_indicator = type_part.split("]")[0] + "]" latex_parts.append(f"\\quad \\text{{{type_indicator}}}") # Combinar todas las partes complete_latex = " ".join(latex_parts) # Limpiar caracteres problemáticos para MathJax complete_latex = complete_latex.replace("__", "_{").replace("**", "^") # Agregar llaves de cierre para subíndices import re complete_latex = re.sub(r'_\{(\w+)', r'_{\1}', complete_latex) return complete_latex except Exception as e: self.logger.error(f"Error generando LaTeX completo: {e}") # Fallback al output original return result.output or "" def _add_to_latex_panel(self, equation_type: str, latex_content: str): """Añade una ecuación al panel LaTeX""" if not hasattr(self, '_latex_equations'): self._latex_equations = [] self._latex_equations.append({ 'type': equation_type, 'content': latex_content }) self.latex_panel.add_equation(equation_type, latex_content) # Actualizar indicador visual self._update_latex_indicator() def _update_latex_indicator(self): """Actualiza el indicador visual de contenido LaTeX""" equation_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0 if equation_count > 0: self.latex_button.setToolTip(f"📐 Panel LaTeX ({equation_count} ecuaciones)") else: self.latex_button.setToolTip("📐 Panel LaTeX (sin ecuaciones)") def _display_output(self, output_data: List[List[tuple]]): """Muestra los datos de salida en el widget""" self.output_text.clear() self.output_text.clickable_links.clear() cursor = self.output_text.textCursor() for line_idx, line_parts in enumerate(output_data): if line_idx > 0: cursor.insertText("\n") if not line_parts or (len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == ""): continue for part_idx, part_data in enumerate(line_parts): if len(part_data) >= 4 and part_data[0] == "clickable": # Link clickeable _, display_text, link_id, result_object = part_data start_pos = cursor.position() cursor.insertText(display_text, self.output_formats.get('clickable')) end_pos = cursor.position() self.output_text.clickable_links[(start_pos, end_pos)] = (link_id, result_object) elif len(part_data) >= 2: tag, content = part_data[0], part_data[1] if not content: continue if part_idx > 0: prev_tag = line_parts[part_idx-1][0] if part_idx > 0 else None if tag not in ["type_indicator"] and prev_tag: cursor.insertText(" ; ") elif tag == "type_indicator" and prev_tag: cursor.insertText(" ") format_obj = self.output_formats.get(tag, None) if format_obj: cursor.insertText(str(content), format_obj) else: cursor.insertText(str(content)) def _clear_output(self): """Limpia el panel de salida""" self.output_text.clear() def _show_error(self, error_msg: str): """Muestra un error en el panel de salida""" self.output_text.clear() cursor = self.output_text.textCursor() cursor.insertText(error_msg, self.output_formats['error']) def _handle_output_link_click(self, link_id: str, result_object): """Maneja clicks en links del panel de salida""" if self.interactive_manager: self.interactive_manager.handle_interactive_click(result_object) def _update_input_expression(self, original_expression: str, new_expression: str): """Actualiza el panel de entrada reemplazando la expresión original""" try: current_content = self.input_text.toPlainText() if original_expression in current_content: updated_content = current_content.replace(original_expression, new_expression, 1) self.input_text.setPlainText(updated_content) self._evaluate_and_update() self.logger.info(f"Expresión actualizada: '{original_expression}' -> '{new_expression}'") else: # Si no se encuentra, agregar al final if current_content and not current_content.endswith('\n'): current_content += '\n' updated_content = current_content + new_expression self.input_text.setPlainText(updated_content) except Exception as e: self.logger.error(f"Error actualizando expresión: {e}") # ========== SISTEMA DE AUTOCOMPLETADO COMPLETO ========== def _handle_key_press(self, event) -> bool: """Maneja eventos de teclado para autocompletado - retorna True si manejó el evento""" # Navegación en popup if self._autocomplete_active or self._variable_popup_active: if event.key() == Qt.Key_Up: self._handle_arrow_key(-1) return True elif event.key() == Qt.Key_Down: self._handle_arrow_key(1) return True elif event.key() == Qt.Key_Tab: self._handle_tab_key() return True elif event.key() == Qt.Key_Escape: self._handle_escape_key() return True elif event.key() in [Qt.Key_Return, Qt.Key_Enter]: self._select_autocomplete() return True # Detectar backspace para cerrar popup si se borra el punto if event.key() == Qt.Key_Backspace and self._autocomplete_active: QTimer.singleShot(1, self._check_dot_removal) # Procesar autocompletado después de insertar carácter if event.text() and not event.modifiers() & Qt.ControlModifier: # Guardar datos del evento para evitar que se elimine el objeto event_text = event.text() event_key = event.key() QTimer.singleShot(10, lambda: self._on_key_release_deferred(event_text, event_key)) return False def _on_key_release_deferred(self, event_text: str, event_key: int): """Maneja eventos después de insertar carácter usando datos guardados""" # Cancelar timer de variables self._variable_popup_timer.stop() # Verificar si acabamos de navegar just_navigated = (time.time() - self._last_navigation_time) < 0.1 # Manejar autocompletado con punto if event_text == '.' and not self._popup_disabled_until_next_dot: if self._variable_popup_active: self._close_autocomplete_popup() self._handle_dot_autocomplete() # Filtrar autocompletado si está activo elif self._autocomplete_active and event_text and not just_navigated: self._filter_autocomplete() # Marcar tiempo del último cambio if event_text: self._last_input_change = time.time() # Programar autocompletado de variables if not self._autocomplete_active and not self._popup_disabled_until_next_dot: self._schedule_variable_autocomplete_improved() def _on_key_release(self, event): """Maneja eventos después de insertar carácter - mantenido para compatibilidad""" return self._on_key_release_deferred(event.text(), event.key()) def _handle_dot_autocomplete(self): """Maneja el autocompletado cuando se escribe un punto""" self._close_autocomplete_popup() cursor = self.input_text.textCursor() cursor_pos = cursor.position() # Obtener línea actual cursor.select(QTextCursor.LineUnderCursor) line_text = cursor.selectedText() # Calcular posición del punto en la línea line_start = cursor.selectionStart() dot_pos_in_line = cursor_pos - line_start - 1 if dot_pos_in_line < 0: return # Guardar posición del trigger self._autocomplete_trigger_pos = cursor_pos self._autocomplete_filter_text = "" # Texto antes del punto text_before_dot = line_text[:dot_pos_in_line].strip() # Determinar si es popup GLOBAL if not text_before_dot: self.logger.debug("Dot en línea vacía. Ofreciendo sugerencias globales.") suggestions = self._get_global_suggestions() if suggestions: self._show_autocomplete_popup(suggestions, is_global_popup=True) return # Es popup de OBJETO obj_expr_str = self._extract_object_expression(text_before_dot) if not obj_expr_str: return self.logger.debug(f"Autocompletado para objeto: '{obj_expr_str}'") # Caso especial para sympy if obj_expr_str == "sympy": methods = SympyHelper.PopupFunctionList() if methods: self._show_autocomplete_popup(methods, is_global_popup=False) return # Preprocesar con bracket parser si es necesario if '[' in obj_expr_str: obj_expr_str = self.engine.parser._transform_brackets(obj_expr_str) # Evaluar la expresión del objeto eval_context = self.engine._get_full_context() try: obj = eval(obj_expr_str, eval_context) self.logger.debug(f"Objeto evaluado: {type(obj)}") # Mostrar métodos del objeto if hasattr(obj, 'PopupFunctionList'): methods = obj.PopupFunctionList() if methods: self._show_autocomplete_popup(methods, is_global_popup=False) except Exception as e: self.logger.debug(f"Error evaluando objeto '{obj_expr_str}': {e}") def _extract_object_expression(self, text: str) -> str: """Extrae la expresión del objeto del texto antes del punto""" obj_expr_regex = r"([a-zA-Z_][a-zA-Z0-9_]*(?:\[[^\]]*\])?(?:(?:\s*\.\s*[a-zA-Z_][a-zA-Z0-9_]*)(?:\[[^\]]*\])?)*)$" match = re.search(obj_expr_regex, text) if match: return match.group(1).replace(" ", "") # Fallback if re.match(r"^[a-zA-Z_0-9\(\)\[\]\.\"\'\+\-\*\/ ]*$", text) and \ not text.endswith(("+", "-", "*", "/", "(", ",")): return text return "" def _get_global_suggestions(self) -> List[Tuple[str, str]]: """Obtiene sugerencias globales para autocompletado""" suggestions = [] try: # Obtener contexto dinámico dynamic_context = get_registered_base_context() for name, class_or_func in dynamic_context.items(): if name[0].isupper(): # Prioritizar capitalizados hint = f"Tipo o función: {name}" if hasattr(class_or_func, '__doc__') and class_or_func.__doc__: first_line = class_or_func.__doc__.strip().split('\n')[0] hint = f"{name} - {first_line}" suggestions.append((name, hint)) # Añadir funciones SymPy sympy_functions = SympyHelper.PopupFunctionList() if sympy_functions: current_names = {s[0] for s in suggestions} for fname, fhint in sympy_functions: if fname not in current_names: suggestions.append((fname, fhint)) except Exception as e: self.logger.debug(f"Error obteniendo sugerencias globales: {e}") suggestions.sort(key=lambda x: x[0]) return suggestions def _schedule_variable_autocomplete_improved(self): """Programa el autocompletado de variables""" if self._autocomplete_active or self._popup_disabled_until_next_dot: return # Verificar que estemos escribiendo cursor = self.input_text.textCursor() cursor.select(QTextCursor.LineUnderCursor) current_line = cursor.selectedText().strip() if not current_line or current_line.endswith('.'): return # Programar para 800ms después self._variable_popup_timer.start(800) def _show_variable_autocomplete_improved(self): """Muestra autocompletado de variables disponibles""" if self._autocomplete_active or self._variable_popup_active: return # Verificar línea actual cursor = self.input_text.textCursor() cursor.select(QTextCursor.LineUnderCursor) current_line = cursor.selectedText().strip() if not current_line or current_line.endswith('.'): return # Obtener variables del contexto try: context = self.engine._get_full_context() variables = [] # Filtrar variables for name, value in context.items(): if (not name.startswith('_') and not callable(value) and name not in ['sympy', 'math', 'numpy', 'plt', 'builtins']): # Descripción del valor value_str = str(value) if len(value_str) > 20: value_str = value_str[:17] + "..." variables.append((name, value_str)) if variables: variables.sort(key=lambda x: x[0]) # Filtrar por palabra actual words = current_line.split() if words: last_word = words[-1] filtered_vars = [ (name, value) for name, value in variables if name.lower().startswith(last_word.lower()) and name != last_word ] if filtered_vars: self._show_variable_popup(filtered_vars) except Exception as e: self.logger.debug(f"Error obteniendo variables: {e}") def _show_autocomplete_popup(self, suggestions: List[Tuple[str, str]], is_global_popup: bool = False): """Muestra popup de autocompletado""" if not suggestions: return self._close_autocomplete_popup() # Guardar estado self._current_suggestions = suggestions.copy() self._is_global_popup = is_global_popup self._autocomplete_active = True # Crear popup self._autocomplete_popup = AutocompletePopup(self) self._autocomplete_popup.set_suggestions(suggestions) self._autocomplete_popup.item_selected.connect(self._on_autocomplete_selected) # Posicionar cursor_rect = self.input_text.cursorRect() global_pos = self.input_text.mapToGlobal(cursor_rect.bottomLeft()) self._autocomplete_popup.move(global_pos) self._autocomplete_popup.adjust_size() self._autocomplete_popup.show() def _show_variable_popup(self, variables: List[Tuple[str, str]]): """Muestra popup de variables""" self._variable_popup_active = True self._autocomplete_active = False # Convertir formato para el popup suggestions = [(name, f"= {value}") for name, value in variables] self._show_autocomplete_popup(suggestions, is_global_popup=False) def _filter_autocomplete(self): """Filtra las sugerencias del autocompletado""" if not self._autocomplete_active or not self._autocomplete_trigger_pos: return # Obtener texto escrito después del punto cursor = self.input_text.textCursor() current_pos = cursor.position() if current_pos > self._autocomplete_trigger_pos: cursor.setPosition(self._autocomplete_trigger_pos) cursor.setPosition(current_pos, QTextCursor.KeepAnchor) filter_text = cursor.selectedText().lower() self._autocomplete_filter_text = filter_text else: self._autocomplete_filter_text = "" # Filtrar sugerencias filtered = [] for name, hint in self._current_suggestions: if name.lower().startswith(self._autocomplete_filter_text): filtered.append((name, hint)) if filtered and self._autocomplete_popup: self._autocomplete_popup.set_suggestions(filtered) self._autocomplete_popup.adjust_size() else: self._close_autocomplete_popup() def _handle_arrow_key(self, direction: int): """Maneja navegación con flechas en el popup""" if self._autocomplete_popup: self._autocomplete_popup.navigate(direction) self._last_navigation_time = time.time() def _handle_tab_key(self): """Maneja tecla TAB para seleccionar""" self._select_autocomplete() def _handle_escape_key(self): """Maneja tecla ESC para cerrar popup""" self._close_autocomplete_popup() if self._autocomplete_active: self._popup_disabled_until_next_dot = True def _select_autocomplete(self): """Selecciona el item actual del autocompletado""" if not self._autocomplete_popup: return selected_text = self._autocomplete_popup.get_selected_text() if selected_text: self._insert_autocomplete_text(selected_text) self._close_autocomplete_popup() def _on_autocomplete_selected(self, text: str): """Callback cuando se selecciona un item del popup""" self._insert_autocomplete_text(text) self._close_autocomplete_popup() def _insert_autocomplete_text(self, text: str): """Inserta el texto seleccionado del autocompletado""" cursor = self.input_text.textCursor() # Para popup de variables if self._variable_popup_active: # Reemplazar palabra actual cursor.select(QTextCursor.WordUnderCursor) cursor.insertText(text) return # Para popup global (después de punto solo) if self._is_global_popup: # Eliminar el punto y añadir función cursor.setPosition(self._autocomplete_trigger_pos - 1) cursor.setPosition(self._autocomplete_trigger_pos, QTextCursor.KeepAnchor) cursor.insertText(text + "()") # Posicionar cursor dentro de paréntesis cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text) + 1) self.input_text.setTextCursor(cursor) else: # Para métodos de objeto if self._autocomplete_filter_text: # Eliminar texto filtrado start_pos = cursor.position() - len(self._autocomplete_filter_text) cursor.setPosition(start_pos) cursor.setPosition(start_pos + len(self._autocomplete_filter_text), QTextCursor.KeepAnchor) cursor.removeSelectedText() # Insertar método cursor.insertText(text + "()") cursor.setPosition(cursor.position() - 1) self.input_text.setTextCursor(cursor) def _check_dot_removal(self): """Verifica si se borró el punto que activó el autocompletado""" cursor = self.input_text.textCursor() if cursor.position() > 0: cursor.setPosition(cursor.position() - 1) cursor.setPosition(cursor.position() + 1, QTextCursor.KeepAnchor) if cursor.selectedText() == '.': self._close_autocomplete_popup() def _close_autocomplete_popup(self): """Cierra el popup de autocompletado""" if self._autocomplete_popup: self._autocomplete_popup.close() self._autocomplete_popup = None # Resetear estado self._autocomplete_active = False self._variable_popup_active = False self._autocomplete_trigger_pos = None self._autocomplete_filter_text = "" self._current_suggestions = [] # ========== MANEJO DE PANEL LATEX ========== 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() self.latex_button.setChecked(True) # Re-renderizar ecuaciones si hay if hasattr(self, '_latex_equations') and self._latex_equations: for eq in self._latex_equations: self.latex_panel.add_equation(eq['type'], eq['content']) else: self.latex_panel.hide() self.latex_button.setChecked(False) # Guardar estado self.settings["latex_panel_visible"] = self.latex_panel_visible # ========== FUNCIONES DE MENÚ ========== def new_session(self): """Inicia una nueva sesión""" self.clear_input() self.clear_output() self.latex_panel.clear_equations() if hasattr(self, '_latex_equations'): self._latex_equations.clear() self._update_status("✨ Nueva sesión iniciada") def load_file(self): """Carga archivo en el editor""" filepath, _ = QFileDialog.getOpenFileName( self, "Cargar archivo", "", "Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)" ) if filepath: try: with open(filepath, "r", encoding="utf-8") as f: content = f.read() self.input_text.setPlainText(content) self._evaluate_and_update() self._update_status(f"📁 Archivo cargado: {Path(filepath).name}") except Exception as e: QMessageBox.critical(self, "Error", f"No se pudo cargar el archivo:\n{e}") def save_file(self): """Guarda contenido del editor""" filepath, _ = QFileDialog.getSaveFileName( self, "Guardar archivo", "", "Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)" ) if filepath: try: content = self.input_text.toPlainText() with open(filepath, "w", encoding="utf-8") as f: f.write(content) self._update_status(f"💾 Archivo guardado: {Path(filepath).name}") except Exception as e: QMessageBox.critical(self, "Error", f"No se pudo guardar el archivo:\n{e}") def clear_input(self): """Limpia panel de entrada""" self.input_text.clear() self._clear_output() def clear_output(self): """Limpia panel de salida y LaTeX""" self._clear_output() self.latex_panel.clear_equations() if hasattr(self, '_latex_equations'): self._latex_equations.clear() def clear_history(self): """Limpia el archivo de historial""" try: if os.path.exists(self.HISTORY_FILE): os.remove(self.HISTORY_FILE) self._update_status("✓ Historial limpiado") except Exception as e: QMessageBox.critical(self, "Error", f"No se pudo limpiar el historial:\n{e}") def reload_types(self): """Recarga el sistema de tipos""" try: self.logger.info("Recargando sistema de tipos...") self._setup_dynamic_helpers() self._evaluate_and_update() self._update_status("✓ Sistema de tipos recargado") except Exception as e: self.logger.error(f"Error recargando tipos: {e}") QMessageBox.critical(self, "Error", f"Error recargando tipos:\n{e}") def show_types_info(self): """Muestra información sobre tipos disponibles""" try: context_info = self.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, "Error", f"Error obteniendo información:\n{e}") def show_types_syntax(self): """Muestra sintaxis de tipos disponibles""" try: types_info = self.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, "Error", f"Error obteniendo sintaxis:\n{e}") 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, "Acerca de", about) def _show_info_dialog(self, title: str, content: str): """Muestra diálogo de información con scroll""" dialog = QMessageBox(self) dialog.setWindowTitle(title) dialog.setIcon(QMessageBox.Information) dialog.setText(content[:200] + "..." if len(content) > 200 else content) dialog.setDetailedText(content) dialog.exec() def _diagnose_mathjax(self): """Ejecuta diagnóstico de MathJax""" if not hasattr(self.latex_panel, '_webview_available') or not self.latex_panel._webview_available: QMessageBox.warning(self, "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.latex_panel._webview_available else "Usando fallback HTML" equations = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0 info = f"""DIAGNÓSTICO MATHJAX Estado: {status} Ecuaciones en memoria: {equations} Panel visible: {self.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, 'latex_panel') panel_visible = self.latex_panel_visible if panel_exists else False webview_available = self.latex_panel._webview_available if panel_exists else False equations_count = len(self._latex_equations) if hasattr(self, '_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) # ========== UTILIDADES ========== def _obtener_ayuda(self, input_str: str) -> Optional[str]: """Obtiene ayuda usando helpers dinámicos""" for helper in self.HELPERS: try: ayuda = helper(input_str) if ayuda: return ayuda except Exception as e: self.logger.debug(f"Error en helper: {e}") return None def _update_status(self, message: str, timeout: int = 0): """Actualiza la barra de estado""" self.status_bar.showMessage(message, timeout) def _load_settings(self) -> Dict[str, Any]: """Carga configuración de la aplicación""" if os.path.exists(self.SETTINGS_FILE): try: with open(self.SETTINGS_FILE, "r", encoding="utf-8") as f: return json.load(f) except: pass return { "window_geometry": None, "splitter_sizes": None, "debug_mode": False, "latex_panel_visible": True } def _save_settings(self): """Guarda configuraciones""" try: # Guardar geometría geometry = self.geometry() self.settings["window_geometry"] = { "x": geometry.x(), "y": geometry.y(), "width": geometry.width(), "height": geometry.height() } # Guardar tamaños del splitter if hasattr(self, 'main_splitter'): self.settings["splitter_sizes"] = self.main_splitter.sizes() self.settings["latex_panel_visible"] = self.latex_panel_visible self.settings["debug_mode"] = self.debug with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f: json.dump(self.settings, f, indent=4, ensure_ascii=False) except Exception as e: self.logger.error(f"Error guardando configuración: {e}") def _load_history(self): """Carga historial de entrada""" try: if os.path.exists(self.HISTORY_FILE): with open(self.HISTORY_FILE, "r", encoding="utf-8") as f: content = f.read() if content.strip(): self.input_text.setPlainText(content) # Evaluación inicial QTimer.singleShot(100, self._evaluate_and_update) except Exception as e: self.logger.error(f"Error cargando historial: {e}") def _save_history(self): """Guarda historial de entrada""" try: content = self.input_text.toPlainText() if content: with open(self.HISTORY_FILE, "w", encoding="utf-8") as f: f.write(content) elif os.path.exists(self.HISTORY_FILE): os.remove(self.HISTORY_FILE) except Exception as e: self.logger.error(f"Error guardando historial: {e}") def _restore_geometry(self): """Restaura geometría guardada""" try: geom = self.settings.get("window_geometry") if geom and isinstance(geom, dict): self.setGeometry(geom["x"], geom["y"], geom["width"], geom["height"]) # Restaurar splitter sizes = self.settings.get("splitter_sizes") if sizes and hasattr(self, 'main_splitter'): self.main_splitter.setSizes(sizes) except Exception as e: self.logger.warning(f"No se pudo restaurar geometría: {e}") def closeEvent(self, event): """Maneja el cierre de la aplicación""" try: # Guardar configuración self._save_settings() self._save_history() # Cerrar ventanas interactivas if self.interactive_manager: self.interactive_manager.close_all_windows() # Cerrar popup si existe self._close_autocomplete_popup() event.accept() except Exception as e: self.logger.error(f"Error durante cierre: {e}") event.accept() def main(): """Función principal""" app = QApplication(sys.argv) app.setApplicationName("Calculadora MAV") app.setOrganizationName("MAV") # Configurar estilo app.setStyle('Fusion') # Crear ventana principal window = HybridCalculatorPySide6() window.show() sys.exit(app.exec()) if __name__ == "__main__": main()