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"
10 #include "nsMenuItemX.h"
11 #include "nsMenuUtilsX.h"
12 #include "nsCocoaUtils.h"
13 #include "nsChildView.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;
66 - (void)menuWillOpen:(NSMenu*)menu {
67 mApplicationMenu->ApplicationMenuOpened();
70 - (void)menuDidClose:(NSMenu*)menu {
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"];
87 mMenuGroupOwner->RegisterForContentChanges(mContent, this);
88 ConstructNativeMenus();
90 ConstructFallbackNativeMenus();
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;
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;
108 if (sQuitItemContent == mQuitItemContent) {
109 sQuitItemContent = nullptr;
111 if (sPrefItemContent == mPrefItemContent) {
112 sPrefItemContent = nullptr;
114 if (sAccountItemContent == mAccountItemContent) {
115 sAccountItemContent = nullptr;
118 mMenuGroupOwner->UnregisterForLocaleChanges();
120 // make sure we unregister ourselves as a content observer
122 mMenuGroupOwner->UnregisterForContentChanges(mContent);
125 for (nsMenuX* menu : mMenuArray) {
126 menu->DetachFromGroupOwnerRecursive();
127 menu->DetachFromParent();
130 if (mApplicationMenuDelegate) {
131 [mApplicationMenuDelegate release];
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()),
149 void nsMenuBarX::ConstructFallbackNativeMenus() {
150 NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
152 if (sApplicationMenu) {
153 // Menu has already been built.
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));
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];
183 sApplicationMenu = [[[[NSApp mainMenu] itemAtIndex:0] submenu] retain];
184 if (!mApplicationMenuDelegate) {
185 mApplicationMenuDelegate = [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
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();
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;
228 // add menu to array that owns our menus
229 mMenuArray.InsertElementAt(aIndex, aMenu);
232 RefPtr<nsIContent> menuContent = aMenu->Content();
233 if (menuContent->GetChildCount() > 0 && !nsMenuUtilsX::NodeIsHiddenOrCollapsed(menuContent)) {
234 MenuChildChangedVisibility(MenuChild(aMenu), true);
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!");
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];
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);
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) {
296 RefPtr<nsMenuX> currentMenu = nullptr;
297 int targetIndex = [[indexes objectAtIndex:0] intValue];
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())) {
305 if (visible == (targetIndex + 1)) {
306 currentMenu = std::move(menu);
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];
324 length = currentMenu->GetItemCount();
325 for (unsigned int j = 0; j < length; j++) {
326 Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
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)) {
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();
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);
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!");
368 return mMenuArray[aIndex].get();
371 nsMenuX* nsMenuBarX::GetXULHelpMenu() {
372 // The Help menu is usually (always?) the last one, so we start there and
374 for (int32_t i = GetMenuCount() - 1; i >= 0; --i) {
375 nsMenuX* aMenu = GetMenuAt(i);
376 if (aMenu && nsMenuX::IsXULHelpMenu(aMenu->Content())) {
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();
392 NSMenu* helpMenu = xulHelpMenu->NativeNSMenu();
394 NSApp.helpMenu = helpMenu;
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
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;
422 nsMenuBarX::sLastGeckoMenuBarPainted = this;
424 gSomeMenuBarPainted = YES;
428 NS_OBJC_END_TRY_ABORT_BLOCK;
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() {
447 if (!mMenuArray.IsEmpty()) {
448 ResetNativeApplicationMenu();
449 CreateApplicationMenu(mMenuArray[0].get());
451 mNeedsRebuild = false;
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();
464 NSInteger insertionPoint = CalculateNativeInsertionPoint(child);
465 [mNativeMenu insertItem:child->NativeNSMenuItem() atIndex:insertionPoint];
466 } else if ([mNativeMenu indexOfItem:item] != -1) {
467 [mNativeMenu removeItem:item];
471 NSInteger nsMenuBarX::CalculateNativeInsertionPoint(nsMenuX* aChild) {
472 NSInteger insertionPoint = MenuContainsAppMenu() ? 1 : 0;
473 for (auto& currMenu : mMenuArray) {
474 if (currMenu == aChild) {
475 return insertionPoint;
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) {
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);
492 menuElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden, u"true"_ns, false);
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();
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;
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;
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;
523 // remove Account Settings item.
524 mAccountItemContent = HideItem(domDoc, u"menu_accountmgr"_ns);
525 if (!sAccountItemContent) {
526 sAccountItemContent = mAccountItemContent;
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);
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();
549 RefPtr<mozilla::dom::Element> menuItem = doc->GetElementById(aNodeID);
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,
561 // Get information from the gecko menu item
563 nsAutoString modifiers;
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);
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];
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);
590 // get the label into NSString-form
591 NSString* labelString =
592 [NSString stringWithCharacters:reinterpret_cast<const unichar*>(label.get())
593 length:label.Length()];
602 // put together the actual NSMenuItem
603 NSMenuItem* newMenuItem = [[NSMenuItem alloc] initWithTitle:labelString
605 keyEquivalent:keyEquiv];
607 newMenuItem.tag = aTag;
608 newMenuItem.target = aTarget;
609 newMenuItem.keyEquivalentModifierMask = macKeyModifiers;
610 newMenuItem.representedObject = mMenuGroupOwner->GetRepresentedObject();
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];
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.
662 if (sApplicationMenu) {
663 if (!mApplicationMenuDelegate) {
664 mApplicationMenuDelegate = [[ApplicationMenuDelegate alloc] initWithApplicationMenu:this];
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;
685 // Add separator if either the About item or software update item exists
686 if (addAboutSeparator) {
687 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
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;
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;
710 // Add separator after Preferences menu
711 if (addPrefsSeparator) {
712 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
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]];
732 BOOL addHideShowSeparator = FALSE;
734 // Add menu item to hide this application
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;
746 // Add menu item to hide other applications
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;
758 // Add menu item to show all applications
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;
770 // Add a separator after the hide/show menus if at least one exists
771 if (addHideShowSeparator) {
772 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
775 BOOL addTouchBarSeparator = NO;
777 // Add Touch Bar customization menu item.
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];
788 addTouchBarSeparator = YES;
790 [itemBeingAdded release];
791 itemBeingAdded = nil;
794 // Add a separator after the Touch Bar menu item if it exists
795 if (addTouchBarSeparator) {
796 [sApplicationMenu addItem:[NSMenuItem separatorItem]];
799 // Add quit menu item
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;
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];
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
838 - (BOOL)performKeyEquivalent:(NSEvent*)aEvent {
839 // We've noticed that Mac OS X expects this check in subclasses before
840 // calling NSMenu's "performKeyEquivalent:".
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) {
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
854 return [super performKeyEquivalent:aEvent];
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) {
870 // Return NO so that we can handle the event via NSView's "keyDown:".
874 - (BOOL)performSuperKeyEquivalent:(NSEvent*)aEvent {
875 return [super performKeyEquivalent:aEvent];
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) {
892 if (![aSender isKindOfClass:[NSMenuItem class]]) {
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) {
908 menuBar = menuGroupOwner->GetMenuBar();
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];
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;
931 nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
934 if (tag == eCommand_ID_Prefs) {
935 nsIContent* mostSpecificContent = sPrefItemContent;
936 if (menuBar && menuBar->mPrefItemContent) {
937 mostSpecificContent = menuBar->mPrefItemContent;
939 nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
942 if (tag == eCommand_ID_Account) {
943 nsIContent* mostSpecificContent = sAccountItemContent;
944 if (menuBar && menuBar->mAccountItemContent) {
945 mostSpecificContent = menuBar->mAccountItemContent;
947 nsMenuUtilsX::DispatchCommandTo(mostSpecificContent, modifierFlags, button);
950 if (tag == eCommand_ID_HideApp) {
951 [NSApp hide:aSender];
954 if (tag == eCommand_ID_HideOthers) {
955 [NSApp hideOtherApplications:aSender];
958 if (tag == eCommand_ID_ShowAll) {
959 [NSApp unhideAllApplications:aSender];
962 if (tag == eCommand_ID_TouchBar) {
963 [NSApp toggleTouchBarCustomizationPalette:aSender];
966 if (tag == eCommand_ID_Quit) {
967 nsIContent* mostSpecificContent = sQuitItemContent;
968 if (menuBar && menuBar->mQuitItemContent) {
969 mostSpecificContent = menuBar->mQuitItemContent;
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);
977 nsCOMPtr<nsIAppStartup> appStartup = mozilla::components::AppStartup::Service();
979 bool userAllowedQuit = true;
980 appStartup->Quit(nsIAppStartup::eAttemptQuit, 0, &userAllowedQuit);
986 // given the commandID, look it up in our hashtable and dispatch to
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);
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
1010 id realTarget = super.target;
1011 if (gMenuItemsExecuteCommands) {
1014 return realTarget ? self : nil;
1018 SEL realAction = super.action;
1019 if (gMenuItemsExecuteCommands) {
1022 return realAction ? @selector(_doNothing:) : nullptr;
1025 - (void)_doNothing:(id)aSender {
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];
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
1060 keyEquivalent:aKeyEquiv
1062 [self _overrideClassOfMenuItem:newItem];
1066 - (void)_overrideClassOfMenuItem:(NSMenuItem*)aMenuItem {
1067 if ([aMenuItem class] == [NSMenuItem class]) {
1068 object_setClass(aMenuItem, [GeckoServicesNSMenuItem class]);