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