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