Backed out changeset 2450366cf7ca (bug 1891629) for causing win msix mochitest failures
[gecko.git] / widget / cocoa / nsMenuX.mm
blobd6003c4faf46506aa18e8e3f795e421eeca0a18e
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   bool shouldShowServices = false;
103   if (mContent->IsElement()) {
104     mContent->AsElement()->GetAttr(nsGkAtoms::label, mLabel);
106     shouldShowServices =
107         mContent->AsElement()->HasAttr(nsGkAtoms::showservicesmenu);
108   }
109   mNativeMenu = CreateMenuWithGeckoString(mLabel, shouldShowServices);
111   // register this menu to be notified when changes are made to our content
112   // object
113   NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one");
114   mMenuGroupOwner->RegisterForContentChanges(mContent, this);
116   mVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
118   NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
119   mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString
120                                                action:nil
121                                         keyEquivalent:@""];
122   mNativeMenuItem.submenu = mNativeMenu;
124   SetEnabled(!mContent->IsElement() ||
125              !mContent->AsElement()->AttrValueIs(
126                  kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
127                  eCaseMatters));
129   // We call RebuildMenu here because keyboard commands are dependent upon
130   // native menu items being created. If we only call RebuildMenu when a menu
131   // is actually selected, then we can't access keyboard commands until the
132   // menu gets selected, which is bad.
133   RebuildMenu();
135   if (IsXULWindowMenu(mContent)) {
136     // Let the OS know that this is our Window menu.
137     NSApp.windowsMenu = mNativeMenu;
138   }
140   mIcon = MakeUnique<nsMenuItemIconX>(this);
142   if (mVisible) {
143     SetupIcon();
144   }
146   NS_OBJC_END_TRY_ABORT_BLOCK;
149 nsMenuX::~nsMenuX() {
150   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
152   // Make sure a pending popupshown event isn't dropped.
153   FlushMenuOpenedRunnable();
155   if (mIsOpen) {
156     [mNativeMenu cancelTracking];
157     MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
158   }
160   // Make sure pending popuphiding/popuphidden events aren't dropped.
161   FlushMenuClosedRunnable();
163   OnHighlightedItemChanged(Nothing());
164   RemoveAll();
166   mNativeMenu.delegate = nil;
167   [mNativeMenu release];
168   [mMenuDelegate release];
169   // autorelease the native menu item so that anything else happening to this
170   // object happens before the native menu item actually dies
171   [mNativeMenuItem autorelease];
173   DetachFromGroupOwnerRecursive();
175   MOZ_COUNT_DTOR(nsMenuX);
177   NS_OBJC_END_TRY_ABORT_BLOCK;
180 void nsMenuX::DetachFromGroupOwnerRecursive() {
181   if (!mMenuGroupOwner) {
182     // Don't recurse if this subtree is already detached.
183     // This avoids repeated recursion during the destruction of nested nsMenuX
184     // structures. Our invariant is: If we are detached, all of our contents are
185     // also detached.
186     return;
187   }
189   if (mMenuGroupOwner && mContent) {
190     mMenuGroupOwner->UnregisterForContentChanges(mContent);
191   }
192   mMenuGroupOwner = nullptr;
194   // Also detach all our children.
195   for (auto& child : mMenuChildren) {
196     child.match(
197         [](const RefPtr<nsMenuX>& aMenu) {
198           aMenu->DetachFromGroupOwnerRecursive();
199         },
200         [](const RefPtr<nsMenuItemX>& aMenuItem) {
201           aMenuItem->DetachFromGroupOwner();
202         });
203   }
206 void nsMenuX::OnMenuWillOpen(dom::Element* aPopupElement) {
207   RefPtr<nsMenuX> kungFuDeathGrip(this);
208   if (mObserver) {
209     mObserver->OnMenuWillOpen(aPopupElement);
210   }
213 void nsMenuX::OnMenuDidOpen(dom::Element* aPopupElement) {
214   RefPtr<nsMenuX> kungFuDeathGrip(this);
215   if (mObserver) {
216     mObserver->OnMenuDidOpen(aPopupElement);
217   }
220 void nsMenuX::OnMenuWillActivateItem(dom::Element* aPopupElement,
221                                      dom::Element* aMenuItemElement) {
222   RefPtr<nsMenuX> kungFuDeathGrip(this);
223   if (mObserver) {
224     mObserver->OnMenuWillActivateItem(aPopupElement, aMenuItemElement);
225   }
228 void nsMenuX::OnMenuClosed(dom::Element* aPopupElement) {
229   RefPtr<nsMenuX> kungFuDeathGrip(this);
230   if (mObserver) {
231     mObserver->OnMenuClosed(aPopupElement);
232   }
235 void nsMenuX::AddMenuChild(MenuChild&& aChild) {
236   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
238   WillInsertChild(aChild);
239   mMenuChildren.AppendElement(aChild);
241   bool isVisible = aChild.match(
242       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
243       [](const RefPtr<nsMenuItemX>& aMenuItem) {
244         return aMenuItem->IsVisible();
245       });
246   NSMenuItem* nativeItem = aChild.match(
247       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
248       [](const RefPtr<nsMenuItemX>& aMenuItem) {
249         return aMenuItem->NativeNSMenuItem();
250       });
252   if (isVisible) {
253     RemovePlaceholderIfPresent();
254     [mNativeMenu addItem:nativeItem];
255     ++mVisibleItemsCount;
256   }
258   NS_OBJC_END_TRY_ABORT_BLOCK;
261 void nsMenuX::InsertMenuChild(MenuChild&& aChild) {
262   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
264   WillInsertChild(aChild);
265   size_t insertionIndex = FindInsertionIndex(aChild);
266   mMenuChildren.InsertElementAt(insertionIndex, aChild);
268   bool isVisible = aChild.match(
269       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
270       [](const RefPtr<nsMenuItemX>& aMenuItem) {
271         return aMenuItem->IsVisible();
272       });
273   if (isVisible) {
274     MenuChildChangedVisibility(aChild, true);
275   }
277   NS_OBJC_END_TRY_ABORT_BLOCK;
280 void nsMenuX::RemoveMenuChild(const MenuChild& aChild) {
281   bool isVisible = aChild.match(
282       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
283       [](const RefPtr<nsMenuItemX>& aMenuItem) {
284         return aMenuItem->IsVisible();
285       });
286   if (isVisible) {
287     MenuChildChangedVisibility(aChild, false);
288   }
290   WillRemoveChild(aChild);
291   mMenuChildren.RemoveElement(aChild);
294 size_t nsMenuX::FindInsertionIndex(const MenuChild& aChild) {
295   nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
296   MOZ_RELEASE_ASSERT(menuPopup);
298   RefPtr<nsIContent> insertedContent = aChild.match(
299       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
300       [](const RefPtr<nsMenuItemX>& aMenuItem) {
301         return aMenuItem->Content();
302       });
304   MOZ_RELEASE_ASSERT(insertedContent->GetParent() == menuPopup);
306   // Iterate over menuPopup's children (insertedContent's siblings) until we
307   // encounter insertedContent. At the same time, keep track of the index in
308   // mMenuChildren.
309   size_t index = 0;
310   for (nsIContent* child = menuPopup->GetFirstChild();
311        child && index < mMenuChildren.Length();
312        child = child->GetNextSibling()) {
313     if (child == insertedContent) {
314       break;
315     }
317     RefPtr<nsIContent> contentAtIndex = mMenuChildren[index].match(
318         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
319         [](const RefPtr<nsMenuItemX>& aMenuItem) {
320           return aMenuItem->Content();
321         });
322     if (child == contentAtIndex) {
323       index++;
324     }
325   }
327   return index;
330 // Includes all items, including hidden/collapsed ones
331 uint32_t nsMenuX::GetItemCount() { return mMenuChildren.Length(); }
333 // Includes all items, including hidden/collapsed ones
334 mozilla::Maybe<nsMenuX::MenuChild> nsMenuX::GetItemAt(uint32_t aPos) {
335   if (aPos >= (uint32_t)mMenuChildren.Length()) {
336     return {};
337   }
339   return Some(mMenuChildren[aPos]);
342 // Only includes visible items
343 nsresult nsMenuX::GetVisibleItemCount(uint32_t& aCount) {
344   aCount = mVisibleItemsCount;
345   return NS_OK;
348 // Only includes visible items. Note that this is provides O(N) access
349 // If you need to iterate or search, consider using GetItemAt and doing your own
350 // filtering
351 Maybe<nsMenuX::MenuChild> nsMenuX::GetVisibleItemAt(uint32_t aPos) {
352   uint32_t count = mMenuChildren.Length();
353   if (aPos >= mVisibleItemsCount || aPos >= count) {
354     return {};
355   }
357   // If there are no invisible items, can provide direct access
358   if (mVisibleItemsCount == count) {
359     return GetItemAt(aPos);
360   }
362   // Otherwise, traverse the array until we find the the item we're looking for.
363   uint32_t visibleNodeIndex = 0;
364   for (uint32_t i = 0; i < count; i++) {
365     MenuChild item = *GetItemAt(i);
366     RefPtr<nsIContent> content = item.match(
367         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
368         [](const RefPtr<nsMenuItemX>& aMenuItem) {
369           return aMenuItem->Content();
370         });
371     if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
372       if (aPos == visibleNodeIndex) {
373         // we found the visible node we're looking for, return it
374         return Some(item);
375       }
376       visibleNodeIndex++;
377     }
378   }
380   return {};
383 Maybe<nsMenuX::MenuChild> nsMenuX::GetItemForElement(
384     Element* aMenuChildElement) {
385   for (auto& child : mMenuChildren) {
386     RefPtr<nsIContent> content = child.match(
387         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
388         [](const RefPtr<nsMenuItemX>& aMenuItem) {
389           return aMenuItem->Content();
390         });
391     if (content == aMenuChildElement) {
392       return Some(child);
393     }
394   }
395   return {};
398 nsresult nsMenuX::RemoveAll() {
399   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
401   [mNativeMenu removeAllItems];
403   for (auto& child : mMenuChildren) {
404     WillRemoveChild(child);
405   }
407   mMenuChildren.Clear();
408   mVisibleItemsCount = 0;
410   return NS_OK;
412   NS_OBJC_END_TRY_ABORT_BLOCK;
415 void nsMenuX::WillInsertChild(const MenuChild& aChild) {
416   if (aChild.is<RefPtr<nsMenuX>>()) {
417     aChild.as<RefPtr<nsMenuX>>()->SetObserver(this);
418   }
421 void nsMenuX::WillRemoveChild(const MenuChild& aChild) {
422   aChild.match(
423       [](const RefPtr<nsMenuX>& aMenu) {
424         aMenu->DetachFromGroupOwnerRecursive();
425         aMenu->DetachFromParent();
426         aMenu->SetObserver(nullptr);
427       },
428       [](const RefPtr<nsMenuItemX>& aMenuItem) {
429         aMenuItem->DetachFromGroupOwner();
430         aMenuItem->DetachFromParent();
431       });
434 void nsMenuX::MenuOpened() {
435   if (mIsOpen) {
436     return;
437   }
439   // Make sure we fire any pending popupshown / popuphiding / popuphidden events
440   // first.
441   FlushMenuOpenedRunnable();
442   FlushMenuClosedRunnable();
444   if (!mDidFirePopupshowingAndIsApprovedToOpen) {
445     // Fire popupshowing now.
446     bool approvedToOpen = OnOpen();
447     if (!approvedToOpen) {
448       // We can only stop menus from opening which we open ourselves. We cannot
449       // stop menubar root menus or menu submenus from opening. For context
450       // menus, we can call OnOpen() before we ask the system to open the menu.
451       NS_WARNING("The popupshowing event had preventDefault() called on it, "
452                  "but in MenuOpened() it "
453                  "is too late to stop the menu from opening.");
454     }
455   }
457   mIsOpen = true;
459   // Reset mDidFirePopupshowingAndIsApprovedToOpen for the next menu opening.
460   mDidFirePopupshowingAndIsApprovedToOpen = false;
462   if (mNeedsRebuild) {
463     OnHighlightedItemChanged(Nothing());
464     RemoveAll();
465     RebuildMenu();
466   }
468   // Fire the popupshown event in MenuOpenedAsync.
469   // MenuOpened() is called during menuWillOpen, and if cancelTracking is called
470   // now, menuDidClose will not be called. The runnable object must not hold a
471   // strong reference to the nsMenuX, so that there is no reference cycle.
472   class MenuOpenedAsyncRunnable final : public mozilla::CancelableRunnable {
473    public:
474     explicit MenuOpenedAsyncRunnable(nsMenuX* aMenu)
475         : CancelableRunnable("MenuOpenedAsyncRunnable"), mMenu(aMenu) {}
477     // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
478     MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
479       if (RefPtr<nsMenuX> menu = mMenu) {
480         menu->MenuOpenedAsync();
481         mMenu = nullptr;
482       }
483       return NS_OK;
484     }
485     nsresult Cancel() override {
486       mMenu = nullptr;
487       return NS_OK;
488     }
490    private:
491     nsMenuX* mMenu;  // weak, cleared by Cancel() and Run()
492   };
493   mPendingAsyncMenuOpenRunnable = new MenuOpenedAsyncRunnable(this);
494   NS_DispatchToCurrentThread(mPendingAsyncMenuOpenRunnable);
497 void nsMenuX::FlushMenuOpenedRunnable() {
498   if (mPendingAsyncMenuOpenRunnable) {
499     MenuOpenedAsync();
500   }
503 void nsMenuX::MenuOpenedAsync() {
504   if (mPendingAsyncMenuOpenRunnable) {
505     mPendingAsyncMenuOpenRunnable->Cancel();
506     mPendingAsyncMenuOpenRunnable = nullptr;
507   }
509   mIsOpenForGecko = true;
511   // Open the node.
512   if (mContent->IsElement()) {
513     mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open,
514                                    u"true"_ns, true);
515   }
517   RefPtr<nsIContent> popupContent = GetMenuPopupContent();
519   // Notify our observer.
520   if (mObserver && popupContent) {
521     mObserver->OnMenuDidOpen(popupContent->AsElement());
522   }
524   // Fire popupshown.
525   nsEventStatus status = nsEventStatus_eIgnore;
526   WidgetMouseEvent event(true, eXULPopupShown, nullptr,
527                          WidgetMouseEvent::eReal);
528   RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
529   EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
532 void nsMenuX::MenuClosed() {
533   if (!mIsOpen) {
534     return;
535   }
537   // Make sure we fire any pending popupshown events first.
538   FlushMenuOpenedRunnable();
540   // If any of our submenus were opened programmatically, make sure they get
541   // closed first.
542   for (auto& child : mMenuChildren) {
543     if (child.is<RefPtr<nsMenuX>>()) {
544       child.as<RefPtr<nsMenuX>>()->MenuClosed();
545     }
546   }
548   mIsOpen = false;
550   // Do the rest of the MenuClosed work in MenuClosedAsync.
551   // MenuClosed() is called from -[NSMenuDelegate menuDidClose:]. If a menuitem
552   // was clicked, menuDidClose is called *before* menuItemHit for the clicked
553   // menu item is called. This runnable will be canceled if ~nsMenuX runs before
554   // the runnable. The runnable object must not hold a strong reference to the
555   // nsMenuX, so that there is no reference cycle.
556   class MenuClosedAsyncRunnable final : public mozilla::CancelableRunnable {
557    public:
558     explicit MenuClosedAsyncRunnable(nsMenuX* aMenu)
559         : CancelableRunnable("MenuClosedAsyncRunnable"), mMenu(aMenu) {}
561     // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
562     MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
563       if (RefPtr<nsMenuX> menu = mMenu) {
564         menu->MenuClosedAsync();
565         mMenu = nullptr;
566       }
567       return NS_OK;
568     }
569     nsresult Cancel() override {
570       mMenu = nullptr;
571       return NS_OK;
572     }
574    private:
575     nsMenuX* mMenu;  // weak, cleared by Cancel() and Run()
576   };
578   mPendingAsyncMenuCloseRunnable = new MenuClosedAsyncRunnable(this);
580   NS_DispatchToCurrentThread(mPendingAsyncMenuCloseRunnable);
583 void nsMenuX::FlushMenuClosedRunnable() {
584   // If any of our submenus have a pending menu closed runnable, make sure those
585   // run first.
586   for (auto& child : mMenuChildren) {
587     if (child.is<RefPtr<nsMenuX>>()) {
588       child.as<RefPtr<nsMenuX>>()->FlushMenuClosedRunnable();
589     }
590   }
592   if (mPendingAsyncMenuCloseRunnable) {
593     MenuClosedAsync();
594   }
597 void nsMenuX::MenuClosedAsync() {
598   if (mPendingAsyncMenuCloseRunnable) {
599     mPendingAsyncMenuCloseRunnable->Cancel();
600     mPendingAsyncMenuCloseRunnable = nullptr;
601   }
603   // If we have pending command events, run those first.
604   nsTArray<PendingCommandEvent> events = std::move(mPendingCommandEvents);
605   for (auto& event : events) {
606     event.mMenuItem->DoCommand(event.mModifiers, event.mButton);
607   }
609   // Make sure no item is highlighted.
610   OnHighlightedItemChanged(Nothing());
612   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
613   nsCOMPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
615   nsEventStatus status = nsEventStatus_eIgnore;
616   WidgetMouseEvent popupHiding(true, eXULPopupHiding, nullptr,
617                                WidgetMouseEvent::eReal);
618   EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHiding, nullptr,
619                             &status);
621   mIsOpenForGecko = false;
623   if (mContent->IsElement()) {
624     mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true);
625   }
627   WidgetMouseEvent popupHidden(true, eXULPopupHidden, nullptr,
628                                WidgetMouseEvent::eReal);
629   EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHidden, nullptr,
630                             &status);
632   // Notify our observer.
633   if (mObserver && popupContent) {
634     mObserver->OnMenuClosed(popupContent->AsElement());
635   }
638 void nsMenuX::ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem,
639                                        NSEventModifierFlags aModifiers,
640                                        int16_t aButton) {
641   if (mIsOpenForGecko) {
642     // Queue the event into mPendingCommandEvents. We will call aItem->DoCommand
643     // in MenuClosedAsync(). We rely on the assumption that MenuClosedAsync will
644     // run soon.
645     mPendingCommandEvents.AppendElement(
646         PendingCommandEvent{std::move(aItem), aModifiers, aButton});
647   } else {
648     // The menu item was activated outside of a regular open / activate / close
649     // sequence. This happens in multiple cases:
650     //  - When a menu item is activated by a keyboard shortcut while all windows
651     //  are closed
652     //    (otherwise those shortcuts go through Gecko's manual keyboard
653     //    handling)
654     //  - When a menu item in the Dock menu is clicked
655     //  - During native menu tests
656     //
657     // Run the command synchronously.
658     aItem->DoCommand(aModifiers, aButton);
659   }
662 bool nsMenuX::Close() {
663   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
665   if (mDidFirePopupshowingAndIsApprovedToOpen && !mIsOpen) {
666     // Close is being called right after this menu was opened, but before
667     // MenuOpened() had a chance to run. Call it here so that we can go through
668     // the entire popupshown -> popuphiding -> popuphidden sequence. Some
669     // callers expect to get a popuphidden event even if they close the popup
670     // before it was fully open.
671     MenuOpened();
672   }
674   FlushMenuOpenedRunnable();
676   bool wasOpen = mIsOpenForGecko;
678   if (mIsOpen) {
679     // Close the menu.
680     // We usually don't get here during normal Firefox usage: If the user closes
681     // the menu by clicking an item, or by clicking outside the menu, or by
682     // pressing escape, then the menu gets closed by macOS, and not by a call to
683     // nsMenuX::Close(). If we do get here, it's usually because we're running
684     // an automated test. Close the menu without the fade-out animation so that
685     // we don't unnecessarily slow down the automated tests.
686     [mNativeMenu cancelTrackingWithoutAnimation];
687     MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
689     // Handle closing synchronously.
690     MenuClosed();
691   }
693   FlushMenuClosedRunnable();
695   return wasOpen;
697   NS_OBJC_END_TRY_ABORT_BLOCK;
700 void nsMenuX::OnHighlightedItemChanged(
701     const Maybe<uint32_t>& aNewHighlightedIndex) {
702   if (mHighlightedItemIndex == aNewHighlightedIndex) {
703     return;
704   }
706   if (mHighlightedItemIndex) {
707     Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*mHighlightedItemIndex);
708     if (target && target->is<RefPtr<nsMenuItemX>>()) {
709       bool handlerCalledPreventDefault;  // but we don't actually care
710       target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(
711           u"DOMMenuItemInactive"_ns, &handlerCalledPreventDefault);
712     }
713   }
714   if (aNewHighlightedIndex) {
715     Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*aNewHighlightedIndex);
716     if (target && target->is<RefPtr<nsMenuItemX>>()) {
717       bool handlerCalledPreventDefault;  // but we don't actually care
718       target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(
719           u"DOMMenuItemActive"_ns, &handlerCalledPreventDefault);
720     }
721   }
722   mHighlightedItemIndex = aNewHighlightedIndex;
725 void nsMenuX::OnWillActivateItem(NSMenuItem* aItem) {
726   if (!mIsOpenForGecko) {
727     return;
728   }
730   if (mMenuGroupOwner && mObserver) {
731     nsMenuItemX* item =
732         mMenuGroupOwner->GetMenuItemForCommandID(uint32_t(aItem.tag));
733     if (item && item->Content()->IsElement()) {
734       RefPtr<dom::Element> itemElement = item->Content()->AsElement();
735       if (nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent()) {
736         mObserver->OnMenuWillActivateItem(popupContent->AsElement(),
737                                           itemElement);
738       }
739     }
740   }
743 // Flushes style.
744 static NSUserInterfaceLayoutDirection DirectionForElement(
745     dom::Element* aElement) {
746   // Get the direction from the computed style so that inheritance into submenus
747   // is respected. aElement may not have a frame.
748   RefPtr<const ComputedStyle> sc =
749       nsComputedDOMStyle::GetComputedStyle(aElement);
750   if (!sc) {
751     return NSApp.userInterfaceLayoutDirection;
752   }
754   switch (sc->StyleVisibility()->mDirection) {
755     case StyleDirection::Ltr:
756       return NSUserInterfaceLayoutDirectionLeftToRight;
757     case StyleDirection::Rtl:
758       return NSUserInterfaceLayoutDirectionRightToLeft;
759   }
762 void nsMenuX::RebuildMenu() {
763   MOZ_RELEASE_ASSERT(mNeedsRebuild);
764   gConstructingMenu = true;
766   // Retrieve our menupopup.
767   nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
768   if (!menuPopup) {
769     gConstructingMenu = false;
770     return;
771   }
773   if (menuPopup->IsElement()) {
774     mNativeMenu.userInterfaceLayoutDirection =
775         DirectionForElement(menuPopup->AsElement());
776   }
778   // Iterate over the kids
779   for (nsIContent* child = menuPopup->GetFirstChild(); child;
780        child = child->GetNextSibling()) {
781     if (Maybe<MenuChild> menuChild = CreateMenuChild(child)) {
782       AddMenuChild(std::move(*menuChild));
783     }
784   }  // for each menu item
786   InsertPlaceholderIfNeeded();
788   gConstructingMenu = false;
789   mNeedsRebuild = false;
792 void nsMenuX::InsertPlaceholderIfNeeded() {
793   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
795   if ([mNativeMenu numberOfItems] == 0) {
796     MOZ_RELEASE_ASSERT(mVisibleItemsCount == 0);
797     NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:@""
798                                                   action:nil
799                                            keyEquivalent:@""];
800     item.enabled = NO;
801     item.view =
802         [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 150, 1)] autorelease];
803     [mNativeMenu addItem:item];
804     [item release];
805   }
807   NS_OBJC_END_TRY_ABORT_BLOCK;
810 void nsMenuX::RemovePlaceholderIfPresent() {
811   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
813   if (mVisibleItemsCount == 0 && [mNativeMenu numberOfItems] == 1) {
814     // Remove the placeholder.
815     [mNativeMenu removeItemAtIndex:0];
816   }
818   NS_OBJC_END_TRY_ABORT_BLOCK;
821 void nsMenuX::SetRebuild(bool aNeedsRebuild) {
822   if (!gConstructingMenu) {
823     mNeedsRebuild = aNeedsRebuild;
824     if (mParent && mParent->AsMenuBar()) {
825       mParent->AsMenuBar()->SetNeedsRebuild();
826     }
827   }
830 nsresult nsMenuX::SetEnabled(bool aIsEnabled) {
831   if (aIsEnabled != mIsEnabled) {
832     // we always want to rebuild when this changes
833     mIsEnabled = aIsEnabled;
834     mNativeMenuItem.enabled = mIsEnabled;
835   }
836   return NS_OK;
839 nsresult nsMenuX::GetEnabled(bool* aIsEnabled) {
840   NS_ENSURE_ARG_POINTER(aIsEnabled);
841   *aIsEnabled = mIsEnabled;
842   return NS_OK;
845 GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& aMenuTitle,
846                                                 bool aShowServices) {
847   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
849   NSString* title = [NSString stringWithCharacters:(UniChar*)aMenuTitle.get()
850                                             length:aMenuTitle.Length()];
851   GeckoNSMenu* myMenu = [[GeckoNSMenu alloc] initWithTitle:title];
852   myMenu.delegate = mMenuDelegate;
854   // We don't want this menu to auto-enable menu items because then Cocoa
855   // overrides our decisions and things get incorrectly enabled/disabled.
856   myMenu.autoenablesItems = NO;
858   // Only show "Services", "Autofill" and similar entries provided by macOS
859   // if our caller wants them:
860   myMenu.allowsContextMenuPlugIns = aShowServices;
862   // we used to install Carbon event handlers here, but since NSMenu* doesn't
863   // create its underlying MenuRef until just before display, we delay until
864   // that happens. Now we install the event handlers when Cocoa notifies
865   // us that a menu is about to display - see the Cocoa MenuDelegate class.
867   return myMenu;
869   NS_OBJC_END_TRY_ABORT_BLOCK;
872 Maybe<nsMenuX::MenuChild> nsMenuX::CreateMenuChild(nsIContent* aContent) {
873   if (aContent->IsAnyOfXULElements(nsGkAtoms::menuitem,
874                                    nsGkAtoms::menuseparator)) {
875     return Some(MenuChild(CreateMenuItem(aContent)));
876   }
877   if (aContent->IsXULElement(nsGkAtoms::menu)) {
878     return Some(
879         MenuChild(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aContent)));
880   }
881   return {};
884 RefPtr<nsMenuItemX> nsMenuX::CreateMenuItem(nsIContent* aMenuItemContent) {
885   MOZ_RELEASE_ASSERT(aMenuItemContent);
887   nsAutoString menuitemName;
888   if (aMenuItemContent->IsElement()) {
889     aMenuItemContent->AsElement()->GetAttr(nsGkAtoms::label, menuitemName);
890   }
892   EMenuItemType itemType = eRegularMenuItemType;
893   if (aMenuItemContent->IsXULElement(nsGkAtoms::menuseparator)) {
894     itemType = eSeparatorMenuItemType;
895   } else if (aMenuItemContent->IsElement()) {
896     static Element::AttrValuesArray strings[] = {nsGkAtoms::checkbox,
897                                                  nsGkAtoms::radio, nullptr};
898     switch (aMenuItemContent->AsElement()->FindAttrValueIn(
899         kNameSpaceID_None, nsGkAtoms::type, strings, eCaseMatters)) {
900       case 0:
901         itemType = eCheckboxMenuItemType;
902         break;
903       case 1:
904         itemType = eRadioMenuItemType;
905         break;
906     }
907   }
909   return MakeRefPtr<nsMenuItemX>(this, menuitemName, itemType, mMenuGroupOwner,
910                                  aMenuItemContent);
913 // This menu is about to open. Returns false if the handler wants to stop the
914 // opening of the menu.
915 bool nsMenuX::OnOpen() {
916   if (mDidFirePopupshowingAndIsApprovedToOpen) {
917     return true;
918   }
920   if (mIsOpen) {
921     NS_WARNING("nsMenuX::OnOpen() called while the menu is already considered "
922                "to be open. This "
923                "seems odd.");
924   }
926   RefPtr<nsIContent> popupContent = GetMenuPopupContent();
928   if (mObserver && popupContent) {
929     mObserver->OnMenuWillOpen(popupContent->AsElement());
930   }
932   nsEventStatus status = nsEventStatus_eIgnore;
933   WidgetMouseEvent event(true, eXULPopupShowing, nullptr,
934                          WidgetMouseEvent::eReal);
936   nsresult rv = NS_OK;
937   RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
938   rv = EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
939   if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) {
940     return false;
941   }
943   DidFirePopupShowing();
945   return true;
948 void nsMenuX::DidFirePopupShowing() {
949   mDidFirePopupshowingAndIsApprovedToOpen = true;
951   // If the open is going to succeed we need to walk our menu items, checking to
952   // see if any of them have a command attribute. If so, several attributes
953   // must potentially be updated.
955   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
956   if (!popupContent) {
957     return;
958   }
960   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
961   if (pm) {
962     pm->UpdateMenuItems(popupContent->AsElement());
963   }
966 // Find the |menupopup| child in the |popup| representing this menu. It should
967 // be one of a very few children so we won't be iterating over a bazillion menu
968 // items to find it (so the strcmp won't kill us).
969 already_AddRefed<nsIContent> nsMenuX::GetMenuPopupContent() {
970   // Check to see if we are a "menupopup" node (if we are a native menu).
971   if (mContent->IsXULElement(nsGkAtoms::menupopup)) {
972     return do_AddRef(mContent);
973   }
975   // Otherwise check our child nodes.
977   for (RefPtr<nsIContent> child = mContent->GetFirstChild(); child;
978        child = child->GetNextSibling()) {
979     if (child->IsXULElement(nsGkAtoms::menupopup)) {
980       return child.forget();
981     }
982   }
984   return nullptr;
987 bool nsMenuX::IsXULHelpMenu(nsIContent* aMenuContent) {
988   bool retval = false;
989   if (aMenuContent && aMenuContent->IsElement()) {
990     nsAutoString id;
991     aMenuContent->AsElement()->GetAttr(nsGkAtoms::id, id);
992     if (id.Equals(u"helpMenu"_ns)) {
993       retval = true;
994     }
995   }
996   return retval;
999 bool nsMenuX::IsXULWindowMenu(nsIContent* aMenuContent) {
1000   bool retval = false;
1001   if (aMenuContent && aMenuContent->IsElement()) {
1002     nsAutoString id;
1003     aMenuContent->AsElement()->GetAttr(nsGkAtoms::id, id);
1004     if (id.Equals(u"windowMenu"_ns)) {
1005       retval = true;
1006     }
1007   }
1008   return retval;
1012 // nsChangeObserver
1015 void nsMenuX::ObserveAttributeChanged(dom::Document* aDocument,
1016                                       nsIContent* aContent,
1017                                       nsAtom* aAttribute) {
1018   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
1020   // ignore the |open| attribute, which is by far the most common
1021   if (gConstructingMenu || (aAttribute == nsGkAtoms::open)) {
1022     return;
1023   }
1025   if (aAttribute == nsGkAtoms::disabled) {
1026     SetEnabled(!mContent->AsElement()->AttrValueIs(
1027         kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
1028         eCaseMatters));
1029   } else if (aAttribute == nsGkAtoms::label) {
1030     mContent->AsElement()->GetAttr(nsGkAtoms::label, mLabel);
1031     NSString* newCocoaLabelString =
1032         nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
1033     mNativeMenu.title = newCocoaLabelString;
1034     mNativeMenuItem.title = newCocoaLabelString;
1035   } else if (aAttribute == nsGkAtoms::hidden ||
1036              aAttribute == nsGkAtoms::collapsed) {
1037     SetRebuild(true);
1039     bool newVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
1041     // don't do anything if the state is correct already
1042     if (newVisible == mVisible) {
1043       return;
1044     }
1046     mVisible = newVisible;
1047     if (mParent) {
1048       RefPtr<nsMenuX> self = this;
1049       mParent->MenuChildChangedVisibility(MenuChild(self), newVisible);
1050     }
1051     if (mVisible) {
1052       SetupIcon();
1053     }
1054   } else if (aAttribute == nsGkAtoms::image) {
1055     SetupIcon();
1056   }
1058   NS_OBJC_END_TRY_ABORT_BLOCK;
1061 void nsMenuX::ObserveContentRemoved(dom::Document* aDocument,
1062                                     nsIContent* aContainer, nsIContent* aChild,
1063                                     nsIContent* aPreviousSibling) {
1064   if (gConstructingMenu) {
1065     return;
1066   }
1068   SetRebuild(true);
1069   mMenuGroupOwner->UnregisterForContentChanges(aChild);
1071   if (!mIsOpen) {
1072     // We will update the menu contents the next time the menu is opened.
1073     return;
1074   }
1076   // The menu is currently open. Remove the child from mMenuChildren and from
1077   // our NSMenu.
1078   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
1079   if (popupContent && aContainer == popupContent && aChild->IsElement()) {
1080     if (Maybe<MenuChild> child = GetItemForElement(aChild->AsElement())) {
1081       RemoveMenuChild(*child);
1082     }
1083   }
1086 void nsMenuX::ObserveContentInserted(dom::Document* aDocument,
1087                                      nsIContent* aContainer,
1088                                      nsIContent* aChild) {
1089   if (gConstructingMenu) {
1090     return;
1091   }
1093   SetRebuild(true);
1095   if (!mIsOpen) {
1096     // We will update the menu contents the next time the menu is opened.
1097     return;
1098   }
1100   // The menu is currently open. Insert the child into mMenuChildren and into
1101   // our NSMenu.
1102   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
1103   if (popupContent && aContainer == popupContent) {
1104     if (Maybe<MenuChild> child = CreateMenuChild(aChild)) {
1105       InsertMenuChild(std::move(*child));
1106     }
1107   }
1110 void nsMenuX::SetupIcon() {
1111   mIcon->SetupIcon(mContent);
1112   mNativeMenuItem.image = mIcon->GetIconImage();
1115 void nsMenuX::IconUpdated() {
1116   mNativeMenuItem.image = mIcon->GetIconImage();
1117   if (mIconListener) {
1118     mIconListener->IconUpdated();
1119   }
1122 void nsMenuX::MenuChildChangedVisibility(const MenuChild& aChild,
1123                                          bool aIsVisible) {
1124   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
1126   NSMenuItem* nativeItem = aChild.match(
1127       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
1128       [](const RefPtr<nsMenuItemX>& aMenuItem) {
1129         return aMenuItem->NativeNSMenuItem();
1130       });
1131   if (aIsVisible) {
1132     MOZ_RELEASE_ASSERT(
1133         !nativeItem.menu,
1134         "The native item should not be in a menu while it is hidden");
1135     RemovePlaceholderIfPresent();
1136     NSInteger insertionPoint = CalculateNativeInsertionPoint(aChild);
1137     [mNativeMenu insertItem:nativeItem atIndex:insertionPoint];
1138     mVisibleItemsCount++;
1139   } else {
1140     MOZ_RELEASE_ASSERT(
1141         [mNativeMenu indexOfItem:nativeItem] != -1,
1142         "The native item should be in this menu while it is visible");
1143     [mNativeMenu removeItem:nativeItem];
1144     mVisibleItemsCount--;
1145     InsertPlaceholderIfNeeded();
1146   }
1148   NS_OBJC_END_TRY_ABORT_BLOCK;
1151 NSInteger nsMenuX::CalculateNativeInsertionPoint(const MenuChild& aChild) {
1152   NSInteger insertionPoint = 0;
1153   for (auto& currItem : mMenuChildren) {
1154     // Using GetItemAt instead of GetVisibleItemAt to avoid O(N^2)
1155     if (currItem == aChild) {
1156       return insertionPoint;
1157     }
1158     NSMenuItem* nativeItem = currItem.match(
1159         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
1160         [](const RefPtr<nsMenuItemX>& aMenuItem) {
1161           return aMenuItem->NativeNSMenuItem();
1162         });
1163     // Only count visible items.
1164     if (nativeItem.menu) {
1165       insertionPoint++;
1166     }
1167   }
1168   return insertionPoint;
1171 void nsMenuX::Dump(uint32_t aIndent) const {
1172   printf(
1173       "%*s - menu [%p] %-16s <%s>", aIndent * 2, "", this,
1174       mLabel.IsEmpty() ? "(empty label)" : NS_ConvertUTF16toUTF8(mLabel).get(),
1175       NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
1176   if (mNeedsRebuild) {
1177     printf(" [NeedsRebuild]");
1178   }
1179   if (mIsOpen) {
1180     printf(" [Open]");
1181   }
1182   if (mVisible) {
1183     printf(" [Visible]");
1184   }
1185   if (mIsEnabled) {
1186     printf(" [IsEnabled]");
1187   }
1188   printf(" (%d visible items)", int(mVisibleItemsCount));
1189   printf("\n");
1190   for (const auto& subitem : mMenuChildren) {
1191     subitem.match(
1192         [=](const RefPtr<nsMenuX>& aMenu) { aMenu->Dump(aIndent + 1); },
1193         [=](const RefPtr<nsMenuItemX>& aMenuItem) {
1194           aMenuItem->Dump(aIndent + 1);
1195         });
1196   }
1200 // MenuDelegate Objective-C class, used to set up Carbon events
1203 @implementation MenuDelegate
1205 - (id)initWithGeckoMenu:(nsMenuX*)geckoMenu {
1206   if ((self = [super init])) {
1207     NS_ASSERTION(geckoMenu, "Cannot initialize native menu delegate with NULL "
1208                             "gecko menu! Will crash!");
1209     mGeckoMenu = geckoMenu;
1210     mBlocksToRunWhenOpen = [[NSMutableArray alloc] init];
1211   }
1212   return self;
1215 - (void)dealloc {
1216   [mBlocksToRunWhenOpen release];
1217   [super dealloc];
1220 - (void)runBlockWhenOpen:(void (^)())block {
1221   [mBlocksToRunWhenOpen addObject:[[block copy] autorelease]];
1224 - (void)menu:(NSMenu*)aMenu willHighlightItem:(NSMenuItem*)aItem {
1225   if (!aMenu || !mGeckoMenu) {
1226     return;
1227   }
1229   Maybe<uint32_t> index =
1230       aItem ? Some(static_cast<uint32_t>([aMenu indexOfItem:aItem]))
1231             : Nothing();
1232   mGeckoMenu->OnHighlightedItemChanged(index);
1235 - (void)menuWillOpen:(NSMenu*)menu {
1236   for (void (^block)() in mBlocksToRunWhenOpen) {
1237     block();
1238   }
1239   [mBlocksToRunWhenOpen removeAllObjects];
1241   if (!mGeckoMenu) {
1242     return;
1243   }
1245   // Don't do anything while the OS is (re)indexing our menus (on Leopard and
1246   // higher).  This stops the Help menu from being able to search in our
1247   // menus, but it also resolves many other problems.
1248   if (nsMenuX::sIndexingMenuLevel > 0) {
1249     return;
1250   }
1252   // Hold a strong reference to mGeckoMenu while calling its methods.
1253   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1254   geckoMenu->MenuOpened();
1257 - (void)menuDidClose:(NSMenu*)menu {
1258   if (!mGeckoMenu) {
1259     return;
1260   }
1262   // Don't do anything while the OS is (re)indexing our menus (on Leopard and
1263   // higher).  This stops the Help menu from being able to search in our
1264   // menus, but it also resolves many other problems.
1265   if (nsMenuX::sIndexingMenuLevel > 0) {
1266     return;
1267   }
1269   // Hold a strong reference to mGeckoMenu while calling its methods.
1270   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1271   geckoMenu->MenuClosed();
1274 // This is called after menuDidClose:.
1275 - (void)menu:(NSMenu*)aMenu willActivateItem:(NSMenuItem*)aItem {
1276   if (!mGeckoMenu) {
1277     return;
1278   }
1280   // Hold a strong reference to mGeckoMenu while calling its methods.
1281   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1282   geckoMenu->OnWillActivateItem(aItem);
1285 @end
1287 // OS X Leopard (at least as of 10.5.2) has an obscure bug triggered by some
1288 // behavior that's present in Mozilla.org browsers but not (as best I can
1289 // tell) in Apple products like Safari.  (It's not yet clear exactly what this
1290 // behavior is.)
1292 // The bug is that sometimes you crash on quit in nsMenuX::RemoveAll(), on a
1293 // call to [NSMenu removeItemAtIndex:].  The crash is caused by trying to
1294 // access a deleted NSMenuItem object (sometimes (perhaps always?) by trying
1295 // to send it a _setChangedFlags: message).  Though this object was deleted
1296 // some time ago, it remains registered as a potential target for a particular
1297 // key equivalent.  So when [NSMenu removeItemAtIndex:] removes the current
1298 // target for that same key equivalent, the OS tries to "activate" the
1299 // previous target.
1301 // The underlying reason appears to be that NSMenu's _addItem:toTable: and
1302 // _removeItem:fromTable: methods (which are used to keep a hashtable of
1303 // registered key equivalents) don't properly "retain" and "release"
1304 // NSMenuItem objects as they are added to and removed from the hashtable.
1306 // Our (hackish) workaround is to shadow the OS's hashtable with another
1307 // hastable of our own (gShadowKeyEquivDB), and use it to "retain" and
1308 // "release" NSMenuItem objects as needed.  This resolves bmo bugs 422287 and
1309 // 423669.  When (if) Apple fixes this bug, we can remove this workaround.
1311 static NSMutableDictionary* gShadowKeyEquivDB = nil;
1313 // Class for values in gShadowKeyEquivDB.
1315 @interface KeyEquivDBItem : NSObject {
1316   NSMenuItem* mItem;
1317   NSMutableSet* mTables;
1320 - (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable;
1321 - (BOOL)hasTable:(NSMapTable*)aTable;
1322 - (int)addTable:(NSMapTable*)aTable;
1323 - (int)removeTable:(NSMapTable*)aTable;
1325 @end
1327 @implementation KeyEquivDBItem
1329 - (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable {
1330   if (!gShadowKeyEquivDB) {
1331     gShadowKeyEquivDB = [[NSMutableDictionary alloc] init];
1332   }
1333   self = [super init];
1334   if (aItem && aTable) {
1335     mTables = [[NSMutableSet alloc] init];
1336     mItem = [aItem retain];
1337     [mTables addObject:[NSValue valueWithPointer:aTable]];
1338   } else {
1339     mTables = nil;
1340     mItem = nil;
1341   }
1342   return self;
1345 - (void)dealloc {
1346   if (mTables) {
1347     [mTables release];
1348   }
1349   if (mItem) {
1350     [mItem release];
1351   }
1352   [super dealloc];
1355 - (BOOL)hasTable:(NSMapTable*)aTable {
1356   return [mTables member:[NSValue valueWithPointer:aTable]] ? YES : NO;
1359 // Does nothing if aTable (its index value) is already present in mTables.
1360 - (int)addTable:(NSMapTable*)aTable {
1361   if (aTable) {
1362     [mTables addObject:[NSValue valueWithPointer:aTable]];
1363   }
1364   return [mTables count];
1367 - (int)removeTable:(NSMapTable*)aTable {
1368   if (aTable) {
1369     NSValue* objectToRemove =
1370         [mTables member:[NSValue valueWithPointer:aTable]];
1371     if (objectToRemove) {
1372       [mTables removeObject:objectToRemove];
1373     }
1374   }
1375   return [mTables count];
1378 @end
1380 @interface NSMenu (MethodSwizzling)
1381 + (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable;
1382 + (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem
1383                         fromTable:(NSMapTable*)aTable;
1384 @end
1386 @implementation NSMenu (MethodSwizzling)
1388 + (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable {
1389   if (aItem && aTable) {
1390     NSValue* key = [NSValue valueWithPointer:aItem];
1391     KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
1392     if (shadowItem) {
1393       [shadowItem addTable:aTable];
1394     } else {
1395       shadowItem = [[KeyEquivDBItem alloc] initWithItem:aItem table:aTable];
1396       [gShadowKeyEquivDB setObject:shadowItem forKey:key];
1397       // Release after [NSMutableDictionary setObject:forKey:] retains it (so
1398       // that it will get dealloced when removeObjectForKey: is called).
1399       [shadowItem release];
1400     }
1401   }
1403   [self nsMenuX_NSMenu_addItem:aItem toTable:aTable];
1406 + (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem
1407                         fromTable:(NSMapTable*)aTable {
1408   [self nsMenuX_NSMenu_removeItem:aItem fromTable:aTable];
1410   if (aItem && aTable) {
1411     NSValue* key = [NSValue valueWithPointer:aItem];
1412     KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
1413     if (shadowItem && [shadowItem hasTable:aTable]) {
1414       if (![shadowItem removeTable:aTable]) {
1415         [gShadowKeyEquivDB removeObjectForKey:key];
1416       }
1417     }
1418   }
1421 @end
1423 // This class is needed to keep track of when the OS is (re)indexing all of
1424 // our menus.  This appears to only happen on Leopard and higher, and can
1425 // be triggered by opening the Help menu.  Some operations are unsafe while
1426 // this is happening -- notably the calls to [[NSImage alloc]
1427 // initWithSize:imageRect.size] and [newImage lockFocus] in nsMenuItemIconX::
1428 // OnStopFrame().  But we don't yet have a complete list, and Apple doesn't
1429 // yet have any documentation on this subject.  (Apple also doesn't yet have
1430 // any documented way to find the information we seek here.)  The "original"
1431 // of this class (the one whose indexMenuBarDynamically method we hook) is
1432 // defined in the Shortcut framework in /System/Library/PrivateFrameworks.
1433 @interface NSObject (SCTGRLIndexMethodSwizzling)
1434 - (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically;
1435 @end
1437 @implementation NSObject (SCTGRLIndexMethodSwizzling)
1439 - (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically {
1440   // This method appears to be called (once) whenever the OS (re)indexes our
1441   // menus.  sIndexingMenuLevel is a int32_t just in case it might be
1442   // reentered.  As it's running, it spawns calls to two undocumented
1443   // HIToolbox methods (_SimulateMenuOpening() and _SimulateMenuClosed()),
1444   // which "simulate" the opening and closing of our menus without actually
1445   // displaying them.
1446   ++nsMenuX::sIndexingMenuLevel;
1447   [self nsMenuX_SCTGRLIndex_indexMenuBarDynamically];
1448   --nsMenuX::sIndexingMenuLevel;
1451 @end
1453 @interface NSObject (NSServicesMenuUpdaterSwizzling)
1454 - (void)nsMenuX_populateMenu:(NSMenu*)aMenu
1455           withServiceEntries:(NSArray*)aServices
1456                   forDisplay:(BOOL)aForDisplay;
1457 @end
1459 @interface _NSServiceEntry : NSObject
1460 - (NSString*)bundleIdentifier;
1461 @end
1463 @implementation NSObject (NSServicesMenuUpdaterSwizzling)
1465 - (void)nsMenuX_populateMenu:(NSMenu*)aMenu
1466           withServiceEntries:(NSArray*)aServices
1467                   forDisplay:(BOOL)aForDisplay {
1468   NSMutableArray* filteredServices = [NSMutableArray array];
1470   // We need to filter some services, such as "Search with Google", since this
1471   // service is duplicating functionality already exposed by our "Search Google
1472   // for..." context menu entry and because it opens in Safari, which can cause
1473   // confusion for users.
1474   for (_NSServiceEntry* service in aServices) {
1475     NSString* bundleId = [service bundleIdentifier];
1476     NSString* msg = [service valueForKey:@"message"];
1477     bool shouldSkip = ([bundleId isEqualToString:@"com.apple.Safari"]) ||
1478                       ([bundleId isEqualToString:@"com.apple.systemuiserver"] &&
1479                        [msg isEqualToString:@"openURL"]);
1480     if (!shouldSkip) {
1481       [filteredServices addObject:service];
1482     }
1483   }
1485   [self nsMenuX_populateMenu:aMenu
1486           withServiceEntries:filteredServices
1487                   forDisplay:aForDisplay];
1490 @end