SXXXXXXX_LauncherTool/launchertool/gui/dialogs/application_dialogs.py

460 lines
24 KiB
Python

# LauncherTool/gui/dialogs/application_dialogs.py
"""
Dialogs for adding and editing applications.
"""
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import logging
from typing import Optional, List, Dict, Any, Callable # Aggiunto Callable
from ...core.config_manager import ConfigManager
from ...core.exceptions import DuplicateNameError, NameNotFoundError, ConfigError, ApplicationNotFoundError
from ..utils_gui import GuiUtils
from ...gui.dialogs.parameter_dialog import AddParameterDialog, EditParameterDialog
logger = logging.getLogger(__name__)
class BaseApplicationDialog(tk.Toplevel):
"""
Base class for Add and Edit Application dialogs.
Provides common widgets and functionality.
"""
def __init__(self, parent: tk.Widget, config_manager: ConfigManager, title: str, load_data_method: Optional[Callable[[], None]] = None):
logger.debug(f"BaseApplicationDialog __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_app_name è definito e gestito dalle sottoclassi prima di chiamare questo costruttore base,
# se necessario per load_data_method.
logger.debug("BaseApplicationDialog: Calling _setup_widgets()")
self._setup_widgets() # Crea tutti i widget
logger.debug("BaseApplicationDialog: _setup_widgets() completed.")
# Carica i dati specifici della sottoclasse (es. per EditDialog) PRIMA di rendere modale e attendere
if load_data_method:
logger.debug("BaseApplicationDialog: Calling provided load_data_method.")
load_data_method()
logger.debug("BaseApplicationDialog: load_data_method completed.")
else:
logger.debug("BaseApplicationDialog: No load_data_method provided or needed.")
GuiUtils.center_window(self, parent)
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
self.grab_set() # Rendi modale
self.focus_set() # Dai focus
logger.debug(f"BaseApplicationDialog __init__ for '{title}': Now calling wait_window().")
self.wait_window(self) # Blocca qui finché il dialogo non viene distrutto
logger.debug(f"BaseApplicationDialog __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)
# --- Application Name ---
name_frame = ttk.Frame(main_frame)
name_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(name_frame, text="Application 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)
# --- Application Path ---
path_frame = ttk.Frame(main_frame)
path_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(path_frame, text="Application Path:").pack(side=tk.LEFT, padx=(0, 5))
self.path_entry_var = tk.StringVar()
self.path_entry = ttk.Entry(path_frame, textvariable=self.path_entry_var, width=60)
self.path_entry.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0,5))
self.browse_button = ttk.Button(path_frame, text="Browse...", command=self._browse_file)
self.browse_button.pack(side=tk.LEFT)
# --- Parameters ---
params_labelframe = ttk.LabelFrame(main_frame, text="Parameters")
params_labelframe.pack(expand=True, fill=tk.BOTH, pady=(0, 10))
params_tree_frame = ttk.Frame(params_labelframe) # Frame to hold tree and scrollbar
params_tree_frame.pack(expand=True, fill=tk.BOTH, padx=5, pady=5)
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', 'Description', 'Default Value', 'Type'),
show='headings',
selectmode='browse',
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='Name')
self.params_tree.heading('Description', text='Description')
self.params_tree.heading('Default Value', text='Default Value')
self.params_tree.heading('Type', text='Type')
self.params_tree.column('Name', width=150, minwidth=100, anchor=tk.W)
self.params_tree.column('Description', width=250, minwidth=150, anchor=tk.W)
self.params_tree.column('Default Value', width=150, minwidth=100, anchor=tk.W)
self.params_tree.column('Type', width=80, minwidth=60, anchor=tk.CENTER)
param_scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
param_scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)
self.params_tree.pack(expand=True, fill=tk.BOTH, side=tk.LEFT)
param_buttons_frame = ttk.Frame(params_labelframe)
param_buttons_frame.pack(fill=tk.X, padx=5, pady=(0,5))
self.add_param_button = ttk.Button(param_buttons_frame, text="Add Param", command=self._add_parameter)
self.add_param_button.pack(side=tk.LEFT, padx=(0,5))
self.edit_param_button = ttk.Button(param_buttons_frame, text="Edit Param", command=self._edit_parameter)
self.edit_param_button.pack(side=tk.LEFT, padx=(0,5))
self.delete_param_button = ttk.Button(param_buttons_frame, text="Delete Param", command=self._delete_parameter)
self.delete_param_button.pack(side=tk.LEFT)
self.params_tree.bind("<<TreeviewSelect>>", self._on_parameter_select)
self._on_parameter_select() # Init button states
# --- Dialog Buttons (Save, Cancel) ---
dialog_buttons_frame = ttk.Frame(main_frame)
dialog_buttons_frame.pack(fill=tk.X, 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 _browse_file(self):
filepath = filedialog.askopenfilename(
title="Select Application Executable",
filetypes=(("Executable files", "*.exe"), ("All files", "*.*")),
parent=self
)
if filepath:
self.path_entry_var.set(filepath)
if not self.name_entry_var.get():
try:
filename = filepath.split('/')[-1].split('\\')[-1]
app_name_suggestion = filename.rsplit('.', 1)[0]
if app_name_suggestion:
self.name_entry_var.set(app_name_suggestion)
except Exception:
pass # Ignore errors in suggestion
def _get_parameters_from_tree(self) -> List[Dict[str, str]]:
parameters = []
for item_id in self.params_tree.get_children():
values = self.params_tree.item(item_id, 'values')
parameters.append({
"name": values[0],
"description": values[1],
"default_value": values[2],
"type": values[3]
})
return parameters
def _populate_parameters_tree(self, parameters: List[Dict[str, str]]):
for item in self.params_tree.get_children():
self.params_tree.delete(item)
logger.debug(f"BaseApplicationDialog: Populating parameters tree with {len(parameters)} parameters.")
for i, param in enumerate(parameters):
param_name = param.get('name','')
description = param.get('description','')
default_value = param.get('default_value','')
param_type = param.get('type','string')
iid_base = param_name if param_name and param_name.strip() else f"param_gen_{i}" # Ensure non-empty iid_base
final_iid = iid_base
counter = 0
# Ensure iid is unique in the tree
while self.params_tree.exists(final_iid):
counter += 1
final_iid = f"{iid_base}_{counter}"
logger.debug(f"BaseApplicationDialog: Inserting parameter: iid='{final_iid}', values=('{param_name}', '{description}', '{default_value}', '{param_type}')")
self.params_tree.insert(
'', tk.END,
iid=final_iid,
values=(param_name, description, default_value, param_type)
)
self._on_parameter_select()
def _on_parameter_select(self, event=None):
is_selection = bool(self.params_tree.selection())
state = tk.NORMAL if is_selection else tk.DISABLED
self.edit_param_button.config(state=state)
self.delete_param_button.config(state=state)
def _add_parameter(self):
logger.debug("BaseApplicationDialog: Add Parameter button clicked.")
dialog = AddParameterDialog(self) # 'self' (BaseApplicationDialog) is the parent
if dialog.result:
new_param_name = dialog.result['name']
# Check for duplicate parameter name before adding to tree
for item_id in self.params_tree.get_children():
if self.params_tree.item(item_id, 'values')[0] == new_param_name:
messagebox.showerror("Duplicate Parameter",
f"A parameter with the name '{new_param_name}' already exists for this application.",
parent=self)
return
# Use the (now validated unique) new_param_name as iid
self.params_tree.insert('', tk.END, iid=new_param_name, values=(
new_param_name,
dialog.result['description'],
dialog.result['default_value'],
dialog.result['type']
))
logger.info(f"BaseApplicationDialog: Parameter '{new_param_name}' added to this dialog's parameter tree.")
self._on_parameter_select()
def _edit_parameter(self):
selected_item_ids = self.params_tree.selection()
if not selected_item_ids:
messagebox.showwarning("Edit Parameter", "Please select a parameter to edit.", parent=self)
return
selected_iid = selected_item_ids[0] # This is the iid of the selected item
item_values = self.params_tree.item(selected_iid, 'values')
param_data_to_edit = {
"name": item_values[0],
"description": item_values[1],
"default_value": item_values[2],
"type": item_values[3]
}
original_param_name = param_data_to_edit["name"] # This is the name before editing
logger.debug(f"BaseApplicationDialog: Edit Parameter button clicked for: {original_param_name} (iid: {selected_iid})")
dialog = EditParameterDialog(self, param_data_to_edit) # 'self' is the parent
if dialog.result:
new_param_name = dialog.result['name']
# Check for duplicate name if the name was changed
if new_param_name != original_param_name:
for item_id in self.params_tree.get_children():
# Don't compare the item with itself (if its iid hasn't changed yet)
# Or, more robustly, check all OTHER items
if item_id != selected_iid and self.params_tree.item(item_id, 'values')[0] == new_param_name:
messagebox.showerror("Duplicate Parameter",
f"Another parameter with the name '{new_param_name}' already exists.",
parent=self)
return
new_values = (
new_param_name,
dialog.result['description'],
dialog.result['default_value'],
dialog.result['type']
)
# If name changed AND iid was the original name, we need to re-insert with new iid
if new_param_name != original_param_name and selected_iid == original_param_name:
logger.debug(f"Parameter name changed from '{original_param_name}' to '{new_param_name}'. Re-inserting in tree.")
self.params_tree.delete(selected_iid)
self.params_tree.insert('', tk.END, iid=new_param_name, values=new_values)
self.params_tree.selection_set(new_param_name) # Reselect the (new) item
else:
# Name didn't change, or iid was not the name, so just update values for the current iid
self.params_tree.item(selected_iid, values=new_values)
logger.debug(f"Parameter values updated for iid '{selected_iid}'. New name: '{new_param_name}'.")
logger.info(f"BaseApplicationDialog: Parameter '{new_param_name}' (originally '{original_param_name}') updated.")
self._on_parameter_select() # Refresh button states
def _delete_parameter(self):
selected_item_ids = self.params_tree.selection()
if not selected_item_ids:
messagebox.showwarning("Delete Parameter", "Please select a parameter to delete.", parent=self)
return
param_name = self.params_tree.item(selected_item_ids[0], 'values')[0]
if messagebox.askyesno("Confirm Delete",
f"Are you sure you want to delete parameter '{param_name}'?",
parent=self):
self.params_tree.delete(selected_item_ids[0])
logger.debug(f"BaseApplicationDialog: Parameter '{param_name}' deleted from this dialog's tree.")
self._on_parameter_select()
def _validate_inputs(self) -> bool:
name = self.name_entry_var.get().strip()
path = self.path_entry_var.get().strip()
if not name:
messagebox.showerror("Validation Error", "Application Name cannot be empty.", parent=self)
self.name_entry.focus_set()
return False
if not path:
messagebox.showerror("Validation Error", "Application Path cannot be empty.", parent=self)
self.path_entry.focus_set()
return False
return True
def _on_save(self):
# This method should be implemented by subclasses (AddApplicationDialog, EditApplicationDialog)
raise NotImplementedError("Subclasses must implement _on_save")
def _on_cancel(self):
logger.debug(f"BaseApplicationDialog: '{self.title()}' cancelled or closed by user.")
self.result = None # Explicitly set result to None for cancellation
self.destroy()
class AddApplicationDialog(BaseApplicationDialog):
"""Dialog for adding a new application."""
def __init__(self, parent: tk.Widget, config_manager: ConfigManager):
# AddApplicationDialog does not need to load initial data from an existing app,
# so load_data_method is None.
super().__init__(parent, config_manager, title="Add New Application", load_data_method=None)
logger.debug("AddApplicationDialog __init__ completed.")
def _on_save(self):
if not self._validate_inputs():
return
app_name = self.name_entry_var.get().strip()
app_path = self.path_entry_var.get().strip()
parameters = self._get_parameters_from_tree() # Gets params currently in this dialog's tree
application_data = {
"name": app_name,
"path": app_path,
"parameters": parameters
}
try:
self.config_manager.add_application(application_data)
self.result = application_data # Set result upon successful save to ConfigManager
logger.info(f"AddApplicationDialog: Application '{app_name}' added successfully to config.")
self.destroy() # Close dialog
except DuplicateNameError as e:
logger.warning(f"AddApplicationDialog: Failed to add application due to duplicate name: {e}")
messagebox.showerror("Error Adding Application", str(e), parent=self)
self.name_entry.focus_set()
except ConfigError as e: # Catch other config related errors (e.g., save failed)
logger.error(f"AddApplicationDialog: Configuration error adding application '{app_name}': {e}", exc_info=True)
messagebox.showerror("Configuration Error", f"Could not save application:\n{e}", parent=self)
except Exception as e: # Catch any other unexpected errors
logger.error(f"AddApplicationDialog: Unexpected error adding application '{app_name}': {e}", exc_info=True)
messagebox.showerror("Unexpected Error", f"An unexpected error occurred:\n{e}", parent=self)
class EditApplicationDialog(BaseApplicationDialog):
"""Dialog for editing an existing application."""
def __init__(self, parent: tk.Widget, config_manager: ConfigManager, application_name_to_edit: str):
logger.debug(f"EditApplicationDialog __init__ - START - App to edit: '{application_name_to_edit}'")
self.original_app_name = application_name_to_edit # Must be set BEFORE super().__init__ if load_data_method uses it
# Pass its own _load_initial_data method to the base class constructor
super().__init__(parent, config_manager, title=f"Edit Application: {self.original_app_name}", load_data_method=self._load_initial_data)
# Code here (after super call) executes only after the dialog (and wait_window in base) has finished.
# Usually, nothing more is needed here as 'result' is checked by the caller (MainWindow).
logger.debug(f"EditApplicationDialog __init__ - END - for '{self.original_app_name}' (after super call has completed)")
def _load_initial_data(self):
# This method is now called by BaseApplicationDialog's __init__ via load_data_method
if not self.original_app_name:
# This case should ideally be caught before even calling super() in __init__
# or by BaseApplicationDialog if load_data_method is None when it shouldn't be.
logger.error("EditApplicationDialog _load_initial_data: original_app_name is None or empty. Dialog should be closing.")
# If parent_widget is None (though it shouldn't be), messagebox might fail.
if self.parent_widget:
messagebox.showerror("Initialization Error",
"Cannot edit: No application name specified for editing.",
parent=self.parent_widget)
self.after_idle(self.destroy) # Ensure dialog closes
return
logger.debug(f"EditApplicationDialog _load_initial_data: Attempting to load for: '{self.original_app_name}'")
try:
app_data = self.config_manager.get_application_by_name(self.original_app_name)
logger.debug(f"EditApplicationDialog _load_initial_data: Data from ConfigManager for '{self.original_app_name}': {app_data}")
if not app_data: # Should be caught by NameNotFoundError, but as a safeguard
logger.error(f"EditApplicationDialog _load_initial_data: ConfigManager returned None (or empty) for '{self.original_app_name}'.")
messagebox.showerror("Load Error",
f"Could not retrieve valid data for application '{self.original_app_name}'.",
parent=self.parent_widget)
self.after_idle(self.destroy)
return
# Populate the dialog's widgets with the loaded data
self.name_entry_var.set(app_data.get("name", ""))
self.path_entry_var.set(app_data.get("path", ""))
logger.debug(f"EditApplicationDialog _load_initial_data: Name set to '{self.name_entry_var.get()}', Path to '{self.path_entry_var.get()}'")
parameters_data = app_data.get("parameters", [])
logger.debug(f"EditApplicationDialog _load_initial_data: Parameters to populate: {parameters_data}")
self._populate_parameters_tree(parameters_data) # Call base method to fill param tree
logger.info(f"EditApplicationDialog _load_initial_data: Successfully loaded data for '{self.original_app_name}'.")
except NameNotFoundError:
logger.error(f"EditApplicationDialog _load_initial_data: App '{self.original_app_name}' not found (NameNotFoundError).")
messagebox.showerror("Load Error",
f"Application '{self.original_app_name}' not found. Cannot edit.",
parent=self.parent_widget) # parent_widget is MainWindow
self.after_idle(self.destroy)
except Exception as e:
logger.error(f"EditApplicationDialog _load_initial_data: Unexpected error for '{self.original_app_name}': {e}", exc_info=True)
messagebox.showerror("Load Error",
f"An unexpected error occurred while loading data for '{self.original_app_name}':\n{e}",
parent=self.parent_widget)
self.after_idle(self.destroy)
def _on_save(self):
# Ensure original_app_name is valid; if _load_initial_data failed, it might not be safe to save.
if not self.original_app_name:
messagebox.showerror("Save Error", "Cannot save: Critical information about the original application is missing.", parent=self)
logger.error("EditApplicationDialog _on_save: Attempted to save without a valid original_app_name.")
return
if not self._validate_inputs(): # Validates current entries in the dialog
return
new_app_name = self.name_entry_var.get().strip()
new_app_path = self.path_entry_var.get().strip()
new_parameters = self._get_parameters_from_tree() # Gets params currently in this dialog's tree
updated_application_data = {
"name": new_app_name,
"path": new_app_path,
"parameters": new_parameters
}
try:
# Use self.original_app_name to identify which app to update in ConfigManager
self.config_manager.update_application(self.original_app_name, updated_application_data)
self.result = updated_application_data # Set result to the successfully updated data
logger.info(f"EditApplicationDialog: Application '{self.original_app_name}' updated to '{new_app_name}' in config.")
self.destroy() # Close dialog
except NameNotFoundError as e: # If original_app_name is somehow no longer in config
logger.error(f"EditApplicationDialog _on_save: Original app '{self.original_app_name}' no longer found: {e}", exc_info=True)
messagebox.showerror("Error Updating Application", str(e), parent=self)
except DuplicateNameError as e: # If new_app_name conflicts with another existing app
logger.warning(f"EditApplicationDialog _on_save: Failed to update due to duplicate name: {e}")
messagebox.showerror("Error Updating Application", str(e), parent=self)
self.name_entry.focus_set() # Keep dialog open and focus on name field
except ConfigError as e: # Other config errors (e.g., save failed)
logger.error(f"EditApplicationDialog _on_save: Config error updating '{new_app_name}': {e}", exc_info=True)
messagebox.showerror("Configuration Error", f"Could not save application updates:\n{e}", parent=self)
except Exception as e: # Catch-all for other unexpected errors
logger.error(f"EditApplicationDialog _on_save: Unexpected error updating '{new_app_name}': {e}", exc_info=True)
messagebox.showerror("Unexpected Error", f"An unexpected error occurred:\n{e}", parent=self)