mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-06-05 04:48:40 +02:00
wip adding dialog adjusted
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user