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.
@@ -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;">&larr; 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()