bec-widgets 0.118.0__py3-none-any.whl → 1.0.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 (26) hide show
  1. CHANGELOG.md +63 -67
  2. PKG-INFO +1 -1
  3. bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +33 -89
  4. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +413 -715
  5. bec_widgets/cli/client.py +22 -0
  6. bec_widgets/qt_utils/compact_popup.py +68 -23
  7. bec_widgets/utils/crosshair.py +11 -2
  8. bec_widgets/widgets/device_line_edit/device_line_edit.py +15 -1
  9. bec_widgets/widgets/figure/plots/waveform/waveform.py +15 -0
  10. bec_widgets/widgets/figure/plots/waveform/waveform_curve.py +3 -2
  11. bec_widgets/widgets/positioner_box/positioner_box.py +10 -4
  12. bec_widgets/widgets/positioner_group/__init__.py +0 -0
  13. bec_widgets/widgets/positioner_group/positioner_group.py +170 -0
  14. bec_widgets/widgets/positioner_group/positioner_group.pyproject +1 -0
  15. bec_widgets/widgets/positioner_group/positioner_group_plugin.py +57 -0
  16. bec_widgets/widgets/positioner_group/register_positioner_group.py +15 -0
  17. bec_widgets/widgets/scan_control/scan_control.py +74 -122
  18. bec_widgets/widgets/scan_control/scan_group_box.py +66 -11
  19. bec_widgets/widgets/stop_button/stop_button.py +1 -1
  20. bec_widgets/widgets/waveform/waveform_widget.py +10 -0
  21. {bec_widgets-0.118.0.dist-info → bec_widgets-1.0.0.dist-info}/METADATA +1 -1
  22. {bec_widgets-0.118.0.dist-info → bec_widgets-1.0.0.dist-info}/RECORD +26 -21
  23. pyproject.toml +1 -1
  24. {bec_widgets-0.118.0.dist-info → bec_widgets-1.0.0.dist-info}/WHEEL +0 -0
  25. {bec_widgets-0.118.0.dist-info → bec_widgets-1.0.0.dist-info}/entry_points.txt +0 -0
  26. {bec_widgets-0.118.0.dist-info → bec_widgets-1.0.0.dist-info}/licenses/LICENSE +0 -0
bec_widgets/cli/client.py CHANGED
@@ -552,6 +552,7 @@ class BECFigure(RPCBase):
552
552
  def image(
553
553
  self,
554
554
  monitor: "str" = None,
555
+ monitor_type: "Literal['1d', '2d']" = "2d",
555
556
  color_bar: "Literal['simple', 'full']" = "full",
556
557
  color_map: "str" = "magma",
557
558
  data: "np.ndarray" = None,
@@ -856,6 +857,7 @@ class BECImageShow(RPCBase):
856
857
  def image(
857
858
  self,
858
859
  monitor: "str",
860
+ monitor_type: "Literal['1d', '2d']" = "2d",
859
861
  color_map: "Optional[str]" = "magma",
860
862
  color_bar: "Optional[Literal['simple', 'full']]" = "full",
861
863
  downsample: "Optional[bool]" = True,
@@ -868,6 +870,7 @@ class BECImageShow(RPCBase):
868
870
 
869
871
  Args:
870
872
  monitor(str): The name of the monitor to display.
873
+ monitor_type(Literal["1d","2d"]): The type of monitor to display.
871
874
  color_bar(Literal["simple","full"]): The type of color bar to display.
872
875
  color_map(str): The color map to use for the image.
873
876
  data(np.ndarray): Custom data to display.
@@ -1180,6 +1183,7 @@ class BECImageWidget(RPCBase):
1180
1183
  def image(
1181
1184
  self,
1182
1185
  monitor: "str",
1186
+ monitor_type: "Optional[Literal['1d', '2d']]" = "2d",
1183
1187
  color_map: "Optional[str]" = "magma",
1184
1188
  color_bar: "Optional[Literal['simple', 'full']]" = "full",
1185
1189
  downsample: "Optional[bool]" = True,
@@ -2096,6 +2100,15 @@ class BECWaveform(RPCBase):
2096
2100
  colormap(str, optional): Scale the colors of curves to colormap. If None, use the default color palette.
2097
2101
  """
2098
2102
 
2103
+ @rpc_call
2104
+ def enable_scatter(self, enable: "bool"):
2105
+ """
2106
+ Enable/Disable scatter plot on all curves.
2107
+
2108
+ Args:
2109
+ enable(bool): If True, enable scatter markers; if False, disable them.
2110
+ """
2111
+
2099
2112
  @rpc_call
2100
2113
  def enable_fps_monitor(self, enable: "bool" = True):
2101
2114
  """
@@ -2419,6 +2432,15 @@ class BECWaveformWidget(RPCBase):
2419
2432
  enabled(bool): If True, enable the FPS monitor.
2420
2433
  """
2421
2434
 
2435
+ @rpc_call
2436
+ def enable_scatter(self, enabled: "bool"):
2437
+ """
2438
+ Enable the scatter plot of the plot widget.
2439
+
2440
+ Args:
2441
+ enabled(bool): If True, enable the scatter plot.
2442
+ """
2443
+
2422
2444
  @rpc_call
2423
2445
  def lock_aspect_ratio(self, lock: "bool"):
2424
2446
  """
@@ -1,7 +1,8 @@
1
+ import time
1
2
  from types import SimpleNamespace
2
3
 
3
4
  from bec_qthemes import material_icon
4
- from qtpy.QtCore import Property, Qt
5
+ from qtpy.QtCore import Property, Qt, Signal
5
6
  from qtpy.QtGui import QColor
6
7
  from qtpy.QtWidgets import (
7
8
  QDialog,
@@ -9,6 +10,7 @@ from qtpy.QtWidgets import (
9
10
  QLabel,
10
11
  QPushButton,
11
12
  QSizePolicy,
13
+ QSpacerItem,
12
14
  QVBoxLayout,
13
15
  QWidget,
14
16
  )
@@ -98,6 +100,7 @@ class PopupDialog(QDialog):
98
100
  def closeEvent(self, event):
99
101
  self.content_widget.setVisible(False)
100
102
  self.content_widget.setParent(self.parent)
103
+ self.done(True)
101
104
 
102
105
 
103
106
  class CompactPopupWidget(QWidget):
@@ -107,29 +110,35 @@ class CompactPopupWidget(QWidget):
107
110
  In the compact form, a LED-like indicator shows a status indicator.
108
111
  """
109
112
 
113
+ expand = Signal(bool)
114
+
110
115
  def __init__(self, parent=None, layout=QVBoxLayout):
111
116
  super().__init__(parent)
112
117
 
113
118
  self._popup_window = None
119
+ self._expand_popup = True
114
120
 
115
121
  QVBoxLayout(self)
116
- self.compact_view = QWidget(self)
117
- self.compact_view.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
118
- QHBoxLayout(self.compact_view)
119
- self.compact_view.layout().setSpacing(0)
120
- self.compact_view.layout().setContentsMargins(0, 0, 0, 0)
121
- self.compact_label = QLabel(self.compact_view)
122
- self.compact_status = LedLabel(self.compact_view)
123
- self.compact_show_popup = QPushButton(self.compact_view)
122
+ self.compact_view_widget = QWidget(self)
123
+ self.compact_view_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
124
+ QHBoxLayout(self.compact_view_widget)
125
+ self.compact_view_widget.layout().setSpacing(0)
126
+ self.compact_view_widget.layout().setContentsMargins(0, 0, 0, 0)
127
+ self.compact_view_widget.layout().addSpacerItem(
128
+ QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed)
129
+ )
130
+ self.compact_label = QLabel(self.compact_view_widget)
131
+ self.compact_status = LedLabel(self.compact_view_widget)
132
+ self.compact_show_popup = QPushButton(self.compact_view_widget)
124
133
  self.compact_show_popup.setFlat(True)
125
134
  self.compact_show_popup.setIcon(
126
- material_icon(icon_name="pan_zoom", size=(10, 10), convert_to_pixmap=False)
135
+ material_icon(icon_name="expand_content", size=(10, 10), convert_to_pixmap=False)
127
136
  )
128
- self.compact_view.layout().addWidget(self.compact_label)
129
- self.compact_view.layout().addWidget(self.compact_status)
130
- self.compact_view.layout().addWidget(self.compact_show_popup)
131
- self.compact_view.setVisible(False)
132
- self.layout().addWidget(self.compact_view)
137
+ self.compact_view_widget.layout().addWidget(self.compact_label)
138
+ self.compact_view_widget.layout().addWidget(self.compact_status)
139
+ self.compact_view_widget.layout().addWidget(self.compact_show_popup)
140
+ self.compact_view_widget.setVisible(False)
141
+ self.layout().addWidget(self.compact_view_widget)
133
142
  self.container = QWidget(self)
134
143
  self.layout().addWidget(self.container)
135
144
  self.container.setVisible(True)
@@ -148,8 +157,36 @@ class CompactPopupWidget(QWidget):
148
157
 
149
158
  def show_popup(self):
150
159
  """Display the contained widgets in a popup dialog"""
151
- self._popup_window = PopupDialog(self.container)
152
- self._popup_window.show()
160
+ if self._expand_popup:
161
+ # show popup
162
+ self._popup_window = PopupDialog(self.container)
163
+ self._popup_window.show()
164
+ self._popup_window.finished.connect(lambda: self.expand.emit(False))
165
+ self.expand.emit(True)
166
+ else:
167
+ if self.compact_view:
168
+ # expand in place
169
+ self.compact_view = False
170
+ self.compact_view_widget.setVisible(True)
171
+ self.compact_label.setVisible(False)
172
+ self.compact_status.setVisible(False)
173
+ self.compact_show_popup.setIcon(
174
+ material_icon(
175
+ icon_name="collapse_content", size=(10, 10), convert_to_pixmap=False
176
+ )
177
+ )
178
+ self.expand.emit(True)
179
+ else:
180
+ # back to compact form
181
+ self.compact_label.setVisible(True)
182
+ self.compact_status.setVisible(True)
183
+ self.compact_show_popup.setIcon(
184
+ material_icon(
185
+ icon_name="expand_content", size=(10, 10), convert_to_pixmap=False
186
+ )
187
+ )
188
+ self.compact_view = True
189
+ self.expand.emit(False)
153
190
 
154
191
  def setSizePolicy(self, size_policy1, size_policy2=None):
155
192
  # setting size policy on the compact popup widget will set
@@ -172,11 +209,11 @@ class CompactPopupWidget(QWidget):
172
209
  self.container.layout().addWidget(widget)
173
210
 
174
211
  @Property(bool)
175
- def compact(self):
176
- return self.compact_view.isVisible()
212
+ def compact_view(self):
213
+ return self.compact_label.isVisible()
177
214
 
178
- @compact.setter
179
- def compact(self, set_compact: bool):
215
+ @compact_view.setter
216
+ def compact_view(self, set_compact: bool):
180
217
  """Sets the compact form
181
218
 
182
219
  If set_compact is True, the compact view is displayed ; otherwise,
@@ -184,11 +221,11 @@ class CompactPopupWidget(QWidget):
184
221
  the container widget or the compact view widget.
185
222
  """
186
223
  if set_compact:
187
- self.compact_view.setVisible(True)
224
+ self.compact_view_widget.setVisible(True)
188
225
  self.container.setVisible(False)
189
226
  QWidget.setSizePolicy(self, QSizePolicy.Fixed, QSizePolicy.Fixed)
190
227
  else:
191
- self.compact_view.setVisible(False)
228
+ self.compact_view_widget.setVisible(False)
192
229
  self.container.setVisible(True)
193
230
  QWidget.setSizePolicy(self, self.container.sizePolicy())
194
231
  if self.parentWidget():
@@ -215,6 +252,14 @@ class CompactPopupWidget(QWidget):
215
252
  self.compact_label.setToolTip(tooltip)
216
253
  self.compact_status.setToolTip(tooltip)
217
254
 
255
+ @Property(bool)
256
+ def expand_popup(self):
257
+ return self._expand_popup
258
+
259
+ @expand_popup.setter
260
+ def expand_popup(self, popup: bool):
261
+ self._expand_popup = popup
262
+
218
263
  def closeEvent(self, event):
219
264
  # Called by Qt, on closing - since the children widgets can be
220
265
  # BECWidgets, it is good to explicitely call 'close' on them,
@@ -8,6 +8,14 @@ from qtpy.QtCore import QObject, Qt
8
8
  from qtpy.QtCore import Signal as pyqtSignal
9
9
 
10
10
 
11
+ class NonDownsamplingScatterPlotItem(pg.ScatterPlotItem):
12
+ def setDownsampling(self, ds=None, auto=None, method=None):
13
+ pass
14
+
15
+ def setClipToView(self, state):
16
+ pass
17
+
18
+
11
19
  class Crosshair(QObject):
12
20
  positionChanged = pyqtSignal(tuple)
13
21
  positionClicked = pyqtSignal(tuple)
@@ -47,6 +55,7 @@ class Crosshair(QObject):
47
55
  self.plot_item.ctrl.derivativeCheck.checkStateChanged.connect(self.check_derivatives)
48
56
  self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
49
57
  self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
58
+ self.plot_item.ctrl.downsampleSpin.valueChanged.connect(self.clear_markers)
50
59
 
51
60
  # Initialize markers
52
61
  self.marker_moved_1d = {}
@@ -64,7 +73,7 @@ class Crosshair(QObject):
64
73
  continue
65
74
  pen = item.opts["pen"]
66
75
  color = pen.color() if hasattr(pen, "color") else pg.mkColor(pen)
67
- marker_moved = pg.ScatterPlotItem(
76
+ marker_moved = NonDownsamplingScatterPlotItem(
68
77
  size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
69
78
  )
70
79
  marker_moved.skip_auto_range = True
@@ -73,7 +82,7 @@ class Crosshair(QObject):
73
82
 
74
83
  # Create glowing effect markers for clicked events
75
84
  for size, alpha in [(18, 64), (14, 128), (10, 255)]:
76
- marker_clicked = pg.ScatterPlotItem(
85
+ marker_clicked = NonDownsamplingScatterPlotItem(
77
86
  size=size,
78
87
  pen=pg.mkPen(None),
79
88
  brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha),
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
- from qtpy.QtCore import QSize
3
+ from qtpy.QtCore import QSize, Signal, Slot
4
4
  from qtpy.QtWidgets import QCompleter, QLineEdit, QSizePolicy
5
5
 
6
6
  from bec_widgets.utils.bec_widget import BECWidget
@@ -24,6 +24,8 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
24
24
  arg_name: Argument name, can be used for the other widgets which has to call some other function in bec using correct argument names.
25
25
  """
26
26
 
27
+ device_selected = Signal(str)
28
+
27
29
  ICON_NAME = "edit_note"
28
30
 
29
31
  def __init__(
@@ -54,6 +56,18 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
54
56
  self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
55
57
  self.setMinimumSize(QSize(100, 0))
56
58
 
59
+ self.editingFinished.connect(self.emit_device_selected)
60
+
61
+ @Slot()
62
+ def emit_device_selected(self):
63
+ """
64
+ Editing finished, let's see which device is selected and emit signal
65
+ """
66
+ device_name = self.text().lower()
67
+ device_obj = getattr(self.dev, device_name, None)
68
+ if device_obj is not None:
69
+ self.device_selected.emit(device_name)
70
+
57
71
  def set_device_filter(self, device_filter: str | list[str]):
58
72
  """
59
73
  Set the device filter.
@@ -72,6 +72,7 @@ class BECWaveform(BECPlotBase):
72
72
  "set_y_lim",
73
73
  "set_grid",
74
74
  "set_colormap",
75
+ "enable_scatter",
75
76
  "enable_fps_monitor",
76
77
  "lock_aspect_ratio",
77
78
  "export",
@@ -371,6 +372,20 @@ class BECWaveform(BECPlotBase):
371
372
  else:
372
373
  raise ValueError("Identifier must be either an integer (index) or a string (curve_id).")
373
374
 
375
+ def enable_scatter(self, enable: bool):
376
+ """
377
+ Enable/Disable scatter plot on all curves.
378
+
379
+ Args:
380
+ enable(bool): If True, enable scatter markers; if False, disable them.
381
+ """
382
+ for curve in self.curves:
383
+ if isinstance(curve, BECCurve):
384
+ if enable:
385
+ curve.set_symbol("o") # You can choose any symbol you like
386
+ else:
387
+ curve.set_symbol(None)
388
+
374
389
  def plot(
375
390
  self,
376
391
  arg1: list | np.ndarray | str | None = None,
@@ -42,7 +42,7 @@ class CurveConfig(ConnectionConfig):
42
42
  parent_id: Optional[str] = Field(None, description="The parent plot of the curve.")
43
43
  label: Optional[str] = Field(None, description="The label of the curve.")
44
44
  color: Optional[str | tuple] = Field(None, description="The color of the curve.")
45
- symbol: Optional[str] = Field("o", description="The symbol of the curve.")
45
+ symbol: Optional[str | None] = Field("o", description="The symbol of the curve.")
46
46
  symbol_color: Optional[str | tuple] = Field(
47
47
  None, description="The color of the symbol of the curve."
48
48
  )
@@ -201,7 +201,8 @@ class BECCurve(BECConnector, pg.PlotDataItem):
201
201
  symbol(str): Symbol of the curve.
202
202
  """
203
203
  self.config.symbol = symbol
204
- self.apply_config()
204
+ self.setSymbol(symbol)
205
+ self.updateItems()
205
206
 
206
207
  def set_symbol_color(self, symbol_color: str):
207
208
  """
@@ -14,6 +14,7 @@ from qtpy.QtCore import Property, Signal, Slot
14
14
  from qtpy.QtGui import QDoubleValidator
15
15
  from qtpy.QtWidgets import QDialog, QDoubleSpinBox, QPushButton, QVBoxLayout, QWidget
16
16
 
17
+ from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
17
18
  from bec_widgets.utils import UILoader
18
19
  from bec_widgets.utils.bec_widget import BECWidget
19
20
  from bec_widgets.utils.colors import get_accent_colors, set_theme
@@ -24,7 +25,7 @@ logger = bec_logger.logger
24
25
  MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
25
26
 
26
27
 
27
- class PositionerBox(BECWidget, QWidget):
28
+ class PositionerBox(BECWidget, CompactPopupWidget):
28
29
  """Simple Widget to control a positioner in box form"""
29
30
 
30
31
  ui_file = "positioner_box.ui"
@@ -44,7 +45,7 @@ class PositionerBox(BECWidget, QWidget):
44
45
  device (Positioner): The device to control.
45
46
  """
46
47
  super().__init__(**kwargs)
47
- QWidget.__init__(self, parent=parent)
48
+ CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
48
49
  self.get_bec_shortcuts()
49
50
  self._device = ""
50
51
  self._limits = None
@@ -63,8 +64,7 @@ class PositionerBox(BECWidget, QWidget):
63
64
  current_path = os.path.dirname(__file__)
64
65
  self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
65
66
 
66
- self.layout = QVBoxLayout(self)
67
- self.layout.addWidget(self.ui)
67
+ self.addWidget(self.ui)
68
68
  self.layout.setSpacing(0)
69
69
  self.layout.setContentsMargins(0, 0, 0, 0)
70
70
 
@@ -135,8 +135,12 @@ class PositionerBox(BECWidget, QWidget):
135
135
  """Setter, checks if device is a string"""
136
136
  if not value or not isinstance(value, str):
137
137
  return
138
+ if not self._check_device_is_valid(value):
139
+ return
138
140
  old_device = self._device
139
141
  self._device = value
142
+ if not self.label:
143
+ self.label = value
140
144
  self.device_changed.emit(old_device, value)
141
145
 
142
146
  @Property(bool)
@@ -241,9 +245,11 @@ class PositionerBox(BECWidget, QWidget):
241
245
  if is_moving:
242
246
  self.ui.spinner_widget.start()
243
247
  self.ui.spinner_widget.setToolTip("Device is moving")
248
+ self.set_global_state("warning")
244
249
  else:
245
250
  self.ui.spinner_widget.stop()
246
251
  self.ui.spinner_widget.setToolTip("Device is idle")
252
+ self.set_global_state("success")
247
253
 
248
254
  if readback_val is not None:
249
255
  self.ui.readback.setText(f"{readback_val:.{precision}f}")
File without changes
@@ -0,0 +1,170 @@
1
+ """ Module for a PositionerGroup widget to control a positioner device."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from bec_lib.device import Positioner
6
+ from bec_lib.logger import bec_logger
7
+ from qtpy.QtCore import Property, QSize, Signal, Slot
8
+ from qtpy.QtWidgets import QGridLayout, QGroupBox, QSizePolicy, QVBoxLayout, QWidget
9
+
10
+ from bec_widgets.utils.bec_widget import BECWidget
11
+ from bec_widgets.widgets.positioner_box.positioner_box import PositionerBox
12
+
13
+ logger = bec_logger.logger
14
+
15
+
16
+ class PositionerGroupBox(QGroupBox):
17
+
18
+ position_update = Signal(float)
19
+
20
+ def __init__(self, parent, dev_name):
21
+ super().__init__(parent)
22
+
23
+ self.device_name = dev_name
24
+
25
+ QVBoxLayout(self)
26
+ self.layout().setContentsMargins(0, 0, 0, 0)
27
+ self.layout().setSpacing(0)
28
+ self.widget = PositionerBox(self, dev_name)
29
+ self.widget.compact_view = True
30
+ self.widget.expand_popup = False
31
+ self.layout().addWidget(self.widget)
32
+ self.widget.position_update.connect(self._on_position_update)
33
+ self.widget.expand.connect(self._on_expand)
34
+ self.setTitle(self.device_name)
35
+ self.widget.init_device() # force readback
36
+
37
+ def _on_expand(self, expand):
38
+ if expand:
39
+ self.setTitle("")
40
+ self.setFlat(True)
41
+ else:
42
+ self.setTitle(self.device_name)
43
+ self.setFlat(False)
44
+
45
+ def _on_position_update(self, pos: float):
46
+ self.position_update.emit(pos)
47
+ self.widget.label = f"%.{self.widget.dev[self.widget.device].precision}f" % pos
48
+
49
+ def close(self):
50
+ self.widget.close()
51
+ super().close()
52
+
53
+
54
+ class PositionerGroup(BECWidget, QWidget):
55
+ """Simple Widget to control a positioner in box form"""
56
+
57
+ ICON_NAME = "grid_view"
58
+ USER_ACCESS = ["set_positioners"]
59
+
60
+ # Signal emitted to inform listeners about a position update of the first positioner
61
+ position_update = Signal(float)
62
+ # Signal emitted to inform listeners about (positioner, pos) updates
63
+ device_position_update = Signal(str, float)
64
+
65
+ def __init__(self, parent=None, **kwargs):
66
+ """Initialize the widget.
67
+
68
+ Args:
69
+ parent: The parent widget.
70
+ """
71
+ super().__init__(**kwargs)
72
+ QWidget.__init__(self, parent)
73
+
74
+ self.get_bec_shortcuts()
75
+
76
+ QGridLayout(self)
77
+ self.layout().setContentsMargins(0, 0, 0, 0)
78
+
79
+ self._device_widgets = {}
80
+ self._grid_ncols = 2
81
+
82
+ def minimumSizeHint(self):
83
+ return QSize(300, 30)
84
+
85
+ @Slot(str)
86
+ def set_positioners(self, device_names: str):
87
+ """Redraw grid with positioners from device_names string
88
+
89
+ Device names must be separated by space
90
+ """
91
+ devs = device_names.split()
92
+ for dev_name in devs:
93
+ if not self._check_device_is_valid(dev_name):
94
+ raise ValueError(f"{dev_name} is not a valid Positioner")
95
+ for i, existing_widget in enumerate(self._device_widgets.values()):
96
+ self.layout().removeWidget(existing_widget)
97
+ existing_widget.position_update.disconnect(self._on_position_update)
98
+ if i == 0:
99
+ existing_widget.position_update.disconnect(self.position_update)
100
+ for i, dev_name in enumerate(devs):
101
+ widget = self._device_widgets.get(dev_name)
102
+ if widget is None:
103
+ widget = PositionerGroupBox(self, dev_name)
104
+ self._device_widgets[dev_name] = widget
105
+ widget.position_update.connect(self._on_position_update)
106
+ if i == 0:
107
+ # only emit 'position_update' for the first positioner in grid
108
+ widget.position_update.connect(self.position_update)
109
+ self.layout().addWidget(widget, i // self._grid_ncols, i % self._grid_ncols)
110
+ to_remove = set(self._device_widgets) - set(devs)
111
+ for dev_name in to_remove:
112
+ self._device_widgets[dev_name].close()
113
+ del self._device_widgets[dev_name]
114
+
115
+ def _check_device_is_valid(self, device: str):
116
+ """Check if the device is a positioner
117
+
118
+ Args:
119
+ device (str): The device name
120
+ """
121
+ if device not in self.dev:
122
+ logger.info(f"Device {device} not found in the device list")
123
+ return False
124
+ if not isinstance(self.dev[device], Positioner):
125
+ logger.info(f"Device {device} is not a positioner")
126
+ return False
127
+ return True
128
+
129
+ def _on_position_update(self, pos: float):
130
+ widget = self.sender()
131
+ self.device_position_update.emit(widget.title(), pos)
132
+
133
+ @Property(str)
134
+ def devices_list(self):
135
+ """Device names string separated by space"""
136
+ return " ".join(self._device_widgets)
137
+
138
+ @devices_list.setter
139
+ def devices_list(self, device_names: str):
140
+ """Set devices list from device names string separated by space"""
141
+ devs = device_names.split()
142
+ for dev_name in devs:
143
+ if not self._check_device_is_valid(dev_name):
144
+ return
145
+ self.set_positioners(device_names)
146
+
147
+ @Property(int)
148
+ def grid_max_cols(self):
149
+ """Max number of columns for widgets grid"""
150
+ return self._grid_ncols
151
+
152
+ @grid_max_cols.setter
153
+ def grid_max_cols(self, ncols: int):
154
+ """Set max number of columns for widgets grid"""
155
+ self._grid_ncols = ncols
156
+ self.set_positioners(self.devices_list)
157
+
158
+
159
+ if __name__ == "__main__": # pragma: no cover
160
+ import sys
161
+
162
+ from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
163
+
164
+ app = QApplication(sys.argv)
165
+ widget = PositionerGroup()
166
+ widget.grid_max_cols = 3
167
+ widget.set_positioners("samx samy samz")
168
+
169
+ widget.show()
170
+ sys.exit(app.exec_())
@@ -0,0 +1 @@
1
+ {'files': ['positioner_group.py']}
@@ -0,0 +1,57 @@
1
+ # Copyright (C) 2022 The Qt Company Ltd.
2
+ # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
3
+
4
+ import os
5
+
6
+ from qtpy.QtDesigner import QDesignerCustomWidgetInterface
7
+
8
+ from bec_widgets.utils.bec_designer import designer_material_icon
9
+ from bec_widgets.widgets.positioner_group.positioner_group import PositionerGroup
10
+
11
+ DOM_XML = """
12
+ <ui language='c++'>
13
+ <widget class='PositionerGroup' name='positioner_group'>
14
+ </widget>
15
+ </ui>
16
+ """
17
+ MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
18
+
19
+
20
+ class PositionerGroupPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
21
+ def __init__(self):
22
+ super().__init__()
23
+ self._form_editor = None
24
+
25
+ def createWidget(self, parent):
26
+ t = PositionerGroup(parent)
27
+ return t
28
+
29
+ def domXml(self):
30
+ return DOM_XML
31
+
32
+ def group(self):
33
+ return "Device Control"
34
+
35
+ def icon(self):
36
+ return designer_material_icon(PositionerGroup.ICON_NAME)
37
+
38
+ def includeFile(self):
39
+ return "positioner_group"
40
+
41
+ def initialize(self, form_editor):
42
+ self._form_editor = form_editor
43
+
44
+ def isContainer(self):
45
+ return False
46
+
47
+ def isInitialized(self):
48
+ return self._form_editor is not None
49
+
50
+ def name(self):
51
+ return "PositionerGroup"
52
+
53
+ def toolTip(self):
54
+ return "Container Widget to control positioners in compact form, in a grid"
55
+
56
+ def whatsThis(self):
57
+ 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.positioner_group.positioner_group_plugin import PositionerGroupPlugin
10
+
11
+ QPyDesignerCustomWidgetCollection.addCustomWidget(PositionerGroupPlugin())
12
+
13
+
14
+ if __name__ == "__main__": # pragma: no cover
15
+ main()