/* * Copyright (C) 2014-2019 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "WebImmediateActionController.h" #if PLATFORM(MAC) #import "DOMElementInternal.h" #import "DOMNodeInternal.h" #import "DOMRangeInternal.h" #import "WebElementDictionary.h" #import "WebFrameInternal.h" #import "WebFrameView.h" #import "WebHTMLViewInternal.h" #import "WebUIDelegatePrivate.h" #import "WebViewInternal.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import @interface WebImmediateActionController () @end @interface WebAnimationController : NSObject @end @implementation WebAnimationController @end @implementation WebImmediateActionController - (instancetype)initWithWebView:(WebView *)webView recognizer:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer { if (!(self = [super init])) return nil; _webView = webView; _type = WebImmediateActionNone; _immediateActionRecognizer = immediateActionRecognizer; return self; } - (void)webViewClosed { _webView = nil; id animationController = [_immediateActionRecognizer animationController]; if (PAL::isQuickLookUIFrameworkAvailable() && [animationController isKindOfClass:PAL::getQLPreviewMenuItemClass()]) { QLPreviewMenuItem *menuItem = (QLPreviewMenuItem *)animationController; menuItem.delegate = nil; } _immediateActionRecognizer = nil; _currentActionContext = nil; } - (BOOL)isEnabled { return [_immediateActionRecognizer isEnabled]; } - (void)setEnabled:(BOOL)enabled { if (enabled == [_immediateActionRecognizer isEnabled]) return; [_immediateActionRecognizer setEnabled:enabled]; if (![_immediateActionRecognizer isEnabled]) [self _cancelImmediateAction]; } - (void)webView:(WebView *)webView didHandleScrollWheel:(NSEvent *)event { [_currentQLPreviewMenuItem close]; [self _clearImmediateActionState]; [_webView _clearTextIndicatorWithAnimation:WebCore::TextIndicatorDismissalAnimation::None]; } - (NSImmediateActionGestureRecognizer *)immediateActionRecognizer { return _immediateActionRecognizer.get(); } - (void)_cancelImmediateAction { // Reset the recognizer by turning it off and on again. if ([_immediateActionRecognizer isEnabled]) { [_immediateActionRecognizer setEnabled:NO]; [_immediateActionRecognizer setEnabled:YES]; } [self _clearImmediateActionState]; [_webView _clearTextIndicatorWithAnimation:WebCore::TextIndicatorDismissalAnimation::FadeOut]; } - (void)_clearImmediateActionState { if (!PAL::isDataDetectorsFrameworkAvailable()) return; DDActionsManager *actionsManager = [PAL::getDDActionsManagerClass() sharedManager]; [actionsManager requestBubbleClosureUnanchorOnFailure:YES]; if (_currentActionContext && _hasActivatedActionContext) { _hasActivatedActionContext = NO; [PAL::getDDActionsManagerClass() didUseActions]; } _type = WebImmediateActionNone; _currentActionContext = nil; _currentQLPreviewMenuItem = nil; _contentPreventsDefault = NO; } - (void)performHitTestAtPoint:(NSPoint)viewPoint { using namespace WebCore; auto* coreFrame = core([_webView _selectedOrMainFrame]); if (!coreFrame) return; constexpr OptionSet hitType { HitTestRequest::Type::ReadOnly, HitTestRequest::Type::Active, HitTestRequest::Type::DisallowUserAgentShadowContent, HitTestRequest::Type::AllowChildFrameContent }; _hitTestResult = coreFrame->eventHandler().hitTestResultAtPoint(WebCore::IntPoint(viewPoint), hitType); coreFrame->mainFrame().eventHandler().setImmediateActionStage(WebCore::ImmediateActionStage::PerformedHitTest); if (auto* element = _hitTestResult.targetElement()) _contentPreventsDefault = element->dispatchMouseForceWillBegin(); } #pragma mark NSImmediateActionGestureRecognizerDelegate - (void)immediateActionRecognizerWillPrepare:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer { if (!_webView) return; NSView *documentView = [[[_webView _selectedOrMainFrame] frameView] documentView]; if (![documentView isKindOfClass:[WebHTMLView class]]) { [self _cancelImmediateAction]; return; } if (immediateActionRecognizer != _immediateActionRecognizer) return; [_webView _setMaintainsInactiveSelection:YES]; NSPoint locationInDocumentView = [immediateActionRecognizer locationInView:documentView]; [self performHitTestAtPoint:locationInDocumentView]; [self _updateImmediateActionItem]; if (![_immediateActionRecognizer animationController]) { // FIXME: We should be able to remove the dispatch_async when rdar://problem/19502927 is resolved. RunLoop::main().dispatch([self, strongSelf = retainPtr(self)] { [self _cancelImmediateAction]; }); } } - (void)immediateActionRecognizerWillBeginAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer { if (!PAL::isDataDetectorsFrameworkAvailable()) return; if (immediateActionRecognizer != _immediateActionRecognizer) return; if (_currentActionContext) { _hasActivatedActionContext = YES; if (![PAL::getDDActionsManagerClass() shouldUseActionsWithContext:_currentActionContext.get()]) [self _cancelImmediateAction]; } } - (void)immediateActionRecognizerDidUpdateAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer { if (immediateActionRecognizer != _immediateActionRecognizer) return; if (WebCore::Frame* coreFrame = [_webView _mainCoreFrame]) coreFrame->eventHandler().setImmediateActionStage(WebCore::ImmediateActionStage::ActionUpdated); if (_contentPreventsDefault) return; [_webView _setTextIndicatorAnimationProgress:[immediateActionRecognizer animationProgress]]; } - (void)immediateActionRecognizerDidCancelAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer { if (immediateActionRecognizer != _immediateActionRecognizer) return; if (WebCore::Frame* coreFrame = [_webView _mainCoreFrame]) { WebCore::ImmediateActionStage lastStage = coreFrame->eventHandler().immediateActionStage(); if (lastStage == WebCore::ImmediateActionStage::ActionUpdated) coreFrame->eventHandler().setImmediateActionStage(WebCore::ImmediateActionStage::ActionCancelledAfterUpdate); else coreFrame->eventHandler().setImmediateActionStage(WebCore::ImmediateActionStage::ActionCancelledWithoutUpdate); } [_webView _setTextIndicatorAnimationProgress:0]; [self _clearImmediateActionState]; [_webView _clearTextIndicatorWithAnimation:WebCore::TextIndicatorDismissalAnimation::None]; [_webView _setMaintainsInactiveSelection:NO]; } - (void)immediateActionRecognizerDidCompleteAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer { if (immediateActionRecognizer != _immediateActionRecognizer) return; if (WebCore::Frame* coreFrame = [_webView _mainCoreFrame]) coreFrame->eventHandler().setImmediateActionStage(WebCore::ImmediateActionStage::ActionCompleted); [_webView _setTextIndicatorAnimationProgress:1]; [_webView _setMaintainsInactiveSelection:NO]; } #pragma mark Immediate actions - (id )_defaultAnimationController { if (_contentPreventsDefault) return adoptNS([[WebAnimationController alloc] init]).autorelease(); NSURL *url = _hitTestResult.absoluteLinkURL(); String absoluteURLString = [url absoluteString]; if (url && _hitTestResult.URLElement()) { if (WTF::protocolIs(absoluteURLString, "mailto"_s)) { _type = WebImmediateActionMailtoLink; return [self _animationControllerForDataDetectedLink]; } if (WTF::protocolIs(absoluteURLString, "tel"_s)) { _type = WebImmediateActionTelLink; return [self _animationControllerForDataDetectedLink]; } if (WTF::protocolIsInHTTPFamily(absoluteURLString)) { _type = WebImmediateActionLinkPreview; auto linkRange = makeRangeSelectingNodeContents(*_hitTestResult.URLElement()); auto indicator = WebCore::TextIndicator::createWithRange(linkRange, { WebCore::TextIndicatorOption::UseBoundingRectAndPaintAllContentForComplexRanges }, WebCore::TextIndicatorPresentationTransition::FadeIn); if (indicator) [_webView _setTextIndicator:*indicator withLifetime:WebCore::TextIndicatorLifetime::Permanent]; QLPreviewMenuItem *item = [NSMenuItem standardQuickLookMenuItem]; item.previewStyle = QLPreviewStylePopover; item.delegate = self; _currentQLPreviewMenuItem = item; return (id )item; } } WebCore::Node* node = _hitTestResult.innerNode(); if ((node && node->isTextNode()) || _hitTestResult.isOverTextInsideFormControlElement()) { if (auto animationController = [self _animationControllerForDataDetectedText]) { _type = WebImmediateActionDataDetectedItem; return animationController; } if (auto animationController = [self _animationControllerForText]) { _type = WebImmediateActionText; return animationController; } } return nil; } - (void)_updateImmediateActionItem { _type = WebImmediateActionNone; id defaultAnimationController = [self _defaultAnimationController]; if (_contentPreventsDefault) { [_immediateActionRecognizer setAnimationController:defaultAnimationController]; return; } // Allow clients the opportunity to override the default immediate action. id customClientAnimationController = nil; if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:immediateActionAnimationControllerForHitTestResult:withType:)]) { RetainPtr webHitTestResult = adoptNS([[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult]); customClientAnimationController = [(id)[_webView UIDelegate] _webView:_webView immediateActionAnimationControllerForHitTestResult:webHitTestResult.get() withType:_type]; } if (customClientAnimationController == [NSNull null]) { [self _cancelImmediateAction]; return; } #if PLATFORM(MAC) // FIXME: We should not permanently disable this for iTunes. rdar://problem/19461358 if (WebCore::MacApplication::isITunes()) { [self _cancelImmediateAction]; return; } #endif if (customClientAnimationController && [customClientAnimationController conformsToProtocol:@protocol(NSImmediateActionAnimationController)]) [_immediateActionRecognizer setAnimationController:(id )customClientAnimationController]; else [_immediateActionRecognizer setAnimationController:defaultAnimationController]; } #pragma mark QLPreviewMenuItemDelegate implementation - (NSView *)menuItem:(NSMenuItem *)menuItem viewAtScreenPoint:(NSPoint)screenPoint { return _webView; } - (id)menuItem:(NSMenuItem *)menuItem previewItemAtPoint:(NSPoint)point { if (!_webView) return nil; return _hitTestResult.absoluteLinkURL(); } - (NSRectEdge)menuItem:(NSMenuItem *)menuItem preferredEdgeForPoint:(NSPoint)point { return NSMaxYEdge; } - (void)menuItemDidClose:(NSMenuItem *)menuItem { [self _clearImmediateActionState]; [_webView _clearTextIndicatorWithAnimation:WebCore::TextIndicatorDismissalAnimation::FadeOut]; } static WebCore::IntRect elementBoundingBoxInWindowCoordinatesFromNode(WebCore::Node* node) { if (!node) return { }; WebCore::Frame* frame = node->document().frame(); if (!frame) return { }; WebCore::FrameView* view = frame->view(); if (!view) return { }; WebCore::RenderObject* renderer = node->renderer(); if (!renderer) return { }; return view->contentsToWindow(renderer->absoluteBoundingBoxRect()); } - (NSRect)menuItem:(NSMenuItem *)menuItem itemFrameForPoint:(NSPoint)point { if (!_webView) return NSZeroRect; WebCore::Node* node = _hitTestResult.innerNode(); if (!node) return NSZeroRect; return elementBoundingBoxInWindowCoordinatesFromNode(node); } - (NSSize)menuItem:(NSMenuItem *)menuItem maxSizeForPoint:(NSPoint)point { if (!_webView) return NSZeroSize; NSSize screenSize = _webView.window.screen.frame.size; WebCore::FloatRect largestRect = WebCore::largestRectWithAspectRatioInsideRect(screenSize.width / screenSize.height, _webView.bounds); return NSMakeSize(largestRect.width() * 0.75, largestRect.height() * 0.75); } #pragma mark Data Detectors actions - (id )_animationControllerForDataDetectedText { if (!PAL::isDataDetectorsFrameworkAvailable()) return nil; std::optional detectedItem; if ([[_webView UIDelegate] respondsToSelector:@selector(_webView:actionContextForHitTestResult:range:)]) { DOMRange *customDataDetectorsRange; auto actionContext = [(id)[_webView UIDelegate] _webView:_webView actionContextForHitTestResult:adoptNS([[WebElementDictionary alloc] initWithHitTestResult:_hitTestResult]).get() range:&customDataDetectorsRange]; if (actionContext && customDataDetectorsRange) { detectedItem = { { actionContext, { }, // FIXME: Seems like an empty rect isn't really OK. makeSimpleRange(*core(customDataDetectorsRange)) } }; } } // If the client didn't give us an action context, try to scan around the hit point. if (!detectedItem) detectedItem = WebCore::DataDetection::detectItemAroundHitTestResult(_hitTestResult); if (!detectedItem) return nil; [detectedItem->actionContext setAltMode:YES]; [detectedItem->actionContext setImmediate:YES]; if (![[PAL::getDDActionsManagerClass() sharedManager] hasActionsForResult:[detectedItem->actionContext mainResult] actionContext:detectedItem->actionContext.get()]) return nil; auto indicator = WebCore::TextIndicator::createWithRange(detectedItem->range, { }, WebCore::TextIndicatorPresentationTransition::FadeIn); _currentActionContext = [detectedItem->actionContext contextForView:_webView altMode:YES interactionStartedHandler:^() { } interactionChangedHandler:^() { if (indicator) [_webView _setTextIndicator:*indicator withLifetime:WebCore::TextIndicatorLifetime::Permanent]; } interactionStoppedHandler:^() { [_webView _clearTextIndicatorWithAnimation:WebCore::TextIndicatorDismissalAnimation::FadeOut]; }]; [_currentActionContext setHighlightFrame:[_webView.window convertRectToScreen:detectedItem->boundingBox]]; NSArray *menuItems = [[PAL::getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()]; if (menuItems.count != 1) return nil; return menuItems.lastObject; } - (id )_animationControllerForDataDetectedLink { if (!PAL::isDataDetectorsFrameworkAvailable()) return nil; auto actionContext = adoptNS([PAL::allocDDActionContextInstance() init]); if (!actionContext) return nil; [actionContext setAltMode:YES]; [actionContext setImmediate:YES]; auto linkRange = makeRangeSelectingNodeContents(*_hitTestResult.URLElement()); auto indicator = WebCore::TextIndicator::createWithRange(linkRange, { }, WebCore::TextIndicatorPresentationTransition::FadeIn); _currentActionContext = [actionContext contextForView:_webView altMode:YES interactionStartedHandler:^() { } interactionChangedHandler:^() { if (indicator) [_webView _setTextIndicator:*indicator withLifetime:WebCore::TextIndicatorLifetime::Permanent]; } interactionStoppedHandler:^() { [_webView _clearTextIndicatorWithAnimation:WebCore::TextIndicatorDismissalAnimation::FadeOut]; }]; [_currentActionContext setHighlightFrame:[_webView.window convertRectToScreen:elementBoundingBoxInWindowCoordinatesFromNode(_hitTestResult.URLElement())]]; NSArray *menuItems = [[PAL::getDDActionsManagerClass() sharedManager] menuItemsForTargetURL:_hitTestResult.absoluteLinkURL().string() actionContext:_currentActionContext.get()]; if (menuItems.count != 1) return nil; return menuItems.lastObject; } #pragma mark Text action + (WebCore::DictionaryPopupInfo)_dictionaryPopupInfoForRange:(const WebCore::SimpleRange&)range inFrame:(WebCore::Frame*)frame withLookupOptions:(NSDictionary *)lookupOptions indicatorOptions:(OptionSet)indicatorOptions transition:(WebCore::TextIndicatorPresentationTransition)presentationTransition { auto& editor = frame->editor(); editor.setIsGettingDictionaryPopupInfo(true); // Dictionary API will accept a whitespace-only string and display UI as if it were real text, // so bail out early to avoid that. WebCore::DictionaryPopupInfo popupInfo; if (plainText(range).find(isNotSpaceOrNewline) == notFound) { editor.setIsGettingDictionaryPopupInfo(false); return popupInfo; } auto style = range.start.container->renderStyle(); float scaledDescent = style ? style->metricsOfPrimaryFont().descent() * frame->page()->pageScaleFactor() : 0; auto quads = WebCore::RenderObject::absoluteTextQuads(range); if (quads.isEmpty()) { editor.setIsGettingDictionaryPopupInfo(false); return popupInfo; } auto rangeRect = frame->view()->contentsToWindow(quads[0].enclosingBoundingBox()); popupInfo.origin = NSMakePoint(rangeRect.x(), rangeRect.y() + scaledDescent); popupInfo.options = lookupOptions; auto attributedString = editingAttributedString(range, WebCore::IncludeImages::No).string; auto scaledAttributedString = adoptNS([[NSMutableAttributedString alloc] initWithString:[attributedString string]]); NSFontManager *fontManager = [NSFontManager sharedFontManager]; [attributedString enumerateAttributesInRange:NSMakeRange(0, [attributedString length]) options:0 usingBlock:^(NSDictionary *attributes, NSRange attributeRange, BOOL *stop) { RetainPtr scaledAttributes = adoptNS([attributes mutableCopy]); NSFont *font = [scaledAttributes objectForKey:NSFontAttributeName]; if (font) font = [fontManager convertFont:font toSize:font.pointSize * frame->page()->pageScaleFactor()]; if (font) [scaledAttributes setObject:font forKey:NSFontAttributeName]; [scaledAttributedString addAttributes:scaledAttributes.get() range:attributeRange]; }]; popupInfo.attributedString = scaledAttributedString.get(); if (auto textIndicator = WebCore::TextIndicator::createWithRange(range, indicatorOptions, presentationTransition)) popupInfo.textIndicator = textIndicator->data(); editor.setIsGettingDictionaryPopupInfo(false); return popupInfo; } - (id)_animationControllerForText { if (!PAL::getLULookupDefinitionModuleClass()) return nil; auto node = _hitTestResult.innerNode(); if (!node) return nil; auto frame = node->document().frame(); if (!frame) return nil; auto range = WebCore::DictionaryLookup::rangeAtHitTestResult(_hitTestResult); if (!range) return nil; auto [dictionaryRange, options] = WTFMove(*range); auto dictionaryPopupInfo = [WebImmediateActionController _dictionaryPopupInfoForRange:dictionaryRange inFrame:frame withLookupOptions:options indicatorOptions: { } transition: WebCore::TextIndicatorPresentationTransition::FadeIn]; if (!dictionaryPopupInfo.attributedString) return nil; return [_webView _animationControllerForDictionaryLookupPopupInfo:dictionaryPopupInfo]; } @end #endif // PLATFORM(MAC)