bec-widgets 1.16.4__py3-none-any.whl → 1.17.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 (35) hide show
  1. CHANGELOG.md +41 -0
  2. PKG-INFO +1 -1
  3. bec_widgets/cli/client.py +46 -0
  4. bec_widgets/cli/server.py +3 -7
  5. bec_widgets/qt_utils/error_popups.py +3 -8
  6. bec_widgets/widgets/containers/dock/dock_area.py +1 -1
  7. bec_widgets/widgets/control/device_control/positioner_box/__init__.py +11 -0
  8. bec_widgets/widgets/control/device_control/positioner_box/_base/__init__.py +3 -0
  9. bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +243 -0
  10. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/__init__.py +0 -0
  11. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +242 -0
  12. bec_widgets/widgets/control/device_control/positioner_box/{positioner_box_plugin.py → positioner_box/positioner_box_plugin.py} +1 -1
  13. bec_widgets/widgets/control/device_control/positioner_box/{register_positioner_box.py → positioner_box/register_positioner_box.py} +1 -1
  14. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/__init__.py +0 -0
  15. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box2_d.pyproject +1 -0
  16. bec_widgets/widgets/control/{device_input/signal_combobox/signal_combo_box_plugin.py → device_control/positioner_box/positioner_box_2d/positioner_box2_d_plugin.py} +11 -9
  17. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +482 -0
  18. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +562 -0
  19. bec_widgets/widgets/control/{device_input/signal_combobox/register_signal_combo_box.py → device_control/positioner_box/positioner_box_2d/register_positioner_box2_d.py} +3 -3
  20. bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line.py → positioner_control_line/positioner_control_line.py} +5 -2
  21. bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line_plugin.py → positioner_control_line/positioner_control_line_plugin.py} +1 -3
  22. bec_widgets/widgets/control/device_control/positioner_box/{register_positioner_control_line.py → positioner_control_line/register_positioner_control_line.py} +1 -1
  23. bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +7 -6
  24. {bec_widgets-1.16.4.dist-info → bec_widgets-1.17.0.dist-info}/METADATA +1 -1
  25. {bec_widgets-1.16.4.dist-info → bec_widgets-1.17.0.dist-info}/RECORD +33 -27
  26. pyproject.toml +1 -1
  27. bec_widgets/widgets/control/device_control/positioner_box/positioner_box.py +0 -352
  28. bec_widgets/widgets/control/device_input/signal_combobox/signal_combo_box.pyproject +0 -1
  29. /bec_widgets/widgets/control/device_control/positioner_box/{positioner_box.pyproject → positioner_box/positioner_box.pyproject} +0 -0
  30. /bec_widgets/widgets/control/device_control/positioner_box/{positioner_box.ui → positioner_box/positioner_box.ui} +0 -0
  31. /bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line.pyproject → positioner_control_line/positioner_control_line.pyproject} +0 -0
  32. /bec_widgets/widgets/control/device_control/positioner_box/{positioner_control_line.ui → positioner_control_line/positioner_control_line.ui} +0 -0
  33. {bec_widgets-1.16.4.dist-info → bec_widgets-1.17.0.dist-info}/WHEEL +0 -0
  34. {bec_widgets-1.16.4.dist-info → bec_widgets-1.17.0.dist-info}/entry_points.txt +0 -0
  35. {bec_widgets-1.16.4.dist-info → bec_widgets-1.17.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md CHANGED
@@ -1,6 +1,47 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v1.17.0 (2025-01-23)
5
+
6
+ ### Bug Fixes
7
+
8
+ - Focus policy and tab order for positioner_box_2d
9
+ ([`6df5710`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6df57103bb57c97bedda570b07a31a3cc6e57d5d))
10
+
11
+ ### Documentation
12
+
13
+ - Add documentation for 2D positioner box
14
+ ([`9a8cc31`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9a8cc31f6c1fa5595f73c2a60372ef10d4c8eabb))
15
+
16
+ ### Features
17
+
18
+ - **widget**: Add 2d positioner box widget
19
+ ([`d2ffddb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2ffddb6d8d2473d8718f5aa650559902067ff12))
20
+
21
+ ### Refactoring
22
+
23
+ - Move positioner_box and line into submodule
24
+ ([`2419521`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2419521f5f05d8ff8ce975219629f77efb7fe6be))
25
+
26
+ PositionerBox and PositionerControlLine are now exported from from
27
+ bec_widgets.widgets.control.device_control.positioner_box, removing one level of hierarchy
28
+
29
+ - Move positioner_box logic to base class
30
+ ([`3770db5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3770db51be68a5f3fa65e0a67a4ed3efd1c7d6fe))
31
+
32
+
33
+ ## v1.16.5 (2025-01-22)
34
+
35
+ ### Bug Fixes
36
+
37
+ - **cli**: Server log level info and error
38
+ ([`df961a9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df961a9b885fa996e0ef44a36c937690670637c8))
39
+
40
+ - **error_popups**: Errors in SafeProperty and in SafeSlot are always logged, even with error
41
+ message popup enabled
42
+ ([`219d43d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/219d43d325260569e17a8eb7d56f63267d6e9649))
43
+
44
+
4
45
  ## v1.16.4 (2025-01-21)
5
46
 
6
47
  ### Bug Fixes
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 1.16.4
3
+ Version: 1.17.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
bec_widgets/cli/client.py CHANGED
@@ -34,6 +34,7 @@ class Widgets(str, enum.Enum):
34
34
  Minesweeper = "Minesweeper"
35
35
  PositionIndicator = "PositionIndicator"
36
36
  PositionerBox = "PositionerBox"
37
+ PositionerBox2D = "PositionerBox2D"
37
38
  PositionerControlLine = "PositionerControlLine"
38
39
  ResetButton = "ResetButton"
39
40
  ResumeButton = "ResumeButton"
@@ -3235,6 +3236,51 @@ class PositionerBox(RPCBase):
3235
3236
  """
3236
3237
 
3237
3238
 
3239
+ class PositionerBox2D(RPCBase):
3240
+ @rpc_call
3241
+ def set_positioner_hor(self, positioner: "str | Positioner"):
3242
+ """
3243
+ Set the device
3244
+
3245
+ Args:
3246
+ positioner (Positioner | str) : Positioner to set, accepts str or the device
3247
+ """
3248
+
3249
+ @rpc_call
3250
+ def set_positioner_ver(self, positioner: "str | Positioner"):
3251
+ """
3252
+ Set the device
3253
+
3254
+ Args:
3255
+ positioner (Positioner | str) : Positioner to set, accepts str or the device
3256
+ """
3257
+
3258
+
3259
+ class PositionerBoxBase(RPCBase):
3260
+ @property
3261
+ @rpc_call
3262
+ def _config_dict(self) -> "dict":
3263
+ """
3264
+ Get the configuration of the widget.
3265
+
3266
+ Returns:
3267
+ dict: The configuration of the widget.
3268
+ """
3269
+
3270
+ @rpc_call
3271
+ def _get_all_rpc(self) -> "dict":
3272
+ """
3273
+ Get all registered RPC objects.
3274
+ """
3275
+
3276
+ @property
3277
+ @rpc_call
3278
+ def _rpc_id(self) -> "str":
3279
+ """
3280
+ Get the RPC ID of the widget.
3281
+ """
3282
+
3283
+
3238
3284
  class PositionerControlLine(RPCBase):
3239
3285
  @rpc_call
3240
3286
  def set_positioner(self, positioner: "str | Positioner"):
bec_widgets/cli/server.py CHANGED
@@ -218,15 +218,11 @@ def main():
218
218
 
219
219
  args = parser.parse_args()
220
220
 
221
+ bec_logger.level = bec_logger.LOGLEVEL.INFO
221
222
  if args.hide:
222
- # if we start hidden, it means we are under control of the client
223
- # -> set the log level to critical to not see all the messages
224
223
  # pylint: disable=protected-access
225
- # bec_logger._stderr_log_level = bec_logger.LOGLEVEL.CRITICAL
226
- bec_logger.level = bec_logger.LOGLEVEL.CRITICAL
227
- else:
228
- # verbose log
229
- bec_logger.level = bec_logger.LOGLEVEL.DEBUG
224
+ bec_logger._stderr_log_level = bec_logger.LOGLEVEL.ERROR
225
+ bec_logger._update_sinks()
230
226
 
231
227
  if args.gui_class == "BECDockArea":
232
228
  gui_class = BECDockArea
@@ -46,8 +46,7 @@ def SafeProperty(prop_type, *prop_args, popup_error: bool = False, default=None,
46
46
 
47
47
  if popup_error:
48
48
  ErrorPopupUtility().custom_exception_hook(*sys.exc_info(), popup_error=True)
49
- else:
50
- logger.error(f"SafeProperty error in GETTER of '{prop_name}':\n{error_msg}")
49
+ logger.error(f"SafeProperty error in GETTER of '{prop_name}':\n{error_msg}")
51
50
  return default
52
51
 
53
52
  class PropertyWrapper:
@@ -74,10 +73,7 @@ def SafeProperty(prop_type, *prop_args, popup_error: bool = False, default=None,
74
73
  ErrorPopupUtility().custom_exception_hook(
75
74
  *sys.exc_info(), popup_error=True
76
75
  )
77
- else:
78
- logger.error(
79
- f"SafeProperty error in SETTER of '{prop_name}':\n{error_msg}"
80
- )
76
+ logger.error(f"SafeProperty error in SETTER of '{prop_name}':\n{error_msg}")
81
77
  return
82
78
 
83
79
  # Return the full read/write Property
@@ -116,8 +112,7 @@ def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
116
112
  ErrorPopupUtility().custom_exception_hook(
117
113
  *sys.exc_info(), popup_error=popup_error
118
114
  )
119
- else:
120
- logger.error(f"SafeSlot error in slot '{slot_name}':\n{error_msg}")
115
+ logger.error(f"SafeSlot error in slot '{slot_name}':\n{error_msg}")
121
116
 
122
117
  return wrapper
123
118
 
@@ -20,7 +20,7 @@ from bec_widgets.qt_utils.toolbar import (
20
20
  from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
21
21
  from bec_widgets.utils.bec_widget import BECWidget
22
22
  from bec_widgets.widgets.containers.dock.dock import BECDock, DockConfig
23
- from bec_widgets.widgets.control.device_control.positioner_box.positioner_box import PositionerBox
23
+ from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
24
24
  from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
25
25
  from bec_widgets.widgets.editors.vscode.vscode import VSCodeEditor
26
26
  from bec_widgets.widgets.plots.image.image_widget import BECImageWidget
@@ -0,0 +1,11 @@
1
+ from bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box import (
2
+ PositionerBox,
3
+ )
4
+ from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_2d.positioner_box_2d import (
5
+ PositionerBox2D,
6
+ )
7
+ from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line.positioner_control_line import (
8
+ PositionerControlLine,
9
+ )
10
+
11
+ __ALL__ = ["PositionerBox", "PositionerControlLine", "PositionerBox2D"]
@@ -0,0 +1,3 @@
1
+ from .positioner_box_base import PositionerBoxBase
2
+
3
+ __ALL__ = ["PositionerBoxBase"]
@@ -0,0 +1,243 @@
1
+ import uuid
2
+ from abc import abstractmethod
3
+ from ast import Tuple
4
+ from typing import Callable, TypedDict
5
+
6
+ from bec_lib.device import Positioner
7
+ from bec_lib.endpoints import MessageEndpoints
8
+ from bec_lib.logger import bec_logger
9
+ from bec_lib.messages import ScanQueueMessage
10
+ from qtpy.QtWidgets import (
11
+ QDialog,
12
+ QDoubleSpinBox,
13
+ QGroupBox,
14
+ QLabel,
15
+ QLineEdit,
16
+ QPushButton,
17
+ QVBoxLayout,
18
+ )
19
+
20
+ from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
21
+ from bec_widgets.utils.bec_widget import BECWidget
22
+ from bec_widgets.widgets.control.device_control.position_indicator.position_indicator import (
23
+ PositionIndicator,
24
+ )
25
+ from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
26
+ from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
27
+ DeviceLineEdit,
28
+ )
29
+ from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
30
+
31
+ logger = bec_logger.logger
32
+
33
+
34
+ class DeviceUpdateUIComponents(TypedDict):
35
+ spinner: SpinnerWidget
36
+ setpoint: QLineEdit
37
+ readback: QLabel
38
+ position_indicator: PositionIndicator
39
+ step_size: QDoubleSpinBox
40
+ device_box: QGroupBox
41
+ stop: QPushButton
42
+ tweak_increase: QPushButton
43
+ tweak_decrease: QPushButton
44
+
45
+
46
+ class PositionerBoxBase(BECWidget, CompactPopupWidget):
47
+ """Contains some core logic for positioner box widgets"""
48
+
49
+ current_path = ""
50
+ ICON_NAME = "switch_right"
51
+
52
+ def __init__(self, parent=None, **kwargs):
53
+ """Initialize the PositionerBox widget.
54
+
55
+ Args:
56
+ parent: The parent widget.
57
+ device (Positioner): The device to control.
58
+ """
59
+ super().__init__(**kwargs)
60
+ CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
61
+ self._dialog = None
62
+ self.get_bec_shortcuts()
63
+
64
+ def _check_device_is_valid(self, device: str):
65
+ """Check if the device is a positioner
66
+
67
+ Args:
68
+ device (str): The device name
69
+ """
70
+ if device not in self.dev:
71
+ logger.info(f"Device {device} not found in the device list")
72
+ return False
73
+ if not isinstance(self.dev[device], Positioner):
74
+ logger.info(f"Device {device} is not a positioner")
75
+ return False
76
+ return True
77
+
78
+ @abstractmethod
79
+ def _device_ui_components(self, device: str) -> DeviceUpdateUIComponents: ...
80
+
81
+ def _init_device(
82
+ self,
83
+ device: str,
84
+ position_emit: Callable[[float], None],
85
+ limit_update: Callable[[tuple[float, float]], None],
86
+ ):
87
+ """Init the device view and readback"""
88
+ if self._check_device_is_valid(device):
89
+ data = self.dev[device].read()
90
+ self._on_device_readback(
91
+ device,
92
+ self._device_ui_components(device),
93
+ {"signals": data},
94
+ {},
95
+ position_emit,
96
+ limit_update,
97
+ )
98
+
99
+ def _stop_device(self, device: str):
100
+ """Stop call"""
101
+ request_id = str(uuid.uuid4())
102
+ params = {"device": device, "rpc_id": request_id, "func": "stop", "args": [], "kwargs": {}}
103
+ msg = ScanQueueMessage(
104
+ scan_type="device_rpc",
105
+ parameter=params,
106
+ queue="emergency",
107
+ metadata={"RID": request_id, "response": False},
108
+ )
109
+ self.client.connector.send(MessageEndpoints.scan_queue_request(), msg)
110
+
111
+ # pylint: disable=unused-argument
112
+ def _on_device_readback(
113
+ self,
114
+ device: str,
115
+ ui_components: DeviceUpdateUIComponents,
116
+ msg_content: dict,
117
+ metadata: dict,
118
+ position_emit: Callable[[float], None],
119
+ limit_update: Callable[[tuple[float, float]], None],
120
+ ):
121
+ signals = msg_content.get("signals", {})
122
+ # pylint: disable=protected-access
123
+ hinted_signals = self.dev[device]._hints
124
+ precision = self.dev[device].precision
125
+
126
+ spinner = ui_components["spinner"]
127
+ position_indicator = ui_components["position_indicator"]
128
+ readback = ui_components["readback"]
129
+ setpoint = ui_components["setpoint"]
130
+
131
+ readback_val = None
132
+ setpoint_val = None
133
+
134
+ if len(hinted_signals) == 1:
135
+ signal = hinted_signals[0]
136
+ readback_val = signals.get(signal, {}).get("value")
137
+
138
+ for setpoint_signal in ["setpoint", "user_setpoint"]:
139
+ setpoint_val = signals.get(f"{device}_{setpoint_signal}", {}).get("value")
140
+ if setpoint_val is not None:
141
+ break
142
+
143
+ for moving_signal in ["motor_done_move", "motor_is_moving"]:
144
+ is_moving = signals.get(f"{device}_{moving_signal}", {}).get("value")
145
+ if is_moving is not None:
146
+ break
147
+
148
+ if is_moving is not None:
149
+ spinner.setVisible(True)
150
+ if is_moving:
151
+ spinner.start()
152
+ spinner.setToolTip("Device is moving")
153
+ self.set_global_state("warning")
154
+ else:
155
+ spinner.stop()
156
+ spinner.setToolTip("Device is idle")
157
+ self.set_global_state("success")
158
+ else:
159
+ spinner.setVisible(False)
160
+
161
+ if readback_val is not None:
162
+ readback.setText(f"{readback_val:.{precision}f}")
163
+ position_emit(readback_val)
164
+
165
+ if setpoint_val is not None:
166
+ setpoint.setText(f"{setpoint_val:.{precision}f}")
167
+
168
+ limits = self.dev[device].limits
169
+ limit_update(limits)
170
+ if limits is not None and readback_val is not None and limits[0] != limits[1]:
171
+ pos = (readback_val - limits[0]) / (limits[1] - limits[0])
172
+ position_indicator.set_value(pos)
173
+
174
+ def _update_limits_ui(
175
+ self, limits: tuple[float, float], position_indicator, setpoint_validator
176
+ ):
177
+ if limits is not None and limits[0] != limits[1]:
178
+ position_indicator.setToolTip(f"Min: {limits[0]}, Max: {limits[1]}")
179
+ setpoint_validator.setRange(limits[0], limits[1])
180
+ else:
181
+ position_indicator.setToolTip("No limits set")
182
+ setpoint_validator.setRange(float("-inf"), float("inf"))
183
+
184
+ def _update_device_ui(self, device: str, ui: DeviceUpdateUIComponents):
185
+ ui["device_box"].setTitle(device)
186
+ ui["readback"].setToolTip(f"{device} readback")
187
+ ui["setpoint"].setToolTip(f"{device} setpoint")
188
+ ui["step_size"].setToolTip(f"Step size for {device}")
189
+ precision = self.dev[device].precision
190
+ if precision is not None:
191
+ ui["step_size"].setDecimals(precision)
192
+ ui["step_size"].setValue(10**-precision * 10)
193
+
194
+ def _swap_readback_signal_connection(self, slot, old_device, new_device):
195
+ self.bec_dispatcher.disconnect_slot(slot, MessageEndpoints.device_readback(old_device))
196
+ self.bec_dispatcher.connect_slot(slot, MessageEndpoints.device_readback(new_device))
197
+
198
+ def _toggle_enable_buttons(self, ui: DeviceUpdateUIComponents, enable: bool) -> None:
199
+ """Toogle enable/disable on available buttons
200
+
201
+ Args:
202
+ enable (bool): Enable buttons
203
+ """
204
+ ui["tweak_increase"].setEnabled(enable)
205
+ ui["tweak_decrease"].setEnabled(enable)
206
+ ui["stop"].setEnabled(enable)
207
+ ui["setpoint"].setEnabled(enable)
208
+ ui["step_size"].setEnabled(enable)
209
+
210
+ def _on_device_change(
211
+ self,
212
+ old_device: str,
213
+ new_device: str,
214
+ position_emit: Callable[[float], None],
215
+ limit_update: Callable[[tuple[float, float]], None],
216
+ on_device_readback: Callable,
217
+ ui: DeviceUpdateUIComponents,
218
+ ):
219
+ logger.info(f"Device changed from {old_device} to {new_device}")
220
+ self._toggle_enable_buttons(ui, True)
221
+ self._init_device(new_device, position_emit, limit_update)
222
+ self._swap_readback_signal_connection(on_device_readback, old_device, new_device)
223
+ self._update_device_ui(new_device, ui)
224
+
225
+ def _open_dialog_selection(self, set_positioner: Callable):
226
+ def _ods():
227
+ """Open dialog window for positioner selection"""
228
+ self._dialog = QDialog(self)
229
+ self._dialog.setWindowTitle("Positioner Selection")
230
+ layout = QVBoxLayout()
231
+ line_edit = DeviceLineEdit(
232
+ self, client=self.client, device_filter=[BECDeviceFilter.POSITIONER]
233
+ )
234
+ line_edit.textChanged.connect(set_positioner)
235
+ layout.addWidget(line_edit)
236
+ close_button = QPushButton("Close")
237
+ close_button.clicked.connect(self._dialog.accept)
238
+ layout.addWidget(close_button)
239
+ self._dialog.setLayout(layout)
240
+ self._dialog.exec()
241
+ self._dialog = None
242
+
243
+ return _ods
@@ -0,0 +1,242 @@
1
+ """ Module for a PositionerBox widget to control a positioner device."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from bec_lib.device import Positioner
8
+ from bec_lib.logger import bec_logger
9
+ from bec_qthemes import material_icon
10
+ from qtpy.QtCore import Signal
11
+ from qtpy.QtGui import QDoubleValidator
12
+ from qtpy.QtWidgets import QDoubleSpinBox
13
+
14
+ from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
15
+ from bec_widgets.utils import UILoader
16
+ from bec_widgets.utils.colors import get_accent_colors, set_theme
17
+ from bec_widgets.widgets.control.device_control.positioner_box._base import PositionerBoxBase
18
+ from bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base import (
19
+ DeviceUpdateUIComponents,
20
+ )
21
+
22
+ logger = bec_logger.logger
23
+
24
+ MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
25
+
26
+
27
+ class PositionerBox(PositionerBoxBase):
28
+ """Simple Widget to control a positioner in box form"""
29
+
30
+ ui_file = "positioner_box.ui"
31
+ dimensions = (234, 224)
32
+
33
+ PLUGIN = True
34
+
35
+ USER_ACCESS = ["set_positioner"]
36
+ device_changed = Signal(str, str)
37
+ # Signal emitted to inform listeners about a position update
38
+ position_update = Signal(float)
39
+
40
+ def __init__(self, parent=None, device: Positioner | str | None = None, **kwargs):
41
+ """Initialize the PositionerBox widget.
42
+
43
+ Args:
44
+ parent: The parent widget.
45
+ device (Positioner): The device to control.
46
+ """
47
+ super().__init__(parent=parent, **kwargs)
48
+
49
+ self._device = ""
50
+ self._limits = None
51
+ if self.current_path == "":
52
+ self.current_path = os.path.dirname(__file__)
53
+
54
+ self.init_ui()
55
+ self.device = device
56
+ self._init_device(self.device, self.position_update.emit, self.update_limits)
57
+
58
+ def init_ui(self):
59
+ """Init the ui"""
60
+ self.device_changed.connect(self.on_device_change)
61
+
62
+ self.ui = UILoader(self).loader(os.path.join(self.current_path, self.ui_file))
63
+
64
+ self.addWidget(self.ui)
65
+ self.layout.setSpacing(0)
66
+ self.layout.setContentsMargins(0, 0, 0, 0)
67
+
68
+ # fix the size of the device box
69
+ db = self.ui.device_box
70
+ db.setFixedHeight(self.dimensions[0])
71
+ db.setFixedWidth(self.dimensions[1])
72
+
73
+ self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
74
+ self.ui.stop.clicked.connect(self.on_stop)
75
+ self.ui.stop.setToolTip("Stop")
76
+ self.ui.stop.setStyleSheet(
77
+ f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
78
+ )
79
+ self.ui.tweak_right.clicked.connect(self.on_tweak_right)
80
+ self.ui.tweak_right.setToolTip("Tweak right")
81
+ self.ui.tweak_left.clicked.connect(self.on_tweak_left)
82
+ self.ui.tweak_left.setToolTip("Tweak left")
83
+ self.ui.setpoint.returnPressed.connect(self.on_setpoint_change)
84
+
85
+ self.setpoint_validator = QDoubleValidator()
86
+ self.ui.setpoint.setValidator(self.setpoint_validator)
87
+ self.ui.spinner_widget.start()
88
+ self.ui.tool_button.clicked.connect(self._open_dialog_selection(self.set_positioner))
89
+ icon = material_icon(icon_name="edit_note", size=(16, 16), convert_to_pixmap=False)
90
+ self.ui.tool_button.setIcon(icon)
91
+
92
+ def force_update_readback(self):
93
+ self._init_device(self.device, self.position_update.emit, self.update_limits)
94
+
95
+ @SafeProperty(str)
96
+ def device(self):
97
+ """Property to set the device"""
98
+ return self._device
99
+
100
+ @device.setter
101
+ def device(self, value: str):
102
+ """Setter, checks if device is a string"""
103
+ if not value or not isinstance(value, str):
104
+ return
105
+ if not self._check_device_is_valid(value):
106
+ return
107
+ old_device = self._device
108
+ self._device = value
109
+ if not self.label:
110
+ self.label = value
111
+ self.device_changed.emit(old_device, value)
112
+
113
+ @SafeProperty(bool)
114
+ def hide_device_selection(self):
115
+ """Hide the device selection"""
116
+ return not self.ui.tool_button.isVisible()
117
+
118
+ @hide_device_selection.setter
119
+ def hide_device_selection(self, value: bool):
120
+ """Set the device selection visibility"""
121
+ self.ui.tool_button.setVisible(not value)
122
+
123
+ @SafeSlot(bool)
124
+ def show_device_selection(self, value: bool):
125
+ """Show the device selection
126
+
127
+ Args:
128
+ value (bool): Show the device selection
129
+ """
130
+ self.hide_device_selection = not value
131
+
132
+ @SafeSlot(str)
133
+ def set_positioner(self, positioner: str | Positioner):
134
+ """Set the device
135
+
136
+ Args:
137
+ positioner (Positioner | str) : Positioner to set, accepts str or the device
138
+ """
139
+ if isinstance(positioner, Positioner):
140
+ positioner = positioner.name
141
+ self.device = positioner
142
+
143
+ @SafeSlot(str, str)
144
+ def on_device_change(self, old_device: str, new_device: str):
145
+ """Upon changing the device, a check will be performed if the device is a Positioner.
146
+
147
+ Args:
148
+ old_device (str): The old device name.
149
+ new_device (str): The new device name.
150
+ """
151
+ if not self._check_device_is_valid(new_device):
152
+ return
153
+ self._on_device_change(
154
+ old_device,
155
+ new_device,
156
+ self.position_update.emit,
157
+ self.update_limits,
158
+ self.on_device_readback,
159
+ self._device_ui_components(new_device),
160
+ )
161
+
162
+ def _device_ui_components(self, device: str) -> DeviceUpdateUIComponents:
163
+ return {
164
+ "spinner": self.ui.spinner_widget,
165
+ "position_indicator": self.ui.position_indicator,
166
+ "readback": self.ui.readback,
167
+ "setpoint": self.ui.setpoint,
168
+ "step_size": self.ui.step_size,
169
+ "device_box": self.ui.device_box,
170
+ "stop": self.ui.stop,
171
+ "tweak_increase": self.ui.tweak_right,
172
+ "tweak_decrease": self.ui.tweak_left,
173
+ }
174
+
175
+ @SafeSlot(dict, dict)
176
+ def on_device_readback(self, msg_content: dict, metadata: dict):
177
+ """Callback for device readback.
178
+
179
+ Args:
180
+ msg_content (dict): The message content.
181
+ metadata (dict): The message metadata.
182
+ """
183
+ self._on_device_readback(
184
+ self.device,
185
+ self._device_ui_components(self.device),
186
+ msg_content,
187
+ metadata,
188
+ self.position_update.emit,
189
+ self.update_limits,
190
+ )
191
+
192
+ def update_limits(self, limits: tuple):
193
+ """Update limits
194
+
195
+ Args:
196
+ limits (tuple): Limits of the positioner
197
+ """
198
+ if limits == self._limits:
199
+ return
200
+ self._limits = limits
201
+ self._update_limits_ui(limits, self.ui.position_indicator, self.setpoint_validator)
202
+
203
+ @SafeSlot()
204
+ def on_stop(self):
205
+ self._stop_device(self.device)
206
+
207
+ @property
208
+ def step_size(self):
209
+ """Step size for tweak"""
210
+ return self.ui.step_size.value()
211
+
212
+ @SafeSlot()
213
+ def on_tweak_right(self):
214
+ """Tweak motor right"""
215
+ self.dev[self.device].move(self.step_size, relative=True)
216
+
217
+ @SafeSlot()
218
+ def on_tweak_left(self):
219
+ """Tweak motor left"""
220
+ self.dev[self.device].move(-self.step_size, relative=True)
221
+
222
+ @SafeSlot()
223
+ def on_setpoint_change(self):
224
+ """Change the setpoint for the motor"""
225
+ self.ui.setpoint.clearFocus()
226
+ setpoint = self.ui.setpoint.text()
227
+ self.dev[self.device].move(float(setpoint), relative=False)
228
+ self.ui.tweak_left.setToolTip(f"Tweak left by {self.step_size}")
229
+ self.ui.tweak_right.setToolTip(f"Tweak right by {self.step_size}")
230
+
231
+
232
+ if __name__ == "__main__": # pragma: no cover
233
+ import sys
234
+
235
+ from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
236
+
237
+ app = QApplication(sys.argv)
238
+ set_theme("dark")
239
+ widget = PositionerBox(device="bpm4i")
240
+
241
+ widget.show()
242
+ sys.exit(app.exec_())