Calc/main_calc_app.py

966 lines
35 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
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 HybridIP4 as IP4
from hex_type import HybridHex as Hex
from bin_type import HybridBin as Bin
from dec_type import HybridDec as Dec
from chr_type import HybridChr as Chr
import sympy
from sympy_helper import Helper as SympyHelper
class HybridCalculatorApp:
"""Aplicación principal del CAS híbrido"""
SETTINGS_FILE = "hybrid_calc_settings.json"
HISTORY_FILE = "hybrid_calc_history.txt"
HELPERS = [
IP4.Helper,
Hex.Helper,
Bin.Helper,
Dec.Helper,
Chr.Helper,
SympyHelper,
]
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:
self.app_icon = tk.PhotoImage(file="icon.png")
self.root.iconphoto(True, self.app_icon)
except tk.TclError:
print("Advertencia: No se pudo cargar 'icon.png' como ícono.")
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")
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_of_dot = int(char_num_str)
obj_expr_str = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{char_idx_of_dot -1}").strip()
if not obj_expr_str:
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()
# Si el argumento es un número, no poner comillas
if arg.isdigit():
obj_expr_str = f"{class_name}({arg})"
else:
obj_expr_str = f"{class_name}('{arg}')"
eval_context = self.engine._get_full_context() if hasattr(self.engine, '_get_full_context') else {}
obj = None
try:
obj = eval(obj_expr_str, eval_context)
except Exception:
return
if obj is not None and hasattr(obj, 'PopupFunctionList'):
methods = obj.PopupFunctionList()
if methods:
self._show_autocomplete_popup(methods)
def _show_autocomplete_popup(self, suggestions):
# 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("<Escape>", lambda e: self._close_autocomplete_popup())
self._autocomplete_listbox.bind("<Double-Button-1>", self._on_autocomplete_select)
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)
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)
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)
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):
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 = self._autocomplete_listbox.get(selection[0])
method_name = selected.split()[0]
self.input_text.insert(tk.INSERT, method_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
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:
output_parts.append(("helper", ayuda))
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, Hex):
return "hex"
elif isinstance(result, Bin):
return "bin"
elif isinstance(result, IP4):
return "ip"
elif isinstance(result, 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()