mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-06-05 04:48:40 +02:00
wip grid design
This commit is contained in:
@@ -7,7 +7,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 Qt, QTimer, Signal, Slot
|
||||
from qtpy.QtCore import QEvent, Qt, QTimer, Signal, Slot
|
||||
from qtpy.QtGui import QColor, QPalette
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -16,6 +16,7 @@ from qtpy.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFormLayout,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
@@ -52,6 +53,8 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
USER_ACCESS = ["state_name", "set_state_name", "remove", "attach", "detach", "screenshot"]
|
||||
|
||||
state_changed = Signal(str, str, str)
|
||||
update_requested = Signal(str, dict)
|
||||
remove_requested = Signal(str)
|
||||
|
||||
_STATUS_LABELS = {
|
||||
"valid": "VALID",
|
||||
@@ -81,14 +84,20 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
)
|
||||
self._state_name: str | None = None
|
||||
self._title: str | None = None
|
||||
self._state_config: dict[str, Any] = {}
|
||||
self._status = "unknown"
|
||||
self._label = "No state information available."
|
||||
self._flash_active = False
|
||||
self._expanded = False
|
||||
|
||||
self._flash_timer = QTimer(self)
|
||||
self._flash_timer.setSingleShot(True)
|
||||
self._flash_timer.timeout.connect(self._clear_state_flash)
|
||||
|
||||
self._header = QWidget(self)
|
||||
self._header.setObjectName("beamline_state_header")
|
||||
self._header.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
self._stripe = QWidget(self)
|
||||
self._stripe.setObjectName("beamline_state_stripe")
|
||||
self._stripe.setFixedWidth(4)
|
||||
@@ -108,6 +117,11 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
self._detail_label.setObjectName("beamline_state_detail")
|
||||
self._detail_label.setTextFormat(Qt.TextFormat.PlainText)
|
||||
self._detail_label.setWordWrap(True)
|
||||
self._expand_button = QToolButton(self)
|
||||
self._expand_button.setObjectName("beamline_state_expand")
|
||||
self._expand_button.setAutoRaise(True)
|
||||
self._expand_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self._expand_button.clicked.connect(self._toggle_expanded)
|
||||
|
||||
text_layout = QVBoxLayout()
|
||||
text_layout.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -115,18 +129,128 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
text_layout.addWidget(self._name_label)
|
||||
text_layout.addWidget(self._detail_label)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 8, 12, 8)
|
||||
layout.setSpacing(10)
|
||||
layout.addWidget(self._stripe)
|
||||
layout.addWidget(self._icon_label)
|
||||
layout.addLayout(text_layout, 1)
|
||||
layout.addWidget(self._status_label, 0, Qt.AlignmentFlag.AlignRight)
|
||||
header_layout = QHBoxLayout(self._header)
|
||||
header_layout.setContentsMargins(10, 8, 12, 8)
|
||||
header_layout.setSpacing(10)
|
||||
header_layout.addWidget(self._stripe)
|
||||
header_layout.addWidget(self._icon_label)
|
||||
header_layout.addLayout(text_layout, 1)
|
||||
header_layout.addWidget(self._status_label, 0, Qt.AlignmentFlag.AlignRight)
|
||||
header_layout.addWidget(self._expand_button)
|
||||
|
||||
self._settings = QWidget(self)
|
||||
self._settings.setObjectName("beamline_state_settings")
|
||||
self._settings.setVisible(False)
|
||||
self._state_type_value = QLabel(self._settings)
|
||||
self._name_value = QLabel(self._settings)
|
||||
self._title_edit = QLineEdit(self._settings)
|
||||
self._device_edit = DeviceComboBox(parent=self._settings, client=client)
|
||||
self._signal_edit = SignalComboBox(
|
||||
parent=self._settings, client=client, require_device=True
|
||||
)
|
||||
self._low_limit_enabled, self._low_limit = self._create_optional_limit_row()
|
||||
self._high_limit_enabled, self._high_limit = self._create_optional_limit_row()
|
||||
self._tolerance = BECSpinBox(self._settings)
|
||||
self._configure_settings_spinbox(self._tolerance)
|
||||
self._device_edit.device_selected.connect(self._on_settings_device_selected)
|
||||
self._device_edit.device_reset.connect(self._on_settings_device_reset)
|
||||
self._low_limit_enabled.toggled.connect(self._low_limit.setEnabled)
|
||||
self._high_limit_enabled.toggled.connect(self._high_limit.setEnabled)
|
||||
|
||||
self._type_label = self._create_settings_label("Type")
|
||||
self._name_settings_label = self._create_settings_label("Name")
|
||||
self._title_label = self._create_settings_label("Title")
|
||||
self._device_label = self._create_settings_label("Device")
|
||||
self._signal_label = self._create_settings_label("Signal")
|
||||
self._low_limit_label = self._create_settings_label("Low limit")
|
||||
self._high_limit_label = self._create_settings_label("High limit")
|
||||
self._tolerance_label = self._create_settings_label("Tolerance")
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setContentsMargins(0, 0, 0, 0)
|
||||
button_layout.setSpacing(8)
|
||||
button_layout.addStretch(1)
|
||||
self._update_button = QPushButton("Update", self._settings)
|
||||
self._update_button.setIcon(material_icon("save", convert_to_pixmap=False))
|
||||
self._remove_button = QPushButton("Remove", self._settings)
|
||||
self._remove_button.setObjectName("beamline_state_remove_button")
|
||||
self._remove_button.setIcon(material_icon("delete", convert_to_pixmap=False))
|
||||
self._update_button.clicked.connect(self._emit_update_requested)
|
||||
self._remove_button.clicked.connect(self._emit_remove_requested)
|
||||
button_layout.addWidget(self._update_button)
|
||||
button_layout.addWidget(self._remove_button)
|
||||
|
||||
self._settings_grid = QGridLayout()
|
||||
self._settings_grid.setContentsMargins(12, 8, 12, 8)
|
||||
self._settings_grid.setHorizontalSpacing(10)
|
||||
self._settings_grid.setVerticalSpacing(8)
|
||||
self._settings_grid.addWidget(self._type_label, 0, 0)
|
||||
self._settings_grid.addWidget(self._state_type_value, 0, 1)
|
||||
self._settings_grid.addWidget(self._name_settings_label, 0, 2)
|
||||
self._settings_grid.addWidget(self._name_value, 0, 3)
|
||||
self._settings_grid.addWidget(self._title_label, 1, 0)
|
||||
self._settings_grid.addWidget(self._title_edit, 1, 1, 1, 3)
|
||||
self._settings_grid.addWidget(self._device_label, 2, 0)
|
||||
self._settings_grid.addWidget(self._device_edit, 2, 1)
|
||||
self._settings_grid.addWidget(self._signal_label, 2, 2)
|
||||
self._settings_grid.addWidget(self._signal_edit, 2, 3)
|
||||
self._settings_grid.addWidget(self._low_limit_label, 3, 0)
|
||||
self._settings_grid.addWidget(self._low_limit.parentWidget(), 3, 1)
|
||||
self._settings_grid.addWidget(self._high_limit_label, 3, 2)
|
||||
self._settings_grid.addWidget(self._high_limit.parentWidget(), 3, 3)
|
||||
self._settings_grid.addWidget(self._tolerance_label, 4, 0)
|
||||
self._settings_grid.addWidget(self._tolerance, 4, 1)
|
||||
self._settings_grid.addLayout(button_layout, 4, 2, 1, 2)
|
||||
self._settings_grid.setColumnStretch(1, 1)
|
||||
self._settings_grid.setColumnStretch(3, 1)
|
||||
|
||||
self._limit_widgets = (
|
||||
self._low_limit_label,
|
||||
self._low_limit.parentWidget(),
|
||||
self._high_limit_label,
|
||||
self._high_limit.parentWidget(),
|
||||
self._tolerance_label,
|
||||
self._tolerance,
|
||||
)
|
||||
|
||||
settings_layout = QVBoxLayout(self._settings)
|
||||
settings_layout.setContentsMargins(0, 0, 0, 0)
|
||||
settings_layout.setSpacing(0)
|
||||
settings_layout.addLayout(self._settings_grid)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(self._header)
|
||||
layout.addWidget(self._settings)
|
||||
self.setLayout(layout)
|
||||
|
||||
for widget in (
|
||||
self._header,
|
||||
self._stripe,
|
||||
self._icon_label,
|
||||
self._name_label,
|
||||
self._status_label,
|
||||
self._detail_label,
|
||||
):
|
||||
widget.installEventFilter(self)
|
||||
|
||||
self.setMinimumHeight(58)
|
||||
self.set_state_name(state_name, title=title)
|
||||
|
||||
def eventFilter(self, watched: object, event: QEvent) -> bool: # noqa: N802
|
||||
if event.type() == QEvent.Type.MouseButtonRelease and watched in {
|
||||
self._header,
|
||||
self._stripe,
|
||||
self._icon_label,
|
||||
self._name_label,
|
||||
self._status_label,
|
||||
self._detail_label,
|
||||
}:
|
||||
self._toggle_expanded()
|
||||
return True
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
@property
|
||||
def state_name(self) -> str | None:
|
||||
"""Name of the BEC beamline state displayed by this pill."""
|
||||
@@ -162,6 +286,11 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
self.update_state, MessageEndpoints.beamline_state(self._state_name)
|
||||
)
|
||||
|
||||
def set_state_config(self, state_config: dict[str, Any]) -> None:
|
||||
"""Set the editable BEC state configuration displayed by the expanded panel."""
|
||||
self._state_config = state_config
|
||||
self._populate_settings()
|
||||
|
||||
def _refresh_latest_state(self) -> None:
|
||||
if self._state_name is None:
|
||||
return
|
||||
@@ -218,6 +347,8 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
self._icon_label.setPixmap(
|
||||
material_icon(icon_name, size=(20, 20), color=on_accent, filled=True)
|
||||
)
|
||||
expand_icon = "expand_less" if self._expanded else "expand_more"
|
||||
self._expand_button.setIcon(material_icon(expand_icon, convert_to_pixmap=False))
|
||||
self._status_label.setText(self._STATUS_LABELS[self._status])
|
||||
self._detail_label.setText(self._label)
|
||||
self.setToolTip(self._label)
|
||||
@@ -248,8 +379,175 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
f"color: {colors['muted']};"
|
||||
"font-size: 11px;"
|
||||
"}"
|
||||
"QWidget#beamline_state_settings {"
|
||||
f"border-top: 1px solid {colors['border']};"
|
||||
"}"
|
||||
"QPushButton#beamline_state_remove_button {"
|
||||
"background-color: #cc181e;"
|
||||
"border: 1px solid #cc181e;"
|
||||
"color: white;"
|
||||
"border-radius: 4px;"
|
||||
"padding: 4px 10px;"
|
||||
"}"
|
||||
"QPushButton#beamline_state_remove_button:hover {"
|
||||
"background-color: #a91419;"
|
||||
"border-color: #a91419;"
|
||||
"}"
|
||||
)
|
||||
|
||||
def _toggle_expanded(self) -> None:
|
||||
self._expanded = not self._expanded
|
||||
self._settings.setVisible(self._expanded)
|
||||
self._apply_visual_state()
|
||||
|
||||
def _populate_settings(self) -> None:
|
||||
state_type = self._state_type()
|
||||
self._state_type_value.setText(state_type or "-")
|
||||
self._name_value.setText(self._state_name or "-")
|
||||
self._title_edit.setText(str(self._state_field("title") or ""))
|
||||
device = str(self._state_field("device") or "")
|
||||
signal = str(self._state_field("signal") or "")
|
||||
self._set_settings_device(device)
|
||||
self._set_settings_signal(signal)
|
||||
|
||||
show_limits = state_type == "DeviceWithinLimitsState" or any(
|
||||
self._state_field(key) is not None for key in ("low_limit", "high_limit", "tolerance")
|
||||
)
|
||||
for widget in self._limit_widgets:
|
||||
widget.setVisible(show_limits)
|
||||
|
||||
if not show_limits:
|
||||
return
|
||||
|
||||
self._set_optional_limit(
|
||||
self._low_limit_enabled, self._low_limit, self._state_field("low_limit")
|
||||
)
|
||||
self._set_optional_limit(
|
||||
self._high_limit_enabled, self._high_limit, self._state_field("high_limit")
|
||||
)
|
||||
tolerance = self._state_field("tolerance")
|
||||
self._tolerance.setValue(float(tolerance) if tolerance is not None else 0.1)
|
||||
|
||||
def edited_parameters(self) -> dict[str, Any]:
|
||||
"""Return editable parameters from the expanded settings panel."""
|
||||
device = self._device_edit.currentText().strip()
|
||||
signal = self._optional_signal()
|
||||
if not device:
|
||||
raise ValueError("Device is required.")
|
||||
|
||||
params: dict[str, Any] = {
|
||||
"title": self._optional_text(self._title_edit),
|
||||
"device": device,
|
||||
"signal": signal,
|
||||
}
|
||||
if self._state_type() == "DeviceWithinLimitsState":
|
||||
params.update(
|
||||
{
|
||||
"low_limit": (
|
||||
self._low_limit.value() if self._low_limit_enabled.isChecked() else None
|
||||
),
|
||||
"high_limit": (
|
||||
self._high_limit.value() if self._high_limit_enabled.isChecked() else None
|
||||
),
|
||||
"tolerance": self._tolerance.value(),
|
||||
}
|
||||
)
|
||||
return params
|
||||
|
||||
def _emit_update_requested(self) -> None:
|
||||
if self._state_name is None:
|
||||
return
|
||||
try:
|
||||
parameters = self.edited_parameters()
|
||||
except ValueError as exc:
|
||||
QMessageBox.warning(self, "Invalid Beamline State", str(exc))
|
||||
return
|
||||
self.update_requested.emit(self._state_name, parameters)
|
||||
|
||||
def _emit_remove_requested(self) -> None:
|
||||
if self._state_name is None:
|
||||
return
|
||||
self.remove_requested.emit(self._state_name)
|
||||
|
||||
def _state_field(self, name: str) -> Any:
|
||||
parameters = self._state_config.get("parameters")
|
||||
if isinstance(parameters, dict) and name in parameters:
|
||||
return parameters.get(name)
|
||||
return self._state_config.get(name)
|
||||
|
||||
def _state_type(self) -> str:
|
||||
return str(self._state_config.get("state_type") or self._state_field("state_type") or "")
|
||||
|
||||
def _on_settings_device_selected(self, device: str) -> None:
|
||||
self._signal_edit.set_device(device)
|
||||
|
||||
def _on_settings_device_reset(self) -> None:
|
||||
self._signal_edit.set_device(None)
|
||||
|
||||
def _set_settings_device(self, device: str) -> None:
|
||||
if not device:
|
||||
self._device_edit.setCurrentText("")
|
||||
self._signal_edit.set_device(None)
|
||||
return
|
||||
self._device_edit.set_device(device)
|
||||
if self._device_edit.currentText() != device:
|
||||
self._device_edit.setCurrentText(device)
|
||||
if self._device_edit.is_valid_input:
|
||||
self._signal_edit.set_device(device)
|
||||
|
||||
def _set_settings_signal(self, signal: str) -> None:
|
||||
if not signal:
|
||||
self._signal_edit.setCurrentText("")
|
||||
return
|
||||
self._signal_edit.set_signal(signal)
|
||||
if (
|
||||
self._signal_edit.currentText() != signal
|
||||
and self._signal_edit.get_signal_name() != signal
|
||||
):
|
||||
self._signal_edit.setCurrentText(signal)
|
||||
|
||||
@staticmethod
|
||||
def _optional_text(line_edit: QLineEdit) -> str | None:
|
||||
value = line_edit.text().strip()
|
||||
return value or None
|
||||
|
||||
def _optional_signal(self) -> str | None:
|
||||
value = self._signal_edit.get_signal_name().strip()
|
||||
return value or None
|
||||
|
||||
@staticmethod
|
||||
def _configure_settings_spinbox(spin_box: BECSpinBox) -> None:
|
||||
spin_box.setRange(-1_000_000_000, 1_000_000_000)
|
||||
spin_box.setDecimals(6)
|
||||
spin_box.setFixedWidth(140)
|
||||
|
||||
def _create_optional_limit_row(self) -> tuple[QCheckBox, BECSpinBox]:
|
||||
container = QWidget(self)
|
||||
checkbox = QCheckBox("Enabled", container)
|
||||
spin_box = BECSpinBox(container)
|
||||
self._configure_settings_spinbox(spin_box)
|
||||
layout = QHBoxLayout(container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(8)
|
||||
layout.addWidget(checkbox)
|
||||
layout.addWidget(spin_box)
|
||||
layout.addStretch(1)
|
||||
return checkbox, spin_box
|
||||
|
||||
def _create_settings_label(self, text: str) -> QLabel:
|
||||
label = QLabel(text, self._settings)
|
||||
label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
label.setTextFormat(Qt.TextFormat.PlainText)
|
||||
return label
|
||||
|
||||
@staticmethod
|
||||
def _set_optional_limit(checkbox: QCheckBox, spin_box: BECSpinBox, value: Any) -> None:
|
||||
enabled = value is not None
|
||||
checkbox.setChecked(enabled)
|
||||
spin_box.setEnabled(enabled)
|
||||
if enabled:
|
||||
spin_box.setValue(float(value))
|
||||
|
||||
def _clear_state_flash(self) -> None:
|
||||
self._flash_active = False
|
||||
self._apply_visual_state()
|
||||
@@ -413,8 +711,14 @@ class AddBeamlineStateDialog(QDialog):
|
||||
if self._cleaned_up:
|
||||
return
|
||||
self._cleaned_up = True
|
||||
self._device.device_selected.disconnect(self._on_valid_device_selected)
|
||||
|
||||
try:
|
||||
self._device.device_selected.disconnect(self._on_valid_device_selected)
|
||||
except RuntimeError:
|
||||
pass
|
||||
try:
|
||||
self._device.device_reset.disconnect(self._on_device_reset)
|
||||
except RuntimeError:
|
||||
pass
|
||||
self._device.close()
|
||||
self._device.deleteLater()
|
||||
self._signal.close()
|
||||
@@ -818,16 +1122,20 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
title = state.get("title") or name
|
||||
if name in self._state_pills:
|
||||
self._state_pills[name].set_state_name(name, title=title)
|
||||
self._state_pills[name].set_state_config(state)
|
||||
continue
|
||||
self._add_pill(name, title=title)
|
||||
self._add_pill(name, title=title, state_config=state)
|
||||
|
||||
self._apply_filters()
|
||||
|
||||
def _add_pill(self, name: str, title: str) -> None:
|
||||
def _add_pill(self, name: str, title: str, state_config: dict[str, Any]) -> None:
|
||||
pill = BeamlineStatePill(
|
||||
parent=self._content, state_name=name, title=title, client=self.client
|
||||
)
|
||||
pill.set_state_config(state_config)
|
||||
pill.state_changed.connect(self._on_pill_state_changed)
|
||||
pill.update_requested.connect(self._update_state_parameters)
|
||||
pill.remove_requested.connect(self._remove_state_requested)
|
||||
self._state_pills[name] = pill
|
||||
|
||||
def _remove_pill(self, name: str) -> None:
|
||||
@@ -836,6 +1144,14 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
pill.state_changed.disconnect(self._on_pill_state_changed)
|
||||
except RuntimeError:
|
||||
pass
|
||||
try:
|
||||
pill.update_requested.disconnect(self._update_state_parameters)
|
||||
except RuntimeError:
|
||||
pass
|
||||
try:
|
||||
pill.remove_requested.disconnect(self._remove_state_requested)
|
||||
except RuntimeError:
|
||||
pass
|
||||
pill.cleanup()
|
||||
self._content_layout.removeWidget(pill)
|
||||
self._hidden_content_layout.removeWidget(pill)
|
||||
@@ -893,6 +1209,43 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
if self._selected_statuses is not None:
|
||||
self._apply_filters()
|
||||
|
||||
@Slot(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
|
||||
if state_client is None or not hasattr(state_client, "update_parameters"):
|
||||
QMessageBox.warning(
|
||||
self, "Cannot Update State", f"Beamline state '{state_name}' is not available."
|
||||
)
|
||||
return
|
||||
try:
|
||||
state_client.update_parameters(**parameters)
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "Cannot Update State", str(exc))
|
||||
|
||||
@Slot(str)
|
||||
def _remove_state_requested(self, state_name: str) -> None:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Remove Beamline State",
|
||||
f"Remove beamline state '{state_name}'?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if reply != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
beamline_states = getattr(self.client, "beamline_states", None)
|
||||
if beamline_states is None or not hasattr(beamline_states, "delete"):
|
||||
QMessageBox.warning(
|
||||
self, "Cannot Remove State", "BEC client has no beamline state manager."
|
||||
)
|
||||
return
|
||||
try:
|
||||
beamline_states.delete(state_name)
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "Cannot Remove State", str(exc))
|
||||
|
||||
def _toggle_hidden_states(self, checked: bool) -> None:
|
||||
self._hidden_expanded = checked
|
||||
self._apply_filters()
|
||||
@@ -934,7 +1287,11 @@ class BeamlineStateManager(BECWidget, QWidget):
|
||||
if isinstance(state, dict):
|
||||
return state
|
||||
if hasattr(state, "model_dump"):
|
||||
return state.model_dump()
|
||||
state_dict = state.model_dump()
|
||||
state_type = getattr(state, "state_type", None)
|
||||
if state_type is not None:
|
||||
state_dict.setdefault("state_type", state_type)
|
||||
return state_dict
|
||||
return {"name": getattr(state, "name", None), "title": getattr(state, "title", None)}
|
||||
|
||||
def cleanup(self) -> None:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import shiboken6
|
||||
from bec_lib import messages
|
||||
from qtpy.QtCore import QCoreApplication, QEvent
|
||||
from qtpy.QtCore import QCoreApplication, QEvent, Qt
|
||||
from qtpy.QtWidgets import QMessageBox
|
||||
|
||||
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||
from bec_widgets.widgets.services.beamline_states.beamline_state_pill import (
|
||||
@@ -8,6 +9,8 @@ from bec_widgets.widgets.services.beamline_states.beamline_state_pill import (
|
||||
BeamlineStateManager,
|
||||
BeamlineStatePill,
|
||||
)
|
||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
|
||||
from bec_widgets.widgets.utility.spinbox.decimal_spinbox import BECSpinBox
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
@@ -42,6 +45,48 @@ def test_beamline_state_pill_ignores_other_states(qtbot, mocked_client):
|
||||
assert widget.toolTip() == "No state information available."
|
||||
|
||||
|
||||
def test_beamline_state_pill_expands_and_emits_updated_limits(qtbot, mocked_client):
|
||||
widget = BeamlineStatePill(state_name="limits", title="Limits", client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
widget.set_state_config(
|
||||
{
|
||||
"name": "limits",
|
||||
"title": "Limits",
|
||||
"state_type": "DeviceWithinLimitsState",
|
||||
"parameters": {
|
||||
"name": "limits",
|
||||
"title": "Limits",
|
||||
"device": "samx",
|
||||
"signal": "samx",
|
||||
"low_limit": 0.0,
|
||||
"high_limit": 10.0,
|
||||
"tolerance": 0.1,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert widget._settings.isHidden()
|
||||
|
||||
qtbot.mouseClick(widget._header, Qt.MouseButton.LeftButton)
|
||||
widget._high_limit.setValue(20.0)
|
||||
|
||||
assert not widget._settings.isHidden()
|
||||
assert isinstance(widget._device_edit, DeviceComboBox)
|
||||
assert isinstance(widget._signal_edit, SignalComboBox)
|
||||
assert widget._device_edit.currentText() == "samx"
|
||||
assert widget.edited_parameters()["high_limit"] == 20.0
|
||||
|
||||
with qtbot.waitSignal(widget.update_requested) as signal:
|
||||
widget._update_button.click()
|
||||
|
||||
assert signal.args[0] == "limits"
|
||||
assert signal.args[1]["device"] == "samx"
|
||||
assert signal.args[1]["signal"] == "samx"
|
||||
assert signal.args[1]["low_limit"] == 0.0
|
||||
assert signal.args[1]["high_limit"] == 20.0
|
||||
assert signal.args[1]["tolerance"] == 0.1
|
||||
|
||||
|
||||
def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client):
|
||||
widget = BeamlineStateManager(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
@@ -194,6 +239,51 @@ def test_beamline_state_manager_filters_devices(qtbot, mocked_client):
|
||||
assert widget._available_devices() == ["samx", "samy"]
|
||||
|
||||
|
||||
def test_beamline_state_manager_updates_state_parameters(qtbot, mocked_client):
|
||||
widget = BeamlineStateManager(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
class StateClient:
|
||||
def __init__(self):
|
||||
self.parameters = None
|
||||
|
||||
def update_parameters(self, **kwargs):
|
||||
self.parameters = kwargs
|
||||
|
||||
class StateManager:
|
||||
def __init__(self):
|
||||
self.limits = StateClient()
|
||||
|
||||
mocked_client.beamline_states = StateManager()
|
||||
widget._update_state_parameters("limits", {"low_limit": -1.0, "high_limit": 20.0})
|
||||
|
||||
assert mocked_client.beamline_states.limits.parameters == {
|
||||
"low_limit": -1.0,
|
||||
"high_limit": 20.0,
|
||||
}
|
||||
|
||||
|
||||
def test_beamline_state_manager_removes_state(qtbot, mocked_client, monkeypatch):
|
||||
widget = BeamlineStateManager(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
class StateManager:
|
||||
def __init__(self):
|
||||
self.deleted = None
|
||||
|
||||
def delete(self, state_name):
|
||||
self.deleted = state_name
|
||||
|
||||
mocked_client.beamline_states = StateManager()
|
||||
monkeypatch.setattr(
|
||||
QMessageBox, "question", lambda *args, **kwargs: QMessageBox.StandardButton.Yes
|
||||
)
|
||||
|
||||
widget._remove_state_requested("limits")
|
||||
|
||||
assert mocked_client.beamline_states.deleted == "limits"
|
||||
|
||||
|
||||
def test_add_beamline_state_dialog_uses_device_signal_widgets_and_normalizes_name(
|
||||
qtbot, mocked_client
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user