bec-widgets 0.96.0__py3-none-any.whl → 0.96.2__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 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,29 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.96.2 (2024-08-22)
4
+
5
+ ### Fix
6
+
7
+ * fix(waveform): validation of custom curves removed ([`af28574`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/af28574bd58457a05f1269f121db01ad627b5769))
8
+
9
+ * fix(waveform): skip validation for curves that are not BECCurve instances ([`617db36`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/617db36ed4932c8a0633724079b695bc67d5c77b))
10
+
11
+ ## v0.96.1 (2024-08-22)
12
+
13
+ ### Ci
14
+
15
+ * ci: fail pytest after 2 failed tests ([`f0203d9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0203d9bf60c4975ba5ab93a057d9091762454d5))
16
+
17
+ ### Fix
18
+
19
+ * fix(crosshair): update markers if necessary ([`4473805`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/44738057a36f5de2bbb55affdd309f92286d4a0f))
20
+
21
+ * fix(waveform_widget): fixed icon appearance ([`f98a9f9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f98a9f9771b93226d47830aa52f45739624f51b4))
22
+
23
+ * fix: bubble-up signals ([`2fe72c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2fe72c9ccb71bcb196a1b78197b73acf9aa3f506))
24
+
25
+ * fix(crosshair): fixed crosshair for image and waveforms ([`37835cb`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37835cbf76ca3ba1081f514ee7793244ac500e7f))
26
+
3
27
  ## v0.96.0 (2024-08-22)
4
28
 
5
29
  ### Documentation
@@ -133,31 +157,3 @@ Terminating client connections has to be done at the application level ([`198c1d
133
157
  ### Fix
134
158
 
135
159
  * fix(positioner_box): icons fixed ([`281633d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/281633deff15b6879dac3a4f0770fa6949aaecdc))
136
-
137
- ### Refactor
138
-
139
- * refactor: add button for positioner selection ([`0d190c5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0d190c5c5996e59fec4bdd44d2003e10e200b009))
140
-
141
- ### Test
142
-
143
- * test(dap): wait for fit ([`6269009`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6269009e5451f830cdee58a514c7858483488a8d))
144
-
145
- * test(auto-update): wait for rendering ([`6d2442d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6d2442d23c683fe92af13df982ce681c07e99cde))
146
-
147
- ## v0.93.4 (2024-08-07)
148
-
149
- ### Fix
150
-
151
- * fix: rename DeviceBox to PositionerBox, fix test for validation ([`37aa371`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37aa371e7c4c62d70abf37abc125db0c088790fe))
152
-
153
- * fix: add validation for bec_lib.device.Positioner; closes #268 ([`eb54e9f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/eb54e9f788e97af23db8fe0c78f8facb8688bb99))
154
-
155
- ## v0.93.3 (2024-08-07)
156
-
157
- ### Fix
158
-
159
- * fix(dock): properly shut down docks and temp areas ([`99ee545`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99ee545e41c6078654958b668b5b329f85553d16))
160
-
161
- ### Test
162
-
163
- * test: removed quit from teardown ([`cf94599`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cf94599c2544d6831c8afbe7b340082077557ed1))
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bec_widgets
3
- Version: 0.96.0
3
+ Version: 0.96.2
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
@@ -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.
@@ -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
- marker_clicked = pg.ScatterPlotItem(
67
- size=10, pen=pg.mkPen(None), brush=pg.mkBrush(color)
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
- marker_clicked_list.append(marker_clicked)
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: The nearest x and y values
100
+ tuple: x and y values snapped to the nearest data
99
101
  """
100
- y_values_1d = []
101
- x_values_1d = []
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
- x_data, y_data = item.xData, item.yData
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
- return None, None
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
- y_values_1d.append(closest_y)
118
- x_values_1d.append(closest_x)
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
- # Handle 1D plot
123
- if y_values_1d:
124
- if all(v is None for v in x_values_1d) or all(v is None for v in y_values_1d):
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
- closest_x = min(x_values_1d, key=lambda xi: abs(xi - x)) # Snap x to closest data point
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
- x, y_values = self.snap_to_data(x, y)
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
- if x is None or all(v is None for v in y_values):
176
- return
177
- coordinate_to_emit = (
178
- round(x, self.precision),
179
- [round(y_val, self.precision) for y_val in y_values],
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
- if x is None or y_values is None:
189
- return
190
- coordinate_to_emit = (x, y_values)
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
- self.check_log()
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
- x, y_values = self.snap_to_data(x, y)
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
- if x is None or all(v is None for v in y_values):
213
- return
214
- coordinate_to_emit = (
215
- round(x, self.precision),
216
- [round(y_val, self.precision) for y_val in y_values],
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
- if x is None or y_values is None:
227
- return
228
- coordinate_to_emit = (x, y_values)
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
- self.marker_2d.setPos([x, y_values])
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
- self.plot_item = self.addPlot(row=0, col=0)
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,
@@ -375,6 +376,10 @@ class BECWaveform(BECPlotBase):
375
376
  if len(self.curves) > 0:
376
377
  # validate all curves
377
378
  for curve in self.curves:
379
+ if not isinstance(curve, BECCurve):
380
+ continue
381
+ if curve.config.source == "custom":
382
+ continue
378
383
  self._validate_x_axis_behaviour(curve.config.signals.y.name, x_name, x_entry, False)
379
384
  self._switch_x_axis_item(
380
385
  f"{x_name}-{x_entry}"
@@ -382,9 +387,12 @@ class BECWaveform(BECPlotBase):
382
387
  else x_name
383
388
  )
384
389
  for curve_id, curve_config in zip(curve_ids, curve_configs):
385
- if curve_config.signals.x:
386
- curve_config.signals.x.name = x_name
387
- curve_config.signals.x.entry = x_entry
390
+ if curve_config.signals is None:
391
+ continue
392
+ if curve_config.signals.x is None:
393
+ continue
394
+ curve_config.signals.x.name = x_name
395
+ curve_config.signals.x.entry = x_entry
388
396
  self.remove_curve(curve_id)
389
397
  self.add_curve_by_config(curve_config)
390
398
 
@@ -408,23 +416,6 @@ class BECWaveform(BECPlotBase):
408
416
  """
409
417
  self.plot_item.enableAutoRange(axis, enabled)
410
418
 
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
419
  def add_curve_custom(
429
420
  self,
430
421
  x: list | np.ndarray,
@@ -935,6 +926,8 @@ class BECWaveform(BECPlotBase):
935
926
  return
936
927
 
937
928
  if current_scan_id != self.scan_id:
929
+ self.reset()
930
+ self.new_scan.emit()
938
931
  self.set_auto_range(True, "xy")
939
932
  self.old_scan_id = self.scan_id
940
933
  self.scan_id = current_scan_id
@@ -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 IconAction, ModularToolBar, SeparatorAction
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": IconAction(
97
- icon_path="settings.svg", tooltip="Open Configuration Dialog"
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bec_widgets
3
- Version: 0.96.0
3
+ Version: 0.96.2
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
@@ -1,12 +1,12 @@
1
1
  .gitignore,sha256=cMQ1MLmnoR88aMCCJwUyfoTnufzl4-ckmHtlFUqHcT4,3253
2
- .gitlab-ci.yml,sha256=BtKhZI3dhK09En1BfpglYi-ZJwG6ZdC-iJr7kXFVfCg,8346
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=cn9vMVxfaE1aginGiTCdUyQqJGCeFJ8LjFsmXsuI8Gg,6733
5
+ CHANGELOG.md,sha256=JevFb8k7ihFuKfooyCqcyptPCHkn6vBmWWLI6YOyNHg,6717
6
6
  LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
7
- PKG-INFO,sha256=wPwVfj4xSkopVoH2p_P6HnIWGsIpRruwbtMoyn-TEW0,1325
7
+ PKG-INFO,sha256=xRvEZ02kp6ROhkDYvw-jCEJA0QqyjeTRUHYJnbgxgXY,1325
8
8
  README.md,sha256=Od69x-RS85Hph0-WwWACwal4yUd67XkEn4APEfHhHFw,2649
9
- pyproject.toml,sha256=K2NOh1jWSsA8Lu9cm-ed-LPb1LoORhaFvSNJcmFGcy4,2416
9
+ pyproject.toml,sha256=S3RcibQIzFA7_Ydzi3rai8TL0k5643CILs3-IMEJ_8s,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=89WddOXPePby2CICUumdN_K_3DBgZPCR8HWUJAwrhDU,6503
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=SubY4FQCI6vUKsmMYGKHR7uYdGQJ6vhoYLuC1XlKS9I,9626
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=TG9FZZJi9ixTeZ1yDOO4sl7fJ524wxbNcr-2_Qd2mcQ,11154
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=-op62_CFX1Gu-Ag6CD5d3ylEDqu_ieTbfoTUF2zf7T8,51800
173
+ bec_widgets/widgets/figure/plots/waveform/waveform.py,sha256=uW9GQHWiVd3q63-yXjQ2uewDiiQ3t226swYTPQhaGoY,51579
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
@@ -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=4aHwkLdn7C8BgvKqFIbOB0YaAxAIIFTG_hG0iLougKQ,19094
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
@@ -366,7 +366,7 @@ tests/unit_tests/test_bec_status_box.py,sha256=gZdjyy9DNuUP9UwleTLj2Dp5HUImiqnkH
366
366
  tests/unit_tests/test_client_utils.py,sha256=CBdWIVJ_UiyFzTJnX3XJm4PGw2uXhFvRCP_Y9ifckbw,2630
367
367
  tests/unit_tests/test_color_map_selector.py,sha256=dTsizpT7TUpX2AEWIc0v09KPOryhWepFXFI9duQ3NF8,1497
368
368
  tests/unit_tests/test_color_validation.py,sha256=xbFbtFDia36XLgaNrX2IwvAX3IDC_Odpj5BGoJSgiIE,2389
369
- tests/unit_tests/test_crosshair.py,sha256=3OMAJ2ZaISYXMOtkXf1rPdy94vCr8njeLi6uHblBL9Q,5045
369
+ tests/unit_tests/test_crosshair.py,sha256=POB9EFCO9gCCAEHgddiyQV28C4rMxIosHX0aR-zr8gg,4595
370
370
  tests/unit_tests/test_device_browser.py,sha256=LOvvUFepNcoQptX7d6oUoomfg5nyjjdCMJ2iHviF9aQ,2948
371
371
  tests/unit_tests/test_device_input_base.py,sha256=LY-3adMb2xM9pBiP6V2bAJF_65csCe2Xfaq5ArVEE1E,2859
372
372
  tests/unit_tests/test_device_input_widgets.py,sha256=Y3mc_EaeQAPxpj6DijvxLLyMPSxnaNN86KhIL4ASO3E,5821
@@ -399,8 +399,8 @@ tests/unit_tests/test_configs/config_device_no_entry.yaml,sha256=hdvue9KLc_kfNzG
399
399
  tests/unit_tests/test_configs/config_scan.yaml,sha256=vo484BbWOjA_e-h6bTjSV9k7QaQHrlAvx-z8wtY-P4E,1915
400
400
  tests/unit_tests/test_msgs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
401
401
  tests/unit_tests/test_msgs/available_scans_message.py,sha256=m_z97hIrjHXXMa2Ex-UvsPmTxOYXfjxyJaGkIY6StTY,46532
402
- bec_widgets-0.96.0.dist-info/METADATA,sha256=wPwVfj4xSkopVoH2p_P6HnIWGsIpRruwbtMoyn-TEW0,1325
403
- bec_widgets-0.96.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
404
- bec_widgets-0.96.0.dist-info/entry_points.txt,sha256=3otEkCdDB9LZJuBLzG4pFLK5Di0CVybN_12IsZrQ-58,166
405
- bec_widgets-0.96.0.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
406
- bec_widgets-0.96.0.dist-info/RECORD,,
402
+ bec_widgets-0.96.2.dist-info/METADATA,sha256=xRvEZ02kp6ROhkDYvw-jCEJA0QqyjeTRUHYJnbgxgXY,1325
403
+ bec_widgets-0.96.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
404
+ bec_widgets-0.96.2.dist-info/entry_points.txt,sha256=3otEkCdDB9LZJuBLzG4pFLK5Di0CVybN_12IsZrQ-58,166
405
+ bec_widgets-0.96.2.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
406
+ bec_widgets-0.96.2.dist-info/RECORD,,
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec_widgets"
7
- version = "0.96.0"
7
+ version = "0.96.2"
8
8
  description = "BEC Widgets"
9
9
  requires-python = ">=3.10"
10
10
  classifiers = [
@@ -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 pyqtgraph as pg
3
+ import pytest
4
4
  from qtpy.QtCore import QPointF
5
5
 
6
- from bec_widgets.utils import Crosshair
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
- def test_mouse_moved_lines(qtbot):
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
- # Create a Crosshair instance
16
- crosshair = Crosshair(plot_item=plot_item, precision=2)
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(qtbot):
36
- # Create a PlotWidget and add a PlotItem
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, [5])]
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
- # Create a Crosshair instance
72
- crosshair = Crosshair(plot_item=plot_item, precision=2)
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(qtbot):
96
- # write similar test for 2D plot
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(qtbot):
125
- # write similar test for 2D plot
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