Calc/main_calc_app.py

1081 lines
41 KiB
Python

"""
Calculadora MAV CAS Híbrida - Aplicación principal
"""
import tkinter as tk
from tkinter import scrolledtext, messagebox, Menu, filedialog
import tkinter.font as tkFont
import json
import os
from pathlib import Path # Added for robust path handling
import threading
from typing import List, Dict, Any, Optional
import re
# Importar componentes del CAS híbrido
from main_evaluation import HybridEvaluationEngine, EvaluationResult
from tl_popup import InteractiveResultManager
from ip4_type import Class_IP4
from hex_type import Class_Hex
from bin_type import Class_Bin
from dec_type import Class_Dec
from chr_type import Class_Chr
import sympy
from sympy_helper import SympyTools as SympyHelper
class HybridCalculatorApp:
"""Aplicación principal del CAS híbrido"""
SETTINGS_FILE = "hybrid_calc_settings.json"
HISTORY_FILE = "hybrid_calc_history.txt"
HELPERS = [
Class_IP4.Helper,
Class_Hex.Helper,
Class_Bin.Helper,
Class_Dec.Helper,
Class_Chr.Helper,
SympyHelper.Helper,
]
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("Calculadora MAV - CAS Híbrido")
# Cargar configuración
self.settings = self._load_settings()
self.root.geometry(self.settings.get("window_geometry", "1000x700"))
self.root.configure(bg="#2b2b2b")
# Configurar ícono
self._setup_icon()
# Componentes principales
self.engine = HybridEvaluationEngine()
self.interactive_manager = None # Se inicializa después de crear widgets
# Estado de la aplicación
self._debounce_job = None
self._syncing_yview = False
self._cached_input_font = None
self.output_buffer = []
# Crear interfaz
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_icon(self):
"""Configura el ícono de la aplicación"""
try:
# Construct path relative to this script file (main_calc_app.py)
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}'.")
# Optionally, set a default Tk icon or simply return
return
self.app_icon = tk.PhotoImage(file=str(icon_path))
self.root.iconphoto(True, self.app_icon)
except tk.TclError as e:
# Provide more specific error, including the path and Tkinter's error message
print(f"Advertencia: No se pudo cargar el ícono desde '{icon_path}'. Error de Tkinter: {e}")
except Exception as e:
# Catch other potential errors during icon loading
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 configuración de la aplicación"""
self.settings["window_geometry"] = self.root.winfo_geometry()
if hasattr(self, "paned_window"):
try:
sash_x_pos = self.paned_window.sash_coord(0)[0]
self.settings["sash_pos_x"] = sash_x_pos
except tk.TclError:
pass
try:
with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f:
json.dump(self.settings, f, indent=4)
except IOError:
messagebox.showwarning("Error", "No se pudieron guardar los ajustes.")
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ú Ayuda
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("type_hint", foreground="#6a6a6a")
# Tags para tipos especializados
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")
# Agregar tag para ayuda contextual
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
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):
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: # Should not happen if a dot was typed
print("DEBUG: _handle_dot_autocomplete called with cursor at beginning of line somehow.")
return
text_before_dot = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{char_idx_after_dot - 1}")
# Si no hay nada o solo espacios antes del punto, ofrecer sugerencias globales
if not text_before_dot.strip():
print("DEBUG: Dot on empty line or after spaces. Offering global suggestions.")
suggestions = []
custom_classes_suggestions = [
("Hex", "Tipo Hexadecimal. Ej: Hex[FF]"),
("Bin", "Tipo Binario. Ej: Bin[1010]"),
("Dec", "Tipo Decimal. Ej: Dec[42]"),
("IP4", "Tipo Dirección IPv4. Ej: IP4[1.2.3.4/24]"),
("Chr", "Tipo Carácter. Ej: Chr[A]"),
]
suggestions.extend(custom_classes_suggestions)
try:
sympy_functions = SympyHelper.PopupFunctionList()
if sympy_functions:
suggestions.extend(sympy_functions)
except Exception as e:
print(f"DEBUG: Error calling SympyHelper.PopupFunctionList() for global: {e}")
if suggestions:
self._show_autocomplete_popup(suggestions, is_global_popup=True)
return
# Si hay texto antes del punto, es para autocompletado de métodos de un objeto
obj_expr_str = text_before_dot.strip()
print(f"DEBUG: Autocomplete triggered for object. Expression: '{obj_expr_str}'")
if not obj_expr_str:
# Esto no debería ocurrir si la lógica anterior para popup global es correcta
print("DEBUG: Object expression is empty. No autocomplete.")
return
# 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
# Preprocesar para convertir sintaxis de corchetes a llamada de clase
# Ejemplo: Hex[FF] -> Hex('FF')
bracket_match = re.match(r"([A-Za-z_][A-Za-z0-9_]*)\[(.*)\]$", obj_expr_str)
if bracket_match:
class_name, arg = bracket_match.groups()
if arg.isdigit():
obj_expr_str = f"{class_name}({arg})"
else:
obj_expr_str = f"{class_name}('{arg}')"
print(f"DEBUG: Preprocessed bracket syntax to: '{obj_expr_str}'")
eval_context = self.engine._get_full_context() if hasattr(self.engine, '_get_full_context') else {}
obj = None
try:
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
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):
# suggestions: lista de tuplas (nombre, hint)
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>", self._on_autocomplete_select)
self._autocomplete_listbox.bind("<Tab>", self._on_autocomplete_select)
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("<FocusOut>", lambda e: self._close_autocomplete_popup(), add=True) # Removed: Caused popup to close immediately
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)
# self.input_text.bind("<Key>", lambda e: self._close_autocomplete_popup(), add=True) # Removed: Too aggressive
# Pasar el flag is_global_popup a los bindings que llaman a _on_autocomplete_select
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))
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)
# Calcular el ancho basado en el texto completo que se muestra en el listbox
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)) # Ajustar el +5 y el límite 80 según sea necesario
self._autocomplete_listbox.config(width=width, height=height)
else:
self._close_autocomplete_popup()
def _navigate_autocomplete(self, event, direction):
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):
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])
# Extraer solo el nombre del ítem, antes de " —"
item_name = selected_text_with_hint.split("")[0].strip()
if is_global:
# Eliminar el punto que activó el popup y luego insertar el nombre
cursor_pos_str = self.input_text.index(tk.INSERT) # Posición actual (después del punto)
line_num, char_num = map(int, cursor_pos_str.split('.'))
# El punto está en char_num - 1 en la línea actual
dot_pos_on_line = char_num - 1
dot_index_str = f"{line_num}.{dot_pos_on_line}"
self.input_text.delete(dot_index_str)
# Insertar el nombre de la función/clase seguido de "()"
insert_text = item_name + "()"
self.input_text.insert(dot_index_str, insert_text)
# Colocar cursor dentro de los paréntesis: después del nombre y el '('
self.input_text.mark_set(tk.INSERT, f"{dot_index_str}+{len(item_name)+1}c")
else:
# Comportamiento existente para métodos de objeto
self.input_text.insert(tk.INSERT, item_name + "()")
self.input_text.mark_set(tk.INSERT, f"{tk.INSERT}-1c") # Cursor dentro de los paréntesis
self._close_autocomplete_popup()
self.input_text.focus_set()
self.on_key_release() # Trigger re-evaluation
return "break"
def _close_autocomplete_popup(self):
if hasattr(self, '_autocomplete_popup') and self._autocomplete_popup:
self._autocomplete_popup.destroy()
self._autocomplete_popup = None
# Consider unbinding the Button-1 events if they were stored with IDs,
# for now, their guard condition `if self._autocomplete_popup:` handles multiple calls.
# Example of how to unbind if IDs were stored:
# if hasattr(self, '_input_text_b1_bind_id'):
# self.input_text.unbind("<Button-1>", self._input_text_b1_bind_id)
# del self._input_text_b1_bind_id
# if hasattr(self, '_root_b1_bind_id'):
# self.root.unbind("<Button-1>", self._root_b1_bind_id)
# del self._root_b1_bind_id
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:
# Mostrar ayuda en un solo renglón, truncando si es necesario
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))
else:
# Resultado normal
if result.result is not None:
# Determinar tag basado en tipo
tag = self._get_result_tag(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)))
# 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(self, result: Any) -> str:
"""Determina el tag de color para un resultado"""
if isinstance(result, Class_Hex):
return "hex"
elif isinstance(result, Class_Bin):
return "bin"
elif isinstance(result, Class_IP4):
return "ip"
elif isinstance(result, Class_Chr):
return "chr_type"
elif isinstance(result, sympy.Basic):
return "symbolic"
else:
return "result"
def _display_output(self, output_data: List[List[tuple]]):
"""Muestra los datos de salida en el widget"""
self.output_text.config(state="normal")
self.output_text.delete("1.0", tk.END)
for line_idx, line_parts in enumerate(output_data):
# Línea vacía
if not line_parts or (len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == ""):
pass
else:
# Mostrar partes de la línea
first_part = True
for tag, content in line_parts:
if not first_part and content:
self.output_text.insert(tk.END, " ; ")
if content:
self.output_text.insert(tk.END, str(content), tag)
first_part = False
# Añadir nueva línea excepto para la última línea
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)
context_menu.add_separator()
context_menu.add_command(label="Insertar ejemplo", command=self.insert_example)
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_quick_guide)
try:
context_menu.tk_popup(event.x_root, event.y_root)
finally:
context_menu.grab_release()
# Métodos de menú y comandos
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 contenido de salida al clipboard"""
content = self.output_text.get("1.0", tk.END).strip()
if content:
self.root.clipboard_clear()
self.root.clipboard_append(content)
def insert_example(self):
"""Inserta código de ejemplo"""
example = """# Calculadora MAV - CAS Híbrido
# Sintaxis nueva con corchetes
# Tipos especializados
Hex[FF] + 1
IP4[192.168.1.100/24].NetworkAddress[]
Bin[1010] * 2
# Matemáticas simbólicas
x + 2*y
diff(x**2 + sin(x), x)
integrate(x**2, x)
# Ecuaciones (detección automática)
x**2 + 2*x - 8 = 0
3*a + b = 10
# Resolver ecuaciones
solve(x**2 + 2*x - 8, x)
a=?
# Variables automáticas
z = 5
w = z**2 + 3
# Plotting interactivo
plot(sin(x), (x, -2*pi, 2*pi))
# Matrices
Matrix([[1, 2], [3, 4]])
"""
self.input_text.delete("1.0", tk.END)
self.input_text.insert("1.0", example)
self._evaluate_and_update()
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_quick_guide(self):
"""Muestra guía rápida"""
guide = """# Calculadora MAV - CAS Híbrido
## Sintaxis Nueva con Corchetes
- IP4[192.168.1.1/24] en lugar de IP4("192.168.1.1/24")
- Hex[FF], Bin[1010], Dec[10.5], Chr[A]
## 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
"""
self._show_help_window("Guía Rápida", guide)
def show_syntax_help(self):
"""Muestra ayuda de sintaxis"""
syntax = """# Sintaxis del CAS Híbrido
## Clases Especializadas (solo corchetes)
IP4[dirección/prefijo] # IP4[192.168.1.1/24]
Hex[valor] # Hex[FF], Hex[255]
Bin[valor] # Bin[1010], Bin[10]
Dec[valor] # Dec[10.5], Dec[10]
Chr[carácter] # Chr[A], Chr[Hello]
## Métodos Disponibles
IP4[...].NetworkAddress[]
IP4[...].BroadcastAddress[]
IP4[...].Nodes()
Hex[...].toDecimal()
## 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"""
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"""
about = """Calculadora MAV - CAS Híbrido
Versión: 2.0
Motor: SymPy + Clases Especializadas
Características:
• Motor algebraico completo (SymPy)
• Sintaxis simplificada con corchetes
• Detección automática de ecuaciones
• Resultados interactivos clickeables
• Tipos especializados (IP4, Hex, Bin, etc.)
• Variables SymPy puras
• Plotting integrado
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)
return
except Exception as e:
print(f"Error cargando historial: {e}")
# Cargar ejemplo por defecto si no hay historial
self.insert_example()
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 obtener_ayuda(self, input_str):
for helper in self.HELPERS:
ayuda = helper(input_str)
if ayuda:
return ayuda
return None
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()