diff --git a/bec_widgets/widgets/services/beamline_states/beamline_state_pill.py b/bec_widgets/widgets/services/beamline_states/beamline_state_pill.py index 75702cb8..1422fe34 100644 --- a/bec_widgets/widgets/services/beamline_states/beamline_state_pill.py +++ b/bec_widgets/widgets/services/beamline_states/beamline_state_pill.py @@ -71,6 +71,7 @@ class BeamlineStatePill(BECWidget, QWidget): } _SETTINGS_FIELD_WIDTH = 280 _VALID_DRAG_PAYLOAD_MODES = {"config", "device"} + _VALID_CARD_BACKGROUND_MODES = {"hover", "always"} MIME_PAYLOAD = "application/x-bec-beamline-state-payload" MIME_CONFIG = "application/x-bec-beamline-state-config" MIME_NAME = "application/x-bec-beamline-state-name" @@ -100,6 +101,7 @@ class BeamlineStatePill(BECWidget, QWidget): self._flash_active = False self._expanded = False self._hovered = False + self._card_background_mode = "hover" self._drag_payload_mode = "config" self._drag_start_position: QPoint | None = None self._drag_started = False @@ -361,6 +363,31 @@ class BeamlineStatePill(BECWidget, QWidget): """Set the payload mode used for drag-and-drop.""" self.drag_payload_mode = mode + @property + def card_background_mode(self) -> str: + """ + Background mode for the pill card. + + Supported values: + ``"hover"``: idle pills are transparent; hover/expanded pills use a card background. + ``"always"``: idle pills keep the status-tinted background. + """ + return self._card_background_mode + + @card_background_mode.setter + def card_background_mode(self, mode: str) -> None: + if mode not in self._VALID_CARD_BACKGROUND_MODES: + valid_modes = ", ".join(sorted(self._VALID_CARD_BACKGROUND_MODES)) + raise ValueError( + f"Invalid card background mode '{mode}'. Expected one of: {valid_modes}." + ) + self._card_background_mode = mode + self._apply_visual_state() + + def set_card_background_mode(self, mode: str) -> None: + """Set when the pill card background should be shown.""" + self.card_background_mode = mode + def _refresh_latest_state(self) -> None: if self._state_name is None: return @@ -411,8 +438,17 @@ class BeamlineStatePill(BECWidget, QWidget): accent = colors["accent"] on_accent = colors["on_accent"] active_card = self._hovered or self._expanded - border = colors["flash_border"] if self._flash_active else colors["border"] - background = colors["flash_background"] if self._flash_active else colors["background"] + show_idle_background = self._card_background_mode == "always" or self._flash_active + border = ( + colors["flash_border"] + if self._flash_active + else colors["border"] if show_idle_background else "transparent" + ) + background = ( + colors["flash_background"] + if self._flash_active + else colors["background"] if show_idle_background else "transparent" + ) if active_card: background = ( "qlineargradient(" @@ -1109,6 +1145,8 @@ class BeamlineStateManager(BECWidget, QWidget): USER_ACCESS = [ "drag_payload_mode", "set_drag_payload_mode", + "card_background_mode", + "set_card_background_mode", "refresh_states", "clear_filters", "remove", @@ -1124,6 +1162,7 @@ class BeamlineStateManager(BECWidget, QWidget): config: ConnectionConfig | None = None, gui_id: str | None = None, drag_payload_mode: str = "config", + card_background_mode: str = "hover", **kwargs, ) -> None: super().__init__( @@ -1137,7 +1176,9 @@ class BeamlineStateManager(BECWidget, QWidget): self._device_filter_text = "" self._hidden_expanded = False self._drag_payload_mode = "config" + self._card_background_mode = "hover" self.drag_payload_mode = drag_payload_mode + self.card_background_mode = card_background_mode self._empty_label = QLabel( "No beamline states available.\n Add new state from toolbar or CLI.", self @@ -1209,6 +1250,32 @@ class BeamlineStateManager(BECWidget, QWidget): """Set the payload mode used for drag-and-drop.""" self.drag_payload_mode = mode + @property + def card_background_mode(self) -> str: + """ + Background mode used by pills in this manager. + + Supported values: + ``"hover"``: idle pills are transparent; hover/expanded pills use a card background. + ``"always"``: idle pills keep the status-tinted background. + """ + return self._card_background_mode + + @card_background_mode.setter + def card_background_mode(self, mode: str) -> None: + if mode not in BeamlineStatePill._VALID_CARD_BACKGROUND_MODES: + valid_modes = ", ".join(sorted(BeamlineStatePill._VALID_CARD_BACKGROUND_MODES)) + raise ValueError( + f"Invalid card background mode '{mode}'. Expected one of: {valid_modes}." + ) + self._card_background_mode = mode + for pill in self._state_pills.values(): + pill.card_background_mode = mode + + def set_card_background_mode(self, mode: str) -> None: + """Set when pill card backgrounds should be shown.""" + self.card_background_mode = mode + def _create_toolbar(self) -> ModularToolBar: toolbar = ModularToolBar(parent=self) @@ -1358,6 +1425,7 @@ class BeamlineStateManager(BECWidget, QWidget): parent=self._content, state_name=name, title=title, client=self.client ) pill.drag_payload_mode = self._drag_payload_mode + pill.card_background_mode = self._card_background_mode pill.set_state_config(state_config) pill.state_changed.connect(self._on_pill_state_changed) pill.update_requested.connect(self._update_state_parameters) @@ -1587,6 +1655,12 @@ if __name__ == "__main__": # pragma: no cover mode_combo.currentIndexChanged.connect( lambda: setattr(manager, "drag_payload_mode", mode_combo.currentData()) ) + background_combo = QComboBox(window) + background_combo.addItem("Card on hover", "hover") + background_combo.addItem("Card always", "always") + background_combo.currentIndexChanged.connect( + lambda: setattr(manager, "card_background_mode", background_combo.currentData()) + ) drop_list = BeamlineStateDropList(window) drop_list.setMinimumWidth(260) @@ -1594,6 +1668,8 @@ if __name__ == "__main__": # pragma: no cover payload_row = QHBoxLayout() payload_row.addWidget(QLabel("Drag payload", window)) payload_row.addWidget(mode_combo) + payload_row.addWidget(QLabel("Card background", window)) + payload_row.addWidget(background_combo) payload_row.addStretch(1) layout.addLayout(payload_row) diff --git a/tests/unit_tests/test_beamline_state_pill.py b/tests/unit_tests/test_beamline_state_pill.py index fb19c131..ba983a7c 100644 --- a/tests/unit_tests/test_beamline_state_pill.py +++ b/tests/unit_tests/test_beamline_state_pill.py @@ -97,6 +97,7 @@ def test_beamline_state_pill_uses_card_style_when_expanded(qtbot, mocked_client) qtbot.addWidget(widget) assert "qlineargradient" not in widget.styleSheet() + assert "#BeamlineStatePill {background: transparent" in widget.styleSheet() widget._toggle_expanded() @@ -104,6 +105,18 @@ def test_beamline_state_pill_uses_card_style_when_expanded(qtbot, mocked_client) assert widget._shadow.isEnabled() +def test_beamline_state_pill_can_keep_idle_background(qtbot, mocked_client): + widget = BeamlineStatePill(state_name="limits", title="Limits", client=mocked_client) + qtbot.addWidget(widget) + + assert "#BeamlineStatePill {background: transparent" in widget.styleSheet() + + widget.card_background_mode = "always" + + assert "#BeamlineStatePill {background: transparent" not in widget.styleSheet() + assert "qlineargradient" not in widget.styleSheet() + + def test_beamline_state_pill_uses_card_style_when_hovered(qtbot, mocked_client): widget = BeamlineStatePill(state_name="limits", title="Limits", client=mocked_client) qtbot.addWidget(widget) @@ -208,6 +221,31 @@ def test_beamline_state_manager_propagates_drag_payload_mode(qtbot, mocked_clien assert widget._state_pills["limits"].drag_payload_mode == "config" +def test_beamline_state_manager_propagates_card_background_mode(qtbot, mocked_client): + widget = BeamlineStateManager(client=mocked_client, card_background_mode="always") + qtbot.addWidget(widget) + + widget.update_available_states( + { + "states": [ + { + "name": "limits", + "title": "Limits", + "state_type": "DeviceWithinLimitsState", + "parameters": {"device": "samx"}, + } + ] + }, + {}, + ) + + assert widget._state_pills["limits"].card_background_mode == "always" + + widget.card_background_mode = "hover" + + assert widget._state_pills["limits"].card_background_mode == "hover" + + def test_beamline_state_manager_filters_status(qtbot, mocked_client): widget = BeamlineStateManager(client=mocked_client) qtbot.addWidget(widget)