1/* 2 * Copyright (C) 2011, 2012 Nokia Corporation and/or its subsidiary(-ies) 3 * Copyright (C) 2011 Benjamin Poulain <benjamin@webkit.org> 4 * 5 * This library is free software; you can redistribute it and/or 6 * modify it under the terms of the GNU Library General Public 7 * License as published by the Free Software Foundation; either 8 * version 2 of the License, or (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 * Library General Public License for more details. 14 * 15 * You should have received a copy of the GNU Library General Public License 16 * along with this program; see the file COPYING.LIB. If not, write to 17 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 * Boston, MA 02110-1301, USA. 19 * 20 */ 21 22 23#include "config.h" 24#include "PageViewportControllerClientQt.h" 25 26#include "WebPageProxy.h" 27#include "qquickwebpage_p.h" 28#include "qquickwebview_p.h" 29#include "qwebkittest_p.h" 30#include <QPointF> 31#include <QTransform> 32#include <QtQuick/qquickitem.h> 33#include <WKAPICast.h> 34#include <WebCore/FloatRect.h> 35#include <WebCore/FloatSize.h> 36 37using namespace WebCore; 38 39namespace WebKit { 40 41static const int kScaleAnimationDurationMillis = 250; 42 43PageViewportControllerClientQt::PageViewportControllerClientQt(QQuickWebView* viewportItem, QQuickWebPage* pageItem) 44 : m_viewportItem(viewportItem) 45 , m_pageItem(pageItem) 46 , m_scaleChange(this) 47 , m_scrollChange(this) 48 , m_touchInteraction(this, false /* shouldSuspend */) 49 , m_scaleAnimation(new ScaleAnimation(this)) 50 , m_activeInteractionCount(0) 51 , m_pinchStartScale(-1) 52 , m_lastCommittedScale(-1) 53 , m_zoomOutScale(0) 54{ 55 m_scaleAnimation->setDuration(kScaleAnimationDurationMillis); 56 m_scaleAnimation->setEasingCurve(QEasingCurve::OutCubic); 57 58 connect(m_viewportItem, SIGNAL(movementStarted()), SLOT(flickMoveStarted()), Qt::DirectConnection); 59 connect(m_viewportItem, SIGNAL(movementEnded()), SLOT(flickMoveEnded()), Qt::DirectConnection); 60 connect(m_viewportItem, SIGNAL(contentXChanged()), SLOT(pageItemPositionChanged())); 61 connect(m_viewportItem, SIGNAL(contentYChanged()), SLOT(pageItemPositionChanged())); 62 63 64 connect(m_scaleAnimation, SIGNAL(stateChanged(QAbstractAnimation::State, QAbstractAnimation::State)), 65 SLOT(scaleAnimationStateChanged(QAbstractAnimation::State, QAbstractAnimation::State))); 66} 67 68void PageViewportControllerClientQt::ScaleAnimation::updateCurrentValue(const QVariant& value) 69{ 70 // Resetting the end value, the easing curve or the duration of the scale animation 71 // triggers a recalculation of the animation interval. This might change the current 72 // value of the animated property. 73 // Make sure we only act on animation value changes if the animation is active. 74 if (!m_controllerClient->scaleAnimationActive()) 75 return; 76 77 QRectF itemRect = value.toRectF(); 78 float itemScale = m_controllerClient->viewportScaleForRect(itemRect); 79 80 m_controllerClient->setContentRectVisiblePositionAtScale(itemRect.topLeft(), itemScale); 81} 82 83void PageViewportControllerClientQt::ViewportInteractionTracker::begin() 84{ 85 if (m_inProgress) 86 return; 87 88 m_inProgress = true; 89 90 if (m_shouldSuspend) 91 toImpl(m_controllerClient->m_viewportItem->pageRef())->suspendActiveDOMObjectsAndAnimations(); 92 93 ++(m_controllerClient->m_activeInteractionCount); 94} 95 96void PageViewportControllerClientQt::ViewportInteractionTracker::end() 97{ 98 if (!m_inProgress) 99 return; 100 101 m_inProgress = false; 102 103 ASSERT(m_controllerClient->m_activeInteractionCount > 0); 104 105 if (!(--(m_controllerClient->m_activeInteractionCount))) 106 toImpl(m_controllerClient->m_viewportItem->pageRef())->resumeActiveDOMObjectsAndAnimations(); 107} 108 109PageViewportControllerClientQt::~PageViewportControllerClientQt() 110{ 111} 112 113void PageViewportControllerClientQt::setContentRectVisiblePositionAtScale(const QPointF& location, qreal itemScale) 114{ 115 ASSERT(itemScale >= 0); 116 117 scaleContent(itemScale); 118 119 // To animate the position together with the scale we multiply the position with the current scale 120 // and add it to the page position (displacement on the flickable contentItem because of additional items). 121 QPointF newPosition(m_pageItem->position() + location * itemScale); 122 123 m_viewportItem->setContentPos(newPosition); 124} 125 126void PageViewportControllerClientQt::animateContentRectVisible(const QRectF& contentRect) 127{ 128 ASSERT(!scaleAnimationActive()); 129 ASSERT(!scrollAnimationActive()); 130 131 QRectF viewportRectInContentCoords = m_viewportItem->mapRectToWebContent(m_viewportItem->boundingRect()); 132 if (contentRect == viewportRectInContentCoords) { 133 m_scaleChange.end(); 134 updateViewportController(); 135 return; 136 } 137 138 // Inform the web process about the requested visible content rect immediately so that new tiles 139 // are rendered at the final destination during the animation. 140 m_controller->didChangeContentsVisibility(contentRect.topLeft(), viewportScaleForRect(contentRect)); 141 142 // Since we have to animate scale and position at the same time the scale animation interpolates 143 // from the current viewport rect in content coordinates to a visible rect of the content. 144 m_scaleAnimation->setStartValue(viewportRectInContentCoords); 145 m_scaleAnimation->setEndValue(contentRect); 146 147 m_scaleAnimation->start(); 148} 149 150void PageViewportControllerClientQt::flickMoveStarted() 151{ 152 m_scrollChange.begin(); 153 m_lastScrollPosition = m_viewportItem->contentPos(); 154} 155 156void PageViewportControllerClientQt::flickMoveEnded() 157{ 158 // This method is called on the end of the pan or pan kinetic animation. 159 m_scrollChange.end(); 160 updateViewportController(); 161} 162 163void PageViewportControllerClientQt::pageItemPositionChanged() 164{ 165 if (m_scaleChange.inProgress()) 166 return; 167 168 QPointF newPosition = m_viewportItem->contentPos(); 169 170 updateViewportController(m_lastScrollPosition - newPosition); 171 172 m_lastScrollPosition = newPosition; 173} 174 175void PageViewportControllerClientQt::scaleAnimationStateChanged(QAbstractAnimation::State newState, QAbstractAnimation::State /*oldState*/) 176{ 177 switch (newState) { 178 case QAbstractAnimation::Running: 179 m_scaleChange.begin(); 180 break; 181 case QAbstractAnimation::Stopped: 182 m_scaleChange.end(); 183 updateViewportController(); 184 break; 185 default: 186 break; 187 } 188} 189 190void PageViewportControllerClientQt::touchBegin() 191{ 192 // Check for sane event delivery. At this point neither a pan gesture nor a pinch gesture should be active. 193 ASSERT(!m_viewportItem->isDragging()); 194 ASSERT(!(m_pinchStartScale > 0)); 195 196 m_controller->setHadUserInteraction(true); 197 198 // Prevent resuming the page during transition between gestures while the user is interacting. 199 // The content is suspended as soon as a pan or pinch gesture or an animation is started. 200 m_touchInteraction.begin(); 201} 202 203void PageViewportControllerClientQt::touchEnd() 204{ 205 m_touchInteraction.end(); 206} 207 208void PageViewportControllerClientQt::focusEditableArea(const QRectF& caretArea, const QRectF& targetArea) 209{ 210 // This can only happen as a result of a user interaction. 211 ASSERT(m_controller->hadUserInteraction()); 212 213 const float editingFixedScale = 2; 214 float targetScale = m_controller->innerBoundedViewportScale(editingFixedScale); 215 const QRectF viewportRect = m_viewportItem->boundingRect(); 216 217 qreal x; 218 const qreal borderOffset = 10; 219 if ((targetArea.width() + borderOffset) * targetScale <= viewportRect.width()) { 220 // Center the input field in the middle of the view, if it is smaller than 221 // the view at the scale target. 222 x = viewportRect.center().x() - targetArea.width() * targetScale / 2.0; 223 } else { 224 // Ensure that the caret always has borderOffset contents pixels to the right 225 // of it, and secondarily (if possible), that the area has borderOffset 226 // contents pixels to the left of it. 227 qreal caretOffset = caretArea.x() - targetArea.x(); 228 x = qMin(viewportRect.width() - (caretOffset + borderOffset) * targetScale, borderOffset * targetScale); 229 } 230 231 const QPointF hotspot = QPointF(targetArea.x(), targetArea.center().y()); 232 const QPointF viewportHotspot = QPointF(x, /* FIXME: visibleCenter */ viewportRect.center().y()); 233 234 QPointF endPosition = hotspot - viewportHotspot / targetScale; 235 endPosition = m_controller->boundContentsPositionAtScale(endPosition, targetScale); 236 QRectF endVisibleContentRect(endPosition, viewportRect.size() / targetScale); 237 238 animateContentRectVisible(endVisibleContentRect); 239} 240 241void PageViewportControllerClientQt::zoomToAreaGestureEnded(const QPointF& touchPoint, const QRectF& targetArea) 242{ 243 // This can only happen as a result of a user interaction. 244 ASSERT(m_controller->hadUserInteraction()); 245 246 if (!targetArea.isValid()) 247 return; 248 249 if (m_scrollChange.inProgress() || m_scaleChange.inProgress()) 250 return; 251 252 const float margin = 10; // We want at least a little bit of margin. 253 QRectF endArea = targetArea.adjusted(-margin, -margin, margin, margin); 254 255 const QRectF viewportRect = m_viewportItem->boundingRect(); 256 257 const qreal minViewportScale = qreal(2.5); 258 qreal targetScale = viewportRect.size().width() / endArea.size().width(); 259 targetScale = m_controller->innerBoundedViewportScale(qMin(minViewportScale, targetScale)); 260 qreal currentScale = m_pageItem->contentsScale(); 261 262 // We want to end up with the target area filling the whole width of the viewport (if possible), 263 // and centralized vertically where the user requested zoom. Thus our hotspot is the center of 264 // the targetArea x-wise and the requested zoom position, y-wise. 265 const QPointF hotspot = QPointF(endArea.center().x(), touchPoint.y()); 266 const QPointF viewportHotspot = viewportRect.center(); 267 268 QPointF endPosition = hotspot - viewportHotspot / targetScale; 269 endPosition = m_controller->boundContentsPositionAtScale(endPosition, targetScale); 270 QRectF endVisibleContentRect(endPosition, viewportRect.size() / targetScale); 271 272 enum { ZoomIn, ZoomBack, ZoomOut, NoZoom } zoomAction = ZoomIn; 273 274 // Zoom back out if attempting to scale to the same current scale, or 275 // attempting to continue scaling out from the inner most level. 276 // Use fuzzy compare with a fixed error to be able to deal with largish differences due to pixel rounding. 277 if (!m_scaleStack.isEmpty() && fuzzyCompare(targetScale, currentScale, 0.01)) { 278 // If moving the viewport would expose more of the targetRect and move at least 40 pixels, update position but do not scale out. 279 QRectF currentContentRect(m_viewportItem->mapRectToWebContent(viewportRect)); 280 QRectF targetIntersection = endVisibleContentRect.intersected(targetArea); 281 if (!currentContentRect.contains(targetIntersection) 282 && (qAbs(endVisibleContentRect.top() - currentContentRect.top()) >= 40 283 || qAbs(endVisibleContentRect.left() - currentContentRect.left()) >= 40)) 284 zoomAction = NoZoom; 285 else 286 zoomAction = ZoomBack; 287 } else if (fuzzyCompare(targetScale, m_zoomOutScale, 0.01)) 288 zoomAction = ZoomBack; 289 else if (targetScale < currentScale) 290 zoomAction = ZoomOut; 291 292 switch (zoomAction) { 293 case ZoomIn: 294 m_scaleStack.append(ScaleStackItem(currentScale, m_viewportItem->contentPos().x() / currentScale)); 295 m_zoomOutScale = targetScale; 296 break; 297 case ZoomBack: { 298 if (m_scaleStack.isEmpty()) { 299 targetScale = m_controller->minimumScale(); 300 endPosition.setY(hotspot.y() - viewportHotspot.y() / targetScale); 301 endPosition.setX(0); 302 m_zoomOutScale = 0; 303 } else { 304 ScaleStackItem lastScale = m_scaleStack.takeLast(); 305 targetScale = lastScale.scale; 306 // Recalculate endPosition and clamp it according to the new scale. 307 endPosition.setY(hotspot.y() - viewportHotspot.y() / targetScale); 308 endPosition.setX(lastScale.xPosition); 309 } 310 endPosition = m_controller->boundContentsPositionAtScale(endPosition, targetScale); 311 endVisibleContentRect = QRectF(endPosition, viewportRect.size() / targetScale); 312 break; 313 } 314 case ZoomOut: 315 // Unstack all scale-levels deeper than the new level, so a zoom-back won't end up zooming in. 316 while (!m_scaleStack.isEmpty() && m_scaleStack.last().scale >= targetScale) 317 m_scaleStack.removeLast(); 318 m_zoomOutScale = targetScale; 319 break; 320 case NoZoom: 321 break; 322 } 323 324 animateContentRectVisible(endVisibleContentRect); 325} 326 327void PageViewportControllerClientQt::clearRelativeZoomState() 328{ 329 m_zoomOutScale = 0; 330 m_scaleStack.clear(); 331} 332 333QRectF PageViewportControllerClientQt::nearestValidVisibleContentsRect() const 334{ 335 float targetScale = m_controller->innerBoundedViewportScale(m_pageItem->contentsScale()); 336 337 const QRectF viewportRect = m_viewportItem->boundingRect(); 338 QPointF viewportHotspot = viewportRect.center(); 339 // Keep the center at the position of the old center, and substract viewportHotspot / targetScale to get the top left position. 340 QPointF endPosition = m_viewportItem->mapToWebContent(viewportHotspot) - viewportHotspot / targetScale; 341 342 endPosition = m_controller->boundContentsPositionAtScale(endPosition, targetScale); 343 return QRectF(endPosition, viewportRect.size() / targetScale); 344} 345 346void PageViewportControllerClientQt::setViewportPosition(const FloatPoint& contentsPoint) 347{ 348 QPointF newPosition((m_pageItem->position() + QPointF(contentsPoint)) * m_pageItem->contentsScale()); 349 // The contentX and contentY property changes trigger a visible rect update. 350 m_viewportItem->setContentPos(newPosition); 351} 352 353void PageViewportControllerClientQt::setPageScaleFactor(float localScale) 354{ 355 scaleContent(localScale); 356} 357 358void PageViewportControllerClientQt::setContentsRectToNearestValidBounds() 359{ 360 float targetScale = m_controller->innerBoundedViewportScale(m_pageItem->contentsScale()); 361 setContentRectVisiblePositionAtScale(nearestValidVisibleContentsRect().topLeft(), targetScale); 362 updateViewportController(); 363} 364 365bool PageViewportControllerClientQt::scrollAnimationActive() const 366{ 367 return m_viewportItem->isFlicking(); 368} 369 370void PageViewportControllerClientQt::panGestureStarted(const QPointF& position, qint64 eventTimestampMillis) 371{ 372 // This can only happen as a result of a user interaction. 373 ASSERT(m_touchInteraction.inProgress()); 374 375 m_viewportItem->handleFlickableMousePress(position, eventTimestampMillis); 376 m_lastPinchCenterInViewportCoordinates = position; 377} 378 379void PageViewportControllerClientQt::panGestureRequestUpdate(const QPointF& position, qint64 eventTimestampMillis) 380{ 381 m_viewportItem->handleFlickableMouseMove(position, eventTimestampMillis); 382 m_lastPinchCenterInViewportCoordinates = position; 383} 384 385void PageViewportControllerClientQt::panGestureEnded(const QPointF& position, qint64 eventTimestampMillis) 386{ 387 m_viewportItem->handleFlickableMouseRelease(position, eventTimestampMillis); 388 m_lastPinchCenterInViewportCoordinates = position; 389} 390 391void PageViewportControllerClientQt::panGestureCancelled() 392{ 393 // Reset the velocity samples of the flickable. 394 // This should only be called by the recognizer if we have a recognized 395 // pan gesture and receive a touch event with multiple touch points 396 // (ie. transition to a pinch gesture) as it does not move the content 397 // back inside valid bounds. 398 // When the pinch gesture ends, the content is positioned and scaled 399 // back to valid boundaries. 400 m_viewportItem->cancelFlick(); 401} 402 403bool PageViewportControllerClientQt::scaleAnimationActive() const 404{ 405 return m_scaleAnimation->state() == QAbstractAnimation::Running; 406} 407 408void PageViewportControllerClientQt::cancelScrollAnimation() 409{ 410 if (!scrollAnimationActive()) 411 return; 412 413 // If the pan gesture recognizer receives a touch begin event 414 // during an ongoing kinetic scroll animation of a previous 415 // pan gesture, the animation is stopped and the content is 416 // immediately positioned back to valid boundaries. 417 418 m_viewportItem->cancelFlick(); 419 setContentsRectToNearestValidBounds(); 420} 421 422void PageViewportControllerClientQt::interruptScaleAnimation() 423{ 424 // This interrupts the scale animation exactly where it is, even if it is out of bounds. 425 m_scaleAnimation->stop(); 426} 427 428void PageViewportControllerClientQt::pinchGestureStarted(const QPointF& pinchCenterInViewportCoordinates) 429{ 430 // This can only happen as a result of a user interaction. 431 ASSERT(m_touchInteraction.inProgress()); 432 433 if (!m_controller->allowsUserScaling() || !m_viewportItem->isInteractive()) 434 return; 435 436 clearRelativeZoomState(); 437 m_scaleChange.begin(); 438 439 m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates; 440 m_pinchStartScale = m_pageItem->contentsScale(); 441} 442 443void PageViewportControllerClientQt::pinchGestureRequestUpdate(const QPointF& pinchCenterInViewportCoordinates, qreal totalScaleFactor) 444{ 445 if (!m_controller->allowsUserScaling() || !m_viewportItem->isInteractive()) 446 return; 447 448 ASSERT(m_scaleChange.inProgress()); 449 ASSERT(m_pinchStartScale > 0); 450 // Changes of the center position should move the page even if the zoom factor does not change. 451 const qreal pinchScale = m_pinchStartScale * totalScaleFactor; 452 453 // Allow zooming out beyond mimimum scale on pages that do not explicitly disallow it. 454 const qreal targetScale = m_controller->outerBoundedViewportScale(pinchScale); 455 456 scaleContent(targetScale, m_viewportItem->mapToWebContent(pinchCenterInViewportCoordinates)); 457 458 const QPointF positionDiff = pinchCenterInViewportCoordinates - m_lastPinchCenterInViewportCoordinates; 459 m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates; 460 461 m_viewportItem->setContentPos(m_viewportItem->contentPos() - positionDiff); 462} 463 464void PageViewportControllerClientQt::pinchGestureEnded() 465{ 466 if (m_pinchStartScale < 0) 467 return; 468 469 ASSERT(m_scaleChange.inProgress()); 470 m_pinchStartScale = -1; 471 472 // This will take care of resuming the content, even if no animation was performed. 473 animateContentRectVisible(nearestValidVisibleContentsRect()); 474} 475 476void PageViewportControllerClientQt::pinchGestureCancelled() 477{ 478 m_pinchStartScale = -1; 479 m_scaleChange.end(); 480 updateViewportController(); 481} 482 483void PageViewportControllerClientQt::didChangeContentsSize(const IntSize& newSize) 484{ 485 m_pageItem->setContentsSize(QSizeF(newSize)); 486 487 // Emit for testing purposes, so that it can be verified that 488 // we didn't do scale adjustment. 489 emit m_viewportItem->experimental()->test()->contentsScaleCommitted(); 490 491 if (!m_scaleChange.inProgress() && !m_scrollChange.inProgress()) 492 setContentsRectToNearestValidBounds(); 493} 494 495void PageViewportControllerClientQt::didChangeVisibleContents() 496{ 497 qreal scale = m_pageItem->contentsScale(); 498 499 if (scale != m_lastCommittedScale) 500 emit m_viewportItem->experimental()->test()->contentsScaleCommitted(); 501 m_lastCommittedScale = scale; 502 503 // Ensure that updatePaintNode is always called before painting. 504 m_pageItem->update(); 505} 506 507void PageViewportControllerClientQt::didChangeViewportAttributes() 508{ 509 clearRelativeZoomState(); 510 emit m_viewportItem->experimental()->test()->viewportChanged(); 511} 512 513void PageViewportControllerClientQt::updateViewportController(const QPointF& trajectory) 514{ 515 FloatPoint viewportPos = m_viewportItem->mapToWebContent(QPointF()); 516 m_controller->didChangeContentsVisibility(viewportPos, m_pageItem->contentsScale(), trajectory); 517} 518 519void PageViewportControllerClientQt::scaleContent(qreal itemScale, const QPointF& centerInCSSCoordinates) 520{ 521 QPointF oldPinchCenterOnViewport = m_viewportItem->mapFromWebContent(centerInCSSCoordinates); 522 m_pageItem->setContentsScale(itemScale); 523 QPointF newPinchCenterOnViewport = m_viewportItem->mapFromWebContent(centerInCSSCoordinates); 524 m_viewportItem->setContentPos(m_viewportItem->contentPos() + (newPinchCenterOnViewport - oldPinchCenterOnViewport)); 525} 526 527float PageViewportControllerClientQt::viewportScaleForRect(const QRectF& rect) const 528{ 529 return static_cast<float>(m_viewportItem->width()) / static_cast<float>(rect.width()); 530} 531 532} // namespace WebKit 533 534#include "moc_PageViewportControllerClientQt.cpp" 535