WK1: Add initial support for immediate actions
[WebKit-https.git] / Source / WebKit / mac / WebView / WebActionMenuController.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 "WebActionMenuController.h"
27
28 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
29
30 #import "DOMElementInternal.h"
31 #import "DOMNodeInternal.h"
32 #import "DOMRangeInternal.h"
33 #import "DictionaryPopupInfo.h"
34 #import "WebDocumentInternal.h"
35 #import "WebElementDictionary.h"
36 #import "WebFrameInternal.h"
37 #import "WebHTMLView.h"
38 #import "WebHTMLViewInternal.h"
39 #import "WebSystemInterface.h"
40 #import "WebUIDelegatePrivate.h"
41 #import "WebViewInternal.h"
42 #import <ImageIO/ImageIO.h>
43 #import <ImageKit/ImageKit.h>
44 #import <WebCore/ArchiveResource.h>
45 #import <WebCore/DataDetection.h>
46 #import <WebCore/DataDetectorsSPI.h>
47 #import <WebCore/DictionaryLookup.h>
48 #import <WebCore/DocumentLoader.h>
49 #import <WebCore/Editor.h>
50 #import <WebCore/Element.h>
51 #import <WebCore/EventHandler.h>
52 #import <WebCore/FocusController.h>
53 #import <WebCore/Frame.h>
54 #import <WebCore/FrameView.h>
55 #import <WebCore/HTMLConverter.h>
56 #import <WebCore/LookupSPI.h>
57 #import <WebCore/NSMenuSPI.h>
58 #import <WebCore/NSSharingServicePickerSPI.h>
59 #import <WebCore/NSSharingServiceSPI.h>
60 #import <WebCore/NSViewSPI.h>
61 #import <WebCore/Page.h>
62 #import <WebCore/QuickLookMacSPI.h>
63 #import <WebCore/Range.h>
64 #import <WebCore/RenderElement.h>
65 #import <WebCore/RenderObject.h>
66 #import <WebCore/SharedBuffer.h>
67 #import <WebCore/SoftLinking.h>
68 #import <WebCore/TextCheckerClient.h>
69 #import <WebCore/TextIndicator.h>
70 #import <WebKitSystemInterface.h>
71 #import <objc/objc-class.h>
72 #import <objc/objc.h>
73
74 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, QuickLookUI)
75 SOFT_LINK_CLASS(QuickLookUI, QLPreviewMenuItem)
76
77 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, ImageKit)
78 SOFT_LINK_CLASS(ImageKit, IKSlideshow)
79
80 @interface WebActionMenuController () <QLPreviewMenuItemDelegate>
81 @end
82
83 using namespace WebCore;
84
85 @implementation WebActionMenuController
86
87 - (id)initWithWebView:(WebView *)webView
88 {
89     if (!(self = [super init]))
90         return nil;
91
92     _webView = webView;
93     _type = WebActionMenuNone;
94
95     return self;
96 }
97
98 - (void)webViewClosed
99 {
100     _webView = nil;
101 }
102
103 - (WebElementDictionary *)performHitTestAtPoint:(NSPoint)windowPoint
104 {
105     WebHTMLView *documentView = [[[_webView _selectedOrMainFrame] frameView] documentView];
106     NSPoint point = [documentView convertPoint:windowPoint fromView:nil];
107
108     Frame* coreFrame = core([documentView _frame]);
109     if (!coreFrame)
110         return nil;
111     _hitTestResult = coreFrame->eventHandler().hitTestResultAtPoint(IntPoint(point));
112
113     return [[[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult] autorelease];
114 }
115
116 - (void)webView:(WebView *)webView willHandleMouseDown:(NSEvent *)event
117 {
118     if (_currentActionContext && _hasActivatedActionContext) {
119         [getDDActionsManagerClass() didUseActions];
120         _hasActivatedActionContext = NO;
121     }
122 }
123
124 - (void)webView:(WebView *)webView didHandleScrollWheel:(NSEvent *)event
125 {
126     [self _dismissActionMenuPopovers];
127 }
128
129 - (void)prepareForMenu:(NSMenu *)menu withEvent:(NSEvent *)event
130 {
131     if (!_webView)
132         return;
133
134     NSMenu *actionMenu = _webView.actionMenu;
135     if (menu != actionMenu)
136         return;
137
138     [self _dismissActionMenuPopovers];
139     [actionMenu removeAllItems];
140
141     WebElementDictionary *hitTestResult = [self performHitTestAtPoint:event.locationInWindow];
142     NSArray *menuItems = [self _defaultMenuItems];
143
144     // Allow clients to customize the menu items.
145     if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:actionMenuItemsForHitTestResult:withType:defaultActionMenuItems:)])
146         menuItems = [[_webView UIDelegate] _webView:_webView actionMenuItemsForHitTestResult:hitTestResult withType:_type defaultActionMenuItems:menuItems];
147
148     for (NSMenuItem *item in menuItems)
149         [actionMenu addItem:item];
150
151     if (_currentActionContext) {
152         _hasActivatedActionContext = YES;
153         if (![getDDActionsManagerClass() shouldUseActionsWithContext:_currentActionContext.get()]) {
154             [menu cancelTracking];
155             [menu removeAllItems];
156         }
157     }
158 }
159
160 - (BOOL)isMenuForTextContent
161 {
162     return _type == WebActionMenuReadOnlyText || _type == WebActionMenuEditableText || _type == WebActionMenuEditableTextWithSuggestions;
163 }
164
165 - (void)focusAndSelectHitTestResult
166 {
167     if (!_hitTestResult.isContentEditable())
168         return;
169
170     Element* element = _hitTestResult.innerElement();
171     if (!element)
172         return;
173
174     Frame* frame = element->document().frame();
175     if (!frame)
176         return;
177
178     frame->page()->focusController().setFocusedElement(element, frame);
179     VisiblePosition position = frame->visiblePositionForPoint(_hitTestResult.roundedPointInInnerNodeFrame());
180     frame->selection().setSelection(position);
181 }
182
183 - (void)willOpenMenu:(NSMenu *)menu withEvent:(NSEvent *)event
184 {
185     if (menu != _webView.actionMenu)
186         return;
187
188     if (!menu.numberOfItems)
189         return;
190
191     if (_type == WebActionMenuDataDetectedItem) {
192         if (menu.numberOfItems == 1)
193             [[_webView _selectedOrMainFrame] _clearSelection];
194         else
195             [self _selectDataDetectedText];
196         return;
197     }
198
199     if (_type == WebActionMenuWhitespaceInEditableArea) {
200         [self focusAndSelectHitTestResult];
201         return;
202     }
203
204     if (![self isMenuForTextContent]) {
205         [[_webView _selectedOrMainFrame] _clearSelection];
206         return;
207     }
208
209     // Action menus for text should highlight the text so that it is clear what the action menu actions
210     // will apply to. If the text is already selected, the menu will use the existing selection.
211     if (!_hitTestResult.isSelected())
212         [self _selectLookupText];
213 }
214
215 - (void)didCloseMenu:(NSMenu *)menu withEvent:(NSEvent *)event
216 {
217     if (menu != _webView.actionMenu)
218         return;
219
220     if (_currentActionContext && _hasActivatedActionContext) {
221         [getDDActionsManagerClass() didUseActions];
222         _hasActivatedActionContext = NO;
223     }
224
225     _type = WebActionMenuNone;
226     _sharingServicePicker = nil;
227     _currentDetectedDataTextIndicator = nil;
228     _currentDetectedDataRange = nil;
229     _currentActionContext = nil;
230 }
231
232 #pragma mark Link actions
233
234 - (void)_openURLFromActionMenu:(id)sender
235 {
236     if (!_webView)
237         return;
238
239     [[NSWorkspace sharedWorkspace] openURL:_hitTestResult.absoluteLinkURL()];
240 }
241
242 - (void)_addToReadingListFromActionMenu:(id)sender
243 {
244     if (!_webView)
245         return;
246
247     NSURL *url = _hitTestResult.absoluteLinkURL();
248     NSSharingService *service = [NSSharingService sharingServiceNamed:NSSharingServiceNameAddToSafariReadingList];
249     [service performWithItems:@[ url ]];
250 }
251
252 static IntRect elementBoundingBoxInWindowCoordinatesFromNode(Node* node)
253 {
254     if (!node)
255         return IntRect();
256
257     Frame* frame = node->document().frame();
258     if (!frame)
259         return IntRect();
260
261     FrameView* view = frame->view();
262     if (!view)
263         return IntRect();
264
265     return view->contentsToWindow(rendererBoundingBox(*node));
266 }
267
268 - (NSArray *)_defaultMenuItemsForLink
269 {
270     RetainPtr<NSMenuItem> openLinkItem = [self _createActionMenuItemForTag:WebActionMenuItemTagOpenLinkInDefaultBrowser];
271
272     BOOL shouldUseStandardQuickLookPreview = [NSMenuItem respondsToSelector:@selector(standardQuickLookMenuItem)];
273     RetainPtr<NSMenuItem> previewLinkItem;
274     RetainPtr<QLPreviewMenuItem> qlPreviewLinkItem;
275     if (shouldUseStandardQuickLookPreview) {
276         qlPreviewLinkItem = [NSMenuItem standardQuickLookMenuItem];
277         [qlPreviewLinkItem setPreviewStyle:QLPreviewStylePopover];
278         [qlPreviewLinkItem setDelegate:self];
279     } else
280         previewLinkItem = [NSMenuItem separatorItem];
281
282     RetainPtr<NSMenuItem> readingListItem = [self _createActionMenuItemForTag:WebActionMenuItemTagAddLinkToSafariReadingList];
283
284     return @[ openLinkItem.get(), shouldUseStandardQuickLookPreview ? qlPreviewLinkItem.get() : previewLinkItem.get(), [NSMenuItem separatorItem], readingListItem.get() ];
285 }
286
287 #pragma mark mailto: and tel: Link actions
288
289 - (NSArray *)_defaultMenuItemsForDataDetectableLink
290 {
291     Node* node = _hitTestResult.innerNode();
292     if (!node)
293         return @[ ];
294
295     RetainPtr<DDActionContext> actionContext = [allocDDActionContextInstance() init];
296
297     // FIXME: Should this show a yellow highlight?
298     _currentActionContext = [actionContext contextForView:_webView altMode:YES interactionStartedHandler:^() {
299     } interactionChangedHandler:^() {
300     } interactionStoppedHandler:^() {
301     }];
302
303     [_currentActionContext setHighlightFrame:elementBoundingBoxInWindowCoordinatesFromNode(node)];
304
305     return [[getDDActionsManagerClass() sharedManager] menuItemsForTargetURL:_hitTestResult.absoluteLinkURL() actionContext:_currentActionContext.get()];
306 }
307
308 #pragma mark Image actions
309
310 - (NSArray *)_defaultMenuItemsForImage
311 {
312     RetainPtr<NSMenuItem> copyImageItem = [self _createActionMenuItemForTag:WebActionMenuItemTagCopyImage];
313
314     RetainPtr<NSMenuItem> addToPhotosItem;
315     if ([self _canAddMediaToPhotos])
316         addToPhotosItem = [self _createActionMenuItemForTag:WebActionMenuItemTagAddImageToPhotos];
317     else
318         addToPhotosItem = [NSMenuItem separatorItem];
319
320     RetainPtr<NSMenuItem> saveToDownloadsItem = [self _createActionMenuItemForTag:WebActionMenuItemTagSaveImageToDownloads];
321     if (!_webView.downloadDelegate)
322         [saveToDownloadsItem setEnabled:NO];
323
324     RetainPtr<NSMenuItem> shareItem = [self _createActionMenuItemForTag:WebActionMenuItemTagShareImage];
325     if (Image* image = _hitTestResult.image()) {
326         RefPtr<SharedBuffer> buffer = image->data();
327         if (buffer) {
328             RetainPtr<NSData> nsData = [NSData dataWithBytes:buffer->data() length:buffer->size()];
329             RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithData:nsData.get()]);
330             _sharingServicePicker = adoptNS([[NSSharingServicePicker alloc] initWithItems:@[ nsImage.get() ]]);
331             [_sharingServicePicker setDelegate:self];
332             [shareItem setSubmenu:[_sharingServicePicker menu]];
333         } else
334             [shareItem setEnabled:NO];
335     }
336
337     return @[ copyImageItem.get(), addToPhotosItem.get(), saveToDownloadsItem.get(), shareItem.get() ];
338 }
339
340 - (void)_copyImage:(id)sender
341 {
342     Frame* frame = core([_webView _selectedOrMainFrame]);
343     if (!frame)
344         return;
345     frame->editor().copyImage(_hitTestResult);
346 }
347
348 static NSString *temporaryPhotosDirectoryPath()
349 {
350     static NSString *temporaryPhotosDirectoryPath;
351
352     if (!temporaryPhotosDirectoryPath) {
353         NSString *temporaryDirectoryTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPhotos-XXXXXX"];
354         CString templateRepresentation = [temporaryDirectoryTemplate fileSystemRepresentation];
355
356         if (mkdtemp(templateRepresentation.mutableData()))
357             temporaryPhotosDirectoryPath = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:templateRepresentation.data() length:templateRepresentation.length()] copy];
358     }
359
360     return temporaryPhotosDirectoryPath;
361 }
362
363 static NSString *pathToPhotoOnDisk(NSString *suggestedFilename)
364 {
365     NSString *photoDirectoryPath = temporaryPhotosDirectoryPath();
366     if (!photoDirectoryPath) {
367         WTFLogAlways("Cannot create temporary photo download directory.");
368         return nil;
369     }
370
371     NSString *path = [photoDirectoryPath stringByAppendingPathComponent:suggestedFilename];
372
373     NSFileManager *fileManager = [NSFileManager defaultManager];
374     if ([fileManager fileExistsAtPath:path]) {
375         NSString *pathTemplatePrefix = [photoDirectoryPath stringByAppendingPathComponent:@"XXXXXX-"];
376         NSString *pathTemplate = [pathTemplatePrefix stringByAppendingString:suggestedFilename];
377         CString pathTemplateRepresentation = [pathTemplate fileSystemRepresentation];
378
379         int fd = mkstemps(pathTemplateRepresentation.mutableData(), pathTemplateRepresentation.length() - strlen([pathTemplatePrefix fileSystemRepresentation]) + 1);
380         if (fd < 0) {
381             WTFLogAlways("Cannot create photo file in the temporary directory (%@).", suggestedFilename);
382             return nil;
383         }
384
385         close(fd);
386         path = [fileManager stringWithFileSystemRepresentation:pathTemplateRepresentation.data() length:pathTemplateRepresentation.length()];
387     }
388
389     return path;
390 }
391
392 - (BOOL)_canAddMediaToPhotos
393 {
394     return [getIKSlideshowClass() canExportToApplication:@"com.apple.Photos"];
395 }
396
397 - (void)_addImageToPhotos:(id)sender
398 {
399     if (![self _canAddMediaToPhotos])
400         return;
401
402     Image* image = _hitTestResult.image();
403     if (!image)
404         return;
405
406     String imageExtension = image->filenameExtension();
407     if (imageExtension.isEmpty())
408         return;
409
410     RefPtr<SharedBuffer> buffer = image->data();
411     if (!buffer)
412         return;
413     RetainPtr<NSData> nsData = [NSData dataWithBytes:buffer->data() length:buffer->size()];
414     RetainPtr<NSString> suggestedFilename = [[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:imageExtension];
415
416     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
417         NSString *filePath = pathToPhotoOnDisk(suggestedFilename.get());
418         if (!filePath)
419             return;
420
421         NSURL *fileURL = [NSURL fileURLWithPath:filePath];
422         [nsData writeToURL:fileURL atomically:NO];
423
424         dispatch_async(dispatch_get_main_queue(), ^{
425             // This API provides no way to report failure, but if 18420778 is fixed so that it does, we should handle this.
426             [getIKSlideshowClass() exportSlideshowItem:filePath toApplication:@"com.apple.Photos"];
427         });
428     });
429 }
430
431 - (void)_saveImageToDownloads:(id)sender
432 {
433     [_webView _downloadURL:_hitTestResult.absoluteImageURL()];
434 }
435
436 #pragma mark Video actions
437
438 - (NSArray*)_defaultMenuItemsForVideo
439 {
440     RetainPtr<NSMenuItem> copyVideoURLItem = [self _createActionMenuItemForTag:WebActionMenuItemTagCopyVideoURL];
441
442     RetainPtr<NSMenuItem> saveToDownloadsItem = [self _createActionMenuItemForTag:WebActionMenuItemTagSaveVideoToDownloads];
443     if (!_hitTestResult.isDownloadableMedia() || !_webView.downloadDelegate)
444         [saveToDownloadsItem setEnabled:NO];
445
446     RetainPtr<NSMenuItem> shareItem = [self _createActionMenuItemForTag:WebActionMenuItemTagShareVideo];
447     NSString *urlToShare = _hitTestResult.absoluteMediaURL();
448     if (!_hitTestResult.isDownloadableMedia()) {
449         [saveToDownloadsItem setEnabled:NO];
450         urlToShare = [_webView mainFrameURL];
451     }
452     _sharingServicePicker = adoptNS([[NSSharingServicePicker alloc] initWithItems:@[ urlToShare ]]);
453     [_sharingServicePicker setDelegate:self];
454     [shareItem setSubmenu:[_sharingServicePicker menu]];
455
456     return @[ copyVideoURLItem.get(), [NSMenuItem separatorItem], saveToDownloadsItem.get(), shareItem.get() ];
457 }
458
459 - (void)_copyVideoURL:(id)sender
460 {
461     NSString *urlToCopy = _hitTestResult.absoluteMediaURL();
462     if (!_hitTestResult.isDownloadableMedia())
463         urlToCopy = [_webView mainFrameURL];
464
465     [[NSPasteboard generalPasteboard] clearContents];
466     [[NSPasteboard generalPasteboard] writeObjects:@[ urlToCopy ]];
467 }
468
469 - (void)_saveVideoToDownloads:(id)sender
470 {
471     [_webView _downloadURL:_hitTestResult.absoluteMediaURL()];
472 }
473
474 #pragma mark Text actions
475
476 - (NSArray *)_defaultMenuItemsForText
477 {
478     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagCopyText];
479     RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagLookupText];
480     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:WebActionMenuItemTagPaste];
481     [pasteItem setEnabled:NO];
482
483     return @[ copyTextItem.get(), lookupTextItem.get(), pasteItem.get() ];
484 }
485
486 - (NSArray *)_defaultMenuItemsForEditableText
487 {
488     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagCopyText];
489     RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagLookupText];
490     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:WebActionMenuItemTagPaste];
491
492     return @[ copyTextItem.get(), lookupTextItem.get(), pasteItem.get() ];
493 }
494
495 - (NSArray *)_defaultMenuItemsForEditableTextWithSuggestions
496 {
497     Frame* frame = core([_webView _selectedOrMainFrame]);
498     if (!frame)
499         return @[ ];
500
501     NSDictionary *options = nil;
502     RefPtr<Range> lookupRange = rangeForDictionaryLookupAtHitTestResult(_hitTestResult, &options);
503     if (!lookupRange)
504         return @[ ];
505
506     String lookupText = lookupRange->text();
507     TextCheckerClient* textChecker = frame->editor().textChecker();
508     if (!textChecker)
509         return @[ ];
510
511     Vector<TextCheckingResult> results = textChecker->checkTextOfParagraph(lookupText, NSTextCheckingTypeSpelling);
512     if (results.isEmpty())
513         return @[ ];
514
515     Vector<String> guesses;
516     frame->editor().textChecker()->getGuessesForWord(lookupText, String(), guesses);
517     if (guesses.isEmpty())
518         return @[ ];
519
520     RetainPtr<NSMenu> spellingSubMenu = adoptNS([[NSMenu alloc] init]);
521     for (const auto& guess : guesses) {
522         RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:guess action:@selector(_changeSelectionToSuggestion:) keyEquivalent:@""]);
523         [item setRepresentedObject:guess];
524         [item setTarget:self];
525         [spellingSubMenu addItem:item.get()];
526     }
527
528     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagCopyText];
529     RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagLookupText];
530     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:WebActionMenuItemTagPaste];
531     RetainPtr<NSMenuItem> textSuggestionsItem = [self _createActionMenuItemForTag:WebActionMenuItemTagTextSuggestions];
532
533     [textSuggestionsItem setSubmenu:spellingSubMenu.get()];
534
535     return @[ copyTextItem.get(), lookupTextItem.get(), pasteItem.get(), textSuggestionsItem.get() ];
536 }
537
538 - (void)_selectDataDetectedText
539 {
540     [_webView _mainCoreFrame]->selection().setSelectedRange(_currentDetectedDataRange.get(), DOWNSTREAM, true);
541 }
542
543 - (NSArray *)_defaultMenuItemsForDataDetectedText
544 {
545     RefPtr<Range> detectedDataRange;
546     FloatRect detectedDataBoundingBox;
547     RetainPtr<DDActionContext> actionContext;
548
549     if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:actionContextForHitTestResult:range:)]) {
550         RetainPtr<WebElementDictionary> hitTestDictionary = adoptNS([[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult]);
551
552         DOMRange *customDataDetectorsRange;
553         actionContext = [[_webView UIDelegate] _webView:_webView actionContextForHitTestResult:hitTestDictionary.get() range:&customDataDetectorsRange];
554
555         if (actionContext && customDataDetectorsRange)
556             detectedDataRange = core(customDataDetectorsRange);
557     }
558
559     // If the client didn't give us an action context, try to scan around the hit point.
560     if (!actionContext || !detectedDataRange)
561         actionContext = DataDetection::detectItemAroundHitTestResult(_hitTestResult, detectedDataBoundingBox, detectedDataRange);
562
563     if (!actionContext || !detectedDataRange)
564         return @[ ];
565
566     [actionContext setAltMode:YES];
567     if ([[getDDActionsManagerClass() sharedManager] respondsToSelector:@selector(hasActionsForResult:actionContext:)]) {
568         if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:[actionContext mainResult] actionContext:actionContext.get()])
569             return @[ ];
570     }
571
572     _currentDetectedDataTextIndicator = TextIndicator::createWithRange(*detectedDataRange, TextIndicatorPresentationTransition::BounceAndCrossfade);
573
574     _currentActionContext = [actionContext contextForView:_webView altMode:YES interactionStartedHandler:^() {
575     } interactionChangedHandler:^() {
576         [self _showTextIndicator];
577     } interactionStoppedHandler:^() {
578         [self _hideTextIndicator];
579     }];
580     _currentDetectedDataRange = detectedDataRange;
581
582     [_currentActionContext setHighlightFrame:[_webView.window convertRectToScreen:detectedDataBoundingBox]];
583
584     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
585     if (menuItems.count == 1 && _currentDetectedDataTextIndicator)
586         _currentDetectedDataTextIndicator->setPresentationTransition(TextIndicatorPresentationTransition::Bounce);
587     return menuItems;
588 }
589
590 - (void)_copySelection:(id)sender
591 {
592     [_webView _executeCoreCommandByName:@"copy" value:nil];
593 }
594
595 - (void)_lookupText:(id)sender
596 {
597     Frame* frame = core([_webView _selectedOrMainFrame]);
598     if (!frame)
599         return;
600
601     DictionaryPopupInfo popupInfo = performDictionaryLookupForSelection(frame, frame->selection().selection());
602     [_webView _showDictionaryLookupPopup:popupInfo];
603 }
604
605 - (void)_paste:(id)sender
606 {
607     [_webView _executeCoreCommandByName:@"paste" value:nil];
608 }
609
610 - (void)_selectLookupText
611 {
612     NSDictionary *options = nil;
613     RefPtr<Range> lookupRange = rangeForDictionaryLookupAtHitTestResult(_hitTestResult, &options);
614     if (!lookupRange)
615         return;
616
617     Frame* frame = _hitTestResult.innerNode()->document().frame();
618     if (!frame)
619         return;
620
621     frame->selection().setSelectedRange(lookupRange.get(), DOWNSTREAM, true);
622 }
623
624 - (void)_changeSelectionToSuggestion:(id)sender
625 {
626     NSString *selectedCorrection = [sender representedObject];
627     if (!selectedCorrection)
628         return;
629
630     ASSERT([selectedCorrection isKindOfClass:[NSString class]]);
631
632     WebHTMLView *documentView = [[[_webView _selectedOrMainFrame] frameView] documentView];
633     [documentView _changeSpellingToWord:selectedCorrection];
634 }
635
636 static DictionaryPopupInfo performDictionaryLookupForSelection(Frame* frame, const VisibleSelection& selection)
637 {
638     NSDictionary *options = nil;
639     DictionaryPopupInfo popupInfo;
640     RefPtr<Range> selectedRange = rangeForDictionaryLookupForSelection(selection, &options);
641     if (selectedRange)
642         popupInfo = performDictionaryLookupForRange(frame, *selectedRange, options, TextIndicatorPresentationTransition::BounceAndCrossfade);
643     return popupInfo;
644 }
645
646 static DictionaryPopupInfo performDictionaryLookupForRange(Frame* frame, Range& range, NSDictionary *options, TextIndicatorPresentationTransition presentationTransition)
647 {
648     DictionaryPopupInfo popupInfo;
649     if (range.text().stripWhiteSpace().isEmpty())
650         return popupInfo;
651     
652     RenderObject* renderer = range.startContainer()->renderer();
653     const RenderStyle& style = renderer->style();
654
655     Vector<FloatQuad> quads;
656     range.textQuads(quads);
657     if (quads.isEmpty())
658         return popupInfo;
659
660     IntRect rangeRect = frame->view()->contentsToWindow(quads[0].enclosingBoundingBox());
661
662     popupInfo.origin = NSMakePoint(rangeRect.x(), rangeRect.y() + (style.fontMetrics().descent() * frame->page()->pageScaleFactor()));
663     popupInfo.options = options;
664
665     NSAttributedString *nsAttributedString = editingAttributedStringFromRange(range, IncludeImagesInAttributedString::No);
666     RetainPtr<NSMutableAttributedString> scaledNSAttributedString = adoptNS([[NSMutableAttributedString alloc] initWithString:[nsAttributedString string]]);
667     NSFontManager *fontManager = [NSFontManager sharedFontManager];
668
669     [nsAttributedString enumerateAttributesInRange:NSMakeRange(0, [nsAttributedString length]) options:0 usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop) {
670         RetainPtr<NSMutableDictionary> scaledAttributes = adoptNS([attributes mutableCopy]);
671
672         NSFont *font = [scaledAttributes objectForKey:NSFontAttributeName];
673         if (font) {
674             font = [fontManager convertFont:font toSize:[font pointSize] * frame->page()->pageScaleFactor()];
675             [scaledAttributes setObject:font forKey:NSFontAttributeName];
676         }
677
678         [scaledNSAttributedString addAttributes:scaledAttributes.get() range:range];
679     }];
680
681     popupInfo.attributedString = scaledNSAttributedString.get();
682     popupInfo.textIndicator = TextIndicator::createWithRange(range, presentationTransition);
683     return popupInfo;
684 }
685
686 #pragma mark Whitespace actions
687
688 - (NSArray *)_defaultMenuItemsForWhitespaceInEditableArea
689 {
690     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagCopyText];
691     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:WebActionMenuItemTagPaste];
692     [copyTextItem setEnabled:NO];
693
694     return @[ copyTextItem.get(), [NSMenuItem separatorItem], pasteItem.get() ];
695 }
696
697 #pragma mark NSSharingServicePickerDelegate implementation
698
699 - (NSArray *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items mask:(NSSharingServiceMask)mask proposedSharingServices:(NSArray *)proposedServices
700 {
701     RetainPtr<NSMutableArray> services = adoptNS([[NSMutableArray alloc] initWithCapacity:proposedServices.count]);
702
703     for (NSSharingService *service in proposedServices) {
704         if ([service.name isEqualToString:NSSharingServiceNameAddToIPhoto])
705             continue;
706         [services addObject:service];
707     }
708
709     return services.autorelease();
710 }
711
712 - (id <NSSharingServiceDelegate>)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker delegateForSharingService:(NSSharingService *)sharingService
713 {
714     return self;
715 }
716
717 #pragma mark NSSharingServiceDelegate implementation
718
719 - (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope
720 {
721     return _webView.window;
722 }
723
724 #pragma mark QLPreviewMenuItemDelegate implementation
725
726 - (NSView *)menuItem:(NSMenuItem *)menuItem viewAtScreenPoint:(NSPoint)screenPoint
727 {
728     return _webView;
729 }
730
731 - (id<QLPreviewItem>)menuItem:(NSMenuItem *)menuItem previewItemAtPoint:(NSPoint)point
732 {
733     if (!_webView)
734         return nil;
735
736     return _hitTestResult.absoluteLinkURL();
737 }
738
739 - (NSRectEdge)menuItem:(NSMenuItem *)menuItem preferredEdgeForPoint:(NSPoint)point
740 {
741     return NSMaxYEdge;
742 }
743
744 #pragma mark Menu Items
745
746 - (RetainPtr<NSMenuItem>)_createActionMenuItemForTag:(uint32_t)tag
747 {
748     SEL selector = nullptr;
749     NSString *title = nil;
750     NSImage *image = nil;
751     bool enabled = true;
752
753     switch (tag) {
754     case WebActionMenuItemTagOpenLinkInDefaultBrowser:
755         selector = @selector(_openURLFromActionMenu:);
756         title = WEB_UI_STRING_KEY("Open", "Open (action menu item)", "action menu item");
757         image = [NSImage imageNamed:@"NSActionMenuOpenInNewWindow"];
758         break;
759
760     case WebActionMenuItemTagAddLinkToSafariReadingList:
761         selector = @selector(_addToReadingListFromActionMenu:);
762         title = WEB_UI_STRING_KEY("Add to Reading List", "Add to Reading List (action menu item)", "action menu item");
763         image = [NSImage imageNamed:@"NSActionMenuAddToReadingList"];
764         break;
765
766     case WebActionMenuItemTagCopyText:
767         selector = @selector(_copySelection:);
768         title = WEB_UI_STRING_KEY("Copy", "Copy (text action menu item)", "text action menu item");
769         image = [NSImage imageNamed:@"NSActionMenuCopy"];
770         enabled = _hitTestResult.allowsCopy();
771         break;
772
773     case WebActionMenuItemTagLookupText:
774         selector = @selector(_lookupText:);
775         title = WEB_UI_STRING_KEY("Look Up", "Look Up (action menu item)", "action menu item");
776         image = [NSImage imageNamed:@"NSActionMenuLookup"];
777         enabled = getLULookupDefinitionModuleClass() && _hitTestResult.allowsCopy();
778         break;
779
780     case WebActionMenuItemTagPaste:
781         selector = @selector(_paste:);
782         title = WEB_UI_STRING_KEY("Paste", "Paste (action menu item)", "action menu item");
783         image = [NSImage imageNamed:@"NSActionMenuPaste"];
784         break;
785
786     case WebActionMenuItemTagTextSuggestions:
787         title = WEB_UI_STRING_KEY("Suggestions", "Suggestions (action menu item)", "action menu item");
788         image = [NSImage imageNamed:@"NSActionMenuSpelling"];
789         break;
790
791     case WebActionMenuItemTagCopyImage:
792         selector = @selector(_copyImage:);
793         title = WEB_UI_STRING_KEY("Copy", "Copy (image action menu item)", "image action menu item");
794         image = [NSImage imageNamed:@"NSActionMenuCopy"];
795         break;
796
797     case WebActionMenuItemTagAddImageToPhotos:
798         selector = @selector(_addImageToPhotos:);
799         title = WEB_UI_STRING_KEY("Add to Photos", "Add to Photos (action menu item)", "action menu item");
800         image = [NSImage imageNamed:@"NSActionMenuAddToPhotos"];
801         break;
802
803     case WebActionMenuItemTagSaveImageToDownloads:
804         selector = @selector(_saveImageToDownloads:);
805         title = WEB_UI_STRING_KEY("Save to Downloads", "Save to Downloads (image action menu item)", "image action menu item");
806         image = [NSImage imageNamed:@"NSActionMenuSaveToDownloads"];
807         break;
808
809     case WebActionMenuItemTagShareImage:
810         title = WEB_UI_STRING_KEY("Share (image action menu item)", "Share (image action menu item)", "image action menu item");
811         image = [NSImage imageNamed:@"NSActionMenuShare"];
812         break;
813
814     case WebActionMenuItemTagCopyVideoURL:
815         selector = @selector(_copyVideoURL:);
816         title = WEB_UI_STRING_KEY("Copy", "Copy (video action menu item)", "video action menu item");
817         image = [NSImage imageNamed:@"NSActionMenuCopy"];
818         break;
819
820     case WebActionMenuItemTagSaveVideoToDownloads:
821         selector = @selector(_saveVideoToDownloads:);
822         title = WEB_UI_STRING_KEY("Save to Downloads", "Save to Downloads (video action menu item)", "video action menu item");
823         image = [NSImage imageNamed:@"NSActionMenuSaveToDownloads"];
824         break;
825
826     case WebActionMenuItemTagShareVideo:
827         title = WEB_UI_STRING_KEY("Share", "Share (video action menu item)", "video action menu item");
828         image = [NSImage imageNamed:@"NSActionMenuShare"];
829         break;
830
831     default:
832         ASSERT_NOT_REACHED();
833         return nil;
834     }
835
836     RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]);
837     [item setImage:image];
838     [item setTarget:self];
839     [item setTag:tag];
840     [item setEnabled:enabled];
841     return item;
842 }
843
844 - (NSArray *)_defaultMenuItems
845 {
846     NSURL *url = _hitTestResult.absoluteLinkURL();
847     NSString *absoluteURLString = [url absoluteString];
848     if (url && WebCore::protocolIsInHTTPFamily(absoluteURLString)) {
849         _type = WebActionMenuLink;
850         return [self _defaultMenuItemsForLink];
851     }
852
853     if (url && WebCore::protocolIs(absoluteURLString, "mailto")) {
854         _type = WebActionMenuMailtoLink;
855         return [self _defaultMenuItemsForDataDetectableLink];
856     }
857
858     if (url && WebCore::protocolIs(absoluteURLString, "tel")) {
859         _type = WebActionMenuTelLink;
860         return [self _defaultMenuItemsForDataDetectableLink];
861     }
862
863     if (!_hitTestResult.absoluteMediaURL().isEmpty()) {
864         _type = WebActionMenuVideo;
865         return [self _defaultMenuItemsForVideo];
866     }
867
868     Image* image = _hitTestResult.image();
869     if (image && !_hitTestResult.absoluteImageURL().isEmpty() && !image->filenameExtension().isEmpty() && image->data() && !image->data()->isEmpty()) {
870         _type = WebActionMenuImage;
871         return [self _defaultMenuItemsForImage];
872     }
873
874     Node* node = _hitTestResult.innerNode();
875     if ((node && node->isTextNode()) || _hitTestResult.isOverTextInsideFormControlElement()) {
876         NSArray *dataDetectorMenuItems = [self _defaultMenuItemsForDataDetectedText];
877         if (_currentActionContext) {
878             // If this is a data detected item with no menu items, we should not fall back to regular text options.
879             if (!dataDetectorMenuItems.count) {
880                 _type = WebActionMenuNone;
881                 return @[ ];
882             }
883             _type = WebActionMenuDataDetectedItem;
884             return dataDetectorMenuItems;
885         }
886
887         if (_hitTestResult.isContentEditable()) {
888             NSArray *editableTextWithSuggestions = [self _defaultMenuItemsForEditableTextWithSuggestions];
889             if (editableTextWithSuggestions.count) {
890                 _type = WebActionMenuEditableTextWithSuggestions;
891                 return editableTextWithSuggestions;
892             }
893
894             _type = WebActionMenuEditableText;
895             return [self _defaultMenuItemsForEditableText];
896         }
897
898         _type = WebActionMenuReadOnlyText;
899         return [self _defaultMenuItemsForText];
900     }
901
902     if (_hitTestResult.isContentEditable()) {
903         _type = WebActionMenuWhitespaceInEditableArea;
904         return [self _defaultMenuItemsForWhitespaceInEditableArea];
905     }
906
907     if (_hitTestResult.isSelected()) {
908         // A selection should present the read-only text menu. It might make more sense to present a new
909         // type of menu with just copy, but for the time being, we should stay consistent with text.
910         _type = WebActionMenuReadOnlyText;
911         return [self _defaultMenuItemsForText];
912     }
913
914     _type = WebActionMenuNone;
915     return @[ ];
916 }
917
918 #pragma mark Text Indicator
919
920 - (void)_showTextIndicator
921 {
922     if (_isShowingTextIndicator)
923         return;
924
925     if (_type == WebActionMenuDataDetectedItem && _currentDetectedDataTextIndicator) {
926         [_webView _setTextIndicator:_currentDetectedDataTextIndicator.get() fadeOut:NO animationCompletionHandler:^ { }];
927         _isShowingTextIndicator = YES;
928     }
929 }
930
931 - (void)_hideTextIndicator
932 {
933     if (!_isShowingTextIndicator)
934         return;
935
936     [_webView _clearTextIndicator];
937     _isShowingTextIndicator = NO;
938 }
939
940 - (void)_dismissActionMenuPopovers
941 {
942     DDActionsManager *actionsManager = [getDDActionsManagerClass() sharedManager];
943     if ([actionsManager respondsToSelector:@selector(requestBubbleClosureUnanchorOnFailure:)])
944         [actionsManager requestBubbleClosureUnanchorOnFailure:YES];
945
946     [self _hideTextIndicator];
947 }
948
949 @end
950
951 #endif // PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000