75 KiB
Windows, dialogs and window lifetime
How windows are created, parented, closed and destroyed; how to tell whether a window is still
alive; how top-level windows show, raise and persist; how modal and modeless dialogs work on each
port; what Orca's DPIDialog/DPIFrame add; the Orca dialog recipe; and the MsgDialog family.
Read it before writing or reviewing any dialog, frame, close handler, Destroy()/delete, or code
that keeps a pointer to a window across an event, a CallAfter or a modal loop.
wx cites are relative to the pinned wx 3.3.2 tree (located as in SKILL.md §Ground truth). wx is
built with wxBUILD_DEBUG_LEVEL=0 and libslic3r_gui with wxDEBUG_LEVEL=0: every wx assert
quoted below is compiled out, so misuse fails silently (dropped call, stuck loop, freed memory), never with an assert dialog. "GTK" below
means wxGTK as Orca builds it on Linux: GTK3 by default (X11 or Wayland); GTK2 is only an opt-out.
Contents: Rules · 1 Creating and parenting · 2 Destroy, delete, Close · 3 Liveness · 4 Top-level windows · 5 Window styles · 6 Dialogs and modality · 7 DPIDialog and DPIFrame · 8 The Orca dialog recipe · 9 MsgDialog family · 10 Overlay frames
Rules
- Free heap-allocated windows with
Destroy(), neverdelete; a modal dialog on the stack is freed by its scope and must never beDestroy()ed. §2 - Never
Destroy()a child window (or an ancestor of it) from inside that child's own event handler: childDestroy()is a synchronousdelete this. Defer withwxTheApp->ScheduleForDestruction(w)or a guardedCallAfter. §2 - A close handler either vetoes or destroys/ends the window; prompts and vetoes only when
CanVeto(); when!CanVeto()it must not veto. §2 - Do irreversible teardown in a close handler only once every veto is decided — the default top-level handler still vetoes a vetoable close while any modal dialog is open. §2
- Give every dialog and frame a live top-level parent (
parent ? parent : wxGetApp().mainframe); a parentless window that only hides on close keeps the process alive. §1, §2 - Never construct a window to reach computation; make the logic
staticor free. §1 - To ask "is this top-level window still alive", check
wxTheApp->IsScheduledForDestruction(w)as well asIsBeingDeleted();wxWeakRefnulls only at the very end of destruction; cross-thread code uses astd::shared_ptr<std::atomic<bool>>alive flag. Re-check after every nested event loop. §3 - A
wxEVT_DESTROYhandler on a parent fires for every descendant: compareGetEventObject()andSkip(). For a top-level window it runs after the derived destructor — do derived cleanup in the destructor. §3 - Accessors reachable from teardown-time events return
nullptrinstead of dereferencing a pimpl or child; the real fix is still to stop the events first. §3 - Bring a top-level window forward with
if (!w->IsShown()) w->Show(); w->Raise();—Raise()never shows, and a redundantShow()is not inert on GTK3. §4 - On macOS, after a native modal (file/dir dialog, native message box) or a generic progress dialog opened from a secondary top-level window, re-raise that window with a deferred, liveness-guarded
Raise(). §4 - Remove style bits with
& ~flag, never!flag. §5 - Always pass the style to a
DPIDialog: theDPIAwaredefault iswxDEFAULT_FRAME_STYLE(resizable, min/max boxes). §5, §7 - A
wxFRAME_FLOAT_ON_PARENTframe takes a non-null top-level parent (parent tomainframe, not to a panel). §5 - End a modal dialog with
EndModal(wxID_*)(orEndDialogfrom inside the class); never withHide(),Show(false)orDestroy()while its loop runs. A heap modal isDestroy()ed afterEndModal()— normally by the caller onceShowModal()returns. §6 - Return codes are
wxID_*ids, never thewxOK/wxCANCEL/wxYES/wxNO/wxCLOSEstyle bits (theMsgDialog"Go to" button'swxFORWARDis the one deliberate exception). §6, §9 - Nested modal dialogs end innermost first;
DPIDialog::EndModalrefuses (logs, leaves the dialog open) on a dialog that is not the innermostDPIDialog. §6, §7 - Never call
EndModal()on a modeless dialog; useHide(),Close(),Destroy(), orEndDialog(rc)inside the class. §6 - Never show a modal dialog directly from a mouse-button or motion handler; defer with
CallAfter. §6 - Run cancel cleanup on every close path: ESC and the title-bar close box go through
Close()andwxID_CANCEL, never through an OrcaButton's handler. §6, §8 - Dialogs derive
DPIDialog, implementon_dpi_changed, callCenterOnParent()after fitting, and callwxGetApp().UpdateDlgDarkUI(this)last; an override ofShowModal()callsDPIDialog::ShowModal(). §7, §8 - The bottom row is
DialogButtonswith untranslated labels; bind every button whose default wx handling is not what you want — Yes/No/Confirm/custom buttons never close the dialog by themselves. §8 - Messages to the user go through the
MsgDialogfamily (MessageDialog,RichMessageDialog,WarningDialog,ErrorDialog,InfoDialog,show_error/show_info), neverwxMessageBox/wxMessageDialogonce the GUI exists; treatwxID_CANCEL(ESC/close box) as "no". §9 show_erroris asynchronous and captures its parent pointer; pass a long-lived parent ornullptr. §9
1 Creating and parenting windows
Contract.
- Child windows are created shown; top-level windows (frames, dialogs) are created hidden "to allow
you to create their contents without flicker" (
interface/wx/window.h:3140-3158). - "Child windows are deleted from within the parent destructor. This includes any children that are
themselves frames or dialogs, so you may wish to close these child frame or dialog windows explicitly
from within the parent close handler" (
docs/doxygen/overviews/windowdeletion.h:96-100). wxDialogwith a NULL parent "will be owned by the application's top window, if any. UsewxDIALOG_NO_PARENTstyle to really make dialog not owned by any window" (interface/wx/dialog.h:166-172). The style doc adds: orphan dialogs are "not recommended for modal dialogs" (interface/wx/dialog.h:122-126).- [source] Only the native owner is substituted;
GetParent()stays NULL. Owner resolution (wxDialogBase::DoGetParentForDialog,src/common/dlgcmn.cpp:180-204): the given parent's top-level parent → the active window's top-level parent →wxApp::GetMainTopWindow(), each rejected if it is pending delete, being deleted, haswxWS_EX_TRANSIENT, or — for modal use — is notIsShownOnScreen()(CheckIfCanBeUsedAsParent,src/common/dlgcmn.cpp:130-178).
Two-step creation. "the underlying window must be created exactly once… if you use the default
constructor… you must call Create() before using the window and if you use the non-default
constructor, you can not call Create()" (interface/wx/window.h:411-417). Methods may be called
between the C++ constructor and Create() — Hide() to build invisibly, Enable(false),
SetExtraStyle() for create-time extra styles (interface/wx/window.h:419-428, 3120-3126; interface/wx/dialog.h:127-133).
auto* panel = new wxPanel(); // C++ object only
panel->Hide(); // legal before Create
panel->Create(parent, wxID_ANY); // native window, still hidden
/* build children */ panel->Show();
[source] Destroy() on a never-created window skips wxEVT_DESTROY (src/common/wincmn.cpp:559-573),
and a never-created top-level window is deleted immediately (src/common/toplvcmn.cpp:102-112; not on
macOS, whose Destroy() always defers, §2 table).
Orca's widgets (StaticBox, Button, TextInput, SpinInput) follow the layering:
default constructor + Create(...), the convenience constructor delegates to both, and each
Create calls its base Create first, then attaches state_handler. ComboBox has only the
convenience constructor, which default-constructs its TextInput base and calls TextInput::Create.
Subclasses keep that layering.
Reparent. Reparent(newParent) moves the window between children lists (and between
wxTopLevelWindows and a parent); "you need to explicitly call wxNotebook::RemovePage() before
reparenting a notebook page" (interface/wx/window.h:730-742). [source] It does not touch
sizers (src/common/wincmn.cpp:1339-1377); GTK hides the widget and re-shows it on idle if the new parent is
visible (src/gtk/window.cpp:5195-5230); MSW just calls ::SetParent.
Platforms.
- MSW fixes a dialog's native owner at
Createtime (wxTopLevelWindowMSW::CreateDialog→GetParentForModalDialog,src/msw/toplevel.cpp:337-352). A dialog created while its parent is not on screen (e.g. inside the parent frame's constructor) is owned by the active window, the main top window, or nothing — wrong z-order and taskbar grouping. - GTK sets
transient_forfor modal dialogs inShowModal()(src/gtk/dialog.cpp:139-144), and for dialogs/float-on-parent/stay-on-top frames with a top-level parent inCreate(src/gtk/toplevel.cpp:774-782). - Wayland: the compositor places top-level windows (see §4); dialog placement comes from
transient_for, so a real parent is the only positioning control there is.
OrcaSlicer. Dialogs take parent ? parent : wxGetApp().mainframe — never orphan a dialog (the
MsgDialog constructor does this substitution itself). Passing a child panel as parent is legal:
the dialog dies with that panel (§2) and CenterOnParent() centres on the panel's screen rect
(wxTopLevelWindowBase::DoCentre, src/common/toplvcmn.cpp:239-279); with no wx parent it centres on the display.
Pitfalls.
- Rule: Never construct a window — especially one hosting a
wxWebView— just to call one of its computation methods; make the computationstaticor a free function. Why: window construction has heavy native side effects (native handles, webview processes, event bindings).is_flush_config_modified()built a fullWipingDialog(with a WebView) only to callCalcFlushingVolumes(); it runs on the UI-rebuild path (Sidebar::msw_rescale,Sidebar::sys_color_changed), and on macOS the language switch froze the app.Cite:// Wrong WipingDialog dlg(wxGetApp().mainframe, extra_flush_volumes); auto m = dlg.CalcFlushingVolumes(i); // Right static VolumeMatrix CalcFlushingVolumes(int extruder_id); // no window needed auto m = WipingDialog::CalcFlushingVolumes(i);80f4a7a40f(WipingDialog::CalcFlushingVolumes/CalcFlushingVolumeinsrc/slic3r/GUI/WipeTowerDialog.hpp). - Rule: Create a dialog lazily, after its parent is on screen, when MSW ownership matters.
Why: the native owner is chosen at
Createand a hidden parent is rejected (above). - Rule:
Reparent()does not move sizer membership.// Wrong: w->Reparent(p); // Right: old_sizer->Detach(w); w->Reparent(p); new_sizer->Add(w, 0, wxEXPAND); p->Layout(); - Rule: Call
Create()exactly once; never on an object built with the non-default constructor.
2 Destroying windows: Destroy, delete, Close and the close event
Contract.
Destroy(): "Use this function instead of the delete operator, since different window classes can be destroyed differently. Frames and dialogs are not destroyed immediately when this function is called -- they are added to a list of windows to be deleted on idle time, when all the window's events have been processed. This prevents problems with events being sent to non-existent windows." Returns true "if the window has either been successfully deleted, or it has been added to the list of windows pending real deletion" (interface/wx/window.h:3607-3618).- Destructor: "Deletes all sub-windows, then deletes itself. Instead of using the delete operator
explicitly, you should normally use Destroy() so that wxWidgets can delete a window only when it is
safe to do so, in idle time" (
interface/wx/window.h:389-394).DestroyChildren()is called automatically by the destructor (interface/wx/window.h:626). - "Windows with parents, such as controls, don't have delayed destruction… For consistency, continue
to use the wxWindow::Destroy function instead of the delete operator when deleting these kinds of
windows explicitly" (
docs/doxygen/overviews/windowdeletion.h:103-109). Close(force)"simply generates a wxCloseEvent whose handler usually tries to close the window. It doesn't close the window itself";force=truemeans the handler cannot veto; returns "true if the event was handled and not vetoed" — a handler that hides instead of destroying still makes it return true. "Calling Close does not guarantee that the window will be destroyed… To guarantee that the window will be destroyed, call wxWindow::Destroy instead" (interface/wx/window.h:3574-3605).- Close handler: "If [CanVeto()] is false, you must destroy the window using wxWindow::Destroy… If you
don't destroy the window, you should call wxCloseEvent::Veto" (
interface/wx/event.h:4708-4717). "The wxCloseEvent handler should only call wxWindow::Destroy to delete the window, and not use the delete operator" (docs/doxygen/overviews/windowdeletion.h:38-42). - Defaults (
docs/doxygen/overviews/windowdeletion.h:63-73): wxDialog's close handler simulateswxID_CANCEL; the cancel handler hides a modeless dialog orEndModal(wxID_CANCEL)s a modal one — "the dialog is not destroyed (it might have been created on the stack)". wxFrame's default close handler callsDestroy(). [source] The top-level default (wxTopLevelWindowBase::OnCloseWindow,src/common/toplvcmn.cpp:535-546) vetoes a vetoable close whilewxModalDialogHook::GetOpenCount() > 0(any app-modal wx or native dialog open), otherwise callsDestroy(). - Deleting from inside a handler: "it may be unsafe for an event handler to delete the object which
generated the event because more events may be still pending for the same object. In this case the
handler may call ScheduleForDestruction() instead" (
interface/wx/app.h:191-212); without an event loop it deletes immediately.
When does Destroy() actually free the window? [source]
| Window | Destroy() does |
Cite |
|---|---|---|
| Child (control, panel) | sends wxEVT_DESTROY, then delete this synchronously; the window detaches from its containing sizer in ~wxWindowBase |
src/common/wincmn.cpp:559-573 |
| Top-level, normal case | appends to wxPendingDelete, hides unless it is the last visible top-level window; deleted by DeletePendingObjects() at the next idle — which also runs inside full yields and modal loops (wxYield, ShowModal), not inside a masked YieldFor (progress-dialog Update) |
src/common/toplvcmn.cpp:102-142; src/common/appbase.cpp:643-662 |
| Top-level, parent already being deleted, or never created (not macOS) | deleted immediately | src/common/toplvcmn.cpp:102-112 |
| Top-level, macOS | always deferred and always Hide()n |
src/osx/toplevel_osx.cpp:96-105 |
| Top-level, MSW | base behaviour plus wxWakeUpIdle() so iconized windows still get deleted |
src/msw/toplevel.cpp:771-784 |
| Any child of a dying parent, top-level children included | DestroyChildren calls the non-virtual wxWindowBase::Destroy(): deleted immediately, not queued |
src/common/wincmn.cpp:586-607 |
wxPopupTransientWindow |
deferred to idle (not hidden); a second Destroy() fails a wxCHECK — see references/popups-menus.md |
src/common/popupcmn.cpp wxPopupTransientWindowBase::Destroy |
Close-handler shapes (pick one per window, never mix; confirm_discard/cleanup stand for your code):
// destroy-on-close (a frame, or a modeless dialog the user owns)
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) {
if (e.CanVeto() && !confirm_discard()) { e.Veto(); return; }
cleanup(); // members still alive here
e.Skip(); // frame: default Destroy() (vetoed while a modal is open, Rule 4)
// modeless dialog: call Destroy() instead -- its default only EndDialog(wxID_CANCEL)s = hides it
});
// hide-on-close (a reusable window someone else owns) — TextureProjectorFrame's shape
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) {
if (e.CanVeto()) { e.Veto(); Hide(); } else e.Skip(); // a forced close must still destroy
});
// usable both modal and modeless — WebDialog::on_close_window's shape
void on_close(wxCloseEvent&) { if (IsModal()) EndModal(wxID_CANCEL); else Destroy(); }
A dialog's e.Skip() reaches wxDialogBase::OnCloseWindow, which ends the dialog
(EndDialog(wxID_CANCEL) = EndModal or Hide) but never destroys it: a modeless dialog closed
with the close box is only hidden (src/common/dlgcmn.cpp:525-559).
Application exit. The app exits when the last top-level window is destroyed
(docs/doxygen/overviews/windowdeletion.h:89-93; SetExitOnFrameDelete(false) disables it, interface/wx/app.h:1195-1207). "By default,
the application stays alive as long as there are any open top level windows" — hidden ones count;
override ShouldPreventAppExit() to return false for unimportant windows (interface/wx/toplevel.h:650-657).
[source] IsLastBeforeExit runs from ~wxTopLevelWindowBase (src/common/toplvcmn.cpp:93-97, 144-189);
GetTopWindow() skips windows pending delete (src/common/appcmn.cpp:185-208).
OrcaSlicer — shapes to follow.
- Modeless singleton owned by
GUI_App(GUI_App::open_terminal_dialog,GUI_App::open_speed_dial):These dialogs bind no close handler, so the close box only hides them (the wx default for a modeless dialog, below) and the reopen path re-shows the same window; they die with their parentif (!m_dlg) { m_dlg = new TerminalDialog(mainframe, wxID_ANY, _L("Plugin Terminal")); m_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& e) { if (e.GetEventObject() == m_dlg) m_dlg = nullptr; // the event also arrives from children e.Skip(); }); } if (!m_dlg->IsShown()) m_dlg->Show(); m_dlg->Raise();mainframe(or an explicitDestroy(), asGUI_App::recreate_GUIdoes for the speed dial), and thewxEVT_DESTROYreset clears the pointer then. A reset that does not compareGetEventObject()clears the pointer when any child of the dialog is destroyed.open_terminal_dialogalso wraps all of this inCallAfterbecause it is reached from a webview script message (§6). - Dual-mode dialog (
WebDialog): close handlerIsModal() ? EndModal(wxID_CANCEL) : Destroy(); forced teardown (WebDialog::destroy_silently) callsEndModalfirst, thenDestroy(), because destroying a modal "can leave ShowModal() running". Registry cleanup runs in~WebDialog, explicitly not in awxEVT_DESTROYhandler (comment there: the event comes from the base~wxDialog, after the members died). - Hide-on-close reusable frame (
TextureProjectorFrame): veto +Hide()whenCanVeto(), elseSkip(). The owner (the texture gizmo) holds the pointer; it is parented tomainframebecausewxFRAME_FLOAT_ON_PARENTneeds a real top-level parent. - Pseudo-modal modeless dialog (
ParamsDialog): see §6 "Modality helpers". - By-value top-level member:
MainFrame::m_settings_dialogis aSettingsDialog(aDPIDialog) stored by value with a NULL parent; its close handler onlyHide()s. Such a window must never beDestroy()ed ordeleted — it dies in member destruction, before the frame's base destructors. Prefer a heap child for new code. - Main-frame replacement (
GUI_App::recreate_GUI):mainframe->shutdown(), create the newMainFrame,SetTopWindow(new_frame), thenold->Destroy()— new frame first, so wx never sees "last top-level window gone" and exits. - Main-frame close (
MainFrameconstructor'swxEVT_CLOSE_WINDOWlambda): every prompt and veto is gated byevent.CanVeto()(gizmo editing,Plater::close_with_confirm, print-host queue); thenset_closing(true),m_plater->reset(),shutdown(),event.Skip()→ defaultDestroy(). Cmd+Q on macOS posts a vetoable close (wxPostEvent(this, wxCloseEvent(wxEVT_CLOSE_WINDOW))); updater paths useClose(true)to force. Its teardown runs beforeSkip(), so a vetoable close that arrives while a modal is open (thewxEVT_QUERY_END_SESSIONhandler inGUI_App::on_init_innersends exactly that) is vetoed by the default handler after teardown — the hazard of Rule 4; don't copy that ordering into new handlers. Sequence detail:references/orca-architecture.md. - Lifetime-tied cleanup:
GUI_App::recreate_GUIattaches awxClientDatasubclass withSetClientObject; its destructor runs in~wxEvtHandler, after all children are gone. - Lazily built window (
Lazy<T>/LazyInstance<T>inLazy.hpp, e.g.MainFrame::m_diff_dialog, aLazy<DiffPresetDialog>): the holder builds the window on demand or at idle, but the window's parent owns it; code that only needs an already-built instance asksT::if_built()instead of forcing a build. Design:docs/HLSD/deferred-page-construction.md,references/orca-architecture.md.
Pitfalls.
- Rule: Destroy heap-allocated top-level windows with
Destroy(), neverdelete; stack-allocated modal dialogs are destroyed by scope. Why:deleteskips the pending-delete queue, and for a top-level windowwxEVT_DESTROYis then sent only from the base destructor.Destroy()delays deletion; it does not stop events: queued events andCallAfters of a window sitting inwxPendingDeletestill run (§3), so deferred code still needs liveness checks.Cite:// Wrong: auto* dlg = new MyDialog(this); dlg->ShowModal(); delete dlg; // Right: auto* dlg = new MyDialog(this); dlg->ShowModal(); dlg->Destroy(); // Right: MyDialog dlg(this); dlg.ShowModal();0a0d59b76b(detail::run_off_thread_with_progressinsrc/slic3r/GUI/PluginsDialog.hpp: the worker body intry/catch, then one main-threadCallAfterthat doestimer->Stop(); delete timer;andprogress->Destroy()only while the host's alive flag is set, or when the caller passed no flag — the progress dialog is a child of the host and has already died with it otherwise, §2 table). Older code thatdeletes a dialog afterShowModal()is legacy; don't extend it. - Rule: Never
Destroy()a control (or its ancestor panel) from inside that control's own handler.Why: child// Wrong: m_btn->Bind(wxEVT_BUTTON, [this](auto&) { m_panel->Destroy(); }); // m_btn is inside m_panel // Right: m_btn->Bind(wxEVT_BUTTON, [this](auto&) { wxTheApp->ScheduleForDestruction(m_panel); m_panel = nullptr; });Destroy()deletes synchronously; the dispatcher then returns into freed memory. - Rule: A close handler that neither destroys nor vetoes, or that vetoes without checking
CanVeto(), is wrong. Why:Close(true), session end and the default top-level handler rely on a non-vetoable close destroying the window; a hide-only handler leaks the window, and a parentless leaked window keeps the process alive after the main frame is gone. Parent it tomainframe,Destroy()it, or overrideShouldPreventAppExit(). - Rule: Do irreversible teardown only when the close is certain.
Why: a handler that tears down and then
Skip()s can still be vetoed bywxTopLevelWindowBase::OnCloseWindowwhen the close is vetoable and any modal dialog is open (for example a vetoable close sent while a dialog is up, as thewxEVT_QUERY_END_SESSIONpath does): the window survives, half torn down.// Wrong Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) { shutdown(); e.Skip(); }); // Right Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) { if (e.CanVeto() && wxModalDialogHook::GetOpenCount() > 0) { e.Veto(); return; } shutdown(); e.Skip(); }); - Rule: A
wxTimermust not outlive the handler that receives its events — make it a member (wxTimer m_timer{this}) or delete it before the owner dies. Detail inreferences/threads-timers-app.md§wxTimer.
3 Liveness: is this window still alive?
IsBeingDeleted(). Doc: true "if this window, or one of its parent windows, is scheduled for
destruction and can be useful to avoid manipulating it as it's usually useless to do something with a
window which is at the point of disappearing anyhow" (interface/wx/window.h:3620-3633). [source] Wrong for top-level windows: the flag is
set only by SendDestroyEvent(), i.e. when deletion actually starts (src/common/wincmn.cpp:541-557);
wxTopLevelWindowBase::Destroy() only queues and hides, so after tlw->Destroy() returns,
tlw->IsBeingDeleted() is false until idle. The parent walk also stops at a top-level window
(m_isBeingDeleted || (!IsTopLevel() && m_parent->IsBeingDeleted()), src/common/wincmn.cpp:535-539): a dialog
does not report its parent frame's deletion.
wxApp::IsScheduledForDestruction(obj) / ScheduleForDestruction(obj) (interface/wx/app.h:191-221):
the first answers "has Destroy() (or ScheduleForDestruction) already been called"; both share
wxPendingDelete (src/common/appbase.cpp:624-641). ScheduleForDestruction defers deletion of any
wxObject, children included; [source] it deletes with plain delete at idle (src/common/appbase.cpp:643-662).
| After… | IsBeingDeleted() |
IsScheduledForDestruction() |
wxWeakRef |
|---|---|---|---|
child->Destroy() returned |
object gone | object gone | null |
tlw->Destroy() returned, before idle |
false | true | non-null |
inside wxEVT_DESTROY handlers and the wx base destructors |
true | false (already removed) | non-null until ~wxTrackable |
inside a top-level window's own derived destructor (deleted at idle or by delete) |
false — SendDestroyEvent() runs later, in ~wxFrameBase or the port's top-level/dialog destructor (src/common/framecmn.cpp:200, src/msw/toplevel.cpp:522, src/gtk/toplevel.cpp:975, src/osx/dialog_osx.cpp:90) |
false | non-null |
wxWeakRef<T> auto-resets "when the object pointed is destroyed"; works for wxEvtHandler/wxWindow
(interface/wx/weakref.h:40-99). [source] The reset happens in ~wxTrackable, the last base
destructor (include/wx/tracker.h) — non-null throughout the destroy sequence and while a top-level window
sits in the pending list. The tracker list is unlocked: main thread only. Orca uses it for the splash
(wxWeakRef<SplashScreen> scrn in GUI_App::on_init_inner), g_delay_webviews (Widgets/WebView.cpp),
DockPanel, GuideFrame.
wxWindowPtr<T> is a shared pointer that calls Destroy() at refcount 0
(interface/wx/windowptr.h:10-26). It does not track: if the window dies another way (parent deletion,
default frame close) the last release calls Destroy() on freed memory. Use it only for parentless,
self-owned top-level windows — the documented ShowWindowModalThenDo idiom (§6).
wxEVT_DESTROY (wxWindowDestroyEvent, interface/wx/event.h:4525-4553): for top-level windows it
is sent "by wxFrame or wxDialog destructor, i.e. after the destructor of the derived class was executed";
for children "just before deleting the window from wxWindow::Destroy()… or from the window destructor if
operator delete was used directly". [source] It derives from wxCommandEvent and propagates to the
parent unless the parent is being deleted (wxWindowBase::TryAfter, src/common/wincmn.cpp:3499-3522), so a
parent's handler fires for every descendant destroyed before it. Derived-class code that must run at
destruction goes in the derived destructor (or call SendDestroyEvent() there).
Pending events and nested loops. [source] ProcessPendingEvents runs queued events and
CallAfters of a top-level window that is already in wxPendingDelete (src/common/appbase.cpp:561-601); only the
actual deletion discards them (~wxEvtHandler → DeletePendingEvents, src/common/event.cpp). Idle
events are skipped for pending-delete windows (src/common/appcmn.cpp:405-428). "Deferred" does not mean "after the
current handler returns": ShowModal(), wxYield() and wxSafeYield() run pending events and
DeletePendingObjects() (src/common/evtloopcmn.cpp:172-192), so a raw pointer to a Destroy()ed
window dies across any of them, and a lambda queued before a modal opens can run while the caller is
still inside ShowModal(). A masked YieldFor — wxProgressDialog::Update/Pulse — runs no idle pass
and only the queued events its categories allow (none on GTK): references/threads-timers-app.md
§Event categories and yields. CallAfter liveness mechanics (self-queued calls are dropped
with their handler; wxGetApp().CallAfter calls are not): references/events.md §CallAfter. Yields:
references/threads-timers-app.md.
OrcaSlicer.
- Alive flag for anything that may run after the window died, including worker threads:
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);, set false in the destructor (PluginsDialog::~PluginsDialog), captured by value, checked inside the lambda. UnlikewxWeakRefit flips at the start of the derived destructor and is thread-safe. - App shutdown gate:
wxGetApp().is_closing(); background-to-UI callbacks check it before posting and again inside theCallAfterlambda (references/threads-timers-app.md).
Pitfalls.
- Rule: For a top-level window that something may have
Destroy()ed, test both flags.// Wrong: if (!tlw->IsBeingDeleted()) use(tlw); // Right: if (!wxTheApp->IsScheduledForDestruction(tlw) && !tlw->IsBeingDeleted()) use(tlw); - Rule: Don't trust
wxWeakRefinside teardown code; combine it withIsBeingDeleted()/IsScheduledForDestruction()or an alive flag. - Rule: In a
wxEVT_DESTROYhandler bound on a parent, compare the event object andSkip().// Wrong: parent->Bind(wxEVT_DESTROY, [this](auto&) { m_child = nullptr; }); // Right: parent->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& e) { if (e.GetEventObject() == m_child) m_child = nullptr; e.Skip(); }); - Rule: After
ShowModal()returns, re-validate anything the modal loop could have destroyed (a popup, a panel rebuilt on a language or preset change) — hold awxWeakRefor alive flag and re-check. - Rule: Make accessors that are reachable from teardown-time events null-safe: guard the pimpl or
child pointer and return
nullptrinstead of dereferencing. Why: on macOS, close and shutdown still deliver events (render, idle, focus) that call back into widget accessors after internals are gone;Plater::get_view3D_canvas3D()crashed on app close until it checked its pimpl.Plater::~Plater() = defaultdestroysstd::unique_ptr<priv> pbefore the basewxWindowdestructor runsDestroyChildren()(src/osx/window_osx.cpp:248,src/gtk/window.cpp:3105,src/msw/window.cpp:421;src/common/wincmn.cpp:586-609), so events raised while children die see a dead pimpl. The null check works only because libc++'s~unique_ptrresets before deleting; the standard does not require that, and touching a member after its destructor ran is UB. Treat the guard as a last line of defence; the real fix is to stop the events (unbind,is_closing()) before teardown.Cite:return p->view3D->get_canvas3d(); // Wrong: crash on close return p ? p->view3D->get_canvas3d() : nullptr; // Righta162e3f031(Plater::get_view3D_canvas3Dinsrc/slic3r/GUI/Plater.cpp).
4 Top-level windows: show, raise, enable, state, geometry
Show / Hide. Show() returns false when nothing changed (interface/wx/window.h:3140-3158).
IsShownOnScreen() = shown and every parent up to the top-level window shown (interface/wx/window.h:3100-3106).
[source] GTK3: wxTopLevelWindowGTK::Show calls GTKSendSizeEventIfNeeded() even when nothing
changes, so a redundant Show(true) can flush a pending size event into layout handlers
(src/gtk/toplevel.cpp:1258-1268). X11 (GTK2/GTK3 without client-side decorations): the first Show()
may be deferred until _NET_FRAME_EXTENTS arrives — IsShown() is already true but the window is not
mapped (src/gtk/toplevel.cpp:1141-1243).
Raise. "only requests the window manager to raise this window… If the window is currently hidden,
this function does not show it", top-level windows only (interface/wx/window.h:3013-3033); true on all ports
since 3.3 (docs/changes.txt:144-146). [source] MSW = ::SetForegroundWindow, subject to the
foreground lock — Windows may only flash the taskbar button (src/msw/toplevel.cpp:650-655); GTK =
gtk_window_present only if shown (src/gtk/toplevel.cpp:1301-1310; during a deferred X11 first show it
already counts as shown); macOS = makeKeyAndOrderFront only if shown (src/osx/nonownedwnd_osx.cpp:289-295, src/osx/cocoa/nonownedwnd.mm:897-899),
which also makes a wxPopupWindow the key window — never Raise() a popup (references/popups-menus.md §5, §10).
Enable. Enable(false) on a parent disables children logically: IsEnabled() reflects ancestors,
IsThisEnabled() the window's own flag (interface/wx/window.h:3060-3070, 3116-3138). [source] On MSW/macOS wx
propagates through NotifyWindowOnEnableChange → DoEnable on non-top-level children — not through the
virtual Enable() — and skips children entirely when a top-level window is disabled, so a modal dialog
does not grey the frame (src/common/wincmn.cpp:1150-1191); GTK relies on native sensitivity. Orca widgets update
their painted state only from their own Enable() override, so disabling an ancestor leaves them
looking enabled: references/orca-widgets.md.
State and geometry (interface/wx/toplevel.h):
| API | Contract / platform note |
|---|---|
Iconize(), Maximize() |
on wxGTK "the change… is not immediate" (:260-274, 335-346); [source] MSW on a hidden window only records the state for the next show (src/msw/toplevel.cpp:661-735) |
Restore() |
on wxGTK does not unmaximize — call Maximize(false) (:394-404) |
ShowFullScreen(show, style) |
also shows a hidden window (:730-750) |
EnableFullScreenView() |
wxOSX only; then ShowFullScreen uses the native Spaces API and only `wxFULLSCREEN_NOTOOLBAR |
RequestUserAttention() |
documented for Win32 (taskbar flash) and wxGTK (:375-392); [source] macOS bounces the dock icon (src/osx/cocoa/nonownedwnd.mm:1306) |
SetIcon/SetIcons |
MSW needs a 16×16 or 32×32 icon; no effect on Wayland — ship a .desktop file (:506-546); [source] no macOS override: stored, never shown |
SetSizeHints/SetMinSize/SetMaxSize |
on a top-level window they also constrain programmatic SetSize() (:576-603) |
SetTransparent() |
on wxGTK call it before the first show (:632-648) |
EnableMaximizeButton/EnableMinimizeButton |
MSW and macOS only (:172-203) |
EnableCloseButton |
all ports, but its result is unreliable on X11, GTK included (:160-170) |
wxEVT_MOVE_START/END |
wxMSW only (:80-87) |
wxEVT_SHOW |
not sent for iconize/restore on MSW (interface/wx/event.h:4905-4914) |
SaveGeometry/RestoreToGeometry, wxPersistentTLW |
serializer-based persistence (:406-492; interface/wx/persist/toplevel.h) — not used by Orca |
Wayland. SetIcon(s) do nothing (interface/wx/toplevel.h:518-521, 539-542); the app id comes from
SetClassName with GTK ≥ 3.24.22 (interface/wx/app.h:765-771). SetPosition()/Move() on a
top-level window is a no-op (the compositor places windows), so CentreOnParent and saved positions are
ignored (observed; recorded in GUI_App::window_pos_restore, not documented by wx). Detection: Slic3r::GUI::is_running_on_wayland(); see references/platforms.md.
OrcaSlicer geometry persistence. Orca uses AppConfig, not wxPersistentTLW:
GUI_App::window_pos_save / window_pos_restore / window_pos_sanitize / window_pos_center (key
window_<name>, a WindowMetrics = screen rect + maximized; restore skips SetPosition on Wayland), and
on_window_geometry(tlw, callback) (GUI_Utils.cpp) to run the callback when geometry is real — MSW
immediately (no wxEVT_SHOW for windows created maximized), Linux on wxEVT_SHOW + CallAfter, macOS on
wxEVT_SHOW. GUI_App::persist_window_geometry(window, default_maximized) saves on
wxEVT_CLOSE_WINDOW (then Skip()) but always uses the key window_mainframe, whatever the window's
name — for any other window call window_pos_save/restore with its own name.
Pitfalls.
- Rule: When restoring a top-level window (the main frame after hiding a popup or overlay frame, or a
singleton dialog being re-opened), call
Show()only if!IsShown(), and callRaise()unconditionally. Why:Raise()never shows a hidden window (3.3 on all ports); on GTK3 a redundantShow(true)runsGTKSendSizeEventIfNeeded(), flushing a pending size event into layout handlers — in Orca this froze the app permanently after hiding the filament-sync popup.Cite:// Wrong mainframe->Show(); mainframe->Raise(); // Right if (!mainframe->IsShown()) mainframe->Show(); mainframe->Raise();dd8cb89f6d(BaseTransparentDPIFrame::on_hide); the same guard inGUI_App::open_terminal_dialog. - Rule: On macOS, after any native modal (
wxFileDialog/wxDirDialog,wxMessageBox/wxMessageDialog) or a genericwxProgressDialogopened from a secondary top-level dialog, re-raise that dialog with a deferred, liveness-guardedRaise()(CallAfter+ alive flag +IsShown()). Why: when the native panel closes, macOS re-activates the app's main window instead of the dialog that opened it, burying the dialog behind the main frame (observed; not documented by wx). wx compensates only for its ownwxDialogmodals —EndModalraises the parent (src/osx/dialog_osx.cpp:191-202) — whilewxFileDialog::ShowModalruns[panel runModal]with no such step (src/osx/cocoa/filedlg.mm:597-620), and on macOSwxProgressDialogis the generic dialog (only MSW has a native one,include/wx/progdlg.h:30-37), normally shown modeless behind a disabler and destroyed, never ended throughEndModal. TheRaise()is deferred to run after the modal has fully torn down, and guarded because the dialog may be destroyed while queued.Cite:void PluginsDialog::restore_z_order() { wxGetApp().CallAfter([this, alive = m_alive]() { if (alive->load(std::memory_order_acquire) && IsShown()) Raise(); }); }0a0d59b76b(PluginsDialog::restore_z_order; also passed as therestorecallback ofdetail::run_off_thread_with_progress). - Rule: Never position windows by absolute coordinates on Wayland, and never expect
SetIconto show on macOS or Wayland.
5 Window styles
Contract.
wxDEFAULT_FRAME_STYLE=wxSYSTEM_MENU | wxRESIZE_BORDER | wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxCLOSE_BOX | wxCAPTION | wxCLIP_CHILDREN(interface/wx/toplevel.h:55-61);wxDEFAULT_DIALOG_STYLE=wxCAPTION | wxSYSTEM_MENU | wxCLOSE_BOX,wxSYSTEM_MENUunused under Unix (interface/wx/dialog.h:20, 99-101). Non-resizable frame:wxDEFAULT_FRAME_STYLE & ~(wxRESIZE_BORDER | wxMAXIMIZE_BOX).wxMINIMIZE_BOX,wxMAXIMIZE_BOX,wxCLOSE_BOXimplicitly enablewxCAPTION"on most systems" (interface/wx/dialog.h:94-114,interface/wx/frame.h:61-80).wxMAXIMIZE_BOXis ignored on wxGTK withoutwxRESIZE_BORDER(interface/wx/frame.h:75-78). ThewxMAXIMIZEstyle works on Windows and GTK only;wxICONIZE/wxMINIMIZEon Windows only (interface/wx/frame.h:59-74).wxSTAY_ON_TOP: above all other windows.wxFRAME_FLOAT_ON_PARENT: above its parent only, "must have a non-null parent".wxFRAME_NO_TASKBAR: no taskbar button on Windows/GTK (GTK only with_NET_WM_STATE_SKIP_TASKBARsupport).wxFRAME_TOOL_WINDOW: small title bar, no taskbar button (interface/wx/frame.h:82-106).- Borders:
wxBORDER_NONE/SIMPLE/SUNKEN/RAISED/STATIC(MSW)/THEME;wxTRANSPARENT_WINDOWis obsolete and does nothing (interface/wx/window.h:191-220). - Extra styles (
SetExtraStyle, some must precede two-stepCreate):wxWS_EX_BLOCK_EVENTS,wxWS_EX_TRANSIENT("Don't use this window as an implicit parent… risk of creating a dialog/frame with this window as a parent, which would lead to a crash"),wxWS_EX_PROCESS_IDLE,wxWS_EX_PROCESS_UI_UPDATES(interface/wx/window.h:262-290). Dialogs setwxWS_EX_BLOCK_EVENTSby default (src/common/dlgcmn.cpp:124-127): command events from inside a dialog never reach its parent frame; frames do not block.
Platforms. [source]
| Style | MSW | macOS | GTK |
|---|---|---|---|
| MIN/MAX/CLOSE_BOX | force WS_CAPTION (src/msw/toplevel.cpp:132-137) — a custom title bar must strip it |
��” | — |
frame with a parent, no FLOAT_ON_PARENT |
unowned; gets its own taskbar button (WS_EX_APPWINDOW) unless wxFRAME_NO_TASKBAR (src/msw/toplevel.cpp:193-244) |
— | — |
wxFRAME_FLOAT_ON_PARENT |
native owner = GetHwndOf(parent) as given (MSWGetParent, src/msw/toplevel.cpp:212-240) — pass a top-level window |
NSFloatingWindowLevel, a global level: floats above other apps' windows too (src/osx/cocoa/nonownedwnd.mm:835-876) |
transient_for the parent's top-level window, set only in Create (src/gtk/toplevel.cpp:774-782) |
wxFRAME_TOOL_WINDOW |
small caption, no taskbar | NSFloatingWindowLevel |
no taskbar |
wxSTAY_ON_TOP |
WS_EX_TOPMOST |
NSModalPanelWindowLevel |
keep-above + transient_for in Create (src/gtk/toplevel.cpp:774-791); runtime change honoured |
runtime SetWindowStyleFlag |
— | re-levels the window (src/osx/cocoa/nonownedwnd.mm:1038-1056) |
updates only STAY_ON_TOP and NO_TASKBAR (src/gtk/toplevel.cpp:1943-1968) |
macOS dialogs are not attached as Cocoa child windows (no addChildWindow, src/osx/cocoa/nonownedwnd.mm:947-991),
so they do not move with their parent; non-tool windows get setHidesOnDeactivate:NO.
OrcaSlicer.
DPIAware's constructor defaultsstyle = wxDEFAULT_FRAME_STYLEandname = wxFrameNameStrfor dialogs too:DPIDialog(parent, id, title)without a style is resizable with min/max boxes. PasswxCAPTION | wxCLOSE_BOXorwxDEFAULT_DIALOG_STYLE(§7).MainFrameusesBORDERLESS_FRAME_STYLE(MainFrame.cpp: min/max/close boxes, pluswxRESIZE_BORDERoff Apple, nowxCAPTION) and paints its own title bar: MSW strips theWS_CAPTIONthat wx adds, macOS callsset_miniaturizable, GTK addsResizeEdgePanels. Custom titlebar rules:references/platforms.md.TextureProjectorFrameis the model for a tool frame floating above the main window:wxCAPTION | wxRESIZE_BORDER | wxCLOSE_BOX | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT, parented tomainframe,SetTransparentbefore the first show.
Pitfalls.
- Rule: Remove style bits with
& ~flag;!flagis 0.// Wrong: !wxCAPTION | !wxCLOSE_BOX | wxBORDER_NONE // == wxBORDER_NONE by accident // Right: wxBORDER_NONE // or: wxDEFAULT_FRAME_STYLE & ~wxCAPTION - Rule: Parent a
wxFRAME_FLOAT_ON_PARENTframe to a top-level window, never to a panel or NULL. Why: with NULL wx asserts (silently in Orca) and ignores the flag (wxTopLevelWindowMSW::MSWGetParent). With a panel, the ports disagree on the owner — MSW passes the panel's own HWND (GetHwndOf(parent)), GTK resolveswxGetTopLevelParent(parent)(src/gtk/toplevel.cpp:774-782) — and the frame is deleted with the panel (§2). A top-level parent gives every port the same owner and lifetime. - Rule: Don't use
wxSTAY_ON_TOPorwxFRAME_FLOAT_ON_PARENTto keep a tool window "above the app" on macOS without accepting that it also floats above other applications.
6 Dialogs and modality
Contract (interface/wx/dialog.h).
- Stack allocation is the sanctioned form for a modal dialog: "the modal dialog is one of the very few
examples of wxWindow-derived objects which may be created on the stack… no need to call Destroy()";
heap form
ShowModal()thendlg->Destroy()(interface/wx/dialog.h:61-88). ShowModal(): "Program flow does not return until the dialog has been dismissed with EndModal()… ShowModal() can't be called twice without intervening EndModal() calls… creates a temporary event loop… also results in a call to wxApp::ProcessPendingEvents()" (interface/wx/dialog.h:597-618). Timers,CallAfters, idle-time deletion, socket and worker events all run inside it.EndModal(retCode)sets the valueShowModal()returns (interface/wx/dialog.h:340-349).Show(false): "The preferred way of dismissing a modal dialog is to use EndModal()" (interface/wx/dialog.h:586-595).ShowWindowModal()is "only fully implemented in wxOSX… under the other platforms it behaves like ShowModal()" (interface/wx/dialog.h:620-638).ShowWindowModalThenDo(functor): the dialog must outlive the functor — hold it in awxWindowPtrcaptured by value (interface/wx/dialog.h:640-678).- "you shouldn't show a modal dialog from a mouse click event handler as this would break the mouse capture
state" — defer with
CallAfter(interface/wx/event.h:490-497). [source] GTK'sShowModalreleases any mouse capture first (GTKReleaseMouseAndNotify,src/gtk/dialog.cpp:137). Capture rules:references/mouse-keyboard-focus.md.
[source] facts.
wxDialogBase::EndDialog(rc)(protected,src/common/dlgcmn.cpp:361-367) =IsModal() ? EndModal(rc) : Hide(): ends either kind from inside the class.- Modal loops nest and unwind LIFO on every port: MSW
wxEventLoopManual::DoStoponly wakes the loop (src/common/evtloopcmn.cpp:388-401), GTK re-entersgtk_main()until its ownm_shouldExit(src/gtk/evtloop.cpp:58-90), macOS keeps a LIFOs_modalStack(src/osx/dialog_osx.cpp:47-60) and stops via[NSApp abortModal], which hits the innermost session (src/osx/cocoa/evtloop.mm:453-456). Every port'sEndModalstops its loop throughwxEventLoopBase::Exit(), whosewxCHECK_RET(IsRunning())returns silently unless that loop is the active (innermost) one (src/common/evtloopcmn.cpp:91-96; MSW viaHide()→wxDialogModalData::ExitLoop,src/msw/dialog.cpp:64-67, 199-207, 261-268; macOSsrc/osx/dialog_osx.cpp:191-194; GTK'sEndModaltestsIsRunning()itself,src/gtk/dialog.cpp:199-202). Ending a lower dialog first therefore hides it but never tells its loop to exit: itsShowModal()does not return even after every dialog above it has ended. On MSW itswxWindowDisabler(owned by the genericwxModalEventLoopthat MSW'sShowModalruns, deleted only in itsOnExit(),include/wx/evtloop.h:377-396) stays alive too, so the other top-level windows remain disabled. GTK uses no disabler: modality is each dialog's owngtk_window_set_modalgrab (src/gtk/dialog.cpp:158). The same silentExit()no-op applies to nestedwxEventLoops:references/threads-timers-app.md§Nested event loops. - Return codes are ids:
wxID_OK = 5100,wxID_CANCEL,wxID_APPLY,wxID_YES,wxID_NO, … (include/wx/defs.h:1847).wxYES 0x2,wxOK 0x4,wxNO 0x8,wxCANCEL 0x10,wxAPPLY 0x20,wxCLOSE 0x40are style bits (include/wx/defs.h:1665-1672) —EndModal(wxCANCEL)makesShowModal() == wxID_CANCELfalse. - Whichever
EndModalruns last beforeShowModal()returns sets the result: every port'sEndModalcallsSetReturnCodeunconditionally andShowModal()returnsGetReturnCode().
Per-port modal behaviour. [source]
MSW (src/msw/dialog.cpp:195-268) |
macOS (src/osx/dialog_osx.cpp:106-202) |
GTK (src/gtk/dialog.cpp:60-205) |
|
|---|---|---|---|
Hide()/Show(false) on a modal dialog |
exits the loop; ShowModal() returns the current code (0 if none set) |
does not exit: clears the modality, orders the window out; ShowModal() stays blocked behind an invisible dialog, and wx's EndDialog path then takes the Hide() branch |
calls the virtual EndModal(wxID_CANCEL), overwriting any code |
EndModal() on a modeless dialog |
sets the code and hides (assert compiled out) | sets the code, hides, raises the parent | sets the code, then wxFAIL + return: the dialog stays visible |
IsModal() between EndModal() and the return of ShowModal() |
true (m_modalData, include/wx/msw/dialog.h:48) |
false | false |
EndModal() raises the parent |
no | yes ("Prevent app frame from taking z-order precedence") | no |
| native owner / transient | fixed at Create (§1) |
none (not a child window) | transient_for set in ShowModal() |
| how other windows are blocked | wxWindowDisabler in a generic wxModalEventLoop (src/msw/dialog.cpp:70) |
Cocoa modal session (src/osx/cocoa/evtloop.mm:424-456) |
gtk_window_set_modal grab; mouse capture released first |
Buttons, ESC and the close box. [source]
SetAffirmativeId(id)(defaultwxID_OK): that button runsValidate()+TransferDataFromWindow()and closes with the id (interface/wx/dialog.h:480-494).SetEscapeId(id): defaultwxID_ANY= thewxID_CANCELbutton if present, else the affirmative one;wxID_NONE= ignore ESC; native dialogs cannot be customized (interface/wx/dialog.h:496-515).CreateStdDialogButtonSizermakeswxButtons and sets the affirmative id (interface/wx/dialog.h:291-304) — Orca usesDialogButtonsinstead (§8).- Routing is by id, not type:
wxDialogBase::OnButton(static table,src/common/dlgcmn.cpp:455-480) handles anywxEVT_BUTTONthat propagates to the dialog — affirmative id →AcceptAndClose()(Validate()+TransferDataFromWindow()thenEndDialog(id)),wxID_APPLY→ validate + transfer, no close, escape id orwxID_CANCEL→EndDialog(wxID_CANCEL), anything else skipped. An OrcaButtonwithwxID_OKorwxID_CANCELand no handler (or a handler thatSkip()s) closes the dialog by itself; a handler bound on the button that does notSkip()suppresses it. - ESC →
wxDialogBase::OnCharHook→SendCloseButtonClickEvent(): tries the escape id (wxID_CANCEL), then the affirmative id, throughEmulateButtonClickIfPresent, which doeswxDynamicCast(FindWindow(id), wxButton)and requires the button enabled and shown (src/common/dlgcmn.cpp:387-453). Orca'sButtonderives fromStaticBox : wxWindow, so emulation finds nothing: in a plainwxDialogwith Orca buttons ESC does nothing; in aDPIDialog,DPIAware's own char hook turns ESC intoClose()first (§7). - Close box (and
Close()) →wxDialogBase::OnCloseWindow(src/common/dlgcmn.cpp:525-559): if shown, trySendCloseButtonClickEvent(); when that finds nowxButton,EndDialog(wxID_CANCEL). The Cancel button's own handler never runs on this path.
Modeless dialogs. Show(); the close box only hides them (above). Long-lived modeless windows (monitor
pages, progress dialogs, plugin dialogs) either destroy themselves in a close handler or are deliberately
hide-on-close and reused (§2 shapes).
Modality helpers.
wxWindowDisabler(winToSkip, winToSkip2)disables all shown and enabled top-level windows except the skipped ones and re-enables them in its destructor (interface/wx/utils.h:58-111). MSW: a skipped window that appears in the taskbar lets the user close the whole app from the taskbar (interface/wx/utils.h:93-99). [source] The destructor re-enables every top-level window not recorded as skipped — including ones created after the disabler (src/common/utilscmn.cpp:1527-1546); on macOS the constructor begins a Cocoa modal session and showswinToSkipif it is not on screen (src/osx/cocoa/evtloop.mm:458-514, 573-584). Keep disablers scoped and strictly nested.wxModalDialogHook::GetOpenCount()(since 3.3.0,interface/wx/modalhook.h:118-126) counts every open modal — generic and native message/file/colour/font/print dialogs all useWX_HOOK_MODAL_DIALOG. Use it for "is any modal open" in new code.wxFrame::SetWindowModality(wxWindowMode)(new in 3.3.2): call before showing;AppModal/WindowModal(interface/wx/frame.h:318-329). [source] Applies immediately in the call, ends at the first hide, addswxFRAME_NO_TASKBARand removeswxMINIMIZE_BOX(src/common/framecmn.cpp:159-195, 304-339); it does not block — the caller keeps running.- Orca's pseudo-modal
ParamsDialog(filament/printer settings): shown modeless withPopup()→Show(), creates a heapwxWindowDisabler(this)in itswxEVT_SHOWhandler and deletes it on hide; its close handler validates (vetoes when validation fails andCanVeto()), hides, and never destroys, so the hosted tabs stay reusable;ParamsDialog::Popup()callsReparent(mainframe)on Windows before showing.
Pitfalls.
- Rule: Use
wxID_*codes withEndModal.// Wrong: EndModal(wxCANCEL); EndModal(wxCLOSE); EndModal(wxOK); // Right: EndModal(wxID_CANCEL); EndModal(wxID_CLOSE); EndModal(wxID_OK); - Rule: Dismiss a modal dialog with
EndModal(rc), notHide()/Show(false). Why: macOS keepsShowModal()blocked behind an invisible dialog; GTK forceswxID_CANCEL; MSW returns whatever code was last set. - Rule:
EndModal()only on a modal dialog; a modeless one is hidden/closed/destroyed (orEndDialog). Why: GTK ignoresEndModalon a modeless dialog — it stays on screen there and only there. - Rule: Never
Destroy()a modal dialog whose loop is still running;EndModal(rc)first, thenDestroy()(heap) — from the caller onceShowModal()has returned, or straight afterEndModal()as forced teardown does, relying on a top-levelDestroy()only queuing the deletion. NeverDestroy()a stack dialog. Cite:WebDialog::destroy_silently(EndModal(wxID_CANCEL)thenDestroy()). - Rule: Close nested modal dialogs innermost-first.
Why: ending a lower one hides it but leaves its
ShowModal()stranded (and, on MSW, its disabler alive) on every port — see the LIFO bullet above. - Rule: Show a modal from a mouse handler only through
CallAfter.The same applies to// Wrong: m_btn->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent&) { MyDialog dlg(this); dlg.ShowModal(); }); // Right: m_btn->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) { e.Skip(); CallAfter([this] { MyDialog dlg(this); dlg.ShowModal(); }); });EndModalbound towxEVT_LEFT_DOWN; bindwxEVT_BUTTONon an OrcaButtoninstead. - Rule: No window work (create, show, raise, destroy, modal dialogs) on the stack of a
wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVEDhandler — WebKitGTK and WKWebView deliver it synchronously. Detail and theWebViewHostDialogcontract:references/webview-gl-aui-media.md. - Rule: Scope a
wxWindowDisableron the stack or tie it strictly to show/hide; never let heap disablers outlive their window or end out of order (on macOS each one is a modal session).
7 DPIDialog and DPIFrame
template<class P> class DPIAware : public P, public wxInspector::wxInspectable (GUI_Utils.hpp);
typedef DPIAware<wxFrame> DPIFrame; class DPIDialog : public DPIAware<wxDialog>. New Orca dialogs and
frames derive one of them. Public API: scale_factor(), prev_scale_factor(), em_unit(),
normal_font(), enable_force_rescale(); on Windows force_color_changed(). Subclasses implement the pure
virtual on_dpi_changed(const wxRect&) and may override on_sys_color_changed().
What the constructor does (one-step construction only; signature
(parent, id, title, pos = wxDefaultPosition, size = wxDefaultSize, style = wxDEFAULT_FRAME_STYLE, name = wxFrameNameStr)):
| Step | Detail | Owner of the topic |
|---|---|---|
| scale factor, normal font | from get_dpi_for_window(this); SetFont(m_normal_font) except on macOS (#ifndef __WXOSX__, avoids name cutting in ObjectList) |
references/dpi-bitmaps-fonts.md |
CenterOnParent() |
runs before any content exists — re-centre after fitting | §8 |
SetupInspectorAccelerator(this) |
wxInspector toggle (Ctrl+Shift+I) in builds without WXINSPECTOR_DISABLE; a later SetAcceleratorTable() on the window replaces it |
references/orca-widgets.md |
MSW update_dark_ui(this) |
no-op (its body only reads the dark flag) | references/colours-dark-mode.md |
update_em_unit() |
non-GTK max(10, 10·scale); GTK max(10, GetTextExtent("m").x - 1) |
references/dpi-bitmaps-fonts.md |
bind wxEVT_DPI_CHANGED (not macOS) |
rescales (Freeze → font/em → on_dpi_changed → Layout → Thaw) when the scale changed and no monitor drag is in progress, and does not Skip() — so wx's default top-level handler (scale the window size by the DPI ratio) never runs (interface/wx/event.h:3572-3583) |
references/dpi-bitmaps-fonts.md |
bind wxEVT_MOVE_START/END (MSW-only events) |
defer rescale while the window is dragged between monitors | references/dpi-bitmaps-fonts.md |
bind wxEVT_SYS_COLOUR_CHANGED |
non-Windows: update_dark_config() + on_sys_color_changed() + Skip(); Windows: swallowed |
references/colours-dark-mode.md |
dialogs only: bind wxEVT_CHAR_HOOK |
WXK_ESCAPE → this->Close(), never skipped; other keys skipped |
below |
ESC in a DPIDialog. ESC = the close box: your wxEVT_CLOSE_WINDOW handler if any, else
wxDialogBase::OnCloseWindow → EndDialog(wxID_CANCEL) (modal: ShowModal() returns wxID_CANCEL;
modeless: hidden). The dynamic DPIAware hook runs before wx's static wxDialogBase::OnCharHook, so
SetEscapeId() has no effect on ESC. A MessageDialog with only Yes/No buttons therefore returns
wxID_CANCEL on ESC or the close box — callers must treat anything but wxID_YES as "no".
dialogStack and the EndModal guard. DPIAware::ShowModal() (same signature as the virtual
wxDialog::ShowModal, so it overrides it) pushes this on the global std::deque<wxDialog*> dialogStack
(GUI_Utils.cpp) and pops it after the loop. DPIDialog::EndModal(retCode) refuses — logs
"DPIAware::EndModal Error…", returns, the dialog stays open and modal — when the stack is non-empty and
this is not dialogStack.front(). It is a guard for the LIFO rule of §6, not a fix for a wx bug: wx
cannot end a lower loop first on any port (the dialog would be hidden with its ShowModal() stranded),
and the guard turns that into a no-op. It only knows
DPIDialogs that went through DPIAware::ShowModal(); native and plain-wx modals are not on the stack.
Consumers: GUI_App::ShowDownNetPluginDlg (searches the stack to avoid a second instance) and the
wxEVT_QUERY_END_SESSION handler in GUI_App::on_init_inner (sends a vetoable close to mainframe, then
EndModal(wxID_ABORT) on every stacked dialog — with the guard only the innermost actually ends). For "is
any modal open" in new code prefer wxModalDialogHook::GetOpenCount().
There is no Destroy() override: heap DPIDialogs follow the stock ShowModal(); Destroy(); or the
stack form.
Pitfalls.
- Rule: Pass the style explicitly.
// Wrong: MyDialog(wxWindow* p) : DPIDialog(p, wxID_ANY, _L("Title")) {} // resizable, min/max boxes // Right: MyDialog(wxWindow* p) : DPIDialog(p, wxID_ANY, _L("Title"), wxDefaultPosition, // wxDefaultSize, wxCAPTION | wxCLOSE_BOX) {} - Rule: Call
CenterOnParent()again afterSetSizerAndFit()(MsgDialog::finalizedoes). Why: theDPIAwareconstructor centred the empty, default-sized window. - Rule: An override of
ShowModal()must callDPIDialog::ShowModal()(asRichMessageDialog::ShowModal→MsgDialog::ShowModal(),UnsavedChangesDialog,TextureImportDialogdo), neverwxDialog::ShowModal(). Why: a dialog that bypasses the push is not ondialogStack; when it is opened above anotherDPIDialog, its ownEndModalis refused and it cannot close. - Rule: To keep ESC from closing a
DPIDialog, veto in the close handler or bind your ownwxEVT_CHAR_HOOKthat swallowsWXK_ESCAPE;SetEscapeId(wxID_NONE)does nothing here.// Right: bound in the subclass constructor, i.e. after DPIAware's hook, so it runs first Bind(wxEVT_CHAR_HOOK, [](wxKeyEvent& e) { if (e.GetKeyCode() != WXK_ESCAPE) e.Skip(); }); - Rule: A child that binds the dialog's
wxEVT_DPI_CHANGEDmust be bound afterDPIAware(any child created in the subclass constructor is) and mustSkip(). Why: dynamic handlers run most-recently-bound first (interface/wx/event.h:592-593) andDPIAware's handler does not skip;DialogButtonsbinds its parent's event in its constructor and callsSkip(), so it runs and thenDPIAwarerescales. It unbinds in its destructor — the model for any widget that binds an event on another window.
8 The Orca dialog recipe
Exemplars: src/slic3r/GUI/CloneDialog.cpp (minimal; DialogButtons with a left-aligned extra button,
an Enter-key wxEVT_CHAR_HOOK that synthesizes the OK wxEVT_BUTTON; its OK handler's wxYield() loop
inside a frozen plater is not a pattern to copy), PurgeModeDialog.cpp (custom-painted clickable card
panels; OK/Cancel Buttons that close purely by id through wxDialogBase::OnButton;
on_dpi_changed with min size + Fit()/Refresh(); its msw_buttons_rescale call also resizes those
Orca Buttons, so leave it out when copying), FilamentPickerDialog.cpp
(larger; helper Create*() methods returning sizers; positions itself next to the sidebar).
class MyDialog : public DPIDialog
{
public:
explicit MyDialog(wxWindow* parent)
: DPIDialog(parent ? parent : static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY,
_L("My Dialog"), wxDefaultPosition, wxDefaultSize,
wxCAPTION | wxCLOSE_BOX) // or wxDEFAULT_DIALOG_STYLE; never omit
{
SetBackgroundColour(*wxWHITE); // light design colour, dark-mapped at the end
SetFont(Label::Body_14);
auto* sizer = new wxBoxSizer(wxVERTICAL);
// ... children: Orca widgets, sizes via FromDIP(n), labels via _L() ...
sizer->Add(content_sizer, 1, wxEXPAND | wxALL, FromDIP(10));
auto* dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); // NOT pre-translated
dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { /* apply */ EndModal(wxID_OK); });
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { /* cancel work */ EndModal(wxID_CANCEL); });
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) { /* same cancel work */ e.Skip(); }); // ESC, close box
sizer->Add(dlg_btns, 0, wxEXPAND);
SetSizerAndFit(sizer);
CenterOnParent(); // DPIAware centred the empty window
wxGetApp().UpdateDlgDarkUI(this); // always last, after all children exist
}
protected:
void on_dpi_changed(const wxRect&) override; // rescale bitmaps/widgets, min sizes, GetSizer()->SetSizeHints(this), Refresh()
};
// caller
MyDialog dlg(this);
if (dlg.ShowModal() == wxID_OK) { /* read results */ }
Conventions and why.
DPIDialogsuppliesscale_factor(),em_unit(), DPI-change handling and ESC handling, and requireson_dpi_changed(const wxRect&). Typical body:Rescale()on Orca widgets,msw_rescale()(+SetBitmap()on the displaying control) onScalableBitmaps, min sizes reset in em units orFromDIP, thenGetSizer()->SetSizeHints(this)(or a dialogSetMinSize+Fit()) andRefresh().msw_buttons_rescale(this, em_unit(), ids)(min height 2.5 em on the windows with those ids) is for rawwxButtons only: it also overrides the style height of OrcaButtons carrying those ids, including theDialogButtonsOK/Cancel. An empty override is acceptable only for trivially simple dialogs (CloneDialog; on MSW it then keeps its old pixel size). Detail:references/dpi-bitmaps-fonts.md§DPIAware rescale path,references/sizers-layout.md§Layout on DPI change.- Parent fallback
parent ? parent : wxGetApp().mainframe— never orphan a dialog (§1). - Title and labels through
_L()(references/strings-i18n-files.md). SetBackgroundColour(*wxWHITE)andSetFont(Label::Body_14)at the top. Setting a font on a dialog is fine on every platform; the macOS "noSetFont" concern isDPIAware's per-DPI default font (ObjectList name cutting), whichDPIAwarealready skips there.- Light design colours everywhere;
wxGetApp().UpdateDlgDarkUI(this)as the last line maps them for dark mode and themes the native parts on Windows; child panels built later useUpdateDarkUIWin. Hand-picked colours go throughStateColor::darkModeColorFor(wxColour("#...")). Detail:references/colours-dark-mode.md. SetSizerAndFit(sizer)on the dialog (AGENTS.md rule); whereSetSizermust come first, follow withsizer->SetSizeHints(this)(MsgDialog::finalizedoesGetSizer()->SetSizeHints(this)). Child panels use plainSetSizer. Detail:references/sizers-layout.md.CenterOnParent()after sizing. Dialogs may set the app icon (SetIcon(wxIcon(encode_path(icon_path.c_str()), wxBITMAP_TYPE_ICO))withresources_dir()/images/OrcaSlicerTitle.ico, asPurgeModeDialogdoes — no effect on macOS or Wayland, §4) and clamp their size withSetMinSize/SetMaxSize(FromDIP(...)).- Modality: construct on the caller's stack,
ShowModal(), read thewxID_*result. Heap form:auto* dlg = new MyDialog(this); dlg->ShowModal(); dlg->Destroy();.Show()is for modeless, long-lived windows (monitor pages, progress dialogs, plugin dialogs), with a close shape from §2.
DialogButtons and how its buttons close the dialog. Slic3r::GUI::DialogButtons(parent, non_translated_labels, primary_btn_translated_label = "", left_aligned_buttons_count = 0) is a wxPanel
of Orca Buttons. It calls _L() on each label itself and assigns a stock id by matching the lower-cased
label (catalogue: references/orca-widgets.md).
| Label | Id | Closes the dialog with no handler bound? |
|---|---|---|
| OK | wxID_OK |
yes — AcceptAndClose() → EndDialog(wxID_OK) (affirmative id) |
| Cancel | wxID_CANCEL |
yes — EndDialog(wxID_CANCEL) |
| Apply, Confirm | wxID_APPLY (both) |
no — Validate() + TransferDataFromWindow() only |
| Yes, No, Save, Delete, … | wxID_YES, wxID_NO, wxID_SAVE, wxID_DELETE, … |
no |
| anything unknown | auto id | no — fetch with GetButtonFromLabel(_L("…")) or GetButtonFromIndex(i) |
Getters (GetOK, GetCANCEL, …), the full label → id map, and how the primary (Confirm-styled) and alert
buttons are chosen — in numeric id order, so {"Save", "OK"} makes Save primary — are in
references/orca-widgets.md §DialogButtons.
Pitfalls.
- Rule: Run cancel cleanup on every close path — the Cancel handler and
wxEVT_CLOSE_WINDOW(or afterShowModal()returns, where every path ends). Why: ESC (viaDPIAware→Close()) and the close box go throughwxDialogBase::OnCloseWindow→EndDialog(wxID_CANCEL); wx's button emulation needs a realwxButton, so an Orca Cancel button's handler never runs on those paths (§6). - Rule: Bind every button whose default handling is not what you want; Yes/No/Apply/Confirm/custom
buttons never close by themselves, and an OK handler that does real work ends the dialog itself
(
EndModal(wxID_OK)) orSkip()s to the defaultAcceptAndClose(). - Rule: Pass untranslated labels to
DialogButtons.// Wrong: new DialogButtons(this, {_L("OK"), _L("Cancel")}); // double translation; no stock ids in non-English UIs // Right: new DialogButtons(this, {"OK", "Cancel"}); - Rule:
UpdateDlgDarkUI(this)runs once, after every child exists; children added later are themed withUpdateDarkUIWin(child). - Rule: Don't create dialogs in a constructor of their parent or before the main frame is shown when MSW
ownership matters (§1); don't keep
CloneDialog'swxYield()loop pattern — long work goes to a job (references/threads-timers-app.md).
9 Message boxes: the MsgDialog family
Rule. Never wxMessageBox/wxMessageDialog/wxRichMessageDialog once the GUI exists; use the themed
replacements in src/slic3r/GUI/MsgDialog.hpp (Slic3r::GUI), rooted in MsgDialog : DPIDialog (logo on the
left, content on the right, Button row underneath, dark-mode and DPI aware). Why: on MSW the TaskDialog-based
native boxes (wxMessageBox, wxMessageDialog, wxRichMessageDialog, wxProgressDialog) ignore dark mode
(interface/wx/app.h:1436-1446), and native boxes cannot match Orca's look. wxMessageBox remains only for
failures before the GUI exists (e.g. GUI_App::load_language). Native message-box style limits:
references/strings-i18n-files.md. Return-value trap when reading old code: wxMessageBox() returns
wxYES/wxNO/wxCANCEL/wxOK/wxHELP, while ShowModal() returns wxID_YES/… (interface/wx/msgdlg.h:269-276 vs 309-311).
| Class | Constructor | Notes |
|---|---|---|
MessageDialog |
(parent, message, caption = "", style = wxOK, forward_str = "", link_text = "", link_callback = nullptr) |
default choice; first four parameters match wxMessageDialog (style wxOK, wxCANCEL, wxYES_NO, wxICON_*); empty caption → " info" |
RichMessageDialog |
(parent, message, caption = "", style = wxOK) |
adds ShowCheckBox(text, checked) / IsCheckBoxChecked() (a "Don't show again" check box added in its ShowModal()); its SetYesNoLabels/SetYesNoCancelLabels/SetOKLabel/SetOKCancelLabels/SetHelpLabel only store strings and never relabel a button |
WarningDialog |
(parent, message, caption = "", style = wxOK) |
empty caption → " warning" |
ErrorDialog |
(parent, msg, has_code_excerpts) |
caption " error"; has_code_excerpts renders source/caret line pairs monospaced (placeholder-parser errors) |
InfoDialog |
`(parent, title, msg, is_marked = false, style = wxOK | wxICON_INFORMATION)` |
DeleteConfirmDialog |
(parent, title, msg) |
a plain DPIDialog, not a MsgDialog: Delete → wxID_OK, Cancel → wxID_CANCEL |
DownloadDialog and FilamentWarningDialog are single-purpose MsgDialog subclasses; read their
constructors before reusing them.
Behaviour (MsgDialog.cpp):
- Window style is always
wxDEFAULT_DIALOG_STYLE; thestyleargument only selects buttons and icon. A NULL parent becomeswxGetApp().mainframe. MsgDialog::apply_style:wxOK→ OK,wxYES→ Yes,wxNO→ No,wxCANCEL→ Cancel; Orca's use ofwxFORWARDadds a "Go to <forward_str>" button and turns OK into "Later" (wxID_CANCEL). Every button's handler isEndModal(btn_id), so compare withwxID_OK/wxID_YES/wxID_NO/wxID_CANCEL— and withwxFORWARD(the style bit0x2000, used as the button id) for "Go to". OK/Yes/Go-to are Confirm-styled and focused;wxNO_DEFAULT,wxCANCEL_DEFAULT,wxHELPandwxSTAY_ON_TOPare ignored.- Icon from the style:
wxAPPLY→ "completed",wxICON_WARNING→ "exclamation",wxICON_INFORMATION→ "info",wxICON_QUESTION→ "question", otherwise the app logo;wxICON_ERRORgreys it. - ESC and the close box return
wxID_CANCELwhatever the buttons (§7). - Content (
add_msg_content): plain text → a wrappedLabelin a scrolled window, so&is a mnemonic — escape user text (references/strings-i18n-files.md); withlink_text/link_callback,is_marked, code excerpts, or a message containing<tr>→ awxHtmlWindowwith the textxml_escaped (is_markedkeeps</>so markup works) and\n→<br>. - Base helpers:
SetButtonLabel(wxID_*, label, set_focus = false)relabels a created button;AddButton(id, label, set_focus = false)appends a choice button that also ends withEndModal(id);show_dsa_button(title = {})adds a "Don't show again"CheckBoxthat postsEVT_CHECKBOX_CHANGE(int = checked) to the dialog;get_checkbox_state(). finalize()(called by each subclass constructor):SetSizeHints,Layout,Fit,CenterOnParent,UpdateDlgDarkUI— the recipe of §8 in one call; a customMsgDialogsubclass ends its constructor with it.
Free helpers (GUI.hpp): show_error(parent, msg, has_code_excerpts = false) is asynchronous — it
shows an ErrorDialog from wxGetApp().CallAfter, capturing the raw parent; show_info(parent, msg, title) and warning_catcher(parent, msg) are synchronous MessageDialogs. The const char*/std::string
overloads decode UTF-8.
Pitfalls.
- Rule: Relabel buttons with
SetButtonLabel, not theRichMessageDialog::Set*Labelsmethods.// Wrong: dlg.SetYesNoLabels(_L("Discard"), _L("Keep")); // stored, never shown // Right: dlg.SetButtonLabel(wxID_YES, _L("Discard")); dlg.SetButtonLabel(wxID_NO, _L("Keep")); - Rule: Test for the positive answer; everything else (No, Cancel, ESC, close box) is "no".
// Wrong: if (dlg.ShowModal() != wxID_NO) discard(); // Right: if (dlg.ShowModal() == wxID_YES) discard(); - Rule: Never compare a
MsgDialogresult withwxYES/wxOK(style bits), exceptwxFORWARDfor "Go to". - Rule: Give
show_errora parent that outlives the deferred call (the main frame, ornullptr, whichMsgDialogmaps to it), not a dialog that may close first; don't rely on the error being visible whenshow_errorreturns.
10 Overlay frames: BaseTransparentDPIFrame
BaseTransparentDPIFrame : DPIFrame (BaseTransparentDPIFrame.hpp/.cpp) is the base for small
semi-transparent prompt frames (text, OK/Cancel Buttons, optional timed fade-out via
DisappearanceMode::TimedDisappearance). Its lifetime design:
- Parent is always
wxGetApp().mainframe; borderless. wxEVT_CLOSE_WINDOW→on_hide()on every close, forced or not (it neither vetoes nor destroys): stop the refresh timer,Hide(), then restore the main frame withif (!mainframe->IsShown()) mainframe->Show(); mainframe->Raise();(§4,dd8cb89f6d). The frame is reused; it is destroyed only throughon_close()→Destroy(), or with the main frame.Show(bool)override starts/stops the refresh timer;on_full_screenaddswxSTAY_ON_TOPon macOS so the overlay stays above a full-screen main window.
When writing a similar overlay: keep the restore rule; give a close handler that respects CanVeto() if the
frame can outlive its owner; own the timer as a member (wxTimer m_timer{this}) rather than a heap
new wxTimer() that is never deleted; write the style as wxBORDER_NONE (not !wxCAPTION | …, §5).