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