""" Calculadora MAV CAS Híbrida - Aplicación principal """ import tkinter as tk from tkinter import scrolledtext, messagebox, Menu, filedialog import tkinter.font as tkFont import json import os from pathlib import Path # Added for robust path handling import threading from typing import List, Dict, Any, Optional import re # Importar componentes del CAS híbrido from main_evaluation import HybridEvaluationEngine, EvaluationResult from sympy_Base import SympyClassBase from tl_popup import InteractiveResultManager, PlotResult # <--- Asegurar que PlotResult se importa from ip4_type import Class_IP4 from hex_type import Class_Hex from bin_type import Class_Bin from dec_type import Class_Dec from chr_type import Class_Chr import sympy from sympy_helper import SympyTools as SympyHelper class HybridCalculatorApp: """Aplicación principal del CAS híbrido""" SETTINGS_FILE = "hybrid_calc_settings.json" HISTORY_FILE = "hybrid_calc_history.txt" HELPERS = [ Class_IP4.Helper, Class_Hex.Helper, Class_Bin.Helper, Class_Dec.Helper, Class_Chr.Helper, SympyHelper.Helper, ] def __init__(self, root: tk.Tk): self.root = root self.root.title("Calculadora MAV - CAS Híbrido") # Cargar configuración self.settings = self._load_settings() self.root.geometry(self.settings.get("window_geometry", "1000x700")) self.root.configure(bg="#2b2b2b") # Configurar ícono self._setup_icon() # Componentes principales self.engine = HybridEvaluationEngine() self.interactive_manager = None # Se inicializa después de crear widgets # Estado de la aplicación self._debounce_job = None self._syncing_yview = False self._cached_input_font = None self.output_buffer = [] # Crear interfaz self.create_widgets() self.setup_interactive_manager() self.load_history() # Configurar eventos de cierre self.root.protocol("WM_DELETE_WINDOW", self.on_close) def _setup_icon(self): """Configura el ícono de la aplicación""" try: # Construct path relative to this script file (main_calc_app.py) script_dir = Path(__file__).resolve().parent icon_path = script_dir / "icon.png" if not icon_path.is_file(): print(f"Advertencia: Archivo de ícono no encontrado en '{icon_path}'.") # Optionally, set a default Tk icon or simply return return self.app_icon = tk.PhotoImage(file=str(icon_path)) self.root.iconphoto(True, self.app_icon) except tk.TclError as e: # Provide more specific error, including the path and Tkinter's error message print(f"Advertencia: No se pudo cargar el ícono desde '{icon_path}'. Error de Tkinter: {e}") except Exception as e: # Catch other potential errors during icon loading print(f"Advertencia: Ocurrió un error inesperado al cargar el ícono desde '{icon_path}': {e}") def _load_settings(self) -> Dict[str, Any]: """Carga configuración de la aplicación""" if os.path.exists(self.SETTINGS_FILE): try: with open(self.SETTINGS_FILE, "r", encoding="utf-8") as f: return json.load(f) except (IOError, json.JSONDecodeError): return {} return {} def _save_settings(self): """Guarda configuración de la aplicación""" self.settings["window_geometry"] = self.root.winfo_geometry() if hasattr(self, "paned_window"): try: sash_x_pos = self.paned_window.sash_coord(0)[0] self.settings["sash_pos_x"] = sash_x_pos except tk.TclError: pass try: with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f: json.dump(self.settings, f, indent=4) except IOError: messagebox.showwarning("Error", "No se pudieron guardar los ajustes.") def create_widgets(self): """Crea la interfaz gráfica""" # Frame principal main_frame = tk.Frame(self.root, bg="#2b2b2b", bd=0) main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # Panel dividido self.paned_window = tk.PanedWindow( main_frame, orient=tk.HORIZONTAL, bg="#2b2b2b", sashrelief=tk.FLAT, sashwidth=4, bd=0, showhandle=False, opaqueresize=True, ) self.paned_window.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) # Panel de entrada initial_input_width = self.settings.get("sash_pos_x", 450) self.input_text = scrolledtext.ScrolledText( self.paned_window, font=("Consolas", 11), bg="#1e1e1e", fg="#d4d4d4", insertbackground="#ffffff", selectbackground="#264f78", undo=True, wrap=tk.NONE, borderwidth=0, highlightthickness=0, relief=tk.FLAT, ) self.paned_window.add( self.input_text, width=initial_input_width, stretch="always", minsize=200 ) # Panel de salida self.output_text = scrolledtext.ScrolledText( self.paned_window, font=("Consolas", 11), bg="#0f0f0f", fg="#00ff00", state="disabled", wrap=tk.NONE, borderwidth=0, highlightthickness=0, relief=tk.FLAT, ) self.paned_window.add( self.output_text, stretch="always", minsize=200 ) # Configurar eventos self.input_text.bind("", 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("class_hint", foreground="#888888") # Gris para la pista de clase self.output_text.tag_configure("type_hint", foreground="#6a6a6a") # Tags para tipos especializados self.output_text.tag_configure("hex", foreground="#f9a825") self.output_text.tag_configure("bin", foreground="#4fc3f7") self.output_text.tag_configure("ip", foreground="#fff176") self.output_text.tag_configure("date", foreground="#ff8a80") self.output_text.tag_configure("chr_type", foreground="#80cbc4") # Agregar tag para ayuda contextual self.output_text.tag_configure("helper", foreground="#ffd700", font=("Consolas", 11, "italic")) def on_key_release(self, event=None): """Maneja eventos de teclado""" if self._debounce_job: self.root.after_cancel(self._debounce_job) # Autocompletado con punto if event and event.char == '.' and self.input_text.focus_get() == self.input_text: self._handle_dot_autocomplete() # Evaluación con debounce self._debounce_job = self.root.after(300, self._evaluate_and_update) def _handle_dot_autocomplete(self): """Maneja el autocompletado cuando se escribe un punto.""" self._close_autocomplete_popup() cursor_index_str = self.input_text.index(tk.INSERT) line_num_str, char_num_str = cursor_index_str.split('.') current_line_num = int(line_num_str) char_idx_after_dot = int(char_num_str) if char_idx_after_dot == 0: # Should not happen if a dot was typed print("DEBUG: Autocomplete: Cursor at beginning of line after dot. No action.") return # Índice del punto en la línea actual (0-based) dot_char_index_in_line = char_idx_after_dot - 1 # Texto en la línea actual HASTA el punto (sin incluirlo) 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 if not stripped_text_before_dot: print("DEBUG: Dot on empty line or after spaces. Offering global suggestions.") suggestions = [] custom_types_suggestions = [ ("Hex", "Tipo Hexadecimal. Ej: Hex[FF]"), ("Bin", "Tipo Binario. Ej: Bin[1010]"), ("Dec", "Tipo Decimal. Ej: Dec[42]"), ("IP4", "Tipo Dirección IPv4. Ej: IP4[1.2.3.4/24]"), ("Chr", "Tipo Carácter. Ej: Chr[A]"), ] suggestions.extend(custom_types_suggestions) try: sympy_functions = SympyHelper.PopupFunctionList() if sympy_functions: suggestions.extend(sympy_functions) except Exception as e: print(f"DEBUG: Error calling SympyHelper.PopupFunctionList() for global: {e}") if suggestions: self._show_autocomplete_popup(suggestions, is_global_popup=True) return # 2. Es un popup de OBJETO. Extraer la expresión del objeto. obj_expr_str_candidate = "" # Regex para `identificador_o_ClaseConCorchetes(.identificador_o_ClaseConCorchetes)*` # Anclado al final de stripped_text_before_dot 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(" ", "") # Quitar espacios como en "obj . method" else: # Heurística: si el regex no coincide, tomar todo stripped_text_before_dot. # Esto podría capturar (a+b) o mi_func() obj_expr_str_candidate = stripped_text_before_dot # Validación simple para evitar evaluar cosas que claramente no son objetos 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: # Debería estar cubierto por el popup global, pero por si acaso. 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 para sintaxis Clase[arg] y metodo[] # Es importante transformar obj_expr_str ANTES de pasarlo a eval(). if '[' in obj_expr_str: # Optimización: solo llamar si hay corchetes original_for_debug = obj_expr_str # self.engine.parser es una instancia de BracketParser 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 eval_context = self.engine._get_full_context() if hasattr(self.engine, '_get_full_context') else {} obj = None try: if not obj_expr_str.strip(): # Seguridad adicional 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) # else: Podríamos añadir un fallback a dir(obj) aquí si se desea para objetos genéricos # print(f"DEBUG: Object {type(obj)} has no PopupFunctionList. dir(obj) could be used.") def _show_autocomplete_popup(self, suggestions, is_global_popup=False): # suggestions: lista de tuplas (nombre, hint) cursor_bbox = self.input_text.bbox(tk.INSERT) if not cursor_bbox: return x, y, _, height = cursor_bbox popup_x = self.input_text.winfo_rootx() + x popup_y = self.input_text.winfo_rooty() + y + height + 2 self._autocomplete_popup = tk.Toplevel(self.root) self._autocomplete_popup.wm_overrideredirect(True) self._autocomplete_popup.wm_geometry(f"+{popup_x}+{popup_y}") self._autocomplete_popup.attributes('-topmost', True) self.root.after(100, lambda: self._autocomplete_popup.attributes('-topmost', False) if self._autocomplete_popup else None) self._autocomplete_listbox = tk.Listbox( self._autocomplete_popup, bg="#3c3f41", fg="#bbbbbb", selectbackground="#007acc", selectforeground="white", borderwidth=1, relief="solid", exportselection=False, activestyle="none" ) for name, hint in suggestions: self._autocomplete_listbox.insert(tk.END, f"{name} — {hint}") if suggestions: self._autocomplete_listbox.select_set(0) self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH) self._autocomplete_listbox.bind("", self._on_autocomplete_select) self._autocomplete_listbox.bind("", self._on_autocomplete_select) 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) # Removed: Caused popup to close immediately 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) # Removed: Too aggressive # Pasar el flag is_global_popup a los bindings que llaman a _on_autocomplete_select 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)) max_len = max(len(name) for name, _ in suggestions) if suggestions else 10 width = max(15, min(max_len + 10, 50)) height = min(len(suggestions), 10) # Calcular el ancho basado en el texto completo que se muestra en el listbox full_text_suggestions = [f"{name} — {hint}" for name, hint in suggestions] max_full_len = max(len(text) for text in full_text_suggestions) if full_text_suggestions else 20 width = max(20, min(max_full_len + 5, 80)) # Ajustar el +5 y el límite 80 según sea necesario self._autocomplete_listbox.config(width=width, height=height) else: self._close_autocomplete_popup() def _navigate_autocomplete(self, event, direction): if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox: return "break" current_selection = self._autocomplete_listbox.curselection() if not current_selection: new_idx = 0 if direction == 1 else self._autocomplete_listbox.size() -1 else: idx = current_selection[0] new_idx = idx + direction if 0 <= new_idx < self._autocomplete_listbox.size(): if current_selection: self._autocomplete_listbox.select_clear(current_selection[0]) self._autocomplete_listbox.select_set(new_idx) self._autocomplete_listbox.activate(new_idx) self._autocomplete_listbox.see(new_idx) return "break" def _on_autocomplete_select(self, event, is_global=False): if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox: return "break" selection = self._autocomplete_listbox.curselection() if not selection: self._close_autocomplete_popup() return "break" selected_text_with_hint = self._autocomplete_listbox.get(selection[0]) # Extraer solo el nombre del ítem, antes de " —" item_name = selected_text_with_hint.split(" —")[0].strip() if is_global: # Eliminar el punto que activó el popup y luego insertar el nombre cursor_pos_str = self.input_text.index(tk.INSERT) # Posición actual (después del punto) line_num, char_num = map(int, cursor_pos_str.split('.')) # El punto está en char_num - 1 en la línea actual dot_pos_on_line = char_num - 1 dot_index_str = f"{line_num}.{dot_pos_on_line}" self.input_text.delete(dot_index_str) # Insertar el nombre de la función/clase seguido de "()" insert_text = item_name + "()" self.input_text.insert(dot_index_str, insert_text) # Colocar cursor dentro de los paréntesis: después del nombre y el '(' self.input_text.mark_set(tk.INSERT, f"{dot_index_str}+{len(item_name)+1}c") else: # Comportamiento existente para métodos de objeto self.input_text.insert(tk.INSERT, item_name + "()") self.input_text.mark_set(tk.INSERT, f"{tk.INSERT}-1c") # Cursor dentro de los paréntesis self._close_autocomplete_popup() self.input_text.focus_set() self.on_key_release() # Trigger re-evaluation return "break" def _close_autocomplete_popup(self): if hasattr(self, '_autocomplete_popup') and self._autocomplete_popup: self._autocomplete_popup.destroy() self._autocomplete_popup = None # Consider unbinding the Button-1 events if they were stored with IDs, # for now, their guard condition `if self._autocomplete_popup:` handles multiple calls. # Example of how to unbind if IDs were stored: # if hasattr(self, '_input_text_b1_bind_id'): # self.input_text.unbind("", self._input_text_b1_bind_id) # del self._input_text_b1_bind_id # if hasattr(self, '_root_b1_bind_id'): # self.root.unbind("", self._root_b1_bind_id) # del self._root_b1_bind_id if hasattr(self, '_autocomplete_listbox') and self._autocomplete_listbox: self._autocomplete_listbox = None def _evaluate_and_update(self): """Evalúa todas las líneas y actualiza la salida""" try: input_content = self.input_text.get("1.0", tk.END) if not input_content.strip(): self._clear_output() return lines = input_content.splitlines() self._evaluate_lines(lines) except Exception as e: self._show_error(f"Error durante evaluación: {e}") def _evaluate_lines(self, lines: List[str]): """Evalúa múltiples líneas de código""" output_data = [] for line_num, line in enumerate(lines, 1): line = line.strip() # Líneas vacías o comentarios if not line or line.startswith('#'): if line: output_data.append([("comment", line)]) else: output_data.append([("", "")]) continue # Evaluar línea result = self.engine.evaluate_line(line) line_output = self._process_evaluation_result(result) output_data.append(line_output) self._display_output(output_data) def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]: """Procesa el resultado de evaluación para display""" output_parts = [] if result.is_error: ayuda = self.obtener_ayuda(result.original_line) if ayuda: # Mostrar ayuda en un solo renglón, truncando si es necesario ayuda_linea = ayuda.replace("\n", " ").replace("\r", " ") if len(ayuda_linea) > 120: ayuda_linea = ayuda_linea[:117] + "..." output_parts.append(("helper", ayuda_linea)) else: output_parts.append(("error", f"Error: {result.error}")) elif result.result_type == "comment": output_parts.append(("comment", result.original_line)) elif result.result_type == "equation_added": output_parts.append(("equation", result.symbolic_result)) elif result.result_type == "assignment": output_parts.append(("info", result.symbolic_result)) else: # Resultado normal if result.result is not None: # Determinar tag basado en tipo tag = self._get_result_tag(result.result) # Verificar si es resultado interactivo if self.interactive_manager and result.is_interactive: interactive_tag, display_text = self.interactive_manager.create_interactive_tag(result.result, self.output_text, "1.0") if interactive_tag: output_parts.append((interactive_tag, display_text)) else: output_parts.append((tag, str(result.result))) else: output_parts.append((tag, str(result.result))) # Añadir pista de clase para el resultado principal primary_result_object = result.result if not isinstance(primary_result_object, PlotResult): # PlotResult ya tiene su propio formato class_display_name = "" if isinstance(primary_result_object, SympyClassBase): class_display_name = type(primary_result_object).__name__.replace("Class_", "") elif isinstance(primary_result_object, sympy.logic.boolalg.BooleanAtom): # sympy.true/false class_display_name = "Boolean" elif isinstance(primary_result_object, sympy.Basic): # Objetos SymPy generales if hasattr(primary_result_object, 'is_number') and primary_result_object.is_number: if hasattr(primary_result_object, 'is_Integer') and primary_result_object.is_Integer: class_display_name = "Integer" elif hasattr(primary_result_object, 'is_Rational') and primary_result_object.is_Rational and not primary_result_object.is_Integer : class_display_name = "Rational" elif hasattr(primary_result_object, 'is_Float') and primary_result_object.is_Float: class_display_name = "Float" else: class_display_name = "SympyNumber" # Otros números de SymPy else: # Expresiones SymPy, símbolos, etc. class_display_name = "Sympy" elif isinstance(primary_result_object, bool): # bool de Python class_display_name = "Boolean" elif isinstance(primary_result_object, (int, float, str, list, dict, tuple, type(None))): class_display_name = type(primary_result_object).__name__.capitalize() if class_display_name == "Nonetype": class_display_name = "None" # Nombres como 'Int', 'Float', 'Str', 'List', 'Dict', 'Tuple' están bien. 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}")) # El espacio se controlará en _display_output # Mostrar información adicional if result.info: output_parts.append(("info", f"({result.info})")) # El espacio se controlará en _display_output return output_parts def _get_result_tag(self, result: Any) -> str: """Determina el tag de color para un resultado""" if isinstance(result, Class_Hex): return "hex" elif isinstance(result, Class_Bin): return "bin" elif isinstance(result, Class_IP4): return "ip" elif isinstance(result, Class_Chr): return "chr_type" elif isinstance(result, sympy.Basic): return "symbolic" else: return "result" def _display_output(self, output_data: List[List[tuple]]): """Muestra los datos de salida en el widget""" self.output_text.config(state="normal") self.output_text.delete("1.0", tk.END) for line_idx, line_parts in enumerate(output_data): # Línea vacía if not line_parts or (len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == ""): pass else: # Mostrar partes de la línea for part_idx, (tag, content) in enumerate(line_parts): if not content: # Omitir contenido vacío continue # Determinar si se necesita un separador antes de esta parte if part_idx > 0: prev_tag, prev_content = line_parts[part_idx-1] if part_idx > 0 else (None, None) # No añadir separador si la parte actual es una "anotación" o si la parte anterior estaba vacía. if tag not in ["class_hint", "numeric", "info"] and prev_content: self.output_text.insert(tk.END, " ; ") # 'numeric' e 'info' necesitan un espacio precedente si siguen a contenido. elif tag in ["numeric", "info"] and prev_content: self.output_text.insert(tk.END, " ") # 'class_hint' se une directamente. if content: # Asegurarse de que hay contenido antes de insertar self.output_text.insert(tk.END, str(content), tag) # 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()