Compare commits

...

4 Commits

24 changed files with 2819 additions and 2647 deletions

View File

@ -1,13 +1,9 @@
ex = (t * 8) / w salario=horas*tarifa
salario=800
tarifa=36
var1 = 2
var2 = 4
vatt1 = 4
IP4
horas=?

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

View File

@ -22,7 +22,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
# Importar motores de evaluación # 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): 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 # Crear motor de evaluación según el módulo especificado
if engine_module == 'main_evaluation_puro': if engine_module == 'main_evaluation_puro':
from .main_evaluation import PureAlgebraicEngine from .evaluation import PureAlgebraicEngine
engine = PureAlgebraicEngine() engine = PureAlgebraicEngine()
else: else:
# Motor por defecto # Motor por defecto

View File

@ -11,7 +11,7 @@ from dataclasses import dataclass
import logging import logging
try: try:
from sympy_helper import SympyHelper from sympy_Helper import SympyHelper
HAS_SYMPY_HELPER = True HAS_SYMPY_HELPER = True
except ImportError: except ImportError:
HAS_SYMPY_HELPER = False HAS_SYMPY_HELPER = False
@ -21,8 +21,8 @@ from .type_registry import (
get_registered_tokenization_patterns, get_registered_tokenization_patterns,
discover_and_register_types discover_and_register_types
) )
from .tl_bracket_parser import BracketParser from .bracket_parser import BracketParser
from .tl_popup import PlotResult from .gui_popup import PlotResult
@dataclass @dataclass
@ -361,10 +361,8 @@ class PureAlgebraicEngine:
var_symbol = sp.Symbol(var_content) var_symbol = sp.Symbol(var_content)
solution_result_obj = self._smart_solve(var_symbol) solution_result_obj = self._smart_solve(var_symbol)
output = str(solution_result_obj) # Usar nuevo formato de salida
numeric = self._get_numeric_approximation(solution_result_obj) output = self._format_output_with_approximation(solution_result_obj)
if numeric and str(solution_result_obj) != str(numeric):
output += f"{numeric}"
current_algebraic_type = type(solution_result_obj).__name__ current_algebraic_type = type(solution_result_obj).__name__
# Los resultados de solve() generalmente no son plots # Los resultados de solve() generalmente no son plots
@ -383,22 +381,16 @@ class PureAlgebraicEngine:
# Para casos más complejos de solve() o si el regex no coincide, # Para casos más complejos de solve() o si el regex no coincide,
# usar _evaluate_expression que ya maneja 'last' y los tipos. # usar _evaluate_expression que ya maneja 'last' y los tipos.
# _evaluate_expression se encargará de self.last_result_object. # _evaluate_expression se encargará de self.last_result_object.
result = self._evaluate_expression(line) # Llama a la versión ya modificada result = self._evaluate_expression(line)
result.is_solve_query = True # Mantener esta bandera
return result return result
# Si no es solve(), podría ser otro tipo de atajo (si se añaden más tarde)
# Por ahora, si no empieza con solve(, lo tratamos como una expresión normal.
return self._evaluate_expression(line)
except Exception as e: except Exception as e:
error_msg = f"Error en atajo de resolución '{line}': {type(e).__name__}: {str(e)}" error_msg = f"Error en solve(): {type(e).__name__}: {str(e)}"
self.logger.error(error_msg) self.logger.error(error_msg)
# No actualizar last_result_object en caso de error
return EvaluationResult(line, error_msg, "error", False, str(e), is_solve_query=True) return EvaluationResult(line, error_msg, "error", False, str(e), is_solve_query=True)
def _evaluate_assignment(self, line: str) -> EvaluationResult: def _evaluate_assignment(self, line: str) -> EvaluationResult:
"""Evalúa una asignación CON FUNCIONALIDAD DUAL: asignación al contexto + ecuación a sympy""" """Evalúa una asignación y gestiona 'last'"""
try: try:
var_name, expression_str = line.split('=', 1) var_name, expression_str = line.split('=', 1)
var_name = var_name.strip() var_name = var_name.strip()
@ -439,7 +431,8 @@ class PureAlgebraicEngine:
# Si falla como ecuación, continuar solo con la asignación # Si falla como ecuación, continuar solo con la asignación
self.logger.warning(f"No se pudo agregar '{line}' como ecuación: {eq_error}") self.logger.warning(f"No se pudo agregar '{line}' como ecuación: {eq_error}")
output = f"{var_name} = {result_obj}" # Usar nuevo formato de salida consistente
output = self._format_output_with_approximation(Eq(sp.Symbol(var_name), result_obj))
# Devolver el resultado de la asignación # Devolver el resultado de la asignación
return EvaluationResult( return EvaluationResult(
@ -489,7 +482,9 @@ class PureAlgebraicEngine:
for free_symbol in equation_obj.free_symbols: for free_symbol in equation_obj.free_symbols:
self.variables.add(str(free_symbol)) self.variables.add(str(free_symbol))
output = str(equation_obj) # Usar nuevo formato de salida
output = self._format_equation_output(equation_obj)
# No actualizar last_result_object para ecuaciones # No actualizar last_result_object para ecuaciones
return EvaluationResult( return EvaluationResult(
input_line=line, input_line=line,
@ -518,20 +513,12 @@ class PureAlgebraicEngine:
if isinstance(result_obj, PlotResult) and not result_obj.original_expression: if isinstance(result_obj, PlotResult) and not result_obj.original_expression:
result_obj.original_expression = line result_obj.original_expression = line
output = str(result_obj) # Usar nuevo formato de salida que maneja Eq() automáticamente
output = self._format_output_with_approximation(result_obj)
result_type_str = "symbolic" # Tipo por defecto result_type_str = "symbolic" # Tipo por defecto
current_algebraic_type = type(result_obj).__name__ current_algebraic_type = type(result_obj).__name__
# Manejo especial para tipos de sympy que pueden necesitar aproximación numérica
if hasattr(result_obj, 'evalf') and not isinstance(result_obj, (sp.Matrix, sp.Basic, sp.Expr)):
# Evitar evalf en matrices directamente o en tipos ya específicos como Integer, Float
pass
numeric_approximation = self._get_numeric_approximation(result_obj)
if numeric_approximation and output != numeric_approximation:
output += f"{numeric_approximation}"
# Considerar si esto cambia el result_type a "numeric_approx"
# Determinar si es un objeto de plotting (para no asignarlo a 'last') # Determinar si es un objeto de plotting (para no asignarlo a 'last')
is_plot_object = False is_plot_object = False
if isinstance(result_obj, PlotResult): if isinstance(result_obj, PlotResult):
@ -565,18 +552,17 @@ class PureAlgebraicEngine:
return EvaluationResult(line, error_msg, "error", False, str(e)) return EvaluationResult(line, error_msg, "error", False, str(e))
def _smart_solve(self, *args, **kwargs): def _smart_solve(self, *args, **kwargs):
"""Función solve inteligente que usa nuestro sistema de ecuaciones""" """Función inteligente de resolución que maneja variables y valores"""
if not args: if len(args) == 1 and isinstance(args[0], (list, tuple)):
# solve() sin argumentos - resolver todo el sistema # solve([ecuaciones], [variables])
if not self.equations:
return "No hay ecuaciones en el sistema"
try: try:
all_vars = list(self.variables) if all(hasattr(eq, 'func') and eq.func.__name__ == 'Equality' for eq in args[0]):
solution = solve(self.equations, all_vars, dict=True) # Es una lista de ecuaciones
solutions = solve(args[0], **kwargs)
if solution: if solutions:
return solution[0] if len(solution) == 1 else solution return solutions
else:
return "Sin solución"
else: else:
return "Sin solución" return "Sin solución"
except Exception as e: except Exception as e:
@ -588,7 +574,7 @@ class PureAlgebraicEngine:
# Si encontramos una solución diferente de la variable # Si encontramos una solución diferente de la variable
if solution_value != var_symbol: if solution_value != var_symbol:
# Verificar si la solución contiene otras variables # Verificar si todas las variables libres tienen valores numéricos
free_symbols = solution_value.free_symbols free_symbols = solution_value.free_symbols
unresolved_vars = free_symbols - {var_symbol} unresolved_vars = free_symbols - {var_symbol}
@ -612,26 +598,18 @@ class PureAlgebraicEngine:
# Todas las variables tienen valores numéricos, resolver iterativamente # Todas las variables tienen valores numéricos, resolver iterativamente
final_value = self._resolve_iteratively(solution_value) final_value = self._resolve_iteratively(solution_value)
# Auto-aplicar la solución numérica al sistema # Verificar que el resultado no sea problemático
if final_value != var_symbol and not str(final_value) in ['True', 'False']: if final_value != var_symbol and not isinstance(final_value, (sp.logic.boolalg.BooleanTrue, sp.logic.boolalg.BooleanFalse)):
self._auto_apply_solution(var_symbol, final_value)
return Eq(var_symbol, final_value) return Eq(var_symbol, final_value)
else: else:
# Hay variables con valores simbólicos, intentar resolver lo máximo posible # Si hay problema con la resolución, devolver simbólico
# Aplicar resolución iterativa parcial return Eq(var_symbol, solution_value)
resolved_solution = self._resolve_iteratively(solution_value)
result_eq = Eq(var_symbol, resolved_solution)
return result_eq
else: else:
# No hay variables sin resolver, intentar resolver completamente # Hay variables con valores simbólicos, devolver ecuación simbólica
final_value = self._resolve_iteratively(solution_value) return Eq(var_symbol, solution_value)
else:
# Auto-aplicar la solución al sistema solo si es un valor específico # No hay variables sin resolver, es ya un valor final
if final_value != var_symbol and not str(final_value) in ['True', 'False']: return Eq(var_symbol, solution_value)
self._auto_apply_solution(var_symbol, final_value)
return Eq(var_symbol, final_value)
else: else:
# Si no hay solución en las ecuaciones, verificar en symbol_table # Si no hay solución en las ecuaciones, verificar en symbol_table
var_name = str(var_symbol) var_name = str(var_symbol)
@ -639,11 +617,10 @@ class PureAlgebraicEngine:
value = self.symbol_table[var_name] value = self.symbol_table[var_name]
# Si el valor en symbol_table es diferente de la variable, devolverlo # Si el valor en symbol_table es diferente de la variable, devolverlo
if value != var_symbol: if value != var_symbol:
final_value = self._resolve_iteratively(value) return Eq(var_symbol, value)
return Eq(var_symbol, final_value)
# Si no hay información, devolver la variable tal como está # Si no hay información, devolver ecuación con la variable sin resolver
return var_symbol return Eq(var_symbol, var_symbol)
else: else:
# solve() con argumentos específicos (múltiples variables, ecuaciones, etc.) # solve() con argumentos específicos (múltiples variables, ecuaciones, etc.)
return solve(*args, **kwargs) return solve(*args, **kwargs)
@ -825,10 +802,21 @@ class PureAlgebraicEngine:
try: try:
float_val = float(numeric_val) float_val = float(numeric_val)
if abs(float_val) > 1e-10: if abs(float_val) > 1e-10:
return f"{float_val:.6f}".rstrip('0').rstrip('.') # Formatear número eliminando ceros innecesarios
if float_val == int(float_val):
# Es un entero, mostrarlo como tal
return str(int(float_val))
else:
# Es decimal, formatear con precisión limitada
formatted = f"{float_val:.10f}".rstrip('0').rstrip('.')
# Si queda muy largo, usar notación más compacta
if len(formatted) > 12:
formatted = f"{float_val:.6g}"
return formatted
except: except:
pass pass
# Para otros tipos (complejos, expresiones, etc.)
return str(numeric_val) return str(numeric_val)
return None return None
except: except:
@ -872,6 +860,63 @@ class PureAlgebraicEngine:
self._load_tokenization_patterns() self._load_tokenization_patterns()
self.logger.info("Tipos y patrones recargados") self.logger.info("Tipos y patrones recargados")
def _format_equation_output(self, result_obj) -> str:
"""Convierte Eq(var, valor) a formato legible var = valor"""
try:
if hasattr(result_obj, 'func') and result_obj.func.__name__ == 'Equality':
# Es una ecuación Eq(lhs, rhs)
lhs = result_obj.lhs
rhs = result_obj.rhs
return f"{lhs} = {rhs}"
else:
# No es una ecuación, devolver string normal
return str(result_obj)
except Exception:
return str(result_obj)
def _format_output_with_approximation(self, result_obj) -> str:
"""Formatea salida con aproximación numérica, convirtiendo Eq() a formato legible"""
main_output = self._format_equation_output(result_obj)
# Obtener aproximación numérica
numeric_approximation = self._get_numeric_approximation(result_obj)
if numeric_approximation and main_output != numeric_approximation:
# Para ecuaciones, formatear también la aproximación
if hasattr(result_obj, 'func') and result_obj.func.__name__ == 'Equality':
# Crear aproximación en formato var = valor_aproximado
try:
lhs = result_obj.lhs
rhs_numeric = result_obj.rhs.evalf() if hasattr(result_obj.rhs, 'evalf') else result_obj.rhs
# Formatear la parte numérica con el mismo sistema
rhs_numeric_str = self._get_numeric_approximation(result_obj.rhs)
if rhs_numeric_str is None:
rhs_numeric_str = str(rhs_numeric)
numeric_output = f"{lhs} = {rhs_numeric_str}"
# Comparar si son realmente diferentes
# Extraer solo las partes de valor para comparar
main_value = str(result_obj.rhs)
if main_value != rhs_numeric_str:
return f"{main_output}{numeric_output}"
else:
# Son iguales, no mostrar aproximación
return main_output
except:
return f"{main_output}{numeric_approximation}"
else:
# Para expresiones no-ecuación, comparar directamente
if str(result_obj) != numeric_approximation:
return f"{main_output}{numeric_approximation}"
else:
# Son iguales, no mostrar aproximación
return main_output
return main_output
# ========== FUNCIÓN DE EVALUACIÓN DIRECTA ========== # ========== FUNCIÓN DE EVALUACIÓN DIRECTA ==========

677
app/gui_autocomplete.py Normal file
View File

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

334
app/gui_evaluation.py Normal file
View File

@ -0,0 +1,334 @@
"""
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 ecuaciones LaTeX en memoria
self._latex_equations.clear()
# Solo limpiar panel visual si está visible
if self.main_window.latex_panel_visible:
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
})
# Solo actualizar el panel si está visible/activo
if self.main_window.latex_panel_visible:
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

505
app/gui_latex.py Normal file
View File

@ -0,0 +1,505 @@
"""
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._webview_initialized = False
self._setup_ui()
# Timer para verificar si MathJax está listo (inicializado bajo demanda)
self._mathjax_check_timer = None
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 (inicialización perezosa)"""
# No cargar HTML inmediatamente para optimizar tiempo de inicio
pass
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("""
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Consolas';
padding: 10px;
}
.equation {
margin: 10px 0;
padding: 10px;
background: #2d2d2d;
border-left: 3px solid #80c7f7;
border-radius: 3px;
}
.comment { border-left-color: #6a9955; }
.assignment { border-left-color: #dcdcaa; }
</style>
<div style="text-align: center; margin-top: 50px; color: #808080;">
📐 Panel de Ecuaciones<br>
<small>Las ecuaciones aparecerán aquí</small>
</div>
""")
def _generate_mathjax_html(self):
"""Genera HTML base con MathJax configurado para SVG"""
return """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
<script>
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
processEscapes: true
},
svg: {
scale: 0.9,
minScale: 0.5,
fontCache: 'global'
},
startup: {
ready: function () {
MathJax.startup.defaultReady();
// Notificar que MathJax está listo
window.mathJaxReady = true;
console.log('MathJax SVG completamente cargado');
}
}
};
</script>
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 8px;
line-height: 1.2;
}
.equation-block {
background: rgba(45, 45, 48, 0.8);
border-left: 3px solid;
margin: 4px 0;
padding: 6px 12px;
border-radius: 4px;
transition: all 0.1s ease;
}
.equation-block:hover {
background: rgba(45, 45, 48, 0.9);
}
.comment { border-left-color: #6a9955; }
.assignment { border-left-color: #dcdcaa; }
.equation { border-left-color: #c586c0; }
.symbolic { border-left-color: #9cdcfe; }
.math-content {
margin: 2px 0;
font-size: 14px;
}
.comment-text {
font-style: italic;
color: #6a9955;
font-size: 12px;
margin: 0;
}
.info-message {
text-align: center;
padding: 20px;
color: #666;
font-size: 12px;
}
/* Optimizaciones para SVG rendering */
mjx-container[jax="SVG"] {
margin: 2px 0 !important;
}
mjx-container[jax="SVG"] svg {
vertical-align: middle;
}
/* Mejorar contraste del SVG */
mjx-container[jax="SVG"] svg text {
fill: #d4d4d4 !important;
}
</style>
</head>
<body>
<div id="equations-container">
<div class="info-message">
Panel de Ecuaciones LaTeX (SVG)
</div>
</div>
<script>
window.mathJaxReady = false;
function addEquation(type, content) {
var container = document.getElementById('equations-container');
// Limpiar mensaje inicial si existe
var infoMsg = container.querySelector('.info-message');
if (infoMsg) infoMsg.remove();
var equation = document.createElement('div');
equation.className = 'equation-block ' + type;
var mathContent = document.createElement('div');
mathContent.className = 'math-content';
if (type === 'comment') {
mathContent.className = 'comment-text';
mathContent.textContent = content;
} else {
mathContent.innerHTML = '$$' + content + '$$';
}
equation.appendChild(mathContent);
container.appendChild(equation);
// Re-renderizar MathJax solo si está listo
if (window.MathJax && window.mathJaxReady && type !== 'comment') {
MathJax.typesetPromise([equation]).catch(function(err) {
console.error('MathJax SVG error:', err);
});
}
}
function clearEquations() {
var container = document.getElementById('equations-container');
container.innerHTML = '<div class="info-message">Panel de Ecuaciones LaTeX (SVG)</div>';
}
// Función para notificar que está listo para renderizar
function triggerInitialRender() {
if (window.mathJaxReady) {
// Notificar a Python que MathJax está listo
console.log('MathJax SVG listo para renderizado inicial');
return true;
}
return false;
}
</script>
</body>
</html>"""
def _ensure_webview_initialized(self):
"""Inicializa WebEngine bajo demanda para optimizar tiempo de carga"""
if self._webview_initialized:
return
try:
if hasattr(self, 'webview') and self._webview_available:
html_content = self._generate_mathjax_html()
self.webview.setHtml(html_content)
# Inicializar timer de verificación MathJax
if self._mathjax_check_timer is None:
self._mathjax_check_timer = QTimer()
self._mathjax_check_timer.timeout.connect(self._check_mathjax_ready)
self._mathjax_check_timer.start(500) # Verificar cada 500ms
self._webview_initialized = True
logging.debug("✅ WebEngine LaTeX inicializado bajo demanda")
except Exception as e:
logging.error(f"Error inicializando WebEngine: {e}")
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:
# Inicializar WebEngine bajo demanda
self._ensure_webview_initialized()
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 and self._webview_initialized:
# 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)
elif hasattr(self, 'text_browser'):
# Para fallback o WebEngine no inicializado, solo si text_browser existe
self._setup_text_browser() # Reset al estado inicial
def _update_text_browser(self):
"""Actualiza el contenido del text browser (fallback)"""
# Verificar que text_browser existe antes de usarlo
if not hasattr(self, 'text_browser'):
return
html_parts = ["""
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Consolas';
margin: 0;
padding: 8px;
line-height: 1.2;
}
.equation {
margin: 4px 0;
padding: 6px 12px;
background: rgba(45, 45, 48, 0.8);
border-left: 3px solid #80c7f7;
border-radius: 4px;
font-size: 12px;
}
.comment {
border-left-color: #6a9955;
font-style: italic;
color: #6a9955;
font-size: 11px;
}
.assignment { border-left-color: #dcdcaa; }
.equation-type { border-left-color: #c586c0; }
.symbolic { border-left-color: #9cdcfe; }
code {
font-family: 'Consolas';
font-size: 11px;
color: #d4d4d4;
}
</style>
"""]
for eq in self.equations:
eq_type = eq['type']
content = eq['content']
css_class = eq_type
if eq_type == 'comment':
html_parts.append(f'<div class="equation {css_class}">{content}</div>')
else:
# Para ecuaciones matemáticas, mostrar en formato de código
html_parts.append(f'<div class="equation {css_class}"><code>{content}</code></div>')
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:
# Verificar si es una ecuación Eq()
if hasattr(result.actual_result_object, 'func') and result.actual_result_object.func.__name__ == 'Equality':
# Es una ecuación, convertir a formato var = valor
lhs = result.actual_result_object.lhs
rhs = result.actual_result_object.rhs
lhs_latex = sympy.latex(lhs)
rhs_latex = sympy.latex(rhs)
main_content = f"{lhs_latex} = {rhs_latex}"
else:
# Intentar generar LaTeX de SymPy para el objeto matemático
main_content = sympy.latex(result.actual_result_object)
# Para asignaciones, el resultado ya viene formateado
if result.is_assignment and not main_content:
# Extraer del output ya formateado
main_content = result.output.split("")[0] if "" in result.output else result.output
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()
# La aproximación ya viene en formato var = valor, mantenerla así
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, ""

341
app/gui_main.py Normal file
View File

@ -0,0 +1,341 @@
"""
Calculadora MAV CAS Híbrida - Aplicación principal PySide6 (Refactorizada)
VERSIÓN MODULAR: Código organizado en módulos especializados
"""
import sys
import os
import logging
from typing import Optional, Dict, Any, List
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QSplitter, QStatusBar
)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QKeySequence, QShortcut, QPixmap, QIcon
# Importar componentes del CAS híbrido
from .evaluation import PureAlgebraicEngine
from .gui_popup import InteractiveResultManager
from .type_registry import get_registered_helper_functions
from .sympy_Helper import SympyTools as SympyHelper
# Importar módulos refactorizados
from .gui_widgets import InputTextEdit, OutputTextEdit, ExpandableLatexButton, AutocompletePopup
from .gui_latex import LatexPanel
from .gui_autocomplete import AutocompleteManager
from .gui_evaluation import EvaluationManager
from .gui_menus import MenuManager
from .gui_settings import SettingsManager
class HybridCalculatorPySide6(QMainWindow):
"""Aplicación principal del CAS híbrido - VERSIÓN COMPLETA"""
SETTINGS_FILE = "./.data/settings.json"
HISTORY_FILE = "./.data/history.txt"
def __init__(self):
super().__init__()
# Configurar logging
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
self.logger = logging.getLogger(__name__)
# Configurar icono de la aplicación
self._setup_application_icon()
# Motor y managers
self.engine = PureAlgebraicEngine()
self.interactive_manager = None
# Inicializar managers
self.settings_manager = SettingsManager(self)
self.menu_manager = MenuManager(self)
self.autocomplete_manager = AutocompleteManager(self)
self.evaluation_manager = EvaluationManager(self)
# Configuración
self.settings = self.settings_manager.settings
self.debug = self.settings.get("debug_mode", False)
# Estado del panel LaTeX
self.latex_panel_visible = self.settings.get("latex_panel_visible", False)
self._latex_equations = []
# Configurar helpers
self._setup_dynamic_helpers()
# Configurar UI
self._setup_ui()
# Configurar componentes usando managers
self.menu_manager.setup_menu()
self._setup_interactive_manager()
# Cargar historial y configuración
self.settings_manager.initialize()
self.logger.info("✅ Calculadora MAV PySide6 (versión modular) inicializada")
def _setup_dynamic_helpers(self):
"""Configura helpers dinámicamente desde el registro de tipos"""
try:
self.HELPERS = get_registered_helper_functions()
self.HELPERS.append(SympyHelper.Helper)
self.logger.info(f"Helpers dinámicos cargados: {len(self.HELPERS)}")
except Exception as e:
self.logger.error(f"Error cargando helpers dinámicos: {e}")
self.HELPERS = [SympyHelper.Helper]
def _setup_ui(self):
"""Configura la interfaz de usuario completa"""
self.setWindowTitle("Calculadora MAV - CAS Híbrido")
self.setGeometry(100, 100, 1000, 700)
# Widget central
central_widget = QWidget()
self.setCentralWidget(central_widget)
# Layout principal horizontal
main_layout = QHBoxLayout(central_widget)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# Splitter principal para entrada y salida
self.main_splitter = QSplitter(Qt.Horizontal)
# Panel de entrada
self.input_text = InputTextEdit(self)
self.input_text.setPlaceholderText("Introduce expresiones matemáticas...")
self.input_text.textChanged.connect(self._on_input_changed)
# Panel de salida
self.output_text = OutputTextEdit()
self.output_text.link_clicked.connect(self._handle_output_link_click)
# Añadir al splitter
self.main_splitter.addWidget(self.input_text)
self.main_splitter.addWidget(self.output_text)
# Configurar tamaños iniciales
self.main_splitter.setSizes([450, 450])
# Sincronizar scroll
self._setup_scroll_sync()
# Añadir splitter al layout
main_layout.addWidget(self.main_splitter)
# Botón expandible para LaTeX
self.latex_button = ExpandableLatexButton()
self.latex_button.clicked.connect(self._toggle_latex_panel)
main_layout.addWidget(self.latex_button)
# Panel LaTeX (inicialmente oculto)
self.latex_panel = LatexPanel(self)
self.latex_panel.setMinimumWidth(300)
self.latex_panel.setMaximumWidth(500)
if self.latex_panel_visible:
main_layout.addWidget(self.latex_panel)
self.latex_button.setChecked(True)
else:
# Asegurar que el panel está oculto por defecto
self.latex_panel.hide()
self.latex_button.setChecked(False)
# Los formatos de salida se configuran automáticamente en EvaluationManager
# Barra de estado
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self._update_status("🔢 Calculadora MAV - Sistema Algebraico Puro")
# Aplicar tema oscuro
self._apply_dark_theme()
def _setup_scroll_sync(self):
"""Sincroniza el scroll entre entrada y salida"""
def sync_input_to_output():
if hasattr(self, '_syncing'):
return
self._syncing = True
self.output_text.verticalScrollBar().setValue(
self.input_text.verticalScrollBar().value()
)
self._syncing = False
def sync_output_to_input():
if hasattr(self, '_syncing'):
return
self._syncing = True
self.input_text.verticalScrollBar().setValue(
self.output_text.verticalScrollBar().value()
)
self._syncing = False
self.input_text.verticalScrollBar().valueChanged.connect(sync_input_to_output)
self.output_text.verticalScrollBar().valueChanged.connect(sync_output_to_input)
def _setup_interactive_manager(self):
"""Configura el manager de interactividad"""
self.interactive_manager = InteractiveResultManager()
def _apply_dark_theme(self):
"""Aplica el tema oscuro completo"""
dark_style = """
QMainWindow {
background-color: #1a1a1a;
color: #d4d4d4;
}
QPlainTextEdit, QTextEdit {
background-color: #1e1e1e;
color: #d4d4d4;
border: 1px solid #3c3c3c;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 11px;
padding: 5px;
selection-background-color: #264f78;
}
QSplitter {
background-color: #1a1a1a;
}
QSplitter::handle {
background-color: #3c3c3c;
width: 2px;
height: 2px;
}
QSplitter::handle:hover {
background-color: #4fc3f7;
}
QMenuBar {
background-color: #2d2d30;
color: #cccccc;
border-bottom: 1px solid #3c3c3c;
}
QMenuBar::item {
background-color: transparent;
padding: 4px 8px;
}
QMenuBar::item:selected {
background-color: #4fc3f7;
color: white;
}
QMenu {
background-color: #2d2d30;
color: #cccccc;
border: 1px solid #3c3c3c;
}
QMenu::item {
padding: 4px 20px;
}
QMenu::item:selected {
background-color: #4fc3f7;
color: white;
}
QStatusBar {
background-color: #007acc;
color: white;
border-top: 1px solid #3c3c3c;
}
"""
self.setStyleSheet(dark_style)
def _on_input_changed(self):
"""Maneja cambios en el texto de entrada"""
# Delegar al autocomplete manager
self.autocomplete_manager.on_input_changed()
# Programar evaluación con debounce
self.evaluation_manager.schedule_evaluation()
def _evaluate_and_update(self):
"""Evalúa y actualiza la salida - método requerido por EvaluationManager"""
self.evaluation_manager.evaluate_and_update()
def _handle_key_press(self, event) -> bool:
"""Maneja eventos de teclado para autocompletado"""
return self.autocomplete_manager.handle_key_press(event)
def _handle_output_link_click(self, link_id: str, result_object):
"""Maneja clicks en links del output"""
# Delegar al evaluation manager
self.evaluation_manager.handle_output_link_click(link_id, result_object)
def _toggle_latex_panel(self):
"""Togglea la visibilidad del panel LaTeX"""
main_layout = self.centralWidget().layout()
if self.latex_panel_visible:
# Ocultar panel
main_layout.removeWidget(self.latex_panel)
self.latex_panel.hide()
self.latex_button.setChecked(False)
self.latex_panel_visible = False
else:
# Mostrar panel
main_layout.addWidget(self.latex_panel)
self.latex_panel.show()
self.latex_button.setChecked(True)
self.latex_panel_visible = True
# Actualizar panel con ecuaciones pendientes
self._sync_latex_equations_on_show()
# Guardar configuración
self.settings_manager.set_setting("latex_panel_visible", self.latex_panel_visible)
def _sync_latex_equations_on_show(self):
"""Sincroniza las ecuaciones LaTeX cuando se muestra el panel"""
try:
# Limpiar panel
self.latex_panel.clear_equations()
# Añadir todas las ecuaciones pendientes
for eq in self.evaluation_manager.get_latex_equations():
self.latex_panel.add_equation(eq['type'], eq['content'])
except Exception as e:
self.logger.error(f"Error sincronizando ecuaciones LaTeX: {e}")
def _update_status(self, message: str, timeout: int = 0):
"""Actualiza la barra de estado"""
self.status_bar.showMessage(message, timeout)
def closeEvent(self, event):
"""Maneja el cierre de la aplicación"""
# Delegar al settings manager
self.settings_manager.save_all()
event.accept()
def _setup_application_icon(self):
"""Configura el icono de la aplicación"""
# Buscar el icono en el directorio raíz del proyecto
icon_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), ".res", "icon.png")
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
self.logger.debug(f"✅ Icono cargado desde: {icon_path}")
else:
self.logger.warning(f"⚠️ Icono no encontrado en: {icon_path}")
def main():
"""Función principal para ejecutar la aplicación"""
app = QApplication(sys.argv)
# Configurar la aplicación
app.setApplicationName("Calculadora MAV")
app.setApplicationVersion("3.0")
app.setOrganizationName("MAV")
# Crear y mostrar la ventana principal
calculator = HybridCalculatorPySide6()
calculator.show()
# Ejecutar el loop principal
sys.exit(app.exec())
if __name__ == "__main__":
main()

517
app/gui_menus.py Normal file
View File

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

124
app/gui_settings.py Normal file
View File

@ -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": False
}
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

186
app/gui_widgets.py Normal file
View File

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

File diff suppressed because it is too large Load Diff

68
calc.py
View File

@ -1,82 +1,26 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Launcher para Calculadora MAV - Versión PySide6 con MathJax Launcher para Calculadora MAV - Versión PySide6 con MathJax
Versión optimizada: sin verificaciones lentas al inicio
""" """
import sys import sys
import os
import subprocess
from pathlib import Path
import importlib.util
import logging
def check_dependencies():
"""Verifica que todas las dependencias estén instaladas"""
required_modules = [
'PySide6',
'sympy',
'numpy',
'matplotlib'
]
missing = []
for module in required_modules:
try:
__import__(module)
except ImportError:
missing.append(module)
if missing:
print("❌ Faltan las siguientes dependencias:")
for module in missing:
print(f" - {module}")
print("\n💡 Para instalar las dependencias, ejecuta:")
print(" pip install -r requirements.txt")
return False
print("✅ Todas las dependencias están instaladas")
return True
def check_pyside6_webengine():
"""Verifica si PySide6 WebEngine está disponible"""
try:
from PySide6.QtWebEngineWidgets import QWebEngineView
print("✅ PySide6 WebEngine disponible para MathJax")
return True
except ImportError:
print("⚠️ PySide6 WebEngine no disponible")
print(" Instalando QtWebEngine...")
try:
subprocess.run([sys.executable, '-m', 'pip', 'install', 'PySide6-WebEngine'],
check=True, capture_output=True)
print("✅ PySide6 WebEngine instalado correctamente")
return True
except subprocess.CalledProcessError:
print("❌ No se pudo instalar PySide6 WebEngine")
return False
def main(): def main():
"""Función principal del launcher""" """Función principal optimizada del launcher"""
print("🚀 Iniciando Calculadora MAV - Diseño Minimalista 3 Paneles") print("🚀 Iniciando Calculadora MAV - Diseño Minimalista 3 Paneles")
print("=" * 60) print("=" * 60)
# Verificar dependencias print("🎯 Iniciando aplicación...")
if not check_dependencies():
sys.exit(1)
# Verificar WebEngine
if not check_pyside6_webengine():
print("⚠️ Continuando sin WebEngine (funcionalidad limitada)")
print("\n🎯 Iniciando aplicación...")
try: try:
# Importar y ejecutar la aplicación # Importar y ejecutar la aplicación directamente
from app.main_calc_app import main as run_app from app.gui_main import main as run_app
run_app() run_app()
except ImportError as e: except ImportError as e:
print(f"❌ Error de importación: {e}") print(f"❌ Error de importación: {e}")
print(" Verifica que todos los archivos del proyecto estén presentes") print(" Verifica que todos los archivos del proyecto estén presentes")
print(" Si faltan dependencias, ejecuta: pip install -r requirements.txt")
sys.exit(1) sys.exit(1)
except Exception as e: except Exception as e:

View File

@ -2,14 +2,14 @@
Clase híbrida para números binarios - ADAPTADA AL NUEVO SISTEMA Clase híbrida para números binarios - ADAPTADA AL NUEVO SISTEMA
Ahora usa IntBase como clase base universal Ahora usa IntBase como clase base universal
""" """
from sympy_Base import SympyClassBase from app.sympy_Base import SympyClassBase
import re import re
# Importación dinámica de IntBase desde el registro # Importación dinámica de IntBase desde el registro
def get_intbase_class(): def get_intbase_class():
"""Obtiene la clase IntBase del registro de tipos""" """Obtiene la clase IntBase del registro de tipos"""
try: try:
from type_registry import get_registered_base_context from app.type_registry import get_registered_base_context
context = get_registered_base_context() context = get_registered_base_context()
return context.get('IntBase') return context.get('IntBase')
except ImportError: except ImportError:

View File

@ -2,7 +2,7 @@
Clase híbrida para caracteres - ADAPTADA AL NUEVO SISTEMA Clase híbrida para caracteres - ADAPTADA AL NUEVO SISTEMA
Archivo: custom_types/chr_type.py Archivo: custom_types/chr_type.py
""" """
from sympy_Base import SympyClassBase from app.sympy_Base import SympyClassBase
import re import re

View File

@ -1,7 +1,7 @@
""" """
Clase híbrida para números decimales - ADAPTADA AL NUEVO SISTEMA Clase híbrida para números decimales - ADAPTADA AL NUEVO SISTEMA
""" """
from sympy_Base import SympyClassBase from app.sympy_Base import SympyClassBase
import re import re

View File

@ -1,7 +1,7 @@
""" """
Clase base universal para patrones x.x.x.x - TIPO REGISTRADO 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 sympy
import re import re
@ -195,7 +195,7 @@ class FourBytes(ClassBase):
"""Convierte cada elemento a la base especificada""" """Convierte cada elemento a la base especificada"""
# Necesitamos importar IntBase desde el registro # Necesitamos importar IntBase desde el registro
try: try:
from type_registry import get_registered_base_context from app.type_registry import get_registered_base_context
context = get_registered_base_context() context = get_registered_base_context()
IntBase = context.get('IntBase') IntBase = context.get('IntBase')
if not IntBase: if not IntBase:

View File

@ -2,14 +2,14 @@
Clase híbrida para números hexadecimales - ADAPTADA AL NUEVO SISTEMA Clase híbrida para números hexadecimales - ADAPTADA AL NUEVO SISTEMA
Ahora usa IntBase como clase base universal Ahora usa IntBase como clase base universal
""" """
from sympy_Base import SympyClassBase from app.sympy_Base import SympyClassBase
import re import re
# Importación dinámica de IntBase desde el registro # Importación dinámica de IntBase desde el registro
def get_intbase_class(): def get_intbase_class():
"""Obtiene la clase IntBase del registro de tipos""" """Obtiene la clase IntBase del registro de tipos"""
try: try:
from type_registry import get_registered_base_context from app.type_registry import get_registered_base_context
context = get_registered_base_context() context = get_registered_base_context()
return context.get('IntBase') return context.get('IntBase')
except ImportError: except ImportError:

View File

@ -1,8 +1,8 @@
""" """
Clase base universal para números en cualquier base - TIPO REGISTRADO Clase base universal para números en cualquier base - TIPO REGISTRADO
""" """
from class_base import ClassBase from app.class_base import ClassBase
from sympy_Base import SympyClassBase from app.sympy_Base import SympyClassBase
import sympy import sympy
import re import re

View File

@ -2,8 +2,8 @@
Clase híbrida para direcciones IPv4 - ADAPTADA AL NUEVO SISTEMA Clase híbrida para direcciones IPv4 - ADAPTADA AL NUEVO SISTEMA
Ahora usa FourBytes como clase base universal para direcciones IP Ahora usa FourBytes como clase base universal para direcciones IP
""" """
from class_base import ClassBase from app.class_base import ClassBase
from sympy_Base import SympyClassBase from app.sympy_Base import SympyClassBase
from typing import Optional, Union, List, Tuple from typing import Optional, Union, List, Tuple
import re import re
import sympy import sympy
@ -12,7 +12,7 @@ import sympy
def get_base_classes(): def get_base_classes():
"""Obtiene las clases base del registro de tipos""" """Obtiene las clases base del registro de tipos"""
try: try:
from type_registry import get_registered_base_context from app.type_registry import get_registered_base_context
context = get_registered_base_context() context = get_registered_base_context()
return context.get('IntBase'), context.get('FourBytes') return context.get('IntBase'), context.get('FourBytes')
except ImportError: except ImportError:

View File

@ -1,7 +1,7 @@
""" """
Clase híbrida para conversión LaTeX - ADAPTADA AL NUEVO SISTEMA 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 sympy
import re import re