0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +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", "readoutPriority": "baseline",
"deviceClass": "ophyd.Device", "deviceClass": "ophyd.Device",
"deviceConfig": {}, "deviceConfig": {},
"deviceTags": ["user device"], "deviceTags": {"user device"},
"enabled": enabled, "enabled": enabled,
"readOnly": False, "readOnly": False,
"name": self.name, "name": self.name,
@ -89,7 +89,7 @@ class FakePositioner(BECPositioner):
"readoutPriority": "baseline", "readoutPriority": "baseline",
"deviceClass": "ophyd_devices.SimPositioner", "deviceClass": "ophyd_devices.SimPositioner",
"deviceConfig": {"delay": 1, "tolerance": 0.01, "update_frequency": 400}, "deviceConfig": {"delay": 1, "tolerance": 0.01, "update_frequency": 400},
"deviceTags": ["user motors"], "deviceTags": {"user motors"},
"enabled": enabled, "enabled": enabled,
"readOnly": False, "readOnly": False,
"name": self.name, "name": self.name,

View File

@ -102,6 +102,8 @@ class TypedForm(BECWidget, QWidget):
self._clear_grid() self._clear_grid()
for r, item in enumerate(self._items): for r, item in enumerate(self._items):
self._add_griditem(item, r) self._add_griditem(item, r)
gl: QGridLayout = self._form_grid.layout()
gl.setRowStretch(gl.rowCount(), 1)
def _add_griditem(self, item: FormItemSpec, row: int): def _add_griditem(self, item: FormItemSpec, row: int):
grid = self._form_grid.layout() grid = self._form_grid.layout()
@ -116,9 +118,9 @@ class TypedForm(BECWidget, QWidget):
def enumerate_form_widgets(self): def enumerate_form_widgets(self):
"""Return a generator over the rows of the form, with the row number, the label widget (to """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 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()) yield GridRow(i, grid.itemAtPosition(i, 0).widget(), grid.itemAtPosition(i, 1).widget())
def _dict_from_grid(self) -> dict[str, DynamicFormItemType]: def _dict_from_grid(self) -> dict[str, DynamicFormItemType]:

View File

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

View File

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

View File

@ -79,7 +79,7 @@ def test_waiting_display(dialog, qtbot):
def test_update_cycle(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): def _mock_send(a, c, w):
dialog.client.device_manager.devices["test_device"]._config = c["test_device"] 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._main_widget.count() == 3
assert list_field.getValue() == [vals[0], extra, list_field._types.default] 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._main_widget.count() == 4
assert list_field.getValue() == [vals[0], extra, list_field._types.default, extra] assert list_field.getValue() == [vals[0], extra, list_field._types.default, extra]