bec-widgets 1.0.1__py3-none-any.whl → 1.1.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 (36) hide show
  1. CHANGELOG.md +26 -24
  2. PKG-INFO +1 -1
  3. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +60 -84
  4. bec_widgets/cli/client.py +56 -0
  5. bec_widgets/tests/__init__.py +0 -0
  6. bec_widgets/tests/utils.py +226 -0
  7. bec_widgets/utils/filter_io.py +156 -0
  8. bec_widgets/utils/widget_io.py +12 -9
  9. bec_widgets/widgets/base_classes/device_input_base.py +331 -62
  10. bec_widgets/widgets/base_classes/device_signal_input_base.py +280 -0
  11. bec_widgets/widgets/dap_combo_box/dap_combo_box_plugin.py +1 -1
  12. bec_widgets/widgets/device_combobox/device_combo_box_plugin.py +1 -1
  13. bec_widgets/widgets/device_combobox/device_combobox.py +118 -41
  14. bec_widgets/widgets/device_line_edit/device_line_edit.py +122 -59
  15. bec_widgets/widgets/device_line_edit/device_line_edit_plugin.py +1 -1
  16. bec_widgets/widgets/image/image_widget.py +7 -1
  17. bec_widgets/widgets/motor_map/motor_map_widget.py +4 -2
  18. bec_widgets/widgets/positioner_box/positioner_box.py +4 -1
  19. bec_widgets/widgets/scan_control/scan_control.py +2 -3
  20. bec_widgets/widgets/scan_control/scan_group_box.py +3 -1
  21. bec_widgets/widgets/signal_combobox/__init__.py +0 -0
  22. bec_widgets/widgets/signal_combobox/register_signal_combobox.py +15 -0
  23. bec_widgets/widgets/signal_combobox/signal_combobox.py +115 -0
  24. bec_widgets/widgets/signal_combobox/signal_combobox.pyproject +1 -0
  25. bec_widgets/widgets/signal_combobox/signal_combobox_plugin.py +54 -0
  26. bec_widgets/widgets/signal_line_edit/__init__.py +0 -0
  27. bec_widgets/widgets/signal_line_edit/register_signal_line_edit.py +15 -0
  28. bec_widgets/widgets/signal_line_edit/signal_line_edit.py +140 -0
  29. bec_widgets/widgets/signal_line_edit/signal_line_edit.pyproject +1 -0
  30. bec_widgets/widgets/signal_line_edit/signal_line_edit_plugin.py +54 -0
  31. {bec_widgets-1.0.1.dist-info → bec_widgets-1.1.0.dist-info}/METADATA +1 -1
  32. {bec_widgets-1.0.1.dist-info → bec_widgets-1.1.0.dist-info}/RECORD +36 -22
  33. pyproject.toml +1 -1
  34. {bec_widgets-1.0.1.dist-info → bec_widgets-1.1.0.dist-info}/WHEEL +0 -0
  35. {bec_widgets-1.0.1.dist-info → bec_widgets-1.1.0.dist-info}/entry_points.txt +0 -0
  36. {bec_widgets-1.0.1.dist-info → bec_widgets-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,280 @@
1
+ from bec_lib.callback_handler import EventType
2
+ from bec_lib.device import Signal
3
+ from bec_lib.logger import bec_logger
4
+ from ophyd import Kind
5
+ from qtpy.QtCore import Property, Slot
6
+
7
+ from bec_widgets.utils import ConnectionConfig
8
+ from bec_widgets.utils.bec_widget import BECWidget
9
+ from bec_widgets.utils.filter_io import FilterIO
10
+ from bec_widgets.utils.widget_io import WidgetIO
11
+
12
+ logger = bec_logger.logger
13
+
14
+
15
+ class DeviceSignalInputBaseConfig(ConnectionConfig):
16
+ """Configuration class for DeviceSignalInputBase."""
17
+
18
+ signal_filter: str | list[str] | None = None
19
+ default: str | None = None
20
+ arg_name: str | None = None
21
+ device: str | None = None
22
+ signals: list[str] | None = None
23
+
24
+
25
+ class DeviceSignalInputBase(BECWidget):
26
+ """
27
+ Mixin base class for device signal input widgets.
28
+ Mixin class for device signal input widgets. This class provides methods to get the device signal list and device
29
+ signal object based on the current text of the widget.
30
+ """
31
+
32
+ _filter_handler = {
33
+ Kind.hinted: "include_hinted_signals",
34
+ Kind.normal: "include_normal_signals",
35
+ Kind.config: "include_config_signals",
36
+ }
37
+
38
+ def __init__(self, client=None, config=None, gui_id: str = None):
39
+ if config is None:
40
+ config = DeviceSignalInputBaseConfig(widget_class=self.__class__.__name__)
41
+ else:
42
+ if isinstance(config, dict):
43
+ config = DeviceSignalInputBaseConfig(**config)
44
+ self.config = config
45
+ super().__init__(client=client, config=config, gui_id=gui_id)
46
+
47
+ self._device = None
48
+ self.get_bec_shortcuts()
49
+ self._signal_filter = []
50
+ self._signals = []
51
+ self._hinted_signals = []
52
+ self._normal_signals = []
53
+ self._config_signals = []
54
+ self.bec_dispatcher.client.callbacks.register(
55
+ EventType.DEVICE_UPDATE, self.update_signals_from_filters
56
+ )
57
+
58
+ ### Qt Slots ###
59
+
60
+ @Slot(str)
61
+ def set_signal(self, signal: str):
62
+ """
63
+ Set the signal.
64
+
65
+ Args:
66
+ signal (str): signal name.
67
+ """
68
+ if self.validate_signal(signal) is True:
69
+ WidgetIO.set_value(widget=self, value=signal)
70
+ self.config.default = signal
71
+ else:
72
+ logger.warning(
73
+ f"Signal {signal} not found for device {self.device} and filtered selection {self.signal_filter}."
74
+ )
75
+
76
+ @Slot(str)
77
+ def set_device(self, device: str | None):
78
+ """
79
+ Set the device. If device is not valid, device will be set to None which happpens
80
+
81
+ Args:
82
+ device(str): device name.
83
+ """
84
+ if self.validate_device(device) is False:
85
+ self._device = None
86
+ else:
87
+ self._device = device
88
+ self.update_signals_from_filters()
89
+
90
+ @Slot(dict, dict)
91
+ @Slot()
92
+ def update_signals_from_filters(
93
+ self, content: dict | None = None, metadata: dict | None = None
94
+ ):
95
+ """Update the filters for the device signals based on list in self.signal_filter.
96
+ In addition, store the hinted, normal and config signals in separate lists to allow
97
+ customisation within QLineEdit.
98
+
99
+ Note:
100
+ Signal and ComputedSignals have no signals. The naming convention follows the device name.
101
+ """
102
+ self.config.signal_filter = self.signal_filter
103
+ # pylint: disable=protected-access
104
+ self._hinted_signals = []
105
+ self._normal_signals = []
106
+ self._config_signals = []
107
+ if self.validate_device(self._device) is False:
108
+ self._device = None
109
+ self.config.device = self._device
110
+ return
111
+ device = self.get_device_object(self._device)
112
+ # See above convention for Signals and ComputedSignals
113
+ if isinstance(device, Signal):
114
+ self._signals = [self._device]
115
+ FilterIO.set_selection(widget=self, selection=[self._device])
116
+ return
117
+ device_info = device._info["signals"]
118
+ if Kind.hinted in self.signal_filter:
119
+ hinted_signals = [
120
+ signal
121
+ for signal, signal_info in device_info.items()
122
+ if (signal_info.get("kind_str", None) == str(Kind.hinted.value))
123
+ ]
124
+ self._hinted_signals = hinted_signals
125
+ if Kind.normal in self.signal_filter:
126
+ normal_signals = [
127
+ signal
128
+ for signal, signal_info in device_info.items()
129
+ if (signal_info.get("kind_str", None) == str(Kind.normal.value))
130
+ ]
131
+ self._normal_signals = normal_signals
132
+ if Kind.config in self.signal_filter:
133
+ config_signals = [
134
+ signal
135
+ for signal, signal_info in device_info.items()
136
+ if (signal_info.get("kind_str", None) == str(Kind.config.value))
137
+ ]
138
+ self._config_signals = config_signals
139
+ self._signals = self._hinted_signals + self._normal_signals + self._config_signals
140
+ FilterIO.set_selection(widget=self, selection=self.signals)
141
+
142
+ ### Qt Properties ###
143
+
144
+ @Property(str)
145
+ def device(self) -> str:
146
+ """Get the selected device."""
147
+ if self._device is None:
148
+ return ""
149
+ return self._device
150
+
151
+ @device.setter
152
+ def device(self, value: str):
153
+ """Set the device and update the filters, only allow devices present in the devicemanager."""
154
+ self._device = value
155
+ self.config.device = value
156
+ self.update_signals_from_filters()
157
+
158
+ @Property(bool)
159
+ def include_hinted_signals(self):
160
+ """Include hinted signals in filters."""
161
+ return Kind.hinted in self.signal_filter
162
+
163
+ @include_hinted_signals.setter
164
+ def include_hinted_signals(self, value: bool):
165
+ if value:
166
+ self._signal_filter.append(Kind.hinted)
167
+ else:
168
+ self._signal_filter.remove(Kind.hinted)
169
+ self.update_signals_from_filters()
170
+
171
+ @Property(bool)
172
+ def include_normal_signals(self):
173
+ """Include normal signals in filters."""
174
+ return Kind.normal in self.signal_filter
175
+
176
+ @include_normal_signals.setter
177
+ def include_normal_signals(self, value: bool):
178
+ if value:
179
+ self._signal_filter.append(Kind.normal)
180
+ else:
181
+ self._signal_filter.remove(Kind.normal)
182
+ self.update_signals_from_filters()
183
+
184
+ @Property(bool)
185
+ def include_config_signals(self):
186
+ """Include config signals in filters."""
187
+ return Kind.config in self.signal_filter
188
+
189
+ @include_config_signals.setter
190
+ def include_config_signals(self, value: bool):
191
+ if value:
192
+ self._signal_filter.append(Kind.config)
193
+ else:
194
+ self._signal_filter.remove(Kind.config)
195
+ self.update_signals_from_filters()
196
+
197
+ ### Properties and Methods ###
198
+
199
+ @property
200
+ def signals(self) -> list[str]:
201
+ """
202
+ Get the list of device signals for the applied filters.
203
+
204
+ Returns:
205
+ list[str]: List of device signals.
206
+ """
207
+ return self._signals
208
+
209
+ @signals.setter
210
+ def signals(self, value: list[str]):
211
+ self._signals = value
212
+ self.config.signals = value
213
+ FilterIO.set_selection(widget=self, selection=value)
214
+
215
+ @property
216
+ def signal_filter(self) -> list[str]:
217
+ """Get the list of filters to apply on the device signals."""
218
+ return self._signal_filter
219
+
220
+ def get_available_filters(self) -> list[str]:
221
+ """Get the available filters."""
222
+ return [entry for entry in self._filter_handler]
223
+
224
+ def set_filter(self, filter_selection: str | list[str]):
225
+ """
226
+ Set the device filter. If None, all devices are included.
227
+
228
+ Args:
229
+ filter_selection (str | list[str]): Device filters from BECDeviceFilter and BECReadoutPriority.
230
+ """
231
+ filters = None
232
+ if isinstance(filter_selection, list):
233
+ filters = [self._filter_handler.get(entry) for entry in filter_selection]
234
+ if isinstance(filter_selection, str):
235
+ filters = [self._filter_handler.get(filter_selection)]
236
+ if filters is None:
237
+ return
238
+ for entry in filters:
239
+ setattr(self, entry, True)
240
+
241
+ def get_device_object(self, device: str) -> object | None:
242
+ """
243
+ Get the device object based on the device name.
244
+
245
+ Args:
246
+ device(str): Device name.
247
+
248
+ Returns:
249
+ object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
250
+ """
251
+ self.validate_device(device)
252
+ dev = getattr(self.dev, device.lower(), None)
253
+ if dev is None:
254
+ logger.warning(f"Device {device} not found in devicemanager.")
255
+ return None
256
+ return dev
257
+
258
+ def validate_device(self, device: str | None, raise_on_false: bool = False) -> bool:
259
+ """
260
+ Validate the device if it is present in current BEC instance.
261
+
262
+ Args:
263
+ device(str): Device to validate.
264
+ """
265
+ if device in self.dev:
266
+ return True
267
+ if raise_on_false is True:
268
+ raise ValueError(f"Device {device} not found in devicemanager.")
269
+ return False
270
+
271
+ def validate_signal(self, signal: str) -> bool:
272
+ """
273
+ Validate the signal if it is present in the device signals.
274
+
275
+ Args:
276
+ signal(str): Signal to validate.
277
+ """
278
+ if signal in self.signals:
279
+ return True
280
+ return False
@@ -27,7 +27,7 @@ class DapComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
27
27
  return DOM_XML
28
28
 
29
29
  def group(self):
30
- return "BEC Selection Widgets"
30
+ return "BEC Input Widgets"
31
31
 
32
32
  def icon(self):
33
33
  return designer_material_icon(DapComboBox.ICON_NAME)
@@ -31,7 +31,7 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
31
31
  return DOM_XML
32
32
 
33
33
  def group(self):
34
- return "Device Control"
34
+ return "BEC Input Widgets"
35
35
 
36
36
  def icon(self):
37
37
  return designer_material_icon(DeviceComboBox.ICON_NAME)
@@ -1,86 +1,163 @@
1
- from typing import TYPE_CHECKING
1
+ from bec_lib.callback_handler import EventType
2
+ from bec_lib.device import ReadoutPriority
3
+ from qtpy.QtCore import QSize, Signal, Slot
4
+ from qtpy.QtGui import QPainter, QPaintEvent, QPen
5
+ from qtpy.QtWidgets import QComboBox, QSizePolicy
2
6
 
3
- from qtpy.QtWidgets import QComboBox
4
-
5
- from bec_widgets.widgets.base_classes.device_input_base import DeviceInputBase, DeviceInputConfig
6
-
7
- if TYPE_CHECKING:
8
- from bec_widgets.widgets.base_classes.device_input_base import DeviceInputConfig
7
+ from bec_widgets.utils.colors import get_accent_colors
8
+ from bec_widgets.widgets.base_classes.device_input_base import (
9
+ BECDeviceFilter,
10
+ DeviceInputBase,
11
+ DeviceInputConfig,
12
+ )
9
13
 
10
14
 
11
15
  class DeviceComboBox(DeviceInputBase, QComboBox):
12
16
  """
13
- Line edit widget for device input with autocomplete for device names.
17
+ Combobox widget for device input with autocomplete for device names.
14
18
 
15
19
  Args:
16
20
  parent: Parent widget.
17
21
  client: BEC client object.
18
22
  config: Device input configuration.
19
23
  gui_id: GUI ID.
20
- device_filter: Device filter, name of the device class.
24
+ device_filter: Device filter, name of the device class from BECDeviceFilter and BECReadoutPriority. Check DeviceInputBase for more details.
21
25
  default: Default device name.
22
26
  arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names.
23
27
  """
24
28
 
25
29
  ICON_NAME = "list_alt"
26
30
 
31
+ device_selected = Signal(str)
32
+ device_config_update = Signal()
33
+
27
34
  def __init__(
28
35
  self,
29
36
  parent=None,
30
37
  client=None,
31
38
  config: DeviceInputConfig = None,
32
39
  gui_id: str | None = None,
33
- device_filter: str | None = None,
40
+ device_filter: BECDeviceFilter | list[BECDeviceFilter] | None = None,
41
+ readout_priority_filter: (
42
+ str | ReadoutPriority | list[str] | list[ReadoutPriority] | None
43
+ ) = None,
44
+ available_devices: list[str] | None = None,
34
45
  default: str | None = None,
35
46
  arg_name: str | None = None,
36
47
  ):
37
48
  super().__init__(client=client, config=config, gui_id=gui_id)
38
49
  QComboBox.__init__(self, parent=parent)
39
- self.setMinimumSize(125, 26)
40
- self.populate_combobox()
41
-
42
50
  if arg_name is not None:
43
51
  self.config.arg_name = arg_name
52
+ self.arg_name = arg_name
53
+ self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
54
+ self.setMinimumSize(QSize(100, 0))
55
+ self._callback_id = None
56
+ self._is_valid_input = False
57
+ self._accent_colors = get_accent_colors()
58
+ # We do not consider the config that is passed here, this produced problems
59
+ # with QtDesigner, since config and input arguments may differ and resolve properly
60
+ # Implementing this logic and config recoverage is postponed.
61
+ # Set available devices if passed
62
+ if available_devices is not None:
63
+ self.set_available_devices(available_devices)
64
+ # Set readout priority filter default is all
65
+ if readout_priority_filter is not None:
66
+ self.set_readout_priority_filter(readout_priority_filter)
67
+ else:
68
+ self.set_readout_priority_filter(
69
+ [
70
+ ReadoutPriority.MONITORED,
71
+ ReadoutPriority.BASELINE,
72
+ ReadoutPriority.ASYNC,
73
+ ReadoutPriority.CONTINUOUS,
74
+ ReadoutPriority.ON_REQUEST,
75
+ ]
76
+ )
77
+ # Device filter default is None
44
78
  if device_filter is not None:
45
79
  self.set_device_filter(device_filter)
80
+ # Set default device if passed
46
81
  if default is not None:
47
- self.set_default_device(default)
48
-
49
- def set_device_filter(self, device_filter: str):
82
+ self.set_device(default)
83
+ self._callback_id = self.bec_dispatcher.client.callbacks.register(
84
+ EventType.DEVICE_UPDATE, self.on_device_update
85
+ )
86
+ self.device_config_update.connect(self.update_devices_from_filters)
87
+ self.currentTextChanged.connect(self.check_validity)
88
+ self.check_validity(self.currentText())
89
+
90
+ def on_device_update(self, action: str, content: dict) -> None:
50
91
  """
51
- Set the device filter.
92
+ Callback for device update events. Triggers the device_update signal.
52
93
 
53
94
  Args:
54
- device_filter(str): Device filter, name of the device class.
95
+ action (str): The action that triggered the event.
96
+ content (dict): The content of the config update.
55
97
  """
56
- super().set_device_filter(device_filter)
57
- self.populate_combobox()
98
+ if action in ["add", "remove", "reload"]:
99
+ self.device_config_update.emit()
100
+
101
+ def cleanup(self):
102
+ """Cleanup the widget."""
103
+ if self._callback_id is not None:
104
+ self.bec_dispatcher.client.callbacks.remove(self._callback_id)
58
105
 
59
- def set_default_device(self, default_device: str):
106
+ def get_current_device(self) -> object:
60
107
  """
61
- Set the default device.
108
+ Get the current device object based on the current value.
62
109
 
63
- Args:
64
- default_device(str): Default device name.
110
+ Returns:
111
+ object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
65
112
  """
66
- super().set_default_device(default_device)
67
- self.setCurrentText(default_device)
113
+ dev_name = self.currentText()
114
+ return self.get_device_object(dev_name)
68
115
 
69
- def populate_combobox(self):
70
- """Populate the combobox with the devices."""
71
- self.devices = self.get_device_list(self.config.device_filter)
72
- self.clear()
73
- self.addItems(self.devices)
116
+ def paintEvent(self, event: QPaintEvent) -> None:
117
+ """Extend the paint event to set the border color based on the validity of the input.
74
118
 
75
- def get_device(self) -> object:
119
+ Args:
120
+ event (PySide6.QtGui.QPaintEvent) : Paint event.
76
121
  """
77
- Get the selected device object.
78
-
79
- Returns:
80
- object: Device object.
122
+ # logger.info(f"Received paint event: {event} in {self.__class__}")
123
+ super().paintEvent(event)
124
+
125
+ if self._is_valid_input is False and self.isEnabled() is True:
126
+ painter = QPainter(self)
127
+ pen = QPen()
128
+ pen.setWidth(2)
129
+ pen.setColor(self._accent_colors.emergency)
130
+ painter.setPen(pen)
131
+ painter.drawRect(self.rect().adjusted(1, 1, -1, -1))
132
+ painter.end()
133
+
134
+ @Slot(str)
135
+ def check_validity(self, input_text: str) -> None:
136
+ """
137
+ Check if the current value is a valid device name.
81
138
  """
82
- device_name = self.currentText()
83
- device_obj = getattr(self.dev, device_name.lower(), None)
84
- if device_obj is None:
85
- raise ValueError(f"Device {device_name} is not found.")
86
- return device_obj
139
+ if self.validate_device(input_text) is True:
140
+ self._is_valid_input = True
141
+ self.device_selected.emit(input_text.lower())
142
+ else:
143
+ self._is_valid_input = False
144
+ self.update()
145
+
146
+
147
+ if __name__ == "__main__": # pragma: no cover
148
+ # pylint: disable=import-outside-toplevel
149
+ from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
150
+
151
+ from bec_widgets.utils.colors import set_theme
152
+
153
+ app = QApplication([])
154
+ set_theme("dark")
155
+ widget = QWidget()
156
+ widget.setFixedSize(200, 200)
157
+ layout = QVBoxLayout()
158
+ widget.setLayout(layout)
159
+ combo = DeviceComboBox()
160
+ combo.devices = ["samx", "dev1", "dev2", "dev3", "dev4"]
161
+ layout.addWidget(combo)
162
+ widget.show()
163
+ app.exec_()