510 lines
19 KiB
Python
510 lines
19 KiB
Python
"""
|
||
Sistema de resultados interactivos con tags clickeables - VERSIÓN CORREGIDA
|
||
"""
|
||
import tkinter as tk
|
||
from tkinter import Toplevel, scrolledtext
|
||
import sympy
|
||
from typing import Any, Optional, Dict, List, Tuple
|
||
import matplotlib.pyplot as plt
|
||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||
import numpy as np
|
||
|
||
|
||
class PlotResult:
|
||
"""Placeholder para resultados de plotting - DEFINICIÓN PRINCIPAL"""
|
||
|
||
def __init__(self, plot_type: str, args: tuple, kwargs: dict):
|
||
self.plot_type = plot_type
|
||
self.args = args
|
||
self.kwargs = kwargs
|
||
|
||
def __str__(self):
|
||
return f"📊 Ver {self.plot_type.title()}"
|
||
|
||
def __repr__(self):
|
||
return f"PlotResult('{self.plot_type}', {self.args}, {self.kwargs})"
|
||
|
||
|
||
class InteractiveResultManager:
|
||
"""Maneja resultados interactivos con ventanas emergentes"""
|
||
|
||
def __init__(self, parent_window: tk.Tk):
|
||
self.parent = parent_window
|
||
self.open_windows: Dict[str, Toplevel] = {}
|
||
|
||
def create_interactive_tag(self, result: Any, text_widget: tk.Text, index: str) -> Optional[Tuple[str, str]]:
|
||
"""
|
||
Crea un tag interactivo para un resultado si es necesario
|
||
|
||
Returns:
|
||
(tag_name, display_text) si se creó tag, None si no es necesario
|
||
"""
|
||
tag_name = None
|
||
display_text = ""
|
||
|
||
# 🔧 CORRECCIÓN: Verificar con isinstance correcto
|
||
if isinstance(result, PlotResult):
|
||
tag_name = f"plot_{id(result)}"
|
||
display_text = f"📊 Ver {result.plot_type.title()}"
|
||
|
||
elif isinstance(result, sympy.Matrix):
|
||
tag_name = f"matrix_{id(result)}"
|
||
rows, cols = result.shape
|
||
display_text = f"📋 Ver Matriz {rows}×{cols}"
|
||
|
||
elif isinstance(result, list) and len(result) > 5:
|
||
tag_name = f"list_{id(result)}"
|
||
display_text = f"📋 Ver Lista ({len(result)} elementos)"
|
||
|
||
elif isinstance(result, dict) and len(result) > 3:
|
||
tag_name = f"dict_{id(result)}"
|
||
display_text = f"🔍 Ver Diccionario ({len(result)} entradas)"
|
||
|
||
elif hasattr(result, '__dict__') and len(str(result)) > 100:
|
||
tag_name = f"object_{id(result)}"
|
||
display_text = f"🔍 Ver Detalles ({type(result).__name__})"
|
||
|
||
# 🔧 CORRECCIÓN: Solo crear tag si se encontró un tipo interactivo
|
||
if tag_name and display_text:
|
||
try:
|
||
# Configurar tag
|
||
text_widget.tag_configure(
|
||
tag_name,
|
||
foreground="#4fc3f7",
|
||
underline=True,
|
||
font=("Consolas", 11, "underline")
|
||
)
|
||
|
||
# Bind click event
|
||
text_widget.tag_bind(
|
||
tag_name,
|
||
"<Button-1>",
|
||
lambda e, r=result: self._handle_interactive_click(r)
|
||
)
|
||
|
||
text_widget.tag_bind(
|
||
tag_name,
|
||
"<Enter>",
|
||
lambda e: text_widget.config(cursor="hand2")
|
||
)
|
||
|
||
text_widget.tag_bind(
|
||
tag_name,
|
||
"<Leave>",
|
||
lambda e: text_widget.config(cursor="")
|
||
)
|
||
|
||
return (tag_name, display_text)
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ Error creando tag interactivo: {e}")
|
||
return None
|
||
|
||
return None
|
||
|
||
def _handle_interactive_click(self, result: Any):
|
||
"""Maneja clicks en elementos interactivos"""
|
||
window_key = f"{type(result).__name__}_{id(result)}"
|
||
|
||
# Si ya existe la ventana, enfocarla
|
||
if window_key in self.open_windows:
|
||
window = self.open_windows[window_key]
|
||
try:
|
||
if window.winfo_exists():
|
||
window.lift()
|
||
window.focus_set()
|
||
return
|
||
else:
|
||
del self.open_windows[window_key]
|
||
except tk.TclError:
|
||
del self.open_windows[window_key]
|
||
|
||
# Crear nueva ventana
|
||
try:
|
||
if isinstance(result, PlotResult):
|
||
self._show_plot_window(result, window_key)
|
||
elif isinstance(result, sympy.Matrix):
|
||
self._show_matrix_window(result, window_key)
|
||
elif isinstance(result, list):
|
||
self._show_list_window(result, window_key)
|
||
elif isinstance(result, dict):
|
||
self._show_dict_window(result, window_key)
|
||
else:
|
||
self._show_object_window(result, window_key)
|
||
except Exception as e:
|
||
print(f"❌ Error abriendo ventana interactiva: {e}")
|
||
|
||
def _show_plot_window(self, plot_result: PlotResult, window_key: str):
|
||
"""Muestra ventana con plot matplotlib"""
|
||
window = self._create_base_window(f"Plot - {plot_result.plot_type}", "800x600")
|
||
self.open_windows[window_key] = window
|
||
|
||
try:
|
||
fig, ax = plt.subplots(figsize=(8, 6))
|
||
|
||
if plot_result.plot_type == "plot":
|
||
self._create_2d_plot(fig, ax, plot_result.args, plot_result.kwargs)
|
||
elif plot_result.plot_type == "plot3d":
|
||
self._create_3d_plot(fig, plot_result.args, plot_result.kwargs)
|
||
|
||
# Embed en tkinter
|
||
canvas = FigureCanvasTkAgg(fig, window)
|
||
canvas.draw()
|
||
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Toolbar para interactividad
|
||
try:
|
||
from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
|
||
toolbar = NavigationToolbar2Tk(canvas, window)
|
||
toolbar.update()
|
||
except ImportError:
|
||
pass # Si no está disponible, continuar sin toolbar
|
||
|
||
except Exception as e:
|
||
error_label = tk.Label(
|
||
window,
|
||
text=f"Error generando plot: {e}",
|
||
fg="red",
|
||
bg="#2b2b2b",
|
||
font=("Consolas", 12)
|
||
)
|
||
error_label.pack(pady=20)
|
||
|
||
print(f"❌ Error en plot: {e}")
|
||
|
||
def _create_2d_plot(self, fig, ax, args, kwargs):
|
||
"""Crea plot 2D usando SymPy"""
|
||
if len(args) >= 1:
|
||
expr = args[0]
|
||
|
||
try:
|
||
if len(args) >= 2:
|
||
# Rango especificado: (variable, start, end)
|
||
var_range = args[1]
|
||
if isinstance(var_range, tuple) and len(var_range) == 3:
|
||
var, start, end = var_range
|
||
x_vals = np.linspace(float(start), float(end), 1000)
|
||
|
||
# Evaluar expresión
|
||
f = sympy.lambdify(var, expr, 'numpy')
|
||
y_vals = f(x_vals)
|
||
|
||
ax.plot(x_vals, y_vals, **kwargs)
|
||
ax.set_xlabel(str(var))
|
||
ax.set_ylabel(str(expr))
|
||
ax.grid(True)
|
||
ax.set_title(f"Plot: {expr}")
|
||
else:
|
||
# Rango por defecto
|
||
free_symbols = list(expr.free_symbols)
|
||
if free_symbols:
|
||
var = free_symbols[0]
|
||
x_vals = np.linspace(-10, 10, 1000)
|
||
f = sympy.lambdify(var, expr, 'numpy')
|
||
y_vals = f(x_vals)
|
||
|
||
ax.plot(x_vals, y_vals, **kwargs)
|
||
ax.set_xlabel(str(var))
|
||
ax.set_ylabel(str(expr))
|
||
ax.grid(True)
|
||
ax.set_title(f"Plot: {expr}")
|
||
|
||
except Exception as e:
|
||
ax.text(0.5, 0.5, f"Error: {e}",
|
||
transform=ax.transAxes, ha='center', va='center')
|
||
ax.set_title("Error en Plot")
|
||
|
||
def _create_3d_plot(self, fig, args, kwargs):
|
||
"""Crea plot 3D"""
|
||
try:
|
||
ax = fig.add_subplot(111, projection='3d')
|
||
|
||
if len(args) >= 3:
|
||
expr = args[0]
|
||
x_range = args[1] # (x, x_start, x_end)
|
||
y_range = args[2] # (y, y_start, y_end)
|
||
|
||
if isinstance(x_range, tuple) and isinstance(y_range, tuple):
|
||
x_var, x_start, x_end = x_range
|
||
y_var, y_start, y_end = y_range
|
||
|
||
x_vals = np.linspace(float(x_start), float(x_end), 50)
|
||
y_vals = np.linspace(float(y_start), float(y_end), 50)
|
||
X, Y = np.meshgrid(x_vals, y_vals)
|
||
|
||
f = sympy.lambdify([x_var, y_var], expr, 'numpy')
|
||
Z = f(X, Y)
|
||
|
||
ax.plot_surface(X, Y, Z, **kwargs)
|
||
ax.set_xlabel(str(x_var))
|
||
ax.set_ylabel(str(y_var))
|
||
ax.set_zlabel(str(expr))
|
||
ax.set_title(f"3D Plot: {expr}")
|
||
|
||
except Exception as e:
|
||
ax.text2D(0.5, 0.5, f"Error: {e}", transform=ax.transAxes)
|
||
|
||
def _show_matrix_window(self, matrix: sympy.Matrix, window_key: str):
|
||
"""Muestra ventana con matriz formateada"""
|
||
rows, cols = matrix.shape
|
||
window = self._create_base_window(f"Matriz {rows}×{cols}", "600x400")
|
||
self.open_windows[window_key] = window
|
||
|
||
# Crear frame con scroll
|
||
frame = tk.Frame(window, bg="#2b2b2b")
|
||
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
text_widget = scrolledtext.ScrolledText(
|
||
frame,
|
||
font=("Courier New", 12),
|
||
bg="#1e1e1e",
|
||
fg="#d4d4d4",
|
||
insertbackground="#ffffff",
|
||
wrap=tk.NONE
|
||
)
|
||
text_widget.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Formatear matriz
|
||
matrix_str = self._format_matrix(matrix)
|
||
text_widget.insert("1.0", matrix_str)
|
||
text_widget.config(state="disabled")
|
||
|
||
# Botones de utilidad
|
||
button_frame = tk.Frame(window, bg="#2b2b2b")
|
||
button_frame.pack(fill=tk.X, padx=10, pady=5)
|
||
|
||
try:
|
||
det_btn = tk.Button(
|
||
button_frame,
|
||
text="Determinante",
|
||
command=lambda: self._show_matrix_property(matrix, "determinante", matrix.det()),
|
||
bg="#3c3c3c",
|
||
fg="white"
|
||
)
|
||
det_btn.pack(side=tk.LEFT, padx=5)
|
||
except:
|
||
pass # Skip si la matriz no es cuadrada
|
||
|
||
if matrix.is_square:
|
||
try:
|
||
inv_btn = tk.Button(
|
||
button_frame,
|
||
text="Inversa",
|
||
command=lambda: self._show_matrix_property(matrix, "inversa", matrix.inv()),
|
||
bg="#3c3c3c",
|
||
fg="white"
|
||
)
|
||
inv_btn.pack(side=tk.LEFT, padx=5)
|
||
except:
|
||
pass # Skip si no es invertible
|
||
|
||
def _format_matrix(self, matrix: sympy.Matrix) -> str:
|
||
"""Formatea una matriz para display"""
|
||
rows, cols = matrix.shape
|
||
|
||
# Calcular ancho máximo de elementos
|
||
max_width = 0
|
||
for i in range(rows):
|
||
for j in range(cols):
|
||
element_str = str(matrix[i, j])
|
||
max_width = max(max_width, len(element_str))
|
||
|
||
max_width = max(max_width, 8) # Mínimo 8 caracteres
|
||
|
||
# Construir representación
|
||
lines = []
|
||
lines.append("┌" + " " * (max_width * cols + cols - 1) + "┐")
|
||
|
||
for i in range(rows):
|
||
line = "│"
|
||
for j in range(cols):
|
||
element_str = str(matrix[i, j])
|
||
padded = element_str.center(max_width)
|
||
line += padded
|
||
if j < cols - 1:
|
||
line += " "
|
||
line += "│"
|
||
lines.append(line)
|
||
|
||
lines.append("└" + " " * (max_width * cols + cols - 1) + "┘")
|
||
|
||
return "\n".join(lines)
|
||
|
||
def _show_matrix_property(self, matrix: sympy.Matrix, prop_name: str, prop_value: Any):
|
||
"""Muestra propiedad de matriz en ventana separada"""
|
||
prop_window = self._create_base_window(f"Matriz - {prop_name.title()}", "400x300")
|
||
|
||
text_widget = scrolledtext.ScrolledText(
|
||
prop_window,
|
||
font=("Courier New", 12),
|
||
bg="#1e1e1e",
|
||
fg="#d4d4d4",
|
||
insertbackground="#ffffff"
|
||
)
|
||
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
if isinstance(prop_value, sympy.Matrix):
|
||
content = f"{prop_name.title()}:\n\n{self._format_matrix(prop_value)}"
|
||
else:
|
||
content = f"{prop_name.title()}: {prop_value}"
|
||
|
||
text_widget.insert("1.0", content)
|
||
text_widget.config(state="disabled")
|
||
|
||
def _show_list_window(self, lst: list, window_key: str):
|
||
"""Muestra ventana con lista expandida"""
|
||
window = self._create_base_window(f"Lista ({len(lst)} elementos)", "500x400")
|
||
self.open_windows[window_key] = window
|
||
|
||
text_widget = scrolledtext.ScrolledText(
|
||
window,
|
||
font=("Consolas", 11),
|
||
bg="#1e1e1e",
|
||
fg="#d4d4d4",
|
||
insertbackground="#ffffff"
|
||
)
|
||
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
content = "Elementos de la lista:\n\n"
|
||
for i, item in enumerate(lst):
|
||
content += f"[{i}] {item}\n"
|
||
|
||
text_widget.insert("1.0", content)
|
||
text_widget.config(state="disabled")
|
||
|
||
def _show_dict_window(self, dct: dict, window_key: str):
|
||
"""Muestra ventana con diccionario expandido"""
|
||
window = self._create_base_window(f"Diccionario ({len(dct)} entradas)", "500x400")
|
||
self.open_windows[window_key] = window
|
||
|
||
text_widget = scrolledtext.ScrolledText(
|
||
window,
|
||
font=("Consolas", 11),
|
||
bg="#1e1e1e",
|
||
fg="#d4d4d4",
|
||
insertbackground="#ffffff"
|
||
)
|
||
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
content = "Entradas del diccionario:\n\n"
|
||
for key, value in dct.items():
|
||
content += f"{key}: {value}\n"
|
||
|
||
text_widget.insert("1.0", content)
|
||
text_widget.config(state="disabled")
|
||
|
||
def _show_object_window(self, obj: Any, window_key: str):
|
||
"""Muestra ventana con detalles de objeto"""
|
||
window = self._create_base_window(f"Objeto - {type(obj).__name__}", "600x500")
|
||
self.open_windows[window_key] = window
|
||
|
||
text_widget = scrolledtext.ScrolledText(
|
||
window,
|
||
font=("Consolas", 11),
|
||
bg="#1e1e1e",
|
||
fg="#d4d4d4",
|
||
insertbackground="#ffffff"
|
||
)
|
||
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
content = f"Objeto: {type(obj).__name__}\n\n"
|
||
content += f"Valor: {obj}\n\n"
|
||
content += f"Representación: {repr(obj)}\n\n"
|
||
|
||
if hasattr(obj, '__dict__'):
|
||
content += "Atributos:\n"
|
||
for attr, value in obj.__dict__.items():
|
||
content += f" {attr}: {value}\n"
|
||
|
||
content += "\nMétodos disponibles:\n"
|
||
for attr in dir(obj):
|
||
if not attr.startswith('_') and callable(getattr(obj, attr, None)):
|
||
content += f" {attr}()\n"
|
||
|
||
text_widget.insert("1.0", content)
|
||
text_widget.config(state="disabled")
|
||
|
||
def _create_base_window(self, title: str, geometry: str = "500x400") -> Toplevel:
|
||
"""Crea ventana base con estilo consistente"""
|
||
window = Toplevel(self.parent)
|
||
window.title(title)
|
||
window.geometry(geometry)
|
||
window.configure(bg="#2b2b2b")
|
||
window.transient(self.parent)
|
||
|
||
# Centrar ventana
|
||
window.update_idletasks()
|
||
x = (window.winfo_screenwidth() // 2) - (window.winfo_width() // 2)
|
||
y = (window.winfo_screenheight() // 2) - (window.winfo_height() // 2)
|
||
window.geometry(f"+{x}+{y}")
|
||
|
||
return window
|
||
|
||
def close_all_windows(self):
|
||
"""Cierra todas las ventanas interactivas"""
|
||
for window in list(self.open_windows.values()):
|
||
try:
|
||
if window.winfo_exists():
|
||
window.destroy()
|
||
except tk.TclError:
|
||
pass
|
||
self.open_windows.clear()
|
||
|
||
|
||
# Función de testing
|
||
def test_interactive_results():
|
||
"""Test del sistema de resultados interactivos"""
|
||
print("🧪 Test Interactive Results - Versión Corregida")
|
||
print("=" * 50)
|
||
|
||
root = tk.Tk()
|
||
root.title("Test Interactive Results")
|
||
|
||
manager = InteractiveResultManager(root)
|
||
|
||
# Crear widget de texto de prueba
|
||
text_widget = tk.Text(root, height=20, width=80, bg="#1e1e1e", fg="#d4d4d4")
|
||
text_widget.pack(padx=10, pady=10)
|
||
|
||
# Test con PlotResult
|
||
print("📊 Testing PlotResult...")
|
||
plot_result = PlotResult("plot", (sympy.sin(sympy.Symbol('x')), (sympy.Symbol('x'), -10, 10)), {})
|
||
tag_info = manager.create_interactive_tag(plot_result, text_widget, "1.0")
|
||
if tag_info:
|
||
tag, display = tag_info
|
||
text_widget.insert("end", f"Plot test: {display}\n", tag)
|
||
print(f" ✅ PlotResult tag creado: {display}")
|
||
else:
|
||
print(f" ❌ PlotResult tag NO creado")
|
||
|
||
# Test con matriz
|
||
print("📋 Testing Matrix...")
|
||
matrix = sympy.Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
||
tag_info = manager.create_interactive_tag(matrix, text_widget, "2.0")
|
||
if tag_info:
|
||
tag, display = tag_info
|
||
text_widget.insert("end", f"Matrix test: {display}\n", tag)
|
||
print(f" ✅ Matrix tag creado: {display}")
|
||
else:
|
||
print(f" ❌ Matrix tag NO creado")
|
||
|
||
# Test con lista
|
||
print("📋 Testing List...")
|
||
long_list = list(range(20))
|
||
tag_info = manager.create_interactive_tag(long_list, text_widget, "3.0")
|
||
if tag_info:
|
||
tag, display = tag_info
|
||
text_widget.insert("end", f"List test: {display}\n", tag)
|
||
print(f" ✅ List tag creado: {display}")
|
||
else:
|
||
print(f" ❌ List tag NO creado")
|
||
|
||
print("\n✅ Test completado. Ventana interactiva abierta.")
|
||
print("🔍 Haz click en los elementos para probar funcionalidad.")
|
||
|
||
root.mainloop()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
test_interactive_results()
|