diff --git a/hybrid_calc_history.txt b/hybrid_calc_history.txt index 1802e66..7522a12 100644 --- a/hybrid_calc_history.txt +++ b/hybrid_calc_history.txt @@ -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} $$ \ No newline at end of file +x = 5 +resultado = x * 2 +valor_test = 100 \ No newline at end of file diff --git a/hybrid_calc_settings.json b/hybrid_calc_settings.json index 958b608..da27a0b 100644 --- a/hybrid_calc_settings.json +++ b/hybrid_calc_settings.json @@ -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, diff --git a/main_calc_app.py b/main_calc_app.py index 444b5c9..f287aad 100644 --- a/main_calc_app.py +++ b/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("", self.on_key_release) + self.input_text.bind("", self.on_key_press) + self.input_text.bind("", self._on_input_click) + self.input_text.bind("", lambda e: self._close_autocomplete_popup()) + + # Bindings para navegación del autocompletado + self.input_text.bind("", self._handle_arrow_key) + self.input_text.bind("", self._handle_arrow_key) + self.input_text.bind("", self._handle_tab_key) + self.input_text.bind("", 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("", self.on_key_release) self.input_text.bind("", lambda e: self._show_context_menu(e, "input")) self.output_text.bind("", 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("", + lambda e: self._select_variable()) + + # Binding para cerrar si se hace click fuera + self.root.bind("", 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("", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g)) - self._autocomplete_listbox.bind("", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g)) - self._autocomplete_listbox.bind("", lambda e: self._close_autocomplete_popup()) - self._autocomplete_listbox.bind("", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g)) - self._autocomplete_listbox.focus_set() - self._autocomplete_listbox.bind("", lambda e: self._navigate_autocomplete(e, -1)) - self._autocomplete_listbox.bind("", lambda e: self._navigate_autocomplete(e, 1)) - - self.input_text.bind("", lambda e: self._close_autocomplete_popup(), add=True) - self.root.bind("", 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("", + lambda e: self._select_autocomplete()) + + # Binding para cerrar si se hace click fuera + self.root.bind("", 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("") + 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"""