WebContextMenuClient.mm   [plain text]


/*
 * Copyright (C) 2006, 2007, 2008, 2015 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. 
 * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE 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 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.
 */

#if !PLATFORM(IOS)

#import "WebContextMenuClient.h"

#import "WebDelegateImplementationCaching.h"
#import "WebElementDictionary.h"
#import "WebFrame.h"
#import "WebFrameInternal.h"
#import "WebHTMLView.h"
#import "WebHTMLViewInternal.h"
#import "WebKitVersionChecks.h"
#import "WebNSPasteboardExtras.h"
#import "WebSharingServicePickerController.h"
#import "WebUIDelegate.h"
#import "WebUIDelegatePrivate.h"
#import "WebView.h"
#import "WebViewInternal.h"
#import <WebCore/BitmapImage.h>
#import <WebCore/ContextMenu.h>
#import <WebCore/ContextMenuController.h>
#import <WebCore/Document.h>
#import <WebCore/Frame.h>
#import <WebCore/FrameView.h>
#import <WebCore/GraphicsContext.h>
#import <WebCore/ImageBuffer.h>
#import <WebCore/LocalizedStrings.h>
#import <WebCore/NSSharingServicePickerSPI.h>
#import <WebCore/Page.h>
#import <WebCore/RenderBox.h>
#import <WebCore/RenderObject.h>
#import <WebCore/SharedBuffer.h>
#import <WebCore/RuntimeApplicationChecks.h>
#import <WebCore/URL.h>
#import <WebKitLegacy/DOMPrivate.h>

using namespace WebCore;

@interface NSApplication ()
- (BOOL)isSpeaking;
- (void)speakString:(NSString *)string;
- (void)stopSpeaking:(id)sender;
@end

WebContextMenuClient::WebContextMenuClient(WebView *webView)
#if ENABLE(SERVICE_CONTROLS)
    : WebSharingServicePickerClient(webView)
#else
    : m_webView(webView)
#endif
{
}

WebContextMenuClient::~WebContextMenuClient()
{
#if ENABLE(SERVICE_CONTROLS)
    if (m_sharingServicePickerController)
        [m_sharingServicePickerController clear];
#endif
}

void WebContextMenuClient::contextMenuDestroyed()
{
    delete this;
}

void WebContextMenuClient::downloadURL(const URL& url)
{
    [m_webView _downloadURL:url];
}

void WebContextMenuClient::searchWithSpotlight()
{
    [m_webView _searchWithSpotlightFromMenu:nil];
}

void WebContextMenuClient::searchWithGoogle(const Frame*)
{
    [m_webView _searchWithGoogleFromMenu:nil];
}

void WebContextMenuClient::lookUpInDictionary(Frame* frame)
{
    WebHTMLView* htmlView = (WebHTMLView*)[[kit(frame) frameView] documentView];
    if(![htmlView isKindOfClass:[WebHTMLView class]])
        return;
    [htmlView _lookUpInDictionaryFromMenu:nil];
}

bool WebContextMenuClient::isSpeaking()
{
    return [NSApp isSpeaking];
}

void WebContextMenuClient::speak(const String& string)
{
    [NSApp speakString:[[(NSString*)string copy] autorelease]];
}

void WebContextMenuClient::stopSpeaking()
{
    [NSApp stopSpeaking:nil];
}

bool WebContextMenuClient::clientFloatRectForNode(Node& node, FloatRect& rect) const
{
    RenderObject* renderer = node.renderer();
    if (!renderer) {
        // This method shouldn't be called in cases where the controlled node hasn't rendered.
        ASSERT_NOT_REACHED();
        return false;
    }

    if (!is<RenderBox>(*renderer))
        return false;
    auto& renderBox = downcast<RenderBox>(*renderer);

    LayoutRect layoutRect = renderBox.clientBoxRect();
    FloatQuad floatQuad = renderBox.localToAbsoluteQuad(FloatQuad(layoutRect));
    rect = floatQuad.boundingBox();

    return true;
}

#if ENABLE(SERVICE_CONTROLS)
void WebContextMenuClient::sharingServicePickerWillBeDestroyed(WebSharingServicePickerController &)
{
    m_sharingServicePickerController = nil;
}

WebCore::FloatRect WebContextMenuClient::screenRectForCurrentSharingServicePickerItem(WebSharingServicePickerController &)
{
    Page* page = [m_webView page];
    if (!page)
        return NSZeroRect;

    Node* node = page->contextMenuController().context().hitTestResult().innerNode();
    if (!node)
        return NSZeroRect;

    FrameView* frameView = node->document().view();
    if (!frameView) {
        // This method shouldn't be called in cases where the controlled node isn't in a rendered view.
        ASSERT_NOT_REACHED();
        return NSZeroRect;
    }

    FloatRect rect;
    if (!clientFloatRectForNode(*node, rect))
        return NSZeroRect;

    // FIXME: https://webkit.org/b/132915
    // Ideally we'd like to convert the content rect to screen coordinates without the lossy float -> int conversion.
    // Creating a rounded int rect works well in practice, but might still lead to off-by-one-pixel problems in edge cases.
    IntRect intRect = roundedIntRect(rect);
    return frameView->contentsToScreen(intRect);
}

RetainPtr<NSImage> WebContextMenuClient::imageForCurrentSharingServicePickerItem(WebSharingServicePickerController &)
{
    Page* page = [m_webView page];
    if (!page)
        return nil;

    Node* node = page->contextMenuController().context().hitTestResult().innerNode();
    if (!node)
        return nil;

    FrameView* frameView = node->document().view();
    if (!frameView) {
        // This method shouldn't be called in cases where the controlled node isn't in a rendered view.
        ASSERT_NOT_REACHED();
        return nil;
    }

    FloatRect rect;
    if (!clientFloatRectForNode(*node, rect))
        return nil;

    // This is effectively a snapshot, and will be painted in an unaccelerated fashion in line with FrameSnapshotting.
    std::unique_ptr<ImageBuffer> buffer = ImageBuffer::create(rect.size(), Unaccelerated);
    if (!buffer)
        return nil;

    VisibleSelection oldSelection = frameView->frame().selection().selection();
    RefPtr<Range> range = Range::create(node->document(), Position(node, Position::PositionIsBeforeAnchor), Position(node, Position::PositionIsAfterAnchor));
    frameView->frame().selection().setSelection(VisibleSelection(*range), FrameSelection::DoNotSetFocus);

    PaintBehavior oldPaintBehavior = frameView->paintBehavior();
    frameView->setPaintBehavior(PaintBehaviorSelectionOnly);

    buffer->context().translate(-toFloatSize(rect.location()));
    frameView->paintContents(buffer->context(), roundedIntRect(rect));

    frameView->frame().selection().setSelection(oldSelection);
    frameView->setPaintBehavior(oldPaintBehavior);

    RefPtr<Image> image = ImageBuffer::sinkIntoImage(WTFMove(buffer));
    if (!image)
        return nil;

    return [[image->getNSImage() retain] autorelease];
}
#endif

NSMenu *WebContextMenuClient::contextMenuForEvent(NSEvent *event, NSView *view, bool& isServicesMenu)
{
    isServicesMenu = false;

    Page* page = [m_webView page];
    if (!page)
        return nil;

#if ENABLE(SERVICE_CONTROLS) && defined(__LP64__)
    if (Image* image = page->contextMenuController().context().controlledImage()) {
        ASSERT(page->contextMenuController().context().hitTestResult().innerNode());

        RetainPtr<NSItemProvider> itemProvider = adoptNS([[NSItemProvider alloc] initWithItem:image->getNSImage() typeIdentifier:@"public.image"]);

        bool isContentEditable = page->contextMenuController().context().hitTestResult().innerNode()->isContentEditable();
        m_sharingServicePickerController = adoptNS([[WebSharingServicePickerController alloc] initWithItems:@[ itemProvider.get() ] includeEditorServices:isContentEditable client:this style:NSSharingServicePickerStyleRollover]);

        isServicesMenu = true;
        return [m_sharingServicePickerController menu];
    }
#endif

    return [view menuForEvent:event];
}

void WebContextMenuClient::showContextMenu()
{
    Page* page = [m_webView page];
    if (!page)
        return;
    Frame* frame = page->contextMenuController().hitTestResult().innerNodeFrame();
    if (!frame)
        return;
    FrameView* frameView = frame->view();
    if (!frameView)
        return;

    NSView* view = frameView->documentView();
    IntPoint point = frameView->contentsToWindow(page->contextMenuController().hitTestResult().roundedPointInInnerNodeFrame());
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    NSEvent* event = [NSEvent mouseEventWithType:NSRightMouseDown location:point modifierFlags:0 timestamp:0 windowNumber:[[view window] windowNumber] context:0 eventNumber:0 clickCount:1 pressure:1];
#pragma clang diagnostic pop

    // Show the contextual menu for this event.
    bool isServicesMenu;
    if (NSMenu *menu = contextMenuForEvent(event, view, isServicesMenu)) {
        if (isServicesMenu)
            [menu popUpMenuPositioningItem:nil atLocation:[view convertPoint:point toView:nil] inView:view];
        else
            [NSMenu popUpContextMenu:menu withEvent:event forView:view];
    }
}

#endif // !PLATFORM(IOS)