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