Move URL from WebCore to WTF
[WebKit-https.git] / Source / WebKit / UIProcess / mac / WKImmediateActionController.mm
1 /*
2  * Copyright (C) 2014-2016 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)
30
31 #import "APIHitTestResult.h"
32 #import "WKNSURLExtras.h"
33 #import "WebPageMessages.h"
34 #import "WebPageProxy.h"
35 #import "WebPageProxyMessages.h"
36 #import "WebProcessProxy.h"
37 #import "WebViewImpl.h"
38 #import <WebCore/DictionaryLookup.h>
39 #import <WebCore/GeometryUtilities.h>
40 #import <WebCore/TextIndicatorWindow.h>
41 #import <pal/spi/mac/DataDetectorsSPI.h>
42 #import <pal/spi/mac/LookupSPI.h>
43 #import <pal/spi/mac/NSMenuSPI.h>
44 #import <pal/spi/mac/NSPopoverSPI.h>
45 #import <pal/spi/mac/QuickLookMacSPI.h>
46 #import <wtf/SoftLinking.h>
47 #import <wtf/URL.h>
48
49 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, QuickLookUI)
50 SOFT_LINK_CLASS(QuickLookUI, QLPreviewMenuItem)
51
52 @interface WKImmediateActionController () <QLPreviewMenuItemDelegate>
53 @end
54
55 @interface WKAnimationController : NSObject <NSImmediateActionAnimationController>
56 @end
57
58 @implementation WKAnimationController
59 @end
60
61 @implementation WKImmediateActionController
62
63 - (instancetype)initWithPage:(WebKit::WebPageProxy&)page view:(NSView *)view viewImpl:(WebKit::WebViewImpl&)viewImpl recognizer:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
64 {
65     self = [super init];
66
67     if (!self)
68         return nil;
69
70     _page = &page;
71     _view = view;
72     _viewImpl = &viewImpl;
73     _type = kWKImmediateActionNone;
74     _immediateActionRecognizer = immediateActionRecognizer;
75     _hasActiveImmediateAction = NO;
76
77     return self;
78 }
79
80 - (void)willDestroyView:(NSView *)view
81 {
82     _page = nullptr;
83     _view = nil;
84     _viewImpl = nullptr;
85     _hitTestResultData = WebKit::WebHitTestResultData();
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         if (DataDetectorsLibrary())
122             [getDDActionsManagerClass() didUseActions];
123     }
124
125     _state = WebKit::ImmediateActionState::None;
126     _hitTestResultData = WebKit::WebHitTestResultData();
127     _contentPreventsDefault = NO;
128     _type = kWKImmediateActionNone;
129     _currentActionContext = nil;
130     _userData = nil;
131     _currentQLPreviewMenuItem = nil;
132     _hasActiveImmediateAction = NO;
133 }
134
135 - (void)didPerformImmediateActionHitTest:(const WebKit::WebHitTestResultData&)hitTestResult contentPreventsDefault:(BOOL)contentPreventsDefault userData:(API::Object*)userData
136 {
137     // If we've already given up on this gesture (either because it was canceled or the
138     // willBeginAnimation timeout expired), we shouldn't build a new animationController for it.
139     if (_state != WebKit::ImmediateActionState::Pending)
140         return;
141
142     // FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies.
143     _state = WebKit::ImmediateActionState::Ready;
144     _hitTestResultData = hitTestResult;
145     _contentPreventsDefault = contentPreventsDefault;
146     _userData = userData;
147
148     [self _updateImmediateActionItem];
149     [self _cancelImmediateActionIfNeeded];
150 }
151
152 - (void)dismissContentRelativeChildWindows
153 {
154     _page->setMaintainsInactiveSelection(false);
155     [_currentQLPreviewMenuItem close];
156 }
157
158 - (BOOL)hasActiveImmediateAction
159 {
160     return _hasActiveImmediateAction;
161 }
162
163 #pragma mark NSImmediateActionGestureRecognizerDelegate
164
165 - (void)immediateActionRecognizerWillPrepare:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
166 {
167     if (immediateActionRecognizer != _immediateActionRecognizer)
168         return;
169
170     _viewImpl->prepareForImmediateActionAnimation();
171
172     _viewImpl->dismissContentRelativeChildWindowsWithAnimation(true);
173
174     _page->setMaintainsInactiveSelection(true);
175
176     _state = WebKit::ImmediateActionState::Pending;
177     immediateActionRecognizer.animationController = nil;
178
179     _page->performImmediateActionHitTestAtLocation([immediateActionRecognizer locationInView:immediateActionRecognizer.view]);
180 }
181
182 - (void)immediateActionRecognizerWillBeginAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
183 {
184     if (immediateActionRecognizer != _immediateActionRecognizer)
185         return;
186
187     if (_state == WebKit::ImmediateActionState::None)
188         return;
189
190     _hasActiveImmediateAction = YES;
191
192     // FIXME: We need to be able to cancel this if the gesture recognizer is cancelled.
193     // FIXME: Connection can be null if the process is closed; we should clean up better in that case.
194     if (_state == WebKit::ImmediateActionState::Pending) {
195         if (auto* connection = _page->process().connection()) {
196             bool receivedReply = connection->waitForAndDispatchImmediately<Messages::WebPageProxy::DidPerformImmediateActionHitTest>(_page->pageID(), Seconds::fromMilliseconds(500));
197             if (!receivedReply)
198                 _state = WebKit::ImmediateActionState::TimedOut;
199         }
200     }
201
202     if (_state != WebKit::ImmediateActionState::Ready) {
203         [self _updateImmediateActionItem];
204         [self _cancelImmediateActionIfNeeded];
205     }
206
207     if (_currentActionContext) {
208         _hasActivatedActionContext = YES;
209         if (DataDetectorsLibrary()) {
210             if (![getDDActionsManagerClass() shouldUseActionsWithContext:_currentActionContext.get()])
211                 [self _cancelImmediateAction];
212         }
213     }
214 }
215
216 - (void)immediateActionRecognizerDidUpdateAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
217 {
218     if (immediateActionRecognizer != _immediateActionRecognizer)
219         return;
220
221     _page->immediateActionDidUpdate();
222     if (_contentPreventsDefault)
223         return;
224
225     _page->setTextIndicatorAnimationProgress([immediateActionRecognizer animationProgress]);
226 }
227
228 - (void)immediateActionRecognizerDidCancelAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
229 {
230     if (immediateActionRecognizer != _immediateActionRecognizer)
231         return;
232
233     _page->immediateActionDidCancel();
234
235     _viewImpl->cancelImmediateActionAnimation();
236
237     _page->setTextIndicatorAnimationProgress(0);
238     [self _clearImmediateActionState];
239     _page->setMaintainsInactiveSelection(false);
240 }
241
242 - (void)immediateActionRecognizerDidCompleteAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
243 {
244     if (immediateActionRecognizer != _immediateActionRecognizer)
245         return;
246
247     _page->immediateActionDidComplete();
248
249     _viewImpl->completeImmediateActionAnimation();
250
251     _page->setTextIndicatorAnimationProgress(1);
252 }
253
254 - (RefPtr<API::HitTestResult>)_webHitTestResult
255 {
256     RefPtr<API::HitTestResult> hitTestResult;
257     if (_state == WebKit::ImmediateActionState::Ready)
258         hitTestResult = API::HitTestResult::create(_hitTestResultData);
259     else
260         hitTestResult = _page->lastMouseMoveHitTestResult();
261
262     return hitTestResult;
263 }
264
265 #pragma mark Immediate actions
266
267 - (id <NSImmediateActionAnimationController>)_defaultAnimationController
268 {
269     if (_contentPreventsDefault)
270         return [[[WKAnimationController alloc] init] autorelease];
271
272     RefPtr<API::HitTestResult> hitTestResult = [self _webHitTestResult];
273
274     if (!hitTestResult)
275         return nil;
276
277     String absoluteLinkURL = hitTestResult->absoluteLinkURL();
278     if (!absoluteLinkURL.isEmpty()) {
279         if (WTF::protocolIs(absoluteLinkURL, "mailto")) {
280             _type = kWKImmediateActionMailtoLink;
281             return [self _animationControllerForDataDetectedLink];
282         }
283
284         if (WTF::protocolIs(absoluteLinkURL, "tel")) {
285             _type = kWKImmediateActionTelLink;
286             return [self _animationControllerForDataDetectedLink];
287         }
288
289         if (WTF::protocolIsInHTTPFamily(absoluteLinkURL)) {
290             _type = kWKImmediateActionLinkPreview;
291
292             QLPreviewMenuItem *item = [NSMenuItem standardQuickLookMenuItem];
293             item.previewStyle = QLPreviewStylePopover;
294             item.delegate = self;
295             _currentQLPreviewMenuItem = item;
296
297             if (auto textIndicator = _hitTestResultData.linkTextIndicator.get())
298                 _page->setTextIndicator(textIndicator->data());
299
300             return (id<NSImmediateActionAnimationController>)item;
301         }
302     }
303
304     if (hitTestResult->isTextNode() || hitTestResult->isOverTextInsideFormControlElement()) {
305         if (auto animationController = [self _animationControllerForDataDetectedText]) {
306             _type = kWKImmediateActionDataDetectedItem;
307             return animationController;
308         }
309
310         if (auto animationController = [self _animationControllerForText]) {
311             _type = kWKImmediateActionLookupText;
312             return animationController;
313         }
314     }
315
316     return nil;
317 }
318
319 - (void)_updateImmediateActionItem
320 {
321     _type = kWKImmediateActionNone;
322
323     id <NSImmediateActionAnimationController> defaultAnimationController = [self _defaultAnimationController];
324
325     if (_contentPreventsDefault) {
326         [_immediateActionRecognizer.get() setAnimationController:defaultAnimationController];
327         return;
328     }
329
330     RefPtr<API::HitTestResult> hitTestResult = [self _webHitTestResult];
331     id customClientAnimationController = _page->immediateActionAnimationControllerForHitTestResult(hitTestResult, _type, _userData);
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 _view;
348 }
349
350 - (id<QLPreviewItem>)menuItem:(NSMenuItem *)menuItem previewItemAtPoint:(NSPoint)point
351 {
352     if (!_view)
353         return nil;
354
355     RefPtr<API::HitTestResult> 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 (!_view)
372         return NSZeroRect;
373
374     RefPtr<API::HitTestResult> hitTestResult = [self _webHitTestResult];
375     return [_view convertRect:hitTestResult->elementBoundingBox() toView:nil];
376 }
377
378 - (NSSize)menuItem:(NSMenuItem *)menuItem maxSizeForPoint:(NSPoint)point
379 {
380     if (!_view)
381         return NSZeroSize;
382
383     NSSize screenSize = _view.window.screen.frame.size;
384     WebCore::FloatRect largestRect = WebCore::largestRectWithAspectRatioInsideRect(screenSize.width / screenSize.height, _view.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     if (!DataDetectorsLibrary())
393         return nil;
394
395     DDActionContext *actionContext = _hitTestResultData.detectedDataActionContext.get();
396     if (!actionContext)
397         return nil;
398
399     actionContext.altMode = YES;
400     actionContext.immediate = YES;
401     if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:actionContext.mainResult actionContext:actionContext])
402         return nil;
403
404     RefPtr<WebKit::WebPageProxy> page = _page;
405     WebCore::PageOverlay::PageOverlayID overlayID = _hitTestResultData.detectedDataOriginatingPageOverlay;
406     _currentActionContext = [actionContext contextForView:_view altMode:YES interactionStartedHandler:^() {
407         page->send(Messages::WebPage::DataDetectorsDidPresentUI(overlayID));
408     } interactionChangedHandler:^() {
409         if (_hitTestResultData.detectedDataTextIndicator)
410             page->setTextIndicator(_hitTestResultData.detectedDataTextIndicator->data());
411         page->send(Messages::WebPage::DataDetectorsDidChangeUI(overlayID));
412     } interactionStoppedHandler:^() {
413         page->send(Messages::WebPage::DataDetectorsDidHideUI(overlayID));
414         [self _clearImmediateActionState];
415     }];
416
417     [_currentActionContext setHighlightFrame:[_view.window convertRectToScreen:[_view convertRect:_hitTestResultData.detectedDataBoundingBox toView:nil]]];
418
419     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
420
421     if (menuItems.count != 1)
422         return nil;
423
424     return (id<NSImmediateActionAnimationController>)menuItems.lastObject;
425 }
426
427 - (id<NSImmediateActionAnimationController>)_animationControllerForDataDetectedLink
428 {
429     if (!DataDetectorsLibrary())
430         return nil;
431
432     RetainPtr<DDActionContext> actionContext = adoptNS([allocDDActionContextInstance() init]);
433
434     if (!actionContext)
435         return nil;
436
437     [actionContext setAltMode:YES];
438     [actionContext setImmediate:YES];
439
440     RefPtr<WebKit::WebPageProxy> page = _page;
441     _currentActionContext = [actionContext contextForView:_view altMode:YES interactionStartedHandler:^() {
442     } interactionChangedHandler:^() {
443         if (_hitTestResultData.linkTextIndicator)
444             page->setTextIndicator(_hitTestResultData.linkTextIndicator->data());
445     } interactionStoppedHandler:^() {
446         [self _clearImmediateActionState];
447     }];
448
449     [_currentActionContext setHighlightFrame:[_view.window convertRectToScreen:[_view convertRect:_hitTestResultData.elementBoundingBox toView:nil]]];
450
451     RefPtr<API::HitTestResult> hitTestResult = [self _webHitTestResult];
452     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForTargetURL:hitTestResult->absoluteLinkURL() actionContext:_currentActionContext.get()];
453
454     if (menuItems.count != 1)
455         return nil;
456
457     return (id<NSImmediateActionAnimationController>)menuItems.lastObject;
458 }
459
460 #pragma mark Text action
461
462 - (id<NSImmediateActionAnimationController>)_animationControllerForText
463 {
464     if (_state != WebKit::ImmediateActionState::Ready)
465         return nil;
466
467     WebCore::DictionaryPopupInfo dictionaryPopupInfo = _hitTestResultData.dictionaryPopupInfo;
468     if (!dictionaryPopupInfo.attributedString)
469         return nil;
470
471     _viewImpl->prepareForDictionaryLookup();
472
473     return WebCore::DictionaryLookup::animationControllerForPopup(dictionaryPopupInfo, _view, [self](WebCore::TextIndicator& textIndicator) {
474         _viewImpl->setTextIndicator(textIndicator, WebCore::TextIndicatorWindowLifetime::Permanent);
475     }, nullptr, [self]() {
476         _viewImpl->clearTextIndicatorWithAnimation(WebCore::TextIndicatorWindowDismissalAnimation::None);
477     });
478 }
479
480 @end
481
482 #endif // PLATFORM(MAC)