/* * 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 #include #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_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->performDragOperation(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) { #if ENABLE(QT_GESTURE_EVENTS) 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())); m_webPageProxy->handleGestureEvent(gesture); #endif } 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(ViewState::IsFocused | ViewState::WindowIsActive); } void QtWebPageEventHandler::handleFocusLost() { m_webPageProxy->viewStateDidChange(ViewState::IsFocused | ViewState::WindowIsActive); } 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 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->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 ENABLE(QT_GESTURE_EVENTS) if (event.type() != WebEvent::GestureSingleTap) return; m_postponeTextInputStateChanged = false; if (!wasEventHandled || !m_webView->hasActiveFocus()) return; updateTextInputState(); #endif } 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 activeTouchPoints; QTouchEvent::TouchPoint currentTouchPoint; qint64 eventTimestampMillis = event->timestamp(); int touchPointCount = 0; if (!isMouseEvent) { const QTouchEvent* touchEvent = static_cast(event); const QList& 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(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 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->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"