Tear down the Design canvas at shutdown

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.
This commit is contained in:
SoftFever
2026-08-31 18:04:12 +08:00
parent 5a4f7f4c3c
commit 493befecc5
5 changed files with 88 additions and 17 deletions
+56 -17
View File
@@ -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;
+6
View File
@@ -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<void(const SketchProfile&, const SketchPlane&)> 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<void(const SketchProfile&, const SketchPlane&)> m_on_sketch_commit;
std::function<void(const std::vector<SketchEntity>&,
const std::vector<SketchEntityConstraintDef>&,
+11
View File
@@ -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.
+2
View File
@@ -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.
+13
View File
@@ -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)