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