Skip to content

SnapshotTool: mouse cursor is not restored outside plot canvas after using the tool #58

@PierreRaybaut

Description

@PierreRaybaut

Summary

After activating the rectangular Snapshot tool and dragging a selection on the plot, the mouse cursor remains visually stuck as a cross (+) over every widget outside the plot canvas (axes, toolbar, surrounding application widgets), even though the canvas itself correctly returns to its normal cursor. The cursor only goes back to the arrow when the mouse pointer actually moves over each affected widget — and even then, on Windows, some widgets stay stuck until the next interaction.

Steps to reproduce

  1. Open a DataLab image panel (or any PlotPy application that exposes the default tool set, including the snapshot tool in its toolbar).
  2. Click the Snapshot tool (snapshot.png) in the toolbar.
  3. Click-and-drag a rectangle anywhere on the plot canvas.
  4. As soon as the mouse button is released, the Resize dialog appears. Click Cancel.
  5. Move the mouse over the axes, the toolbar, or any widget surrounding the plot canvas.

Expected: the cursor is the regular arrow (or the cursor associated with each widget).

Observed: the cursor remains a cross (+) over the axes and toolbar. The plot canvas itself shows the correct cursor. It does not require validating the export — cancelling any of the dialogs the snapshot tool opens is enough to trigger it.

Why only the snapshot tool

PlotPy ships several rectangular tools that change the canvas cursor while dragging (rectangular zoom, profile, image stats, annotations, …). None of them exhibit this bug. The snapshot tool is the only one that opens a modal dialog (ResizeDialog, then a parameter dialog, then the file dialog) directly inside its action callback (save_snapshot).

Root cause

RectangularActionTool.end_rect is called from inside the rubber-band mouseReleaseEvent handler chain. For most rectangular tools, the action callback returns immediately, the handler returns, and Qt cleanly releases the implicit pointer grab that was active during the drag.

For the snapshot tool, the action callback (save_snapshot) calls ResizeDialog.exec() (and other modal dialogs) before returning. Running a nested event loop while the outer mouseRelease handler is still on the stack leaves Qt's implicit pointer grab in an unclean state on Windows: when the dialog later closes, the cross cursor that the canvas was using during the drag remains "imprinted" on every neighbouring widget that inherits its cursor from the same logical grab — until the pointer crosses into each of them and Qt re-evaluates the cursor.

Additionally, the canvas-only setCursor(Arrow) performed by the default-tool switch (triggered by SIG_TOOL_JOB_FINISHED) was being emitted after the dialogs closed, missing the only window during which Qt could have refreshed the cursor on the surrounding widgets while the grab was still live.

Fix

The fix is fully contained in SnapshotTool — no signature or behaviour change for any other rectangular tool. SnapshotTool.end_rect overrides the base implementation so that:

  1. Synchronously (still inside the mouseRelease handler chain):
    • SIG_TOOL_JOB_FINISHED is emitted.
    • set_active_item is called.
    • This causes the default-tool switch and its associated canvas.setCursor(Arrow) to happen while Qt's pointer grab is still live, allowing the cursor to be refreshed on neighbouring widgets.
  2. Deferred via QTimer.singleShot(0, ...):
    • action_func(plot, p0, p1) (= save_snapshot) is invoked.
    • The mouse-release handler returns first, Qt cleanly releases the implicit grab, and only then are the modal dialogs opened.

The base RectangularActionTool.end_rect and all other rectangular tools remain strictly unchanged.

Behavioural note

The order in which SnapshotTool emits SIG_TOOL_JOB_FINISHED is now before save_snapshot runs (instead of after). This is consistent with the deferred execution model and matches user expectations (the tool's "job" is signalled as finished as soon as the rectangular selection ends, not after the user has interacted with all subsequent dialogs). No internal PlotPy code relies on the previous ordering.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions