gecko-dev/widget/cocoa/NativeMenuMac.mm
Markus Stange 98e99d969c Bug 1926630 - Make CheckNativeMenuConsistency detect duplicate menuitems more reliably. r=mac-reviewers,bradwerth
In the case of this bug, we were creating different NSMenuItem
objects which ended up referring to the same nsIContent object.

Differential Revision: https://phabricator.services.mozilla.com/D227404
2024-11-06 03:22:50 +00:00

414 lines
14 KiB
Plaintext

/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */
/* 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/. */
#import <Cocoa/Cocoa.h>
#include "NativeMenuMac.h"
#include "mozilla/Assertions.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Element.h"
#include "MOZMenuOpeningCoordinator.h"
#include "nsISupports.h"
#include "nsGkAtoms.h"
#include "nsMenuGroupOwnerX.h"
#include "nsMenuItemX.h"
#include "nsMenuUtilsX.h"
#include "nsNativeThemeColors.h"
#include "nsObjCExceptions.h"
#include "nsThreadUtils.h"
#include "PresShell.h"
#include "nsCocoaUtils.h"
#include "nsIFrame.h"
#include "nsPresContext.h"
#include "nsDeviceContext.h"
namespace mozilla {
using dom::Element;
namespace widget {
NativeMenuMac::NativeMenuMac(dom::Element* aElement)
: mElement(aElement), mContainerStatusBarItem(nil) {
MOZ_RELEASE_ASSERT(
aElement->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menupopup));
mMenuGroupOwner = new nsMenuGroupOwnerX(aElement, nullptr);
mMenu = MakeRefPtr<nsMenuX>(nullptr, mMenuGroupOwner, aElement);
mMenu->SetObserver(this);
mMenu->SetIconListener(this);
mMenu->SetupIcon();
}
NativeMenuMac::~NativeMenuMac() {
mMenu->DetachFromGroupOwnerRecursive();
mMenu->ClearObserver();
mMenu->ClearIconListener();
}
static void UpdateMenu(nsMenuX* aMenu) {
aMenu->MenuOpened();
aMenu->MenuClosed();
uint32_t itemCount = aMenu->GetItemCount();
for (uint32_t i = 0; i < itemCount; i++) {
nsMenuX::MenuChild menuObject = *aMenu->GetItemAt(i);
if (menuObject.is<RefPtr<nsMenuX>>()) {
UpdateMenu(menuObject.as<RefPtr<nsMenuX>>());
}
}
}
void NativeMenuMac::MenuWillOpen() {
// Force an update on the mMenu by faking an open/close on all of
// its submenus.
UpdateMenu(mMenu.get());
}
bool NativeMenuMac::ActivateNativeMenuItemAt(const nsAString& aIndexString) {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
NSMenu* menu = mMenu->NativeNSMenu();
nsMenuUtilsX::CheckNativeMenuConsistency(menu);
NSString* locationString =
[NSString stringWithCharacters:reinterpret_cast<const unichar*>(
aIndexString.BeginReading())
length:aIndexString.Length()];
NSMenuItem* item =
nsMenuUtilsX::NativeMenuItemWithLocation(menu, locationString, false);
// We can't perform an action on an item with a submenu, that will raise
// an obj-c exception.
if (item && !item.hasSubmenu) {
NSMenu* parent = item.menu;
if (parent) {
// NSLog(@"Performing action for native menu item titled: %@\n",
// [[currentSubmenu itemAtIndex:targetIndex] title]);
mozilla::AutoRestore<bool> autoRestore(
nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest);
nsMenuUtilsX::gIsSynchronouslyActivatingNativeMenuItemDuringTest = true;
[parent performActionForItemAtIndex:[parent indexOfItem:item]];
return true;
}
}
return false;
NS_OBJC_END_TRY_ABORT_BLOCK;
}
void NativeMenuMac::ForceUpdateNativeMenuAt(const nsAString& aIndexString) {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
NSString* locationString =
[NSString stringWithCharacters:reinterpret_cast<const unichar*>(
aIndexString.BeginReading())
length:aIndexString.Length()];
NSArray<NSString*>* indexes =
[locationString componentsSeparatedByString:@"|"];
RefPtr<nsMenuX> currentMenu = mMenu.get();
// now find the correct submenu
unsigned int indexCount = indexes.count;
for (unsigned int i = 1; currentMenu && i < indexCount; i++) {
int targetIndex = [indexes objectAtIndex:i].intValue;
int visible = 0;
uint32_t length = currentMenu->GetItemCount();
for (unsigned int j = 0; j < length; j++) {
Maybe<nsMenuX::MenuChild> targetMenu = currentMenu->GetItemAt(j);
if (!targetMenu) {
return;
}
RefPtr<nsIContent> content = targetMenu->match(
[](const RefPtr<nsMenuX>& aMenu) { return aMenu->Content(); },
[](const RefPtr<nsMenuItemX>& aMenuItem) {
return aMenuItem->Content();
});
if (!nsMenuUtilsX::NodeIsHiddenOrCollapsed(content)) {
visible++;
if (targetMenu->is<RefPtr<nsMenuX>>() && visible == (targetIndex + 1)) {
currentMenu = targetMenu->as<RefPtr<nsMenuX>>();
break;
}
}
}
}
// fake open/close to cause lazy update to happen
currentMenu->MenuOpened();
currentMenu->MenuClosed();
NS_OBJC_END_TRY_ABORT_BLOCK;
}
void NativeMenuMac::IconUpdated() {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
if (mContainerStatusBarItem) {
NSImage* menuImage = mMenu->NativeNSMenuItem().image;
if (menuImage) {
[menuImage setTemplate:YES];
}
mContainerStatusBarItem.button.image = menuImage;
}
NS_OBJC_END_TRY_ABORT_BLOCK;
}
void NativeMenuMac::SetContainerStatusBarItem(NSStatusItem* aItem) {
mContainerStatusBarItem = aItem;
IconUpdated();
}
void NativeMenuMac::Dump() {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
mMenu->Dump(0);
nsMenuUtilsX::DumpNativeMenu(mMenu->NativeNSMenu());
NS_OBJC_END_TRY_ABORT_BLOCK;
}
void NativeMenuMac::OnMenuWillOpen(dom::Element* aPopupElement) {
if (aPopupElement == mElement) {
return;
}
// Our caller isn't keeping us alive, so make sure we stay alive throughout
// this function in case one of the observer notifications destroys us.
RefPtr<NativeMenuMac> kungFuDeathGrip(this);
for (NativeMenu::Observer* observer : mObservers.Clone()) {
observer->OnNativeSubMenuWillOpen(aPopupElement);
}
}
void NativeMenuMac::OnMenuDidOpen(dom::Element* aPopupElement) {
// Our caller isn't keeping us alive, so make sure we stay alive throughout
// this function in case one of the observer notifications destroys us.
RefPtr<NativeMenuMac> kungFuDeathGrip(this);
for (NativeMenu::Observer* observer : mObservers.Clone()) {
if (aPopupElement == mElement) {
observer->OnNativeMenuOpened();
} else {
observer->OnNativeSubMenuDidOpen(aPopupElement);
}
}
}
void NativeMenuMac::OnMenuWillActivateItem(dom::Element* aPopupElement,
dom::Element* aMenuItemElement) {
// Our caller isn't keeping us alive, so make sure we stay alive throughout
// this function in case one of the observer notifications destroys us.
RefPtr<NativeMenuMac> kungFuDeathGrip(this);
for (NativeMenu::Observer* observer : mObservers.Clone()) {
observer->OnNativeMenuWillActivateItem(aMenuItemElement);
}
}
void NativeMenuMac::OnMenuClosed(dom::Element* aPopupElement) {
// Our caller isn't keeping us alive, so make sure we stay alive throughout
// this function in case one of the observer notifications destroys us.
RefPtr<NativeMenuMac> kungFuDeathGrip(this);
for (NativeMenu::Observer* observer : mObservers.Clone()) {
if (aPopupElement == mElement) {
observer->OnNativeMenuClosed();
} else {
observer->OnNativeSubMenuClosed(aPopupElement);
}
}
}
static NSView* NativeViewForFrame(nsIFrame* aFrame) {
nsIWidget* widget = aFrame->GetNearestWidget();
return (NSView*)widget->GetNativeData(NS_NATIVE_WIDGET);
}
static NSAppearance* NativeAppearanceForContent(nsIContent* aContent) {
nsIFrame* f = aContent->GetPrimaryFrame();
if (!f) {
return nil;
}
return NSAppearanceForColorScheme(LookAndFeel::ColorSchemeForFrame(f));
}
void NativeMenuMac::ShowAsContextMenu(nsIFrame* aClickedFrame,
const CSSIntPoint& aPosition,
bool aIsContextMenu) {
nsPresContext* pc = aClickedFrame->PresContext();
auto cssToDesktopScale =
pc->CSSToDevPixelScale() / pc->DeviceContext()->GetDesktopToDeviceScale();
const DesktopPoint desktopPoint = aPosition * cssToDesktopScale;
mMenu->PopupShowingEventWasSentAndApprovedExternally();
NSMenu* menu = mMenu->NativeNSMenu();
NSView* view = NativeViewForFrame(aClickedFrame);
NSAppearance* appearance = NativeAppearanceForContent(mMenu->Content());
NSPoint locationOnScreen = nsCocoaUtils::GeckoPointToCocoaPoint(desktopPoint);
// Let the MOZMenuOpeningCoordinator do the actual opening, so that this
// ShowAsContextMenu call does not spawn a nested event loop, which would be
// surprising to our callers.
mOpeningHandle = [MOZMenuOpeningCoordinator.sharedInstance
asynchronouslyOpenMenu:menu
atScreenPosition:locationOnScreen
forView:view
withAppearance:appearance
asContextMenu:aIsContextMenu];
}
bool NativeMenuMac::Close() {
if (mOpeningHandle) {
// In case the menu was trying to open, but this Close() call interrupted
// it, cancel opening.
[MOZMenuOpeningCoordinator.sharedInstance
cancelAsynchronousOpening:mOpeningHandle];
}
return mMenu->Close();
}
RefPtr<nsMenuX> NativeMenuMac::GetOpenMenuContainingElement(
dom::Element* aElement) {
nsTArray<RefPtr<dom::Element>> submenuChain;
RefPtr<dom::Element> currentElement = aElement->GetParentElement();
while (currentElement && currentElement != mElement) {
if (currentElement->IsXULElement(nsGkAtoms::menu)) {
submenuChain.AppendElement(currentElement);
}
currentElement = currentElement->GetParentElement();
}
if (!currentElement) {
// aElement was not a descendent of mElement. Refuse to activate the item.
return nullptr;
}
// Traverse submenuChain from shallow to deep, to find the nsMenuX that
// contains aElement.
submenuChain.Reverse();
RefPtr<nsMenuX> menu = mMenu;
for (const auto& submenu : submenuChain) {
if (!menu->IsOpenForGecko()) {
// Refuse to descend into closed menus.
return nullptr;
}
Maybe<nsMenuX::MenuChild> menuChild = menu->GetItemForElement(submenu);
if (!menuChild || !menuChild->is<RefPtr<nsMenuX>>()) {
// Couldn't find submenu.
return nullptr;
}
menu = menuChild->as<RefPtr<nsMenuX>>();
}
if (!menu->IsOpenForGecko()) {
// Refuse to descend into closed menus.
return nullptr;
}
return menu;
}
static NSEventModifierFlags ConvertModifierFlags(Modifiers aModifiers) {
NSEventModifierFlags flags = 0;
if (aModifiers & MODIFIER_CONTROL) {
flags |= NSEventModifierFlagControl;
}
if (aModifiers & MODIFIER_ALT) {
flags |= NSEventModifierFlagOption;
}
if (aModifiers & MODIFIER_SHIFT) {
flags |= NSEventModifierFlagShift;
}
if (aModifiers & MODIFIER_META) {
flags |= NSEventModifierFlagCommand;
}
return flags;
}
void NativeMenuMac::ActivateItem(dom::Element* aItemElement,
Modifiers aModifiers, int16_t aButton,
ErrorResult& aRv) {
RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aItemElement);
if (!menu) {
aRv.ThrowInvalidStateError("Menu containing menu item is not open");
return;
}
nsMenuUtilsX::CheckNativeMenuConsistency(menu->NativeNSMenu());
Maybe<nsMenuX::MenuChild> child = menu->GetItemForElement(aItemElement);
if (!child || !child->is<RefPtr<nsMenuItemX>>()) {
aRv.ThrowInvalidStateError("Could not find the supplied menu item");
return;
}
RefPtr<nsMenuItemX> item = std::move(child->as<RefPtr<nsMenuItemX>>());
if (!item->IsVisible()) {
aRv.ThrowInvalidStateError("Menu item is not visible");
return;
}
NSMenuItem* nativeItem = [item->NativeNSMenuItem() retain];
// First, initiate the closing of the NSMenu.
// This synchronously calls the menu delegate's menuDidClose handler. So
// menuDidClose is what runs first; this matches the order of events for
// user-initiated menu item activation. This call doesn't immediately hide the
// menu; the menu only hides once the stack unwinds from NSMenu's nested
// "tracking" event loop.
[mMenu->NativeNSMenu() cancelTrackingWithoutAnimation];
// Next, call OnWillActivateItem. This also matches the order of calls that
// happen when a user activates a menu item in the real world: -[MenuDelegate
// menu:willActivateItem:] runs after menuDidClose.
menu->OnWillActivateItem(nativeItem);
// Finally, call ActivateItemAfterClosing. This also mimics the order in the
// real world: menuItemHit is called after menu:willActivateItem:.
menu->ActivateItemAfterClosing(std::move(item),
ConvertModifierFlags(aModifiers), aButton);
// Tell our native event loop that it should not process any more work before
// unwinding the stack, so that we can get out of the menu's nested event loop
// as fast as possible. This was needed to fix spurious failures in tests,
// where a call to cancelTrackingWithoutAnimation was ignored if more native
// events were processed before the event loop was exited. As a result, the
// menu stayed open forever and the test never finished.
MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
[nativeItem release];
}
void NativeMenuMac::OpenSubmenu(dom::Element* aMenuElement) {
if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
if (item && item->is<RefPtr<nsMenuX>>()) {
item->as<RefPtr<nsMenuX>>()->MenuOpened();
}
}
}
void NativeMenuMac::CloseSubmenu(dom::Element* aMenuElement) {
if (RefPtr<nsMenuX> menu = GetOpenMenuContainingElement(aMenuElement)) {
Maybe<nsMenuX::MenuChild> item = menu->GetItemForElement(aMenuElement);
if (item && item->is<RefPtr<nsMenuX>>()) {
item->as<RefPtr<nsMenuX>>()->MenuClosed();
}
}
}
RefPtr<Element> NativeMenuMac::Element() { return mElement; }
} // namespace widget
} // namespace mozilla