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