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