Calc/main_calc_app.py

1600 lines
66 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 logging # <--- AÑADIDO
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
# Usar logging para estas advertencias iniciales
module_logger = logging.getLogger(__name__)
if not MARKDOWN_AVAILABLE:
module_logger.warning("La librería 'markdown' no está instalada. La ayuda se mostrará en texto plano.")
if MARKDOWN_AVAILABLE and HTML_VIEWER_TYPE is None:
module_logger.warning("'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.logger = logging.getLogger(__name__) # <--- AÑADIDO: Logger para la instancia
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.Helper al final
self.HELPERS.append(SympyHelper.Helper)
# Usar logger en lugar de print, y sin emoji para la consola
self.logger.info(f"Helpers dinámicos cargados: {len(self.HELPERS)}") # Original: 🆘
except Exception as e:
# Usar logger en lugar de print, y sin emoji para la consola
self.logger.error(f"Error cargando helpers dinámicos: {e}", exc_info=True) # Original: ⚠️
# Fallback a helpers básicos
self.HELPERS = [SympyHelper.Helper]
def reload_types(self):
"""Recarga el sistema de tipos (útil para desarrollo)"""
try:
self.logger.info("Recargando sistema de tipos...") # Original: 🔄
# Recargar engine
self.engine.reload_types()
# Recargar helpers
self._setup_dynamic_helpers()
# Re-evaluar contenido actual
self._evaluate_and_update()
self.logger.info("Sistema de tipos recargado.") # Original: ✅
except Exception as e:
self.logger.error(f"Error recargando tipos: {e}", exc_info=True) # Original: ❌
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():
self.logger.warning(f"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:
self.logger.warning(f"No se pudo cargar el ícono desde '{icon_path}'. Error de Tkinter: {e}")
except Exception as e:
self.logger.warning(f"Ocurrió un error inesperado al cargar el ícono desde '{icon_path}': {e}", exc_info=True)
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:
self.logger.error(f"Error guardando configuración: {e}", exc_info=True)
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 historial", command=self.clear_history)
# 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 y auto-dimensionado
self._debounce_job = self.root.after(300, self._process_input_and_adjust_layout)
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:
self.logger.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:
self.logger.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:
self.logger.debug(f"Error calling Helper for {name}: {e_helper}")
pass
suggestions.append((name, hint))
except Exception as e:
self.logger.debug(f"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:
self.logger.debug(f"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(("+", "-", "*", "/", "(", ",")):
self.logger.debug(f"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
self.logger.debug(f"Autocomplete for object. Extracted: '{obj_expr_str}' from: '{text_on_line_up_to_dot}'")
if not obj_expr_str:
self.logger.debug("Object expression is empty after extraction. No autocomplete.")
return
# 3. Caso especial para el módulo sympy
if obj_expr_str == "sympy":
self.logger.debug(f"Detected 'sympy.', using SympyHelper for suggestions.")
try:
methods = SympyHelper.PopupFunctionList()
if methods:
self._show_autocomplete_popup(methods, is_global_popup=False)
else:
self.logger.debug(f"SympyHelper.PopupFunctionList returned no methods.")
except Exception as e:
self.logger.debug(f"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 and self.debug: # Solo loguear si self.debug es True
self.logger.debug(f"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():
self.logger.debug("Object expression became empty before eval. No action.")
return
self.logger.debug(f"Attempting to eval: '{obj_expr_str}'")
obj = eval(obj_expr_str, eval_context)
self.logger.debug(f"Eval successful. Object: {type(obj)}, Value: {obj}")
except Exception as e:
self.logger.debug(f"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
# NUEVO: Limpiar completamente el contexto antes de cada evaluación
# Esto garantiza que cada modificación reevalúe todo desde cero
self.engine.clear_all()
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:
self.logger.debug(f"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:
self.logger.debug(f"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()
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._process_input_and_adjust_layout()
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_history(self):
"""Limpia el archivo de historial"""
try:
if os.path.exists(self.HISTORY_FILE):
os.remove(self.HISTORY_FILE)
messagebox.showinfo("Éxito", "Historial limpiado correctamente.")
except Exception as e:
messagebox.showerror("Error", f"No se pudo limpiar el historial:\n{e}")
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_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 y realiza evaluación inicial"""
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)
# Hacer evaluación inicial para mostrar resultados del historial
# Esto mantiene el comportamiento de contexto limpio pero muestra resultados
self.root.after_idle(self._process_input_and_adjust_layout)
except Exception as e:
self.logger.error(f"Error cargando historial: {e}", exc_info=True)
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:
self.logger.error(f"Error guardando historial: {e}", exc_info=True)
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:
self.logger.error(f"Error al renderizar Markdown a HTML: {e}", exc_info=True)
# 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:
self.logger.debug(f"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 _get_input_font(self):
"""Obtiene o crea y cachea el objeto tk.Font para el panel de entrada."""
if not self._cached_input_font:
# Asume la fuente configurada en create_widgets: ("Consolas", 11)
self._cached_input_font = tkFont.Font(family="Consolas", size=11)
return self._cached_input_font
def _adjust_input_pane_width(self):
"""Ajusta el ancho del panel de entrada según su contenido."""
if not hasattr(self, 'paned_window') or not self.paned_window.winfo_exists():
return
# Esperar a que la ventana tenga un tamaño válido
if self.paned_window.winfo_width() <= 1:
return # Se reintentará en la siguiente llamada (ej. por KeyRelease)
# Obtener contenido excluyendo el último newline automático del widget Text
input_content = self.input_text.get("1.0", f"{tk.END}-1c")
lines = input_content.splitlines()
input_font = self._get_input_font()
max_pixel_width = 0
if not input_content.strip(): # Si está vacío o solo espacios en blanco
max_pixel_width = 5 # Ancho mínimo para el cursor o como placeholder
else:
for line in lines:
measured_width = input_font.measure(line) if line.strip() else input_font.measure(" ")
if measured_width > max_pixel_width:
max_pixel_width = measured_width
padding = 40 # Relleno para barra de desplazamiento, márgenes, etc.
width_needed_by_text = max_pixel_width + padding
# Debugging opcional (descomenta si necesitas depurar)
if self.debug:
self.logger.debug(f"--- Adjusting Input Pane ---")
self.logger.debug(f"Input content: '{input_content[:50]}...'")
self.logger.debug(f"Max pixel width of text: {max_pixel_width}")
self.logger.debug(f"Width needed by text (max_pixel_width + padding): {width_needed_by_text}")
min_input_pane_width = 200 # Definido en create_widgets
min_output_pane_width = 200 # Definido en create_widgets
total_width = self.paned_window.winfo_width()
current_sash_pos = 0
try:
sash_coords = self.paned_window.sash_coord(0)
if sash_coords:
current_sash_pos = sash_coords[0]
else:
if self.debug:
self.logger.debug("Could not get sash_coord.")
return
except tk.TclError:
if self.debug:
self.logger.debug("TclError getting sash_coord.")
return
if self.debug:
self.logger.debug(f"Current sash position (input pane width): {current_sash_pos}")
if width_needed_by_text > current_sash_pos:
if self.debug:
self.logger.debug(f"Condition MET: Text needs more space ({width_needed_by_text} > {current_sash_pos})")
new_input_width = width_needed_by_text # Punto de partida
# Asegurar que el nuevo ancho no sea menor que el mínimo del panel de entrada
new_input_width = max(new_input_width, min_input_pane_width)
# Asegurar que el panel de salida conserve su espacio mínimo
if total_width - new_input_width < min_output_pane_width:
new_input_width = total_width - min_output_pane_width
new_input_width = max(new_input_width, min_input_pane_width) # Re-verificar mínimo del input
# Aplicar un ratio máximo para el panel de entrada
max_input_ratio = 0.75 # Podría ser una constante de clase
max_width_by_ratio = int(total_width * max_input_ratio)
if new_input_width > max_width_by_ratio:
if max_width_by_ratio >= min_input_pane_width and \
(total_width - max_width_by_ratio) >= min_output_pane_width:
new_input_width = max_width_by_ratio
final_new_input_width = max(0, int(new_input_width)) # No debe ser negativo
if self.debug:
self.logger.debug(f"Calculated final new input width: {final_new_input_width}")
# Mover el sash solo si el nuevo ancho es significativamente mayor que el actual (umbral ajustado)
sash_adjustment_threshold = 3 # Píxeles
if final_new_input_width > current_sash_pos and \
(final_new_input_width - current_sash_pos) >= sash_adjustment_threshold:
if self.debug:
self.logger.debug(f"Condition MET for sash_place: New width {final_new_input_width} is significantly larger (diff >= {sash_adjustment_threshold}).")
try:
if self.paned_window.winfo_exists() and total_width >= (min_input_pane_width + min_output_pane_width):
self.paned_window.sash_place(0, final_new_input_width, 0) # Añadido el argumento y=0
if self.debug:
self.logger.debug(f"Sash placed at: {final_new_input_width}")
elif self.debug:
self.logger.debug("Paned window not ready or total width too small for sash_place.")
except tk.TclError as e_sash:
if self.debug:
self.logger.debug(f"TclError during sash_place: {e_sash}")
pass
elif self.debug:
self.logger.debug(f"Condition NOT MET for sash_place: final_new_input_width ({final_new_input_width}) vs current_sash_pos ({current_sash_pos}) or threshold ({sash_adjustment_threshold}).")
elif self.debug:
self.logger.debug(f"Condition NOT MET: Text does not need more space ({width_needed_by_text} <= {current_sash_pos})")
if self.debug:
self.logger.debug(f"--- End Adjusting Input Pane ---")
def _process_input_and_adjust_layout(self):
"""Evalúa todas las líneas y luego ajusta el ancho del panel de entrada."""
self._evaluate_and_update()
self._adjust_input_pane_width()
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()