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,1841 @@
|
|
|
1
|
+
"""Numeric range sliders with custom painting, dialogs, and overlays."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numbers
|
|
6
|
+
import random
|
|
7
|
+
from typing import Any, Callable, Dict, Generic, List, Optional, Tuple, TypeVar
|
|
8
|
+
|
|
9
|
+
from PySide6.QtCore import QEvent, QObject, QRect, Qt, Signal
|
|
10
|
+
from PySide6.QtGui import QColor, QFontMetrics, QPainter, QPalette, QPen, QShowEvent, QTextDocument
|
|
11
|
+
from PySide6.QtWidgets import (
|
|
12
|
+
QDialog,
|
|
13
|
+
QDialogButtonBox,
|
|
14
|
+
QFormLayout,
|
|
15
|
+
QHBoxLayout,
|
|
16
|
+
QLabel,
|
|
17
|
+
QSpinBox,
|
|
18
|
+
QStyle,
|
|
19
|
+
QStyleOptionSlider,
|
|
20
|
+
QStylePainter,
|
|
21
|
+
QWidget,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from ...dialogs import AskResult, show_warning
|
|
25
|
+
from ...formatting import format_is_valid, get_numeric_format_field
|
|
26
|
+
from ...labels import MathLabel
|
|
27
|
+
from ._defaults import UNIT_LABEL_SIZE, SliderBorderState
|
|
28
|
+
from ._dialogs import range_slider_ask_clamp_value, range_slider_ask_extend_range
|
|
29
|
+
from .base import SliderFeature, SliderWidgetBase
|
|
30
|
+
from .single import EditConfigDialog, make_line_edit
|
|
31
|
+
|
|
32
|
+
TNum = TypeVar("TNum", int, float)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SetValuesDialog(QDialog, Generic[TNum]):
|
|
36
|
+
"""Dialog for editing a lower and upper bound pair."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
slider_name: str,
|
|
41
|
+
value_is_int: bool,
|
|
42
|
+
current_lower: TNum,
|
|
43
|
+
current_upper: TNum,
|
|
44
|
+
fmt: str,
|
|
45
|
+
min_limit: TNum | None,
|
|
46
|
+
max_limit: TNum | None,
|
|
47
|
+
min_limit_inclusive: bool,
|
|
48
|
+
max_limit_inclusive: bool,
|
|
49
|
+
validate: Callable[[TNum, TNum], str | None],
|
|
50
|
+
parent: Optional[QWidget] = None,
|
|
51
|
+
):
|
|
52
|
+
"""Initialize the value-pair dialog.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
slider_name: Display name of the slider.
|
|
56
|
+
value_is_int: Whether the slider uses integer values.
|
|
57
|
+
current_lower: Current lower bound.
|
|
58
|
+
current_upper: Current upper bound.
|
|
59
|
+
fmt: Current display format string.
|
|
60
|
+
min_limit: Optional lower hard limit.
|
|
61
|
+
max_limit: Optional upper hard limit.
|
|
62
|
+
min_limit_inclusive: Whether the lower hard limit is inclusive.
|
|
63
|
+
max_limit_inclusive: Whether the upper hard limit is inclusive.
|
|
64
|
+
validate: Callable checking (lower, upper); returns an error
|
|
65
|
+
message or None.
|
|
66
|
+
parent: Optional parent widget.
|
|
67
|
+
"""
|
|
68
|
+
super().__init__(parent)
|
|
69
|
+
self.setWindowTitle("Set Values")
|
|
70
|
+
self._validate = validate
|
|
71
|
+
|
|
72
|
+
self._edit_lower = make_line_edit(
|
|
73
|
+
value_is_int, current_lower, fmt, min_limit, max_limit, min_limit_inclusive, max_limit_inclusive
|
|
74
|
+
)
|
|
75
|
+
self._edit_upper = make_line_edit(
|
|
76
|
+
value_is_int, current_upper, fmt, min_limit, max_limit, min_limit_inclusive, max_limit_inclusive
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=self)
|
|
80
|
+
buttons.accepted.connect(self.accept)
|
|
81
|
+
buttons.rejected.connect(self.reject)
|
|
82
|
+
|
|
83
|
+
form = QFormLayout(self)
|
|
84
|
+
form.addRow("Range Slider:", MathLabel(slider_name, alignment=Qt.AlignLeft | Qt.AlignVCenter))
|
|
85
|
+
form.addRow("Lower:", self._edit_lower)
|
|
86
|
+
form.addRow("Upper:", self._edit_upper)
|
|
87
|
+
form.addRow(buttons)
|
|
88
|
+
|
|
89
|
+
self.setSizeGripEnabled(False)
|
|
90
|
+
self.adjustSize()
|
|
91
|
+
self.setFixedSize(self.size())
|
|
92
|
+
|
|
93
|
+
def accept(self) -> None:
|
|
94
|
+
"""Validate the lower and upper values before accepting."""
|
|
95
|
+
msg = self._validate(self._edit_lower.get_value(), self._edit_upper.get_value())
|
|
96
|
+
if msg:
|
|
97
|
+
show_warning("Invalid values", msg, parent=self)
|
|
98
|
+
return
|
|
99
|
+
super().accept()
|
|
100
|
+
|
|
101
|
+
def get_values(self) -> Tuple[TNum, TNum]:
|
|
102
|
+
"""Return the lower and upper values.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Tuple containing the lower and upper values.
|
|
106
|
+
"""
|
|
107
|
+
return self._edit_lower.get_value(), self._edit_upper.get_value()
|
|
108
|
+
|
|
109
|
+
def showEvent(self, event: QShowEvent) -> None:
|
|
110
|
+
"""Focus the lower editor when the dialog is shown.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
event: Qt show event.
|
|
114
|
+
"""
|
|
115
|
+
super().showEvent(event)
|
|
116
|
+
self._edit_lower.setFocus()
|
|
117
|
+
self._edit_lower.selectAll()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class SetMarginsDialog(QDialog):
|
|
121
|
+
"""Dialog for editing lower and upper margin values."""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
slider_name: str,
|
|
126
|
+
margin_lower: int,
|
|
127
|
+
margin_upper: int,
|
|
128
|
+
parent: Optional[QWidget] = None,
|
|
129
|
+
):
|
|
130
|
+
"""Initialize the margins dialog.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
slider_name: Display name of the slider.
|
|
134
|
+
margin_lower: Current lower margin.
|
|
135
|
+
margin_upper: Current upper margin.
|
|
136
|
+
parent: Optional parent widget.
|
|
137
|
+
"""
|
|
138
|
+
super().__init__(parent)
|
|
139
|
+
self.setWindowTitle("Set Margins")
|
|
140
|
+
|
|
141
|
+
self._margin_lo_sb = self._build_spin_box(margin_lower)
|
|
142
|
+
self._margin_up_sb = self._build_spin_box(margin_upper)
|
|
143
|
+
|
|
144
|
+
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=self)
|
|
145
|
+
buttons.accepted.connect(self.accept)
|
|
146
|
+
buttons.rejected.connect(self.reject)
|
|
147
|
+
|
|
148
|
+
form = QFormLayout(self)
|
|
149
|
+
form.addRow("Range Slider:", MathLabel(slider_name, alignment=Qt.AlignLeft | Qt.AlignVCenter))
|
|
150
|
+
form.addRow("Upper:", self._margin_up_sb)
|
|
151
|
+
form.addRow("Lower:", self._margin_lo_sb)
|
|
152
|
+
form.addRow(buttons)
|
|
153
|
+
|
|
154
|
+
self.setSizeGripEnabled(False)
|
|
155
|
+
self.adjustSize()
|
|
156
|
+
self.setFixedSize(self.size())
|
|
157
|
+
|
|
158
|
+
def _build_spin_box(self, value: int) -> QSpinBox:
|
|
159
|
+
"""Create a spin box configured for margin editing.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
value: Initial spin-box value.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Configured spin box.
|
|
166
|
+
"""
|
|
167
|
+
sb = QSpinBox(self)
|
|
168
|
+
sb.setSuffix(" %")
|
|
169
|
+
sb.setMinimum(0)
|
|
170
|
+
sb.setMaximum(100)
|
|
171
|
+
sb.setValue(value)
|
|
172
|
+
sb.setFixedWidth(sb.sizeHint().width())
|
|
173
|
+
return sb
|
|
174
|
+
|
|
175
|
+
def get_values(self) -> Tuple[int, int]:
|
|
176
|
+
"""Return the margin values.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Tuple containing lower and upper margins.
|
|
180
|
+
"""
|
|
181
|
+
return self._margin_lo_sb.value(), self._margin_up_sb.value()
|
|
182
|
+
|
|
183
|
+
def showEvent(self, event: QShowEvent) -> None:
|
|
184
|
+
"""Focus the lower-margin editor when the dialog is shown.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
event: Qt show event.
|
|
188
|
+
"""
|
|
189
|
+
super().showEvent(event)
|
|
190
|
+
self._margin_lo_sb.setFocus()
|
|
191
|
+
self._margin_lo_sb.selectAll()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class DualHandleSlider(QWidget):
|
|
195
|
+
"""Custom slider widget with independently draggable lower and upper handles."""
|
|
196
|
+
|
|
197
|
+
valueChanged = Signal(tuple)
|
|
198
|
+
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
minimum=0.0,
|
|
202
|
+
maximum=100.0,
|
|
203
|
+
values=(20.0, 80.0),
|
|
204
|
+
orientation=Qt.Horizontal,
|
|
205
|
+
parent=None,
|
|
206
|
+
):
|
|
207
|
+
"""Initialize the dual-handle slider.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
minimum: Minimum allowed value.
|
|
211
|
+
maximum: Maximum allowed value.
|
|
212
|
+
values: Initial lower and upper values.
|
|
213
|
+
orientation: Slider orientation.
|
|
214
|
+
parent: Optional parent widget.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
ValueError: If ``maximum <= minimum``.
|
|
218
|
+
"""
|
|
219
|
+
super().__init__(parent)
|
|
220
|
+
|
|
221
|
+
self._min = float(minimum)
|
|
222
|
+
self._max = float(maximum)
|
|
223
|
+
self._lower = float(values[0])
|
|
224
|
+
self._upper = float(values[1])
|
|
225
|
+
self._orientation = orientation
|
|
226
|
+
|
|
227
|
+
self._active_handle = None
|
|
228
|
+
self._margin = 7
|
|
229
|
+
self._show_ticks = False
|
|
230
|
+
self._tick_count = 2
|
|
231
|
+
|
|
232
|
+
self.setMouseTracking(True)
|
|
233
|
+
|
|
234
|
+
if self._orientation == Qt.Horizontal:
|
|
235
|
+
self.setMinimumSize(80, 30)
|
|
236
|
+
else:
|
|
237
|
+
self.setMinimumSize(30, 80)
|
|
238
|
+
|
|
239
|
+
self._validate()
|
|
240
|
+
|
|
241
|
+
def _is_horizontal(self):
|
|
242
|
+
"""Return whether the slider orientation is horizontal."""
|
|
243
|
+
return self._orientation == Qt.Horizontal
|
|
244
|
+
|
|
245
|
+
def _pixel_min(self):
|
|
246
|
+
"""Return the minimum usable pixel coordinate along the slider."""
|
|
247
|
+
return self._margin
|
|
248
|
+
|
|
249
|
+
def _pixel_max(self):
|
|
250
|
+
"""Return the maximum usable pixel coordinate along the slider."""
|
|
251
|
+
if self._is_horizontal():
|
|
252
|
+
return self.width() - self._margin
|
|
253
|
+
return self.height() - self._margin
|
|
254
|
+
|
|
255
|
+
def _pixel_span(self):
|
|
256
|
+
"""Return the usable pixel span of the slider."""
|
|
257
|
+
return max(1, self._pixel_max() - self._pixel_min())
|
|
258
|
+
|
|
259
|
+
def _value_to_pixel(self, value):
|
|
260
|
+
"""Convert a logical value to a coordinate along the slider axis.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
value: Logical slider value.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Axis coordinate (measured from the minimum end).
|
|
267
|
+
"""
|
|
268
|
+
value = max(self._min, min(value, self._max))
|
|
269
|
+
frac = (value - self._min) / (self._max - self._min)
|
|
270
|
+
return int(round(self._pixel_min() + frac * self._pixel_span()))
|
|
271
|
+
|
|
272
|
+
def _pixel_to_value(self, pixel):
|
|
273
|
+
"""Convert an axis coordinate to a logical slider value.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
pixel: Coordinate along the slider axis.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Logical slider value.
|
|
280
|
+
"""
|
|
281
|
+
pixel = max(self._pixel_min(), min(pixel, self._pixel_max()))
|
|
282
|
+
frac = (pixel - self._pixel_min()) / self._pixel_span()
|
|
283
|
+
return self._min + frac * (self._max - self._min)
|
|
284
|
+
|
|
285
|
+
def _style_option(self, value: float) -> QStyleOptionSlider:
|
|
286
|
+
"""Build a style option describing a handle at a given value.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
value: Logical slider value.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Configured style option.
|
|
293
|
+
"""
|
|
294
|
+
opt = QStyleOptionSlider()
|
|
295
|
+
opt.initFrom(self)
|
|
296
|
+
opt.orientation = self._orientation
|
|
297
|
+
opt.minimum = 0
|
|
298
|
+
opt.maximum = 10000
|
|
299
|
+
|
|
300
|
+
pos = self._value_to_style_pos(value)
|
|
301
|
+
opt.sliderPosition = pos
|
|
302
|
+
opt.sliderValue = pos
|
|
303
|
+
opt.upsideDown = self._orientation == Qt.Vertical
|
|
304
|
+
opt.subControls = QStyle.SC_SliderGroove | QStyle.SC_SliderHandle
|
|
305
|
+
return opt
|
|
306
|
+
|
|
307
|
+
def _handle_rect(self, value: float) -> QRect:
|
|
308
|
+
"""Return the handle rectangle for a given value.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
value: Logical slider value.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Handle rectangle in widget coordinates.
|
|
315
|
+
"""
|
|
316
|
+
opt = self._style_option(value)
|
|
317
|
+
return self.style().subControlRect(QStyle.CC_Slider, opt, QStyle.SC_SliderHandle, self)
|
|
318
|
+
|
|
319
|
+
def _value_to_style_pos(self, value: float) -> int:
|
|
320
|
+
"""Convert a logical value to the style system's slider position.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
value: Logical slider value.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Style slider position in the range [0, 10000].
|
|
327
|
+
"""
|
|
328
|
+
frac = (value - self._min) / (self._max - self._min)
|
|
329
|
+
return int(round(frac * 10000))
|
|
330
|
+
|
|
331
|
+
def _hit_test(self, pos):
|
|
332
|
+
"""Return which handle contains the given point.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
pos: Mouse position in widget coordinates.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
``"lower"``, ``"upper"``, or ``None``.
|
|
339
|
+
"""
|
|
340
|
+
lower_rect = self._handle_rect(self._lower)
|
|
341
|
+
upper_rect = self._handle_rect(self._upper)
|
|
342
|
+
|
|
343
|
+
in_lower = lower_rect.contains(pos)
|
|
344
|
+
in_upper = upper_rect.contains(pos)
|
|
345
|
+
|
|
346
|
+
if in_lower and in_upper:
|
|
347
|
+
if self._is_horizontal():
|
|
348
|
+
dl = abs(pos.x() - lower_rect.center().x())
|
|
349
|
+
du = abs(pos.x() - upper_rect.center().x())
|
|
350
|
+
else:
|
|
351
|
+
dl = abs(pos.y() - lower_rect.center().y())
|
|
352
|
+
du = abs(pos.y() - upper_rect.center().y())
|
|
353
|
+
return "lower" if dl <= du else "upper"
|
|
354
|
+
|
|
355
|
+
if in_lower:
|
|
356
|
+
return "lower"
|
|
357
|
+
if in_upper:
|
|
358
|
+
return "upper"
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
def _pos_along_slider(self, pos):
|
|
362
|
+
"""Return the position projected onto the slider axis.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
pos: Mouse position in widget coordinates.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Coordinate along the logical slider axis.
|
|
369
|
+
"""
|
|
370
|
+
if self._is_horizontal():
|
|
371
|
+
return pos.x()
|
|
372
|
+
return self._pixel_min() + self._pixel_max() - pos.y()
|
|
373
|
+
|
|
374
|
+
def _closest_handle(self, pos):
|
|
375
|
+
"""Return the handle closest to a point.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
pos: Mouse position in widget coordinates.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
``"lower"`` or ``"upper"``.
|
|
382
|
+
"""
|
|
383
|
+
lower_c = self._handle_rect(self._lower).center()
|
|
384
|
+
upper_c = self._handle_rect(self._upper).center()
|
|
385
|
+
if self._is_horizontal():
|
|
386
|
+
dl = abs(pos.x() - lower_c.x())
|
|
387
|
+
du = abs(pos.x() - upper_c.x())
|
|
388
|
+
else:
|
|
389
|
+
dl = abs(pos.y() - lower_c.y())
|
|
390
|
+
du = abs(pos.y() - upper_c.y())
|
|
391
|
+
return "lower" if dl <= du else "upper"
|
|
392
|
+
|
|
393
|
+
def _clamp_pixel(self, pixel: int):
|
|
394
|
+
"""Move the active handle to a valid axis coordinate.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
pixel: Proposed coordinate along the slider axis.
|
|
398
|
+
"""
|
|
399
|
+
if self._active_handle == "lower":
|
|
400
|
+
p_up = self._value_to_pixel(self._upper)
|
|
401
|
+
new_p = max(self._pixel_min(), min(pixel, p_up - 1))
|
|
402
|
+
self.setValues((self._pixel_to_value(new_p), self._upper))
|
|
403
|
+
else:
|
|
404
|
+
p_lo = self._value_to_pixel(self._lower)
|
|
405
|
+
new_p = min(self._pixel_max(), max(pixel, p_lo + 1))
|
|
406
|
+
self.setValues((self._lower, self._pixel_to_value(new_p)))
|
|
407
|
+
|
|
408
|
+
def _draw_ticks(self, painter, groove_rect):
|
|
409
|
+
"""Draw tick marks along the slider groove.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
painter: Painter used for drawing.
|
|
413
|
+
groove_rect: Groove rectangle in widget coordinates.
|
|
414
|
+
"""
|
|
415
|
+
if not self._show_ticks:
|
|
416
|
+
return
|
|
417
|
+
painter.save()
|
|
418
|
+
pen = painter.pen()
|
|
419
|
+
pen.setColor(self.palette().color(QPalette.Disabled, QPalette.WindowText))
|
|
420
|
+
painter.setPen(pen)
|
|
421
|
+
|
|
422
|
+
n = max(2, self._tick_count)
|
|
423
|
+
|
|
424
|
+
if self._is_horizontal():
|
|
425
|
+
y1 = groove_rect.bottom() + 6
|
|
426
|
+
y2 = y1 + 3
|
|
427
|
+
for i in range(n):
|
|
428
|
+
frac = i / (n - 1)
|
|
429
|
+
x = int(round(self._pixel_min() + frac * self._pixel_span()))
|
|
430
|
+
painter.drawLine(x, y1, x, y2)
|
|
431
|
+
else:
|
|
432
|
+
x1 = groove_rect.right() + 6
|
|
433
|
+
x2 = x1 + 3
|
|
434
|
+
for i in range(n):
|
|
435
|
+
frac = i / (n - 1)
|
|
436
|
+
y = int(round(self._pixel_max() - frac * self._pixel_span()))
|
|
437
|
+
painter.drawLine(x1, y, x2, y)
|
|
438
|
+
painter.restore()
|
|
439
|
+
|
|
440
|
+
def _validate(self):
|
|
441
|
+
"""Validate the configured slider range.
|
|
442
|
+
|
|
443
|
+
Raises:
|
|
444
|
+
ValueError: If ``maximum <= minimum``.
|
|
445
|
+
"""
|
|
446
|
+
if self._max <= self._min:
|
|
447
|
+
raise ValueError("maximum must be greater than minimum")
|
|
448
|
+
|
|
449
|
+
def values(self):
|
|
450
|
+
"""Return the current lower and upper values."""
|
|
451
|
+
return self._lower, self._upper
|
|
452
|
+
|
|
453
|
+
def setValues(self, values):
|
|
454
|
+
"""Set the current lower and upper values.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
values: Tuple containing lower and upper values.
|
|
458
|
+
"""
|
|
459
|
+
new_vals = (float(values[0]), float(values[1]))
|
|
460
|
+
old_vals = (self._lower, self._upper)
|
|
461
|
+
self._lower, self._upper = new_vals
|
|
462
|
+
self.update()
|
|
463
|
+
if new_vals != old_vals:
|
|
464
|
+
self.valueChanged.emit(new_vals)
|
|
465
|
+
|
|
466
|
+
def setRange(self, minimum, maximum):
|
|
467
|
+
"""Set the allowed slider range.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
minimum: New minimum value.
|
|
471
|
+
maximum: New maximum value.
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
ValueError: If ``maximum <= minimum``.
|
|
475
|
+
"""
|
|
476
|
+
minimum, maximum = float(minimum), float(maximum)
|
|
477
|
+
if maximum <= minimum:
|
|
478
|
+
raise ValueError("maximum must be greater than minimum")
|
|
479
|
+
self._min, self._max = minimum, maximum
|
|
480
|
+
self.setValues((self._lower, self._upper))
|
|
481
|
+
|
|
482
|
+
def orientation(self):
|
|
483
|
+
"""Return the slider orientation."""
|
|
484
|
+
return self._orientation
|
|
485
|
+
|
|
486
|
+
def setShowTicks(self, state: bool):
|
|
487
|
+
"""Enable or disable tick rendering.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
state: Whether ticks should be drawn.
|
|
491
|
+
"""
|
|
492
|
+
self._show_ticks = bool(state)
|
|
493
|
+
self.update()
|
|
494
|
+
|
|
495
|
+
def setTickCount(self, count: int):
|
|
496
|
+
"""Set the number of ticks drawn on the slider.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
count: Desired tick count.
|
|
500
|
+
"""
|
|
501
|
+
self._tick_count = max(2, int(count))
|
|
502
|
+
self.update()
|
|
503
|
+
|
|
504
|
+
def mousePressEvent(self, event):
|
|
505
|
+
"""Pick a handle and move it to the click position.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
event: Qt mouse event.
|
|
509
|
+
"""
|
|
510
|
+
if event.button() != Qt.LeftButton:
|
|
511
|
+
return super().mousePressEvent(event)
|
|
512
|
+
|
|
513
|
+
pos = event.position().toPoint()
|
|
514
|
+
handle = self._hit_test(pos)
|
|
515
|
+
if handle is None:
|
|
516
|
+
handle = self._closest_handle(pos)
|
|
517
|
+
|
|
518
|
+
self._active_handle = handle
|
|
519
|
+
self._clamp_pixel(self._pos_along_slider(pos))
|
|
520
|
+
event.accept()
|
|
521
|
+
|
|
522
|
+
def mouseMoveEvent(self, event):
|
|
523
|
+
"""Drag the active handle.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
event: Qt mouse event.
|
|
527
|
+
"""
|
|
528
|
+
if self._active_handle is None:
|
|
529
|
+
return super().mouseMoveEvent(event)
|
|
530
|
+
|
|
531
|
+
self._clamp_pixel(self._pos_along_slider(event.position().toPoint()))
|
|
532
|
+
event.accept()
|
|
533
|
+
|
|
534
|
+
def mouseReleaseEvent(self, event):
|
|
535
|
+
"""Release the active handle.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
event: Qt mouse event.
|
|
539
|
+
"""
|
|
540
|
+
self._active_handle = None
|
|
541
|
+
event.accept()
|
|
542
|
+
|
|
543
|
+
def paintEvent(self, event):
|
|
544
|
+
"""Paint the groove, active span, ticks, and both handles.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
event: Qt paint event.
|
|
548
|
+
"""
|
|
549
|
+
painter = QStylePainter(self)
|
|
550
|
+
style = self.style()
|
|
551
|
+
|
|
552
|
+
lower_opt = self._style_option(self._lower)
|
|
553
|
+
upper_opt = self._style_option(self._upper)
|
|
554
|
+
|
|
555
|
+
groove_rect = style.subControlRect(QStyle.CC_Slider, lower_opt, QStyle.SC_SliderGroove, self)
|
|
556
|
+
lower_handle_rect = style.subControlRect(QStyle.CC_Slider, lower_opt, QStyle.SC_SliderHandle, self)
|
|
557
|
+
upper_handle_rect = style.subControlRect(QStyle.CC_Slider, upper_opt, QStyle.SC_SliderHandle, self)
|
|
558
|
+
|
|
559
|
+
base_opt = self._style_option(self._min)
|
|
560
|
+
base_opt.subControls = QStyle.SC_SliderGroove
|
|
561
|
+
|
|
562
|
+
painter.save()
|
|
563
|
+
painter.setClipRect(groove_rect.adjusted(-1, -1, 1, 1))
|
|
564
|
+
painter.drawComplexControl(QStyle.CC_Slider, base_opt)
|
|
565
|
+
painter.restore()
|
|
566
|
+
|
|
567
|
+
if self.isEnabled():
|
|
568
|
+
active_opt = self._style_option(self._upper)
|
|
569
|
+
active_opt.subControls = QStyle.SC_SliderGroove
|
|
570
|
+
if self._is_horizontal():
|
|
571
|
+
left = lower_handle_rect.center().x()
|
|
572
|
+
right = upper_handle_rect.center().x()
|
|
573
|
+
if right > left:
|
|
574
|
+
painter.save()
|
|
575
|
+
painter.setClipRect(QRect(left, groove_rect.top() - 2, right - left + 1, groove_rect.height() + 4))
|
|
576
|
+
painter.drawComplexControl(QStyle.CC_Slider, active_opt)
|
|
577
|
+
painter.restore()
|
|
578
|
+
else:
|
|
579
|
+
top = upper_handle_rect.center().y()
|
|
580
|
+
bottom = lower_handle_rect.center().y()
|
|
581
|
+
if bottom > top:
|
|
582
|
+
painter.save()
|
|
583
|
+
painter.setClipRect(QRect(groove_rect.left() - 2, top, groove_rect.width() + 4, bottom - top + 1))
|
|
584
|
+
painter.drawComplexControl(QStyle.CC_Slider, active_opt)
|
|
585
|
+
painter.restore()
|
|
586
|
+
|
|
587
|
+
self._draw_ticks(painter, groove_rect)
|
|
588
|
+
|
|
589
|
+
for handle, value in (("lower", self._lower), ("upper", self._upper)):
|
|
590
|
+
opt = self._style_option(value)
|
|
591
|
+
opt.subControls = QStyle.SC_SliderHandle
|
|
592
|
+
if self._active_handle == handle and self.isEnabled():
|
|
593
|
+
opt.activeSubControls = QStyle.SC_SliderHandle
|
|
594
|
+
opt.state |= QStyle.State_Sunken
|
|
595
|
+
painter.drawComplexControl(QStyle.CC_Slider, opt)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
class RangeMarkerOverlay(QWidget):
|
|
599
|
+
"""Transparent overlay that draws two default markers for a range slider."""
|
|
600
|
+
|
|
601
|
+
def __init__(
|
|
602
|
+
self,
|
|
603
|
+
slider: "DualHandleSlider",
|
|
604
|
+
lower_value_fn,
|
|
605
|
+
upper_value_fn,
|
|
606
|
+
*,
|
|
607
|
+
thickness: int = 2,
|
|
608
|
+
color: QColor | str = Qt.red,
|
|
609
|
+
):
|
|
610
|
+
"""Initialize the range-marker overlay.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
slider: Target dual-handle slider.
|
|
614
|
+
lower_value_fn: Callable returning the lower marker value.
|
|
615
|
+
upper_value_fn: Callable returning the upper marker value.
|
|
616
|
+
thickness: Marker line thickness in pixels.
|
|
617
|
+
color: Marker color.
|
|
618
|
+
"""
|
|
619
|
+
super().__init__(slider)
|
|
620
|
+
self._slider = slider
|
|
621
|
+
self._lower_value_fn = lower_value_fn
|
|
622
|
+
self._upper_value_fn = upper_value_fn
|
|
623
|
+
self._thickness = thickness
|
|
624
|
+
self._color = QColor(color)
|
|
625
|
+
|
|
626
|
+
self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
|
|
627
|
+
self.setAttribute(Qt.WA_NoSystemBackground, True)
|
|
628
|
+
self.setAttribute(Qt.WA_TranslucentBackground, True)
|
|
629
|
+
|
|
630
|
+
slider.installEventFilter(self)
|
|
631
|
+
self._sync_geometry()
|
|
632
|
+
self.show()
|
|
633
|
+
|
|
634
|
+
def _current_color(self):
|
|
635
|
+
"""Return the effective marker color for the current enabled state."""
|
|
636
|
+
c = QColor(self._color)
|
|
637
|
+
if not self._slider.isEnabled():
|
|
638
|
+
ref = self.palette().color(QPalette.Disabled, QPalette.WindowText)
|
|
639
|
+
c.setAlphaF(ref.alphaF())
|
|
640
|
+
return c
|
|
641
|
+
|
|
642
|
+
def eventFilter(self, obj: QObject, ev: QEvent) -> bool:
|
|
643
|
+
"""Synchronize overlay state when the target slider changes.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
obj: Watched object.
|
|
647
|
+
ev: Event delivered to the watched object.
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
Result of the base-class event filter.
|
|
651
|
+
"""
|
|
652
|
+
if obj is self._slider and ev.type() in (
|
|
653
|
+
QEvent.Resize,
|
|
654
|
+
QEvent.Show,
|
|
655
|
+
QEvent.Hide,
|
|
656
|
+
QEvent.EnabledChange,
|
|
657
|
+
):
|
|
658
|
+
self._sync_geometry()
|
|
659
|
+
self.update()
|
|
660
|
+
return super().eventFilter(obj, ev)
|
|
661
|
+
|
|
662
|
+
def _sync_geometry(self) -> None:
|
|
663
|
+
"""Synchronize geometry and visibility with the target slider."""
|
|
664
|
+
self.setGeometry(self._slider.rect())
|
|
665
|
+
self.setVisible(self._slider.isVisible())
|
|
666
|
+
|
|
667
|
+
def _draw_one_marker(self, painter: QPainter, value: float) -> None:
|
|
668
|
+
"""Draw a single marker line.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
painter: Painter used for drawing.
|
|
672
|
+
value: Logical slider value at which to draw the marker.
|
|
673
|
+
"""
|
|
674
|
+
groove_rect = self._slider.style().subControlRect(
|
|
675
|
+
QStyle.CC_Slider,
|
|
676
|
+
self._slider._style_option(value),
|
|
677
|
+
QStyle.SC_SliderGroove,
|
|
678
|
+
self._slider,
|
|
679
|
+
)
|
|
680
|
+
handle_rect = self._slider._handle_rect(value)
|
|
681
|
+
if groove_rect.isNull() or handle_rect.isNull():
|
|
682
|
+
return
|
|
683
|
+
|
|
684
|
+
c = handle_rect.center()
|
|
685
|
+
if self._slider.orientation() == Qt.Horizontal:
|
|
686
|
+
painter.drawLine(c.x(), groove_rect.top(), c.x(), groove_rect.bottom())
|
|
687
|
+
else:
|
|
688
|
+
painter.drawLine(groove_rect.left(), c.y(), groove_rect.right(), c.y())
|
|
689
|
+
|
|
690
|
+
def paintEvent(self, _ev) -> None:
|
|
691
|
+
"""Paint both default markers.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
_ev: Qt paint event.
|
|
695
|
+
"""
|
|
696
|
+
p = QPainter(self)
|
|
697
|
+
p.setRenderHint(QPainter.Antialiasing, False)
|
|
698
|
+
pen = QPen(self._current_color())
|
|
699
|
+
pen.setWidth(self._thickness)
|
|
700
|
+
p.setPen(pen)
|
|
701
|
+
self._draw_one_marker(p, float(self._lower_value_fn()))
|
|
702
|
+
self._draw_one_marker(p, float(self._upper_value_fn()))
|
|
703
|
+
p.end()
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
class LabeledRangeSliderBase(SliderWidgetBase[Any], Generic[TNum]):
|
|
707
|
+
"""Base widget for labeled range sliders built on ``DualHandleSlider``.
|
|
708
|
+
|
|
709
|
+
Frame, menu, layout, widths and events come from ``SliderWidgetBase``; this
|
|
710
|
+
class adds the ``[lower, upper]`` display and the range-specific hooks.
|
|
711
|
+
|
|
712
|
+
Signals ``valueChanged``, ``defaultChanged`` and ``configChanged`` are
|
|
713
|
+
emitted only for user actions (dragging, context menu, dialogs).
|
|
714
|
+
Programmatic setters never emit them.
|
|
715
|
+
"""
|
|
716
|
+
|
|
717
|
+
_RANGE_MARGIN_H = 3
|
|
718
|
+
_RANGE_MARGIN_V = 3
|
|
719
|
+
value_is_int: bool = False
|
|
720
|
+
|
|
721
|
+
def __init__(
|
|
722
|
+
self,
|
|
723
|
+
label: str,
|
|
724
|
+
unit: str,
|
|
725
|
+
min_val: TNum,
|
|
726
|
+
max_val: TNum,
|
|
727
|
+
init_vals: Optional[Tuple[TNum, TNum]],
|
|
728
|
+
orientation: Qt.Orientation,
|
|
729
|
+
val_fmt: str,
|
|
730
|
+
show_range: bool,
|
|
731
|
+
features: SliderFeature,
|
|
732
|
+
parent: Optional[QWidget],
|
|
733
|
+
):
|
|
734
|
+
"""Initialize the labeled range-slider base widget.
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
label: Slider label text.
|
|
738
|
+
unit: Optional unit label.
|
|
739
|
+
min_val: Initial minimum value.
|
|
740
|
+
max_val: Initial maximum value.
|
|
741
|
+
init_vals: Initial lower and upper values, or ``None`` for 25 % /
|
|
742
|
+
75 % of the range.
|
|
743
|
+
orientation: Slider orientation.
|
|
744
|
+
val_fmt: Value display format string.
|
|
745
|
+
show_range: Initial visibility of range labels and ticks; change
|
|
746
|
+
later with ``set_show_range``.
|
|
747
|
+
features: Context-menu features offered by the slider.
|
|
748
|
+
parent: Optional parent widget.
|
|
749
|
+
"""
|
|
750
|
+
super().__init__(
|
|
751
|
+
label=label,
|
|
752
|
+
orientation=orientation,
|
|
753
|
+
val_fmt=val_fmt,
|
|
754
|
+
show_range=show_range,
|
|
755
|
+
features=features,
|
|
756
|
+
parent=parent,
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
self._unit = unit
|
|
760
|
+
self._min_value = min_val
|
|
761
|
+
self._max_value = max_val
|
|
762
|
+
|
|
763
|
+
if init_vals is None:
|
|
764
|
+
span = max_val - min_val
|
|
765
|
+
lo = self.evaluate_value(min_val + 0.25 * span)
|
|
766
|
+
hi = self.evaluate_value(min_val + 0.75 * span)
|
|
767
|
+
else:
|
|
768
|
+
lo = self.evaluate_value(init_vals[0])
|
|
769
|
+
hi = self.evaluate_value(init_vals[1])
|
|
770
|
+
|
|
771
|
+
self._current_lower = lo
|
|
772
|
+
self._current_upper = hi
|
|
773
|
+
self._default_lower = lo
|
|
774
|
+
self._default_upper = hi
|
|
775
|
+
|
|
776
|
+
self._value_wrap = QWidget(self)
|
|
777
|
+
self._value_l_label = self._make_value_label("[")
|
|
778
|
+
self._value_lo_label = self._make_value_label("")
|
|
779
|
+
self._value_c_label = self._make_value_label(", ")
|
|
780
|
+
self._value_hi_label = self._make_value_label("")
|
|
781
|
+
self._value_r_label = self._make_value_label("]")
|
|
782
|
+
hl = QHBoxLayout(self._value_wrap)
|
|
783
|
+
hl.setContentsMargins(0, 0, 0, 0)
|
|
784
|
+
hl.setSpacing(0)
|
|
785
|
+
for w in (self._value_l_label, self._value_lo_label, self._value_c_label,
|
|
786
|
+
self._value_hi_label, self._value_r_label):
|
|
787
|
+
hl.addWidget(w)
|
|
788
|
+
if unit:
|
|
789
|
+
self._unit_label = MathLabel(f"({unit})", parent=self, fix_size=True, font_size=UNIT_LABEL_SIZE)
|
|
790
|
+
hl.addWidget(self._unit_label)
|
|
791
|
+
hl.addStretch(1)
|
|
792
|
+
|
|
793
|
+
self._attach_slider(
|
|
794
|
+
DualHandleSlider(
|
|
795
|
+
minimum=float(min_val),
|
|
796
|
+
maximum=float(max_val),
|
|
797
|
+
values=(float(lo), float(hi)),
|
|
798
|
+
orientation=orientation,
|
|
799
|
+
parent=self,
|
|
800
|
+
)
|
|
801
|
+
)
|
|
802
|
+
self.set_format(val_fmt)
|
|
803
|
+
self._make_layout()
|
|
804
|
+
|
|
805
|
+
@staticmethod
|
|
806
|
+
def _make_value_label(text: str) -> QLabel:
|
|
807
|
+
"""Create a QLabel used in the formatted range display.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
text: Initial label text.
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
Configured value label.
|
|
814
|
+
"""
|
|
815
|
+
label = QLabel(text)
|
|
816
|
+
label.setObjectName("slider_value")
|
|
817
|
+
return label
|
|
818
|
+
|
|
819
|
+
# ----- SliderWidgetBase hooks -----
|
|
820
|
+
def _make_default_marker(self) -> QWidget:
|
|
821
|
+
"""Create the two default-value markers."""
|
|
822
|
+
return RangeMarkerOverlay(
|
|
823
|
+
self._slider,
|
|
824
|
+
lower_value_fn=lambda: float(self._default_lower),
|
|
825
|
+
upper_value_fn=lambda: float(self._default_upper),
|
|
826
|
+
thickness=2,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
def _value_widget(self) -> QWidget:
|
|
830
|
+
"""Return the ``[lower, upper]`` display with its unit."""
|
|
831
|
+
return self._value_wrap
|
|
832
|
+
|
|
833
|
+
def _align_value_labels(self, horizontal: bool) -> None:
|
|
834
|
+
"""Align the five value labels for the orientation.
|
|
835
|
+
|
|
836
|
+
Args:
|
|
837
|
+
horizontal: Whether the layout is horizontal.
|
|
838
|
+
"""
|
|
839
|
+
if horizontal:
|
|
840
|
+
fixed = Qt.AlignVCenter
|
|
841
|
+
value = Qt.AlignVCenter | Qt.AlignLeft
|
|
842
|
+
else:
|
|
843
|
+
fixed = Qt.AlignHCenter | Qt.AlignVCenter
|
|
844
|
+
value = Qt.AlignLeft | Qt.AlignVCenter
|
|
845
|
+
for label in (self._value_l_label, self._value_c_label, self._value_r_label):
|
|
846
|
+
label.setAlignment(fixed)
|
|
847
|
+
for label in (self._value_lo_label, self._value_hi_label):
|
|
848
|
+
label.setAlignment(value)
|
|
849
|
+
|
|
850
|
+
def _value_text_labels(self) -> List[QLabel]:
|
|
851
|
+
"""Return the labels whose text format follows the value format."""
|
|
852
|
+
return [self._value_lo_label, self._value_hi_label, self._min_label, self._max_label]
|
|
853
|
+
|
|
854
|
+
def _round_values_to_format(self) -> None:
|
|
855
|
+
"""Round current, default, min and max to the current format."""
|
|
856
|
+
self._current_lower = self.evaluate_value(self._current_lower)
|
|
857
|
+
self._current_upper = self.evaluate_value(self._current_upper)
|
|
858
|
+
self._default_lower = self.evaluate_value(self._default_lower)
|
|
859
|
+
self._default_upper = self.evaluate_value(self._default_upper)
|
|
860
|
+
self._min_value = self.evaluate_value(self._min_value)
|
|
861
|
+
self._max_value = self.evaluate_value(self._max_value)
|
|
862
|
+
|
|
863
|
+
def _apply_range_visibility(self, show: bool) -> None:
|
|
864
|
+
"""Show or hide the slider ticks.
|
|
865
|
+
|
|
866
|
+
Args:
|
|
867
|
+
show: Whether range ticks should be shown.
|
|
868
|
+
"""
|
|
869
|
+
self._slider.setShowTicks(show)
|
|
870
|
+
|
|
871
|
+
def _update_value_width(self) -> None:
|
|
872
|
+
"""Measure the width required by the ``[lower, upper]`` display."""
|
|
873
|
+
l = self._value_lo_label
|
|
874
|
+
l.ensurePolished()
|
|
875
|
+
values = self._iter_width_values(max_samples=200)
|
|
876
|
+
|
|
877
|
+
value_w = 0
|
|
878
|
+
if l.textFormat() == Qt.PlainText:
|
|
879
|
+
fm = QFontMetrics(l.font())
|
|
880
|
+
for v in values:
|
|
881
|
+
value_w = max(value_w, fm.horizontalAdvance(self._value_to_text(v)))
|
|
882
|
+
else:
|
|
883
|
+
doc = QTextDocument()
|
|
884
|
+
doc.setDefaultFont(l.font())
|
|
885
|
+
doc.setTextWidth(-1)
|
|
886
|
+
if self._orient == Qt.Horizontal:
|
|
887
|
+
doc.setDocumentMargin(0)
|
|
888
|
+
for v in values:
|
|
889
|
+
doc.setHtml(self._value_to_text(v))
|
|
890
|
+
value_w = max(value_w, int(doc.idealWidth()))
|
|
891
|
+
|
|
892
|
+
self._value_lo_label.setFixedWidth(value_w)
|
|
893
|
+
self._value_hi_label.setFixedWidth(value_w)
|
|
894
|
+
self._set_measured_value_width(self._value_wrap.sizeHint().width())
|
|
895
|
+
|
|
896
|
+
# ----- value handling -----
|
|
897
|
+
def evaluate_value(self, value: TNum, eval_fmt: str | None = None) -> TNum:
|
|
898
|
+
"""Normalize a value according to the evaluation format.
|
|
899
|
+
|
|
900
|
+
Args:
|
|
901
|
+
value: Value to normalize.
|
|
902
|
+
eval_fmt: Format to use instead of the current one.
|
|
903
|
+
|
|
904
|
+
Returns:
|
|
905
|
+
Normalized typed value.
|
|
906
|
+
"""
|
|
907
|
+
raise NotImplementedError
|
|
908
|
+
|
|
909
|
+
def _range_to_text(self, values: Tuple[TNum, TNum]) -> str:
|
|
910
|
+
"""Format a lower/upper pair for display.
|
|
911
|
+
|
|
912
|
+
Args:
|
|
913
|
+
values: Pair of lower and upper values.
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
Formatted range string.
|
|
917
|
+
"""
|
|
918
|
+
lo, hi = values
|
|
919
|
+
return f"[{self._value_to_text(lo)} , {self._value_to_text(hi)}]"
|
|
920
|
+
|
|
921
|
+
def _update_value_label(self) -> None:
|
|
922
|
+
"""Refresh the lower and upper value labels."""
|
|
923
|
+
self._value_lo_label.setText(self._value_to_text(self._current_lower))
|
|
924
|
+
self._value_hi_label.setText(self._value_to_text(self._current_upper))
|
|
925
|
+
|
|
926
|
+
def _update_all_labels(self) -> None:
|
|
927
|
+
"""Refresh value and range labels."""
|
|
928
|
+
self._update_value_label()
|
|
929
|
+
self._min_label.setText(self._value_to_text(self._min_value))
|
|
930
|
+
self._max_label.setText(self._value_to_text(self._max_value))
|
|
931
|
+
|
|
932
|
+
def _update_marker(self) -> None:
|
|
933
|
+
"""Repaint the default markers, if any."""
|
|
934
|
+
if self._default_red_marker is not None:
|
|
935
|
+
self._default_red_marker.update()
|
|
936
|
+
|
|
937
|
+
def _iter_width_values(self, max_samples: int = 200) -> list[TNum]:
|
|
938
|
+
"""Generate representative values for width measurement.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
max_samples: Maximum number of samples to generate.
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
List of representative values.
|
|
945
|
+
"""
|
|
946
|
+
a = self.evaluate_value(self._min_value)
|
|
947
|
+
b = self.evaluate_value(self._max_value)
|
|
948
|
+
|
|
949
|
+
if self.value_is_int:
|
|
950
|
+
mn, mx = int(a), int(b)
|
|
951
|
+
span = mx - mn
|
|
952
|
+
if span <= max_samples:
|
|
953
|
+
return [self.evaluate_value(v) for v in range(mn, mx + 1)]
|
|
954
|
+
|
|
955
|
+
vals = {
|
|
956
|
+
mn,
|
|
957
|
+
mx,
|
|
958
|
+
int(self._current_lower),
|
|
959
|
+
int(self._current_upper),
|
|
960
|
+
int(self._default_lower),
|
|
961
|
+
int(self._default_upper),
|
|
962
|
+
}
|
|
963
|
+
n_lin = min(max_samples - len(vals), 100)
|
|
964
|
+
for k in range(1, n_lin + 1):
|
|
965
|
+
vals.add(mn + (span * k) // (n_lin + 1))
|
|
966
|
+
|
|
967
|
+
rng = random.Random(0)
|
|
968
|
+
tries = 0
|
|
969
|
+
while len(vals) < max_samples and tries < 10 * max_samples:
|
|
970
|
+
vals.add(rng.randint(mn, mx))
|
|
971
|
+
tries += 1
|
|
972
|
+
return [self.evaluate_value(v) for v in sorted(vals)]
|
|
973
|
+
|
|
974
|
+
vals = []
|
|
975
|
+
seen = set()
|
|
976
|
+
|
|
977
|
+
def add(v):
|
|
978
|
+
"""Add a value if its formatted representation is new."""
|
|
979
|
+
v = self.evaluate_value(v)
|
|
980
|
+
key = self._value_to_text(v)
|
|
981
|
+
if key not in seen:
|
|
982
|
+
seen.add(key)
|
|
983
|
+
vals.append(v)
|
|
984
|
+
|
|
985
|
+
for v in (a, b, self._current_lower, self._current_upper, self._default_lower, self._default_upper):
|
|
986
|
+
add(v)
|
|
987
|
+
|
|
988
|
+
n_lin = min(max_samples, 100)
|
|
989
|
+
for k in range(n_lin + 1):
|
|
990
|
+
add(a + k / n_lin * (b - a))
|
|
991
|
+
|
|
992
|
+
rng = random.Random(0)
|
|
993
|
+
tries = 0
|
|
994
|
+
while len(vals) < max_samples and tries < 20 * max_samples:
|
|
995
|
+
add(rng.uniform(a, b))
|
|
996
|
+
tries += 1
|
|
997
|
+
return vals
|
|
998
|
+
|
|
999
|
+
def _on_slider_values_changed(self, vals: Tuple[float, float]) -> None:
|
|
1000
|
+
"""Handle lower/upper changes from the dual-handle slider.
|
|
1001
|
+
|
|
1002
|
+
Args:
|
|
1003
|
+
vals: New lower and upper values from the internal slider.
|
|
1004
|
+
"""
|
|
1005
|
+
lo = self.evaluate_value(vals[0])
|
|
1006
|
+
hi = self.evaluate_value(vals[1])
|
|
1007
|
+
|
|
1008
|
+
if hi <= lo:
|
|
1009
|
+
self._slider.setValues((self._current_lower, self._current_upper))
|
|
1010
|
+
elif lo != self._current_lower or hi != self._current_upper:
|
|
1011
|
+
self._current_lower = lo
|
|
1012
|
+
self._current_upper = hi
|
|
1013
|
+
self._update_value_label()
|
|
1014
|
+
if not self._quiet:
|
|
1015
|
+
self.valueChanged.emit(self.get_value())
|
|
1016
|
+
|
|
1017
|
+
def save_current_value_as_default_value(self) -> None:
|
|
1018
|
+
"""Store the current lower and upper values as defaults (user action)."""
|
|
1019
|
+
self._default_lower = self._current_lower
|
|
1020
|
+
self._default_upper = self._current_upper
|
|
1021
|
+
with self._quiet_updates():
|
|
1022
|
+
self._reset_to_default_value()
|
|
1023
|
+
self._update_marker()
|
|
1024
|
+
self.defaultChanged.emit((self._default_lower, self._default_upper))
|
|
1025
|
+
|
|
1026
|
+
def _reset_to_default_value(self) -> None:
|
|
1027
|
+
"""Move the handles to the default lower and upper values."""
|
|
1028
|
+
self._slider.setValues((float(self._default_lower), float(self._default_upper)))
|
|
1029
|
+
|
|
1030
|
+
def get_value(self) -> Any:
|
|
1031
|
+
"""Return the current lower and upper values.
|
|
1032
|
+
|
|
1033
|
+
Returns:
|
|
1034
|
+
Tuple containing lower and upper values.
|
|
1035
|
+
"""
|
|
1036
|
+
return self._current_lower, self._current_upper
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
class NumericRangeSliderBase(LabeledRangeSliderBase[TNum], Generic[TNum]):
|
|
1040
|
+
"""Base class for integer and float range sliders.
|
|
1041
|
+
|
|
1042
|
+
The slider works in one of two modes: *values* (the handles define the
|
|
1043
|
+
range) or *margins* (lower/upper percentages are used instead and the
|
|
1044
|
+
handles are disabled). The mode and margins are part of the value returned
|
|
1045
|
+
by ``get_value``.
|
|
1046
|
+
"""
|
|
1047
|
+
|
|
1048
|
+
def __init__(
|
|
1049
|
+
self,
|
|
1050
|
+
label: str,
|
|
1051
|
+
unit: str,
|
|
1052
|
+
value_is_int: bool,
|
|
1053
|
+
min_val: TNum,
|
|
1054
|
+
max_val: TNum,
|
|
1055
|
+
init_vals: Optional[Tuple[TNum, TNum]],
|
|
1056
|
+
val_fmt: str,
|
|
1057
|
+
min_limit: TNum | None,
|
|
1058
|
+
max_limit: TNum | None,
|
|
1059
|
+
min_limit_inclusive: bool,
|
|
1060
|
+
max_limit_inclusive: bool,
|
|
1061
|
+
use_margins: bool,
|
|
1062
|
+
margins: Tuple[int, int],
|
|
1063
|
+
show_range: bool,
|
|
1064
|
+
orientation: Qt.Orientation,
|
|
1065
|
+
features: SliderFeature,
|
|
1066
|
+
parent: Optional[QWidget] = None,
|
|
1067
|
+
):
|
|
1068
|
+
"""Initialize the numeric range-slider base class.
|
|
1069
|
+
|
|
1070
|
+
Args:
|
|
1071
|
+
label: Slider label text.
|
|
1072
|
+
unit: Optional unit label.
|
|
1073
|
+
value_is_int: Whether the slider uses integer values.
|
|
1074
|
+
min_val: Initial minimum value.
|
|
1075
|
+
max_val: Initial maximum value.
|
|
1076
|
+
init_vals: Initial lower and upper values.
|
|
1077
|
+
val_fmt: Value display format string.
|
|
1078
|
+
min_limit: Optional lower hard limit.
|
|
1079
|
+
max_limit: Optional upper hard limit.
|
|
1080
|
+
min_limit_inclusive: Whether the lower hard limit is inclusive.
|
|
1081
|
+
max_limit_inclusive: Whether the upper hard limit is inclusive.
|
|
1082
|
+
use_margins: Whether the slider starts in margins mode.
|
|
1083
|
+
margins: Lower and upper margins in percent (0-100).
|
|
1084
|
+
show_range: Initial visibility of range labels and ticks; change
|
|
1085
|
+
later with ``set_show_range``.
|
|
1086
|
+
orientation: Slider orientation.
|
|
1087
|
+
features: Context-menu features offered by the slider.
|
|
1088
|
+
parent: Optional parent widget.
|
|
1089
|
+
|
|
1090
|
+
Raises:
|
|
1091
|
+
ValueError: If the range violates the limits, ``init_vals`` is
|
|
1092
|
+
outside the range or limits or its upper value is not greater
|
|
1093
|
+
than its lower value, a margin is outside 0-100, or
|
|
1094
|
+
``use_margins`` is set without ``SliderFeature.MARGINS``.
|
|
1095
|
+
TypeError: If ``init_vals`` contains non-numeric values.
|
|
1096
|
+
"""
|
|
1097
|
+
if use_margins and SliderFeature.MARGINS not in features:
|
|
1098
|
+
raise ValueError("use_margins=True requires SliderFeature.MARGINS in features")
|
|
1099
|
+
|
|
1100
|
+
super().__init__(
|
|
1101
|
+
label=label,
|
|
1102
|
+
unit=unit,
|
|
1103
|
+
min_val=min_val,
|
|
1104
|
+
max_val=max_val,
|
|
1105
|
+
init_vals=init_vals,
|
|
1106
|
+
orientation=orientation,
|
|
1107
|
+
val_fmt=val_fmt,
|
|
1108
|
+
show_range=show_range,
|
|
1109
|
+
features=features,
|
|
1110
|
+
parent=parent,
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
self.value_is_int = value_is_int
|
|
1114
|
+
self._min_limit = min_limit
|
|
1115
|
+
self._max_limit = max_limit
|
|
1116
|
+
self._min_limit_inclusive = min_limit_inclusive
|
|
1117
|
+
self._max_limit_inclusive = max_limit_inclusive
|
|
1118
|
+
self._use_margins = bool(use_margins)
|
|
1119
|
+
self._margin_lower, self._margin_upper = self._validate_margins(margins)
|
|
1120
|
+
|
|
1121
|
+
self._validate_limit_one("min_val", self._min_value)
|
|
1122
|
+
self._validate_limit_one("max_val", self._max_value)
|
|
1123
|
+
lo, hi = self._validate_pair(self.get_current_value())
|
|
1124
|
+
if not (self._min_value <= lo and hi <= self._max_value):
|
|
1125
|
+
raise ValueError(
|
|
1126
|
+
f"init_vals ({lo}, {hi}) must lie within [{self._min_value}, {self._max_value}]"
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
self._add_right_click_items()
|
|
1130
|
+
self._slider.valueChanged.connect(self._on_slider_values_changed)
|
|
1131
|
+
|
|
1132
|
+
self._apply_mode()
|
|
1133
|
+
self._update_all_labels()
|
|
1134
|
+
self._update_value_width()
|
|
1135
|
+
|
|
1136
|
+
# ----- validation -----
|
|
1137
|
+
def _validate_limit_one(self, name: str, value: TNum) -> None:
|
|
1138
|
+
"""Validate that one value lies within the hard limits.
|
|
1139
|
+
|
|
1140
|
+
Args:
|
|
1141
|
+
name: Name of the value being validated.
|
|
1142
|
+
value: Value to validate.
|
|
1143
|
+
|
|
1144
|
+
Raises:
|
|
1145
|
+
ValueError: If the value violates a limit.
|
|
1146
|
+
"""
|
|
1147
|
+
if self._min_limit is not None:
|
|
1148
|
+
ok = value >= self._min_limit if self._min_limit_inclusive else value > self._min_limit
|
|
1149
|
+
if not ok:
|
|
1150
|
+
raise ValueError(f"{name} must be within the allowed limits.")
|
|
1151
|
+
|
|
1152
|
+
if self._max_limit is not None:
|
|
1153
|
+
ok = value <= self._max_limit if self._max_limit_inclusive else value < self._max_limit
|
|
1154
|
+
if not ok:
|
|
1155
|
+
raise ValueError(f"{name} must be within the allowed limits.")
|
|
1156
|
+
|
|
1157
|
+
def _check_number(self, name: str, value: Any) -> None:
|
|
1158
|
+
"""Check that a value has an acceptable numeric type.
|
|
1159
|
+
|
|
1160
|
+
Args:
|
|
1161
|
+
name: Name of the value being checked.
|
|
1162
|
+
value: Value to check.
|
|
1163
|
+
|
|
1164
|
+
Raises:
|
|
1165
|
+
TypeError: If the value is not a real number (or not an integer on
|
|
1166
|
+
integer sliders). ``bool`` is rejected.
|
|
1167
|
+
"""
|
|
1168
|
+
if isinstance(value, bool) or not isinstance(value, numbers.Real):
|
|
1169
|
+
raise TypeError(f"{name} must be a number, not {type(value).__name__}")
|
|
1170
|
+
if self.value_is_int and not isinstance(value, numbers.Integral):
|
|
1171
|
+
raise TypeError(f"{name} must be an integer, not {type(value).__name__}")
|
|
1172
|
+
|
|
1173
|
+
def _validate_pair(self, values: Tuple[Any, Any]) -> Tuple[TNum, TNum]:
|
|
1174
|
+
"""Validate and normalize a lower/upper pair.
|
|
1175
|
+
|
|
1176
|
+
Args:
|
|
1177
|
+
values: Pair of proposed lower and upper values.
|
|
1178
|
+
|
|
1179
|
+
Returns:
|
|
1180
|
+
Normalized lower and upper values.
|
|
1181
|
+
|
|
1182
|
+
Raises:
|
|
1183
|
+
TypeError: If a value is not numeric.
|
|
1184
|
+
ValueError: If a value violates the limits or upper <= lower.
|
|
1185
|
+
"""
|
|
1186
|
+
self._check_number("lower", values[0])
|
|
1187
|
+
self._check_number("upper", values[1])
|
|
1188
|
+
lo = self.evaluate_value(values[0])
|
|
1189
|
+
hi = self.evaluate_value(values[1])
|
|
1190
|
+
|
|
1191
|
+
self._validate_limit_one("lower", lo)
|
|
1192
|
+
self._validate_limit_one("upper", hi)
|
|
1193
|
+
|
|
1194
|
+
if hi <= lo:
|
|
1195
|
+
raise ValueError("upper must be greater than lower")
|
|
1196
|
+
return lo, hi
|
|
1197
|
+
|
|
1198
|
+
@staticmethod
|
|
1199
|
+
def _validate_margins(margins: Tuple[Any, Any]) -> Tuple[int, int]:
|
|
1200
|
+
"""Validate a lower/upper margin pair.
|
|
1201
|
+
|
|
1202
|
+
Args:
|
|
1203
|
+
margins: Lower and upper margins in percent.
|
|
1204
|
+
|
|
1205
|
+
Returns:
|
|
1206
|
+
Margins as integers.
|
|
1207
|
+
|
|
1208
|
+
Raises:
|
|
1209
|
+
ValueError: If a margin is outside 0-100.
|
|
1210
|
+
"""
|
|
1211
|
+
m_lo, m_up = int(margins[0]), int(margins[1])
|
|
1212
|
+
if not (0 <= m_lo <= 100 and 0 <= m_up <= 100):
|
|
1213
|
+
raise ValueError("margins must be within 0-100")
|
|
1214
|
+
return m_lo, m_up
|
|
1215
|
+
|
|
1216
|
+
def _check_new_pair(self, lower: TNum, upper: TNum) -> str | None:
|
|
1217
|
+
"""Check a proposed lower/upper pair (for the Set Values dialog).
|
|
1218
|
+
|
|
1219
|
+
Args:
|
|
1220
|
+
lower: Proposed lower value.
|
|
1221
|
+
upper: Proposed upper value.
|
|
1222
|
+
|
|
1223
|
+
Returns:
|
|
1224
|
+
Error message, or ``None`` if the pair is valid.
|
|
1225
|
+
"""
|
|
1226
|
+
try:
|
|
1227
|
+
self._validate_pair((lower, upper))
|
|
1228
|
+
except (TypeError, ValueError) as e:
|
|
1229
|
+
return str(e)
|
|
1230
|
+
return None
|
|
1231
|
+
|
|
1232
|
+
def _check_new_config(self, new_min: TNum, new_max: TNum, new_fmt: str) -> str | None:
|
|
1233
|
+
"""Check a proposed range and format before applying them.
|
|
1234
|
+
|
|
1235
|
+
Args:
|
|
1236
|
+
new_min: Proposed minimum value.
|
|
1237
|
+
new_max: Proposed maximum value.
|
|
1238
|
+
new_fmt: Proposed display format string.
|
|
1239
|
+
|
|
1240
|
+
Returns:
|
|
1241
|
+
Error message, or ``None`` if the configuration is valid.
|
|
1242
|
+
"""
|
|
1243
|
+
eval_fmt = get_numeric_format_field(new_fmt)
|
|
1244
|
+
mn = self.evaluate_value(new_min, eval_fmt)
|
|
1245
|
+
mx = self.evaluate_value(new_max, eval_fmt)
|
|
1246
|
+
if not format_is_valid(new_fmt, mn):
|
|
1247
|
+
return f"Invalid format: {new_fmt!r}"
|
|
1248
|
+
if mn >= mx:
|
|
1249
|
+
return f"Min must be less than max (with this format: {mn} and {mx})."
|
|
1250
|
+
try:
|
|
1251
|
+
self._validate_limit_one("Min", mn)
|
|
1252
|
+
self._validate_limit_one("Max", mx)
|
|
1253
|
+
except ValueError as e:
|
|
1254
|
+
return str(e)
|
|
1255
|
+
return None
|
|
1256
|
+
|
|
1257
|
+
def _validate_config(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
1258
|
+
"""Merge and check a configuration mapping before applying it.
|
|
1259
|
+
|
|
1260
|
+
Args:
|
|
1261
|
+
config: Partial or complete configuration mapping.
|
|
1262
|
+
|
|
1263
|
+
Returns:
|
|
1264
|
+
Complete, validated configuration mapping.
|
|
1265
|
+
|
|
1266
|
+
Raises:
|
|
1267
|
+
ValueError: If the range or format is invalid.
|
|
1268
|
+
"""
|
|
1269
|
+
config = super()._validate_config(config)
|
|
1270
|
+
msg = self._check_new_config(config["min_val"], config["max_val"], config["val_fmt"])
|
|
1271
|
+
if msg:
|
|
1272
|
+
raise ValueError(msg)
|
|
1273
|
+
return config
|
|
1274
|
+
|
|
1275
|
+
def _validate_value(self, value: Dict[str, Any]) -> Dict[str, Any]:
|
|
1276
|
+
"""Validate and complete a value mapping.
|
|
1277
|
+
|
|
1278
|
+
Args:
|
|
1279
|
+
value: Mapping with optional ``use_margins``, ``range`` and
|
|
1280
|
+
``margins`` entries; missing entries keep their current values.
|
|
1281
|
+
|
|
1282
|
+
Returns:
|
|
1283
|
+
Complete, normalized mapping.
|
|
1284
|
+
|
|
1285
|
+
Raises:
|
|
1286
|
+
TypeError: If ``value`` is not a mapping or the range is not numeric.
|
|
1287
|
+
ValueError: If the range, margins or mode are invalid.
|
|
1288
|
+
"""
|
|
1289
|
+
if not isinstance(value, dict):
|
|
1290
|
+
raise TypeError(f"value must be a dict, not {type(value).__name__}")
|
|
1291
|
+
use_margins = bool(value.get("use_margins", self._use_margins))
|
|
1292
|
+
if use_margins and SliderFeature.MARGINS not in self._features:
|
|
1293
|
+
raise ValueError("use_margins=True requires SliderFeature.MARGINS")
|
|
1294
|
+
margins = self._validate_margins(value.get("margins", (self._margin_lower, self._margin_upper)))
|
|
1295
|
+
pair = self._validate_pair(value.get("range", self.get_current_value()))
|
|
1296
|
+
return {"use_margins": use_margins, "range": pair, "margins": margins}
|
|
1297
|
+
|
|
1298
|
+
@staticmethod
|
|
1299
|
+
def _clamp_pair_to_range(values: Tuple[TNum, TNum], new_min: TNum, new_max: TNum) -> Tuple[TNum, TNum]:
|
|
1300
|
+
"""Clamp a lower/upper pair to a new numeric range.
|
|
1301
|
+
|
|
1302
|
+
Each value is clamped into ``[new_min, new_max]``. If that leaves no
|
|
1303
|
+
valid pair (upper not greater than lower), the full range is used.
|
|
1304
|
+
|
|
1305
|
+
Args:
|
|
1306
|
+
values: Pair of lower and upper values.
|
|
1307
|
+
new_min: New minimum range value.
|
|
1308
|
+
new_max: New maximum range value.
|
|
1309
|
+
|
|
1310
|
+
Returns:
|
|
1311
|
+
Clamped lower and upper values, with lower < upper.
|
|
1312
|
+
"""
|
|
1313
|
+
lo, hi = values
|
|
1314
|
+
lo = min(max(lo, new_min), new_max)
|
|
1315
|
+
hi = min(max(hi, new_min), new_max)
|
|
1316
|
+
if hi <= lo:
|
|
1317
|
+
return new_min, new_max
|
|
1318
|
+
return lo, hi
|
|
1319
|
+
|
|
1320
|
+
# ----- context menu -----
|
|
1321
|
+
def _add_right_click_items(self) -> None:
|
|
1322
|
+
"""Append range-slider context-menu items allowed by ``features``."""
|
|
1323
|
+
f = self._features
|
|
1324
|
+
items: List[Dict[str, Any]] = [{"id": "sep"}]
|
|
1325
|
+
if SliderFeature.MARGINS in f:
|
|
1326
|
+
items.append({"id": "use_values", "text": "Use Values", "checkable": True, "checked": False})
|
|
1327
|
+
if SliderFeature.SET_VALUE in f:
|
|
1328
|
+
items.append({"id": "set_values", "text": "Set Values..."})
|
|
1329
|
+
items.append({"id": "sep"})
|
|
1330
|
+
if SliderFeature.MARGINS in f:
|
|
1331
|
+
items += [
|
|
1332
|
+
{"id": "use_margins", "text": "Use Margins", "checkable": True, "checked": False},
|
|
1333
|
+
{"id": "set_margins", "text": "Set Margins..."},
|
|
1334
|
+
]
|
|
1335
|
+
if SliderFeature.CONFIG in f:
|
|
1336
|
+
items += [{"id": "sep"}, {"id": "edit_config", "text": "Edit Configuration..."}]
|
|
1337
|
+
self._right_click_items.extend(items)
|
|
1338
|
+
|
|
1339
|
+
def _right_click_requested(self, item_id, checked: bool) -> None:
|
|
1340
|
+
"""Dispatch range-slider context-menu actions (user actions).
|
|
1341
|
+
|
|
1342
|
+
Args:
|
|
1343
|
+
item_id: Triggered menu item identifier.
|
|
1344
|
+
checked: Checked state associated with the action.
|
|
1345
|
+
"""
|
|
1346
|
+
super()._right_click_requested(item_id, checked)
|
|
1347
|
+
if item_id == "edit_config":
|
|
1348
|
+
self._open_edit_config_dlg()
|
|
1349
|
+
elif item_id == "set_values":
|
|
1350
|
+
self._open_set_values_dlg()
|
|
1351
|
+
elif item_id == "set_margins":
|
|
1352
|
+
self._open_set_margins_dlg()
|
|
1353
|
+
elif item_id in ("use_values", "use_margins"):
|
|
1354
|
+
# Clicking the active mode unchecks it (checked=False): just restore the marks.
|
|
1355
|
+
if checked:
|
|
1356
|
+
self._use_margins = item_id == "use_margins"
|
|
1357
|
+
self._apply_mode()
|
|
1358
|
+
self.valueChanged.emit(self.get_value())
|
|
1359
|
+
else:
|
|
1360
|
+
self._apply_mode()
|
|
1361
|
+
|
|
1362
|
+
def _apply_mode(self) -> None:
|
|
1363
|
+
"""Apply the current values/margins mode to the menu and widgets (no signals)."""
|
|
1364
|
+
use_margins = self._use_margins
|
|
1365
|
+
self._set_right_click_item_checked("use_margins", use_margins)
|
|
1366
|
+
self._set_right_click_item_checked("use_values", not use_margins)
|
|
1367
|
+
|
|
1368
|
+
self._set_right_click_item_enabled("set_margins", use_margins)
|
|
1369
|
+
self._set_right_click_item_enabled("set_values", not use_margins)
|
|
1370
|
+
self._set_right_click_item_enabled("save_as_default", not use_margins)
|
|
1371
|
+
self._set_right_click_item_enabled("reset_to_default", not use_margins)
|
|
1372
|
+
|
|
1373
|
+
for w in (self._slider, self._min_label, self._max_label, self._value_wrap):
|
|
1374
|
+
w.setEnabled(not use_margins)
|
|
1375
|
+
|
|
1376
|
+
self._sync_overlay()
|
|
1377
|
+
|
|
1378
|
+
# ----- dialogs (user actions) -----
|
|
1379
|
+
def _open_edit_config_dlg(self) -> None:
|
|
1380
|
+
"""Open the configuration dialog and apply accepted changes.
|
|
1381
|
+
|
|
1382
|
+
If the new range would exclude the current or default pair, the user
|
|
1383
|
+
is asked whether to clamp them. Cancelling restores the previous format
|
|
1384
|
+
and values.
|
|
1385
|
+
"""
|
|
1386
|
+
dlg = EditConfigDialog[TNum](
|
|
1387
|
+
slider_name=self._label_text,
|
|
1388
|
+
value_is_int=self.value_is_int,
|
|
1389
|
+
min_val=self._min_value,
|
|
1390
|
+
max_val=self._max_value,
|
|
1391
|
+
val_fmt=self._val_fmt,
|
|
1392
|
+
min_limit=self._min_limit,
|
|
1393
|
+
max_limit=self._max_limit,
|
|
1394
|
+
min_limit_inclusive=self._min_limit_inclusive,
|
|
1395
|
+
max_limit_inclusive=self._max_limit_inclusive,
|
|
1396
|
+
validate=self._check_new_config,
|
|
1397
|
+
parent=self,
|
|
1398
|
+
)
|
|
1399
|
+
if dlg.exec() != QDialog.Accepted:
|
|
1400
|
+
return
|
|
1401
|
+
new_min, new_max, new_fmt = dlg.get_values()
|
|
1402
|
+
|
|
1403
|
+
# snapshot before set_format rounds anything
|
|
1404
|
+
old_fmt = self._val_fmt
|
|
1405
|
+
old_min, old_max = self._min_value, self._max_value
|
|
1406
|
+
saved_cur = self.get_current_value()
|
|
1407
|
+
saved_dft = self.get_default_value()
|
|
1408
|
+
old_value = self.get_value()
|
|
1409
|
+
|
|
1410
|
+
self.set_format(new_fmt)
|
|
1411
|
+
new_min = self.evaluate_value(new_min)
|
|
1412
|
+
new_max = self.evaluate_value(new_max)
|
|
1413
|
+
|
|
1414
|
+
cur = self.get_current_value()
|
|
1415
|
+
dft = self.get_default_value()
|
|
1416
|
+
new_cur = self._clamp_pair_to_range(cur, new_min, new_max)
|
|
1417
|
+
new_dft = self._clamp_pair_to_range(dft, new_min, new_max)
|
|
1418
|
+
|
|
1419
|
+
if new_cur != cur or new_dft != dft:
|
|
1420
|
+
self.set_border(SliderBorderState.ERROR)
|
|
1421
|
+
try:
|
|
1422
|
+
result = range_slider_ask_clamp_value(
|
|
1423
|
+
proposed_range=self._range_to_text((new_min, new_max)),
|
|
1424
|
+
old_cur=self._range_to_text(cur),
|
|
1425
|
+
old_dft=self._range_to_text(dft),
|
|
1426
|
+
new_cur=self._range_to_text(new_cur),
|
|
1427
|
+
new_dft=self._range_to_text(new_dft),
|
|
1428
|
+
parent=self,
|
|
1429
|
+
)
|
|
1430
|
+
finally:
|
|
1431
|
+
self.set_border(SliderBorderState.OFF)
|
|
1432
|
+
if result != AskResult.YES:
|
|
1433
|
+
self.set_format(old_fmt)
|
|
1434
|
+
self._current_lower, self._current_upper = saved_cur
|
|
1435
|
+
self._default_lower, self._default_upper = saved_dft
|
|
1436
|
+
self._min_value, self._max_value = old_min, old_max
|
|
1437
|
+
return
|
|
1438
|
+
|
|
1439
|
+
self.set_range(new_min, new_max)
|
|
1440
|
+
|
|
1441
|
+
if self.get_value() != old_value:
|
|
1442
|
+
self.valueChanged.emit(self.get_value())
|
|
1443
|
+
if self.get_default_value() != saved_dft:
|
|
1444
|
+
self.defaultChanged.emit(self.get_default_value())
|
|
1445
|
+
if (old_min, old_max, old_fmt) != (self._min_value, self._max_value, self._val_fmt):
|
|
1446
|
+
self.configChanged.emit(self.get_config())
|
|
1447
|
+
|
|
1448
|
+
def _open_set_values_dlg(self) -> None:
|
|
1449
|
+
"""Open the value-pair dialog and apply the accepted pair.
|
|
1450
|
+
|
|
1451
|
+
Only the current pair changes; the default stays as it is.
|
|
1452
|
+
"""
|
|
1453
|
+
dlg = SetValuesDialog[TNum](
|
|
1454
|
+
slider_name=self._label_text,
|
|
1455
|
+
value_is_int=self.value_is_int,
|
|
1456
|
+
current_lower=self._current_lower,
|
|
1457
|
+
current_upper=self._current_upper,
|
|
1458
|
+
fmt=self._val_fmt,
|
|
1459
|
+
min_limit=self._min_limit,
|
|
1460
|
+
max_limit=self._max_limit,
|
|
1461
|
+
min_limit_inclusive=self._min_limit_inclusive,
|
|
1462
|
+
max_limit_inclusive=self._max_limit_inclusive,
|
|
1463
|
+
validate=self._check_new_pair,
|
|
1464
|
+
parent=self,
|
|
1465
|
+
)
|
|
1466
|
+
if dlg.exec() != QDialog.Accepted:
|
|
1467
|
+
return
|
|
1468
|
+
|
|
1469
|
+
new_pair = self._validate_pair(dlg.get_values())
|
|
1470
|
+
old_value = self.get_value()
|
|
1471
|
+
old_range = (self._min_value, self._max_value)
|
|
1472
|
+
|
|
1473
|
+
if not (self._min_value <= new_pair[0] and new_pair[1] <= self._max_value):
|
|
1474
|
+
new_min = min(self._min_value, new_pair[0])
|
|
1475
|
+
new_max = max(self._max_value, new_pair[1])
|
|
1476
|
+
self.set_border(SliderBorderState.ERROR)
|
|
1477
|
+
try:
|
|
1478
|
+
result = range_slider_ask_extend_range(
|
|
1479
|
+
proposed_vals=self._range_to_text(new_pair),
|
|
1480
|
+
old_range=self._range_to_text(old_range),
|
|
1481
|
+
new_range=self._range_to_text((new_min, new_max)),
|
|
1482
|
+
parent=self,
|
|
1483
|
+
)
|
|
1484
|
+
finally:
|
|
1485
|
+
self.set_border(SliderBorderState.OFF)
|
|
1486
|
+
if result != AskResult.YES:
|
|
1487
|
+
return
|
|
1488
|
+
|
|
1489
|
+
self.set_value({"range": new_pair}, keep_default_value=True)
|
|
1490
|
+
|
|
1491
|
+
if self.get_value() != old_value:
|
|
1492
|
+
self.valueChanged.emit(self.get_value())
|
|
1493
|
+
if (self._min_value, self._max_value) != old_range:
|
|
1494
|
+
self.configChanged.emit(self.get_config())
|
|
1495
|
+
|
|
1496
|
+
def _open_set_margins_dlg(self) -> None:
|
|
1497
|
+
"""Open the margins dialog and apply accepted changes."""
|
|
1498
|
+
dlg = SetMarginsDialog(
|
|
1499
|
+
slider_name=self._label_text,
|
|
1500
|
+
margin_lower=self._margin_lower,
|
|
1501
|
+
margin_upper=self._margin_upper,
|
|
1502
|
+
parent=self,
|
|
1503
|
+
)
|
|
1504
|
+
if dlg.exec() != QDialog.Accepted:
|
|
1505
|
+
return
|
|
1506
|
+
|
|
1507
|
+
new_margins = dlg.get_values()
|
|
1508
|
+
if new_margins != (self._margin_lower, self._margin_upper):
|
|
1509
|
+
self._margin_lower, self._margin_upper = new_margins
|
|
1510
|
+
self.valueChanged.emit(self.get_value())
|
|
1511
|
+
|
|
1512
|
+
# ----- config -----
|
|
1513
|
+
def get_config(self) -> dict:
|
|
1514
|
+
"""Return the current configuration mapping.
|
|
1515
|
+
|
|
1516
|
+
Returns:
|
|
1517
|
+
Mapping containing the current range and format.
|
|
1518
|
+
"""
|
|
1519
|
+
return {
|
|
1520
|
+
"min_val": self._min_value,
|
|
1521
|
+
"max_val": self._max_value,
|
|
1522
|
+
"val_fmt": self._val_fmt,
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
def _apply_config(self, config: dict) -> None:
|
|
1526
|
+
"""Apply a validated configuration mapping (no signals).
|
|
1527
|
+
|
|
1528
|
+
Args:
|
|
1529
|
+
config: Configuration mapping to apply.
|
|
1530
|
+
"""
|
|
1531
|
+
self.set_format(config["val_fmt"])
|
|
1532
|
+
self.set_range(config["min_val"], config["max_val"])
|
|
1533
|
+
|
|
1534
|
+
# ----- value API (programmatic, no signals) -----
|
|
1535
|
+
def get_value(self) -> Dict[str, Any]:
|
|
1536
|
+
"""Return the current mode, range and margins.
|
|
1537
|
+
|
|
1538
|
+
Returns:
|
|
1539
|
+
Mapping with ``use_margins``, ``range`` and ``margins``.
|
|
1540
|
+
"""
|
|
1541
|
+
return {
|
|
1542
|
+
"use_margins": self._use_margins,
|
|
1543
|
+
"range": (self._current_lower, self._current_upper),
|
|
1544
|
+
"margins": (self._margin_lower, self._margin_upper),
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
def get_current_value(self) -> Tuple[TNum, TNum]:
|
|
1548
|
+
"""Return the current lower and upper values."""
|
|
1549
|
+
return self._current_lower, self._current_upper
|
|
1550
|
+
|
|
1551
|
+
def get_default_value(self) -> Tuple[TNum, TNum]:
|
|
1552
|
+
"""Return the stored default lower and upper values."""
|
|
1553
|
+
return self._default_lower, self._default_upper
|
|
1554
|
+
|
|
1555
|
+
def get_min_value(self) -> TNum:
|
|
1556
|
+
"""Return the current minimum range value."""
|
|
1557
|
+
return self._min_value
|
|
1558
|
+
|
|
1559
|
+
def get_max_value(self) -> TNum:
|
|
1560
|
+
"""Return the current maximum range value."""
|
|
1561
|
+
return self._max_value
|
|
1562
|
+
|
|
1563
|
+
def set_value(self, value: Dict[str, Any], keep_default_value: bool = False) -> None:
|
|
1564
|
+
"""Set the mode, range and/or margins (no signals).
|
|
1565
|
+
|
|
1566
|
+
Missing entries keep their current values. Everything is validated
|
|
1567
|
+
before anything changes. The visible range is extended if needed.
|
|
1568
|
+
|
|
1569
|
+
Args:
|
|
1570
|
+
value: Mapping with optional ``use_margins``, ``range`` and
|
|
1571
|
+
``margins`` entries.
|
|
1572
|
+
keep_default_value: Whether to keep the stored default range.
|
|
1573
|
+
|
|
1574
|
+
Raises:
|
|
1575
|
+
TypeError: If ``value`` is not a mapping or the range is not numeric.
|
|
1576
|
+
ValueError: If the range, margins or mode are invalid.
|
|
1577
|
+
"""
|
|
1578
|
+
self._apply_value(self._validate_value(value), keep_default_value)
|
|
1579
|
+
|
|
1580
|
+
def _apply_value(self, value: Dict[str, Any], keep_default_value: bool = False) -> None:
|
|
1581
|
+
"""Apply a validated value mapping.
|
|
1582
|
+
|
|
1583
|
+
Args:
|
|
1584
|
+
value: Mapping returned by ``_validate_value``.
|
|
1585
|
+
keep_default_value: Whether to keep the stored default range.
|
|
1586
|
+
"""
|
|
1587
|
+
lo, hi = value["range"]
|
|
1588
|
+
self._use_margins = value["use_margins"]
|
|
1589
|
+
self._margin_lower, self._margin_upper = value["margins"]
|
|
1590
|
+
self._current_lower, self._current_upper = lo, hi
|
|
1591
|
+
if not keep_default_value:
|
|
1592
|
+
self._default_lower, self._default_upper = lo, hi
|
|
1593
|
+
|
|
1594
|
+
self._min_value = min(self._min_value, lo)
|
|
1595
|
+
self._max_value = max(self._max_value, hi)
|
|
1596
|
+
|
|
1597
|
+
with self._quiet_updates():
|
|
1598
|
+
self._slider.setRange(float(self._min_value), float(self._max_value))
|
|
1599
|
+
self._slider.setValues((float(lo), float(hi)))
|
|
1600
|
+
|
|
1601
|
+
self._update_marker()
|
|
1602
|
+
self._update_all_labels()
|
|
1603
|
+
self._update_value_width()
|
|
1604
|
+
self._apply_mode()
|
|
1605
|
+
|
|
1606
|
+
def set_default_value(self, value: Tuple[TNum, TNum]) -> None:
|
|
1607
|
+
"""Set the stored default lower and upper values (no signals).
|
|
1608
|
+
|
|
1609
|
+
The visible range is extended if needed.
|
|
1610
|
+
|
|
1611
|
+
Args:
|
|
1612
|
+
value: New default lower and upper values.
|
|
1613
|
+
|
|
1614
|
+
Raises:
|
|
1615
|
+
TypeError: If a value is not numeric.
|
|
1616
|
+
ValueError: If the pair violates the limits or upper <= lower.
|
|
1617
|
+
"""
|
|
1618
|
+
lo, hi = self._validate_pair(value)
|
|
1619
|
+
self._default_lower, self._default_upper = lo, hi
|
|
1620
|
+
self._min_value = min(self._min_value, lo)
|
|
1621
|
+
self._max_value = max(self._max_value, hi)
|
|
1622
|
+
|
|
1623
|
+
with self._quiet_updates():
|
|
1624
|
+
self._slider.setRange(float(self._min_value), float(self._max_value))
|
|
1625
|
+
self._slider.setValues((float(self._current_lower), float(self._current_upper)))
|
|
1626
|
+
|
|
1627
|
+
self._update_marker()
|
|
1628
|
+
self._update_all_labels()
|
|
1629
|
+
self._update_value_width()
|
|
1630
|
+
|
|
1631
|
+
def set_range(self, min_val: TNum, max_val: TNum) -> None:
|
|
1632
|
+
"""Set the numeric range and clamp current/default pairs (no signals).
|
|
1633
|
+
|
|
1634
|
+
Args:
|
|
1635
|
+
min_val: New minimum value.
|
|
1636
|
+
max_val: New maximum value.
|
|
1637
|
+
|
|
1638
|
+
Raises:
|
|
1639
|
+
ValueError: If a bound violates the limits or ``max_val <= min_val``.
|
|
1640
|
+
"""
|
|
1641
|
+
min_val = self.evaluate_value(min_val)
|
|
1642
|
+
max_val = self.evaluate_value(max_val)
|
|
1643
|
+
self._validate_limit_one("min", min_val)
|
|
1644
|
+
self._validate_limit_one("max", max_val)
|
|
1645
|
+
if max_val <= min_val:
|
|
1646
|
+
raise ValueError("max_val must be greater than min_val")
|
|
1647
|
+
|
|
1648
|
+
self._min_value, self._max_value = min_val, max_val
|
|
1649
|
+
self._current_lower, self._current_upper = self._clamp_pair_to_range(
|
|
1650
|
+
self.get_current_value(), min_val, max_val
|
|
1651
|
+
)
|
|
1652
|
+
self._default_lower, self._default_upper = self._clamp_pair_to_range(
|
|
1653
|
+
self.get_default_value(), min_val, max_val
|
|
1654
|
+
)
|
|
1655
|
+
|
|
1656
|
+
with self._quiet_updates():
|
|
1657
|
+
self._slider.setRange(float(min_val), float(max_val))
|
|
1658
|
+
self._slider.setValues((float(self._current_lower), float(self._current_upper)))
|
|
1659
|
+
|
|
1660
|
+
self._update_marker()
|
|
1661
|
+
self._update_all_labels()
|
|
1662
|
+
self._update_value_width()
|
|
1663
|
+
|
|
1664
|
+
def set_min_value(self, min_val: TNum) -> None:
|
|
1665
|
+
"""Set only the minimum bound (no signals).
|
|
1666
|
+
|
|
1667
|
+
Args:
|
|
1668
|
+
min_val: New minimum value.
|
|
1669
|
+
"""
|
|
1670
|
+
self.set_range(min_val, self._max_value)
|
|
1671
|
+
|
|
1672
|
+
def set_max_value(self, max_val: TNum) -> None:
|
|
1673
|
+
"""Set only the maximum bound (no signals).
|
|
1674
|
+
|
|
1675
|
+
Args:
|
|
1676
|
+
max_val: New maximum value.
|
|
1677
|
+
"""
|
|
1678
|
+
self.set_range(self._min_value, max_val)
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
class IntRangeSlider(NumericRangeSliderBase[int]):
|
|
1682
|
+
"""Integer-valued range slider widget."""
|
|
1683
|
+
|
|
1684
|
+
def __init__(
|
|
1685
|
+
self,
|
|
1686
|
+
label: str,
|
|
1687
|
+
min_val: int,
|
|
1688
|
+
max_val: int,
|
|
1689
|
+
unit: str = "",
|
|
1690
|
+
init_vals: Optional[Tuple[int, int]] = None,
|
|
1691
|
+
val_fmt: str = "{:d}",
|
|
1692
|
+
min_limit: int | None = None,
|
|
1693
|
+
max_limit: int | None = None,
|
|
1694
|
+
min_limit_inclusive: bool = True,
|
|
1695
|
+
max_limit_inclusive: bool = True,
|
|
1696
|
+
use_margins: bool = False,
|
|
1697
|
+
margins: Tuple[int, int] = (10, 10),
|
|
1698
|
+
show_range: bool = True,
|
|
1699
|
+
orientation: Qt.Orientation = Qt.Horizontal,
|
|
1700
|
+
features: SliderFeature = SliderFeature.ALL,
|
|
1701
|
+
parent: Optional[QWidget] = None,
|
|
1702
|
+
):
|
|
1703
|
+
"""Initialize the integer range slider.
|
|
1704
|
+
|
|
1705
|
+
Args:
|
|
1706
|
+
label: Slider label text.
|
|
1707
|
+
min_val: Minimum finite value.
|
|
1708
|
+
max_val: Maximum finite value.
|
|
1709
|
+
unit: Optional unit label.
|
|
1710
|
+
init_vals: Initial lower and upper values.
|
|
1711
|
+
val_fmt: Integer display format string.
|
|
1712
|
+
min_limit: Optional lower hard limit.
|
|
1713
|
+
max_limit: Optional upper hard limit.
|
|
1714
|
+
min_limit_inclusive: Whether the lower hard limit is inclusive.
|
|
1715
|
+
max_limit_inclusive: Whether the upper hard limit is inclusive.
|
|
1716
|
+
use_margins: Whether the slider starts in margins mode.
|
|
1717
|
+
margins: Initial lower and upper margins in percent.
|
|
1718
|
+
show_range: Initial visibility of range labels and ticks; change
|
|
1719
|
+
later with ``set_show_range``.
|
|
1720
|
+
orientation: Slider orientation.
|
|
1721
|
+
features: Context-menu features offered by the slider. Defaults
|
|
1722
|
+
to ``SliderFeature.ALL``; ``SPECIAL`` and ``VALUES`` are ignored.
|
|
1723
|
+
parent: Optional parent widget.
|
|
1724
|
+
"""
|
|
1725
|
+
super().__init__(
|
|
1726
|
+
label=label,
|
|
1727
|
+
unit=unit,
|
|
1728
|
+
value_is_int=True,
|
|
1729
|
+
min_val=min_val,
|
|
1730
|
+
max_val=max_val,
|
|
1731
|
+
init_vals=init_vals,
|
|
1732
|
+
val_fmt=val_fmt,
|
|
1733
|
+
min_limit=min_limit,
|
|
1734
|
+
max_limit=max_limit,
|
|
1735
|
+
min_limit_inclusive=min_limit_inclusive,
|
|
1736
|
+
max_limit_inclusive=max_limit_inclusive,
|
|
1737
|
+
use_margins=use_margins,
|
|
1738
|
+
margins=margins,
|
|
1739
|
+
show_range=show_range,
|
|
1740
|
+
orientation=orientation,
|
|
1741
|
+
features=features,
|
|
1742
|
+
parent=parent,
|
|
1743
|
+
)
|
|
1744
|
+
|
|
1745
|
+
def evaluate_value(self, value: int, eval_fmt: str | None = None) -> int:
|
|
1746
|
+
"""Normalize an integer value using the evaluation format.
|
|
1747
|
+
|
|
1748
|
+
Args:
|
|
1749
|
+
value: Integer value to normalize.
|
|
1750
|
+
eval_fmt: Format to use instead of the current one.
|
|
1751
|
+
|
|
1752
|
+
Returns:
|
|
1753
|
+
Normalized integer value.
|
|
1754
|
+
"""
|
|
1755
|
+
fmt = self._eval_fmt if eval_fmt is None else eval_fmt
|
|
1756
|
+
try:
|
|
1757
|
+
return int(fmt.format(value))
|
|
1758
|
+
except Exception:
|
|
1759
|
+
return int(value)
|
|
1760
|
+
|
|
1761
|
+
|
|
1762
|
+
class FloatRangeSlider(NumericRangeSliderBase[float]):
|
|
1763
|
+
"""Floating-point range slider widget."""
|
|
1764
|
+
|
|
1765
|
+
def __init__(
|
|
1766
|
+
self,
|
|
1767
|
+
label: str,
|
|
1768
|
+
min_val: float,
|
|
1769
|
+
max_val: float,
|
|
1770
|
+
unit: str = "",
|
|
1771
|
+
init_vals: Optional[Tuple[float, float]] = None,
|
|
1772
|
+
val_fmt: str = "{:.3f}",
|
|
1773
|
+
min_limit: float | None = None,
|
|
1774
|
+
max_limit: float | None = None,
|
|
1775
|
+
min_limit_inclusive: bool = True,
|
|
1776
|
+
max_limit_inclusive: bool = True,
|
|
1777
|
+
use_margins: bool = False,
|
|
1778
|
+
margins: Tuple[int, int] = (10, 10),
|
|
1779
|
+
show_range: bool = True,
|
|
1780
|
+
orientation: Qt.Orientation = Qt.Horizontal,
|
|
1781
|
+
features: SliderFeature = SliderFeature.ALL,
|
|
1782
|
+
parent: Optional[QWidget] = None,
|
|
1783
|
+
):
|
|
1784
|
+
"""Initialize the floating-point range slider.
|
|
1785
|
+
|
|
1786
|
+
Args:
|
|
1787
|
+
label: Slider label text.
|
|
1788
|
+
min_val: Minimum finite value.
|
|
1789
|
+
max_val: Maximum finite value.
|
|
1790
|
+
unit: Optional unit label.
|
|
1791
|
+
init_vals: Initial lower and upper values.
|
|
1792
|
+
val_fmt: Float display format string.
|
|
1793
|
+
min_limit: Optional lower hard limit.
|
|
1794
|
+
max_limit: Optional upper hard limit.
|
|
1795
|
+
min_limit_inclusive: Whether the lower hard limit is inclusive.
|
|
1796
|
+
max_limit_inclusive: Whether the upper hard limit is inclusive.
|
|
1797
|
+
use_margins: Whether the slider starts in margins mode.
|
|
1798
|
+
margins: Initial lower and upper margins in percent.
|
|
1799
|
+
show_range: Initial visibility of range labels and ticks; change
|
|
1800
|
+
later with ``set_show_range``.
|
|
1801
|
+
orientation: Slider orientation.
|
|
1802
|
+
features: Context-menu features offered by the slider. Defaults
|
|
1803
|
+
to ``SliderFeature.ALL``; ``SPECIAL`` and ``VALUES`` are ignored.
|
|
1804
|
+
parent: Optional parent widget.
|
|
1805
|
+
"""
|
|
1806
|
+
super().__init__(
|
|
1807
|
+
label=label,
|
|
1808
|
+
unit=unit,
|
|
1809
|
+
value_is_int=False,
|
|
1810
|
+
min_val=min_val,
|
|
1811
|
+
max_val=max_val,
|
|
1812
|
+
init_vals=init_vals,
|
|
1813
|
+
val_fmt=val_fmt,
|
|
1814
|
+
min_limit=min_limit,
|
|
1815
|
+
max_limit=max_limit,
|
|
1816
|
+
min_limit_inclusive=min_limit_inclusive,
|
|
1817
|
+
max_limit_inclusive=max_limit_inclusive,
|
|
1818
|
+
use_margins=use_margins,
|
|
1819
|
+
margins=margins,
|
|
1820
|
+
show_range=show_range,
|
|
1821
|
+
orientation=orientation,
|
|
1822
|
+
features=features,
|
|
1823
|
+
parent=parent,
|
|
1824
|
+
)
|
|
1825
|
+
|
|
1826
|
+
def evaluate_value(self, value: float, eval_fmt: str | None = None) -> float:
|
|
1827
|
+
"""Normalize a float value using the evaluation format.
|
|
1828
|
+
|
|
1829
|
+
Args:
|
|
1830
|
+
value: Float value to normalize.
|
|
1831
|
+
eval_fmt: Format to use instead of the current one.
|
|
1832
|
+
|
|
1833
|
+
Returns:
|
|
1834
|
+
Normalized float value.
|
|
1835
|
+
"""
|
|
1836
|
+
fmt = self._eval_fmt if eval_fmt is None else eval_fmt
|
|
1837
|
+
try:
|
|
1838
|
+
return float(fmt.format(float(value)))
|
|
1839
|
+
except Exception:
|
|
1840
|
+
return float(value)
|
|
1841
|
+
|