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