TouchEventHandler.cpp   [plain text]


/*
 * Copyright (C) 2010, 2011, 2012, 2013 Research In Motion Limited. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "config.h"
#include "TouchEventHandler.h"

#include "BlackBerryPlatformSystemSound.h"
#include "DOMSupport.h"
#include "Document.h"
#include "DocumentMarkerController.h"
#include "FocusController.h"
#include "Frame.h"
#include "FrameView.h"
#include "HTMLAnchorElement.h"
#include "HTMLAreaElement.h"
#include "HTMLImageElement.h"
#include "HTMLInputElement.h"
#include "HTMLNames.h"
#include "HTMLPlugInElement.h"
#include "InRegionScroller_p.h"
#include "InputHandler.h"
#include "IntRect.h"
#include "IntSize.h"
#include "Node.h"
#include "Page.h"
#include "PlatformMouseEvent.h"
#include "PlatformTouchEvent.h"
#include "RenderLayer.h"
#include "RenderTheme.h"
#include "RenderView.h"
#include "SelectionHandler.h"
#include "WebKitThreadViewportAccessor.h"
#include "WebPage_p.h"
#include "WebTapHighlight.h"

#include <BlackBerryPlatformViewportAccessor.h>
#include <wtf/MathExtras.h>

using namespace WebCore;
using namespace WTF;

namespace BlackBerry {
namespace WebKit {

TouchEventHandler::TouchEventHandler(WebPagePrivate* webpage)
    : m_webPage(webpage)
    , m_existingTouchMode(ProcessedTouchEvents)
    , m_shouldRequestSpellCheckOptions(false)
{
}

TouchEventHandler::~TouchEventHandler()
{
}

void TouchEventHandler::doFatFingers(const Platform::TouchPoint& point)
{
    m_lastScreenPoint = point.screenPosition();
    m_lastFatFingersResult.reset(); // Theoretically this shouldn't be required. Keep it just in case states get mangled.
    m_webPage->postponeDocumentStyleRecalc();
    m_lastFatFingersResult = FatFingers(m_webPage, point.documentContentPosition(), FatFingers::ClickableElement).findBestPoint();
    m_webPage->resumeDocumentStyleRecalc();
}

void TouchEventHandler::sendClickAtFatFingersPoint(unsigned modifiers)
{
    bool shiftActive = modifiers & KEYMOD_SHIFT;
    bool altActive = modifiers & KEYMOD_ALT;
    bool ctrlActive = modifiers & KEYMOD_CTRL;

    handleFatFingerPressed(shiftActive, altActive, ctrlActive);

    const Platform::ViewportAccessor* viewportAccessor = m_webPage->m_webkitThreadViewportAccessor;
    IntPoint documentViewportAdjustedPosition = viewportAccessor->documentViewportFromContents(m_lastFatFingersResult.adjustedPosition());
    PlatformMouseEvent mouseRelease(documentViewportAdjustedPosition, m_lastScreenPoint, PlatformEvent::MouseReleased, 1, LeftButton, shiftActive, ctrlActive, altActive, TouchScreen);

    m_webPage->handleMouseEvent(mouseRelease);
}

void TouchEventHandler::handleTouchHold()
{
    // Clear and reset focus if the user touch-holds on a different frame.
    // Special case for highlighting text on a frame not currently under focus [PR 285211].
    Node* nodeUnderFatFinger = m_lastFatFingersResult.node();
    if (nodeUnderFatFinger && nodeUnderFatFinger->document()->frame() != m_webPage->focusedOrMainFrame()) {
        m_webPage->clearFocusNode();
        m_webPage->m_selectionHandler->cancelSelection();
        m_webPage->m_page->focusController()->setFocusedFrame(nodeUnderFatFinger->document()->frame());
    }
}

void TouchEventHandler::handleTouchPoint(const Platform::TouchPoint& point, unsigned modifiers)
{
    // Enable input mode on any touch event.
    m_webPage->m_inputHandler->setInputModeEnabled();

    bool shiftActive = modifiers & KEYMOD_SHIFT;
    bool altActive = modifiers & KEYMOD_ALT;
    bool ctrlActive = modifiers & KEYMOD_CTRL;

    switch (point.state()) {
    case Platform::TouchPoint::TouchPressed:
        {
            // Clear spellcheck state on any touch event
            m_webPage->m_inputHandler->clearDidSpellCheckState();

            if (!m_lastFatFingersResult.isValid())
                doFatFingers(point);

            // Check for text selection
            if (m_lastFatFingersResult.isTextInput()) {
                Element* elementUnderFatFinger = m_lastFatFingersResult.nodeAsElementIfApplicable(FatFingersResult::ShadowContentNotAllowed, true /* shouldUseRootEditableElement */);
                m_shouldRequestSpellCheckOptions = m_webPage->m_inputHandler->shouldRequestSpellCheckingOptionsForPoint(m_lastFatFingersResult.adjustedPosition(), elementUnderFatFinger, m_spellCheckOptionRequest);
            }

            m_webPage->m_inputHandler->elementTouched(lastFatFingersResult().nodeAsElementIfApplicable(FatFingersResult::ShadowContentNotAllowed));

            handleFatFingerPressed(shiftActive, altActive, ctrlActive);
            break;
        }
    case Platform::TouchPoint::TouchReleased:
        {

            if (!m_shouldRequestSpellCheckOptions)
                m_webPage->m_inputHandler->processPendingKeyboardVisibilityChange();

            // The rebase has eliminated a necessary event when the mouse does not
            // trigger an actual selection change preventing re-showing of the
            // keyboard. If input mode is active, call showVirtualKeyboard which
            // will update the state and display keyboard if needed.
            if (m_webPage->m_inputHandler->isInputMode())
                m_webPage->m_inputHandler->notifyClientOfKeyboardVisibilityChange(true);

            m_webPage->m_tapHighlight->hide();
            m_webPage->m_selectionHighlight->hide();

            const Platform::ViewportAccessor* viewportAccessor = m_webPage->m_webkitThreadViewportAccessor;
            IntPoint documentViewportAdjustedPosition = viewportAccessor->documentViewportFromContents(m_lastFatFingersResult.adjustedPosition());
            PlatformMouseEvent mouseEvent(documentViewportAdjustedPosition, m_lastScreenPoint, PlatformEvent::MouseReleased, 1, LeftButton, shiftActive, ctrlActive, altActive, TouchScreen);

            m_webPage->handleMouseEvent(mouseEvent);

            if (m_shouldRequestSpellCheckOptions) {
                IntPoint pixelPositionRelativeToViewport = viewportAccessor->pixelViewportFromContents(
                    viewportAccessor->roundToPixelFromDocumentContents(WebCore::FloatPoint(m_lastFatFingersResult.adjustedPosition())));
                IntSize screenOffset(m_lastScreenPoint - pixelPositionRelativeToViewport);
                m_webPage->m_inputHandler->requestSpellingCheckingOptions(m_spellCheckOptionRequest, screenOffset);
                m_shouldRequestSpellCheckOptions = false;
            }

            m_lastFatFingersResult.reset(); // Reset the fat finger result as its no longer valid when a user's finger is not on the screen.
            break;
        }
    case Platform::TouchPoint::TouchMoved:
        {
            // Clear spellcheck state on any touch event
            m_webPage->m_inputHandler->clearDidSpellCheckState();

            // You can still send mouse move events
            PlatformMouseEvent mouseEvent(point.documentViewportPosition(), m_lastScreenPoint, PlatformEvent::MouseMoved, 1, LeftButton, shiftActive, ctrlActive, altActive, TouchScreen);
            m_lastScreenPoint = point.screenPosition();
            m_webPage->handleMouseEvent(mouseEvent);
            break;
        }
    default:
        break;
    }
}

void TouchEventHandler::handleFatFingerPressed(bool shiftActive, bool altActive, bool ctrlActive)
{
    const Platform::ViewportAccessor* viewportAccessor = m_webPage->m_webkitThreadViewportAccessor;
    IntPoint documentViewportAdjustedPosition = viewportAccessor->documentViewportFromContents(m_lastFatFingersResult.adjustedPosition());

    // First update the mouse position with a MouseMoved event.
    PlatformMouseEvent mouseMoveEvent(documentViewportAdjustedPosition, m_lastScreenPoint, PlatformEvent::MouseMoved, 0, LeftButton, shiftActive, ctrlActive, altActive, TouchScreen);
    m_webPage->handleMouseEvent(mouseMoveEvent);

    // Then send the MousePressed event.
    PlatformMouseEvent mousePressedEvent(documentViewportAdjustedPosition, m_lastScreenPoint, PlatformEvent::MousePressed, 1, LeftButton, shiftActive, ctrlActive, altActive, TouchScreen);
    m_webPage->handleMouseEvent(mousePressedEvent);
}

// This method filters what element will get tap-highlight'ed or not. To start with,
// we are going to highlight links (anchors with a valid href element), and elements
// whose tap highlight color value is different than the default value.
static Element* elementForTapHighlight(Element* elementUnderFatFinger)
{
    // Do not bail out right way here if there element does not have a renderer.
    // It is the casefor <map> (descendent of <area>) elements. The associated <image>
    // element actually has the renderer.
    if (elementUnderFatFinger->renderer()) {
        Color tapHighlightColor = elementUnderFatFinger->renderStyle()->tapHighlightColor();
        if (tapHighlightColor != RenderTheme::defaultTheme()->platformTapHighlightColor())
            return elementUnderFatFinger;
    }

    bool isArea = elementUnderFatFinger->hasTagName(HTMLNames::areaTag);
    Node* linkNode = elementUnderFatFinger->isLink() ? elementUnderFatFinger : 0;
    if (!linkNode || !linkNode->isHTMLElement() || (!linkNode->renderer() && !isArea))
        return 0;

    ASSERT(linkNode->isLink());

    // FatFingers class selector ensure only anchor with valid href attr value get here.
    // It includes empty hrefs.
    Element* highlightCandidateElement = toElement(linkNode);

    if (!isArea)
        return highlightCandidateElement;

    HTMLAreaElement* area = static_cast<HTMLAreaElement*>(highlightCandidateElement);
    HTMLImageElement* image = area->imageElement();
    if (image && image->renderer())
        return image;

    return 0;
}

void TouchEventHandler::drawTapHighlight()
{
    Element* elementUnderFatFinger = m_lastFatFingersResult.nodeAsElementIfApplicable();
    if (!elementUnderFatFinger)
        return;

    Element* element = elementForTapHighlight(elementUnderFatFinger);
    if (!element)
        return;

    // Get the element bounding rect in transformed coordinates so we can extract
    // the focus ring relative position each rect.
    RenderObject* renderer = element->renderer();
    ASSERT(renderer);

    Frame* elementFrame = element->document()->frame();
    ASSERT(elementFrame);

    FrameView* elementFrameView = elementFrame->view();
    if (!elementFrameView)
        return;

    // Tell the client if the element is either in a scrollable container or in a fixed positioned container.
    // On the client side, this info is being used to hide the tap highlight window on scroll.
    RenderLayer* layer = m_webPage->enclosingFixedPositionedAncestorOrSelfIfFixedPositioned(renderer->enclosingLayer());
    bool shouldHideTapHighlightRightAfterScrolling = !layer->renderer()->isRenderView();
    shouldHideTapHighlightRightAfterScrolling |= !!m_webPage->m_inRegionScroller->d->isActive();

    IntPoint framePos(m_webPage->frameOffset(elementFrame));

    // FIXME: We can get more precise on the <map> case by calculating the rect with HTMLAreaElement::computeRect().
    IntRect absoluteRect(renderer->absoluteClippedOverflowRect());
    absoluteRect.move(framePos.x(), framePos.y());

    IntRect clippingRect;
    if (elementFrame == m_webPage->mainFrame())
        clippingRect = IntRect(IntPoint(0, 0), elementFrameView->contentsSize());
    else
        clippingRect = m_webPage->mainFrame()->view()->windowToContents(m_webPage->getRecursiveVisibleWindowRect(elementFrameView, true /*noClipToMainFrame*/));
    clippingRect = intersection(absoluteRect, clippingRect);

    Vector<FloatQuad> focusRingQuads;
    renderer->absoluteFocusRingQuads(focusRingQuads);

    Platform::IntRectRegion region;
    for (size_t i = 0; i < focusRingQuads.size(); ++i) {
        IntRect rect = focusRingQuads[i].enclosingBoundingBox();
        rect.move(framePos.x(), framePos.y());
        IntRect clippedRect = intersection(clippingRect, rect);
        // FIXME we shouldn't have any empty rects here PR 246960
        if (clippedRect.isEmpty())
            continue;
        clippedRect.inflate(2);
        region = unionRegions(region, Platform::IntRect(clippedRect));
    }

    Color highlightColor = element->renderStyle()->tapHighlightColor();

    m_webPage->m_tapHighlight->draw(region,
        highlightColor.red(), highlightColor.green(), highlightColor.blue(), highlightColor.alpha(),
        shouldHideTapHighlightRightAfterScrolling);
}

void TouchEventHandler::playSoundIfAnchorIsTarget() const
{
    if (m_lastFatFingersResult.node() && m_lastFatFingersResult.node()->isLink())
        BlackBerry::Platform::SystemSound::instance()->playSound(BlackBerry::Platform::SystemSoundType::InputKeypress);
}

}
}