2004 lines
75 KiB
Python
2004 lines
75 KiB
Python
"""
|
|
Calculadora MAV CAS Híbrida - Aplicación principal PySide6
|
|
VERSIÓN COMPLETA: Preserva TODA la funcionalidad de la versión tkinter
|
|
"""
|
|
import sys
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import time
|
|
from pathlib import Path
|
|
from typing import List, Dict, Any, Optional, Tuple
|
|
|
|
from PySide6.QtWidgets import (
|
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QTextEdit, QPlainTextEdit, QSplitter, QPushButton, QLabel,
|
|
QFrame, QMenuBar, QMenu, QStatusBar, QMessageBox, QFileDialog,
|
|
QListWidget, QListWidgetItem, QTextBrowser, QScrollArea,
|
|
QDockWidget, QToolBar
|
|
)
|
|
from PySide6.QtCore import (
|
|
Qt, QTimer, Signal, QUrl, QSize, QRect, QPoint, Slot,
|
|
QPropertyAnimation, QEasingCurve, QEvent
|
|
)
|
|
from PySide6.QtGui import (
|
|
QFont, QTextCursor, QTextCharFormat, QColor, QIcon,
|
|
QSyntaxHighlighter, QTextDocument, QKeySequence,
|
|
QShortcut, QFontMetrics, QPalette, QTextOption, QAction
|
|
)
|
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
|
from PySide6.QtWebEngineCore import QWebEngineSettings
|
|
|
|
# Importar componentes del CAS híbrido
|
|
from main_evaluation_puro import PureAlgebraicEngine, EvaluationResult
|
|
from tl_popup_pyside6 import InteractiveResultManager, PlotResult
|
|
from type_registry import get_registered_helper_functions, get_registered_base_context
|
|
import sympy
|
|
from sympy_helper import SympyTools as SympyHelper
|
|
|
|
|
|
class InputTextEdit(QPlainTextEdit):
|
|
"""Editor de texto personalizado con eventos mejorados"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.parent_app = parent
|
|
self.setLineWrapMode(QPlainTextEdit.NoWrap)
|
|
self.setFont(QFont("Consolas", 11))
|
|
|
|
def keyPressEvent(self, event):
|
|
"""Override para manejar autocompletado"""
|
|
if hasattr(self.parent_app, '_handle_key_press'):
|
|
# Dejar que el parent maneje primero para autocompletado
|
|
if self.parent_app._handle_key_press(event):
|
|
return
|
|
super().keyPressEvent(event)
|
|
|
|
|
|
class OutputTextEdit(QTextEdit):
|
|
"""Editor de salida con soporte para links clickeables"""
|
|
|
|
link_clicked = Signal(str, object) # link_id, result_object
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setReadOnly(True)
|
|
self.setFont(QFont("Consolas", 11))
|
|
self.clickable_links = {} # {(start, end): (link_id, object)}
|
|
|
|
def mousePressEvent(self, event):
|
|
"""Detecta clicks en links"""
|
|
if event.button() == Qt.LeftButton:
|
|
cursor = self.cursorForPosition(event.pos())
|
|
pos = cursor.position()
|
|
|
|
# Buscar si el click fue en un link
|
|
for (start, end), (link_id, obj) in self.clickable_links.items():
|
|
if start <= pos <= end:
|
|
self.link_clicked.emit(link_id, obj)
|
|
return
|
|
|
|
super().mousePressEvent(event)
|
|
|
|
|
|
class ExpandableLatexButton(QPushButton):
|
|
"""Botón expandible para mostrar/ocultar panel LaTeX"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__("📐", parent)
|
|
self.setFixedWidth(25)
|
|
self.setToolTip("Mostrar/ocultar panel LaTeX (F12)")
|
|
self.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #3c3c3c;
|
|
color: #80c7f7;
|
|
border: none;
|
|
font-size: 14px;
|
|
padding: 5px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #4fc3f7;
|
|
color: white;
|
|
}
|
|
QPushButton:checked {
|
|
background-color: #4fc3f7;
|
|
color: white;
|
|
}
|
|
""")
|
|
self.setCheckable(True)
|
|
|
|
|
|
class LatexPanel(QWidget):
|
|
"""Panel LaTeX con WebEngine o fallback a HTML"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.equations = []
|
|
self._webview_available = False
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
"""Configura la UI del panel"""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Header
|
|
header = QFrame()
|
|
header.setFixedHeight(30)
|
|
header.setStyleSheet("background-color: #1a1a1a; border-bottom: 1px solid #3c3c3c;")
|
|
header_layout = QHBoxLayout(header)
|
|
header_layout.setContentsMargins(10, 0, 10, 0)
|
|
|
|
title = QLabel("📐 Ecuaciones LaTeX")
|
|
title.setStyleSheet("color: #80c7f7; font-weight: bold;")
|
|
header_layout.addWidget(title)
|
|
header_layout.addStretch()
|
|
|
|
layout.addWidget(header)
|
|
|
|
# Intentar crear WebEngineView
|
|
try:
|
|
self.webview = QWebEngineView()
|
|
self.webview.setContextMenuPolicy(Qt.NoContextMenu)
|
|
self._setup_webview()
|
|
layout.addWidget(self.webview)
|
|
self._webview_available = True
|
|
logging.debug("✅ WebEngineView disponible para LaTeX")
|
|
except Exception as e:
|
|
logging.warning(f"⚠️ WebEngineView no disponible: {e}")
|
|
# Fallback a QTextBrowser
|
|
self.text_browser = QTextBrowser()
|
|
self.text_browser.setOpenExternalLinks(False)
|
|
self._setup_text_browser()
|
|
layout.addWidget(self.text_browser)
|
|
self._webview_available = False
|
|
|
|
def _setup_webview(self):
|
|
"""Configura WebEngineView con MathJax"""
|
|
html_content = self._generate_mathjax_html()
|
|
self.webview.setHtml(html_content)
|
|
|
|
def _setup_text_browser(self):
|
|
"""Configura el browser de texto como fallback"""
|
|
self.text_browser.setStyleSheet("""
|
|
QTextBrowser {
|
|
background-color: #1a1a1a;
|
|
color: #d4d4d4;
|
|
border: none;
|
|
font-family: 'Consolas';
|
|
font-size: 11px;
|
|
padding: 10px;
|
|
}
|
|
""")
|
|
self.text_browser.setHtml("""
|
|
<style>
|
|
body {
|
|
background-color: #1a1a1a;
|
|
color: #d4d4d4;
|
|
font-family: 'Consolas';
|
|
padding: 10px;
|
|
}
|
|
.equation {
|
|
margin: 10px 0;
|
|
padding: 10px;
|
|
background: #2d2d2d;
|
|
border-left: 3px solid #80c7f7;
|
|
border-radius: 3px;
|
|
}
|
|
.comment { border-left-color: #6a9955; }
|
|
.assignment { border-left-color: #dcdcaa; }
|
|
</style>
|
|
<div style="text-align: center; margin-top: 50px; color: #808080;">
|
|
📐 Panel de Ecuaciones<br>
|
|
<small>Las ecuaciones aparecerán aquí</small>
|
|
</div>
|
|
""")
|
|
|
|
def _generate_mathjax_html(self):
|
|
"""Genera HTML base con MathJax"""
|
|
return """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
|
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
|
<script>
|
|
window.MathJax = {
|
|
tex: {
|
|
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
|
displayMath: [['$$', '$$'], ['\\[', '\\]']],
|
|
processEscapes: true
|
|
},
|
|
chtml: {
|
|
scale: 1.1,
|
|
minScale: 0.5
|
|
}
|
|
};
|
|
</script>
|
|
<style>
|
|
body {
|
|
background-color: #1a1a1a;
|
|
color: #d4d4d4;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
line-height: 1.6;
|
|
}
|
|
.equation-block {
|
|
background: rgba(45, 45, 48, 0.8);
|
|
border-left: 4px solid;
|
|
margin: 12px 0;
|
|
padding: 15px 20px;
|
|
border-radius: 8px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.equation-block:hover {
|
|
transform: translateY(-1px);
|
|
background: rgba(45, 45, 48, 0.9);
|
|
}
|
|
.comment { border-left-color: #6a9955; }
|
|
.assignment { border-left-color: #dcdcaa; }
|
|
.equation { border-left-color: #c586c0; }
|
|
.symbolic { border-left-color: #9cdcfe; }
|
|
|
|
.equation-type {
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
margin-bottom: 8px;
|
|
opacity: 0.8;
|
|
}
|
|
.comment .equation-type { color: #6a9955; }
|
|
.assignment .equation-type { color: #dcdcaa; }
|
|
.equation .equation-type { color: #c586c0; }
|
|
.symbolic .equation-type { color: #9cdcfe; }
|
|
|
|
.info-message {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
color: #888;
|
|
font-style: italic;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="equations-container">
|
|
<div class="info-message">
|
|
📐 Panel de Ecuaciones LaTeX<br/>
|
|
<small>Las ecuaciones aparecerán aquí automáticamente</small>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function addEquation(type, content) {
|
|
var container = document.getElementById('equations-container');
|
|
|
|
// Limpiar mensaje inicial si existe
|
|
var infoMsg = container.querySelector('.info-message');
|
|
if (infoMsg) infoMsg.remove();
|
|
|
|
var equation = document.createElement('div');
|
|
equation.className = 'equation-block ' + type;
|
|
|
|
var typeLabel = document.createElement('div');
|
|
typeLabel.className = 'equation-type';
|
|
typeLabel.textContent = type.charAt(0).toUpperCase() + type.slice(1);
|
|
|
|
var mathContent = document.createElement('div');
|
|
if (type === 'comment') {
|
|
mathContent.style.fontStyle = 'italic';
|
|
mathContent.style.color = '#6a9955';
|
|
mathContent.textContent = content;
|
|
} else {
|
|
mathContent.innerHTML = '$$' + content + '$$';
|
|
}
|
|
|
|
equation.appendChild(typeLabel);
|
|
equation.appendChild(mathContent);
|
|
container.appendChild(equation);
|
|
|
|
// Re-renderizar MathJax
|
|
if (window.MathJax && type !== 'comment') {
|
|
MathJax.typesetPromise([equation]).catch(function(err) {
|
|
console.error('MathJax error:', err);
|
|
});
|
|
}
|
|
}
|
|
|
|
function clearEquations() {
|
|
var container = document.getElementById('equations-container');
|
|
container.innerHTML = '<div class="info-message">📐 Panel de Ecuaciones LaTeX<br/><small>Las ecuaciones aparecerán aquí automáticamente</small></div>';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>"""
|
|
|
|
def add_equation(self, eq_type: str, content: str):
|
|
"""Añade una ecuación al panel"""
|
|
self.equations.append({'type': eq_type, 'content': content})
|
|
|
|
if self._webview_available:
|
|
# Escapar contenido para JavaScript
|
|
escaped_content = content.replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"')
|
|
js_code = f"addEquation('{eq_type}', '{escaped_content}');"
|
|
self.webview.page().runJavaScript(js_code)
|
|
else:
|
|
# Actualizar HTML en text browser
|
|
self._update_text_browser()
|
|
|
|
def clear_equations(self):
|
|
"""Limpia todas las ecuaciones"""
|
|
self.equations.clear()
|
|
|
|
if self._webview_available:
|
|
self.webview.page().runJavaScript("clearEquations();")
|
|
else:
|
|
self._setup_text_browser() # Reset al estado inicial
|
|
|
|
def _update_text_browser(self):
|
|
"""Actualiza el contenido del text browser (fallback)"""
|
|
html_parts = ["""
|
|
<style>
|
|
body { background-color: #1a1a1a; color: #d4d4d4; font-family: 'Consolas'; }
|
|
.equation { margin: 10px 0; padding: 10px; background: #2d2d2d; border-left: 3px solid #80c7f7; border-radius: 3px; }
|
|
.comment { border-left-color: #6a9955; font-style: italic; }
|
|
.assignment { border-left-color: #dcdcaa; }
|
|
.equation-type { border-left-color: #c586c0; }
|
|
.symbolic { border-left-color: #9cdcfe; }
|
|
.type-label { font-size: 10px; color: #808080; margin-bottom: 5px; }
|
|
</style>
|
|
"""]
|
|
|
|
for eq in self.equations:
|
|
eq_type = eq['type']
|
|
content = eq['content']
|
|
|
|
type_label = eq_type.capitalize()
|
|
css_class = eq_type
|
|
|
|
if eq_type == 'comment':
|
|
html_parts.append(f'<div class="equation {css_class}"><div class="type-label">{type_label}</div>{content}</div>')
|
|
else:
|
|
# Para ecuaciones matemáticas, mostrar en formato de código
|
|
html_parts.append(f'<div class="equation {css_class}"><div class="type-label">{type_label}</div><code>{content}</code></div>')
|
|
|
|
self.text_browser.setHtml(''.join(html_parts))
|
|
|
|
|
|
class AutocompletePopup(QWidget):
|
|
"""Popup de autocompletado modeless"""
|
|
|
|
item_selected = Signal(str) # Emite el texto seleccionado
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(None, Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
|
self.setAttribute(Qt.WA_ShowWithoutActivating, True)
|
|
self.setFocusPolicy(Qt.NoFocus)
|
|
|
|
# Lista de sugerencias
|
|
self.listbox = QListWidget(self)
|
|
self.listbox.setFocusPolicy(Qt.NoFocus)
|
|
self.listbox.itemDoubleClicked.connect(self._on_item_double_clicked)
|
|
|
|
# Layout
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.addWidget(self.listbox)
|
|
|
|
# Estilo
|
|
self.setStyleSheet("""
|
|
QWidget {
|
|
background-color: #3c3f41;
|
|
border: 1px solid #4fc3f7;
|
|
border-radius: 4px;
|
|
}
|
|
QListWidget {
|
|
background-color: #3c3f41;
|
|
color: #bbbbbb;
|
|
border: none;
|
|
font-family: 'Consolas';
|
|
font-size: 10px;
|
|
outline: none;
|
|
}
|
|
QListWidget::item {
|
|
padding: 3px 8px;
|
|
border: none;
|
|
}
|
|
QListWidget::item:selected {
|
|
background-color: #007acc;
|
|
color: white;
|
|
}
|
|
QListWidget::item:hover {
|
|
background-color: #094771;
|
|
color: #e0e0e0;
|
|
}
|
|
""")
|
|
|
|
self._suggestions = []
|
|
self._selected_index = 0
|
|
|
|
def set_suggestions(self, suggestions: List[Tuple[str, str]]):
|
|
"""Establece las sugerencias [(nombre, descripción), ...]"""
|
|
self._suggestions = suggestions
|
|
self.listbox.clear()
|
|
|
|
for name, desc in suggestions:
|
|
self.listbox.addItem(f"{name} — {desc}")
|
|
|
|
if self.listbox.count() > 0:
|
|
self.listbox.setCurrentRow(0)
|
|
self._selected_index = 0
|
|
|
|
def navigate(self, direction: int):
|
|
"""Navega por las sugerencias (direction: -1=arriba, 1=abajo)"""
|
|
if self.listbox.count() == 0:
|
|
return
|
|
|
|
new_index = (self._selected_index + direction) % self.listbox.count()
|
|
self._selected_index = new_index
|
|
self.listbox.setCurrentRow(new_index)
|
|
|
|
def get_selected_text(self) -> str:
|
|
"""Obtiene el texto de la sugerencia seleccionada"""
|
|
if 0 <= self._selected_index < len(self._suggestions):
|
|
return self._suggestions[self._selected_index][0]
|
|
return ""
|
|
|
|
def _on_item_double_clicked(self, item):
|
|
"""Maneja doble click en un item"""
|
|
text = item.text().split(" —")[0].strip()
|
|
self.item_selected.emit(text)
|
|
|
|
def adjust_size(self):
|
|
"""Ajusta el tamaño del popup según el contenido"""
|
|
if self.listbox.count() == 0:
|
|
return
|
|
|
|
# Calcular tamaño necesario
|
|
max_width = 300
|
|
for i in range(self.listbox.count()):
|
|
item = self.listbox.item(i)
|
|
width = self.listbox.fontMetrics().horizontalAdvance(item.text()) + 20
|
|
max_width = max(max_width, width)
|
|
|
|
max_width = min(max_width, 600)
|
|
height = min(self.listbox.count() * 20 + 10, 200)
|
|
|
|
self.setFixedSize(max_width, height)
|
|
|
|
|
|
class HybridCalculatorPySide6(QMainWindow):
|
|
"""Aplicación principal del CAS híbrido - VERSIÓN COMPLETA"""
|
|
|
|
SETTINGS_FILE = "hybrid_calc_settings.json"
|
|
HISTORY_FILE = "hybrid_calc_history.txt"
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
# Configurar logging
|
|
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
# Motor y managers
|
|
self.engine = PureAlgebraicEngine()
|
|
self.interactive_manager = None
|
|
|
|
# Configuración
|
|
self.settings = self._load_settings()
|
|
self.debug = self.settings.get("debug_mode", False)
|
|
|
|
# ========== VARIABLES DE AUTOCOMPLETADO (COMPLETO COMO EN TKINTER) ==========
|
|
self._autocomplete_popup = None
|
|
self._autocomplete_active = False
|
|
self._autocomplete_suggestions = []
|
|
self._autocomplete_filter_text = ""
|
|
self._autocomplete_trigger_pos = None
|
|
self._popup_disabled_until_next_dot = False
|
|
self._variable_popup_active = False
|
|
self._last_navigation_time = 0
|
|
self._last_input_change = 0
|
|
self._current_suggestions = []
|
|
self._is_global_popup = False
|
|
|
|
# Timers
|
|
self._debounce_timer = QTimer()
|
|
self._debounce_timer.setSingleShot(True)
|
|
self._debounce_timer.timeout.connect(self._evaluate_and_update)
|
|
|
|
self._variable_popup_timer = QTimer()
|
|
self._variable_popup_timer.setSingleShot(True)
|
|
self._variable_popup_timer.timeout.connect(self._show_variable_autocomplete_improved)
|
|
|
|
# Estado del panel LaTeX
|
|
self.latex_panel_visible = self.settings.get("latex_panel_visible", True)
|
|
self._latex_equations = []
|
|
|
|
# Configurar helpers
|
|
self._setup_dynamic_helpers()
|
|
|
|
# Configurar UI
|
|
self._setup_ui()
|
|
self._setup_menu()
|
|
self._setup_shortcuts()
|
|
self._setup_interactive_manager()
|
|
|
|
# Cargar historial y configuración
|
|
self._load_history()
|
|
self._restore_geometry()
|
|
|
|
self.logger.info("✅ Calculadora MAV PySide6 (versión completa) inicializada")
|
|
|
|
def _setup_dynamic_helpers(self):
|
|
"""Configura helpers dinámicamente desde el registro de tipos"""
|
|
try:
|
|
self.HELPERS = get_registered_helper_functions()
|
|
self.HELPERS.append(SympyHelper.Helper)
|
|
self.logger.info(f"Helpers dinámicos cargados: {len(self.HELPERS)}")
|
|
except Exception as e:
|
|
self.logger.error(f"Error cargando helpers dinámicos: {e}")
|
|
self.HELPERS = [SympyHelper.Helper]
|
|
|
|
def _setup_ui(self):
|
|
"""Configura la interfaz de usuario completa"""
|
|
self.setWindowTitle("Calculadora MAV - CAS Híbrido")
|
|
self.setGeometry(100, 100, 1000, 700)
|
|
|
|
# Widget central
|
|
central_widget = QWidget()
|
|
self.setCentralWidget(central_widget)
|
|
|
|
# Layout principal horizontal
|
|
main_layout = QHBoxLayout(central_widget)
|
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
main_layout.setSpacing(0)
|
|
|
|
# Splitter principal para entrada y salida
|
|
self.main_splitter = QSplitter(Qt.Horizontal)
|
|
|
|
# Panel de entrada
|
|
self.input_text = InputTextEdit(self)
|
|
self.input_text.setPlaceholderText("Introduce expresiones matemáticas...")
|
|
self.input_text.textChanged.connect(self._on_input_changed)
|
|
|
|
# Panel de salida
|
|
self.output_text = OutputTextEdit()
|
|
self.output_text.link_clicked.connect(self._handle_output_link_click)
|
|
|
|
# Añadir al splitter
|
|
self.main_splitter.addWidget(self.input_text)
|
|
self.main_splitter.addWidget(self.output_text)
|
|
|
|
# Configurar tamaños iniciales
|
|
self.main_splitter.setSizes([450, 450])
|
|
|
|
# Sincronizar scroll
|
|
self._setup_scroll_sync()
|
|
|
|
# Añadir splitter al layout
|
|
main_layout.addWidget(self.main_splitter)
|
|
|
|
# Botón expandible para LaTeX
|
|
self.latex_button = ExpandableLatexButton()
|
|
self.latex_button.clicked.connect(self._toggle_latex_panel)
|
|
main_layout.addWidget(self.latex_button)
|
|
|
|
# Panel LaTeX (inicialmente oculto)
|
|
self.latex_panel = LatexPanel()
|
|
self.latex_panel.setMinimumWidth(300)
|
|
self.latex_panel.setMaximumWidth(500)
|
|
|
|
if self.latex_panel_visible:
|
|
main_layout.addWidget(self.latex_panel)
|
|
self.latex_button.setChecked(True)
|
|
else:
|
|
self.latex_panel.hide()
|
|
|
|
# Configurar tags de salida
|
|
self._setup_output_tags()
|
|
|
|
# Barra de estado
|
|
self.status_bar = QStatusBar()
|
|
self.setStatusBar(self.status_bar)
|
|
self._update_status("🔢 Calculadora MAV - Sistema Algebraico Puro")
|
|
|
|
# Aplicar tema oscuro
|
|
self._apply_dark_theme()
|
|
|
|
def _setup_scroll_sync(self):
|
|
"""Sincroniza el scroll entre entrada y salida"""
|
|
def sync_input_to_output():
|
|
if hasattr(self, '_syncing'):
|
|
return
|
|
self._syncing = True
|
|
self.output_text.verticalScrollBar().setValue(
|
|
self.input_text.verticalScrollBar().value()
|
|
)
|
|
self._syncing = False
|
|
|
|
def sync_output_to_input():
|
|
if hasattr(self, '_syncing'):
|
|
return
|
|
self._syncing = True
|
|
self.input_text.verticalScrollBar().setValue(
|
|
self.output_text.verticalScrollBar().value()
|
|
)
|
|
self._syncing = False
|
|
|
|
self.input_text.verticalScrollBar().valueChanged.connect(sync_input_to_output)
|
|
self.output_text.verticalScrollBar().valueChanged.connect(sync_output_to_input)
|
|
|
|
def _setup_output_tags(self):
|
|
"""Configura los formatos de texto para la salida"""
|
|
self.output_formats = {
|
|
'error': self._create_format("#f44747", bold=True),
|
|
'comment': self._create_format("#6a9955", italic=True),
|
|
'assignment': self._create_format("#dcdcaa"),
|
|
'equation': self._create_format("#c586c0"),
|
|
'symbolic': self._create_format("#9cdcfe"),
|
|
'numeric': self._create_format("#b5cea8"),
|
|
'boolean': self._create_format("#569cd6"),
|
|
'string': self._create_format("#ce9178"),
|
|
'custom_type': self._create_format("#4ec9b0"),
|
|
'plot': self._create_format("#569cd6", underline=True),
|
|
'type_indicator': self._create_format("#808080"),
|
|
'clickable': self._create_format("#4fc3f7", underline=True),
|
|
'helper': self._create_format("#ffd700", italic=True)
|
|
}
|
|
|
|
def _create_format(self, color: str, bold: bool = False, italic: bool = False, underline: bool = False) -> QTextCharFormat:
|
|
"""Crea un formato de texto"""
|
|
fmt = QTextCharFormat()
|
|
fmt.setForeground(QColor(color))
|
|
if bold:
|
|
fmt.setFontWeight(QFont.Bold)
|
|
if italic:
|
|
fmt.setFontItalic(True)
|
|
if underline:
|
|
fmt.setFontUnderline(True)
|
|
return fmt
|
|
|
|
def _setup_menu(self):
|
|
"""Configura el menú completo"""
|
|
menubar = self.menuBar()
|
|
|
|
# Menú Archivo
|
|
file_menu = menubar.addMenu("Archivo")
|
|
|
|
new_action = QAction("Nuevo", self)
|
|
new_action.setShortcut(QKeySequence.New)
|
|
new_action.triggered.connect(self.new_session)
|
|
file_menu.addAction(new_action)
|
|
|
|
file_menu.addSeparator()
|
|
|
|
load_action = QAction("Cargar...", self)
|
|
load_action.setShortcut(QKeySequence.Open)
|
|
load_action.triggered.connect(self.load_file)
|
|
file_menu.addAction(load_action)
|
|
|
|
save_action = QAction("Guardar como...", self)
|
|
save_action.setShortcut(QKeySequence.Save)
|
|
save_action.triggered.connect(self.save_file)
|
|
file_menu.addAction(save_action)
|
|
|
|
file_menu.addSeparator()
|
|
|
|
exit_action = QAction("Salir", self)
|
|
exit_action.triggered.connect(self.close)
|
|
file_menu.addAction(exit_action)
|
|
|
|
# Menú Editar
|
|
edit_menu = menubar.addMenu("Editar")
|
|
|
|
clear_input_action = QAction("Limpiar entrada", self)
|
|
clear_input_action.triggered.connect(self.clear_input)
|
|
edit_menu.addAction(clear_input_action)
|
|
|
|
clear_output_action = QAction("Limpiar salida", self)
|
|
clear_output_action.triggered.connect(self.clear_output)
|
|
edit_menu.addAction(clear_output_action)
|
|
|
|
edit_menu.addSeparator()
|
|
|
|
clear_history_action = QAction("Limpiar historial", self)
|
|
clear_history_action.triggered.connect(self.clear_history)
|
|
edit_menu.addAction(clear_history_action)
|
|
|
|
# Menú Ver
|
|
view_menu = menubar.addMenu("Ver")
|
|
|
|
toggle_latex_action = QAction("📐 Panel LaTeX", self)
|
|
toggle_latex_action.setShortcut(QKeySequence("F12"))
|
|
toggle_latex_action.triggered.connect(self._toggle_latex_panel)
|
|
view_menu.addAction(toggle_latex_action)
|
|
|
|
view_menu.addSeparator()
|
|
|
|
system_info_action = QAction("Información del sistema", self)
|
|
system_info_action.triggered.connect(self.show_types_info)
|
|
view_menu.addAction(system_info_action)
|
|
|
|
# Menú Herramientas
|
|
tools_menu = menubar.addMenu("Herramientas")
|
|
|
|
reload_types_action = QAction("Recargar Tipos Personalizados", self)
|
|
reload_types_action.triggered.connect(self.reload_types)
|
|
tools_menu.addAction(reload_types_action)
|
|
|
|
tools_menu.addSeparator()
|
|
|
|
# Menú de diagnóstico
|
|
diag_menu = tools_menu.addMenu("Diagnóstico")
|
|
|
|
mathjax_diag_action = QAction("🔍 Diagnóstico MathJax", self)
|
|
mathjax_diag_action.triggered.connect(self._diagnose_mathjax)
|
|
diag_menu.addAction(mathjax_diag_action)
|
|
|
|
latex_status_action = QAction("📊 Estado Panel LaTeX", self)
|
|
latex_status_action.triggered.connect(self._show_latex_panel_status)
|
|
diag_menu.addAction(latex_status_action)
|
|
|
|
# Menú Tipos
|
|
types_menu = menubar.addMenu("Tipos")
|
|
|
|
types_info_action = QAction("Información de tipos", self)
|
|
types_info_action.triggered.connect(self.show_types_info)
|
|
types_menu.addAction(types_info_action)
|
|
|
|
types_menu.addSeparator()
|
|
|
|
types_syntax_action = QAction("Sintaxis de tipos", self)
|
|
types_syntax_action.triggered.connect(self.show_types_syntax)
|
|
types_menu.addAction(types_syntax_action)
|
|
|
|
# Menú Ayuda
|
|
help_menu = menubar.addMenu("Ayuda")
|
|
|
|
quick_guide_action = QAction("Guía rápida", self)
|
|
quick_guide_action.triggered.connect(self.show_quick_guide)
|
|
help_menu.addAction(quick_guide_action)
|
|
|
|
syntax_help_action = QAction("Sintaxis", self)
|
|
syntax_help_action.triggered.connect(self.show_syntax_help)
|
|
help_menu.addAction(syntax_help_action)
|
|
|
|
sympy_funcs_action = QAction("Funciones SymPy", self)
|
|
sympy_funcs_action.triggered.connect(self.show_sympy_functions)
|
|
help_menu.addAction(sympy_funcs_action)
|
|
|
|
help_menu.addSeparator()
|
|
|
|
about_action = QAction("Acerca de", self)
|
|
about_action.triggered.connect(self.show_about)
|
|
help_menu.addAction(about_action)
|
|
|
|
def _setup_shortcuts(self):
|
|
"""Configura atajos de teclado"""
|
|
# Evaluación manual
|
|
eval_shortcut = QShortcut(QKeySequence("Ctrl+Return"), self)
|
|
eval_shortcut.activated.connect(self._evaluate_and_update)
|
|
|
|
# Toggle LaTeX panel
|
|
latex_shortcut = QShortcut(QKeySequence("F12"), self)
|
|
latex_shortcut.activated.connect(self._toggle_latex_panel)
|
|
|
|
# Nuevo
|
|
new_shortcut = QShortcut(QKeySequence.New, self)
|
|
new_shortcut.activated.connect(self.new_session)
|
|
|
|
def _setup_interactive_manager(self):
|
|
"""Configura el gestor de resultados interactivos"""
|
|
self.interactive_manager = InteractiveResultManager(self)
|
|
self.interactive_manager.set_update_callback(self._update_input_expression)
|
|
|
|
def _apply_dark_theme(self):
|
|
"""Aplica tema oscuro a la aplicación"""
|
|
dark_theme = """
|
|
QMainWindow {
|
|
background-color: #2b2b2b;
|
|
color: #d4d4d4;
|
|
}
|
|
QPlainTextEdit, QTextEdit {
|
|
background-color: #1e1e1e;
|
|
color: #d4d4d4;
|
|
border: 1px solid #3c3c3c;
|
|
selection-background-color: #264f78;
|
|
}
|
|
QMenuBar {
|
|
background-color: #2d2d30;
|
|
color: #d4d4d4;
|
|
border-bottom: 1px solid #3c3c3c;
|
|
}
|
|
QMenuBar::item:selected {
|
|
background-color: #094771;
|
|
}
|
|
QMenu {
|
|
background-color: #2d2d30;
|
|
color: #d4d4d4;
|
|
border: 1px solid #3c3c3c;
|
|
}
|
|
QMenu::item:selected {
|
|
background-color: #094771;
|
|
}
|
|
QStatusBar {
|
|
background-color: #2b2b2b;
|
|
color: #80c7f7;
|
|
border-top: 1px solid #3c3c3c;
|
|
}
|
|
QSplitter::handle {
|
|
background-color: #3c3c3c;
|
|
width: 4px;
|
|
}
|
|
QSplitter::handle:hover {
|
|
background-color: #007acc;
|
|
}
|
|
QScrollBar:vertical {
|
|
background-color: #1e1e1e;
|
|
width: 12px;
|
|
border: none;
|
|
}
|
|
QScrollBar::handle:vertical {
|
|
background-color: #3c3c3c;
|
|
min-height: 20px;
|
|
border-radius: 6px;
|
|
}
|
|
QScrollBar::handle:vertical:hover {
|
|
background-color: #4c4c4c;
|
|
}
|
|
"""
|
|
self.setStyleSheet(dark_theme)
|
|
|
|
# ========== SISTEMA DE EVALUACIÓN ==========
|
|
|
|
def _on_input_changed(self):
|
|
"""Maneja cambios en la entrada con debounce"""
|
|
self._debounce_timer.stop()
|
|
self._debounce_timer.start(300)
|
|
|
|
# Cancelar popup de variables si existe
|
|
self._variable_popup_timer.stop()
|
|
|
|
# Programar autocompletado de variables
|
|
if not self._autocomplete_active and not self._popup_disabled_until_next_dot:
|
|
self._variable_popup_timer.start(800)
|
|
|
|
def _evaluate_and_update(self):
|
|
"""Evalúa todas las líneas y actualiza la salida"""
|
|
try:
|
|
input_content = self.input_text.toPlainText()
|
|
if not input_content.strip():
|
|
self._clear_output()
|
|
return
|
|
|
|
# Limpiar contexto del motor
|
|
self.engine.equations.clear()
|
|
self.engine.symbol_table.clear()
|
|
self.engine.variables.clear()
|
|
self.logger.debug("Contexto del motor limpiado")
|
|
|
|
# Limpiar panel LaTeX
|
|
if hasattr(self, '_latex_equations'):
|
|
self._latex_equations.clear()
|
|
self.latex_panel.clear_equations()
|
|
|
|
lines = input_content.splitlines()
|
|
self._evaluate_lines(lines)
|
|
|
|
except Exception as e:
|
|
self._show_error(f"Error durante evaluación: {e}")
|
|
|
|
def _evaluate_lines(self, lines: List[str]):
|
|
"""Evalúa múltiples líneas de código"""
|
|
output_data = []
|
|
|
|
for line_num, line in enumerate(lines, 1):
|
|
line_stripped = line.strip()
|
|
|
|
# Líneas vacías o comentarios
|
|
if not line_stripped or line_stripped.startswith('#'):
|
|
if line_stripped:
|
|
output_data.append([("comment", line_stripped)])
|
|
# Añadir comentario al panel LaTeX
|
|
if line_stripped.startswith('#'):
|
|
comment_text = line_stripped[1:].strip()
|
|
self._add_to_latex_panel("comment", comment_text)
|
|
else:
|
|
output_data.append([("", "")])
|
|
continue
|
|
|
|
# Evaluar línea
|
|
result = self.engine.evaluate_line(line_stripped)
|
|
line_output = self._process_evaluation_result(result)
|
|
output_data.append(line_output)
|
|
|
|
self._display_output(output_data)
|
|
|
|
def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]:
|
|
"""Procesa el resultado de evaluación para display"""
|
|
output_parts = []
|
|
indicator_text: Optional[str] = None
|
|
|
|
# Añadir al panel LaTeX si es aplicable
|
|
self._add_to_latex_panel_if_applicable(result)
|
|
|
|
if result.result_type == "comment":
|
|
output_parts.append(("comment", result.output if result.output is not None else ""))
|
|
return output_parts
|
|
|
|
if not result.success:
|
|
# Manejo de errores con ayuda contextual
|
|
error_msg = f"Error: {result.error_message}"
|
|
|
|
# Intentar obtener ayuda
|
|
ayuda_text = self._obtener_ayuda(result.input_line)
|
|
if ayuda_text:
|
|
ayuda_linea = ayuda_text.replace("\n", " ").strip()
|
|
if len(ayuda_linea) > 120:
|
|
ayuda_linea = ayuda_linea[:117] + "..."
|
|
|
|
output_parts.append(("error", error_msg))
|
|
output_parts.append(("\n", "\n"))
|
|
output_parts.append(("helper", f"Sugerencia: {ayuda_linea}"))
|
|
else:
|
|
output_parts.append(("error", error_msg))
|
|
|
|
else:
|
|
# Intentar crear tag interactivo
|
|
if self.interactive_manager:
|
|
interactive_info = self.interactive_manager.create_interactive_link(
|
|
result.actual_result_object
|
|
)
|
|
|
|
if interactive_info:
|
|
link_id, display_text, result_object = interactive_info
|
|
output_parts.append(("clickable", display_text, link_id, result_object))
|
|
|
|
# Añadir indicador de tipo algebraico
|
|
if result.algebraic_type:
|
|
indicator_text = f"[{result.algebraic_type}]"
|
|
output_parts.append((" ", " "))
|
|
output_parts.append(("type_indicator", indicator_text))
|
|
return output_parts
|
|
|
|
# Si no es interactivo, usar formato normal
|
|
main_output_tag = "base"
|
|
|
|
if result.is_assignment:
|
|
main_output_tag = "assignment"
|
|
indicator_text = "[=]"
|
|
elif result.is_equation:
|
|
main_output_tag = "equation"
|
|
indicator_text = "[eq]"
|
|
elif result.result_type == "plot":
|
|
main_output_tag = "plot"
|
|
else:
|
|
# Determinar tag según tipo algebraico
|
|
if result.algebraic_type:
|
|
type_lower = result.algebraic_type.lower()
|
|
if isinstance(result.actual_result_object, sympy.Basic):
|
|
main_output_tag = "symbolic"
|
|
elif type_lower in ["int", "float", "complex"]:
|
|
main_output_tag = "numeric"
|
|
elif type_lower == "bool":
|
|
main_output_tag = "boolean"
|
|
elif type_lower == "str":
|
|
main_output_tag = "string"
|
|
else:
|
|
main_output_tag = "custom_type"
|
|
|
|
if result.algebraic_type:
|
|
is_collection = any(kw in result.algebraic_type.lower()
|
|
for kw in ["matrix", "list", "dict", "tuple", "vector", "array"])
|
|
if is_collection or isinstance(result.actual_result_object, sympy.Basic):
|
|
indicator_text = f"[{result.algebraic_type}]"
|
|
|
|
output_parts.append((main_output_tag, result.output if result.output is not None else ""))
|
|
|
|
if indicator_text:
|
|
output_parts.append((" ", " "))
|
|
output_parts.append(("type_indicator", indicator_text))
|
|
|
|
return output_parts
|
|
|
|
def _add_to_latex_panel_if_applicable(self, result: EvaluationResult):
|
|
"""Agrega resultado al panel LaTeX si es aplicable"""
|
|
try:
|
|
should_add_to_latex = False
|
|
equation_type = "comment"
|
|
|
|
if result.result_type == "comment":
|
|
should_add_to_latex = True
|
|
equation_type = "comment"
|
|
elif result.is_assignment:
|
|
should_add_to_latex = True
|
|
equation_type = "assignment"
|
|
elif result.is_equation:
|
|
should_add_to_latex = True
|
|
equation_type = "equation"
|
|
elif result.success and result.output:
|
|
# Agregar si tiene contenido matemático
|
|
math_indicators = ['=', '+', '-', '*', '/', '^', 'sqrt', 'sin', 'cos', 'tan', 'log', 'exp']
|
|
if any(indicator in result.output for indicator in math_indicators):
|
|
should_add_to_latex = True
|
|
equation_type = "symbolic"
|
|
elif result.actual_result_object is not None and isinstance(result.actual_result_object, sympy.Basic):
|
|
should_add_to_latex = True
|
|
equation_type = "symbolic"
|
|
|
|
if should_add_to_latex:
|
|
latex_content = ""
|
|
|
|
if result.actual_result_object is not None:
|
|
try:
|
|
latex_content = sympy.latex(result.actual_result_object)
|
|
except:
|
|
latex_content = result.output if result.output else str(result.actual_result_object)
|
|
else:
|
|
latex_content = result.output if result.output else ""
|
|
|
|
self._add_to_latex_panel(equation_type, latex_content)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error procesando para panel LaTeX: {e}")
|
|
|
|
def _add_to_latex_panel(self, equation_type: str, latex_content: str):
|
|
"""Añade una ecuación al panel LaTeX"""
|
|
if not hasattr(self, '_latex_equations'):
|
|
self._latex_equations = []
|
|
|
|
self._latex_equations.append({
|
|
'type': equation_type,
|
|
'content': latex_content
|
|
})
|
|
|
|
self.latex_panel.add_equation(equation_type, latex_content)
|
|
|
|
# Actualizar indicador visual
|
|
self._update_latex_indicator()
|
|
|
|
def _update_latex_indicator(self):
|
|
"""Actualiza el indicador visual de contenido LaTeX"""
|
|
equation_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0
|
|
|
|
if equation_count > 0:
|
|
self.latex_button.setToolTip(f"📐 Panel LaTeX ({equation_count} ecuaciones)")
|
|
else:
|
|
self.latex_button.setToolTip("📐 Panel LaTeX (sin ecuaciones)")
|
|
|
|
def _display_output(self, output_data: List[List[tuple]]):
|
|
"""Muestra los datos de salida en el widget"""
|
|
self.output_text.clear()
|
|
self.output_text.clickable_links.clear()
|
|
|
|
cursor = self.output_text.textCursor()
|
|
|
|
for line_idx, line_parts in enumerate(output_data):
|
|
if line_idx > 0:
|
|
cursor.insertText("\n")
|
|
|
|
if not line_parts or (len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == ""):
|
|
continue
|
|
|
|
for part_idx, part_data in enumerate(line_parts):
|
|
if len(part_data) >= 4 and part_data[0] == "clickable":
|
|
# Link clickeable
|
|
_, display_text, link_id, result_object = part_data
|
|
start_pos = cursor.position()
|
|
cursor.insertText(display_text, self.output_formats.get('clickable'))
|
|
end_pos = cursor.position()
|
|
self.output_text.clickable_links[(start_pos, end_pos)] = (link_id, result_object)
|
|
|
|
elif len(part_data) >= 2:
|
|
tag, content = part_data[0], part_data[1]
|
|
if not content:
|
|
continue
|
|
|
|
if part_idx > 0:
|
|
prev_tag = line_parts[part_idx-1][0] if part_idx > 0 else None
|
|
if tag not in ["type_indicator"] and prev_tag:
|
|
cursor.insertText(" ; ")
|
|
elif tag == "type_indicator" and prev_tag:
|
|
cursor.insertText(" ")
|
|
|
|
format_obj = self.output_formats.get(tag, None)
|
|
if format_obj:
|
|
cursor.insertText(str(content), format_obj)
|
|
else:
|
|
cursor.insertText(str(content))
|
|
|
|
def _clear_output(self):
|
|
"""Limpia el panel de salida"""
|
|
self.output_text.clear()
|
|
|
|
def _show_error(self, error_msg: str):
|
|
"""Muestra un error en el panel de salida"""
|
|
self.output_text.clear()
|
|
cursor = self.output_text.textCursor()
|
|
cursor.insertText(error_msg, self.output_formats['error'])
|
|
|
|
def _handle_output_link_click(self, link_id: str, result_object):
|
|
"""Maneja clicks en links del panel de salida"""
|
|
if self.interactive_manager:
|
|
self.interactive_manager.handle_interactive_click(result_object)
|
|
|
|
def _update_input_expression(self, original_expression: str, new_expression: str):
|
|
"""Actualiza el panel de entrada reemplazando la expresión original"""
|
|
try:
|
|
current_content = self.input_text.toPlainText()
|
|
|
|
if original_expression in current_content:
|
|
updated_content = current_content.replace(original_expression, new_expression, 1)
|
|
self.input_text.setPlainText(updated_content)
|
|
self._evaluate_and_update()
|
|
self.logger.info(f"Expresión actualizada: '{original_expression}' -> '{new_expression}'")
|
|
else:
|
|
# Si no se encuentra, agregar al final
|
|
if current_content and not current_content.endswith('\n'):
|
|
current_content += '\n'
|
|
updated_content = current_content + new_expression
|
|
self.input_text.setPlainText(updated_content)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error actualizando expresión: {e}")
|
|
|
|
# ========== SISTEMA DE AUTOCOMPLETADO COMPLETO ==========
|
|
|
|
def _handle_key_press(self, event) -> bool:
|
|
"""Maneja eventos de teclado para autocompletado - retorna True si manejó el evento"""
|
|
# Navegación en popup
|
|
if self._autocomplete_active or self._variable_popup_active:
|
|
if event.key() == Qt.Key_Up:
|
|
self._handle_arrow_key(-1)
|
|
return True
|
|
elif event.key() == Qt.Key_Down:
|
|
self._handle_arrow_key(1)
|
|
return True
|
|
elif event.key() == Qt.Key_Tab:
|
|
self._handle_tab_key()
|
|
return True
|
|
elif event.key() == Qt.Key_Escape:
|
|
self._handle_escape_key()
|
|
return True
|
|
elif event.key() in [Qt.Key_Return, Qt.Key_Enter]:
|
|
self._select_autocomplete()
|
|
return True
|
|
|
|
# Detectar backspace para cerrar popup si se borra el punto
|
|
if event.key() == Qt.Key_Backspace and self._autocomplete_active:
|
|
QTimer.singleShot(1, self._check_dot_removal)
|
|
|
|
# Procesar autocompletado después de insertar carácter
|
|
if event.text() and not event.modifiers() & Qt.ControlModifier:
|
|
# Guardar datos del evento para evitar que se elimine el objeto
|
|
event_text = event.text()
|
|
event_key = event.key()
|
|
QTimer.singleShot(10, lambda: self._on_key_release_deferred(event_text, event_key))
|
|
|
|
return False
|
|
|
|
def _on_key_release_deferred(self, event_text: str, event_key: int):
|
|
"""Maneja eventos después de insertar carácter usando datos guardados"""
|
|
# Cancelar timer de variables
|
|
self._variable_popup_timer.stop()
|
|
|
|
# Verificar si acabamos de navegar
|
|
just_navigated = (time.time() - self._last_navigation_time) < 0.1
|
|
|
|
# Manejar autocompletado con punto
|
|
if event_text == '.' and not self._popup_disabled_until_next_dot:
|
|
if self._variable_popup_active:
|
|
self._close_autocomplete_popup()
|
|
self._handle_dot_autocomplete()
|
|
|
|
# Filtrar autocompletado si está activo
|
|
elif self._autocomplete_active and event_text and not just_navigated:
|
|
self._filter_autocomplete()
|
|
|
|
# Marcar tiempo del último cambio
|
|
if event_text:
|
|
self._last_input_change = time.time()
|
|
|
|
# Programar autocompletado de variables
|
|
if not self._autocomplete_active and not self._popup_disabled_until_next_dot:
|
|
self._schedule_variable_autocomplete_improved()
|
|
|
|
def _on_key_release(self, event):
|
|
"""Maneja eventos después de insertar carácter - mantenido para compatibilidad"""
|
|
return self._on_key_release_deferred(event.text(), event.key())
|
|
|
|
def _handle_dot_autocomplete(self):
|
|
"""Maneja el autocompletado cuando se escribe un punto"""
|
|
self._close_autocomplete_popup()
|
|
|
|
cursor = self.input_text.textCursor()
|
|
cursor_pos = cursor.position()
|
|
|
|
# Obtener línea actual
|
|
cursor.select(QTextCursor.LineUnderCursor)
|
|
line_text = cursor.selectedText()
|
|
|
|
# Calcular posición del punto en la línea
|
|
line_start = cursor.selectionStart()
|
|
dot_pos_in_line = cursor_pos - line_start - 1
|
|
|
|
if dot_pos_in_line < 0:
|
|
return
|
|
|
|
# Guardar posición del trigger
|
|
self._autocomplete_trigger_pos = cursor_pos
|
|
self._autocomplete_filter_text = ""
|
|
|
|
# Texto antes del punto
|
|
text_before_dot = line_text[:dot_pos_in_line].strip()
|
|
|
|
# Determinar si es popup GLOBAL
|
|
if not text_before_dot:
|
|
self.logger.debug("Dot en línea vacía. Ofreciendo sugerencias globales.")
|
|
suggestions = self._get_global_suggestions()
|
|
if suggestions:
|
|
self._show_autocomplete_popup(suggestions, is_global_popup=True)
|
|
return
|
|
|
|
# Es popup de OBJETO
|
|
obj_expr_str = self._extract_object_expression(text_before_dot)
|
|
if not obj_expr_str:
|
|
return
|
|
|
|
self.logger.debug(f"Autocompletado para objeto: '{obj_expr_str}'")
|
|
|
|
# Caso especial para sympy
|
|
if obj_expr_str == "sympy":
|
|
methods = SympyHelper.PopupFunctionList()
|
|
if methods:
|
|
self._show_autocomplete_popup(methods, is_global_popup=False)
|
|
return
|
|
|
|
# Preprocesar con bracket parser si es necesario
|
|
if '[' in obj_expr_str:
|
|
obj_expr_str = self.engine.parser._transform_brackets(obj_expr_str)
|
|
|
|
# Evaluar la expresión del objeto
|
|
eval_context = self.engine._get_full_context()
|
|
try:
|
|
obj = eval(obj_expr_str, eval_context)
|
|
self.logger.debug(f"Objeto evaluado: {type(obj)}")
|
|
|
|
# Mostrar métodos del objeto
|
|
if hasattr(obj, 'PopupFunctionList'):
|
|
methods = obj.PopupFunctionList()
|
|
if methods:
|
|
self._show_autocomplete_popup(methods, is_global_popup=False)
|
|
|
|
except Exception as e:
|
|
self.logger.debug(f"Error evaluando objeto '{obj_expr_str}': {e}")
|
|
|
|
def _extract_object_expression(self, text: str) -> str:
|
|
"""Extrae la expresión del objeto del texto antes del punto"""
|
|
obj_expr_regex = r"([a-zA-Z_][a-zA-Z0-9_]*(?:\[[^\]]*\])?(?:(?:\s*\.\s*[a-zA-Z_][a-zA-Z0-9_]*)(?:\[[^\]]*\])?)*)$"
|
|
match = re.search(obj_expr_regex, text)
|
|
|
|
if match:
|
|
return match.group(1).replace(" ", "")
|
|
|
|
# Fallback
|
|
if re.match(r"^[a-zA-Z_0-9\(\)\[\]\.\"\'\+\-\*\/ ]*$", text) and \
|
|
not text.endswith(("+", "-", "*", "/", "(", ",")):
|
|
return text
|
|
|
|
return ""
|
|
|
|
def _get_global_suggestions(self) -> List[Tuple[str, str]]:
|
|
"""Obtiene sugerencias globales para autocompletado"""
|
|
suggestions = []
|
|
|
|
try:
|
|
# Obtener contexto dinámico
|
|
dynamic_context = get_registered_base_context()
|
|
|
|
for name, class_or_func in dynamic_context.items():
|
|
if name[0].isupper(): # Prioritizar capitalizados
|
|
hint = f"Tipo o función: {name}"
|
|
if hasattr(class_or_func, '__doc__') and class_or_func.__doc__:
|
|
first_line = class_or_func.__doc__.strip().split('\n')[0]
|
|
hint = f"{name} - {first_line}"
|
|
suggestions.append((name, hint))
|
|
|
|
# Añadir funciones SymPy
|
|
sympy_functions = SympyHelper.PopupFunctionList()
|
|
if sympy_functions:
|
|
current_names = {s[0] for s in suggestions}
|
|
for fname, fhint in sympy_functions:
|
|
if fname not in current_names:
|
|
suggestions.append((fname, fhint))
|
|
|
|
except Exception as e:
|
|
self.logger.debug(f"Error obteniendo sugerencias globales: {e}")
|
|
|
|
suggestions.sort(key=lambda x: x[0])
|
|
return suggestions
|
|
|
|
def _schedule_variable_autocomplete_improved(self):
|
|
"""Programa el autocompletado de variables"""
|
|
if self._autocomplete_active or self._popup_disabled_until_next_dot:
|
|
return
|
|
|
|
# Verificar que estemos escribiendo
|
|
cursor = self.input_text.textCursor()
|
|
cursor.select(QTextCursor.LineUnderCursor)
|
|
current_line = cursor.selectedText().strip()
|
|
|
|
if not current_line or current_line.endswith('.'):
|
|
return
|
|
|
|
# Programar para 800ms después
|
|
self._variable_popup_timer.start(800)
|
|
|
|
def _show_variable_autocomplete_improved(self):
|
|
"""Muestra autocompletado de variables disponibles"""
|
|
if self._autocomplete_active or self._variable_popup_active:
|
|
return
|
|
|
|
# Verificar línea actual
|
|
cursor = self.input_text.textCursor()
|
|
cursor.select(QTextCursor.LineUnderCursor)
|
|
current_line = cursor.selectedText().strip()
|
|
|
|
if not current_line or current_line.endswith('.'):
|
|
return
|
|
|
|
# Obtener variables del contexto
|
|
try:
|
|
context = self.engine._get_full_context()
|
|
variables = []
|
|
|
|
# Filtrar variables
|
|
for name, value in context.items():
|
|
if (not name.startswith('_') and
|
|
not callable(value) and
|
|
name not in ['sympy', 'math', 'numpy', 'plt', 'builtins']):
|
|
|
|
# Descripción del valor
|
|
value_str = str(value)
|
|
if len(value_str) > 20:
|
|
value_str = value_str[:17] + "..."
|
|
|
|
variables.append((name, value_str))
|
|
|
|
if variables:
|
|
variables.sort(key=lambda x: x[0])
|
|
|
|
# Filtrar por palabra actual
|
|
words = current_line.split()
|
|
if words:
|
|
last_word = words[-1]
|
|
filtered_vars = [
|
|
(name, value) for name, value in variables
|
|
if name.lower().startswith(last_word.lower()) and name != last_word
|
|
]
|
|
|
|
if filtered_vars:
|
|
self._show_variable_popup(filtered_vars)
|
|
|
|
except Exception as e:
|
|
self.logger.debug(f"Error obteniendo variables: {e}")
|
|
|
|
def _show_autocomplete_popup(self, suggestions: List[Tuple[str, str]], is_global_popup: bool = False):
|
|
"""Muestra popup de autocompletado"""
|
|
if not suggestions:
|
|
return
|
|
|
|
self._close_autocomplete_popup()
|
|
|
|
# Guardar estado
|
|
self._current_suggestions = suggestions.copy()
|
|
self._is_global_popup = is_global_popup
|
|
self._autocomplete_active = True
|
|
|
|
# Crear popup
|
|
self._autocomplete_popup = AutocompletePopup(self)
|
|
self._autocomplete_popup.set_suggestions(suggestions)
|
|
self._autocomplete_popup.item_selected.connect(self._on_autocomplete_selected)
|
|
|
|
# Posicionar
|
|
cursor_rect = self.input_text.cursorRect()
|
|
global_pos = self.input_text.mapToGlobal(cursor_rect.bottomLeft())
|
|
self._autocomplete_popup.move(global_pos)
|
|
self._autocomplete_popup.adjust_size()
|
|
self._autocomplete_popup.show()
|
|
|
|
def _show_variable_popup(self, variables: List[Tuple[str, str]]):
|
|
"""Muestra popup de variables"""
|
|
self._variable_popup_active = True
|
|
self._autocomplete_active = False
|
|
|
|
# Convertir formato para el popup
|
|
suggestions = [(name, f"= {value}") for name, value in variables]
|
|
self._show_autocomplete_popup(suggestions, is_global_popup=False)
|
|
|
|
def _filter_autocomplete(self):
|
|
"""Filtra las sugerencias del autocompletado"""
|
|
if not self._autocomplete_active or not self._autocomplete_trigger_pos:
|
|
return
|
|
|
|
# Obtener texto escrito después del punto
|
|
cursor = self.input_text.textCursor()
|
|
current_pos = cursor.position()
|
|
|
|
if current_pos > self._autocomplete_trigger_pos:
|
|
cursor.setPosition(self._autocomplete_trigger_pos)
|
|
cursor.setPosition(current_pos, QTextCursor.KeepAnchor)
|
|
filter_text = cursor.selectedText().lower()
|
|
self._autocomplete_filter_text = filter_text
|
|
else:
|
|
self._autocomplete_filter_text = ""
|
|
|
|
# Filtrar sugerencias
|
|
filtered = []
|
|
for name, hint in self._current_suggestions:
|
|
if name.lower().startswith(self._autocomplete_filter_text):
|
|
filtered.append((name, hint))
|
|
|
|
if filtered and self._autocomplete_popup:
|
|
self._autocomplete_popup.set_suggestions(filtered)
|
|
self._autocomplete_popup.adjust_size()
|
|
else:
|
|
self._close_autocomplete_popup()
|
|
|
|
def _handle_arrow_key(self, direction: int):
|
|
"""Maneja navegación con flechas en el popup"""
|
|
if self._autocomplete_popup:
|
|
self._autocomplete_popup.navigate(direction)
|
|
self._last_navigation_time = time.time()
|
|
|
|
def _handle_tab_key(self):
|
|
"""Maneja tecla TAB para seleccionar"""
|
|
self._select_autocomplete()
|
|
|
|
def _handle_escape_key(self):
|
|
"""Maneja tecla ESC para cerrar popup"""
|
|
self._close_autocomplete_popup()
|
|
if self._autocomplete_active:
|
|
self._popup_disabled_until_next_dot = True
|
|
|
|
def _select_autocomplete(self):
|
|
"""Selecciona el item actual del autocompletado"""
|
|
if not self._autocomplete_popup:
|
|
return
|
|
|
|
selected_text = self._autocomplete_popup.get_selected_text()
|
|
if selected_text:
|
|
self._insert_autocomplete_text(selected_text)
|
|
|
|
self._close_autocomplete_popup()
|
|
|
|
def _on_autocomplete_selected(self, text: str):
|
|
"""Callback cuando se selecciona un item del popup"""
|
|
self._insert_autocomplete_text(text)
|
|
self._close_autocomplete_popup()
|
|
|
|
def _insert_autocomplete_text(self, text: str):
|
|
"""Inserta el texto seleccionado del autocompletado"""
|
|
cursor = self.input_text.textCursor()
|
|
|
|
# Para popup de variables
|
|
if self._variable_popup_active:
|
|
# Reemplazar palabra actual
|
|
cursor.select(QTextCursor.WordUnderCursor)
|
|
cursor.insertText(text)
|
|
return
|
|
|
|
# Para popup global (después de punto solo)
|
|
if self._is_global_popup:
|
|
# Eliminar el punto y añadir función
|
|
cursor.setPosition(self._autocomplete_trigger_pos - 1)
|
|
cursor.setPosition(self._autocomplete_trigger_pos, QTextCursor.KeepAnchor)
|
|
cursor.insertText(text + "()")
|
|
# Posicionar cursor dentro de paréntesis
|
|
cursor.setPosition(self._autocomplete_trigger_pos - 1 + len(text) + 1)
|
|
self.input_text.setTextCursor(cursor)
|
|
else:
|
|
# Para métodos de objeto
|
|
if self._autocomplete_filter_text:
|
|
# Eliminar texto filtrado
|
|
start_pos = cursor.position() - len(self._autocomplete_filter_text)
|
|
cursor.setPosition(start_pos)
|
|
cursor.setPosition(start_pos + len(self._autocomplete_filter_text), QTextCursor.KeepAnchor)
|
|
cursor.removeSelectedText()
|
|
|
|
# Insertar método
|
|
cursor.insertText(text + "()")
|
|
cursor.setPosition(cursor.position() - 1)
|
|
self.input_text.setTextCursor(cursor)
|
|
|
|
def _check_dot_removal(self):
|
|
"""Verifica si se borró el punto que activó el autocompletado"""
|
|
cursor = self.input_text.textCursor()
|
|
if cursor.position() > 0:
|
|
cursor.setPosition(cursor.position() - 1)
|
|
cursor.setPosition(cursor.position() + 1, QTextCursor.KeepAnchor)
|
|
if cursor.selectedText() == '.':
|
|
self._close_autocomplete_popup()
|
|
|
|
def _close_autocomplete_popup(self):
|
|
"""Cierra el popup de autocompletado"""
|
|
if self._autocomplete_popup:
|
|
self._autocomplete_popup.close()
|
|
self._autocomplete_popup = None
|
|
|
|
# Resetear estado
|
|
self._autocomplete_active = False
|
|
self._variable_popup_active = False
|
|
self._autocomplete_trigger_pos = None
|
|
self._autocomplete_filter_text = ""
|
|
self._current_suggestions = []
|
|
|
|
# ========== MANEJO DE PANEL LATEX ==========
|
|
|
|
def _toggle_latex_panel(self):
|
|
"""Muestra/oculta el panel LaTeX"""
|
|
self.latex_panel_visible = not self.latex_panel_visible
|
|
|
|
if self.latex_panel_visible:
|
|
self.latex_panel.show()
|
|
self.latex_button.setChecked(True)
|
|
# Re-renderizar ecuaciones si hay
|
|
if hasattr(self, '_latex_equations') and self._latex_equations:
|
|
for eq in self._latex_equations:
|
|
self.latex_panel.add_equation(eq['type'], eq['content'])
|
|
else:
|
|
self.latex_panel.hide()
|
|
self.latex_button.setChecked(False)
|
|
|
|
# Guardar estado
|
|
self.settings["latex_panel_visible"] = self.latex_panel_visible
|
|
|
|
# ========== FUNCIONES DE MENÚ ==========
|
|
|
|
def new_session(self):
|
|
"""Inicia una nueva sesión"""
|
|
self.clear_input()
|
|
self.clear_output()
|
|
self.latex_panel.clear_equations()
|
|
if hasattr(self, '_latex_equations'):
|
|
self._latex_equations.clear()
|
|
self._update_status("✨ Nueva sesión iniciada")
|
|
|
|
def load_file(self):
|
|
"""Carga archivo en el editor"""
|
|
filepath, _ = QFileDialog.getOpenFileName(
|
|
self, "Cargar archivo", "",
|
|
"Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)"
|
|
)
|
|
|
|
if filepath:
|
|
try:
|
|
with open(filepath, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
|
|
self.input_text.setPlainText(content)
|
|
self._evaluate_and_update()
|
|
self._update_status(f"📁 Archivo cargado: {Path(filepath).name}")
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"No se pudo cargar el archivo:\n{e}")
|
|
|
|
def save_file(self):
|
|
"""Guarda contenido del editor"""
|
|
filepath, _ = QFileDialog.getSaveFileName(
|
|
self, "Guardar archivo", "",
|
|
"Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)"
|
|
)
|
|
|
|
if filepath:
|
|
try:
|
|
content = self.input_text.toPlainText()
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
f.write(content)
|
|
|
|
self._update_status(f"💾 Archivo guardado: {Path(filepath).name}")
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"No se pudo guardar el archivo:\n{e}")
|
|
|
|
def clear_input(self):
|
|
"""Limpia panel de entrada"""
|
|
self.input_text.clear()
|
|
self._clear_output()
|
|
|
|
def clear_output(self):
|
|
"""Limpia panel de salida y LaTeX"""
|
|
self._clear_output()
|
|
self.latex_panel.clear_equations()
|
|
if hasattr(self, '_latex_equations'):
|
|
self._latex_equations.clear()
|
|
|
|
def clear_history(self):
|
|
"""Limpia el archivo de historial"""
|
|
try:
|
|
if os.path.exists(self.HISTORY_FILE):
|
|
os.remove(self.HISTORY_FILE)
|
|
self._update_status("✓ Historial limpiado")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"No se pudo limpiar el historial:\n{e}")
|
|
|
|
def reload_types(self):
|
|
"""Recarga el sistema de tipos"""
|
|
try:
|
|
self.logger.info("Recargando sistema de tipos...")
|
|
self._setup_dynamic_helpers()
|
|
self._evaluate_and_update()
|
|
self._update_status("✓ Sistema de tipos recargado")
|
|
except Exception as e:
|
|
self.logger.error(f"Error recargando tipos: {e}")
|
|
QMessageBox.critical(self, "Error", f"Error recargando tipos:\n{e}")
|
|
|
|
def show_types_info(self):
|
|
"""Muestra información sobre tipos disponibles"""
|
|
try:
|
|
context_info = self.engine.get_context_info()
|
|
|
|
info_text = f"""INFORMACIÓN DEL SISTEMA ALGEBRAICO PURO
|
|
|
|
Ecuaciones en el sistema: {context_info.get('equations', 0)}
|
|
Variables definidas: {context_info.get('variables', 0)}
|
|
Variables activas: {', '.join(context_info.get('variable_names', []))}
|
|
|
|
CARACTERÍSTICAS:
|
|
• Sistema de ecuaciones puras con SymPy
|
|
• Todas las asignaciones son ecuaciones
|
|
• Resolución automática de sistemas
|
|
• Evaluación numérica inteligente
|
|
• Atajo x=? equivale a solve(x)
|
|
"""
|
|
|
|
self._show_info_dialog("Información del Sistema", info_text)
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Error obteniendo información:\n{e}")
|
|
|
|
def show_types_syntax(self):
|
|
"""Muestra sintaxis de tipos disponibles"""
|
|
try:
|
|
types_info = self.engine.get_available_types()
|
|
syntax_text = "SINTAXIS DE TIPOS DISPONIBLES\n\n"
|
|
|
|
# Aquí iría el código para mostrar sintaxis
|
|
# Similar al original pero adaptado para PySide6
|
|
|
|
self._show_info_dialog("Sintaxis de Tipos", syntax_text)
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Error obteniendo sintaxis:\n{e}")
|
|
|
|
def show_quick_guide(self):
|
|
"""Muestra guía rápida"""
|
|
guide = """# Calculadora MAV - CAS Híbrido
|
|
|
|
## Sistema de Tipos Dinámico
|
|
El sistema detecta automáticamente tipos disponibles en custom_types/
|
|
|
|
## Sintaxis Nueva con Corchetes
|
|
- Sintaxis: Tipo[valor] en lugar de Tipo("valor")
|
|
- Ejemplos: Hex[FF], Bin[1010], Dec[10.5], Chr[A]
|
|
- Use menú Tipos → Información de tipos para ver tipos disponibles
|
|
|
|
## Ecuaciones Automáticas
|
|
- x**2 + 2*x = 8 (detectado automáticamente)
|
|
- a + b = 10 (agregado al sistema)
|
|
- variable=? (atajo para solve(variable))
|
|
|
|
## Funciones SymPy Disponibles
|
|
- solve(), diff(), integrate(), limit(), series()
|
|
- sin(), cos(), tan(), exp(), log(), sqrt()
|
|
- Matrix(), plot(), plot3d()
|
|
|
|
## Resultados Interactivos
|
|
- 📊 Ver Plot (click para ventana matplotlib)
|
|
- 📋 Ver Matriz (click para vista expandida)
|
|
- 📋 Ver Lista (click para contenido completo)
|
|
|
|
## Variables Automáticas
|
|
- Todas las variables son símbolos SymPy
|
|
- x = 5 crea Symbol('x') con valor 5
|
|
- Evaluación simbólica + numérica automática
|
|
|
|
## Autocompletado Dinámico
|
|
- Escriba "." después de cualquier objeto para ver métodos
|
|
- El sistema usa los tipos registrados automáticamente
|
|
"""
|
|
|
|
self._show_info_dialog("Guía Rápida", guide)
|
|
|
|
def show_syntax_help(self):
|
|
"""Muestra ayuda de sintaxis"""
|
|
syntax = """# Sintaxis del CAS Híbrido
|
|
|
|
## Sistema de Tipos Dinámico
|
|
Los tipos se detectan automáticamente desde custom_types/
|
|
Use menú Tipos → Información de tipos para ver tipos disponibles
|
|
|
|
## Sintaxis con Corchetes (Dinámica)
|
|
Tipo[valor] # Sintaxis general
|
|
Tipo[arg1; arg2] # Múltiples argumentos
|
|
|
|
## Métodos Disponibles (Dinámicos)
|
|
Tipo[...].método() # Métodos específicos del tipo
|
|
objeto.método[] # Método sin argumentos
|
|
|
|
## Ecuaciones (detección automática)
|
|
expresión = expresión # Ecuación simple
|
|
expresión == expresión # Igualdad SymPy
|
|
expresión > expresión # Desigualdad SymPy
|
|
|
|
## Resolver
|
|
solve(ecuación, variable)
|
|
variable=? # Atajo para solve(variable)
|
|
|
|
## Variables SymPy Puras
|
|
x = valor # Crea Symbol('x')
|
|
expresión # Evaluación simbólica automática
|
|
"""
|
|
|
|
self._show_info_dialog("Sintaxis", syntax)
|
|
|
|
def show_sympy_functions(self):
|
|
"""Muestra funciones SymPy disponibles"""
|
|
functions = """# Funciones SymPy Disponibles
|
|
|
|
## Matemáticas Básicas
|
|
sin(x), cos(x), tan(x)
|
|
asin(x), acos(x), atan(x)
|
|
sinh(x), cosh(x), tanh(x)
|
|
exp(x), log(x), sqrt(x)
|
|
abs(x), sign(x), factorial(x)
|
|
|
|
## Cálculo
|
|
diff(expr, var) # Derivada
|
|
integrate(expr, var) # Integral indefinida
|
|
integrate(expr, (var, a, b)) # Integral definida
|
|
limit(expr, var, punto) # Límite
|
|
series(expr, var, punto, n) # Serie de Taylor
|
|
|
|
## Álgebra
|
|
solve(ecuación, variable)
|
|
simplify(expr), expand(expr)
|
|
factor(expr), collect(expr, var)
|
|
cancel(expr), apart(expr, var)
|
|
|
|
## Álgebra Lineal
|
|
Matrix([[a, b], [c, d]])
|
|
det(matrix), inv(matrix)
|
|
|
|
## Plotting
|
|
plot(expr, (var, inicio, fin))
|
|
plot3d(expr, (x, x1, x2), (y, y1, y2))
|
|
|
|
## Constantes
|
|
pi, E, I (imaginario), oo (infinito)
|
|
"""
|
|
|
|
self._show_info_dialog("Funciones SymPy", functions)
|
|
|
|
def show_about(self):
|
|
"""Muestra información sobre la aplicación"""
|
|
about = """Calculadora MAV - CAS Híbrido
|
|
|
|
Versión: 2.1 PySide6 (Sistema de Tipos Dinámico)
|
|
Motor: SymPy + Auto-descubrimiento de Tipos
|
|
|
|
Características:
|
|
• Motor algebraico completo (SymPy)
|
|
• Sistema de tipos dinámico y extensible
|
|
• Sintaxis simplificada con corchetes
|
|
• Detección automática de ecuaciones
|
|
• Resultados interactivos clickeables
|
|
• Auto-descubrimiento de tipos en custom_types/
|
|
• Variables SymPy puras
|
|
• Plotting integrado
|
|
• Autocompletado dinámico
|
|
|
|
Desarrollado para cálculo matemático avanzado
|
|
con soporte especializado para redes,
|
|
programación y análisis numérico.
|
|
"""
|
|
|
|
QMessageBox.about(self, "Acerca de", about)
|
|
|
|
def _show_info_dialog(self, title: str, content: str):
|
|
"""Muestra diálogo de información con scroll"""
|
|
dialog = QMessageBox(self)
|
|
dialog.setWindowTitle(title)
|
|
dialog.setIcon(QMessageBox.Information)
|
|
dialog.setText(content[:200] + "..." if len(content) > 200 else content)
|
|
dialog.setDetailedText(content)
|
|
dialog.exec()
|
|
|
|
def _diagnose_mathjax(self):
|
|
"""Ejecuta diagnóstico de MathJax"""
|
|
if not hasattr(self.latex_panel, '_webview_available') or not self.latex_panel._webview_available:
|
|
QMessageBox.warning(self, "Diagnóstico", "Panel LaTeX no usa WebEngine (usando fallback)")
|
|
return
|
|
|
|
# Aquí iría el código de diagnóstico
|
|
# Por ahora solo mostrar estado
|
|
status = "WebEngine disponible" if self.latex_panel._webview_available else "Usando fallback HTML"
|
|
equations = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0
|
|
|
|
info = f"""DIAGNÓSTICO MATHJAX
|
|
|
|
Estado: {status}
|
|
Ecuaciones en memoria: {equations}
|
|
Panel visible: {self.latex_panel_visible}
|
|
|
|
Para depuración completa, revise la consola del navegador
|
|
en el WebEngineView.
|
|
"""
|
|
|
|
self._show_info_dialog("Diagnóstico MathJax", info)
|
|
|
|
def _show_latex_panel_status(self):
|
|
"""Muestra estado del panel LaTeX"""
|
|
panel_exists = hasattr(self, 'latex_panel')
|
|
panel_visible = self.latex_panel_visible if panel_exists else False
|
|
webview_available = self.latex_panel._webview_available if panel_exists else False
|
|
equations_count = len(self._latex_equations) if hasattr(self, '_latex_equations') else 0
|
|
|
|
status_message = f"""ESTADO DEL PANEL LATEX
|
|
|
|
COMPONENTES:
|
|
• Panel creado: {'✓' if panel_exists else '✗'}
|
|
• Panel visible: {'✓' if panel_visible else '✗'}
|
|
• WebEngine disponible: {'✓' if webview_available else '✗'}
|
|
|
|
CONTENIDO:
|
|
• Ecuaciones en memoria: {equations_count}
|
|
|
|
PARA SOLUCIONAR:
|
|
1. Si las ecuaciones están en memoria pero no se ven:
|
|
→ Cerrar y reabrir el panel LaTeX
|
|
2. Si WebEngine no está disponible:
|
|
→ Instalar con: pip install PySide6-WebEngine
|
|
"""
|
|
|
|
self._show_info_dialog("Estado Panel LaTeX", status_message)
|
|
|
|
# ========== UTILIDADES ==========
|
|
|
|
def _obtener_ayuda(self, input_str: str) -> Optional[str]:
|
|
"""Obtiene ayuda usando helpers dinámicos"""
|
|
for helper in self.HELPERS:
|
|
try:
|
|
ayuda = helper(input_str)
|
|
if ayuda:
|
|
return ayuda
|
|
except Exception as e:
|
|
self.logger.debug(f"Error en helper: {e}")
|
|
return None
|
|
|
|
def _update_status(self, message: str, timeout: int = 0):
|
|
"""Actualiza la barra de estado"""
|
|
self.status_bar.showMessage(message, timeout)
|
|
|
|
def _load_settings(self) -> Dict[str, Any]:
|
|
"""Carga configuración de la aplicación"""
|
|
if os.path.exists(self.SETTINGS_FILE):
|
|
try:
|
|
with open(self.SETTINGS_FILE, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
except:
|
|
pass
|
|
return {
|
|
"window_geometry": None,
|
|
"splitter_sizes": None,
|
|
"debug_mode": False,
|
|
"latex_panel_visible": True
|
|
}
|
|
|
|
def _save_settings(self):
|
|
"""Guarda configuraciones"""
|
|
try:
|
|
# Guardar geometría
|
|
geometry = self.geometry()
|
|
self.settings["window_geometry"] = {
|
|
"x": geometry.x(),
|
|
"y": geometry.y(),
|
|
"width": geometry.width(),
|
|
"height": geometry.height()
|
|
}
|
|
|
|
# Guardar tamaños del splitter
|
|
if hasattr(self, 'main_splitter'):
|
|
self.settings["splitter_sizes"] = self.main_splitter.sizes()
|
|
|
|
self.settings["latex_panel_visible"] = self.latex_panel_visible
|
|
self.settings["debug_mode"] = self.debug
|
|
|
|
with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f:
|
|
json.dump(self.settings, f, indent=4, ensure_ascii=False)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error guardando configuración: {e}")
|
|
|
|
def _load_history(self):
|
|
"""Carga historial de entrada"""
|
|
try:
|
|
if os.path.exists(self.HISTORY_FILE):
|
|
with open(self.HISTORY_FILE, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
if content.strip():
|
|
self.input_text.setPlainText(content)
|
|
# Evaluación inicial
|
|
QTimer.singleShot(100, self._evaluate_and_update)
|
|
except Exception as e:
|
|
self.logger.error(f"Error cargando historial: {e}")
|
|
|
|
def _save_history(self):
|
|
"""Guarda historial de entrada"""
|
|
try:
|
|
content = self.input_text.toPlainText()
|
|
if content:
|
|
with open(self.HISTORY_FILE, "w", encoding="utf-8") as f:
|
|
f.write(content)
|
|
elif os.path.exists(self.HISTORY_FILE):
|
|
os.remove(self.HISTORY_FILE)
|
|
except Exception as e:
|
|
self.logger.error(f"Error guardando historial: {e}")
|
|
|
|
def _restore_geometry(self):
|
|
"""Restaura geometría guardada"""
|
|
try:
|
|
geom = self.settings.get("window_geometry")
|
|
if geom and isinstance(geom, dict):
|
|
self.setGeometry(geom["x"], geom["y"], geom["width"], geom["height"])
|
|
|
|
# Restaurar splitter
|
|
sizes = self.settings.get("splitter_sizes")
|
|
if sizes and hasattr(self, 'main_splitter'):
|
|
self.main_splitter.setSizes(sizes)
|
|
|
|
except Exception as e:
|
|
self.logger.warning(f"No se pudo restaurar geometría: {e}")
|
|
|
|
def closeEvent(self, event):
|
|
"""Maneja el cierre de la aplicación"""
|
|
try:
|
|
# Guardar configuración
|
|
self._save_settings()
|
|
self._save_history()
|
|
|
|
# Cerrar ventanas interactivas
|
|
if self.interactive_manager:
|
|
self.interactive_manager.close_all_windows()
|
|
|
|
# Cerrar popup si existe
|
|
self._close_autocomplete_popup()
|
|
|
|
event.accept()
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error durante cierre: {e}")
|
|
event.accept()
|
|
|
|
|
|
def main():
|
|
"""Función principal"""
|
|
app = QApplication(sys.argv)
|
|
app.setApplicationName("Calculadora MAV")
|
|
app.setOrganizationName("MAV")
|
|
|
|
# Configurar estilo
|
|
app.setStyle('Fusion')
|
|
|
|
# Crear ventana principal
|
|
window = HybridCalculatorPySide6()
|
|
window.show()
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|