diff --git a/demo_completo.py b/demo_completo.py
new file mode 100644
index 0000000..83276fc
--- /dev/null
+++ b/demo_completo.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+"""
+Demostración completa de Calculadora MAV PySide6
+Muestra todas las características implementadas
+"""
+import sys
+
+def show_features():
+ """Muestra todas las características implementadas"""
+ print("🎯 CALCULADORA MAV - TODAS LAS CARACTERÍSTICAS")
+ print("=" * 60)
+ print()
+ print("✅ CARACTERÍSTICAS IMPLEMENTADAS:")
+ print()
+ print("🖥️ DISEÑO MINIMALISTA:")
+ print(" • 3 paneles con splitters redimensionables")
+ print(" • Correspondencia 1:1 línea por línea")
+ print(" • Tema oscuro optimizado")
+ print()
+ print("🧮 MOTOR ALGEBRAICO:")
+ print(" • Contexto se limpia entre ciclos ✅")
+ print(" • Motor original PureAlgebraicEngine preservado")
+ print(" • Tipos personalizados integrados")
+ print()
+ print("📐 PANEL MATHJAX OPTIMIZADO:")
+ print(" • Altura reducida sin texto de tipo ✅")
+ print(" • Renderizado más compacto")
+ print(" • Colores diferenciados por tipo")
+ print()
+ print("💬 AUTOCOMPLETADO RESTAURADO:")
+ print(" • Popup de variables después de 800ms ✅")
+ print(" • Autocompletado con punto (objeto.método)")
+ print(" • Navegación con ↑↓, selección con Tab")
+ print(" • Filtrado en tiempo real")
+ print()
+ print("🎮 CONTROLES:")
+ print(" • Ctrl+Enter / Shift+Enter: Evaluar")
+ print(" • F12: Mostrar/ocultar panel LaTeX")
+ print(" • Punto (.): Autocompletado de métodos")
+ print(" • Tab: Seleccionar autocompletado")
+ print(" • Escape: Cerrar popup")
+ print(" • ↑↓: Navegar autocompletado")
+ print()
+ print("📝 EJEMPLOS PARA PROBAR:")
+ print(" # Este comentario aparece en LaTeX")
+ print(" x**2 + y**2 = r**2")
+ print(" a = 5*x + 3")
+ print(" Matrix([[1, 2], [3, 4]])")
+ print(" solve(x**2 - 4, x)")
+ print(" diff(x**3, x)")
+ print(" # Prueba autocompletado escribiendo 'a.' después de asignar")
+ print()
+
+def run_demo():
+ """Ejecuta la demostración"""
+ show_features()
+
+ try:
+ response = input("¿Ejecutar la aplicación con todas las características? (s/N): ").strip().lower()
+ if response in ['s', 'sí', 'si', 'y', 'yes']:
+ print("\n🚀 Iniciando Calculadora MAV con todas las mejoras...")
+
+ # Verificar dependencias
+ try:
+ from PySide6.QtWidgets import QApplication
+ from PySide6.QtWebEngineWidgets import QWebEngineView
+ print("✅ PySide6 y WebEngine disponibles")
+ except ImportError as e:
+ print(f"❌ Falta dependencia: {e}")
+ print(" Ejecuta: pip install PySide6 PySide6-WebEngine")
+ return 1
+
+ # Iniciar aplicación
+ from main_calc_app_pyside6 import main as run_app
+ print("📝 Aplicación con:")
+ print(" ✅ Contexto limpio entre ciclos")
+ print(" ✅ Panel LaTeX compacto")
+ print(" ✅ Splitters redimensionables")
+ print(" ✅ Autocompletado completo")
+ print()
+
+ run_app()
+
+ else:
+ print("👋 ¡Hasta luego!")
+
+ except KeyboardInterrupt:
+ print("\n\n🚪 Demo cancelada")
+ return 0
+ except Exception as e:
+ print(f"\n❌ Error: {e}")
+ import traceback
+ traceback.print_exc()
+ return 1
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(run_demo())
\ No newline at end of file
diff --git a/hybrid_calc_history.txt b/hybrid_calc_history.txt
index 017ead5..7e4c8a6 100644
--- a/hybrid_calc_history.txt
+++ b/hybrid_calc_history.txt
@@ -1,8 +1,10 @@
x**2 + y**2 = r**2
+
r=?
-a=r*5+5
-
+a=(r*5+5)**2
+resultado = f + p
+res
\ No newline at end of file
diff --git a/main_calc_app_pyside6.py b/main_calc_app_pyside6.py
index 8b9c87f..acd0ef8 100644
--- a/main_calc_app_pyside6.py
+++ b/main_calc_app_pyside6.py
@@ -16,7 +16,8 @@ from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTextEdit, QPlainTextEdit, QSplitter, QPushButton, QLabel,
QFrame, QMenuBar, QStatusBar, QMessageBox, QFileDialog,
- QScrollArea, QSizePolicy
+ QScrollArea, QSizePolicy, QListWidget, QListWidgetItem,
+ QCompleter, QAbstractItemView
)
from PySide6.QtCore import (
Qt, QTimer, QThread, QObject, Signal, QUrl, QSize
@@ -152,33 +153,27 @@ class MathJaxPanel(QWebEngineView):
font-family: 'Consolas', 'Courier New', monospace;
background-color: #1e1e1e;
color: #d4d4d4;
- margin: 8px;
+ margin: 4px;
padding: 0;
- line-height: 1.4;
- font-size: 14px;
+ line-height: 1.2;
+ font-size: 13px;
}
.equation {
- margin: 6px 0;
- padding: 8px;
+ margin: 2px 0;
+ padding: 4px 6px;
background-color: #2d2d30;
- border-left: 3px solid #4fc3f7;
- border-radius: 3px;
- transition: all 0.2s ease;
+ border-left: 2px solid #4fc3f7;
+ border-radius: 2px;
+ transition: all 0.15s ease;
}
.equation:hover {
background-color: #363636;
border-left-color: #82aaff;
}
- .equation-type {
- font-size: 0.75em;
- color: #4fc3f7;
- margin-bottom: 4px;
- text-transform: uppercase;
- font-weight: bold;
- opacity: 0.8;
- }
.math-content {
- font-size: 1.0em;
+ font-size: 0.9em;
+ margin: 0;
+ padding: 0;
}
.assignment { border-left-color: #c3e88d; }
.assignment .equation-type { color: #c3e88d; }
@@ -218,10 +213,7 @@ class MathJaxPanel(QWebEngineView):
const equationDiv = document.createElement('div');
equationDiv.className = 'equation ' + type;
- equationDiv.innerHTML = `
-
${type}
- $$${content}$$
- `;
+ equationDiv.innerHTML = `$$${content}$$
`;
document.getElementById('equations-container').appendChild(equationDiv);
@@ -287,6 +279,32 @@ class HybridCalculatorPySide6(QMainWindow):
self._debounce_timer.setSingleShot(True)
self._debounce_timer.timeout.connect(self._evaluate_and_update)
+ # ========== VARIABLES DE AUTOCOMPLETADO COMPLETO (ADAPTADO DE TKINTER) ==========
+ # Popup principal de autocompletado
+ self._autocomplete_popup = None
+ self._autocomplete_listbox = None
+ self._autocomplete_active = False
+ self._autocomplete_suggestions = []
+ self._autocomplete_filter_text = ""
+ self._autocomplete_trigger_pos = None
+ self._popup_disabled_until_next_dot = False
+
+ # Popup de variables (timer-based)
+ self._variable_popup_active = False
+ self._variable_popup_job = None
+ self._last_input_change = 0
+ self._last_navigation_time = 0
+
+ # Estado de filtrado y navegación
+ self._current_suggestions = []
+ self._is_global_popup = False
+ self._selected_index = 0
+
+ # Timers para autocompletado
+ self._variable_popup_timer = QTimer()
+ self._variable_popup_timer.setSingleShot(True)
+ self._variable_popup_timer.timeout.connect(self._show_variable_autocomplete)
+
# ========== CONFIGURAR HELPERS DINÁMICOS (COMO EN ORIGINAL) ==========
self._setup_dynamic_helpers()
@@ -324,14 +342,9 @@ class HybridCalculatorPySide6(QMainWindow):
self.setGeometry(100, 100, 1400, 800)
self.setStyleSheet(self.get_minimal_dark_theme())
- # 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(1)
+ # ========== SPLITTER PRINCIPAL CON 3 PANELES ==========
+ main_splitter = QSplitter(Qt.Horizontal)
+ self.setCentralWidget(main_splitter)
# ========== PANEL 1: ENTRADA ==========
self.input_text = LineNumberedPlainTextEdit()
@@ -339,6 +352,9 @@ class HybridCalculatorPySide6(QMainWindow):
self.input_text.setPlaceholderText("Introduce expresiones matemáticas...")
self.input_text.textChanged.connect(self._on_input_changed)
+ # Configurar eventos de teclado para autocompletado
+ self.input_text.keyPressEvent = self._handle_key_press
+
# Configurar highlighter
self.highlighter = MathInputHighlighter(self.input_text.document())
@@ -358,11 +374,16 @@ class HybridCalculatorPySide6(QMainWindow):
# ========== PANEL 3: MATHJAX (COLAPSABLE) ==========
self.latex_panel = MathJaxPanel()
- # ========== LAYOUT DE 3 PANELES ==========
- # Paneles 1 y 2 tienen mismo ancho, panel 3 es más estrecho
- main_layout.addWidget(self.input_text, 1)
- main_layout.addWidget(self.output_text, 1)
- main_layout.addWidget(self.latex_panel, 0)
+ # ========== AÑADIR PANELES AL SPLITTER ==========
+ main_splitter.addWidget(self.input_text)
+ main_splitter.addWidget(self.output_text)
+ main_splitter.addWidget(self.latex_panel)
+
+ # Configurar tamaños iniciales y política de redimensionado
+ main_splitter.setSizes([400, 400, 300]) # Ancho inicial de cada panel
+ main_splitter.setStretchFactor(0, 1) # Panel entrada: estirable
+ main_splitter.setStretchFactor(1, 1) # Panel salida: estirable
+ main_splitter.setStretchFactor(2, 0) # Panel LaTeX: tamaño fijo
# ========== CONFIGURAR TAGS DE SALIDA ==========
self.setup_output_tags()
@@ -401,6 +422,10 @@ class HybridCalculatorPySide6(QMainWindow):
def _on_input_changed(self):
"""Maneja cambios en la entrada con debounce - COMO EN ORIGINAL"""
self._debounce_timer.start(300) # 300ms delay
+
+ # Programar autocompletado de variables si no hay popup activo
+ if not self._autocomplete_active and not self._variable_popup_active:
+ self._variable_popup_timer.start(800) # 800ms delay para variables
def _evaluate_and_update(self):
"""Evalúa la entrada y actualiza la salida - USANDO MOTOR ORIGINAL"""
@@ -419,6 +444,9 @@ class HybridCalculatorPySide6(QMainWindow):
def _evaluate_lines(self, lines: List[str]):
"""Evalúa líneas usando el motor original - SIN CAMBIOS EN LÓGICA"""
try:
+ # ========== LIMPIAR CONTEXTO DEL MOTOR (COMO EN ORIGINAL) ==========
+ self.engine.clear_context()
+
# Limpiar panel LaTeX
self.latex_panel.clear_equations()
@@ -598,6 +626,17 @@ class HybridCalculatorPySide6(QMainWindow):
color: white;
border: none;
}
+ QSplitter::handle {
+ background-color: #4fc3f7;
+ border-radius: 1px;
+ }
+ QSplitter::handle:horizontal {
+ width: 3px;
+ margin: 2px 0;
+ }
+ QSplitter::handle:horizontal:hover {
+ background-color: #82aaff;
+ }
"""
def setup_shortcuts(self):
@@ -766,6 +805,701 @@ class HybridCalculatorPySide6(QMainWindow):
self.save_settings()
self.save_history()
event.accept()
+
+ # ========== SISTEMA DE AUTOCOMPLETADO ==========
+
+ def _handle_key_press(self, event):
+ """Maneja eventos de teclado para autocompletado - SISTEMA COMPLETO DE TKINTER"""
+ # Procesar navegación en popup ANTES de insertar el carácter
+ if (self._autocomplete_active or self._variable_popup_active):
+ if event.key() == Qt.Key_Up:
+ self._handle_arrow_key(-1)
+ event.accept()
+ return
+ elif event.key() == Qt.Key_Down:
+ self._handle_arrow_key(1)
+ event.accept()
+ return
+ elif event.key() == Qt.Key_Tab:
+ self._handle_tab_key()
+ event.accept()
+ return
+ elif event.key() == Qt.Key_Escape:
+ self._handle_escape_key()
+ event.accept()
+ return
+
+ # Detectar backspace para cerrar popup de funciones si se borra el punto
+ if event.key() == Qt.Key_Backspace and self._autocomplete_active:
+ QPlainTextEdit.keyPressEvent(self.input_text, event)
+ QTimer.singleShot(1, self._check_dot_removal)
+ return
+
+ # Llamar al método original para insertar el carácter
+ QPlainTextEdit.keyPressEvent(self.input_text, event)
+
+ # Procesar autocompletado DESPUÉS de insertar el carácter
+ self._on_key_release(event)
+
+ def _on_key_release(self, event):
+ """Maneja eventos después de insertar carácter - SISTEMA COMPLETO DE TKINTER"""
+ # Cancelar timer de variables si existe
+ if hasattr(self, '_variable_popup_job') and self._variable_popup_job:
+ self._variable_popup_timer.stop()
+ self._variable_popup_job = None
+
+ # Verificar si acabamos de navegar (evitar filtrado inmediato)
+ import time
+ just_navigated = (time.time() - self._last_navigation_time) < 0.1
+
+ # Manejar autocompletado con punto
+ if event.text() == '.' and not self._popup_disabled_until_next_dot:
+ # Cerrar popup de variables si está activo
+ if self._variable_popup_active:
+ self._close_autocomplete_popup()
+
+ self._handle_dot_autocomplete()
+
+ # Filtrar autocompletado si está activo (pero no si acabamos de navegar)
+ elif self._autocomplete_active and event.text() and event.text().isprintable() and not just_navigated:
+ self._filter_autocomplete()
+
+ # Marcar tiempo del último cambio de input
+ if event.text() and event.text().isprintable():
+ self._last_input_change = time.time()
+
+ # Programar autocompletado de variables (nuevo sistema)
+ if not self._autocomplete_active and not self._popup_disabled_until_next_dot:
+ self._schedule_variable_autocomplete()
+
+ def _schedule_variable_autocomplete(self):
+ """Programa el autocompletado de variables mientras se escribe"""
+ if self._autocomplete_active or self._popup_disabled_until_next_dot:
+ return
+
+ # Verificar que estemos escribiendo (no solo navegando)
+ cursor = self.input_text.textCursor()
+ cursor.select(QTextCursor.LineUnderCursor)
+ current_line = cursor.selectedText().strip()
+
+ if not current_line or current_line.endswith('.'):
+ return
+
+ # Programar para 800ms después
+ self._variable_popup_timer.start(800)
+
+ def _handle_dot_autocomplete(self):
+ """Maneja el autocompletado cuando se escribe un punto - VERSIÓN COMPLETA DE TKINTER"""
+ self._close_autocomplete_popup()
+
+ # Obtener posición del cursor y línea actual
+ cursor = self.input_text.textCursor()
+ cursor_pos = cursor.position()
+ cursor.select(QTextCursor.LineUnderCursor)
+ line_text = cursor.selectedText()
+
+ # Obtener posición en la línea
+ line_start = cursor.selectionStart()
+ char_pos_in_line = cursor_pos - line_start
+
+ if char_pos_in_line == 0:
+ return
+
+ # Guardar posición donde se activó el autocompletado
+ self._autocomplete_trigger_pos = cursor_pos
+ self._autocomplete_filter_text = ""
+
+ # Obtener texto antes del punto
+ dot_char_index_in_line = char_pos_in_line - 1
+ text_on_line_up_to_dot = line_text[:dot_char_index_in_line]
+ stripped_text_before_dot = text_on_line_up_to_dot.strip()
+
+ # 1. Determinar si es un popup GLOBAL (usando contexto dinámico)
+ if not stripped_text_before_dot:
+ self.logger.debug("Punto en línea vacía. Ofreciendo sugerencias globales.")
+ suggestions = []
+
+ # Usar contexto dinámico del registro
+ try:
+ from type_registry import get_registered_base_context
+ dynamic_context = get_registered_base_context()
+
+ for name, class_or_func in dynamic_context.items():
+ if name[0].isupper(): # Prioritizar nombres capitalizados
+ hint = f"Tipo o función: {name}"
+ if hasattr(class_or_func, '__doc__') and class_or_func.__doc__:
+ first_line_doc = class_or_func.__doc__.strip().split('\n')[0]
+ hint = f"{name} - {first_line_doc}"
+ elif hasattr(class_or_func, 'Helper'):
+ try:
+ helper_text = class_or_func.Helper(name)
+ if helper_text:
+ hint = helper_text.split('\n')[0]
+ except Exception:
+ pass
+ suggestions.append((name, hint))
+
+ except Exception as e:
+ self.logger.debug(f"Error obteniendo contexto dinámico: {e}")
+ suggestions = [("sin", "Función seno"), ("cos", "Función coseno")]
+
+ # Añadir funciones de SympyHelper
+ try:
+ from sympy_helper import SympyTools as SympyHelper
+ sympy_functions = SympyHelper.PopupFunctionList()
+ if sympy_functions:
+ current_suggestion_names = {s[0] for s in suggestions}
+ for fname, fhint in sympy_functions:
+ if fname not in current_suggestion_names:
+ suggestions.append((fname, fhint))
+ except Exception as e:
+ self.logger.debug(f"Error llamando SympyHelper.PopupFunctionList() para global: {e}")
+
+ if suggestions:
+ suggestions.sort(key=lambda x: x[0])
+ self._show_autocomplete_popup(suggestions, is_global_popup=True)
+ return
+
+ # 2. Es un popup de OBJETO
+ import re
+ obj_expr_str_candidate = ""
+ 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, stripped_text_before_dot)
+
+ if match:
+ obj_expr_str_candidate = match.group(1).replace(" ", "")
+ else:
+ obj_expr_str_candidate = stripped_text_before_dot
+ if not obj_expr_str_candidate or \
+ not re.match(r"^[a-zA-Z_0-9\(\)\[\]\.\"\'\+\-\*\/ ]*$", obj_expr_str_candidate) or \
+ obj_expr_str_candidate.endswith(("+", "-", "*", "/", "(", ",")):
+ self.logger.debug(f"Expresión extraída '{obj_expr_str_candidate}' no es válida para autocompletado.")
+ return
+
+ obj_expr_str = obj_expr_str_candidate
+ self.logger.debug(f"Autocompletado para objeto. Extraído: '{obj_expr_str}'")
+
+ if not obj_expr_str:
+ return
+
+ # 3. Caso especial para el módulo sympy
+ if obj_expr_str == "sympy":
+ try:
+ from sympy_helper import SympyTools as SympyHelper
+ methods = SympyHelper.PopupFunctionList()
+ if methods:
+ self._show_autocomplete_popup(methods, is_global_popup=False)
+ else:
+ self.logger.debug(f"SympyHelper.PopupFunctionList devolvió métodos vacíos.")
+ except Exception as e:
+ self.logger.debug(f"Error llamando SympyHelper.PopupFunctionList(): {e}")
+ return
+
+ # 4. Preprocesar con BracketParser si es necesario
+ if '[' in obj_expr_str:
+ original_for_debug = obj_expr_str
+ obj_expr_str = self.engine.parser._transform_brackets(obj_expr_str)
+ if obj_expr_str != original_for_debug and self.debug:
+ self.logger.debug(f"Preprocesado por BracketParser: '{original_for_debug}' -> '{obj_expr_str}'")
+
+ # 5. Evaluar la expresión del objeto
+ eval_context = self.engine._get_full_context()
+ obj = None
+ try:
+ if not obj_expr_str.strip():
+ return
+ self.logger.debug(f"Intentando evaluar: '{obj_expr_str}'")
+ obj = eval(obj_expr_str, eval_context)
+ self.logger.debug(f"Evaluación exitosa. Objeto: {type(obj)}, Valor: {obj}")
+ except Exception as e:
+ self.logger.debug(f"Error evaluando expresión de objeto '{obj_expr_str}': {e}")
+ return
+
+ # 6. Mostrar popup de autocompletado para el objeto
+ if obj is not None and hasattr(obj, 'PopupFunctionList'):
+ methods = obj.PopupFunctionList()
+ if methods:
+ self._show_autocomplete_popup(methods, is_global_popup=False)
+
+ def _check_dot_removal(self):
+ """Verifica si se va a borrar el punto que activó el autocompletado"""
+ try:
+ cursor = self.input_text.textCursor()
+ cursor_pos = cursor.position()
+
+ if cursor_pos > 0:
+ # Obtener el carácter anterior al cursor
+ cursor.setPosition(cursor_pos - 1)
+ cursor.setPosition(cursor_pos, QTextCursor.KeepAnchor)
+ prev_char = cursor.selectedText()
+
+ # Si el carácter anterior es un punto, cerrar el popup
+ if prev_char == '.':
+ self._close_autocomplete_popup()
+
+ except Exception:
+ # Error de posición, cerrar popup por seguridad
+ self._close_autocomplete_popup()
+
+ def _handle_arrow_key(self, direction):
+ """Maneja las teclas de flecha cuando el popup está activo"""
+ if not self._autocomplete_active and not self._variable_popup_active:
+ return
+
+ self._navigate_autocomplete_improved(direction)
+
+ # Marcar tiempo de navegación para evitar filtrado inmediato
+ import time
+ self._last_navigation_time = time.time()
+
+ def _handle_tab_key(self):
+ """Maneja la tecla TAB para seleccionar del popup"""
+ if self._autocomplete_active or self._variable_popup_active:
+ self._select_autocomplete()
+
+ def _handle_escape_key(self):
+ """Maneja la tecla ESC para cerrar popup"""
+ if self._autocomplete_active or self._variable_popup_active:
+ self._close_autocomplete_popup()
+ if self._autocomplete_active:
+ self._popup_disabled_until_next_dot = True
+
+ def _navigate_autocomplete_improved(self, direction):
+ """Navegación mejorada en el popup de autocompletado"""
+ if not self._autocomplete_listbox:
+ return
+
+ current_row = self._autocomplete_listbox.currentRow()
+ row_count = self._autocomplete_listbox.count()
+
+ if row_count == 0:
+ return
+
+ if current_row == -1:
+ new_row = 0 if direction == 1 else row_count - 1
+ else:
+ new_row = (current_row + direction) % row_count # Navegación circular
+
+ # Actualizar selección
+ self._autocomplete_listbox.setCurrentRow(new_row)
+ self._selected_index = new_row
+
+ def _show_variable_autocomplete(self):
+ """Muestra autocompletado de variables disponibles - VERSIÓN COMPLETA DE TKINTER"""
+ if self._autocomplete_active or self._variable_popup_active:
+ return
+
+ # Verificar que aún estemos en una línea válida
+ cursor = self.input_text.textCursor()
+ cursor.select(QTextCursor.LineUnderCursor)
+ current_line = cursor.selectedText().strip()
+
+ if not current_line or current_line.endswith('.'):
+ return
+
+ # Obtener variables del contexto
+ try:
+ context = self.engine._get_full_context()
+ symbol_table = getattr(self.engine, 'symbol_table', {})
+
+ variables = []
+
+ # Filtrar variables (excluir funciones built-in y módulos)
+ for name, value in {**context, **symbol_table}.items():
+ is_underscore = name.startswith('_')
+ is_callable = callable(value)
+ has_module = hasattr(value, '__module__')
+ is_excluded = name in ['sympy', 'math', 'numpy', 'plt', 'builtins']
+
+ # Permitir variables de SymPy específicamente
+ is_sympy_symbol = hasattr(value, 'is_symbol') or 'sympy' in str(type(value)).lower()
+
+ if (not is_underscore and
+ not is_callable and
+ (not has_module or is_sympy_symbol) and
+ not is_excluded):
+
+ # Crear descripción del valor (más corta)
+ 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])
+
+ # Obtener texto actual para filtrado
+ words = current_line.split()
+
+ if words:
+ last_word = words[-1]
+
+ # Filtrar variables que empiecen con la palabra actual
+ # Y que la palabra actual no sea igual a una variable existente
+ filtered_vars = [
+ (name, value) for name, value in variables
+ if name.lower().startswith(last_word.lower()) and name != last_word
+ ]
+
+ if filtered_vars:
+ # Mostrar popup de variables
+ self._show_variable_popup(filtered_vars)
+
+ except Exception as e:
+ self.logger.debug(f"Error obteniendo variables para autocompletado: {e}")
+
+ def _show_autocomplete_popup(self, suggestions, is_global_popup=False):
+ """Muestra popup de autocompletado modeless con filtrado - SISTEMA COMPLETO DE TKINTER"""
+ self._close_autocomplete_popup()
+
+ if not suggestions:
+ return
+
+ # Guardar sugerencias originales y estado
+ self._current_suggestions = suggestions.copy()
+ self._is_global_popup = is_global_popup
+ self._autocomplete_active = True
+
+ # Crear popup modeless
+ self._autocomplete_popup = QWidget(self, Qt.Popup | Qt.FramelessWindowHint)
+ self._autocomplete_popup.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;
+ }
+ QListWidget::item {
+ padding: 3px 8px;
+ border: none;
+ }
+ QListWidget::item:selected {
+ background-color: #007acc;
+ color: white;
+ }
+ """)
+
+ layout = QVBoxLayout(self._autocomplete_popup)
+ layout.setContentsMargins(0, 0, 0, 0)
+
+ # Crear listbox con nombre correcto para compatibilidad
+ self._autocomplete_listbox = QListWidget()
+ self._autocomplete_listbox.setMaximumHeight(150)
+
+ # Llenar con sugerencias iniciales
+ self._populate_listbox(suggestions)
+
+ if self._autocomplete_listbox.count() > 0:
+ self._autocomplete_listbox.setCurrentRow(0)
+ self._selected_index = 0
+
+ # Bindings solo para el listbox (no roba focus del input)
+ self._autocomplete_listbox.itemDoubleClicked.connect(self._select_autocomplete)
+ layout.addWidget(self._autocomplete_listbox)
+
+ # Calcular tamaño
+ self._resize_popup()
+
+ # Posicionar popup
+ cursor_rect = self.input_text.cursorRect()
+ global_pos = self.input_text.mapToGlobal(cursor_rect.bottomLeft())
+ self._autocomplete_popup.move(global_pos)
+ self._autocomplete_popup.show()
+
+ def _populate_listbox(self, suggestions):
+ """Llena el listbox con las sugerencias"""
+ if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox:
+ return
+
+ self._autocomplete_listbox.clear()
+ for name, hint in suggestions:
+ self._autocomplete_listbox.addItem(f"{name} — {hint}")
+
+ def _resize_popup(self):
+ """Redimensiona el popup según el contenido"""
+ if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox:
+ return
+
+ size = self._autocomplete_listbox.count()
+ if size == 0:
+ return
+
+ # Calcular dimensiones
+ max_len = 20
+ for i in range(size):
+ item_text = self._autocomplete_listbox.item(i).text()
+ max_len = max(max_len, len(item_text))
+
+ width = min(max_len * 8, 600) # Aproximación de ancho en píxeles
+ height = min(size * 20, 200) # Altura por ítem
+
+ self._autocomplete_popup.setFixedSize(width, height)
+
+ def _show_variable_popup(self, variables):
+ """Muestra popup de variables con estilo menos invasivo - VERSIÓN COMPLETA DE TKINTER"""
+ self._close_autocomplete_popup()
+
+ if not variables:
+ return
+
+ # Marcar como popup de variables activo
+ self._variable_popup_active = True
+ self._autocomplete_active = False # No es el popup principal
+
+ # Crear popup más discreto
+ self._autocomplete_popup = QWidget(self, Qt.Popup | Qt.FramelessWindowHint)
+ self._autocomplete_popup.setStyleSheet("""
+ QWidget {
+ background-color: #2d2d30;
+ border: 1px solid #c3e88d;
+ border-radius: 4px;
+ }
+ QListWidget {
+ background-color: #2d2d30;
+ color: #c9c9c9;
+ border: none;
+ font-family: 'Consolas';
+ font-size: 10px;
+ }
+ QListWidget::item {
+ padding: 2px 6px;
+ border: none;
+ }
+ QListWidget::item:selected {
+ background-color: #4a4a4a;
+ color: #ffffff;
+ }
+ """)
+
+ layout = QVBoxLayout(self._autocomplete_popup)
+ layout.setContentsMargins(0, 0, 0, 0)
+
+ # Usar nombre correcto para compatibilidad
+ self._autocomplete_listbox = QListWidget()
+ self._autocomplete_listbox.setMaximumHeight(120)
+
+ # Llenar con variables (formato más simple)
+ for name, value in variables:
+ self._autocomplete_listbox.addItem(f"{name} = {value}")
+
+ if variables:
+ self._autocomplete_listbox.setCurrentRow(0)
+ self._selected_index = 0
+
+ # Solo doble-click para seleccionar (más discreto)
+ self._autocomplete_listbox.itemDoubleClicked.connect(self._select_variable)
+
+ layout.addWidget(self._autocomplete_listbox)
+
+ # Calcular tamaño más compacto
+ max_len = 15
+ for name, value in variables:
+ item_text = f"{name} = {value}"
+ max_len = max(max_len, len(item_text))
+
+ width = min((max_len + 3) * 8, 320)
+ height = min(len(variables) * 18, 100)
+
+ self._autocomplete_popup.setFixedSize(width, height)
+
+ # Posicionar popup
+ cursor_rect = self.input_text.cursorRect()
+ global_pos = self.input_text.mapToGlobal(cursor_rect.bottomLeft())
+ self._autocomplete_popup.move(global_pos)
+ self._autocomplete_popup.show()
+ else:
+ self._close_autocomplete_popup()
+
+ def _select_variable(self):
+ """Selecciona una variable del popup de variables"""
+ if not self._autocomplete_listbox:
+ return
+
+ current_item = self._autocomplete_listbox.currentItem()
+ if not current_item:
+ return
+
+ # Obtener nombre de variable
+ selected_text = current_item.text()
+ var_name = selected_text.split(" = ")[0].strip()
+
+ # Obtener posición de la palabra actual
+ cursor = self.input_text.textCursor()
+ cursor.select(QTextCursor.LineUnderCursor)
+ current_line = cursor.selectedText()
+
+ # Encontrar la palabra que estamos completando
+ words = current_line.split()
+ if words:
+ last_word = words[-1]
+ # Buscar posición de la última palabra en la línea
+ word_start_pos = current_line.rfind(last_word)
+ if word_start_pos >= 0:
+ # Calcular posición absoluta
+ line_start = cursor.selectionStart()
+ abs_word_start = line_start + word_start_pos
+ abs_word_end = abs_word_start + len(last_word)
+
+ # Reemplazar la palabra parcial con la variable completa
+ cursor.setPosition(abs_word_start)
+ cursor.setPosition(abs_word_end, QTextCursor.KeepAnchor)
+ cursor.insertText(var_name)
+ self.input_text.setTextCursor(cursor)
+
+ # Cerrar popup
+ self._close_autocomplete_popup()
+
+ def _select_autocomplete(self):
+ """Selecciona el item actual del autocompletado - VERSIÓN COMPLETA DE TKINTER"""
+ if not self._autocomplete_listbox:
+ return
+
+ current_item = self._autocomplete_listbox.currentItem()
+ if not current_item:
+ return
+
+ # Obtener texto seleccionado
+ selected_text = current_item.text()
+
+ # Determinar si es popup de variables o funciones
+ is_variable_popup = self._variable_popup_active
+
+ if is_variable_popup:
+ # Para popup de variables, usar el método específico
+ self._select_variable()
+ return
+
+ # Para popup de funciones, extraer nombre
+ item_name = selected_text.split(" —")[0].strip()
+ is_variable = " = " in selected_text # Nuevo formato de variables
+
+ # Insertar en la posición correcta
+ if hasattr(self, '_is_global_popup') and self._is_global_popup:
+ # Para popup global, reemplazar el punto con la función
+ cursor = self.input_text.textCursor()
+ cursor_pos = cursor.position()
+
+ # Buscar el punto anterior
+ cursor.movePosition(QTextCursor.StartOfLine)
+ line_start = cursor.position()
+ line_text = cursor.block().text()
+ dot_pos = line_text.rfind('.', 0, cursor_pos - line_start)
+
+ if dot_pos >= 0:
+ # Eliminar punto y texto filtrado
+ abs_dot_pos = line_start + dot_pos
+ cursor.setPosition(abs_dot_pos)
+ cursor.setPosition(cursor_pos, QTextCursor.KeepAnchor)
+
+ # Insertar función (no variables en popup global)
+ insert_text = item_name + "()"
+ cursor.insertText(insert_text)
+
+ # Posicionar cursor dentro de los paréntesis
+ new_pos = abs_dot_pos + len(item_name) + 1
+ cursor.setPosition(new_pos)
+ self.input_text.setTextCursor(cursor)
+ else:
+ # Para popup de objeto/variables
+ cursor = self.input_text.textCursor()
+ current_pos = cursor.position()
+
+ # Eliminar texto filtrado si existe
+ if self._autocomplete_filter_text:
+ start_pos = current_pos - len(self._autocomplete_filter_text)
+ cursor.setPosition(start_pos)
+ cursor.setPosition(current_pos, QTextCursor.KeepAnchor)
+ cursor.removeSelectedText()
+ current_pos = start_pos
+
+ # Insertar según el tipo
+ if is_variable:
+ # Solo insertar el nombre de la variable
+ insert_text = item_name
+ cursor.insertText(insert_text)
+ cursor.setPosition(current_pos + len(item_name))
+ else:
+ # Insertar método con paréntesis
+ insert_text = item_name + "()"
+ cursor.insertText(insert_text)
+ cursor.setPosition(current_pos + len(item_name) + 1)
+
+ self.input_text.setTextCursor(cursor)
+
+ # Cerrar popup y enfocar input
+ self._close_autocomplete_popup()
+
+ def _filter_autocomplete(self):
+ """Filtra las sugerencias basándose en el texto escrito después del punto"""
+ if not self._autocomplete_active or not self._autocomplete_trigger_pos:
+ return
+
+ # Obtener texto escrito después del punto
+ cursor = self.input_text.textCursor()
+ current_pos = cursor.position()
+
+ try:
+ # Calcular texto filtrado
+ if current_pos > self._autocomplete_trigger_pos:
+ cursor.setPosition(self._autocomplete_trigger_pos)
+ cursor.setPosition(current_pos, QTextCursor.KeepAnchor)
+ filter_text = cursor.selectedText().lower()
+ self._autocomplete_filter_text = filter_text
+ else:
+ self._autocomplete_filter_text = ""
+ except Exception:
+ # Posición inválida, cerrar popup
+ self._close_autocomplete_popup()
+ return
+
+ # Filtrar sugerencias
+ filtered = []
+ for name, hint in self._current_suggestions:
+ if name.lower().startswith(self._autocomplete_filter_text):
+ filtered.append((name, hint))
+
+ if filtered:
+ # Actualizar listbox con sugerencias filtradas
+ self._populate_listbox(filtered)
+ if self._autocomplete_listbox.count() > 0:
+ self._autocomplete_listbox.setCurrentRow(0)
+ self._selected_index = 0
+ self._resize_popup()
+ else:
+ # No hay coincidencias, cerrar popup
+ self._close_autocomplete_popup()
+
+ def _close_autocomplete_popup(self):
+ """Cierra popup de autocomplete y resetea estado - VERSIÓN COMPLETA DE TKINTER"""
+ if self._autocomplete_popup:
+ try:
+ self._autocomplete_popup.close()
+ self._autocomplete_popup.deleteLater()
+ except:
+ pass
+ self._autocomplete_popup = None
+
+ if hasattr(self, '_autocomplete_listbox'):
+ self._autocomplete_listbox = None
+
+ # Resetear estado del autocompletado
+ self._autocomplete_active = False
+ self._variable_popup_active = False
+ self._autocomplete_trigger_pos = None
+ self._autocomplete_filter_text = ""
+ self._current_suggestions = []
+ self._selected_index = 0
+
+ # Detener timers
+ if hasattr(self, '_variable_popup_timer'):
+ self._variable_popup_timer.stop()
def main():