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

feat: add filter i/o utility class

This commit is contained in:
2024-10-08 15:31:02 +02:00
parent acb79020d4
commit 0350833f36
40 changed files with 1951 additions and 387 deletions

View File

@ -38,6 +38,8 @@ class Widgets(str, enum.Enum):
ResumeButton = "ResumeButton" ResumeButton = "ResumeButton"
RingProgressBar = "RingProgressBar" RingProgressBar = "RingProgressBar"
ScanControl = "ScanControl" ScanControl = "ScanControl"
SignalComboBox = "SignalComboBox"
SignalLineEdit = "SignalLineEdit"
StopButton = "StopButton" StopButton = "StopButton"
TextBox = "TextBox" TextBox = "TextBox"
VSCodeEditor = "VSCodeEditor" VSCodeEditor = "VSCodeEditor"
@ -2591,6 +2593,24 @@ class DeviceLineEdit(RPCBase):
""" """
class DeviceSignalInputBase(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def _get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
class LMFitDialog(RPCBase): class LMFitDialog(RPCBase):
@property @property
@rpc_call @rpc_call
@ -3003,6 +3023,42 @@ class ScanControl(RPCBase):
""" """
class SignalComboBox(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def _get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
class SignalLineEdit(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def _get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
class StopButton(RPCBase): class StopButton(RPCBase):
@property @property
@rpc_call @rpc_call

View File

View File

@ -0,0 +1,225 @@
from unittest.mock import MagicMock
from bec_lib.client import BECClient
from bec_lib.device import Device as BECDevice
from bec_lib.device import Positioner as BECPositioner
from bec_lib.device import ReadoutPriority
from bec_lib.devicemanager import DeviceContainer
from bec_lib.redis_connector import RedisConnector
class FakeDevice(BECDevice):
"""Fake minimal positioner class for testing."""
def __init__(self, name, enabled=True, readout_priority=ReadoutPriority.MONITORED):
super().__init__(name=name)
self._enabled = enabled
self.signals = {self.name: {"value": 1.0}}
self.description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
self._readout_priority = readout_priority
self._config = {
"readoutPriority": "baseline",
"deviceClass": "ophyd.Device",
"deviceConfig": {},
"deviceTags": ["user device"],
"enabled": enabled,
"readOnly": False,
"name": self.name,
}
@property
def readout_priority(self):
return self._readout_priority
@readout_priority.setter
def readout_priority(self, value):
self._readout_priority = value
@property
def limits(self) -> tuple[float, float]:
return self._limits
@limits.setter
def limits(self, value: tuple[float, float]):
self._limits = value
def __contains__(self, item):
return item == self.name
@property
def _hints(self):
return [self.name]
def set_value(self, fake_value: float = 1.0) -> None:
"""
Setup fake value for device readout
Args:
fake_value(float): Desired fake value
"""
self.signals[self.name]["value"] = fake_value
def describe(self) -> dict:
"""
Get the description of the device
Returns:
dict: Description of the device
"""
return self.description
class FakePositioner(BECPositioner):
def __init__(
self,
name,
enabled=True,
limits=None,
read_value=1.0,
readout_priority=ReadoutPriority.MONITORED,
):
super().__init__(name=name)
# self.limits = limits if limits is not None else [0.0, 0.0]
self.read_value = read_value
self._enabled = enabled
self._limits = limits
self._readout_priority = readout_priority
self.signals = {self.name: {"value": 1.0}}
self.description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
self._config = {
"readoutPriority": "baseline",
"deviceClass": "ophyd_devices.SimPositioner",
"deviceConfig": {"delay": 1, "tolerance": 0.01, "update_frequency": 400},
"deviceTags": ["user motors"],
"enabled": enabled,
"readOnly": False,
"name": self.name,
}
self._info = {
"signals": {
"readback": {"kind_str": "5"}, # hinted
"setpoint": {"kind_str": "1"}, # normal
"velocity": {"kind_str": "2"}, # config
}
}
@property
def readout_priority(self):
return self._readout_priority
@readout_priority.setter
def readout_priority(self, value):
self._readout_priority = value
@property
def enabled(self) -> bool:
return self._enabled
@enabled.setter
def enabled(self, value: bool):
self._enabled = value
@property
def limits(self) -> tuple[float, float]:
return self._limits
@limits.setter
def limits(self, value: tuple[float, float]):
self._limits = value
def __contains__(self, item):
return item == self.name
@property
def _hints(self):
return [self.name]
def set_value(self, fake_value: float = 1.0) -> None:
"""
Setup fake value for device readout
Args:
fake_value(float): Desired fake value
"""
self.signals[self.name]["value"] = fake_value
def describe(self) -> dict:
"""
Get the description of the device
Returns:
dict: Description of the device
"""
return self.description
@property
def precision(self):
return 3
def set_read_value(self, value):
self.read_value = value
def read(self):
return {
self.name: {"value": self.read_value},
f"{self.name}_setpoint": {"value": self.read_value},
f"{self.name}_motor_is_moving": {"value": 0},
}
def set_limits(self, limits):
self.limits = limits
def move(self, value, relative=False):
"""Simulates moving the device to a new position."""
if relative:
self.read_value += value
else:
self.read_value = value
# Respect the limits
self.read_value = max(min(self.read_value, self.limits[1]), self.limits[0])
@property
def readback(self):
return MagicMock(get=MagicMock(return_value=self.read_value))
class Positioner(FakePositioner):
"""just placeholder for testing embedded isinstance check in DeviceCombobox"""
def __init__(self, name="test", limits=None, read_value=1.0):
super().__init__(name, limits, read_value)
class Device(FakeDevice):
"""just placeholder for testing embedded isinstance check in DeviceCombobox"""
def __init__(self, name, enabled=True):
super().__init__(name, enabled)
class DMMock:
def __init__(self):
self.devices = DeviceContainer()
self.enabled_devices = [device for device in self.devices if device.enabled]
def add_devives(self, devices: list):
for device in devices:
self.devices[device.name] = device
DEVICES = [
FakePositioner("samx", limits=[-10, 10], read_value=2.0),
FakePositioner("samy", limits=[-5, 5], read_value=3.0),
FakePositioner("samz", limits=[-8, 8], read_value=4.0),
FakePositioner("aptrx", limits=None, read_value=4.0),
FakePositioner("aptry", limits=None, read_value=5.0),
FakeDevice("gauss_bpm"),
FakeDevice("gauss_adc1"),
FakeDevice("gauss_adc2"),
FakeDevice("gauss_adc3"),
FakeDevice("bpm4i"),
FakeDevice("bpm3a"),
FakeDevice("bpm3i"),
FakeDevice("eiger"),
FakeDevice("waveform1d"),
FakeDevice("async_device", readout_priority=ReadoutPriority.ASYNC),
Positioner("test", limits=[-10, 10], read_value=2.0),
Device("test_device"),
]

View File

@ -0,0 +1,156 @@
"""Module for handling filter I/O operations in BEC Widgets for input fields.
These operations include filtering device/signal names and/or device types.
"""
from abc import ABC, abstractmethod
from bec_lib.logger import bec_logger
from qtpy.QtCore import QStringListModel
from qtpy.QtWidgets import QComboBox, QCompleter, QLineEdit
logger = bec_logger.logger
class WidgetFilterHandler(ABC):
"""Abstract base class for widget filter handlers"""
@abstractmethod
def set_selection(self, widget, selection: list) -> None:
"""Set the filtered_selection for the widget
Args:
selection (list): Filtered selection of items
"""
@abstractmethod
def check_input(self, widget, text: str) -> bool:
"""Check if the input text is in the filtered selection
Args:
widget: Widget instance
text (str): Input text
Returns:
bool: True if the input text is in the filtered selection
"""
class LineEditFilterHandler(WidgetFilterHandler):
"""Handler for QLineEdit widget"""
def set_selection(self, widget: QLineEdit, selection: list) -> None:
"""Set the selection for the widget to the completer model
Args:
widget (QLineEdit): The QLineEdit widget
selection (list): Filtered selection of items
"""
if not isinstance(widget.completer, QCompleter):
completer = QCompleter(widget)
widget.setCompleter(completer)
widget.completer.setModel(QStringListModel(selection, widget))
def check_input(self, widget: QLineEdit, text: str) -> bool:
"""Check if the input text is in the filtered selection
Args:
widget (QLineEdit): The QLineEdit widget
text (str): Input text
Returns:
bool: True if the input text is in the filtered selection
"""
model = widget.completer.model()
model_data = [model.data(model.index(i)) for i in range(model.rowCount())]
return text in model_data
class ComboBoxFilterHandler(WidgetFilterHandler):
"""Handler for QComboBox widget"""
def set_selection(self, widget: QComboBox, selection: list) -> None:
"""Set the selection for the widget to the completer model
Args:
widget (QComboBox): The QComboBox widget
selection (list): Filtered selection of items
"""
widget.clear()
widget.addItems(selection)
def check_input(self, widget: QComboBox, text: str) -> bool:
"""Check if the input text is in the filtered selection
Args:
widget (QComboBox): The QComboBox widget
text (str): Input text
Returns:
bool: True if the input text is in the filtered selection
"""
return text in [widget.itemText(i) for i in range(widget.count())]
class FilterIO:
"""Public interface to set filters for input widgets.
It supports the list of widgets stored in class attribute _handlers.
"""
_handlers = {QLineEdit: LineEditFilterHandler, QComboBox: ComboBoxFilterHandler}
@staticmethod
def set_selection(widget, selection: list, ignore_errors=False):
"""
Retrieve value from the widget instance.
Args:
widget: Widget instance.
selection(list): List of filtered selection items.
ignore_errors(bool, optional): Whether to ignore if no handler is found.
"""
handler_class = FilterIO._find_handler(widget)
if handler_class:
return handler_class().set_selection(widget=widget, selection=selection)
if not ignore_errors:
raise ValueError(
f"No matching handler for widget type: {type(widget)} in handler list {FilterIO._handlers}"
)
return None
@staticmethod
def check_input(widget, text: str, ignore_errors=False):
"""
Check if the input text is in the filtered selection.
Args:
widget: Widget instance.
text(str): Input text.
ignore_errors(bool, optional): Whether to ignore if no handler is found.
Returns:
bool: True if the input text is in the filtered selection.
"""
handler_class = FilterIO._find_handler(widget)
if handler_class:
return handler_class().check_input(widget=widget, text=text)
if not ignore_errors:
raise ValueError(
f"No matching handler for widget type: {type(widget)} in handler list {FilterIO._handlers}"
)
return None
@staticmethod
def _find_handler(widget):
"""
Find the appropriate handler for the widget by checking its base classes.
Args:
widget: Widget instance.
Returns:
handler_class: The handler class if found, otherwise None.
"""
for base in type(widget).__mro__:
if base in FilterIO._handlers:
return FilterIO._handlers[base]
return None

View File

@ -1,22 +1,65 @@
from __future__ import annotations from __future__ import annotations
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 import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.filter_io import FilterIO
from bec_widgets.utils.widget_io import WidgetIO
logger = bec_logger.logger
class BECDeviceFilter(enum.Enum):
"""Filter for the device classes."""
DEVICE = "Device"
POSITIONER = "Positioner"
SIGNAL = "Signal"
COMPUTED_SIGNAL = "ComputedSignal"
class DeviceInputConfig(ConnectionConfig): class DeviceInputConfig(ConnectionConfig):
device_filter: str | list[str] | None = None device_filter: list[BECDeviceFilter] | None = None
readout_filter: list[ReadoutPriority] | None = None
devices: list[str] | None = None
default: str | None = None default: str | None = None
arg_name: str | None = None arg_name: str | None = None
class DeviceInputBase(BECWidget): class DeviceInputBase(BECWidget):
""" """
Mixin class for device input widgets. This class provides methods to get the device list and device object based Mixin base class for device input widgets.
on the current text of the widget. It allows to filter devices from BEC based on
device class and readout priority.
""" """
def __init__(self, client=None, config=None, gui_id=None): _device_handler = {
BECDeviceFilter.DEVICE: Device,
BECDeviceFilter.POSITIONER: Positioner,
BECDeviceFilter.SIGNAL: Signal,
BECDeviceFilter.COMPUTED_SIGNAL: ComputedSignal,
}
_filter_handler = {
BECDeviceFilter.DEVICE: "include_device",
BECDeviceFilter.POSITIONER: "include_positioner",
BECDeviceFilter.SIGNAL: "include_signal",
BECDeviceFilter.COMPUTED_SIGNAL: "include_computed_signal",
ReadoutPriority.MONITORED: "readout_monitored",
ReadoutPriority.BASELINE: "readout_baseline",
ReadoutPriority.ASYNC: "readout_async",
ReadoutPriority.CONTINUOUS: "readout_continuous",
ReadoutPriority.ON_REQUEST: "readout_on_request",
}
def __init__(self, client=None, config=None, gui_id: str = None):
if config is None: if config is None:
config = DeviceInputConfig(widget_class=self.__class__.__name__) config = DeviceInputConfig(widget_class=self.__class__.__name__)
else: else:
@ -24,15 +67,192 @@ class DeviceInputBase(BECWidget):
config = DeviceInputConfig(**config) config = DeviceInputConfig(**config)
self.config = config self.config = config
super().__init__(client=client, config=config, gui_id=gui_id) super().__init__(client=client, config=config, gui_id=gui_id)
self.get_bec_shortcuts() self.get_bec_shortcuts()
self._device_filter = None self._device_filter = []
self._readout_filter = []
self._devices = [] self._devices = []
### QtSlots ###
@Slot(str)
def set_device(self, device: str):
"""
Set the device.
Args:
device (str): Default name.
"""
if self.validate_device(device, raise_on_false=True) is True:
WidgetIO.set_value(widget=self, value=device)
self.config.default = device
else:
logger.warning(f"Device {device} is not in the filtered selection.")
@Slot()
def update_devices_from_filters(self):
"""Update the devices based on the current filter selection
in self.device_filter and self.readout_filter."""
self.config.device_filter = self.device_filter
self.config.readout_filter = self.readout_filter
all_dev = self.dev.enabled_devices
# Filter based on device class
devs = [dev for dev in all_dev 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]
@Slot(list)
def set_available_devices(self, devices: list[str]):
"""
Set the devices. If a device in the list is not valid, it will not be considered.
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
### QtProperties ###
@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."""
return self.config.default
@default.setter
def default(self, value: str):
if self.validate_device(value, raise_on_false=False) is False:
return
self.set_device(value)
@Property(bool)
def include_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()
@Property(bool)
def include_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):
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:
self._device_filter.remove(BECDeviceFilter.POSITIONER)
self.update_devices_from_filters()
@Property(bool)
def include_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):
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:
self._device_filter.remove(BECDeviceFilter.SIGNAL)
self.update_devices_from_filters()
@Property(bool)
def include_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):
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:
self._device_filter.remove(BECDeviceFilter.COMPUTED_SIGNAL)
self.update_devices_from_filters()
@Property(bool)
def readout_monitored(self):
"""Include devices with readout priority Monitored in filters."""
return ReadoutPriority.MONITORED in self.readout_filter
@readout_monitored.setter
def readout_monitored(self, value: bool):
if value is True and ReadoutPriority.MONITORED not in self.readout_filter:
self._readout_filter.append(ReadoutPriority.MONITORED)
if value is False and ReadoutPriority.MONITORED in self.readout_filter:
self._readout_filter.remove(ReadoutPriority.MONITORED)
self.update_devices_from_filters()
@Property(bool)
def readout_baseline(self):
"""Include devices with readout priority Baseline in filters."""
return ReadoutPriority.BASELINE in self.readout_filter
@readout_baseline.setter
def readout_baseline(self, value: bool):
if value is True and ReadoutPriority.BASELINE not in self.readout_filter:
self._readout_filter.append(ReadoutPriority.BASELINE)
if value is False and ReadoutPriority.BASELINE in self.readout_filter:
self._readout_filter.remove(ReadoutPriority.BASELINE)
self.update_devices_from_filters()
@Property(bool)
def readout_async(self):
"""Include devices with readout priority Async in filters."""
return ReadoutPriority.ASYNC in self.readout_filter
@readout_async.setter
def readout_async(self, value: bool):
if value is True and ReadoutPriority.ASYNC not in self.readout_filter:
self._readout_filter.append(ReadoutPriority.ASYNC)
if value is False and ReadoutPriority.ASYNC in self.readout_filter:
self._readout_filter.remove(ReadoutPriority.ASYNC)
self.update_devices_from_filters()
@Property(bool)
def readout_continuous(self):
"""Include devices with readout priority continuous in filters."""
return ReadoutPriority.CONTINUOUS in self.readout_filter
@readout_continuous.setter
def readout_continuous(self, value: bool):
if value is True and ReadoutPriority.CONTINUOUS not in self.readout_filter:
self._readout_filter.append(ReadoutPriority.CONTINUOUS)
if value is False and ReadoutPriority.CONTINUOUS in self.readout_filter:
self._readout_filter.remove(ReadoutPriority.CONTINUOUS)
self.update_devices_from_filters()
@Property(bool)
def readout_on_request(self):
"""Include devices with readout priority OnRequest in filters."""
return ReadoutPriority.ON_REQUEST in self.readout_filter
@readout_on_request.setter
def readout_on_request(self, value: bool):
if value is True and ReadoutPriority.ON_REQUEST not in self.readout_filter:
self._readout_filter.append(ReadoutPriority.ON_REQUEST)
if value is False and ReadoutPriority.ON_REQUEST in self.readout_filter:
self._readout_filter.remove(ReadoutPriority.ON_REQUEST)
self.update_devices_from_filters()
### Python Methods and Properties ###
@property @property
def devices(self) -> list[str]: def devices(self) -> list[str]:
""" """
Get the list of devices. Get the list of devices for the applied filters.
Returns: Returns:
list[str]: List of devices. list[str]: List of devices.
@ -41,83 +261,116 @@ class DeviceInputBase(BECWidget):
@devices.setter @devices.setter
def devices(self, value: list[str]): def devices(self, value: list[str]):
"""
Set the list of devices.
Args:
value: List of devices.
"""
self._devices = value self._devices = value
self.config.devices = value
FilterIO.set_selection(widget=self, selection=value)
def set_device_filter(self, device_filter: str | list[str]): @property
def device_filter(self) -> list[object]:
"""Get the list of filters to apply on the devices."""
return self._device_filter
@property
def readout_filter(self) -> list[str]:
"""Get the list of filters to apply on the devices"""
return self._readout_filter
def get_available_filters(self) -> list:
"""Get the available filters."""
return [entry for entry in BECDeviceFilter]
def get_readout_priority_filters(self) -> list:
"""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]
):
""" """
Set the device filter. Set the device filter. If None is passed, no filters are applied and all devices included.
Args: Args:
device_filter(str): Device filter, name of the device class. filter_selection (str | list[str]): Device filters. It is recommended to make an enum for the filters.
""" """
self.validate_device_filter(device_filter) filters = None
self.config.device_filter = device_filter if isinstance(filter_selection, list):
self._device_filter = device_filter filters = [self._filter_handler.get(entry) for entry in filter_selection]
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.")
for entry in filters:
setattr(self, entry, True)
def set_default_device(self, default_device: str): @typechecked
def set_readout_priority_filter(
self, filter_selection: str | ReadoutPriority | list[str] | list[ReadoutPriority]
):
""" """
Set the default device. Set the readout priority filter. If None is passed, all filters are included.
Args: Args:
default_device(str): Default device name. filter_selection (str | list[str]): Readout priority filters.
""" """
self.validate_device(default_device) filters = None
self.config.default = default_device if isinstance(filter_selection, list):
filters = [self._filter_handler.get(entry) for entry in filter_selection]
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(
f"Readout priority filter {filter_selection} is not in the readout priority list."
)
for entry in filters:
setattr(self, entry, True)
def get_device_list(self, filter: str | list[str] | None = None) -> list[str]: 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.
Get the list of device names based on the filter of current BEC client.
Args: Args:
filter(str|None): Class name filter to apply on the device list. 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.
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:
"""
Get the device object based on the device name.
Args:
device(str): Device name.
Returns: Returns:
devices(list[str]): List of device names. object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
""" """
all_devices = self.dev.enabled_devices self.validate_device(device)
if filter is not None: dev = getattr(self.dev, device.lower(), None)
self.validate_device_filter(filter) if dev is None:
if isinstance(filter, str): raise ValueError(
filter = [filter] f"Device {device} is not found in devicemanager {self.dev} as enabled device."
devices = [device.name for device in all_devices if device.__class__.__name__ in filter] )
else: return dev
devices = [device.name for device in all_devices]
return devices
def get_available_filters(self): def validate_device(self, device: str, raise_on_false: bool = False) -> bool:
""" """
Get the available device classes which can be used as filters. Validate the device if it is present in the filtered device selection.
"""
all_devices = self.dev.enabled_devices
filters = {device.__class__.__name__ for device in all_devices}
return filters
def validate_device_filter(self, filter: str | list[str]) -> None:
"""
Validate the device filter if the class name is present in the current BEC instance.
Args:
filter(str|list[str]): Class name to use as a device filter.
"""
if isinstance(filter, str):
filter = [filter]
available_filters = self.get_available_filters()
for f in filter:
if f not in available_filters:
raise ValueError(f"Device filter {f} is not valid.")
def validate_device(self, device: str) -> None:
"""
Validate the device if it is present in current BEC instance.
Args: Args:
device(str): Device to validate. device(str): Device to validate.
""" """
if device not in self.get_device_list(self.config.device_filter): if device in self.devices:
raise ValueError(f"Device {device} is not valid.") return True
if raise_on_false is True:
raise ValueError(f"Device {device} is not in filtered selection.")

View File

@ -0,0 +1,277 @@
import enum
from bec_lib.logger import bec_logger
from qtpy.QtCore import Property, Slot
from bec_widgets.utils import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.filter_io import FilterIO
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."""
signal_filter: str | list[str] | None = None
default: str | None = None
arg_name: str | None = None
device: str | None = None
signals: list[str] | None = None
class DeviceSignalInputBase(BECWidget):
"""
Mixin base class for device signal input widgets.
Mixin class for device signal input widgets. This class provides methods to get the device signal list and device
signal object based on the current text of the widget.
"""
_filter_handler = {
BECSignalFilter.HINTED: "include_hinted_signals",
BECSignalFilter.NORMAL: "include_normal_signals",
BECSignalFilter.CONFIG: "include_config_signals",
}
def __init__(self, client=None, config=None, gui_id: str = None):
if config is None:
config = DeviceSignalInputBaseConfig(widget_class=self.__class__.__name__)
else:
if isinstance(config, dict):
config = DeviceSignalInputBaseConfig(**config)
self.config = config
super().__init__(client=client, config=config, gui_id=gui_id)
self._device = None
self.get_bec_shortcuts()
self._signal_filter = []
self._signals = []
self._hinted_signals = []
self._normal_signals = []
self._config_signals = []
### Qt Slots ###
@Slot(str)
def set_signal(self, signal: str):
"""
Set the signal.
Args:
signal (str): signal name.
"""
if self.validate_signal(signal, raise_on_false=False) is True:
WidgetIO.set_value(widget=self, value=signal)
self.config.default = signal
else:
logger.warning(
f"Signal {signal} not found for device {self.device} and filtered selection {self.signal_filter}."
)
@Slot(str)
def set_device(self, device: str | None):
"""
Set the device.
Args:
device(str): device name.
"""
if device is None:
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.")
@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."""
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:
return
device = self.get_device_object(self._device)
device_info = device._info["signals"]
if BECSignalFilter.HINTED in self.signal_filter or len(self.signal_filter) == 0:
hinted_signals = [
signal
for signal, signal_info in device_info.items()
if (signal_info.get("kind_str", None) == BECSignalFilter.HINTED)
]
self._hinted_signals = hinted_signals
if BECSignalFilter.NORMAL in self.signal_filter or len(self.signal_filter) == 0:
normal_signals = [
signal
for signal, signal_info in device_info.items()
if (signal_info.get("kind_str", None) == BECSignalFilter.NORMAL)
]
self._normal_signals = normal_signals
if BECSignalFilter.CONFIG in self.signal_filter or len(self.signal_filter) == 0:
config_signals = [
signal
for signal, signal_info in device_info.items()
if (signal_info.get("kind_str", None) == BECSignalFilter.CONFIG)
]
self._config_signals = config_signals
self._signals = self._hinted_signals + self._normal_signals + self._config_signals
FilterIO.set_selection(widget=self, selection=self.signals)
### Qt Properties ###
@Property(str)
def device(self) -> str:
"""Get the selected device."""
if self._device is None:
return ""
return self._device
@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()
@Property(bool)
def include_hinted_signals(self):
"""Include hinted signals in filters."""
return BECSignalFilter.HINTED in self.signal_filter
@include_hinted_signals.setter
def include_hinted_signals(self, value: bool):
if value:
self._signal_filter.append(BECSignalFilter.HINTED)
else:
self._signal_filter.remove(BECSignalFilter.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
@include_normal_signals.setter
def include_normal_signals(self, value: bool):
if value:
self._signal_filter.append(BECSignalFilter.NORMAL)
else:
self._signal_filter.remove(BECSignalFilter.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
@include_config_signals.setter
def include_config_signals(self, value: bool):
if value:
self._signal_filter.append(BECSignalFilter.CONFIG)
else:
self._signal_filter.remove(BECSignalFilter.CONFIG)
self.update_signals_from_filters()
### Properties and Methods ###
@property
def signals(self) -> list[str]:
"""
Get the list of device signals for the applied filters.
Returns:
list[str]: List of device signals.
"""
return self._signals
@signals.setter
def signals(self, value: list[str]):
self._signals = value
self.config.signals = value
FilterIO.set_selection(widget=self, selection=value)
@property
def signal_filter(self) -> list[str]:
"""Get the list of filters to apply on the device signals."""
return self._signal_filter
def get_available_filters(self) -> list[str]:
"""Get the available filters."""
return [entry for entry in self._filter_handler]
def set_filter(self, filter_selection: str | list[str]):
"""
Set the device filter. If None, all devices are included.
Args:
filter_selection (str | list[str]): Device filters from BECDeviceFilter and BECReadoutPriority.
"""
filters = None
if isinstance(filter_selection, list):
filters = [self._filter_handler.get(entry) for entry in filter_selection]
if isinstance(filter_selection, str):
filters = [self._filter_handler.get(filter_selection)]
if filters is None:
return
for entry in filters:
setattr(self, entry, True)
def get_device_object(self, device: str) -> object:
"""
Get the device object based on the device name.
Args:
device(str): Device name.
Returns:
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
"""
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}.")
return dev
def validate_device(self, device: str, raise_on_false: bool = False) -> bool:
"""
Validate the device if it is present in current BEC instance.
Args:
device(str): Device to validate.
"""
if device in self.dev:
return True
if raise_on_false is True:
raise ValueError(f"Device {device} not found in devicemanager.")
return False
def validate_signal(self, signal: str, raise_on_false: bool = False) -> bool:
"""
Validate the signal if it is present in the device signals.
Args:
signal(str): Signal to validate.
"""
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

@ -27,7 +27,7 @@ class DapComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML return DOM_XML
def group(self): def group(self):
return "BEC Selection Widgets" return "BEC Input Widgets"
def icon(self): def icon(self):
return designer_material_icon(DapComboBox.ICON_NAME) return designer_material_icon(DapComboBox.ICON_NAME)

View File

@ -31,7 +31,7 @@ class DeviceComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML return DOM_XML
def group(self): def group(self):
return "Device Control" return "BEC Input Widgets"
def icon(self): def icon(self):
return designer_material_icon(DeviceComboBox.ICON_NAME) return designer_material_icon(DeviceComboBox.ICON_NAME)

View File

@ -1,23 +1,25 @@
from typing import TYPE_CHECKING from bec_lib.device import ReadoutPriority
from qtpy.QtCore import QSize
from qtpy.QtWidgets import QComboBox, QSizePolicy
from qtpy.QtWidgets import QComboBox from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.widgets.base_classes.device_input_base import (
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputBase, DeviceInputConfig BECDeviceFilter,
DeviceInputBase,
if TYPE_CHECKING: DeviceInputConfig,
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputConfig )
class DeviceComboBox(DeviceInputBase, QComboBox): class DeviceComboBox(DeviceInputBase, QComboBox):
""" """
Line edit widget for device input with autocomplete for device names. Combobox widget for device input with autocomplete for device names.
Args: Args:
parent: Parent widget. parent: Parent widget.
client: BEC client object. client: BEC client object.
config: Device input configuration. config: Device input configuration.
gui_id: GUI ID. gui_id: GUI ID.
device_filter: Device filter, name of the device class. device_filter: Device filter, name of the device class from BECDeviceFilter and BECReadoutPriority. Check DeviceInputBase for more details.
default: Default device name. 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. arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names.
""" """
@ -30,57 +32,68 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
client=None, client=None,
config: DeviceInputConfig = None, config: DeviceInputConfig = None,
gui_id: str | None = None, gui_id: str | None = None,
device_filter: str | None = None, device_filter: BECDeviceFilter | list[BECDeviceFilter] | None = None,
readout_priority_filter: (
str | ReadoutPriority | list[str] | list[ReadoutPriority] | None
) = None,
device_list: list[str] | None = None,
default: str | None = None, default: str | None = None,
arg_name: str | None = None, arg_name: str | None = None,
): ):
super().__init__(client=client, config=config, gui_id=gui_id) super().__init__(client=client, config=config, gui_id=gui_id)
QComboBox.__init__(self, parent=parent) QComboBox.__init__(self, parent=parent)
self.setMinimumSize(125, 26)
self.populate_combobox()
if arg_name is not None: if arg_name is not None:
self.config.arg_name = arg_name self.config.arg_name = arg_name
self.arg_name = arg_name
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
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
)
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
if device_filter is not None: if device_filter is not None:
self.set_device_filter(device_filter) 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
if default is not None: if default is not None:
self.set_default_device(default) self.set_device(default)
def set_device_filter(self, device_filter: str): def get_current_device(self) -> object:
""" """
Set the device filter. Get the current device object based on the current value.
Args:
device_filter(str): Device filter, name of the device class.
"""
super().set_device_filter(device_filter)
self.populate_combobox()
def set_default_device(self, default_device: str):
"""
Set the default device.
Args:
default_device(str): Default device name.
"""
super().set_default_device(default_device)
self.setCurrentText(default_device)
def populate_combobox(self):
"""Populate the combobox with the devices."""
self.devices = self.get_device_list(self.config.device_filter)
self.clear()
self.addItems(self.devices)
def get_device(self) -> object:
"""
Get the selected device object.
Returns: Returns:
object: Device object. object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
""" """
device_name = self.currentText() dev_name = self.currentText()
device_obj = getattr(self.dev, device_name.lower(), None) return self.get_device_object(dev_name)
if device_obj is None:
raise ValueError(f"Device {device_name} is not found.")
return device_obj if __name__ == "__main__": # pragma: no cover
# pylint: disable=import-outside-toplevel
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.utils.colors import set_theme
app = QApplication([])
set_theme("dark")
widget = QWidget()
widget.setFixedSize(200, 200)
layout = QVBoxLayout()
widget.setLayout(layout)
combo = DeviceComboBox()
layout.addWidget(combo)
widget.show()
app.exec_()

View File

@ -1,13 +1,14 @@
from typing import TYPE_CHECKING from bec_lib.device import ReadoutPriority
from qtpy.QtCore import QSize
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 QCompleter, QLineEdit, QSizePolicy
from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputBase, DeviceInputConfig from bec_widgets.widgets.base_classes.device_input_base import (
BECDeviceFilter,
if TYPE_CHECKING: DeviceInputBase,
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputConfig DeviceInputConfig,
)
class DeviceLineEdit(DeviceInputBase, QLineEdit): class DeviceLineEdit(DeviceInputBase, QLineEdit):
@ -19,7 +20,7 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
client: BEC client object. client: BEC client object.
config: Device input configuration. config: Device input configuration.
gui_id: GUI ID. gui_id: GUI ID.
device_filter: Device filter, name of the device class. device_filter: Device filter, name of the device class from BECDeviceFilter and ReadoutPriority. Check DeviceInputBase for more details.
default: Default device name. 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. arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names.
""" """
@ -34,80 +35,99 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
client=None, client=None,
config: DeviceInputConfig = None, config: DeviceInputConfig = None,
gui_id: str | None = None, gui_id: str | None = None,
device_filter: str | list[str] | None = None, device_filter: BECDeviceFilter | list[BECDeviceFilter] | None = None,
readout_priority_filter: (
str | ReadoutPriority | list[str] | list[ReadoutPriority] | None
) = None,
device_list: list[str] | None = None,
default: str | None = None, default: str | None = None,
arg_name: str | None = None, arg_name: str | None = None,
): ):
super().__init__(client=client, config=config, gui_id=gui_id) super().__init__(client=client, config=config, gui_id=gui_id)
QLineEdit.__init__(self, parent=parent) QLineEdit.__init__(self, parent=parent)
self._is_valid_input = False
self.completer = QCompleter(self) self.completer = QCompleter(self)
self.setCompleter(self.completer) self.setCompleter(self.completer)
self.populate_completer()
if arg_name is not None: if arg_name is not None:
self.config.arg_name = arg_name self.config.arg_name = arg_name
self.arg_name = arg_name self.arg_name = arg_name
if device_filter is not None:
self.set_device_filter(device_filter)
if default is not None:
self.set_default_device(default)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.setMinimumSize(QSize(100, 0)) 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
)
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
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
if default is not None:
self.set_device(default)
self.textChanged.connect(self.check_validity)
self.editingFinished.connect(self.emit_device_selected) def get_current_device(self) -> object:
@Slot()
def emit_device_selected(self):
""" """
Editing finished, let's see which device is selected and emit signal Get the current device object based on the current value.
"""
device_name = self.text().lower()
device_obj = getattr(self.dev, device_name, None)
if device_obj is not None:
self.device_selected.emit(device_name)
def set_device_filter(self, device_filter: str | list[str]):
"""
Set the device filter.
Args:
device_filter (str | list[str]): Device filter, name of the device class.
"""
super().set_device_filter(device_filter)
self.populate_completer()
def set_default_device(self, default_device: str):
"""
Set the default device.
Args:
default_device (str): Default device name.
"""
super().set_default_device(default_device)
self.setText(default_device)
def populate_completer(self):
"""Populate the completer with the devices."""
self.devices = self.get_device_list(self.config.device_filter)
self.completer.setModel(self.create_completer_model(self.devices))
def create_completer_model(self, devices: list[str]):
"""Create a model for the completer."""
from qtpy.QtCore import QStringListModel
return QStringListModel(devices, self)
def get_device(self) -> object:
"""
Get the selected device object.
Returns: Returns:
object: Device object. object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
""" """
device_name = self.text() dev_name = self.text()
device_obj = getattr(self.dev, device_name.lower(), None) return self.get_device_object(dev_name)
if device_obj is None:
raise ValueError(f"Device {device_name} is not found.") def paintEvent(self, event: QPaintEvent) -> None:
return device_obj """Extend the paint event to set the border color based on the validity of the input.
Args:
event (PySide6.QtGui.QPaintEvent) : Paint event.
"""
super().paintEvent(event)
painter = QPainter(self)
pen = QPen()
pen.setWidth(2)
if self._is_valid_input is False and self.isEnabled() is True:
pen.setColor(self._accent_colors.emergency)
painter.setPen(pen)
painter.drawRect(self.rect().adjusted(1, 1, -1, -1))
def check_validity(self, input_text: str) -> None:
"""
Check if the current value is a valid device name.
"""
if self.validate_device(input_text) is True:
self._is_valid_input = True
self.device_selected.emit(input_text.lower())
else:
self._is_valid_input = False
self.update()
if __name__ == "__main__": # pragma: no cover
# pylint: disable=import-outside-toplevel
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.utils.colors import set_theme
app = QApplication([])
set_theme("dark")
widget = QWidget()
widget.setFixedSize(200, 200)
layout = QVBoxLayout()
widget.setLayout(layout)
line_edit = DeviceLineEdit()
line_edit.include_positioner = True
layout.addWidget(line_edit)
widget.show()
app.exec_()

View File

@ -31,7 +31,7 @@ class DeviceLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return DOM_XML return DOM_XML
def group(self): def group(self):
return "Device Control" return "BEC Input Widgets"
def icon(self): def icon(self):
return designer_material_icon(DeviceLineEdit.ICON_NAME) return designer_material_icon(DeviceLineEdit.ICON_NAME)

View File

@ -16,6 +16,7 @@ from bec_widgets.qt_utils.toolbar import (
WidgetAction, WidgetAction,
) )
from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
from bec_widgets.widgets.figure import BECFigure from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.figure.plots.axis_settings import AxisSettings from bec_widgets.widgets.figure.plots.axis_settings import AxisSettings
@ -69,7 +70,7 @@ class BECImageWidget(BECWidget, QWidget):
self.toolbar = ModularToolBar( self.toolbar = ModularToolBar(
actions={ actions={
"monitor": DeviceSelectionAction( "monitor": DeviceSelectionAction(
"Monitor:", DeviceComboBox(device_filter="Device") "Monitor:", DeviceComboBox(device_filter=BECDeviceFilter.DEVICE)
), ),
"monitor_type": WidgetAction(widget=self.dim_combo_box), "monitor_type": WidgetAction(widget=self.dim_combo_box),
"connect": MaterialIconAction(icon_name="link", tooltip="Connect Device"), "connect": MaterialIconAction(icon_name="link", tooltip="Connect Device"),

View File

@ -2,11 +2,13 @@ from __future__ import annotations
import sys import sys
from bec_lib.device import ReadoutPriority
from qtpy.QtWidgets import QVBoxLayout, QWidget from qtpy.QtWidgets import QVBoxLayout, QWidget
from bec_widgets.qt_utils.settings_dialog import SettingsDialog from bec_widgets.qt_utils.settings_dialog import SettingsDialog
from bec_widgets.qt_utils.toolbar import DeviceSelectionAction, MaterialIconAction, ModularToolBar from bec_widgets.qt_utils.toolbar import DeviceSelectionAction, MaterialIconAction, ModularToolBar
from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
from bec_widgets.widgets.figure import BECFigure from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.figure.plots.motor_map.motor_map import MotorMapConfig from bec_widgets.widgets.figure.plots.motor_map.motor_map import MotorMapConfig
@ -50,10 +52,10 @@ class BECMotorMapWidget(BECWidget, QWidget):
self.toolbar = ModularToolBar( self.toolbar = ModularToolBar(
actions={ actions={
"motor_x": DeviceSelectionAction( "motor_x": DeviceSelectionAction(
"Motor X:", DeviceComboBox(device_filter="Positioner") "Motor X:", DeviceComboBox(device_filter=[BECDeviceFilter.POSITIONER])
), ),
"motor_y": DeviceSelectionAction( "motor_y": DeviceSelectionAction(
"Motor Y:", DeviceComboBox(device_filter="Positioner") "Motor Y:", DeviceComboBox(device_filter=[BECDeviceFilter.POSITIONER])
), ),
"connect": MaterialIconAction(icon_name="link", tooltip="Connect Motors"), "connect": MaterialIconAction(icon_name="link", tooltip="Connect Motors"),
"history": MaterialIconAction(icon_name="history", tooltip="Reset Trace History"), "history": MaterialIconAction(icon_name="history", tooltip="Reset Trace History"),

View File

@ -18,6 +18,7 @@ from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
from bec_widgets.utils import UILoader from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import get_accent_colors, set_theme from bec_widgets.utils.colors import get_accent_colors, set_theme
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
logger = bec_logger.logger logger = bec_logger.logger
@ -97,7 +98,9 @@ class PositionerBox(BECWidget, CompactPopupWidget):
self._dialog = QDialog(self) self._dialog = QDialog(self)
self._dialog.setWindowTitle("Positioner Selection") self._dialog.setWindowTitle("Positioner Selection")
layout = QVBoxLayout() layout = QVBoxLayout()
line_edit = DeviceLineEdit(self, client=self.client, device_filter="Positioner") line_edit = DeviceLineEdit(
self, client=self.client, device_filter=[BECDeviceFilter.POSITIONER]
)
line_edit.textChanged.connect(self.set_positioner) line_edit.textChanged.connect(self.set_positioner)
layout.addWidget(line_edit) layout.addWidget(line_edit)
close_button = QPushButton("Close") close_button = QPushButton("Close")

View File

@ -306,7 +306,7 @@ class ScanGroupBox(QGroupBox):
try: # In case that the bundle size changes try: # In case that the bundle size changes
widget = self.layout.itemAtPosition(i, j).widget() widget = self.layout.itemAtPosition(i, j).widget()
if isinstance(widget, DeviceLineEdit) and device_object: if isinstance(widget, DeviceLineEdit) and device_object:
value = widget.get_device() value = widget.get_current_device()
else: else:
value = WidgetIO.get_value(widget) value = WidgetIO.get_value(widget)
args.append(value) args.append(value)

View File

@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.signal_combobox.signal_combobox_plugin import SignalComboBoxPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(SignalComboBoxPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@ -0,0 +1,90 @@
from qtpy.QtCore import QSize
from qtpy.QtWidgets import QComboBox, QSizePolicy
from bec_widgets.utils.filter_io import ComboBoxFilterHandler, FilterIO
from bec_widgets.widgets.base_classes.device_signal_input_base import DeviceSignalInputBase
class SignalComboBox(DeviceSignalInputBase, QComboBox):
"""
Line edit widget for device input with autocomplete for device names.
Args:
parent: Parent widget.
client: BEC client object.
config: Device input configuration.
gui_id: GUI ID.
device_filter: Device filter, name of the device class from BECDeviceFilter and BECReadoutPriority. Check DeviceInputBase for more details.
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.
"""
ICON_NAME = "list_alt"
def __init__(
self,
parent=None,
client=None,
config: DeviceSignalInputBase = None,
gui_id: str | None = None,
device: str | None = None,
signal_filter: str | list[str] | None = None,
default: str | None = None,
arg_name: str | None = None,
):
super().__init__(client=client, config=config, gui_id=gui_id)
QComboBox.__init__(self, parent=parent)
if arg_name is not None:
self.config.arg_name = arg_name
self.arg_name = arg_name
if default is not None:
self.set_device(default)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.setMinimumSize(QSize(100, 0))
signal_filter = signal_filter if not None else self.config.signal_filter
if signal_filter is not None:
self.set_filter(signal_filter)
device = device if not None else self.config.device
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)
def update_signals_from_filters(self):
"""Update the filters for the combobox"""
super().update_signals_from_filters()
# pylint: disable=protected-access
if FilterIO._find_handler(self) is ComboBoxFilterHandler:
if len(self._config_signals) > 0:
self.insertItem(
len(self._hinted_signals) + len(self._normal_signals), "Config Signals"
)
self.model().item(len(self._hinted_signals) + len(self._normal_signals)).setEnabled(
False
)
if len(self._normal_signals) > 0:
self.insertItem(len(self._hinted_signals), "Normal Signals")
self.model().item(len(self._hinted_signals)).setEnabled(False)
if len(self._hinted_signals) > 0:
self.insertItem(0, "Hinted Signals")
self.model().item(0).setEnabled(False)
if __name__ == "__main__": # pragma: no cover
# pylint: disable=import-outside-toplevel
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.utils.colors import set_theme
app = QApplication([])
set_theme("dark")
widget = QWidget()
widget.setFixedSize(200, 200)
layout = QVBoxLayout()
widget.setLayout(layout)
box = SignalComboBox(device="samx")
layout.addWidget(box)
widget.show()
app.exec_()

View File

@ -0,0 +1 @@
{'files': ['signal_combobox.py']}

View File

@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.signal_combobox.signal_combobox import SignalComboBox
DOM_XML = """
<ui language='c++'>
<widget class='SignalComboBox' name='signal_combobox'>
</widget>
</ui>
"""
class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = SignalComboBox(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "BEC Input Widgets"
def icon(self):
return designer_material_icon(SignalComboBox.ICON_NAME)
def includeFile(self):
return "signal_combobox"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "SignalComboBox"
def toolTip(self):
return ""
def whatsThis(self):
return self.toolTip()

View File

@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.signal_line_edit.signal_line_edit_plugin import SignalLineEditPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(SignalLineEditPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@ -0,0 +1,114 @@
from qtpy.QtCore import QSize, Slot
from qtpy.QtGui import QPainter, QPaintEvent, QPen
from qtpy.QtWidgets import QCompleter, QLineEdit, QSizePolicy
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.widgets.base_classes.device_signal_input_base import DeviceSignalInputBase
class SignalLineEdit(DeviceSignalInputBase, QLineEdit):
"""
Line edit widget for device input with autocomplete for device names.
Args:
parent: Parent widget.
client: BEC client object.
config: Device input configuration.
gui_id: GUI ID.
device_filter: Device filter, name of the device class from BECDeviceFilter and BECReadoutPriority. Check DeviceInputBase for more details.
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.
"""
ICON_NAME = "vital_signs"
def __init__(
self,
parent=None,
client=None,
config: DeviceSignalInputBase = None,
gui_id: str | None = None,
device: str | None = None,
signal_filter: str | list[str] | None = None,
default: str | None = None,
arg_name: str | None = None,
):
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)
if arg_name is not None:
self.config.arg_name = arg_name
self.arg_name = arg_name
if default is not None:
self.set_device(default)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.setMinimumSize(QSize(100, 0))
signal_filter = signal_filter if not None else self.config.signal_filter
if signal_filter is not None:
self.set_filter(signal_filter)
device = device if not None else self.config.device
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)
def get_current_device(self) -> object:
"""
Get the current device object based on the current value.
Returns:
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
"""
dev_name = self.text()
return self.get_device_object(dev_name)
def paintEvent(self, event: QPaintEvent) -> None:
"""Extend the paint event to set the border color based on the validity of the input.
Args:
event (PySide6.QtGui.QPaintEvent) : Paint event.
"""
super().paintEvent(event)
painter = QPainter(self)
pen = QPen()
pen.setWidth(2)
if self._is_valid_input is False and self.isEnabled() is True:
pen.setColor(self._accent_colors.emergency)
painter.setPen(pen)
painter.drawRect(self.rect().adjusted(1, 1, -1, -1))
@Slot(str)
def check_validity(self, input_text: str) -> None:
"""
Check if the current value is a valid device name.
"""
# i
if self.validate_signal(input_text) is True:
self._is_valid_input = True
else:
self._is_valid_input = False
self.update()
if __name__ == "__main__": # pragma: no cover
# pylint: disable=import-outside-toplevel
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.utils.colors import set_theme
app = QApplication([])
set_theme("dark")
widget = QWidget()
widget.setFixedSize(200, 200)
layout = QVBoxLayout()
widget.setLayout(layout)
layout.addWidget(SignalLineEdit(device="samx"))
widget.show()
app.exec_()

View File

@ -0,0 +1 @@
{'files': ['signal_line_edit.py']}

View File

@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.signal_line_edit.signal_line_edit import SignalLineEdit
DOM_XML = """
<ui language='c++'>
<widget class='SignalLineEdit' name='signal_line_edit'>
</widget>
</ui>
"""
class SignalLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = SignalLineEdit(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "BEC Input Widgets"
def icon(self):
return designer_material_icon(SignalLineEdit.ICON_NAME)
def includeFile(self):
return "signal_line_edit"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "SignalLineEdit"
def toolTip(self):
return ""
def whatsThis(self):
return self.toolTip()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -13,9 +13,11 @@ The `DeviceLineEdit` widget provides a line edit interface with autocomplete fun
The `DeviceComboBox` widget offers a dropdown interface for device selection, providing a more visual way to browse through available devices. The `DeviceComboBox` widget offers a dropdown interface for device selection, providing a more visual way to browse through available devices.
## Key Features: ## Key Features:
- **Device Filtering**: Both widgets allow users to filter devices by their class names, ensuring that only relevant devices are shown. - **Device Filtering**: Both widgets allow users to filter devices by device type and readout priority, ensuring that only relevant devices are shown.
- **Default Device Setting**: Users can set a default device to be pre-selected when the widget is initialized. - **Default Device Setting**: Users can set a default device to be pre-selected when the widget is initialized.
- **Set Device Selection**: Both widgets allow users to set the available devices to be displayed independent of the applied filters.
- **Real-Time Autocomplete (LineEdit)**: The `DeviceLineEdit` widget supports real-time autocomplete, helping users find devices faster. - **Real-Time Autocomplete (LineEdit)**: The `DeviceLineEdit` widget supports real-time autocomplete, helping users find devices faster.
- **Real-Time Input Validation (LineEdit)**: User input is validated in real-time with a red border around the `DeviceLineEdit` indicating an invalid input.
- **Dropdown Selection (ComboBox)**: The `DeviceComboBox` widget displays devices in a dropdown list, making selection straightforward. - **Dropdown Selection (ComboBox)**: The `DeviceComboBox` widget displays devices in a dropdown list, making selection straightforward.
- **QtDesigner Integration**: Both widgets can be added as custom widgets in `QtDesigner` or instantiated directly in code. - **QtDesigner Integration**: Both widgets can be added as custom widgets in `QtDesigner` or instantiated directly in code.
@ -28,11 +30,15 @@ Both `DeviceLineEdit` and `DeviceComboBox` can be integrated within a GUI applic
## Example 1 - Creating a DeviceLineEdit in Code ## Example 1 - Creating a DeviceLineEdit in Code
In this example, we demonstrate how to create a `DeviceLineEdit` widget in code and customize its behavior. In this example, we demonstrate how to create a `DeviceLineEdit` widget in code and customize its behavior.
We filter down to Positioners with readout_priority Baseline.
Note, if we do not specify a device_filter or readout_filter, all enabled devices will be included.
```python ```python
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.widgets.device_line_edit import DeviceLineEdit from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
from bec_lib.device import ReadoutPriority
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
class MyGui(QWidget): class MyGui(QWidget):
def __init__(self): def __init__(self):
@ -40,7 +46,7 @@ class MyGui(QWidget):
self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget
# Create and add the DeviceLineEdit to the layout # Create and add the DeviceLineEdit to the layout
self.device_line_edit = DeviceLineEdit(device_filter="Motor") self.device_line_edit = DeviceLineEdit(device_filter=BECDeviceFilter.POSITIONER, readout_priority_filter=ReadoutPriority.BASELINE)
self.layout().addWidget(self.device_line_edit) self.layout().addWidget(self.device_line_edit)
# Example of how this custom GUI might be used: # Example of how this custom GUI might be used:
@ -56,7 +62,9 @@ Similarly, here is an example of creating a `DeviceComboBox` widget in code and
```python ```python
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.widgets.device_combo_box import DeviceComboBox from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
from bec_lib.device import ReadoutPriority
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
class MyGui(QWidget): class MyGui(QWidget):
def __init__(self): def __init__(self):
@ -64,8 +72,8 @@ class MyGui(QWidget):
self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget
# Create and add the DeviceComboBox to the layout # Create and add the DeviceComboBox to the layout
self.device_combo_box = DeviceComboBox(device_filter="Motor") self.device_combobox = DeviceComboBox(device_filter=BECDeviceFilter.POSITIONER, readout_priority_filter=ReadoutPriority.BASELINE)
self.layout().addWidget(self.device_combo_box) self.layout().addWidget(self.device_combobox)
# Example of how this custom GUI might be used: # Example of how this custom GUI might be used:
app = QApplication([]) app = QApplication([])
@ -80,11 +88,24 @@ Both `DeviceLineEdit` and `DeviceComboBox` allow you to set a default device tha
```python ```python
# Set default device for DeviceLineEdit # Set default device for DeviceLineEdit
self.device_line_edit.set_default_device("motor1") self.device_line_edit.set_device("motor1")
# Set default device for DeviceComboBox # Set default device for DeviceComboBox
self.device_combo_box.set_default_device("motor2") self.device_combo_box.set_device("motor2")
# Set the available devices to be displayed independent of the applied filters
self.device_combo_box.set_available_devices(["motor1", "motor2", "motor3"])
``` ```
````
````{tab} BEC Designer
Both widgets are also available as plugins for the BEC Designer. We have included Qt properties for both widgets, allowing customization of filtering and default device settings directly from the designer. In addition to the common signals and slots for `DeviceLineEdit` and `DeviceComboBox`, the following slots are available:
- `set_device(str)` to set the default device
- `update_devices()` to refresh the devices list
The following Qt properties are also included:
```{figure} ./QProperties_DeviceInput.png
```
```` ````
````{tab} API - ComboBox ````{tab} API - ComboBox

View File

@ -0,0 +1,114 @@
(user.widgets.signal_input)=
# Signal Input Widgets
````{tab} Overview
The `Signal Input Widgets` consist of two primary widgets: `SignalLineEdit` and `SignalComboBox`. Both widgets are designed to facilitate the selection of the available signals for a selected device within the current BEC session. These widgets allow users to filter, search, and select signals dynamically. The widgets can either be integrated into a GUI through direct code instantiation or by using `QtDesigner`.
## SignalLineEdit
The `SignalLineEdit` widget provides a line edit interface with autocomplete functionality for the available of signals associated with the selected device. This widget is ideal for users who prefer to type in the signal name directly. If no device is selected, the autocomplete will be empty. In addition, the widget will display a red border around the line edit if the input signal is invalid.
## SignalComboBox
The `SignalComboBox` widget offers a dropdown interface for choosing a signal from the available signals of a device. It will further categorise the signals according to its `kind`: `hinted`, `normal` and `config`. For more information about `kind`, please check the [ophyd documentation](https://nsls-ii.github.io/ophyd/signals.html#kind). This widget is ideal for users who prefer to select signals from a list.
## Key Features:
- **Signal Filtering**: Both widgets allow users to filter devices by signal types(`kind`). No selected filter will show all signals.
- **Real-Time Autocomplete (LineEdit)**: The `SignalLineEdit` widget supports real-time autocomplete, helping users find devices faster.
- **Real-Time Input Validation (LineEdit)**: User input is validated in real-time with a red border around the `SignalLineEdit` indicating an invalid input.
- **Dropdown Selection (SignalComboBox)**: The `SignalComboBox` widget displays the sorted signals of the device
- **QtDesigner Integration**: Both widgets can be added as custom widgets in `QtDesigner` or instantiated directly in code.
````
````{tab} Examples
Both `SignalLineEdit` and `SignalComboBox` can be integrated within a GUI application through direct code instantiation or by using `QtDesigner`. Below are examples demonstrating how to create and use these widgets.
## Example 1 - Creating a SignalLineEdit in Code
In this example, we demonstrate how to create a `SignalLineEdit` widget in code and customize its behavior.
We will select `samx`, which is a motor in the BEC simulation device config, and filter the signals to `normal` and `hinted`.
Note, not specifying signal_filter will include all signals.
```python
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.widgets.signal_line_edit.signal_line_edit import SignalLineEdit
from bec_widgets.widgets.base_classes.device_signal_input_base import BECSignalFilter
class MyGui(QWidget):
def __init__(self):
super().__init__()
self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget
# Create and add the DeviceLineEdit to the layout
self.signal_line_edit = SignalLineEdit(device="samx", signal_filter=[BECSignalFilter.NORMAL, BECSignalFilter.HINTED])
self.layout().addWidget(self.signal_line_edit)
# Example of how this custom GUI might be used:
app = QApplication([])
my_gui = MyGui()
my_gui.show()
app.exec_()
```
## Example 2 - Creating a DeviceComboBox in Code
Similarly, here is an example of creating a `DeviceComboBox` widget in code and customizing its behavior.
```python
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.widgets.signal_combobox.signal_combobox import SignalComboBox
from bec_widgets.widgets.base_classes.device_signal_input_base import BECSignalFilter
class MyGui(QWidget):
def __init__(self):
super().__init__()
self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget
# Create and add the DeviceLineEdit to the layout
self.signal_combobox = SignalComboBox(device="samx", signal_filter=[BECSignalFilter.NORMAL, BECSignalFilter.HINTED])
self.layout().addWidget(self.signal_combobox)
# Example of how this custom GUI might be used:
app = QApplication([])
my_gui = MyGui()
my_gui.show()
app.exec_()
```
## Example 3 - Setting Default Device
Both `SignalLineEdit` and `SignalComboBox` allow you to set a default device that will be selected when the widget is initialized.
```python
# Set default device for DeviceLineEdit
self.signal_line_edit.set_device("motor1")
# Set default device for DeviceComboBox
self.signal_combobox.set_device("motor2")
```
````
````{tab} BEC Designer
Both widgets are also available as plugins for the BEC Designer. We have included Qt properties for both widgets, allowing customization of filtering and default device settings directly from the designer. In addition to the common signals and slots for `SignalLineEdit` and `SignalComboBox`, the following slots are available:
- `set_device(str)` to set the default device
- `set_signal(str)` to set the default signal
- `update_signals_from_filters()` to refresh the devices list based on the current filters
The following Qt properties are also included:
```{figure} ./signal_input_qproperties.png
```
````
````{tab} API - ComboBox
```{eval-rst}
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.SignalComboBox.rst
```
````
````{tab} API - LineEdit
```{eval-rst}
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.SignalLineEdit.rst
```
````

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -159,6 +159,14 @@ Various buttons which manage the control of the BEC Queue.
Choose individual device from current session. Choose individual device from current session.
``` ```
```{grid-item-card} Signal Input Widgets
:link: user.widgets.signal_input
:link-type: ref
:img-top: /assets/widget_screenshots/signal_inputs.png
Choose individual signals available for a selected device.
```
```{grid-item-card} Text Box Widget ```{grid-item-card} Text Box Widget
:link: user.widgets.text_box :link: user.widgets.text_box
:link-type: ref :link-type: ref

View File

@ -3,147 +3,9 @@ from unittest.mock import MagicMock, patch
import fakeredis import fakeredis
import pytest import pytest
from bec_lib.client import BECClient
from bec_lib.device import Positioner, ReadoutPriority
from bec_lib.devicemanager import DeviceContainer
from bec_lib.redis_connector import RedisConnector from bec_lib.redis_connector import RedisConnector
from bec_widgets.test_utils.client_mocks import DEVICES, DMMock, FakePositioner, Positioner
class FakeDevice:
"""Fake minimal positioner class for testing."""
def __init__(self, name, enabled=True, readout_priority=ReadoutPriority.MONITORED):
self.name = name
self.enabled = enabled
self.signals = {self.name: {"value": 1.0}}
self.description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
self.readout_priority = readout_priority
self._config = {
"readoutPriority": "baseline",
"deviceClass": "ophyd_devices.SimPositioner",
"deviceConfig": {
"delay": 1,
"limits": [-50, 50],
"tolerance": 0.01,
"update_frequency": 400,
},
"deviceTags": ["user motors"],
"enabled": enabled,
"readOnly": False,
"name": self.name,
}
def __contains__(self, item):
return item == self.name
@property
def _hints(self):
return [self.name]
def set_value(self, fake_value: float = 1.0) -> None:
"""
Setup fake value for device readout
Args:
fake_value(float): Desired fake value
"""
self.signals[self.name]["value"] = fake_value
def describe(self) -> dict:
"""
Get the description of the device
Returns:
dict: Description of the device
"""
return self.description
class FakePositioner(FakeDevice):
def __init__(
self,
name,
enabled=True,
limits=None,
read_value=1.0,
readout_priority=ReadoutPriority.MONITORED,
):
super().__init__(name, enabled, readout_priority)
self.limits = limits if limits is not None else [0, 0]
self.read_value = read_value
self.name = name
@property
def precision(self):
return 3
def set_read_value(self, value):
self.read_value = value
def read(self):
return {
self.name: {"value": self.read_value},
f"{self.name}_setpoint": {"value": self.read_value},
f"{self.name}_motor_is_moving": {"value": 0},
}
def set_limits(self, limits):
self.limits = limits
def move(self, value, relative=False):
"""Simulates moving the device to a new position."""
if relative:
self.read_value += value
else:
self.read_value = value
# Respect the limits
self.read_value = max(min(self.read_value, self.limits[1]), self.limits[0])
@property
def readback(self):
return MagicMock(get=MagicMock(return_value=self.read_value))
class Positioner(FakePositioner):
"""just placeholder for testing embedded isinstance check in DeviceCombobox"""
def __init__(self, name="test", limits=None, read_value=1.0):
super().__init__(name, limits, read_value)
class Device(FakeDevice):
"""just placeholder for testing embedded isinstance check in DeviceCombobox"""
def __init__(self, name, enabled=True):
super().__init__(name, enabled)
class DMMock:
def __init__(self):
self.devices = DeviceContainer()
def add_devives(self, devices: list):
for device in devices:
self.devices[device.name] = device
DEVICES = [
FakePositioner("samx", limits=[-10, 10], read_value=2.0),
FakePositioner("samy", limits=[-5, 5], read_value=3.0),
FakePositioner("samz", limits=[-8, 8], read_value=4.0),
FakePositioner("aptrx", limits=None, read_value=4.0),
FakePositioner("aptry", limits=None, read_value=5.0),
FakeDevice("gauss_bpm"),
FakeDevice("gauss_adc1"),
FakeDevice("gauss_adc2"),
FakeDevice("gauss_adc3"),
FakeDevice("bpm4i"),
FakeDevice("bpm3a"),
FakeDevice("bpm3i"),
FakeDevice("eiger"),
FakeDevice("waveform1d"),
FakeDevice("async_device", readout_priority=ReadoutPriority.ASYNC),
Positioner("test", limits=[-10, 10], read_value=2.0),
Device("test_device"),
]
def fake_redis_server(host, port): def fake_redis_server(host, port):

View File

@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch
import pyqtgraph as pg import pyqtgraph as pg
import pytest import pytest
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
from bec_widgets.widgets.image.image_widget import BECImageWidget from bec_widgets.widgets.image.image_widget import BECImageWidget
from .client_mocks import mocked_client from .client_mocks import mocked_client
@ -11,7 +12,6 @@ from .client_mocks import mocked_client
@pytest.fixture @pytest.fixture
def image_widget(qtbot, mocked_client): def image_widget(qtbot, mocked_client):
widget = BECImageWidget(client=mocked_client()) widget = BECImageWidget(client=mocked_client())
widget.toolbar.widgets["monitor"].device_combobox.set_device_filter("FakeDevice")
qtbot.addWidget(widget) qtbot.addWidget(widget)
qtbot.waitExposed(widget) qtbot.waitExposed(widget)
yield widget yield widget
@ -32,7 +32,8 @@ def test_image_widget_init(image_widget):
assert image_widget._image is not None assert image_widget._image is not None
assert ( assert (
image_widget.toolbar.widgets["monitor"].device_combobox.config.device_filter == "FakeDevice" BECDeviceFilter.DEVICE
in image_widget.toolbar.widgets["monitor"].device_combobox.config.device_filter
) )
assert image_widget.toolbar.widgets["drag_mode"].action.isChecked() == True assert image_widget.toolbar.widgets["drag_mode"].action.isChecked() == True
assert image_widget.toolbar.widgets["rectangle_mode"].action.isChecked() == False assert image_widget.toolbar.widgets["rectangle_mode"].action.isChecked() == False

View File

@ -4,8 +4,7 @@ import pytest
from bec_widgets.cli.client import BECFigure from bec_widgets.cli.client import BECFigure
from bec_widgets.cli.client_utils import BECGuiClientMixin, _start_plot_process from bec_widgets.cli.client_utils import BECGuiClientMixin, _start_plot_process
from bec_widgets.test_utils.client_mocks import FakeDevice
from .client_mocks import FakeDevice
@pytest.fixture @pytest.fixture

View File

@ -1,13 +1,21 @@
import pytest from unittest import mock
from qtpy.QtWidgets import QWidget
from bec_widgets.widgets.base_classes.device_input_base import DeviceInputBase import pytest
from bec_lib.device import ReadoutPriority
from qtpy.QtWidgets import QWidget
from typeguard import TypeCheckError
from bec_widgets.test_utils.client_mocks import FakePositioner
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter, DeviceInputBase
from .client_mocks import mocked_client from .client_mocks import mocked_client
from .conftest import create_widget
# DeviceInputBase is meant to be mixed in a QWidget # DeviceInputBase is meant to be mixed in a QWidget
class DeviceInputWidget(DeviceInputBase, QWidget): class DeviceInputWidget(DeviceInputBase, QWidget):
"""Thin wrapper around DeviceInputBase to make it a QWidget"""
def __init__(self, parent=None, client=None, config=None, gui_id=None): def __init__(self, parent=None, client=None, config=None, gui_id=None):
super().__init__(client=client, config=config, gui_id=gui_id) super().__init__(client=client, config=config, gui_id=gui_id)
QWidget.__init__(self, parent=parent) QWidget.__init__(self, parent=parent)
@ -15,13 +23,15 @@ class DeviceInputWidget(DeviceInputBase, QWidget):
@pytest.fixture @pytest.fixture
def device_input_base(qtbot, mocked_client): def device_input_base(qtbot, mocked_client):
widget = DeviceInputWidget(client=mocked_client) """Fixture with mocked FilterIO and WidgetIO"""
qtbot.addWidget(widget) with mock.patch("bec_widgets.utils.filter_io.FilterIO.set_selection"):
qtbot.waitExposed(widget) with mock.patch("bec_widgets.utils.widget_io.WidgetIO.set_value"):
yield widget widget = create_widget(qtbot=qtbot, widget=DeviceInputWidget, client=mocked_client)
yield widget
def test_device_input_base_init(device_input_base): def test_device_input_base_init(device_input_base):
"""Test init"""
assert device_input_base is not None assert device_input_base is not None
assert device_input_base.client is not None assert device_input_base.client is not None
assert isinstance(device_input_base, DeviceInputBase) assert isinstance(device_input_base, DeviceInputBase)
@ -32,45 +42,95 @@ def test_device_input_base_init(device_input_base):
def test_device_input_base_init_with_config(mocked_client): def test_device_input_base_init_with_config(mocked_client):
"""Test init with Config"""
config = { config = {
"widget_class": "DeviceInputWidget", "widget_class": "DeviceInputWidget",
"gui_id": "test_gui_id", "gui_id": "test_gui_id",
"device_filter": "FakePositioner", "device_filter": [BECDeviceFilter.POSITIONER],
"default": "samx", "default": "samx",
} }
widget = DeviceInputWidget(client=mocked_client, config=config) widget = DeviceInputWidget(client=mocked_client, config=config)
assert widget.config.gui_id == "test_gui_id" assert widget.config.gui_id == "test_gui_id"
assert widget.config.device_filter == "FakePositioner" assert widget.config.device_filter == [BECDeviceFilter.POSITIONER]
assert widget.config.default == "samx" assert widget.config.default == "samx"
def test_device_input_base_set_device_filter(device_input_base): def test_device_input_base_set_device_filter(device_input_base):
device_input_base.set_device_filter("FakePositioner") """Test device filter setter."""
assert device_input_base.config.device_filter == "FakePositioner" device_input_base.set_device_filter(BECDeviceFilter.POSITIONER)
assert device_input_base.config.device_filter == [BECDeviceFilter.POSITIONER]
def test_device_input_base_set_device_filter_error(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: with pytest.raises(ValueError) as excinfo:
device_input_base.set_device_filter("NonExistingClass") device_input_base.set_device_filter("NonExistingClass")
assert "Device filter NonExistingClass is not in the device list." in str(excinfo.value) assert "Device filter NonExistingClass is not in the device list." in str(excinfo.value)
def test_device_input_base_set_default_device(device_input_base): def test_device_input_base_set_default_device(device_input_base):
device_input_base.set_default_device("samx") """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_filter(BECDeviceFilter.POSITIONER)
device_input_base.set_readout_priority_filter(ReadoutPriority.MONITORED)
device_input_base.set_device("samx")
assert device_input_base.config.default == "samx" assert device_input_base.config.default == "samx"
def test_device_input_base_set_default_device_error(device_input_base):
with pytest.raises(ValueError) as excinfo:
device_input_base.set_default_device("NonExistingDevice")
assert "Default device NonExistingDevice is not in the device list." in str(excinfo.value)
def test_device_input_base_get_device_list(device_input_base):
devices = device_input_base.get_device_list("FakePositioner")
assert devices == ["samx", "samy", "samz", "aptrx", "aptry"]
def test_device_input_base_get_filters(device_input_base): def test_device_input_base_get_filters(device_input_base):
"""Test getting the available filters."""
filters = device_input_base.get_available_filters() filters = device_input_base.get_available_filters()
assert filters == {"FakePositioner", "FakeDevice", "Positioner", "Device"} selection = [
BECDeviceFilter.POSITIONER,
BECDeviceFilter.DEVICE,
BECDeviceFilter.COMPUTED_SIGNAL,
BECDeviceFilter.SIGNAL,
] + [
ReadoutPriority.MONITORED,
ReadoutPriority.BASELINE,
ReadoutPriority.ASYNC,
ReadoutPriority.ON_REQUEST,
]
assert [entry for entry in filters if entry in selection]
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
assert device_input_base.device_filter == [BECDeviceFilter.DEVICE]
device_input_base.include_positioner = True
assert device_input_base.device_filter == [BECDeviceFilter.DEVICE, BECDeviceFilter.POSITIONER]
device_input_base.include_computed_signal = True
assert device_input_base.device_filter == [
BECDeviceFilter.DEVICE,
BECDeviceFilter.POSITIONER,
BECDeviceFilter.COMPUTED_SIGNAL,
]
device_input_base.include_signal = True
assert device_input_base.device_filter == [
BECDeviceFilter.DEVICE,
BECDeviceFilter.POSITIONER,
BECDeviceFilter.COMPUTED_SIGNAL,
BECDeviceFilter.SIGNAL,
]
assert device_input_base.readout_filter == []
device_input_base.readout_async = True
assert device_input_base.readout_filter == [ReadoutPriority.ASYNC]
device_input_base.readout_baseline = True
assert device_input_base.readout_filter == [ReadoutPriority.ASYNC, ReadoutPriority.BASELINE]
device_input_base.readout_monitored = True
assert device_input_base.readout_filter == [
ReadoutPriority.ASYNC,
ReadoutPriority.BASELINE,
ReadoutPriority.MONITORED,
]
device_input_base.readout_on_request = True
assert device_input_base.readout_filter == [
ReadoutPriority.ASYNC,
ReadoutPriority.BASELINE,
ReadoutPriority.MONITORED,
ReadoutPriority.ON_REQUEST,
]

View File

@ -1,5 +1,7 @@
import pytest import pytest
from bec_lib.device import ReadoutPriority
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox from bec_widgets.widgets.device_combobox.device_combobox import DeviceComboBox
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
@ -19,7 +21,7 @@ def device_input_combobox_with_config(qtbot, mocked_client):
config = { config = {
"widget_class": "DeviceComboBox", "widget_class": "DeviceComboBox",
"gui_id": "test_gui_id", "gui_id": "test_gui_id",
"device_filter": "FakePositioner", "device_filter": [BECDeviceFilter.POSITIONER],
"default": "samx", "default": "samx",
"arg_name": "test_arg_name", "arg_name": "test_arg_name",
} }
@ -34,7 +36,7 @@ def device_input_combobox_with_kwargs(qtbot, mocked_client):
widget = DeviceComboBox( widget = DeviceComboBox(
client=mocked_client, client=mocked_client,
gui_id="test_gui_id", gui_id="test_gui_id",
device_filter="FakePositioner", device_filter=[BECDeviceFilter.POSITIONER],
default="samx", default="samx",
arg_name="test_arg_name", arg_name="test_arg_name",
) )
@ -48,7 +50,6 @@ def test_device_input_combobox_init(device_input_combobox):
assert device_input_combobox.client is not None assert device_input_combobox.client is not None
assert isinstance(device_input_combobox, DeviceComboBox) assert isinstance(device_input_combobox, DeviceComboBox)
assert device_input_combobox.config.widget_class == "DeviceComboBox" assert device_input_combobox.config.widget_class == "DeviceComboBox"
assert device_input_combobox.config.device_filter is None
assert device_input_combobox.config.default is None assert device_input_combobox.config.default is None
assert device_input_combobox.devices == [ assert device_input_combobox.devices == [
"samx", "samx",
@ -73,14 +74,14 @@ def test_device_input_combobox_init(device_input_combobox):
def test_device_input_combobox_init_with_config(device_input_combobox_with_config): 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.gui_id == "test_gui_id"
assert device_input_combobox_with_config.config.device_filter == "FakePositioner" 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.default == "samx"
assert device_input_combobox_with_config.config.arg_name == "test_arg_name" 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): 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.gui_id == "test_gui_id"
assert device_input_combobox_with_kwargs.config.device_filter == "FakePositioner" assert device_input_combobox_with_kwargs.config.device_filter == [BECDeviceFilter.POSITIONER]
assert device_input_combobox_with_kwargs.config.default == "samx" assert device_input_combobox_with_kwargs.config.default == "samx"
assert device_input_combobox_with_kwargs.config.arg_name == "test_arg_name" assert device_input_combobox_with_kwargs.config.arg_name == "test_arg_name"
@ -88,7 +89,7 @@ def test_device_input_combobox_init_with_kwargs(device_input_combobox_with_kwarg
def test_get_device_from_input_combobox_init(device_input_combobox): def test_get_device_from_input_combobox_init(device_input_combobox):
device_input_combobox.setCurrentIndex(0) device_input_combobox.setCurrentIndex(0)
device_text = device_input_combobox.currentText() device_text = device_input_combobox.currentText()
current_device = device_input_combobox.get_device() current_device = device_input_combobox.get_current_device()
assert current_device.name == device_text assert current_device.name == device_text
@ -106,7 +107,7 @@ def device_input_line_edit_with_config(qtbot, mocked_client):
config = { config = {
"widget_class": "DeviceLineEdit", "widget_class": "DeviceLineEdit",
"gui_id": "test_gui_id", "gui_id": "test_gui_id",
"device_filter": "FakePositioner", "device_filter": [BECDeviceFilter.POSITIONER],
"default": "samx", "default": "samx",
"arg_name": "test_arg_name", "arg_name": "test_arg_name",
} }
@ -121,7 +122,7 @@ def device_input_line_edit_with_kwargs(qtbot, mocked_client):
widget = DeviceLineEdit( widget = DeviceLineEdit(
client=mocked_client, client=mocked_client,
gui_id="test_gui_id", gui_id="test_gui_id",
device_filter="FakePositioner", device_filter=[BECDeviceFilter.POSITIONER],
default="samx", default="samx",
arg_name="test_arg_name", arg_name="test_arg_name",
) )
@ -135,7 +136,8 @@ def test_device_input_line_edit_init(device_input_line_edit):
assert device_input_line_edit.client is not None assert device_input_line_edit.client is not None
assert isinstance(device_input_line_edit, DeviceLineEdit) assert isinstance(device_input_line_edit, DeviceLineEdit)
assert device_input_line_edit.config.widget_class == "DeviceLineEdit" assert device_input_line_edit.config.widget_class == "DeviceLineEdit"
assert device_input_line_edit.config.device_filter is None assert device_input_line_edit.config.device_filter == []
assert device_input_line_edit.config.readout_filter == []
assert device_input_line_edit.config.default is None assert device_input_line_edit.config.default is None
assert device_input_line_edit.devices == [ assert device_input_line_edit.devices == [
"samx", "samx",
@ -160,14 +162,14 @@ 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): 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.gui_id == "test_gui_id"
assert device_input_line_edit_with_config.config.device_filter == "FakePositioner" 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.default == "samx"
assert device_input_line_edit_with_config.config.arg_name == "test_arg_name" 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): 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.gui_id == "test_gui_id"
assert device_input_line_edit_with_kwargs.config.device_filter == "FakePositioner" assert device_input_line_edit_with_kwargs.config.device_filter == [BECDeviceFilter.POSITIONER]
assert device_input_line_edit_with_kwargs.config.default == "samx" assert device_input_line_edit_with_kwargs.config.default == "samx"
assert device_input_line_edit_with_kwargs.config.arg_name == "test_arg_name" assert device_input_line_edit_with_kwargs.config.arg_name == "test_arg_name"
@ -175,6 +177,6 @@ def test_device_input_line_edit_init_with_kwargs(device_input_line_edit_with_kwa
def test_get_device_from_input_line_edit_init(device_input_line_edit): def test_get_device_from_input_line_edit_init(device_input_line_edit):
device_input_line_edit.setText("samx") device_input_line_edit.setText("samx")
device_text = device_input_line_edit.text() device_text = device_input_line_edit.text()
current_device = device_input_line_edit.get_device() current_device = device_input_line_edit.get_current_device()
assert current_device.name == device_text assert current_device.name == device_text

View File

@ -0,0 +1,103 @@
from unittest import mock
import pytest
from qtpy.QtWidgets import QWidget
from bec_widgets.widgets.base_classes.device_signal_input_base import (
BECSignalFilter,
DeviceSignalInputBase,
)
from bec_widgets.widgets.signal_combobox.signal_combobox import SignalComboBox
from bec_widgets.widgets.signal_line_edit.signal_line_edit import SignalLineEdit
from .client_mocks import mocked_client
from .conftest import create_widget
class DeviceInputWidget(DeviceSignalInputBase, QWidget):
"""Thin wrapper around DeviceInputBase to make it a QWidget"""
def __init__(self, parent=None, client=None, config=None, gui_id=None):
super().__init__(client=client, config=config, gui_id=gui_id)
QWidget.__init__(self, parent=parent)
@pytest.fixture
def device_signal_base(qtbot, mocked_client):
"""Fixture with mocked FilterIO and WidgetIO"""
with mock.patch("bec_widgets.utils.filter_io.FilterIO.set_selection"):
with mock.patch("bec_widgets.utils.widget_io.WidgetIO.set_value"):
widget = create_widget(qtbot=qtbot, widget=DeviceInputWidget, client=mocked_client)
yield widget
@pytest.fixture
def device_signal_combobox(qtbot, mocked_client):
"""Fixture with mocked FilterIO and WidgetIO"""
widget = create_widget(qtbot=qtbot, widget=SignalComboBox, client=mocked_client)
yield widget
@pytest.fixture
def device_signal_line_edit(qtbot, mocked_client):
"""Fixture with mocked FilterIO and WidgetIO"""
widget = create_widget(qtbot=qtbot, widget=SignalLineEdit, client=mocked_client)
yield widget
def test_device_signal_base_init(device_signal_base):
"""Test if the DeviceSignalInputBase is initialized correctly"""
assert device_signal_base._device is None
assert device_signal_base._signal_filter == []
assert device_signal_base._signals == []
assert device_signal_base._hinted_signals == []
assert device_signal_base._normal_signals == []
assert device_signal_base._config_signals == []
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]
device_signal_base.include_normal_signals = True
assert device_signal_base._signal_filter == [BECSignalFilter.CONFIG, BECSignalFilter.NORMAL]
device_signal_base.include_hinted_signals = True
assert device_signal_base._signal_filter == [
BECSignalFilter.CONFIG,
BECSignalFilter.NORMAL,
BECSignalFilter.HINTED,
]
def test_device_signal_set_device(device_signal_base):
"""Test if the set_device method works correctly"""
device_signal_base.include_hinted_signals = True
device_signal_base.set_device("samx")
assert device_signal_base.device == "samx"
assert device_signal_base.signals == ["readback"]
device_signal_base.include_normal_signals = True
assert device_signal_base.signals == ["readback", "setpoint"]
device_signal_base.include_config_signals = True
assert device_signal_base.signals == ["readback", "setpoint", "velocity"]
def test_signal_combobox(device_signal_combobox):
"""Test the signal_combobox"""
device_signal_combobox._signals == []
device_signal_combobox.include_normal_signals = True
device_signal_combobox.include_hinted_signals = True
device_signal_combobox.include_config_signals = True
device_signal_combobox.signals == []
device_signal_combobox.set_device("samx")
device_signal_combobox.signals == ["readback", "setpoint", "velocity"]
def test_signal_lineeidt(device_signal_line_edit):
"""Test the signal_combobox"""
device_signal_line_edit._signals == []
device_signal_line_edit.include_normal_signals = True
device_signal_line_edit.include_hinted_signals = True
device_signal_line_edit.include_config_signals = True
device_signal_line_edit.signals == []
device_signal_line_edit.set_device("samx")
device_signal_line_edit.signals == ["readback", "setpoint", "velocity"]

View File

@ -0,0 +1,45 @@
import pytest
from bec_widgets.utils.filter_io import FilterIO
from bec_widgets.widgets.dap_combo_box.dap_combo_box import DapComboBox
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
from .client_mocks import mocked_client
from .conftest import create_widget
@pytest.fixture(scope="function")
def dap_mock(qtbot, mocked_client):
"""Fixture for QLineEdit widget"""
models = ["GaussianModel", "LorentzModel", "SineModel"]
mocked_client.dap._available_dap_plugins.keys.return_value = models
widget = create_widget(qtbot, DapComboBox, client=mocked_client)
return widget
@pytest.fixture(scope="function")
def line_edit_mock(qtbot, mocked_client):
"""Fixture for QLineEdit widget"""
widget = create_widget(qtbot, DeviceLineEdit, client=mocked_client)
return widget
def test_set_selection_combo_box(dap_mock):
"""Test set selection for QComboBox using DapComboBox"""
assert dap_mock.fit_model_combobox.count() == 3
FilterIO.set_selection(dap_mock.fit_model_combobox, selection=["testA", "testB"])
assert dap_mock.fit_model_combobox.count() == 2
assert FilterIO.check_input(widget=dap_mock.fit_model_combobox, text="testA") is True
def test_set_selection_line_edit(line_edit_mock):
"""Test set selection for QComboBox using DapComboBox"""
FilterIO.set_selection(line_edit_mock, selection=["testA", "testB"])
assert line_edit_mock.completer.model().rowCount() == 2
model = line_edit_mock.completer.model()
model_data = [model.data(model.index(i)) for i in range(model.rowCount())]
assert model_data == ["testA", "testB"]
assert FilterIO.check_input(widget=line_edit_mock, text="testA") is True
FilterIO.set_selection(line_edit_mock, selection=["testC"])
assert FilterIO.check_input(widget=line_edit_mock, text="testA") is False
assert FilterIO.check_input(widget=line_edit_mock, text="testC") is True

View File

@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from bec_widgets.widgets.base_classes.device_input_base import BECDeviceFilter
from bec_widgets.widgets.motor_map.motor_map_dialog.motor_map_settings import MotorMapSettings from bec_widgets.widgets.motor_map.motor_map_dialog.motor_map_settings import MotorMapSettings
from bec_widgets.widgets.motor_map.motor_map_widget import BECMotorMapWidget from bec_widgets.widgets.motor_map.motor_map_widget import BECMotorMapWidget
@ -11,8 +12,8 @@ from .client_mocks import mocked_client
@pytest.fixture @pytest.fixture
def motor_map_widget(qtbot, mocked_client): def motor_map_widget(qtbot, mocked_client):
widget = BECMotorMapWidget(client=mocked_client()) widget = BECMotorMapWidget(client=mocked_client())
widget.toolbar.widgets["motor_x"].device_combobox.set_device_filter("FakePositioner") widget.toolbar.widgets["motor_x"].device_combobox.set_device_filter(BECDeviceFilter.POSITIONER)
widget.toolbar.widgets["motor_y"].device_combobox.set_device_filter("FakePositioner") widget.toolbar.widgets["motor_y"].device_combobox.set_device_filter(BECDeviceFilter.POSITIONER)
qtbot.addWidget(widget) qtbot.addWidget(widget)
qtbot.waitExposed(widget) qtbot.waitExposed(widget)
yield widget yield widget
@ -35,14 +36,12 @@ def test_motor_map_widget_init(motor_map_widget):
assert motor_map_widget.toolbar.widgets["connect"].action.isEnabled() == True assert motor_map_widget.toolbar.widgets["connect"].action.isEnabled() == True
assert motor_map_widget.toolbar.widgets["config"].action.isEnabled() == False assert motor_map_widget.toolbar.widgets["config"].action.isEnabled() == False
assert motor_map_widget.toolbar.widgets["history"].action.isEnabled() == False assert motor_map_widget.toolbar.widgets["history"].action.isEnabled() == False
assert ( assert motor_map_widget.toolbar.widgets["motor_x"].device_combobox.config.device_filter == [
motor_map_widget.toolbar.widgets["motor_x"].device_combobox.config.device_filter BECDeviceFilter.POSITIONER
== "FakePositioner" ]
) assert motor_map_widget.toolbar.widgets["motor_y"].device_combobox.config.device_filter == [
assert ( BECDeviceFilter.POSITIONER
motor_map_widget.toolbar.widgets["motor_y"].device_combobox.config.device_filter ]
== "FakePositioner"
)
assert motor_map_widget.map.motor_x is None assert motor_map_widget.map.motor_x is None
assert motor_map_widget.map.motor_y is None assert motor_map_widget.map.motor_y is None