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