/* * 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 "QtViewportInteractionEngine.h" #include "WebPageProxy.h" #include "qquickwebpage_p.h" #include "qquickwebview_p.h" #include #include #include #include #include #include #include #include #include #include #include #include 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_interactionEngine(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) { connect(qApp->inputPanel(), SIGNAL(visibleChanged()), this, SLOT(inputPanelVisibleChanged())); } QtWebPageEventHandler::~QtWebPageEventHandler() { disconnect(qApp->inputPanel(), 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()); 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(); } m_webPageProxy->handleMouseEvent(NativeWebMouseEvent(ev, fromItemTransform, m_clickCount)); m_lastClick = webPagePoint; m_clickTimer.start(qApp->styleHints()->mouseDoubleClickInterval(), this); } void QtWebPageEventHandler::handleMouseReleaseEvent(QMouseEvent* ev) { 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. QTransform fromItemTransform = m_webPage->transformFromItem(); QHoverEvent fakeEvent(QEvent::HoverMove, QPoint(INT_MIN, INT_MIN), fromItemTransform.map(ev->oldPosF())); fakeEvent.setTimestamp(ev->timestamp()); handleHoverMoveEvent(&fakeEvent); } void QtWebPageEventHandler::handleHoverMoveEvent(QHoverEvent* ev) { QTransform fromItemTransform = m_webPage->transformFromItem(); QMouseEvent me(QEvent::MouseMove, fromItemTransform.map(ev->posF()), Qt::NoButton, Qt::NoButton, Qt::NoModifier); me.setAccepted(ev->isAccepted()); me.setTimestamp(ev->timestamp()); 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::handlePotentialSingleTapEvent(const QTouchEvent::TouchPoint& point) { #if ENABLE(TOUCH_EVENTS) if (point.pos() == QPointF()) { // An empty point deactivates the highlighting. m_webPageProxy->handlePotentialActivation(IntPoint(), IntSize()); } else { 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::handleSingleTapEvent(const QTouchEvent::TouchPoint& point) { 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) { 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::handleFocusOutEvent(QFocusEvent*) { m_webPageProxy->viewStateDidChange(WebPageProxy::ViewIsFocused | WebPageProxy::ViewWindowIsActive); } void QtWebPageEventHandler::setViewportInteractionEngine(QtViewportInteractionEngine* engine) { m_interactionEngine = engine; } 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 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().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->inputPanel()->visible() == visible) return; qApp->inputPanel()->setVisible(visible); } void QtWebPageEventHandler::inputPanelVisibleChanged() { if (!m_interactionEngine) return; // We only respond to the input panel becoming visible. if (!m_webView->hasActiveFocus() || !qApp->inputPanel()->visible()) return; const EditorState& editor = m_webPageProxy->editorState(); if (editor.isContentEditable) m_interactionEngine->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->inputPanel()->update(Qt::ImQueryInput | Qt::ImEnabled); setInputPanelVisible(editor.isContentEditable); } void QtWebPageEventHandler::doneWithGestureEvent(const WebGestureEvent& event, bool wasEventHandled) { if (event.type() != WebEvent::GestureSingleTap) return; m_postponeTextInputStateChanged = false; if (!wasEventHandled || !m_webView->hasActiveFocus()) return; updateTextInputState(); } #if ENABLE(TOUCH_EVENTS) void QtWebPageEventHandler::doneWithTouchEvent(const NativeWebTouchEvent& event, bool wasEventHandled) { if (!m_interactionEngine) return; if (wasEventHandled || event.type() == WebEvent::TouchCancel) { resetGestureRecognizers(); return; } const QTouchEvent* ev = event.nativeEvent(); switch (ev->type()) { case QEvent::TouchBegin: ASSERT(!m_interactionEngine->panGestureActive()); ASSERT(!m_interactionEngine->pinchGestureActive()); m_interactionEngine->touchBegin(); // The interaction engine 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::TouchUpdate: // The scale animation can only be interrupted by a pinch gesture, which will then take over. if (m_interactionEngine->scaleAnimationActive() && m_pinchGestureRecognizer.isRecognized()) m_interactionEngine->interruptScaleAnimation(); break; case QEvent::TouchEnd: m_interactionEngine->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_interactionEngine->scaleAnimationActive()) return; const QList& touchPoints = ev->touchPoints(); const int touchPointCount = touchPoints.size(); qint64 eventTimestampMillis = ev->timestamp(); QList activeTouchPoints; activeTouchPoints.reserve(touchPointCount); for (int i = 0; i < touchPointCount; ++i) { if (touchPoints[i].state() != Qt::TouchPointReleased) activeTouchPoints << touchPoints[i]; } const int activeTouchPointCount = activeTouchPoints.size(); if (!activeTouchPointCount) { if (touchPointCount == 1) { // No active touch points, one finger released. if (!m_panGestureRecognizer.isRecognized()) m_tapGestureRecognizer.update(ev->type(), touchPoints.first()); m_panGestureRecognizer.finish(touchPoints.first(), eventTimestampMillis); } 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(ev->type(), touchPoints.first()); } #endif void QtWebPageEventHandler::didFindZoomableArea(const IntPoint& target, const IntRect& area) { if (!m_interactionEngine) return; // FIXME: As the find method might not respond immediately during load etc, // we should ignore all but the latest request. m_interactionEngine->zoomToAreaGestureEnded(QPointF(target), QRectF(area)); } void QtWebPageEventHandler::startDrag(const WebCore::DragData& dragData, PassRefPtr dragImage) { QImage dragQImage; if (dragImage) dragQImage = dragImage->createQImage(); else if (dragData.platformData() && dragData.platformData()->hasImage()) dragQImage = qvariant_cast(dragData.platformData()->imageData()); DragOperation dragOperationMask = dragData.draggingSourceOperationMask(); QMimeData* mimeData = const_cast(dragData.platformData()); Qt::DropActions supportedDropActions = dragOperationToDropActions(dragOperationMask); QPoint clientPosition; QPoint globalPosition; Qt::DropAction actualDropAction = Qt::IgnoreAction; if (QWindow* window = m_webPage->canvas()) { 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"