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**
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.

View File

@ -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
if re.match(r"^\s*Bin\b", input_str, re.IGNORECASE):
return 'Ej: Bin[1010], Bin[10]\nFunciones: toDecimal()'
return None
Conversiones:
- toDecimal(): Convierte a decimal
"""
@staticmethod
def PopupFunctionList():
"""Lista de métodos sugeridos para autocompletado de Bin"""
return [
("toDecimal", "Convierte a decimal"),
]
def toDecimal(self):
"""Convierte a decimal"""

View File

@ -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
if re.match(r"^\s*Chr\b", input_str, re.IGNORECASE):
return "Ej: Chr[A], Chr[65]\nFunciones: toDecimal(), toHex(), toBin()"
return None
Conversiones:
- toDecimal(): Obtiene código ASCII
- toHex(): Convierte a hexadecimal
- toBin(): Convierte a binario
"""
@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"""

View File

@ -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
if re.match(r"^\s*Dec\b", input_str, re.IGNORECASE):
return 'Ej: Dec[42], Dec[100]\nFunciones: toHex(), toBin()'
return None
Conversiones:
- toHex(): Convierte a hexadecimal
- toBin(): Convierte a binario
"""
@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"""

View File

@ -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
if re.match(r"^\s*Hex\b", input_str, re.IGNORECASE):
return 'Ej: Hex[FF], Hex[255]\nFunciones: toDecimal()'
return None
Conversiones:
- toDecimal(): Convierte a decimal
"""
@staticmethod
def PopupFunctionList():
"""Lista de métodos sugeridos para autocompletado de Hex"""
return [
("toDecimal", "Convierte a decimal"),
]
def toDecimal(self):
"""Convierte a decimal"""

View File

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

View File

@ -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
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
Métodos disponibles:
- NetworkAddress(): Obtiene dirección de red
- BroadcastAddress(): Obtiene dirección de broadcast
- Nodes(): Obtiene número de nodos disponibles
"""
@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"""

View File

@ -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("<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):
"""Evalúa todas las líneas y actualiza la salida"""
@ -309,6 +426,10 @@ class HybridCalculatorApp:
output_parts = []
if result.is_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))
@ -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"""

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