Unresponsive Web processes sometimes throw ObjC exceptions under didPerformActionMenu...
[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     // If we've already given up on this gesture (either because it was canceled or the
135     // willBeginAnimation timeout expired), we shouldn't build a new animationController for it.
136     if (_state != ImmediateActionState::Pending)
137         return;
138
139     // FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies.
140     _state = ImmediateActionState::Ready;
141     _hitTestResult = hitTestResult;
142     _userData = userData;
143
144     [self _updateImmediateActionItem];
145     [self _cancelImmediateActionIfNeeded];
146 }
147
148 - (void)dismissContentRelativeChildWindows
149 {
150     [_currentQLPreviewMenuItem close];
151 }
152
153 #pragma mark NSImmediateActionGestureRecognizerDelegate
154
155 - (void)immediateActionRecognizerWillPrepare:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
156 {
157     if (immediateActionRecognizer != _immediateActionRecognizer)
158         return;
159
160     _page->setMaintainsInactiveSelection(true);
161
162     [_wkView _dismissContentRelativeChildWindows];
163
164     _page->performActionMenuHitTestAtLocation([immediateActionRecognizer locationInView:immediateActionRecognizer.view], true);
165
166     _state = ImmediateActionState::Pending;
167     immediateActionRecognizer.animationController = nil;
168 }
169
170 - (void)immediateActionRecognizerWillBeginAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
171 {
172     if (immediateActionRecognizer != _immediateActionRecognizer)
173         return;
174
175     if (_state == ImmediateActionState::None)
176         return;
177
178     // FIXME: We need to be able to cancel this if the gesture recognizer is cancelled.
179     // FIXME: Connection can be null if the process is closed; we should clean up better in that case.
180     if (_state == ImmediateActionState::Pending) {
181         if (auto* connection = _page->process().connection()) {
182             bool receivedReply = connection->waitForAndDispatchImmediately<Messages::WebPageProxy::DidPerformActionMenuHitTest>(_page->pageID(), std::chrono::milliseconds(500));
183             if (!receivedReply)
184                 _state = ImmediateActionState::TimedOut;
185         }
186     }
187
188     if (_state != ImmediateActionState::Ready) {
189         [self _updateImmediateActionItem];
190         [self _cancelImmediateActionIfNeeded];
191     }
192 }
193
194 - (void)immediateActionRecognizerDidUpdateAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
195 {
196     if (immediateActionRecognizer != _immediateActionRecognizer)
197         return;
198
199     _page->setTextIndicatorAnimationProgress([immediateActionRecognizer animationProgress]);
200 }
201
202 - (void)immediateActionRecognizerDidCancelAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
203 {
204     if (immediateActionRecognizer != _immediateActionRecognizer)
205         return;
206
207     _page->setTextIndicatorAnimationProgress(0);
208     [self _clearImmediateActionState];
209     _page->setMaintainsInactiveSelection(false);
210 }
211
212 - (void)immediateActionRecognizerDidCompleteAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
213 {
214     if (immediateActionRecognizer != _immediateActionRecognizer)
215         return;
216
217     _page->setTextIndicatorAnimationProgress(1);
218     _page->setMaintainsInactiveSelection(false);
219 }
220
221 - (PassRefPtr<WebHitTestResult>)_webHitTestResult
222 {
223     RefPtr<WebHitTestResult> hitTestResult;
224     if (_state == ImmediateActionState::Ready)
225         hitTestResult = WebHitTestResult::create(_hitTestResult.hitTestResult);
226     else
227         hitTestResult = _page->lastMouseMoveHitTestResult();
228
229     return hitTestResult.release();
230 }
231
232 #pragma mark Immediate actions
233
234 - (id <NSImmediateActionAnimationController>)_defaultAnimationController
235 {
236     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
237
238     if (!hitTestResult)
239         return nil;
240
241     String absoluteLinkURL = hitTestResult->absoluteLinkURL();
242     if (!absoluteLinkURL.isEmpty() && WebCore::protocolIsInHTTPFamily(absoluteLinkURL)) {
243         _type = kWKImmediateActionLinkPreview;
244
245         if (TextIndicator *textIndicator = _hitTestResult.linkTextIndicator.get())
246             _page->setTextIndicator(textIndicator->data(), false);
247
248         RetainPtr<QLPreviewMenuItem> qlPreviewLinkItem = [NSMenuItem standardQuickLookMenuItem];
249         [qlPreviewLinkItem setPreviewStyle:QLPreviewStylePopover];
250         [qlPreviewLinkItem setDelegate:self];
251         _currentQLPreviewMenuItem = qlPreviewLinkItem.get();
252         return (id<NSImmediateActionAnimationController>)qlPreviewLinkItem.get();
253     }
254
255     if (hitTestResult->isTextNode() || hitTestResult->isOverTextInsideFormControlElement()) {
256         if (NSMenuItem *immediateActionItem = [self _menuItemForDataDetectedText]) {
257             _type = kWKImmediateActionDataDetectedItem;
258             return (id<NSImmediateActionAnimationController>)immediateActionItem;
259         }
260
261         if (id<NSImmediateActionAnimationController> textAnimationController = [self _animationControllerForText]) {
262             _type = kWKImmediateActionLookupText;
263             return textAnimationController;
264         }
265     }
266
267     return nil;
268 }
269
270 - (void)_updateImmediateActionItem
271 {
272     _type = kWKImmediateActionNone;
273
274     id <NSImmediateActionAnimationController> defaultAnimationController = [self _defaultAnimationController];
275
276     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
277     id customClientAnimationController = [_wkView _immediateActionAnimationControllerForHitTestResult:toAPI(hitTestResult.get()) withType:_type userData:toAPI(_userData.get())];
278     if (customClientAnimationController == [NSNull null]) {
279         [self _cancelImmediateAction];
280         return;
281     }
282     if (customClientAnimationController && [customClientAnimationController conformsToProtocol:@protocol(NSImmediateActionAnimationController)])
283         [_immediateActionRecognizer setAnimationController:(id <NSImmediateActionAnimationController>)customClientAnimationController];
284     else
285         [_immediateActionRecognizer setAnimationController:defaultAnimationController];
286 }
287
288 #pragma mark QLPreviewMenuItemDelegate implementation
289
290 - (NSView *)menuItem:(NSMenuItem *)menuItem viewAtScreenPoint:(NSPoint)screenPoint
291 {
292     return _wkView;
293 }
294
295 - (id<QLPreviewItem>)menuItem:(NSMenuItem *)menuItem previewItemAtPoint:(NSPoint)point
296 {
297     if (!_wkView)
298         return nil;
299
300     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
301     return [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()];
302 }
303
304 - (NSRectEdge)menuItem:(NSMenuItem *)menuItem preferredEdgeForPoint:(NSPoint)point
305 {
306     return NSMaxYEdge;
307 }
308
309 - (void)menuItemDidClose:(NSMenuItem *)menuItem
310 {
311     [self _clearImmediateActionState];
312 }
313
314 - (NSRect)menuItem:(NSMenuItem *)menuItem itemFrameForPoint:(NSPoint)point
315 {
316     if (!_wkView)
317         return NSZeroRect;
318
319     RefPtr<WebHitTestResult> hitTestResult = [self _webHitTestResult];
320     return [_wkView convertRect:hitTestResult->elementBoundingBox() toView:nil];
321 }
322
323 - (NSSize)menuItem:(NSMenuItem *)menuItem maxSizeForPoint:(NSPoint)point
324 {
325     if (!_wkView)
326         return NSZeroSize;
327
328     NSSize screenSize = _wkView.window.screen.frame.size;
329     FloatRect largestRect = largestRectWithAspectRatioInsideRect(screenSize.width / screenSize.height, _wkView.bounds);
330     return NSMakeSize(largestRect.width() * 0.75, largestRect.height() * 0.75);
331 }
332
333 #pragma mark Data Detectors actions
334
335 - (NSMenuItem *)_menuItemForDataDetectedText
336 {
337     DDActionContext *actionContext = _hitTestResult.actionContext.get();
338     if (!actionContext)
339         return nil;
340
341     actionContext.altMode = YES;
342     actionContext.immediate = YES;
343     if ([[getDDActionsManagerClass() sharedManager] respondsToSelector:@selector(hasActionsForResult:actionContext:)]) {
344         if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:actionContext.mainResult actionContext:actionContext])
345             return nil;
346     }
347
348     RefPtr<WebPageProxy> page = _page;
349     PageOverlay::PageOverlayID overlayID = _hitTestResult.detectedDataOriginatingPageOverlay;
350     _currentActionContext = [actionContext contextForView:_wkView altMode:YES interactionStartedHandler:^() {
351         page->send(Messages::WebPage::DataDetectorsDidPresentUI(overlayID));
352     } interactionChangedHandler:^() {
353         if (_hitTestResult.detectedDataTextIndicator)
354             page->setTextIndicator(_hitTestResult.detectedDataTextIndicator->data(), false);
355         page->send(Messages::WebPage::DataDetectorsDidChangeUI(overlayID));
356     } interactionStoppedHandler:^() {
357         page->send(Messages::WebPage::DataDetectorsDidHideUI(overlayID));
358         [self _clearImmediateActionState];
359     }];
360
361     [_currentActionContext setHighlightFrame:[_wkView.window convertRectToScreen:[_wkView convertRect:_hitTestResult.detectedDataBoundingBox toView:nil]]];
362
363     NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
364
365     if (menuItems.count != 1)
366         return nil;
367
368     return menuItems.lastObject;
369 }
370
371 #pragma mark Text action
372
373 - (id<NSImmediateActionAnimationController>)_animationControllerForText
374 {
375     if (_state != ImmediateActionState::Ready)
376         return nil;
377
378     if (!getLULookupDefinitionModuleClass())
379         return nil;
380
381     DictionaryPopupInfo dictionaryPopupInfo = _hitTestResult.dictionaryPopupInfo;
382     if (!dictionaryPopupInfo.attributedString.string)
383         return nil;
384
385     // Convert baseline to screen coordinates.
386     NSPoint textBaselineOrigin = dictionaryPopupInfo.origin;
387     textBaselineOrigin = [_wkView convertPoint:textBaselineOrigin toView:nil];
388     textBaselineOrigin = [_wkView.window convertRectToScreen:NSMakeRect(textBaselineOrigin.x, textBaselineOrigin.y, 0, 0)].origin;
389
390     RetainPtr<NSMutableDictionary> mutableOptions = adoptNS([(NSDictionary *)dictionaryPopupInfo.options.get() mutableCopy]);
391     if (canLoadLUTermOptionDisableSearchTermIndicator() && dictionaryPopupInfo.textIndicator.contentImage) {
392         [_wkView _setTextIndicator:TextIndicator::create(dictionaryPopupInfo.textIndicator) fadeOut:NO];
393         [mutableOptions setObject:@YES forKey:getLUTermOptionDisableSearchTermIndicator()];
394         return [getLULookupDefinitionModuleClass() lookupAnimationControllerForTerm:dictionaryPopupInfo.attributedString.string.get() atLocation:textBaselineOrigin options:mutableOptions.get()];
395     }
396     return [getLULookupDefinitionModuleClass() lookupAnimationControllerForTerm:dictionaryPopupInfo.attributedString.string.get() atLocation:textBaselineOrigin options:mutableOptions.get()];
397 }
398
399 @end
400
401 #endif // PLATFORM(MAC)