From e44cc3af8f25f4d5b7c163e52ba31f0263312e6e Mon Sep 17 00:00:00 2001 From: Miguel Date: Mon, 2 Jun 2025 15:07:45 +0200 Subject: [PATCH] Primera version de Autocompletado --- .doc/refactoring_guide.md | 4 +- bin_type.py | 19 +++--- chr_type.py | 23 ++++--- dec_type.py | 20 +++--- hex_type.py | 19 +++--- hybrid_calc_history.txt | 6 +- ip4_type.py | 23 ++++--- main_calc_app.py | 136 ++++++++++++++++++++++++++++++++++++-- sympy_helper.py | 25 +++++++ 9 files changed, 223 insertions(+), 52 deletions(-) create mode 100644 sympy_helper.py diff --git a/.doc/refactoring_guide.md b/.doc/refactoring_guide.md index d0c7c58..11f6c22 100644 --- a/.doc/refactoring_guide.md +++ b/.doc/refactoring_guide.md @@ -360,7 +360,7 @@ def Helper(input_str): ### 3. **Manejo Centralizado de Helpers** En el motor principal de la aplicación, se debe mantener una lista de todas las funciones Helper disponibles (incluyendo la de SymPy). -Al evaluar la línea de entrada: +Al evaluar la línea de entrada y esta da error de evaluacion entonces: - Se llama a cada Helper en orden. - Si alguna Helper retorna una ayuda, se muestra esa ayuda al usuario (en la línea de resultado, tooltip, etc.). @@ -388,7 +388,7 @@ def obtener_ayuda(input_str): ### 4. **Autocompletado de Métodos y Funciones (Popup tras el punto)** - Cuando el usuario escribe un punto (`.`) después de un objeto válido, se evalúa el objeto y se obtiene la lista de métodos disponibles. -- Se muestra un popup de autocompletado con los métodos relevantes (filtrando los no útiles). +- Se muestra un popup de autocompletado con los métodos relevantes (filtrando los no útiles). La lista de funciones se debe obtener de una funcione de cada objeto llamada PopupFunctionList() esta funcion en cada objeto mantendra la lista de las funciones disponibles y una explicacion corta tipo hint. Esta funcion retorna una lista de tuplas con el nombre de la funcion y el hint. - El usuario puede seleccionar un método con el teclado o mouse, y se inserta automáticamente (con paréntesis si corresponde). - El popup solo aparece tras el punto, no en cada pulsación de tecla, para no ser invasivo. diff --git a/bin_type.py b/bin_type.py index 89c0095..7f7385e 100644 --- a/bin_type.py +++ b/bin_type.py @@ -2,6 +2,7 @@ Clase híbrida para números binarios """ from hybrid_base import HybridCalcType +import re class HybridBin(HybridCalcType): @@ -38,14 +39,16 @@ class HybridBin(HybridCalcType): @staticmethod def Helper(input_str): """Ayuda contextual para Bin""" - return """ - Formato Bin: - - Con prefijo: 0b1010 - - Sin prefijo: 1010 - - Conversiones: - - toDecimal(): Convierte a decimal - """ + if re.match(r"^\s*Bin\b", input_str, re.IGNORECASE): + return 'Ej: Bin[1010], Bin[10]\nFunciones: toDecimal()' + return None + + @staticmethod + def PopupFunctionList(): + """Lista de métodos sugeridos para autocompletado de Bin""" + return [ + ("toDecimal", "Convierte a decimal"), + ] def toDecimal(self): """Convierte a decimal""" diff --git a/chr_type.py b/chr_type.py index fe8d373..41e8f7d 100644 --- a/chr_type.py +++ b/chr_type.py @@ -2,6 +2,7 @@ Clase híbrida para caracteres """ from hybrid_base import HybridCalcType +import re class HybridChr(HybridCalcType): @@ -39,16 +40,18 @@ class HybridChr(HybridCalcType): @staticmethod def Helper(input_str): """Ayuda contextual para Chr""" - return """ - Formato Chr: - - Carácter: 'A' - - Código ASCII: 65 - - Conversiones: - - toDecimal(): Obtiene código ASCII - - toHex(): Convierte a hexadecimal - - toBin(): Convierte a binario - """ + if re.match(r"^\s*Chr\b", input_str, re.IGNORECASE): + return "Ej: Chr[A], Chr[65]\nFunciones: toDecimal(), toHex(), toBin()" + return None + + @staticmethod + def PopupFunctionList(): + """Lista de métodos sugeridos para autocompletado de Chr""" + return [ + ("toDecimal", "Obtiene código ASCII"), + ("toHex", "Convierte a hexadecimal"), + ("toBin", "Convierte a binario"), + ] def toDecimal(self): """Obtiene código ASCII""" diff --git a/dec_type.py b/dec_type.py index 5cc8d2c..db063da 100644 --- a/dec_type.py +++ b/dec_type.py @@ -2,6 +2,7 @@ Clase híbrida para números decimales """ from hybrid_base import HybridCalcType +import re class HybridDec(HybridCalcType): @@ -34,14 +35,17 @@ class HybridDec(HybridCalcType): @staticmethod def Helper(input_str): """Ayuda contextual para Dec""" - return """ - Formato Dec: - - Número entero: 42 - - Conversiones: - - toHex(): Convierte a hexadecimal - - toBin(): Convierte a binario - """ + if re.match(r"^\s*Dec\b", input_str, re.IGNORECASE): + return 'Ej: Dec[42], Dec[100]\nFunciones: toHex(), toBin()' + return None + + @staticmethod + def PopupFunctionList(): + """Lista de métodos sugeridos para autocompletado de Dec""" + return [ + ("toHex", "Convierte a hexadecimal"), + ("toBin", "Convierte a binario"), + ] def toHex(self): """Convierte a hexadecimal""" diff --git a/hex_type.py b/hex_type.py index c6375c2..aa1c8ef 100644 --- a/hex_type.py +++ b/hex_type.py @@ -2,6 +2,7 @@ Clase híbrida para números hexadecimales """ from hybrid_base import HybridCalcType +import re class HybridHex(HybridCalcType): @@ -38,14 +39,16 @@ class HybridHex(HybridCalcType): @staticmethod def Helper(input_str): """Ayuda contextual para Hex""" - return """ - Formato Hex: - - Con prefijo: 0x1A - - Sin prefijo: 1A - - Conversiones: - - toDecimal(): Convierte a decimal - """ + if re.match(r"^\s*Hex\b", input_str, re.IGNORECASE): + return 'Ej: Hex[FF], Hex[255]\nFunciones: toDecimal()' + return None + + @staticmethod + def PopupFunctionList(): + """Lista de métodos sugeridos para autocompletado de Hex""" + return [ + ("toDecimal", "Convierte a decimal"), + ] def toDecimal(self): """Convierte a decimal""" diff --git a/hybrid_calc_history.txt b/hybrid_calc_history.txt index d1005b4..bc8d254 100644 --- a/hybrid_calc_history.txt +++ b/hybrid_calc_history.txt @@ -2,4 +2,8 @@ Hex[FF] ip=IP4[192.168.1.100/24] -500/25 \ No newline at end of file +500/25 + +Dec[Hex[ff]] + +Hex[ff] \ No newline at end of file diff --git a/ip4_type.py b/ip4_type.py index 15c31fc..df6719d 100644 --- a/ip4_type.py +++ b/ip4_type.py @@ -115,17 +115,18 @@ class HybridIP4(HybridCalcType): @staticmethod def Helper(input_str): """Ayuda contextual para IP4""" - return """ - Formato IP4: - - CIDR: 192.168.1.1/24 - - Con máscara: 192.168.1.1 255.255.255.0 - - Solo IP: 192.168.1.1 - - Métodos disponibles: - - NetworkAddress(): Obtiene dirección de red - - BroadcastAddress(): Obtiene dirección de broadcast - - Nodes(): Obtiene número de nodos disponibles - """ + if re.match(r"^\s*IP4\b", input_str, re.IGNORECASE): + return 'Ej: IP4[192.168.1.1/24], IP4[10.0.0.1, 8], o IP4[172.16.0.5, 255.255.0.0]\nFunciones: NetworkAddress(), BroadcastAddress(), Nodes()' + return None + + @staticmethod + def PopupFunctionList(): + """Lista de métodos sugeridos para autocompletado de IP4""" + return [ + ("NetworkAddress", "Obtiene la dirección de red"), + ("BroadcastAddress", "Obtiene la dirección de broadcast"), + ("Nodes", "Cantidad de nodos disponibles"), + ] def NetworkAddress(self): """Obtiene la dirección de red""" diff --git a/main_calc_app.py b/main_calc_app.py index 70e7b79..a896df7 100644 --- a/main_calc_app.py +++ b/main_calc_app.py @@ -8,6 +8,7 @@ import json import os import threading from typing import List, Dict, Any, Optional +import re # Importar componentes del CAS híbrido from main_evaluation import HybridEvaluationEngine, EvaluationResult @@ -18,6 +19,7 @@ from bin_type import HybridBin as Bin from dec_type import HybridDec as Dec from chr_type import HybridChr as Chr import sympy +from sympy_helper import Helper as SympyHelper class HybridCalculatorApp: @@ -26,6 +28,15 @@ class HybridCalculatorApp: SETTINGS_FILE = "hybrid_calc_settings.json" HISTORY_FILE = "hybrid_calc_history.txt" + HELPERS = [ + IP4.Helper, + Hex.Helper, + Bin.Helper, + Dec.Helper, + Chr.Helper, + SympyHelper, + ] + def __init__(self, root: tk.Tk): self.root = root self.root.title("Calculadora MAV - CAS Híbrido") @@ -264,9 +275,115 @@ class HybridCalculatorApp: self._debounce_job = self.root.after(300, self._evaluate_and_update) def _handle_dot_autocomplete(self): - """Maneja autocompletado con punto (simplificado por ahora)""" - # TODO: Implementar autocompletado para métodos de objetos - pass + self._close_autocomplete_popup() + cursor_index_str = self.input_text.index(tk.INSERT) + line_num_str, char_num_str = cursor_index_str.split('.') + current_line_num = int(line_num_str) + char_idx_of_dot = int(char_num_str) + obj_expr_str = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{char_idx_of_dot -1}").strip() + if not obj_expr_str: + return + + # Preprocesar para convertir sintaxis de corchetes a llamada de clase + # Ejemplo: Hex[FF] -> Hex('FF') + bracket_match = re.match(r"([A-Za-z_][A-Za-z0-9_]*)\[(.*)\]$", obj_expr_str) + if bracket_match: + class_name, arg = bracket_match.groups() + # Si el argumento es un número, no poner comillas + if arg.isdigit(): + obj_expr_str = f"{class_name}({arg})" + else: + obj_expr_str = f"{class_name}('{arg}')" + + eval_context = self.engine._get_full_context() if hasattr(self.engine, '_get_full_context') else {} + obj = None + try: + obj = eval(obj_expr_str, eval_context) + except Exception: + return + if obj is not None and hasattr(obj, 'PopupFunctionList'): + methods = obj.PopupFunctionList() + if methods: + self._show_autocomplete_popup(methods) + + def _show_autocomplete_popup(self, suggestions): + # suggestions: lista de tuplas (nombre, hint) + cursor_bbox = self.input_text.bbox(tk.INSERT) + if not cursor_bbox: + return + x, y, _, height = cursor_bbox + popup_x = self.input_text.winfo_rootx() + x + popup_y = self.input_text.winfo_rooty() + y + height + 2 + 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) + self._autocomplete_listbox = tk.Listbox( + 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}") + if suggestions: + self._autocomplete_listbox.select_set(0) + self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH) + self._autocomplete_listbox.bind("", self._on_autocomplete_select) + self._autocomplete_listbox.bind("", lambda e: self._close_autocomplete_popup()) + self._autocomplete_listbox.bind("", self._on_autocomplete_select) + 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.input_text.bind("", lambda e: self._close_autocomplete_popup(), add=True) + self.root.bind("", lambda e: self._close_autocomplete_popup(), add=True) + self.input_text.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) + self._autocomplete_listbox.config(width=width, height=height) + else: + self._close_autocomplete_popup() + + def _navigate_autocomplete(self, event, direction): + if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox: + return "break" + current_selection = self._autocomplete_listbox.curselection() + if not current_selection: + new_idx = 0 if direction == 1 else self._autocomplete_listbox.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): + if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox: + return "break" + selection = self._autocomplete_listbox.curselection() + if not selection: + self._close_autocomplete_popup() + return "break" + selected = self._autocomplete_listbox.get(selection[0]) + method_name = selected.split()[0] + self.input_text.insert(tk.INSERT, method_name + "()") + self.input_text.mark_set(tk.INSERT, f"{tk.INSERT}-1c") # Cursor dentro de los paréntesis + self._close_autocomplete_popup() + self.input_text.focus_set() + self.on_key_release() # Trigger re-evaluation + return "break" + + def _close_autocomplete_popup(self): + if hasattr(self, '_autocomplete_popup') and self._autocomplete_popup: + self._autocomplete_popup.destroy() + self._autocomplete_popup = None + self._autocomplete_listbox = None def _evaluate_and_update(self): """Evalúa todas las líneas y actualiza la salida""" @@ -309,7 +426,11 @@ class HybridCalculatorApp: output_parts = [] if result.is_error: - output_parts.append(("error", f"Error: {result.error}")) + ayuda = self.obtener_ayuda(result.original_line) + if ayuda: + output_parts.append(("helper", ayuda)) + else: + output_parts.append(("error", f"Error: {result.error}")) elif result.result_type == "comment": output_parts.append(("comment", result.original_line)) elif result.result_type == "equation_added": @@ -819,6 +940,13 @@ programación y análisis numérico. self.root.destroy() + def obtener_ayuda(self, input_str): + for helper in self.HELPERS: + ayuda = helper(input_str) + if ayuda: + return ayuda + return None + def main(): """Función principal""" diff --git a/sympy_helper.py b/sympy_helper.py new file mode 100644 index 0000000..f81a370 --- /dev/null +++ b/sympy_helper.py @@ -0,0 +1,25 @@ +import re + +def Helper(input_str): + """Ayuda contextual para funciones SymPy comunes""" + sympy_funcs = { + "diff": "Derivada: diff(expr, var). Ej: diff(sin(x), x)", + "integrate": "Integral: integrate(expr, var). Ej: integrate(x**2, x)", + "solve": "Resolver ecuaciones: solve(expr, var). Ej: solve(x**2-1, x)", + "limit": "Límite: limit(expr, var, valor). Ej: limit(sin(x)/x, x, 0)", + "series": "Serie de Taylor: series(expr, var, punto, n). Ej: series(exp(x), x, 0, 5)", + "Matrix": "Matrices: Matrix([[a, b], [c, d]]). Ej: Matrix([[1,2],[3,4]])", + "plot": "Gráfica: plot(expr, (var, a, b)). Ej: plot(sin(x), (x, 0, 2*pi))", + "plot3d": "Gráfica 3D: plot3d(expr, (x, a, b), (y, c, d))", + "simplify": "Simplificar: simplify(expr). Ej: simplify((x**2 + 2*x + 1))", + "expand": "Expandir: expand(expr). Ej: expand((x+1)**2)", + "factor": "Factorizar: factor(expr). Ej: factor(x**2 + 2*x + 1)", + "collect": "Agrupar: collect(expr, var). Ej: collect(x*y + x, x)", + "cancel": "Cancelar: cancel(expr). Ej: cancel((x**2 + 2*x + 1)/(x+1))", + "apart": "Fracciones parciales: apart(expr, var). Ej: apart(1/(x*(x+1)), x)", + "together": "Unir fracciones: together(expr). Ej: together(1/x + 1/y)", + } + for func, ayuda in sympy_funcs.items(): + if input_str.strip().startswith(func): + return ayuda + return None \ No newline at end of file