bec-widgets 0.82.2__py3-none-any.whl → 0.83.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.
Files changed (40) hide show
  1. .gitlab-ci.yml +3 -0
  2. CHANGELOG.md +26 -28
  3. PKG-INFO +1 -1
  4. bec_widgets/utils/bec_connector.py +2 -0
  5. bec_widgets/utils/bec_widget.py +6 -0
  6. bec_widgets/utils/generate_designer_plugin.py +6 -5
  7. bec_widgets/utils/reference_utils.py +92 -0
  8. bec_widgets/widgets/console/console.py +1 -1
  9. bec_widgets/widgets/device_box/__init__.py +0 -0
  10. bec_widgets/widgets/device_box/device_box.py +197 -0
  11. bec_widgets/widgets/device_box/device_box.pyproject +1 -0
  12. bec_widgets/widgets/device_box/device_box.ui +179 -0
  13. bec_widgets/widgets/device_box/device_box_plugin.py +54 -0
  14. bec_widgets/widgets/device_box/register_device_box.py +15 -0
  15. bec_widgets/widgets/figure/figure.py +3 -3
  16. bec_widgets/widgets/position_indicator/position_indicator.py +71 -0
  17. bec_widgets/widgets/position_indicator/position_indicator.pyproject +1 -0
  18. bec_widgets/widgets/position_indicator/position_indicator_plugin.py +54 -0
  19. bec_widgets/widgets/position_indicator/register_position_indicator.py +17 -0
  20. bec_widgets/widgets/spinner/__init__.py +0 -0
  21. bec_widgets/widgets/spinner/register_spinner_widget.py +15 -0
  22. bec_widgets/widgets/spinner/spinner.py +85 -0
  23. bec_widgets/widgets/spinner/spinner_widget.pyproject +1 -0
  24. bec_widgets/widgets/spinner/spinner_widget_plugin.py +54 -0
  25. bec_widgets/widgets/vscode/vscode.py +0 -14
  26. bec_widgets/widgets/website/website.py +1 -1
  27. {bec_widgets-0.82.2.dist-info → bec_widgets-0.83.0.dist-info}/METADATA +1 -1
  28. {bec_widgets-0.82.2.dist-info → bec_widgets-0.83.0.dist-info}/RECORD +40 -18
  29. pyproject.toml +1 -1
  30. tests/references/SpinnerWidget/SpinnerWidget_darwin.png +0 -0
  31. tests/references/SpinnerWidget/SpinnerWidget_linux.png +0 -0
  32. tests/references/SpinnerWidget/SpinnerWidget_started_darwin.png +0 -0
  33. tests/references/SpinnerWidget/SpinnerWidget_started_linux.png +0 -0
  34. tests/unit_tests/client_mocks.py +9 -1
  35. tests/unit_tests/test_device_box.py +98 -0
  36. tests/unit_tests/test_spinner.py +30 -0
  37. tests/unit_tests/test_vscode_widget.py +27 -32
  38. {bec_widgets-0.82.2.dist-info → bec_widgets-0.83.0.dist-info}/WHEEL +0 -0
  39. {bec_widgets-0.82.2.dist-info → bec_widgets-0.83.0.dist-info}/entry_points.txt +0 -0
  40. {bec_widgets-0.82.2.dist-info → bec_widgets-0.83.0.dist-info}/licenses/LICENSE +0 -0
.gitlab-ci.yml CHANGED
@@ -144,6 +144,9 @@ tests:
144
144
  coverage_report:
145
145
  coverage_format: cobertura
146
146
  path: coverage.xml
147
+ paths:
148
+ - tests/reference_failures/
149
+ when: always
147
150
 
148
151
  test-matrix:
149
152
  parallel:
CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.83.0 (2024-07-08)
4
+
5
+ ### Feature
6
+
7
+ * feat: added reference utils to compare renderings of widgets ([`2988fd3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2988fd387e6b8076fffec1d57e3ccab89ddb2aeb))
8
+
9
+ * feat(widgets): added device box with spinner ([`1b017ed`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1b017edfad8e78fa079210486123976695b8915c))
10
+
11
+ * feat(designer): added option to skip the widget validation for DesignerPluginGenerator ([`41bcb80`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/41bcb801674ab6c4d6069bba34ffee09c9e665db))
12
+
13
+ ### Fix
14
+
15
+ * fix(terminal): added default args to avoid designer crashes on startup ([`360d171`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/360d17135573e44b80ab517756da3c0b31daab0f))
16
+
17
+ * fix(widget): fixed widget cleanup routine ([`2b29e34`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2b29e34b52d056349647bb2fcf649b749a60d292))
18
+
19
+ * fix(bec_widget): added cleanup method to bec widget base class ([`fd8766e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fd8766ed87770661da6591aeb4df5abdaf38afc7))
20
+
21
+ * fix(website): fixed dummy input ([`903ce7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/903ce7d46b5d37d40486d0fda92d3694d3faca62))
22
+
23
+ ### Test
24
+
25
+ * test(vscode): fixed vscode tests for new cleanup routine ([`eb26e2a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/eb26e2a11b229a52efe2e6d4fb28d760d3740136))
26
+
27
+ * test(vscode): improved vscode test ([`5de8804`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5de8804da1e41eafad2472344904b3324438c13b))
28
+
3
29
  ## v0.82.2 (2024-07-08)
4
30
 
5
31
  ### Fix
@@ -117,31 +143,3 @@
117
143
  * fix(toolbar): change default color to black to match BECFigure theme ([`b8774e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b8774e0b0bc43dcd00f94f42539a778e507ca27d))
118
144
 
119
145
  * fix(motor_map): fixed bug with residual trace after changing motors ([`aaa0d10`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/aaa0d1003d2e94b45bafe4f700852c2c05288aea))
120
-
121
- * fix(widget_io): widget handler adjusted for spinboxes and comboboxes ([`3dc0532`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3dc0532df05b6ec0a2522107fa0b1e210ce7d91b))
122
-
123
- ### Refactor
124
-
125
- * refactor(toolbar): cleanup and adjusted colors ([`96863ad`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/96863adf53c15112645d20eb6200733617801c6d))
126
-
127
- ## v0.78.1 (2024-07-02)
128
-
129
- ### Fix
130
-
131
- * fix(ui_loader): ui loader is compatible with bec plugins ([`b787759`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b787759f44486dc7af2c03811efb156041e4b6cb))
132
-
133
- ## v0.78.0 (2024-07-02)
134
-
135
- ### Feature
136
-
137
- * feat(color_button): patched ColorButton from pyqtgraph to be able to be opened in another QDialog ([`c36bb80`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c36bb80d6a4939802a4a1c8e5452c7b94bac185e))
138
-
139
- ## v0.77.0 (2024-07-02)
140
-
141
- ### Fix
142
-
143
- * fix(waveform): scatter 2D brush error ([`215d59c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/215d59c8bfe7fda9aff8cec8353bef9e1ce2eca1))
144
-
145
- * fix(figure): API cleanup ([`008a33a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/008a33a9b192473cc58e90cd6d98c5bcb5f7b8c0))
146
-
147
- * fix(figure): if/else logic corrected in subplot_factory ([`3e78723`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3e787234c7274b0698423d7bf9a4c54ec46bad5f))
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bec_widgets
3
- Version: 0.82.2
3
+ Version: 0.83.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
@@ -289,6 +289,8 @@ class BECConnector(BECWidget):
289
289
  print("No more connections. Shutting down GUI BEC client.")
290
290
  self.bec_dispatcher.disconnect_all()
291
291
  self.client.shutdown()
292
+ if hasattr(super(), "cleanup"):
293
+ super().cleanup()
292
294
 
293
295
  # def closeEvent(self, event):
294
296
  # self.cleanup()
@@ -1,2 +1,8 @@
1
1
  class BECWidget:
2
2
  """Base class for all BEC widgets."""
3
+
4
+ def closeEvent(self, event):
5
+ if hasattr(self, "cleanup"):
6
+ self.cleanup()
7
+ if hasattr(super(), "closeEvent"):
8
+ super().closeEvent(event)
@@ -58,11 +58,12 @@ class DesignerPluginGenerator:
58
58
  os.path.dirname(os.path.abspath(__file__)), "plugin_templates"
59
59
  )
60
60
 
61
- def run(self):
61
+ def run(self, validate=True):
62
62
  if self._excluded:
63
63
  print(f"Plugin {self.widget.__name__} is excluded from generation.")
64
64
  return
65
- self._check_class_validity()
65
+ if validate:
66
+ self._check_class_validity()
66
67
  self._load_templates()
67
68
  self._write_templates()
68
69
 
@@ -142,7 +143,7 @@ class DesignerPluginGenerator:
142
143
 
143
144
  if __name__ == "__main__": # pragma: no cover
144
145
  # from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
145
- from bec_widgets.widgets.dock import BECDockArea
146
+ from bec_widgets.widgets.spinner.spinner import SpinnerWidget
146
147
 
147
- generator = DesignerPluginGenerator(BECDockArea)
148
- generator.run()
148
+ generator = DesignerPluginGenerator(SpinnerWidget)
149
+ generator.run(validate=False)
@@ -0,0 +1,92 @@
1
+ import os
2
+ import sys
3
+
4
+ from PIL import Image, ImageChops
5
+ from qtpy.QtGui import QPixmap
6
+
7
+ import bec_widgets
8
+
9
+ REFERENCE_DIR = os.path.join(
10
+ os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/references"
11
+ )
12
+ REFERENCE_DIR_FAILURES = os.path.join(
13
+ os.path.dirname(os.path.dirname(bec_widgets.__file__)), "tests/reference_failures"
14
+ )
15
+
16
+
17
+ def compare_images(image1_path: str, reference_image_path: str):
18
+ """
19
+ Load two images and compare them pixel by pixel
20
+
21
+ Args:
22
+ image1_path(str): The path to the first image
23
+ reference_image_path(str): The path to the reference image
24
+
25
+ Raises:
26
+ ValueError: If the images are different
27
+ """
28
+ image1 = Image.open(image1_path)
29
+ image2 = Image.open(reference_image_path)
30
+ if image1.size != image2.size:
31
+ raise ValueError("Image size has changed")
32
+ diff = ImageChops.difference(image1, image2)
33
+ if diff.getbbox():
34
+ # copy image1 to the reference directory to upload as artifact
35
+ os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
36
+ image_name = os.path.join(REFERENCE_DIR_FAILURES, os.path.basename(image1_path))
37
+ image1.save(image_name)
38
+ print(f"Image saved to {image_name}")
39
+
40
+ raise ValueError("Images are different")
41
+
42
+
43
+ def snap_and_compare(widget: any, output_directory: str, suffix: str = ""):
44
+ """
45
+ Save a rendering of a widget and compare it to a reference image
46
+
47
+ Args:
48
+ widget(any): The widget to render
49
+ output_directory(str): The directory to save the image to
50
+ suffix(str): A suffix to append to the image name
51
+
52
+ Raises:
53
+ ValueError: If the images are different
54
+
55
+ Examples:
56
+ snap_and_compare(widget, tmpdir, suffix="started")
57
+
58
+ """
59
+
60
+ if not isinstance(output_directory, str):
61
+ output_directory = str(output_directory)
62
+
63
+ os_suffix = sys.platform
64
+
65
+ name = (
66
+ f"{widget.__class__.__name__}_{suffix}_{os_suffix}.png"
67
+ if suffix
68
+ else f"{widget.__class__.__name__}_{os_suffix}.png"
69
+ )
70
+
71
+ # Save the widget to a pixmap
72
+ test_image_path = os.path.join(output_directory, name)
73
+ pixmap = QPixmap(widget.size())
74
+ widget.render(pixmap)
75
+ pixmap.save(test_image_path)
76
+
77
+ try:
78
+ reference_path = os.path.join(REFERENCE_DIR, f"{widget.__class__.__name__}")
79
+ reference_image_path = os.path.join(reference_path, name)
80
+
81
+ if not os.path.exists(reference_image_path):
82
+ raise ValueError(f"Reference image not found: {reference_image_path}")
83
+
84
+ compare_images(test_image_path, reference_image_path)
85
+
86
+ except ValueError:
87
+ image = Image.open(test_image_path)
88
+ os.makedirs(REFERENCE_DIR_FAILURES, exist_ok=True)
89
+ image_name = os.path.join(REFERENCE_DIR_FAILURES, name)
90
+ image.save(image_name)
91
+ print(f"Image saved to {image_name}")
92
+ raise
@@ -222,7 +222,7 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
222
222
  Start ``Backend`` process and render Pyte output as text.
223
223
  """
224
224
 
225
- def __init__(self, parent, numColumns, numLines, **kwargs):
225
+ def __init__(self, parent, numColumns=125, numLines=50, **kwargs):
226
226
  super().__init__(parent)
227
227
 
228
228
  # file descriptor to communicate with the subprocess
File without changes
@@ -0,0 +1,197 @@
1
+ import os
2
+ import uuid
3
+
4
+ from bec_lib.endpoints import MessageEndpoints
5
+ from bec_lib.messages import ScanQueueMessage
6
+ from qtpy.QtCore import Property, Signal, Slot
7
+ from qtpy.QtGui import QDoubleValidator
8
+ from qtpy.QtWidgets import QDoubleSpinBox, QVBoxLayout, QWidget
9
+
10
+ from bec_widgets.utils import UILoader
11
+ from bec_widgets.utils.bec_connector import BECConnector
12
+
13
+
14
+ class DeviceBox(BECConnector, QWidget):
15
+ device_changed = Signal(str, str)
16
+
17
+ def __init__(self, parent=None, device=None, *args, **kwargs):
18
+ super().__init__(*args, **kwargs)
19
+ QWidget.__init__(self, parent=parent)
20
+ self.get_bec_shortcuts()
21
+ self._device = ""
22
+ self._limits = None
23
+
24
+ self.init_ui()
25
+
26
+ if device is not None:
27
+ self.device = device
28
+ self.init_device()
29
+
30
+ def init_ui(self):
31
+ self.device_changed.connect(self.on_device_change)
32
+
33
+ current_path = os.path.dirname(__file__)
34
+ self.ui = UILoader(self).loader(os.path.join(current_path, "device_box.ui"))
35
+
36
+ self.layout = QVBoxLayout(self)
37
+ self.layout.addWidget(self.ui)
38
+ self.layout.setSpacing(0)
39
+ self.layout.setContentsMargins(0, 0, 0, 0)
40
+
41
+ # fix the size of the device box
42
+ db = self.ui.device_box
43
+ db.setFixedHeight(234)
44
+ db.setFixedWidth(224)
45
+
46
+ self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
47
+ self.ui.stop.clicked.connect(self.on_stop)
48
+ self.ui.tweak_right.clicked.connect(self.on_tweak_right)
49
+ self.ui.tweak_right.setToolTip("Tweak right")
50
+ self.ui.tweak_left.clicked.connect(self.on_tweak_left)
51
+ self.ui.tweak_left.setToolTip("Tweak left")
52
+ self.ui.setpoint.returnPressed.connect(self.on_setpoint_change)
53
+
54
+ self.setpoint_validator = QDoubleValidator()
55
+ self.ui.setpoint.setValidator(self.setpoint_validator)
56
+ self.ui.spinner_widget.start()
57
+
58
+ def init_device(self):
59
+ if self.device in self.dev:
60
+ data = self.dev[self.device].read()
61
+ self.on_device_readback({"signals": data}, {})
62
+
63
+ @Property(str)
64
+ def device(self):
65
+ return self._device
66
+
67
+ @device.setter
68
+ def device(self, value):
69
+ if not value or not isinstance(value, str):
70
+ return
71
+ old_device = self._device
72
+ self._device = value
73
+ self.device_changed.emit(old_device, value)
74
+
75
+ @Slot(str, str)
76
+ def on_device_change(self, old_device: str, new_device: str):
77
+ if new_device not in self.dev:
78
+ print(f"Device {new_device} not found in the device list")
79
+ return
80
+ print(f"Device changed from {old_device} to {new_device}")
81
+ self.init_device()
82
+ self.bec_dispatcher.disconnect_slot(
83
+ self.on_device_readback, MessageEndpoints.device_readback(old_device)
84
+ )
85
+ self.bec_dispatcher.connect_slot(
86
+ self.on_device_readback, MessageEndpoints.device_readback(new_device)
87
+ )
88
+ self.ui.device_box.setTitle(new_device)
89
+ self.ui.readback.setToolTip(f"{self.device} readback")
90
+ self.ui.setpoint.setToolTip(f"{self.device} setpoint")
91
+ self.ui.step_size.setToolTip(f"Step size for {new_device}")
92
+
93
+ precision = self.dev[new_device].precision
94
+ if precision is not None:
95
+ self.ui.step_size.setDecimals(precision)
96
+ self.ui.step_size.setValue(10**-precision * 10)
97
+
98
+ @Slot(dict, dict)
99
+ def on_device_readback(self, msg_content: dict, metadata: dict):
100
+ signals = msg_content.get("signals", {})
101
+ # pylint: disable=protected-access
102
+ hinted_signals = self.dev[self.device]._hints
103
+ precision = self.dev[self.device].precision
104
+
105
+ readback_val = None
106
+ setpoint_val = None
107
+
108
+ if len(hinted_signals) == 1:
109
+ signal = hinted_signals[0]
110
+ readback_val = signals.get(signal, {}).get("value")
111
+
112
+ if f"{self.device}_setpoint" in signals:
113
+ setpoint_val = signals.get(f"{self.device}_setpoint", {}).get("value")
114
+
115
+ if f"{self.device}_motor_is_moving" in signals:
116
+ is_moving = signals.get(f"{self.device}_motor_is_moving", {}).get("value")
117
+ if is_moving:
118
+ self.ui.spinner_widget.start()
119
+ self.ui.spinner_widget.setToolTip("Device is moving")
120
+ else:
121
+ self.ui.spinner_widget.stop()
122
+ self.ui.spinner_widget.setToolTip("Device is idle")
123
+
124
+ if readback_val is not None:
125
+ self.ui.readback.setText(f"{readback_val:.{precision}f}")
126
+
127
+ if setpoint_val is not None:
128
+ self.ui.setpoint.setText(f"{setpoint_val:.{precision}f}")
129
+
130
+ limits = self.dev[self.device].limits
131
+ self.update_limits(limits)
132
+ if limits is not None and readback_val is not None and limits[0] != limits[1]:
133
+ pos = (readback_val - limits[0]) / (limits[1] - limits[0])
134
+ self.ui.position_indicator.on_position_update(pos)
135
+
136
+ def update_limits(self, limits):
137
+ if limits == self._limits:
138
+ return
139
+ self._limits = limits
140
+ if limits is not None and limits[0] != limits[1]:
141
+ self.ui.position_indicator.setToolTip(f"Min: {limits[0]}, Max: {limits[1]}")
142
+ self.setpoint_validator.setRange(limits[0], limits[1])
143
+ else:
144
+ self.ui.position_indicator.setToolTip("No limits set")
145
+ self.setpoint_validator.setRange(float("-inf"), float("inf"))
146
+
147
+ @Slot()
148
+ def on_stop(self):
149
+ request_id = str(uuid.uuid4())
150
+ params = {
151
+ "device": self.device,
152
+ "rpc_id": request_id,
153
+ "func": "stop",
154
+ "args": [],
155
+ "kwargs": {},
156
+ }
157
+ msg = ScanQueueMessage(
158
+ scan_type="device_rpc",
159
+ parameter=params,
160
+ queue="emergency",
161
+ metadata={"RID": request_id, "response": False},
162
+ )
163
+ self.client.connector.send(MessageEndpoints.scan_queue_request(), msg)
164
+
165
+ @property
166
+ def step_size(self):
167
+ return self.ui.step_size.value()
168
+
169
+ @Slot()
170
+ def on_tweak_right(self):
171
+ self.dev[self.device].move(self.step_size, relative=True)
172
+
173
+ @Slot()
174
+ def on_tweak_left(self):
175
+ self.dev[self.device].move(-self.step_size, relative=True)
176
+
177
+ @Slot()
178
+ def on_setpoint_change(self):
179
+ self.ui.setpoint.clearFocus()
180
+ setpoint = self.ui.setpoint.text()
181
+ self.dev[self.device].move(float(setpoint), relative=False)
182
+ self.ui.tweak_left.setToolTip(f"Tweak left by {self.step_size}")
183
+ self.ui.tweak_right.setToolTip(f"Tweak right by {self.step_size}")
184
+
185
+
186
+ if __name__ == "__main__": # pragma: no cover
187
+ import sys
188
+
189
+ import qdarktheme
190
+ from qtpy.QtWidgets import QApplication
191
+
192
+ app = QApplication(sys.argv)
193
+ qdarktheme.setup_theme("light")
194
+ widget = DeviceBox(device="samx")
195
+
196
+ widget.show()
197
+ sys.exit(app.exec_())
@@ -0,0 +1 @@
1
+ {'files': ['device_box.py']}
@@ -0,0 +1,179 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ui version="4.0">
3
+ <class>Form</class>
4
+ <widget class="QWidget" name="Form">
5
+ <property name="geometry">
6
+ <rect>
7
+ <x>0</x>
8
+ <y>0</y>
9
+ <width>251</width>
10
+ <height>289</height>
11
+ </rect>
12
+ </property>
13
+ <property name="minimumSize">
14
+ <size>
15
+ <width>0</width>
16
+ <height>192</height>
17
+ </size>
18
+ </property>
19
+ <property name="maximumSize">
20
+ <size>
21
+ <width>16777215</width>
22
+ <height>16777215</height>
23
+ </size>
24
+ </property>
25
+ <property name="windowTitle">
26
+ <string>Form</string>
27
+ </property>
28
+ <layout class="QVBoxLayout" name="verticalLayout">
29
+ <item>
30
+ <widget class="QGroupBox" name="device_box">
31
+ <property name="title">
32
+ <string>Device Name</string>
33
+ </property>
34
+ <layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0">
35
+ <property name="topMargin">
36
+ <number>0</number>
37
+ </property>
38
+ <item row="3" column="1">
39
+ <widget class="QDoubleSpinBox" name="step_size"/>
40
+ </item>
41
+ <item row="3" column="2">
42
+ <widget class="QToolButton" name="tweak_right">
43
+ <property name="minimumSize">
44
+ <size>
45
+ <width>50</width>
46
+ <height>50</height>
47
+ </size>
48
+ </property>
49
+ <property name="maximumSize">
50
+ <size>
51
+ <width>50</width>
52
+ <height>50</height>
53
+ </size>
54
+ </property>
55
+ <property name="text">
56
+ <string>...</string>
57
+ </property>
58
+ <property name="iconSize">
59
+ <size>
60
+ <width>30</width>
61
+ <height>30</height>
62
+ </size>
63
+ </property>
64
+ <property name="arrowType">
65
+ <enum>Qt::ArrowType::RightArrow</enum>
66
+ </property>
67
+ </widget>
68
+ </item>
69
+ <item row="2" column="0" colspan="3">
70
+ <widget class="QLineEdit" name="setpoint"/>
71
+ </item>
72
+ <item row="3" column="0">
73
+ <widget class="QToolButton" name="tweak_left">
74
+ <property name="minimumSize">
75
+ <size>
76
+ <width>50</width>
77
+ <height>50</height>
78
+ </size>
79
+ </property>
80
+ <property name="maximumSize">
81
+ <size>
82
+ <width>50</width>
83
+ <height>50</height>
84
+ </size>
85
+ </property>
86
+ <property name="text">
87
+ <string>...</string>
88
+ </property>
89
+ <property name="iconSize">
90
+ <size>
91
+ <width>30</width>
92
+ <height>30</height>
93
+ </size>
94
+ </property>
95
+ <property name="arrowType">
96
+ <enum>Qt::ArrowType::LeftArrow</enum>
97
+ </property>
98
+ </widget>
99
+ </item>
100
+ <item row="4" column="0" colspan="3">
101
+ <widget class="QPushButton" name="stop">
102
+ <property name="text">
103
+ <string>Stop</string>
104
+ </property>
105
+ </widget>
106
+ </item>
107
+ <item row="0" column="0" colspan="3">
108
+ <layout class="QVBoxLayout" name="verticalLayout_2">
109
+ <item>
110
+ <layout class="QHBoxLayout" name="horizontalLayout">
111
+ <item>
112
+ <spacer name="horizontalSpacer">
113
+ <property name="orientation">
114
+ <enum>Qt::Orientation::Horizontal</enum>
115
+ </property>
116
+ <property name="sizeType">
117
+ <enum>QSizePolicy::Policy::Expanding</enum>
118
+ </property>
119
+ <property name="sizeHint" stdset="0">
120
+ <size>
121
+ <width>40</width>
122
+ <height>20</height>
123
+ </size>
124
+ </property>
125
+ </spacer>
126
+ </item>
127
+ <item>
128
+ <widget class="SpinnerWidget" name="spinner_widget">
129
+ <property name="minimumSize">
130
+ <size>
131
+ <width>25</width>
132
+ <height>25</height>
133
+ </size>
134
+ </property>
135
+ <property name="maximumSize">
136
+ <size>
137
+ <width>25</width>
138
+ <height>25</height>
139
+ </size>
140
+ </property>
141
+ </widget>
142
+ </item>
143
+ </layout>
144
+ </item>
145
+ <item>
146
+ <widget class="PositionIndicator" name="position_indicator"/>
147
+ </item>
148
+ <item>
149
+ <widget class="QLabel" name="readback">
150
+ <property name="text">
151
+ <string>Position</string>
152
+ </property>
153
+ <property name="alignment">
154
+ <set>Qt::AlignmentFlag::AlignCenter</set>
155
+ </property>
156
+ </widget>
157
+ </item>
158
+ </layout>
159
+ </item>
160
+ </layout>
161
+ </widget>
162
+ </item>
163
+ </layout>
164
+ </widget>
165
+ <customwidgets>
166
+ <customwidget>
167
+ <class>SpinnerWidget</class>
168
+ <extends>QWidget</extends>
169
+ <header>spinner_widget</header>
170
+ </customwidget>
171
+ <customwidget>
172
+ <class>PositionIndicator</class>
173
+ <extends>QWidget</extends>
174
+ <header>position_indicator</header>
175
+ </customwidget>
176
+ </customwidgets>
177
+ <resources/>
178
+ <connections/>
179
+ </ui>
@@ -0,0 +1,54 @@
1
+ # Copyright (C) 2022 The Qt Company Ltd.
2
+ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
3
+
4
+ from qtpy.QtDesigner import QDesignerCustomWidgetInterface
5
+ from qtpy.QtGui import QIcon
6
+
7
+ from bec_widgets.widgets.device_box.device_box import DeviceBox
8
+
9
+ DOM_XML = """
10
+ <ui language='c++'>
11
+ <widget class='DeviceBox' name='device_box'>
12
+ </widget>
13
+ </ui>
14
+ """
15
+
16
+
17
+ class DeviceBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
18
+ def __init__(self):
19
+ super().__init__()
20
+ self._form_editor = None
21
+
22
+ def createWidget(self, parent):
23
+ t = DeviceBox(parent)
24
+ return t
25
+
26
+ def domXml(self):
27
+ return DOM_XML
28
+
29
+ def group(self):
30
+ return "Device Control"
31
+
32
+ def icon(self):
33
+ return QIcon()
34
+
35
+ def includeFile(self):
36
+ return "device_box"
37
+
38
+ def initialize(self, form_editor):
39
+ self._form_editor = form_editor
40
+
41
+ def isContainer(self):
42
+ return False
43
+
44
+ def isInitialized(self):
45
+ return self._form_editor is not None
46
+
47
+ def name(self):
48
+ return "DeviceBox"
49
+
50
+ def toolTip(self):
51
+ return "A widget for controlling a single positioner. "
52
+
53
+ def whatsThis(self):
54
+ return self.toolTip()
@@ -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.device_box.device_box_plugin import DeviceBoxPlugin
10
+
11
+ QPyDesignerCustomWidgetCollection.addCustomWidget(DeviceBoxPlugin())
12
+
13
+
14
+ if __name__ == "__main__": # pragma: no cover
15
+ main()