Bug 1777562 [wpt PR 34663] - [FedCM] Rename FederatedCredential to IdentityCredential...
[gecko.git] / widget / cocoa / nsMenuBarX.mm
blob70a17d87f8536e105f34118ef0f08878aad6d814
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 "nsMenuBarX.h"
9 #include "nsMenuX.h"
10 #include "nsMenuItemX.h"
11 #include "nsMenuUtilsX.h"
12 #include "nsCocoaUtils.h"
13 #include "nsChildView.h"
15 #include "nsCOMPtr.h"
16 #include "nsString.h"
17 #include "nsGkAtoms.h"
18 #include "nsObjCExceptions.h"
19 #include "nsThreadUtils.h"
20 #include "nsTouchBarNativeAPIDefines.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 // defined in nsCocoaWindow.mm.
42 extern BOOL sTouchBarIsInitialized;
44 // We keep references to the first quit and pref item content nodes we find, which
45 // will be from the hidden window. We use these when the document for the current
46 // window does not have a quit or pref item. We don't need strong refs here because
47 // these items are always strong ref'd by their owning menu bar (instance variable).
48 static nsIContent* sAboutItemContent = nullptr;
49 static nsIContent* sPrefItemContent = nullptr;
50 static nsIContent* sAccountItemContent = nullptr;
51 static nsIContent* sQuitItemContent = nullptr;
54 // ApplicationMenuDelegate Objective-C class
57 @implementation ApplicationMenuDelegate
59 - (id)initWithApplicationMenu:(nsMenuBarX*)aApplicationMenu {
60   if ((self = [super init])) {
61     mApplicationMenu = aApplicationMenu;
62   }
63   return self;
66 - (void)menuWillOpen:(NSMenu*)menu {
67   mApplicationMenu->ApplicationMenuOpened();
70 - (void)menuDidClose:(NSMenu*)menu {
73 @end
75 nsMenuBarX::nsMenuBarX(mozilla::dom::Element* aElement)
76     : mNeedsRebuild(false), mApplicationMenuDelegate(nil) {
77   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
79   mMenuGroupOwner = new nsMenuGroupOwnerX(aElement, this);
80   mMenuGroupOwner->RegisterForLocaleChanges();
81   mNativeMenu = [[GeckoNSMenu alloc] initWithTitle:@"MainMenuBar"];
83   mContent = aElement;
85   if (mContent) {
86     AquifyMenuBar();
87     mMenuGroupOwner->RegisterForContentChanges(mContent, this);
88     ConstructNativeMenus();
89   } else {
90     ConstructFallbackNativeMenus();
91   }
93   NS_OBJC_END_TRY_ABORT_BLOCK;
96 nsMenuBarX::~nsMenuBarX() {
97   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
99   if (nsMenuBarX::sLastGeckoMenuBarPainted == this) {
100     nsMenuBarX::sLastGeckoMenuBarPainted = nullptr;
101   }
103   // the quit/pref items of a random window might have been used if there was no
104   // hidden window, thus we need to invalidate the weak references.
105   if (sAboutItemContent == mAboutItemContent) {
106     sAboutItemContent = nullptr;
107   }
108   if (sQuitItemContent == mQuitItemContent) {
109     sQuitItemContent = nullptr;
110   }
111   if (sPrefItemContent == mPrefItemContent) {
112     sPrefItemContent = nullptr;
113   }
114   if (sAccountItemContent == mAccountItemContent) {
115     sAccountItemContent = nullptr;
116   }
118   mMenuGroupOwner->UnregisterForLocaleChanges();
120   // make sure we unregister ourselves as a content observer
121   if (mContent) {
122     mMenuGroupOwner->UnregisterForContentChanges(mContent);
123   }
125   for (nsMenuX* menu : mMenuArray) {
126     menu->DetachFromGroupOwnerRecursive();
127     menu->DetachFromParent();
128   }
130   if (mApplicationMenuDelegate) {
131     [mApplicationMenuDelegate release];
132   }
134   [mNativeMenu release];
136   NS_OBJC_END_TRY_ABORT_BLOCK;
139 void nsMenuBarX::ConstructNativeMenus() {
140   for (nsIContent* menuContent = mContent->GetFirstChild(); menuContent;
141        menuContent = menuContent->GetNextSibling()) {
142     if (menuContent->IsXULElement(nsGkAtoms::menu)) {
143       InsertMenuAtIndex(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, menuContent->AsElement()),
144                         GetMenuCount());
145     }
146   }
149 void nsMenuBarX::ConstructFallbackNativeMenus() {
150   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
152   if (sApplicationMenu) {
153     // Menu has already been built.
154     return;
155   }
157   nsCOMPtr<nsIStringBundle> stringBundle;
159   nsCOMPtr<nsIStringBundleService> bundleSvc = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
160   bundleSvc->CreateBundle("chrome://global/locale/fallbackMenubar.properties",
161                           getter_AddRefs(stringBundle));
163   if (!stringBundle) {
164     return;
165   }
167   nsAutoString labelUTF16;
168   nsAutoString keyUTF16;
170   const char* labelProp = "quitMenuitem.label";
171   const char* keyProp = "quitMenuitem.key";
173   stringBundle->GetStringFromName(labelProp, labelUTF16);
174   stringBundle->GetStringFromName(keyProp, keyUTF16);
176   NSString* labelStr = [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(labelUTF16).get()];
177   NSString* keyStr = [NSString stringWithUTF8String:NS_ConvertUTF16toUTF8(keyUTF16).get()];
179   if (!nsMenuBarX::sNativeEventTarget) {
180     nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
181   }
183   sApplicationMenu = [[[[NSApp mainMenu] itemAtIndex:0] submenu] retain];
184   if (!mApplicationMenuDelegate) {
185     mApplicationMenuDelegate = [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
186   }
187   sApplicationMenu.delegate = mApplicationMenuDelegate;
188   NSMenuItem* quitMenuItem = [[[NSMenuItem alloc] initWithTitle:labelStr
189                                                          action:@selector(menuItemHit:)
190                                                   keyEquivalent:keyStr] autorelease];
191   quitMenuItem.target = nsMenuBarX::sNativeEventTarget;
192   quitMenuItem.tag = eCommand_ID_Quit;
193   [sApplicationMenu addItem:quitMenuItem];
194   sApplicationMenuIsFallback = YES;
196   NS_OBJC_END_TRY_ABORT_BLOCK;
199 uint32_t nsMenuBarX::GetMenuCount() { return mMenuArray.Length(); }
201 bool nsMenuBarX::MenuContainsAppMenu() {
202   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
204   return (mNativeMenu.numberOfItems > 0 && [mNativeMenu itemAtIndex:0].submenu == sApplicationMenu);
206   NS_OBJC_END_TRY_ABORT_BLOCK;
209 void nsMenuBarX::InsertMenuAtIndex(RefPtr<nsMenuX>&& aMenu, uint32_t aIndex) {
210   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
212   // If we've only yet created a fallback global Application menu (using
213   // ContructFallbackNativeMenus()), destroy it before recreating it properly.
214   if (sApplicationMenu && sApplicationMenuIsFallback) {
215     ResetNativeApplicationMenu();
216   }
217   // If we haven't created a global Application menu yet, do it.
218   if (!sApplicationMenu) {
219     CreateApplicationMenu(aMenu.get());
221     // Hook the new Application menu up to the menu bar.
222     NSMenu* mainMenu = NSApp.mainMenu;
223     NS_ASSERTION(mainMenu.numberOfItems > 0,
224                  "Main menu does not have any items, something is terribly wrong!");
225     [mainMenu itemAtIndex:0].submenu = sApplicationMenu;
226   }
228   // add menu to array that owns our menus
229   mMenuArray.InsertElementAt(aIndex, aMenu);
231   // hook up submenus
232   RefPtr<nsIContent> menuContent = aMenu->Content();
233   if (menuContent->GetChildCount() > 0 && !nsMenuUtilsX::NodeIsHiddenOrCollapsed(menuContent)) {
234     MenuChildChangedVisibility(MenuChild(aMenu), true);
235   }
237   NS_OBJC_END_TRY_ABORT_BLOCK;
240 void nsMenuBarX::RemoveMenuAtIndex(uint32_t aIndex) {
241   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
243   if (mMenuArray.Length() <= aIndex) {
244     NS_ERROR("Attempting submenu removal with bad index!");
245     return;
246   }
248   RefPtr<nsMenuX> menu = mMenuArray[aIndex];
249   mMenuArray.RemoveElementAt(aIndex);
251   menu->DetachFromGroupOwnerRecursive();
252   menu->DetachFromParent();
254   // Our native menu and our internal menu object array might be out of sync.
255   // This happens, for example, when a submenu is hidden. Because of this we
256   // should not assume that a native submenu is hooked up.
257   NSMenuItem* nativeMenuItem = menu->NativeNSMenuItem();
258   int nativeMenuItemIndex = [mNativeMenu indexOfItem:nativeMenuItem];
259   if (nativeMenuItemIndex != -1) {
260     [mNativeMenu removeItemAtIndex:nativeMenuItemIndex];
261   }
263   NS_OBJC_END_TRY_ABORT_BLOCK;
266 void nsMenuBarX::ObserveAttributeChanged(mozilla::dom::Document* aDocument, nsIContent* aContent,
267                                          nsAtom* aAttribute) {}
269 void nsMenuBarX::ObserveContentRemoved(mozilla::dom::Document* aDocument, nsIContent* aContainer,
270                                        nsIContent* aChild, nsIContent* aPreviousSibling) {
271   nsINode* parent = NODE_FROM(aContainer, aDocument);
272   MOZ_ASSERT(parent);
273   const Maybe<uint32_t> index = parent->ComputeIndexOf(aPreviousSibling);
274   MOZ_ASSERT(*index != UINT32_MAX);
275   RemoveMenuAtIndex(index.isSome() ? *index + 1u : 0u);
278 void nsMenuBarX::ObserveContentInserted(mozilla::dom::Document* aDocument, nsIContent* aContainer,
279                                         nsIContent* aChild) {
280   InsertMenuAtIndex(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aChild),
281                     aContainer->ComputeIndexOf(aChild).valueOr(UINT32_MAX));
284 void nsMenuBarX::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
285   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
287   NSString* locationString =
288       [NSString stringWithCharacters:reinterpret_cast<const unichar*>(aIndexString.BeginReading())
289                               length:aIndexString.Length()];
290   NSArray* indexes = [locationString componentsSeparatedByString:@"|"];
291   unsigned int indexCount = indexes.count;
292   if (indexCount == 0) {
293     return;
294   }
296   RefPtr<nsMenuX> currentMenu = nullptr;
297   int targetIndex = [[indexes objectAtIndex:0] intValue];
298   int visible = 0;
299   uint32_t length = mMenuArray.Length();
300   // first find a menu in the menu bar
301   for (unsigned int i = 0; i < length; i++) {
302     RefPtr<nsMenuX> menu = mMenuArray[i];
303     if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(menu->Content())) {
304       visible++;
305       if (visible == (targetIndex + 1)) {
306         currentMenu = std::move(menu);
307         break;
308       }
309     }
310   }
312   if (!currentMenu) {
313     return;
314   }
316   // fake open/close to cause lazy update to happen so submenus populate
317   currentMenu->MenuOpened();
318   currentMenu->MenuClosed();
320   // now find the correct submenu
321   for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
322     targetIndex = [[indexes objectAtIndex:i] intValue];
323     visible = 0;
324     length = currentMenu->GetItemCount();
325     for (unsigned int j = 0; j < length; j++) {
326       Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
327       if (!targetMenu) {
328         return;
329       }
330       RefPtr<nsIContent> content = targetMenu->match(
331           [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
332           [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
333       if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
334         visible++;
335         if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
336           currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
337           // fake open/close to cause lazy update to happen
338           currentMenu->MenuOpened();
339           currentMenu->MenuClosed();
340           break;
341         }
342       }
343     }
344   }
346   NS_OBJC_END_TRY_ABORT_BLOCK;
349 // Calling this forces a full reload of the menu system, reloading all native
350 // menus and their items.
351 // Without this testing is hard because changes to the DOM affect the native
352 // menu system lazily.
353 void nsMenuBarX::ForceNativeMenuReload() {
354   // tear down everything
355   while (GetMenuCount() > 0) {
356     RemoveMenuAtIndex(0);
357   }
359   // construct everything
360   ConstructNativeMenus();
363 nsMenuX* nsMenuBarX::GetMenuAt(uint32_t aIndex) {
364   if (mMenuArray.Length() <= aIndex) {
365     NS_ERROR("Requesting menu at invalid index!");
366     return nullptr;
367   }
368   return mMenuArray[aIndex].get();
371 nsMenuX* nsMenuBarX::GetXULHelpMenu() {
372   // The Help menu is usually (always?) the last one, so we start there and
373   // count back.
374   for (int32_t i = GetMenuCount() - 1; i >= 0; --i) {
375     nsMenuX* aMenu = GetMenuAt(i);
376     if (aMenu && nsMenuX::IsXULHelpMenu(aMenu->Content())) {
377       return aMenu;
378     }
379   }
380   return nil;
383 // On SnowLeopard and later we must tell the OS which is our Help menu.
384 // Otherwise it will only add Spotlight for Help (the Search item) to our
385 // Help menu if its label/title is "Help" -- i.e. if the menu is in English.
386 // This resolves bugs 489196 and 539317.
387 void nsMenuBarX::SetSystemHelpMenu() {
388   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
390   nsMenuX* xulHelpMenu = GetXULHelpMenu();
391   if (xulHelpMenu) {
392     NSMenu* helpMenu = xulHelpMenu->NativeNSMenu();
393     if (helpMenu) {
394       NSApp.helpMenu = helpMenu;
395     }
396   }
398   NS_OBJC_END_TRY_ABORT_BLOCK;
401 nsresult nsMenuBarX::Paint() {
402   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
404   // Don't try to optimize anything in this painting by checking
405   // sLastGeckoMenuBarPainted because the menubar can be manipulated by
406   // native dialogs and sheet code and other things besides this paint method.
408   // We have to keep the same menu item for the Application menu so we keep
409   // passing it along.
410   NSMenu* outgoingMenu = NSApp.mainMenu;
411   NS_ASSERTION(outgoingMenu.numberOfItems > 0,
412                "Main menu does not have any items, something is terribly wrong!");
414   NSMenuItem* appMenuItem = [[outgoingMenu itemAtIndex:0] retain];
415   [outgoingMenu removeItemAtIndex:0];
416   [mNativeMenu insertItem:appMenuItem atIndex:0];
417   [appMenuItem release];
419   // Set menu bar and event target.
420   NSApp.mainMenu = mNativeMenu;
421   SetSystemHelpMenu();
422   nsMenuBarX::sLastGeckoMenuBarPainted = this;
424   gSomeMenuBarPainted = YES;
426   return NS_OK;
428   NS_OBJC_END_TRY_ABORT_BLOCK;
431 /* static */
432 void nsMenuBarX::ResetNativeApplicationMenu() {
433   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
435   [sApplicationMenu removeAllItems];
436   [sApplicationMenu release];
437   sApplicationMenu = nil;
438   sApplicationMenuIsFallback = NO;
440   NS_OBJC_END_TRY_ABORT_BLOCK;
443 void nsMenuBarX::SetNeedsRebuild() { mNeedsRebuild = true; }
445 void nsMenuBarX::ApplicationMenuOpened() {
446   if (mNeedsRebuild) {
447     if (!mMenuArray.IsEmpty()) {
448       ResetNativeApplicationMenu();
449       CreateApplicationMenu(mMenuArray[0].get());
450     }
451     mNeedsRebuild = false;
452   }
455 bool nsMenuBarX::PerformKeyEquivalent(NSEvent* aEvent) {
456   return [mNativeMenu performSuperKeyEquivalent:aEvent];
459 void nsMenuBarX::MenuChildChangedVisibility(const MenuChild& aChild, bool aIsVisible) {
460   MOZ_RELEASE_ASSERT(aChild.is<RefPtr<nsMenuX>>(), "nsMenuBarX only has nsMenuX children");
461   const RefPtr<nsMenuX>& child = aChild.as<RefPtr<nsMenuX>>();
462   NSMenuItem* item = child->NativeNSMenuItem();
463   if (aIsVisible) {
464     NSInteger insertionPoint = CalculateNativeInsertionPoint(child);
465     [mNativeMenu insertItem:child->NativeNSMenuItem() atIndex:insertionPoint];
466   } else if ([mNativeMenu indexOfItem:item] != -1) {
467     [mNativeMenu removeItem:item];
468   }
471 NSInteger nsMenuBarX::CalculateNativeInsertionPoint(nsMenuX* aChild) {
472   NSInteger insertionPoint = MenuContainsAppMenu() ? 1 : 0;
473   for (auto& currMenu : mMenuArray) {
474     if (currMenu == aChild) {
475       return insertionPoint;
476     }
477     // Only count items that are inside a menu.
478     // XXXmstange Not sure what would cause free-standing items. Maybe for collapsed/hidden menus?
479     // In that case, an nsMenuX::IsVisible() method would be better.
480     if (currMenu->NativeNSMenuItem().menu) {
481       insertionPoint++;
482     }
483   }
484   return insertionPoint;
487 // Hide the item in the menu by setting the 'hidden' attribute. Returns it so
488 // the caller can hang onto it if they so choose.
489 RefPtr<Element> nsMenuBarX::HideItem(mozilla::dom::Document* aDocument, const nsAString& aID) {
490   RefPtr<Element> menuElement = aDocument->GetElementById(aID);
491   if (menuElement) {
492     menuElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden, u"true"_ns, false);
493   }
494   return menuElement;
497 // Do what is necessary to conform to the Aqua guidelines for menus.
498 void nsMenuBarX::AquifyMenuBar() {
499   RefPtr<mozilla::dom::Document> domDoc = mContent->GetComposedDoc();
500   if (domDoc) {
501     // remove the "About..." item and its separator
502     HideItem(domDoc, u"aboutSeparator"_ns);
503     mAboutItemContent = HideItem(domDoc, u"aboutName"_ns);
504     if (!sAboutItemContent) {
505       sAboutItemContent = mAboutItemContent;
506     }
508     // remove quit item and its separator
509     HideItem(domDoc, u"menu_FileQuitSeparator"_ns);
510     mQuitItemContent = HideItem(domDoc, u"menu_FileQuitItem"_ns);
511     if (!sQuitItemContent) {
512       sQuitItemContent = mQuitItemContent;
513     }
515     // remove prefs item and its separator, but save off the pref content node
516     // so we can invoke its command later.
517     HideItem(domDoc, u"menu_PrefsSeparator"_ns);
518     mPrefItemContent = HideItem(domDoc, u"menu_preferences"_ns);
519     if (!sPrefItemContent) {
520       sPrefItemContent = mPrefItemContent;
521     }
523     // remove Account Settings item.
524     mAccountItemContent = HideItem(domDoc, u"menu_accountmgr"_ns);
525     if (!sAccountItemContent) {
526       sAccountItemContent = mAccountItemContent;
527     }
529     // hide items that we use for the Application menu
530     HideItem(domDoc, u"menu_mac_services"_ns);
531     HideItem(domDoc, u"menu_mac_hide_app"_ns);
532     HideItem(domDoc, u"menu_mac_hide_others"_ns);
533     HideItem(domDoc, u"menu_mac_show_all"_ns);
534     HideItem(domDoc, u"menu_mac_touch_bar"_ns);
535   }
538 // for creating menu items destined for the Application menu
539 NSMenuItem* nsMenuBarX::CreateNativeAppMenuItem(nsMenuX* aMenu, const nsAString& aNodeID,
540                                                 SEL aAction, int aTag,
541                                                 NativeMenuItemTarget* aTarget) {
542   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
544   RefPtr<mozilla::dom::Document> doc = aMenu->Content()->GetUncomposedDoc();
545   if (!doc) {
546     return nil;
547   }
549   RefPtr<mozilla::dom::Element> menuItem = doc->GetElementById(aNodeID);
550   if (!menuItem) {
551     return nil;
552   }
554   // Check collapsed rather than hidden since the app menu items are always
555   // hidden in AquifyMenuBar.
556   if (menuItem->AttrValueIs(kNameSpaceID_None, nsGkAtoms::collapsed, nsGkAtoms::_true,
557                             eCaseMatters)) {
558     return nil;
559   }
561   // Get information from the gecko menu item
562   nsAutoString label;
563   nsAutoString modifiers;
564   nsAutoString key;
565   menuItem->GetAttr(nsGkAtoms::label, label);
566   menuItem->GetAttr(nsGkAtoms::modifiers, modifiers);
567   menuItem->GetAttr(nsGkAtoms::key, key);
569   // Get more information about the key equivalent. Start by
570   // finding the key node we need.
571   NSString* keyEquiv = nil;
572   unsigned int macKeyModifiers = 0;
573   if (!key.IsEmpty()) {
574     RefPtr<Element> keyElement = doc->GetElementById(key);
575     if (keyElement) {
576       // first grab the key equivalent character
577       nsAutoString keyChar(u" "_ns);
578       keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::key, keyChar);
579       if (!keyChar.EqualsLiteral(" ")) {
580         keyEquiv = [[NSString stringWithCharacters:reinterpret_cast<const unichar*>(keyChar.get())
581                                             length:keyChar.Length()] lowercaseString];
582       }
583       // now grab the key equivalent modifiers
584       nsAutoString modifiersStr;
585       keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiersStr);
586       uint8_t geckoModifiers = nsMenuUtilsX::GeckoModifiersForNodeAttribute(modifiersStr);
587       macKeyModifiers = nsMenuUtilsX::MacModifiersForGeckoModifiers(geckoModifiers);
588     }
589   }
590   // get the label into NSString-form
591   NSString* labelString =
592       [NSString stringWithCharacters:reinterpret_cast<const unichar*>(label.get())
593                               length:label.Length()];
595   if (!labelString) {
596     labelString = @"";
597   }
598   if (!keyEquiv) {
599     keyEquiv = @"";
600   }
602   // put together the actual NSMenuItem
603   NSMenuItem* newMenuItem = [[NSMenuItem alloc] initWithTitle:labelString
604                                                        action:aAction
605                                                 keyEquivalent:keyEquiv];
607   newMenuItem.tag = aTag;
608   newMenuItem.target = aTarget;
609   newMenuItem.keyEquivalentModifierMask = macKeyModifiers;
610   newMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
612   return newMenuItem;
614   NS_OBJC_END_TRY_ABORT_BLOCK;
617 // build the Application menu shared by all menu bars
618 void nsMenuBarX::CreateApplicationMenu(nsMenuX* aMenu) {
619   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
621   // At this point, the application menu is the application menu from
622   // the nib in cocoa widgets. We do not have a way to create an application
623   // menu manually, so we grab the one from the nib and use that.
624   sApplicationMenu = [[NSApp.mainMenu itemAtIndex:0].submenu retain];
626   /*
627     We support the following menu items here:
629     Menu Item                DOM Node ID             Notes
631     ========================
632     = About This App       = <- aboutName
633     ========================
634     = Preferences...       = <- menu_preferences
635     = Account Settings     = <- menu_accountmgr      Only on Thunderbird
636     ========================
637     = Services     >       = <- menu_mac_services    <- (do not define key equivalent)
638     ========================
639     = Hide App             = <- menu_mac_hide_app
640     = Hide Others          = <- menu_mac_hide_others
641     = Show All             = <- menu_mac_show_all
642     ========================
643     = Customize Touch Bar… = <- menu_mac_touch_bar
644     ========================
645     = Quit                 = <- menu_FileQuitItem
646     ========================
648     If any of them are ommitted from the application's DOM, we just don't add
649     them. We always add a "Quit" item, but if an app developer does not provide a
650     DOM node with the right ID for the Quit item, we add it in English. App
651     developers need only add each node with a label and a key equivalent (if they
652     want one). Other attributes are optional. Like so:
654     <menuitem id="menu_preferences"
655            label="&preferencesCmdMac.label;"
656              key="open_prefs_key"/>
658     We need to use this system for localization purposes, until we have a better way
659     to define the Application menu to be used on Mac OS X.
660   */
662   if (sApplicationMenu) {
663     if (!mApplicationMenuDelegate) {
664       mApplicationMenuDelegate = [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
665     }
666     sApplicationMenu.delegate = mApplicationMenuDelegate;
668     // This code reads attributes we are going to care about from the DOM elements
670     NSMenuItem* itemBeingAdded = nil;
671     BOOL addAboutSeparator = FALSE;
672     BOOL addPrefsSeparator = FALSE;
674     // Add the About menu item
675     itemBeingAdded = CreateNativeAppMenuItem(aMenu, u"aboutName"_ns, @selector(menuItemHit:),
676                                              eCommand_ID_About, nsMenuBarX::sNativeEventTarget);
677     if (itemBeingAdded) {
678       [sApplicationMenu addItem:itemBeingAdded];
679       [itemBeingAdded release];
680       itemBeingAdded = nil;
682       addAboutSeparator = TRUE;
683     }
685     // Add separator if either the About item or software update item exists
686     if (addAboutSeparator) {
687       [sApplicationMenu addItem:[NSMenuItem separatorItem]];
688     }
690     // Add the Preferences menu item
691     itemBeingAdded = CreateNativeAppMenuItem(aMenu, u"menu_preferences"_ns, @selector(menuItemHit:),
692                                              eCommand_ID_Prefs, nsMenuBarX::sNativeEventTarget);
693     if (itemBeingAdded) {
694       [sApplicationMenu addItem:itemBeingAdded];
695       [itemBeingAdded release];
696       itemBeingAdded = nil;
698       addPrefsSeparator = TRUE;
699     }
701     // Add the Account Settings menu item. This is Thunderbird only
702     itemBeingAdded = CreateNativeAppMenuItem(aMenu, u"menu_accountmgr"_ns, @selector(menuItemHit:),
703                                              eCommand_ID_Account, nsMenuBarX::sNativeEventTarget);
704     if (itemBeingAdded) {
705       [sApplicationMenu addItem:itemBeingAdded];
706       [itemBeingAdded release];
707       itemBeingAdded = nil;
708     }
710     // Add separator after Preferences menu
711     if (addPrefsSeparator) {
712       [sApplicationMenu addItem:[NSMenuItem separatorItem]];
713     }
715     // Add Services menu item
716     itemBeingAdded = CreateNativeAppMenuItem(aMenu, u"menu_mac_services"_ns, nil, 0, nil);
717     if (itemBeingAdded) {
718       [sApplicationMenu addItem:itemBeingAdded];
720       // set this menu item up as the Mac OS X Services menu
721       NSMenu* servicesMenu = [[GeckoServicesNSMenu alloc] initWithTitle:@""];
722       itemBeingAdded.submenu = servicesMenu;
723       NSApp.servicesMenu = servicesMenu;
725       [itemBeingAdded release];
726       itemBeingAdded = nil;
728       // Add separator after Services menu
729       [sApplicationMenu addItem:[NSMenuItem separatorItem]];
730     }
732     BOOL addHideShowSeparator = FALSE;
734     // Add menu item to hide this application
735     itemBeingAdded =
736         CreateNativeAppMenuItem(aMenu, u"menu_mac_hide_app"_ns, @selector(menuItemHit:),
737                                 eCommand_ID_HideApp, nsMenuBarX::sNativeEventTarget);
738     if (itemBeingAdded) {
739       [sApplicationMenu addItem:itemBeingAdded];
740       [itemBeingAdded release];
741       itemBeingAdded = nil;
743       addHideShowSeparator = TRUE;
744     }
746     // Add menu item to hide other applications
747     itemBeingAdded =
748         CreateNativeAppMenuItem(aMenu, u"menu_mac_hide_others"_ns, @selector(menuItemHit:),
749                                 eCommand_ID_HideOthers, nsMenuBarX::sNativeEventTarget);
750     if (itemBeingAdded) {
751       [sApplicationMenu addItem:itemBeingAdded];
752       [itemBeingAdded release];
753       itemBeingAdded = nil;
755       addHideShowSeparator = TRUE;
756     }
758     // Add menu item to show all applications
759     itemBeingAdded =
760         CreateNativeAppMenuItem(aMenu, u"menu_mac_show_all"_ns, @selector(menuItemHit:),
761                                 eCommand_ID_ShowAll, nsMenuBarX::sNativeEventTarget);
762     if (itemBeingAdded) {
763       [sApplicationMenu addItem:itemBeingAdded];
764       [itemBeingAdded release];
765       itemBeingAdded = nil;
767       addHideShowSeparator = TRUE;
768     }
770     // Add a separator after the hide/show menus if at least one exists
771     if (addHideShowSeparator) {
772       [sApplicationMenu addItem:[NSMenuItem separatorItem]];
773     }
775     BOOL addTouchBarSeparator = NO;
777     // Add Touch Bar customization menu item.
778     itemBeingAdded =
779         CreateNativeAppMenuItem(aMenu, u"menu_mac_touch_bar"_ns, @selector(menuItemHit:),
780                                 eCommand_ID_TouchBar, nsMenuBarX::sNativeEventTarget);
782     if (itemBeingAdded) {
783       [sApplicationMenu addItem:itemBeingAdded];
784       // We hide the menu item on Macs that don't have a Touch Bar.
785       if (!sTouchBarIsInitialized) {
786         [itemBeingAdded setHidden:YES];
787       } else {
788         addTouchBarSeparator = YES;
789       }
790       [itemBeingAdded release];
791       itemBeingAdded = nil;
792     }
794     // Add a separator after the Touch Bar menu item if it exists
795     if (addTouchBarSeparator) {
796       [sApplicationMenu addItem:[NSMenuItem separatorItem]];
797     }
799     // Add quit menu item
800     itemBeingAdded =
801         CreateNativeAppMenuItem(aMenu, u"menu_FileQuitItem"_ns, @selector(menuItemHit:),
802                                 eCommand_ID_Quit, nsMenuBarX::sNativeEventTarget);
803     if (itemBeingAdded) {
804       [sApplicationMenu addItem:itemBeingAdded];
805       [itemBeingAdded release];
806       itemBeingAdded = nil;
807     } else {
808       // the current application does not have a DOM node for "Quit". Add one
809       // anyway, in English.
810       NSMenuItem* defaultQuitItem = [[[NSMenuItem alloc] initWithTitle:@"Quit"
811                                                                 action:@selector(menuItemHit:)
812                                                          keyEquivalent:@"q"] autorelease];
813       defaultQuitItem.target = nsMenuBarX::sNativeEventTarget;
814       defaultQuitItem.tag = eCommand_ID_Quit;
815       [sApplicationMenu addItem:defaultQuitItem];
816     }
817   }
819   NS_OBJC_END_TRY_ABORT_BLOCK;
823 // Objective-C class used to allow us to have keyboard commands
824 // look like they are doing something but actually do nothing.
825 // We allow mouse actions to work normally.
828 // Controls whether or not native menu items should invoke their commands.
829 static BOOL gMenuItemsExecuteCommands = YES;
831 @implementation GeckoNSMenu
833 // Keyboard commands should not cause menu items to invoke their
834 // commands when there is a key window because we'd rather send
835 // the keyboard command to the window. We still have the menus
836 // go through the mechanics so they'll give the proper visual
837 // feedback.
838 - (BOOL)performKeyEquivalent:(NSEvent*)aEvent {
839   // We've noticed that Mac OS X expects this check in subclasses before
840   // calling NSMenu's "performKeyEquivalent:".
841   //
842   // There is no case in which we'd need to do anything or return YES
843   // when we have no items so we can just do this check first.
844   if (self.numberOfItems <= 0) {
845     return NO;
846   }
848   NSWindow* keyWindow = NSApp.keyWindow;
850   // If there is no key window then just behave normally. This
851   // probably means that this menu is associated with Gecko's
852   // hidden window.
853   if (!keyWindow) {
854     return [super performKeyEquivalent:aEvent];
855   }
857   NSResponder* firstResponder = keyWindow.firstResponder;
859   gMenuItemsExecuteCommands = NO;
860   [super performKeyEquivalent:aEvent];
861   gMenuItemsExecuteCommands = YES;  // return to default
863   // Return YES if we invoked a command and there is now no key window or we changed
864   // the first responder. In this case we do not want to propagate the event because
865   // we don't want it handled again.
866   if (!NSApp.keyWindow || NSApp.keyWindow.firstResponder != firstResponder) {
867     return YES;
868   }
870   // Return NO so that we can handle the event via NSView's "keyDown:".
871   return NO;
874 - (BOOL)performSuperKeyEquivalent:(NSEvent*)aEvent {
875   return [super performKeyEquivalent:aEvent];
878 @end
881 // Objective-C class used as action target for menu items
884 @implementation NativeMenuItemTarget
886 // called when some menu item in this menu gets hit
887 - (IBAction)menuItemHit:(id)aSender {
888   if (!gMenuItemsExecuteCommands) {
889     return;
890   }
892   if (![aSender isKindOfClass:[NSMenuItem class]]) {
893     return;
894   }
896   NSMenuItem* nativeMenuItem = (NSMenuItem*)aSender;
897   NSInteger tag = nativeMenuItem.tag;
899   nsMenuGroupOwnerX* menuGroupOwner = nullptr;
900   nsMenuBarX* menuBar = nullptr;
901   MOZMenuItemRepresentedObject* representedObject = nativeMenuItem.representedObject;
903   if (representedObject) {
904     menuGroupOwner = representedObject.menuGroupOwner;
905     if (!menuGroupOwner) {
906       return;
907     }
908     menuBar = menuGroupOwner->GetMenuBar();
909   }
911   // Notify containing menu about the fact that a menu item will be activated.
912   NSMenu* menu = nativeMenuItem.menu;
913   if ([menu.delegate isKindOfClass:[MenuDelegate class]]) {
914     [(MenuDelegate*)menu.delegate menu:menu willActivateItem:nativeMenuItem];
915   }
917   // Get the modifier flags and button for this menu item activation. The menu system does not pass
918   // an NSEvent to our action selector, but we can query the current NSEvent instead. The current
919   // NSEvent can be a key event or a mouseup event, depending on how the menu item is activated.
920   NSEventModifierFlags modifierFlags = NSApp.currentEvent ? NSApp.currentEvent.modifierFlags : 0;
921   mozilla::MouseButton button = NSApp.currentEvent
922                                     ? nsCocoaUtils::ButtonForEvent(NSApp.currentEvent)
923                                     : mozilla::MouseButton::ePrimary;
925   // Do special processing if this is for an app-global command.
926   if (tag == eCommand_ID_About) {
927     nsIContent* mostSpecificContent = sAboutItemContent;
928     if (menuBar && menuBar->mAboutItemContent) {
929       mostSpecificContent = menuBar->mAboutItemContent;
930     }
931     nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
932     return;
933   }
934   if (tag == eCommand_ID_Prefs) {
935     nsIContent* mostSpecificContent = sPrefItemContent;
936     if (menuBar && menuBar->mPrefItemContent) {
937       mostSpecificContent = menuBar->mPrefItemContent;
938     }
939     nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
940     return;
941   }
942   if (tag == eCommand_ID_Account) {
943     nsIContent* mostSpecificContent = sAccountItemContent;
944     if (menuBar && menuBar->mAccountItemContent) {
945       mostSpecificContent = menuBar->mAccountItemContent;
946     }
947     nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
948     return;
949   }
950   if (tag == eCommand_ID_HideApp) {
951     [NSApp hide:aSender];
952     return;
953   }
954   if (tag == eCommand_ID_HideOthers) {
955     [NSApp hideOtherApplications:aSender];
956     return;
957   }
958   if (tag == eCommand_ID_ShowAll) {
959     [NSApp unhideAllApplications:aSender];
960     return;
961   }
962   if (tag == eCommand_ID_TouchBar) {
963     [NSApp toggleTouchBarCustomizationPalette:aSender];
964     return;
965   }
966   if (tag == eCommand_ID_Quit) {
967     nsIContent* mostSpecificContent = sQuitItemContent;
968     if (menuBar && menuBar->mQuitItemContent) {
969       mostSpecificContent = menuBar->mQuitItemContent;
970     }
971     // If we have some content for quit we execute it. Otherwise we send a native app terminate
972     // message. If you want to stop a quit from happening, provide quit content and return
973     // the event as unhandled.
974     if (mostSpecificContent) {
975       nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
976     } else {
977       nsCOMPtr<nsIAppStartup> appStartup = mozilla::components::AppStartup::Service();
978       if (appStartup) {
979         bool userAllowedQuit = true;
980         appStartup->Quit(nsIAppStartup::eAttemptQuit, 0, &userAllowedQuit);
981       }
982     }
983     return;
984   }
986   // given the commandID, look it up in our hashtable and dispatch to
987   // that menu item.
988   if (menuGroupOwner) {
989     if (RefPtr<nsMenuItemX> menuItem =
990             menuGroupOwner->GetMenuItemForCommandID(static_cast<uint32_t>(tag))) {
991       if (nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest) {
992         menuItem->DoCommand(modifierFlags, button);
993       } else if (RefPtr<nsMenuX> menu = menuItem->ParentMenu()) {
994         menu->ActivateItemAfterClosing(std::move(menuItem), modifierFlags, button);
995       }
996     }
997   }
1000 @end
1002 // Objective-C class used for menu items on the Services menu to allow Gecko
1003 // to override their standard behavior in order to stop key equivalents from
1004 // firing in certain instances. When gMenuItemsExecuteCommands is NO, we return
1005 // a dummy target and action instead of the actual target and action.
1007 @implementation GeckoServicesNSMenuItem
1009 - (id)target {
1010   id realTarget = super.target;
1011   if (gMenuItemsExecuteCommands) {
1012     return realTarget;
1013   }
1014   return realTarget ? self : nil;
1017 - (SEL)action {
1018   SEL realAction = super.action;
1019   if (gMenuItemsExecuteCommands) {
1020     return realAction;
1021   }
1022   return realAction ? @selector(_doNothing:) : nullptr;
1025 - (void)_doNothing:(id)aSender {
1028 @end
1030 // Objective-C class used as the Services menu so that Gecko can override the
1031 // standard behavior of the Services menu in order to stop key equivalents
1032 // from firing in certain instances.
1034 @implementation GeckoServicesNSMenu
1036 - (void)addItem:(NSMenuItem*)aNewItem {
1037   [self _overrideClassOfMenuItem:aNewItem];
1038   [super addItem:aNewItem];
1041 - (NSMenuItem*)addItemWithTitle:(NSString*)aString
1042                          action:(SEL)aSelector
1043                   keyEquivalent:(NSString*)aKeyEquiv {
1044   NSMenuItem* newItem = [super addItemWithTitle:aString action:aSelector keyEquivalent:aKeyEquiv];
1045   [self _overrideClassOfMenuItem:newItem];
1046   return newItem;
1049 - (void)insertItem:(NSMenuItem*)aNewItem atIndex:(NSInteger)aIndex {
1050   [self _overrideClassOfMenuItem:aNewItem];
1051   [super insertItem:aNewItem atIndex:aIndex];
1054 - (NSMenuItem*)insertItemWithTitle:(NSString*)aString
1055                             action:(SEL)aSelector
1056                      keyEquivalent:(NSString*)aKeyEquiv
1057                            atIndex:(NSInteger)aIndex {
1058   NSMenuItem* newItem = [super insertItemWithTitle:aString
1059                                             action:aSelector
1060                                      keyEquivalent:aKeyEquiv
1061                                            atIndex:aIndex];
1062   [self _overrideClassOfMenuItem:newItem];
1063   return newItem;
1066 - (void)_overrideClassOfMenuItem:(NSMenuItem*)aMenuItem {
1067   if ([aMenuItem class] == [NSMenuItem class]) {
1068     object_setClass(aMenuItem, [GeckoServicesNSMenuItem class]);
1069   }
1072 @end