diff options
author | Simon Hausmann <simon.hausmann@nokia.com> | 2012-01-06 14:44:00 +0100 |
---|---|---|
committer | Simon Hausmann <simon.hausmann@nokia.com> | 2012-01-06 14:44:00 +0100 |
commit | 40736c5763bf61337c8c14e16d8587db021a87d4 (patch) | |
tree | b17a9c00042ad89cb1308e2484491799aa14e9f8 /Source/WebKit2/UIProcess/qt/QtViewportInteractionEngine.cpp | |
download | qtwebkit-40736c5763bf61337c8c14e16d8587db021a87d4.tar.gz |
Imported WebKit commit 2ea9d364d0f6efa8fa64acf19f451504c59be0e4 (http://svn.webkit.org/repository/webkit/trunk@104285)
Diffstat (limited to 'Source/WebKit2/UIProcess/qt/QtViewportInteractionEngine.cpp')
-rw-r--r-- | Source/WebKit2/UIProcess/qt/QtViewportInteractionEngine.cpp | 589 |
1 files changed, 589 insertions, 0 deletions
diff --git a/Source/WebKit2/UIProcess/qt/QtViewportInteractionEngine.cpp b/Source/WebKit2/UIProcess/qt/QtViewportInteractionEngine.cpp new file mode 100644 index 000000000..788349ae0 --- /dev/null +++ b/Source/WebKit2/UIProcess/qt/QtViewportInteractionEngine.cpp @@ -0,0 +1,589 @@ +/* + * Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies) + * Copyright (C) 2011 Benjamin Poulain <benjamin@webkit.org> + * + * 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 "QtViewportInteractionEngine.h" + +#include "PassOwnPtr.h" +#include <QPointF> +#include <QScrollEvent> +#include <QScrollPrepareEvent> +#include <QScrollerProperties> +#include <QWheelEvent> +#include <QtQuick/qquickitem.h> + +namespace WebKit { + +static const int kScaleAnimationDurationMillis = 250; + +// UPDATE DEFERRING (SUSPEND/RESUME) +// ================================= +// +// When interaction with the content, either by animating or by the hand of the user, +// it is important to ensure smooth animations of at least 60fps in order to give a +// good user experience. +// +// In order to do this we need to get rid of unknown factors. These include device +// sensors (geolocation, orientation updates etc), CSS3 animations, JavaScript +// exectution, sub resource loads etc. We do this by emitting suspend and resume +// signals, which are then handled by the viewport and propagates to the right place. +// +// For this reason the ViewportUpdateDeferrer guard must be used when we interact +// or animate the content. +// +// It should be noted that when we update content properties, we might receive notify +// signals send my the content item itself, and care should be taken to not act on +// these unconditionally. An example of this is the pinch zoom, which changes the +// position and will thus result in a QQuickWebPage::geometryChanged() signal getting +// emitted. +// +// If something should only be executed during update deferring, it is possible to +// check for that using ASSERT(m_suspendCount). + +class ViewportUpdateDeferrer { +public: + ViewportUpdateDeferrer(QtViewportInteractionEngine* engine) + : engine(engine) + { + if (engine->m_suspendCount++) + return; + + emit engine->contentSuspendRequested(); + } + + ~ViewportUpdateDeferrer() + { + if (--(engine->m_suspendCount)) + return; + + emit engine->contentResumeRequested(); + + // Make sure that tiles all around the viewport will be requested. + emit engine->viewportTrajectoryVectorChanged(QPointF()); + } + +private: + QtViewportInteractionEngine* const engine; +}; + +inline qreal QtViewportInteractionEngine::cssScaleFromItem(qreal itemScale) +{ + return itemScale / m_constraints.devicePixelRatio; +} + +inline qreal QtViewportInteractionEngine::itemScaleFromCSS(qreal cssScale) +{ + return cssScale * m_constraints.devicePixelRatio; +} + +inline qreal QtViewportInteractionEngine::itemCoordFromCSS(qreal value) +{ + return value * m_constraints.devicePixelRatio; +} + +inline QRectF QtViewportInteractionEngine::itemRectFromCSS(const QRectF& cssRect) +{ + QRectF itemRect; + + itemRect.setX(itemCoordFromCSS(cssRect.x())); + itemRect.setY(itemCoordFromCSS(cssRect.y())); + itemRect.setWidth(itemCoordFromCSS(cssRect.width())); + itemRect.setHeight(itemCoordFromCSS(cssRect.height())); + + return itemRect; +} + +QtViewportInteractionEngine::QtViewportInteractionEngine(const QQuickItem* viewport, QQuickItem* content) + : m_viewport(viewport) + , m_content(content) + , m_suspendCount(0) + , m_scaleAnimation(new ScaleAnimation(this)) + , m_pinchStartScale(-1) +{ + reset(); + + QScrollerProperties properties = scroller()->scrollerProperties(); + + // The QtPanGestureRecognizer is responsible for recognizing the gesture + // thus we need to disable the drag start distance. + properties.setScrollMetric(QScrollerProperties::DragStartDistance, 0.0); + + // Set some default QScroller constrains to mimic the physics engine of the N9 browser. + properties.setScrollMetric(QScrollerProperties::AxisLockThreshold, 0.66); + properties.setScrollMetric(QScrollerProperties::ScrollingCurve, QEasingCurve(QEasingCurve::OutExpo)); + properties.setScrollMetric(QScrollerProperties::DecelerationFactor, 0.05); + properties.setScrollMetric(QScrollerProperties::MaximumVelocity, 0.635); + properties.setScrollMetric(QScrollerProperties::OvershootDragResistanceFactor, 0.33); + properties.setScrollMetric(QScrollerProperties::OvershootScrollDistanceFactor, 0.33); + + scroller()->setScrollerProperties(properties); + + connect(m_content, SIGNAL(widthChanged()), this, SLOT(itemSizeChanged()), Qt::DirectConnection); + connect(m_content, SIGNAL(heightChanged()), this, SLOT(itemSizeChanged()), Qt::DirectConnection); + + connect(m_scaleAnimation, SIGNAL(valueChanged(QVariant)), + SLOT(scaleAnimationValueChanged(QVariant)), Qt::DirectConnection); + connect(m_scaleAnimation, SIGNAL(stateChanged(QAbstractAnimation::State, QAbstractAnimation::State)), + SLOT(scaleAnimationStateChanged(QAbstractAnimation::State, QAbstractAnimation::State)), Qt::DirectConnection); + + connect(scroller(), SIGNAL(stateChanged(QScroller::State)), + SLOT(scrollStateChanged(QScroller::State)), Qt::DirectConnection); +} + +QtViewportInteractionEngine::~QtViewportInteractionEngine() +{ +} + +qreal QtViewportInteractionEngine::innerBoundedCSSScale(qreal cssScale) +{ + return qBound(m_constraints.minimumScale, cssScale, m_constraints.maximumScale); +} + +qreal QtViewportInteractionEngine::outerBoundedCSSScale(qreal cssScale) +{ + if (m_constraints.isUserScalable) { + // Bounded by [0.1, 10.0] like the viewport meta code in WebCore. + qreal hardMin = qMax<qreal>(0.1, qreal(0.5) * m_constraints.minimumScale); + qreal hardMax = qMin<qreal>(10, qreal(2.0) * m_constraints.maximumScale); + return qBound(hardMin, cssScale, hardMax); + } + return innerBoundedCSSScale(cssScale); +} + +void QtViewportInteractionEngine::setItemRectVisible(const QRectF& itemRect) +{ + ViewportUpdateDeferrer guard(this); + + qreal itemScale = m_viewport->width() / itemRect.width(); + + m_content->setScale(itemScale); + + // We need to animate the content but the position represents the viewport hence we need to invert the position here. + // To animate the position together with the scale we multiply the position with the current scale; + m_content->setPos(- itemRect.topLeft() * itemScale); +} + +bool QtViewportInteractionEngine::animateItemRectVisible(const QRectF& itemRect) +{ + QRectF currentItemRectVisible = m_content->mapRectFromItem(m_viewport, m_viewport->boundingRect()); + if (itemRect == currentItemRectVisible) + return false; + + m_scaleAnimation->setDuration(kScaleAnimationDurationMillis); + m_scaleAnimation->setEasingCurve(QEasingCurve::OutCubic); + + m_scaleAnimation->setStartValue(currentItemRectVisible); + m_scaleAnimation->setEndValue(itemRect); + + m_scaleAnimation->start(); + return true; +} + +void QtViewportInteractionEngine::scaleAnimationStateChanged(QAbstractAnimation::State newState, QAbstractAnimation::State /*oldState*/) +{ + switch (newState) { + case QAbstractAnimation::Running: + if (!m_scaleUpdateDeferrer) + m_scaleUpdateDeferrer = adoptPtr(new ViewportUpdateDeferrer(this)); + break; + case QAbstractAnimation::Stopped: + m_scaleUpdateDeferrer.clear(); + break; + default: + break; + } +} + +void QtViewportInteractionEngine::scrollStateChanged(QScroller::State newState) +{ + switch (newState) { + case QScroller::Inactive: + // FIXME: QScroller gets when even when tapping while it is scrolling. + m_scrollUpdateDeferrer.clear(); + break; + case QScroller::Pressed: + case QScroller::Dragging: + case QScroller::Scrolling: + if (m_scrollUpdateDeferrer) + break; + m_scrollUpdateDeferrer = adoptPtr(new ViewportUpdateDeferrer(this)); + break; + default: + break; + } +} + +bool QtViewportInteractionEngine::event(QEvent* event) +{ + switch (event->type()) { + case QEvent::ScrollPrepare: { + QScrollPrepareEvent* prepareEvent = static_cast<QScrollPrepareEvent*>(event); + const QRectF viewportRect = m_viewport->boundingRect(); + const QRectF contentRect = m_viewport->mapRectFromItem(m_content, m_content->boundingRect()); + const QRectF posRange = computePosRangeForItemAtScale(m_content->scale()); + prepareEvent->setContentPosRange(posRange); + prepareEvent->setViewportSize(viewportRect.size()); + + // As we want to push the contents and not actually scroll it, we need to invert the positions here. + prepareEvent->setContentPos(-contentRect.topLeft()); + prepareEvent->accept(); + return true; + } + case QEvent::Scroll: { + QScrollEvent* scrollEvent = static_cast<QScrollEvent*>(event); + QPointF newPos = -scrollEvent->contentPos() - scrollEvent->overshootDistance(); + if (m_content->pos() != newPos) { + QPointF currentPosInContentCoordinates = m_content->mapToItem(m_content->parentItem(), m_content->pos()); + QPointF newPosInContentCoordinates = m_content->mapToItem(m_content->parentItem(), newPos); + + // This must be emitted before viewportUpdateRequested so that the web process knows where to look for tiles. + emit viewportTrajectoryVectorChanged(currentPosInContentCoordinates- newPosInContentCoordinates); + m_content->setPos(newPos); + } + return true; + } + default: + break; + } + return QObject::event(event); +} + +static inline QPointF boundPosition(const QPointF minPosition, const QPointF& position, const QPointF& maxPosition) +{ + return QPointF(qBound(minPosition.x(), position.x(), maxPosition.x()), + qBound(minPosition.y(), position.y(), maxPosition.y())); +} + +void QtViewportInteractionEngine::wheelEvent(QWheelEvent* ev) +{ + if (scrollAnimationActive() || scaleAnimationActive() || pinchGestureActive()) + return; // Ignore. + + int delta = ev->delta(); + QPointF newPos = -m_content->pos(); + + // A delta that is not mod 120 indicates a device that is sending + // fine-resolution scroll events, so use the delta as number of wheel ticks + // and number of pixels to scroll. See also webkit.org/b/29601 + bool fullTick = !(delta % 120); + + static const int cDefaultQtScrollStep = 20; + static const int wheelScrollLines = 3; + int scrollLines = (fullTick) ? wheelScrollLines * cDefaultQtScrollStep : 1; + + delta = (fullTick) ? delta / 120.0f : delta; + delta *= scrollLines; + + if (ev->orientation() == Qt::Horizontal) + newPos.rx() += delta; + else + newPos.ry() += delta; + + QRectF endPosRange = computePosRangeForItemAtScale(m_content->scale()); + m_content->setPos(-boundPosition(endPosRange.topLeft(), newPos, endPosRange.bottomRight())); +} + +void QtViewportInteractionEngine::pagePositionRequest(const QPoint& pagePosition) +{ + // Ignore the request if suspended. Can only happen due to delay in event delivery. + if (m_suspendCount) + return; + + qreal endItemScale = m_content->scale(); // Stay at same scale. + + QRectF endPosRange = computePosRangeForItemAtScale(endItemScale); + QPointF endPosition = boundPosition(endPosRange.topLeft(), pagePosition * endItemScale, endPosRange.bottomRight()); + + QRectF endVisibleContentRect(endPosition / endItemScale, m_viewport->boundingRect().size() / endItemScale); + + setItemRectVisible(endVisibleContentRect); +} + +QRectF QtViewportInteractionEngine::computePosRangeForItemAtScale(qreal itemScale) const +{ + const QSizeF contentItemSize = m_content->boundingRect().size() * itemScale; + const QSizeF viewportItemSize = m_viewport->boundingRect().size(); + + const qreal horizontalRange = contentItemSize.width() - viewportItemSize.width(); + const qreal verticalRange = contentItemSize.height() - viewportItemSize.height(); + + return QRectF(QPointF(0, 0), QSizeF(horizontalRange, verticalRange)); +} + +void QtViewportInteractionEngine::focusEditableArea(const QRectF& caretArea, const QRectF& targetArea) +{ + QRectF endArea = itemRectFromCSS(targetArea); + + qreal endItemScale = itemScaleFromCSS(innerBoundedCSSScale(2.0)); + const QRectF viewportRect = m_viewport->boundingRect(); + + qreal x; + const qreal borderOffset = 10; + if ((endArea.width() + borderOffset) * endItemScale <= viewportRect.width()) { + // Center the input field in the middle of the view, if it is smaller than + // the view at the scale target. + x = viewportRect.center().x() - endArea.width() * endItemScale / 2.0; + } else { + // Ensure that the caret always has borderOffset contents pixels to the right + // of it, and secondarily (if possible), that the area has borderOffset + // contents pixels to the left of it. + qreal caretOffset = itemCoordFromCSS(caretArea.x()) - endArea.x(); + x = qMin(viewportRect.width() - (caretOffset + borderOffset) * endItemScale, borderOffset * endItemScale); + } + + const QPointF hotspot = QPointF(endArea.x(), endArea.center().y()); + const QPointF viewportHotspot = QPointF(x, /* FIXME: visibleCenter */ viewportRect.center().y()); + + QPointF endPosition = hotspot * endItemScale - viewportHotspot; + QRectF endPosRange = computePosRangeForItemAtScale(endItemScale); + + endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight()); + + QRectF endVisibleContentRect(endPosition / endItemScale, viewportRect.size() / endItemScale); + + animateItemRectVisible(endVisibleContentRect); +} + +void QtViewportInteractionEngine::zoomToAreaGestureEnded(const QPointF& touchPoint, const QRectF& targetArea) +{ + if (!targetArea.isValid()) + return; + + if (scrollAnimationActive() || scaleAnimationActive()) + return; + + const int margin = 10; // We want at least a little bit or margin. + QRectF endArea = itemRectFromCSS(targetArea.adjusted(-margin, -margin, margin, margin)); + + const QRectF viewportRect = m_viewport->boundingRect(); + + qreal targetCSSScale = cssScaleFromItem(viewportRect.size().width() / endArea.size().width()); + qreal endItemScale = itemScaleFromCSS(innerBoundedCSSScale(qMin(targetCSSScale, qreal(2.5)))); + + // We want to end up with the target area filling the whole width of the viewport (if possible), + // and centralized vertically where the user requested zoom. Thus our hotspot is the center of + // the targetArea x-wise and the requested zoom position, y-wise. + const QPointF hotspot = QPointF(endArea.center().x(), touchPoint.y() * m_constraints.devicePixelRatio); + const QPointF viewportHotspot = viewportRect.center(); + + QPointF endPosition = hotspot * endItemScale - viewportHotspot; + + QRectF endPosRange = computePosRangeForItemAtScale(endItemScale); + endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight()); + + QRectF endVisibleContentRect(endPosition / endItemScale, viewportRect.size() / endItemScale); + + animateItemRectVisible(endVisibleContentRect); +} + +bool QtViewportInteractionEngine::ensureContentWithinViewportBoundary(bool immediate) +{ + ASSERT(m_suspendCount); + + if (scrollAnimationActive() || scaleAnimationActive()) + return false; + + qreal endItemScale = itemScaleFromCSS(innerBoundedCSSScale(currentCSSScale())); + + const QRectF viewportRect = m_viewport->boundingRect(); + QPointF viewportHotspot = viewportRect.center(); + + QPointF endPosition = m_content->mapFromItem(m_viewport, viewportHotspot) * endItemScale - viewportHotspot; + + QRectF endPosRange = computePosRangeForItemAtScale(endItemScale); + endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight()); + + QRectF endVisibleContentRect(endPosition / endItemScale, viewportRect.size() / endItemScale); + + if (immediate) { + setItemRectVisible(endVisibleContentRect); + return true; + } + return !animateItemRectVisible(endVisibleContentRect); +} + +void QtViewportInteractionEngine::reset() +{ + ASSERT(!m_suspendCount); + + m_hadUserInteraction = false; + + scroller()->stop(); + m_scaleAnimation->stop(); +} + +void QtViewportInteractionEngine::applyConstraints(const Constraints& constraints) +{ + // We always have to apply the constrains even if they didn't change, as + // the initial scale might need to be applied. + + ViewportUpdateDeferrer guard(this); + + m_constraints = constraints; + + if (!m_hadUserInteraction) { + qreal initialScale = innerBoundedCSSScale(m_constraints.initialScale); + m_content->setScale(itemScaleFromCSS(initialScale)); + } + + // If the web app changes successively changes the viewport on purpose + // it wants to be in control and we should disable animations. + ensureContentWithinViewportBoundary(/* immediate */ true); +} + +qreal QtViewportInteractionEngine::currentCSSScale() +{ + return cssScaleFromItem(m_content->scale()); +} + +bool QtViewportInteractionEngine::scrollAnimationActive() const +{ + QScroller* scroller = const_cast<QtViewportInteractionEngine*>(this)->scroller(); + return scroller->state() == QScroller::Scrolling; +} + +void QtViewportInteractionEngine::interruptScrollAnimation() +{ + // Stopping the scroller immediately stops kinetic scrolling and if the view is out of bounds it + // is moved inside valid bounds immediately as well. This is the behavior that we want. + scroller()->stop(); +} + +bool QtViewportInteractionEngine::panGestureActive() const +{ + QScroller* scroller = const_cast<QtViewportInteractionEngine*>(this)->scroller(); + return scroller->state() == QScroller::Pressed || scroller->state() == QScroller::Dragging; +} + +void QtViewportInteractionEngine::panGestureStarted(const QPointF& touchPoint, qint64 eventTimestampMillis) +{ + m_hadUserInteraction = true; + scroller()->handleInput(QScroller::InputPress, m_viewport->mapFromItem(m_content, touchPoint), eventTimestampMillis); +} + +void QtViewportInteractionEngine::panGestureRequestUpdate(const QPointF& touchPoint, qint64 eventTimestampMillis) +{ + scroller()->handleInput(QScroller::InputMove, m_viewport->mapFromItem(m_content, touchPoint), eventTimestampMillis); +} + +void QtViewportInteractionEngine::panGestureCancelled() +{ + // Stopping the scroller immediately stops kinetic scrolling and if the view is out of bounds it + // is moved inside valid bounds immediately as well. This is the behavior that we want. + scroller()->stop(); +} + +void QtViewportInteractionEngine::panGestureEnded(const QPointF& touchPoint, qint64 eventTimestampMillis) +{ + scroller()->handleInput(QScroller::InputRelease, m_viewport->mapFromItem(m_content, touchPoint), eventTimestampMillis); +} + +bool QtViewportInteractionEngine::scaleAnimationActive() const +{ + return m_scaleAnimation->state() == QAbstractAnimation::Running; +} + +void QtViewportInteractionEngine::interruptScaleAnimation() +{ + // This interrupts the scale animation exactly where it is, even if it is out of bounds. + m_scaleAnimation->stop(); +} + +bool QtViewportInteractionEngine::pinchGestureActive() const +{ + return m_pinchStartScale > 0; +} + +void QtViewportInteractionEngine::pinchGestureStarted(const QPointF& pinchCenterInContentCoordinates) +{ + ASSERT(!m_suspendCount); + + if (!m_constraints.isUserScalable) + return; + + m_hadUserInteraction = true; + + m_scaleUpdateDeferrer = adoptPtr(new ViewportUpdateDeferrer(this)); + + m_lastPinchCenterInViewportCoordinates = m_viewport->mapFromItem(m_content, pinchCenterInContentCoordinates); + m_pinchStartScale = m_content->scale(); + + // Reset the tiling look-ahead vector so that tiles all around the viewport will be requested on pinch-end. + emit viewportTrajectoryVectorChanged(QPointF()); +} + +void QtViewportInteractionEngine::pinchGestureRequestUpdate(const QPointF& pinchCenterInContentCoordinates, qreal totalScaleFactor) +{ + ASSERT(m_suspendCount); + + if (!m_constraints.isUserScalable) + return; + + // Changes of the center position should move the page even if the zoom factor + // does not change. + const qreal cssScale = cssScaleFromItem(m_pinchStartScale * totalScaleFactor); + + // Allow zooming out beyond mimimum scale on pages that do not explicitly disallow it. + const qreal targetCSSScale = outerBoundedCSSScale(cssScale); + + QPointF pinchCenterInViewportCoordinates = m_viewport->mapFromItem(m_content, pinchCenterInContentCoordinates); + + scaleContent(pinchCenterInContentCoordinates, targetCSSScale); + + const QPointF positionDiff = pinchCenterInViewportCoordinates - m_lastPinchCenterInViewportCoordinates; + m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates; + m_content->setPos(m_content->pos() + positionDiff); +} + +void QtViewportInteractionEngine::pinchGestureEnded() +{ + ASSERT(m_suspendCount); + + if (!m_constraints.isUserScalable) + return; + + m_pinchStartScale = -1; + // Clear the update deferrer now if we're in our final position and there won't be any animation to clear it later. + if (ensureContentWithinViewportBoundary()) + m_scaleUpdateDeferrer.clear(); +} + +void QtViewportInteractionEngine::itemSizeChanged() +{ + // FIXME: This needs to be done smarter. What happens if it resizes when we were interacting? + if (m_suspendCount) + return; + + ViewportUpdateDeferrer guard(this); + ensureContentWithinViewportBoundary(); +} + +void QtViewportInteractionEngine::scaleContent(const QPointF& centerInContentCoordinates, qreal cssScale) +{ + QPointF oldPinchCenterOnParent = m_content->mapToItem(m_content->parentItem(), centerInContentCoordinates); + m_content->setScale(itemScaleFromCSS(cssScale)); + QPointF newPinchCenterOnParent = m_content->mapToItem(m_content->parentItem(), centerInContentCoordinates); + m_content->setPos(m_content->pos() - (newPinchCenterOnParent - oldPinchCenterOnParent)); +} + +#include "moc_QtViewportInteractionEngine.cpp" + +} |