feat(beamline-states): collapse all functionality with cleanup of not used settings widgets if state is not dirty

This commit is contained in:
2026-06-11 17:03:36 +02:00
parent 6577a814b7
commit 3897451381
4 changed files with 104 additions and 2 deletions
+6
View File
@@ -729,6 +729,12 @@ class BeamlineStateManager(RPCBase):
None
"""
@rpc_call
def collapse_all(self) -> "None":
"""
Collapse the settings panel of all displayed state pills.
"""
@rpc_call
def state_summary(self) -> "dict[str, dict[str, str]]":
"""
@@ -208,7 +208,15 @@ class BeamlineStateManager(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "format_list_bulleted"
USER_ACCESS = ["clear_filters", "state_summary", "remove", "attach", "detach", "screenshot"]
USER_ACCESS = [
"clear_filters",
"collapse_all",
"state_summary",
"remove",
"attach",
"detach",
"screenshot",
]
def __init__(
self,
@@ -294,22 +302,28 @@ class BeamlineStateManager(BECWidget, QWidget):
clear_filters = MaterialIconAction(
"filter_alt_off", "Clear beamline state filters", filled=True, parent=self
)
collapse_all = MaterialIconAction(
"collapse_all", "Collapse all states", filled=True, parent=self
)
add_state.action.triggered.connect(self.open_add_state_dialog)
filter_states.action.triggered.connect(self.open_status_filter_dialog)
filter_devices.action.triggered.connect(self.open_device_filter_dialog)
clear_filters.action.triggered.connect(self.clear_filters)
collapse_all.action.triggered.connect(self.collapse_all)
toolbar.components.add_safe("add_state", add_state)
toolbar.components.add_safe("filter_states", filter_states)
toolbar.components.add_safe("filter_devices", filter_devices)
toolbar.components.add_safe("clear_filters", clear_filters)
toolbar.components.add_safe("collapse_all", collapse_all)
bundle = ToolbarBundle("beamline_state_manager", toolbar.components)
bundle.add_action("add_state")
bundle.add_action("filter_states")
bundle.add_action("filter_devices")
bundle.add_action("clear_filters")
bundle.add_action("collapse_all")
toolbar.add_bundle(bundle)
toolbar.show_bundles(["beamline_state_manager"])
return toolbar
@@ -382,6 +396,12 @@ class BeamlineStateManager(BECWidget, QWidget):
self._hidden_expanded = False
self._apply_filters()
@SafeSlot()
def collapse_all(self) -> None:
"""Collapse the settings panel of all displayed state pills."""
for pill in self._state_pills.values():
pill.set_expanded(False)
def state_summary(self) -> dict[str, dict[str, str]]:
"""
Return the displayed beamline states with their current status and label.
@@ -410,7 +410,14 @@ class BeamlineStatePill(BECWidget, QWidget):
return self._expanded
def set_expanded(self, expanded: bool) -> None:
"""Set the editable settings panel expanded state."""
"""
Set the editable settings panel expanded state.
The settings form is built on demand when the panel expands and released again on
collapse, so collapsed pills do not keep live device/signal widgets and their BEC
subscriptions around. Unsaved edits are discarded on collapse.
"""
expanded = bool(expanded)
if expanded == self._expanded:
return
@@ -418,6 +425,8 @@ class BeamlineStatePill(BECWidget, QWidget):
self._ensure_settings_form_current()
self._expanded = expanded
self._settings.setVisible(expanded)
if not expanded:
self._release_config_form()
self._apply_visual_state()
self.row_height_changed.emit()
@@ -438,6 +447,18 @@ class BeamlineStatePill(BECWidget, QWidget):
self.mark_current_settings_clean()
return self._ensure_config_form()
def _release_config_form(self) -> None:
if self._config_form is None:
return
self._config_form_host.removeWidget(self._config_form)
self._config_form.cleanup()
self._config_form.setParent(None)
self._config_form.deleteLater()
self._config_form = None
self._settings_baseline = {}
self._settings_form_stale = True
self._update_settings_dirty_state()
def _populate_settings(self) -> None:
self._populating_settings = True
try:
@@ -132,6 +132,44 @@ def test_beamline_state_pill_reverts_changed_settings(qtbot, mocked_client):
)
def test_beamline_state_pill_releases_form_on_collapse(qtbot, mocked_client):
limits_pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client)
limits_pill.set_state_config(_limits_state())
limits_pill.set_expanded(True)
device_widget = limits_pill._config_form.input_widget("device")
limits_pill.set_expanded(False)
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
assert limits_pill._config_form is None
assert not shiboken6.isValid(device_widget)
limits_pill.set_expanded(True)
assert limits_pill._config_form is not None
assert limits_pill._config_form.input_widget("high_limit").value() == 10.0
assert not limits_pill._update_button.isEnabled()
def test_beamline_state_pill_collapse_discards_unsaved_edits(qtbot, mocked_client):
limits_pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client)
limits_pill.set_state_config(_limits_state())
limits_pill.set_expanded(True)
limits_pill._config_form.input_widget("high_limit").setValue(20.0)
assert limits_pill._update_button.isEnabled()
limits_pill.set_expanded(False)
assert limits_pill._config_form is None
limits_pill.set_expanded(True)
assert limits_pill._config_form.input_widget("high_limit").value() == 10.0
assert not limits_pill._update_button.isEnabled()
def test_beamline_state_pill_does_not_override_themed_input_controls(qtbot, mocked_client):
limits_pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client)
limits_pill.set_state_config(_limits_state())
@@ -358,6 +396,23 @@ def test_beamline_state_manager_filters_devices(qtbot, mocked_client, monkeypatc
assert captured["parent"] is beamline_state_manager
def test_beamline_state_manager_collapse_all(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
beamline_state_manager.update_available_states(
{"states": [_limits_state(), _state("shutter_open", "ShutterState", {"device": "samy"})]},
{},
)
for pill in beamline_state_manager._state_pills.values():
pill.set_expanded(True)
assert all(pill.is_expanded() for pill in beamline_state_manager._state_pills.values())
collapse_action = beamline_state_manager._toolbar.components.get_action("collapse_all")
collapse_action.action.trigger()
assert not any(pill.is_expanded() for pill in beamline_state_manager._state_pills.values())
def test_beamline_state_manager_backend_echo_repopulates_expanded_pill(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
beamline_state_manager.update_available_states({"states": [_limits_state()]}, {})