521 lines
20 KiB
Python
521 lines
20 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
|
|
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
|