387 lines
19 KiB
Python
387 lines
19 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, Callable # Aggiunto Callable
|
|
import copy
|
|
|
|
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.
|
|
"""
|
|
# Aggiunto attributo per memorizzare i nomi delle app
|
|
_application_names: List[str] = []
|
|
|
|
def __init__(self, parent: tk.Widget, config_manager: ConfigManager, title: str, load_data_method: Optional[Callable[[], None]] = None): # Aggiunto load_data_method
|
|
logger.debug(f"BaseStepDialog __init__ - START - Title: '{title}'")
|
|
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]] = []
|
|
|
|
logger.debug("BaseStepDialog: Calling _setup_widgets()")
|
|
self._setup_widgets()
|
|
logger.debug("BaseStepDialog: _setup_widgets() completed.")
|
|
|
|
if load_data_method:
|
|
logger.debug("BaseStepDialog: Calling provided load_data_method.")
|
|
load_data_method()
|
|
logger.debug("BaseStepDialog: load_data_method completed.")
|
|
else:
|
|
logger.debug("BaseStepDialog: No load_data_method provided or needed (e.g., for Add dialog).")
|
|
# Per AddDialog, carica i parametri per l'app eventualmente selezionata (nessuna)
|
|
self._load_parameters_for_selected_app(self.app_combo_var.get() or None)
|
|
|
|
|
|
GuiUtils.center_window(self, parent)
|
|
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
|
|
|
self.grab_set()
|
|
self.focus_set()
|
|
logger.debug(f"BaseStepDialog __init__ for '{title}': Now calling wait_window().")
|
|
self.wait_window(self)
|
|
logger.debug(f"BaseStepDialog __init__ - END - Title: '{title}' (after wait_window)")
|
|
|
|
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)
|
|
|
|
# --- Application Selection ---
|
|
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() # Chiama per riempire i valori
|
|
self.app_combo.bind("<<ComboboxSelected>>", self._on_application_selected_event)
|
|
|
|
# --- Wait Time ---
|
|
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")
|
|
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)
|
|
|
|
# --- Step-Specific Parameters ---
|
|
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
|
|
self.current_edit_widget = None
|
|
|
|
# --- Dialog Buttons (Save, Cancel) ---
|
|
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:
|
|
# Salva i nomi nell'attributo di classe/istanza
|
|
self._application_names = sorted([app["name"] for app in self.config_manager.get_applications()])
|
|
self.app_combo['values'] = self._application_names # Imposta i valori nel widget
|
|
logger.debug(f"Populated application combobox with names: {self._application_names}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to populate applications combobox: {e}", exc_info=True)
|
|
self._application_names = [] # Assicura che sia una lista vuota in caso di errore
|
|
self.app_combo['values'] = []
|
|
messagebox.showerror("Error", "Could not load application list.", parent=self)
|
|
|
|
def _on_application_selected_event(self, event=None):
|
|
selected_app_name = self.app_combo_var.get()
|
|
# Solo se l'utente cambia selezione rispetto a quella corrente
|
|
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 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]):
|
|
self.step_application_name = app_name
|
|
|
|
if not app_name:
|
|
self.application_defined_parameters = []
|
|
self._populate_step_parameters_tree() # Aggiorna (svuota) albero parametri
|
|
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 (in _load_parameters).")
|
|
self.application_defined_parameters = []
|
|
self._populate_step_parameters_tree()
|
|
except Exception as e:
|
|
logger.error(f"Error loading parameters for app '{app_name}': {e}", exc_info=True)
|
|
self.application_defined_parameters = []
|
|
self._populate_step_parameters_tree()
|
|
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 :
|
|
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}' (Double-click 'Override Value' to edit)")
|
|
|
|
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, "") # Ottiene override o stringa vuota
|
|
|
|
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': # Colonna 'Step Value'
|
|
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]
|
|
|
|
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:
|
|
param_name = self.current_edit_item_iid # iid è il nome del parametro
|
|
if save:
|
|
new_value = self.current_edit_widget.get()
|
|
current_values = list(self.params_tree.item(self.current_edit_item_iid, 'values'))
|
|
current_values[2] = new_value # Aggiorna la colonna 'Step Value'
|
|
self.params_tree.item(self.current_edit_item_iid, values=tuple(current_values))
|
|
|
|
# Aggiorna il dizionario degli override specifici del passo
|
|
if new_value.strip() == "": # Se l'override viene cancellato
|
|
if param_name in self.step_specific_parameters:
|
|
del self.step_specific_parameters[param_name]
|
|
logger.debug(f"Override for step parameter '{param_name}' cleared.")
|
|
else: # Se viene impostato un nuovo override
|
|
self.step_specific_parameters[param_name] = new_value
|
|
logger.debug(f"Override for step parameter '{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 # Salva il float validato
|
|
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 dovrebbe essere già impostato da _load_parameters_for_selected_app
|
|
if not self.step_application_name or self.step_application_name != selected_app:
|
|
logger.warning("Validation mismatch: step_application_name != selected_app in combobox.")
|
|
self.step_application_name = selected_app # Sincronizza per sicurezza
|
|
|
|
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"BaseStepDialog: '{self.title()}' cancelled or closed by user.")
|
|
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):
|
|
# Non passa load_data_method, verrà gestito dalla base
|
|
super().__init__(parent, config_manager, title="Add New Step", load_data_method=None)
|
|
logger.debug("AddStepDialog __init__ completed.")
|
|
|
|
def _on_save(self):
|
|
self._finish_editing_param_value(save=True) # Finalizza edit inline
|
|
if not self._validate_inputs():
|
|
return
|
|
|
|
# I valori validati sono in self.step_application_name, self.step_wait_time
|
|
# e self.step_specific_parameters
|
|
self.result = {
|
|
"application": self.step_application_name,
|
|
"wait_time": self.step_wait_time,
|
|
"parameters": copy.deepcopy(self.step_specific_parameters) # Usa una copia
|
|
}
|
|
logger.info(f"AddStepDialog: Step data for '{self.step_application_name}' prepared.")
|
|
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]):
|
|
logger.debug(f"EditStepDialog __init__ - START - Step data to edit: {step_data_to_edit}")
|
|
# Salva i dati PRIMA di chiamare super, perché _load_initial_data ne avrà bisogno
|
|
self.step_data_to_edit = step_data_to_edit
|
|
# Passa self._load_initial_data a super()
|
|
super().__init__(parent, config_manager, title=f"Edit Step: {step_data_to_edit.get('application', '')}", load_data_method=self._load_initial_data)
|
|
logger.debug(f"EditStepDialog __init__ - END (after super call completed)")
|
|
|
|
def _load_initial_data(self):
|
|
# Chiamato da BaseStepDialog.__init__
|
|
if not self.step_data_to_edit: # Sicurezza
|
|
logger.error("EditStepDialog _load_initial_data: step_data_to_edit is missing.")
|
|
self.after_idle(self.destroy)
|
|
return
|
|
|
|
app_name = self.step_data_to_edit.get("application")
|
|
wait_time = self.step_data_to_edit.get("wait_time", 0.0)
|
|
# Importante: Fa una copia profonda subito per step_specific_parameters
|
|
self.step_specific_parameters = copy.deepcopy(self.step_data_to_edit.get("parameters", {}))
|
|
logger.debug(f"EditStepDialog _load_initial_data: Initial step overrides: {self.step_specific_parameters}")
|
|
|
|
# Usa la lista _application_names caricata da _populate_applications_combobox
|
|
if app_name and app_name in self._application_names:
|
|
self.app_combo_var.set(app_name)
|
|
# Chiamata esplicita per caricare i parametri dell'app e popolare l'albero
|
|
# Non si affida all'evento ComboboxSelected qui perché potremmo essere ancora nel costruttore
|
|
self._load_parameters_for_selected_app(app_name)
|
|
else:
|
|
if app_name:
|
|
logger.warning(f"EditStepDialog _load_initial_data: App '{app_name}' from step data not found in available apps: {self._application_names}. Clearing selection.")
|
|
messagebox.showwarning("Application Not Found",
|
|
f"The application '{app_name}' originally selected for this step was not found.\n"
|
|
"Please select a valid application.", parent=self)
|
|
self.app_combo_var.set("")
|
|
self._load_parameters_for_selected_app(None) # Svuota l'albero dei parametri
|
|
|
|
self.wait_time_var.set(f"{float(wait_time):.1f}")
|
|
logger.info(f"EditStepDialog _load_initial_data: Data loaded for app '{app_name}'.")
|
|
|
|
|
|
def _on_save(self):
|
|
self._finish_editing_param_value(save=True) # Finalizza edit inline
|
|
if not self._validate_inputs():
|
|
return
|
|
|
|
# I valori validati sono in self.step_application_name, self.step_wait_time
|
|
# e self.step_specific_parameters
|
|
self.result = {
|
|
"application": self.step_application_name,
|
|
"wait_time": self.step_wait_time,
|
|
"parameters": copy.deepcopy(self.step_specific_parameters) # Usa una copia
|
|
}
|
|
original_app = self.step_data_to_edit.get('application', 'N/A')
|
|
logger.info(f"EditStepDialog: Step data for '{self.step_application_name}' (original: '{original_app}') prepared.")
|
|
self.destroy() |