SXXXXXXX_LauncherTool/launchertool/gui/dialogs/step_dialogs.py

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()