428 lines
22 KiB
Python
428 lines
22 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, Callable # Aggiunto Callable
|
|
import copy
|
|
|
|
from ...core.config_manager import ConfigManager
|
|
from ...core.exceptions import DuplicateNameError, NameNotFoundError, ConfigError
|
|
from ..utils_gui import GuiUtils
|
|
from .step_dialogs import AddStepDialog, EditStepDialog # Assicurati che sia importato
|
|
|
|
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, load_data_method: Optional[Callable[[], None]] = None): # Aggiunto load_data_method
|
|
logger.debug(f"BaseSequenceDialog __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.original_sequence_name è gestito dalle sottoclassi
|
|
|
|
self.current_steps_data: List[Dict[str, Any]] = [] # Lista dei passi per la sequenza corrente
|
|
|
|
logger.debug("BaseSequenceDialog: Calling _setup_widgets()")
|
|
self._setup_widgets() # Crea i widget
|
|
logger.debug("BaseSequenceDialog: _setup_widgets() completed.")
|
|
|
|
# Carica i dati specifici della sottoclasse (es. per EditDialog) PRIMA di rendere modale e attendere
|
|
if load_data_method:
|
|
logger.debug("BaseSequenceDialog: Calling provided load_data_method.")
|
|
load_data_method()
|
|
logger.debug("BaseSequenceDialog: load_data_method completed.")
|
|
else:
|
|
logger.debug("BaseSequenceDialog: No load_data_method provided or needed (e.g., for Add dialog).")
|
|
if not hasattr(self, 'original_sequence_name'): # Se è AddDialog, popola albero vuoto
|
|
self._populate_steps_tree()
|
|
|
|
|
|
GuiUtils.center_window(self, parent)
|
|
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
|
|
|
self.grab_set()
|
|
self.focus_set()
|
|
logger.debug(f"BaseSequenceDialog __init__ for '{title}': Now calling wait_window().")
|
|
self.wait_window(self)
|
|
logger.debug(f"BaseSequenceDialog __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)
|
|
|
|
# --- Sequence Name ---
|
|
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 ---
|
|
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() # Init button states
|
|
|
|
# --- Dialog Buttons (Save, Cancel) ---
|
|
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) # Spacer
|
|
|
|
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):
|
|
logger.debug(f"BaseSequenceDialog: Populating steps tree with {len(self.current_steps_data)} steps.")
|
|
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)
|
|
# "parameters" in step_data è un dizionario. Se non è vuoto, ci sono custom params.
|
|
custom_params_set = "Yes" if step_data.get("parameters") else "No"
|
|
|
|
current_iid = str(i) # Usare l'indice come iid per facilitare riordino e selezione
|
|
self.steps_tree.insert(
|
|
'', tk.END,
|
|
iid=current_iid,
|
|
values=(i + 1, app_name, f"{float(wait_time):.1f}", custom_params_set)
|
|
)
|
|
logger.debug(f"Inserted step: iid='{current_iid}', values=({i+1}, '{app_name}', {wait_time}, '{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]) # iid è l'indice del passo
|
|
except ValueError:
|
|
logger.warning(f"Invalid iid '{selected_ids[0]}' in _on_step_select.")
|
|
is_selection = False # Tratta come se non ci fosse selezione valida per i pulsanti di spostamento
|
|
|
|
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("BaseSequenceDialog: Add Step button clicked.")
|
|
dialog = AddStepDialog(self, self.config_manager) # 'self' (BaseSequenceDialog) è il parent
|
|
if dialog.result: # dialog.result è il dizionario step_data
|
|
self.current_steps_data.append(dialog.result)
|
|
self._populate_steps_tree() # Aggiorna la treeview con il nuovo passo
|
|
logger.info(f"BaseSequenceDialog: Step for app '{dialog.result['application']}' added to current sequence steps.")
|
|
|
|
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]) # L'iid è l'indice
|
|
step_data_to_edit = self.current_steps_data[step_index]
|
|
except (ValueError, IndexError) as e:
|
|
logger.error(f"BaseSequenceDialog: Could not get step data for editing. Selection iid: '{selected_ids[0]}', Error: {e}")
|
|
messagebox.showerror("Error", "Could not retrieve step data for editing.", parent=self)
|
|
return
|
|
|
|
logger.debug(f"BaseSequenceDialog: Edit Step button clicked for step at index {step_index}.")
|
|
# Passa una COPIA PROFONDA dei dati del passo al dialogo di modifica, così le modifiche
|
|
# non si riflettono su self.current_steps_data finché EditStepDialog non ritorna con successo.
|
|
dialog = EditStepDialog(self, self.config_manager, copy.deepcopy(step_data_to_edit))
|
|
if dialog.result: # dialog.result è il dizionario step_data aggiornato
|
|
self.current_steps_data[step_index] = dialog.result # Sostituisci con i dati aggiornati
|
|
self._populate_steps_tree() # Aggiorna la treeview
|
|
logger.info(f"BaseSequenceDialog: Step for app '{dialog.result['application']}' (index {step_index}) updated.")
|
|
|
|
|
|
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) as e:
|
|
logger.error(f"BaseSequenceDialog: Could not get step data for deletion. Selection iid: '{selected_ids[0]}', Error: {e}")
|
|
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() # Ridisegna la treeview
|
|
logger.info(f"BaseSequenceDialog: Step for app '{step_app_name}' (original index {step_index}) deleted.")
|
|
|
|
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) # Il nuovo iid (indice) dell'elemento spostato
|
|
self._populate_steps_tree()
|
|
if self.steps_tree.exists(new_selected_iid): # Riprova a selezionare
|
|
self.steps_tree.selection_set(new_selected_iid)
|
|
self.steps_tree.focus(new_selected_iid)
|
|
logger.debug(f"Moved step from index {idx} to {idx-1}.")
|
|
except (ValueError, IndexError) as e:
|
|
logger.error(f"Error moving step up: {e}, selection iid: {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)
|
|
logger.debug(f"Moved step from index {idx} to {idx+1}.")
|
|
except (ValueError, IndexError) as e:
|
|
logger.error(f"Error moving step down: {e}, selection iid: {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"BaseSequenceDialog: '{self.title()}' cancelled or closed by user.")
|
|
self.result = None
|
|
self.destroy()
|
|
|
|
|
|
class AddSequenceDialog(BaseSequenceDialog):
|
|
"""Dialog for adding a new sequence."""
|
|
def __init__(self, parent: tk.Widget, config_manager: ConfigManager):
|
|
# AddSequenceDialog non carica dati esistenti, passa None per load_data_method
|
|
# La _populate_steps_tree (vuota) viene chiamata dalla base se load_data_method è None
|
|
super().__init__(parent, config_manager, title="Add New Sequence", load_data_method=None)
|
|
logger.debug("AddSequenceDialog __init__ completed.")
|
|
|
|
|
|
def _on_save(self):
|
|
if not self._validate_inputs():
|
|
return
|
|
|
|
seq_name = self.name_entry_var.get().strip()
|
|
# self.current_steps_data è già aggiornata dai dialoghi dei passi e dal riordino
|
|
|
|
# Salva una copia profonda per evitare che modifiche future a current_steps_data
|
|
# (se il dialogo venisse riutilizzato o manipolato dopo) influenzino i dati salvati.
|
|
sequence_data = {
|
|
"name": seq_name,
|
|
"steps": copy.deepcopy(self.current_steps_data)
|
|
}
|
|
|
|
try:
|
|
self.config_manager.add_sequence(sequence_data)
|
|
self.result = sequence_data # Imposta result per MainWindow
|
|
logger.info(f"AddSequenceDialog: Sequence '{seq_name}' added successfully to config.")
|
|
self.destroy() # Chiudi dialogo
|
|
except DuplicateNameError as e:
|
|
logger.warning(f"AddSequenceDialog: Failed to add sequence due to duplicate name: {e}")
|
|
messagebox.showerror("Error Adding Sequence", str(e), parent=self)
|
|
self.name_entry.focus_set()
|
|
except ConfigError as e:
|
|
logger.error(f"AddSequenceDialog: 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"AddSequenceDialog: 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):
|
|
logger.debug(f"EditSequenceDialog __init__ - START - Sequence to edit: '{sequence_name_to_edit}'")
|
|
self.original_sequence_name = sequence_name_to_edit # Deve essere impostato PRIMA di super()
|
|
|
|
# Passa self._load_initial_data al costruttore della classe base
|
|
super().__init__(parent, config_manager, title=f"Edit Sequence: {self.original_sequence_name}", load_data_method=self._load_initial_data)
|
|
|
|
logger.debug(f"EditSequenceDialog __init__ - END - for '{self.original_sequence_name}' (after super call completed)")
|
|
|
|
|
|
def _load_initial_data(self):
|
|
# Questo metodo è chiamato da BaseSequenceDialog.__init__
|
|
if not self.original_sequence_name:
|
|
logger.error("EditSequenceDialog _load_initial_data: original_sequence_name is None or empty. Dialog should close.")
|
|
if self.parent_widget: # Dovrebbe sempre esistere
|
|
messagebox.showerror("Initialization Error",
|
|
"Cannot edit: No sequence name specified for editing.",
|
|
parent=self.parent_widget)
|
|
self.after_idle(self.destroy)
|
|
return
|
|
|
|
logger.debug(f"EditSequenceDialog _load_initial_data: Attempting to load for: '{self.original_sequence_name}'")
|
|
try:
|
|
seq_data = self.config_manager.get_sequence_by_name(self.original_sequence_name)
|
|
logger.debug(f"EditSequenceDialog _load_initial_data: Data from ConfigManager for '{self.original_sequence_name}': {seq_data}")
|
|
|
|
if not seq_data: # Salvaguardia
|
|
logger.error(f"EditSequenceDialog _load_initial_data: ConfigManager returned None for '{self.original_sequence_name}'.")
|
|
messagebox.showerror("Load Error",
|
|
f"Could not retrieve data for sequence '{self.original_sequence_name}'.",
|
|
parent=self.parent_widget)
|
|
self.after_idle(self.destroy)
|
|
return
|
|
|
|
self.name_entry_var.set(seq_data.get("name", ""))
|
|
# Usa copy.deepcopy per i passi, poiché contengono dizionari (mutabili)
|
|
self.current_steps_data = copy.deepcopy(seq_data.get("steps", []))
|
|
|
|
logger.debug(f"EditSequenceDialog _load_initial_data: Name set to '{self.name_entry_var.get()}'. Number of steps: {len(self.current_steps_data)}")
|
|
self._populate_steps_tree() # Popola la treeview con i passi caricati
|
|
|
|
logger.info(f"EditSequenceDialog _load_initial_data: Successfully loaded data for '{self.original_sequence_name}'.")
|
|
|
|
except NameNotFoundError:
|
|
logger.error(f"EditSequenceDialog _load_initial_data: Sequence '{self.original_sequence_name}' not found (NameNotFoundError).")
|
|
messagebox.showerror("Load Error",
|
|
f"Sequence '{self.original_sequence_name}' not found. Cannot edit.",
|
|
parent=self.parent_widget)
|
|
self.after_idle(self.destroy)
|
|
except Exception as e:
|
|
logger.error(f"EditSequenceDialog _load_initial_data: Unexpected error for '{self.original_sequence_name}': {e}", exc_info=True)
|
|
messagebox.showerror("Load Error",
|
|
f"An unexpected error occurred while loading data for '{self.original_sequence_name}':\n{e}",
|
|
parent=self.parent_widget)
|
|
self.after_idle(self.destroy)
|
|
|
|
def _on_save(self):
|
|
if not self.original_sequence_name:
|
|
messagebox.showerror("Save Error", "Cannot save: Critical information about the original sequence is missing.", parent=self)
|
|
logger.error("EditSequenceDialog _on_save: Attempted to save without a valid original_sequence_name.")
|
|
return
|
|
|
|
if not self._validate_inputs():
|
|
return
|
|
|
|
new_seq_name = self.name_entry_var.get().strip()
|
|
# self.current_steps_data è già aggiornata
|
|
|
|
updated_sequence_data = {
|
|
"name": new_seq_name,
|
|
"steps": copy.deepcopy(self.current_steps_data) # Salva una copia profonda
|
|
}
|
|
|
|
try:
|
|
self.config_manager.update_sequence(self.original_sequence_name, updated_sequence_data)
|
|
self.result = updated_sequence_data
|
|
logger.info(f"EditSequenceDialog: Sequence '{self.original_sequence_name}' updated to '{new_seq_name}' in config.")
|
|
self.destroy()
|
|
except NameNotFoundError as e:
|
|
logger.error(f"EditSequenceDialog _on_save: Original sequence '{self.original_sequence_name}' no longer found: {e}", exc_info=True)
|
|
messagebox.showerror("Error Updating Sequence", str(e), parent=self)
|
|
except DuplicateNameError as e:
|
|
logger.warning(f"EditSequenceDialog _on_save: Failed to update due to duplicate name: {e}")
|
|
messagebox.showerror("Error Updating Sequence", str(e), parent=self)
|
|
self.name_entry.focus_set()
|
|
except ConfigError as e:
|
|
logger.error(f"EditSequenceDialog _on_save: Config error updating '{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"EditSequenceDialog _on_save: Unexpected error updating '{new_seq_name}': {e}", exc_info=True)
|
|
messagebox.showerror("Unexpected Error", f"An unexpected error occurred:\n{e}", parent=self) |