Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / widget / cocoa / nsMenuBarX.mm
blobaa599c52b6fb9520a2e644cfe5c43f8dabaa32ea
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"
14 #include "nsMenuX.h"
16 #include "nsCOMPtr.h"
17 #include "nsString.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;
68   }
69   return self;
72 - (void)menuWillOpen:(NSMenu*)menu {
73   mApplicationMenu->ApplicationMenuOpened();
76 - (void)menuDidClose:(NSMenu*)menu {
79 @end
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"];
89   mContent = aElement;
91   if (mContent) {
92     AquifyMenuBar();
93     mMenuGroupOwner->RegisterForContentChanges(mContent, this);
94     ConstructNativeMenus();
95   } else {
96     ConstructFallbackNativeMenus();
97   }
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;
107   }
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;
113   }
114   if (sQuitItemContent == mQuitItemContent) {
115     sQuitItemContent = nullptr;
116   }
117   if (sPrefItemContent == mPrefItemContent) {
118     sPrefItemContent = nullptr;
119   }
120   if (sAccountItemContent == mAccountItemContent) {
121     sAccountItemContent = nullptr;
122   }
124   mMenuGroupOwner->UnregisterForLocaleChanges();
126   // make sure we unregister ourselves as a content observer
127   if (mContent) {
128     mMenuGroupOwner->UnregisterForContentChanges(mContent);
129   }
131   for (nsMenuX* menu : mMenuArray) {
132     menu->DetachFromGroupOwnerRecursive();
133     menu->DetachFromParent();
134   }
136   if (mApplicationMenuDelegate) {
137     [mApplicationMenuDelegate release];
138   }
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)) {
149       InsertMenuAtIndex(
150           MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, menuContent->AsElement()),
151           GetMenuCount());
152     }
153   }
156 void nsMenuBarX::ConstructFallbackNativeMenus() {
157   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
159   if (sApplicationMenu) {
160     // Menu has already been built.
161     return;
162   }
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));
171   if (!stringBundle) {
172     return;
173   }
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);
184   NSString* labelStr =
185       [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(labelUTF16).get()];
186   NSString* keyStr =
187       [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(keyUTF16).get()];
189   if (!nsMenuBarX::sNativeEventTarget) {
190     nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
191   }
193   sApplicationMenu = [[[[NSApp mainMenu] itemAtIndex:0] submenu] retain];
194   if (!mApplicationMenuDelegate) {
195     mApplicationMenuDelegate =
196         [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
197   }
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();
229   }
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;
236     NS_ASSERTION(
237         mainMenu.numberOfItems > 0,
238         "Main menu does not have any items, something is terribly wrong!");
239     [mainMenu itemAtIndex:0].submenu = sApplicationMenu;
240   }
242   // add menu to array that owns our menus
243   mMenuArray.InsertElementAt(aIndex, aMenu);
245   // hook up submenus
246   RefPtr<nsIContent> menuContent = aMenu->Content();
247   if (menuContent->GetChildCount() > 0 &&
248       !nsMenuUtilsX::NodeIsHiddenOrCollapsed(menuContent)) {
249     MenuChildChangedVisibility(MenuChild(aMenu), true);
250   }
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!");
260     return;
261   }
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];
276   }
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);
289   MOZ_ASSERT(parent);
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) {
312     return;
313   }
315   RefPtr<nsMenuX> currentMenu = nullptr;
316   int targetIndex = [[indexes objectAtIndex:0] intValue];
317   int visible = 0;
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())) {
323       visible++;
324       if (visible == (targetIndex + 1)) {
325         currentMenu = std::move(menu);
326         break;
327       }
328     }
329   }
331   if (!currentMenu) {
332     return;
333   }
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];
342     visible = 0;
343     length = currentMenu->GetItemCount();
344     for (unsigned int j = 0; j < length; j++) {
345       Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
346       if (!targetMenu) {
347         return;
348       }
349       RefPtr<nsIContent> content = targetMenu->match(
350           [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
351           [](const RefPtr<nsMenuItemX>& aMenuItem) {
352             return aMenuItem->Content();
353           });
354       if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
355         visible++;
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();
361           break;
362         }
363       }
364     }
365   }
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);
378   }
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!");
387     return nullptr;
388   }
389   return mMenuArray[aIndex].get();
392 nsMenuX* nsMenuBarX::GetXULHelpMenu() {
393   // The Help menu is usually (always?) the last one, so we start there and
394   // count back.
395   for (int32_t i = GetMenuCount() - 1; i >= 0; --i) {
396     nsMenuX* aMenu = GetMenuAt(i);
397     if (aMenu && nsMenuX::IsXULHelpMenu(aMenu->Content())) {
398       return aMenu;
399     }
400   }
401   return nil;
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();
412   if (xulHelpMenu) {
413     NSMenu* helpMenu = xulHelpMenu->NativeNSMenu();
414     if (helpMenu) {
415       NSApp.helpMenu = helpMenu;
416     }
417   }
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
428 // menus:
429 //   - Start Dictation...
430 //   - Emoji & Symbols
432 // Removing these items before setting `NSApp.mainMenu` prevents this slowdown.
433 // See bug 1808223.
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];
442     if (item.hidden &&
443         (item.action == @selector(startDictation:) ||
444          item.action == @selector(orderFrontCharacterPalette:))) {
445       [itemsToRemove addObject:@(i)];
446     }
448     if (item.hasSubmenu && RemoveProblematicMenuItems(item.submenu)) {
449       return true;
450     }
451   }
453   bool didRemoveItems = false;
454   for (NSNumber* index in [itemsToRemove reverseObjectEnumerator]) {
455     [aMenu removeItemAtIndex:index.integerValue];
456     didRemoveItems = true;
457   }
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
470   // passing it along.
471   NSMenu* outgoingMenu = [NSApp.mainMenu retain];
472   NS_ASSERTION(
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];
478   if (appMenuItem) {
479     [mNativeMenu insertItem:appMenuItem atIndex:0];
480   }
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;
498   SetSystemHelpMenu();
499   nsMenuBarX::sLastGeckoMenuBarPainted = this;
501   gSomeMenuBarPainted = YES;
503   return NS_OK;
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));
515 /* static */
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() {
530   if (mNeedsRebuild) {
531     if (!mMenuArray.IsEmpty()) {
532       ResetNativeApplicationMenu();
533       CreateApplicationMenu(mMenuArray[0].get());
534     }
535     mNeedsRebuild = false;
536   }
539 bool nsMenuBarX::PerformKeyEquivalent(NSEvent* aEvent) {
540   return [mNativeMenu performSuperKeyEquivalent:aEvent];
543 void nsMenuBarX::MenuChildChangedVisibility(const MenuChild& aChild,
544                                             bool aIsVisible) {
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();
549   if (aIsVisible) {
550     NSInteger insertionPoint = CalculateNativeInsertionPoint(child);
551     [mNativeMenu insertItem:item atIndex:insertionPoint];
552   } else if ([mNativeMenu indexOfItem:item] != -1) {
553     [mNativeMenu removeItem:item];
554   }
557 NSInteger nsMenuBarX::CalculateNativeInsertionPoint(nsMenuX* aChild) {
558   NSInteger insertionPoint = MenuContainsAppMenu() ? 1 : 0;
559   for (auto& currMenu : mMenuArray) {
560     if (currMenu == aChild) {
561       return insertionPoint;
562     }
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
566     // would be better.
567     if (currMenu->NativeNSMenuItem().menu) {
568       insertionPoint++;
569     }
570   }
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);
579   if (menuElement) {
580     menuElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden, u"true"_ns,
581                          false);
582   }
583   return menuElement;
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();
589   if (domDoc) {
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;
595     }
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;
602     }
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;
610     }
612     // remove Account Settings item.
613     mAccountItemContent = HideItem(domDoc, u"menu_accountmgr"_ns);
614     if (!sAccountItemContent) {
615       sAccountItemContent = mAccountItemContent;
616     }
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);
624   }
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();
635   if (!doc) {
636     return nil;
637   }
639   RefPtr<mozilla::dom::Element> menuItem = doc->GetElementById(aNodeID);
640   if (!menuItem) {
641     return nil;
642   }
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)) {
648     return nil;
649   }
651   // Get information from the gecko menu item
652   nsAutoString label;
653   nsAutoString modifiers;
654   nsAutoString key;
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);
665     if (keyElement) {
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];
673       }
674       // now grab the key equivalent modifiers
675       nsAutoString modifiersStr;
676       keyElement->GetAttr(nsGkAtoms::modifiers, modifiersStr);
677       uint8_t geckoModifiers =
678           nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
679       macKeyModifiers =
680           nsMenuUtilsX::MacModifiersForGeckoModifiers(geckoModifiers);
681     }
682   }
683   // get the label into NSString-form
684   NSString* labelString = [NSString
685       stringWithCharacters:reinterpret_cast<const unichar*>(label.get())
686                     length:label.Length()];
688   if (!labelString) {
689     labelString = @"";
690   }
691   if (!keyEquiv) {
692     keyEquiv = @"";
693   }
695   // put together the actual NSMenuItem
696   NSMenuItem* newMenuItem = [[GeckoNSMenuItem alloc] initWithTitle:labelString
697                                                             action:aAction
698                                                      keyEquivalent:keyEquiv];
700   newMenuItem.tag = aTag;
701   newMenuItem.target = aTarget;
702   newMenuItem.keyEquivalentModifierMask = macKeyModifiers;
703   newMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
705   return newMenuItem;
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];
719   /*
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
731     equivalent)
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.
754   */
756   if (sApplicationMenu) {
757     if (!mApplicationMenuDelegate) {
758       mApplicationMenuDelegate =
759           [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
760     }
761     sApplicationMenu.delegate = mApplicationMenuDelegate;
763     // This code reads attributes we are going to care about from the DOM
764     // elements
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;
780     }
782     // Add separator if either the About item or software update item exists
783     if (addAboutSeparator) {
784       [sApplicationMenu addItem:[NSMenuItem separatorItem]];
785     }
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;
797     }
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;
807     }
809     // Add separator after Preferences menu
810     if (addPrefsSeparator) {
811       [sApplicationMenu addItem:[NSMenuItem separatorItem]];
812     }
814     // Add Services menu item
815     itemBeingAdded =
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]];
830     }
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;
844     }
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;
856     }
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;
868     }
870     // Add a separator after the hide/show menus if at least one exists
871     if (addHideShowSeparator) {
872       [sApplicationMenu addItem:[NSMenuItem separatorItem]];
873     }
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];
887       } else {
888         addTouchBarSeparator = YES;
889       }
890       [itemBeingAdded release];
891       itemBeingAdded = nil;
892     }
894     // Add a separator after the Touch Bar menu item if it exists
895     if (addTouchBarSeparator) {
896       [sApplicationMenu addItem:[NSMenuItem separatorItem]];
897     }
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;
907     } else {
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];
917     }
918   }
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
929 - (id)target {
930   id realTarget = super.target;
931   if (gMenuItemsExecuteCommands) {
932     return realTarget;
933   }
934   return realTarget ? self : nil;
937 - (SEL)action {
938   SEL realAction = super.action;
939   if (gMenuItemsExecuteCommands) {
940     return realAction;
941   }
942   return realAction ? @selector(_doNothing:) : nullptr;
945 - (void)_doNothing:(id)aSender {
948 @end
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
961 // feedback.
962 - (BOOL)performKeyEquivalent:(NSEvent*)aEvent {
963   // We've noticed that Mac OS X expects this check in subclasses before
964   // calling NSMenu's "performKeyEquivalent:".
965   //
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) {
969     return NO;
970   }
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
976   // hidden window.
977   if (!keyWindow) {
978     return [super performKeyEquivalent:aEvent];
979   }
981   NSResponder* firstResponder = keyWindow.firstResponder;
983   if ([keyWindow isKindOfClass:[BaseWindow class]]) {
984     gMenuItemsExecuteCommands = NO;
985   }
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) {
997     return YES;
998   }
1000   // Return NO so that we can handle the event via NSView's "keyDown:".
1001   return NO;
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
1017                                          action:aSelector
1018                                   keyEquivalent:aKeyEquiv];
1019   [self _overrideClassOfMenuItem:newItem];
1020   return 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
1033                                             action:aSelector
1034                                      keyEquivalent:aKeyEquiv
1035                                            atIndex:aIndex];
1036   [self _overrideClassOfMenuItem:newItem];
1037   return newItem;
1040 - (void)_overrideClassOfMenuItem:(NSMenuItem*)aMenuItem {
1041   if ([aMenuItem class] == [NSMenuItem class]) {
1042     // See class comment for `GeckoNSMenuItem` above for an explanation of why
1043     // we do this.
1044     object_setClass(aMenuItem, [GeckoNSMenuItem class]);
1045   }
1048 @end
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
1059   // commands.
1060   MOZ_RELEASE_ASSERT(gMenuItemsExecuteCommands);
1062   if (![aSender isKindOfClass:[NSMenuItem class]]) {
1063     return;
1064   }
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) {
1077       return;
1078     }
1079     menuBar = menuGroupOwner->GetMenuBar();
1080   }
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];
1086   }
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;
1103     }
1104     nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
1105     return;
1106   }
1107   if (tag == eCommand_ID_Prefs) {
1108     nsIContent* mostSpecificContent = sPrefItemContent;
1109     if (menuBar && menuBar->mPrefItemContent) {
1110       mostSpecificContent = menuBar->mPrefItemContent;
1111     }
1112     nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
1113     return;
1114   }
1115   if (tag == eCommand_ID_Account) {
1116     nsIContent* mostSpecificContent = sAccountItemContent;
1117     if (menuBar && menuBar->mAccountItemContent) {
1118       mostSpecificContent = menuBar->mAccountItemContent;
1119     }
1120     nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
1121     return;
1122   }
1123   if (tag == eCommand_ID_HideApp) {
1124     [NSApp hide:aSender];
1125     return;
1126   }
1127   if (tag == eCommand_ID_HideOthers) {
1128     [NSApp hideOtherApplications:aSender];
1129     return;
1130   }
1131   if (tag == eCommand_ID_ShowAll) {
1132     [NSApp unhideAllApplications:aSender];
1133     return;
1134   }
1135   if (tag == eCommand_ID_TouchBar) {
1136     [NSApp toggleTouchBarCustomizationPalette:aSender];
1137     return;
1138   }
1139   if (tag == eCommand_ID_Quit) {
1140     nsIContent* mostSpecificContent = sQuitItemContent;
1141     if (menuBar && menuBar->mQuitItemContent) {
1142       mostSpecificContent = menuBar->mQuitItemContent;
1143     }
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,
1149                                       button);
1150     } else {
1151       nsCOMPtr<nsIAppStartup> appStartup =
1152           mozilla::components::AppStartup::Service();
1153       if (appStartup) {
1154         bool userAllowedQuit = true;
1155         appStartup->Quit(nsIAppStartup::eAttemptQuit, 0, &userAllowedQuit);
1156       }
1157     }
1158     return;
1159   }
1161   // given the commandID, look it up in our hashtable and dispatch to
1162   // that menu item.
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,
1170                                        button);
1171       }
1172     }
1173   }
1176 @end