Images shared to photos via action menu are padded with zeroes at the end
[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 "WKNSURLExtras.h"
32 #import "WKViewInternal.h"
33 #import "WebKitSystemInterface.h"
34 #import "WebPageMessages.h"
35 #import "WebPageProxy.h"
36 #import "WebPageProxyMessages.h"
37 #import "WebProcessPool.h"
38 #import "WebProcessProxy.h"
39 #import <Foundation/Foundation.h>
40 #import <ImageIO/ImageIO.h>
41 #import <ImageKit/ImageKit.h>
42 #import <WebCore/DataDetectorsSPI.h>
43 #import <WebCore/LocalizedStrings.h>
44 #import <WebCore/NSMenuSPI.h>
45 #import <WebCore/NSSharingServiceSPI.h>
46 #import <WebCore/NSSharingServicePickerSPI.h>
47 #import <WebCore/NSViewSPI.h>
48 #import <WebCore/SoftLinking.h>
49 #import <WebCore/TextIndicator.h>
50 #import <WebCore/URL.h>
51
52 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, ImageKit)
53 SOFT_LINK_CLASS(ImageKit, IKSlideshow)
54
55 using namespace WebCore;
56 using namespace WebKit;
57
58 @interface WKActionMenuController () <NSSharingServiceDelegate, NSSharingServicePickerDelegate>
59 - (void)_updateActionMenuItems;
60 - (BOOL)_canAddMediaToPhotos;
61 - (void)_clearActionMenuState;
62 @end
63
64 @interface WKView (WKDeprecatedSPI)
65 - (NSArray *)_actionMenuItemsForHitTestResult:(WKHitTestResultRef)hitTestResult defaultActionMenuItems:(NSArray *)defaultMenuItems;
66 @end
67
68 @implementation WKActionMenuController
69
70 - (instancetype)initWithPage:(WebPageProxy&)page view:(WKView *)wkView
71 {
72     self = [super init];
73
74     if (!self)
75         return nil;
76
77     _page = &page;
78     _wkView = wkView;
79     _type = kWKActionMenuNone;
80
81     return self;
82 }
83
84 - (void)willDestroyView:(WKView *)view
85 {
86     _page = nullptr;
87     _wkView = nil;
88     _hitTestResult = ActionMenuHitTestResult();
89     _currentActionContext = nil;
90 }
91
92 - (void)wkView:(WKView *)wkView willHandleMouseDown:(NSEvent *)event
93 {
94     [self _clearActionMenuState];
95 }
96
97 - (void)prepareForMenu:(NSMenu *)menu withEvent:(NSEvent *)event
98 {
99     if (menu != _wkView._actionMenu)
100         return;
101
102     [_wkView _dismissContentRelativeChildWindows];
103
104     _page->performActionMenuHitTestAtLocation([_wkView convertPoint:event.locationInWindow fromView:nil], false);
105
106     _state = ActionMenuState::Pending;
107     [self _updateActionMenuItems];
108 }
109
110 - (BOOL)isMenuForTextContent
111 {
112     return _type == kWKActionMenuReadOnlyText || _type == kWKActionMenuEditableText || _type == kWKActionMenuEditableTextWithSuggestions;
113 }
114
115 - (void)willOpenMenu:(NSMenu *)menu withEvent:(NSEvent *)event
116 {
117     if (menu != _wkView._actionMenu)
118         return;
119
120     if (!menu.numberOfItems)
121         return;
122
123     if (_type == kWKActionMenuDataDetectedItem) {
124         if (menu.numberOfItems == 1)
125             _page->clearSelection();
126         else
127             _page->selectLastActionMenuRange();
128         return;
129     }
130
131     if (_type == kWKActionMenuWhitespaceInEditableArea) {
132         _page->focusAndSelectLastActionMenuHitTestResult();
133         return;
134     }
135
136     if (![self isMenuForTextContent]) {
137         _page->clearSelection();
138         return;
139     }
140
141     // Action menus for text should highlight the text so that it is clear what the action menu actions
142     // will apply to. If the text is already selected, the menu will use the existing selection.
143     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
144     if (!hitTestResult->isSelected())
145         _page->selectLastActionMenuRange();
146 }
147
148 - (void)didCloseMenu:(NSMenu *)menu withEvent:(NSEvent *)event
149 {
150     if (menu != _wkView._actionMenu)
151         return;
152
153     [self _clearActionMenuState];
154 }
155
156 - (void)_clearActionMenuState
157 {
158     if (_currentActionContext && _hasActivatedActionContext) {
159         [getDDActionsManagerClass() didUseActions];
160         _hasActivatedActionContext = NO;
161     }
162
163     _state = ActionMenuState::None;
164     _hitTestResult = ActionMenuHitTestResult();
165     _type = kWKActionMenuNone;
166     _sharingServicePicker = nil;
167     _currentActionContext = nil;
168     _userData = nil;
169 }
170
171 - (void)didPerformActionMenuHitTest:(const ActionMenuHitTestResult&)hitTestResult userData:(API::Object*)userData
172 {
173     // FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies.
174     _state = ActionMenuState::Ready;
175     _hitTestResult = hitTestResult;
176     _userData = userData;
177
178     [self _updateActionMenuItems];
179 }
180
181 #pragma mark Link actions
182
183 - (NSURL *)_hitLinkURL
184 {
185     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
186     return [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()];
187 }
188
189 - (NSArray *)_defaultMenuItemsForLink
190 {
191     RetainPtr<NSMenuItem> openLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagOpenLinkInDefaultBrowser];
192     RetainPtr<NSMenuItem> readingListItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddLinkToSafariReadingList];
193     RetainPtr<NSMenuItem> shareItem = [self _createShareActionMenuItemForTag:kWKContextActionItemTagShareLink withItems:@[ [self _hitLinkURL] ]];
194
195     return @[ openLinkItem.get(), shareItem.get(), [NSMenuItem separatorItem], readingListItem.get() ];
196 }
197
198 - (void)_openURLFromActionMenu:(id)sender
199 {
200     [[NSWorkspace sharedWorkspace] openURL:[self _hitLinkURL]];
201 }
202
203 - (void)_addToReadingListFromActionMenu:(id)sender
204 {
205     NSSharingService *service = [NSSharingService sharingServiceNamed:NSSharingServiceNameAddToSafariReadingList];
206     [service performWithItems:@[ [self _hitLinkURL] ]];
207 }
208
209 #pragma mark Video actions
210
211 - (NSArray *)_defaultMenuItemsForVideo
212 {
213     RetainPtr<NSMenuItem> copyVideoURLItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyVideoURL];
214     RetainPtr<NSMenuItem> saveToDownloadsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagSaveVideoToDownloads];
215
216     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
217     String urlToShare = hitTestResult->isDownloadableMedia() ? hitTestResult->absoluteMediaURL() : _page->mainFrame()->url();
218     RetainPtr<NSMenuItem> shareItem = [self _createShareActionMenuItemForTag:kWKContextActionItemTagShareVideo withItems:@[ urlToShare ]];
219
220     return @[ copyVideoURLItem.get(), [NSMenuItem separatorItem], saveToDownloadsItem.get(), shareItem.get() ];
221 }
222
223 - (void)_copyVideoURL:(id)sender
224 {
225     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
226     String urlToCopy = hitTestResult->absoluteMediaURL();
227     if (!hitTestResult->isDownloadableMedia())
228         urlToCopy = _page->mainFrame()->url();
229
230     [[NSPasteboard generalPasteboard] clearContents];
231     [[NSPasteboard generalPasteboard] writeObjects:@[ urlToCopy ]];
232 }
233
234 - (void)_saveVideoToDownloads:(id)sender
235 {
236     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
237     _page->process().processPool().download(_page, hitTestResult->absoluteMediaURL());
238 }
239
240 #pragma mark Image actions
241
242 - (NSImage *)_hitTestResultImage
243 {
244     RefPtr<SharedMemory> imageSharedMemory = _hitTestResult.imageSharedMemory;
245     if (!imageSharedMemory)
246         return nil;
247
248     RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithData:[NSData dataWithBytes:imageSharedMemory->data() length:_hitTestResult.imageSize]]);
249     return nsImage.autorelease();
250 }
251
252 - (NSArray *)_defaultMenuItemsForImage
253 {
254     RetainPtr<NSMenuItem> copyImageItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyImage];
255     RetainPtr<NSMenuItem> addToPhotosItem;
256     if ([self _canAddMediaToPhotos])
257         addToPhotosItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddImageToPhotos];
258     else
259         addToPhotosItem = [NSMenuItem separatorItem];
260     RetainPtr<NSMenuItem> saveToDownloadsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagSaveImageToDownloads];
261     RetainPtr<NSMenuItem> shareItem = [self _createShareActionMenuItemForTag:kWKContextActionItemTagShareImage withItems:@[ [self _hitTestResultImage] ]];
262
263     return @[ copyImageItem.get(), addToPhotosItem.get(), saveToDownloadsItem.get(), shareItem.get() ];
264 }
265
266 - (void)_copyImage:(id)sender
267 {
268     RetainPtr<NSImage> image = [self _hitTestResultImage];
269     if (!image)
270         return;
271
272     [[NSPasteboard generalPasteboard] clearContents];
273     [[NSPasteboard generalPasteboard] writeObjects:@[ image.get() ]];
274 }
275
276 - (void)_saveImageToDownloads:(id)sender
277 {
278     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
279     _page->process().processPool().download(_page, hitTestResult->absoluteImageURL());
280 }
281
282 // FIXME: We should try to share this with WebPageProxyMac's similar PDF functions.
283 static NSString *temporaryPhotosDirectoryPath()
284 {
285     static NSString *temporaryPhotosDirectoryPath;
286
287     if (!temporaryPhotosDirectoryPath) {
288         NSString *temporaryDirectoryTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPhotos-XXXXXX"];
289         CString templateRepresentation = [temporaryDirectoryTemplate fileSystemRepresentation];
290
291         if (mkdtemp(templateRepresentation.mutableData()))
292             temporaryPhotosDirectoryPath = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:templateRepresentation.data() length:templateRepresentation.length()] copy];
293     }
294
295     return temporaryPhotosDirectoryPath;
296 }
297
298 static NSString *pathToPhotoOnDisk(NSString *suggestedFilename)
299 {
300     NSString *photoDirectoryPath = temporaryPhotosDirectoryPath();
301     if (!photoDirectoryPath) {
302         WTFLogAlways("Cannot create temporary photo download directory.");
303         return nil;
304     }
305
306     NSString *path = [photoDirectoryPath stringByAppendingPathComponent:suggestedFilename];
307
308     NSFileManager *fileManager = [NSFileManager defaultManager];
309     if ([fileManager fileExistsAtPath:path]) {
310         NSString *pathTemplatePrefix = [photoDirectoryPath stringByAppendingPathComponent:@"XXXXXX-"];
311         NSString *pathTemplate = [pathTemplatePrefix stringByAppendingString:suggestedFilename];
312         CString pathTemplateRepresentation = [pathTemplate fileSystemRepresentation];
313
314         int fd = mkstemps(pathTemplateRepresentation.mutableData(), pathTemplateRepresentation.length() - strlen([pathTemplatePrefix fileSystemRepresentation]) + 1);
315         if (fd < 0) {
316             WTFLogAlways("Cannot create photo file in the temporary directory (%@).", suggestedFilename);
317             return nil;
318         }
319
320         close(fd);
321         path = [fileManager stringWithFileSystemRepresentation:pathTemplateRepresentation.data() length:pathTemplateRepresentation.length()];
322     }
323
324     return path;
325 }
326
327 - (BOOL)_canAddMediaToPhotos
328 {
329     return [getIKSlideshowClass() canExportToApplication:@"com.apple.Photos"];
330 }
331
332 - (void)_addImageToPhotos:(id)sender
333 {
334     if (![self _canAddMediaToPhotos])
335         return;
336
337     RefPtr<SharedMemory> imageSharedMemory = _hitTestResult.imageSharedMemory;
338     if (!imageSharedMemory->data() || _hitTestResult.imageExtension.isEmpty() || !_hitTestResult.imageSize)
339         return;
340
341     RetainPtr<NSData> imageData = adoptNS([[NSData alloc] initWithBytes:imageSharedMemory->data() length:_hitTestResult.imageSize]);
342     RetainPtr<NSString> suggestedFilename = [[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:_hitTestResult.imageExtension];
343
344     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
345         NSString *filePath = pathToPhotoOnDisk(suggestedFilename.get());
346         if (!filePath)
347             return;
348
349         NSURL *fileURL = [NSURL fileURLWithPath:filePath];
350         [imageData writeToURL:fileURL atomically:NO];
351
352         dispatch_async(dispatch_get_main_queue(), ^{
353             // This API provides no way to report failure, but if 18420778 is fixed so that it does, we should handle this.
354             [getIKSlideshowClass() exportSlideshowItem:filePath toApplication:@"com.apple.Photos"];
355         });
356     });
357 }
358
359 #pragma mark Text actions
360
361 - (NSArray *)_defaultMenuItemsForDataDetectedText
362 {
363     DDActionContext *actionContext = _hitTestResult.actionContext.get();
364     if (!actionContext)
365         return @[ ];
366
367     actionContext.altMode = YES;
368     if ([[getDDActionsManagerClass() sharedManager] respondsToSelector:@selector(hasActionsForResult:actionContext:)]) {
369         if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:actionContext.mainResult actionContext:actionContext])
370             return @[ ];
371     }
372
373     // Ref our WebPageProxy for use in the blocks below.
374     RefPtr<WebPageProxy> page = _page;
375     PageOverlay::PageOverlayID overlayID = _hitTestResult.detectedDataOriginatingPageOverlay;
376     _currentActionContext = [actionContext contextForView:_wkView altMode:YES interactionStartedHandler:^() {
377         page->send(Messages::WebPage::DataDetectorsDidPresentUI(overlayID));
378     } interactionChangedHandler:^() {
379         if (_hitTestResult.detectedDataTextIndicator)
380             page->setTextIndicator(_hitTestResult.detectedDataTextIndicator->data(), false);
381         page->send(Messages::WebPage::DataDetectorsDidChangeUI(overlayID));
382     } interactionStoppedHandler:^() {
383         page->send(Messages::WebPage::DataDetectorsDidHideUI(overlayID));
384         page->clearTextIndicator();
385     }];
386
387     [_currentActionContext setHighlightFrame:[_wkView.window convertRectToScreen:[_wkView convertRect:_hitTestResult.detectedDataBoundingBox toView:nil]]];
388
389     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
390     return menuItems;
391 }
392
393 - (NSArray *)_defaultMenuItemsForText
394 {
395     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyText];
396     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPaste];
397     [pasteItem setEnabled:NO];
398
399     return @[ copyTextItem.get(), [NSMenuItem separatorItem], pasteItem.get() ];
400 }
401
402 - (NSArray *)_defaultMenuItemsForEditableText
403 {
404     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyText];
405     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPaste];
406
407     return @[ copyTextItem.get(), [NSMenuItem separatorItem], pasteItem.get() ];
408 }
409
410 - (NSArray *)_defaultMenuItemsForEditableTextWithSuggestions
411 {
412     if (_hitTestResult.lookupText.isEmpty())
413         return @[ ];
414
415     Vector<TextCheckingResult> results;
416     _page->checkTextOfParagraph(_hitTestResult.lookupText, NSTextCheckingTypeSpelling, results);
417     if (results.isEmpty())
418         return @[ ];
419
420     Vector<String> guesses;
421     _page->getGuessesForWord(_hitTestResult.lookupText, String(), guesses);
422     if (guesses.isEmpty())
423         return @[ ];
424
425     RetainPtr<NSMenu> spellingSubMenu = adoptNS([[NSMenu alloc] init]);
426     for (const auto& guess : guesses) {
427         RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:guess action:@selector(_changeSelectionToSuggestion:) keyEquivalent:@""]);
428         [item setRepresentedObject:guess];
429         [item setTarget:self];
430         [spellingSubMenu addItem:item.get()];
431     }
432
433     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyText];
434     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPaste];
435     RetainPtr<NSMenuItem> textSuggestionsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagTextSuggestions];
436
437     [textSuggestionsItem setSubmenu:spellingSubMenu.get()];
438
439     return @[ copyTextItem.get(), [NSMenuItem separatorItem], pasteItem.get(), textSuggestionsItem.get() ];
440 }
441
442 - (void)_copySelection:(id)sender
443 {
444     _page->executeEditCommand("copy");
445 }
446
447 - (void)_paste:(id)sender
448 {
449     _page->executeEditCommand("paste");
450 }
451
452 - (void)_changeSelectionToSuggestion:(id)sender
453 {
454     NSString *selectedCorrection = [sender representedObject];
455     if (!selectedCorrection)
456         return;
457
458     ASSERT([selectedCorrection isKindOfClass:[NSString class]]);
459
460     _page->changeSpellingToWord(selectedCorrection);
461 }
462
463 #pragma mark Whitespace actions
464
465 - (NSArray *)_defaultMenuItemsForWhitespaceInEditableArea
466 {
467     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyText];
468     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPaste];
469     [copyTextItem setEnabled:NO];
470
471     return @[ copyTextItem.get(), [NSMenuItem separatorItem], pasteItem.get() ];
472 }
473
474 #pragma mark mailto: and tel: Link actions
475
476 - (NSArray *)_defaultMenuItemsForDataDetectableLink
477 {
478     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
479     RetainPtr<DDActionContext> actionContext = adoptNS([allocDDActionContextInstance() init]);
480
481     // FIXME: Should this show a yellow highlight?
482     _currentActionContext = [actionContext contextForView:_wkView altMode:YES interactionStartedHandler:^() {
483     } interactionChangedHandler:^() {
484     } interactionStoppedHandler:^() {
485     }];
486
487     [_currentActionContext setHighlightFrame:[_wkView.window convertRectToScreen:[_wkView convertRect:_hitTestResult.detectedDataBoundingBox toView:nil]]];
488
489     return [[getDDActionsManagerClass() sharedManager] menuItemsForTargetURL:hitTestResult->absoluteLinkURL() actionContext:_currentActionContext.get()];
490 }
491
492 #pragma mark NSMenuDelegate implementation
493
494 - (void)menuNeedsUpdate:(NSMenu *)menu
495 {
496     if (menu != _wkView._actionMenu)
497         return;
498
499     ASSERT(_state != ActionMenuState::None);
500
501     // FIXME: We need to be able to cancel this if the menu goes away.
502     // FIXME: Connection can be null if the process is closed; we should clean up better in that case.
503     if (_state == ActionMenuState::Pending) {
504         if (auto* connection = _page->process().connection()) {
505             bool receivedReply = connection->waitForAndDispatchImmediately<Messages::WebPageProxy::DidPerformActionMenuHitTest>(_page->pageID(), std::chrono::milliseconds(500));
506             if (!receivedReply)
507                 _state = ActionMenuState::TimedOut;
508         }
509     }
510
511     if (_state != ActionMenuState::Ready)
512         [self _updateActionMenuItems];
513
514     if (_currentActionContext) {
515         _hasActivatedActionContext = YES;
516         if (![getDDActionsManagerClass() shouldUseActionsWithContext:_currentActionContext.get()]) {
517             [menu cancelTracking];
518             [menu removeAllItems];
519         }
520     }
521 }
522
523 #pragma mark NSSharingServicePickerDelegate implementation
524
525 - (NSArray *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items mask:(NSSharingServiceMask)mask proposedSharingServices:(NSArray *)proposedServices
526 {
527     RetainPtr<NSMutableArray> services = adoptNS([[NSMutableArray alloc] initWithCapacity:proposedServices.count]);
528
529     for (NSSharingService *service in proposedServices) {
530         if ([service.name isEqualToString:NSSharingServiceNameAddToIPhoto])
531             continue;
532         [services addObject:service];
533     }
534
535     return services.autorelease();
536 }
537
538 - (id <NSSharingServiceDelegate>)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker delegateForSharingService:(NSSharingService *)sharingService
539 {
540     return self;
541 }
542
543 #pragma mark NSSharingServiceDelegate implementation
544
545 - (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope
546 {
547     return _wkView.window;
548 }
549
550 #pragma mark Menu Items
551
552 - (RetainPtr<NSMenuItem>)_createActionMenuItemForTag:(uint32_t)tag
553 {
554     SEL selector = nullptr;
555     NSString *title = nil;
556     NSImage *image = nil;
557     bool enabled = true;
558     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
559
560     switch (tag) {
561     case kWKContextActionItemTagOpenLinkInDefaultBrowser:
562         selector = @selector(_openURLFromActionMenu:);
563         title = WEB_UI_STRING_KEY("Open", "Open (action menu item)", "action menu item");
564         image = [NSImage imageNamed:@"NSActionMenuOpenInNewWindow"];
565         break;
566
567     case kWKContextActionItemTagPreviewLink:
568         ASSERT_NOT_REACHED();
569         break;
570
571     case kWKContextActionItemTagAddLinkToSafariReadingList:
572         selector = @selector(_addToReadingListFromActionMenu:);
573         title = WEB_UI_STRING_KEY("Add to Reading List", "Add to Reading List (action menu item)", "action menu item");
574         image = [NSImage imageNamed:@"NSActionMenuAddToReadingList"];
575         break;
576
577     case kWKContextActionItemTagCopyImage:
578         selector = @selector(_copyImage:);
579         title = WEB_UI_STRING_KEY("Copy", "Copy (image action menu item)", "image action menu item");
580         image = [NSImage imageNamed:@"NSActionMenuCopy"];
581         break;
582
583     case kWKContextActionItemTagAddImageToPhotos:
584         selector = @selector(_addImageToPhotos:);
585         title = WEB_UI_STRING_KEY("Add to Photos", "Add to Photos (action menu item)", "action menu item");
586         image = [NSImage imageNamed:@"NSActionMenuAddToPhotos"];
587         break;
588
589     case kWKContextActionItemTagSaveImageToDownloads:
590         selector = @selector(_saveImageToDownloads:);
591         title = WEB_UI_STRING_KEY("Save to Downloads", "Save to Downloads (image action menu item)", "image action menu item");
592         image = [NSImage imageNamed:@"NSActionMenuSaveToDownloads"];
593         break;
594
595     case kWKContextActionItemTagCopyText:
596         selector = @selector(_copySelection:);
597         title = WEB_UI_STRING_KEY("Copy", "Copy (text action menu item)", "text action menu item");
598         image = [NSImage imageNamed:@"NSActionMenuCopy"];
599         enabled = hitTestResult->allowsCopy();
600         break;
601
602     case kWKContextActionItemTagPaste:
603         selector = @selector(_paste:);
604         title = WEB_UI_STRING_KEY("Paste", "Paste (action menu item)", "action menu item");
605         image = [NSImage imageNamed:@"NSActionMenuPaste"];
606         break;
607
608     case kWKContextActionItemTagTextSuggestions:
609         title = WEB_UI_STRING_KEY("Suggestions", "Suggestions (action menu item)", "action menu item");
610         image = [NSImage imageNamed:@"NSActionMenuSpelling"];
611         break;
612
613     case kWKContextActionItemTagCopyVideoURL:
614         selector = @selector(_copyVideoURL:);
615         title = WEB_UI_STRING_KEY("Copy", "Copy (video action menu item)", "video action menu item");
616         image = [NSImage imageNamed:@"NSActionMenuCopy"];
617         break;
618
619     case kWKContextActionItemTagSaveVideoToDownloads:
620         selector = @selector(_saveVideoToDownloads:);
621         title = WEB_UI_STRING_KEY("Save to Downloads", "Save to Downloads (video action menu item)", "video action menu item");
622         image = [NSImage imageNamed:@"NSActionMenuSaveToDownloads"];
623         break;
624
625     default:
626         ASSERT_NOT_REACHED();
627         return nil;
628     }
629
630     RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]);
631     [item setImage:image];
632     [item setTarget:self];
633     [item setTag:tag];
634     [item setEnabled:enabled];
635     return item;
636 }
637
638 - (RetainPtr<NSMenuItem>)_createShareActionMenuItemForTag:(uint32_t)tag withItems:(NSArray *)items
639 {
640     RetainPtr<NSMenuItem> shareItem = [NSMenuItem standardShareMenuItemWithItems:items];
641     [shareItem setTag:tag];
642
643     NSSharingServicePicker *sharingServicePicker = [shareItem representedObject];
644     sharingServicePicker.delegate = self;
645
646     return shareItem;
647 }
648
649 - (PassRefPtr<WebHitTestResult>)_webHitTestResult
650 {
651     RefPtr<WebHitTestResult> hitTestResult;
652     if (_state == ActionMenuState::Ready)
653         hitTestResult = WebHitTestResult::create(_hitTestResult.hitTestResult);
654     else
655         hitTestResult = _page->lastMouseMoveHitTestResult();
656
657     return hitTestResult.release();
658 }
659
660 - (NSArray *)_defaultMenuItems
661 {
662     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
663     if (!hitTestResult) {
664         _type = kWKActionMenuNone;
665         return _state == ActionMenuState::Pending ? @[ [NSMenuItem separatorItem] ] : @[ ];
666     }
667
668     String absoluteLinkURL = hitTestResult->absoluteLinkURL();
669     if (!absoluteLinkURL.isEmpty()) {
670         if (WebCore::protocolIsInHTTPFamily(absoluteLinkURL)) {
671             _type = kWKActionMenuLink;
672             return [self _defaultMenuItemsForLink];
673         }
674
675         if (protocolIs(absoluteLinkURL, "mailto")) {
676             _type = kWKActionMenuMailtoLink;
677             return [self _defaultMenuItemsForDataDetectableLink];
678         }
679
680         if (protocolIs(absoluteLinkURL, "tel")) {
681             _type = kWKActionMenuTelLink;
682             return [self _defaultMenuItemsForDataDetectableLink];
683         }
684     }
685
686     if (!hitTestResult->absoluteMediaURL().isEmpty()) {
687         _type = kWKActionMenuVideo;
688         return [self _defaultMenuItemsForVideo];
689     }
690
691     if (!hitTestResult->absoluteImageURL().isEmpty() && _hitTestResult.imageSharedMemory && !_hitTestResult.imageExtension.isEmpty()) {
692         _type = kWKActionMenuImage;
693         return [self _defaultMenuItemsForImage];
694     }
695
696     if (hitTestResult->isTextNode() || hitTestResult->isOverTextInsideFormControlElement()) {
697         NSArray *dataDetectorMenuItems = [self _defaultMenuItemsForDataDetectedText];
698         if (_currentActionContext) {
699             // If this is a data detected item with no menu items, we should not fall back to regular text options.
700             if (!dataDetectorMenuItems.count) {
701                 _type = kWKActionMenuNone;
702                 return @[ ];
703             }
704             _type = kWKActionMenuDataDetectedItem;
705             return dataDetectorMenuItems;
706         }
707
708         if (hitTestResult->isContentEditable()) {
709             NSArray *editableTextWithSuggestions = [self _defaultMenuItemsForEditableTextWithSuggestions];
710             if (editableTextWithSuggestions.count) {
711                 _type = kWKActionMenuEditableTextWithSuggestions;
712                 return editableTextWithSuggestions;
713             }
714
715             _type = kWKActionMenuEditableText;
716             return [self _defaultMenuItemsForEditableText];
717         }
718
719         _type = kWKActionMenuReadOnlyText;
720         return [self _defaultMenuItemsForText];
721     }
722
723     if (hitTestResult->isContentEditable()) {
724         _type = kWKActionMenuWhitespaceInEditableArea;
725         return [self _defaultMenuItemsForWhitespaceInEditableArea];
726     }
727
728     if (hitTestResult->isSelected()) {
729         // A selection should present the read-only text menu. It might make more sense to present a new
730         // type of menu with just copy, but for the time being, we should stay consistent with text.
731         _type = kWKActionMenuReadOnlyText;
732         return [self _defaultMenuItemsForText];
733     }
734
735     _type = kWKActionMenuNone;
736     return _state == ActionMenuState::Pending ? @[ [NSMenuItem separatorItem] ] : @[ ];
737 }
738
739 - (void)_updateActionMenuItems
740 {
741     [_wkView._actionMenu removeAllItems];
742
743     NSArray *menuItems = [self _defaultMenuItems];
744     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
745
746     if ([_wkView respondsToSelector:@selector(_actionMenuItemsForHitTestResult:defaultActionMenuItems:)])
747         menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(hitTestResult.get()) defaultActionMenuItems:menuItems];
748     else
749         menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(hitTestResult.get()) withType:_type defaultActionMenuItems:menuItems userData:toAPI(_userData.get())];
750
751     for (NSMenuItem *item in menuItems)
752         [_wkView._actionMenu addItem:item];
753
754     if (!_wkView._actionMenu.numberOfItems)
755         [_wkView._actionMenu cancelTracking];
756 }
757
758 @end
759
760 #endif // PLATFORM(MAC)