Compare commits

..

15 Commits

Author SHA1 Message Date
wyzula_j e747432096 fix(crosshair): human adjustments to copilot PR 2026-05-28 11:38:59 +02:00
copilot-swe-agent[bot] 641c2b3481 test(crosshair): clarify clamping expectation in crosshair 2D test 2026-05-28 11:38:37 +02:00
copilot-swe-agent[bot] eee86bdfa9 fix(crosshair): harden crosshair click scene position handling 2026-05-28 11:38:20 +02:00
copilot-swe-agent[bot] 28546f4dfd build: add pyqtgraph 0.14 compatibility updates 2026-05-28 11:38:06 +02:00
semantic-release c9fc0a82b9 3.13.3
Automatically generated by python-semantic-release
2026-05-22 12:30:17 +00:00
wakonig_k 668b1bd9cd fix(tests): rename description attribute to _description in FakeDevice 2026-05-22 14:29:28 +02:00
semantic-release 1a6c8bf30f 3.13.2
Automatically generated by python-semantic-release
2026-05-22 08:57:04 +00:00
wakonig_k c346bd0f18 fix(tests): rename description attribute to _description in FakePositioner 2026-05-22 10:56:05 +02:00
semantic-release 5f86e41a03 3.13.1
Automatically generated by python-semantic-release
2026-05-21 14:41:40 +00:00
wakonig_k f7a48b5f6a fix(gui): replace window.show() with window.raise_window() and add hide() method 2026-05-21 16:40:51 +02:00
wakonig_k b4beb274da fix: use .show instead of .start 2026-05-21 16:40:51 +02:00
semantic-release 80694d151f 3.13.0
Automatically generated by python-semantic-release
2026-05-21 14:20:49 +00:00
wakonig_k f03a5d9e85 feat(rpc-base): set default RPC timeout and allow customization 2026-05-21 16:19:48 +02:00
semantic-release 5e8f0e8083 3.12.2
Automatically generated by python-semantic-release
2026-05-21 13:29:11 +00:00
wyzula_j 9eb05416ab fix(toggle): disable styling implemented 2026-05-21 15:28:17 +02:00
13 changed files with 242 additions and 97 deletions
+43
View File
@@ -1,6 +1,49 @@
# CHANGELOG
## v3.13.3 (2026-05-22)
### Bug Fixes
- **tests**: Rename description attribute to _description in FakeDevice
([`668b1bd`](https://github.com/bec-project/bec_widgets/commit/668b1bd9cd158fc12cff2c340d7317f30a212121))
## v3.13.2 (2026-05-22)
### Bug Fixes
- **tests**: Rename description attribute to _description in FakePositioner
([`c346bd0`](https://github.com/bec-project/bec_widgets/commit/c346bd0f18ce873ff5ca6c59150c9581c9edca8d))
## v3.13.1 (2026-05-21)
### Bug Fixes
- Use .show instead of .start
([`b4beb27`](https://github.com/bec-project/bec_widgets/commit/b4beb274da745da618f9b37ec241cd0109c088f1))
- **gui**: Replace window.show() with window.raise_window() and add hide() method
([`f7a48b5`](https://github.com/bec-project/bec_widgets/commit/f7a48b5f6a51d391dca26ca42d03bad4f278ff22))
## v3.13.0 (2026-05-21)
### Features
- **rpc-base**: Set default RPC timeout and allow customization
([`f03a5d9`](https://github.com/bec-project/bec_widgets/commit/f03a5d9e853bd62b8ec1bad1c1e112fe01befe70))
## v3.12.2 (2026-05-21)
### Bug Fixes
- **toggle**: Disable styling implemented
([`9eb0541`](https://github.com/bec-project/bec_widgets/commit/9eb05416ab68dcb88732dca8974c665030d34e0b))
## v3.12.1 (2026-05-21)
### Bug Fixes
+14 -3
View File
@@ -222,6 +222,7 @@ class BECGuiClient(RPCBase):
self._ipython_registry: dict[str, RPCReference] = {}
self.available_widgets = AvailableWidgetsNamespace()
register_serializer_extension()
self._rpc_timeout = 5
####################
#### Client API ####
@@ -232,6 +233,16 @@ class BECGuiClient(RPCBase):
"""The launcher object."""
return RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self, object_name="launcher")
def set_rpc_timeout(self, timeout: float):
"""Set the timeout for RPC calls to the GUI server.
Args:
timeout(float): The timeout in seconds.
"""
if not isinstance(timeout, (int, float)) or timeout < 0:
raise ValueError("Timeout must be a non-negative number.")
self._rpc_timeout = timeout
def _safe_register_stream(self, endpoint: EndpointInfo, cb: Callable, **kwargs):
"""Check if already registered for registration in idempotent functions."""
if not self._client.connector.any_stream_is_registered(endpoint, cb=cb):
@@ -358,7 +369,7 @@ class BECGuiClient(RPCBase):
)
if not self._check_if_server_is_alive():
self.start(wait=True)
self.show(wait=True)
if wait:
with wait_for_server(self):
return self._new_impl(
@@ -550,7 +561,7 @@ class BECGuiClient(RPCBase):
if self.launcher and len(self._top_level) == 0:
self.launcher._run_rpc("show") # pylint: disable=protected-access
for window in self._top_level.values():
window.show()
window.raise_window()
def _show_all(self):
with wait_for_server(self):
@@ -569,7 +580,7 @@ class BECGuiClient(RPCBase):
if self.launcher and len(self._top_level) == 0:
self.launcher._run_rpc("raise") # pylint: disable=protected-access
for window in self._top_level.values():
window._run_rpc("raise") # type: ignore[attr-defined]
window.raise_window()
def _raise_all(self):
with wait_for_server(self):
+13 -3
View File
@@ -24,6 +24,8 @@ else:
# pylint: disable=protected-access
_DEFAULT_RPC_TIMEOUT = object()
def _name_arg(arg):
if isinstance(arg, DeviceBaseWithConfig):
@@ -154,6 +156,7 @@ class RPCReference:
class RPCBase:
def __init__(
self,
gui_id: str | None = None,
@@ -207,12 +210,16 @@ class RPCBase:
# Use explicit call to ensure action name is 'raise' (not 'raise_')
return self._run_rpc("raise")
def hide(self):
"""Hide this widget (or its container)."""
return self._run_rpc("hide")
def _run_rpc(
self,
method,
*args,
wait_for_rpc_response=True,
timeout=5,
wait_for_rpc_response: bool = True,
timeout: float | None | object = _DEFAULT_RPC_TIMEOUT,
gui_id: str | None = None,
**kwargs,
) -> Any:
@@ -223,13 +230,16 @@ class RPCBase:
method: The method to call.
args: The arguments to pass to the method.
wait_for_rpc_response: Whether to wait for the RPC response.
timeout: The timeout for the RPC response.
timeout: The timeout for the RPC response. If omitted, the client's default RPC
timeout is used. If explicitly set to None, wait indefinitely.
gui_id: The GUI ID to use for the RPC call. If None, the default GUI ID is used.
kwargs: The keyword arguments to pass to the method.
Returns:
The result of the RPC call.
"""
if timeout is _DEFAULT_RPC_TIMEOUT:
timeout = self._root._rpc_timeout
if method in ["show", "hide", "raise"] and gui_id is None:
obj = self._root._server_registry.get(self._gui_id)
if obj is None:
+4 -4
View File
@@ -15,7 +15,7 @@ class FakeDevice(BECDevice):
super().__init__(name=name)
self._enabled = enabled
self.signals = {self.name: {"value": 1.0}}
self.description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
self._description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
self._readout_priority = readout_priority
self._config = {
"readoutPriority": "baseline",
@@ -74,7 +74,7 @@ class FakeDevice(BECDevice):
Returns:
dict: Description of the device
"""
return self.description
return self._description
class FakePositioner(BECPositioner):
@@ -96,7 +96,7 @@ class FakePositioner(BECPositioner):
self._limits = limits
self._readout_priority = readout_priority
self.signals = {self.name: {"value": 1.0}}
self.description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
self._description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
self._config = {
"readoutPriority": "baseline",
"deviceClass": "ophyd_devices.SimPositioner",
@@ -176,7 +176,7 @@ class FakePositioner(BECPositioner):
Returns:
dict: Description of the device
"""
return self.description
return self._description
@property
def precision(self):
+12 -4
View File
@@ -429,10 +429,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 crosshair visuals
self.v_line.setPos(x)
@@ -493,8 +493,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()
scaled_x, scaled_y = self.scale_emitted_coordinates(mouse_point.x(), mouse_point.y())
self.crosshairClicked.emit((scaled_x, scaled_y))
@@ -545,6 +549,10 @@ class Crosshair(QObject):
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]:
+35 -9
View File
@@ -1,6 +1,6 @@
import sys
from qtpy.QtCore import Property, QEasingCurve, QPointF, QPropertyAnimation, Qt, Signal
from qtpy.QtCore import Property, QEasingCurve, QEvent, QPointF, QPropertyAnimation, Qt, Signal
from qtpy.QtGui import QColor, QPainter
from qtpy.QtWidgets import QApplication, QWidget
@@ -41,10 +41,22 @@ class ToggleSwitch(QWidget):
theme = getattr(QApplication.instance(), "theme", None)
colors = theme.colors if theme else {}
self._active_track_color = colors.get("PRIMARY", QColor(33, 150, 243))
self._active_thumb_color = colors.get("ON_PRIMARY", QColor(255, 255, 255))
self._inactive_track_color = colors.get("SEPARATOR", QColor(200, 200, 200))
self._inactive_thumb_color = colors.get("ON_PRIMARY", QColor(255, 255, 255))
self._active_track_color = self._theme_color(colors, "PRIMARY", QColor(33, 150, 243))
self._active_thumb_color = self._theme_color(colors, "ON_PRIMARY", QColor(255, 255, 255))
self._inactive_track_color = self._theme_color(colors, "SEPARATOR", QColor(200, 200, 200))
self._inactive_thumb_color = self._theme_color(colors, "ON_PRIMARY", QColor(255, 255, 255))
self._disabled_track_color = self._theme_color(colors, "DISABLED_BG", QColor(220, 220, 220))
self._disabled_thumb_color = self._theme_color(colors, "DISABLED_FG", QColor(150, 150, 150))
self._disabled_border_color = self._theme_color(
colors, "DISABLED_BORDER", QColor(170, 170, 170)
)
if hasattr(self, "_checked"):
self.update_colors()
@staticmethod
def _theme_color(colors: dict, key: str, fallback: QColor) -> QColor:
color = colors.get(key, fallback)
return color if isinstance(color, QColor) else QColor(color)
@Property(bool)
def checked(self):
@@ -119,29 +131,40 @@ class ToggleSwitch(QWidget):
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# Draw track
painter.setBrush(self._track_color)
painter.setPen(Qt.NoPen)
painter.setPen(self._disabled_border_color if not self.isEnabled() else Qt.PenStyle.NoPen)
painter.drawRoundedRect(
0, 0, self.width(), self.height(), self.height() / 2, self.height() / 2
)
# Draw thumb
painter.setBrush(self._thumb_color)
painter.setPen(Qt.PenStyle.NoPen)
diameter = int(self.height() * 0.8)
painter.drawEllipse(int(self._thumb_pos.x()), int(self._thumb_pos.y()), diameter, diameter)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
if self.isEnabled() and event.button() == Qt.MouseButton.LeftButton:
self.checked = not self.checked
def update_colors(self):
if not self.isEnabled():
self._thumb_color = self._disabled_thumb_color
self._track_color = self._disabled_track_color
return
self._thumb_color = self.active_thumb_color if self._checked else self.inactive_thumb_color
self._track_color = self.active_track_color if self._checked else self.inactive_track_color
def changeEvent(self, event):
if event.type() == QEvent.Type.EnabledChange:
self.update_colors()
self.update()
super().changeEvent(event)
def get_thumb_pos(self, checked):
return QPointF(self.width() - self.height() + 3, 2) if checked else QPointF(3, 2)
@@ -167,7 +190,7 @@ class ToggleSwitch(QWidget):
if __name__ == "__main__": # pragma: no cover
from qtpy.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from qtpy.QtWidgets import QHBoxLayout, QWidget
from bec_widgets.utils.colors import apply_theme
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
@@ -177,9 +200,12 @@ if __name__ == "__main__": # pragma: no cover
widget = QWidget()
layout = QHBoxLayout(widget)
toggle = ToggleSwitch()
toggle_disabled = ToggleSwitch()
dark_mode_btn = DarkModeButton()
layout.addWidget(toggle)
layout.addWidget(toggle_disabled)
layout.addWidget(dark_mode_btn)
toggle_disabled.setEnabled(False)
window = QWidget()
window.setLayout(layout)
window.show()
+7 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "bec_widgets"
version = "3.12.1"
version = "3.13.3"
description = "BEC Widgets"
requires-python = ">=3.11"
classifiers = [
@@ -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",
@@ -66,6 +66,11 @@ qtermwidget = ["pyside6_qtermwidget"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
+1 -1
View File
@@ -45,7 +45,7 @@ def connected_client_gui_obj(qtbot, gui_id, bec_client_lib):
"""
gui = BECGuiClient(gui_id=gui_id)
try:
gui.start(wait=True)
gui.show(wait=True)
qtbot.waitUntil(lambda: hasattr(gui, "bec"), timeout=5000)
gui.bec.delete_all() # ensure clean state
qtbot.waitUntil(lambda: len(gui.bec.widget_list()) == 0, timeout=10000)
+4 -4
View File
@@ -143,11 +143,11 @@ def test_rpc_gui_obj(connected_client_gui_obj: BECGuiClient, qtbot):
qtbot.wait(500)
gui.kill_server()
assert not gui._gui_is_alive()
gui.start(wait=True)
gui.show(wait=True)
assert gui._gui_is_alive()
# calling start multiple times should not change anything
gui.start(wait=True)
gui.start(wait=True)
# calling show multiple times should not change anything
gui.show(wait=True)
gui.show(wait=True)
def wait_for_gui_started():
return "bec" in gui.windows
+1 -1
View File
@@ -75,7 +75,7 @@ def connected_client_gui_obj(qtbot_scope_module, gui_id, bec_client_lib):
"""
gui = BECGuiClient(gui_id=gui_id)
try:
gui.start(wait=True)
gui.show(wait=True)
qtbot_scope_module.waitUntil(lambda: hasattr(gui, "bec"), timeout=5000)
gui.bec.delete_all() # ensure clean state
qtbot_scope_module.waitUntil(lambda: len(gui.bec.widget_list()) == 0, timeout=10000)
+10 -1
View File
@@ -5,6 +5,7 @@ import pytest
from bec_widgets.cli.client import BECDockArea
from bec_widgets.cli.client_utils import BECGuiClient, _start_plot_process
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCResponseTimeoutError, rpc_timeout
@pytest.fixture
@@ -220,7 +221,7 @@ def test_client_utils_new_starts_server_when_not_alive():
with mock.patch("bec_widgets.cli.client_utils.wait_for_server", _no_wait_for_server):
with (
mock.patch.object(gui, "_check_if_server_is_alive", return_value=False),
mock.patch.object(gui, "start") as mock_start,
mock.patch.object(gui, "show") as mock_start,
):
gui.new(wait=False, startup_profile=None)
@@ -257,3 +258,11 @@ def test_client_utils_delete_falls_back_to_direct_close():
gui.delete("dock")
widget._run_rpc.assert_called_once_with("close")
def test_client_utils_gui_client_set_rpc_timeout():
gui = BECGuiClient()
assert gui._rpc_timeout == 5
gui.set_rpc_timeout(10)
assert gui._rpc_timeout == 10
+75 -65
View File
@@ -14,6 +14,18 @@ from .conftest import create_widget
# 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()
@@ -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
@@ -38,20 +51,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 +69,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 +78,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 +120,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 +138,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 +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 = []
@@ -181,11 +198,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 +206,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(_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):
@@ -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"}
+23
View File
@@ -36,3 +36,26 @@ def test_toggle_click(qtbot, toggle):
qtbot.mouseClick(toggle, Qt.LeftButton)
toggle.paintEvent(None)
assert toggle.checked is not init_state
def test_toggle_disabled_state_blocks_clicks_and_restores_colors(qtbot, toggle):
toggle.checked = True
assert toggle._track_color == toggle.active_track_color
assert toggle._thumb_color == toggle.active_thumb_color
toggle.setEnabled(False)
assert toggle._track_color == toggle._disabled_track_color
assert toggle._thumb_color == toggle._disabled_thumb_color
qtbot.mouseClick(toggle, Qt.LeftButton)
assert toggle.checked is True
assert toggle._track_color == toggle._disabled_track_color
assert toggle._thumb_color == toggle._disabled_thumb_color
toggle.setEnabled(True)
assert toggle.checked is True
assert toggle._track_color == toggle.active_track_color
assert toggle._thumb_color == toggle.active_thumb_color