# LauncherTool/gui/main_window.py """ Defines the main window of the Application Launcher. """ import tkinter as tk from tkinter import ttk, messagebox, filedialog import logging # threading non è più strettamente necessario qui dato che ExecutionHandler gestisce l'async con 'after' # import threading # Importazioni relative al progetto from ..core.config_manager import ConfigManager from ..core.execution_handler import ExecutionHandler from ..core.exceptions import ( ConfigError, DuplicateNameError, NameNotFoundError, SequenceNotFoundError, ApplicationNotFoundError ) from .utils_gui import GuiUtils from .dialogs.application_dialogs import AddApplicationDialog, EditApplicationDialog from .dialogs.sequence_dialogs import AddSequenceDialog, EditSequenceDialog logger = logging.getLogger(__name__) class MainWindow(tk.Tk): """ Main application window for the Launcher Tool. Inherits from tk.Tk to be the root window. """ def __init__(self, config_file_path: str): """ Initializes the main application window. Args: config_file_path (str): Path to the configuration file. """ super().__init__() self.title("Application Launcher") logger.info("Initializing MainWindow.") self.config_manager = ConfigManager(config_file_path=config_file_path) try: self.config_manager.load_config() except ConfigError as e: logger.critical(f"Failed to load configuration: {e}", exc_info=True) messagebox.showerror( "Configuration Error", f"Could not load configuration file:\n{e}\n\n" "The application might not work correctly or will start with " "an empty default configuration." ) self.execution_handler = ExecutionHandler( config_manager=self.config_manager, output_callback=self._append_to_output, step_delay_callback=self.after ) self.protocol("WM_DELETE_WINDOW", self._on_closing) self._create_main_widgets() self._populate_initial_data() self.minsize(800, 600) GuiUtils.center_window(self) def _create_main_widgets(self): logger.debug("Creating main widgets.") self.notebook = ttk.Notebook(self) self.notebook.pack(expand=True, fill=tk.BOTH, padx=10, pady=10) self._create_applications_tab() self._create_sequences_tab() self._create_execution_tab() self.notebook.select(2) def _populate_initial_data(self): logger.debug("Populating initial data in GUI.") self.refresh_applications_tree() self.refresh_sequences_tree() self.refresh_execution_tab_combobox() # --- Application Tab Methods --- def _create_applications_tab(self): logger.debug("Creating Applications tab.") self.applications_tab = ttk.Frame(self.notebook) self.notebook.add(self.applications_tab, text='Applications') tree_frame = ttk.Frame(self.applications_tab) tree_frame.pack(expand=True, fill=tk.BOTH, padx=10, pady=(10,0)) app_scrollbar_y = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL) app_scrollbar_x = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL) self.applications_tree = ttk.Treeview( tree_frame, columns=('Name', 'Path'), show='headings', selectmode='browse', yscrollcommand=app_scrollbar_y.set, xscrollcommand=app_scrollbar_x.set ) app_scrollbar_y.config(command=self.applications_tree.yview) app_scrollbar_x.config(command=self.applications_tree.xview) self.applications_tree.heading('Name', text='Name') self.applications_tree.heading('Path', text='Path') self.applications_tree.column('Name', width=200, minwidth=150, anchor=tk.W) self.applications_tree.column('Path', width=500, minwidth=300, anchor=tk.W) app_scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y) app_scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X) self.applications_tree.pack(expand=True, fill=tk.BOTH, side=tk.LEFT) buttons_frame = ttk.Frame(self.applications_tab) buttons_frame.pack(fill=tk.X, padx=10, pady=5) self.add_app_button = ttk.Button( buttons_frame, text='Add Application', command=self._show_add_application_dialog ) self.add_app_button.pack(side=tk.LEFT, padx=5) self.edit_app_button = ttk.Button( buttons_frame, text='Edit Application', command=self._show_edit_application_dialog ) self.edit_app_button.pack(side=tk.LEFT, padx=5) self.delete_app_button = ttk.Button( buttons_frame, text='Delete Application', command=self._delete_selected_application ) self.delete_app_button.pack(side=tk.LEFT, padx=5) self.applications_tree.bind('<>', self._on_application_select) self._on_application_select() def _on_application_select(self, event=None): is_selection = bool(self.applications_tree.selection()) state = tk.NORMAL if is_selection else tk.DISABLED self.edit_app_button.config(state=state) self.delete_app_button.config(state=state) def refresh_applications_tree(self): logger.debug("Refreshing applications tree.") selected_id_tuple = self.applications_tree.selection() selected_id = selected_id_tuple[0] if selected_id_tuple else None for item in self.applications_tree.get_children(): self.applications_tree.delete(item) try: for app_data in self.config_manager.get_applications(): # Use app_data['name'] as iid for reliable selection restoration self.applications_tree.insert('', tk.END, values=(app_data['name'], app_data['path']), iid=app_data['name']) except ConfigError as e: logger.error(f"ConfigError while getting applications: {e}", exc_info=True) messagebox.showerror("Error", f"Could not load applications: {e}") except Exception as e: logger.error(f"Error refreshing applications tree: {e}", exc_info=True) messagebox.showerror("Error", f"An unexpected error occurred while refreshing applications: {e}") if selected_id and self.applications_tree.exists(selected_id): self.applications_tree.selection_set(selected_id) self._on_application_select() def _show_add_application_dialog(self): logger.info("Showing Add Application dialog.") dialog = AddApplicationDialog(self, self.config_manager) if dialog.result: self.refresh_applications_tree() self.refresh_sequences_tree() # An app name might be used in sequences def _show_edit_application_dialog(self): selected_item_ids = self.applications_tree.selection() if not selected_item_ids: messagebox.showinfo("Edit Application", "Please select an application to edit.", parent=self) return # Assuming iid is the application name application_name = selected_item_ids[0] logger.info(f"Showing Edit Application dialog for: {application_name}") dialog = EditApplicationDialog(self, self.config_manager, application_name) if dialog.result: self.refresh_applications_tree() self.refresh_sequences_tree() def _delete_selected_application(self): selected_item_ids = self.applications_tree.selection() if not selected_item_ids: messagebox.showinfo("Delete Application", "Please select an application to delete.", parent=self) return application_name = selected_item_ids[0] if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete application '{application_name}'?\n" "It will also be removed from all sequence steps.", parent=self): logger.info(f"Attempting to delete application: {application_name}") try: self.config_manager.delete_application(application_name) messagebox.showinfo("Success", f"Application '{application_name}' deleted.", parent=self) self.refresh_applications_tree() self.refresh_sequences_tree() except NameNotFoundError: logger.warning(f"Delete failed: Application '{application_name}' not found.") messagebox.showerror("Error", f"Application '{application_name}' not found.", parent=self) except ConfigError as e: logger.error(f"ConfigError deleting application '{application_name}': {e}", exc_info=True) messagebox.showerror("Error", f"Failed to delete application '{application_name}':\n{e}", parent=self) # --- Sequences Tab Methods --- def _create_sequences_tab(self): logger.debug("Creating Sequences tab.") self.sequences_tab = ttk.Frame(self.notebook) self.notebook.add(self.sequences_tab, text='Sequences') tree_frame = ttk.Frame(self.sequences_tab) tree_frame.pack(expand=True, fill=tk.BOTH, padx=10, pady=(10,0)) seq_scrollbar_y = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL) self.sequences_tree = ttk.Treeview( tree_frame, columns=('Name', 'Steps'), show='headings', selectmode='browse', yscrollcommand=seq_scrollbar_y.set ) seq_scrollbar_y.config(command=self.sequences_tree.yview) self.sequences_tree.heading('Name', text='Sequence Name') self.sequences_tree.heading('Steps', text='Number of Steps') self.sequences_tree.column('Name', width=300, minwidth=200, anchor=tk.W) self.sequences_tree.column('Steps', width=100, minwidth=80, anchor=tk.CENTER) seq_scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y) self.sequences_tree.pack(expand=True, fill=tk.BOTH, side=tk.LEFT) buttons_frame = ttk.Frame(self.sequences_tab) buttons_frame.pack(fill=tk.X, padx=10, pady=5) self.add_seq_button = ttk.Button( buttons_frame, text='Add Sequence', command=self._show_add_sequence_dialog ) self.add_seq_button.pack(side=tk.LEFT, padx=5) self.edit_seq_button = ttk.Button( buttons_frame, text='Edit Sequence', command=self._show_edit_sequence_dialog ) self.edit_seq_button.pack(side=tk.LEFT, padx=5) self.delete_seq_button = ttk.Button( buttons_frame, text='Delete Sequence', command=self._delete_selected_sequence ) self.delete_seq_button.pack(side=tk.LEFT, padx=5) self.sequences_tree.bind('<>', self._on_sequence_select) self._on_sequence_select() def _on_sequence_select(self, event=None): is_selection = bool(self.sequences_tree.selection()) state = tk.NORMAL if is_selection else tk.DISABLED self.edit_seq_button.config(state=state) self.delete_seq_button.config(state=state) def refresh_sequences_tree(self): logger.debug("Refreshing sequences tree.") selected_id_tuple = self.sequences_tree.selection() selected_id = selected_id_tuple[0] if selected_id_tuple else None for item in self.sequences_tree.get_children(): self.sequences_tree.delete(item) try: for seq_data in self.config_manager.get_sequences(): num_steps = len(seq_data.get("steps", [])) # Use seq_data['name'] as iid self.sequences_tree.insert('', tk.END, values=(seq_data['name'], num_steps), iid=seq_data['name']) except ConfigError as e: logger.error(f"ConfigError while getting sequences: {e}", exc_info=True) messagebox.showerror("Error", f"Could not load sequences: {e}", parent=self) except Exception as e: logger.error(f"Error refreshing sequences tree: {e}", exc_info=True) messagebox.showerror("Error", f"An unexpected error occurred while refreshing sequences: {e}", parent=self) if selected_id and self.sequences_tree.exists(selected_id): self.sequences_tree.selection_set(selected_id) self._on_sequence_select() def _show_add_sequence_dialog(self): logger.info("Showing Add Sequence dialog.") dialog = AddSequenceDialog(self, self.config_manager) if dialog.result: self.refresh_sequences_tree() self.refresh_execution_tab_combobox() def _show_edit_sequence_dialog(self): selected_item_ids = self.sequences_tree.selection() if not selected_item_ids: messagebox.showinfo("Edit Sequence", "Please select a sequence to edit.", parent=self) return sequence_name = selected_item_ids[0] # Assuming iid is sequence name logger.info(f"Showing Edit Sequence dialog for: {sequence_name}") dialog = EditSequenceDialog(self, self.config_manager, sequence_name) if dialog.result: self.refresh_sequences_tree() self.refresh_execution_tab_combobox() def _delete_selected_sequence(self): selected_item_ids = self.sequences_tree.selection() if not selected_item_ids: messagebox.showinfo("Delete Sequence", "Please select a sequence to delete.", parent=self) return sequence_name = selected_item_ids[0] # Assuming iid is sequence name if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete sequence '{sequence_name}'?", parent=self): logger.info(f"Attempting to delete sequence: {sequence_name}") try: self.config_manager.delete_sequence(sequence_name) messagebox.showinfo("Success", f"Sequence '{sequence_name}' deleted.", parent=self) self.refresh_sequences_tree() self.refresh_execution_tab_combobox() except NameNotFoundError: logger.warning(f"Delete failed: Sequence '{sequence_name}' not found.") messagebox.showerror("Error", f"Sequence '{sequence_name}' not found.", parent=self) except ConfigError as e: logger.error(f"ConfigError deleting sequence '{sequence_name}': {e}", exc_info=True) messagebox.showerror("Error", f"Failed to delete sequence '{sequence_name}':\n{e}", parent=self) # --- Execution Tab Methods --- def _create_execution_tab(self): logger.debug("Creating Execution tab.") self.execution_tab = ttk.Frame(self.notebook) self.notebook.add(self.execution_tab, text='Execution') top_frame = ttk.Frame(self.execution_tab) top_frame.pack(fill=tk.X, padx=10, pady=10) sequence_label = ttk.Label(top_frame, text="Select Sequence:") sequence_label.pack(side=tk.LEFT, padx=(0, 5)) self.sequence_combo_var = tk.StringVar() self.sequence_combo = ttk.Combobox( top_frame, textvariable=self.sequence_combo_var, state="readonly", width=40 ) self.sequence_combo.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=5) self.sequence_combo.bind("<>", self._on_execution_sequence_select) self.run_button = ttk.Button( top_frame, text="Run Sequence", command=self._run_selected_sequence ) self.run_button.pack(side=tk.LEFT, padx=5) output_frame = ttk.LabelFrame(self.execution_tab, text="Output Log") output_frame.pack(expand=True, fill=tk.BOTH, padx=10, pady=(0, 10)) output_scrollbar_y = ttk.Scrollbar(output_frame, orient=tk.VERTICAL) self.output_text = tk.Text( output_frame, height=15, wrap=tk.WORD, yscrollcommand=output_scrollbar_y.set, state=tk.DISABLED ) output_scrollbar_y.config(command=self.output_text.yview) output_scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y) self.output_text.pack(expand=True, fill=tk.BOTH, side=tk.LEFT, padx=5, pady=5) self._on_execution_sequence_select() # Initialize button state def _on_execution_sequence_select(self, event=None): if self.sequence_combo_var.get(): self.run_button.config(state=tk.NORMAL) else: self.run_button.config(state=tk.DISABLED) def refresh_execution_tab_combobox(self): logger.debug("Refreshing execution tab combobox.") current_selection = self.sequence_combo_var.get() try: sequence_names = [seq["name"] for seq in self.config_manager.get_sequences()] self.sequence_combo['values'] = sorted(sequence_names) if current_selection and current_selection in sequence_names: self.sequence_combo_var.set(current_selection) elif sequence_names: self.sequence_combo_var.set(sequence_names[0]) else: self.sequence_combo_var.set("") except ConfigError as e: logger.error(f"ConfigError while getting sequences for combobox: {e}", exc_info=True) messagebox.showerror("Error", f"Could not load sequences for execution: {e}", parent=self) except Exception as e: logger.error(f"Error refreshing execution combobox: {e}", exc_info=True) self._on_execution_sequence_select() def _append_to_output(self, message: str): if not hasattr(self, 'output_text') or not self.output_text.winfo_exists(): logger.warning("Output text widget no longer exists, cannot append message.") return try: self.output_text.config(state=tk.NORMAL) self.output_text.insert(tk.END, message) self.output_text.see(tk.END) self.output_text.config(state=tk.DISABLED) except tk.TclError as e: # Can happen if widget is destroyed during async operation logger.warning(f"TclError appending to output (widget might be destroyed): {e}") def _run_selected_sequence(self): selected_sequence_name = self.sequence_combo_var.get() if not selected_sequence_name: messagebox.showinfo("Run Sequence", "Please select a sequence to run.", parent=self) return self.output_text.config(state=tk.NORMAL) self.output_text.delete("1.0", tk.END) self.output_text.config(state=tk.DISABLED) self.run_button.config(state=tk.DISABLED) try: logger.info(f"Attempting to run sequence: {selected_sequence_name}") self.execution_handler.run_sequence_async(selected_sequence_name) except SequenceNotFoundError: msg = f"Sequence '{selected_sequence_name}' not found. It might have been deleted." logger.error(msg) messagebox.showerror("Error Running Sequence", msg, parent=self) self._append_to_output(f"ERROR: {msg}\n") self.run_button.config(state=tk.NORMAL if self.sequence_combo_var.get() else tk.DISABLED) except ApplicationNotFoundError as e: msg = f"Application '{e.name}' required by sequence '{selected_sequence_name}' not found." logger.error(msg) messagebox.showerror("Error Running Sequence", msg, parent=self) self._append_to_output(f"ERROR: {msg}\n") self.run_button.config(state=tk.NORMAL if self.sequence_combo_var.get() else tk.DISABLED) except Exception as e: logger.error(f"Unexpected error starting sequence '{selected_sequence_name}': {e}", exc_info=True) messagebox.showerror("Error", f"An unexpected error occurred: {e}", parent=self) self._append_to_output(f"UNEXPECTED ERROR: {e}\n") # Non reimpostare il pulsante run qui, ExecutionHandler lo farà o emetterà un "completato" # Il pulsante run verrà riabilitato quando la sequenza termina (tramite messaggio "Sequence completed.") # o se fallisce prima di iniziare. Se la sequenza inizia, il pulsante rimane disabilitato. # Per ora, lo riabilitiamo se la sequenza fallisce subito. # TODO: Una gestione più fine dello stato del run_button potrebbe essere necessaria # in base allo stato effettivo di ExecutionHandler. # Se la sequenza è effettivamente iniziata, il pulsante dovrebbe rimanere disabilitato # fino a "Sequence completed" o un errore durante l'esecuzione. # Il _log_and_output in ExecutionHandler con "Sequence completed." o errori # potrebbe essere usato per triggerare la riabilitazione del pulsante. # Questo richiederebbe un altro callback da ExecutionHandler a MainWindow. # --- General Methods --- def _on_closing(self): logger.info("Close button clicked. Application shutting down.") if hasattr(self, 'execution_handler') and self.execution_handler: # Qui potresti voler interrompere una sequenza in esecuzione se necessario, # ma Popen con DETACHED_PROCESS lancia processi indipendenti. # La logica di `after` verrà interrotta con `destroy`. pass self.destroy() def run(self): self.mainloop()