fune/widget/cocoa/nsMenuX.h
Markus Stange 954599ea69 Bug 1748815 - Always run command events outside the context menu event loop. r=bradwerth
This patch aligns the code paths for programmatic menu item activation and
user-initiated menu item activation.

Before this patch, user-initiated menu item activation caused
the command event to fire synchronously from menuItemHit.
After this patch, the command event fires from MenuClosedAsync,
which, if an item was activated, is called asynchronously once
the menu's nested event loop has been exited. (If no item has been
activated, MenuClosedAsync is called *inside* the menu's event
loop so that popuphiding / popuphidden events for submenus don't
get delayed.)

This patch makes three major changes to align the two code paths:

 - menuItemHit now calls ActivateItemAfterClosing. This fixes bug 1748815.
 - NativeMenuMac::ActivateItem (used in automated tests) calls the
   relevant methods in the same order as user-initiated item activation.

This means that what we're testing is now closer to what we're shipping.

This patch also removes the call to runAfterMenuClosed. The runnable
that calls MenuClosedAsync is already guaranteed to run outside the
menu's event loop when a menu item was activated (I'm 99% sure about this):
For user-initiated activations, the macOS code exits the loop immediately
after calling menuItemHit and doesn't give our CFRunLoopSource another
chance to run until the stack is unwound. For test-initiated activations,
we set MOZMenuOpeningCoordinator.needToUnwindForMenuClosing which tells
our native event loop to not run anything until the stack is unwound.

Differential Revision: https://phabricator.services.mozilla.com/D149316
2022-06-23 15:05:39 +00:00

287 lines
12 KiB
Objective-C

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef nsMenuX_h_
#define nsMenuX_h_
#import <Cocoa/Cocoa.h>
#include "mozilla/EventForwards.h"
#include "mozilla/RefPtr.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Variant.h"
#include "nsISupports.h"
#include "nsMenuParentX.h"
#include "nsMenuBarX.h"
#include "nsMenuGroupOwnerX.h"
#include "nsMenuItemIconX.h"
#include "nsCOMPtr.h"
#include "nsChangeObserver.h"
#include "nsThreadUtils.h"
class nsMenuX;
class nsMenuItemX;
class nsIWidget;
// MenuDelegate is used to receive Cocoa notifications for setting
// up carbon events. Protocol is defined as of 10.6 SDK.
@interface MenuDelegate : NSObject <NSMenuDelegate> {
nsMenuX* mGeckoMenu; // weak ref
NSMutableArray* mBlocksToRunWhenOpen;
}
- (id)initWithGeckoMenu:(nsMenuX*)geckoMenu;
- (void)runBlockWhenOpen:(void (^)())block;
- (void)menu:(NSMenu*)menu willActivateItem:(NSMenuItem*)item;
@property BOOL menuIsInMenubar;
@end
class nsMenuXObserver {
public:
// Called when a menu in this menu subtree opens, before popupshowing.
// No strong reference is held to the observer during the call.
virtual void OnMenuWillOpen(mozilla::dom::Element* aPopupElement) = 0;
// Called when a menu in this menu subtree opened, after popupshown.
// No strong reference is held to the observer during the call.
virtual void OnMenuDidOpen(mozilla::dom::Element* aPopupElement) = 0;
// Called before a menu item is activated.
virtual void OnMenuWillActivateItem(mozilla::dom::Element* aPopupElement,
mozilla::dom::Element* aMenuItemElement) = 0;
// Called when a menu in this menu subtree closed, after popuphidden.
// No strong reference is held to the observer during the call.
virtual void OnMenuClosed(mozilla::dom::Element* aPopupElement) = 0;
};
// Once instantiated, this object lives until its DOM node or its parent window is destroyed.
// Do not hold references to this, they can become invalid any time the DOM node can be destroyed.
class nsMenuX final : public nsMenuParentX,
public nsChangeObserver,
public nsMenuItemIconX::Listener,
public nsMenuXObserver {
public:
using Observer = nsMenuXObserver;
// aParent is optional.
nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsIContent* aContent);
NS_INLINE_DECL_REFCOUNTING(nsMenuX)
// If > 0, the OS is indexing all the app's menus (triggered by opening
// Help menu on Leopard and higher). There are some things that are
// unsafe to do while this is happening.
static int32_t sIndexingMenuLevel;
NS_DECL_CHANGEOBSERVER
// nsMenuItemIconX::Listener
void IconUpdated() override;
// nsMenuXObserver, to forward notifications from our children to our observer.
void OnMenuWillOpen(mozilla::dom::Element* aPopupElement) override;
void OnMenuDidOpen(mozilla::dom::Element* aPopupElement) override;
void OnMenuWillActivateItem(mozilla::dom::Element* aPopupElement,
mozilla::dom::Element* aMenuItemElement) override;
void OnMenuClosed(mozilla::dom::Element* aPopupElement) override;
bool IsVisible() const { return mVisible; }
// Unregisters nsMenuX from the nsMenuGroupOwner, and nulls out the group owner pointer, on this
// nsMenuX and also all nested nsMenuX and nsMenuItemX objects.
// This is needed because nsMenuX is reference-counted and can outlive its owner, and the menu
// group owner asserts that everything has been unregistered when it is destroyed.
void DetachFromGroupOwnerRecursive();
// Nulls out our reference to the parent.
// This is needed because nsMenuX is reference-counted and can outlive its parent.
void DetachFromParent() { mParent = nullptr; }
mozilla::Maybe<MenuChild> GetItemAt(uint32_t aPos);
uint32_t GetItemCount();
mozilla::Maybe<MenuChild> GetVisibleItemAt(uint32_t aPos);
nsresult GetVisibleItemCount(uint32_t& aCount);
mozilla::Maybe<MenuChild> GetItemForElement(mozilla::dom::Element* aMenuChildElement);
// Asynchronously runs the command event on aItem, after the root menu has closed.
void ActivateItemAfterClosing(RefPtr<nsMenuItemX>&& aItem, NSEventModifierFlags aModifiers,
int16_t aButton);
bool IsOpenForGecko() const { return mIsOpenForGecko; }
// Fires the popupshowing event and returns whether the handler allows the popup to open.
// 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.
MOZ_CAN_RUN_SCRIPT bool OnOpen();
void PopupShowingEventWasSentAndApprovedExternally() { DidFirePopupShowing(); }
// Called from the menu delegate during menuWillOpen, or to simulate opening.
// Ignored if the menu is already considered open.
// 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.
// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
MOZ_CAN_RUN_SCRIPT_BOUNDARY void MenuOpened();
// Called from the menu delegate during menuDidClose, or to simulate closing.
// Ignored if the menu is already considered closed.
// 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 MenuClosed();
// Close the menu if it's open, and flush any pending popuphiding / popuphidden events.
bool Close();
// Called from the menu delegate during menu:willHighlightItem:.
// If called with Nothing(), it means that no item is highlighted.
// The index only accounts for visible items, i.e. items for which there exists an NSMenuItem* in
// mNativeMenu.
void OnHighlightedItemChanged(const mozilla::Maybe<uint32_t>& aNewHighlightedIndex);
// Called from the menu delegate before an item anywhere in this menu is activated.
// Called after MenuClosed().
void OnWillActivateItem(NSMenuItem* aItem);
void SetRebuild(bool aMenuEvent);
void SetupIcon();
nsIContent* Content() { return mContent; }
NSMenuItem* NativeNSMenuItem() { return mNativeMenuItem; }
GeckoNSMenu* NativeNSMenu() { return mNativeMenu; }
void SetIconListener(nsMenuItemIconX::Listener* aListener) { mIconListener = aListener; }
void ClearIconListener() { mIconListener = nullptr; }
// nsMenuParentX
void MenuChildChangedVisibility(const MenuChild& aChild, bool aIsVisible) override;
void Dump(uint32_t aIndent) const;
static bool IsXULHelpMenu(nsIContent* aMenuContent);
// Set an observer that gets notified of menu opening and closing.
// The menu does not keep a strong reference the observer. The observer must
// remove itself before it is destroyed.
void SetObserver(Observer* aObserver) { mObserver = aObserver; }
// Stop observing.
void ClearObserver() { mObserver = nullptr; }
protected:
virtual ~nsMenuX();
void RebuildMenu();
nsresult RemoveAll();
nsresult SetEnabled(bool aIsEnabled);
nsresult GetEnabled(bool* aIsEnabled);
already_AddRefed<nsIContent> GetMenuPopupContent();
void WillInsertChild(const MenuChild& aChild);
void WillRemoveChild(const MenuChild& aChild);
void AddMenuChild(MenuChild&& aChild);
void InsertMenuChild(MenuChild&& aChild);
void RemoveMenuChild(const MenuChild& aChild);
mozilla::Maybe<MenuChild> CreateMenuChild(nsIContent* aContent);
RefPtr<nsMenuItemX> CreateMenuItem(nsIContent* aMenuItemContent);
GeckoNSMenu* CreateMenuWithGeckoString(nsString& aMenuTitle);
void DidFirePopupShowing();
// Find the index at which aChild needs to be inserted into mMenuChildren such that mMenuChildren
// remains in correct content order, i.e. the order in mMenuChildren is the same as the order of
// the DOM children of our <menupopup>.
size_t FindInsertionIndex(const MenuChild& aChild);
// Calculates the index at which aChild's NSMenuItem should be inserted into our NSMenu.
// The order of NSMenuItems in the NSMenu is the same as the order of menu children in
// mMenuChildren; the only difference is that mMenuChildren contains both visible and invisible
// children, and the NSMenu only contains visible items. So the insertion index is equal to the
// number of visible previous siblings of aChild in mMenuChildren.
NSInteger CalculateNativeInsertionPoint(const MenuChild& aChild);
// Fires the popupshown event.
MOZ_CAN_RUN_SCRIPT 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.
// 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.
MOZ_CAN_RUN_SCRIPT void MenuClosedAsync();
// If mPendingAsyncMenuOpenRunnable is non-null, call MenuOpenedAsync() to send out the pending
// popupshown event.
// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
MOZ_CAN_RUN_SCRIPT_BOUNDARY void FlushMenuOpenedRunnable();
// If mPendingAsyncMenuCloseRunnable is non-null, call MenuClosedAsync() to send out pending
// popuphiding/popuphidden events.
// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
MOZ_CAN_RUN_SCRIPT_BOUNDARY void FlushMenuClosedRunnable();
// Make sure the NSMenu contains at least one item, even if mVisibleItemsCount is zero.
// Otherwise it won't open.
void InsertPlaceholderIfNeeded();
// Remove the placeholder before adding an item to mNativeNSMenu.
void RemovePlaceholderIfPresent();
nsCOMPtr<nsIContent> mContent; // XUL <menu> or <menupopup>
// Contains nsMenuX and nsMenuItemX objects
nsTArray<MenuChild> mMenuChildren;
nsString mLabel;
uint32_t mVisibleItemsCount = 0; // cache
nsMenuParentX* mParent = nullptr; // [weak]
nsMenuGroupOwnerX* mMenuGroupOwner = nullptr; // [weak]
nsMenuItemIconX::Listener* mIconListener = nullptr; // [weak]
mozilla::UniquePtr<nsMenuItemIconX> mIcon;
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
// menuItemHit.
RefPtr<mozilla::CancelableRunnable> mPendingAsyncMenuCloseRunnable;
struct PendingCommandEvent {
RefPtr<nsMenuItemX> mMenuItem;
NSEventModifierFlags mModifiers;
int16_t mButton;
};
// Any pending command events.
// These are queued by ActivateItemAfterClosing and run by MenuClosedAsync.
nsTArray<PendingCommandEvent> mPendingCommandEvents;
GeckoNSMenu* mNativeMenu = nil; // [strong]
MenuDelegate* mMenuDelegate = nil; // [strong]
// nsMenuX objects should always have a valid native menu item.
NSMenuItem* mNativeMenuItem = nil; // [strong]
// Nothing() if no item is highlighted. The index only accounts for visible items.
mozilla::Maybe<uint32_t> mHighlightedItemIndex;
bool mIsEnabled = true;
bool mNeedsRebuild = true;
// Whether the native NSMenu is considered open.
// Also affected by MenuOpened() / MenuClosed() calls for simulated opening / closing.
bool mIsOpen = false;
// Whether the popup is open from Gecko's perspective, based on popupshowing / popuphiding events.
bool mIsOpenForGecko = false;
bool mVisible = true;
// true between an OnOpen() call that returned true, and the subsequent call
// to MenuOpened().
bool mDidFirePopupshowingAndIsApprovedToOpen = false;
};
#endif // nsMenuX_h_