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.
@@ -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
- return str(self._data[index.row()][index.column()])
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), 0))
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, 0))
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__(self, parent: QWidget | None = None, initial_data: list[list[str]] = []):
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 info.is_required()
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, device=device, icon=map_device_type_to_icon(device_obj)
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.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
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
- 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
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
- self._title.clicked.connect(self.switch_expanded_state)
69
- self._title_icon.clicked.connect(self.switch_expanded_state)
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 = DeviceItemForm(parent=self, pretty_display=True)
66
+ self.form = DeviceConfigForm(parent=self, pretty_display=True)
76
67
  self._contents.layout().addWidget(self.form)
77
- if self._data:
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
- item = DeviceItem("Device")
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_())