API — ICoreEssentials/UI/Backends/AppKit/MenuBar
The public contract of 1 header(s) under ICoreEssentials/UI/Backends/AppKit/MenuBar — 1 class/struct definition(s), 39 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.
| Header | Defines | Declarations | Bases |
|---|---|---|---|
ICoreAppKitMenuBar.h | ICoreAppKitMenuBar | 39 | — |
ICoreAppKitMenuBar.h#
ICoreEssentials/UI/Backends/AppKit/MenuBar/ICoreAppKitMenuBar.h
ICoreAppKitMenuBar#
ICoreAppKitMenuBar.h:29 · class · 39 declaration(s)
The AppKit backend's system menu bar: real NSMenu / NSMenuItem objects in the strip beside the Apple logo.
class ICoreAppKitMenuBar {
public:
// ---- the chord split ---------------------------------------------------
// Whether this chord may be handed to a menu item as its key equivalent.
//
// True for anything carrying a modifier, and for the function keys. FALSE
// for a bare printable or editing key, and that exclusion is the point: the
// system answers a key equivalent BEFORE the focused control ever sees the
// key, so a bare Delete on a menu row would eat every deletion in every
// text field in the application. Those chords go to the registry instead,
// where the text-entry stand-down can still reach them.
[[nodiscard]] static bool chordIsSafeAsKeyEquivalent(std::uint32_t chord);
// Chord -> what NSMenuItem wants: the key-equivalent string (UTF-8) and the
// modifier mask. False if the chord has no representable equivalent, in
// which case neither output is touched.
//
// ⚠⚠ SHIFT DECIDES THE CASE OF THE STRING. AppKit matches the equivalent
// against the event's -charactersIgnoringModifiers, which ignores every
// modifier EXCEPT Shift, so ⇧⌘N must be spelled @"N" and ⌘S must be
// spelled @"s". Setting the mask bit and leaving the string lowercase is
// the spelling that looks right and never fires. Measured both ways round;
// the table is in the .mm.
//
// ⚠ Shift + a printable NON-letter is REFUSED (false). ⇧1 is "!" on a US
// board and something else on every other one, so no equivalent written
// from the chord alone is right on more than one keyboard. Those go to the
// shortcut registry, which compares modifier bits rather than glyphs.
static bool chordToKeyEquivalent(std::uint32_t chord,
std::string& outKeyEquivalent,
unsigned long long& outModifierMask);
// ---- where a row really belongs on this desktop ------------------------
// Three rows do not live where an application declares them. macOS puts
// About, Preferences and Quit in the APPLICATION menu, and a Mac user looks
// for them there and nowhere else -- an app with "Exit" at the bottom of its
// File menu reads as a port, because that is exactly what it is.
//
// ⚠ THE Qt BACKEND ALREADY DOES THIS, WHICH IS WHY NOT DOING IT WOULD BE A
// REGRESSION RATHER THAN A MISSING FEATURE. QAction::menuRole defaults to
// TextHeuristicRole, so Qt reads the row's TEXT and relocates it; this tree
// never sets a role explicitly (searched: zero hits), so the heuristic is
// what runs today on macOS. Reproducing it keeps the two backends showing
// the same menus.
enum Role : int { RoleNone = 0, RoleAbout = 1, RolePreferences = 2, RoleQuit = 3 };
// The role of a row with this text, by the same reading Qt's heuristic
// makes. `&` mnemonics are stripped first.
//
// ⚠ A DELIBERATE SUBSET, and the divergence is stated rather than hidden:
// Qt's heuristic also recognises Cut/Copy/Paste/Select-All roles, which on
// Cocoa only matter for a standard Edit menu Qt can synthesise. This
// application builds its own Edit menu and those rows stay in it on BOTH
// backends, so reproducing them here would move rows Qt leaves alone.
[[nodiscard]] static int roleForItemText(const std::string& text);
// ---- building the bar --------------------------------------------------
// A fresh NSMenu to hang top-level menus from, ALREADY CARRYING THE
// APPLICATION MENU (see the .mm: the first slot is not the app's to use).
// Autoenabling is off on every menu this class makes.
[[nodiscard]] static void* createMenuBar();
// Whether this process has an application object at all. False in a
// `--console` run that never brought one up.
//
// ⚠⚠ WITHOUT ONE, THIS CLASS FAILS SILENTLY IN TWO PLACES, AND BOTH LOOK
// LIKE SUCCESS. Measured, no crash and no warning:
//
// installAsMainMenu() -[nil setMainMenu:] is a no-op, so the bar is
// simply never installed and mainMenu() is null
// performKeyEquivalent() returns TRUE for a matching row and runs
// NOTHING -- the action is dispatched through
// the application object, which is not there
//
// The second is the dangerous one: a caller using that BOOL to decide
// whether to pass a key on would swallow every accelerator in the process.
// Ask this before believing either result.
[[nodiscard]] static bool hasApplicationObject();
// [NSApp setMainMenu:]. The bar becomes the one on screen. Returns false if
// there is no application object to install it into, rather than reporting
// a success that did not happen.
static bool installAsMainMenu(void* menuBar);
[[nodiscard]] static void* mainMenu();
// Ties `menuBar` to the window holding `hostView` (an NSView*): it becomes
// the main menu whenever that window becomes MAIN, and the application's own
// bar -- the last bar installed that was never tied to a window -- whenever
// any other window does. Null `hostView` unties it. The bar on screen is
// re-decided at once, against the current main window, because building a
// bar installs it (icoreCreateSystemMenuBar) and a tool window's bar must
// not stay up over the editor until the next window switch.
//
// ⚠ -windowDidBecomeMain:, NOT -windowDidBecomeKey:. A popup refuses main
// (ICoreAppKitWindow.mm, canBecomeMainWindow) but may take the keyboard, and
// an ICoreMenu dropped from a tool window must not swap the tool window's
// menus out while it is open.
//
// ⚠ THE HOST IS A VIEW, AND ITS WINDOW IS ASKED AT SWITCH TIME: a widget
// promoted to a top level is in no window until it is first shown.
static void scopeToWindow(void* menuBar, void* hostView);
// A top-level menu. Returns the NSMenu to fill, not the NSMenuItem holding
// it -- the caller fills menus, and the item is bookkeeping.
[[nodiscard]] static void* addTopLevelMenu(void* menuBar, const std::string& title);
// A row. `chord` may be 0 for no shortcut; a chord that is not safe as a key
// equivalent is DRAWN but not installed, and installAsKeyEquivalent tells
// the caller which happened so it can register the other half itself.
//
// ⚠ A ROW WHOSE TEXT CARRIES A ROLE IS RELOCATED, not added to `menu`. It
// goes to the application menu, and the handle returned is the row THERE --
// so a caller that keeps the handle to enable, tick or rename it goes on
// working. This is what stops an app's "Exit ⌘Q" colliding with the
// application menu's own Quit, which is the collision this application
// really has (ICorePrimaryWindow declares Exit on Ctrl+Q).
[[nodiscard]] static void* addItem(void* menu, const std::string& text,
std::uint32_t chord,
std::function<void()> onActivated,
bool* outInstalledAsKeyEquivalent = nullptr);
static void* addSeparator(void* menu);
// The row already carrying this chord's key equivalent anywhere in `menuBar`,
// or null. Submenus are walked.
//
// ⚠⚠ A DUPLICATE KEY EQUIVALENT IS THE QUIETEST FAILURE THIS CLASS HAS.
// AppKit answers the FIRST row holding a given equivalent and the second is
// dead -- it still draws its shortcut in the menu, so the app looks correct
// and one command simply never runs. addItem() therefore REFUSES to install
// a duplicate and reports it through outInstalledAsKeyEquivalent, leaving
// the caller to put the chord on the registry instead. This function is how
// a caller (or an audit) asks the question directly.
[[nodiscard]] static void* itemWithKeyEquivalent(void* menuBar, std::uint32_t chord);
// A group title. macOS has no caption row, so this is a disabled item --
// which is what the platform's own menus use and what Qt's addSection()
// produces here.
[[nodiscard]] static void* addCaption(void* menu, const std::string& text);
// A nested menu, returned ready to fill. Makes the row that opens it too.
[[nodiscard]] static void* addSubMenu(void* menu, const std::string& text);
// The same, on a row that ALREADY EXISTS: `item` becomes the row that opens
// the returned menu, instead of a second row being added beside it.
//
// ⚠ IT EXISTS BECAUSE THE WRAPPER ABOVE BUILDS THE ROW FIRST AND DECIDES IT
// OPENS A MENU SECOND. ICoreMenu::addSubMenu() adds a row for the text and
// then turns it into a submenu holder -- which is precisely what a QAction
// holding a QMenu is on the other backend. Reaching for addSubMenu() there
// would leave the first row in place and add a second one for the same
// text: a menu with every submenu listed twice, once dead. Measured shape,
// not a hypothetical -- it is what the first version of ICoreMenuMirror.mm
// did before this function existed.
//
// `item` must already be in `menu`; the menu it is given is titled `text`,
// because AppKit draws the SUBMENU's title in the strip and ignores the
// holder's.
[[nodiscard]] static void* attachSubMenu(void* menu, void* item, const std::string& text);
// ---- keeping a row in step with its painted twin -----------------------
static void setItemText(void* item, const std::string& text);
static void setItemEnabled(void* item, bool enabled);
static void setItemCheckable(void* item, bool checkable);
static void setItemChecked(void* item, bool checked);
[[nodiscard]] static bool isItemEnabled(void* item);
[[nodiscard]] static bool isItemChecked(void* item);
[[nodiscard]] static std::string itemText(void* item);
[[nodiscard]] static std::string itemKeyEquivalent(void* item);
[[nodiscard]] static unsigned long long itemKeyEquivalentModifierMask(void* item);
static void clearMenu(void* menu);
[[nodiscard]] static int itemCount(void* menu);
[[nodiscard]] static int topLevelCount(void* menuBar);
// What the application menu's Quit row runs. AppKit's own -terminate: by
// default, which tears the process down without asking anyone.
//
// ⚠ THE APPLICATION MENU IS WHERE A MAC USER QUITS, AND IT IS NOT THE APP'S
// OWN "Exit" ROW. An app that declares Exit on its File menu has TWO quit
// paths on this platform, and the one carrying ⌘Q is whichever AppKit finds
// first -- the application menu, because it is slot 0. So an Exit row's
// shutdown work is skipped unless the same work is ALSO on this hook.
// Setting it is how the two paths become one command.
//
// The seat cannot reach that work itself: it lives above this module and
// ICoreEssentials never depends on the SDK (rule 1). Hence a hook.
static void setQuitHandler(std::function<void()> onQuit);
// Run just before `menu` is displayed -- where a menu refreshes what its
// rows say and which are enabled, so it never opens on stale state. The
// hook may REBUILD the menu: this application's project menu clears and
// refills itself from here on every open.
//
// On this backend it is an NSMenu delegate's -menuNeedsUpdate:, the message
// AppKit sends before it displays a menu.
static void setMenuAboutToShowHook(void* menu, std::function<void()> onAboutToShow);
// Run just after `menu` closes, however it closed -- a row ran, Esc, a
// click outside. This is what drives `ICoreMenu::onMenuClosed`, which the
// menu bar needs in order to un-press the title that opened it.
static void setMenuClosedHook(void* menu, std::function<void()> onClosed);
// Whether `menu` is on screen right now, tracked from -menuWillOpen: and
// -menuDidClose:.
//
// ⚠ NSMenu HAS NO "am I showing" PROPERTY, so this is bookkeeping rather
// than a question asked of AppKit -- it is only as good as the delegate
// messages, and a menu this class did not give a delegate always answers
// false.
[[nodiscard]] static bool isMenuOpen(void* menu);
// Sends that message now, without displaying anything.
//
// ⚠ `[NSMenu update]` DOES NOT CALL THE DELEGATE. Measured, 2026-08-20, in
// every combination of autoenablesItems: -menuNeedsUpdate: fires 0 times
// for `[m update]` and once for a direct send. This function used to be
// documented as "what [NSMenu update] calls", which was an untested claim
// about an API -- the same shape as §0.24's ARC sentence, and wrong the
// same way. It sends the message itself.
//
// ⚠ SO THIS IS A STAND-IN FOR DISPLAY, NOT THE DISPLAY PATH. Displaying a
// menu needs a window server, which no suite here has. What is proved by
// driving it this way is everything on THIS side of the message -- that the
// hook runs, that it may rebuild its own menu, that the delegate is still
// alive. That AppKit really sends -menuNeedsUpdate: before display is
// AppKit's documented contract and is NOT proved here.
static void updateMenu(void* menu);
// ---- the stand-down guard ----------------------------------------------
// Asked before a row's callback runs. True means "the caret is in a text
// entry", and the row stands down -- the same reading the Qt backend's
// runFromNative() makes. Unset means never stand down.
//
// ⚠ A CLICK IS NOT A KEYSTROKE, AND THE GUARD MUST NOT TREAT THEM ALIKE.
// The guard exists because a KEY EQUIVALENT is answered before the focused
// field sees the key, so the field has the better claim on it. A user who
// opens a menu and clicks a row has made no such ambiguous gesture -- they
// have said exactly which command they want, and standing it down means a
// menu that does nothing while a text field happens to hold focus. The Qt
// backend draws the same line with its just-closed flag; this seat draws it
// from the menu's own -menuDidClose:.
static void setTextEntryGuard(std::function<bool()> guard);
// The default guard: true when the key window's first responder is a text
// view that is actually editable. Installed by createMenuBar().
[[nodiscard]] static bool defaultTextEntryGuard();
// ---- driving it without a window server --------------------------------
// [menu performKeyEquivalent:] against a synthesized NSEvent*. Returns true
// if a row MATCHED. This is how the suite proves the key-equivalent half
// without typing and without a bar on screen.
//
// ⚠⚠ "MATCHED" IS NOT "RAN", AND THE TWO COME APART IN TWO DIFFERENT WAYS.
// A matched row that is DISABLED reports true and does not run; and with no
// application object (see hasApplicationObject) EVERY matched row reports
// true and does not run. Both are AppKit's behaviour, measured, not this
// class's choice -- but a caller that reads the BOOL as "the key was
// handled" will swallow keys in both cases.
static bool performKeyEquivalent(void* menuBar, void* nsEvent);
// Runs a row exactly as clicking it does.
static bool activateItem(void* item);
// What -menuWillOpen: and -menuDidClose: do, callable directly. A real
// click is always
// preceded by its menu closing; the suite has no menu to open, so it says
// so explicitly rather than leaving the click/keystroke distinction
// untested for want of a window server.
static void notifyMenuClosedForTest();
static void notifyMenuOpenedForTest(void* menu);
static void notifyMenuClosedForTest(void* menu);
// One bar, released -- the peer of createMenuBar() rather than of reset().
//
// ⚠ IT EXISTS BECAUSE A DESTRUCTOR MUST NOT CALL reset(). reset() is the
// suite's between-cases teardown and clears the PROCESS-wide slots as well
// (the application-menu cache, the text-entry guard, the quit handler), so
// a wrapper that used it to free its own bar would silently disarm hooks
// that were never its to touch. This releases the bar and nothing else.
//
// Uninstalls it first if it is the one on screen -- a released main menu is
// a bar AppKit still holds a pointer to -- and drops the application-menu
// cache if it pointed into this bar, since a role row relocated into a menu
// that is no longer anywhere is a row nobody can reach.
static void releaseMenuBar(void* menuBar);
// Everything this class made, released. The suite calls it between cases.
static void reset();
};
};