1c8aceeaac8e71db71d100d1d5a6a31ea422780d
[WebKit.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 "PassOwnPtr.h"
26 #include <QPointF>
27 #include <QScrollEvent>
28 #include <QScrollPrepareEvent>
29 #include <QScrollerProperties>
30 #include <QtDeclarative/qquickitem.h>
31
32 namespace WebKit {
33
34 static const int kScaleAnimationDurationMillis = 250;
35
36 // Updating content properties cause the notify signals to be sent by the content item itself.
37 // We manage these differently, as we do not want to act on them when we are the ones changing the content.
38 //
39 // This guard makes sure that the signal viewportUpdateRequested() is sent only when necessary.
40 //
41 // When multiple guards are alive, their lifetime must be perfectly imbricated (e.g. if used ouside stack
42 // frames). We rely on the first one to trigger the update at the end.
43 //
44 // Our public QtViewportInteractionEngine methods should use the guard if they update content in any situation.
45
46 class ViewportUpdateGuard {
47 public:
48     ViewportUpdateGuard(QtViewportInteractionEngine* engine)
49         : engine(engine)
50         , startPosition(engine->m_content->pos())
51         , startScale(engine->m_content->scale())
52         , startWidth(engine->m_content->width())
53         , startHeight(engine->m_content->height())
54
55     {
56         ++(engine->m_pendingUpdates);
57     }
58
59     ~ViewportUpdateGuard()
60     {
61         if (--(engine->m_pendingUpdates))
62             return;
63
64         // Make sure that tiles all around the viewport will be requested.
65         emit engine->viewportTrajectoryVectorChanged(QPointF());
66
67         QQuickItem* item = engine->m_content;
68         bool needsRepaint = startPosition != item->pos() || startScale != item->scale()
69             || startWidth != item->width() || startHeight != item->height();
70
71         // We must notify the change so the client can rely on us for all changes of geometry.
72         if (needsRepaint)
73             emit engine->viewportUpdateRequested();
74     }
75
76 private:
77     QtViewportInteractionEngine* const engine;
78
79     const QPointF startPosition;
80     const qreal startScale;
81     const qreal startWidth;
82     const qreal startHeight;
83 };
84
85 inline qreal QtViewportInteractionEngine::cssScaleFromItem(qreal itemScale)
86 {
87     return itemScale / m_constraints.devicePixelRatio;
88 }
89
90 inline qreal QtViewportInteractionEngine::itemScaleFromCSS(qreal cssScale)
91 {
92     return cssScale * m_constraints.devicePixelRatio;
93 }
94
95 QtViewportInteractionEngine::QtViewportInteractionEngine(const QQuickItem* viewport, QQuickItem* content)
96     : m_viewport(viewport)
97     , m_content(content)
98     , m_pendingUpdates(0)
99     , m_scaleAnimation(new ScaleAnimation(this))
100     , m_pinchStartScale(1.f)
101 {
102     reset();
103
104     connect(m_content, SIGNAL(widthChanged()), this, SLOT(itemSizeChanged()), Qt::DirectConnection);
105     connect(m_content, SIGNAL(heightChanged()), this, SLOT(itemSizeChanged()), Qt::DirectConnection);
106
107     connect(m_scaleAnimation, SIGNAL(valueChanged(QVariant)),
108             SLOT(scaleAnimationValueChanged(QVariant)), Qt::DirectConnection);
109     connect(m_scaleAnimation, SIGNAL(stateChanged(QAbstractAnimation::State, QAbstractAnimation::State)),
110             SLOT(scaleAnimationStateChanged(QAbstractAnimation::State, QAbstractAnimation::State)), Qt::DirectConnection);
111 }
112
113 QtViewportInteractionEngine::~QtViewportInteractionEngine()
114 {
115 }
116
117 qreal QtViewportInteractionEngine::innerBoundedCSSScale(qreal cssScale)
118 {
119     return qBound(m_constraints.minimumScale, cssScale, m_constraints.maximumScale);
120 }
121
122 qreal QtViewportInteractionEngine::outerBoundedCSSScale(qreal cssScale)
123 {
124     if (m_constraints.isUserScalable) {
125         // Bounded by [0.1, 10.0] like the viewport meta code in WebCore.
126         qreal hardMin = qMax<qreal>(0.1, qreal(0.5) * m_constraints.minimumScale);
127         qreal hardMax = qMin<qreal>(10, qreal(2.0) * m_constraints.maximumScale);
128         return qBound(hardMin, cssScale, hardMax);
129     }
130     return innerBoundedCSSScale(cssScale);
131 }
132
133 void QtViewportInteractionEngine::setItemRectVisible(const QRectF& itemRect)
134 {
135     ViewportUpdateGuard guard(this);
136
137     qreal itemScale = m_viewport->width() / itemRect.width();
138
139     m_content->setScale(itemScale);
140
141     // We need to animate the content but the position represents the viewport hence we need to invert the position here.
142     // To animate the position together with the scale we multiply the position with the current scale;
143     m_content->setPos(- itemRect.topLeft() * itemScale);
144 }
145
146 void QtViewportInteractionEngine::animateItemRectVisible(const QRectF& itemRect)
147 {
148     QRectF currentItemRectVisible = m_content->mapRectFromItem(m_viewport, m_viewport->boundingRect());
149     if (itemRect == currentItemRectVisible)
150         return;
151
152     m_scaleAnimation->setDuration(kScaleAnimationDurationMillis);
153     m_scaleAnimation->setEasingCurve(QEasingCurve::OutCubic);
154
155     m_scaleAnimation->setStartValue(currentItemRectVisible);
156     m_scaleAnimation->setEndValue(itemRect);
157
158     m_scaleAnimation->start();
159 }
160
161 void QtViewportInteractionEngine::scaleAnimationStateChanged(QAbstractAnimation::State newState, QAbstractAnimation::State /*oldState*/)
162 {
163     switch (newState) {
164     case QAbstractAnimation::Running:
165         m_pinchViewportUpdateDeferrer = adoptPtr(new ViewportUpdateGuard(this));
166         break;
167     case QAbstractAnimation::Stopped:
168         m_pinchViewportUpdateDeferrer.clear();
169         break;
170     default:
171         break;
172     }
173 }
174
175 bool QtViewportInteractionEngine::event(QEvent* event)
176 {
177     switch (event->type()) {
178     case QEvent::ScrollPrepare: {
179         QScrollPrepareEvent* prepareEvent = static_cast<QScrollPrepareEvent*>(event);
180         const QRectF viewportRect = m_viewport->boundingRect();
181         const QRectF contentRect = m_viewport->mapRectFromItem(m_content, m_content->boundingRect());
182         const QRectF posRange = computePosRangeForItemAtScale(m_content->scale());
183         prepareEvent->setContentPosRange(posRange);
184         prepareEvent->setViewportSize(viewportRect.size());
185
186         // As we want to push the contents and not actually scroll it, we need to invert the positions here.
187         prepareEvent->setContentPos(-contentRect.topLeft());
188         prepareEvent->accept();
189         return true;
190     }
191     case QEvent::Scroll: {
192         QScrollEvent* scrollEvent = static_cast<QScrollEvent*>(event);
193         QPointF newPos = -scrollEvent->contentPos() - scrollEvent->overshootDistance();
194         if (m_content->pos() != newPos) {
195             ViewportUpdateGuard guard(this);
196
197             QPointF currentPosInContentCoordinates = m_content->mapToItem(m_content->parentItem(), m_content->pos());
198             QPointF newPosInContentCoordinates = m_content->mapToItem(m_content->parentItem(), newPos);
199
200             // This must be emitted before viewportUpdateRequested so that the web process knows where to look for tiles.
201             emit viewportTrajectoryVectorChanged(currentPosInContentCoordinates- newPosInContentCoordinates);
202             m_content->setPos(newPos);
203         }
204         return true;
205     }
206     default:
207         break;
208     }
209     return QObject::event(event);
210 }
211
212 static inline QPointF boundPosition(const QPointF minPosition, const QPointF& position, const QPointF& maxPosition)
213 {
214     return QPointF(qBound(minPosition.x(), position.x(), maxPosition.x()),
215                    qBound(minPosition.y(), position.y(), maxPosition.y()));
216 }
217
218 void QtViewportInteractionEngine::pagePositionRequest(const QPoint& pagePosition)
219 {
220     // FIXME: Assert when we are suspending properly.
221     if (scrollAnimationActive() || scaleAnimationActive() || pinchGestureActive())
222         return; // Ignore.
223
224     qreal endItemScale = m_content->scale(); // Stay at same scale.
225
226     QRectF endPosRange = computePosRangeForItemAtScale(endItemScale);
227     QPointF endPosition = boundPosition(endPosRange.topLeft(), pagePosition * endItemScale, endPosRange.bottomRight());
228
229     QRectF endVisibleContentRect(endPosition / endItemScale, m_viewport->boundingRect().size() / endItemScale);
230
231     setItemRectVisible(endVisibleContentRect);
232 }
233
234 QRectF QtViewportInteractionEngine::computePosRangeForItemAtScale(qreal itemScale) const
235 {
236     const QSizeF contentItemSize = m_content->boundingRect().size() * itemScale;
237     const QSizeF viewportItemSize = m_viewport->boundingRect().size();
238
239     const qreal horizontalRange = contentItemSize.width() - viewportItemSize.width();
240     const qreal verticalRange = contentItemSize.height() - viewportItemSize.height();
241
242     return QRectF(QPointF(0, 0), QSizeF(horizontalRange, verticalRange));
243 }
244
245 void QtViewportInteractionEngine::zoomToAreaGestureEnded(const QPointF& touchPoint, const QRectF& targetArea)
246 {
247     if (!targetArea.isValid())
248         return;
249
250     if (scrollAnimationActive() || scaleAnimationActive())
251         return;
252
253     const int margin = 10; // We want at least a little bit or margin.
254     QRectF endArea = targetArea.adjusted(-margin, -margin, margin, margin);
255     endArea.setX(endArea.x() * m_constraints.devicePixelRatio);
256     endArea.setY(endArea.y() * m_constraints.devicePixelRatio);
257     endArea.setWidth(endArea.width() * m_constraints.devicePixelRatio);
258     endArea.setHeight(endArea.height() * m_constraints.devicePixelRatio);
259
260     const QRectF viewportRect = m_viewport->boundingRect();
261
262     qreal targetCSSScale = cssScaleFromItem(viewportRect.size().width() / endArea.size().width());
263     qreal endItemScale = itemScaleFromCSS(innerBoundedCSSScale(qMin(targetCSSScale, qreal(2.5))));
264
265     // We want to end up with the target area filling the whole width of the viewport (if possible),
266     // and centralized vertically where the user requested zoom. Thus our hotspot is the center of
267     // the targetArea x-wise and the requested zoom position, y-wise.
268     const QPointF hotspot = QPointF(endArea.center().x(), touchPoint.y() * m_constraints.devicePixelRatio);
269     const QPointF viewportHotspot = viewportRect.center();
270
271     QPointF endPosition = hotspot * endItemScale - viewportHotspot;
272
273     QRectF endPosRange = computePosRangeForItemAtScale(endItemScale);
274     endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight());
275
276     QRectF endVisibleContentRect(endPosition / endItemScale, viewportRect.size() / endItemScale);
277
278     animateItemRectVisible(endVisibleContentRect);
279 }
280
281 void QtViewportInteractionEngine::ensureContentWithinViewportBoundary()
282 {
283     if (scrollAnimationActive() || scaleAnimationActive())
284         return;
285
286     qreal currentCSSScale = cssScaleFromItem(m_content->scale());
287     bool userHasScaledContent = m_userInteractionFlags & UserHasScaledContent;
288
289     if (!userHasScaledContent)
290         currentCSSScale = m_constraints.initialScale;
291
292     qreal endItemScale = itemScaleFromCSS(innerBoundedCSSScale(currentCSSScale));
293
294     const QRectF viewportRect = m_viewport->boundingRect();
295     const QPointF viewportHotspot = viewportRect.center();
296
297     QPointF endPosition = m_content->mapFromItem(m_viewport, viewportHotspot) * endItemScale - viewportHotspot;
298
299     QRectF endPosRange = computePosRangeForItemAtScale(endItemScale);
300     endPosition = boundPosition(endPosRange.topLeft(), endPosition, endPosRange.bottomRight());
301
302     QRectF endVisibleContentRect(endPosition / endItemScale, viewportRect.size() / endItemScale);
303
304     if (!userHasScaledContent)
305         setItemRectVisible(endVisibleContentRect);
306     else
307         animateItemRectVisible(endVisibleContentRect);
308 }
309
310 void QtViewportInteractionEngine::reset()
311 {
312     ViewportUpdateGuard guard(this);
313     m_userInteractionFlags = UserHasNotInteractedWithContent;
314
315     scroller()->stop();
316     m_scaleAnimation->stop();
317
318     QScrollerProperties properties = scroller()->scrollerProperties();
319
320     // The QtPanGestureRecognizer is responsible for recognizing the gesture
321     // thus we need to disable the drag start distance.
322     properties.setScrollMetric(QScrollerProperties::DragStartDistance, 0.0);
323
324     // Set some default QScroller constrains to mimic the physics engine of the N9 browser.
325     properties.setScrollMetric(QScrollerProperties::AxisLockThreshold, 0.66);
326     properties.setScrollMetric(QScrollerProperties::ScrollingCurve, QEasingCurve(QEasingCurve::OutExpo));
327     properties.setScrollMetric(QScrollerProperties::DecelerationFactor, 0.05);
328     properties.setScrollMetric(QScrollerProperties::MaximumVelocity, 0.635);
329     properties.setScrollMetric(QScrollerProperties::OvershootDragResistanceFactor, 0.33);
330     properties.setScrollMetric(QScrollerProperties::OvershootScrollDistanceFactor, 0.33);
331
332     scroller()->setScrollerProperties(properties);
333     setConstraints(Constraints());
334 }
335
336 void QtViewportInteractionEngine::setConstraints(const Constraints& constraints)
337 {
338     if (m_constraints == constraints)
339         return;
340
341     ViewportUpdateGuard guard(this);
342     m_constraints = constraints;
343
344     ensureContentWithinViewportBoundary();
345 }
346
347 bool QtViewportInteractionEngine::scrollAnimationActive() const
348 {
349     QScroller* scroller = const_cast<QtViewportInteractionEngine*>(this)->scroller();
350     return scroller->state() == QScroller::Scrolling;
351 }
352
353 void QtViewportInteractionEngine::interruptScrollAnimation()
354 {
355     // Stopping the scroller immediately stops kinetic scrolling and if the view is out of bounds it
356     // is moved inside valid bounds immediately as well. This is the behavior that we want.
357     scroller()->stop();
358 }
359
360 bool QtViewportInteractionEngine::panGestureActive() const
361 {
362     QScroller* scroller = const_cast<QtViewportInteractionEngine*>(this)->scroller();
363     return scroller->state() == QScroller::Pressed || scroller->state() == QScroller::Dragging;
364 }
365
366 void QtViewportInteractionEngine::panGestureStarted(const QPointF& touchPoint, qint64 eventTimestampMillis)
367 {
368     // FIXME: suspend the Web engine (stop animated GIF, etc).
369     m_userInteractionFlags |= UserHasMovedContent;
370     scroller()->handleInput(QScroller::InputPress, m_viewport->mapFromItem(m_content, touchPoint), eventTimestampMillis);
371 }
372
373 void QtViewportInteractionEngine::panGestureRequestUpdate(const QPointF& touchPoint, qint64 eventTimestampMillis)
374 {
375     ViewportUpdateGuard guard(this);
376     scroller()->handleInput(QScroller::InputMove, m_viewport->mapFromItem(m_content, touchPoint), eventTimestampMillis);
377 }
378
379 void QtViewportInteractionEngine::panGestureCancelled()
380 {
381     // Stopping the scroller immediately stops kinetic scrolling and if the view is out of bounds it
382     // is moved inside valid bounds immediately as well. This is the behavior that we want.
383     scroller()->stop();
384 }
385
386 void QtViewportInteractionEngine::panGestureEnded(const QPointF& touchPoint, qint64 eventTimestampMillis)
387 {
388     ViewportUpdateGuard guard(this);
389     scroller()->handleInput(QScroller::InputRelease, m_viewport->mapFromItem(m_content, touchPoint), eventTimestampMillis);
390 }
391
392 bool QtViewportInteractionEngine::scaleAnimationActive() const
393 {
394     return m_scaleAnimation->state() == QAbstractAnimation::Running;
395 }
396
397 void QtViewportInteractionEngine::interruptScaleAnimation()
398 {
399     // This interrupts the scale animation exactly where it is, even if it is out of bounds.
400     m_scaleAnimation->stop();
401 }
402
403 bool QtViewportInteractionEngine::pinchGestureActive() const
404 {
405     return !!m_pinchViewportUpdateDeferrer;
406 }
407
408 void QtViewportInteractionEngine::pinchGestureStarted(const QPointF& pinchCenterInContentCoordinates)
409 {
410     if (!m_constraints.isUserScalable)
411         return;
412
413     m_pinchViewportUpdateDeferrer = adoptPtr(new ViewportUpdateGuard(this));
414     m_lastPinchCenterInViewportCoordinates = m_viewport->mapFromItem(m_content, pinchCenterInContentCoordinates);
415     m_userInteractionFlags |= UserHasScaledContent;
416     m_userInteractionFlags |= UserHasMovedContent;
417     m_pinchStartScale = m_content->scale();
418
419     // Reset the tiling look-ahead vector so that tiles all around the viewport will be requested on pinch-end.
420     emit viewportTrajectoryVectorChanged(QPointF());
421 }
422
423 void QtViewportInteractionEngine::pinchGestureRequestUpdate(const QPointF& pinchCenterInContentCoordinates, qreal totalScaleFactor)
424 {
425     if (!m_constraints.isUserScalable)
426         return;
427
428     ViewportUpdateGuard guard(this);
429
430     //  Changes of the center position should move the page even if the zoom factor
431     //  does not change.
432     const qreal cssScale = cssScaleFromItem(m_pinchStartScale * totalScaleFactor);
433
434     // Allow zooming out beyond mimimum scale on pages that do not explicitly disallow it.
435     const qreal targetCSSScale = outerBoundedCSSScale(cssScale);
436
437     QPointF pinchCenterInViewportCoordinates = m_viewport->mapFromItem(m_content, pinchCenterInContentCoordinates);
438
439     scaleContent(pinchCenterInContentCoordinates, targetCSSScale);
440
441     const QPointF positionDiff = pinchCenterInViewportCoordinates - m_lastPinchCenterInViewportCoordinates;
442     m_lastPinchCenterInViewportCoordinates = pinchCenterInViewportCoordinates;
443     m_content->setPos(m_content->pos() + positionDiff);
444 }
445
446 void QtViewportInteractionEngine::pinchGestureEnded()
447 {
448     if (!m_constraints.isUserScalable)
449         return;
450
451     m_pinchViewportUpdateDeferrer.clear();
452     // FIXME: resume the engine after the animation.
453     ensureContentWithinViewportBoundary();
454 }
455
456 void QtViewportInteractionEngine::itemSizeChanged()
457 {
458     // FIXME: This needs to be done smarter. What happens if it resizes when we were interacting?
459     if (m_pendingUpdates)
460         return;
461
462     ViewportUpdateGuard guard(this);
463     ensureContentWithinViewportBoundary();
464 }
465
466 void QtViewportInteractionEngine::scaleContent(const QPointF& centerInContentCoordinates, qreal cssScale)
467 {
468     QPointF oldPinchCenterOnParent = m_content->mapToItem(m_content->parentItem(), centerInContentCoordinates);
469     m_content->setScale(itemScaleFromCSS(cssScale));
470     QPointF newPinchCenterOnParent = m_content->mapToItem(m_content->parentItem(), centerInContentCoordinates);
471     m_content->setPos(m_content->pos() - (newPinchCenterOnParent - oldPinchCenterOnParent));
472 }
473
474 #include "moc_QtViewportInteractionEngine.cpp"
475
476 }