Backed out changeset b4a0f8afc02e (bug 1857946) for causing bc failures at browser...
[gecko.git] / widget / cocoa / nsMenuX.mm
blob2a7187f106484fc4143ee6888cd91639cae24d1f
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 "nsMenuX.h"
8 #include <_types/_uint32_t.h>
9 #include <dlfcn.h>
11 #include "mozilla/dom/Document.h"
12 #include "mozilla/dom/ScriptSettings.h"
13 #include "mozilla/EventDispatcher.h"
14 #include "mozilla/MouseEvents.h"
16 #include "MOZMenuOpeningCoordinator.h"
17 #include "nsMenuItemX.h"
18 #include "nsMenuUtilsX.h"
19 #include "nsMenuItemIconX.h"
21 #include "nsObjCExceptions.h"
23 #include "nsComputedDOMStyle.h"
24 #include "nsThreadUtils.h"
25 #include "nsToolkit.h"
26 #include "nsCocoaUtils.h"
27 #include "nsCOMPtr.h"
28 #include "prinrval.h"
29 #include "nsString.h"
30 #include "nsReadableUtils.h"
31 #include "nsUnicharUtils.h"
32 #include "nsGkAtoms.h"
33 #include "nsCRT.h"
34 #include "nsBaseWidget.h"
36 #include "nsIContent.h"
37 #include "nsIDocumentObserver.h"
38 #include "nsIComponentManager.h"
39 #include "nsIRollupListener.h"
40 #include "nsIServiceManager.h"
41 #include "nsXULPopupManager.h"
43 using namespace mozilla;
44 using namespace mozilla::dom;
46 static bool gConstructingMenu = false;
47 static bool gMenuMethodsSwizzled = false;
49 int32_t nsMenuX::sIndexingMenuLevel = 0;
51 // TODO: It is unclear whether this is still needed.
52 static void SwizzleDynamicIndexingMethods() {
53   if (gMenuMethodsSwizzled) {
54     return;
55   }
57   nsToolkit::SwizzleMethods([NSMenu class], @selector(_addItem:toTable:),
58                             @selector(nsMenuX_NSMenu_addItem:toTable:), true);
59   nsToolkit::SwizzleMethods([NSMenu class], @selector(_removeItem:fromTable:),
60                             @selector(nsMenuX_NSMenu_removeItem:fromTable:),
61                             true);
62   // On SnowLeopard the Shortcut framework (which contains the
63   // SCTGRLIndex class) is loaded on demand, whenever the user first opens
64   // a menu (which normally hasn't happened yet).  So we need to load it
65   // here explicitly.
66   dlopen("/System/Library/PrivateFrameworks/Shortcut.framework/Shortcut",
67          RTLD_LAZY);
68   Class SCTGRLIndexClass = ::NSClassFromString(@"SCTGRLIndex");
69   nsToolkit::SwizzleMethods(
70       SCTGRLIndexClass, @selector(indexMenuBarDynamically),
71       @selector(nsMenuX_SCTGRLIndex_indexMenuBarDynamically));
73   Class NSServicesMenuUpdaterClass =
74       ::NSClassFromString(@"_NSServicesMenuUpdater");
75   nsToolkit::SwizzleMethods(
76       NSServicesMenuUpdaterClass,
77       @selector(populateMenu:withServiceEntries:forDisplay:),
78       @selector(nsMenuX_populateMenu:withServiceEntries:forDisplay:));
80   gMenuMethodsSwizzled = true;
84 // nsMenuX
87 nsMenuX::nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner,
88                  nsIContent* aContent)
89     : mContent(aContent), mParent(aParent), mMenuGroupOwner(aMenuGroupOwner) {
90   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
92   MOZ_COUNT_CTOR(nsMenuX);
94   SwizzleDynamicIndexingMethods();
96   mMenuDelegate = [[MenuDelegate alloc] initWithGeckoMenu:this];
98   if (!nsMenuBarX::sNativeEventTarget) {
99     nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
100   }
102   if (mContent->IsElement()) {
103     mContent->AsElement()->GetAttr(nsGkAtoms::label, mLabel);
104   }
105   mNativeMenu = CreateMenuWithGeckoString(mLabel);
107   // register this menu to be notified when changes are made to our content
108   // object
109   NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one");
110   mMenuGroupOwner->RegisterForContentChanges(mContent, this);
112   mVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
114   NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
115   mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString
116                                                action:nil
117                                         keyEquivalent:@""];
118   mNativeMenuItem.submenu = mNativeMenu;
120   SetEnabled(!mContent->IsElement() ||
121              !mContent->AsElement()->AttrValueIs(
122                  kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
123                  eCaseMatters));
125   // We call RebuildMenu here because keyboard commands are dependent upon
126   // native menu items being created. If we only call RebuildMenu when a menu
127   // is actually selected, then we can't access keyboard commands until the
128   // menu gets selected, which is bad.
129   RebuildMenu();
131   if (IsXULWindowMenu(mContent)) {
132     // Let the OS know that this is our Window menu.
133     NSApp.windowsMenu = mNativeMenu;
134   }
136   mIcon = MakeUnique<nsMenuItemIconX>(this);
138   if (mVisible) {
139     SetupIcon();
140   }
142   NS_OBJC_END_TRY_ABORT_BLOCK;
145 nsMenuX::~nsMenuX() {
146   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
148   // Make sure a pending popupshown event isn't dropped.
149   FlushMenuOpenedRunnable();
151   if (mIsOpen) {
152     [mNativeMenu cancelTracking];
153     MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
154   }
156   // Make sure pending popuphiding/popuphidden events aren't dropped.
157   FlushMenuClosedRunnable();
159   OnHighlightedItemChanged(Nothing());
160   RemoveAll();
162   mNativeMenu.delegate = nil;
163   [mNativeMenu release];
164   [mMenuDelegate release];
165   // autorelease the native menu item so that anything else happening to this
166   // object happens before the native menu item actually dies
167   [mNativeMenuItem autorelease];
169   DetachFromGroupOwnerRecursive();
171   MOZ_COUNT_DTOR(nsMenuX);
173   NS_OBJC_END_TRY_ABORT_BLOCK;
176 void nsMenuX::DetachFromGroupOwnerRecursive() {
177   if (!mMenuGroupOwner) {
178     // Don't recurse if this subtree is already detached.
179     // This avoids repeated recursion during the destruction of nested nsMenuX
180     // structures. Our invariant is: If we are detached, all of our contents are
181     // also detached.
182     return;
183   }
185   if (mMenuGroupOwner && mContent) {
186     mMenuGroupOwner->UnregisterForContentChanges(mContent);
187   }
188   mMenuGroupOwner = nullptr;
190   // Also detach all our children.
191   for (auto& child : mMenuChildren) {
192     child.match(
193         [](const RefPtr<nsMenuX>& aMenu) {
194           aMenu->DetachFromGroupOwnerRecursive();
195         },
196         [](const RefPtr<nsMenuItemX>& aMenuItem) {
197           aMenuItem->DetachFromGroupOwner();
198         });
199   }
202 void nsMenuX::OnMenuWillOpen(dom::Element* aPopupElement) {
203   RefPtr<nsMenuX> kungFuDeathGrip(this);
204   if (mObserver) {
205     mObserver->OnMenuWillOpen(aPopupElement);
206   }
209 void nsMenuX::OnMenuDidOpen(dom::Element* aPopupElement) {
210   RefPtr<nsMenuX> kungFuDeathGrip(this);
211   if (mObserver) {
212     mObserver->OnMenuDidOpen(aPopupElement);
213   }
216 void nsMenuX::OnMenuWillActivateItem(dom::Element* aPopupElement,
217                                      dom::Element* aMenuItemElement) {
218   RefPtr<nsMenuX> kungFuDeathGrip(this);
219   if (mObserver) {
220     mObserver->OnMenuWillActivateItem(aPopupElement, aMenuItemElement);
221   }
224 void nsMenuX::OnMenuClosed(dom::Element* aPopupElement) {
225   RefPtr<nsMenuX> kungFuDeathGrip(this);
226   if (mObserver) {
227     mObserver->OnMenuClosed(aPopupElement);
228   }
231 void nsMenuX::AddMenuChild(MenuChild&& aChild) {
232   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
234   WillInsertChild(aChild);
235   mMenuChildren.AppendElement(aChild);
237   bool isVisible = aChild.match(
238       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
239       [](const RefPtr<nsMenuItemX>& aMenuItem) {
240         return aMenuItem->IsVisible();
241       });
242   NSMenuItem* nativeItem = aChild.match(
243       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
244       [](const RefPtr<nsMenuItemX>& aMenuItem) {
245         return aMenuItem->NativeNSMenuItem();
246       });
248   if (isVisible) {
249     RemovePlaceholderIfPresent();
250     [mNativeMenu addItem:nativeItem];
251     ++mVisibleItemsCount;
252   }
254   NS_OBJC_END_TRY_ABORT_BLOCK;
257 void nsMenuX::InsertMenuChild(MenuChild&& aChild) {
258   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
260   WillInsertChild(aChild);
261   size_t insertionIndex = FindInsertionIndex(aChild);
262   mMenuChildren.InsertElementAt(insertionIndex, aChild);
264   bool isVisible = aChild.match(
265       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
266       [](const RefPtr<nsMenuItemX>& aMenuItem) {
267         return aMenuItem->IsVisible();
268       });
269   if (isVisible) {
270     MenuChildChangedVisibility(aChild, true);
271   }
273   NS_OBJC_END_TRY_ABORT_BLOCK;
276 void nsMenuX::RemoveMenuChild(const MenuChild& aChild) {
277   bool isVisible = aChild.match(
278       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
279       [](const RefPtr<nsMenuItemX>& aMenuItem) {
280         return aMenuItem->IsVisible();
281       });
282   if (isVisible) {
283     MenuChildChangedVisibility(aChild, false);
284   }
286   WillRemoveChild(aChild);
287   mMenuChildren.RemoveElement(aChild);
290 size_t nsMenuX::FindInsertionIndex(const MenuChild& aChild) {
291   nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
292   MOZ_RELEASE_ASSERT(menuPopup);
294   RefPtr<nsIContent> insertedContent = aChild.match(
295       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
296       [](const RefPtr<nsMenuItemX>& aMenuItem) {
297         return aMenuItem->Content();
298       });
300   MOZ_RELEASE_ASSERT(insertedContent->GetParent() == menuPopup);
302   // Iterate over menuPopup's children (insertedContent's siblings) until we
303   // encounter insertedContent. At the same time, keep track of the index in
304   // mMenuChildren.
305   size_t index = 0;
306   for (nsIContent* child = menuPopup->GetFirstChild();
307        child && index < mMenuChildren.Length();
308        child = child->GetNextSibling()) {
309     if (child == insertedContent) {
310       break;
311     }
313     RefPtr<nsIContent> contentAtIndex = mMenuChildren[index].match(
314         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
315         [](const RefPtr<nsMenuItemX>& aMenuItem) {
316           return aMenuItem->Content();
317         });
318     if (child == contentAtIndex) {
319       index++;
320     }
321   }
323   return index;
326 // Includes all items, including hidden/collapsed ones
327 uint32_t nsMenuX::GetItemCount() { return mMenuChildren.Length(); }
329 // Includes all items, including hidden/collapsed ones
330 mozilla::Maybe<nsMenuX::MenuChild> nsMenuX::GetItemAt(uint32_t aPos) {
331   if (aPos >= (uint32_t)mMenuChildren.Length()) {
332     return {};
333   }
335   return Some(mMenuChildren[aPos]);
338 // Only includes visible items
339 nsresult nsMenuX::GetVisibleItemCount(uint32_t& aCount) {
340   aCount = mVisibleItemsCount;
341   return NS_OK;
344 // Only includes visible items. Note that this is provides O(N) access
345 // If you need to iterate or search, consider using GetItemAt and doing your own
346 // filtering
347 Maybe<nsMenuX::MenuChild> nsMenuX::GetVisibleItemAt(uint32_t aPos) {
348   uint32_t count = mMenuChildren.Length();
349   if (aPos >= mVisibleItemsCount || aPos >= count) {
350     return {};
351   }
353   // If there are no invisible items, can provide direct access
354   if (mVisibleItemsCount == count) {
355     return GetItemAt(aPos);
356   }
358   // Otherwise, traverse the array until we find the the item we're looking for.
359   uint32_t visibleNodeIndex = 0;
360   for (uint32_t i = 0; i < count; i++) {
361     MenuChild item = *GetItemAt(i);
362     RefPtr<nsIContent> content = item.match(
363         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
364         [](const RefPtr<nsMenuItemX>& aMenuItem) {
365           return aMenuItem->Content();
366         });
367     if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
368       if (aPos == visibleNodeIndex) {
369         // we found the visible node we're looking for, return it
370         return Some(item);
371       }
372       visibleNodeIndex++;
373     }
374   }
376   return {};
379 Maybe<nsMenuX::MenuChild> nsMenuX::GetItemForElement(
380     Element* aMenuChildElement) {
381   for (auto& child : mMenuChildren) {
382     RefPtr<nsIContent> content = child.match(
383         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
384         [](const RefPtr<nsMenuItemX>& aMenuItem) {
385           return aMenuItem->Content();
386         });
387     if (content == aMenuChildElement) {
388       return Some(child);
389     }
390   }
391   return {};
394 nsresult nsMenuX::RemoveAll() {
395   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
397   [mNativeMenu removeAllItems];
399   for (auto& child : mMenuChildren) {
400     WillRemoveChild(child);
401   }
403   mMenuChildren.Clear();
404   mVisibleItemsCount = 0;
406   return NS_OK;
408   NS_OBJC_END_TRY_ABORT_BLOCK;
411 void nsMenuX::WillInsertChild(const MenuChild& aChild) {
412   if (aChild.is<RefPtr<nsMenuX>>()) {
413     aChild.as<RefPtr<nsMenuX>>()->SetObserver(this);
414   }
417 void nsMenuX::WillRemoveChild(const MenuChild& aChild) {
418   aChild.match(
419       [](const RefPtr<nsMenuX>& aMenu) {
420         aMenu->DetachFromGroupOwnerRecursive();
421         aMenu->DetachFromParent();
422         aMenu->SetObserver(nullptr);
423       },
424       [](const RefPtr<nsMenuItemX>& aMenuItem) {
425         aMenuItem->DetachFromGroupOwner();
426         aMenuItem->DetachFromParent();
427       });
430 void nsMenuX::MenuOpened() {
431   if (mIsOpen) {
432     return;
433   }
435   // Make sure we fire any pending popupshown / popuphiding / popuphidden events
436   // first.
437   FlushMenuOpenedRunnable();
438   FlushMenuClosedRunnable();
440   if (!mDidFirePopupshowingAndIsApprovedToOpen) {
441     // Fire popupshowing now.
442     bool approvedToOpen = OnOpen();
443     if (!approvedToOpen) {
444       // We can only stop menus from opening which we open ourselves. We cannot
445       // stop menubar root menus or menu submenus from opening. For context
446       // menus, we can call OnOpen() before we ask the system to open the menu.
447       NS_WARNING("The popupshowing event had preventDefault() called on it, "
448                  "but in MenuOpened() it "
449                  "is too late to stop the menu from opening.");
450     }
451   }
453   mIsOpen = true;
455   // Reset mDidFirePopupshowingAndIsApprovedToOpen for the next menu opening.
456   mDidFirePopupshowingAndIsApprovedToOpen = false;
458   if (mNeedsRebuild) {
459     OnHighlightedItemChanged(Nothing());
460     RemoveAll();
461     RebuildMenu();
462   }
464   // Fire the popupshown event in MenuOpenedAsync.
465   // MenuOpened() is called during menuWillOpen, and if cancelTracking is called
466   // now, menuDidClose will not be called. The runnable object must not hold a
467   // strong reference to the nsMenuX, so that there is no reference cycle.
468   class MenuOpenedAsyncRunnable final : public mozilla::CancelableRunnable {
469    public:
470     explicit MenuOpenedAsyncRunnable(nsMenuX* aMenu)
471         : CancelableRunnable("MenuOpenedAsyncRunnable"), mMenu(aMenu) {}
473     // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
474     MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
475       if (RefPtr<nsMenuX> menu = mMenu) {
476         menu->MenuOpenedAsync();
477         mMenu = nullptr;
478       }
479       return NS_OK;
480     }
481     nsresult Cancel() override {
482       mMenu = nullptr;
483       return NS_OK;
484     }
486    private:
487     nsMenuX* mMenu;  // weak, cleared by Cancel() and Run()
488   };
489   mPendingAsyncMenuOpenRunnable = new MenuOpenedAsyncRunnable(this);
490   NS_DispatchToCurrentThread(mPendingAsyncMenuOpenRunnable);
493 void nsMenuX::FlushMenuOpenedRunnable() {
494   if (mPendingAsyncMenuOpenRunnable) {
495     MenuOpenedAsync();
496   }
499 void nsMenuX::MenuOpenedAsync() {
500   if (mPendingAsyncMenuOpenRunnable) {
501     mPendingAsyncMenuOpenRunnable->Cancel();
502     mPendingAsyncMenuOpenRunnable = nullptr;
503   }
505   mIsOpenForGecko = true;
507   // Open the node.
508   if (mContent->IsElement()) {
509     mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open,
510                                    u"true"_ns, true);
511   }
513   RefPtr<nsIContent> popupContent = GetMenuPopupContent();
515   // Notify our observer.
516   if (mObserver && popupContent) {
517     mObserver->OnMenuDidOpen(popupContent->AsElement());
518   }
520   // Fire popupshown.
521   nsEventStatus status = nsEventStatus_eIgnore;
522   WidgetMouseEvent event(true, eXULPopupShown, nullptr,
523                          WidgetMouseEvent::eReal);
524   RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
525   EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
528 void nsMenuX::MenuClosed() {
529   if (!mIsOpen) {
530     return;
531   }
533   // Make sure we fire any pending popupshown events first.
534   FlushMenuOpenedRunnable();
536   // If any of our submenus were opened programmatically, make sure they get
537   // closed first.
538   for (auto& child : mMenuChildren) {
539     if (child.is<RefPtr<nsMenuX>>()) {
540       child.as<RefPtr<nsMenuX>>()->MenuClosed();
541     }
542   }
544   mIsOpen = false;
546   // Do the rest of the MenuClosed work in MenuClosedAsync.
547   // MenuClosed() is called from -[NSMenuDelegate menuDidClose:]. If a menuitem
548   // was clicked, menuDidClose is called *before* menuItemHit for the clicked
549   // menu item is called. This runnable will be canceled if ~nsMenuX runs before
550   // the runnable. The runnable object must not hold a strong reference to the
551   // nsMenuX, so that there is no reference cycle.
552   class MenuClosedAsyncRunnable final : public mozilla::CancelableRunnable {
553    public:
554     explicit MenuClosedAsyncRunnable(nsMenuX* aMenu)
555         : CancelableRunnable("MenuClosedAsyncRunnable"), mMenu(aMenu) {}
557     // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
558     MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
559       if (RefPtr<nsMenuX> menu = mMenu) {
560         menu->MenuClosedAsync();
561         mMenu = nullptr;
562       }
563       return NS_OK;
564     }
565     nsresult Cancel() override {
566       mMenu = nullptr;
567       return NS_OK;
568     }
570    private:
571     nsMenuX* mMenu;  // weak, cleared by Cancel() and Run()
572   };
574   mPendingAsyncMenuCloseRunnable = new MenuClosedAsyncRunnable(this);
576   NS_DispatchToCurrentThread(mPendingAsyncMenuCloseRunnable);
579 void nsMenuX::FlushMenuClosedRunnable() {
580   // If any of our submenus have a pending menu closed runnable, make sure those
581   // run first.
582   for (auto& child : mMenuChildren) {
583     if (child.is<RefPtr<nsMenuX>>()) {
584       child.as<RefPtr<nsMenuX>>()->FlushMenuClosedRunnable();
585     }
586   }
588   if (mPendingAsyncMenuCloseRunnable) {
589     MenuClosedAsync();
590   }
593 void nsMenuX::MenuClosedAsync() {
594   if (mPendingAsyncMenuCloseRunnable) {
595     mPendingAsyncMenuCloseRunnable->Cancel();
596     mPendingAsyncMenuCloseRunnable = nullptr;
597   }
599   // If we have pending command events, run those first.
600   nsTArray<PendingCommandEvent> events = std::move(mPendingCommandEvents);
601   for (auto& event : events) {
602     event.mMenuItem->DoCommand(event.mModifiers, event.mButton);
603   }
605   // Make sure no item is highlighted.
606   OnHighlightedItemChanged(Nothing());
608   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
609   nsCOMPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
611   nsEventStatus status = nsEventStatus_eIgnore;
612   WidgetMouseEvent popupHiding(true, eXULPopupHiding, nullptr,
613                                WidgetMouseEvent::eReal);
614   EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHiding, nullptr,
615                             &status);
617   mIsOpenForGecko = false;
619   if (mContent->IsElement()) {
620     mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true);
621   }
623   WidgetMouseEvent popupHidden(true, eXULPopupHidden, nullptr,
624                                WidgetMouseEvent::eReal);
625   EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHidden, nullptr,
626                             &status);
628   // Notify our observer.
629   if (mObserver && popupContent) {
630     mObserver->OnMenuClosed(popupContent->AsElement());
631   }
634 void nsMenuX::ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem,
635                                        NSEventModifierFlags aModifiers,
636                                        int16_t aButton) {
637   if (mIsOpenForGecko) {
638     // Queue the event into mPendingCommandEvents. We will call aItem->DoCommand
639     // in MenuClosedAsync(). We rely on the assumption that MenuClosedAsync will
640     // run soon.
641     mPendingCommandEvents.AppendElement(
642         PendingCommandEvent{std::move(aItem), aModifiers, aButton});
643   } else {
644     // The menu item was activated outside of a regular open / activate / close
645     // sequence. This happens in multiple cases:
646     //  - When a menu item is activated by a keyboard shortcut while all windows
647     //  are closed
648     //    (otherwise those shortcuts go through Gecko's manual keyboard
649     //    handling)
650     //  - When a menu item in the Dock menu is clicked
651     //  - During native menu tests
652     //
653     // Run the command synchronously.
654     aItem->DoCommand(aModifiers, aButton);
655   }
658 bool nsMenuX::Close() {
659   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
661   if (mDidFirePopupshowingAndIsApprovedToOpen && !mIsOpen) {
662     // Close is being called right after this menu was opened, but before
663     // MenuOpened() had a chance to run. Call it here so that we can go through
664     // the entire popupshown -> popuphiding -> popuphidden sequence. Some
665     // callers expect to get a popuphidden event even if they close the popup
666     // before it was fully open.
667     MenuOpened();
668   }
670   FlushMenuOpenedRunnable();
672   bool wasOpen = mIsOpenForGecko;
674   if (mIsOpen) {
675     // Close the menu.
676     // We usually don't get here during normal Firefox usage: If the user closes
677     // the menu by clicking an item, or by clicking outside the menu, or by
678     // pressing escape, then the menu gets closed by macOS, and not by a call to
679     // nsMenuX::Close(). If we do get here, it's usually because we're running
680     // an automated test. Close the menu without the fade-out animation so that
681     // we don't unnecessarily slow down the automated tests.
682     [mNativeMenu cancelTrackingWithoutAnimation];
683     MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
685     // Handle closing synchronously.
686     MenuClosed();
687   }
689   FlushMenuClosedRunnable();
691   return wasOpen;
693   NS_OBJC_END_TRY_ABORT_BLOCK;
696 void nsMenuX::OnHighlightedItemChanged(
697     const Maybe<uint32_t>& aNewHighlightedIndex) {
698   if (mHighlightedItemIndex == aNewHighlightedIndex) {
699     return;
700   }
702   if (mHighlightedItemIndex) {
703     Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*mHighlightedItemIndex);
704     if (target && target->is<RefPtr<nsMenuItemX>>()) {
705       bool handlerCalledPreventDefault;  // but we don't actually care
706       target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(
707           u"DOMMenuItemInactive"_ns, &handlerCalledPreventDefault);
708     }
709   }
710   if (aNewHighlightedIndex) {
711     Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*aNewHighlightedIndex);
712     if (target && target->is<RefPtr<nsMenuItemX>>()) {
713       bool handlerCalledPreventDefault;  // but we don't actually care
714       target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(
715           u"DOMMenuItemActive"_ns, &handlerCalledPreventDefault);
716     }
717   }
718   mHighlightedItemIndex = aNewHighlightedIndex;
721 void nsMenuX::OnWillActivateItem(NSMenuItem* aItem) {
722   if (!mIsOpenForGecko) {
723     return;
724   }
726   if (mMenuGroupOwner && mObserver) {
727     nsMenuItemX* item =
728         mMenuGroupOwner->GetMenuItemForCommandID(uint32_t(aItem.tag));
729     if (item && item->Content()->IsElement()) {
730       RefPtr<dom::Element> itemElement = item->Content()->AsElement();
731       if (nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent()) {
732         mObserver->OnMenuWillActivateItem(popupContent->AsElement(),
733                                           itemElement);
734       }
735     }
736   }
739 // Flushes style.
740 static NSUserInterfaceLayoutDirection DirectionForElement(
741     dom::Element* aElement) {
742   // Get the direction from the computed style so that inheritance into submenus
743   // is respected. aElement may not have a frame.
744   RefPtr<const ComputedStyle> sc =
745       nsComputedDOMStyle::GetComputedStyle(aElement);
746   if (!sc) {
747     return NSApp.userInterfaceLayoutDirection;
748   }
750   switch (sc->StyleVisibility()->mDirection) {
751     case StyleDirection::Ltr:
752       return NSUserInterfaceLayoutDirectionLeftToRight;
753     case StyleDirection::Rtl:
754       return NSUserInterfaceLayoutDirectionRightToLeft;
755   }
758 void nsMenuX::RebuildMenu() {
759   MOZ_RELEASE_ASSERT(mNeedsRebuild);
760   gConstructingMenu = true;
762   // Retrieve our menupopup.
763   nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
764   if (!menuPopup) {
765     gConstructingMenu = false;
766     return;
767   }
769   if (menuPopup->IsElement()) {
770     mNativeMenu.userInterfaceLayoutDirection =
771         DirectionForElement(menuPopup->AsElement());
772   }
774   // Iterate over the kids
775   for (nsIContent* child = menuPopup->GetFirstChild(); child;
776        child = child->GetNextSibling()) {
777     if (Maybe<MenuChild> menuChild = CreateMenuChild(child)) {
778       AddMenuChild(std::move(*menuChild));
779     }
780   }  // for each menu item
782   InsertPlaceholderIfNeeded();
784   gConstructingMenu = false;
785   mNeedsRebuild = false;
788 void nsMenuX::InsertPlaceholderIfNeeded() {
789   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
791   if ([mNativeMenu numberOfItems] == 0) {
792     MOZ_RELEASE_ASSERT(mVisibleItemsCount == 0);
793     NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:@""
794                                                   action:nil
795                                            keyEquivalent:@""];
796     item.enabled = NO;
797     item.view =
798         [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 150, 1)] autorelease];
799     [mNativeMenu addItem:item];
800     [item release];
801   }
803   NS_OBJC_END_TRY_ABORT_BLOCK;
806 void nsMenuX::RemovePlaceholderIfPresent() {
807   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
809   if (mVisibleItemsCount == 0 && [mNativeMenu numberOfItems] == 1) {
810     // Remove the placeholder.
811     [mNativeMenu removeItemAtIndex:0];
812   }
814   NS_OBJC_END_TRY_ABORT_BLOCK;
817 void nsMenuX::SetRebuild(bool aNeedsRebuild) {
818   if (!gConstructingMenu) {
819     mNeedsRebuild = aNeedsRebuild;
820     if (mParent && mParent->AsMenuBar()) {
821       mParent->AsMenuBar()->SetNeedsRebuild();
822     }
823   }
826 nsresult nsMenuX::SetEnabled(bool aIsEnabled) {
827   if (aIsEnabled != mIsEnabled) {
828     // we always want to rebuild when this changes
829     mIsEnabled = aIsEnabled;
830     mNativeMenuItem.enabled = mIsEnabled;
831   }
832   return NS_OK;
835 nsresult nsMenuX::GetEnabled(bool* aIsEnabled) {
836   NS_ENSURE_ARG_POINTER(aIsEnabled);
837   *aIsEnabled = mIsEnabled;
838   return NS_OK;
841 GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& aMenuTitle) {
842   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
844   NSString* title = [NSString stringWithCharacters:(UniChar*)aMenuTitle.get()
845                                             length:aMenuTitle.Length()];
846   GeckoNSMenu* myMenu = [[GeckoNSMenu alloc] initWithTitle:title];
847   myMenu.delegate = mMenuDelegate;
849   // We don't want this menu to auto-enable menu items because then Cocoa
850   // overrides our decisions and things get incorrectly enabled/disabled.
851   myMenu.autoenablesItems = NO;
853   // we used to install Carbon event handlers here, but since NSMenu* doesn't
854   // create its underlying MenuRef until just before display, we delay until
855   // that happens. Now we install the event handlers when Cocoa notifies
856   // us that a menu is about to display - see the Cocoa MenuDelegate class.
858   return myMenu;
860   NS_OBJC_END_TRY_ABORT_BLOCK;
863 Maybe<nsMenuX::MenuChild> nsMenuX::CreateMenuChild(nsIContent* aContent) {
864   if (aContent->IsAnyOfXULElements(nsGkAtoms::menuitem,
865                                    nsGkAtoms::menuseparator)) {
866     return Some(MenuChild(CreateMenuItem(aContent)));
867   }
868   if (aContent->IsXULElement(nsGkAtoms::menu)) {
869     return Some(
870         MenuChild(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aContent)));
871   }
872   return {};
875 RefPtr<nsMenuItemX> nsMenuX::CreateMenuItem(nsIContent* aMenuItemContent) {
876   MOZ_RELEASE_ASSERT(aMenuItemContent);
878   nsAutoString menuitemName;
879   if (aMenuItemContent->IsElement()) {
880     aMenuItemContent->AsElement()->GetAttr(nsGkAtoms::label, menuitemName);
881   }
883   EMenuItemType itemType = eRegularMenuItemType;
884   if (aMenuItemContent->IsXULElement(nsGkAtoms::menuseparator)) {
885     itemType = eSeparatorMenuItemType;
886   } else if (aMenuItemContent->IsElement()) {
887     static Element::AttrValuesArray strings[] = {nsGkAtoms::checkbox,
888                                                  nsGkAtoms::radio, nullptr};
889     switch (aMenuItemContent->AsElement()->FindAttrValueIn(
890         kNameSpaceID_None, nsGkAtoms::type, strings, eCaseMatters)) {
891       case 0:
892         itemType = eCheckboxMenuItemType;
893         break;
894       case 1:
895         itemType = eRadioMenuItemType;
896         break;
897     }
898   }
900   return MakeRefPtr<nsMenuItemX>(this, menuitemName, itemType, mMenuGroupOwner,
901                                  aMenuItemContent);
904 // This menu is about to open. Returns false if the handler wants to stop the
905 // opening of the menu.
906 bool nsMenuX::OnOpen() {
907   if (mDidFirePopupshowingAndIsApprovedToOpen) {
908     return true;
909   }
911   if (mIsOpen) {
912     NS_WARNING("nsMenuX::OnOpen() called while the menu is already considered "
913                "to be open. This "
914                "seems odd.");
915   }
917   RefPtr<nsIContent> popupContent = GetMenuPopupContent();
919   if (mObserver && popupContent) {
920     mObserver->OnMenuWillOpen(popupContent->AsElement());
921   }
923   nsEventStatus status = nsEventStatus_eIgnore;
924   WidgetMouseEvent event(true, eXULPopupShowing, nullptr,
925                          WidgetMouseEvent::eReal);
927   nsresult rv = NS_OK;
928   RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
929   rv = EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
930   if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) {
931     return false;
932   }
934   DidFirePopupShowing();
936   return true;
939 void nsMenuX::DidFirePopupShowing() {
940   mDidFirePopupshowingAndIsApprovedToOpen = true;
942   // If the open is going to succeed we need to walk our menu items, checking to
943   // see if any of them have a command attribute. If so, several attributes
944   // must potentially be updated.
946   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
947   if (!popupContent) {
948     return;
949   }
951   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
952   if (pm) {
953     pm->UpdateMenuItems(popupContent->AsElement());
954   }
957 // Find the |menupopup| child in the |popup| representing this menu. It should
958 // be one of a very few children so we won't be iterating over a bazillion menu
959 // items to find it (so the strcmp won't kill us).
960 already_AddRefed<nsIContent> nsMenuX::GetMenuPopupContent() {
961   // Check to see if we are a "menupopup" node (if we are a native menu).
962   if (mContent->IsXULElement(nsGkAtoms::menupopup)) {
963     return do_AddRef(mContent);
964   }
966   // Otherwise check our child nodes.
968   for (RefPtr<nsIContent> child = mContent->GetFirstChild(); child;
969        child = child->GetNextSibling()) {
970     if (child->IsXULElement(nsGkAtoms::menupopup)) {
971       return child.forget();
972     }
973   }
975   return nullptr;
978 bool nsMenuX::IsXULHelpMenu(nsIContent* aMenuContent) {
979   bool retval = false;
980   if (aMenuContent && aMenuContent->IsElement()) {
981     nsAutoString id;
982     aMenuContent->AsElement()->GetAttr(nsGkAtoms::id, id);
983     if (id.Equals(u"helpMenu"_ns)) {
984       retval = true;
985     }
986   }
987   return retval;
990 bool nsMenuX::IsXULWindowMenu(nsIContent* aMenuContent) {
991   bool retval = false;
992   if (aMenuContent && aMenuContent->IsElement()) {
993     nsAutoString id;
994     aMenuContent->AsElement()->GetAttr(nsGkAtoms::id, id);
995     if (id.Equals(u"windowMenu"_ns)) {
996       retval = true;
997     }
998   }
999   return retval;
1003 // nsChangeObserver
1006 void nsMenuX::ObserveAttributeChanged(dom::Document* aDocument,
1007                                       nsIContent* aContent,
1008                                       nsAtom* aAttribute) {
1009   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
1011   // ignore the |open| attribute, which is by far the most common
1012   if (gConstructingMenu || (aAttribute == nsGkAtoms::open)) {
1013     return;
1014   }
1016   if (aAttribute == nsGkAtoms::disabled) {
1017     SetEnabled(!mContent->AsElement()->AttrValueIs(
1018         kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
1019         eCaseMatters));
1020   } else if (aAttribute == nsGkAtoms::label) {
1021     mContent->AsElement()->GetAttr(nsGkAtoms::label, mLabel);
1022     NSString* newCocoaLabelString =
1023         nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
1024     mNativeMenu.title = newCocoaLabelString;
1025     mNativeMenuItem.title = newCocoaLabelString;
1026   } else if (aAttribute == nsGkAtoms::hidden ||
1027              aAttribute == nsGkAtoms::collapsed) {
1028     SetRebuild(true);
1030     bool newVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
1032     // don't do anything if the state is correct already
1033     if (newVisible == mVisible) {
1034       return;
1035     }
1037     mVisible = newVisible;
1038     if (mParent) {
1039       RefPtr<nsMenuX> self = this;
1040       mParent->MenuChildChangedVisibility(MenuChild(self), newVisible);
1041     }
1042     if (mVisible) {
1043       SetupIcon();
1044     }
1045   } else if (aAttribute == nsGkAtoms::image) {
1046     SetupIcon();
1047   }
1049   NS_OBJC_END_TRY_ABORT_BLOCK;
1052 void nsMenuX::ObserveContentRemoved(dom::Document* aDocument,
1053                                     nsIContent* aContainer, nsIContent* aChild,
1054                                     nsIContent* aPreviousSibling) {
1055   if (gConstructingMenu) {
1056     return;
1057   }
1059   SetRebuild(true);
1060   mMenuGroupOwner->UnregisterForContentChanges(aChild);
1062   if (!mIsOpen) {
1063     // We will update the menu contents the next time the menu is opened.
1064     return;
1065   }
1067   // The menu is currently open. Remove the child from mMenuChildren and from
1068   // our NSMenu.
1069   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
1070   if (popupContent && aContainer == popupContent && aChild->IsElement()) {
1071     if (Maybe<MenuChild> child = GetItemForElement(aChild->AsElement())) {
1072       RemoveMenuChild(*child);
1073     }
1074   }
1077 void nsMenuX::ObserveContentInserted(dom::Document* aDocument,
1078                                      nsIContent* aContainer,
1079                                      nsIContent* aChild) {
1080   if (gConstructingMenu) {
1081     return;
1082   }
1084   SetRebuild(true);
1086   if (!mIsOpen) {
1087     // We will update the menu contents the next time the menu is opened.
1088     return;
1089   }
1091   // The menu is currently open. Insert the child into mMenuChildren and into
1092   // our NSMenu.
1093   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
1094   if (popupContent && aContainer == popupContent) {
1095     if (Maybe<MenuChild> child = CreateMenuChild(aChild)) {
1096       InsertMenuChild(std::move(*child));
1097     }
1098   }
1101 void nsMenuX::SetupIcon() {
1102   mIcon->SetupIcon(mContent);
1103   mNativeMenuItem.image = mIcon->GetIconImage();
1106 void nsMenuX::IconUpdated() {
1107   mNativeMenuItem.image = mIcon->GetIconImage();
1108   if (mIconListener) {
1109     mIconListener->IconUpdated();
1110   }
1113 void nsMenuX::MenuChildChangedVisibility(const MenuChild& aChild,
1114                                          bool aIsVisible) {
1115   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
1117   NSMenuItem* nativeItem = aChild.match(
1118       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
1119       [](const RefPtr<nsMenuItemX>& aMenuItem) {
1120         return aMenuItem->NativeNSMenuItem();
1121       });
1122   if (aIsVisible) {
1123     MOZ_RELEASE_ASSERT(
1124         !nativeItem.menu,
1125         "The native item should not be in a menu while it is hidden");
1126     RemovePlaceholderIfPresent();
1127     NSInteger insertionPoint = CalculateNativeInsertionPoint(aChild);
1128     [mNativeMenu insertItem:nativeItem atIndex:insertionPoint];
1129     mVisibleItemsCount++;
1130   } else {
1131     MOZ_RELEASE_ASSERT(
1132         [mNativeMenu indexOfItem:nativeItem] != -1,
1133         "The native item should be in this menu while it is visible");
1134     [mNativeMenu removeItem:nativeItem];
1135     mVisibleItemsCount--;
1136     InsertPlaceholderIfNeeded();
1137   }
1139   NS_OBJC_END_TRY_ABORT_BLOCK;
1142 NSInteger nsMenuX::CalculateNativeInsertionPoint(const MenuChild& aChild) {
1143   NSInteger insertionPoint = 0;
1144   for (auto& currItem : mMenuChildren) {
1145     // Using GetItemAt instead of GetVisibleItemAt to avoid O(N^2)
1146     if (currItem == aChild) {
1147       return insertionPoint;
1148     }
1149     NSMenuItem* nativeItem = currItem.match(
1150         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
1151         [](const RefPtr<nsMenuItemX>& aMenuItem) {
1152           return aMenuItem->NativeNSMenuItem();
1153         });
1154     // Only count visible items.
1155     if (nativeItem.menu) {
1156       insertionPoint++;
1157     }
1158   }
1159   return insertionPoint;
1162 void nsMenuX::Dump(uint32_t aIndent) const {
1163   printf(
1164       "%*s - menu [%p] %-16s <%s>", aIndent * 2, "", this,
1165       mLabel.IsEmpty() ? "(empty label)" : NS_ConvertUTF16toUTF8(mLabel).get(),
1166       NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
1167   if (mNeedsRebuild) {
1168     printf(" [NeedsRebuild]");
1169   }
1170   if (mIsOpen) {
1171     printf(" [Open]");
1172   }
1173   if (mVisible) {
1174     printf(" [Visible]");
1175   }
1176   if (mIsEnabled) {
1177     printf(" [IsEnabled]");
1178   }
1179   printf(" (%d visible items)", int(mVisibleItemsCount));
1180   printf("\n");
1181   for (const auto& subitem : mMenuChildren) {
1182     subitem.match(
1183         [=](const RefPtr<nsMenuX>& aMenu) { aMenu->Dump(aIndent + 1); },
1184         [=](const RefPtr<nsMenuItemX>& aMenuItem) {
1185           aMenuItem->Dump(aIndent + 1);
1186         });
1187   }
1191 // MenuDelegate Objective-C class, used to set up Carbon events
1194 @implementation MenuDelegate
1196 - (id)initWithGeckoMenu:(nsMenuX*)geckoMenu {
1197   if ((self = [super init])) {
1198     NS_ASSERTION(geckoMenu, "Cannot initialize native menu delegate with NULL "
1199                             "gecko menu! Will crash!");
1200     mGeckoMenu = geckoMenu;
1201     mBlocksToRunWhenOpen = [[NSMutableArray alloc] init];
1202   }
1203   return self;
1206 - (void)dealloc {
1207   [mBlocksToRunWhenOpen release];
1208   [super dealloc];
1211 - (void)runBlockWhenOpen:(void (^)())block {
1212   [mBlocksToRunWhenOpen addObject:[[block copy] autorelease]];
1215 - (void)menu:(NSMenu*)aMenu willHighlightItem:(NSMenuItem*)aItem {
1216   if (!aMenu || !mGeckoMenu) {
1217     return;
1218   }
1220   Maybe<uint32_t> index =
1221       aItem ? Some(static_cast<uint32_t>([aMenu indexOfItem:aItem]))
1222             : Nothing();
1223   mGeckoMenu->OnHighlightedItemChanged(index);
1226 - (void)menuWillOpen:(NSMenu*)menu {
1227   for (void (^block)() in mBlocksToRunWhenOpen) {
1228     block();
1229   }
1230   [mBlocksToRunWhenOpen removeAllObjects];
1232   if (!mGeckoMenu) {
1233     return;
1234   }
1236   // Don't do anything while the OS is (re)indexing our menus (on Leopard and
1237   // higher).  This stops the Help menu from being able to search in our
1238   // menus, but it also resolves many other problems.
1239   if (nsMenuX::sIndexingMenuLevel > 0) {
1240     return;
1241   }
1243   // Hold a strong reference to mGeckoMenu while calling its methods.
1244   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1245   geckoMenu->MenuOpened();
1248 - (void)menuDidClose:(NSMenu*)menu {
1249   if (!mGeckoMenu) {
1250     return;
1251   }
1253   // Don't do anything while the OS is (re)indexing our menus (on Leopard and
1254   // higher).  This stops the Help menu from being able to search in our
1255   // menus, but it also resolves many other problems.
1256   if (nsMenuX::sIndexingMenuLevel > 0) {
1257     return;
1258   }
1260   // Hold a strong reference to mGeckoMenu while calling its methods.
1261   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1262   geckoMenu->MenuClosed();
1265 // This is called after menuDidClose:.
1266 - (void)menu:(NSMenu*)aMenu willActivateItem:(NSMenuItem*)aItem {
1267   if (!mGeckoMenu) {
1268     return;
1269   }
1271   // Hold a strong reference to mGeckoMenu while calling its methods.
1272   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1273   geckoMenu->OnWillActivateItem(aItem);
1276 @end
1278 // OS X Leopard (at least as of 10.5.2) has an obscure bug triggered by some
1279 // behavior that's present in Mozilla.org browsers but not (as best I can
1280 // tell) in Apple products like Safari.  (It's not yet clear exactly what this
1281 // behavior is.)
1283 // The bug is that sometimes you crash on quit in nsMenuX::RemoveAll(), on a
1284 // call to [NSMenu removeItemAtIndex:].  The crash is caused by trying to
1285 // access a deleted NSMenuItem object (sometimes (perhaps always?) by trying
1286 // to send it a _setChangedFlags: message).  Though this object was deleted
1287 // some time ago, it remains registered as a potential target for a particular
1288 // key equivalent.  So when [NSMenu removeItemAtIndex:] removes the current
1289 // target for that same key equivalent, the OS tries to "activate" the
1290 // previous target.
1292 // The underlying reason appears to be that NSMenu's _addItem:toTable: and
1293 // _removeItem:fromTable: methods (which are used to keep a hashtable of
1294 // registered key equivalents) don't properly "retain" and "release"
1295 // NSMenuItem objects as they are added to and removed from the hashtable.
1297 // Our (hackish) workaround is to shadow the OS's hashtable with another
1298 // hastable of our own (gShadowKeyEquivDB), and use it to "retain" and
1299 // "release" NSMenuItem objects as needed.  This resolves bmo bugs 422287 and
1300 // 423669.  When (if) Apple fixes this bug, we can remove this workaround.
1302 static NSMutableDictionary* gShadowKeyEquivDB = nil;
1304 // Class for values in gShadowKeyEquivDB.
1306 @interface KeyEquivDBItem : NSObject {
1307   NSMenuItem* mItem;
1308   NSMutableSet* mTables;
1311 - (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable;
1312 - (BOOL)hasTable:(NSMapTable*)aTable;
1313 - (int)addTable:(NSMapTable*)aTable;
1314 - (int)removeTable:(NSMapTable*)aTable;
1316 @end
1318 @implementation KeyEquivDBItem
1320 - (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable {
1321   if (!gShadowKeyEquivDB) {
1322     gShadowKeyEquivDB = [[NSMutableDictionary alloc] init];
1323   }
1324   self = [super init];
1325   if (aItem && aTable) {
1326     mTables = [[NSMutableSet alloc] init];
1327     mItem = [aItem retain];
1328     [mTables addObject:[NSValue valueWithPointer:aTable]];
1329   } else {
1330     mTables = nil;
1331     mItem = nil;
1332   }
1333   return self;
1336 - (void)dealloc {
1337   if (mTables) {
1338     [mTables release];
1339   }
1340   if (mItem) {
1341     [mItem release];
1342   }
1343   [super dealloc];
1346 - (BOOL)hasTable:(NSMapTable*)aTable {
1347   return [mTables member:[NSValue valueWithPointer:aTable]] ? YES : NO;
1350 // Does nothing if aTable (its index value) is already present in mTables.
1351 - (int)addTable:(NSMapTable*)aTable {
1352   if (aTable) {
1353     [mTables addObject:[NSValue valueWithPointer:aTable]];
1354   }
1355   return [mTables count];
1358 - (int)removeTable:(NSMapTable*)aTable {
1359   if (aTable) {
1360     NSValue* objectToRemove =
1361         [mTables member:[NSValue valueWithPointer:aTable]];
1362     if (objectToRemove) {
1363       [mTables removeObject:objectToRemove];
1364     }
1365   }
1366   return [mTables count];
1369 @end
1371 @interface NSMenu (MethodSwizzling)
1372 + (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable;
1373 + (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem
1374                         fromTable:(NSMapTable*)aTable;
1375 @end
1377 @implementation NSMenu (MethodSwizzling)
1379 + (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable {
1380   if (aItem && aTable) {
1381     NSValue* key = [NSValue valueWithPointer:aItem];
1382     KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
1383     if (shadowItem) {
1384       [shadowItem addTable:aTable];
1385     } else {
1386       shadowItem = [[KeyEquivDBItem alloc] initWithItem:aItem table:aTable];
1387       [gShadowKeyEquivDB setObject:shadowItem forKey:key];
1388       // Release after [NSMutableDictionary setObject:forKey:] retains it (so
1389       // that it will get dealloced when removeObjectForKey: is called).
1390       [shadowItem release];
1391     }
1392   }
1394   [self nsMenuX_NSMenu_addItem:aItem toTable:aTable];
1397 + (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem
1398                         fromTable:(NSMapTable*)aTable {
1399   [self nsMenuX_NSMenu_removeItem:aItem fromTable:aTable];
1401   if (aItem && aTable) {
1402     NSValue* key = [NSValue valueWithPointer:aItem];
1403     KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
1404     if (shadowItem && [shadowItem hasTable:aTable]) {
1405       if (![shadowItem removeTable:aTable]) {
1406         [gShadowKeyEquivDB removeObjectForKey:key];
1407       }
1408     }
1409   }
1412 @end
1414 // This class is needed to keep track of when the OS is (re)indexing all of
1415 // our menus.  This appears to only happen on Leopard and higher, and can
1416 // be triggered by opening the Help menu.  Some operations are unsafe while
1417 // this is happening -- notably the calls to [[NSImage alloc]
1418 // initWithSize:imageRect.size] and [newImage lockFocus] in nsMenuItemIconX::
1419 // OnStopFrame().  But we don't yet have a complete list, and Apple doesn't
1420 // yet have any documentation on this subject.  (Apple also doesn't yet have
1421 // any documented way to find the information we seek here.)  The "original"
1422 // of this class (the one whose indexMenuBarDynamically method we hook) is
1423 // defined in the Shortcut framework in /System/Library/PrivateFrameworks.
1424 @interface NSObject (SCTGRLIndexMethodSwizzling)
1425 - (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically;
1426 @end
1428 @implementation NSObject (SCTGRLIndexMethodSwizzling)
1430 - (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically {
1431   // This method appears to be called (once) whenever the OS (re)indexes our
1432   // menus.  sIndexingMenuLevel is a int32_t just in case it might be
1433   // reentered.  As it's running, it spawns calls to two undocumented
1434   // HIToolbox methods (_SimulateMenuOpening() and _SimulateMenuClosed()),
1435   // which "simulate" the opening and closing of our menus without actually
1436   // displaying them.
1437   ++nsMenuX::sIndexingMenuLevel;
1438   [self nsMenuX_SCTGRLIndex_indexMenuBarDynamically];
1439   --nsMenuX::sIndexingMenuLevel;
1442 @end
1444 @interface NSObject (NSServicesMenuUpdaterSwizzling)
1445 - (void)nsMenuX_populateMenu:(NSMenu*)aMenu
1446           withServiceEntries:(NSArray*)aServices
1447                   forDisplay:(BOOL)aForDisplay;
1448 @end
1450 @interface _NSServiceEntry : NSObject
1451 - (NSString*)bundleIdentifier;
1452 @end
1454 @implementation NSObject (NSServicesMenuUpdaterSwizzling)
1456 - (void)nsMenuX_populateMenu:(NSMenu*)aMenu
1457           withServiceEntries:(NSArray*)aServices
1458                   forDisplay:(BOOL)aForDisplay {
1459   NSMutableArray* filteredServices = [NSMutableArray array];
1461   // We need to filter some services, such as "Search with Google", since this
1462   // service is duplicating functionality already exposed by our "Search Google
1463   // for..." context menu entry and because it opens in Safari, which can cause
1464   // confusion for users.
1465   for (_NSServiceEntry* service in aServices) {
1466     NSString* bundleId = [service bundleIdentifier];
1467     NSString* msg = [service valueForKey:@"message"];
1468     bool shouldSkip = ([bundleId isEqualToString:@"com.apple.Safari"]) ||
1469                       ([bundleId isEqualToString:@"com.apple.systemuiserver"] &&
1470                        [msg isEqualToString:@"openURL"]);
1471     if (!shouldSkip) {
1472       [filteredServices addObject:service];
1473     }
1474   }
1476   [self nsMenuX_populateMenu:aMenu
1477           withServiceEntries:filteredServices
1478                   forDisplay:aForDisplay];
1481 @end