fix(beamline_states): trigger on Warning checkbox in the setting of the beamline pill

This commit is contained in:
2026-06-16 14:57:53 +02:00
committed by Klaus Wakonig
parent fe305d5d07
commit 73d49d9dfa
3 changed files with 104 additions and 0 deletions
@@ -219,6 +219,7 @@ class _BeamlineStatePillDelegate(QStyledItemDelegate):
pill.update_requested.connect(self._manager._update_state_parameters)
pill.remove_requested.connect(self._manager._remove_state_requested)
pill.scan_interlock_toggle_requested.connect(self._manager._on_interlock_toggle_requested)
pill.scan_interlock_statuses_changed.connect(self._manager._on_interlock_statuses_changed)
pill.row_height_changed.connect(lambda name=name: self._manager._sync_pill_item_size(name))
self._manager._state_pills[str(name)] = pill
return pill
@@ -712,6 +713,19 @@ class BeamlineStateManager(BECWidget, QWidget):
return
self._refresh_scan_interlock()
@SafeSlot(str, object)
def _on_interlock_statuses_changed(self, state_name: str, statuses: object) -> None:
# Only persist when the state is currently watched; otherwise the pill keeps the
# preference for when it is locked later.
if state_name not in self._interlock_states:
return
try:
self._scan_interlock.add_state_to_interlock(state_name, list(statuses))
except Exception as exc:
QMessageBox.warning(self, "Cannot Update Scan Interlock", str(exc))
return
self._refresh_scan_interlock()
def _open_persistent_editors(self, expanded_names: set[str] | None = None) -> None:
expanded_names = expanded_names or set()
for row in range(self._model.rowCount()):
@@ -9,6 +9,7 @@ from qtpy.QtCore import Property, QEasingCurve, QPropertyAnimation, Qt, Signal
from qtpy.QtGui import QColor, QMouseEvent, QPalette
from qtpy.QtWidgets import (
QApplication,
QCheckBox,
QFormLayout,
QGraphicsDropShadowEffect,
QHBoxLayout,
@@ -64,6 +65,7 @@ class BeamlineStatePill(BECWidget, QWidget):
update_requested = Signal(str, object)
remove_requested = Signal(str)
scan_interlock_toggle_requested = Signal(str, bool)
scan_interlock_statuses_changed = Signal(str, object)
row_height_changed = Signal()
_STATUS_LABELS = BEAMLINE_STATE_STATUS_LABELS
@@ -217,11 +219,22 @@ class BeamlineStatePill(BECWidget, QWidget):
self._settings_form.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
self._settings_form.addRow("Type", self._state_type_value)
self._interlock_warning_checkbox = QCheckBox(
"Trigger ScanInterlock on WARNING state", self._settings
)
self._interlock_warning_checkbox.setToolTip(
"By default both VALID and WARNING are accepted. Enable this so a WARNING status also "
"trips the scan interlock (only VALID accepted)."
)
self._interlock_warning_checkbox.toggled.connect(self._on_interlock_warning_toggled)
self._sync_interlock_warning_checkbox()
settings_layout = QVBoxLayout(self._settings)
settings_layout.setContentsMargins(12, 8, 12, 12)
settings_layout.setSpacing(8)
settings_layout.addLayout(self._settings_form)
settings_layout.addLayout(self._config_form_host)
settings_layout.addWidget(self._interlock_warning_checkbox)
settings_layout.addLayout(button_layout)
layout = QVBoxLayout(self)
@@ -320,6 +333,7 @@ class BeamlineStatePill(BECWidget, QWidget):
triggered = bool(triggered) and required_statuses is not None
if required_statuses is not None:
self._interlock_statuses = list(required_statuses)
self._sync_interlock_warning_checkbox()
if (required_statuses, triggered) == (
self._interlock_required_statuses,
self._interlock_triggered,
@@ -343,6 +357,22 @@ class BeamlineStatePill(BECWidget, QWidget):
def set_interlock_statuses(self, statuses: list[str]) -> None:
"""Configure the accepted scan-interlock statuses for this state."""
self._interlock_statuses = list(statuses)
self._sync_interlock_warning_checkbox()
def _sync_interlock_warning_checkbox(self) -> None:
trigger_on_warning = "warning" not in self._interlock_statuses
self._interlock_warning_checkbox.blockSignals(True)
self._interlock_warning_checkbox.setChecked(trigger_on_warning)
self._interlock_warning_checkbox.blockSignals(False)
@SafeSlot(bool)
def _on_interlock_warning_toggled(self, trigger_on_warning: bool) -> None:
statuses = ["valid"] if trigger_on_warning else ["valid", "warning"]
if statuses == self._interlock_statuses:
return
self._interlock_statuses = statuses
if self._state_name is not None:
self.scan_interlock_statuses_changed.emit(self._state_name, statuses)
@SafeSlot()
def _emit_interlock_toggle_requested(self) -> None:
@@ -892,6 +892,66 @@ def test_beamline_state_manager_pill_toggle_calls_backend(qtbot, mocked_client):
assert beamline_state_manager._interlock_states == {}
def test_beamline_state_pill_settings_warning_checkbox(qtbot, mocked_client):
pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client)
# Default accepts VALID and WARNING, so the checkbox is unchecked.
assert pill._interlock_warning_checkbox.isChecked() is False
assert pill.interlock_statuses == ["valid", "warning"]
with qtbot.waitSignal(pill.scan_interlock_statuses_changed) as signal:
pill._interlock_warning_checkbox.setChecked(True)
assert signal.args == ["limits", ["valid"]]
assert pill.interlock_statuses == ["valid"]
pill._interlock_warning_checkbox.setChecked(False)
assert pill.interlock_statuses == ["valid", "warning"]
def test_beamline_state_pill_settings_warning_checkbox_reflects_statuses(qtbot, mocked_client):
pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client)
pill.set_interlock_statuses(["valid"])
assert pill._interlock_warning_checkbox.isChecked() is True
pill.set_interlock_statuses(["valid", "warning"])
assert pill._interlock_warning_checkbox.isChecked() is False
def test_beamline_state_manager_settings_warning_reenrolls_watched_state(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
beamline_state_manager.update_available_states({"states": [_limits_state()]}, {})
fake_interlock = _FakeScanInterlock(
enabled=True, states_watched={"limits": ["valid", "warning"]}
)
_install_fake_scan_interlock(beamline_state_manager, fake_interlock)
pill = beamline_state_manager._state_pills["limits"]
assert pill.interlock_statuses == ["valid", "warning"]
pill._interlock_warning_checkbox.setChecked(True)
assert fake_interlock.added[-1] == ("limits", ["valid"])
assert beamline_state_manager._interlock_states == {"limits": ["valid"]}
def test_beamline_state_manager_settings_warning_no_backend_when_not_watched(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
beamline_state_manager.update_available_states({"states": [_limits_state()]}, {})
fake_interlock = _FakeScanInterlock()
_install_fake_scan_interlock(beamline_state_manager, fake_interlock)
pill = beamline_state_manager._state_pills["limits"]
pill._interlock_warning_checkbox.setChecked(True)
# Not watched yet: no backend write, but the preference is kept and used when locked.
assert fake_interlock.added == []
assert pill.interlock_statuses == ["valid"]
pill._interlock_button.click()
assert fake_interlock.added == [("limits", ["valid"])]
def test_beamline_state_manager_lock_uses_dialog_status_preference(
qtbot, mocked_client, monkeypatch
):