blob: 089de051cb76ecbed856502643857ebc6820532c [file] [log] [blame]
/*
* Copyright (C) 2010 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "WebContextMenuProxyMac.h"
#if PLATFORM(MAC)
#import "APIContextMenuClient.h"
#import "DataReference.h"
#import "MenuUtilities.h"
#import "PageClientImplMac.h"
#import "ServicesController.h"
#import "ShareableBitmap.h"
#import "StringUtilities.h"
#import "WKMenuItemIdentifiersPrivate.h"
#import "WKSharingServicePickerDelegate.h"
#import "WebContextMenuItem.h"
#import "WebContextMenuItemData.h"
#import "WebContextMenuListenerProxy.h"
#import "WebPageProxy.h"
#import "WebProcessProxy.h"
#import <WebCore/GraphicsContext.h>
#import <WebCore/IntRect.h>
#import <pal/spi/mac/NSMenuSPI.h>
#import <pal/spi/mac/NSSharingServicePickerSPI.h>
#import <pal/spi/mac/NSSharingServiceSPI.h>
#import <wtf/RetainPtr.h>
@interface WKUserDataWrapper : NSObject {
RefPtr<API::Object> _webUserData;
}
- (id)initWithUserData:(API::Object*)userData;
- (API::Object*)userData;
@end
@implementation WKUserDataWrapper
- (id)initWithUserData:(API::Object*)userData
{
self = [super init];
if (!self)
return nil;
_webUserData = userData;
return self;
}
- (API::Object*)userData
{
return _webUserData.get();
}
@end
@interface WKSelectionHandlerWrapper : NSObject {
WTF::Function<void ()> _selectionHandler;
}
- (id)initWithSelectionHandler:(WTF::Function<void ()>&&)selectionHandler;
- (void)executeSelectionHandler;
@end
@implementation WKSelectionHandlerWrapper
- (id)initWithSelectionHandler:(WTF::Function<void ()>&&)selectionHandler
{
self = [super init];
if (!self)
return nil;
_selectionHandler = WTFMove(selectionHandler);
return self;
}
- (void)executeSelectionHandler
{
if (_selectionHandler)
_selectionHandler();
}
@end
@interface WKMenuTarget : NSObject {
WebKit::WebContextMenuProxyMac* _menuProxy;
}
+ (WKMenuTarget *)sharedMenuTarget;
- (WebKit::WebContextMenuProxyMac*)menuProxy;
- (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy;
- (void)forwardContextMenuAction:(id)sender;
@end
@implementation WKMenuTarget
+ (WKMenuTarget*)sharedMenuTarget
{
static WKMenuTarget* target = [[WKMenuTarget alloc] init];
return target;
}
- (WebKit::WebContextMenuProxyMac*)menuProxy
{
return _menuProxy;
}
- (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy
{
_menuProxy = menuProxy;
}
- (void)forwardContextMenuAction:(id)sender
{
id representedObject = [sender representedObject];
// NSMenuItems with a represented selection handler belong solely to the UI process
// and don't need any further processing after the selection handler is called.
if ([representedObject isKindOfClass:[WKSelectionHandlerWrapper class]]) {
[representedObject executeSelectionHandler];
return;
}
ASSERT(!sender || [sender isKindOfClass:NSMenuItem.class]);
WebKit::WebContextMenuItemData item(WebCore::ActionType, static_cast<WebCore::ContextMenuAction>([sender tag]), [sender title], [sender isEnabled], [(NSMenuItem *)sender state] == NSControlStateValueOn);
if (representedObject) {
ASSERT([representedObject isKindOfClass:[WKUserDataWrapper class]]);
item.setUserData([static_cast<WKUserDataWrapper *>(representedObject) userData]);
}
_menuProxy->contextMenuItemSelected(item);
}
@end
namespace WebKit {
using namespace WebCore;
WebContextMenuProxyMac::WebContextMenuProxyMac(NSView* webView, WebPageProxy& page, ContextMenuContextData&& context, const UserData& userData)
: WebContextMenuProxy(WTFMove(context), userData)
, m_webView(webView)
, m_page(page)
{
}
WebContextMenuProxyMac::~WebContextMenuProxyMac()
{
[m_menu cancelTracking];
if (m_contextMenuListener) {
m_contextMenuListener->invalidate();
m_contextMenuListener = nullptr;
}
}
void WebContextMenuProxyMac::contextMenuItemSelected(const WebContextMenuItemData& item)
{
#if ENABLE(SERVICE_CONTROLS)
clearServicesMenu();
#endif
m_page.contextMenuItemSelected(item);
}
#if ENABLE(SERVICE_CONTROLS)
void WebContextMenuProxyMac::setupServicesMenu()
{
bool includeEditorServices = m_context.controlledDataIsEditable();
bool hasControlledImage = m_context.controlledImage();
NSArray *items = nil;
if (hasControlledImage) {
RefPtr<ShareableBitmap> image = m_context.controlledImage();
if (!image)
return;
auto cgImage = image->makeCGImage();
auto nsImage = adoptNS([[NSImage alloc] initWithCGImage:cgImage.get() size:image->size()]);
auto itemProvider = adoptNS([[NSItemProvider alloc] initWithItem:[nsImage TIFFRepresentation] typeIdentifier:(__bridge NSString *)kUTTypeTIFF]);
items = @[ itemProvider.get() ];
} else if (!m_context.controlledSelectionData().isEmpty()) {
auto selectionData = adoptNS([[NSData alloc] initWithBytes:static_cast<const void*>(m_context.controlledSelectionData().data()) length:m_context.controlledSelectionData().size()]);
auto selection = adoptNS([[NSAttributedString alloc] initWithRTFD:selectionData.get() documentAttributes:nil]);
items = @[ selection.get() ];
} else {
LOG_ERROR("No service controlled item represented in the context");
return;
}
RetainPtr<NSSharingServicePicker> picker = adoptNS([[NSSharingServicePicker alloc] initWithItems:items]);
[picker setStyle:hasControlledImage ? NSSharingServicePickerStyleRollover : NSSharingServicePickerStyleTextSelection];
[picker setDelegate:[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate]];
[[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:picker.get()];
[[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setFiltersEditingServices:!includeEditorServices];
[[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setHandlesEditingReplacement:includeEditorServices];
m_menu = adoptNS([[picker menu] copy]);
if (!hasControlledImage)
[m_menu setShowsStateColumn:YES];
// Explicitly add a menu item for each telephone number that is in the selection.
Vector<RetainPtr<NSMenuItem>> telephoneNumberMenuItems;
for (auto& telephoneNumber : m_context.selectedTelephoneNumbers()) {
if (NSMenuItem *item = menuItemForTelephoneNumber(telephoneNumber)) {
[item setIndentationLevel:1];
telephoneNumberMenuItems.append(item);
}
}
if (!telephoneNumberMenuItems.isEmpty()) {
if (m_menu)
[m_menu insertItem:[NSMenuItem separatorItem] atIndex:0];
else
m_menu = adoptNS([[NSMenu alloc] init]);
int itemPosition = 0;
auto groupEntry = adoptNS([[NSMenuItem alloc] initWithTitle:menuItemTitleForTelephoneNumberGroup() action:nil keyEquivalent:@""]);
[groupEntry setEnabled:NO];
[m_menu insertItem:groupEntry.get() atIndex:itemPosition++];
for (auto& menuItem : telephoneNumberMenuItems)
[m_menu insertItem:menuItem.get() atIndex:itemPosition++];
}
// If there is no services menu, then the existing services on the system have changed, so refresh that list of services.
// If <rdar://problem/17954709> is resolved then we can more accurately keep the list up to date without this call.
if (!m_menu)
ServicesController::singleton().refreshExistingServices();
}
void WebContextMenuProxyMac::showServicesMenu()
{
setupServicesMenu();
[[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setMenuProxy:this];
[m_menu popUpMenuPositioningItem:nil atLocation:m_context.menuLocation() inView:m_webView];
}
void WebContextMenuProxyMac::clearServicesMenu()
{
[[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:nullptr];
m_menu = nullptr;
}
RetainPtr<NSMenuItem> WebContextMenuProxyMac::createShareMenuItem()
{
const WebHitTestResultData& hitTestData = m_context.webHitTestResultData();
auto items = adoptNS([[NSMutableArray alloc] init]);
if (!hitTestData.absoluteLinkURL.isEmpty()) {
auto absoluteLinkURL = URL({ }, hitTestData.absoluteLinkURL);
if (!absoluteLinkURL.isEmpty())
[items addObject:(NSURL *)absoluteLinkURL];
}
if (hitTestData.isDownloadableMedia && !hitTestData.absoluteMediaURL.isEmpty()) {
auto downloadableMediaURL = URL({ }, hitTestData.absoluteMediaURL);
if (!downloadableMediaURL.isEmpty())
[items addObject:(NSURL *)downloadableMediaURL];
}
if (hitTestData.imageSharedMemory && hitTestData.imageSize) {
if (auto image = adoptNS([[NSImage alloc] initWithData:[NSData dataWithBytes:(unsigned char*)hitTestData.imageSharedMemory->data() length:hitTestData.imageSize]]))
[items addObject:image.get()];
}
if (!m_context.selectedText().isEmpty())
[items addObject:(NSString *)m_context.selectedText()];
if (![items count])
return nil;
RetainPtr<NSMenuItem> item = [NSMenuItem standardShareMenuItemForItems:items.get()];
if (!item)
return nil;
NSSharingServicePicker *sharingServicePicker = [item representedObject];
sharingServicePicker.delegate = [WKSharingServicePickerDelegate sharedSharingServicePickerDelegate];
[[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setFiltersEditingServices:NO];
[[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setHandlesEditingReplacement:NO];
[[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setMenuProxy:this];
// Setting the picker lets the delegate retain it to keep it alive, but this picker is kept alive by the menu item.
[[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:nil];
[item setIdentifier:_WKMenuItemIdentifierShareMenu];
return item;
}
#endif
void WebContextMenuProxyMac::show()
{
Ref<WebPageProxy> protect(m_page);
#if ENABLE(SERVICE_CONTROLS)
if (m_context.isServicesMenu()) {
showServicesMenu();
return;
}
#endif
showContextMenu();
}
RetainPtr<NSMenu> WebContextMenuProxyMac::createContextMenuFromItems(const Vector<WebContextMenuItemData>& items)
{
auto menu = adoptNS([[NSMenu alloc] initWithTitle:@""]);
[menu setAutoenablesItems:NO];
for (auto& item : items) {
if (auto menuItem = createContextMenuItem(item))
[menu addItem:menuItem.get()];
}
return menu;
}
static NSString *menuItemIdentifier(const WebCore::ContextMenuAction action)
{
switch (action) {
case ContextMenuItemTagCopy:
return _WKMenuItemIdentifierCopy;
case ContextMenuItemTagCopyImageToClipboard:
return _WKMenuItemIdentifierCopyImage;
case ContextMenuItemTagCopyLinkToClipboard:
return _WKMenuItemIdentifierCopyLink;
case ContextMenuItemTagCopyMediaLinkToClipboard:
return _WKMenuItemIdentifierCopyMediaLink;
case ContextMenuItemTagDownloadImageToDisk:
return _WKMenuItemIdentifierDownloadImage;
case ContextMenuItemTagDownloadLinkToDisk:
return _WKMenuItemIdentifierDownloadLinkedFile;
case ContextMenuItemTagDownloadMediaToDisk:
return _WKMenuItemIdentifierDownloadMedia;
case ContextMenuItemTagGoBack:
return _WKMenuItemIdentifierGoBack;
case ContextMenuItemTagGoForward:
return _WKMenuItemIdentifierGoForward;
case ContextMenuItemTagInspectElement:
return _WKMenuItemIdentifierInspectElement;
case ContextMenuItemTagLookUpInDictionary:
return _WKMenuItemIdentifierLookUp;
case ContextMenuItemTagOpenFrameInNewWindow:
return _WKMenuItemIdentifierOpenFrameInNewWindow;
case ContextMenuItemTagOpenImageInNewWindow:
return _WKMenuItemIdentifierOpenImageInNewWindow;
case ContextMenuItemTagOpenLink:
return _WKMenuItemIdentifierOpenLink;
case ContextMenuItemTagOpenLinkInNewWindow:
return _WKMenuItemIdentifierOpenLinkInNewWindow;
case ContextMenuItemTagOpenMediaInNewWindow:
return _WKMenuItemIdentifierOpenMediaInNewWindow;
case ContextMenuItemTagPaste:
return _WKMenuItemIdentifierPaste;
case ContextMenuItemTagReload:
return _WKMenuItemIdentifierReload;
case ContextMenuItemTagSearchWeb:
return _WKMenuItemIdentifierSearchWeb;
case ContextMenuItemTagToggleMediaControls:
return _WKMenuItemIdentifierShowHideMediaControls;
case ContextMenuItemTagToggleVideoEnhancedFullscreen:
return _WKMenuItemIdentifierToggleEnhancedFullScreen;
case ContextMenuItemTagToggleVideoFullscreen:
return _WKMenuItemIdentifierToggleFullScreen;
case ContextMenuItemTagShareMenu:
return _WKMenuItemIdentifierShareMenu;
case ContextMenuItemTagSpeechMenu:
return _WKMenuItemIdentifierSpeechMenu;
default:
return nil;
}
}
RetainPtr<NSMenuItem> WebContextMenuProxyMac::createContextMenuItem(const WebContextMenuItemData& item)
{
#if ENABLE(SERVICE_CONTROLS)
if (item.action() == ContextMenuItemTagShareMenu)
return createShareMenuItem();
#endif
switch (item.type()) {
case WebCore::ActionType:
case WebCore::CheckableActionType: {
auto menuItem = adoptNS([[NSMenuItem alloc] initWithTitle:item.title() action:@selector(forwardContextMenuAction:) keyEquivalent:@""]);
[menuItem setTag:item.action()];
[menuItem setEnabled:item.enabled()];
[menuItem setState:item.checked() ? NSControlStateValueOn : NSControlStateValueOff];
[menuItem setTarget:[WKMenuTarget sharedMenuTarget]];
[menuItem setIdentifier:menuItemIdentifier(item.action())];
if (item.userData()) {
auto wrapper = adoptNS([[WKUserDataWrapper alloc] initWithUserData:item.userData()]);
[menuItem setRepresentedObject:wrapper.get()];
}
return menuItem;
}
case WebCore::SeparatorType:
return [NSMenuItem separatorItem];
case WebCore::SubmenuType: {
auto menuItem = adoptNS([[NSMenuItem alloc] initWithTitle:item.title() action:nullptr keyEquivalent:@""]);
[menuItem setEnabled:item.enabled()];
[menuItem setSubmenu:createContextMenuFromItems(item.submenu()).get()];
[menuItem setIdentifier:menuItemIdentifier(item.action())];
return menuItem;
}
}
}
void WebContextMenuProxyMac::showContextMenuWithItems(Vector<Ref<WebContextMenuItem>>&& items)
{
if (m_page.contextMenuClient().showContextMenu(m_page, m_context.menuLocation(), items))
return;
if (items.isEmpty())
return;
Vector<WebContextMenuItemData> data;
data.reserveInitialCapacity(items.size());
for (auto& item : items)
data.uncheckedAppend(item->data());
auto menu = createContextMenuFromItems(data);
[[WKMenuTarget sharedMenuTarget] setMenuProxy:this];
m_page.contextMenuClient().menuFromProposedMenu(m_page, menu.get(), m_context.webHitTestResultData(), m_userData.object(), [this, protectedThis = makeRef(*this)] (RetainPtr<NSMenu>&& menu) {
m_menu = WTFMove(menu);
NSPoint menuLocation = [m_webView convertPoint:m_context.menuLocation() toView:nil];
NSEvent *event = [NSEvent mouseEventWithType:NSEventTypeRightMouseUp location:menuLocation modifierFlags:0 timestamp:0 windowNumber:m_webView.window.windowNumber context:nil eventNumber:0 clickCount:0 pressure:0];
[NSMenu popUpContextMenu:m_menu.get() withEvent:event forView:m_webView];
if (m_contextMenuListener) {
m_contextMenuListener->invalidate();
m_contextMenuListener = nullptr;
}
});
}
void WebContextMenuProxyMac::showContextMenu()
{
Vector<Ref<WebContextMenuItem>> proposedAPIItems;
for (auto& item : m_context.menuItems())
proposedAPIItems.append(WebContextMenuItem::create(item));
if (m_contextMenuListener) {
m_contextMenuListener->invalidate();
m_contextMenuListener = nullptr;
}
m_contextMenuListener = WebContextMenuListenerProxy::create(this);
m_page.contextMenuClient().getContextMenuFromProposedMenu(m_page, WTFMove(proposedAPIItems), *m_contextMenuListener, m_context.webHitTestResultData(), m_page.process().transformHandlesToObjects(m_userData.object()).get());
}
NSWindow *WebContextMenuProxyMac::window() const
{
return [m_webView window];
}
} // namespace WebKit
#endif // PLATFORM(MAC)