QtViewportInteractionEngine.cpp [plain text]
#include "config.h"
#include "QtViewportInteractionEngine.h"
#include "qquickwebpage_p.h"
#include "qquickwebview_p.h"
#include <QPointF>
#include <QTransform>
#include <QWheelEvent>
#include <QtQuick/qquickitem.h>
#include <wtf/PassOwnPtr.h>
namespace WebKit {
static const int kScaleAnimationDurationMillis = 250;
class ViewportUpdateDeferrer {
public:
enum SuspendContentFlag { DeferUpdate, DeferUpdateAndSuspendContent };
ViewportUpdateDeferrer(QtViewportInteractionEngine* engine, SuspendContentFlag suspendContentFlag = DeferUpdate)
: engine(engine)
{
engine->m_suspendCount++;
if (suspendContentFlag == DeferUpdateAndSuspendContent && !engine->m_hasSuspendedContent) {
engine->m_hasSuspendedContent = true;
emit engine->contentSuspendRequested();
}
}
~ViewportUpdateDeferrer()
{
if (--(engine->m_suspendCount))
return;
if (engine->m_hasSuspendedContent) {
engine->m_hasSuspendedContent = false;
emit engine->contentResumeRequested();
}
emit engine->contentViewportChanged(QPointF());
}
private:
QtViewportInteractionEngine* const engine;
};
static inline bool fuzzyCompare(qreal a, qreal b, qreal epsilon)
{
return qAbs(a - b) < epsilon;
}
inline qreal QtViewportInteractionEngine::cssScaleFromItem(qreal itemScale)
{
return itemScale / m_devicePixelRatio;
}
inline qreal QtViewportInteractionEngine::itemScaleFromCSS(qreal cssScale)
{
return cssScale * m_devicePixelRatio;
}
inline qreal QtViewportInteractionEngine::itemCoordFromCSS(qreal value)
{
return value * m_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(QQuickWebView* viewport, QQuickWebPage* content)
: m_viewport(viewport)
, m_content(content)
, m_suspendCount(0)
, m_hasSuspendedContent(false)
, m_hadUserInteraction(false)
, m_scaleAnimation(new ScaleAnimation(this))
, m_pinchStartScale(-1)
, m_zoomOutScale(0.0)
{
reset();
connect(m_content, SIGNAL(widthChanged()), SLOT(itemSizeChanged()), Qt::DirectConnection);
connect(m_content, SIGNAL(heightChanged()), SLOT(itemSizeChanged()), Qt::DirectConnection);
connect(m_viewport, SIGNAL(movementStarted()), SLOT(flickableMoveStarted()), Qt::DirectConnection);
connect(m_viewport, SIGNAL(movementEnded()), SLOT(flickableMoveEnded()), 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);
}
QtViewportInteractionEngine::~QtViewportInteractionEngine()
{
}
qreal QtViewportInteractionEngine::innerBoundedCSSScale(qreal cssScale)
{
return qBound(m_minimumScale, cssScale, m_maximumScale);
}
qreal QtViewportInteractionEngine::outerBoundedCSSScale(qreal cssScale)
{
if (m_allowsUserScaling) {
qreal hardMin = qMax<qreal>(0.1, qreal(0.5) * m_minimumScale);
qreal hardMax = qMin<qreal>(10, qreal(2.0) * m_maximumScale);
return qBound(hardMin, cssScale, hardMax);
}
return innerBoundedCSSScale(cssScale);
}
void QtViewportInteractionEngine::setItemRectVisible(const QRectF& itemRect)
{
if (itemRect.isEmpty())
return;
ViewportUpdateDeferrer guard(this);
qreal itemScale = m_viewport->width() / itemRect.width();
m_content->setContentsScale(itemScale);
QPointF newPosition(m_content->pos() + (itemRect.topLeft() * itemScale));
m_viewport->setContentPos(newPosition);
}
bool QtViewportInteractionEngine::animateItemRectVisible(const QRectF& itemRect)
{
QRectF currentItemRectVisible = m_viewport->mapRectToWebContent(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::flickableMoveStarted()
{
Q_ASSERT(m_viewport->isMoving());
m_scrollUpdateDeferrer = adoptPtr(new ViewportUpdateDeferrer(this, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent));
m_lastScrollPosition = m_viewport->contentPos();
connect(m_viewport, SIGNAL(contentXChanged()), SLOT(flickableMovingPositionUpdate()));
connect(m_viewport, SIGNAL(contentYChanged()), SLOT(flickableMovingPositionUpdate()));
}
void QtViewportInteractionEngine::flickableMoveEnded()
{
Q_ASSERT(!m_viewport->isMoving());
m_scrollUpdateDeferrer.clear();
m_lastScrollPosition = QPointF();
disconnect(m_viewport, SIGNAL(contentXChanged()), this, SLOT(flickableMovingPositionUpdate()));
disconnect(m_viewport, SIGNAL(contentYChanged()), this, SLOT(flickableMovingPositionUpdate()));
}
void QtViewportInteractionEngine::flickableMovingPositionUpdate()
{
QPointF newPosition = m_viewport->contentPos();
emit contentViewportChanged(m_lastScrollPosition - newPosition);
m_lastScrollPosition = newPosition;
}
void QtViewportInteractionEngine::scaleAnimationStateChanged(QAbstractAnimation::State newState, QAbstractAnimation::State )
{
switch (newState) {
case QAbstractAnimation::Running:
m_viewport->cancelFlick();
if (!m_scaleUpdateDeferrer)
m_scaleUpdateDeferrer = adoptPtr(new ViewportUpdateDeferrer(this, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent));
break;
case QAbstractAnimation::Stopped:
m_scaleUpdateDeferrer.clear();
break;
default:
break;
}
}
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;
static const int cDefaultQtScrollStep = 20;
static const int wheelScrollLines = 3;
const int wheelTick = wheelScrollLines * cDefaultQtScrollStep;
int pixelDelta = ev->delta() * (wheelTick / 120.f);
QPointF newPosition = m_viewport->contentPos();
if (ev->orientation() == Qt::Horizontal)
newPosition.rx() -= pixelDelta;
else
newPosition.ry() -= pixelDelta;
QRectF endPosRange = computePosRangeForItemAtScale(m_content->contentsScale());
QPointF currentPosition = m_viewport->contentPos();
newPosition = boundPosition(endPosRange.topLeft(), newPosition, endPosRange.bottomRight());
m_viewport->setContentPos(newPosition);
emit contentViewportChanged(currentPosition - newPosition);
}
void QtViewportInteractionEngine::pagePositionRequest(const QPoint& pagePosition)
{
if (m_suspendCount)
return;
qreal endItemScale = m_content->contentsScale();
QRectF endPosRange = computePosRangeForItemAtScale(endItemScale);
QPointF endPosition = boundPosition(endPosRange.topLeft(), pagePosition * endItemScale, endPosRange.bottomRight());
QRectF endVisibleContentRect(endPosition / endItemScale, m_viewport->boundingRect().size() / endItemScale);
setItemRectVisible(endVisibleContentRect);
}
void QtViewportInteractionEngine::touchBegin()
{
if (scrollAnimationActive())
m_touchUpdateDeferrer = adoptPtr(new ViewportUpdateDeferrer(this, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent));
}
void QtViewportInteractionEngine::touchEnd()
{
m_touchUpdateDeferrer.clear();
}
QRectF QtViewportInteractionEngine::computePosRangeForItemAtScale(qreal itemScale) const
{
const QSizeF contentItemSize = m_content->contentsSize() * 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()) {
x = viewportRect.center().x() - endArea.width() * endItemScale / 2.0;
} else {
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, 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; 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))));
qreal currentScale = m_content->contentsScale();
const QPointF hotspot = QPointF(endArea.center().x(), touchPoint.y() * m_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);
enum { ZoomIn, ZoomBack, ZoomOut, NoZoom } zoomAction = ZoomIn;
if (!m_scaleStack.isEmpty()) {
if (fuzzyCompare(endItemScale, currentScale, 0.01)) {
QRectF currentContentRect(m_viewport->contentPos() / currentScale, viewportRect.size() / currentScale);
QRectF targetIntersection = endVisibleContentRect.intersected(targetArea);
if (!currentContentRect.contains(targetIntersection) && (qAbs(endVisibleContentRect.top() - currentContentRect.top()) >= 40 || qAbs(endVisibleContentRect.left() - currentContentRect.left()) >= 40))
zoomAction = NoZoom;
else
zoomAction = ZoomBack;
} else if (fuzzyCompare(endItemScale, m_zoomOutScale, 0.01))
zoomAction = ZoomBack;
else if (endItemScale < currentScale)
zoomAction = ZoomOut;
}
switch (zoomAction) {
case ZoomIn:
m_scaleStack.append(ScaleStackItem(currentScale, m_viewport->contentPos().x()));
m_zoomOutScale = endItemScale;
break;
case ZoomBack: {
ScaleStackItem lastScale = m_scaleStack.takeLast();
endItemScale = lastScale.scale;
endPosition.setY(hotspot.y() * endItemScale - viewportHotspot.y());
endPosition.setX(lastScale.xPosition);
endPosRange = computePosRangeForItemAtScale(endItemScale);
endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight());
endVisibleContentRect = QRectF(endPosition / endItemScale, viewportRect.size() / endItemScale);
break;
}
case ZoomOut:
while (!m_scaleStack.isEmpty() && m_scaleStack.last().scale >= endItemScale)
m_scaleStack.removeLast();
m_zoomOutScale = endItemScale;
break;
case NoZoom:
break;
}
animateItemRectVisible(endVisibleContentRect);
}
bool QtViewportInteractionEngine::ensureContentWithinViewportBoundary(bool immediate)
{
if (scrollAnimationActive() || scaleAnimationActive())
return false;
qreal endItemScale = itemScaleFromCSS(innerBoundedCSSScale(currentCSSScale()));
const QRectF viewportRect = m_viewport->boundingRect();
QPointF viewportHotspot = viewportRect.center();
QPointF endPosition = m_viewport->mapToWebContent(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;
m_allowsUserScaling = false;
m_minimumScale = 1;
m_maximumScale = 1;
m_devicePixelRatio = 1;
m_pinchStartScale = -1;
m_zoomOutScale = 0.0;
m_viewport->cancelFlick();
m_scaleAnimation->stop();
m_scaleUpdateDeferrer.clear();
m_scrollUpdateDeferrer.clear();
m_scaleStack.clear();
}
void QtViewportInteractionEngine::setCSSScaleBounds(qreal minimum, qreal maximum)
{
m_minimumScale = minimum;
m_maximumScale = maximum;
}
void QtViewportInteractionEngine::setCSSScale(qreal scale)
{
ViewportUpdateDeferrer guard(this);
qreal newScale = innerBoundedCSSScale(scale);
m_content->setContentsScale(itemScaleFromCSS(newScale));
}
qreal QtViewportInteractionEngine::currentCSSScale()
{
return cssScaleFromItem(m_content->contentsScale());
}
bool QtViewportInteractionEngine::scrollAnimationActive() const
{
return m_viewport->isFlicking();
}
bool QtViewportInteractionEngine::panGestureActive() const
{
return m_viewport->isDragging();
}
void QtViewportInteractionEngine::panGestureStarted(const QPointF& position, qint64 eventTimestampMillis)
{
m_hadUserInteraction = true;
m_viewport->handleFlickableMousePress(position, eventTimestampMillis);
m_lastPinchCenterInViewportCoordinates = position;
}
void QtViewportInteractionEngine::panGestureRequestUpdate(const QPointF& position, qint64 eventTimestampMillis)
{
m_viewport->handleFlickableMouseMove(position, eventTimestampMillis);
m_lastPinchCenterInViewportCoordinates = position;
}
void QtViewportInteractionEngine::panGestureEnded(const QPointF& position, qint64 eventTimestampMillis)
{
m_viewport->handleFlickableMouseRelease(position, eventTimestampMillis);
m_lastPinchCenterInViewportCoordinates = position;
}
void QtViewportInteractionEngine::panGestureCancelled()
{
m_viewport->cancelFlick();
}
bool QtViewportInteractionEngine::scaleAnimationActive() const
{
return m_scaleAnimation->state() == QAbstractAnimation::Running;
}
void QtViewportInteractionEngine::cancelScrollAnimation()
{
ViewportUpdateDeferrer guard(this);
m_viewport->cancelFlick();
ensureContentWithinViewportBoundary( true);
}
void QtViewportInteractionEngine::interruptScaleAnimation()
{
m_scaleAnimation->stop();
}
bool QtViewportInteractionEngine::pinchGestureActive() const
{
return m_pinchStartScale > 0;
}
void QtViewportInteractionEngine::pinchGestureStarted(const QPointF& pinchCenterInViewportCoordinates)
{
if (!m_allowsUserScaling)
return;
m_hadUserInteraction = true;
m_scaleStack.clear();
m_zoomOutScale = 0.0;
m_scaleUpdateDeferrer = adoptPtr(new ViewportUpdateDeferrer(this, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent));
m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates;
m_pinchStartScale = m_content->contentsScale();
emit contentViewportChanged(QPointF());
}
void QtViewportInteractionEngine::pinchGestureRequestUpdate(const QPointF& pinchCenterInViewportCoordinates, qreal totalScaleFactor)
{
ASSERT(m_suspendCount);
if (!m_allowsUserScaling)
return;
const qreal cssScale = cssScaleFromItem(m_pinchStartScale * totalScaleFactor);
const qreal targetCSSScale = outerBoundedCSSScale(cssScale);
scaleContent(m_viewport->mapToWebContent(pinchCenterInViewportCoordinates), targetCSSScale);
const QPointF positionDiff = pinchCenterInViewportCoordinates - m_lastPinchCenterInViewportCoordinates;
m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates;
m_viewport->setContentPos(m_viewport->contentPos() - positionDiff);
}
void QtViewportInteractionEngine::pinchGestureEnded()
{
ASSERT(m_suspendCount);
if (!m_allowsUserScaling)
return;
m_pinchStartScale = -1;
if (ensureContentWithinViewportBoundary())
m_scaleUpdateDeferrer.clear();
}
void QtViewportInteractionEngine::pinchGestureCancelled()
{
m_pinchStartScale = -1;
m_scaleUpdateDeferrer.clear();
}
void QtViewportInteractionEngine::itemSizeChanged()
{
if (m_suspendCount)
return;
ViewportUpdateDeferrer guard(this);
ensureContentWithinViewportBoundary(true);
}
void QtViewportInteractionEngine::scaleContent(const QPointF& centerInCSSCoordinates, qreal cssScale)
{
QPointF oldPinchCenterOnViewport = m_viewport->mapFromWebContent(centerInCSSCoordinates);
m_content->setContentsScale(itemScaleFromCSS(cssScale));
QPointF newPinchCenterOnViewport = m_viewport->mapFromWebContent(centerInCSSCoordinates);
m_viewport->setContentPos(m_viewport->contentPos() + (newPinchCenterOnViewport - oldPinchCenterOnViewport));
}
}
#include "moc_QtViewportInteractionEngine.cpp"