bec-widgets 0.95.1__py3-none-any.whl → 0.96.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.
- .gitlab-ci.yml +2 -2
- CHANGELOG.md +26 -24
- PKG-INFO +1 -1
- bec_widgets/qt_utils/toolbar.py +29 -1
- bec_widgets/utils/crosshair.py +115 -74
- bec_widgets/widgets/figure/plots/plot_base.py +56 -1
- bec_widgets/widgets/figure/plots/waveform/waveform.py +3 -17
- bec_widgets/widgets/scan_control/scan_control.py +77 -16
- bec_widgets/widgets/waveform/waveform_widget.py +32 -3
- {bec_widgets-0.95.1.dist-info → bec_widgets-0.96.1.dist-info}/METADATA +1 -1
- {bec_widgets-0.95.1.dist-info → bec_widgets-0.96.1.dist-info}/RECORD +18 -17
- docs/user/widgets/scan_control/hide_scan_control.png +0 -0
- docs/user/widgets/scan_control/scan_control.md +9 -1
- pyproject.toml +1 -1
- tests/unit_tests/test_crosshair.py +40 -47
- {bec_widgets-0.95.1.dist-info → bec_widgets-0.96.1.dist-info}/WHEEL +0 -0
- {bec_widgets-0.95.1.dist-info → bec_widgets-0.96.1.dist-info}/entry_points.txt +0 -0
- {bec_widgets-0.95.1.dist-info → bec_widgets-0.96.1.dist-info}/licenses/LICENSE +0 -0
.gitlab-ci.yml
CHANGED
@@ -140,7 +140,7 @@ tests:
|
|
140
140
|
- *install-os-packages
|
141
141
|
- *install-repos
|
142
142
|
- pip install -e .[dev,pyqt6]
|
143
|
-
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
|
143
|
+
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --maxfail=2 --random-order --full-trace ./tests/unit_tests
|
144
144
|
- coverage report
|
145
145
|
- coverage xml
|
146
146
|
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
@@ -177,7 +177,7 @@ test-matrix:
|
|
177
177
|
- *install-os-packages
|
178
178
|
- *install-repos
|
179
179
|
- pip install -e .[dev,$QT_PCKG]
|
180
|
-
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
180
|
+
- pytest -v --maxfail=2 --junitxml=report.xml --random-order ./tests/unit_tests
|
181
181
|
allow_failure: true
|
182
182
|
|
183
183
|
end-2-end-conda:
|
CHANGELOG.md
CHANGED
@@ -1,5 +1,31 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## v0.96.1 (2024-08-22)
|
4
|
+
|
5
|
+
### Ci
|
6
|
+
|
7
|
+
* ci: fail pytest after 2 failed tests ([`f0203d9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0203d9bf60c4975ba5ab93a057d9091762454d5))
|
8
|
+
|
9
|
+
### Fix
|
10
|
+
|
11
|
+
* fix(crosshair): update markers if necessary ([`4473805`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/44738057a36f5de2bbb55affdd309f92286d4a0f))
|
12
|
+
|
13
|
+
* fix(waveform_widget): fixed icon appearance ([`f98a9f9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f98a9f9771b93226d47830aa52f45739624f51b4))
|
14
|
+
|
15
|
+
* fix: bubble-up signals ([`2fe72c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2fe72c9ccb71bcb196a1b78197b73acf9aa3f506))
|
16
|
+
|
17
|
+
* fix(crosshair): fixed crosshair for image and waveforms ([`37835cb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37835cbf76ca3ba1081f514ee7793244ac500e7f))
|
18
|
+
|
19
|
+
## v0.96.0 (2024-08-22)
|
20
|
+
|
21
|
+
### Documentation
|
22
|
+
|
23
|
+
* docs(scan_control): added designer options ([`9d7718c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9d7718c3d9badf14150174410b9958a3134a1e23))
|
24
|
+
|
25
|
+
### Feature
|
26
|
+
|
27
|
+
* feat(scan_control): added the ability to configure the scan control widget from designer ([`9d8fb0b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9d8fb0b761efa92972399bcd9aea28e956074380))
|
28
|
+
|
3
29
|
## v0.95.1 (2024-08-22)
|
4
30
|
|
5
31
|
### Documentation
|
@@ -133,27 +159,3 @@ Terminating client connections has to be done at the application level ([`198c1d
|
|
133
159
|
* test(dap): wait for fit ([`6269009`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6269009e5451f830cdee58a514c7858483488a8d))
|
134
160
|
|
135
161
|
* test(auto-update): wait for rendering ([`6d2442d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6d2442d23c683fe92af13df982ce681c07e99cde))
|
136
|
-
|
137
|
-
## v0.93.4 (2024-08-07)
|
138
|
-
|
139
|
-
### Fix
|
140
|
-
|
141
|
-
* fix: rename DeviceBox to PositionerBox, fix test for validation ([`37aa371`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37aa371e7c4c62d70abf37abc125db0c088790fe))
|
142
|
-
|
143
|
-
* fix: add validation for bec_lib.device.Positioner; closes #268 ([`eb54e9f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/eb54e9f788e97af23db8fe0c78f8facb8688bb99))
|
144
|
-
|
145
|
-
## v0.93.3 (2024-08-07)
|
146
|
-
|
147
|
-
### Fix
|
148
|
-
|
149
|
-
* fix(dock): properly shut down docks and temp areas ([`99ee545`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99ee545e41c6078654958b668b5b329f85553d16))
|
150
|
-
|
151
|
-
* fix(settings): shut down settings dialog ([`b50b3a2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b50b3a27e68956e10e8169a0aa698c911d2d9642))
|
152
|
-
|
153
|
-
* fix(website): fixed teardown of website widgets ([`a3d4f5a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3d4f5ac4bc52acfed2791a1724fade6972ed320))
|
154
|
-
|
155
|
-
### Test
|
156
|
-
|
157
|
-
* test: removed quit from teardown ([`cf94599`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cf94599c2544d6831c8afbe7b340082077557ed1))
|
158
|
-
|
159
|
-
* test: removed explicit call to close the widget ([`bf6294e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bf6294ecbfd494565d2dc215e4d7e0c280ac7745))
|
PKG-INFO
CHANGED
bec_widgets/qt_utils/toolbar.py
CHANGED
@@ -3,8 +3,9 @@ import os
|
|
3
3
|
from abc import ABC, abstractmethod
|
4
4
|
from collections import defaultdict
|
5
5
|
|
6
|
+
from bec_qthemes import material_icon
|
6
7
|
from qtpy.QtCore import QSize
|
7
|
-
from qtpy.QtGui import QAction, QIcon
|
8
|
+
from qtpy.QtGui import QAction, QGuiApplication, QIcon
|
8
9
|
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QToolBar, QToolButton, QWidget
|
9
10
|
|
10
11
|
import bec_widgets
|
@@ -70,6 +71,33 @@ class IconAction(ToolBarAction):
|
|
70
71
|
toolbar.addAction(self.action)
|
71
72
|
|
72
73
|
|
74
|
+
class MaterialIconAction:
|
75
|
+
"""
|
76
|
+
Abstract base class for toolbar actions.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
icon_path (str, optional): The name of the icon file from `assets/toolbar_icons`. Defaults to None.
|
80
|
+
tooltip (bool, optional): The tooltip for the action. Defaults to None.
|
81
|
+
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
82
|
+
"""
|
83
|
+
|
84
|
+
def __init__(self, icon_name: str = None, tooltip: str = None, checkable: bool = False):
|
85
|
+
self.icon_name = icon_name
|
86
|
+
self.tooltip = tooltip
|
87
|
+
self.checkable = checkable
|
88
|
+
self.action = None
|
89
|
+
|
90
|
+
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
91
|
+
palette = QGuiApplication.palette()
|
92
|
+
color = "#FFFFFF" # FIXME: This should be a theme color but the toolbar doesn't respect the theme atm
|
93
|
+
# one fixed, change it to palette.toolTipBase().color()
|
94
|
+
|
95
|
+
icon = material_icon(self.icon_name, size=(20, 20), color=color)
|
96
|
+
self.action = QAction(QIcon(icon), self.tooltip, target)
|
97
|
+
self.action.setCheckable(self.checkable)
|
98
|
+
toolbar.addAction(self.action)
|
99
|
+
|
100
|
+
|
73
101
|
class DeviceSelectionAction(ToolBarAction):
|
74
102
|
"""
|
75
103
|
Action for selecting a device in a combobox.
|
bec_widgets/utils/crosshair.py
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
|
1
3
|
import numpy as np
|
2
4
|
import pyqtgraph as pg
|
3
5
|
|
4
6
|
# from qtpy.QtCore import QObject, pyqtSignal
|
5
|
-
from qtpy.QtCore import QObject
|
7
|
+
from qtpy.QtCore import QObject, Qt
|
6
8
|
from qtpy.QtCore import Signal as pyqtSignal
|
7
9
|
|
8
10
|
|
@@ -26,10 +28,13 @@ class Crosshair(QObject):
|
|
26
28
|
super().__init__(parent)
|
27
29
|
self.is_log_y = None
|
28
30
|
self.is_log_x = None
|
31
|
+
self.is_derivative = None
|
29
32
|
self.plot_item = plot_item
|
30
33
|
self.precision = precision
|
31
34
|
self.v_line = pg.InfiniteLine(angle=90, movable=False)
|
35
|
+
self.v_line.skip_auto_range = True
|
32
36
|
self.h_line = pg.InfiniteLine(angle=0, movable=False)
|
37
|
+
self.h_line.skip_auto_range = True
|
33
38
|
self.plot_item.addItem(self.v_line, ignoreBounds=True)
|
34
39
|
self.plot_item.addItem(self.h_line, ignoreBounds=True)
|
35
40
|
self.proxy = pg.SignalProxy(
|
@@ -37,74 +42,75 @@ class Crosshair(QObject):
|
|
37
42
|
)
|
38
43
|
self.plot_item.scene().sigMouseClicked.connect(self.mouse_clicked)
|
39
44
|
|
45
|
+
self.plot_item.ctrl.derivativeCheck.checkStateChanged.connect(self.check_derivatives)
|
46
|
+
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
|
47
|
+
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
|
48
|
+
|
40
49
|
# Initialize markers
|
41
|
-
self.marker_moved_1d =
|
42
|
-
self.marker_clicked_1d =
|
50
|
+
self.marker_moved_1d = {}
|
51
|
+
self.marker_clicked_1d = {}
|
43
52
|
self.marker_2d = None
|
44
53
|
self.update_markers()
|
45
54
|
|
46
55
|
def update_markers(self):
|
47
56
|
"""Update the markers for the crosshair, creating new ones if necessary."""
|
48
57
|
|
49
|
-
# Clear existing markers
|
50
|
-
for marker in self.marker_moved_1d + self.marker_clicked_1d:
|
51
|
-
self.plot_item.removeItem(marker)
|
52
|
-
if self.marker_2d:
|
53
|
-
self.plot_item.removeItem(self.marker_2d)
|
54
|
-
|
55
58
|
# Create new markers
|
56
|
-
self.marker_moved_1d = []
|
57
|
-
self.marker_clicked_1d = []
|
58
|
-
self.marker_2d = None
|
59
59
|
for item in self.plot_item.items:
|
60
60
|
if isinstance(item, pg.PlotDataItem): # 1D plot
|
61
|
+
if item.name() in self.marker_moved_1d:
|
62
|
+
continue
|
61
63
|
pen = item.opts["pen"]
|
62
64
|
color = pen.color() if hasattr(pen, "color") else pg.mkColor(pen)
|
63
65
|
marker_moved = pg.ScatterPlotItem(
|
64
66
|
size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
|
65
67
|
)
|
66
|
-
|
67
|
-
|
68
|
-
)
|
69
|
-
self.marker_moved_1d.append(marker_moved)
|
68
|
+
marker_moved.skip_auto_range = True
|
69
|
+
self.marker_moved_1d[item.name()] = marker_moved
|
70
70
|
self.plot_item.addItem(marker_moved)
|
71
|
+
|
71
72
|
# Create glowing effect markers for clicked events
|
72
|
-
marker_clicked_list = []
|
73
73
|
for size, alpha in [(18, 64), (14, 128), (10, 255)]:
|
74
74
|
marker_clicked = pg.ScatterPlotItem(
|
75
75
|
size=size,
|
76
76
|
pen=pg.mkPen(None),
|
77
77
|
brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha),
|
78
78
|
)
|
79
|
-
|
79
|
+
marker_clicked.skip_auto_range = True
|
80
|
+
self.marker_clicked_1d[item.name()] = marker_clicked
|
80
81
|
self.plot_item.addItem(marker_clicked)
|
81
82
|
|
82
|
-
self.marker_clicked_1d.append(marker_clicked_list)
|
83
83
|
elif isinstance(item, pg.ImageItem): # 2D plot
|
84
|
+
if self.marker_2d is not None:
|
85
|
+
continue
|
84
86
|
self.marker_2d = pg.ROI(
|
85
87
|
[0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False
|
86
88
|
)
|
87
89
|
self.plot_item.addItem(self.marker_2d)
|
88
90
|
|
89
|
-
def snap_to_data(self, x, y) -> tuple:
|
91
|
+
def snap_to_data(self, x, y) -> tuple[defaultdict[list], defaultdict[list]]:
|
90
92
|
"""
|
91
93
|
Finds the nearest data points to the given x and y coordinates.
|
92
94
|
|
93
95
|
Args:
|
94
|
-
x: The x-coordinate
|
95
|
-
y: The y-coordinate
|
96
|
+
x: The x-coordinate of the mouse cursor
|
97
|
+
y: The y-coordinate of the mouse cursor
|
96
98
|
|
97
99
|
Returns:
|
98
|
-
tuple:
|
100
|
+
tuple: x and y values snapped to the nearest data
|
99
101
|
"""
|
100
|
-
|
101
|
-
|
102
|
+
y_values = defaultdict(list)
|
103
|
+
x_values = defaultdict(list)
|
102
104
|
image_2d = None
|
103
105
|
|
104
106
|
# Iterate through items in the plot
|
105
107
|
for item in self.plot_item.items:
|
106
108
|
if isinstance(item, pg.PlotDataItem): # 1D plot
|
107
|
-
|
109
|
+
name = item.name()
|
110
|
+
plot_data = item._getDisplayDataset()
|
111
|
+
if plot_data is None:
|
112
|
+
continue
|
113
|
+
x_data, y_data = plot_data.x, plot_data.y
|
108
114
|
if x_data is not None and y_data is not None:
|
109
115
|
if self.is_log_x:
|
110
116
|
min_x_data = np.min(x_data[x_data > 0])
|
@@ -112,25 +118,25 @@ class Crosshair(QObject):
|
|
112
118
|
min_x_data = np.min(x_data)
|
113
119
|
max_x_data = np.max(x_data)
|
114
120
|
if x < min_x_data or x > max_x_data:
|
115
|
-
|
121
|
+
y_values[name] = None
|
122
|
+
x_values[name] = None
|
123
|
+
continue
|
116
124
|
closest_x, closest_y = self.closest_x_y_value(x, x_data, y_data)
|
117
|
-
|
118
|
-
|
125
|
+
y_values[name] = closest_y
|
126
|
+
x_values[name] = closest_x
|
119
127
|
elif isinstance(item, pg.ImageItem): # 2D plot
|
128
|
+
name = item.config.monitor
|
120
129
|
image_2d = item.image
|
130
|
+
# clip the x and y values to the image dimensions to avoid out of bounds errors
|
131
|
+
y_values[name] = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
132
|
+
x_values[name] = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
121
133
|
|
122
|
-
|
123
|
-
|
124
|
-
|
134
|
+
if x_values and y_values:
|
135
|
+
if all(v is None for v in x_values.values()) or all(
|
136
|
+
v is None for v in y_values.values()
|
137
|
+
):
|
125
138
|
return None, None
|
126
|
-
|
127
|
-
return closest_x, y_values_1d
|
128
|
-
|
129
|
-
# Handle 2D plot
|
130
|
-
if image_2d is not None:
|
131
|
-
x_idx = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
132
|
-
y_idx = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
133
|
-
return x_idx, y_idx
|
139
|
+
return x_values, y_values
|
134
140
|
|
135
141
|
return None, None
|
136
142
|
|
@@ -156,8 +162,8 @@ class Crosshair(QObject):
|
|
156
162
|
Args:
|
157
163
|
event: The mouse moved event
|
158
164
|
"""
|
159
|
-
self.check_log()
|
160
165
|
pos = event[0]
|
166
|
+
self.update_markers()
|
161
167
|
if self.plot_item.vb.sceneBoundingRect().contains(pos):
|
162
168
|
mouse_point = self.plot_item.vb.mapSceneToView(pos)
|
163
169
|
self.v_line.setPos(mouse_point.x())
|
@@ -168,27 +174,34 @@ class Crosshair(QObject):
|
|
168
174
|
x = 10**x
|
169
175
|
if self.is_log_y:
|
170
176
|
y = 10**y
|
171
|
-
|
177
|
+
x_snap_values, y_snap_values = self.snap_to_data(x, y)
|
178
|
+
if x_snap_values is None or y_snap_values is None:
|
179
|
+
return
|
180
|
+
if all(v is None for v in x_snap_values.values()) or all(
|
181
|
+
v is None for v in y_snap_values.values()
|
182
|
+
):
|
183
|
+
# not sure how we got here, but just to be safe...
|
184
|
+
return
|
172
185
|
|
173
186
|
for item in self.plot_item.items:
|
174
187
|
if isinstance(item, pg.PlotDataItem):
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
)
|
188
|
+
name = item.name()
|
189
|
+
x, y = x_snap_values[name], y_snap_values[name]
|
190
|
+
if x is None or y is None:
|
191
|
+
continue
|
192
|
+
self.marker_moved_1d[name].setData([x], [y])
|
193
|
+
coordinate_to_emit = (name, round(x, self.precision), round(y, self.precision))
|
181
194
|
self.coordinatesChanged1D.emit(coordinate_to_emit)
|
182
|
-
for i, y_val in enumerate(y_values):
|
183
|
-
self.marker_moved_1d[i].setData(
|
184
|
-
[x if not self.is_log_x else np.log10(x)],
|
185
|
-
[y_val if not self.is_log_y else np.log10(y_val)],
|
186
|
-
)
|
187
195
|
elif isinstance(item, pg.ImageItem):
|
188
|
-
|
189
|
-
|
190
|
-
|
196
|
+
name = item.config.monitor
|
197
|
+
x, y = x_snap_values[name], y_snap_values[name]
|
198
|
+
if x is None or y is None:
|
199
|
+
continue
|
200
|
+
self.marker_2d.setPos([x, y])
|
201
|
+
coordinate_to_emit = (name, x, y)
|
191
202
|
self.coordinatesChanged2D.emit(coordinate_to_emit)
|
203
|
+
else:
|
204
|
+
continue
|
192
205
|
|
193
206
|
def mouse_clicked(self, event):
|
194
207
|
"""Handles the mouse clicked event, updating the crosshair position and emitting signals.
|
@@ -196,7 +209,11 @@ class Crosshair(QObject):
|
|
196
209
|
Args:
|
197
210
|
event: The mouse clicked event
|
198
211
|
"""
|
199
|
-
|
212
|
+
|
213
|
+
# we only accept left mouse clicks
|
214
|
+
if event.button() != Qt.MouseButton.LeftButton:
|
215
|
+
return
|
216
|
+
self.update_markers()
|
200
217
|
if self.plot_item.vb.sceneBoundingRect().contains(event._scenePos):
|
201
218
|
mouse_point = self.plot_item.vb.mapSceneToView(event._scenePos)
|
202
219
|
x, y = mouse_point.x(), mouse_point.y()
|
@@ -205,31 +222,55 @@ class Crosshair(QObject):
|
|
205
222
|
x = 10**x
|
206
223
|
if self.is_log_y:
|
207
224
|
y = 10**y
|
208
|
-
|
225
|
+
x_snap_values, y_snap_values = self.snap_to_data(x, y)
|
226
|
+
|
227
|
+
if x_snap_values is None or y_snap_values is None:
|
228
|
+
return
|
229
|
+
if all(v is None for v in x_snap_values.values()) or all(
|
230
|
+
v is None for v in y_snap_values.values()
|
231
|
+
):
|
232
|
+
# not sure how we got here, but just to be safe...
|
233
|
+
return
|
209
234
|
|
210
235
|
for item in self.plot_item.items:
|
211
236
|
if isinstance(item, pg.PlotDataItem):
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
)
|
237
|
+
name = item.name()
|
238
|
+
x, y = x_snap_values[name], y_snap_values[name]
|
239
|
+
if x is None or y is None:
|
240
|
+
continue
|
241
|
+
self.marker_clicked_1d[name].setData([x], [y])
|
242
|
+
coordinate_to_emit = (name, round(x, self.precision), round(y, self.precision))
|
218
243
|
self.coordinatesClicked1D.emit(coordinate_to_emit)
|
219
|
-
for i, y_val in enumerate(y_values):
|
220
|
-
for marker in self.marker_clicked_1d[i]:
|
221
|
-
marker.setData(
|
222
|
-
[x if not self.is_log_x else np.log10(x)],
|
223
|
-
[y_val if not self.is_log_y else np.log10(y_val)],
|
224
|
-
)
|
225
244
|
elif isinstance(item, pg.ImageItem):
|
226
|
-
|
227
|
-
|
228
|
-
|
245
|
+
name = item.config.monitor
|
246
|
+
x, y = x_snap_values[name], y_snap_values[name]
|
247
|
+
if x is None or y is None:
|
248
|
+
continue
|
249
|
+
self.marker_2d.setPos([x, y])
|
250
|
+
coordinate_to_emit = (name, x, y)
|
229
251
|
self.coordinatesClicked2D.emit(coordinate_to_emit)
|
230
|
-
|
252
|
+
else:
|
253
|
+
continue
|
254
|
+
|
255
|
+
def clear_markers(self):
|
256
|
+
"""Clears the markers from the plot."""
|
257
|
+
for marker in self.marker_moved_1d.values():
|
258
|
+
marker.clear()
|
259
|
+
for marker in self.marker_clicked_1d.values():
|
260
|
+
marker.clear()
|
231
261
|
|
232
262
|
def check_log(self):
|
233
263
|
"""Checks if the x or y axis is in log scale and updates the internal state accordingly."""
|
234
264
|
self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
|
235
265
|
self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
|
266
|
+
self.clear_markers()
|
267
|
+
|
268
|
+
def check_derivatives(self):
|
269
|
+
"""Checks if the derivatives are enabled and updates the internal state accordingly."""
|
270
|
+
self.is_derivative = self.plot_item.ctrl.derivativeCheck.isChecked()
|
271
|
+
self.clear_markers()
|
272
|
+
|
273
|
+
def cleanup(self):
|
274
|
+
self.v_line.deleteLater()
|
275
|
+
self.h_line.deleteLater()
|
276
|
+
self.clear_markers()
|
@@ -4,9 +4,11 @@ from typing import Literal, Optional
|
|
4
4
|
|
5
5
|
import pyqtgraph as pg
|
6
6
|
from pydantic import BaseModel, Field
|
7
|
+
from qtpy.QtCore import Signal, Slot
|
7
8
|
from qtpy.QtWidgets import QWidget
|
8
9
|
|
9
10
|
from bec_widgets.utils import BECConnector, ConnectionConfig
|
11
|
+
from bec_widgets.utils.crosshair import Crosshair
|
10
12
|
|
11
13
|
|
12
14
|
class AxisConfig(BaseModel):
|
@@ -41,7 +43,21 @@ class SubplotConfig(ConnectionConfig):
|
|
41
43
|
)
|
42
44
|
|
43
45
|
|
46
|
+
class BECViewBox(pg.ViewBox):
|
47
|
+
|
48
|
+
def itemBoundsChanged(self, item):
|
49
|
+
self._itemBoundsCache.pop(item, None)
|
50
|
+
if (self.state["autoRange"][0] is not False) or (self.state["autoRange"][1] is not False):
|
51
|
+
# check if the call is coming from a mouse-move event
|
52
|
+
if hasattr(item, "skip_auto_range") and item.skip_auto_range:
|
53
|
+
return
|
54
|
+
self._autoRangeNeedsUpdate = True
|
55
|
+
self.update()
|
56
|
+
|
57
|
+
|
44
58
|
class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
59
|
+
crosshair_coordinates_changed = Signal(tuple)
|
60
|
+
crosshair_coordinates_clicked = Signal(tuple)
|
45
61
|
USER_ACCESS = [
|
46
62
|
"_config_dict",
|
47
63
|
"set",
|
@@ -73,9 +89,13 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
|
73
89
|
pg.GraphicsLayout.__init__(self, parent)
|
74
90
|
|
75
91
|
self.figure = parent_figure
|
76
|
-
|
92
|
+
|
93
|
+
# self.plot_item = self.addPlot(row=0, col=0)
|
94
|
+
self.plot_item = pg.PlotItem(viewBox=BECViewBox(parent=self, enableMenu=True), parent=self)
|
95
|
+
self.addItem(self.plot_item, row=0, col=0)
|
77
96
|
|
78
97
|
self.add_legend()
|
98
|
+
self.crosshair = None
|
79
99
|
|
80
100
|
def set(self, **kwargs) -> None:
|
81
101
|
"""
|
@@ -304,6 +324,40 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
|
304
324
|
"""
|
305
325
|
self.plot_item.enableAutoRange(axis, enabled)
|
306
326
|
|
327
|
+
def hook_crosshair(self) -> None:
|
328
|
+
"""Hook the crosshair to all plots."""
|
329
|
+
if self.crosshair is None:
|
330
|
+
self.crosshair = Crosshair(self.plot_item, precision=3)
|
331
|
+
self.crosshair.coordinatesChanged1D.connect(self.crosshair_coordinates_changed)
|
332
|
+
self.crosshair.coordinatesClicked1D.connect(self.crosshair_coordinates_clicked)
|
333
|
+
self.crosshair.coordinatesChanged2D.connect(self.crosshair_coordinates_changed)
|
334
|
+
self.crosshair.coordinatesClicked2D.connect(self.crosshair_coordinates_clicked)
|
335
|
+
|
336
|
+
def unhook_crosshair(self) -> None:
|
337
|
+
"""Unhook the crosshair from all plots."""
|
338
|
+
if self.crosshair is not None:
|
339
|
+
self.crosshair.coordinatesChanged1D.disconnect(self.crosshair_coordinates_changed)
|
340
|
+
self.crosshair.coordinatesClicked1D.disconnect(self.crosshair_coordinates_clicked)
|
341
|
+
self.crosshair.coordinatesChanged2D.disconnect(self.crosshair_coordinates_changed)
|
342
|
+
self.crosshair.coordinatesClicked2D.disconnect(self.crosshair_coordinates_clicked)
|
343
|
+
self.crosshair.cleanup()
|
344
|
+
self.crosshair.deleteLater()
|
345
|
+
self.crosshair = None
|
346
|
+
|
347
|
+
def toggle_crosshair(self) -> None:
|
348
|
+
"""Toggle the crosshair on all plots."""
|
349
|
+
if self.crosshair is None:
|
350
|
+
return self.hook_crosshair()
|
351
|
+
|
352
|
+
self.unhook_crosshair()
|
353
|
+
|
354
|
+
@Slot()
|
355
|
+
def reset(self) -> None:
|
356
|
+
"""Reset the plot widget."""
|
357
|
+
if self.crosshair is not None:
|
358
|
+
self.crosshair.clear_markers()
|
359
|
+
self.crosshair.update_markers()
|
360
|
+
|
307
361
|
def export(self):
|
308
362
|
"""Show the Export Dialog of the plot widget."""
|
309
363
|
scene = self.plot_item.scene()
|
@@ -317,6 +371,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
|
317
371
|
|
318
372
|
def cleanup_pyqtgraph(self):
|
319
373
|
"""Cleanup pyqtgraph items."""
|
374
|
+
self.unhook_crosshair()
|
320
375
|
item = self.plot_item
|
321
376
|
item.vb.menu.close()
|
322
377
|
item.vb.menu.deleteLater()
|
@@ -77,6 +77,7 @@ class BECWaveform(BECPlotBase):
|
|
77
77
|
dap_params_update = pyqtSignal(dict)
|
78
78
|
dap_summary_update = pyqtSignal(dict)
|
79
79
|
autorange_signal = pyqtSignal()
|
80
|
+
new_scan = pyqtSignal()
|
80
81
|
|
81
82
|
def __init__(
|
82
83
|
self,
|
@@ -408,23 +409,6 @@ class BECWaveform(BECPlotBase):
|
|
408
409
|
"""
|
409
410
|
self.plot_item.enableAutoRange(axis, enabled)
|
410
411
|
|
411
|
-
@Slot()
|
412
|
-
def auto_range(self):
|
413
|
-
self.plot_item.autoRange()
|
414
|
-
|
415
|
-
def set_auto_range(self, enabled: bool, axis: str = "xy"):
|
416
|
-
"""
|
417
|
-
Set the auto range of the plot widget.
|
418
|
-
|
419
|
-
Args:
|
420
|
-
enabled(bool): If True, enable the auto range.
|
421
|
-
axis(str, optional): The axis to enable the auto range.
|
422
|
-
- "xy": Enable auto range for both x and y axis.
|
423
|
-
- "x": Enable auto range for x axis.
|
424
|
-
- "y": Enable auto range for y axis.
|
425
|
-
"""
|
426
|
-
self.plot_item.enableAutoRange(axis, enabled)
|
427
|
-
|
428
412
|
def add_curve_custom(
|
429
413
|
self,
|
430
414
|
x: list | np.ndarray,
|
@@ -935,6 +919,8 @@ class BECWaveform(BECPlotBase):
|
|
935
919
|
return
|
936
920
|
|
937
921
|
if current_scan_id != self.scan_id:
|
922
|
+
self.reset()
|
923
|
+
self.new_scan.emit()
|
938
924
|
self.set_auto_range(True, "xy")
|
939
925
|
self.old_scan_id = self.scan_id
|
940
926
|
self.scan_id = current_scan_id
|
@@ -1,9 +1,11 @@
|
|
1
1
|
from bec_lib.endpoints import MessageEndpoints
|
2
|
+
from qtpy.QtCore import Property, Signal, Slot
|
2
3
|
from qtpy.QtWidgets import (
|
3
4
|
QApplication,
|
4
5
|
QComboBox,
|
5
6
|
QGridLayout,
|
6
7
|
QGroupBox,
|
8
|
+
QHBoxLayout,
|
7
9
|
QPushButton,
|
8
10
|
QSizePolicy,
|
9
11
|
QVBoxLayout,
|
@@ -18,6 +20,9 @@ from bec_widgets.widgets.stop_button.stop_button import StopButton
|
|
18
20
|
|
19
21
|
class ScanControl(BECWidget, QWidget):
|
20
22
|
|
23
|
+
scan_started = Signal()
|
24
|
+
scan_selected = Signal(str)
|
25
|
+
|
21
26
|
def __init__(
|
22
27
|
self, parent=None, client=None, gui_id: str | None = None, allowed_scans: list | None = None
|
23
28
|
):
|
@@ -50,11 +55,26 @@ class ScanControl(BECWidget, QWidget):
|
|
50
55
|
self.layout.addWidget(self.scan_selection_group)
|
51
56
|
|
52
57
|
# Connect signals
|
53
|
-
self.comboBox_scan_selection.currentIndexChanged.connect(self.
|
58
|
+
self.comboBox_scan_selection.currentIndexChanged.connect(self.on_scan_selection_changed)
|
54
59
|
self.button_run_scan.clicked.connect(self.run_scan)
|
60
|
+
|
61
|
+
# Add bundle button
|
62
|
+
self.button_add_bundle = QPushButton("Add Bundle")
|
63
|
+
self.button_add_bundle.setVisible(False)
|
64
|
+
# Remove bundle button
|
65
|
+
self.button_remove_bundle = QPushButton("Remove Bundle")
|
66
|
+
self.button_remove_bundle.setVisible(False)
|
67
|
+
|
68
|
+
bundle_layout = QHBoxLayout()
|
69
|
+
bundle_layout.addWidget(self.button_add_bundle)
|
70
|
+
bundle_layout.addWidget(self.button_remove_bundle)
|
71
|
+
self.layout.addLayout(bundle_layout)
|
72
|
+
|
55
73
|
self.button_add_bundle.clicked.connect(self.add_arg_bundle)
|
56
74
|
self.button_remove_bundle.clicked.connect(self.remove_arg_bundle)
|
57
75
|
|
76
|
+
self.scan_selected.connect(self.scan_select)
|
77
|
+
|
58
78
|
# Initialize scan selection
|
59
79
|
self.populate_scans()
|
60
80
|
|
@@ -69,21 +89,16 @@ class ScanControl(BECWidget, QWidget):
|
|
69
89
|
scan_selection_group = QGroupBox("Scan Selection", self)
|
70
90
|
self.scan_selection_layout = QGridLayout(scan_selection_group)
|
71
91
|
self.comboBox_scan_selection = QComboBox(scan_selection_group)
|
92
|
+
|
72
93
|
# Run button
|
73
94
|
self.button_run_scan = QPushButton("Start", scan_selection_group)
|
74
95
|
self.button_run_scan.setStyleSheet("background-color: #559900; color: white")
|
75
96
|
# Stop button
|
76
97
|
self.button_stop_scan = StopButton(parent=scan_selection_group)
|
77
|
-
# Add bundle button
|
78
|
-
self.button_add_bundle = QPushButton("Add Bundle", scan_selection_group)
|
79
|
-
# Remove bundle button
|
80
|
-
self.button_remove_bundle = QPushButton("Remove Bundle", scan_selection_group)
|
81
98
|
|
82
99
|
self.scan_selection_layout.addWidget(self.comboBox_scan_selection, 0, 0, 1, 2)
|
83
100
|
self.scan_selection_layout.addWidget(self.button_run_scan, 1, 0)
|
84
101
|
self.scan_selection_layout.addWidget(self.button_stop_scan, 1, 1)
|
85
|
-
self.scan_selection_layout.addWidget(self.button_add_bundle, 2, 0)
|
86
|
-
self.scan_selection_layout.addWidget(self.button_remove_bundle, 2, 1)
|
87
102
|
|
88
103
|
return scan_selection_group
|
89
104
|
|
@@ -104,23 +119,65 @@ class ScanControl(BECWidget, QWidget):
|
|
104
119
|
allowed_scans = self.allowed_scans
|
105
120
|
self.comboBox_scan_selection.addItems(allowed_scans)
|
106
121
|
|
107
|
-
def
|
122
|
+
def on_scan_selection_changed(self, index: int):
|
108
123
|
"""Callback for scan selection combo box"""
|
109
|
-
self.reset_layout()
|
110
124
|
selected_scan_name = self.comboBox_scan_selection.currentText()
|
111
|
-
|
125
|
+
self.scan_selected.emit(selected_scan_name)
|
126
|
+
|
127
|
+
@Property(bool)
|
128
|
+
def hide_scan_control_buttons(self):
|
129
|
+
return not self.button_run_scan.isVisible()
|
130
|
+
|
131
|
+
@hide_scan_control_buttons.setter
|
132
|
+
def hide_scan_control_buttons(self, hide: bool):
|
133
|
+
self.show_scan_control_buttons(not hide)
|
134
|
+
|
135
|
+
@Slot(bool)
|
136
|
+
def show_scan_control_buttons(self, show: bool):
|
137
|
+
"""Shows or hides the scan control buttons."""
|
138
|
+
self.button_run_scan.setVisible(show)
|
139
|
+
self.button_stop_scan.setVisible(show)
|
140
|
+
|
141
|
+
show_group = show or self.button_run_scan.isVisible()
|
142
|
+
self.scan_selection_group.setVisible(show_group)
|
143
|
+
|
144
|
+
@Property(bool)
|
145
|
+
def hide_scan_selection_combobox(self):
|
146
|
+
return not self.comboBox_scan_selection.isVisible()
|
147
|
+
|
148
|
+
@hide_scan_selection_combobox.setter
|
149
|
+
def hide_scan_selection_combobox(self, hide: bool):
|
150
|
+
self.show_scan_selection_combobox(not hide)
|
151
|
+
|
152
|
+
@Slot(bool)
|
153
|
+
def show_scan_selection_combobox(self, show: bool):
|
154
|
+
"""Shows or hides the scan selection combobox."""
|
155
|
+
self.comboBox_scan_selection.setVisible(show)
|
156
|
+
|
157
|
+
show_group = show or self.button_run_scan.isVisible()
|
158
|
+
self.scan_selection_group.setVisible(show_group)
|
159
|
+
|
160
|
+
@Slot(str)
|
161
|
+
def scan_select(self, scan_name: str):
|
162
|
+
"""
|
163
|
+
Slot for scan selection. Updates the scan control layout based on the selected scan.
|
164
|
+
|
165
|
+
Args:
|
166
|
+
scan_name(str): Name of the selected scan.
|
167
|
+
"""
|
168
|
+
self.reset_layout()
|
169
|
+
selected_scan_info = self.available_scans.get(scan_name, {})
|
112
170
|
|
113
171
|
gui_config = selected_scan_info.get("gui_config", {})
|
114
172
|
self.arg_group = gui_config.get("arg_group", None)
|
115
173
|
self.kwarg_groups = gui_config.get("kwarg_groups", None)
|
116
174
|
|
117
|
-
|
118
|
-
|
119
|
-
|
175
|
+
show_bundle_buttons = bool(self.arg_group["arg_inputs"])
|
176
|
+
|
177
|
+
self.button_add_bundle.setVisible(show_bundle_buttons)
|
178
|
+
self.button_remove_bundle.setVisible(show_bundle_buttons)
|
120
179
|
|
121
|
-
if
|
122
|
-
self.button_add_bundle.setEnabled(True)
|
123
|
-
self.button_remove_bundle.setEnabled(True)
|
180
|
+
if show_bundle_buttons:
|
124
181
|
self.add_arg_group(self.arg_group)
|
125
182
|
if len(self.kwarg_groups) > 0:
|
126
183
|
self.add_kwargs_boxes(self.kwarg_groups)
|
@@ -151,9 +208,11 @@ class ScanControl(BECWidget, QWidget):
|
|
151
208
|
self.arg_box.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
152
209
|
self.layout.addWidget(self.arg_box)
|
153
210
|
|
211
|
+
@Slot()
|
154
212
|
def add_arg_bundle(self):
|
155
213
|
self.arg_box.add_widget_bundle()
|
156
214
|
|
215
|
+
@Slot()
|
157
216
|
def remove_arg_bundle(self):
|
158
217
|
self.arg_box.remove_widget_bundle()
|
159
218
|
|
@@ -172,7 +231,9 @@ class ScanControl(BECWidget, QWidget):
|
|
172
231
|
box.deleteLater()
|
173
232
|
self.kwarg_boxes = []
|
174
233
|
|
234
|
+
@Slot()
|
175
235
|
def run_scan(self):
|
236
|
+
self.scan_started.emit()
|
176
237
|
args = []
|
177
238
|
kwargs = {}
|
178
239
|
if self.arg_box is not None:
|
@@ -5,11 +5,17 @@ from typing import Literal
|
|
5
5
|
|
6
6
|
import numpy as np
|
7
7
|
import pyqtgraph as pg
|
8
|
+
from qtpy.QtCore import Signal
|
8
9
|
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
9
10
|
|
10
11
|
from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility
|
11
12
|
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
12
|
-
from bec_widgets.qt_utils.toolbar import
|
13
|
+
from bec_widgets.qt_utils.toolbar import (
|
14
|
+
IconAction,
|
15
|
+
MaterialIconAction,
|
16
|
+
ModularToolBar,
|
17
|
+
SeparatorAction,
|
18
|
+
)
|
13
19
|
from bec_widgets.utils.bec_widget import BECWidget
|
14
20
|
from bec_widgets.widgets.figure import BECFigure
|
15
21
|
from bec_widgets.widgets.figure.plots.axis_settings import AxisSettings
|
@@ -51,6 +57,14 @@ class BECWaveformWidget(BECWidget, QWidget):
|
|
51
57
|
"export",
|
52
58
|
"export_to_matplotlib",
|
53
59
|
]
|
60
|
+
scan_signal_update = Signal()
|
61
|
+
async_signal_update = Signal()
|
62
|
+
dap_params_update = Signal(dict)
|
63
|
+
dap_summary_update = Signal(dict)
|
64
|
+
autorange_signal = Signal()
|
65
|
+
new_scan = Signal()
|
66
|
+
crosshair_coordinates_changed = Signal(tuple)
|
67
|
+
crosshair_coordinates_clicked = Signal(tuple)
|
54
68
|
|
55
69
|
def __init__(
|
56
70
|
self,
|
@@ -93,8 +107,11 @@ class BECWaveformWidget(BECWidget, QWidget):
|
|
93
107
|
"fit_params": IconAction(
|
94
108
|
icon_path="fitting_parameters.svg", tooltip="Open Fitting Parameters"
|
95
109
|
),
|
96
|
-
"axis_settings":
|
97
|
-
|
110
|
+
"axis_settings": MaterialIconAction(
|
111
|
+
icon_name="settings", tooltip="Open Configuration Dialog"
|
112
|
+
),
|
113
|
+
"crosshair": MaterialIconAction(
|
114
|
+
icon_name="point_scan", tooltip="Show Crosshair", checkable=True
|
98
115
|
),
|
99
116
|
},
|
100
117
|
target_widget=self,
|
@@ -110,8 +127,19 @@ class BECWaveformWidget(BECWidget, QWidget):
|
|
110
127
|
|
111
128
|
self.config = config
|
112
129
|
|
130
|
+
self.hook_waveform_signals()
|
113
131
|
self._hook_actions()
|
114
132
|
|
133
|
+
def hook_waveform_signals(self):
|
134
|
+
self.waveform.scan_signal_update.connect(self.scan_signal_update)
|
135
|
+
self.waveform.async_signal_update.connect(self.async_signal_update)
|
136
|
+
self.waveform.dap_params_update.connect(self.dap_params_update)
|
137
|
+
self.waveform.dap_summary_update.connect(self.dap_summary_update)
|
138
|
+
self.waveform.autorange_signal.connect(self.autorange_signal)
|
139
|
+
self.waveform.new_scan.connect(self.new_scan)
|
140
|
+
self.waveform.crosshair_coordinates_changed.connect(self.crosshair_coordinates_changed)
|
141
|
+
self.waveform.crosshair_coordinates_clicked.connect(self.crosshair_coordinates_clicked)
|
142
|
+
|
115
143
|
def _hook_actions(self):
|
116
144
|
self.toolbar.widgets["save"].action.triggered.connect(self.export)
|
117
145
|
self.toolbar.widgets["matplotlib"].action.triggered.connect(self.export_to_matplotlib)
|
@@ -123,6 +151,7 @@ class BECWaveformWidget(BECWidget, QWidget):
|
|
123
151
|
self.toolbar.widgets["curves"].action.triggered.connect(self.show_curve_settings)
|
124
152
|
self.toolbar.widgets["fit_params"].action.triggered.connect(self.show_fit_summary_dialog)
|
125
153
|
self.toolbar.widgets["axis_settings"].action.triggered.connect(self.show_axis_settings)
|
154
|
+
self.toolbar.widgets["crosshair"].action.triggered.connect(self.waveform.toggle_crosshair)
|
126
155
|
# self.toolbar.widgets["import"].action.triggered.connect(
|
127
156
|
# lambda: self.load_config(path=None, gui=True)
|
128
157
|
# )
|
@@ -1,12 +1,12 @@
|
|
1
1
|
.gitignore,sha256=cMQ1MLmnoR88aMCCJwUyfoTnufzl4-ckmHtlFUqHcT4,3253
|
2
|
-
.gitlab-ci.yml,sha256=
|
2
|
+
.gitlab-ci.yml,sha256=9dZ_EvimZrDzG8MtII0qtBYMM_Nc6xqh2iFRds0rlvE,8370
|
3
3
|
.pylintrc,sha256=eeY8YwSI74oFfq6IYIbCqnx3Vk8ZncKaatv96n_Y8Rs,18544
|
4
4
|
.readthedocs.yaml,sha256=aSOc277LqXcsTI6lgvm_JY80lMlr69GbPKgivua2cS0,603
|
5
|
-
CHANGELOG.md,sha256=
|
5
|
+
CHANGELOG.md,sha256=G7yvFhOdFjI1LmaT9vJxABkSXd4gnW3xjXG6UbYfscI,6796
|
6
6
|
LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
|
7
|
-
PKG-INFO,sha256=
|
7
|
+
PKG-INFO,sha256=95zqBPCR1UAwYTKKaUgm5m_raNwyZ55ZGEEvq4TtAZI,1325
|
8
8
|
README.md,sha256=Od69x-RS85Hph0-WwWACwal4yUd67XkEn4APEfHhHFw,2649
|
9
|
-
pyproject.toml,sha256=
|
9
|
+
pyproject.toml,sha256=AIkAxwJP8nku-9NZwg_bEQttSF31hf-kCXErAucwnTw,2416
|
10
10
|
.git_hooks/pre-commit,sha256=n3RofIZHJl8zfJJIUomcMyYGFi_rwq4CC19z0snz3FI,286
|
11
11
|
.gitlab/issue_templates/bug_report_template.md,sha256=gAuyEwl7XlnebBrkiJ9AqffSNOywmr8vygUFWKTuQeI,386
|
12
12
|
.gitlab/issue_templates/documentation_update_template.md,sha256=FHLdb3TS_D9aL4CYZCjyXSulbaW5mrN2CmwTaeLPbNw,860
|
@@ -85,7 +85,7 @@ bec_widgets/qt_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
85
85
|
bec_widgets/qt_utils/error_popups.py,sha256=y9gKKWaafp468ioHr96nBhf02ZpEgjDc-BAVOTWh-e8,7680
|
86
86
|
bec_widgets/qt_utils/redis_message_waiter.py,sha256=fvL_QgC0cTDv_FPJdRyp5AKjf401EJU4z3r38p47ydY,1745
|
87
87
|
bec_widgets/qt_utils/settings_dialog.py,sha256=NhtzTer_xzlB2lLLrGklkI1QYLJEWQpJoZbCz4o5daI,3645
|
88
|
-
bec_widgets/qt_utils/toolbar.py,sha256=
|
88
|
+
bec_widgets/qt_utils/toolbar.py,sha256=A5vXNlF7kxvdcPCmRvsy9P3f6M5NomlT2PgtUmnt94U,7669
|
89
89
|
bec_widgets/utils/__init__.py,sha256=1930ji1Jj6dVuY81Wd2kYBhHYNV-2R0bN_L4o9zBj1U,533
|
90
90
|
bec_widgets/utils/bec_connector.py,sha256=SivHKXVyNVqeu3kCXYEPpbleTVw8g1cW0FKq1QrQgco,9987
|
91
91
|
bec_widgets/utils/bec_designer.py,sha256=ak3G8FdojUPjVBBwdPXw7tN5P2Uxr-SSoQt394jXeAA,4308
|
@@ -94,7 +94,7 @@ bec_widgets/utils/bec_table.py,sha256=nA2b8ukSeUfquFMAxGrUVOqdrzMoDYD6O_4EYbOG2z
|
|
94
94
|
bec_widgets/utils/bec_widget.py,sha256=Bo2v1aP7rgSAQajW8GBJbI3iovTn_hGCsmeFMo7bT10,707
|
95
95
|
bec_widgets/utils/colors.py,sha256=hNIi99EpMv3t3hTJIL2jBe5nzh5f2fuCyEKXEPWSQSc,10501
|
96
96
|
bec_widgets/utils/container_utils.py,sha256=m3VUyAYmSWkEwApP9tBvKxPYVtc2kHw4toxIpMryJy4,1495
|
97
|
-
bec_widgets/utils/crosshair.py,sha256=
|
97
|
+
bec_widgets/utils/crosshair.py,sha256=ywj4Pr2Xx8tFsD5qrHIocanKlNqDd51ElbEfxenuYM0,11363
|
98
98
|
bec_widgets/utils/entry_validator.py,sha256=3skJIsUwTYicT76AMHm_M78RiWtUgyD2zb-Rxo2HdHQ,1313
|
99
99
|
bec_widgets/utils/generate_designer_plugin.py,sha256=eidqauS8YLgoxkPntPL0oSG_lYqI2D7fSyOZvOtCU_U,5891
|
100
100
|
bec_widgets/utils/layout_manager.py,sha256=H0nKsIMaPxRkof1MEXlSmW6w1dFxA6astaGzf4stI84,4727
|
@@ -162,7 +162,7 @@ bec_widgets/widgets/figure/figure.py,sha256=yujtlDj5NutRJ0gfHMkCVXvBNj5r2z3pb3gw
|
|
162
162
|
bec_widgets/widgets/figure/plots/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
163
163
|
bec_widgets/widgets/figure/plots/axis_settings.py,sha256=QxRpQwgfBr1H0HTjfOpiXi_-n8I0BaZhS8LRXNeVfFg,3544
|
164
164
|
bec_widgets/widgets/figure/plots/axis_settings.ui,sha256=a2qIuK9lyi9HCyrSvPr6wxzmm1FymaWcpmyOhMIiFt8,11013
|
165
|
-
bec_widgets/widgets/figure/plots/plot_base.py,sha256=
|
165
|
+
bec_widgets/widgets/figure/plots/plot_base.py,sha256=30KsfR6SlbeuV_t96kILcPU17vf_qMHajlvFwfBvN2Y,13593
|
166
166
|
bec_widgets/widgets/figure/plots/image/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
167
167
|
bec_widgets/widgets/figure/plots/image/image.py,sha256=y2MqgJv6Njv-huDN_exn0Fq1rAh5vs_assKCKHgQH6I,24941
|
168
168
|
bec_widgets/widgets/figure/plots/image/image_item.py,sha256=RljjbkqJEr2cKDlqj1j5GQ1h89jpqOV-OpFz1TbED8I,10937
|
@@ -170,7 +170,7 @@ bec_widgets/widgets/figure/plots/image/image_processor.py,sha256=GeTtWjbldy6VejM
|
|
170
170
|
bec_widgets/widgets/figure/plots/motor_map/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
171
171
|
bec_widgets/widgets/figure/plots/motor_map/motor_map.py,sha256=wgARzsm98Y8SHPPwVp1LzNlXCxKEi6a8by8yYzIWsbY,18319
|
172
172
|
bec_widgets/widgets/figure/plots/waveform/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
173
|
-
bec_widgets/widgets/figure/plots/waveform/waveform.py,sha256
|
173
|
+
bec_widgets/widgets/figure/plots/waveform/waveform.py,sha256=rc8IMLCOkGGOhCg9QOdgAfEMbxQbZEcBG-h8Xed4U5s,51310
|
174
174
|
bec_widgets/widgets/figure/plots/waveform/waveform_curve.py,sha256=ZwRxSfPHbMWEvgUC-mL2orpZvtxR-DcrYAFikkdWEzk,8654
|
175
175
|
bec_widgets/widgets/image/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
176
176
|
bec_widgets/widgets/image/bec_image_widget.pyproject,sha256=PHisdBo5_5UCApd27GkizzqgfdjsDx2bFZa_p9LiSW8,30
|
@@ -211,7 +211,7 @@ bec_widgets/widgets/ring_progress_bar/ring_progress_bar.pyproject,sha256=ZNYDnKD
|
|
211
211
|
bec_widgets/widgets/ring_progress_bar/ring_progress_bar_plugin.py,sha256=f6eAuDEvmnl0gy-cBwUkuUS4yr1VGjVEa40N5MIXwF0,1511
|
212
212
|
bec_widgets/widgets/scan_control/__init__.py,sha256=IOfHl15vxb_uC6KN62-PeUzbBha_vQyqkkXbJ2HU674,38
|
213
213
|
bec_widgets/widgets/scan_control/register_scan_control.py,sha256=xUX2yR0-MaIMg9_y9qe50yDDphzsh2x1b5PMrF90yPM,475
|
214
|
-
bec_widgets/widgets/scan_control/scan_control.py,sha256=
|
214
|
+
bec_widgets/widgets/scan_control/scan_control.py,sha256=30pcaVaBVo6JxJuMH1ul1s-sPuWqCUKsUQ-XfAgzRZ8,9384
|
215
215
|
bec_widgets/widgets/scan_control/scan_control.pyproject,sha256=eTgVDFKToIH8_BbJjM2RvbOLr7HnYoidX0SAHx640DM,30
|
216
216
|
bec_widgets/widgets/scan_control/scan_control_plugin.py,sha256=t3_qTACqYcvl_Wr102bIdVDyw6uNK1B94fh_wjs-lxg,1477
|
217
217
|
bec_widgets/widgets/scan_control/scan_group_box.py,sha256=BpX9ZphqOhdEbQnGzNeNlmuZsMS__U97CkBMical2FY,9300
|
@@ -244,7 +244,7 @@ bec_widgets/widgets/waveform/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
|
|
244
244
|
bec_widgets/widgets/waveform/bec_waveform_widget.pyproject,sha256=GLD8GN9dXx9wNbtnevrxqqcwk7vKV-Uv8QYSycdaoaI,33
|
245
245
|
bec_widgets/widgets/waveform/bec_waveform_widget_plugin.py,sha256=f8zgTQGp6hbfxOAnMqLvaI3N9KwxEMtUKkZQZBAL_rw,1514
|
246
246
|
bec_widgets/widgets/waveform/register_bec_waveform_widget.py,sha256=qZHVZH_lP2hvzkG1Ra0EyrXlMeLkRCy0aceH-bfJ1cs,490
|
247
|
-
bec_widgets/widgets/waveform/waveform_widget.py,sha256=
|
247
|
+
bec_widgets/widgets/waveform/waveform_widget.py,sha256=nzRDgo_qfmmp_aSGbXhwy2U7yq5cR0pHdjM0YDCvTKE,20409
|
248
248
|
bec_widgets/widgets/waveform/waveform_popups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
249
249
|
bec_widgets/widgets/waveform/waveform_popups/curve_dialog/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
250
250
|
bec_widgets/widgets/waveform/waveform_popups/curve_dialog/curve_dialog.py,sha256=S1j44i1xxJtmCcNtbOsxF8XdklMPsG9t4-1DZ2YfOPw,13128
|
@@ -329,8 +329,9 @@ docs/user/widgets/positioner_box/positioner_box.md,sha256=voOU_wfwd-qBykk-60PWoO
|
|
329
329
|
docs/user/widgets/progress_bar/progress_bar.gif,sha256=5jh0Zw2BBGPuNxszV1DBLJCb4_6glIRX-U2ABjnsK2k,5263592
|
330
330
|
docs/user/widgets/progress_bar/ring_progress_bar.md,sha256=F9q2UrwZWwNzbjs1ONfSgP1dqgb5dx8AbOoTZOJGzWA,3705
|
331
331
|
docs/user/widgets/queue/queue.md,sha256=Re37dp_YHTa9N6TTobScO_n3Fniq2_Ca2Ms3DOb0suA,2110
|
332
|
+
docs/user/widgets/scan_control/hide_scan_control.png,sha256=juhkTBdz-WnJfPhj6UEkOiiCD6HfTinoETokF20nhsA,21890
|
332
333
|
docs/user/widgets/scan_control/scan_control.gif,sha256=zrVOZgleMbu7Jd8AAIn2fQ08tNAEMSud3g0ZLyNUcjQ,1506739
|
333
|
-
docs/user/widgets/scan_control/scan_control.md,sha256=
|
334
|
+
docs/user/widgets/scan_control/scan_control.md,sha256=4pMTZJHx9VMYgEVGG_ybiJxwNbdQqAqa_dvM6lz0LMs,3275
|
334
335
|
docs/user/widgets/spinner/spinner.md,sha256=MN7LeAkNOqRZ_A6c4PGv6KsiGbjm7Q-nkGkL5CcxOeE,2465
|
335
336
|
docs/user/widgets/text_box/text_box.md,sha256=u8UoApmnH3LilWUQ73ASd6ts0FQfM6eRqg3GG9iQhOc,2508
|
336
337
|
docs/user/widgets/toggle/toggle.md,sha256=vmhqbTU93R9DUeR0a85gGIRPc07cSl3_vtsVN4r0yO0,2983
|
@@ -365,7 +366,7 @@ tests/unit_tests/test_bec_status_box.py,sha256=gZdjyy9DNuUP9UwleTLj2Dp5HUImiqnkH
|
|
365
366
|
tests/unit_tests/test_client_utils.py,sha256=CBdWIVJ_UiyFzTJnX3XJm4PGw2uXhFvRCP_Y9ifckbw,2630
|
366
367
|
tests/unit_tests/test_color_map_selector.py,sha256=dTsizpT7TUpX2AEWIc0v09KPOryhWepFXFI9duQ3NF8,1497
|
367
368
|
tests/unit_tests/test_color_validation.py,sha256=xbFbtFDia36XLgaNrX2IwvAX3IDC_Odpj5BGoJSgiIE,2389
|
368
|
-
tests/unit_tests/test_crosshair.py,sha256=
|
369
|
+
tests/unit_tests/test_crosshair.py,sha256=POB9EFCO9gCCAEHgddiyQV28C4rMxIosHX0aR-zr8gg,4595
|
369
370
|
tests/unit_tests/test_device_browser.py,sha256=LOvvUFepNcoQptX7d6oUoomfg5nyjjdCMJ2iHviF9aQ,2948
|
370
371
|
tests/unit_tests/test_device_input_base.py,sha256=LY-3adMb2xM9pBiP6V2bAJF_65csCe2Xfaq5ArVEE1E,2859
|
371
372
|
tests/unit_tests/test_device_input_widgets.py,sha256=Y3mc_EaeQAPxpj6DijvxLLyMPSxnaNN86KhIL4ASO3E,5821
|
@@ -398,8 +399,8 @@ tests/unit_tests/test_configs/config_device_no_entry.yaml,sha256=hdvue9KLc_kfNzG
|
|
398
399
|
tests/unit_tests/test_configs/config_scan.yaml,sha256=vo484BbWOjA_e-h6bTjSV9k7QaQHrlAvx-z8wtY-P4E,1915
|
399
400
|
tests/unit_tests/test_msgs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
400
401
|
tests/unit_tests/test_msgs/available_scans_message.py,sha256=m_z97hIrjHXXMa2Ex-UvsPmTxOYXfjxyJaGkIY6StTY,46532
|
401
|
-
bec_widgets-0.
|
402
|
-
bec_widgets-0.
|
403
|
-
bec_widgets-0.
|
404
|
-
bec_widgets-0.
|
405
|
-
bec_widgets-0.
|
402
|
+
bec_widgets-0.96.1.dist-info/METADATA,sha256=95zqBPCR1UAwYTKKaUgm5m_raNwyZ55ZGEEvq4TtAZI,1325
|
403
|
+
bec_widgets-0.96.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
404
|
+
bec_widgets-0.96.1.dist-info/entry_points.txt,sha256=3otEkCdDB9LZJuBLzG4pFLK5Di0CVybN_12IsZrQ-58,166
|
405
|
+
bec_widgets-0.96.1.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
|
406
|
+
bec_widgets-0.96.1.dist-info/RECORD,,
|
Binary file
|
@@ -23,11 +23,19 @@ By default, this widget supports scans that are derived from the following base
|
|
23
23
|
The full procedure how to design `gui_config` for your custom scan class is described in the [Scan GUI Configuration](https://bec.readthedocs.io/en/latest/developer/scans/scan_gui_config.html) tutorial.
|
24
24
|
```
|
25
25
|
|
26
|
+
## BECDesigner Customization
|
27
|
+
Within the BECDesigner's [property editor](https://doc.qt.io/qt-6/designer-widget-mode.html#the-property-editor/), the `ScanControl` widget can be customized to suit your application's requirements. The widget provides the following customization options:
|
28
|
+
- **Hide Scan Control**: Allows you to hide the scan control buttons from the widget interface. This is useful when you want to place the control buttons in a different location.
|
29
|
+
- **Hide Scan Selection**: Allows you to hide the scan selection combobox from the widget interface. This is useful when you want to restrict the user to a specific scan type or implement a custom scan selection mechanism.
|
30
|
+
|
31
|
+
```{figure} ./hide_scan_control.png
|
32
|
+
```
|
33
|
+
|
26
34
|
````
|
27
35
|
|
28
36
|
````{tab} Examples
|
29
37
|
|
30
|
-
The `ScanControl` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `
|
38
|
+
The `ScanControl` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BECDesigner`. Below are examples demonstrating how to create and use the `ScanControl` widget.
|
31
39
|
|
32
40
|
## Example 1 - Adding Scan Control Widget to BECDockArea
|
33
41
|
|
pyproject.toml
CHANGED
@@ -1,19 +1,40 @@
|
|
1
1
|
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
2
2
|
import numpy as np
|
3
|
-
import
|
3
|
+
import pytest
|
4
4
|
from qtpy.QtCore import QPointF
|
5
5
|
|
6
|
-
from bec_widgets.
|
6
|
+
from bec_widgets.widgets.image.image_widget import BECImageWidget
|
7
|
+
from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget
|
7
8
|
|
9
|
+
from .client_mocks import mocked_client
|
8
10
|
|
9
|
-
|
10
|
-
# Create a PlotWidget and add a PlotItem
|
11
|
-
plot_widget = pg.PlotWidget(title="1D PlotWidget with multiple curves")
|
12
|
-
plot_item = plot_widget.getPlotItem()
|
13
|
-
plot_item.plot([1, 2, 3], [4, 5, 6])
|
11
|
+
# pylint: disable = redefined-outer-name
|
14
12
|
|
15
|
-
|
16
|
-
|
13
|
+
|
14
|
+
@pytest.fixture
|
15
|
+
def plot_widget_with_crosshair(qtbot, mocked_client):
|
16
|
+
widget = BECWaveformWidget(client=mocked_client())
|
17
|
+
widget.plot(x=[1, 2, 3], y=[4, 5, 6])
|
18
|
+
widget.waveform.hook_crosshair()
|
19
|
+
qtbot.addWidget(widget)
|
20
|
+
qtbot.waitExposed(widget)
|
21
|
+
|
22
|
+
yield widget.waveform.crosshair, widget.waveform.plot_item
|
23
|
+
|
24
|
+
|
25
|
+
@pytest.fixture
|
26
|
+
def image_widget_with_crosshair(qtbot, mocked_client):
|
27
|
+
widget = BECImageWidget(client=mocked_client())
|
28
|
+
widget._image.add_custom_image(name="test", data=np.random.random((100, 200)))
|
29
|
+
widget._image.hook_crosshair()
|
30
|
+
qtbot.addWidget(widget)
|
31
|
+
qtbot.waitExposed(widget)
|
32
|
+
|
33
|
+
yield widget._image.crosshair, widget._image.plot_item
|
34
|
+
|
35
|
+
|
36
|
+
def test_mouse_moved_lines(plot_widget_with_crosshair):
|
37
|
+
crosshair, plot_item = plot_widget_with_crosshair
|
17
38
|
|
18
39
|
# Connect the signals to slots that will store the emitted values
|
19
40
|
emitted_values_1D = []
|
@@ -32,14 +53,8 @@ def test_mouse_moved_lines(qtbot):
|
|
32
53
|
assert crosshair.h_line.pos().y() == 5
|
33
54
|
|
34
55
|
|
35
|
-
def test_mouse_moved_signals(
|
36
|
-
|
37
|
-
plot_widget = pg.PlotWidget(title="1D PlotWidget with multiple curves")
|
38
|
-
plot_item = plot_widget.getPlotItem()
|
39
|
-
plot_item.plot([1, 2, 3], [4, 5, 6])
|
40
|
-
|
41
|
-
# Create a Crosshair instance
|
42
|
-
crosshair = Crosshair(plot_item=plot_item, precision=2)
|
56
|
+
def test_mouse_moved_signals(plot_widget_with_crosshair):
|
57
|
+
crosshair, plot_item = plot_widget_with_crosshair
|
43
58
|
|
44
59
|
# Create a slot that will store the emitted values as tuples
|
45
60
|
emitted_values_1D = []
|
@@ -59,17 +74,11 @@ def test_mouse_moved_signals(qtbot):
|
|
59
74
|
crosshair.mouse_moved(event_mock)
|
60
75
|
|
61
76
|
# Assert the expected behavior
|
62
|
-
assert emitted_values_1D == [(2,
|
63
|
-
|
77
|
+
assert emitted_values_1D == [("Curve 1", 2, 5)]
|
64
78
|
|
65
|
-
def test_mouse_moved_signals_outside(qtbot):
|
66
|
-
# Create a PlotWidget and add a PlotItem
|
67
|
-
plot_widget = pg.PlotWidget(title="1D PlotWidget with multiple curves")
|
68
|
-
plot_item = plot_widget.getPlotItem()
|
69
|
-
plot_item.plot([1, 2, 3], [4, 5, 6])
|
70
79
|
|
71
|
-
|
72
|
-
crosshair
|
80
|
+
def test_mouse_moved_signals_outside(plot_widget_with_crosshair):
|
81
|
+
crosshair, plot_item = plot_widget_with_crosshair
|
73
82
|
|
74
83
|
# Create a slot that will store the emitted values as tuples
|
75
84
|
emitted_values_1D = []
|
@@ -92,17 +101,9 @@ def test_mouse_moved_signals_outside(qtbot):
|
|
92
101
|
assert emitted_values_1D == []
|
93
102
|
|
94
103
|
|
95
|
-
def test_mouse_moved_signals_2D(
|
96
|
-
|
104
|
+
def test_mouse_moved_signals_2D(image_widget_with_crosshair):
|
105
|
+
crosshair, plot_item = image_widget_with_crosshair
|
97
106
|
|
98
|
-
# Create a PlotWidget and add a PlotItem
|
99
|
-
plot_widget = pg.PlotWidget(title="2D plot with crosshair and ROI square")
|
100
|
-
data_2D = np.random.random((100, 200))
|
101
|
-
plot_item = plot_widget.getPlotItem()
|
102
|
-
image_item = pg.ImageItem(data_2D)
|
103
|
-
plot_item.addItem(image_item)
|
104
|
-
# Create a Crosshair instance
|
105
|
-
crosshair = Crosshair(plot_item=plot_item)
|
106
107
|
# Create a slot that will store the emitted values as tuples
|
107
108
|
emitted_values_2D = []
|
108
109
|
|
@@ -118,20 +119,12 @@ def test_mouse_moved_signals_2D(qtbot):
|
|
118
119
|
# Call the mouse_moved method
|
119
120
|
crosshair.mouse_moved(event_mock)
|
120
121
|
# Assert the expected behavior
|
121
|
-
assert emitted_values_2D == [(22.0, 55.0)]
|
122
|
+
assert emitted_values_2D == [("test", 22.0, 55.0)]
|
122
123
|
|
123
124
|
|
124
|
-
def test_mouse_moved_signals_2D_outside(
|
125
|
-
|
125
|
+
def test_mouse_moved_signals_2D_outside(image_widget_with_crosshair):
|
126
|
+
crosshair, plot_item = image_widget_with_crosshair
|
126
127
|
|
127
|
-
# Create a PlotWidget and add a PlotItem
|
128
|
-
plot_widget = pg.PlotWidget(title="2D plot with crosshair and ROI square")
|
129
|
-
data_2D = np.random.random((100, 200))
|
130
|
-
plot_item = plot_widget.getPlotItem()
|
131
|
-
image_item = pg.ImageItem(data_2D)
|
132
|
-
plot_item.addItem(image_item)
|
133
|
-
# Create a Crosshair instance
|
134
|
-
crosshair = Crosshair(plot_item=plot_item, precision=2)
|
135
128
|
# Create a slot that will store the emitted values as tuples
|
136
129
|
emitted_values_2D = []
|
137
130
|
|
File without changes
|
File without changes
|
File without changes
|