2 * Copyright (C) 2014 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
26 #import "WebImmediateActionController.h"
30 #import "DOMElementInternal.h"
31 #import "DOMNodeInternal.h"
32 #import "DOMRangeInternal.h"
33 #import "WebElementDictionary.h"
34 #import "WebFrameInternal.h"
35 #import "WebHTMLView.h"
36 #import "WebHTMLViewInternal.h"
37 #import "WebUIDelegatePrivate.h"
38 #import "WebViewInternal.h"
39 #import <WebCore/DataDetection.h>
40 #import <WebCore/DataDetectorsSPI.h>
41 #import <WebCore/DictionaryLookup.h>
42 #import <WebCore/Editor.h>
43 #import <WebCore/EventHandler.h>
44 #import <WebCore/FocusController.h>
45 #import <WebCore/Frame.h>
46 #import <WebCore/FrameView.h>
47 #import <WebCore/GeometryUtilities.h>
48 #import <WebCore/HTMLConverter.h>
49 #import <WebCore/LookupSPI.h>
50 #import <WebCore/NSMenuSPI.h>
51 #import <WebCore/Page.h>
52 #import <WebCore/QuickLookMacSPI.h>
53 #import <WebCore/RenderElement.h>
54 #import <WebCore/RenderObject.h>
55 #import <WebCore/RuntimeApplicationChecks.h>
56 #import <WebCore/SoftLinking.h>
57 #import <WebCore/TextIndicator.h>
58 #import <objc/objc-class.h>
61 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, QuickLookUI)
62 SOFT_LINK_CLASS(QuickLookUI, QLPreviewMenuItem)
64 @interface WebImmediateActionController () <QLPreviewMenuItemDelegate>
67 @interface WebAnimationController : NSObject <NSImmediateActionAnimationController>
70 @implementation WebAnimationController
73 using namespace WebCore;
75 @implementation WebImmediateActionController
77 - (instancetype)initWithWebView:(WebView *)webView recognizer:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
79 if (!(self = [super init]))
83 _type = WebImmediateActionNone;
84 _immediateActionRecognizer = immediateActionRecognizer;
93 id animationController = [_immediateActionRecognizer animationController];
94 if ([animationController isKindOfClass:NSClassFromString(@"QLPreviewMenuItem")]) {
95 QLPreviewMenuItem *menuItem = (QLPreviewMenuItem *)animationController;
96 menuItem.delegate = nil;
99 _immediateActionRecognizer = nil;
100 _currentActionContext = nil;
103 - (void)webView:(WebView *)webView didHandleScrollWheel:(NSEvent *)event
105 [_currentQLPreviewMenuItem close];
106 [self _clearImmediateActionState];
107 [_webView _clearTextIndicatorWithAnimation:TextIndicatorWindowDismissalAnimation::None];
110 - (NSImmediateActionGestureRecognizer *)immediateActionRecognizer
112 return _immediateActionRecognizer.get();
115 - (void)_cancelImmediateAction
117 // Reset the recognizer by turning it off and on again.
118 [_immediateActionRecognizer setEnabled:NO];
119 [_immediateActionRecognizer setEnabled:YES];
121 [self _clearImmediateActionState];
122 [_webView _clearTextIndicatorWithAnimation:TextIndicatorWindowDismissalAnimation::FadeOut];
125 - (void)_clearImmediateActionState
127 if (!DataDetectorsLibrary())
130 DDActionsManager *actionsManager = [getDDActionsManagerClass() sharedManager];
131 if ([actionsManager respondsToSelector:@selector(requestBubbleClosureUnanchorOnFailure:)])
132 [actionsManager requestBubbleClosureUnanchorOnFailure:YES];
134 if (_currentActionContext && _hasActivatedActionContext) {
135 _hasActivatedActionContext = NO;
136 [getDDActionsManagerClass() didUseActions];
139 _type = WebImmediateActionNone;
140 _currentActionContext = nil;
141 _currentQLPreviewMenuItem = nil;
142 _contentPreventsDefault = NO;
145 - (void)performHitTestAtPoint:(NSPoint)viewPoint
147 Frame* coreFrame = core([[[[_webView _selectedOrMainFrame] frameView] documentView] _frame]);
150 _hitTestResult = coreFrame->eventHandler().hitTestResultAtPoint(IntPoint(viewPoint));
151 coreFrame->eventHandler().setImmediateActionStage(ImmediateActionStage::PerformedHitTest);
153 if (Element* element = _hitTestResult.innerElement())
154 _contentPreventsDefault = element->dispatchMouseForceWillBegin();
157 #pragma mark NSImmediateActionGestureRecognizerDelegate
159 - (void)immediateActionRecognizerWillPrepare:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
164 NSView *documentView = [[[_webView _selectedOrMainFrame] frameView] documentView];
165 if (![documentView isKindOfClass:[WebHTMLView class]]) {
166 [self _cancelImmediateAction];
170 if (immediateActionRecognizer != _immediateActionRecognizer)
173 [_webView _setMaintainsInactiveSelection:YES];
175 NSPoint locationInDocumentView = [immediateActionRecognizer locationInView:documentView];
176 [self performHitTestAtPoint:locationInDocumentView];
177 [self _updateImmediateActionItem];
179 if (![_immediateActionRecognizer animationController]) {
180 // FIXME: We should be able to remove the dispatch_async when rdar://problem/19502927 is resolved.
181 dispatch_async(dispatch_get_main_queue(), ^{
182 [self _cancelImmediateAction];
187 - (void)immediateActionRecognizerWillBeginAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
189 if (!DataDetectorsLibrary())
192 if (immediateActionRecognizer != _immediateActionRecognizer)
195 if (_currentActionContext) {
196 _hasActivatedActionContext = YES;
197 if (![getDDActionsManagerClass() shouldUseActionsWithContext:_currentActionContext.get()])
198 [self _cancelImmediateAction];
202 - (void)immediateActionRecognizerDidUpdateAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
204 if (immediateActionRecognizer != _immediateActionRecognizer)
207 Frame* coreFrame = core([[[[_webView _selectedOrMainFrame] frameView] documentView] _frame]);
210 coreFrame->eventHandler().setImmediateActionStage(ImmediateActionStage::ActionUpdated);
211 if (_contentPreventsDefault)
214 [_webView _setTextIndicatorAnimationProgress:[immediateActionRecognizer animationProgress]];
217 - (void)immediateActionRecognizerDidCancelAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
219 if (immediateActionRecognizer != _immediateActionRecognizer)
222 NSView *documentView = [[[_webView _selectedOrMainFrame] frameView] documentView];
223 if (![documentView isKindOfClass:[WebHTMLView class]])
226 Frame* coreFrame = core([(WebHTMLView *)documentView _frame]);
228 ImmediateActionStage lastStage = coreFrame->eventHandler().immediateActionStage();
229 if (lastStage == ImmediateActionStage::ActionUpdated)
230 coreFrame->eventHandler().setImmediateActionStage(ImmediateActionStage::ActionCancelledAfterUpdate);
232 coreFrame->eventHandler().setImmediateActionStage(ImmediateActionStage::ActionCancelledWithoutUpdate);
235 [_webView _setTextIndicatorAnimationProgress:0];
236 [self _clearImmediateActionState];
237 [_webView _clearTextIndicatorWithAnimation:TextIndicatorWindowDismissalAnimation::None];
238 [_webView _setMaintainsInactiveSelection:NO];
241 - (void)immediateActionRecognizerDidCompleteAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer
243 if (immediateActionRecognizer != _immediateActionRecognizer)
246 Frame* coreFrame = core([[[[_webView _selectedOrMainFrame] frameView] documentView] _frame]);
249 coreFrame->eventHandler().setImmediateActionStage(ImmediateActionStage::ActionCompleted);
251 [_webView _setTextIndicatorAnimationProgress:1];
252 [_webView _setMaintainsInactiveSelection:NO];
255 #pragma mark Immediate actions
257 - (id <NSImmediateActionAnimationController>)_defaultAnimationController
259 if (_contentPreventsDefault)
260 return [[[WebAnimationController alloc] init] autorelease];
262 NSURL *url = _hitTestResult.absoluteLinkURL();
263 NSString *absoluteURLString = [url absoluteString];
264 if (url && _hitTestResult.URLElement()) {
265 if (protocolIs(absoluteURLString, "mailto")) {
266 _type = WebImmediateActionMailtoLink;
267 return [self _animationControllerForDataDetectedLink];
270 if (protocolIs(absoluteURLString, "tel")) {
271 _type = WebImmediateActionTelLink;
272 return [self _animationControllerForDataDetectedLink];
275 if (WebCore::protocolIsInHTTPFamily(absoluteURLString)) {
276 _type = WebImmediateActionLinkPreview;
278 RefPtr<Range> linkRange = rangeOfContents(*_hitTestResult.URLElement());
279 auto indicator = TextIndicator::createWithRange(*linkRange, TextIndicatorOptionUseBoundingRectAndPaintAllContentForComplexRanges, TextIndicatorPresentationTransition::FadeIn);
281 [_webView _setTextIndicator:*indicator withLifetime:TextIndicatorWindowLifetime::Permanent];
283 QLPreviewMenuItem *item = [NSMenuItem standardQuickLookMenuItem];
284 item.previewStyle = QLPreviewStylePopover;
285 item.delegate = self;
286 _currentQLPreviewMenuItem = item;
287 return (id <NSImmediateActionAnimationController>)item;
291 Node* node = _hitTestResult.innerNode();
292 if ((node && node->isTextNode()) || _hitTestResult.isOverTextInsideFormControlElement()) {
293 if (auto animationController = [self _animationControllerForDataDetectedText]) {
294 _type = WebImmediateActionDataDetectedItem;
295 return animationController;
298 if (auto animationController = [self _animationControllerForText]) {
299 _type = WebImmediateActionText;
300 return animationController;
307 - (void)_updateImmediateActionItem
309 _type = WebImmediateActionNone;
311 id <NSImmediateActionAnimationController> defaultAnimationController = [self _defaultAnimationController];
313 if (_contentPreventsDefault) {
314 [_immediateActionRecognizer setAnimationController:defaultAnimationController];
318 // Allow clients the opportunity to override the default immediate action.
319 id customClientAnimationController = nil;
320 if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:immediateActionAnimationControllerForHitTestResult:withType:)]) {
321 RetainPtr<WebElementDictionary> webHitTestResult = adoptNS([[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult]);
322 customClientAnimationController = [(id)[_webView UIDelegate] _webView:_webView immediateActionAnimationControllerForHitTestResult:webHitTestResult.get() withType:_type];
325 if (customClientAnimationController == [NSNull null]) {
326 [self _cancelImmediateAction];
331 // FIXME: We should not permanently disable this for iTunes. rdar://problem/19461358
332 if (MacApplication::isITunes()) {
333 [self _cancelImmediateAction];
338 if (customClientAnimationController && [customClientAnimationController conformsToProtocol:@protocol(NSImmediateActionAnimationController)])
339 [_immediateActionRecognizer setAnimationController:(id <NSImmediateActionAnimationController>)customClientAnimationController];
341 [_immediateActionRecognizer setAnimationController:defaultAnimationController];
344 #pragma mark QLPreviewMenuItemDelegate implementation
346 - (NSView *)menuItem:(NSMenuItem *)menuItem viewAtScreenPoint:(NSPoint)screenPoint
351 - (id<QLPreviewItem>)menuItem:(NSMenuItem *)menuItem previewItemAtPoint:(NSPoint)point
356 return _hitTestResult.absoluteLinkURL();
359 - (NSRectEdge)menuItem:(NSMenuItem *)menuItem preferredEdgeForPoint:(NSPoint)point
364 - (void)menuItemDidClose:(NSMenuItem *)menuItem
366 [self _clearImmediateActionState];
367 [_webView _clearTextIndicatorWithAnimation:TextIndicatorWindowDismissalAnimation::FadeOut];
370 static IntRect elementBoundingBoxInWindowCoordinatesFromNode(Node* node)
375 Frame* frame = node->document().frame();
379 FrameView* view = frame->view();
383 RenderObject* renderer = node->renderer();
387 return view->contentsToWindow(renderer->absoluteBoundingBoxRect());
390 - (NSRect)menuItem:(NSMenuItem *)menuItem itemFrameForPoint:(NSPoint)point
395 Node* node = _hitTestResult.innerNode();
399 return elementBoundingBoxInWindowCoordinatesFromNode(node);
402 - (NSSize)menuItem:(NSMenuItem *)menuItem maxSizeForPoint:(NSPoint)point
407 NSSize screenSize = _webView.window.screen.frame.size;
408 FloatRect largestRect = largestRectWithAspectRatioInsideRect(screenSize.width / screenSize.height, _webView.bounds);
409 return NSMakeSize(largestRect.width() * 0.75, largestRect.height() * 0.75);
412 #pragma mark Data Detectors actions
414 - (id <NSImmediateActionAnimationController>)_animationControllerForDataDetectedText
416 if (!DataDetectorsLibrary())
419 RefPtr<Range> detectedDataRange;
420 FloatRect detectedDataBoundingBox;
421 RetainPtr<DDActionContext> actionContext;
423 if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:actionContextForHitTestResult:range:)]) {
424 RetainPtr<WebElementDictionary> hitTestDictionary = adoptNS([[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult]);
426 DOMRange *customDataDetectorsRange;
427 actionContext = [(id)[_webView UIDelegate] _webView:_webView actionContextForHitTestResult:hitTestDictionary.get() range:&customDataDetectorsRange];
429 if (actionContext && customDataDetectorsRange)
430 detectedDataRange = core(customDataDetectorsRange);
433 // If the client didn't give us an action context, try to scan around the hit point.
434 if (!actionContext || !detectedDataRange)
435 actionContext = DataDetection::detectItemAroundHitTestResult(_hitTestResult, detectedDataBoundingBox, detectedDataRange);
437 if (!actionContext || !detectedDataRange)
440 [actionContext setAltMode:YES];
441 [actionContext setImmediate:YES];
442 if ([[getDDActionsManagerClass() sharedManager] respondsToSelector:@selector(hasActionsForResult:actionContext:)]) {
443 if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:[actionContext mainResult] actionContext:actionContext.get()])
447 auto indicator = TextIndicator::createWithRange(*detectedDataRange, TextIndicatorOptionDefault, TextIndicatorPresentationTransition::FadeIn);
449 _currentActionContext = [actionContext contextForView:_webView altMode:YES interactionStartedHandler:^() {
450 } interactionChangedHandler:^() {
452 [_webView _setTextIndicator:*indicator withLifetime:TextIndicatorWindowLifetime::Permanent];
453 } interactionStoppedHandler:^() {
454 [_webView _clearTextIndicatorWithAnimation:TextIndicatorWindowDismissalAnimation::FadeOut];
457 [_currentActionContext setHighlightFrame:[_webView.window convertRectToScreen:detectedDataBoundingBox]];
459 NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()];
460 if (menuItems.count != 1)
463 return menuItems.lastObject;
466 - (id <NSImmediateActionAnimationController>)_animationControllerForDataDetectedLink
468 if (!DataDetectorsLibrary())
471 RetainPtr<DDActionContext> actionContext = adoptNS([allocDDActionContextInstance() init]);
476 [actionContext setAltMode:YES];
477 [actionContext setImmediate:YES];
479 RefPtr<Range> linkRange = rangeOfContents(*_hitTestResult.URLElement());
482 auto indicator = TextIndicator::createWithRange(*linkRange, TextIndicatorOptionDefault, TextIndicatorPresentationTransition::FadeIn);
484 _currentActionContext = [actionContext contextForView:_webView altMode:YES interactionStartedHandler:^() {
485 } interactionChangedHandler:^() {
487 [_webView _setTextIndicator:*indicator withLifetime:TextIndicatorWindowLifetime::Permanent];
488 } interactionStoppedHandler:^() {
489 [_webView _clearTextIndicatorWithAnimation:TextIndicatorWindowDismissalAnimation::FadeOut];
492 [_currentActionContext setHighlightFrame:[_webView.window convertRectToScreen:elementBoundingBoxInWindowCoordinatesFromNode(_hitTestResult.URLElement())]];
494 NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForTargetURL:_hitTestResult.absoluteLinkURL() actionContext:_currentActionContext.get()];
495 if (menuItems.count != 1)
498 return menuItems.lastObject;
501 #pragma mark Text action
503 + (DictionaryPopupInfo)_dictionaryPopupInfoForRange:(Range&)range inFrame:(Frame*)frame withLookupOptions:(NSDictionary *)lookupOptions indicatorOptions:(TextIndicatorOptions)indicatorOptions transition:(TextIndicatorPresentationTransition)presentationTransition
505 Editor& editor = frame->editor();
506 editor.setIsGettingDictionaryPopupInfo(true);
508 // Dictionary API will accept a whitespace-only string and display UI as if it were real text,
509 // so bail out early to avoid that.
510 DictionaryPopupInfo popupInfo;
511 if (range.text().stripWhiteSpace().isEmpty()) {
512 editor.setIsGettingDictionaryPopupInfo(false);
516 RenderObject* renderer = range.startContainer().renderer();
517 const RenderStyle& style = renderer->style();
519 Vector<FloatQuad> quads;
520 range.absoluteTextQuads(quads);
521 if (quads.isEmpty()) {
522 editor.setIsGettingDictionaryPopupInfo(false);
526 IntRect rangeRect = frame->view()->contentsToWindow(quads[0].enclosingBoundingBox());
528 popupInfo.origin = NSMakePoint(rangeRect.x(), rangeRect.y() + (style.fontMetrics().descent() * frame->page()->pageScaleFactor()));
529 popupInfo.options = lookupOptions;
531 NSAttributedString *nsAttributedString = editingAttributedStringFromRange(range, IncludeImagesInAttributedString::No);
532 RetainPtr<NSMutableAttributedString> scaledNSAttributedString = adoptNS([[NSMutableAttributedString alloc] initWithString:[nsAttributedString string]]);
533 NSFontManager *fontManager = [NSFontManager sharedFontManager];
535 [nsAttributedString enumerateAttributesInRange:NSMakeRange(0, [nsAttributedString length]) options:0 usingBlock:^(NSDictionary *attributes, NSRange attributeRange, BOOL *stop) {
536 RetainPtr<NSMutableDictionary> scaledAttributes = adoptNS([attributes mutableCopy]);
538 NSFont *font = [scaledAttributes objectForKey:NSFontAttributeName];
540 font = [fontManager convertFont:font toSize:[font pointSize] * frame->page()->pageScaleFactor()];
541 [scaledAttributes setObject:font forKey:NSFontAttributeName];
544 [scaledNSAttributedString addAttributes:scaledAttributes.get() range:attributeRange];
547 popupInfo.attributedString = scaledNSAttributedString.get();
549 if (auto textIndicator = TextIndicator::createWithRange(range, indicatorOptions, presentationTransition))
550 popupInfo.textIndicator = textIndicator->data();
552 editor.setIsGettingDictionaryPopupInfo(false);
556 - (id<NSImmediateActionAnimationController>)_animationControllerForText
558 if (!getLULookupDefinitionModuleClass())
561 Node* node = _hitTestResult.innerNode();
565 Frame* frame = node->document().frame();
569 NSDictionary *options = nil;
570 RefPtr<Range> dictionaryRange = DictionaryLookup::rangeAtHitTestResult(_hitTestResult, &options);
571 if (!dictionaryRange)
574 DictionaryPopupInfo dictionaryPopupInfo = [WebImmediateActionController _dictionaryPopupInfoForRange:*dictionaryRange inFrame:frame withLookupOptions:options indicatorOptions:TextIndicatorOptionDefault transition: TextIndicatorPresentationTransition::FadeIn];
575 if (!dictionaryPopupInfo.attributedString)
578 return [_webView _animationControllerForDictionaryLookupPopupInfo:dictionaryPopupInfo];
583 #endif // PLATFORM(MAC)