mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-04 16:02:51 +01:00
feat(device_combobox): device filter added based on its signal classes
This commit is contained in:
@@ -32,6 +32,7 @@ class DeviceInputConfig(ConnectionConfig):
|
||||
default: str | None = None
|
||||
arg_name: str | None = None
|
||||
apply_filter: bool = True
|
||||
signal_class_filter: list[str] = []
|
||||
|
||||
@field_validator("device_filter")
|
||||
@classmethod
|
||||
@@ -125,11 +126,13 @@ class DeviceInputBase(BECWidget):
|
||||
current_device = WidgetIO.get_value(widget=self, as_string=True)
|
||||
self.config.device_filter = self.device_filter
|
||||
self.config.readout_filter = self.readout_filter
|
||||
self.config.signal_class_filter = self.signal_class_filter
|
||||
if self.apply_filter is False:
|
||||
return
|
||||
all_dev = self.dev.enabled_devices
|
||||
devs = self._filter_devices_by_signal_class(all_dev)
|
||||
# Filter based on device class
|
||||
devs = [dev for dev in all_dev if self._check_device_filter(dev)]
|
||||
devs = [dev for dev in devs if self._check_device_filter(dev)]
|
||||
# Filter based on readout priority
|
||||
devs = [dev for dev in devs if self._check_readout_filter(dev)]
|
||||
self.devices = [device.name for device in devs]
|
||||
@@ -190,6 +193,27 @@ class DeviceInputBase(BECWidget):
|
||||
self.config.apply_filter = value
|
||||
self.update_devices_from_filters()
|
||||
|
||||
@SafeProperty("QStringList")
|
||||
def signal_class_filter(self) -> list[str]:
|
||||
"""
|
||||
Get the signal class filter for devices.
|
||||
|
||||
Returns:
|
||||
list[str]: List of signal class names used for filtering devices.
|
||||
"""
|
||||
return self.config.signal_class_filter
|
||||
|
||||
@signal_class_filter.setter
|
||||
def signal_class_filter(self, value: list[str] | None):
|
||||
"""
|
||||
Set the signal class filter and update the device list.
|
||||
|
||||
Args:
|
||||
value (list[str] | None): List of signal class names to filter by.
|
||||
"""
|
||||
self.config.signal_class_filter = value or []
|
||||
self.update_devices_from_filters()
|
||||
|
||||
@SafeProperty(bool)
|
||||
def filter_to_device(self):
|
||||
"""Include devices in filters."""
|
||||
@@ -379,6 +403,20 @@ class DeviceInputBase(BECWidget):
|
||||
"""
|
||||
return all(isinstance(device, self._device_handler[entry]) for entry in self.device_filter)
|
||||
|
||||
def _filter_devices_by_signal_class(
|
||||
self, devices: list[Device | BECSignal | ComputedSignal | Positioner]
|
||||
) -> list[Device | BECSignal | ComputedSignal | Positioner]:
|
||||
"""Filter devices by signal class, if a signal class filter is set."""
|
||||
if not self.config.signal_class_filter:
|
||||
return devices
|
||||
if not self.client or not hasattr(self.client, "device_manager"):
|
||||
return []
|
||||
signals = FilterIO.update_with_signal_class(
|
||||
widget=self, signal_class_filter=self.config.signal_class_filter, client=self.client
|
||||
)
|
||||
allowed_devices = {device_name for device_name, _, _ in signals}
|
||||
return [dev for dev in devices if dev.name in allowed_devices]
|
||||
|
||||
def _check_readout_filter(
|
||||
self, device: Device | BECSignal | ComputedSignal | Positioner
|
||||
) -> bool:
|
||||
|
||||
@@ -27,6 +27,7 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
|
||||
available_devices: List of available devices, if passed, it sets apply filters to false and device/readout priority filters will not be applied.
|
||||
default: Default device name.
|
||||
arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names.
|
||||
signal_class_filter: List of signal classes to filter the devices by. Only devices with signals of these classes will be shown.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["set_device", "devices"]
|
||||
@@ -51,6 +52,7 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
|
||||
available_devices: list[str] | None = None,
|
||||
default: str | None = None,
|
||||
arg_name: str | None = None,
|
||||
signal_class_filter: list[str] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
@@ -63,6 +65,7 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
|
||||
self._is_valid_input = False
|
||||
self._accent_colors = get_accent_colors()
|
||||
self._set_first_element_as_empty = False
|
||||
|
||||
# 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.
|
||||
@@ -85,6 +88,10 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
|
||||
# Device filter default is None
|
||||
if device_filter is not None:
|
||||
self.set_device_filter(device_filter)
|
||||
|
||||
if signal_class_filter is not None:
|
||||
self.signal_class_filter = signal_class_filter
|
||||
|
||||
# Set default device if passed
|
||||
if default is not None:
|
||||
self.set_device(default)
|
||||
@@ -184,18 +191,62 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
|
||||
app = QApplication([])
|
||||
apply_theme("dark")
|
||||
widget = QWidget()
|
||||
widget.setFixedSize(200, 200)
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
widget.setWindowTitle("DeviceComboBox demo")
|
||||
layout = QVBoxLayout(widget)
|
||||
|
||||
layout.addWidget(QLabel("Device filter controls"))
|
||||
controls = QHBoxLayout()
|
||||
layout.addLayout(controls)
|
||||
|
||||
class_input = QLineEdit()
|
||||
class_input.setPlaceholderText("signal_class_filter (comma-separated), e.g. AsyncSignal")
|
||||
controls.addWidget(class_input)
|
||||
|
||||
filter_device = QCheckBox("Device")
|
||||
filter_positioner = QCheckBox("Positioner")
|
||||
filter_signal = QCheckBox("Signal")
|
||||
filter_computed = QCheckBox("ComputedSignal")
|
||||
controls.addWidget(filter_device)
|
||||
controls.addWidget(filter_positioner)
|
||||
controls.addWidget(filter_signal)
|
||||
controls.addWidget(filter_computed)
|
||||
|
||||
combo = DeviceComboBox()
|
||||
combo.devices = ["samx", "dev1", "dev2", "dev3", "dev4"]
|
||||
combo.set_first_element_as_empty = True
|
||||
layout.addWidget(combo)
|
||||
|
||||
def _apply_filters():
|
||||
raw = class_input.text().strip()
|
||||
if raw:
|
||||
combo.signal_class_filter = [entry.strip() for entry in raw.split(",") if entry.strip()]
|
||||
else:
|
||||
combo.signal_class_filter = []
|
||||
combo.filter_to_device = filter_device.isChecked()
|
||||
combo.filter_to_positioner = filter_positioner.isChecked()
|
||||
combo.filter_to_signal = filter_signal.isChecked()
|
||||
combo.filter_to_computed_signal = filter_computed.isChecked()
|
||||
|
||||
class_input.textChanged.connect(_apply_filters)
|
||||
filter_device.toggled.connect(_apply_filters)
|
||||
filter_positioner.toggled.connect(_apply_filters)
|
||||
filter_signal.toggled.connect(_apply_filters)
|
||||
filter_computed.toggled.connect(_apply_filters)
|
||||
_apply_filters()
|
||||
|
||||
widget.show()
|
||||
app.exec_()
|
||||
|
||||
@@ -9,6 +9,7 @@ from bec_widgets.widgets.control.device_input.base_classes.device_input_base imp
|
||||
DeviceInputBase,
|
||||
DeviceInputConfig,
|
||||
)
|
||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
@@ -142,3 +143,24 @@ def test_device_input_base_properties(device_input_base):
|
||||
ReadoutPriority.MONITORED,
|
||||
ReadoutPriority.ON_REQUEST,
|
||||
]
|
||||
|
||||
|
||||
def test_device_combobox_signal_class_filter(qtbot, mocked_client):
|
||||
"""Test device filtering via signal_class_filter on combobox."""
|
||||
mocked_client.device_manager.get_bec_signals = mock.MagicMock(
|
||||
return_value=[
|
||||
("samx", "async_signal", {"signal_class": "AsyncSignal"}),
|
||||
("samy", "async_signal", {"signal_class": "AsyncSignal"}),
|
||||
("bpm4i", "async_signal", {"signal_class": "AsyncSignal"}),
|
||||
]
|
||||
)
|
||||
widget = create_widget(
|
||||
qtbot=qtbot,
|
||||
widget=DeviceComboBox,
|
||||
client=mocked_client,
|
||||
signal_class_filter=["AsyncSignal"],
|
||||
)
|
||||
|
||||
devices = [widget.itemText(i) for i in range(widget.count())]
|
||||
assert set(devices) == {"samx", "samy", "bpm4i"}
|
||||
assert widget.signal_class_filter == ["AsyncSignal"]
|
||||
|
||||
Reference in New Issue
Block a user