add profiler gui
This commit is contained in:
parent
4319b21559
commit
f16862ce29
@ -5,11 +5,11 @@ import json
|
||||
import os
|
||||
import logging
|
||||
import appdirs # For platform-independent config/data directories
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional, List # Added List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_FILE_NAME = "gdb_debug_gui_settings.v2.json"
|
||||
CONFIG_FILE_NAME = "gdb_debug_gui_settings.v2.json" # Keeping v2 for now, can be incremented later if structure changes drastically
|
||||
APP_NAME = "GDBDebugGui"
|
||||
APP_AUTHOR = "CppPythonDebug"
|
||||
|
||||
@ -60,15 +60,18 @@ class AppSettings:
|
||||
"max_string_length": 2048,
|
||||
},
|
||||
"gui": {
|
||||
"main_window_geometry": "850x650", # Adjusted height
|
||||
"main_window_geometry": "850x650",
|
||||
"config_window_geometry": "650x550",
|
||||
}
|
||||
},
|
||||
"profiles": [] # NEW: Added profiles category, defaults to an empty list
|
||||
}
|
||||
|
||||
def _load_settings(self) -> None:
|
||||
logger.info(f"Attempting to load settings from: {self.config_filepath}")
|
||||
if not os.path.exists(self.config_filepath):
|
||||
logger.info("Settings file not found. Using default settings.")
|
||||
logger.info("Settings file not found. Using default settings and creating a new one on save.")
|
||||
# No return here, self._settings already holds defaults.
|
||||
# The file will be created on the first save.
|
||||
return
|
||||
|
||||
try:
|
||||
@ -76,12 +79,15 @@ class AppSettings:
|
||||
loaded_settings = json.load(f)
|
||||
|
||||
defaults = self._get_default_settings()
|
||||
# Merge loaded settings into defaults. This ensures new default keys are added.
|
||||
# And existing loaded settings override defaults.
|
||||
self._settings = self._recursive_update(defaults, loaded_settings)
|
||||
logger.info("Settings loaded and merged successfully.")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Error decoding JSON from settings file: {self.config_filepath}. Using defaults.", exc_info=True)
|
||||
self._settings = self._get_default_settings()
|
||||
logger.error(f"Error decoding JSON from settings file: {self.config_filepath}. Using defaults and attempting to preserve problematic file.", exc_info=True)
|
||||
self._backup_corrupted_settings_file() # Backup corrupted file
|
||||
self._settings = self._get_default_settings() # Fallback to defaults
|
||||
except IOError as e:
|
||||
logger.error(f"IOError reading settings file {self.config_filepath}: {e}. Using defaults.", exc_info=True)
|
||||
self._settings = self._get_default_settings()
|
||||
@ -89,14 +95,40 @@ class AppSettings:
|
||||
logger.error(f"Unexpected error loading settings from {self.config_filepath}: {e}. Using defaults.", exc_info=True)
|
||||
self._settings = self._get_default_settings()
|
||||
|
||||
def _backup_corrupted_settings_file(self) -> None:
|
||||
if os.path.exists(self.config_filepath):
|
||||
try:
|
||||
corrupted_backup_path = self.config_filepath + ".corrupted"
|
||||
# Simple backup: if backup already exists, append a number or timestamp (optional complexity)
|
||||
if os.path.exists(corrupted_backup_path):
|
||||
import time
|
||||
corrupted_backup_path += f".{int(time.time())}"
|
||||
os.rename(self.config_filepath, corrupted_backup_path)
|
||||
logger.info(f"Backed up corrupted settings file to: {corrupted_backup_path}")
|
||||
except Exception as backup_e:
|
||||
logger.error(f"Could not backup corrupted settings file '{self.config_filepath}': {backup_e}")
|
||||
|
||||
|
||||
def _recursive_update(self, d: Dict[str, Any], u: Dict[str, Any]) -> Dict[str, Any]:
|
||||
for k, v_default in d.items():
|
||||
if k in u:
|
||||
if isinstance(v_default, dict) and isinstance(u[k], dict):
|
||||
d[k] = self._recursive_update(v_default, u[k])
|
||||
"""
|
||||
Recursively updates dictionary 'd' with values from dictionary 'u'.
|
||||
If a key in 'u' is also a dictionary and exists in 'd' as a dictionary,
|
||||
it recursively updates the sub-dictionary.
|
||||
Otherwise, it replaces the value in 'd' or adds a new key-value pair from 'u'.
|
||||
This also preserves keys in 'd' that are not in 'u'.
|
||||
"""
|
||||
# Start with a copy of d to preserve keys in d not in u, and to handle new keys from u
|
||||
merged = d.copy()
|
||||
|
||||
for k, v_u in u.items():
|
||||
if isinstance(merged.get(k), dict) and isinstance(v_u, dict):
|
||||
merged[k] = self._recursive_update(merged[k], v_u)
|
||||
else:
|
||||
d[k] = u[k]
|
||||
return d
|
||||
# If k is new to d, or if types don't match for recursive update,
|
||||
# or if it's not a dict, then u's value overwrites/is_added.
|
||||
merged[k] = v_u
|
||||
return merged
|
||||
|
||||
|
||||
def save_settings(self) -> bool:
|
||||
logger.info(f"Attempting to save settings to: {self.config_filepath}")
|
||||
@ -117,51 +149,99 @@ class AppSettings:
|
||||
try:
|
||||
return self._settings[category][key]
|
||||
except KeyError:
|
||||
logger.warning(f"Setting not found: category='{category}', key='{key}'. Returning default: {default}")
|
||||
logger.warning(f"Setting not found: category='{category}', key='{key}'. Returning provided default: {default}")
|
||||
# If category itself is missing, this would raise KeyError on self._settings[category]
|
||||
# It's better to use .get() for the category as well for robustness
|
||||
# category_dict = self._settings.get(category)
|
||||
# if category_dict is not None:
|
||||
# return category_dict.get(key, default)
|
||||
# logger.warning(f"Setting category='{category}' or key='{key}' not found. Returning default: {default}")
|
||||
return default
|
||||
|
||||
# MODIFIED: Added new method
|
||||
def get_category_settings(self, category: str, default_if_category_missing: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieves all settings (the entire sub-dictionary) within a specific category.
|
||||
|
||||
Args:
|
||||
category: The category of the settings to retrieve.
|
||||
default_if_category_missing: Value to return if the category itself is not found.
|
||||
Defaults to an empty dictionary.
|
||||
|
||||
Returns:
|
||||
A dictionary of settings for the category.
|
||||
"""
|
||||
# Ensure default_if_category_missing is a dict if not None, or default to empty dict
|
||||
effective_default = default_if_category_missing if default_if_category_missing is not None else {}
|
||||
|
||||
settings_category = self._settings.get(category)
|
||||
if settings_category is None:
|
||||
logger.warning(f"Settings category not found: '{category}'. Returning provided default or empty dict.")
|
||||
return effective_default # Return the provided default or an empty dict
|
||||
return effective_default
|
||||
|
||||
# Ensure the returned value is a dictionary, even if something went wrong with types
|
||||
if not isinstance(settings_category, dict):
|
||||
# This case is less likely now with recursive_update ensuring structure for known default categories.
|
||||
# However, if "profiles" was loaded as something other than a list (and we change its type later),
|
||||
# or a new top-level category is introduced incorrectly.
|
||||
# For "profiles", we expect a List, not a Dict.
|
||||
if category == "profiles" and not isinstance(settings_category, list):
|
||||
logger.error(f"Settings category '{category}' is expected to be a list but is {type(settings_category)}. Returning empty list as default.")
|
||||
return [] # Specific handling for profiles if it's malformed as not a list
|
||||
elif category != "profiles" and not isinstance(settings_category, dict):
|
||||
logger.error(f"Settings category '{category}' is not a dictionary (type: {type(settings_category)}). Returning empty dict.")
|
||||
return {} # Fallback to empty dict if the category data is not a dict
|
||||
return {}
|
||||
|
||||
return settings_category
|
||||
|
||||
# NEW: Method to specifically get profiles
|
||||
def get_profiles(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Retrieves the list of all profiles.
|
||||
|
||||
Returns:
|
||||
A list of profile dictionaries. Returns an empty list if "profiles"
|
||||
category is missing or not a list.
|
||||
"""
|
||||
profiles_data = self._settings.get("profiles")
|
||||
if isinstance(profiles_data, list):
|
||||
return profiles_data
|
||||
elif profiles_data is None:
|
||||
logger.warning("Profiles category not found in settings. Returning empty list.")
|
||||
else:
|
||||
logger.error(f"Profiles data in settings is not a list (type: {type(profiles_data)}). Returning empty list.")
|
||||
|
||||
# Ensure a list is always returned, even if it means resetting to default (empty list)
|
||||
# if the data is malformed.
|
||||
self._settings["profiles"] = [] # Correct malformed data
|
||||
return []
|
||||
|
||||
# NEW: Method to specifically set profiles
|
||||
def set_profiles(self, profiles_list: List[Dict[str, Any]]) -> None:
|
||||
"""
|
||||
Sets the entire list of profiles.
|
||||
|
||||
Args:
|
||||
profiles_list: A list of profile dictionaries.
|
||||
"""
|
||||
if not isinstance(profiles_list, list):
|
||||
logger.error(f"Attempted to set profiles with non-list type: {type(profiles_list)}. Aborting set.")
|
||||
return
|
||||
if not all(isinstance(p, dict) for p in profiles_list):
|
||||
logger.error("Attempted to set profiles with a list containing non-dictionary items. Aborting set.")
|
||||
return
|
||||
|
||||
self._settings["profiles"] = profiles_list
|
||||
logger.info(f"Profiles list updated. Count: {len(profiles_list)}")
|
||||
|
||||
|
||||
def get_all_settings(self) -> Dict[str, Any]:
|
||||
return self._settings
|
||||
|
||||
def set_setting(self, category: str, key: str, value: Any) -> None:
|
||||
if category not in self._settings:
|
||||
self._settings[category] = {}
|
||||
self._settings[category] = {} # Create category if it doesn't exist
|
||||
logger.info(f"Created new settings category: '{category}'")
|
||||
|
||||
if key not in self._settings[category] and self._settings[category]:
|
||||
# Special handling if category is "profiles" - it should be a list, not a dict of key-value settings
|
||||
if category == "profiles":
|
||||
logger.error(f"Cannot use set_setting for 'profiles' category. Use set_profiles() instead.")
|
||||
# Or, decide if set_setting("profiles", "some_profile_name_as_key", profile_dict_value) makes sense.
|
||||
# For now, we assume "profiles" is a list managed by get_profiles/set_profiles.
|
||||
return
|
||||
|
||||
if key not in self._settings[category] and self._settings[category]: # Check if category has items
|
||||
logger.warning(f"Adding new key '{key}' to category '{category}' with value: {value}")
|
||||
|
||||
self._settings[category][key] = value
|
||||
|
||||
def update_settings_bulk(self, new_settings_dict: Dict[str, Any]) -> None:
|
||||
# Use the same merging logic as _load_settings to ensure consistency
|
||||
self._settings = self._recursive_update(self._settings, new_settings_dict)
|
||||
logger.info("Settings updated in bulk.")
|
||||
logger.info("Settings updated in bulk (merged).")
|
||||
0
cpp_python_debug/core/profile_executor.py
Normal file
0
cpp_python_debug/core/profile_executor.py
Normal file
195
cpp_python_debug/gui/action_editor_window.py
Normal file
195
cpp_python_debug/gui/action_editor_window.py
Normal file
@ -0,0 +1,195 @@
|
||||
# File: cpp_python_debug/gui/action_editor_window.py
|
||||
# Provides a Toplevel window for editing a single debug action.
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, messagebox
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ActionEditorWindow(tk.Toplevel):
|
||||
"""
|
||||
A modal dialog for creating or editing a single debug action for a profile.
|
||||
"""
|
||||
def __init__(self, parent: tk.Widget,
|
||||
action_data: Optional[Dict[str, Any]] = None,
|
||||
is_new: bool = True):
|
||||
super().__init__(parent)
|
||||
self.parent_window = parent
|
||||
self.is_new_action = is_new
|
||||
self.result: Optional[Dict[str, Any]] = None # To store the action data on OK
|
||||
|
||||
title = "Add New Action" if self.is_new_action else "Edit Action"
|
||||
self.title(title)
|
||||
self.geometry("600x550") # Adjusted size
|
||||
self.resizable(False, False)
|
||||
|
||||
self.transient(parent)
|
||||
self.grab_set() # Make modal
|
||||
|
||||
# --- StringVars and other Tkinter variables for action fields ---
|
||||
self.breakpoint_var = tk.StringVar()
|
||||
# variables_to_dump will be handled by ScrolledText
|
||||
self.output_format_var = tk.StringVar()
|
||||
self.output_directory_var = tk.StringVar()
|
||||
self.filename_pattern_var = tk.StringVar()
|
||||
self.continue_after_dump_var = tk.BooleanVar()
|
||||
# self.max_hits_var = tk.StringVar() # For later
|
||||
|
||||
self._initial_action_data = action_data.copy() if action_data else None # Store original for cancel/comparison
|
||||
|
||||
self._create_widgets()
|
||||
self._load_action_data(action_data)
|
||||
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
||||
self.wait_window() # Important for modal dialog behavior to get result
|
||||
|
||||
def _create_widgets(self) -> None:
|
||||
main_frame = ttk.Frame(self, padding="15")
|
||||
main_frame.pack(expand=True, fill=tk.BOTH)
|
||||
main_frame.columnconfigure(1, weight=1) # Make entry fields expand
|
||||
|
||||
row_idx = 0
|
||||
|
||||
# Breakpoint Location
|
||||
ttk.Label(main_frame, text="Breakpoint Location:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
ttk.Entry(main_frame, textvariable=self.breakpoint_var, width=60).grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
|
||||
row_idx += 1
|
||||
|
||||
# MODIFIED LINE:
|
||||
bp_help_label = ttk.Label(main_frame, text="(e.g., main, file.cpp:123, MyClass::foo)", foreground="gray")
|
||||
bp_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10))
|
||||
row_idx += 1
|
||||
|
||||
# Variables to Dump
|
||||
ttk.Label(main_frame, text="Variables to Dump:").grid(row=row_idx, column=0, sticky="nw", padx=5, pady=5) # nw for top-alignment
|
||||
self.variables_text = scrolledtext.ScrolledText(main_frame, wrap=tk.WORD, height=5, width=58, font=("Consolas", 9))
|
||||
self.variables_text.grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
|
||||
row_idx += 1
|
||||
|
||||
# MODIFIED LINE:
|
||||
vars_help_label = ttk.Label(main_frame, text="(One variable/expression per line)", foreground="gray")
|
||||
vars_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10))
|
||||
row_idx += 1
|
||||
|
||||
# Output Format
|
||||
ttk.Label(main_frame, text="Output Format:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.output_format_combo = ttk.Combobox(main_frame, textvariable=self.output_format_var, values=["json", "csv"], state="readonly", width=10)
|
||||
self.output_format_combo.grid(row=row_idx, column=1, sticky="w", padx=5, pady=5)
|
||||
row_idx += 1
|
||||
|
||||
# Output Directory
|
||||
ttk.Label(main_frame, text="Output Directory:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
ttk.Entry(main_frame, textvariable=self.output_directory_var, width=50).grid(row=row_idx, column=1, sticky="ew", padx=5, pady=5)
|
||||
ttk.Button(main_frame, text="Browse...", command=self._browse_output_dir).grid(row=row_idx, column=2, padx=5, pady=5)
|
||||
row_idx += 1
|
||||
|
||||
# Filename Pattern
|
||||
ttk.Label(main_frame, text="Filename Pattern:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
ttk.Entry(main_frame, textvariable=self.filename_pattern_var, width=60).grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
|
||||
row_idx += 1
|
||||
|
||||
# MODIFIED LINE:
|
||||
pattern_help_label = ttk.Label(main_frame, text="(Placeholders: {profile_name}, {breakpoint}, {variable}, {timestamp}, {format})", foreground="gray")
|
||||
pattern_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10))
|
||||
row_idx += 1
|
||||
|
||||
# Continue After Dump
|
||||
self.continue_check = ttk.Checkbutton(main_frame, text="Continue execution after dump", variable=self.continue_after_dump_var)
|
||||
self.continue_check.grid(row=row_idx, column=0, columnspan=3, sticky="w", padx=5, pady=10)
|
||||
row_idx += 1
|
||||
|
||||
# --- Buttons Frame ---
|
||||
buttons_frame = ttk.Frame(main_frame)
|
||||
buttons_frame.grid(row=row_idx, column=0, columnspan=3, pady=(15,5), sticky="e")
|
||||
|
||||
ttk.Button(buttons_frame, text="OK", command=self._on_ok, width=10).pack(side=tk.RIGHT, padx=5)
|
||||
ttk.Button(buttons_frame, text="Cancel", command=self._on_cancel, width=10).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
def _load_action_data(self, action_data: Optional[Dict[str, Any]]) -> None:
|
||||
"""Loads action data into the form fields."""
|
||||
from ..gui.profile_manager_window import DEFAULT_ACTION # Lazy import for default
|
||||
|
||||
data_to_load = action_data if action_data else DEFAULT_ACTION.copy()
|
||||
|
||||
self.breakpoint_var.set(data_to_load.get("breakpoint_location", DEFAULT_ACTION["breakpoint_location"]))
|
||||
|
||||
variables_list = data_to_load.get("variables_to_dump", DEFAULT_ACTION["variables_to_dump"])
|
||||
if isinstance(variables_list, list):
|
||||
self.variables_text.insert(tk.END, "\n".join(variables_list))
|
||||
else: # Fallback if it's not a list for some reason
|
||||
self.variables_text.insert(tk.END, str(variables_list))
|
||||
|
||||
self.output_format_var.set(data_to_load.get("output_format", DEFAULT_ACTION["output_format"]))
|
||||
self.output_directory_var.set(data_to_load.get("output_directory", DEFAULT_ACTION["output_directory"]))
|
||||
self.filename_pattern_var.set(data_to_load.get("filename_pattern", DEFAULT_ACTION["filename_pattern"]))
|
||||
self.continue_after_dump_var.set(data_to_load.get("continue_after_dump", DEFAULT_ACTION["continue_after_dump"]))
|
||||
|
||||
def _browse_output_dir(self) -> None:
|
||||
current_path = self.output_directory_var.get()
|
||||
initial_dir = current_path if current_path and os.path.isdir(current_path) else None
|
||||
path = filedialog.askdirectory(
|
||||
title="Select Output Directory for Dumps",
|
||||
initialdir=initial_dir,
|
||||
parent=self # Ensure dialog is on top
|
||||
)
|
||||
if path:
|
||||
self.output_directory_var.set(path)
|
||||
|
||||
def _validate_data(self) -> bool:
|
||||
"""Validates the current form data."""
|
||||
if not self.breakpoint_var.get().strip():
|
||||
messagebox.showerror("Validation Error", "Breakpoint Location cannot be empty.", parent=self)
|
||||
return False
|
||||
|
||||
variables_str = self.variables_text.get("1.0", tk.END).strip()
|
||||
if not variables_str:
|
||||
messagebox.showerror("Validation Error", "Variables to Dump cannot be empty.", parent=self)
|
||||
return False
|
||||
|
||||
if not self.output_directory_var.get().strip():
|
||||
messagebox.showerror("Validation Error", "Output Directory cannot be empty.", parent=self)
|
||||
return False
|
||||
|
||||
if not self.filename_pattern_var.get().strip():
|
||||
messagebox.showerror("Validation Error", "Filename Pattern cannot be empty.", parent=self)
|
||||
return False
|
||||
# Basic check for common placeholders
|
||||
if not any(p in self.filename_pattern_var.get() for p in ["{breakpoint}", "{variable}", "{timestamp}"]):
|
||||
messagebox.showwarning("Validation Warning",
|
||||
"Filename Pattern seems to be missing common placeholders like {breakpoint}, {variable}, or {timestamp}. This might lead to overwritten files.",
|
||||
parent=self)
|
||||
|
||||
return True
|
||||
|
||||
def _on_ok(self) -> None:
|
||||
"""Handles the OK button click."""
|
||||
if not self._validate_data():
|
||||
return
|
||||
|
||||
variables_list = [line.strip() for line in self.variables_text.get("1.0", tk.END).strip().splitlines() if line.strip()]
|
||||
if not variables_list : # Double check after stripping
|
||||
messagebox.showerror("Validation Error", "Variables to Dump cannot be empty after processing.", parent=self)
|
||||
return
|
||||
|
||||
self.result = {
|
||||
"breakpoint_location": self.breakpoint_var.get().strip(),
|
||||
"variables_to_dump": variables_list,
|
||||
"output_format": self.output_format_var.get(),
|
||||
"output_directory": self.output_directory_var.get().strip(),
|
||||
"filename_pattern": self.filename_pattern_var.get().strip(),
|
||||
"continue_after_dump": self.continue_after_dump_var.get()
|
||||
}
|
||||
# logger.debug(f"ActionEditorWindow result on OK: {self.result}")
|
||||
self.destroy()
|
||||
|
||||
def _on_cancel(self) -> None:
|
||||
"""Handles the Cancel button click or window close."""
|
||||
self.result = None # Explicitly set result to None on cancel
|
||||
# logger.debug("ActionEditorWindow cancelled.")
|
||||
self.destroy()
|
||||
|
||||
# Public method to get the result after the dialog closes
|
||||
def get_result(self) -> Optional[Dict[str, Any]]:
|
||||
return self.result
|
||||
@ -12,7 +12,8 @@ import re
|
||||
from ..core.gdb_controller import GDBSession
|
||||
from ..core.output_formatter import save_to_json, save_to_csv
|
||||
from ..core.config_manager import AppSettings
|
||||
from .config_window import ConfigWindow # MODIFIED: Import ConfigWindow
|
||||
from .config_window import ConfigWindow
|
||||
from .profile_manager_window import ProfileManagerWindow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -71,6 +72,17 @@ class GDBGui(tk.Tk):
|
||||
options_menu.add_separator()
|
||||
options_menu.add_command(label="Exit", command=self._on_closing_window)
|
||||
|
||||
profiles_menu = Menu(self.menubar, tearoff=0)
|
||||
self.menubar.add_cascade(label="Profiles", menu=profiles_menu)
|
||||
profiles_menu.add_command(label="Manage Profiles...", command=self._open_profile_manager_window)
|
||||
|
||||
def _open_profile_manager_window(self):
|
||||
logger.info("Opening Profile Manager window.")
|
||||
# Pass 'self' as parent and self.app_settings
|
||||
profile_win = ProfileManagerWindow(self, self.app_settings)
|
||||
self.wait_window(profile_win) # Makes it modal
|
||||
logger.info("Profile Manager window closed.")
|
||||
|
||||
def _open_config_window(self):
|
||||
logger.debug("Opening configuration window.")
|
||||
config_win = ConfigWindow(self, self.app_settings)
|
||||
|
||||
662
cpp_python_debug/gui/profile_manager_window.py
Normal file
662
cpp_python_debug/gui/profile_manager_window.py
Normal file
@ -0,0 +1,662 @@
|
||||
# File: cpp_python_debug/gui/profile_manager_window.py
|
||||
# (Assicurati che gli import siano corretti, specialmente per ActionEditorWindow)
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, filedialog # Removed scrolledtext for actions JSON
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
from typing import TYPE_CHECKING, List, Dict, Any, Optional
|
||||
|
||||
# NEW: Import ActionEditorWindow
|
||||
from .action_editor_window import ActionEditorWindow
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..core.config_manager import AppSettings
|
||||
from .main_window import GDBGui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Default structure for a new action (remains useful)
|
||||
DEFAULT_ACTION = {
|
||||
"breakpoint_location": "main",
|
||||
"variables_to_dump": ["my_variable"],
|
||||
"output_format": "json",
|
||||
"output_directory": "./debug_dumps",
|
||||
"filename_pattern": "{profile_name}_{breakpoint}_{variable}_{timestamp}.{format}",
|
||||
"continue_after_dump": True
|
||||
}
|
||||
|
||||
# Default structure for a new profile (actions will be an empty list initially or one default)
|
||||
DEFAULT_PROFILE = {
|
||||
"profile_name": "New Profile",
|
||||
"target_executable": "",
|
||||
"program_parameters": "",
|
||||
"actions": [] # Start with an empty list, actions are added via UI
|
||||
}
|
||||
|
||||
|
||||
class ProfileManagerWindow(tk.Toplevel):
|
||||
# ... (init, _load_profiles_from_settings, etc. rimangono simili) ...
|
||||
def __init__(self, parent: 'GDBGui', app_settings: 'AppSettings'):
|
||||
super().__init__(parent)
|
||||
self.parent_window = parent
|
||||
self.app_settings = app_settings
|
||||
|
||||
self.title("Profile Manager")
|
||||
self.geometry("950x700")
|
||||
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
|
||||
self._profiles_data: List[Dict[str, Any]] = []
|
||||
self._selected_profile_index: Optional[int] = None
|
||||
self._selected_action_index_in_profile: Optional[int] = None # NEW
|
||||
self._current_profile_modified_in_form: bool = False
|
||||
self._profiles_list_changed_overall: bool = False
|
||||
|
||||
self.profile_name_var = tk.StringVar()
|
||||
self.target_exe_var = tk.StringVar()
|
||||
self.program_params_var = tk.StringVar()
|
||||
|
||||
self._load_profiles_from_settings()
|
||||
self._create_widgets() # This will now include the new actions list UI
|
||||
self._populate_profiles_listbox()
|
||||
|
||||
if self._profiles_data:
|
||||
self._select_profile_by_index(0)
|
||||
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_closing_button)
|
||||
|
||||
self.profile_name_var.trace_add("write", self._mark_form_as_modified)
|
||||
self.target_exe_var.trace_add("write", self._mark_form_as_modified)
|
||||
self.program_params_var.trace_add("write", self._mark_form_as_modified)
|
||||
|
||||
|
||||
def _mark_form_as_modified(self, *args):
|
||||
self._current_profile_modified_in_form = True
|
||||
|
||||
|
||||
def _load_profiles_from_settings(self) -> None:
|
||||
self._profiles_data = []
|
||||
loaded_profiles = self.app_settings.get_profiles()
|
||||
for profile_dict in loaded_profiles:
|
||||
# Ensure 'actions' key exists and is a list, make a deep copy for editing
|
||||
copied_profile = profile_dict.copy() # shallow copy of top-level
|
||||
copied_profile["actions"] = [action.copy() for action in profile_dict.get("actions", [])] # deep copy of actions list
|
||||
self._profiles_data.append(copied_profile)
|
||||
|
||||
self._profiles_list_changed_overall = False
|
||||
logger.debug(f"Loaded {len(self._profiles_data)} profiles into ProfileManagerWindow.")
|
||||
|
||||
|
||||
def _create_widgets(self) -> None:
|
||||
main_frame = ttk.Frame(self, padding="10")
|
||||
main_frame.pack(expand=True, fill=tk.BOTH)
|
||||
|
||||
main_frame.columnconfigure(0, weight=1, minsize=200)
|
||||
main_frame.columnconfigure(1, weight=3)
|
||||
main_frame.rowconfigure(0, weight=1)
|
||||
main_frame.rowconfigure(1, weight=0)
|
||||
|
||||
left_pane = ttk.Frame(main_frame)
|
||||
left_pane.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
|
||||
left_pane.rowconfigure(0, weight=1)
|
||||
left_pane.rowconfigure(1, weight=0)
|
||||
left_pane.columnconfigure(0, weight=1)
|
||||
|
||||
profiles_list_frame = ttk.LabelFrame(left_pane, text="Profiles", padding="5")
|
||||
profiles_list_frame.grid(row=0, column=0, sticky="nsew", pady=(0,5))
|
||||
profiles_list_frame.rowconfigure(0, weight=1)
|
||||
profiles_list_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.profiles_listbox = tk.Listbox(profiles_list_frame, exportselection=False, selectmode=tk.SINGLE)
|
||||
self.profiles_listbox.grid(row=0, column=0, sticky="nsew")
|
||||
self.profiles_listbox.bind("<<ListboxSelect>>", self._on_profile_select)
|
||||
|
||||
listbox_scrollbar_y = ttk.Scrollbar(profiles_list_frame, orient=tk.VERTICAL, command=self.profiles_listbox.yview)
|
||||
listbox_scrollbar_y.grid(row=0, column=1, sticky="ns")
|
||||
self.profiles_listbox.configure(yscrollcommand=listbox_scrollbar_y.set)
|
||||
|
||||
profiles_list_controls_frame = ttk.Frame(left_pane)
|
||||
profiles_list_controls_frame.grid(row=1, column=0, sticky="ew", pady=5)
|
||||
|
||||
ttk.Button(profiles_list_controls_frame, text="New", command=self._new_profile).pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
|
||||
self.duplicate_button = ttk.Button(profiles_list_controls_frame, text="Duplicate", command=self._duplicate_profile, state=tk.DISABLED)
|
||||
self.duplicate_button.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
|
||||
self.delete_button = ttk.Button(profiles_list_controls_frame, text="Delete", command=self._delete_profile, state=tk.DISABLED)
|
||||
self.delete_button.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
|
||||
|
||||
right_pane = ttk.Frame(main_frame)
|
||||
right_pane.grid(row=0, column=1, sticky="nsew")
|
||||
right_pane.rowconfigure(1, weight=1) # Actions list/controls will go here
|
||||
right_pane.columnconfigure(0, weight=1)
|
||||
|
||||
details_form_frame = ttk.LabelFrame(right_pane, text="Profile Details", padding="10")
|
||||
details_form_frame.grid(row=0, column=0, sticky="new", pady=(0,5))
|
||||
details_form_frame.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(details_form_frame, text="Profile Name:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=3)
|
||||
self.profile_name_entry = ttk.Entry(details_form_frame, textvariable=self.profile_name_var, width=60, state=tk.DISABLED)
|
||||
self.profile_name_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=3)
|
||||
|
||||
ttk.Label(details_form_frame, text="Target Executable:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=3)
|
||||
self.target_exe_entry = ttk.Entry(details_form_frame, textvariable=self.target_exe_var, state=tk.DISABLED)
|
||||
self.target_exe_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=3)
|
||||
self.browse_exe_button = ttk.Button(details_form_frame, text="Browse...", command=self._browse_target_executable, state=tk.DISABLED)
|
||||
self.browse_exe_button.grid(row=1, column=2, padx=5, pady=3)
|
||||
|
||||
ttk.Label(details_form_frame, text="Program Parameters:").grid(row=2, column=0, sticky=tk.W, padx=5, pady=3)
|
||||
self.program_params_entry = ttk.Entry(details_form_frame, textvariable=self.program_params_var, state=tk.DISABLED)
|
||||
self.program_params_entry.grid(row=2, column=1, columnspan=2, sticky="ew", padx=5, pady=3)
|
||||
|
||||
# --- NEW: Actions Management UI ---
|
||||
actions_ui_frame = ttk.LabelFrame(right_pane, text="Debug Actions", padding="10")
|
||||
actions_ui_frame.grid(row=1, column=0, sticky="nsew", pady=5)
|
||||
actions_ui_frame.rowconfigure(0, weight=1) # Listbox for actions
|
||||
actions_ui_frame.columnconfigure(0, weight=1) # Listbox
|
||||
actions_ui_frame.columnconfigure(1, weight=0) # Buttons for actions
|
||||
|
||||
self.actions_listbox = tk.Listbox(actions_ui_frame, exportselection=False, selectmode=tk.SINGLE, height=8)
|
||||
self.actions_listbox.grid(row=0, column=0, sticky="nsew", pady=5, padx=(0,5))
|
||||
self.actions_listbox.bind("<<ListboxSelect>>", self._on_action_select_in_listbox) # New handler
|
||||
|
||||
actions_listbox_scrolly = ttk.Scrollbar(actions_ui_frame, orient=tk.VERTICAL, command=self.actions_listbox.yview)
|
||||
actions_listbox_scrolly.grid(row=0, column=1, sticky="ns", pady=5) # Attach to column next to listbox
|
||||
self.actions_listbox.configure(yscrollcommand=actions_listbox_scrolly.set)
|
||||
|
||||
action_buttons_frame = ttk.Frame(actions_ui_frame)
|
||||
action_buttons_frame.grid(row=0, column=2, sticky="ns", padx=(5,0), pady=5) # Buttons to the right of listbox
|
||||
|
||||
self.add_action_button = ttk.Button(action_buttons_frame, text="Add...", command=self._add_action, state=tk.DISABLED)
|
||||
self.add_action_button.pack(fill=tk.X, pady=2)
|
||||
self.edit_action_button = ttk.Button(action_buttons_frame, text="Edit...", command=self._edit_action, state=tk.DISABLED)
|
||||
self.edit_action_button.pack(fill=tk.X, pady=2)
|
||||
self.remove_action_button = ttk.Button(action_buttons_frame, text="Remove", command=self._remove_action, state=tk.DISABLED)
|
||||
self.remove_action_button.pack(fill=tk.X, pady=2)
|
||||
# Move Up/Down buttons can be added later
|
||||
# self.move_action_up_button = ttk.Button(action_buttons_frame, text="Up", command=self._move_action_up, state=tk.DISABLED)
|
||||
# self.move_action_up_button.pack(fill=tk.X, pady=2)
|
||||
# self.move_action_down_button = ttk.Button(action_buttons_frame, text="Down", command=self._move_action_down, state=tk.DISABLED)
|
||||
# self.move_action_down_button.pack(fill=tk.X, pady=2)
|
||||
|
||||
|
||||
bottom_buttons_frame = ttk.Frame(main_frame)
|
||||
bottom_buttons_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(10,0))
|
||||
bottom_buttons_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.save_button = ttk.Button(bottom_buttons_frame, text="Save All Changes", command=self._save_all_profiles_to_settings, state=tk.NORMAL)
|
||||
self.save_button.grid(row=0, column=1, padx=5, pady=5, sticky=tk.E)
|
||||
ttk.Button(bottom_buttons_frame, text="Close", command=self._on_closing_button).grid(row=0, column=2, padx=5, pady=5, sticky=tk.E)
|
||||
|
||||
def _populate_profiles_listbox(self) -> None:
|
||||
self.profiles_listbox.delete(0, tk.END)
|
||||
for i, profile in enumerate(self._profiles_data):
|
||||
display_name = profile.get("profile_name", f"Unnamed Profile {i+1}")
|
||||
self.profiles_listbox.insert(tk.END, display_name)
|
||||
self._update_profile_action_buttons_state()
|
||||
|
||||
def _on_profile_select(self, event: Optional[tk.Event] = None) -> None:
|
||||
selection_indices = self.profiles_listbox.curselection()
|
||||
if not selection_indices:
|
||||
self._clear_profile_form()
|
||||
self._selected_profile_index = None
|
||||
self._update_profile_action_buttons_state()
|
||||
self._populate_actions_listbox() # Clear actions listbox
|
||||
return
|
||||
|
||||
new_selected_index = selection_indices[0]
|
||||
|
||||
if self._selected_profile_index is not None and self._current_profile_modified_in_form:
|
||||
# Only save if the actual profile data would change
|
||||
# This check might need to be more robust if comparing complex objects
|
||||
current_data_in_form = self._get_data_from_form() # Hypothetical method to get form data for current profile
|
||||
if current_data_in_form != self._profiles_data[self._selected_profile_index]:
|
||||
if not self._save_current_form_to_profile_data(self._selected_profile_index):
|
||||
# If save failed (e.g. validation), re-select previous to avoid data loss in form
|
||||
self.profiles_listbox.selection_set(self._selected_profile_index)
|
||||
return
|
||||
|
||||
self._select_profile_by_index(new_selected_index)
|
||||
self._update_profile_action_buttons_state()
|
||||
|
||||
def _get_data_from_form(self) -> Dict[str, Any]:
|
||||
# Helper to get current profile data from form fields (excluding actions for now)
|
||||
# Actions are managed directly in self._profiles_data through their editor.
|
||||
return {
|
||||
"profile_name": self.profile_name_var.get(),
|
||||
"target_executable": self.target_exe_var.get(),
|
||||
"program_parameters": self.program_params_var.get(),
|
||||
# Actions are NOT read from a text widget anymore here.
|
||||
# They are part of self._profiles_data[self._selected_profile_index]['actions']
|
||||
}
|
||||
|
||||
def _select_profile_by_index(self, index: int) -> None:
|
||||
if not (0 <= index < len(self._profiles_data)):
|
||||
self._clear_profile_form()
|
||||
self._selected_profile_index = None
|
||||
return
|
||||
|
||||
self._selected_profile_index = index
|
||||
profile = self._profiles_data[index]
|
||||
|
||||
self.profile_name_var.set(profile.get("profile_name", ""))
|
||||
self.target_exe_var.set(profile.get("target_executable", ""))
|
||||
self.program_params_var.set(profile.get("program_parameters", ""))
|
||||
|
||||
self._populate_actions_listbox() # NEW: Populate actions list for this profile
|
||||
|
||||
self._enable_profile_form_editing(True)
|
||||
self._current_profile_modified_in_form = False
|
||||
|
||||
# Ensure listbox selection matches
|
||||
if not self.profiles_listbox.curselection() or self.profiles_listbox.curselection()[0] != index:
|
||||
self.profiles_listbox.selection_clear(0, tk.END)
|
||||
self.profiles_listbox.selection_set(index)
|
||||
self.profiles_listbox.activate(index)
|
||||
|
||||
self.add_action_button.config(state=tk.NORMAL) # Enable add action if a profile is selected
|
||||
|
||||
def _clear_profile_form(self) -> None:
|
||||
self.profile_name_var.set("")
|
||||
self.target_exe_var.set("")
|
||||
self.program_params_var.set("")
|
||||
|
||||
self._populate_actions_listbox() # Will clear if no profile selected or profile has no actions
|
||||
|
||||
self._enable_profile_form_editing(False)
|
||||
self._current_profile_modified_in_form = False
|
||||
self.add_action_button.config(state=tk.DISABLED)
|
||||
self.edit_action_button.config(state=tk.DISABLED)
|
||||
self.remove_action_button.config(state=tk.DISABLED)
|
||||
|
||||
|
||||
def _enable_profile_form_editing(self, enable: bool) -> None:
|
||||
state = tk.NORMAL if enable else tk.DISABLED
|
||||
self.profile_name_entry.config(state=state)
|
||||
self.target_exe_entry.config(state=state)
|
||||
self.browse_exe_button.config(state=state)
|
||||
self.program_params_entry.config(state=state)
|
||||
# The actions listbox and its buttons' states are handled by _populate_actions_listbox
|
||||
# and _on_action_select_in_listbox
|
||||
|
||||
def _update_profile_action_buttons_state(self) -> None:
|
||||
if self._selected_profile_index is not None and self._profiles_data:
|
||||
self.duplicate_button.config(state=tk.NORMAL)
|
||||
self.delete_button.config(state=tk.NORMAL)
|
||||
self.add_action_button.config(state=tk.NORMAL) # Enable Add Action if a profile is selected
|
||||
else:
|
||||
self.duplicate_button.config(state=tk.DISABLED)
|
||||
self.delete_button.config(state=tk.DISABLED)
|
||||
self.add_action_button.config(state=tk.DISABLED)
|
||||
|
||||
|
||||
def _browse_target_executable(self) -> None:
|
||||
# ... (same as before)
|
||||
current_path = self.target_exe_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="Select Target Executable",
|
||||
filetypes=[("Executable files", ("*.exe", "*")), ("All files", "*.*")],
|
||||
initialdir=initial_dir,
|
||||
parent=self
|
||||
)
|
||||
if path:
|
||||
self.target_exe_var.set(path)
|
||||
self._mark_form_as_modified()
|
||||
|
||||
# REMOVED: _validate_current_form_actions_json() - actions are no longer from a JSON text widget
|
||||
|
||||
def _save_current_form_to_profile_data(self, profile_index: int) -> bool:
|
||||
if not (0 <= profile_index < len(self._profiles_data)):
|
||||
logger.error(f"Invalid profile index {profile_index} for saving form.")
|
||||
return False
|
||||
|
||||
profile_name = self.profile_name_var.get().strip()
|
||||
if not profile_name:
|
||||
messagebox.showerror("Validation Error", "Profile Name cannot be empty.", parent=self)
|
||||
self.profile_name_entry.focus_set()
|
||||
return False
|
||||
|
||||
for i, p_item in enumerate(self._profiles_data):
|
||||
if i != profile_index and p_item.get("profile_name") == profile_name:
|
||||
messagebox.showerror("Validation Error", f"Profile name '{profile_name}' already exists.", parent=self)
|
||||
self.profile_name_entry.focus_set()
|
||||
return False
|
||||
|
||||
# Actions are now part of self._profiles_data[profile_index]["actions"] directly,
|
||||
# manipulated by _add_action, _edit_action, _remove_action.
|
||||
# No need to parse from a text widget here.
|
||||
|
||||
target_profile = self._profiles_data[profile_index]
|
||||
old_name = target_profile.get("profile_name")
|
||||
|
||||
target_profile["profile_name"] = profile_name
|
||||
target_profile["target_executable"] = self.target_exe_var.get().strip()
|
||||
target_profile["program_parameters"] = self.program_params_var.get()
|
||||
# target_profile["actions"] is already up-to-date if actions were modified via their dedicated UI flow.
|
||||
|
||||
self._current_profile_modified_in_form = False
|
||||
self._profiles_list_changed_overall = True
|
||||
|
||||
if old_name != profile_name:
|
||||
self.profiles_listbox.delete(profile_index)
|
||||
self.profiles_listbox.insert(profile_index, profile_name)
|
||||
self.profiles_listbox.selection_set(profile_index)
|
||||
|
||||
logger.info(f"Profile '{profile_name}' (index {profile_index}) basic details updated in internal list.")
|
||||
return True
|
||||
|
||||
def _new_profile(self) -> None:
|
||||
if self._selected_profile_index is not None and self._current_profile_modified_in_form:
|
||||
response = messagebox.askyesnocancel("Unsaved Changes",
|
||||
f"Profile '{self._profiles_data[self._selected_profile_index].get('profile_name')}' has unsaved changes in the form.\n"
|
||||
"Do you want to save them before creating a new profile?",
|
||||
default=messagebox.CANCEL, parent=self)
|
||||
if response is True: # Yes
|
||||
if not self._save_current_form_to_profile_data(self._selected_profile_index):
|
||||
return
|
||||
elif response is None: # Cancel
|
||||
return
|
||||
# If No, proceed without saving form changes
|
||||
|
||||
new_p_template = DEFAULT_PROFILE.copy() # Get the template
|
||||
new_p = {
|
||||
"profile_name": "", # Will be set
|
||||
"target_executable": new_p_template.get("target_executable",""),
|
||||
"program_parameters": new_p_template.get("program_parameters",""),
|
||||
"actions": [] # Start with no actions; user adds them
|
||||
}
|
||||
|
||||
base_name = "New Profile"
|
||||
name_candidate = base_name
|
||||
count = 1
|
||||
existing_names = {p.get("profile_name") for p in self._profiles_data}
|
||||
while name_candidate in existing_names:
|
||||
name_candidate = f"{base_name} ({count})"
|
||||
count += 1
|
||||
new_p["profile_name"] = name_candidate
|
||||
|
||||
self._profiles_data.append(new_p)
|
||||
self._profiles_list_changed_overall = True
|
||||
self._populate_profiles_listbox()
|
||||
self._select_profile_by_index(len(self._profiles_data) - 1)
|
||||
self.profile_name_entry.focus_set()
|
||||
self.profile_name_entry.selection_range(0, tk.END)
|
||||
# New profile is inherently "modified" from a saved state perspective.
|
||||
# _current_profile_modified_in_form might be set by name_var trace.
|
||||
self._mark_form_as_modified()
|
||||
|
||||
|
||||
def _duplicate_profile(self) -> None:
|
||||
# ... (Mostly same, but ensure actions are deep-copied if they are complex) ...
|
||||
if self._selected_profile_index is None: return
|
||||
|
||||
if self._current_profile_modified_in_form:
|
||||
if not self._save_current_form_to_profile_data(self._selected_profile_index):
|
||||
return
|
||||
|
||||
original_profile = self._profiles_data[self._selected_profile_index]
|
||||
|
||||
duplicated_profile = {
|
||||
"profile_name": "",
|
||||
"target_executable": original_profile.get("target_executable", ""),
|
||||
"program_parameters": original_profile.get("program_parameters", ""),
|
||||
# Deep copy of actions list and each action dictionary within it
|
||||
"actions": [action.copy() for action in original_profile.get("actions", [])]
|
||||
}
|
||||
|
||||
base_name = f"{original_profile.get('profile_name', 'Profile')}_copy"
|
||||
# ... (rest of unique name generation logic is same)
|
||||
name_candidate = base_name
|
||||
count = 1
|
||||
existing_names = {p.get("profile_name") for p in self._profiles_data}
|
||||
while name_candidate in existing_names:
|
||||
name_candidate = f"{base_name}_{count}"
|
||||
count += 1
|
||||
duplicated_profile["profile_name"] = name_candidate
|
||||
|
||||
self._profiles_data.append(duplicated_profile)
|
||||
self._profiles_list_changed_overall = True
|
||||
self._populate_profiles_listbox()
|
||||
self._select_profile_by_index(len(self._profiles_data) - 1)
|
||||
self._mark_form_as_modified()
|
||||
|
||||
def _delete_profile(self) -> None:
|
||||
# ... (same as before) ...
|
||||
if self._selected_profile_index is None or not self._profiles_data:
|
||||
return
|
||||
|
||||
profile_to_delete = self._profiles_data[self._selected_profile_index]
|
||||
profile_name = profile_to_delete.get("profile_name", "this profile")
|
||||
|
||||
if not messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete '{profile_name}'?", parent=self):
|
||||
return
|
||||
|
||||
original_selection_index_before_delete = self._selected_profile_index
|
||||
del self._profiles_data[original_selection_index_before_delete]
|
||||
self._profiles_list_changed_overall = True
|
||||
|
||||
# Adjust selection after deletion
|
||||
new_list_size = len(self._profiles_data)
|
||||
self._selected_profile_index = None # Clear old selection index first
|
||||
self._populate_profiles_listbox() # Repopulate before trying to select
|
||||
|
||||
if new_list_size == 0:
|
||||
self._clear_profile_form()
|
||||
else:
|
||||
# Try to select the item that was at the same index, or the last item if out of bounds
|
||||
new_selection_idx = min(original_selection_index_before_delete, new_list_size - 1)
|
||||
self._select_profile_by_index(new_selection_idx)
|
||||
|
||||
|
||||
def _save_all_profiles_to_settings(self) -> None:
|
||||
if self._selected_profile_index is not None and self._current_profile_modified_in_form:
|
||||
if not self._save_current_form_to_profile_data(self._selected_profile_index):
|
||||
messagebox.showerror("Save Error", "Could not save changes from the form for the selected profile. Aborting save to settings.", parent=self)
|
||||
return
|
||||
|
||||
profile_names_seen = set()
|
||||
for i, profile in enumerate(self._profiles_data):
|
||||
name = profile.get("profile_name", "").strip()
|
||||
if not name:
|
||||
messagebox.showerror("Validation Error", f"Profile at index {i} (try selecting it) has an empty name. Please provide a name.", parent=self)
|
||||
# Try to select it for the user to fix
|
||||
if self.profiles_listbox.size() > i : self._select_profile_by_index(i)
|
||||
return
|
||||
if name in profile_names_seen:
|
||||
messagebox.showerror("Validation Error", f"Duplicate profile name '{name}' found. Profile names must be unique.", parent=self)
|
||||
if self.profiles_listbox.size() > i : self._select_profile_by_index(i)
|
||||
return
|
||||
profile_names_seen.add(name)
|
||||
|
||||
# Validate actions within each profile (basic check: must be a list of dicts)
|
||||
actions = profile.get("actions")
|
||||
if not isinstance(actions, list):
|
||||
messagebox.showerror("Data Error", f"Profile '{name}' has malformed actions (not a list). Please correct.", parent=self)
|
||||
if self.profiles_listbox.size() > i : self._select_profile_by_index(i)
|
||||
return
|
||||
for idx, action in enumerate(actions):
|
||||
if not isinstance(action, dict):
|
||||
messagebox.showerror("Data Error", f"Profile '{name}', action {idx+1} is malformed (not a dictionary). Please correct.", parent=self)
|
||||
if self.profiles_listbox.size() > i : self._select_profile_by_index(i)
|
||||
return
|
||||
# Could add more specific action validation here if ActionEditor is not fully trusted yet
|
||||
|
||||
self.app_settings.set_profiles(self._profiles_data)
|
||||
if self.app_settings.save_settings():
|
||||
logger.info(f"All {len(self._profiles_data)} profiles saved to AppSettings.")
|
||||
messagebox.showinfo("Profiles Saved", "All profile changes have been saved.", parent=self)
|
||||
self._current_profile_modified_in_form = False
|
||||
self._profiles_list_changed_overall = False
|
||||
else:
|
||||
messagebox.showerror("Save Error", "Could not save profiles to the settings file. Check logs.", parent=self)
|
||||
|
||||
def _on_closing_button(self) -> None:
|
||||
# ... (same logic as before, using _current_profile_modified_in_form and _profiles_list_changed_overall)
|
||||
needs_save_prompt = False
|
||||
prompt_message = ""
|
||||
|
||||
if self._selected_profile_index is not None and self._current_profile_modified_in_form:
|
||||
needs_save_prompt = True
|
||||
profile_name = self._profiles_data[self._selected_profile_index].get('profile_name', 'current profile')
|
||||
prompt_message = f"Profile '{profile_name}' has unsaved changes in the form.\n"
|
||||
|
||||
if self._profiles_list_changed_overall and not needs_save_prompt : # Only check overall if form isn't already prompting
|
||||
needs_save_prompt = True
|
||||
prompt_message = "The list of profiles (additions, deletions, or saved edits) has changed.\n"
|
||||
elif self._profiles_list_changed_overall and needs_save_prompt: # Both have changes
|
||||
prompt_message += "Additionally, the overall list of profiles has changed.\n"
|
||||
|
||||
|
||||
if needs_save_prompt:
|
||||
prompt_message += "Do you want to save all changes before closing?"
|
||||
response = messagebox.askyesnocancel("Unsaved Changes", prompt_message, default=messagebox.CANCEL, parent=self)
|
||||
if response is True: # Yes
|
||||
self._save_all_profiles_to_settings()
|
||||
# If save failed, _save_all_profiles_to_settings shows message, we still proceed to close
|
||||
elif response is None: # Cancel
|
||||
return
|
||||
# If False (No), proceed to close without saving
|
||||
|
||||
self.parent_window.focus_set()
|
||||
self.destroy()
|
||||
|
||||
# --- NEW Methods for Actions Management ---
|
||||
def _populate_actions_listbox(self) -> None:
|
||||
"""Populates the actions listbox for the currently selected profile."""
|
||||
self.actions_listbox.delete(0, tk.END)
|
||||
self._selected_action_index_in_profile = None # Reset action selection
|
||||
|
||||
if self._selected_profile_index is not None and \
|
||||
0 <= self._selected_profile_index < len(self._profiles_data):
|
||||
profile = self._profiles_data[self._selected_profile_index]
|
||||
actions = profile.get("actions", [])
|
||||
for i, action in enumerate(actions):
|
||||
# Display a summary of the action. Customize as needed.
|
||||
bp = action.get("breakpoint_location", "N/A")
|
||||
num_vars = len(action.get("variables_to_dump", []))
|
||||
fmt = action.get("output_format", "N/A")
|
||||
cont = "Yes" if action.get("continue_after_dump", False) else "No"
|
||||
summary = f"BP: {bp} (Vars: {num_vars}, Fmt: {fmt}, Cont: {cont})"
|
||||
self.actions_listbox.insert(tk.END, summary)
|
||||
|
||||
self._update_action_buttons_state() # Update Edit/Remove based on selection
|
||||
|
||||
|
||||
def _on_action_select_in_listbox(self, event: Optional[tk.Event] = None) -> None:
|
||||
"""Handles selection of an action in the actions_listbox."""
|
||||
selection_indices = self.actions_listbox.curselection()
|
||||
if selection_indices:
|
||||
self._selected_action_index_in_profile = selection_indices[0]
|
||||
else:
|
||||
self._selected_action_index_in_profile = None
|
||||
self._update_action_buttons_state()
|
||||
|
||||
def _update_action_buttons_state(self) -> None:
|
||||
"""Updates the state of Edit and Remove action buttons."""
|
||||
profile_selected = self._selected_profile_index is not None
|
||||
action_selected = self._selected_action_index_in_profile is not None
|
||||
|
||||
self.add_action_button.config(state=tk.NORMAL if profile_selected else tk.DISABLED)
|
||||
self.edit_action_button.config(state=tk.NORMAL if action_selected else tk.DISABLED)
|
||||
self.remove_action_button.config(state=tk.NORMAL if action_selected else tk.DISABLED)
|
||||
# self.move_action_up_button.config(state=tk.NORMAL if action_selected and self._selected_action_index_in_profile > 0 else tk.DISABLED)
|
||||
# num_actions = 0
|
||||
# if profile_selected and "actions" in self._profiles_data[self._selected_profile_index]:
|
||||
# num_actions = len(self._profiles_data[self._selected_profile_index]["actions"])
|
||||
# self.move_action_down_button.config(state=tk.NORMAL if action_selected and self._selected_action_index_in_profile < (num_actions -1) else tk.DISABLED)
|
||||
|
||||
|
||||
def _add_action(self) -> None:
|
||||
if self._selected_profile_index is None: return
|
||||
|
||||
editor = ActionEditorWindow(self, action_data=None, is_new=True) # Pass self (ProfileManagerWindow) as parent
|
||||
new_action_data = editor.get_result()
|
||||
|
||||
if new_action_data:
|
||||
profile = self._profiles_data[self._selected_profile_index]
|
||||
if "actions" not in profile or not isinstance(profile["actions"], list):
|
||||
profile["actions"] = [] # Ensure actions list exists
|
||||
profile["actions"].append(new_action_data)
|
||||
|
||||
self._profiles_list_changed_overall = True # Mark profile as changed
|
||||
self._current_profile_modified_in_form = True # This profile in form has changed
|
||||
self._populate_actions_listbox() # Refresh actions list
|
||||
self.actions_listbox.selection_set(tk.END) # Select the new action
|
||||
self._on_action_select_in_listbox() # Update button states
|
||||
logger.info(f"Added new action to profile '{profile.get('profile_name')}'.")
|
||||
|
||||
def _edit_action(self) -> None:
|
||||
if self._selected_profile_index is None or self._selected_action_index_in_profile is None:
|
||||
logger.warning("Edit action called but no profile or action is selected.")
|
||||
return
|
||||
|
||||
# Ensure the selected action index is valid for the current actions list
|
||||
profile = self._profiles_data[self._selected_profile_index]
|
||||
actions_list = profile.get("actions", [])
|
||||
|
||||
# Defensive check for the selected action index
|
||||
if not (0 <= self._selected_action_index_in_profile < len(actions_list)):
|
||||
logger.error(f"Selected action index {self._selected_action_index_in_profile} is out of bounds for actions list of length {len(actions_list)}.")
|
||||
self._selected_action_index_in_profile = None # Invalidate selection
|
||||
self._update_action_buttons_state() # Update UI
|
||||
messagebox.showerror("Error", "The selected action index is no longer valid. Please re-select.", parent=self)
|
||||
return
|
||||
|
||||
action_to_edit = actions_list[self._selected_action_index_in_profile]
|
||||
|
||||
editor = ActionEditorWindow(self, action_data=action_to_edit, is_new=False)
|
||||
updated_action_data = editor.get_result()
|
||||
|
||||
if updated_action_data:
|
||||
profile["actions"][self._selected_action_index_in_profile] = updated_action_data
|
||||
|
||||
self._profiles_list_changed_overall = True
|
||||
self._current_profile_modified_in_form = True # Mark profile as changed because an action within it changed
|
||||
|
||||
# Store the current selection index because _populate_actions_listbox clears it
|
||||
current_action_idx_to_reselect = self._selected_action_index_in_profile
|
||||
|
||||
self._populate_actions_listbox() # Refresh to show updated summary
|
||||
|
||||
# MODIFIED: Ensure re-selection is valid
|
||||
if current_action_idx_to_reselect is not None and \
|
||||
0 <= current_action_idx_to_reselect < self.actions_listbox.size():
|
||||
self.actions_listbox.selection_set(current_action_idx_to_reselect)
|
||||
|
||||
self._on_action_select_in_listbox() # This will correctly set _selected_action_index_in_profile based on actual listbox selection
|
||||
logger.info(f"Edited action in profile '{profile.get('profile_name')}'.")
|
||||
else:
|
||||
# If editor was cancelled, ensure the original selection state is maintained or cleared if necessary
|
||||
# No, _on_action_select_in_listbox() will handle this if we re-select.
|
||||
# If editor was cancelled, the selection in listbox shouldn't change from user's perspective
|
||||
# unless the list itself was repopulated for other reasons.
|
||||
# Let's ensure the listbox selection status is re-evaluated by _on_action_select_in_listbox
|
||||
self._on_action_select_in_listbox() # Call to ensure button states are correct after editor close
|
||||
pass
|
||||
|
||||
|
||||
def _remove_action(self) -> None:
|
||||
if self._selected_profile_index is None or self._selected_action_index_in_profile is None:
|
||||
return
|
||||
|
||||
profile = self._profiles_data[self._selected_profile_index]
|
||||
action_summary_to_delete = self.actions_listbox.get(self._selected_action_index_in_profile)
|
||||
|
||||
if not messagebox.askyesno("Confirm Delete Action",
|
||||
f"Are you sure you want to delete this action?\n\n{action_summary_to_delete}",
|
||||
parent=self):
|
||||
return
|
||||
|
||||
del profile.get("actions", [])[self._selected_action_index_in_profile]
|
||||
|
||||
self._profiles_list_changed_overall = True
|
||||
self._current_profile_modified_in_form = True
|
||||
self._populate_actions_listbox() # Refresh
|
||||
# Try to select the next item or previous if last was deleted
|
||||
num_actions = len(profile.get("actions", []))
|
||||
if num_actions > 0:
|
||||
new_selection = min(self._selected_action_index_in_profile, num_actions -1)
|
||||
if new_selection >=0:
|
||||
self.actions_listbox.selection_set(new_selection)
|
||||
self._on_action_select_in_listbox() # Update button states
|
||||
2
todo.md
2
todo.md
@ -54,7 +54,7 @@ DEBUG_STRING_TRACE: _serializ
|
||||
|
||||
|
||||
|
||||
Ottima idea! La funzionalità dei "profili di lancio automatici" aggiungerebbe un valore enorme al tuo strumento, trasformandolo da un helper interattivo a uno strumento di data-collection e analisi automatizzata durante il debug. Questo è particolarmente utile per:
|
||||
Ottima idea! La funzionalità che vorrei introdurre è quella che io chiamo "profili di lancio automatici" trasformando da un helper interattivo a uno strumento di data-collection e analisi automatizzata durante il debug. Questo è particolarmente utile per:
|
||||
|
||||
* **Regression Testing**: Verificare che determinate strutture dati mantengano valori attesi in punti specifici dell'esecuzione dopo modifiche al codice.
|
||||
* **Performance Profiling (rudimentale)**: Collezionare dati in vari punti per capire come evolvono.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user