From ff35dacf4c14e3158b3201d316c0d9c27a3f4ab8 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sun, 23 Aug 2026 18:43:41 +0800 Subject: [PATCH] Fix UI hanging on Mac --- src/slic3r/GUI/GradientCurveEditor.cpp | 8 ++++ src/slic3r/GUI/GradientCurveEditor.hpp | 2 + src/slic3r/GUI/MixedFilamentDialog.cpp | 54 ++++++++++++++++++-------- src/slic3r/GUI/MixedFilamentDialog.hpp | 9 ++++- src/slic3r/GUI/TextureImportDialog.cpp | 21 +++++++++- 5 files changed, 74 insertions(+), 20 deletions(-) diff --git a/src/slic3r/GUI/GradientCurveEditor.cpp b/src/slic3r/GUI/GradientCurveEditor.cpp index 782961aba4..d34e71a132 100644 --- a/src/slic3r/GUI/GradientCurveEditor.cpp +++ b/src/slic3r/GUI/GradientCurveEditor.cpp @@ -85,6 +85,14 @@ GradientCurveEditor::GradientCurveEditor(wxWindow* parent, }); } +GradientCurveEditor::~GradientCurveEditor() +{ + // See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it + // still holds the capture wedges mouse input for the whole application. + if (HasCapture()) + ReleaseMouse(); +} + void GradientCurveEditor::set_points(const PointList& pts) { m_points = pts; diff --git a/src/slic3r/GUI/GradientCurveEditor.hpp b/src/slic3r/GUI/GradientCurveEditor.hpp index f9858cab11..f2e082aff5 100644 --- a/src/slic3r/GUI/GradientCurveEditor.hpp +++ b/src/slic3r/GUI/GradientCurveEditor.hpp @@ -36,6 +36,8 @@ public: const wxColour& color_low = wxColour(217, 217, 217), const wxColour& color_high = wxColour(217, 217, 217)); + ~GradientCurveEditor() override; + // Replace the entire point list. The widget enforces x in [0,1], y in [0,1], // sorts by x, and clamps the first / last x to 0 / 1. Tangent overrides are // preserved as-is (NaN entries continue to use PCHIP defaults). diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp index c81c90fe1c..b72846452f 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.cpp +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -154,6 +154,18 @@ MixedFilamentDialog::MixedFilamentDialog(wxWindow* parent, m_preview_bmp_three = wxBitmap(img); } +MixedFilamentDialog::~MixedFilamentDialog() +{ + // Backstop: a child must never be destroyed while it still holds the mouse + // capture. wxWidgets only asserts about this (compiled out in release), and + // the macOS port never unwinds its capture stack, so the stale entry would + // make wxNSWindow::sendEvent swallow every mouse event in the application. + if (m_ratio_bar && m_ratio_bar->HasCapture()) + m_ratio_bar->ReleaseMouse(); + if (m_triangle_panel && m_triangle_panel->HasCapture()) + m_triangle_panel->ReleaseMouse(); +} + MixedFilamentDialog::MixedFilamentDialog(wxWindow* parent, const MixedFilamentResult& existing, const std::vector& physical_colors, @@ -892,24 +904,30 @@ wxBoxSizer* MixedFilamentDialog::create_ratio_slider() m_ratio_bar->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) { if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) commit_ratio_editor(true); - m_dragging = true; - m_ratio_bar->CaptureMouse(); + m_ratio_dragging = true; + if (!m_ratio_bar->HasCapture()) + m_ratio_bar->CaptureMouse(); int new_ratio = 100 - (int)(e.GetX() * 100.0 / m_ratio_bar->GetClientSize().GetWidth() + 0.5); on_ratio_changed(std::max(MIN_COMPONENT_RATIO, std::min(100 - MIN_COMPONENT_RATIO, new_ratio))); }); m_ratio_bar->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) { - if (!m_dragging) return; + if (!m_ratio_dragging) return; int new_ratio = 100 - (int)(e.GetX() * 100.0 / m_ratio_bar->GetClientSize().GetWidth() + 0.5); on_ratio_changed(std::max(MIN_COMPONENT_RATIO, std::min(100 - MIN_COMPONENT_RATIO, new_ratio))); }); + // Release whenever the capture is held, not only when the drag flag is set: + // the flag can be cleared behind our back, and a capture that outlives the + // widget wedges mouse input for the whole application. m_ratio_bar->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent&) { - if (m_dragging) { - m_dragging = false; - if (m_ratio_bar->HasCapture()) - m_ratio_bar->ReleaseMouse(); - } + m_ratio_dragging = false; + if (m_ratio_bar->HasCapture()) + m_ratio_bar->ReleaseMouse(); + }); + + m_ratio_bar->Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + m_ratio_dragging = false; }); sizer->Add(m_ratio_bar, 0, wxEXPAND); @@ -1122,11 +1140,12 @@ wxBoxSizer* MixedFilamentDialog::create_triangle_picker() // clicks outside the triangle must not change the mix ratio. if (!tri_contains(p, v0, v1, v2)) return; - m_dragging = true; - m_triangle_panel->CaptureMouse(); + m_tri_dragging = true; + if (!m_triangle_panel->HasCapture()) + m_triangle_panel->CaptureMouse(); } - if (!m_dragging) return; + if (!m_tri_dragging) return; TriPoint clamped = tri_clamp(p, v0, v1, v2); tri_barycentric(clamped, v0, v1, v2, m_tri_wx, m_tri_wy, m_tri_wz); @@ -1161,15 +1180,16 @@ wxBoxSizer* MixedFilamentDialog::create_triangle_picker() handle_mouse(e, true); }); m_triangle_panel->Bind(wxEVT_MOTION, [this, handle_mouse](wxMouseEvent& e) { - if (m_dragging) + if (m_tri_dragging) handle_mouse(e, false); }); m_triangle_panel->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent&) { - if (m_dragging) { - m_dragging = false; - if (m_triangle_panel->HasCapture()) - m_triangle_panel->ReleaseMouse(); - } + m_tri_dragging = false; + if (m_triangle_panel->HasCapture()) + m_triangle_panel->ReleaseMouse(); + }); + m_triangle_panel->Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + m_tri_dragging = false; }); sizer->Add(m_triangle_panel, 0); diff --git a/src/slic3r/GUI/MixedFilamentDialog.hpp b/src/slic3r/GUI/MixedFilamentDialog.hpp index a1af146897..6085f77873 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.hpp +++ b/src/slic3r/GUI/MixedFilamentDialog.hpp @@ -53,6 +53,8 @@ public: const std::vector& physical_names, const std::vector& physical_types = {}); + ~MixedFilamentDialog(); + MixedFilamentResult get_result() const { return m_result; } protected: @@ -160,8 +162,11 @@ private: wxBitmap m_preview_bmp_two; wxBitmap m_preview_bmp_three; - // Drag state - bool m_dragging{false}; + // Drag state. The ratio bar and the triangle picker capture the mouse + // independently, so they must not share a flag: a mouse-up on one would + // otherwise clear the other's flag and skip its ReleaseMouse(). + bool m_ratio_dragging{false}; + bool m_tri_dragging{false}; std::vector m_ratio_manual_order; size_t m_ratio_editor_idx{0}; bool m_ratio_editor_committing{false}; diff --git a/src/slic3r/GUI/TextureImportDialog.cpp b/src/slic3r/GUI/TextureImportDialog.cpp index e2886687f3..688898ed0a 100644 --- a/src/slic3r/GUI/TextureImportDialog.cpp +++ b/src/slic3r/GUI/TextureImportDialog.cpp @@ -184,6 +184,7 @@ public: GreenSlider(wxWindow* parent, int value, int minVal, int maxVal, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize); + ~GreenSlider() override; int GetValue() const; void SetValue(int val); bool Enable(bool enable = true) override; @@ -213,6 +214,15 @@ GreenSlider::GreenSlider(wxWindow* parent, int value, int minVal, int maxVal, Bind(wxEVT_LEFT_DOWN, &GreenSlider::OnMouse, this); Bind(wxEVT_LEFT_UP, &GreenSlider::OnMouse, this); Bind(wxEVT_MOTION, &GreenSlider::OnMouse, this); + Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { m_dragging = false; }); +} + +GreenSlider::~GreenSlider() +{ + // See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it + // still holds the capture wedges mouse input for the whole application. + if (HasCapture()) + ReleaseMouse(); } int GreenSlider::GetValue() const { return m_value; } @@ -302,7 +312,7 @@ void GreenSlider::OnMouse(wxMouseEvent& evt) if (evt.LeftDown()) { m_dragging = true; - CaptureMouse(); + if (!HasCapture()) CaptureMouse(); update(evt.GetX()); } else if (evt.LeftUp()) { m_dragging = false; @@ -1019,11 +1029,20 @@ TexturePreviewCanvas::TexturePreviewCanvas(wxWindow* parent, const wxGLAttribute Bind(wxEVT_MIDDLE_DOWN, &TexturePreviewCanvas::on_mouse, this); Bind(wxEVT_MIDDLE_UP, &TexturePreviewCanvas::on_mouse, this); Bind(wxEVT_MOTION, &TexturePreviewCanvas::on_mouse, this); + Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { + m_drag_mode = DragMode::None; + m_reset_overlay_pressed = false; + }); Bind(wxEVT_LEAVE_WINDOW, &TexturePreviewCanvas::on_mouse, this); } TexturePreviewCanvas::~TexturePreviewCanvas() { + // See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it + // still holds the capture wedges mouse input for the whole application. + if (HasCapture()) + ReleaseMouse(); + if (m_context) { SetCurrent(*m_context); if (m_tex_id)