diff --git a/widget/cocoa/nsMenuX.h b/widget/cocoa/nsMenuX.h index 9b9068d8a81a65e0dd147be30f559d99d72f2318..91f1dbd6194cd82bb194be2548a8df268638c851 100644 --- a/widget/cocoa/nsMenuX.h +++ b/widget/cocoa/nsMenuX.h @@ -89,7 +89,6 @@ class nsMenuX final : public nsMenuParentX, // Called from the menu delegate during menuWillOpen, or to simulate opening. // Ignored if the menu is already considered open. - // Fires the popupshown event, if it hasn't been sent yet for this opening. // When calling this method, the caller must hold a strong reference to this object, because other // references to this object can be dropped during the handling of the DOM event. void MenuOpened(); @@ -171,6 +170,9 @@ class nsMenuX final : public nsMenuParentX, // number of visible previous siblings of aChild in mMenuChildren. NSInteger CalculateNativeInsertionPoint(nsMenuX* aChild); + // Fires the popupshown event. + void MenuOpenedAsync(); + // Called from mPendingAsyncMenuCloseRunnable asynchronously after MenuClosed(), so that it runs // after any potential menuItemHit calls for clicked menu items. // Fires popuphiding and popuphidden events. @@ -178,6 +180,10 @@ class nsMenuX final : public nsMenuParentX, // references to this object can be dropped during the handling of the DOM event. void MenuClosedAsync(); + // If mPendingAsyncMenuOpenRunnable is non-null, call MenuOpenedAsync() to send out the pending + // popupshown event. + void FlushMenuOpenedRunnable(); + // If mPendingAsyncMenuCloseRunnable is non-null, call MenuClosedAsync() to send out pending // popuphiding/popuphidden events. void FlushMenuClosedRunnable(); @@ -196,6 +202,9 @@ class nsMenuX final : public nsMenuParentX, Observer* mObserver = nullptr; // non-owning pointer to our observer + // Non-null between a call to MenuOpened() and MenuOpenedAsync(). + RefPtr<mozilla::CancelableRunnable> mPendingAsyncMenuOpenRunnable; + // Non-null between a call to MenuClosed() and MenuClosedAsync(). // This is asynchronous so that, if a menu item is clicked, we can fire popuphiding *after* we // execute the menu item command. The macOS menu system calls menuWillClose *before* it calls diff --git a/widget/cocoa/nsMenuX.mm b/widget/cocoa/nsMenuX.mm index c80024db5a1f3a3b0fb7993ad5caa1d02b597d79..43381f7838c18253d83ed9f880281484b4b74030 100644 --- a/widget/cocoa/nsMenuX.mm +++ b/widget/cocoa/nsMenuX.mm @@ -128,6 +128,9 @@ nsMenuX::nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsI nsMenuX::~nsMenuX() { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + // Make sure a pending popupshown event isn't dropped. + FlushMenuOpenedRunnable(); + if (mIsOpen) { [mNativeMenu cancelTracking]; } @@ -332,6 +335,10 @@ void nsMenuX::MenuOpened() { return; } + // Make sure we fire any pending popupshown / popuphiding / popuphidden events first. + FlushMenuOpenedRunnable(); + FlushMenuClosedRunnable(); + if (!mDidFirePopupshowingAndIsApprovedToOpen) { // Fire popupshowing now. bool approvedToOpen = OnOpen(); @@ -346,8 +353,56 @@ void nsMenuX::MenuOpened() { mIsOpen = true; - // Make sure we fire any pending popuphiding / popuphidden events first. - FlushMenuClosedRunnable(); + // Reset mDidFirePopupshowingAndIsApprovedToOpen for the next menu opening. + mDidFirePopupshowingAndIsApprovedToOpen = false; + + if (mNeedsRebuild) { + OnHighlightedItemChanged(Nothing()); + RemoveAll(); + RebuildMenu(); + } + + // Fire the popupshown event in MenuOpenedAsync. + // MenuOpened() is called during menuWillOpen, and if cancelTracking is called now, menuDidClose + // will not be called. + // The runnable object must not hold a strong reference to the nsMenuX, so that there is no + // reference cycle. + class MenuOpenedAsyncRunnable final : public mozilla::CancelableRunnable { + public: + explicit MenuOpenedAsyncRunnable(nsMenuX* aMenu) + : CancelableRunnable("MenuOpenedAsyncRunnable"), mMenu(aMenu) {} + + nsresult Run() override { + if (mMenu) { + RefPtr<nsMenuX> menu = mMenu; + menu->MenuOpenedAsync(); + mMenu = nullptr; + } + return NS_OK; + } + nsresult Cancel() override { + mMenu = nullptr; + return NS_OK; + } + + private: + nsMenuX* mMenu; // weak, cleared by Cancel() and Run() + }; + mPendingAsyncMenuOpenRunnable = new MenuOpenedAsyncRunnable(this); + NS_DispatchToCurrentThread(mPendingAsyncMenuOpenRunnable); +} + +void nsMenuX::FlushMenuOpenedRunnable() { + if (mPendingAsyncMenuOpenRunnable) { + MenuOpenedAsync(); + } +} + +void nsMenuX::MenuOpenedAsync() { + if (mPendingAsyncMenuOpenRunnable) { + mPendingAsyncMenuOpenRunnable->Cancel(); + mPendingAsyncMenuOpenRunnable = nullptr; + } mIsOpenForGecko = true; @@ -356,26 +411,17 @@ void nsMenuX::MenuOpened() { mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, true); } + // Fire popupshown. nsEventStatus status = nsEventStatus_eIgnore; WidgetMouseEvent event(true, eXULPopupShown, nullptr, WidgetMouseEvent::eReal); - nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent(); nsIContent* dispatchTo = popupContent ? popupContent : mContent; EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status); - // Reset mDidFirePopupshowingAndIsApprovedToOpen for then next menu opening. - mDidFirePopupshowingAndIsApprovedToOpen = false; - // Notify our observer. if (mObserver) { mObserver->OnMenuOpened(); } - - if (mNeedsRebuild) { - OnHighlightedItemChanged(Nothing()); - RemoveAll(); - RebuildMenu(); - } } void nsMenuX::MenuClosed() { @@ -383,6 +429,9 @@ void nsMenuX::MenuClosed() { return; } + // Make sure we fire any pending popupshown events first. + FlushMenuOpenedRunnable(); + // If any of our submenus were opened programmatically, make sure they get closed first. for (auto& child : mMenuChildren) { if (child.is<RefPtr<nsMenuX>>()) { @@ -518,6 +567,8 @@ void nsMenuX::ActivateItemAndClose(RefPtr<nsMenuItemX>&& aItem, NSEventModifierF bool nsMenuX::Close() { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; + FlushMenuOpenedRunnable(); + bool wasOpen = mIsOpenForGecko; if (mIsOpen) {