bec-widgets 2.15.0__py3-none-any.whl → 2.16.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 +72 -0
- PKG-INFO +1 -1
- bec_widgets/tests/utils.py +2 -2
- bec_widgets/utils/expandable_frame.py +12 -7
- bec_widgets/utils/forms_from_types/forms.py +40 -22
- bec_widgets/utils/forms_from_types/items.py +282 -32
- bec_widgets/widgets/containers/main_window/addons/scroll_label.py +24 -3
- bec_widgets/widgets/containers/main_window/main_window.py +32 -1
- bec_widgets/widgets/editors/dict_backed_table.py +69 -9
- bec_widgets/widgets/editors/scan_metadata/_util.py +3 -1
- bec_widgets/widgets/services/device_browser/device_browser.py +5 -6
- bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py +254 -0
- bec_widgets/widgets/services/device_browser/device_item/device_config_form.py +60 -0
- bec_widgets/widgets/services/device_browser/device_item/device_item.py +52 -54
- bec_widgets/widgets/utility/toggle/toggle.py +9 -0
- {bec_widgets-2.15.0.dist-info → bec_widgets-2.16.0.dist-info}/METADATA +1 -1
- {bec_widgets-2.15.0.dist-info → bec_widgets-2.16.0.dist-info}/RECORD +21 -19
- pyproject.toml +1 -1
- {bec_widgets-2.15.0.dist-info → bec_widgets-2.16.0.dist-info}/WHEEL +0 -0
- {bec_widgets-2.15.0.dist-info → bec_widgets-2.16.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-2.15.0.dist-info → bec_widgets-2.16.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,6 +4,7 @@ from typing import Any
|
|
4
4
|
|
5
5
|
from qtpy import QtWidgets
|
6
6
|
from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal # type: ignore
|
7
|
+
from qtpy.QtGui import QFontMetrics
|
7
8
|
from qtpy.QtWidgets import (
|
8
9
|
QApplication,
|
9
10
|
QHBoxLayout,
|
@@ -14,7 +15,9 @@ from qtpy.QtWidgets import (
|
|
14
15
|
QWidget,
|
15
16
|
)
|
16
17
|
|
17
|
-
from bec_widgets.utils.error_popups import SafeSlot
|
18
|
+
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
19
|
+
|
20
|
+
_NOT_SET = object()
|
18
21
|
|
19
22
|
|
20
23
|
class DictBackedTableModel(QAbstractTableModel):
|
@@ -26,6 +29,7 @@ class DictBackedTableModel(QAbstractTableModel):
|
|
26
29
|
data (list[list[str]]): list of key-value pairs to initialise with"""
|
27
30
|
super().__init__()
|
28
31
|
self._data: list[list[str]] = data
|
32
|
+
self._default = _NOT_SET
|
29
33
|
self._disallowed_keys: list[str] = []
|
30
34
|
|
31
35
|
# pylint: disable=missing-function-docstring
|
@@ -51,7 +55,10 @@ class DictBackedTableModel(QAbstractTableModel):
|
|
51
55
|
Qt.ItemDataRole.EditRole,
|
52
56
|
Qt.ItemDataRole.ToolTipRole,
|
53
57
|
]:
|
54
|
-
|
58
|
+
try:
|
59
|
+
return str(self._data[index.row()][index.column()])
|
60
|
+
except IndexError:
|
61
|
+
return None
|
55
62
|
|
56
63
|
def setData(self, index, value, role):
|
57
64
|
if role == Qt.ItemDataRole.EditRole:
|
@@ -63,9 +70,10 @@ class DictBackedTableModel(QAbstractTableModel):
|
|
63
70
|
return False
|
64
71
|
|
65
72
|
def replaceData(self, data: dict):
|
73
|
+
self.delete_rows(list(range(len(self._data))))
|
66
74
|
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),
|
75
|
+
self._data = [[str(k), str(v)] for k, v in data.items()]
|
76
|
+
self.dataChanged.emit(self.index(0, 0), self.index(len(self._data), 1))
|
69
77
|
|
70
78
|
def update_disallowed_keys(self, keys: list[str]):
|
71
79
|
"""Set the list of keys which may not be used.
|
@@ -76,7 +84,7 @@ class DictBackedTableModel(QAbstractTableModel):
|
|
76
84
|
for i, item in enumerate(self._data):
|
77
85
|
if item[0] in self._disallowed_keys:
|
78
86
|
self._data[i][0] = ""
|
79
|
-
self.dataChanged.emit(self.index(i, 0), self.index(i,
|
87
|
+
self.dataChanged.emit(self.index(i, 0), self.index(i, 1))
|
80
88
|
|
81
89
|
def _other_keys(self, row: int):
|
82
90
|
return [r[0] for r in self._data[:row] + self._data[row + 1 :]]
|
@@ -105,24 +113,39 @@ class DictBackedTableModel(QAbstractTableModel):
|
|
105
113
|
@SafeSlot()
|
106
114
|
def add_row(self):
|
107
115
|
self.insertRow(self.rowCount())
|
116
|
+
self.dataChanged.emit(self.index(self.rowCount(), 0), self.index(self.rowCount(), 1), 0)
|
108
117
|
|
109
118
|
@SafeSlot(list)
|
110
119
|
def delete_rows(self, rows: list[int]):
|
111
120
|
# delete from the end so indices stay correct
|
112
121
|
for row in sorted(rows, reverse=True):
|
122
|
+
self.dataChanged.emit(self.index(row, 0), self.index(row, 1), 0)
|
113
123
|
self.removeRows(row, 1, QModelIndex())
|
114
124
|
|
125
|
+
def set_default(self, value: dict | None):
|
126
|
+
self._default = value
|
127
|
+
|
115
128
|
def dump_dict(self):
|
116
|
-
if self._data
|
129
|
+
if self._data in [[], [[]], [["", ""]]]:
|
130
|
+
if self._default is not _NOT_SET:
|
131
|
+
return self._default
|
117
132
|
return {}
|
118
133
|
return dict(self._data)
|
119
134
|
|
135
|
+
def length(self):
|
136
|
+
return len(self._data)
|
137
|
+
|
120
138
|
|
121
139
|
class DictBackedTable(QWidget):
|
122
140
|
delete_rows = Signal(list)
|
123
141
|
data_changed = Signal(dict)
|
124
142
|
|
125
|
-
def __init__(
|
143
|
+
def __init__(
|
144
|
+
self,
|
145
|
+
parent: QWidget | None = None,
|
146
|
+
initial_data: list[list[str]] = [],
|
147
|
+
autoscale_to_data: bool = True,
|
148
|
+
):
|
126
149
|
"""Widget which uses a DictBackedTableModel to display an editable table
|
127
150
|
which can be extracted as a dict.
|
128
151
|
|
@@ -133,15 +156,25 @@ class DictBackedTable(QWidget):
|
|
133
156
|
|
134
157
|
self._layout = QHBoxLayout()
|
135
158
|
self.setLayout(self._layout)
|
159
|
+
self._layout.setContentsMargins(0, 0, 0, 0)
|
160
|
+
|
136
161
|
self._table_model = DictBackedTableModel(initial_data)
|
137
162
|
self._table_view = QTreeView()
|
163
|
+
|
138
164
|
self._table_view.setModel(self._table_model)
|
165
|
+
self._min_lines = 3
|
166
|
+
self.set_height_in_lines(len(initial_data))
|
139
167
|
self._table_view.setSizePolicy(
|
140
168
|
QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
141
169
|
)
|
142
170
|
self._table_view.setAlternatingRowColors(True)
|
171
|
+
self._table_view.setUniformRowHeights(True)
|
172
|
+
self._table_view.setWordWrap(False)
|
143
173
|
self._table_view.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
|
144
174
|
self._table_view.header().setSectionResizeMode(5, QtWidgets.QHeaderView.Stretch)
|
175
|
+
self.autoscale = autoscale_to_data
|
176
|
+
if self.autoscale:
|
177
|
+
self.data_changed.connect(self.scale_to_data)
|
145
178
|
self._layout.addWidget(self._table_view)
|
146
179
|
|
147
180
|
self._button_holder = QWidget()
|
@@ -157,8 +190,12 @@ class DictBackedTable(QWidget):
|
|
157
190
|
self._add_button.clicked.connect(self._table_model.add_row)
|
158
191
|
self._remove_button.clicked.connect(self.delete_selected_rows)
|
159
192
|
self.delete_rows.connect(self._table_model.delete_rows)
|
193
|
+
|
160
194
|
self._table_model.dataChanged.connect(lambda *_: self.data_changed.emit(self.dump_dict()))
|
161
195
|
|
196
|
+
def set_default(self, value: dict | None):
|
197
|
+
self._table_model.set_default(value)
|
198
|
+
|
162
199
|
def set_button_visibility(self, value: bool):
|
163
200
|
self._button_holder.setVisible(value)
|
164
201
|
|
@@ -166,8 +203,8 @@ class DictBackedTable(QWidget):
|
|
166
203
|
def clear(self):
|
167
204
|
self._table_model.replaceData({})
|
168
205
|
|
169
|
-
def replace_data(self, data: dict):
|
170
|
-
self._table_model.replaceData(data)
|
206
|
+
def replace_data(self, data: dict | None):
|
207
|
+
self._table_model.replaceData(data or {})
|
171
208
|
|
172
209
|
def delete_selected_rows(self):
|
173
210
|
"""Delete rows which are part of the selection model"""
|
@@ -187,6 +224,29 @@ class DictBackedTable(QWidget):
|
|
187
224
|
keys (list[str]): list of keys which are forbidden."""
|
188
225
|
self._table_model.update_disallowed_keys(keys)
|
189
226
|
|
227
|
+
def set_height_in_lines(self, lines: int):
|
228
|
+
self._table_view.setMaximumHeight(
|
229
|
+
int(QFontMetrics(self._table_view.font()).height() * max(lines + 2, self._min_lines))
|
230
|
+
)
|
231
|
+
|
232
|
+
@SafeSlot()
|
233
|
+
@SafeSlot(dict)
|
234
|
+
def scale_to_data(self, *_):
|
235
|
+
self.set_height_in_lines(self._table_model.length())
|
236
|
+
|
237
|
+
@SafeProperty(bool)
|
238
|
+
def autoscale(self): # type: ignore
|
239
|
+
return self._autoscale
|
240
|
+
|
241
|
+
@autoscale.setter
|
242
|
+
def autoscale(self, autoscale: bool):
|
243
|
+
self._autoscale = autoscale
|
244
|
+
if self._autoscale:
|
245
|
+
self.scale_to_data()
|
246
|
+
self.data_changed.connect(self.scale_to_data)
|
247
|
+
else:
|
248
|
+
self.data_changed.disconnect(self.scale_to_data)
|
249
|
+
|
190
250
|
|
191
251
|
if __name__ == "__main__": # pragma: no cover
|
192
252
|
from bec_widgets.utils.colors import set_theme
|
@@ -67,4 +67,6 @@ def field_default(info: FieldInfo):
|
|
67
67
|
|
68
68
|
|
69
69
|
def clearable_required(info: FieldInfo):
|
70
|
-
return type(None) in get_args(info.annotation) or
|
70
|
+
return type(None) in get_args(info.annotation) or (
|
71
|
+
info.is_required() and info.default is PydanticUndefined
|
72
|
+
)
|
@@ -87,14 +87,13 @@ class DeviceBrowser(BECWidget, QWidget):
|
|
87
87
|
for device, device_obj in self.dev.items():
|
88
88
|
item = QListWidgetItem(self.dev_list)
|
89
89
|
device_item = DeviceItem(
|
90
|
-
parent=self,
|
90
|
+
parent=self,
|
91
|
+
device=device,
|
92
|
+
devices=self.dev,
|
93
|
+
icon=map_device_type_to_icon(device_obj),
|
91
94
|
)
|
92
|
-
|
93
95
|
device_item.expansion_state_changed.connect(partial(_updatesize, item, device_item))
|
94
|
-
|
95
|
-
device_config = self.dev[device]._config # pylint: disable=protected-access
|
96
|
-
device_item.set_display_config(device_config)
|
97
|
-
tooltip = device_config.get("description", "")
|
96
|
+
tooltip = self.dev[device]._config.get("description", "")
|
98
97
|
device_item.setToolTip(tooltip)
|
99
98
|
device_item.broadcast_size_hint.connect(item.setSizeHint)
|
100
99
|
item.setSizeHint(device_item.sizeHint())
|
@@ -0,0 +1,254 @@
|
|
1
|
+
from ast import literal_eval
|
2
|
+
|
3
|
+
from bec_lib.atlas_models import Device as DeviceConfigModel
|
4
|
+
from bec_lib.config_helper import CONF as DEVICE_CONF_KEYS
|
5
|
+
from bec_lib.config_helper import ConfigHelper
|
6
|
+
from bec_lib.logger import bec_logger
|
7
|
+
from qtpy.QtCore import QObject, QRunnable, QSize, Qt, QThreadPool, Signal
|
8
|
+
from qtpy.QtWidgets import (
|
9
|
+
QApplication,
|
10
|
+
QDialog,
|
11
|
+
QDialogButtonBox,
|
12
|
+
QLabel,
|
13
|
+
QStackedLayout,
|
14
|
+
QVBoxLayout,
|
15
|
+
QWidget,
|
16
|
+
)
|
17
|
+
|
18
|
+
from bec_widgets.utils.bec_widget import BECWidget
|
19
|
+
from bec_widgets.utils.error_popups import SafeSlot
|
20
|
+
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
|
21
|
+
DeviceConfigForm,
|
22
|
+
)
|
23
|
+
from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
|
24
|
+
|
25
|
+
logger = bec_logger.logger
|
26
|
+
|
27
|
+
|
28
|
+
class _CommSignals(QObject):
|
29
|
+
error = Signal(Exception)
|
30
|
+
done = Signal()
|
31
|
+
|
32
|
+
|
33
|
+
class _CommunicateUpdate(QRunnable):
|
34
|
+
|
35
|
+
def __init__(self, config_helper: ConfigHelper, device: str, config: dict) -> None:
|
36
|
+
super().__init__()
|
37
|
+
self.config_helper = config_helper
|
38
|
+
self.device = device
|
39
|
+
self.config = config
|
40
|
+
self.signals = _CommSignals()
|
41
|
+
|
42
|
+
@SafeSlot()
|
43
|
+
def run(self):
|
44
|
+
try:
|
45
|
+
timeout = self.config_helper.suggested_timeout_s(self.config)
|
46
|
+
RID = self.config_helper.send_config_request(
|
47
|
+
action="update", config={self.device: self.config}, wait_for_response=False
|
48
|
+
)
|
49
|
+
logger.info("Waiting for config reply")
|
50
|
+
reply = self.config_helper.wait_for_config_reply(RID, timeout=timeout)
|
51
|
+
self.config_helper.handle_update_reply(reply, RID, timeout)
|
52
|
+
logger.info("Done updating config!")
|
53
|
+
except Exception as e:
|
54
|
+
self.signals.error.emit(e)
|
55
|
+
finally:
|
56
|
+
self.signals.done.emit()
|
57
|
+
|
58
|
+
|
59
|
+
class DeviceConfigDialog(BECWidget, QDialog):
|
60
|
+
RPC = False
|
61
|
+
applied = Signal()
|
62
|
+
|
63
|
+
def __init__(
|
64
|
+
self,
|
65
|
+
parent=None,
|
66
|
+
device: str | None = None,
|
67
|
+
config_helper: ConfigHelper | None = None,
|
68
|
+
**kwargs,
|
69
|
+
):
|
70
|
+
super().__init__(parent=parent, **kwargs)
|
71
|
+
self._config_helper = config_helper or ConfigHelper(
|
72
|
+
self.client.connector, self.client._service_name
|
73
|
+
)
|
74
|
+
self.threadpool = QThreadPool()
|
75
|
+
self._device = device
|
76
|
+
self.setWindowTitle(f"Edit config for: {device}")
|
77
|
+
self._container = QStackedLayout()
|
78
|
+
self._container.setStackingMode(QStackedLayout.StackAll)
|
79
|
+
|
80
|
+
self._layout = QVBoxLayout()
|
81
|
+
user_warning = QLabel(
|
82
|
+
"Warning: edit items here at your own risk - minimal validation is applied to the entered values.\n"
|
83
|
+
"Items in the deviceConfig dictionary should correspond to python literals, e.g. numbers, lists, strings (including quotes), etc."
|
84
|
+
)
|
85
|
+
user_warning.setWordWrap(True)
|
86
|
+
user_warning.setStyleSheet("QLabel { color: red; }")
|
87
|
+
self._layout.addWidget(user_warning)
|
88
|
+
self._add_form()
|
89
|
+
self._add_overlay()
|
90
|
+
self._add_buttons()
|
91
|
+
|
92
|
+
self.setLayout(self._container)
|
93
|
+
self._overlay_widget.setVisible(False)
|
94
|
+
|
95
|
+
def _add_form(self):
|
96
|
+
self._form_widget = QWidget()
|
97
|
+
self._form_widget.setLayout(self._layout)
|
98
|
+
self._form = DeviceConfigForm()
|
99
|
+
self._layout.addWidget(self._form)
|
100
|
+
|
101
|
+
for row in self._form.enumerate_form_widgets():
|
102
|
+
if row.label.property("_model_field_name") in DEVICE_CONF_KEYS.NON_UPDATABLE:
|
103
|
+
row.widget._set_pretty_display()
|
104
|
+
|
105
|
+
self._fetch_config()
|
106
|
+
self._fill_form()
|
107
|
+
self._container.addWidget(self._form_widget)
|
108
|
+
|
109
|
+
def _add_overlay(self):
|
110
|
+
self._overlay_widget = QWidget()
|
111
|
+
self._overlay_widget.setStyleSheet("background-color:rgba(128,128,128,128);")
|
112
|
+
self._overlay_widget.setAutoFillBackground(True)
|
113
|
+
self._overlay_layout = QVBoxLayout()
|
114
|
+
self._overlay_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
115
|
+
self._overlay_widget.setLayout(self._overlay_layout)
|
116
|
+
|
117
|
+
self._spinner = SpinnerWidget(parent=self)
|
118
|
+
self._spinner.setMinimumSize(QSize(100, 100))
|
119
|
+
self._overlay_layout.addWidget(self._spinner)
|
120
|
+
self._container.addWidget(self._overlay_widget)
|
121
|
+
|
122
|
+
def _add_buttons(self):
|
123
|
+
button_box = QDialogButtonBox(
|
124
|
+
QDialogButtonBox.Apply | QDialogButtonBox.Ok | QDialogButtonBox.Cancel
|
125
|
+
)
|
126
|
+
button_box.button(QDialogButtonBox.Apply).clicked.connect(self.apply)
|
127
|
+
button_box.accepted.connect(self.accept)
|
128
|
+
button_box.rejected.connect(self.reject)
|
129
|
+
self._layout.addWidget(button_box)
|
130
|
+
|
131
|
+
def _fetch_config(self):
|
132
|
+
self._initial_config = {}
|
133
|
+
if (
|
134
|
+
self.client.device_manager is not None
|
135
|
+
and self._device in self.client.device_manager.devices
|
136
|
+
):
|
137
|
+
self._initial_config = self.client.device_manager.devices.get(self._device)._config
|
138
|
+
|
139
|
+
def _fill_form(self):
|
140
|
+
self._form.set_data(DeviceConfigModel.model_validate(self._initial_config))
|
141
|
+
|
142
|
+
def updated_config(self):
|
143
|
+
new_config = self._form.get_form_data()
|
144
|
+
diff = {
|
145
|
+
k: v for k, v in new_config.items() if self._initial_config.get(k) != new_config.get(k)
|
146
|
+
}
|
147
|
+
if diff.get("deviceConfig") is not None:
|
148
|
+
# TODO: special cased in some parts of device manager but not others, should
|
149
|
+
# be removed in config update as with below issue
|
150
|
+
diff["deviceConfig"].pop("device_access", None)
|
151
|
+
# TODO: replace when https://github.com/bec-project/bec/issues/528 is resolved
|
152
|
+
diff["deviceConfig"] = {
|
153
|
+
k: literal_eval(str(v)) for k, v in diff["deviceConfig"].items()
|
154
|
+
}
|
155
|
+
return diff
|
156
|
+
|
157
|
+
@SafeSlot()
|
158
|
+
def apply(self):
|
159
|
+
self._process_update_action()
|
160
|
+
self.applied.emit()
|
161
|
+
|
162
|
+
@SafeSlot()
|
163
|
+
def accept(self):
|
164
|
+
self._process_update_action()
|
165
|
+
return super().accept()
|
166
|
+
|
167
|
+
def _process_update_action(self):
|
168
|
+
updated_config = self.updated_config()
|
169
|
+
if (device_name := updated_config.get("name")) == "":
|
170
|
+
logger.warning("Can't create a device with no name!")
|
171
|
+
elif set(updated_config.keys()) & set(DEVICE_CONF_KEYS.NON_UPDATABLE):
|
172
|
+
logger.info(
|
173
|
+
f"Removing old device {self._device} and adding new device {device_name or self._device} with modified config: {updated_config}"
|
174
|
+
)
|
175
|
+
else:
|
176
|
+
self._update_device_config(updated_config)
|
177
|
+
|
178
|
+
def _update_device_config(self, config: dict):
|
179
|
+
if self._device is None:
|
180
|
+
return
|
181
|
+
if config == {}:
|
182
|
+
logger.info("No changes made to device config")
|
183
|
+
return
|
184
|
+
logger.info(f"Sending request to update device config: {config}")
|
185
|
+
|
186
|
+
self._start_waiting_display()
|
187
|
+
communicate_update = _CommunicateUpdate(self._config_helper, self._device, config)
|
188
|
+
communicate_update.signals.error.connect(self.update_error)
|
189
|
+
communicate_update.signals.done.connect(self.update_done)
|
190
|
+
self.threadpool.start(communicate_update)
|
191
|
+
|
192
|
+
@SafeSlot()
|
193
|
+
def update_done(self):
|
194
|
+
self._stop_waiting_display()
|
195
|
+
self._fetch_config()
|
196
|
+
self._fill_form()
|
197
|
+
|
198
|
+
@SafeSlot(Exception, popup_error=True)
|
199
|
+
def update_error(self, e: Exception):
|
200
|
+
raise RuntimeError("Failed to update device configuration") from e
|
201
|
+
|
202
|
+
def _start_waiting_display(self):
|
203
|
+
self._overlay_widget.setVisible(True)
|
204
|
+
self._spinner.start()
|
205
|
+
QApplication.processEvents()
|
206
|
+
|
207
|
+
def _stop_waiting_display(self):
|
208
|
+
self._overlay_widget.setVisible(False)
|
209
|
+
self._spinner.stop()
|
210
|
+
QApplication.processEvents()
|
211
|
+
|
212
|
+
|
213
|
+
def main(): # pragma: no cover
|
214
|
+
import sys
|
215
|
+
|
216
|
+
from qtpy.QtWidgets import QApplication, QLineEdit, QPushButton, QWidget
|
217
|
+
|
218
|
+
from bec_widgets.utils.colors import set_theme
|
219
|
+
|
220
|
+
dialog = None
|
221
|
+
|
222
|
+
app = QApplication(sys.argv)
|
223
|
+
set_theme("light")
|
224
|
+
widget = QWidget()
|
225
|
+
widget.setLayout(QVBoxLayout())
|
226
|
+
|
227
|
+
device = QLineEdit()
|
228
|
+
widget.layout().addWidget(device)
|
229
|
+
|
230
|
+
def _destroy_dialog(*_):
|
231
|
+
nonlocal dialog
|
232
|
+
dialog = None
|
233
|
+
|
234
|
+
def accept(*args):
|
235
|
+
logger.success(f"submitted device config form {dialog} {args}")
|
236
|
+
_destroy_dialog()
|
237
|
+
|
238
|
+
def _show_dialog(*_):
|
239
|
+
nonlocal dialog
|
240
|
+
if dialog is None:
|
241
|
+
dialog = DeviceConfigDialog(device=device.text())
|
242
|
+
dialog.accepted.connect(accept)
|
243
|
+
dialog.rejected.connect(_destroy_dialog)
|
244
|
+
dialog.open()
|
245
|
+
|
246
|
+
button = QPushButton("Show device dialog")
|
247
|
+
widget.layout().addWidget(button)
|
248
|
+
button.clicked.connect(_show_dialog)
|
249
|
+
widget.show()
|
250
|
+
sys.exit(app.exec_())
|
251
|
+
|
252
|
+
|
253
|
+
if __name__ == "__main__":
|
254
|
+
main()
|
@@ -0,0 +1,60 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from bec_lib.atlas_models import Device as DeviceConfigModel
|
4
|
+
from pydantic import BaseModel
|
5
|
+
from qtpy.QtWidgets import QApplication
|
6
|
+
|
7
|
+
from bec_widgets.utils.colors import get_theme_name
|
8
|
+
from bec_widgets.utils.forms_from_types import styles
|
9
|
+
from bec_widgets.utils.forms_from_types.forms import PydanticModelForm
|
10
|
+
from bec_widgets.utils.forms_from_types.items import (
|
11
|
+
DEFAULT_WIDGET_TYPES,
|
12
|
+
BoolFormItem,
|
13
|
+
BoolToggleFormItem,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class DeviceConfigForm(PydanticModelForm):
|
18
|
+
RPC = False
|
19
|
+
PLUGIN = False
|
20
|
+
|
21
|
+
def __init__(self, parent=None, client=None, pretty_display=False, **kwargs):
|
22
|
+
super().__init__(
|
23
|
+
parent=parent,
|
24
|
+
data_model=DeviceConfigModel,
|
25
|
+
pretty_display=pretty_display,
|
26
|
+
client=client,
|
27
|
+
**kwargs,
|
28
|
+
)
|
29
|
+
self._widget_types = DEFAULT_WIDGET_TYPES.copy()
|
30
|
+
self._widget_types["bool"] = (lambda spec: spec.item_type is bool, BoolToggleFormItem)
|
31
|
+
self._widget_types["optional_bool"] = (
|
32
|
+
lambda spec: spec.item_type == bool | None,
|
33
|
+
BoolFormItem,
|
34
|
+
)
|
35
|
+
self._validity.setVisible(False)
|
36
|
+
self._connect_to_theme_change()
|
37
|
+
self.populate()
|
38
|
+
|
39
|
+
def _post_init(self): ...
|
40
|
+
|
41
|
+
def set_pretty_display_theme(self, theme: str | None = None):
|
42
|
+
if theme is None:
|
43
|
+
theme = get_theme_name()
|
44
|
+
self.setStyleSheet(styles.pretty_display_theme(theme))
|
45
|
+
|
46
|
+
def get_form_data(self):
|
47
|
+
"""Get the entered metadata as a dict."""
|
48
|
+
return self._md_schema.model_validate(super().get_form_data()).model_dump()
|
49
|
+
|
50
|
+
def _connect_to_theme_change(self):
|
51
|
+
"""Connect to the theme change signal."""
|
52
|
+
qapp = QApplication.instance()
|
53
|
+
if hasattr(qapp, "theme_signal"):
|
54
|
+
qapp.theme_signal.theme_updated.connect(self.set_pretty_display_theme) # type: ignore
|
55
|
+
|
56
|
+
def set_schema(self, schema: type[BaseModel]):
|
57
|
+
raise TypeError("This class doesn't support changing the schema")
|
58
|
+
|
59
|
+
def set_data(self, data: DeviceConfigModel): # type: ignore # This class locks the type
|
60
|
+
super().set_data(data)
|
@@ -3,49 +3,27 @@ from __future__ import annotations
|
|
3
3
|
from typing import TYPE_CHECKING
|
4
4
|
|
5
5
|
from bec_lib.atlas_models import Device as DeviceConfigModel
|
6
|
+
from bec_lib.devicemanager import DeviceContainer
|
6
7
|
from bec_lib.logger import bec_logger
|
8
|
+
from bec_qthemes import material_icon
|
7
9
|
from qtpy.QtCore import QMimeData, QSize, Qt, Signal
|
8
10
|
from qtpy.QtGui import QDrag
|
9
|
-
from qtpy.QtWidgets import QApplication, QHBoxLayout, QWidget
|
11
|
+
from qtpy.QtWidgets import QApplication, QHBoxLayout, QToolButton, QWidget
|
10
12
|
|
11
|
-
from bec_widgets.utils.colors import get_theme_name
|
12
13
|
from bec_widgets.utils.error_popups import SafeSlot
|
13
14
|
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
|
14
|
-
from bec_widgets.
|
15
|
-
|
16
|
-
|
15
|
+
from bec_widgets.widgets.services.device_browser.device_item.device_config_dialog import (
|
16
|
+
DeviceConfigDialog,
|
17
|
+
)
|
18
|
+
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
|
19
|
+
DeviceConfigForm,
|
20
|
+
)
|
17
21
|
|
18
22
|
if TYPE_CHECKING: # pragma: no cover
|
19
23
|
from qtpy.QtGui import QMouseEvent
|
20
24
|
|
21
|
-
logger = bec_logger.logger
|
22
|
-
|
23
|
-
|
24
|
-
class DeviceItemForm(PydanticModelForm):
|
25
|
-
RPC = False
|
26
|
-
PLUGIN = False
|
27
|
-
|
28
|
-
def __init__(self, parent=None, client=None, pretty_display=False, **kwargs):
|
29
|
-
super().__init__(
|
30
|
-
parent=parent,
|
31
|
-
data_model=DeviceConfigModel,
|
32
|
-
pretty_display=pretty_display,
|
33
|
-
client=client,
|
34
|
-
**kwargs,
|
35
|
-
)
|
36
|
-
self._validity.setVisible(False)
|
37
|
-
self._connect_to_theme_change()
|
38
|
-
|
39
|
-
def set_pretty_display_theme(self, theme: str | None = None):
|
40
|
-
if theme is None:
|
41
|
-
theme = get_theme_name()
|
42
|
-
self.setStyleSheet(styles.pretty_display_theme(theme))
|
43
25
|
|
44
|
-
|
45
|
-
"""Connect to the theme change signal."""
|
46
|
-
qapp = QApplication.instance()
|
47
|
-
if hasattr(qapp, "theme_signal"):
|
48
|
-
qapp.theme_signal.theme_updated.connect(self.set_pretty_display_theme) # type: ignore
|
26
|
+
logger = bec_logger.logger
|
49
27
|
|
50
28
|
|
51
29
|
class DeviceItem(ExpandableGroupFrame):
|
@@ -53,9 +31,9 @@ class DeviceItem(ExpandableGroupFrame):
|
|
53
31
|
|
54
32
|
RPC = False
|
55
33
|
|
56
|
-
def __init__(self, parent, device: str, icon: str = "") -> None:
|
34
|
+
def __init__(self, parent, device: str, devices: DeviceContainer, icon: str = "") -> None:
|
57
35
|
super().__init__(parent, title=device, expanded=False, icon=icon)
|
58
|
-
|
36
|
+
self.dev = devices
|
59
37
|
self._drag_pos = None
|
60
38
|
self._expanded_first_time = False
|
61
39
|
self._data = None
|
@@ -65,17 +43,29 @@ class DeviceItem(ExpandableGroupFrame):
|
|
65
43
|
self.set_layout(layout)
|
66
44
|
|
67
45
|
self.adjustSize()
|
68
|
-
|
69
|
-
|
46
|
+
|
47
|
+
def _create_title_layout(self, title: str, icon: str):
|
48
|
+
super()._create_title_layout(title, icon)
|
49
|
+
self.edit_button = QToolButton()
|
50
|
+
self.edit_button.setIcon(
|
51
|
+
material_icon(icon_name="edit", size=(10, 10), convert_to_pixmap=False)
|
52
|
+
)
|
53
|
+
self._title_layout.insertWidget(self._title_layout.count() - 1, self.edit_button)
|
54
|
+
self.edit_button.clicked.connect(self._create_edit_dialog)
|
55
|
+
|
56
|
+
def _create_edit_dialog(self):
|
57
|
+
dialog = DeviceConfigDialog(parent=self, device=self.device)
|
58
|
+
dialog.accepted.connect(self._reload_config)
|
59
|
+
dialog.applied.connect(self._reload_config)
|
60
|
+
dialog.open()
|
70
61
|
|
71
62
|
@SafeSlot()
|
72
63
|
def switch_expanded_state(self):
|
73
64
|
if not self.expanded and not self._expanded_first_time:
|
74
65
|
self._expanded_first_time = True
|
75
|
-
self.form =
|
66
|
+
self.form = DeviceConfigForm(parent=self, pretty_display=True)
|
76
67
|
self._contents.layout().addWidget(self.form)
|
77
|
-
|
78
|
-
self.form.set_data(self._data)
|
68
|
+
self._reload_config()
|
79
69
|
self.broadcast_size_hint.emit(self.sizeHint())
|
80
70
|
super().switch_expanded_state()
|
81
71
|
if self._expanded_first_time:
|
@@ -86,6 +76,10 @@ class DeviceItem(ExpandableGroupFrame):
|
|
86
76
|
self.adjustSize()
|
87
77
|
self.broadcast_size_hint.emit(self.sizeHint())
|
88
78
|
|
79
|
+
@SafeSlot(popup_error=True)
|
80
|
+
def _reload_config(self, *_):
|
81
|
+
self.set_display_config(self.dev[self.device]._config)
|
82
|
+
|
89
83
|
def set_display_config(self, config_dict: dict):
|
90
84
|
"""Set the displayed information from a device config dict, which must conform to the
|
91
85
|
bec_lib.atlas_models.Device config model."""
|
@@ -118,29 +112,33 @@ class DeviceItem(ExpandableGroupFrame):
|
|
118
112
|
|
119
113
|
if __name__ == "__main__": # pragma: no cover
|
120
114
|
import sys
|
115
|
+
from unittest.mock import MagicMock
|
121
116
|
|
122
117
|
from qtpy.QtWidgets import QApplication
|
123
118
|
|
119
|
+
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
|
120
|
+
DeviceConfigForm,
|
121
|
+
)
|
122
|
+
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
123
|
+
|
124
124
|
app = QApplication(sys.argv)
|
125
125
|
widget = QWidget()
|
126
126
|
layout = QHBoxLayout()
|
127
127
|
widget.setLayout(layout)
|
128
|
-
|
128
|
+
mock_config = {
|
129
|
+
"name": "Test Device",
|
130
|
+
"enabled": True,
|
131
|
+
"deviceClass": "FakeDeviceClass",
|
132
|
+
"deviceConfig": {"kwarg1": "value1"},
|
133
|
+
"readoutPriority": "baseline",
|
134
|
+
"description": "A device for testing out a widget",
|
135
|
+
"readOnly": True,
|
136
|
+
"softwareTrigger": False,
|
137
|
+
"deviceTags": {"tag1", "tag2", "tag3"},
|
138
|
+
"userParameter": {"some_setting": "some_ value"},
|
139
|
+
}
|
140
|
+
item = DeviceItem(widget, "Device", {"Device": MagicMock(enabled=True, _config=mock_config)})
|
129
141
|
layout.addWidget(DarkModeButton())
|
130
142
|
layout.addWidget(item)
|
131
|
-
item.set_display_config(
|
132
|
-
{
|
133
|
-
"name": "Test Device",
|
134
|
-
"enabled": True,
|
135
|
-
"deviceClass": "FakeDeviceClass",
|
136
|
-
"deviceConfig": {"kwarg1": "value1"},
|
137
|
-
"readoutPriority": "baseline",
|
138
|
-
"description": "A device for testing out a widget",
|
139
|
-
"readOnly": True,
|
140
|
-
"softwareTrigger": False,
|
141
|
-
"deviceTags": ["tag1", "tag2", "tag3"],
|
142
|
-
"userParameter": {"some_setting": "some_ value"},
|
143
|
-
}
|
144
|
-
)
|
145
143
|
widget.show()
|
146
144
|
sys.exit(app.exec_())
|