diff --git a/bec_widgets/widgets/control/buttons/stop_button/stop_button.py b/bec_widgets/widgets/control/buttons/stop_button/stop_button.py index 218fa2e9..3534a1fa 100644 --- a/bec_widgets/widgets/control/buttons/stop_button/stop_button.py +++ b/bec_widgets/widgets/control/buttons/stop_button/stop_button.py @@ -1,5 +1,5 @@ from bec_qthemes import material_icon -from qtpy.QtCore import Qt +from qtpy.QtCore import Qt, QTimer from qtpy.QtWidgets import QHBoxLayout, QPushButton, QSizePolicy, QToolButton, QWidget from bec_widgets.utils.bec_widget import BECWidget @@ -12,6 +12,9 @@ class StopButton(BECWidget, QWidget): PLUGIN = True ICON_NAME = "dangerous" RPC = False + ABORT_LABEL = "Stop" + EMERGENCY_STOP_LABEL = "Emergency Stop" + EMERGENCY_STOP_TIMEOUT_MS = 2000 def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs): super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs) @@ -22,15 +25,19 @@ class StopButton(BECWidget, QWidget): self.layout.setSpacing(0) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setAlignment(Qt.AlignmentFlag.AlignVCenter) + self._emergency_stop_active = False + self._reset_timer = QTimer(self) + self._reset_timer.setSingleShot(True) + self._reset_timer.timeout.connect(self._deactivate_emergency_stop) if toolbar: icon = material_icon("stop", color="#cc181e", filled=True, convert_to_pixmap=False) self.button = QToolButton(icon=icon) - self.button.setToolTip("Stop the scan queue") + self.button.setToolTip(self.ABORT_LABEL) else: self.button = QPushButton() self.button.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) - self.button.setText("Stop") + self.button.setText(self.ABORT_LABEL) self.button.setProperty("variant", "danger") self.button.clicked.connect(self.stop_scan) @@ -41,12 +48,39 @@ class StopButton(BECWidget, QWidget): self, ): # , scan_id: str | None = None): #FIXME scan_id will be added when combining with Queue widget """ - Stop the scan. + Abort the scan by default, then temporarily offer an emergency stop. Args: - scan_id(str|None): The scan id to stop. If None, the current scan will be stopped. + scan_id(str|None): The scan id to stop. If None, the current scan will be targeted. """ - self.queue.request_scan_halt() + if self._emergency_stop_active: + self.queue.request_scan_halt() + self._activate_emergency_stop() + return + + self.queue.request_scan_abortion() + self._activate_emergency_stop() + + def _activate_emergency_stop(self) -> None: + self._emergency_stop_active = True + self._set_button_label(self.EMERGENCY_STOP_LABEL) + self._reset_timer.start(self.EMERGENCY_STOP_TIMEOUT_MS) + + def _deactivate_emergency_stop(self) -> None: + self._emergency_stop_active = False + self._set_button_label(self.ABORT_LABEL) + + def _set_button_label(self, label: str) -> None: + if hasattr(self.button, "setText"): + self.button.setText(label) + self.button.setToolTip(label) + + def cleanup(self): + """Stop and dispose the emergency-stop reset timer before widget teardown.""" + self._reset_timer.stop() + self._reset_timer.timeout.disconnect(self._deactivate_emergency_stop) + self._reset_timer.deleteLater() + super().cleanup() if __name__ == "__main__": # pragma: no cover diff --git a/tests/unit_tests/test_stop_button.py b/tests/unit_tests/test_stop_button.py index e428a7de..b9ce5773 100644 --- a/tests/unit_tests/test_stop_button.py +++ b/tests/unit_tests/test_stop_button.py @@ -1,5 +1,7 @@ # pylint: disable=missing-function-docstring, missing-module-docstring, unused-import +from unittest.mock import MagicMock + import pytest from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton @@ -10,13 +12,48 @@ from .client_mocks import mocked_client @pytest.fixture def stop_button(qtbot, mocked_client): widget = StopButton(client=mocked_client) + widget.queue = MagicMock() qtbot.addWidget(widget) qtbot.waitExposed(widget) - yield widget + yield widget, qtbot def test_stop_button(stop_button): - assert stop_button.button.text() == "Stop" - stop_button.button.click() - assert stop_button.queue.request_scan_halt.called - stop_button.close() + widget, qtbot = stop_button + + assert widget.button.text() == "Stop" + + widget.button.click() + + assert widget.queue.request_scan_abortion.called + assert widget.button.text() == "Emergency Stop" + + qtbot.wait(widget.EMERGENCY_STOP_TIMEOUT_MS // 2) + widget.button.click() + + assert widget.queue.request_scan_halt.called + + qtbot.wait((widget.EMERGENCY_STOP_TIMEOUT_MS // 2) + 100) + assert widget.button.text() == "Emergency Stop" + + qtbot.wait((widget.EMERGENCY_STOP_TIMEOUT_MS // 2) + 100) + assert widget.button.text() == "Stop" + + widget.close() + + +def test_stop_button_click_extends_emergency_timeout(stop_button): + widget, qtbot = stop_button + + widget.button.click() + qtbot.wait(widget.EMERGENCY_STOP_TIMEOUT_MS - 250) + + widget.button.click() + assert widget.button.text() == "Emergency Stop" + + qtbot.wait(200) + assert widget.button.text() == "Emergency Stop" + + qtbot.wait(widget.EMERGENCY_STOP_TIMEOUT_MS - 100) + assert widget.button.text() == "Stop" + widget.close()