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,592 @@
|
|
|
1
|
+
"""Shared base for labeled slider widgets (single-value and range sliders)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from enum import Flag, auto
|
|
7
|
+
from typing import Any, Dict, Generic, List, Optional, TypeVar
|
|
8
|
+
|
|
9
|
+
from PySide6.QtCore import QEvent, Qt
|
|
10
|
+
from PySide6.QtWidgets import QGridLayout, QLabel, QMenu, QSizePolicy, QVBoxLayout, QWidget
|
|
11
|
+
|
|
12
|
+
from ...formatting import get_numeric_format_field, is_sci_format, value_to_text
|
|
13
|
+
from ...labels import MathLabel
|
|
14
|
+
from ..base import ParameterWidgetBase
|
|
15
|
+
from ._defaults import SLIDER_BORDER_STYLE_SHEET, SLIDER_LABEL_SIZE, SliderBorderState
|
|
16
|
+
from ._right_click_overlay import RightClickOverlay
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SliderFeature(Flag):
|
|
23
|
+
"""Optional context-menu features of a slider.
|
|
24
|
+
|
|
25
|
+
Each flag decides whether a group of menu items is offered at all.
|
|
26
|
+
Sliders ignore flags that do not apply to them.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
NONE: No menu and no default marker.
|
|
30
|
+
DEFAULTS: "Save as Default" / "Reset to Default" and the default marker.
|
|
31
|
+
SET_VALUE: "Set Value..." (or "Set Values..." on range sliders).
|
|
32
|
+
CONFIG: "Edit Configuration...".
|
|
33
|
+
SPECIAL: "Set Value to ±inf" (still subject to limits).
|
|
34
|
+
VALUES: "Show Values..." (``MultiVariableSlider`` only).
|
|
35
|
+
MARGINS: "Use Values" / "Use Margins" / "Set Margins..." (range
|
|
36
|
+
sliders only).
|
|
37
|
+
ALL: All of the above.
|
|
38
|
+
"""
|
|
39
|
+
NONE = 0
|
|
40
|
+
DEFAULTS = auto()
|
|
41
|
+
SET_VALUE = auto()
|
|
42
|
+
CONFIG = auto()
|
|
43
|
+
SPECIAL = auto()
|
|
44
|
+
VALUES = auto()
|
|
45
|
+
MARGINS = auto()
|
|
46
|
+
ALL = DEFAULTS | SET_VALUE | CONFIG | SPECIAL | VALUES | MARGINS
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_json(cls, data: str | list[str] | None) -> "SliderFeature":
|
|
50
|
+
"""Build flags from a name, a list of names, or None (= ALL).
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
data: A feature name, a list of names, or None.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Combined feature flags; ``ALL`` when ``data`` is None.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If a name is not a known feature.
|
|
60
|
+
"""
|
|
61
|
+
if data is None:
|
|
62
|
+
return cls.ALL
|
|
63
|
+
names = [data] if isinstance(data, str) else data
|
|
64
|
+
result = cls.NONE
|
|
65
|
+
for name in names:
|
|
66
|
+
try:
|
|
67
|
+
result |= cls[name.strip().upper()]
|
|
68
|
+
except KeyError:
|
|
69
|
+
valid = ", ".join(m.name for m in cls)
|
|
70
|
+
raise ValueError(f"unknown slider feature {name!r}; valid: {valid}") from None
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SliderWidgetBase(ParameterWidgetBase[T], Generic[T]):
|
|
75
|
+
"""Common frame for labeled sliders: label, inner slider, value display, menu.
|
|
76
|
+
|
|
77
|
+
The base owns everything that does not depend on the kind of slider: the
|
|
78
|
+
border, the context menu, the right-click overlay, the grid layout, range
|
|
79
|
+
labels, format handling and name/value width bookkeeping. Subclasses build
|
|
80
|
+
their inner slider and value display and fill in a few hooks.
|
|
81
|
+
|
|
82
|
+
Subclass ``__init__`` order:
|
|
83
|
+
1. ``super().__init__(...)``
|
|
84
|
+
2. set up own state and the value-display widgets
|
|
85
|
+
3. ``self._attach_slider(inner_slider)``
|
|
86
|
+
4. ``self.set_format(val_fmt)``
|
|
87
|
+
5. ``self._make_layout()``
|
|
88
|
+
|
|
89
|
+
Signals ``valueChanged``, ``defaultChanged`` and ``configChanged`` are
|
|
90
|
+
emitted only for user actions (dragging, context menu, dialogs).
|
|
91
|
+
Programmatic setters never emit them.
|
|
92
|
+
|
|
93
|
+
Widths: ``set_name_width`` / ``set_value_width`` store a requested minimum
|
|
94
|
+
(for aligning sliders in a panel); the measured widths are kept separately
|
|
95
|
+
and the layout uses the larger of the two. ``get_name_width`` /
|
|
96
|
+
``get_value_width`` return the measured widths.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
_RANGE_MARGIN_H = 8
|
|
100
|
+
"""Slider top margin (px) when range labels are shown, horizontal layout."""
|
|
101
|
+
_RANGE_MARGIN_V = 7
|
|
102
|
+
"""Slider left margin (px) when range labels are shown, vertical layout."""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
label: str,
|
|
107
|
+
orientation: Qt.Orientation,
|
|
108
|
+
val_fmt: str,
|
|
109
|
+
show_range: bool,
|
|
110
|
+
features: SliderFeature,
|
|
111
|
+
parent: Optional[QWidget],
|
|
112
|
+
):
|
|
113
|
+
"""Initialize the common slider frame.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
label: Slider label shown next to the control.
|
|
117
|
+
orientation: Slider orientation.
|
|
118
|
+
val_fmt: Value display format string.
|
|
119
|
+
show_range: Initial visibility of range labels and ticks; change
|
|
120
|
+
later with ``set_show_range``.
|
|
121
|
+
features: Context-menu features offered by the slider.
|
|
122
|
+
parent: Optional parent widget.
|
|
123
|
+
"""
|
|
124
|
+
super().__init__(parent)
|
|
125
|
+
|
|
126
|
+
self.setObjectName("labeled_slider")
|
|
127
|
+
self.setAttribute(Qt.WA_StyledBackground, True)
|
|
128
|
+
self.setStyleSheet(SLIDER_BORDER_STYLE_SHEET)
|
|
129
|
+
self.set_border(SliderBorderState.OFF)
|
|
130
|
+
|
|
131
|
+
self._label_text = label
|
|
132
|
+
self._orient = orientation
|
|
133
|
+
self._show_range = show_range
|
|
134
|
+
self._val_fmt = val_fmt
|
|
135
|
+
self._eval_fmt = get_numeric_format_field(val_fmt)
|
|
136
|
+
self._use_sci_html = False
|
|
137
|
+
self._right_click_enable = True
|
|
138
|
+
self._quiet = 0
|
|
139
|
+
self._features = features
|
|
140
|
+
|
|
141
|
+
self._slider: QWidget | None = None
|
|
142
|
+
self._right_click_overlay: RightClickOverlay | None = None
|
|
143
|
+
self._default_red_marker: QWidget | None = None
|
|
144
|
+
self._gl: QGridLayout | None = None
|
|
145
|
+
self._sl: QVBoxLayout | None = None
|
|
146
|
+
|
|
147
|
+
self._name_label = MathLabel(label, parent=self, font_size=SLIDER_LABEL_SIZE)
|
|
148
|
+
self._min_label = QLabel()
|
|
149
|
+
self._max_label = QLabel()
|
|
150
|
+
self._min_label.setObjectName("slider_range")
|
|
151
|
+
self._max_label.setObjectName("slider_range")
|
|
152
|
+
|
|
153
|
+
self._right_click_items: List[Dict[str, Any]] = []
|
|
154
|
+
if SliderFeature.DEFAULTS in features:
|
|
155
|
+
self._right_click_items += [
|
|
156
|
+
{"id": "save_as_default", "text": "Save as Default"},
|
|
157
|
+
{"id": "reset_to_default", "text": "Reset to Default"},
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
self._measured_name_width = self._name_label.sizeHint().width()
|
|
161
|
+
|
|
162
|
+
# ----- hooks for subclasses -----
|
|
163
|
+
def _make_default_marker(self) -> QWidget:
|
|
164
|
+
"""Create the overlay that marks the default value(s) on the slider.
|
|
165
|
+
|
|
166
|
+
Called by ``_attach_slider`` only when ``DEFAULTS`` is set.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Marker overlay widget (must provide ``update()``).
|
|
170
|
+
"""
|
|
171
|
+
raise NotImplementedError
|
|
172
|
+
|
|
173
|
+
def _value_widget(self) -> QWidget:
|
|
174
|
+
"""Return the widget showing the current value (and unit).
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Value-display widget placed in the layout.
|
|
178
|
+
"""
|
|
179
|
+
raise NotImplementedError
|
|
180
|
+
|
|
181
|
+
def _align_value_labels(self, horizontal: bool) -> None:
|
|
182
|
+
"""Set the alignment of the value label(s) for the orientation.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
horizontal: Whether the layout is horizontal.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def _value_text_labels(self) -> List[QLabel]:
|
|
189
|
+
"""Return the labels that switch between plain and rich text.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Labels whose text format follows the value format.
|
|
193
|
+
"""
|
|
194
|
+
return [self._min_label, self._max_label]
|
|
195
|
+
|
|
196
|
+
def _round_values_to_format(self) -> None:
|
|
197
|
+
"""Round the stored values to the current format (called by ``set_format``)."""
|
|
198
|
+
|
|
199
|
+
def _apply_range_visibility(self, show: bool) -> None:
|
|
200
|
+
"""Show or hide the inner slider's ticks.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
show: Whether range ticks should be shown.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def _update_value_width(self) -> None:
|
|
207
|
+
"""Measure the value display and call ``_set_measured_value_width``."""
|
|
208
|
+
raise NotImplementedError
|
|
209
|
+
|
|
210
|
+
# ----- setup helpers for subclasses -----
|
|
211
|
+
def _attach_slider(self, slider: QWidget) -> None:
|
|
212
|
+
"""Register the inner slider and create its overlays.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
slider: The inner slider widget.
|
|
216
|
+
"""
|
|
217
|
+
self._slider = slider
|
|
218
|
+
self._right_click_overlay = RightClickOverlay(
|
|
219
|
+
target_widget=slider,
|
|
220
|
+
owner_widget=self,
|
|
221
|
+
parent=self,
|
|
222
|
+
)
|
|
223
|
+
self._right_click_overlay.hide()
|
|
224
|
+
if SliderFeature.DEFAULTS in self._features:
|
|
225
|
+
self._default_red_marker = self._make_default_marker()
|
|
226
|
+
|
|
227
|
+
def _make_layout(self) -> None:
|
|
228
|
+
"""Build the grid layout for the current orientation."""
|
|
229
|
+
gl = QGridLayout(self)
|
|
230
|
+
gl.setContentsMargins(0, 0, 0, 0)
|
|
231
|
+
|
|
232
|
+
slider_wrap = QWidget()
|
|
233
|
+
sl = QVBoxLayout(slider_wrap)
|
|
234
|
+
sl.setContentsMargins(0, 0, 0, 0)
|
|
235
|
+
sl.setSpacing(0)
|
|
236
|
+
sl.addWidget(self._slider)
|
|
237
|
+
|
|
238
|
+
value_widget = self._value_widget()
|
|
239
|
+
|
|
240
|
+
if self._orient == Qt.Horizontal:
|
|
241
|
+
gl.setVerticalSpacing(0)
|
|
242
|
+
common = Qt.AlignVCenter
|
|
243
|
+
self._name_label.setAlignment(common | Qt.AlignRight)
|
|
244
|
+
self._align_value_labels(True)
|
|
245
|
+
gl.addWidget(self._name_label, 0, 0, common | Qt.AlignRight)
|
|
246
|
+
gl.addWidget(slider_wrap, 0, 1, 1, 3, common)
|
|
247
|
+
gl.addWidget(value_widget, 0, 4, common | Qt.AlignLeft)
|
|
248
|
+
gl.addWidget(self._min_label, 1, 1)
|
|
249
|
+
gl.addWidget(self._max_label, 1, 3)
|
|
250
|
+
|
|
251
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
252
|
+
gl.setColumnStretch(2, 1)
|
|
253
|
+
else:
|
|
254
|
+
gl.setHorizontalSpacing(0)
|
|
255
|
+
gl.setVerticalSpacing(2)
|
|
256
|
+
common = Qt.AlignHCenter
|
|
257
|
+
self._align_value_labels(False)
|
|
258
|
+
self._name_label.setAlignment(common | Qt.AlignTop)
|
|
259
|
+
|
|
260
|
+
self._max_label.setContentsMargins(5, 0, 0, 0)
|
|
261
|
+
self._min_label.setContentsMargins(5, 0, 0, 1)
|
|
262
|
+
|
|
263
|
+
gl.addWidget(value_widget, 0, 0, 1, 3, common | Qt.AlignBottom)
|
|
264
|
+
gl.addWidget(slider_wrap, 1, 1, 3, 1, common)
|
|
265
|
+
gl.addWidget(self._name_label, 4, 0, 1, 3, common | Qt.AlignTop)
|
|
266
|
+
gl.addWidget(self._max_label, 1, 2, 1, 2)
|
|
267
|
+
gl.addWidget(self._min_label, 3, 2, 1, 2)
|
|
268
|
+
|
|
269
|
+
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
|
270
|
+
gl.setRowStretch(2, 1)
|
|
271
|
+
|
|
272
|
+
self._gl = gl
|
|
273
|
+
self._sl = sl
|
|
274
|
+
self._sync_overlay()
|
|
275
|
+
|
|
276
|
+
def _set_measured_value_width(self, new_w: int) -> None:
|
|
277
|
+
"""Store a newly measured value width, relayout, and report changes.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
new_w: Measured width of the value display in pixels.
|
|
281
|
+
"""
|
|
282
|
+
old_w = self._measured_value_width
|
|
283
|
+
self._measured_value_width = new_w
|
|
284
|
+
self._update_layout()
|
|
285
|
+
if old_w != new_w:
|
|
286
|
+
self.valueWidthChanged.emit(new_w)
|
|
287
|
+
|
|
288
|
+
# ----- internal helpers -----
|
|
289
|
+
def _sync_overlay(self) -> None:
|
|
290
|
+
"""Keep the right-click overlay aligned with the inner slider."""
|
|
291
|
+
if self._right_click_overlay is not None:
|
|
292
|
+
self._right_click_overlay.sync_to_target()
|
|
293
|
+
|
|
294
|
+
@contextmanager
|
|
295
|
+
def _quiet_updates(self):
|
|
296
|
+
"""Suppress user-action signals while code moves the slider."""
|
|
297
|
+
self._quiet += 1
|
|
298
|
+
try:
|
|
299
|
+
yield
|
|
300
|
+
finally:
|
|
301
|
+
self._quiet -= 1
|
|
302
|
+
|
|
303
|
+
def _update_eval_fmt(self) -> None:
|
|
304
|
+
"""Build a safe numeric format used for internal value normalization."""
|
|
305
|
+
self._eval_fmt = get_numeric_format_field(self._val_fmt)
|
|
306
|
+
|
|
307
|
+
def _value_to_text(self, value: Any) -> str:
|
|
308
|
+
"""Format a value for display.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
value: Value to format.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Formatted display string.
|
|
315
|
+
"""
|
|
316
|
+
return value_to_text(value=value, fmt=self._val_fmt)
|
|
317
|
+
|
|
318
|
+
def _update_layout(self) -> None:
|
|
319
|
+
"""Apply range visibility and width constraints to the layout."""
|
|
320
|
+
if self._gl is None:
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
show_range = self._show_range
|
|
324
|
+
self._apply_range_visibility(show_range)
|
|
325
|
+
self._min_label.setVisible(show_range)
|
|
326
|
+
self._max_label.setVisible(show_range)
|
|
327
|
+
|
|
328
|
+
name_w = self._effective_name_width()
|
|
329
|
+
value_w = self._effective_value_width()
|
|
330
|
+
|
|
331
|
+
if self._orient == Qt.Horizontal:
|
|
332
|
+
self._sl.setContentsMargins(0, self._RANGE_MARGIN_H if show_range else 0, 0, 0)
|
|
333
|
+
self._gl.setColumnMinimumWidth(0, name_w)
|
|
334
|
+
self._gl.setColumnMinimumWidth(4, value_w)
|
|
335
|
+
else:
|
|
336
|
+
self._sl.setContentsMargins(self._RANGE_MARGIN_V if show_range else 0, 0, 0, 0)
|
|
337
|
+
width = max(name_w, value_w)
|
|
338
|
+
slider_width = self._sl.sizeHint().width()
|
|
339
|
+
self._gl.setColumnMinimumWidth(0, max(10, (width - slider_width) // 2))
|
|
340
|
+
self._gl.setColumnMinimumWidth(1, slider_width)
|
|
341
|
+
self._gl.setColumnMinimumWidth(2, max(10, (width - slider_width) // 2))
|
|
342
|
+
|
|
343
|
+
self._gl.invalidate()
|
|
344
|
+
widget = self._gl.parentWidget()
|
|
345
|
+
if widget:
|
|
346
|
+
widget.updateGeometry()
|
|
347
|
+
self._sync_overlay()
|
|
348
|
+
|
|
349
|
+
# ----- context menu -----
|
|
350
|
+
def _set_right_click_item_checked(self, act_id, checked: bool) -> bool:
|
|
351
|
+
"""Update the stored checked state of a context-menu item.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
act_id: Menu item identifier.
|
|
355
|
+
checked: Checked state to store.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
``True`` if the item was found, otherwise ``False``.
|
|
359
|
+
"""
|
|
360
|
+
for item in self._right_click_items:
|
|
361
|
+
if item.get("id") == act_id and act_id != "sep":
|
|
362
|
+
item["checkable"] = bool(item.get("checkable", True))
|
|
363
|
+
item["checked"] = bool(checked)
|
|
364
|
+
return True
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
def _set_right_click_item_enabled(self, act_id, enabled: bool) -> bool:
|
|
368
|
+
"""Update the stored enabled state of a context-menu item.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
act_id: Menu item identifier.
|
|
372
|
+
enabled: Enabled state to store.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
``True`` if the item was found, otherwise ``False``.
|
|
376
|
+
"""
|
|
377
|
+
for item in self._right_click_items:
|
|
378
|
+
if item.get("id") == act_id and act_id != "sep":
|
|
379
|
+
item["enabled"] = bool(enabled)
|
|
380
|
+
return True
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
def _right_click_requested(self, item_id, checked: bool) -> None:
|
|
384
|
+
"""Dispatch common context-menu actions.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
item_id: Triggered menu item identifier.
|
|
388
|
+
checked: Checked state associated with the action.
|
|
389
|
+
"""
|
|
390
|
+
if item_id == "save_as_default":
|
|
391
|
+
self.save_current_value_as_default_value()
|
|
392
|
+
elif item_id == "reset_to_default":
|
|
393
|
+
self._reset_to_default_value()
|
|
394
|
+
|
|
395
|
+
def save_current_value_as_default_value(self) -> None:
|
|
396
|
+
"""Store the current value as the default value (user action)."""
|
|
397
|
+
|
|
398
|
+
def _reset_to_default_value(self) -> None:
|
|
399
|
+
"""Reset the control to its stored default value (user action)."""
|
|
400
|
+
|
|
401
|
+
def _show_context_menu(self, global_pos) -> None:
|
|
402
|
+
"""Build and display the context menu.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
global_pos: Global screen position where the menu should open.
|
|
406
|
+
"""
|
|
407
|
+
if not self._right_click_enable:
|
|
408
|
+
return
|
|
409
|
+
if not any(item.get("id") != "sep" for item in self._right_click_items):
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
self.set_border(SliderBorderState.NORMAL)
|
|
413
|
+
self.repaint()
|
|
414
|
+
try:
|
|
415
|
+
menu = QMenu()
|
|
416
|
+
menu.setSeparatorsCollapsible(True)
|
|
417
|
+
item_by_id = {}
|
|
418
|
+
|
|
419
|
+
for item in self._right_click_items:
|
|
420
|
+
act_id = item.get("id")
|
|
421
|
+
if act_id == "sep":
|
|
422
|
+
menu.addSeparator()
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
text = str(item.get("text", act_id if act_id is not None else "None"))
|
|
426
|
+
act = menu.addAction(text)
|
|
427
|
+
act.setEnabled(bool(item.get("enabled", True)))
|
|
428
|
+
checkable = bool(item.get("checkable", False))
|
|
429
|
+
act.setCheckable(checkable)
|
|
430
|
+
if checkable:
|
|
431
|
+
act.setChecked(bool(item.get("checked", False)))
|
|
432
|
+
act.setData(act_id)
|
|
433
|
+
item_by_id[act_id] = item
|
|
434
|
+
|
|
435
|
+
action = menu.exec(global_pos)
|
|
436
|
+
if action is None:
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
act_id = action.data()
|
|
440
|
+
if act_id is None:
|
|
441
|
+
act_id = action.text()
|
|
442
|
+
|
|
443
|
+
new_checked = False
|
|
444
|
+
if action.isCheckable():
|
|
445
|
+
old_checked = bool(item_by_id.get(act_id, {}).get("checked", action.isChecked()))
|
|
446
|
+
new_checked = bool(action.isChecked())
|
|
447
|
+
if new_checked == old_checked:
|
|
448
|
+
new_checked = not old_checked
|
|
449
|
+
action.setChecked(new_checked)
|
|
450
|
+
if act_id in item_by_id:
|
|
451
|
+
item_by_id[act_id]["checked"] = new_checked
|
|
452
|
+
|
|
453
|
+
self._right_click_requested(act_id, new_checked)
|
|
454
|
+
finally:
|
|
455
|
+
self.set_border(SliderBorderState.OFF)
|
|
456
|
+
|
|
457
|
+
# ----- public API -----
|
|
458
|
+
def set_format(self, val_fmt: str) -> None:
|
|
459
|
+
"""Set the display format and round stored values to it (no signals).
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
val_fmt: New format string.
|
|
463
|
+
"""
|
|
464
|
+
self._val_fmt = val_fmt
|
|
465
|
+
self._update_eval_fmt()
|
|
466
|
+
self._round_values_to_format()
|
|
467
|
+
|
|
468
|
+
self._use_sci_html = is_sci_format(val_fmt)
|
|
469
|
+
text_fmt = Qt.RichText if self._use_sci_html else Qt.PlainText
|
|
470
|
+
for label in self._value_text_labels():
|
|
471
|
+
label.setTextFormat(text_fmt)
|
|
472
|
+
|
|
473
|
+
def set_border(self, state: SliderBorderState) -> None:
|
|
474
|
+
"""Set the visual border state of the widget.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
state: Border state enum value.
|
|
478
|
+
"""
|
|
479
|
+
self.setProperty("borderState", state.value)
|
|
480
|
+
self.style().unpolish(self)
|
|
481
|
+
self.style().polish(self)
|
|
482
|
+
self.update()
|
|
483
|
+
|
|
484
|
+
def set_right_click_enabled(self, state: bool) -> None:
|
|
485
|
+
"""Enable or disable the whole context menu at runtime.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
state: Whether right-click actions should be enabled.
|
|
489
|
+
"""
|
|
490
|
+
self._right_click_enable = state
|
|
491
|
+
|
|
492
|
+
def set_slider_enabled(self, enabled: bool) -> None:
|
|
493
|
+
"""Enable or disable the inner slider widget.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
enabled: Whether the inner slider should be enabled.
|
|
497
|
+
"""
|
|
498
|
+
self._slider.setEnabled(enabled)
|
|
499
|
+
self._sync_overlay()
|
|
500
|
+
|
|
501
|
+
def set_show_range(self, show: bool) -> None:
|
|
502
|
+
"""Show or hide the range labels and ticks.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
show: Whether range labels and ticks should be shown.
|
|
506
|
+
"""
|
|
507
|
+
show = bool(show)
|
|
508
|
+
if show == self._show_range:
|
|
509
|
+
return
|
|
510
|
+
self._show_range = show
|
|
511
|
+
self._update_layout()
|
|
512
|
+
|
|
513
|
+
def set_label(self, label: str) -> None:
|
|
514
|
+
"""Set the displayed slider label.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
label: New slider label text.
|
|
518
|
+
"""
|
|
519
|
+
self._label_text = label
|
|
520
|
+
self._name_label.set_text(label)
|
|
521
|
+
self._measured_name_width = self._name_label.sizeHint().width()
|
|
522
|
+
self._update_layout()
|
|
523
|
+
self.nameWidthChanged.emit(self._measured_name_width)
|
|
524
|
+
|
|
525
|
+
def get_value_width(self) -> int:
|
|
526
|
+
"""Re-measure and return the width of the value display.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
Width in pixels.
|
|
530
|
+
"""
|
|
531
|
+
was_blocked = self.blockSignals(True)
|
|
532
|
+
try:
|
|
533
|
+
self._update_value_width()
|
|
534
|
+
finally:
|
|
535
|
+
self.blockSignals(was_blocked)
|
|
536
|
+
return self._measured_value_width
|
|
537
|
+
|
|
538
|
+
# ----- Qt events -----
|
|
539
|
+
def contextMenuEvent(self, event) -> None:
|
|
540
|
+
"""Open the context menu.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
event: Qt context-menu event.
|
|
544
|
+
"""
|
|
545
|
+
self._show_context_menu(event.globalPos())
|
|
546
|
+
|
|
547
|
+
def resizeEvent(self, event) -> None:
|
|
548
|
+
"""Keep the overlay aligned after a resize.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
event: Qt resize event.
|
|
552
|
+
"""
|
|
553
|
+
super().resizeEvent(event)
|
|
554
|
+
self._sync_overlay()
|
|
555
|
+
|
|
556
|
+
def showEvent(self, event) -> None:
|
|
557
|
+
"""Keep the overlay aligned after the widget is shown.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
event: Qt show event.
|
|
561
|
+
"""
|
|
562
|
+
super().showEvent(event)
|
|
563
|
+
self._sync_overlay()
|
|
564
|
+
|
|
565
|
+
def moveEvent(self, event) -> None:
|
|
566
|
+
"""Keep the overlay aligned after a move.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
event: Qt move event.
|
|
570
|
+
"""
|
|
571
|
+
super().moveEvent(event)
|
|
572
|
+
self._sync_overlay()
|
|
573
|
+
|
|
574
|
+
def changeEvent(self, event) -> None:
|
|
575
|
+
"""Handle enabled-state changes and refresh overlays.
|
|
576
|
+
|
|
577
|
+
The inner slider's enabled state is deliberately not touched here:
|
|
578
|
+
Qt propagates it from the parent, and modes that lock the slider
|
|
579
|
+
(±inf, margins) rely on its explicit disable surviving a parent
|
|
580
|
+
re-enable.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
event: Qt change event.
|
|
584
|
+
"""
|
|
585
|
+
super().changeEvent(event)
|
|
586
|
+
if event.type() == QEvent.EnabledChange:
|
|
587
|
+
self._sync_overlay()
|
|
588
|
+
if self._default_red_marker is not None:
|
|
589
|
+
self._default_red_marker.update()
|
|
590
|
+
if not self.isEnabled():
|
|
591
|
+
self.set_border(SliderBorderState.OFF)
|
|
592
|
+
self.update()
|