Actualización del historial de cálculos y mejoras en el sistema de autocompletado.

This commit is contained in:
Miguel 2025-06-07 16:10:21 +02:00
parent 6475661a02
commit 018537c291
3 changed files with 559 additions and 98 deletions

View File

@ -1,31 +1,5 @@
# ✅ Expresión simple
$$ \frac{x^2 + 1}{x - 1} $$
# → (x**2 + 1)/(x - 1)
# ✅ Con operaciones adicionales
$$ \frac{x^2 + 1}{x - 1} $$ + 5
# → 5 + (x**2 + 1)/(x - 1)
# ✅ Asignaciones directas
resultado = $$ \frac{a + b}{c} $$
# → resultado = (a + b)/c
# ✅ En medio de expresiones
2 * $$ \sqrt{x^2 + y^2} $$ - 1
# → 2*sqrt(x**2 + y**2) - 1
# ✅ Múltiples LaTeX en una línea
$$ x^2 $$ + $$ y^2 $$
# → x**2 + y**2
# ✅ Con variables existentes
x = 2
y = 3
resultado = $$ \frac{x^2 + y^2}{x + y} $$
# → resultado = 13/5
$$Brix_{Bev} = \frac{Brix_{syr} + Brix_{H_2O} \cdot R_M}{R_M + 1}$$
$$ resultado = \frac{a + b}{c} $$
x = 5
resultado = x * 2
valor_test = 100

View File

@ -1,6 +1,6 @@
{
"window_geometry": "1383x700+203+1261",
"sash_pos_x": 736,
"sash_pos_x": 721,
"symbolic_mode": true,
"show_numeric_approximation": true,
"keep_symbolic_fractions": true,

View File

@ -79,9 +79,20 @@ class HybridCalculatorApp:
if hasattr(self.engine, 'logger'):
self.engine.logger.setLevel(logging.DEBUG if self.debug else logging.INFO)
# Autocompletado
self.autocomplete_popup = None
self.current_suggestions = []
# ========== SISTEMA DE AUTOCOMPLETADO MEJORADO ==========
self._autocomplete_popup = None
self._autocomplete_listbox = None
self._current_suggestions = []
self._autocomplete_active = False
self._autocomplete_trigger_pos = None
self._autocomplete_filter_text = ""
self._popup_disabled_until_next_dot = False
self._last_navigation_time = 0 # Para evitar filtrado tras navegación
# Variables para autocompletado de variables
self._variable_popup_job = None
self._variable_popup_active = False
self._last_input_change = 0
# Configurar ícono
self._setup_icon()
@ -120,6 +131,18 @@ class HybridCalculatorApp:
# Configurar eventos de cierre
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
# ========== BINDINGS DE TECLADO MEJORADOS ==========
self.input_text.bind("<KeyRelease>", self.on_key_release)
self.input_text.bind("<KeyPress>", self.on_key_press)
self.input_text.bind("<Button-1>", self._on_input_click)
self.input_text.bind("<FocusIn>", lambda e: self._close_autocomplete_popup())
# Bindings para navegación del autocompletado
self.input_text.bind("<Up>", self._handle_arrow_key)
self.input_text.bind("<Down>", self._handle_arrow_key)
self.input_text.bind("<Tab>", self._handle_tab_key)
self.input_text.bind("<Escape>", self._handle_escape_key)
def _setup_dynamic_helpers(self):
"""Configura helpers dinámicamente desde el registro de tipos"""
@ -282,7 +305,6 @@ CARACTERÍSTICAS:
)
# Configurar eventos
self.input_text.bind("<KeyRelease>", self.on_key_release)
self.input_text.bind("<Button-3>", lambda e: self._show_context_menu(e, "input"))
self.output_text.bind("<Button-3>", lambda e: self._show_context_menu(e, "output"))
@ -407,20 +429,299 @@ CARACTERÍSTICAS:
# self.output_text.tag_configure("IP4", foreground="#4ec9b0")
# self.output_text.tag_configure("IntBase", foreground="#4ec9b0")
def on_key_press(self, event=None):
"""Maneja eventos de presión de tecla (antes de que se inserte el carácter)"""
# Si el popup está activo, manejar navegación y selección
if (self._autocomplete_active or self._variable_popup_active) and event:
if event.keysym in ['Up', 'Down']:
return self._handle_arrow_key(event)
elif event.keysym == 'Tab':
return self._handle_tab_key(event)
elif event.keysym == 'Escape':
return self._handle_escape_key(event)
# Detectar backspace para cerrar popup de funciones si se borra el punto
if event and event.keysym == 'BackSpace' and self._autocomplete_active:
self._check_dot_removal()
def _check_dot_removal(self):
"""Verifica si se va a borrar el punto que activó el autocompletado"""
try:
# Obtener posición del cursor
cursor_pos = self.input_text.index(tk.INSERT)
# Obtener el carácter anterior al cursor
if cursor_pos != "1.0": # No estamos al inicio del texto
prev_char_pos = f"{cursor_pos}-1c"
prev_char = self.input_text.get(prev_char_pos, cursor_pos)
# Si el carácter anterior es un punto, cerrar el popup
if prev_char == '.':
# Programar cierre después del backspace
self.root.after(1, self._close_autocomplete_popup)
except tk.TclError:
# Error de posición, cerrar popup por seguridad
self._close_autocomplete_popup()
def on_key_release(self, event=None):
"""Maneja eventos de teclado"""
"""Maneja eventos de teclado después de insertar carácter"""
if self._debounce_job:
self.root.after_cancel(self._debounce_job)
# Autocompletado con punto (usando contexto dinámico)
# Cancelar job de autocompletado de variables si existe
if self._variable_popup_job:
self.root.after_cancel(self._variable_popup_job)
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 and event.char == '.' and self.input_text.focus_get() == self.input_text:
self._handle_dot_autocomplete()
# Cerrar popup de variables si está activo
if self._variable_popup_active:
self._close_autocomplete_popup()
if not self._popup_disabled_until_next_dot:
self._handle_dot_autocomplete()
else:
# Resetear flag cuando se escribe un nuevo punto
self._popup_disabled_until_next_dot = False
# Filtrar autocompletado si está activo (pero no si acabamos de navegar)
elif self._autocomplete_active and event and event.char.isprintable() and not just_navigated:
self._filter_autocomplete()
# Marcar tiempo del último cambio de input
if event and event.char.isprintable():
self._last_input_change = time.time()
# Evaluación con debounce y auto-dimensionado
self._debounce_job = self.root.after(300, self._process_input_and_adjust_layout)
# Programar autocompletado de variables (nuevo sistema)
self._schedule_variable_autocomplete_improved()
def _schedule_variable_autocomplete_improved(self):
"""Programa el autocompletado de variables mientras se escribe"""
# Solo si no hay popup de funciones activo
if self._autocomplete_active or self._popup_disabled_until_next_dot:
self.logger.debug("Variable autocomplete: Saltando - popup activo o deshabilitado")
return
# Verificar que estemos escribiendo (no solo navegando)
current_line = self.input_text.get("insert linestart", "insert lineend").strip()
if not current_line or current_line.endswith('.'):
self.logger.debug(f"Variable autocomplete: Saltando - línea vacía o termina en punto: '{current_line}'")
return
# Cancelar job anterior si existe
if self._variable_popup_job:
self.root.after_cancel(self._variable_popup_job)
self.logger.debug(f"Variable autocomplete: Programando para línea: '{current_line}'")
# Programar para 800ms después
self._variable_popup_job = self.root.after(800, self._show_variable_autocomplete_improved)
def _show_variable_autocomplete_improved(self):
"""Muestra autocompletado de variables disponibles (simplificado)"""
self.logger.debug("Variable autocomplete: Ejecutando show_variable_autocomplete_improved")
if self._autocomplete_active or self._variable_popup_active:
self.logger.debug("Variable autocomplete: Saltando - ya hay popup activo")
return # Ya hay un popup activo
# Verificar que aún estemos en una línea válida
current_line = self.input_text.get("insert linestart", "insert lineend").strip()
if not current_line or current_line.endswith('.'):
self.logger.debug(f"Variable autocomplete: Saltando - línea inválida: '{current_line}'")
self._variable_popup_job = None
return
# Obtener variables del contexto
try:
context = self.engine._get_full_context()
self.logger.debug(f"Variable autocomplete: Contexto completo tiene {len(context)} elementos")
# Mostrar tabla de símbolos específicamente
symbol_table = getattr(self.engine, 'symbol_table', {})
self.logger.debug(f"Variable autocomplete: Symbol table tiene {len(symbol_table)} elementos: {list(symbol_table.keys())}")
variables = []
# Filtrar variables (excluir funciones built-in y módulos)
for name, value in context.items():
# Debug detallado de cada elemento
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 (ANTES del log)
is_sympy_symbol = hasattr(value, 'is_symbol') or 'sympy' in str(type(value)).lower()
self.logger.debug(f"Variable autocomplete: Analizando '{name}': underscore={is_underscore}, callable={is_callable}, module={has_module}, excluded={is_excluded}, sympy_symbol={is_sympy_symbol}, type={type(value)}")
if (not is_underscore and
not is_callable and
(not has_module or is_sympy_symbol) and # Permitir SymPy symbols
not is_excluded):
self.logger.debug(f"Variable autocomplete: ✅ Aceptando variable '{name}' = {value}")
# 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))
else:
self.logger.debug(f"Variable autocomplete: ❌ Rechazando '{name}' por filtros")
self.logger.debug(f"Variable autocomplete: Encontradas {len(variables)} variables totales")
if variables:
variables.sort(key=lambda x: x[0])
# Obtener texto actual para filtrado
words = current_line.split()
self.logger.debug(f"Variable autocomplete: Palabras en línea: {words}")
if words:
last_word = words[-1]
self.logger.debug(f"Variable autocomplete: Última palabra: '{last_word}'")
# 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
]
self.logger.debug(f"Variable autocomplete: Variables filtradas: {len(filtered_vars)}")
if filtered_vars:
# Posicionar en el cursor actual
self._autocomplete_trigger_pos = self.input_text.index(tk.INSERT)
self._autocomplete_filter_text = ""
# Mostrar popup de variables menos invasivo
self._show_variable_popup(filtered_vars)
self.logger.debug(f"Mostrando autocompletado de variables: {len(filtered_vars)} encontradas")
else:
self.logger.debug("Variable autocomplete: No hay variables filtradas que mostrar")
else:
self.logger.debug("Variable autocomplete: No hay palabras en la línea")
else:
self.logger.debug("Variable autocomplete: No hay variables en el contexto")
except Exception as e:
self.logger.debug(f"Error obteniendo variables para autocompletado: {e}")
# Limpiar job
self._variable_popup_job = None
def _show_variable_popup(self, variables):
"""Muestra popup de variables con estilo menos invasivo"""
cursor_bbox = self.input_text.bbox(tk.INSERT)
if not cursor_bbox:
return
# Marcar como popup de variables activo
self._variable_popup_active = True
self._autocomplete_active = False # No es el popup principal
x, y, _, height = cursor_bbox
popup_x = self.input_text.winfo_rootx() + x
popup_y = self.input_text.winfo_rooty() + y + height + 2
# Crear popup más discreto
self._autocomplete_popup = tk.Toplevel(self.root)
self._autocomplete_popup.wm_overrideredirect(True)
self._autocomplete_popup.wm_geometry(f"+{popup_x}+{popup_y}")
self._autocomplete_popup.attributes('-topmost', True)
# Crear listbox con colores discretos pero visibles
self._autocomplete_listbox = tk.Listbox(
self._autocomplete_popup,
bg="#2d2d30", # Más oscuro
fg="#c9c9c9", # Texto más visible que antes
selectbackground="#4a4a4a", # Selección más visible
selectforeground="#ffffff",
borderwidth=1,
relief="solid",
exportselection=False,
activestyle="none",
font=("Consolas", 10) # Fuente legible
)
# Llenar con variables (formato más simple)
for name, value in variables:
self._autocomplete_listbox.insert(tk.END, f"{name} = {value}")
if variables:
self._autocomplete_listbox.select_set(0)
self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH)
# Solo doble-click para seleccionar (más discreto)
self._autocomplete_listbox.bind("<Double-Button-1>",
lambda e: self._select_variable())
# Binding para cerrar si se hace click fuera
self.root.bind("<Button-1>", self._on_click_outside_variable, add=True)
# 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, 40)
height = min(len(variables), 5)
self._autocomplete_listbox.config(width=width, height=height)
else:
self._close_autocomplete_popup()
def _handle_arrow_key(self, event):
"""Maneja las teclas de flecha cuando el popup está activo"""
if not self._autocomplete_active and not self._variable_popup_active:
return # Permitir comportamiento normal
direction = -1 if event.keysym == 'Up' else 1
self._navigate_autocomplete_improved(direction)
# Marcar tiempo de navegación para evitar filtrado inmediato
import time
self._last_navigation_time = time.time()
return "break" # Prevenir comportamiento normal
def _handle_tab_key(self, event):
"""Maneja la tecla TAB para seleccionar del popup"""
if self._autocomplete_active or self._variable_popup_active:
self._select_autocomplete()
return "break"
return # Permitir comportamiento normal si no hay popup
def _handle_escape_key(self, event):
"""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
return "break"
return # Permitir comportamiento normal si no hay popup
def _on_input_click(self, event):
"""Maneja clicks en el campo de entrada"""
self._close_autocomplete_popup()
def _handle_dot_autocomplete(self):
"""Maneja el autocompletado cuando se escribe un punto - VERSIÓN DINÁMICA"""
"""Maneja el autocompletado cuando se escribe un punto - VERSIÓN MEJORADA"""
self._close_autocomplete_popup()
cursor_index_str = self.input_text.index(tk.INSERT)
line_num_str, char_num_str = cursor_index_str.split('.')
@ -431,6 +732,10 @@ CARACTERÍSTICAS:
self.logger.debug("Autocomplete: Cursor at beginning of line after dot. No action.")
return
# Guardar posición donde se activó el autocompletado
self._autocomplete_trigger_pos = f"{current_line_num}.{char_idx_after_dot}"
self._autocomplete_filter_text = ""
dot_char_index_in_line = char_idx_after_dot - 1
text_on_line_up_to_dot = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{dot_char_index_in_line}")
@ -545,104 +850,285 @@ CARACTERÍSTICAS:
self._show_autocomplete_popup(methods, is_global_popup=False)
def _show_autocomplete_popup(self, suggestions, is_global_popup=False):
"""Muestra popup de autocompletado (sin cambios)"""
"""Muestra popup de autocompletado modeless con filtrado"""
cursor_bbox = self.input_text.bbox(tk.INSERT)
if not cursor_bbox:
return
# Guardar sugerencias originales y estado
self._current_suggestions = suggestions.copy()
self._is_global_popup = is_global_popup
self._autocomplete_active = True
x, y, _, height = cursor_bbox
popup_x = self.input_text.winfo_rootx() + x
popup_y = self.input_text.winfo_rooty() + y + height + 2
# Crear popup modeless
self._autocomplete_popup = tk.Toplevel(self.root)
self._autocomplete_popup.wm_overrideredirect(True)
self._autocomplete_popup.wm_geometry(f"+{popup_x}+{popup_y}")
self._autocomplete_popup.attributes('-topmost', True)
self.root.after(100, lambda: self._autocomplete_popup.attributes('-topmost', False) if self._autocomplete_popup else None)
# Crear listbox
self._autocomplete_listbox = tk.Listbox(
self._autocomplete_popup, bg="#3c3f41", fg="#bbbbbb",
selectbackground="#007acc", selectforeground="white",
borderwidth=1, relief="solid", exportselection=False, activestyle="none"
self._autocomplete_popup,
bg="#3c3f41",
fg="#bbbbbb",
selectbackground="#007acc",
selectforeground="white",
borderwidth=1,
relief="solid",
exportselection=False,
activestyle="none"
)
for name, hint in suggestions:
self._autocomplete_listbox.insert(tk.END, f"{name}{hint}")
# Llenar con sugerencias iniciales
self._populate_listbox(suggestions)
if suggestions:
self._autocomplete_listbox.select_set(0)
self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH)
self._autocomplete_listbox.bind("<Return>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g))
self._autocomplete_listbox.bind("<Tab>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g))
self._autocomplete_listbox.bind("<Escape>", lambda e: self._close_autocomplete_popup())
self._autocomplete_listbox.bind("<Double-Button-1>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g))
self._autocomplete_listbox.focus_set()
self._autocomplete_listbox.bind("<Up>", lambda e: self._navigate_autocomplete(e, -1))
self._autocomplete_listbox.bind("<Down>", lambda e: self._navigate_autocomplete(e, 1))
self.input_text.bind("<Button-1>", lambda e: self._close_autocomplete_popup(), add=True)
self.root.bind("<Button-1>", lambda e: self._close_autocomplete_popup(), add=True)
max_len = max(len(name) for name, _ in suggestions) if suggestions else 10
width = max(15, min(max_len + 10, 50))
height = min(len(suggestions), 10)
full_text_suggestions = [f"{name}{hint}" for name, hint in suggestions]
max_full_len = max(len(text) for text in full_text_suggestions) if full_text_suggestions else 20
width = max(20, min(max_full_len + 5, 80))
self._autocomplete_listbox.config(width=width, height=height)
# Bindings solo para el listbox (no roba focus del input)
self._autocomplete_listbox.bind("<Double-Button-1>",
lambda e: self._select_autocomplete())
# Binding para cerrar si se hace click fuera
self.root.bind("<Button-1>", self._on_click_outside, add=True)
# Calcular tamaño
self._resize_popup()
else:
self._close_autocomplete_popup()
def _navigate_autocomplete(self, event, direction):
"""Navegación en autocomplete (sin cambios)"""
if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox:
return "break"
def _populate_listbox(self, suggestions):
"""Llena el listbox con las sugerencias"""
self._autocomplete_listbox.delete(0, tk.END)
for name, hint in suggestions:
self._autocomplete_listbox.insert(tk.END, f"{name}{hint}")
def _resize_popup(self):
"""Redimensiona el popup según el contenido"""
if not self._autocomplete_listbox:
return
size = self._autocomplete_listbox.size()
if size == 0:
return
# Calcular dimensiones
max_len = 20
for i in range(size):
item_text = self._autocomplete_listbox.get(i)
max_len = max(max_len, len(item_text))
width = min(max_len + 5, 80)
height = min(size, 10)
self._autocomplete_listbox.config(width=width, height=height)
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
current_pos = self.input_text.index(tk.INSERT)
try:
filter_text = self.input_text.get(self._autocomplete_trigger_pos, current_pos)
self._autocomplete_filter_text = filter_text.lower()
except tk.TclError:
# 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)
self._autocomplete_listbox.select_set(0)
self._resize_popup()
else:
# No hay coincidencias, cerrar popup
self._close_autocomplete_popup()
def _navigate_autocomplete_improved(self, direction):
"""Navegación mejorada en el popup de autocompletado"""
if not self._autocomplete_listbox:
return
current_selection = self._autocomplete_listbox.curselection()
size = self._autocomplete_listbox.size()
if size == 0:
return
if not current_selection:
new_idx = 0 if direction == 1 else self._autocomplete_listbox.size() -1
new_idx = 0 if direction == 1 else size - 1
else:
idx = current_selection[0]
new_idx = idx + direction
if 0 <= new_idx < self._autocomplete_listbox.size():
if current_selection:
self._autocomplete_listbox.select_clear(current_selection[0])
self._autocomplete_listbox.select_set(new_idx)
self._autocomplete_listbox.activate(new_idx)
self._autocomplete_listbox.see(new_idx)
return "break"
def _on_autocomplete_select(self, event, is_global=False):
"""Selección de autocomplete (sin cambios)"""
if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox:
return "break"
new_idx = (idx + direction) % size # Navegación circular
# Actualizar selección
if current_selection:
self._autocomplete_listbox.select_clear(current_selection[0])
self._autocomplete_listbox.select_set(new_idx)
self._autocomplete_listbox.activate(new_idx)
self._autocomplete_listbox.see(new_idx)
def _select_autocomplete(self):
"""Selecciona el item actual del autocompletado"""
if not self._autocomplete_listbox:
return
selection = self._autocomplete_listbox.curselection()
if not selection:
self._close_autocomplete_popup()
return "break"
return
selected_text_with_hint = self._autocomplete_listbox.get(selection[0])
item_name = selected_text_with_hint.split("")[0].strip()
if is_global:
# Obtener texto seleccionado
selected_text = self._autocomplete_listbox.get(selection[0])
# 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_pos_str = self.input_text.index(tk.INSERT)
line_num, char_num = map(int, cursor_pos_str.split('.'))
dot_pos_on_line = char_num - 1
dot_pos_on_line = char_num - len(self._autocomplete_filter_text) - 1
dot_index_str = f"{line_num}.{dot_pos_on_line}"
self.input_text.delete(dot_index_str)
# Eliminar punto y texto filtrado
end_pos = f"{line_num}.{char_num}"
self.input_text.delete(dot_index_str, end_pos)
# Insertar función (no variables en popup global)
insert_text = item_name + "()"
self.input_text.insert(dot_index_str, insert_text)
self.input_text.mark_set(tk.INSERT, f"{dot_index_str}+{len(item_name)+1}c")
else:
self.input_text.insert(tk.INSERT, item_name + "()")
self.input_text.mark_set(tk.INSERT, f"{tk.INSERT}-1c")
# Para popup de objeto/variables
current_pos = self.input_text.index(tk.INSERT)
# Eliminar texto filtrado si existe
if self._autocomplete_filter_text:
start_pos = f"{current_pos}-{len(self._autocomplete_filter_text)}c"
self.input_text.delete(start_pos, current_pos)
current_pos = start_pos
# Insertar según el tipo
if is_variable:
# Solo insertar el nombre de la variable
insert_text = item_name
self.input_text.insert(current_pos, insert_text)
self.input_text.mark_set(tk.INSERT, f"{current_pos}+{len(item_name)}c")
else:
# Insertar método con paréntesis
insert_text = item_name + "()"
self.input_text.insert(current_pos, insert_text)
self.input_text.mark_set(tk.INSERT, f"{current_pos}+{len(item_name)+1}c")
# Cerrar popup y enfocar input
self._close_autocomplete_popup()
self.input_text.focus_set()
self.on_key_release()
return "break"
def _select_variable(self):
"""Selecciona una variable del popup de variables"""
if not self._autocomplete_listbox:
return
selection = self._autocomplete_listbox.curselection()
if not selection:
return
# Obtener nombre de variable
selected_text = self._autocomplete_listbox.get(selection[0])
var_name = selected_text.split(" = ")[0].strip()
# Obtener posición de la palabra actual
current_line = self.input_text.get("insert linestart", "insert lineend")
cursor_pos = self.input_text.index(tk.INSERT)
line_start = self.input_text.index("insert linestart")
# Encontrar la palabra que estamos completando
words = current_line.split()
if words:
last_word = words[-1]
# Buscar posición de la última palabra
word_start_pos = current_line.rfind(last_word)
if word_start_pos >= 0:
# Calcular posición absoluta
abs_word_start = f"{line_start.split('.')[0]}.{word_start_pos}"
abs_word_end = f"{line_start.split('.')[0]}.{word_start_pos + len(last_word)}"
# Reemplazar la palabra parcial con la variable completa
self.input_text.delete(abs_word_start, abs_word_end)
self.input_text.insert(abs_word_start, var_name)
self.input_text.mark_set(tk.INSERT, f"{abs_word_start}+{len(var_name)}c")
# Cerrar popup
self._close_autocomplete_popup()
self.input_text.focus_set()
def _on_click_outside_variable(self, event):
"""Maneja clicks fuera del popup de variables"""
if self._autocomplete_popup and event.widget not in [
self._autocomplete_popup, self._autocomplete_listbox
]:
self._close_autocomplete_popup()
def _on_click_outside(self, event):
"""Maneja clicks fuera del popup"""
if self._autocomplete_popup and event.widget not in [
self._autocomplete_popup, self._autocomplete_listbox
]:
self._close_autocomplete_popup()
def _navigate_autocomplete(self, event, direction):
"""Navegación en autocomplete (mantenido por compatibilidad)"""
return self._navigate_autocomplete_improved(direction)
def _close_autocomplete_popup(self):
"""Cierra popup de autocomplete (sin cambios)"""
"""Cierra popup de autocomplete y resetea estado"""
if hasattr(self, '_autocomplete_popup') and self._autocomplete_popup:
self._autocomplete_popup.destroy()
try:
self._autocomplete_popup.destroy()
except tk.TclError:
pass # Ya fue destruido
self._autocomplete_popup = None
if hasattr(self, '_autocomplete_listbox') and 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 = []
# Remover bindings temporales
try:
self.root.unbind("<Button-1>")
except tk.TclError:
pass
def _evaluate_and_update(self):
"""Evalúa todas las líneas y actualiza la salida"""
@ -652,9 +1138,10 @@ CARACTERÍSTICAS:
self._clear_output()
return
# NUEVO: Limpiar completamente el contexto antes de cada evaluación
# Esto garantiza que cada modificación reevalúe todo desde cero
self.engine.clear_context()
# MODIFICADO: Solo limpiar ecuaciones, NO las variables del usuario
# Las variables deben persistir para el autocompletado
self.engine.equations.clear() # Solo limpiar ecuaciones
self.logger.debug("Ecuaciones del motor limpiadas antes de evaluación (variables mantenidas)")
lines = input_content.splitlines()
self._evaluate_lines(lines)
@ -898,7 +1385,7 @@ CARACTERÍSTICAS:
"""Inicia nueva sesión"""
self.clear_input()
self.clear_output()
self.engine.clear_context() # Limpiar contexto del motor
self.engine.clear_context() # Aquí sí limpiamos todo porque es nueva sesión
def load_file(self):
"""Carga archivo en el editor"""