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