[iOS] Add setting to ignore viewport scaling constraints
[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
481     return @[ copyTextItem.get(), lookupTextItem.get() ];
482 }
483
484 - (NSArray *)_defaultMenuItemsForEditableText
485 {
486     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagCopyText];
487     RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagLookupText];
488     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:WebActionMenuItemTagPaste];
489
490     return @[ copyTextItem.get(), lookupTextItem.get(), pasteItem.get() ];
491 }
492
493 - (NSArray *)_defaultMenuItemsForEditableTextWithSuggestions
494 {
495     Frame* frame = core([_webView _selectedOrMainFrame]);
496     if (!frame)
497         return @[ ];
498
499     NSDictionary *options = nil;
500     RefPtr<Range> lookupRange = rangeForDictionaryLookupAtHitTestResult(_hitTestResult, &options);
501     if (!lookupRange)
502         return @[ ];
503
504     String lookupText = lookupRange->text();
505     TextCheckerClient* textChecker = frame->editor().textChecker();
506     if (!textChecker)
507         return @[ ];
508
509     Vector<TextCheckingResult> results = textChecker->checkTextOfParagraph(lookupText, NSTextCheckingTypeSpelling);
510     if (results.isEmpty())
511         return @[ ];
512
513     Vector<String> guesses;
514     frame->editor().textChecker()->getGuessesForWord(lookupText, String(), guesses);
515     if (guesses.isEmpty())
516         return @[ ];
517
518     RetainPtr<NSMenu> spellingSubMenu = adoptNS([[NSMenu alloc] init]);
519     for (const auto& guess : guesses) {
520         RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:guess action:@selector(_changeSelectionToSuggestion:) keyEquivalent:@""]);
521         [item setRepresentedObject:guess];
522         [item setTarget:self];
523         [spellingSubMenu addItem:item.get()];
524     }
525
526     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagCopyText];
527     RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:WebActionMenuItemTagLookupText];
528     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:WebActionMenuItemTagPaste];
529     RetainPtr<NSMenuItem> textSuggestionsItem = [self _createActionMenuItemForTag:WebActionMenuItemTagTextSuggestions];
530
531     [textSuggestionsItem setSubmenu:spellingSubMenu.get()];
532
533     return @[ copyTextItem.get(), lookupTextItem.get(), pasteItem.get(), textSuggestionsItem.get() ];
534 }
535
536 - (void)_selectDataDetectedText
537 {
538     [_webView _mainCoreFrame]->selection().setSelectedRange(_currentDetectedDataRange.get(), DOWNSTREAM, true);
539 }
540
541 - (NSArray *)_defaultMenuItemsForDataDetectedText
542 {
543     RefPtr<Range> detectedDataRange;
544     FloatRect detectedDataBoundingBox;
545     RetainPtr<DDActionContext> actionContext;
546
547     if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:actionContextForHitTestResult:range:)]) {
548         RetainPtr<WebElementDictionary> hitTestDictionary = adoptNS([[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult]);
549
550         DOMRange *customDataDetectorsRange;
551         actionContext = [[_webView UIDelegate] _webView:_webView actionContextForHitTestResult:hitTestDictionary.get() range:&customDataDetectorsRange];
552
553         if (actionContext && customDataDetectorsRange)
554             detectedDataRange = core(customDataDetectorsRange);
555     }
556
557     // If the client didn't give us an action context, try to scan around the hit point.
558     if (!actionContext || !detectedDataRange)
559         actionContext = DataDetection::detectItemAroundHitTestResult(_hitTestResult, detectedDataBoundingBox, detectedDataRange);
560
561     if (!actionContext || !detectedDataRange)
562         return @[ ];
563
564     [actionContext setAltMode:YES];
565     if ([[getDDActionsManagerClass() sharedManager] respondsToSelector:@selector(hasActionsForResult:actionContext:)]) {
566         if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:[actionContext mainResult] actionContext:actionContext.get()])
567             return @[ ];
568     }
569
570     _currentDetectedDataTextIndicator = TextIndicator::createWithRange(*detectedDataRange, TextIndicatorPresentationTransition::BounceAndCrossfade);
571
572     _currentActionContext = [actionContext contextForView:_webView altMode:YES interactionStartedHandler:^() {
573     } interactionChangedHandler:^() {
574         [self _showTextIndicator];
575     } interactionStoppedHandler:^() {
576         [self _hideTextIndicator];
577     }];
578     _currentDetectedDataRange = detectedDataRange;
579
580     [_currentActionContext setHighlightFrame:[_webView.window convertRectToScreen:detectedDataBoundingBox]];
581
582     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
583     if (menuItems.count == 1 && _currentDetectedDataTextIndicator)
584         _currentDetectedDataTextIndicator->setPresentationTransition(TextIndicatorPresentationTransition::Bounce);
585     return menuItems;
586 }
587
588 - (void)_copySelection:(id)sender
589 {
590     [_webView _executeCoreCommandByName:@"copy" value:nil];
591 }
592
593 - (void)_lookupText:(id)sender
594 {
595     Frame* frame = core([_webView _selectedOrMainFrame]);
596     if (!frame)
597         return;
598
599     DictionaryPopupInfo popupInfo = performDictionaryLookupForSelection(frame, frame->selection().selection());
600     [_webView _showDictionaryLookupPopup:popupInfo];
601 }
602
603 - (void)_paste:(id)sender
604 {
605     [_webView _executeCoreCommandByName:@"paste" value:nil];
606 }
607
608 - (void)_selectLookupText
609 {
610     NSDictionary *options = nil;
611     RefPtr<Range> lookupRange = rangeForDictionaryLookupAtHitTestResult(_hitTestResult, &options);
612     if (!lookupRange)
613         return;
614
615     Frame* frame = _hitTestResult.innerNode()->document().frame();
616     if (!frame)
617         return;
618
619     frame->selection().setSelectedRange(lookupRange.get(), DOWNSTREAM, true);
620 }
621
622 - (void)_changeSelectionToSuggestion:(id)sender
623 {
624     NSString *selectedCorrection = [sender representedObject];
625     if (!selectedCorrection)
626         return;
627
628     ASSERT([selectedCorrection isKindOfClass:[NSString class]]);
629
630     WebHTMLView *documentView = [[[_webView _selectedOrMainFrame] frameView] documentView];
631     [documentView _changeSpellingToWord:selectedCorrection];
632 }
633
634 static DictionaryPopupInfo performDictionaryLookupForSelection(Frame* frame, const VisibleSelection& selection)
635 {
636     NSDictionary *options = nil;
637     DictionaryPopupInfo popupInfo;
638     RefPtr<Range> selectedRange = rangeForDictionaryLookupForSelection(selection, &options);
639     if (selectedRange)
640         popupInfo = performDictionaryLookupForRange(frame, *selectedRange, options, TextIndicatorPresentationTransition::BounceAndCrossfade);
641     return popupInfo;
642 }
643
644 static DictionaryPopupInfo performDictionaryLookupForRange(Frame* frame, Range& range, NSDictionary *options, TextIndicatorPresentationTransition presentationTransition)
645 {
646     DictionaryPopupInfo popupInfo;
647     if (range.text().stripWhiteSpace().isEmpty())
648         return popupInfo;
649     
650     RenderObject* renderer = range.startContainer()->renderer();
651     const RenderStyle& style = renderer->style();
652
653     Vector<FloatQuad> quads;
654     range.textQuads(quads);
655     if (quads.isEmpty())
656         return popupInfo;
657
658     IntRect rangeRect = frame->view()->contentsToWindow(quads[0].enclosingBoundingBox());
659
660     popupInfo.origin = NSMakePoint(rangeRect.x(), rangeRect.y() + (style.fontMetrics().descent() * frame->page()->pageScaleFactor()));
661     popupInfo.options = options;
662
663     NSAttributedString *nsAttributedString = editingAttributedStringFromRange(range, IncludeImagesInAttributedString::No);
664     RetainPtr<NSMutableAttributedString> scaledNSAttributedString = adoptNS([[NSMutableAttributedString alloc] initWithString:[nsAttributedString string]]);
665     NSFontManager *fontManager = [NSFontManager sharedFontManager];
666
667     [nsAttributedString enumerateAttributesInRange:NSMakeRange(0, [nsAttributedString length]) options:0 usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop) {
668         RetainPtr<NSMutableDictionary> scaledAttributes = adoptNS([attributes mutableCopy]);
669
670         NSFont *font = [scaledAttributes objectForKey:NSFontAttributeName];
671         if (font) {
672             font = [fontManager convertFont:font toSize:[font pointSize] * frame->page()->pageScaleFactor()];
673             [scaledAttributes setObject:font forKey:NSFontAttributeName];
674         }
675
676         [scaledNSAttributedString addAttributes:scaledAttributes.get() range:range];
677     }];
678
679     popupInfo.attributedString = scaledNSAttributedString.get();
680     popupInfo.textIndicator = TextIndicator::createWithRange(range, presentationTransition);
681     return popupInfo;
682 }
683
684 #pragma mark Whitespace actions
685
686 - (NSArray *)_defaultMenuItemsForWhitespaceInEditableArea
687 {
688     RetainPtr<NSMenuItem> pasteItem = [self _createActionMenuItemForTag:WebActionMenuItemTagPaste];
689
690     return @[ [NSMenuItem separatorItem], [NSMenuItem separatorItem], pasteItem.get() ];
691 }
692
693 #pragma mark NSSharingServicePickerDelegate implementation
694
695 - (NSArray *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items mask:(NSSharingServiceMask)mask proposedSharingServices:(NSArray *)proposedServices
696 {
697     RetainPtr<NSMutableArray> services = adoptNS([[NSMutableArray alloc] initWithCapacity:proposedServices.count]);
698
699     for (NSSharingService *service in proposedServices) {
700         if ([service.name isEqualToString:NSSharingServiceNameAddToIPhoto])
701             continue;
702         [services addObject:service];
703     }
704
705     return services.autorelease();
706 }
707
708 - (id <NSSharingServiceDelegate>)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker delegateForSharingService:(NSSharingService *)sharingService
709 {
710     return self;
711 }
712
713 #pragma mark NSSharingServiceDelegate implementation
714
715 - (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope
716 {
717     return _webView.window;
718 }
719
720 #pragma mark QLPreviewMenuItemDelegate implementation
721
722 - (NSView *)menuItem:(NSMenuItem *)menuItem viewAtScreenPoint:(NSPoint)screenPoint
723 {
724     return _webView;
725 }
726
727 - (id<QLPreviewItem>)menuItem:(NSMenuItem *)menuItem previewItemAtPoint:(NSPoint)point
728 {
729     if (!_webView)
730         return nil;
731
732     return _hitTestResult.absoluteLinkURL();
733 }
734
735 - (NSRectEdge)menuItem:(NSMenuItem *)menuItem preferredEdgeForPoint:(NSPoint)point
736 {
737     return NSMaxYEdge;
738 }
739
740 #pragma mark Menu Items
741
742 - (RetainPtr<NSMenuItem>)_createActionMenuItemForTag:(uint32_t)tag
743 {
744     SEL selector = nullptr;
745     NSString *title = nil;
746     NSImage *image = nil;
747     bool enabled = true;
748
749     switch (tag) {
750     case WebActionMenuItemTagOpenLinkInDefaultBrowser:
751         selector = @selector(_openURLFromActionMenu:);
752         title = WEB_UI_STRING_KEY("Open", "Open (action menu item)", "action menu item");
753         image = [NSImage imageNamed:@"NSActionMenuOpenInNewWindow"];
754         break;
755
756     case WebActionMenuItemTagAddLinkToSafariReadingList:
757         selector = @selector(_addToReadingListFromActionMenu:);
758         title = WEB_UI_STRING_KEY("Add to Reading List", "Add to Reading List (action menu item)", "action menu item");
759         image = [NSImage imageNamed:@"NSActionMenuAddToReadingList"];
760         break;
761
762     case WebActionMenuItemTagCopyText:
763         selector = @selector(_copySelection:);
764         title = WEB_UI_STRING_KEY("Copy", "Copy (text action menu item)", "text action menu item");
765         image = [NSImage imageNamed:@"NSActionMenuCopy"];
766         enabled = _hitTestResult.allowsCopy();
767         break;
768
769     case WebActionMenuItemTagLookupText:
770         selector = @selector(_lookupText:);
771         title = WEB_UI_STRING_KEY("Look Up", "Look Up (action menu item)", "action menu item");
772         image = [NSImage imageNamed:@"NSActionMenuLookup"];
773         enabled = getLULookupDefinitionModuleClass() && _hitTestResult.allowsCopy();
774         break;
775
776     case WebActionMenuItemTagPaste:
777         selector = @selector(_paste:);
778         title = WEB_UI_STRING_KEY("Paste", "Paste (action menu item)", "action menu item");
779         image = [NSImage imageNamed:@"NSActionMenuPaste"];
780         break;
781
782     case WebActionMenuItemTagTextSuggestions:
783         title = WEB_UI_STRING_KEY("Suggestions", "Suggestions (action menu item)", "action menu item");
784         image = [NSImage imageNamed:@"NSActionMenuSpelling"];
785         break;
786
787     case WebActionMenuItemTagCopyImage:
788         selector = @selector(_copyImage:);
789         title = WEB_UI_STRING_KEY("Copy", "Copy (image action menu item)", "image action menu item");
790         image = [NSImage imageNamed:@"NSActionMenuCopy"];
791         break;
792
793     case WebActionMenuItemTagAddImageToPhotos:
794         selector = @selector(_addImageToPhotos:);
795         title = WEB_UI_STRING_KEY("Add to Photos", "Add to Photos (action menu item)", "action menu item");
796         image = [NSImage imageNamed:@"NSActionMenuAddToPhotos"];
797         break;
798
799     case WebActionMenuItemTagSaveImageToDownloads:
800         selector = @selector(_saveImageToDownloads:);
801         title = WEB_UI_STRING_KEY("Save to Downloads", "Save to Downloads (image action menu item)", "image action menu item");
802         image = [NSImage imageNamed:@"NSActionMenuSaveToDownloads"];
803         break;
804
805     case WebActionMenuItemTagShareImage:
806         title = WEB_UI_STRING_KEY("Share (image action menu item)", "Share (image action menu item)", "image action menu item");
807         image = [NSImage imageNamed:@"NSActionMenuShare"];
808         break;
809
810     case WebActionMenuItemTagCopyVideoURL:
811         selector = @selector(_copyVideoURL:);
812         title = WEB_UI_STRING_KEY("Copy", "Copy (video action menu item)", "video action menu item");
813         image = [NSImage imageNamed:@"NSActionMenuCopy"];
814         break;
815
816     case WebActionMenuItemTagSaveVideoToDownloads:
817         selector = @selector(_saveVideoToDownloads:);
818         title = WEB_UI_STRING_KEY("Save to Downloads", "Save to Downloads (video action menu item)", "video action menu item");
819         image = [NSImage imageNamed:@"NSActionMenuSaveToDownloads"];
820         break;
821
822     case WebActionMenuItemTagShareVideo:
823         title = WEB_UI_STRING_KEY("Share", "Share (video action menu item)", "video action menu item");
824         image = [NSImage imageNamed:@"NSActionMenuShare"];
825         break;
826
827     default:
828         ASSERT_NOT_REACHED();
829         return nil;
830     }
831
832     RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]);
833     [item setImage:image];
834     [item setTarget:self];
835     [item setTag:tag];
836     [item setEnabled:enabled];
837     return item;
838 }
839
840 - (NSArray *)_defaultMenuItems
841 {
842     NSURL *url = _hitTestResult.absoluteLinkURL();
843     NSString *absoluteURLString = [url absoluteString];
844     if (url && WebCore::protocolIsInHTTPFamily(absoluteURLString)) {
845         _type = WebActionMenuLink;
846         return [self _defaultMenuItemsForLink];
847     }
848
849     if (url && WebCore::protocolIs(absoluteURLString, "mailto")) {
850         _type = WebActionMenuMailtoLink;
851         return [self _defaultMenuItemsForDataDetectableLink];
852     }
853
854     if (url && WebCore::protocolIs(absoluteURLString, "tel")) {
855         _type = WebActionMenuTelLink;
856         return [self _defaultMenuItemsForDataDetectableLink];
857     }
858
859     if (!_hitTestResult.absoluteMediaURL().isEmpty()) {
860         _type = WebActionMenuVideo;
861         return [self _defaultMenuItemsForVideo];
862     }
863
864     Image* image = _hitTestResult.image();
865     if (image && !_hitTestResult.absoluteImageURL().isEmpty() && !image->filenameExtension().isEmpty() && image->data() && !image->data()->isEmpty()) {
866         _type = WebActionMenuImage;
867         return [self _defaultMenuItemsForImage];
868     }
869
870     Node* node = _hitTestResult.innerNode();
871     if ((node && node->isTextNode()) || _hitTestResult.isOverTextInsideFormControlElement()) {
872         NSArray *dataDetectorMenuItems = [self _defaultMenuItemsForDataDetectedText];
873         if (_currentActionContext) {
874             // If this is a data detected item with no menu items, we should not fall back to regular text options.
875             if (!dataDetectorMenuItems.count) {
876                 _type = WebActionMenuNone;
877                 return @[ ];
878             }
879             _type = WebActionMenuDataDetectedItem;
880             return dataDetectorMenuItems;
881         }
882
883         if (_hitTestResult.isContentEditable()) {
884             NSArray *editableTextWithSuggestions = [self _defaultMenuItemsForEditableTextWithSuggestions];
885             if (editableTextWithSuggestions.count) {
886                 _type = WebActionMenuEditableTextWithSuggestions;
887                 return editableTextWithSuggestions;
888             }
889
890             _type = WebActionMenuEditableText;
891             return [self _defaultMenuItemsForEditableText];
892         }
893
894         _type = WebActionMenuReadOnlyText;
895         return [self _defaultMenuItemsForText];
896     }
897
898     if (_hitTestResult.isContentEditable()) {
899         _type = WebActionMenuWhitespaceInEditableArea;
900         return [self _defaultMenuItemsForWhitespaceInEditableArea];
901     }
902
903     if (_hitTestResult.isSelected()) {
904         // A selection should present the read-only text menu. It might make more sense to present a new
905         // type of menu with just copy, but for the time being, we should stay consistent with text.
906         _type = WebActionMenuReadOnlyText;
907         return [self _defaultMenuItemsForText];
908     }
909
910     _type = WebActionMenuNone;
911     return @[ ];
912 }
913
914 #pragma mark Text Indicator
915
916 - (void)_showTextIndicator
917 {
918     if (_isShowingTextIndicator)
919         return;
920
921     if (_type == WebActionMenuDataDetectedItem && _currentDetectedDataTextIndicator) {
922         [_webView _setTextIndicator:_currentDetectedDataTextIndicator.get() fadeOut:NO animationCompletionHandler:^ { }];
923         _isShowingTextIndicator = YES;
924     }
925 }
926
927 - (void)_hideTextIndicator
928 {
929     if (!_isShowingTextIndicator)
930         return;
931
932     [_webView _clearTextIndicator];
933     _isShowingTextIndicator = NO;
934 }
935
936 - (void)_dismissActionMenuPopovers
937 {
938     DDActionsManager *actionsManager = [getDDActionsManagerClass() sharedManager];
939     if ([actionsManager respondsToSelector:@selector(requestBubbleClosureUnanchorOnFailure:)])
940         [actionsManager requestBubbleClosureUnanchorOnFailure:YES];
941
942     [self _hideTextIndicator];
943 }
944
945 @end
946
947 #endif // PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000