mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-28 15:23:51 +00:00
0fe787991e
This is consistent with other platforms, and with non-native popups. Differential Revision: https://phabricator.services.mozilla.com/D134336
392 lines
13 KiB
Plaintext
392 lines
13 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/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 "nsCocoaFeatures.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]);
|
|
[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.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* NativeViewForContent(nsIContent* aContent) {
|
|
dom::Document* doc = aContent->GetUncomposedDoc();
|
|
if (!doc) {
|
|
return nil;
|
|
}
|
|
|
|
PresShell* presShell = doc->GetPresShell();
|
|
if (!presShell) {
|
|
return nil;
|
|
}
|
|
|
|
nsIFrame* frame = presShell->GetRootFrame();
|
|
if (!frame) {
|
|
return nil;
|
|
}
|
|
|
|
nsIWidget* widget = frame->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(const DesktopPoint& aPosition) {
|
|
mMenu->PopupShowingEventWasSentAndApprovedExternally();
|
|
|
|
NSMenu* menu = mMenu->NativeNSMenu();
|
|
NSView* view = NativeViewForContent(mMenu->Content());
|
|
NSAppearance* appearance = NativeAppearanceForContent(mMenu->Content());
|
|
NSPoint locationOnScreen = nsCocoaUtils::GeckoPointToCocoaPoint(aPosition);
|
|
|
|
// 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];
|
|
}
|
|
|
|
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;
|
|
}
|
|
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];
|
|
|
|
menu->ActivateItemAfterClosing(std::move(item), ConvertModifierFlags(aModifiers), aButton);
|
|
|
|
// Notify the entire menu structure that the menu is closing in response to ActivateItem.
|
|
mMenu->MenuClosed(true);
|
|
|
|
// Close the menu.
|
|
// cancelTracking(WithoutAnimation) is asynchronous; the menu only hides once the stack unwinds
|
|
// from NSMenu's nested "tracking" event loop.
|
|
// However, cancelTrackingWithoutAnimation synchronously calls the menu delegate's menuDidClose
|
|
// handler, at least on macOS 11. However, the resulting MenuClosed call will not do anything
|
|
// because we already called MenuClosed above.
|
|
[mMenu->NativeNSMenu() cancelTrackingWithoutAnimation];
|
|
MOZMenuOpeningCoordinator.needToUnwindForMenuClosing = YES;
|
|
|
|
// Call OnWillActivateItem at the end, to match 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);
|
|
|
|
[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
|