Calc/app/tl_popup.py

592 lines
22 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 PYSIDE6
"""
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QTextEdit,
QLabel, QLineEdit, QPushButton, QFrame, QScrollArea)
from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QFont, QTextCursor, QTextCharFormat, QColor
from PySide6.QtWebEngineWidgets import QWebEngineView
import sympy
from typing import Any, Optional, Dict, List, Tuple
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
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(QWidget):
"""Maneja resultados interactivos con ventanas emergentes - PySide6"""
plot_requested = Signal(object) # Señal para solicitar mostrar plot en MathJax
def __init__(self, parent_window=None):
super().__init__()
self.parent = parent_window
self.open_windows: Dict[str, QWidget] = {}
self.update_input_callback = None
self._setup_styles()
def _setup_styles(self):
"""Configurar estilos para ventanas emergentes"""
self.window_style = """
QWidget {
background-color: #2b2b2b;
color: #d4d4d4;
}
QTextEdit {
background-color: #1e1e1e;
color: #d4d4d4;
border: 1px solid #3c3c3c;
font-family: 'Consolas';
font-size: 11px;
}
QLineEdit {
background-color: #1e1e1e;
color: #d4d4d4;
border: 1px solid #3c3c3c;
padding: 5px;
font-family: 'Consolas';
font-size: 11px;
}
QPushButton {
background-color: #4fc3f7;
color: white;
border: none;
padding: 5px 10px;
font-family: 'Consolas';
font-size: 9px;
}
QPushButton:hover {
background-color: #29b6f6;
}
QPushButton:pressed {
background-color: #0288d1;
}
QLabel {
color: #d4d4d4;
font-family: 'Consolas';
font-size: 10px;
}
"""
def set_update_callback(self, callback):
"""Establece el callback para actualizar el panel de entrada"""
self.update_input_callback = callback
def create_interactive_link(self, result: Any) -> Optional[Tuple[str, str, Any]]:
"""
Crea un link interactivo para un resultado si es necesario
Returns:
(link_id, display_text, result_object) si se creó link, None si no es necesario
"""
link_id = None
display_text = ""
if isinstance(result, PlotResult):
link_id = f"plot_{id(result)}"
display_text = f"📊 Ver {result.plot_type.title()}"
elif isinstance(result, sympy.Matrix):
link_id = f"matrix_{id(result)}"
rows, cols = result.shape
display_text = f"📋 Ver Matriz {rows}×{cols}"
elif isinstance(result, list) and len(result) > 5:
link_id = f"list_{id(result)}"
display_text = f"📋 Ver Lista ({len(result)} elementos)"
elif isinstance(result, dict) and len(result) > 3:
link_id = f"dict_{id(result)}"
display_text = f"🔍 Ver Diccionario ({len(result)} entradas)"
elif hasattr(result, '__dict__') and len(str(result)) > 100:
link_id = f"object_{id(result)}"
display_text = f"🔍 Ver Detalles ({type(result).__name__})"
if link_id and display_text:
return (link_id, display_text, result)
return None
def handle_interactive_click(self, result: Any, is_mathjax_click: bool = False):
"""Maneja clicks en elementos interactivos"""
if isinstance(result, PlotResult):
if not is_mathjax_click:
# Primera vez: mostrar en MathJax
self.plot_requested.emit(result)
else:
# Click en MathJax: abrir ventana emergente
self._show_plot_window(result)
else:
# Otros tipos siempre abren ventana
self._handle_other_interactive_click(result)
def _handle_other_interactive_click(self, result: Any):
"""Maneja clicks en elementos no-plot"""
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 and not window.isHidden():
window.raise_()
window.activateWindow()
return
else:
del self.open_windows[window_key]
# Crear nueva ventana
try:
if 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):
"""Muestra ventana con plot matplotlib e interfaz de edición"""
window_key = f"plot_{id(plot_result)}"
# Si ya existe la ventana, enfocarla
if window_key in self.open_windows:
window = self.open_windows[window_key]
if window and not window.isHidden():
window.raise_()
window.activateWindow()
return
else:
del self.open_windows[window_key]
# Obtener posición de la ventana principal
if self.parent:
parent_pos = self.parent.pos()
parent_size = self.parent.size()
plot_window_x = parent_pos.x() + parent_size.width()
plot_window_y = parent_pos.y()
else:
plot_window_x = 100
plot_window_y = 100
# Crear ventana
window = QWidget()
window.setWindowTitle(f"Plot - {plot_result.plot_type}")
window.resize(700, 600)
window.move(plot_window_x, plot_window_y)
window.setStyleSheet(self.window_style)
# Layout principal
layout = QVBoxLayout(window)
# Frame superior para edición
edit_frame = QFrame()
edit_layout = QHBoxLayout(edit_frame)
# Label y campo de edición
label = QLabel("Expresión:")
expression_edit = QLineEdit(plot_result.original_expression)
redraw_btn = QPushButton("Redibujar")
edit_layout.addWidget(label)
edit_layout.addWidget(expression_edit)
edit_layout.addWidget(redraw_btn)
# Frame para el canvas del plot
canvas_frame = QFrame()
canvas_layout = QVBoxLayout(canvas_frame)
layout.addWidget(edit_frame)
layout.addWidget(canvas_frame)
# Crear plot inicial
self._create_plot_in_frame(plot_result, canvas_frame)
# Conectar botón de redibujar
def redraw():
self._redraw_plot(plot_result, canvas_frame, expression_edit.text())
redraw_btn.clicked.connect(redraw)
# Configurar cierre
def on_close():
edited_expression = expression_edit.text().strip()
original_expression = plot_result.original_expression.strip()
if edited_expression != original_expression and self.update_input_callback:
self.update_input_callback(original_expression, edited_expression)
if window_key in self.open_windows:
del self.open_windows[window_key]
window.closeEvent = lambda event: (on_close(), event.accept())
# Registrar y mostrar ventana
self.open_windows[window_key] = window
window.show()
# Focus en el campo de edición
expression_edit.setFocus()
expression_edit.selectAll()
def _redraw_plot(self, plot_result: PlotResult, canvas_frame: QFrame, new_expression: str):
"""Redibuja el plot con una nueva expresión"""
try:
# Limpiar el frame actual
layout = canvas_frame.layout()
if layout:
while layout.count():
child = layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
# 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:],
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
if not canvas_frame.layout():
canvas_frame.setLayout(QVBoxLayout())
error_label = QLabel(f"Error en expresión: {e}")
error_label.setStyleSheet("color: #f44747; font-size: 11px;")
error_label.setWordWrap(True)
canvas_frame.layout().addWidget(error_label)
def _create_plot_in_frame(self, plot_result: PlotResult, parent_frame: QFrame):
"""Crea el plot dentro del frame especificado"""
try:
if not parent_frame.layout():
parent_frame.setLayout(QVBoxLayout())
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 PySide6
canvas = FigureCanvasQTAgg(fig)
parent_frame.layout().addWidget(canvas)
except Exception as e:
if not parent_frame.layout():
parent_frame.setLayout(QVBoxLayout())
error_label = QLabel(f"Error generando plot: {e}")
error_label.setStyleSheet("color: #f44747; font-size: 12px;")
error_label.setWordWrap(True)
parent_frame.layout().addWidget(error_label)
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}"
window = self._create_base_window(window_title, 600, 500)
self.open_windows[window_key] = window
layout = QVBoxLayout(window)
# Widget de texto con scroll
text_widget = QTextEdit()
text_widget.setFont(QFont("Courier New", 12))
text_widget.setReadOnly(True)
# Formatear matriz
matrix_str = self._format_matrix(matrix)
text_widget.setPlainText(matrix_str)
layout.addWidget(text_widget)
# Botones de utilidad
button_frame = QFrame()
button_layout = QHBoxLayout(button_frame)
if matrix.is_square:
try:
det_btn = QPushButton("Determinante")
det_btn.clicked.connect(
lambda: self._show_matrix_property(matrix, "determinante", matrix.det())
)
button_layout.addWidget(det_btn)
inv_btn = QPushButton("Inversa")
inv_btn.clicked.connect(
lambda: self._show_matrix_property(matrix, "inversa", matrix.inv())
)
button_layout.addWidget(inv_btn)
except:
pass
layout.addWidget(button_frame)
window.show()
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"""
window = self._create_base_window(f"Matriz - {prop_name.title()}", 400, 300)
layout = QVBoxLayout(window)
text_widget = QTextEdit()
text_widget.setFont(QFont("Courier New", 12))
text_widget.setReadOnly(True)
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.setPlainText(content)
layout.addWidget(text_widget)
window.show()
def _show_list_window(self, lst: list, window_key: str):
"""Muestra ventana con lista expandida"""
window_title = f"Lista ({len(lst)} elementos)"
window = self._create_base_window(window_title, 500, 400)
self.open_windows[window_key] = window
layout = QVBoxLayout(window)
text_widget = QTextEdit()
text_widget.setFont(QFont("Consolas", 11))
text_widget.setReadOnly(True)
content = "Elementos de la lista:\n\n"
for i, item in enumerate(lst):
content += f"[{i}] {item}\n"
text_widget.setPlainText(content)
layout.addWidget(text_widget)
window.show()
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)", 500, 400)
self.open_windows[window_key] = window
layout = QVBoxLayout(window)
text_widget = QTextEdit()
text_widget.setFont(QFont("Consolas", 11))
text_widget.setReadOnly(True)
content = "Entradas del diccionario:\n\n"
for key, value in dct.items():
content += f"{key}: {value}\n"
text_widget.setPlainText(content)
layout.addWidget(text_widget)
window.show()
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__}", 600, 500)
self.open_windows[window_key] = window
layout = QVBoxLayout(window)
text_widget = QTextEdit()
text_widget.setFont(QFont("Consolas", 11))
text_widget.setReadOnly(True)
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.setPlainText(content)
layout.addWidget(text_widget)
window.show()
def _create_base_window(self, title: str, width: int = 500, height: int = 400) -> QWidget:
"""Crea ventana base con estilo consistente"""
window = QWidget()
window.setWindowTitle(title)
window.resize(width, height)
window.setStyleSheet(self.window_style)
# Centrar ventana
if self.parent:
parent_pos = self.parent.pos()
parent_size = self.parent.size()
x = parent_pos.x() + (parent_size.width() - width) // 2
y = parent_pos.y() + (parent_size.height() - height) // 2
window.move(x, y)
return window
def close_all_windows(self):
"""Cierra todas las ventanas interactivas"""
windows_to_close = list(self.open_windows.values())
for window in windows_to_close:
if window and not window.isHidden():
window.close()
self.open_windows.clear()
# Cerrar todas las figuras de matplotlib
try:
plt.close('all')
except:
pass