1501 lines
63 KiB
Python
1501 lines
63 KiB
Python
# 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 = 750
|
|
|
|
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("<<ListboxSelect>>", 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) # Label si espande
|
|
symbols_summary_frame.columnconfigure(1, weight=0) # Bottone fisso
|
|
|
|
row_s = 0
|
|
ttk.Label(symbols_summary_frame, textvariable=self.functions_count_var).grid(
|
|
row=row_s, 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=row_s, column=1, padx=(10, 5), pady=2, sticky="w"
|
|
)
|
|
row_s += 1
|
|
ttk.Label(symbols_summary_frame, textvariable=self.variables_count_var).grid(
|
|
row=row_s, column=0, 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=row_s, column=1, padx=(10, 5), pady=2, sticky="w"
|
|
)
|
|
row_s += 1
|
|
ttk.Label(symbols_summary_frame, textvariable=self.types_count_var).grid(
|
|
row=row_s, 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=row_s, column=1, padx=(10, 5), pady=2, sticky="w"
|
|
)
|
|
row_s += 1
|
|
ttk.Label(symbols_summary_frame, textvariable=self.sources_count_var).grid(
|
|
row=row_s, column=0, 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=row_s, column=1, padx=(10, 5), pady=2, sticky="w"
|
|
)
|
|
# row_s += 1 # Non necessario se è l'ultimo
|
|
|
|
# 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=6
|
|
)
|
|
self.actions_listbox.grid(
|
|
row=0, column=0, sticky="nsew", pady=5, padx=(0, 5)
|
|
) # padx a dx per scrollbar
|
|
self.actions_listbox.bind(
|
|
"<<ListboxSelect>>", 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)
|