377 lines
17 KiB
Python
377 lines
17 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
|
|
|
|
from launchertool.core.config_manager import ConfigManager
|
|
from launchertool.core.exceptions import DuplicateNameError, NameNotFoundError, ConfigError
|
|
from launchertool.gui.utils_gui import GuiUtils
|
|
from launchertool.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):
|
|
super().__init__(parent)
|
|
self.transient(parent)
|
|
self.title(title)
|
|
self.parent_widget = parent # Explicitly store parent for messagebox context
|
|
self.config_manager = config_manager
|
|
self.result: Optional[Dict[str, Any]] = None
|
|
self.original_app_name: Optional[str] = None
|
|
|
|
self._setup_widgets()
|
|
|
|
GuiUtils.center_window(self, parent)
|
|
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
|
self.grab_set()
|
|
self.focus_set()
|
|
self.wait_window(self)
|
|
|
|
|
|
def _setup_widgets(self):
|
|
main_frame = ttk.Frame(self, padding="10")
|
|
main_frame.pack(expand=True, fill=tk.BOTH)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
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()
|
|
|
|
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)
|
|
|
|
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 # Ensure dialog is modal to this Toplevel
|
|
)
|
|
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
|
|
|
|
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)
|
|
for i, param in enumerate(parameters):
|
|
# Use param name for iid if unique, otherwise generate one
|
|
param_name = param.get('name','')
|
|
iid = param_name if param_name else f"param_gen_{i}"
|
|
# Ensure iid is unique if names can be non-unique (though they shouldn't)
|
|
counter = 0
|
|
final_iid = iid
|
|
while self.params_tree.exists(final_iid):
|
|
counter += 1
|
|
final_iid = f"{iid}_{counter}"
|
|
|
|
self.params_tree.insert(
|
|
'', tk.END,
|
|
iid=final_iid,
|
|
values=(param_name,
|
|
param.get('description',''),
|
|
param.get('default_value',''),
|
|
param.get('type','string'))
|
|
)
|
|
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("Add Parameter button clicked.")
|
|
dialog = AddParameterDialog(self)
|
|
if dialog.result:
|
|
# Check for duplicate parameter name before adding to tree
|
|
new_param_name = dialog.result['name']
|
|
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
|
|
|
|
param_iid = new_param_name # Use name as iid, assuming unique
|
|
self.params_tree.insert('', tk.END, iid=param_iid, values=(
|
|
new_param_name,
|
|
dialog.result['description'],
|
|
dialog.result['default_value'],
|
|
dialog.result['type']
|
|
))
|
|
logger.info(f"Parameter '{new_param_name}' added to application dialog 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]
|
|
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"]
|
|
logger.debug(f"Edit Parameter button clicked for: {original_param_name}")
|
|
|
|
dialog = EditParameterDialog(self, param_data_to_edit)
|
|
if dialog.result:
|
|
new_param_name = dialog.result['name']
|
|
# Check for duplicate name if name changed
|
|
if new_param_name != original_param_name:
|
|
for item_id in self.params_tree.get_children():
|
|
if item_id != selected_iid and 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.",
|
|
parent=self)
|
|
return
|
|
|
|
# If name changed, we need to delete old and insert new, or re-tag. Simpler to update values & iid.
|
|
self.params_tree.item(selected_iid, values=(
|
|
new_param_name,
|
|
dialog.result['description'],
|
|
dialog.result['default_value'],
|
|
dialog.result['type']
|
|
))
|
|
# If iid was based on name, and name changes, treeview might get confused.
|
|
# It's safer if iid is independent or handled carefully on name change.
|
|
# For now, assume iid can be the new name IF it's unique.
|
|
if new_param_name != original_param_name:
|
|
# Detach and re-insert with new iid if name (used as iid) changed
|
|
values = self.params_tree.item(selected_iid, 'values')
|
|
self.params_tree.delete(selected_iid)
|
|
self.params_tree.insert('', tk.END, iid=new_param_name, values=values)
|
|
self.params_tree.selection_set(new_param_name) # Reselect
|
|
|
|
logger.info(f"Parameter '{new_param_name}' updated in application dialog tree.")
|
|
|
|
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"Parameter '{param_name}' deleted from 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):
|
|
raise NotImplementedError("Subclasses must implement _on_save")
|
|
|
|
def _on_cancel(self):
|
|
logger.debug(f"{self.title()} cancelled or closed.")
|
|
self.result = None
|
|
self.destroy()
|
|
|
|
|
|
class AddApplicationDialog(BaseApplicationDialog):
|
|
"""Dialog for adding a new application."""
|
|
def __init__(self, parent: tk.Widget, config_manager: ConfigManager):
|
|
super().__init__(parent, config_manager, title="Add New Application")
|
|
|
|
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()
|
|
|
|
application_data = {
|
|
"name": app_name,
|
|
"path": app_path,
|
|
"parameters": parameters
|
|
}
|
|
|
|
try:
|
|
self.config_manager.add_application(application_data)
|
|
self.result = application_data
|
|
logger.info(f"Application '{app_name}' added successfully through dialog.")
|
|
# Message shown by MainWindow after successful dialog.result
|
|
# messagebox.showinfo("Success", f"Application '{app_name}' added successfully.", parent=self.parent_widget)
|
|
self.destroy()
|
|
except DuplicateNameError as e:
|
|
logger.warning(f"Failed to add application: {e}")
|
|
messagebox.showerror("Error Adding Application", str(e), parent=self)
|
|
self.name_entry.focus_set()
|
|
except ConfigError as e:
|
|
logger.error(f"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:
|
|
logger.error(f"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):
|
|
self.original_app_name = application_name_to_edit
|
|
super().__init__(parent, config_manager, title=f"Edit Application: {application_name_to_edit}")
|
|
self._load_initial_data()
|
|
|
|
def _load_initial_data(self):
|
|
try:
|
|
app_data = self.config_manager.get_application_by_name(self.original_app_name)
|
|
self.name_entry_var.set(app_data.get("name", ""))
|
|
self.path_entry_var.set(app_data.get("path", ""))
|
|
self._populate_parameters_tree(app_data.get("parameters", []))
|
|
except NameNotFoundError:
|
|
logger.error(f"Cannot edit application. Name '{self.original_app_name}' not found.")
|
|
messagebox.showerror("Error", f"Application '{self.original_app_name}' not found. Cannot edit.", parent=self.parent_widget)
|
|
self.destroy()
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error loading application data for edit: {e}", exc_info=True)
|
|
messagebox.showerror("Error", f"Could not load application data for editing:\n{e}", parent=self.parent_widget)
|
|
self.destroy()
|
|
|
|
def _on_save(self):
|
|
if not self._validate_inputs():
|
|
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()
|
|
|
|
updated_application_data = {
|
|
"name": new_app_name,
|
|
"path": new_app_path,
|
|
"parameters": new_parameters
|
|
}
|
|
|
|
try:
|
|
self.config_manager.update_application(self.original_app_name, updated_application_data)
|
|
self.result = updated_application_data
|
|
logger.info(f"Application '{self.original_app_name}' updated to '{new_app_name}' successfully through dialog.")
|
|
# messagebox.showinfo("Success", f"Application '{new_app_name}' updated successfully.", parent=self.parent_widget)
|
|
self.destroy()
|
|
except NameNotFoundError as e:
|
|
logger.error(f"Update failed: Original application '{self.original_app_name}' not found: {e}", exc_info=True)
|
|
messagebox.showerror("Error Updating Application", str(e), parent=self)
|
|
except DuplicateNameError as e:
|
|
logger.warning(f"Failed to update application: {e}")
|
|
messagebox.showerror("Error Updating Application", str(e), parent=self)
|
|
self.name_entry.focus_set()
|
|
except ConfigError as e:
|
|
logger.error(f"Configuration error updating application '{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:
|
|
logger.error(f"Unexpected error updating application '{new_app_name}': {e}", exc_info=True)
|
|
messagebox.showerror("Unexpected Error", f"An unexpected error occurred:\n{e}", parent=self) |