Calc/main_calc_app.py

1578 lines
62 KiB
Python

"""
Calculadora MAV CAS Híbrida - Aplicación principal
VERSIÓN ADAPTADA AL NUEVO SISTEMA DE TIPOS
"""
import tkinter as tk
from tkinter import scrolledtext, messagebox, Menu, filedialog
import tkinter.font as tkFont
import json
import os
from pathlib import Path
import threading
from typing import List, Dict, Any, Optional
import re
# ========== IMPORTS PARA SISTEMA DE AYUDA ==========
# Para la ayuda en HTML
MARKDOWN_AVAILABLE = False
HTML_VIEWER_TYPE = None
try:
import markdown
MARKDOWN_AVAILABLE = True
except ImportError:
# markdown not available, MARKDOWN_AVAILABLE remains False
pass
# Intentar importar visores HTML
try:
import tkinterweb
HTML_VIEWER_TYPE = "tkinterweb"
except ImportError:
try:
from tkhtmlview import HTMLScrolledText
HTML_VIEWER_TYPE = "tkhtmlview"
except ImportError:
HTML_VIEWER_TYPE = None
if not MARKDOWN_AVAILABLE:
print("Advertencia: La librería 'markdown' no está instalada. La ayuda se mostrará en texto plano.")
if MARKDOWN_AVAILABLE and HTML_VIEWER_TYPE is None:
print("Advertencia: 'markdown' está disponible, pero no se encontró un visor HTML (tkinterweb/tkhtmlview). La ayuda se mostrará en texto plano.")
# ========== IMPORTS ADAPTADOS AL NUEVO SISTEMA ==========
# Importar componentes del CAS híbrido con nuevo sistema de tipos
from main_evaluation import HybridEvaluationEngine, EvaluationResult
from tl_popup import InteractiveResultManager, PlotResult
from type_registry import get_registered_helper_functions, get_registered_base_context
import sympy
from sympy_helper import SympyTools as SympyHelper
class HybridCalculatorApp:
"""Aplicación principal del CAS híbrido - ADAPTADA AL NUEVO SISTEMA"""
SETTINGS_FILE = "hybrid_calc_settings.json"
HISTORY_FILE = "hybrid_calc_history.txt"
HELP_FILE = "readme.md" # ========== NUEVO: Archivo de ayuda externo ==========
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("Calculadora MAV - CAS Híbrido")
# Configuración y estado
self.settings = self._load_settings()
self.root.geometry(self.settings.get("window_geometry", "1000x700"))
self.root.configure(bg="#2b2b2b")
# Configurar motor con configuraciones cargadas
self.engine = HybridEvaluationEngine(auto_discover_types=True, types_directory="custom_types")
self._apply_symbolic_settings() # NUEVO: Aplicar configuraciones simbólicas
# Debug desde configuración
self.debug = self.settings.get("debug", False)
self.engine.debug = self.debug
# Autocompletado
self.autocomplete_popup = None
self.current_suggestions = []
# Configurar ícono
self._setup_icon()
# ========== COMPONENTES PRINCIPALES CON NUEVO SISTEMA ==========
self.interactive_manager = None # Se inicializa después de crear widgets
# ========== HELPERS DINÁMICOS DEL REGISTRO ==========
self._setup_dynamic_helpers()
# Estado de la aplicación
self._debounce_job = None
self._syncing_yview = False
self._cached_input_font = None
self.output_buffer = []
# ========== BARRA DE ESTADO ==========
self.status_frame = tk.Frame(self.root, bg="#2b2b2b", height=25)
self.status_frame.pack(side=tk.BOTTOM, fill=tk.X)
self.status_frame.pack_propagate(False)
self.status_label = tk.Label(
self.status_frame,
text=self._get_status_text(),
bg="#2b2b2b",
fg="#80c7f7",
font=("Consolas", 9),
anchor=tk.W
)
self.status_label.pack(side=tk.LEFT, padx=10, pady=2)
# ========== PANEL PRINCIPAL ==========
self.create_widgets()
self.setup_interactive_manager()
self.load_history()
# Configurar eventos de cierre
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
def _setup_dynamic_helpers(self):
"""Configura helpers dinámicamente desde el registro de tipos"""
try:
# Obtener helpers registrados dinámicamente
self.HELPERS = get_registered_helper_functions()
# Añadir SympyHelper al final
self.HELPERS.append(SympyHelper.Helper)
print(f"🆘 Helpers dinámicos cargados: {len(self.HELPERS)}")
except Exception as e:
print(f"⚠️ Error cargando helpers dinámicos: {e}")
# Fallback a helpers básicos
self.HELPERS = [SympyHelper.Helper]
def reload_types(self):
"""Recarga el sistema de tipos (útil para desarrollo)"""
try:
print("🔄 Recargando sistema de tipos...")
# Recargar engine
self.engine.reload_types()
# Recargar helpers
self._setup_dynamic_helpers()
# Re-evaluar contenido actual
self._evaluate_and_update()
print("✅ Sistema de tipos recargado")
except Exception as e:
print(f"❌ Error recargando tipos: {e}")
messagebox.showerror("Error", f"Error recargando tipos:\n{e}")
def show_types_info(self):
"""Muestra información sobre tipos disponibles"""
try:
types_info = self.engine.get_available_types()
info_text = f"""INFORMACIÓN DEL SISTEMA DE TIPOS
Clases registradas: {len(types_info.get('registered_classes', {}))}
Clases con sintaxis de corchetes: {len(types_info.get('bracket_classes', []))}
Entradas en contexto: {types_info.get('total_context_entries', 0)}
Helper functions: {types_info.get('helper_functions_count', 0)}
CLASES DISPONIBLES:
"""
for name, cls in types_info.get('registered_classes', {}).items():
info_text += f"{name}: {cls.__name__}\n"
info_text += f"\nCLASES CON SINTAXIS DE CORCHETES:\n"
for name in types_info.get('bracket_classes', []):
info_text += f"{name}[...]\n"
# Mostrar en ventana
self._show_help_window("Información de Tipos", info_text)
except Exception as e:
messagebox.showerror("Error", f"Error obteniendo información de tipos:\n{e}")
def _setup_icon(self):
"""Configura el ícono de la aplicación"""
try:
script_dir = Path(__file__).resolve().parent
icon_path = script_dir / "icon.png"
if not icon_path.is_file():
print(f"Advertencia: Archivo de ícono no encontrado en '{icon_path}'.")
return
self.app_icon = tk.PhotoImage(file=str(icon_path))
self.root.iconphoto(True, self.app_icon)
except tk.TclError as e:
print(f"Advertencia: No se pudo cargar el ícono desde '{icon_path}'. Error de Tkinter: {e}")
except Exception as e:
print(f"Advertencia: Ocurrió un error inesperado al cargar el ícono desde '{icon_path}': {e}")
def _load_settings(self) -> Dict[str, Any]:
"""Carga configuración de la aplicación"""
if os.path.exists(self.SETTINGS_FILE):
try:
with open(self.SETTINGS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except (IOError, json.JSONDecodeError):
return {}
return {}
def _save_settings(self):
"""Guarda configuraciones en archivo JSON"""
try:
# Obtener geometría actual
self.settings["window_geometry"] = self.root.geometry()
# Guardar posición del panel divisor si existe
if hasattr(self, 'paned_window'):
sash_pos = self.paned_window.sash_coord(0)[0]
self.settings["sash_pos_x"] = sash_pos
with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f:
json.dump(self.settings, f, indent=4, ensure_ascii=False)
except Exception as e:
if self.debug:
print(f"Error guardando configuración: {e}")
def update_symbolic_settings(self, symbolic_mode=None, show_numeric=None,
keep_fractions=None, auto_simplify=None):
"""Actualiza configuraciones simbólicas y las guarda"""
if symbolic_mode is not None:
self.settings["symbolic_mode"] = symbolic_mode
if show_numeric is not None:
self.settings["show_numeric_approximation"] = show_numeric
if keep_fractions is not None:
self.settings["keep_symbolic_fractions"] = keep_fractions
if auto_simplify is not None:
self.settings["auto_simplify"] = auto_simplify
# Aplicar al motor
self._apply_symbolic_settings()
# Actualizar barra de estado
if hasattr(self, 'status_label'):
self.status_label.config(text=self._get_status_text())
# Guardar configuraciones
self._save_settings()
def create_widgets(self):
"""Crea la interfaz gráfica"""
# Frame principal
main_frame = tk.Frame(self.root, bg="#2b2b2b", bd=0)
main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
# Panel dividido
self.paned_window = tk.PanedWindow(
main_frame, orient=tk.HORIZONTAL, bg="#2b2b2b",
sashrelief=tk.FLAT, sashwidth=4, bd=0,
showhandle=False, opaqueresize=True,
)
self.paned_window.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
# Panel de entrada
initial_input_width = self.settings.get("sash_pos_x", 450)
self.input_text = scrolledtext.ScrolledText(
self.paned_window,
font=("Consolas", 11),
bg="#1e1e1e",
fg="#d4d4d4",
insertbackground="#ffffff",
selectbackground="#264f78",
undo=True,
wrap=tk.NONE,
borderwidth=0,
highlightthickness=0,
relief=tk.FLAT,
)
self.paned_window.add(
self.input_text,
width=initial_input_width,
stretch="always",
minsize=200
)
# Panel de salida
self.output_text = scrolledtext.ScrolledText(
self.paned_window,
font=("Consolas", 11),
bg="#0f0f0f",
fg="#00ff00",
state="disabled",
wrap=tk.NONE,
borderwidth=0,
highlightthickness=0,
relief=tk.FLAT,
)
self.paned_window.add(
self.output_text,
stretch="always",
minsize=200
)
# Configurar eventos
self.input_text.bind("<KeyRelease>", self.on_key_release)
self.input_text.bind("<Button-3>", lambda e: self._show_context_menu(e, "input"))
self.output_text.bind("<Button-3>", lambda e: self._show_context_menu(e, "output"))
# Configurar scroll sincronizado
self.setup_scroll_sync()
# Configurar tags de salida
self.setup_output_tags()
# Crear menú
self.create_menu()
def setup_interactive_manager(self):
"""Configura el gestor de resultados interactivos"""
self.interactive_manager = InteractiveResultManager(self.root)
def create_menu(self):
"""Crea el menú de la aplicación"""
menubar = Menu(self.root)
self.root.config(menu=menubar)
# Menú Archivo
file_menu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Archivo", menu=file_menu)
file_menu.add_command(label="Nuevo", command=self.new_session)
file_menu.add_separator()
file_menu.add_command(label="Cargar...", command=self.load_file)
file_menu.add_command(label="Guardar como...", command=self.save_file)
file_menu.add_separator()
file_menu.add_command(label="Salir", command=self.on_close)
# Menú Editar
edit_menu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Editar", menu=edit_menu)
edit_menu.add_command(label="Limpiar entrada", command=self.clear_input)
edit_menu.add_command(label="Limpiar salida", command=self.clear_output)
edit_menu.add_separator()
edit_menu.add_command(label="Limpiar variables", command=self.clear_variables)
edit_menu.add_command(label="Limpiar ecuaciones", command=self.clear_equations)
edit_menu.add_command(label="Limpiar todo", command=self.clear_all)
# Menú CAS
cas_menu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="CAS", menu=cas_menu)
cas_menu.add_command(label="Mostrar variables", command=self.show_variables)
cas_menu.add_command(label="Mostrar ecuaciones", command=self.show_equations)
cas_menu.add_separator()
cas_menu.add_command(label="Resolver sistema", command=self.solve_system)
# Menú Configuración
config_menu = Menu(menubar, tearoff=0, bg="#3c3c3c", fg="white")
menubar.add_cascade(label="Configuración", menu=config_menu)
# Variables para checkbuttons
self.symbolic_mode_var = tk.BooleanVar(value=self.settings.get("symbolic_mode", True))
self.show_numeric_var = tk.BooleanVar(value=self.settings.get("show_numeric_approximation", True))
self.keep_fractions_var = tk.BooleanVar(value=self.settings.get("keep_symbolic_fractions", True))
# Modo simbólico
config_menu.add_checkbutton(
label="Modo Simbólico",
variable=self.symbolic_mode_var,
command=self.toggle_symbolic_mode
)
config_menu.add_checkbutton(
label="Mostrar Aproximación Numérica",
variable=self.show_numeric_var,
command=self.toggle_numeric_approximation
)
config_menu.add_checkbutton(
label="Mantener Fracciones Simbólicas",
variable=self.keep_fractions_var,
command=self.toggle_symbolic_fractions
)
config_menu.add_separator()
config_menu.add_command(label="Recargar Tipos Personalizados", command=self.reload_types)
# ========== MENÚ TIPOS (NUEVO) ==========
types_menu = Menu(menubar, tearoff=0, bg="#3c3c3c", fg="white")
menubar.add_cascade(label="Tipos", menu=types_menu)
types_menu.add_command(label="Información de tipos", command=self.show_types_info)
types_menu.add_separator()
types_menu.add_command(label="Sintaxis de tipos", command=self.show_types_syntax)
# Menú Ayuda (actualizado)
help_menu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Ayuda", menu=help_menu)
help_menu.add_command(label="Guía rápida", command=self.show_quick_guide)
help_menu.add_command(label="Sintaxis", command=self.show_syntax_help)
help_menu.add_command(label="Funciones SymPy", command=self.show_sympy_functions)
help_menu.add_separator()
help_menu.add_command(label="Acerca de", command=self.show_about)
def setup_scroll_sync(self):
"""Configura scroll sincronizado entre paneles"""
def _yscroll_input_command(*args):
self.input_text.vbar.set(*args)
if not self._syncing_yview:
self._syncing_yview = True
self.output_text.yview_moveto(args[0])
self._syncing_yview = False
def _yscroll_output_command(*args):
self.output_text.vbar.set(*args)
if not self._syncing_yview:
self._syncing_yview = True
self.input_text.yview_moveto(args[0])
self._syncing_yview = False
def _unified_mouse_wheel(event):
if self._syncing_yview:
return "break"
if hasattr(event, "widget") and event.widget:
event.widget.yview_scroll(int(-1 * (event.delta / 120)), "units")
return "break"
self.input_text.config(yscrollcommand=_yscroll_input_command)
self.output_text.config(yscrollcommand=_yscroll_output_command)
self.input_text.bind("<MouseWheel>", _unified_mouse_wheel)
self.output_text.bind("<MouseWheel>", _unified_mouse_wheel)
def setup_output_tags(self):
"""Configura tags para coloreado de salida"""
self.output_text.tag_configure("error", foreground="#ff6b6b", font=("Consolas", 11, "bold"))
self.output_text.tag_configure("result", foreground="#abdbe3")
self.output_text.tag_configure("symbolic", foreground="#82aaff")
self.output_text.tag_configure("numeric", foreground="#c3e88d")
self.output_text.tag_configure("equation", foreground="#c792ea")
self.output_text.tag_configure("info", foreground="#ffcb6b")
self.output_text.tag_configure("comment", foreground="#546e7a")
self.output_text.tag_configure("class_hint", foreground="#888888")
self.output_text.tag_configure("type_hint", foreground="#6a6a6a")
# Tags para tipos especializados (genéricos para cualquier tipo)
self.output_text.tag_configure("custom_type", foreground="#f9a825")
self.output_text.tag_configure("hex", foreground="#f9a825")
self.output_text.tag_configure("bin", foreground="#4fc3f7")
self.output_text.tag_configure("ip", foreground="#fff176")
self.output_text.tag_configure("date", foreground="#ff8a80")
self.output_text.tag_configure("chr_type", foreground="#80cbc4")
self.output_text.tag_configure("helper", foreground="#ffd700", font=("Consolas", 11, "italic"))
def on_key_release(self, event=None):
"""Maneja eventos de teclado"""
if self._debounce_job:
self.root.after_cancel(self._debounce_job)
# Autocompletado con punto (usando contexto dinámico)
if event and event.char == '.' and self.input_text.focus_get() == self.input_text:
self._handle_dot_autocomplete()
# Evaluación con debounce
self._debounce_job = self.root.after(300, self._evaluate_and_update)
def _handle_dot_autocomplete(self):
"""Maneja el autocompletado cuando se escribe un punto - VERSIÓN DINÁMICA"""
self._close_autocomplete_popup()
cursor_index_str = self.input_text.index(tk.INSERT)
line_num_str, char_num_str = cursor_index_str.split('.')
current_line_num = int(line_num_str)
char_idx_after_dot = int(char_num_str)
if char_idx_after_dot == 0:
print("DEBUG: Autocomplete: Cursor at beginning of line after dot. No action.")
return
dot_char_index_in_line = char_idx_after_dot - 1
text_on_line_up_to_dot = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{dot_char_index_in_line}")
stripped_text_before_dot = text_on_line_up_to_dot.strip()
# 1. Determinar si es un popup GLOBAL (usando contexto dinámico)
if not stripped_text_before_dot:
print("DEBUG: Dot on empty line or after spaces. Offering global suggestions.")
suggestions = []
# ========== USAR CONTEXTO DINÁMICO DEL REGISTRO ==========
try:
dynamic_context = get_registered_base_context()
for name, class_or_func in dynamic_context.items():
if name[0].isupper(): # Prioritizar nombres capitalizados
hint = f"Tipo o función: {name}"
if hasattr(class_or_func, '__doc__') and class_or_func.__doc__:
first_line_doc = class_or_func.__doc__.strip().split('\n')[0]
hint = f"{name} - {first_line_doc}"
elif hasattr(class_or_func, 'Helper'):
try:
helper_text = class_or_func.Helper(name)
if helper_text:
hint = helper_text.split('\n')[0]
except Exception as e_helper:
print(f"DEBUG: Error calling Helper for {name}: {e_helper}")
pass
suggestions.append((name, hint))
except Exception as e:
print(f"DEBUG: Error obteniendo contexto dinámico: {e}")
# Fallback básico
suggestions = [("sin", "Función seno"), ("cos", "Función coseno")]
# Añadir funciones de SympyHelper
try:
sympy_functions = SympyHelper.PopupFunctionList()
if sympy_functions:
current_suggestion_names = {s[0] for s in suggestions}
for fname, fhint in sympy_functions:
if fname not in current_suggestion_names:
suggestions.append((fname, fhint))
except Exception as e:
print(f"DEBUG: Error calling SympyHelper.PopupFunctionList() for global: {e}")
if suggestions:
suggestions.sort(key=lambda x: x[0])
self._show_autocomplete_popup(suggestions, is_global_popup=True)
return
# 2. Es un popup de OBJETO
obj_expr_str_candidate = ""
obj_expr_regex = r"([a-zA-Z_][a-zA-Z0-9_]*(?:\[[^\]]*\])?(?:(?:\s*\.\s*[a-zA-Z_][a-zA-Z0-9_]*)(?:\[[^\]]*\])?)*)$"
match = re.search(obj_expr_regex, stripped_text_before_dot)
if match:
obj_expr_str_candidate = match.group(1).replace(" ", "")
else:
obj_expr_str_candidate = stripped_text_before_dot
if not obj_expr_str_candidate or \
not re.match(r"^[a-zA-Z_0-9\(\)\[\]\.\"\'\+\-\*\/ ]*$", obj_expr_str_candidate) or \
obj_expr_str_candidate.endswith(("+", "-", "*", "/", "(", ",")):
print(f"DEBUG: Extracted expr '{obj_expr_str_candidate}' from '{stripped_text_before_dot}' not a valid object for dot autocomplete.")
return
obj_expr_str = obj_expr_str_candidate
print(f"DEBUG: Autocomplete for object. Extracted: '{obj_expr_str}' from: '{text_on_line_up_to_dot}'")
if not obj_expr_str:
print("DEBUG: Object expression is empty after extraction. No autocomplete.")
return
# 3. Caso especial para el módulo sympy
if obj_expr_str == "sympy":
print(f"DEBUG: Detected 'sympy.', using SympyHelper for suggestions.")
try:
methods = SympyHelper.PopupFunctionList()
if methods:
self._show_autocomplete_popup(methods, is_global_popup=False)
else:
print(f"DEBUG: SympyHelper.PopupFunctionList returned no methods.")
except Exception as e:
print(f"DEBUG: Error calling SympyHelper.PopupFunctionList(): {e}")
return
# 4. Preprocesar con BracketParser
if '[' in obj_expr_str:
original_for_debug = obj_expr_str
obj_expr_str = self.engine.parser._transform_brackets(obj_expr_str)
if obj_expr_str != original_for_debug:
print(f"DEBUG: Preprocessed by BracketParser: '{original_for_debug}' -> '{obj_expr_str}'")
# 5. Evaluar la expresión del objeto (usando contexto dinámico)
eval_context = self.engine._get_full_context()
obj = None
try:
if not obj_expr_str.strip():
print("DEBUG: Object expression became empty before eval. No action.")
return
print(f"DEBUG: Attempting to eval: '{obj_expr_str}'")
obj = eval(obj_expr_str, eval_context)
print(f"DEBUG: Eval successful. Object: {type(obj)}, Value: {obj}")
except Exception as e:
print(f"DEBUG: Error evaluating object expression '{obj_expr_str}': {e}")
return
# 6. Mostrar popup de autocompletado para el objeto
if obj is not None and hasattr(obj, 'PopupFunctionList'):
methods = obj.PopupFunctionList()
if methods:
self._show_autocomplete_popup(methods, is_global_popup=False)
def _show_autocomplete_popup(self, suggestions, is_global_popup=False):
"""Muestra popup de autocompletado (sin cambios)"""
cursor_bbox = self.input_text.bbox(tk.INSERT)
if not cursor_bbox:
return
x, y, _, height = cursor_bbox
popup_x = self.input_text.winfo_rootx() + x
popup_y = self.input_text.winfo_rooty() + y + height + 2
self._autocomplete_popup = tk.Toplevel(self.root)
self._autocomplete_popup.wm_overrideredirect(True)
self._autocomplete_popup.wm_geometry(f"+{popup_x}+{popup_y}")
self._autocomplete_popup.attributes('-topmost', True)
self.root.after(100, lambda: self._autocomplete_popup.attributes('-topmost', False) if self._autocomplete_popup else None)
self._autocomplete_listbox = tk.Listbox(
self._autocomplete_popup, bg="#3c3f41", fg="#bbbbbb",
selectbackground="#007acc", selectforeground="white",
borderwidth=1, relief="solid", exportselection=False, activestyle="none"
)
for name, hint in suggestions:
self._autocomplete_listbox.insert(tk.END, f"{name}{hint}")
if suggestions:
self._autocomplete_listbox.select_set(0)
self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH)
self._autocomplete_listbox.bind("<Return>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g))
self._autocomplete_listbox.bind("<Tab>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g))
self._autocomplete_listbox.bind("<Escape>", lambda e: self._close_autocomplete_popup())
self._autocomplete_listbox.bind("<Double-Button-1>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g))
self._autocomplete_listbox.focus_set()
self._autocomplete_listbox.bind("<Up>", lambda e: self._navigate_autocomplete(e, -1))
self._autocomplete_listbox.bind("<Down>", lambda e: self._navigate_autocomplete(e, 1))
self.input_text.bind("<Button-1>", lambda e: self._close_autocomplete_popup(), add=True)
self.root.bind("<Button-1>", lambda e: self._close_autocomplete_popup(), add=True)
max_len = max(len(name) for name, _ in suggestions) if suggestions else 10
width = max(15, min(max_len + 10, 50))
height = min(len(suggestions), 10)
full_text_suggestions = [f"{name}{hint}" for name, hint in suggestions]
max_full_len = max(len(text) for text in full_text_suggestions) if full_text_suggestions else 20
width = max(20, min(max_full_len + 5, 80))
self._autocomplete_listbox.config(width=width, height=height)
else:
self._close_autocomplete_popup()
def _navigate_autocomplete(self, event, direction):
"""Navegación en autocomplete (sin cambios)"""
if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox:
return "break"
current_selection = self._autocomplete_listbox.curselection()
if not current_selection:
new_idx = 0 if direction == 1 else self._autocomplete_listbox.size() -1
else:
idx = current_selection[0]
new_idx = idx + direction
if 0 <= new_idx < self._autocomplete_listbox.size():
if current_selection:
self._autocomplete_listbox.select_clear(current_selection[0])
self._autocomplete_listbox.select_set(new_idx)
self._autocomplete_listbox.activate(new_idx)
self._autocomplete_listbox.see(new_idx)
return "break"
def _on_autocomplete_select(self, event, is_global=False):
"""Selección de autocomplete (sin cambios)"""
if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox:
return "break"
selection = self._autocomplete_listbox.curselection()
if not selection:
self._close_autocomplete_popup()
return "break"
selected_text_with_hint = self._autocomplete_listbox.get(selection[0])
item_name = selected_text_with_hint.split("")[0].strip()
if is_global:
cursor_pos_str = self.input_text.index(tk.INSERT)
line_num, char_num = map(int, cursor_pos_str.split('.'))
dot_pos_on_line = char_num - 1
dot_index_str = f"{line_num}.{dot_pos_on_line}"
self.input_text.delete(dot_index_str)
insert_text = item_name + "()"
self.input_text.insert(dot_index_str, insert_text)
self.input_text.mark_set(tk.INSERT, f"{dot_index_str}+{len(item_name)+1}c")
else:
self.input_text.insert(tk.INSERT, item_name + "()")
self.input_text.mark_set(tk.INSERT, f"{tk.INSERT}-1c")
self._close_autocomplete_popup()
self.input_text.focus_set()
self.on_key_release()
return "break"
def _close_autocomplete_popup(self):
"""Cierra popup de autocomplete (sin cambios)"""
if hasattr(self, '_autocomplete_popup') and self._autocomplete_popup:
self._autocomplete_popup.destroy()
self._autocomplete_popup = None
if hasattr(self, '_autocomplete_listbox') and self._autocomplete_listbox:
self._autocomplete_listbox = None
def _evaluate_and_update(self):
"""Evalúa todas las líneas y actualiza la salida"""
try:
input_content = self.input_text.get("1.0", tk.END)
if not input_content.strip():
self._clear_output()
return
lines = input_content.splitlines()
self._evaluate_lines(lines)
except Exception as e:
self._show_error(f"Error durante evaluación: {e}")
def _evaluate_lines(self, lines: List[str]):
"""Evalúa múltiples líneas de código"""
output_data = []
for line_num, line in enumerate(lines, 1):
line = line.strip()
# Líneas vacías o comentarios
if not line or line.startswith('#'):
if line:
output_data.append([("comment", line)])
else:
output_data.append([("", "")])
continue
# Evaluar línea
result = self.engine.evaluate_line(line)
line_output = self._process_evaluation_result(result)
output_data.append(line_output)
self._display_output(output_data)
def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]:
"""Procesa el resultado de evaluación para display"""
output_parts = []
if result.is_error:
ayuda = self.obtener_ayuda(result.original_line)
if ayuda:
ayuda_linea = ayuda.replace("\n", " ").replace("\r", " ")
if len(ayuda_linea) > 120:
ayuda_linea = ayuda_linea[:117] + "..."
output_parts.append(("helper", ayuda_linea))
else:
output_parts.append(("error", f"Error: {result.error}"))
elif result.result_type == "comment":
output_parts.append(("comment", result.original_line))
elif result.result_type == "equation_added":
output_parts.append(("equation", result.symbolic_result))
elif result.result_type == "assignment":
output_parts.append(("info", result.symbolic_result))
# Mostrar evaluación numérica para asignaciones si existe
if result.numeric_result is not None and result.numeric_result != result.result:
output_parts.append(("numeric", f"{result.numeric_result}"))
else:
# Resultado normal
if result.result is not None:
# Determinar tag basado en tipo (DINÁMICO)
tag = self._get_result_tag_dynamic(result.result)
# Verificar si es resultado interactivo
if self.interactive_manager and result.is_interactive:
interactive_tag, display_text = self.interactive_manager.create_interactive_tag(result.result, self.output_text, "1.0")
if interactive_tag:
output_parts.append((interactive_tag, display_text))
else:
output_parts.append((tag, str(result.result)))
else:
output_parts.append((tag, str(result.result)))
# Añadir pista de clase para el resultado principal
primary_result_object = result.result
if not isinstance(primary_result_object, PlotResult):
class_display_name = self._get_class_display_name_dynamic(primary_result_object)
if class_display_name:
output_parts.append(("class_hint", f"[{class_display_name}]"))
# Mostrar evaluación numérica si existe
if result.numeric_result is not None and result.numeric_result != result.result:
output_parts.append(("numeric", f"{result.numeric_result}"))
# Mostrar información adicional
if result.info:
output_parts.append(("info", f"({result.info})"))
return output_parts
def _get_result_tag_dynamic(self, result: Any) -> str:
"""Determina el tag de color para un resultado - VERSIÓN DINÁMICA"""
# Obtener clases registradas dinámicamente del sistema de tipos
try:
registered_classes = self.engine.get_available_types().get('registered_classes', {})
# Verificar si es una instancia de alguna clase registrada
for name, cls in registered_classes.items():
if isinstance(result, cls):
# Usar tags específicos basados en el nombre de la clase
name_lower = name.lower()
if name_lower == "hex":
return "hex"
elif name_lower == "bin":
return "bin"
elif name_lower in ["ip4", "ip"]:
return "ip"
elif name_lower == "chr":
return "chr_type"
elif name_lower == "date":
return "date"
else:
return "custom_type" # Tag genérico para tipos personalizados
except Exception as e:
if self.debug:
print(f"DEBUG: Error en get_result_tag_dynamic: {e}")
# Fallback a tags existentes para tipos no registrados
if isinstance(result, sympy.Basic):
return "symbolic"
else:
return "result"
def _get_class_display_name_dynamic(self, obj: Any) -> str:
"""Obtiene nombre de clase para display - VERSIÓN DINÁMICA"""
try:
# Verificar si es una clase registrada dinámicamente
registered_classes = self.engine.get_available_types().get('registered_classes', {})
for name, cls in registered_classes.items():
if isinstance(obj, cls):
return name
except Exception as e:
if self.debug:
print(f"DEBUG: Error en get_class_display_name_dynamic: {e}")
# Fallback a lógica existente para tipos nativos
if isinstance(obj, sympy.logic.boolalg.BooleanAtom):
return "Boolean"
elif isinstance(obj, sympy.Basic):
if hasattr(obj, 'is_number') and obj.is_number:
if hasattr(obj, 'is_Integer') and obj.is_Integer:
return "Integer"
elif hasattr(obj, 'is_Rational') and obj.is_Rational and not obj.is_Integer:
return "Rational"
elif hasattr(obj, 'is_Float') and obj.is_Float:
return "Float"
else:
return "SympyNumber"
else:
return "Sympy"
elif isinstance(obj, bool):
return "Boolean"
elif isinstance(obj, (int, float, str, list, dict, tuple, type(None))):
class_display_name = type(obj).__name__.capitalize()
if class_display_name == "Nonetype":
class_display_name = "None"
return class_display_name
return ""
def _display_output(self, output_data: List[List[tuple]]):
"""Muestra los datos de salida en el widget (sin cambios)"""
self.output_text.config(state="normal")
self.output_text.delete("1.0", tk.END)
for line_idx, line_parts in enumerate(output_data):
if not line_parts or (len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == ""):
pass
else:
for part_idx, (tag, content) in enumerate(line_parts):
if not content:
continue
if part_idx > 0:
prev_tag, prev_content = line_parts[part_idx-1] if part_idx > 0 else (None, None)
if tag not in ["class_hint", "numeric", "info"] and prev_content:
self.output_text.insert(tk.END, " ; ")
elif tag in ["numeric", "info"] and prev_content:
self.output_text.insert(tk.END, " ")
if content:
self.output_text.insert(tk.END, str(content), tag)
if line_idx < len(output_data) - 1:
self.output_text.insert(tk.END, "\n")
self.output_text.config(state="disabled")
def _clear_output(self):
"""Limpia el panel de salida"""
self.output_text.config(state="normal")
self.output_text.delete("1.0", tk.END)
self.output_text.config(state="disabled")
def _show_error(self, error_msg: str):
"""Muestra un error en el panel de salida"""
self.output_text.config(state="normal")
self.output_text.delete("1.0", tk.END)
self.output_text.insert("1.0", error_msg, "error")
self.output_text.config(state="disabled")
def _show_context_menu(self, event, panel_type: str):
"""Muestra menú contextual"""
context_menu = Menu(
self.root, tearoff=0, bg="#3c3c3c", fg="white",
activebackground="#007acc", activeforeground="white",
relief=tk.FLAT, bd=1,
)
if panel_type == "input":
context_menu.add_command(label="Cortar", command=lambda: self.input_text.event_generate("<<Cut>>"))
context_menu.add_command(label="Copiar", command=lambda: self.input_text.event_generate("<<Copy>>"))
context_menu.add_command(label="Pegar", command=lambda: self.input_text.event_generate("<<Paste>>"))
context_menu.add_separator()
context_menu.add_command(label="Limpiar entrada", command=self.clear_input)
elif panel_type == "output":
context_menu.add_command(label="Copiar todo", command=self.copy_output)
context_menu.add_command(label="Limpiar salida", command=self.clear_output)
context_menu.add_separator()
context_menu.add_command(label="Ayuda", command=self.show_help_window)
try:
context_menu.tk_popup(event.x_root, event.y_root)
finally:
context_menu.grab_release()
# ========== MÉTODOS DE MENÚ Y COMANDOS (la mayoría sin cambios) ==========
def new_session(self):
"""Inicia nueva sesión"""
self.clear_input()
self.clear_output()
self.engine.clear_all()
def load_file(self):
"""Carga archivo en el editor"""
filepath = filedialog.askopenfilename(
title="Cargar archivo",
filetypes=[
("Archivos de texto", "*.txt"),
("Archivos Python", "*.py"),
("Todos los archivos", "*.*")
]
)
if filepath:
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
self.input_text.delete("1.0", tk.END)
self.input_text.insert("1.0", content)
self._evaluate_and_update()
except Exception as e:
messagebox.showerror("Error", f"No se pudo cargar el archivo:\n{e}")
def save_file(self):
"""Guarda contenido del editor"""
filepath = filedialog.asksaveasfilename(
title="Guardar archivo",
defaultextension=".txt",
filetypes=[
("Archivos de texto", "*.txt"),
("Archivos Python", "*.py"),
("Todos los archivos", "*.*")
]
)
if filepath:
try:
content = self.input_text.get("1.0", tk.END)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
messagebox.showinfo("Éxito", "Archivo guardado correctamente.")
except Exception as e:
messagebox.showerror("Error", f"No se pudo guardar el archivo:\n{e}")
def clear_input(self):
"""Limpia panel de entrada"""
self.input_text.delete("1.0", tk.END)
self._clear_output()
def clear_output(self):
"""Limpia panel de salida"""
self._clear_output()
def clear_variables(self):
"""Limpia variables del motor"""
self.engine.clear_variables()
self._evaluate_and_update()
def clear_equations(self):
"""Limpia ecuaciones del motor"""
self.engine.clear_equations()
self._evaluate_and_update()
def clear_all(self):
"""Limpia variables y ecuaciones"""
self.engine.clear_all()
self._evaluate_and_update()
def copy_output(self):
"""Copia el contenido de la salida al portapapeles"""
content = self.output_text.get("1.0", tk.END).strip()
if content:
self.root.clipboard_clear()
self.root.clipboard_append(content)
def show_variables(self):
"""Muestra ventana con variables definidas"""
variables = self.engine.symbol_table
window = tk.Toplevel(self.root)
window.title("Variables Definidas")
window.geometry("500x400")
window.configure(bg="#2b2b2b")
text_widget = scrolledtext.ScrolledText(
window,
font=("Consolas", 11),
bg="#1e1e1e",
fg="#d4d4d4"
)
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
if variables:
content = "Variables definidas:\n\n"
for name, value in variables.items():
content += f"{name} = {value}\n"
else:
content = "No hay variables definidas."
text_widget.insert("1.0", content)
text_widget.config(state="disabled")
def show_equations(self):
"""Muestra ventana con ecuaciones definidas"""
equations = self.engine.equations
window = tk.Toplevel(self.root)
window.title("Ecuaciones Definidas")
window.geometry("500x400")
window.configure(bg="#2b2b2b")
text_widget = scrolledtext.ScrolledText(
window,
font=("Consolas", 11),
bg="#1e1e1e",
fg="#d4d4d4"
)
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
if equations:
content = "Ecuaciones en el sistema:\n\n"
for i, eq in enumerate(equations, 1):
content += f"{i}. {eq}\n"
else:
content = "No hay ecuaciones en el sistema."
text_widget.insert("1.0", content)
text_widget.config(state="disabled")
def solve_system(self):
"""Resuelve el sistema de ecuaciones"""
try:
if not self.engine.equations:
messagebox.showinfo("Info", "No hay ecuaciones para resolver.")
return
solutions = self.engine.solve_system()
window = tk.Toplevel(self.root)
window.title("Soluciones del Sistema")
window.geometry("500x400")
window.configure(bg="#2b2b2b")
text_widget = scrolledtext.ScrolledText(
window,
font=("Consolas", 11),
bg="#1e1e1e",
fg="#d4d4d4"
)
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
content = "Soluciones del sistema:\n\n"
if isinstance(solutions, dict):
for var, value in solutions.items():
content += f"{var} = {value}\n"
else:
content += str(solutions)
text_widget.insert("1.0", content)
text_widget.config(state="disabled")
except Exception as e:
messagebox.showerror("Error", f"Error resolviendo sistema:\n{e}")
def show_types_syntax(self):
"""Muestra sintaxis de tipos disponibles - NUEVA FUNCIÓN"""
try:
types_info = self.engine.get_available_types()
registered_classes = types_info.get('registered_classes', {})
syntax_text = "SINTAXIS DE TIPOS DISPONIBLES\n\n"
if not registered_classes:
syntax_text += "No hay tipos personalizados disponibles.\n"
else:
syntax_text += "Tipos personalizados detectados:\n\n"
for name, cls in sorted(registered_classes.items()):
syntax_text += f"=== {name} ===\n"
# Sintaxis básica
syntax_text += f"Sintaxis: {name}[valor]\n"
syntax_text += f"Alias: {name.lower()}[valor]\n"
# Obtener ayuda si está disponible
if hasattr(cls, 'Helper'):
try:
help_text = cls.Helper(name)
if help_text:
syntax_text += f"Ayuda: {help_text}\n"
except:
pass
# Obtener métodos si está disponible
if hasattr(cls, 'PopupFunctionList'):
try:
methods = cls.PopupFunctionList()
if methods:
syntax_text += "Métodos disponibles:\n"
for method_name, method_desc in methods:
syntax_text += f"{method_name}() - {method_desc}\n"
except:
pass
syntax_text += "\n"
self._show_help_window("Sintaxis de Tipos", syntax_text)
except Exception as e:
messagebox.showerror("Error", f"Error obteniendo sintaxis de tipos:\n{e}")
def show_quick_guide(self):
"""Muestra guía rápida - ACTUALIZADA"""
guide = """# Calculadora MAV - CAS Híbrido
## Sistema de Tipos Dinámico
El sistema detecta automáticamente tipos disponibles en custom_types/
## Sintaxis Nueva con Corchetes
- Sintaxis: Tipo[valor] en lugar de Tipo("valor")
- Ejemplos: Hex[FF], Bin[1010], Dec[10.5], Chr[A]
- Use menú Tipos → Información de tipos para ver tipos disponibles
## Ecuaciones Automáticas
- x**2 + 2*x = 8 (detectado automáticamente)
- a + b = 10 (agregado al sistema)
- variable=? (atajo para solve(variable))
## Funciones SymPy Disponibles
- solve(), diff(), integrate(), limit(), series()
- sin(), cos(), tan(), exp(), log(), sqrt()
- Matrix(), plot(), plot3d()
## Resultados Interactivos
- 📊 Ver Plot (click para ventana matplotlib)
- 📋 Ver Matriz (click para vista expandida)
- 📋 Ver Lista (click para contenido completo)
## Variables Automáticas
- Todas las variables son símbolos SymPy
- x = 5 crea Symbol('x') con valor 5
- Evaluación simbólica + numérica automática
## Autocompletado Dinámico
- Escriba "." después de cualquier objeto para ver métodos
- El sistema usa los tipos registrados automáticamente
"""
self._show_help_window("Guía Rápida", guide)
def show_syntax_help(self):
"""Muestra ayuda de sintaxis - ACTUALIZADA"""
syntax = """# Sintaxis del CAS Híbrido
## Sistema de Tipos Dinámico
Los tipos se detectan automáticamente desde custom_types/
Use menú Tipos → Información de tipos para ver tipos disponibles
## Sintaxis con Corchetes (Dinámica)
Tipo[valor] # Sintaxis general
Tipo[arg1; arg2] # Múltiples argumentos
## Métodos Disponibles (Dinámicos)
Tipo[...].método() # Métodos específicos del tipo
objeto.método[] # Método sin argumentos
## Ecuaciones (detección automática)
expresión = expresión # Ecuación simple
expresión == expresión # Igualdad SymPy
expresión > expresión # Desigualdad SymPy
## Resolver
solve(ecuación, variable)
variable=? # Atajo para solve(variable)
## Variables SymPy Puras
x = valor # Crea Symbol('x')
expresión # Evaluación simbólica automática
"""
self._show_help_window("Sintaxis", syntax)
def show_sympy_functions(self):
"""Muestra funciones SymPy disponibles (sin cambios)"""
functions = """# Funciones SymPy Disponibles
## Matemáticas Básicas
sin(x), cos(x), tan(x)
asin(x), acos(x), atan(x)
sinh(x), cosh(x), tanh(x)
exp(x), log(x), sqrt(x)
abs(x), sign(x), factorial(x)
## Cálculo
diff(expr, var) # Derivada
integrate(expr, var) # Integral indefinida
integrate(expr, (var, a, b)) # Integral definida
limit(expr, var, punto) # Límite
series(expr, var, punto, n) # Serie de Taylor
## Álgebra
solve(ecuación, variable)
simplify(expr), expand(expr)
factor(expr), collect(expr, var)
cancel(expr), apart(expr, var)
## Álgebra Lineal
Matrix([[a, b], [c, d]])
det(matrix), inv(matrix)
## Plotting
plot(expr, (var, inicio, fin))
plot3d(expr, (x, x1, x2), (y, y1, y2))
## Constantes
pi, E, I (imaginario), oo (infinito)
"""
self._show_help_window("Funciones SymPy", functions)
def show_about(self):
"""Muestra información sobre la aplicación - ACTUALIZADA"""
about = """Calculadora MAV - CAS Híbrido
Versión: 2.1 (Sistema de Tipos Dinámico)
Motor: SymPy + Auto-descubrimiento de Tipos
Características:
• Motor algebraico completo (SymPy)
• Sistema de tipos dinámico y extensible
• Sintaxis simplificada con corchetes
• Detección automática de ecuaciones
• Resultados interactivos clickeables
• Auto-descubrimiento de tipos en custom_types/
• Variables SymPy puras
• Plotting integrado
• Autocompletado dinámico
NUEVO: Sistema de Tipos Dinámico
• Detección automática de nuevos tipos
• Organización modular en custom_types/
• Registro automático sin modificar código
• Escalabilidad mejorada
Desarrollado para cálculo matemático avanzado
con soporte especializado para redes,
programación y análisis numérico.
"""
messagebox.showinfo("Acerca de", about)
def _show_help_window(self, title: str, content: str):
"""Muestra ventana de ayuda"""
window = tk.Toplevel(self.root)
window.title(title)
window.geometry("700x500")
window.configure(bg="#2b2b2b")
text_widget = scrolledtext.ScrolledText(
window,
font=("Consolas", 10),
bg="#1e1e1e",
fg="#d4d4d4",
wrap=tk.WORD
)
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
text_widget.insert("1.0", content)
text_widget.config(state="disabled")
def load_history(self):
"""Carga historial de entrada"""
try:
if os.path.exists(self.HISTORY_FILE):
with open(self.HISTORY_FILE, "r", encoding="utf-8") as f:
content = f.read()
if content.strip():
self.input_text.insert("1.0", content)
self.root.after_idle(self._evaluate_and_update)
except Exception as e:
print(f"Error cargando historial: {e}")
def save_history(self):
"""Guarda historial de entrada"""
try:
content = self.input_text.get("1.0", tk.END).rstrip("\n")
if content:
with open(self.HISTORY_FILE, "w", encoding="utf-8") as f:
f.write(content)
elif os.path.exists(self.HISTORY_FILE):
os.remove(self.HISTORY_FILE)
except Exception as e:
print(f"Error guardando historial: {e}")
def on_close(self):
"""Maneja cierre de la aplicación"""
self.save_history()
self._save_settings()
if self.interactive_manager:
self.interactive_manager.close_all_windows()
self.root.destroy()
def show_help_window(self):
"""Muestra ventana de ayuda con archivo externo - NUEVO SISTEMA"""
help_win = tk.Toplevel(self.root)
help_win.title("Ayuda - Calculadora MAV CAS Híbrido")
help_win.geometry("750x600")
help_win.configure(bg="#1e1e1e")
help_win.transient(self.root)
readme_content = self._get_help_content()
if MARKDOWN_AVAILABLE and HTML_VIEWER_TYPE:
try:
# CSS para un tema oscuro, consistente con la UI de la calculadora
dark_theme_css = """
<style>
h1, h2, h3, h4, h5, h6 {
color: #569cd6;
margin-top: 1.2em;
margin-bottom: 0.6em;
border-bottom: 1px solid #3a3a3a;
padding-bottom: 0.3em;
}
h1 { font-size: 1.8em; }
h2 { font-size: 1.5em; }
h3 { font-size: 1.3em; }
p {
line-height: 1.65;
margin-bottom: 0.8em;
}
a {
color: #4fc3f7;
text-decoration: none;
}
a:hover {
color: #80dfff;
text-decoration: underline;
}
code {
font-family: "Consolas", "Courier New", monospace;
background-color: #2d2d2d;
color: #ce9178;
padding: 3px 6px;
border-radius: 4px;
border: 1px solid #3c3c3c;
font-size: 0.95em;
}
pre {
background-color: #1e1e1e;
border: 1px solid #3c3c3c;
border-radius: 4px;
padding: 12px;
overflow-x: auto;
margin: 1em 0;
}
pre > code {
background-color: transparent !important;
color: inherit !important;
padding: 0 !important;
border-radius: 0 !important;
border: none !important;
font-size: 1em !important;
}
ul, ol { padding-left: 25px; color: #c8c8c8; }
li { margin-bottom: 0.4em; }
hr { border: 0; height: 1px; background: #3a3a3a; margin: 1.5em 0; }
table { border-collapse: collapse; width: 90%; margin: 1em auto; }
th, td { border: 1px solid #4a4a4a; padding: 8px; text-align: left; }
th { background-color: #2d2d2d; color: #9cdcfe; font-weight: bold; }
td { background-color: #1e1e1e; }
blockquote {
border-left: 4px solid #569cd6;
margin: 1em 0;
padding: 0.5em 1em;
background-color: #2d2d30;
font-style: italic;
}
</style>
"""
html_fragment = markdown.markdown(
readme_content, extensions=["fenced_code", "codehilite", "tables", "nl2br", "admonition"],
extension_configs={"codehilite": {"noclasses": True, "pygments_style": "monokai"}}
)
# Construir un documento HTML completo
content_for_viewer = f"""
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Ayuda de Calculadora</title>
{dark_theme_css}
</head>
<body style="background-color: #1e1e1e; color: #d4d4d4; font-family: 'Segoe UI', sans-serif; font-size: 10pt; margin:0; padding:12px;">
{html_fragment}
</body>
</html>
"""
if HTML_VIEWER_TYPE == "tkinterweb":
html_viewer = tkinterweb.HtmlFrame(help_win, messages_enabled=False)
html_viewer.load_html(content_for_viewer)
elif HTML_VIEWER_TYPE == "tkhtmlview":
html_viewer = HTMLScrolledText(help_win)
html_viewer.configure(bg="#1e1e1e")
html_viewer.set_html(content_for_viewer)
html_viewer.pack(padx=0, pady=0, fill=tk.BOTH, expand=True)
except Exception as e:
print(f"Error al renderizar Markdown a HTML: {e}")
# Fallback to text if HTML fails
self._show_text_help(help_win, readme_content)
else:
self._show_text_help(help_win, readme_content)
# Botón de cerrar
close_button = tk.Button(
help_win, text="Cerrar", command=help_win.destroy,
bg="#3c3c3c", fg="white", relief=tk.FLAT, padx=10,
)
close_button.pack(pady=(5, 10))
def _get_help_content(self):
"""Obtiene el contenido de ayuda desde archivo externo o genera uno por defecto"""
try:
if os.path.exists(self.HELP_FILE):
with open(self.HELP_FILE, "r", encoding="utf-8") as f:
return f.read()
except IOError:
pass
# Contenido por defecto si no se encuentra el archivo
return """# Calculadora MAV - CAS Híbrido
## Sistema de Tipos Dinámico
El sistema detecta automáticamente tipos disponibles en `custom_types/`
## Sintaxis Nueva con Corchetes
- **Sintaxis**: `Tipo[valor]` en lugar de `Tipo("valor")`
- **Ejemplos**: `Hex[FF]`, `Bin[1010]`, `Dec[10.5]`, `Chr[A]`
- Use menú **Tipos → Información de tipos** para ver tipos disponibles
## Ecuaciones Automáticas
- `x**2 + 2*x = 8` (detectado automáticamente)
- `a + b = 10` (agregado al sistema)
- `variable=?` (atajo para solve(variable))
## Funciones SymPy Disponibles
- `solve()`, `diff()`, `integrate()`, `limit()`, `series()`
- `sin()`, `cos()`, `tan()`, `exp()`, `log()`, `sqrt()`
- `Matrix()`, `plot()`, `plot3d()`
## Resultados Interactivos
- 📊 **Ver Plot** (click para ventana matplotlib)
- 📋 **Ver Matriz** (click para vista expandida)
- 📋 **Ver Lista** (click para contenido completo)
## Variables Automáticas
- Todas las variables son símbolos SymPy
- `x = 5` crea Symbol('x') con valor 5
- Evaluación simbólica + numérica automática
## Autocompletado Dinámico
- Escriba "." después de cualquier objeto para ver métodos
- El sistema usa los tipos registrados automáticamente
## Menú Contextual (clic derecho)
- **En entrada**: Cortar, Copiar, Pegar, Limpiar entrada, Ayuda
- **En salida**: Copiar todo, Limpiar salida, Ayuda
## Desarrollo
Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el directorio raíz de la aplicación.
"""
def _show_text_help(self, help_win, content):
"""Muestra la ayuda en texto plano cuando markdown no está disponible"""
text_widget = scrolledtext.ScrolledText(
help_win, font=("Consolas", 10), bg="#1e1e1e", fg="#d4d4d4",
wrap=tk.WORD, borderwidth=0, highlightthickness=0
)
text_widget.insert("1.0", content)
text_widget.config(state="disabled")
text_widget.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
def obtener_ayuda(self, input_str):
"""Obtiene ayuda usando helpers dinámicos"""
for helper in self.HELPERS:
try:
ayuda = helper(input_str)
if ayuda:
return ayuda
except Exception as e:
print(f"DEBUG: Error en helper: {e}")
continue
return None
def _apply_symbolic_settings(self):
"""Aplica configuraciones simbólicas al motor de evaluación"""
symbolic_mode = self.settings.get("symbolic_mode", True)
show_numeric = self.settings.get("show_numeric_approximation", True)
keep_fractions = self.settings.get("keep_symbolic_fractions", True)
auto_simplify = self.settings.get("auto_simplify", False)
self.engine.set_symbolic_mode(
symbolic_mode=symbolic_mode,
show_numeric=show_numeric,
keep_fractions=keep_fractions,
auto_simplify=auto_simplify
)
def toggle_symbolic_mode(self):
"""Alterna el modo simbólico"""
new_value = self.symbolic_mode_var.get()
self.update_symbolic_settings(symbolic_mode=new_value)
def toggle_numeric_approximation(self):
"""Alterna la aproximación numérica"""
new_value = self.show_numeric_var.get()
self.update_symbolic_settings(show_numeric=new_value)
def toggle_symbolic_fractions(self):
"""Alterna la mantención de fracciones simbólicas"""
new_value = self.keep_fractions_var.get()
self.update_symbolic_settings(keep_fractions=new_value)
def _get_status_text(self):
"""Obtiene el texto de estado actual"""
mode = "🔢 Simbólico" if self.settings.get("symbolic_mode", True) else "🧮 Numérico"
numeric_indicator = "" if self.settings.get("show_numeric_approximation", True) else ""
fractions_indicator = " 📐" if self.settings.get("keep_symbolic_fractions", True) else ""
return f"{mode}{numeric_indicator}{fractions_indicator}"
def main():
"""Función principal"""
root = tk.Tk()
app = HybridCalculatorApp(root)
try:
root.iconname("Calculadora MAV CAS")
except tk.TclError:
pass
root.mainloop()
if __name__ == "__main__":
main()