sciwidgets 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sciwidgets/__init__.py +8 -0
- sciwidgets/dialogs/__init__.py +31 -0
- sciwidgets/dialogs/__main__.py +128 -0
- sciwidgets/dialogs/message_dialogs.py +553 -0
- sciwidgets/formatting.py +161 -0
- sciwidgets/labels/__init__.py +3 -0
- sciwidgets/labels/__main__.py +124 -0
- sciwidgets/labels/math_label.py +505 -0
- sciwidgets/params/__init__.py +18 -0
- sciwidgets/params/__main__.py +81 -0
- sciwidgets/params/base.py +220 -0
- sciwidgets/params/combo_box.py +106 -0
- sciwidgets/params/line_edit.py +132 -0
- sciwidgets/params/numeric_line_edit.py +680 -0
- sciwidgets/params/sliders/__init__.py +36 -0
- sciwidgets/params/sliders/__main__.py +215 -0
- sciwidgets/params/sliders/_defaults.py +36 -0
- sciwidgets/params/sliders/_dialogs.py +174 -0
- sciwidgets/params/sliders/_right_click_overlay.py +115 -0
- sciwidgets/params/sliders/base.py +592 -0
- sciwidgets/params/sliders/multi.py +660 -0
- sciwidgets/params/sliders/range.py +1841 -0
- sciwidgets/params/sliders/single.py +2042 -0
- sciwidgets/params/toggle.py +116 -0
- sciwidgets/style.py +175 -0
- sciwidgets-0.1.2.dist-info/METADATA +173 -0
- sciwidgets-0.1.2.dist-info/RECORD +30 -0
- sciwidgets-0.1.2.dist-info/WHEEL +5 -0
- sciwidgets-0.1.2.dist-info/licenses/LICENSE +21 -0
- sciwidgets-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Demo for the params subpackage: ``python -m sciwidgets.params``."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from PySide6.QtWidgets import (
|
|
6
|
+
QApplication, QFormLayout, QLabel, QVBoxLayout, QWidget,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from sciwidgets.params.combo_box import ParameterComboBox
|
|
10
|
+
from sciwidgets.params.line_edit import ParameterLineEdit
|
|
11
|
+
from sciwidgets.params.numeric_line_edit import NumericLineEdit
|
|
12
|
+
from sciwidgets.params.toggle import ParameterToggle
|
|
13
|
+
from sciwidgets.style import apply_sci_style
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _demo_main() -> int:
|
|
17
|
+
"""Run the params demo window.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
int: Qt application exit code.
|
|
21
|
+
"""
|
|
22
|
+
app = QApplication(sys.argv)
|
|
23
|
+
apply_sci_style(app)
|
|
24
|
+
|
|
25
|
+
win = QWidget()
|
|
26
|
+
win.setWindowTitle("Params demo")
|
|
27
|
+
layout = QVBoxLayout(win)
|
|
28
|
+
|
|
29
|
+
# --- NumericLineEdit ------------------------------------------------
|
|
30
|
+
f_edit = NumericLineEdit(
|
|
31
|
+
init_val=1.0, val_fmt="{:0.3g}", min_limit=-100.0,
|
|
32
|
+
width_chars=11, max_length=10,
|
|
33
|
+
)
|
|
34
|
+
i_edit = NumericLineEdit(
|
|
35
|
+
init_val=1, value_is_int=True, min_limit=-100, max_limit=100,
|
|
36
|
+
width_chars=10, max_length=9,
|
|
37
|
+
)
|
|
38
|
+
committed = QLabel("")
|
|
39
|
+
f_edit.valueChanged.connect(lambda v: committed.setText(f"float: {v}"))
|
|
40
|
+
i_edit.valueChanged.connect(lambda v: committed.setText(f"int: {v}"))
|
|
41
|
+
|
|
42
|
+
form = QFormLayout()
|
|
43
|
+
form.addRow("Integer [-100, 100]:", i_edit)
|
|
44
|
+
form.addRow("Float [-100, ∞):", f_edit)
|
|
45
|
+
form.addRow("Last committed:", committed)
|
|
46
|
+
layout.addLayout(form)
|
|
47
|
+
# --- Simple parameter widgets ----------------------------------------
|
|
48
|
+
t_edit = ParameterLineEdit(
|
|
49
|
+
label="t", init_val=5, val_fmt="{:0.1f}", unit_label=r"\mathrm{s}",
|
|
50
|
+
min_limit=0.0, min_limit_inclusive=False, parent=win,
|
|
51
|
+
)
|
|
52
|
+
n_edit = ParameterLineEdit(label=r"A_\alpha", init_val=5, value_is_int=True, parent=win)
|
|
53
|
+
unit = ParameterComboBox(
|
|
54
|
+
label="t", items=["T₀", "d", "h", "m", "S", "ms", "µs", "ns"], parent=win,
|
|
55
|
+
)
|
|
56
|
+
unit.set_value("ns")
|
|
57
|
+
toggle = ParameterToggle(
|
|
58
|
+
text="Normalize to max",
|
|
59
|
+
label_on=r"F = I_\mathrm{res} / \max(I_\mathrm{res}) \times I_\mathrm{scale}",
|
|
60
|
+
label_off=r"F = I_\mathrm{res} \times I_\mathrm{scale}",
|
|
61
|
+
parent=win,
|
|
62
|
+
)
|
|
63
|
+
plain = ParameterToggle(text="Plain checkbox (no labels)", checked=False, parent=win)
|
|
64
|
+
|
|
65
|
+
# Align the name column the way a panel would
|
|
66
|
+
widgets = [t_edit, n_edit, unit, toggle, plain]
|
|
67
|
+
name_w = max(w.get_name_width() for w in widgets)
|
|
68
|
+
for w in widgets:
|
|
69
|
+
w.set_name_width(name_w)
|
|
70
|
+
w.valueChanged.connect(lambda v, w=w: print(type(w).__name__, v))
|
|
71
|
+
layout.addWidget(w)
|
|
72
|
+
|
|
73
|
+
layout.addStretch(1)
|
|
74
|
+
|
|
75
|
+
win.resize(520, 420)
|
|
76
|
+
win.show()
|
|
77
|
+
return app.exec()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
raise SystemExit(_demo_main())
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Common base class for parameter widgets with value and config signals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Signal
|
|
8
|
+
from PySide6.QtWidgets import QWidget
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ParameterWidgetBase(QWidget, Generic[T]):
|
|
14
|
+
"""Provide a common interface for parameter widgets.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
valueChanged: Emitted when the user changes the value.
|
|
18
|
+
defaultChanged: Emitted when the user changes the default value.
|
|
19
|
+
configChanged: Emitted when the user changes the configuration.
|
|
20
|
+
valueWidthChanged: Emitted when the value display width changes.
|
|
21
|
+
nameWidthChanged: Emitted when the name display width changes.
|
|
22
|
+
|
|
23
|
+
Note:
|
|
24
|
+
``valueChanged``, ``defaultChanged`` and ``configChanged`` are for
|
|
25
|
+
user actions only; programmatic setters such as ``set_value`` and
|
|
26
|
+
``set_config`` do not emit them.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
valueChanged = Signal(object)
|
|
30
|
+
defaultChanged = Signal(object)
|
|
31
|
+
configChanged = Signal(object)
|
|
32
|
+
valueWidthChanged = Signal(int)
|
|
33
|
+
nameWidthChanged = Signal(int)
|
|
34
|
+
|
|
35
|
+
def __init__(self, parent: QWidget | None = None) -> None:
|
|
36
|
+
"""Initialize the base parameter widget.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
parent: Optional parent widget.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
super().__init__(parent)
|
|
43
|
+
self._name_width: int = 0 # requested via set_name_width
|
|
44
|
+
self._value_width: int = 0 # requested via set_value_width
|
|
45
|
+
self._measured_name_width: int = 0 # set by subclasses
|
|
46
|
+
self._measured_value_width: int = 0 # set by subclasses
|
|
47
|
+
|
|
48
|
+
def _update_layout(self) -> None:
|
|
49
|
+
"""Update the widget layout to reflect the current internal state.
|
|
50
|
+
|
|
51
|
+
Subclasses should override this method when name or value width changes
|
|
52
|
+
require a visual layout update.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def get_value(self) -> T:
|
|
58
|
+
"""Return the current widget value.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The current typed value stored by the widget.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
NotImplementedError: If the subclass does not implement value
|
|
65
|
+
retrieval.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
|
|
70
|
+
def set_value(self, value: T) -> None:
|
|
71
|
+
"""Validate and apply a new widget value.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
value: New value to store in the widget.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
validated = self._validate_value(value)
|
|
78
|
+
self._apply_value(validated)
|
|
79
|
+
|
|
80
|
+
def _validate_value(self, value: T) -> T:
|
|
81
|
+
"""Validate a candidate value before applying it.
|
|
82
|
+
|
|
83
|
+
The default implementation returns the value unchanged. Subclasses can
|
|
84
|
+
override this method to enforce type, range, or domain-specific checks.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
value: Candidate value to validate.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Validated value, possibly normalized.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
def _apply_value(self, value: T) -> None:
|
|
96
|
+
"""Apply a validated value to the widget state.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
value: Previously validated value to store.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
NotImplementedError: If the subclass does not implement value
|
|
103
|
+
application.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
|
|
108
|
+
def get_config(self) -> dict[str, Any]:
|
|
109
|
+
"""Return the current configuration mapping.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Configuration dictionary for the widget. The base implementation
|
|
113
|
+
returns an empty mapping.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
return {}
|
|
117
|
+
|
|
118
|
+
def set_config(self, config: dict[str, Any]) -> None:
|
|
119
|
+
"""Validate and apply configuration values.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
config: Partial or complete configuration mapping to apply.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
validated = self._validate_config(config)
|
|
126
|
+
self._apply_config(validated)
|
|
127
|
+
|
|
128
|
+
def _validate_config(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
129
|
+
"""Validate and normalize a configuration mapping.
|
|
130
|
+
|
|
131
|
+
The base implementation requires ``config`` to be a dictionary and
|
|
132
|
+
merges it with the current configuration, preserving existing values for
|
|
133
|
+
missing keys.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
config: Candidate configuration mapping.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Normalized configuration dictionary.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
TypeError: If ``config`` is not a dictionary.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
if not isinstance(config, dict):
|
|
146
|
+
raise TypeError(f"config must be dict, got {type(config).__name__}")
|
|
147
|
+
old_config = self.get_config()
|
|
148
|
+
new_config = {}
|
|
149
|
+
for k, v in old_config.items():
|
|
150
|
+
if k not in config:
|
|
151
|
+
new_config[k] = v
|
|
152
|
+
else:
|
|
153
|
+
new_config[k] = config[k]
|
|
154
|
+
return new_config
|
|
155
|
+
|
|
156
|
+
def _apply_config(self, config: dict[str, Any]) -> None:
|
|
157
|
+
"""Apply a validated configuration mapping.
|
|
158
|
+
|
|
159
|
+
Subclasses can override this method to store configuration state and
|
|
160
|
+
update the widget accordingly.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
config: Previously validated configuration mapping.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
_ = config
|
|
167
|
+
|
|
168
|
+
def get_name_width(self) -> int:
|
|
169
|
+
"""Return the measured width of the name label.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Width in pixels.
|
|
173
|
+
"""
|
|
174
|
+
return self._measured_name_width
|
|
175
|
+
|
|
176
|
+
def get_value_width(self) -> int:
|
|
177
|
+
"""Return the measured width of the value area.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Width in pixels.
|
|
181
|
+
"""
|
|
182
|
+
return self._measured_value_width
|
|
183
|
+
|
|
184
|
+
def _effective_name_width(self) -> int:
|
|
185
|
+
"""Return the width the layout should reserve for the name label."""
|
|
186
|
+
return max(self._name_width, self._measured_name_width)
|
|
187
|
+
|
|
188
|
+
def _effective_value_width(self) -> int:
|
|
189
|
+
"""Return the width the layout should reserve for the value area."""
|
|
190
|
+
return max(self._value_width, self._measured_value_width)
|
|
191
|
+
|
|
192
|
+
def set_name_width(self, width: int) -> None:
|
|
193
|
+
"""Set a requested minimum width for the name label.
|
|
194
|
+
|
|
195
|
+
Used to align several widgets in a panel. The layout reserves the
|
|
196
|
+
larger of this value and the label's own measured width, so a smaller
|
|
197
|
+
request never cuts the label. ``get_name_width`` still returns the
|
|
198
|
+
measured width.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
width: Requested minimum width in pixels.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
self._name_width = int(width)
|
|
205
|
+
self._update_layout()
|
|
206
|
+
|
|
207
|
+
def set_value_width(self, width: int) -> None:
|
|
208
|
+
"""Set a requested minimum width for the value area.
|
|
209
|
+
|
|
210
|
+
Used to align several widgets in a panel. The layout reserves the
|
|
211
|
+
larger of this value and the value area's own measured width, so a
|
|
212
|
+
smaller request never cuts the value. ``get_value_width`` still
|
|
213
|
+
returns the measured width.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
width: Requested minimum width in pixels.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
self._value_width = int(width)
|
|
220
|
+
self._update_layout()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Discrete parameter widget backed by a combo box."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt
|
|
8
|
+
from PySide6.QtWidgets import QComboBox, QGridLayout, QWidget
|
|
9
|
+
|
|
10
|
+
from ..labels import MathLabel
|
|
11
|
+
from .base import ParameterWidgetBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ParameterComboBox(ParameterWidgetBase[str]):
|
|
15
|
+
"""Parameter widget that exposes a discrete choice through a combo box."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
label: str,
|
|
20
|
+
items: List[str],
|
|
21
|
+
parent: QWidget | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Initialize the parameter combo-box widget.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
label: Parameter name displayed to the left of the combo box.
|
|
27
|
+
items: Ordered list of selectable string values.
|
|
28
|
+
parent: Optional parent widget.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
super().__init__(parent)
|
|
32
|
+
|
|
33
|
+
self._label = MathLabel(f"{label}=", alignment=Qt.AlignRight | Qt.AlignVCenter)
|
|
34
|
+
self._measured_name_width = self._label.sizeHint().width()
|
|
35
|
+
combo = QComboBox(self)
|
|
36
|
+
for v in items:
|
|
37
|
+
combo.addItem(v)
|
|
38
|
+
combo.activated.connect(self._on_activated)
|
|
39
|
+
self._combo = combo
|
|
40
|
+
self._last_value = combo.currentText()
|
|
41
|
+
|
|
42
|
+
self._make_layout()
|
|
43
|
+
|
|
44
|
+
def _make_layout(self) -> None:
|
|
45
|
+
"""Create and assign the grid layout used by this widget."""
|
|
46
|
+
|
|
47
|
+
gl = QGridLayout(self)
|
|
48
|
+
gl.setSpacing(0)
|
|
49
|
+
gl.addWidget(self._label, 0, 0)
|
|
50
|
+
gl.addWidget(self._combo, 0, 1)
|
|
51
|
+
gl.setColumnStretch(0, 0)
|
|
52
|
+
gl.setColumnStretch(1, 0)
|
|
53
|
+
gl.setColumnStretch(2, 1)
|
|
54
|
+
self._gl = gl
|
|
55
|
+
self._update_layout()
|
|
56
|
+
|
|
57
|
+
def _update_layout(self) -> None:
|
|
58
|
+
"""Reserve the effective name width for the label column."""
|
|
59
|
+
|
|
60
|
+
self._gl.setColumnMinimumWidth(0, self._effective_name_width())
|
|
61
|
+
|
|
62
|
+
def _on_activated(self, _index: int) -> None:
|
|
63
|
+
"""Emit the value after the user picks a different item."""
|
|
64
|
+
value = self.get_value()
|
|
65
|
+
if value != self._last_value:
|
|
66
|
+
self._last_value = value
|
|
67
|
+
self.valueChanged.emit(value)
|
|
68
|
+
|
|
69
|
+
def get_value(self) -> str:
|
|
70
|
+
"""Return the currently selected combo-box text.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Selected string value.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
return self._combo.currentText()
|
|
77
|
+
|
|
78
|
+
def _validate_value(self, value: str) -> str:
|
|
79
|
+
"""Validate a value before applying it to the combo box.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
value: Proposed combo-box text.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Validated combo-box text.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
TypeError: If ``value`` is not a string.
|
|
89
|
+
ValueError: If no combo-box item matches ``value``.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
if not isinstance(value, str):
|
|
93
|
+
raise TypeError("'value' must be a string")
|
|
94
|
+
if self._combo.findText(value) < 0:
|
|
95
|
+
raise ValueError(f"No combo item found for text={value!r}")
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
def _apply_value(self, value: str) -> None:
|
|
99
|
+
"""Apply a validated value by selecting the matching combo item.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
value: Combo-box text to select.
|
|
103
|
+
"""
|
|
104
|
+
self._combo.setCurrentIndex(self._combo.findText(value))
|
|
105
|
+
self._last_value = value
|
|
106
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Text and scalar parameter widget backed by a line edit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numbers
|
|
6
|
+
from typing import Optional, Union
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import Qt
|
|
9
|
+
from PySide6.QtWidgets import QWidget, QGridLayout
|
|
10
|
+
|
|
11
|
+
from ..labels import MathLabel
|
|
12
|
+
from .base import ParameterWidgetBase
|
|
13
|
+
from .numeric_line_edit import NumericLineEdit
|
|
14
|
+
|
|
15
|
+
Num = Union[int, float]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ParameterLineEdit(ParameterWidgetBase[Num]):
|
|
19
|
+
"""Parameter widget that exposes a numeric value through a line edit."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
label: str,
|
|
24
|
+
init_val: Num,
|
|
25
|
+
val_fmt: str = "{:0.6g}",
|
|
26
|
+
value_is_int: bool = False,
|
|
27
|
+
unit_label: str = "",
|
|
28
|
+
min_limit: Num | None = None,
|
|
29
|
+
max_limit: Num | None = None,
|
|
30
|
+
min_limit_inclusive: bool = True,
|
|
31
|
+
max_limit_inclusive: bool = True,
|
|
32
|
+
max_length: Optional[int] = None,
|
|
33
|
+
width_chars: Optional[int] = None,
|
|
34
|
+
parent: Optional[QWidget] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Initialize the parameter line edit.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
label: Parameter name displayed to the left of the line edit.
|
|
40
|
+
init_val: Initial value.
|
|
41
|
+
val_fmt: Display format string.
|
|
42
|
+
value_is_int: Whether the value is an integer.
|
|
43
|
+
unit_label: Optional unit shown to the right, as LaTeX.
|
|
44
|
+
min_limit: Optional lower hard limit.
|
|
45
|
+
max_limit: Optional upper hard limit.
|
|
46
|
+
min_limit_inclusive: Whether the lower hard limit is inclusive.
|
|
47
|
+
max_limit_inclusive: Whether the upper hard limit is inclusive.
|
|
48
|
+
max_length: Optional maximum number of characters.
|
|
49
|
+
width_chars: Optional width of the line edit in characters.
|
|
50
|
+
parent: Optional parent widget.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
super().__init__(parent)
|
|
54
|
+
|
|
55
|
+
self._value_is_int = value_is_int
|
|
56
|
+
self._parse = int if value_is_int else float
|
|
57
|
+
self._label = MathLabel(f"{label}=", alignment=Qt.AlignRight | Qt.AlignVCenter)
|
|
58
|
+
self._measured_name_width = self._label.sizeHint().width()
|
|
59
|
+
self._unit_label: MathLabel | None = None
|
|
60
|
+
if unit_label:
|
|
61
|
+
self._unit_label = MathLabel(f"({unit_label})")
|
|
62
|
+
self._line_edit = NumericLineEdit(
|
|
63
|
+
init_val=init_val,
|
|
64
|
+
value_is_int=value_is_int,
|
|
65
|
+
val_fmt=val_fmt,
|
|
66
|
+
min_limit=min_limit,
|
|
67
|
+
max_limit=max_limit,
|
|
68
|
+
min_limit_inclusive=min_limit_inclusive,
|
|
69
|
+
max_limit_inclusive=max_limit_inclusive,
|
|
70
|
+
place_holder_text="Enter an int" if value_is_int else "Enter a float",
|
|
71
|
+
max_length=max_length,
|
|
72
|
+
width_chars=width_chars,
|
|
73
|
+
)
|
|
74
|
+
self._line_edit.valueChanged.connect(self._on_values_changed)
|
|
75
|
+
|
|
76
|
+
self._make_layout()
|
|
77
|
+
|
|
78
|
+
def _make_layout(self) -> None:
|
|
79
|
+
"""Create the layout used by this widget."""
|
|
80
|
+
|
|
81
|
+
gl = QGridLayout(self)
|
|
82
|
+
gl.setSpacing(0)
|
|
83
|
+
gl.addWidget(self._label, 0, 0)
|
|
84
|
+
gl.addWidget(self._line_edit, 0, 1)
|
|
85
|
+
if self._unit_label is not None:
|
|
86
|
+
gl.addWidget(self._unit_label, 0, 2)
|
|
87
|
+
gl.setColumnStretch(3, 1)
|
|
88
|
+
else:
|
|
89
|
+
gl.setColumnStretch(2, 1)
|
|
90
|
+
self._gl = gl
|
|
91
|
+
self._update_layout()
|
|
92
|
+
|
|
93
|
+
def _update_layout(self) -> None:
|
|
94
|
+
"""Reserve the effective name width for the label column."""
|
|
95
|
+
|
|
96
|
+
self._gl.setColumnMinimumWidth(0, self._effective_name_width())
|
|
97
|
+
|
|
98
|
+
# ----- internal helpers -----
|
|
99
|
+
def _on_values_changed(self) -> None:
|
|
100
|
+
"""Handle the values changed event."""
|
|
101
|
+
|
|
102
|
+
self.valueChanged.emit(self.get_value())
|
|
103
|
+
|
|
104
|
+
# ----- public API -----
|
|
105
|
+
def get_value(self) -> Num:
|
|
106
|
+
"""Return the current value."""
|
|
107
|
+
|
|
108
|
+
return self._line_edit.get_value()
|
|
109
|
+
|
|
110
|
+
def _validate_value(self, value: Num) -> Num:
|
|
111
|
+
"""Validate a value before applying it.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
value: Proposed value.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Validated value.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
TypeError: If ``value`` is not a number, or not an integer on an
|
|
121
|
+
integer line edit. ``bool`` is rejected.
|
|
122
|
+
"""
|
|
123
|
+
if isinstance(value, bool) or not isinstance(value, numbers.Real):
|
|
124
|
+
raise TypeError(f"value must be a number, not {type(value).__name__}")
|
|
125
|
+
if self._value_is_int and not isinstance(value, numbers.Integral):
|
|
126
|
+
raise TypeError(f"value must be an int, not {type(value).__name__}")
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
def _apply_value(self, value: Num) -> None:
|
|
130
|
+
"""Apply a validated value to the widget state."""
|
|
131
|
+
|
|
132
|
+
self._line_edit.set_value(self._parse(value))
|