505 lines
22 KiB
Python
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() |