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

feat: (#493) add dict to dynamic form types

This commit is contained in:
2025-05-13 17:09:06 +02:00
committed by David Perl
parent a25c1a8039
commit 92d1d6435d
6 changed files with 66 additions and 23 deletions

View File

@ -26,6 +26,7 @@ from qtpy.QtWidgets import (
QWidget,
)
from bec_widgets.widgets.editors.dict_backed_table import DictBackedTable
from bec_widgets.widgets.editors.scan_metadata._util import (
clearable_required,
field_default,
@ -94,7 +95,7 @@ class ClearableBoolEntry(QWidget):
self._false.setToolTip(tooltip)
DynamicFormItemType = str | int | float | Decimal | bool
DynamicFormItemType = str | int | float | Decimal | bool | dict
class DynamicFormItem(QWidget):
@ -205,12 +206,12 @@ class FloatDecimalMetadataField(DynamicFormItem):
self._main_widget.textChanged.connect(self._value_changed)
def _add_main_widget(self) -> None:
precision = field_precision(self._spec.info)
self._main_widget = QDoubleSpinBox()
self._layout.addWidget(self._main_widget)
min_, max_ = field_limits(self._spec.info, float)
min_, max_ = field_limits(self._spec.info, float, precision)
self._main_widget.setMinimum(min_)
self._main_widget.setMaximum(max_)
precision = field_precision(self._spec.info)
if precision:
self._main_widget.setDecimals(precision)
minstr = f"{float(min_):.3f}" if abs(min_) <= 1000 else f"{float(min_):.3e}"
@ -254,6 +255,23 @@ class BoolMetadataField(DynamicFormItem):
self._main_widget.setChecked(value)
class DictMetadataField(DynamicFormItem):
def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
super().__init__(parent=parent, spec=spec)
self._main_widget.data_changed.connect(self._value_changed)
def _add_main_widget(self) -> None:
self._main_widget = DictBackedTable([])
self._layout.addWidget(self._main_widget)
self._main_widget.setToolTip(self._describe(""))
def getValue(self):
return self._main_widget.dump_dict()
def setValue(self, value):
self._main_widget.replace_data(value)
def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormItem]:
if annotation in [str, str | None]:
return StrMetadataField
@ -263,6 +281,14 @@ def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormIte
return FloatDecimalMetadataField
if annotation in [bool, bool | None]:
return BoolMetadataField
if annotation in [dict, dict | None] or (
isinstance(annotation, GenericAlias) and annotation.__origin__ is dict
):
return DictMetadataField
if annotation in [list, list | None] or (
isinstance(annotation, GenericAlias) and annotation.__origin__ is list
):
return StrMetadataField
else:
logger.warning(f"Type {annotation} is not (yet) supported in metadata form creation.")
return StrMetadataField

View File

@ -57,6 +57,11 @@ class DictBackedTableModel(QAbstractTableModel):
return True
return False
def replaceData(self, data: dict):
self.resetInternalData()
self._data = [[k, v] for k, v in data.items()]
self.dataChanged.emit(self.index(0, 0), self.index(len(self._data), 0))
def update_disallowed_keys(self, keys: list[str]):
"""Set the list of keys which may not be used.
@ -110,7 +115,7 @@ class DictBackedTableModel(QAbstractTableModel):
class DictBackedTable(QWidget):
delete_rows = Signal(list)
data_updated = Signal()
data_changed = Signal(dict)
def __init__(self, initial_data: list[list[str]]):
"""Widget which uses a DictBackedTableModel to display an editable table
@ -143,11 +148,14 @@ class DictBackedTable(QWidget):
self._add_button.clicked.connect(self._table_model.add_row)
self._remove_button.clicked.connect(self.delete_selected_rows)
self.delete_rows.connect(self._table_model.delete_rows)
self._table_model.dataChanged.connect(self._emit_data_updated)
self._table_model.dataChanged.connect(lambda *_: self.data_changed.emit(self.dump_dict()))
def _emit_data_updated(self, *args, **kwargs):
"""Just to swallow the args"""
self.data_updated.emit()
@SafeSlot()
def clear(self):
self._table_model.replaceData({})
def replace_data(self, data: dict):
self._table_model.replaceData(data)
def delete_selected_rows(self):
"""Delete rows which are part of the selection model"""

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import sys
from decimal import Decimal
from math import inf, nextafter
from math import copysign, inf, nextafter
from typing import TYPE_CHECKING, TypeVar, get_args
from annotated_types import Ge, Gt, Le, Lt
@ -23,16 +23,19 @@ _MAXFLOAT = sys.float_info.max
T = TypeVar("T", int, float, Decimal)
def field_limits(info: FieldInfo, type_: type[T]) -> tuple[T, T]:
def field_limits(info: FieldInfo, type_: type[T], prec: int | None = None) -> tuple[T, T]:
def _nextafter(x, y):
return nextafter(x, y) if prec is None else x + (10 ** (-prec)) * (copysign(1, y))
_min = _MININT if type_ is int else _MINFLOAT
_max = _MAXINT if type_ is int else _MAXFLOAT
for md in info.metadata:
if isinstance(md, Ge):
_min = type_(md.ge) # type: ignore
if isinstance(md, Gt):
_min = type_(md.gt) + 1 if type_ is int else nextafter(type_(md.gt), inf) # type: ignore
_min = type_(md.gt) + 1 if type_ is int else _nextafter(type_(md.gt), inf) # type: ignore
if isinstance(md, Lt):
_max = type_(md.lt) - 1 if type_ is int else nextafter(type_(md.lt), -inf) # type: ignore
_max = type_(md.lt) - 1 if type_ is int else _nextafter(type_(md.lt), -inf) # type: ignore
if isinstance(md, Le):
_max = type_(md.le) # type: ignore
return _min, _max # type: ignore

View File

@ -43,7 +43,7 @@ class ScanMetadata(PydanticModelForm):
self._additional_metadata = DictBackedTable(initial_extras or [])
self._scan_name = scan_name or ""
self._md_schema = get_metadata_schema_for_scan(self._scan_name)
self._additional_metadata.data_updated.connect(self.validate_form)
self._additional_metadata.data_changed.connect(self.validate_form)
super().__init__(parent=parent, data_model=self._md_schema, client=client, **kwargs)