Actualización del historial de cálculos y mejoras en el sistema de autocompletado.
This commit is contained in:
parent
6475661a02
commit
018537c291
|
@ -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
|
|
@ -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,
|
||||
|
|
623
main_calc_app.py
623
main_calc_app.py
|
@ -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"""
|
||||
|
|
Loading…
Reference in New Issue