# File: cpp_python_debug/gui/profile_manager_window.py import tkinter as tk from tkinter import ( ttk, messagebox, filedialog, ) # filedialog non sembra usato qui, ma lascio import logging import json # Mantenuto per deep copy e potenziale debug import os import threading import time # Non usato direttamente, ma potrebbe servire in futuro per ritardi from typing import ( TYPE_CHECKING, List, Dict, Any, Optional, Callable, Union, Tuple, ) # Aggiunto Tuple # MODIFICA: Import assoluti from cpp_python_debug.gui.dialogs import ( SymbolAnalysisProgressDialog, SymbolListViewerDialog, FunctionSelectorDialog, ) from cpp_python_debug.core import file_utils # Per calculate_file_checksum from cpp_python_debug.core.symbol_analyzer import SymbolAnalyzer from cpp_python_debug.core.gdb_mi_session import GDBMISession from cpp_python_debug.gui.action_editor_window import ActionEditorWindow if TYPE_CHECKING: from cpp_python_debug.core.config_manager import AppSettings # Evita import diretto di GDBGui per prevenire potenziale import circolare stretto se GDBGui importasse ProfileManagerWindow # Useremo 'tk.Widget' o 'Any' per il parent se GDBGui non è strettamente necessario per type checking qui. # In questo caso, GDBGui è il parent di ProfileManagerWindow, quindi il type hint è utile. from cpp_python_debug.gui.main_window import GDBGui logger = logging.getLogger(__name__) # Logger specifico per questo modulo # Default per una nuova azione, copiato da ActionEditorWindow per coerenza locale # Idealmente, questo potrebbe essere in un modulo di costanti condiviso. DEFAULT_ACTION_CONFIG = { "breakpoint_location": "main", "variables_to_dump": ["my_variable"], "output_format": "json", "output_directory": "./gdb_dumps", # Default generico, potrebbe essere sovrascritto da AppSettings "filename_pattern": "{profile_name}_{app_name}_{breakpoint}_{variable}_{timestamp}.{format}", "continue_after_dump": True, "dump_on_every_hit": True, } DEFAULT_PROFILE_CONFIG = { "profile_name": "New Profile", "target_executable": "", "program_parameters": "", "symbol_analysis": None, # Inizializzato a None "actions": [DEFAULT_ACTION_CONFIG.copy()], # Inizia con una azione di default } class ProfileManagerWindow(tk.Toplevel): # (Costruttore e metodi come prima, con correzioni agli import e type hints) def __init__( self, parent: "GDBGui", app_settings: "AppSettings" ): # Parent è GDBGui super().__init__(parent) self.parent_window = parent # Riferimento al parent (GDBGui) self.app_settings: "AppSettings" = app_settings self.title("Profile Manager") # La geometria potrebbe essere salvata/caricata da AppSettings se desiderato # default_geometry = self.app_settings.get_setting("gui", "profile_manager_geometry", "1050x750") # self.geometry(default_geometry) # Per ora, usiamo una dimensione fissa come nel tuo codice originale window_width = 1050 window_height = 900 self.parent_window.update_idletasks() parent_x = self.parent_window.winfo_x() parent_y = self.parent_window.winfo_y() parent_width = self.parent_window.winfo_width() parent_height = self.parent_window.winfo_height() position_x = max(0, parent_x + (parent_width // 2) - (window_width // 2)) position_y = max(0, parent_y + (parent_height // 2) - (window_height // 2)) self.geometry(f"{window_width}x{window_height}+{position_x}+{position_y}") self.transient(parent) self.grab_set() self._profiles_data: List[Dict[str, Any]] = [] self._selected_profile_index: Optional[int] = None self._selected_action_index_in_profile: Optional[int] = None self._current_profile_modified_in_form: bool = False self._profiles_list_changed_overall: bool = ( False # Traccia se la lista profili è cambiata ) # Variabili Tkinter per il form del profilo self.profile_name_var = tk.StringVar() self.target_exe_var = tk.StringVar() self.program_params_var = tk.StringVar() # Variabili Tkinter per lo stato dell'analisi simboli self._current_profile_target_exe_details_label_var = tk.StringVar( value="Target Executable: N/A" ) self._current_profile_analysis_status_label_var = tk.StringVar( value="Symbol Analysis: Not Performed" ) self.progress_dialog: Optional[SymbolAnalysisProgressDialog] = None # Variabili Tkinter per i conteggi dei simboli self.functions_count_var = tk.StringVar(value="Functions: N/A") self.variables_count_var = tk.StringVar(value="Globals: N/A") self.types_count_var = tk.StringVar(value="Types: N/A") self.sources_count_var = tk.StringVar(value="Sources: N/A") self._load_profiles_from_settings() self._create_widgets() self._populate_profiles_listbox() # Popola la listbox con i profili caricati if self._profiles_data: # Se ci sono profili, seleziona il primo self._select_profile_by_index(0) else: # Altrimenti, aggiorna lo stato per riflettere nessun profilo selezionato self._update_analysis_status_display() # Questo dovrebbe gestire il caso "N/A" self.protocol("WM_DELETE_WINDOW", self._on_closing_button) # Aggiungi trace per marcare modifiche nel form self.profile_name_var.trace_add("write", self._mark_form_as_modified) self.target_exe_var.trace_add( "write", lambda *args: self._on_target_exe_changed_in_form() ) self.program_params_var.trace_add("write", self._mark_form_as_modified) # self.wait_window() # Rimosso, gestito da GDBGui def _mark_form_as_modified(self, *args): # (Implementazione come prima) self._current_profile_modified_in_form = True def _on_target_exe_changed_in_form(self, *args): # (Implementazione come prima) self._mark_form_as_modified(*args) self._update_analysis_status_display() # Aggiorna lo stato dell'analisi quando il target cambia def _load_profiles_from_settings(self) -> None: # (Implementazione come prima, ma con logging e deep copy più espliciti) self._profiles_data = [] # Resetta la lista interna loaded_profiles_from_settings = ( self.app_settings.get_profiles() ) # Questo dovrebbe già restituire una copia for profile_dict_original in loaded_profiles_from_settings: # Crea una deep copy per evitare modifiche all'oggetto in AppSettings # fino a un salvataggio esplicito. json.loads(json.dumps(...)) è un modo semplice per una deep copy. try: copied_profile = json.loads(json.dumps(profile_dict_original)) except (TypeError, json.JSONDecodeError) as e: logger.error( f"Could not deep copy profile, skipping: {profile_dict_original}. Error: {e}" ) continue # Assicura che le chiavi essenziali esistano e siano del tipo corretto if "actions" not in copied_profile or not isinstance( copied_profile["actions"], list ): copied_profile["actions"] = [] if ( "symbol_analysis" not in copied_profile ): # Può essere None se non ancora analizzato copied_profile["symbol_analysis"] = None # Assicura che ogni azione sia un dizionario copied_profile["actions"] = [ action for action in copied_profile["actions"] if isinstance(action, dict) ] self._profiles_data.append(copied_profile) self._profiles_list_changed_overall = ( False # Flag resettato dopo il caricamento iniziale ) logger.debug( f"Loaded {len(self._profiles_data)} profiles into ProfileManagerWindow from AppSettings." ) def _create_widgets(self) -> None: # (Implementazione come prima, ma con parent=self per dialoghi se necessario, # e assicurandosi che i type hint per i callback siano corretti) main_frame = ttk.Frame(self, padding="10") main_frame.pack(expand=True, fill=tk.BOTH) main_frame.columnconfigure( 0, weight=1, minsize=250 ) # Pane sinistro (lista profili) main_frame.columnconfigure(1, weight=3) # Pane destro (dettagli profilo) main_frame.rowconfigure(0, weight=1) # Riga principale per i due pani main_frame.rowconfigure(1, weight=0) # Riga per i bottoni in basso # --- Left Pane: Profiles List and Controls --- left_pane = ttk.Frame(main_frame) left_pane.grid(row=0, column=0, sticky="nsew", padx=(0, 10)) left_pane.rowconfigure(0, weight=1) # Listbox si espande left_pane.rowconfigure(1, weight=0) # Controlli listbox fissi left_pane.columnconfigure(0, weight=1) profiles_list_frame = ttk.LabelFrame(left_pane, text="Profiles", padding="5") profiles_list_frame.grid(row=0, column=0, sticky="nsew", pady=(0, 5)) profiles_list_frame.rowconfigure(0, weight=1) profiles_list_frame.columnconfigure(0, weight=1) self.profiles_listbox = tk.Listbox( profiles_list_frame, exportselection=False, selectmode=tk.SINGLE, width=30 ) self.profiles_listbox.grid(row=0, column=0, sticky="nsew") self.profiles_listbox.bind("<>", self._on_profile_select) listbox_scrollbar_y = ttk.Scrollbar( profiles_list_frame, orient=tk.VERTICAL, command=self.profiles_listbox.yview ) listbox_scrollbar_y.grid(row=0, column=1, sticky="ns") self.profiles_listbox.configure(yscrollcommand=listbox_scrollbar_y.set) profiles_list_controls_frame = ttk.Frame(left_pane) # Non un LabelFrame profiles_list_controls_frame.grid(row=1, column=0, sticky="ew", pady=5) button_width = 10 ttk.Button( profiles_list_controls_frame, text="New", command=self._new_profile, width=button_width, ).pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True) self.duplicate_button = ttk.Button( profiles_list_controls_frame, text="Duplicate", command=self._duplicate_profile, state=tk.DISABLED, width=button_width, ) self.duplicate_button.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True) self.delete_button = ttk.Button( profiles_list_controls_frame, text="Delete", command=self._delete_profile, state=tk.DISABLED, width=button_width, ) self.delete_button.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True) # --- Right Pane: Profile Details, Analysis, Actions --- right_pane = ttk.Frame(main_frame) right_pane.grid(row=0, column=1, sticky="nsew") right_pane.rowconfigure(0, weight=0) # Details Form right_pane.rowconfigure(1, weight=0) # Analysis Control right_pane.rowconfigure(2, weight=0) # Symbols Summary right_pane.rowconfigure(3, weight=1) # Actions UI (espandibile) right_pane.columnconfigure(0, weight=1) # Profile Details Form details_form_frame = ttk.LabelFrame( right_pane, text="Profile Details", padding="10" ) details_form_frame.grid( row=0, column=0, sticky="new", pady=(0, 5) ) # new = North, East, West details_form_frame.columnconfigure( 1, weight=1 ) # Colonna degli entry si espande ttk.Label(details_form_frame, text="Profile Name:").grid( row=0, column=0, sticky=tk.W, padx=5, pady=3 ) self.profile_name_entry = ttk.Entry( details_form_frame, textvariable=self.profile_name_var, state=tk.DISABLED ) self.profile_name_entry.grid( row=0, column=1, columnspan=2, sticky="ew", padx=5, pady=3 ) ttk.Label(details_form_frame, text="Target Executable:").grid( row=1, column=0, sticky=tk.W, padx=5, pady=3 ) self.target_exe_entry = ttk.Entry( details_form_frame, textvariable=self.target_exe_var, state=tk.DISABLED ) self.target_exe_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=3) self.browse_exe_button = ttk.Button( details_form_frame, text="Browse...", command=self._browse_target_executable, state=tk.DISABLED, ) self.browse_exe_button.grid(row=1, column=2, padx=5, pady=3) ttk.Label(details_form_frame, text="Program Parameters:").grid( row=2, column=0, sticky=tk.W, padx=5, pady=3 ) self.program_params_entry = ttk.Entry( details_form_frame, textvariable=self.program_params_var, state=tk.DISABLED ) self.program_params_entry.grid( row=2, column=1, columnspan=2, sticky="ew", padx=5, pady=3 ) # Symbol Analysis Control analysis_control_frame = ttk.LabelFrame( right_pane, text="Symbol Analysis Status & Control", padding="10" ) analysis_control_frame.grid(row=1, column=0, sticky="new", pady=5) analysis_control_frame.columnconfigure(0, weight=1) # Label di stato si espande analysis_control_frame.columnconfigure(1, weight=0) # Bottone fisso self.target_exe_details_label = ttk.Label( analysis_control_frame, textvariable=self._current_profile_target_exe_details_label_var, wraplength=500, justify=tk.LEFT, ) # wraplength per testo lungo self.target_exe_details_label.grid( row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(2, 5) ) self.analysis_status_label = ttk.Label( analysis_control_frame, textvariable=self._current_profile_analysis_status_label_var, foreground="blue", wraplength=350, ) self.analysis_status_label.grid(row=1, column=0, sticky="w", padx=5, pady=2) self.analyse_symbols_button = ttk.Button( analysis_control_frame, text="Analyse Target Symbols", command=self._trigger_symbol_analysis, state=tk.DISABLED, ) self.analyse_symbols_button.grid(row=1, column=1, sticky="e", padx=5, pady=2) # Analyzed Symbols Summary symbols_summary_frame = ttk.LabelFrame( right_pane, text="Analyzed Symbols Summary", padding="10" ) symbols_summary_frame.grid(row=2, column=0, sticky="new", pady=5) symbols_summary_frame.columnconfigure(0, weight=1) symbols_summary_frame.columnconfigure(1, weight=0) symbols_summary_frame.columnconfigure(2, weight=1) symbols_summary_frame.columnconfigure(3, weight=0) # Row 0: Functions and Globals ttk.Label(symbols_summary_frame, textvariable=self.functions_count_var).grid( row=0, column=0, sticky="w", padx=5, pady=2 ) self.view_functions_button = ttk.Button( symbols_summary_frame, text="View...", command=self._view_analyzed_functions, state=tk.DISABLED, width=8, ) self.view_functions_button.grid(row=0, column=1, padx=(5, 10), pady=2) ttk.Label(symbols_summary_frame, textvariable=self.variables_count_var).grid( row=0, column=2, sticky="w", padx=5, pady=2 ) self.view_variables_button = ttk.Button( symbols_summary_frame, text="View...", command=self._view_analyzed_variables, state=tk.DISABLED, width=8, ) self.view_variables_button.grid(row=0, column=3, padx=(5, 10), pady=2) # Row 1: Types and Sources ttk.Label(symbols_summary_frame, textvariable=self.types_count_var).grid( row=1, column=0, sticky="w", padx=5, pady=2 ) self.view_types_button = ttk.Button( symbols_summary_frame, text="View...", command=self._view_analyzed_types, state=tk.DISABLED, width=8, ) self.view_types_button.grid(row=1, column=1, padx=(5, 10), pady=2) ttk.Label(symbols_summary_frame, textvariable=self.sources_count_var).grid( row=1, column=2, sticky="w", padx=5, pady=2 ) self.view_sources_button = ttk.Button( symbols_summary_frame, text="View...", command=self._view_analyzed_sources, state=tk.DISABLED, width=8, ) self.view_sources_button.grid(row=1, column=3, padx=(5, 10), pady=2) # Debug Actions UI actions_ui_frame = ttk.LabelFrame( right_pane, text="Debug Actions", padding="10" ) actions_ui_frame.grid( row=3, column=0, sticky="nsew", pady=5 ) # Modificato da row=4 a row=3 actions_ui_frame.rowconfigure(0, weight=1) # Listbox si espande actions_ui_frame.columnconfigure(0, weight=1) # Listbox si espande actions_ui_frame.columnconfigure(1, weight=0) # Scrollbar actions_ui_frame.columnconfigure(2, weight=0) # Bottoni laterali self.actions_listbox = tk.Listbox( actions_ui_frame, exportselection=False, selectmode=tk.SINGLE, height=10 ) self.actions_listbox.grid( row=0, column=0, sticky="nsew", pady=5, padx=(0, 5) ) # padx a dx per scrollbar self.actions_listbox.bind( "<>", self._on_action_select_in_listbox ) actions_listbox_scrolly = ttk.Scrollbar( actions_ui_frame, orient=tk.VERTICAL, command=self.actions_listbox.yview ) actions_listbox_scrolly.grid( row=0, column=1, sticky="ns", pady=5 ) # pady=5 per allineare self.actions_listbox.configure(yscrollcommand=actions_listbox_scrolly.set) action_buttons_frame = ttk.Frame(actions_ui_frame) # Non LabelFrame action_buttons_frame.grid( row=0, column=2, sticky="ns", padx=(5, 0), pady=5 ) # Allineato con listbox action_btn_width = 8 self.add_action_button = ttk.Button( action_buttons_frame, text="Add...", command=self._add_action, state=tk.DISABLED, width=action_btn_width, ) self.add_action_button.pack(fill=tk.X, pady=2, anchor="n") self.edit_action_button = ttk.Button( action_buttons_frame, text="Edit...", command=self._edit_action, state=tk.DISABLED, width=action_btn_width, ) self.edit_action_button.pack(fill=tk.X, pady=2, anchor="n") self.remove_action_button = ttk.Button( action_buttons_frame, text="Remove", command=self._remove_action, state=tk.DISABLED, width=action_btn_width, ) self.remove_action_button.pack(fill=tk.X, pady=2, anchor="n") # --- Bottom Buttons (Save All, Close) --- bottom_buttons_frame = ttk.Frame(main_frame) bottom_buttons_frame.grid( row=1, column=0, columnspan=2, sticky="sew", pady=(10, 0) ) # columnspan=2 per coprire entrambi i pani # Frame interno per allineare i bottoni a destra bottom_buttons_inner_frame = ttk.Frame(bottom_buttons_frame) bottom_buttons_inner_frame.pack( side=tk.RIGHT ) # Allinea il frame dei bottoni a destra self.save_button = ttk.Button( bottom_buttons_inner_frame, text="Save All Changes", command=self._save_all_profiles_to_settings, ) self.save_button.pack( side=tk.LEFT, padx=5, pady=5 ) # Usare LEFT per ordine corretto se il frame è pack(RIGHT) ttk.Button( bottom_buttons_inner_frame, text="Close", command=self._on_closing_button ).pack(side=tk.LEFT, padx=5, pady=5) def _populate_profiles_listbox(self) -> None: # (Implementazione come prima) self.profiles_listbox.delete(0, tk.END) for i, profile in enumerate(self._profiles_data): display_name = profile.get("profile_name", f"Unnamed Profile {i+1}") self.profiles_listbox.insert(tk.END, display_name) self._update_profile_action_buttons_state() # Aggiorna stato bottoni New/Dup/Del self._update_action_buttons_state() # Aggiorna stato bottoni Add/Edit/Remove Action def _on_profile_select(self, event: Optional[tk.Event] = None) -> None: # (Implementazione come prima) selection_indices = self.profiles_listbox.curselection() new_selected_index: Optional[int] = None if selection_indices: new_selected_index = selection_indices[0] if ( self._selected_profile_index is not None and self._selected_profile_index != new_selected_index and self._current_profile_modified_in_form ): current_profile_name_in_form = self.profile_name_var.get() if ( not current_profile_name_in_form and self._profiles_data and 0 <= self._selected_profile_index < len(self._profiles_data) ): current_profile_name_in_form = self._profiles_data[ self._selected_profile_index ].get("profile_name", "the previously selected profile") prompt = ( f"Profile '{current_profile_name_in_form}' has unsaved changes in the form.\n" "Do you want to apply these changes before switching?" ) response = messagebox.askyesnocancel( "Unsaved Changes", prompt, parent=self, default=messagebox.CANCEL ) if response is True: # Yes, save if not self._save_current_form_to_profile_data( self._selected_profile_index ): # Save failed, revert selection in listbox self.profiles_listbox.selection_clear(0, tk.END) if ( self._selected_profile_index is not None ): # Check again as it might have been cleared self.profiles_listbox.selection_set( self._selected_profile_index ) self.profiles_listbox.activate(self._selected_profile_index) return # Abort switch elif response is None: # Cancelled switch # Revert selection in listbox to the previously selected one self.profiles_listbox.selection_clear(0, tk.END) if self._selected_profile_index is not None: self.profiles_listbox.selection_set(self._selected_profile_index) self.profiles_listbox.activate(self._selected_profile_index) return # Abort switch # If response is False (No, discard), proceed with selection change # Proceed with selection change if new_selected_index is not None: self._select_profile_by_index(new_selected_index) else: # No selection or selection cleared self._clear_profile_form() self._selected_profile_index = None # Explicitly set to None self._update_profile_action_buttons_state() self._update_analysis_status_display() # Aggiorna anche qui def _get_data_from_form( self, ) -> Dict[str, Any]: # Non usata direttamente, ma utile per coerenza # (Implementazione come prima) return { "profile_name": self.profile_name_var.get(), "target_executable": self.target_exe_var.get(), "program_parameters": self.program_params_var.get(), } def _select_profile_by_index(self, index: int) -> None: # (Implementazione come prima) if not (0 <= index < len(self._profiles_data)): logger.warning(f"Attempted to select invalid profile index: {index}") self._clear_profile_form() self._selected_profile_index = None return self._selected_profile_index = index profile = self._profiles_data[index] self.profile_name_var.set(profile.get("profile_name", "")) self.target_exe_var.set(profile.get("target_executable", "")) self.program_params_var.set(profile.get("program_parameters", "")) self._populate_actions_listbox() # Popola azioni per il profilo selezionato self._enable_profile_form_editing(True) self._current_profile_modified_in_form = False # Resetta flag dopo caricamento # Sincronizza la selezione della listbox se non già fatto dall'evento if ( not self.profiles_listbox.curselection() or self.profiles_listbox.curselection()[0] != index ): self.profiles_listbox.selection_clear(0, tk.END) self.profiles_listbox.selection_set(index) self.profiles_listbox.activate(index) self.profiles_listbox.see(index) self.add_action_button.config(state=tk.NORMAL) # Abilita aggiunta azioni self._update_analysis_status_display() # Aggiorna stato analisi def _clear_profile_form(self) -> None: # (Implementazione come prima) self.profile_name_var.set("") self.target_exe_var.set("") self.program_params_var.set("") self._populate_actions_listbox() # Svuota la lista azioni self._enable_profile_form_editing(False) self._current_profile_modified_in_form = False self.add_action_button.config(state=tk.DISABLED) self.edit_action_button.config(state=tk.DISABLED) self.remove_action_button.config(state=tk.DISABLED) self._update_analysis_status_display() # Resetta anche lo stato dell'analisi def _enable_profile_form_editing(self, enable: bool) -> None: # (Implementazione come prima) state = tk.NORMAL if enable else tk.DISABLED self.profile_name_entry.config(state=state) self.target_exe_entry.config(state=state) self.browse_exe_button.config(state=state) self.program_params_entry.config(state=state) def _update_profile_action_buttons_state(self) -> None: # (Implementazione come prima) profile_selected = self._selected_profile_index is not None self.duplicate_button.config( state=tk.NORMAL if profile_selected else tk.DISABLED ) self.delete_button.config(state=tk.NORMAL if profile_selected else tk.DISABLED) def _browse_target_executable(self) -> None: # (Implementazione come prima) current_path = self.target_exe_var.get() initial_dir = ( os.path.dirname(current_path) if current_path and os.path.exists(os.path.dirname(current_path)) else None ) path = filedialog.askopenfilename( title="Select Target Executable", filetypes=[("Executable files", ("*.exe", "*")), ("All files", "*.*")], initialdir=initial_dir, parent=self, ) if path: self.target_exe_var.set( path ) # Questo attiverà _on_target_exe_changed_in_form def _save_current_form_to_profile_data(self, profile_index: int) -> bool: # (Implementazione come prima, ma con controllo indice migliorato) if not (self._profiles_data and 0 <= profile_index < len(self._profiles_data)): logger.error(f"Invalid profile index {profile_index} for saving form data.") return False profile_name = self.profile_name_var.get().strip() if not profile_name: messagebox.showerror( "Validation Error", "Profile Name cannot be empty.", parent=self ) self.profile_name_entry.focus_set() return False for i, p_item in enumerate(self._profiles_data): if i != profile_index and p_item.get("profile_name") == profile_name: messagebox.showerror( "Validation Error", f"Profile name '{profile_name}' already exists. Names must be unique.", parent=self, ) self.profile_name_entry.focus_set() return False target_profile_in_list = self._profiles_data[profile_index] old_name = target_profile_in_list.get("profile_name") old_target_exe = target_profile_in_list.get("target_executable") new_target_exe = self.target_exe_var.get().strip() target_profile_in_list["profile_name"] = profile_name target_profile_in_list["target_executable"] = new_target_exe target_profile_in_list["program_parameters"] = ( self.program_params_var.get() ) # Strip non necessario se è vuoto self._current_profile_modified_in_form = False # Form salvato nei dati interni self._profiles_list_changed_overall = True if old_name != profile_name: # Aggiorna la listbox se il nome è cambiato self.profiles_listbox.delete(profile_index) self.profiles_listbox.insert(profile_index, profile_name) self.profiles_listbox.selection_set(profile_index) # Riseleziona if ( old_target_exe != new_target_exe ): # Se il target exe è cambiato, l'analisi precedente è invalidata logger.info( f"Target executable changed for profile '{profile_name}'. Invalidating previous symbol analysis." ) target_profile_in_list["symbol_analysis"] = ( None # Invalida i dati di analisi ) self._update_analysis_status_display() # Aggiorna UI per riflettere questo logger.info( f"Profile '{profile_name}' (index {profile_index}) details from form saved to internal list." ) return True def _new_profile(self) -> None: # (Implementazione come prima) if ( self._selected_profile_index is not None and self._current_profile_modified_in_form ): current_profile_name_display = self.profile_name_var.get() or ( self._profiles_data[self._selected_profile_index].get("profile_name") if self._profiles_data and 0 <= self._selected_profile_index < len(self._profiles_data) else "current profile" ) prompt = ( f"Profile '{current_profile_name_display}' has unsaved changes in the form.\n" "Do you want to save them before creating a new profile?" ) response = messagebox.askyesnocancel( "Unsaved Changes", prompt, default=messagebox.CANCEL, parent=self ) if response is True: if not self._save_current_form_to_profile_data( self._selected_profile_index ): return elif response is None: return new_p = json.loads( json.dumps(DEFAULT_PROFILE_CONFIG) ) # Usa DEFAULT_PROFILE_CONFIG base_name = new_p["profile_name"] name_candidate = base_name count = 1 existing_names = {p.get("profile_name") for p in self._profiles_data} while name_candidate in existing_names: name_candidate = f"{base_name} ({count})" count += 1 new_p["profile_name"] = name_candidate self._profiles_data.append(new_p) self._profiles_list_changed_overall = True self._populate_profiles_listbox() self._select_profile_by_index(len(self._profiles_data) - 1) self.profile_name_entry.focus_set() self.profile_name_entry.selection_range(0, tk.END) self._mark_form_as_modified() def _duplicate_profile(self) -> None: # (Implementazione come prima) if self._selected_profile_index is None: return if self._current_profile_modified_in_form: if not self._save_current_form_to_profile_data( self._selected_profile_index ): return original_profile = self._profiles_data[self._selected_profile_index] duplicated_profile = json.loads(json.dumps(original_profile)) base_name = f"{original_profile.get('profile_name', 'Profile')}_copy" name_candidate = base_name count = 1 existing_names = {p.get("profile_name") for p in self._profiles_data} while name_candidate in existing_names: name_candidate = f"{base_name}_{count}" count += 1 duplicated_profile["profile_name"] = name_candidate self._profiles_data.append(duplicated_profile) self._profiles_list_changed_overall = True self._populate_profiles_listbox() self._select_profile_by_index(len(self._profiles_data) - 1) self._mark_form_as_modified() def _delete_profile(self) -> None: # (Implementazione come prima) if self._selected_profile_index is None or not self._profiles_data: return profile_to_delete = self._profiles_data[self._selected_profile_index] profile_name = profile_to_delete.get("profile_name", "this profile") if not messagebox.askyesno( "Confirm Delete", f"Are you sure you want to delete profile '{profile_name}'?", parent=self, ): return idx_to_delete = self._selected_profile_index del self._profiles_data[idx_to_delete] self._profiles_list_changed_overall = True new_list_size = len(self._profiles_data) self._selected_profile_index = None # Resetta prima di ripopolare self._populate_profiles_listbox() if new_list_size == 0: self._clear_profile_form() else: new_selection_idx = min(idx_to_delete, new_list_size - 1) if new_selection_idx >= 0: self._select_profile_by_index(new_selection_idx) else: self._clear_profile_form() def _save_all_profiles_to_settings(self) -> None: # (Implementazione come prima) if ( self._selected_profile_index is not None and self._current_profile_modified_in_form ): if not self._save_current_form_to_profile_data( self._selected_profile_index ): messagebox.showerror( "Save Error", "Could not save changes from the form for the selected profile. Aborting save to settings.", parent=self, ) return profile_names_seen = set() for i, profile in enumerate(self._profiles_data): name = profile.get("profile_name", "").strip() if not name: messagebox.showerror( "Validation Error", f"Profile at index {i} (0-based) has an empty name.", parent=self, ) if self._selected_profile_index != i: self._select_profile_by_index(i) self.profile_name_entry.focus_set() return if name in profile_names_seen: messagebox.showerror( "Validation Error", f"Duplicate profile name '{name}' found.", parent=self, ) first_occurrence_index = next( ( idx for idx, p in enumerate(self._profiles_data) if p.get("profile_name") == name ), -1, ) if ( first_occurrence_index != -1 and self._selected_profile_index != first_occurrence_index ): self._select_profile_by_index(first_occurrence_index) self.profile_name_entry.focus_set() return profile_names_seen.add(name) actions = profile.get("actions") if not isinstance(actions, list): messagebox.showerror( "Data Error", f"Profile '{name}' has malformed actions (not a list).", parent=self, ) return for idx_a, action in enumerate(actions): if not isinstance(action, dict): messagebox.showerror( "Data Error", f"Profile '{name}', action #{idx_a+1} is malformed (not a dict).", parent=self, ) return self.app_settings.set_profiles(self._profiles_data) if self.app_settings.save_settings(): logger.info( f"All {len(self._profiles_data)} profiles saved to AppSettings." ) messagebox.showinfo( "Profiles Saved", "All profile changes have been saved.", parent=self ) self._current_profile_modified_in_form = False self._profiles_list_changed_overall = False else: messagebox.showerror( "Save Error", "Could not save profiles to the settings file. Check logs.", parent=self, ) def _on_closing_button(self) -> None: # (Implementazione come prima) needs_save_prompt = False prompt_message = "" if ( self._selected_profile_index is not None and self._current_profile_modified_in_form ): needs_save_prompt = True profile_name = self.profile_name_var.get() or ( self._profiles_data[self._selected_profile_index].get("profile_name") if self._profiles_data and 0 <= self._selected_profile_index < len(self._profiles_data) else "current profile" ) prompt_message = ( f"Profile '{profile_name}' has unsaved changes in the form.\n" ) if self._profiles_list_changed_overall: needs_save_prompt = True prompt_message += ( "Additionally, the overall list of profiles (or their content) has changed.\n" if prompt_message else "The list of profiles (or their content) has changed.\n" ) if needs_save_prompt: prompt_message += "Do you want to save all changes before closing?" response = messagebox.askyesnocancel( "Unsaved Changes", prompt_message, default=messagebox.CANCEL, parent=self, ) if response is True: self._save_all_profiles_to_settings() elif response is None: return if self.progress_dialog and self.progress_dialog.winfo_exists(): logger.warning( "Closing ProfileManagerWindow while symbol analysis dialog open." ) if hasattr(self.parent_window, "focus_set") and callable( self.parent_window.focus_set ): self.parent_window.focus_set() self.destroy() # --- Metodi per la gestione delle Azioni (Listbox, Add, Edit, Remove) --- def _populate_actions_listbox(self) -> None: # (Implementazione come prima) self.actions_listbox.delete(0, tk.END) self._selected_action_index_in_profile = None if ( self._selected_profile_index is not None and 0 <= self._selected_profile_index < len(self._profiles_data) ): profile = self._profiles_data[self._selected_profile_index] actions = profile.get("actions", []) for i, action in enumerate(actions): bp = action.get("breakpoint_location", "N/A")[:30] num_vars = len(action.get("variables_to_dump", [])) fmt = action.get("output_format", "N/A") cont = "Yes" if action.get("continue_after_dump", False) else "No" dump_freq = ( "Always" if action.get("dump_on_every_hit", True) else "Once" ) summary = ( f"BP: {bp}{'...' if len(action.get('breakpoint_location', '')) > 30 else ''} " f"(Vars:{num_vars}, Fmt:{fmt}, Cont:{cont}, Hit:{dump_freq})" ) self.actions_listbox.insert(tk.END, summary) self._update_action_buttons_state() def _on_action_select_in_listbox(self, event: Optional[tk.Event] = None) -> None: # (Implementazione come prima) selection_indices = self.actions_listbox.curselection() if selection_indices: self._selected_action_index_in_profile = selection_indices[0] else: self._selected_action_index_in_profile = None self._update_action_buttons_state() def _update_action_buttons_state(self) -> None: # (Implementazione come prima) profile_selected = self._selected_profile_index is not None action_selected = self._selected_action_index_in_profile is not None self.add_action_button.config( state=tk.NORMAL if profile_selected else tk.DISABLED ) self.edit_action_button.config( state=tk.NORMAL if action_selected else tk.DISABLED ) self.remove_action_button.config( state=tk.NORMAL if action_selected else tk.DISABLED ) def _add_action(self) -> None: # (Implementazione come prima, ma con type hint e logica AppSettings corretta) if self._selected_profile_index is None: return if self._current_profile_modified_in_form: if not self._save_current_form_to_profile_data( self._selected_profile_index ): return current_profile = self._profiles_data[self._selected_profile_index] current_profile_target_exe = current_profile.get("target_executable", "") current_profile_program_params = current_profile.get("program_parameters", "") symbol_analysis_data = current_profile.get("symbol_analysis") editor = ActionEditorWindow( self, action_data=None, is_new=True, target_executable_path=current_profile_target_exe, app_settings=self.app_settings, # Passa l'istanza corretta symbol_analysis_data=symbol_analysis_data, program_parameters_for_scope=current_profile_program_params, ) self.wait_window(editor) # Rendi modale new_action_data = editor.get_result() if new_action_data: if "actions" not in current_profile or not isinstance( current_profile["actions"], list ): current_profile["actions"] = [] current_profile["actions"].append(new_action_data) self._profiles_list_changed_overall = True self._populate_actions_listbox() self.actions_listbox.selection_set(tk.END) self._on_action_select_in_listbox() def _edit_action(self) -> None: # (Implementazione come prima, ma con type hint e logica AppSettings corretta) if ( self._selected_profile_index is None or self._selected_action_index_in_profile is None ): return if self._current_profile_modified_in_form: if not self._save_current_form_to_profile_data( self._selected_profile_index ): return current_profile = self._profiles_data[self._selected_profile_index] actions_list = current_profile.get("actions", []) if not (0 <= self._selected_action_index_in_profile < len(actions_list)): logger.error("Selected action index out of bounds.") return action_to_edit = actions_list[self._selected_action_index_in_profile] current_profile_target_exe = current_profile.get("target_executable", "") current_profile_program_params = current_profile.get("program_parameters", "") symbol_analysis_data = current_profile.get("symbol_analysis") editor = ActionEditorWindow( self, action_data=action_to_edit, is_new=False, target_executable_path=current_profile_target_exe, app_settings=self.app_settings, # Passa l'istanza corretta symbol_analysis_data=symbol_analysis_data, program_parameters_for_scope=current_profile_program_params, ) self.wait_window(editor) # Rendi modale updated_action_data = editor.get_result() if updated_action_data: current_profile["actions"][ self._selected_action_index_in_profile ] = updated_action_data self._profiles_list_changed_overall = True idx_to_reselect = self._selected_action_index_in_profile self._populate_actions_listbox() if ( idx_to_reselect is not None and 0 <= idx_to_reselect < self.actions_listbox.size() ): self.actions_listbox.selection_set(idx_to_reselect) self._on_action_select_in_listbox() def _remove_action(self) -> None: # (Implementazione come prima) if ( self._selected_profile_index is None or self._selected_action_index_in_profile is None ): return profile = self._profiles_data[self._selected_profile_index] action_summary_to_delete = self.actions_listbox.get( self._selected_action_index_in_profile ) if not messagebox.askyesno( "Confirm Delete Action", f"Delete action?\n\n{action_summary_to_delete}", parent=self, ): return actions_list = profile.get("actions") if isinstance( actions_list, list ) and 0 <= self._selected_action_index_in_profile < len(actions_list): del actions_list[self._selected_action_index_in_profile] self._profiles_list_changed_overall = True idx_to_reselect_after_delete = self._selected_action_index_in_profile self._populate_actions_listbox() num_actions_remaining = len(actions_list) if num_actions_remaining > 0: new_selection = min( idx_to_reselect_after_delete, num_actions_remaining - 1 ) if new_selection >= 0: self.actions_listbox.selection_set(new_selection) self._on_action_select_in_listbox() else: logger.error("Could not remove action: list missing or index invalid.") # --- Metodi per Symbol Analysis (come prima, ma con type hint e logica AppSettings corretta) --- def _update_analysis_status_display(self) -> None: # (Implementazione come prima, ma assicurati che file_utils sia importato per calculate_file_checksum) if self._selected_profile_index is None or not ( 0 <= self._selected_profile_index < len(self._profiles_data) ): self._current_profile_target_exe_details_label_var.set( "Target Executable: N/A (No profile selected)" ) self._current_profile_analysis_status_label_var.set( "Symbol Analysis: Select a profile." ) self.analyse_symbols_button.config(state=tk.DISABLED) self.functions_count_var.set("Functions: N/A") self.view_functions_button.config(state=tk.DISABLED) self.variables_count_var.set("Globals: N/A") self.view_variables_button.config(state=tk.DISABLED) self.types_count_var.set("Types: N/A") self.view_types_button.config(state=tk.DISABLED) self.sources_count_var.set("Sources: N/A") self.view_sources_button.config(state=tk.DISABLED) return profile = self._profiles_data[self._selected_profile_index] target_exe_in_form = self.target_exe_var.get().strip() exe_display_name = ( os.path.basename(target_exe_in_form) if target_exe_in_form else "N/A" ) details_text_lines = [f"Target in Form: {exe_display_name}"] status_text = "Symbol Analysis: " status_color = "blue" # Default color funcs_count_text = "Functions: N/A" view_funcs_btn_state = tk.DISABLED vars_count_text = "Globals: N/A" view_vars_btn_state = tk.DISABLED types_count_text = "Types: N/A" view_types_btn_state = tk.DISABLED sources_count_text = "Sources: N/A" view_sources_btn_state = tk.DISABLED analysis_button_state = tk.DISABLED if target_exe_in_form and os.path.isfile(target_exe_in_form): analysis_button_state = tk.NORMAL # Can analyse if exe path is valid if not target_exe_in_form: status_text += "Target executable not specified in form." elif not os.path.isfile(target_exe_in_form): status_text += f"Target '{exe_display_name}' (from form) not found on disk." status_color = "red" else: # Target in form is a valid file analysis_data = profile.get("symbol_analysis") if analysis_data and isinstance(analysis_data, dict): symbols_dict = analysis_data.get("symbols", {}) num_functions = symbols_dict.get("functions_count", 0) funcs_count_text = f"Functions: {num_functions}" if num_functions > 0: view_funcs_btn_state = tk.NORMAL num_variables = symbols_dict.get("global_variables_count", 0) vars_count_text = f"Globals: {num_variables}" if num_variables > 0: view_vars_btn_state = tk.NORMAL num_types = symbols_dict.get("types_count", 0) types_count_text = f"Types: {num_types}" if num_types > 0: view_types_btn_state = tk.NORMAL num_sources = symbols_dict.get("source_files_count", 0) sources_count_text = f"Sources: {num_sources}" if num_sources > 0: view_sources_btn_state = tk.NORMAL saved_exe_at_analysis = analysis_data.get( "analyzed_executable_path", "Unknown" ) details_text_lines.append( f"Last Analysis on File: {os.path.basename(saved_exe_at_analysis)}" ) details_text_lines.append( f" File Timestamp (at analysis): {analysis_data.get('executable_timestamp', 'N/A')}" ) details_text_lines.append( f" Analysis Date: {analysis_data.get('analysis_timestamp', 'N/A')}" ) saved_checksum = analysis_data.get("executable_checksum") details_text_lines.append( f" Saved Checksum: {saved_checksum or 'N/A'}" ) current_checksum_for_form_exe = file_utils.calculate_file_checksum( target_exe_in_form ) details_text_lines.append( f" Current Form Exe Checksum: {current_checksum_for_form_exe or 'N/A (calc failed)'}" ) if os.path.normpath(saved_exe_at_analysis) != os.path.normpath( target_exe_in_form ): status_text += ( "TARGET CHANGED since last analysis. RE-ANALYSIS RECOMMENDED." ) status_color = "orange red" view_funcs_btn_state = view_vars_btn_state = ( view_types_btn_state ) = view_sources_btn_state = tk.DISABLED elif ( saved_checksum and current_checksum_for_form_exe and saved_checksum == current_checksum_for_form_exe ): status_text += "Up-to-date." status_color = "dark green" elif saved_checksum and current_checksum_for_form_exe: # Mismatch status_text += ( "EXECUTABLE CHANGED (checksum mismatch). RE-ANALYSIS REQUIRED." ) status_color = "red" view_funcs_btn_state = view_vars_btn_state = ( view_types_btn_state ) = view_sources_btn_state = tk.DISABLED else: # Checksum missing or calc failed for current status_text += "Status unclear (checksums). Consider re-analysing." status_color = "orange" # Mantieni i bottoni view abilitati se c'è data, ma con avviso else: status_text += "Not performed. Click 'Analyse' to generate." status_color = "blue" self.analyse_symbols_button.config(state=analysis_button_state) self._current_profile_target_exe_details_label_var.set( "\n".join(details_text_lines) ) self._current_profile_analysis_status_label_var.set(status_text) self.analysis_status_label.config(foreground=status_color) self.functions_count_var.set(funcs_count_text) self.view_functions_button.config(state=view_funcs_btn_state) self.variables_count_var.set(vars_count_text) self.view_variables_button.config(state=view_vars_btn_state) self.types_count_var.set(types_count_text) self.view_types_button.config(state=view_types_btn_state) self.sources_count_var.set(sources_count_text) self.view_sources_button.config(state=view_sources_btn_state) def _trigger_symbol_analysis(self) -> None: # (Implementazione come prima, assicurati che SymbolAnalyzer sia importato correttamente) if self._selected_profile_index is None: messagebox.showerror("Error", "No profile selected.", parent=self) return target_exe_for_analysis = self.target_exe_var.get().strip() if not target_exe_for_analysis or not os.path.isfile(target_exe_for_analysis): messagebox.showerror( "Error", "Target executable path in the form is invalid.", parent=self ) self._update_analysis_status_display() 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 configured or not found: {gdb_exe_path}", parent=self, ) return profile_to_update = self._profiles_data[self._selected_profile_index] self.progress_dialog = SymbolAnalysisProgressDialog( self ) # parent è self (ProfileManagerWindow) # Passa GDBMISession come classe per l'analisi symbol_analyzer = SymbolAnalyzer( gdb_exe_path, self.app_settings, gdb_session_class=GDBMISession ) analysis_thread = threading.Thread( target=self._perform_symbol_analysis_thread, args=( profile_to_update, target_exe_for_analysis, symbol_analyzer, self.progress_dialog, ), daemon=True, ) analysis_thread.start() def _perform_symbol_analysis_thread( self, profile_to_update: Dict[str, Any], target_exe_path: str, symbol_analyzer: SymbolAnalyzer, progress_dialog: SymbolAnalysisProgressDialog, ): # (Implementazione come prima) analysis_data_dict: Dict[str, Any] = {} analysis_succeeded_overall = False def gui_log(msg: str): if progress_dialog and progress_dialog.winfo_exists(): self.after(0, progress_dialog.log_message, msg) def gui_set_status(msg: str): if progress_dialog and progress_dialog.winfo_exists(): self.after(0, progress_dialog.set_status, msg) try: gui_log( f"Starting symbol analysis for: {os.path.basename(target_exe_path)}" ) gui_set_status(f"Analyzing {os.path.basename(target_exe_path)}...") analysis_data_dict = symbol_analyzer.analyze( target_exe_path=target_exe_path, progress_callback=gui_log, status_callback=gui_set_status, ) if analysis_data_dict: analysis_succeeded_overall = True gui_set_status("Symbol analysis successfully completed.") gui_log("\nSymbol analysis successfully completed.") else: gui_set_status("Symbol analysis failed. Check logs.") gui_log("\nSymbol analysis failed.") except Exception as e: logger.error( f"Error in symbol analysis thread for '{target_exe_path}': {e}", exc_info=True, ) error_msg = f"THREAD ERROR: {type(e).__name__} - {e}" gui_log(f"\n{error_msg}") gui_set_status(error_msg) finally: if self.winfo_exists(): # Assicura che ProfileManagerWindow esista ancora self.after( 0, self._finalize_symbol_analysis, profile_to_update, analysis_data_dict, analysis_succeeded_overall, progress_dialog, ) def _finalize_symbol_analysis( self, profile_to_update: Dict[str, Any], analysis_data: Dict[str, Any], success: bool, progress_dialog: SymbolAnalysisProgressDialog, ): # (Implementazione come prima) if success: profile_to_update["symbol_analysis"] = analysis_data self._profiles_list_changed_overall = True logger.info( f"Symbol analysis data updated for profile: '{profile_to_update.get('profile_name')}'." ) if self.winfo_exists(): messagebox.showinfo( "Analysis Complete", "Symbol analysis has finished successfully.", parent=self, ) else: logger.error( f"Symbol analysis failed for profile: '{profile_to_update.get('profile_name')}'." ) if self.winfo_exists(): messagebox.showerror( "Analysis Failed", "Symbol analysis did not complete successfully. Check logs.", parent=self, ) if progress_dialog and progress_dialog.winfo_exists(): progress_dialog.analysis_complete_or_failed(success) if self.winfo_exists(): self._update_analysis_status_display() def _get_symbols_for_display( self, category: str ) -> List[Union[str, Dict[str, Any]]]: # (Implementazione come prima) if self._selected_profile_index is None or not ( 0 <= self._selected_profile_index < len(self._profiles_data) ): return [] profile = self._profiles_data[self._selected_profile_index] analysis_data = profile.get("symbol_analysis") if not analysis_data or not isinstance(analysis_data.get("symbols"), dict): return [] return analysis_data["symbols"].get(category, []) def _view_analyzed_functions(self) -> None: # (Come prima) functions_list = self._get_symbols_for_display("functions") if not functions_list: messagebox.showinfo( "No Functions", "No functions from analysis.", parent=self ) return self._show_symbol_list_dialog("Functions", functions_list) def _view_analyzed_variables(self) -> None: # (Come prima) variables_list = self._get_symbols_for_display("global_variables") if not variables_list: messagebox.showinfo( "No Global Variables", "No globals from analysis.", parent=self ) return self._show_symbol_list_dialog("Global Variables", variables_list) def _view_analyzed_types(self) -> None: # (Come prima) types_list = self._get_symbols_for_display("types") if not types_list: messagebox.showinfo("No Types", "No types from analysis.", parent=self) return self._show_symbol_list_dialog("Types", types_list) def _view_analyzed_sources(self) -> None: # (Come prima) source_files_list = self._get_symbols_for_display("source_files") if not source_files_list: messagebox.showinfo( "No Source Files", "No sources from analysis.", parent=self ) return self._show_symbol_list_dialog("Source Files", source_files_list) def _show_symbol_list_dialog( self, symbol_type: str, symbols: List[Union[str, Dict[str, Any]]], ) -> None: # (Implementazione come prima, ma assicurati che file_utils sia importato per calculate_file_checksum) if self._selected_profile_index is None: return target_exe_in_form = self.target_exe_var.get().strip() # Usa il valore del form profile = self._profiles_data[self._selected_profile_index] analysis_data = profile.get("symbol_analysis") if not analysis_data: messagebox.showerror("Error", "No analysis data available.", parent=self) return analyzed_exe_path = analysis_data.get("analyzed_executable_path", "") exe_name_for_title = ( os.path.basename(target_exe_in_form) if target_exe_in_form else "Unknown Target" ) is_obsolete = True if ( target_exe_in_form and os.path.isfile(target_exe_in_form) and os.path.normpath(analyzed_exe_path) == os.path.normpath(target_exe_in_form) ): current_checksum = file_utils.calculate_file_checksum( target_exe_in_form ) # USA file_utils saved_checksum = analysis_data.get("executable_checksum") if ( current_checksum and saved_checksum and current_checksum == saved_checksum ): is_obsolete = False title_suffix = " (Analysis data might be obsolete)" if is_obsolete else "" dialog_title = ( f"Analyzed {symbol_type} for '{exe_name_for_title}'{title_suffix}" ) # SymbolListViewerDialog è già importato SymbolListViewerDialog(self, symbols, title=dialog_title)