Compare commits
No commits in common. "fd1ff0a421caf2aaa5aef3b73a291adf4688c5b3" and "01651fc4548b013db30bdbc26b0e1a8cea8ff22c" have entirely different histories.
fd1ff0a421
...
01651fc454
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
Use MemoriaDeEvolucion.md in the .doc folder to access the latest modifications and concepts for this project. For each modification, add the important knowledge and decisions to the file, keeping the style simple with plain text, minimal code, and summarized content. Also, include a summarized version of the user's prompt each time to maintain context of the request.
|
|
||||||
|
|
||||||
The aplication comments and variables must be in english.
|
|
|
@ -1,49 +1,14 @@
|
||||||
# Italiano
|
|
||||||
total_italiano = 72
|
|
||||||
hechas_italiano = 29
|
|
||||||
faltan_italiano = total_italiano - hechas_italiano
|
|
||||||
|
|
||||||
# Matematica
|
salario=horas*tarifa
|
||||||
total_mate = 40
|
salario=800
|
||||||
hechas_mate = 8
|
tarifa=36
|
||||||
faltan_mate = total_mate - hechas_mate
|
|
||||||
|
horas=?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Matematica Libro
|
|
||||||
total__mate2 = 26
|
|
||||||
hechas_mate2 = 12
|
|
||||||
faltan_mate2 = total__mate2 - hechas_mate2
|
|
||||||
|
|
||||||
# Ingles
|
|
||||||
total__english = 48
|
|
||||||
hechas_english = 35
|
|
||||||
faltan_english = total__english - hechas_english
|
|
||||||
|
|
||||||
|
|
||||||
# Detective ITA
|
|
||||||
total__detective_ita = 52
|
|
||||||
hechas_detective_ita = 52
|
|
||||||
faltan_detective_ita = total__detective_ita - hechas_detective_ita
|
|
||||||
|
|
||||||
# Estado actual de dias
|
|
||||||
dias_pasados = Today()-Date('06/06/25')
|
|
||||||
dias_faltan = Date('14/9/2025')-Today()
|
|
||||||
dias_totales = Date('14/9/2025')-Date('06/06/25')
|
|
||||||
|
|
||||||
# Resumen
|
|
||||||
total = total_italiano+total_mate+total__mate2+total__english + total__detective_ita
|
|
||||||
hecho = hechas_italiano + hechas_mate + hechas_mate2 + hechas_english + hechas_detective_ita
|
|
||||||
faltan = total - hecho
|
|
||||||
porcentaje_hecho = (hecho/total)*100
|
|
||||||
|
|
||||||
# Porcentaje de Vaciones ya pasada
|
|
||||||
100*dias_pasados/dias_totales
|
|
||||||
|
|
||||||
|
|
||||||
# Promedio actual
|
|
||||||
hecho/dias_pasados
|
|
||||||
|
|
||||||
# Minimo para terminar
|
|
||||||
hacer_por_dia = faltan/dias_faltan
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,6 @@ __pycache__/
|
||||||
|
|
||||||
*.json
|
*.json
|
||||||
|
|
||||||
.mathjax/
|
|
||||||
|
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
|
|
|
@ -407,13 +407,6 @@ class PureAlgebraicEngine:
|
||||||
eval_context = self._get_complete_context()
|
eval_context = self._get_complete_context()
|
||||||
result_obj = sympify(expression_str, locals=eval_context)
|
result_obj = sympify(expression_str, locals=eval_context)
|
||||||
|
|
||||||
# NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica automática en asignaciones
|
|
||||||
if hasattr(result_obj, 'free_symbols') and result_obj.free_symbols and self.equations:
|
|
||||||
simplified_obj = self._apply_symbolic_simplification(result_obj)
|
|
||||||
if simplified_obj != result_obj:
|
|
||||||
result_obj = simplified_obj
|
|
||||||
self.logger.debug(f"Simplificación simbólica aplicada en asignación: {expression_str} → {result_obj}")
|
|
||||||
|
|
||||||
# 1. ASIGNACIÓN AL CONTEXTO (para evaluación directa)
|
# 1. ASIGNACIÓN AL CONTEXTO (para evaluación directa)
|
||||||
self.symbol_table[var_name] = result_obj
|
self.symbol_table[var_name] = result_obj
|
||||||
self.variables.add(var_name)
|
self.variables.add(var_name)
|
||||||
|
@ -516,13 +509,6 @@ class PureAlgebraicEngine:
|
||||||
eval_context = self._get_complete_context()
|
eval_context = self._get_complete_context()
|
||||||
result_obj = sympify(line, locals=eval_context)
|
result_obj = sympify(line, locals=eval_context)
|
||||||
|
|
||||||
# NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica automática
|
|
||||||
if hasattr(result_obj, 'free_symbols') and result_obj.free_symbols and self.equations:
|
|
||||||
simplified_obj = self._apply_symbolic_simplification(result_obj)
|
|
||||||
if simplified_obj != result_obj:
|
|
||||||
result_obj = simplified_obj
|
|
||||||
self.logger.debug(f"Simplificación simbólica aplicada: {line} → {result_obj}")
|
|
||||||
|
|
||||||
# Si es un PlotResult, asegurar que tenga la línea original
|
# Si es un PlotResult, asegurar que tenga la línea original
|
||||||
if isinstance(result_obj, PlotResult) and not result_obj.original_expression:
|
if isinstance(result_obj, PlotResult) and not result_obj.original_expression:
|
||||||
result_obj.original_expression = line
|
result_obj.original_expression = line
|
||||||
|
@ -614,33 +600,15 @@ class PureAlgebraicEngine:
|
||||||
|
|
||||||
# Verificar que el resultado no sea problemático
|
# Verificar que el resultado no sea problemático
|
||||||
if final_value != var_symbol and not isinstance(final_value, (sp.logic.boolalg.BooleanTrue, sp.logic.boolalg.BooleanFalse)):
|
if final_value != var_symbol and not isinstance(final_value, (sp.logic.boolalg.BooleanTrue, sp.logic.boolalg.BooleanFalse)):
|
||||||
# NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica al resultado final
|
|
||||||
if hasattr(final_value, 'free_symbols') and final_value.free_symbols and self.equations:
|
|
||||||
simplified_final = self._apply_symbolic_simplification(final_value)
|
|
||||||
if simplified_final != final_value:
|
|
||||||
self.logger.debug(f"Simplificación aplicada en resultado final: {final_value} → {simplified_final}")
|
|
||||||
final_value = simplified_final
|
|
||||||
return Eq(var_symbol, final_value)
|
return Eq(var_symbol, final_value)
|
||||||
else:
|
else:
|
||||||
# Si hay problema con la resolución, devolver simbólico
|
# Si hay problema con la resolución, devolver simbólico
|
||||||
# Aplicar simplificación también al valor simbólico
|
|
||||||
if hasattr(solution_value, 'free_symbols') and solution_value.free_symbols and self.equations:
|
|
||||||
simplified_solution = self._apply_symbolic_simplification(solution_value)
|
|
||||||
if simplified_solution != solution_value:
|
|
||||||
self.logger.debug(f"Simplificación aplicada en solución simbólica: {solution_value} → {simplified_solution}")
|
|
||||||
solution_value = simplified_solution
|
|
||||||
return Eq(var_symbol, solution_value)
|
return Eq(var_symbol, solution_value)
|
||||||
else:
|
else:
|
||||||
# Hay variables con valores simbólicos, devolver ecuación simbólica
|
# Hay variables con valores simbólicos, devolver ecuación simbólica
|
||||||
return Eq(var_symbol, solution_value)
|
return Eq(var_symbol, solution_value)
|
||||||
else:
|
else:
|
||||||
# No hay variables sin resolver, es ya un valor final
|
# No hay variables sin resolver, es ya un valor final
|
||||||
# NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica al valor final
|
|
||||||
if hasattr(solution_value, 'free_symbols') and solution_value.free_symbols and self.equations:
|
|
||||||
simplified_solution = self._apply_symbolic_simplification(solution_value)
|
|
||||||
if simplified_solution != solution_value:
|
|
||||||
self.logger.debug(f"Simplificación aplicada en valor final: {solution_value} → {simplified_solution}")
|
|
||||||
solution_value = simplified_solution
|
|
||||||
return Eq(var_symbol, solution_value)
|
return Eq(var_symbol, solution_value)
|
||||||
else:
|
else:
|
||||||
# Si no hay solución en las ecuaciones, verificar en symbol_table
|
# Si no hay solución en las ecuaciones, verificar en symbol_table
|
||||||
|
@ -649,12 +617,6 @@ class PureAlgebraicEngine:
|
||||||
value = self.symbol_table[var_name]
|
value = self.symbol_table[var_name]
|
||||||
# Si el valor en symbol_table es diferente de la variable, devolverlo
|
# Si el valor en symbol_table es diferente de la variable, devolverlo
|
||||||
if value != var_symbol:
|
if value != var_symbol:
|
||||||
# NUEVA FUNCIONALIDAD: Aplicar simplificación simbólica también aquí
|
|
||||||
if hasattr(value, 'free_symbols') and value.free_symbols and self.equations:
|
|
||||||
simplified_value = self._apply_symbolic_simplification(value)
|
|
||||||
if simplified_value != value:
|
|
||||||
self.logger.debug(f"Simplificación aplicada en solve: {value} → {simplified_value}")
|
|
||||||
value = simplified_value
|
|
||||||
return Eq(var_symbol, value)
|
return Eq(var_symbol, value)
|
||||||
|
|
||||||
# Si no hay información, devolver ecuación con la variable sin resolver
|
# Si no hay información, devolver ecuación con la variable sin resolver
|
||||||
|
@ -753,85 +715,6 @@ class PureAlgebraicEngine:
|
||||||
self.logger.debug(f"Error resolviendo {var_symbol}: {e}")
|
self.logger.debug(f"Error resolviendo {var_symbol}: {e}")
|
||||||
return var_symbol
|
return var_symbol
|
||||||
|
|
||||||
def _apply_symbolic_simplification(self, expression):
|
|
||||||
"""
|
|
||||||
Aplica simplificación simbólica automática usando ecuaciones definidas
|
|
||||||
|
|
||||||
Esta función sustituye símbolos por sus definiciones de ecuaciones cuando es posible,
|
|
||||||
permitiendo simplificaciones como: mS = mmS/1000, entonces 50000*Hz*mS/mmS → 50*Hz
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
current_expr = expression
|
|
||||||
max_iterations = 5 # Evitar bucles infinitos
|
|
||||||
|
|
||||||
for iteration in range(max_iterations):
|
|
||||||
substituted = current_expr
|
|
||||||
substitution_made = False
|
|
||||||
|
|
||||||
# Obtener símbolos libres de la expresión actual
|
|
||||||
free_symbols = substituted.free_symbols
|
|
||||||
|
|
||||||
# Para cada símbolo en la expresión, buscar si hay una ecuación que lo defina
|
|
||||||
for symbol in free_symbols:
|
|
||||||
symbol_definition = self._find_symbol_definition(symbol)
|
|
||||||
|
|
||||||
if symbol_definition is not None and symbol_definition != symbol:
|
|
||||||
# Aplicar la sustitución
|
|
||||||
new_expr = substituted.subs(symbol, symbol_definition)
|
|
||||||
|
|
||||||
# Simplificar después de la sustitución
|
|
||||||
simplified = sp.simplify(new_expr)
|
|
||||||
|
|
||||||
if simplified != substituted:
|
|
||||||
substituted = simplified
|
|
||||||
substitution_made = True
|
|
||||||
self.logger.debug(f"Símbolo {symbol} sustituido por {symbol_definition}")
|
|
||||||
|
|
||||||
# Si no se hicieron sustituciones, terminar
|
|
||||||
if not substitution_made:
|
|
||||||
break
|
|
||||||
|
|
||||||
current_expr = substituted
|
|
||||||
|
|
||||||
return current_expr
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.debug(f"Error en simplificación simbólica: {e}")
|
|
||||||
return expression
|
|
||||||
|
|
||||||
def _find_symbol_definition(self, symbol):
|
|
||||||
"""
|
|
||||||
Busca la definición de un símbolo en las ecuaciones del sistema
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
La expresión que define al símbolo, o None si no se encuentra
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Buscar en ecuaciones donde el símbolo aparece solo en el lado izquierdo
|
|
||||||
for eq in self.equations:
|
|
||||||
if hasattr(eq, 'lhs') and hasattr(eq, 'rhs'):
|
|
||||||
# Caso 1: símbolo = expresión
|
|
||||||
if eq.lhs == symbol:
|
|
||||||
return eq.rhs
|
|
||||||
|
|
||||||
# Caso 2: expresión = símbolo
|
|
||||||
elif eq.rhs == symbol:
|
|
||||||
return eq.lhs
|
|
||||||
|
|
||||||
# Buscar en symbol_table como respaldo
|
|
||||||
symbol_name = str(symbol)
|
|
||||||
if symbol_name in self.symbol_table:
|
|
||||||
value = self.symbol_table[symbol_name]
|
|
||||||
# Solo usar si el valor es diferente del símbolo mismo
|
|
||||||
if value != symbol:
|
|
||||||
return value
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.debug(f"Error buscando definición de {symbol}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _resolve_iteratively(self, expression, max_iterations=10):
|
def _resolve_iteratively(self, expression, max_iterations=10):
|
||||||
"""Resuelve una expresión iterativamente sustituyendo valores conocidos"""
|
"""Resuelve una expresión iterativamente sustituyendo valores conocidos"""
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -40,7 +40,7 @@ class EvaluationManager:
|
||||||
'custom_type': self._create_format("#4ec9b0"),
|
'custom_type': self._create_format("#4ec9b0"),
|
||||||
'plot': self._create_format("#569cd6", underline=True),
|
'plot': self._create_format("#569cd6", underline=True),
|
||||||
'type_indicator': self._create_format("#808080"),
|
'type_indicator': self._create_format("#808080"),
|
||||||
'clickable': self._create_format("#4fc3f7", underline=True, bold=True),
|
'clickable': self._create_format("#4fc3f7", underline=True),
|
||||||
'helper': self._create_format("#ffd700", italic=True)
|
'helper': self._create_format("#ffd700", italic=True)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,7 +254,6 @@ class EvaluationManager:
|
||||||
cursor.insertText(display_text, self.output_formats.get('clickable'))
|
cursor.insertText(display_text, self.output_formats.get('clickable'))
|
||||||
end_pos = cursor.position()
|
end_pos = cursor.position()
|
||||||
self.main_window.output_text.clickable_links[(start_pos, end_pos)] = (link_id, result_object)
|
self.main_window.output_text.clickable_links[(start_pos, end_pos)] = (link_id, result_object)
|
||||||
self.logger.debug(f"🔗 Link creado: {display_text} en posición {start_pos}-{end_pos}, ID: {link_id}")
|
|
||||||
|
|
||||||
elif len(part_data) >= 2:
|
elif len(part_data) >= 2:
|
||||||
tag, content = part_data[0], part_data[1]
|
tag, content = part_data[0], part_data[1]
|
||||||
|
@ -333,10 +332,3 @@ class EvaluationManager:
|
||||||
"""Programa una evaluación con debounce"""
|
"""Programa una evaluación con debounce"""
|
||||||
self._debounce_timer.stop()
|
self._debounce_timer.stop()
|
||||||
self._debounce_timer.start(300) # 300ms de debounce
|
self._debounce_timer.start(300) # 300ms de debounce
|
||||||
|
|
||||||
def handle_output_link_click(self, link_id: str, result_object):
|
|
||||||
"""Maneja clicks en links del output delegando al interactive manager"""
|
|
||||||
if self.main_window.interactive_manager:
|
|
||||||
self.main_window.interactive_manager.handle_interactive_click(result_object)
|
|
||||||
else:
|
|
||||||
self.logger.warning("Interactive manager no disponible para manejar click")
|
|
270
app/gui_latex.py
270
app/gui_latex.py
|
@ -10,7 +10,6 @@ from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional
|
||||||
from .evaluation import EvaluationResult
|
from .evaluation import EvaluationResult
|
||||||
from .mathjax_manager import MathJaxManager
|
|
||||||
import sympy
|
import sympy
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,12 +24,6 @@ class LatexPanel(QWidget):
|
||||||
self._pending_equations = []
|
self._pending_equations = []
|
||||||
self._parent_calculator = parent
|
self._parent_calculator = parent
|
||||||
self._webview_initialized = False
|
self._webview_initialized = False
|
||||||
self._mathjax_timeout_count = 0
|
|
||||||
self._max_mathjax_timeout = 20 # 10 segundos (20 * 500ms)
|
|
||||||
|
|
||||||
# Manager de MathJax local
|
|
||||||
self.mathjax_manager = MathJaxManager()
|
|
||||||
|
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
|
|
||||||
# Timer para verificar si MathJax está listo (inicializado bajo demanda)
|
# Timer para verificar si MathJax está listo (inicializado bajo demanda)
|
||||||
|
@ -114,131 +107,93 @@ class LatexPanel(QWidget):
|
||||||
""")
|
""")
|
||||||
|
|
||||||
def _generate_mathjax_html(self):
|
def _generate_mathjax_html(self):
|
||||||
"""Genera HTML base con MathJax configurado para SVG (local o CDN)"""
|
"""Genera HTML base con MathJax configurado para SVG"""
|
||||||
# Obtener URL de MathJax (local si está disponible, CDN como fallback)
|
return """
|
||||||
mathjax_url = self.mathjax_manager.get_mathjax_url()
|
<!DOCTYPE html>
|
||||||
|
|
||||||
return f"""<!DOCTYPE html>
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
<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-svg.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Intentar cargar MathJax local primero, luego CDN como fallback
|
window.MathJax = {
|
||||||
window.mathjaxLoadAttempts = 0;
|
tex: {
|
||||||
window.mathJaxReady = false;
|
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
||||||
|
displayMath: [['$$', '$$'], ['\\[', '\\]']],
|
||||||
function loadMathJax(url, isFallback = false) {{
|
|
||||||
return new Promise((resolve, reject) => {{
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'MathJax-script';
|
|
||||||
script.async = true;
|
|
||||||
script.src = url;
|
|
||||||
|
|
||||||
script.onload = () => {{
|
|
||||||
console.log('MathJax cargado desde:', url);
|
|
||||||
resolve();
|
|
||||||
}};
|
|
||||||
|
|
||||||
script.onerror = () => {{
|
|
||||||
console.error('Error cargando MathJax desde:', url);
|
|
||||||
reject(new Error('MathJax load failed'));
|
|
||||||
}};
|
|
||||||
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}});
|
|
||||||
}}
|
|
||||||
|
|
||||||
// Configuración de MathJax
|
|
||||||
window.MathJax = {{
|
|
||||||
tex: {{
|
|
||||||
inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],
|
|
||||||
displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],
|
|
||||||
processEscapes: true
|
processEscapes: true
|
||||||
}},
|
},
|
||||||
svg: {{
|
svg: {
|
||||||
scale: 0.9,
|
scale: 0.9,
|
||||||
minScale: 0.5,
|
minScale: 0.5,
|
||||||
fontCache: 'global'
|
fontCache: 'global'
|
||||||
}},
|
},
|
||||||
startup: {{
|
startup: {
|
||||||
ready: function () {{
|
ready: function () {
|
||||||
MathJax.startup.defaultReady();
|
MathJax.startup.defaultReady();
|
||||||
|
// Notificar que MathJax está listo
|
||||||
window.mathJaxReady = true;
|
window.mathJaxReady = true;
|
||||||
console.log('MathJax SVG completamente listo');
|
console.log('MathJax SVG completamente cargado');
|
||||||
}}
|
}
|
||||||
}}
|
}
|
||||||
}};
|
};
|
||||||
|
|
||||||
// Intentar cargar MathJax local, luego CDN
|
|
||||||
loadMathJax('{mathjax_url}').catch(() => {{
|
|
||||||
console.log('Fallback a CDN de MathJax...');
|
|
||||||
return loadMathJax('https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js', true);
|
|
||||||
}}).catch(() => {{
|
|
||||||
console.error('No se pudo cargar MathJax desde ninguna fuente');
|
|
||||||
// Marcar como listo para mostrar contenido sin renderizado
|
|
||||||
setTimeout(() => {{
|
|
||||||
window.mathJaxReady = true;
|
|
||||||
console.log('Modo fallback activado - sin renderizado LaTeX');
|
|
||||||
}}, 2000);
|
|
||||||
}});
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
body {{
|
body {
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
font-family: 'Segoe UI', Arial, sans-serif;
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}}
|
}
|
||||||
.equation-block {{
|
.equation-block {
|
||||||
background: rgba(45, 45, 48, 0.8);
|
background: rgba(45, 45, 48, 0.8);
|
||||||
border-left: 3px solid;
|
border-left: 3px solid;
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: all 0.1s ease;
|
transition: all 0.1s ease;
|
||||||
}}
|
}
|
||||||
.equation-block:hover {{
|
.equation-block:hover {
|
||||||
background: rgba(45, 45, 48, 0.9);
|
background: rgba(45, 45, 48, 0.9);
|
||||||
}}
|
}
|
||||||
.comment {{ border-left-color: #6a9955; }}
|
.comment { border-left-color: #6a9955; }
|
||||||
.assignment {{ border-left-color: #dcdcaa; }}
|
.assignment { border-left-color: #dcdcaa; }
|
||||||
.equation {{ border-left-color: #c586c0; }}
|
.equation { border-left-color: #c586c0; }
|
||||||
.symbolic {{ border-left-color: #9cdcfe; }}
|
.symbolic { border-left-color: #9cdcfe; }
|
||||||
|
|
||||||
.math-content {{
|
.math-content {
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}}
|
}
|
||||||
|
|
||||||
.comment-text {{
|
.comment-text {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #6a9955;
|
color: #6a9955;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}}
|
}
|
||||||
|
|
||||||
.info-message {{
|
.info-message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}}
|
}
|
||||||
|
|
||||||
/* Optimizaciones para SVG rendering */
|
/* Optimizaciones para SVG rendering */
|
||||||
mjx-container[jax="SVG"] {{
|
mjx-container[jax="SVG"] {
|
||||||
margin: 2px 0 !important;
|
margin: 2px 0 !important;
|
||||||
}}
|
}
|
||||||
|
|
||||||
mjx-container[jax="SVG"] svg {{
|
mjx-container[jax="SVG"] svg {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}}
|
}
|
||||||
|
|
||||||
mjx-container[jax="SVG"] svg text {{
|
/* Mejorar contraste del SVG */
|
||||||
|
mjx-container[jax="SVG"] svg text {
|
||||||
fill: #d4d4d4 !important;
|
fill: #d4d4d4 !important;
|
||||||
}}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -249,13 +204,10 @@ class LatexPanel(QWidget):
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function addEquation(type, content) {{
|
window.mathJaxReady = false;
|
||||||
try {{
|
|
||||||
|
function addEquation(type, content) {
|
||||||
var container = document.getElementById('equations-container');
|
var container = document.getElementById('equations-container');
|
||||||
if (!container) {{
|
|
||||||
console.error('Container no encontrado');
|
|
||||||
return;
|
|
||||||
}}
|
|
||||||
|
|
||||||
// Limpiar mensaje inicial si existe
|
// Limpiar mensaje inicial si existe
|
||||||
var infoMsg = container.querySelector('.info-message');
|
var infoMsg = container.querySelector('.info-message');
|
||||||
|
@ -267,72 +219,38 @@ class LatexPanel(QWidget):
|
||||||
var mathContent = document.createElement('div');
|
var mathContent = document.createElement('div');
|
||||||
mathContent.className = 'math-content';
|
mathContent.className = 'math-content';
|
||||||
|
|
||||||
if (type === 'comment') {{
|
if (type === 'comment') {
|
||||||
mathContent.className = 'comment-text';
|
mathContent.className = 'comment-text';
|
||||||
mathContent.textContent = content;
|
mathContent.textContent = content;
|
||||||
console.log('Comentario agregado:', content);
|
} else {
|
||||||
}} else {{
|
|
||||||
mathContent.innerHTML = '$$' + content + '$$';
|
mathContent.innerHTML = '$$' + content + '$$';
|
||||||
console.log('Ecuación agregada:', content);
|
}
|
||||||
}}
|
|
||||||
|
|
||||||
equation.appendChild(mathContent);
|
equation.appendChild(mathContent);
|
||||||
container.appendChild(equation);
|
container.appendChild(equation);
|
||||||
|
|
||||||
// Re-renderizar MathJax solo si está listo
|
// Re-renderizar MathJax solo si está listo
|
||||||
if (window.MathJax && window.mathJaxReady && type !== 'comment') {{
|
if (window.MathJax && window.mathJaxReady && type !== 'comment') {
|
||||||
console.log('Renderizando con MathJax...');
|
MathJax.typesetPromise([equation]).catch(function(err) {
|
||||||
MathJax.typesetPromise([equation]).then(() => {{
|
console.error('MathJax SVG error:', err);
|
||||||
console.log('Ecuación renderizada exitosamente');
|
});
|
||||||
}}).catch(function(err) {{
|
}
|
||||||
console.error('Error renderizando con MathJax:', err);
|
}
|
||||||
// Mostrar como texto plano si falla
|
|
||||||
mathContent.innerHTML = content;
|
|
||||||
}});
|
|
||||||
}} else if (type !== 'comment') {{
|
|
||||||
console.log('MathJax no listo, mostrando como texto:', window.mathJaxReady);
|
|
||||||
// Mostrar como texto plano si MathJax no está listo
|
|
||||||
mathContent.innerHTML = content;
|
|
||||||
}}
|
|
||||||
|
|
||||||
}} catch (e) {{
|
function clearEquations() {
|
||||||
console.error('Error en addEquation:', e);
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
function clearEquations() {{
|
|
||||||
try {{
|
|
||||||
var container = document.getElementById('equations-container');
|
var container = document.getElementById('equations-container');
|
||||||
if (container) {{
|
|
||||||
container.innerHTML = '<div class="info-message">Panel de Ecuaciones LaTeX (SVG)</div>';
|
container.innerHTML = '<div class="info-message">Panel de Ecuaciones LaTeX (SVG)</div>';
|
||||||
console.log('Ecuaciones limpiadas');
|
}
|
||||||
}}
|
|
||||||
}} catch (e) {{
|
|
||||||
console.error('Error en clearEquations:', e);
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
// Función para notificar que está listo para renderizar
|
// Función para notificar que está listo para renderizar
|
||||||
function triggerInitialRender() {{
|
function triggerInitialRender() {
|
||||||
try {{
|
if (window.mathJaxReady) {
|
||||||
console.log('Verificando estado MathJax:', window.mathJaxReady);
|
// Notificar a Python que MathJax está listo
|
||||||
if (window.mathJaxReady) {{
|
|
||||||
console.log('MathJax SVG listo para renderizado inicial');
|
console.log('MathJax SVG listo para renderizado inicial');
|
||||||
return true;
|
return true;
|
||||||
}}
|
}
|
||||||
return false;
|
return false;
|
||||||
}} catch (e) {{
|
}
|
||||||
console.error('Error en triggerInitialRender:', e);
|
|
||||||
return false;
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
// Monitor de estado para debug
|
|
||||||
setInterval(() => {{
|
|
||||||
if (window.mathJaxReady) {{
|
|
||||||
console.log('🎯 MathJax listo detectado');
|
|
||||||
}}
|
|
||||||
}}, 3000);
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>"""
|
||||||
|
@ -363,67 +281,27 @@ class LatexPanel(QWidget):
|
||||||
if not self._webview_available:
|
if not self._webview_available:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Verificar múltiples condiciones de MathJax
|
# Verificar si MathJax está listo
|
||||||
js_check = """
|
self.webview.page().runJavaScript(
|
||||||
(function() {
|
"window.mathJaxReady || false;",
|
||||||
try {
|
self._on_mathjax_ready_check
|
||||||
// Verificar si window.mathJaxReady está definido
|
)
|
||||||
if (window.mathJaxReady === true) return true;
|
|
||||||
|
|
||||||
// Verificar si MathJax está disponible y listo
|
|
||||||
if (window.MathJax && window.MathJax.startup && window.MathJax.startup.document && window.MathJax.startup.document.state() >= 8) {
|
|
||||||
window.mathJaxReady = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar si existen las funciones necesarias
|
|
||||||
if (typeof addEquation === 'function' && typeof clearEquations === 'function') {
|
|
||||||
window.mathJaxReady = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch(e) {
|
|
||||||
console.error('Error verificando MathJax:', e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.webview.page().runJavaScript(js_check, self._on_mathjax_ready_check)
|
|
||||||
|
|
||||||
def _on_mathjax_ready_check(self, ready):
|
def _on_mathjax_ready_check(self, ready):
|
||||||
"""Callback cuando se verifica el estado de MathJax"""
|
"""Callback cuando se verifica el estado de MathJax"""
|
||||||
logging.debug(f"🔍 MathJax ready check: {ready}")
|
|
||||||
|
|
||||||
if ready and not self._mathjax_ready:
|
if ready and not self._mathjax_ready:
|
||||||
self._mathjax_ready = True
|
self._mathjax_ready = True
|
||||||
self._mathjax_check_timer.stop()
|
self._mathjax_check_timer.stop()
|
||||||
logging.debug(f"✅ MathJax listo, procesando {len(self._pending_equations)} ecuaciones pendientes")
|
logging.debug("✅ MathJax listo, procesando ecuaciones pendientes")
|
||||||
|
|
||||||
# Renderizar ecuaciones pendientes
|
# Renderizar ecuaciones pendientes
|
||||||
for eq in self._pending_equations:
|
for eq in self._pending_equations:
|
||||||
self._add_equation_to_webview(eq['type'], eq['content'])
|
self._add_equation_to_webview(eq['type'], eq['content'])
|
||||||
logging.debug(f"📊 Ecuación agregada al WebView: {eq['type']}")
|
|
||||||
self._pending_equations.clear()
|
self._pending_equations.clear()
|
||||||
|
|
||||||
# Trigger initial render si hay un calculador padre
|
# Trigger initial render si hay un calculador padre
|
||||||
if self._parent_calculator and hasattr(self._parent_calculator, '_trigger_initial_latex_render'):
|
if self._parent_calculator and hasattr(self._parent_calculator, '_trigger_initial_latex_render'):
|
||||||
self._parent_calculator._trigger_initial_latex_render()
|
self._parent_calculator._trigger_initial_latex_render()
|
||||||
elif not ready:
|
|
||||||
self._mathjax_timeout_count += 1
|
|
||||||
if self._mathjax_timeout_count >= self._max_mathjax_timeout:
|
|
||||||
# Timeout alcanzado, forzar como listo para mostrar contenido en texto plano
|
|
||||||
logging.warning("⚠️ MathJax timeout - cambiando a modo fallback")
|
|
||||||
self._mathjax_ready = True
|
|
||||||
self._mathjax_check_timer.stop()
|
|
||||||
|
|
||||||
# Procesar ecuaciones pendientes como texto plano
|
|
||||||
for eq in self._pending_equations:
|
|
||||||
self._add_equation_as_text(eq['type'], eq['content'])
|
|
||||||
self._pending_equations.clear()
|
|
||||||
else:
|
|
||||||
logging.debug(f"⏳ MathJax aún no está listo ({self._mathjax_timeout_count}/{self._max_mathjax_timeout}), continuando verificación...")
|
|
||||||
|
|
||||||
def _add_equation_to_webview(self, eq_type: str, content: str):
|
def _add_equation_to_webview(self, eq_type: str, content: str):
|
||||||
"""Añade una ecuación directamente al webview"""
|
"""Añade una ecuación directamente al webview"""
|
||||||
|
@ -432,22 +310,6 @@ class LatexPanel(QWidget):
|
||||||
js_code = f"addEquation('{eq_type}', '{escaped_content}');"
|
js_code = f"addEquation('{eq_type}', '{escaped_content}');"
|
||||||
self.webview.page().runJavaScript(js_code)
|
self.webview.page().runJavaScript(js_code)
|
||||||
|
|
||||||
def _add_equation_as_text(self, eq_type: str, content: str):
|
|
||||||
"""Añade una ecuación como texto plano cuando MathJax falla"""
|
|
||||||
if self._webview_available:
|
|
||||||
escaped_content = content.replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"')
|
|
||||||
# Agregar ecuación como texto plano sin MathJax
|
|
||||||
js_code = f"""
|
|
||||||
var container = document.getElementById('equations-container');
|
|
||||||
if (container) {{
|
|
||||||
var equation = document.createElement('div');
|
|
||||||
equation.className = 'equation-block {eq_type}';
|
|
||||||
equation.innerHTML = '<div class="math-content" style="font-family: monospace;">{escaped_content}</div>';
|
|
||||||
container.appendChild(equation);
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
self.webview.page().runJavaScript(js_code)
|
|
||||||
|
|
||||||
def add_equation(self, eq_type: str, content: str):
|
def add_equation(self, eq_type: str, content: str):
|
||||||
"""Añade una ecuación al panel"""
|
"""Añade una ecuación al panel"""
|
||||||
self.equations.append({'type': eq_type, 'content': content})
|
self.equations.append({'type': eq_type, 'content': content})
|
||||||
|
|
|
@ -124,40 +124,26 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
# Sincronizar scroll
|
# Sincronizar scroll
|
||||||
self._setup_scroll_sync()
|
self._setup_scroll_sync()
|
||||||
|
|
||||||
# Crear splitter secundario para LaTeX
|
# Añadir splitter al layout
|
||||||
self.secondary_splitter = QSplitter(Qt.Horizontal)
|
main_layout.addWidget(self.main_splitter)
|
||||||
self.secondary_splitter.addWidget(self.main_splitter)
|
|
||||||
|
|
||||||
# Botón expandible para LaTeX
|
# Botón expandible para LaTeX
|
||||||
self.latex_button = ExpandableLatexButton()
|
self.latex_button = ExpandableLatexButton()
|
||||||
self.latex_button.clicked.connect(self._toggle_latex_panel)
|
self.latex_button.clicked.connect(self._toggle_latex_panel)
|
||||||
|
main_layout.addWidget(self.latex_button)
|
||||||
|
|
||||||
# Panel LaTeX con splitter
|
# Panel LaTeX (inicialmente oculto)
|
||||||
self.latex_panel = LatexPanel(self)
|
self.latex_panel = LatexPanel(self)
|
||||||
self.latex_panel.setMinimumWidth(200)
|
self.latex_panel.setMinimumWidth(300)
|
||||||
|
self.latex_panel.setMaximumWidth(500)
|
||||||
|
|
||||||
if self.latex_panel_visible:
|
if self.latex_panel_visible:
|
||||||
self.secondary_splitter.addWidget(self.latex_panel)
|
main_layout.addWidget(self.latex_panel)
|
||||||
self.latex_button.setChecked(True)
|
self.latex_button.setChecked(True)
|
||||||
# Configurar tamaños: 70% para main_splitter, 30% para latex_panel
|
|
||||||
latex_width = self.settings.get("latex_panel_width", 300)
|
|
||||||
total_width = 1000 # Ancho por defecto de la ventana
|
|
||||||
main_width = total_width - latex_width
|
|
||||||
self.secondary_splitter.setSizes([main_width, latex_width])
|
|
||||||
else:
|
else:
|
||||||
# Asegurar que el panel está oculto por defecto
|
# Asegurar que el panel está oculto por defecto
|
||||||
self.latex_panel.hide()
|
self.latex_panel.hide()
|
||||||
self.latex_button.setChecked(False)
|
self.latex_button.setChecked(False)
|
||||||
self.secondary_splitter.setSizes([1000])
|
|
||||||
|
|
||||||
# Layout principal con splitter secundario y botón
|
|
||||||
splitter_layout = QHBoxLayout()
|
|
||||||
splitter_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
splitter_layout.setSpacing(0)
|
|
||||||
splitter_layout.addWidget(self.secondary_splitter)
|
|
||||||
splitter_layout.addWidget(self.latex_button)
|
|
||||||
|
|
||||||
main_layout.addLayout(splitter_layout)
|
|
||||||
|
|
||||||
# Los formatos de salida se configuran automáticamente en EvaluationManager
|
# Los formatos de salida se configuran automáticamente en EvaluationManager
|
||||||
|
|
||||||
|
@ -169,9 +155,6 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
# Aplicar tema oscuro
|
# Aplicar tema oscuro
|
||||||
self._apply_dark_theme()
|
self._apply_dark_theme()
|
||||||
|
|
||||||
# Aplicar tamaño de fuente guardado
|
|
||||||
self._apply_saved_font_size()
|
|
||||||
|
|
||||||
def _setup_scroll_sync(self):
|
def _setup_scroll_sync(self):
|
||||||
"""Sincroniza el scroll entre entrada y salida"""
|
"""Sincroniza el scroll entre entrada y salida"""
|
||||||
def sync_input_to_output():
|
def sync_input_to_output():
|
||||||
|
@ -259,22 +242,6 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
"""
|
"""
|
||||||
self.setStyleSheet(dark_style)
|
self.setStyleSheet(dark_style)
|
||||||
|
|
||||||
def _apply_saved_font_size(self):
|
|
||||||
"""Aplica el tamaño de fuente guardado"""
|
|
||||||
is_large = self.settings_manager.get_setting("font_size_large", False)
|
|
||||||
font_size = 14 if is_large else 11
|
|
||||||
|
|
||||||
# Aplicar al estilo actual
|
|
||||||
current_style = self.styleSheet()
|
|
||||||
import re
|
|
||||||
|
|
||||||
# Reemplazar font-size en QPlainTextEdit y QTextEdit
|
|
||||||
pattern = r'(QPlainTextEdit, QTextEdit\s*\{[^}]*?)font-size:\s*\d+px;([^}]*\})'
|
|
||||||
replacement = rf'\1font-size: {font_size}px;\2'
|
|
||||||
|
|
||||||
new_style = re.sub(pattern, replacement, current_style)
|
|
||||||
self.setStyleSheet(new_style)
|
|
||||||
|
|
||||||
def _on_input_changed(self):
|
def _on_input_changed(self):
|
||||||
"""Maneja cambios en el texto de entrada"""
|
"""Maneja cambios en el texto de entrada"""
|
||||||
# Delegar al autocomplete manager
|
# Delegar al autocomplete manager
|
||||||
|
@ -293,45 +260,26 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
|
|
||||||
def _handle_output_link_click(self, link_id: str, result_object):
|
def _handle_output_link_click(self, link_id: str, result_object):
|
||||||
"""Maneja clicks en links del output"""
|
"""Maneja clicks en links del output"""
|
||||||
self.logger.debug(f"🖱️ Click en link recibido: {link_id}, objeto: {type(result_object).__name__}")
|
|
||||||
# Delegar al evaluation manager
|
# Delegar al evaluation manager
|
||||||
self.evaluation_manager.handle_output_link_click(link_id, result_object)
|
self.evaluation_manager.handle_output_link_click(link_id, result_object)
|
||||||
|
|
||||||
def _toggle_latex_panel(self):
|
def _toggle_latex_panel(self):
|
||||||
"""Togglea la visibilidad del panel LaTeX"""
|
"""Togglea la visibilidad del panel LaTeX"""
|
||||||
|
main_layout = self.centralWidget().layout()
|
||||||
|
|
||||||
if self.latex_panel_visible:
|
if self.latex_panel_visible:
|
||||||
# Ocultar panel
|
# Ocultar panel
|
||||||
self.secondary_splitter.widget(1).hide() if self.secondary_splitter.count() > 1 else None
|
main_layout.removeWidget(self.latex_panel)
|
||||||
self.latex_panel.hide()
|
self.latex_panel.hide()
|
||||||
self.latex_button.setChecked(False)
|
self.latex_button.setChecked(False)
|
||||||
self.latex_panel_visible = False
|
self.latex_panel_visible = False
|
||||||
self.secondary_splitter.setSizes([1000])
|
|
||||||
else:
|
else:
|
||||||
# Mostrar panel
|
# Mostrar panel
|
||||||
if self.secondary_splitter.count() == 1:
|
main_layout.addWidget(self.latex_panel)
|
||||||
self.secondary_splitter.addWidget(self.latex_panel)
|
|
||||||
|
|
||||||
self.latex_panel.show()
|
self.latex_panel.show()
|
||||||
self.latex_button.setChecked(True)
|
self.latex_button.setChecked(True)
|
||||||
self.latex_panel_visible = True
|
self.latex_panel_visible = True
|
||||||
|
|
||||||
# Configurar tamaños con límites
|
|
||||||
latex_width = self.settings.get("latex_panel_width", 300)
|
|
||||||
total_width = self.width()
|
|
||||||
|
|
||||||
# CORRECCIÓN: Aplicar límites para evitar crecimiento descontrolado
|
|
||||||
min_latex_width = 200
|
|
||||||
max_latex_width = min(600, total_width * 0.5) # Máximo 50% del ancho total
|
|
||||||
latex_width = max(min_latex_width, min(latex_width, max_latex_width))
|
|
||||||
|
|
||||||
main_width = total_width - latex_width - 50 # 50 para el botón
|
|
||||||
# Asegurar que main_width no sea negativo
|
|
||||||
if main_width < 200:
|
|
||||||
main_width = 200
|
|
||||||
latex_width = total_width - main_width - 50
|
|
||||||
|
|
||||||
self.secondary_splitter.setSizes([main_width, latex_width])
|
|
||||||
|
|
||||||
# Actualizar panel con ecuaciones pendientes
|
# Actualizar panel con ecuaciones pendientes
|
||||||
self._sync_latex_equations_on_show()
|
self._sync_latex_equations_on_show()
|
||||||
|
|
||||||
|
@ -357,19 +305,6 @@ class HybridCalculatorPySide6(QMainWindow):
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Maneja el cierre de la aplicación"""
|
"""Maneja el cierre de la aplicación"""
|
||||||
# Guardar ancho del panel LaTeX si está visible con límites de validación
|
|
||||||
if self.latex_panel_visible and self.secondary_splitter.count() > 1:
|
|
||||||
sizes = self.secondary_splitter.sizes()
|
|
||||||
if len(sizes) > 1:
|
|
||||||
latex_width = sizes[1]
|
|
||||||
|
|
||||||
# CORRECCIÓN: Validar y limitar el ancho antes de guardarlo
|
|
||||||
min_latex_width = 200
|
|
||||||
max_latex_width = 600
|
|
||||||
latex_width = max(min_latex_width, min(latex_width, max_latex_width))
|
|
||||||
|
|
||||||
self.settings_manager.set_setting("latex_panel_width", latex_width)
|
|
||||||
|
|
||||||
# Delegar al settings manager
|
# Delegar al settings manager
|
||||||
self.settings_manager.save_all()
|
self.settings_manager.save_all()
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
282
app/gui_menus.py
282
app/gui_menus.py
|
@ -1,11 +1,12 @@
|
||||||
"""
|
"""
|
||||||
Sistema de Menús y Diálogos para la Calculadora MAV CAS Híbrida
|
Sistema de Menús y Diálogos para la Calculadora MAV CAS Híbrida
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PySide6.QtWidgets import QMenuBar, QMenu, QMessageBox, QFileDialog
|
from PySide6.QtWidgets import (
|
||||||
|
QMenuBar, QMenu, QMessageBox, QFileDialog
|
||||||
|
)
|
||||||
from PySide6.QtGui import QAction, QKeySequence
|
from PySide6.QtGui import QAction, QKeySequence
|
||||||
from PySide6.QtCore import QTimer
|
from PySide6.QtCore import QTimer
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
|
@ -42,10 +43,6 @@ class MenuManager:
|
||||||
save_action.triggered.connect(self.save_file)
|
save_action.triggered.connect(self.save_file)
|
||||||
file_menu.addAction(save_action)
|
file_menu.addAction(save_action)
|
||||||
|
|
||||||
# Menú de archivos recientes
|
|
||||||
self.recent_files_menu = file_menu.addMenu("Archivos recientes")
|
|
||||||
self._update_recent_files_menu()
|
|
||||||
|
|
||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
|
|
||||||
exit_action = QAction("Salir", self.main_window)
|
exit_action = QAction("Salir", self.main_window)
|
||||||
|
@ -79,33 +76,6 @@ class MenuManager:
|
||||||
|
|
||||||
view_menu.addSeparator()
|
view_menu.addSeparator()
|
||||||
|
|
||||||
# Submenu de fuentes
|
|
||||||
font_menu = view_menu.addMenu("Tamaño de fuente")
|
|
||||||
|
|
||||||
font_small_action = QAction("Pequeña (11px)", self.main_window)
|
|
||||||
font_small_action.setCheckable(True)
|
|
||||||
font_small_action.triggered.connect(lambda: self.set_font_size(False))
|
|
||||||
font_menu.addAction(font_small_action)
|
|
||||||
|
|
||||||
font_large_action = QAction("Grande (14px)", self.main_window)
|
|
||||||
font_large_action.setCheckable(True)
|
|
||||||
font_large_action.triggered.connect(lambda: self.set_font_size(True))
|
|
||||||
font_menu.addAction(font_large_action)
|
|
||||||
|
|
||||||
# Marcar el tamaño actual
|
|
||||||
is_large = self.main_window.settings_manager.get_setting(
|
|
||||||
"font_size_large", False
|
|
||||||
)
|
|
||||||
if is_large:
|
|
||||||
font_large_action.setChecked(True)
|
|
||||||
else:
|
|
||||||
font_small_action.setChecked(True)
|
|
||||||
|
|
||||||
self.font_small_action = font_small_action
|
|
||||||
self.font_large_action = font_large_action
|
|
||||||
|
|
||||||
view_menu.addSeparator()
|
|
||||||
|
|
||||||
system_info_action = QAction("Información del sistema", self.main_window)
|
system_info_action = QAction("Información del sistema", self.main_window)
|
||||||
system_info_action.triggered.connect(self.show_types_info)
|
system_info_action.triggered.connect(self.show_types_info)
|
||||||
view_menu.addAction(system_info_action)
|
view_menu.addAction(system_info_action)
|
||||||
|
@ -130,10 +100,6 @@ class MenuManager:
|
||||||
latex_status_action.triggered.connect(self._show_latex_panel_status)
|
latex_status_action.triggered.connect(self._show_latex_panel_status)
|
||||||
diag_menu.addAction(latex_status_action)
|
diag_menu.addAction(latex_status_action)
|
||||||
|
|
||||||
save_html_action = QAction("💾 Guardar HTML del Panel LaTeX", self.main_window)
|
|
||||||
save_html_action.triggered.connect(self._save_latex_html)
|
|
||||||
diag_menu.addAction(save_html_action)
|
|
||||||
|
|
||||||
diag_menu.addSeparator()
|
diag_menu.addSeparator()
|
||||||
|
|
||||||
copy_debug_action = QAction("📋 Copiar Debug al Portapapeles", self.main_window)
|
copy_debug_action = QAction("📋 Copiar Debug al Portapapeles", self.main_window)
|
||||||
|
@ -182,50 +148,34 @@ class MenuManager:
|
||||||
self.clear_input()
|
self.clear_input()
|
||||||
self.clear_output()
|
self.clear_output()
|
||||||
self.main_window.latex_panel.clear_equations()
|
self.main_window.latex_panel.clear_equations()
|
||||||
if hasattr(self.main_window, "_latex_equations"):
|
if hasattr(self.main_window, '_latex_equations'):
|
||||||
self.main_window._latex_equations.clear()
|
self.main_window._latex_equations.clear()
|
||||||
self.main_window._update_status("✨ Nueva sesión iniciada")
|
self.main_window._update_status("✨ Nueva sesión iniciada")
|
||||||
|
|
||||||
def load_file(self):
|
def load_file(self):
|
||||||
"""Carga archivo en el editor"""
|
"""Carga archivo en el editor"""
|
||||||
filepath, _ = QFileDialog.getOpenFileName(
|
filepath, _ = QFileDialog.getOpenFileName(
|
||||||
self.main_window,
|
self.main_window, "Cargar archivo", "",
|
||||||
"Cargar archivo",
|
"Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)"
|
||||||
"",
|
|
||||||
"Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if filepath:
|
if filepath:
|
||||||
self._load_file_content(filepath)
|
|
||||||
|
|
||||||
def _load_file_content(self, filepath: str):
|
|
||||||
"""Carga el contenido de un archivo en el editor."""
|
|
||||||
try:
|
try:
|
||||||
with open(filepath, "r", encoding="utf-8") as f:
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
self.main_window.input_text.setPlainText(content)
|
self.main_window.input_text.setPlainText(content)
|
||||||
self.main_window._evaluate_and_update()
|
self.main_window._evaluate_and_update()
|
||||||
self.main_window._update_status(
|
self.main_window._update_status(f"📁 Archivo cargado: {Path(filepath).name}")
|
||||||
f"📁 Archivo cargado: {Path(filepath).name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Añadir a recientes y actualizar menú
|
|
||||||
self.main_window.settings_manager.add_recent_file(filepath)
|
|
||||||
self._update_recent_files_menu()
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(self.main_window, "Error", f"No se pudo cargar el archivo:\n{e}")
|
||||||
self.main_window, "Error", f"No se pudo cargar el archivo:\n{e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def save_file(self):
|
def save_file(self):
|
||||||
"""Guarda contenido del editor"""
|
"""Guarda contenido del editor"""
|
||||||
filepath, _ = QFileDialog.getSaveFileName(
|
filepath, _ = QFileDialog.getSaveFileName(
|
||||||
self.main_window,
|
self.main_window, "Guardar archivo", "",
|
||||||
"Guardar archivo",
|
"Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)"
|
||||||
"",
|
|
||||||
"Archivos de texto (*.txt);;Archivos Python (*.py);;Todos los archivos (*.*)",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if filepath:
|
if filepath:
|
||||||
|
@ -234,36 +184,10 @@ class MenuManager:
|
||||||
with open(filepath, "w", encoding="utf-8") as f:
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
self.main_window._update_status(
|
self.main_window._update_status(f"💾 Archivo guardado: {Path(filepath).name}")
|
||||||
f"💾 Archivo guardado: {Path(filepath).name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Añadir a recientes y actualizar menú
|
|
||||||
self.main_window.settings_manager.add_recent_file(filepath)
|
|
||||||
self._update_recent_files_menu()
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(self.main_window, "Error", f"No se pudo guardar el archivo:\n{e}")
|
||||||
self.main_window, "Error", f"No se pudo guardar el archivo:\n{e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _update_recent_files_menu(self):
|
|
||||||
"""Actualiza el menú de archivos recientes"""
|
|
||||||
self.recent_files_menu.clear()
|
|
||||||
recent_files = self.main_window.settings_manager.get_setting("recent_files", [])
|
|
||||||
|
|
||||||
if not recent_files:
|
|
||||||
empty_action = QAction("(Vacío)", self.main_window)
|
|
||||||
empty_action.setEnabled(False)
|
|
||||||
self.recent_files_menu.addAction(empty_action)
|
|
||||||
return
|
|
||||||
|
|
||||||
for filepath in recent_files:
|
|
||||||
action = QAction(filepath, self.main_window)
|
|
||||||
action.triggered.connect(
|
|
||||||
lambda checked=False, path=filepath: self._load_file_content(path)
|
|
||||||
)
|
|
||||||
self.recent_files_menu.addAction(action)
|
|
||||||
|
|
||||||
# ========== FUNCIONES DE MENÚ EDITAR ==========
|
# ========== FUNCIONES DE MENÚ EDITAR ==========
|
||||||
|
|
||||||
|
@ -276,7 +200,7 @@ class MenuManager:
|
||||||
"""Limpia panel de salida y LaTeX"""
|
"""Limpia panel de salida y LaTeX"""
|
||||||
self.main_window._clear_output()
|
self.main_window._clear_output()
|
||||||
self.main_window.latex_panel.clear_equations()
|
self.main_window.latex_panel.clear_equations()
|
||||||
if hasattr(self.main_window, "_latex_equations"):
|
if hasattr(self.main_window, '_latex_equations'):
|
||||||
self.main_window._latex_equations.clear()
|
self.main_window._latex_equations.clear()
|
||||||
|
|
||||||
def clear_history(self):
|
def clear_history(self):
|
||||||
|
@ -286,50 +210,7 @@ class MenuManager:
|
||||||
os.remove(self.main_window.HISTORY_FILE)
|
os.remove(self.main_window.HISTORY_FILE)
|
||||||
self.main_window._update_status("✓ Historial limpiado")
|
self.main_window._update_status("✓ Historial limpiado")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(self.main_window, "Error", f"No se pudo limpiar el historial:\n{e}")
|
||||||
self.main_window, "Error", f"No se pudo limpiar el historial:\n{e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_font_size(self, is_large: bool):
|
|
||||||
"""Cambia el tamaño de fuente de los paneles"""
|
|
||||||
# Guardar configuración
|
|
||||||
self.main_window.settings_manager.set_setting("font_size_large", is_large)
|
|
||||||
|
|
||||||
# Actualizar checkboxes
|
|
||||||
self.font_small_action.setChecked(not is_large)
|
|
||||||
self.font_large_action.setChecked(is_large)
|
|
||||||
|
|
||||||
# Aplicar nuevo tamaño
|
|
||||||
font_size = 14 if is_large else 11
|
|
||||||
self._apply_font_size_to_panels(font_size)
|
|
||||||
|
|
||||||
status_text = f"🔤 Fuente cambiada a {'grande' if is_large else 'pequeña'} ({font_size}px)"
|
|
||||||
self.main_window._update_status(status_text)
|
|
||||||
|
|
||||||
def _apply_font_size_to_panels(self, font_size: int):
|
|
||||||
"""Aplica el tamaño de fuente a los paneles de texto"""
|
|
||||||
style_update = f"""
|
|
||||||
QPlainTextEdit, QTextEdit {{
|
|
||||||
font-size: {font_size}px;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Actualizar estilo de los paneles principales
|
|
||||||
current_style = self.main_window.styleSheet()
|
|
||||||
# Reemplazar solo la parte de font-size en QPlainTextEdit y QTextEdit
|
|
||||||
import re
|
|
||||||
|
|
||||||
# Patrón para encontrar y reemplazar font-size en QPlainTextEdit y QTextEdit
|
|
||||||
pattern = r"(QPlainTextEdit, QTextEdit\s*\{[^}]*?)font-size:\s*\d+px;([^}]*\})"
|
|
||||||
replacement = rf"\1font-size: {font_size}px;\2"
|
|
||||||
|
|
||||||
new_style = re.sub(pattern, replacement, current_style)
|
|
||||||
|
|
||||||
# Si no se encontró el patrón, agregar el estilo
|
|
||||||
if new_style == current_style:
|
|
||||||
new_style += style_update
|
|
||||||
|
|
||||||
self.main_window.setStyleSheet(new_style)
|
|
||||||
|
|
||||||
# ========== FUNCIONES DE MENÚ HERRAMIENTAS ==========
|
# ========== FUNCIONES DE MENÚ HERRAMIENTAS ==========
|
||||||
|
|
||||||
|
@ -342,9 +223,7 @@ class MenuManager:
|
||||||
self.main_window._update_status("✓ Sistema de tipos recargado")
|
self.main_window._update_status("✓ Sistema de tipos recargado")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error recargando tipos: {e}")
|
self.logger.error(f"Error recargando tipos: {e}")
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(self.main_window, "Error", f"Error recargando tipos:\n{e}")
|
||||||
self.main_window, "Error", f"Error recargando tipos:\n{e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def show_types_info(self):
|
def show_types_info(self):
|
||||||
"""Muestra información sobre tipos disponibles"""
|
"""Muestra información sobre tipos disponibles"""
|
||||||
|
@ -368,9 +247,7 @@ CARACTERÍSTICAS:
|
||||||
self._show_info_dialog("Información del Sistema", info_text)
|
self._show_info_dialog("Información del Sistema", info_text)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(self.main_window, "Error", f"Error obteniendo información:\n{e}")
|
||||||
self.main_window, "Error", f"Error obteniendo información:\n{e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def show_types_syntax(self):
|
def show_types_syntax(self):
|
||||||
"""Muestra sintaxis de tipos disponibles"""
|
"""Muestra sintaxis de tipos disponibles"""
|
||||||
|
@ -384,37 +261,20 @@ CARACTERÍSTICAS:
|
||||||
self._show_info_dialog("Sintaxis de Tipos", syntax_text)
|
self._show_info_dialog("Sintaxis de Tipos", syntax_text)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(self.main_window, "Error", f"Error obteniendo sintaxis:\n{e}")
|
||||||
self.main_window, "Error", f"Error obteniendo sintaxis:\n{e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ========== FUNCIONES DE DIAGNÓSTICO ==========
|
# ========== FUNCIONES DE DIAGNÓSTICO ==========
|
||||||
|
|
||||||
def _diagnose_mathjax(self):
|
def _diagnose_mathjax(self):
|
||||||
"""Ejecuta diagnóstico de MathJax"""
|
"""Ejecuta diagnóstico de MathJax"""
|
||||||
if (
|
if not hasattr(self.main_window.latex_panel, '_webview_available') or not self.main_window.latex_panel._webview_available:
|
||||||
not hasattr(self.main_window.latex_panel, "_webview_available")
|
QMessageBox.warning(self.main_window, "Diagnóstico", "Panel LaTeX no usa WebEngine (usando fallback)")
|
||||||
or not self.main_window.latex_panel._webview_available
|
|
||||||
):
|
|
||||||
QMessageBox.warning(
|
|
||||||
self.main_window,
|
|
||||||
"Diagnóstico",
|
|
||||||
"Panel LaTeX no usa WebEngine (usando fallback)",
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Aquí iría el código de diagnóstico
|
# Aquí iría el código de diagnóstico
|
||||||
# Por ahora solo mostrar estado
|
# Por ahora solo mostrar estado
|
||||||
status = (
|
status = "WebEngine disponible" if self.main_window.latex_panel._webview_available else "Usando fallback HTML"
|
||||||
"WebEngine disponible"
|
equations = len(self.main_window._latex_equations) if hasattr(self.main_window, '_latex_equations') else 0
|
||||||
if self.main_window.latex_panel._webview_available
|
|
||||||
else "Usando fallback HTML"
|
|
||||||
)
|
|
||||||
equations = (
|
|
||||||
len(self.main_window._latex_equations)
|
|
||||||
if hasattr(self.main_window, "_latex_equations")
|
|
||||||
else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
info = f"""DIAGNÓSTICO MATHJAX
|
info = f"""DIAGNÓSTICO MATHJAX
|
||||||
|
|
||||||
|
@ -430,16 +290,10 @@ en el WebEngineView.
|
||||||
|
|
||||||
def _show_latex_panel_status(self):
|
def _show_latex_panel_status(self):
|
||||||
"""Muestra estado del panel LaTeX"""
|
"""Muestra estado del panel LaTeX"""
|
||||||
panel_exists = hasattr(self.main_window, "latex_panel")
|
panel_exists = hasattr(self.main_window, 'latex_panel')
|
||||||
panel_visible = self.main_window.latex_panel_visible if panel_exists else False
|
panel_visible = self.main_window.latex_panel_visible if panel_exists else False
|
||||||
webview_available = (
|
webview_available = self.main_window.latex_panel._webview_available if panel_exists else False
|
||||||
self.main_window.latex_panel._webview_available if panel_exists else False
|
equations_count = len(self.main_window._latex_equations) if hasattr(self.main_window, '_latex_equations') else 0
|
||||||
)
|
|
||||||
equations_count = (
|
|
||||||
len(self.main_window._latex_equations)
|
|
||||||
if hasattr(self.main_window, "_latex_equations")
|
|
||||||
else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
status_message = f"""ESTADO DEL PANEL LATEX
|
status_message = f"""ESTADO DEL PANEL LATEX
|
||||||
|
|
||||||
|
@ -460,73 +314,6 @@ PARA SOLUCIONAR:
|
||||||
|
|
||||||
self._show_info_dialog("Estado Panel LaTeX", status_message)
|
self._show_info_dialog("Estado Panel LaTeX", status_message)
|
||||||
|
|
||||||
def _save_latex_html(self):
|
|
||||||
"""Guarda el HTML del panel LaTeX para diagnóstico"""
|
|
||||||
try:
|
|
||||||
# Obtener HTML del panel LaTeX
|
|
||||||
latex_panel = self.main_window.latex_panel
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasattr(latex_panel, "_webview_available")
|
|
||||||
and latex_panel._webview_available
|
|
||||||
):
|
|
||||||
# Panel con WebEngine
|
|
||||||
if hasattr(latex_panel, "webview") and latex_panel._webview_initialized:
|
|
||||||
# Obtener HTML actual del WebView
|
|
||||||
def on_html_received(html_content):
|
|
||||||
self._save_html_to_file(html_content, "webview_current")
|
|
||||||
|
|
||||||
latex_panel.webview.page().toHtml(on_html_received)
|
|
||||||
|
|
||||||
# También guardar el HTML base generado
|
|
||||||
base_html = latex_panel._generate_mathjax_html()
|
|
||||||
self._save_html_to_file(base_html, "webview_base")
|
|
||||||
else:
|
|
||||||
# WebEngine no inicializado, solo HTML base
|
|
||||||
base_html = latex_panel._generate_mathjax_html()
|
|
||||||
self._save_html_to_file(base_html, "webview_not_initialized")
|
|
||||||
else:
|
|
||||||
# Panel con fallback text browser
|
|
||||||
if hasattr(latex_panel, "text_browser"):
|
|
||||||
fallback_html = latex_panel.text_browser.toHtml()
|
|
||||||
self._save_html_to_file(fallback_html, "fallback_textbrowser")
|
|
||||||
else:
|
|
||||||
self._save_html_to_file(
|
|
||||||
"No hay contenido HTML disponible", "no_content"
|
|
||||||
)
|
|
||||||
|
|
||||||
QMessageBox.information(
|
|
||||||
self.main_window,
|
|
||||||
"HTML Guardado",
|
|
||||||
"El HTML del panel LaTeX ha sido guardado en ./debug_html/ para diagnóstico.",
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Error guardando HTML del panel LaTeX: {e}")
|
|
||||||
QMessageBox.critical(
|
|
||||||
self.main_window, "Error", f"Error guardando HTML: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _save_html_to_file(self, html_content, file_suffix):
|
|
||||||
"""Guarda contenido HTML a un archivo"""
|
|
||||||
# Crear directorio de debug si no existe
|
|
||||||
debug_dir = Path("./debug_html")
|
|
||||||
debug_dir.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
# Generar nombre de archivo con timestamp
|
|
||||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
||||||
filename = f"latex_panel_{file_suffix}_{timestamp}.html"
|
|
||||||
filepath = debug_dir / filename
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(filepath, "w", encoding="utf-8") as f:
|
|
||||||
f.write(html_content)
|
|
||||||
|
|
||||||
self.logger.info(f"HTML guardado en: {filepath}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Error escribiendo archivo HTML: {e}")
|
|
||||||
|
|
||||||
def _copy_debug_to_clipboard(self):
|
def _copy_debug_to_clipboard(self):
|
||||||
"""Copia información de debug completa al portapapeles"""
|
"""Copia información de debug completa al portapapeles"""
|
||||||
try:
|
try:
|
||||||
|
@ -541,16 +328,11 @@ PARA SOLUCIONAR:
|
||||||
|
|
||||||
# Obtener ecuaciones LaTeX si están disponibles
|
# Obtener ecuaciones LaTeX si están disponibles
|
||||||
latex_equations = ""
|
latex_equations = ""
|
||||||
if (
|
if hasattr(self.main_window, '_latex_equations') and self.main_window._latex_equations:
|
||||||
hasattr(self.main_window, "_latex_equations")
|
latex_equations = "\\n".join([
|
||||||
and self.main_window._latex_equations
|
|
||||||
):
|
|
||||||
latex_equations = "\\n".join(
|
|
||||||
[
|
|
||||||
f"[{eq['type']}] {eq['content']}"
|
f"[{eq['type']}] {eq['content']}"
|
||||||
for eq in self.main_window._latex_equations
|
for eq in self.main_window._latex_equations
|
||||||
]
|
])
|
||||||
)
|
|
||||||
|
|
||||||
# Crear reporte de debug completo
|
# Crear reporte de debug completo
|
||||||
debug_report = f"""=== REPORTE DEBUG CALCULADORA MAV ===
|
debug_report = f"""=== REPORTE DEBUG CALCULADORA MAV ===
|
||||||
|
@ -583,17 +365,11 @@ Panel LaTeX visible: {self.main_window.latex_panel_visible}
|
||||||
clipboard.setText(debug_report)
|
clipboard.setText(debug_report)
|
||||||
|
|
||||||
# Mostrar confirmación
|
# Mostrar confirmación
|
||||||
self.main_window._update_status(
|
self.main_window._update_status("📋 Información de debug copiada al portapapeles", 3000)
|
||||||
"📋 Información de debug copiada al portapapeles", 3000
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error copiando debug: {e}")
|
self.logger.error(f"Error copiando debug: {e}")
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(self.main_window, "Error", f"Error copiando debug al portapapeles:\\n{e}")
|
||||||
self.main_window,
|
|
||||||
"Error",
|
|
||||||
f"Error copiando debug al portapapeles:\\n{e}",
|
|
||||||
)
|
|
||||||
|
|
||||||
# ========== FUNCIONES DE MENÚ AYUDA ==========
|
# ========== FUNCIONES DE MENÚ AYUDA ==========
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,8 @@ from PySide6.QtGui import QFont, QTextCursor, QTextCharFormat, QColor
|
||||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||||
import sympy
|
import sympy
|
||||||
from typing import Any, Optional, Dict, List, Tuple
|
from typing import Any, Optional, Dict, List, Tuple
|
||||||
try:
|
import matplotlib.pyplot as plt
|
||||||
import matplotlib.pyplot as plt
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
|
||||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
|
|
||||||
MATPLOTLIB_AVAILABLE = True
|
|
||||||
except ImportError:
|
|
||||||
print("⚠️ Matplotlib no disponible - plots no funcionarán")
|
|
||||||
MATPLOTLIB_AVAILABLE = False
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
@ -132,8 +127,11 @@ class InteractiveResultManager(QWidget):
|
||||||
def handle_interactive_click(self, result: Any, is_mathjax_click: bool = False):
|
def handle_interactive_click(self, result: Any, is_mathjax_click: bool = False):
|
||||||
"""Maneja clicks en elementos interactivos"""
|
"""Maneja clicks en elementos interactivos"""
|
||||||
if isinstance(result, PlotResult):
|
if isinstance(result, PlotResult):
|
||||||
# Abrir ventana de plot directamente
|
if not is_mathjax_click:
|
||||||
print(f"🖱️ Abriendo ventana de plot para: {result.plot_type}")
|
# Primera vez: mostrar en MathJax
|
||||||
|
self.plot_requested.emit(result)
|
||||||
|
else:
|
||||||
|
# Click en MathJax: abrir ventana emergente
|
||||||
self._show_plot_window(result)
|
self._show_plot_window(result)
|
||||||
else:
|
else:
|
||||||
# Otros tipos siempre abren ventana
|
# Otros tipos siempre abren ventana
|
||||||
|
@ -305,18 +303,7 @@ class InteractiveResultManager(QWidget):
|
||||||
if not parent_frame.layout():
|
if not parent_frame.layout():
|
||||||
parent_frame.setLayout(QVBoxLayout())
|
parent_frame.setLayout(QVBoxLayout())
|
||||||
|
|
||||||
if not MATPLOTLIB_AVAILABLE:
|
|
||||||
error_label = QLabel("Matplotlib no está disponible.\nInstala con: pip install matplotlib")
|
|
||||||
error_label.setStyleSheet("color: #f44747; font-size: 12px;")
|
|
||||||
error_label.setWordWrap(True)
|
|
||||||
parent_frame.layout().addWidget(error_label)
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"📊 Creando plot {plot_result.plot_type} con args: {plot_result.args}")
|
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(8, 6))
|
fig, ax = plt.subplots(figsize=(8, 6))
|
||||||
fig.patch.set_facecolor('#1a1a1a') # Fondo oscuro
|
|
||||||
ax.set_facecolor('#1a1a1a')
|
|
||||||
|
|
||||||
if plot_result.plot_type == "plot":
|
if plot_result.plot_type == "plot":
|
||||||
self._create_2d_plot(fig, ax, plot_result.args, plot_result.kwargs)
|
self._create_2d_plot(fig, ax, plot_result.args, plot_result.kwargs)
|
||||||
|
@ -327,13 +314,7 @@ class InteractiveResultManager(QWidget):
|
||||||
canvas = FigureCanvasQTAgg(fig)
|
canvas = FigureCanvasQTAgg(fig)
|
||||||
parent_frame.layout().addWidget(canvas)
|
parent_frame.layout().addWidget(canvas)
|
||||||
|
|
||||||
print("✅ Plot creado exitosamente")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error creando plot: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
if not parent_frame.layout():
|
if not parent_frame.layout():
|
||||||
parent_frame.setLayout(QVBoxLayout())
|
parent_frame.setLayout(QVBoxLayout())
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""
|
"""
|
||||||
Sistema de Configuración y Persistencia para la Calculadora MAV CAS Híbrida
|
Sistema de Configuración y Persistencia para la Calculadora MAV CAS Híbrida
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
@ -32,10 +31,7 @@ class SettingsManager:
|
||||||
"window_geometry": None,
|
"window_geometry": None,
|
||||||
"splitter_sizes": None,
|
"splitter_sizes": None,
|
||||||
"debug_mode": False,
|
"debug_mode": False,
|
||||||
"latex_panel_visible": False,
|
"latex_panel_visible": False
|
||||||
"latex_panel_width": 300,
|
|
||||||
"font_size_large": False, # False = pequeña (11px), True = grande (14px)
|
|
||||||
"recent_files": [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _save_settings(self):
|
def _save_settings(self):
|
||||||
|
@ -50,15 +46,15 @@ class SettingsManager:
|
||||||
"x": geometry.x(),
|
"x": geometry.x(),
|
||||||
"y": geometry.y(),
|
"y": geometry.y(),
|
||||||
"width": geometry.width(),
|
"width": geometry.width(),
|
||||||
"height": geometry.height(),
|
"height": geometry.height()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Guardar tamaños del splitter
|
# Guardar tamaños del splitter
|
||||||
if hasattr(self.main_window, "main_splitter"):
|
if hasattr(self.main_window, 'main_splitter'):
|
||||||
self.settings["splitter_sizes"] = self.main_window.main_splitter.sizes()
|
self.settings["splitter_sizes"] = self.main_window.main_splitter.sizes()
|
||||||
|
|
||||||
self.settings["latex_panel_visible"] = self.main_window.latex_panel_visible
|
self.settings["latex_panel_visible"] = self.main_window.latex_panel_visible
|
||||||
self.settings["debug_mode"] = getattr(self.main_window, "debug", False)
|
self.settings["debug_mode"] = getattr(self.main_window, 'debug', False)
|
||||||
|
|
||||||
with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f:
|
with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f:
|
||||||
json.dump(self.settings, f, indent=4, ensure_ascii=False)
|
json.dump(self.settings, f, indent=4, ensure_ascii=False)
|
||||||
|
@ -99,13 +95,11 @@ class SettingsManager:
|
||||||
try:
|
try:
|
||||||
geom = self.settings.get("window_geometry")
|
geom = self.settings.get("window_geometry")
|
||||||
if geom and isinstance(geom, dict):
|
if geom and isinstance(geom, dict):
|
||||||
self.main_window.setGeometry(
|
self.main_window.setGeometry(geom["x"], geom["y"], geom["width"], geom["height"])
|
||||||
geom["x"], geom["y"], geom["width"], geom["height"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Restaurar splitter
|
# Restaurar splitter
|
||||||
sizes = self.settings.get("splitter_sizes")
|
sizes = self.settings.get("splitter_sizes")
|
||||||
if sizes and hasattr(self.main_window, "main_splitter"):
|
if sizes and hasattr(self.main_window, 'main_splitter'):
|
||||||
self.main_window.main_splitter.setSizes(sizes)
|
self.main_window.main_splitter.setSizes(sizes)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -128,21 +122,3 @@ class SettingsManager:
|
||||||
def set_setting(self, key: str, value):
|
def set_setting(self, key: str, value):
|
||||||
"""Establece un valor de configuración"""
|
"""Establece un valor de configuración"""
|
||||||
self.settings[key] = value
|
self.settings[key] = value
|
||||||
|
|
||||||
def add_recent_file(self, filepath: str):
|
|
||||||
"""Añade un archivo a la lista de recientes y guarda."""
|
|
||||||
try:
|
|
||||||
recent_files = self.get_setting("recent_files", [])
|
|
||||||
|
|
||||||
# Si el archivo ya está, lo eliminamos para volver a añadirlo al principio
|
|
||||||
if filepath in recent_files:
|
|
||||||
recent_files.remove(filepath)
|
|
||||||
|
|
||||||
# Añadir al principio de la lista
|
|
||||||
recent_files.insert(0, filepath)
|
|
||||||
|
|
||||||
# Limitar la lista a 10 elementos
|
|
||||||
self.set_setting("recent_files", recent_files[:10])
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Error añadiendo archivo reciente: {e}")
|
|
||||||
|
|
|
@ -41,7 +41,6 @@ class OutputTextEdit(QTextEdit):
|
||||||
self.setReadOnly(True)
|
self.setReadOnly(True)
|
||||||
self.setFont(QFont("Consolas", 11))
|
self.setFont(QFont("Consolas", 11))
|
||||||
self.clickable_links = {} # {(start, end): (link_id, object)}
|
self.clickable_links = {} # {(start, end): (link_id, object)}
|
||||||
self.setMouseTracking(True) # Habilitar tracking del mouse
|
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
"""Detecta clicks en links"""
|
"""Detecta clicks en links"""
|
||||||
|
@ -49,38 +48,14 @@ class OutputTextEdit(QTextEdit):
|
||||||
cursor = self.cursorForPosition(event.pos())
|
cursor = self.cursorForPosition(event.pos())
|
||||||
pos = cursor.position()
|
pos = cursor.position()
|
||||||
|
|
||||||
logging.debug(f"🖱️ Click en posición {pos}, links disponibles: {len(self.clickable_links)}")
|
|
||||||
|
|
||||||
# Buscar si el click fue en un link
|
# Buscar si el click fue en un link
|
||||||
for (start, end), (link_id, obj) in self.clickable_links.items():
|
for (start, end), (link_id, obj) in self.clickable_links.items():
|
||||||
logging.debug(f"🔍 Verificando link {start}-{end}: {link_id}")
|
|
||||||
if start <= pos <= end:
|
if start <= pos <= end:
|
||||||
logging.debug(f"✅ Click en link detectado: {link_id}")
|
|
||||||
self.link_clicked.emit(link_id, obj)
|
self.link_clicked.emit(link_id, obj)
|
||||||
return
|
return
|
||||||
|
|
||||||
super().mousePressEvent(event)
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
|
||||||
"""Cambia el cursor cuando está sobre un link"""
|
|
||||||
cursor = self.cursorForPosition(event.pos())
|
|
||||||
pos = cursor.position()
|
|
||||||
|
|
||||||
# Verificar si el mouse está sobre un link
|
|
||||||
over_link = False
|
|
||||||
for (start, end), (link_id, obj) in self.clickable_links.items():
|
|
||||||
if start <= pos <= end:
|
|
||||||
over_link = True
|
|
||||||
break
|
|
||||||
|
|
||||||
# Cambiar cursor
|
|
||||||
if over_link:
|
|
||||||
self.setCursor(Qt.PointingHandCursor)
|
|
||||||
else:
|
|
||||||
self.setCursor(Qt.ArrowCursor)
|
|
||||||
|
|
||||||
super().mouseMoveEvent(event)
|
|
||||||
|
|
||||||
|
|
||||||
class ExpandableLatexButton(QPushButton):
|
class ExpandableLatexButton(QPushButton):
|
||||||
"""Botón expandible para mostrar/ocultar panel LaTeX"""
|
"""Botón expandible para mostrar/ocultar panel LaTeX"""
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
"""
|
|
||||||
Sistema de gestión de MathJax local con descarga automática
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import zipfile
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from urllib.request import urlretrieve
|
|
||||||
from urllib.error import URLError
|
|
||||||
|
|
||||||
|
|
||||||
class MathJaxManager:
|
|
||||||
"""Gestor de MathJax local con descarga automática"""
|
|
||||||
|
|
||||||
MATHJAX_VERSION = "3.2.2"
|
|
||||||
MATHJAX_URL = f"https://github.com/mathjax/MathJax/archive/{MATHJAX_VERSION}.zip"
|
|
||||||
LOCAL_DIR = Path(".mathjax")
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.logger = logging.getLogger(__name__)
|
|
||||||
self.mathjax_dir = self.LOCAL_DIR / f"MathJax-{self.MATHJAX_VERSION}"
|
|
||||||
self.es5_dir = self.mathjax_dir / "es5"
|
|
||||||
|
|
||||||
def get_mathjax_url(self) -> str:
|
|
||||||
"""
|
|
||||||
Obtiene la URL de MathJax (local si está disponible, CDN si no)
|
|
||||||
"""
|
|
||||||
if self.is_local_available():
|
|
||||||
# Construir URL local relativa para file://
|
|
||||||
local_url = f"file:///{self.es5_dir.absolute().as_posix()}/tex-svg.js"
|
|
||||||
self.logger.debug(f"Usando MathJax local: {local_url}")
|
|
||||||
return local_url
|
|
||||||
else:
|
|
||||||
# Descargar automáticamente si no existe
|
|
||||||
if self.download_mathjax():
|
|
||||||
local_url = f"file:///{self.es5_dir.absolute().as_posix()}/tex-svg.js"
|
|
||||||
self.logger.info(f"MathJax descargado y configurado localmente: {local_url}")
|
|
||||||
return local_url
|
|
||||||
else:
|
|
||||||
# Fallback a CDN
|
|
||||||
cdn_url = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"
|
|
||||||
self.logger.warning(f"Fallback a CDN: {cdn_url}")
|
|
||||||
return cdn_url
|
|
||||||
|
|
||||||
def is_local_available(self) -> bool:
|
|
||||||
"""Verifica si MathJax está disponible localmente"""
|
|
||||||
tex_svg_file = self.es5_dir / "tex-svg.js"
|
|
||||||
return tex_svg_file.exists() and tex_svg_file.is_file()
|
|
||||||
|
|
||||||
def download_mathjax(self) -> bool:
|
|
||||||
"""
|
|
||||||
Descarga MathJax automáticamente si no está disponible
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True si la descarga fue exitosa, False si falló
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.logger.info(f"Descargando MathJax {self.MATHJAX_VERSION}...")
|
|
||||||
|
|
||||||
# Crear directorio si no existe
|
|
||||||
self.LOCAL_DIR.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
# Archivo temporal para la descarga
|
|
||||||
zip_path = self.LOCAL_DIR / f"mathjax-{self.MATHJAX_VERSION}.zip"
|
|
||||||
|
|
||||||
# Descargar archivo ZIP
|
|
||||||
self.logger.debug(f"Descargando desde: {self.MATHJAX_URL}")
|
|
||||||
urlretrieve(self.MATHJAX_URL, zip_path)
|
|
||||||
|
|
||||||
# Extraer archivo
|
|
||||||
self.logger.debug(f"Extrayendo en: {self.LOCAL_DIR}")
|
|
||||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
|
||||||
zip_ref.extractall(self.LOCAL_DIR)
|
|
||||||
|
|
||||||
# Limpiar archivo ZIP
|
|
||||||
zip_path.unlink()
|
|
||||||
|
|
||||||
# Verificar que se extrajo correctamente
|
|
||||||
if self.is_local_available():
|
|
||||||
self.logger.info(f"✅ MathJax {self.MATHJAX_VERSION} descargado correctamente")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.logger.error("❌ Error: MathJax no se extrajo correctamente")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except URLError as e:
|
|
||||||
self.logger.error(f"❌ Error de red descargando MathJax: {e}")
|
|
||||||
return False
|
|
||||||
except zipfile.BadZipFile as e:
|
|
||||||
self.logger.error(f"❌ Error: archivo ZIP corrupto: {e}")
|
|
||||||
# Limpiar archivo corrupto
|
|
||||||
if zip_path.exists():
|
|
||||||
zip_path.unlink()
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"❌ Error inesperado descargando MathJax: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def clean_local_mathjax(self):
|
|
||||||
"""
|
|
||||||
Limpia la instalación local de MathJax
|
|
||||||
(Para forzar redownload en la siguiente inicialización)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if self.LOCAL_DIR.exists():
|
|
||||||
import shutil
|
|
||||||
shutil.rmtree(self.LOCAL_DIR)
|
|
||||||
self.logger.info("🗑️ MathJax local limpiado")
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Error limpiando MathJax local: {e}")
|
|
||||||
|
|
||||||
def get_status_info(self) -> dict:
|
|
||||||
"""Obtiene información del estado de MathJax"""
|
|
||||||
return {
|
|
||||||
"local_available": self.is_local_available(),
|
|
||||||
"local_dir": str(self.LOCAL_DIR.absolute()),
|
|
||||||
"mathjax_dir": str(self.mathjax_dir.absolute()) if self.mathjax_dir.exists() else None,
|
|
||||||
"version": self.MATHJAX_VERSION,
|
|
||||||
"tex_svg_file": str(self.es5_dir / "tex-svg.js") if self.is_local_available() else None
|
|
||||||
}
|
|
|
@ -1,377 +0,0 @@
|
||||||
"""
|
|
||||||
Módulo de manejo de fechas para el sistema de tipos
|
|
||||||
Permite crear y manipular fechas con aritmética de días y semanas, soporte para
|
|
||||||
operaciones simbólicas a través de SymPy, y utilidades comunes como cálculo de
|
|
||||||
fines de semana, días hábiles y diferencias entre fechas.
|
|
||||||
|
|
||||||
Ejemplo de uso:
|
|
||||||
>>> d = Date("10/5/25") # 10 de mayo de 2025
|
|
||||||
>>> d + 7 # 17/5/25
|
|
||||||
>>> d.day_of_week() # 'Sábado'
|
|
||||||
>>> d.is_weekend() # True
|
|
||||||
>>> d.next_business_day() # 12/5/25
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from typing import List, Tuple, Optional, Union
|
|
||||||
import sympy
|
|
||||||
|
|
||||||
from app.class_base import (
|
|
||||||
ClassBase,
|
|
||||||
) # noqa: F401 (aún no se usa, pero se importa para coherencia)
|
|
||||||
from app.sympy_Base import SympyClassBase
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"Class_Date",
|
|
||||||
"Date", # alias de conveniencia
|
|
||||||
"Today", # nueva función constructora
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_date_str(date_str: str) -> datetime.date:
|
|
||||||
"""Convierte una cadena a objeto ``datetime.date``.
|
|
||||||
|
|
||||||
Acepta los formatos comunes:
|
|
||||||
- DD/MM/YY (se asume 2000-2099 para YY < 70 y 1900-1999 para el resto)
|
|
||||||
- DD/MM/YYYY
|
|
||||||
- YYYY-MM-DD (ISO-8601)
|
|
||||||
|
|
||||||
Asimismo se admiten separadores «/», «-» o «.».
|
|
||||||
"""
|
|
||||||
date_str = date_str.strip()
|
|
||||||
|
|
||||||
# Intentar ISO-8601 directamente
|
|
||||||
try:
|
|
||||||
return datetime.date.fromisoformat(date_str)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Reemplazar posibles separadores por «/» para simplificar
|
|
||||||
normalised = date_str.replace("-", "/").replace(".", "/")
|
|
||||||
parts = normalised.split("/")
|
|
||||||
if len(parts) != 3:
|
|
||||||
raise ValueError(f"Formato de fecha desconocido: '{date_str}'")
|
|
||||||
|
|
||||||
# Detectar si el primer campo es año (ISO) o día
|
|
||||||
if len(parts[0]) == 4: # AAAA/MM/DD
|
|
||||||
year = int(parts[0])
|
|
||||||
month = int(parts[1])
|
|
||||||
day = int(parts[2])
|
|
||||||
else: # DD/MM/AA o DD/MM/YYYY
|
|
||||||
day = int(parts[0])
|
|
||||||
month = int(parts[1])
|
|
||||||
year_part = parts[2]
|
|
||||||
if len(year_part) == 2:
|
|
||||||
yy = int(year_part)
|
|
||||||
year = 2000 + yy if yy < 70 else 1900 + yy
|
|
||||||
else:
|
|
||||||
year = int(year_part)
|
|
||||||
return datetime.date(year, month, day)
|
|
||||||
|
|
||||||
|
|
||||||
class Class_Date(SympyClassBase):
|
|
||||||
"""Clase de fecha con aritmética de días y semanas.
|
|
||||||
|
|
||||||
Internamente la fecha se almacena como el ordinal devuelto por
|
|
||||||
:py:meth:`datetime.date.toordinal`, lo que facilita la aritmética con
|
|
||||||
números enteros (p.ej. añadir días).
|
|
||||||
"""
|
|
||||||
|
|
||||||
_op_priority = 20 # preferencia alta en operaciones de SymPy
|
|
||||||
|
|
||||||
def __init__(self, value: Union[str, datetime.date, int, sympy.Expr]):
|
|
||||||
# Manejar expresiones SymPy (p. ej. símbolos o enteros simbólicos)
|
|
||||||
if isinstance(value, sympy.Expr) and not isinstance(value, sympy.Integer):
|
|
||||||
# Modo simbólico puro (no intentamos resolver a fecha real)
|
|
||||||
self._ordinal_expr = value
|
|
||||||
self._has_symbols = True
|
|
||||||
date_repr = str(value)
|
|
||||||
else:
|
|
||||||
# Convertir todos los casos a «datetime.date» y ordinal entero
|
|
||||||
if isinstance(value, datetime.date):
|
|
||||||
date_obj = value
|
|
||||||
elif isinstance(value, str):
|
|
||||||
date_obj = _parse_date_str(value)
|
|
||||||
elif isinstance(value, (int, sympy.Integer)):
|
|
||||||
# Se interpreta como ordinal absoluto
|
|
||||||
date_obj = datetime.date.fromordinal(int(value))
|
|
||||||
else:
|
|
||||||
raise TypeError(
|
|
||||||
"value debe ser str, datetime.date o entero (ordinal), "
|
|
||||||
f"recibido: {type(value)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._ordinal = date_obj.toordinal()
|
|
||||||
self._has_symbols = False
|
|
||||||
self._date = date_obj
|
|
||||||
self._ordinal_expr = sympy.Integer(self._ordinal)
|
|
||||||
date_repr = self._date.isoformat()
|
|
||||||
|
|
||||||
super().__init__(self._ordinal_expr, date_repr)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Representación
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def __repr__(self): # noqa: D401
|
|
||||||
if self._has_symbols:
|
|
||||||
return f"Date({sympy.sstr(self._ordinal_expr)})"
|
|
||||||
return f"Date('{self.__str__()}')"
|
|
||||||
|
|
||||||
def __str__(self): # noqa: D401
|
|
||||||
if self._has_symbols:
|
|
||||||
return f"Date({sympy.sstr(self._ordinal_expr)})"
|
|
||||||
return self._date.strftime("%d/%m/%y")
|
|
||||||
|
|
||||||
def _sympystr(self, printer): # pragma: no cover
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Conversión
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def to_datetime(self) -> datetime.date:
|
|
||||||
"""Devuelve la fecha como objeto ``datetime.date``."""
|
|
||||||
if self._has_symbols:
|
|
||||||
raise ValueError(
|
|
||||||
"La fecha es simbólica y no se puede convertir a 'datetime.date'."
|
|
||||||
)
|
|
||||||
return self._date
|
|
||||||
|
|
||||||
def to_ordinal(self) -> int:
|
|
||||||
"""Devuelve el ordinal (días desde 01-01-0001)."""
|
|
||||||
return int(self._ordinal_expr)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Aritmética
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def __add__(self, other):
|
|
||||||
"""Añadir días o expresiones simbólicas: ``fecha + n`` o ``fecha + símbolo``."""
|
|
||||||
# Soporte para expresiones SymPy (símbolos, enteros simbólicos, etc.)
|
|
||||||
if isinstance(other, sympy.Expr):
|
|
||||||
new_expr = self._ordinal_expr + other
|
|
||||||
return Class_Date(new_expr)
|
|
||||||
if hasattr(other, "__int__"):
|
|
||||||
return self.add_days(int(other))
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
__radd__ = __add__
|
|
||||||
|
|
||||||
def __sub__(self, other):
|
|
||||||
"""Resta días o devuelve diferencia: ``fecha - n``, ``fecha - símbolo``, o diferencia entre fechas."""
|
|
||||||
if isinstance(other, sympy.Expr):
|
|
||||||
# Restar expresión simbólica -> nueva fecha simbólica
|
|
||||||
new_expr = self._ordinal_expr - other
|
|
||||||
return Class_Date(new_expr)
|
|
||||||
if hasattr(other, "__int__"):
|
|
||||||
return self.add_days(-int(other))
|
|
||||||
if isinstance(other, Class_Date):
|
|
||||||
# Diferencia (puede ser simbólica)
|
|
||||||
return self._ordinal_expr - other._ordinal_expr
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def add_days(self, n: int) -> "Class_Date":
|
|
||||||
"""Devuelve una nueva fecha desplazada *n* días."""
|
|
||||||
if self._has_symbols:
|
|
||||||
new_expr = self._ordinal_expr + n
|
|
||||||
return Class_Date(new_expr)
|
|
||||||
else:
|
|
||||||
new_date = self._date + datetime.timedelta(days=n)
|
|
||||||
return Class_Date(new_date)
|
|
||||||
|
|
||||||
def add_weeks(self, n: int) -> "Class_Date":
|
|
||||||
"""Devuelve una nueva fecha desplazada *n* semanas."""
|
|
||||||
return self.add_days(n * 7)
|
|
||||||
|
|
||||||
def next_day(self) -> "Class_Date":
|
|
||||||
return self.add_days(1)
|
|
||||||
|
|
||||||
def previous_day(self) -> "Class_Date":
|
|
||||||
return self.add_days(-1)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Información de calendario
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
_WEEKDAY_NAMES_ES = [
|
|
||||||
"Lunes",
|
|
||||||
"Martes",
|
|
||||||
"Miércoles",
|
|
||||||
"Jueves",
|
|
||||||
"Viernes",
|
|
||||||
"Sábado",
|
|
||||||
"Domingo",
|
|
||||||
]
|
|
||||||
|
|
||||||
def day_of_week(self) -> str:
|
|
||||||
"""Nombre del día de la semana en español (Lunes-Domingo)."""
|
|
||||||
if self._has_symbols:
|
|
||||||
raise ValueError(
|
|
||||||
"La fecha es simbólica; no se puede determinar el día de la semana."
|
|
||||||
)
|
|
||||||
weekday = self._date.weekday() # 0 = lunes
|
|
||||||
return self._WEEKDAY_NAMES_ES[weekday]
|
|
||||||
|
|
||||||
def is_weekend(self) -> bool:
|
|
||||||
if self._has_symbols:
|
|
||||||
raise ValueError(
|
|
||||||
"La fecha es simbólica; no se puede determinar si es fin de semana."
|
|
||||||
)
|
|
||||||
return self._date.weekday() >= 5
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Días hábiles
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def is_business_day(self, holidays: Optional[List[datetime.date]] = None) -> bool:
|
|
||||||
"""Indica si es día laborable (L-V) y no festivo.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
holidays: lista opcional de festivos como ``datetime.date``.
|
|
||||||
"""
|
|
||||||
if self._has_symbols:
|
|
||||||
raise ValueError(
|
|
||||||
"La fecha es simbólica; no se puede evaluar si es laborable."
|
|
||||||
)
|
|
||||||
if self.is_weekend():
|
|
||||||
return False
|
|
||||||
if holidays and self._date in holidays:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def next_business_day(
|
|
||||||
self, holidays: Optional[List[datetime.date]] = None
|
|
||||||
) -> "Class_Date":
|
|
||||||
"""Devuelve el siguiente día laborable."""
|
|
||||||
candidate = self.next_day()
|
|
||||||
while not candidate.is_business_day(holidays):
|
|
||||||
candidate = candidate.next_day()
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
def business_days_between(
|
|
||||||
self, other: "Class_Date", holidays: Optional[List[datetime.date]] = None
|
|
||||||
) -> int:
|
|
||||||
"""Cuenta días laborables entre dos fechas (excluye la inicial, incluye la final si procede)."""
|
|
||||||
if not isinstance(other, Class_Date):
|
|
||||||
raise TypeError("other debe ser Class_Date")
|
|
||||||
start_ord = min(self.to_ordinal(), other.to_ordinal()) + 1
|
|
||||||
end_ord = max(self.to_ordinal(), other.to_ordinal())
|
|
||||||
count = 0
|
|
||||||
for ord_val in range(start_ord, end_ord + 1):
|
|
||||||
d = Class_Date(ord_val)
|
|
||||||
if d.is_business_day(holidays):
|
|
||||||
count += 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Diferencias
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def days_between(self, other: "Class_Date") -> int:
|
|
||||||
"""Número absoluto de días entre dos fechas."""
|
|
||||||
if not isinstance(other, Class_Date):
|
|
||||||
raise TypeError("other debe ser Class_Date")
|
|
||||||
return abs(self - other)
|
|
||||||
|
|
||||||
def weeks_between(self, other: "Class_Date") -> float:
|
|
||||||
"""Número de semanas (con decimales) entre dos fechas."""
|
|
||||||
return self.days_between(other) / 7
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Comparaciones y ecuaciones simbólicas
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def __eq__(self, other): # type: ignore[override]
|
|
||||||
"""Devuelve una ecuación SymPy en vez de comparar booleanamente cuando se usa con objetos Date o expresiones SymPy."""
|
|
||||||
if isinstance(other, Class_Date):
|
|
||||||
return sympy.Eq(self._ordinal_expr, other._ordinal_expr)
|
|
||||||
if isinstance(other, sympy.Expr):
|
|
||||||
return sympy.Eq(self._ordinal_expr, other)
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Helper para autocompletado & paneles GUI
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def Helper(input_str): # pragma: no cover
|
|
||||||
if input_str.strip().lower().startswith("date("):
|
|
||||||
return (
|
|
||||||
"Ej: date('10/5/25'), date('2025-05-10')\n" # noqa: E501
|
|
||||||
"Funciones: add_days(), add_weeks(), next_business_day(), day_of_week(), is_weekend(), is_business_day(), days_between()…"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def PopupFunctionList(): # pragma: no cover
|
|
||||||
return [
|
|
||||||
("add_days", "Suma N días"),
|
|
||||||
("add_weeks", "Suma N semanas"),
|
|
||||||
("next_day", "Día siguiente"),
|
|
||||||
("previous_day", "Día anterior"),
|
|
||||||
("next_business_day", "Siguiente día hábil"),
|
|
||||||
("is_business_day", "¿Es día laborable?"),
|
|
||||||
("day_of_week", "Nombre del día de la semana"),
|
|
||||||
("is_weekend", "¿Es fin de semana?"),
|
|
||||||
("days_between", "Días entre dos fechas"),
|
|
||||||
("weeks_between", "Semanas entre dos fechas"),
|
|
||||||
("to_datetime", "Convertir a datetime.date"),
|
|
||||||
("to_ordinal", "Ordinal (nº de días)"),
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def today(cls) -> "Class_Date":
|
|
||||||
"""Fecha actual (según *datetime.date.today()*)."""
|
|
||||||
return cls(datetime.date.today())
|
|
||||||
|
|
||||||
# Alias interno por consistencia con el resto del sistema
|
|
||||||
Today = today
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Alias de usuario final — ajustado al sistema de tokenización existente
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def Date(value: Union[str, datetime.date, int, sympy.Expr]): # noqa: N802
|
|
||||||
"""Alias que imita la sintaxis ``date('…')`` empleada por el usuario."""
|
|
||||||
return Class_Date(value)
|
|
||||||
|
|
||||||
|
|
||||||
def Today(): # noqa: N802
|
|
||||||
"""Construye un objeto Date con la fecha actual (hoy)."""
|
|
||||||
return Class_Date.today()
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Registro para el sistema de tipos dinámico
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def register_classes_in_module(): # pragma: no cover
|
|
||||||
"""Devuelve la lista de clases para el registro automático."""
|
|
||||||
return [
|
|
||||||
(
|
|
||||||
"Date",
|
|
||||||
Class_Date,
|
|
||||||
"SympyClassBase",
|
|
||||||
{
|
|
||||||
"add_lowercase": True, # permite usar ``date`` minúscula
|
|
||||||
"supports_brackets": False,
|
|
||||||
"description": "Fechas con aritmética y utilidades de calendario",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Today",
|
|
||||||
Today,
|
|
||||||
"function",
|
|
||||||
{
|
|
||||||
"add_lowercase": True,
|
|
||||||
"supports_brackets": False,
|
|
||||||
"description": "Fecha actual (hoy)",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,153 +0,0 @@
|
||||||
"""
|
|
||||||
Clase para símbolos SymPy - Tokenización automática de corchetes
|
|
||||||
Convierte [texto] automáticamente a symbols('texto')
|
|
||||||
|
|
||||||
EJEMPLOS DE USO:
|
|
||||||
k = 100 * [Hz] → k = 100 * symbols('Hz')
|
|
||||||
E = [m] * [c]**2 → E = symbols('m') * symbols('c')**2
|
|
||||||
f = [omega] / (2*[pi]) → f = symbols('omega') / (2*symbols('pi'))
|
|
||||||
|
|
||||||
CARACTERÍSTICAS:
|
|
||||||
- Tokenización automática de [nombre] a symbols('nombre')
|
|
||||||
- Integración completa con SymPy para álgebra simbólica
|
|
||||||
- Soporte para operaciones matemáticas completas
|
|
||||||
- Prioridad alta en el sistema de tokenización
|
|
||||||
"""
|
|
||||||
from app.sympy_Base import SympyClassBase
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class SymPySymbol(SympyClassBase):
|
|
||||||
"""
|
|
||||||
Wrapper para símbolos de SymPy con tokenización automática de corchetes
|
|
||||||
|
|
||||||
Esta clase permite usar la sintaxis [nombre] como shortcut para symbols('nombre'),
|
|
||||||
mejorando significativamente la experiencia de usuario para trabajo algebraico.
|
|
||||||
|
|
||||||
Uso: [Hz] se convierte automáticamente a symbols('Hz')
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, symbol_name):
|
|
||||||
"""Inicialización del símbolo SymPy"""
|
|
||||||
from sympy import symbols
|
|
||||||
|
|
||||||
if isinstance(symbol_name, str):
|
|
||||||
self.symbol_name = symbol_name
|
|
||||||
self.symbol = symbols(symbol_name)
|
|
||||||
else:
|
|
||||||
raise TypeError(f"SymPySymbol requiere un string, recibido: {type(symbol_name)}")
|
|
||||||
|
|
||||||
# Llamar al constructor base
|
|
||||||
super().__init__(self.symbol, str(self.symbol))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
"""Representación string"""
|
|
||||||
return str(self.symbol)
|
|
||||||
|
|
||||||
def _sympystr(self, printer):
|
|
||||||
"""Representación SymPy string"""
|
|
||||||
return str(self.symbol)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"Symbol({self.symbol_name})"
|
|
||||||
|
|
||||||
# ========== DELEGACIÓN DE OPERADORES A SYMPY ==========
|
|
||||||
|
|
||||||
def __add__(self, other):
|
|
||||||
"""Suma delegada al símbolo SymPy"""
|
|
||||||
return self.symbol + other
|
|
||||||
|
|
||||||
def __radd__(self, other):
|
|
||||||
return other + self.symbol
|
|
||||||
|
|
||||||
def __sub__(self, other):
|
|
||||||
"""Resta delegada al símbolo SymPy"""
|
|
||||||
return self.symbol - other
|
|
||||||
|
|
||||||
def __rsub__(self, other):
|
|
||||||
return other - self.symbol
|
|
||||||
|
|
||||||
def __mul__(self, other):
|
|
||||||
"""Multiplicación delegada al símbolo SymPy"""
|
|
||||||
return self.symbol * other
|
|
||||||
|
|
||||||
def __rmul__(self, other):
|
|
||||||
return other * self.symbol
|
|
||||||
|
|
||||||
def __truediv__(self, other):
|
|
||||||
"""División delegada al símbolo SymPy"""
|
|
||||||
return self.symbol / other
|
|
||||||
|
|
||||||
def __rtruediv__(self, other):
|
|
||||||
return other / self.symbol
|
|
||||||
|
|
||||||
def __pow__(self, other):
|
|
||||||
"""Potencia delegada al símbolo SymPy"""
|
|
||||||
return self.symbol ** other
|
|
||||||
|
|
||||||
def __rpow__(self, other):
|
|
||||||
return other ** self.symbol
|
|
||||||
|
|
||||||
# ========== MÉTODOS ESTÁTICOS ==========
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def Helper(input_str):
|
|
||||||
"""Ayuda contextual para símbolos"""
|
|
||||||
if re.search(r'\[.*\]', input_str):
|
|
||||||
return ('Uso: [Hz] se convierte automáticamente a symbols("Hz")\n'
|
|
||||||
'Para unidades físicas, frecuencias, variables simbólicas, etc.\n'
|
|
||||||
'Ejemplos: [x], [omega], [pi], [Hz], [m], [c]')
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def PopupFunctionList():
|
|
||||||
"""Lista de métodos sugeridos para símbolos"""
|
|
||||||
return [
|
|
||||||
("subs", "Sustituir valores: symbol.subs(x, valor)"),
|
|
||||||
("expand", "Expandir expresión"),
|
|
||||||
("simplify", "Simplificar expresión"),
|
|
||||||
("factor", "Factorizar expresión"),
|
|
||||||
("solve", "Resolver ecuaciones"),
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_tokenization_patterns():
|
|
||||||
"""
|
|
||||||
Define la regla de tokenización principal: [texto] → symbols('texto')
|
|
||||||
|
|
||||||
PATRÓN: \[([a-zA-Z_][a-zA-Z0-9_]*)\]
|
|
||||||
- Captura texto entre corchetes que empiece con letra o _
|
|
||||||
- Permite números después del primer carácter
|
|
||||||
- Convierte a llamada symbols() automáticamente
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict]: Lista de reglas de tokenización
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'pattern': r'\[([a-zA-Z_][a-zA-Z0-9_]*)\]',
|
|
||||||
'replacement': lambda match: f'symbols("{match.group(1)}")',
|
|
||||||
'priority': 10, # ALTA prioridad - muy específico y común
|
|
||||||
'description': 'Símbolos con corchetes: [Hz], [x], [omega] → symbols("Hz"), etc.'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# ========== FUNCIÓN DE REGISTRO ==========
|
|
||||||
|
|
||||||
def register_classes_in_module():
|
|
||||||
"""
|
|
||||||
Registra la clase SymPySymbol en el sistema de tipos
|
|
||||||
|
|
||||||
Configuración:
|
|
||||||
- add_lowercase: True (permite sympysymbol() además de SymPySymbol())
|
|
||||||
- supports_brackets: False (los corchetes se manejan vía tokenización)
|
|
||||||
- Integración automática con el sistema de tokenización distribuida
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
("SymPySymbol", SymPySymbol, "SympyClassBase", {
|
|
||||||
"add_lowercase": True,
|
|
||||||
"supports_brackets": False, # Los corchetes se manejan vía tokenización
|
|
||||||
"description": "Símbolos SymPy con tokenización automática de corchetes [nombre]"
|
|
||||||
}),
|
|
||||||
]
|
|
|
@ -1,218 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
|
||||||
<script>
|
|
||||||
// Intentar cargar MathJax local primero, luego CDN como fallback
|
|
||||||
window.mathjaxLoadAttempts = 0;
|
|
||||||
window.mathJaxReady = false;
|
|
||||||
|
|
||||||
function loadMathJax(url, isFallback = false) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.id = 'MathJax-script';
|
|
||||||
script.async = true;
|
|
||||||
script.src = url;
|
|
||||||
|
|
||||||
script.onload = () => {
|
|
||||||
console.log('MathJax cargado desde:', url);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
script.onerror = () => {
|
|
||||||
console.error('Error cargando MathJax desde:', url);
|
|
||||||
reject(new Error('MathJax load failed'));
|
|
||||||
};
|
|
||||||
|
|
||||||
document.head.appendChild(script);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuración de MathJax
|
|
||||||
window.MathJax = {
|
|
||||||
tex: {
|
|
||||||
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
|
||||||
displayMath: [['$$', '$$'], ['\\[', '\\]']],
|
|
||||||
processEscapes: true
|
|
||||||
},
|
|
||||||
svg: {
|
|
||||||
scale: 0.9,
|
|
||||||
minScale: 0.5,
|
|
||||||
fontCache: 'global'
|
|
||||||
},
|
|
||||||
startup: {
|
|
||||||
ready: function () {
|
|
||||||
MathJax.startup.defaultReady();
|
|
||||||
window.mathJaxReady = true;
|
|
||||||
console.log('MathJax SVG completamente listo');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Intentar cargar MathJax local, luego CDN
|
|
||||||
loadMathJax('file:///D:/Proyectos/Scripts/Calcv2/.mathjax/MathJax-3.2.2/es5/tex-svg.js').catch(() => {
|
|
||||||
console.log('Fallback a CDN de MathJax...');
|
|
||||||
return loadMathJax('https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js', true);
|
|
||||||
}).catch(() => {
|
|
||||||
console.error('No se pudo cargar MathJax desde ninguna fuente');
|
|
||||||
// Marcar como listo para mostrar contenido sin renderizado
|
|
||||||
setTimeout(() => {
|
|
||||||
window.mathJaxReady = true;
|
|
||||||
console.log('Modo fallback activado - sin renderizado LaTeX');
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
color: #d4d4d4;
|
|
||||||
font-family: 'Segoe UI', Arial, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
.equation-block {
|
|
||||||
background: rgba(45, 45, 48, 0.8);
|
|
||||||
border-left: 3px solid;
|
|
||||||
margin: 4px 0;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.1s ease;
|
|
||||||
}
|
|
||||||
.equation-block:hover {
|
|
||||||
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; }
|
|
||||||
|
|
||||||
.math-content {
|
|
||||||
margin: 2px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-text {
|
|
||||||
font-style: italic;
|
|
||||||
color: #6a9955;
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-message {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optimizaciones para SVG rendering */
|
|
||||||
mjx-container[jax="SVG"] {
|
|
||||||
margin: 2px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
mjx-container[jax="SVG"] svg {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
mjx-container[jax="SVG"] svg text {
|
|
||||||
fill: #d4d4d4 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="equations-container">
|
|
||||||
<div class="info-message">
|
|
||||||
Panel de Ecuaciones LaTeX (SVG)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function addEquation(type, content) {
|
|
||||||
try {
|
|
||||||
var container = document.getElementById('equations-container');
|
|
||||||
if (!container) {
|
|
||||||
console.error('Container no encontrado');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 mathContent = document.createElement('div');
|
|
||||||
mathContent.className = 'math-content';
|
|
||||||
|
|
||||||
if (type === 'comment') {
|
|
||||||
mathContent.className = 'comment-text';
|
|
||||||
mathContent.textContent = content;
|
|
||||||
console.log('Comentario agregado:', content);
|
|
||||||
} else {
|
|
||||||
mathContent.innerHTML = '$$' + content + '$$';
|
|
||||||
console.log('Ecuación agregada:', content);
|
|
||||||
}
|
|
||||||
|
|
||||||
equation.appendChild(mathContent);
|
|
||||||
container.appendChild(equation);
|
|
||||||
|
|
||||||
// Re-renderizar MathJax solo si está listo
|
|
||||||
if (window.MathJax && window.mathJaxReady && type !== 'comment') {
|
|
||||||
console.log('Renderizando con MathJax...');
|
|
||||||
MathJax.typesetPromise([equation]).then(() => {
|
|
||||||
console.log('Ecuación renderizada exitosamente');
|
|
||||||
}).catch(function(err) {
|
|
||||||
console.error('Error renderizando con MathJax:', err);
|
|
||||||
// Mostrar como texto plano si falla
|
|
||||||
mathContent.innerHTML = content;
|
|
||||||
});
|
|
||||||
} else if (type !== 'comment') {
|
|
||||||
console.log('MathJax no listo, mostrando como texto:', window.mathJaxReady);
|
|
||||||
// Mostrar como texto plano si MathJax no está listo
|
|
||||||
mathContent.innerHTML = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error en addEquation:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearEquations() {
|
|
||||||
try {
|
|
||||||
var container = document.getElementById('equations-container');
|
|
||||||
if (container) {
|
|
||||||
container.innerHTML = '<div class="info-message">Panel de Ecuaciones LaTeX (SVG)</div>';
|
|
||||||
console.log('Ecuaciones limpiadas');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error en clearEquations:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Función para notificar que está listo para renderizar
|
|
||||||
function triggerInitialRender() {
|
|
||||||
try {
|
|
||||||
console.log('Verificando estado MathJax:', window.mathJaxReady);
|
|
||||||
if (window.mathJaxReady) {
|
|
||||||
console.log('MathJax SVG listo para renderizado inicial');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error en triggerInitialRender:', e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monitor de estado para debug
|
|
||||||
setInterval(() => {
|
|
||||||
if (window.mathJaxReady) {
|
|
||||||
console.log('🎯 MathJax listo detectado');
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue