""" 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("", self.on_key_release) self.input_text.bind("", lambda e: self._show_context_menu(e, "input")) self.output_text.bind("", 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("", _unified_mouse_wheel) self.output_text.bind("", _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("", self._on_autocomplete_select) self._autocomplete_listbox.bind("", lambda e: self._close_autocomplete_popup()) self._autocomplete_listbox.bind("", self._on_autocomplete_select) self._autocomplete_listbox.focus_set() self._autocomplete_listbox.bind("", lambda e: self._navigate_autocomplete(e, -1)) self._autocomplete_listbox.bind("", lambda e: self._navigate_autocomplete(e, 1)) self.input_text.bind("", lambda e: self._close_autocomplete_popup(), add=True) self.input_text.bind("", lambda e: self._close_autocomplete_popup(), add=True) self.root.bind("", lambda e: self._close_autocomplete_popup(), add=True) self.input_text.bind("", 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("<>")) context_menu.add_command(label="Copiar", command=lambda: self.input_text.event_generate("<>")) context_menu.add_command(label="Pegar", command=lambda: self.input_text.event_generate("<>")) 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()