QtWebPageEventHandler.cpp   [plain text]


/*
 * Copyright (C) 2010, 2011 Nokia Corporation and/or its subsidiary(-ies)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This program 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this program; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 */

#include "config.h"
#include "QtWebPageEventHandler.h"

#include "NativeWebKeyboardEvent.h"
#include "NativeWebMouseEvent.h"
#include "NativeWebWheelEvent.h"
#include "PageViewportControllerClientQt.h"
#include "WebPageProxy.h"
#include "qquickwebpage_p.h"
#include "qquickwebview_p.h"
#include <QCursor>
#include <QDrag>
#include <QGuiApplication>
#include <QInputEvent>
#include <QInputMethod>
#include <QMimeData>
#include <QMouseEvent>
#include <QQuickWindow>
#include <QStyleHints>
#include <QTextFormat>
#include <QTouchEvent>
#include <QTransform>
#include <WebCore/DragData.h>
#include <WebCore/Editor.h>

using namespace WebCore;

namespace WebKit {

static inline Qt::DropAction dragOperationToDropAction(unsigned dragOperation)
{
    Qt::DropAction result = Qt::IgnoreAction;
    if (dragOperation & DragOperationCopy)
        result = Qt::CopyAction;
    else if (dragOperation & DragOperationMove)
        result = Qt::MoveAction;
    else if (dragOperation & DragOperationGeneric)
        result = Qt::MoveAction;
    else if (dragOperation & DragOperationLink)
        result = Qt::LinkAction;
    return result;
}

static inline Qt::DropActions dragOperationToDropActions(unsigned dragOperations)
{
    Qt::DropActions result = Qt::IgnoreAction;
    if (dragOperations & DragOperationCopy)
        result |= Qt::CopyAction;
    if (dragOperations & DragOperationMove)
        result |= Qt::MoveAction;
    if (dragOperations & DragOperationGeneric)
        result |= Qt::MoveAction;
    if (dragOperations & DragOperationLink)
        result |= Qt::LinkAction;
    return result;
}

static inline WebCore::DragOperation dropActionToDragOperation(Qt::DropActions actions)
{
    unsigned result = 0;
    if (actions & Qt::CopyAction)
        result |= DragOperationCopy;
    if (actions & Qt::MoveAction)
        result |= (DragOperationMove | DragOperationGeneric);
    if (actions & Qt::LinkAction)
        result |= DragOperationLink;
    if (result == (DragOperationCopy | DragOperationMove | DragOperationGeneric | DragOperationLink))
        result = DragOperationEvery;
    return (DragOperation)result;
}

QtWebPageEventHandler::QtWebPageEventHandler(WKPageRef pageRef, QQuickWebPage* qmlWebPage, QQuickWebView* qmlWebView)
    : m_webPageProxy(toImpl(pageRef))
    , m_viewportController(0)
    , m_panGestureRecognizer(this)
    , m_pinchGestureRecognizer(this)
    , m_tapGestureRecognizer(this)
    , m_webPage(qmlWebPage)
    , m_webView(qmlWebView)
    , m_previousClickButton(Qt::NoButton)
    , m_clickCount(0)
    , m_postponeTextInputStateChanged(false)
    , m_isTapHighlightActive(false)
    , m_isMouseButtonPressed(false)
{
    connect(qApp->inputMethod(), SIGNAL(visibleChanged()), this, SLOT(inputPanelVisibleChanged()));
}

QtWebPageEventHandler::~QtWebPageEventHandler()
{
    disconnect(qApp->inputMethod(), SIGNAL(visibleChanged()), this, SLOT(inputPanelVisibleChanged()));
}

void QtWebPageEventHandler::handleMouseMoveEvent(QMouseEvent* ev)
{
    // For some reason mouse press results in mouse hover (which is
    // converted to mouse move for WebKit). We ignore these hover
    // events by comparing lastPos with newPos.
    // NOTE: lastPos from the event always comes empty, so we work
    // around that here.
    static QPointF lastPos = QPointF();
    QTransform fromItemTransform = m_webPage->transformFromItem();
    QPointF webPagePoint = fromItemTransform.map(ev->localPos());
    ev->accept();
    if (lastPos == webPagePoint)
        return;
    lastPos = webPagePoint;

    m_webPageProxy->handleMouseEvent(NativeWebMouseEvent(ev, fromItemTransform, /*eventClickCount*/ 0));
}

void QtWebPageEventHandler::handleMousePressEvent(QMouseEvent* ev)
{
    QTransform fromItemTransform = m_webPage->transformFromItem();
    QPointF webPagePoint = fromItemTransform.map(ev->localPos());

    if (m_clickTimer.isActive()
        && m_previousClickButton == ev->button()
        && (webPagePoint - m_lastClick).manhattanLength() < qApp->styleHints()->startDragDistance()) {
        m_clickCount++;
    } else {
        m_clickCount = 1;
        m_previousClickButton = ev->button();
    }

    ev->accept();
    m_webPageProxy->handleMouseEvent(NativeWebMouseEvent(ev, fromItemTransform, m_clickCount));

    m_lastClick = webPagePoint;
    m_clickTimer.start(qApp->styleHints()->mouseDoubleClickInterval(), this);
}

void QtWebPageEventHandler::handleMouseReleaseEvent(QMouseEvent* ev)
{
    ev->accept();
    QTransform fromItemTransform = m_webPage->transformFromItem();
    m_webPageProxy->handleMouseEvent(NativeWebMouseEvent(ev, fromItemTransform, /*eventClickCount*/ 0));
}

void QtWebPageEventHandler::handleWheelEvent(QWheelEvent* ev)
{
    QTransform fromItemTransform = m_webPage->transformFromItem();
    m_webPageProxy->handleWheelEvent(NativeWebWheelEvent(ev, fromItemTransform));
}

void QtWebPageEventHandler::handleHoverLeaveEvent(QHoverEvent* ev)
{
    // To get the correct behavior of mouseout, we need to turn the Leave event of our webview into a mouse move
    // to a very far region.
    QHoverEvent fakeEvent(QEvent::HoverMove, QPoint(INT_MIN, INT_MIN), ev->oldPosF());
    fakeEvent.setTimestamp(ev->timestamp());
    // This will apply the transform on the event.
    handleHoverMoveEvent(&fakeEvent);
}

void QtWebPageEventHandler::handleHoverMoveEvent(QHoverEvent* ev)
{
    QMouseEvent me(QEvent::MouseMove, ev->posF(), Qt::NoButton, Qt::NoButton, Qt::NoModifier);
    me.setAccepted(ev->isAccepted());
    me.setTimestamp(ev->timestamp());
    // This will apply the transform on the event.
    handleMouseMoveEvent(&me);
}

void QtWebPageEventHandler::handleDragEnterEvent(QDragEnterEvent* ev)
{
    m_webPageProxy->resetDragOperation();
    QTransform fromItemTransform = m_webPage->transformFromItem();
    // FIXME: Should not use QCursor::pos()
    DragData dragData(ev->mimeData(), fromItemTransform.map(ev->pos()), QCursor::pos(), dropActionToDragOperation(ev->possibleActions()));
    m_webPageProxy->dragEntered(&dragData);
    ev->acceptProposedAction();
}

void QtWebPageEventHandler::handleDragLeaveEvent(QDragLeaveEvent* ev)
{
    bool accepted = ev->isAccepted();

    // FIXME: Should not use QCursor::pos()
    DragData dragData(0, IntPoint(), QCursor::pos(), DragOperationNone);
    m_webPageProxy->dragExited(&dragData);
    m_webPageProxy->resetDragOperation();

    ev->setAccepted(accepted);
}

void QtWebPageEventHandler::handleDragMoveEvent(QDragMoveEvent* ev)
{
    bool accepted = ev->isAccepted();

    QTransform fromItemTransform = m_webPage->transformFromItem();
    // FIXME: Should not use QCursor::pos()
    DragData dragData(ev->mimeData(), fromItemTransform.map(ev->pos()), QCursor::pos(), dropActionToDragOperation(ev->possibleActions()));
    m_webPageProxy->dragUpdated(&dragData);
    ev->setDropAction(dragOperationToDropAction(m_webPageProxy->dragSession().operation));
    if (m_webPageProxy->dragSession().operation != DragOperationNone)
        ev->accept();

    ev->setAccepted(accepted);
}

void QtWebPageEventHandler::handleDropEvent(QDropEvent* ev)
{
    bool accepted = ev->isAccepted();
    QTransform fromItemTransform = m_webPage->transformFromItem();
    // FIXME: Should not use QCursor::pos()
    DragData dragData(ev->mimeData(), fromItemTransform.map(ev->pos()), QCursor::pos(), dropActionToDragOperation(ev->possibleActions()));
    SandboxExtension::Handle handle;
    SandboxExtension::HandleArray sandboxExtensionForUpload;
    m_webPageProxy->performDrag(&dragData, String(), handle, sandboxExtensionForUpload);
    ev->setDropAction(dragOperationToDropAction(m_webPageProxy->dragSession().operation));
    ev->accept();

    ev->setAccepted(accepted);
}

void QtWebPageEventHandler::activateTapHighlight(const QTouchEvent::TouchPoint& point)
{
#if ENABLE(TOUCH_EVENTS)
    ASSERT(!point.pos().toPoint().isNull());
    ASSERT(!m_isTapHighlightActive);
    m_isTapHighlightActive = true;
    QTransform fromItemTransform = m_webPage->transformFromItem();
    m_webPageProxy->handlePotentialActivation(IntPoint(fromItemTransform.map(point.pos()).toPoint()), IntSize(point.rect().size().toSize()));
#else
    Q_UNUSED(point);
#endif
}

void QtWebPageEventHandler::deactivateTapHighlight()
{
#if ENABLE(TOUCH_EVENTS)
    if (!m_isTapHighlightActive)
        return;

    // An empty point deactivates the highlighting.
    m_webPageProxy->handlePotentialActivation(IntPoint(), IntSize());
    m_isTapHighlightActive = false;
#endif
}

void QtWebPageEventHandler::handleSingleTapEvent(const QTouchEvent::TouchPoint& point)
{
    deactivateTapHighlight();
    m_postponeTextInputStateChanged = true;

    QTransform fromItemTransform = m_webPage->transformFromItem();
    WebGestureEvent gesture(WebEvent::GestureSingleTap, fromItemTransform.map(point.pos()).toPoint(), point.screenPos().toPoint(), WebEvent::Modifiers(0), 0, IntSize(point.rect().size().toSize()), FloatPoint(0, 0));
    m_webPageProxy->handleGestureEvent(gesture);
}

void QtWebPageEventHandler::handleDoubleTapEvent(const QTouchEvent::TouchPoint& point)
{
    if (!m_webView->isInteractive())
        return;

    deactivateTapHighlight();
    QTransform fromItemTransform = m_webPage->transformFromItem();
    m_webPageProxy->findZoomableAreaForPoint(fromItemTransform.map(point.pos()).toPoint(), IntSize(point.rect().size().toSize()));
}

void QtWebPageEventHandler::timerEvent(QTimerEvent* ev)
{
    int timerId = ev->timerId();
    if (timerId == m_clickTimer.timerId())
        m_clickTimer.stop();
    else
        QObject::timerEvent(ev);
}

void QtWebPageEventHandler::handleKeyPressEvent(QKeyEvent* ev)
{
    m_webPageProxy->handleKeyboardEvent(NativeWebKeyboardEvent(ev));
}

void QtWebPageEventHandler::handleKeyReleaseEvent(QKeyEvent* ev)
{
    m_webPageProxy->handleKeyboardEvent(NativeWebKeyboardEvent(ev));
}

void QtWebPageEventHandler::handleFocusInEvent(QFocusEvent*)
{
    m_webPageProxy->viewStateDidChange(WebPageProxy::ViewIsFocused | WebPageProxy::ViewWindowIsActive);
}

void QtWebPageEventHandler::handleFocusLost()
{
    m_webPageProxy->viewStateDidChange(WebPageProxy::ViewIsFocused | WebPageProxy::ViewWindowIsActive);
}

void QtWebPageEventHandler::setViewportController(PageViewportControllerClientQt* controller)
{
    m_viewportController = controller;
}

void QtWebPageEventHandler::handleInputMethodEvent(QInputMethodEvent* ev)
{
    QString commit = ev->commitString();
    QString composition = ev->preeditString();

    int replacementStart = ev->replacementStart();
    int replacementLength = ev->replacementLength();

    // NOTE: We might want to handle events of one char as special
    // and resend them as key events to make web site completion work.

    int cursorPositionWithinComposition = 0;

    Vector<CompositionUnderline> underlines;

    for (int i = 0; i < ev->attributes().size(); ++i) {
        const QInputMethodEvent::Attribute& attr = ev->attributes().at(i);
        switch (attr.type) {
        case QInputMethodEvent::TextFormat: {
            if (composition.isEmpty())
                break;

            QTextCharFormat textCharFormat = attr.value.value<QTextFormat>().toCharFormat();
            QColor qcolor = textCharFormat.underlineColor();
            Color color = makeRGBA(qcolor.red(), qcolor.green(), qcolor.blue(), qcolor.alpha());
            int start = qMin(attr.start, (attr.start + attr.length));
            int end = qMax(attr.start, (attr.start + attr.length));
            underlines.append(CompositionUnderline(start, end, color, false));
            break;
        }
        case QInputMethodEvent::Cursor:
            if (attr.length)
                cursorPositionWithinComposition = attr.start;
            break;
        // Selection is handled further down.
        default: break;
        }
    }

    if (composition.isEmpty()) {
        int selectionStart = -1;
        int selectionLength = 0;
        for (int i = 0; i < ev->attributes().size(); ++i) {
            const QInputMethodEvent::Attribute& attr = ev->attributes().at(i);
            if (attr.type == QInputMethodEvent::Selection) {
                selectionStart = attr.start;
                selectionLength = attr.length;

                ASSERT(selectionStart >= 0);
                ASSERT(selectionLength >= 0);
                break;
            }
        }

        m_webPageProxy->confirmComposition(commit, selectionStart, selectionLength);
    } else {
        ASSERT(cursorPositionWithinComposition >= 0);
        ASSERT(replacementStart >= 0);

        m_webPageProxy->setComposition(composition, underlines,
            cursorPositionWithinComposition, cursorPositionWithinComposition,
            replacementStart, replacementLength);
    }

    ev->accept();
}

void QtWebPageEventHandler::handleTouchEvent(QTouchEvent* event)
{
#if ENABLE(TOUCH_EVENTS)
    QTransform fromItemTransform = m_webPage->transformFromItem();
    m_webPageProxy->handleTouchEvent(NativeWebTouchEvent(event, fromItemTransform));
    event->accept();
#else
    ASSERT_NOT_REACHED();
    event->ignore();
#endif
}

void QtWebPageEventHandler::resetGestureRecognizers()
{
    m_panGestureRecognizer.cancel();
    m_pinchGestureRecognizer.cancel();
    m_tapGestureRecognizer.cancel();
}

static void setInputPanelVisible(bool visible)
{
    if (qApp->inputMethod()->isVisible() == visible)
        return;

    qApp->inputMethod()->setVisible(visible);
}

void QtWebPageEventHandler::inputPanelVisibleChanged()
{
    if (!m_viewportController)
        return;

    // We only respond to the input panel becoming visible.
    if (!m_webView->hasActiveFocus() || !qApp->inputMethod()->isVisible())
        return;

    const EditorState& editor = m_webPageProxy->editorState();
    if (editor.isContentEditable)
        m_viewportController->focusEditableArea(QRectF(editor.cursorRect), QRectF(editor.editorRect));
}

void QtWebPageEventHandler::updateTextInputState()
{
    if (m_postponeTextInputStateChanged)
        return;

    const EditorState& editor = m_webPageProxy->editorState();

    m_webView->setFlag(QQuickItem::ItemAcceptsInputMethod, editor.isContentEditable);

    if (!m_webView->hasActiveFocus())
        return;

    qApp->inputMethod()->update(Qt::ImQueryInput | Qt::ImEnabled | Qt::ImHints);

    setInputPanelVisible(editor.isContentEditable);
}

void QtWebPageEventHandler::handleWillSetInputMethodState()
{
    if (qApp->inputMethod()->isVisible())
        qApp->inputMethod()->commit();
}

void QtWebPageEventHandler::doneWithGestureEvent(const WebGestureEvent& event, bool wasEventHandled)
{
    if (event.type() != WebEvent::GestureSingleTap)
        return;

    m_postponeTextInputStateChanged = false;

    if (!wasEventHandled || !m_webView->hasActiveFocus())
        return;

    updateTextInputState();
}

void QtWebPageEventHandler::handleInputEvent(const QInputEvent* event)
{
    if (m_viewportController) {
        switch (event->type()) {
        case QEvent::MouseButtonPress:
        case QEvent::TouchBegin:
            m_viewportController->touchBegin();

            // The page viewport controller might still be animating kinetic scrolling or a scale animation
            // such as double-tap to zoom or the bounce back effect. A touch stops the kinetic scrolling
            // where as it does not stop the scale animation.
            // The gesture recognizer stops the kinetic scrolling animation if needed.
            break;
        case QEvent::MouseMove:
        case QEvent::TouchUpdate:
            // The scale animation can only be interrupted by a pinch gesture, which will then take over.
            if (m_viewportController->scaleAnimationActive() && m_pinchGestureRecognizer.isRecognized())
                m_viewportController->interruptScaleAnimation();
            break;
        case QEvent::MouseButtonRelease:
        case QEvent::TouchEnd:
            m_viewportController->touchEnd();
            break;
        default:
            break;
        }

        // If the scale animation is active we don't pass the event to the recognizers. In the future
        // we would want to queue the event here and repost then when the animation ends.
        if (m_viewportController->scaleAnimationActive())
            return;
    }

    bool isMouseEvent = false;

    switch (event->type()) {
    case QEvent::MouseButtonPress:
        isMouseEvent = true;
        m_isMouseButtonPressed = true;
        break;
    case QEvent::MouseMove:
        if (!m_isMouseButtonPressed)
            return;
        isMouseEvent = true;
        break;
    case QEvent::MouseButtonRelease:
        isMouseEvent = true;
        m_isMouseButtonPressed = false;
        break;
    case QEvent::MouseButtonDblClick:
        return;
    default:
        break;
    }

    QList<QTouchEvent::TouchPoint> activeTouchPoints;
    QTouchEvent::TouchPoint currentTouchPoint;
    qint64 eventTimestampMillis = event->timestamp();
    int touchPointCount = 0;

    if (!isMouseEvent) {
        const QTouchEvent* touchEvent = static_cast<const QTouchEvent*>(event);
        const QList<QTouchEvent::TouchPoint>& touchPoints = touchEvent->touchPoints();
        currentTouchPoint = touchPoints.first();
        touchPointCount = touchPoints.size();
        activeTouchPoints.reserve(touchPointCount);

        for (int i = 0; i < touchPointCount; ++i) {
            if (touchPoints[i].state() != Qt::TouchPointReleased)
                activeTouchPoints << touchPoints[i];
        }
    } else {
        const QMouseEvent* mouseEvent = static_cast<const QMouseEvent*>(event);
        touchPointCount = 1;

        // Make a distinction between mouse events on the basis of pressed buttons.
        currentTouchPoint.setId(mouseEvent->buttons());
        currentTouchPoint.setScreenPos(mouseEvent->screenPos());
        // For tap gesture hit testing the float touch rect is translated to
        // an int rect representing the radius of the touch point (size/2),
        // thus the touch rect has to have a size of at least 2.
        currentTouchPoint.setRect(QRectF(mouseEvent->localPos(), QSizeF(2, 2)));

        if (m_isMouseButtonPressed)
            activeTouchPoints << currentTouchPoint;
    }

    const int activeTouchPointCount = activeTouchPoints.size();

    if (!activeTouchPointCount) {
        if (touchPointCount == 1) {
            // No active touch points, one finger released.
            if (m_panGestureRecognizer.isRecognized())
                m_panGestureRecognizer.finish(currentTouchPoint, eventTimestampMillis);
            else {
                // The events did not result in a pan gesture.
                m_panGestureRecognizer.cancel();
                m_tapGestureRecognizer.finish(currentTouchPoint);
            }

        } else
            m_pinchGestureRecognizer.finish();

        // Early return since this was a touch-end event.
        return;
    } else if (activeTouchPointCount == 1) {
        // If the pinch gesture recognizer was previously in active state the content might
        // be out of valid zoom boundaries, thus we need to finish the pinch gesture here.
        // This will resume the content to valid zoom levels before the pan gesture is started.
        m_pinchGestureRecognizer.finish();
        m_panGestureRecognizer.update(activeTouchPoints.first(), eventTimestampMillis);
    } else if (activeTouchPointCount == 2) {
        m_panGestureRecognizer.cancel();
        m_pinchGestureRecognizer.update(activeTouchPoints.first(), activeTouchPoints.last());
    }

    if (m_panGestureRecognizer.isRecognized() || m_pinchGestureRecognizer.isRecognized() || m_webView->isMoving())
        m_tapGestureRecognizer.cancel();
    else if (touchPointCount == 1)
        m_tapGestureRecognizer.update(currentTouchPoint);

}

#if ENABLE(TOUCH_EVENTS)
void QtWebPageEventHandler::doneWithTouchEvent(const NativeWebTouchEvent& event, bool wasEventHandled)
{
    if (wasEventHandled || event.type() == WebEvent::TouchCancel) {
        m_panGestureRecognizer.cancel();
        m_pinchGestureRecognizer.cancel();
        if (event.type() != WebEvent::TouchMove)
            m_tapGestureRecognizer.cancel();
        return;
    }

    const QTouchEvent* ev = event.nativeEvent();

    handleInputEvent(ev);
}
#endif

void QtWebPageEventHandler::didFindZoomableArea(const IntPoint& target, const IntRect& area)
{
    if (!m_viewportController)
        return;

    // FIXME: As the find method might not respond immediately during load etc,
    // we should ignore all but the latest request.
    m_viewportController->zoomToAreaGestureEnded(QPointF(target), QRectF(area));
}

void QtWebPageEventHandler::startDrag(const WebCore::DragData& dragData, PassRefPtr<ShareableBitmap> dragImage)
{
    QImage dragQImage;
    if (dragImage)
        dragQImage = dragImage->createQImage();
    else if (dragData.platformData() && dragData.platformData()->hasImage())
        dragQImage = qvariant_cast<QImage>(dragData.platformData()->imageData());

    DragOperation dragOperationMask = dragData.draggingSourceOperationMask();
    QMimeData* mimeData = const_cast<QMimeData*>(dragData.platformData());
    Qt::DropActions supportedDropActions = dragOperationToDropActions(dragOperationMask);

    QPoint clientPosition;
    QPoint globalPosition;
    Qt::DropAction actualDropAction = Qt::IgnoreAction;

    if (QWindow* window = m_webPage->window()) {
        QDrag* drag = new QDrag(window);
        drag->setPixmap(QPixmap::fromImage(dragQImage));
        drag->setMimeData(mimeData);
        actualDropAction = drag->exec(supportedDropActions);
        globalPosition = QCursor::pos();
        clientPosition = window->mapFromGlobal(globalPosition);
    }

    m_webPageProxy->dragEnded(clientPosition, globalPosition, dropActionToDragOperation(actualDropAction));
}

} // namespace WebKit

#include "moc_QtWebPageEventHandler.cpp"