Wip BL states adding dialog

This commit is contained in:
2026-05-29 15:39:21 +02:00
parent 328a68cc49
commit 0883719c4e
3 changed files with 624 additions and 18 deletions
@@ -1,6 +1,6 @@
from bec_widgets.widgets.services.beamline_states.beamline_state_pill import (
BeamlineStateList,
BeamlineStateManager,
BeamlineStatePill,
)
__all__ = ["BeamlineStateList", "BeamlineStatePill"]
__all__ = ["BeamlineStateManager", "BeamlineStatePill"]
@@ -1,24 +1,42 @@
from __future__ import annotations
import re
import sys
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.QtGui import QColor, QPalette
from qtpy.QtWidgets import (
QApplication,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
QFormLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QMessageBox,
QPushButton,
QScrollArea,
QSizePolicy,
QToolButton,
QVBoxLayout,
QWidget,
)
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.toolbars.actions import MaterialIconAction
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
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
class BeamlineStatePill(BECWidget, QWidget):
@@ -291,17 +309,295 @@ class BeamlineStatePill(BECWidget, QWidget):
super().cleanup()
class BeamlineStateList(BECWidget, QWidget):
"""
Widget displaying all BEC beamline states as a vertical list of pills.
class AddBeamlineStateDialog(QDialog):
"""Dialog for creating supported beamline state configurations."""
The list subscribes to ``MessageEndpoints.available_beamline_states()`` and creates, updates,
or removes child ``BeamlineStatePill`` widgets as the set of configured states changes.
def __init__(self, parent: QWidget | None = None, client=None) -> None:
super().__init__(parent=parent)
self.setWindowTitle("Add Beamline State")
self._cleaned_up = False
self._config: (
bl_states.DeviceStateConfig | bl_states.DeviceWithinLimitsStateConfig | None
) = None
self._auto_generated_name: str | None = None
self._type_combo = QComboBox(self)
self._type_combo.addItem("Device within limits", "device_within_limits")
self._type_combo.addItem("Shutter", "shutter")
self._type_combo.currentIndexChanged.connect(self._update_field_visibility)
self._name = QLineEdit(self)
self._name.setPlaceholderText("samx_limits")
self._title = QLineEdit(self)
self._device = DeviceComboBox(parent=self, client=client)
self._signal = SignalComboBox(parent=self, client=client, require_device=True)
self._low_limit = QDoubleSpinBox(self)
self._high_limit = QDoubleSpinBox(self)
self._tolerance = QDoubleSpinBox(self)
self._device.device_selected.connect(self._on_valid_device_selected)
self._device.device_reset.connect(lambda: self._signal.set_device(None))
for spin_box in (self._low_limit, self._high_limit):
spin_box.setRange(-1_000_000_000, 1_000_000_000)
spin_box.setDecimals(6)
self._low_limit.setValue(0.0)
self._high_limit.setValue(10.0)
self._tolerance.setRange(0.0, 1_000_000_000)
self._tolerance.setDecimals(6)
self._tolerance.setValue(0.1)
self._form = QFormLayout()
self._form.addRow("State type", self._type_combo)
self._form.addRow("Name", self._name)
self._form.addRow("Title", self._title)
self._form.addRow("Device", self._device)
self._form.addRow("Signal", self._signal)
self._form.addRow("Low limit", self._low_limit)
self._form.addRow("High limit", self._high_limit)
self._form.addRow("Tolerance", self._tolerance)
self._buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self
)
self._buttons.accepted.connect(self.accept)
self._buttons.rejected.connect(self.reject)
layout = QVBoxLayout(self)
layout.addLayout(self._form)
layout.addWidget(self._buttons)
self.setLayout(layout)
self._update_field_visibility()
def config(self) -> bl_states.DeviceStateConfig | bl_states.DeviceWithinLimitsStateConfig:
state_type = self._type_combo.currentData()
name = self._state_name()
title = self._optional_text(self._title)
device = self._selected_device()
signal = self._selected_signal()
if state_type == "shutter":
return bl_states.DeviceStateConfig(name=name, title=title, device=device, signal=signal)
return bl_states.DeviceWithinLimitsStateConfig(
name=name,
title=title,
device=device,
signal=signal,
low_limit=self._low_limit.value(),
high_limit=self._high_limit.value(),
tolerance=self._tolerance.value(),
)
def accept(self) -> None:
try:
self._config = self.config()
except Exception as exc:
QMessageBox.warning(self, "Invalid Beamline State", str(exc))
return
super().accept()
@property
def config_result(
self,
) -> bl_states.DeviceStateConfig | bl_states.DeviceWithinLimitsStateConfig:
if self._config is None:
raise RuntimeError("Beamline state dialog was not accepted with a valid config.")
return self._config
def cleanup(self) -> None:
if self._cleaned_up:
return
self._cleaned_up = True
self._device.cleanup()
self._signal.cleanup()
def done(self, result: int) -> None:
try:
self.cleanup()
finally:
super().done(result)
def _on_valid_device_selected(self, device: str) -> None:
self._signal.set_device(device)
current_name = self._name.text().strip()
if current_name and current_name != self._auto_generated_name:
return
generated_name = f"{self._normalize_identifier(device)}_{self._state_name_suffix()}"
self._auto_generated_name = generated_name
self._name.setText(generated_name)
def _update_field_visibility(self) -> 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)
label = self._form.labelForField(widget)
if label is not None:
label.setVisible(show_limits)
def _state_name(self) -> str:
raw_name = self._name.text().strip()
if not raw_name:
raise ValueError("Name is required.")
name = self._normalize_identifier(raw_name)
self._name.setText(name)
return name
def _selected_device(self) -> str:
device = self._device.currentText().strip()
if not device:
raise ValueError("Device is required.")
if not self._device.is_valid_input:
raise ValueError(f"Device '{device}' is not available.")
return device
def _selected_signal(self) -> str | None:
signal = self._signal.get_signal_name().strip()
if not signal:
return None
if not self._signal.is_valid_input:
raise ValueError(
f"Signal '{signal}' is not available for device '{self._device.currentText()}'."
)
return signal
@staticmethod
def _optional_text(line_edit: QLineEdit) -> str | None:
value = line_edit.text().strip()
return value or None
@staticmethod
def _normalize_identifier(value: str) -> str:
name = re.sub(r"\W+", "_", value.strip())
name = re.sub(r"_+", "_", name).strip("_")
if not name:
raise ValueError("Name must contain at least one letter, number, or underscore.")
if name[0].isdigit():
name = f"state_{name}"
return name
def _state_name_suffix(self) -> str:
if self._type_combo.currentData() == "device_within_limits":
return "limits"
return "state"
class StateFilterDialog(QDialog):
"""Dialog for selecting visible beamline states."""
def __init__(
self,
state_configs: dict[str, dict[str, Any]],
selected_state_names: set[str] | None,
parent: QWidget | None = None,
) -> None:
super().__init__(parent=parent)
self.setWindowTitle("Filter Beamline States")
self._checkboxes: dict[str, QCheckBox] = {}
controls = QHBoxLayout()
select_all = QPushButton("Select all", self)
clear = QPushButton("Clear", self)
select_all.clicked.connect(lambda: self._set_all(True))
clear.clicked.connect(lambda: self._set_all(False))
controls.addWidget(select_all)
controls.addWidget(clear)
controls.addStretch(1)
list_layout = QVBoxLayout()
for name, state in sorted(state_configs.items()):
checkbox = QCheckBox(str(state.get("title") or name), self)
checkbox.setChecked(selected_state_names is None or name in selected_state_names)
checkbox.setToolTip(name)
self._checkboxes[name] = checkbox
list_layout.addWidget(checkbox)
list_layout.addStretch(1)
box = QGroupBox("Displayed states", self)
box.setLayout(list_layout)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout = QVBoxLayout(self)
layout.addLayout(controls)
layout.addWidget(box)
layout.addWidget(buttons)
self.setLayout(layout)
def selected_state_names(self) -> set[str] | None:
selected = {name for name, checkbox in self._checkboxes.items() if checkbox.isChecked()}
if selected == set(self._checkboxes):
return None
return selected
def _set_all(self, checked: bool) -> None:
for checkbox in self._checkboxes.values():
checkbox.setChecked(checked)
class DeviceFilterDialog(QDialog):
"""Dialog for filtering beamline states by configured device."""
def __init__(
self,
devices: list[str],
selected_devices: set[str] | None,
device_filter_text: str,
parent: QWidget | None = None,
) -> None:
super().__init__(parent=parent)
self.setWindowTitle("Filter Beamline State Devices")
self._checkboxes: dict[str, QCheckBox] = {}
self._device_text = QLineEdit(self)
self._device_text.setPlaceholderText("Device name or comma-separated names")
self._device_text.setText(device_filter_text)
list_layout = QVBoxLayout()
for device in devices:
checkbox = QCheckBox(device, self)
checkbox.setChecked(selected_devices is not None and device in selected_devices)
self._checkboxes[device] = checkbox
list_layout.addWidget(checkbox)
list_layout.addStretch(1)
box = QGroupBox("Known devices", self)
box.setLayout(list_layout)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout = QVBoxLayout(self)
layout.addWidget(self._device_text)
layout.addWidget(box)
layout.addWidget(buttons)
self.setLayout(layout)
def selected_devices(self) -> set[str] | None:
selected = {device for device, checkbox in self._checkboxes.items() if checkbox.isChecked()}
return selected or None
def filter_text(self) -> str:
return self._device_text.text().strip()
class BeamlineStateManager(BECWidget, QWidget):
"""
Widget displaying and managing all BEC beamline states.
The manager subscribes to ``MessageEndpoints.available_beamline_states()`` and creates,
updates, or removes child ``BeamlineStatePill`` widgets as the set of configured states changes.
"""
PLUGIN = True
ICON_NAME = "format_list_bulleted"
USER_ACCESS = ["refresh_states", "remove", "attach", "detach", "screenshot"]
USER_ACCESS = ["refresh_states", "clear_filters", "remove", "attach", "detach", "screenshot"]
def __init__(
self,
@@ -315,15 +611,22 @@ class BeamlineStateList(BECWidget, QWidget):
parent=parent, client=client, config=config, gui_id=gui_id, theme_update=True, **kwargs
)
self._state_pills: dict[str, BeamlineStatePill] = {}
self._state_configs: dict[str, dict[str, Any]] = {}
self._state_order: list[str] = []
self._selected_state_names: set[str] | None = None
self._selected_devices: set[str] | None = None
self._device_filter_text = ""
self._hidden_expanded = False
self._empty_label = QLabel("No beamline states available.", self)
self._empty_label = QLabel("No beamline states available.\n Add new state from toolbar or CLI.", self)
self._empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._toolbar = self._create_toolbar()
self._content = QWidget(self)
self._content_layout = QVBoxLayout(self._content)
self._content_layout.setContentsMargins(0, 0, 0, 0)
self._content_layout.setSpacing(6)
self._content_layout.addWidget(self._empty_label)
self._content_layout.addStretch(1)
self._scroll_area = QScrollArea(self)
@@ -334,7 +637,21 @@ class BeamlineStateList(BECWidget, QWidget):
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(6)
layout.addWidget(self._toolbar)
layout.addWidget(self._empty_label)
layout.addWidget(self._scroll_area)
self._hidden_summary = QToolButton(self)
self._hidden_summary.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
self._hidden_summary.setCheckable(True)
self._hidden_summary.clicked.connect(self._toggle_hidden_states)
layout.addWidget(self._hidden_summary)
self._hidden_content = QWidget(self)
self._hidden_content_layout = QVBoxLayout(self._hidden_content)
self._hidden_content_layout.setContentsMargins(0, 0, 0, 0)
self._hidden_content_layout.setSpacing(6)
self._hidden_content_layout.addStretch(1)
layout.addWidget(self._hidden_content)
self.setLayout(layout)
self.bec_dispatcher.connect_slot(
@@ -343,12 +660,106 @@ class BeamlineStateList(BECWidget, QWidget):
from_start=True,
)
self.refresh_states()
self._refresh_hidden_summary()
def _create_toolbar(self) -> ModularToolBar:
toolbar = ModularToolBar(parent=self)
add_state = MaterialIconAction("add", "Add beamline state", filled=True, parent=self)
filter_states = MaterialIconAction(
"filter_alt", "Filter displayed states", filled=True, parent=self
)
filter_devices = MaterialIconAction(
"devices", "Filter displayed devices", filled=True, parent=self
)
clear_filters = MaterialIconAction(
"filter_alt_off", "Clear beamline state filters", filled=True, parent=self
)
refresh = MaterialIconAction(
"restart_alt", "Refresh beamline states", filled=True, parent=self
)
add_state.action.triggered.connect(self.open_add_state_dialog)
filter_states.action.triggered.connect(self.open_state_filter_dialog)
filter_devices.action.triggered.connect(self.open_device_filter_dialog)
clear_filters.action.triggered.connect(self.clear_filters)
refresh.action.triggered.connect(self.refresh_states)
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("refresh", refresh)
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("refresh")
toolbar.add_bundle(bundle)
toolbar.show_bundles(["beamline_state_manager"])
return toolbar
@Slot(str)
def apply_theme(self, _theme: str) -> None:
self.setStyleSheet("BeamlineStateList { border: none; }")
colors = BeamlineStatePill._state_colors("unknown")
self.setStyleSheet(
"BeamlineStateManager { border: none; }"
"QToolButton#hidden_states_summary {"
f"background-color: {colors['background']};"
f"border: 1px solid {colors['border']};"
"border-radius: 6px;"
"padding: 6px;"
"text-align: left;"
"}"
)
for pill in self._state_pills.values():
pill.apply_theme(_theme)
self._refresh_hidden_summary()
@Slot()
def open_add_state_dialog(self) -> None:
dialog = AddBeamlineStateDialog(self, client=self.client)
if dialog.exec() != QDialog.Accepted:
return
beamline_states = getattr(self.client, "beamline_states", None)
if beamline_states is None:
QMessageBox.warning(
self, "Cannot Add State", "BEC client has no beamline state manager."
)
return
try:
beamline_states.add(dialog.config_result)
except Exception as exc:
QMessageBox.warning(self, "Cannot Add State", str(exc))
@Slot()
def open_state_filter_dialog(self) -> None:
dialog = StateFilterDialog(self._state_configs, self._selected_state_names, self)
if dialog.exec() != QDialog.Accepted:
return
self._selected_state_names = dialog.selected_state_names()
self._apply_filters()
@Slot()
def open_device_filter_dialog(self) -> None:
dialog = DeviceFilterDialog(
self._available_devices(), self._selected_devices, self._device_filter_text, self
)
if dialog.exec() != QDialog.Accepted:
return
self._selected_devices = dialog.selected_devices()
self._device_filter_text = dialog.filter_text()
self._apply_filters()
@Slot()
def clear_filters(self) -> None:
self._selected_state_names = None
self._selected_devices = None
self._device_filter_text = ""
self._hidden_expanded = False
self._apply_filters()
def refresh_states(self) -> None:
"""Fetch the latest cached available beamline states and update the list immediately."""
@@ -368,7 +779,9 @@ class BeamlineStateList(BECWidget, QWidget):
states = content.get("states", [])
state_configs = [self._state_config_to_dict(state) for state in states]
state_configs = [state for state in state_configs if state.get("name")]
state_names = {str(state["name"]) for state in state_configs}
self._state_configs = {str(state["name"]): state for state in state_configs}
self._state_order = [str(state["name"]) for state in state_configs]
state_names = set(self._state_order)
for removed_name in sorted(set(self._state_pills) - state_names):
self._remove_pill(removed_name)
@@ -381,22 +794,102 @@ class BeamlineStateList(BECWidget, QWidget):
continue
self._add_pill(name, title=title)
self._empty_label.setVisible(not self._state_pills)
self._apply_filters()
def _add_pill(self, name: str, title: str) -> None:
pill = BeamlineStatePill(
parent=self._content, state_name=name, title=title, client=self.client
)
self._state_pills[name] = pill
self._content_layout.insertWidget(max(self._content_layout.count() - 1, 0), pill)
def _remove_pill(self, name: str) -> None:
pill = self._state_pills.pop(name)
pill.cleanup()
self._content_layout.removeWidget(pill)
self._hidden_content_layout.removeWidget(pill)
pill.setParent(None)
pill.deleteLater()
def _apply_filters(self) -> None:
visible_names = []
hidden_names = []
for name in self._state_order:
if self._is_state_visible(name):
visible_names.append(name)
else:
hidden_names.append(name)
self._move_pills_to_layout(visible_names, self._content_layout)
self._move_pills_to_layout(hidden_names, self._hidden_content_layout)
self._empty_label.setVisible(not visible_names)
self._hidden_content.setVisible(self._hidden_expanded and bool(hidden_names))
self._refresh_hidden_summary(hidden_count=len(hidden_names))
def _move_pills_to_layout(self, names: list[str], layout: QVBoxLayout) -> None:
for name in names:
pill = self._state_pills[name]
self._content_layout.removeWidget(pill)
self._hidden_content_layout.removeWidget(pill)
layout.insertWidget(max(layout.count() - 1, 0), pill)
pill.setVisible(True)
def _is_state_visible(self, name: str) -> bool:
if self._selected_state_names is not None and name not in self._selected_state_names:
return False
device = self._state_device(self._state_configs.get(name, {}))
if self._selected_devices is not None and device not in self._selected_devices:
return False
tokens = [
token.strip().casefold()
for token in self._device_filter_text.split(",")
if token.strip()
]
if tokens:
if device is None:
return False
device_lower = device.casefold()
if not any(token in device_lower for token in tokens):
return False
return True
def _toggle_hidden_states(self, checked: bool) -> None:
self._hidden_expanded = checked
self._apply_filters()
def _refresh_hidden_summary(self, hidden_count: int | None = None) -> None:
if hidden_count is None:
hidden_count = sum(1 for name in self._state_order if not self._is_state_visible(name))
self._hidden_summary.setObjectName("hidden_states_summary")
self._hidden_summary.setVisible(hidden_count > 0)
self._hidden_summary.setChecked(self._hidden_expanded and hidden_count > 0)
icon_name = "expand_less" if self._hidden_expanded else "expand_more"
self._hidden_summary.setIcon(material_icon(icon_name, convert_to_pixmap=False))
suffix = "state is" if hidden_count == 1 else "states are"
action = "Hide" if self._hidden_expanded else "Show"
self._hidden_summary.setText(
f"{hidden_count} {suffix} hidden by filters. {action} hidden states."
)
self._hidden_content.setVisible(self._hidden_expanded and hidden_count > 0)
def _available_devices(self) -> list[str]:
devices = {
device
for state in self._state_configs.values()
if (device := self._state_device(state)) is not None
}
return sorted(devices)
@staticmethod
def _state_device(state: dict[str, Any]) -> str | None:
parameters = state.get("parameters")
if isinstance(parameters, dict):
device = parameters.get("device")
else:
device = state.get("device")
return str(device) if device else None
@staticmethod
def _state_config_to_dict(state: Any) -> dict[str, Any]:
if isinstance(state, dict):
@@ -411,6 +904,7 @@ class BeamlineStateList(BECWidget, QWidget):
)
for name in list(self._state_pills):
self._remove_pill(name)
self._toolbar.components.cleanup()
super().cleanup()
@@ -432,7 +926,7 @@ if __name__ == "__main__": # pragma: no cover
theme_row.addStretch(1)
theme_row.addWidget(DarkModeButton(parent=window))
layout.addLayout(theme_row)
layout.addWidget(BeamlineStateList(parent=window))
layout.addWidget(BeamlineStateManager(parent=window))
window.resize(420, 480)
window.show()
+115 -3
View File
@@ -1,7 +1,9 @@
from bec_lib import messages
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.widgets.services.beamline_states.beamline_state_pill import (
BeamlineStateList,
AddBeamlineStateDialog,
BeamlineStateManager,
BeamlineStatePill,
)
@@ -37,8 +39,8 @@ def test_beamline_state_pill_ignores_other_states(qtbot, mocked_client):
assert widget.toolTip() == "No state information available."
def test_beamline_state_list_adds_and_removes_pills(qtbot, mocked_client):
widget = BeamlineStateList(client=mocked_client)
def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client):
widget = BeamlineStateManager(client=mocked_client)
qtbot.addWidget(widget)
widget.update_available_states(
@@ -77,3 +79,113 @@ def test_beamline_state_list_adds_and_removes_pills(qtbot, mocked_client):
)
assert sorted(widget._state_pills) == ["limits"]
def test_beamline_state_manager_filters_states(qtbot, mocked_client):
widget = BeamlineStateManager(client=mocked_client)
qtbot.addWidget(widget)
widget.update_available_states(
{
"states": [
{
"name": "shutter_open",
"title": "Shutter",
"state_type": "ShutterState",
"parameters": {"device": "samy"},
},
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {"device": "samx"},
},
]
},
{},
)
assert isinstance(widget._toolbar, ModularToolBar)
widget._selected_state_names = {"limits"}
widget._apply_filters()
assert not widget._hidden_summary.isHidden()
assert "1 state is hidden" in widget._hidden_summary.text()
assert widget._state_pills["limits"].parent() is widget._content
assert widget._state_pills["shutter_open"].parent() is widget._hidden_content
widget._toggle_hidden_states(True)
assert not widget._hidden_content.isHidden()
def test_beamline_state_manager_filters_devices(qtbot, mocked_client):
widget = BeamlineStateManager(client=mocked_client)
qtbot.addWidget(widget)
widget.update_available_states(
{
"states": [
{
"name": "samx_limits",
"title": "samx",
"state_type": "DeviceWithinLimitsState",
"parameters": {"device": "samx"},
},
{
"name": "samy_limits",
"title": "samy",
"state_type": "DeviceWithinLimitsState",
"parameters": {"device": "samy"},
},
]
},
{},
)
widget._device_filter_text = "samx"
widget._apply_filters()
assert not widget._hidden_summary.isHidden()
assert "1 state is hidden" in widget._hidden_summary.text()
assert widget._available_devices() == ["samx", "samy"]
def test_add_beamline_state_dialog_uses_device_signal_widgets_and_normalizes_name(
qtbot, mocked_client
):
dialog = AddBeamlineStateDialog(client=mocked_client)
qtbot.addWidget(dialog)
dialog._type_combo.setCurrentIndex(1)
dialog._name.setText("samx-limits")
dialog._title.setText("samx-limits-15")
dialog._device.set_device("samx")
dialog._signal.set_signal("samx")
dialog._high_limit.setValue(15.0)
config = dialog.config()
assert config.name == "samx_limits"
assert config.title == "samx-limits-15"
assert config.device == "samx"
assert config.signal == "samx"
assert config.low_limit == 0.0
assert config.high_limit == 15.0
def test_add_beamline_state_dialog_generates_name_only_after_valid_device_selection(
qtbot, mocked_client
):
dialog = AddBeamlineStateDialog(client=mocked_client)
qtbot.addWidget(dialog)
dialog._type_combo.setCurrentIndex(1)
dialog._device.setCurrentText("s")
assert dialog._name.text() == ""
dialog._device.set_device("samx")
assert dialog._name.text() == "samx_limits"