"""
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