b8c85ef4bb1fabaf08959e77590d82a8f1b97d5e
[WebKit-https.git] / Source / WebKit2 / UIProcess / qt / QtViewportInteractionEngine.cpp
1 /*
2  * Copyright (C) 2011 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 #include "config.h"
23 #include "QtViewportInteractionEngine.h"
24
25 #include "qquickwebpage_p.h"
26 #include "qquickwebview_p.h"
27 #include <QPointF>
28 #include <QTransform>
29 #include <QWheelEvent>
30 #include <QtQuick/qquickitem.h>
31 #include <wtf/PassOwnPtr.h>
32
33 namespace WebKit {
34
35 static const int kScaleAnimationDurationMillis = 250;
36
37 // UPDATE DEFERRING (SUSPEND/RESUME)
38 // =================================
39 //
40 // When interaction with the content, either by animating or by the hand of the user,
41 // it is important to ensure smooth animations of at least 60fps in order to give a
42 // good user experience.
43 //
44 // In order to do this we need to get rid of unknown factors. These include device
45 // sensors (geolocation, orientation updates etc), CSS3 animations, JavaScript
46 // exectution, sub resource loads etc. We do this by emitting suspend and resume
47 // signals, which are then handled by the viewport and propagates to the right place.
48 //
49 // For this reason the ViewportUpdateDeferrer guard must be used when we interact
50 // or animate the content.
51 //
52 // It should be noted that when we update content properties, we might receive notify
53 // signals send my the content item itself, and care should be taken to not act on
54 // these unconditionally. An example of this is the pinch zoom, which changes the
55 // position and will thus result in a QQuickWebPage::geometryChanged() signal getting
56 // emitted.
57 //
58 // If something should only be executed during update deferring, it is possible to
59 // check for that using ASSERT(m_suspendCount).
60
61 class ViewportUpdateDeferrer {
62 public:
63     enum SuspendContentFlag { DeferUpdate, DeferUpdateAndSuspendContent };
64     ViewportUpdateDeferrer(QtViewportInteractionEngine* engine, SuspendContentFlag suspendContentFlag = DeferUpdate)
65         : engine(engine)
66     {
67         engine->m_suspendCount++;
68
69         // There is no need to suspend content for immediate updates
70         // only during animations or longer gestures.
71         if (suspendContentFlag == DeferUpdateAndSuspendContent && !engine->m_hasSuspendedContent) {
72             engine->m_hasSuspendedContent = true;
73             emit engine->contentSuspendRequested();
74         }
75     }
76
77     ~ViewportUpdateDeferrer()
78     {
79         if (--(engine->m_suspendCount))
80             return;
81
82         if (engine->m_hasSuspendedContent) {
83             engine->m_hasSuspendedContent = false;
84             emit engine->contentResumeRequested();
85         }
86
87         // Make sure that tiles all around the viewport will be requested.
88         emit engine->contentViewportChanged(QPointF());
89     }
90
91 private:
92     QtViewportInteractionEngine* const engine;
93 };
94
95 // A floating point compare with absolute error.
96 static inline bool fuzzyCompare(qreal a, qreal b, qreal epsilon)
97 {
98     return qAbs(a - b) < epsilon;
99 }
100
101 inline qreal QtViewportInteractionEngine::cssScaleFromItem(qreal itemScale)
102 {
103     return itemScale / m_devicePixelRatio;
104 }
105
106 inline qreal QtViewportInteractionEngine::itemScaleFromCSS(qreal cssScale)
107 {
108     return cssScale * m_devicePixelRatio;
109 }
110
111 inline qreal QtViewportInteractionEngine::itemCoordFromCSS(qreal value)
112 {
113     return value * m_devicePixelRatio;
114 }
115
116 inline QRectF QtViewportInteractionEngine::itemRectFromCSS(const QRectF& cssRect)
117 {
118     QRectF itemRect;
119
120     itemRect.setX(itemCoordFromCSS(cssRect.x()));
121     itemRect.setY(itemCoordFromCSS(cssRect.y()));
122     itemRect.setWidth(itemCoordFromCSS(cssRect.width()));
123     itemRect.setHeight(itemCoordFromCSS(cssRect.height()));
124
125     return itemRect;
126 }
127
128 QtViewportInteractionEngine::QtViewportInteractionEngine(QQuickWebView* viewport, QQuickWebPage* content)
129     : m_viewport(viewport)
130     , m_content(content)
131     , m_suspendCount(0)
132     , m_hasSuspendedContent(false)
133     , m_hadUserInteraction(false)
134     , m_scaleAnimation(new ScaleAnimation(this))
135     , m_pinchStartScale(-1)
136     , m_zoomOutScale(0.0)
137 {
138     reset();
139
140     connect(m_content, SIGNAL(widthChanged()), SLOT(itemSizeChanged()), Qt::DirectConnection);
141     connect(m_content, SIGNAL(heightChanged()), SLOT(itemSizeChanged()), Qt::DirectConnection);
142     connect(m_viewport, SIGNAL(movementStarted()), SLOT(flickableMoveStarted()), Qt::DirectConnection);
143     connect(m_viewport, SIGNAL(movementEnded()), SLOT(flickableMoveEnded()), Qt::DirectConnection);
144
145     connect(m_scaleAnimation, SIGNAL(valueChanged(QVariant)),
146             SLOT(scaleAnimationValueChanged(QVariant)), Qt::DirectConnection);
147     connect(m_scaleAnimation, SIGNAL(stateChanged(QAbstractAnimation::State, QAbstractAnimation::State)),
148             SLOT(scaleAnimationStateChanged(QAbstractAnimation::State, QAbstractAnimation::State)), Qt::DirectConnection);
149 }
150
151 QtViewportInteractionEngine::~QtViewportInteractionEngine()
152 {
153 }
154
155 qreal QtViewportInteractionEngine::innerBoundedCSSScale(qreal cssScale)
156 {
157     return qBound(m_minimumScale, cssScale, m_maximumScale);
158 }
159
160 qreal QtViewportInteractionEngine::outerBoundedCSSScale(qreal cssScale)
161 {
162     if (m_allowsUserScaling) {
163         // Bounded by [0.1, 10.0] like the viewport meta code in WebCore.
164         qreal hardMin = qMax<qreal>(0.1, qreal(0.5) * m_minimumScale);
165         qreal hardMax = qMin<qreal>(10, qreal(2.0) * m_maximumScale);
166         return qBound(hardMin, cssScale, hardMax);
167     }
168     return innerBoundedCSSScale(cssScale);
169 }
170
171 void QtViewportInteractionEngine::setItemRectVisible(const QRectF& itemRect)
172 {
173     if (itemRect.isEmpty())
174         return;
175
176     ViewportUpdateDeferrer guard(this);
177
178     qreal itemScale = m_viewport->width() / itemRect.width();
179
180     m_content->setContentsScale(itemScale);
181
182     // To animate the position together with the scale we multiply the position with the current scale
183     // and add it to the page position (displacement on the flickable contentItem because of additional items).
184     QPointF newPosition(m_content->pos() + (itemRect.topLeft() * itemScale));
185
186     m_viewport->setContentPos(newPosition);
187 }
188
189 bool QtViewportInteractionEngine::animateItemRectVisible(const QRectF& itemRect)
190 {
191     QRectF currentItemRectVisible = m_viewport->mapRectToWebContent(m_viewport->boundingRect());
192     if (itemRect == currentItemRectVisible)
193         return false;
194
195     // FIXME: Investigate why that animation doesn't run when we are unfocused.
196     if (!m_viewport->isVisible() || !m_viewport->hasFocus()) {
197         // Apply the end result immediately when we are non-visible.
198         setItemRectVisible(itemRect);
199         return true;
200     }
201
202     m_scaleAnimation->setDuration(kScaleAnimationDurationMillis);
203     m_scaleAnimation->setEasingCurve(QEasingCurve::OutCubic);
204
205     m_scaleAnimation->setStartValue(currentItemRectVisible);
206     m_scaleAnimation->setEndValue(itemRect);
207
208     m_scaleAnimation->start();
209     return true;
210 }
211
212 void QtViewportInteractionEngine::flickableMoveStarted()
213 {
214     Q_ASSERT(m_viewport->isMoving());
215     m_scrollUpdateDeferrer = adoptPtr(new ViewportUpdateDeferrer(this, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent));
216
217     m_lastScrollPosition = m_viewport->contentPos();
218     connect(m_viewport, SIGNAL(contentXChanged()), SLOT(flickableMovingPositionUpdate()));
219     connect(m_viewport, SIGNAL(contentYChanged()), SLOT(flickableMovingPositionUpdate()));
220 }
221
222 void QtViewportInteractionEngine::flickableMoveEnded()
223 {
224     Q_ASSERT(!m_viewport->isMoving());
225     // This method is called on the end of the pan or pan kinetic animation.
226     m_scrollUpdateDeferrer.clear();
227
228     m_lastScrollPosition = QPointF();
229     disconnect(m_viewport, SIGNAL(contentXChanged()), this, SLOT(flickableMovingPositionUpdate()));
230     disconnect(m_viewport, SIGNAL(contentYChanged()), this, SLOT(flickableMovingPositionUpdate()));
231 }
232
233 void QtViewportInteractionEngine::flickableMovingPositionUpdate()
234 {
235     QPointF newPosition = m_viewport->contentPos();
236
237     emit contentViewportChanged(m_lastScrollPosition - newPosition);
238
239     m_lastScrollPosition = newPosition;
240 }
241
242 void QtViewportInteractionEngine::scaleAnimationStateChanged(QAbstractAnimation::State newState, QAbstractAnimation::State /*oldState*/)
243 {
244     switch (newState) {
245     case QAbstractAnimation::Running:
246         m_viewport->cancelFlick();
247         if (!m_scaleUpdateDeferrer)
248             m_scaleUpdateDeferrer = adoptPtr(new ViewportUpdateDeferrer(this, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent));
249         break;
250     case QAbstractAnimation::Stopped:
251         m_scaleUpdateDeferrer.clear();
252         break;
253     default:
254         break;
255     }
256 }
257
258 static inline QPointF boundPosition(const QPointF minPosition, const QPointF& position, const QPointF& maxPosition)
259 {
260     return QPointF(qBound(minPosition.x(), position.x(), maxPosition.x()),
261                    qBound(minPosition.y(), position.y(), maxPosition.y()));
262 }
263
264 void QtViewportInteractionEngine::wheelEvent(QWheelEvent* ev)
265 {
266     if (scrollAnimationActive() || scaleAnimationActive() || pinchGestureActive())
267         return; // Ignore.
268
269
270     // A normal scroll-tick should have a delta of 120 (1/8) degrees. Convert this to
271     // local standard scroll step of 3 lines of 20 pixels.
272     static const int cDefaultQtScrollStep = 20;
273     static const int wheelScrollLines = 3;
274     const int wheelTick = wheelScrollLines * cDefaultQtScrollStep;
275
276     int pixelDelta = ev->delta() * (wheelTick / 120.f);
277
278     QPointF newPosition = m_viewport->contentPos();
279
280     if (ev->orientation() == Qt::Horizontal)
281         newPosition.rx() -= pixelDelta;
282     else
283         newPosition.ry() -= pixelDelta;
284
285     QRectF endPosRange = computePosRangeForItemAtScale(m_content->contentsScale());
286
287     QPointF currentPosition = m_viewport->contentPos();
288     newPosition = boundPosition(endPosRange.topLeft(), newPosition, endPosRange.bottomRight());
289     m_viewport->setContentPos(newPosition);
290
291     emit contentViewportChanged(currentPosition - newPosition);
292 }
293
294 void QtViewportInteractionEngine::pagePositionRequest(const QPoint& pagePosition)
295 {
296     // Ignore the request if suspended. Can only happen due to delay in event delivery.
297     if (m_suspendCount)
298         return;
299
300     qreal endItemScale = m_content->contentsScale(); // Stay at same scale.
301
302     QRectF endPosRange = computePosRangeForItemAtScale(endItemScale);
303     QPointF endPosition = boundPosition(endPosRange.topLeft(), pagePosition * endItemScale, endPosRange.bottomRight());
304
305     QRectF endVisibleContentRect(endPosition / endItemScale, m_viewport->boundingRect().size() / endItemScale);
306
307     setItemRectVisible(endVisibleContentRect);
308 }
309
310 void QtViewportInteractionEngine::touchBegin()
311 {
312     // Prevents resuming the page between the user's flicks of the page while the animation is running.
313     if (scrollAnimationActive())
314         m_touchUpdateDeferrer = adoptPtr(new ViewportUpdateDeferrer(this, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent));
315 }
316
317 void QtViewportInteractionEngine::touchEnd()
318 {
319     m_touchUpdateDeferrer.clear();
320 }
321
322 QRectF QtViewportInteractionEngine::computePosRangeForItemAtScale(qreal itemScale) const
323 {
324     const QSizeF contentItemSize = m_content->contentsSize() * itemScale;
325     const QSizeF viewportItemSize = m_viewport->boundingRect().size();
326
327     const qreal horizontalRange = contentItemSize.width() - viewportItemSize.width();
328     const qreal verticalRange = contentItemSize.height() - viewportItemSize.height();
329
330     return QRectF(QPointF(0, 0), QSizeF(horizontalRange, verticalRange));
331 }
332
333 void QtViewportInteractionEngine::focusEditableArea(const QRectF& caretArea, const QRectF& targetArea)
334 {
335     QRectF endArea = itemRectFromCSS(targetArea);
336
337     qreal endItemScale = itemScaleFromCSS(innerBoundedCSSScale(2.0));
338     const QRectF viewportRect = m_viewport->boundingRect();
339
340     qreal x;
341     const qreal borderOffset = 10;
342     if ((endArea.width() + borderOffset) * endItemScale <= viewportRect.width()) {
343         // Center the input field in the middle of the view, if it is smaller than
344         // the view at the scale target.
345         x = viewportRect.center().x() - endArea.width() * endItemScale / 2.0;
346     } else {
347         // Ensure that the caret always has borderOffset contents pixels to the right
348         // of it, and secondarily (if possible), that the area has borderOffset
349         // contents pixels to the left of it.
350         qreal caretOffset = itemCoordFromCSS(caretArea.x()) - endArea.x();
351         x = qMin(viewportRect.width() - (caretOffset + borderOffset) * endItemScale, borderOffset * endItemScale);
352     }
353
354     const QPointF hotspot = QPointF(endArea.x(), endArea.center().y());
355     const QPointF viewportHotspot = QPointF(x, /* FIXME: visibleCenter */ viewportRect.center().y());
356
357     QPointF endPosition = hotspot * endItemScale - viewportHotspot;
358     QRectF endPosRange = computePosRangeForItemAtScale(endItemScale);
359
360     endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight());
361
362     QRectF endVisibleContentRect(endPosition / endItemScale, viewportRect.size() / endItemScale);
363
364     animateItemRectVisible(endVisibleContentRect);
365 }
366
367 void QtViewportInteractionEngine::zoomToAreaGestureEnded(const QPointF& touchPoint, const QRectF& targetArea)
368 {
369     if (!targetArea.isValid())
370         return;
371
372     if (scrollAnimationActive() || scaleAnimationActive())
373         return;
374
375     m_hadUserInteraction = true;
376
377     const int margin = 10; // We want at least a little bit of margin.
378     QRectF endArea = itemRectFromCSS(targetArea.adjusted(-margin, -margin, margin, margin));
379
380     const QRectF viewportRect = m_viewport->boundingRect();
381
382     qreal targetCSSScale = viewportRect.size().width() / endArea.size().width();
383     qreal endCSSScale = innerBoundedCSSScale(qMin(targetCSSScale, qreal(2.5)));
384     qreal endItemScale = itemScaleFromCSS(endCSSScale);
385     qreal currentScale = m_content->contentsScale();
386
387     // We want to end up with the target area filling the whole width of the viewport (if possible),
388     // and centralized vertically where the user requested zoom. Thus our hotspot is the center of
389     // the targetArea x-wise and the requested zoom position, y-wise.
390     const QPointF hotspot = QPointF(endArea.center().x(), itemCoordFromCSS(touchPoint.y()));
391     const QPointF viewportHotspot = viewportRect.center();
392
393     QPointF endPosition = hotspot * endCSSScale - viewportHotspot;
394
395     QRectF endPosRange = computePosRangeForItemAtScale(endItemScale);
396     endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight());
397
398     QRectF endVisibleContentRect(endPosition / endItemScale, viewportRect.size() / endItemScale);
399
400     enum { ZoomIn, ZoomBack, ZoomOut, NoZoom } zoomAction = ZoomIn;
401
402     if (!m_scaleStack.isEmpty()) {
403         // Zoom back out if attempting to scale to the same current scale, or
404         // attempting to continue scaling out from the inner most level.
405         // Use fuzzy compare with a fixed error to be able to deal with largish differences due to pixel rounding.
406         if (fuzzyCompare(endItemScale, currentScale, 0.01)) {
407             // If moving the viewport would expose more of the targetRect and move at least 40 pixels, update position but do not scale out.
408             QRectF currentContentRect(m_viewport->contentPos() / currentScale, viewportRect.size() / currentScale);
409             QRectF targetIntersection = endVisibleContentRect.intersected(targetArea);
410             if (!currentContentRect.contains(targetIntersection) && (qAbs(endVisibleContentRect.top() - currentContentRect.top()) >= 40 || qAbs(endVisibleContentRect.left() - currentContentRect.left()) >= 40))
411                 zoomAction = NoZoom;
412             else
413                 zoomAction = ZoomBack;
414         } else if (fuzzyCompare(endItemScale, m_zoomOutScale, 0.01))
415             zoomAction = ZoomBack;
416         else if (endItemScale < currentScale)
417             zoomAction = ZoomOut;
418     }
419
420     switch (zoomAction) {
421     case ZoomIn:
422         m_scaleStack.append(ScaleStackItem(currentScale, m_viewport->contentPos().x()));
423         m_zoomOutScale = endItemScale;
424         break;
425     case ZoomBack: {
426         ScaleStackItem lastScale = m_scaleStack.takeLast();
427         endItemScale = lastScale.scale;
428         endCSSScale = cssScaleFromItem(lastScale.scale);
429         // Recalculate endPosition and bound it according to new scale.
430         endPosition.setY(hotspot.y() * endCSSScale - viewportHotspot.y());
431         endPosition.setX(lastScale.xPosition);
432         endPosRange = computePosRangeForItemAtScale(endItemScale);
433         endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight());
434         endVisibleContentRect = QRectF(endPosition / endItemScale, viewportRect.size() / endItemScale);
435         break;
436     }
437     case ZoomOut:
438         // Unstack all scale-levels deeper than the new level, so a zoom-back won't end up zooming in.
439         while (!m_scaleStack.isEmpty() && m_scaleStack.last().scale >= endItemScale)
440             m_scaleStack.removeLast();
441         m_zoomOutScale = endItemScale;
442         break;
443     case NoZoom:
444         break;
445     }
446
447     animateItemRectVisible(endVisibleContentRect);
448 }
449
450 bool QtViewportInteractionEngine::ensureContentWithinViewportBoundary(bool immediate)
451 {
452     if (scrollAnimationActive() || scaleAnimationActive())
453         return false;
454
455     qreal endItemScale = itemScaleFromCSS(innerBoundedCSSScale(currentCSSScale()));
456
457     const QRectF viewportRect = m_viewport->boundingRect();
458     QPointF viewportHotspot = viewportRect.center();
459
460     QPointF endPosition = m_viewport->mapToWebContent(viewportHotspot) * endItemScale - viewportHotspot;
461
462     QRectF endPosRange = computePosRangeForItemAtScale(endItemScale);
463     endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight());
464
465     QRectF endVisibleContentRect(endPosition / endItemScale, viewportRect.size() / endItemScale);
466
467     if (immediate) {
468         setItemRectVisible(endVisibleContentRect);
469         return true;
470     }
471     return !animateItemRectVisible(endVisibleContentRect);
472 }
473
474 void QtViewportInteractionEngine::reset()
475 {
476     ASSERT(!m_suspendCount);
477
478     m_hadUserInteraction = false;
479
480     m_allowsUserScaling = false;
481     m_minimumScale = 1;
482     m_maximumScale = 1;
483     m_devicePixelRatio = 1;
484     m_pinchStartScale = -1;
485     m_zoomOutScale = 0.0;
486
487     m_viewport->cancelFlick();
488     m_scaleAnimation->stop();
489     m_scaleUpdateDeferrer.clear();
490     m_scrollUpdateDeferrer.clear();
491     m_scaleStack.clear();
492 }
493
494 void QtViewportInteractionEngine::setCSSScaleBounds(qreal minimum, qreal maximum)
495 {
496     m_minimumScale = minimum;
497     m_maximumScale = maximum;
498 }
499
500 void QtViewportInteractionEngine::setCSSScale(qreal scale)
501 {
502     ViewportUpdateDeferrer guard(this);
503
504     qreal newScale = innerBoundedCSSScale(scale);
505     m_content->setContentsScale(itemScaleFromCSS(newScale));
506 }
507
508 qreal QtViewportInteractionEngine::currentCSSScale()
509 {
510     return cssScaleFromItem(m_content->contentsScale());
511 }
512
513 bool QtViewportInteractionEngine::scrollAnimationActive() const
514 {
515     return m_viewport->isFlicking();
516 }
517
518 bool QtViewportInteractionEngine::panGestureActive() const
519 {
520     return m_viewport->isDragging();
521 }
522
523 void QtViewportInteractionEngine::panGestureStarted(const QPointF& position, qint64 eventTimestampMillis)
524 {
525     m_hadUserInteraction = true;
526     m_viewport->handleFlickableMousePress(position, eventTimestampMillis);
527     m_lastPinchCenterInViewportCoordinates = position;
528 }
529
530 void QtViewportInteractionEngine::panGestureRequestUpdate(const QPointF& position, qint64 eventTimestampMillis)
531 {
532     m_viewport->handleFlickableMouseMove(position, eventTimestampMillis);
533     m_lastPinchCenterInViewportCoordinates = position;
534 }
535
536 void QtViewportInteractionEngine::panGestureEnded(const QPointF& position, qint64 eventTimestampMillis)
537 {
538     m_viewport->handleFlickableMouseRelease(position, eventTimestampMillis);
539     m_lastPinchCenterInViewportCoordinates = position;
540 }
541
542 void QtViewportInteractionEngine::panGestureCancelled()
543 {
544     // Reset the velocity samples of the flickable.
545     // This should only be called by the recognizer if we have a recognized
546     // pan gesture and receive a touch event with multiple touch points
547     // (ie. transition to a pinch gesture) as it does not move the content
548     // back inside valid bounds.
549     // When the pinch gesture ends, the content is positioned and scaled
550     // back to valid boundaries.
551     m_viewport->cancelFlick();
552 }
553
554 bool QtViewportInteractionEngine::scaleAnimationActive() const
555 {
556     return m_scaleAnimation->state() == QAbstractAnimation::Running;
557 }
558
559 void QtViewportInteractionEngine::cancelScrollAnimation()
560 {
561     ViewportUpdateDeferrer guard(this);
562
563     // If the pan gesture recognizer receives a touch begin event
564     // during an ongoing kinetic scroll animation of a previous
565     // pan gesture, the animation is stopped and the content is
566     // immediately positioned back to valid boundaries.
567
568     m_viewport->cancelFlick();
569     ensureContentWithinViewportBoundary(/*immediate*/ true);
570 }
571
572 void QtViewportInteractionEngine::interruptScaleAnimation()
573 {
574     // This interrupts the scale animation exactly where it is, even if it is out of bounds.
575     m_scaleAnimation->stop();
576 }
577
578 bool QtViewportInteractionEngine::pinchGestureActive() const
579 {
580     return m_pinchStartScale > 0;
581 }
582
583 void QtViewportInteractionEngine::pinchGestureStarted(const QPointF& pinchCenterInViewportCoordinates)
584 {
585     if (!m_allowsUserScaling)
586         return;
587
588     m_hadUserInteraction = true;
589     m_scaleStack.clear();
590     m_zoomOutScale = 0.0;
591
592     m_scaleUpdateDeferrer = adoptPtr(new ViewportUpdateDeferrer(this, ViewportUpdateDeferrer::DeferUpdateAndSuspendContent));
593
594     m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates;
595     m_pinchStartScale = m_content->contentsScale();
596
597     // Reset the tiling look-ahead vector so that tiles all around the viewport will be requested on pinch-end.
598     emit contentViewportChanged(QPointF());
599 }
600
601 void QtViewportInteractionEngine::pinchGestureRequestUpdate(const QPointF& pinchCenterInViewportCoordinates, qreal totalScaleFactor)
602 {
603     ASSERT(m_suspendCount);
604
605     if (!m_allowsUserScaling)
606         return;
607
608     //  Changes of the center position should move the page even if the zoom factor
609     //  does not change.
610     const qreal cssScale = cssScaleFromItem(m_pinchStartScale * totalScaleFactor);
611
612     // Allow zooming out beyond mimimum scale on pages that do not explicitly disallow it.
613     const qreal targetCSSScale = outerBoundedCSSScale(cssScale);
614
615     scaleContent(m_viewport->mapToWebContent(pinchCenterInViewportCoordinates), targetCSSScale);
616
617     const QPointF positionDiff = pinchCenterInViewportCoordinates - m_lastPinchCenterInViewportCoordinates;
618     m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates;
619
620     m_viewport->setContentPos(m_viewport->contentPos() - positionDiff);
621 }
622
623 void QtViewportInteractionEngine::pinchGestureEnded()
624 {
625     ASSERT(m_suspendCount);
626
627     if (!m_allowsUserScaling)
628         return;
629
630     m_pinchStartScale = -1;
631     // Clear the update deferrer now if we're in our final position and there won't be any animation to clear it later.
632     if (ensureContentWithinViewportBoundary())
633         m_scaleUpdateDeferrer.clear();
634 }
635
636 void QtViewportInteractionEngine::pinchGestureCancelled()
637 {
638     m_pinchStartScale = -1;
639     m_scaleUpdateDeferrer.clear();
640 }
641
642 void QtViewportInteractionEngine::itemSizeChanged()
643 {
644     // FIXME: This needs to be done smarter. What happens if it resizes when we were interacting?
645     if (m_suspendCount)
646         return;
647
648     ViewportUpdateDeferrer guard(this);
649     ensureContentWithinViewportBoundary(true);
650 }
651
652 void QtViewportInteractionEngine::scaleContent(const QPointF& centerInCSSCoordinates, qreal cssScale)
653 {
654     QPointF oldPinchCenterOnViewport = m_viewport->mapFromWebContent(centerInCSSCoordinates);
655     m_content->setContentsScale(itemScaleFromCSS(cssScale));
656     QPointF newPinchCenterOnViewport = m_viewport->mapFromWebContent(centerInCSSCoordinates);
657
658     m_viewport->setContentPos(m_viewport->contentPos() + (newPinchCenterOnViewport - oldPinchCenterOnViewport));
659 }
660
661 } // namespace WebKit
662
663 #include "moc_QtViewportInteractionEngine.cpp"
664
665