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

346 lines
16 KiB
Python

# LauncherTool/gui/dialogs/step_dialogs.py
"""
Dialogs for adding and editing steps within a sequence.
"""
import tkinter as tk
from tkinter import ttk, messagebox
import logging
from typing import Optional, List, Dict, Any
import copy # For deep copy of step_specific_parameters
from ...core.config_manager import ConfigManager
from ...core.exceptions import ApplicationNotFoundError
from ..utils_gui import GuiUtils
logger = logging.getLogger(__name__)
class BaseStepDialog(tk.Toplevel):
"""
Base class for Add and Edit Step 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.step_application_name: Optional[str] = None
self.step_wait_time: float = 0.0
self.step_specific_parameters: Dict[str, str] = {}
self.application_defined_parameters: List[Dict[str, str]] = []
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)
app_frame = ttk.Frame(main_frame)
app_frame.grid(row=0, column=0, sticky=tk.EW, pady=(0, 5))
ttk.Label(app_frame, text="Application:").pack(side=tk.LEFT, padx=(0,5))
self.app_combo_var = tk.StringVar()
self.app_combo = ttk.Combobox(
app_frame,
textvariable=self.app_combo_var,
state="readonly",
width=48
)
self.app_combo.pack(side=tk.LEFT, expand=True, fill=tk.X)
self._populate_applications_combobox()
self.app_combo.bind("<<ComboboxSelected>>", self._on_application_selected_event)
wait_frame = ttk.Frame(main_frame)
wait_frame.grid(row=1, column=0, sticky=tk.EW, pady=(0, 10))
ttk.Label(wait_frame, text="Wait Time (seconds):").pack(side=tk.LEFT, padx=(0,5))
self.wait_time_var = tk.StringVar(value="0.0") # Initialize here
self.wait_time_spinbox = ttk.Spinbox(
wait_frame,
from_=0.0,
to=3600.0,
increment=0.1,
textvariable=self.wait_time_var,
width=10,
format="%.1f"
)
self.wait_time_spinbox.pack(side=tk.LEFT)
self.params_labelframe = ttk.LabelFrame(main_frame, text="Configure Parameters for this Step")
self.params_labelframe.grid(row=2, column=0, sticky=tk.NSEW, pady=(0,10))
main_frame.rowconfigure(2, weight=1)
self.params_labelframe.columnconfigure(0, weight=1)
params_tree_frame = ttk.Frame(self.params_labelframe)
params_tree_frame.pack(expand=True, fill=tk.BOTH, padx=5, pady=5)
params_tree_frame.columnconfigure(0, weight=1)
params_tree_frame.rowconfigure(0, weight=1)
param_scrollbar_y = ttk.Scrollbar(params_tree_frame, orient=tk.VERTICAL)
param_scrollbar_x = ttk.Scrollbar(params_tree_frame, orient=tk.HORIZONTAL)
self.params_tree = ttk.Treeview(
params_tree_frame,
columns=('Name', 'App Default', 'Step Value', 'Description'),
show='headings',
selectmode='none',
yscrollcommand=param_scrollbar_y.set,
xscrollcommand=param_scrollbar_x.set
)
param_scrollbar_y.config(command=self.params_tree.yview)
param_scrollbar_x.config(command=self.params_tree.xview)
self.params_tree.heading('Name', text='Param Name')
self.params_tree.heading('App Default', text='App Default Value')
self.params_tree.heading('Step Value', text='Override Value for Step')
self.params_tree.heading('Description', text='Description')
self.params_tree.column('Name', width=120, minwidth=100, anchor=tk.W)
self.params_tree.column('App Default', width=120, minwidth=100, anchor=tk.W)
self.params_tree.column('Step Value', width=150, minwidth=120, anchor=tk.W)
self.params_tree.column('Description', width=180, minwidth=150, anchor=tk.W)
param_scrollbar_y.grid(row=0, column=1, sticky=tk.NS)
param_scrollbar_x.grid(row=1, column=0, sticky=tk.EW)
self.params_tree.grid(row=0, column=0, sticky=tk.NSEW)
self.params_tree.bind('<Double-1>', self._on_param_tree_double_click)
self.current_edit_item_iid = None # Store iid of item being edited
self.current_edit_widget = None
dialog_buttons_frame = ttk.Frame(main_frame)
dialog_buttons_frame.grid(row=3, 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_applications_combobox(self):
try:
app_names = [app["name"] for app in self.config_manager.get_applications()]
self.app_combo['values'] = sorted(app_names)
except Exception as e:
logger.error(f"Failed to populate applications combobox: {e}", exc_info=True)
messagebox.showerror("Error", "Could not load application list.", parent=self)
def _on_application_selected_event(self, event=None):
"""Handles the ComboboxSelected event, clears overrides if app changed by user."""
selected_app_name = self.app_combo_var.get()
# Only clear overrides if the app was changed by user interaction,
# not during initial load where step_application_name might be the same.
if self.step_application_name is not None and self.step_application_name != selected_app_name:
logger.debug(f"Application changed by user from '{self.step_application_name}' to '{selected_app_name}'. Clearing step-specific parameter overrides.")
self.step_specific_parameters.clear()
self._load_parameters_for_selected_app(selected_app_name)
def _load_parameters_for_selected_app(self, app_name: Optional[str]):
"""Loads application defined parameters and populates the tree."""
self.step_application_name = app_name # Update current app for the step
if not app_name:
self.application_defined_parameters = []
self._populate_step_parameters_tree()
return
try:
app_data = self.config_manager.get_application_by_name(app_name)
self.application_defined_parameters = app_data.get("parameters", [])
self._populate_step_parameters_tree()
except ApplicationNotFoundError:
logger.warning(f"Selected application '{app_name}' not found in config.")
self.application_defined_parameters = []
# self.step_specific_parameters.clear() # Already handled if app changed via event
self._populate_step_parameters_tree()
except Exception as e:
logger.error(f"Error loading parameters for app '{app_name}': {e}", exc_info=True)
messagebox.showerror("Error", f"Could not load parameters for {app_name}.", parent=self)
def _populate_step_parameters_tree(self):
self._finish_editing_param_value(save=True)
for item in self.params_tree.get_children():
self.params_tree.delete(item)
if not self.step_application_name : # No app selected yet
self.params_labelframe.config(text="Select an Application to Configure Parameters")
return
if not self.application_defined_parameters:
self.params_labelframe.config(text=f"No Parameters Defined for '{self.step_application_name}'")
return
self.params_labelframe.config(text=f"Configure Parameters for '{self.step_application_name}' in this Step")
for app_param in self.application_defined_parameters:
param_name = app_param.get("name", "UnnamedParam")
app_default = app_param.get("default_value", "")
description = app_param.get("description", "")
step_value = self.step_specific_parameters.get(param_name, "")
# Use param_name as iid, assuming it's unique within an app's params
self.params_tree.insert(
'', tk.END,
iid=param_name,
values=(param_name, app_default, step_value, description)
)
def _on_param_tree_double_click(self, event):
self._finish_editing_param_value(save=True)
region = self.params_tree.identify_region(event.x, event.y)
column_id_str = self.params_tree.identify_column(event.x)
if region != 'cell' or column_id_str != '#3':
return
self.current_edit_item_iid = self.params_tree.identify_row(event.y)
if not self.current_edit_item_iid:
return
x, y, width, height = self.params_tree.bbox(self.current_edit_item_iid, column=column_id_str)
current_values = self.params_tree.item(self.current_edit_item_iid, 'values')
current_step_value = current_values[2] # 'Step Value'
entry_var = tk.StringVar(value=current_step_value)
self.current_edit_widget = ttk.Entry(self.params_tree, textvariable=entry_var)
self.current_edit_widget.place(x=x, y=y, width=width, height=height, anchor='nw')
self.current_edit_widget.focus_set()
self.current_edit_widget.selection_range(0, tk.END)
self.current_edit_widget.bind("<Return>", lambda e: self._finish_editing_param_value(save=True))
self.current_edit_widget.bind("<KP_Enter>", lambda e: self._finish_editing_param_value(save=True))
self.current_edit_widget.bind("<Escape>", lambda e: self._finish_editing_param_value(save=False))
self.current_edit_widget.bind("<FocusOut>", lambda e: self._finish_editing_param_value(save=True))
def _finish_editing_param_value(self, save: bool):
if self.current_edit_widget and self.current_edit_item_iid:
if save:
new_value = self.current_edit_widget.get()
# iid is the param_name
param_name = self.current_edit_item_iid
current_values = list(self.params_tree.item(self.current_edit_item_iid, 'values'))
current_values[2] = new_value
self.params_tree.item(self.current_edit_item_iid, values=tuple(current_values))
if new_value.strip() == "" and param_name in self.step_specific_parameters :
del self.step_specific_parameters[param_name]
logger.debug(f"Override for '{param_name}' cleared.")
elif new_value.strip() != "":
self.step_specific_parameters[param_name] = new_value
logger.debug(f"Override for '{param_name}' set to '{new_value}'.")
self.current_edit_widget.destroy()
self.current_edit_widget = None
self.current_edit_item_iid = None
self.params_tree.focus_set()
def _validate_inputs(self) -> bool:
selected_app = self.app_combo_var.get()
if not selected_app:
messagebox.showerror("Validation Error", "An application must be selected.", parent=self)
self.app_combo.focus_set()
return False
try:
wait_time_str = self.wait_time_var.get()
current_step_wait_time = float(wait_time_str)
if current_step_wait_time < 0:
messagebox.showerror("Validation Error", "Wait Time cannot be negative.", parent=self)
self.wait_time_spinbox.focus_set()
return False
self.step_wait_time = current_step_wait_time # Store validated float
except ValueError:
messagebox.showerror("Validation Error", "Invalid Wait Time. Please enter a number.", parent=self)
self.wait_time_spinbox.focus_set()
return False
# self.step_application_name is already set by _load_parameters_for_selected_app
return True
def _on_save(self):
raise NotImplementedError("Subclasses must implement _on_save")
def _on_cancel(self):
self._finish_editing_param_value(save=False)
logger.debug(f"{self.title()} cancelled or closed.")
self.result = None
self.destroy()
class AddStepDialog(BaseStepDialog):
"""Dialog for adding a new step to a sequence."""
def __init__(self, parent: tk.Widget, config_manager: ConfigManager):
super().__init__(parent, config_manager, title="Add New Step")
# Initial call to set the label frame text correctly if no app is selected initially
self._load_parameters_for_selected_app(None)
def _on_save(self):
self._finish_editing_param_value(save=True)
if not self._validate_inputs():
return
self.result = {
"application": self.step_application_name, # Set by _load_parameters_for_selected_app
"wait_time": self.step_wait_time, # Set by _validate_inputs
"parameters": copy.deepcopy(self.step_specific_parameters)
}
logger.info(f"Step data for '{self.step_application_name}' prepared for adding.")
self.destroy()
class EditStepDialog(BaseStepDialog):
"""Dialog for editing an existing step in a sequence."""
def __init__(self, parent: tk.Widget, config_manager: ConfigManager, step_data_to_edit: Dict[str, Any]):
self.step_data_to_edit = step_data_to_edit
super().__init__(parent, config_manager, title=f"Edit Step: {step_data_to_edit.get('application', '')}")
self._load_initial_data()
def _load_initial_data(self):
app_name = self.step_data_to_edit.get("application")
wait_time = self.step_data_to_edit.get("wait_time", 0.0)
# Deep copy to avoid modifying the original dict from sequence_dialog's current_steps_data
self.step_specific_parameters = copy.deepcopy(self.step_data_to_edit.get("parameters", {}))
if app_name and app_name in self.app_combo['values']:
self.app_combo_var.set(app_name)
# This will trigger _on_application_selected_event, then _load_parameters_for_selected_app
# which loads app_defined_parameters and populates tree using existing step_specific_parameters
self._load_parameters_for_selected_app(app_name)
else:
if app_name:
logger.warning(f"App '{app_name}' from step data not in config. Clearing selection.")
self.app_combo_var.set("")
self._load_parameters_for_selected_app(None)
self.wait_time_var.set(f"{float(wait_time):.1f}")
# _populate_step_parameters_tree is implicitly called via _load_parameters_for_selected_app
def _on_save(self):
self._finish_editing_param_value(save=True)
if not self._validate_inputs():
return
self.result = {
"application": self.step_application_name,
"wait_time": self.step_wait_time,
"parameters": copy.deepcopy(self.step_specific_parameters)
}
logger.info(f"Step data for '{self.step_application_name}' (original: '{self.step_data_to_edit.get('application')}') prepared.")
self.destroy()