From 0629137956dd2742cd8fcf5628d0f088f55d4d64 Mon Sep 17 00:00:00 2001 From: Miguel Date: Sun, 8 Jun 2025 00:08:26 +0200 Subject: [PATCH] =?UTF-8?q?Mejora=20del=20sistema=20de=20autocompletado=20?= =?UTF-8?q?y=20ajustes=20en=20la=20interfaz=20de=20la=20calculadora.=20Se?= =?UTF-8?q?=20implementa=20un=20nuevo=20popup=20de=20autocompletado=20para?= =?UTF-8?q?=20variables=20y=20funciones,=20optimizando=20la=20navegaci?= =?UTF-8?q?=C3=B3n=20y=20selecci=C3=B3n.=20Se=20ajustan=20estilos=20y=20se?= =?UTF-8?q?=20mejora=20la=20gesti=C3=B3n=20de=20eventos=20de=20teclado.=20?= =?UTF-8?q?Se=20actualiza=20el=20historial=20de=20c=C3=A1lculos=20con=20nu?= =?UTF-8?q?evas=20expresiones=20y=20se=20optimiza=20la=20l=C3=B3gica=20de?= =?UTF-8?q?=20evaluaci=C3=B3n.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo_completo.py | 99 +++++ hybrid_calc_history.txt | 6 +- main_calc_app_pyside6.py | 804 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 872 insertions(+), 37 deletions(-) create mode 100644 demo_completo.py 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():