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