LCOV - code coverage report
Current view: top level - src/pairinteraction_gui - main_window.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 89 114 78.1 %
Date: 2025-06-06 09:09:03 Functions: 9 20 45.0 %

          Line data    Source code
       1             : # SPDX-FileCopyrightText: 2025 Pairinteraction Developers
       2             : # SPDX-License-Identifier: LGPL-3.0-or-later
       3             : 
       4           1 : import logging
       5           1 : from typing import TYPE_CHECKING, Optional, TypeVar
       6             : 
       7           1 : from PySide6.QtCore import QObject, QSize, Qt
       8           1 : from PySide6.QtGui import QAction, QActionGroup, QCloseEvent, QIcon, QKeySequence, QShortcut
       9           1 : from PySide6.QtWidgets import (
      10             :     QDockWidget,
      11             :     QMainWindow,
      12             :     QMessageBox,
      13             :     QSizePolicy,
      14             :     QStatusBar,
      15             :     QToolBar,
      16             :     QWidget,
      17             : )
      18             : 
      19           1 : import pairinteraction
      20           1 : from pairinteraction._wrapped import Database
      21           1 : from pairinteraction_gui.app import Application
      22           1 : from pairinteraction_gui.page import (
      23             :     LifetimesPage,
      24             :     OneAtomPage,
      25             :     TwoAtomsPage,
      26             : )
      27           1 : from pairinteraction_gui.page.base_page import SimulationPage
      28           1 : from pairinteraction_gui.qobjects import NamedStackedWidget
      29           1 : from pairinteraction_gui.theme import main_theme
      30           1 : from pairinteraction_gui.utils import download_databases_mp
      31           1 : from pairinteraction_gui.worker import MultiProcessWorker, MultiThreadWorker
      32             : 
      33             : if TYPE_CHECKING:
      34             :     from pairinteraction_gui.page import BasePage
      35             : 
      36             :     ChildType = TypeVar("ChildType", bound=QObject)
      37             : 
      38           1 : logger = logging.getLogger(__name__)
      39             : 
      40             : 
      41           1 : class MainWindow(QMainWindow):
      42             :     """Main window for the PairInteraction GUI application."""
      43             : 
      44           1 :     def __init__(self) -> None:
      45             :         """Initialize the main window."""
      46           1 :         super().__init__()
      47             : 
      48           1 :         self.setWindowTitle(f"PairInteraction v{pairinteraction.__version__}")
      49           1 :         self.resize(1200, 800)
      50           1 :         self.setStyleSheet(main_theme)
      51             : 
      52           1 :         self.statusbar = self.setup_statusbar()
      53           1 :         self.dockwidget = self.setup_dockwidget()
      54             : 
      55           1 :         self.stacked_pages = self.setup_stacked_pages()
      56           1 :         self.toolbar = self.setup_toolbar()
      57             : 
      58           1 :         self.init_keyboard_shortcuts()
      59           1 :         self.connect_signals()
      60             : 
      61           1 :         MultiProcessWorker.create_pool()
      62             : 
      63           1 :     def connect_signals(self) -> None:
      64             :         """Connect signals to slots."""
      65           1 :         self.signals = Application.instance().signals
      66             : 
      67           1 :         self.signals.ask_download_database.connect(self.ask_download_database)
      68             : 
      69           1 :     def findChild(  # type: ignore [override] # explicitly override type hints
      70             :         self, type_: type["ChildType"], name: str, options: Optional["Qt.FindChildOption"] = None
      71             :     ) -> "ChildType":
      72           1 :         if options is None:
      73           1 :             options = Qt.FindChildOption.FindChildrenRecursively
      74           1 :         return super().findChild(type_, name, options)  # type: ignore [return-value] # explicitly override type hints
      75             : 
      76           1 :     def setup_statusbar(self) -> QStatusBar:
      77             :         """Set up the status bar.
      78             : 
      79             :         The status bar message is set to "Ready" by default.
      80             :         It can be updated with a new message by either from the main window instance:
      81             :             `self.statusbar.showMessage("Ready", timeout=0)`
      82             :         or from outside the main window instance:
      83             :             `QApplication.sendEvent(self, QStatusTipEvent("Ready"))`
      84             :         """
      85           1 :         statusbar = QStatusBar(self)
      86           1 :         statusbar.setFixedHeight(25)
      87           1 :         self.setStatusBar(statusbar)
      88           1 :         statusbar.showMessage("Ready", timeout=0)
      89           1 :         return statusbar
      90             : 
      91           1 :     def setup_dockwidget(self) -> QDockWidget:
      92             :         """Create a configuration dock widget for the main window."""
      93           1 :         dockwidget = QDockWidget()
      94           1 :         dockwidget.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea)
      95           1 :         dockwidget.setTitleBarWidget(QWidget())  # This removes the title bar
      96             : 
      97           1 :         dockwidget.setMinimumWidth(375)
      98           1 :         dockwidget.setVisible(False)
      99           1 :         self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dockwidget)
     100           1 :         return dockwidget
     101             : 
     102           1 :     def setup_stacked_pages(self) -> NamedStackedWidget["BasePage"]:
     103             :         """Set up the different pages for each toolbar option."""
     104           1 :         stacked_pages = NamedStackedWidget["BasePage"]()
     105           1 :         self.setCentralWidget(stacked_pages)
     106             : 
     107           1 :         stacked_pages.addNamedWidget(OneAtomPage(), "OneAtomPage")
     108           1 :         stacked_pages.addNamedWidget(TwoAtomsPage(), "TwoAtomsPage")
     109           1 :         stacked_pages.addNamedWidget(LifetimesPage(), "LifetimesPage")
     110             :         # stacked_pages.addNamedWidget(C6Page(), "C6Page")
     111             : 
     112             :         # stacked_pages.addNamedWidget(SettingsPage(), "SettingsPage")
     113             :         # stacked_pages.addNamedWidget(AboutPage(), "AboutPage")
     114           1 :         return stacked_pages
     115             : 
     116           1 :     def setup_toolbar(self) -> QToolBar:
     117             :         """Set up the toolbar with icon buttons."""
     118           1 :         toolbar = QToolBar("Sidebar")
     119           1 :         toolbar.setMovable(False)
     120           1 :         toolbar.setOrientation(Qt.Orientation.Vertical)
     121           1 :         toolbar.setIconSize(QSize(32, 32))
     122           1 :         toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
     123             : 
     124           1 :         toolbar_group = QActionGroup(self)
     125           1 :         toolbar_group.setExclusive(True)
     126             : 
     127           1 :         for name, page in self.stacked_pages.items():
     128             :             # add a spacer widget
     129           1 :             if name == "about":
     130           0 :                 spacer_widget = QWidget()
     131           0 :                 spacer_widget.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
     132           0 :                 toolbar.addWidget(spacer_widget)
     133             : 
     134           1 :             action = QAction(self)
     135           1 :             action.setObjectName(name)
     136           1 :             action.setText(page.title)
     137           1 :             action.setToolTip(page.tooltip)
     138           1 :             action.setCheckable(True)
     139           1 :             if page.icon_path:
     140           0 :                 action.setIcon(QIcon(str(page.icon_path)))
     141             : 
     142           1 :             toolbar.addAction(action)
     143           1 :             toolbar_group.addAction(action)
     144             : 
     145           1 :             action.triggered.connect(lambda checked, name=name: self.stacked_pages.setCurrentNamedWidget(name))
     146             : 
     147           1 :         default_page = "OneAtomPage"
     148           1 :         self.findChild(QAction, default_page).setChecked(True)
     149           1 :         self.stacked_pages.setCurrentNamedWidget(default_page)
     150             : 
     151           1 :         self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, toolbar)
     152             : 
     153           1 :         return toolbar
     154             : 
     155           1 :     def init_keyboard_shortcuts(self) -> None:
     156             :         """Initialize keyboard shortcuts."""
     157             :         # Add Ctrl+W shortcut to close the window
     158           1 :         close_shortcut = QShortcut(QKeySequence("Ctrl+W"), self)
     159           1 :         close_shortcut.activated.connect(lambda: logger.info("Ctrl+W detected. Shutting down gracefully..."))
     160           1 :         close_shortcut.activated.connect(self.close)
     161             : 
     162           1 :     def closeEvent(self, event: QCloseEvent) -> None:
     163             :         """Make sure to also call Application.quit() when closing the window."""
     164           1 :         logger.debug("Close event triggered.")
     165           1 :         Application.quit()
     166           1 :         event.accept()
     167             : 
     168           1 :     def ask_download_database(self, species: str) -> bool:
     169           0 :         msg_box = QMessageBox()
     170           0 :         msg_box.setWindowTitle("Download missing database tables?")
     171           0 :         msg_box.setText(f"Database tables for {species} not found.")
     172           0 :         msg_box.setInformativeText("Would you like to download the missing database tables?")
     173           0 :         msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
     174             : 
     175           0 :         download = msg_box.exec() == QMessageBox.StandardButton.Yes
     176           0 :         if download:
     177           0 :             self.statusbar.showMessage("Downloading database table ...", timeout=0)
     178             : 
     179           0 :             worker = MultiThreadWorker(lambda: download_databases_mp([species]))
     180           0 :             worker.enable_busy_indicator(self.stacked_pages.currentWidget())
     181             : 
     182           0 :             msg = "Successfully downloaded database table for " + species
     183           0 :             worker.signals.result.connect(lambda _result: self.statusbar.showMessage(msg, timeout=0))
     184           0 :             worker.signals.result.connect(lambda _result: setattr(Database, "_global_database", None))
     185           0 :             worker.signals.result.connect(lambda _result: MultiProcessWorker.terminate_all(create_new_pool=True))
     186           0 :             page = self.stacked_pages.currentWidget()
     187           0 :             if isinstance(page, SimulationPage):
     188           0 :                 ket_config = page.ket_config
     189           0 :                 for i in range(ket_config.n_atoms):
     190           0 :                     worker.signals.result.connect(
     191             :                         lambda _, atom=i: ket_config.signal_species_changed.emit(ket_config.get_species(atom), atom)
     192             :                     )
     193           0 :             worker.start()
     194             : 
     195           0 :         return download

Generated by: LCOV version 1.16