diff --git a/bec_widgets/test_utils/client_mocks.py b/bec_widgets/test_utils/client_mocks.py index 7c2fa571..dc6035f5 100644 --- a/bec_widgets/test_utils/client_mocks.py +++ b/bec_widgets/test_utils/client_mocks.py @@ -217,7 +217,7 @@ DEVICES = [ FakeDevice("bpm4i"), FakeDevice("bpm3a"), FakeDevice("bpm3i"), - FakeDevice("eiger"), + FakeDevice("eiger", readout_priority=ReadoutPriority.ASYNC), FakeDevice("waveform1d"), FakeDevice("async_device", readout_priority=ReadoutPriority.ASYNC), Positioner("test", limits=[-10, 10], read_value=2.0), diff --git a/bec_widgets/utils/filter_io.py b/bec_widgets/utils/filter_io.py index 248c1d7f..3aa71bde 100644 --- a/bec_widgets/utils/filter_io.py +++ b/bec_widgets/utils/filter_io.py @@ -99,7 +99,7 @@ class FilterIO: _handlers = {QLineEdit: LineEditFilterHandler, QComboBox: ComboBoxFilterHandler} @staticmethod - def set_selection(widget, selection: list, ignore_errors=False): + def set_selection(widget, selection: list, ignore_errors=True): """ Retrieve value from the widget instance. @@ -118,7 +118,7 @@ class FilterIO: return None @staticmethod - def check_input(widget, text: str, ignore_errors=False): + def check_input(widget, text: str, ignore_errors=True): """ Check if the input text is in the filtered selection. diff --git a/bec_widgets/widgets/base_classes/device_input_base.py b/bec_widgets/widgets/base_classes/device_input_base.py index 9df3b1a2..8c53d0ef 100644 --- a/bec_widgets/widgets/base_classes/device_input_base.py +++ b/bec_widgets/widgets/base_classes/device_input_base.py @@ -5,7 +5,6 @@ import enum from bec_lib.device import ComputedSignal, Device, Positioner, ReadoutPriority, Signal from bec_lib.logger import bec_logger from qtpy.QtCore import Property, Slot -from typeguard import typechecked from bec_widgets.utils import ConnectionConfig from bec_widgets.utils.bec_widget import BECWidget @@ -25,11 +24,12 @@ class BECDeviceFilter(enum.Enum): class DeviceInputConfig(ConnectionConfig): - device_filter: list[BECDeviceFilter] | None = None - readout_filter: list[ReadoutPriority] | None = None - devices: list[str] | None = None + device_filter: list[BECDeviceFilter] = [] + readout_filter: list[ReadoutPriority] = [] + devices: list[str] = [] default: str | None = None arg_name: str | None = None + apply_filter: bool = True class DeviceInputBase(BECWidget): @@ -47,10 +47,10 @@ class DeviceInputBase(BECWidget): } _filter_handler = { - BECDeviceFilter.DEVICE: "include_device", - BECDeviceFilter.POSITIONER: "include_positioner", - BECDeviceFilter.SIGNAL: "include_signal", - BECDeviceFilter.COMPUTED_SIGNAL: "include_computed_signal", + BECDeviceFilter.DEVICE: "filter_to_device", + BECDeviceFilter.POSITIONER: "filter_to_positioner", + BECDeviceFilter.SIGNAL: "filter_to_signal", + BECDeviceFilter.COMPUTED_SIGNAL: "filter_to_computed_signal", ReadoutPriority.MONITORED: "readout_monitored", ReadoutPriority.BASELINE: "readout_baseline", ReadoutPriority.ASYNC: "readout_async", @@ -66,7 +66,7 @@ class DeviceInputBase(BECWidget): if isinstance(config, dict): config = DeviceInputConfig(**config) self.config = config - super().__init__(client=client, config=config, gui_id=gui_id) + super().__init__(client=client, config=config, gui_id=gui_id, theme_update=True) self.get_bec_shortcuts() self._device_filter = [] self._readout_filter = [] @@ -82,7 +82,7 @@ class DeviceInputBase(BECWidget): Args: device (str): Default name. """ - if self.validate_device(device, raise_on_false=True) is True: + if self.validate_device(device) is True: WidgetIO.set_value(widget=self, value=device) self.config.default = device else: @@ -91,9 +91,13 @@ class DeviceInputBase(BECWidget): @Slot() def update_devices_from_filters(self): """Update the devices based on the current filter selection - in self.device_filter and self.readout_filter.""" + in self.device_filter and self.readout_filter. If apply_filter is False, + it will not apply the filters, store the filter settings and return. + """ self.config.device_filter = self.device_filter self.config.readout_filter = self.readout_filter + if self.apply_filter is False: + return all_dev = self.dev.enabled_devices # Filter based on device class devs = [dev for dev in all_dev if self._check_device_filter(dev)] @@ -109,16 +113,37 @@ class DeviceInputBase(BECWidget): Args: devices (list[str]): List of devices. """ - valid_dev = [] - all_dev_names = [dev.name for dev in self.dev.enabled_devices] - for device in devices: - if device not in all_dev_names: - continue - valid_dev.append(device) - self.devices = valid_dev + self.apply_filter = False + self.devices = devices ### QtProperties ### + @Property( + "QStringList", + doc="List of devices. If updated, it will disable the apply filters property.", + ) + def devices(self) -> list[str]: + """ + Get the list of devices for the applied filters. + + Returns: + list[str]: List of devices. + """ + return self._devices + + @devices.setter + def devices(self, value: list): + valid_dev = [] + all_dev_names = [dev.name for dev in self.dev.enabled_devices] + for dev in value: + if dev in all_dev_names: + valid_dev.append(dev) + self._devices = valid_dev + self.config.devices = valid_dev + + FilterIO.set_selection(widget=self, selection=valid_dev) + # QTimer.singleShot(200, lambda: FilterIO.set_selection(widget=self, selection=valid_dev)) + @Property(str) def default(self): """Get the default device name. If set through this property, it will update only if the device is within the filtered selection.""" @@ -126,30 +151,52 @@ class DeviceInputBase(BECWidget): @default.setter def default(self, value: str): - if self.validate_device(value, raise_on_false=False) is False: - return - self.set_device(value) + def set_default(): + if self.validate_device(value) is False: + return + self.set_device(value) + + set_default() + # QTimer.singleShot(200, set_default) @Property(bool) - def include_device(self): + def apply_filter(self): + """Apply the filters on the devices.""" + return self.config.apply_filter + + @apply_filter.setter + def apply_filter(self, value: bool): + def apply_filters(): + self.config.apply_filter = value + self.update_devices_from_filters() + + apply_filters() + # QTimer.singleShot(200, apply_filters) + + @Property(bool) + def filter_to_device(self): """Include devices in filters.""" return BECDeviceFilter.DEVICE in self.device_filter - @include_device.setter - def include_device(self, value: bool): - if value is True and BECDeviceFilter.DEVICE not in self.device_filter: - self._device_filter.append(BECDeviceFilter.DEVICE) - if value is False and BECDeviceFilter.DEVICE in self.device_filter: - self._device_filter.remove(BECDeviceFilter.DEVICE) - self.update_devices_from_filters() + @filter_to_device.setter + def filter_to_device(self, value: bool): + def set_filter(): + if value is True and BECDeviceFilter.DEVICE not in self.device_filter: + self._device_filter.append(BECDeviceFilter.DEVICE) + if value is False and BECDeviceFilter.DEVICE in self.device_filter: + self._device_filter.remove(BECDeviceFilter.DEVICE) + self.update_devices_from_filters() + + set_filter() + # QTimer.singleShot(200, set_filter) @Property(bool) - def include_positioner(self): + def filter_to_positioner(self): """Include devices of type Positioner in filters.""" return BECDeviceFilter.POSITIONER in self.device_filter - @include_positioner.setter - def include_positioner(self, value: bool): + @filter_to_positioner.setter + def filter_to_positioner(self, value: bool): if value is True and BECDeviceFilter.POSITIONER not in self.device_filter: self._device_filter.append(BECDeviceFilter.POSITIONER) if value is False and BECDeviceFilter.POSITIONER in self.device_filter: @@ -157,12 +204,12 @@ class DeviceInputBase(BECWidget): self.update_devices_from_filters() @Property(bool) - def include_signal(self): + def filter_to_signal(self): """Include devices of type Signal in filters.""" return BECDeviceFilter.SIGNAL in self.device_filter - @include_signal.setter - def include_signal(self, value: bool): + @filter_to_signal.setter + def filter_to_signal(self, value: bool): if value is True and BECDeviceFilter.SIGNAL not in self.device_filter: self._device_filter.append(BECDeviceFilter.SIGNAL) if value is False and BECDeviceFilter.SIGNAL in self.device_filter: @@ -170,12 +217,12 @@ class DeviceInputBase(BECWidget): self.update_devices_from_filters() @Property(bool) - def include_computed_signal(self): + def filter_to_computed_signal(self): """Include devices of type ComputedSignal in filters.""" return BECDeviceFilter.COMPUTED_SIGNAL in self.device_filter - @include_computed_signal.setter - def include_computed_signal(self, value: bool): + @filter_to_computed_signal.setter + def filter_to_computed_signal(self, value: bool): if value is True and BECDeviceFilter.COMPUTED_SIGNAL not in self.device_filter: self._device_filter.append(BECDeviceFilter.COMPUTED_SIGNAL) if value is False and BECDeviceFilter.COMPUTED_SIGNAL in self.device_filter: @@ -249,22 +296,6 @@ class DeviceInputBase(BECWidget): ### Python Methods and Properties ### - @property - def devices(self) -> list[str]: - """ - Get the list of devices for the applied filters. - - Returns: - list[str]: List of devices. - """ - return self._devices - - @devices.setter - def devices(self, value: list[str]): - self._devices = value - self.config.devices = value - FilterIO.set_selection(widget=self, selection=value) - @property def device_filter(self) -> list[object]: """Get the list of filters to apply on the devices.""" @@ -283,7 +314,6 @@ class DeviceInputBase(BECWidget): """Get the available readout priority filters.""" return [entry for entry in ReadoutPriority] - @typechecked def set_device_filter( self, filter_selection: str | BECDeviceFilter | list[str] | list[BECDeviceFilter] ): @@ -299,11 +329,11 @@ class DeviceInputBase(BECWidget): if isinstance(filter_selection, str) or isinstance(filter_selection, BECDeviceFilter): filters = [self._filter_handler.get(filter_selection)] if filters is None or any([entry is None for entry in filters]): - raise ValueError(f"Device filter {filter_selection} is not in the device list.") + logger.warning(f"Device filter {filter_selection} is not in the device filter list.") + return for entry in filters: setattr(self, entry, True) - @typechecked def set_readout_priority_filter( self, filter_selection: str | ReadoutPriority | list[str] | list[ReadoutPriority] ): @@ -319,30 +349,27 @@ class DeviceInputBase(BECWidget): if isinstance(filter_selection, str) or isinstance(filter_selection, ReadoutPriority): filters = [self._filter_handler.get(filter_selection)] if filters is None or any([entry is None for entry in filters]): - raise ValueError( + logger.warning( f"Readout priority filter {filter_selection} is not in the readout priority list." ) + return for entry in filters: setattr(self, entry, True) def _check_device_filter(self, device: Device | Signal | ComputedSignal | Positioner) -> bool: - """If filters are defined, return True. Else return if the device complies to all active filters. + """Check if filter for device type is applied or not. Args: device(Device | Signal | ComputedSignal | Positioner): Device object. """ - if len(self.device_filter) == 0: - return True return all(isinstance(device, self._device_handler[entry]) for entry in self.device_filter) def _check_readout_filter(self, device: Device | Signal | ComputedSignal | Positioner) -> bool: - """If filters are defined, return True. Else return if the device complies to all active filters. + """Check if filter for readout priority is applied or not. Args: device(Device | Signal | ComputedSignal | Positioner): Device object. """ - if len(self.readout_filter) == 0: - return True return device.readout_priority in self.readout_filter def get_device_object(self, device: str) -> object: @@ -363,7 +390,7 @@ class DeviceInputBase(BECWidget): ) return dev - def validate_device(self, device: str, raise_on_false: bool = False) -> bool: + def validate_device(self, device: str) -> bool: """ Validate the device if it is present in the filtered device selection. @@ -372,5 +399,4 @@ class DeviceInputBase(BECWidget): """ if device in self.devices: return True - if raise_on_false is True: - raise ValueError(f"Device {device} is not in filtered selection.") + return False diff --git a/bec_widgets/widgets/base_classes/device_signal_input_base.py b/bec_widgets/widgets/base_classes/device_signal_input_base.py index 28253e9a..003101bf 100644 --- a/bec_widgets/widgets/base_classes/device_signal_input_base.py +++ b/bec_widgets/widgets/base_classes/device_signal_input_base.py @@ -1,6 +1,6 @@ -import enum - +from bec_lib.device import Signal from bec_lib.logger import bec_logger +from ophyd import Kind from qtpy.QtCore import Property, Slot from bec_widgets.utils import ConnectionConfig @@ -11,14 +11,6 @@ from bec_widgets.utils.widget_io import WidgetIO logger = bec_logger.logger -class BECSignalFilter(str, enum.Enum): - """Filter for the device signals.""" - - HINTED = "5" - NORMAL = "1" - CONFIG = "2" - - class DeviceSignalInputBaseConfig(ConnectionConfig): """Configuration class for DeviceSignalInputBase.""" @@ -37,9 +29,9 @@ class DeviceSignalInputBase(BECWidget): """ _filter_handler = { - BECSignalFilter.HINTED: "include_hinted_signals", - BECSignalFilter.NORMAL: "include_normal_signals", - BECSignalFilter.CONFIG: "include_config_signals", + Kind.hinted: "include_hinted_signals", + Kind.normal: "include_normal_signals", + Kind.config: "include_config_signals", } def __init__(self, client=None, config=None, gui_id: str = None): @@ -69,7 +61,7 @@ class DeviceSignalInputBase(BECWidget): Args: signal (str): signal name. """ - if self.validate_signal(signal, raise_on_false=False) is True: + if self.validate_signal(signal) is True: WidgetIO.set_value(widget=self, value=signal) self.config.default = signal else: @@ -80,52 +72,61 @@ class DeviceSignalInputBase(BECWidget): @Slot(str) def set_device(self, device: str | None): """ - Set the device. + Set the device. If device is not valid, device will be set to None which happpens Args: device(str): device name. """ - if device is None: + if self.validate_device(device) is False: self._device = None - if self.validate_device(device, raise_on_false=False) is True: - self._device = device - self.update_signals_from_filters() else: - logger.warning(f"Device {device} not found in device_manager.") + self._device = device + self.update_signals_from_filters() @Slot() def update_signals_from_filters(self): """Update the filters for the device signals based on list in self.signal_filter. In addition, store the hinted, normal and config signals in separate lists to allow - customisation within QLineEdit.""" + customisation within QLineEdit. + + Note: + Signal and ComputedSignals have no signals. The naming convention follows the device name. + """ self.config.signal_filter = self.signal_filter # pylint: disable=protected-access self._hinted_signals = [] self._normal_signals = [] self._config_signals = [] - if self._device is None: + if self.validate_device(self._device) is False: + self._device = None + self.config.device = self._device return device = self.get_device_object(self._device) + # See above convention for Signals and ComputedSignals + if isinstance(device, Signal): + self._signals = [self._device] + FilterIO.set_selection(widget=self, selection=[self._device]) + return device_info = device._info["signals"] - if BECSignalFilter.HINTED in self.signal_filter or len(self.signal_filter) == 0: + if Kind.hinted in self.signal_filter: hinted_signals = [ signal for signal, signal_info in device_info.items() - if (signal_info.get("kind_str", None) == BECSignalFilter.HINTED) + if (signal_info.get("kind_str", None) == str(Kind.hinted.value)) ] self._hinted_signals = hinted_signals - if BECSignalFilter.NORMAL in self.signal_filter or len(self.signal_filter) == 0: + if Kind.normal in self.signal_filter: normal_signals = [ signal for signal, signal_info in device_info.items() - if (signal_info.get("kind_str", None) == BECSignalFilter.NORMAL) + if (signal_info.get("kind_str", None) == str(Kind.normal.value)) ] self._normal_signals = normal_signals - if BECSignalFilter.CONFIG in self.signal_filter or len(self.signal_filter) == 0: + if Kind.config in self.signal_filter: config_signals = [ signal for signal, signal_info in device_info.items() - if (signal_info.get("kind_str", None) == BECSignalFilter.CONFIG) + if (signal_info.get("kind_str", None) == str(Kind.config.value)) ] self._config_signals = config_signals self._signals = self._hinted_signals + self._normal_signals + self._config_signals @@ -143,8 +144,6 @@ class DeviceSignalInputBase(BECWidget): @device.setter def device(self, value: str): """Set the device and update the filters, only allow devices present in the devicemanager.""" - if self.validate_device(value) is False: - return self._device = value self.config.device = value self.update_signals_from_filters() @@ -152,40 +151,40 @@ class DeviceSignalInputBase(BECWidget): @Property(bool) def include_hinted_signals(self): """Include hinted signals in filters.""" - return BECSignalFilter.HINTED in self.signal_filter + return Kind.hinted in self.signal_filter @include_hinted_signals.setter def include_hinted_signals(self, value: bool): if value: - self._signal_filter.append(BECSignalFilter.HINTED) + self._signal_filter.append(Kind.hinted) else: - self._signal_filter.remove(BECSignalFilter.HINTED) + self._signal_filter.remove(Kind.hinted) self.update_signals_from_filters() @Property(bool) def include_normal_signals(self): """Include normal signals in filters.""" - return BECSignalFilter.NORMAL in self.signal_filter + return Kind.normal in self.signal_filter @include_normal_signals.setter def include_normal_signals(self, value: bool): if value: - self._signal_filter.append(BECSignalFilter.NORMAL) + self._signal_filter.append(Kind.normal) else: - self._signal_filter.remove(BECSignalFilter.NORMAL) + self._signal_filter.remove(Kind.normal) self.update_signals_from_filters() @Property(bool) def include_config_signals(self): """Include config signals in filters.""" - return BECSignalFilter.CONFIG in self.signal_filter + return Kind.config in self.signal_filter @include_config_signals.setter def include_config_signals(self, value: bool): if value: - self._signal_filter.append(BECSignalFilter.CONFIG) + self._signal_filter.append(Kind.config) else: - self._signal_filter.remove(BECSignalFilter.CONFIG) + self._signal_filter.remove(Kind.config) self.update_signals_from_filters() ### Properties and Methods ### @@ -232,7 +231,7 @@ class DeviceSignalInputBase(BECWidget): for entry in filters: setattr(self, entry, True) - def get_device_object(self, device: str) -> object: + def get_device_object(self, device: str) -> object | None: """ Get the device object based on the device name. @@ -245,10 +244,11 @@ class DeviceSignalInputBase(BECWidget): self.validate_device(device) dev = getattr(self.dev, device.lower(), None) if dev is None: - raise ValueError(f"Device {device} is not found in devicemanager {self.dev}.") + logger.warning(f"Device {device} not found in devicemanager.") + return None return dev - def validate_device(self, device: str, raise_on_false: bool = False) -> bool: + def validate_device(self, device: str | None, raise_on_false: bool = False) -> bool: """ Validate the device if it is present in current BEC instance. @@ -261,7 +261,7 @@ class DeviceSignalInputBase(BECWidget): raise ValueError(f"Device {device} not found in devicemanager.") return False - def validate_signal(self, signal: str, raise_on_false: bool = False) -> bool: + def validate_signal(self, signal: str) -> bool: """ Validate the signal if it is present in the device signals. @@ -270,8 +270,4 @@ class DeviceSignalInputBase(BECWidget): """ if signal in self.signals: return True - if raise_on_false is True: - raise ValueError( - f"Signal {signal} not found for device {self.device} and filtered selection {self.signal_filter}." - ) return False diff --git a/bec_widgets/widgets/device_combobox/device_combobox.py b/bec_widgets/widgets/device_combobox/device_combobox.py index 06eb4e3e..9400f146 100644 --- a/bec_widgets/widgets/device_combobox/device_combobox.py +++ b/bec_widgets/widgets/device_combobox/device_combobox.py @@ -36,7 +36,7 @@ class DeviceComboBox(DeviceInputBase, QComboBox): readout_priority_filter: ( str | ReadoutPriority | list[str] | list[ReadoutPriority] | None ) = None, - device_list: list[str] | None = None, + available_devices: list[str] | None = None, default: str | None = None, arg_name: str | None = None, ): @@ -49,24 +49,29 @@ class DeviceComboBox(DeviceInputBase, QComboBox): self.setMinimumSize(QSize(100, 0)) self._is_valid_input = False self._accent_colors = get_accent_colors() - # Set readout priority filter and device filter. - # If value is set directly in init, this overrules value from the config - readout_priority_filter = ( - readout_priority_filter - if readout_priority_filter is not None - else self.config.readout_filter - ) + # We do not consider the config that is passed here, this produced problems + # with QtDesigner, since config and input arguments may differ and resolve properly + # Implementing this logic and config recoverage is postponed. + # Set available devices if passed + if available_devices is not None: + self.set_available_devices(available_devices) + # Set readout priority filter default is all if readout_priority_filter is not None: self.set_readout_priority_filter(readout_priority_filter) - device_filter = device_filter if device_filter is not None else self.config.device_filter + else: + self.set_readout_priority_filter( + [ + ReadoutPriority.MONITORED, + ReadoutPriority.BASELINE, + ReadoutPriority.ASYNC, + ReadoutPriority.CONTINUOUS, + ReadoutPriority.ON_REQUEST, + ] + ) + # Device filter default is None if device_filter is not None: self.set_device_filter(device_filter) - device_list = device_list if device_list is not None else self.config.devices - if device_list is not None: - self.set_available_devices(device_list) - else: - self.update_devices_from_filters() - default = default if default is not None else self.config.default + # Set default device if passed if default is not None: self.set_device(default) diff --git a/bec_widgets/widgets/device_line_edit/device_line_edit.py b/bec_widgets/widgets/device_line_edit/device_line_edit.py index 420f50f1..669d05af 100644 --- a/bec_widgets/widgets/device_line_edit/device_line_edit.py +++ b/bec_widgets/widgets/device_line_edit/device_line_edit.py @@ -1,7 +1,8 @@ from bec_lib.device import ReadoutPriority -from qtpy.QtCore import QSize +from bec_lib.logger import bec_logger +from qtpy.QtCore import QSize, Signal, Slot from qtpy.QtGui import QPainter, QPaintEvent, QPen -from qtpy.QtWidgets import QCompleter, QLineEdit, QSizePolicy +from qtpy.QtWidgets import QApplication, QCompleter, QLineEdit, QSizePolicy from bec_widgets.utils.colors import get_accent_colors from bec_widgets.widgets.base_classes.device_input_base import ( @@ -10,6 +11,8 @@ from bec_widgets.widgets.base_classes.device_input_base import ( DeviceInputConfig, ) +logger = bec_logger.logger + class DeviceLineEdit(DeviceInputBase, QLineEdit): """ @@ -39,39 +42,46 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit): readout_priority_filter: ( str | ReadoutPriority | list[str] | list[ReadoutPriority] | None ) = None, - device_list: list[str] | None = None, + available_devices: list[str] | None = None, default: str | None = None, arg_name: str | None = None, ): + self._is_valid_input = False + self._accent_colors = get_accent_colors() super().__init__(client=client, config=config, gui_id=gui_id) QLineEdit.__init__(self, parent=parent) - self._is_valid_input = False self.completer = QCompleter(self) self.setCompleter(self.completer) + if arg_name is not None: self.config.arg_name = arg_name self.arg_name = arg_name self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) self.setMinimumSize(QSize(100, 0)) - self._accent_colors = get_accent_colors() - # Set readout priority filter and device filter. - # If value is set directly in init, this overrules value from the config - readout_priority_filter = ( - readout_priority_filter - if readout_priority_filter is not None - else self.config.readout_filter - ) + + # We do not consider the config that is passed here, this produced problems + # with QtDesigner, since config and input arguments may differ and resolve properly + # Implementing this logic and config recoverage is postponed. + # Set available devices if passed + if available_devices is not None: + self.set_available_devices(available_devices) + # Set readout priority filter default is all if readout_priority_filter is not None: self.set_readout_priority_filter(readout_priority_filter) - device_filter = device_filter if device_filter is not None else self.config.device_filter + else: + self.set_readout_priority_filter( + [ + ReadoutPriority.MONITORED, + ReadoutPriority.BASELINE, + ReadoutPriority.ASYNC, + ReadoutPriority.CONTINUOUS, + ReadoutPriority.ON_REQUEST, + ] + ) + # Device filter default is None if device_filter is not None: self.set_device_filter(device_filter) - device_list = device_list if device_list is not None else self.config.devices - if device_list is not None: - self.set_available_devices(device_list) - else: - self.update_devices_from_filters() - default = default if default is not None else self.config.default + # Set default device if passed if default is not None: self.set_device(default) self.textChanged.connect(self.check_validity) @@ -92,16 +102,19 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit): Args: event (PySide6.QtGui.QPaintEvent) : Paint event. """ + # logger.info(f"Received paint event: {event} in {self.__class__}") super().paintEvent(event) - painter = QPainter(self) - pen = QPen() - pen.setWidth(2) if self._is_valid_input is False and self.isEnabled() is True: + painter = QPainter(self) + pen = QPen() + pen.setWidth(2) pen.setColor(self._accent_colors.emergency) painter.setPen(pen) painter.drawRect(self.rect().adjusted(1, 1, -1, -1)) + painter.end() + @Slot(str) def check_validity(self, input_text: str) -> None: """ Check if the current value is a valid device name. @@ -116,9 +129,10 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit): if __name__ == "__main__": # pragma: no cover # pylint: disable=import-outside-toplevel - from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget + from qtpy.QtWidgets import QVBoxLayout, QWidget from bec_widgets.utils.colors import set_theme + from bec_widgets.widgets.signal_combobox.signal_combobox import SignalComboBox app = QApplication([]) set_theme("dark") @@ -127,7 +141,12 @@ if __name__ == "__main__": # pragma: no cover layout = QVBoxLayout() widget.setLayout(layout) line_edit = DeviceLineEdit() - line_edit.include_positioner = True + line_edit.filter_to_positioner = True + signal_line_edit = SignalComboBox() + line_edit.textChanged.connect(signal_line_edit.set_device) + line_edit.set_available_devices(["samx", "samy", "samz"]) + line_edit.set_device("samx") layout.addWidget(line_edit) + layout.addWidget(signal_line_edit) widget.show() app.exec_() diff --git a/bec_widgets/widgets/image/image_widget.py b/bec_widgets/widgets/image/image_widget.py index 97376604..209f1715 100644 --- a/bec_widgets/widgets/image/image_widget.py +++ b/bec_widgets/widgets/image/image_widget.py @@ -4,6 +4,7 @@ import sys from typing import Literal, Optional import pyqtgraph as pg +from bec_lib.device import ReadoutPriority from qtpy.QtWidgets import QComboBox, QVBoxLayout, QWidget from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility @@ -70,7 +71,11 @@ class BECImageWidget(BECWidget, QWidget): self.toolbar = ModularToolBar( actions={ "monitor": DeviceSelectionAction( - "Monitor:", DeviceComboBox(device_filter=BECDeviceFilter.DEVICE) + "Monitor:", + DeviceComboBox( + device_filter=BECDeviceFilter.DEVICE, + readout_priority_filter=[ReadoutPriority.ASYNC], + ), ), "monitor_type": WidgetAction(widget=self.dim_combo_box), "connect": MaterialIconAction(icon_name="link", tooltip="Connect Device"), diff --git a/bec_widgets/widgets/scan_control/scan_group_box.py b/bec_widgets/widgets/scan_control/scan_group_box.py index 2a369ca6..fca8ea9a 100644 --- a/bec_widgets/widgets/scan_control/scan_group_box.py +++ b/bec_widgets/widgets/scan_control/scan_group_box.py @@ -21,6 +21,7 @@ from qtpy.QtWidgets import ( ) from bec_widgets.utils.widget_io import WidgetIO +from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit logger = bec_logger.logger @@ -233,6 +234,7 @@ class ScanGroupBox(QGroupBox): default = None widget = widget_class(arg_name=arg_name, default=default) if isinstance(widget, DeviceLineEdit): + widget.set_device_filter(BECDeviceFilter.DEVICE) self.selected_devices[widget] = "" widget.device_selected.connect(self.emit_device_selected) tooltip = item.get("tooltip", None) diff --git a/bec_widgets/widgets/signal_combobox/signal_combobox.py b/bec_widgets/widgets/signal_combobox/signal_combobox.py index 27459b65..ec60b3ea 100644 --- a/bec_widgets/widgets/signal_combobox/signal_combobox.py +++ b/bec_widgets/widgets/signal_combobox/signal_combobox.py @@ -1,4 +1,6 @@ -from qtpy.QtCore import QSize +from bec_lib.device import Positioner +from ophyd import Kind +from qtpy.QtCore import QSize, Signal, Slot from qtpy.QtWidgets import QComboBox, QSizePolicy from bec_widgets.utils.filter_io import ComboBoxFilterHandler, FilterIO @@ -21,6 +23,8 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox): ICON_NAME = "list_alt" + device_signal_changed = Signal(str) + def __init__( self, parent=None, @@ -42,13 +46,16 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox): self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) self.setMinimumSize(QSize(100, 0)) - signal_filter = signal_filter if not None else self.config.signal_filter + # We do not consider the config that is passed here, this produced problems + # with QtDesigner, since config and input arguments may differ and resolve properly + # Implementing this logic and config recoverage is postponed. + self.currentTextChanged.connect(self.on_text_changed) if signal_filter is not None: self.set_filter(signal_filter) - device = device if not None else self.config.device + else: + self.set_filter([Kind.hinted, Kind.normal, Kind.config]) if device is not None: self.set_device(device) - default = default if not None else self.config.default if default is not None: self.set_signal(default) @@ -71,6 +78,24 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox): self.insertItem(0, "Hinted Signals") self.model().item(0).setEnabled(False) + @Slot(str) + def on_text_changed(self, text: str): + """Slot for text changed. If a device is selected and the signal is changed and valid it emits a signal. + For a positioner, the readback value has to be renamed to the device name. + + Args: + text (str): Text in the combobox. + """ + if self.validate_device(self.device) is False: + return + if self.validate_signal(text) is False: + return + if text == "readback" and isinstance(self.get_device_object(self.device), Positioner): + device_signal = self.device + else: + device_signal = f"{self.device}_{text}" + self.device_signal_changed.emit(device_signal) + if __name__ == "__main__": # pragma: no cover # pylint: disable=import-outside-toplevel diff --git a/bec_widgets/widgets/signal_line_edit/signal_line_edit.py b/bec_widgets/widgets/signal_line_edit/signal_line_edit.py index e2441ab9..b6e9e9c2 100644 --- a/bec_widgets/widgets/signal_line_edit/signal_line_edit.py +++ b/bec_widgets/widgets/signal_line_edit/signal_line_edit.py @@ -1,4 +1,5 @@ -from qtpy.QtCore import QSize, Slot +from ophyd import Kind +from qtpy.QtCore import QSize, Signal, Slot from qtpy.QtGui import QPainter, QPaintEvent, QPen from qtpy.QtWidgets import QCompleter, QLineEdit, QSizePolicy @@ -20,6 +21,8 @@ class SignalLineEdit(DeviceSignalInputBase, QLineEdit): arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names. """ + device_signal_changed = Signal(str) + ICON_NAME = "vital_signs" def __init__( @@ -33,9 +36,9 @@ class SignalLineEdit(DeviceSignalInputBase, QLineEdit): default: str | None = None, arg_name: str | None = None, ): + self._is_valid_input = False super().__init__(client=client, config=config, gui_id=gui_id) QLineEdit.__init__(self, parent=parent) - self._is_valid_input = False self._accent_colors = get_accent_colors() self.completer = QCompleter(self) self.setCompleter(self.completer) @@ -47,13 +50,15 @@ class SignalLineEdit(DeviceSignalInputBase, QLineEdit): self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) self.setMinimumSize(QSize(100, 0)) - signal_filter = signal_filter if not None else self.config.signal_filter + # We do not consider the config that is passed here, this produced problems + # with QtDesigner, since config and input arguments may differ and resolve properly + # Implementing this logic and config recoverage is postponed. if signal_filter is not None: self.set_filter(signal_filter) - device = device if not None else self.config.device + else: + self.set_filter([Kind.hinted, Kind.normal, Kind.config]) if device is not None: self.set_device(device) - default = default if not None else self.config.default if default is not None: self.set_signal(default) self.textChanged.connect(self.check_validity) @@ -89,9 +94,10 @@ class SignalLineEdit(DeviceSignalInputBase, QLineEdit): """ Check if the current value is a valid device name. """ - # i if self.validate_signal(input_text) is True: self._is_valid_input = True + if self.validate_device(self.device) is True: + self.device_signal_changed.emit(input_text) else: self._is_valid_input = False self.update() diff --git a/tests/unit_tests/test_device_input_base.py b/tests/unit_tests/test_device_input_base.py index a8652e01..97e5d438 100644 --- a/tests/unit_tests/test_device_input_base.py +++ b/tests/unit_tests/test_device_input_base.py @@ -36,7 +36,7 @@ def test_device_input_base_init(device_input_base): assert device_input_base.client is not None assert isinstance(device_input_base, DeviceInputBase) assert device_input_base.config.widget_class == "DeviceInputWidget" - assert device_input_base.config.device_filter is None + assert device_input_base.config.device_filter == [] assert device_input_base.config.default is None assert device_input_base.devices == [] @@ -62,17 +62,15 @@ def test_device_input_base_set_device_filter(device_input_base): def test_device_input_base_set_device_filter_error(device_input_base): - """Test set_device_filter with Noneexisting class""" - with pytest.raises(ValueError) as excinfo: - device_input_base.set_device_filter("NonExistingClass") - assert "Device filter NonExistingClass is not in the device list." in str(excinfo.value) + """Test set_device_filter with Noneexisting class. This should not raise. It writes a log message entry.""" + device_input_base.set_device_filter("NonExistingClass") + assert device_input_base.device_filter == [] def test_device_input_base_set_default_device(device_input_base): """Test setting the default device. Also tests the update_devices method.""" - with pytest.raises(ValueError) as excinfo: - device_input_base.set_device("samx") - assert "Device samx is not in filtered selection." in str(excinfo.value) + device_input_base.set_device("samx") + assert device_input_base.config.default == None device_input_base.set_device_filter(BECDeviceFilter.POSITIONER) device_input_base.set_readout_priority_filter(ReadoutPriority.MONITORED) device_input_base.set_device("samx") @@ -99,17 +97,17 @@ def test_device_input_base_get_filters(device_input_base): def test_device_input_base_properties(device_input_base): """Test setting the properties of the device input base.""" assert device_input_base.device_filter == [] - device_input_base.include_device = True + device_input_base.filter_to_device = True assert device_input_base.device_filter == [BECDeviceFilter.DEVICE] - device_input_base.include_positioner = True + device_input_base.filter_to_positioner = True assert device_input_base.device_filter == [BECDeviceFilter.DEVICE, BECDeviceFilter.POSITIONER] - device_input_base.include_computed_signal = True + device_input_base.filter_to_computed_signal = True assert device_input_base.device_filter == [ BECDeviceFilter.DEVICE, BECDeviceFilter.POSITIONER, BECDeviceFilter.COMPUTED_SIGNAL, ] - device_input_base.include_signal = True + device_input_base.filter_to_signal = True assert device_input_base.device_filter == [ BECDeviceFilter.DEVICE, BECDeviceFilter.POSITIONER, diff --git a/tests/unit_tests/test_device_input_widgets.py b/tests/unit_tests/test_device_input_widgets.py index 59faaaf5..7d941776 100644 --- a/tests/unit_tests/test_device_input_widgets.py +++ b/tests/unit_tests/test_device_input_widgets.py @@ -16,21 +16,6 @@ def device_input_combobox(qtbot, mocked_client): yield widget -@pytest.fixture -def device_input_combobox_with_config(qtbot, mocked_client): - config = { - "widget_class": "DeviceComboBox", - "gui_id": "test_gui_id", - "device_filter": [BECDeviceFilter.POSITIONER], - "default": "samx", - "arg_name": "test_arg_name", - } - widget = DeviceComboBox(client=mocked_client, config=config) - qtbot.addWidget(widget) - qtbot.waitExposed(widget) - yield widget - - @pytest.fixture def device_input_combobox_with_kwargs(qtbot, mocked_client): widget = DeviceComboBox( @@ -72,13 +57,6 @@ def test_device_input_combobox_init(device_input_combobox): ] -def test_device_input_combobox_init_with_config(device_input_combobox_with_config): - assert device_input_combobox_with_config.config.gui_id == "test_gui_id" - assert device_input_combobox_with_config.config.device_filter == [BECDeviceFilter.POSITIONER] - assert device_input_combobox_with_config.config.default == "samx" - assert device_input_combobox_with_config.config.arg_name == "test_arg_name" - - def test_device_input_combobox_init_with_kwargs(device_input_combobox_with_kwargs): assert device_input_combobox_with_kwargs.config.gui_id == "test_gui_id" assert device_input_combobox_with_kwargs.config.device_filter == [BECDeviceFilter.POSITIONER] @@ -102,21 +80,6 @@ def device_input_line_edit(qtbot, mocked_client): yield widget -@pytest.fixture -def device_input_line_edit_with_config(qtbot, mocked_client): - config = { - "widget_class": "DeviceLineEdit", - "gui_id": "test_gui_id", - "device_filter": [BECDeviceFilter.POSITIONER], - "default": "samx", - "arg_name": "test_arg_name", - } - widget = DeviceLineEdit(client=mocked_client, config=config) - qtbot.addWidget(widget) - qtbot.waitExposed(widget) - yield widget - - @pytest.fixture def device_input_line_edit_with_kwargs(qtbot, mocked_client): widget = DeviceLineEdit( @@ -137,7 +100,13 @@ def test_device_input_line_edit_init(device_input_line_edit): assert isinstance(device_input_line_edit, DeviceLineEdit) assert device_input_line_edit.config.widget_class == "DeviceLineEdit" assert device_input_line_edit.config.device_filter == [] - assert device_input_line_edit.config.readout_filter == [] + assert device_input_line_edit.config.readout_filter == [ + ReadoutPriority.MONITORED, + ReadoutPriority.BASELINE, + ReadoutPriority.ASYNC, + ReadoutPriority.CONTINUOUS, + ReadoutPriority.ON_REQUEST, + ] assert device_input_line_edit.config.default is None assert device_input_line_edit.devices == [ "samx", @@ -160,13 +129,6 @@ def test_device_input_line_edit_init(device_input_line_edit): ] -def test_device_input_line_edit_init_with_config(device_input_line_edit_with_config): - assert device_input_line_edit_with_config.config.gui_id == "test_gui_id" - assert device_input_line_edit_with_config.config.device_filter == [BECDeviceFilter.POSITIONER] - assert device_input_line_edit_with_config.config.default == "samx" - assert device_input_line_edit_with_config.config.arg_name == "test_arg_name" - - def test_device_input_line_edit_init_with_kwargs(device_input_line_edit_with_kwargs): assert device_input_line_edit_with_kwargs.config.gui_id == "test_gui_id" assert device_input_line_edit_with_kwargs.config.device_filter == [BECDeviceFilter.POSITIONER] diff --git a/tests/unit_tests/test_device_signal_input.py b/tests/unit_tests/test_device_signal_input.py index ce404f8f..2dffe993 100644 --- a/tests/unit_tests/test_device_signal_input.py +++ b/tests/unit_tests/test_device_signal_input.py @@ -1,12 +1,12 @@ from unittest import mock import pytest +from ophyd import Kind from qtpy.QtWidgets import QWidget -from bec_widgets.widgets.base_classes.device_signal_input_base import ( - BECSignalFilter, - DeviceSignalInputBase, -) +from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter +from bec_widgets.widgets.base_classes.device_signal_input_base import DeviceSignalInputBase +from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox from bec_widgets.widgets.signal_combobox.signal_combobox import SignalComboBox from bec_widgets.widgets.signal_line_edit.signal_line_edit import SignalLineEdit @@ -45,6 +45,19 @@ def device_signal_line_edit(qtbot, mocked_client): yield widget +@pytest.fixture +def test_device_signal_combo(qtbot, mocked_client): + """Fixture to create a SignalComboBox widget and a DeviceInputWidget widget""" + input = create_widget( + qtbot=qtbot, + widget=DeviceComboBox, + client=mocked_client, + device_filter=[BECDeviceFilter.POSITIONER], + ) + signal = create_widget(qtbot=qtbot, widget=SignalComboBox, client=mocked_client) + yield input, signal + + def test_device_signal_base_init(device_signal_base): """Test if the DeviceSignalInputBase is initialized correctly""" assert device_signal_base._device is None @@ -58,15 +71,11 @@ def test_device_signal_base_init(device_signal_base): def test_device_signal_qproperties(device_signal_base): """Test if the DeviceSignalInputBase has the correct QProperties""" device_signal_base.include_config_signals = True - assert device_signal_base._signal_filter == [BECSignalFilter.CONFIG] + assert device_signal_base._signal_filter == [Kind.config] device_signal_base.include_normal_signals = True - assert device_signal_base._signal_filter == [BECSignalFilter.CONFIG, BECSignalFilter.NORMAL] + assert device_signal_base._signal_filter == [Kind.config, Kind.normal] device_signal_base.include_hinted_signals = True - assert device_signal_base._signal_filter == [ - BECSignalFilter.CONFIG, - BECSignalFilter.NORMAL, - BECSignalFilter.HINTED, - ] + assert device_signal_base._signal_filter == [Kind.config, Kind.normal, Kind.hinted] def test_device_signal_set_device(device_signal_base):