327 lines
18 KiB
Python
327 lines
18 KiB
Python
# 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
|
|
import os
|
|
|
|
if TYPE_CHECKING:
|
|
from ..core.config_manager import AppSettings
|
|
from .main_window import GDBGui
|
|
|
|
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"):
|
|
super().__init__(parent)
|
|
self.parent_window = parent
|
|
self.app_settings = app_settings
|
|
|
|
self.title("Application Configuration")
|
|
default_geometry = self.app_settings.get_setting("gui", "config_window_geometry", "700x620") # Default se non trovato
|
|
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()
|
|
|
|
# Renamed and default changed
|
|
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)
|
|
|
|
def _load_current_settings_to_vars(self):
|
|
logger.debug("Loading current settings into ConfigWindow variables.")
|
|
default_settings = self.app_settings._get_default_settings() # Access defaults for fallbacks
|
|
|
|
self.gdb_exe_path_var.set(
|
|
self.app_settings.get_setting("general", "gdb_executable_path", default_settings["general"]["gdb_executable_path"])
|
|
)
|
|
self.gdb_dumper_script_path_var.set(
|
|
self.app_settings.get_setting("general", "gdb_dumper_script_path", default_settings["general"]["gdb_dumper_script_path"])
|
|
)
|
|
|
|
self.timeout_gdb_start_var.set(str(self.app_settings.get_setting("timeouts", "gdb_start", default_settings["timeouts"]["gdb_start"])))
|
|
self.timeout_gdb_command_var.set(str(self.app_settings.get_setting("timeouts", "gdb_command", default_settings["timeouts"]["gdb_command"])))
|
|
self.timeout_program_run_var.set(str(self.app_settings.get_setting("timeouts", "program_run_continue", default_settings["timeouts"]["program_run_continue"])))
|
|
self.timeout_dump_variable_var.set(str(self.app_settings.get_setting("timeouts", "dump_variable", default_settings["timeouts"]["dump_variable"])))
|
|
self.timeout_kill_program_var.set(str(self.app_settings.get_setting("timeouts", "kill_program", default_settings["timeouts"]["kill_program"])))
|
|
self.timeout_gdb_quit_var.set(str(self.app_settings.get_setting("timeouts", "gdb_quit", default_settings["timeouts"]["gdb_quit"])))
|
|
|
|
self.dumper_max_array_elements_var.set(str(self.app_settings.get_setting("dumper_options", "max_array_elements", default_settings["dumper_options"]["max_array_elements"])))
|
|
self.dumper_max_recursion_depth_var.set(str(self.app_settings.get_setting("dumper_options", "max_recursion_depth", default_settings["dumper_options"]["max_recursion_depth"])))
|
|
self.dumper_max_string_length_var.set(str(self.app_settings.get_setting("dumper_options", "max_string_length", default_settings["dumper_options"]["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_settings["dumper_options"]["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_settings["dumper_options"]["diagnostic_json_output_dir"])
|
|
)
|
|
|
|
def _create_widgets(self):
|
|
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) # Revised layout
|
|
|
|
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) # Revised layout for diagnostic dir
|
|
|
|
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[[], None]] = None,
|
|
browse_filetypes: Optional[List[Tuple[str, Any]]] = None) -> int:
|
|
"""Helper to create a consistent path input group."""
|
|
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: browse_command(textvariable, label_text, browse_filetypes))
|
|
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):
|
|
paths_config_frame = ttk.LabelFrame(parent_frame, text="Core Paths", padding="10")
|
|
paths_config_frame.pack(expand=True, fill=tk.BOTH, pady=5) # Fill BOTH
|
|
paths_config_frame.columnconfigure(0, weight=1) # Allow the inner content to expand
|
|
|
|
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", "*.*")]
|
|
)
|
|
# Add a spacer to push subsequent content down if paths_config_frame expands
|
|
parent_frame.rowconfigure(current_row, weight=1)
|
|
|
|
|
|
def _create_timeouts_widgets(self, parent_frame: ttk.Frame):
|
|
timeouts_config_frame = ttk.LabelFrame(parent_frame, text="GDB Operation Timeouts", padding="10")
|
|
timeouts_config_frame.pack(expand=True, fill=tk.X, pady=5)
|
|
# Grid configuration for alignment
|
|
timeouts_config_frame.columnconfigure(0, weight=0, pad=5) # Label column
|
|
timeouts_config_frame.columnconfigure(1, weight=0, pad=5) # Spinbox column
|
|
timeouts_config_frame.columnconfigure(2, weight=1, pad=5) # Tooltip column (stretches)
|
|
|
|
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):
|
|
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) # Fill BOTH
|
|
dumper_config_frame.columnconfigure(0, weight=1) # Allow inner content to expand
|
|
|
|
# Numeric Dumper Options (similar to timeouts)
|
|
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)
|
|
|
|
# Diagnostic Dump Options (new layout)
|
|
current_row = 1 # Start after numeric options frame
|
|
|
|
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 user's home.)",
|
|
browse_command=self._browse_directory
|
|
)
|
|
# Add a spacer to push subsequent content down if dumper_config_frame expands
|
|
parent_frame.rowconfigure(current_row, weight=1)
|
|
|
|
|
|
def _browse_file(self, target_var: tk.StringVar, title: str, filetypes=None):
|
|
current_path = target_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=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=None): # Add unused_filetypes for consistent signature with _browse_file
|
|
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:
|
|
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 a path is specified (not empty), it should be a valid directory
|
|
# If empty, it defaults to user's home/gdb_dumper_diag_dumps (which is fine)
|
|
if diag_output_dir and not os.path.isdir(diag_output_dir):
|
|
try: # Attempt to create it if it doesn't exist and is specified
|
|
os.makedirs(diag_output_dir, exist_ok=True)
|
|
logger.info(f"Created diagnostic output directory: {diag_output_dir}")
|
|
except OSError as e:
|
|
errors.append(f"Diagnostic JSON Output Dir '{diag_output_dir}' is not a valid directory and could not be created: {e}")
|
|
|
|
if errors: messagebox.showerror("Validation Error", "\n".join(errors), parent=self); return False
|
|
return True
|
|
|
|
def _apply_and_save_settings(self):
|
|
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 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: {e}", parent=self)
|
|
|
|
def _cancel(self):
|
|
logger.info("Configuration changes cancelled by user.")
|
|
self.destroy()
|
|
|
|
def _on_closing_button(self):
|
|
logger.debug("ConfigWindow close button pressed.")
|
|
self._cancel() |