mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-06-05 12:58:40 +02:00
wip safeslot safeproperty migration
This commit is contained in:
@@ -8,7 +8,7 @@ from typing import Any
|
||||
from bec_lib import bl_states
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import QByteArray, QEvent, QMimeData, QPoint, Qt, QTimer, Signal, Slot
|
||||
from qtpy.QtCore import QByteArray, QEvent, QMimeData, QPoint, Qt, QTimer, Signal
|
||||
from qtpy.QtGui import QColor, QCursor, QDrag, QPalette
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -33,6 +33,7 @@ from qtpy.QtWidgets import (
|
||||
|
||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||
@@ -41,6 +42,12 @@ from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox im
|
||||
from bec_widgets.widgets.utility.spinbox.decimal_spinbox import BECSpinBox
|
||||
|
||||
|
||||
def _coerce_bool(value: Any) -> bool:
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
return bool(value)
|
||||
|
||||
|
||||
class BeamlineStatePill(BECWidget, QWidget):
|
||||
"""
|
||||
Compact widget showing one BEC beamline state.
|
||||
@@ -71,7 +78,6 @@ 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"
|
||||
@@ -101,7 +107,7 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
self._flash_active = False
|
||||
self._expanded = False
|
||||
self._hovered = False
|
||||
self._card_background_mode = "hover"
|
||||
self._idle_card_background = False
|
||||
self._drag_payload_mode = "config"
|
||||
self._drag_start_position: QPoint | None = None
|
||||
self._drag_started = False
|
||||
@@ -301,11 +307,15 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
QTimer.singleShot(0, self._sync_hover_state)
|
||||
super().leaveEvent(event)
|
||||
|
||||
@property
|
||||
@SafeProperty(str, default=None)
|
||||
def state_name(self) -> str | None:
|
||||
"""Name of the BEC beamline state displayed by this pill."""
|
||||
return self._state_name
|
||||
|
||||
@state_name.setter
|
||||
def state_name(self, state_name: str | None) -> None:
|
||||
self.set_state_name(state_name)
|
||||
|
||||
def set_state_name(self, state_name: str | None, title: str | None = None) -> None:
|
||||
"""
|
||||
Set the BEC beamline state this pill displays.
|
||||
@@ -341,7 +351,7 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
self._state_config = state_config
|
||||
self._populate_settings()
|
||||
|
||||
@property
|
||||
@SafeProperty(str)
|
||||
def drag_payload_mode(self) -> str:
|
||||
"""
|
||||
Payload mode used when dragging this pill.
|
||||
@@ -363,30 +373,21 @@ 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:
|
||||
@SafeProperty(bool, default=False)
|
||||
def idle_card_background(self) -> bool:
|
||||
"""
|
||||
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.
|
||||
Whether idle collapsed pills keep the status-tinted card background.
|
||||
"""
|
||||
return self._card_background_mode
|
||||
return self._idle_card_background
|
||||
|
||||
@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
|
||||
@idle_card_background.setter
|
||||
def idle_card_background(self, enabled: bool) -> None:
|
||||
self._idle_card_background = _coerce_bool(enabled)
|
||||
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 set_idle_card_background(self, enabled: bool) -> None:
|
||||
"""Set whether idle collapsed pills keep the status-tinted card background."""
|
||||
self.idle_card_background = enabled
|
||||
|
||||
def _refresh_latest_state(self) -> None:
|
||||
if self._state_name is None:
|
||||
@@ -401,7 +402,7 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
if isinstance(content, dict):
|
||||
self.update_state(content, getattr(data, "metadata", {}))
|
||||
|
||||
@Slot(dict, dict)
|
||||
@SafeSlot(dict, dict)
|
||||
def update_state(
|
||||
self, content: dict[str, Any], _metadata: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
@@ -418,7 +419,7 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
self._set_visual_state(status, label, flash=status_changed)
|
||||
self.state_changed.emit(self._state_name or str(name or ""), status, label)
|
||||
|
||||
@Slot(str)
|
||||
@SafeSlot(str)
|
||||
def apply_theme(self, _theme: str) -> None:
|
||||
self._apply_visual_state()
|
||||
|
||||
@@ -438,7 +439,7 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
accent = colors["accent"]
|
||||
on_accent = colors["on_accent"]
|
||||
active_card = self._hovered or self._expanded
|
||||
show_idle_background = self._card_background_mode == "always" or self._flash_active
|
||||
show_idle_background = self._idle_card_background or self._flash_active
|
||||
border = (
|
||||
colors["flash_border"]
|
||||
if self._flash_active
|
||||
@@ -517,6 +518,7 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
"}"
|
||||
)
|
||||
|
||||
@SafeSlot()
|
||||
def _toggle_expanded(self) -> None:
|
||||
self._expanded = not self._expanded
|
||||
self._settings.setVisible(self._expanded)
|
||||
@@ -528,6 +530,7 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
self._hovered = hovered
|
||||
self._apply_visual_state()
|
||||
|
||||
@SafeSlot()
|
||||
def _sync_hover_state(self) -> None:
|
||||
inside = self.rect().contains(self.mapFromGlobal(QCursor.pos()))
|
||||
self._set_hovered(inside)
|
||||
@@ -586,6 +589,7 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
)
|
||||
return params
|
||||
|
||||
@SafeSlot()
|
||||
def _emit_update_requested(self) -> None:
|
||||
if self._state_name is None:
|
||||
return
|
||||
@@ -596,6 +600,7 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
return
|
||||
self.update_requested.emit(self._state_name, parameters)
|
||||
|
||||
@SafeSlot()
|
||||
def _emit_remove_requested(self) -> None:
|
||||
if self._state_name is None:
|
||||
return
|
||||
@@ -655,9 +660,11 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
return event.position().toPoint()
|
||||
return event.pos()
|
||||
|
||||
@SafeSlot(str)
|
||||
def _on_settings_device_selected(self, device: str) -> None:
|
||||
self._signal_edit.set_device(device)
|
||||
|
||||
@SafeSlot()
|
||||
def _on_settings_device_reset(self) -> None:
|
||||
self._signal_edit.set_device(None)
|
||||
|
||||
@@ -757,6 +764,7 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
if label is not None:
|
||||
label.setVisible(visible)
|
||||
|
||||
@SafeSlot()
|
||||
def _clear_state_flash(self) -> None:
|
||||
self._flash_active = False
|
||||
self._apply_visual_state()
|
||||
@@ -949,6 +957,7 @@ class AddBeamlineStateDialog(QDialog):
|
||||
self._signal.close()
|
||||
self._signal.deleteLater()
|
||||
|
||||
@SafeSlot(str)
|
||||
def _on_valid_device_selected(self, device: str) -> None:
|
||||
if self._cleaned_up:
|
||||
return
|
||||
@@ -960,12 +969,14 @@ class AddBeamlineStateDialog(QDialog):
|
||||
self._auto_generated_name = generated_name
|
||||
self._name.setText(generated_name)
|
||||
|
||||
@SafeSlot()
|
||||
def _on_device_reset(self) -> None:
|
||||
if self._cleaned_up:
|
||||
return
|
||||
self._signal.set_device(None)
|
||||
|
||||
def _update_field_visibility(self) -> None:
|
||||
@SafeSlot(int)
|
||||
def _update_field_visibility(self, _index: int = 0) -> None:
|
||||
show_limits = self._type_combo.currentData() == "device_within_limits"
|
||||
for widget in (self._low_limit, self._high_limit, self._tolerance):
|
||||
widget.setVisible(show_limits)
|
||||
@@ -1145,8 +1156,8 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
USER_ACCESS = [
|
||||
"drag_payload_mode",
|
||||
"set_drag_payload_mode",
|
||||
"card_background_mode",
|
||||
"set_card_background_mode",
|
||||
"idle_card_background",
|
||||
"set_idle_card_background",
|
||||
"refresh_states",
|
||||
"clear_filters",
|
||||
"remove",
|
||||
@@ -1162,7 +1173,7 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
config: ConnectionConfig | None = None,
|
||||
gui_id: str | None = None,
|
||||
drag_payload_mode: str = "config",
|
||||
card_background_mode: str = "hover",
|
||||
idle_card_background: bool = False,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
@@ -1176,9 +1187,9 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
self._device_filter_text = ""
|
||||
self._hidden_expanded = False
|
||||
self._drag_payload_mode = "config"
|
||||
self._card_background_mode = "hover"
|
||||
self._idle_card_background = False
|
||||
self.drag_payload_mode = drag_payload_mode
|
||||
self.card_background_mode = card_background_mode
|
||||
self.idle_card_background = idle_card_background
|
||||
|
||||
self._empty_label = QLabel(
|
||||
"No beamline states available.\n Add new state from toolbar or CLI.", self
|
||||
@@ -1226,7 +1237,7 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
self.refresh_states()
|
||||
self._refresh_hidden_summary()
|
||||
|
||||
@property
|
||||
@SafeProperty(str)
|
||||
def drag_payload_mode(self) -> str:
|
||||
"""
|
||||
Payload mode used by pills dragged from this manager.
|
||||
@@ -1250,31 +1261,22 @@ 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:
|
||||
@SafeProperty(bool, default=False)
|
||||
def idle_card_background(self) -> bool:
|
||||
"""
|
||||
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.
|
||||
Whether idle collapsed pills keep the status-tinted card background.
|
||||
"""
|
||||
return self._card_background_mode
|
||||
return self._idle_card_background
|
||||
|
||||
@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
|
||||
@idle_card_background.setter
|
||||
def idle_card_background(self, enabled: bool) -> None:
|
||||
self._idle_card_background = _coerce_bool(enabled)
|
||||
for pill in self._state_pills.values():
|
||||
pill.card_background_mode = mode
|
||||
pill.idle_card_background = self._idle_card_background
|
||||
|
||||
def set_card_background_mode(self, mode: str) -> None:
|
||||
"""Set when pill card backgrounds should be shown."""
|
||||
self.card_background_mode = mode
|
||||
def set_idle_card_background(self, enabled: bool) -> None:
|
||||
"""Set whether idle collapsed pills keep the status-tinted card background."""
|
||||
self.idle_card_background = enabled
|
||||
|
||||
def _create_toolbar(self) -> ModularToolBar:
|
||||
toolbar = ModularToolBar(parent=self)
|
||||
@@ -1315,7 +1317,7 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
toolbar.show_bundles(["beamline_state_manager"])
|
||||
return toolbar
|
||||
|
||||
@Slot(str)
|
||||
@SafeSlot(str)
|
||||
def apply_theme(self, _theme: str) -> None:
|
||||
colors = BeamlineStatePill._state_colors("unknown")
|
||||
self.setStyleSheet(
|
||||
@@ -1332,7 +1334,7 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
pill.apply_theme(_theme)
|
||||
self._refresh_hidden_summary()
|
||||
|
||||
@Slot()
|
||||
@SafeSlot()
|
||||
def open_add_state_dialog(self) -> None:
|
||||
dialog = AddBeamlineStateDialog(self, client=self.client)
|
||||
config = None
|
||||
@@ -1357,7 +1359,7 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "Cannot Add State", str(exc))
|
||||
|
||||
@Slot()
|
||||
@SafeSlot()
|
||||
def open_status_filter_dialog(self) -> None:
|
||||
dialog = StatusFilterDialog(self._selected_statuses, self)
|
||||
if dialog.exec() != QDialog.Accepted:
|
||||
@@ -1365,7 +1367,7 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
self._selected_statuses = dialog.selected_statuses()
|
||||
self._apply_filters()
|
||||
|
||||
@Slot()
|
||||
@SafeSlot()
|
||||
def open_device_filter_dialog(self) -> None:
|
||||
dialog = DeviceFilterDialog(
|
||||
self._available_devices(), self._selected_devices, self._device_filter_text, self
|
||||
@@ -1376,7 +1378,7 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
self._device_filter_text = dialog.filter_text()
|
||||
self._apply_filters()
|
||||
|
||||
@Slot()
|
||||
@SafeSlot()
|
||||
def clear_filters(self) -> None:
|
||||
self._selected_statuses = None
|
||||
self._selected_devices = None
|
||||
@@ -1384,6 +1386,7 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
self._hidden_expanded = False
|
||||
self._apply_filters()
|
||||
|
||||
@SafeSlot()
|
||||
def refresh_states(self) -> None:
|
||||
"""Fetch the latest cached available beamline states and update the list immediately."""
|
||||
msg_container = self.client.connector.get_last(MessageEndpoints.available_beamline_states())
|
||||
@@ -1394,7 +1397,7 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
if isinstance(content, dict):
|
||||
self.update_available_states(content, getattr(data, "metadata", {}))
|
||||
|
||||
@Slot(dict, dict)
|
||||
@SafeSlot(dict, dict)
|
||||
def update_available_states(
|
||||
self, content: dict[str, Any], _metadata: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
@@ -1425,7 +1428,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.idle_card_background = self._idle_card_background
|
||||
pill.set_state_config(state_config)
|
||||
pill.state_changed.connect(self._on_pill_state_changed)
|
||||
pill.update_requested.connect(self._update_state_parameters)
|
||||
@@ -1499,11 +1502,12 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
return False
|
||||
return True
|
||||
|
||||
@SafeSlot(str, str, str)
|
||||
def _on_pill_state_changed(self, _name: str, _status: str, _label: str) -> None:
|
||||
if self._selected_statuses is not None:
|
||||
self._apply_filters()
|
||||
|
||||
@Slot(str, dict)
|
||||
@SafeSlot(str, dict)
|
||||
def _update_state_parameters(self, state_name: str, parameters: dict[str, Any]) -> None:
|
||||
beamline_states = getattr(self.client, "beamline_states", None)
|
||||
state_client = getattr(beamline_states, state_name, None) if beamline_states else None
|
||||
@@ -1517,7 +1521,7 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "Cannot Update State", str(exc))
|
||||
|
||||
@Slot(str)
|
||||
@SafeSlot(str)
|
||||
def _remove_state_requested(self, state_name: str) -> None:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
@@ -1540,6 +1544,7 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "Cannot Remove State", str(exc))
|
||||
|
||||
@SafeSlot(bool)
|
||||
def _toggle_hidden_states(self, checked: bool) -> None:
|
||||
self._hidden_expanded = checked
|
||||
self._apply_filters()
|
||||
@@ -1655,11 +1660,9 @@ 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())
|
||||
idle_background = QCheckBox("Idle card background", window)
|
||||
idle_background.toggled.connect(
|
||||
lambda checked: setattr(manager, "idle_card_background", checked)
|
||||
)
|
||||
|
||||
drop_list = BeamlineStateDropList(window)
|
||||
@@ -1668,8 +1671,7 @@ 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.addWidget(idle_background)
|
||||
payload_row.addStretch(1)
|
||||
layout.addLayout(payload_row)
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ def test_beamline_state_pill_can_keep_idle_background(qtbot, mocked_client):
|
||||
|
||||
assert "#BeamlineStatePill {background: transparent" in widget.styleSheet()
|
||||
|
||||
widget.card_background_mode = "always"
|
||||
widget.idle_card_background = True
|
||||
|
||||
assert "#BeamlineStatePill {background: transparent" not in widget.styleSheet()
|
||||
assert "qlineargradient" not in widget.styleSheet()
|
||||
@@ -221,8 +221,8 @@ 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")
|
||||
def test_beamline_state_manager_propagates_idle_card_background(qtbot, mocked_client):
|
||||
widget = BeamlineStateManager(client=mocked_client, idle_card_background=True)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
widget.update_available_states(
|
||||
@@ -239,11 +239,11 @@ def test_beamline_state_manager_propagates_card_background_mode(qtbot, mocked_cl
|
||||
{},
|
||||
)
|
||||
|
||||
assert widget._state_pills["limits"].card_background_mode == "always"
|
||||
assert widget._state_pills["limits"].idle_card_background is True
|
||||
|
||||
widget.card_background_mode = "hover"
|
||||
widget.idle_card_background = False
|
||||
|
||||
assert widget._state_pills["limits"].card_background_mode == "hover"
|
||||
assert widget._state_pills["limits"].idle_card_background is False
|
||||
|
||||
|
||||
def test_beamline_state_manager_filters_status(qtbot, mocked_client):
|
||||
|
||||
Reference in New Issue
Block a user