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