wip adding dialog adjusted

This commit is contained in:
2026-05-29 16:02:57 +02:00
parent 0883719c4e
commit 67aa66edc9
2 changed files with 143 additions and 44 deletions
@@ -15,7 +15,6 @@ from qtpy.QtWidgets import (
QComboBox,
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
QFormLayout,
QGroupBox,
QHBoxLayout,
@@ -37,6 +36,7 @@ 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
from bec_widgets.widgets.utility.spinbox.decimal_spinbox import BECSpinBox
class BeamlineStatePill(BECWidget, QWidget):
@@ -331,11 +331,11 @@ class AddBeamlineStateDialog(QDialog):
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._low_limit = BECSpinBox(self)
self._high_limit = BECSpinBox(self)
self._tolerance = BECSpinBox(self)
self._device.device_selected.connect(self._on_valid_device_selected)
self._device.device_reset.connect(lambda: self._signal.set_device(None))
self._device.device_reset.connect(self._on_device_reset)
for spin_box in (self._low_limit, self._high_limit):
spin_box.setRange(-1_000_000_000, 1_000_000_000)
@@ -345,6 +345,10 @@ class AddBeamlineStateDialog(QDialog):
self._tolerance.setRange(0.0, 1_000_000_000)
self._tolerance.setDecimals(6)
self._tolerance.setValue(0.1)
for field in self._input_fields():
field.setFixedWidth(280)
for spin_box in (self._low_limit, self._high_limit, self._tolerance):
spin_box.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self._form = QFormLayout()
self._form.addRow("State type", self._type_combo)
@@ -367,6 +371,7 @@ class AddBeamlineStateDialog(QDialog):
layout.addWidget(self._buttons)
self.setLayout(layout)
self._update_field_visibility()
self.setFixedSize(self.sizeHint().expandedTo(self.minimumSizeHint()))
def config(self) -> bl_states.DeviceStateConfig | bl_states.DeviceWithinLimitsStateConfig:
state_type = self._type_combo.currentData()
@@ -408,16 +413,16 @@ class AddBeamlineStateDialog(QDialog):
if self._cleaned_up:
return
self._cleaned_up = True
self._device.cleanup()
self._signal.cleanup()
self._device.device_selected.disconnect(self._on_valid_device_selected)
def done(self, result: int) -> None:
try:
self.cleanup()
finally:
super().done(result)
self._device.close()
self._device.deleteLater()
self._signal.close()
self._signal.deleteLater()
def _on_valid_device_selected(self, device: str) -> None:
if self._cleaned_up:
return
self._signal.set_device(device)
current_name = self._name.text().strip()
if current_name and current_name != self._auto_generated_name:
@@ -426,6 +431,11 @@ class AddBeamlineStateDialog(QDialog):
self._auto_generated_name = generated_name
self._name.setText(generated_name)
def _on_device_reset(self) -> None:
if self._cleaned_up:
return
self._signal.set_device(None)
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):
@@ -480,18 +490,25 @@ class AddBeamlineStateDialog(QDialog):
return "limits"
return "state"
def _input_fields(self) -> tuple[QWidget, ...]:
return (
self._type_combo,
self._name,
self._title,
self._device,
self._signal,
self._low_limit,
self._high_limit,
self._tolerance,
)
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:
class StatusFilterDialog(QDialog):
"""Dialog for selecting visible beamline state statuses."""
def __init__(self, selected_statuses: set[str] | None, parent: QWidget | None = None) -> None:
super().__init__(parent=parent)
self.setWindowTitle("Filter Beamline States")
self.setWindowTitle("Filter Beamline State Status")
self._checkboxes: dict[str, QCheckBox] = {}
controls = QHBoxLayout()
@@ -504,15 +521,14 @@ class StateFilterDialog(QDialog):
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
for status, label in BeamlineStatePill._STATUS_LABELS.items():
checkbox = QCheckBox(label, self)
checkbox.setChecked(selected_statuses is None or status in selected_statuses)
self._checkboxes[status] = checkbox
list_layout.addWidget(checkbox)
list_layout.addStretch(1)
box = QGroupBox("Displayed states", self)
box = QGroupBox("Displayed status", self)
box.setLayout(list_layout)
buttons = QDialogButtonBox(
@@ -527,8 +543,8 @@ class StateFilterDialog(QDialog):
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()}
def selected_statuses(self) -> set[str] | None:
selected = {status for status, checkbox in self._checkboxes.items() if checkbox.isChecked()}
if selected == set(self._checkboxes):
return None
return selected
@@ -613,12 +629,14 @@ class BeamlineStateManager(BECWidget, QWidget):
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_statuses: 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.\n Add new state from toolbar or CLI.", 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()
@@ -667,7 +685,7 @@ class BeamlineStateManager(BECWidget, QWidget):
add_state = MaterialIconAction("add", "Add beamline state", filled=True, parent=self)
filter_states = MaterialIconAction(
"filter_alt", "Filter displayed states", filled=True, parent=self
"filter_alt", "Filter displayed state status", filled=True, parent=self
)
filter_devices = MaterialIconAction(
"devices", "Filter displayed devices", filled=True, parent=self
@@ -680,7 +698,7 @@ class BeamlineStateManager(BECWidget, QWidget):
)
add_state.action.triggered.connect(self.open_add_state_dialog)
filter_states.action.triggered.connect(self.open_state_filter_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)
refresh.action.triggered.connect(self.refresh_states)
@@ -721,7 +739,16 @@ class BeamlineStateManager(BECWidget, QWidget):
@Slot()
def open_add_state_dialog(self) -> None:
dialog = AddBeamlineStateDialog(self, client=self.client)
if dialog.exec() != QDialog.Accepted:
config = None
try:
accepted = dialog.exec() == QDialog.Accepted
if accepted:
config = dialog.config_result
finally:
dialog.cleanup()
dialog.deleteLater()
if config is None:
return
beamline_states = getattr(self.client, "beamline_states", None)
if beamline_states is None:
@@ -730,16 +757,16 @@ class BeamlineStateManager(BECWidget, QWidget):
)
return
try:
beamline_states.add(dialog.config_result)
beamline_states.add(config)
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)
def open_status_filter_dialog(self) -> None:
dialog = StatusFilterDialog(self._selected_statuses, self)
if dialog.exec() != QDialog.Accepted:
return
self._selected_state_names = dialog.selected_state_names()
self._selected_statuses = dialog.selected_statuses()
self._apply_filters()
@Slot()
@@ -755,7 +782,7 @@ class BeamlineStateManager(BECWidget, QWidget):
@Slot()
def clear_filters(self) -> None:
self._selected_state_names = None
self._selected_statuses = None
self._selected_devices = None
self._device_filter_text = ""
self._hidden_expanded = False
@@ -800,10 +827,15 @@ class BeamlineStateManager(BECWidget, QWidget):
pill = BeamlineStatePill(
parent=self._content, state_name=name, title=title, client=self.client
)
pill.state_changed.connect(self._on_pill_state_changed)
self._state_pills[name] = pill
def _remove_pill(self, name: str) -> None:
pill = self._state_pills.pop(name)
try:
pill.state_changed.disconnect(self._on_pill_state_changed)
except RuntimeError:
pass
pill.cleanup()
self._content_layout.removeWidget(pill)
self._hidden_content_layout.removeWidget(pill)
@@ -834,7 +866,10 @@ class BeamlineStateManager(BECWidget, QWidget):
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:
pill = self._state_pills.get(name)
if self._selected_statuses is not None and (
pill is None or pill._status not in self._selected_statuses
):
return False
device = self._state_device(self._state_configs.get(name, {}))
@@ -854,6 +889,10 @@ class BeamlineStateManager(BECWidget, QWidget):
return False
return True
def _on_pill_state_changed(self, _name: str, _status: str, _label: str) -> None:
if self._selected_statuses is not None:
self._apply_filters()
def _toggle_hidden_states(self, checked: bool) -> None:
self._hidden_expanded = checked
self._apply_filters()
+64 -4
View File
@@ -1,4 +1,6 @@
import shiboken6
from bec_lib import messages
from qtpy.QtCore import QCoreApplication, QEvent
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.widgets.services.beamline_states.beamline_state_pill import (
@@ -6,6 +8,7 @@ from bec_widgets.widgets.services.beamline_states.beamline_state_pill import (
BeamlineStateManager,
BeamlineStatePill,
)
from bec_widgets.widgets.utility.spinbox.decimal_spinbox import BECSpinBox
from .client_mocks import mocked_client
@@ -81,7 +84,7 @@ def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client):
assert sorted(widget._state_pills) == ["limits"]
def test_beamline_state_manager_filters_states(qtbot, mocked_client):
def test_beamline_state_manager_filters_status(qtbot, mocked_client):
widget = BeamlineStateManager(client=mocked_client)
qtbot.addWidget(widget)
@@ -107,7 +110,13 @@ def test_beamline_state_manager_filters_states(qtbot, mocked_client):
assert isinstance(widget._toolbar, ModularToolBar)
widget._selected_state_names = {"limits"}
widget._state_pills["limits"].update_state(
{"name": "limits", "status": "valid", "label": "Within limits."}, {}
)
widget._state_pills["shutter_open"].update_state(
{"name": "shutter_open", "status": "invalid", "label": "Closed."}, {}
)
widget._selected_statuses = {"valid"}
widget._apply_filters()
assert not widget._hidden_summary.isHidden()
@@ -120,6 +129,39 @@ def test_beamline_state_manager_filters_states(qtbot, mocked_client):
assert not widget._hidden_content.isHidden()
def test_beamline_state_manager_status_filter_reacts_to_state_changes(qtbot, mocked_client):
widget = BeamlineStateManager(client=mocked_client)
qtbot.addWidget(widget)
widget.update_available_states(
{
"states": [
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {"device": "samx"},
}
]
},
{},
)
widget._selected_statuses = {"valid"}
widget._state_pills["limits"].update_state(
{"name": "limits", "status": "valid", "label": "Within limits."}, {}
)
assert widget._hidden_summary.isHidden()
widget._state_pills["limits"].update_state(
{"name": "limits", "status": "invalid", "label": "Out of limits."}, {}
)
assert not widget._hidden_summary.isHidden()
assert widget._state_pills["limits"].parent() is widget._hidden_content
def test_beamline_state_manager_filters_devices(qtbot, mocked_client):
widget = BeamlineStateManager(client=mocked_client)
qtbot.addWidget(widget)
@@ -158,7 +200,6 @@ def test_add_beamline_state_dialog_uses_device_signal_widgets_and_normalizes_nam
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")
@@ -173,6 +214,9 @@ def test_add_beamline_state_dialog_uses_device_signal_widgets_and_normalizes_nam
assert config.signal == "samx"
assert config.low_limit == 0.0
assert config.high_limit == 15.0
assert isinstance(dialog._low_limit, BECSpinBox)
assert isinstance(dialog._high_limit, BECSpinBox)
assert dialog._low_limit.width() == dialog._device.width()
def test_add_beamline_state_dialog_generates_name_only_after_valid_device_selection(
@@ -181,7 +225,6 @@ def test_add_beamline_state_dialog_generates_name_only_after_valid_device_select
dialog = AddBeamlineStateDialog(client=mocked_client)
qtbot.addWidget(dialog)
dialog._type_combo.setCurrentIndex(1)
dialog._device.setCurrentText("s")
assert dialog._name.text() == ""
@@ -189,3 +232,20 @@ def test_add_beamline_state_dialog_generates_name_only_after_valid_device_select
dialog._device.set_device("samx")
assert dialog._name.text() == "samx_limits"
def test_add_beamline_state_dialog_cleanup_deletes_device_widgets(qtbot, mocked_client):
dialog = AddBeamlineStateDialog(client=mocked_client)
qtbot.addWidget(dialog)
device = dialog._device
signal = dialog._signal
dialog.reject()
assert shiboken6.isValid(device)
assert shiboken6.isValid(signal)
dialog.cleanup()
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
assert not shiboken6.isValid(device)
assert not shiboken6.isValid(signal)