# 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("<>", 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('', 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("", lambda e: self._finish_editing_param_value(save=True)) self.current_edit_widget.bind("", lambda e: self._finish_editing_param_value(save=True)) self.current_edit_widget.bind("", lambda e: self._finish_editing_param_value(save=False)) self.current_edit_widget.bind("", 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()