""" 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 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" 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 CON NUEVO SISTEMA ========== self.engine = HybridEvaluationEngine(auto_discover_types=True) 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 = [] # 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_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 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Ú TIPOS (NUEVO) ========== types_menu = Menu(menubar, tearoff=0) 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_command(label="Recargar tipos", command=self.reload_types) 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)) 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 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 si existen, sino usar genérico 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" else: return "custom_type" # Tag genérico para tipos personalizados except Exception as e: print(f"DEBUG: Error en get_result_tag_dynamic: {e}") # Fallback a tags existentes if hasattr(result, '__class__') and 'Class_' in result.__class__.__name__: return "custom_type" elif 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 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: print(f"DEBUG: Error en get_class_display_name_dynamic: {e}") # Fallback a lógica existente if hasattr(obj, '__class__'): class_name = obj.__class__.__name__ if class_name.startswith('Class_'): return class_name.replace("Class_", "") elif class_name.endswith('Mask'): return class_name 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) 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 (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 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 - ACTUALIZADO CON TIPOS DINÁMICOS""" # Obtener tipos disponibles dinámicamente try: available_types = self.engine.get_available_types() registered_classes = available_types.get('registered_classes', {}) except: registered_classes = {} # Crear ejemplo base example = """# Calculadora MAV - CAS Híbrido con Sistema de Tipos Dinámico # Sintaxis nueva con corchetes """ # Añadir ejemplos de tipos disponibles dinámicamente if registered_classes: example += "# Tipos especializados disponibles\n" for name in sorted(registered_classes.keys()): if name == "Hex": example += "Hex[FF] + 1\n" elif name == "Bin": example += "Bin[1010] * 2\n" elif name == "Chr": example += "Chr[A].toHex()\n" elif name == "Dec": example += "Dec[42].toBin()\n" elif name == "IP4": example += "IP4[192.168.1.100/24].NetworkAddress()\n" elif name == "IP4Mask": example += "IP4Mask[24].hosts_count()\n" example += "\n" # Resto del ejemplo (sin cambios) example += """# 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_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) 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): """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 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()