1
0
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:
2026-01-21 16:03:49 +01:00
committed by Jan Wyzula
parent 8d75c2af1c
commit 6e398e8077
3 changed files with 117 additions and 6 deletions

View File

@@ -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:

View File

@@ -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_()

View File

@@ -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"]