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
|