blob: 359be91dc3d9d3b4125cee1203ab416ccb8a31a1 [file] [log] [blame]
/*
* Copyright (C) 2014 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 "WKActionMenuController.h"
#if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
#import "WKNSURLExtras.h"
#import "WKViewInternal.h"
#import "WKWebView.h"
#import "WKWebViewInternal.h"
#import "WebContext.h"
#import "WebKitSystemInterface.h"
#import "WebPageMessages.h"
#import "WebPageProxy.h"
#import "WebPageProxyMessages.h"
#import "WebProcessProxy.h"
#import <Foundation/Foundation.h>
#import <ImageIO/ImageIO.h>
#import <ImageKit/ImageKit.h>
#import <WebCore/DataDetectorsSPI.h>
#import <WebCore/GeometryUtilities.h>
#import <WebCore/LocalizedStrings.h>
#import <WebCore/LookupSPI.h>
#import <WebCore/NSSharingServiceSPI.h>
#import <WebCore/NSSharingServicePickerSPI.h>
#import <WebCore/NSViewSPI.h>
#import <WebCore/SoftLinking.h>
#import <WebCore/TextIndicator.h>
#import <WebCore/URL.h>
SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, ImageKit)
SOFT_LINK_CLASS(ImageKit, IKSlideshow)
using namespace WebCore;
using namespace WebKit;
@interface WKActionMenuController () <NSSharingServiceDelegate, NSSharingServicePickerDelegate, NSPopoverDelegate>
- (void)_updateActionMenuItems;
- (BOOL)_canAddMediaToPhotos;
- (void)_showTextIndicator;
- (void)_hideTextIndicator;
- (void)_clearActionMenuState;
@end
@interface WKView (WKDeprecatedSPI)
- (NSArray *)_actionMenuItemsForHitTestResult:(WKHitTestResultRef)hitTestResult defaultActionMenuItems:(NSArray *)defaultMenuItems;
@end
#if WK_API_ENABLED
static const CGFloat previewViewInset = 3;
static const CGFloat previewViewTitleHeight = 34;
@class WKPagePreviewViewController;
@protocol WKPagePreviewViewControllerDelegate <NSObject>
- (NSView *)pagePreviewViewController:(WKPagePreviewViewController *)pagePreviewViewController viewForPreviewingURL:(NSURL *)url initialFrameSize:(NSSize)initialFrameSize;
- (NSString *)pagePreviewViewController:(WKPagePreviewViewController *)pagePreviewViewController titleForPreviewOfURL:(NSURL *)url;
- (void)pagePreviewViewControllerWasClicked:(WKPagePreviewViewController *)pagePreviewViewController;
@end
@interface WKPagePreviewViewController : NSViewController {
@public
NSSize _mainViewSize;
RetainPtr<NSURL> _url;
RetainPtr<NSView> _previewView;
RetainPtr<NSTextField> _titleTextField;
RetainPtr<NSString> _previewTitle;
id <WKPagePreviewViewControllerDelegate> _delegate;
CGFloat _popoverToViewScale;
}
@property (nonatomic, copy) NSString *previewTitle;
- (instancetype)initWithPageURL:(NSURL *)URL mainViewSize:(NSSize)size popoverToViewScale:(CGFloat)scale;
+ (NSSize)previewPadding;
@end
@implementation WKPagePreviewViewController
- (instancetype)initWithPageURL:(NSURL *)URL mainViewSize:(NSSize)size popoverToViewScale:(CGFloat)scale
{
if (!(self = [super init]))
return nil;
_url = URL;
_mainViewSize = size;
_popoverToViewScale = scale;
return self;
}
- (NSString *)previewTitle
{
return _previewTitle.get();
}
- (void)setPreviewTitle:(NSString *)previewTitle
{
if ([_previewTitle isEqualToString:previewTitle])
return;
// Keep a separate copy around in case this is received before the view hierarchy is created.
_previewTitle = adoptNS([previewTitle copy]);
[_titleTextField setStringValue:previewTitle ? previewTitle : @""];
}
+ (NSSize)previewPadding
{
return NSMakeSize(2 * previewViewInset, previewViewTitleHeight + 2 * previewViewInset);
}
- (void)loadView
{
NSRect defaultFrame = NSMakeRect(0, 0, _mainViewSize.width, _mainViewSize.height);
_previewView = [_delegate pagePreviewViewController:self viewForPreviewingURL:_url.get() initialFrameSize:defaultFrame.size];
if (!_previewView) {
RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:defaultFrame]);
[webView _setIgnoresNonWheelMouseEvents:YES];
if (_url) {
NSURLRequest *request = [NSURLRequest requestWithURL:_url.get()];
[webView loadRequest:request];
}
_previewView = webView;
}
RetainPtr<NSClickGestureRecognizer> clickRecognizer = adoptNS([[NSClickGestureRecognizer alloc] initWithTarget:self action:@selector(_clickRecognized:)]);
[_previewView addGestureRecognizer:clickRecognizer.get()];
NSRect previewFrame = [_previewView frame];
NSRect containerFrame = previewFrame;
NSSize totalPadding = [[self class] previewPadding];
containerFrame.size.width += totalPadding.width;
containerFrame.size.height += totalPadding.height;
previewFrame = NSOffsetRect(previewFrame, previewViewInset, previewViewInset);
RetainPtr<NSView> containerView = adoptNS([[NSView alloc] initWithFrame:containerFrame]);
[containerView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
[containerView addSubview:_previewView.get()];
[_previewView setFrame:previewFrame];
[_previewView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
_titleTextField = adoptNS([[NSTextField alloc] init]);
[_titleTextField setWantsLayer:YES];
[_titleTextField setAutoresizingMask:NSViewWidthSizable | NSViewMinYMargin];
[_titleTextField setEditable:NO];
[_titleTextField setBezeled:NO];
[_titleTextField setDrawsBackground:NO];
[_titleTextField setAlignment:NSCenterTextAlignment];
[_titleTextField setUsesSingleLineMode:YES];
[_titleTextField setLineBreakMode:NSLineBreakByTruncatingTail];
[_titleTextField setTextColor:[NSColor labelColor]];
NSString *title = _previewTitle.get();
if (!title)
title = [_delegate pagePreviewViewController:self titleForPreviewOfURL:_url.get()];
if (!title)
title = [_url absoluteString];
[_titleTextField setStringValue:title ? title : @""];
[_titleTextField sizeToFit];
NSSize titleFittingSize = [_titleTextField frame].size;
CGFloat textFieldCenteringOffset = (NSMaxY(containerFrame) - NSMaxY(previewFrame) - titleFittingSize.height) / 2;
NSRect titleFrame = previewFrame;
titleFrame.size.height = titleFittingSize.height;
titleFrame.origin.y = NSMaxY(previewFrame) + textFieldCenteringOffset;
[_titleTextField setFrame:titleFrame];
[containerView addSubview:_titleTextField.get()];
// Setting the webView bounds will scale it to 75% of the _mainViewSize.
[_previewView setBounds:NSMakeRect(0, 0, _mainViewSize.width / _popoverToViewScale, _mainViewSize.height / _popoverToViewScale)];
self.view = containerView.get();
}
- (void)_clickRecognized:(NSGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer.state == NSGestureRecognizerStateBegan)
[_delegate pagePreviewViewControllerWasClicked:self];
}
@end
@interface WKActionMenuController () <WKPagePreviewViewControllerDelegate>
@end
#endif
@implementation WKActionMenuController
- (instancetype)initWithPage:(WebPageProxy&)page view:(WKView *)wkView
{
self = [super init];
if (!self)
return nil;
_page = &page;
_wkView = wkView;
_type = kWKActionMenuNone;
return self;
}
- (void)willDestroyView:(WKView *)view
{
_page = nullptr;
_wkView = nil;
_hitTestResult = ActionMenuHitTestResult();
_currentActionContext = nil;
}
- (void)wkView:(WKView *)wkView willHandleMouseDown:(NSEvent *)event
{
[self _clearActionMenuState];
}
- (void)prepareForMenu:(NSMenu *)menu withEvent:(NSEvent *)event
{
if (menu != _wkView.actionMenu)
return;
if (_wkView._shouldIgnoreMouseEvents) {
[menu cancelTracking];
return;
}
[self dismissActionMenuPopovers];
_eventLocationInView = [_wkView convertPoint:event.locationInWindow fromView:nil];
_page->performActionMenuHitTestAtLocation(_eventLocationInView);
_state = ActionMenuState::Pending;
[self _updateActionMenuItems];
_shouldKeepPreviewPopoverOpen = NO;
}
- (BOOL)isMenuForTextContent
{
return _type == kWKActionMenuReadOnlyText || _type == kWKActionMenuEditableText || _type == kWKActionMenuEditableTextWithSuggestions;
}
- (void)willOpenMenu:(NSMenu *)menu withEvent:(NSEvent *)event
{
if (menu != _wkView.actionMenu)
return;
if (!menu.numberOfItems)
return;
if (_type == kWKActionMenuDataDetectedItem) {
if (menu.numberOfItems == 1)
_page->clearSelection();
else
_page->selectLastActionMenuRange();
return;
}
if (_type == kWKActionMenuWhitespaceInEditableArea) {
_page->focusAndSelectLastActionMenuHitTestResult();
return;
}
#if WK_API_ENABLED
if (_type == kWKActionMenuLink)
[self _createPreviewPopover];
#endif
if (![self isMenuForTextContent]) {
_page->clearSelection();
return;
}
// Action menus for text should highlight the text so that it is clear what the action menu actions
// will apply to. If the text is already selected, the menu will use the existing selection.
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
if (!hitTestResult->isSelected())
_page->selectLastActionMenuRange();
}
- (void)didCloseMenu:(NSMenu *)menu withEvent:(NSEvent *)event
{
if (menu != _wkView.actionMenu)
return;
[_previewPopover setBehavior:NSPopoverBehaviorTransient];
if (!_shouldKeepPreviewPopoverOpen)
[self _clearPreviewPopover];
[self _clearActionMenuState];
}
- (void)_clearActionMenuState
{
if (_type == kWKActionMenuDataDetectedItem && _currentActionContext && _hasActivatedActionContext) {
[getDDActionsManagerClass() didUseActions];
_hasActivatedActionContext = NO;
}
_state = ActionMenuState::None;
_hitTestResult = ActionMenuHitTestResult();
_type = kWKActionMenuNone;
_sharingServicePicker = nil;
_currentActionContext = nil;
_userData = nil;
}
- (void)didPerformActionMenuHitTest:(const ActionMenuHitTestResult&)hitTestResult userData:(API::Object*)userData
{
// FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies.
_state = ActionMenuState::Ready;
_hitTestResult = hitTestResult;
_userData = userData;
[self _updateActionMenuItems];
}
- (void)dismissActionMenuPopovers
{
DDActionsManager *actionsManager = [getDDActionsManagerClass() sharedManager];
if ([actionsManager respondsToSelector:@selector(requestBubbleClosureUnanchorOnFailure:)])
[actionsManager requestBubbleClosureUnanchorOnFailure:YES];
[self _hideTextIndicator];
[self _clearPreviewPopover];
}
- (void)setPreviewTitle:(NSString *)previewTitle
{
#if WK_API_ENABLED
[_previewViewController setPreviewTitle:previewTitle];
#endif
}
#pragma mark Text Indicator
- (void)_showTextIndicator
{
if (_isShowingTextIndicator)
return;
if (_hitTestResult.detectedDataTextIndicator) {
_page->setTextIndicator(_hitTestResult.detectedDataTextIndicator->data(), false);
_isShowingTextIndicator = YES;
}
}
- (void)_hideTextIndicator
{
if (!_isShowingTextIndicator)
return;
_page->clearTextIndicator();
_isShowingTextIndicator = NO;
}
#pragma mark Link actions
- (NSArray *)_defaultMenuItemsForLink
{
RetainPtr<NSMenuItem> openLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagOpenLinkInDefaultBrowser];
#if WK_API_ENABLED
RetainPtr<NSMenuItem> previewLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPreviewLink];
#else
RetainPtr<NSMenuItem> previewLinkItem = [NSMenuItem separatorItem];
#endif
RetainPtr<NSMenuItem> readingListItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddLinkToSafariReadingList];
return @[ openLinkItem.get(), previewLinkItem.get(), [NSMenuItem separatorItem], readingListItem.get() ];
}
- (void)_openURLFromActionMenu:(id)sender
{
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
[[NSWorkspace sharedWorkspace] openURL:[NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()]];
}
- (void)_addToReadingListFromActionMenu:(id)sender
{
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
NSSharingService *service = [NSSharingService sharingServiceNamed:NSSharingServiceNameAddToSafariReadingList];
[service performWithItems:@[ [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()] ]];
}
#if WK_API_ENABLED
- (void)_keepPreviewOpenFromActionMenu:(id)sender
{
_shouldKeepPreviewPopoverOpen = YES;
}
- (void)_previewURLFromActionMenu:(id)sender
{
ASSERT(_previewPopover);
// We might already have a preview showing if the menu item was highlighted earlier.
if ([_previewPopover isShown])
return;
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
[_previewPopover showRelativeToRect:_popoverOriginRect ofView:_wkView preferredEdge:NSMaxYEdge];
}
- (void)_createPreviewPopover
{
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
NSURL *url = [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()];
_popoverOriginRect = hitTestResult->elementBoundingBox();
NSSize previewPadding = [WKPagePreviewViewController previewPadding];
NSSize popoverSize = [self _preferredPopoverSizeWithPreviewPadding:previewPadding];
CGFloat actualPopoverToViewScale = popoverSize.width / NSWidth(_wkView.bounds);
popoverSize.width += previewPadding.width;
popoverSize.height += previewPadding.height;
_previewViewController = adoptNS([[WKPagePreviewViewController alloc] initWithPageURL:url mainViewSize:_wkView.bounds.size popoverToViewScale:actualPopoverToViewScale]);
_previewViewController->_delegate = self;
[_previewViewController loadView];
_previewPopover = adoptNS([[NSPopover alloc] init]);
[_previewPopover setBehavior:NSPopoverBehaviorApplicationDefined];
[_previewPopover setContentSize:popoverSize];
[_previewPopover setContentViewController:_previewViewController.get()];
[_previewPopover setDelegate:self];
}
static bool targetSizeFitsInAvailableSpace(NSSize targetSize, NSSize availableSpace)
{
return targetSize.width <= availableSpace.width && targetSize.height <= availableSpace.height;
}
- (NSSize)largestPopoverSize
{
NSSize screenSize = _wkView.window.screen.frame.size;
if (screenSize.width == 1280 && screenSize.height == 800)
return NSMakeSize(1240, 674);
if (screenSize.width == 1366 && screenSize.height == 768)
return NSMakeSize(1264, 642);
if (screenSize.width == 1440 && screenSize.height == 900)
return NSMakeSize(1264, 760);
if (screenSize.width == 1680 && screenSize.height == 1050)
return NSMakeSize(1324, 910);
return NSMakeSize(1324, 940);
}
- (NSSize)_preferredPopoverSizeWithPreviewPadding:(NSSize)previewPadding
{
static const CGFloat preferredPopoverToViewScale = 0.75;
static const NSSize screenPadding = {40, 40};
static const NSSize smallestPopoverSize = NSMakeSize(500, 300);
const NSSize effectivePadding = NSMakeSize(screenPadding.width + previewPadding.width, screenPadding.height + previewPadding.height);
NSWindow *window = _wkView.window;
NSRect originScreenRect = [window convertRectToScreen:[_wkView convertRect:_popoverOriginRect toView:nil]];
NSRect screenFrame = window.screen.visibleFrame;
NSRect wkViewBounds = _wkView.bounds;
NSSize targetSize = NSMakeSize(NSWidth(wkViewBounds) * preferredPopoverToViewScale, NSHeight(wkViewBounds) * preferredPopoverToViewScale);
NSSize largestPopoverSize = [self largestPopoverSize];
CGFloat availableSpaceAbove = NSMaxY(screenFrame) - NSMaxY(originScreenRect);
CGFloat availableSpaceBelow = NSMinY(originScreenRect) - NSMinY(screenFrame);
CGFloat maxAvailableVerticalSpace = fmax(availableSpaceAbove, availableSpaceBelow) - effectivePadding.height;
NSSize maxSpaceAvailableOnYEdge = NSMakeSize(screenFrame.size.width - effectivePadding.height, maxAvailableVerticalSpace);
if (targetSizeFitsInAvailableSpace(targetSize, maxSpaceAvailableOnYEdge) && targetSizeFitsInAvailableSpace(targetSize, largestPopoverSize))
return targetSize;
CGFloat availableSpaceAtLeft = NSMinX(originScreenRect) - NSMinX(screenFrame);
CGFloat availableSpaceAtRight = NSMaxX(screenFrame) - NSMaxX(originScreenRect);
CGFloat maxAvailableHorizontalSpace = fmax(availableSpaceAtLeft, availableSpaceAtRight) - effectivePadding.width;
NSSize maxSpaceAvailableOnXEdge = NSMakeSize(maxAvailableHorizontalSpace, screenFrame.size.height - effectivePadding.width);
if (targetSizeFitsInAvailableSpace(targetSize, maxSpaceAvailableOnXEdge) && targetSizeFitsInAvailableSpace(targetSize, largestPopoverSize))
return targetSize;
// Adjust the maximum space available if it is larger than the largest popover size.
if (maxSpaceAvailableOnYEdge.width > largestPopoverSize.width && maxSpaceAvailableOnYEdge.height > largestPopoverSize.height)
maxSpaceAvailableOnYEdge = largestPopoverSize;
if (maxSpaceAvailableOnXEdge.width > largestPopoverSize.width && maxSpaceAvailableOnXEdge.height > largestPopoverSize.height)
maxSpaceAvailableOnXEdge = largestPopoverSize;
// If the target size doesn't fit anywhere, we'll find the largest rect that does fit that also maintains the original view's aspect ratio.
CGFloat aspectRatio = wkViewBounds.size.width / wkViewBounds.size.height;
FloatRect maxVerticalTargetSizePreservingAspectRatioRect = largestRectWithAspectRatioInsideRect(aspectRatio, FloatRect(0, 0, maxSpaceAvailableOnYEdge.width, maxSpaceAvailableOnYEdge.height));
FloatRect maxHorizontalTargetSizePreservingAspectRatioRect = largestRectWithAspectRatioInsideRect(aspectRatio, FloatRect(0, 0, maxSpaceAvailableOnXEdge.width, maxSpaceAvailableOnXEdge.height));
NSSize maxVerticalTargetSizePreservingAspectRatio = NSMakeSize(maxVerticalTargetSizePreservingAspectRatioRect.width(), maxVerticalTargetSizePreservingAspectRatioRect.height());
NSSize maxHortizontalTargetSizePreservingAspectRatio = NSMakeSize(maxHorizontalTargetSizePreservingAspectRatioRect.width(), maxHorizontalTargetSizePreservingAspectRatioRect.height());
NSSize computedTargetSize;
if ((maxVerticalTargetSizePreservingAspectRatio.width * maxVerticalTargetSizePreservingAspectRatio.height) > (maxHortizontalTargetSizePreservingAspectRatio.width * maxHortizontalTargetSizePreservingAspectRatio.height))
computedTargetSize = maxVerticalTargetSizePreservingAspectRatio;
computedTargetSize = maxHortizontalTargetSizePreservingAspectRatio;
// Now make sure what we've computed isn't too small.
if (computedTargetSize.width < smallestPopoverSize.width && computedTargetSize.height < smallestPopoverSize.height) {
float limitWidth = smallestPopoverSize.width > computedTargetSize.width ? smallestPopoverSize.width : computedTargetSize.width;
float limitHeight = smallestPopoverSize.height > computedTargetSize.height ? smallestPopoverSize.height : computedTargetSize.height;
FloatRect targetRectLargerThanMinSize = largestRectWithAspectRatioInsideRect(aspectRatio, FloatRect(0, 0, limitWidth, limitHeight));
computedTargetSize = NSMakeSize(targetRectLargerThanMinSize.size().width(), targetRectLargerThanMinSize.size().height());
// If our orignal computedTargetSize was so small that we had to get here and make a new computedTargetSize that is
// larger than the minimum, then the elementBoundingBox of the _hitTestResult is probably huge. So we should use
// the event origin as the popover origin in this case and not worry about obscuring the _hitTestResult.
_popoverOriginRect.origin = _eventLocationInView;
_popoverOriginRect.size = NSMakeSize(1, 1);
}
return computedTargetSize;
}
#endif // WK_API_ENABLED
- (void)_clearPreviewPopover
{
#if WK_API_ENABLED
if (_previewViewController) {
_previewViewController->_delegate = nil;
[_wkView _finishPreviewingURL:_previewViewController->_url.get() withPreviewView:_previewViewController->_previewView.get()];
_previewViewController = nil;
}
#endif
[_previewPopover close];
[_previewPopover setDelegate:nil];
_previewPopover = nil;
}
#pragma mark Video actions
- (NSArray *)_defaultMenuItemsForVideo
{
RetainPtr<NSMenuItem> copyVideoURLItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyVideoURL];
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
RetainPtr<NSMenuItem> saveToDownloadsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagSaveVideoToDownloads];
RetainPtr<NSMenuItem> shareItem = [self _createActionMenuItemForTag:kWKContextActionItemTagShareVideo];
String urlToShare = hitTestResult->absoluteMediaURL();
if (!hitTestResult->isDownloadableMedia()) {
[saveToDownloadsItem setEnabled:NO];
urlToShare = _page->mainFrame()->url();
}
if (!urlToShare.isEmpty()) {
_sharingServicePicker = adoptNS([[NSSharingServicePicker alloc] initWithItems:@[ urlToShare ]]);
[_sharingServicePicker setDelegate:self];
[shareItem setSubmenu:[_sharingServicePicker menu]];
}
return @[ copyVideoURLItem.get(), [NSMenuItem separatorItem], saveToDownloadsItem.get(), shareItem.get() ];
}
- (void)_copyVideoURL:(id)sender
{
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
String urlToCopy = hitTestResult->absoluteMediaURL();
if (!hitTestResult->isDownloadableMedia())
urlToCopy = _page->mainFrame()->url();
[[NSPasteboard generalPasteboard] clearContents];
[[NSPasteboard generalPasteboard] writeObjects:@[ urlToCopy ]];
}
- (void)_saveVideoToDownloads:(id)sender
{
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
_page->process().context().download(_page, hitTestResult->absoluteMediaURL());
}
#pragma mark Image actions
- (NSImage *)_hitTestResultImage
{
RefPtr<SharedMemory> imageSharedMemory = _hitTestResult.imageSharedMemory;
if (!imageSharedMemory)
return nil;
RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithData:[NSData dataWithBytes:imageSharedMemory->data() length:imageSharedMemory->size()]]);
return nsImage.autorelease();
}
- (NSArray *)_defaultMenuItemsForImage
{
RetainPtr<NSMenuItem> copyImageItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyImage];
RetainPtr<NSMenuItem> addToPhotosItem;
if ([self _canAddMediaToPhotos])
addToPhotosItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddImageToPhotos];
else
addToPhotosItem = [NSMenuItem separatorItem];
RetainPtr<NSMenuItem> saveToDownloadsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagSaveImageToDownloads];
RetainPtr<NSMenuItem> shareItem = [self _createActionMenuItemForTag:kWKContextActionItemTagShareImage];
if (RetainPtr<NSImage> image = [self _hitTestResultImage]) {
_sharingServicePicker = adoptNS([[NSSharingServicePicker alloc] initWithItems:@[ image.get() ]]);
[_sharingServicePicker setDelegate:self];
[shareItem setSubmenu:[_sharingServicePicker menu]];
}
return @[ copyImageItem.get(), addToPhotosItem.get(), saveToDownloadsItem.get(), shareItem.get() ];
}
- (void)_copyImage:(id)sender
{
RetainPtr<NSImage> image = [self _hitTestResultImage];
if (!image)
return;
[[NSPasteboard generalPasteboard] clearContents];
[[NSPasteboard generalPasteboard] writeObjects:@[ image.get() ]];
}
- (void)_saveImageToDownloads:(id)sender
{
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
_page->process().context().download(_page, hitTestResult->absoluteImageURL());
}
// FIXME: We should try to share this with WebPageProxyMac's similar PDF functions.
static NSString *temporaryPhotosDirectoryPath()
{
static NSString *temporaryPhotosDirectoryPath;
if (!temporaryPhotosDirectoryPath) {
NSString *temporaryDirectoryTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPhotos-XXXXXX"];
CString templateRepresentation = [temporaryDirectoryTemplate fileSystemRepresentation];
if (mkdtemp(templateRepresentation.mutableData()))
temporaryPhotosDirectoryPath = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:templateRepresentation.data() length:templateRepresentation.length()] copy];
}
return temporaryPhotosDirectoryPath;
}
static NSString *pathToPhotoOnDisk(NSString *suggestedFilename)
{
NSString *photoDirectoryPath = temporaryPhotosDirectoryPath();
if (!photoDirectoryPath) {
WTFLogAlways("Cannot create temporary photo download directory.");
return nil;
}
NSString *path = [photoDirectoryPath stringByAppendingPathComponent:suggestedFilename];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:path]) {
NSString *pathTemplatePrefix = [photoDirectoryPath stringByAppendingPathComponent:@"XXXXXX-"];
NSString *pathTemplate = [pathTemplatePrefix stringByAppendingString:suggestedFilename];
CString pathTemplateRepresentation = [pathTemplate fileSystemRepresentation];
int fd = mkstemps(pathTemplateRepresentation.mutableData(), pathTemplateRepresentation.length() - strlen([pathTemplatePrefix fileSystemRepresentation]) + 1);
if (fd < 0) {
WTFLogAlways("Cannot create photo file in the temporary directory (%@).", suggestedFilename);
return nil;
}
close(fd);
path = [fileManager stringWithFileSystemRepresentation:pathTemplateRepresentation.data() length:pathTemplateRepresentation.length()];
}
return path;
}
- (BOOL)_canAddMediaToPhotos
{
return [getIKSlideshowClass() canExportToApplication:@"com.apple.Photos"];
}
- (void)_addImageToPhotos:(id)sender
{
if (![self _canAddMediaToPhotos])
return;
RefPtr<SharedMemory> imageSharedMemory = _hitTestResult.imageSharedMemory;
if (!imageSharedMemory->size() || _hitTestResult.imageExtension.isEmpty())
return;
RetainPtr<NSData> imageData = adoptNS([[NSData alloc] initWithBytes:imageSharedMemory->data() length:imageSharedMemory->size()]);
RetainPtr<NSString> suggestedFilename = [[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:_hitTestResult.imageExtension];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *filePath = pathToPhotoOnDisk(suggestedFilename.get());
if (!filePath)
return;
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
[imageData writeToURL:fileURL atomically:NO];
dispatch_async(dispatch_get_main_queue(), ^{
// This API provides no way to report failure, but if 18420778 is fixed so that it does, we should handle this.
[getIKSlideshowClass() exportSlideshowItem:filePath toApplication:@"com.apple.Photos"];
});
});
}
#pragma mark Text actions
- (NSArray *)_defaultMenuItemsForDataDetectedText
{
DDActionContext *actionContext = _hitTestResult.actionContext.get();
if (!actionContext)
return @[ ];
actionContext.altMode = YES;
if ([[getDDActionsManagerClass() sharedManager] respondsToSelector:@selector(hasActionsForResult:actionContext:)]) {
if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:actionContext.mainResult actionContext:actionContext])
return @[ ];
}
// Ref our WebPageProxy for use in the blocks below.
RefPtr<WebPageProxy> page = _page;
PageOverlay::PageOverlayID overlayID = _hitTestResult.detectedDataOriginatingPageOverlay;
_currentActionContext = [actionContext contextForView:_wkView altMode:YES interactionStartedHandler:^() {
page->send(Messages::WebPage::DataDetectorsDidPresentUI(overlayID));
} interactionChangedHandler:^() {
[self _showTextIndicator];
page->send(Messages::WebPage::DataDetectorsDidChangeUI(overlayID));
} interactionStoppedHandler:^() {
[self _hideTextIndicator];
page->send(Messages::WebPage::DataDetectorsDidHideUI(overlayID));
}];
[_currentActionContext setHighlightFrame:[_wkView.window convertRectToScreen:[_wkView convertRect:_hitTestResult.detectedDataBoundingBox toView:nil]]];
NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
if (menuItems.count == 1 && _hitTestResult.detectedDataTextIndicator)
_hitTestResult.detectedDataTextIndicator->setPresentationTransition(TextIndicatorPresentationTransition::Bounce);
return menuItems;
}
- (NSArray *)_defaultMenuItemsForText
{
RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyText];
RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagLookupText];
return @[ copyTextItem.get(), lookupTextItem.get() ];
}
- (NSArray *)_defaultMenuItemsForEditableText
{
RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyText];
RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagLookupText];
RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPaste];
return @[ copyTextItem.get(), lookupTextItem.get(), pasteItem.get() ];
}
- (NSArray *)_defaultMenuItemsForEditableTextWithSuggestions
{
if (_hitTestResult.lookupText.isEmpty())
return @[ ];
Vector<TextCheckingResult> results;
_page->checkTextOfParagraph(_hitTestResult.lookupText, NSTextCheckingTypeSpelling, results);
if (results.isEmpty())
return @[ ];
Vector<String> guesses;
_page->getGuessesForWord(_hitTestResult.lookupText, String(), guesses);
if (guesses.isEmpty())
return @[ ];
RetainPtr<NSMenu> spellingSubMenu = adoptNS([[NSMenu alloc] init]);
for (const auto& guess : guesses) {
RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:guess action:@selector(_changeSelectionToSuggestion:) keyEquivalent:@""]);
[item setRepresentedObject:guess];
[item setTarget:self];
[spellingSubMenu addItem:item.get()];
}
RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyText];
RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagLookupText];
RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPaste];
RetainPtr<NSMenuItem> textSuggestionsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagTextSuggestions];
[textSuggestionsItem setSubmenu:spellingSubMenu.get()];
return @[ copyTextItem.get(), lookupTextItem.get(), pasteItem.get(), textSuggestionsItem.get() ];
}
- (void)_copySelection:(id)sender
{
_page->executeEditCommand("copy");
}
- (void)_paste:(id)sender
{
_page->executeEditCommand("paste");
}
- (void)_lookupText:(id)sender
{
_page->performDictionaryLookupOfCurrentSelection();
}
- (void)_changeSelectionToSuggestion:(id)sender
{
NSString *selectedCorrection = [sender representedObject];
if (!selectedCorrection)
return;
ASSERT([selectedCorrection isKindOfClass:[NSString class]]);
_page->changeSpellingToWord(selectedCorrection);
}
#pragma mark Whitespace actions
- (NSArray *)_defaultMenuItemsForWhitespaceInEditableArea
{
RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPaste];
return @[ [NSMenuItem separatorItem], [NSMenuItem separatorItem], pasteItem.get() ];
}
#pragma mark Mailto Link actions
- (NSArray *)_defaultMenuItemsForMailtoLink
{
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
// FIXME: Should this show a yellow highlight?
RetainPtr<DDActionContext> actionContext = [[getDDActionContextClass() alloc] init];
[actionContext setAltMode:YES];
[actionContext setHighlightFrame:[_wkView.window convertRectToScreen:[_wkView convertRect:hitTestResult->elementBoundingBox() toView:nil]]];
return [[getDDActionsManagerClass() sharedManager] menuItemsForTargetURL:hitTestResult->absoluteLinkURL() actionContext:actionContext.get()];
}
#pragma mark NSMenuDelegate implementation
- (void)menuNeedsUpdate:(NSMenu *)menu
{
if (menu != _wkView.actionMenu)
return;
ASSERT(_state != ActionMenuState::None);
// FIXME: We need to be able to cancel this if the menu goes away.
// FIXME: Connection can be null if the process is closed; we should clean up better in that case.
if (_state == ActionMenuState::Pending) {
if (auto* connection = _page->process().connection())
connection->waitForAndDispatchImmediately<Messages::WebPageProxy::DidPerformActionMenuHitTest>(_page->pageID(), std::chrono::milliseconds(500));
}
if (_state != ActionMenuState::Ready)
[self _updateActionMenuItems];
if (_type == kWKActionMenuDataDetectedItem && _currentActionContext) {
_hasActivatedActionContext = YES;
if (![getDDActionsManagerClass() shouldUseActionsWithContext:_currentActionContext.get()]) {
[menu cancelTracking];
[menu removeAllItems];
}
}
}
- (void)menu:(NSMenu *)menu willHighlightItem:(NSMenuItem *)item
{
#if WK_API_ENABLED
if (item.tag != kWKContextActionItemTagPreviewLink)
return;
[self _previewURLFromActionMenu:item];
#endif
}
#pragma mark NSSharingServicePickerDelegate implementation
- (NSArray *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items mask:(NSSharingServiceMask)mask proposedSharingServices:(NSArray *)proposedServices
{
RetainPtr<NSMutableArray> services = adoptNS([[NSMutableArray alloc] initWithCapacity:proposedServices.count]);
for (NSSharingService *service in proposedServices) {
if ([service.name isEqualToString:NSSharingServiceNameAddToIPhoto])
continue;
[services addObject:service];
}
return services.autorelease();
}
- (id <NSSharingServiceDelegate>)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker delegateForSharingService:(NSSharingService *)sharingService
{
return self;
}
#pragma mark NSSharingServiceDelegate implementation
- (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope
{
return _wkView.window;
}
#pragma mark NSPopoverDelegate implementation
- (void)popoverWillClose:(NSNotification *)notification
{
_shouldKeepPreviewPopoverOpen = NO;
[self _clearPreviewPopover];
}
#pragma mark Menu Items
- (RetainPtr<NSMenuItem>)_createActionMenuItemForTag:(uint32_t)tag
{
SEL selector = nullptr;
NSString *title = nil;
NSImage *image = nil;
bool enabled = true;
switch (tag) {
case kWKContextActionItemTagOpenLinkInDefaultBrowser:
selector = @selector(_openURLFromActionMenu:);
title = WEB_UI_STRING_KEY("Open", "Open (action menu item)", "action menu item");
image = [NSImage imageNamed:@"NSActionMenuOpenInNewWindow"];
break;
#if WK_API_ENABLED
case kWKContextActionItemTagPreviewLink:
selector = @selector(_keepPreviewOpenFromActionMenu:);
title = @"";
image = [NSImage imageNamed:@"NSActionMenuQuickLook"];
break;
#endif
case kWKContextActionItemTagAddLinkToSafariReadingList:
selector = @selector(_addToReadingListFromActionMenu:);
title = WEB_UI_STRING_KEY("Add to Reading List", "Add to Reading List (action menu item)", "action menu item");
image = [NSImage imageNamed:@"NSActionMenuAddToReadingList"];
break;
case kWKContextActionItemTagCopyImage:
selector = @selector(_copyImage:);
title = WEB_UI_STRING_KEY("Copy", "Copy (image action menu item)", "image action menu item");
image = [NSImage imageNamed:@"NSActionMenuCopy"];
break;
case kWKContextActionItemTagAddImageToPhotos:
selector = @selector(_addImageToPhotos:);
title = WEB_UI_STRING_KEY("Add to Photos", "Add to Photos (action menu item)", "action menu item");
image = [NSImage imageNamed:@"NSActionMenuAddToPhotos"];
break;
case kWKContextActionItemTagSaveImageToDownloads:
selector = @selector(_saveImageToDownloads:);
title = WEB_UI_STRING_KEY("Save to Downloads", "Save to Downloads (image action menu item)", "image action menu item");
image = [NSImage imageNamed:@"NSActionMenuSaveToDownloads"];
break;
case kWKContextActionItemTagShareImage:
title = WEB_UI_STRING_KEY("Share (image action menu item)", "Share (image action menu item)", "image action menu item");
image = [NSImage imageNamed:@"NSActionMenuShare"];
break;
case kWKContextActionItemTagCopyText:
selector = @selector(_copySelection:);
title = WEB_UI_STRING_KEY("Copy", "Copy (text action menu item)", "text action menu item");
image = [NSImage imageNamed:@"NSActionMenuCopy"];
break;
case kWKContextActionItemTagLookupText:
selector = @selector(_lookupText:);
title = WEB_UI_STRING_KEY("Look Up", "Look Up (action menu item)", "action menu item");
image = [NSImage imageNamed:@"NSActionMenuLookup"];
enabled = getLULookupDefinitionModuleClass();
break;
case kWKContextActionItemTagPaste:
selector = @selector(_paste:);
title = WEB_UI_STRING_KEY("Paste", "Paste (action menu item)", "action menu item");
image = [NSImage imageNamed:@"NSActionMenuPaste"];
break;
case kWKContextActionItemTagTextSuggestions:
title = WEB_UI_STRING_KEY("Suggestions", "Suggestions (action menu item)", "action menu item");
image = [NSImage imageNamed:@"NSActionMenuSpelling"];
break;
case kWKContextActionItemTagCopyVideoURL:
selector = @selector(_copyVideoURL:);
title = WEB_UI_STRING_KEY("Copy", "Copy (video action menu item)", "video action menu item");
image = [NSImage imageNamed:@"NSActionMenuCopy"];
break;
case kWKContextActionItemTagSaveVideoToDownloads:
selector = @selector(_saveVideoToDownloads:);
title = WEB_UI_STRING_KEY("Save to Downloads", "Save to Downloads (video action menu item)", "video action menu item");
image = [NSImage imageNamed:@"NSActionMenuSaveToDownloads"];
break;
case kWKContextActionItemTagShareVideo:
title = WEB_UI_STRING_KEY("Share", "Share (video action menu item)", "video action menu item");
image = [NSImage imageNamed:@"NSActionMenuShare"];
break;
default:
ASSERT_NOT_REACHED();
return nil;
}
RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]);
[item setImage:image];
[item setTarget:self];
[item setTag:tag];
[item setEnabled:enabled];
return item;
}
- (PassRefPtr<WebHitTestResult>)_webHitTestResult
{
RefPtr<WebHitTestResult> hitTestResult;
if (_state == ActionMenuState::Ready)
hitTestResult = WebHitTestResult::create(_hitTestResult.hitTestResult);
else
hitTestResult = _page->lastMouseMoveHitTestResult();
return hitTestResult.release();
}
- (NSArray *)_defaultMenuItems
{
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
if (!hitTestResult) {
_type = kWKActionMenuNone;
return _state != ActionMenuState::Ready ? @[ [NSMenuItem separatorItem] ] : @[ ];
}
String absoluteLinkURL = hitTestResult->absoluteLinkURL();
if (!absoluteLinkURL.isEmpty()) {
if (WebCore::protocolIsInHTTPFamily(absoluteLinkURL)) {
_type = kWKActionMenuLink;
return [self _defaultMenuItemsForLink];
}
if (protocolIs(absoluteLinkURL, "mailto")) {
_type = kWKActionMenuMailtoLink;
return [self _defaultMenuItemsForMailtoLink];
}
}
if (!hitTestResult->absoluteMediaURL().isEmpty()) {
_type = kWKActionMenuVideo;
return [self _defaultMenuItemsForVideo];
}
if (!hitTestResult->absoluteImageURL().isEmpty() && _hitTestResult.imageSharedMemory && !_hitTestResult.imageExtension.isEmpty()) {
_type = kWKActionMenuImage;
return [self _defaultMenuItemsForImage];
}
if (hitTestResult->isTextNode()) {
NSArray *dataDetectorMenuItems = [self _defaultMenuItemsForDataDetectedText];
if (_currentActionContext) {
// If this is a data detected item with no menu items, we should not fall back to regular text options.
if (!dataDetectorMenuItems.count) {
_type = kWKActionMenuNone;
return @[ ];
}
_type = kWKActionMenuDataDetectedItem;
return dataDetectorMenuItems;
}
if (hitTestResult->isContentEditable()) {
NSArray *editableTextWithSuggestions = [self _defaultMenuItemsForEditableTextWithSuggestions];
if (editableTextWithSuggestions.count) {
_type = kWKActionMenuEditableTextWithSuggestions;
return editableTextWithSuggestions;
}
_type = kWKActionMenuEditableText;
return [self _defaultMenuItemsForEditableText];
}
_type = kWKActionMenuReadOnlyText;
return [self _defaultMenuItemsForText];
}
if (hitTestResult->isContentEditable()) {
_type = kWKActionMenuWhitespaceInEditableArea;
return [self _defaultMenuItemsForWhitespaceInEditableArea];
}
if (hitTestResult->isSelected()) {
// A selection should present the read-only text menu. It might make more sense to present a new
// type of menu with just copy, but for the time being, we should stay consistent with text.
_type = kWKActionMenuReadOnlyText;
return [self _defaultMenuItemsForText];
}
_type = kWKActionMenuNone;
return _state != ActionMenuState::Ready ? @[ [NSMenuItem separatorItem] ] : @[ ];
}
- (void)_updateActionMenuItems
{
[_wkView.actionMenu removeAllItems];
NSArray *menuItems = [self _defaultMenuItems];
RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
if ([_wkView respondsToSelector:@selector(_actionMenuItemsForHitTestResult:defaultActionMenuItems:)])
menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(hitTestResult.get()) defaultActionMenuItems:menuItems];
else
menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(hitTestResult.get()) withType:_type defaultActionMenuItems:menuItems userData:toAPI(_userData.get())];
for (NSMenuItem *item in menuItems)
[_wkView.actionMenu addItem:item];
if (_state == ActionMenuState::Ready && !_wkView.actionMenu.numberOfItems)
[_wkView.actionMenu cancelTracking];
}
#if WK_API_ENABLED
#pragma mark WKPagePreviewViewControllerDelegate
- (NSView *)pagePreviewViewController:(WKPagePreviewViewController *)pagePreviewViewController viewForPreviewingURL:(NSURL *)url initialFrameSize:(NSSize)initialFrameSize
{
return [_wkView _viewForPreviewingURL:url initialFrameSize:initialFrameSize];
}
- (NSString *)pagePreviewViewController:(WKPagePreviewViewController *)pagePreviewViewController titleForPreviewOfURL:(NSURL *)url
{
return [_wkView _titleForPreviewOfURL:url];
}
- (void)pagePreviewViewControllerWasClicked:(WKPagePreviewViewController *)pagePreviewViewController
{
if (NSURL *url = pagePreviewViewController->_url.get())
[_wkView _handleClickInPreviewView:pagePreviewViewController->_previewView.get() URL:url];
}
#endif
@end
#endif // PLATFORM(MAC)