SXXXXXXX_CppPythonDebug/cpp_python_debug/gui/config_window.py
2025-06-09 10:52:43 +02:00

616 lines
23 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,
) # 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