SXXXXXXX_CppPythonDebug/cpp_python_debug/gui/profile_manager_window.py

869 lines
37 KiB
Python

# 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