fune/widget/cocoa/NativeMenuMac.mm
2021-03-23 14:38:29 +00:00

272 lines
9.7 KiB
Text

/* -*- 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 "nsThreadUtils.h"
#include "mozilla/dom/Document.h"
#include "NativeMenuMac.h"
#include "mozilla/Assertions.h"
#include "mozilla/dom/Element.h"
#include "nsISupports.h"
#include "nsGkAtoms.h"
#include "nsGkAtoms.h"
#include "nsMenuGroupOwnerX.h"
#include "nsMenuItemX.h"
#include "nsMenuUtilsX.h"
#include "nsObjCExceptions.h"
#include "mozilla/dom/Document.h"
#include "PresShell.h"
#include "nsCocoaUtils.h"
#include "nsIFrame.h"
namespace mozilla {
using dom::Element;
namespace widget {
NativeMenuMac::NativeMenuMac(mozilla::dom::Element* 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::OnMenuOpened() {
// 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->OnNativeMenuOpened();
}
}
void NativeMenuMac::OnMenuClosed() {
// 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->OnNativeMenuClosed();
}
}
static NSView* NativeViewForContent(nsIContent* aContent) {
mozilla::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);
}
bool NativeMenuMac::ShowAsContextMenu(const mozilla::DesktopPoint& aPosition) {
bool allowOpening = mMenu->OnOpen();
if (!allowOpening) {
// preventDefault() was called on the popupshowing event. Do not display the menu.
return false;
}
// Do the actual opening off of a runnable, so that this ShowAsContextMenu call does not spawn a
// nested event loop, which would be surprising to our callers.
mozilla::DesktopPoint position = aPosition;
RefPtr<NativeMenuMac> self = this;
NS_DispatchToCurrentThread(NS_NewRunnableFunction("nsStandaloneNativeMenu::OpenMenu",
[=]() { self->OpenMenu(position); }));
return true;
}
void NativeMenuMac::OpenMenu(const mozilla::DesktopPoint& aPosition) {
// There are multiple ways to display an NSMenu as a context menu.
//
// 1. We can return the NSMenu from -[ChildView menuForEvent:] and the NSView will open it for
// us.
// 2. We can call +[NSMenu popUpContextMenu:withEvent:forView:] inside a mouseDown handler with a
// real mouse down event.
// 3. We can call +[NSMenu popUpContextMenu:withEvent:forView:] at a later time, with a real
// mouse event that we stored earlier.
// 4. We can call +[NSMenu popUpContextMenu:withEvent:forView:] at any time, with a synthetic
// mouse event that we create just for that purpose.
// 5. We can call -[NSMenu popUpMenuPositioningItem:atLocation:inView:] and it just takes a
// position, not an event.
//
// 1-4 look the same, 5 looks different: 5 is made for use with NSPopUpButton, where the selected
// item needs to be shown at a specific position. If a tall menu is opened with a position close
// to the bottom edge of the screen, 5 results in a cropped menu with scroll arrows, even if the
// entire menu would fit on the screen, due to the positioning constraint.
// 1-2 only work if the menu contents are known synchronously during the call to menuForEvent or
// during the mouseDown event handler.
// NativeMenuMac::ShowAsContextMenu can be called at any time. It could be called during a
// menuForEvent call (during a "contextmenu" event handler), or during a mouseDown handler, or at
// a later time.
// The code below uses option 4 as the preferred option because it's the simplest: It works in all
// scenarios and it doesn't have the positioning drawbacks of option 5.
NSView* view = NativeViewForContent(mMenu->Content());
NSMenu* nativeMenu = mMenu->NativeNSMenu();
NSPoint locationOnScreen = nsCocoaUtils::GeckoPointToCocoaPoint(aPosition);
if (view) {
// Create a synthetic event at the right location and open the menu [option 4].
NSPoint locationInWindow = nsCocoaUtils::ConvertPointFromScreen(view.window, locationOnScreen);
NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeRightMouseDown
location:locationInWindow
modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber:view.window.windowNumber
context:nil
eventNumber:0
clickCount:1
pressure:0.0f];
[NSMenu popUpContextMenu:nativeMenu withEvent:event forView:view];
} else {
// Open the menu using popUpMenuPositioningItem:atLocation:inView: [option 5].
// This is not preferred, because it positions the menu differently from how a native context
// menu would be positioned; it enforces locationOnScreen for the top left corner even if this
// means that the menu will be displayed in a clipped fashion with scroll arrows.
[nativeMenu popUpMenuPositioningItem:nil atLocation:locationOnScreen inView:nil];
}
}
} // namespace widget
} // namespace mozilla