mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-07-25 12:42:58 +02:00
241 lines
8.2 KiB
Python
241 lines
8.2 KiB
Python
import numpy as np
|
|
import pyqtgraph as pg
|
|
import pytest
|
|
from qtpy.QtCore import QPoint, QPointF, Qt
|
|
|
|
from bec_widgets.widgets.plots.image.bec_histogram_lut_item import (
|
|
BECColorBarItem,
|
|
BECHistogramLUTItem,
|
|
RangeDialog,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def hist_item(qtbot):
|
|
"""A BECHistogramLUTItem living inside a real scene, cleaned up after the test."""
|
|
glw = pg.GraphicsLayoutWidget()
|
|
qtbot.addWidget(glw)
|
|
qtbot.waitExposed(glw)
|
|
item = BECHistogramLUTItem()
|
|
glw.addItem(item, row=0, col=0)
|
|
item.setImageItem(pg.ImageItem(np.arange(100, dtype=float).reshape(10, 10)))
|
|
yield item
|
|
item.cleanup()
|
|
|
|
|
|
def test_menu_actions_present(hist_item):
|
|
labels = [a.text() for a in hist_item._bec_menu.actions() if a.text()]
|
|
assert labels == [
|
|
"Colormap",
|
|
"Select colormap",
|
|
"Image scaling (levels)",
|
|
"Set levels…",
|
|
"Autoscale levels",
|
|
"Histogram display range",
|
|
"Set histogram range…",
|
|
"Reset histogram range",
|
|
]
|
|
|
|
|
|
def _menu_action(hist_item: BECHistogramLUTItem, text: str):
|
|
return next(a for a in hist_item._bec_menu.actions() if a.text() == text)
|
|
|
|
|
|
def test_colormap_submenu_forwards_name(hist_item):
|
|
"""Picking a colormap in the submenu re-emits its name on the BEC signal."""
|
|
assert hist_item._colormap_menu.actions() # the pyqtgraph colormap menu is populated
|
|
received = []
|
|
hist_item.sigColorMapChangeRequested.connect(received.append)
|
|
hist_item._colormap_menu.sigColorMapTriggered.emit(pg.colormap.get("viridis"))
|
|
assert received == ["viridis"]
|
|
|
|
|
|
def test_default_viewbox_menu_bypassed(hist_item):
|
|
"""The histogram view shows the BEC menu, not pyqtgraph's default plot menu."""
|
|
vb = hist_item.vb
|
|
assert vb.getMenu(None) is hist_item._bec_menu
|
|
assert vb.getContextMenus(None) == hist_item._bec_menu.actions()
|
|
# The original ViewBoxMenu is kept alive (not replaced) so pyqtgraph internals
|
|
# such as updateViewLists() keep working.
|
|
assert vb.menu.__class__.__name__ == "ViewBoxMenu"
|
|
vb.updateViewLists() # would raise AttributeError if vb.menu were a plain QMenu
|
|
|
|
|
|
def test_raise_context_menu_pops_bec_menu(hist_item, monkeypatch):
|
|
calls = []
|
|
monkeypatch.setattr(hist_item._bec_menu, "popup", lambda pos: calls.append(pos))
|
|
|
|
class _Pos:
|
|
def toPoint(self):
|
|
return QPoint(1, 2)
|
|
|
|
class _Event:
|
|
def screenPos(self):
|
|
return _Pos()
|
|
|
|
hist_item.vb.raiseContextMenu(_Event())
|
|
assert calls == [QPoint(1, 2)]
|
|
|
|
|
|
def test_set_levels_action_emits_request(hist_item, monkeypatch):
|
|
monkeypatch.setattr(RangeDialog, "get_new_range", staticmethod(lambda *a, **k: (3.0, 9.0)))
|
|
received = []
|
|
hist_item.sigColorLevelsChangeRequested.connect(received.append)
|
|
_menu_action(hist_item, "Set levels…").trigger()
|
|
assert received == [(3.0, 9.0)]
|
|
|
|
|
|
def test_set_levels_action_cancel_emits_nothing(hist_item, monkeypatch):
|
|
monkeypatch.setattr(RangeDialog, "get_new_range", staticmethod(lambda *a, **k: None))
|
|
received = []
|
|
hist_item.sigColorLevelsChangeRequested.connect(received.append)
|
|
_menu_action(hist_item, "Set levels…").trigger()
|
|
assert received == []
|
|
|
|
|
|
def test_autoscale_levels_action_emits_request(hist_item):
|
|
received = []
|
|
hist_item.sigAutoLevelsRequested.connect(lambda: received.append(True))
|
|
_menu_action(hist_item, "Autoscale levels").trigger()
|
|
assert received == [True]
|
|
|
|
|
|
def test_set_histogram_range_action_applies_range(hist_item, monkeypatch):
|
|
monkeypatch.setattr(RangeDialog, "get_new_range", staticmethod(lambda *a, **k: (5.0, 25.0)))
|
|
recorded = []
|
|
monkeypatch.setattr(hist_item, "setHistogramRange", lambda mn, mx: recorded.append((mn, mx)))
|
|
_menu_action(hist_item, "Set histogram range…").trigger()
|
|
assert recorded == [(5.0, 25.0)]
|
|
|
|
|
|
def test_reset_histogram_range_action_reenables_histogram_autorange(hist_item):
|
|
hist_item.setHistogramRange(5.0, 25.0)
|
|
assert hist_item.vb.autoRangeEnabled()[1] is False
|
|
|
|
_menu_action(hist_item, "Reset histogram range").trigger()
|
|
|
|
assert hist_item.vb.autoRangeEnabled()[1]
|
|
|
|
|
|
def test_cleanup_restores_viewbox_and_clears_menu(hist_item):
|
|
vb = hist_item.vb
|
|
assert {"getMenu", "getContextMenus", "raiseContextMenu"} <= set(vb.__dict__)
|
|
hist_item.cleanup()
|
|
assert not ({"getMenu", "getContextMenus", "raiseContextMenu"} & set(vb.__dict__))
|
|
assert hist_item._bec_menu is None
|
|
assert hist_item._colormap_menu is None
|
|
# idempotent: cleaning up again (as the fixture teardown will) must not raise
|
|
hist_item.cleanup()
|
|
|
|
|
|
def test_range_dialog_orders_values(qtbot):
|
|
dialog = RangeDialog(None, title="t", label="l", vmin=1.0, vmax=4.0)
|
|
qtbot.addWidget(dialog)
|
|
qtbot.waitExposed(dialog)
|
|
dialog.min_spinbox.setValue(8.0)
|
|
dialog.max_spinbox.setValue(2.0)
|
|
assert dialog.get_range() == (2.0, 8.0)
|
|
|
|
|
|
def test_range_dialog_returns_fractional_values(qtbot):
|
|
"""The dialog hands fractional values through unchanged (regression context:
|
|
they used to be truncated later by the QPoint-based tuple conversion)."""
|
|
dialog = RangeDialog(None, title="t", label="l", vmin=0.25, vmax=10.5)
|
|
qtbot.addWidget(dialog)
|
|
qtbot.waitExposed(dialog)
|
|
assert dialog.get_range() == (0.25, 10.5)
|
|
|
|
|
|
def test_colormap_menu_hides_none_entry(hist_item):
|
|
none_actions = [a for a in hist_item._colormap_menu.actions() if a.text() == "None"]
|
|
assert none_actions and not none_actions[0].isVisible()
|
|
|
|
|
|
################################################################################
|
|
# BECColorBarItem (simple colorbar)
|
|
|
|
|
|
@pytest.fixture
|
|
def color_bar_item(qtbot):
|
|
"""A BECColorBarItem attached to an image inside a real scene."""
|
|
glw = pg.GraphicsLayoutWidget()
|
|
qtbot.addWidget(glw)
|
|
qtbot.waitExposed(glw)
|
|
image_item = pg.ImageItem(np.arange(100, dtype=float).reshape(10, 10))
|
|
item = BECColorBarItem()
|
|
item.setImageItem(image_item)
|
|
glw.addItem(item, row=0, col=0)
|
|
yield item
|
|
item.cleanup()
|
|
|
|
|
|
class _FakeRightClick:
|
|
def __init__(self):
|
|
self.accepted = False
|
|
|
|
def button(self):
|
|
return Qt.MouseButton.RightButton
|
|
|
|
def accept(self):
|
|
self.accepted = True
|
|
|
|
def screenPos(self):
|
|
return QPointF(3.0, 4.0)
|
|
|
|
|
|
def test_simple_colorbar_menu_actions_present(color_bar_item):
|
|
labels = [a.text() for a in color_bar_item._bec_menu.actions() if a.text()]
|
|
assert labels == [
|
|
"Colormap",
|
|
"Select colormap",
|
|
"Image scaling (levels)",
|
|
"Set levels…",
|
|
"Autoscale levels",
|
|
]
|
|
|
|
|
|
def test_simple_colorbar_native_menu_disabled(color_bar_item):
|
|
"""pyqtgraph's built-in colormap menu is off; the BEC menu replaces it."""
|
|
assert color_bar_item.colorMapMenu is False
|
|
|
|
|
|
def test_simple_colorbar_right_click_pops_bec_menu(color_bar_item, monkeypatch):
|
|
calls = []
|
|
monkeypatch.setattr(color_bar_item._bec_menu, "popup", lambda pos: calls.append(pos))
|
|
ev = _FakeRightClick()
|
|
color_bar_item.mouseClickEvent(ev)
|
|
assert ev.accepted
|
|
assert calls == [QPoint(3, 4)]
|
|
|
|
|
|
def test_simple_colorbar_set_levels_action_emits_request(color_bar_item, monkeypatch):
|
|
monkeypatch.setattr(RangeDialog, "get_new_range", staticmethod(lambda *a, **k: (3.0, 9.0)))
|
|
received = []
|
|
color_bar_item.sigColorLevelsChangeRequested.connect(received.append)
|
|
action = next(a for a in color_bar_item._bec_menu.actions() if a.text() == "Set levels…")
|
|
action.trigger()
|
|
assert received == [(3.0, 9.0)]
|
|
|
|
|
|
def test_simple_colorbar_autoscale_action_emits_request(color_bar_item):
|
|
received = []
|
|
color_bar_item.sigAutoLevelsRequested.connect(lambda: received.append(True))
|
|
action = next(a for a in color_bar_item._bec_menu.actions() if a.text() == "Autoscale levels")
|
|
action.trigger()
|
|
assert received == [True]
|
|
|
|
|
|
def test_simple_colorbar_colormap_submenu_forwards_name(color_bar_item):
|
|
received = []
|
|
color_bar_item.sigColorMapChangeRequested.connect(received.append)
|
|
color_bar_item._colormap_menu.sigColorMapTriggered.emit(pg.colormap.get("viridis"))
|
|
assert received == ["viridis"]
|
|
|
|
|
|
def test_simple_colorbar_cleanup_idempotent(color_bar_item):
|
|
color_bar_item.cleanup()
|
|
assert color_bar_item._bec_menu is None
|
|
assert color_bar_item._colormap_menu is None
|
|
# idempotent: cleaning up again (as the fixture teardown will) must not raise
|
|
color_bar_item.cleanup()
|