Source code for hbat.gui.preset_manager_dialog

"""
Preset Manager dialog for HBAT GUI.

This module provides a dedicated dialog for managing analysis parameter presets,
including loading, saving, and organizing preset files.
"""

import json
import os
import tkinter as tk
from datetime import datetime
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from typing import Any, Dict, Optional, Callable

from ..constants.parameters import ParametersDefault
from ..core.analysis import AnalysisParameters


[docs] class PresetManagerDialog: """Dialog for managing analysis parameter presets."""
[docs] def __init__(self, parent: tk.Tk, current_params: Optional[AnalysisParameters] = None): """Initialize preset manager dialog. :param parent: Parent window :type parent: tk.Tk :param current_params: Current analysis parameters :type current_params: Optional[AnalysisParameters] """ self.parent = parent self.current_params = current_params or AnalysisParameters() self.selected_preset_data = None self.result = None self.preset_file_paths = {} # Store mapping of item_id to file path # Create dialog window self.dialog = tk.Toplevel(parent) self.dialog.title("Preset Manager") self.dialog.geometry("800x600") self.dialog.resizable(True, True) # Make dialog modal self.dialog.transient(parent) self.dialog.grab_set() # Create widgets self._create_widgets() # Center the dialog self.dialog.update_idletasks() x = (self.dialog.winfo_screenwidth() // 2) - (self.dialog.winfo_width() // 2) y = (self.dialog.winfo_screenheight() // 2) - (self.dialog.winfo_height() // 2) self.dialog.geometry(f"+{x}+{y}") # Handle window closing self.dialog.protocol("WM_DELETE_WINDOW", self._cancel) # Load available presets self._refresh_preset_list()
def _create_widgets(self): """Create dialog widgets.""" # Main frame with padding main_frame = ttk.Frame(self.dialog, padding="20") main_frame.pack(fill=tk.BOTH, expand=True) # Title title_label = ttk.Label( main_frame, text="HBAT Analysis Parameter Presets", font=("TkDefaultFont", 12, "bold") ) title_label.pack(pady=(0, 20)) # Create notebook for different sections notebook = ttk.Notebook(main_frame) notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 20)) # Load presets tab self._create_load_tab(notebook) # Save presets tab self._create_save_tab(notebook) # Manage presets tab self._create_manage_tab(notebook) # Button frame button_frame = ttk.Frame(main_frame) button_frame.pack(fill=tk.X) ttk.Button( button_frame, text="Load Selected", command=self._load_selected_preset ).pack(side=tk.RIGHT, padx=(5, 0)) ttk.Button( button_frame, text="Cancel", command=self._cancel ).pack(side=tk.RIGHT) def _create_load_tab(self, notebook): """Create the load presets tab.""" load_frame = ttk.Frame(notebook, padding="10") notebook.add(load_frame, text="Load Preset") # Instructions instructions = ttk.Label( load_frame, text="Select a preset to load parameters:", font=("TkDefaultFont", 10) ) instructions.pack(anchor=tk.W, pady=(0, 10)) # Preset list frame list_frame = ttk.Frame(load_frame) list_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # Treeview for presets columns = ("Name", "Description", "Date", "Location") self.preset_tree = ttk.Treeview(list_frame, columns=columns, show="tree headings", height=12) # Configure columns self.preset_tree.heading("#0", text="", anchor=tk.W) self.preset_tree.column("#0", width=30, minwidth=30) for col in columns: self.preset_tree.heading(col, text=col, anchor=tk.W) self.preset_tree.column("Name", width=150, minwidth=100) self.preset_tree.column("Description", width=200, minwidth=150) self.preset_tree.column("Date", width=120, minwidth=100) self.preset_tree.column("Location", width=100, minwidth=80) # Scrollbars for treeview v_scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.preset_tree.yview) h_scrollbar = ttk.Scrollbar(list_frame, orient=tk.HORIZONTAL, command=self.preset_tree.xview) self.preset_tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) # Grid layout for treeview and scrollbars self.preset_tree.grid(row=0, column=0, sticky="nsew") v_scrollbar.grid(row=0, column=1, sticky="ns") h_scrollbar.grid(row=1, column=0, sticky="ew") list_frame.grid_rowconfigure(0, weight=1) list_frame.grid_columnconfigure(0, weight=1) # Bind selection event self.preset_tree.bind("<<TreeviewSelect>>", self._on_preset_select) self.preset_tree.bind("<Double-1>", lambda e: self._load_selected_preset()) # Action buttons action_frame = ttk.Frame(load_frame) action_frame.pack(fill=tk.X) ttk.Button( action_frame, text="Browse...", command=self._browse_for_preset ).pack(side=tk.LEFT, padx=(0, 5)) ttk.Button( action_frame, text="Refresh", command=self._refresh_preset_list ).pack(side=tk.LEFT, padx=5) def _create_save_tab(self, notebook): """Create the save presets tab.""" save_frame = ttk.Frame(notebook, padding="10") notebook.add(save_frame, text="Save Preset") # Instructions instructions = ttk.Label( save_frame, text="Save current parameters as a preset:", font=("TkDefaultFont", 10) ) instructions.pack(anchor=tk.W, pady=(0, 20)) # Preset info frame info_frame = ttk.LabelFrame(save_frame, text="Preset Information", padding="10") info_frame.pack(fill=tk.X, pady=(0, 20)) # Name entry ttk.Label(info_frame, text="Name:").grid(row=0, column=0, sticky=tk.W, pady=2) self.name_var = tk.StringVar(value=f"Custom Preset {datetime.now().strftime('%Y%m%d_%H%M%S')}") name_entry = ttk.Entry(info_frame, textvariable=self.name_var, width=40) name_entry.grid(row=0, column=1, sticky=tk.EW, padx=(10, 0), pady=2) # Description entry ttk.Label(info_frame, text="Description:").grid(row=1, column=0, sticky=tk.NW, pady=2) self.description_var = tk.StringVar(value="Custom HBAT Analysis Parameters") desc_entry = ttk.Entry(info_frame, textvariable=self.description_var, width=40) desc_entry.grid(row=1, column=1, sticky=tk.EW, padx=(10, 0), pady=2) info_frame.grid_columnconfigure(1, weight=1) # Current parameters preview preview_frame = ttk.LabelFrame(save_frame, text="Current Parameters", padding="10") preview_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 20)) # Create text widget for parameters preview self.preview_text = tk.Text(preview_frame, height=10, width=60, font=("Courier", 9)) preview_scrollbar = ttk.Scrollbar(preview_frame, orient=tk.VERTICAL, command=self.preview_text.yview) self.preview_text.configure(yscrollcommand=preview_scrollbar.set) self.preview_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) preview_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # Update preview self._update_parameters_preview() # Save button ttk.Button( save_frame, text="Save Preset", command=self._save_current_preset ).pack(pady=(0, 10)) def _create_manage_tab(self, notebook): """Create the manage presets tab.""" manage_frame = ttk.Frame(notebook, padding="10") notebook.add(manage_frame, text="Manage") # Instructions instructions = ttk.Label( manage_frame, text="Manage your preset files:", font=("TkDefaultFont", 10) ) instructions.pack(anchor=tk.W, pady=(0, 20)) # Directory info dir_frame = ttk.LabelFrame(manage_frame, text="Preset Directories", padding="10") dir_frame.pack(fill=tk.X, pady=(0, 20)) # User presets directory user_dir = self._get_presets_directory() ttk.Label(dir_frame, text="User Presets:").pack(anchor=tk.W) user_dir_frame = ttk.Frame(dir_frame) user_dir_frame.pack(fill=tk.X, pady=(2, 10)) ttk.Label(user_dir_frame, text=user_dir, font=("Courier", 8)).pack(side=tk.LEFT, fill=tk.X, expand=True) ttk.Button( user_dir_frame, text="Open", command=lambda: self._open_directory(user_dir) ).pack(side=tk.RIGHT) # Example presets directory example_dir = self._get_example_presets_directory() ttk.Label(dir_frame, text="Example Presets:").pack(anchor=tk.W) example_dir_frame = ttk.Frame(dir_frame) example_dir_frame.pack(fill=tk.X, pady=2) ttk.Label(example_dir_frame, text=example_dir, font=("Courier", 8)).pack(side=tk.LEFT, fill=tk.X, expand=True) ttk.Button( example_dir_frame, text="Open", command=lambda: self._open_directory(example_dir) ).pack(side=tk.RIGHT) # Actions actions_frame = ttk.LabelFrame(manage_frame, text="Actions", padding="10") actions_frame.pack(fill=tk.X) ttk.Button( actions_frame, text="Open Preset Directory", command=lambda: self._open_directory(user_dir) ).pack(anchor=tk.W, pady=2) ttk.Button( actions_frame, text="Import Preset...", command=self._import_preset ).pack(anchor=tk.W, pady=2) def _refresh_preset_list(self): """Refresh the list of available presets.""" # Clear existing items and file path mapping for item in self.preset_tree.get_children(): self.preset_tree.delete(item) self.preset_file_paths.clear() # Load presets from different locations self._load_presets_from_directory("User", self._get_presets_directory()) self._load_presets_from_directory("Examples", self._get_example_presets_directory()) def _load_presets_from_directory(self, category: str, directory: str): """Load presets from a specific directory.""" if not os.path.exists(directory): return # Create category node category_node = self.preset_tree.insert("", "end", text=category, values=("", "", "", "")) try: for filename in os.listdir(directory): if filename.endswith(('.hbat', '.json')): filepath = os.path.join(directory, filename) try: with open(filepath, 'r') as f: data = json.load(f) # Extract preset info name = data.get("name", os.path.splitext(filename)[0]) description = data.get("description", "") created = data.get("created", "") # Format date if created: try: date_obj = datetime.fromisoformat(created.replace('Z', '+00:00')) formatted_date = date_obj.strftime("%Y-%m-%d %H:%M") except: formatted_date = created else: # Use file modification time mtime = os.path.getmtime(filepath) formatted_date = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M") # Insert preset item item_id = self.preset_tree.insert( category_node, "end", text="📄", values=(name, description, formatted_date, category) ) # Store file path in mapping for later use self.preset_file_paths[item_id] = filepath except Exception as e: print(f"Error loading preset {filename}: {e}") except Exception as e: print(f"Error reading directory {directory}: {e}") # Expand category node self.preset_tree.item(category_node, open=True) def _on_preset_select(self, event): """Handle preset selection.""" selection = self.preset_tree.selection() if not selection: return item = selection[0] filepath = self.preset_file_paths.get(item) # Only process if it's a preset file (not a category) if filepath and os.path.exists(filepath): try: with open(filepath, 'r') as f: self.selected_preset_data = json.load(f) except Exception as e: messagebox.showerror("Error", f"Failed to load preset: {str(e)}") self.selected_preset_data = None def _load_selected_preset(self): """Load the selected preset.""" if not self.selected_preset_data: messagebox.showwarning("Warning", "Please select a preset to load.") return self.result = self.selected_preset_data self.dialog.destroy() def _browse_for_preset(self): """Browse for a preset file.""" filename = filedialog.askopenfilename( title="Load Preset File", filetypes=[("HBAT Presets", "*.hbat"), ("JSON files", "*.json"), ("All files", "*.*")] ) if filename: try: with open(filename, 'r') as f: data = json.load(f) self.selected_preset_data = data self.result = data self.dialog.destroy() except Exception as e: messagebox.showerror("Error", f"Failed to load preset: {str(e)}") def _save_current_preset(self): """Save current parameters as a preset.""" name = self.name_var.get().strip() if not name: messagebox.showwarning("Warning", "Please enter a name for the preset.") return # Get save filename user_dir = self._get_presets_directory() filename = filedialog.asksaveasfilename( title="Save Preset", initialdir=user_dir, initialfile=f"{name}.hbat", defaultextension=".hbat", filetypes=[("HBAT Presets", "*.hbat"), ("JSON files", "*.json")] ) if filename: try: preset_data = self._create_preset_data() preset_data["name"] = name preset_data["description"] = self.description_var.get() with open(filename, 'w') as f: json.dump(preset_data, f, indent=2) messagebox.showinfo("Success", f"Preset saved successfully to:\n{os.path.basename(filename)}") self._refresh_preset_list() except Exception as e: messagebox.showerror("Error", f"Failed to save preset: {str(e)}") def _import_preset(self): """Import a preset file to the user directory.""" filename = filedialog.askopenfilename( title="Import Preset File", filetypes=[("HBAT Presets", "*.hbat"), ("JSON files", "*.json"), ("All files", "*.*")] ) if filename: try: # Copy to user directory user_dir = self._get_presets_directory() dest_filename = os.path.join(user_dir, os.path.basename(filename)) import shutil shutil.copy2(filename, dest_filename) messagebox.showinfo("Success", f"Preset imported successfully to:\n{dest_filename}") self._refresh_preset_list() except Exception as e: messagebox.showerror("Error", f"Failed to import preset: {str(e)}") def _open_directory(self, directory: str): """Open a directory in the file manager.""" if not os.path.exists(directory): os.makedirs(directory, exist_ok=True) import subprocess import sys try: if sys.platform == "win32": os.startfile(directory) elif sys.platform == "darwin": subprocess.run(["open", directory]) else: subprocess.run(["xdg-open", directory]) except Exception as e: messagebox.showerror("Error", f"Failed to open directory: {str(e)}") def _update_parameters_preview(self): """Update the parameters preview text.""" self.preview_text.delete(1.0, tk.END) params = self.current_params preview = f"""Hydrogen Bond Parameters: H...A Distance: {params.hb_distance_cutoff:.1f} Å D-H...A Angle: {params.hb_angle_cutoff:.0f}° D...A Distance: {params.hb_donor_acceptor_cutoff:.1f} Å Weak Hydrogen Bond Parameters (C-H···O): H...A Distance: {params.whb_distance_cutoff:.1f} Å D-H...A Angle: {params.whb_angle_cutoff:.0f}° D...A Distance: {params.whb_donor_acceptor_cutoff:.1f} Å Halogen Bond Parameters: X...A Distance: {params.xb_distance_cutoff:.1f} Å C-X...A Angle: {params.xb_angle_cutoff:.0f}° π Interaction Parameters: General H...π Distance: {params.pi_distance_cutoff:.1f} Å General D-H...π Angle: {params.pi_angle_cutoff:.0f}° Hydrogen-π Interactions: C-H...π Distance: {params.pi_ch_distance_cutoff:.1f} Å, Angle: {params.pi_ch_angle_cutoff:.0f}° N-H...π Distance: {params.pi_nh_distance_cutoff:.1f} Å, Angle: {params.pi_nh_angle_cutoff:.0f}° O-H...π Distance: {params.pi_oh_distance_cutoff:.1f} Å, Angle: {params.pi_oh_angle_cutoff:.0f}° S-H...π Distance: {params.pi_sh_distance_cutoff:.1f} Å, Angle: {params.pi_sh_angle_cutoff:.0f}° Halogen-π Interactions: C-Cl...π Distance: {params.pi_ccl_distance_cutoff:.1f} Å, Angle: {params.pi_ccl_angle_cutoff:.0f}° C-Br...π Distance: {params.pi_cbr_distance_cutoff:.1f} Å, Angle: {params.pi_cbr_angle_cutoff:.0f}° C-I...π Distance: {params.pi_ci_distance_cutoff:.1f} Å, Angle: {params.pi_ci_angle_cutoff:.0f}° General Parameters: Covalent Bond Factor: {params.covalent_cutoff_factor:.2f} Analysis Mode: {params.analysis_mode} """ # Add PDB fixing parameters if they exist if hasattr(params, 'fix_pdb_enabled'): pdb_preview = f""" PDB Fixing Parameters: Enabled: {params.fix_pdb_enabled} Method: {params.fix_pdb_method} Add Hydrogens: {params.fix_pdb_add_hydrogens} Add Heavy Atoms: {params.fix_pdb_add_heavy_atoms} Replace Nonstandard: {params.fix_pdb_replace_nonstandard} Remove Heterogens: {params.fix_pdb_remove_heterogens} Keep Water: {params.fix_pdb_keep_water} """ preview += pdb_preview self.preview_text.insert(1.0, preview) self.preview_text.config(state=tk.DISABLED) def _create_preset_data(self) -> Dict[str, Any]: """Create preset data from current parameters.""" params = self.current_params preset_data = { "format_version": "1.0", "application": "HBAT", "created": datetime.now().isoformat(), "description": self.description_var.get(), "parameters": { "hydrogen_bonds": { "h_a_distance_cutoff": params.hb_distance_cutoff, "dha_angle_cutoff": params.hb_angle_cutoff, "d_a_distance_cutoff": params.hb_donor_acceptor_cutoff }, "halogen_bonds": { "x_a_distance_cutoff": params.xb_distance_cutoff, "dxa_angle_cutoff": params.xb_angle_cutoff }, "pi_interactions": { "h_pi_distance_cutoff": params.pi_distance_cutoff, "dh_pi_angle_cutoff": params.pi_angle_cutoff }, "general": { "covalent_cutoff_factor": params.covalent_cutoff_factor, "analysis_mode": params.analysis_mode } } } # Add PDB fixing parameters if they exist if hasattr(params, 'fix_pdb_enabled'): preset_data["parameters"]["pdb_fixing"] = { "enabled": params.fix_pdb_enabled, "method": params.fix_pdb_method, "add_hydrogens": params.fix_pdb_add_hydrogens, "add_heavy_atoms": params.fix_pdb_add_heavy_atoms, "replace_nonstandard": params.fix_pdb_replace_nonstandard, "remove_heterogens": params.fix_pdb_remove_heterogens, "keep_water": params.fix_pdb_keep_water } return preset_data def _get_presets_directory(self) -> str: """Get or create the user presets directory.""" home_dir = os.path.expanduser("~") presets_dir = os.path.join(home_dir, ".hbat", "presets") os.makedirs(presets_dir, exist_ok=True) return presets_dir def _get_example_presets_directory(self) -> str: """Get the example presets directory relative to the package.""" current_dir = os.path.dirname(os.path.abspath(__file__)) package_root = os.path.dirname(os.path.dirname(current_dir)) example_presets_dir = os.path.join(package_root, "example_presets") return example_presets_dir def _cancel(self): """Cancel and close dialog.""" self.result = None self.dialog.destroy()
[docs] def get_result(self) -> Optional[Dict[str, Any]]: """Get the dialog result. :returns: Selected preset data or None if cancelled :rtype: Optional[Dict[str, Any]] """ self.dialog.wait_window() return self.result