Don't register for Lookup notifications until needed
[WebKit-https.git] / Source / WebKit2 / UIProcess / mac / WKImmediateActionController.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 "config.h"
27 #import "WKImmediateActionController.h"
28
29 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
30
31 #import "WKNSURLExtras.h"
32 #import "WKViewInternal.h"
33 #import "WebPageMessages.h"
34 #import "WebPageProxy.h"
35 #import "WebPageProxyMessages.h"
36 #import "WebProcessProxy.h"
37 #import <WebCore/DataDetectorsSPI.h>
38 #import <WebCore/GeometryUtilities.h>
39 #import <WebCore/LookupSPI.h>
40 #import <WebCore/NSMenuSPI.h>
41 #import <WebCore/NSPopoverSPI.h>
42 #import <WebCore/QuickLookMacSPI.h>
43 #import <WebCore/SoftLinking.h>
44 #import <WebCore/URL.h>
45
46 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, QuickLookUI)
47 SOFT_LINK_CLASS(QuickLookUI, QLPreviewMenuItem)
48 SOFT_LINK_CONSTANT_MAY_FAIL(Lookup, LUTermOptionDisableSearchTermIndicator, NSString *)
49
50 using namespace WebCore;
51 using namespace WebKit;
52
53 @interface WKImmediateActionController () <QLPreviewMenuItemDelegate>
54 @end
55
56 @interface WebAnimationController : NSObject <NSImmediateActionAnimationController> {
57 }
58 @end
59
60 @implementation WebAnimationController
61 @end
62
63 @implementation WKImmediateActionController
64
65 - (instancetype)initWithPage:(WebPageProxy&)page view:(WKView *)wkView recognizer:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
66 {
67     self = [super init];
68
69     if (!self)
70         return nil;
71
72     _page = &page;
73     _wkView = wkView;
74     _type = kWKImmediateActionNone;
75     _immediateActionRecognizer = immediateActionRecognizer;
76     _hasActiveImmediateAction = NO;
77
78     return self;
79 }
80
81 - (void)willDestroyView:(WKView *)view
82 {
83     _page = nullptr;
84     _wkView = nil;
85     _hitTestResultData = WebHitTestResult::Data();
86     _contentPreventsDefault = NO;
87     
88     id animationController = [_immediateActionRecognizer animationController];
89     if ([animationController isKindOfClass:NSClassFromString(@"QLPreviewMenuItem")]) {
90         QLPreviewMenuItem *menuItem = (QLPreviewMenuItem *)animationController;
91         menuItem.delegate = nil;
92     }
93
94     _immediateActionRecognizer = nil;
95     _currentActionContext = nil;
96     _hasActiveImmediateAction = NO;
97 }
98
99 - (void)_cancelImmediateAction
100 {
101     // Reset the recognizer by turning it off and on again.
102     [_immediateActionRecognizer setEnabled:NO];
103     [_immediateActionRecognizer setEnabled:YES];
104
105     [self _clearImmediateActionState];
106 }
107
108 - (void)_cancelImmediateActionIfNeeded
109 {
110     if (![_immediateActionRecognizer animationController])
111         [self _cancelImmediateAction];
112 }
113
114 - (void)_clearImmediateActionState
115 {
116     if (_page)
117         _page->clearTextIndicator();
118
119     if (_currentActionContext && _hasActivatedActionContext) {
120         _hasActivatedActionContext = NO;
121         [getDDActionsManagerClass() didUseActions];
122     }
123
124     _state = ImmediateActionState::None;
125     _hitTestResultData = WebHitTestResult::Data();
126     _contentPreventsDefault = NO;
127     _type = kWKImmediateActionNone;
128     _currentActionContext = nil;
129     _userData = nil;
130     _currentQLPreviewMenuItem = nil;
131     _hasActiveImmediateAction = NO;
132 }
133
134 - (void)didPerformActionMenuHitTest:(const WebHitTestResult::Data&)hitTestResult contentPreventsDefault:(BOOL)contentPreventsDefault userData:(API::Object*)userData
135 {
136     // If we've already given up on this gesture (either because it was canceled or the
137     // willBeginAnimation timeout expired), we shouldn't build a new animationController for it.
138     if (_state != ImmediateActionState::Pending)
139         return;
140
141     // FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies.
142     _state = ImmediateActionState::Ready;
143     _hitTestResultData = hitTestResult;
144     _contentPreventsDefault = contentPreventsDefault;
145     _userData = userData;
146
147     [self _updateImmediateActionItem];
148     [self _cancelImmediateActionIfNeeded];
149 }
150
151 - (void)dismissContentRelativeChildWindows
152 {
153     _page->setMaintainsInactiveSelection(false);
154     [_currentQLPreviewMenuItem close];
155 }
156
157 - (BOOL)hasActiveImmediateAction
158 {
159     return _hasActiveImmediateAction;
160 }
161
162 #pragma mark NSImmediateActionGestureRecognizerDelegate
163
164 - (void)immediateActionRecognizerWillPrepare:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
165 {
166     if (immediateActionRecognizer != _immediateActionRecognizer)
167         return;
168
169     [_wkView _prepareForImmediateActionAnimation];
170
171     [_wkView _dismissContentRelativeChildWindows];
172
173     _page->setMaintainsInactiveSelection(true);
174
175     _page->performActionMenuHitTestAtLocation([immediateActionRecognizer locationInView:immediateActionRecognizer.view], true);
176
177     _state = ImmediateActionState::Pending;
178     immediateActionRecognizer.animationController = nil;
179 }
180
181 - (void)immediateActionRecognizerWillBeginAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
182 {
183     if (immediateActionRecognizer != _immediateActionRecognizer)
184         return;
185
186     if (_state == ImmediateActionState::None)
187         return;
188
189     _hasActiveImmediateAction = YES;
190
191     // FIXME: We need to be able to cancel this if the gesture recognizer is cancelled.
192     // FIXME: Connection can be null if the process is closed; we should clean up better in that case.
193     if (_state == ImmediateActionState::Pending) {
194         if (auto* connection = _page->process().connection()) {
195             bool receivedReply = connection->waitForAndDispatchImmediately<Messages::WebPageProxy::DidPerformActionMenuHitTest>(_page->pageID(), std::chrono::milliseconds(500));
196             if (!receivedReply)
197                 _state = ImmediateActionState::TimedOut;
198         }
199     }
200
201     if (_state != ImmediateActionState::Ready) {
202         [self _updateImmediateActionItem];
203         [self _cancelImmediateActionIfNeeded];
204     }
205
206     if (_currentActionContext) {
207         _hasActivatedActionContext = YES;
208         if (![getDDActionsManagerClass() shouldUseActionsWithContext:_currentActionContext.get()])
209             [self _cancelImmediateAction];
210     }
211 }
212
213 - (void)immediateActionRecognizerDidUpdateAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
214 {
215     if (immediateActionRecognizer != _immediateActionRecognizer)
216         return;
217
218     _page->immediateActionDidUpdate();
219     if (_contentPreventsDefault)
220         return;
221
222     _page->setTextIndicatorAnimationProgress([immediateActionRecognizer animationProgress]);
223 }
224
225 - (void)immediateActionRecognizerDidCancelAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
226 {
227     if (immediateActionRecognizer != _immediateActionRecognizer)
228         return;
229
230     _page->immediateActionDidCancel();
231
232     [_wkView _cancelImmediateActionAnimation];
233
234     _page->setTextIndicatorAnimationProgress(0);
235     [self _clearImmediateActionState];
236     _page->setMaintainsInactiveSelection(false);
237 }
238
239 - (void)immediateActionRecognizerDidCompleteAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
240 {
241     if (immediateActionRecognizer != _immediateActionRecognizer)
242         return;
243
244     _page->immediateActionDidComplete();
245
246     [_wkView _completeImmediateActionAnimation];
247
248     _page->setTextIndicatorAnimationProgress(1);
249 }
250
251 - (PassRefPtr<WebHitTestResult>)_webHitTestResult
252 {
253     RefPtr<WebHitTestResult> hitTestResult;
254     if (_state == ImmediateActionState::Ready)
255         hitTestResult = WebHitTestResult::create(_hitTestResultData);
256     else
257         hitTestResult = _page->lastMouseMoveHitTestResult();
258
259     return hitTestResult.release();
260 }
261
262 #pragma mark Immediate actions
263
264 - (id <NSImmediateActionAnimationController>)_defaultAnimationController
265 {
266     if (_contentPreventsDefault) {
267         RetainPtr<WebAnimationController> dummyController = [[WebAnimationController alloc] init];
268         return dummyController.get();
269     }
270
271     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
272
273     if (!hitTestResult)
274         return nil;
275
276     String absoluteLinkURL = hitTestResult->absoluteLinkURL();
277     if (!absoluteLinkURL.isEmpty()) {
278         if (protocolIs(absoluteLinkURL, "mailto")) {
279             _type = kWKImmediateActionMailtoLink;
280             return [self _animationControllerForDataDetectedLink];
281         }
282
283         if (protocolIs(absoluteLinkURL, "tel")) {
284             _type = kWKImmediateActionTelLink;
285             return [self _animationControllerForDataDetectedLink];
286         }
287
288         if (WebCore::protocolIsInHTTPFamily(absoluteLinkURL)) {
289             _type = kWKImmediateActionLinkPreview;
290
291             QLPreviewMenuItem *item = [NSMenuItem standardQuickLookMenuItem];
292             item.previewStyle = QLPreviewStylePopover;
293             item.delegate = self;
294             _currentQLPreviewMenuItem = item;
295
296             if (TextIndicator *textIndicator = _hitTestResultData.linkTextIndicator.get())
297                 _page->setTextIndicator(textIndicator->data());
298
299             return (id<NSImmediateActionAnimationController>)item;
300         }
301     }
302
303     if (hitTestResult->isTextNode() || hitTestResult->isOverTextInsideFormControlElement()) {
304         if (auto animationController = [self _animationControllerForDataDetectedText]) {
305             _type = kWKImmediateActionDataDetectedItem;
306             return animationController;
307         }
308
309         if (auto animationController = [self _animationControllerForText]) {
310             _type = kWKImmediateActionLookupText;
311             return animationController;
312         }
313     }
314
315     return nil;
316 }
317
318 - (void)_updateImmediateActionItem
319 {
320     _type = kWKImmediateActionNone;
321
322     id <NSImmediateActionAnimationController> defaultAnimationController = [self _defaultAnimationController];
323
324     if (_contentPreventsDefault) {
325         [_immediateActionRecognizer.get() setAnimationController:defaultAnimationController];
326         return;
327     }
328
329     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
330     id customClientAnimationController = [_wkView _immediateActionAnimationControllerForHitTestResult:toAPI(hitTestResult.get()) withType:_type userData:toAPI(_userData.get())];
331
332     if (customClientAnimationController == [NSNull null]) {
333         [self _cancelImmediateAction];
334         return;
335     }
336
337     if (customClientAnimationController && [customClientAnimationController conformsToProtocol:@protocol(NSImmediateActionAnimationController)])
338         [_immediateActionRecognizer setAnimationController:(id <NSImmediateActionAnimationController>)customClientAnimationController];
339     else
340         [_immediateActionRecognizer setAnimationController:defaultAnimationController];
341 }
342
343 #pragma mark QLPreviewMenuItemDelegate implementation
344
345 - (NSView *)menuItem:(NSMenuItem *)menuItem viewAtScreenPoint:(NSPoint)screenPoint
346 {
347     return _wkView;
348 }
349
350 - (id<QLPreviewItem>)menuItem:(NSMenuItem *)menuItem previewItemAtPoint:(NSPoint)point
351 {
352     if (!_wkView)
353         return nil;
354
355     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
356     return [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()];
357 }
358
359 - (NSRectEdge)menuItem:(NSMenuItem *)menuItem preferredEdgeForPoint:(NSPoint)point
360 {
361     return NSMaxYEdge;
362 }
363
364 - (void)menuItemDidClose:(NSMenuItem *)menuItem
365 {
366     [self _clearImmediateActionState];
367 }
368
369 - (NSRect)menuItem:(NSMenuItem *)menuItem itemFrameForPoint:(NSPoint)point
370 {
371     if (!_wkView)
372         return NSZeroRect;
373
374     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
375     return [_wkView convertRect:hitTestResult->elementBoundingBox() toView:nil];
376 }
377
378 - (NSSize)menuItem:(NSMenuItem *)menuItem maxSizeForPoint:(NSPoint)point
379 {
380     if (!_wkView)
381         return NSZeroSize;
382
383     NSSize screenSize = _wkView.window.screen.frame.size;
384     FloatRect largestRect = largestRectWithAspectRatioInsideRect(screenSize.width / screenSize.height, _wkView.bounds);
385     return NSMakeSize(largestRect.width() * 0.75, largestRect.height() * 0.75);
386 }
387
388 #pragma mark Data Detectors actions
389
390 - (id<NSImmediateActionAnimationController>)_animationControllerForDataDetectedText
391 {
392     DDActionContext *actionContext = _hitTestResultData.detectedDataActionContext.get();
393     if (!actionContext)
394         return nil;
395
396     actionContext.altMode = YES;
397     actionContext.immediate = YES;
398     if ([[getDDActionsManagerClass() sharedManager] respondsToSelector:@selector(hasActionsForResult:actionContext:)]) {
399         if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:actionContext.mainResult actionContext:actionContext])
400             return nil;
401     }
402
403     RefPtr<WebPageProxy> page = _page;
404     PageOverlay::PageOverlayID overlayID = _hitTestResultData.detectedDataOriginatingPageOverlay;
405     _currentActionContext = [actionContext contextForView:_wkView altMode:YES interactionStartedHandler:^() {
406         page->send(Messages::WebPage::DataDetectorsDidPresentUI(overlayID));
407     } interactionChangedHandler:^() {
408         if (_hitTestResultData.detectedDataTextIndicator)
409             page->setTextIndicator(_hitTestResultData.detectedDataTextIndicator->data());
410         page->send(Messages::WebPage::DataDetectorsDidChangeUI(overlayID));
411     } interactionStoppedHandler:^() {
412         page->send(Messages::WebPage::DataDetectorsDidHideUI(overlayID));
413         [self _clearImmediateActionState];
414     }];
415
416     [_currentActionContext setHighlightFrame:[_wkView.window convertRectToScreen:[_wkView convertRect:_hitTestResultData.detectedDataBoundingBox toView:nil]]];
417
418     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
419
420     if (menuItems.count != 1)
421         return nil;
422
423     return (id<NSImmediateActionAnimationController>)menuItems.lastObject;
424 }
425
426 - (id<NSImmediateActionAnimationController>)_animationControllerForDataDetectedLink
427 {
428     RetainPtr<DDActionContext> actionContext = adoptNS([allocDDActionContextInstance() init]);
429
430     if (!actionContext)
431         return nil;
432
433     [actionContext setAltMode:YES];
434     [actionContext setImmediate:YES];
435
436     RefPtr<WebPageProxy> page = _page;
437     _currentActionContext = [actionContext contextForView:_wkView altMode:YES interactionStartedHandler:^() {
438     } interactionChangedHandler:^() {
439         if (_hitTestResultData.linkTextIndicator)
440             page->setTextIndicator(_hitTestResultData.linkTextIndicator->data());
441     } interactionStoppedHandler:^() {
442         [self _clearImmediateActionState];
443     }];
444
445     [_currentActionContext setHighlightFrame:[_wkView.window convertRectToScreen:[_wkView convertRect:_hitTestResultData.elementBoundingBox toView:nil]]];
446
447     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
448     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForTargetURL:hitTestResult->absoluteLinkURL() actionContext:_currentActionContext.get()];
449
450     if (menuItems.count != 1)
451         return nil;
452
453     return (id<NSImmediateActionAnimationController>)menuItems.lastObject;
454 }
455
456 #pragma mark Text action
457
458 - (id<NSImmediateActionAnimationController>)_animationControllerForText
459 {
460     if (_state != ImmediateActionState::Ready)
461         return nil;
462
463     if (!getLULookupDefinitionModuleClass())
464         return nil;
465
466     DictionaryPopupInfo dictionaryPopupInfo = _hitTestResultData.dictionaryPopupInfo;
467     if (!dictionaryPopupInfo.attributedString.string)
468         return nil;
469
470     [_wkView _prepareForDictionaryLookup];
471
472     // Convert baseline to screen coordinates.
473     NSPoint textBaselineOrigin = dictionaryPopupInfo.origin;
474     textBaselineOrigin = [_wkView convertPoint:textBaselineOrigin toView:nil];
475     textBaselineOrigin = [_wkView.window convertRectToScreen:NSMakeRect(textBaselineOrigin.x, textBaselineOrigin.y, 0, 0)].origin;
476
477     RetainPtr<NSMutableDictionary> mutableOptions = adoptNS([(NSDictionary *)dictionaryPopupInfo.options.get() mutableCopy]);
478     if (canLoadLUTermOptionDisableSearchTermIndicator() && dictionaryPopupInfo.textIndicator.contentImage) {
479         RefPtr<TextIndicator> indicator = TextIndicator::create(dictionaryPopupInfo.textIndicator);
480         [_wkView _setTextIndicator:*indicator withLifetime:TextIndicatorLifetime::Permanent];
481         [mutableOptions setObject:@YES forKey:getLUTermOptionDisableSearchTermIndicator()];
482         return [getLULookupDefinitionModuleClass() lookupAnimationControllerForTerm:dictionaryPopupInfo.attributedString.string.get() atLocation:textBaselineOrigin options:mutableOptions.get()];
483     }
484     return [getLULookupDefinitionModuleClass() lookupAnimationControllerForTerm:dictionaryPopupInfo.attributedString.string.get() atLocation:textBaselineOrigin options:mutableOptions.get()];
485 }
486
487 @end
488
489 #endif // PLATFORM(MAC)