/* * Copyright (C) 2011, 2012 Nokia Corporation and/or its subsidiary(-ies) * Copyright (C) 2011 Benjamin Poulain * * 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 "PageViewportControllerClientQt.h" #include "qquickwebpage_p.h" #include "qquickwebview_p.h" #include "qwebkittest_p.h" #include #include #include #include #include namespace WebKit { static const int kScaleAnimationDurationMillis = 250; PageViewportControllerClientQt::PageViewportControllerClientQt(QQuickWebView* viewportItem, QQuickWebPage* pageItem) : m_viewportItem(viewportItem) , m_pageItem(pageItem) , m_scaleAnimation(new ScaleAnimation(this)) , m_pinchStartScale(-1) , m_lastCommittedScale(-1) , m_zoomOutScale(0) , m_ignoreViewportChanges(true) { m_scaleAnimation->setDuration(kScaleAnimationDurationMillis); m_scaleAnimation->setEasingCurve(QEasingCurve::OutCubic); connect(m_viewportItem, SIGNAL(movementStarted()), SLOT(flickMoveStarted()), Qt::DirectConnection); connect(m_viewportItem, SIGNAL(movementEnded()), SLOT(flickMoveEnded()), Qt::DirectConnection); connect(m_viewportItem, SIGNAL(contentXChanged()), SLOT(pageItemPositionChanged())); connect(m_viewportItem, SIGNAL(contentYChanged()), SLOT(pageItemPositionChanged())); connect(m_scaleAnimation, SIGNAL(stateChanged(QAbstractAnimation::State, QAbstractAnimation::State)), SLOT(scaleAnimationStateChanged(QAbstractAnimation::State, QAbstractAnimation::State))); } void PageViewportControllerClientQt::ScaleAnimation::updateCurrentValue(const QVariant& value) { // Resetting the end value, the easing curve or the duration of the scale animation // triggers a recalculation of the animation interval. This might change the current // value of the animated property. // Make sure we only act on animation value changes if the animation is active. if (!m_controllerClient->scaleAnimationActive()) return; QRectF itemRect = value.toRectF(); float itemScale = m_controllerClient->viewportScaleForRect(itemRect); m_controllerClient->setContentRectVisiblePositionAtScale(itemRect.topLeft(), itemScale); } PageViewportControllerClientQt::~PageViewportControllerClientQt() { } void PageViewportControllerClientQt::setContentRectVisiblePositionAtScale(const QPointF& location, qreal itemScale) { ASSERT(itemScale >= 0); scaleContent(itemScale); // To animate the position together with the scale we multiply the position with the current scale // and add it to the page position (displacement on the flickable contentItem because of additional items). QPointF newPosition(m_pageItem->pos() + location * itemScale); m_viewportItem->setContentPos(newPosition); } void PageViewportControllerClientQt::animateContentRectVisible(const QRectF& contentRect) { ASSERT(m_scaleAnimation->state() == QAbstractAnimation::Stopped); ASSERT(!scrollAnimationActive()); if (scrollAnimationActive()) return; QRectF viewportRectInContentCoords = m_viewportItem->mapRectToWebContent(m_viewportItem->boundingRect()); if (contentRect == viewportRectInContentCoords) { updateViewportController(); return; } // Since we have to animate scale and position at the same time the scale animation interpolates // from the current viewport rect in content coordinates to a visible rect of the content. m_scaleAnimation->setStartValue(viewportRectInContentCoords); m_scaleAnimation->setEndValue(contentRect); // Inform the web process about the requested visible content rect // if zooming-out so that no white tiles are exposed during animation. if (viewportRectInContentCoords.width() / contentRect.width() < m_pageItem->contentsScale()) m_controller->setVisibleContentsRect(contentRect, viewportScaleForRect(contentRect)); m_scaleAnimation->start(); } void PageViewportControllerClientQt::flickMoveStarted() { Q_ASSERT(m_viewportItem->isMoving()); m_scrollUpdateDeferrer.reset(new ViewportUpdateDeferrer(m_controller, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent)); m_lastScrollPosition = m_viewportItem->contentPos(); m_ignoreViewportChanges = false; } void PageViewportControllerClientQt::flickMoveEnded() { Q_ASSERT(!m_viewportItem->isMoving()); // This method is called on the end of the pan or pan kinetic animation. m_ignoreViewportChanges = true; m_scrollUpdateDeferrer.reset(); } void PageViewportControllerClientQt::pageItemPositionChanged() { if (m_ignoreViewportChanges) return; QPointF newPosition = m_viewportItem->contentPos(); updateViewportController(m_lastScrollPosition - newPosition); m_lastScrollPosition = newPosition; } void PageViewportControllerClientQt::scaleAnimationStateChanged(QAbstractAnimation::State newState, QAbstractAnimation::State /*oldState*/) { switch (newState) { case QAbstractAnimation::Running: m_viewportItem->cancelFlick(); ASSERT(!m_animationUpdateDeferrer); m_animationUpdateDeferrer.reset(new ViewportUpdateDeferrer(m_controller, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent)); break; case QAbstractAnimation::Stopped: m_animationUpdateDeferrer.reset(); break; default: break; } } void PageViewportControllerClientQt::touchBegin() { m_controller->setHadUserInteraction(true); // Prevents resuming the page between the user's flicks of the page while the animation is running. if (scrollAnimationActive()) m_touchUpdateDeferrer.reset(new ViewportUpdateDeferrer(m_controller, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent)); } void PageViewportControllerClientQt::touchEnd() { m_touchUpdateDeferrer.reset(); } void PageViewportControllerClientQt::focusEditableArea(const QRectF& caretArea, const QRectF& targetArea) { // This can only happen as a result of a user interaction. ASSERT(m_controller->hadUserInteraction()); QRectF endArea = m_controller->convertToViewport(targetArea); qreal endItemScale = m_controller->convertToViewport(m_controller->innerBoundedContentsScale(2.0)); const QRectF viewportRect = m_viewportItem->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 = m_controller->convertToViewport(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 = m_controller->positionRangeForContentAtScale(endItemScale); endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight()); QRectF endVisibleContentRect(endPosition / endItemScale, viewportRect.size() / endItemScale); animateContentRectVisible(endVisibleContentRect); } void PageViewportControllerClientQt::zoomToAreaGestureEnded(const QPointF& touchPoint, const QRectF& targetArea) { // This can only happen as a result of a user interaction. ASSERT(m_controller->hadUserInteraction()); if (!targetArea.isValid()) return; if (m_controller->hasSuspendedContent()) return; const int margin = 10; // We want at least a little bit of margin. QRectF endArea = m_controller->convertToViewport(targetArea.adjusted(-margin, -margin, margin, margin)); const QRectF viewportRect = m_viewportItem->boundingRect(); qreal targetCSSScale = viewportRect.size().width() / endArea.size().width(); qreal endCSSScale = m_controller->innerBoundedContentsScale(qMin(targetCSSScale, qreal(2.5))); qreal endItemScale = m_controller->convertToViewport(endCSSScale); qreal currentScale = m_pageItem->contentsScale(); // 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(), m_controller->convertToViewport(touchPoint.y())); const QPointF viewportHotspot = viewportRect.center(); QPointF endPosition = hotspot * endCSSScale - viewportHotspot; QRectF endPosRange = m_controller->positionRangeForContentAtScale(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()) { // Zoom back out if attempting to scale to the same current scale, or // attempting to continue scaling out from the inner most level. // Use fuzzy compare with a fixed error to be able to deal with largish differences due to pixel rounding. if (fuzzyCompare(endItemScale, currentScale, 0.01)) { // If moving the viewport would expose more of the targetRect and move at least 40 pixels, update position but do not scale out. QRectF currentContentRect(visibleContentsRect()); 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_viewportItem->contentPos().x())); m_zoomOutScale = endItemScale; break; case ZoomBack: { ScaleStackItem lastScale = m_scaleStack.takeLast(); endItemScale = lastScale.scale; endCSSScale = m_controller->convertFromViewport(lastScale.scale); // Recalculate endPosition and bound it according to new scale. endPosition.setY(hotspot.y() * endCSSScale - viewportHotspot.y()); endPosition.setX(lastScale.xPosition); endPosRange = m_controller->positionRangeForContentAtScale(endItemScale); endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight()); endVisibleContentRect = QRectF(endPosition / endItemScale, viewportRect.size() / endItemScale); break; } case ZoomOut: // Unstack all scale-levels deeper than the new level, so a zoom-back won't end up zooming in. while (!m_scaleStack.isEmpty() && m_scaleStack.last().scale >= endItemScale) m_scaleStack.removeLast(); m_zoomOutScale = endItemScale; break; case NoZoom: break; } animateContentRectVisible(endVisibleContentRect); } QRectF PageViewportControllerClientQt::nearestValidVisibleContentsRect() const { float cssScale = m_controller->convertFromViewport(m_pageItem->contentsScale()); float endItemScale = m_controller->convertToViewport(m_controller->innerBoundedContentsScale(cssScale)); const QRectF viewportRect = m_viewportItem->boundingRect(); QPointF viewportHotspot = viewportRect.center(); QPointF endPosition = m_viewportItem->mapToWebContent(viewportHotspot) * endItemScale - viewportHotspot; FloatRect endPosRange = m_controller->positionRangeForContentAtScale(endItemScale); endPosition = boundPosition(endPosRange.minXMinYCorner(), endPosition, endPosRange.maxXMaxYCorner()); QRectF endVisibleContentRect(endPosition / endItemScale, viewportRect.size() / endItemScale); return endVisibleContentRect; } void PageViewportControllerClientQt::setContentsPosition(const FloatPoint& localPoint) { QPointF newPosition(m_pageItem->pos() + QPointF(localPoint)); m_viewportItem->setContentPos(newPosition); updateViewportController(); } void PageViewportControllerClientQt::setContentsScale(float localScale, bool treatAsInitialValue) { if (treatAsInitialValue) { m_zoomOutScale = 0; m_scaleStack.clear(); setContentRectVisiblePositionAtScale(QPointF(), localScale); } else scaleContent(localScale); updateViewportController(); } void PageViewportControllerClientQt::setContentsRectToNearestValidBounds() { ViewportUpdateDeferrer guard(m_controller); float validCSSScale = m_controller->innerBoundedContentsScale(m_controller->convertFromViewport(m_pageItem->contentsScale())); setContentRectVisiblePositionAtScale(nearestValidVisibleContentsRect().topLeft(), m_controller->convertToViewport(validCSSScale)); } void PageViewportControllerClientQt::didResumeContent() { updateViewportController(); } bool PageViewportControllerClientQt::scrollAnimationActive() const { return m_viewportItem->isFlicking(); } bool PageViewportControllerClientQt::panGestureActive() const { return m_controller->hadUserInteraction() && m_viewportItem->isDragging(); } void PageViewportControllerClientQt::panGestureStarted(const QPointF& position, qint64 eventTimestampMillis) { // This can only happen as a result of a user interaction. ASSERT(m_controller->hadUserInteraction()); m_viewportItem->handleFlickableMousePress(position, eventTimestampMillis); m_lastPinchCenterInViewportCoordinates = position; } void PageViewportControllerClientQt::panGestureRequestUpdate(const QPointF& position, qint64 eventTimestampMillis) { m_viewportItem->handleFlickableMouseMove(position, eventTimestampMillis); m_lastPinchCenterInViewportCoordinates = position; } void PageViewportControllerClientQt::panGestureEnded(const QPointF& position, qint64 eventTimestampMillis) { m_viewportItem->handleFlickableMouseRelease(position, eventTimestampMillis); m_lastPinchCenterInViewportCoordinates = position; } void PageViewportControllerClientQt::panGestureCancelled() { // Reset the velocity samples of the flickable. // This should only be called by the recognizer if we have a recognized // pan gesture and receive a touch event with multiple touch points // (ie. transition to a pinch gesture) as it does not move the content // back inside valid bounds. // When the pinch gesture ends, the content is positioned and scaled // back to valid boundaries. m_viewportItem->cancelFlick(); } bool PageViewportControllerClientQt::scaleAnimationActive() const { return m_scaleAnimation->state() == QAbstractAnimation::Running; } void PageViewportControllerClientQt::cancelScrollAnimation() { if (!scrollAnimationActive()) return; // If the pan gesture recognizer receives a touch begin event // during an ongoing kinetic scroll animation of a previous // pan gesture, the animation is stopped and the content is // immediately positioned back to valid boundaries. m_viewportItem->cancelFlick(); setContentsRectToNearestValidBounds(); } void PageViewportControllerClientQt::interruptScaleAnimation() { // This interrupts the scale animation exactly where it is, even if it is out of bounds. m_scaleAnimation->stop(); } bool PageViewportControllerClientQt::pinchGestureActive() const { return m_controller->hadUserInteraction() && (m_pinchStartScale > 0); } void PageViewportControllerClientQt::pinchGestureStarted(const QPointF& pinchCenterInViewportCoordinates) { // This can only happen as a result of a user interaction. ASSERT(m_controller->hadUserInteraction()); if (!m_controller->allowsUserScaling()) return; m_scaleStack.clear(); m_zoomOutScale = 0.0; m_scaleUpdateDeferrer.reset(new ViewportUpdateDeferrer(m_controller, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent)); m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates; m_pinchStartScale = m_pageItem->contentsScale(); } void PageViewportControllerClientQt::pinchGestureRequestUpdate(const QPointF& pinchCenterInViewportCoordinates, qreal totalScaleFactor) { ASSERT(m_controller->hasSuspendedContent()); if (!m_controller->allowsUserScaling()) return; // Changes of the center position should move the page even if the zoom factor does not change. const qreal cssScale = m_controller->convertFromViewport(m_pinchStartScale * totalScaleFactor); // Allow zooming out beyond mimimum scale on pages that do not explicitly disallow it. const qreal targetItemScale = m_controller->convertToViewport(m_controller->outerBoundedContentsScale(cssScale)); scaleContent(targetItemScale, m_viewportItem->mapToWebContent(pinchCenterInViewportCoordinates)); const QPointF positionDiff = pinchCenterInViewportCoordinates - m_lastPinchCenterInViewportCoordinates; m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates; m_viewportItem->setContentPos(m_viewportItem->contentPos() - positionDiff); // Inform the web process to render the currently visible area with low-resolution tiles not // to expose white tiles during pinch gestures and to show fixed position layers correctly. // The actual scale is restored after the pinch gesture ends. updateViewportController(QPointF(), 1); } void PageViewportControllerClientQt::pinchGestureEnded() { ASSERT(m_controller->hasSuspendedContent()); if (!m_controller->allowsUserScaling()) return; m_pinchStartScale = -1; animateContentRectVisible(nearestValidVisibleContentsRect()); m_scaleUpdateDeferrer.reset(); // Clear after starting potential animation, which takes over deferring. } void PageViewportControllerClientQt::pinchGestureCancelled() { m_pinchStartScale = -1; m_scaleUpdateDeferrer.reset(); } QRectF PageViewportControllerClientQt::visibleContentsRect() const { const QRectF visibleRect(m_viewportItem->boundingRect().intersected(m_pageItem->boundingRect())); return m_viewportItem->mapRectToWebContent(visibleRect); } void PageViewportControllerClientQt::didChangeContentsSize() { // Emit for testing purposes, so that it can be verified that // we didn't do scale adjustment. emit m_viewportItem->experimental()->test()->contentsScaleCommitted(); if (!m_controller->hasSuspendedContent()) setContentsRectToNearestValidBounds(); } void PageViewportControllerClientQt::didChangeVisibleContents() { qreal scale = m_pageItem->contentsScale(); if (scale != m_lastCommittedScale) emit m_viewportItem->experimental()->test()->contentsScaleCommitted(); m_lastCommittedScale = scale; // Ensure that updatePaintNode is always called before painting. m_pageItem->update(); } void PageViewportControllerClientQt::didChangeViewportAttributes() { // Make sure we apply the new initial scale when deferring ends. ViewportUpdateDeferrer guard(m_controller); emit m_viewportItem->experimental()->test()->devicePixelRatioChanged(); emit m_viewportItem->experimental()->test()->viewportChanged(); } void PageViewportControllerClientQt::updateViewportController(const QPointF& trajectory, qreal scale) { FloatRect currentVisibleRect(visibleContentsRect()); float viewportScale = (scale < 0) ? viewportScaleForRect(currentVisibleRect) : scale; m_controller->setVisibleContentsRect(currentVisibleRect, viewportScale, trajectory); } void PageViewportControllerClientQt::scaleContent(qreal itemScale, const QPointF& centerInCSSCoordinates) { QPointF oldPinchCenterOnViewport = m_viewportItem->mapFromWebContent(centerInCSSCoordinates); m_pageItem->setContentsScale(itemScale); QPointF newPinchCenterOnViewport = m_viewportItem->mapFromWebContent(centerInCSSCoordinates); m_viewportItem->setContentPos(m_viewportItem->contentPos() + (newPinchCenterOnViewport - oldPinchCenterOnViewport)); } float PageViewportControllerClientQt::viewportScaleForRect(const QRectF& rect) const { return static_cast(m_viewportItem->width()) / static_cast(rect.width()); } } // namespace WebKit #include "moc_PageViewportControllerClientQt.cpp"