mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 19:21:50 +02:00
feat(curve settings): add combobox selection for device and signal
This commit is contained in:
@ -96,9 +96,21 @@ class FakePositioner(BECPositioner):
|
||||
}
|
||||
self._info = {
|
||||
"signals": {
|
||||
"readback": {"kind_str": "hinted"}, # hinted
|
||||
"setpoint": {"kind_str": "normal"}, # normal
|
||||
"velocity": {"kind_str": "config"}, # config
|
||||
"readback": {
|
||||
"kind_str": "hinted",
|
||||
"component_name": "readback",
|
||||
"obj_name": self.name,
|
||||
}, # hinted
|
||||
"setpoint": {
|
||||
"kind_str": "normal",
|
||||
"component_name": "setpoint",
|
||||
"obj_name": f"{self.name}_setpoint",
|
||||
}, # normal
|
||||
"velocity": {
|
||||
"kind_str": "config",
|
||||
"component_name": "velocity",
|
||||
"obj_name": f"{self.name}_velocity",
|
||||
}, # config
|
||||
}
|
||||
}
|
||||
self.signals = {
|
||||
|
@ -8,6 +8,8 @@ from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QStringListModel
|
||||
from qtpy.QtWidgets import QComboBox, QCompleter, QLineEdit
|
||||
|
||||
from bec_widgets.utils.ophyd_kind_util import Kind
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
@ -36,6 +38,23 @@ class WidgetFilterHandler(ABC):
|
||||
bool: True if the input text is in the filtered selection
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def update_with_kind(
|
||||
self, kind: Kind, signal_filter: set, device_info: dict, device_name: str
|
||||
) -> list[str | tuple]:
|
||||
"""Update the selection based on the kind of signal.
|
||||
|
||||
Args:
|
||||
kind (Kind): The kind of signal to filter.
|
||||
signal_filter (set): Set of signal kinds to filter.
|
||||
device_info (dict): Dictionary containing device information.
|
||||
device_name (str): Name of the device.
|
||||
|
||||
Returns:
|
||||
list[str | tuple]: A list of filtered signals based on the kind.
|
||||
"""
|
||||
# This method should be implemented in subclasses or extended as needed
|
||||
|
||||
|
||||
class LineEditFilterHandler(WidgetFilterHandler):
|
||||
"""Handler for QLineEdit widget"""
|
||||
@ -69,6 +88,27 @@ class LineEditFilterHandler(WidgetFilterHandler):
|
||||
model_data = [model.data(model.index(i)) for i in range(model.rowCount())]
|
||||
return text in model_data
|
||||
|
||||
def update_with_kind(
|
||||
self, kind: Kind, signal_filter: set, device_info: dict, device_name: str
|
||||
) -> list[str | tuple]:
|
||||
"""Update the selection based on the kind of signal.
|
||||
|
||||
Args:
|
||||
kind (Kind): The kind of signal to filter.
|
||||
signal_filter (set): Set of signal kinds to filter.
|
||||
device_info (dict): Dictionary containing device information.
|
||||
device_name (str): Name of the device.
|
||||
|
||||
Returns:
|
||||
list[str | tuple]: A list of filtered signals based on the kind.
|
||||
"""
|
||||
|
||||
return [
|
||||
signal
|
||||
for signal, signal_info in device_info.items()
|
||||
if kind in signal_filter and (signal_info.get("kind_str", None) == str(kind.name))
|
||||
]
|
||||
|
||||
|
||||
class ComboBoxFilterHandler(WidgetFilterHandler):
|
||||
"""Handler for QComboBox widget"""
|
||||
@ -102,6 +142,40 @@ class ComboBoxFilterHandler(WidgetFilterHandler):
|
||||
"""
|
||||
return text in [widget.itemText(i) for i in range(widget.count())]
|
||||
|
||||
def update_with_kind(
|
||||
self, kind: Kind, signal_filter: set, device_info: dict, device_name: str
|
||||
) -> list[str | tuple]:
|
||||
"""Update the selection based on the kind of signal.
|
||||
|
||||
Args:
|
||||
kind (Kind): The kind of signal to filter.
|
||||
signal_filter (set): Set of signal kinds to filter.
|
||||
device_info (dict): Dictionary containing device information.
|
||||
device_name (str): Name of the device.
|
||||
|
||||
Returns:
|
||||
list[str | tuple]: A list of filtered signals based on the kind.
|
||||
"""
|
||||
out = []
|
||||
for signal, signal_info in device_info.items():
|
||||
if kind not in signal_filter or (signal_info.get("kind_str", None) != str(kind.name)):
|
||||
continue
|
||||
obj_name = signal_info.get("obj_name", "")
|
||||
component_name = signal_info.get("component_name", "")
|
||||
signal_wo_device = obj_name.removeprefix(f"{device_name}_")
|
||||
if not signal_wo_device:
|
||||
signal_wo_device = obj_name
|
||||
|
||||
if signal_wo_device != signal and component_name.replace(".", "_") != signal_wo_device:
|
||||
# If the object name is not the same as the signal name, we use the object name
|
||||
# to display in the combobox.
|
||||
out.append((f"{signal_wo_device} ({signal})", signal_info))
|
||||
else:
|
||||
# If the object name is the same as the signal name, we do not change it.
|
||||
out.append((signal, signal_info))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class FilterIO:
|
||||
"""Public interface to set filters for input widgets.
|
||||
@ -152,6 +226,35 @@ class FilterIO:
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def update_with_kind(
|
||||
widget, kind: Kind, signal_filter: set, device_info: dict, device_name: str
|
||||
) -> list[str | tuple]:
|
||||
"""
|
||||
Update the selection based on the kind of signal.
|
||||
|
||||
Args:
|
||||
widget: Widget instance.
|
||||
kind (Kind): The kind of signal to filter.
|
||||
signal_filter (set): Set of signal kinds to filter.
|
||||
device_info (dict): Dictionary containing device information.
|
||||
device_name (str): Name of the device.
|
||||
|
||||
Returns:
|
||||
list[str | tuple]: A list of filtered signals based on the kind.
|
||||
"""
|
||||
handler_class = FilterIO._find_handler(widget)
|
||||
if handler_class:
|
||||
return handler_class().update_with_kind(
|
||||
kind=kind,
|
||||
signal_filter=signal_filter,
|
||||
device_info=device_info,
|
||||
device_name=device_name,
|
||||
)
|
||||
raise ValueError(
|
||||
f"No matching handler for widget type: {type(widget)} in handler list {FilterIO._handlers}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _find_handler(widget):
|
||||
"""
|
||||
|
@ -6,7 +6,7 @@ from qtpy.QtCore import Property
|
||||
from bec_widgets.utils import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.filter_io import FilterIO
|
||||
from bec_widgets.utils.filter_io import FilterIO, LineEditFilterHandler
|
||||
from bec_widgets.utils.ophyd_kind_util import Kind
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
@ -108,25 +108,32 @@ class DeviceSignalInputBase(BECWidget):
|
||||
if not self.validate_device(self._device):
|
||||
self._device = None
|
||||
self.config.device = self._device
|
||||
return
|
||||
device = self.get_device_object(self._device)
|
||||
# See above convention for Signals and ComputedSignals
|
||||
if isinstance(device, Signal):
|
||||
self._signals = [self._device]
|
||||
self._hinted_signals = [self._device]
|
||||
self._signals = []
|
||||
self._hinted_signals = []
|
||||
self._normal_signals = []
|
||||
self._config_signals = []
|
||||
FilterIO.set_selection(widget=self, selection=self._signals)
|
||||
return
|
||||
device = self.get_device_object(self._device)
|
||||
device_info = device._info.get("signals", {})
|
||||
|
||||
# See above convention for Signals and ComputedSignals
|
||||
if isinstance(device, Signal):
|
||||
self._signals = [(self._device, {})]
|
||||
self._hinted_signals = [(self._device, {})]
|
||||
self._normal_signals = []
|
||||
self._config_signals = []
|
||||
FilterIO.set_selection(widget=self, selection=self._signals)
|
||||
return
|
||||
|
||||
def _update(kind: Kind):
|
||||
return [
|
||||
signal
|
||||
for signal, signal_info in device_info.items()
|
||||
if kind in self.signal_filter
|
||||
and (signal_info.get("kind_str", None) == str(kind.name))
|
||||
]
|
||||
return FilterIO.update_with_kind(
|
||||
widget=self,
|
||||
kind=kind,
|
||||
signal_filter=self.signal_filter,
|
||||
device_info=device_info,
|
||||
device_name=self._device,
|
||||
)
|
||||
|
||||
self._hinted_signals = _update(Kind.hinted)
|
||||
self._normal_signals = _update(Kind.normal)
|
||||
@ -271,8 +278,11 @@ class DeviceSignalInputBase(BECWidget):
|
||||
Args:
|
||||
signal(str): Signal to validate.
|
||||
"""
|
||||
if signal in self.signals:
|
||||
return True
|
||||
for entry in self.signals:
|
||||
if isinstance(entry, tuple):
|
||||
entry = entry[0]
|
||||
if entry == signal:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _process_config_input(self, config: DeviceSignalInputBaseConfig | dict | None):
|
||||
|
@ -110,11 +110,7 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
|
||||
return
|
||||
if self.validate_signal(text) is False:
|
||||
return
|
||||
if text == "readback" and isinstance(self.get_device_object(self.device), Positioner):
|
||||
device_signal = self.device
|
||||
else:
|
||||
device_signal = f"{self.device}_{text}"
|
||||
self.device_signal_changed.emit(device_signal)
|
||||
self.device_signal_changed.emit(text)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
|
@ -102,7 +102,7 @@ class CurveSetting(SettingWidget):
|
||||
|
||||
self.layout.addWidget(self.y_axis_box)
|
||||
|
||||
@SafeSlot()
|
||||
@SafeSlot(popup_error=True)
|
||||
def accept_changes(self):
|
||||
"""
|
||||
Accepts the changes made in the settings widget and applies them to the target widget.
|
||||
|
@ -5,13 +5,12 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes._icon.material_icons import material_icon
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import (
|
||||
QColorDialog,
|
||||
QComboBox,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
@ -27,9 +26,8 @@ from bec_widgets.utils import ConnectionConfig, EntryValidator
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import Colors
|
||||
from bec_widgets.utils.toolbar import MaterialIconAction, ModularToolBar
|
||||
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
|
||||
DeviceLineEdit,
|
||||
)
|
||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
|
||||
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
|
||||
from bec_widgets.widgets.plots.waveform.curve import CurveConfig, DeviceSignal
|
||||
from bec_widgets.widgets.utility.visual.color_button_native.color_button_native import (
|
||||
@ -125,11 +123,40 @@ class CurveRow(QTreeWidgetItem):
|
||||
"""Create columns 1 and 2. For device rows, we have device/entry edits; for dap rows, label/model combo."""
|
||||
if self.source == "device":
|
||||
# Device row: columns 1..2 are device line edits
|
||||
self.device_edit = DeviceLineEdit(parent=self.tree)
|
||||
self.entry_edit = QLineEdit(parent=self.tree) # TODO in future will be signal line edit
|
||||
self.device_edit = DeviceComboBox(parent=self.tree)
|
||||
self.device_edit.insertItem(0, "")
|
||||
self.device_edit.setEditable(True)
|
||||
self.entry_edit = SignalComboBox(parent=self.tree)
|
||||
self.entry_edit.include_config_signals = False
|
||||
self.entry_edit.insertItem(0, "")
|
||||
self.entry_edit.setEditable(True)
|
||||
self.device_edit.currentTextChanged.connect(self.entry_edit.set_device)
|
||||
self.device_edit.device_reset.connect(self.entry_edit.reset_selection)
|
||||
if self.config.signal:
|
||||
self.device_edit.setText(self.config.signal.name or "")
|
||||
self.entry_edit.setText(self.config.signal.entry or "")
|
||||
device_index = self.device_edit.findText(self.config.signal.name or "")
|
||||
if device_index >= 0:
|
||||
self.device_edit.setCurrentIndex(device_index)
|
||||
# Force the entry_edit to update based on the device name
|
||||
self.device_edit.currentTextChanged.emit(self.device_edit.currentText())
|
||||
else:
|
||||
# If the device name is not found, set the first enabled item
|
||||
self.device_edit.setCurrentIndex(0)
|
||||
|
||||
for i in range(self.entry_edit.count()):
|
||||
entry_data = self.entry_edit.itemData(i)
|
||||
if entry_data and entry_data.get("obj_name") == self.config.signal.entry:
|
||||
# If the device name matches an object name, set it
|
||||
self.entry_edit.setCurrentIndex(i)
|
||||
break
|
||||
else:
|
||||
# If no match found, set the first enabled item
|
||||
for i in range(self.entry_edit.count()):
|
||||
model = self.entry_edit.model()
|
||||
if model.flags(model.index(i, 0)) & Qt.ItemIsEnabled:
|
||||
self.entry_edit.setCurrentIndex(i)
|
||||
break
|
||||
else:
|
||||
self.entry_edit.setCurrentIndex(0)
|
||||
|
||||
self.tree.setItemWidget(self, 1, self.device_edit)
|
||||
self.tree.setItemWidget(self, 2, self.entry_edit)
|
||||
@ -268,13 +295,22 @@ class CurveRow(QTreeWidgetItem):
|
||||
# Gather device name/entry
|
||||
device_name = ""
|
||||
device_entry = ""
|
||||
|
||||
## TODO: Move this to itemData
|
||||
if hasattr(self, "device_edit"):
|
||||
device_name = self.device_edit.text()
|
||||
device_name = self.device_edit.currentText()
|
||||
if hasattr(self, "entry_edit"):
|
||||
device_entry = self.entry_validator.validate_signal(
|
||||
name=device_name, entry=self.entry_edit.text()
|
||||
)
|
||||
self.entry_edit.setText(device_entry)
|
||||
device_entry = self.entry_edit.currentText()
|
||||
index = self.entry_edit.findText(device_entry)
|
||||
if index > -1:
|
||||
device_entry_info = self.entry_edit.itemData(index)
|
||||
if device_entry_info:
|
||||
device_entry = device_entry_info.get("obj_name", device_entry)
|
||||
else:
|
||||
device_entry = self.entry_validator.validate_signal(
|
||||
name=device_name, entry=device_entry
|
||||
)
|
||||
|
||||
self.config.signal = DeviceSignal(name=device_name, entry=device_entry)
|
||||
self.config.source = "device"
|
||||
self.config.label = f"{device_name}-{device_entry}"
|
||||
@ -390,13 +426,20 @@ class CurveTree(BECWidget, QWidget):
|
||||
self.tree = QTreeWidget()
|
||||
self.tree.setColumnCount(7)
|
||||
self.tree.setHeaderLabels(["Actions", "Name", "Entry", "Color", "Style", "Width", "Symbol"])
|
||||
|
||||
header = self.tree.header()
|
||||
for idx in range(self.tree.columnCount()):
|
||||
if idx in (1, 2): # Device name and entry should stretch
|
||||
header.setSectionResizeMode(idx, QHeaderView.Stretch)
|
||||
else:
|
||||
header.setSectionResizeMode(idx, QHeaderView.Fixed)
|
||||
header.setStretchLastSection(False)
|
||||
self.tree.setColumnWidth(0, 90)
|
||||
self.tree.setColumnWidth(1, 100)
|
||||
self.tree.setColumnWidth(2, 100)
|
||||
self.tree.setColumnWidth(3, 70)
|
||||
self.tree.setColumnWidth(4, 80)
|
||||
self.tree.setColumnWidth(5, 40)
|
||||
self.tree.setColumnWidth(6, 40)
|
||||
self.tree.setColumnWidth(5, 50)
|
||||
self.tree.setColumnWidth(6, 50)
|
||||
|
||||
self.layout.addWidget(self.tree)
|
||||
|
||||
def _init_color_buffer(self, size: int):
|
||||
|
@ -12,12 +12,11 @@ may not be created immediately after the rpc call is made.
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from bec_widgets.cli.client import BECDockArea
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
|
||||
|
||||
PYTEST_TIMEOUT = 50
|
||||
@ -321,20 +320,20 @@ def test_widgets_e2e_signal_combobox(qtbot, connected_client_gui_obj, random_gen
|
||||
gui = connected_client_gui_obj
|
||||
bec = gui._client
|
||||
# Create dock_area, dock, widget
|
||||
dock, widget = create_widget(qtbot, gui, gui.available_widgets.SignalComboBox)
|
||||
dock: client.BECDock
|
||||
_, widget = create_widget(qtbot, gui, gui.available_widgets.SignalComboBox)
|
||||
widget: client.SignalComboBox
|
||||
|
||||
widget.set_device("samx")
|
||||
info = bec.device_manager.devices.samx._info["signals"]
|
||||
assert widget.signals == [
|
||||
"readback",
|
||||
"setpoint",
|
||||
"motor_is_moving",
|
||||
"velocity",
|
||||
"acceleration",
|
||||
"tolerance",
|
||||
["samx (readback)", info.get("readback")],
|
||||
["setpoint", info.get("setpoint")],
|
||||
["motor_is_moving", info.get("motor_is_moving")],
|
||||
["velocity", info.get("velocity")],
|
||||
["acceleration", info.get("acceleration")],
|
||||
["tolerance", info.get("tolerance")],
|
||||
]
|
||||
widget.set_signal("readback")
|
||||
widget.set_signal("samx (readback)")
|
||||
|
||||
# Test removing the widget, or leaving it open for the next test
|
||||
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||
|
@ -94,18 +94,6 @@ def test_device_signal_qproperties(device_signal_base):
|
||||
assert device_signal_base._signal_filter == {Kind.config, Kind.normal}
|
||||
|
||||
|
||||
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(qtbot, device_signal_combobox):
|
||||
"""Test the signal_combobox"""
|
||||
container = []
|
||||
@ -120,17 +108,25 @@ def test_signal_combobox(qtbot, device_signal_combobox):
|
||||
device_signal_combobox.include_config_signals = True
|
||||
assert device_signal_combobox.signals == []
|
||||
device_signal_combobox.set_device("samx")
|
||||
assert device_signal_combobox.signals == ["readback", "setpoint", "velocity"]
|
||||
samx = device_signal_combobox.dev.samx
|
||||
assert device_signal_combobox.signals == [
|
||||
("samx (readback)", samx._info["signals"].get("readback")),
|
||||
("setpoint", samx._info["signals"].get("setpoint")),
|
||||
("velocity", samx._info["signals"].get("velocity")),
|
||||
]
|
||||
qtbot.wait(100)
|
||||
assert container == ["samx"]
|
||||
assert container == ["samx (readback)"]
|
||||
# Set the type of class from the FakeDevice to Signal
|
||||
fake_signal = FakeSignal(name="fake_signal")
|
||||
fake_signal = FakeSignal(name="fake_signal", info={"device_info": {"signals": {}}})
|
||||
device_signal_combobox.client.device_manager.add_devices([fake_signal])
|
||||
device_signal_combobox.set_device("fake_signal")
|
||||
assert device_signal_combobox.signals == ["fake_signal"]
|
||||
fake_signal = device_signal_combobox.dev.fake_signal
|
||||
assert device_signal_combobox.signals == [
|
||||
("fake_signal", fake_signal._info["signals"].get("fake_signal", {}))
|
||||
]
|
||||
assert device_signal_combobox._config_signals == []
|
||||
assert device_signal_combobox._normal_signals == []
|
||||
assert device_signal_combobox._hinted_signals == ["fake_signal"]
|
||||
assert device_signal_combobox._hinted_signals == [("fake_signal", {})]
|
||||
|
||||
|
||||
def test_signal_lineedit(device_signal_line_edit):
|
||||
|
Reference in New Issue
Block a user