From 758d07c35a1eb1bef64a66af29a51b9019c34846 Mon Sep 17 00:00:00 2001 From: crash0verride11 <3526616+crash0verride11@users.noreply.github.com> Date: Sat, 1 Aug 2026 21:54:47 -0400 Subject: [PATCH] FIX: macOS popups dismissed by external AXRaise, swallowing the click Any process holding macOS accessibility permission can raise another application's windows via the AXRaise action. While a popup is open this makes the raised window key, the popup resigns key, and wxPopupFocusHandler dismisses it: AXRaise (another process, over Mach IPC) -> -[NSWindow makeKeyAndOrderFront:] -> -[NSWindow makeKeyWindow] -> -[NSWindow resignKeyWindow] -> wxWidgetCocoaImpl::DoNotifyFocusEvent -> wxPopupFocusHandler::OnKillFocus When the raise arrives on mouse-down the popup is gone before the click is delivered, so the click is lost. The popup's NSWindow is also left on screen at NSPopUpMenuWindowLevel, and since DropDown sets wxBG_STYLE_PAINT its background is never auto-erased, so it renders as a blank rectangle above every other application. Everything deriving from PopupWindow is affected: DropDown, AmsMapingPopup, ColorPickerPopup. Dropdowns with submenus appear to work because with two popups open the click dismisses one and still reaches the other. wxWidgets is correct to dismiss a transient popup that lost focus; the problem is that the focus loss was not caused by the user. Push a guard ahead of wxPopupFocusHandler that swallows wxEVT_KILL_FOCUS while the pointer is still inside the popup, and lets it through otherwise. Two details are load-bearing: - The guard must be installed on m_focus, not on the focus argument. On macOS wxPopupTransientWindow::Popup() reassigns m_focus = FindFocus() before pushing its own handler, and the two are different windows. - wxFocusEvent::GetWindow() cannot be used to detect this: it is null for a key-window change, which is not a first-responder change. Hence the pointer-location test. Clicking outside (wxPopupWindowHandler, EVT_LEFT_DOWN on m_child) and leaving the application are handled elsewhere and still dismiss normally. macOS-only; no behaviour change on Windows or Linux. Diagnosed by instrumenting PopupWindow to capture event ordering and a backtrace at the point of dismissal; instrumentation removed before commit. Co-Authored-By: Claude Opus 5 --- src/slic3r/GUI/Widgets/PopupWindow.cpp | 69 ++++++++++++++++++++++++++ src/slic3r/GUI/Widgets/PopupWindow.hpp | 9 ++++ 2 files changed, 78 insertions(+) diff --git a/src/slic3r/GUI/Widgets/PopupWindow.cpp b/src/slic3r/GUI/Widgets/PopupWindow.cpp index 01bfacf27e..d0d09d67dd 100644 --- a/src/slic3r/GUI/Widgets/PopupWindow.cpp +++ b/src/slic3r/GUI/Widgets/PopupWindow.cpp @@ -28,6 +28,9 @@ bool PopupWindow::Create(wxWindow *parent, int style) PopupWindow::~PopupWindow() { +#ifdef __WXOSX__ + removeFocusGuard(); +#endif #ifdef __WXGTK__ GetTopParent(this)->Unbind(wxEVT_ACTIVATE, &PopupWindow::topWindowActivate, this); #endif @@ -40,6 +43,72 @@ PopupWindow::~PopupWindow() #ifdef __WXOSX__ +// Any process holding accessibility permission can raise our windows via the +// AXRaise action. The raised window becomes key, an open popup resigns key, and +// wxPopupFocusHandler dismisses it. When the raise happens on mouse-down, the +// popup is gone before the click is delivered: the click is lost, and the popup's +// NSWindow is left on screen unpainted at NSPopUpMenuWindowLevel. +// +// A raise is not the user dismissing the popup, so swallow the kill-focus while +// the pointer is still inside the popup. wxFocusEvent::GetWindow() cannot be used +// to detect this - it is null for a key-window change, which is not a +// first-responder change. +// +// Other dismissal paths are unaffected: clicking outside goes through +// wxPopupWindowHandler (EVT_LEFT_DOWN on m_child), and leaving the application +// through its own deactivation path. +class PopupWindow::SameAppFocusGuard : public wxEvtHandler +{ +public: + explicit SameAppFocusGuard(PopupWindow *popup) : m_popup(popup) + { + Bind(wxEVT_KILL_FOCUS, &SameAppFocusGuard::OnKillFocus, this); + } + +private: + void OnKillFocus(wxFocusEvent &event) + { + // Pointer still over the popup, so this was not the user dismissing it: + // stop here so wxPopupFocusHandler behind us never runs. + if (m_popup->GetScreenRect().Contains(wxGetMousePosition())) + return; + event.Skip(); + } + + PopupWindow *m_popup; +}; + +void PopupWindow::Popup(wxWindow *focus) +{ + wxPopupTransientWindow::Popup(focus); + // Guard m_focus, not the focus argument: on macOS the base class reassigns + // m_focus = FindFocus() before pushing wxPopupFocusHandler onto it, and the + // two are different windows. Pushing after the base call puts us ahead of + // wxPopupFocusHandler in the handler chain. + if (m_focus_guard == nullptr && m_focus != nullptr) { + m_focus_guard = new SameAppFocusGuard(this); + m_guarded_window = m_focus; + m_focus->PushEventHandler(m_focus_guard); + } +} + +void PopupWindow::Dismiss() +{ + removeFocusGuard(); + wxPopupTransientWindow::Dismiss(); +} + +void PopupWindow::removeFocusGuard() +{ + if (m_focus_guard) { + if (m_guarded_window) + m_guarded_window->RemoveEventHandler(m_focus_guard); + delete m_focus_guard; + m_focus_guard = nullptr; + } + m_guarded_window = nullptr; +} + static wxEvtHandler * HitTest(wxWindow * parent, wxMouseEvent &evt) { auto pt = evt.GetPosition(); diff --git a/src/slic3r/GUI/Widgets/PopupWindow.hpp b/src/slic3r/GUI/Widgets/PopupWindow.hpp index b81d37bcd2..fbe57c34dd 100644 --- a/src/slic3r/GUI/Widgets/PopupWindow.hpp +++ b/src/slic3r/GUI/Widgets/PopupWindow.hpp @@ -17,10 +17,19 @@ class PopupWindow : public wxPopupTransientWindow #ifdef __WXMSW__ void BindUnfocusEvent(); #endif +#ifdef __WXOSX__ + void Popup(wxWindow *focus = nullptr) override; + void Dismiss() override; +#endif private: #ifdef __WXOSX__ void OnMouseEvent2(wxMouseEvent &evt); wxEvtHandler * hovered { this }; + + class SameAppFocusGuard; + SameAppFocusGuard *m_focus_guard { nullptr }; + wxWindow * m_guarded_window { nullptr }; + void removeFocusGuard(); #endif #ifdef __WXGTK__