Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions bec_widgets/utils/crosshair.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -493,8 +493,12 @@ 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_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()
scaled_x, scaled_y = self.scale_emitted_coordinates(mouse_point.x(), mouse_point.y())
self.crosshairClicked.emit((scaled_x, scaled_y))
Expand Down Expand Up @@ -545,6 +549,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]:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
140 changes: 75 additions & 65 deletions tests/unit_tests/test_crosshair.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@
# pylint: disable = redefined-outer-name


class _FakeMouseClickEvent:
def __init__(self, scene_pos: QPointF, button: Qt.MouseButton = Qt.MouseButton.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()
Expand All @@ -22,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
Expand All @@ -38,28 +51,25 @@ 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


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)
assert np.isclose(crosshair.h_line.pos().y(), 5)


def test_mouse_moved_signals(plot_widget_with_crosshair):
crosshair, plot_item = plot_widget_with_crosshair
crosshair, _ = plot_widget_with_crosshair

emitted_values_1D = []

Expand All @@ -68,43 +78,40 @@ 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 = []
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
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=(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 = []

Expand All @@ -113,17 +120,16 @@ 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):
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 = []

Expand All @@ -132,23 +138,34 @@ 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 == [("ImageItem", 99, 99)]
Comment thread
wyzula-jan marked this conversation as resolved.

assert emitted_values_2D == []

def test_mouse_moved_signals_2D_outside_view_range_ignored(image_widget_with_crosshair):
crosshair, _ = image_widget_with_crosshair

def test_marker_positions_after_mouse_move(plot_widget_with_crosshair):
crosshair, plot_item = plot_widget_with_crosshair
emitted_values_2D = []
emitted_positions = []

crosshair.coordinatesChanged2D.connect(emitted_values_2D.append)
crosshair.crosshairChanged.connect(emitted_positions.append)

pos_in_view = QPointF(2, 5)
pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view)
event_mock = [pos_in_scene]
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))

crosshair.mouse_moved(event_mock)
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))

marker = crosshair.marker_moved_1d["Curve 1"]
marker_x, marker_y = marker.getData()
Expand All @@ -172,7 +189,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 = []

Expand All @@ -181,45 +198,41 @@ 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]

assert np.isclose(x, 2)
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)

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)
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
qtbot.mouseClick(graphics_view.viewport(), Qt.LeftButton, pos=pos_in_widget)
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(round(x, 1), 2)
assert np.isclose(round(y, 1), 5)
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):
Expand Down Expand Up @@ -359,20 +372,17 @@ 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)
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"}
Loading