0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 11:11:49 +02:00

feat: add set form item

This commit is contained in:
2025-06-13 12:37:33 +02:00
committed by David Perl
parent 1a350c3b16
commit be73349c70
6 changed files with 71 additions and 22 deletions

View File

@ -19,7 +19,7 @@ class FakeDevice(BECDevice):
"readoutPriority": "baseline",
"deviceClass": "ophyd.Device",
"deviceConfig": {},
"deviceTags": ["user device"],
"deviceTags": {"user device"},
"enabled": enabled,
"readOnly": False,
"name": self.name,
@ -89,7 +89,7 @@ class FakePositioner(BECPositioner):
"readoutPriority": "baseline",
"deviceClass": "ophyd_devices.SimPositioner",
"deviceConfig": {"delay": 1, "tolerance": 0.01, "update_frequency": 400},
"deviceTags": ["user motors"],
"deviceTags": {"user motors"},
"enabled": enabled,
"readOnly": False,
"name": self.name,

View File

@ -102,6 +102,8 @@ class TypedForm(BECWidget, QWidget):
self._clear_grid()
for r, item in enumerate(self._items):
self._add_griditem(item, r)
gl: QGridLayout = self._form_grid.layout()
gl.setRowStretch(gl.rowCount(), 1)
def _add_griditem(self, item: FormItemSpec, row: int):
grid = self._form_grid.layout()
@ -116,9 +118,9 @@ class TypedForm(BECWidget, QWidget):
def enumerate_form_widgets(self):
"""Return a generator over the rows of the form, with the row number, the label widget (to
which the field name is attached as a property), and the entry widget"""
which the field name is attached as a property "_model_field_name"), and the entry widget"""
grid: QGridLayout = self._form_grid.layout() # type: ignore
for i in range(grid.rowCount()):
for i in range(grid.rowCount() - 1): # One extra row for stretch
yield GridRow(i, grid.itemAtPosition(i, 0).widget(), grid.itemAtPosition(i, 1).widget())
def _dict_from_grid(self) -> dict[str, DynamicFormItemType]:

View File

@ -4,13 +4,14 @@ import typing
from abc import abstractmethod
from decimal import Decimal
from types import GenericAlias, UnionType
from typing import Callable, Final, Generic, Literal, NamedTuple, OrderedDict, TypeVar, get_args
from typing import Callable, Final, Iterable, Literal, NamedTuple, OrderedDict, get_args
from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefined
from PySide6 import QtCore
from PySide6.QtWidgets import QComboBox
from qtpy.QtCore import Signal # type: ignore
from qtpy.QtWidgets import (
@ -348,14 +349,12 @@ class DictFormItem(DynamicFormItem):
class _ItemAndWidgetType(NamedTuple):
# TODO: this should be generic but not supported in 3.10
item: type[int | float | str]
widget: type
widget: type[QWidget]
default: int | float | str
class ListFormItem(DynamicFormItem):
def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
super().__init__(parent=parent, spec=spec)
self._main_widget: QListWidget
if spec.info.annotation is list:
self._types = _ItemAndWidgetType(str, QLineEdit, "")
elif isinstance(spec.info.annotation, GenericAlias):
@ -367,7 +366,9 @@ class ListFormItem(DynamicFormItem):
if args == {float} or args == {int, float}:
self._types = _ItemAndWidgetType(float, QDoubleSpinBox, 0.0)
else:
self._types = _ItemAndWidgetType(str, QLineEdit, 0)
self._types = _ItemAndWidgetType(str, QLineEdit, "")
super().__init__(parent=parent, spec=spec)
self._main_widget: QListWidget
self._data = []
def _add_main_widget(self) -> None:
@ -396,24 +397,30 @@ class ListFormItem(DynamicFormItem):
def _repop(self, data):
self._main_widget.clear()
for val in data:
self._add_item(val)
self._add_list_item(val)
def _add_item(self, val=None):
def _add_data_item(self, val=None):
val = val or self._types.default
self._data.append(val)
self._add_list_item(val)
def _add_list_item(self, val):
item = QListWidgetItem(self._main_widget)
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEditable)
item_widget = self._types.widget(parent=self)
WidgetIO.set_value(item_widget, val)
self._main_widget.setItemWidget(item, item_widget)
self._main_widget.addItem(item)
WidgetIO.connect_widget_change_signal(item_widget, self._update)
return item_widget
def _update(self, _, value, *args):
self._data[self._main_widget.currentRow()] = value
@SafeSlot()
def _add_row(self):
self._add_item(0)
self._add_data_item(self._types.default)
self._repop(self._data)
@SafeSlot()
def _delete_row(self):
@ -422,6 +429,7 @@ class ListFormItem(DynamicFormItem):
row = self._main_widget.currentRow()
self._main_widget.takeItem(row)
self._data.pop(row)
self._repop(self._data)
@SafeSlot()
def clear(self):
@ -430,10 +438,11 @@ class ListFormItem(DynamicFormItem):
def getValue(self):
return self._data
def setValue(self, value: list):
def setValue(self, value: Iterable):
if set(map(type, value)) | {self._types.item} != {self._types.item}:
raise ValueError(f"This widget only accepts items of type {self._types.item}")
self._repop(value)
self._data = list(value)
self._repop(self._data)
class StrLiteralFormItem(DynamicFormItem):
@ -460,6 +469,44 @@ class StrLiteralFormItem(DynamicFormItem):
self._main_widget.setCurrentIndex(-1)
class SetFormItem(ListFormItem):
def _add_main_widget(self) -> None:
super()._add_main_widget()
self._add_item_field = self._types.widget()
self._buttons.addWidget(QLabel("Add new:"))
self._buttons.addWidget(self._add_item_field)
@SafeSlot()
def _add_row(self):
self._add_data_item(WidgetIO.get_value(self._add_item_field))
self._repop(self._data)
def _update(self, _, value, *args):
if value in self._data:
return
return super()._update(_, value, *args)
def _add_data_item(self, val=None):
val = val or self._types.default
if val == self._types.default or val in self._data:
return
self._data.append(val)
self._add_list_item(val)
def _add_list_item(self, val):
item_widget = super()._add_list_item(val)
if isinstance(item_widget, QLineEdit):
item_widget.setReadOnly(True)
return item_widget
def getValue(self):
return set(self._data)
def setValue(self, value: set):
return super().setValue(set(self._data))
WidgetTypeRegistry = OrderedDict[str, tuple[Callable[[FormItemSpec], bool], type[DynamicFormItem]]]
DEFAULT_WIDGET_TYPES: Final[WidgetTypeRegistry] = OrderedDict() | {
@ -511,6 +558,7 @@ def widget_from_type(
if __name__ == "__main__": # pragma: no cover
class TestModel(BaseModel):
value0: set = Field(set(["a", "b"]))
value1: str | None = Field(None)
value2: bool | None = Field(None)
value3: bool = Field(True)
@ -525,15 +573,14 @@ if __name__ == "__main__": # pragma: no cover
w.setLayout(layout)
items = []
for i, (field_name, info) in enumerate(TestModel.model_fields.items()):
spec = spec = FormItemSpec(item_type=info.annotation, name=field_name, info=info)
layout.addWidget(QLabel(field_name), i, 0)
widg = widget_from_type(info.annotation)(
spec=FormItemSpec(item_type=info.annotation, name=field_name, info=info)
)
widg = widget_from_type(spec)(spec=spec)
items.append(widg)
layout.addWidget(widg, i, 1)
items[5].setValue([1, 2, 3, 4])
items[6].setValue(["1", "2", "asdfg", "qwerty"])
items[6].setValue([1, 2, 3, 4])
items[7].setValue(["1", "2", "asdfg", "qwerty"])
w.show()
app.exec()

View File

@ -131,7 +131,7 @@ if __name__ == "__main__": # pragma: no cover
"description": "A device for testing out a widget",
"readOnly": True,
"softwareTrigger": False,
"deviceTags": ["tag1", "tag2", "tag3"],
"deviceTags": {"tag1", "tag2", "tag3"},
"userParameter": {"some_setting": "some_ value"},
}
)

View File

@ -79,7 +79,7 @@ def test_waiting_display(dialog, qtbot):
def test_update_cycle(dialog, qtbot):
update = {"enabled": False, "readoutPriority": "baseline", "deviceTags": ["tag"]}
update = {"enabled": False, "readoutPriority": "baseline", "deviceTags": {"tag"}}
def _mock_send(a, c, w):
dialog.client.device_manager.devices["test_device"]._config = c["test_device"]

View File

@ -104,7 +104,7 @@ def test_list_metadata_field(list_field_and_values: tuple[ListFormItem, list, An
assert list_field._main_widget.count() == 3
assert list_field.getValue() == [vals[0], extra, list_field._types.default]
list_field._add_item(extra)
list_field._add_data_item(extra)
assert list_field._main_widget.count() == 4
assert list_field.getValue() == [vals[0], extra, list_field._types.default, extra]