Source code for hbat.gui.main_window

"""
Main GUI window for HBAT application.

This module provides the main tkinter interface for the HBAT application,
allowing users to load PDB files, configure analysis parameters, and view results.
"""

import os
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
from typing import Optional

from ..constants import APP_NAME, APP_VERSION, GUIDefaults
from ..core.analysis import AnalysisParameters, HBondAnalyzer
from .parameter_panel import ParameterPanel
from .results_panel import ResultsPanel


[docs] class MainWindow: """Main application window for HBAT. This class provides the primary GUI interface for HBAT, including file loading, parameter configuration, analysis execution, and results visualization. :param None: This class takes no parameters during initialization """
[docs] def __init__(self) -> None: """Initialize the main window. Sets up the complete GUI interface including menus, toolbar, main content area, and status bar. :returns: None :rtype: None """ self.root = tk.Tk() self.root.title(f"{APP_NAME} v{APP_VERSION}") self.root.geometry(f"{GUIDefaults.WINDOW_WIDTH}x{GUIDefaults.WINDOW_HEIGHT}") self.root.minsize(GUIDefaults.MIN_WINDOW_WIDTH, GUIDefaults.MIN_WINDOW_HEIGHT) # Analysis components self.analyzer: Optional[HBondAnalyzer] = None self.current_file: Optional[str] = None self.analysis_thread: Optional[threading.Thread] = None # Create UI components self._create_menu() self._create_toolbar() self._create_main_content() self._create_status_bar() # Set up event handlers self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
def _create_menu(self) -> None: """Create the menu bar. Sets up the application menu with File, Analysis, Tools, and Help menus, including keyboard shortcuts and event bindings. :returns: None :rtype: None """ menubar = tk.Menu(self.root) self.root.config(menu=menubar) # File menu file_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="File", menu=file_menu) file_menu.add_command( label="Open PDB File...", accelerator="Ctrl+O", command=self._open_file ) file_menu.add_separator() file_menu.add_command( label="Save Results...", accelerator="Ctrl+S", command=self._save_results ) file_menu.add_command( label="Export All...", accelerator="Ctrl+E", command=self._export_all ) file_menu.add_separator() file_menu.add_command( label="Exit", accelerator="Ctrl+Q", command=self._on_closing ) # Analysis menu analysis_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Analysis", menu=analysis_menu) analysis_menu.add_command( label="Run Analysis", accelerator="F5", command=self._run_analysis ) analysis_menu.add_command(label="Clear Results", command=self._clear_results) analysis_menu.add_separator() analysis_menu.add_command( label="Reset Parameters", command=self._reset_parameters ) # Tools menu tools_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Tools", menu=tools_menu) tools_menu.add_command(label="Generate Charts", command=self._generate_charts) tools_menu.add_command( label="Export Visualization", command=self._export_visualization ) # Help menu help_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Help", menu=help_menu) help_menu.add_command(label="About", command=self._show_about) help_menu.add_command(label="User Guide", command=self._show_help) # Bind keyboard shortcuts self.root.bind("<Control-o>", lambda e: self._open_file()) self.root.bind("<Control-s>", lambda e: self._save_results()) self.root.bind("<Control-e>", lambda e: self._export_all()) self.root.bind("<Control-q>", lambda e: self._on_closing()) self.root.bind("<F5>", lambda e: self._run_analysis()) def _create_toolbar(self) -> None: """Create the toolbar. Creates a toolbar with buttons for common operations like file opening, analysis execution, and result management. Includes a progress bar. :returns: None :rtype: None """ toolbar = ttk.Frame(self.root) toolbar.pack(fill=tk.X, padx=5, pady=2) # File operations ttk.Button(toolbar, text="Open PDB", command=self._open_file).pack( side=tk.LEFT, padx=2 ) ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, padx=5, fill=tk.Y) # Analysis operations self.run_button = ttk.Button( toolbar, text="Run Analysis", command=self._run_analysis, state=tk.DISABLED ) self.run_button.pack(side=tk.LEFT, padx=2) ttk.Button(toolbar, text="Clear Results", command=self._clear_results).pack( side=tk.LEFT, padx=2 ) ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, padx=5, fill=tk.Y) # Export operations ttk.Button(toolbar, text="Save Results", command=self._save_results).pack( side=tk.LEFT, padx=2 ) # Progress bar self.progress_var = tk.DoubleVar() self.progress_bar = ttk.Progressbar( toolbar, variable=self.progress_var, mode="indeterminate" ) self.progress_bar.pack(side=tk.RIGHT, padx=5, fill=tk.X, expand=True) def _create_main_content(self) -> None: """Create the main content area. Sets up the main interface with a paned window containing file content, parameter panels, and results display areas. :returns: None :rtype: None """ # Create main paned window main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL) main_paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # Left panel - File content and parameters left_frame = ttk.Frame(main_paned) main_paned.add(left_frame, weight=1) # Create notebook for left panel left_notebook = ttk.Notebook(left_frame) left_notebook.pack(fill=tk.BOTH, expand=True) # File content tab file_frame = ttk.Frame(left_notebook) left_notebook.add(file_frame, text="PDB File") self.file_text = scrolledtext.ScrolledText( file_frame, wrap=tk.NONE, font=("Courier", 9) ) self.file_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # Parameters tab self.parameter_panel = ParameterPanel(left_notebook) left_notebook.add(self.parameter_panel.frame, text="Parameters") # Right panel - Results right_frame = ttk.Frame(main_paned) main_paned.add(right_frame, weight=2) self.results_panel = ResultsPanel(right_frame) # Set initial pane positions main_paned.sashpos(0, GUIDefaults.LEFT_PANEL_WIDTH) def _create_status_bar(self) -> None: """Create the status bar. Creates a status bar at the bottom of the window to display application state and progress information. :returns: None :rtype: None """ self.status_var = tk.StringVar() self.status_var.set("Ready") status_frame = ttk.Frame(self.root) status_frame.pack(fill=tk.X, side=tk.BOTTOM) ttk.Label( status_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W ).pack(fill=tk.X, padx=2, pady=1) def _open_file(self) -> None: """Open a PDB file. Displays a file dialog to select a PDB file, loads its content, and enables analysis functionality. :returns: None :rtype: None """ filename = filedialog.askopenfilename( title="Open PDB File", filetypes=[("PDB files", "*.pdb"), ("All files", "*.*")], ) if filename: try: self.current_file = filename self._load_file_content(filename) self.run_button.config(state=tk.NORMAL) self.status_var.set(f"Loaded: {os.path.basename(filename)}") self._clear_results() except Exception as e: messagebox.showerror("Error", f"Failed to load file:\n{str(e)}") self.status_var.set("Error loading file") def _load_file_content(self, filename: str) -> None: """Load and display file content. Reads the PDB file content and displays it in the text widget with syntax highlighting for PDB record types. :param filename: Path to the PDB file to load :type filename: str :returns: None :rtype: None :raises Exception: If file cannot be read """ try: with open(filename, "r") as file: content = file.read() self.file_text.delete(1.0, tk.END) self.file_text.insert(1.0, content) # Highlight ATOM and HETATM lines self._highlight_pdb_records() except Exception as e: raise Exception(f"Cannot read file: {e}") def _highlight_pdb_records(self) -> None: """Highlight important PDB record types. Applies color coding to different PDB record types (ATOM, HETATM, HEADER, etc.) for better readability. :returns: None :rtype: None """ # Configure text tags self.file_text.tag_configure("atom", foreground="blue") self.file_text.tag_configure("hetatm", foreground="red") self.file_text.tag_configure( "header", foreground="green", font=("Courier", 9, "bold") ) content = self.file_text.get(1.0, tk.END) lines = content.split("\n") for i, line in enumerate(lines): line_start = f"{i+1}.0" line_end = f"{i+1}.end" if line.startswith("ATOM"): self.file_text.tag_add("atom", line_start, line_end) elif line.startswith("HETATM"): self.file_text.tag_add("hetatm", line_start, line_end) elif line.startswith(("HEADER", "TITLE", "COMPND")): self.file_text.tag_add("header", line_start, line_end) def _run_analysis(self) -> None: """Run the molecular interaction analysis. Initiates analysis in a separate thread using current parameters and loaded PDB file. Updates UI to show progress. :returns: None :rtype: None """ if not self.current_file: messagebox.showwarning("Warning", "Please open a PDB file first.") return if self.analysis_thread and self.analysis_thread.is_alive(): messagebox.showinfo("Info", "Analysis is already running.") return # Get parameters from the parameter panel params = self.parameter_panel.get_parameters() # Start analysis in a separate thread self.analysis_thread = threading.Thread( target=self._perform_analysis, args=(params,) ) self.analysis_thread.daemon = True self.analysis_thread.start() # Update UI self.run_button.config(state=tk.DISABLED) self.progress_bar.config(mode="indeterminate") self.progress_bar.start(GUIDefaults.PROGRESS_BAR_INTERVAL) self.status_var.set("Running analysis...") def _perform_analysis(self, params: AnalysisParameters) -> None: """Perform the analysis in a separate thread. Executes the molecular interaction analysis using the provided parameters and updates the UI upon completion or error. :param params: Analysis parameters to use :type params: AnalysisParameters :returns: None :rtype: None """ try: # Create analyzer self.analyzer = HBondAnalyzer(params) # Run analysis success = self.analyzer.analyze_file(self.current_file) if success: # Update results on main thread self.root.after(0, self._analysis_complete) else: self.root.after(0, self._analysis_error, "Analysis failed") except Exception as e: self.root.after(0, self._analysis_error, str(e)) def _analysis_complete(self) -> None: """Handle successful analysis completion. Updates the UI after successful analysis, stops progress indication, displays results, and shows completion notification. :returns: None :rtype: None """ self.progress_bar.stop() self.progress_bar.config(mode="determinate") self.progress_var.set(0) self.run_button.config(state=tk.NORMAL) # Update results panel self.results_panel.update_results(self.analyzer) # Update status stats = self.analyzer.get_statistics() self.status_var.set( f"Analysis complete - H-bonds: {stats['hydrogen_bonds']}, " f"X-bonds: {stats['halogen_bonds']}, π-interactions: {stats['pi_interactions']}" ) messagebox.showinfo("Success", "Analysis completed successfully!") def _analysis_error(self, error_msg: str) -> None: """Handle analysis error. Updates the UI after analysis failure, stops progress indication, and displays error message to the user. :param error_msg: Error message to display :type error_msg: str :returns: None :rtype: None """ self.progress_bar.stop() self.progress_bar.config(mode="determinate") self.progress_var.set(0) self.run_button.config(state=tk.NORMAL) self.status_var.set("Analysis failed") messagebox.showerror("Analysis Error", f"Analysis failed:\n{error_msg}") def _clear_results(self) -> None: """Clear analysis results. Clears all analysis results from the interface and resets the analyzer state. :returns: None :rtype: None """ self.results_panel.clear_results() self.analyzer = None self.status_var.set("Results cleared") def _save_results(self) -> None: """Save analysis results to file. Displays a file dialog to save analysis results in text format. Requires completed analysis to function. :returns: None :rtype: None """ if not self.analyzer: messagebox.showwarning("Warning", "No results to save. Run analysis first.") return filename = filedialog.asksaveasfilename( title="Save Results", defaultextension=".txt", filetypes=[ ("Text files", "*.txt"), ("CSV files", "*.csv"), ("All files", "*.*"), ], ) if filename: try: self._export_results_to_file(filename) messagebox.showinfo("Success", f"Results saved to {filename}") self.status_var.set(f"Results saved to {os.path.basename(filename)}") except Exception as e: messagebox.showerror("Error", f"Failed to save results:\n{str(e)}") def _export_results_to_file(self, filename: str) -> None: """Export results to a file. Writes complete analysis results to the specified file in human-readable text format. :param filename: Path to the output file :type filename: str :returns: None :rtype: None """ with open(filename, "w") as f: f.write("HBAT Analysis Results\n") f.write("=" * 50 + "\n\n") if self.current_file: f.write(f"Input file: {self.current_file}\n\n") # Write summary stats = self.analyzer.get_statistics() f.write("Summary:\n") f.write(f" Hydrogen bonds: {stats['hydrogen_bonds']}\n") f.write(f" Halogen bonds: {stats['halogen_bonds']}\n") f.write(f" π interactions: {stats['pi_interactions']}\n") f.write(f" Total interactions: {stats['total_interactions']}\n\n") # Write detailed results f.write("Hydrogen Bonds:\n") f.write("-" * 30 + "\n") for hb in self.analyzer.hydrogen_bonds: f.write(f"{hb}\n") f.write("\nHalogen Bonds:\n") f.write("-" * 30 + "\n") for xb in self.analyzer.halogen_bonds: f.write(f"{xb}\n") f.write("\nπ Interactions:\n") f.write("-" * 30 + "\n") for pi in self.analyzer.pi_interactions: f.write(f"{pi}\n") def _export_all(self) -> None: """Export all results in multiple formats. Exports analysis results to a directory in multiple file formats for comprehensive data preservation. :returns: None :rtype: None """ if not self.analyzer: messagebox.showwarning( "Warning", "No results to export. Run analysis first." ) return directory = filedialog.askdirectory(title="Select Export Directory") if directory: try: base_name = ( os.path.splitext(os.path.basename(self.current_file))[0] if self.current_file else "hbat_results" ) # Export text summary self._export_results_to_file( os.path.join(directory, f"{base_name}_summary.txt") ) messagebox.showinfo("Success", f"Results exported to {directory}") self.status_var.set("All results exported") except Exception as e: messagebox.showerror("Error", f"Failed to export results:\n{str(e)}") def _generate_charts(self) -> None: """Generate analysis charts. Creates visual charts and graphs from analysis results. Currently shows placeholder message for future implementation. :returns: None :rtype: None """ if not self.analyzer: messagebox.showwarning( "Warning", "No results available. Run analysis first." ) return messagebox.showinfo("Info", "Chart generation feature coming soon!") def _export_visualization(self) -> None: """Export visualization scripts. Exports scripts for external visualization tools. Currently shows placeholder message for future implementation. :returns: None :rtype: None """ if not self.analyzer: messagebox.showwarning( "Warning", "No results available. Run analysis first." ) return messagebox.showinfo("Info", "Visualization export feature coming soon!") def _reset_parameters(self) -> None: """Reset analysis parameters to defaults. Restores all analysis parameters to their default values as defined in the application constants. :returns: None :rtype: None """ self.parameter_panel.reset_to_defaults() self.status_var.set("Parameters reset to defaults") def _show_about(self) -> None: """Show about dialog. Displays application information including version, authors, and institutional affiliation. :returns: None :rtype: None """ about_text = """ HBAT - Hydrogen Bond Analysis Tool v2.0 A comprehensive tool for analyzing hydrogen bonds, halogen bonds, and X-H...π interactions in protein structures. Original Authors: Abhishek Tiwari & Sunil Kumar Panigrahi Guide & Project Head: Prof Gautam R. Desiraju School of Chemistry, University of Hyderabad Python conversion: 2025 """ messagebox.showinfo("About HBAT", about_text.strip()) def _show_help(self) -> None: """Show help dialog. Displays basic usage instructions and guidance for using the HBAT GUI application. :returns: None :rtype: None """ help_text = """ HBAT User Guide 1. Open a PDB file using File > Open PDB File 2. Adjust analysis parameters in the Parameters tab 3. Click 'Run Analysis' or press F5 to start analysis 4. View results in the Results panel 5. Save or export results using the File menu For more detailed information, please refer to the documentation. """ messagebox.showinfo("Help", help_text.strip()) def _on_closing(self) -> None: """Handle window closing event. Handles application shutdown, checking for running analysis and prompting user confirmation if needed. :returns: None :rtype: None """ if self.analysis_thread and self.analysis_thread.is_alive(): result = messagebox.askyesno( "Confirm Exit", "Analysis is running. Are you sure you want to exit?" ) if not result: return self.root.destroy()
[docs] def run(self) -> None: """Start the GUI application. Enters the main GUI event loop to begin accepting user interactions. This method blocks until the application is closed. :returns: None :rtype: None """ self.root.mainloop()