Calc/tl_popup.py

670 lines
26 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 - 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, original_expression: str = None):
self.plot_type = plot_type
self.args = args
self.kwargs = kwargs
self.original_expression = original_expression or ""
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] = {}
self.update_input_callback = None # Callback para actualizar el panel de entrada
def set_update_callback(self, callback):
"""Establece el callback para actualizar el panel de entrada"""
self.update_input_callback = callback
def create_interactive_tag(self, result: Any, text_widget: tk.Text) -> 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 e interfaz de edición"""
# Asegurar que las dimensiones de la ventana principal estén actualizadas
self.parent.update_idletasks()
parent_x = self.parent.winfo_x()
parent_y = self.parent.winfo_y()
parent_width = self.parent.winfo_width()
parent_height = self.parent.winfo_height()
# Definir dimensiones y posición para la ventana del plot
plot_window_width = 700 # Aumentado para dar espacio al campo de edición
plot_window_height = parent_height # Misma altura que la ventana principal
# Posicionar la ventana del plot a la derecha de la ventana principal
plot_window_x = parent_x + parent_width
plot_window_y = parent_y # Misma posición Y que la ventana principal
window_title = f"Plot - {plot_result.plot_type}"
# Crear la ventana base especificando la posición
window = self._create_base_window(
window_title,
width=plot_window_width,
height=plot_window_height,
pos_x=plot_window_x,
pos_y=plot_window_y
)
self.open_windows[window_key] = window
# Frame principal para organizar la ventana
main_frame = tk.Frame(window, bg="#2b2b2b")
main_frame.pack(fill=tk.BOTH, expand=True)
# Frame superior para el campo de edición
edit_frame = tk.Frame(main_frame, bg="#2b2b2b")
edit_frame.pack(fill=tk.X, padx=10, pady=5)
# Label para el campo de edición
tk.Label(
edit_frame,
text="Expresión:",
bg="#2b2b2b",
fg="#d4d4d4",
font=("Consolas", 10)
).pack(side=tk.LEFT)
# Campo de entrada para editar la expresión
self.current_expression = tk.StringVar()
self.current_expression.set(plot_result.original_expression)
expression_entry = tk.Entry(
edit_frame,
textvariable=self.current_expression,
bg="#1e1e1e",
fg="#d4d4d4",
font=("Consolas", 11),
insertbackground="#ffffff"
)
expression_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 5))
# Botón para redibujar
redraw_btn = tk.Button(
edit_frame,
text="Redibujar",
command=lambda: self._redraw_plot(plot_result, canvas_frame, expression_entry.get()),
bg="#4fc3f7",
fg="white",
font=("Consolas", 9),
relief=tk.FLAT
)
redraw_btn.pack(side=tk.RIGHT, padx=5)
# Frame para el canvas del plot
canvas_frame = tk.Frame(main_frame, bg="#2b2b2b")
canvas_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# Configurar el protocolo de cierre para guardar la expresión editada
def on_window_close():
edited_expression = expression_entry.get().strip()
original_expression = plot_result.original_expression.strip()
# Si la expresión cambió y tenemos un callback, actualizar el panel de entrada
if edited_expression != original_expression and self.update_input_callback:
self.update_input_callback(original_expression, edited_expression)
# Limpiar la ventana del registro
if window_key in self.open_windows:
del self.open_windows[window_key]
# Cerrar la ventana
window.destroy()
window.protocol("WM_DELETE_WINDOW", on_window_close)
# Crear el plot inicial
self._create_plot_in_frame(plot_result, canvas_frame)
# Hacer focus en el campo de entrada para edición inmediata
expression_entry.focus_set()
expression_entry.select_range(0, tk.END)
def _redraw_plot(self, plot_result: PlotResult, canvas_frame: tk.Frame, new_expression: str):
"""Redibuja el plot con una nueva expresión"""
try:
# Limpiar el frame actual
for widget in canvas_frame.winfo_children():
widget.destroy()
# Evaluar la nueva expresión
import sympy as sp
# Crear contexto básico para evaluación
eval_context = {
'sin': sp.sin, 'cos': sp.cos, 'tan': sp.tan,
'exp': sp.exp, 'log': sp.log, 'sqrt': sp.sqrt,
'pi': sp.pi, 'e': sp.E, 'x': sp.Symbol('x'), 'y': sp.Symbol('y'),
'z': sp.Symbol('z'), 't': sp.Symbol('t')
}
# Evaluar la expresión
new_expr = sp.sympify(new_expression, locals=eval_context)
# Crear nuevo PlotResult con la expresión actualizada
new_plot_result = PlotResult(
plot_result.plot_type,
(new_expr,) + plot_result.args[1:], # Mantener argumentos adicionales
plot_result.kwargs,
new_expression
)
# Actualizar la expresión original en el objeto
plot_result.original_expression = new_expression
# Redibujar
self._create_plot_in_frame(new_plot_result, canvas_frame)
except Exception as e:
# Mostrar error en el frame
error_label = tk.Label(
canvas_frame,
text=f"Error en expresión: {e}",
fg="#f44747",
bg="#2b2b2b",
font=("Consolas", 11),
wraplength=600
)
error_label.pack(pady=20)
def _create_plot_in_frame(self, plot_result: PlotResult, parent_frame: tk.Frame):
"""Crea el plot dentro del frame especificado"""
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, parent_frame)
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, parent_frame)
toolbar.update()
except ImportError:
pass # Si no está disponible, continuar sin toolbar
except Exception as e:
error_label = tk.Label(
parent_frame,
text=f"Error generando plot: {e}",
fg="#f44747",
bg="#2b2b2b",
font=("Consolas", 12)
)
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]
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_title = f"Matriz {rows}×{cols}"
# Asegurar que las dimensiones de la ventana principal estén actualizadas
self.parent.update_idletasks()
parent_x = self.parent.winfo_x()
parent_y = self.parent.winfo_y()
parent_width = self.parent.winfo_width()
parent_height = self.parent.winfo_height()
# Definir dimensiones y posición para la ventana de la matriz
matrix_window_width = 600 # Ancho deseado
matrix_window_height = parent_height # Misma altura que la ventana principal
matrix_window_x = parent_x + parent_width # A la derecha
matrix_window_y = parent_y # Misma posición Y
window = self._create_base_window(window_title, width=matrix_window_width, height=matrix_window_height,
pos_x=matrix_window_x, pos_y=matrix_window_y)
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()}", width=400, height=300)
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_title = f"Lista ({len(lst)} elementos)"
# Asegurar que las dimensiones de la ventana principal estén actualizadas
self.parent.update_idletasks()
parent_x = self.parent.winfo_x()
parent_y = self.parent.winfo_y()
parent_width = self.parent.winfo_width()
parent_height = self.parent.winfo_height()
# Definir dimensiones y posición para la ventana de la lista
list_window_width = 500 # Ancho deseado
list_window_height = parent_height # Misma altura que la ventana principal
list_window_x = parent_x + parent_width # A la derecha
list_window_y = parent_y # Misma posición Y
window = self._create_base_window(window_title, width=list_window_width, height=list_window_height,
pos_x=list_window_x, pos_y=list_window_y)
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)", width=500, height=400)
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__}", width=600, height=500)
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,
width: int = 500,
height: int = 400,
pos_x: Optional[int] = None,
pos_y: Optional[int] = None) -> Toplevel:
"""Crea ventana base con estilo consistente y posición opcional"""
window = Toplevel(self.parent)
window.title(title)
window.configure(bg="#2b2b2b")
window.transient(self.parent) # Hace que la ventana aparezca encima del padre
# Construir la cadena de geometría completa WxH+X+Y
geometry_str = f"{width}x{height}"
if pos_x is not None and pos_y is not None:
# Usar posición provista, asegurándose de que no sea negativa
final_x = max(0, pos_x)
final_y = max(0, pos_y)
geometry_str += f"+{final_x}+{final_y}"
else:
# Centrar ventana si no se especifica posición
# Para centrar, necesitamos las dimensiones de la pantalla
# y las dimensiones de la ventana (width, height ya las tenemos)
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
center_x = (screen_width // 2) - (width // 2)
center_y = (screen_height // 2) - (height // 2)
final_x = max(0, center_x)
final_y = max(0, center_y)
geometry_str += f"+{final_x}+{final_y}"
window.geometry(geometry_str) # Aplicar tamaño y posición de una sola vez
return window
def close_all_windows(self):
"""Cierra todas las ventanas interactivas de forma segura"""
windows_to_close = list(self.open_windows.items())
for window_key, window in windows_to_close:
try:
if window and window.winfo_exists():
# Forzar el cierre del protocolo de cierre si existe
window.protocol("WM_DELETE_WINDOW", window.destroy)
window.quit() # Detener el mainloop de la ventana si lo tiene
window.destroy() # Destruir la ventana
except tk.TclError:
# La ventana ya fue destruida o no es válida
pass
except Exception as e:
print(f"Error cerrando ventana {window_key}: {e}")
# Limpiar el diccionario
self.open_windows.clear()
# Cerrar todas las figuras de matplotlib para liberar memoria
try:
plt.close('all')
except:
pass