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/. */
5
6#include "nsMenuX.h"
7
8#include <_types/_uint32_t.h>
9#include <dlfcn.h>
10
11#include "mozilla/dom/Document.h"
12#include "mozilla/dom/ScriptSettings.h"
13#include "mozilla/EventDispatcher.h"
14#include "mozilla/MouseEvents.h"
15
16#include "MOZMenuOpeningCoordinator.h"
17#include "nsMenuItemX.h"
18#include "nsMenuUtilsX.h"
19#include "nsMenuItemIconX.h"
20
21#include "nsObjCExceptions.h"
22
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 "plstr.h"
33#include "nsGkAtoms.h"
34#include "nsCRT.h"
35#include "nsBaseWidget.h"
36
37#include "nsIContent.h"
38#include "nsIDocumentObserver.h"
39#include "nsIComponentManager.h"
40#include "nsIRollupListener.h"
41#include "nsIServiceManager.h"
42#include "nsXULPopupManager.h"
43
44using namespace mozilla;
45using namespace mozilla::dom;
46
47static bool gConstructingMenu = false;
48static bool gMenuMethodsSwizzled = false;
49
50int32_t nsMenuX::sIndexingMenuLevel = 0;
51
52// TODO: It is unclear whether this is still needed.
53static void SwizzleDynamicIndexingMethods() {
54  if (gMenuMethodsSwizzled) {
55    return;
56  }
57
58  nsToolkit::SwizzleMethods([NSMenu class], @selector(_addItem:toTable:),
59                            @selector(nsMenuX_NSMenu_addItem:toTable:), true);
60  nsToolkit::SwizzleMethods([NSMenu class], @selector(_removeItem:fromTable:),
61                            @selector(nsMenuX_NSMenu_removeItem:fromTable:), 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", RTLD_LAZY);
67  Class SCTGRLIndexClass = ::NSClassFromString(@"SCTGRLIndex");
68  nsToolkit::SwizzleMethods(SCTGRLIndexClass, @selector(indexMenuBarDynamically),
69                            @selector(nsMenuX_SCTGRLIndex_indexMenuBarDynamically));
70
71  gMenuMethodsSwizzled = true;
72}
73
74//
75// nsMenuX
76//
77
78nsMenuX::nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aContent)
79    : mContent(aContent), mParent(aParent), mMenuGroupOwner(aMenuGroupOwner) {
80  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
81
82  MOZ_COUNT_CTOR(nsMenuX);
83
84  SwizzleDynamicIndexingMethods();
85
86  mMenuDelegate = [[MenuDelegate alloc] initWithGeckoMenu:this];
87  mMenuDelegate.menuIsInMenubar = mMenuGroupOwner->GetMenuBar() != nullptr;
88
89  if (!nsMenuBarX::sNativeEventTarget) {
90    nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
91  }
92
93  if (mContent->IsElement()) {
94    mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, mLabel);
95  }
96  mNativeMenu = CreateMenuWithGeckoString(mLabel);
97
98  // register this menu to be notified when changes are made to our content object
99  NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one");
100  mMenuGroupOwner->RegisterForContentChanges(mContent, this);
101
102  mVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
103
104  NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
105  mNativeMenuItem = [[NSMenuItem alloc] initWithTitle:newCocoaLabelString
106                                               action:nil
107                                        keyEquivalent:@""];
108  mNativeMenuItem.submenu = mNativeMenu;
109
110  SetEnabled(!mContent->IsElement() ||
111             !mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
112                                                 nsGkAtoms::_true, eCaseMatters));
113
114  // We call RebuildMenu here because keyboard commands are dependent upon
115  // native menu items being created. If we only call RebuildMenu when a menu
116  // is actually selected, then we can't access keyboard commands until the
117  // menu gets selected, which is bad.
118  RebuildMenu();
119
120  mIcon = MakeUnique<nsMenuItemIconX>(this);
121
122  if (mVisible) {
123    SetupIcon();
124  }
125
126  NS_OBJC_END_TRY_ABORT_BLOCK;
127}
128
129nsMenuX::~nsMenuX() {
130  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
131
132  // Make sure a pending popupshown event isn't dropped.
133  FlushMenuOpenedRunnable();
134
135  if (mIsOpen) {
136    [mNativeMenu cancelTracking];
137    MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
138  }
139
140  // Make sure pending popuphiding/popuphidden events aren't dropped.
141  FlushMenuClosedRunnable();
142
143  OnHighlightedItemChanged(Nothing());
144  RemoveAll();
145
146  mNativeMenu.delegate = nil;
147  [mNativeMenu release];
148  [mMenuDelegate release];
149  // autorelease the native menu item so that anything else happening to this
150  // object happens before the native menu item actually dies
151  [mNativeMenuItem autorelease];
152
153  DetachFromGroupOwnerRecursive();
154
155  MOZ_COUNT_DTOR(nsMenuX);
156
157  NS_OBJC_END_TRY_ABORT_BLOCK;
158}
159
160void nsMenuX::DetachFromGroupOwnerRecursive() {
161  if (!mMenuGroupOwner) {
162    // Don't recurse if this subtree is already detached.
163    // This avoids repeated recursion during the destruction of nested nsMenuX structures.
164    // Our invariant is: If we are detached, all of our contents are also detached.
165    return;
166  }
167
168  if (mMenuGroupOwner && mContent) {
169    mMenuGroupOwner->UnregisterForContentChanges(mContent);
170  }
171  mMenuGroupOwner = nullptr;
172
173  // Also detach all our children.
174  for (auto& child : mMenuChildren) {
175    child.match([](const RefPtr<nsMenuX>& aMenu) { aMenu->DetachFromGroupOwnerRecursive(); },
176                [](const RefPtr<nsMenuItemX>& aMenuItem) { aMenuItem->DetachFromGroupOwner(); });
177  }
178}
179
180void nsMenuX::OnMenuWillOpen(dom::Element* aPopupElement) {
181  RefPtr<nsMenuX> kungFuDeathGrip(this);
182  if (mObserver) {
183    mObserver->OnMenuWillOpen(aPopupElement);
184  }
185}
186
187void nsMenuX::OnMenuDidOpen(dom::Element* aPopupElement) {
188  RefPtr<nsMenuX> kungFuDeathGrip(this);
189  if (mObserver) {
190    mObserver->OnMenuDidOpen(aPopupElement);
191  }
192}
193
194void nsMenuX::OnMenuWillActivateItem(dom::Element* aPopupElement, dom::Element* aMenuItemElement) {
195  RefPtr<nsMenuX> kungFuDeathGrip(this);
196  if (mObserver) {
197    mObserver->OnMenuWillActivateItem(aPopupElement, aMenuItemElement);
198  }
199}
200
201void nsMenuX::OnMenuClosed(dom::Element* aPopupElement) {
202  RefPtr<nsMenuX> kungFuDeathGrip(this);
203  if (mObserver) {
204    mObserver->OnMenuClosed(aPopupElement);
205  }
206}
207
208void nsMenuX::AddMenuChild(MenuChild&& aChild) {
209  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
210
211  WillInsertChild(aChild);
212  mMenuChildren.AppendElement(aChild);
213
214  bool isVisible =
215      aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
216                   [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); });
217  NSMenuItem* nativeItem = aChild.match(
218      [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
219      [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); });
220
221  if (isVisible) {
222    RemovePlaceholderIfPresent();
223    [mNativeMenu addItem:nativeItem];
224    ++mVisibleItemsCount;
225  }
226
227  NS_OBJC_END_TRY_ABORT_BLOCK;
228}
229
230void nsMenuX::InsertMenuChild(MenuChild&& aChild) {
231  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
232
233  WillInsertChild(aChild);
234  size_t insertionIndex = FindInsertionIndex(aChild);
235  mMenuChildren.InsertElementAt(insertionIndex, aChild);
236
237  bool isVisible =
238      aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
239                   [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); });
240  if (isVisible) {
241    MenuChildChangedVisibility(aChild, true);
242  }
243
244  NS_OBJC_END_TRY_ABORT_BLOCK;
245}
246
247void nsMenuX::RemoveMenuChild(const MenuChild& aChild) {
248  bool isVisible =
249      aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->IsVisible(); },
250                   [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->IsVisible(); });
251  if (isVisible) {
252    MenuChildChangedVisibility(aChild, false);
253  }
254
255  WillRemoveChild(aChild);
256  mMenuChildren.RemoveElement(aChild);
257}
258
259size_t nsMenuX::FindInsertionIndex(const MenuChild& aChild) {
260  nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
261  MOZ_RELEASE_ASSERT(menuPopup);
262
263  RefPtr<nsIContent> insertedContent =
264      aChild.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
265                   [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
266
267  MOZ_RELEASE_ASSERT(insertedContent->GetParent() == menuPopup);
268
269  // Iterate over menuPopup's children (insertedContent's siblings) until we encounter
270  // insertedContent. At the same time, keep track of the index in mMenuChildren.
271  size_t index = 0;
272  for (nsIContent* child = menuPopup->GetFirstChild(); child && index < mMenuChildren.Length();
273       child = child->GetNextSibling()) {
274    if (child == insertedContent) {
275      break;
276    }
277
278    RefPtr<nsIContent> contentAtIndex = mMenuChildren[index].match(
279        [](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
280        [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
281    if (child == contentAtIndex) {
282      index++;
283    }
284  }
285
286  return index;
287}
288
289// Includes all items, including hidden/collapsed ones
290uint32_t nsMenuX::GetItemCount() { return mMenuChildren.Length(); }
291
292// Includes all items, including hidden/collapsed ones
293mozilla::Maybe<nsMenuX::MenuChild> nsMenuX::GetItemAt(uint32_t aPos) {
294  if (aPos >= (uint32_t)mMenuChildren.Length()) {
295    return {};
296  }
297
298  return Some(mMenuChildren[aPos]);
299}
300
301// Only includes visible items
302nsresult nsMenuX::GetVisibleItemCount(uint32_t& aCount) {
303  aCount = mVisibleItemsCount;
304  return NS_OK;
305}
306
307// Only includes visible items. Note that this is provides O(N) access
308// If you need to iterate or search, consider using GetItemAt and doing your own filtering
309Maybe<nsMenuX::MenuChild> nsMenuX::GetVisibleItemAt(uint32_t aPos) {
310  uint32_t count = mMenuChildren.Length();
311  if (aPos >= mVisibleItemsCount || aPos >= count) {
312    return {};
313  }
314
315  // If there are no invisible items, can provide direct access
316  if (mVisibleItemsCount == count) {
317    return GetItemAt(aPos);
318  }
319
320  // Otherwise, traverse the array until we find the the item we're looking for.
321  uint32_t visibleNodeIndex = 0;
322  for (uint32_t i = 0; i < count; i++) {
323    MenuChild item = *GetItemAt(i);
324    RefPtr<nsIContent> content =
325        item.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
326                   [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
327    if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
328      if (aPos == visibleNodeIndex) {
329        // we found the visible node we're looking for, return it
330        return Some(item);
331      }
332      visibleNodeIndex++;
333    }
334  }
335
336  return {};
337}
338
339Maybe<nsMenuX::MenuChild> nsMenuX::GetItemForElement(Element* aMenuChildElement) {
340  for (auto& child : mMenuChildren) {
341    RefPtr<nsIContent> content =
342        child.match([](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
343                    [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->Content(); });
344    if (content == aMenuChildElement) {
345      return Some(child);
346    }
347  }
348  return {};
349}
350
351nsresult nsMenuX::RemoveAll() {
352  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
353
354  [mNativeMenu removeAllItems];
355
356  for (auto& child : mMenuChildren) {
357    WillRemoveChild(child);
358  }
359
360  mMenuChildren.Clear();
361  mVisibleItemsCount = 0;
362
363  return NS_OK;
364
365  NS_OBJC_END_TRY_ABORT_BLOCK;
366}
367
368void nsMenuX::WillInsertChild(const MenuChild& aChild) {
369  if (aChild.is<RefPtr<nsMenuX>>()) {
370    aChild.as<RefPtr<nsMenuX>>()->SetObserver(this);
371  }
372}
373
374void nsMenuX::WillRemoveChild(const MenuChild& aChild) {
375  aChild.match(
376      [](const RefPtr<nsMenuX>& aMenu) {
377        aMenu->DetachFromGroupOwnerRecursive();
378        aMenu->DetachFromParent();
379        aMenu->SetObserver(nullptr);
380      },
381      [](const RefPtr<nsMenuItemX>& aMenuItem) {
382        aMenuItem->DetachFromGroupOwner();
383        aMenuItem->DetachFromParent();
384      });
385}
386
387void nsMenuX::MenuOpened() {
388  if (mIsOpen) {
389    return;
390  }
391
392  // Make sure we fire any pending popupshown / popuphiding / popuphidden events first.
393  FlushMenuOpenedRunnable();
394  FlushMenuClosedRunnable();
395
396  if (!mDidFirePopupshowingAndIsApprovedToOpen) {
397    // Fire popupshowing now.
398    bool approvedToOpen = OnOpen();
399    if (!approvedToOpen) {
400      // We can only stop menus from opening which we open ourselves. We cannot stop menubar root
401      // menus or menu submenus from opening.
402      // For context menus, we can call OnOpen() before we ask the system to open the menu.
403      NS_WARNING("The popupshowing event had preventDefault() called on it, but in MenuOpened() it "
404                 "is too late to stop the menu from opening.");
405    }
406  }
407
408  mIsOpen = true;
409
410  // Reset mDidFirePopupshowingAndIsApprovedToOpen for the next menu opening.
411  mDidFirePopupshowingAndIsApprovedToOpen = false;
412
413  if (mNeedsRebuild) {
414    OnHighlightedItemChanged(Nothing());
415    RemoveAll();
416    RebuildMenu();
417  }
418
419  // Fire the popupshown event in MenuOpenedAsync.
420  // MenuOpened() is called during menuWillOpen, and if cancelTracking is called now, menuDidClose
421  // will not be called.
422  // The runnable object must not hold a strong reference to the nsMenuX, so that there is no
423  // reference cycle.
424  class MenuOpenedAsyncRunnable final : public mozilla::CancelableRunnable {
425   public:
426    explicit MenuOpenedAsyncRunnable(nsMenuX* aMenu)
427        : CancelableRunnable("MenuOpenedAsyncRunnable"), mMenu(aMenu) {}
428
429    nsresult Run() override {
430      if (mMenu) {
431        RefPtr<nsMenuX> menu = mMenu;
432        menu->MenuOpenedAsync();
433        mMenu = nullptr;
434      }
435      return NS_OK;
436    }
437    nsresult Cancel() override {
438      mMenu = nullptr;
439      return NS_OK;
440    }
441
442   private:
443    nsMenuX* mMenu;  // weak, cleared by Cancel() and Run()
444  };
445  mPendingAsyncMenuOpenRunnable = new MenuOpenedAsyncRunnable(this);
446  NS_DispatchToCurrentThread(mPendingAsyncMenuOpenRunnable);
447}
448
449void nsMenuX::FlushMenuOpenedRunnable() {
450  if (mPendingAsyncMenuOpenRunnable) {
451    MenuOpenedAsync();
452  }
453}
454
455void nsMenuX::MenuOpenedAsync() {
456  if (mPendingAsyncMenuOpenRunnable) {
457    mPendingAsyncMenuOpenRunnable->Cancel();
458    mPendingAsyncMenuOpenRunnable = nullptr;
459  }
460
461  mIsOpenForGecko = true;
462
463  // Open the node.
464  if (mContent->IsElement()) {
465    mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, true);
466  }
467
468  nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
469
470  // Notify our observer.
471  if (mObserver && popupContent) {
472    mObserver->OnMenuDidOpen(popupContent->AsElement());
473  }
474
475  // Fire popupshown.
476  nsEventStatus status = nsEventStatus_eIgnore;
477  WidgetMouseEvent event(true, eXULPopupShown, nullptr, WidgetMouseEvent::eReal);
478  nsIContent* dispatchTo = popupContent ? popupContent : mContent;
479  EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
480}
481
482void nsMenuX::MenuClosed(bool aEntireMenuClosingDueToActivateItem) {
483  if (!mIsOpen) {
484    return;
485  }
486
487  // Make sure we fire any pending popupshown events first.
488  FlushMenuOpenedRunnable();
489
490  // If any of our submenus were opened programmatically, make sure they get closed first.
491  for (auto& child : mMenuChildren) {
492    if (child.is<RefPtr<nsMenuX>>()) {
493      child.as<RefPtr<nsMenuX>>()->MenuClosed(aEntireMenuClosingDueToActivateItem);
494    }
495  }
496
497  mIsOpen = false;
498
499  // Do the rest of the MenuClosed work in MenuClosedAsync.
500  // MenuClosed() is called from -[NSMenuDelegate menuDidClose:]. If a menuitem was clicked,
501  // menuDidClose is called *before* menuItemHit for the clicked menu item is called.
502  // This runnable will be canceled if ~nsMenuX runs before the runnable.
503  // The runnable object must not hold a strong reference to the nsMenuX, so that there is no
504  // reference cycle.
505  class MenuClosedAsyncRunnable final : public mozilla::CancelableRunnable {
506   public:
507    explicit MenuClosedAsyncRunnable(nsMenuX* aMenu)
508        : CancelableRunnable("MenuClosedAsyncRunnable"), mMenu(aMenu) {}
509
510    nsresult Run() override {
511      if (mMenu) {
512        RefPtr<nsMenuX> menu = mMenu;
513        menu->MenuClosedAsync();
514        mMenu = nullptr;
515      }
516      return NS_OK;
517    }
518    nsresult Cancel() override {
519      mMenu = nullptr;
520      return NS_OK;
521    }
522
523   private:
524    nsMenuX* mMenu;  // weak, cleared by Cancel() and Run()
525  };
526
527  mPendingAsyncMenuCloseRunnable = new MenuClosedAsyncRunnable(this);
528
529  if (aEntireMenuClosingDueToActivateItem) {
530    // Delay the call to MenuClosedAsync until after the menu's event loop has been exited, by using
531    // -[MOZMenuOpeningCoordinator runAfterMenuClosed:]. Otherwise, the runnable might potentially
532    // run before the event loop has been exited, and MenuClosedAsync() would flush the pending
533    // command runnable for the menu activation, and then the command event would run inside the
534    // menu's event loop which is what we're trying to avoid.
535    [MOZMenuOpeningCoordinator.sharedInstance runAfterMenuClosed:mPendingAsyncMenuCloseRunnable];
536  } else {
537    // Just dispatch to the Gecko event queue.
538    // One way to get here is if a submenu is closed but the rest of the menu stays open; in that
539    // case, we really can't use runAfterMenuClosed because the submenu's MenuClosedAsync method
540    // would run way too late.
541    NS_DispatchToCurrentThread(mPendingAsyncMenuCloseRunnable);
542  }
543}
544
545void nsMenuX::FlushMenuClosedRunnable() {
546  // If any of our submenus have a pending menu closed runnable, make sure those run first.
547  for (auto& child : mMenuChildren) {
548    if (child.is<RefPtr<nsMenuX>>()) {
549      child.as<RefPtr<nsMenuX>>()->FlushMenuClosedRunnable();
550    }
551  }
552
553  if (mPendingAsyncMenuCloseRunnable) {
554    MenuClosedAsync();
555  }
556}
557
558void nsMenuX::MenuClosedAsync() {
559  if (mPendingAsyncMenuCloseRunnable) {
560    mPendingAsyncMenuCloseRunnable->Cancel();
561    mPendingAsyncMenuCloseRunnable = nullptr;
562  }
563
564  // If we have pending command events, run those first.
565  nsTArray<RefPtr<Runnable>> runnables = std::move(mPendingCommandRunnables);
566  for (auto& runnable : runnables) {
567    runnable->Run();
568  }
569
570  // Make sure no item is highlighted.
571  OnHighlightedItemChanged(Nothing());
572
573  nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
574  nsCOMPtr<nsIContent> dispatchTo = popupContent ? popupContent : mContent;
575
576  nsEventStatus status = nsEventStatus_eIgnore;
577  WidgetMouseEvent popupHiding(true, eXULPopupHiding, nullptr, WidgetMouseEvent::eReal);
578  EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHiding, nullptr, &status);
579
580  mIsOpenForGecko = false;
581
582  if (mContent->IsElement()) {
583    mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open, true);
584  }
585
586  WidgetMouseEvent popupHidden(true, eXULPopupHidden, nullptr, WidgetMouseEvent::eReal);
587  EventDispatcher::Dispatch(dispatchTo, nullptr, &popupHidden, nullptr, &status);
588
589  // Notify our observer.
590  if (mObserver && popupContent) {
591    mObserver->OnMenuClosed(popupContent->AsElement());
592  }
593}
594
595void nsMenuX::ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem, NSEventModifierFlags aModifiers,
596                                       int16_t aButton) {
597  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
598
599  class DoCommandRunnable final : public mozilla::Runnable {
600   public:
601    explicit DoCommandRunnable(RefPtr<nsMenuItemX>&& aItem, NSEventModifierFlags aModifiers,
602                               int16_t aButton)
603        : Runnable("DoCommandRunnable"),
604          mMenuItem(aItem),
605          mModifiers(aModifiers),
606          mButton(aButton) {}
607
608    nsresult Run() override {
609      if (mMenuItem) {
610        RefPtr<nsMenuItemX> menuItem = std::move(mMenuItem);
611        menuItem->DoCommand(mModifiers, mButton);
612      }
613      return NS_OK;
614    }
615
616   private:
617    RefPtr<nsMenuItemX> mMenuItem;  // cleared by Run()
618    NSEventModifierFlags mModifiers;
619    int16_t mButton;
620  };
621  RefPtr<Runnable> doCommandAsync = new DoCommandRunnable(std::move(aItem), aModifiers, aButton);
622  mPendingCommandRunnables.AppendElement(doCommandAsync);
623
624  // Delay the command event until after the menu's event loop has been exited, by using
625  // -[MOZMenuOpeningCoordinator runAfterMenuClosed:]. Otherwise, the runnable might potentially
626  // run inside the menu's nested event loop, and command event handlers can do arbitrary things
627  // like opening modal windows which spawn more nested event loops. This repeated nesting of event
628  // loops is something we'd like to avoid.
629  [MOZMenuOpeningCoordinator.sharedInstance runAfterMenuClosed:std::move(doCommandAsync)];
630
631  NS_OBJC_END_TRY_ABORT_BLOCK;
632}
633
634bool nsMenuX::Close() {
635  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
636
637  if (mDidFirePopupshowingAndIsApprovedToOpen && !mIsOpen) {
638    // Close is being called right after this menu was opened, but before MenuOpened() had a chance
639    // to run. Call it here so that we can go through the entire popupshown -> popuphiding ->
640    // popuphidden sequence. Some callers expect to get a popuphidden event even if they close the
641    // popup before it was fully open.
642    MenuOpened();
643  }
644
645  FlushMenuOpenedRunnable();
646
647  bool wasOpen = mIsOpenForGecko;
648
649  if (mIsOpen) {
650    // Close the menu.
651    // We usually don't get here during normal Firefox usage: If the user closes the menu by
652    // clicking an item, or by clicking outside the menu, or by pressing escape, then the menu gets
653    // closed by macOS, and not by a call to nsMenuX::Close().
654    // If we do get here, it's usually because we're running an automated test. Close the menu
655    // without the fade-out animation so that we don't unnecessarily slow down the automated tests.
656    [mNativeMenu cancelTrackingWithoutAnimation];
657    MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
658
659    // Handle closing synchronously.
660    MenuClosed();
661  }
662
663  FlushMenuClosedRunnable();
664
665  return wasOpen;
666
667  NS_OBJC_END_TRY_ABORT_BLOCK;
668}
669
670void nsMenuX::OnHighlightedItemChanged(const Maybe<uint32_t>& aNewHighlightedIndex) {
671  if (mHighlightedItemIndex == aNewHighlightedIndex) {
672    return;
673  }
674
675  if (mHighlightedItemIndex) {
676    Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*mHighlightedItemIndex);
677    if (target && target->is<RefPtr<nsMenuItemX>>()) {
678      bool handlerCalledPreventDefault;  // but we don't actually care
679      target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(u"DOMMenuItemInactive"_ns,
680                                                          &handlerCalledPreventDefault);
681    }
682  }
683  if (aNewHighlightedIndex) {
684    Maybe<nsMenuX::MenuChild> target = GetVisibleItemAt(*aNewHighlightedIndex);
685    if (target && target->is<RefPtr<nsMenuItemX>>()) {
686      bool handlerCalledPreventDefault;  // but we don't actually care
687      target->as<RefPtr<nsMenuItemX>>()->DispatchDOMEvent(u"DOMMenuItemActive"_ns,
688                                                          &handlerCalledPreventDefault);
689    }
690  }
691  mHighlightedItemIndex = aNewHighlightedIndex;
692}
693
694void nsMenuX::OnWillActivateItem(NSMenuItem* aItem) {
695  if (!mIsOpenForGecko) {
696    return;
697  }
698
699  if (mMenuGroupOwner && mObserver) {
700    nsMenuItemX* item = mMenuGroupOwner->GetMenuItemForCommandID(uint32_t(aItem.tag));
701    if (item && item->Content()->IsElement()) {
702      RefPtr<dom::Element> itemElement = item->Content()->AsElement();
703      if (nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent()) {
704        mObserver->OnMenuWillActivateItem(popupContent->AsElement(), itemElement);
705      }
706    }
707  }
708}
709
710// Flushes style.
711static NSUserInterfaceLayoutDirection DirectionForElement(dom::Element* aElement) {
712  // Get the direction from the computed style so that inheritance into submenus is respected.
713  // aElement may not have a frame.
714  RefPtr<ComputedStyle> sc = nsComputedDOMStyle::GetComputedStyle(aElement, nullptr);
715  if (!sc) {
716    return NSApp.userInterfaceLayoutDirection;
717  }
718
719  switch (sc->StyleVisibility()->mDirection) {
720    case StyleDirection::Ltr:
721      return NSUserInterfaceLayoutDirectionLeftToRight;
722    case StyleDirection::Rtl:
723      return NSUserInterfaceLayoutDirectionRightToLeft;
724  }
725}
726
727void nsMenuX::RebuildMenu() {
728  MOZ_RELEASE_ASSERT(mNeedsRebuild);
729  gConstructingMenu = true;
730
731  // Retrieve our menupopup.
732  nsCOMPtr<nsIContent> menuPopup = GetMenuPopupContent();
733  if (!menuPopup) {
734    gConstructingMenu = false;
735    return;
736  }
737
738  if (menuPopup->IsElement()) {
739    mNativeMenu.userInterfaceLayoutDirection = DirectionForElement(menuPopup->AsElement());
740  }
741
742  // Iterate over the kids
743  for (nsIContent* child = menuPopup->GetFirstChild(); child; child = child->GetNextSibling()) {
744    if (Maybe<MenuChild> menuChild = CreateMenuChild(child)) {
745      AddMenuChild(std::move(*menuChild));
746    }
747  }  // for each menu item
748
749  InsertPlaceholderIfNeeded();
750
751  gConstructingMenu = false;
752  mNeedsRebuild = false;
753}
754
755void nsMenuX::InsertPlaceholderIfNeeded() {
756  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
757
758  if ([mNativeMenu numberOfItems] == 0) {
759    MOZ_RELEASE_ASSERT(mVisibleItemsCount == 0);
760    NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
761    item.enabled = NO;
762    item.view = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 150, 1)] autorelease];
763    [mNativeMenu addItem:item];
764    [item release];
765  }
766
767  NS_OBJC_END_TRY_ABORT_BLOCK;
768}
769
770void nsMenuX::RemovePlaceholderIfPresent() {
771  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
772
773  if (mVisibleItemsCount == 0 && [mNativeMenu numberOfItems] == 1) {
774    // Remove the placeholder.
775    [mNativeMenu removeItemAtIndex:0];
776  }
777
778  NS_OBJC_END_TRY_ABORT_BLOCK;
779}
780
781void nsMenuX::SetRebuild(bool aNeedsRebuild) {
782  if (!gConstructingMenu) {
783    mNeedsRebuild = aNeedsRebuild;
784    if (mParent && mParent->AsMenuBar()) {
785      mParent->AsMenuBar()->SetNeedsRebuild();
786    }
787  }
788}
789
790nsresult nsMenuX::SetEnabled(bool aIsEnabled) {
791  if (aIsEnabled != mIsEnabled) {
792    // we always want to rebuild when this changes
793    mIsEnabled = aIsEnabled;
794    mNativeMenuItem.enabled = mIsEnabled;
795  }
796  return NS_OK;
797}
798
799nsresult nsMenuX::GetEnabled(bool* aIsEnabled) {
800  NS_ENSURE_ARG_POINTER(aIsEnabled);
801  *aIsEnabled = mIsEnabled;
802  return NS_OK;
803}
804
805GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& aMenuTitle) {
806  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
807
808  NSString* title = [NSString stringWithCharacters:(UniChar*)aMenuTitle.get()
809                                            length:aMenuTitle.Length()];
810  GeckoNSMenu* myMenu = [[GeckoNSMenu alloc] initWithTitle:title];
811  myMenu.delegate = mMenuDelegate;
812
813  // We don't want this menu to auto-enable menu items because then Cocoa
814  // overrides our decisions and things get incorrectly enabled/disabled.
815  myMenu.autoenablesItems = NO;
816
817  // Disable the Services item for now. Bug 660452 tracks turning this on for the appropriate menus.
818  myMenu.allowsContextMenuPlugIns = NO;
819
820  // we used to install Carbon event handlers here, but since NSMenu* doesn't
821  // create its underlying MenuRef until just before display, we delay until
822  // that happens. Now we install the event handlers when Cocoa notifies
823  // us that a menu is about to display - see the Cocoa MenuDelegate class.
824
825  return myMenu;
826
827  NS_OBJC_END_TRY_ABORT_BLOCK;
828}
829
830Maybe<nsMenuX::MenuChild> nsMenuX::CreateMenuChild(nsIContent* aContent) {
831  if (aContent->IsAnyOfXULElements(nsGkAtoms::menuitem, nsGkAtoms::menuseparator)) {
832    return Some(MenuChild(CreateMenuItem(aContent)));
833  }
834  if (aContent->IsXULElement(nsGkAtoms::menu)) {
835    return Some(MenuChild(MakeRefPtr<nsMenuX>(this, mMenuGroupOwner, aContent)));
836  }
837  return {};
838}
839
840RefPtr<nsMenuItemX> nsMenuX::CreateMenuItem(nsIContent* aMenuItemContent) {
841  MOZ_RELEASE_ASSERT(aMenuItemContent);
842
843  nsAutoString menuitemName;
844  if (aMenuItemContent->IsElement()) {
845    aMenuItemContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, menuitemName);
846  }
847
848  EMenuItemType itemType = eRegularMenuItemType;
849  if (aMenuItemContent->IsXULElement(nsGkAtoms::menuseparator)) {
850    itemType = eSeparatorMenuItemType;
851  } else if (aMenuItemContent->IsElement()) {
852    static Element::AttrValuesArray strings[] = {nsGkAtoms::checkbox, nsGkAtoms::radio, nullptr};
853    switch (aMenuItemContent->AsElement()->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type,
854                                                           strings, eCaseMatters)) {
855      case 0:
856        itemType = eCheckboxMenuItemType;
857        break;
858      case 1:
859        itemType = eRadioMenuItemType;
860        break;
861    }
862  }
863
864  return MakeRefPtr<nsMenuItemX>(this, menuitemName, itemType, mMenuGroupOwner, aMenuItemContent);
865}
866
867// This menu is about to open. Returns false if the handler wants to stop the opening of the menu.
868bool nsMenuX::OnOpen() {
869  if (mDidFirePopupshowingAndIsApprovedToOpen) {
870    return true;
871  }
872
873  if (mIsOpen) {
874    NS_WARNING("nsMenuX::OnOpen() called while the menu is already considered to be open. This "
875               "seems odd.");
876  }
877
878  nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
879
880  if (mObserver && popupContent) {
881    mObserver->OnMenuWillOpen(popupContent->AsElement());
882  }
883
884  nsEventStatus status = nsEventStatus_eIgnore;
885  WidgetMouseEvent event(true, eXULPopupShowing, nullptr, WidgetMouseEvent::eReal);
886
887  nsresult rv = NS_OK;
888  nsIContent* dispatchTo = popupContent ? popupContent : mContent;
889  rv = EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
890  if (NS_FAILED(rv) || status == nsEventStatus_eConsumeNoDefault) {
891    return false;
892  }
893
894  DidFirePopupShowing();
895
896  return true;
897}
898
899void nsMenuX::DidFirePopupShowing() {
900  mDidFirePopupshowingAndIsApprovedToOpen = true;
901
902  // If the open is going to succeed we need to walk our menu items, checking to
903  // see if any of them have a command attribute. If so, several attributes
904  // must potentially be updated.
905
906  nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
907  if (!popupContent) {
908    return;
909  }
910
911  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
912  if (pm) {
913    pm->UpdateMenuItems(popupContent);
914  }
915}
916
917// Find the |menupopup| child in the |popup| representing this menu. It should be one
918// of a very few children so we won't be iterating over a bazillion menu items to find
919// it (so the strcmp won't kill us).
920already_AddRefed<nsIContent> nsMenuX::GetMenuPopupContent() {
921  // Check to see if we are a "menupopup" node (if we are a native menu).
922  if (mContent->IsXULElement(nsGkAtoms::menupopup)) {
923    return do_AddRef(mContent);
924  }
925
926  // Otherwise check our child nodes.
927
928  for (RefPtr<nsIContent> child = mContent->GetFirstChild(); child;
929       child = child->GetNextSibling()) {
930    if (child->IsXULElement(nsGkAtoms::menupopup)) {
931      return child.forget();
932    }
933  }
934
935  return nullptr;
936}
937
938bool nsMenuX::IsXULHelpMenu(nsIContent* aMenuContent) {
939  bool retval = false;
940  if (aMenuContent && aMenuContent->IsElement()) {
941    nsAutoString id;
942    aMenuContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id);
943    if (id.Equals(u"helpMenu"_ns)) {
944      retval = true;
945    }
946  }
947  return retval;
948}
949
950//
951// nsChangeObserver
952//
953
954void nsMenuX::ObserveAttributeChanged(dom::Document* aDocument, nsIContent* aContent,
955                                      nsAtom* aAttribute) {
956  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
957
958  // ignore the |open| attribute, which is by far the most common
959  if (gConstructingMenu || (aAttribute == nsGkAtoms::open)) {
960    return;
961  }
962
963  if (aAttribute == nsGkAtoms::disabled) {
964    SetEnabled(!mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
965                                                   nsGkAtoms::_true, eCaseMatters));
966  } else if (aAttribute == nsGkAtoms::label) {
967    mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, mLabel);
968    NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
969    mNativeMenu.title = newCocoaLabelString;
970    mNativeMenuItem.title = newCocoaLabelString;
971  } else if (aAttribute == nsGkAtoms::hidden || aAttribute == nsGkAtoms::collapsed) {
972    SetRebuild(true);
973
974    bool newVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
975
976    // don't do anything if the state is correct already
977    if (newVisible == mVisible) {
978      return;
979    }
980
981    mVisible = newVisible;
982    if (mParent) {
983      RefPtr<nsMenuX> self = this;
984      mParent->MenuChildChangedVisibility(MenuChild(self), newVisible);
985    }
986    if (mVisible) {
987      SetupIcon();
988    }
989  } else if (aAttribute == nsGkAtoms::image) {
990    SetupIcon();
991  }
992
993  NS_OBJC_END_TRY_ABORT_BLOCK;
994}
995
996void nsMenuX::ObserveContentRemoved(dom::Document* aDocument, nsIContent* aContainer,
997                                    nsIContent* aChild, nsIContent* aPreviousSibling) {
998  if (gConstructingMenu) {
999    return;
1000  }
1001
1002  SetRebuild(true);
1003  mMenuGroupOwner->UnregisterForContentChanges(aChild);
1004
1005  if (!mIsOpen) {
1006    // We will update the menu contents the next time the menu is opened.
1007    return;
1008  }
1009
1010  // The menu is currently open. Remove the child from mMenuChildren and from our NSMenu.
1011  nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
1012  if (popupContent && aContainer == popupContent && aChild->IsElement()) {
1013    if (Maybe<MenuChild> child = GetItemForElement(aChild->AsElement())) {
1014      RemoveMenuChild(*child);
1015    }
1016  }
1017}
1018
1019void nsMenuX::ObserveContentInserted(dom::Document* aDocument, nsIContent* aContainer,
1020                                     nsIContent* aChild) {
1021  if (gConstructingMenu) {
1022    return;
1023  }
1024
1025  SetRebuild(true);
1026
1027  if (!mIsOpen) {
1028    // We will update the menu contents the next time the menu is opened.
1029    return;
1030  }
1031
1032  // The menu is currently open. Insert the child into mMenuChildren and into our NSMenu.
1033  nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
1034  if (popupContent && aContainer == popupContent) {
1035    if (Maybe<MenuChild> child = CreateMenuChild(aChild)) {
1036      InsertMenuChild(std::move(*child));
1037    }
1038  }
1039}
1040
1041void nsMenuX::SetupIcon() {
1042  mIcon->SetupIcon(mContent);
1043  mNativeMenuItem.image = mIcon->GetIconImage();
1044}
1045
1046void nsMenuX::IconUpdated() {
1047  mNativeMenuItem.image = mIcon->GetIconImage();
1048  if (mIconListener) {
1049    mIconListener->IconUpdated();
1050  }
1051}
1052
1053void nsMenuX::MenuChildChangedVisibility(const MenuChild& aChild, bool aIsVisible) {
1054  NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
1055
1056  NSMenuItem* nativeItem = aChild.match(
1057      [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
1058      [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); });
1059  if (aIsVisible) {
1060    MOZ_RELEASE_ASSERT(!nativeItem.menu,
1061                       "The native item should not be in a menu while it is hidden");
1062    RemovePlaceholderIfPresent();
1063    NSInteger insertionPoint = CalculateNativeInsertionPoint(aChild);
1064    [mNativeMenu insertItem:nativeItem atIndex:insertionPoint];
1065    mVisibleItemsCount++;
1066  } else {
1067    MOZ_RELEASE_ASSERT([mNativeMenu indexOfItem:nativeItem] != -1,
1068                       "The native item should be in this menu while it is visible");
1069    [mNativeMenu removeItem:nativeItem];
1070    mVisibleItemsCount--;
1071    InsertPlaceholderIfNeeded();
1072  }
1073
1074  NS_OBJC_END_TRY_ABORT_BLOCK;
1075}
1076
1077NSInteger nsMenuX::CalculateNativeInsertionPoint(const MenuChild& aChild) {
1078  NSInteger insertionPoint = 0;
1079  for (auto& currItem : mMenuChildren) {
1080    // Using GetItemAt instead of GetVisibleItemAt to avoid O(N^2)
1081    if (currItem == aChild) {
1082      return insertionPoint;
1083    }
1084    NSMenuItem* nativeItem = currItem.match(
1085        [](const RefPtr<nsMenuX>& aMenu) { return aMenu->NativeNSMenuItem(); },
1086        [](const RefPtr<nsMenuItemX>& aMenuItem) { return aMenuItem->NativeNSMenuItem(); });
1087    // Only count visible items.
1088    if (nativeItem.menu) {
1089      insertionPoint++;
1090    }
1091  }
1092  return insertionPoint;
1093}
1094
1095void nsMenuX::Dump(uint32_t aIndent) const {
1096  printf("%*s - menu [%p] %-16s <%s>", aIndent * 2, "", this,
1097         mLabel.IsEmpty() ? "(empty label)" : NS_ConvertUTF16toUTF8(mLabel).get(),
1098         NS_ConvertUTF16toUTF8(mContent->NodeName()).get());
1099  if (mNeedsRebuild) {
1100    printf(" [NeedsRebuild]");
1101  }
1102  if (mIsOpen) {
1103    printf(" [Open]");
1104  }
1105  if (mVisible) {
1106    printf(" [Visible]");
1107  }
1108  if (mIsEnabled) {
1109    printf(" [IsEnabled]");
1110  }
1111  printf(" (%d visible items)", int(mVisibleItemsCount));
1112  printf("\n");
1113  for (const auto& subitem : mMenuChildren) {
1114    subitem.match([=](const RefPtr<nsMenuX>& aMenu) { aMenu->Dump(aIndent + 1); },
1115                  [=](const RefPtr<nsMenuItemX>& aMenuItem) { aMenuItem->Dump(aIndent + 1); });
1116  }
1117}
1118
1119//
1120// MenuDelegate Objective-C class, used to set up Carbon events
1121//
1122
1123@implementation MenuDelegate
1124
1125- (id)initWithGeckoMenu:(nsMenuX*)geckoMenu {
1126  if ((self = [super init])) {
1127    NS_ASSERTION(geckoMenu,
1128                 "Cannot initialize native menu delegate with NULL gecko menu! Will crash!");
1129    mGeckoMenu = geckoMenu;
1130    mBlocksToRunWhenOpen = [[NSMutableArray alloc] init];
1131  }
1132  return self;
1133}
1134
1135- (void)dealloc {
1136  [mBlocksToRunWhenOpen release];
1137  [super dealloc];
1138}
1139
1140- (void)runBlockWhenOpen:(void (^)())block {
1141  [mBlocksToRunWhenOpen addObject:[[block copy] autorelease]];
1142}
1143
1144- (void)menu:(NSMenu*)aMenu willHighlightItem:(NSMenuItem*)aItem {
1145  if (!aMenu || !mGeckoMenu) {
1146    return;
1147  }
1148
1149  Maybe<uint32_t> index =
1150      aItem ? Some(static_cast<uint32_t>([aMenu indexOfItem:aItem])) : Nothing();
1151  mGeckoMenu->OnHighlightedItemChanged(index);
1152}
1153
1154- (void)menuWillOpen:(NSMenu*)menu {
1155  for (void (^block)() in mBlocksToRunWhenOpen) {
1156    block();
1157  }
1158  [mBlocksToRunWhenOpen removeAllObjects];
1159
1160  if (!mGeckoMenu) {
1161    return;
1162  }
1163
1164  // Don't do anything while the OS is (re)indexing our menus (on Leopard and
1165  // higher).  This stops the Help menu from being able to search in our
1166  // menus, but it also resolves many other problems.
1167  if (nsMenuX::sIndexingMenuLevel > 0) {
1168    return;
1169  }
1170
1171  if (self.menuIsInMenubar) {
1172    // If a menu in the menubar is trying open while a non-native menu is open, roll up the
1173    // non-native menu and reject the menubar opening attempt, effectively consuming the event.
1174    nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
1175    if (rollupListener) {
1176      nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget();
1177      if (rollupWidget) {
1178        rollupListener->Rollup(0, true, nullptr, nullptr);
1179        [menu cancelTracking];
1180        return;
1181      }
1182    }
1183  }
1184
1185  // Hold a strong reference to mGeckoMenu while calling its methods.
1186  RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1187  geckoMenu->MenuOpened();
1188}
1189
1190- (void)menuDidClose:(NSMenu*)menu {
1191  if (!mGeckoMenu) {
1192    return;
1193  }
1194
1195  // Don't do anything while the OS is (re)indexing our menus (on Leopard and
1196  // higher).  This stops the Help menu from being able to search in our
1197  // menus, but it also resolves many other problems.
1198  if (nsMenuX::sIndexingMenuLevel > 0) {
1199    return;
1200  }
1201
1202  // Hold a strong reference to mGeckoMenu while calling its methods.
1203  RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1204  geckoMenu->MenuClosed();
1205}
1206
1207// This is called after menuDidClose:.
1208- (void)menu:(NSMenu*)aMenu willActivateItem:(NSMenuItem*)aItem {
1209  if (!mGeckoMenu) {
1210    return;
1211  }
1212
1213  // Hold a strong reference to mGeckoMenu while calling its methods.
1214  RefPtr<nsMenuX> geckoMenu = mGeckoMenu;
1215  geckoMenu->OnWillActivateItem(aItem);
1216}
1217
1218@end
1219
1220// OS X Leopard (at least as of 10.5.2) has an obscure bug triggered by some
1221// behavior that's present in Mozilla.org browsers but not (as best I can
1222// tell) in Apple products like Safari.  (It's not yet clear exactly what this
1223// behavior is.)
1224//
1225// The bug is that sometimes you crash on quit in nsMenuX::RemoveAll(), on a
1226// call to [NSMenu removeItemAtIndex:].  The crash is caused by trying to
1227// access a deleted NSMenuItem object (sometimes (perhaps always?) by trying
1228// to send it a _setChangedFlags: message).  Though this object was deleted
1229// some time ago, it remains registered as a potential target for a particular
1230// key equivalent.  So when [NSMenu removeItemAtIndex:] removes the current
1231// target for that same key equivalent, the OS tries to "activate" the
1232// previous target.
1233//
1234// The underlying reason appears to be that NSMenu's _addItem:toTable: and
1235// _removeItem:fromTable: methods (which are used to keep a hashtable of
1236// registered key equivalents) don't properly "retain" and "release"
1237// NSMenuItem objects as they are added to and removed from the hashtable.
1238//
1239// Our (hackish) workaround is to shadow the OS's hashtable with another
1240// hastable of our own (gShadowKeyEquivDB), and use it to "retain" and
1241// "release" NSMenuItem objects as needed.  This resolves bmo bugs 422287 and
1242// 423669.  When (if) Apple fixes this bug, we can remove this workaround.
1243
1244static NSMutableDictionary* gShadowKeyEquivDB = nil;
1245
1246// Class for values in gShadowKeyEquivDB.
1247
1248@interface KeyEquivDBItem : NSObject {
1249  NSMenuItem* mItem;
1250  NSMutableSet* mTables;
1251}
1252
1253- (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable;
1254- (BOOL)hasTable:(NSMapTable*)aTable;
1255- (int)addTable:(NSMapTable*)aTable;
1256- (int)removeTable:(NSMapTable*)aTable;
1257
1258@end
1259
1260@implementation KeyEquivDBItem
1261
1262- (id)initWithItem:(NSMenuItem*)aItem table:(NSMapTable*)aTable {
1263  if (!gShadowKeyEquivDB) {
1264    gShadowKeyEquivDB = [[NSMutableDictionary alloc] init];
1265  }
1266  self = [super init];
1267  if (aItem && aTable) {
1268    mTables = [[NSMutableSet alloc] init];
1269    mItem = [aItem retain];
1270    [mTables addObject:[NSValue valueWithPointer:aTable]];
1271  } else {
1272    mTables = nil;
1273    mItem = nil;
1274  }
1275  return self;
1276}
1277
1278- (void)dealloc {
1279  if (mTables) {
1280    [mTables release];
1281  }
1282  if (mItem) {
1283    [mItem release];
1284  }
1285  [super dealloc];
1286}
1287
1288- (BOOL)hasTable:(NSMapTable*)aTable {
1289  return [mTables member:[NSValue valueWithPointer:aTable]] ? YES : NO;
1290}
1291
1292// Does nothing if aTable (its index value) is already present in mTables.
1293- (int)addTable:(NSMapTable*)aTable {
1294  if (aTable) {
1295    [mTables addObject:[NSValue valueWithPointer:aTable]];
1296  }
1297  return [mTables count];
1298}
1299
1300- (int)removeTable:(NSMapTable*)aTable {
1301  if (aTable) {
1302    NSValue* objectToRemove = [mTables member:[NSValue valueWithPointer:aTable]];
1303    if (objectToRemove) {
1304      [mTables removeObject:objectToRemove];
1305    }
1306  }
1307  return [mTables count];
1308}
1309
1310@end
1311
1312@interface NSMenu (MethodSwizzling)
1313+ (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable;
1314+ (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem fromTable:(NSMapTable*)aTable;
1315@end
1316
1317@implementation NSMenu (MethodSwizzling)
1318
1319+ (void)nsMenuX_NSMenu_addItem:(NSMenuItem*)aItem toTable:(NSMapTable*)aTable {
1320  if (aItem && aTable) {
1321    NSValue* key = [NSValue valueWithPointer:aItem];
1322    KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
1323    if (shadowItem) {
1324      [shadowItem addTable:aTable];
1325    } else {
1326      shadowItem = [[KeyEquivDBItem alloc] initWithItem:aItem table:aTable];
1327      [gShadowKeyEquivDB setObject:shadowItem forKey:key];
1328      // Release after [NSMutableDictionary setObject:forKey:] retains it (so
1329      // that it will get dealloced when removeObjectForKey: is called).
1330      [shadowItem release];
1331    }
1332  }
1333
1334  [self nsMenuX_NSMenu_addItem:aItem toTable:aTable];
1335}
1336
1337+ (void)nsMenuX_NSMenu_removeItem:(NSMenuItem*)aItem fromTable:(NSMapTable*)aTable {
1338  [self nsMenuX_NSMenu_removeItem:aItem fromTable:aTable];
1339
1340  if (aItem && aTable) {
1341    NSValue* key = [NSValue valueWithPointer:aItem];
1342    KeyEquivDBItem* shadowItem = [gShadowKeyEquivDB objectForKey:key];
1343    if (shadowItem && [shadowItem hasTable:aTable]) {
1344      if (![shadowItem removeTable:aTable]) {
1345        [gShadowKeyEquivDB removeObjectForKey:key];
1346      }
1347    }
1348  }
1349}
1350
1351@end
1352
1353// This class is needed to keep track of when the OS is (re)indexing all of
1354// our menus.  This appears to only happen on Leopard and higher, and can
1355// be triggered by opening the Help menu.  Some operations are unsafe while
1356// this is happening -- notably the calls to [[NSImage alloc]
1357// initWithSize:imageRect.size] and [newImage lockFocus] in nsMenuItemIconX::
1358// OnStopFrame().  But we don't yet have a complete list, and Apple doesn't
1359// yet have any documentation on this subject.  (Apple also doesn't yet have
1360// any documented way to find the information we seek here.)  The "original"
1361// of this class (the one whose indexMenuBarDynamically method we hook) is
1362// defined in the Shortcut framework in /System/Library/PrivateFrameworks.
1363@interface NSObject (SCTGRLIndexMethodSwizzling)
1364- (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically;
1365@end
1366
1367@implementation NSObject (SCTGRLIndexMethodSwizzling)
1368
1369- (void)nsMenuX_SCTGRLIndex_indexMenuBarDynamically {
1370  // This method appears to be called (once) whenever the OS (re)indexes our
1371  // menus.  sIndexingMenuLevel is a int32_t just in case it might be
1372  // reentered.  As it's running, it spawns calls to two undocumented
1373  // HIToolbox methods (_SimulateMenuOpening() and _SimulateMenuClosed()),
1374  // which "simulate" the opening and closing of our menus without actually
1375  // displaying them.
1376  ++nsMenuX::sIndexingMenuLevel;
1377  [self nsMenuX_SCTGRLIndex_indexMenuBarDynamically];
1378  --nsMenuX::sIndexingMenuLevel;
1379}
1380
1381@end
1382