summaryrefslogtreecommitdiffstats
path: root/widget/cocoa/nsMenuBarX.mm
diff options
context:
space:
mode:
Diffstat (limited to 'widget/cocoa/nsMenuBarX.mm')
-rw-r--r--widget/cocoa/nsMenuBarX.mm1169
1 files changed, 1169 insertions, 0 deletions
diff --git a/widget/cocoa/nsMenuBarX.mm b/widget/cocoa/nsMenuBarX.mm
new file mode 100644
index 0000000000..9865fc196d
--- /dev/null
+++ b/widget/cocoa/nsMenuBarX.mm
@@ -0,0 +1,1169 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <objc/objc-runtime.h>
+
+#include "nsMenuBarX.h"
+#include "nsMenuX.h"
+#include "nsMenuItemX.h"
+#include "nsMenuUtilsX.h"
+#include "nsCocoaUtils.h"
+#include "nsChildView.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsGkAtoms.h"
+#include "nsObjCExceptions.h"
+#include "nsThreadUtils.h"
+
+#include "nsIContent.h"
+#include "nsIWidget.h"
+#include "mozilla/dom/Document.h"
+#include "nsIAppStartup.h"
+#include "nsIStringBundle.h"
+#include "nsToolkitCompsCID.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/dom/Element.h"
+
+using namespace mozilla;
+using mozilla::dom::Element;
+
+NativeMenuItemTarget* nsMenuBarX::sNativeEventTarget = nil;
+nsMenuBarX* nsMenuBarX::sLastGeckoMenuBarPainted = nullptr;
+NSMenu* sApplicationMenu = nil;
+BOOL sApplicationMenuIsFallback = NO;
+BOOL gSomeMenuBarPainted = NO;
+
+// defined in nsCocoaWindow.mm.
+extern BOOL sTouchBarIsInitialized;
+
+// We keep references to the first quit and pref item content nodes we find,
+// which will be from the hidden window. We use these when the document for the
+// current window does not have a quit or pref item. We don't need strong refs
+// here because these items are always strong ref'd by their owning menu bar
+// (instance variable).
+static nsIContent* sAboutItemContent = nullptr;
+static nsIContent* sPrefItemContent = nullptr;
+static nsIContent* sAccountItemContent = nullptr;
+static nsIContent* sQuitItemContent = nullptr;
+
+//
+// ApplicationMenuDelegate Objective-C class
+//
+
+@implementation ApplicationMenuDelegate
+
+- (id)initWithApplicationMenu:(nsMenuBarX*)aApplicationMenu {
+ if ((self = [super init])) {
+ mApplicationMenu = aApplicationMenu;
+ }
+ return self;
+}
+
+- (void)menuWillOpen:(NSMenu*)menu {
+ mApplicationMenu->ApplicationMenuOpened();
+}
+
+- (void)menuDidClose:(NSMenu*)menu {
+}
+
+@end
+
+nsMenuBarX::nsMenuBarX(mozilla::dom::Element* aElement)
+ : mNeedsRebuild(false), mApplicationMenuDelegate(nil) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ mMenuGroupOwner = new nsMenuGroupOwnerX(aElement, this);
+ mMenuGroupOwner->RegisterForLocaleChanges();
+ mNativeMenu = [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar"];
+
+ mContent = aElement;
+
+ if (mContent) {
+ AquifyMenuBar();
+ mMenuGroupOwner->RegisterForContentChanges(mContent, this);
+ ConstructNativeMenus();
+ } else {
+ ConstructFallbackNativeMenus();
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+nsMenuBarX::~nsMenuBarX() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (nsMenuBarX::sLastGeckoMenuBarPainted == this) {
+ nsMenuBarX::sLastGeckoMenuBarPainted = nullptr;
+ }
+
+ // the quit/pref items of a random window might have been used if there was no
+ // hidden window, thus we need to invalidate the weak references.
+ if (sAboutItemContent == mAboutItemContent) {
+ sAboutItemContent = nullptr;
+ }
+ if (sQuitItemContent == mQuitItemContent) {
+ sQuitItemContent = nullptr;
+ }
+ if (sPrefItemContent == mPrefItemContent) {
+ sPrefItemContent = nullptr;
+ }
+ if (sAccountItemContent == mAccountItemContent) {
+ sAccountItemContent = nullptr;
+ }
+
+ mMenuGroupOwner->UnregisterForLocaleChanges();
+
+ // make sure we unregister ourselves as a content observer
+ if (mContent) {
+ mMenuGroupOwner->UnregisterForContentChanges(mContent);
+ }
+
+ for (nsMenuX* menu : mMenuArray) {
+ menu->DetachFromGroupOwnerRecursive();
+ menu->DetachFromParent();
+ }
+
+ if (mApplicationMenuDelegate) {
+ [mApplicationMenuDelegate release];
+ }
+
+ [mNativeMenu release];
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::ConstructNativeMenus() {
+ for (nsIContent* menuContent = mContent->GetFirstChild(); menuContent;
+ menuContent = menuContent->GetNextSibling()) {
+ if (menuContent->IsXULElement(nsGkAtoms::menu)) {
+ InsertMenuAtIndex(
+ MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, menuContent->AsElement()),
+ GetMenuCount());
+ }
+ }
+}
+
+void nsMenuBarX::ConstructFallbackNativeMenus() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (sApplicationMenu) {
+ // Menu has already been built.
+ return;
+ }
+
+ nsCOMPtr<nsIStringBundle> stringBundle;
+
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ bundleSvc->CreateBundle("chrome://global/locale/fallbackMenubar.properties",
+ getter_AddRefs(stringBundle));
+
+ if (!stringBundle) {
+ return;
+ }
+
+ nsAutoString labelUTF16;
+ nsAutoString keyUTF16;
+
+ const char* labelProp = "quitMenuitem.label";
+ const char* keyProp = "quitMenuitem.key";
+
+ stringBundle->GetStringFromName(labelProp, labelUTF16);
+ stringBundle->GetStringFromName(keyProp, keyUTF16);
+
+ NSString* labelStr =
+ [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(labelUTF16).get()];
+ NSString* keyStr =
+ [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(keyUTF16).get()];
+
+ if (!nsMenuBarX::sNativeEventTarget) {
+ nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
+ }
+
+ sApplicationMenu = [[[[NSApp mainMenu] itemAtIndex:0] submenu] retain];
+ if (!mApplicationMenuDelegate) {
+ mApplicationMenuDelegate =
+ [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
+ }
+ sApplicationMenu.delegate = mApplicationMenuDelegate;
+ NSMenuItem* quitMenuItem =
+ [[[NSMenuItem alloc] initWithTitle:labelStr
+ action:@selector(menuItemHit:)
+ keyEquivalent:keyStr] autorelease];
+ quitMenuItem.target = nsMenuBarX::sNativeEventTarget;
+ quitMenuItem.tag = eCommand_ID_Quit;
+ [sApplicationMenu addItem:quitMenuItem];
+ sApplicationMenuIsFallback = YES;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+uint32_t nsMenuBarX::GetMenuCount() { return mMenuArray.Length(); }
+
+bool nsMenuBarX::MenuContainsAppMenu() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ return (mNativeMenu.numberOfItems > 0 &&
+ [mNativeMenu itemAtIndex:0].submenu == sApplicationMenu);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::InsertMenuAtIndex(RefPtr<nsMenuX>&& aMenu, uint32_t aIndex) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // If we've only yet created a fallback global Application menu (using
+ // ContructFallbackNativeMenus()), destroy it before recreating it properly.
+ if (sApplicationMenu && sApplicationMenuIsFallback) {
+ ResetNativeApplicationMenu();
+ }
+ // If we haven't created a global Application menu yet, do it.
+ if (!sApplicationMenu) {
+ CreateApplicationMenu(aMenu.get());
+
+ // Hook the new Application menu up to the menu bar.
+ NSMenu* mainMenu = NSApp.mainMenu;
+ NS_ASSERTION(
+ mainMenu.numberOfItems > 0,
+ "Main menu does not have any items, something is terribly wrong!");
+ [mainMenu itemAtIndex:0].submenu = sApplicationMenu;
+ }
+
+ // add menu to array that owns our menus
+ mMenuArray.InsertElementAt(aIndex, aMenu);
+
+ // hook up submenus
+ RefPtr<nsIContent> menuContent = aMenu->Content();
+ if (menuContent->GetChildCount() > 0 &&
+ !nsMenuUtilsX::NodeIsHiddenOrCollapsed(menuContent)) {
+ MenuChildChangedVisibility(MenuChild(aMenu), true);
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::RemoveMenuAtIndex(uint32_t aIndex) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ if (mMenuArray.Length() <= aIndex) {
+ NS_ERROR("Attempting submenu removal with bad index!");
+ return;
+ }
+
+ RefPtr<nsMenuX> menu = mMenuArray[aIndex];
+ mMenuArray.RemoveElementAt(aIndex);
+
+ menu->DetachFromGroupOwnerRecursive();
+ menu->DetachFromParent();
+
+ // Our native menu and our internal menu object array might be out of sync.
+ // This happens, for example, when a submenu is hidden. Because of this we
+ // should not assume that a native submenu is hooked up.
+ NSMenuItem* nativeMenuItem = menu->NativeNSMenuItem();
+ int nativeMenuItemIndex = [mNativeMenu indexOfItem:nativeMenuItem];
+ if (nativeMenuItemIndex != -1) {
+ [mNativeMenu removeItemAtIndex:nativeMenuItemIndex];
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::ObserveAttributeChanged(mozilla::dom::Document* aDocument,
+ nsIContent* aContent,
+ nsAtom* aAttribute) {}
+
+void nsMenuBarX::ObserveContentRemoved(mozilla::dom::Document* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ nsINode* parent = NODE_FROM(aContainer, aDocument);
+ MOZ_ASSERT(parent);
+ const Maybe<uint32_t> index = parent->ComputeIndexOf(aPreviousSibling);
+ MOZ_ASSERT(*index != UINT32_MAX);
+ RemoveMenuAtIndex(index.isSome() ? *index + 1u : 0u);
+}
+
+void nsMenuBarX::ObserveContentInserted(mozilla::dom::Document* aDocument,
+ nsIContent* aContainer,
+ nsIContent* aChild) {
+ InsertMenuAtIndex(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aChild),
+ aContainer->ComputeIndexOf(aChild).valueOr(UINT32_MAX));
+}
+
+void nsMenuBarX::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSString* locationString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
+ aIndexString.BeginReading())
+ length:aIndexString.Length()];
+ NSArray* indexes = [locationString componentsSeparatedByString:@"|"];
+ unsigned int indexCount = indexes.count;
+ if (indexCount == 0) {
+ return;
+ }
+
+ RefPtr<nsMenuX> currentMenu = nullptr;
+ int targetIndex = [[indexes objectAtIndex:0] intValue];
+ int visible = 0;
+ uint32_t length = mMenuArray.Length();
+ // first find a menu in the menu bar
+ for (unsigned int i = 0; i < length; i++) {
+ RefPtr<nsMenuX> menu = mMenuArray[i];
+ if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(menu->Content())) {
+ visible++;
+ if (visible == (targetIndex + 1)) {
+ currentMenu = std::move(menu);
+ break;
+ }
+ }
+ }
+
+ if (!currentMenu) {
+ return;
+ }
+
+ // fake open/close to cause lazy update to happen so submenus populate
+ currentMenu->MenuOpened();
+ currentMenu->MenuClosed();
+
+ // now find the correct submenu
+ for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
+ targetIndex = [[indexes objectAtIndex:i] intValue];
+ visible = 0;
+ length = currentMenu->GetItemCount();
+ for (unsigned int j = 0; j < length; j++) {
+ Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
+ if (!targetMenu) {
+ return;
+ }
+ RefPtr<nsIContent> content = targetMenu->match(
+ [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
+ [](const RefPtr<nsMenuItemX>& aMenuItem) {
+ return aMenuItem->Content();
+ });
+ if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
+ visible++;
+ if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
+ currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
+ // fake open/close to cause lazy update to happen
+ currentMenu->MenuOpened();
+ currentMenu->MenuClosed();
+ break;
+ }
+ }
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+// Calling this forces a full reload of the menu system, reloading all native
+// menus and their items.
+// Without this testing is hard because changes to the DOM affect the native
+// menu system lazily.
+void nsMenuBarX::ForceNativeMenuReload() {
+ // tear down everything
+ while (GetMenuCount() > 0) {
+ RemoveMenuAtIndex(0);
+ }
+
+ // construct everything
+ ConstructNativeMenus();
+}
+
+nsMenuX* nsMenuBarX::GetMenuAt(uint32_t aIndex) {
+ if (mMenuArray.Length() <= aIndex) {
+ NS_ERROR("Requesting menu at invalid index!");
+ return nullptr;
+ }
+ return mMenuArray[aIndex].get();
+}
+
+nsMenuX* nsMenuBarX::GetXULHelpMenu() {
+ // The Help menu is usually (always?) the last one, so we start there and
+ // count back.
+ for (int32_t i = GetMenuCount() - 1; i >= 0; --i) {
+ nsMenuX* aMenu = GetMenuAt(i);
+ if (aMenu && nsMenuX::IsXULHelpMenu(aMenu->Content())) {
+ return aMenu;
+ }
+ }
+ return nil;
+}
+
+// On SnowLeopard and later we must tell the OS which is our Help menu.
+// Otherwise it will only add Spotlight for Help (the Search item) to our
+// Help menu if its label/title is "Help" -- i.e. if the menu is in English.
+// This resolves bugs 489196 and 539317.
+void nsMenuBarX::SetSystemHelpMenu() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ nsMenuX* xulHelpMenu = GetXULHelpMenu();
+ if (xulHelpMenu) {
+ NSMenu* helpMenu = xulHelpMenu->NativeNSMenu();
+ if (helpMenu) {
+ NSApp.helpMenu = helpMenu;
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+// macOS is adding some (currently 3) hidden menu items every time that
+// `NSApp.mainMenu` is set, but never removes them. This ultimately causes a
+// significant slowdown when switching between windows because the number of
+// items in `NSApp.mainMenu` is growing without bounds.
+//
+// The known hidden, problematic menu items are associated with the following
+// menus:
+// - Start Dictation...
+// - Emoji & Symbols
+//
+// Removing these items before setting `NSApp.mainMenu` prevents this slowdown.
+// See bug 1808223.
+static bool RemoveProblematicMenuItems(NSMenu* aMenu) {
+ uint8_t problematicMenuItemCount = 3;
+ NSMutableArray* itemsToRemove =
+ [NSMutableArray arrayWithCapacity:problematicMenuItemCount];
+
+ for (NSInteger i = 0; i < aMenu.numberOfItems; i++) {
+ NSMenuItem* item = [aMenu itemAtIndex:i];
+
+ if (item.hidden &&
+ (item.action == @selector(startDictation:) ||
+ item.action == @selector(orderFrontCharacterPalette:))) {
+ [itemsToRemove addObject:@(i)];
+ }
+
+ if (item.hasSubmenu && RemoveProblematicMenuItems(item.submenu)) {
+ return true;
+ }
+ }
+
+ bool didRemoveItems = false;
+ for (NSNumber* index in [itemsToRemove reverseObjectEnumerator]) {
+ [aMenu removeItemAtIndex:index.integerValue];
+ didRemoveItems = true;
+ }
+
+ return didRemoveItems;
+}
+
+nsresult nsMenuBarX::Paint() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Don't try to optimize anything in this painting by checking
+ // sLastGeckoMenuBarPainted because the menubar can be manipulated by
+ // native dialogs and sheet code and other things besides this paint method.
+
+ // We have to keep the same menu item for the Application menu so we keep
+ // passing it along.
+ NSMenu* outgoingMenu = [NSApp.mainMenu retain];
+ NS_ASSERTION(
+ outgoingMenu.numberOfItems > 0,
+ "Main menu does not have any items, something is terribly wrong!");
+
+ NSMenuItem* appMenuItem = [[outgoingMenu itemAtIndex:0] retain];
+ [outgoingMenu removeItemAtIndex:0];
+ if (appMenuItem) {
+ [mNativeMenu insertItem:appMenuItem atIndex:0];
+ }
+ [appMenuItem release];
+ [outgoingMenu release];
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ RemoveProblematicMenuItems(mNativeMenu);
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // Set menu bar and event target.
+ NSApp.mainMenu = mNativeMenu;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ SetSystemHelpMenu();
+ nsMenuBarX::sLastGeckoMenuBarPainted = this;
+
+ gSomeMenuBarPainted = YES;
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+/* static */
+void nsMenuBarX::ResetNativeApplicationMenu() {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ [sApplicationMenu removeAllItems];
+ [sApplicationMenu release];
+ sApplicationMenu = nil;
+ sApplicationMenuIsFallback = NO;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+void nsMenuBarX::SetNeedsRebuild() { mNeedsRebuild = true; }
+
+void nsMenuBarX::ApplicationMenuOpened() {
+ if (mNeedsRebuild) {
+ if (!mMenuArray.IsEmpty()) {
+ ResetNativeApplicationMenu();
+ CreateApplicationMenu(mMenuArray[0].get());
+ }
+ mNeedsRebuild = false;
+ }
+}
+
+bool nsMenuBarX::PerformKeyEquivalent(NSEvent* aEvent) {
+ return [mNativeMenu performSuperKeyEquivalent:aEvent];
+}
+
+void nsMenuBarX::MenuChildChangedVisibility(const MenuChild& aChild,
+ bool aIsVisible) {
+ MOZ_RELEASE_ASSERT(aChild.is<RefPtr<nsMenuX>>(),
+ "nsMenuBarX only has nsMenuX children");
+ const RefPtr<nsMenuX>& child = aChild.as<RefPtr<nsMenuX>>();
+ NSMenuItem* item = child->NativeNSMenuItem();
+ if (aIsVisible) {
+ NSInteger insertionPoint = CalculateNativeInsertionPoint(child);
+ [mNativeMenu insertItem:item atIndex:insertionPoint];
+ } else if ([mNativeMenu indexOfItem:item] != -1) {
+ [mNativeMenu removeItem:item];
+ }
+}
+
+NSInteger nsMenuBarX::CalculateNativeInsertionPoint(nsMenuX* aChild) {
+ NSInteger insertionPoint = MenuContainsAppMenu() ? 1 : 0;
+ for (auto& currMenu : mMenuArray) {
+ if (currMenu == aChild) {
+ return insertionPoint;
+ }
+ // Only count items that are inside a menu.
+ // XXXmstange Not sure what would cause free-standing items. Maybe for
+ // collapsed/hidden menus? In that case, an nsMenuX::IsVisible() method
+ // would be better.
+ if (currMenu->NativeNSMenuItem().menu) {
+ insertionPoint++;
+ }
+ }
+ return insertionPoint;
+}
+
+// Hide the item in the menu by setting the 'hidden' attribute. Returns it so
+// the caller can hang onto it if they so choose.
+RefPtr<Element> nsMenuBarX::HideItem(mozilla::dom::Document* aDocument,
+ const nsAString& aID) {
+ RefPtr<Element> menuElement = aDocument->GetElementById(aID);
+ if (menuElement) {
+ menuElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden, u"true"_ns,
+ false);
+ }
+ return menuElement;
+}
+
+// Do what is necessary to conform to the Aqua guidelines for menus.
+void nsMenuBarX::AquifyMenuBar() {
+ RefPtr<mozilla::dom::Document> domDoc = mContent->GetComposedDoc();
+ if (domDoc) {
+ // remove the "About..." item and its separator
+ HideItem(domDoc, u"aboutSeparator"_ns);
+ mAboutItemContent = HideItem(domDoc, u"aboutName"_ns);
+ if (!sAboutItemContent) {
+ sAboutItemContent = mAboutItemContent;
+ }
+
+ // remove quit item and its separator
+ HideItem(domDoc, u"menu_FileQuitSeparator"_ns);
+ mQuitItemContent = HideItem(domDoc, u"menu_FileQuitItem"_ns);
+ if (!sQuitItemContent) {
+ sQuitItemContent = mQuitItemContent;
+ }
+
+ // remove prefs item and its separator, but save off the pref content node
+ // so we can invoke its command later.
+ HideItem(domDoc, u"menu_PrefsSeparator"_ns);
+ mPrefItemContent = HideItem(domDoc, u"menu_preferences"_ns);
+ if (!sPrefItemContent) {
+ sPrefItemContent = mPrefItemContent;
+ }
+
+ // remove Account Settings item.
+ mAccountItemContent = HideItem(domDoc, u"menu_accountmgr"_ns);
+ if (!sAccountItemContent) {
+ sAccountItemContent = mAccountItemContent;
+ }
+
+ // hide items that we use for the Application menu
+ HideItem(domDoc, u"menu_mac_services"_ns);
+ HideItem(domDoc, u"menu_mac_hide_app"_ns);
+ HideItem(domDoc, u"menu_mac_hide_others"_ns);
+ HideItem(domDoc, u"menu_mac_show_all"_ns);
+ HideItem(domDoc, u"menu_mac_touch_bar"_ns);
+ }
+}
+
+// for creating menu items destined for the Application menu
+NSMenuItem* nsMenuBarX::CreateNativeAppMenuItem(nsMenuX* aMenu,
+ const nsAString& aNodeID,
+ SEL aAction, int aTag,
+ NativeMenuItemTarget* aTarget) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ RefPtr<mozilla::dom::Document> doc = aMenu->Content()->GetUncomposedDoc();
+ if (!doc) {
+ return nil;
+ }
+
+ RefPtr<mozilla::dom::Element> menuItem = doc->GetElementById(aNodeID);
+ if (!menuItem) {
+ return nil;
+ }
+
+ // Check collapsed rather than hidden since the app menu items are always
+ // hidden in AquifyMenuBar.
+ if (menuItem->AttrValueIs(kNameSpaceID_None, nsGkAtoms::collapsed,
+ nsGkAtoms::_true, eCaseMatters)) {
+ return nil;
+ }
+
+ // Get information from the gecko menu item
+ nsAutoString label;
+ nsAutoString modifiers;
+ nsAutoString key;
+ menuItem->GetAttr(nsGkAtoms::label, label);
+ menuItem->GetAttr(nsGkAtoms::modifiers, modifiers);
+ menuItem->GetAttr(nsGkAtoms::key, key);
+
+ // Get more information about the key equivalent. Start by
+ // finding the key node we need.
+ NSString* keyEquiv = nil;
+ unsigned int macKeyModifiers = 0;
+ if (!key.IsEmpty()) {
+ RefPtr<Element> keyElement = doc->GetElementById(key);
+ if (keyElement) {
+ // first grab the key equivalent character
+ nsAutoString keyChar(u" "_ns);
+ keyElement->GetAttr(nsGkAtoms::key, keyChar);
+ if (!keyChar.EqualsLiteral(" ")) {
+ keyEquiv = [[NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(keyChar.get())
+ length:keyChar.Length()] lowercaseString];
+ }
+ // now grab the key equivalent modifiers
+ nsAutoString modifiersStr;
+ keyElement->GetAttr(nsGkAtoms::modifiers, modifiersStr);
+ uint8_t geckoModifiers =
+ nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
+ macKeyModifiers =
+ nsMenuUtilsX::MacModifiersForGeckoModifiers(geckoModifiers);
+ }
+ }
+ // get the label into NSString-form
+ NSString* labelString = [NSString
+ stringWithCharacters:reinterpret_cast<const unichar*>(label.get())
+ length:label.Length()];
+
+ if (!labelString) {
+ labelString = @"";
+ }
+ if (!keyEquiv) {
+ keyEquiv = @"";
+ }
+
+ // put together the actual NSMenuItem
+ NSMenuItem* newMenuItem = [[NSMenuItem alloc] initWithTitle:labelString
+ action:aAction
+ keyEquivalent:keyEquiv];
+
+ newMenuItem.tag = aTag;
+ newMenuItem.target = aTarget;
+ newMenuItem.keyEquivalentModifierMask = macKeyModifiers;
+ newMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
+
+ return newMenuItem;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+// build the Application menu shared by all menu bars
+void nsMenuBarX::CreateApplicationMenu(nsMenuX* aMenu) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ // At this point, the application menu is the application menu from
+ // the nib in cocoa widgets. We do not have a way to create an application
+ // menu manually, so we grab the one from the nib and use that.
+ sApplicationMenu = [[NSApp.mainMenu itemAtIndex:0].submenu retain];
+
+ /*
+ We support the following menu items here:
+
+ Menu Item DOM Node ID Notes
+
+ ========================
+ = About This App = <- aboutName
+ ========================
+ = Preferences... = <- menu_preferences
+ = Account Settings = <- menu_accountmgr Only on Thunderbird
+ ========================
+ = Services > = <- menu_mac_services <- (do not define key
+ equivalent)
+ ========================
+ = Hide App = <- menu_mac_hide_app
+ = Hide Others = <- menu_mac_hide_others
+ = Show All = <- menu_mac_show_all
+ ========================
+ = Customize Touch Bar… = <- menu_mac_touch_bar
+ ========================
+ = Quit = <- menu_FileQuitItem
+ ========================
+
+ If any of them are ommitted from the application's DOM, we just don't add
+ them. We always add a "Quit" item, but if an app developer does not provide
+ a DOM node with the right ID for the Quit item, we add it in English. App
+ developers need only add each node with a label and a key equivalent (if
+ they want one). Other attributes are optional. Like so:
+
+ <menuitem id="menu_preferences"
+ label="&preferencesCmdMac.label;"
+ key="open_prefs_key"/>
+
+ We need to use this system for localization purposes, until we have a better
+ way to define the Application menu to be used on Mac OS X.
+ */
+
+ if (sApplicationMenu) {
+ if (!mApplicationMenuDelegate) {
+ mApplicationMenuDelegate =
+ [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
+ }
+ sApplicationMenu.delegate = mApplicationMenuDelegate;
+
+ // This code reads attributes we are going to care about from the DOM
+ // elements
+
+ NSMenuItem* itemBeingAdded = nil;
+ BOOL addAboutSeparator = FALSE;
+ BOOL addPrefsSeparator = FALSE;
+
+ // Add the About menu item
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"aboutName"_ns, @selector(menuItemHit:), eCommand_ID_About,
+ nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addAboutSeparator = TRUE;
+ }
+
+ // Add separator if either the About item or software update item exists
+ if (addAboutSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ // Add the Preferences menu item
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_preferences"_ns, @selector(menuItemHit:),
+ eCommand_ID_Prefs, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addPrefsSeparator = TRUE;
+ }
+
+ // Add the Account Settings menu item. This is Thunderbird only
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_accountmgr"_ns, @selector(menuItemHit:),
+ eCommand_ID_Account, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+ }
+
+ // Add separator after Preferences menu
+ if (addPrefsSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ // Add Services menu item
+ itemBeingAdded =
+ CreateNativeAppMenuItem(aMenu, u"menu_mac_services"_ns, nil, 0, nil);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+
+ // set this menu item up as the Mac OS X Services menu
+ NSMenu* servicesMenu = [[GeckoServicesNSMenu alloc] initWithTitle:@""];
+ itemBeingAdded.submenu = servicesMenu;
+ NSApp.servicesMenu = servicesMenu;
+
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ // Add separator after Services menu
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ BOOL addHideShowSeparator = FALSE;
+
+ // Add menu item to hide this application
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_mac_hide_app"_ns, @selector(menuItemHit:),
+ eCommand_ID_HideApp, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addHideShowSeparator = TRUE;
+ }
+
+ // Add menu item to hide other applications
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_mac_hide_others"_ns, @selector(menuItemHit:),
+ eCommand_ID_HideOthers, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addHideShowSeparator = TRUE;
+ }
+
+ // Add menu item to show all applications
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_mac_show_all"_ns, @selector(menuItemHit:),
+ eCommand_ID_ShowAll, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+
+ addHideShowSeparator = TRUE;
+ }
+
+ // Add a separator after the hide/show menus if at least one exists
+ if (addHideShowSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ BOOL addTouchBarSeparator = NO;
+
+ // Add Touch Bar customization menu item.
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_mac_touch_bar"_ns, @selector(menuItemHit:),
+ eCommand_ID_TouchBar, nsMenuBarX::sNativeEventTarget);
+
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ // We hide the menu item on Macs that don't have a Touch Bar.
+ if (!sTouchBarIsInitialized) {
+ [itemBeingAdded setHidden:YES];
+ } else {
+ addTouchBarSeparator = YES;
+ }
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+ }
+
+ // Add a separator after the Touch Bar menu item if it exists
+ if (addTouchBarSeparator) {
+ [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+ }
+
+ // Add quit menu item
+ itemBeingAdded = CreateNativeAppMenuItem(
+ aMenu, u"menu_FileQuitItem"_ns, @selector(menuItemHit:),
+ eCommand_ID_Quit, nsMenuBarX::sNativeEventTarget);
+ if (itemBeingAdded) {
+ [sApplicationMenu addItem:itemBeingAdded];
+ [itemBeingAdded release];
+ itemBeingAdded = nil;
+ } else {
+ // the current application does not have a DOM node for "Quit". Add one
+ // anyway, in English.
+ NSMenuItem* defaultQuitItem =
+ [[[NSMenuItem alloc] initWithTitle:@"Quit"
+ action:@selector(menuItemHit:)
+ keyEquivalent:@"q"] autorelease];
+ defaultQuitItem.target = nsMenuBarX::sNativeEventTarget;
+ defaultQuitItem.tag = eCommand_ID_Quit;
+ [sApplicationMenu addItem:defaultQuitItem];
+ }
+ }
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+}
+
+//
+// Objective-C class used to allow us to have keyboard commands
+// look like they are doing something but actually do nothing.
+// We allow mouse actions to work normally.
+//
+
+// Controls whether or not native menu items should invoke their commands.
+static BOOL gMenuItemsExecuteCommands = YES;
+
+@implementation GeckoNSMenu
+
+// Keyboard commands should not cause menu items to invoke their
+// commands when there is a key window because we'd rather send
+// the keyboard command to the window. We still have the menus
+// go through the mechanics so they'll give the proper visual
+// feedback.
+- (BOOL)performKeyEquivalent:(NSEvent*)aEvent {
+ // We've noticed that Mac OS X expects this check in subclasses before
+ // calling NSMenu's "performKeyEquivalent:".
+ //
+ // There is no case in which we'd need to do anything or return YES
+ // when we have no items so we can just do this check first.
+ if (self.numberOfItems <= 0) {
+ return NO;
+ }
+
+ NSWindow* keyWindow = NSApp.keyWindow;
+
+ // If there is no key window then just behave normally. This
+ // probably means that this menu is associated with Gecko's
+ // hidden window.
+ if (!keyWindow) {
+ return [super performKeyEquivalent:aEvent];
+ }
+
+ NSResponder* firstResponder = keyWindow.firstResponder;
+
+ gMenuItemsExecuteCommands = NO;
+ [super performKeyEquivalent:aEvent];
+ gMenuItemsExecuteCommands = YES; // return to default
+
+ // Return YES if we invoked a command and there is now no key window or we
+ // changed the first responder. In this case we do not want to propagate the
+ // event because we don't want it handled again.
+ if (!NSApp.keyWindow || NSApp.keyWindow.firstResponder != firstResponder) {
+ return YES;
+ }
+
+ // Return NO so that we can handle the event via NSView's "keyDown:".
+ return NO;
+}
+
+- (BOOL)performSuperKeyEquivalent:(NSEvent*)aEvent {
+ return [super performKeyEquivalent:aEvent];
+}
+
+@end
+
+//
+// Objective-C class used as action target for menu items
+//
+
+@implementation NativeMenuItemTarget
+
+// called when some menu item in this menu gets hit
+- (IBAction)menuItemHit:(id)aSender {
+ if (!gMenuItemsExecuteCommands) {
+ return;
+ }
+
+ if (![aSender isKindOfClass:[NSMenuItem class]]) {
+ return;
+ }
+
+ NSMenuItem* nativeMenuItem = (NSMenuItem*)aSender;
+ NSInteger tag = nativeMenuItem.tag;
+
+ nsMenuGroupOwnerX* menuGroupOwner = nullptr;
+ nsMenuBarX* menuBar = nullptr;
+ MOZMenuItemRepresentedObject* representedObject =
+ nativeMenuItem.representedObject;
+
+ if (representedObject) {
+ menuGroupOwner = representedObject.menuGroupOwner;
+ if (!menuGroupOwner) {
+ return;
+ }
+ menuBar = menuGroupOwner->GetMenuBar();
+ }
+
+ // Notify containing menu about the fact that a menu item will be activated.
+ NSMenu* menu = nativeMenuItem.menu;
+ if ([menu.delegate isKindOfClass:[MenuDelegate class]]) {
+ [(MenuDelegate*)menu.delegate menu:menu willActivateItem:nativeMenuItem];
+ }
+
+ // Get the modifier flags and button for this menu item activation. The menu
+ // system does not pass an NSEvent to our action selector, but we can query
+ // the current NSEvent instead. The current NSEvent can be a key event or a
+ // mouseup event, depending on how the menu item is activated.
+ NSEventModifierFlags modifierFlags =
+ NSApp.currentEvent ? NSApp.currentEvent.modifierFlags : 0;
+ mozilla::MouseButton button =
+ NSApp.currentEvent ? nsCocoaUtils::ButtonForEvent(NSApp.currentEvent)
+ : mozilla::MouseButton::ePrimary;
+
+ // Do special processing if this is for an app-global command.
+ if (tag == eCommand_ID_About) {
+ nsIContent* mostSpecificContent = sAboutItemContent;
+ if (menuBar && menuBar->mAboutItemContent) {
+ mostSpecificContent = menuBar->mAboutItemContent;
+ }
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
+ return;
+ }
+ if (tag == eCommand_ID_Prefs) {
+ nsIContent* mostSpecificContent = sPrefItemContent;
+ if (menuBar && menuBar->mPrefItemContent) {
+ mostSpecificContent = menuBar->mPrefItemContent;
+ }
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
+ return;
+ }
+ if (tag == eCommand_ID_Account) {
+ nsIContent* mostSpecificContent = sAccountItemContent;
+ if (menuBar && menuBar->mAccountItemContent) {
+ mostSpecificContent = menuBar->mAccountItemContent;
+ }
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
+ return;
+ }
+ if (tag == eCommand_ID_HideApp) {
+ [NSApp hide:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_HideOthers) {
+ [NSApp hideOtherApplications:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_ShowAll) {
+ [NSApp unhideAllApplications:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_TouchBar) {
+ [NSApp toggleTouchBarCustomizationPalette:aSender];
+ return;
+ }
+ if (tag == eCommand_ID_Quit) {
+ nsIContent* mostSpecificContent = sQuitItemContent;
+ if (menuBar && menuBar->mQuitItemContent) {
+ mostSpecificContent = menuBar->mQuitItemContent;
+ }
+ // If we have some content for quit we execute it. Otherwise we send a
+ // native app terminate message. If you want to stop a quit from happening,
+ // provide quit content and return the event as unhandled.
+ if (mostSpecificContent) {
+ nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags,
+ button);
+ } else {
+ nsCOMPtr<nsIAppStartup> appStartup =
+ mozilla::components::AppStartup::Service();
+ if (appStartup) {
+ bool userAllowedQuit = true;
+ appStartup->Quit(nsIAppStartup::eAttemptQuit, 0, &userAllowedQuit);
+ }
+ }
+ return;
+ }
+
+ // given the commandID, look it up in our hashtable and dispatch to
+ // that menu item.
+ if (menuGroupOwner) {
+ if (RefPtr<nsMenuItemX> menuItem = menuGroupOwner->GetMenuItemForCommandID(
+ static_cast<uint32_t>(tag))) {
+ if (nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest) {
+ menuItem->DoCommand(modifierFlags, button);
+ } else if (RefPtr<nsMenuX> menu = menuItem->ParentMenu()) {
+ menu->ActivateItemAfterClosing(std::move(menuItem), modifierFlags,
+ button);
+ }
+ }
+ }
+}
+
+@end
+
+// Objective-C class used for menu items on the Services menu to allow Gecko
+// to override their standard behavior in order to stop key equivalents from
+// firing in certain instances. When gMenuItemsExecuteCommands is NO, we return
+// a dummy target and action instead of the actual target and action.
+
+@implementation GeckoServicesNSMenuItem
+
+- (id)target {
+ id realTarget = super.target;
+ if (gMenuItemsExecuteCommands) {
+ return realTarget;
+ }
+ return realTarget ? self : nil;
+}
+
+- (SEL)action {
+ SEL realAction = super.action;
+ if (gMenuItemsExecuteCommands) {
+ return realAction;
+ }
+ return realAction ? @selector(_doNothing:) : nullptr;
+}
+
+- (void)_doNothing:(id)aSender {
+}
+
+@end
+
+// Objective-C class used as the Services menu so that Gecko can override the
+// standard behavior of the Services menu in order to stop key equivalents
+// from firing in certain instances.
+
+@implementation GeckoServicesNSMenu
+
+- (void)addItem:(NSMenuItem*)aNewItem {
+ [self _overrideClassOfMenuItem:aNewItem];
+ [super addItem:aNewItem];
+}
+
+- (NSMenuItem*)addItemWithTitle:(NSString*)aString
+ action:(SEL)aSelector
+ keyEquivalent:(NSString*)aKeyEquiv {
+ NSMenuItem* newItem = [super addItemWithTitle:aString
+ action:aSelector
+ keyEquivalent:aKeyEquiv];
+ [self _overrideClassOfMenuItem:newItem];
+ return newItem;
+}
+
+- (void)insertItem:(NSMenuItem*)aNewItem atIndex:(NSInteger)aIndex {
+ [self _overrideClassOfMenuItem:aNewItem];
+ [super insertItem:aNewItem atIndex:aIndex];
+}
+
+- (NSMenuItem*)insertItemWithTitle:(NSString*)aString
+ action:(SEL)aSelector
+ keyEquivalent:(NSString*)aKeyEquiv
+ atIndex:(NSInteger)aIndex {
+ NSMenuItem* newItem = [super insertItemWithTitle:aString
+ action:aSelector
+ keyEquivalent:aKeyEquiv
+ atIndex:aIndex];
+ [self _overrideClassOfMenuItem:newItem];
+ return newItem;
+}
+
+- (void)_overrideClassOfMenuItem:(NSMenuItem*)aMenuItem {
+ if ([aMenuItem class] == [NSMenuItem class]) {
+ object_setClass(aMenuItem, [GeckoServicesNSMenuItem class]);
+ }
+}
+
+@end