Data detected results with no menu items should not fall through to regular text
[WebKit-https.git] / Source / WebKit2 / UIProcess / mac / WKActionMenuController.mm
1 /*
2  * Copyright (C) 2014 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "WKActionMenuController.h"
28
29 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
30
31 #import "TextIndicator.h"
32 #import "WKNSURLExtras.h"
33 #import "WKViewInternal.h"
34 #import "WKWebView.h"
35 #import "WKWebViewInternal.h"
36 #import "WebContext.h"
37 #import "WebKitSystemInterface.h"
38 #import "WebPageMessages.h"
39 #import "WebPageProxy.h"
40 #import "WebPageProxyMessages.h"
41 #import "WebProcessProxy.h"
42 #import <Foundation/Foundation.h>
43 #import <ImageIO/ImageIO.h>
44 #import <ImageKit/ImageKit.h>
45 #import <WebCore/DataDetectorsSPI.h>
46 #import <WebCore/GeometryUtilities.h>
47 #import <WebCore/LocalizedStrings.h>
48 #import <WebCore/LookupSPI.h>
49 #import <WebCore/NSSharingServiceSPI.h>
50 #import <WebCore/NSSharingServicePickerSPI.h>
51 #import <WebCore/NSViewSPI.h>
52 #import <WebCore/SoftLinking.h>
53 #import <WebCore/URL.h>
54
55 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, ImageKit)
56 SOFT_LINK_CLASS(ImageKit, IKSlideshow)
57
58 using namespace WebCore;
59 using namespace WebKit;
60
61 @interface WKActionMenuController () <NSSharingServiceDelegate, NSSharingServicePickerDelegate, NSPopoverDelegate>
62 - (void)_updateActionMenuItems;
63 - (BOOL)_canAddMediaToPhotos;
64 - (void)_showTextIndicator;
65 - (void)_hideTextIndicator;
66 @end
67
68 @interface WKView (WKDeprecatedSPI)
69 - (NSArray *)_actionMenuItemsForHitTestResult:(WKHitTestResultRef)hitTestResult defaultActionMenuItems:(NSArray *)defaultMenuItems;
70 @end
71
72 #if WK_API_ENABLED
73
74 @class WKPagePreviewViewController;
75
76 @protocol WKPagePreviewViewControllerDelegate <NSObject>
77 - (NSView *)pagePreviewViewController:(WKPagePreviewViewController *)pagePreviewViewController viewForPreviewingURL:(NSURL *)url initialFrameSize:(NSSize)initialFrameSize;
78 - (void)pagePreviewViewControllerWasClicked:(WKPagePreviewViewController *)pagePreviewViewController;
79 @end
80
81 @interface WKPagePreviewViewController : NSViewController {
82 @public
83     NSSize _mainViewSize;
84     RetainPtr<NSURL> _url;
85     id <WKPagePreviewViewControllerDelegate> _delegate;
86     CGFloat _popoverToViewScale;
87 }
88
89 - (instancetype)initWithPageURL:(NSURL *)URL mainViewSize:(NSSize)size popoverToViewScale:(CGFloat)scale;
90
91 @end
92
93 @implementation WKPagePreviewViewController
94
95 - (instancetype)initWithPageURL:(NSURL *)URL mainViewSize:(NSSize)size popoverToViewScale:(CGFloat)scale
96 {
97     if (!(self = [super init]))
98         return nil;
99
100     _url = URL;
101     _mainViewSize = size;
102     _popoverToViewScale = scale;
103
104     return self;
105 }
106
107 - (void)loadView
108 {
109     NSRect defaultFrame = NSMakeRect(0, 0, _mainViewSize.width, _mainViewSize.height);
110     RetainPtr<NSView> previewView = [_delegate pagePreviewViewController:self viewForPreviewingURL:_url.get() initialFrameSize:defaultFrame.size];
111     if (!previewView) {
112         RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:defaultFrame]);
113         [webView _setIgnoresNonWheelMouseEvents:YES];
114         if (_url) {
115             NSURLRequest *request = [NSURLRequest requestWithURL:_url.get()];
116             [webView loadRequest:request];
117         }
118         previewView = webView;
119     }
120
121     // Setting the webView bounds will scale it to 75% of the _mainViewSize.
122     [previewView setBounds:NSMakeRect(0, 0, _mainViewSize.width / _popoverToViewScale, _mainViewSize.height / _popoverToViewScale)];
123
124     RetainPtr<NSClickGestureRecognizer> clickRecognizer = adoptNS([[NSClickGestureRecognizer alloc] initWithTarget:self action:@selector(_clickRecognized:)]);
125     [previewView addGestureRecognizer:clickRecognizer.get()];
126     self.view = previewView.get();
127 }
128
129 - (void)_clickRecognized:(NSGestureRecognizer *)gestureRecognizer
130 {
131     [_delegate pagePreviewViewControllerWasClicked:self];
132 }
133
134 @end
135
136 @interface WKActionMenuController () <WKPagePreviewViewControllerDelegate>
137 @end
138
139 #endif
140
141 @implementation WKActionMenuController
142
143 - (instancetype)initWithPage:(WebPageProxy&)page view:(WKView *)wkView
144 {
145     self = [super init];
146
147     if (!self)
148         return nil;
149
150     _page = &page;
151     _wkView = wkView;
152     _type = kWKActionMenuNone;
153
154     return self;
155 }
156
157 - (void)willDestroyView:(WKView *)view
158 {
159     _page = nullptr;
160     _wkView = nil;
161     _hitTestResult = ActionMenuHitTestResult();
162     _currentActionContext = nil;
163 }
164
165 - (void)prepareForMenu:(NSMenu *)menu withEvent:(NSEvent *)event
166 {
167     if (menu != _wkView.actionMenu)
168         return;
169
170     if (_wkView._shouldIgnoreMouseEvents) {
171         [menu cancelTracking];
172         return;
173     }
174
175     [self dismissActionMenuPopovers];
176
177     _page->performActionMenuHitTestAtLocation([_wkView convertPoint:event.locationInWindow fromView:nil]);
178
179     _state = ActionMenuState::Pending;
180     [self _updateActionMenuItems];
181
182     _shouldKeepPreviewPopoverOpen = NO;
183 }
184
185 - (BOOL)isMenuForTextContent
186 {
187     return _type == kWKActionMenuReadOnlyText || _type == kWKActionMenuEditableText || _type == kWKActionMenuEditableTextWithSuggestions;
188 }
189
190 - (void)willOpenMenu:(NSMenu *)menu withEvent:(NSEvent *)event
191 {
192     if (menu != _wkView.actionMenu)
193         return;
194
195     if (_type == kWKActionMenuDataDetectedItem) {
196         if (_currentActionContext && ![getDDActionsManagerClass() shouldUseActionsWithContext:_currentActionContext.get()]) {
197             [menu cancelTracking];
198             return;
199         }
200         if (menu.numberOfItems == 1)
201             _page->clearSelection();
202         else
203             _page->selectLastActionMenuRange();
204         return;
205     }
206
207     if (_type == kWKActionMenuWhitespaceInEditableArea) {
208         _page->focusAndSelectLastActionMenuHitTestResult();
209         return;
210     }
211
212     if (![self isMenuForTextContent]) {
213 #if WK_API_ENABLED
214         [self _createPreviewPopover];
215 #endif
216         _page->clearSelection();
217         return;
218     }
219
220     // Action menus for text should highlight the text so that it is clear what the action menu actions
221     // will apply to. If the text is already selected, the menu will use the existing selection.
222     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
223     if (!hitTestResult->isSelected())
224         _page->selectLastActionMenuRange();
225 }
226
227 - (void)didCloseMenu:(NSMenu *)menu withEvent:(NSEvent *)event
228 {
229     if (menu != _wkView.actionMenu)
230         return;
231
232     if (_type == kWKActionMenuDataDetectedItem) {
233         if (_currentActionContext)
234             [getDDActionsManagerClass() didUseActions];
235     }
236
237     [_previewPopover setBehavior:NSPopoverBehaviorTransient];
238     if (!_shouldKeepPreviewPopoverOpen)
239         [self _clearPreviewPopover];
240
241     _state = ActionMenuState::None;
242     _hitTestResult = ActionMenuHitTestResult();
243     _type = kWKActionMenuNone;
244     _sharingServicePicker = nil;
245     _currentActionContext = nil;
246 }
247
248 - (void)didPerformActionMenuHitTest:(const ActionMenuHitTestResult&)hitTestResult userData:(API::Object*)userData
249 {
250     // FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies.
251     _state = ActionMenuState::Ready;
252     _hitTestResult = hitTestResult;
253     _userData = userData;
254
255     [self _updateActionMenuItems];
256 }
257
258 - (void)dismissActionMenuPopovers
259 {
260     DDActionsManager *actionsManager = [getDDActionsManagerClass() sharedManager];
261     if ([actionsManager respondsToSelector:@selector(requestBubbleClosureUnanchorOnFailure:)])
262         [actionsManager requestBubbleClosureUnanchorOnFailure:YES];
263
264     [self _hideTextIndicator];
265     [self _clearPreviewPopover];
266 }
267
268 #pragma mark Text Indicator
269
270 - (void)_showTextIndicator
271 {
272     if (_isShowingTextIndicator)
273         return;
274
275     if (_hitTestResult.detectedDataTextIndicator) {
276         _page->setTextIndicator(_hitTestResult.detectedDataTextIndicator->data(), false, true);
277         _isShowingTextIndicator = YES;
278     }
279 }
280
281 - (void)_hideTextIndicator
282 {
283     if (!_isShowingTextIndicator)
284         return;
285
286     _page->clearTextIndicator(false, true);
287     _isShowingTextIndicator = NO;
288 }
289
290 #pragma mark Link actions
291
292 - (NSArray *)_defaultMenuItemsForLink
293 {
294     RetainPtr<NSMenuItem> openLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagOpenLinkInDefaultBrowser];
295 #if WK_API_ENABLED
296     RetainPtr<NSMenuItem> previewLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPreviewLink];
297 #else
298     RetainPtr<NSMenuItem> previewLinkItem = [NSMenuItem separatorItem];
299 #endif
300     RetainPtr<NSMenuItem> readingListItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddLinkToSafariReadingList];
301
302     return @[ openLinkItem.get(), previewLinkItem.get(), [NSMenuItem separatorItem], readingListItem.get() ];
303 }
304
305 - (void)_openURLFromActionMenu:(id)sender
306 {
307     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
308     [[NSWorkspace sharedWorkspace] openURL:[NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()]];
309 }
310
311 - (void)_addToReadingListFromActionMenu:(id)sender
312 {
313     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
314     NSSharingService *service = [NSSharingService sharingServiceNamed:NSSharingServiceNameAddToSafariReadingList];
315     [service performWithItems:@[ [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()] ]];
316 }
317
318 #if WK_API_ENABLED
319 - (void)_keepPreviewOpenFromActionMenu:(id)sender
320 {
321     _shouldKeepPreviewPopoverOpen = YES;
322 }
323
324 - (void)_previewURLFromActionMenu:(id)sender
325 {
326     ASSERT(_previewPopover);
327
328     // We might already have a preview showing if the menu item was highlighted earlier.
329     if ([_previewPopover isShown])
330         return;
331
332     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
333     NSRect originRect = hitTestResult->elementBoundingBox();
334     [_previewPopover showRelativeToRect:originRect ofView:_wkView preferredEdge:NSMaxYEdge];
335 }
336
337 - (void)_createPreviewPopover
338 {
339     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
340     NSURL *url = [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()];
341     NSRect originRect = hitTestResult->elementBoundingBox();
342
343     NSSize popoverSize = [self _preferredSizeForPopoverPresentedFromOriginRect:originRect];
344     CGFloat actualPopoverToViewScale = popoverSize.width / NSWidth(_wkView.bounds);
345     _previewViewController = adoptNS([[WKPagePreviewViewController alloc] initWithPageURL:url mainViewSize:_wkView.bounds.size popoverToViewScale:actualPopoverToViewScale]);
346     _previewViewController->_delegate = self;
347     [_previewViewController loadView];
348
349     _previewPopover = adoptNS([[NSPopover alloc] init]);
350     [_previewPopover setBehavior:NSPopoverBehaviorApplicationDefined];
351     [_previewPopover setContentSize:popoverSize];
352     [_previewPopover setContentViewController:_previewViewController.get()];
353     [_previewPopover setDelegate:self];
354 }
355
356 static bool targetSizeFitsInAvailableSpace(NSSize targetSize, NSSize availableSpace)
357 {
358     return targetSize.width <= availableSpace.width && targetSize.height <= availableSpace.height;
359 }
360
361 - (NSSize)_preferredSizeForPopoverPresentedFromOriginRect:(NSRect)originRect
362 {
363     static const CGFloat preferredPopoverToViewScale = 0.75;
364     static const CGFloat screenPadding = 40;
365
366     NSWindow *window = _wkView.window;
367     NSRect originScreenRect = [window convertRectToScreen:[_wkView convertRect:originRect toView:nil]];
368     NSRect screenFrame = window.screen.visibleFrame;
369
370     NSRect wkViewBounds = _wkView.bounds;
371     NSSize targetSize = NSMakeSize(NSWidth(wkViewBounds) * preferredPopoverToViewScale, NSHeight(wkViewBounds) * preferredPopoverToViewScale);
372
373     CGFloat availableSpaceAbove = NSMaxY(screenFrame) - NSMaxY(originScreenRect);
374     CGFloat availableSpaceBelow = NSMinY(originScreenRect) - NSMinY(screenFrame);
375     CGFloat maxAvailableVerticalSpace = fmax(availableSpaceAbove, availableSpaceBelow) - screenPadding;
376     NSSize maxSpaceAvailableOnYEdge = NSMakeSize(screenFrame.size.width - screenPadding, maxAvailableVerticalSpace);
377     if (targetSizeFitsInAvailableSpace(targetSize, maxSpaceAvailableOnYEdge))
378         return targetSize;
379
380     CGFloat availableSpaceAtLeft = NSMinX(originScreenRect) - NSMinX(screenFrame);
381     CGFloat availableSpaceAtRight = NSMaxX(screenFrame) - NSMaxX(originScreenRect);
382     CGFloat maxAvailableHorizontalSpace = fmax(availableSpaceAtLeft, availableSpaceAtRight) - screenPadding;
383     NSSize maxSpaceAvailableOnXEdge = NSMakeSize(maxAvailableHorizontalSpace, screenFrame.size.height - screenPadding);
384     if (targetSizeFitsInAvailableSpace(targetSize, maxSpaceAvailableOnXEdge))
385         return targetSize;
386
387     // 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.
388     CGFloat aspectRatio = wkViewBounds.size.width / wkViewBounds.size.height;
389     FloatRect maxVerticalTargetSizePreservingAspectRatioRect = largestRectWithAspectRatioInsideRect(aspectRatio, FloatRect(0, 0, maxSpaceAvailableOnYEdge.width, maxSpaceAvailableOnYEdge.height));
390     FloatRect maxHorizontalTargetSizePreservingAspectRatioRect = largestRectWithAspectRatioInsideRect(aspectRatio, FloatRect(0, 0, maxSpaceAvailableOnXEdge.width, maxSpaceAvailableOnXEdge.height));
391
392     NSSize maxVerticalTargetSizePreservingAspectRatio = NSMakeSize(maxVerticalTargetSizePreservingAspectRatioRect.width(), maxVerticalTargetSizePreservingAspectRatioRect.height());
393     NSSize maxHortizontalTargetSizePreservingAspectRatio = NSMakeSize(maxHorizontalTargetSizePreservingAspectRatioRect.width(), maxHorizontalTargetSizePreservingAspectRatioRect.height());
394
395     if ((maxVerticalTargetSizePreservingAspectRatio.width * maxVerticalTargetSizePreservingAspectRatio.height) > (maxHortizontalTargetSizePreservingAspectRatio.width * maxHortizontalTargetSizePreservingAspectRatio.height))
396         return maxVerticalTargetSizePreservingAspectRatio;
397     return maxHortizontalTargetSizePreservingAspectRatio;
398 }
399
400 #endif // WK_API_ENABLED
401
402 - (void)_clearPreviewPopover
403 {
404 #if WK_API_ENABLED
405     if (_previewViewController) {
406         _previewViewController->_delegate = nil;
407         [_wkView _finishPreviewingURL:_previewViewController->_url.get() withPreviewView:[_previewViewController view]];
408         _previewViewController = nil;
409     }
410 #endif
411
412     [_previewPopover close];
413     [_previewPopover setDelegate:nil];
414     _previewPopover = nil;
415 }
416
417 #pragma mark Video actions
418
419 - (NSArray *)_defaultMenuItemsForVideo
420 {
421     RetainPtr<NSMenuItem> copyVideoURLItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyVideoURL];
422
423     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
424     RetainPtr<NSMenuItem> saveToDownloadsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagSaveVideoToDownloads];
425     RetainPtr<NSMenuItem> shareItem = [self _createActionMenuItemForTag:kWKContextActionItemTagShareVideo];
426
427     String urlToShare = hitTestResult->absoluteMediaURL();
428     if (!hitTestResult->isDownloadableMedia()) {
429         [saveToDownloadsItem setEnabled:NO];
430         urlToShare = _page->mainFrame()->url();
431     }
432
433     if (!urlToShare.isEmpty()) {
434         _sharingServicePicker = adoptNS([[NSSharingServicePicker alloc] initWithItems:@[ urlToShare ]]);
435         [_sharingServicePicker setDelegate:self];
436         [shareItem setSubmenu:[_sharingServicePicker menu]];
437     }
438
439     return @[ copyVideoURLItem.get(), [NSMenuItem separatorItem], saveToDownloadsItem.get(), shareItem.get() ];
440 }
441
442 - (void)_copyVideoURL:(id)sender
443 {
444     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
445     String urlToCopy = hitTestResult->absoluteMediaURL();
446     if (!hitTestResult->isDownloadableMedia())
447         urlToCopy = _page->mainFrame()->url();
448
449     [[NSPasteboard generalPasteboard] clearContents];
450     [[NSPasteboard generalPasteboard] writeObjects:@[ urlToCopy ]];
451 }
452
453 - (void)_saveVideoToDownloads:(id)sender
454 {
455     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
456     _page->process().context().download(_page, hitTestResult->absoluteMediaURL());
457 }
458
459 #pragma mark Image actions
460
461 - (NSImage *)_hitTestResultImage
462 {
463     RefPtr<SharedMemory> imageSharedMemory = _hitTestResult.imageSharedMemory;
464     if (!imageSharedMemory)
465         return nil;
466
467     RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithData:[NSData dataWithBytes:imageSharedMemory->data() length:imageSharedMemory->size()]]);
468     return nsImage.autorelease();
469 }
470
471 - (NSArray *)_defaultMenuItemsForImage
472 {
473     RetainPtr<NSMenuItem> copyImageItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyImage];
474     RetainPtr<NSMenuItem> addToPhotosItem;
475     if ([self _canAddMediaToPhotos])
476         addToPhotosItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddImageToPhotos];
477     else
478         addToPhotosItem = [NSMenuItem separatorItem];
479     RetainPtr<NSMenuItem> saveToDownloadsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagSaveImageToDownloads];
480     RetainPtr<NSMenuItem> shareItem = [self _createActionMenuItemForTag:kWKContextActionItemTagShareImage];
481
482     if (RetainPtr<NSImage> image = [self _hitTestResultImage]) {
483         _sharingServicePicker = adoptNS([[NSSharingServicePicker alloc] initWithItems:@[ image.get() ]]);
484         [_sharingServicePicker setDelegate:self];
485         [shareItem setSubmenu:[_sharingServicePicker menu]];
486     }
487
488     return @[ copyImageItem.get(), addToPhotosItem.get(), saveToDownloadsItem.get(), shareItem.get() ];
489 }
490
491 - (void)_copyImage:(id)sender
492 {
493     RetainPtr<NSImage> image = [self _hitTestResultImage];
494     if (!image)
495         return;
496
497     [[NSPasteboard generalPasteboard] clearContents];
498     [[NSPasteboard generalPasteboard] writeObjects:@[ image.get() ]];
499 }
500
501 - (void)_saveImageToDownloads:(id)sender
502 {
503     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
504     _page->process().context().download(_page, hitTestResult->absoluteImageURL());
505 }
506
507 // FIXME: We should try to share this with WebPageProxyMac's similar PDF functions.
508 static NSString *temporaryPhotosDirectoryPath()
509 {
510     static NSString *temporaryPhotosDirectoryPath;
511
512     if (!temporaryPhotosDirectoryPath) {
513         NSString *temporaryDirectoryTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPhotos-XXXXXX"];
514         CString templateRepresentation = [temporaryDirectoryTemplate fileSystemRepresentation];
515
516         if (mkdtemp(templateRepresentation.mutableData()))
517             temporaryPhotosDirectoryPath = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:templateRepresentation.data() length:templateRepresentation.length()] copy];
518     }
519
520     return temporaryPhotosDirectoryPath;
521 }
522
523 static NSString *pathToPhotoOnDisk(NSString *suggestedFilename)
524 {
525     NSString *photoDirectoryPath = temporaryPhotosDirectoryPath();
526     if (!photoDirectoryPath) {
527         WTFLogAlways("Cannot create temporary photo download directory.");
528         return nil;
529     }
530
531     NSString *path = [photoDirectoryPath stringByAppendingPathComponent:suggestedFilename];
532
533     NSFileManager *fileManager = [NSFileManager defaultManager];
534     if ([fileManager fileExistsAtPath:path]) {
535         NSString *pathTemplatePrefix = [photoDirectoryPath stringByAppendingPathComponent:@"XXXXXX-"];
536         NSString *pathTemplate = [pathTemplatePrefix stringByAppendingString:suggestedFilename];
537         CString pathTemplateRepresentation = [pathTemplate fileSystemRepresentation];
538
539         int fd = mkstemps(pathTemplateRepresentation.mutableData(), pathTemplateRepresentation.length() - strlen([pathTemplatePrefix fileSystemRepresentation]) + 1);
540         if (fd < 0) {
541             WTFLogAlways("Cannot create photo file in the temporary directory (%@).", suggestedFilename);
542             return nil;
543         }
544
545         close(fd);
546         path = [fileManager stringWithFileSystemRepresentation:pathTemplateRepresentation.data() length:pathTemplateRepresentation.length()];
547     }
548
549     return path;
550 }
551
552 - (BOOL)_canAddMediaToPhotos
553 {
554     return [getIKSlideshowClass() canExportToApplication:@"com.apple.Photos"];
555 }
556
557 - (void)_addImageToPhotos:(id)sender
558 {
559     if (![self _canAddMediaToPhotos])
560         return;
561
562     RefPtr<SharedMemory> imageSharedMemory = _hitTestResult.imageSharedMemory;
563     if (!imageSharedMemory->size() || _hitTestResult.imageExtension.isEmpty())
564         return;
565
566     RetainPtr<NSData> imageData = adoptNS([[NSData alloc] initWithBytes:imageSharedMemory->data() length:imageSharedMemory->size()]);
567     RetainPtr<NSString> suggestedFilename = [[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:_hitTestResult.imageExtension];
568
569     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
570         NSString *filePath = pathToPhotoOnDisk(suggestedFilename.get());
571         if (!filePath)
572             return;
573
574         NSURL *fileURL = [NSURL fileURLWithPath:filePath];
575         [imageData writeToURL:fileURL atomically:NO];
576
577         dispatch_async(dispatch_get_main_queue(), ^{
578             // This API provides no way to report failure, but if 18420778 is fixed so that it does, we should handle this.
579             [getIKSlideshowClass() exportSlideshowItem:filePath toApplication:@"com.apple.Photos"];
580         });
581     });
582 }
583
584 #pragma mark Text actions
585
586 - (NSArray *)_defaultMenuItemsForDataDetectedText
587 {
588     DDActionContext *actionContext = _hitTestResult.actionContext.get();
589     if (!actionContext)
590         return @[ ];
591
592     // Ref our WebPageProxy for use in the blocks below.
593     RefPtr<WebPageProxy> page = _page;
594     PageOverlay::PageOverlayID overlayID = _hitTestResult.detectedDataOriginatingPageOverlay;
595     _currentActionContext = [actionContext contextForView:_wkView altMode:YES interactionStartedHandler:^() {
596         page->send(Messages::WebPage::DataDetectorsDidPresentUI(overlayID));
597     } interactionChangedHandler:^() {
598         [self _showTextIndicator];
599         page->send(Messages::WebPage::DataDetectorsDidChangeUI(overlayID));
600     } interactionStoppedHandler:^() {
601         [self _hideTextIndicator];
602         page->send(Messages::WebPage::DataDetectorsDidHideUI(overlayID));
603     }];
604
605     [_currentActionContext setHighlightFrame:[_wkView.window convertRectToScreen:[_wkView convertRect:_hitTestResult.detectedDataBoundingBox toView:nil]]];
606
607     return [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
608 }
609
610 - (NSArray *)_defaultMenuItemsForText
611 {
612     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyText];
613     RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagLookupText];
614
615     return @[ copyTextItem.get(), lookupTextItem.get() ];
616 }
617
618 - (NSArray *)_defaultMenuItemsForEditableText
619 {
620     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyText];
621     RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagLookupText];
622     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPaste];
623
624     return @[ copyTextItem.get(), lookupTextItem.get(), pasteItem.get() ];
625 }
626
627 - (NSArray *)_defaultMenuItemsForEditableTextWithSuggestions
628 {
629     if (_hitTestResult.lookupText.isEmpty())
630         return @[ ];
631
632     Vector<TextCheckingResult> results;
633     _page->checkTextOfParagraph(_hitTestResult.lookupText, NSTextCheckingTypeSpelling, results);
634     if (results.isEmpty())
635         return @[ ];
636
637     Vector<String> guesses;
638     _page->getGuessesForWord(_hitTestResult.lookupText, String(), guesses);
639     if (guesses.isEmpty())
640         return @[ ];
641
642     RetainPtr<NSMenu> spellingSubMenu = adoptNS([[NSMenu alloc] init]);
643     for (const auto& guess : guesses) {
644         RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:guess action:@selector(_changeSelectionToSuggestion:) keyEquivalent:@""]);
645         [item setRepresentedObject:guess];
646         [item setTarget:self];
647         [spellingSubMenu addItem:item.get()];
648     }
649
650     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyText];
651     RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagLookupText];
652     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPaste];
653     RetainPtr<NSMenuItem> textSuggestionsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagTextSuggestions];
654
655     [textSuggestionsItem setSubmenu:spellingSubMenu.get()];
656
657     return @[ copyTextItem.get(), lookupTextItem.get(), pasteItem.get(), textSuggestionsItem.get() ];
658 }
659
660 - (void)_copySelection:(id)sender
661 {
662     _page->executeEditCommand("copy");
663 }
664
665 - (void)_paste:(id)sender
666 {
667     _page->executeEditCommand("paste");
668 }
669
670 - (void)_lookupText:(id)sender
671 {
672     _page->performDictionaryLookupOfCurrentSelection();
673 }
674
675 - (void)_changeSelectionToSuggestion:(id)sender
676 {
677     NSString *selectedCorrection = [sender representedObject];
678     if (!selectedCorrection)
679         return;
680
681     ASSERT([selectedCorrection isKindOfClass:[NSString class]]);
682
683     _page->changeSpellingToWord(selectedCorrection);
684 }
685
686 #pragma mark Whitespace actions
687
688 - (NSArray *)_defaultMenuItemsForWhitespaceInEditableArea
689 {
690     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPaste];
691
692     return @[ [NSMenuItem separatorItem], [NSMenuItem separatorItem], pasteItem.get() ];
693 }
694
695 #pragma mark Mailto Link actions
696
697 - (NSArray *)_defaultMenuItemsForMailtoLink
698 {
699     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
700
701     // FIXME: Should this show a yellow highlight?
702     RetainPtr<DDActionContext> actionContext = [[getDDActionContextClass() alloc] init];
703     [actionContext setForActionMenuContent:YES];
704     [actionContext setHighlightFrame:[_wkView.window convertRectToScreen:[_wkView convertRect:hitTestResult->elementBoundingBox() toView:nil]]];
705     return [[getDDActionsManagerClass() sharedManager] menuItemsForTargetURL:hitTestResult->absoluteLinkURL() actionContext:actionContext.get()];
706 }
707
708 #pragma mark NSMenuDelegate implementation
709
710 - (void)menuNeedsUpdate:(NSMenu *)menu
711 {
712     if (menu != _wkView.actionMenu)
713         return;
714
715     ASSERT(_state != ActionMenuState::None);
716
717     // FIXME: We need to be able to cancel this if the menu goes away.
718     // FIXME: Connection can be null if the process is closed; we should clean up better in that case.
719     if (_state == ActionMenuState::Pending) {
720         if (auto* connection = _page->process().connection())
721             connection->waitForAndDispatchImmediately<Messages::WebPageProxy::DidPerformActionMenuHitTest>(_page->pageID(), std::chrono::milliseconds(500));
722     }
723
724     if (_state != ActionMenuState::Ready)
725         [self _updateActionMenuItems];
726 }
727
728 - (void)menu:(NSMenu *)menu willHighlightItem:(NSMenuItem *)item
729 {
730 #if WK_API_ENABLED
731     if (item.tag != kWKContextActionItemTagPreviewLink)
732         return;
733     [self _previewURLFromActionMenu:item];
734 #endif
735 }
736
737 #pragma mark NSSharingServicePickerDelegate implementation
738
739 - (NSArray *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items mask:(NSSharingServiceMask)mask proposedSharingServices:(NSArray *)proposedServices
740 {
741     RetainPtr<NSMutableArray> services = adoptNS([[NSMutableArray alloc] initWithCapacity:proposedServices.count]);
742
743     for (NSSharingService *service in proposedServices) {
744         if ([service.name isEqualToString:NSSharingServiceNameAddToIPhoto])
745             continue;
746         [services addObject:service];
747     }
748
749     return services.autorelease();
750 }
751
752 - (id <NSSharingServiceDelegate>)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker delegateForSharingService:(NSSharingService *)sharingService
753 {
754     return self;
755 }
756
757 #pragma mark NSSharingServiceDelegate implementation
758
759 - (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope
760 {
761     return _wkView.window;
762 }
763
764 #pragma mark NSPopoverDelegate implementation
765
766 - (void)popoverWillClose:(NSNotification *)notification
767 {
768     _shouldKeepPreviewPopoverOpen = NO;
769     [self _clearPreviewPopover];
770 }
771
772 #pragma mark Menu Items
773
774 - (RetainPtr<NSMenuItem>)_createActionMenuItemForTag:(uint32_t)tag
775 {
776     SEL selector = nullptr;
777     NSString *title = nil;
778     NSImage *image = nil;
779     bool enabled = true;
780
781     switch (tag) {
782     case kWKContextActionItemTagOpenLinkInDefaultBrowser:
783         selector = @selector(_openURLFromActionMenu:);
784         title = WEB_UI_STRING_KEY("Open", "Open (action menu item)", "action menu item");
785         image = [NSImage imageNamed:@"NSActionMenuOpenInNewWindow"];
786         break;
787
788 #if WK_API_ENABLED
789     case kWKContextActionItemTagPreviewLink:
790         selector = @selector(_keepPreviewOpenFromActionMenu:);
791         title = WEB_UI_STRING_KEY("Preview", "Preview (action menu item)", "action menu item");
792         image = [NSImage imageNamed:@"NSActionMenuQuickLook"];
793         break;
794 #endif
795
796     case kWKContextActionItemTagAddLinkToSafariReadingList:
797         selector = @selector(_addToReadingListFromActionMenu:);
798         title = WEB_UI_STRING_KEY("Add to Reading List", "Add to Reading List (action menu item)", "action menu item");
799         image = [NSImage imageNamed:@"NSActionMenuAddToReadingList"];
800         break;
801
802     case kWKContextActionItemTagCopyImage:
803         selector = @selector(_copyImage:);
804         title = WEB_UI_STRING_KEY("Copy", "Copy (image action menu item)", "image action menu item");
805         image = [NSImage imageNamed:@"NSActionMenuCopy"];
806         break;
807
808     case kWKContextActionItemTagAddImageToPhotos:
809         selector = @selector(_addImageToPhotos:);
810         title = WEB_UI_STRING_KEY("Add to Photos", "Add to Photos (action menu item)", "action menu item");
811         image = [NSImage imageNamed:@"NSActionMenuAddToPhotos"];
812         break;
813
814     case kWKContextActionItemTagSaveImageToDownloads:
815         selector = @selector(_saveImageToDownloads:);
816         title = WEB_UI_STRING_KEY("Save to Downloads", "Save to Downloads (image action menu item)", "image action menu item");
817         image = [NSImage imageNamed:@"NSActionMenuSaveToDownloads"];
818         break;
819
820     case kWKContextActionItemTagShareImage:
821         title = WEB_UI_STRING_KEY("Share (image action menu item)", "Share (image action menu item)", "image action menu item");
822         image = [NSImage imageNamed:@"NSActionMenuShare"];
823         break;
824
825     case kWKContextActionItemTagCopyText:
826         selector = @selector(_copySelection:);
827         title = WEB_UI_STRING_KEY("Copy", "Copy (text action menu item)", "text action menu item");
828         image = [NSImage imageNamed:@"NSActionMenuCopy"];
829         break;
830
831     case kWKContextActionItemTagLookupText:
832         selector = @selector(_lookupText:);
833         title = WEB_UI_STRING_KEY("Look Up", "Look Up (action menu item)", "action menu item");
834         image = [NSImage imageNamed:@"NSActionMenuLookup"];
835         enabled = getLULookupDefinitionModuleClass();
836         break;
837
838     case kWKContextActionItemTagPaste:
839         selector = @selector(_paste:);
840         title = WEB_UI_STRING_KEY("Paste", "Paste (action menu item)", "action menu item");
841         image = [NSImage imageNamed:@"NSActionMenuPaste"];
842         break;
843
844     case kWKContextActionItemTagTextSuggestions:
845         title = WEB_UI_STRING_KEY("Suggestions", "Suggestions (action menu item)", "action menu item");
846         image = [NSImage imageNamed:@"NSActionMenuSpelling"];
847         break;
848
849     case kWKContextActionItemTagCopyVideoURL:
850         selector = @selector(_copyVideoURL:);
851         title = WEB_UI_STRING_KEY("Copy", "Copy (video action menu item)", "video action menu item");
852         image = [NSImage imageNamed:@"NSActionMenuCopy"];
853         break;
854
855     case kWKContextActionItemTagSaveVideoToDownloads:
856         selector = @selector(_saveVideoToDownloads:);
857         title = WEB_UI_STRING_KEY("Save to Downloads", "Save to Downloads (video action menu item)", "video action menu item");
858         image = [NSImage imageNamed:@"NSActionMenuSaveToDownloads"];
859         break;
860
861     case kWKContextActionItemTagShareVideo:
862         title = WEB_UI_STRING_KEY("Share", "Share (video action menu item)", "video action menu item");
863         image = [NSImage imageNamed:@"NSActionMenuShare"];
864         break;
865
866     default:
867         ASSERT_NOT_REACHED();
868         return nil;
869     }
870
871     RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]);
872     [item setImage:image];
873     [item setTarget:self];
874     [item setTag:tag];
875     [item setEnabled:enabled];
876     return item;
877 }
878
879 - (PassRefPtr<WebHitTestResult>)_webHitTestResult
880 {
881     RefPtr<WebHitTestResult> hitTestResult;
882     if (_state == ActionMenuState::Ready)
883         hitTestResult = WebHitTestResult::create(_hitTestResult.hitTestResult);
884     else
885         hitTestResult = _page->lastMouseMoveHitTestResult();
886
887     return hitTestResult.release();
888 }
889
890 - (NSArray *)_defaultMenuItems
891 {
892     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
893     if (!hitTestResult) {
894         _type = kWKActionMenuNone;
895         return _state != ActionMenuState::Ready ? @[ [NSMenuItem separatorItem] ] : @[ ];
896     }
897
898     String absoluteLinkURL = hitTestResult->absoluteLinkURL();
899     if (!absoluteLinkURL.isEmpty()) {
900         if (WebCore::protocolIsInHTTPFamily(absoluteLinkURL)) {
901             _type = kWKActionMenuLink;
902             return [self _defaultMenuItemsForLink];
903         }
904
905         if (protocolIs(absoluteLinkURL, "mailto")) {
906             _type = kWKActionMenuMailtoLink;
907             return [self _defaultMenuItemsForMailtoLink];
908         }
909     }
910
911     if (!hitTestResult->absoluteMediaURL().isEmpty()) {
912         _type = kWKActionMenuVideo;
913         return [self _defaultMenuItemsForVideo];
914     }
915
916     if (!hitTestResult->absoluteImageURL().isEmpty() && _hitTestResult.imageSharedMemory && !_hitTestResult.imageExtension.isEmpty()) {
917         _type = kWKActionMenuImage;
918         return [self _defaultMenuItemsForImage];
919     }
920
921     if (hitTestResult->isTextNode()) {
922         NSArray *dataDetectorMenuItems = [self _defaultMenuItemsForDataDetectedText];
923         if (_currentActionContext) {
924             // If this is a data detected item with no menu items, we should not fall back to regular text options.
925             if (!dataDetectorMenuItems.count) {
926                 _type = kWKActionMenuNone;
927                 return @[ ];
928             }
929             _type = kWKActionMenuDataDetectedItem;
930             return dataDetectorMenuItems;
931         }
932
933         if (hitTestResult->isContentEditable()) {
934             NSArray *editableTextWithSuggestions = [self _defaultMenuItemsForEditableTextWithSuggestions];
935             if (editableTextWithSuggestions.count) {
936                 _type = kWKActionMenuEditableTextWithSuggestions;
937                 return editableTextWithSuggestions;
938             }
939
940             _type = kWKActionMenuEditableText;
941             return [self _defaultMenuItemsForEditableText];
942         }
943
944         _type = kWKActionMenuReadOnlyText;
945         return [self _defaultMenuItemsForText];
946     }
947
948     if (hitTestResult->isContentEditable()) {
949         _type = kWKActionMenuWhitespaceInEditableArea;
950         return [self _defaultMenuItemsForWhitespaceInEditableArea];
951     }
952
953     _type = kWKActionMenuNone;
954     return _state != ActionMenuState::Ready ? @[ [NSMenuItem separatorItem] ] : @[ ];
955 }
956
957 - (void)_updateActionMenuItems
958 {
959     [_wkView.actionMenu removeAllItems];
960
961     NSArray *menuItems = [self _defaultMenuItems];
962     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
963
964     if ([_wkView respondsToSelector:@selector(_actionMenuItemsForHitTestResult:defaultActionMenuItems:)])
965         menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(hitTestResult.get()) defaultActionMenuItems:menuItems];
966     else
967         menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(hitTestResult.get()) withType:_type defaultActionMenuItems:menuItems userData:toAPI(_userData.get())];
968
969     for (NSMenuItem *item in menuItems)
970         [_wkView.actionMenu addItem:item];
971
972     if (_state == ActionMenuState::Ready && !_wkView.actionMenu.numberOfItems)
973         [_wkView.actionMenu cancelTracking];
974 }
975
976 #if WK_API_ENABLED
977
978 #pragma mark WKPagePreviewViewControllerDelegate
979
980 - (NSView *)pagePreviewViewController:(WKPagePreviewViewController *)pagePreviewViewController viewForPreviewingURL:(NSURL *)url initialFrameSize:(NSSize)initialFrameSize
981 {
982     return [_wkView _viewForPreviewingURL:url initialFrameSize:initialFrameSize];
983 }
984
985 - (void)pagePreviewViewControllerWasClicked:(WKPagePreviewViewController *)pagePreviewViewController
986 {
987     if (NSURL *url = pagePreviewViewController->_url.get())
988         [[NSWorkspace sharedWorkspace] openURL:url];
989 }
990
991 #endif
992
993 @end
994
995 #endif // PLATFORM(MAC)