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:
@ -24,6 +24,7 @@ class Widgets(str, enum.Enum):
|
||||
DeviceComboBox = "DeviceComboBox"
|
||||
DeviceLineEdit = "DeviceLineEdit"
|
||||
PositionerBox = "PositionerBox"
|
||||
PositionerControlLine = "PositionerControlLine"
|
||||
RingProgressBar = "RingProgressBar"
|
||||
ScanControl = "ScanControl"
|
||||
StopButton = "StopButton"
|
||||
@ -488,7 +489,7 @@ class BECFigure(RPCBase):
|
||||
col: "int | None" = None,
|
||||
dap: "str | None" = None,
|
||||
config: "dict | None" = None,
|
||||
**axis_kwargs,
|
||||
**axis_kwargs
|
||||
) -> "BECWaveform":
|
||||
"""
|
||||
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,
|
||||
col: "int | None" = None,
|
||||
config: "dict | None" = None,
|
||||
**axis_kwargs,
|
||||
**axis_kwargs
|
||||
) -> "BECImageShow":
|
||||
"""
|
||||
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,
|
||||
col: "int | None" = None,
|
||||
config: "dict | None" = None,
|
||||
**axis_kwargs,
|
||||
**axis_kwargs
|
||||
) -> "BECMotorMap":
|
||||
"""
|
||||
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,
|
||||
opacity: "Optional[float]" = 1.0,
|
||||
vrange: "Optional[tuple[int, int]]" = None,
|
||||
**kwargs,
|
||||
**kwargs
|
||||
) -> "BECImageItem":
|
||||
"""
|
||||
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,
|
||||
opacity: "Optional[float]" = 1.0,
|
||||
vrange: "Optional[tuple[int, int]]" = None,
|
||||
**kwargs,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
None
|
||||
@ -1146,7 +1147,7 @@ class BECImageWidget(RPCBase):
|
||||
downsample: "Optional[bool]" = True,
|
||||
opacity: "Optional[float]" = 1.0,
|
||||
vrange: "Optional[tuple[int, int]]" = None,
|
||||
**kwargs,
|
||||
**kwargs
|
||||
) -> "BECImageItem":
|
||||
"""
|
||||
None
|
||||
@ -1729,7 +1730,7 @@ class BECWaveform(RPCBase):
|
||||
label: "str | None" = None,
|
||||
validate: "bool" = True,
|
||||
dap: "str | None" = None,
|
||||
**kwargs,
|
||||
**kwargs
|
||||
) -> "BECCurve":
|
||||
"""
|
||||
Plot a curve to the plot widget.
|
||||
@ -1768,7 +1769,7 @@ class BECWaveform(RPCBase):
|
||||
color: "Optional[str]" = None,
|
||||
dap: "str" = "GaussianModel",
|
||||
validate_bec: "bool" = True,
|
||||
**kwargs,
|
||||
**kwargs
|
||||
) -> "BECCurve":
|
||||
"""
|
||||
Add LMFIT dap model curve to the plot widget.
|
||||
@ -2043,7 +2044,7 @@ class BECWaveformWidget(RPCBase):
|
||||
label: "str | None" = None,
|
||||
validate: "bool" = True,
|
||||
dap: "str | None" = None,
|
||||
**kwargs,
|
||||
**kwargs
|
||||
) -> "BECCurve":
|
||||
"""
|
||||
Plot a curve to the plot widget.
|
||||
@ -2077,7 +2078,7 @@ class BECWaveformWidget(RPCBase):
|
||||
color: "str | None" = None,
|
||||
dap: "str" = "GaussianModel",
|
||||
validate_bec: "bool" = True,
|
||||
**kwargs,
|
||||
**kwargs
|
||||
) -> "BECCurve":
|
||||
"""
|
||||
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):
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
|
@ -24,6 +24,9 @@ MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
class PositionerBox(BECWidget, QWidget):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
|
||||
ui_file = "positioner_box.ui"
|
||||
dimensions = (234, 224)
|
||||
|
||||
USER_ACCESS = ["set_positioner"]
|
||||
device_changed = Signal(str, str)
|
||||
|
||||
@ -51,7 +54,7 @@ class PositionerBox(BECWidget, QWidget):
|
||||
self.device_changed.connect(self.on_device_change)
|
||||
|
||||
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.addWidget(self.ui)
|
||||
@ -60,8 +63,8 @@ class PositionerBox(BECWidget, QWidget):
|
||||
|
||||
# fix the size of the device box
|
||||
db = self.ui.device_box
|
||||
db.setFixedHeight(234)
|
||||
db.setFixedWidth(224)
|
||||
db.setFixedHeight(self.dimensions[0])
|
||||
db.setFixedWidth(self.dimensions[1])
|
||||
|
||||
self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
|
||||
self.ui.stop.clicked.connect(self.on_stop)
|
||||
|
@ -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_())
|
@ -0,0 +1 @@
|
||||
{'files': ['positioner_control_line.py']}
|
209
bec_widgets/widgets/positioner_box/positioner_control_line.ui
Normal file
209
bec_widgets/widgets/positioner_box/positioner_control_line.ui
Normal 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>
|
@ -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()
|
@ -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()
|
@ -7,12 +7,14 @@ from bec_lib.messages import ScanQueueMessage
|
||||
from qtpy.QtGui import QValidator
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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:
|
||||
mock_uuid.return_value = "fake_uuid"
|
||||
with mock.patch(
|
||||
@ -25,6 +27,7 @@ def positioner_box(qtbot, mocked_client):
|
||||
|
||||
|
||||
def test_positioner_box(positioner_box):
|
||||
"""Test init of positioner box"""
|
||||
assert positioner_box.device == "samx"
|
||||
data = positioner_box.dev["samx"].read()
|
||||
# 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):
|
||||
"""Test update of limits"""
|
||||
positioner_box._limits = None
|
||||
positioner_box.update_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):
|
||||
"""Test on stop button"""
|
||||
with mock.patch.object(positioner_box.client.connector, "send") as mock_send:
|
||||
positioner_box.on_stop()
|
||||
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):
|
||||
"""Test positioner box setpoint change"""
|
||||
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
|
||||
positioner_box.ui.setpoint.setText("100")
|
||||
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):
|
||||
"""Test tweak right button"""
|
||||
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
|
||||
positioner_box.ui.step_size.setValue(0.1)
|
||||
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):
|
||||
"""Test tweak left button"""
|
||||
with mock.patch.object(positioner_box.dev["samx"], "move") as mock_move:
|
||||
positioner_box.ui.step_size.setValue(0.1)
|
||||
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):
|
||||
"""Test setpoint out of range"""
|
||||
positioner_box.update_limits([0, 10])
|
||||
positioner_box.ui.setpoint.setText("100")
|
||||
positioner_box.on_setpoint_change()
|
||||
assert positioner_box.ui.setpoint.text() == "100"
|
||||
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
|
||||
|
Reference in New Issue
Block a user