# File: cpp_python_debug/gui/action_editor_window.py import tkinter as tk from tkinter import ttk, scrolledtext, messagebox, filedialog # Aggiunto filedialog se non c'era import logging import os # Aggiunto os from typing import Dict, Any, Optional, List # Import per la nuova funzionalità from ..core.gdb_controller import GDBSession from ..core.config_manager import AppSettings # Per ottenere gdb_path, timeouts, etc. logger = logging.getLogger(__name__) class FunctionSelectorDialog(tk.Toplevel): """Dialog to select a function from a list.""" def __init__(self, parent, functions_list: List[str], title="Select Function"): super().__init__(parent) self.title(title) self.geometry("500x400") self.transient(parent) self.grab_set() self.result: Optional[str] = None list_frame = ttk.Frame(self, padding="10") list_frame.pack(expand=True, fill=tk.BOTH) list_frame.rowconfigure(0, weight=1) list_frame.columnconfigure(0, weight=1) self.listbox = tk.Listbox(list_frame, selectmode=tk.SINGLE) self.listbox.grid(row=0, column=0, sticky="nsew") scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.listbox.yview) scrollbar.grid(row=0, column=1, sticky="ns") self.listbox.configure(yscrollcommand=scrollbar.set) for func_name in functions_list: self.listbox.insert(tk.END, func_name) self.listbox.bind("", self._on_ok) button_frame = ttk.Frame(self, padding=(10,0,10,10)) button_frame.pack(fill=tk.X) ttk.Button(button_frame, text="OK", command=self._on_ok).pack(side=tk.RIGHT, padx=5) ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT) if functions_list: self.listbox.selection_set(0) # Seleziona il primo elemento self.listbox.focus_set() self.protocol("WM_DELETE_WINDOW", self._on_cancel) self.wait_window() def _on_ok(self, event=None): # Aggiunto event=None per il double-click selection = self.listbox.curselection() if selection: self.result = self.listbox.get(selection[0]) self.destroy() def _on_cancel(self): self.result = None self.destroy() class ActionEditorWindow(tk.Toplevel): def __init__(self, parent: tk.Widget, action_data: Optional[Dict[str, Any]] = None, is_new: bool = True, target_executable_path: Optional[str] = None, # NUOVO PARAMETRO app_settings: Optional[AppSettings] = None): # NUOVO PARAMETRO super().__init__(parent) self.parent_window = parent self.is_new_action = is_new self.result: Optional[Dict[str, Any]] = None # --- NUOVI ATTRIBUTI --- self.target_executable_path = target_executable_path self.app_settings = app_settings # Necessario per gdb_path, timeouts # --- FINE NUOVI ATTRIBUTI --- title = "Add New Action" if self.is_new_action else "Edit Action" self.title(title) self.geometry("650x580") # Aumentato leggermente per il nuovo bottone self.resizable(False, False) self.transient(parent) self.grab_set() self.breakpoint_var = tk.StringVar() self.output_format_var = tk.StringVar() self.output_directory_var = tk.StringVar() self.filename_pattern_var = tk.StringVar() self.continue_after_dump_var = tk.BooleanVar() self._initial_action_data = action_data.copy() if action_data else None self._create_widgets() self._load_action_data(action_data) self.protocol("WM_DELETE_WINDOW", self._on_cancel) self.wait_window() def _create_widgets(self) -> None: main_frame = ttk.Frame(self, padding="15") main_frame.pack(expand=True, fill=tk.BOTH) main_frame.columnconfigure(1, weight=1) # Colonna per gli input si espande row_idx = 0 # --- Breakpoint Location (Layout Corretto) --- ttk.Label(main_frame, text="Breakpoint Location:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5) bp_input_frame = ttk.Frame(main_frame) # Frame per Entry e Bottone Browse bp_input_frame.grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5) bp_input_frame.columnconfigure(0, weight=1) # L'Entry si espande dentro questo frame self.bp_entry = ttk.Entry(bp_input_frame, textvariable=self.breakpoint_var) self.bp_entry.grid(row=0, column=0, sticky="ew", padx=(0,5)) # padx a destra per spaziare dal bottone self.browse_funcs_button = ttk.Button(bp_input_frame, text="Browse Functions...", command=self._browse_functions) self.browse_funcs_button.grid(row=0, column=1, sticky=tk.E) # Bottone a destra dell'entry # Stato iniziale del bottone "Browse Functions..." if not self.target_executable_path or not self.app_settings or \ not os.path.isfile(self.target_executable_path): # Aggiunto controllo isfile self.browse_funcs_button.config(state=tk.DISABLED) if self.target_executable_path and not os.path.isfile(self.target_executable_path): logger.warning(f"Target executable '{self.target_executable_path}' not found, disabling function browser.") row_idx += 1 # --- Fine Breakpoint Location --- bp_help_label = ttk.Label(main_frame, text="(e.g., main, file.cpp:123, MyClass::foo)", foreground="gray") bp_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10)) row_idx += 1 # Variables to Dump ttk.Label(main_frame, text="Variables to Dump:").grid(row=row_idx, column=0, sticky="nw", padx=5, pady=5) self.variables_text = scrolledtext.ScrolledText(main_frame, wrap=tk.WORD, height=5, width=58, font=("Consolas", 9)) # width era 58 self.variables_text.grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5) row_idx += 1 vars_help_label = ttk.Label(main_frame, text="(One variable/expression per line)", foreground="gray") vars_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10)) row_idx += 1 # Output Format ttk.Label(main_frame, text="Output Format:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5) self.output_format_combo = ttk.Combobox(main_frame, textvariable=self.output_format_var, values=["json", "csv"], state="readonly", width=10) self.output_format_combo.grid(row=row_idx, column=1, sticky="w", padx=5, pady=5) # columnspan rimosso, non necessario row_idx += 1 # Output Directory ttk.Label(main_frame, text="Output Directory:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5) self.output_dir_entry = ttk.Entry(main_frame, textvariable=self.output_directory_var) # width rimosso per farlo espandere con la colonna self.output_dir_entry.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=5) ttk.Button(main_frame, text="Browse...", command=self._browse_output_dir).grid(row=row_idx, column=2, padx=5, pady=5) row_idx += 1 # Filename Pattern ttk.Label(main_frame, text="Filename Pattern:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5) self.filename_pattern_entry = ttk.Entry(main_frame, textvariable=self.filename_pattern_var) # width rimosso self.filename_pattern_entry.grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5) row_idx += 1 pattern_help_label = ttk.Label(main_frame, text="(Placeholders: {profile_name}, {breakpoint}, {variable}, {timestamp}, {format})", foreground="gray") pattern_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10)) row_idx += 1 # Continue After Dump self.continue_check = ttk.Checkbutton(main_frame, text="Continue execution after dump", variable=self.continue_after_dump_var) self.continue_check.grid(row=row_idx, column=0, columnspan=3, sticky="w", padx=5, pady=10) # sticky w row_idx += 1 # Buttons Frame buttons_frame = ttk.Frame(main_frame) buttons_frame.grid(row=row_idx, column=0, columnspan=3, pady=(15,5), sticky="e") # sticky e ttk.Button(buttons_frame, text="OK", command=self._on_ok, width=10).pack(side=tk.RIGHT, padx=5) ttk.Button(buttons_frame, text="Cancel", command=self._on_cancel, width=10).pack(side=tk.RIGHT, padx=5) def _load_action_data(self, action_data: Optional[Dict[str, Any]]) -> None: # Importa DEFAULT_ACTION qui se non è già globale o un attributo di classe # Dobbiamo importarlo da ProfileManagerWindow o definirlo qui. Per ora, assumiamo sia disponibile. # Idealmente, DEFAULT_ACTION dovrebbe essere in un modulo `defaults` o simile. # Assumiamo per ora che sia stato passato correttamente o che ProfileManagerWindow lo gestisca. # Per questo esempio, lo ridefinisco localmente se necessario. _DEFAULT_ACTION_LOCAL = { "breakpoint_location": "main", "variables_to_dump": ["my_variable"], "output_format": "json", "output_directory": "./debug_dumps", "filename_pattern": "{profile_name}_{breakpoint}_{variable}_{timestamp}.{format}", "continue_after_dump": True } data_to_load = action_data if action_data else _DEFAULT_ACTION_LOCAL.copy() self.breakpoint_var.set(data_to_load.get("breakpoint_location", _DEFAULT_ACTION_LOCAL["breakpoint_location"])) variables_list = data_to_load.get("variables_to_dump", _DEFAULT_ACTION_LOCAL["variables_to_dump"]) if isinstance(variables_list, list): self.variables_text.insert(tk.END, "\n".join(variables_list)) else: self.variables_text.insert(tk.END, str(variables_list)) self.output_format_var.set(data_to_load.get("output_format", _DEFAULT_ACTION_LOCAL["output_format"])) self.output_directory_var.set(data_to_load.get("output_directory", _DEFAULT_ACTION_LOCAL["output_directory"])) self.filename_pattern_var.set(data_to_load.get("filename_pattern", _DEFAULT_ACTION_LOCAL["filename_pattern"])) self.continue_after_dump_var.set(data_to_load.get("continue_after_dump", _DEFAULT_ACTION_LOCAL["continue_after_dump"])) def _browse_output_dir(self) -> None: # Invariato current_path = self.output_directory_var.get() initial_dir = current_path if current_path and os.path.isdir(current_path) else None path = filedialog.askdirectory( title="Select Output Directory for Dumps", initialdir=initial_dir, parent=self ) if path: self.output_directory_var.set(path) def _validate_data(self) -> bool: # Invariato if not self.breakpoint_var.get().strip(): messagebox.showerror("Validation Error", "Breakpoint Location cannot be empty.", parent=self) return False variables_str = self.variables_text.get("1.0", tk.END).strip() if not variables_str: messagebox.showerror("Validation Error", "Variables to Dump cannot be empty.", parent=self) return False if not self.output_directory_var.get().strip(): messagebox.showerror("Validation Error", "Output Directory cannot be empty.", parent=self) return False if not self.filename_pattern_var.get().strip(): messagebox.showerror("Validation Error", "Filename Pattern cannot be empty.", parent=self) return False if not any(p in self.filename_pattern_var.get() for p in ["{breakpoint}", "{variable}", "{timestamp}"]): messagebox.showwarning("Validation Warning", "Filename Pattern seems to be missing common placeholders like {breakpoint}, {variable}, or {timestamp}. This might lead to overwritten files.", parent=self) return True def _on_ok(self) -> None: # Invariato if not self._validate_data(): return variables_list = [line.strip() for line in self.variables_text.get("1.0", tk.END).strip().splitlines() if line.strip()] if not variables_list : messagebox.showerror("Validation Error", "Variables to Dump cannot be empty after processing.", parent=self) return self.result = { "breakpoint_location": self.breakpoint_var.get().strip(), "variables_to_dump": variables_list, "output_format": self.output_format_var.get(), "output_directory": self.output_directory_var.get().strip(), "filename_pattern": self.filename_pattern_var.get().strip(), "continue_after_dump": self.continue_after_dump_var.get() } self.destroy() def _on_cancel(self) -> None: # Invariato self.result = None self.destroy() def get_result(self) -> Optional[Dict[str, Any]]: # Invariato return self.result # --- NUOVA FUNZIONE --- def _browse_functions(self) -> None: if not self.target_executable_path or not os.path.isfile(self.target_executable_path): messagebox.showerror("Error", "Target executable for the profile is not set or invalid. Cannot browse functions.", parent=self) return if not self.app_settings: messagebox.showerror("Error", "Application settings not available. Cannot determine GDB path.", parent=self) return gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path") if not gdb_exe_path or not os.path.isfile(gdb_exe_path): messagebox.showerror("GDB Error", f"GDB executable not found or not configured: {gdb_exe_path}", parent=self) return # Mostra un messaggio di attesa self.config(cursor="watch") self.update_idletasks() temp_gdb_session: Optional[GDBSession] = None functions_list: List[str] = [] error_occurred = False try: logger.info(f"Creating temporary GDBSession for function listing. Target: {self.target_executable_path}") # Creiamo una sessione GDBSession senza script dumper e senza opzioni dumper, # serve solo per 'info functions'. temp_gdb_session = GDBSession( gdb_path=gdb_exe_path, executable_path=self.target_executable_path, gdb_script_full_path=None, # Non serve lo script dumper per info functions dumper_options={} ) # Usiamo timeout brevi per l'avvio e il comando startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30) // 2 # Più breve command_timeout = self.app_settings.get_setting("timeouts", "gdb_command", 30) // 2 # Più breve logger.debug("Starting temporary GDB session...") temp_gdb_session.start(timeout=startup_timeout) # Avvia GDB if not temp_gdb_session.symbols_found: messagebox.showwarning("No Debug Symbols", "GDB reported no debugging symbols found in the executable. " "The function list might be empty or incomplete.", parent=self) # Continuiamo comunque, GDB potrebbe listare alcuni simboli non-debugging. logger.debug("Listing functions...") functions_list = temp_gdb_session.list_functions(timeout=command_timeout) except FileNotFoundError as fnf_e: logger.error(f"Error during function browsing (FileNotFound): {fnf_e}", exc_info=True) messagebox.showerror("Error", f"File not found during GDB interaction: {fnf_e}", parent=self) error_occurred = True except (ConnectionError, TimeoutError) as session_e: logger.error(f"Session error during function browsing: {session_e}", exc_info=True) messagebox.showerror("GDB Error", f"Could not communicate with GDB to list functions: {session_e}", parent=self) error_occurred = True except Exception as e: logger.error(f"Unexpected error during function browsing: {e}", exc_info=True) messagebox.showerror("Error", f"An unexpected error occurred: {e}", parent=self) error_occurred = True finally: if temp_gdb_session and temp_gdb_session.is_alive(): logger.debug("Quitting temporary GDB session...") quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit", 10) // 2 try: temp_gdb_session.quit(timeout=quit_timeout) except Exception as e_quit: logger.error(f"Error quitting temporary GDB session: {e_quit}") self.config(cursor="") # Ripristina cursore if not error_occurred: if functions_list: dialog = FunctionSelectorDialog(self, functions_list) selected_function = dialog.result if selected_function: self.breakpoint_var.set(selected_function) else: messagebox.showinfo("No Functions", "No functions found or GDB did not return any.", parent=self)