bec-widgets 2.10.3__py3-none-any.whl → 2.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CHANGELOG.md +84 -0
- PKG-INFO +1 -1
- bec_widgets/cli/client.py +5 -5
- bec_widgets/tests/utils.py +2 -2
- bec_widgets/utils/clickable_label.py +13 -0
- bec_widgets/utils/colors.py +7 -4
- bec_widgets/utils/expandable_frame.py +58 -7
- bec_widgets/utils/forms_from_types/forms.py +107 -28
- bec_widgets/utils/forms_from_types/items.py +88 -12
- bec_widgets/utils/forms_from_types/styles.py +21 -0
- bec_widgets/utils/generate_designer_plugin.py +10 -21
- bec_widgets/widgets/control/scan_control/scan_control.py +1 -1
- bec_widgets/widgets/editors/dict_backed_table.py +31 -11
- bec_widgets/widgets/editors/scan_metadata/_util.py +7 -4
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +10 -4
- bec_widgets/widgets/plots/image/image.py +128 -856
- bec_widgets/widgets/plots/image/image_base.py +1062 -0
- bec_widgets/widgets/plots/image/image_item.py +7 -6
- bec_widgets/widgets/services/device_browser/device_browser.py +61 -29
- bec_widgets/widgets/services/device_browser/device_item/device_item.py +97 -19
- bec_widgets/widgets/services/device_browser/util.py +11 -0
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/METADATA +1 -1
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/RECORD +27 -23
- pyproject.toml +1 -1
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/WHEEL +0 -0
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-2.10.3.dist-info → bec_widgets-2.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,12 +2,12 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from abc import abstractmethod
|
4
4
|
from decimal import Decimal
|
5
|
-
from types import UnionType
|
6
|
-
from typing import
|
5
|
+
from types import GenericAlias, UnionType
|
6
|
+
from typing import Literal
|
7
7
|
|
8
8
|
from bec_lib.logger import bec_logger
|
9
9
|
from bec_qthemes import material_icon
|
10
|
-
from pydantic import BaseModel, ConfigDict, Field
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
11
11
|
from pydantic.fields import FieldInfo
|
12
12
|
from qtpy.QtCore import Signal # type: ignore
|
13
13
|
from qtpy.QtWidgets import (
|
@@ -21,11 +21,13 @@ from qtpy.QtWidgets import (
|
|
21
21
|
QLayout,
|
22
22
|
QLineEdit,
|
23
23
|
QRadioButton,
|
24
|
+
QSizePolicy,
|
24
25
|
QSpinBox,
|
25
26
|
QToolButton,
|
26
27
|
QWidget,
|
27
28
|
)
|
28
29
|
|
30
|
+
from bec_widgets.widgets.editors.dict_backed_table import DictBackedTable
|
29
31
|
from bec_widgets.widgets.editors.scan_metadata._util import (
|
30
32
|
clearable_required,
|
31
33
|
field_default,
|
@@ -46,9 +48,36 @@ class FormItemSpec(BaseModel):
|
|
46
48
|
"""
|
47
49
|
|
48
50
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
49
|
-
|
51
|
+
|
52
|
+
item_type: type | UnionType | GenericAlias
|
50
53
|
name: str
|
51
54
|
info: FieldInfo = FieldInfo()
|
55
|
+
pretty_display: bool = Field(
|
56
|
+
default=False,
|
57
|
+
description="Whether to use a pretty display for the widget. Defaults to False. If True, disables the widget, doesn't add a clear button, and adapts the stylesheet for non-editable display.",
|
58
|
+
)
|
59
|
+
|
60
|
+
@field_validator("item_type", mode="before")
|
61
|
+
@classmethod
|
62
|
+
def _validate_type(cls, v):
|
63
|
+
allowed_primitives = [str, int, float, bool]
|
64
|
+
if isinstance(v, (type, UnionType)):
|
65
|
+
return v
|
66
|
+
if isinstance(v, GenericAlias):
|
67
|
+
if v.__origin__ in [list, dict] and all(
|
68
|
+
arg in allowed_primitives for arg in v.__args__
|
69
|
+
):
|
70
|
+
return v
|
71
|
+
raise ValueError(
|
72
|
+
f"Generics of type {v} are not supported - only lists and dicts of primitive types {allowed_primitives}"
|
73
|
+
)
|
74
|
+
if type(v) is type(Literal[""]): # _LiteralGenericAlias is not exported from typing
|
75
|
+
arg_types = set(type(arg) for arg in v.__args__)
|
76
|
+
if len(arg_types) != 1:
|
77
|
+
raise ValueError("Mixtures of literal types are not supported!")
|
78
|
+
if (t := arg_types.pop()) in allowed_primitives:
|
79
|
+
return t
|
80
|
+
raise ValueError(f"Literals of type {t} are not supported")
|
52
81
|
|
53
82
|
|
54
83
|
class ClearableBoolEntry(QWidget):
|
@@ -94,10 +123,20 @@ class ClearableBoolEntry(QWidget):
|
|
94
123
|
self._false.setToolTip(tooltip)
|
95
124
|
|
96
125
|
|
126
|
+
DynamicFormItemType = str | int | float | Decimal | bool | dict
|
127
|
+
|
128
|
+
|
97
129
|
class DynamicFormItem(QWidget):
|
98
130
|
valueChanged = Signal()
|
99
131
|
|
100
132
|
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
133
|
+
"""
|
134
|
+
Initializes the form item widget.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
parent (QWidget | None, optional): The parent widget. Defaults to None.
|
138
|
+
spec (FormItemSpec): The specification for the form item.
|
139
|
+
"""
|
101
140
|
super().__init__(parent)
|
102
141
|
self._spec = spec
|
103
142
|
self._layout = QHBoxLayout()
|
@@ -107,11 +146,16 @@ class DynamicFormItem(QWidget):
|
|
107
146
|
self._desc = self._spec.info.description
|
108
147
|
self.setLayout(self._layout)
|
109
148
|
self._add_main_widget()
|
110
|
-
|
111
|
-
|
149
|
+
self._main_widget: QWidget
|
150
|
+
self._main_widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
|
151
|
+
if not spec.pretty_display:
|
152
|
+
if clearable_required(spec.info):
|
153
|
+
self._add_clear_button()
|
154
|
+
else:
|
155
|
+
self._set_pretty_display()
|
112
156
|
|
113
157
|
@abstractmethod
|
114
|
-
def getValue(self): ...
|
158
|
+
def getValue(self) -> DynamicFormItemType: ...
|
115
159
|
|
116
160
|
@abstractmethod
|
117
161
|
def setValue(self, value): ...
|
@@ -121,6 +165,9 @@ class DynamicFormItem(QWidget):
|
|
121
165
|
"""Add the main data entry widget to self._main_widget and appply any
|
122
166
|
constraints from the field info"""
|
123
167
|
|
168
|
+
def _set_pretty_display(self):
|
169
|
+
self.setEnabled(False)
|
170
|
+
|
124
171
|
def _describe(self, pad=" "):
|
125
172
|
return pad + (self._desc if self._desc else "")
|
126
173
|
|
@@ -164,7 +211,7 @@ class StrMetadataField(DynamicFormItem):
|
|
164
211
|
def setValue(self, value: str):
|
165
212
|
if value is None:
|
166
213
|
self._main_widget.setText("")
|
167
|
-
self._main_widget.setText(value)
|
214
|
+
self._main_widget.setText(str(value))
|
168
215
|
|
169
216
|
|
170
217
|
class IntMetadataField(DynamicFormItem):
|
@@ -202,12 +249,12 @@ class FloatDecimalMetadataField(DynamicFormItem):
|
|
202
249
|
self._main_widget.textChanged.connect(self._value_changed)
|
203
250
|
|
204
251
|
def _add_main_widget(self) -> None:
|
252
|
+
precision = field_precision(self._spec.info)
|
205
253
|
self._main_widget = QDoubleSpinBox()
|
206
254
|
self._layout.addWidget(self._main_widget)
|
207
|
-
min_, max_ = field_limits(self._spec.info,
|
255
|
+
min_, max_ = field_limits(self._spec.info, float, precision)
|
208
256
|
self._main_widget.setMinimum(min_)
|
209
257
|
self._main_widget.setMaximum(max_)
|
210
|
-
precision = field_precision(self._spec.info)
|
211
258
|
if precision:
|
212
259
|
self._main_widget.setDecimals(precision)
|
213
260
|
minstr = f"{float(min_):.3f}" if abs(min_) <= 1000 else f"{float(min_):.3e}"
|
@@ -224,10 +271,10 @@ class FloatDecimalMetadataField(DynamicFormItem):
|
|
224
271
|
return self._default
|
225
272
|
return self._main_widget.value()
|
226
273
|
|
227
|
-
def setValue(self, value: float):
|
274
|
+
def setValue(self, value: float | Decimal):
|
228
275
|
if value is None:
|
229
276
|
self._main_widget.clear()
|
230
|
-
self._main_widget.setValue(value)
|
277
|
+
self._main_widget.setValue(float(value))
|
231
278
|
|
232
279
|
|
233
280
|
class BoolMetadataField(DynamicFormItem):
|
@@ -251,6 +298,27 @@ class BoolMetadataField(DynamicFormItem):
|
|
251
298
|
self._main_widget.setChecked(value)
|
252
299
|
|
253
300
|
|
301
|
+
class DictMetadataField(DynamicFormItem):
|
302
|
+
def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
|
303
|
+
super().__init__(parent=parent, spec=spec)
|
304
|
+
self._main_widget.data_changed.connect(self._value_changed)
|
305
|
+
|
306
|
+
def _set_pretty_display(self):
|
307
|
+
self._main_widget.set_button_visibility(False)
|
308
|
+
super()._set_pretty_display()
|
309
|
+
|
310
|
+
def _add_main_widget(self) -> None:
|
311
|
+
self._main_widget = DictBackedTable(self, [])
|
312
|
+
self._layout.addWidget(self._main_widget)
|
313
|
+
self._main_widget.setToolTip(self._describe(""))
|
314
|
+
|
315
|
+
def getValue(self):
|
316
|
+
return self._main_widget.dump_dict()
|
317
|
+
|
318
|
+
def setValue(self, value):
|
319
|
+
self._main_widget.replace_data(value)
|
320
|
+
|
321
|
+
|
254
322
|
def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormItem]:
|
255
323
|
if annotation in [str, str | None]:
|
256
324
|
return StrMetadataField
|
@@ -260,6 +328,14 @@ def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormIte
|
|
260
328
|
return FloatDecimalMetadataField
|
261
329
|
if annotation in [bool, bool | None]:
|
262
330
|
return BoolMetadataField
|
331
|
+
if annotation in [dict, dict | None] or (
|
332
|
+
isinstance(annotation, GenericAlias) and annotation.__origin__ is dict
|
333
|
+
):
|
334
|
+
return DictMetadataField
|
335
|
+
if annotation in [list, list | None] or (
|
336
|
+
isinstance(annotation, GenericAlias) and annotation.__origin__ is list
|
337
|
+
):
|
338
|
+
return StrMetadataField
|
263
339
|
else:
|
264
340
|
logger.warning(f"Type {annotation} is not (yet) supported in metadata form creation.")
|
265
341
|
return StrMetadataField
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import bec_qthemes
|
2
|
+
|
3
|
+
|
4
|
+
def pretty_display_theme(theme: str = "dark"):
|
5
|
+
palette = bec_qthemes.load_palette(theme)
|
6
|
+
foreground = palette.text().color().name()
|
7
|
+
background = palette.base().color().name()
|
8
|
+
border = palette.shadow().color().name()
|
9
|
+
accent = palette.accent().color().name()
|
10
|
+
return f"""
|
11
|
+
QWidget {{color: {foreground}; background-color: {background}}}
|
12
|
+
QLabel {{ font-weight: bold; }}
|
13
|
+
QLineEdit,QLabel,QTreeView {{ border-style: solid; border-width: 2px; border-color: {border} }}
|
14
|
+
QRadioButton {{ color: {foreground}; }}
|
15
|
+
QRadioButton::indicator::checked {{ color: {accent}; }}
|
16
|
+
QCheckBox {{ color: {accent}; }}
|
17
|
+
"""
|
18
|
+
|
19
|
+
|
20
|
+
if __name__ == "__main__":
|
21
|
+
print(pretty_display_theme())
|
@@ -8,6 +8,9 @@ from qtpy.QtCore import QObject
|
|
8
8
|
from bec_widgets.utils.name_utils import pascal_to_snake
|
9
9
|
|
10
10
|
EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock", "BECFigure"]
|
11
|
+
_PARENT_ARG_REGEX = r".__init__\(\s*(?:parent\)|parent=parent,?|parent,?)"
|
12
|
+
_SELF_PARENT_ARG_REGEX = r".__init__\(\s*self,\s*(?:parent\)|parent=parent,?|parent,?)"
|
13
|
+
SUPER_INIT_REGEX = re.compile(r"super\(\)" + _PARENT_ARG_REGEX, re.MULTILINE)
|
11
14
|
|
12
15
|
|
13
16
|
class PluginFilenames(NamedTuple):
|
@@ -90,34 +93,20 @@ class DesignerPluginGenerator:
|
|
90
93
|
|
91
94
|
# Check if the widget class calls the super constructor with parent argument
|
92
95
|
init_source = inspect.getsource(self.widget.__init__)
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
)
|
98
|
-
super_init_found = (
|
99
|
-
bool(
|
100
|
-
init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent=parent") > 0
|
101
|
-
)
|
102
|
-
or bool(init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent,") > 0)
|
103
|
-
or bool(init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent)") > 0)
|
96
|
+
class_re = re.compile(base_cls[0].__name__ + _SELF_PARENT_ARG_REGEX, re.MULTILINE)
|
97
|
+
cls_init_found = class_re.search(init_source) is not None
|
98
|
+
super_self_re = re.compile(
|
99
|
+
rf"super\({base_cls[0].__name__}, self\)" + _PARENT_ARG_REGEX, re.MULTILINE
|
104
100
|
)
|
101
|
+
super_init_found = super_self_re.search(init_source) is not None
|
105
102
|
if issubclass(self.widget.__bases__[0], QObject) and not super_init_found:
|
106
|
-
super_init_found = (
|
107
|
-
bool(init_source.find("super().__init__(parent=parent") > 0)
|
108
|
-
or bool(init_source.find("super().__init__(parent,") > 0)
|
109
|
-
or bool(init_source.find("super().__init__(parent)") > 0)
|
110
|
-
)
|
103
|
+
super_init_found = SUPER_INIT_REGEX.search(init_source) is not None
|
111
104
|
|
112
105
|
# for the new style classes, we only have one super call. We can therefore check if the
|
113
106
|
# number of __init__ calls is 2 (the class itself and the super class)
|
114
107
|
num_inits = re.findall(r"__init__", init_source)
|
115
108
|
if len(num_inits) == 2 and not super_init_found:
|
116
|
-
super_init_found =
|
117
|
-
init_source.find("super().__init__(parent=parent") > 0
|
118
|
-
or init_source.find("super().__init__(parent,") > 0
|
119
|
-
or init_source.find("super().__init__(parent)") > 0
|
120
|
-
)
|
109
|
+
super_init_found = SUPER_INIT_REGEX.search(init_source) is not None
|
121
110
|
|
122
111
|
if not cls_init_found and not super_init_found:
|
123
112
|
raise ValueError(
|
@@ -89,6 +89,7 @@ class ScanControl(BECWidget, QWidget):
|
|
89
89
|
self.config.allowed_scans = allowed_scans
|
90
90
|
|
91
91
|
self._scan_metadata: dict | None = None
|
92
|
+
self._metadata_form = ScanMetadata(parent=self)
|
92
93
|
|
93
94
|
# Create and set main layout
|
94
95
|
self._init_UI()
|
@@ -165,7 +166,6 @@ class ScanControl(BECWidget, QWidget):
|
|
165
166
|
self.layout.addStretch()
|
166
167
|
|
167
168
|
def _add_metadata_form(self):
|
168
|
-
self._metadata_form = ScanMetadata(parent=self)
|
169
169
|
self.layout.addWidget(self._metadata_form)
|
170
170
|
self._metadata_form.update_with_new_scan(self.comboBox_scan_selection.currentText())
|
171
171
|
self.scan_selected.connect(self._metadata_form.update_with_new_scan)
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from typing import Any
|
4
4
|
|
5
|
+
from qtpy import QtWidgets
|
5
6
|
from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal # type: ignore
|
6
7
|
from qtpy.QtWidgets import (
|
7
8
|
QApplication,
|
@@ -45,7 +46,11 @@ class DictBackedTableModel(QAbstractTableModel):
|
|
45
46
|
|
46
47
|
def data(self, index, role=Qt.ItemDataRole):
|
47
48
|
if index.isValid():
|
48
|
-
if role
|
49
|
+
if role in [
|
50
|
+
Qt.ItemDataRole.DisplayRole,
|
51
|
+
Qt.ItemDataRole.EditRole,
|
52
|
+
Qt.ItemDataRole.ToolTipRole,
|
53
|
+
]:
|
49
54
|
return str(self._data[index.row()][index.column()])
|
50
55
|
|
51
56
|
def setData(self, index, value, role):
|
@@ -57,6 +62,11 @@ class DictBackedTableModel(QAbstractTableModel):
|
|
57
62
|
return True
|
58
63
|
return False
|
59
64
|
|
65
|
+
def replaceData(self, data: dict):
|
66
|
+
self.resetInternalData()
|
67
|
+
self._data = [[k, v] for k, v in data.items()]
|
68
|
+
self.dataChanged.emit(self.index(0, 0), self.index(len(self._data), 0))
|
69
|
+
|
60
70
|
def update_disallowed_keys(self, keys: list[str]):
|
61
71
|
"""Set the list of keys which may not be used.
|
62
72
|
|
@@ -110,16 +120,16 @@ class DictBackedTableModel(QAbstractTableModel):
|
|
110
120
|
|
111
121
|
class DictBackedTable(QWidget):
|
112
122
|
delete_rows = Signal(list)
|
113
|
-
|
123
|
+
data_changed = Signal(dict)
|
114
124
|
|
115
|
-
def __init__(self, initial_data: list[list[str]]):
|
125
|
+
def __init__(self, parent: QWidget | None = None, initial_data: list[list[str]] = []):
|
116
126
|
"""Widget which uses a DictBackedTableModel to display an editable table
|
117
127
|
which can be extracted as a dict.
|
118
128
|
|
119
129
|
Args:
|
120
130
|
initial_data (list[list[str]]): list of key-value pairs to initialise with
|
121
131
|
"""
|
122
|
-
super().__init__()
|
132
|
+
super().__init__(parent)
|
123
133
|
|
124
134
|
self._layout = QHBoxLayout()
|
125
135
|
self.setLayout(self._layout)
|
@@ -127,13 +137,17 @@ class DictBackedTable(QWidget):
|
|
127
137
|
self._table_view = QTreeView()
|
128
138
|
self._table_view.setModel(self._table_model)
|
129
139
|
self._table_view.setSizePolicy(
|
130
|
-
QSizePolicy(QSizePolicy.Policy.
|
140
|
+
QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
131
141
|
)
|
132
142
|
self._table_view.setAlternatingRowColors(True)
|
143
|
+
self._table_view.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
|
144
|
+
self._table_view.header().setSectionResizeMode(5, QtWidgets.QHeaderView.Stretch)
|
133
145
|
self._layout.addWidget(self._table_view)
|
134
146
|
|
147
|
+
self._button_holder = QWidget()
|
135
148
|
self._buttons = QVBoxLayout()
|
136
|
-
self.
|
149
|
+
self._button_holder.setLayout(self._buttons)
|
150
|
+
self._layout.addWidget(self._button_holder)
|
137
151
|
self._add_button = QPushButton("+")
|
138
152
|
self._add_button.setToolTip("add a new row")
|
139
153
|
self._remove_button = QPushButton("-")
|
@@ -143,11 +157,17 @@ class DictBackedTable(QWidget):
|
|
143
157
|
self._add_button.clicked.connect(self._table_model.add_row)
|
144
158
|
self._remove_button.clicked.connect(self.delete_selected_rows)
|
145
159
|
self.delete_rows.connect(self._table_model.delete_rows)
|
146
|
-
self._table_model.dataChanged.connect(self.
|
160
|
+
self._table_model.dataChanged.connect(lambda *_: self.data_changed.emit(self.dump_dict()))
|
161
|
+
|
162
|
+
def set_button_visibility(self, value: bool):
|
163
|
+
self._button_holder.setVisible(value)
|
164
|
+
|
165
|
+
@SafeSlot()
|
166
|
+
def clear(self):
|
167
|
+
self._table_model.replaceData({})
|
147
168
|
|
148
|
-
def
|
149
|
-
|
150
|
-
self.data_updated.emit()
|
169
|
+
def replace_data(self, data: dict):
|
170
|
+
self._table_model.replaceData(data)
|
151
171
|
|
152
172
|
def delete_selected_rows(self):
|
153
173
|
"""Delete rows which are part of the selection model"""
|
@@ -174,6 +194,6 @@ if __name__ == "__main__": # pragma: no cover
|
|
174
194
|
app = QApplication([])
|
175
195
|
set_theme("dark")
|
176
196
|
|
177
|
-
window = DictBackedTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
|
197
|
+
window = DictBackedTable(None, [["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
|
178
198
|
window.show()
|
179
199
|
app.exec()
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import sys
|
4
4
|
from decimal import Decimal
|
5
|
-
from math import inf, nextafter
|
5
|
+
from math import copysign, inf, nextafter
|
6
6
|
from typing import TYPE_CHECKING, TypeVar, get_args
|
7
7
|
|
8
8
|
from annotated_types import Ge, Gt, Le, Lt
|
@@ -23,16 +23,19 @@ _MAXFLOAT = sys.float_info.max
|
|
23
23
|
T = TypeVar("T", int, float, Decimal)
|
24
24
|
|
25
25
|
|
26
|
-
def field_limits(info: FieldInfo, type_: type[T]) -> tuple[T, T]:
|
26
|
+
def field_limits(info: FieldInfo, type_: type[T], prec: int | None = None) -> tuple[T, T]:
|
27
|
+
def _nextafter(x, y):
|
28
|
+
return nextafter(x, y) if prec is None else x + (10 ** (-prec)) * (copysign(1, y))
|
29
|
+
|
27
30
|
_min = _MININT if type_ is int else _MINFLOAT
|
28
31
|
_max = _MAXINT if type_ is int else _MAXFLOAT
|
29
32
|
for md in info.metadata:
|
30
33
|
if isinstance(md, Ge):
|
31
34
|
_min = type_(md.ge) # type: ignore
|
32
35
|
if isinstance(md, Gt):
|
33
|
-
_min = type_(md.gt) + 1 if type_ is int else
|
36
|
+
_min = type_(md.gt) + 1 if type_ is int else _nextafter(type_(md.gt), inf) # type: ignore
|
34
37
|
if isinstance(md, Lt):
|
35
|
-
_max = type_(md.lt) - 1 if type_ is int else
|
38
|
+
_max = type_(md.lt) - 1 if type_ is int else _nextafter(type_(md.lt), -inf) # type: ignore
|
36
39
|
if isinstance(md, Le):
|
37
40
|
_max = type_(md.le) # type: ignore
|
38
41
|
return _min, _max # type: ignore
|
@@ -16,6 +16,9 @@ logger = bec_logger.logger
|
|
16
16
|
|
17
17
|
|
18
18
|
class ScanMetadata(PydanticModelForm):
|
19
|
+
|
20
|
+
RPC = False
|
21
|
+
|
19
22
|
def __init__(
|
20
23
|
self,
|
21
24
|
parent=None,
|
@@ -36,16 +39,18 @@ class ScanMetadata(PydanticModelForm):
|
|
36
39
|
|
37
40
|
# self.populate() gets called in super().__init__
|
38
41
|
# so make sure self._additional_metadata exists
|
39
|
-
self._additional_md_box = ExpandableGroupFrame(
|
42
|
+
self._additional_md_box = ExpandableGroupFrame(
|
43
|
+
parent, "Additional metadata", expanded=False
|
44
|
+
)
|
40
45
|
self._additional_md_box_layout = QHBoxLayout()
|
41
46
|
self._additional_md_box.set_layout(self._additional_md_box_layout)
|
42
47
|
|
43
|
-
self._additional_metadata = DictBackedTable(initial_extras or [])
|
48
|
+
self._additional_metadata = DictBackedTable(parent, initial_extras or [])
|
44
49
|
self._scan_name = scan_name or ""
|
45
50
|
self._md_schema = get_metadata_schema_for_scan(self._scan_name)
|
46
|
-
self._additional_metadata.
|
51
|
+
self._additional_metadata.data_changed.connect(self.validate_form)
|
47
52
|
|
48
|
-
super().__init__(parent=parent,
|
53
|
+
super().__init__(parent=parent, data_model=self._md_schema, client=client, **kwargs)
|
49
54
|
|
50
55
|
self._layout.addWidget(self._additional_md_box)
|
51
56
|
self._additional_md_box_layout.addWidget(self._additional_metadata)
|
@@ -127,6 +132,7 @@ if __name__ == "__main__": # pragma: no cover
|
|
127
132
|
w.setLayout(layout)
|
128
133
|
|
129
134
|
scan_metadata = ScanMetadata(
|
135
|
+
parent=w,
|
130
136
|
scan_name="grid_scan",
|
131
137
|
initial_extras=[["key1", "value1"], ["key2", "value2"], ["key3", "value3"]],
|
132
138
|
)
|