bec-widgets 2.11.0__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 +33 -0
- PKG-INFO +1 -1
- 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/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.11.0.dist-info → bec_widgets-2.12.0.dist-info}/METADATA +1 -1
- {bec_widgets-2.11.0.dist-info → bec_widgets-2.12.0.dist-info}/RECORD +23 -20
- pyproject.toml +1 -1
- {bec_widgets-2.11.0.dist-info → bec_widgets-2.12.0.dist-info}/WHEEL +0 -0
- {bec_widgets-2.11.0.dist-info → bec_widgets-2.12.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-2.11.0.dist-info → bec_widgets-2.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
)
|
@@ -1,15 +1,22 @@
|
|
1
1
|
import os
|
2
2
|
import re
|
3
|
-
from
|
3
|
+
from functools import partial
|
4
4
|
|
5
5
|
from bec_lib.callback_handler import EventType
|
6
|
+
from bec_lib.logger import bec_logger
|
7
|
+
from bec_lib.messages import ConfigAction
|
6
8
|
from pyqtgraph import SignalProxy
|
7
|
-
from qtpy.QtCore import
|
8
|
-
from qtpy.QtWidgets import QListWidgetItem, QVBoxLayout, QWidget
|
9
|
+
from qtpy.QtCore import QSize, Signal
|
10
|
+
from qtpy.QtWidgets import QListWidget, QListWidgetItem, QVBoxLayout, QWidget
|
9
11
|
|
12
|
+
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
10
13
|
from bec_widgets.utils.bec_widget import BECWidget
|
14
|
+
from bec_widgets.utils.error_popups import SafeSlot
|
11
15
|
from bec_widgets.utils.ui_loader import UILoader
|
12
16
|
from bec_widgets.widgets.services.device_browser.device_item import DeviceItem
|
17
|
+
from bec_widgets.widgets.services.device_browser.util import map_device_type_to_icon
|
18
|
+
|
19
|
+
logger = bec_logger.logger
|
13
20
|
|
14
21
|
|
15
22
|
class DeviceBrowser(BECWidget, QWidget):
|
@@ -23,18 +30,18 @@ class DeviceBrowser(BECWidget, QWidget):
|
|
23
30
|
|
24
31
|
def __init__(
|
25
32
|
self,
|
26
|
-
parent:
|
33
|
+
parent: QWidget | None = None,
|
27
34
|
config=None,
|
28
35
|
client=None,
|
29
|
-
gui_id:
|
36
|
+
gui_id: str | None = None,
|
30
37
|
**kwargs,
|
31
38
|
) -> None:
|
32
39
|
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
33
|
-
|
34
40
|
self.get_bec_shortcuts()
|
35
41
|
self.ui = None
|
36
42
|
self.ini_ui()
|
37
|
-
|
43
|
+
self.dev_list: QListWidget = self.ui.device_list
|
44
|
+
self.dev_list.setVerticalScrollMode(QListWidget.ScrollMode.ScrollPerPixel)
|
38
45
|
self.proxy_device_update = SignalProxy(
|
39
46
|
self.ui.filter_input.textChanged, rateLimit=500, slot=self.update_device_list
|
40
47
|
)
|
@@ -43,6 +50,7 @@ class DeviceBrowser(BECWidget, QWidget):
|
|
43
50
|
)
|
44
51
|
self.device_update.connect(self.update_device_list)
|
45
52
|
|
53
|
+
self.init_device_list()
|
46
54
|
self.update_device_list()
|
47
55
|
|
48
56
|
def ini_ui(self) -> None:
|
@@ -50,14 +58,12 @@ class DeviceBrowser(BECWidget, QWidget):
|
|
50
58
|
Initialize the UI by loading the UI file and setting the layout.
|
51
59
|
"""
|
52
60
|
layout = QVBoxLayout()
|
53
|
-
layout.setContentsMargins(0, 0, 0, 0)
|
54
|
-
|
55
61
|
ui_file_path = os.path.join(os.path.dirname(__file__), "device_browser.ui")
|
56
62
|
self.ui = UILoader(self).loader(ui_file_path)
|
57
63
|
layout.addWidget(self.ui)
|
58
64
|
self.setLayout(layout)
|
59
65
|
|
60
|
-
def on_device_update(self, action:
|
66
|
+
def on_device_update(self, action: ConfigAction, content: dict) -> None:
|
61
67
|
"""
|
62
68
|
Callback for device update events. Triggers the device_update signal.
|
63
69
|
|
@@ -68,8 +74,43 @@ class DeviceBrowser(BECWidget, QWidget):
|
|
68
74
|
if action in ["add", "remove", "reload"]:
|
69
75
|
self.device_update.emit()
|
70
76
|
|
71
|
-
|
72
|
-
|
77
|
+
def init_device_list(self):
|
78
|
+
self.dev_list.clear()
|
79
|
+
self._device_items: dict[str, QListWidgetItem] = {}
|
80
|
+
|
81
|
+
def _updatesize(item: QListWidgetItem, device_item: DeviceItem):
|
82
|
+
device_item.adjustSize()
|
83
|
+
item.setSizeHint(QSize(device_item.width(), device_item.height()))
|
84
|
+
logger.debug(f"Adjusting {item} size to {device_item.width(), device_item.height()}")
|
85
|
+
|
86
|
+
with RPCRegister.delayed_broadcast():
|
87
|
+
for device, device_obj in self.dev.items():
|
88
|
+
item = QListWidgetItem(self.dev_list)
|
89
|
+
device_item = DeviceItem(
|
90
|
+
parent=self, device=device, icon=map_device_type_to_icon(device_obj)
|
91
|
+
)
|
92
|
+
|
93
|
+
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", "")
|
98
|
+
device_item.setToolTip(tooltip)
|
99
|
+
device_item.broadcast_size_hint.connect(item.setSizeHint)
|
100
|
+
item.setSizeHint(device_item.sizeHint())
|
101
|
+
|
102
|
+
self.dev_list.setItemWidget(item, device_item)
|
103
|
+
self.dev_list.addItem(item)
|
104
|
+
self._device_items[device] = item
|
105
|
+
|
106
|
+
@SafeSlot()
|
107
|
+
def reset_device_list(self) -> None:
|
108
|
+
self.init_device_list()
|
109
|
+
self.update_device_list()
|
110
|
+
|
111
|
+
@SafeSlot()
|
112
|
+
@SafeSlot(str)
|
113
|
+
def update_device_list(self, *_) -> None:
|
73
114
|
"""
|
74
115
|
Update the device list based on the filter input.
|
75
116
|
There are two ways to trigger this function:
|
@@ -80,23 +121,14 @@ class DeviceBrowser(BECWidget, QWidget):
|
|
80
121
|
"""
|
81
122
|
filter_text = self.ui.filter_input.text()
|
82
123
|
try:
|
83
|
-
regex = re.compile(filter_text, re.IGNORECASE)
|
124
|
+
self.regex = re.compile(filter_text, re.IGNORECASE)
|
84
125
|
except re.error:
|
85
|
-
regex = None # Invalid regex, disable filtering
|
86
|
-
|
87
|
-
|
88
|
-
|
126
|
+
self.regex = None # Invalid regex, disable filtering
|
127
|
+
for device in self.dev:
|
128
|
+
self._device_items[device].setHidden(False)
|
129
|
+
return
|
89
130
|
for device in self.dev:
|
90
|
-
|
91
|
-
item = QListWidgetItem(dev_list)
|
92
|
-
device_item = DeviceItem(device)
|
93
|
-
|
94
|
-
# pylint: disable=protected-access
|
95
|
-
tooltip = self.dev[device]._config.get("description", "")
|
96
|
-
device_item.setToolTip(tooltip)
|
97
|
-
item.setSizeHint(device_item.sizeHint())
|
98
|
-
dev_list.setItemWidget(item, device_item)
|
99
|
-
dev_list.addItem(item)
|
131
|
+
self._device_items[device].setHidden(not self.regex.search(device))
|
100
132
|
|
101
133
|
|
102
134
|
if __name__ == "__main__": # pragma: no cover
|
@@ -104,10 +136,10 @@ if __name__ == "__main__": # pragma: no cover
|
|
104
136
|
|
105
137
|
from qtpy.QtWidgets import QApplication
|
106
138
|
|
107
|
-
from bec_widgets.utils.colors import
|
139
|
+
from bec_widgets.utils.colors import set_theme
|
108
140
|
|
109
141
|
app = QApplication(sys.argv)
|
110
|
-
|
142
|
+
set_theme("light")
|
111
143
|
widget = DeviceBrowser()
|
112
144
|
widget.show()
|
113
145
|
sys.exit(app.exec_())
|
@@ -2,10 +2,18 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from typing import TYPE_CHECKING
|
4
4
|
|
5
|
+
from bec_lib.atlas_models import Device as DeviceConfigModel
|
5
6
|
from bec_lib.logger import bec_logger
|
6
|
-
from qtpy.QtCore import QMimeData, Qt
|
7
|
+
from qtpy.QtCore import QMimeData, QSize, Qt, Signal
|
7
8
|
from qtpy.QtGui import QDrag
|
8
|
-
from qtpy.QtWidgets import QApplication, QHBoxLayout,
|
9
|
+
from qtpy.QtWidgets import QApplication, QHBoxLayout, QWidget
|
10
|
+
|
11
|
+
from bec_widgets.utils.colors import get_theme_name
|
12
|
+
from bec_widgets.utils.error_popups import SafeSlot
|
13
|
+
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
|
14
|
+
from bec_widgets.utils.forms_from_types import styles
|
15
|
+
from bec_widgets.utils.forms_from_types.forms import PydanticModelForm
|
16
|
+
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
9
17
|
|
10
18
|
if TYPE_CHECKING: # pragma: no cover
|
11
19
|
from qtpy.QtGui import QMouseEvent
|
@@ -13,26 +21,77 @@ if TYPE_CHECKING: # pragma: no cover
|
|
13
21
|
logger = bec_logger.logger
|
14
22
|
|
15
23
|
|
16
|
-
class
|
17
|
-
|
18
|
-
|
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
|
+
|
44
|
+
def _connect_to_theme_change(self):
|
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
|
19
49
|
|
20
|
-
self._drag_pos = None
|
21
50
|
|
51
|
+
class DeviceItem(ExpandableGroupFrame):
|
52
|
+
broadcast_size_hint = Signal(QSize)
|
53
|
+
|
54
|
+
RPC = False
|
55
|
+
|
56
|
+
def __init__(self, parent, device: str, icon: str = "") -> None:
|
57
|
+
super().__init__(parent, title=device, expanded=False, icon=icon)
|
58
|
+
|
59
|
+
self._drag_pos = None
|
60
|
+
self._expanded_first_time = False
|
61
|
+
self._data = None
|
22
62
|
self.device = device
|
23
63
|
layout = QHBoxLayout()
|
24
|
-
layout.setContentsMargins(
|
25
|
-
self.
|
26
|
-
|
27
|
-
self.
|
28
|
-
|
29
|
-
self.
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
64
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
65
|
+
self.set_layout(layout)
|
66
|
+
|
67
|
+
self.adjustSize()
|
68
|
+
self._title.clicked.connect(self.switch_expanded_state)
|
69
|
+
self._title_icon.clicked.connect(self.switch_expanded_state)
|
70
|
+
|
71
|
+
@SafeSlot()
|
72
|
+
def switch_expanded_state(self):
|
73
|
+
if not self.expanded and not self._expanded_first_time:
|
74
|
+
self._expanded_first_time = True
|
75
|
+
self.form = DeviceItemForm(parent=self, pretty_display=True)
|
76
|
+
self._contents.layout().addWidget(self.form)
|
77
|
+
if self._data:
|
78
|
+
self.form.set_data(self._data)
|
79
|
+
self.broadcast_size_hint.emit(self.sizeHint())
|
80
|
+
super().switch_expanded_state()
|
81
|
+
if self._expanded_first_time:
|
82
|
+
self.form.adjustSize()
|
83
|
+
self.updateGeometry()
|
84
|
+
if self._expanded:
|
85
|
+
self.form.set_pretty_display_theme()
|
86
|
+
self.adjustSize()
|
87
|
+
self.broadcast_size_hint.emit(self.sizeHint())
|
88
|
+
|
89
|
+
def set_display_config(self, config_dict: dict):
|
90
|
+
"""Set the displayed information from a device config dict, which must conform to the
|
91
|
+
bec_lib.atlas_models.Device config model."""
|
92
|
+
self._data = DeviceConfigModel.model_validate(config_dict)
|
93
|
+
if self._expanded_first_time:
|
94
|
+
self.form.set_data(self._data)
|
36
95
|
|
37
96
|
def mousePressEvent(self, event: QMouseEvent) -> None:
|
38
97
|
super().mousePressEvent(event)
|
@@ -63,6 +122,25 @@ if __name__ == "__main__": # pragma: no cover
|
|
63
122
|
from qtpy.QtWidgets import QApplication
|
64
123
|
|
65
124
|
app = QApplication(sys.argv)
|
66
|
-
widget =
|
125
|
+
widget = QWidget()
|
126
|
+
layout = QHBoxLayout()
|
127
|
+
widget.setLayout(layout)
|
128
|
+
item = DeviceItem("Device")
|
129
|
+
layout.addWidget(DarkModeButton())
|
130
|
+
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
|
+
)
|
67
145
|
widget.show()
|
68
146
|
sys.exit(app.exec_())
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from bec_lib.device import Device
|
2
|
+
|
3
|
+
|
4
|
+
def map_device_type_to_icon(device_obj: Device) -> str:
|
5
|
+
"""Associate device types with material icon names"""
|
6
|
+
match device_obj._info.get("device_base_class", "").lower():
|
7
|
+
case "positioner":
|
8
|
+
return "precision_manufacturing"
|
9
|
+
case "signal":
|
10
|
+
return "vital_signs"
|
11
|
+
return "deployed_code"
|