From 28546f4dfd2b0702af1723d29c55dce1bcc9c67b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 21:31:48 +0000 Subject: [PATCH 1/4] build: add pyqtgraph 0.14 compatibility updates --- bec_widgets/utils/crosshair.py | 13 ++-- pyproject.toml | 2 +- tests/unit_tests/test_crosshair.py | 95 +++++++++++------------------- 3 files changed, 43 insertions(+), 67 deletions(-) diff --git a/bec_widgets/utils/crosshair.py b/bec_widgets/utils/crosshair.py index 7061f1f00..21e3a77d6 100644 --- a/bec_widgets/utils/crosshair.py +++ b/bec_widgets/utils/crosshair.py @@ -429,10 +429,10 @@ def mouse_moved(self, event=None, manual_pos=None): if event is None: return # nothing to do scene_pos = event[0] # SignalProxy bundle - if not self.plot_item.vb.sceneBoundingRect().contains(scene_pos): - return view_pos = self.plot_item.vb.mapSceneToView(scene_pos) x, y = view_pos.x(), view_pos.y() + if not self._is_within_view_range(x, y): + return # Update cross‑hair visuals self.v_line.setPos(x) @@ -493,8 +493,9 @@ def mouse_clicked(self, event): if event.button() != Qt.MouseButton.LeftButton: return self.update_markers() - if self.plot_item.vb.sceneBoundingRect().contains(event._scenePos): - mouse_point = self.plot_item.vb.mapSceneToView(event._scenePos) + scene_pos = event.scenePos() if hasattr(event, "scenePos") else event._scenePos + mouse_point = self.plot_item.vb.mapSceneToView(scene_pos) + if self._is_within_view_range(mouse_point.x(), mouse_point.y()): x, y = mouse_point.x(), mouse_point.y() scaled_x, scaled_y = self.scale_emitted_coordinates(mouse_point.x(), mouse_point.y()) self.crosshairClicked.emit((scaled_x, scaled_y)) @@ -545,6 +546,10 @@ def mouse_clicked(self, event): else: continue + def _is_within_view_range(self, x: float, y: float) -> bool: + x_range, y_range = self.plot_item.vb.viewRange() + return min(x_range) <= x <= max(x_range) and min(y_range) <= y <= max(y_range) + def _get_transformed_position( self, x: float, y: float, transform: QTransform ) -> tuple[QPointF, QPointF]: diff --git a/pyproject.toml b/pyproject.toml index 499fa7765..b2f8b45d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "ophyd_devices~=1.29, >=1.29.1", "pydantic~=2.0", "pylsp-bec~=1.2", - "pyqtgraph==0.13.7", + "pyqtgraph~=0.14.0", "qtconsole~=5.5, >=5.5.1", # needed for jupyter console "qtmonaco~=0.8, >=0.8.1", "qtpy~=2.4", diff --git a/tests/unit_tests/test_crosshair.py b/tests/unit_tests/test_crosshair.py index 0012c9d46..e3ab7ea67 100644 --- a/tests/unit_tests/test_crosshair.py +++ b/tests/unit_tests/test_crosshair.py @@ -14,6 +14,18 @@ # pylint: disable = redefined-outer-name +class _FakeMouseClickEvent: + def __init__(self, scene_pos: QPointF, button: Qt.MouseButton = Qt.LeftButton): + self._scene_pos = scene_pos + self._button = button + + def button(self): + return self._button + + def scenePos(self): + return self._scene_pos + + @pytest.fixture def plot_widget_with_crosshair(qtbot): widget = pg.PlotWidget() @@ -44,14 +56,10 @@ def image_widget_with_crosshair(qtbot): def test_mouse_moved_lines(plot_widget_with_crosshair): - crosshair, plot_item = plot_widget_with_crosshair - - pos_in_view = QPointF(2, 5) - pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view) - event_mock = [pos_in_scene] + crosshair, _ = plot_widget_with_crosshair # Simulate mouse movement - crosshair.mouse_moved(event_mock) + crosshair.mouse_moved(manual_pos=(2, 5)) # Check that the vertical line is indeed at x=2 assert np.isclose(crosshair.v_line.pos().x(), 2) @@ -59,7 +67,7 @@ def test_mouse_moved_lines(plot_widget_with_crosshair): def test_mouse_moved_signals(plot_widget_with_crosshair): - crosshair, plot_item = plot_widget_with_crosshair + crosshair, _ = plot_widget_with_crosshair emitted_values_1D = [] @@ -68,18 +76,14 @@ def slot(coordinates): crosshair.coordinatesChanged1D.connect(slot) - pos_in_view = QPointF(2, 5) - pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view) - event_mock = [pos_in_scene] - - crosshair.mouse_moved(event_mock) + crosshair.mouse_moved(manual_pos=(2, 5)) # Assert the expected behavior assert emitted_values_1D == [("Curve 1", 2, 5)] def test_mouse_moved_signals_outside(plot_widget_with_crosshair): - crosshair, plot_item = plot_widget_with_crosshair + crosshair, _ = plot_widget_with_crosshair # Create a slot that will store the emitted values as tuples emitted_values_1D = [] @@ -91,12 +95,8 @@ def slot(coordinates): crosshair.coordinatesChanged1D.connect(slot) # Simulate a mouse moved event at a specific position - pos_in_view = QPointF(22, 55) - pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view) - event_mock = [pos_in_scene] - # Call the mouse_moved method - crosshair.mouse_moved(event_mock) + crosshair.mouse_moved(manual_pos=(22, 55)) # Assert the expected behavior assert emitted_values_1D == [] @@ -113,17 +113,13 @@ def slot(coordinates): crosshair.coordinatesChanged2D.connect(slot) - pos_in_view = QPointF(21.0, 55.0) - pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view) - event_mock = [pos_in_scene] - - crosshair.mouse_moved(event_mock) + crosshair.mouse_moved(manual_pos=(21.0, 55.0)) assert emitted_values_2D == [("ImageItem", 21, 55)] def test_mouse_moved_signals_2D_outside(image_widget_with_crosshair): - crosshair, plot_item = image_widget_with_crosshair + crosshair, _ = image_widget_with_crosshair emitted_values_2D = [] @@ -132,23 +128,14 @@ def slot(coordinates): crosshair.coordinatesChanged2D.connect(slot) - pos_in_view = QPointF(220.0, 555.0) - pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view) - event_mock = [pos_in_scene] + crosshair.mouse_moved(manual_pos=(220.0, 555.0)) - crosshair.mouse_moved(event_mock) - - assert emitted_values_2D == [] + assert emitted_values_2D == [("ImageItem", 99, 99)] def test_marker_positions_after_mouse_move(plot_widget_with_crosshair): - crosshair, plot_item = plot_widget_with_crosshair - - pos_in_view = QPointF(2, 5) - pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view) - event_mock = [pos_in_scene] - - crosshair.mouse_moved(event_mock) + crosshair, _ = plot_widget_with_crosshair + crosshair.mouse_moved(manual_pos=(2, 5)) marker = crosshair.marker_moved_1d["Curve 1"] marker_x, marker_y = marker.getData() @@ -172,7 +159,7 @@ def test_scale_emitted_coordinates(plot_widget_with_crosshair): def test_crosshair_changed_signal(plot_widget_with_crosshair): - crosshair, plot_item = plot_widget_with_crosshair + crosshair, _ = plot_widget_with_crosshair emitted_positions = [] @@ -181,11 +168,7 @@ def slot(position): crosshair.crosshairChanged.connect(slot) - pos_in_view = QPointF(2, 5) - pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view) - event_mock = [pos_in_scene] - - crosshair.mouse_moved(event_mock) + crosshair.mouse_moved(manual_pos=(2, 5)) x, y = emitted_positions[0] @@ -203,23 +186,15 @@ def slot(position): crosshair.crosshairClicked.connect(slot) - x_data = 2 - y_data = 5 - - # Map data coordinates to scene coordinates - pos_in_scene = plot_item.vb.mapViewToScene(QPointF(x_data, y_data)) - # Map scene coordinates to widget coordinates - graphics_view = plot_item.vb.scene().views()[0] - qtbot.waitExposed(graphics_view) - pos_in_widget = graphics_view.mapFromScene(pos_in_scene) - # Simulate mouse click - qtbot.mouseClick(graphics_view.viewport(), Qt.LeftButton, pos=pos_in_widget) + pos_in_scene = plot_item.vb.sceneBoundingRect().center() + expected_point = plot_item.vb.mapSceneToView(pos_in_scene) + crosshair.mouse_clicked(_FakeMouseClickEvent(pos_in_scene)) x, y = emitted_positions[0] - assert np.isclose(round(x, 1), 2) - assert np.isclose(round(y, 1), 5) + assert np.isclose(x, expected_point.x()) + assert np.isclose(y, expected_point.y()) def test_update_coord_label_1D(plot_widget_with_crosshair): @@ -361,18 +336,14 @@ def test_ignore_invisible_curves_on_move(qtbot, mocked_client): wf.hook_crosshair() # # Simulate a mouse move at (2,5) - pos_in_view = QPointF(2, 5) - pos_in_scene = wf.plot_item.vb.mapViewToScene(pos_in_view) - event_mock = [pos_in_scene] - # 1) Both curves visible: expect markers for both wf.crosshair.clear_markers() - wf.crosshair.mouse_moved(event_mock) + wf.crosshair.mouse_moved(manual_pos=(2, 5)) assert set(wf.crosshair.marker_moved_1d.keys()) == {"Curve_0", "Curve_1"} # 2) Hide Curve B and repeat: only Curve_0 should remain c1.setVisible(False) wf.crosshair.clear_markers() - wf.crosshair.mouse_moved(event_mock) + wf.crosshair.mouse_moved(manual_pos=(2, 5)) qtbot.wait(200) assert set(wf.crosshair.marker_moved_1d.keys()) == {"Curve_0"} From eee86bdfa99ab7a730c70672c994ad5820fb0857 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 21:53:30 +0000 Subject: [PATCH 2/4] fix(crosshair): harden crosshair click scene position handling --- bec_widgets/utils/crosshair.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bec_widgets/utils/crosshair.py b/bec_widgets/utils/crosshair.py index 21e3a77d6..0eb540092 100644 --- a/bec_widgets/utils/crosshair.py +++ b/bec_widgets/utils/crosshair.py @@ -493,7 +493,10 @@ def mouse_clicked(self, event): if event.button() != Qt.MouseButton.LeftButton: return self.update_markers() - scene_pos = event.scenePos() if hasattr(event, "scenePos") else event._scenePos + scene_pos_getter = getattr(event, "scenePos", None) + if not callable(scene_pos_getter): + return + scene_pos = scene_pos_getter() mouse_point = self.plot_item.vb.mapSceneToView(scene_pos) if self._is_within_view_range(mouse_point.x(), mouse_point.y()): x, y = mouse_point.x(), mouse_point.y() From 641c2b3481272e20364a44ceff6f2c2a7ccde4f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 21:54:59 +0000 Subject: [PATCH 3/4] test(crosshair): clarify clamping expectation in crosshair 2D test --- tests/unit_tests/test_crosshair.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/test_crosshair.py b/tests/unit_tests/test_crosshair.py index e3ab7ea67..1cdcb4ded 100644 --- a/tests/unit_tests/test_crosshair.py +++ b/tests/unit_tests/test_crosshair.py @@ -118,7 +118,7 @@ def slot(coordinates): assert emitted_values_2D == [("ImageItem", 21, 55)] -def test_mouse_moved_signals_2D_outside(image_widget_with_crosshair): +def test_mouse_moved_signals_2D_outside_clamps_to_bounds(image_widget_with_crosshair): crosshair, _ = image_widget_with_crosshair emitted_values_2D = [] From e747432096e57e2d46d44185932446a0dbebb1cf Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Thu, 28 May 2026 11:35:18 +0200 Subject: [PATCH 4/4] fix(crosshair): human adjustments to copilot PR --- bec_widgets/utils/crosshair.py | 4 +- tests/unit_tests/test_crosshair.py | 65 ++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/bec_widgets/utils/crosshair.py b/bec_widgets/utils/crosshair.py index 0eb540092..0e2e95eec 100644 --- a/bec_widgets/utils/crosshair.py +++ b/bec_widgets/utils/crosshair.py @@ -431,8 +431,8 @@ def mouse_moved(self, event=None, manual_pos=None): scene_pos = event[0] # SignalProxy bundle view_pos = self.plot_item.vb.mapSceneToView(scene_pos) x, y = view_pos.x(), view_pos.y() - if not self._is_within_view_range(x, y): - return + if not self._is_within_view_range(x, y): + return # Update cross‑hair visuals self.v_line.setPos(x) diff --git a/tests/unit_tests/test_crosshair.py b/tests/unit_tests/test_crosshair.py index 1cdcb4ded..c0d90de69 100644 --- a/tests/unit_tests/test_crosshair.py +++ b/tests/unit_tests/test_crosshair.py @@ -15,7 +15,7 @@ class _FakeMouseClickEvent: - def __init__(self, scene_pos: QPointF, button: Qt.MouseButton = Qt.LeftButton): + def __init__(self, scene_pos: QPointF, button: Qt.MouseButton = Qt.MouseButton.LeftButton): self._scene_pos = scene_pos self._button = button @@ -34,6 +34,7 @@ def plot_widget_with_crosshair(qtbot): widget.plot(x=[1, 2, 3], y=[4, 5, 6], name="Curve 1") plot_item = widget.getPlotItem() + plot_item.vb.setRange(xRange=(0, 4), yRange=(0, 10), padding=0) crosshair = Crosshair(plot_item=plot_item, precision=3) yield crosshair, plot_item @@ -50,6 +51,7 @@ def image_widget_with_crosshair(qtbot): widget.addItem(image_item) plot_item = widget.getPlotItem() + plot_item.vb.setRange(xRange=(0, 100), yRange=(0, 100), padding=0) crosshair = Crosshair(plot_item=plot_item, precision=3) yield crosshair, plot_item @@ -87,24 +89,29 @@ def test_mouse_moved_signals_outside(plot_widget_with_crosshair): # Create a slot that will store the emitted values as tuples emitted_values_1D = [] + emitted_positions = [] def slot(coordinates): emitted_values_1D.append(coordinates) # Connect the signal to the custom slot crosshair.coordinatesChanged1D.connect(slot) + crosshair.crosshairChanged.connect(emitted_positions.append) - # Simulate a mouse moved event at a specific position - # Call the mouse_moved method + crosshair.mouse_moved(manual_pos=(2, 5)) + emitted_positions.clear() + emitted_values_1D.clear() crosshair.mouse_moved(manual_pos=(22, 55)) # Assert the expected behavior assert emitted_values_1D == [] + assert emitted_positions == [] + assert np.isclose(crosshair.v_line.pos().x(), 2) + assert np.isclose(crosshair.h_line.pos().y(), 5) def test_mouse_moved_signals_2D(image_widget_with_crosshair): - crosshair, plot_item = image_widget_with_crosshair - image_item = plot_item.items[0] + crosshair, _ = image_widget_with_crosshair emitted_values_2D = [] @@ -118,8 +125,11 @@ def slot(coordinates): assert emitted_values_2D == [("ImageItem", 21, 55)] -def test_mouse_moved_signals_2D_outside_clamps_to_bounds(image_widget_with_crosshair): - crosshair, _ = image_widget_with_crosshair +def test_mouse_moved_signals_2D_outside_image_bounds_clamps_inside_view_range( + image_widget_with_crosshair, +): + crosshair, plot_item = image_widget_with_crosshair + plot_item.vb.setRange(xRange=(0, 300), yRange=(0, 600), padding=0) emitted_values_2D = [] @@ -133,6 +143,26 @@ def slot(coordinates): assert emitted_values_2D == [("ImageItem", 99, 99)] +def test_mouse_moved_signals_2D_outside_view_range_ignored(image_widget_with_crosshair): + crosshair, _ = image_widget_with_crosshair + + emitted_values_2D = [] + emitted_positions = [] + + crosshair.coordinatesChanged2D.connect(emitted_values_2D.append) + crosshair.crosshairChanged.connect(emitted_positions.append) + + crosshair.mouse_moved(manual_pos=(21.0, 55.0)) + emitted_positions.clear() + emitted_values_2D.clear() + crosshair.mouse_moved(manual_pos=(220.0, 555.0)) + + assert emitted_values_2D == [] + assert emitted_positions == [] + assert np.isclose(crosshair.v_line.pos().x(), 21.0) + assert np.isclose(crosshair.h_line.pos().y(), 55.0) + + def test_marker_positions_after_mouse_move(plot_widget_with_crosshair): crosshair, _ = plot_widget_with_crosshair crosshair.mouse_moved(manual_pos=(2, 5)) @@ -176,25 +206,33 @@ def slot(position): assert np.isclose(y, 5) -def test_crosshair_clicked_signal(qtbot, plot_widget_with_crosshair): +def test_crosshair_clicked_signal(plot_widget_with_crosshair): crosshair, plot_item = plot_widget_with_crosshair emitted_positions = [] + emitted_view_positions = [] def slot(position): emitted_positions.append(position) crosshair.crosshairClicked.connect(slot) + crosshair.positionClicked.connect(emitted_view_positions.append) + + crosshair.is_log_x = True + crosshair.is_log_y = True + plot_item.vb.setRange(xRange=(0, 1), yRange=(0, 1), padding=0) - # Simulate mouse click - pos_in_scene = plot_item.vb.sceneBoundingRect().center() - expected_point = plot_item.vb.mapSceneToView(pos_in_scene) + known_view_point = QPointF(np.log10(2), np.log10(5)) + pos_in_scene = plot_item.vb.mapViewToScene(known_view_point) crosshair.mouse_clicked(_FakeMouseClickEvent(pos_in_scene)) x, y = emitted_positions[0] + view_x, view_y = emitted_view_positions[0] - assert np.isclose(x, expected_point.x()) - assert np.isclose(y, expected_point.y()) + assert np.isclose(x, 2) + assert np.isclose(y, 5) + assert np.isclose(view_x, known_view_point.x()) + assert np.isclose(view_y, known_view_point.y()) def test_update_coord_label_1D(plot_widget_with_crosshair): @@ -334,6 +372,7 @@ def test_ignore_invisible_curves_on_move(qtbot, mocked_client): c0 = wf.plot(x=[1, 2, 3], y=[1, 4, 9], name="Curve_0") c1 = wf.plot(x=[1, 2, 3], y=[2, 5, 10], name="Curve_1") wf.hook_crosshair() + wf.crosshair.plot_item.vb.setRange(xRange=(0, 4), yRange=(0, 10), padding=0) # # Simulate a mouse move at (2,5) # 1) Both curves visible: expect markers for both