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