Bug 1835241 - Part 6: Use the same parameter names at definition and declaration...
[gecko.git] / widget / cocoa / nsMenuX.mm
blob97d9342054348758591ca451f958af972b73306f
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:), true);
61   // On SnowLeopard the Shortcut framework (which contains the
62   // SCTGRLIndex class) is loaded on demand, whenever the user first opens
63   // a menu (which normally hasn't happened yet).  So we need to load it
64   // here explicitly.
65   dlopen("/System/Library/PrivateFrameworks/Shortcut.framework/Shortcut", RTLD_LAZY);
66   Class SCTGRLIndexClass = ::NSClassFromString(@"SCTGRLIndex");
67   nsToolkit::SwizzleMethods(SCTGRLIndexClass, @selector(indexMenuBarDynamically),
68                             @selector(nsMenuX_SCTGRLIndex_indexMenuBarDynamically));
70   Class NSServicesMenuUpdaterClass = ::NSClassFromString(@"_NSServicesMenuUpdater");
71   nsToolkit::SwizzleMethods(NSServicesMenuUpdaterClass,
72                             @selector(populateMenu:withServiceEntries:forDisplay:),
73                             @selector(nsMenuX_populateMenu:withServiceEntries:forDisplay:));
75   gMenuMethodsSwizzled = true;
79 // nsMenuX
82 nsMenuX::nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aContent)
83     : mContent(aContent), mParent(aParent), mMenuGroupOwner(aMenuGroupOwner) {
84   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
86   MOZ_COUNT_CTOR(nsMenuX);
88   SwizzleDynamicIndexingMethods();
90   mMenuDelegate = [[MenuDelegate alloc] initWithGeckoMenu:this];
91   mMenuDelegate.menuIsInMenubar = mMenuGroupOwner->GetMenuBar() != nullptr;
93   if (!nsMenuBarX::sNativeEventTarget) {
94     nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
95   }
97   if (mContent->IsElement()) {
98     mContent->AsElement()->GetAttr(nsGkAtoms::label, mLabel);
99   }
100   mNativeMenu = CreateMenuWithGeckoString(mLabel);
102   // register this menu to be notified when changes are made to our content object
103   NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one");
104   mMenuGroupOwner->RegisterForContentChanges(mContent, this);
106   mVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
108   NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
109   mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString
110                                                action:nil
111                                         keyEquivalent:@""];
112   mNativeMenuItem.submenu = mNativeMenu;
114   SetEnabled(!mContent->IsElement() ||
115              !mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
116                                                  nsGkAtoms::_true, eCaseMatters));
118   // We call RebuildMenu here because keyboard commands are dependent upon
119   // native menu items being created. If we only call RebuildMenu when a menu
120   // is actually selected, then we can't access keyboard commands until the
121   // menu gets selected, which is bad.
122   RebuildMenu();
124   if (IsXULWindowMenu(mContent)) {
125     // Let the OS know that this is our Window menu.
126     NSApp.windowsMenu = mNativeMenu;
127   }
129   mIcon = MakeUnique<nsMenuItemIconX>(this);
131   if (mVisible) {
132     SetupIcon();
133   }
135   NS_OBJC_END_TRY_ABORT_BLOCK;
138 nsMenuX::~nsMenuX() {
139   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
141   // Make sure a pending popupshown event isn't dropped.
142   FlushMenuOpenedRunnable();
144   if (mIsOpen) {
145     [mNativeMenu cancelTracking];
146     MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
147   }
149   // Make sure pending popuphiding/popuphidden events aren't dropped.
150   FlushMenuClosedRunnable();
152   OnHighlightedItemChanged(Nothing());
153   RemoveAll();
155   mNativeMenu.delegate = nil;
156   [mNativeMenu release];
157   [mMenuDelegate release];
158   // autorelease the native menu item so that anything else happening to this
159   // object happens before the native menu item actually dies
160   [mNativeMenuItem autorelease];
162   DetachFromGroupOwnerRecursive();
164   MOZ_COUNT_DTOR(nsMenuX);
166   NS_OBJC_END_TRY_ABORT_BLOCK;
169 void nsMenuX::DetachFromGroupOwnerRecursive() {
170   if (!mMenuGroupOwner) {
171     // Don't recurse if this subtree is already detached.
172     // This avoids repeated recursion during the destruction of nested nsMenuX structures.
173     // Our invariant is: If we are detached, all of our contents are also detached.
174     return;
175   }
177   if (mMenuGroupOwner && mContent) {
178     mMenuGroupOwner->UnregisterForContentChanges(mContent);
179   }
180   mMenuGroupOwner = nullptr;
182   // Also detach all our children.
183   for (auto& child : mMenuChildren) {
184     child.match([](const RefPtr<nsMenuX>& aMenu) { aMenu->DetachFromGroupOwnerRecursive(); },
185                 [](const RefPtr<nsMenuItemX>& aMenuItem) { aMenuItem->DetachFromGroupOwner(); });
186   }
189 void nsMenuX::OnMenuWillOpen(dom::Element* aPopupElement) {
190   RefPtr<nsMenuX> kungFuDeathGrip(this);
191   if (mObserver) {
192     mObserver->OnMenuWillOpen(aPopupElement);
193   }
196 void nsMenuX::OnMenuDidOpen(dom::Element* aPopupElement) {
197   RefPtr<nsMenuX> kungFuDeathGrip(this);
198   if (mObserver) {
199     mObserver->OnMenuDidOpen(aPopupElement);
200   }
203 void nsMenuX::OnMenuWillActivateItem(dom::Element* aPopupElement, dom::Element* aMenuItemElement) {
204   RefPtr<nsMenuX> kungFuDeathGrip(this);
205   if (mObserver) {
206     mObserver->OnMenuWillActivateItem(aPopupElement, aMenuItemElement);
207   }
210 void nsMenuX::OnMenuClosed(dom::Element* aPopupElement) {
211   RefPtr<nsMenuX> kungFuDeathGrip(this);
212   if (mObserver) {
213     mObserver->OnMenuClosed(aPopupElement);
214   }
217 void nsMenuX::AddMenuChild(MenuChild&& aChild) {
218   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
220   WillInsertChild(aChild);
221   mMenuChildren.AppendElement(aChild);
223   bool isVisible =
224       aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
225                    [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); });
226   NSMenuItem* nativeItem = aChild.match(
227       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
228       [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); });
230   if (isVisible) {
231     RemovePlaceholderIfPresent();
232     [mNativeMenu addItem:nativeItem];
233     ++mVisibleItemsCount;
234   }
236   NS_OBJC_END_TRY_ABORT_BLOCK;
239 void nsMenuX::InsertMenuChild(MenuChild&& aChild) {
240   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
242   WillInsertChild(aChild);
243   size_t insertionIndex = FindInsertionIndex(aChild);
244   mMenuChildren.InsertElementAt(insertionIndex, aChild);
246   bool isVisible =
247       aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
248                    [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); });
249   if (isVisible) {
250     MenuChildChangedVisibility(aChild, true);
251   }
253   NS_OBJC_END_TRY_ABORT_BLOCK;
256 void nsMenuX::RemoveMenuChild(const MenuChild& aChild) {
257   bool isVisible =
258       aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
259                    [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); });
260   if (isVisible) {
261     MenuChildChangedVisibility(aChild, false);
262   }
264   WillRemoveChild(aChild);
265   mMenuChildren.RemoveElement(aChild);
268 size_t nsMenuX::FindInsertionIndex(const MenuChild& aChild) {
269   nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
270   MOZ_RELEASE_ASSERT(menuPopup);
272   RefPtr<nsIContent> insertedContent =
273       aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
274                    [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
276   MOZ_RELEASE_ASSERT(insertedContent->GetParent() == menuPopup);
278   // Iterate over menuPopup's children (insertedContent's siblings) until we encounter
279   // insertedContent. At the same time, keep track of the index in mMenuChildren.
280   size_t index = 0;
281   for (nsIContent* child = menuPopup->GetFirstChild(); child && index < mMenuChildren.Length();
282        child = child->GetNextSibling()) {
283     if (child == insertedContent) {
284       break;
285     }
287     RefPtr<nsIContent> contentAtIndex = mMenuChildren[index].match(
288         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
289         [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
290     if (child == contentAtIndex) {
291       index++;
292     }
293   }
295   return index;
298 // Includes all items, including hidden/collapsed ones
299 uint32_t nsMenuX::GetItemCount() { return mMenuChildren.Length(); }
301 // Includes all items, including hidden/collapsed ones
302 mozilla::Maybe<nsMenuX::MenuChild> nsMenuX::GetItemAt(uint32_t aPos) {
303   if (aPos >= (uint32_t)mMenuChildren.Length()) {
304     return {};
305   }
307   return Some(mMenuChildren[aPos]);
310 // Only includes visible items
311 nsresult nsMenuX::GetVisibleItemCount(uint32_t& aCount) {
312   aCount = mVisibleItemsCount;
313   return NS_OK;
316 // Only includes visible items. Note that this is provides O(N) access
317 // If you need to iterate or search, consider using GetItemAt and doing your own filtering
318 Maybe<nsMenuX::MenuChild> nsMenuX::GetVisibleItemAt(uint32_t aPos) {
319   uint32_t count = mMenuChildren.Length();
320   if (aPos >= mVisibleItemsCount || aPos >= count) {
321     return {};
322   }
324   // If there are no invisible items, can provide direct access
325   if (mVisibleItemsCount == count) {
326     return GetItemAt(aPos);
327   }
329   // Otherwise, traverse the array until we find the the item we're looking for.
330   uint32_t visibleNodeIndex = 0;
331   for (uint32_t i = 0; i < count; i++) {
332     MenuChild item = *GetItemAt(i);
333     RefPtr<nsIContent> content =
334         item.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
335                    [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
336     if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
337       if (aPos == visibleNodeIndex) {
338         // we found the visible node we're looking for, return it
339         return Some(item);
340       }
341       visibleNodeIndex++;
342     }
343   }
345   return {};
348 Maybe<nsMenuX::MenuChild> nsMenuX::GetItemForElement(Element* aMenuChildElement) {
349   for (auto& child : mMenuChildren) {
350     RefPtr<nsIContent> content =
351         child.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
352                     [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
353     if (content == aMenuChildElement) {
354       return Some(child);
355     }
356   }
357   return {};
360 nsresult nsMenuX::RemoveAll() {
361   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
363   [mNativeMenu removeAllItems];
365   for (auto& child : mMenuChildren) {
366     WillRemoveChild(child);
367   }
369   mMenuChildren.Clear();
370   mVisibleItemsCount = 0;
372   return NS_OK;
374   NS_OBJC_END_TRY_ABORT_BLOCK;
377 void nsMenuX::WillInsertChild(const MenuChild& aChild) {
378   if (aChild.is<RefPtr<nsMenuX>>()) {
379     aChild.as<RefPtr<nsMenuX>>()->SetObserver(this);
380   }
383 void nsMenuX::WillRemoveChild(const MenuChild& aChild) {
384   aChild.match(
385       [](const RefPtr<nsMenuX>& aMenu) {
386         aMenu->DetachFromGroupOwnerRecursive();
387         aMenu->DetachFromParent();
388         aMenu->SetObserver(nullptr);
389       },
390       [](const RefPtr<nsMenuItemX>& aMenuItem) {
391         aMenuItem->DetachFromGroupOwner();
392         aMenuItem->DetachFromParent();
393       });
396 void nsMenuX::MenuOpened() {
397   if (mIsOpen) {
398     return;
399   }
401   // Make sure we fire any pending popupshown / popuphiding / popuphidden events first.
402   FlushMenuOpenedRunnable();
403   FlushMenuClosedRunnable();
405   if (!mDidFirePopupshowingAndIsApprovedToOpen) {
406     // Fire popupshowing now.
407     bool approvedToOpen = OnOpen();
408     if (!approvedToOpen) {
409       // We can only stop menus from opening which we open ourselves. We cannot stop menubar root
410       // menus or menu submenus from opening.
411       // For context menus, we can call OnOpen() before we ask the system to open the menu.
412       NS_WARNING("The popupshowing event had preventDefault() called on it, but in MenuOpened() it "
413                  "is too late to stop the menu from opening.");
414     }
415   }
417   mIsOpen = true;
419   // Reset mDidFirePopupshowingAndIsApprovedToOpen for the next menu opening.
420   mDidFirePopupshowingAndIsApprovedToOpen = false;
422   if (mNeedsRebuild) {
423     OnHighlightedItemChanged(Nothing());
424     RemoveAll();
425     RebuildMenu();
426   }
428   // Fire the popupshown event in MenuOpenedAsync.
429   // MenuOpened() is called during menuWillOpen, and if cancelTracking is called now, menuDidClose
430   // will not be called.
431   // The runnable object must not hold a strong reference to the nsMenuX, so that there is no
432   // reference cycle.
433   class MenuOpenedAsyncRunnable final : public mozilla::CancelableRunnable {
434    public:
435     explicit MenuOpenedAsyncRunnable(nsMenuX* aMenu)
436         : CancelableRunnable("MenuOpenedAsyncRunnable"), mMenu(aMenu) {}
438     // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
439     MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
440       if (RefPtr<nsMenuX> menu = mMenu) {
441         menu->MenuOpenedAsync();
442         mMenu = nullptr;
443       }
444       return NS_OK;
445     }
446     nsresult Cancel() override {
447       mMenu = nullptr;
448       return NS_OK;
449     }
451    private:
452     nsMenuX* mMenu;  // weak, cleared by Cancel() and Run()
453   };
454   mPendingAsyncMenuOpenRunnable = new MenuOpenedAsyncRunnable(this);
455   NS_DispatchToCurrentThread(mPendingAsyncMenuOpenRunnable);
458 void nsMenuX::FlushMenuOpenedRunnable() {
459   if (mPendingAsyncMenuOpenRunnable) {
460     MenuOpenedAsync();
461   }
464 void nsMenuX::MenuOpenedAsync() {
465   if (mPendingAsyncMenuOpenRunnable) {
466     mPendingAsyncMenuOpenRunnable->Cancel();
467     mPendingAsyncMenuOpenRunnable = nullptr;
468   }
470   mIsOpenForGecko = true;
472   // Open the node.
473   if (mContent->IsElement()) {
474     mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, true);
475   }
477   RefPtr<nsIContent> popupContent = GetMenuPopupContent();
479   // Notify our observer.
480   if (mObserver && popupContent) {
481     mObserver->OnMenuDidOpen(popupContent->AsElement());
482   }
484   // Fire popupshown.
485   nsEventStatus status = nsEventStatus_eIgnore;
486   WidgetMouseEvent event(true, eXULPopupShown, nullptr, WidgetMouseEvent::eReal);
487   RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
488   EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
491 void nsMenuX::MenuClosed() {
492   if (!mIsOpen) {
493     return;
494   }
496   // Make sure we fire any pending popupshown events first.
497   FlushMenuOpenedRunnable();
499   // If any of our submenus were opened programmatically, make sure they get closed first.
500   for (auto& child : mMenuChildren) {
501     if (child.is<RefPtr<nsMenuX>>()) {
502       child.as<RefPtr<nsMenuX>>()->MenuClosed();
503     }
504   }
506   mIsOpen = false;
508   // Do the rest of the MenuClosed work in MenuClosedAsync.
509   // MenuClosed() is called from -[NSMenuDelegate menuDidClose:]. If a menuitem was clicked,
510   // menuDidClose is called *before* menuItemHit for the clicked menu item is called.
511   // This runnable will be canceled if ~nsMenuX runs before the runnable.
512   // The runnable object must not hold a strong reference to the nsMenuX, so that there is no
513   // reference cycle.
514   class MenuClosedAsyncRunnable final : public mozilla::CancelableRunnable {
515    public:
516     explicit MenuClosedAsyncRunnable(nsMenuX* aMenu)
517         : CancelableRunnable("MenuClosedAsyncRunnable"), mMenu(aMenu) {}
519     // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
520     MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
521       if (RefPtr<nsMenuX> menu = mMenu) {
522         menu->MenuClosedAsync();
523         mMenu = nullptr;
524       }
525       return NS_OK;
526     }
527     nsresult Cancel() override {
528       mMenu = nullptr;
529       return NS_OK;
530     }
532    private:
533     nsMenuX* mMenu;  // weak, cleared by Cancel() and Run()
534   };
536   mPendingAsyncMenuCloseRunnable = new MenuClosedAsyncRunnable(this);
538   NS_DispatchToCurrentThread(mPendingAsyncMenuCloseRunnable);
541 void nsMenuX::FlushMenuClosedRunnable() {
542   // If any of our submenus have a pending menu closed runnable, make sure those run first.
543   for (auto& child : mMenuChildren) {
544     if (child.is<RefPtr<nsMenuX>>()) {
545       child.as<RefPtr<nsMenuX>>()->FlushMenuClosedRunnable();
546     }
547   }
549   if (mPendingAsyncMenuCloseRunnable) {
550     MenuClosedAsync();
551   }
554 void nsMenuX::MenuClosedAsync() {
555   if (mPendingAsyncMenuCloseRunnable) {
556     mPendingAsyncMenuCloseRunnable->Cancel();
557     mPendingAsyncMenuCloseRunnable = nullptr;
558   }
560   // If we have pending command events, run those first.
561   nsTArray<PendingCommandEvent> events = std::move(mPendingCommandEvents);
562   for (auto& event : events) {
563     event.mMenuItem->DoCommand(event.mModifiers, event.mButton);
564   }
566   // Make sure no item is highlighted.
567   OnHighlightedItemChanged(Nothing());
569   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
570   nsCOMPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
572   nsEventStatus status = nsEventStatus_eIgnore;
573   WidgetMouseEvent popupHiding(true, eXULPopupHiding, nullptr, WidgetMouseEvent::eReal);
574   EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHiding, nullptr, &status);
576   mIsOpenForGecko = false;
578   if (mContent->IsElement()) {
579     mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true);
580   }
582   WidgetMouseEvent popupHidden(true, eXULPopupHidden, nullptr, WidgetMouseEvent::eReal);
583   EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHidden, nullptr, &status);
585   // Notify our observer.
586   if (mObserver && popupContent) {
587     mObserver->OnMenuClosed(popupContent->AsElement());
588   }
591 void nsMenuX::ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem, NSEventModifierFlags aModifiers,
592                                        int16_t aButton) {
593   if (mIsOpenForGecko) {
594     // Queue the event into mPendingCommandEvents. We will call aItem->DoCommand in
595     // MenuClosedAsync(). We rely on the assumption that MenuClosedAsync will run soon.
596     mPendingCommandEvents.AppendElement(PendingCommandEvent{std::move(aItem), aModifiers, aButton});
597   } else {
598     // The menu item was activated outside of a regular open / activate / close sequence.
599     // This happens in multiple cases:
600     //  - When a menu item is activated by a keyboard shortcut while all windows are closed
601     //    (otherwise those shortcuts go through Gecko's manual keyboard handling)
602     //  - When a menu item in the Dock menu is clicked
603     //  - During native menu tests
604     //
605     // Run the command synchronously.
606     aItem->DoCommand(aModifiers, aButton);
607   }
610 bool nsMenuX::Close() {
611   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
613   if (mDidFirePopupshowingAndIsApprovedToOpen && !mIsOpen) {
614     // Close is being called right after this menu was opened, but before MenuOpened() had a chance
615     // to run. Call it here so that we can go through the entire popupshown -> popuphiding ->
616     // popuphidden sequence. Some callers expect to get a popuphidden event even if they close the
617     // popup before it was fully open.
618     MenuOpened();
619   }
621   FlushMenuOpenedRunnable();
623   bool wasOpen = mIsOpenForGecko;
625   if (mIsOpen) {
626     // Close the menu.
627     // We usually don't get here during normal Firefox usage: If the user closes the menu by
628     // clicking an item, or by clicking outside the menu, or by pressing escape, then the menu gets
629     // closed by macOS, and not by a call to nsMenuX::Close().
630     // If we do get here, it's usually because we're running an automated test. Close the menu
631     // without the fade-out animation so that we don't unnecessarily slow down the automated tests.
632     [mNativeMenu cancelTrackingWithoutAnimation];
633     MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
635     // Handle closing synchronously.
636     MenuClosed();
637   }
639   FlushMenuClosedRunnable();
641   return wasOpen;
643   NS_OBJC_END_TRY_ABORT_BLOCK;
646 void nsMenuX::OnHighlightedItemChanged(const Maybe<uint32_t>& aNewHighlightedIndex) {
647   if (mHighlightedItemIndex == aNewHighlightedIndex) {
648     return;
649   }
651   if (mHighlightedItemIndex) {
652     Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*mHighlightedItemIndex);
653     if (target && target->is<RefPtr<nsMenuItemX>>()) {
654       bool handlerCalledPreventDefault;  // but we don't actually care
655       target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(u"DOMMenuItemInactive"_ns,
656                                                           &handlerCalledPreventDefault);
657     }
658   }
659   if (aNewHighlightedIndex) {
660     Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*aNewHighlightedIndex);
661     if (target && target->is<RefPtr<nsMenuItemX>>()) {
662       bool handlerCalledPreventDefault;  // but we don't actually care
663       target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(u"DOMMenuItemActive"_ns,
664                                                           &handlerCalledPreventDefault);
665     }
666   }
667   mHighlightedItemIndex = aNewHighlightedIndex;
670 void nsMenuX::OnWillActivateItem(NSMenuItem* aItem) {
671   if (!mIsOpenForGecko) {
672     return;
673   }
675   if (mMenuGroupOwner && mObserver) {
676     nsMenuItemX* item = mMenuGroupOwner->GetMenuItemForCommandID(uint32_t(aItem.tag));
677     if (item && item->Content()->IsElement()) {
678       RefPtr<dom::Element> itemElement = item->Content()->AsElement();
679       if (nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent()) {
680         mObserver->OnMenuWillActivateItem(popupContent->AsElement(), itemElement);
681       }
682     }
683   }
686 // Flushes style.
687 static NSUserInterfaceLayoutDirection DirectionForElement(dom::Element* aElement) {
688   // Get the direction from the computed style so that inheritance into submenus is respected.
689   // aElement may not have a frame.
690   RefPtr<const ComputedStyle> sc = nsComputedDOMStyle::GetComputedStyle(aElement);
691   if (!sc) {
692     return NSApp.userInterfaceLayoutDirection;
693   }
695   switch (sc->StyleVisibility()->mDirection) {
696     case StyleDirection::Ltr:
697       return NSUserInterfaceLayoutDirectionLeftToRight;
698     case StyleDirection::Rtl:
699       return NSUserInterfaceLayoutDirectionRightToLeft;
700   }
703 void nsMenuX::RebuildMenu() {
704   MOZ_RELEASE_ASSERT(mNeedsRebuild);
705   gConstructingMenu = true;
707   // Retrieve our menupopup.
708   nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
709   if (!menuPopup) {
710     gConstructingMenu = false;
711     return;
712   }
714   if (menuPopup->IsElement()) {
715     mNativeMenu.userInterfaceLayoutDirection = DirectionForElement(menuPopup->AsElement());
716   }
718   // Iterate over the kids
719   for (nsIContent* child = menuPopup->GetFirstChild(); child; child = child->GetNextSibling()) {
720     if (Maybe<MenuChild> menuChild = CreateMenuChild(child)) {
721       AddMenuChild(std::move(*menuChild));
722     }
723   }  // for each menu item
725   InsertPlaceholderIfNeeded();
727   gConstructingMenu = false;
728   mNeedsRebuild = false;
731 void nsMenuX::InsertPlaceholderIfNeeded() {
732   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
734   if ([mNativeMenu numberOfItems] == 0) {
735     MOZ_RELEASE_ASSERT(mVisibleItemsCount == 0);
736     NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
737     item.enabled = NO;
738     item.view = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 150, 1)] autorelease];
739     [mNativeMenu addItem:item];
740     [item release];
741   }
743   NS_OBJC_END_TRY_ABORT_BLOCK;
746 void nsMenuX::RemovePlaceholderIfPresent() {
747   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
749   if (mVisibleItemsCount == 0 && [mNativeMenu numberOfItems] == 1) {
750     // Remove the placeholder.
751     [mNativeMenu removeItemAtIndex:0];
752   }
754   NS_OBJC_END_TRY_ABORT_BLOCK;
757 void nsMenuX::SetRebuild(bool aNeedsRebuild) {
758   if (!gConstructingMenu) {
759     mNeedsRebuild = aNeedsRebuild;
760     if (mParent && mParent->AsMenuBar()) {
761       mParent->AsMenuBar()->SetNeedsRebuild();
762     }
763   }
766 nsresult nsMenuX::SetEnabled(bool aIsEnabled) {
767   if (aIsEnabled != mIsEnabled) {
768     // we always want to rebuild when this changes
769     mIsEnabled = aIsEnabled;
770     mNativeMenuItem.enabled = mIsEnabled;
771   }
772   return NS_OK;
775 nsresult nsMenuX::GetEnabled(bool* aIsEnabled) {
776   NS_ENSURE_ARG_POINTER(aIsEnabled);
777   *aIsEnabled = mIsEnabled;
778   return NS_OK;
781 GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& aMenuTitle) {
782   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
784   NSString* title = [NSString stringWithCharacters:(UniChar*)aMenuTitle.get()
785                                             length:aMenuTitle.Length()];
786   GeckoNSMenu* myMenu = [[GeckoNSMenu alloc] initWithTitle:title];
787   myMenu.delegate = mMenuDelegate;
789   // We don't want this menu to auto-enable menu items because then Cocoa
790   // overrides our decisions and things get incorrectly enabled/disabled.
791   myMenu.autoenablesItems = NO;
793   // we used to install Carbon event handlers here, but since NSMenu* doesn't
794   // create its underlying MenuRef until just before display, we delay until
795   // that happens. Now we install the event handlers when Cocoa notifies
796   // us that a menu is about to display - see the Cocoa MenuDelegate class.
798   return myMenu;
800   NS_OBJC_END_TRY_ABORT_BLOCK;
803 Maybe<nsMenuX::MenuChild> nsMenuX::CreateMenuChild(nsIContent* aContent) {
804   if (aContent->IsAnyOfXULElements(nsGkAtoms::menuitem, nsGkAtoms::menuseparator)) {
805     return Some(MenuChild(CreateMenuItem(aContent)));
806   }
807   if (aContent->IsXULElement(nsGkAtoms::menu)) {
808     return Some(MenuChild(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aContent)));
809   }
810   return {};
813 RefPtr<nsMenuItemX> nsMenuX::CreateMenuItem(nsIContent* aMenuItemContent) {
814   MOZ_RELEASE_ASSERT(aMenuItemContent);
816   nsAutoString menuitemName;
817   if (aMenuItemContent->IsElement()) {
818     aMenuItemContent->AsElement()->GetAttr(nsGkAtoms::label, menuitemName);
819   }
821   EMenuItemType itemType = eRegularMenuItemType;
822   if (aMenuItemContent->IsXULElement(nsGkAtoms::menuseparator)) {
823     itemType = eSeparatorMenuItemType;
824   } else if (aMenuItemContent->IsElement()) {
825     static Element::AttrValuesArray strings[] = {nsGkAtoms::checkbox, nsGkAtoms::radio, nullptr};
826     switch (aMenuItemContent->AsElement()->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type,
827                                                            strings, eCaseMatters)) {
828       case 0:
829         itemType = eCheckboxMenuItemType;
830         break;
831       case 1:
832         itemType = eRadioMenuItemType;
833         break;
834     }
835   }
837   return MakeRefPtr<nsMenuItemX>(this, menuitemName, itemType, mMenuGroupOwner, aMenuItemContent);
840 // This menu is about to open. Returns false if the handler wants to stop the opening of the menu.
841 bool nsMenuX::OnOpen() {
842   if (mDidFirePopupshowingAndIsApprovedToOpen) {
843     return true;
844   }
846   if (mIsOpen) {
847     NS_WARNING("nsMenuX::OnOpen() called while the menu is already considered to be open. This "
848                "seems odd.");
849   }
851   RefPtr<nsIContent> popupContent = GetMenuPopupContent();
853   if (mObserver && popupContent) {
854     mObserver->OnMenuWillOpen(popupContent->AsElement());
855   }
857   nsEventStatus status = nsEventStatus_eIgnore;
858   WidgetMouseEvent event(true, eXULPopupShowing, nullptr, WidgetMouseEvent::eReal);
860   nsresult rv = NS_OK;
861   RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
862   rv = EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
863   if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) {
864     return false;
865   }
867   DidFirePopupShowing();
869   return true;
872 void nsMenuX::DidFirePopupShowing() {
873   mDidFirePopupshowingAndIsApprovedToOpen = true;
875   // If the open is going to succeed we need to walk our menu items, checking to
876   // see if any of them have a command attribute. If so, several attributes
877   // must potentially be updated.
879   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
880   if (!popupContent) {
881     return;
882   }
884   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
885   if (pm) {
886     pm->UpdateMenuItems(popupContent->AsElement());
887   }
890 // Find the |menupopup| child in the |popup| representing this menu. It should be one
891 // of a very few children so we won't be iterating over a bazillion menu items to find
892 // it (so the strcmp won't kill us).
893 already_AddRefed<nsIContent> nsMenuX::GetMenuPopupContent() {
894   // Check to see if we are a "menupopup" node (if we are a native menu).
895   if (mContent->IsXULElement(nsGkAtoms::menupopup)) {
896     return do_AddRef(mContent);
897   }
899   // Otherwise check our child nodes.
901   for (RefPtr<nsIContent> child = mContent->GetFirstChild(); child;
902        child = child->GetNextSibling()) {
903     if (child->IsXULElement(nsGkAtoms::menupopup)) {
904       return child.forget();
905     }
906   }
908   return nullptr;
911 bool nsMenuX::IsXULHelpMenu(nsIContent* aMenuContent) {
912   bool retval = false;
913   if (aMenuContent && aMenuContent->IsElement()) {
914     nsAutoString id;
915     aMenuContent->AsElement()->GetAttr(nsGkAtoms::id, id);
916     if (id.Equals(u"helpMenu"_ns)) {
917       retval = true;
918     }
919   }
920   return retval;
923 bool nsMenuX::IsXULWindowMenu(nsIContent* aMenuContent) {
924   bool retval = false;
925   if (aMenuContent && aMenuContent->IsElement()) {
926     nsAutoString id;
927     aMenuContent->AsElement()->GetAttr(nsGkAtoms::id, id);
928     if (id.Equals(u"windowMenu"_ns)) {
929       retval = true;
930     }
931   }
932   return retval;
936 // nsChangeObserver
939 void nsMenuX::ObserveAttributeChanged(dom::Document* aDocument, nsIContent* aContent,
940                                       nsAtom* aAttribute) {
941   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
943   // ignore the |open| attribute, which is by far the most common
944   if (gConstructingMenu || (aAttribute == nsGkAtoms::open)) {
945     return;
946   }
948   if (aAttribute == nsGkAtoms::disabled) {
949     SetEnabled(!mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
950                                                    nsGkAtoms::_true, eCaseMatters));
951   } else if (aAttribute == nsGkAtoms::label) {
952     mContent->AsElement()->GetAttr(nsGkAtoms::label, mLabel);
953     NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
954     mNativeMenu.title = newCocoaLabelString;
955     mNativeMenuItem.title = newCocoaLabelString;
956   } else if (aAttribute == nsGkAtoms::hidden || aAttribute == nsGkAtoms::collapsed) {
957     SetRebuild(true);
959     bool newVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
961     // don't do anything if the state is correct already
962     if (newVisible == mVisible) {
963       return;
964     }
966     mVisible = newVisible;
967     if (mParent) {
968       RefPtr<nsMenuX> self = this;
969       mParent->MenuChildChangedVisibility(MenuChild(self), newVisible);
970     }
971     if (mVisible) {
972       SetupIcon();
973     }
974   } else if (aAttribute == nsGkAtoms::image) {
975     SetupIcon();
976   }
978   NS_OBJC_END_TRY_ABORT_BLOCK;
981 void nsMenuX::ObserveContentRemoved(dom::Document* aDocument, nsIContent* aContainer,
982                                     nsIContent* aChild, nsIContent* aPreviousSibling) {
983   if (gConstructingMenu) {
984     return;
985   }
987   SetRebuild(true);
988   mMenuGroupOwner->UnregisterForContentChanges(aChild);
990   if (!mIsOpen) {
991     // We will update the menu contents the next time the menu is opened.
992     return;
993   }
995   // The menu is currently open. Remove the child from mMenuChildren and from our NSMenu.
996   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
997   if (popupContent && aContainer == popupContent && aChild->IsElement()) {
998     if (Maybe<MenuChild> child = GetItemForElement(aChild->AsElement())) {
999       RemoveMenuChild(*child);
1000     }
1001   }
1004 void nsMenuX::ObserveContentInserted(dom::Document* aDocument, nsIContent* aContainer,
1005                                      nsIContent* aChild) {
1006   if (gConstructingMenu) {
1007     return;
1008   }
1010   SetRebuild(true);
1012   if (!mIsOpen) {
1013     // We will update the menu contents the next time the menu is opened.
1014     return;
1015   }
1017   // The menu is currently open. Insert the child into mMenuChildren and into our NSMenu.
1018   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
1019   if (popupContent && aContainer == popupContent) {
1020     if (Maybe<MenuChild> child = CreateMenuChild(aChild)) {
1021       InsertMenuChild(std::move(*child));
1022     }
1023   }
1026 void nsMenuX::SetupIcon() {
1027   mIcon->SetupIcon(mContent);
1028   mNativeMenuItem.image = mIcon->GetIconImage();
1031 void nsMenuX::IconUpdated() {
1032   mNativeMenuItem.image = mIcon->GetIconImage();
1033   if (mIconListener) {
1034     mIconListener->IconUpdated();
1035   }
1038 void nsMenuX::MenuChildChangedVisibility(const MenuChild& aChild, bool aIsVisible) {
1039   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
1041   NSMenuItem* nativeItem = aChild.match(
1042       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
1043       [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); });
1044   if (aIsVisible) {
1045     MOZ_RELEASE_ASSERT(!nativeItem.menu,
1046                        "The native item should not be in a menu while it is hidden");
1047     RemovePlaceholderIfPresent();
1048     NSInteger insertionPoint = CalculateNativeInsertionPoint(aChild);
1049     [mNativeMenu insertItem:nativeItem atIndex:insertionPoint];
1050     mVisibleItemsCount++;
1051   } else {
1052     MOZ_RELEASE_ASSERT([mNativeMenu indexOfItem:nativeItem] != -1,
1053                        "The native item should be in this menu while it is visible");
1054     [mNativeMenu removeItem:nativeItem];
1055     mVisibleItemsCount--;
1056     InsertPlaceholderIfNeeded();
1057   }
1059   NS_OBJC_END_TRY_ABORT_BLOCK;
1062 NSInteger nsMenuX::CalculateNativeInsertionPoint(const MenuChild& aChild) {
1063   NSInteger insertionPoint = 0;
1064   for (auto& currItem : mMenuChildren) {
1065     // Using GetItemAt instead of GetVisibleItemAt to avoid O(N^2)
1066     if (currItem == aChild) {
1067       return insertionPoint;
1068     }
1069     NSMenuItem* nativeItem = currItem.match(
1070         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
1071         [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); });
1072     // Only count visible items.
1073     if (nativeItem.menu) {
1074       insertionPoint++;
1075     }
1076   }
1077   return insertionPoint;
1080 void nsMenuX::Dump(uint32_t aIndent) const {
1081   printf("%*s - menu [%p] %-16s <%s>", aIndent * 2, "", this,
1082          mLabel.IsEmpty() ? "(empty label)" : NS_ConvertUTF16toUTF8(mLabel).get(),
1083          NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
1084   if (mNeedsRebuild) {
1085     printf(" [NeedsRebuild]");
1086   }
1087   if (mIsOpen) {
1088     printf(" [Open]");
1089   }
1090   if (mVisible) {
1091     printf(" [Visible]");
1092   }
1093   if (mIsEnabled) {
1094     printf(" [IsEnabled]");
1095   }
1096   printf(" (%d visible items)", int(mVisibleItemsCount));
1097   printf("\n");
1098   for (const auto& subitem : mMenuChildren) {
1099     subitem.match([=](const RefPtr<nsMenuX>& aMenu) { aMenu->Dump(aIndent + 1); },
1100                   [=](const RefPtr<nsMenuItemX>& aMenuItem) { aMenuItem->Dump(aIndent + 1); });
1101   }
1105 // MenuDelegate Objective-C class, used to set up Carbon events
1108 @implementation MenuDelegate
1110 - (id)initWithGeckoMenu:(nsMenuX*)geckoMenu {
1111   if ((self = [super init])) {
1112     NS_ASSERTION(geckoMenu,
1113                  "Cannot initialize native menu delegate with NULL gecko menu! Will crash!");
1114     mGeckoMenu = geckoMenu;
1115     mBlocksToRunWhenOpen = [[NSMutableArray alloc] init];
1116   }
1117   return self;
1120 - (void)dealloc {
1121   [mBlocksToRunWhenOpen release];
1122   [super dealloc];
1125 - (void)runBlockWhenOpen:(void (^)())block {
1126   [mBlocksToRunWhenOpen addObject:[[block copy] autorelease]];
1129 - (void)menu:(NSMenu*)aMenu willHighlightItem:(NSMenuItem*)aItem {
1130   if (!aMenu || !mGeckoMenu) {
1131     return;
1132   }
1134   Maybe<uint32_t> index =
1135       aItem ? Some(static_cast<uint32_t>([aMenu indexOfItem:aItem])) : Nothing();
1136   mGeckoMenu->OnHighlightedItemChanged(index);
1139 - (void)menuWillOpen:(NSMenu*)menu {
1140   for (void (^block)() in mBlocksToRunWhenOpen) {
1141     block();
1142   }
1143   [mBlocksToRunWhenOpen removeAllObjects];
1145   if (!mGeckoMenu) {
1146     return;
1147   }
1149   // Don't do anything while the OS is (re)indexing our menus (on Leopard and
1150   // higher).  This stops the Help menu from being able to search in our
1151   // menus, but it also resolves many other problems.
1152   if (nsMenuX::sIndexingMenuLevel > 0) {
1153     return;
1154   }
1156   if (self.menuIsInMenubar) {
1157     // If a menu in the menubar is trying open while a non-native menu is open, roll up the
1158     // non-native menu and reject the menubar opening attempt, effectively consuming the event.
1159     nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
1160     if (rollupListener) {
1161       nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget();
1162       if (rollupWidget) {
1163         rollupListener->Rollup({0, nsIRollupListener::FlushViews::Yes});
1164         [menu cancelTracking];
1165         return;
1166       }
1167     }
1168   }
1170   // Hold a strong reference to mGeckoMenu while calling its methods.
1171   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1172   geckoMenu->MenuOpened();
1175 - (void)menuDidClose:(NSMenu*)menu {
1176   if (!mGeckoMenu) {
1177     return;
1178   }
1180   // Don't do anything while the OS is (re)indexing our menus (on Leopard and
1181   // higher).  This stops the Help menu from being able to search in our
1182   // menus, but it also resolves many other problems.
1183   if (nsMenuX::sIndexingMenuLevel > 0) {
1184     return;
1185   }
1187   // Hold a strong reference to mGeckoMenu while calling its methods.
1188   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1189   geckoMenu->MenuClosed();
1192 // This is called after menuDidClose:.
1193 - (void)menu:(NSMenu*)aMenu willActivateItem:(NSMenuItem*)aItem {
1194   if (!mGeckoMenu) {
1195     return;
1196   }
1198   // Hold a strong reference to mGeckoMenu while calling its methods.
1199   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1200   geckoMenu->OnWillActivateItem(aItem);
1203 @end
1205 // OS X Leopard (at least as of 10.5.2) has an obscure bug triggered by some
1206 // behavior that's present in Mozilla.org browsers but not (as best I can
1207 // tell) in Apple products like Safari.  (It's not yet clear exactly what this
1208 // behavior is.)
1210 // The bug is that sometimes you crash on quit in nsMenuX::RemoveAll(), on a
1211 // call to [NSMenu removeItemAtIndex:].  The crash is caused by trying to
1212 // access a deleted NSMenuItem object (sometimes (perhaps always?) by trying
1213 // to send it a _setChangedFlags: message).  Though this object was deleted
1214 // some time ago, it remains registered as a potential target for a particular
1215 // key equivalent.  So when [NSMenu removeItemAtIndex:] removes the current
1216 // target for that same key equivalent, the OS tries to "activate" the
1217 // previous target.
1219 // The underlying reason appears to be that NSMenu's _addItem:toTable: and
1220 // _removeItem:fromTable: methods (which are used to keep a hashtable of
1221 // registered key equivalents) don't properly "retain" and "release"
1222 // NSMenuItem objects as they are added to and removed from the hashtable.
1224 // Our (hackish) workaround is to shadow the OS's hashtable with another
1225 // hastable of our own (gShadowKeyEquivDB), and use it to "retain" and
1226 // "release" NSMenuItem objects as needed.  This resolves bmo bugs 422287 and
1227 // 423669.  When (if) Apple fixes this bug, we can remove this workaround.
1229 static NSMutableDictionary* gShadowKeyEquivDB = nil;
1231 // Class for values in gShadowKeyEquivDB.
1233 @interface KeyEquivDBItem : NSObject {
1234   NSMenuItem* mItem;
1235   NSMutableSet* mTables;
1238 - (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable;
1239 - (BOOL)hasTable:(NSMapTable*)aTable;
1240 - (int)addTable:(NSMapTable*)aTable;
1241 - (int)removeTable:(NSMapTable*)aTable;
1243 @end
1245 @implementation KeyEquivDBItem
1247 - (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable {
1248   if (!gShadowKeyEquivDB) {
1249     gShadowKeyEquivDB = [[NSMutableDictionary alloc] init];
1250   }
1251   self = [super init];
1252   if (aItem && aTable) {
1253     mTables = [[NSMutableSet alloc] init];
1254     mItem = [aItem retain];
1255     [mTables addObject:[NSValue valueWithPointer:aTable]];
1256   } else {
1257     mTables = nil;
1258     mItem = nil;
1259   }
1260   return self;
1263 - (void)dealloc {
1264   if (mTables) {
1265     [mTables release];
1266   }
1267   if (mItem) {
1268     [mItem release];
1269   }
1270   [super dealloc];
1273 - (BOOL)hasTable:(NSMapTable*)aTable {
1274   return [mTables member:[NSValue valueWithPointer:aTable]] ? YES : NO;
1277 // Does nothing if aTable (its index value) is already present in mTables.
1278 - (int)addTable:(NSMapTable*)aTable {
1279   if (aTable) {
1280     [mTables addObject:[NSValue valueWithPointer:aTable]];
1281   }
1282   return [mTables count];
1285 - (int)removeTable:(NSMapTable*)aTable {
1286   if (aTable) {
1287     NSValue* objectToRemove = [mTables member:[NSValue valueWithPointer:aTable]];
1288     if (objectToRemove) {
1289       [mTables removeObject:objectToRemove];
1290     }
1291   }
1292   return [mTables count];
1295 @end
1297 @interface NSMenu (MethodSwizzling)
1298 + (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable;
1299 + (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem fromTable:(NSMapTable*)aTable;
1300 @end
1302 @implementation NSMenu (MethodSwizzling)
1304 + (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable {
1305   if (aItem && aTable) {
1306     NSValue* key = [NSValue valueWithPointer:aItem];
1307     KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
1308     if (shadowItem) {
1309       [shadowItem addTable:aTable];
1310     } else {
1311       shadowItem = [[KeyEquivDBItem alloc] initWithItem:aItem table:aTable];
1312       [gShadowKeyEquivDB setObject:shadowItem forKey:key];
1313       // Release after [NSMutableDictionary setObject:forKey:] retains it (so
1314       // that it will get dealloced when removeObjectForKey: is called).
1315       [shadowItem release];
1316     }
1317   }
1319   [self nsMenuX_NSMenu_addItem:aItem toTable:aTable];
1322 + (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem fromTable:(NSMapTable*)aTable {
1323   [self nsMenuX_NSMenu_removeItem:aItem fromTable:aTable];
1325   if (aItem && aTable) {
1326     NSValue* key = [NSValue valueWithPointer:aItem];
1327     KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
1328     if (shadowItem && [shadowItem hasTable:aTable]) {
1329       if (![shadowItem removeTable:aTable]) {
1330         [gShadowKeyEquivDB removeObjectForKey:key];
1331       }
1332     }
1333   }
1336 @end
1338 // This class is needed to keep track of when the OS is (re)indexing all of
1339 // our menus.  This appears to only happen on Leopard and higher, and can
1340 // be triggered by opening the Help menu.  Some operations are unsafe while
1341 // this is happening -- notably the calls to [[NSImage alloc]
1342 // initWithSize:imageRect.size] and [newImage lockFocus] in nsMenuItemIconX::
1343 // OnStopFrame().  But we don't yet have a complete list, and Apple doesn't
1344 // yet have any documentation on this subject.  (Apple also doesn't yet have
1345 // any documented way to find the information we seek here.)  The "original"
1346 // of this class (the one whose indexMenuBarDynamically method we hook) is
1347 // defined in the Shortcut framework in /System/Library/PrivateFrameworks.
1348 @interface NSObject (SCTGRLIndexMethodSwizzling)
1349 - (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically;
1350 @end
1352 @implementation NSObject (SCTGRLIndexMethodSwizzling)
1354 - (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically {
1355   // This method appears to be called (once) whenever the OS (re)indexes our
1356   // menus.  sIndexingMenuLevel is a int32_t just in case it might be
1357   // reentered.  As it's running, it spawns calls to two undocumented
1358   // HIToolbox methods (_SimulateMenuOpening() and _SimulateMenuClosed()),
1359   // which "simulate" the opening and closing of our menus without actually
1360   // displaying them.
1361   ++nsMenuX::sIndexingMenuLevel;
1362   [self nsMenuX_SCTGRLIndex_indexMenuBarDynamically];
1363   --nsMenuX::sIndexingMenuLevel;
1366 @end
1368 @interface NSObject (NSServicesMenuUpdaterSwizzling)
1369 - (void)nsMenuX_populateMenu:(NSMenu*)aMenu
1370           withServiceEntries:(NSArray*)aServices
1371                   forDisplay:(BOOL)aForDisplay;
1372 @end
1374 @interface _NSServiceEntry : NSObject
1375 - (NSString*)bundleIdentifier;
1376 @end
1378 @implementation NSObject (NSServicesMenuUpdaterSwizzling)
1380 - (void)nsMenuX_populateMenu:(NSMenu*)aMenu
1381           withServiceEntries:(NSArray*)aServices
1382                   forDisplay:(BOOL)aForDisplay {
1383   NSMutableArray* filteredServices = [NSMutableArray array];
1385   // We need to filter some services, such as "Search with Google", since this
1386   // service is duplicating functionality already exposed by our "Search Google
1387   // for..." context menu entry and because it opens in Safari, which can cause
1388   // confusion for users.
1389   for (_NSServiceEntry* service in aServices) {
1390     NSString* bundleId = [service bundleIdentifier];
1391     NSString* msg = [service valueForKey:@"message"];
1392     bool shouldSkip = ([bundleId isEqualToString:@"com.apple.Safari"]) ||
1393                       ([bundleId isEqualToString:@"com.apple.systemuiserver"] &&
1394                        [msg isEqualToString:@"openURL"]);
1395     if (!shouldSkip) {
1396       [filteredServices addObject:service];
1397     }
1398   }
1400   [self nsMenuX_populateMenu:aMenu withServiceEntries:filteredServices forDisplay:aForDisplay];
1403 @end