0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 11:41:49 +02:00

feat: add PositionerControlLine

This commit is contained in:
2024-08-08 14:34:46 +02:00
parent a50d9c7b3f
commit c80a7cd108
8 changed files with 372 additions and 13 deletions

View File

@ -24,6 +24,7 @@ class Widgets(str, enum.Enum):
DeviceComboBox = "DeviceComboBox" DeviceComboBox = "DeviceComboBox"
DeviceLineEdit = "DeviceLineEdit" DeviceLineEdit = "DeviceLineEdit"
PositionerBox = "PositionerBox" PositionerBox = "PositionerBox"
PositionerControlLine = "PositionerControlLine"
RingProgressBar = "RingProgressBar" RingProgressBar = "RingProgressBar"
ScanControl = "ScanControl" ScanControl = "ScanControl"
StopButton = "StopButton" StopButton = "StopButton"
@ -488,7 +489,7 @@ class BECFigure(RPCBase):
col: "int | None" = None, col: "int | None" = None,
dap: "str | None" = None, dap: "str | None" = None,
config: "dict | None" = None, config: "dict | None" = None,
**axis_kwargs, **axis_kwargs
) -> "BECWaveform": ) -> "BECWaveform":
""" """
Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure. Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
@ -530,7 +531,7 @@ class BECFigure(RPCBase):
row: "int | None" = None, row: "int | None" = None,
col: "int | None" = None, col: "int | None" = None,
config: "dict | None" = None, config: "dict | None" = None,
**axis_kwargs, **axis_kwargs
) -> "BECImageShow": ) -> "BECImageShow":
""" """
Add an image to the figure. Always access the first image widget in the figure. Add an image to the figure. Always access the first image widget in the figure.
@ -560,7 +561,7 @@ class BECFigure(RPCBase):
row: "int | None" = None, row: "int | None" = None,
col: "int | None" = None, col: "int | None" = None,
config: "dict | None" = None, config: "dict | None" = None,
**axis_kwargs, **axis_kwargs
) -> "BECMotorMap": ) -> "BECMotorMap":
""" """
Add a motor map to the figure. Always access the first motor map widget in the figure. Add a motor map to the figure. Always access the first motor map widget in the figure.
@ -831,7 +832,7 @@ class BECImageShow(RPCBase):
downsample: "Optional[bool]" = True, downsample: "Optional[bool]" = True,
opacity: "Optional[float]" = 1.0, opacity: "Optional[float]" = 1.0,
vrange: "Optional[tuple[int, int]]" = None, vrange: "Optional[tuple[int, int]]" = None,
**kwargs, **kwargs
) -> "BECImageItem": ) -> "BECImageItem":
""" """
Add an image to the figure. Always access the first image widget in the figure. Add an image to the figure. Always access the first image widget in the figure.
@ -857,7 +858,7 @@ class BECImageShow(RPCBase):
downsample: "Optional[bool]" = True, downsample: "Optional[bool]" = True,
opacity: "Optional[float]" = 1.0, opacity: "Optional[float]" = 1.0,
vrange: "Optional[tuple[int, int]]" = None, vrange: "Optional[tuple[int, int]]" = None,
**kwargs, **kwargs
): ):
""" """
None None
@ -1146,7 +1147,7 @@ class BECImageWidget(RPCBase):
downsample: "Optional[bool]" = True, downsample: "Optional[bool]" = True,
opacity: "Optional[float]" = 1.0, opacity: "Optional[float]" = 1.0,
vrange: "Optional[tuple[int, int]]" = None, vrange: "Optional[tuple[int, int]]" = None,
**kwargs, **kwargs
) -> "BECImageItem": ) -> "BECImageItem":
""" """
None None
@ -1729,7 +1730,7 @@ class BECWaveform(RPCBase):
label: "str | None" = None, label: "str | None" = None,
validate: "bool" = True, validate: "bool" = True,
dap: "str | None" = None, dap: "str | None" = None,
**kwargs, **kwargs
) -> "BECCurve": ) -> "BECCurve":
""" """
Plot a curve to the plot widget. Plot a curve to the plot widget.
@ -1768,7 +1769,7 @@ class BECWaveform(RPCBase):
color: "Optional[str]" = None, color: "Optional[str]" = None,
dap: "str" = "GaussianModel", dap: "str" = "GaussianModel",
validate_bec: "bool" = True, validate_bec: "bool" = True,
**kwargs, **kwargs
) -> "BECCurve": ) -> "BECCurve":
""" """
Add LMFIT dap model curve to the plot widget. Add LMFIT dap model curve to the plot widget.
@ -2043,7 +2044,7 @@ class BECWaveformWidget(RPCBase):
label: "str | None" = None, label: "str | None" = None,
validate: "bool" = True, validate: "bool" = True,
dap: "str | None" = None, dap: "str | None" = None,
**kwargs, **kwargs
) -> "BECCurve": ) -> "BECCurve":
""" """
Plot a curve to the plot widget. Plot a curve to the plot widget.
@ -2077,7 +2078,7 @@ class BECWaveformWidget(RPCBase):
color: "str | None" = None, color: "str | None" = None,
dap: "str" = "GaussianModel", dap: "str" = "GaussianModel",
validate_bec: "bool" = True, validate_bec: "bool" = True,
**kwargs, **kwargs
) -> "BECCurve": ) -> "BECCurve":
""" """
Add LMFIT dap model curve to the plot widget. Add LMFIT dap model curve to the plot widget.
@ -2352,6 +2353,17 @@ class PositionerBox(RPCBase):
""" """
class PositionerControlLine(RPCBase):
@rpc_call
def set_positioner(self, positioner: str):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
class Ring(RPCBase): class Ring(RPCBase):
@rpc_call @rpc_call
def _get_all_rpc(self) -> "dict": def _get_all_rpc(self) -> "dict":

View File

@ -24,6 +24,9 @@ MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
class PositionerBox(BECWidget, QWidget): class PositionerBox(BECWidget, QWidget):
"""Simple Widget to control a positioner in box form""" """Simple Widget to control a positioner in box form"""
ui_file = "positioner_box.ui"
dimensions = (234, 224)
USER_ACCESS = ["set_positioner"] USER_ACCESS = ["set_positioner"]
device_changed = Signal(str, str) device_changed = Signal(str, str)
@ -51,7 +54,7 @@ class PositionerBox(BECWidget, QWidget):
self.device_changed.connect(self.on_device_change) self.device_changed.connect(self.on_device_change)
current_path = os.path.dirname(__file__) current_path = os.path.dirname(__file__)
self.ui = UILoader(self).loader(os.path.join(current_path, "positioner_box.ui")) self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
self.layout = QVBoxLayout(self) self.layout = QVBoxLayout(self)
self.layout.addWidget(self.ui) self.layout.addWidget(self.ui)
@ -60,8 +63,8 @@ class PositionerBox(BECWidget, QWidget):
# fix the size of the device box # fix the size of the device box
db = self.ui.device_box db = self.ui.device_box
db.setFixedHeight(234) db.setFixedHeight(self.dimensions[0])
db.setFixedWidth(224) db.setFixedWidth(self.dimensions[1])
self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType) self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
self.ui.stop.clicked.connect(self.on_stop) self.ui.stop.clicked.connect(self.on_stop)

View File

@ -0,0 +1,33 @@
from bec_lib.device import Positioner
from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
class PositionerControlLine(PositionerBox):
"""A widget that controls a single device."""
ui_file = "positioner_control_line.ui"
dimensions = (70, 800) # height, width
def __init__(self, parent=None, device: Positioner = None, *args, **kwargs):
"""Initialize the DeviceControlLine.
Args:
parent: The parent widget.
device (Positioner): The device to control.
"""
super().__init__(parent=parent, device=device, *args, **kwargs)
if __name__ == "__main__": # pragma: no cover
import sys
import qdarktheme
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
qdarktheme.setup_theme("dark")
widget = PositionerControlLine(device="samy")
widget.show()
sys.exit(app.exec_())

View File

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

View File

@ -0,0 +1,209 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>785</width>
<height>91</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="device_box">
<property name="title">
<string>Device Name</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QToolButton" name="tool_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="PositionIndicator" name="position_indicator"/>
</item>
<item>
<widget class="QLabel" name="readback">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Position</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="setpoint">
<property name="minimumSize">
<size>
<width>150</width>
<height>24</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="stop">
<property name="text">
<string>Stop</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="tweak_left">
<property name="minimumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="arrowType">
<enum>Qt::ArrowType::LeftArrow</enum>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="step_size"/>
</item>
<item>
<widget class="QToolButton" name="tweak_right">
<property name="minimumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="arrowType">
<enum>Qt::ArrowType::RightArrow</enum>
</property>
</widget>
</item>
<item>
<widget class="SpinnerWidget" name="spinner_widget">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SpinnerWidget</class>
<extends>QWidget</extends>
<header>spinner_widget</header>
</customwidget>
<customwidget>
<class>PositionIndicator</class>
<extends>QWidget</extends>
<header>position_indicator</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,58 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.positioner_box.positioner_control_line import PositionerControlLine
DOM_XML = """
<ui language='c++'>
<widget class='PositionerControlLine' name='positioner_control_line'>
</widget>
</ui>
"""
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
class PositionerControlLinePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = PositionerControlLine(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "Device Control"
def icon(self):
icon_path = os.path.join(MODULE_PATH, "assets", "designer_icons", "positioner_box.png")
return QIcon(icon_path)
def includeFile(self):
return "positioner_control_line"
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 "PositionerControlLine"
def toolTip(self):
return "A widget that controls a single positioner in line form."
def whatsThis(self):
return self.toolTip()

View File

@ -0,0 +1,17 @@
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.positioner_box.positioner_control_line_plugin import (
PositionerControlLinePlugin,
)
QPyDesignerCustomWidgetCollection.addCustomWidget(PositionerControlLinePlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@ -7,12 +7,14 @@ from bec_lib.messages import ScanQueueMessage
from qtpy.QtGui import QValidator from qtpy.QtGui import QValidator
from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
from bec_widgets.widgets.positioner_box.positioner_control_line import PositionerControlLine
from .client_mocks import mocked_client from .client_mocks import mocked_client
@pytest.fixture @pytest.fixture
def positioner_box(qtbot, mocked_client): def positioner_box(qtbot, mocked_client):
"""Fixture for PositionerBox widget"""
with mock.patch("bec_widgets.widgets.positioner_box.positioner_box.uuid.uuid4") as mock_uuid: with mock.patch("bec_widgets.widgets.positioner_box.positioner_box.uuid.uuid4") as mock_uuid:
mock_uuid.return_value = "fake_uuid" mock_uuid.return_value = "fake_uuid"
with mock.patch( with mock.patch(
@ -25,6 +27,7 @@ def positioner_box(qtbot, mocked_client):
def test_positioner_box(positioner_box): def test_positioner_box(positioner_box):
"""Test init of positioner box"""
assert positioner_box.device == "samx" assert positioner_box.device == "samx"
data = positioner_box.dev["samx"].read() data = positioner_box.dev["samx"].read()
# Avoid check for Positioner class from BEC in _init_device # Avoid check for Positioner class from BEC in _init_device
@ -42,6 +45,7 @@ def test_positioner_box(positioner_box):
def test_positioner_box_update_limits(positioner_box): def test_positioner_box_update_limits(positioner_box):
"""Test update of limits"""
positioner_box._limits = None positioner_box._limits = None
positioner_box.update_limits([0, 10]) positioner_box.update_limits([0, 10])
assert positioner_box._limits == [0, 10] assert positioner_box._limits == [0, 10]
@ -63,6 +67,7 @@ def test_positioner_box_update_limits(positioner_box):
def test_positioner_box_on_stop(positioner_box): def test_positioner_box_on_stop(positioner_box):
"""Test on stop button"""
with mock.patch.object(positioner_box.client.connector, "send") as mock_send: with mock.patch.object(positioner_box.client.connector, "send") as mock_send:
positioner_box.on_stop() positioner_box.on_stop()
params = {"device": "samx", "rpc_id": "fake_uuid", "func": "stop", "args": [], "kwargs": {}} params = {"device": "samx", "rpc_id": "fake_uuid", "func": "stop", "args": [], "kwargs": {}}
@ -76,6 +81,7 @@ def test_positioner_box_on_stop(positioner_box):
def test_positioner_box_setpoint_change(positioner_box): def test_positioner_box_setpoint_change(positioner_box):
"""Test positioner box setpoint change"""
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move: with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
positioner_box.ui.setpoint.setText("100") positioner_box.ui.setpoint.setText("100")
positioner_box.on_setpoint_change() positioner_box.on_setpoint_change()
@ -83,6 +89,7 @@ def test_positioner_box_setpoint_change(positioner_box):
def test_positioner_box_on_tweak_right(positioner_box): def test_positioner_box_on_tweak_right(positioner_box):
"""Test tweak right button"""
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move: with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
positioner_box.ui.step_size.setValue(0.1) positioner_box.ui.step_size.setValue(0.1)
positioner_box.on_tweak_right() positioner_box.on_tweak_right()
@ -90,6 +97,7 @@ def test_positioner_box_on_tweak_right(positioner_box):
def test_positioner_box_on_tweak_left(positioner_box): def test_positioner_box_on_tweak_left(positioner_box):
"""Test tweak left button"""
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move: with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
positioner_box.ui.step_size.setValue(0.1) positioner_box.ui.step_size.setValue(0.1)
positioner_box.on_tweak_left() positioner_box.on_tweak_left()
@ -97,8 +105,26 @@ def test_positioner_box_on_tweak_left(positioner_box):
def test_positioner_box_setpoint_out_of_range(positioner_box): def test_positioner_box_setpoint_out_of_range(positioner_box):
"""Test setpoint out of range"""
positioner_box.update_limits([0, 10]) positioner_box.update_limits([0, 10])
positioner_box.ui.setpoint.setText("100") positioner_box.ui.setpoint.setText("100")
positioner_box.on_setpoint_change() positioner_box.on_setpoint_change()
assert positioner_box.ui.setpoint.text() == "100" assert positioner_box.ui.setpoint.text() == "100"
assert positioner_box.ui.setpoint.hasAcceptableInput() == False assert positioner_box.ui.setpoint.hasAcceptableInput() == False
def test_positioner_control_line(qtbot, mocked_client):
"""Test PositionerControlLine.
Inherits from PositionerBox, but the layout is changed. Check dimensions only
"""
with mock.patch("bec_widgets.widgets.positioner_box.positioner_box.uuid.uuid4") as mock_uuid:
mock_uuid.return_value = "fake_uuid"
with mock.patch(
"bec_widgets.widgets.positioner_box.positioner_box.PositionerBox._check_device_is_valid",
return_value=True,
):
db = PositionerControlLine(device="samx", client=mocked_client)
qtbot.addWidget(db)
assert db.ui.device_box.height() == 70
assert db.ui.device_box.width() == 800