0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

refactor: cleanup, added device_signal for signal inputs

This commit is contained in:
2024-10-21 13:00:55 +02:00
parent 0350833f36
commit 6fb20552ff
13 changed files with 288 additions and 235 deletions

View File

@ -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),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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