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.
- CHANGELOG.md +63 -67
- PKG-INFO +1 -1
- bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +33 -89
- bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +413 -715
- bec_widgets/cli/client.py +22 -0
- bec_widgets/qt_utils/compact_popup.py +68 -23
- bec_widgets/utils/crosshair.py +11 -2
- bec_widgets/widgets/device_line_edit/device_line_edit.py +15 -1
- bec_widgets/widgets/figure/plots/waveform/waveform.py +15 -0
- bec_widgets/widgets/figure/plots/waveform/waveform_curve.py +3 -2
- bec_widgets/widgets/positioner_box/positioner_box.py +10 -4
- bec_widgets/widgets/positioner_group/__init__.py +0 -0
- bec_widgets/widgets/positioner_group/positioner_group.py +170 -0
- bec_widgets/widgets/positioner_group/positioner_group.pyproject +1 -0
- bec_widgets/widgets/positioner_group/positioner_group_plugin.py +57 -0
- bec_widgets/widgets/positioner_group/register_positioner_group.py +15 -0
- bec_widgets/widgets/scan_control/scan_control.py +74 -122
- bec_widgets/widgets/scan_control/scan_group_box.py +66 -11
- bec_widgets/widgets/stop_button/stop_button.py +1 -1
- bec_widgets/widgets/waveform/waveform_widget.py +10 -0
- {bec_widgets-0.118.0.dist-info → bec_widgets-1.0.0.dist-info}/METADATA +1 -1
- {bec_widgets-0.118.0.dist-info → bec_widgets-1.0.0.dist-info}/RECORD +26 -21
- pyproject.toml +1 -1
- {bec_widgets-0.118.0.dist-info → bec_widgets-1.0.0.dist-info}/WHEEL +0 -0
- {bec_widgets-0.118.0.dist-info → bec_widgets-1.0.0.dist-info}/entry_points.txt +0 -0
- {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.
|
117
|
-
self.
|
118
|
-
QHBoxLayout(self.
|
119
|
-
self.
|
120
|
-
self.
|
121
|
-
self.
|
122
|
-
|
123
|
-
|
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="
|
135
|
+
material_icon(icon_name="expand_content", size=(10, 10), convert_to_pixmap=False)
|
127
136
|
)
|
128
|
-
self.
|
129
|
-
self.
|
130
|
-
self.
|
131
|
-
self.
|
132
|
-
self.layout().addWidget(self.
|
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
|
-
|
152
|
-
|
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
|
176
|
-
return self.
|
212
|
+
def compact_view(self):
|
213
|
+
return self.compact_label.isVisible()
|
177
214
|
|
178
|
-
@
|
179
|
-
def
|
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.
|
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.
|
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,
|
bec_widgets/utils/crosshair.py
CHANGED
@@ -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 =
|
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 =
|
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.
|
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,
|
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
|
-
|
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.
|
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()
|