From 79a89f817fcea1315fac1f6c70f18d16b38f4efa Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 2 Oct 2026 18:16:05 +0800 Subject: [PATCH] Add a never-Raise-a-popup rule to the orca-wxwidgets skill --- .claude/skills/orca-wxwidgets/SKILL.md | 3 +- .../orca-wxwidgets/references/popups-menus.md | 32 +++++++++++++++++-- .../references/windows-dialogs.md | 3 +- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.claude/skills/orca-wxwidgets/SKILL.md b/.claude/skills/orca-wxwidgets/SKILL.md index e3ec65b4a8..263d2e390f 100644 --- a/.claude/skills/orca-wxwidgets/SKILL.md +++ b/.claude/skills/orca-wxwidgets/SKILL.md @@ -206,7 +206,8 @@ known class with a pitfall entry and a fixing commit. - **macOS:** capture-lost is never sent (a leaked capture freezes all clicks); transient popups hover- dismiss across a gap — anchor flush and re-verify the cursor; native modals (file/dir dialogs, native message boxes) and generic progress dialogs re-activate the main window, so re-raise a secondary window - afterwards with a deferred, liveness-guarded `Raise()`; a live menu accelerator consumes the key before + afterwards with a deferred, liveness-guarded `Raise()` — but never `Raise()` a `wxPopupWindow`, which makes + it the key window; a live menu accelerator consumes the key before any wx key event; Control+click arrives as a right-click. - **Windows:** `IsDark()` and `wxSYS_COLOUR_*` follow the system app mode, not Orca's theme — use `dark_mode()`; menu bitmaps follow `check_dark_mode()`; windows are not double-buffered by default in diff --git a/.claude/skills/orca-wxwidgets/references/popups-menus.md b/.claude/skills/orca-wxwidgets/references/popups-menus.md index 00c666cfb7..be1abb69a8 100644 --- a/.claude/skills/orca-wxwidgets/references/popups-menus.md +++ b/.claude/skills/orca-wxwidgets/references/popups-menus.md @@ -57,8 +57,9 @@ Contents: [Rules](#rules) · [1. Choosing the window kind](#1-choosing-the-windo 17. Don't shrink a dropdown below two rows to fit the screen. (§7) 18. Content that needs typing focus or hosts a `wxWebView` uses a frameless `wxDialog` that hides on deactivation, not a transient popup. (§10) -19. Display-only overlays (HUDs, toasts) use a plain `wxPopupWindow`: it never takes focus and never - auto-dismisses. (§4, §10) +19. Display-only overlays (HUDs, toasts) use a plain `wxPopupWindow`: `Show()` never gives it focus and it + never auto-dismisses. Never `Raise()` any `wxPopupWindow`: `Raise()` is for top-level windows only, and on + macOS it makes the popup the key window. (§4, §5, §10) 20. On MSW, don't `SetFocus()` another window on hover while `wxCurrentPopupWindow` is non-null. (§8) 21. Menu items use `wxID_ANY` and read `item->GetId()`. `wxNewId()` is deprecated. (§13) 22. Set a menu item's bitmap before `Append`. Don't expect icons on check or radio items. Never call @@ -356,6 +357,13 @@ compensates for (§6). with `ShowWithoutActivating` → `setHidesOnDeactivate:YES` + `orderFront` (`src/osx/carbon/popupwin.cpp:56-75`, `nonownedwnd.mm:938-945`). When the app deactivates, Cocoa hides the panel and shows it again on reactivation. wx never calls `OnDismiss`, and `IsShown()` stays true. This applies to plain `wxPopupWindow` overlays too. +- `Raise()` activates the popup. It is `makeKeyAndOrderFront` (`src/osx/nonownedwnd_osx.cpp:289-295`, + `nonownedwnd.mm:897-899`), and `wxNSPanel` answers `canBecomeKeyWindow` with YES (`nonownedwnd.mm:271`), so + the popup becomes the key window. Keys go to it, and the frame loses key status: `windowDidResignKey` → + `HandleActivated(0, false)` → `wxEVT_ACTIVATE(false)` on the frame (`nonownedwnd.mm:567-576`, + `nonownedwnd_osx.cpp:303-310`). Hiding the key popup gives key back to the frame, which then gets + `wxEVT_ACTIVATE(true)` (observed; AppKit behaviour, not in the wx tree). A popup at `NSPopUpMenuWindowLevel` + is already above its frame, so `Raise()` buys nothing. - Capture: `Show(true)` makes `m_child` capture the mouse ("Assume that the mouse is outside the popup to begin with", `popupcmn.cpp:421-426`). `OnIdle` releases the capture while the cursor is inside and re-captures it outside, but only when the mouse position has changed since the last idle pass. `s_posLast` is a @@ -739,8 +747,26 @@ created with `wxBORDER_NONE | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT | wxF - **Rule:** For overlays that must never take keyboard focus (above a GL surface), use a plain `wxPopupWindow(top, wxBORDER_NONE)`, not a `wxFrame`. **Why:** A frame took the X input focus and swallowed every shortcut until the user clicked the canvas. A popup - window cannot take focus. On macOS these overlays hide while the app is inactive (§5). + window does not take focus when shown. On macOS these overlays hide while the app is inactive (§5). Cite: `CAD/DesignCanvas.cpp` (`m_hud`, `m_status_hud`). +- **Rule:** Never `Raise()` a `wxPopupWindow`. To bring an overlay up, `Show()` it if it is hidden, then + `Move()` it. + **Why:** `Raise()` is documented for top-level windows only (`interface/wx/window.h:3028-3029`), and a popup + derives from `wxNonOwnedWindow`, not `wxTopLevelWindow` (`include/wx/popupwin.h:33`). On macOS it makes the + popup the key window (§5): the popup takes the keys meant for the window below it, and the frame receives + `wxEVT_ACTIVATE(false)`. A frame activate handler that hides the overlay on deactivation and re-places it on + activation then loops: each `Raise()` deactivates the frame, the hide reactivates it, and the re-place raises + again, recursing until the main thread's stack overflows. A popup's `Show()` is `ShowWithoutActivating` + and is safe. + ```cpp + // Wrong + if (!overlay->IsShown()) overlay->Show(); + overlay->Move(pos); + overlay->Raise(); + // Right + if (!overlay->IsShown()) overlay->Show(); + overlay->Move(pos); + ``` ## 11. wxComboCtrl / wxComboPopup diff --git a/.claude/skills/orca-wxwidgets/references/windows-dialogs.md b/.claude/skills/orca-wxwidgets/references/windows-dialogs.md index 6b405f942c..1c5be5af4c 100644 --- a/.claude/skills/orca-wxwidgets/references/windows-dialogs.md +++ b/.claude/skills/orca-wxwidgets/references/windows-dialogs.md @@ -428,7 +428,8 @@ this function does *not* show it", top-level windows only (`interface/wx/window. 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:896-899`). +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