Line data Source code
1 : # SPDX-FileCopyrightText: 2025 Pairinteraction Developers
2 : # SPDX-License-Identifier: LGPL-3.0-or-later
3 :
4 0 : import logging
5 0 : from typing import TYPE_CHECKING, Optional, TypeVar
6 :
7 0 : from PySide6.QtCore import QObject, QSize, Qt
8 0 : from PySide6.QtGui import QAction, QActionGroup, QCloseEvent, QIcon, QKeySequence, QShortcut
9 0 : from PySide6.QtWidgets import (
10 : QDockWidget,
11 : QMainWindow,
12 : QMessageBox,
13 : QSizePolicy,
14 : QStatusBar,
15 : QToolBar,
16 : QWidget,
17 : )
18 :
19 0 : from pairinteraction_gui.app import Application
20 0 : from pairinteraction_gui.page import (
21 : LifetimesPage,
22 : OneAtomPage,
23 : TwoAtomsPage,
24 : )
25 0 : from pairinteraction_gui.qobjects import NamedStackedWidget
26 :
27 : if TYPE_CHECKING:
28 : from pairinteraction_gui.page import BasePage
29 :
30 : ChildType = TypeVar("ChildType", bound=QObject)
31 :
32 0 : logger = logging.getLogger(__name__)
33 :
34 :
35 0 : class MainWindow(QMainWindow):
36 : """Main window for the PairInteraction GUI application."""
37 :
38 0 : def __init__(self) -> None:
39 : """Initialize the main window."""
40 0 : super().__init__()
41 :
42 0 : self.setWindowTitle("PairInteraction")
43 0 : self.resize(1200, 800)
44 :
45 0 : self.apply_modern_style()
46 :
47 0 : self.statusbar = self.setup_statusbar()
48 0 : self.dockwidget = self.setup_dockwidget()
49 :
50 0 : self.stacked_pages = self.setup_stacked_pages()
51 0 : self.toolbar = self.setup_toolbar()
52 :
53 0 : self.init_keyboard_shortcuts()
54 0 : self.connect_signals()
55 :
56 0 : def connect_signals(self) -> None:
57 : """Connect signals to slots."""
58 0 : self.signals = Application.instance().signals
59 :
60 0 : self.signals.ask_download_database.connect(self.ask_download_database)
61 :
62 0 : def findChild( # type: ignore [override] # explicitly override type hints
63 : self, type_: type["ChildType"], name: str, options: Optional["Qt.FindChildOption"] = None
64 : ) -> "ChildType":
65 0 : if options is None:
66 0 : options = Qt.FindChildOption.FindChildrenRecursively
67 0 : return super().findChild(type_, name, options) # type: ignore [return-value] # explicitly override type hints
68 :
69 0 : def apply_modern_style(self) -> None:
70 : """Apply modern styling to the application."""
71 0 : self.setStyleSheet("""
72 : QMainWindow {
73 : background-color: #ffffff;
74 : }
75 : QStatusBar {
76 : background-color: #343a40;
77 : color: #ffffff;
78 : }
79 : """)
80 :
81 0 : def setup_statusbar(self) -> QStatusBar:
82 : """Set up the status bar.
83 :
84 : The status bar message is set to "Ready" by default.
85 : It can be updated with a new message by either from the main window instance:
86 : `self.statusbar.showMessage("Ready", timeout=0)`
87 : or from outside the main window instance:
88 : `QApplication.sendEvent(self, QStatusTipEvent("Ready"))`
89 : """
90 0 : statusbar = QStatusBar(self)
91 0 : statusbar.setFixedHeight(25)
92 0 : self.setStatusBar(statusbar)
93 0 : statusbar.showMessage("Ready", timeout=0)
94 0 : return statusbar
95 :
96 0 : def setup_dockwidget(self) -> QDockWidget:
97 : """Create a configuration dock widget for the main window."""
98 0 : dockwidget = QDockWidget()
99 0 : dockwidget.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea)
100 0 : dockwidget.setTitleBarWidget(QWidget()) # This removes the title bar
101 :
102 0 : dockwidget.setMinimumWidth(375)
103 0 : dockwidget.setStyleSheet("""
104 : QToolBox {
105 : background-color: white;
106 : border: 1px solid #ffffff;
107 : border-radius: 5px;
108 : }
109 : QToolBox::tab {
110 : background-color: #343a40;
111 : border: 1px solid #ffffff;
112 : border-radius: 4px;
113 : color: #ffffff;
114 : font-weight: bold;
115 : font-size: 15px;
116 : }
117 : QToolBox::tab:selected {
118 : background-color: #007bff;
119 : }
120 : QToolBox::tab:hover:!selected {
121 : background-color: #495057;
122 : }
123 : QLabel {
124 : color: black;
125 : font-size: 14px;
126 : }
127 : """)
128 0 : dockwidget.setVisible(False)
129 0 : self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dockwidget)
130 0 : return dockwidget
131 :
132 0 : def setup_stacked_pages(self) -> NamedStackedWidget["BasePage"]:
133 : """Set up the different pages for each toolbar option."""
134 0 : stacked_pages = NamedStackedWidget["BasePage"]()
135 0 : self.setCentralWidget(stacked_pages)
136 :
137 0 : stacked_pages.addNamedWidget(OneAtomPage(), "system_atom")
138 0 : stacked_pages.addNamedWidget(TwoAtomsPage(), "system_pair")
139 0 : stacked_pages.addNamedWidget(LifetimesPage(), "lifetimes")
140 : # stacked_pages.addNamedWidget(C6Page(), "c6")
141 :
142 : # stacked_pages.addNamedWidget(SettingsPage(), "settings")
143 : # stacked_pages.addNamedWidget(AboutPage(), "about")
144 0 : return stacked_pages
145 :
146 0 : def setup_toolbar(self) -> QToolBar:
147 : """Set up the toolbar with icon buttons."""
148 0 : toolbar = QToolBar("Sidebar")
149 0 : toolbar.setMovable(False)
150 0 : toolbar.setOrientation(Qt.Orientation.Vertical)
151 0 : toolbar.setIconSize(QSize(32, 32))
152 0 : toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
153 0 : toolbar.setStyleSheet("""
154 : QToolBar {
155 : background-color: #343a40;
156 : border: none;
157 : spacing: 15px;
158 : padding: 10px 5px;
159 : }
160 : QToolButton {
161 : border: none;
162 : padding: 8px;
163 : margin: 5px;
164 : color: #ffffff;
165 : font-weight: bold;
166 : font-size: 15px;
167 : }
168 : QToolButton:hover {
169 : background-color: #495057;
170 : }
171 : QToolButton:pressed {
172 : background-color: #000000;
173 : }
174 : QToolButton:checked {
175 : background-color: #007bff;
176 : }
177 : """)
178 :
179 0 : toolbar_group = QActionGroup(self)
180 0 : toolbar_group.setExclusive(True)
181 :
182 0 : for name, page in self.stacked_pages.items():
183 : # add a spacer widget
184 0 : if name == "about":
185 0 : spacer_widget = QWidget()
186 0 : spacer_widget.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
187 0 : toolbar.addWidget(spacer_widget)
188 :
189 0 : action = QAction(self)
190 0 : action.setObjectName(name)
191 0 : action.setText(page.title)
192 0 : action.setToolTip(page.tooltip)
193 0 : action.setCheckable(True)
194 0 : if page.icon_path:
195 0 : action.setIcon(QIcon(str(page.icon_path)))
196 :
197 0 : toolbar.addAction(action)
198 0 : toolbar_group.addAction(action)
199 :
200 0 : action.triggered.connect(lambda checked, name=name: self.stacked_pages.setCurrentNamedWidget(name))
201 :
202 0 : default_page = "system_atom"
203 0 : self.findChild(QAction, default_page).setChecked(True)
204 0 : self.stacked_pages.setCurrentNamedWidget(default_page)
205 :
206 0 : self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, toolbar)
207 :
208 0 : return toolbar
209 :
210 0 : def init_keyboard_shortcuts(self) -> None:
211 : """Initialize keyboard shortcuts."""
212 : # Add Ctrl+W shortcut to close the window
213 0 : close_shortcut = QShortcut(QKeySequence("Ctrl+W"), self)
214 0 : close_shortcut.activated.connect(lambda: logger.info("Ctrl+W detected. Shutting down gracefully..."))
215 0 : close_shortcut.activated.connect(self.close)
216 :
217 0 : def closeEvent(self, event: QCloseEvent) -> None:
218 : """Make sure to also call Application.quit() when closing the window."""
219 0 : logger.debug("Close event triggered.")
220 0 : Application.quit()
221 0 : event.accept()
222 :
223 0 : def ask_download_database(self, species: str) -> bool:
224 0 : msg_box = QMessageBox()
225 0 : msg_box.setWindowTitle("Download missing database tables?")
226 0 : msg_box.setText(f"Database tables for {species} not found.")
227 0 : msg_box.setInformativeText("Would you like to download the missing database tables?")
228 0 : msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
229 :
230 0 : download = msg_box.exec() == QMessageBox.StandardButton.Yes
231 0 : if download:
232 0 : from pairinteraction._wrapped import Database
233 0 : from pairinteraction.cli import download_databases
234 :
235 0 : Database._global_database = None
236 0 : download_databases([species])
237 0 : return download
|