Compare commits
4 Commits
9ba73a9db6
...
01651fc454
Author | SHA1 | Date |
---|---|---|
|
01651fc454 | |
|
639081c0d0 | |
|
e51e6bf1ae | |
|
c219b62857 |
|
@ -1,13 +1,9 @@
|
||||||
|
|
||||||
ex = (t * 8) / w
|
salario=horas*tarifa
|
||||||
|
salario=800
|
||||||
|
tarifa=36
|
||||||
var1 = 2
|
|
||||||
var2 = 4
|
|
||||||
vatt1 = 4
|
|
||||||
|
|
||||||
IP4
|
|
||||||
|
|
||||||
|
horas=?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
@ -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
|
|
@ -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)
|
||||||
|
else:
|
||||||
return Eq(var_symbol, final_value)
|
# Si hay problema con la resolución, devolver simbólico
|
||||||
|
return Eq(var_symbol, solution_value)
|
||||||
else:
|
else:
|
||||||
# Hay variables con valores simbólicos, intentar resolver lo máximo posible
|
# Hay variables con valores simbólicos, devolver ecuación simbólica
|
||||||
# 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
|
# No hay variables sin resolver, es ya un valor final
|
||||||
final_value = self._resolve_iteratively(solution_value)
|
return Eq(var_symbol, solution_value)
|
||||||
|
|
||||||
# Auto-aplicar la solución al sistema solo si es un valor específico
|
|
||||||
if final_value != var_symbol and not str(final_value) in ['True', 'False']:
|
|
||||||
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 ==========
|
||||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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, ""
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
2497
app/main_calc_app.py
2497
app/main_calc_app.py
File diff suppressed because it is too large
Load Diff
68
calc.py
68
calc.py
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue