# File: cpp_python_debug/gui/config_window.py # Provides a Toplevel window for configuring application settings. import tkinter as tk from tkinter import ttk, filedialog, messagebox import logging from typing import ( TYPE_CHECKING, Optional, Callable, List, Tuple, Any, ) # Aggiunto Tuple, Any import os import time # Aggiunto per _backup_corrupted_settings_file # MODIFICA: Import assoluti per i moduli del pacchetto if TYPE_CHECKING: from cpp_python_debug.core.config_manager import AppSettings from cpp_python_debug.gui.main_window import ( GDBGui, ) # Usato solo per type hinting del parent logger = logging.getLogger(__name__) class ConfigWindow(tk.Toplevel): """ A Toplevel window for viewing and editing application settings. """ def __init__( self, parent: "GDBGui", app_settings: "AppSettings" ): # Corretto type hint per parent super().__init__(parent) self.parent_window = parent # Mantenuto per reference, anche se 'parent' è già il Toplevel parent self.app_settings = app_settings self.title("Application Configuration") # Recupera il default dalla definizione in AppSettings se la chiave non esiste nel file JSON default_gui_settings = self.app_settings._get_default_settings().get("gui", {}) default_geometry = self.app_settings.get_setting( "gui", "config_window_geometry", default_gui_settings.get( "config_window_geometry", "700x620" ), # Fallback finale ) self.geometry(default_geometry) self.transient(parent) self.grab_set() # StringVars & BooleanVars for editable settings self.gdb_exe_path_var = tk.StringVar() self.gdb_dumper_script_path_var = tk.StringVar() self.timeout_gdb_start_var = tk.StringVar() self.timeout_gdb_command_var = tk.StringVar() self.timeout_program_run_var = tk.StringVar() self.timeout_dump_variable_var = tk.StringVar() self.timeout_kill_program_var = tk.StringVar() self.timeout_gdb_quit_var = tk.StringVar() self.dumper_max_array_elements_var = tk.StringVar() self.dumper_max_recursion_depth_var = tk.StringVar() self.dumper_max_string_length_var = tk.StringVar() self.dumper_dump_diagnostic_json_to_file_var = tk.BooleanVar() self.dumper_diagnostic_json_output_dir_var = tk.StringVar() self._load_current_settings_to_vars() self._create_widgets() self.protocol("WM_DELETE_WINDOW", self._on_closing_button) # self.wait_window() # Rimosso da qui, la finestra chiamante (main_window) farà wait_window def _load_current_settings_to_vars(self): logger.debug("Loading current settings into ConfigWindow variables.") # Ottieni i valori di default una volta per evitare chiamate multiple a _get_default_settings() default_settings = self.app_settings._get_default_settings() default_general = default_settings.get("general", {}) default_timeouts = default_settings.get("timeouts", {}) default_dumper_opts = default_settings.get("dumper_options", {}) self.gdb_exe_path_var.set( self.app_settings.get_setting( "general", "gdb_executable_path", default_general.get("gdb_executable_path"), ) ) self.gdb_dumper_script_path_var.set( self.app_settings.get_setting( "general", "gdb_dumper_script_path", default_general.get("gdb_dumper_script_path"), ) ) self.timeout_gdb_start_var.set( str( self.app_settings.get_setting( "timeouts", "gdb_start", default_timeouts.get("gdb_start") ) ) ) self.timeout_gdb_command_var.set( str( self.app_settings.get_setting( "timeouts", "gdb_command", default_timeouts.get("gdb_command") ) ) ) self.timeout_program_run_var.set( str( self.app_settings.get_setting( "timeouts", "program_run_continue", default_timeouts.get("program_run_continue"), ) ) ) self.timeout_dump_variable_var.set( str( self.app_settings.get_setting( "timeouts", "dump_variable", default_timeouts.get("dump_variable") ) ) ) self.timeout_kill_program_var.set( str( self.app_settings.get_setting( "timeouts", "kill_program", default_timeouts.get("kill_program") ) ) ) self.timeout_gdb_quit_var.set( str( self.app_settings.get_setting( "timeouts", "gdb_quit", default_timeouts.get("gdb_quit") ) ) ) self.dumper_max_array_elements_var.set( str( self.app_settings.get_setting( "dumper_options", "max_array_elements", default_dumper_opts.get("max_array_elements"), ) ) ) self.dumper_max_recursion_depth_var.set( str( self.app_settings.get_setting( "dumper_options", "max_recursion_depth", default_dumper_opts.get("max_recursion_depth"), ) ) ) self.dumper_max_string_length_var.set( str( self.app_settings.get_setting( "dumper_options", "max_string_length", default_dumper_opts.get("max_string_length"), ) ) ) self.dumper_dump_diagnostic_json_to_file_var.set( self.app_settings.get_setting( "dumper_options", "dump_diagnostic_json_to_file", default_dumper_opts.get("dump_diagnostic_json_to_file"), ) ) self.dumper_diagnostic_json_output_dir_var.set( self.app_settings.get_setting( "dumper_options", "diagnostic_json_output_dir", default_dumper_opts.get("diagnostic_json_output_dir"), ) ) def _create_widgets(self): # (Implementazione come prima, ma con `parent=self` per `filedialog`) main_frame = ttk.Frame(self, padding="10") main_frame.pack(expand=True, fill=tk.BOTH) notebook = ttk.Notebook(main_frame) notebook.pack(expand=True, fill=tk.BOTH, pady=5) paths_frame = ttk.Frame(notebook, padding="10") notebook.add(paths_frame, text="Paths & Directories") self._create_paths_widgets_revised(paths_frame) timeouts_frame = ttk.Frame(notebook, padding="10") notebook.add(timeouts_frame, text="Timeouts (s)") self._create_timeouts_widgets(timeouts_frame) dumper_frame = ttk.Frame(notebook, padding="10") notebook.add(dumper_frame, text="Dumper Options") self._create_dumper_widgets_revised(dumper_frame) button_frame = ttk.Frame(main_frame, padding=(0, 10, 0, 0)) button_frame.pack(fill=tk.X, side=tk.BOTTOM) ttk.Button( button_frame, text="Save", command=self._apply_and_save_settings ).pack(side=tk.RIGHT, padx=5) ttk.Button(button_frame, text="Cancel", command=self._cancel).pack( side=tk.RIGHT ) def _create_path_input_group( self, parent: ttk.Frame, row_start: int, label_text: str, textvariable: tk.StringVar, help_text: str, browse_command: Optional[ Callable[[tk.StringVar, str, Optional[List[Tuple[str, Any]]]], None] ] = None, # Corretto tipo Callable browse_filetypes: Optional[List[Tuple[str, Any]]] = None, ) -> int: # (Implementazione come prima) ttk.Label(parent, text=label_text).grid( row=row_start, column=0, columnspan=2, sticky=tk.W, padx=5, pady=(5, 0) ) row_start += 1 entry_frame = ttk.Frame(parent) entry_frame.grid( row=row_start, column=0, columnspan=2, sticky="ew", padx=5, pady=(0, 2) ) entry_frame.columnconfigure(0, weight=1) entry = ttk.Entry(entry_frame, textvariable=textvariable) entry.grid(row=0, column=0, sticky="ew") if browse_command: browse_btn = ttk.Button( entry_frame, text="Browse...", command=lambda tv=textvariable, tl=label_text, ft=browse_filetypes: browse_command( tv, tl, ft ), ) browse_btn.grid(row=0, column=1, padx=(5, 0)) row_start += 1 ttk.Label( parent, text=help_text, foreground="gray", font=("TkDefaultFont", 8) ).grid(row=row_start, column=0, columnspan=2, sticky=tk.W, padx=7, pady=(0, 10)) row_start += 1 return row_start def _create_paths_widgets_revised(self, parent_frame: ttk.Frame): # (Implementazione come prima) paths_config_frame = ttk.LabelFrame( parent_frame, text="Core Paths", padding="10" ) paths_config_frame.pack(expand=True, fill=tk.BOTH, pady=5) paths_config_frame.columnconfigure(0, weight=1) current_row = 0 current_row = self._create_path_input_group( paths_config_frame, current_row, "GDB Executable Path:", self.gdb_exe_path_var, "(Full path to the gdb.exe or gdb command)", browse_command=self._browse_file, browse_filetypes=[("Executable files", "*.exe gdb"), ("All files", "*.*")], ) current_row = self._create_path_input_group( paths_config_frame, current_row, "GDB Python Dumper Script Path (Optional):", self.gdb_dumper_script_path_var, "(Full path to the gdb_dumper.py script. Leave empty if not using advanced JSON dump.)", browse_command=self._browse_file, browse_filetypes=[("Python files", "*.py"), ("All files", "*.*")], ) parent_frame.rowconfigure(current_row, weight=1) def _create_timeouts_widgets(self, parent_frame: ttk.Frame): # (Implementazione come prima) timeouts_config_frame = ttk.LabelFrame( parent_frame, text="GDB Operation Timeouts", padding="10" ) timeouts_config_frame.pack(expand=True, fill=tk.X, pady=5) timeouts_config_frame.columnconfigure(0, weight=0, pad=5) timeouts_config_frame.columnconfigure(1, weight=0, pad=5) timeouts_config_frame.columnconfigure(2, weight=1, pad=5) timeout_settings = [ ( "GDB Start:", self.timeout_gdb_start_var, "Timeout for GDB process to start.", ), ( "GDB Command:", self.timeout_gdb_command_var, "Default timeout for GDB commands.", ), ( "Program Run/Continue:", self.timeout_program_run_var, "Timeout for 'run' or 'continue'.", ), ( "Dump Variable:", self.timeout_dump_variable_var, "Timeout for 'dump_json' operation.", ), ( "Kill Program:", self.timeout_kill_program_var, "Timeout for GDB to confirm 'kill'.", ), ( "GDB Quit:", self.timeout_gdb_quit_var, "Timeout for GDB to exit via 'quit'.", ), ] for i, (label_text, var, tooltip_text) in enumerate(timeout_settings): ttk.Label(timeouts_config_frame, text=label_text).grid( row=i, column=0, sticky=tk.W ) spinbox = ttk.Spinbox( timeouts_config_frame, textvariable=var, from_=1, to=3600, increment=1, width=8, ) spinbox.grid(row=i, column=1, sticky=tk.W) ttk.Label( timeouts_config_frame, text=f"({tooltip_text})", foreground="gray", font=("TkDefaultFont", 8), ).grid(row=i, column=2, sticky=tk.W, padx=5) def _create_dumper_widgets_revised(self, parent_frame: ttk.Frame): # (Implementazione come prima) dumper_config_frame = ttk.LabelFrame( parent_frame, text="GDB Python Dumper Script Options", padding="10" ) dumper_config_frame.pack(expand=True, fill=tk.BOTH, pady=5) dumper_config_frame.columnconfigure(0, weight=1) numeric_options_frame = ttk.Frame(dumper_config_frame) numeric_options_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10)) numeric_options_frame.columnconfigure(0, weight=0, pad=5) numeric_options_frame.columnconfigure(1, weight=0, pad=5) numeric_options_frame.columnconfigure(2, weight=1, pad=5) dumper_settings = [ ( "Max Array Elements:", self.dumper_max_array_elements_var, "Max elements from arrays/containers.", ), ( "Max Recursion Depth:", self.dumper_max_recursion_depth_var, "Max depth for recursive structures.", ), ( "Max String Length:", self.dumper_max_string_length_var, "Max characters from a string.", ), ] for i, (label_text, var, tooltip_text) in enumerate(dumper_settings): ttk.Label(numeric_options_frame, text=label_text).grid( row=i, column=0, sticky=tk.W ) spinbox = ttk.Spinbox( numeric_options_frame, textvariable=var, from_=0, to=100000, increment=1, width=8, ) spinbox.grid(row=i, column=1, sticky=tk.W) ttk.Label( numeric_options_frame, text=f"({tooltip_text})", foreground="gray", font=("TkDefaultFont", 8), ).grid(row=i, column=2, sticky=tk.W, padx=5) current_row = 1 ttk.Checkbutton( dumper_config_frame, text="Enable Diagnostic JSON Dump to File", variable=self.dumper_dump_diagnostic_json_to_file_var, ).grid(row=current_row, column=0, sticky=tk.W, padx=5, pady=(10, 2)) current_row += 1 ttk.Label( dumper_config_frame, text="(For debugging the dumper script. Saves raw JSON from GDB dumper.)", foreground="gray", font=("TkDefaultFont", 8), ).grid(row=current_row, column=0, sticky=tk.W, padx=7, pady=(0, 10)) current_row += 1 current_row = self._create_path_input_group( dumper_config_frame, current_row, "Diagnostic JSON Output Directory:", self.dumper_diagnostic_json_output_dir_var, "(Folder for diagnostic dumps. Leave empty for default in app config dir.)", # Modificato help text browse_command=self._browse_directory, # Ora usa la sua funzione dedicata ) parent_frame.rowconfigure(current_row, weight=1) def _browse_file( self, target_var: tk.StringVar, title: str, filetypes: Optional[List[Tuple[str, Any]]] = None, ): # (Implementazione come prima) current_path = target_var.get() initial_dir = ( os.path.dirname(current_path) if current_path and os.path.isdir(os.path.dirname(current_path)) else None ) # Corretto isdir path = filedialog.askopenfilename( title=title, filetypes=filetypes or [("All files", "*.*")], initialdir=initial_dir, parent=self, ) if path: target_var.set(path) def _browse_directory( self, target_var: tk.StringVar, title: str, _unused_filetypes: Optional[List[Tuple[str, Any]]] = None, ): # Aggiunto _unused per coerenza # (Implementazione come prima) current_path = target_var.get() initial_dir = ( current_path if current_path and os.path.isdir(current_path) else None ) path = filedialog.askdirectory(title=title, initialdir=initial_dir, parent=self) if path: target_var.set(path) def _validate_settings(self) -> bool: # (Implementazione come prima, ma con time importato per _backup_corrupted_settings_file se fosse qui) validations = [ (self.timeout_gdb_start_var, "GDB Start Timeout", 1, 3600), (self.timeout_gdb_command_var, "GDB Command Timeout", 1, 3600), (self.timeout_program_run_var, "Program Run/Continue Timeout", 1, 7200), (self.timeout_dump_variable_var, "Dump Variable Timeout", 1, 3600), (self.timeout_kill_program_var, "Kill Program Timeout", 1, 300), (self.timeout_gdb_quit_var, "GDB Quit Timeout", 1, 300), (self.dumper_max_array_elements_var, "Max Array Elements", 0, 100000), (self.dumper_max_recursion_depth_var, "Max Recursion Depth", 1, 100), (self.dumper_max_string_length_var, "Max String Length", 0, 1048576), ] errors = [] for var, name, min_val, max_val in validations: try: value = int(var.get()) if not (min_val <= value <= max_val): errors.append( f"{name} must be between {min_val}-{max_val}. Got: {value}" ) except ValueError: errors.append(f"{name} must be a valid integer. Got: '{var.get()}'") if not self.gdb_exe_path_var.get(): errors.append("GDB Executable path cannot be empty.") if self.dumper_dump_diagnostic_json_to_file_var.get(): diag_output_dir = self.dumper_diagnostic_json_output_dir_var.get().strip() if ( diag_output_dir ): # Solo se specificato, altrimenti usa default da AppSettings if not os.path.isdir(diag_output_dir): try: os.makedirs(diag_output_dir, exist_ok=True) logger.info( f"Created diagnostic output directory during validation: {diag_output_dir}" ) except OSError as e_create_diag: errors.append( f"Diagnostic JSON Output Dir '{diag_output_dir}' is not valid and could not be created: {e_create_diag}" ) if errors: messagebox.showerror("Validation Error", "\n".join(errors), parent=self) return False return True def _apply_and_save_settings(self): # (Implementazione come prima) logger.info("Attempting to apply and save settings from ConfigWindow.") if not self._validate_settings(): logger.warning("Settings validation failed.") return try: self.app_settings.set_setting( "general", "gdb_executable_path", self.gdb_exe_path_var.get() ) self.app_settings.set_setting( "general", "gdb_dumper_script_path", self.gdb_dumper_script_path_var.get(), ) self.app_settings.set_setting( "timeouts", "gdb_start", int(self.timeout_gdb_start_var.get()) ) self.app_settings.set_setting( "timeouts", "gdb_command", int(self.timeout_gdb_command_var.get()) ) self.app_settings.set_setting( "timeouts", "program_run_continue", int(self.timeout_program_run_var.get()), ) self.app_settings.set_setting( "timeouts", "dump_variable", int(self.timeout_dump_variable_var.get()) ) self.app_settings.set_setting( "timeouts", "kill_program", int(self.timeout_kill_program_var.get()) ) self.app_settings.set_setting( "timeouts", "gdb_quit", int(self.timeout_gdb_quit_var.get()) ) self.app_settings.set_setting( "dumper_options", "max_array_elements", int(self.dumper_max_array_elements_var.get()), ) self.app_settings.set_setting( "dumper_options", "max_recursion_depth", int(self.dumper_max_recursion_depth_var.get()), ) self.app_settings.set_setting( "dumper_options", "max_string_length", int(self.dumper_max_string_length_var.get()), ) self.app_settings.set_setting( "dumper_options", "dump_diagnostic_json_to_file", self.dumper_dump_diagnostic_json_to_file_var.get(), ) self.app_settings.set_setting( "dumper_options", "diagnostic_json_output_dir", self.dumper_diagnostic_json_output_dir_var.get().strip(), ) self.app_settings.set_setting( "gui", "config_window_geometry", self.geometry() ) if self.app_settings.save_settings(): logger.info("Settings applied and saved successfully.") messagebox.showinfo( "Settings Saved", "Configuration has been saved.", parent=self ) self.destroy() else: logger.error("Failed to save settings to file.") messagebox.showerror( "Save Error", "Could not save settings. Check application logs.", parent=self, ) except Exception as e: logger.error(f"Unexpected error applying settings: {e}", exc_info=True) messagebox.showerror( "Internal Error", f"An unexpected error occurred while applying settings: {e}", parent=self, ) def _cancel(self): # (Implementazione come prima) logger.info("Configuration changes cancelled by user.") self.destroy() def _on_closing_button(self): # (Implementazione come prima) logger.debug("ConfigWindow close button (X) pressed.") self._cancel() # Tratta come un cancel