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