diff --git a/cpp_python_debug/core/config_manager.py b/cpp_python_debug/core/config_manager.py index 872a734..992a369 100644 --- a/cpp_python_debug/core/config_manager.py +++ b/cpp_python_debug/core/config_manager.py @@ -5,12 +5,12 @@ 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" -APP_NAME = "GDBDebugGui" +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" class AppSettings: @@ -46,7 +46,7 @@ class AppSettings: "default_variable_to_dump": "", "default_program_parameters": "", }, - "timeouts": { + "timeouts": { "gdb_start": 30, "gdb_command": 30, "program_run_continue": 120, @@ -54,21 +54,24 @@ class AppSettings: "kill_program": 20, "gdb_quit": 10, }, - "dumper_options": { + "dumper_options": { "max_array_elements": 100, "max_recursion_depth": 10, "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,18 +95,44 @@ 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]) - else: - d[k] = u[k] - return d + """ + 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: + # 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}") - self._ensure_config_dir_exists() + self._ensure_config_dir_exists() try: with open(self.config_filepath, 'w', encoding='utf-8') as f: @@ -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): - 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 + # 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 {} 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 + 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.") \ No newline at end of file + logger.info("Settings updated in bulk (merged).") \ No newline at end of file diff --git a/cpp_python_debug/core/profile_executor.py b/cpp_python_debug/core/profile_executor.py new file mode 100644 index 0000000..e69de29 diff --git a/cpp_python_debug/gui/action_editor_window.py b/cpp_python_debug/gui/action_editor_window.py new file mode 100644 index 0000000..c146344 --- /dev/null +++ b/cpp_python_debug/gui/action_editor_window.py @@ -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 \ No newline at end of file diff --git a/cpp_python_debug/gui/main_window.py b/cpp_python_debug/gui/main_window.py index dedaa49..f1cf781 100644 --- a/cpp_python_debug/gui/main_window.py +++ b/cpp_python_debug/gui/main_window.py @@ -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__) @@ -70,6 +71,17 @@ class GDBGui(tk.Tk): options_menu.add_command(label="Configure Application...", command=self._open_config_window) 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.") diff --git a/cpp_python_debug/gui/profile_manager_window.py b/cpp_python_debug/gui/profile_manager_window.py new file mode 100644 index 0000000..18817fb --- /dev/null +++ b/cpp_python_debug/gui/profile_manager_window.py @@ -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("<>", 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("<>", 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 \ No newline at end of file diff --git a/todo.md b/todo.md index fa38dc7..e9a90bd 100644 --- a/todo.md +++ b/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.