bec-widgets 2.8.3__py3-none-any.whl → 2.9.0__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.
Files changed (25) hide show
  1. CHANGELOG.md +37 -0
  2. PKG-INFO +1 -1
  3. bec_widgets/cli/client.py +129 -0
  4. bec_widgets/utils/bec_dispatcher.py +6 -4
  5. bec_widgets/utils/crosshair.py +72 -9
  6. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +13 -12
  7. bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +11 -5
  8. bec_widgets/widgets/plots/image/image.py +2 -0
  9. bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +2 -0
  10. bec_widgets/widgets/plots/plot_base.py +27 -1
  11. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +2 -0
  12. bec_widgets/widgets/plots/setting_menus/axis_settings.py +3 -0
  13. bec_widgets/widgets/plots/setting_menus/axis_settings_horizontal.ui +111 -88
  14. bec_widgets/widgets/plots/setting_menus/axis_settings_vertical.ui +74 -43
  15. bec_widgets/widgets/plots/waveform/waveform.py +2 -0
  16. bec_widgets/widgets/utility/signal_label/register_signal_label.py +15 -0
  17. bec_widgets/widgets/utility/signal_label/signal_label.py +456 -0
  18. bec_widgets/widgets/utility/signal_label/signal_label.pyproject +1 -0
  19. bec_widgets/widgets/utility/signal_label/signal_label_plugin.py +54 -0
  20. {bec_widgets-2.8.3.dist-info → bec_widgets-2.9.0.dist-info}/METADATA +1 -1
  21. {bec_widgets-2.8.3.dist-info → bec_widgets-2.9.0.dist-info}/RECORD +25 -21
  22. pyproject.toml +1 -1
  23. {bec_widgets-2.8.3.dist-info → bec_widgets-2.9.0.dist-info}/WHEEL +0 -0
  24. {bec_widgets-2.8.3.dist-info → bec_widgets-2.9.0.dist-info}/entry_points.txt +0 -0
  25. {bec_widgets-2.8.3.dist-info → bec_widgets-2.9.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md CHANGED
@@ -1,6 +1,43 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v2.9.0 (2025-05-30)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **DeviceSignalInput**: Improve robustness
9
+ ([`91195ae`](https://github.com/bec-project/bec_widgets/commit/91195ae0fdf024daf2daaa4ea2963992b4e40e04))
10
+
11
+ use set for storing filter properties to allow multiple set to true or false
12
+
13
+ ### Code Style
14
+
15
+ - Typing in bec_dispatcher
16
+ ([`a6c5c21`](https://github.com/bec-project/bec_widgets/commit/a6c5c21afaa6dcf33ce71027e8730354ee34e3b4))
17
+
18
+ ### Documentation
19
+
20
+ - Add usage docs for signal label widget
21
+ ([`2b9919b`](https://github.com/bec-project/bec_widgets/commit/2b9919bb34a66708f4b910ffc17dc253e9b7f70d))
22
+
23
+ ### Features
24
+
25
+ - (#569) add signal label widget
26
+ ([`822e7d0`](https://github.com/bec-project/bec_widgets/commit/822e7d06ff7479d006ae99942fed5e2c836831ce))
27
+
28
+ add a widget which shows the current value of a signal from BEC. configurable with many properties
29
+ in designer. intended for use mainly in static GUIs.
30
+
31
+
32
+ ## v2.8.4 (2025-05-30)
33
+
34
+ ### Bug Fixes
35
+
36
+ - **crosshair**: Label decimal precision is dynamically scaled with the plot zoom; API of all
37
+ affected widgets adjusted; option added to PlotBase; closes #637
38
+ ([`c8128fa`](https://github.com/bec-project/bec_widgets/commit/c8128faf79c43487921aada9dbf1869ef5bda93c))
39
+
40
+
4
41
  ## v2.8.3 (2025-05-30)
5
42
 
6
43
  ### Bug Fixes
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.8.3
3
+ Version: 2.9.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
bec_widgets/cli/client.py CHANGED
@@ -52,6 +52,7 @@ _Widgets = {
52
52
  "ScanControl": "ScanControl",
53
53
  "ScatterWaveform": "ScatterWaveform",
54
54
  "SignalComboBox": "SignalComboBox",
55
+ "SignalLabel": "SignalLabel",
55
56
  "SignalLineEdit": "SignalLineEdit",
56
57
  "StopButton": "StopButton",
57
58
  "TextBox": "TextBox",
@@ -1221,6 +1222,20 @@ class Image(RPCBase):
1221
1222
  Set auto range for the y-axis.
1222
1223
  """
1223
1224
 
1225
+ @property
1226
+ @rpc_call
1227
+ def minimal_crosshair_precision(self) -> "int":
1228
+ """
1229
+ Minimum decimal places for crosshair when dynamic precision is enabled.
1230
+ """
1231
+
1232
+ @minimal_crosshair_precision.setter
1233
+ @rpc_call
1234
+ def minimal_crosshair_precision(self) -> "int":
1235
+ """
1236
+ Minimum decimal places for crosshair when dynamic precision is enabled.
1237
+ """
1238
+
1224
1239
  @property
1225
1240
  @rpc_call
1226
1241
  def color_map(self) -> "str":
@@ -2350,6 +2365,20 @@ class MultiWaveform(RPCBase):
2350
2365
  The font size of the legend font.
2351
2366
  """
2352
2367
 
2368
+ @property
2369
+ @rpc_call
2370
+ def minimal_crosshair_precision(self) -> "int":
2371
+ """
2372
+ Minimum decimal places for crosshair when dynamic precision is enabled.
2373
+ """
2374
+
2375
+ @minimal_crosshair_precision.setter
2376
+ @rpc_call
2377
+ def minimal_crosshair_precision(self) -> "int":
2378
+ """
2379
+ Minimum decimal places for crosshair when dynamic precision is enabled.
2380
+ """
2381
+
2353
2382
  @property
2354
2383
  @rpc_call
2355
2384
  def highlighted_index(self):
@@ -3315,6 +3344,20 @@ class ScatterWaveform(RPCBase):
3315
3344
  The font size of the legend font.
3316
3345
  """
3317
3346
 
3347
+ @property
3348
+ @rpc_call
3349
+ def minimal_crosshair_precision(self) -> "int":
3350
+ """
3351
+ Minimum decimal places for crosshair when dynamic precision is enabled.
3352
+ """
3353
+
3354
+ @minimal_crosshair_precision.setter
3355
+ @rpc_call
3356
+ def minimal_crosshair_precision(self) -> "int":
3357
+ """
3358
+ Minimum decimal places for crosshair when dynamic precision is enabled.
3359
+ """
3360
+
3318
3361
  @property
3319
3362
  @rpc_call
3320
3363
  def main_curve(self) -> "ScatterCurve":
@@ -3417,6 +3460,78 @@ class SignalComboBox(RPCBase):
3417
3460
  """
3418
3461
 
3419
3462
 
3463
+ class SignalLabel(RPCBase):
3464
+ @property
3465
+ @rpc_call
3466
+ def custom_label(self) -> "str":
3467
+ """
3468
+ Use a cusom label rather than the signal name
3469
+ """
3470
+
3471
+ @property
3472
+ @rpc_call
3473
+ def custom_units(self) -> "str":
3474
+ """
3475
+ Use a custom unit string
3476
+ """
3477
+
3478
+ @custom_label.setter
3479
+ @rpc_call
3480
+ def custom_label(self) -> "str":
3481
+ """
3482
+ Use a cusom label rather than the signal name
3483
+ """
3484
+
3485
+ @custom_units.setter
3486
+ @rpc_call
3487
+ def custom_units(self) -> "str":
3488
+ """
3489
+ Use a custom unit string
3490
+ """
3491
+
3492
+ @property
3493
+ @rpc_call
3494
+ def decimal_places(self) -> "int":
3495
+ """
3496
+ Format to a given number of decimal_places. Set to 0 to disable.
3497
+ """
3498
+
3499
+ @decimal_places.setter
3500
+ @rpc_call
3501
+ def decimal_places(self) -> "int":
3502
+ """
3503
+ Format to a given number of decimal_places. Set to 0 to disable.
3504
+ """
3505
+
3506
+ @property
3507
+ @rpc_call
3508
+ def show_default_units(self) -> "bool":
3509
+ """
3510
+ Show default units obtained from the signal alongside it
3511
+ """
3512
+
3513
+ @show_default_units.setter
3514
+ @rpc_call
3515
+ def show_default_units(self) -> "bool":
3516
+ """
3517
+ Show default units obtained from the signal alongside it
3518
+ """
3519
+
3520
+ @property
3521
+ @rpc_call
3522
+ def show_select_button(self) -> "bool":
3523
+ """
3524
+ Show the button to select the signal to display
3525
+ """
3526
+
3527
+ @show_select_button.setter
3528
+ @rpc_call
3529
+ def show_select_button(self) -> "bool":
3530
+ """
3531
+ Show the button to select the signal to display
3532
+ """
3533
+
3534
+
3420
3535
  class SignalLineEdit(RPCBase):
3421
3536
  """Line edit widget for device input with autocomplete for device names."""
3422
3537
 
@@ -3789,6 +3904,20 @@ class Waveform(RPCBase):
3789
3904
  The font size of the legend font.
3790
3905
  """
3791
3906
 
3907
+ @property
3908
+ @rpc_call
3909
+ def minimal_crosshair_precision(self) -> "int":
3910
+ """
3911
+ Minimum decimal places for crosshair when dynamic precision is enabled.
3912
+ """
3913
+
3914
+ @minimal_crosshair_precision.setter
3915
+ @rpc_call
3916
+ def minimal_crosshair_precision(self) -> "int":
3917
+ """
3918
+ Minimum decimal places for crosshair when dynamic precision is enabled.
3919
+ """
3920
+
3792
3921
  @property
3793
3922
  @rpc_call
3794
3923
  def curves(self) -> "list[Curve]":
@@ -163,7 +163,7 @@ class BECDispatcher:
163
163
  def connect_slot(
164
164
  self,
165
165
  slot: Callable,
166
- topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]],
166
+ topics: EndpointInfo | str | list[EndpointInfo] | list[str],
167
167
  cb_info: dict | None = None,
168
168
  **kwargs,
169
169
  ) -> None:
@@ -172,7 +172,7 @@ class BECDispatcher:
172
172
  Args:
173
173
  slot (Callable): A slot method/function that accepts two inputs: content and metadata of
174
174
  the corresponding pub/sub message
175
- topics (EndpointInfo | str | list): A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
175
+ topics EndpointInfo | str | list[EndpointInfo] | list[str]: A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
176
176
  cb_info (dict | None): A dictionary containing information about the callback. Defaults to None.
177
177
  """
178
178
  qt_slot = QtThreadSafeCallback(cb=slot, cb_info=cb_info)
@@ -183,13 +183,15 @@ class BECDispatcher:
183
183
  topics_str, _ = self.client.connector._convert_endpointinfo(topics)
184
184
  qt_slot.topics.update(set(topics_str))
185
185
 
186
- def disconnect_slot(self, slot: Callable, topics: Union[str, list]):
186
+ def disconnect_slot(
187
+ self, slot: Callable, topics: EndpointInfo | str | list[EndpointInfo] | list[str]
188
+ ):
187
189
  """
188
190
  Disconnect a slot from a topic.
189
191
 
190
192
  Args:
191
193
  slot(Callable): The slot to disconnect
192
- topics(Union[str, list]): The topic(s) to disconnect from
194
+ topics EndpointInfo | str | list[EndpointInfo] | list[str]: A topic or list of topics to unsub from.
193
195
  """
194
196
  # find the right slot to disconnect from ;
195
197
  # slot callbacks are wrapped in QtThreadSafeCallback objects,
@@ -34,13 +34,21 @@ class Crosshair(QObject):
34
34
  coordinatesChanged2D = Signal(tuple)
35
35
  coordinatesClicked2D = Signal(tuple)
36
36
 
37
- def __init__(self, plot_item: pg.PlotItem, precision: int = 3, parent=None):
37
+ def __init__(
38
+ self,
39
+ plot_item: pg.PlotItem,
40
+ precision: int | None = None,
41
+ *,
42
+ min_precision: int = 2,
43
+ parent=None,
44
+ ):
38
45
  """
39
46
  Crosshair for 1D and 2D plots.
40
47
 
41
48
  Args:
42
49
  plot_item (pyqtgraph.PlotItem): The plot item to which the crosshair will be attached.
43
- precision (int, optional): Number of decimal places to round the coordinates to. Defaults to None.
50
+ precision (int | None, optional): Fixed number of decimal places to display. If *None*, precision is chosen dynamically from the current view range.
51
+ min_precision (int, optional): The lower bound (in decimal places) used when dynamic precision is enabled. Defaults to 2.
44
52
  parent (QObject, optional): Parent object for the QObject. Defaults to None.
45
53
  """
46
54
  super().__init__(parent)
@@ -48,7 +56,9 @@ class Crosshair(QObject):
48
56
  self.is_log_x = None
49
57
  self.is_derivative = None
50
58
  self.plot_item = plot_item
51
- self.precision = precision
59
+ self._precision = precision
60
+ self._min_precision = max(0, int(min_precision)) # ensure non‑negative
61
+
52
62
  self.v_line = pg.InfiniteLine(angle=90, movable=False)
53
63
  self.v_line.skip_auto_range = True
54
64
  self.h_line = pg.InfiniteLine(angle=0, movable=False)
@@ -93,6 +103,56 @@ class Crosshair(QObject):
93
103
 
94
104
  self._connect_to_theme_change()
95
105
 
106
+ @property
107
+ def precision(self) -> int | None:
108
+ """Fixed number of decimals; ``None`` enables dynamic mode."""
109
+ return self._precision
110
+
111
+ @precision.setter
112
+ def precision(self, value: int | None):
113
+ """
114
+ Set the fixed number of decimals to display.
115
+
116
+ Args:
117
+ value(int | None): The number of decimals to display. If `None`, dynamic precision is used based on the view range.
118
+ """
119
+ self._precision = value
120
+
121
+ @property
122
+ def min_precision(self) -> int:
123
+ """Lower bound on decimals when dynamic precision is used."""
124
+ return self._min_precision
125
+
126
+ @min_precision.setter
127
+ def min_precision(self, value: int):
128
+ """
129
+ Set the lower bound on decimals when dynamic precision is used.
130
+
131
+ Args:
132
+ value(int): The minimum number of decimals to display. Must be non-negative.
133
+ """
134
+ self._min_precision = max(0, int(value))
135
+
136
+ def _current_precision(self) -> int:
137
+ """
138
+ Get the current precision based on the view range or fixed precision.
139
+ """
140
+ if self._precision is not None:
141
+ return self._precision
142
+
143
+ # Dynamically choose precision from the smaller visible span
144
+ view_range = self.plot_item.vb.viewRange()
145
+ x_span = abs(view_range[0][1] - view_range[0][0])
146
+ y_span = abs(view_range[1][1] - view_range[1][0])
147
+
148
+ # Ignore zero spans that can appear during initialisation
149
+ spans = [s for s in (x_span, y_span) if s > 0]
150
+ span = min(spans) if spans else 1.0
151
+
152
+ exponent = np.floor(np.log10(span)) # order of magnitude
153
+ decimals = max(0, int(-exponent) + 1)
154
+ return max(self._min_precision, decimals)
155
+
96
156
  def _connect_to_theme_change(self):
97
157
  """Connect to the theme change signal."""
98
158
  qapp = QApplication.instance()
@@ -324,6 +384,7 @@ class Crosshair(QObject):
324
384
  # not sure how we got here, but just to be safe...
325
385
  return
326
386
 
387
+ precision = self._current_precision()
327
388
  for item in self.items:
328
389
  if isinstance(item, pg.PlotDataItem):
329
390
  name = item.name() or str(id(item))
@@ -334,8 +395,8 @@ class Crosshair(QObject):
334
395
  x_snapped_scaled, y_snapped_scaled = self.scale_emitted_coordinates(x, y)
335
396
  coordinate_to_emit = (
336
397
  name,
337
- round(x_snapped_scaled, self.precision),
338
- round(y_snapped_scaled, self.precision),
398
+ round(x_snapped_scaled, precision),
399
+ round(y_snapped_scaled, precision),
339
400
  )
340
401
  self.coordinatesChanged1D.emit(coordinate_to_emit)
341
402
  elif isinstance(item, pg.ImageItem):
@@ -380,6 +441,7 @@ class Crosshair(QObject):
380
441
  # not sure how we got here, but just to be safe...
381
442
  return
382
443
 
444
+ precision = self._current_precision()
383
445
  for item in self.items:
384
446
  if isinstance(item, pg.PlotDataItem):
385
447
  name = item.name() or str(id(item))
@@ -391,8 +453,8 @@ class Crosshair(QObject):
391
453
  x_snapped_scaled, y_snapped_scaled = self.scale_emitted_coordinates(x, y)
392
454
  coordinate_to_emit = (
393
455
  name,
394
- round(x_snapped_scaled, self.precision),
395
- round(y_snapped_scaled, self.precision),
456
+ round(x_snapped_scaled, precision),
457
+ round(y_snapped_scaled, precision),
396
458
  )
397
459
  self.coordinatesClicked1D.emit(coordinate_to_emit)
398
460
  elif isinstance(item, pg.ImageItem):
@@ -443,7 +505,8 @@ class Crosshair(QObject):
443
505
  """
444
506
  x, y = pos
445
507
  x_scaled, y_scaled = self.scale_emitted_coordinates(x, y)
446
- text = f"({x_scaled:.{self.precision}g}, {y_scaled:.{self.precision}g})"
508
+ precision = self._current_precision()
509
+ text = f"({x_scaled:.{precision}f}, {y_scaled:.{precision}f})"
447
510
  for item in self.items:
448
511
  if isinstance(item, pg.ImageItem):
449
512
  image = item.image
@@ -452,7 +515,7 @@ class Crosshair(QObject):
452
515
  ix = int(np.clip(x, 0, image.shape[0] - 1))
453
516
  iy = int(np.clip(y, 0, image.shape[1] - 1))
454
517
  intensity = image[ix, iy]
455
- text += f"\nIntensity: {intensity:.{self.precision}g}"
518
+ text += f"\nIntensity: {intensity:.{precision}f}"
456
519
  break
457
520
  # Update coordinate label
458
521
  self.coord_label.setText(text)
@@ -1,10 +1,11 @@
1
1
  from bec_lib.callback_handler import EventType
2
2
  from bec_lib.device import Signal
3
3
  from bec_lib.logger import bec_logger
4
- from qtpy.QtCore import Property, Slot
4
+ from qtpy.QtCore import Property
5
5
 
6
6
  from bec_widgets.utils import ConnectionConfig
7
7
  from bec_widgets.utils.bec_widget import BECWidget
8
+ from bec_widgets.utils.error_popups import SafeSlot
8
9
  from bec_widgets.utils.filter_io import FilterIO
9
10
  from bec_widgets.utils.ophyd_kind_util import Kind
10
11
  from bec_widgets.utils.widget_io import WidgetIO
@@ -49,7 +50,7 @@ class DeviceSignalInputBase(BECWidget):
49
50
 
50
51
  self._device = None
51
52
  self.get_bec_shortcuts()
52
- self._signal_filter = []
53
+ self._signal_filter = set()
53
54
  self._signals = []
54
55
  self._hinted_signals = []
55
56
  self._normal_signals = []
@@ -60,7 +61,7 @@ class DeviceSignalInputBase(BECWidget):
60
61
 
61
62
  ### Qt Slots ###
62
63
 
63
- @Slot(str)
64
+ @SafeSlot(str)
64
65
  def set_signal(self, signal: str):
65
66
  """
66
67
  Set the signal.
@@ -76,7 +77,7 @@ class DeviceSignalInputBase(BECWidget):
76
77
  f"Signal {signal} not found for device {self.device} and filtered selection {self.signal_filter}."
77
78
  )
78
79
 
79
- @Slot(str)
80
+ @SafeSlot(str)
80
81
  def set_device(self, device: str | None):
81
82
  """
82
83
  Set the device. If device is not valid, device will be set to None which happens
@@ -90,8 +91,8 @@ class DeviceSignalInputBase(BECWidget):
90
91
  self._device = device
91
92
  self.update_signals_from_filters()
92
93
 
93
- @Slot(dict, dict)
94
- @Slot()
94
+ @SafeSlot(dict, dict)
95
+ @SafeSlot()
95
96
  def update_signals_from_filters(
96
97
  self, content: dict | None = None, metadata: dict | None = None
97
98
  ):
@@ -158,9 +159,9 @@ class DeviceSignalInputBase(BECWidget):
158
159
  @include_hinted_signals.setter
159
160
  def include_hinted_signals(self, value: bool):
160
161
  if value:
161
- self._signal_filter.append(Kind.hinted)
162
+ self._signal_filter.add(Kind.hinted)
162
163
  else:
163
- self._signal_filter.remove(Kind.hinted)
164
+ self._signal_filter.discard(Kind.hinted)
164
165
  self.update_signals_from_filters()
165
166
 
166
167
  @Property(bool)
@@ -171,9 +172,9 @@ class DeviceSignalInputBase(BECWidget):
171
172
  @include_normal_signals.setter
172
173
  def include_normal_signals(self, value: bool):
173
174
  if value:
174
- self._signal_filter.append(Kind.normal)
175
+ self._signal_filter.add(Kind.normal)
175
176
  else:
176
- self._signal_filter.remove(Kind.normal)
177
+ self._signal_filter.discard(Kind.normal)
177
178
  self.update_signals_from_filters()
178
179
 
179
180
  @Property(bool)
@@ -184,9 +185,9 @@ class DeviceSignalInputBase(BECWidget):
184
185
  @include_config_signals.setter
185
186
  def include_config_signals(self, value: bool):
186
187
  if value:
187
- self._signal_filter.append(Kind.config)
188
+ self._signal_filter.add(Kind.config)
188
189
  else:
189
- self._signal_filter.remove(Kind.config)
190
+ self._signal_filter.discard(Kind.config)
190
191
  self.update_signals_from_filters()
191
192
 
192
193
  ### Properties and Methods ###
@@ -1,11 +1,13 @@
1
1
  from bec_lib.device import Positioner
2
- from qtpy.QtCore import QSize, Signal, Slot
2
+ from qtpy.QtCore import QSize, Signal
3
3
  from qtpy.QtWidgets import QComboBox, QSizePolicy
4
4
 
5
+ from bec_widgets.utils.error_popups import SafeSlot
5
6
  from bec_widgets.utils.filter_io import ComboBoxFilterHandler, FilterIO
6
7
  from bec_widgets.utils.ophyd_kind_util import Kind
7
8
  from bec_widgets.widgets.control.device_input.base_classes.device_signal_input_base import (
8
9
  DeviceSignalInputBase,
10
+ DeviceSignalInputBaseConfig,
9
11
  )
10
12
 
11
13
 
@@ -35,7 +37,7 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
35
37
  self,
36
38
  parent=None,
37
39
  client=None,
38
- config: DeviceSignalInputBase = None,
40
+ config: DeviceSignalInputBaseConfig | None = None,
39
41
  gui_id: str | None = None,
40
42
  device: str | None = None,
41
43
  signal_filter: str | list[str] | None = None,
@@ -65,9 +67,13 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
65
67
  if default is not None:
66
68
  self.set_signal(default)
67
69
 
68
- def update_signals_from_filters(self):
70
+ @SafeSlot()
71
+ @SafeSlot(dict, dict)
72
+ def update_signals_from_filters(
73
+ self, content: dict | None = None, metadata: dict | None = None
74
+ ):
69
75
  """Update the filters for the combobox"""
70
- super().update_signals_from_filters()
76
+ super().update_signals_from_filters(content, metadata)
71
77
  # pylint: disable=protected-access
72
78
  if FilterIO._find_handler(self) is ComboBoxFilterHandler:
73
79
  if len(self._config_signals) > 0:
@@ -84,7 +90,7 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
84
90
  self.insertItem(0, "Hinted Signals")
85
91
  self.model().item(0).setEnabled(False)
86
92
 
87
- @Slot(str)
93
+ @SafeSlot(str)
88
94
  def on_text_changed(self, text: str):
89
95
  """Slot for text changed. If a device is selected and the signal is changed and valid it emits a signal.
90
96
  For a positioner, the readback value has to be renamed to the device name.
@@ -88,6 +88,8 @@ class Image(PlotBase):
88
88
  "auto_range_x.setter",
89
89
  "auto_range_y",
90
90
  "auto_range_y.setter",
91
+ "minimal_crosshair_precision",
92
+ "minimal_crosshair_precision.setter",
91
93
  # ImageView Specific Settings
92
94
  "color_map",
93
95
  "color_map.setter",
@@ -91,6 +91,8 @@ class MultiWaveform(PlotBase):
91
91
  "y_log.setter",
92
92
  "legend_label_size",
93
93
  "legend_label_size.setter",
94
+ "minimal_crosshair_precision",
95
+ "minimal_crosshair_precision.setter",
94
96
  # MultiWaveform Specific RPC Access
95
97
  "highlighted_index",
96
98
  "highlighted_index.setter",
@@ -116,6 +116,7 @@ class PlotBase(BECWidget, QWidget):
116
116
  self._user_y_label = ""
117
117
  self._y_label_suffix = ""
118
118
  self._y_axis_units = ""
119
+ self._minimal_crosshair_precision = 3
119
120
 
120
121
  # Plot Indicator Items
121
122
  self.tick_item = BECTickItem(parent=self, plot_item=self.plot_item)
@@ -978,7 +979,9 @@ class PlotBase(BECWidget, QWidget):
978
979
  def hook_crosshair(self) -> None:
979
980
  """Hook the crosshair to all plots."""
980
981
  if self.crosshair is None:
981
- self.crosshair = Crosshair(self.plot_item, precision=3)
982
+ self.crosshair = Crosshair(
983
+ self.plot_item, min_precision=self._minimal_crosshair_precision
984
+ )
982
985
  self.crosshair.crosshairChanged.connect(self.crosshair_position_changed)
983
986
  self.crosshair.crosshairClicked.connect(self.crosshair_position_clicked)
984
987
  self.crosshair.coordinatesChanged1D.connect(self.crosshair_coordinates_changed)
@@ -1006,6 +1009,29 @@ class PlotBase(BECWidget, QWidget):
1006
1009
 
1007
1010
  self.unhook_crosshair()
1008
1011
 
1012
+ @SafeProperty(
1013
+ int, doc="Minimum decimal places for crosshair when dynamic precision is enabled."
1014
+ )
1015
+ def minimal_crosshair_precision(self) -> int:
1016
+ """
1017
+ Minimum decimal places for crosshair when dynamic precision is enabled.
1018
+ """
1019
+ return self._minimal_crosshair_precision
1020
+
1021
+ @minimal_crosshair_precision.setter
1022
+ def minimal_crosshair_precision(self, value: int):
1023
+ """
1024
+ Set the minimum decimal places for crosshair when dynamic precision is enabled.
1025
+
1026
+ Args:
1027
+ value(int): The minimum decimal places to set.
1028
+ """
1029
+ value_int = max(0, int(value))
1030
+ self._minimal_crosshair_precision = value_int
1031
+ if self.crosshair is not None:
1032
+ self.crosshair.min_precision = value_int
1033
+ self.property_changed.emit("minimal_crosshair_precision", value_int)
1034
+
1009
1035
  @SafeSlot()
1010
1036
  def reset(self) -> None:
1011
1037
  """Reset the plot widget."""
@@ -82,6 +82,8 @@ class ScatterWaveform(PlotBase):
82
82
  "y_log.setter",
83
83
  "legend_label_size",
84
84
  "legend_label_size.setter",
85
+ "minimal_crosshair_precision",
86
+ "minimal_crosshair_precision.setter",
85
87
  # Scatter Waveform Specific RPC Access
86
88
  "main_curve",
87
89
  "color_map",
@@ -60,6 +60,7 @@ class AxisSettings(SettingWidget):
60
60
  self.ui.y_grid,
61
61
  self.ui.inner_axes,
62
62
  self.ui.outer_axes,
63
+ self.ui.minimal_crosshair_precision,
63
64
  ]:
64
65
  WidgetIO.connect_widget_change_signal(widget, self.set_property)
65
66
 
@@ -121,6 +122,7 @@ class AxisSettings(SettingWidget):
121
122
  self.ui.y_max,
122
123
  self.ui.y_log,
123
124
  self.ui.y_grid,
125
+ self.ui.minimal_crosshair_precision,
124
126
  ]:
125
127
  property_name = widget.objectName()
126
128
  value = getattr(self.target_widget, property_name)
@@ -144,6 +146,7 @@ class AxisSettings(SettingWidget):
144
146
  self.ui.y_grid,
145
147
  self.ui.outer_axes,
146
148
  self.ui.inner_axes,
149
+ self.ui.minimal_crosshair_precision,
147
150
  ]:
148
151
  property_name = widget.objectName()
149
152
  value = WidgetIO.get_value(widget)