""" 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, "", lambda e, r=result: self._handle_interactive_click(r) ) text_widget.tag_bind( tag_name, "", lambda e: text_widget.config(cursor="hand2") ) text_widget.tag_bind( tag_name, "", 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