# 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