diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py
index 99b8fd6b..1ea0ed7e 100644
--- a/bec_widgets/cli/client.py
+++ b/bec_widgets/cli/client.py
@@ -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":
diff --git a/bec_widgets/widgets/positioner_box/positioner_box.py b/bec_widgets/widgets/positioner_box/positioner_box.py
index 2bac5da3..230af994 100644
--- a/bec_widgets/widgets/positioner_box/positioner_box.py
+++ b/bec_widgets/widgets/positioner_box/positioner_box.py
@@ -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)
diff --git a/bec_widgets/widgets/positioner_box/positioner_control_line.py b/bec_widgets/widgets/positioner_box/positioner_control_line.py
new file mode 100644
index 00000000..5e4a1802
--- /dev/null
+++ b/bec_widgets/widgets/positioner_box/positioner_control_line.py
@@ -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_())
diff --git a/bec_widgets/widgets/positioner_box/positioner_control_line.pyproject b/bec_widgets/widgets/positioner_box/positioner_control_line.pyproject
new file mode 100644
index 00000000..83cde225
--- /dev/null
+++ b/bec_widgets/widgets/positioner_box/positioner_control_line.pyproject
@@ -0,0 +1 @@
+{'files': ['positioner_control_line.py']}
\ No newline at end of file
diff --git a/bec_widgets/widgets/positioner_box/positioner_control_line.ui b/bec_widgets/widgets/positioner_box/positioner_control_line.ui
new file mode 100644
index 00000000..fc1320d6
--- /dev/null
+++ b/bec_widgets/widgets/positioner_box/positioner_control_line.ui
@@ -0,0 +1,209 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 785
+ 91
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ Form
+
+
+ -
+
+
+ Device Name
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 25
+ 25
+
+
+
+
+ 25
+ 25
+
+
+
+ ...
+
+
+
+ -
+
+
-
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+
+ 150
+ 16777215
+
+
+
+ Position
+
+
+ Qt::AlignmentFlag::AlignCenter
+
+
+
+ -
+
+
+
+ 150
+ 24
+
+
+
+
+ 150
+ 16777215
+
+
+
+ Qt::AlignmentFlag::AlignCenter
+
+
+
+
+
+ -
+
+
+ Stop
+
+
+
+ -
+
+
+
+ 30
+ 30
+
+
+
+
+ 30
+ 30
+
+
+
+ ...
+
+
+
+ 30
+ 30
+
+
+
+ Qt::ArrowType::LeftArrow
+
+
+
+ -
+
+
+ -
+
+
+
+ 30
+ 30
+
+
+
+
+ 30
+ 30
+
+
+
+ ...
+
+
+
+ 30
+ 30
+
+
+
+ Qt::ArrowType::RightArrow
+
+
+
+ -
+
+
+
+ 25
+ 25
+
+
+
+
+ 25
+ 25
+
+
+
+
+
+
+
+
+
+
+
+ SpinnerWidget
+ QWidget
+
+
+
+ PositionIndicator
+ QWidget
+
+
+
+
+
+
diff --git a/bec_widgets/widgets/positioner_box/positioner_control_line_plugin.py b/bec_widgets/widgets/positioner_box/positioner_control_line_plugin.py
new file mode 100644
index 00000000..41bb3d6d
--- /dev/null
+++ b/bec_widgets/widgets/positioner_box/positioner_control_line_plugin.py
@@ -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 = """
+
+
+
+
+"""
+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()
diff --git a/bec_widgets/widgets/positioner_box/register_positioner_control_line.py b/bec_widgets/widgets/positioner_box/register_positioner_control_line.py
new file mode 100644
index 00000000..598a311e
--- /dev/null
+++ b/bec_widgets/widgets/positioner_box/register_positioner_control_line.py
@@ -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()
diff --git a/tests/unit_tests/test_positioner_box.py b/tests/unit_tests/test_positioner_box.py
index 85c9b5a3..1665f07e 100644
--- a/tests/unit_tests/test_positioner_box.py
+++ b/tests/unit_tests/test_positioner_box.py
@@ -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