From 19c1d633cc95fb6eaa74623298cee5eee20a9cbe Mon Sep 17 00:00:00 2001 From: ExPikaPaka Date: Thu, 8 Oct 2026 09:04:01 +0200 Subject: [PATCH] UV editor: keep the mouse capture balanced The canvas captured the mouse on every button press without checking whether it already held one, released it in a single place, and handled no capture loss at all. Two sequences leaked a capture: pressing a second button during a drag nested a second one that the single release on button-up could not undo, and a modal R/S skipped that release entirely while waiting for a confirming click that may never come. A leaked capture is not a local problem on macOS, where wxEVT_MOUSE_CAPTURE_LOST is never sent and nothing recovers it. While any wx window holds a capture, wxOSX routes every mouse event to that window and never calls through to NSWindow, so the application stops seeing motion and enter/leave, and native tooltips stop appearing anywhere in it. Capture through grab_mouse()/drop_mouse() so there is at most one, give it back on any button-up including a modal gesture (which tracks the pointer and needs no capture), and cancel on wxEVT_MOUSE_CAPTURE_LOST: commit nothing, put back what a modal rotate or scale already applied, and do not release a capture that is already gone. --- src/slic3r/GUI/UVEditorCanvas.cpp | 48 +++++++++++++++++++++++++++++-- src/slic3r/GUI/UVEditorCanvas.hpp | 12 ++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/UVEditorCanvas.cpp b/src/slic3r/GUI/UVEditorCanvas.cpp index e12829c5bf..22ed92ca48 100644 --- a/src/slic3r/GUI/UVEditorCanvas.cpp +++ b/src/slic3r/GUI/UVEditorCanvas.cpp @@ -176,6 +176,7 @@ UVEditorCanvas::UVEditorCanvas(wxWindow *parent) Bind(wxEVT_MIDDLE_DOWN, &UVEditorCanvas::on_mouse, this); Bind(wxEVT_MIDDLE_UP, &UVEditorCanvas::on_mouse, this); Bind(wxEVT_MOTION, &UVEditorCanvas::on_mouse, this); + Bind(wxEVT_MOUSE_CAPTURE_LOST, &UVEditorCanvas::on_capture_lost, this); Bind(wxEVT_MOUSEWHEEL, &UVEditorCanvas::on_mouse, this); Bind(wxEVT_LEAVE_WINDOW, &UVEditorCanvas::on_leave, this); Bind(wxEVT_KEY_DOWN, &UVEditorCanvas::on_key, this); @@ -727,10 +728,47 @@ void UVEditorCanvas::end_gesture() m_rot_raw_deg = 0.f; m_rot_applied_deg = 0.f; m_modal_scale_accum = 1.f; + drop_mouse(); +} + +void UVEditorCanvas::cancel_gesture() +{ + // Undo what the gesture already applied live, as the Esc path does, and commit nothing. + if (m_on_island_edit) { + if (m_gesture == Gesture::RotateIslandModal && m_rot_applied_deg != 0.f) + m_on_island_edit(m_selected_island, Vec2f::Zero(), -m_rot_applied_deg, 1.f, false); + if (m_gesture == Gesture::ScaleIslandModal && m_modal_scale_accum != 1.f) + m_on_island_edit(m_selected_island, Vec2f::Zero(), 0.f, 1.f / m_modal_scale_accum, false); + } + + m_gesture = Gesture::None; + m_rot_raw_deg = 0.f; + m_rot_applied_deg = 0.f; + m_modal_scale_accum = 1.f; + m_vertex_edit_moved = false; +} + +void UVEditorCanvas::grab_mouse() +{ + if (!HasCapture()) + CaptureMouse(); +} + +void UVEditorCanvas::drop_mouse() +{ if (HasCapture()) ReleaseMouse(); } +// The capture was taken from us (a dialog opened, another application grabbed the pointer). wx +// requires this to cancel the gesture: no commit, no Skip(), and no ReleaseMouse() - the capture is +// already gone, and releasing it again would unbalance the stack. +void UVEditorCanvas::on_capture_lost(wxMouseCaptureLostEvent &) +{ + cancel_gesture(); + Refresh(); +} + void UVEditorCanvas::on_key(wxKeyEvent &evt) { const int key = evt.GetKeyCode(); @@ -909,7 +947,7 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt) } m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan; } - CaptureMouse(); + grab_mouse(); Refresh(); } else if (type == wxEVT_RIGHT_DOWN && m_selected_island >= 0 && m_select_mode == SelectMode::Island) { const Vec2f rel = screen_to_uv(pos) - island_centroid(m_selected_island); @@ -919,12 +957,16 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt) m_rot_base_deg = island_rotation_deg(m_selected_island); m_rot_display_deg = m_rot_base_deg; m_gesture_last_angle = std::atan2(rel.y(), rel.x()); - CaptureMouse(); + grab_mouse(); } else if (type == wxEVT_MIDDLE_DOWN) { m_gesture = Gesture::Pan; m_drag_last_px = pos; - CaptureMouse(); + grab_mouse(); } else if (type == wxEVT_LEFT_UP || type == wxEVT_RIGHT_UP || type == wxEVT_MIDDLE_UP) { + // The drag is over either way. A modal R/S keeps running until a click confirms it, but it + // tracks the pointer over this canvas and needs no capture to do so, so the capture goes back + // here rather than waiting for that click - which may never come. + drop_mouse(); if (m_gesture != Gesture::RotateIslandModal && m_gesture != Gesture::ScaleIslandModal) { end_gesture(); Refresh(); diff --git a/src/slic3r/GUI/UVEditorCanvas.hpp b/src/slic3r/GUI/UVEditorCanvas.hpp index 0dd088c4e8..c467564c42 100644 --- a/src/slic3r/GUI/UVEditorCanvas.hpp +++ b/src/slic3r/GUI/UVEditorCanvas.hpp @@ -10,6 +10,7 @@ // pulls in the platform's real GL/gl.h, and glad/gl.h errors out if that happens first (it wants // to be the one to define the standard include guards GL/gl.h itself defines). #include +#include #include #include #include @@ -194,6 +195,7 @@ private: void on_paint(wxPaintEvent &evt); void on_size(wxSizeEvent &evt); void on_mouse(wxMouseEvent &evt); + void on_capture_lost(wxMouseCaptureLostEvent &evt); void on_key(wxKeyEvent &evt); void on_leave(wxMouseEvent &evt); // drops the +/- cursor hint when the pointer leaves the canvas void on_erase_background(wxEraseEvent &evt) {} // required to avoid flicker on MSW, deliberately a no-op @@ -238,6 +240,16 @@ private: // vertex of some *other* island, in texture-UV space. Zero if nothing is within reach (#2). Vec2f snap_correction(int island) const; void end_gesture(); + // Cancels the gesture instead of finishing it: nothing is committed, and a modal rotate/scale is + // put back the way Esc puts it back. Used when the capture is taken away from us. + void cancel_gesture(); + // One capture at a time, released exactly once. Pressing a second button mid-drag would otherwise + // nest a second capture that the single release on button-up cannot undo, and macOS never sends + // wxEVT_MOUSE_CAPTURE_LOST to recover from that. A leaked capture is not a local problem there: + // while any wx window holds one, wxOSX routes every mouse event to it and the application stops + // seeing enter/leave and motion entirely, which also takes its tooltips down. + void grab_mouse(); + void drop_mouse(); // Rebuilds the status line from the current gesture/selection and pushes it to m_on_status. void update_status(); // Re-picks what a click at `pos` would grab in the current select mode, and repaints when that changed.