4144d0acfba5a0797560a014029ae59644723e71
[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 "WKPagePreviewViewController.h"
33 #import "WKPreviewPopoverAnimationController.h"
34 #import "WKViewInternal.h"
35 #import "WebPageMessages.h"
36 #import "WebPageProxy.h"
37 #import "WebPageProxyMessages.h"
38 #import "WebProcessProxy.h"
39 #import <WebCore/DataDetectorsSPI.h>
40 #import <WebCore/LookupSPI.h>
41 #import <WebCore/NSMenuSPI.h>
42 #import <WebCore/NSPopoverSPI.h>
43 #import <WebCore/QuickLookMacSPI.h>
44 #import <WebCore/SoftLinking.h>
45 #import <WebCore/URL.h>
46
47 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, QuickLookUI)
48 SOFT_LINK_CLASS(QuickLookUI, QLPreviewMenuItem)
49 SOFT_LINK_CONSTANT_MAY_FAIL(Lookup, LUTermOptionDisableSearchTermIndicator, NSString *)
50
51 using namespace WebCore;
52 using namespace WebKit;
53
54 @interface WKImmediateActionController () <QLPreviewMenuItemDelegate>
55 @end
56
57 @implementation WKImmediateActionController
58
59 - (instancetype)initWithPage:(WebPageProxy&)page view:(WKView *)wkView recognizer:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
60 {
61     self = [super init];
62
63     if (!self)
64         return nil;
65
66     _page = &page;
67     _wkView = wkView;
68     _type = kWKImmediateActionNone;
69     _immediateActionRecognizer = immediateActionRecognizer;
70
71     return self;
72 }
73
74 - (void)willDestroyView:(WKView *)view
75 {
76     [_previewAnimationController close];
77
78     _page = nullptr;
79     _wkView = nil;
80     _hitTestResult = ActionMenuHitTestResult();
81     _immediateActionRecognizer = nil;
82     _currentActionContext = nil;
83 }
84
85 - (void)wkView:(WKView *)wkView willHandleMouseDown:(NSEvent *)event
86 {
87     [self _clearImmediateActionState];
88 }
89
90 - (void)_cancelImmediateAction
91 {
92     // Reset the recognizer by turning it off and on again.
93     _immediateActionRecognizer.enabled = NO;
94     _immediateActionRecognizer.enabled = YES;
95
96     [self _clearImmediateActionState];
97 }
98
99 - (void)_clearImmediateActionState
100 {
101     [self hidePreview];
102
103     _page->clearTextIndicator();
104
105     if (_currentActionContext && _hasActivatedActionContext) {
106         [getDDActionsManagerClass() didUseActions];
107         _hasActivatedActionContext = NO;
108     }
109
110     _state = ImmediateActionState::None;
111     _hitTestResult = ActionMenuHitTestResult();
112     _type = kWKImmediateActionNone;
113     _currentActionContext = nil;
114     _userData = nil;
115 }
116
117 - (void)didPerformActionMenuHitTest:(const ActionMenuHitTestResult&)hitTestResult userData:(API::Object*)userData
118 {
119     // FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies.
120     _state = ImmediateActionState::Ready;
121     _hitTestResult = hitTestResult;
122     _userData = userData;
123
124     [self _updateImmediateActionItem];
125 }
126
127 #pragma mark NSImmediateActionGestureRecognizerDelegate
128
129 - (void)immediateActionRecognizerWillPrepare:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
130 {
131     if (immediateActionRecognizer != _immediateActionRecognizer)
132         return;
133
134     _page->setMaintainsInactiveSelection(true);
135
136     [_wkView _dismissContentRelativeChildWindows];
137
138     _eventLocationInView = [immediateActionRecognizer locationInView:immediateActionRecognizer.view];
139     _page->performActionMenuHitTestAtLocation(_eventLocationInView, true);
140
141     _state = ImmediateActionState::Pending;
142     immediateActionRecognizer.animationController = nil;
143 }
144
145 - (void)immediateActionRecognizerWillBeginAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
146 {
147     if (immediateActionRecognizer != _immediateActionRecognizer)
148         return;
149
150     if (_state == ImmediateActionState::None)
151         return;
152
153     // FIXME: We need to be able to cancel this if the gesture recognizer is cancelled.
154     // FIXME: Connection can be null if the process is closed; we should clean up better in that case.
155     if (_state == ImmediateActionState::Pending) {
156         if (auto* connection = _page->process().connection()) {
157             bool receivedReply = connection->waitForAndDispatchImmediately<Messages::WebPageProxy::DidPerformActionMenuHitTest>(_page->pageID(), std::chrono::milliseconds(500));
158             if (!receivedReply)
159                 _state = ImmediateActionState::TimedOut;
160         }
161     }
162
163     if (_state != ImmediateActionState::Ready)
164         [self _updateImmediateActionItem];
165
166     if (!_immediateActionRecognizer.animationController) {
167         [self _cancelImmediateAction];
168         return;
169     }
170
171     if (_currentActionContext) {
172         _hasActivatedActionContext = YES;
173         if (![getDDActionsManagerClass() shouldUseActionsWithContext:_currentActionContext.get()])
174             [self _cancelImmediateAction];
175     }
176 }
177
178 - (void)immediateActionRecognizerDidUpdateAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
179 {
180     if (immediateActionRecognizer != _immediateActionRecognizer)
181         return;
182
183     _page->setTextIndicatorAnimationProgress([immediateActionRecognizer animationProgress]);
184 }
185
186 - (void)immediateActionRecognizerDidCancelAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
187 {
188     if (immediateActionRecognizer != _immediateActionRecognizer)
189         return;
190
191     _page->setTextIndicatorAnimationProgress(0);
192     [self _clearImmediateActionState];
193     _page->setMaintainsInactiveSelection(false);
194 }
195
196 - (void)immediateActionRecognizerDidCompleteAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
197 {
198     if (immediateActionRecognizer != _immediateActionRecognizer)
199         return;
200
201     _page->setTextIndicatorAnimationProgress(1);
202     _page->setMaintainsInactiveSelection(false);
203 }
204
205 - (PassRefPtr<WebHitTestResult>)_webHitTestResult
206 {
207     RefPtr<WebHitTestResult> hitTestResult;
208     if (_state == ImmediateActionState::Ready)
209         hitTestResult = WebHitTestResult::create(_hitTestResult.hitTestResult);
210     else
211         hitTestResult = _page->lastMouseMoveHitTestResult();
212
213     return hitTestResult.release();
214 }
215
216 #pragma mark Immediate actions
217
218 - (id <NSImmediateActionAnimationController>)_defaultAnimationController
219 {
220     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
221
222     if (!hitTestResult)
223         return nil;
224
225     String absoluteLinkURL = hitTestResult->absoluteLinkURL();
226     if (!absoluteLinkURL.isEmpty() && WebCore::protocolIsInHTTPFamily(absoluteLinkURL)) {
227         _type = kWKImmediateActionLinkPreview;
228
229         BOOL shouldUseStandardQuickLookPreview = [_wkView _shouldUseStandardQuickLookPreview] && [NSMenuItem respondsToSelector:@selector(standardQuickLookMenuItem)];
230         if (shouldUseStandardQuickLookPreview) {
231             RetainPtr<NSMenuItem> previewLinkItem;
232             RetainPtr<QLPreviewMenuItem> qlPreviewLinkItem;
233             if (shouldUseStandardQuickLookPreview) {
234                 qlPreviewLinkItem = [NSMenuItem standardQuickLookMenuItem];
235                 [qlPreviewLinkItem setPreviewStyle:QLPreviewStylePopover];
236                 [qlPreviewLinkItem setDelegate:self];
237             }
238             return (id<NSImmediateActionAnimationController>)qlPreviewLinkItem.get();
239         }
240
241         if (id<NSImmediateActionAnimationController> previewController = [self _animationControllerForCustomPreview])
242             return previewController;
243         return nil;
244
245     }
246
247     if (hitTestResult->isTextNode() || hitTestResult->isOverTextInsideFormControlElement()) {
248         if (NSMenuItem *immediateActionItem = [self _menuItemForDataDetectedText]) {
249             _type = kWKImmediateActionDataDetectedItem;
250             return (id<NSImmediateActionAnimationController>)immediateActionItem;
251         }
252
253         if (id<NSImmediateActionAnimationController> textAnimationController = [self _animationControllerForText]) {
254             _type = kWKImmediateActionLookupText;
255             return textAnimationController;
256         }
257     }
258
259     return nil;
260 }
261
262 - (void)_updateImmediateActionItem
263 {
264     _type = kWKImmediateActionNone;
265
266     id <NSImmediateActionAnimationController> defaultAnimationController = [self _defaultAnimationController];
267
268     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
269     id customClientAnimationController = [_wkView _immediateActionAnimationControllerForHitTestResult:toAPI(hitTestResult.get()) withType:_type userData:toAPI(_userData.get())];
270     if (customClientAnimationController == [NSNull null]) {
271         [self _cancelImmediateAction];
272         return;
273     }
274     if (customClientAnimationController && [customClientAnimationController conformsToProtocol:@protocol(NSImmediateActionAnimationController)])
275         _immediateActionRecognizer.animationController = (id <NSImmediateActionAnimationController>)customClientAnimationController;
276     else
277         _immediateActionRecognizer.animationController = defaultAnimationController;
278 }
279
280 #pragma mark Link Preview action
281
282 - (void)hidePreview
283 {
284     [_previewAnimationController close];
285 }
286
287 - (void)setPreviewTitle:(NSString *)previewTitle
288 {
289     [_previewAnimationController setPreviewTitle:previewTitle];
290 }
291
292 - (void)setPreviewLoading:(BOOL)loading
293 {
294     [_previewAnimationController setPreviewLoading:loading];
295 }
296
297 - (void)setPreviewOverrideImage:(NSImage *)image
298 {
299     [_previewAnimationController setPreviewOverrideImage:image];
300 }
301
302 - (id<NSImmediateActionAnimationController>)_animationControllerForCustomPreview
303 {
304     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
305     RetainPtr<NSURL> url = [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()];
306
307     if (_hitTestResult.linkTextIndicator)
308         _page->setTextIndicator(_hitTestResult.linkTextIndicator->data(), false);
309
310     _previewAnimationController = adoptNS([[WKPreviewPopoverAnimationController alloc] initWithURL:url.get() view:_wkView page:*_page originRect:hitTestResult->elementBoundingBox() eventLocationInView:_eventLocationInView]);
311
312     return _previewAnimationController.get();
313 }
314
315 #pragma mark QLPreviewMenuItemDelegate implementation
316
317 - (NSView *)menuItem:(NSMenuItem *)menuItem viewAtScreenPoint:(NSPoint)screenPoint
318 {
319     return _wkView;
320 }
321
322 - (id<QLPreviewItem>)menuItem:(NSMenuItem *)menuItem previewItemAtPoint:(NSPoint)point
323 {
324     if (!_wkView)
325         return nil;
326
327     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
328     return [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()];
329 }
330
331 - (NSRectEdge)menuItem:(NSMenuItem *)menuItem preferredEdgeForPoint:(NSPoint)point
332 {
333     return NSMaxYEdge;
334 }
335
336 #pragma mark Data Detectors actions
337
338 - (NSMenuItem *)_menuItemForDataDetectedText
339 {
340     DDActionContext *actionContext = _hitTestResult.actionContext.get();
341     if (!actionContext)
342         return nil;
343
344     actionContext.altMode = YES;
345     actionContext.immediate = YES;
346     if ([[getDDActionsManagerClass() sharedManager] respondsToSelector:@selector(hasActionsForResult:actionContext:)]) {
347         if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:actionContext.mainResult actionContext:actionContext])
348             return nil;
349     }
350
351     RefPtr<WebPageProxy> page = _page;
352     PageOverlay::PageOverlayID overlayID = _hitTestResult.detectedDataOriginatingPageOverlay;
353     _currentActionContext = [actionContext contextForView:_wkView altMode:YES interactionStartedHandler:^() {
354         page->send(Messages::WebPage::DataDetectorsDidPresentUI(overlayID));
355     } interactionChangedHandler:^() {
356         if (_hitTestResult.detectedDataTextIndicator)
357             page->setTextIndicator(_hitTestResult.detectedDataTextIndicator->data(), false);
358         page->send(Messages::WebPage::DataDetectorsDidChangeUI(overlayID));
359     } interactionStoppedHandler:^() {
360         page->send(Messages::WebPage::DataDetectorsDidHideUI(overlayID));
361         page->clearTextIndicator();
362     }];
363
364     [_currentActionContext setHighlightFrame:[_wkView.window convertRectToScreen:[_wkView convertRect:_hitTestResult.detectedDataBoundingBox toView:nil]]];
365
366     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
367
368     if (menuItems.count != 1)
369         return nil;
370
371     return menuItems.lastObject;
372 }
373
374 #pragma mark Text action
375
376 - (id<NSImmediateActionAnimationController>)_animationControllerForText
377 {
378     if (_state != ImmediateActionState::Ready)
379         return nil;
380
381     if (!getLULookupDefinitionModuleClass())
382         return nil;
383
384     DictionaryPopupInfo dictionaryPopupInfo = _hitTestResult.dictionaryPopupInfo;
385     if (!dictionaryPopupInfo.attributedString.string)
386         return nil;
387
388     // Convert baseline to screen coordinates.
389     NSPoint textBaselineOrigin = dictionaryPopupInfo.origin;
390     textBaselineOrigin = [_wkView convertPoint:textBaselineOrigin toView:nil];
391     textBaselineOrigin = [_wkView.window convertRectToScreen:NSMakeRect(textBaselineOrigin.x, textBaselineOrigin.y, 0, 0)].origin;
392
393     RetainPtr<NSMutableDictionary> mutableOptions = adoptNS([(NSDictionary *)dictionaryPopupInfo.options.get() mutableCopy]);
394     if (canLoadLUTermOptionDisableSearchTermIndicator() && dictionaryPopupInfo.textIndicator.contentImage) {
395         [_wkView _setTextIndicator:TextIndicator::create(dictionaryPopupInfo.textIndicator) fadeOut:NO];
396         [mutableOptions setObject:@YES forKey:getLUTermOptionDisableSearchTermIndicator()];
397         return [getLULookupDefinitionModuleClass() lookupAnimationControllerForTerm:dictionaryPopupInfo.attributedString.string.get() atLocation:textBaselineOrigin options:mutableOptions.get()];
398     }
399     return [getLULookupDefinitionModuleClass() lookupAnimationControllerForTerm:dictionaryPopupInfo.attributedString.string.get() atLocation:textBaselineOrigin options:mutableOptions.get()];
400 }
401
402 @end
403
404 #endif // PLATFORM(MAC)