Deselection of text causes a noticeable jump on force touch machines
[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             if (indicator)
264                 [_webView _setTextIndicator:*indicator withLifetime:TextIndicatorLifetime::Permanent];
265
266             QLPreviewMenuItem *item = [NSMenuItem standardQuickLookMenuItem];
267             item.previewStyle = QLPreviewStylePopover;
268             item.delegate = self;
269             _currentQLPreviewMenuItem = item;
270             return (id <NSImmediateActionAnimationController>)item;
271         }
272     }
273
274     Node* node = _hitTestResult.innerNode();
275     if ((node && node->isTextNode()) || _hitTestResult.isOverTextInsideFormControlElement()) {
276         if (auto animationController = [self _animationControllerForDataDetectedText]) {
277             _type = WebImmediateActionDataDetectedItem;
278             return animationController;
279         }
280
281         if (auto animationController = [self _animationControllerForText]) {
282             _type = WebImmediateActionText;
283             return animationController;
284         }
285     }
286
287     return nil;
288 }
289
290 - (void)_updateImmediateActionItem
291 {
292     _type = WebImmediateActionNone;
293
294     id <NSImmediateActionAnimationController> defaultAnimationController = [self _defaultAnimationController];
295
296     if (_contentPreventsDefault) {
297         [_immediateActionRecognizer setAnimationController:defaultAnimationController];
298         return;
299     }
300
301     // Allow clients the opportunity to override the default immediate action.
302     id customClientAnimationController = nil;
303     if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:immediateActionAnimationControllerForHitTestResult:withType:)]) {
304         RetainPtr<WebElementDictionary> webHitTestResult = adoptNS([[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult]);
305         customClientAnimationController = [(id)[_webView UIDelegate] _webView:_webView immediateActionAnimationControllerForHitTestResult:webHitTestResult.get() withType:_type];
306     }
307
308     // FIXME: We should not permanently disable this for iTunes. rdar://problem/19461358
309     if (customClientAnimationController == [NSNull null] || applicationIsITunes()) {
310         [self _cancelImmediateAction];
311         return;
312     }
313     if (customClientAnimationController && [customClientAnimationController conformsToProtocol:@protocol(NSImmediateActionAnimationController)])
314         [_immediateActionRecognizer setAnimationController:(id <NSImmediateActionAnimationController>)customClientAnimationController];
315     else
316         [_immediateActionRecognizer setAnimationController:defaultAnimationController];
317 }
318
319 #pragma mark QLPreviewMenuItemDelegate implementation
320
321 - (NSView *)menuItem:(NSMenuItem *)menuItem viewAtScreenPoint:(NSPoint)screenPoint
322 {
323     return _webView;
324 }
325
326 - (id<QLPreviewItem>)menuItem:(NSMenuItem *)menuItem previewItemAtPoint:(NSPoint)point
327 {
328     if (!_webView)
329         return nil;
330
331     return _hitTestResult.absoluteLinkURL();
332 }
333
334 - (NSRectEdge)menuItem:(NSMenuItem *)menuItem preferredEdgeForPoint:(NSPoint)point
335 {
336     return NSMaxYEdge;
337 }
338
339 - (void)menuItemDidClose:(NSMenuItem *)menuItem
340 {
341     [self _clearImmediateActionState];
342     [_webView _clearTextIndicatorWithAnimation:TextIndicatorDismissalAnimation::FadeOut];
343 }
344
345 static IntRect elementBoundingBoxInWindowCoordinatesFromNode(Node* node)
346 {
347     if (!node)
348         return IntRect();
349
350     Frame* frame = node->document().frame();
351     if (!frame)
352         return IntRect();
353
354     FrameView* view = frame->view();
355     if (!view)
356         return IntRect();
357
358     RenderObject* renderer = node->renderer();
359     if (!renderer)
360         return IntRect();
361
362     return view->contentsToWindow(renderer->absoluteBoundingBoxRect());
363 }
364
365 - (NSRect)menuItem:(NSMenuItem *)menuItem itemFrameForPoint:(NSPoint)point
366 {
367     if (!_webView)
368         return NSZeroRect;
369
370     Node* node = _hitTestResult.innerNode();
371     if (!node)
372         return NSZeroRect;
373
374     return elementBoundingBoxInWindowCoordinatesFromNode(node);
375 }
376
377 - (NSSize)menuItem:(NSMenuItem *)menuItem maxSizeForPoint:(NSPoint)point
378 {
379     if (!_webView)
380         return NSZeroSize;
381
382     NSSize screenSize = _webView.window.screen.frame.size;
383     FloatRect largestRect = largestRectWithAspectRatioInsideRect(screenSize.width / screenSize.height, _webView.bounds);
384     return NSMakeSize(largestRect.width() * 0.75, largestRect.height() * 0.75);
385 }
386
387 #pragma mark Data Detectors actions
388
389 - (id <NSImmediateActionAnimationController>)_animationControllerForDataDetectedText
390 {
391     RefPtr<Range> detectedDataRange;
392     FloatRect detectedDataBoundingBox;
393     RetainPtr<DDActionContext> actionContext;
394
395     if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:actionContextForHitTestResult:range:)]) {
396         RetainPtr<WebElementDictionary> hitTestDictionary = adoptNS([[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult]);
397
398         DOMRange *customDataDetectorsRange;
399         actionContext = [(id)[_webView UIDelegate] _webView:_webView actionContextForHitTestResult:hitTestDictionary.get() range:&customDataDetectorsRange];
400
401         if (actionContext && customDataDetectorsRange)
402             detectedDataRange = core(customDataDetectorsRange);
403     }
404
405     // If the client didn't give us an action context, try to scan around the hit point.
406     if (!actionContext || !detectedDataRange)
407         actionContext = DataDetection::detectItemAroundHitTestResult(_hitTestResult, detectedDataBoundingBox, detectedDataRange);
408
409     if (!actionContext || !detectedDataRange)
410         return nil;
411
412     [actionContext setAltMode:YES];
413     [actionContext setImmediate:YES];
414     if ([[getDDActionsManagerClass() sharedManager] respondsToSelector:@selector(hasActionsForResult:actionContext:)]) {
415         if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:[actionContext mainResult] actionContext:actionContext.get()])
416             return nil;
417     }
418
419     RefPtr<TextIndicator> detectedDataTextIndicator = TextIndicator::createWithRange(*detectedDataRange, TextIndicatorPresentationTransition::FadeIn);
420
421     _currentActionContext = [actionContext contextForView:_webView altMode:YES interactionStartedHandler:^() {
422     } interactionChangedHandler:^() {
423         if (detectedDataTextIndicator)
424             [_webView _setTextIndicator:*detectedDataTextIndicator withLifetime:TextIndicatorLifetime::Permanent];
425     } interactionStoppedHandler:^() {
426         [_webView _clearTextIndicatorWithAnimation:TextIndicatorDismissalAnimation::FadeOut];
427     }];
428
429     [_currentActionContext setHighlightFrame:[_webView.window convertRectToScreen:detectedDataBoundingBox]];
430
431     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
432     if (menuItems.count != 1)
433         return nil;
434
435     return menuItems.lastObject;
436 }
437
438 - (id <NSImmediateActionAnimationController>)_animationControllerForDataDetectedLink
439 {
440     RetainPtr<DDActionContext> actionContext = adoptNS([allocDDActionContextInstance() init]);
441
442     if (!actionContext)
443         return nil;
444
445     [actionContext setAltMode:YES];
446     [actionContext setImmediate:YES];
447
448     RefPtr<Range> linkRange = rangeOfContents(*_hitTestResult.URLElement());
449     if (!linkRange)
450         return nullptr;
451     RefPtr<TextIndicator> indicator = TextIndicator::createWithRange(*linkRange, TextIndicatorPresentationTransition::FadeIn);
452
453     _currentActionContext = [actionContext contextForView:_webView altMode:YES interactionStartedHandler:^() {
454     } interactionChangedHandler:^() {
455         if (indicator)
456             [_webView _setTextIndicator:*indicator withLifetime:TextIndicatorLifetime::Permanent];
457     } interactionStoppedHandler:^() {
458         [_webView _clearTextIndicatorWithAnimation:TextIndicatorDismissalAnimation::FadeOut];
459     }];
460
461     [_currentActionContext setHighlightFrame:[_webView.window convertRectToScreen:elementBoundingBoxInWindowCoordinatesFromNode(_hitTestResult.URLElement())]];
462
463     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForTargetURL:_hitTestResult.absoluteLinkURL() actionContext:_currentActionContext.get()];
464     if (menuItems.count != 1)
465         return nil;
466     
467     return menuItems.lastObject;
468 }
469
470 #pragma mark Text action
471
472 static DictionaryPopupInfo dictionaryPopupInfoForRange(Frame* frame, Range& range, NSDictionary *options, TextIndicatorPresentationTransition presentationTransition)
473 {
474     DictionaryPopupInfo popupInfo;
475     if (range.text().stripWhiteSpace().isEmpty())
476         return popupInfo;
477     
478     RenderObject* renderer = range.startContainer()->renderer();
479     const RenderStyle& style = renderer->style();
480
481     Vector<FloatQuad> quads;
482     range.textQuads(quads);
483     if (quads.isEmpty())
484         return popupInfo;
485
486     IntRect rangeRect = frame->view()->contentsToWindow(quads[0].enclosingBoundingBox());
487
488     popupInfo.origin = NSMakePoint(rangeRect.x(), rangeRect.y() + (style.fontMetrics().descent() * frame->page()->pageScaleFactor()));
489     popupInfo.options = options;
490
491     NSAttributedString *nsAttributedString = editingAttributedStringFromRange(range, IncludeImagesInAttributedString::No);
492     RetainPtr<NSMutableAttributedString> scaledNSAttributedString = adoptNS([[NSMutableAttributedString alloc] initWithString:[nsAttributedString string]]);
493     NSFontManager *fontManager = [NSFontManager sharedFontManager];
494
495     [nsAttributedString enumerateAttributesInRange:NSMakeRange(0, [nsAttributedString length]) options:0 usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop) {
496         RetainPtr<NSMutableDictionary> scaledAttributes = adoptNS([attributes mutableCopy]);
497
498         NSFont *font = [scaledAttributes objectForKey:NSFontAttributeName];
499         if (font) {
500             font = [fontManager convertFont:font toSize:[font pointSize] * frame->page()->pageScaleFactor()];
501             [scaledAttributes setObject:font forKey:NSFontAttributeName];
502         }
503
504         [scaledNSAttributedString addAttributes:scaledAttributes.get() range:range];
505     }];
506
507     popupInfo.attributedString = scaledNSAttributedString.get();
508     popupInfo.textIndicator = TextIndicator::createWithRange(range, presentationTransition);
509     return popupInfo;
510 }
511
512 - (id<NSImmediateActionAnimationController>)_animationControllerForText
513 {
514     if (!getLULookupDefinitionModuleClass())
515         return nil;
516
517     Node* node = _hitTestResult.innerNode();
518     if (!node)
519         return nil;
520
521     Frame* frame = node->document().frame();
522     if (!frame)
523         return nil;
524
525     NSDictionary *options = nil;
526     RefPtr<Range> dictionaryRange = rangeForDictionaryLookupAtHitTestResult(_hitTestResult, &options);
527     if (!dictionaryRange)
528         return nil;
529
530     RefPtr<Range> selectionRange = frame->page()->focusController().focusedOrMainFrame().selection().selection().firstRange();
531     DictionaryPopupInfo dictionaryPopupInfo = dictionaryPopupInfoForRange(frame, *dictionaryRange, options, TextIndicatorPresentationTransition::FadeIn);
532     if (!dictionaryPopupInfo.attributedString)
533         return nil;
534
535     return [_webView _animationControllerForDictionaryLookupPopupInfo:dictionaryPopupInfo];
536 }
537
538 @end
539
540 #endif // PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000