mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-07-25 12:42:58 +02:00
build: support PySide6 6.11.1, pyqtgraph 0.14 and PySide6-QtAds 5.0.0, remove force x11 on Linux
This commit is contained in:
@@ -1,13 +1,5 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bec_widgets.widgets.containers.qt_ads as QtAds
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
qt_platform = os.environ.get("QT_QPA_PLATFORM", "")
|
||||
if qt_platform != "offscreen":
|
||||
os.environ["QT_QPA_PLATFORM"] = "xcb"
|
||||
|
||||
# Default QtAds configuration
|
||||
QtAds.CDockManager.setConfigFlag(QtAds.CDockManager.eConfigFlag.FocusHighlighting, True)
|
||||
QtAds.CDockManager.setConfigFlag(
|
||||
|
||||
@@ -408,7 +408,7 @@ class BECWidget(BECConnector):
|
||||
"""Wrap the close even to ensure the rpc_register is cleaned up."""
|
||||
try:
|
||||
if not self._destroyed:
|
||||
self.cleanup()
|
||||
self._destroyed = True
|
||||
self.cleanup()
|
||||
finally:
|
||||
super().closeEvent(event) # pylint: disable=no-member
|
||||
|
||||
@@ -529,10 +529,10 @@ class Crosshair(QObject):
|
||||
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)
|
||||
@@ -594,8 +594,12 @@ class Crosshair(QObject):
|
||||
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()
|
||||
# Keep the raw click position; ``x``/``y`` are reused/snapped below.
|
||||
pin_x, pin_y = mouse_point.x(), mouse_point.y()
|
||||
@@ -652,7 +656,7 @@ class Crosshair(QObject):
|
||||
# removes it, otherwise (re)place a single pin at the clicked point.
|
||||
if event.double():
|
||||
self.clear_pin()
|
||||
elif self._pin_hit(event._scenePos):
|
||||
elif self._pin_hit(scene_pos):
|
||||
self.clear_pin()
|
||||
else:
|
||||
self.set_pin(pin_x, pin_y, x_snap_values, y_snap_values)
|
||||
@@ -853,6 +857,10 @@ class Crosshair(QObject):
|
||||
delta = pin_scene - scene_pos
|
||||
return (delta.x() ** 2 + delta.y() ** 2) ** 0.5 <= self._pin_hit_radius_px
|
||||
|
||||
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]:
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import bec_qthemes
|
||||
from bec_qthemes.qss_editor.qss_editor import (
|
||||
THEMES_PATH,
|
||||
build_palette_from_mapping,
|
||||
read_theme_xml,
|
||||
)
|
||||
|
||||
|
||||
def pretty_display_theme(theme: str = "dark"):
|
||||
palette = bec_qthemes.load_palette(theme)
|
||||
_, mapping = read_theme_xml(THEMES_PATH / f"{theme}.xml")
|
||||
palette = build_palette_from_mapping(mapping)
|
||||
foreground = palette.text().color().name()
|
||||
background = palette.base().color().name()
|
||||
border = palette.shadow().color().name()
|
||||
accent = palette.accent().color().name()
|
||||
# palette.highlight() rather than accent(): on Qt 6.10+ accent() returns the
|
||||
# platform accent, and bec_qthemes palettes only set Highlight anyway.
|
||||
accent = palette.highlight().color().name()
|
||||
return f"""
|
||||
QWidget {{color: {foreground}; background-color: {background}}}
|
||||
QLabel {{ font-weight: bold; }}
|
||||
|
||||
@@ -1465,6 +1465,15 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
for dock in self.dock_list():
|
||||
self._delete_dock(dock)
|
||||
|
||||
def cleanup(self):
|
||||
"""Tear down all docks via the Qt ADS API before the base BECWidget cleanup runs.
|
||||
|
||||
Explicitly releasing dock widgets through the CDockManager API first prevents its
|
||||
destructor from interacting badly with dock widgets that are deleted outside of it.
|
||||
"""
|
||||
self.delete_all()
|
||||
super().cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
@@ -144,7 +144,12 @@ class BecConsoleRegistry:
|
||||
return None
|
||||
|
||||
window = console.window()
|
||||
if window is not None and window is not console and self._is_valid_qobject(window):
|
||||
if (
|
||||
window is not None
|
||||
and window is not console
|
||||
and self._is_valid_qobject(window)
|
||||
and not getattr(window, "_destroyed", False)
|
||||
):
|
||||
return window
|
||||
|
||||
if not avoid_console:
|
||||
|
||||
@@ -11,7 +11,7 @@ from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
|
||||
from bec_qthemes import material_icon
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from qtpy.QtCore import QObject, Qt, QThread, QTimer, Signal
|
||||
from qtpy.QtCore import QObject, QRectF, Qt, QThread, QTimer, Signal
|
||||
from qtpy.QtGui import QTransform
|
||||
from qtpy.QtWidgets import QDialog, QPushButton, QVBoxLayout
|
||||
from toolz import partition
|
||||
@@ -93,6 +93,30 @@ class HeatmapConfig(ConnectionConfig):
|
||||
_validate_color_palette = field_validator("color_map")(Colors.validate_color_map)
|
||||
|
||||
|
||||
class _TextOnlyLegendSample(pg.graphicsItems.LegendItem.ItemSample):
|
||||
"""Zero-size legend sample for text-only rows in the config label.
|
||||
|
||||
The stock ItemSample expects a plottable item with an ``opts`` dict; since
|
||||
PySide 6.10 an exception raised inside its paint() override propagates out
|
||||
of the C++ paint loop and crashes the application, so the config label rows
|
||||
must not carry a real sample item.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(item=None)
|
||||
self.setFixedWidth(0)
|
||||
self.setFixedHeight(0)
|
||||
|
||||
def boundingRect(self):
|
||||
return QRectF(0, 0, 0, 0)
|
||||
|
||||
def paint(self, p, *args):
|
||||
pass
|
||||
|
||||
def mouseClickEvent(self, event):
|
||||
event.ignore()
|
||||
|
||||
|
||||
@dataclass
|
||||
class _InterpolationRequest:
|
||||
"""Immutable payload describing an interpolation request for the worker thread.
|
||||
@@ -977,14 +1001,14 @@ class Heatmap(ImageBase):
|
||||
self.config_label.clear()
|
||||
# Indicate whether the widget follows the live acquisition or is pinned to a history scan
|
||||
mode = "history" if self._history_scan_id is not None else "live"
|
||||
self.config_label.addItem(self.plot_item, f"Scan: {scan_msg.scan_number} ({mode})")
|
||||
self.config_label.addItem(self.plot_item, f"Scan Name: {scan_msg.scan_name}")
|
||||
self.config_label.addItem(_TextOnlyLegendSample(), f"Scan: {scan_msg.scan_number} ({mode})")
|
||||
self.config_label.addItem(_TextOnlyLegendSample(), f"Scan Name: {scan_msg.scan_name}")
|
||||
if scan_msg.scan_name != "grid_scan" or self._image_config.enforce_interpolation:
|
||||
self.config_label.addItem(
|
||||
self.plot_item, f"Interpolation: {self._image_config.interpolation}"
|
||||
_TextOnlyLegendSample(), f"Interpolation: {self._image_config.interpolation}"
|
||||
)
|
||||
self.config_label.addItem(
|
||||
self.plot_item, f"Oversampling: {self._image_config.oversampling_factor}x"
|
||||
_TextOnlyLegendSample(), f"Oversampling: {self._image_config.oversampling_factor}x"
|
||||
)
|
||||
|
||||
def get_image_data(
|
||||
|
||||
@@ -58,7 +58,9 @@ class SpinnerWidget(QWidget):
|
||||
|
||||
color_palette = get_theme_palette()
|
||||
|
||||
color = QColor(color_palette.accent().color())
|
||||
# Use the theme highlight color; palette.accent() resolved to it before Qt 6.10,
|
||||
# but now falls back to the platform accent since the theme does not set the role.
|
||||
color = QColor(color_palette.highlight().color())
|
||||
|
||||
rect.adjust(line_width, line_width, -line_width, -line_width)
|
||||
|
||||
|
||||
+3
-3
@@ -10,8 +10,8 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"PyJWT~=2.9",
|
||||
"PySide6==6.9.0",
|
||||
"PySide6-QtAds==4.4.0",
|
||||
"PySide6==6.11.1",
|
||||
"PySide6-QtAds==5.0.0",
|
||||
"bec_ipython_client~=3.134", # needed for jupyter console
|
||||
"bec_lib~=3.134",
|
||||
"bec_qthemes~=1.0, >=1.3.4",
|
||||
@@ -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",
|
||||
"python-slugify~=8.0",
|
||||
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
||||
"qtmonaco~=0.8, >=0.8.1",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -68,6 +68,10 @@ def qapplication(qtbot, request, testable_qtimer_class): # pylint: disable=unus
|
||||
# stop pyepics dispatcher for leaking tests
|
||||
|
||||
_dispatcher.stop()
|
||||
from bec_widgets.widgets.editors.bec_console.bec_console import _bec_console_registry
|
||||
|
||||
_bec_console_registry.clear()
|
||||
process_all_deferred_deletes(qapp)
|
||||
if request.node.stash._storage.get("failed"):
|
||||
print("Test failed, skipping cleanup checks")
|
||||
return
|
||||
|
||||
@@ -14,6 +14,33 @@ from .conftest import create_widget
|
||||
# pylint: disable = redefined-outer-name
|
||||
|
||||
|
||||
class _FakeClickEvent:
|
||||
"""Minimal public-API stand-in for a pyqtgraph MouseClickEvent."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scene_pos: QPointF,
|
||||
button: Qt.MouseButton = Qt.MouseButton.LeftButton,
|
||||
double: bool = False,
|
||||
):
|
||||
self._scene_pos = scene_pos
|
||||
self._button = button
|
||||
self._double = double
|
||||
self.accepted = False
|
||||
|
||||
def button(self):
|
||||
return self._button
|
||||
|
||||
def scenePos(self):
|
||||
return self._scene_pos
|
||||
|
||||
def double(self):
|
||||
return self._double
|
||||
|
||||
def accept(self):
|
||||
self.accepted = True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def plot_widget_with_crosshair(qtbot):
|
||||
widget = pg.PlotWidget()
|
||||
@@ -22,6 +49,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
|
||||
@@ -38,20 +66,17 @@ 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)
|
||||
@@ -59,7 +84,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,43 +93,40 @@ def test_mouse_moved_signals(plot_widget_with_crosshair):
|
||||
|
||||
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 = []
|
||||
|
||||
@@ -113,17 +135,16 @@ def test_mouse_moved_signals_2D(image_widget_with_crosshair):
|
||||
|
||||
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 = []
|
||||
|
||||
@@ -132,23 +153,34 @@ def test_mouse_moved_signals_2D_outside(image_widget_with_crosshair):
|
||||
|
||||
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)]
|
||||
|
||||
|
||||
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_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 +204,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 +213,7 @@ def test_crosshair_changed_signal(plot_widget_with_crosshair):
|
||||
|
||||
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]
|
||||
|
||||
@@ -193,33 +221,33 @@ def test_crosshair_changed_signal(plot_widget_with_crosshair):
|
||||
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
|
||||
crosshair.is_log_x = True
|
||||
crosshair.is_log_y = True
|
||||
plot_item.vb.setRange(xRange=(0, 1), yRange=(0, 1), padding=0)
|
||||
|
||||
# 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)
|
||||
known_view_point = QPointF(np.log10(2), np.log10(5))
|
||||
pos_in_scene = plot_item.vb.mapViewToScene(known_view_point)
|
||||
crosshair.mouse_clicked(_FakeClickEvent(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):
|
||||
@@ -372,21 +400,18 @@ 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"}
|
||||
|
||||
@@ -396,25 +421,6 @@ def test_ignore_invisible_curves_on_move(qtbot, mocked_client):
|
||||
###############################################
|
||||
|
||||
|
||||
class _FakeClickEvent:
|
||||
"""Minimal stand-in for a pyqtgraph MouseClickEvent for routing tests."""
|
||||
|
||||
def __init__(self, scene_pos, button=Qt.LeftButton, double=False):
|
||||
self._scenePos = scene_pos
|
||||
self._button = button
|
||||
self._double = double
|
||||
self.accepted = False
|
||||
|
||||
def button(self):
|
||||
return self._button
|
||||
|
||||
def double(self):
|
||||
return self._double
|
||||
|
||||
def accept(self):
|
||||
self.accepted = True
|
||||
|
||||
|
||||
def test_set_pin_1d_creates_marker_and_emits(plot_widget_with_crosshair):
|
||||
crosshair, _ = plot_widget_with_crosshair
|
||||
pinned = []
|
||||
|
||||
@@ -531,6 +531,7 @@ class TestDeviceManagerView:
|
||||
# Assign the mocked client
|
||||
widget.device_manager_widget.client = mocked_client
|
||||
qtbot.addWidget(widget)
|
||||
widget.show()
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from bec_widgets.widgets.plots.heatmap.heatmap import (
|
||||
HeatmapDeviceSignal,
|
||||
_InterpolationRequest,
|
||||
_StepInterpolationWorker,
|
||||
_TextOnlyLegendSample,
|
||||
)
|
||||
|
||||
# pytest: disable=unused-import
|
||||
@@ -1055,6 +1056,27 @@ def test_heatmap_config_label_shows_live_or_history(heatmap_widget):
|
||||
assert "Scan: 5 (history)" in labels
|
||||
|
||||
|
||||
def test_heatmap_config_label_paints_without_error(heatmap_widget):
|
||||
"""The config label rows must survive a real paint pass.
|
||||
|
||||
A stock ItemSample around a PlotItem raises in paint(); since PySide 6.10
|
||||
override exceptions propagate out of the C++ paint loop and segfault.
|
||||
"""
|
||||
scan_msg = mock.MagicMock()
|
||||
scan_msg.scan_number = 5
|
||||
scan_msg.scan_name = "line_scan"
|
||||
heatmap_widget.status_message = scan_msg
|
||||
heatmap_widget._image_config.show_config_label = True
|
||||
heatmap_widget.redraw_config_label()
|
||||
|
||||
samples = [sample for sample, _ in heatmap_widget.config_label.items]
|
||||
assert samples
|
||||
assert all(isinstance(sample, _TextOnlyLegendSample) for sample in samples)
|
||||
|
||||
pixmap = heatmap_widget.grab()
|
||||
assert not pixmap.isNull()
|
||||
|
||||
|
||||
def test_heatmap_settings_scan_index_syncs_with_widget(heatmap_widget, qtbot, scan_history_factory):
|
||||
"""
|
||||
The scan index combobox in the settings dialog mirrors the heatmap state:
|
||||
|
||||
@@ -1214,6 +1214,9 @@ def test_crosshair_moves_update_profiles_immediately(qtbot, mocked_client):
|
||||
switch = bec_image_view.toolbar.components.get_action("image_switch_crosshair")
|
||||
switch.actions["crosshair_roi"].action.trigger()
|
||||
qtbot.wait(50)
|
||||
bec_image_view.crosshair.plot_item.vb.setRange(
|
||||
xRange=(0, test_data.shape[0]), yRange=(0, test_data.shape[1]), padding=0
|
||||
)
|
||||
|
||||
# Position the crosshair lines and emit the snapped pixel (as mouse_moved does).
|
||||
bec_image_view.crosshair.mouse_moved(manual_pos=(1.2, 2.2))
|
||||
@@ -1233,6 +1236,9 @@ def test_live_label_intensity_updates_on_image_update(qtbot, mocked_client):
|
||||
test_data = np.arange(25, dtype=float).reshape(5, 5)
|
||||
bec_image_view.on_image_update_2d({"data": test_data}, {})
|
||||
bec_image_view.hook_crosshair()
|
||||
bec_image_view.crosshair.plot_item.vb.setRange(
|
||||
xRange=(0, test_data.shape[0]), yRange=(0, test_data.shape[1]), padding=0
|
||||
)
|
||||
|
||||
bec_image_view.crosshair.mouse_moved(manual_pos=(2.5, 3.5))
|
||||
assert "Intensity: 13.000" in bec_image_view.crosshair.coord_label.toPlainText()
|
||||
|
||||
@@ -80,7 +80,7 @@ def test_export_import_config(example_widget):
|
||||
"value": 10,
|
||||
},
|
||||
"QTableWidget ()": {
|
||||
"QAbstractButton ()": {},
|
||||
"QAbstractButton (qt_tableview_cornerbutton)": {},
|
||||
"QAbstractTableModel ()": {},
|
||||
"QHeaderView ()": {
|
||||
"QItemSelectionModel ()": {},
|
||||
|
||||
Reference in New Issue
Block a user