2
* Copyright (C) 2011, 2012 Nokia Corporation and/or its subsidiary(-ies)
3
* Copyright (C) 2011 Benjamin Poulain <benjamin@webkit.org>
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.
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.
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.
24
#include "PageViewportControllerClientQt.h"
26
#include "qquickwebpage_p.h"
27
#include "qquickwebview_p.h"
28
#include "qwebkittest_p.h"
31
#include <QtQuick/qquickitem.h>
32
#include <WebCore/FloatRect.h>
33
#include <WebCore/FloatSize.h>
35
using namespace WebCore;
39
static const int kScaleAnimationDurationMillis = 250;
41
PageViewportControllerClientQt::PageViewportControllerClientQt(QQuickWebView* viewportItem, QQuickWebPage* pageItem)
42
: m_viewportItem(viewportItem)
43
, m_pageItem(pageItem)
44
, m_scaleAnimation(new ScaleAnimation(this))
45
, m_pinchStartScale(-1)
46
, m_lastCommittedScale(-1)
48
, m_isUserInteracting(false)
49
, m_ignoreViewportChanges(true)
51
m_scaleAnimation->setDuration(kScaleAnimationDurationMillis);
52
m_scaleAnimation->setEasingCurve(QEasingCurve::OutCubic);
54
connect(m_viewportItem, SIGNAL(movementStarted()), SLOT(flickMoveStarted()), Qt::DirectConnection);
55
connect(m_viewportItem, SIGNAL(movementEnded()), SLOT(flickMoveEnded()), Qt::DirectConnection);
56
connect(m_viewportItem, SIGNAL(contentXChanged()), SLOT(pageItemPositionChanged()));
57
connect(m_viewportItem, SIGNAL(contentYChanged()), SLOT(pageItemPositionChanged()));
60
connect(m_scaleAnimation, SIGNAL(stateChanged(QAbstractAnimation::State, QAbstractAnimation::State)),
61
SLOT(scaleAnimationStateChanged(QAbstractAnimation::State, QAbstractAnimation::State)));
64
void PageViewportControllerClientQt::ScaleAnimation::updateCurrentValue(const QVariant& value)
66
// Resetting the end value, the easing curve or the duration of the scale animation
67
// triggers a recalculation of the animation interval. This might change the current
68
// value of the animated property.
69
// Make sure we only act on animation value changes if the animation is active.
70
if (!m_controllerClient->scaleAnimationActive())
73
QRectF itemRect = value.toRectF();
74
float itemScale = m_controllerClient->viewportScaleForRect(itemRect);
76
m_controllerClient->setContentRectVisiblePositionAtScale(itemRect.topLeft(), itemScale);
79
PageViewportControllerClientQt::~PageViewportControllerClientQt()
83
void PageViewportControllerClientQt::setContentRectVisiblePositionAtScale(const QPointF& location, qreal itemScale)
85
ASSERT(itemScale >= 0);
87
scaleContent(itemScale);
89
// To animate the position together with the scale we multiply the position with the current scale
90
// and add it to the page position (displacement on the flickable contentItem because of additional items).
91
QPointF newPosition(m_pageItem->pos() + location * itemScale);
93
m_viewportItem->setContentPos(newPosition);
96
void PageViewportControllerClientQt::animateContentRectVisible(const QRectF& contentRect)
98
ASSERT(m_scaleAnimation->state() == QAbstractAnimation::Stopped);
100
ASSERT(!scrollAnimationActive());
101
if (scrollAnimationActive())
104
QRectF viewportRectInContentCoords = m_viewportItem->mapRectToWebContent(m_viewportItem->boundingRect());
105
if (contentRect == viewportRectInContentCoords) {
106
m_controller->resumeContent();
110
// Inform the web process about the requested visible content rect immediately so that new tiles
111
// are rendered at the final destination during the animation.
112
m_controller->didChangeContentsVisibility(contentRect.topLeft(), viewportScaleForRect(contentRect));
114
// Since we have to animate scale and position at the same time the scale animation interpolates
115
// from the current viewport rect in content coordinates to a visible rect of the content.
116
m_scaleAnimation->setStartValue(viewportRectInContentCoords);
117
m_scaleAnimation->setEndValue(contentRect);
119
m_scaleAnimation->start();
122
void PageViewportControllerClientQt::flickMoveStarted()
124
m_controller->suspendContent();
126
m_lastScrollPosition = m_viewportItem->contentPos();
128
m_ignoreViewportChanges = false;
131
void PageViewportControllerClientQt::flickMoveEnded()
133
// This method is called on the end of the pan or pan kinetic animation.
135
m_ignoreViewportChanges = true;
136
if (!m_isUserInteracting)
137
m_controller->resumeContent();
140
void PageViewportControllerClientQt::pageItemPositionChanged()
142
if (m_ignoreViewportChanges)
145
QPointF newPosition = m_viewportItem->contentPos();
147
updateViewportController(m_lastScrollPosition - newPosition);
149
m_lastScrollPosition = newPosition;
152
void PageViewportControllerClientQt::scaleAnimationStateChanged(QAbstractAnimation::State newState, QAbstractAnimation::State /*oldState*/)
155
case QAbstractAnimation::Running:
156
m_viewportItem->cancelFlick();
157
m_controller->suspendContent();
159
case QAbstractAnimation::Stopped:
160
m_controller->resumeContent();
167
void PageViewportControllerClientQt::touchBegin()
169
m_controller->setHadUserInteraction(true);
171
// Prevents resuming the page between the user's flicks of the page.
172
m_isUserInteracting = true;
175
void PageViewportControllerClientQt::touchEnd()
177
m_isUserInteracting = false;
180
void PageViewportControllerClientQt::focusEditableArea(const QRectF& caretArea, const QRectF& targetArea)
182
// This can only happen as a result of a user interaction.
183
ASSERT(m_controller->hadUserInteraction());
185
const float editingFixedScale = 2 * m_controller->devicePixelRatio();
186
float targetScale = m_controller->innerBoundedViewportScale(editingFixedScale);
187
const QRectF viewportRect = m_viewportItem->boundingRect();
190
const qreal borderOffset = 10 * m_controller->devicePixelRatio();
191
if ((targetArea.width() + borderOffset) * targetScale <= viewportRect.width()) {
192
// Center the input field in the middle of the view, if it is smaller than
193
// the view at the scale target.
194
x = viewportRect.center().x() - targetArea.width() * targetScale / 2.0;
196
// Ensure that the caret always has borderOffset contents pixels to the right
197
// of it, and secondarily (if possible), that the area has borderOffset
198
// contents pixels to the left of it.
199
qreal caretOffset = caretArea.x() - targetArea.x();
200
x = qMin(viewportRect.width() - (caretOffset + borderOffset) * targetScale, borderOffset * targetScale);
203
const QPointF hotspot = QPointF(targetArea.x(), targetArea.center().y());
204
const QPointF viewportHotspot = QPointF(x, /* FIXME: visibleCenter */ viewportRect.center().y());
206
QPointF endPosition = hotspot - viewportHotspot / targetScale;
207
endPosition = m_controller->clampViewportToContents(endPosition, targetScale);
208
QRectF endVisibleContentRect(endPosition, viewportRect.size() / targetScale);
210
animateContentRectVisible(endVisibleContentRect);
213
void PageViewportControllerClientQt::zoomToAreaGestureEnded(const QPointF& touchPoint, const QRectF& targetArea)
215
// This can only happen as a result of a user interaction.
216
ASSERT(m_controller->hadUserInteraction());
218
if (!targetArea.isValid())
221
if (m_controller->hasSuspendedContent())
224
const float margin = 10 * m_controller->devicePixelRatio(); // We want at least a little bit of margin.
225
QRectF endArea = targetArea.adjusted(-margin, -margin, margin, margin);
227
const QRectF viewportRect = m_viewportItem->boundingRect();
229
qreal minViewportScale = qreal(2.5) * m_controller->devicePixelRatio();
230
qreal targetScale = viewportRect.size().width() / endArea.size().width();
231
targetScale = m_controller->innerBoundedViewportScale(qMin(minViewportScale, targetScale));
232
qreal currentScale = m_pageItem->contentsScale();
234
// We want to end up with the target area filling the whole width of the viewport (if possible),
235
// and centralized vertically where the user requested zoom. Thus our hotspot is the center of
236
// the targetArea x-wise and the requested zoom position, y-wise.
237
const QPointF hotspot = QPointF(endArea.center().x(), touchPoint.y());
238
const QPointF viewportHotspot = viewportRect.center();
240
QPointF endPosition = hotspot - viewportHotspot / targetScale;
241
endPosition = m_controller->clampViewportToContents(endPosition, targetScale);
242
QRectF endVisibleContentRect(endPosition, viewportRect.size() / targetScale);
244
enum { ZoomIn, ZoomBack, ZoomOut, NoZoom } zoomAction = ZoomIn;
246
// Zoom back out if attempting to scale to the same current scale, or
247
// attempting to continue scaling out from the inner most level.
248
// Use fuzzy compare with a fixed error to be able to deal with largish differences due to pixel rounding.
249
if (!m_scaleStack.isEmpty() && fuzzyCompare(targetScale, currentScale, 0.01)) {
250
// If moving the viewport would expose more of the targetRect and move at least 40 pixels, update position but do not scale out.
251
QRectF currentContentRect(m_viewportItem->mapRectToWebContent(viewportRect));
252
QRectF targetIntersection = endVisibleContentRect.intersected(targetArea);
253
if (!currentContentRect.contains(targetIntersection)
254
&& (qAbs(endVisibleContentRect.top() - currentContentRect.top()) >= 40
255
|| qAbs(endVisibleContentRect.left() - currentContentRect.left()) >= 40))
258
zoomAction = ZoomBack;
259
} else if (fuzzyCompare(targetScale, m_zoomOutScale, 0.01))
260
zoomAction = ZoomBack;
261
else if (targetScale < currentScale)
262
zoomAction = ZoomOut;
264
switch (zoomAction) {
266
m_scaleStack.append(ScaleStackItem(currentScale, m_viewportItem->contentPos().x() / currentScale));
267
m_zoomOutScale = targetScale;
270
if (m_scaleStack.isEmpty()) {
271
targetScale = m_controller->minimumContentsScale() * m_controller->devicePixelRatio();
272
endPosition.setY(hotspot.y() - viewportHotspot.y() / targetScale);
276
ScaleStackItem lastScale = m_scaleStack.takeLast();
277
targetScale = lastScale.scale;
278
// Recalculate endPosition and clamp it according to the new scale.
279
endPosition.setY(hotspot.y() - viewportHotspot.y() / targetScale);
280
endPosition.setX(lastScale.xPosition);
282
endPosition = m_controller->clampViewportToContents(endPosition, targetScale);
283
endVisibleContentRect = QRectF(endPosition, viewportRect.size() / targetScale);
287
// Unstack all scale-levels deeper than the new level, so a zoom-back won't end up zooming in.
288
while (!m_scaleStack.isEmpty() && m_scaleStack.last().scale >= targetScale)
289
m_scaleStack.removeLast();
290
m_zoomOutScale = targetScale;
296
animateContentRectVisible(endVisibleContentRect);
299
void PageViewportControllerClientQt::clearRelativeZoomState()
302
m_scaleStack.clear();
305
QRectF PageViewportControllerClientQt::nearestValidVisibleContentsRect() const
307
float targetScale = m_controller->innerBoundedViewportScale(m_pageItem->contentsScale());
309
const QRectF viewportRect = m_viewportItem->boundingRect();
310
QPointF viewportHotspot = viewportRect.center();
311
// Keep the center at the position of the old center, and substract viewportHotspot / targetScale to get the top left position.
312
QPointF endPosition = m_viewportItem->mapToWebContent(viewportHotspot) - viewportHotspot / targetScale;
314
endPosition = m_controller->clampViewportToContents(endPosition, targetScale);
315
return QRectF(endPosition, viewportRect.size() / targetScale);
318
void PageViewportControllerClientQt::setViewportPosition(const FloatPoint& contentsPoint)
320
QPointF newPosition((m_pageItem->pos() + QPointF(contentsPoint)) * m_pageItem->contentsScale());
321
m_viewportItem->setContentPos(newPosition);
322
updateViewportController();
325
void PageViewportControllerClientQt::setContentsScale(float localScale, bool treatAsInitialValue)
327
if (treatAsInitialValue) {
328
clearRelativeZoomState();
329
setContentRectVisiblePositionAtScale(QPointF(), localScale);
331
scaleContent(localScale);
334
void PageViewportControllerClientQt::setContentsRectToNearestValidBounds()
336
float targetScale = m_controller->innerBoundedViewportScale(m_pageItem->contentsScale());
337
setContentRectVisiblePositionAtScale(nearestValidVisibleContentsRect().topLeft(), targetScale);
338
updateViewportController();
341
void PageViewportControllerClientQt::didResumeContent()
343
// Make sure that tiles all around the viewport will be requested.
344
updateViewportController();
347
bool PageViewportControllerClientQt::scrollAnimationActive() const
349
return m_viewportItem->isFlicking();
352
bool PageViewportControllerClientQt::panGestureActive() const
354
return m_controller->hadUserInteraction() && m_viewportItem->isDragging();
357
void PageViewportControllerClientQt::panGestureStarted(const QPointF& position, qint64 eventTimestampMillis)
359
// This can only happen as a result of a user interaction.
360
ASSERT(m_controller->hadUserInteraction());
362
m_viewportItem->handleFlickableMousePress(position, eventTimestampMillis);
363
m_lastPinchCenterInViewportCoordinates = position;
366
void PageViewportControllerClientQt::panGestureRequestUpdate(const QPointF& position, qint64 eventTimestampMillis)
368
m_viewportItem->handleFlickableMouseMove(position, eventTimestampMillis);
369
m_lastPinchCenterInViewportCoordinates = position;
372
void PageViewportControllerClientQt::panGestureEnded(const QPointF& position, qint64 eventTimestampMillis)
374
m_viewportItem->handleFlickableMouseRelease(position, eventTimestampMillis);
375
m_lastPinchCenterInViewportCoordinates = position;
378
void PageViewportControllerClientQt::panGestureCancelled()
380
// Reset the velocity samples of the flickable.
381
// This should only be called by the recognizer if we have a recognized
382
// pan gesture and receive a touch event with multiple touch points
383
// (ie. transition to a pinch gesture) as it does not move the content
384
// back inside valid bounds.
385
// When the pinch gesture ends, the content is positioned and scaled
386
// back to valid boundaries.
387
m_viewportItem->cancelFlick();
390
bool PageViewportControllerClientQt::scaleAnimationActive() const
392
return m_scaleAnimation->state() == QAbstractAnimation::Running;
395
void PageViewportControllerClientQt::cancelScrollAnimation()
397
if (!scrollAnimationActive())
400
// If the pan gesture recognizer receives a touch begin event
401
// during an ongoing kinetic scroll animation of a previous
402
// pan gesture, the animation is stopped and the content is
403
// immediately positioned back to valid boundaries.
405
m_viewportItem->cancelFlick();
406
setContentsRectToNearestValidBounds();
409
void PageViewportControllerClientQt::interruptScaleAnimation()
411
// This interrupts the scale animation exactly where it is, even if it is out of bounds.
412
m_scaleAnimation->stop();
415
bool PageViewportControllerClientQt::pinchGestureActive() const
417
return m_controller->hadUserInteraction() && (m_pinchStartScale > 0);
420
void PageViewportControllerClientQt::pinchGestureStarted(const QPointF& pinchCenterInViewportCoordinates)
422
// This can only happen as a result of a user interaction.
423
ASSERT(m_controller->hadUserInteraction());
425
if (!m_controller->allowsUserScaling())
428
clearRelativeZoomState();
430
m_controller->suspendContent();
432
m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates;
433
m_pinchStartScale = m_pageItem->contentsScale();
436
void PageViewportControllerClientQt::pinchGestureRequestUpdate(const QPointF& pinchCenterInViewportCoordinates, qreal totalScaleFactor)
438
ASSERT(m_controller->hasSuspendedContent());
440
if (!m_controller->allowsUserScaling())
443
// Changes of the center position should move the page even if the zoom factor does not change.
444
const qreal pinchScale = m_pinchStartScale * totalScaleFactor;
446
// Allow zooming out beyond mimimum scale on pages that do not explicitly disallow it.
447
const qreal targetScale = m_controller->outerBoundedViewportScale(pinchScale);
449
scaleContent(targetScale, m_viewportItem->mapToWebContent(pinchCenterInViewportCoordinates));
451
const QPointF positionDiff = pinchCenterInViewportCoordinates - m_lastPinchCenterInViewportCoordinates;
452
m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates;
454
m_viewportItem->setContentPos(m_viewportItem->contentPos() - positionDiff);
457
void PageViewportControllerClientQt::pinchGestureEnded()
459
ASSERT(m_controller->hasSuspendedContent());
461
if (!m_controller->allowsUserScaling())
464
m_pinchStartScale = -1;
466
// This will take care of resuming the content, even if no animation was performed.
467
animateContentRectVisible(nearestValidVisibleContentsRect());
470
void PageViewportControllerClientQt::pinchGestureCancelled()
472
m_pinchStartScale = -1;
473
m_controller->resumeContent();
476
void PageViewportControllerClientQt::didChangeContentsSize(const IntSize& newSize)
478
m_pageItem->setContentsSize(QSizeF(newSize));
480
// Emit for testing purposes, so that it can be verified that
481
// we didn't do scale adjustment.
482
emit m_viewportItem->experimental()->test()->contentsScaleCommitted();
484
if (!m_controller->hasSuspendedContent())
485
setContentsRectToNearestValidBounds();
488
void PageViewportControllerClientQt::didChangeVisibleContents()
490
qreal scale = m_pageItem->contentsScale();
492
if (scale != m_lastCommittedScale)
493
emit m_viewportItem->experimental()->test()->contentsScaleCommitted();
494
m_lastCommittedScale = scale;
496
// Ensure that updatePaintNode is always called before painting.
497
m_pageItem->update();
500
void PageViewportControllerClientQt::didChangeViewportAttributes()
502
clearRelativeZoomState();
503
emit m_viewportItem->experimental()->test()->viewportChanged();
506
void PageViewportControllerClientQt::updateViewportController(const QPointF& trajectory)
508
FloatPoint viewportPos = m_viewportItem->mapToWebContent(QPointF());
509
m_controller->didChangeContentsVisibility(viewportPos, m_pageItem->contentsScale(), trajectory);
512
void PageViewportControllerClientQt::scaleContent(qreal itemScale, const QPointF& centerInCSSCoordinates)
514
QPointF oldPinchCenterOnViewport = m_viewportItem->mapFromWebContent(centerInCSSCoordinates);
515
m_pageItem->setContentsScale(itemScale);
516
QPointF newPinchCenterOnViewport = m_viewportItem->mapFromWebContent(centerInCSSCoordinates);
517
m_viewportItem->setContentPos(m_viewportItem->contentPos() + (newPinchCenterOnViewport - oldPinchCenterOnViewport));
520
float PageViewportControllerClientQt::viewportScaleForRect(const QRectF& rect) const
522
return static_cast<float>(m_viewportItem->width()) / static_cast<float>(rect.width());
525
} // namespace WebKit
527
#include "moc_PageViewportControllerClientQt.cpp"