# 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 import os if TYPE_CHECKING: from ..core.config_manager import ( AppSettings, ) # To avoid circular import for type hinting from .main_window import GDBGui # For parent type hint 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"): """ Initializes the configuration window. Args: parent: The parent Tkinter window. app_settings: The AppSettings instance managing configurations. """ super().__init__(parent) self.parent_window = parent self.app_settings = app_settings self.title("Application Configuration") # Load geometry from AppSettings, provide a default default_geometry = "650x550" # Adjusted for more content self.geometry( self.app_settings.get_setting( "gui", "config_window_geometry", default_geometry ) ) # Make the window modal (optional, but common for config dialogs) self.transient(parent) # Keep on top of parent self.grab_set() # Direct all events to this window # StringVars for editable settings # General Paths self.gdb_exe_path_var = tk.StringVar() self.gdb_dumper_script_path_var = tk.StringVar() # Timeouts (will use Spinbox or validated Entry) 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() # Dumper Options 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._load_current_settings_to_vars() self._create_widgets() self.protocol( "WM_DELETE_WINDOW", self._on_closing_button ) # Handle window X button def _load_current_settings_to_vars(self): """Loads settings from AppSettings into the Tkinter StringVars.""" logger.debug("Loading current settings into ConfigWindow StringVars.") # General self.gdb_exe_path_var.set( self.app_settings.get_setting("general", "gdb_executable_path", "gdb") ) self.gdb_dumper_script_path_var.set( self.app_settings.get_setting("general", "gdb_dumper_script_path", "") ) # Timeouts self.timeout_gdb_start_var.set( str(self.app_settings.get_setting("timeouts", "gdb_start", 30)) ) self.timeout_gdb_command_var.set( str(self.app_settings.get_setting("timeouts", "gdb_command", 30)) ) self.timeout_program_run_var.set( str(self.app_settings.get_setting("timeouts", "program_run_continue", 120)) ) self.timeout_dump_variable_var.set( str(self.app_settings.get_setting("timeouts", "dump_variable", 60)) ) self.timeout_kill_program_var.set( str(self.app_settings.get_setting("timeouts", "kill_program", 20)) ) self.timeout_gdb_quit_var.set( str(self.app_settings.get_setting("timeouts", "gdb_quit", 10)) ) # Dumper Options self.dumper_max_array_elements_var.set( str( self.app_settings.get_setting( "dumper_options", "max_array_elements", 100 ) ) ) self.dumper_max_recursion_depth_var.set( str( self.app_settings.get_setting( "dumper_options", "max_recursion_depth", 10 ) ) ) self.dumper_max_string_length_var.set( str( self.app_settings.get_setting( "dumper_options", "max_string_length", 2048 ) ) ) def _create_widgets(self): """Creates and lays out widgets for the configuration window.""" 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 Tab --- paths_frame = ttk.Frame(notebook, padding="10") notebook.add(paths_frame, text="Paths") self._create_paths_widgets(paths_frame) # --- Timeouts Tab --- timeouts_frame = ttk.Frame(notebook, padding="10") notebook.add(timeouts_frame, text="Timeouts (seconds)") self._create_timeouts_widgets(timeouts_frame) # --- Dumper Tab --- dumper_frame = ttk.Frame(notebook, padding="10") notebook.add(dumper_frame, text="Dumper Options") self._create_dumper_widgets(dumper_frame) # --- Buttons 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 ) # ttk.Button(button_frame, text="Apply", command=self._apply_settings).pack(side=tk.RIGHT, padx=5) # Optional Apply def _create_paths_widgets(self, parent_frame: ttk.Frame): """Creates widgets for path configurations.""" paths_config_frame = ttk.LabelFrame( parent_frame, text="Executable and Script Paths", padding="10" ) paths_config_frame.pack(expand=True, fill=tk.X, pady=5) paths_config_frame.columnconfigure(1, weight=1) row_idx = 0 # GDB Executable ttk.Label(paths_config_frame, text="GDB Executable:").grid( row=row_idx, column=0, sticky=tk.W, padx=5, pady=3 ) ttk.Entry(paths_config_frame, textvariable=self.gdb_exe_path_var).grid( row=row_idx, column=1, sticky=(tk.W, tk.E), padx=5, pady=3 ) ttk.Button( paths_config_frame, text="Browse...", command=lambda: self._browse_file( self.gdb_exe_path_var, "Select GDB Executable", [("Executable files", "*.exe"), ("All files", "*.*")], ), ).grid(row=row_idx, column=2, padx=5, pady=3) row_idx += 1 # GDB Dumper Script ttk.Label(paths_config_frame, text="GDB Dumper Script:").grid( row=row_idx, column=0, sticky=tk.W, padx=5, pady=3 ) ttk.Entry( paths_config_frame, textvariable=self.gdb_dumper_script_path_var ).grid(row=row_idx, column=1, sticky=(tk.W, tk.E), padx=5, pady=3) ttk.Button( paths_config_frame, text="Browse...", command=lambda: self._browse_file( self.gdb_dumper_script_path_var, "Select GDB Python Dumper Script", [("Python files", "*.py"), ("All files", "*.*")], ), ).grid(row=row_idx, column=2, padx=5, pady=3) # row_idx += 1 # Add other path-related settings here if any in the future def _create_timeouts_widgets(self, parent_frame: ttk.Frame): """Creates widgets for timeout configurations using Spinbox for integer input.""" 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(1, weight=1) # Allow Spinboxes to align timeout_settings = [ ( "GDB Start:", self.timeout_gdb_start_var, "Timeout for GDB process to start and respond.", ), ( "GDB Command:", self.timeout_gdb_command_var, "Default timeout for generic GDB commands.", ), ( "Program Run/Continue:", self.timeout_program_run_var, "Timeout for 'run' or 'continue' commands.", ), ( "Dump Variable:", self.timeout_dump_variable_var, "Timeout for the 'dump_json' operation.", ), ( "Kill Program:", self.timeout_kill_program_var, "Timeout for GDB to confirm program termination via 'kill'.", ), ( "GDB Quit:", self.timeout_gdb_quit_var, "Timeout for GDB to exit after 'quit' command.", ), ] 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, padx=5, pady=3 ) # Using Spinbox for integer input with a reasonable range spinbox = ttk.Spinbox( timeouts_config_frame, textvariable=var, from_=1, to=3600, increment=1, width=10, ) spinbox.grid( row=i, column=1, sticky=tk.W, padx=5, pady=3 ) # Changed to sticky W if ( tooltip_text ): # Simple tooltip (can be enhanced with a dedicated tooltip library) ttk.Label( timeouts_config_frame, text=f"({tooltip_text})", foreground="gray", font=("TkDefaultFont", 8), ).grid(row=i, column=2, sticky=tk.W, padx=5, pady=3) def _create_dumper_widgets(self, parent_frame: ttk.Frame): """Creates widgets for GDB dumper script configurations.""" dumper_config_frame = ttk.LabelFrame( parent_frame, text="GDB Python Dumper Script Options", padding="10" ) dumper_config_frame.pack(expand=True, fill=tk.X, pady=5) dumper_config_frame.columnconfigure(1, weight=1) dumper_settings = [ ( "Max Array Elements:", self.dumper_max_array_elements_var, "Max elements to dump from arrays/containers.", ), ( "Max Recursion Depth:", self.dumper_max_recursion_depth_var, "Max depth for recursive data structures.", ), ( "Max String Length:", self.dumper_max_string_length_var, "Max characters to dump from a string.", ), ] for i, (label_text, var, tooltip_text) in enumerate(dumper_settings): ttk.Label(dumper_config_frame, text=label_text).grid( row=i, column=0, sticky=tk.W, padx=5, pady=3 ) spinbox = ttk.Spinbox( dumper_config_frame, textvariable=var, from_=0, to=10000, increment=1, width=10, ) # 0 might mean unlimited for some spinbox.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3) if tooltip_text: ttk.Label( dumper_config_frame, text=f"({tooltip_text})", foreground="gray", font=("TkDefaultFont", 8), ).grid(row=i, column=2, sticky=tk.W, padx=5, pady=3) def _browse_file(self, target_var: tk.StringVar, title: str, filetypes=None): """Helper to browse for a file and set the target_var.""" current_path = target_var.get() initial_dir = tk.NONE # Default to no initial dir if current_path: if os.path.isdir(current_path): initial_dir = current_path elif os.path.exists(os.path.dirname(current_path)): initial_dir = os.path.dirname(current_path) path = filedialog.askopenfilename( title=title, filetypes=filetypes or [("All files", "*.*")], initialdir=( initial_dir if initial_dir else None ), # Pass None if not determined parent=self, # Ensure dialog is on top of this window ) if path: target_var.set(path) def _validate_settings(self) -> bool: """ Validates the current values in the StringVars. Shows error messages if validation fails. Returns: bool: True if all settings are valid, False otherwise. """ 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, ), # Longer for program run (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, ), # 0 could mean "no direct limit by this script" (self.dumper_max_recursion_depth_var, "Max Recursion Depth", 1, 100), ( self.dumper_max_string_length_var, "Max String Length", 0, 1048576, ), # Up to 1MB ] 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} and {max_val}. Got: {value}" ) except ValueError: errors.append(f"{name} must be a valid integer. Got: '{var.get()}'") # Path validations (existence is not strictly enforced here, but could be) if not self.gdb_exe_path_var.get(): # GDB exe path is mandatory errors.append("GDB Executable path cannot be empty.") # Dumper script path can be empty. if errors: messagebox.showerror("Validation Error", "\n".join(errors), parent=self) return False return True def _apply_and_save_settings(self): """Validates, applies settings to AppSettings, saves, and informs the user.""" logger.info("Attempting to apply and save settings from ConfigWindow.") if not self._validate_settings(): logger.warning("Settings validation failed.") return try: # Update AppSettings instance # General 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(), ) # Timeouts 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()) ) # Dumper Options 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()), ) # GUI (save current config window geometry) 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 ) # Inform parent window (GDBGui) to re-read relevant settings if necessary # For example, if GDB path changed, main window might need to update its display or behavior # This could be done via a callback or by the parent explicitly re-checking after this window closes. # For now, the main window reads most settings on its own startup or when actions are performed. # GDB path for example is read when "Start GDB" is clicked. # However, if the main window was displaying the GDB path from settings, it should update. # Let's assume the parent will re-evaluate critical settings as needed. # The main window's StringVars for paths are NOT directly linked to AppSettings after init, # so if these are changed here, the main window UI won't reflect it until it's closed and reopened, # or if we implement a mechanism to refresh them. # For simplicity now, changes here will take effect next time main window needs them or on app restart. self.destroy() # Close the config window after saving else: logger.error("Failed to save settings to file.") messagebox.showerror( "Save Error", "Could not save settings to the configuration file. Check logs.", parent=self, ) except ( ValueError ) as ve: # Should be caught by _validate_settings, but as a safeguard logger.error(f"ValueError during settings application: {ve}", exc_info=True) messagebox.showerror( "Internal Error", f"Error applying settings: {ve}", 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): """Closes the window without saving changes.""" logger.info("Configuration changes cancelled by user.") # Optionally, if you want to check for unsaved changes: # if self._settings_changed(): # if not messagebox.askyesno("Confirm Cancel", "Discard changes and close?", parent=self): # return self.destroy() def _on_closing_button(self): """Handles the event when the window's 'X' (close) button is pressed.""" # Treat same as cancel for simplicity, or ask user to save if changes were made. logger.debug("ConfigWindow close button pressed.") self._cancel() # Optional: Method to check if settings were changed from their initial loaded state # def _settings_changed(self) -> bool: # # Compare current var values with what was initially loaded from app_settings # # This is more complex as you need to store initial values or re-compare with app_settings # return True # Placeholder