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