# 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("<>", 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)