From 1e190923196f8b28c92dfdd83b9ce90873dd792d Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 21 Oct 2025 14:26:04 +0200 Subject: [PATCH] feat(positioner_box_2d): added properties to enable/disable vertical and horizontal controls --- bec_widgets/cli/client.py | 28 +++++++++ .../positioner_box_2d/positioner_box_2d.py | 53 ++++++++++++++++- tests/unit_tests/test_positioner_box_2d.py | 57 +++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 71324b71..6cc8a388 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -3534,6 +3534,34 @@ class PositionerBox2D(RPCBase): Take a screenshot of the dock area and save it to a file. """ + @property + @rpc_call + def enable_controls_hor(self) -> "bool": + """ + Persisted switch for horizontal control buttons (tweak/step). + """ + + @enable_controls_hor.setter + @rpc_call + def enable_controls_hor(self) -> "bool": + """ + Persisted switch for horizontal control buttons (tweak/step). + """ + + @property + @rpc_call + def enable_controls_ver(self) -> "bool": + """ + Persisted switch for vertical control buttons (tweak/step). + """ + + @enable_controls_ver.setter + @rpc_call + def enable_controls_ver(self) -> "bool": + """ + Persisted switch for vertical control buttons (tweak/step). + """ + class PositionerControlLine(RPCBase): """A widget that controls a single device.""" diff --git a/bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py b/bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py index 37bfc90b..23630380 100644 --- a/bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +++ b/bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py @@ -34,7 +34,15 @@ class PositionerBox2D(PositionerBoxBase): PLUGIN = True RPC = True - USER_ACCESS = ["set_positioner_hor", "set_positioner_ver", "screenshot"] + USER_ACCESS = [ + "set_positioner_hor", + "set_positioner_ver", + "screenshot", + "enable_controls_hor", + "enable_controls_hor.setter", + "enable_controls_ver", + "enable_controls_ver.setter", + ] device_changed_hor = Signal(str, str) device_changed_ver = Signal(str, str) @@ -63,6 +71,8 @@ class PositionerBox2D(PositionerBoxBase): self._limits_hor = None self._limits_ver = None self._dialog = None + self._enable_controls_hor = True + self._enable_controls_ver = True if self.current_path == "": self.current_path = os.path.dirname(__file__) self.init_ui() @@ -281,6 +291,7 @@ class PositionerBox2D(PositionerBoxBase): self.on_device_readback_hor, self._device_ui_components_hv("horizontal"), ) + self._apply_controls_enabled("horizontal") @SafeSlot(str, str) def on_device_change_ver(self, old_device: str, new_device: str): @@ -300,6 +311,7 @@ class PositionerBox2D(PositionerBoxBase): self.on_device_readback_ver, self._device_ui_components_hv("vertical"), ) + self._apply_controls_enabled("vertical") def _device_ui_components_hv(self, device: DeviceId) -> DeviceUpdateUIComponents: if device == "horizontal": @@ -337,6 +349,25 @@ class PositionerBox2D(PositionerBoxBase): if device == self.device_ver: return self._device_ui_components_hv("vertical") + def _apply_controls_enabled(self, axis: DeviceId): + state = self._enable_controls_hor if axis == "horizontal" else self._enable_controls_ver + if axis == "horizontal": + widgets = [ + self.ui.tweak_increase_hor, + self.ui.tweak_decrease_hor, + self.ui.step_increase_hor, + self.ui.step_decrease_hor, + ] + else: + widgets = [ + self.ui.tweak_increase_ver, + self.ui.tweak_decrease_ver, + self.ui.step_increase_ver, + self.ui.step_decrease_ver, + ] + for w in widgets: + w.setEnabled(state) + @SafeSlot(dict, dict) def on_device_readback_hor(self, msg_content: dict, metadata: dict): """Callback for device readback. @@ -417,6 +448,26 @@ class PositionerBox2D(PositionerBoxBase): """Step size for tweak""" self.ui.step_size_ver.setValue(val) + @SafeProperty(bool) + def enable_controls_hor(self) -> bool: + """Persisted switch for horizontal control buttons (tweak/step).""" + return self._enable_controls_hor + + @enable_controls_hor.setter + def enable_controls_hor(self, value: bool): + self._enable_controls_hor = value + self._apply_controls_enabled("horizontal") + + @SafeProperty(bool) + def enable_controls_ver(self) -> bool: + """Persisted switch for vertical control buttons (tweak/step).""" + return self._enable_controls_ver + + @enable_controls_ver.setter + def enable_controls_ver(self, value: bool): + self._enable_controls_ver = value + self._apply_controls_enabled("vertical") + @SafeSlot() def on_tweak_inc_hor(self): """Tweak device a up""" diff --git a/tests/unit_tests/test_positioner_box_2d.py b/tests/unit_tests/test_positioner_box_2d.py index f3cd891c..40535ab0 100644 --- a/tests/unit_tests/test_positioner_box_2d.py +++ b/tests/unit_tests/test_positioner_box_2d.py @@ -80,3 +80,60 @@ def test_positioner_box_setpoint_changes(positioner_box_2d: PositionerBox2D): positioner_box_2d.ui.setpoint_ver.setText("100") positioner_box_2d.on_setpoint_change_ver() mock_move.assert_called_once_with(100, relative=False) + + +def _hor_buttons(widget: PositionerBox2D): + return [ + widget.ui.tweak_increase_hor, + widget.ui.tweak_decrease_hor, + widget.ui.step_increase_hor, + widget.ui.step_decrease_hor, + ] + + +def _ver_buttons(widget: PositionerBox2D): + return [ + widget.ui.tweak_increase_ver, + widget.ui.tweak_decrease_ver, + widget.ui.step_increase_ver, + widget.ui.step_decrease_ver, + ] + + +def test_controls_default_enabled(positioner_box_2d: PositionerBox2D): + """By default both axes controls are enabled and UI reflects it.""" + assert positioner_box_2d.enable_controls_hor is True + assert positioner_box_2d.enable_controls_ver is True + assert all(w.isEnabled() for w in _hor_buttons(positioner_box_2d)) + assert all(w.isEnabled() for w in _ver_buttons(positioner_box_2d)) + + +def test_disable_enable_controls_and_persist_across_device_change( + positioner_box_2d: PositionerBox2D, qtbot +): + """Disabling an axis should disable its buttons and remain disabled after device (re)binding.""" + # Disable horizontal and verify UI + positioner_box_2d.enable_controls_hor = False + assert positioner_box_2d.enable_controls_hor is False + assert all(not w.isEnabled() for w in _hor_buttons(positioner_box_2d)) + + # Simulate a horizontal device change; state must persist after queued re-apply + positioner_box_2d.on_device_change_hor("samx", "samx") + qtbot.waitUntil(lambda: all(not w.isEnabled() for w in _hor_buttons(positioner_box_2d))) + + # Re-enable and verify UI + positioner_box_2d.enable_controls_hor = True + qtbot.waitUntil(lambda: all(w.isEnabled() for w in _hor_buttons(positioner_box_2d))) + + # Disable vertical and verify UI + positioner_box_2d.enable_controls_ver = False + assert positioner_box_2d.enable_controls_ver is False + assert all(not w.isEnabled() for w in _ver_buttons(positioner_box_2d)) + + # Simulate a vertical device change; state must persist after queued re-apply + positioner_box_2d.on_device_change_ver("samy", "samy") + qtbot.waitUntil(lambda: all(not w.isEnabled() for w in _ver_buttons(positioner_box_2d))) + + # Re-enable and verify UI + positioner_box_2d.enable_controls_ver = True + qtbot.waitUntil(lambda: all(w.isEnabled() for w in _ver_buttons(positioner_box_2d)))