mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 04:41:11 +00:00
98e99d969c
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
414 lines
14 KiB
Plaintext
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
|