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 +
spinner_widget
+
+ + PositionIndicator + QWidget +
position_indicator
+
+
+ + +
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