670 lines
26 KiB
Python
670 lines
26 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, 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
|