| /* |
| * Copyright (C) 2014-2016 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 "config.h" |
| #import "WKImmediateActionController.h" |
| |
| #if PLATFORM(MAC) |
| |
| #import "APIHitTestResult.h" |
| #import "WKNSURLExtras.h" |
| #import "WebPageMessages.h" |
| #import "WebPageProxy.h" |
| #import "WebPageProxyMessages.h" |
| #import "WebProcessProxy.h" |
| #import "WebViewImpl.h" |
| #import <WebCore/DictionaryLookup.h> |
| #import <WebCore/GeometryUtilities.h> |
| #import <WebCore/TextIndicatorWindow.h> |
| #import <pal/spi/mac/DataDetectorsSPI.h> |
| #import <pal/spi/mac/LookupSPI.h> |
| #import <pal/spi/mac/NSMenuSPI.h> |
| #import <pal/spi/mac/NSPopoverSPI.h> |
| #import <pal/spi/mac/QuickLookMacSPI.h> |
| #import <wtf/SoftLinking.h> |
| #import <wtf/URL.h> |
| |
| SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, QuickLookUI) |
| SOFT_LINK_CLASS(QuickLookUI, QLPreviewMenuItem) |
| |
| @interface WKImmediateActionController () <QLPreviewMenuItemDelegate> |
| @end |
| |
| @interface WKAnimationController : NSObject <NSImmediateActionAnimationController> |
| @end |
| |
| @implementation WKAnimationController |
| @end |
| |
| @implementation WKImmediateActionController |
| |
| - (instancetype)initWithPage:(WebKit::WebPageProxy&)page view:(NSView *)view viewImpl:(WebKit::WebViewImpl&)viewImpl recognizer:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer |
| { |
| self = [super init]; |
| |
| if (!self) |
| return nil; |
| |
| _page = &page; |
| _view = view; |
| _viewImpl = &viewImpl; |
| _type = kWKImmediateActionNone; |
| _immediateActionRecognizer = immediateActionRecognizer; |
| _hasActiveImmediateAction = NO; |
| |
| return self; |
| } |
| |
| - (void)willDestroyView:(NSView *)view |
| { |
| _page = nullptr; |
| _view = nil; |
| _viewImpl = nullptr; |
| _hitTestResultData = WebKit::WebHitTestResultData(); |
| _contentPreventsDefault = NO; |
| |
| id animationController = [_immediateActionRecognizer animationController]; |
| if ([animationController isKindOfClass:NSClassFromString(@"QLPreviewMenuItem")]) { |
| QLPreviewMenuItem *menuItem = (QLPreviewMenuItem *)animationController; |
| menuItem.delegate = nil; |
| } |
| |
| _immediateActionRecognizer = nil; |
| _currentActionContext = nil; |
| _hasActiveImmediateAction = NO; |
| } |
| |
| - (void)_cancelImmediateAction |
| { |
| // Reset the recognizer by turning it off and on again. |
| [_immediateActionRecognizer setEnabled:NO]; |
| [_immediateActionRecognizer setEnabled:YES]; |
| |
| [self _clearImmediateActionState]; |
| } |
| |
| - (void)_cancelImmediateActionIfNeeded |
| { |
| if (![_immediateActionRecognizer animationController]) |
| [self _cancelImmediateAction]; |
| } |
| |
| - (void)_clearImmediateActionState |
| { |
| if (_page) |
| _page->clearTextIndicator(); |
| |
| if (_currentActionContext && _hasActivatedActionContext) { |
| _hasActivatedActionContext = NO; |
| if (DataDetectorsLibrary()) |
| [getDDActionsManagerClass() didUseActions]; |
| } |
| |
| _state = WebKit::ImmediateActionState::None; |
| _hitTestResultData = WebKit::WebHitTestResultData(); |
| _contentPreventsDefault = NO; |
| _type = kWKImmediateActionNone; |
| _currentActionContext = nil; |
| _userData = nil; |
| _currentQLPreviewMenuItem = nil; |
| _hasActiveImmediateAction = NO; |
| } |
| |
| - (void)didPerformImmediateActionHitTest:(const WebKit::WebHitTestResultData&)hitTestResult contentPreventsDefault:(BOOL)contentPreventsDefault userData:(API::Object*)userData |
| { |
| // If we've already given up on this gesture (either because it was canceled or the |
| // willBeginAnimation timeout expired), we shouldn't build a new animationController for it. |
| if (_state != WebKit::ImmediateActionState::Pending) |
| return; |
| |
| // FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies. |
| _state = WebKit::ImmediateActionState::Ready; |
| _hitTestResultData = hitTestResult; |
| _contentPreventsDefault = contentPreventsDefault; |
| _userData = userData; |
| |
| [self _updateImmediateActionItem]; |
| [self _cancelImmediateActionIfNeeded]; |
| } |
| |
| - (void)dismissContentRelativeChildWindows |
| { |
| _page->setMaintainsInactiveSelection(false); |
| [_currentQLPreviewMenuItem close]; |
| } |
| |
| - (BOOL)hasActiveImmediateAction |
| { |
| return _hasActiveImmediateAction; |
| } |
| |
| #pragma mark NSImmediateActionGestureRecognizerDelegate |
| |
| - (void)immediateActionRecognizerWillPrepare:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer |
| { |
| if (immediateActionRecognizer != _immediateActionRecognizer) |
| return; |
| |
| _viewImpl->prepareForImmediateActionAnimation(); |
| |
| _viewImpl->dismissContentRelativeChildWindowsWithAnimation(true); |
| |
| _page->setMaintainsInactiveSelection(true); |
| |
| _state = WebKit::ImmediateActionState::Pending; |
| immediateActionRecognizer.animationController = nil; |
| |
| _page->performImmediateActionHitTestAtLocation([immediateActionRecognizer locationInView:immediateActionRecognizer.view]); |
| } |
| |
| - (void)immediateActionRecognizerWillBeginAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer |
| { |
| if (immediateActionRecognizer != _immediateActionRecognizer) |
| return; |
| |
| if (_state == WebKit::ImmediateActionState::None) |
| return; |
| |
| _hasActiveImmediateAction = YES; |
| |
| // FIXME: We need to be able to cancel this if the gesture recognizer is cancelled. |
| // FIXME: Connection can be null if the process is closed; we should clean up better in that case. |
| if (_state == WebKit::ImmediateActionState::Pending) { |
| if (auto* connection = _page->process().connection()) { |
| bool receivedReply = connection->waitForAndDispatchImmediately<Messages::WebPageProxy::DidPerformImmediateActionHitTest>(_page->webPageID(), Seconds::fromMilliseconds(500)); |
| if (!receivedReply) |
| _state = WebKit::ImmediateActionState::TimedOut; |
| } |
| } |
| |
| if (_state != WebKit::ImmediateActionState::Ready) { |
| [self _updateImmediateActionItem]; |
| [self _cancelImmediateActionIfNeeded]; |
| } |
| |
| if (_currentActionContext) { |
| _hasActivatedActionContext = YES; |
| if (DataDetectorsLibrary()) { |
| if (![getDDActionsManagerClass() shouldUseActionsWithContext:_currentActionContext.get()]) |
| [self _cancelImmediateAction]; |
| } |
| } |
| } |
| |
| - (void)immediateActionRecognizerDidUpdateAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer |
| { |
| if (immediateActionRecognizer != _immediateActionRecognizer) |
| return; |
| |
| _page->immediateActionDidUpdate(); |
| if (_contentPreventsDefault) |
| return; |
| |
| _page->setTextIndicatorAnimationProgress([immediateActionRecognizer animationProgress]); |
| } |
| |
| - (void)immediateActionRecognizerDidCancelAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer |
| { |
| if (immediateActionRecognizer != _immediateActionRecognizer) |
| return; |
| |
| _page->immediateActionDidCancel(); |
| |
| _viewImpl->cancelImmediateActionAnimation(); |
| |
| _page->setTextIndicatorAnimationProgress(0); |
| [self _clearImmediateActionState]; |
| _page->setMaintainsInactiveSelection(false); |
| } |
| |
| - (void)immediateActionRecognizerDidCompleteAnimation:(NSImmediateActionGestureRecognizer *)immediateActionRecognizer |
| { |
| if (immediateActionRecognizer != _immediateActionRecognizer) |
| return; |
| |
| _page->immediateActionDidComplete(); |
| |
| _viewImpl->completeImmediateActionAnimation(); |
| |
| _page->setTextIndicatorAnimationProgress(1); |
| } |
| |
| - (RefPtr<API::HitTestResult>)_webHitTestResult |
| { |
| RefPtr<API::HitTestResult> hitTestResult; |
| if (_state == WebKit::ImmediateActionState::Ready) |
| hitTestResult = API::HitTestResult::create(_hitTestResultData); |
| else |
| hitTestResult = _page->lastMouseMoveHitTestResult(); |
| |
| return hitTestResult; |
| } |
| |
| #pragma mark Immediate actions |
| |
| - (id <NSImmediateActionAnimationController>)_defaultAnimationController |
| { |
| if (_contentPreventsDefault) |
| return [[[WKAnimationController alloc] init] autorelease]; |
| |
| RefPtr<API::HitTestResult> hitTestResult = [self _webHitTestResult]; |
| |
| if (!hitTestResult) |
| return nil; |
| |
| String absoluteLinkURL = hitTestResult->absoluteLinkURL(); |
| if (!absoluteLinkURL.isEmpty()) { |
| if (WTF::protocolIs(absoluteLinkURL, "mailto")) { |
| _type = kWKImmediateActionMailtoLink; |
| return [self _animationControllerForDataDetectedLink]; |
| } |
| |
| if (WTF::protocolIs(absoluteLinkURL, "tel")) { |
| _type = kWKImmediateActionTelLink; |
| return [self _animationControllerForDataDetectedLink]; |
| } |
| |
| if (WTF::protocolIsInHTTPFamily(absoluteLinkURL)) { |
| _type = kWKImmediateActionLinkPreview; |
| |
| QLPreviewMenuItem *item = [NSMenuItem standardQuickLookMenuItem]; |
| item.previewStyle = QLPreviewStylePopover; |
| item.delegate = self; |
| _currentQLPreviewMenuItem = item; |
| |
| if (auto textIndicator = _hitTestResultData.linkTextIndicator.get()) |
| _page->setTextIndicator(textIndicator->data()); |
| |
| return (id<NSImmediateActionAnimationController>)item; |
| } |
| } |
| |
| if (hitTestResult->isTextNode() || hitTestResult->isOverTextInsideFormControlElement()) { |
| if (auto animationController = [self _animationControllerForDataDetectedText]) { |
| _type = kWKImmediateActionDataDetectedItem; |
| return animationController; |
| } |
| |
| if (auto animationController = [self _animationControllerForText]) { |
| _type = kWKImmediateActionLookupText; |
| return animationController; |
| } |
| } |
| |
| return nil; |
| } |
| |
| - (void)_updateImmediateActionItem |
| { |
| _type = kWKImmediateActionNone; |
| |
| id <NSImmediateActionAnimationController> defaultAnimationController = [self _defaultAnimationController]; |
| |
| if (_contentPreventsDefault) { |
| [_immediateActionRecognizer.get() setAnimationController:defaultAnimationController]; |
| return; |
| } |
| |
| RefPtr<API::HitTestResult> hitTestResult = [self _webHitTestResult]; |
| if (!hitTestResult) { |
| [self _cancelImmediateAction]; |
| return; |
| } |
| |
| id customClientAnimationController = _page->immediateActionAnimationControllerForHitTestResult(hitTestResult, _type, _userData); |
| if (customClientAnimationController == [NSNull null]) { |
| [self _cancelImmediateAction]; |
| return; |
| } |
| |
| if (customClientAnimationController && [customClientAnimationController conformsToProtocol:@protocol(NSImmediateActionAnimationController)]) |
| [_immediateActionRecognizer setAnimationController:(id <NSImmediateActionAnimationController>)customClientAnimationController]; |
| else |
| [_immediateActionRecognizer setAnimationController:defaultAnimationController]; |
| } |
| |
| #pragma mark QLPreviewMenuItemDelegate implementation |
| |
| - (NSView *)menuItem:(NSMenuItem *)menuItem viewAtScreenPoint:(NSPoint)screenPoint |
| { |
| return _view; |
| } |
| |
| - (id<QLPreviewItem>)menuItem:(NSMenuItem *)menuItem previewItemAtPoint:(NSPoint)point |
| { |
| if (!_view) |
| return nil; |
| |
| RefPtr<API::HitTestResult> hitTestResult = [self _webHitTestResult]; |
| if (!hitTestResult) |
| return nil; |
| |
| return [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()]; |
| } |
| |
| - (NSRectEdge)menuItem:(NSMenuItem *)menuItem preferredEdgeForPoint:(NSPoint)point |
| { |
| return NSMaxYEdge; |
| } |
| |
| - (void)menuItemDidClose:(NSMenuItem *)menuItem |
| { |
| [self _clearImmediateActionState]; |
| } |
| |
| - (NSRect)menuItem:(NSMenuItem *)menuItem itemFrameForPoint:(NSPoint)point |
| { |
| if (!_view) |
| return NSZeroRect; |
| |
| RefPtr<API::HitTestResult> hitTestResult = [self _webHitTestResult]; |
| if (!hitTestResult) |
| return NSZeroRect; |
| |
| return [_view convertRect:hitTestResult->elementBoundingBox() toView:nil]; |
| } |
| |
| - (NSSize)menuItem:(NSMenuItem *)menuItem maxSizeForPoint:(NSPoint)point |
| { |
| if (!_view) |
| return NSZeroSize; |
| |
| NSSize screenSize = _view.window.screen.frame.size; |
| WebCore::FloatRect largestRect = WebCore::largestRectWithAspectRatioInsideRect(screenSize.width / screenSize.height, _view.bounds); |
| return NSMakeSize(largestRect.width() * 0.75, largestRect.height() * 0.75); |
| } |
| |
| #pragma mark Data Detectors actions |
| |
| - (id<NSImmediateActionAnimationController>)_animationControllerForDataDetectedText |
| { |
| if (!DataDetectorsLibrary()) |
| return nil; |
| |
| DDActionContext *actionContext = _hitTestResultData.detectedDataActionContext.get(); |
| if (!actionContext) |
| return nil; |
| |
| actionContext.altMode = YES; |
| actionContext.immediate = YES; |
| if (![[getDDActionsManagerClass() sharedManager] hasActionsForResult:actionContext.mainResult actionContext:actionContext]) |
| return nil; |
| |
| RefPtr<WebKit::WebPageProxy> page = _page; |
| WebCore::PageOverlay::PageOverlayID overlayID = _hitTestResultData.detectedDataOriginatingPageOverlay; |
| _currentActionContext = [actionContext contextForView:_view altMode:YES interactionStartedHandler:^() { |
| page->send(Messages::WebPage::DataDetectorsDidPresentUI(overlayID)); |
| } interactionChangedHandler:^() { |
| if (_hitTestResultData.detectedDataTextIndicator) |
| page->setTextIndicator(_hitTestResultData.detectedDataTextIndicator->data()); |
| page->send(Messages::WebPage::DataDetectorsDidChangeUI(overlayID)); |
| } interactionStoppedHandler:^() { |
| page->send(Messages::WebPage::DataDetectorsDidHideUI(overlayID)); |
| [self _clearImmediateActionState]; |
| }]; |
| |
| [_currentActionContext setHighlightFrame:[_view.window convertRectToScreen:[_view convertRect:_hitTestResultData.detectedDataBoundingBox toView:nil]]]; |
| |
| NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_currentActionContext mainResult] actionContext:_currentActionContext.get()]; |
| |
| if (menuItems.count != 1) |
| return nil; |
| |
| return (id<NSImmediateActionAnimationController>)menuItems.lastObject; |
| } |
| |
| - (id<NSImmediateActionAnimationController>)_animationControllerForDataDetectedLink |
| { |
| if (!DataDetectorsLibrary()) |
| return nil; |
| |
| RetainPtr<DDActionContext> actionContext = adoptNS([allocDDActionContextInstance() init]); |
| |
| if (!actionContext) |
| return nil; |
| |
| [actionContext setAltMode:YES]; |
| [actionContext setImmediate:YES]; |
| |
| RefPtr<WebKit::WebPageProxy> page = _page; |
| _currentActionContext = [actionContext contextForView:_view altMode:YES interactionStartedHandler:^() { |
| } interactionChangedHandler:^() { |
| if (_hitTestResultData.linkTextIndicator) |
| page->setTextIndicator(_hitTestResultData.linkTextIndicator->data()); |
| } interactionStoppedHandler:^() { |
| [self _clearImmediateActionState]; |
| }]; |
| |
| [_currentActionContext setHighlightFrame:[_view.window convertRectToScreen:[_view convertRect:_hitTestResultData.elementBoundingBox toView:nil]]]; |
| |
| RefPtr<API::HitTestResult> hitTestResult = [self _webHitTestResult]; |
| if (!hitTestResult) |
| return nil; |
| |
| NSArray *menuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForTargetURL:hitTestResult->absoluteLinkURL() actionContext:_currentActionContext.get()]; |
| |
| if (menuItems.count != 1) |
| return nil; |
| |
| return (id<NSImmediateActionAnimationController>)menuItems.lastObject; |
| } |
| |
| #pragma mark Text action |
| |
| - (id<NSImmediateActionAnimationController>)_animationControllerForText |
| { |
| if (_state != WebKit::ImmediateActionState::Ready) |
| return nil; |
| |
| WebCore::DictionaryPopupInfo dictionaryPopupInfo = _hitTestResultData.dictionaryPopupInfo; |
| if (!dictionaryPopupInfo.attributedString) |
| return nil; |
| |
| _viewImpl->prepareForDictionaryLookup(); |
| |
| return WebCore::DictionaryLookup::animationControllerForPopup(dictionaryPopupInfo, _view, [self](WebCore::TextIndicator& textIndicator) { |
| _viewImpl->setTextIndicator(textIndicator, WebCore::TextIndicatorWindowLifetime::Permanent); |
| }, nullptr, [self]() { |
| _viewImpl->clearTextIndicatorWithAnimation(WebCore::TextIndicatorWindowDismissalAnimation::None); |
| }); |
| } |
| |
| @end |
| |
| #endif // PLATFORM(MAC) |