SXXXXXXX_LauncherTool/launchertool/gui/main_window.py

505 lines
22 KiB
Python

# 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__)
# --- Import Version Info FOR THE WRAPPER ITSELF ---
try:
# Use absolute import based on package name
from launchertool import _version as wrapper_version
WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})"
WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}"
except ImportError:
# This might happen if you run the wrapper directly from source
# without generating its _version.py first (if you use that approach for the wrapper itself)
WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)"
WRAPPER_BUILD_INFO = "Wrapper build time unknown"
# --- End Import Version Info ---
# --- Constants for Version Generation ---
DEFAULT_VERSION = "0.0.0+unknown"
DEFAULT_COMMIT = "Unknown"
DEFAULT_BRANCH = "Unknown"
# --- End Constants ---
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(f"Application Launcher - {WRAPPER_APP_VERSION_STRING}")
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('<<TreeviewSelect>>', 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
application_name = selected_item_ids[0] # Questo è l'iid
logger.info(f"Attempting to show Edit Application dialog for: '{application_name}'") # LOG
if not application_name: # Aggiungi un controllo esplicito
messagebox.showerror("Error", "Selected application has an invalid name.", parent=self)
logger.error("Selected application for edit has no name (iid was empty).")
return
dialog = EditApplicationDialog(self, self.config_manager, application_name)
if dialog.result: # dialog.result è impostato solo se il dialogo salva con successo
self.refresh_applications_tree()
self.refresh_sequences_tree() # App name change impacts sequences
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('<<TreeviewSelect>>', 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("<<ComboboxSelected>>", 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()