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:
parent
0cbf9dbf79
commit
0629137956
|
@ -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())
|
|
@ -1,8 +1,10 @@
|
||||||
x**2 + y**2 = r**2
|
x**2 + y**2 = r**2
|
||||||
|
|
||||||
|
|
||||||
r=?
|
r=?
|
||||||
|
|
||||||
a=r*5+5
|
a=(r*5+5)**2
|
||||||
|
|
||||||
|
|
||||||
|
resultado = f + p
|
||||||
|
|
||||||
|
res
|
|
@ -16,7 +16,8 @@ from PySide6.QtWidgets import (
|
||||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QTextEdit, QPlainTextEdit, QSplitter, QPushButton, QLabel,
|
QTextEdit, QPlainTextEdit, QSplitter, QPushButton, QLabel,
|
||||||
QFrame, QMenuBar, QStatusBar, QMessageBox, QFileDialog,
|
QFrame, QMenuBar, QStatusBar, QMessageBox, QFileDialog,
|
||||||
QScrollArea, QSizePolicy
|
QScrollArea, QSizePolicy, QListWidget, QListWidgetItem,
|
||||||
|
QCompleter, QAbstractItemView
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import (
|
from PySide6.QtCore import (
|
||||||
Qt, QTimer, QThread, QObject, Signal, QUrl, QSize
|
Qt, QTimer, QThread, QObject, Signal, QUrl, QSize
|
||||||
|
@ -152,33 +153,27 @@ class MathJaxPanel(QWebEngineView):
|
||||||
font-family: 'Consolas', 'Courier New', monospace;
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
margin: 8px;
|
margin: 4px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
line-height: 1.4;
|
line-height: 1.2;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.equation {
|
.equation {
|
||||||
margin: 6px 0;
|
margin: 2px 0;
|
||||||
padding: 8px;
|
padding: 4px 6px;
|
||||||
background-color: #2d2d30;
|
background-color: #2d2d30;
|
||||||
border-left: 3px solid #4fc3f7;
|
border-left: 2px solid #4fc3f7;
|
||||||
border-radius: 3px;
|
border-radius: 2px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
.equation:hover {
|
.equation:hover {
|
||||||
background-color: #363636;
|
background-color: #363636;
|
||||||
border-left-color: #82aaff;
|
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 {
|
.math-content {
|
||||||
font-size: 1.0em;
|
font-size: 0.9em;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
.assignment { border-left-color: #c3e88d; }
|
.assignment { border-left-color: #c3e88d; }
|
||||||
.assignment .equation-type { color: #c3e88d; }
|
.assignment .equation-type { color: #c3e88d; }
|
||||||
|
@ -218,10 +213,7 @@ class MathJaxPanel(QWebEngineView):
|
||||||
|
|
||||||
const equationDiv = document.createElement('div');
|
const equationDiv = document.createElement('div');
|
||||||
equationDiv.className = 'equation ' + type;
|
equationDiv.className = 'equation ' + type;
|
||||||
equationDiv.innerHTML = `
|
equationDiv.innerHTML = `<div class="math-content">$$${content}$$</div>`;
|
||||||
<div class="equation-type">${type}</div>
|
|
||||||
<div class="math-content">$$${content}$$</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('equations-container').appendChild(equationDiv);
|
document.getElementById('equations-container').appendChild(equationDiv);
|
||||||
|
|
||||||
|
@ -287,6 +279,32 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
self._debounce_timer.setSingleShot(True)
|
self._debounce_timer.setSingleShot(True)
|
||||||
self._debounce_timer.timeout.connect(self._evaluate_and_update)
|
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) ==========
|
# ========== CONFIGURAR HELPERS DINÁMICOS (COMO EN ORIGINAL) ==========
|
||||||
self._setup_dynamic_helpers()
|
self._setup_dynamic_helpers()
|
||||||
|
|
||||||
|
@ -324,14 +342,9 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
self.setGeometry(100, 100, 1400, 800)
|
self.setGeometry(100, 100, 1400, 800)
|
||||||
self.setStyleSheet(self.get_minimal_dark_theme())
|
self.setStyleSheet(self.get_minimal_dark_theme())
|
||||||
|
|
||||||
# Widget central
|
# ========== SPLITTER PRINCIPAL CON 3 PANELES ==========
|
||||||
central_widget = QWidget()
|
main_splitter = QSplitter(Qt.Horizontal)
|
||||||
self.setCentralWidget(central_widget)
|
self.setCentralWidget(main_splitter)
|
||||||
|
|
||||||
# Layout principal horizontal
|
|
||||||
main_layout = QHBoxLayout(central_widget)
|
|
||||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
main_layout.setSpacing(1)
|
|
||||||
|
|
||||||
# ========== PANEL 1: ENTRADA ==========
|
# ========== PANEL 1: ENTRADA ==========
|
||||||
self.input_text = LineNumberedPlainTextEdit()
|
self.input_text = LineNumberedPlainTextEdit()
|
||||||
|
@ -339,6 +352,9 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
self.input_text.setPlaceholderText("Introduce expresiones matemáticas...")
|
self.input_text.setPlaceholderText("Introduce expresiones matemáticas...")
|
||||||
self.input_text.textChanged.connect(self._on_input_changed)
|
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
|
# Configurar highlighter
|
||||||
self.highlighter = MathInputHighlighter(self.input_text.document())
|
self.highlighter = MathInputHighlighter(self.input_text.document())
|
||||||
|
|
||||||
|
@ -358,11 +374,16 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
# ========== PANEL 3: MATHJAX (COLAPSABLE) ==========
|
# ========== PANEL 3: MATHJAX (COLAPSABLE) ==========
|
||||||
self.latex_panel = MathJaxPanel()
|
self.latex_panel = MathJaxPanel()
|
||||||
|
|
||||||
# ========== LAYOUT DE 3 PANELES ==========
|
# ========== AÑADIR PANELES AL SPLITTER ==========
|
||||||
# Paneles 1 y 2 tienen mismo ancho, panel 3 es más estrecho
|
main_splitter.addWidget(self.input_text)
|
||||||
main_layout.addWidget(self.input_text, 1)
|
main_splitter.addWidget(self.output_text)
|
||||||
main_layout.addWidget(self.output_text, 1)
|
main_splitter.addWidget(self.latex_panel)
|
||||||
main_layout.addWidget(self.latex_panel, 0)
|
|
||||||
|
# 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 ==========
|
# ========== CONFIGURAR TAGS DE SALIDA ==========
|
||||||
self.setup_output_tags()
|
self.setup_output_tags()
|
||||||
|
@ -401,6 +422,10 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
def _on_input_changed(self):
|
def _on_input_changed(self):
|
||||||
"""Maneja cambios en la entrada con debounce - COMO EN ORIGINAL"""
|
"""Maneja cambios en la entrada con debounce - COMO EN ORIGINAL"""
|
||||||
self._debounce_timer.start(300) # 300ms delay
|
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):
|
def _evaluate_and_update(self):
|
||||||
"""Evalúa la entrada y actualiza la salida - USANDO MOTOR ORIGINAL"""
|
"""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]):
|
def _evaluate_lines(self, lines: List[str]):
|
||||||
"""Evalúa líneas usando el motor original - SIN CAMBIOS EN LÓGICA"""
|
"""Evalúa líneas usando el motor original - SIN CAMBIOS EN LÓGICA"""
|
||||||
try:
|
try:
|
||||||
|
# ========== LIMPIAR CONTEXTO DEL MOTOR (COMO EN ORIGINAL) ==========
|
||||||
|
self.engine.clear_context()
|
||||||
|
|
||||||
# Limpiar panel LaTeX
|
# Limpiar panel LaTeX
|
||||||
self.latex_panel.clear_equations()
|
self.latex_panel.clear_equations()
|
||||||
|
|
||||||
|
@ -598,6 +626,17 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
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):
|
def setup_shortcuts(self):
|
||||||
|
@ -766,6 +805,701 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
self.save_history()
|
self.save_history()
|
||||||
event.accept()
|
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():
|
def main():
|
||||||
|
|
Loading…
Reference in New Issue