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