bec-widgets 1.19.2__py3-none-any.whl → 1.21.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 +20 -0
- PKG-INFO +2 -3
- bec_widgets/cli/client.py +21 -0
- bec_widgets/widgets/__init__.py +0 -1
- bec_widgets/widgets/containers/dock/dock_area.py +7 -0
- bec_widgets/widgets/editors/scan_metadata/__init__.py +7 -0
- bec_widgets/widgets/editors/scan_metadata/_metadata_widgets.py +275 -0
- bec_widgets/widgets/editors/scan_metadata/_util.py +67 -0
- bec_widgets/widgets/editors/scan_metadata/additional_metadata_table.py +146 -0
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +196 -0
- bec_widgets/widgets/editors/text_box/text_box.py +5 -5
- bec_widgets/widgets/utility/logpanel/__init__.py +3 -0
- bec_widgets/widgets/utility/logpanel/_util.py +58 -0
- bec_widgets/widgets/utility/logpanel/log_panel.pyproject +1 -0
- bec_widgets/widgets/utility/logpanel/log_panel_plugin.py +54 -0
- bec_widgets/widgets/utility/logpanel/logpanel.py +529 -0
- bec_widgets/widgets/utility/logpanel/register_log_panel.py +15 -0
- {bec_widgets-1.19.2.dist-info → bec_widgets-1.21.0.dist-info}/METADATA +2 -3
- {bec_widgets-1.19.2.dist-info → bec_widgets-1.21.0.dist-info}/RECORD +23 -12
- pyproject.toml +4 -4
- {bec_widgets-1.19.2.dist-info → bec_widgets-1.21.0.dist-info}/WHEEL +0 -0
- {bec_widgets-1.19.2.dist-info → bec_widgets-1.21.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.19.2.dist-info → bec_widgets-1.21.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
@@ -1,6 +1,26 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
3
|
|
4
|
+
## v1.21.0 (2025-02-17)
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
- Generated form for scan metadata
|
9
|
+
([`1708bd4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1708bd405f86b1353828b01fbf5f98383a19ec2a))
|
10
|
+
|
11
|
+
|
12
|
+
## v1.20.0 (2025-02-06)
|
13
|
+
|
14
|
+
### Features
|
15
|
+
|
16
|
+
- **widget**: Add LogPanel widget
|
17
|
+
([`b3217b7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b3217b7ca5cabe8798f06787de4ae3f3ec1af3b6))
|
18
|
+
|
19
|
+
hopefully without segfaults - compared to first implementation: - explicitly set parent of all
|
20
|
+
dialog components - try/except and log for redis new message callback - pass in ServiceStatusMixin
|
21
|
+
and explicitly clean it up
|
22
|
+
|
23
|
+
|
4
24
|
## v1.19.2 (2025-02-06)
|
5
25
|
|
6
26
|
### Bug Fixes
|
PKG-INFO
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bec_widgets
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.21.0
|
4
4
|
Summary: BEC Widgets
|
5
5
|
Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
|
6
6
|
Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
|
@@ -16,6 +16,7 @@ Requires-Dist: black~=24.0
|
|
16
16
|
Requires-Dist: isort>=5.13.2,~=5.13
|
17
17
|
Requires-Dist: pydantic~=2.0
|
18
18
|
Requires-Dist: pyqtgraph~=0.13
|
19
|
+
Requires-Dist: pyside6==6.7.2
|
19
20
|
Requires-Dist: pyte
|
20
21
|
Requires-Dist: qtconsole>=5.5.1,~=5.5
|
21
22
|
Requires-Dist: qtpy~=2.4
|
@@ -28,5 +29,3 @@ Requires-Dist: pytest-random-order~=1.1; extra == 'dev'
|
|
28
29
|
Requires-Dist: pytest-timeout~=2.2; extra == 'dev'
|
29
30
|
Requires-Dist: pytest-xvfb~=3.0; extra == 'dev'
|
30
31
|
Requires-Dist: pytest~=8.0; extra == 'dev'
|
31
|
-
Provides-Extra: pyside6
|
32
|
-
Requires-Dist: pyside6==6.7.2; extra == 'pyside6'
|
bec_widgets/cli/client.py
CHANGED
@@ -31,6 +31,7 @@ class Widgets(str, enum.Enum):
|
|
31
31
|
DeviceComboBox = "DeviceComboBox"
|
32
32
|
DeviceLineEdit = "DeviceLineEdit"
|
33
33
|
LMFitDialog = "LMFitDialog"
|
34
|
+
LogPanel = "LogPanel"
|
34
35
|
Minesweeper = "Minesweeper"
|
35
36
|
PositionIndicator = "PositionIndicator"
|
36
37
|
PositionerBox = "PositionerBox"
|
@@ -3183,6 +3184,26 @@ class LMFitDialog(RPCBase):
|
|
3183
3184
|
"""
|
3184
3185
|
|
3185
3186
|
|
3187
|
+
class LogPanel(RPCBase):
|
3188
|
+
@rpc_call
|
3189
|
+
def set_plain_text(self, text: str) -> None:
|
3190
|
+
"""
|
3191
|
+
Set the plain text of the widget.
|
3192
|
+
|
3193
|
+
Args:
|
3194
|
+
text (str): The text to set.
|
3195
|
+
"""
|
3196
|
+
|
3197
|
+
@rpc_call
|
3198
|
+
def set_html_text(self, text: str) -> None:
|
3199
|
+
"""
|
3200
|
+
Set the HTML text of the widget.
|
3201
|
+
|
3202
|
+
Args:
|
3203
|
+
text (str): The text to set.
|
3204
|
+
"""
|
3205
|
+
|
3206
|
+
|
3186
3207
|
class Minesweeper(RPCBase): ...
|
3187
3208
|
|
3188
3209
|
|
bec_widgets/widgets/__init__.py
CHANGED
@@ -1 +0,0 @@
|
|
1
|
-
|
@@ -30,6 +30,7 @@ from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
|
|
30
30
|
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
|
31
31
|
from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
|
32
32
|
from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatusBox
|
33
|
+
from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
|
33
34
|
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
34
35
|
|
35
36
|
|
@@ -139,6 +140,9 @@ class BECDockArea(BECWidget, QWidget):
|
|
139
140
|
tooltip="Add Circular ProgressBar",
|
140
141
|
filled=True,
|
141
142
|
),
|
143
|
+
"log_panel": MaterialIconAction(
|
144
|
+
icon_name=LogPanel.ICON_NAME, tooltip="Add LogPanel", filled=True
|
145
|
+
),
|
142
146
|
},
|
143
147
|
),
|
144
148
|
"separator_2": SeparatorAction(),
|
@@ -200,6 +204,9 @@ class BECDockArea(BECWidget, QWidget):
|
|
200
204
|
self.toolbar.widgets["menu_utils"].widgets["progress_bar"].triggered.connect(
|
201
205
|
lambda: self.add_dock(widget="RingProgressBar", prefix="progress_bar")
|
202
206
|
)
|
207
|
+
self.toolbar.widgets["menu_utils"].widgets["log_panel"].triggered.connect(
|
208
|
+
lambda: self.add_dock(widget="LogPanel", prefix="log_panel")
|
209
|
+
)
|
203
210
|
|
204
211
|
# Icons
|
205
212
|
self.toolbar.widgets["attach_all"].action.triggered.connect(self.attach_all)
|
@@ -0,0 +1,7 @@
|
|
1
|
+
from bec_widgets.widgets.editors.scan_metadata.additional_metadata_table import (
|
2
|
+
AdditionalMetadataTable,
|
3
|
+
AdditionalMetadataTableModel,
|
4
|
+
)
|
5
|
+
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
|
6
|
+
|
7
|
+
__all__ = ["ScanMetadata", "AdditionalMetadataTable", "AdditionalMetadataTableModel"]
|
@@ -0,0 +1,275 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from abc import abstractmethod
|
4
|
+
from decimal import Decimal
|
5
|
+
from typing import TYPE_CHECKING, Callable, get_args
|
6
|
+
|
7
|
+
from bec_lib.logger import bec_logger
|
8
|
+
from bec_qthemes import material_icon
|
9
|
+
from pydantic import BaseModel, Field
|
10
|
+
from qtpy.QtCore import Signal # type: ignore
|
11
|
+
from qtpy.QtWidgets import (
|
12
|
+
QApplication,
|
13
|
+
QButtonGroup,
|
14
|
+
QCheckBox,
|
15
|
+
QDoubleSpinBox,
|
16
|
+
QGridLayout,
|
17
|
+
QHBoxLayout,
|
18
|
+
QLabel,
|
19
|
+
QLayout,
|
20
|
+
QLineEdit,
|
21
|
+
QRadioButton,
|
22
|
+
QSpinBox,
|
23
|
+
QToolButton,
|
24
|
+
QWidget,
|
25
|
+
)
|
26
|
+
|
27
|
+
from bec_widgets.widgets.editors.scan_metadata._util import (
|
28
|
+
clearable_required,
|
29
|
+
field_default,
|
30
|
+
field_limits,
|
31
|
+
field_maxlen,
|
32
|
+
field_minlen,
|
33
|
+
field_precision,
|
34
|
+
)
|
35
|
+
|
36
|
+
if TYPE_CHECKING:
|
37
|
+
from pydantic.fields import FieldInfo
|
38
|
+
|
39
|
+
logger = bec_logger.logger
|
40
|
+
|
41
|
+
|
42
|
+
class ClearableBoolEntry(QWidget):
|
43
|
+
stateChanged = Signal()
|
44
|
+
|
45
|
+
def __init__(self, parent: QWidget | None = None) -> None:
|
46
|
+
super().__init__(parent)
|
47
|
+
self._layout = QHBoxLayout()
|
48
|
+
self._layout.setContentsMargins(0, 0, 0, 0)
|
49
|
+
self.setLayout(self._layout)
|
50
|
+
self._layout.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
|
51
|
+
self._entry = QButtonGroup()
|
52
|
+
self._true = QRadioButton("true", parent=self)
|
53
|
+
self._false = QRadioButton("false", parent=self)
|
54
|
+
for button in [self._true, self._false]:
|
55
|
+
self._layout.addWidget(button)
|
56
|
+
self._entry.addButton(button)
|
57
|
+
button.toggled.connect(self.stateChanged)
|
58
|
+
|
59
|
+
def clear(self):
|
60
|
+
self._entry.setExclusive(False)
|
61
|
+
self._true.setChecked(False)
|
62
|
+
self._false.setChecked(False)
|
63
|
+
self._entry.setExclusive(True)
|
64
|
+
|
65
|
+
def isChecked(self) -> bool | None:
|
66
|
+
if not self._true.isChecked() and not self._false.isChecked():
|
67
|
+
return None
|
68
|
+
return self._true.isChecked()
|
69
|
+
|
70
|
+
def setChecked(self, value: bool | None):
|
71
|
+
if value is None:
|
72
|
+
self.clear()
|
73
|
+
elif value:
|
74
|
+
self._true.setChecked(True)
|
75
|
+
self._false.setChecked(False)
|
76
|
+
else:
|
77
|
+
self._true.setChecked(False)
|
78
|
+
self._false.setChecked(True)
|
79
|
+
|
80
|
+
def setToolTip(self, tooltip: str):
|
81
|
+
self._true.setToolTip(tooltip)
|
82
|
+
self._false.setToolTip(tooltip)
|
83
|
+
|
84
|
+
|
85
|
+
class MetadataWidget(QWidget):
|
86
|
+
|
87
|
+
valueChanged = Signal()
|
88
|
+
|
89
|
+
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
90
|
+
super().__init__(parent)
|
91
|
+
self._info = info
|
92
|
+
self._layout = QHBoxLayout()
|
93
|
+
self._layout.setContentsMargins(0, 0, 0, 0)
|
94
|
+
self._layout.setSizeConstraint(QLayout.SizeConstraint.SetMaximumSize)
|
95
|
+
self._default = field_default(self._info)
|
96
|
+
self._desc = self._info.description
|
97
|
+
self.setLayout(self._layout)
|
98
|
+
self._add_main_widget()
|
99
|
+
if clearable_required(info):
|
100
|
+
self._add_clear_button()
|
101
|
+
|
102
|
+
@abstractmethod
|
103
|
+
def getValue(self): ...
|
104
|
+
|
105
|
+
@abstractmethod
|
106
|
+
def setValue(self, value): ...
|
107
|
+
|
108
|
+
@abstractmethod
|
109
|
+
def _add_main_widget(self) -> None:
|
110
|
+
"""Add the main data entry widget to self._main_widget and appply any
|
111
|
+
constraints from the field info"""
|
112
|
+
|
113
|
+
def _describe(self, pad=" "):
|
114
|
+
return pad + (self._desc if self._desc else "")
|
115
|
+
|
116
|
+
def _add_clear_button(self):
|
117
|
+
self._clear_button = QToolButton()
|
118
|
+
self._clear_button.setIcon(
|
119
|
+
material_icon(icon_name="close", size=(10, 10), convert_to_pixmap=False)
|
120
|
+
)
|
121
|
+
self._layout.addWidget(self._clear_button)
|
122
|
+
# the widget added in _add_main_widget must implement .clear() if value is not required
|
123
|
+
self._clear_button.setToolTip("Clear value or reset to default.")
|
124
|
+
self._clear_button.clicked.connect(self._main_widget.clear) # type: ignore
|
125
|
+
|
126
|
+
def _value_changed(self, *_, **__):
|
127
|
+
self.valueChanged.emit()
|
128
|
+
|
129
|
+
|
130
|
+
class StrMetadataField(MetadataWidget):
|
131
|
+
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
132
|
+
super().__init__(info, parent)
|
133
|
+
self._main_widget.textChanged.connect(self._value_changed)
|
134
|
+
|
135
|
+
def _add_main_widget(self) -> None:
|
136
|
+
self._main_widget = QLineEdit()
|
137
|
+
self._layout.addWidget(self._main_widget)
|
138
|
+
min_length, max_length = field_minlen(self._info), field_maxlen(self._info)
|
139
|
+
if max_length:
|
140
|
+
self._main_widget.setMaxLength(max_length)
|
141
|
+
self._main_widget.setToolTip(
|
142
|
+
f"(length min: {min_length} max: {max_length}){self._describe()}"
|
143
|
+
)
|
144
|
+
if self._default:
|
145
|
+
self._main_widget.setText(self._default)
|
146
|
+
self._add_clear_button()
|
147
|
+
|
148
|
+
def getValue(self):
|
149
|
+
if self._main_widget.text() == "":
|
150
|
+
return self._default
|
151
|
+
return self._main_widget.text()
|
152
|
+
|
153
|
+
def setValue(self, value: str):
|
154
|
+
if value is None:
|
155
|
+
self._main_widget.setText("")
|
156
|
+
self._main_widget.setText(value)
|
157
|
+
|
158
|
+
|
159
|
+
class IntMetadataField(MetadataWidget):
|
160
|
+
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
161
|
+
super().__init__(info, parent)
|
162
|
+
self._main_widget.textChanged.connect(self._value_changed)
|
163
|
+
|
164
|
+
def _add_main_widget(self) -> None:
|
165
|
+
self._main_widget = QSpinBox()
|
166
|
+
self._layout.addWidget(self._main_widget)
|
167
|
+
min_, max_ = field_limits(self._info, int)
|
168
|
+
self._main_widget.setMinimum(min_)
|
169
|
+
self._main_widget.setMaximum(max_)
|
170
|
+
self._main_widget.setToolTip(f"(range {min_} to {max_}){self._describe()}")
|
171
|
+
if self._default is not None:
|
172
|
+
self._main_widget.setValue(self._default)
|
173
|
+
self._add_clear_button()
|
174
|
+
else:
|
175
|
+
self._main_widget.clear()
|
176
|
+
|
177
|
+
def getValue(self):
|
178
|
+
if self._main_widget.text() == "":
|
179
|
+
return self._default
|
180
|
+
return self._main_widget.value()
|
181
|
+
|
182
|
+
def setValue(self, value: int):
|
183
|
+
if value is None:
|
184
|
+
self._main_widget.clear()
|
185
|
+
self._main_widget.setValue(value)
|
186
|
+
|
187
|
+
|
188
|
+
class FloatDecimalMetadataField(MetadataWidget):
|
189
|
+
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
190
|
+
super().__init__(info, parent)
|
191
|
+
self._main_widget.textChanged.connect(self._value_changed)
|
192
|
+
|
193
|
+
def _add_main_widget(self) -> None:
|
194
|
+
self._main_widget = QDoubleSpinBox()
|
195
|
+
self._layout.addWidget(self._main_widget)
|
196
|
+
min_, max_ = field_limits(self._info, int)
|
197
|
+
self._main_widget.setMinimum(min_)
|
198
|
+
self._main_widget.setMaximum(max_)
|
199
|
+
precision = field_precision(self._info)
|
200
|
+
if precision:
|
201
|
+
self._main_widget.setDecimals(precision)
|
202
|
+
minstr = f"{float(min_):.3f}" if abs(min_) <= 1000 else f"{float(min_):.3e}"
|
203
|
+
maxstr = f"{float(max_):.3f}" if abs(max_) <= 1000 else f"{float(max_):.3e}"
|
204
|
+
self._main_widget.setToolTip(f"(range {minstr} to {maxstr}){self._describe()}")
|
205
|
+
if self._default is not None:
|
206
|
+
self._main_widget.setValue(self._default)
|
207
|
+
self._add_clear_button()
|
208
|
+
else:
|
209
|
+
self._main_widget.clear()
|
210
|
+
|
211
|
+
def getValue(self):
|
212
|
+
if self._main_widget.text() == "":
|
213
|
+
return self._default
|
214
|
+
return self._main_widget.value()
|
215
|
+
|
216
|
+
def setValue(self, value: float):
|
217
|
+
if value is None:
|
218
|
+
self._main_widget.clear()
|
219
|
+
self._main_widget.setValue(value)
|
220
|
+
|
221
|
+
|
222
|
+
class BoolMetadataField(MetadataWidget):
|
223
|
+
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
224
|
+
super().__init__(info, parent)
|
225
|
+
self._main_widget.stateChanged.connect(self._value_changed)
|
226
|
+
|
227
|
+
def _add_main_widget(self) -> None:
|
228
|
+
if clearable_required(self._info):
|
229
|
+
self._main_widget = ClearableBoolEntry()
|
230
|
+
else:
|
231
|
+
self._main_widget = QCheckBox()
|
232
|
+
self._layout.addWidget(self._main_widget)
|
233
|
+
self._main_widget.setToolTip(self._describe(""))
|
234
|
+
self._main_widget.setChecked(self._default) # type: ignore # if there is no default then it will be ClearableBoolEntry and can be set with None
|
235
|
+
|
236
|
+
def getValue(self):
|
237
|
+
return self._main_widget.isChecked()
|
238
|
+
|
239
|
+
def setValue(self, value):
|
240
|
+
self._main_widget.setChecked(value)
|
241
|
+
|
242
|
+
|
243
|
+
def widget_from_type(annotation: type | None) -> Callable[[FieldInfo], MetadataWidget]:
|
244
|
+
if annotation in [str, str | None]:
|
245
|
+
return StrMetadataField
|
246
|
+
if annotation in [int, int | None]:
|
247
|
+
return IntMetadataField
|
248
|
+
if annotation in [float, float | None, Decimal, Decimal | None]:
|
249
|
+
return FloatDecimalMetadataField
|
250
|
+
if annotation in [bool, bool | None]:
|
251
|
+
return BoolMetadataField
|
252
|
+
else:
|
253
|
+
logger.warning(f"Type {annotation} is not (yet) supported in metadata form creation.")
|
254
|
+
return StrMetadataField
|
255
|
+
|
256
|
+
|
257
|
+
if __name__ == "__main__": # pragma: no cover
|
258
|
+
|
259
|
+
class TestModel(BaseModel):
|
260
|
+
value1: str | None = Field(None)
|
261
|
+
value2: bool | None = Field(None)
|
262
|
+
value3: bool = Field(True)
|
263
|
+
value4: int = Field(123)
|
264
|
+
value5: int | None = Field()
|
265
|
+
|
266
|
+
app = QApplication([])
|
267
|
+
w = QWidget()
|
268
|
+
layout = QGridLayout()
|
269
|
+
w.setLayout(layout)
|
270
|
+
for i, (field_name, info) in enumerate(TestModel.model_fields.items()):
|
271
|
+
layout.addWidget(QLabel(field_name), i, 0)
|
272
|
+
layout.addWidget(widget_from_type(info.annotation)(info), i, 1)
|
273
|
+
|
274
|
+
w.show()
|
275
|
+
app.exec()
|
@@ -0,0 +1,67 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import sys
|
4
|
+
from decimal import Decimal
|
5
|
+
from math import inf, nextafter
|
6
|
+
from typing import TYPE_CHECKING, TypeVar, get_args
|
7
|
+
|
8
|
+
from annotated_types import Ge, Gt, Le, Lt
|
9
|
+
from bec_lib.logger import bec_logger
|
10
|
+
from pydantic_core import PydanticUndefined
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from pydantic.fields import FieldInfo
|
14
|
+
|
15
|
+
logger = bec_logger.logger
|
16
|
+
|
17
|
+
|
18
|
+
_MININT = -2147483648
|
19
|
+
_MAXINT = 2147483647
|
20
|
+
_MINFLOAT = -sys.float_info.max
|
21
|
+
_MAXFLOAT = sys.float_info.max
|
22
|
+
|
23
|
+
T = TypeVar("T", int, float, Decimal)
|
24
|
+
|
25
|
+
|
26
|
+
def field_limits(info: FieldInfo, type_: type[T]) -> tuple[T, T]:
|
27
|
+
_min = _MININT if type_ is int else _MINFLOAT
|
28
|
+
_max = _MAXINT if type_ is int else _MAXFLOAT
|
29
|
+
for md in info.metadata:
|
30
|
+
if isinstance(md, Ge):
|
31
|
+
_min = type_(md.ge) # type: ignore
|
32
|
+
if isinstance(md, Gt):
|
33
|
+
_min = type_(md.gt) + 1 if type_ is int else nextafter(type_(md.gt), inf) # type: ignore
|
34
|
+
if isinstance(md, Lt):
|
35
|
+
_max = type_(md.lt) - 1 if type_ is int else nextafter(type_(md.lt), -inf) # type: ignore
|
36
|
+
if isinstance(md, Le):
|
37
|
+
_max = type_(md.le) # type: ignore
|
38
|
+
return _min, _max # type: ignore
|
39
|
+
|
40
|
+
|
41
|
+
def _get_anno(info: FieldInfo, annotation: str, default):
|
42
|
+
for md in info.metadata:
|
43
|
+
if hasattr(md, annotation):
|
44
|
+
return getattr(md, annotation)
|
45
|
+
return default
|
46
|
+
|
47
|
+
|
48
|
+
def field_precision(info: FieldInfo):
|
49
|
+
return _get_anno(info, "decimal_places", 307)
|
50
|
+
|
51
|
+
|
52
|
+
def field_maxlen(info: FieldInfo):
|
53
|
+
return _get_anno(info, "max_length", None)
|
54
|
+
|
55
|
+
|
56
|
+
def field_minlen(info: FieldInfo):
|
57
|
+
return _get_anno(info, "min_length", None)
|
58
|
+
|
59
|
+
|
60
|
+
def field_default(info: FieldInfo):
|
61
|
+
if info.default is PydanticUndefined:
|
62
|
+
return
|
63
|
+
return info.default
|
64
|
+
|
65
|
+
|
66
|
+
def clearable_required(info: FieldInfo):
|
67
|
+
return type(None) in get_args(info.annotation) or info.is_required()
|
@@ -0,0 +1,146 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal # type: ignore
|
6
|
+
from qtpy.QtWidgets import (
|
7
|
+
QApplication,
|
8
|
+
QHBoxLayout,
|
9
|
+
QLabel,
|
10
|
+
QPushButton,
|
11
|
+
QTableView,
|
12
|
+
QVBoxLayout,
|
13
|
+
QWidget,
|
14
|
+
)
|
15
|
+
|
16
|
+
from bec_widgets.qt_utils.error_popups import SafeSlot
|
17
|
+
|
18
|
+
|
19
|
+
class AdditionalMetadataTableModel(QAbstractTableModel):
|
20
|
+
def __init__(self, data):
|
21
|
+
super().__init__()
|
22
|
+
self._data: list[list[str]] = data
|
23
|
+
self._disallowed_keys: list[str] = []
|
24
|
+
|
25
|
+
def headerData(
|
26
|
+
self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole()
|
27
|
+
) -> Any:
|
28
|
+
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
|
29
|
+
return "Key" if section == 0 else "Value"
|
30
|
+
return super().headerData(section, orientation, role)
|
31
|
+
|
32
|
+
def rowCount(self, index: QModelIndex = QModelIndex()):
|
33
|
+
return 0 if index.isValid() else len(self._data)
|
34
|
+
|
35
|
+
def columnCount(self, index: QModelIndex = QModelIndex()):
|
36
|
+
return 0 if index.isValid() else 2
|
37
|
+
|
38
|
+
def data(self, index, role=Qt.ItemDataRole):
|
39
|
+
if index.isValid():
|
40
|
+
if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole:
|
41
|
+
return str(self._data[index.row()][index.column()])
|
42
|
+
|
43
|
+
def setData(self, index, value, role):
|
44
|
+
if role == Qt.ItemDataRole.EditRole:
|
45
|
+
if value in self._disallowed_keys or value in self._other_keys(index.row()):
|
46
|
+
return False
|
47
|
+
self._data[index.row()][index.column()] = str(value)
|
48
|
+
return True
|
49
|
+
return False
|
50
|
+
|
51
|
+
def update_disallowed_keys(self, keys: list[str]):
|
52
|
+
self._disallowed_keys = keys
|
53
|
+
for i, item in enumerate(self._data):
|
54
|
+
if item[0] in self._disallowed_keys:
|
55
|
+
self._data[i][0] = ""
|
56
|
+
self.dataChanged.emit(self.index(i, 0), self.index(i, 0))
|
57
|
+
|
58
|
+
def _other_keys(self, row: int):
|
59
|
+
return [r[0] for r in self._data[:row] + self._data[row + 1 :]]
|
60
|
+
|
61
|
+
def flags(self, _):
|
62
|
+
return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable
|
63
|
+
|
64
|
+
def insertRows(self, row, number, index):
|
65
|
+
"""We only support adding one at a time for now"""
|
66
|
+
if row != self.rowCount() or number != 1:
|
67
|
+
return False
|
68
|
+
self.beginInsertRows(QModelIndex(), 0, 0)
|
69
|
+
self._data.append(["", ""])
|
70
|
+
self.endInsertRows()
|
71
|
+
return True
|
72
|
+
|
73
|
+
def removeRows(self, row, number, index):
|
74
|
+
"""This can only be consecutive, so instead of trying to be clever, only support removing one at a time"""
|
75
|
+
if number != 1:
|
76
|
+
return False
|
77
|
+
self.beginRemoveRows(QModelIndex(), row, row)
|
78
|
+
del self._data[row]
|
79
|
+
self.endRemoveRows()
|
80
|
+
return True
|
81
|
+
|
82
|
+
@SafeSlot()
|
83
|
+
def add_row(self):
|
84
|
+
self.insertRow(self.rowCount())
|
85
|
+
|
86
|
+
@SafeSlot(list)
|
87
|
+
def delete_rows(self, rows: list[int]):
|
88
|
+
# delete from the end so indices stay correct
|
89
|
+
for row in sorted(rows, reverse=True):
|
90
|
+
self.removeRows(row, 1, QModelIndex())
|
91
|
+
|
92
|
+
def dump_dict(self):
|
93
|
+
if self._data == [[]]:
|
94
|
+
return {}
|
95
|
+
return dict(self._data)
|
96
|
+
|
97
|
+
|
98
|
+
class AdditionalMetadataTable(QWidget):
|
99
|
+
|
100
|
+
delete_rows = Signal(list)
|
101
|
+
|
102
|
+
def __init__(self, initial_data: list[list[str]]):
|
103
|
+
super().__init__()
|
104
|
+
|
105
|
+
self._layout = QHBoxLayout()
|
106
|
+
self.setLayout(self._layout)
|
107
|
+
self._table_model = AdditionalMetadataTableModel(initial_data)
|
108
|
+
self._table_view = QTableView()
|
109
|
+
self._table_view.setModel(self._table_model)
|
110
|
+
self._table_view.horizontalHeader().setStretchLastSection(True)
|
111
|
+
self._layout.addWidget(self._table_view)
|
112
|
+
|
113
|
+
self._buttons = QVBoxLayout()
|
114
|
+
self._layout.addLayout(self._buttons)
|
115
|
+
self._add_button = QPushButton("+")
|
116
|
+
self._add_button.setToolTip("add a new row")
|
117
|
+
self._remove_button = QPushButton("-")
|
118
|
+
self._remove_button.setToolTip("delete rows containing any selected cells")
|
119
|
+
self._buttons.addWidget(self._add_button)
|
120
|
+
self._buttons.addWidget(self._remove_button)
|
121
|
+
self._add_button.clicked.connect(self._table_model.add_row)
|
122
|
+
self._remove_button.clicked.connect(self.delete_selected_rows)
|
123
|
+
self.delete_rows.connect(self._table_model.delete_rows)
|
124
|
+
|
125
|
+
def delete_selected_rows(self):
|
126
|
+
cells: list[QModelIndex] = self._table_view.selectionModel().selectedIndexes()
|
127
|
+
row_indices = list({r.row() for r in cells})
|
128
|
+
if row_indices:
|
129
|
+
self.delete_rows.emit(row_indices)
|
130
|
+
|
131
|
+
def dump_dict(self):
|
132
|
+
return self._table_model.dump_dict()
|
133
|
+
|
134
|
+
def update_disallowed_keys(self, keys: list[str]):
|
135
|
+
self._table_model.update_disallowed_keys(keys)
|
136
|
+
|
137
|
+
|
138
|
+
if __name__ == "__main__": # pragma: no cover
|
139
|
+
from bec_widgets.utils.colors import set_theme
|
140
|
+
|
141
|
+
app = QApplication([])
|
142
|
+
set_theme("dark")
|
143
|
+
|
144
|
+
window = AdditionalMetadataTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
|
145
|
+
window.show()
|
146
|
+
app.exec()
|