Calc/interactive_results.py

442 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Sistema de resultados interactivos con tags clickeables
"""
import tkinter as tk
from tkinter import Toplevel, scrolledtext
import sympy
from typing import Any, Optional, Dict, List
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
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[str]:
"""
Crea un tag interactivo para un resultado si es necesario
Returns:
Tag name si se creó, None si no es necesario
"""
tag_name = None
display_text = ""
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__})"
if tag_name:
# 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
return None, str(result)
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]
if window.winfo_exists():
window.lift()
window.focus_set()
return
else:
del self.open_windows[window_key]
# Crear nueva ventana
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)
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)
except Exception as e:
error_label = tk.Label(
window,
text=f"Error generando plot: {e}",
fg="red",
bg="#2b2b2b"
)
error_label.pack(pady=20)
def _create_2d_plot(self, fig, ax, args, kwargs):
"""Crea plot 2D usando SymPy"""
if len(args) >= 1:
expr = args[0]
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}")
def _create_3d_plot(self, fig, args, kwargs):
"""Crea plot 3D"""
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}")
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)
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)
if matrix.is_square:
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)
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))
# 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 self.open_windows.values():
if window.winfo_exists():
window.destroy()
self.open_windows.clear()
# Importar PlotResult desde el motor de evaluación
class PlotResult:
"""Placeholder para resultados de plotting"""
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})"
# Función de testing
def test_interactive_results():
"""Test del sistema de resultados interactivos"""
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)
text_widget.pack(padx=10, pady=10)
# Test con matriz
matrix = sympy.Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
tag, display = manager.create_interactive_tag(matrix, text_widget, "1.0")
text_widget.insert("end", f"Matriz test: {display}\n", tag)
# Test con lista
long_list = list(range(20))
tag, display = manager.create_interactive_tag(long_list, text_widget, "2.0")
text_widget.insert("end", f"Lista test: {display}\n", tag)
# Test con plot
plot_result = PlotResult("plot", (sympy.sin(sympy.Symbol('x')), (sympy.Symbol('x'), -10, 10)), {})
tag, display = manager.create_interactive_tag(plot_result, text_widget, "3.0")
text_widget.insert("end", f"Plot test: {display}\n", tag)
root.mainloop()
if __name__ == "__main__":
test_interactive_results()