Mejora del sistema de autocompletado y ajustes en la interfaz de la calculadora. Se implementa un nuevo popup de autocompletado para variables y funciones, optimizando la navegación y selección. Se ajustan estilos y se mejora la gestión de eventos de teclado. Se actualiza el historial de cálculos con nuevas expresiones y se optimiza la lógica de evaluación.

This commit is contained in:
Miguel 2025-06-08 00:08:26 +02:00
parent 0cbf9dbf79
commit 0629137956
3 changed files with 872 additions and 37 deletions

99
demo_completo.py Normal file
View File

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

View File

@ -1,8 +1,10 @@
x**2 + y**2 = r**2
r=?
a=r*5+5
a=(r*5+5)**2
resultado = f + p
res

View File

@ -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 = `
<div class="equation-type">${type}</div>
<div class="math-content">$$${content}$$</div>
`;
equationDiv.innerHTML = `<div class="math-content">$$${content}$$</div>`;
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()
@ -402,6 +423,10 @@ class HybridCalculatorPySide6(QMainWindow):
"""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"""
try:
@ -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):
@ -767,6 +806,701 @@ class HybridCalculatorPySide6(QMainWindow):
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():
"""Función principal"""