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