bec-widgets 2.3.0__py3-none-any.whl → 2.4.3__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.
- .github/ISSUE_TEMPLATE/bug_report.md +26 -0
- .github/ISSUE_TEMPLATE/feature_request.md +48 -0
- .github/workflows/check_pr.yml +28 -0
- .github/workflows/ci.yml +36 -0
- .github/workflows/end2end-conda.yml +48 -0
- .github/workflows/formatter.yml +61 -0
- .github/workflows/generate-cli-check.yml +49 -0
- .github/workflows/pytest-matrix.yml +48 -0
- .github/workflows/pytest.yml +64 -0
- .github/workflows/semantic_release.yml +103 -0
- CHANGELOG.md +1713 -1546
- LICENSE +1 -1
- PKG-INFO +2 -1
- README.md +11 -0
- bec_widgets/cli/client.py +11 -0
- bec_widgets/tests/utils.py +3 -3
- bec_widgets/utils/entry_validator.py +13 -3
- bec_widgets/utils/side_panel.py +65 -39
- bec_widgets/utils/toolbar.py +79 -0
- bec_widgets/widgets/containers/layout_manager/layout_manager.py +34 -1
- bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +1 -1
- bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +27 -31
- bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +1 -1
- bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +1 -1
- bec_widgets/widgets/editors/dict_backed_table.py +7 -0
- bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +1 -0
- bec_widgets/widgets/editors/web_console/register_web_console.py +15 -0
- bec_widgets/widgets/editors/web_console/web_console.py +230 -0
- bec_widgets/widgets/editors/web_console/web_console.pyproject +1 -0
- bec_widgets/widgets/editors/web_console/web_console_plugin.py +54 -0
- bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +11 -46
- bec_widgets/widgets/utility/visual/color_button_native/__init__.py +0 -0
- bec_widgets/widgets/utility/visual/color_button_native/color_button_native.py +58 -0
- bec_widgets/widgets/utility/visual/color_button_native/color_button_native.pyproject +1 -0
- bec_widgets/widgets/utility/visual/color_button_native/color_button_native_plugin.py +56 -0
- bec_widgets/widgets/utility/visual/color_button_native/register_color_button_native.py +17 -0
- {bec_widgets-2.3.0.dist-info → bec_widgets-2.4.3.dist-info}/METADATA +2 -1
- {bec_widgets-2.3.0.dist-info → bec_widgets-2.4.3.dist-info}/RECORD +42 -23
- {bec_widgets-2.3.0.dist-info → bec_widgets-2.4.3.dist-info}/licenses/LICENSE +1 -1
- pyproject.toml +17 -5
- {bec_widgets-2.3.0.dist-info → bec_widgets-2.4.3.dist-info}/WHEEL +0 -0
- {bec_widgets-2.3.0.dist-info → bec_widgets-2.4.3.dist-info}/entry_points.txt +0 -0
LICENSE
CHANGED
PKG-INFO
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bec_widgets
|
3
|
-
Version: 2.3
|
3
|
+
Version: 2.4.3
|
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
|
@@ -25,6 +25,7 @@ Requires-Dist: coverage~=7.0; extra == 'dev'
|
|
25
25
|
Requires-Dist: fakeredis>=2.23.2,~=2.23; extra == 'dev'
|
26
26
|
Requires-Dist: isort>=5.13.2,~=5.13; extra == 'dev'
|
27
27
|
Requires-Dist: pytest-bec-e2e<=4.0,>=2.21.4; extra == 'dev'
|
28
|
+
Requires-Dist: pytest-cov~=6.1.1; extra == 'dev'
|
28
29
|
Requires-Dist: pytest-qt~=4.4; extra == 'dev'
|
29
30
|
Requires-Dist: pytest-random-order~=1.1; extra == 'dev'
|
30
31
|
Requires-Dist: pytest-timeout~=2.2; extra == 'dev'
|
README.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
# BEC Widgets
|
2
2
|
|
3
|
+
|
4
|
+
[](https://github.com/bec-project/bec_widgets/actions/workflows/ci.yml)
|
5
|
+
[](https://pypi.org/project/bec-widgets/)
|
6
|
+
[](./LICENSE)
|
7
|
+
[](https://github.com/psf/black)
|
8
|
+
[](https://www.python.org)
|
9
|
+
[](https://doc.qt.io/qtforpython/)
|
10
|
+
[](https://conventionalcommits.org)
|
11
|
+
[](https://codecov.io/gh/bec-project/bec_widgets)
|
12
|
+
|
13
|
+
|
3
14
|
**⚠️ Important Notice:**
|
4
15
|
|
5
16
|
🚨 **PyQt6 is no longer supported** due to incompatibilities with Qt Designer. Please use **PySide6** instead. 🚨
|
bec_widgets/cli/client.py
CHANGED
@@ -55,6 +55,7 @@ _Widgets = {
|
|
55
55
|
"TextBox": "TextBox",
|
56
56
|
"VSCodeEditor": "VSCodeEditor",
|
57
57
|
"Waveform": "Waveform",
|
58
|
+
"WebConsole": "WebConsole",
|
58
59
|
"WebsiteWidget": "WebsiteWidget",
|
59
60
|
}
|
60
61
|
|
@@ -3501,6 +3502,16 @@ class Waveform(RPCBase):
|
|
3501
3502
|
"""
|
3502
3503
|
|
3503
3504
|
|
3505
|
+
class WebConsole(RPCBase):
|
3506
|
+
"""A simple widget to display a website"""
|
3507
|
+
|
3508
|
+
@rpc_call
|
3509
|
+
def remove(self):
|
3510
|
+
"""
|
3511
|
+
Cleanup the BECConnector
|
3512
|
+
"""
|
3513
|
+
|
3514
|
+
|
3504
3515
|
class WebsiteWidget(RPCBase):
|
3505
3516
|
"""A simple widget to display a website"""
|
3506
3517
|
|
bec_widgets/tests/utils.py
CHANGED
@@ -96,9 +96,9 @@ class FakePositioner(BECPositioner):
|
|
96
96
|
}
|
97
97
|
self._info = {
|
98
98
|
"signals": {
|
99
|
-
"readback": {"kind_str": "
|
100
|
-
"setpoint": {"kind_str": "
|
101
|
-
"velocity": {"kind_str": "
|
99
|
+
"readback": {"kind_str": "hinted"}, # hinted
|
100
|
+
"setpoint": {"kind_str": "normal"}, # normal
|
101
|
+
"velocity": {"kind_str": "config"}, # config
|
102
102
|
}
|
103
103
|
}
|
104
104
|
self.signals = {
|
@@ -17,13 +17,23 @@ class EntryValidator:
|
|
17
17
|
raise ValueError(f"Device '{name}' not found in current BEC session")
|
18
18
|
|
19
19
|
device = self.devices[name]
|
20
|
-
|
20
|
+
|
21
|
+
# Build list of available signal entries from device._info['signals']
|
22
|
+
signals_dict = getattr(device, "_info", {}).get("signals", {})
|
23
|
+
available_entries = [
|
24
|
+
sig.get("obj_name") for sig in signals_dict.values() if sig.get("obj_name")
|
25
|
+
]
|
26
|
+
|
27
|
+
# If no signals are found, means device is a signal, use the device name as the entry
|
28
|
+
if not available_entries:
|
29
|
+
available_entries = [name]
|
21
30
|
|
22
31
|
if entry is None or entry == "":
|
23
32
|
entry = next(iter(device._hints), name) if hasattr(device, "_hints") else name
|
24
|
-
if entry not in
|
33
|
+
if entry not in available_entries:
|
25
34
|
raise ValueError(
|
26
|
-
f"Entry '{entry}' not found in device '{name}' signals.
|
35
|
+
f"Entry '{entry}' not found in device '{name}' signals. "
|
36
|
+
f"Available signals: '{available_entries}'"
|
27
37
|
)
|
28
38
|
|
29
39
|
return entry
|
bec_widgets/utils/side_panel.py
CHANGED
@@ -31,6 +31,7 @@ class SidePanel(QWidget):
|
|
31
31
|
panel_max_width: int = 200,
|
32
32
|
animation_duration: int = 200,
|
33
33
|
animations_enabled: bool = True,
|
34
|
+
show_toolbar: bool = True,
|
34
35
|
):
|
35
36
|
super().__init__(parent=parent)
|
36
37
|
|
@@ -40,6 +41,7 @@ class SidePanel(QWidget):
|
|
40
41
|
self._panel_max_width = panel_max_width
|
41
42
|
self._animation_duration = animation_duration
|
42
43
|
self._animations_enabled = animations_enabled
|
44
|
+
self._show_toolbar = show_toolbar
|
43
45
|
|
44
46
|
self._panel_width = 0
|
45
47
|
self._panel_height = 0
|
@@ -71,13 +73,14 @@ class SidePanel(QWidget):
|
|
71
73
|
self.stack_widget.setMinimumWidth(5)
|
72
74
|
self.stack_widget.setMaximumWidth(self._panel_max_width)
|
73
75
|
|
74
|
-
if self._orientation
|
75
|
-
self.
|
76
|
-
|
77
|
-
else:
|
78
|
-
self.main_layout.addWidget(self.container)
|
79
|
-
self.main_layout.addWidget(self.toolbar)
|
76
|
+
if self._orientation in ("left", "right"):
|
77
|
+
if self._show_toolbar:
|
78
|
+
self.main_layout.addWidget(self.toolbar)
|
80
79
|
|
80
|
+
if self._orientation == "left":
|
81
|
+
self.main_layout.addWidget(self.container)
|
82
|
+
else:
|
83
|
+
self.main_layout.insertWidget(0, self.container)
|
81
84
|
self.container.layout.addWidget(self.stack_widget)
|
82
85
|
|
83
86
|
self.menu_anim = QPropertyAnimation(self, b"panel_width")
|
@@ -102,11 +105,13 @@ class SidePanel(QWidget):
|
|
102
105
|
self.stack_widget.setMaximumHeight(self._panel_max_width)
|
103
106
|
|
104
107
|
if self._orientation == "top":
|
105
|
-
self.
|
108
|
+
if self._show_toolbar:
|
109
|
+
self.main_layout.addWidget(self.toolbar)
|
106
110
|
self.main_layout.addWidget(self.container)
|
107
111
|
else:
|
108
112
|
self.main_layout.addWidget(self.container)
|
109
|
-
self.
|
113
|
+
if self._show_toolbar:
|
114
|
+
self.main_layout.addWidget(self.toolbar)
|
110
115
|
|
111
116
|
self.container.layout.addWidget(self.stack_widget)
|
112
117
|
|
@@ -233,21 +238,24 @@ class SidePanel(QWidget):
|
|
233
238
|
|
234
239
|
def add_menu(
|
235
240
|
self,
|
236
|
-
action_id: str,
|
237
|
-
icon_name: str,
|
238
|
-
tooltip: str,
|
239
241
|
widget: QWidget,
|
242
|
+
action_id: str | None = None,
|
243
|
+
icon_name: str | None = None,
|
244
|
+
tooltip: str | None = None,
|
240
245
|
title: str | None = None,
|
241
|
-
):
|
246
|
+
) -> int:
|
242
247
|
"""
|
243
248
|
Add a menu to the side panel.
|
244
249
|
|
245
250
|
Args:
|
246
|
-
action_id(str): The ID of the action.
|
247
|
-
icon_name(str): The name of the icon.
|
248
|
-
tooltip(str): The tooltip for the action.
|
249
251
|
widget(QWidget): The widget to add to the panel.
|
250
|
-
|
252
|
+
action_id(str | None): The ID of the action. Optional if no toolbar action is needed.
|
253
|
+
icon_name(str | None): The name of the icon. Optional if no toolbar action is needed.
|
254
|
+
tooltip(str | None): The tooltip for the action. Optional if no toolbar action is needed.
|
255
|
+
title(str | None): The title of the panel.
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
int: The index of the added panel, which can be used with show_panel() and switch_to().
|
251
259
|
"""
|
252
260
|
# container_widget: top-level container for the stacked page
|
253
261
|
container_widget = QWidget()
|
@@ -278,32 +286,35 @@ class SidePanel(QWidget):
|
|
278
286
|
index = self.stack_widget.count()
|
279
287
|
self.stack_widget.addWidget(container_widget)
|
280
288
|
|
281
|
-
# Add an action to the toolbar
|
282
|
-
|
283
|
-
|
289
|
+
# Add an action to the toolbar if action_id, icon_name, and tooltip are provided
|
290
|
+
if action_id is not None and icon_name is not None and tooltip is not None:
|
291
|
+
action = MaterialIconAction(icon_name=icon_name, tooltip=tooltip, checkable=True)
|
292
|
+
self.toolbar.add_action(action_id, action, target_widget=self)
|
284
293
|
|
285
|
-
|
286
|
-
|
287
|
-
|
294
|
+
def on_action_toggled(checked: bool):
|
295
|
+
if self.switching_actions:
|
296
|
+
return
|
288
297
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
298
|
+
if checked:
|
299
|
+
if self.current_action and self.current_action != action.action:
|
300
|
+
self.switching_actions = True
|
301
|
+
self.current_action.setChecked(False)
|
302
|
+
self.switching_actions = False
|
294
303
|
|
295
|
-
|
304
|
+
self.current_action = action.action
|
296
305
|
|
297
|
-
|
298
|
-
|
306
|
+
if not self.panel_visible:
|
307
|
+
self.show_panel(index)
|
308
|
+
else:
|
309
|
+
self.switch_to(index)
|
299
310
|
else:
|
300
|
-
self.
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
311
|
+
if self.current_action == action.action:
|
312
|
+
self.current_action = None
|
313
|
+
self.hide_panel()
|
314
|
+
|
315
|
+
action.action.toggled.connect(on_action_toggled)
|
305
316
|
|
306
|
-
|
317
|
+
return index
|
307
318
|
|
308
319
|
|
309
320
|
############################################
|
@@ -332,41 +343,56 @@ class ExampleApp(QMainWindow): # pragma: no cover
|
|
332
343
|
self.add_side_menus()
|
333
344
|
|
334
345
|
def add_side_menus(self):
|
346
|
+
# Example 1: With action, icon, and tooltip
|
335
347
|
widget1 = QWidget()
|
336
348
|
layout1 = QVBoxLayout(widget1)
|
337
349
|
for i in range(15):
|
338
350
|
layout1.addWidget(QLabel(f"Widget 1 label row {i}"))
|
339
351
|
self.side_panel.add_menu(
|
352
|
+
widget=widget1,
|
340
353
|
action_id="widget1",
|
341
354
|
icon_name="counter_1",
|
342
355
|
tooltip="Show Widget 1",
|
343
|
-
widget=widget1,
|
344
356
|
title="Widget 1 Panel",
|
345
357
|
)
|
346
358
|
|
359
|
+
# Example 2: With action, icon, and tooltip
|
347
360
|
widget2 = QWidget()
|
348
361
|
layout2 = QVBoxLayout(widget2)
|
349
362
|
layout2.addWidget(QLabel("Short widget 2 content"))
|
350
363
|
self.side_panel.add_menu(
|
364
|
+
widget=widget2,
|
351
365
|
action_id="widget2",
|
352
366
|
icon_name="counter_2",
|
353
367
|
tooltip="Show Widget 2",
|
354
|
-
widget=widget2,
|
355
368
|
title="Widget 2 Panel",
|
356
369
|
)
|
357
370
|
|
371
|
+
# Example 3: With action, icon, and tooltip
|
358
372
|
widget3 = QWidget()
|
359
373
|
layout3 = QVBoxLayout(widget3)
|
360
374
|
for i in range(10):
|
361
375
|
layout3.addWidget(QLabel(f"Line {i} for Widget 3"))
|
362
376
|
self.side_panel.add_menu(
|
377
|
+
widget=widget3,
|
363
378
|
action_id="widget3",
|
364
379
|
icon_name="counter_3",
|
365
380
|
tooltip="Show Widget 3",
|
366
|
-
widget=widget3,
|
367
381
|
title="Widget 3 Panel",
|
368
382
|
)
|
369
383
|
|
384
|
+
# Example 4: Without action, icon, and tooltip (can only be shown programmatically)
|
385
|
+
widget4 = QWidget()
|
386
|
+
layout4 = QVBoxLayout(widget4)
|
387
|
+
layout4.addWidget(QLabel("This panel has no toolbar button"))
|
388
|
+
layout4.addWidget(QLabel("It can only be shown programmatically"))
|
389
|
+
self.hidden_panel_index = self.side_panel.add_menu(widget=widget4, title="Hidden Panel")
|
390
|
+
|
391
|
+
# Example of how to show the hidden panel programmatically after 3 seconds
|
392
|
+
from qtpy.QtCore import QTimer
|
393
|
+
|
394
|
+
QTimer.singleShot(3000, lambda: self.side_panel.show_panel(self.hidden_panel_index))
|
395
|
+
|
370
396
|
|
371
397
|
if __name__ == "__main__": # pragma: no cover
|
372
398
|
app = QApplication(sys.argv)
|
bec_widgets/utils/toolbar.py
CHANGED
@@ -702,6 +702,85 @@ class ModularToolBar(QToolBar):
|
|
702
702
|
self.bundles[bundle_id].append(action_id)
|
703
703
|
self.update_separators()
|
704
704
|
|
705
|
+
def remove_action(self, action_id: str):
|
706
|
+
"""
|
707
|
+
Completely remove a single action from the toolbar.
|
708
|
+
|
709
|
+
The method takes care of both standalone actions and actions that are
|
710
|
+
part of an existing bundle.
|
711
|
+
|
712
|
+
Args:
|
713
|
+
action_id (str): Unique identifier for the action.
|
714
|
+
"""
|
715
|
+
if action_id not in self.widgets:
|
716
|
+
raise ValueError(f"Action with ID '{action_id}' does not exist.")
|
717
|
+
|
718
|
+
# Identify potential bundle membership
|
719
|
+
parent_bundle = None
|
720
|
+
for b_id, a_ids in self.bundles.items():
|
721
|
+
if action_id in a_ids:
|
722
|
+
parent_bundle = b_id
|
723
|
+
break
|
724
|
+
|
725
|
+
# 1. Remove the QAction from the QToolBar and delete it
|
726
|
+
tool_action = self.widgets.pop(action_id)
|
727
|
+
if hasattr(tool_action, "action") and tool_action.action is not None:
|
728
|
+
self.removeAction(tool_action.action)
|
729
|
+
tool_action.action.deleteLater()
|
730
|
+
|
731
|
+
# 2. Clean bundle bookkeeping if the action belonged to one
|
732
|
+
if parent_bundle:
|
733
|
+
self.bundles[parent_bundle].remove(action_id)
|
734
|
+
# If the bundle becomes empty, get rid of the bundle entry as well
|
735
|
+
if not self.bundles[parent_bundle]:
|
736
|
+
self.remove_bundle(parent_bundle)
|
737
|
+
|
738
|
+
# 3. Remove from the ordering list
|
739
|
+
self.toolbar_items = [
|
740
|
+
item
|
741
|
+
for item in self.toolbar_items
|
742
|
+
if not (item[0] == "action" and item[1] == action_id)
|
743
|
+
]
|
744
|
+
|
745
|
+
self.update_separators()
|
746
|
+
|
747
|
+
def remove_bundle(self, bundle_id: str):
|
748
|
+
"""
|
749
|
+
Remove an entire bundle (and all of its actions) from the toolbar.
|
750
|
+
|
751
|
+
Args:
|
752
|
+
bundle_id (str): Unique identifier for the bundle.
|
753
|
+
"""
|
754
|
+
if bundle_id not in self.bundles:
|
755
|
+
raise ValueError(f"Bundle '{bundle_id}' does not exist.")
|
756
|
+
|
757
|
+
# Remove every action belonging to this bundle
|
758
|
+
for action_id in list(self.bundles[bundle_id]): # copy the list
|
759
|
+
if action_id in self.widgets:
|
760
|
+
tool_action = self.widgets.pop(action_id)
|
761
|
+
if hasattr(tool_action, "action") and tool_action.action is not None:
|
762
|
+
self.removeAction(tool_action.action)
|
763
|
+
tool_action.action.deleteLater()
|
764
|
+
|
765
|
+
# Drop the bundle entry
|
766
|
+
self.bundles.pop(bundle_id, None)
|
767
|
+
|
768
|
+
# Remove bundle entry and its preceding separator (if any) from the ordering list
|
769
|
+
cleaned_items = []
|
770
|
+
skip_next_separator = False
|
771
|
+
for item_type, ident in self.toolbar_items:
|
772
|
+
if item_type == "bundle" and ident == bundle_id:
|
773
|
+
# mark to skip one following separator if present
|
774
|
+
skip_next_separator = True
|
775
|
+
continue
|
776
|
+
if skip_next_separator and item_type == "separator":
|
777
|
+
skip_next_separator = False
|
778
|
+
continue
|
779
|
+
cleaned_items.append((item_type, ident))
|
780
|
+
self.toolbar_items = cleaned_items
|
781
|
+
|
782
|
+
self.update_separators()
|
783
|
+
|
705
784
|
def contextMenuEvent(self, event):
|
706
785
|
"""
|
707
786
|
Overrides the context menu event to show toolbar actions with checkboxes and icons.
|
@@ -53,7 +53,7 @@ class LayoutManagerWidget(QWidget):
|
|
53
53
|
self,
|
54
54
|
widget: QWidget | str,
|
55
55
|
row: int | None = None,
|
56
|
-
col:
|
56
|
+
col: int | None = None,
|
57
57
|
rowspan: int = 1,
|
58
58
|
colspan: int = 1,
|
59
59
|
shift_existing: bool = True,
|
@@ -138,6 +138,39 @@ class LayoutManagerWidget(QWidget):
|
|
138
138
|
ref_row, ref_col, ref_rowspan, ref_colspan = self.widget_positions[reference_widget]
|
139
139
|
|
140
140
|
# Determine new widget position based on the specified relative position
|
141
|
+
|
142
|
+
# If adding to the left or right with shifting, shift the entire column
|
143
|
+
if (
|
144
|
+
position in ("left", "right")
|
145
|
+
and shift_existing
|
146
|
+
and shift_direction in ("left", "right")
|
147
|
+
):
|
148
|
+
column = ref_col
|
149
|
+
# Collect all rows in this column and sort for safe shifting
|
150
|
+
rows = sorted(
|
151
|
+
{row for (row, col) in self.position_widgets.keys() if col == column},
|
152
|
+
reverse=(shift_direction == "right"),
|
153
|
+
)
|
154
|
+
# Shift each widget in the column
|
155
|
+
for r in rows:
|
156
|
+
self.shift_widgets(direction=shift_direction, start_row=r, start_col=column)
|
157
|
+
# Update reference widget's position after the column shift
|
158
|
+
ref_row, ref_col, ref_rowspan, ref_colspan = self.widget_positions[reference_widget]
|
159
|
+
new_row = ref_row
|
160
|
+
# Compute insertion column based on relative position
|
161
|
+
if position == "left":
|
162
|
+
new_col = ref_col - ref_colspan
|
163
|
+
else:
|
164
|
+
new_col = ref_col + ref_colspan
|
165
|
+
# Add the new widget without triggering another shift
|
166
|
+
return self.add_widget(
|
167
|
+
widget=widget,
|
168
|
+
row=new_row,
|
169
|
+
col=new_col,
|
170
|
+
rowspan=rowspan,
|
171
|
+
colspan=colspan,
|
172
|
+
shift_existing=False,
|
173
|
+
)
|
141
174
|
if position == "left":
|
142
175
|
new_row = ref_row
|
143
176
|
new_col = ref_col - 1
|
@@ -397,7 +397,7 @@ class DeviceInputBase(BECWidget):
|
|
397
397
|
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
|
398
398
|
"""
|
399
399
|
self.validate_device(device)
|
400
|
-
dev = getattr(self.dev, device
|
400
|
+
dev = getattr(self.dev, device, None)
|
401
401
|
if dev is None:
|
402
402
|
raise ValueError(
|
403
403
|
f"Device {device} is not found in the device manager {self.dev} as enabled device."
|
@@ -36,14 +36,16 @@ class DeviceSignalInputBase(BECWidget):
|
|
36
36
|
Kind.config: "include_config_signals",
|
37
37
|
}
|
38
38
|
|
39
|
-
def __init__(
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
39
|
+
def __init__(
|
40
|
+
self,
|
41
|
+
client=None,
|
42
|
+
config: DeviceSignalInputBaseConfig | dict | None = None,
|
43
|
+
gui_id: str = None,
|
44
|
+
**kwargs,
|
45
|
+
):
|
46
|
+
|
47
|
+
self.config = self._process_config_input(config)
|
48
|
+
super().__init__(client=client, config=self.config, gui_id=gui_id, **kwargs)
|
47
49
|
|
48
50
|
self._device = None
|
49
51
|
self.get_bec_shortcuts()
|
@@ -102,10 +104,7 @@ class DeviceSignalInputBase(BECWidget):
|
|
102
104
|
"""
|
103
105
|
self.config.signal_filter = self.signal_filter
|
104
106
|
# pylint: disable=protected-access
|
105
|
-
self.
|
106
|
-
self._normal_signals = []
|
107
|
-
self._config_signals = []
|
108
|
-
if self.validate_device(self._device) is False:
|
107
|
+
if not self.validate_device(self._device):
|
109
108
|
self._device = None
|
110
109
|
self.config.device = self._device
|
111
110
|
return
|
@@ -116,27 +115,19 @@ class DeviceSignalInputBase(BECWidget):
|
|
116
115
|
FilterIO.set_selection(widget=self, selection=[self._device])
|
117
116
|
return
|
118
117
|
device_info = device._info["signals"]
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
for signal, signal_info in device_info.items()
|
123
|
-
if (signal_info.get("kind_str", None) == str(Kind.hinted.value))
|
124
|
-
]
|
125
|
-
self._hinted_signals = hinted_signals
|
126
|
-
if Kind.normal in self.signal_filter:
|
127
|
-
normal_signals = [
|
128
|
-
signal
|
129
|
-
for signal, signal_info in device_info.items()
|
130
|
-
if (signal_info.get("kind_str", None) == str(Kind.normal.value))
|
131
|
-
]
|
132
|
-
self._normal_signals = normal_signals
|
133
|
-
if Kind.config in self.signal_filter:
|
134
|
-
config_signals = [
|
118
|
+
|
119
|
+
def _update(kind: Kind):
|
120
|
+
return [
|
135
121
|
signal
|
136
122
|
for signal, signal_info in device_info.items()
|
137
|
-
if
|
123
|
+
if kind in self.signal_filter
|
124
|
+
and (signal_info.get("kind_str", None) == str(kind.name))
|
138
125
|
]
|
139
|
-
|
126
|
+
|
127
|
+
self._hinted_signals = _update(Kind.hinted)
|
128
|
+
self._normal_signals = _update(Kind.normal)
|
129
|
+
self._config_signals = _update(Kind.config)
|
130
|
+
|
140
131
|
self._signals = self._hinted_signals + self._normal_signals + self._config_signals
|
141
132
|
FilterIO.set_selection(widget=self, selection=self.signals)
|
142
133
|
|
@@ -250,7 +241,7 @@ class DeviceSignalInputBase(BECWidget):
|
|
250
241
|
object: Device object, can be device of type Device, Positioner, Signal or ComputedSignal.
|
251
242
|
"""
|
252
243
|
self.validate_device(device)
|
253
|
-
dev = getattr(self.dev, device
|
244
|
+
dev = getattr(self.dev, device, None)
|
254
245
|
if dev is None:
|
255
246
|
logger.warning(f"Device {device} not found in devicemanager.")
|
256
247
|
return None
|
@@ -279,3 +270,8 @@ class DeviceSignalInputBase(BECWidget):
|
|
279
270
|
if signal in self.signals:
|
280
271
|
return True
|
281
272
|
return False
|
273
|
+
|
274
|
+
def _process_config_input(self, config: DeviceSignalInputBaseConfig | dict | None):
|
275
|
+
if config is None:
|
276
|
+
return DeviceSignalInputBaseConfig(widget_class=self.__class__.__name__)
|
277
|
+
return DeviceSignalInputBaseConfig.model_validate(config)
|
@@ -140,7 +140,7 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
|
|
140
140
|
"""
|
141
141
|
if self.validate_device(input_text) is True:
|
142
142
|
self._is_valid_input = True
|
143
|
-
self.device_selected.emit(input_text
|
143
|
+
self.device_selected.emit(input_text)
|
144
144
|
else:
|
145
145
|
self._is_valid_input = False
|
146
146
|
self.update()
|
@@ -147,7 +147,7 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
|
|
147
147
|
"""
|
148
148
|
if self.validate_device(input_text) is True:
|
149
149
|
self._is_valid_input = True
|
150
|
-
self.device_selected.emit(input_text
|
150
|
+
self.device_selected.emit(input_text)
|
151
151
|
else:
|
152
152
|
self._is_valid_input = False
|
153
153
|
self.update()
|
@@ -53,6 +53,7 @@ class DictBackedTableModel(QAbstractTableModel):
|
|
53
53
|
if value in self._disallowed_keys or value in self._other_keys(index.row()):
|
54
54
|
return False
|
55
55
|
self._data[index.row()][index.column()] = str(value)
|
56
|
+
self.dataChanged.emit(index, index)
|
56
57
|
return True
|
57
58
|
return False
|
58
59
|
|
@@ -109,6 +110,7 @@ class DictBackedTableModel(QAbstractTableModel):
|
|
109
110
|
|
110
111
|
class DictBackedTable(QWidget):
|
111
112
|
delete_rows = Signal(list)
|
113
|
+
data_updated = Signal()
|
112
114
|
|
113
115
|
def __init__(self, initial_data: list[list[str]]):
|
114
116
|
"""Widget which uses a DictBackedTableModel to display an editable table
|
@@ -141,6 +143,11 @@ class DictBackedTable(QWidget):
|
|
141
143
|
self._add_button.clicked.connect(self._table_model.add_row)
|
142
144
|
self._remove_button.clicked.connect(self.delete_selected_rows)
|
143
145
|
self.delete_rows.connect(self._table_model.delete_rows)
|
146
|
+
self._table_model.dataChanged.connect(self._emit_data_updated)
|
147
|
+
|
148
|
+
def _emit_data_updated(self, *args, **kwargs):
|
149
|
+
"""Just to swallow the args"""
|
150
|
+
self.data_updated.emit()
|
144
151
|
|
145
152
|
def delete_selected_rows(self):
|
146
153
|
"""Delete rows which are part of the selection model"""
|
@@ -43,6 +43,7 @@ class ScanMetadata(PydanticModelForm):
|
|
43
43
|
self._additional_metadata = DictBackedTable(initial_extras or [])
|
44
44
|
self._scan_name = scan_name or ""
|
45
45
|
self._md_schema = get_metadata_schema_for_scan(self._scan_name)
|
46
|
+
self._additional_metadata.data_updated.connect(self.validate_form)
|
46
47
|
|
47
48
|
super().__init__(parent=parent, metadata_model=self._md_schema, client=client, **kwargs)
|
48
49
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
def main(): # pragma: no cover
|
2
|
+
from qtpy import PYSIDE6
|
3
|
+
|
4
|
+
if not PYSIDE6:
|
5
|
+
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
6
|
+
return
|
7
|
+
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
8
|
+
|
9
|
+
from bec_widgets.widgets.editors.web_console.web_console_plugin import WebConsolePlugin
|
10
|
+
|
11
|
+
QPyDesignerCustomWidgetCollection.addCustomWidget(WebConsolePlugin())
|
12
|
+
|
13
|
+
|
14
|
+
if __name__ == "__main__": # pragma: no cover
|
15
|
+
main()
|