From c219b628570707421eb52a6d364fd138355be7ce Mon Sep 17 00:00:00 2001 From: Miguel Date: Wed, 11 Jun 2025 20:49:06 +0200 Subject: [PATCH] Refactorizacion de gui_main --- .data/history.txt | 4 +- ...tl_bracket_parser.py => bracket_parser.py} | 0 app/{simple_debug.py => debug_api.py} | 4 +- app/{main_evaluation.py => evaluation.py} | 6 +- app/gui_autocomplete.py | 677 +++++ app/gui_evaluation.py | 329 +++ app/gui_latex.py | 481 ++++ app/gui_main.py | 305 ++ app/gui_menus.py | 517 ++++ app/{tl_popup.py => gui_popup.py} | 0 app/gui_settings.py | 124 + app/gui_widgets.py | 186 ++ app/main_calc_app.py | 2497 ----------------- calc.py | 2 +- custom_types/bin_type.py | 4 +- custom_types/chr_type.py | 2 +- custom_types/dec_type.py | 2 +- custom_types/fourbytes_type.py | 4 +- custom_types/hex_type.py | 4 +- custom_types/intbase_type.py | 4 +- custom_types/ip4_type.py | 6 +- custom_types/latex_type.py | 2 +- 22 files changed, 2640 insertions(+), 2520 deletions(-) rename app/{tl_bracket_parser.py => bracket_parser.py} (100%) rename app/{simple_debug.py => debug_api.py} (98%) rename app/{main_evaluation.py => evaluation.py} (99%) create mode 100644 app/gui_autocomplete.py create mode 100644 app/gui_evaluation.py create mode 100644 app/gui_latex.py create mode 100644 app/gui_main.py create mode 100644 app/gui_menus.py rename app/{tl_popup.py => gui_popup.py} (100%) create mode 100644 app/gui_settings.py create mode 100644 app/gui_widgets.py delete mode 100644 app/main_calc_app.py diff --git a/.data/history.txt b/.data/history.txt index f9b3227..77357dd 100644 --- a/.data/history.txt +++ b/.data/history.txt @@ -6,9 +6,7 @@ var1 = 2 var2 = 4 vatt1 = 4 -IP4 - - +vatvatt1() diff --git a/app/tl_bracket_parser.py b/app/bracket_parser.py similarity index 100% rename from app/tl_bracket_parser.py rename to app/bracket_parser.py diff --git a/app/simple_debug.py b/app/debug_api.py similarity index 98% rename from app/simple_debug.py rename to app/debug_api.py index b91a9f8..7bbf502 100644 --- a/app/simple_debug.py +++ b/app/debug_api.py @@ -22,7 +22,7 @@ from datetime import datetime from pathlib import Path # Importar motores de evaluación -from .main_evaluation import HybridEvaluationEngine +from .evaluation import HybridEvaluationEngine def run_debug(input_file: str, output_file: str = None, verbose: bool = False): @@ -59,7 +59,7 @@ def run_debug(input_file: str, output_file: str = None, verbose: bool = False): # Crear motor de evaluación según el módulo especificado if engine_module == 'main_evaluation_puro': - from .main_evaluation import PureAlgebraicEngine + from .evaluation import PureAlgebraicEngine engine = PureAlgebraicEngine() else: # Motor por defecto diff --git a/app/main_evaluation.py b/app/evaluation.py similarity index 99% rename from app/main_evaluation.py rename to app/evaluation.py index 047685a..78cc997 100644 --- a/app/main_evaluation.py +++ b/app/evaluation.py @@ -11,7 +11,7 @@ from dataclasses import dataclass import logging try: - from sympy_helper import SympyHelper + from sympy_Helper import SympyHelper HAS_SYMPY_HELPER = True except ImportError: HAS_SYMPY_HELPER = False @@ -21,8 +21,8 @@ from .type_registry import ( get_registered_tokenization_patterns, discover_and_register_types ) -from .tl_bracket_parser import BracketParser -from .tl_popup import PlotResult +from .bracket_parser import BracketParser +from .gui_popup import PlotResult @dataclass diff --git a/app/gui_autocomplete.py b/app/gui_autocomplete.py new file mode 100644 index 0000000..aa303b4 --- /dev/null +++ b/app/gui_autocomplete.py @@ -0,0 +1,677 @@ +""" +Sistema de Autocompletado para la Calculadora MAV CAS Híbrida +""" +import time +import re +from typing import List, Tuple, Optional +from PySide6.QtCore import Qt, QTimer +from PySide6.QtGui import QTextCursor +import logging +from .gui_widgets import AutocompletePopup +from .sympy_Helper import SympyTools as SympyHelper +from .type_registry import get_registered_base_context + + +class AutocompleteManager: + """Gestor del sistema de autocompletado""" + + def __init__(self, parent_app): + self.parent_app = parent_app + self.logger = logging.getLogger(__name__) + + # Variables de autocompletado + 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._variable_popup_timer = QTimer() + self._variable_popup_timer.setSingleShot(True) + self._variable_popup_timer.timeout.connect(self._show_variable_autocomplete_improved) + + 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 filtrar autocompletado o cerrar popup + if event.key() == Qt.Key_Backspace: + if self._autocomplete_active: + # Filtrar dinámicamente al eliminar caracteres + QTimer.singleShot(1, self._filter_autocomplete) + QTimer.singleShot(1, self._check_dot_removal) + else: + # Activar autocompletado de variables si no está activo + QTimer.singleShot(50, self._schedule_variable_autocomplete_improved) + + # 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 + + # Caracteres que cierran el autocompletado + closing_chars = [' ', '+', '-', '*', '/', '(', ')', '=', ',', ';', '>', '<', '!'] + + # Cerrar autocompletado con símbolos y espacio + if self._autocomplete_active and event_text in closing_chars: + self._close_autocomplete_popup() + return + + # 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 (incluye caracteres alfanuméricos y backspace procesado) + elif self._autocomplete_active 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 - también cuando se eliminen caracteres + if not self._autocomplete_active and not self._popup_disabled_until_next_dot: + self._schedule_variable_autocomplete_improved() + elif self._variable_popup_active and not just_navigated: + # Si el popup de variables está activo, regenerar dinámicamente + QTimer.singleShot(50, self._regenerate_variable_autocomplete) + + def _handle_dot_autocomplete(self): + """Maneja el autocompletado cuando se escribe un punto""" + self._close_autocomplete_popup() + + cursor = self.parent_app.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.parent_app.engine.parser._transform_brackets(obj_expr_str) + + # Evaluar la expresión del objeto + eval_context = self.parent_app.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)) + + # Añadir variables del contexto actual + try: + context = self.parent_app.engine._get_full_context() + current_names = {s[0] for s in suggestions} + + for name, value in context.items(): + if (not name.startswith('_') and + not callable(value) and + name not in ['sympy', 'math', 'numpy', 'plt', 'builtins'] and + name not in current_names): + + # Descripción del valor + value_str = str(value) + if len(value_str) > 20: + value_str = value_str[:17] + "..." + + suggestions.append((name, f"Variable = {value_str}")) + except Exception: + pass + + 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.parent_app.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 _regenerate_variable_autocomplete(self): + """Regenera dinámicamente el autocompletado de variables""" + if not self._variable_popup_active: + return + + # Obtener la palabra actual bajo el cursor + cursor = self.parent_app.input_text.textCursor() + cursor.select(QTextCursor.WordUnderCursor) + filter_text = cursor.selectedText().lower() + + # Obtener variables del contexto actual + try: + context = self.parent_app.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, f"= {value_str}")) + + if variables: + variables.sort(key=lambda x: x[0]) + + # Filtrar por texto actual si existe + if filter_text: + filtered_vars = [ + (name, value) for name, value in variables + if name.lower().startswith(filter_text) and name.lower() != filter_text + ] + else: + filtered_vars = variables + + if filtered_vars: + # Actualizar sugerencias y popup + self._current_suggestions = variables + if self._autocomplete_popup: + self._autocomplete_popup.set_suggestions(filtered_vars) + self._autocomplete_popup.adjust_size() + else: + self._close_autocomplete_popup() + else: + self._close_autocomplete_popup() + + except Exception as e: + self.logger.debug(f"Error regenerando variables: {e}") + + 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.parent_app.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.parent_app.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.parent_app) + self._autocomplete_popup.set_suggestions(suggestions) + self._autocomplete_popup.item_selected.connect(self._on_autocomplete_selected) + + # Posicionar + cursor_rect = self.parent_app.input_text.cursorRect() + global_pos = self.parent_app.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 = True # También debe estar activo para el filtrado + + # Las variables ya vienen en el formato correcto (nombre, descripción) + self._show_autocomplete_popup(variables, is_global_popup=False) + + def _filter_autocomplete(self): + """Filtra las sugerencias del autocompletado dinámicamente""" + if not self._autocomplete_active: + return + + cursor = self.parent_app.input_text.textCursor() + current_pos = cursor.position() + + # Para popup de variables (filtrar por texto parcial) + if self._variable_popup_active: + # Obtener la palabra actual bajo el cursor + cursor.select(QTextCursor.WordUnderCursor) + filter_text = cursor.selectedText().lower() + self._autocomplete_filter_text = filter_text + + # Si no hay texto que filtrar, regenerar popup con todas las variables + if not filter_text: + # Regenerar popup dinámicamente con variables actuales + try: + context = self.parent_app.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, f"= {value_str}")) + + if variables: + variables.sort(key=lambda x: x[0]) + self._current_suggestions = variables + if self._autocomplete_popup: + self._autocomplete_popup.set_suggestions(variables) + self._autocomplete_popup.adjust_size() + else: + self._close_autocomplete_popup() + except Exception: + self._close_autocomplete_popup() + return + + # Filtrar sugerencias de variables existentes + regenerar con contexto actual + try: + context = self.parent_app.engine._get_full_context() + all_variables = [] + + # Obtener todas las variables actuales + 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] + "..." + + all_variables.append((name, f"= {value_str}")) + + # Filtrar por el texto parcial + filtered = [] + for name, hint in all_variables: + if name.lower().startswith(filter_text): + filtered.append((name, hint)) + + # Actualizar sugerencias actuales + self._current_suggestions = all_variables + + if filtered and self._autocomplete_popup: + self._autocomplete_popup.set_suggestions(filtered) + self._autocomplete_popup.adjust_size() + else: + self._close_autocomplete_popup() + + except Exception: + # Fallback al método anterior + filtered = [] + for name, hint in self._current_suggestions: + if name.lower().startswith(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() + + # Para popup de métodos (filtrar después del punto) + elif self._autocomplete_trigger_pos: + 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: + # Si el cursor está antes del punto trigger, cerrar popup + self._close_autocomplete_popup() + return + + # Filtrar sugerencias de métodos + 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.parent_app.input_text.textCursor() + + # Para popup de variables + if self._variable_popup_active: + # Obtener el texto parcial ya escrito + current_pos = cursor.position() + cursor.select(QTextCursor.WordUnderCursor) + partial_text = cursor.selectedText() + + # Verificar si hay texto parcial que ya coincide con el inicio de la variable + if partial_text and text.lower().startswith(partial_text.lower()): + # Reemplazar completamente la palabra seleccionada con la variable completa + cursor.insertText(text) + elif partial_text: + # Si hay texto parcial pero no coincide, reemplazar completamente + cursor.insertText(text) + else: + # Si no hay texto parcial, insertar directamente + 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/variable + if self._autocomplete_trigger_pos is not None: + cursor.setPosition(self._autocomplete_trigger_pos - 1) + else: + cursor.setPosition(0) + if self._autocomplete_trigger_pos is not None: + cursor.setPosition(self._autocomplete_trigger_pos, QTextCursor.KeepAnchor) + + # Verificar si es una función o variable para decidir si agregar paréntesis + try: + context = self.parent_app.engine._get_full_context() + if text in context: + obj = context[text] + if callable(obj): + # Es una función, agregar paréntesis + cursor.insertText(text + "()") + cursor.setPosition((self._autocomplete_trigger_pos or 0) - 1 + len(text) + 1) + else: + # Es una variable, no agregar paréntesis + cursor.insertText(text) + cursor.setPosition((self._autocomplete_trigger_pos or 0) - 1 + len(text)) + else: + # Por defecto, asumir que es función si no está en contexto + cursor.insertText(text + "()") + cursor.setPosition((self._autocomplete_trigger_pos or 0) - 1 + len(text) + 1) + except Exception: + # Fallback: agregar paréntesis por defecto + cursor.insertText(text + "()") + cursor.setPosition((self._autocomplete_trigger_pos or 0) - 1 + len(text) + 1) + + self.parent_app.input_text.setTextCursor(cursor) + else: + # Para métodos de objeto - considerar texto ya escrito después del punto + if self._autocomplete_filter_text: + # Calcular la posición correcta del texto ya escrito + current_pos = cursor.position() + filter_len = len(self._autocomplete_filter_text) + + # Seleccionar el texto filtrado para reemplazarlo + cursor.setPosition(current_pos - filter_len) + cursor.setPosition(current_pos, QTextCursor.KeepAnchor) + cursor.removeSelectedText() + + # Insertar el texto completo del método + cursor.insertText(text + "()") + cursor.setPosition(cursor.position() - 1) + else: + # Si no hay texto filtrado, insertar normalmente + cursor.insertText(text + "()") + cursor.setPosition(cursor.position() - 1) + + self.parent_app.input_text.setTextCursor(cursor) + + def _check_dot_removal(self): + """Verifica si se borró el punto que activó el autocompletado""" + if not self._autocomplete_active or not self._autocomplete_trigger_pos: + return + + cursor = self.parent_app.input_text.textCursor() + current_pos = cursor.position() + + # Si el cursor está antes de la posición del trigger, el punto fue eliminado + if current_pos < self._autocomplete_trigger_pos: + self._close_autocomplete_popup() + return + + # Verificar si todavía existe el punto en la posición trigger + if self._autocomplete_trigger_pos > 0: + cursor.setPosition(self._autocomplete_trigger_pos - 1) + cursor.setPosition(self._autocomplete_trigger_pos, 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 = [] + + def stop_timers(self): + """Detiene todos los timers del autocompletado""" + self._variable_popup_timer.stop() + + def reset_state(self): + """Resetea el estado del autocompletado""" + self._close_autocomplete_popup() + self._popup_disabled_until_next_dot = False + + def on_input_changed(self): + """Método llamado cuando cambia el texto de entrada""" + # Marcar tiempo del cambio + self._last_input_change = time.time() + + # Si hay autocompletado activo, filtrar dinámicamente + if self._autocomplete_active: + QTimer.singleShot(10, self._filter_autocomplete) + elif not self._popup_disabled_until_next_dot: + # Programar autocompletado de variables + self._schedule_variable_autocomplete_improved() \ No newline at end of file diff --git a/app/gui_evaluation.py b/app/gui_evaluation.py new file mode 100644 index 0000000..a2aab35 --- /dev/null +++ b/app/gui_evaluation.py @@ -0,0 +1,329 @@ +""" +Sistema de Evaluación y Procesamiento para la Calculadora MAV CAS Híbrida +""" +import logging +from typing import List, Dict, Any, Optional, Tuple +from PySide6.QtGui import QTextCursor, QTextCharFormat, QColor, QFont +from .evaluation import EvaluationResult +from .gui_latex import LatexProcessor +import sympy + + +class EvaluationManager: + """Gestor del sistema de evaluación y procesamiento de resultados""" + + def __init__(self, main_window): + self.main_window = main_window + self.logger = logging.getLogger(__name__) + self._latex_equations = [] + + # Timer para debounce de evaluación + from PySide6.QtCore import QTimer + self._debounce_timer = QTimer() + self._debounce_timer.setSingleShot(True) + self._debounce_timer.timeout.connect(self.evaluate_and_update) + + # Configurar formatos de salida + self._setup_output_formats() + + def _setup_output_formats(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 evaluate_and_update(self): + """Evalúa todas las líneas y actualiza la salida""" + try: + input_content = self.main_window.input_text.toPlainText() + if not input_content.strip(): + self._clear_output() + return + + # Limpiar contexto del motor + self.main_window.engine.equations.clear() + self.main_window.engine.symbol_table.clear() + self.main_window.engine.variables.clear() + self.logger.debug("Contexto del motor limpiado") + + # Limpiar panel LaTeX solo cuando hay contenido nuevo para evaluar + self._latex_equations.clear() + self.main_window.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 _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.main_window.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.main_window.interactive_manager: + interactive_info = self.main_window.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, equation_type = LatexProcessor.should_add_to_latex(result) + + if should_add: + # Generar contenido LaTeX que incluya toda la información del output + latex_content = LatexProcessor.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 _add_to_latex_panel(self, equation_type: str, latex_content: str): + """Añade una ecuación al panel LaTeX""" + self._latex_equations.append({ + 'type': equation_type, + 'content': latex_content + }) + + self.main_window.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 equation_count > 0: + self.main_window.latex_button.setToolTip(f"📐 Panel LaTeX ({equation_count} ecuaciones)") + else: + self.main_window.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.main_window.output_text.clear() + self.main_window.output_text.clickable_links.clear() + + cursor = self.main_window.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.main_window.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.main_window.output_text.clear() + + def _show_error(self, error_msg: str): + """Muestra un error en el panel de salida""" + self.main_window.output_text.clear() + cursor = self.main_window.output_text.textCursor() + cursor.insertText(error_msg, self.output_formats['error']) + + # Intentar obtener ayuda para el error + try: + input_content = self.main_window.input_text.toPlainText() + last_line = input_content.strip().split('\n')[-1] if input_content.strip() else "" + + if last_line: + ayuda = self._obtener_ayuda(last_line) + if ayuda: + cursor.insertText("\n\n💡 Ayuda:\n", self.output_formats['helper']) + cursor.insertText(ayuda, self.output_formats['helper']) + + except Exception as e: + self.logger.debug(f"Error obteniendo ayuda: {e}") + + def _obtener_ayuda(self, input_str: str) -> Optional[str]: + """Obtiene ayuda usando helpers dinámicos""" + for helper in self.main_window.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 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.main_window.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 clear_latex_equations(self): + """Limpia las ecuaciones LaTeX""" + self._latex_equations.clear() + self._update_latex_indicator() + + def get_latex_equations(self): + """Obtiene las ecuaciones LaTeX actuales""" + return self._latex_equations.copy() if self._latex_equations else [] + + def schedule_evaluation(self): + """Programa una evaluación con debounce""" + self._debounce_timer.stop() + self._debounce_timer.start(300) # 300ms de debounce \ No newline at end of file diff --git a/app/gui_latex.py b/app/gui_latex.py new file mode 100644 index 0000000..07fbc4e --- /dev/null +++ b/app/gui_latex.py @@ -0,0 +1,481 @@ +""" +Sistema de Panel LaTeX para la Calculadora MAV CAS Híbrida +""" +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame, QTextBrowser +) +from PySide6.QtCore import Qt, QTimer +from PySide6.QtGui import QFont +from PySide6.QtWebEngineWidgets import QWebEngineView +import logging +from typing import List, Dict, Optional +from .evaluation import EvaluationResult +import sympy + + +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: + # Usar JavaScript para limpiar dinámicamente si MathJax está listo + if self._mathjax_ready: + self.webview.page().runJavaScript("clearEquations();") + else: + # Si MathJax no está listo, recargar HTML base limpio + html_content = self._generate_mathjax_html() + self.webview.setHtml(html_content) + 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 LatexProcessor: + """Procesador de contenido LaTeX""" + + @staticmethod + def generate_complete_latex_content(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 + 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: + 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: + logging.error(f"Error generando LaTeX completo: {e}") + # Fallback al output original + return result.output or "" + + @staticmethod + def should_add_to_latex(result: EvaluationResult) -> tuple[bool, str]: + """Determina si un resultado debe agregarse al panel LaTeX y qué tipo usar""" + try: + if result.result_type == "comment": + return True, "comment" + elif result.is_assignment: + return True, "assignment" + elif result.is_equation: + return True, "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): + return True, "symbolic" + elif result.actual_result_object is not None and isinstance(result.actual_result_object, sympy.Basic): + return True, "symbolic" + + return False, "" + + except Exception: + return False, "" \ No newline at end of file diff --git a/app/gui_main.py b/app/gui_main.py new file mode 100644 index 0000000..e6508ec --- /dev/null +++ b/app/gui_main.py @@ -0,0 +1,305 @@ +""" +Calculadora MAV CAS Híbrida - Aplicación principal PySide6 (Refactorizada) +VERSIÓN MODULAR: Código organizado en módulos especializados +""" +import sys +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 + +# 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__) + + # 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", True) + 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: + self.latex_panel.hide() + + # 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""" + if self.latex_panel_visible: + self.latex_panel.hide() + self.latex_button.setChecked(False) + self.latex_panel_visible = False + else: + main_layout = self.centralWidget().layout() + main_layout.addWidget(self.latex_panel) + self.latex_panel.show() + self.latex_button.setChecked(True) + self.latex_panel_visible = True + + # Guardar configuración + self.settings_manager.set_setting("latex_panel_visible", self.latex_panel_visible) + + 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 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() \ No newline at end of file diff --git a/app/gui_menus.py b/app/gui_menus.py new file mode 100644 index 0000000..56f80e0 --- /dev/null +++ b/app/gui_menus.py @@ -0,0 +1,517 @@ +""" +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) + + 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() + + 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) + + 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: + 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}") + + 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}") + + except Exception as e: + QMessageBox.critical(self.main_window, "Error", f"No se pudo guardar el archivo:\n{e}") + + # ========== 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}") + + # ========== 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 _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() \ No newline at end of file diff --git a/app/tl_popup.py b/app/gui_popup.py similarity index 100% rename from app/tl_popup.py rename to app/gui_popup.py diff --git a/app/gui_settings.py b/app/gui_settings.py new file mode 100644 index 0000000..080b0bf --- /dev/null +++ b/app/gui_settings.py @@ -0,0 +1,124 @@ +""" +Sistema de Configuración y Persistencia para la Calculadora MAV CAS Híbrida +""" +import os +import json +import logging +from typing import Dict, Any +from PySide6.QtCore import QTimer + + +class SettingsManager: + """Gestor de configuración y persistencia""" + + SETTINGS_FILE = "./.data/settings.json" + HISTORY_FILE = "./.data/history.txt" + + def __init__(self, main_window): + self.main_window = main_window + self.logger = logging.getLogger(__name__) + self.settings = self._load_settings() + + 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: + # Crear directorio si no existe + os.makedirs(os.path.dirname(self.SETTINGS_FILE), exist_ok=True) + + # Guardar geometría + geometry = self.main_window.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_window, 'main_splitter'): + self.settings["splitter_sizes"] = self.main_window.main_splitter.sizes() + + self.settings["latex_panel_visible"] = self.main_window.latex_panel_visible + self.settings["debug_mode"] = getattr(self.main_window, 'debug', False) + + 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.main_window.input_text.setPlainText(content) + # Evaluación inicial + QTimer.singleShot(100, self.main_window._evaluate_and_update) + except Exception as e: + self.logger.error(f"Error cargando historial: {e}") + + def _save_history(self): + """Guarda historial de entrada""" + try: + # Crear directorio si no existe + os.makedirs(os.path.dirname(self.HISTORY_FILE), exist_ok=True) + + content = self.main_window.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.main_window.setGeometry(geom["x"], geom["y"], geom["width"], geom["height"]) + + # Restaurar splitter + sizes = self.settings.get("splitter_sizes") + if sizes and hasattr(self.main_window, 'main_splitter'): + self.main_window.main_splitter.setSizes(sizes) + + except Exception as e: + self.logger.warning(f"No se pudo restaurar geometría: {e}") + + def initialize(self): + """Inicializa la configuración cargando datos""" + self._load_history() + self._restore_geometry() + + def save_all(self): + """Guarda toda la configuración""" + self._save_settings() + self._save_history() + + def get_setting(self, key: str, default=None): + """Obtiene un valor de configuración""" + return self.settings.get(key, default) + + def set_setting(self, key: str, value): + """Establece un valor de configuración""" + self.settings[key] = value \ No newline at end of file diff --git a/app/gui_widgets.py b/app/gui_widgets.py new file mode 100644 index 0000000..e94785a --- /dev/null +++ b/app/gui_widgets.py @@ -0,0 +1,186 @@ +""" +Widgets personalizados para la Calculadora MAV CAS Híbrida +""" +from PySide6.QtWidgets import ( + QPlainTextEdit, QTextEdit, QPushButton, QWidget, QVBoxLayout, + QHBoxLayout, QLabel, QFrame, QListWidget, QListWidgetItem, + QTextBrowser +) +from PySide6.QtCore import Qt, QTimer, Signal +from PySide6.QtGui import QFont, QTextCursor +from PySide6.QtWebEngineWidgets import QWebEngineView +import logging +from typing import List, Tuple + + +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 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) \ No newline at end of file diff --git a/app/main_calc_app.py b/app/main_calc_app.py deleted file mode 100644 index f4f1680..0000000 --- a/app/main_calc_app.py +++ /dev/null @@ -1,2497 +0,0 @@ -""" -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, - QClipboard -) -from PySide6.QtWebEngineWidgets import QWebEngineView -from PySide6.QtWebEngineCore import QWebEngineSettings - -# Importar componentes del CAS híbrido -from .main_evaluation import PureAlgebraicEngine, EvaluationResult -from .tl_popup import InteractiveResultManager, PlotResult -from .type_registry import get_registered_helper_functions, get_registered_base_context -import sympy -from .sympy_helper import SympyTools as SympyHelper - - -class 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: - # Usar JavaScript para limpiar dinámicamente si MathJax está listo - if self._mathjax_ready: - self.webview.page().runJavaScript("clearEquations();") - else: - # Si MathJax no está listo, recargar HTML base limpio - html_content = self._generate_mathjax_html() - self.webview.setHtml(html_content) - 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 = "./.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__) - - # 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) - - diag_menu.addSeparator() - - copy_debug_action = QAction("📋 Copiar Debug al Portapapeles", self) - 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) - 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() - # NO limpiar panel LaTeX cuando no hay contenido - mantener ecuaciones previas - 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 solo cuando hay contenido nuevo para evaluar - 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']) - - # Intentar obtener ayuda para el error - try: - input_content = self.input_text.toPlainText() - last_line = input_content.strip().split('\n')[-1] if input_content.strip() else "" - - if last_line: - ayuda = self._obtener_ayuda(last_line) - if ayuda: - cursor.insertText("\n\n💡 Ayuda:\n", self.output_formats['helper']) - cursor.insertText(ayuda, self.output_formats['helper']) - - except Exception as e: - self.logger.debug(f"Error obteniendo ayuda: {e}") - - 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 filtrar autocompletado o cerrar popup - if event.key() == Qt.Key_Backspace: - if self._autocomplete_active: - # Filtrar dinámicamente al eliminar caracteres - QTimer.singleShot(1, self._filter_autocomplete) - QTimer.singleShot(1, self._check_dot_removal) - else: - # Activar autocompletado de variables si no está activo - QTimer.singleShot(50, self._schedule_variable_autocomplete_improved) - - # 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 - - # Caracteres que cierran el autocompletado - closing_chars = [' ', '+', '-', '*', '/', '(', ')', '=', ',', ';', '>', '<', '!'] - - # Cerrar autocompletado con símbolos y espacio - if self._autocomplete_active and event_text in closing_chars: - self._close_autocomplete_popup() - return - - # 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 (incluye caracteres alfanuméricos y backspace procesado) - elif self._autocomplete_active 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 - también cuando se eliminen caracteres - if not self._autocomplete_active and not self._popup_disabled_until_next_dot: - self._schedule_variable_autocomplete_improved() - elif self._variable_popup_active and not just_navigated: - # Si el popup de variables está activo, regenerar dinámicamente - QTimer.singleShot(50, self._regenerate_variable_autocomplete) - - 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)) - - # Añadir variables del contexto actual - try: - context = self.engine._get_full_context() - current_names = {s[0] for s in suggestions} - - for name, value in context.items(): - if (not name.startswith('_') and - not callable(value) and - name not in ['sympy', 'math', 'numpy', 'plt', 'builtins'] and - name not in current_names): - - # Descripción del valor - value_str = str(value) - if len(value_str) > 20: - value_str = value_str[:17] + "..." - - suggestions.append((name, f"Variable = {value_str}")) - except Exception: - pass - - 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 _regenerate_variable_autocomplete(self): - """Regenera dinámicamente el autocompletado de variables""" - if not self._variable_popup_active: - return - - # Obtener la palabra actual bajo el cursor - cursor = self.input_text.textCursor() - cursor.select(QTextCursor.WordUnderCursor) - filter_text = cursor.selectedText().lower() - - # Obtener variables del contexto actual - 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, f"= {value_str}")) - - if variables: - variables.sort(key=lambda x: x[0]) - - # Filtrar por texto actual si existe - if filter_text: - filtered_vars = [ - (name, value) for name, value in variables - if name.lower().startswith(filter_text) and name.lower() != filter_text - ] - else: - filtered_vars = variables - - if filtered_vars: - # Actualizar sugerencias y popup - self._current_suggestions = variables - if self._autocomplete_popup: - self._autocomplete_popup.set_suggestions(filtered_vars) - self._autocomplete_popup.adjust_size() - else: - self._close_autocomplete_popup() - else: - self._close_autocomplete_popup() - - except Exception as e: - self.logger.debug(f"Error regenerando variables: {e}") - - 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 = True # También debe estar activo para el filtrado - - # Las variables ya vienen en el formato correcto (nombre, descripción) - self._show_autocomplete_popup(variables, is_global_popup=False) - - def _filter_autocomplete(self): - """Filtra las sugerencias del autocompletado dinámicamente""" - if not self._autocomplete_active: - return - - cursor = self.input_text.textCursor() - current_pos = cursor.position() - - # Para popup de variables (filtrar por texto parcial) - if self._variable_popup_active: - # Obtener la palabra actual bajo el cursor - cursor.select(QTextCursor.WordUnderCursor) - filter_text = cursor.selectedText().lower() - self._autocomplete_filter_text = filter_text - - # Si no hay texto que filtrar, regenerar popup con todas las variables - if not filter_text: - # Regenerar popup dinámicamente con variables actuales - 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, f"= {value_str}")) - - if variables: - variables.sort(key=lambda x: x[0]) - self._current_suggestions = variables - if self._autocomplete_popup: - self._autocomplete_popup.set_suggestions(variables) - self._autocomplete_popup.adjust_size() - else: - self._close_autocomplete_popup() - except Exception: - self._close_autocomplete_popup() - return - - # Filtrar sugerencias de variables existentes + regenerar con contexto actual - try: - context = self.engine._get_full_context() - all_variables = [] - - # Obtener todas las variables actuales - 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] + "..." - - all_variables.append((name, f"= {value_str}")) - - # Filtrar por el texto parcial - filtered = [] - for name, hint in all_variables: - if name.lower().startswith(filter_text): - filtered.append((name, hint)) - - # Actualizar sugerencias actuales - self._current_suggestions = all_variables - - if filtered and self._autocomplete_popup: - self._autocomplete_popup.set_suggestions(filtered) - self._autocomplete_popup.adjust_size() - else: - self._close_autocomplete_popup() - - except Exception: - # Fallback al método anterior - filtered = [] - for name, hint in self._current_suggestions: - if name.lower().startswith(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() - - # Para popup de métodos (filtrar después del punto) - elif self._autocomplete_trigger_pos: - 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: - # Si el cursor está antes del punto trigger, cerrar popup - self._close_autocomplete_popup() - return - - # Filtrar sugerencias de métodos - 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: - # Obtener el texto parcial ya escrito - current_pos = cursor.position() - cursor.select(QTextCursor.WordUnderCursor) - partial_text = cursor.selectedText() - - # Verificar si hay texto parcial que ya coincide con el inicio de la variable - if partial_text and text.lower().startswith(partial_text.lower()): - # Reemplazar completamente la palabra seleccionada con la variable completa - cursor.insertText(text) - elif partial_text: - # Si hay texto parcial pero no coincide, reemplazar completamente - cursor.insertText(text) - else: - # Si no hay texto parcial, insertar directamente - 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/variable - cursor.setPosition(self._autocomplete_trigger_pos - 1) - cursor.setPosition(self._autocomplete_trigger_pos, QTextCursor.KeepAnchor) - - # Verificar si es una función o variable para decidir si agregar paréntesis - try: - context = self.engine._get_full_context() - if text in context: - obj = context[text] - if callable(obj): - # Es una función, agregar paréntesis - cursor.insertText(text + "()") - cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text) + 1) - else: - # Es una variable, no agregar paréntesis - cursor.insertText(text) - cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text)) - else: - # Por defecto, asumir que es función si no está en contexto - cursor.insertText(text + "()") - cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text) + 1) - except Exception: - # Fallback: agregar paréntesis por defecto - cursor.insertText(text + "()") - cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text) + 1) - - self.input_text.setTextCursor(cursor) - else: - # Para métodos de objeto - considerar texto ya escrito después del punto - if self._autocomplete_filter_text: - # Calcular la posición correcta del texto ya escrito - current_pos = cursor.position() - filter_len = len(self._autocomplete_filter_text) - - # Seleccionar el texto filtrado para reemplazarlo - cursor.setPosition(current_pos - filter_len) - cursor.setPosition(current_pos, QTextCursor.KeepAnchor) - cursor.removeSelectedText() - - # Insertar el texto completo del método - cursor.insertText(text + "()") - cursor.setPosition(cursor.position() - 1) - else: - # Si no hay texto filtrado, insertar normalmente - 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""" - if not self._autocomplete_active or not self._autocomplete_trigger_pos: - return - - cursor = self.input_text.textCursor() - current_pos = cursor.position() - - # Si el cursor está antes de la posición del trigger, el punto fue eliminado - if current_pos < self._autocomplete_trigger_pos: - self._close_autocomplete_popup() - return - - # Verificar si todavía existe el punto en la posición trigger - if self._autocomplete_trigger_pos > 0: - cursor.setPosition(self._autocomplete_trigger_pos - 1) - cursor.setPosition(self._autocomplete_trigger_pos, 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) - - def _copy_debug_to_clipboard(self): - """Copia información de debug completa al portapapeles""" - try: - # Obtener contenido de entrada - input_content = self.input_text.toPlainText() - - # Obtener contenido de salida (texto plano) - output_content = self.output_text.toPlainText() - - # Obtener información del sistema - context_info = self.engine.get_context_info() - - # Obtener ecuaciones LaTeX si están disponibles - latex_equations = "" - if hasattr(self, '_latex_equations') and self._latex_equations: - latex_equations = "\\n".join([ - f"[{eq['type']}] {eq['content']}" - for eq in self._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._latex_equations) if hasattr(self, '_latex_equations') else 0} -{latex_equations} - -=== CONFIGURACIÓN === -WebEngine disponible: {self.latex_panel._webview_available} -MathJax listo: {getattr(self.latex_panel, '_mathjax_ready', False)} -Panel LaTeX visible: {self.latex_panel_visible} - -=== FIN REPORTE ===""" - - # Copiar al portapapeles - clipboard = QApplication.clipboard() - clipboard.setText(debug_report) - - # Mostrar confirmación - self._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, "Error", f"Error copiando debug al portapapeles:\\n{e}") - - 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() diff --git a/calc.py b/calc.py index 1186510..b0e59d7 100644 --- a/calc.py +++ b/calc.py @@ -71,7 +71,7 @@ def main(): try: # Importar y ejecutar la aplicación - from app.main_calc_app import main as run_app + from app.gui_main import main as run_app run_app() except ImportError as e: diff --git a/custom_types/bin_type.py b/custom_types/bin_type.py index 5255644..18c8a14 100644 --- a/custom_types/bin_type.py +++ b/custom_types/bin_type.py @@ -2,14 +2,14 @@ Clase híbrida para números binarios - ADAPTADA AL NUEVO SISTEMA Ahora usa IntBase como clase base universal """ -from sympy_Base import SympyClassBase +from app.sympy_Base import SympyClassBase import re # Importación dinámica de IntBase desde el registro def get_intbase_class(): """Obtiene la clase IntBase del registro de tipos""" try: - from type_registry import get_registered_base_context + from app.type_registry import get_registered_base_context context = get_registered_base_context() return context.get('IntBase') except ImportError: diff --git a/custom_types/chr_type.py b/custom_types/chr_type.py index a5b7020..8dd23fe 100644 --- a/custom_types/chr_type.py +++ b/custom_types/chr_type.py @@ -2,7 +2,7 @@ Clase híbrida para caracteres - ADAPTADA AL NUEVO SISTEMA Archivo: custom_types/chr_type.py """ -from sympy_Base import SympyClassBase +from app.sympy_Base import SympyClassBase import re diff --git a/custom_types/dec_type.py b/custom_types/dec_type.py index 0e3d55e..39b1a1d 100644 --- a/custom_types/dec_type.py +++ b/custom_types/dec_type.py @@ -1,7 +1,7 @@ """ Clase híbrida para números decimales - ADAPTADA AL NUEVO SISTEMA """ -from sympy_Base import SympyClassBase +from app.sympy_Base import SympyClassBase import re diff --git a/custom_types/fourbytes_type.py b/custom_types/fourbytes_type.py index d7b2794..a1204b3 100644 --- a/custom_types/fourbytes_type.py +++ b/custom_types/fourbytes_type.py @@ -1,7 +1,7 @@ """ Clase base universal para patrones x.x.x.x - TIPO REGISTRADO """ -from class_base import ClassBase +from app.class_base import ClassBase import sympy import re @@ -195,7 +195,7 @@ class FourBytes(ClassBase): """Convierte cada elemento a la base especificada""" # Necesitamos importar IntBase desde el registro try: - from type_registry import get_registered_base_context + from app.type_registry import get_registered_base_context context = get_registered_base_context() IntBase = context.get('IntBase') if not IntBase: diff --git a/custom_types/hex_type.py b/custom_types/hex_type.py index b0f4250..1c99879 100644 --- a/custom_types/hex_type.py +++ b/custom_types/hex_type.py @@ -2,14 +2,14 @@ Clase híbrida para números hexadecimales - ADAPTADA AL NUEVO SISTEMA Ahora usa IntBase como clase base universal """ -from sympy_Base import SympyClassBase +from app.sympy_Base import SympyClassBase import re # Importación dinámica de IntBase desde el registro def get_intbase_class(): """Obtiene la clase IntBase del registro de tipos""" try: - from type_registry import get_registered_base_context + from app.type_registry import get_registered_base_context context = get_registered_base_context() return context.get('IntBase') except ImportError: diff --git a/custom_types/intbase_type.py b/custom_types/intbase_type.py index efbb9eb..0692cdd 100644 --- a/custom_types/intbase_type.py +++ b/custom_types/intbase_type.py @@ -1,8 +1,8 @@ """ Clase base universal para números en cualquier base - TIPO REGISTRADO """ -from class_base import ClassBase -from sympy_Base import SympyClassBase +from app.class_base import ClassBase +from app.sympy_Base import SympyClassBase import sympy import re diff --git a/custom_types/ip4_type.py b/custom_types/ip4_type.py index e7e7a05..0205383 100644 --- a/custom_types/ip4_type.py +++ b/custom_types/ip4_type.py @@ -2,8 +2,8 @@ Clase híbrida para direcciones IPv4 - ADAPTADA AL NUEVO SISTEMA Ahora usa FourBytes como clase base universal para direcciones IP """ -from class_base import ClassBase -from sympy_Base import SympyClassBase +from app.class_base import ClassBase +from app.sympy_Base import SympyClassBase from typing import Optional, Union, List, Tuple import re import sympy @@ -12,7 +12,7 @@ import sympy def get_base_classes(): """Obtiene las clases base del registro de tipos""" try: - from type_registry import get_registered_base_context + from app.type_registry import get_registered_base_context context = get_registered_base_context() return context.get('IntBase'), context.get('FourBytes') except ImportError: diff --git a/custom_types/latex_type.py b/custom_types/latex_type.py index 045b833..6885099 100644 --- a/custom_types/latex_type.py +++ b/custom_types/latex_type.py @@ -1,7 +1,7 @@ """ Clase híbrida para conversión LaTeX - ADAPTADA AL NUEVO SISTEMA """ -from sympy_Base import SympyClassBase +from app.sympy_Base import SympyClassBase import sympy import re