Primera version de Autocompletado
This commit is contained in:
parent
261b20df5c
commit
e44cc3af8f
|
@ -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.
|
||||
|
||||
|
|
17
bin_type.py
17
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
|
||||
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"""
|
||||
|
|
21
chr_type.py
21
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
|
||||
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"""
|
||||
|
|
18
dec_type.py
18
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
|
||||
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"""
|
||||
|
|
17
hex_type.py
17
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
|
||||
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"""
|
||||
|
|
|
@ -3,3 +3,7 @@ ip=IP4[192.168.1.100/24]
|
|||
|
||||
|
||||
500/25
|
||||
|
||||
Dec[Hex[ff]]
|
||||
|
||||
Hex[ff]
|
21
ip4_type.py
21
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
|
||||
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"""
|
||||
|
|
134
main_calc_app.py
134
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("<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"""
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue