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 typing as t
6 1 : from typing import TYPE_CHECKING, Callable, Generic, TypeVar
7 :
8 1 : import numpy as np
9 1 : from PySide6.QtWidgets import (
10 : QCheckBox,
11 : QLabel,
12 : QSpacerItem,
13 : )
14 :
15 1 : from pairinteraction_gui.qobjects.spin_boxes import DoubleSpinBox, HalfIntSpinBox, IntSpinBox
16 1 : from pairinteraction_gui.qobjects.widget import WidgetH
17 :
18 : if TYPE_CHECKING:
19 : from PySide6.QtWidgets import (
20 : QWidget,
21 : )
22 :
23 : P = TypeVar("P")
24 :
25 1 : ValueType = TypeVar("ValueType", int, float, complex)
26 :
27 :
28 1 : @t.runtime_checkable
29 1 : class NotSet(t.Protocol):
30 : """Singleton for a not set value and type at the same time.
31 :
32 : See Also:
33 : https://stackoverflow.com/questions/77571796/how-to-create-singleton-object-which-could-be-used-both-as-type-and-value-simi
34 :
35 : """
36 :
37 : @staticmethod
38 : def __not_set() -> None: ...
39 :
40 :
41 1 : class Item(WidgetH):
42 1 : margin = (20, 0, 20, 0)
43 1 : spacing = 10
44 :
45 1 : def __init__(
46 : self,
47 : parent: QWidget,
48 : label: str,
49 : checked: bool = True,
50 : ) -> None:
51 1 : self.checkbox = QCheckBox()
52 1 : self.checkbox.setChecked(checked)
53 :
54 1 : self.label = label
55 1 : self._label = QLabel(label)
56 1 : self._label.setMinimumWidth(25)
57 :
58 1 : super().__init__(parent)
59 :
60 1 : def setupWidget(self) -> None:
61 1 : self.layout().addWidget(self.checkbox)
62 1 : self.layout().addWidget(self._label)
63 :
64 1 : def postSetupWidget(self) -> None:
65 1 : self.layout().addStretch(1)
66 :
67 1 : def isChecked(self) -> bool:
68 : """Return the state of the checkbox."""
69 1 : return self.checkbox.isChecked()
70 :
71 1 : def setChecked(self, checked: bool) -> None:
72 : """Set the state of the checkbox."""
73 1 : self.checkbox.setChecked(checked)
74 :
75 1 : def connectAll(self, func: Callable[[], None]) -> None:
76 : """Connect the function to the spinbox.valueChanged signal."""
77 0 : self.checkbox.stateChanged.connect(lambda state: func())
78 :
79 :
80 1 : class _QnItem(WidgetH, Generic[ValueType]):
81 : """Widget for displaying a range with min and max spinboxes."""
82 :
83 1 : margin = (20, 0, 20, 0)
84 1 : spacing = 10
85 1 : _spinbox_class: type[IntSpinBox | HalfIntSpinBox | DoubleSpinBox]
86 :
87 1 : def __init__(
88 : self,
89 : parent: QWidget,
90 : label: str,
91 : vmin: ValueType = 0,
92 : vmax: ValueType = 999,
93 : vdefault: ValueType = 0,
94 : vstep: ValueType | None = None,
95 : unit: str = "",
96 : tooltip: str | None = None,
97 : checkable: bool = True,
98 : checked: bool = True,
99 : ) -> None:
100 1 : tooltip = tooltip if tooltip is not None else f"{label} in {unit}"
101 :
102 1 : self.checkbox: QCheckBox | QSpacerItem
103 1 : if checkable:
104 1 : self.checkbox = QCheckBox()
105 1 : self.checkbox.setChecked(checked)
106 1 : self.checkbox.stateChanged.connect(self._on_checkbox_changed)
107 : else:
108 1 : self.checkbox = QSpacerItem(25, 0)
109 :
110 1 : self.label = QLabel(label)
111 1 : self.label.setMinimumWidth(25)
112 :
113 1 : self.spinbox = self._spinbox_class(parent, vmin, vmax, vdefault, vstep, tooltip=tooltip) # type: ignore [arg-type]
114 1 : self.spinbox.setObjectName(f"{label.lower()}")
115 1 : self.spinbox.setMinimumWidth(100)
116 :
117 1 : self.unit = QLabel(unit)
118 :
119 1 : super().__init__(parent)
120 :
121 1 : def setupWidget(self) -> None:
122 1 : if isinstance(self.checkbox, QCheckBox):
123 1 : self.layout().addWidget(self.checkbox)
124 1 : elif isinstance(self.checkbox, QSpacerItem):
125 1 : self.layout().addItem(self.checkbox)
126 1 : self.layout().addWidget(self.label)
127 1 : self.layout().addWidget(self.spinbox)
128 1 : self.layout().addWidget(self.unit)
129 :
130 1 : def postSetupWidget(self) -> None:
131 1 : self.layout().addStretch(1)
132 1 : self._on_checkbox_changed(self.isChecked())
133 :
134 1 : def connectAll(self, func: Callable[[], None]) -> None:
135 : """Connect the function to the spinbox.valueChanged signal."""
136 1 : if isinstance(self.checkbox, QCheckBox):
137 1 : self.checkbox.stateChanged.connect(lambda state: func())
138 1 : self.spinbox.valueChanged.connect(lambda value: func())
139 :
140 1 : def _on_checkbox_changed(self, state: bool) -> None:
141 : """Update the enabled state of widget when checkbox changes."""
142 1 : self.spinbox.setEnabled(state)
143 :
144 1 : def isChecked(self) -> bool:
145 : """Return the state of the checkbox."""
146 1 : if isinstance(self.checkbox, QSpacerItem):
147 1 : return True
148 1 : return self.checkbox.isChecked()
149 :
150 1 : def setChecked(self, checked: bool) -> None:
151 : """Set the state of the checkbox."""
152 1 : if isinstance(self.checkbox, QSpacerItem):
153 0 : if checked:
154 0 : return
155 0 : raise ValueError("Cannot uncheck a non-checkable item.")
156 1 : self.checkbox.setChecked(checked)
157 :
158 1 : def value(
159 : self,
160 : default: P | NotSet = NotSet,
161 : ) -> ValueType | P:
162 : """Return the value of the spinbox."""
163 1 : if not self.isChecked():
164 1 : if isinstance(default, NotSet):
165 0 : raise ValueError("Checkbox is not checked and no default value is provided.")
166 1 : return default
167 1 : return self.spinbox.value() # type: ignore [return-value]
168 :
169 1 : def setValue(self, value: ValueType) -> None:
170 : """Set the value of the spinbox and set the checkbox state to checked if applicable."""
171 1 : if isinstance(self.checkbox, QCheckBox):
172 1 : self.checkbox.setChecked(True)
173 1 : self.spinbox.setValue(value) # type: ignore [arg-type]
174 :
175 :
176 1 : class QnItemInt(_QnItem[int]):
177 1 : _spinbox_class = IntSpinBox
178 :
179 :
180 1 : class QnItemHalfInt(_QnItem[float]):
181 1 : _spinbox_class = HalfIntSpinBox
182 :
183 :
184 1 : class QnItemDouble(_QnItem[float]):
185 1 : _spinbox_class = DoubleSpinBox
186 :
187 :
188 1 : class RangeItem(WidgetH):
189 : """Widget for displaying a range with min and max spinboxes."""
190 :
191 1 : margin = (20, 0, 20, 0)
192 1 : spacing = 10
193 :
194 1 : def __init__(
195 : self,
196 : parent: QWidget,
197 : label: str,
198 : vdefaults: tuple[float, float] = (0, 0),
199 : vrange: tuple[float, float] = (-np.inf, np.inf),
200 : unit: str = "",
201 : tooltip_label: str | None = None,
202 : checkable: bool = True,
203 : checked: bool = True,
204 : ) -> None:
205 1 : tooltip_label = tooltip_label if tooltip_label is not None else label
206 :
207 1 : self.checkbox: QCheckBox | QSpacerItem
208 1 : if checkable:
209 1 : self.checkbox = QCheckBox()
210 1 : self.checkbox.setChecked(checked)
211 1 : self.checkbox.stateChanged.connect(self._on_checkbox_changed)
212 : else:
213 0 : self.checkbox = QSpacerItem(25, 0)
214 :
215 1 : self.label = QLabel(label)
216 1 : self.label.setMinimumWidth(25)
217 :
218 1 : self.min_spinbox = DoubleSpinBox(parent, *vrange, vdefaults[0], tooltip=f"Minimum {tooltip_label} in {unit}")
219 1 : self.max_spinbox = DoubleSpinBox(parent, *vrange, vdefaults[1], tooltip=f"Maximum {tooltip_label} in {unit}")
220 1 : self.min_spinbox.setObjectName(f"{label.lower()}_min")
221 1 : self.max_spinbox.setObjectName(f"{label.lower()}_max")
222 :
223 1 : self.unit = QLabel(unit)
224 :
225 1 : super().__init__(parent)
226 :
227 1 : def setupWidget(self) -> None:
228 1 : if isinstance(self.checkbox, QCheckBox):
229 1 : self.layout().addWidget(self.checkbox)
230 0 : elif isinstance(self.checkbox, QSpacerItem):
231 0 : self.layout().addItem(self.checkbox)
232 1 : self.layout().addWidget(self.label)
233 :
234 1 : self.layout().addWidget(self.min_spinbox)
235 1 : self.layout().addWidget(QLabel("to"))
236 1 : self.layout().addWidget(self.max_spinbox)
237 :
238 1 : self.layout().addWidget(self.unit)
239 :
240 1 : def postSetupWidget(self) -> None:
241 1 : self.layout().addStretch(1)
242 1 : self._on_checkbox_changed(self.isChecked())
243 :
244 1 : @property
245 1 : def spinboxes(self) -> tuple[DoubleSpinBox, DoubleSpinBox]:
246 : """Return the min and max spinboxes."""
247 1 : return (self.min_spinbox, self.max_spinbox)
248 :
249 1 : def connectAll(self, func: Callable[[], None]) -> None:
250 : """Connect the function to the spinbox.valueChanged signal."""
251 0 : if isinstance(self.checkbox, QCheckBox):
252 0 : self.checkbox.stateChanged.connect(lambda state: func())
253 0 : for spinbox in self.spinboxes:
254 0 : spinbox.valueChanged.connect(lambda value: func())
255 :
256 1 : def _on_checkbox_changed(self, state: bool) -> None:
257 : """Update the enabled state of widgets when checkbox changes."""
258 1 : for spinbox in self.spinboxes:
259 1 : spinbox.setEnabled(state)
260 :
261 1 : def isChecked(self) -> bool:
262 : """Return the state of the checkbox."""
263 1 : if isinstance(self.checkbox, QSpacerItem):
264 0 : return True
265 1 : return self.checkbox.isChecked()
266 :
267 1 : def setChecked(self, checked: bool) -> None:
268 : """Set the state of the checkbox."""
269 1 : if isinstance(self.checkbox, QSpacerItem):
270 0 : if checked:
271 0 : return
272 0 : raise ValueError("Cannot uncheck a non-checkable item.")
273 1 : self.checkbox.setChecked(checked)
274 :
275 1 : def values(self, default: tuple[float, float] | P | NotSet = NotSet) -> tuple[float, float] | P:
276 : """Return the values of the min and max spinboxes."""
277 1 : if not self.isChecked():
278 0 : if isinstance(default, NotSet):
279 0 : raise ValueError("Checkbox is not checked and no default value is provided.")
280 0 : return default
281 1 : return (self.min_spinbox.value(), self.max_spinbox.value())
282 :
283 1 : def setValues(self, vmin: float, vmax: float) -> None:
284 : """Set the values of the min and max spinboxes and set the checkbox state to checked if applicable."""
285 1 : if isinstance(self.checkbox, QCheckBox):
286 1 : self.checkbox.setChecked(True)
287 1 : self.min_spinbox.setValue(vmin)
288 1 : self.max_spinbox.setValue(vmax)
|