Bug 1931425 - Limit how often moz-label's #setStyles runs r=reusable-components-revie...
[gecko.git] / widget / cocoa / nsMenuX.mm
blob2cf6fd054377955f51c093fd8e77fe7ae554903e
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 = [[GeckoNSMenuItem 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   bool isXULWindowMenu = IsXULWindowMenu(mContent);
136   if (isXULWindowMenu) {
137     // Let the OS know that this is our Window menu.
138     NSApp.windowsMenu = mNativeMenu;
139   }
141   mIcon = MakeUnique<nsMenuItemIconX>(this);
143   if (mVisible) {
144     if (!isXULWindowMenu) {
145       SetRebuild(true);
146     }
147     SetupIcon();
148   }
150   NS_OBJC_END_TRY_ABORT_BLOCK;
153 nsMenuX::~nsMenuX() {
154   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
156   // Make sure a pending popupshown event isn't dropped.
157   FlushMenuOpenedRunnable();
159   if (mIsOpen) {
160     [mNativeMenu cancelTracking];
161     MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
162   }
164   // Make sure pending popuphiding/popuphidden events aren't dropped.
165   FlushMenuClosedRunnable();
167   OnHighlightedItemChanged(Nothing());
168   RemoveAll();
170   mNativeMenu.delegate = nil;
171   [mNativeMenu release];
172   [mMenuDelegate release];
173   // autorelease the native menu item so that anything else happening to this
174   // object happens before the native menu item actually dies
175   [mNativeMenuItem autorelease];
177   DetachFromGroupOwnerRecursive();
179   MOZ_COUNT_DTOR(nsMenuX);
181   NS_OBJC_END_TRY_ABORT_BLOCK;
184 void nsMenuX::DetachFromGroupOwnerRecursive() {
185   if (!mMenuGroupOwner) {
186     // Don't recurse if this subtree is already detached.
187     // This avoids repeated recursion during the destruction of nested nsMenuX
188     // structures. Our invariant is: If we are detached, all of our contents are
189     // also detached.
190     return;
191   }
193   if (mMenuGroupOwner && mContent) {
194     mMenuGroupOwner->UnregisterForContentChanges(mContent);
195   }
196   mMenuGroupOwner = nullptr;
198   // Also detach all our children.
199   for (auto& child : mMenuChildren) {
200     child.match(
201         [](const RefPtr<nsMenuX>& aMenu) {
202           aMenu->DetachFromGroupOwnerRecursive();
203         },
204         [](const RefPtr<nsMenuItemX>& aMenuItem) {
205           aMenuItem->DetachFromGroupOwner();
206         });
207   }
210 void nsMenuX::OnMenuWillOpen(dom::Element* aPopupElement) {
211   RefPtr<nsMenuX> kungFuDeathGrip(this);
212   if (mObserver) {
213     mObserver->OnMenuWillOpen(aPopupElement);
214   }
217 void nsMenuX::OnMenuDidOpen(dom::Element* aPopupElement) {
218   RefPtr<nsMenuX> kungFuDeathGrip(this);
219   if (mObserver) {
220     mObserver->OnMenuDidOpen(aPopupElement);
221   }
224 void nsMenuX::OnMenuWillActivateItem(dom::Element* aPopupElement,
225                                      dom::Element* aMenuItemElement) {
226   RefPtr<nsMenuX> kungFuDeathGrip(this);
227   if (mObserver) {
228     mObserver->OnMenuWillActivateItem(aPopupElement, aMenuItemElement);
229   }
232 void nsMenuX::OnMenuClosed(dom::Element* aPopupElement) {
233   RefPtr<nsMenuX> kungFuDeathGrip(this);
234   if (mObserver) {
235     mObserver->OnMenuClosed(aPopupElement);
236   }
239 void nsMenuX::AddMenuChild(MenuChild&& aChild) {
240   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
242   WillInsertChild(aChild);
243   mMenuChildren.AppendElement(aChild);
245   bool isVisible = aChild.match(
246       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
247       [](const RefPtr<nsMenuItemX>& aMenuItem) {
248         return aMenuItem->IsVisible();
249       });
250   NSMenuItem* nativeItem = aChild.match(
251       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
252       [](const RefPtr<nsMenuItemX>& aMenuItem) {
253         return aMenuItem->NativeNSMenuItem();
254       });
256   if (isVisible) {
257     RemovePlaceholderIfPresent();
258     [mNativeMenu addItem:nativeItem];
259     ++mVisibleItemsCount;
260   }
262   NS_OBJC_END_TRY_ABORT_BLOCK;
265 void nsMenuX::InsertMenuChild(MenuChild&& aChild) {
266   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
268   WillInsertChild(aChild);
269   size_t insertionIndex = FindInsertionIndex(aChild);
270   mMenuChildren.InsertElementAt(insertionIndex, aChild);
272   bool isVisible = aChild.match(
273       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
274       [](const RefPtr<nsMenuItemX>& aMenuItem) {
275         return aMenuItem->IsVisible();
276       });
277   if (isVisible) {
278     MenuChildChangedVisibility(aChild, true);
279   }
281   NS_OBJC_END_TRY_ABORT_BLOCK;
284 void nsMenuX::RemoveMenuChild(const MenuChild& aChild) {
285   bool isVisible = aChild.match(
286       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
287       [](const RefPtr<nsMenuItemX>& aMenuItem) {
288         return aMenuItem->IsVisible();
289       });
290   if (isVisible) {
291     MenuChildChangedVisibility(aChild, false);
292   }
294   WillRemoveChild(aChild);
295   mMenuChildren.RemoveElement(aChild);
298 size_t nsMenuX::FindInsertionIndex(const MenuChild& aChild) {
299   nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
300   MOZ_RELEASE_ASSERT(menuPopup);
302   RefPtr<nsIContent> insertedContent = aChild.match(
303       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
304       [](const RefPtr<nsMenuItemX>& aMenuItem) {
305         return aMenuItem->Content();
306       });
308   MOZ_RELEASE_ASSERT(insertedContent->GetParent() == menuPopup);
310   // Iterate over menuPopup's children (insertedContent's siblings) until we
311   // encounter insertedContent. At the same time, keep track of the index in
312   // mMenuChildren.
313   size_t index = 0;
314   for (nsIContent* child = menuPopup->GetFirstChild();
315        child && index < mMenuChildren.Length();
316        child = child->GetNextSibling()) {
317     if (child == insertedContent) {
318       break;
319     }
321     RefPtr<nsIContent> contentAtIndex = mMenuChildren[index].match(
322         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
323         [](const RefPtr<nsMenuItemX>& aMenuItem) {
324           return aMenuItem->Content();
325         });
326     if (child == contentAtIndex) {
327       index++;
328     }
329   }
331   return index;
334 // Includes all items, including hidden/collapsed ones
335 uint32_t nsMenuX::GetItemCount() { return mMenuChildren.Length(); }
337 // Includes all items, including hidden/collapsed ones
338 mozilla::Maybe<nsMenuX::MenuChild> nsMenuX::GetItemAt(uint32_t aPos) {
339   if (aPos >= (uint32_t)mMenuChildren.Length()) {
340     return {};
341   }
343   return Some(mMenuChildren[aPos]);
346 // Only includes visible items
347 nsresult nsMenuX::GetVisibleItemCount(uint32_t& aCount) {
348   aCount = mVisibleItemsCount;
349   return NS_OK;
352 // Only includes visible items. Note that this is provides O(N) access
353 // If you need to iterate or search, consider using GetItemAt and doing your own
354 // filtering
355 Maybe<nsMenuX::MenuChild> nsMenuX::GetVisibleItemAt(uint32_t aPos) {
356   uint32_t count = mMenuChildren.Length();
357   if (aPos >= mVisibleItemsCount || aPos >= count) {
358     return {};
359   }
361   // If there are no invisible items, can provide direct access
362   if (mVisibleItemsCount == count) {
363     return GetItemAt(aPos);
364   }
366   // Otherwise, traverse the array until we find the the item we're looking for.
367   uint32_t visibleNodeIndex = 0;
368   for (uint32_t i = 0; i < count; i++) {
369     MenuChild item = *GetItemAt(i);
370     RefPtr<nsIContent> content = item.match(
371         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
372         [](const RefPtr<nsMenuItemX>& aMenuItem) {
373           return aMenuItem->Content();
374         });
375     if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
376       if (aPos == visibleNodeIndex) {
377         // we found the visible node we're looking for, return it
378         return Some(item);
379       }
380       visibleNodeIndex++;
381     }
382   }
384   return {};
387 Maybe<nsMenuX::MenuChild> nsMenuX::GetItemForElement(
388     Element* aMenuChildElement) {
389   for (auto& child : mMenuChildren) {
390     RefPtr<nsIContent> content = child.match(
391         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
392         [](const RefPtr<nsMenuItemX>& aMenuItem) {
393           return aMenuItem->Content();
394         });
395     if (content == aMenuChildElement) {
396       return Some(child);
397     }
398   }
399   return {};
402 nsresult nsMenuX::RemoveAll() {
403   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
405   [mNativeMenu removeAllItems];
407   for (auto& child : mMenuChildren) {
408     WillRemoveChild(child);
409   }
411   mMenuChildren.Clear();
412   mVisibleItemsCount = 0;
414   return NS_OK;
416   NS_OBJC_END_TRY_ABORT_BLOCK;
419 void nsMenuX::WillInsertChild(const MenuChild& aChild) {
420   if (aChild.is<RefPtr<nsMenuX>>()) {
421     aChild.as<RefPtr<nsMenuX>>()->SetObserver(this);
422   }
425 void nsMenuX::WillRemoveChild(const MenuChild& aChild) {
426   aChild.match(
427       [](const RefPtr<nsMenuX>& aMenu) {
428         aMenu->DetachFromGroupOwnerRecursive();
429         aMenu->DetachFromParent();
430         aMenu->SetObserver(nullptr);
431       },
432       [](const RefPtr<nsMenuItemX>& aMenuItem) {
433         aMenuItem->DetachFromGroupOwner();
434         aMenuItem->DetachFromParent();
435       });
438 void nsMenuX::MenuOpened() {
439   if (mIsOpen) {
440     return;
441   }
443   // Make sure we fire any pending popupshown / popuphiding / popuphidden events
444   // first.
445   FlushMenuOpenedRunnable();
446   FlushMenuClosedRunnable();
448   if (!mDidFirePopupshowingAndIsApprovedToOpen) {
449     // Fire popupshowing now.
450     bool approvedToOpen = OnOpen();
451     if (!approvedToOpen) {
452       // We can only stop menus from opening which we open ourselves. We cannot
453       // stop menubar root menus or menu submenus from opening. For context
454       // menus, we can call OnOpen() before we ask the system to open the menu.
455       NS_WARNING("The popupshowing event had preventDefault() called on it, "
456                  "but in MenuOpened() it "
457                  "is too late to stop the menu from opening.");
458     }
459   }
461   mIsOpen = true;
463   // Reset mDidFirePopupshowingAndIsApprovedToOpen for the next menu opening.
464   mDidFirePopupshowingAndIsApprovedToOpen = false;
466   if (mNeedsRebuild) {
467     OnHighlightedItemChanged(Nothing());
468     RemoveAll();
469     RebuildMenu();
470   }
472   // Fire the popupshown event in MenuOpenedAsync.
473   // MenuOpened() is called during menuWillOpen, and if cancelTracking is called
474   // now, menuDidClose will not be called. The runnable object must not hold a
475   // strong reference to the nsMenuX, so that there is no reference cycle.
476   class MenuOpenedAsyncRunnable final : public mozilla::CancelableRunnable {
477    public:
478     explicit MenuOpenedAsyncRunnable(nsMenuX* aMenu)
479         : CancelableRunnable("MenuOpenedAsyncRunnable"), mMenu(aMenu) {}
481     // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
482     MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
483       if (RefPtr<nsMenuX> menu = mMenu) {
484         menu->MenuOpenedAsync();
485         mMenu = nullptr;
486       }
487       return NS_OK;
488     }
489     nsresult Cancel() override {
490       mMenu = nullptr;
491       return NS_OK;
492     }
494    private:
495     nsMenuX* mMenu;  // weak, cleared by Cancel() and Run()
496   };
497   mPendingAsyncMenuOpenRunnable = new MenuOpenedAsyncRunnable(this);
498   NS_DispatchToCurrentThread(mPendingAsyncMenuOpenRunnable);
501 void nsMenuX::FlushMenuOpenedRunnable() {
502   if (mPendingAsyncMenuOpenRunnable) {
503     MenuOpenedAsync();
504   }
507 void nsMenuX::MenuOpenedAsync() {
508   if (mPendingAsyncMenuOpenRunnable) {
509     mPendingAsyncMenuOpenRunnable->Cancel();
510     mPendingAsyncMenuOpenRunnable = nullptr;
511   }
513   mIsOpenForGecko = true;
515   // Open the node.
516   if (mContent->IsElement()) {
517     mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open,
518                                    u"true"_ns, true);
519   }
521   RefPtr<nsIContent> popupContent = GetMenuPopupContent();
523   // Notify our observer.
524   if (mObserver && popupContent) {
525     mObserver->OnMenuDidOpen(popupContent->AsElement());
526   }
528   // Fire popupshown.
529   nsEventStatus status = nsEventStatus_eIgnore;
530   WidgetMouseEvent event(true, eXULPopupShown, nullptr,
531                          WidgetMouseEvent::eReal);
532   RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
533   EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
536 void nsMenuX::MenuClosed() {
537   if (!mIsOpen) {
538     return;
539   }
541   // Make sure we fire any pending popupshown events first.
542   FlushMenuOpenedRunnable();
544   // If any of our submenus were opened programmatically, make sure they get
545   // closed first.
546   for (auto& child : mMenuChildren) {
547     if (child.is<RefPtr<nsMenuX>>()) {
548       child.as<RefPtr<nsMenuX>>()->MenuClosed();
549     }
550   }
552   mIsOpen = false;
554   // Do the rest of the MenuClosed work in MenuClosedAsync.
555   // MenuClosed() is called from -[NSMenuDelegate menuDidClose:]. If a menuitem
556   // was clicked, menuDidClose is called *before* menuItemHit for the clicked
557   // menu item is called. This runnable will be canceled if ~nsMenuX runs before
558   // the runnable. The runnable object must not hold a strong reference to the
559   // nsMenuX, so that there is no reference cycle.
560   class MenuClosedAsyncRunnable final : public mozilla::CancelableRunnable {
561    public:
562     explicit MenuClosedAsyncRunnable(nsMenuX* aMenu)
563         : CancelableRunnable("MenuClosedAsyncRunnable"), mMenu(aMenu) {}
565     // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
566     MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult Run() override {
567       if (RefPtr<nsMenuX> menu = mMenu) {
568         menu->MenuClosedAsync();
569         mMenu = nullptr;
570       }
571       return NS_OK;
572     }
573     nsresult Cancel() override {
574       mMenu = nullptr;
575       return NS_OK;
576     }
578    private:
579     nsMenuX* mMenu;  // weak, cleared by Cancel() and Run()
580   };
582   mPendingAsyncMenuCloseRunnable = new MenuClosedAsyncRunnable(this);
584   NS_DispatchToCurrentThread(mPendingAsyncMenuCloseRunnable);
587 void nsMenuX::FlushMenuClosedRunnable() {
588   // If any of our submenus have a pending menu closed runnable, make sure those
589   // run first.
590   for (auto& child : mMenuChildren) {
591     if (child.is<RefPtr<nsMenuX>>()) {
592       child.as<RefPtr<nsMenuX>>()->FlushMenuClosedRunnable();
593     }
594   }
596   if (mPendingAsyncMenuCloseRunnable) {
597     MenuClosedAsync();
598   }
601 void nsMenuX::MenuClosedAsync() {
602   if (mPendingAsyncMenuCloseRunnable) {
603     mPendingAsyncMenuCloseRunnable->Cancel();
604     mPendingAsyncMenuCloseRunnable = nullptr;
605   }
607   // If we have pending command events, run those first.
608   nsTArray<PendingCommandEvent> events = std::move(mPendingCommandEvents);
609   for (auto& event : events) {
610     event.mMenuItem->DoCommand(event.mModifiers, event.mButton);
611   }
613   // Make sure no item is highlighted.
614   OnHighlightedItemChanged(Nothing());
616   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
617   nsCOMPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
619   nsEventStatus status = nsEventStatus_eIgnore;
620   WidgetMouseEvent popupHiding(true, eXULPopupHiding, nullptr,
621                                WidgetMouseEvent::eReal);
622   EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHiding, nullptr,
623                             &status);
625   mIsOpenForGecko = false;
627   if (mContent->IsElement()) {
628     mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true);
629   }
631   WidgetMouseEvent popupHidden(true, eXULPopupHidden, nullptr,
632                                WidgetMouseEvent::eReal);
633   EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHidden, nullptr,
634                             &status);
636   // Notify our observer.
637   if (mObserver && popupContent) {
638     mObserver->OnMenuClosed(popupContent->AsElement());
639   }
642 void nsMenuX::ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem,
643                                        NSEventModifierFlags aModifiers,
644                                        int16_t aButton) {
645   if (mIsOpenForGecko) {
646     // Queue the event into mPendingCommandEvents. We will call aItem->DoCommand
647     // in MenuClosedAsync(). We rely on the assumption that MenuClosedAsync will
648     // run soon.
649     mPendingCommandEvents.AppendElement(
650         PendingCommandEvent{std::move(aItem), aModifiers, aButton});
651   } else {
652     // The menu item was activated outside of a regular open / activate / close
653     // sequence. This happens in multiple cases:
654     //  - When a menu item is activated by a keyboard shortcut while all windows
655     //  are closed
656     //    (otherwise those shortcuts go through Gecko's manual keyboard
657     //    handling)
658     //  - When a menu item in the Dock menu is clicked
659     //  - During native menu tests
660     //
661     // Run the command synchronously.
662     aItem->DoCommand(aModifiers, aButton);
663   }
666 bool nsMenuX::Close() {
667   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
669   if (mDidFirePopupshowingAndIsApprovedToOpen && !mIsOpen) {
670     // Close is being called right after this menu was opened, but before
671     // MenuOpened() had a chance to run. Call it here so that we can go through
672     // the entire popupshown -> popuphiding -> popuphidden sequence. Some
673     // callers expect to get a popuphidden event even if they close the popup
674     // before it was fully open.
675     MenuOpened();
676   }
678   FlushMenuOpenedRunnable();
680   bool wasOpen = mIsOpenForGecko;
682   if (mIsOpen) {
683     // Close the menu.
684     // We usually don't get here during normal Firefox usage: If the user closes
685     // the menu by clicking an item, or by clicking outside the menu, or by
686     // pressing escape, then the menu gets closed by macOS, and not by a call to
687     // nsMenuX::Close(). If we do get here, it's usually because we're running
688     // an automated test. Close the menu without the fade-out animation so that
689     // we don't unnecessarily slow down the automated tests.
690     [mNativeMenu cancelTrackingWithoutAnimation];
691     MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
693     // Handle closing synchronously.
694     MenuClosed();
695   }
697   FlushMenuClosedRunnable();
699   return wasOpen;
701   NS_OBJC_END_TRY_ABORT_BLOCK;
704 void nsMenuX::OnHighlightedItemChanged(
705     const Maybe<uint32_t>& aNewHighlightedIndex) {
706   if (mHighlightedItemIndex == aNewHighlightedIndex) {
707     return;
708   }
710   if (mHighlightedItemIndex) {
711     Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*mHighlightedItemIndex);
712     if (target && target->is<RefPtr<nsMenuItemX>>()) {
713       bool handlerCalledPreventDefault;  // but we don't actually care
714       target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(
715           u"DOMMenuItemInactive"_ns, &handlerCalledPreventDefault);
716     }
717   }
718   if (aNewHighlightedIndex) {
719     Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*aNewHighlightedIndex);
720     if (target && target->is<RefPtr<nsMenuItemX>>()) {
721       bool handlerCalledPreventDefault;  // but we don't actually care
722       target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(
723           u"DOMMenuItemActive"_ns, &handlerCalledPreventDefault);
724     }
725   }
726   mHighlightedItemIndex = aNewHighlightedIndex;
729 void nsMenuX::OnWillActivateItem(NSMenuItem* aItem) {
730   if (!mIsOpenForGecko) {
731     return;
732   }
734   if (mMenuGroupOwner && mObserver) {
735     nsMenuItemX* item =
736         mMenuGroupOwner->GetMenuItemForCommandID(uint32_t(aItem.tag));
737     if (item && item->Content()->IsElement()) {
738       RefPtr<dom::Element> itemElement = item->Content()->AsElement();
739       if (nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent()) {
740         mObserver->OnMenuWillActivateItem(popupContent->AsElement(),
741                                           itemElement);
742       }
743     }
744   }
747 // Flushes style.
748 static NSUserInterfaceLayoutDirection DirectionForElement(
749     dom::Element* aElement) {
750   // Get the direction from the computed style so that inheritance into submenus
751   // is respected. aElement may not have a frame.
752   RefPtr<const ComputedStyle> sc =
753       nsComputedDOMStyle::GetComputedStyle(aElement);
754   if (!sc) {
755     return NSApp.userInterfaceLayoutDirection;
756   }
758   switch (sc->StyleVisibility()->mDirection) {
759     case StyleDirection::Ltr:
760       return NSUserInterfaceLayoutDirectionLeftToRight;
761     case StyleDirection::Rtl:
762       return NSUserInterfaceLayoutDirectionRightToLeft;
763   }
766 void nsMenuX::RebuildMenu() {
767   MOZ_RELEASE_ASSERT(mNeedsRebuild);
768   gConstructingMenu = true;
770   // Retrieve our menupopup.
771   nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
772   if (!menuPopup) {
773     gConstructingMenu = false;
774     return;
775   }
777   if (menuPopup->IsElement()) {
778     mNativeMenu.userInterfaceLayoutDirection =
779         DirectionForElement(menuPopup->AsElement());
780   }
782   // Iterate over the kids
783   for (nsIContent* child = menuPopup->GetFirstChild(); child;
784        child = child->GetNextSibling()) {
785     if (Maybe<MenuChild> menuChild = CreateMenuChild(child)) {
786       AddMenuChild(std::move(*menuChild));
787     }
788   }  // for each menu item
790   InsertPlaceholderIfNeeded();
792   gConstructingMenu = false;
793   mNeedsRebuild = false;
796 void nsMenuX::InsertPlaceholderIfNeeded() {
797   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
799   if ([mNativeMenu numberOfItems] == 0) {
800     MOZ_RELEASE_ASSERT(mVisibleItemsCount == 0);
801     NSMenuItem* item = [[GeckoNSMenuItem alloc] initWithTitle:@""
802                                                        action:nil
803                                                 keyEquivalent:@""];
804     item.enabled = NO;
805     item.view =
806         [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 150, 1)] autorelease];
807     [mNativeMenu addItem:item];
808     [item release];
809   }
811   NS_OBJC_END_TRY_ABORT_BLOCK;
814 void nsMenuX::RemovePlaceholderIfPresent() {
815   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
817   if (mVisibleItemsCount == 0 && [mNativeMenu numberOfItems] == 1) {
818     // Remove the placeholder.
819     [mNativeMenu removeItemAtIndex:0];
820   }
822   NS_OBJC_END_TRY_ABORT_BLOCK;
825 void nsMenuX::SetRebuild(bool aNeedsRebuild) {
826   if (!gConstructingMenu) {
827     mNeedsRebuild = aNeedsRebuild;
828     if (mParent && mParent->AsMenuBar()) {
829       mParent->AsMenuBar()->SetNeedsRebuild();
830     }
831   }
834 nsresult nsMenuX::SetEnabled(bool aIsEnabled) {
835   if (aIsEnabled != mIsEnabled) {
836     // we always want to rebuild when this changes
837     mIsEnabled = aIsEnabled;
838     mNativeMenuItem.enabled = mIsEnabled;
839   }
840   return NS_OK;
843 nsresult nsMenuX::GetEnabled(bool* aIsEnabled) {
844   NS_ENSURE_ARG_POINTER(aIsEnabled);
845   *aIsEnabled = mIsEnabled;
846   return NS_OK;
849 GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& aMenuTitle,
850                                                 bool aShowServices) {
851   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
853   NSString* title = [NSString stringWithCharacters:(UniChar*)aMenuTitle.get()
854                                             length:aMenuTitle.Length()];
855   GeckoNSMenu* myMenu = [[GeckoNSMenu alloc] initWithTitle:title];
856   myMenu.delegate = mMenuDelegate;
858   // We don't want this menu to auto-enable menu items because then Cocoa
859   // overrides our decisions and things get incorrectly enabled/disabled.
860   myMenu.autoenablesItems = NO;
862   // Only show "Services", "Autofill" and similar entries provided by macOS
863   // if our caller wants them:
864   myMenu.allowsContextMenuPlugIns = aShowServices;
866   // we used to install Carbon event handlers here, but since NSMenu* doesn't
867   // create its underlying MenuRef until just before display, we delay until
868   // that happens. Now we install the event handlers when Cocoa notifies
869   // us that a menu is about to display - see the Cocoa MenuDelegate class.
871   return myMenu;
873   NS_OBJC_END_TRY_ABORT_BLOCK;
876 Maybe<nsMenuX::MenuChild> nsMenuX::CreateMenuChild(nsIContent* aContent) {
877   if (aContent->IsAnyOfXULElements(nsGkAtoms::menuitem,
878                                    nsGkAtoms::menuseparator)) {
879     return Some(MenuChild(CreateMenuItem(aContent)));
880   }
881   if (aContent->IsXULElement(nsGkAtoms::menu)) {
882     return Some(
883         MenuChild(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aContent)));
884   }
885   return {};
888 RefPtr<nsMenuItemX> nsMenuX::CreateMenuItem(nsIContent* aMenuItemContent) {
889   MOZ_RELEASE_ASSERT(aMenuItemContent);
891   nsAutoString menuitemName;
892   if (aMenuItemContent->IsElement()) {
893     aMenuItemContent->AsElement()->GetAttr(nsGkAtoms::label, menuitemName);
894   }
896   EMenuItemType itemType = eRegularMenuItemType;
897   if (aMenuItemContent->IsXULElement(nsGkAtoms::menuseparator)) {
898     itemType = eSeparatorMenuItemType;
899   } else if (aMenuItemContent->IsElement()) {
900     static Element::AttrValuesArray strings[] = {nsGkAtoms::checkbox,
901                                                  nsGkAtoms::radio, nullptr};
902     switch (aMenuItemContent->AsElement()->FindAttrValueIn(
903         kNameSpaceID_None, nsGkAtoms::type, strings, eCaseMatters)) {
904       case 0:
905         itemType = eCheckboxMenuItemType;
906         break;
907       case 1:
908         itemType = eRadioMenuItemType;
909         break;
910     }
911   }
913   return MakeRefPtr<nsMenuItemX>(this, menuitemName, itemType, mMenuGroupOwner,
914                                  aMenuItemContent);
917 // This menu is about to open. Returns false if the handler wants to stop the
918 // opening of the menu.
919 bool nsMenuX::OnOpen() {
920   if (mDidFirePopupshowingAndIsApprovedToOpen) {
921     return true;
922   }
924   if (mIsOpen) {
925     NS_WARNING("nsMenuX::OnOpen() called while the menu is already considered "
926                "to be open. This "
927                "seems odd.");
928   }
930   RefPtr<nsIContent> popupContent = GetMenuPopupContent();
932   if (mObserver && popupContent) {
933     mObserver->OnMenuWillOpen(popupContent->AsElement());
934   }
936   nsEventStatus status = nsEventStatus_eIgnore;
937   WidgetMouseEvent event(true, eXULPopupShowing, nullptr,
938                          WidgetMouseEvent::eReal);
940   nsresult rv = NS_OK;
941   RefPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
942   rv = EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
943   if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) {
944     return false;
945   }
947   DidFirePopupShowing();
949   return true;
952 void nsMenuX::DidFirePopupShowing() {
953   mDidFirePopupshowingAndIsApprovedToOpen = true;
955   // If the open is going to succeed we need to walk our menu items, checking to
956   // see if any of them have a command attribute. If so, several attributes
957   // must potentially be updated.
959   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
960   if (!popupContent) {
961     return;
962   }
964   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
965   if (pm) {
966     pm->UpdateMenuItems(popupContent->AsElement());
967   }
970 // Find the |menupopup| child in the |popup| representing this menu. It should
971 // be one of a very few children so we won't be iterating over a bazillion menu
972 // items to find it (so the strcmp won't kill us).
973 already_AddRefed<nsIContent> nsMenuX::GetMenuPopupContent() {
974   // Check to see if we are a "menupopup" node (if we are a native menu).
975   if (mContent->IsXULElement(nsGkAtoms::menupopup)) {
976     return do_AddRef(mContent);
977   }
979   // Otherwise check our child nodes.
981   for (RefPtr<nsIContent> child = mContent->GetFirstChild(); child;
982        child = child->GetNextSibling()) {
983     if (child->IsXULElement(nsGkAtoms::menupopup)) {
984       return child.forget();
985     }
986   }
988   return nullptr;
991 bool nsMenuX::IsXULHelpMenu(nsIContent* aMenuContent) {
992   bool retval = false;
993   if (aMenuContent && aMenuContent->IsElement()) {
994     nsAutoString id;
995     aMenuContent->AsElement()->GetAttr(nsGkAtoms::id, id);
996     if (id.Equals(u"helpMenu"_ns)) {
997       retval = true;
998     }
999   }
1000   return retval;
1003 bool nsMenuX::IsXULWindowMenu(nsIContent* aMenuContent) {
1004   bool retval = false;
1005   if (aMenuContent && aMenuContent->IsElement()) {
1006     nsAutoString id;
1007     aMenuContent->AsElement()->GetAttr(nsGkAtoms::id, id);
1008     if (id.Equals(u"windowMenu"_ns)) {
1009       retval = true;
1010     }
1011   }
1012   return retval;
1016 // nsChangeObserver
1019 void nsMenuX::ObserveAttributeChanged(dom::Document* aDocument,
1020                                       nsIContent* aContent,
1021                                       nsAtom* aAttribute) {
1022   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
1024   // ignore the |open| attribute, which is by far the most common
1025   if (gConstructingMenu || (aAttribute == nsGkAtoms::open)) {
1026     return;
1027   }
1029   if (aAttribute == nsGkAtoms::disabled) {
1030     SetEnabled(!mContent->AsElement()->AttrValueIs(
1031         kNameSpaceID_None, nsGkAtoms::disabled, nsGkAtoms::_true,
1032         eCaseMatters));
1033   } else if (aAttribute == nsGkAtoms::label) {
1034     mContent->AsElement()->GetAttr(nsGkAtoms::label, mLabel);
1035     NSString* newCocoaLabelString =
1036         nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
1037     mNativeMenu.title = newCocoaLabelString;
1038     mNativeMenuItem.title = newCocoaLabelString;
1039   } else if (aAttribute == nsGkAtoms::hidden ||
1040              aAttribute == nsGkAtoms::collapsed) {
1041     SetRebuild(true);
1043     bool newVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
1045     // don't do anything if the state is correct already
1046     if (newVisible == mVisible) {
1047       return;
1048     }
1050     mVisible = newVisible;
1051     if (mParent) {
1052       RefPtr<nsMenuX> self = this;
1053       mParent->MenuChildChangedVisibility(MenuChild(self), newVisible);
1054     }
1055     if (mVisible) {
1056       SetupIcon();
1057     }
1058   } else if (aAttribute == nsGkAtoms::image) {
1059     SetupIcon();
1060   }
1062   NS_OBJC_END_TRY_ABORT_BLOCK;
1065 void nsMenuX::ObserveContentRemoved(dom::Document* aDocument,
1066                                     nsIContent* aContainer,
1067                                     nsIContent* aChild) {
1068   if (gConstructingMenu) {
1069     return;
1070   }
1072   SetRebuild(true);
1073   mMenuGroupOwner->UnregisterForContentChanges(aChild);
1075   if (!mIsOpen) {
1076     // We will update the menu contents the next time the menu is opened.
1077     return;
1078   }
1080   // The menu is currently open. Remove the child from mMenuChildren and from
1081   // our NSMenu.
1082   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
1083   if (popupContent && aContainer == popupContent && aChild->IsElement()) {
1084     if (Maybe<MenuChild> child = GetItemForElement(aChild->AsElement())) {
1085       RemoveMenuChild(*child);
1086     }
1087   }
1090 void nsMenuX::ObserveContentInserted(dom::Document* aDocument,
1091                                      nsIContent* aContainer,
1092                                      nsIContent* aChild) {
1093   if (gConstructingMenu) {
1094     return;
1095   }
1097   SetRebuild(true);
1099   if (!mIsOpen) {
1100     // We will update the menu contents the next time the menu is opened.
1101     return;
1102   }
1104   // The menu is currently open. Insert the child into mMenuChildren and into
1105   // our NSMenu.
1106   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
1107   if (popupContent && aContainer == popupContent) {
1108     if (Maybe<MenuChild> child = CreateMenuChild(aChild)) {
1109       InsertMenuChild(std::move(*child));
1110     }
1111   }
1114 void nsMenuX::SetupIcon() {
1115   mIcon->SetupIcon(mContent);
1116   mNativeMenuItem.image = mIcon->GetIconImage();
1119 void nsMenuX::IconUpdated() {
1120   mNativeMenuItem.image = mIcon->GetIconImage();
1121   if (mIconListener) {
1122     mIconListener->IconUpdated();
1123   }
1126 void nsMenuX::MenuChildChangedVisibility(const MenuChild& aChild,
1127                                          bool aIsVisible) {
1128   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
1130   NSMenuItem* nativeItem = aChild.match(
1131       [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
1132       [](const RefPtr<nsMenuItemX>& aMenuItem) {
1133         return aMenuItem->NativeNSMenuItem();
1134       });
1135   if (aIsVisible) {
1136     MOZ_RELEASE_ASSERT(
1137         !nativeItem.menu,
1138         "The native item should not be in a menu while it is hidden");
1139     RemovePlaceholderIfPresent();
1140     NSInteger insertionPoint = CalculateNativeInsertionPoint(aChild);
1141     [mNativeMenu insertItem:nativeItem atIndex:insertionPoint];
1142     mVisibleItemsCount++;
1143   } else {
1144     MOZ_RELEASE_ASSERT(
1145         [mNativeMenu indexOfItem:nativeItem] != -1,
1146         "The native item should be in this menu while it is visible");
1147     [mNativeMenu removeItem:nativeItem];
1148     mVisibleItemsCount--;
1149     InsertPlaceholderIfNeeded();
1150   }
1152   NS_OBJC_END_TRY_ABORT_BLOCK;
1155 NSInteger nsMenuX::CalculateNativeInsertionPoint(const MenuChild& aChild) {
1156   NSInteger insertionPoint = 0;
1157   for (auto& currItem : mMenuChildren) {
1158     // Using GetItemAt instead of GetVisibleItemAt to avoid O(N^2)
1159     if (currItem == aChild) {
1160       return insertionPoint;
1161     }
1162     NSMenuItem* nativeItem = currItem.match(
1163         [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
1164         [](const RefPtr<nsMenuItemX>& aMenuItem) {
1165           return aMenuItem->NativeNSMenuItem();
1166         });
1167     // Only count visible items.
1168     if (nativeItem.menu) {
1169       insertionPoint++;
1170     }
1171   }
1172   return insertionPoint;
1175 void nsMenuX::Dump(uint32_t aIndent) const {
1176   printf(
1177       "%*s - menu [%p] %-16s <%s>", aIndent * 2, "", this,
1178       mLabel.IsEmpty() ? "(empty label)" : NS_ConvertUTF16toUTF8(mLabel).get(),
1179       NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
1180   if (mNeedsRebuild) {
1181     printf(" [NeedsRebuild]");
1182   }
1183   if (mIsOpen) {
1184     printf(" [Open]");
1185   }
1186   if (mVisible) {
1187     printf(" [Visible]");
1188   }
1189   if (mIsEnabled) {
1190     printf(" [IsEnabled]");
1191   }
1192   printf(" (%d visible items)", int(mVisibleItemsCount));
1193   printf("\n");
1194   for (const auto& subitem : mMenuChildren) {
1195     subitem.match(
1196         [=](const RefPtr<nsMenuX>& aMenu) { aMenu->Dump(aIndent + 1); },
1197         [=](const RefPtr<nsMenuItemX>& aMenuItem) {
1198           aMenuItem->Dump(aIndent + 1);
1199         });
1200   }
1204 // MenuDelegate Objective-C class, used to set up Carbon events
1207 @implementation MenuDelegate
1209 - (id)initWithGeckoMenu:(nsMenuX*)geckoMenu {
1210   if ((self = [super init])) {
1211     NS_ASSERTION(geckoMenu, "Cannot initialize native menu delegate with NULL "
1212                             "gecko menu! Will crash!");
1213     mGeckoMenu = geckoMenu;
1214     mBlocksToRunWhenOpen = [[NSMutableArray alloc] init];
1215   }
1216   return self;
1219 - (void)dealloc {
1220   [mBlocksToRunWhenOpen release];
1221   [super dealloc];
1224 - (void)runBlockWhenOpen:(void (^)())block {
1225   [mBlocksToRunWhenOpen addObject:[[block copy] autorelease]];
1228 - (void)menu:(NSMenu*)aMenu willHighlightItem:(NSMenuItem*)aItem {
1229   if (!aMenu || !mGeckoMenu) {
1230     return;
1231   }
1233   Maybe<uint32_t> index =
1234       aItem ? Some(static_cast<uint32_t>([aMenu indexOfItem:aItem]))
1235             : Nothing();
1236   mGeckoMenu->OnHighlightedItemChanged(index);
1239 - (void)menuWillOpen:(NSMenu*)menu {
1240   for (void (^block)() in mBlocksToRunWhenOpen) {
1241     block();
1242   }
1243   [mBlocksToRunWhenOpen removeAllObjects];
1245   if (!mGeckoMenu) {
1246     return;
1247   }
1249   // Don't do anything while the OS is (re)indexing our menus (on Leopard and
1250   // higher).  This stops the Help menu from being able to search in our
1251   // menus, but it also resolves many other problems.
1252   if (nsMenuX::sIndexingMenuLevel > 0) {
1253     return;
1254   }
1256   // Hold a strong reference to mGeckoMenu while calling its methods.
1257   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1258   geckoMenu->MenuOpened();
1261 - (void)menuDidClose:(NSMenu*)menu {
1262   if (!mGeckoMenu) {
1263     return;
1264   }
1266   // Don't do anything while the OS is (re)indexing our menus (on Leopard and
1267   // higher).  This stops the Help menu from being able to search in our
1268   // menus, but it also resolves many other problems.
1269   if (nsMenuX::sIndexingMenuLevel > 0) {
1270     return;
1271   }
1273   // Hold a strong reference to mGeckoMenu while calling its methods.
1274   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1275   geckoMenu->MenuClosed();
1278 // This is called after menuDidClose:.
1279 - (void)menu:(NSMenu*)aMenu willActivateItem:(NSMenuItem*)aItem {
1280   if (!mGeckoMenu) {
1281     return;
1282   }
1284   // Hold a strong reference to mGeckoMenu while calling its methods.
1285   RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1286   geckoMenu->OnWillActivateItem(aItem);
1289 @end
1291 // OS X Leopard (at least as of 10.5.2) has an obscure bug triggered by some
1292 // behavior that's present in Mozilla.org browsers but not (as best I can
1293 // tell) in Apple products like Safari.  (It's not yet clear exactly what this
1294 // behavior is.)
1296 // The bug is that sometimes you crash on quit in nsMenuX::RemoveAll(), on a
1297 // call to [NSMenu removeItemAtIndex:].  The crash is caused by trying to
1298 // access a deleted NSMenuItem object (sometimes (perhaps always?) by trying
1299 // to send it a _setChangedFlags: message).  Though this object was deleted
1300 // some time ago, it remains registered as a potential target for a particular
1301 // key equivalent.  So when [NSMenu removeItemAtIndex:] removes the current
1302 // target for that same key equivalent, the OS tries to "activate" the
1303 // previous target.
1305 // The underlying reason appears to be that NSMenu's _addItem:toTable: and
1306 // _removeItem:fromTable: methods (which are used to keep a hashtable of
1307 // registered key equivalents) don't properly "retain" and "release"
1308 // NSMenuItem objects as they are added to and removed from the hashtable.
1310 // Our (hackish) workaround is to shadow the OS's hashtable with another
1311 // hastable of our own (gShadowKeyEquivDB), and use it to "retain" and
1312 // "release" NSMenuItem objects as needed.  This resolves bmo bugs 422287 and
1313 // 423669.  When (if) Apple fixes this bug, we can remove this workaround.
1315 static NSMutableDictionary* gShadowKeyEquivDB = nil;
1317 // Class for values in gShadowKeyEquivDB.
1319 @interface KeyEquivDBItem : NSObject {
1320   NSMenuItem* mItem;
1321   NSMutableSet* mTables;
1324 - (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable;
1325 - (BOOL)hasTable:(NSMapTable*)aTable;
1326 - (int)addTable:(NSMapTable*)aTable;
1327 - (int)removeTable:(NSMapTable*)aTable;
1329 @end
1331 @implementation KeyEquivDBItem
1333 - (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable {
1334   if (!gShadowKeyEquivDB) {
1335     gShadowKeyEquivDB = [[NSMutableDictionary alloc] init];
1336   }
1337   self = [super init];
1338   if (aItem && aTable) {
1339     mTables = [[NSMutableSet alloc] init];
1340     mItem = [aItem retain];
1341     [mTables addObject:[NSValue valueWithPointer:aTable]];
1342   } else {
1343     mTables = nil;
1344     mItem = nil;
1345   }
1346   return self;
1349 - (void)dealloc {
1350   if (mTables) {
1351     [mTables release];
1352   }
1353   if (mItem) {
1354     [mItem release];
1355   }
1356   [super dealloc];
1359 - (BOOL)hasTable:(NSMapTable*)aTable {
1360   return [mTables member:[NSValue valueWithPointer:aTable]] ? YES : NO;
1363 // Does nothing if aTable (its index value) is already present in mTables.
1364 - (int)addTable:(NSMapTable*)aTable {
1365   if (aTable) {
1366     [mTables addObject:[NSValue valueWithPointer:aTable]];
1367   }
1368   return [mTables count];
1371 - (int)removeTable:(NSMapTable*)aTable {
1372   if (aTable) {
1373     NSValue* objectToRemove =
1374         [mTables member:[NSValue valueWithPointer:aTable]];
1375     if (objectToRemove) {
1376       [mTables removeObject:objectToRemove];
1377     }
1378   }
1379   return [mTables count];
1382 @end
1384 @interface NSMenu (MethodSwizzling)
1385 + (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable;
1386 + (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem
1387                         fromTable:(NSMapTable*)aTable;
1388 @end
1390 @implementation NSMenu (MethodSwizzling)
1392 + (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable {
1393   if (aItem && aTable) {
1394     NSValue* key = [NSValue valueWithPointer:aItem];
1395     KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
1396     if (shadowItem) {
1397       [shadowItem addTable:aTable];
1398     } else {
1399       shadowItem = [[KeyEquivDBItem alloc] initWithItem:aItem table:aTable];
1400       [gShadowKeyEquivDB setObject:shadowItem forKey:key];
1401       // Release after [NSMutableDictionary setObject:forKey:] retains it (so
1402       // that it will get dealloced when removeObjectForKey: is called).
1403       [shadowItem release];
1404     }
1405   }
1407   [self nsMenuX_NSMenu_addItem:aItem toTable:aTable];
1410 + (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem
1411                         fromTable:(NSMapTable*)aTable {
1412   [self nsMenuX_NSMenu_removeItem:aItem fromTable:aTable];
1414   if (aItem && aTable) {
1415     NSValue* key = [NSValue valueWithPointer:aItem];
1416     KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
1417     if (shadowItem && [shadowItem hasTable:aTable]) {
1418       if (![shadowItem removeTable:aTable]) {
1419         [gShadowKeyEquivDB removeObjectForKey:key];
1420       }
1421     }
1422   }
1425 @end
1427 // This class is needed to keep track of when the OS is (re)indexing all of
1428 // our menus.  This appears to only happen on Leopard and higher, and can
1429 // be triggered by opening the Help menu.  Some operations are unsafe while
1430 // this is happening -- notably the calls to [[NSImage alloc]
1431 // initWithSize:imageRect.size] and [newImage lockFocus] in nsMenuItemIconX::
1432 // OnStopFrame().  But we don't yet have a complete list, and Apple doesn't
1433 // yet have any documentation on this subject.  (Apple also doesn't yet have
1434 // any documented way to find the information we seek here.)  The "original"
1435 // of this class (the one whose indexMenuBarDynamically method we hook) is
1436 // defined in the Shortcut framework in /System/Library/PrivateFrameworks.
1437 @interface NSObject (SCTGRLIndexMethodSwizzling)
1438 - (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically;
1439 @end
1441 @implementation NSObject (SCTGRLIndexMethodSwizzling)
1443 - (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically {
1444   // This method appears to be called (once) whenever the OS (re)indexes our
1445   // menus.  sIndexingMenuLevel is a int32_t just in case it might be
1446   // reentered.  As it's running, it spawns calls to two undocumented
1447   // HIToolbox methods (_SimulateMenuOpening() and _SimulateMenuClosed()),
1448   // which "simulate" the opening and closing of our menus without actually
1449   // displaying them.
1450   ++nsMenuX::sIndexingMenuLevel;
1451   [self nsMenuX_SCTGRLIndex_indexMenuBarDynamically];
1452   --nsMenuX::sIndexingMenuLevel;
1455 @end
1457 @interface NSObject (NSServicesMenuUpdaterSwizzling)
1458 - (void)nsMenuX_populateMenu:(NSMenu*)aMenu
1459           withServiceEntries:(NSArray*)aServices
1460                   forDisplay:(BOOL)aForDisplay;
1461 @end
1463 @interface _NSServiceEntry : NSObject
1464 - (NSString*)bundleIdentifier;
1465 @end
1467 @implementation NSObject (NSServicesMenuUpdaterSwizzling)
1469 - (void)nsMenuX_populateMenu:(NSMenu*)aMenu
1470           withServiceEntries:(NSArray*)aServices
1471                   forDisplay:(BOOL)aForDisplay {
1472   NSMutableArray* filteredServices = [NSMutableArray array];
1474   // We need to filter some services, such as "Search with Google", since this
1475   // service is duplicating functionality already exposed by our "Search Google
1476   // for..." context menu entry and because it opens in Safari, which can cause
1477   // confusion for users.
1478   for (_NSServiceEntry* service in aServices) {
1479     NSString* bundleId = [service bundleIdentifier];
1480     NSString* msg = [service valueForKey:@"message"];
1481     bool shouldSkip = ([bundleId isEqualToString:@"com.apple.Safari"]) ||
1482                       ([bundleId isEqualToString:@"com.apple.systemuiserver"] &&
1483                        [msg isEqualToString:@"openURL"]);
1484     if (!shouldSkip) {
1485       [filteredServices addObject:service];
1486     }
1487   }
1489   [self nsMenuX_populateMenu:aMenu
1490           withServiceEntries:filteredServices
1491                   forDisplay:aForDisplay];
1494 @end