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,660 @@
|
|
|
1
|
+
"""Array-aware slider widget for editing one value from a vector at a time."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import math
|
|
5
|
+
from typing import Literal, Optional, Sequence
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from PySide6.QtCore import Qt, Signal
|
|
9
|
+
from PySide6.QtWidgets import (
|
|
10
|
+
QDialog,
|
|
11
|
+
QDialogButtonBox,
|
|
12
|
+
QStyle,
|
|
13
|
+
QTextEdit,
|
|
14
|
+
QVBoxLayout,
|
|
15
|
+
QWidget,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ...dialogs import AskResult
|
|
19
|
+
from ._defaults import SliderBorderState
|
|
20
|
+
from ._dialogs import multi_slider_ask_clamp_value
|
|
21
|
+
from .base import SliderFeature
|
|
22
|
+
from .single import EditConfigDialog, FloatSlider, SpecialValue, normalize_special_value
|
|
23
|
+
|
|
24
|
+
class MultiVariableSlider(FloatSlider):
|
|
25
|
+
"""Float slider that exposes one element of an internal value array at a time.
|
|
26
|
+
|
|
27
|
+
The widget stores per-index current values, default values, and optional
|
|
28
|
+
special values such as ``+inf`` and ``-inf``. The selected index is shown on
|
|
29
|
+
the base ``FloatSlider`` interface, and updates to the active slider value
|
|
30
|
+
are written back to the corresponding array element.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
arrayChanged: Emitted when the user changes the stored current-value
|
|
34
|
+
array (dragging, context menu, dialogs). Programmatic methods such
|
|
35
|
+
as ``set_arr_values``, ``set_range`` or ``set_use_same_values`` do
|
|
36
|
+
not emit it. The emitted object is a copy of the array.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
arrayChanged = Signal(object)
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
label: str,
|
|
44
|
+
html_label: str,
|
|
45
|
+
min_val: float,
|
|
46
|
+
max_val: float,
|
|
47
|
+
init_vals: Sequence[float | str],
|
|
48
|
+
init_idx: int = 0,
|
|
49
|
+
unit: str = "",
|
|
50
|
+
use_same_values: bool = False,
|
|
51
|
+
steps: int = 10000,
|
|
52
|
+
val_fmt: str = "{:.3f}",
|
|
53
|
+
min_limit: float | None = None,
|
|
54
|
+
max_limit: float | None = None,
|
|
55
|
+
min_limit_inclusive: bool = True,
|
|
56
|
+
max_limit_inclusive: bool = True,
|
|
57
|
+
show_range: bool = True,
|
|
58
|
+
orientation: Qt.Orientation = Qt.Horizontal,
|
|
59
|
+
features: SliderFeature = SliderFeature.ALL,
|
|
60
|
+
parent: Optional[QWidget] = None,
|
|
61
|
+
):
|
|
62
|
+
"""Initialize the multi-variable slider.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
label: LaTeX-style label shown beside the slider.
|
|
66
|
+
html_label: HTML label used in the values dialog table header.
|
|
67
|
+
min_val: Minimum editable numeric range.
|
|
68
|
+
max_val: Maximum editable numeric range.
|
|
69
|
+
init_vals: Initial per-index values. Entries may be finite numbers or
|
|
70
|
+
special infinity markers.
|
|
71
|
+
init_idx: Initial active index in the stored array.
|
|
72
|
+
unit: Optional unit label shown beside the value.
|
|
73
|
+
use_same_values: If ``True``, changing one entry propagates the same
|
|
74
|
+
value to all entries.
|
|
75
|
+
steps: Integer resolution of the underlying float slider.
|
|
76
|
+
val_fmt: Display format string for finite values.
|
|
77
|
+
min_limit: Optional lower hard limit.
|
|
78
|
+
max_limit: Optional upper hard limit.
|
|
79
|
+
min_limit_inclusive: Whether the lower hard limit is inclusive.
|
|
80
|
+
max_limit_inclusive: Whether the upper hard limit is inclusive.
|
|
81
|
+
show_range: Initial visibility of range labels and ticks; change
|
|
82
|
+
later with ``set_show_range``.
|
|
83
|
+
orientation: Slider orientation.
|
|
84
|
+
features: Context-menu features offered by the slider.
|
|
85
|
+
Defaults to ``SliderFeature.ALL``.
|
|
86
|
+
parent: Optional parent widget.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If ``init_vals`` is empty, contains a finite value outside
|
|
90
|
+
the editable range, or contains an infinity marker that is not
|
|
91
|
+
permitted (missing ``SliderFeature.SPECIAL`` or a conflicting
|
|
92
|
+
hard limit).
|
|
93
|
+
"""
|
|
94
|
+
init_vals = list(init_vals)
|
|
95
|
+
if len(init_vals) == 0:
|
|
96
|
+
raise ValueError("init_values must not be empty")
|
|
97
|
+
|
|
98
|
+
init_special_arr: list[SpecialValue | None] = []
|
|
99
|
+
finite_init_values: list[float] = []
|
|
100
|
+
|
|
101
|
+
for i, v in enumerate(init_vals):
|
|
102
|
+
sv = normalize_special_value(v)
|
|
103
|
+
init_special_arr.append(sv)
|
|
104
|
+
if sv is None:
|
|
105
|
+
fv = float(v)
|
|
106
|
+
if not (min_val <= fv <= max_val):
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"init_values must all lie within [{min_val}, {max_val}], "
|
|
109
|
+
f"but found value {fv} at index {i}"
|
|
110
|
+
)
|
|
111
|
+
finite_init_values.append(fv)
|
|
112
|
+
else:
|
|
113
|
+
if SliderFeature.SPECIAL not in features:
|
|
114
|
+
raise ValueError(f"init_values[{i}] is {sv!r}, which requires SliderFeature.SPECIAL")
|
|
115
|
+
if sv == "+inf" and max_limit is not None:
|
|
116
|
+
raise ValueError(f"init_values[{i}] cannot be +inf when max_limit is set")
|
|
117
|
+
if sv == "-inf" and min_limit is not None:
|
|
118
|
+
raise ValueError(f"init_values[{i}] cannot be -inf when min_limit is set")
|
|
119
|
+
finite_init_values.append(max_val if sv == "+inf" else min_val)
|
|
120
|
+
|
|
121
|
+
self._arr_length = len(finite_init_values)
|
|
122
|
+
self._current_idx = init_idx % self._arr_length
|
|
123
|
+
self._current_arr = np.asarray(finite_init_values.copy())
|
|
124
|
+
self._default_arr = np.asarray(finite_init_values.copy())
|
|
125
|
+
self._current_special_arr = init_special_arr.copy()
|
|
126
|
+
self._use_same_values = False
|
|
127
|
+
|
|
128
|
+
init_val = init_special_arr[self._current_idx]
|
|
129
|
+
if init_val is None:
|
|
130
|
+
init_val = finite_init_values[self._current_idx]
|
|
131
|
+
|
|
132
|
+
super().__init__(
|
|
133
|
+
label=label,
|
|
134
|
+
unit=unit,
|
|
135
|
+
min_val=min_val,
|
|
136
|
+
max_val=max_val,
|
|
137
|
+
steps=steps,
|
|
138
|
+
init_val=init_val,
|
|
139
|
+
val_fmt=val_fmt,
|
|
140
|
+
min_limit=min_limit,
|
|
141
|
+
max_limit=max_limit,
|
|
142
|
+
min_limit_inclusive=min_limit_inclusive,
|
|
143
|
+
max_limit_inclusive=max_limit_inclusive,
|
|
144
|
+
show_range=show_range,
|
|
145
|
+
orientation=orientation,
|
|
146
|
+
features=features,
|
|
147
|
+
parent=parent,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
self.html_label = html_label
|
|
151
|
+
|
|
152
|
+
if SliderFeature.VALUES in self._features:
|
|
153
|
+
self._right_click_items += [
|
|
154
|
+
{"id": "sep"},
|
|
155
|
+
{"id": "show_val", "text": "Show Values..."},
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
self.set_arr_values(self.get_arr_values(), idx=self._current_idx)
|
|
159
|
+
self.set_use_same_values(use_same_values)
|
|
160
|
+
|
|
161
|
+
self.valueChanged.connect(self._on_single_value_changed)
|
|
162
|
+
self.defaultChanged.connect(self._on_single_default_changed)
|
|
163
|
+
|
|
164
|
+
# ----- internal helpers -----
|
|
165
|
+
def _arr_value_text(self, idx: int) -> str:
|
|
166
|
+
"""Return the formatted text for the value stored at ``idx``.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
idx: Array index to inspect.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Formatted finite value text, ``"+inf"``, or ``"-inf"``.
|
|
173
|
+
"""
|
|
174
|
+
sv = self._current_special_arr[idx]
|
|
175
|
+
if sv == "+inf":
|
|
176
|
+
return "+inf"
|
|
177
|
+
if sv == "-inf":
|
|
178
|
+
return "-inf"
|
|
179
|
+
return self._value_to_text(self._current_arr[idx])
|
|
180
|
+
|
|
181
|
+
def _store_active(self) -> None:
|
|
182
|
+
"""Write the active slider state into the stored arrays.
|
|
183
|
+
|
|
184
|
+
In same-values mode, every entry receives the active state.
|
|
185
|
+
"""
|
|
186
|
+
cur = float(self._current_value)
|
|
187
|
+
dft = float(self._default_value)
|
|
188
|
+
sv = self._current_special_value
|
|
189
|
+
if self._use_same_values:
|
|
190
|
+
self._current_arr[:] = cur
|
|
191
|
+
self._default_arr[:] = dft
|
|
192
|
+
self._current_special_arr[:] = [sv] * self._arr_length
|
|
193
|
+
else:
|
|
194
|
+
i = self._current_idx
|
|
195
|
+
self._current_arr[i] = cur
|
|
196
|
+
self._default_arr[i] = dft
|
|
197
|
+
self._current_special_arr[i] = sv
|
|
198
|
+
|
|
199
|
+
def _update_slider_for_current_idx(self) -> None:
|
|
200
|
+
"""Synchronize the base slider state with the currently selected index."""
|
|
201
|
+
self._current_value = float(self._current_arr[self._current_idx])
|
|
202
|
+
self._default_value = float(self._default_arr[self._current_idx])
|
|
203
|
+
self._current_special_value = self._current_special_arr[self._current_idx]
|
|
204
|
+
|
|
205
|
+
self._update_mapping_anchors()
|
|
206
|
+
with self._quiet_updates():
|
|
207
|
+
self._apply_range_to_slider()
|
|
208
|
+
self._slider.setValue(self._value_to_pos(self._current_value))
|
|
209
|
+
|
|
210
|
+
if self._current_special_value is not None:
|
|
211
|
+
self._enter_special_mode(self._current_special_value)
|
|
212
|
+
else:
|
|
213
|
+
self._exit_special_mode()
|
|
214
|
+
|
|
215
|
+
if self._default_red_marker is not None:
|
|
216
|
+
self._default_red_marker.update()
|
|
217
|
+
self._update_all_labels()
|
|
218
|
+
|
|
219
|
+
def _on_single_value_changed(self, _value) -> None:
|
|
220
|
+
"""Store the active value after a user change and report the array."""
|
|
221
|
+
self._store_active()
|
|
222
|
+
self.arrayChanged.emit(self.get_arr_values())
|
|
223
|
+
|
|
224
|
+
def _on_single_default_changed(self, _value) -> None:
|
|
225
|
+
"""Store the active default after a user change."""
|
|
226
|
+
self._store_active()
|
|
227
|
+
|
|
228
|
+
# ----- right click -----
|
|
229
|
+
def _right_click_requested(self, item_id, checked: bool) -> None:
|
|
230
|
+
"""Handle a context-menu action.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
item_id: Identifier of the selected menu action.
|
|
234
|
+
checked: Checked state associated with the action.
|
|
235
|
+
"""
|
|
236
|
+
super()._right_click_requested(item_id, checked)
|
|
237
|
+
if item_id == "show_val":
|
|
238
|
+
self._open_show_values_dlg()
|
|
239
|
+
|
|
240
|
+
def _open_edit_config_dlg(self) -> None:
|
|
241
|
+
"""Open and apply the configuration dialog for range and format.
|
|
242
|
+
|
|
243
|
+
If the new range would exclude current or default array values, the
|
|
244
|
+
user is asked whether those values should be clamped. Cancelling
|
|
245
|
+
restores the previous format and values.
|
|
246
|
+
"""
|
|
247
|
+
dlg = EditConfigDialog(
|
|
248
|
+
slider_name=self._label_text,
|
|
249
|
+
value_is_int=self.value_is_int,
|
|
250
|
+
min_val=self._min_value,
|
|
251
|
+
max_val=self._max_value,
|
|
252
|
+
val_fmt=self._val_fmt,
|
|
253
|
+
min_limit=self._min_limit,
|
|
254
|
+
max_limit=self._max_limit,
|
|
255
|
+
min_limit_inclusive=self._min_limit_inclusive,
|
|
256
|
+
max_limit_inclusive=self._max_limit_inclusive,
|
|
257
|
+
validate=self._check_new_config,
|
|
258
|
+
parent=self,
|
|
259
|
+
)
|
|
260
|
+
if dlg.exec() != QDialog.Accepted:
|
|
261
|
+
return
|
|
262
|
+
new_min, new_max, new_fmt = dlg.get_values()
|
|
263
|
+
|
|
264
|
+
# snapshot before set_format rounds anything
|
|
265
|
+
old_fmt = self._val_fmt
|
|
266
|
+
old_min, old_max = self._min_value, self._max_value
|
|
267
|
+
saved_cur_arr = self._current_arr.copy()
|
|
268
|
+
saved_dft_arr = self._default_arr.copy()
|
|
269
|
+
saved_cur, saved_dft = self._current_value, self._default_value
|
|
270
|
+
old_arr_values = self.get_arr_values()
|
|
271
|
+
old_value = self.get_value()
|
|
272
|
+
|
|
273
|
+
self.set_format(new_fmt)
|
|
274
|
+
new_min = self.evaluate_value(new_min)
|
|
275
|
+
new_max = self.evaluate_value(new_max)
|
|
276
|
+
|
|
277
|
+
# ±inf entries only hold a placeholder, so leave them out of the check
|
|
278
|
+
finite = np.array([sv is None for sv in self._current_special_arr])
|
|
279
|
+
cur_vals = self._current_arr[finite]
|
|
280
|
+
dft_vals = self._default_arr
|
|
281
|
+
|
|
282
|
+
cur_out = cur_vals.size > 0 and (cur_vals.min() < new_min or cur_vals.max() > new_max)
|
|
283
|
+
dft_out = dft_vals.min() < new_min or dft_vals.max() > new_max
|
|
284
|
+
|
|
285
|
+
if cur_out or dft_out:
|
|
286
|
+
self.set_border(SliderBorderState.ERROR)
|
|
287
|
+
try:
|
|
288
|
+
result = multi_slider_ask_clamp_value(
|
|
289
|
+
proposed_range=f"[{self._value_to_text(new_min)}, {self._value_to_text(new_max)}]",
|
|
290
|
+
cur_min=self._value_to_text(cur_vals.min()) if cur_vals.size else "-",
|
|
291
|
+
cur_max=self._value_to_text(cur_vals.max()) if cur_vals.size else "-",
|
|
292
|
+
dft_min=self._value_to_text(dft_vals.min()),
|
|
293
|
+
dft_max=self._value_to_text(dft_vals.max()),
|
|
294
|
+
parent=self,
|
|
295
|
+
)
|
|
296
|
+
finally:
|
|
297
|
+
self.set_border(SliderBorderState.OFF)
|
|
298
|
+
if result != AskResult.YES:
|
|
299
|
+
self.set_format(old_fmt)
|
|
300
|
+
self._current_arr, self._default_arr = saved_cur_arr, saved_dft_arr
|
|
301
|
+
self._current_value, self._default_value = saved_cur, saved_dft
|
|
302
|
+
self._min_value, self._max_value = old_min, old_max
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
self.set_range(new_min, new_max)
|
|
306
|
+
|
|
307
|
+
# report what the user's change did
|
|
308
|
+
if self.get_value() != old_value:
|
|
309
|
+
self.valueChanged.emit(self.get_value()) # handler also emits arrayChanged
|
|
310
|
+
elif not np.array_equal(self.get_arr_values(), old_arr_values):
|
|
311
|
+
self.arrayChanged.emit(self.get_arr_values())
|
|
312
|
+
if self._default_value != saved_dft:
|
|
313
|
+
self.defaultChanged.emit(self._default_value)
|
|
314
|
+
if (old_min, old_max, old_fmt) != (self._min_value, self._max_value, self._val_fmt):
|
|
315
|
+
self.configChanged.emit(self.get_config())
|
|
316
|
+
|
|
317
|
+
def _values_as_html(self) -> str:
|
|
318
|
+
"""Build an HTML table containing all current values.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
HTML string for a two-column table showing index and formatted value.
|
|
322
|
+
The active index is highlighted.
|
|
323
|
+
"""
|
|
324
|
+
rows = [
|
|
325
|
+
f"""
|
|
326
|
+
<tr>
|
|
327
|
+
<th style="padding:2px 12px 6px 0; text-align:left; border-bottom:1px solid #999;">Index</th>
|
|
328
|
+
<th style="padding:2px 0 6px 0; text-align:left; border-bottom:1px solid #999;">{self.html_label}</th>
|
|
329
|
+
</tr>
|
|
330
|
+
"""
|
|
331
|
+
]
|
|
332
|
+
for i in range(self._arr_length):
|
|
333
|
+
value_text = self._arr_value_text(i)
|
|
334
|
+
if i == self._current_idx:
|
|
335
|
+
rows.append(
|
|
336
|
+
f"""
|
|
337
|
+
<tr style="background-color:#eaf3ff;">
|
|
338
|
+
<td style="padding:2px 12px 2px 0; white-space:nowrap;" align="right">{i}:</td>
|
|
339
|
+
<td style="padding:2px 0; white-space:nowrap;"><b>{value_text}</b> <span style="color:#888;">← current</span></td>
|
|
340
|
+
</tr>
|
|
341
|
+
"""
|
|
342
|
+
)
|
|
343
|
+
else:
|
|
344
|
+
rows.append(
|
|
345
|
+
f"""
|
|
346
|
+
<tr>
|
|
347
|
+
<td style="padding:2px 12px 2px 0; white-space:nowrap;" align="right">{i}:</td>
|
|
348
|
+
<td style="padding:2px 0; white-space:nowrap;"><b>{value_text}</b></td>
|
|
349
|
+
</tr>
|
|
350
|
+
"""
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
return f"""
|
|
354
|
+
<table cellspacing="0" cellpadding="0">
|
|
355
|
+
{''.join(rows)}
|
|
356
|
+
</table>
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
def _open_show_values_dlg(self) -> None:
|
|
360
|
+
"""Open a read-only dialog showing the formatted values table."""
|
|
361
|
+
dlg = QDialog(self)
|
|
362
|
+
dlg.setWindowTitle("Values")
|
|
363
|
+
|
|
364
|
+
text_box = QTextEdit(dlg)
|
|
365
|
+
text_box.setReadOnly(True)
|
|
366
|
+
text_box.setAcceptRichText(True)
|
|
367
|
+
|
|
368
|
+
html = self._values_as_html()
|
|
369
|
+
text_box.setHtml(html)
|
|
370
|
+
|
|
371
|
+
btn_box = QDialogButtonBox(QDialogButtonBox.Ok, parent=dlg)
|
|
372
|
+
btn_box.accepted.connect(dlg.accept)
|
|
373
|
+
|
|
374
|
+
layout = QVBoxLayout(dlg)
|
|
375
|
+
layout.addWidget(text_box)
|
|
376
|
+
layout.addWidget(btn_box)
|
|
377
|
+
|
|
378
|
+
doc = text_box.document()
|
|
379
|
+
doc.adjustSize()
|
|
380
|
+
|
|
381
|
+
doc_w = int(doc.idealWidth())
|
|
382
|
+
frame_w = 2 * text_box.frameWidth()
|
|
383
|
+
margins = text_box.contentsMargins().left() + text_box.contentsMargins().right()
|
|
384
|
+
scrollbar_w = text_box.style().pixelMetric(QStyle.PM_ScrollBarExtent)
|
|
385
|
+
|
|
386
|
+
dlg_w = doc_w + frame_w + margins + scrollbar_w + 30
|
|
387
|
+
dlg_h = min(700, 140 + 22 * self._arr_length)
|
|
388
|
+
|
|
389
|
+
dlg.resize(dlg_w, dlg_h)
|
|
390
|
+
dlg.exec()
|
|
391
|
+
|
|
392
|
+
# ----- public API -----
|
|
393
|
+
def get_value_text(self, idx: int) -> str:
|
|
394
|
+
"""Return the formatted text for the value at a given index.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
idx: Array index. Wrapped modulo the current array length.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Formatted value text.
|
|
401
|
+
"""
|
|
402
|
+
idx %= self._arr_length
|
|
403
|
+
return self._arr_value_text(idx)
|
|
404
|
+
|
|
405
|
+
def set_format(self, val_fmt: str) -> None:
|
|
406
|
+
"""Update the display format and re-evaluate stored values.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
val_fmt: New display format string.
|
|
410
|
+
|
|
411
|
+
Note:
|
|
412
|
+
All stored current and default finite values are rounded to the
|
|
413
|
+
new format. No signals are emitted.
|
|
414
|
+
"""
|
|
415
|
+
super().set_format(val_fmt)
|
|
416
|
+
for i in range(self._arr_length):
|
|
417
|
+
self._current_arr[i] = self.evaluate_value(self._current_arr[i])
|
|
418
|
+
self._default_arr[i] = self.evaluate_value(self._default_arr[i])
|
|
419
|
+
|
|
420
|
+
def get_index(self) -> int:
|
|
421
|
+
"""Return the currently selected array index.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Current active index.
|
|
425
|
+
"""
|
|
426
|
+
return self._current_idx
|
|
427
|
+
|
|
428
|
+
def set_index(self, idx: int) -> None:
|
|
429
|
+
"""Select the active array index.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
idx: New index. Wrapped modulo the current array length.
|
|
433
|
+
"""
|
|
434
|
+
idx %= self._arr_length
|
|
435
|
+
self._current_idx = idx
|
|
436
|
+
self._update_slider_for_current_idx()
|
|
437
|
+
|
|
438
|
+
def get_arr_values(self) -> np.ndarray:
|
|
439
|
+
"""Return a copy of the current array values.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
NumPy array containing finite values or infinities that reflect the
|
|
443
|
+
stored special-value state.
|
|
444
|
+
"""
|
|
445
|
+
out = self._current_arr.copy()
|
|
446
|
+
for i, sv in enumerate(self._current_special_arr):
|
|
447
|
+
if sv == "+inf":
|
|
448
|
+
out[i] = math.inf
|
|
449
|
+
elif sv == "-inf":
|
|
450
|
+
out[i] = -math.inf
|
|
451
|
+
return out
|
|
452
|
+
|
|
453
|
+
def set_value(self, value, keep_default_value: bool = False) -> None:
|
|
454
|
+
"""Set the active value and store it in the array (no signals)."""
|
|
455
|
+
super().set_value(value, keep_default_value)
|
|
456
|
+
self._store_active()
|
|
457
|
+
|
|
458
|
+
def set_default_value(self, value: float) -> None:
|
|
459
|
+
"""Set the active default and store it in the array (no signals)."""
|
|
460
|
+
super().set_default_value(value)
|
|
461
|
+
self._store_active()
|
|
462
|
+
|
|
463
|
+
def set_arr_values(
|
|
464
|
+
self,
|
|
465
|
+
values: Sequence[float | str],
|
|
466
|
+
idx: int = 0,
|
|
467
|
+
default_preserve_mode: Literal["none", "first", "all"] = "none",
|
|
468
|
+
same_values_mode: Literal["ignore", "apply"] = "ignore",
|
|
469
|
+
) -> None:
|
|
470
|
+
"""Replace the stored current values array.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
values: New values. Entries may be real numbers or ``±inf``
|
|
474
|
+
markers; each must satisfy the slider's limits and features.
|
|
475
|
+
idx: Index to select after updating the array. Wrapped modulo the new
|
|
476
|
+
array length.
|
|
477
|
+
default_preserve_mode: Policy for preserving old default values.
|
|
478
|
+
|
|
479
|
+
- ``"none"``: Reset all default values from ``values``.
|
|
480
|
+
- ``"first"``: Preserve only the previous default at index 0.
|
|
481
|
+
- ``"all"``: Preserve all previous defaults where possible.
|
|
482
|
+
same_values_mode: Policy controlling interaction with same-values
|
|
483
|
+
mode.
|
|
484
|
+
|
|
485
|
+
- ``"ignore"``: Store ``values`` as provided.
|
|
486
|
+
- ``"apply"``: If same-values mode is enabled, force all entries
|
|
487
|
+
(current, default and ±inf state) to match the selected entry.
|
|
488
|
+
|
|
489
|
+
Note:
|
|
490
|
+
All entries are validated before anything changes. If the new
|
|
491
|
+
values extend beyond the current editable range, the range is
|
|
492
|
+
expanded to include them. No signals are emitted.
|
|
493
|
+
|
|
494
|
+
Raises:
|
|
495
|
+
ValueError: If ``values`` is empty, an entry violates the limits or
|
|
496
|
+
is a ±inf marker that is not permitted, or a mode is invalid.
|
|
497
|
+
TypeError: If an entry is neither a real number nor a ±inf marker.
|
|
498
|
+
"""
|
|
499
|
+
if default_preserve_mode not in {"none", "first", "all"}:
|
|
500
|
+
raise ValueError("default_preserve_mode must be one of: 'none', 'first', 'all'")
|
|
501
|
+
if same_values_mode not in {"ignore", "apply"}:
|
|
502
|
+
raise ValueError("same_values_mode must be one of: 'ignore', 'apply'")
|
|
503
|
+
|
|
504
|
+
N = len(values)
|
|
505
|
+
if N == 0:
|
|
506
|
+
raise ValueError("values must not be empty")
|
|
507
|
+
|
|
508
|
+
# validate everything before touching the widget
|
|
509
|
+
new_current: list[float] = []
|
|
510
|
+
new_special: list[SpecialValue | None] = []
|
|
511
|
+
for i, v in enumerate(values):
|
|
512
|
+
sv = normalize_special_value(v)
|
|
513
|
+
try:
|
|
514
|
+
if sv is None:
|
|
515
|
+
new_current.append(self._validate_value(v))
|
|
516
|
+
else:
|
|
517
|
+
self._check_special_permitted(sv)
|
|
518
|
+
new_current.append(self._max_value if sv == "+inf" else self._min_value)
|
|
519
|
+
except (TypeError, ValueError) as e:
|
|
520
|
+
raise type(e)(f"values[{i}]: {e}") from None
|
|
521
|
+
new_special.append(sv)
|
|
522
|
+
|
|
523
|
+
selected_idx = idx % N
|
|
524
|
+
cur_arr = np.asarray(new_current, dtype=float)
|
|
525
|
+
dft_arr = cur_arr.copy()
|
|
526
|
+
|
|
527
|
+
if default_preserve_mode == "first":
|
|
528
|
+
dft_arr[0] = self._default_arr[0]
|
|
529
|
+
elif default_preserve_mode == "all":
|
|
530
|
+
n_copy = min(len(self._default_arr), N)
|
|
531
|
+
dft_arr[:n_copy] = self._default_arr[:n_copy]
|
|
532
|
+
|
|
533
|
+
if same_values_mode == "apply" and self._use_same_values:
|
|
534
|
+
cur_arr[:] = cur_arr[selected_idx]
|
|
535
|
+
dft_arr[:] = dft_arr[selected_idx]
|
|
536
|
+
new_special = [new_special[selected_idx]] * N
|
|
537
|
+
|
|
538
|
+
# commit
|
|
539
|
+
self._arr_length = N
|
|
540
|
+
self._current_arr = cur_arr
|
|
541
|
+
self._default_arr = dft_arr
|
|
542
|
+
self._current_special_arr = new_special
|
|
543
|
+
self._min_value = min(self._min_value, float(cur_arr.min()))
|
|
544
|
+
self._max_value = max(self._max_value, float(cur_arr.max()))
|
|
545
|
+
|
|
546
|
+
self.set_index(selected_idx)
|
|
547
|
+
self._update_value_width()
|
|
548
|
+
|
|
549
|
+
def set_range(self, min_val: float, max_val: float) -> None:
|
|
550
|
+
"""Set the editable numeric range and clamp stored values if needed.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
min_val: New minimum value.
|
|
554
|
+
max_val: New maximum value.
|
|
555
|
+
|
|
556
|
+
Note:
|
|
557
|
+
Both the current and default arrays are clamped to the new range. The
|
|
558
|
+
underlying slider, labels, and layout are refreshed. No signals are
|
|
559
|
+
emitted.
|
|
560
|
+
|
|
561
|
+
Raises:
|
|
562
|
+
ValueError: If ``min_val >= max_val`` or either bound violates the
|
|
563
|
+
hard-limit constraints.
|
|
564
|
+
"""
|
|
565
|
+
new_min_val, new_max_val = self._validate_range(min_val, max_val)
|
|
566
|
+
self._min_value = new_min_val
|
|
567
|
+
self._max_value = new_max_val
|
|
568
|
+
for i in range(self._arr_length):
|
|
569
|
+
cur_val = self._current_arr[i]
|
|
570
|
+
dft_val = self._default_arr[i]
|
|
571
|
+
if cur_val < new_min_val:
|
|
572
|
+
self._current_arr[i] = new_min_val
|
|
573
|
+
elif cur_val > new_max_val:
|
|
574
|
+
self._current_arr[i] = new_max_val
|
|
575
|
+
if dft_val < new_min_val:
|
|
576
|
+
self._default_arr[i] = new_min_val
|
|
577
|
+
elif dft_val > new_max_val:
|
|
578
|
+
self._default_arr[i] = new_max_val
|
|
579
|
+
|
|
580
|
+
self._current_value = float(self._current_arr[self._current_idx])
|
|
581
|
+
self._default_value = float(self._default_arr[self._current_idx])
|
|
582
|
+
|
|
583
|
+
self._update_mapping_anchors()
|
|
584
|
+
with self._quiet_updates():
|
|
585
|
+
self._apply_range_to_slider()
|
|
586
|
+
self._slider.setValue(self._value_to_pos(self._current_value))
|
|
587
|
+
self._update_all_labels()
|
|
588
|
+
self._update_value_width()
|
|
589
|
+
self._update_layout()
|
|
590
|
+
|
|
591
|
+
def set_use_same_values(self, same: bool) -> None:
|
|
592
|
+
"""Enable or disable same-values mode.
|
|
593
|
+
|
|
594
|
+
When enabled, all entries take the active value, default and special
|
|
595
|
+
state. No signals are emitted.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
same: Whether all entries should track the currently selected value.
|
|
599
|
+
"""
|
|
600
|
+
self._use_same_values = bool(same)
|
|
601
|
+
if same:
|
|
602
|
+
self._store_active()
|
|
603
|
+
self._update_slider_for_current_idx()
|
|
604
|
+
|
|
605
|
+
def get_arr_length(self) -> int:
|
|
606
|
+
"""Return the number of stored values.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Length of the current/default arrays.
|
|
610
|
+
"""
|
|
611
|
+
return self._arr_length
|
|
612
|
+
|
|
613
|
+
def set_arr_length(self, arr_length: int) -> None:
|
|
614
|
+
"""Resize the stored arrays.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
arr_length: New array length. Must be greater than zero.
|
|
618
|
+
|
|
619
|
+
Note:
|
|
620
|
+
When extending the arrays, new entries are filled with the value,
|
|
621
|
+
default value and ±inf state from index 0. When shrinking, trailing
|
|
622
|
+
entries are discarded. If the current index becomes invalid after
|
|
623
|
+
shrinking, the last remaining index is selected. No signals are
|
|
624
|
+
emitted.
|
|
625
|
+
|
|
626
|
+
Raises:
|
|
627
|
+
ValueError: If ``arr_length <= 0``.
|
|
628
|
+
"""
|
|
629
|
+
if arr_length <= 0:
|
|
630
|
+
raise ValueError("arr_length must be > 0")
|
|
631
|
+
|
|
632
|
+
old_length = self._arr_length
|
|
633
|
+
if old_length == arr_length:
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
if old_length < arr_length:
|
|
637
|
+
fill_cur = self._current_arr[0]
|
|
638
|
+
fill_dft = self._default_arr[0]
|
|
639
|
+
fill_cur_sv = self._current_special_arr[0]
|
|
640
|
+
|
|
641
|
+
n_add = arr_length - old_length
|
|
642
|
+
self._current_arr = np.concatenate([
|
|
643
|
+
self._current_arr,
|
|
644
|
+
np.full(n_add, fill_cur, dtype=self._current_arr.dtype),
|
|
645
|
+
])
|
|
646
|
+
self._default_arr = np.concatenate([
|
|
647
|
+
self._default_arr,
|
|
648
|
+
np.full(n_add, fill_dft, dtype=self._default_arr.dtype),
|
|
649
|
+
])
|
|
650
|
+
self._current_special_arr.extend([fill_cur_sv] * n_add)
|
|
651
|
+
else:
|
|
652
|
+
self._current_arr = self._current_arr[:arr_length]
|
|
653
|
+
self._default_arr = self._default_arr[:arr_length]
|
|
654
|
+
self._current_special_arr = self._current_special_arr[:arr_length]
|
|
655
|
+
|
|
656
|
+
self._arr_length = arr_length
|
|
657
|
+
if self._current_idx >= self._arr_length:
|
|
658
|
+
self._current_idx = self._arr_length - 1
|
|
659
|
+
self._update_slider_for_current_idx()
|
|
660
|
+
self._update_value_width()
|