1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include <objc/objc-runtime.h>
8 #include "nsChildView.h"
9 #include "nsCocoaUtils.h"
10 #include "nsCocoaWindow.h"
11 #include "nsMenuBarX.h"
12 #include "nsMenuItemX.h"
13 #include "nsMenuUtilsX.h"
18 #include "nsGkAtoms.h"
19 #include "nsObjCExceptions.h"
20 #include "nsThreadUtils.h"
22 #include "nsIContent.h"
23 #include "nsIWidget.h"
24 #include "mozilla/dom/Document.h"
25 #include "nsIAppStartup.h"
26 #include "nsIStringBundle.h"
27 #include "nsToolkitCompsCID.h"
29 #include "mozilla/Components.h"
30 #include "mozilla/dom/Element.h"
32 using namespace mozilla;
33 using mozilla::dom::Element;
35 NativeMenuItemTarget* nsMenuBarX::sNativeEventTarget = nil;
36 nsMenuBarX* nsMenuBarX::sLastGeckoMenuBarPainted = nullptr;
37 NSMenu* sApplicationMenu = nil;
38 BOOL sApplicationMenuIsFallback = NO;
39 BOOL gSomeMenuBarPainted = NO;
41 // Controls whether or not native menu items should invoke their commands. See
42 // class comments for `GeckoNSMenuItem` and `GeckoNSMenu` below for an
43 // explanation of why this switch is necessary.
44 static BOOL gMenuItemsExecuteCommands = YES;
46 // defined in nsCocoaWindow.mm.
47 extern BOOL sTouchBarIsInitialized;
49 // We keep references to the first quit and pref item content nodes we find,
50 // which will be from the hidden window. We use these when the document for the
51 // current window does not have a quit or pref item. We don't need strong refs
52 // here because these items are always strong ref'd by their owning menu bar
53 // (instance variable).
54 static nsIContent* sAboutItemContent = nullptr;
55 static nsIContent* sPrefItemContent = nullptr;
56 static nsIContent* sAccountItemContent = nullptr;
57 static nsIContent* sQuitItemContent = nullptr;
60 // ApplicationMenuDelegate Objective-C class
63 @implementation ApplicationMenuDelegate
65 - (id)initWithApplicationMenu:(nsMenuBarX*)aApplicationMenu {
66 if ((self = [super init])) {
67 mApplicationMenu = aApplicationMenu;
72 - (void)menuWillOpen:(NSMenu*)menu {
73 mApplicationMenu->ApplicationMenuOpened();
76 - (void)menuDidClose:(NSMenu*)menu {
81 nsMenuBarX::nsMenuBarX(mozilla::dom::Element* aElement)
82 : mNeedsRebuild(false), mApplicationMenuDelegate(nil) {
83 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
85 mMenuGroupOwner = new nsMenuGroupOwnerX(aElement, this);
86 mMenuGroupOwner->RegisterForLocaleChanges();
87 mNativeMenu = [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar"];
93 mMenuGroupOwner->RegisterForContentChanges(mContent, this);
94 ConstructNativeMenus();
96 ConstructFallbackNativeMenus();
99 NS_OBJC_END_TRY_ABORT_BLOCK;
102 nsMenuBarX::~nsMenuBarX() {
103 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
105 if (nsMenuBarX::sLastGeckoMenuBarPainted == this) {
106 nsMenuBarX::sLastGeckoMenuBarPainted = nullptr;
109 // the quit/pref items of a random window might have been used if there was no
110 // hidden window, thus we need to invalidate the weak references.
111 if (sAboutItemContent == mAboutItemContent) {
112 sAboutItemContent = nullptr;
114 if (sQuitItemContent == mQuitItemContent) {
115 sQuitItemContent = nullptr;
117 if (sPrefItemContent == mPrefItemContent) {
118 sPrefItemContent = nullptr;
120 if (sAccountItemContent == mAccountItemContent) {
121 sAccountItemContent = nullptr;
124 mMenuGroupOwner->UnregisterForLocaleChanges();
126 // make sure we unregister ourselves as a content observer
128 mMenuGroupOwner->UnregisterForContentChanges(mContent);
131 for (nsMenuX* menu : mMenuArray) {
132 menu->DetachFromGroupOwnerRecursive();
133 menu->DetachFromParent();
136 if (mApplicationMenuDelegate) {
137 [mApplicationMenuDelegate release];
140 [mNativeMenu release];
142 NS_OBJC_END_TRY_ABORT_BLOCK;
145 void nsMenuBarX::ConstructNativeMenus() {
146 for (nsIContent* menuContent = mContent->GetFirstChild(); menuContent;
147 menuContent = menuContent->GetNextSibling()) {
148 if (menuContent->IsXULElement(nsGkAtoms::menu)) {
150 MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, menuContent->AsElement()),
156 void nsMenuBarX::ConstructFallbackNativeMenus() {
157 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
159 if (sApplicationMenu) {
160 // Menu has already been built.
164 nsCOMPtr<nsIStringBundle> stringBundle;
166 nsCOMPtr<nsIStringBundleService> bundleSvc =
167 do_GetService(NS_STRINGBUNDLE_CONTRACTID);
168 bundleSvc->CreateBundle("chrome://global/locale/fallbackMenubar.properties",
169 getter_AddRefs(stringBundle));
175 nsAutoString labelUTF16;
176 nsAutoString keyUTF16;
178 const char* labelProp = "quitMenuitem.label";
179 const char* keyProp = "quitMenuitem.key";
181 stringBundle->GetStringFromName(labelProp, labelUTF16);
182 stringBundle->GetStringFromName(keyProp, keyUTF16);
185 [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(labelUTF16).get()];
187 [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(keyUTF16).get()];
189 if (!nsMenuBarX::sNativeEventTarget) {
190 nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
193 sApplicationMenu = [[[[NSApp mainMenu] itemAtIndex:0] submenu] retain];
194 if (!mApplicationMenuDelegate) {
195 mApplicationMenuDelegate =
196 [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
198 sApplicationMenu.delegate = mApplicationMenuDelegate;
199 NSMenuItem* quitMenuItem =
200 [[[GeckoNSMenuItem alloc] initWithTitle:labelStr
201 action:@selector(menuItemHit:)
202 keyEquivalent:keyStr] autorelease];
203 quitMenuItem.target = nsMenuBarX::sNativeEventTarget;
204 quitMenuItem.tag = eCommand_ID_Quit;
205 [sApplicationMenu addItem:quitMenuItem];
206 sApplicationMenuIsFallback = YES;
208 NS_OBJC_END_TRY_ABORT_BLOCK;
211 uint32_t nsMenuBarX::GetMenuCount() { return mMenuArray.Length(); }
213 bool nsMenuBarX::MenuContainsAppMenu() {
214 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
216 return (mNativeMenu.numberOfItems > 0 &&
217 [mNativeMenu itemAtIndex:0].submenu == sApplicationMenu);
219 NS_OBJC_END_TRY_ABORT_BLOCK;
222 void nsMenuBarX::InsertMenuAtIndex(RefPtr<nsMenuX>&& aMenu, uint32_t aIndex) {
223 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
225 // If we've only yet created a fallback global Application menu (using
226 // ContructFallbackNativeMenus()), destroy it before recreating it properly.
227 if (sApplicationMenu && sApplicationMenuIsFallback) {
228 ResetNativeApplicationMenu();
230 // If we haven't created a global Application menu yet, do it.
231 if (!sApplicationMenu) {
232 CreateApplicationMenu(aMenu.get());
234 // Hook the new Application menu up to the menu bar.
235 NSMenu* mainMenu = NSApp.mainMenu;
237 mainMenu.numberOfItems > 0,
238 "Main menu does not have any items, something is terribly wrong!");
239 [mainMenu itemAtIndex:0].submenu = sApplicationMenu;
242 // add menu to array that owns our menus
243 mMenuArray.InsertElementAt(aIndex, aMenu);
246 RefPtr<nsIContent> menuContent = aMenu->Content();
247 if (menuContent->GetChildCount() > 0 &&
248 !nsMenuUtilsX::NodeIsHiddenOrCollapsed(menuContent)) {
249 MenuChildChangedVisibility(MenuChild(aMenu), true);
252 NS_OBJC_END_TRY_ABORT_BLOCK;
255 void nsMenuBarX::RemoveMenuAtIndex(uint32_t aIndex) {
256 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
258 if (mMenuArray.Length() <= aIndex) {
259 NS_ERROR("Attempting submenu removal with bad index!");
263 RefPtr<nsMenuX> menu = mMenuArray[aIndex];
264 mMenuArray.RemoveElementAt(aIndex);
266 menu->DetachFromGroupOwnerRecursive();
267 menu->DetachFromParent();
269 // Our native menu and our internal menu object array might be out of sync.
270 // This happens, for example, when a submenu is hidden. Because of this we
271 // should not assume that a native submenu is hooked up.
272 NSMenuItem* nativeMenuItem = menu->NativeNSMenuItem();
273 int nativeMenuItemIndex = [mNativeMenu indexOfItem:nativeMenuItem];
274 if (nativeMenuItemIndex != -1) {
275 [mNativeMenu removeItemAtIndex:nativeMenuItemIndex];
278 NS_OBJC_END_TRY_ABORT_BLOCK;
281 void nsMenuBarX::ObserveAttributeChanged(mozilla::dom::Document* aDocument,
282 nsIContent* aContent,
283 nsAtom* aAttribute) {}
285 void nsMenuBarX::ObserveContentRemoved(mozilla::dom::Document* aDocument,
286 nsIContent* aContainer,
287 nsIContent* aChild) {
288 nsINode* parent = NODE_FROM(aContainer, aDocument);
290 const Maybe<uint32_t> index = parent->ComputeIndexOf(aChild);
291 MOZ_ASSERT(*index != UINT32_MAX);
292 RemoveMenuAtIndex(index.valueOr(0u));
295 void nsMenuBarX::ObserveContentInserted(mozilla::dom::Document* aDocument,
296 nsIContent* aContainer,
297 nsIContent* aChild) {
298 InsertMenuAtIndex(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aChild),
299 aContainer->ComputeIndexOf(aChild).valueOr(UINT32_MAX));
302 void nsMenuBarX::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
303 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
305 NSString* locationString =
306 [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
307 aIndexString.BeginReading())
308 length:aIndexString.Length()];
309 NSArray* indexes = [locationString componentsSeparatedByString:@"|"];
310 unsigned int indexCount = indexes.count;
311 if (indexCount == 0) {
315 RefPtr<nsMenuX> currentMenu = nullptr;
316 int targetIndex = [[indexes objectAtIndex:0] intValue];
318 uint32_t length = mMenuArray.Length();
319 // first find a menu in the menu bar
320 for (unsigned int i = 0; i < length; i++) {
321 RefPtr<nsMenuX> menu = mMenuArray[i];
322 if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(menu->Content())) {
324 if (visible == (targetIndex + 1)) {
325 currentMenu = std::move(menu);
335 // fake open/close to cause lazy update to happen so submenus populate
336 currentMenu->MenuOpened();
337 currentMenu->MenuClosed();
339 // now find the correct submenu
340 for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
341 targetIndex = [[indexes objectAtIndex:i] intValue];
343 length = currentMenu->GetItemCount();
344 for (unsigned int j = 0; j < length; j++) {
345 Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
349 RefPtr<nsIContent> content = targetMenu->match(
350 [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
351 [](const RefPtr<nsMenuItemX>& aMenuItem) {
352 return aMenuItem->Content();
354 if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
356 if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
357 currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
358 // fake open/close to cause lazy update to happen
359 currentMenu->MenuOpened();
360 currentMenu->MenuClosed();
367 NS_OBJC_END_TRY_ABORT_BLOCK;
370 // Calling this forces a full reload of the menu system, reloading all native
371 // menus and their items.
372 // Without this testing is hard because changes to the DOM affect the native
373 // menu system lazily.
374 void nsMenuBarX::ForceNativeMenuReload() {
375 // tear down everything
376 while (GetMenuCount() > 0) {
377 RemoveMenuAtIndex(0);
380 // construct everything
381 ConstructNativeMenus();
384 nsMenuX* nsMenuBarX::GetMenuAt(uint32_t aIndex) {
385 if (mMenuArray.Length() <= aIndex) {
386 NS_ERROR("Requesting menu at invalid index!");
389 return mMenuArray[aIndex].get();
392 nsMenuX* nsMenuBarX::GetXULHelpMenu() {
393 // The Help menu is usually (always?) the last one, so we start there and
395 for (int32_t i = GetMenuCount() - 1; i >= 0; --i) {
396 nsMenuX* aMenu = GetMenuAt(i);
397 if (aMenu && nsMenuX::IsXULHelpMenu(aMenu->Content())) {
404 // On SnowLeopard and later we must tell the OS which is our Help menu.
405 // Otherwise it will only add Spotlight for Help (the Search item) to our
406 // Help menu if its label/title is "Help" -- i.e. if the menu is in English.
407 // This resolves bugs 489196 and 539317.
408 void nsMenuBarX::SetSystemHelpMenu() {
409 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
411 nsMenuX* xulHelpMenu = GetXULHelpMenu();
413 NSMenu* helpMenu = xulHelpMenu->NativeNSMenu();
415 NSApp.helpMenu = helpMenu;
419 NS_OBJC_END_TRY_ABORT_BLOCK;
422 // macOS is adding some (currently 3) hidden menu items every time that
423 // `NSApp.mainMenu` is set, but never removes them. This ultimately causes a
424 // significant slowdown when switching between windows because the number of
425 // items in `NSApp.mainMenu` is growing without bounds.
427 // The known hidden, problematic menu items are associated with the following
429 // - Start Dictation...
432 // Removing these items before setting `NSApp.mainMenu` prevents this slowdown.
434 static bool RemoveProblematicMenuItems(NSMenu* aMenu) {
435 uint8_t problematicMenuItemCount = 3;
436 NSMutableArray* itemsToRemove =
437 [NSMutableArray arrayWithCapacity:problematicMenuItemCount];
439 for (NSInteger i = 0; i < aMenu.numberOfItems; i++) {
440 NSMenuItem* item = [aMenu itemAtIndex:i];
443 (item.action == @selector(startDictation:) ||
444 item.action == @selector(orderFrontCharacterPalette:))) {
445 [itemsToRemove addObject:@(i)];
448 if (item.hasSubmenu && RemoveProblematicMenuItems(item.submenu)) {
453 bool didRemoveItems = false;
454 for (NSNumber* index in [itemsToRemove reverseObjectEnumerator]) {
455 [aMenu removeItemAtIndex:index.integerValue];
456 didRemoveItems = true;
459 return didRemoveItems;
462 nsresult nsMenuBarX::Paint() {
463 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
465 // Don't try to optimize anything in this painting by checking
466 // sLastGeckoMenuBarPainted because the menubar can be manipulated by
467 // native dialogs and sheet code and other things besides this paint method.
469 // We have to keep the same menu item for the Application menu so we keep
471 NSMenu* outgoingMenu = [NSApp.mainMenu retain];
473 outgoingMenu.numberOfItems > 0,
474 "Main menu does not have any items, something is terribly wrong!");
476 NSMenuItem* appMenuItem = [[outgoingMenu itemAtIndex:0] retain];
477 [outgoingMenu removeItemAtIndex:0];
479 [mNativeMenu insertItem:appMenuItem atIndex:0];
481 [appMenuItem release];
482 [outgoingMenu release];
484 NS_OBJC_END_TRY_ABORT_BLOCK;
485 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
487 RemoveProblematicMenuItems(mNativeMenu);
489 NS_OBJC_END_TRY_ABORT_BLOCK;
490 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
492 // Set menu bar and event target.
493 NSApp.mainMenu = mNativeMenu;
495 NS_OBJC_END_TRY_ABORT_BLOCK;
496 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
499 nsMenuBarX::sLastGeckoMenuBarPainted = this;
501 gSomeMenuBarPainted = YES;
505 NS_OBJC_END_TRY_ABORT_BLOCK;
508 // Dispatching the paint of the menu bar prevents crashes when macOS is actively
509 // enumerating the menu items in `NSApp.mainMenu`.
510 void nsMenuBarX::PaintAsync() {
511 NS_DispatchToCurrentThread(
512 NewRunnableMethod("PaintMenuBar", this, &nsMenuBarX::Paint));
516 void nsMenuBarX::ResetNativeApplicationMenu() {
517 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
519 [sApplicationMenu removeAllItems];
520 [sApplicationMenu release];
521 sApplicationMenu = nil;
522 sApplicationMenuIsFallback = NO;
524 NS_OBJC_END_TRY_ABORT_BLOCK;
527 void nsMenuBarX::SetNeedsRebuild() { mNeedsRebuild = true; }
529 void nsMenuBarX::ApplicationMenuOpened() {
531 if (!mMenuArray.IsEmpty()) {
532 ResetNativeApplicationMenu();
533 CreateApplicationMenu(mMenuArray[0].get());
535 mNeedsRebuild = false;
539 bool nsMenuBarX::PerformKeyEquivalent(NSEvent* aEvent) {
540 return [mNativeMenu performSuperKeyEquivalent:aEvent];
543 void nsMenuBarX::MenuChildChangedVisibility(const MenuChild& aChild,
545 MOZ_RELEASE_ASSERT(aChild.is<RefPtr<nsMenuX>>(),
546 "nsMenuBarX only has nsMenuX children");
547 const RefPtr<nsMenuX>& child = aChild.as<RefPtr<nsMenuX>>();
548 NSMenuItem* item = child->NativeNSMenuItem();
550 NSInteger insertionPoint = CalculateNativeInsertionPoint(child);
551 [mNativeMenu insertItem:item atIndex:insertionPoint];
552 } else if ([mNativeMenu indexOfItem:item] != -1) {
553 [mNativeMenu removeItem:item];
557 NSInteger nsMenuBarX::CalculateNativeInsertionPoint(nsMenuX* aChild) {
558 NSInteger insertionPoint = MenuContainsAppMenu() ? 1 : 0;
559 for (auto& currMenu : mMenuArray) {
560 if (currMenu == aChild) {
561 return insertionPoint;
563 // Only count items that are inside a menu.
564 // XXXmstange Not sure what would cause free-standing items. Maybe for
565 // collapsed/hidden menus? In that case, an nsMenuX::IsVisible() method
567 if (currMenu->NativeNSMenuItem().menu) {
571 return insertionPoint;
574 // Hide the item in the menu by setting the 'hidden' attribute. Returns it so
575 // the caller can hang onto it if they so choose.
576 RefPtr<Element> nsMenuBarX::HideItem(mozilla::dom::Document* aDocument,
577 const nsAString& aID) {
578 RefPtr<Element> menuElement = aDocument->GetElementById(aID);
580 menuElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden, u"true"_ns,
586 // Do what is necessary to conform to the Aqua guidelines for menus.
587 void nsMenuBarX::AquifyMenuBar() {
588 RefPtr<mozilla::dom::Document> domDoc = mContent->GetComposedDoc();
590 // remove the "About..." item and its separator
591 HideItem(domDoc, u"aboutSeparator"_ns);
592 mAboutItemContent = HideItem(domDoc, u"aboutName"_ns);
593 if (!sAboutItemContent) {
594 sAboutItemContent = mAboutItemContent;
597 // remove quit item and its separator
598 HideItem(domDoc, u"menu_FileQuitSeparator"_ns);
599 mQuitItemContent = HideItem(domDoc, u"menu_FileQuitItem"_ns);
600 if (!sQuitItemContent) {
601 sQuitItemContent = mQuitItemContent;
604 // remove prefs item and its separator, but save off the pref content node
605 // so we can invoke its command later.
606 HideItem(domDoc, u"menu_PrefsSeparator"_ns);
607 mPrefItemContent = HideItem(domDoc, u"menu_preferences"_ns);
608 if (!sPrefItemContent) {
609 sPrefItemContent = mPrefItemContent;
612 // remove Account Settings item.
613 mAccountItemContent = HideItem(domDoc, u"menu_accountmgr"_ns);
614 if (!sAccountItemContent) {
615 sAccountItemContent = mAccountItemContent;
618 // hide items that we use for the Application menu
619 HideItem(domDoc, u"menu_mac_services"_ns);
620 HideItem(domDoc, u"menu_mac_hide_app"_ns);
621 HideItem(domDoc, u"menu_mac_hide_others"_ns);
622 HideItem(domDoc, u"menu_mac_show_all"_ns);
623 HideItem(domDoc, u"menu_mac_touch_bar"_ns);
627 // for creating menu items destined for the Application menu
628 NSMenuItem* nsMenuBarX::CreateNativeAppMenuItem(nsMenuX* aMenu,
629 const nsAString& aNodeID,
630 SEL aAction, int aTag,
631 NativeMenuItemTarget* aTarget) {
632 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
634 RefPtr<mozilla::dom::Document> doc = aMenu->Content()->GetUncomposedDoc();
639 RefPtr<mozilla::dom::Element> menuItem = doc->GetElementById(aNodeID);
644 // Check collapsed rather than hidden since the app menu items are always
645 // hidden in AquifyMenuBar.
646 if (menuItem->AttrValueIs(kNameSpaceID_None, nsGkAtoms::collapsed,
647 nsGkAtoms::_true, eCaseMatters)) {
651 // Get information from the gecko menu item
653 nsAutoString modifiers;
655 menuItem->GetAttr(nsGkAtoms::label, label);
656 menuItem->GetAttr(nsGkAtoms::modifiers, modifiers);
657 menuItem->GetAttr(nsGkAtoms::key, key);
659 // Get more information about the key equivalent. Start by
660 // finding the key node we need.
661 NSString* keyEquiv = nil;
662 unsigned int macKeyModifiers = 0;
663 if (!key.IsEmpty()) {
664 RefPtr<Element> keyElement = doc->GetElementById(key);
666 // first grab the key equivalent character
667 nsAutoString keyChar(u" "_ns);
668 keyElement->GetAttr(nsGkAtoms::key, keyChar);
669 if (!keyChar.EqualsLiteral(" ")) {
670 keyEquiv = [[NSString
671 stringWithCharacters:reinterpret_cast<const unichar*>(keyChar.get())
672 length:keyChar.Length()] lowercaseString];
674 // now grab the key equivalent modifiers
675 nsAutoString modifiersStr;
676 keyElement->GetAttr(nsGkAtoms::modifiers, modifiersStr);
677 uint8_t geckoModifiers =
678 nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
680 nsMenuUtilsX::MacModifiersForGeckoModifiers(geckoModifiers);
683 // get the label into NSString-form
684 NSString* labelString = [NSString
685 stringWithCharacters:reinterpret_cast<const unichar*>(label.get())
686 length:label.Length()];
695 // put together the actual NSMenuItem
696 NSMenuItem* newMenuItem = [[GeckoNSMenuItem alloc] initWithTitle:labelString
698 keyEquivalent:keyEquiv];
700 newMenuItem.tag = aTag;
701 newMenuItem.target = aTarget;
702 newMenuItem.keyEquivalentModifierMask = macKeyModifiers;
703 newMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
707 NS_OBJC_END_TRY_ABORT_BLOCK;
710 // build the Application menu shared by all menu bars
711 void nsMenuBarX::CreateApplicationMenu(nsMenuX* aMenu) {
712 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
714 // At this point, the application menu is the application menu from
715 // the nib in cocoa widgets. We do not have a way to create an application
716 // menu manually, so we grab the one from the nib and use that.
717 sApplicationMenu = [[NSApp.mainMenu itemAtIndex:0].submenu retain];
720 We support the following menu items here:
722 Menu Item DOM Node ID Notes
724 ========================
725 = About This App = <- aboutName
726 ========================
727 = Preferences... = <- menu_preferences
728 = Account Settings = <- menu_accountmgr Only on Thunderbird
729 ========================
730 = Services > = <- menu_mac_services <- (do not define key
732 ========================
733 = Hide App = <- menu_mac_hide_app
734 = Hide Others = <- menu_mac_hide_others
735 = Show All = <- menu_mac_show_all
736 ========================
737 = Customize Touch Bar… = <- menu_mac_touch_bar
738 ========================
739 = Quit = <- menu_FileQuitItem
740 ========================
742 If any of them are ommitted from the application's DOM, we just don't add
743 them. We always add a "Quit" item, but if an app developer does not provide
744 a DOM node with the right ID for the Quit item, we add it in English. App
745 developers need only add each node with a label and a key equivalent (if
746 they want one). Other attributes are optional. Like so:
748 <menuitem id="menu_preferences"
749 label="&preferencesCmdMac.label;"
750 key="open_prefs_key"/>
752 We need to use this system for localization purposes, until we have a better
753 way to define the Application menu to be used on Mac OS X.
756 if (sApplicationMenu) {
757 if (!mApplicationMenuDelegate) {
758 mApplicationMenuDelegate =
759 [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
761 sApplicationMenu.delegate = mApplicationMenuDelegate;
763 // This code reads attributes we are going to care about from the DOM
766 NSMenuItem* itemBeingAdded = nil;
767 BOOL addAboutSeparator = FALSE;
768 BOOL addPrefsSeparator = FALSE;
770 // Add the About menu item
771 itemBeingAdded = CreateNativeAppMenuItem(
772 aMenu, u"aboutName"_ns, @selector(menuItemHit:), eCommand_ID_About,
773 nsMenuBarX::sNativeEventTarget);
774 if (itemBeingAdded) {
775 [sApplicationMenu addItem:itemBeingAdded];
776 [itemBeingAdded release];
777 itemBeingAdded = nil;
779 addAboutSeparator = TRUE;
782 // Add separator if either the About item or software update item exists
783 if (addAboutSeparator) {
784 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
787 // Add the Preferences menu item
788 itemBeingAdded = CreateNativeAppMenuItem(
789 aMenu, u"menu_preferences"_ns, @selector(menuItemHit:),
790 eCommand_ID_Prefs, nsMenuBarX::sNativeEventTarget);
791 if (itemBeingAdded) {
792 [sApplicationMenu addItem:itemBeingAdded];
793 [itemBeingAdded release];
794 itemBeingAdded = nil;
796 addPrefsSeparator = TRUE;
799 // Add the Account Settings menu item. This is Thunderbird only
800 itemBeingAdded = CreateNativeAppMenuItem(
801 aMenu, u"menu_accountmgr"_ns, @selector(menuItemHit:),
802 eCommand_ID_Account, nsMenuBarX::sNativeEventTarget);
803 if (itemBeingAdded) {
804 [sApplicationMenu addItem:itemBeingAdded];
805 [itemBeingAdded release];
806 itemBeingAdded = nil;
809 // Add separator after Preferences menu
810 if (addPrefsSeparator) {
811 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
814 // Add Services menu item
816 CreateNativeAppMenuItem(aMenu, u"menu_mac_services"_ns, nil, 0, nil);
817 if (itemBeingAdded) {
818 [sApplicationMenu addItem:itemBeingAdded];
820 // set this menu item up as the Mac OS X Services menu
821 NSMenu* servicesMenu = [[GeckoNSMenu alloc] initWithTitle:@""];
822 itemBeingAdded.submenu = servicesMenu;
823 NSApp.servicesMenu = servicesMenu;
825 [itemBeingAdded release];
826 itemBeingAdded = nil;
828 // Add separator after Services menu
829 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
832 BOOL addHideShowSeparator = FALSE;
834 // Add menu item to hide this application
835 itemBeingAdded = CreateNativeAppMenuItem(
836 aMenu, u"menu_mac_hide_app"_ns, @selector(menuItemHit:),
837 eCommand_ID_HideApp, nsMenuBarX::sNativeEventTarget);
838 if (itemBeingAdded) {
839 [sApplicationMenu addItem:itemBeingAdded];
840 [itemBeingAdded release];
841 itemBeingAdded = nil;
843 addHideShowSeparator = TRUE;
846 // Add menu item to hide other applications
847 itemBeingAdded = CreateNativeAppMenuItem(
848 aMenu, u"menu_mac_hide_others"_ns, @selector(menuItemHit:),
849 eCommand_ID_HideOthers, nsMenuBarX::sNativeEventTarget);
850 if (itemBeingAdded) {
851 [sApplicationMenu addItem:itemBeingAdded];
852 [itemBeingAdded release];
853 itemBeingAdded = nil;
855 addHideShowSeparator = TRUE;
858 // Add menu item to show all applications
859 itemBeingAdded = CreateNativeAppMenuItem(
860 aMenu, u"menu_mac_show_all"_ns, @selector(menuItemHit:),
861 eCommand_ID_ShowAll, nsMenuBarX::sNativeEventTarget);
862 if (itemBeingAdded) {
863 [sApplicationMenu addItem:itemBeingAdded];
864 [itemBeingAdded release];
865 itemBeingAdded = nil;
867 addHideShowSeparator = TRUE;
870 // Add a separator after the hide/show menus if at least one exists
871 if (addHideShowSeparator) {
872 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
875 BOOL addTouchBarSeparator = NO;
877 // Add Touch Bar customization menu item.
878 itemBeingAdded = CreateNativeAppMenuItem(
879 aMenu, u"menu_mac_touch_bar"_ns, @selector(menuItemHit:),
880 eCommand_ID_TouchBar, nsMenuBarX::sNativeEventTarget);
882 if (itemBeingAdded) {
883 [sApplicationMenu addItem:itemBeingAdded];
884 // We hide the menu item on Macs that don't have a Touch Bar.
885 if (!sTouchBarIsInitialized) {
886 [itemBeingAdded setHidden:YES];
888 addTouchBarSeparator = YES;
890 [itemBeingAdded release];
891 itemBeingAdded = nil;
894 // Add a separator after the Touch Bar menu item if it exists
895 if (addTouchBarSeparator) {
896 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
899 // Add quit menu item
900 itemBeingAdded = CreateNativeAppMenuItem(
901 aMenu, u"menu_FileQuitItem"_ns, @selector(menuItemHit:),
902 eCommand_ID_Quit, nsMenuBarX::sNativeEventTarget);
903 if (itemBeingAdded) {
904 [sApplicationMenu addItem:itemBeingAdded];
905 [itemBeingAdded release];
906 itemBeingAdded = nil;
908 // the current application does not have a DOM node for "Quit". Add one
909 // anyway, in English.
910 NSMenuItem* defaultQuitItem =
911 [[[GeckoNSMenuItem alloc] initWithTitle:@"Quit"
912 action:@selector(menuItemHit:)
913 keyEquivalent:@"q"] autorelease];
914 defaultQuitItem.target = nsMenuBarX::sNativeEventTarget;
915 defaultQuitItem.tag = eCommand_ID_Quit;
916 [sApplicationMenu addItem:defaultQuitItem];
920 NS_OBJC_END_TRY_ABORT_BLOCK;
923 // Objective-C class used for menu items to allow Gecko to override their
924 // standard behavior in order to stop key equivalents from firing in certain
925 // instances. When gMenuItemsExecuteCommands is NO, we return a dummy target and
926 // action instead of the actual target and action.
927 @implementation GeckoNSMenuItem
930 id realTarget = super.target;
931 if (gMenuItemsExecuteCommands) {
934 return realTarget ? self : nil;
938 SEL realAction = super.action;
939 if (gMenuItemsExecuteCommands) {
942 return realAction ? @selector(_doNothing:) : nullptr;
945 - (void)_doNothing:(id)aSender {
951 // Objective-C class used to allow us to have keyboard commands
952 // look like they are doing something but actually do nothing.
953 // We allow mouse actions to work normally.
955 @implementation GeckoNSMenu
957 // Keyboard commands should not cause menu items to invoke their
958 // commands when there is a key window because we'd rather send
959 // the keyboard command to the window. We still have the menus
960 // go through the mechanics so they'll give the proper visual
962 - (BOOL)performKeyEquivalent:(NSEvent*)aEvent {
963 // We've noticed that Mac OS X expects this check in subclasses before
964 // calling NSMenu's "performKeyEquivalent:".
966 // There is no case in which we'd need to do anything or return YES
967 // when we have no items so we can just do this check first.
968 if (self.numberOfItems <= 0) {
972 NSWindow* keyWindow = NSApp.keyWindow;
974 // If there is no key window then just behave normally. This
975 // probably means that this menu is associated with Gecko's
978 return [super performKeyEquivalent:aEvent];
981 NSResponder* firstResponder = keyWindow.firstResponder;
983 if ([keyWindow isKindOfClass:[BaseWindow class]]) {
984 gMenuItemsExecuteCommands = NO;
987 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
988 [super performKeyEquivalent:aEvent];
989 NS_OBJC_END_TRY_IGNORE_BLOCK
991 gMenuItemsExecuteCommands = YES; // return to default
993 // Return YES if we invoked a command and there is now no key window or we
994 // changed the first responder. In this case we do not want to propagate the
995 // event because we don't want it handled again.
996 if (!NSApp.keyWindow || NSApp.keyWindow.firstResponder != firstResponder) {
1000 // Return NO so that we can handle the event via NSView's "keyDown:".
1004 - (BOOL)performSuperKeyEquivalent:(NSEvent*)aEvent {
1005 return [super performKeyEquivalent:aEvent];
1008 - (void)addItem:(NSMenuItem*)aNewItem {
1009 [self _overrideClassOfMenuItem:aNewItem];
1010 [super addItem:aNewItem];
1013 - (NSMenuItem*)addItemWithTitle:(NSString*)aString
1014 action:(SEL)aSelector
1015 keyEquivalent:(NSString*)aKeyEquiv {
1016 NSMenuItem* newItem = [super addItemWithTitle:aString
1018 keyEquivalent:aKeyEquiv];
1019 [self _overrideClassOfMenuItem:newItem];
1023 - (void)insertItem:(NSMenuItem*)aNewItem atIndex:(NSInteger)aIndex {
1024 [self _overrideClassOfMenuItem:aNewItem];
1025 [super insertItem:aNewItem atIndex:aIndex];
1028 - (NSMenuItem*)insertItemWithTitle:(NSString*)aString
1029 action:(SEL)aSelector
1030 keyEquivalent:(NSString*)aKeyEquiv
1031 atIndex:(NSInteger)aIndex {
1032 NSMenuItem* newItem = [super insertItemWithTitle:aString
1034 keyEquivalent:aKeyEquiv
1036 [self _overrideClassOfMenuItem:newItem];
1040 - (void)_overrideClassOfMenuItem:(NSMenuItem*)aMenuItem {
1041 if ([aMenuItem class] == [NSMenuItem class]) {
1042 // See class comment for `GeckoNSMenuItem` above for an explanation of why
1044 object_setClass(aMenuItem, [GeckoNSMenuItem class]);
1051 // Objective-C class used as action target for menu items
1054 @implementation NativeMenuItemTarget
1056 // called when some menu item in this menu gets hit
1057 - (IBAction)menuItemHit:(id)aSender {
1058 // We should never get here when we do not want menu items to execute their
1060 MOZ_RELEASE_ASSERT(gMenuItemsExecuteCommands);
1062 if (![aSender isKindOfClass:[NSMenuItem class]]) {
1066 NSMenuItem* nativeMenuItem = (NSMenuItem*)aSender;
1067 NSInteger tag = nativeMenuItem.tag;
1069 nsMenuGroupOwnerX* menuGroupOwner = nullptr;
1070 nsMenuBarX* menuBar = nullptr;
1071 MOZMenuItemRepresentedObject* representedObject =
1072 nativeMenuItem.representedObject;
1074 if (representedObject) {
1075 menuGroupOwner = representedObject.menuGroupOwner;
1076 if (!menuGroupOwner) {
1079 menuBar = menuGroupOwner->GetMenuBar();
1082 // Notify containing menu about the fact that a menu item will be activated.
1083 NSMenu* menu = nativeMenuItem.menu;
1084 if ([menu.delegate isKindOfClass:[MenuDelegate class]]) {
1085 [(MenuDelegate*)menu.delegate menu:menu willActivateItem:nativeMenuItem];
1088 // Get the modifier flags and button for this menu item activation. The menu
1089 // system does not pass an NSEvent to our action selector, but we can query
1090 // the current NSEvent instead. The current NSEvent can be a key event or a
1091 // mouseup event, depending on how the menu item is activated.
1092 NSEventModifierFlags modifierFlags =
1093 NSApp.currentEvent ? NSApp.currentEvent.modifierFlags : 0;
1094 mozilla::MouseButton button =
1095 NSApp.currentEvent ? nsCocoaUtils::ButtonForEvent(NSApp.currentEvent)
1096 : mozilla::MouseButton::ePrimary;
1098 // Do special processing if this is for an app-global command.
1099 if (tag == eCommand_ID_About) {
1100 nsIContent* mostSpecificContent = sAboutItemContent;
1101 if (menuBar && menuBar->mAboutItemContent) {
1102 mostSpecificContent = menuBar->mAboutItemContent;
1104 nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
1107 if (tag == eCommand_ID_Prefs) {
1108 nsIContent* mostSpecificContent = sPrefItemContent;
1109 if (menuBar && menuBar->mPrefItemContent) {
1110 mostSpecificContent = menuBar->mPrefItemContent;
1112 nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
1115 if (tag == eCommand_ID_Account) {
1116 nsIContent* mostSpecificContent = sAccountItemContent;
1117 if (menuBar && menuBar->mAccountItemContent) {
1118 mostSpecificContent = menuBar->mAccountItemContent;
1120 nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
1123 if (tag == eCommand_ID_HideApp) {
1124 [NSApp hide:aSender];
1127 if (tag == eCommand_ID_HideOthers) {
1128 [NSApp hideOtherApplications:aSender];
1131 if (tag == eCommand_ID_ShowAll) {
1132 [NSApp unhideAllApplications:aSender];
1135 if (tag == eCommand_ID_TouchBar) {
1136 [NSApp toggleTouchBarCustomizationPalette:aSender];
1139 if (tag == eCommand_ID_Quit) {
1140 nsIContent* mostSpecificContent = sQuitItemContent;
1141 if (menuBar && menuBar->mQuitItemContent) {
1142 mostSpecificContent = menuBar->mQuitItemContent;
1144 // If we have some content for quit we execute it. Otherwise we send a
1145 // native app terminate message. If you want to stop a quit from happening,
1146 // provide quit content and return the event as unhandled.
1147 if (mostSpecificContent) {
1148 nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags,
1151 nsCOMPtr<nsIAppStartup> appStartup =
1152 mozilla::components::AppStartup::Service();
1154 bool userAllowedQuit = true;
1155 appStartup->Quit(nsIAppStartup::eAttemptQuit, 0, &userAllowedQuit);
1161 // given the commandID, look it up in our hashtable and dispatch to
1163 if (menuGroupOwner) {
1164 if (RefPtr<nsMenuItemX> menuItem = menuGroupOwner->GetMenuItemForCommandID(
1165 static_cast<uint32_t>(tag))) {
1166 if (nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest) {
1167 menuItem->DoCommand(modifierFlags, button);
1168 } else if (RefPtr<nsMenuX> menu = menuItem->ParentMenu()) {
1169 menu->ActivateItemAfterClosing(std::move(menuItem), modifierFlags,