SXXXXXXX_LauncherTool/launchertool/gui/dialogs/sequence_dialogs.py
2025-05-07 16:04:50 +02:00

356 lines
16 KiB
Python

# LauncherTool/gui/dialogs/sequence_dialogs.py
"""
Dialogs for adding and editing sequences of applications.
"""
import tkinter as tk
from tkinter import ttk, messagebox
import logging
from typing import Optional, List, Dict, Any
import copy # For deep copying steps list if necessary
from ...core.config_manager import ConfigManager
from ...core.exceptions import DuplicateNameError, NameNotFoundError, ConfigError
from ..utils_gui import GuiUtils
from .step_dialogs import AddStepDialog, EditStepDialog
logger = logging.getLogger(__name__)
class BaseSequenceDialog(tk.Toplevel):
"""
Base class for Add and Edit Sequence dialogs.
"""
def __init__(self, parent: tk.Widget, config_manager: ConfigManager, title: str):
super().__init__(parent)
self.transient(parent)
self.title(title)
self.parent_widget = parent
self.config_manager = config_manager
self.result: Optional[Dict[str, Any]] = None
self.original_sequence_name: Optional[str] = None
self.current_steps_data: List[Dict[str, Any]] = []
self._setup_widgets()
GuiUtils.center_window(self, parent)
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
self.grab_set()
self.focus_set()
self.wait_window(self)
def _setup_widgets(self):
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(expand=True, fill=tk.BOTH)
main_frame.columnconfigure(0, weight=1)
name_frame = ttk.Frame(main_frame)
name_frame.grid(row=0, column=0, sticky=tk.EW, pady=(0, 5))
ttk.Label(name_frame, text="Sequence Name:").pack(side=tk.LEFT, padx=(0, 5))
self.name_entry_var = tk.StringVar()
self.name_entry = ttk.Entry(name_frame, textvariable=self.name_entry_var, width=50)
self.name_entry.pack(side=tk.LEFT, expand=True, fill=tk.X)
steps_labelframe = ttk.LabelFrame(main_frame, text="Sequence Steps")
steps_labelframe.grid(row=1, column=0, sticky=tk.NSEW, pady=(0, 10))
main_frame.rowconfigure(1, weight=1)
steps_labelframe.columnconfigure(0, weight=1)
steps_tree_frame = ttk.Frame(steps_labelframe)
steps_tree_frame.pack(expand=True, fill=tk.BOTH, padx=5, pady=5)
steps_tree_frame.columnconfigure(0, weight=1)
steps_tree_frame.rowconfigure(0, weight=1)
step_scrollbar_y = ttk.Scrollbar(steps_tree_frame, orient=tk.VERTICAL)
step_scrollbar_x = ttk.Scrollbar(steps_tree_frame, orient=tk.HORIZONTAL)
self.steps_tree = ttk.Treeview(
steps_tree_frame,
columns=('Order', 'Application', 'Wait Time (s)', 'Parameters'),
show='headings',
selectmode='browse',
yscrollcommand=step_scrollbar_y.set,
xscrollcommand=step_scrollbar_x.set
)
step_scrollbar_y.config(command=self.steps_tree.yview)
step_scrollbar_x.config(command=self.steps_tree.xview)
self.steps_tree.heading('Order', text='#')
self.steps_tree.heading('Application', text='Application Name')
self.steps_tree.heading('Wait Time (s)', text='Wait (s)')
self.steps_tree.heading('Parameters', text='Custom Params')
self.steps_tree.column('Order', width=30, minwidth=30, anchor=tk.CENTER, stretch=False)
self.steps_tree.column('Application', width=200, minwidth=150, anchor=tk.W)
self.steps_tree.column('Wait Time (s)', width=70, minwidth=60, anchor=tk.CENTER)
self.steps_tree.column('Parameters', width=100, minwidth=80, anchor=tk.CENTER)
step_scrollbar_y.grid(row=0, column=1, sticky=tk.NS)
step_scrollbar_x.grid(row=1, column=0, sticky=tk.EW)
self.steps_tree.grid(row=0, column=0, sticky=tk.NSEW)
step_buttons_frame = ttk.Frame(steps_labelframe)
step_buttons_frame.pack(fill=tk.X, padx=5, pady=(0,5))
self.add_step_button = ttk.Button(step_buttons_frame, text="Add Step", command=self._add_step)
self.add_step_button.pack(side=tk.LEFT, padx=(0,5))
self.edit_step_button = ttk.Button(step_buttons_frame, text="Edit Step", command=self._edit_step)
self.edit_step_button.pack(side=tk.LEFT, padx=(0,5))
self.delete_step_button = ttk.Button(step_buttons_frame, text="Delete Step", command=self._delete_step)
self.delete_step_button.pack(side=tk.LEFT, padx=(0,5))
self.move_step_up_button = ttk.Button(step_buttons_frame, text="Move Up", command=self._move_step_up)
self.move_step_up_button.pack(side=tk.LEFT, padx=(10,5))
self.move_step_down_button = ttk.Button(step_buttons_frame, text="Move Down", command=self._move_step_down)
self.move_step_down_button.pack(side=tk.LEFT)
self.steps_tree.bind("<<TreeviewSelect>>", self._on_step_select)
self._on_step_select()
dialog_buttons_frame = ttk.Frame(main_frame)
dialog_buttons_frame.grid(row=2, column=0, sticky=tk.EW, pady=(10, 0))
ttk.Frame(dialog_buttons_frame).pack(side=tk.LEFT, expand=True)
self.save_button = ttk.Button(dialog_buttons_frame, text="Save", command=self._on_save)
self.save_button.pack(side=tk.LEFT, padx=(0,5))
self.cancel_button = ttk.Button(dialog_buttons_frame, text="Cancel", command=self._on_cancel)
self.cancel_button.pack(side=tk.LEFT)
def _populate_steps_tree(self):
selected_iid_tuple = self.steps_tree.selection()
selected_iid = selected_iid_tuple[0] if selected_iid_tuple else None
for item in self.steps_tree.get_children():
self.steps_tree.delete(item)
for i, step_data in enumerate(self.current_steps_data):
app_name = step_data.get("application", "N/A")
wait_time = step_data.get("wait_time", 0.0)
custom_params_set = "Yes" if step_data.get("parameters") else "No"
# Use index as iid
current_iid = str(i)
self.steps_tree.insert(
'', tk.END,
iid=current_iid,
values=(i + 1, app_name, f"{float(wait_time):.1f}", custom_params_set)
)
if selected_iid and self.steps_tree.exists(selected_iid):
self.steps_tree.selection_set(selected_iid)
self.steps_tree.focus(selected_iid)
self._on_step_select()
def _on_step_select(self, event=None):
selected_ids = self.steps_tree.selection()
is_selection = bool(selected_ids)
num_steps = len(self.current_steps_data)
state_edit_delete = tk.NORMAL if is_selection else tk.DISABLED
self.edit_step_button.config(state=state_edit_delete)
self.delete_step_button.config(state=state_edit_delete)
idx = -1
if is_selection:
try:
idx = int(selected_ids[0])
except ValueError:
is_selection = False
can_move_up = is_selection and idx > 0
self.move_step_up_button.config(state=tk.NORMAL if can_move_up else tk.DISABLED)
can_move_down = is_selection and idx < (num_steps - 1)
self.move_step_down_button.config(state=tk.NORMAL if can_move_down else tk.DISABLED)
def _add_step(self):
logger.debug("Add Step button clicked.")
dialog = AddStepDialog(self, self.config_manager)
if dialog.result:
self.current_steps_data.append(dialog.result)
self._populate_steps_tree()
logger.info(f"Step for app '{dialog.result['application']}' added to sequence dialog.")
def _edit_step(self):
selected_ids = self.steps_tree.selection()
if not selected_ids:
messagebox.showwarning("Edit Step", "Please select a step to edit.", parent=self)
return
try:
step_index = int(selected_ids[0])
step_data_to_edit = self.current_steps_data[step_index]
except (ValueError, IndexError):
logger.error(f"Could not get step data for editing, selection: {selected_ids[0]}")
messagebox.showerror("Error", "Could not retrieve step data for editing.", parent=self)
return
logger.debug(f"Edit Step button clicked for step index: {step_index}")
# Create a deep copy to pass to the dialog so modifications don't affect current_steps_data until save
dialog = EditStepDialog(self, self.config_manager, copy.deepcopy(step_data_to_edit))
if dialog.result:
self.current_steps_data[step_index] = dialog.result
self._populate_steps_tree()
logger.info(f"Step for app '{dialog.result['application']}' updated in sequence dialog.")
def _delete_step(self):
selected_ids = self.steps_tree.selection()
if not selected_ids:
messagebox.showwarning("Delete Step", "Please select a step to delete.", parent=self)
return
try:
step_index = int(selected_ids[0])
step_app_name = self.current_steps_data[step_index].get("application", "Unknown")
except (ValueError, IndexError):
logger.error(f"Could not get step data for deletion, selection: {selected_ids[0]}")
messagebox.showerror("Error", "Could not retrieve step data for deletion.", parent=self)
return
if messagebox.askyesno("Confirm Delete",
f"Are you sure you want to delete step #{step_index + 1} ('{step_app_name}')?",
parent=self):
del self.current_steps_data[step_index]
self._populate_steps_tree()
logger.info(f"Step for app '{step_app_name}' deleted from sequence dialog.")
def _move_step_up(self):
selected_ids = self.steps_tree.selection()
if not selected_ids: return
try:
idx = int(selected_ids[0])
if idx > 0:
self.current_steps_data[idx], self.current_steps_data[idx-1] = \
self.current_steps_data[idx-1], self.current_steps_data[idx]
new_selected_iid = str(idx-1)
self._populate_steps_tree()
if self.steps_tree.exists(new_selected_iid):
self.steps_tree.selection_set(new_selected_iid)
self.steps_tree.focus(new_selected_iid)
except (ValueError, IndexError) as e:
logger.error(f"Error moving step up: {e}, selection: {selected_ids[0]}")
def _move_step_down(self):
selected_ids = self.steps_tree.selection()
if not selected_ids: return
try:
idx = int(selected_ids[0])
if idx < len(self.current_steps_data) - 1:
self.current_steps_data[idx], self.current_steps_data[idx+1] = \
self.current_steps_data[idx+1], self.current_steps_data[idx]
new_selected_iid = str(idx+1)
self._populate_steps_tree()
if self.steps_tree.exists(new_selected_iid):
self.steps_tree.selection_set(new_selected_iid)
self.steps_tree.focus(new_selected_iid)
except (ValueError, IndexError) as e:
logger.error(f"Error moving step down: {e}, selection: {selected_ids[0]}")
def _validate_inputs(self) -> bool:
name = self.name_entry_var.get().strip()
if not name:
messagebox.showerror("Validation Error", "Sequence Name cannot be empty.", parent=self)
self.name_entry.focus_set()
return False
return True
def _on_save(self):
raise NotImplementedError("Subclasses must implement _on_save")
def _on_cancel(self):
logger.debug(f"{self.title()} cancelled or closed.")
self.result = None
self.destroy()
class AddSequenceDialog(BaseSequenceDialog):
"""Dialog for adding a new sequence."""
def __init__(self, parent: tk.Widget, config_manager: ConfigManager):
super().__init__(parent, config_manager, title="Add New Sequence")
self._populate_steps_tree()
def _on_save(self):
if not self._validate_inputs():
return
seq_name = self.name_entry_var.get().strip()
sequence_data = {
"name": seq_name,
"steps": copy.deepcopy(self.current_steps_data) # Save a deep copy
}
try:
self.config_manager.add_sequence(sequence_data)
self.result = sequence_data
logger.info(f"Sequence '{seq_name}' added successfully through dialog.")
# messagebox.showinfo("Success", f"Sequence '{seq_name}' added successfully.", parent=self.parent_widget)
self.destroy()
except DuplicateNameError as e:
logger.warning(f"Failed to add sequence: {e}")
messagebox.showerror("Error Adding Sequence", str(e), parent=self)
self.name_entry.focus_set()
except ConfigError as e:
logger.error(f"Configuration error adding sequence '{seq_name}': {e}", exc_info=True)
messagebox.showerror("Configuration Error", f"Could not save sequence:\n{e}", parent=self)
except Exception as e:
logger.error(f"Unexpected error adding sequence '{seq_name}': {e}", exc_info=True)
messagebox.showerror("Unexpected Error", f"An unexpected error occurred:\n{e}", parent=self)
class EditSequenceDialog(BaseSequenceDialog):
"""Dialog for editing an existing sequence."""
def __init__(self, parent: tk.Widget, config_manager: ConfigManager, sequence_name_to_edit: str):
self.original_sequence_name = sequence_name_to_edit
super().__init__(parent, config_manager, title=f"Edit Sequence: {sequence_name_to_edit}")
self._load_initial_data()
def _load_initial_data(self):
try:
seq_data = self.config_manager.get_sequence_by_name(self.original_sequence_name)
self.name_entry_var.set(seq_data.get("name", ""))
# Make a deep copy of steps for editing
self.current_steps_data = copy.deepcopy(seq_data.get("steps", []))
self._populate_steps_tree()
except NameNotFoundError:
logger.error(f"Cannot edit sequence. Name '{self.original_sequence_name}' not found.")
messagebox.showerror("Error", f"Sequence '{self.original_sequence_name}' not found.", parent=self.parent_widget)
self.destroy()
except Exception as e:
logger.error(f"Unexpected error loading sequence data for edit: {e}", exc_info=True)
messagebox.showerror("Error", f"Could not load sequence data for editing:\n{e}", parent=self.parent_widget)
self.destroy()
def _on_save(self):
if not self._validate_inputs():
return
new_seq_name = self.name_entry_var.get().strip()
updated_sequence_data = {
"name": new_seq_name,
"steps": copy.deepcopy(self.current_steps_data) # Save a deep copy
}
try:
self.config_manager.update_sequence(self.original_sequence_name, updated_sequence_data)
self.result = updated_sequence_data
logger.info(f"Sequence '{self.original_sequence_name}' updated to '{new_seq_name}'.")
# messagebox.showinfo("Success", f"Sequence '{new_seq_name}' updated successfully.", parent=self.parent_widget)
self.destroy()
except NameNotFoundError as e:
logger.error(f"Update failed: Original sequence '{self.original_sequence_name}' not found: {e}", exc_info=True)
messagebox.showerror("Error Updating Sequence", str(e), parent=self)
except DuplicateNameError as e:
logger.warning(f"Failed to update sequence: {e}")
messagebox.showerror("Error Updating Sequence", str(e), parent=self)
self.name_entry.focus_set()
except ConfigError as e:
logger.error(f"Configuration error updating sequence '{new_seq_name}': {e}", exc_info=True)
messagebox.showerror("Configuration Error", f"Could not save sequence updates:\n{e}", parent=self)
except Exception as e:
logger.error(f"Unexpected error updating sequence '{new_seq_name}': {e}", exc_info=True)
messagebox.showerror("Unexpected Error", f"An unexpected error occurred:\n{e}", parent=self)