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