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