From 493befecc5440c29ea13840cc51bd4bce84e4333 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 31 Aug 2026 17:54:27 +0800 Subject: [PATCH] Tear down the Design canvas at shutdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Design tab's viewport is the fourth GLCanvas3D on the shared GL context and the only one the plater does not own, so unbind_canvas_event_handlers() and reset_canvas_volumes() never reached it — the macOS Command+Q and Debian cases those calls exist for. Its frame-level handlers become members so they can be unbound. --- src/slic3r/GUI/CAD/DesignCanvas.cpp | 73 ++++++++++++++++++++++------- src/slic3r/GUI/CAD/DesignCanvas.hpp | 6 +++ src/slic3r/GUI/CAD/DesignPanel.cpp | 11 +++++ src/slic3r/GUI/CAD/DesignPanel.hpp | 2 + src/slic3r/GUI/Plater.cpp | 13 +++++ 5 files changed, 88 insertions(+), 17 deletions(-) diff --git a/src/slic3r/GUI/CAD/DesignCanvas.cpp b/src/slic3r/GUI/CAD/DesignCanvas.cpp index dff9aa2175..148b32db4f 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.cpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.cpp @@ -160,9 +160,9 @@ DesignCanvas::DesignCanvas(wxWindow* parent) m_status_hud->Hide(); } // A floating frame does not follow its parent, so the anchor has to be recomputed whenever - // the canvas changes size. The readout HUD gets away without this because it is transient; - // the status line is on screen almost permanently and would visibly detach. The wxEVT_SIZE - // bind that does it lives BELOW bind_event_handlers() and not here — see the note there. + // the canvas changes size (that bind is below bind_event_handlers(), for the reason given + // there). The readout HUD gets away without this because it is transient; the status line is + // on screen almost permanently and would visibly detach. // ...and it does not follow the WINDOW either. A popup is override-redirect: the window // manager does not own it, so minimising the app leaves the chip sitting on the bare desktop // (seen on the rig: whole screen black, chip still there), and it stacks above other @@ -171,19 +171,17 @@ DesignCanvas::DesignCanvas(wxWindow* parent) // the same case one step weaker: the chip belongs to a viewport the user is no longer // looking at. Showing it back is safe because a popup cannot take focus, so neither event // can be re-triggered by our own Show(). + // Members rather than lambdas so unbind_canvas_event_handlers() can Unbind them: these sit on + // a frame that OUTLIVES this canvas, and a lambda cannot be unbound. if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget)) { - top->Bind(wxEVT_ICONIZE, [this](wxIconizeEvent& e) { - show_status_hud(!e.IsIconized()); e.Skip(); - }); - top->Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& e) { - show_status_hud(e.GetActive()); e.Skip(); - }); + top->Bind(wxEVT_ICONIZE, &DesignCanvas::on_frame_iconize, this); + top->Bind(wxEVT_ACTIVATE, &DesignCanvas::on_frame_activate, this); // The anchor is an ABSOLUTE SCREEN position (ClientToScreen below), so moving the window // moves the canvas out from under a chip that stays where it was. Dragging the frame by // its title bar left the chip stranded mid-viewport until the next size, status or tab // change happened to re-place it. Nothing on the canvas fires for a move that does not // also resize, so it has to come from the frame. - top->Bind(wxEVT_MOVE, [this](wxMoveEvent& e) { place_status_hud(); e.Skip(); }); + top->Bind(wxEVT_MOVE, &DesignCanvas::on_status_hud_reanchor, this); } refresh_bed(); @@ -200,13 +198,11 @@ DesignCanvas::DesignCanvas(wxWindow* parent) // running first, which is only true while this call stays ahead of them. m_canvas->bind_event_handlers(); - // The status-chip re-anchor promised further up, bound here for exactly that reason: above - // this line the lambda never ran at all, so the chip kept a stale screen anchor and a stale - // wrap width after every resize that did not also move the frame or change the status text - // (dragging the right or bottom edge, a splitter move). The e.Skip() is load-bearing the - // other way: wx clears the skip flag before each handler and stops only on one that leaves - // it clear, so skipping falls through to on_size and the canvas is still marked dirty. - m_canvas_widget->Bind(wxEVT_SIZE, [this](wxSizeEvent& e) { place_status_hud(); e.Skip(); }); + // The status-chip re-anchor promised above, bound AFTER the call so it runs first — ahead of + // it the handler never ran at all, leaving a stale anchor and wrap width after any resize + // that did not also move the frame or change the text. Its e.Skip() is load-bearing the + // other way: it falls through to on_size, which is what still marks the canvas dirty. + m_canvas_widget->Bind(wxEVT_SIZE, &DesignCanvas::on_status_hud_reanchor, this); // The Design GL canvas only receives key events (Esc to exit/enter Select, Ctrl+Z undo) // while it holds keyboard focus. Clicking a side-panel button steals focus, after which @@ -227,6 +223,29 @@ DesignCanvas::DesignCanvas(wxWindow* parent) SetMinSize(wxSize(300, 300)); } +// Both are idempotent, and neither destroys anything: the destructor still owns that. +void DesignCanvas::unbind_canvas_event_handlers() +{ + if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget)) { + top->Unbind(wxEVT_ICONIZE, &DesignCanvas::on_frame_iconize, this); + top->Unbind(wxEVT_ACTIVATE, &DesignCanvas::on_frame_activate, this); + top->Unbind(wxEVT_MOVE, &DesignCanvas::on_status_hud_reanchor, this); + } + // Before the popups go down, or a resize still in flight re-places and re-shows the chip. + if (m_canvas_widget) + m_canvas_widget->Unbind(wxEVT_SIZE, &DesignCanvas::on_status_hud_reanchor, this); + // A popup is override-redirect: it does not go down with the frame, so one left showing sits + // on the bare desktop for however long the teardown takes. + show_status_hud(false); + if (m_hud) m_hud->Hide(); + if (m_canvas) m_canvas->unbind_event_handlers(); +} + +void DesignCanvas::reset_canvas_volumes() +{ + if (m_canvas) m_canvas->reset_volumes(); +} + DesignCanvas::~DesignCanvas() { if (m_hud) m_hud->Destroy(); @@ -1190,6 +1209,26 @@ void DesignCanvas::place_status_hud() m_status_hud->Move(bl); } +void DesignCanvas::on_frame_iconize(wxIconizeEvent& e) +{ + show_status_hud(!e.IsIconized()); + e.Skip(); +} + +void DesignCanvas::on_frame_activate(wxActivateEvent& e) +{ + show_status_hud(e.GetActive()); + e.Skip(); +} + +// wxEvent& so one handler serves both events that invalidate the anchor: the frame moving out +// from under the chip, and the canvas resizing under it. +void DesignCanvas::on_status_hud_reanchor(wxEvent& e) +{ + place_status_hud(); + e.Skip(); +} + void DesignCanvas::show_status_hud(bool on) { if (!m_status_hud) return; diff --git a/src/slic3r/GUI/CAD/DesignCanvas.hpp b/src/slic3r/GUI/CAD/DesignCanvas.hpp index 2b4c4ff009..f8c5b6bc57 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.hpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.hpp @@ -77,6 +77,8 @@ public: // own view across a tab switch. void enter_viewport(); void leave_viewport(); + void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown + void reset_canvas_volumes(); void set_show_bed(bool b); // view option: draw the printer bed + plate grid, or not void cancel_sketch(); void set_on_sketch_commit(std::function cb); @@ -385,6 +387,10 @@ private: wxColour m_status_hud_colour; void place_status_hud(); // re-anchors to the canvas corner (also on resize) void apply_status_label(); // SetLabel + Wrap to the canvas width + Fit, always together + // On the top-level frame, which outlives this canvas — members so they can be unbound. + void on_frame_iconize(wxIconizeEvent& e); + void on_frame_activate(wxActivateEvent& e); + void on_status_hud_reanchor(wxEvent& e); // frame wxEVT_MOVE and canvas wxEVT_SIZE std::function m_on_sketch_commit; std::function&, const std::vector&, diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index 13fab6e752..edebf0a32f 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -6991,6 +6991,17 @@ void DesignPanel::on_tab_shown() if (m_viewport) m_viewport->force_repaint(); // the page was just re-shown: paint it for real } +// Application close / language switch, from the plater's canvas teardown. +void DesignPanel::unbind_canvas_event_handlers() +{ + if (m_viewport) m_viewport->unbind_canvas_event_handlers(); +} + +void DesignPanel::reset_canvas_volumes() +{ + if (m_viewport) m_viewport->reset_canvas_volumes(); +} + // Match Prepare's sidebar width instead of hardcoding one. Design used a fixed 264 px against // Prepare's ~467, so the canvas edge jumped sideways on every tab switch; reading the live width // also means the two stay aligned if Orca ever changes its sidebar. diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp index 762c8ebef0..2410eb0742 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.hpp +++ b/src/slic3r/GUI/CAD/DesignPanel.hpp @@ -51,6 +51,8 @@ public: explicit DesignPanel(wxWindow* parent); void on_tab_shown(); // re-sync bed to the active printer when the Design tab is activated void on_tab_hidden(); // another tab took over: take the viewport status line down with us + void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown + void reset_canvas_volumes(); void clear_document(); // New Project / Open Project: drop the document with the project // Rebuild off the UI thread (progress dialog only if it turns out to be slow), so a feature // op on a heavy imported solid does not freeze the window. Returns m_doc.recompute()'s result. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c67abdfa6f..2591029e1c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -13557,6 +13557,14 @@ void Plater::priv::unbind_canvas_event_handlers() if (assemble_view != nullptr) assemble_view->get_canvas3d()->unbind_event_handlers(); + +#ifdef SLIC3R_CAD + // The Design tab's viewport is a fourth GLCanvas3D on the same shared GL context, owned by + // MainFrame rather than by us — same reach as reset() uses for clear_document(). Null until + // the tab has been opened once, so most sessions skip it. + if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr) + wxGetApp().mainframe->m_design_panel->unbind_canvas_event_handlers(); +#endif } void Plater::priv::reset_canvas_volumes() @@ -13566,6 +13574,11 @@ void Plater::priv::reset_canvas_volumes() if (preview != nullptr) preview->get_canvas3d()->reset_volumes(); + +#ifdef SLIC3R_CAD + if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr) + wxGetApp().mainframe->m_design_panel->reset_canvas_volumes(); +#endif } bool Plater::priv::check_ams_status_impl(bool is_slice_all)