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

Generated by: LCOV version 1.16