bec-widgets 0.82.2__py3-none-any.whl → 0.83.1__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 (46) hide show
  1. .gitlab-ci.yml +3 -0
  2. CHANGELOG.md +42 -46
  3. PKG-INFO +2 -2
  4. bec_widgets/examples/jupyter_console/jupyter_console_window.py +2 -2
  5. bec_widgets/qt_utils/toolbar.py +3 -1
  6. bec_widgets/utils/bec_connector.py +2 -0
  7. bec_widgets/utils/bec_widget.py +6 -0
  8. bec_widgets/utils/colors.py +27 -0
  9. bec_widgets/utils/generate_designer_plugin.py +6 -5
  10. bec_widgets/utils/reference_utils.py +92 -0
  11. bec_widgets/widgets/bec_status_box/bec_status_box.py +2 -2
  12. bec_widgets/widgets/console/console.py +1 -1
  13. bec_widgets/widgets/device_box/__init__.py +0 -0
  14. bec_widgets/widgets/device_box/device_box.py +197 -0
  15. bec_widgets/widgets/device_box/device_box.pyproject +1 -0
  16. bec_widgets/widgets/device_box/device_box.ui +179 -0
  17. bec_widgets/widgets/device_box/device_box_plugin.py +54 -0
  18. bec_widgets/widgets/device_box/register_device_box.py +15 -0
  19. bec_widgets/widgets/figure/figure.py +5 -6
  20. bec_widgets/widgets/figure/plots/axis_settings.py +2 -2
  21. bec_widgets/widgets/position_indicator/position_indicator.py +71 -0
  22. bec_widgets/widgets/position_indicator/position_indicator.pyproject +1 -0
  23. bec_widgets/widgets/position_indicator/position_indicator_plugin.py +54 -0
  24. bec_widgets/widgets/position_indicator/register_position_indicator.py +17 -0
  25. bec_widgets/widgets/scan_control/scan_control.py +2 -2
  26. bec_widgets/widgets/spinner/__init__.py +0 -0
  27. bec_widgets/widgets/spinner/register_spinner_widget.py +15 -0
  28. bec_widgets/widgets/spinner/spinner.py +86 -0
  29. bec_widgets/widgets/spinner/spinner_widget.pyproject +1 -0
  30. bec_widgets/widgets/spinner/spinner_widget_plugin.py +54 -0
  31. bec_widgets/widgets/vscode/vscode.py +0 -14
  32. bec_widgets/widgets/website/website.py +1 -1
  33. {bec_widgets-0.82.2.dist-info → bec_widgets-0.83.1.dist-info}/METADATA +2 -2
  34. {bec_widgets-0.82.2.dist-info → bec_widgets-0.83.1.dist-info}/RECORD +46 -24
  35. pyproject.toml +2 -2
  36. tests/references/SpinnerWidget/SpinnerWidget_darwin.png +0 -0
  37. tests/references/SpinnerWidget/SpinnerWidget_linux.png +0 -0
  38. tests/references/SpinnerWidget/SpinnerWidget_started_darwin.png +0 -0
  39. tests/references/SpinnerWidget/SpinnerWidget_started_linux.png +0 -0
  40. tests/unit_tests/client_mocks.py +9 -1
  41. tests/unit_tests/test_device_box.py +98 -0
  42. tests/unit_tests/test_spinner.py +30 -0
  43. tests/unit_tests/test_vscode_widget.py +27 -32
  44. {bec_widgets-0.82.2.dist-info → bec_widgets-0.83.1.dist-info}/WHEEL +0 -0
  45. {bec_widgets-0.82.2.dist-info → bec_widgets-0.83.1.dist-info}/entry_points.txt +0 -0
  46. {bec_widgets-0.82.2.dist-info → bec_widgets-0.83.1.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,47 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.83.1 (2024-07-14)
4
+
5
+ ### Fix
6
+
7
+ * fix(toolbar): default transparent background ([`eab7883`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/eab78839792f175b7ac127ca603385c6baa5ff15))
8
+
9
+ * fix: use apply_theme ([`2d4249e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2d4249e73a792fed1c2c7ab79bb8aec38c57466c))
10
+
11
+ * fix: spinner: update reference image for widget test, use apply_theme ([`63db135`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/63db1352ee883d35670b3a692dbe51d6d01872ae))
12
+
13
+ * fix: replace pyqtdarktheme by qdarkstyle, add 'apply_theme' function (in utils/colors.py) ([`8308115`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8308115f3646245d825fc47ab57297d3460bbcf5))
14
+
15
+ ### Test
16
+
17
+ * test(toolbar): added reference pngs for spinner for Darwin ([`11a7204`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/11a7204c98e0bf211a8721d296b45d24a3102b97))
18
+
19
+ ## v0.83.0 (2024-07-08)
20
+
21
+ ### Feature
22
+
23
+ * feat: added reference utils to compare renderings of widgets ([`2988fd3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2988fd387e6b8076fffec1d57e3ccab89ddb2aeb))
24
+
25
+ * feat(widgets): added device box with spinner ([`1b017ed`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1b017edfad8e78fa079210486123976695b8915c))
26
+
27
+ * feat(designer): added option to skip the widget validation for DesignerPluginGenerator ([`41bcb80`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/41bcb801674ab6c4d6069bba34ffee09c9e665db))
28
+
29
+ ### Fix
30
+
31
+ * fix(terminal): added default args to avoid designer crashes on startup ([`360d171`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/360d17135573e44b80ab517756da3c0b31daab0f))
32
+
33
+ * fix(widget): fixed widget cleanup routine ([`2b29e34`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2b29e34b52d056349647bb2fcf649b749a60d292))
34
+
35
+ * fix(bec_widget): added cleanup method to bec widget base class ([`fd8766e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fd8766ed87770661da6591aeb4df5abdaf38afc7))
36
+
37
+ * fix(website): fixed dummy input ([`903ce7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/903ce7d46b5d37d40486d0fda92d3694d3faca62))
38
+
39
+ ### Test
40
+
41
+ * test(vscode): fixed vscode tests for new cleanup routine ([`eb26e2a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/eb26e2a11b229a52efe2e6d4fb28d760d3740136))
42
+
43
+ * test(vscode): improved vscode test ([`5de8804`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5de8804da1e41eafad2472344904b3324438c13b))
44
+
3
45
  ## v0.82.2 (2024-07-08)
4
46
 
5
47
  ### Fix
@@ -99,49 +141,3 @@
99
141
  * fix: overwrite closeEvent and call super class ([`bc0ef78`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bc0ef7893ef100b71b62101c459655509b534a56))
100
142
 
101
143
  ## v0.79.1 (2024-07-03)
102
-
103
- ### Fix
104
-
105
- * fix: use libdir env var to preload Python library, also for Linux platform ([`d7718d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d7718d4dcb9728c050b6421388af4d484f3741f2))
106
-
107
- ## v0.79.0 (2024-07-03)
108
-
109
- ### Feature
110
-
111
- * feat(motor_map_widget): standalone MotorMap Widget with toolbar + plugin ([`6e75642`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6e756420907d7093557e945bc92bc4cfc0138d07))
112
-
113
- * feat(motor_map): method to reset history trace ([`5960918`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5960918137dd41cdeb94e50f8abc4f169cf45c11))
114
-
115
- ### Fix
116
-
117
- * fix(toolbar): change default color to black to match BECFigure theme ([`b8774e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b8774e0b0bc43dcd00f94f42539a778e507ca27d))
118
-
119
- * 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.1
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
@@ -14,9 +14,9 @@ Requires-Dist: bec-lib~=2.16
14
14
  Requires-Dist: black~=24.0
15
15
  Requires-Dist: isort>=5.13.2,~=5.13
16
16
  Requires-Dist: pydantic~=2.0
17
- Requires-Dist: pyqtdarktheme~=2.1
18
17
  Requires-Dist: pyqtgraph~=0.13
19
18
  Requires-Dist: pyte
19
+ Requires-Dist: qdarkstyle>=3.2.2
20
20
  Requires-Dist: qtconsole>=5.5.1,~=5.5
21
21
  Requires-Dist: qtpy~=2.4
22
22
  Provides-Extra: dev
@@ -2,7 +2,6 @@ import os
2
2
 
3
3
  import numpy as np
4
4
  import pyqtgraph as pg
5
- import qdarktheme
6
5
  from qtconsole.inprocess import QtInProcessKernelManager
7
6
  from qtconsole.rich_jupyter_widget import RichJupyterWidget
8
7
  from qtpy.QtCore import QSize
@@ -10,6 +9,7 @@ from qtpy.QtGui import QIcon
10
9
  from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
11
10
 
12
11
  from bec_widgets.utils import BECDispatcher, UILoader
12
+ from bec_widgets.utils.colors import apply_theme
13
13
  from bec_widgets.widgets.dock.dock_area import BECDockArea
14
14
  from bec_widgets.widgets.figure import BECFigure
15
15
  from bec_widgets.widgets.jupyter_console.jupyter_console import BECJupyterConsole
@@ -147,7 +147,7 @@ if __name__ == "__main__": # pragma: no cover
147
147
  app = QApplication(sys.argv)
148
148
  app.setApplicationName("Jupyter Console")
149
149
  app.setApplicationDisplayName("Jupyter Console")
150
- qdarktheme.setup_theme("auto")
150
+ apply_theme("dark")
151
151
  icon = QIcon()
152
152
  icon.addFile(os.path.join(module_path, "assets", "terminal_icon.png"), size=QSize(48, 48))
153
153
  app.setWindowIcon(icon)
@@ -26,7 +26,9 @@ class ModularToolBar(QToolBar):
26
26
  color (str, optional): The background color of the toolbar. Defaults to "black".
27
27
  """
28
28
 
29
- def __init__(self, parent=None, actions=None, target_widget=None, color: str = "black"):
29
+ def __init__(
30
+ self, parent=None, actions=None, target_widget=None, color: str = "rgba(255, 255, 255, 0)"
31
+ ):
30
32
  super().__init__(parent)
31
33
 
32
34
  self.widgets = defaultdict(dict)
@@ -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)
@@ -1,10 +1,37 @@
1
+ import itertools
1
2
  import re
2
3
  from typing import Literal
3
4
 
4
5
  import numpy as np
5
6
  import pyqtgraph as pg
7
+ import qdarkstyle
6
8
  from pydantic_core import PydanticCustomError
9
+ from qdarkstyle import DarkPalette, LightPalette
7
10
  from qtpy.QtGui import QColor
11
+ from qtpy.QtWidgets import QApplication
12
+
13
+ CURRENT_THEME = "dark"
14
+
15
+
16
+ def get_theme_palette():
17
+ return DarkPalette if CURRENT_THEME == "dark" else LightPalette
18
+
19
+
20
+ def apply_theme(theme: Literal["dark", "light"]):
21
+ global CURRENT_THEME
22
+ CURRENT_THEME = theme
23
+
24
+ app = QApplication.instance()
25
+ # go through all pyqtgraph widgets and set background
26
+ children = itertools.chain.from_iterable(
27
+ top.findChildren(pg.GraphicsLayoutWidget) for top in app.topLevelWidgets()
28
+ )
29
+ for pg_widget in children:
30
+ pg_widget.setBackground("k" if theme == "dark" else "w")
31
+
32
+ # now define stylesheet according to theme and apply it
33
+ style = qdarkstyle.load_stylesheet(palette=get_theme_palette())
34
+ app.setStyleSheet(style)
8
35
 
9
36
 
10
37
  class Colors:
@@ -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
@@ -9,12 +9,12 @@ from collections import defaultdict
9
9
  from dataclasses import dataclass
10
10
  from typing import TYPE_CHECKING
11
11
 
12
- import qdarktheme
13
12
  from bec_lib.utils.import_utils import lazy_import_from
14
13
  from qtpy.QtCore import QObject, QTimer, Signal, Slot
15
14
  from qtpy.QtWidgets import QHBoxLayout, QTreeWidget, QTreeWidgetItem, QWidget
16
15
 
17
16
  from bec_widgets.utils.bec_connector import BECConnector
17
+ from bec_widgets.utils.colors import apply_theme
18
18
  from bec_widgets.widgets.bec_status_box.status_item import StatusItem
19
19
 
20
20
  if TYPE_CHECKING:
@@ -306,7 +306,7 @@ def main():
306
306
  from qtpy.QtWidgets import QApplication
307
307
 
308
308
  app = QApplication(sys.argv)
309
- qdarktheme.setup_theme("auto")
309
+ apply_theme("dark")
310
310
  main_window = BECStatusBox()
311
311
  main_window.show()
312
312
  sys.exit(app.exec())
@@ -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
+ from bec_widgets.utils.colors import apply_theme
13
+
14
+
15
+ class DeviceBox(BECConnector, QWidget):
16
+ device_changed = Signal(str, str)
17
+
18
+ def __init__(self, parent=None, device=None, *args, **kwargs):
19
+ super().__init__(*args, **kwargs)
20
+ QWidget.__init__(self, parent=parent)
21
+ self.get_bec_shortcuts()
22
+ self._device = ""
23
+ self._limits = None
24
+
25
+ self.init_ui()
26
+
27
+ if device is not None:
28
+ self.device = device
29
+ self.init_device()
30
+
31
+ def init_ui(self):
32
+ self.device_changed.connect(self.on_device_change)
33
+
34
+ current_path = os.path.dirname(__file__)
35
+ self.ui = UILoader(self).loader(os.path.join(current_path, "device_box.ui"))
36
+
37
+ self.layout = QVBoxLayout(self)
38
+ self.layout.addWidget(self.ui)
39
+ self.layout.setSpacing(0)
40
+ self.layout.setContentsMargins(0, 0, 0, 0)
41
+
42
+ # fix the size of the device box
43
+ db = self.ui.device_box
44
+ db.setFixedHeight(234)
45
+ db.setFixedWidth(224)
46
+
47
+ self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
48
+ self.ui.stop.clicked.connect(self.on_stop)
49
+ self.ui.tweak_right.clicked.connect(self.on_tweak_right)
50
+ self.ui.tweak_right.setToolTip("Tweak right")
51
+ self.ui.tweak_left.clicked.connect(self.on_tweak_left)
52
+ self.ui.tweak_left.setToolTip("Tweak left")
53
+ self.ui.setpoint.returnPressed.connect(self.on_setpoint_change)
54
+
55
+ self.setpoint_validator = QDoubleValidator()
56
+ self.ui.setpoint.setValidator(self.setpoint_validator)
57
+ self.ui.spinner_widget.start()
58
+
59
+ def init_device(self):
60
+ if self.device in self.dev:
61
+ data = self.dev[self.device].read()
62
+ self.on_device_readback({"signals": data}, {})
63
+
64
+ @Property(str)
65
+ def device(self):
66
+ return self._device
67
+
68
+ @device.setter
69
+ def device(self, value):
70
+ if not value or not isinstance(value, str):
71
+ return
72
+ old_device = self._device
73
+ self._device = value
74
+ self.device_changed.emit(old_device, value)
75
+
76
+ @Slot(str, str)
77
+ def on_device_change(self, old_device: str, new_device: str):
78
+ if new_device not in self.dev:
79
+ print(f"Device {new_device} not found in the device list")
80
+ return
81
+ print(f"Device changed from {old_device} to {new_device}")
82
+ self.init_device()
83
+ self.bec_dispatcher.disconnect_slot(
84
+ self.on_device_readback, MessageEndpoints.device_readback(old_device)
85
+ )
86
+ self.bec_dispatcher.connect_slot(
87
+ self.on_device_readback, MessageEndpoints.device_readback(new_device)
88
+ )
89
+ self.ui.device_box.setTitle(new_device)
90
+ self.ui.readback.setToolTip(f"{self.device} readback")
91
+ self.ui.setpoint.setToolTip(f"{self.device} setpoint")
92
+ self.ui.step_size.setToolTip(f"Step size for {new_device}")
93
+
94
+ precision = self.dev[new_device].precision
95
+ if precision is not None:
96
+ self.ui.step_size.setDecimals(precision)
97
+ self.ui.step_size.setValue(10**-precision * 10)
98
+
99
+ @Slot(dict, dict)
100
+ def on_device_readback(self, msg_content: dict, metadata: dict):
101
+ signals = msg_content.get("signals", {})
102
+ # pylint: disable=protected-access
103
+ hinted_signals = self.dev[self.device]._hints
104
+ precision = self.dev[self.device].precision
105
+
106
+ readback_val = None
107
+ setpoint_val = None
108
+
109
+ if len(hinted_signals) == 1:
110
+ signal = hinted_signals[0]
111
+ readback_val = signals.get(signal, {}).get("value")
112
+
113
+ if f"{self.device}_setpoint" in signals:
114
+ setpoint_val = signals.get(f"{self.device}_setpoint", {}).get("value")
115
+
116
+ if f"{self.device}_motor_is_moving" in signals:
117
+ is_moving = signals.get(f"{self.device}_motor_is_moving", {}).get("value")
118
+ if is_moving:
119
+ self.ui.spinner_widget.start()
120
+ self.ui.spinner_widget.setToolTip("Device is moving")
121
+ else:
122
+ self.ui.spinner_widget.stop()
123
+ self.ui.spinner_widget.setToolTip("Device is idle")
124
+
125
+ if readback_val is not None:
126
+ self.ui.readback.setText(f"{readback_val:.{precision}f}")
127
+
128
+ if setpoint_val is not None:
129
+ self.ui.setpoint.setText(f"{setpoint_val:.{precision}f}")
130
+
131
+ limits = self.dev[self.device].limits
132
+ self.update_limits(limits)
133
+ if limits is not None and readback_val is not None and limits[0] != limits[1]:
134
+ pos = (readback_val - limits[0]) / (limits[1] - limits[0])
135
+ self.ui.position_indicator.on_position_update(pos)
136
+
137
+ def update_limits(self, limits):
138
+ if limits == self._limits:
139
+ return
140
+ self._limits = limits
141
+ if limits is not None and limits[0] != limits[1]:
142
+ self.ui.position_indicator.setToolTip(f"Min: {limits[0]}, Max: {limits[1]}")
143
+ self.setpoint_validator.setRange(limits[0], limits[1])
144
+ else:
145
+ self.ui.position_indicator.setToolTip("No limits set")
146
+ self.setpoint_validator.setRange(float("-inf"), float("inf"))
147
+
148
+ @Slot()
149
+ def on_stop(self):
150
+ request_id = str(uuid.uuid4())
151
+ params = {
152
+ "device": self.device,
153
+ "rpc_id": request_id,
154
+ "func": "stop",
155
+ "args": [],
156
+ "kwargs": {},
157
+ }
158
+ msg = ScanQueueMessage(
159
+ scan_type="device_rpc",
160
+ parameter=params,
161
+ queue="emergency",
162
+ metadata={"RID": request_id, "response": False},
163
+ )
164
+ self.client.connector.send(MessageEndpoints.scan_queue_request(), msg)
165
+
166
+ @property
167
+ def step_size(self):
168
+ return self.ui.step_size.value()
169
+
170
+ @Slot()
171
+ def on_tweak_right(self):
172
+ self.dev[self.device].move(self.step_size, relative=True)
173
+
174
+ @Slot()
175
+ def on_tweak_left(self):
176
+ self.dev[self.device].move(-self.step_size, relative=True)
177
+
178
+ @Slot()
179
+ def on_setpoint_change(self):
180
+ self.ui.setpoint.clearFocus()
181
+ setpoint = self.ui.setpoint.text()
182
+ self.dev[self.device].move(float(setpoint), relative=False)
183
+ self.ui.tweak_left.setToolTip(f"Tweak left by {self.step_size}")
184
+ self.ui.tweak_right.setToolTip(f"Tweak right by {self.step_size}")
185
+
186
+
187
+ if __name__ == "__main__": # pragma: no cover
188
+ import sys
189
+
190
+ from qtpy.QtWidgets import QApplication
191
+
192
+ app = QApplication(sys.argv)
193
+ apply_theme("light")
194
+ widget = DeviceBox(device="samx")
195
+
196
+ widget.show()
197
+ sys.exit(app.exec_())
@@ -0,0 +1 @@
1
+ {'files': ['device_box.py']}