31aa4bc03b68d3a0e1cf09b672ae5d78bf82a152
[WebKit-https.git] / Source / WebKit / mac / WebView / WebImmediateActionController.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 "WebImmediateActionController.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 "WebElementDictionary.h"
35 #import "WebFrameInternal.h"
36 #import "WebHTMLView.h"
37 #import "WebHTMLViewInternal.h"
38 #import "WebUIDelegatePrivate.h"
39 #import "WebViewInternal.h"
40 #import <WebCore/DataDetection.h>
41 #import <WebCore/DataDetectorsSPI.h>
42 #import <WebCore/DictionaryLookup.h>
43 #import <WebCore/EventHandler.h>
44 #import <WebCore/FocusController.h>
45 #import <WebCore/Frame.h>
46 #import <WebCore/FrameView.h>
47 #import <WebCore/GeometryUtilities.h>
48 #import <WebCore/HTMLConverter.h>
49 #import <WebCore/LookupSPI.h>
50 #import <WebCore/NSMenuSPI.h>
51 #import <WebCore/Page.h>
52 #import <WebCore/QuickLookMacSPI.h>
53 #import <WebCore/RenderElement.h>
54 #import <WebCore/RenderObject.h>
55 #import <WebCore/RuntimeApplicationChecks.h>
56 #import <WebCore/SoftLinking.h>
57 #import <WebCore/TextIndicator.h>
58 #import <objc/objc-class.h>
59 #import <objc/objc.h>
60
61 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, QuickLookUI)
62 SOFT_LINK_CLASS(QuickLookUI, QLPreviewMenuItem)
63
64 @interface WebImmediateActionController () <QLPreviewMenuItemDelegate>
65 @end
66
67 @interface WebAnimationController : NSObject <NSImmediateActionAnimationController> {
68 }
69 @end
70
71 @implementation WebAnimationController
72 @end
73
74 using namespace WebCore;
75
76 @implementation WebImmediateActionController
77
78 - (instancetype)initWithWebView:(WebView *)webView recognizer:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
79 {
80     if (!(self = [super init]))
81         return nil;
82
83     _webView = webView;
84     _type = WebImmediateActionNone;
85     _immediateActionRecognizer = immediateActionRecognizer;
86
87     return self;
88 }
89
90 - (void)webViewClosed
91 {
92     _webView = nil;
93
94     id animationController = [_immediateActionRecognizer animationController];
95     if ([animationController isKindOfClass:NSClassFromString(@"QLPreviewMenuItem")]) {
96         QLPreviewMenuItem *menuItem = (QLPreviewMenuItem *)animationController;
97         menuItem.delegate = nil;
98     }
99
100     _immediateActionRecognizer = nil;
101     _currentActionContext = nil;
102 }
103
104 - (void)webView:(WebView *)webView didHandleScrollWheel:(NSEvent *)event
105 {
106     [_currentQLPreviewMenuItem close];
107     [self _clearImmediateActionState];
108     [_webView _clearTextIndicatorWithAnimation:TextIndicatorDismissalAnimation::None];
109 }
110
111 - (NSImmediateActionGestureRecognizer *)immediateActionRecognizer
112 {
113     return _immediateActionRecognizer.get();
114 }
115
116 - (void)_cancelImmediateAction
117 {
118     // Reset the recognizer by turning it off and on again.
119     [_immediateActionRecognizer setEnabled:NO];
120     [_immediateActionRecognizer setEnabled:YES];
121
122     [self _clearImmediateActionState];
123     [_webView _clearTextIndicatorWithAnimation:TextIndicatorDismissalAnimation::FadeOut];
124 }
125
126 - (void)_clearImmediateActionState
127 {
128     DDActionsManager *actionsManager = [getDDActionsManagerClass() sharedManager];
129     if ([actionsManager respondsToSelector:@selector(requestBubbleClosureUnanchorOnFailure:)])
130         [actionsManager requestBubbleClosureUnanchorOnFailure:YES];
131
132     if (_currentActionContext && _hasActivatedActionContext) {
133         _hasActivatedActionContext = NO;
134         [getDDActionsManagerClass() didUseActions];
135     }
136
137     _type = WebImmediateActionNone;
138     _currentActionContext = nil;
139     _currentQLPreviewMenuItem = nil;
140     _contentPreventsDefault = NO;
141 }
142
143 - (void)performHitTestAtPoint:(NSPoint)viewPoint
144 {
145     Frame* coreFrame = core([[[[_webView _selectedOrMainFrame] frameView] documentView] _frame]);
146     if (!coreFrame)
147         return;
148     _hitTestResult = coreFrame->eventHandler().hitTestResultAtPoint(IntPoint(viewPoint));
149     coreFrame->eventHandler().setImmediateActionStage(ImmediateActionStage::PerformedHitTest);
150
151     if (Element* element = _hitTestResult.innerElement())
152         _contentPreventsDefault = element->dispatchMouseForceWillBegin();
153 }
154
155 #pragma mark NSImmediateActionGestureRecognizerDelegate
156
157 - (void)immediateActionRecognizerWillPrepare:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
158 {
159     if (!_webView)
160         return;
161
162     if (immediateActionRecognizer != _immediateActionRecognizer)
163         return;
164
165     [_webView _setMaintainsInactiveSelection:YES];
166
167     WebHTMLView *documentView = [[[_webView _selectedOrMainFrame] frameView] documentView];
168     NSPoint locationInDocumentView = [immediateActionRecognizer locationInView:documentView];
169     [self performHitTestAtPoint:locationInDocumentView];
170     [self _updateImmediateActionItem];
171
172     if (![_immediateActionRecognizer animationController]) {
173         // FIXME: We should be able to remove the dispatch_async when rdar://problem/19502927 is resolved.
174         dispatch_async(dispatch_get_main_queue(), ^{
175             [self _cancelImmediateAction];
176         });
177     }
178 }
179
180 - (void)immediateActionRecognizerWillBeginAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
181 {
182     if (immediateActionRecognizer != _immediateActionRecognizer)
183         return;
184
185     if (_currentActionContext) {
186         _hasActivatedActionContext = YES;
187         if (![getDDActionsManagerClass() shouldUseActionsWithContext:_currentActionContext.get()])
188             [self _cancelImmediateAction];
189     }
190 }
191
192 - (void)immediateActionRecognizerDidUpdateAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
193 {
194     if (immediateActionRecognizer != _immediateActionRecognizer)
195         return;
196
197     Frame* coreFrame = core([[[[_webView _selectedOrMainFrame] frameView] documentView] _frame]);
198     if (!coreFrame)
199         return;
200     coreFrame->eventHandler().setImmediateActionStage(ImmediateActionStage::ActionUpdated);
201     if (_contentPreventsDefault)
202         return;
203
204     [_webView _setTextIndicatorAnimationProgress:[immediateActionRecognizer animationProgress]];
205 }
206
207 - (void)immediateActionRecognizerDidCancelAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
208 {
209     if (immediateActionRecognizer != _immediateActionRecognizer)
210         return;
211
212     Frame* coreFrame = core([[[[_webView _selectedOrMainFrame] frameView] documentView] _frame]);
213     if (coreFrame)
214         coreFrame->eventHandler().setImmediateActionStage(ImmediateActionStage::ActionCancelled);
215
216     [_webView _setTextIndicatorAnimationProgress:0];
217     [self _clearImmediateActionState];
218     [_webView _clearTextIndicatorWithAnimation:TextIndicatorDismissalAnimation::None];
219     [_webView _setMaintainsInactiveSelection:NO];
220 }
221
222 - (void)immediateActionRecognizerDidCompleteAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
223 {
224     if (immediateActionRecognizer != _immediateActionRecognizer)
225         return;
226
227     Frame* coreFrame = core([[[[_webView _selectedOrMainFrame] frameView] documentView] _frame]);
228     if (!coreFrame)
229         return;
230     coreFrame->eventHandler().setImmediateActionStage(ImmediateActionStage::ActionCompleted);
231
232     [_webView _setTextIndicatorAnimationProgress:1];
233     [_webView _setMaintainsInactiveSelection:NO];
234 }
235
236 #pragma mark Immediate actions
237
238 - (id <NSImmediateActionAnimationController>)_defaultAnimationController
239 {
240     if (_contentPreventsDefault) {
241         RetainPtr<WebAnimationController> dummyController = [[WebAnimationController alloc] init];
242         return dummyController.get();
243     }
244
245     NSURL *url = _hitTestResult.absoluteLinkURL();
246     NSString *absoluteURLString = [url absoluteString];
247     if (url && _hitTestResult.URLElement()) {
248         if (protocolIs(absoluteURLString, "mailto")) {
249             _type = WebImmediateActionMailtoLink;
250             return [self _animationControllerForDataDetectedLink];
251         }
252
253         if (protocolIs(absoluteURLString, "tel")) {
254             _type = WebImmediateActionTelLink;
255             return [self _animationControllerForDataDetectedLink];
256         }
257
258         if (WebCore::protocolIsInHTTPFamily(absoluteURLString)) {
259             _type = WebImmediateActionLinkPreview;
260
261             RefPtr<Range> linkRange = rangeOfContents(*_hitTestResult.URLElement());
262             RefPtr<TextIndicator> indicator = TextIndicator::createWithRange(*linkRange, TextIndicatorPresentationTransition::FadeIn);
263             [_webView _setTextIndicator:*indicator withLifetime:TextIndicatorLifetime::Permanent];
264
265             QLPreviewMenuItem *item = [NSMenuItem standardQuickLookMenuItem];
266             item.previewStyle = QLPreviewStylePopover;
267             item.delegate = self;
268             _currentQLPreviewMenuItem = item;
269             return (id <NSImmediateActionAnimationController>)item;
270         }
271     }
272
273     Node* node = _hitTestResult.innerNode();
274     if ((node && node->isTextNode()) || _hitTestResult.isOverTextInsideFormControlElement()) {
275         if (auto animationController = [self _animationControllerForDataDetectedText]) {
276             _type = WebImmediateActionDataDetectedItem;
277             return animationController;
278         }
279
280         if (auto animationController = [self _animationControllerForText]) {
281             _type = WebImmediateActionText;
282             return animationController;
283         }
284     }
285
286     return nil;
287 }
288
289 - (void)_updateImmediateActionItem
290 {
291     _type = WebImmediateActionNone;
292
293     id <NSImmediateActionAnimationController> defaultAnimationController = [self _defaultAnimationController];
294
295     if (_contentPreventsDefault) {
296         [_immediateActionRecognizer setAnimationController:defaultAnimationController];
297         return;
298     }
299
300     // Allow clients the opportunity to override the default immediate action.
301     id customClientAnimationController = nil;
302     if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:immediateActionAnimationControllerForHitTestResult:withType:)]) {
303         RetainPtr<WebElementDictionary> webHitTestResult = adoptNS([[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult]);
304         customClientAnimationController = [(id)[_webView UIDelegate] _webView:_webView immediateActionAnimationControllerForHitTestResult:webHitTestResult.get() withType:_type];
305     }
306
307     // FIXME: We should not permanently disable this for iTunes. rdar://problem/19461358
308     if (customClientAnimationController == [NSNull null] || applicationIsITunes()) {
309         [self _cancelImmediateAction];
310         return;
311     }
312     if (customClientAnimationController && [customClientAnimationController conformsToProtocol:@protocol(NSImmediateActionAnimationController)])
313         [_immediateActionRecognizer setAnimationController:(id <NSImmediateActionAnimationController>)customClientAnimationController];
314     else
315         [_immediateActionRecognizer setAnimationController:defaultAnimationController];
316 }
317
318 #pragma mark QLPreviewMenuItemDelegate implementation
319
320 - (NSView *)menuItem:(NSMenuItem *)menuItem viewAtScreenPoint:(NSPoint)screenPoint
321 {
322     return _webView;
323 }
324
325 - (id<QLPreviewItem>)menuItem:(NSMenuItem *)menuItem previewItemAtPoint:(NSPoint)point
326 {
327     if (!_webView)
328         return nil;
329
330     return _hitTestResult.absoluteLinkURL();
331 }
332
333 - (NSRectEdge)menuItem:(NSMenuItem *)menuItem preferredEdgeForPoint:(NSPoint)point
334 {
335     return NSMaxYEdge;
336 }
337
338 - (void)menuItemDidClose:(NSMenuItem *)menuItem
339 {
340     [self _clearImmediateActionState];
341     [_webView _clearTextIndicatorWithAnimation:TextIndicatorDismissalAnimation::FadeOut];
342 }
343
344 static IntRect elementBoundingBoxInWindowCoordinatesFromNode(Node* node)
345 {
346     if (!node)
347         return IntRect();
348
349     Frame* frame = node->document().frame();
350     if (!frame)
351         return IntRect();
352
353     FrameView* view = frame->view();
354     if (!view)
355         return IntRect();
356
357     RenderObject* renderer = node->renderer();
358     if (!renderer)
359         return IntRect();
360
361     return view->contentsToWindow(renderer->absoluteBoundingBoxRect());
362 }
363
364 - (NSRect)menuItem:(NSMenuItem *)menuItem itemFrameForPoint:(NSPoint)point
365 {
366     if (!_webView)
367         return NSZeroRect;
368
369     Node* node = _hitTestResult.innerNode();
370     if (!node)
371         return NSZeroRect;
372
373     return elementBoundingBoxInWindowCoordinatesFromNode(node);
374 }
375
376 - (NSSize)menuItem:(NSMenuItem *)menuItem maxSizeForPoint:(NSPoint)point
377 {
378     if (!_webView)
379         return NSZeroSize;
380
381     NSSize screenSize = _webView.window.screen.frame.size;
382     FloatRect largestRect = largestRectWithAspectRatioInsideRect(screenSize.width / screenSize.height, _webView.bounds);
383     return NSMakeSize(largestRect.width() * 0.75, largestRect.height() * 0.75);
384 }
385
386 #pragma mark Data Detectors actions
387
388 - (id <NSImmediateActionAnimationController>)_animationControllerForDataDetectedText
389 {
390     RefPtr<Range> detectedDataRange;
391     FloatRect detectedDataBoundingBox;
392     RetainPtr<DDActionContext> actionContext;
393
394     if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:actionContextForHitTestResult:range:)]) {
395         RetainPtr<WebElementDictionary> hitTestDictionary = adoptNS([[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult]);
396
397         DOMRange *customDataDetectorsRange;
398         actionContext = [(id)[_webView UIDelegate] _webView:_webView actionContextForHitTestResult:hitTestDictionary.get() range:&customDataDetectorsRange];
399
400         if (actionContext && customDataDetectorsRange)
401             detectedDataRange = core(customDataDetectorsRange);
402     }
403
404     // If the client didn't give us an action context, try to scan around the hit point.
405     if (!actionContext || !detectedDataRange)
406         actionContext = DataDetection::detectItemAroundHitTestResult(_hitTestResult, detectedDataBoundingBox, detectedDataRange);
407
408     if (!actionContext || !detectedDataRange)
409         return nil;
410
411     [actionContext setAltMode:YES];
412     [actionContext setImmediate:YES];
413     if ([[getDDActionsManagerClass() sharedManager] respondsToSelector:@selector(hasActionsForResult:actionContext:)]) {
414         if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:[actionContext mainResult] actionContext:actionContext.get()])
415             return nil;
416     }
417
418     RefPtr<TextIndicator> detectedDataTextIndicator = TextIndicator::createWithRange(*detectedDataRange, TextIndicatorPresentationTransition::FadeIn);
419
420     _currentActionContext = [actionContext contextForView:_webView altMode:YES interactionStartedHandler:^() {
421     } interactionChangedHandler:^() {
422         [_webView _setTextIndicator:*detectedDataTextIndicator withLifetime:TextIndicatorLifetime::Permanent];
423     } interactionStoppedHandler:^() {
424         [_webView _clearTextIndicatorWithAnimation:TextIndicatorDismissalAnimation::FadeOut];
425     }];
426
427     [_currentActionContext setHighlightFrame:[_webView.window convertRectToScreen:detectedDataBoundingBox]];
428
429     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
430     if (menuItems.count != 1)
431         return nil;
432
433     return menuItems.lastObject;
434 }
435
436 - (id <NSImmediateActionAnimationController>)_animationControllerForDataDetectedLink
437 {
438     RetainPtr<DDActionContext> actionContext = adoptNS([allocDDActionContextInstance() init]);
439
440     if (!actionContext)
441         return nil;
442
443     [actionContext setAltMode:YES];
444     [actionContext setImmediate:YES];
445
446     RefPtr<Range> linkRange = rangeOfContents(*_hitTestResult.URLElement());
447     if (!linkRange)
448         return nullptr;
449     RefPtr<TextIndicator> indicator = TextIndicator::createWithRange(*linkRange, TextIndicatorPresentationTransition::FadeIn);
450
451     _currentActionContext = [actionContext contextForView:_webView altMode:YES interactionStartedHandler:^() {
452     } interactionChangedHandler:^() {
453         [_webView _setTextIndicator:*indicator withLifetime:TextIndicatorLifetime::Permanent];
454     } interactionStoppedHandler:^() {
455         [_webView _clearTextIndicatorWithAnimation:TextIndicatorDismissalAnimation::FadeOut];
456     }];
457
458     [_currentActionContext setHighlightFrame:[_webView.window convertRectToScreen:elementBoundingBoxInWindowCoordinatesFromNode(_hitTestResult.URLElement())]];
459
460     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForTargetURL:_hitTestResult.absoluteLinkURL() actionContext:_currentActionContext.get()];
461     if (menuItems.count != 1)
462         return nil;
463     
464     return menuItems.lastObject;
465 }
466
467 #pragma mark Text action
468
469 static DictionaryPopupInfo dictionaryPopupInfoForRange(Frame* frame, Range& range, NSDictionary *options, TextIndicatorPresentationTransition presentationTransition)
470 {
471     DictionaryPopupInfo popupInfo;
472     if (range.text().stripWhiteSpace().isEmpty())
473         return popupInfo;
474     
475     RenderObject* renderer = range.startContainer()->renderer();
476     const RenderStyle& style = renderer->style();
477
478     Vector<FloatQuad> quads;
479     range.textQuads(quads);
480     if (quads.isEmpty())
481         return popupInfo;
482
483     IntRect rangeRect = frame->view()->contentsToWindow(quads[0].enclosingBoundingBox());
484
485     popupInfo.origin = NSMakePoint(rangeRect.x(), rangeRect.y() + (style.fontMetrics().descent() * frame->page()->pageScaleFactor()));
486     popupInfo.options = options;
487
488     NSAttributedString *nsAttributedString = editingAttributedStringFromRange(range, IncludeImagesInAttributedString::No);
489     RetainPtr<NSMutableAttributedString> scaledNSAttributedString = adoptNS([[NSMutableAttributedString alloc] initWithString:[nsAttributedString string]]);
490     NSFontManager *fontManager = [NSFontManager sharedFontManager];
491
492     [nsAttributedString enumerateAttributesInRange:NSMakeRange(0, [nsAttributedString length]) options:0 usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop) {
493         RetainPtr<NSMutableDictionary> scaledAttributes = adoptNS([attributes mutableCopy]);
494
495         NSFont *font = [scaledAttributes objectForKey:NSFontAttributeName];
496         if (font) {
497             font = [fontManager convertFont:font toSize:[font pointSize] * frame->page()->pageScaleFactor()];
498             [scaledAttributes setObject:font forKey:NSFontAttributeName];
499         }
500
501         [scaledNSAttributedString addAttributes:scaledAttributes.get() range:range];
502     }];
503
504     popupInfo.attributedString = scaledNSAttributedString.get();
505     popupInfo.textIndicator = TextIndicator::createWithRange(range, presentationTransition);
506     return popupInfo;
507 }
508
509 - (id<NSImmediateActionAnimationController>)_animationControllerForText
510 {
511     if (!getLULookupDefinitionModuleClass())
512         return nil;
513
514     Node* node = _hitTestResult.innerNode();
515     if (!node)
516         return nil;
517
518     Frame* frame = node->document().frame();
519     if (!frame)
520         return nil;
521
522     NSDictionary *options = nil;
523     RefPtr<Range> dictionaryRange = rangeForDictionaryLookupAtHitTestResult(_hitTestResult, &options);
524     if (!dictionaryRange)
525         return nil;
526
527     RefPtr<Range> selectionRange = frame->page()->focusController().focusedOrMainFrame().selection().selection().firstRange();
528     bool rangeMatchesSelection = areRangesEqual(dictionaryRange.get(), selectionRange.get());
529     DictionaryPopupInfo dictionaryPopupInfo = dictionaryPopupInfoForRange(frame, *dictionaryRange, options, rangeMatchesSelection ? TextIndicatorPresentationTransition::Crossfade : TextIndicatorPresentationTransition::FadeIn);
530     if (!dictionaryPopupInfo.attributedString)
531         return nil;
532
533     return [_webView _animationControllerForDictionaryLookupPopupInfo:dictionaryPopupInfo];
534 }
535
536 @end
537
538 #endif // PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000