Primera version de Autocompletado

This commit is contained in:
Miguel 2025-06-02 15:07:45 +02:00
parent 261b20df5c
commit e44cc3af8f
9 changed files with 223 additions and 52 deletions

View File

@ -360,7 +360,7 @@ def Helper(input_str):
### 3. **Manejo Centralizado de Helpers** ### 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). 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. - 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.). - 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)** ### 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. - 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 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. - El popup solo aparece tras el punto, no en cada pulsación de tecla, para no ser invasivo.

View File

@ -2,6 +2,7 @@
Clase híbrida para números binarios Clase híbrida para números binarios
""" """
from hybrid_base import HybridCalcType from hybrid_base import HybridCalcType
import re
class HybridBin(HybridCalcType): class HybridBin(HybridCalcType):
@ -38,14 +39,16 @@ class HybridBin(HybridCalcType):
@staticmethod @staticmethod
def Helper(input_str): def Helper(input_str):
"""Ayuda contextual para Bin""" """Ayuda contextual para Bin"""
return """ if re.match(r"^\s*Bin\b", input_str, re.IGNORECASE):
Formato Bin: return 'Ej: Bin[1010], Bin[10]\nFunciones: toDecimal()'
- Con prefijo: 0b1010 return None
- Sin prefijo: 1010
Conversiones: @staticmethod
- toDecimal(): Convierte a decimal def PopupFunctionList():
""" """Lista de métodos sugeridos para autocompletado de Bin"""
return [
("toDecimal", "Convierte a decimal"),
]
def toDecimal(self): def toDecimal(self):
"""Convierte a decimal""" """Convierte a decimal"""

View File

@ -2,6 +2,7 @@
Clase híbrida para caracteres Clase híbrida para caracteres
""" """
from hybrid_base import HybridCalcType from hybrid_base import HybridCalcType
import re
class HybridChr(HybridCalcType): class HybridChr(HybridCalcType):
@ -39,16 +40,18 @@ class HybridChr(HybridCalcType):
@staticmethod @staticmethod
def Helper(input_str): def Helper(input_str):
"""Ayuda contextual para Chr""" """Ayuda contextual para Chr"""
return """ if re.match(r"^\s*Chr\b", input_str, re.IGNORECASE):
Formato Chr: return "Ej: Chr[A], Chr[65]\nFunciones: toDecimal(), toHex(), toBin()"
- Carácter: 'A' return None
- Código ASCII: 65
Conversiones: @staticmethod
- toDecimal(): Obtiene código ASCII def PopupFunctionList():
- toHex(): Convierte a hexadecimal """Lista de métodos sugeridos para autocompletado de Chr"""
- toBin(): Convierte a binario return [
""" ("toDecimal", "Obtiene código ASCII"),
("toHex", "Convierte a hexadecimal"),
("toBin", "Convierte a binario"),
]
def toDecimal(self): def toDecimal(self):
"""Obtiene código ASCII""" """Obtiene código ASCII"""

View File

@ -2,6 +2,7 @@
Clase híbrida para números decimales Clase híbrida para números decimales
""" """
from hybrid_base import HybridCalcType from hybrid_base import HybridCalcType
import re
class HybridDec(HybridCalcType): class HybridDec(HybridCalcType):
@ -34,14 +35,17 @@ class HybridDec(HybridCalcType):
@staticmethod @staticmethod
def Helper(input_str): def Helper(input_str):
"""Ayuda contextual para Dec""" """Ayuda contextual para Dec"""
return """ if re.match(r"^\s*Dec\b", input_str, re.IGNORECASE):
Formato Dec: return 'Ej: Dec[42], Dec[100]\nFunciones: toHex(), toBin()'
- Número entero: 42 return None
Conversiones: @staticmethod
- toHex(): Convierte a hexadecimal def PopupFunctionList():
- toBin(): Convierte a binario """Lista de métodos sugeridos para autocompletado de Dec"""
""" return [
("toHex", "Convierte a hexadecimal"),
("toBin", "Convierte a binario"),
]
def toHex(self): def toHex(self):
"""Convierte a hexadecimal""" """Convierte a hexadecimal"""

View File

@ -2,6 +2,7 @@
Clase híbrida para números hexadecimales Clase híbrida para números hexadecimales
""" """
from hybrid_base import HybridCalcType from hybrid_base import HybridCalcType
import re
class HybridHex(HybridCalcType): class HybridHex(HybridCalcType):
@ -38,14 +39,16 @@ class HybridHex(HybridCalcType):
@staticmethod @staticmethod
def Helper(input_str): def Helper(input_str):
"""Ayuda contextual para Hex""" """Ayuda contextual para Hex"""
return """ if re.match(r"^\s*Hex\b", input_str, re.IGNORECASE):
Formato Hex: return 'Ej: Hex[FF], Hex[255]\nFunciones: toDecimal()'
- Con prefijo: 0x1A return None
- Sin prefijo: 1A
Conversiones: @staticmethod
- toDecimal(): Convierte a decimal def PopupFunctionList():
""" """Lista de métodos sugeridos para autocompletado de Hex"""
return [
("toDecimal", "Convierte a decimal"),
]
def toDecimal(self): def toDecimal(self):
"""Convierte a decimal""" """Convierte a decimal"""

View File

@ -3,3 +3,7 @@ ip=IP4[192.168.1.100/24]
500/25 500/25
Dec[Hex[ff]]
Hex[ff]

View File

@ -115,17 +115,18 @@ class HybridIP4(HybridCalcType):
@staticmethod @staticmethod
def Helper(input_str): def Helper(input_str):
"""Ayuda contextual para IP4""" """Ayuda contextual para IP4"""
return """ if re.match(r"^\s*IP4\b", input_str, re.IGNORECASE):
Formato IP4: 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()'
- CIDR: 192.168.1.1/24 return None
- Con máscara: 192.168.1.1 255.255.255.0
- Solo IP: 192.168.1.1
Métodos disponibles: @staticmethod
- NetworkAddress(): Obtiene dirección de red def PopupFunctionList():
- BroadcastAddress(): Obtiene dirección de broadcast """Lista de métodos sugeridos para autocompletado de IP4"""
- Nodes(): Obtiene número de nodos disponibles return [
""" ("NetworkAddress", "Obtiene la dirección de red"),
("BroadcastAddress", "Obtiene la dirección de broadcast"),
("Nodes", "Cantidad de nodos disponibles"),
]
def NetworkAddress(self): def NetworkAddress(self):
"""Obtiene la dirección de red""" """Obtiene la dirección de red"""

View File

@ -8,6 +8,7 @@ import json
import os import os
import threading import threading
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import re
# Importar componentes del CAS híbrido # Importar componentes del CAS híbrido
from main_evaluation import HybridEvaluationEngine, EvaluationResult 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 dec_type import HybridDec as Dec
from chr_type import HybridChr as Chr from chr_type import HybridChr as Chr
import sympy import sympy
from sympy_helper import Helper as SympyHelper
class HybridCalculatorApp: class HybridCalculatorApp:
@ -26,6 +28,15 @@ class HybridCalculatorApp:
SETTINGS_FILE = "hybrid_calc_settings.json" SETTINGS_FILE = "hybrid_calc_settings.json"
HISTORY_FILE = "hybrid_calc_history.txt" HISTORY_FILE = "hybrid_calc_history.txt"
HELPERS = [
IP4.Helper,
Hex.Helper,
Bin.Helper,
Dec.Helper,
Chr.Helper,
SympyHelper,
]
def __init__(self, root: tk.Tk): def __init__(self, root: tk.Tk):
self.root = root self.root = root
self.root.title("Calculadora MAV - CAS Híbrido") 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) self._debounce_job = self.root.after(300, self._evaluate_and_update)
def _handle_dot_autocomplete(self): def _handle_dot_autocomplete(self):
"""Maneja autocompletado con punto (simplificado por ahora)""" self._close_autocomplete_popup()
# TODO: Implementar autocompletado para métodos de objetos cursor_index_str = self.input_text.index(tk.INSERT)
pass 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("<Return>", self._on_autocomplete_select)
self._autocomplete_listbox.bind("<Escape>", lambda e: self._close_autocomplete_popup())
self._autocomplete_listbox.bind("<Double-Button-1>", self._on_autocomplete_select)
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("<FocusOut>", lambda e: self._close_autocomplete_popup(), add=True)
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)
self.input_text.bind("<Key>", 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): def _evaluate_and_update(self):
"""Evalúa todas las líneas y actualiza la salida""" """Evalúa todas las líneas y actualiza la salida"""
@ -309,7 +426,11 @@ class HybridCalculatorApp:
output_parts = [] output_parts = []
if result.is_error: 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": elif result.result_type == "comment":
output_parts.append(("comment", result.original_line)) output_parts.append(("comment", result.original_line))
elif result.result_type == "equation_added": elif result.result_type == "equation_added":
@ -819,6 +940,13 @@ programación y análisis numérico.
self.root.destroy() 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(): def main():
"""Función principal""" """Función principal"""

25
sympy_helper.py Normal file
View File

@ -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