Support snapMode in PathView
authorMartin Jones <martin.jones@nokia.com>
Wed, 21 Mar 2012 03:18:05 +0000 (13:18 +1000)
committerQt by Nokia <qt-info@nokia.com>
Wed, 4 Apr 2012 04:00:40 +0000 (06:00 +0200)
PathView missed out on snapMode, which is especially useful for
its SnapOneItem mode.

Change-Id: I0e9b080ef72d9bc1e305445cd8dfde8e21e4e3da
Reviewed-by: Bea Lam <bea.lam@nokia.com>

src/quick/items/qquickpathview.cpp
src/quick/items/qquickpathview_p.h
src/quick/items/qquickpathview_p_p.h
tests/auto/quick/qquickpathview/data/panels.qml [new file with mode: 0644]
tests/auto/quick/qquickpathview/tst_qquickpathview.cpp

index 962ddda..836943c 100644 (file)
 
 // The number of samples to use in calculating the velocity of a flick
 #ifndef QML_FLICK_SAMPLEBUFFER
-#define QML_FLICK_SAMPLEBUFFER 3
+#define QML_FLICK_SAMPLEBUFFER 1
 #endif
 
 // The number of samples to discard when calculating the flick velocity.
 // Touch panels often produce inaccurate results as the finger is lifted.
 #ifndef QML_FLICK_DISCARDSAMPLES
-#define QML_FLICK_DISCARDSAMPLES 1
+#define QML_FLICK_DISCARDSAMPLES 0
 #endif
 
 // The default maximum velocity of a flick.
@@ -114,8 +114,8 @@ void QQuickPathViewAttached::setValue(const QByteArray &name, const QVariant &va
 }
 
 QQuickPathViewPrivate::QQuickPathViewPrivate()
-  : path(0), currentIndex(0), currentItemOffset(0.0), startPc(0), lastDist(0)
-    , lastElapsed(0), offset(0.0), offsetAdj(0.0), mappedRange(1.0)
+  : path(0), currentIndex(0), currentItemOffset(0.0), startPc(0)
+    , offset(0.0), offsetAdj(0.0), mappedRange(1.0)
     , stealMouse(false), ownModel(false), interactive(true), haveHighlightRange(true)
     , autoHighlight(true), highlightUp(false), layoutScheduled(false)
     , moving(false), flicking(false), requestedOnPath(false), inRequest(false)
@@ -127,7 +127,7 @@ QQuickPathViewPrivate::QQuickPathViewPrivate()
     , highlightPosition(0)
     , highlightRangeStart(0), highlightRangeEnd(0)
     , highlightRangeMode(QQuickPathView::StrictlyEnforceRange)
-    , highlightMoveDuration(300), modelCount(0)
+    , highlightMoveDuration(300), modelCount(0), snapMode(QQuickPathView::NoSnap)
 {
 }
 
@@ -139,7 +139,7 @@ void QQuickPathViewPrivate::init()
     q->setFlag(QQuickItem::ItemIsFocusScope);
     q->setFiltersChildMouseEvents(true);
     FAST_CONNECT(&tl, SIGNAL(updated()), q, SLOT(ticked()))
-    lastPosTime.invalidate();
+    timer.invalidate();
     FAST_CONNECT(&tl, SIGNAL(completed()), q, SLOT(movementEnding()))
 }
 
@@ -266,7 +266,8 @@ qreal QQuickPathViewPrivate::positionOfIndex(qreal index) const
 
     if (model && index >= 0 && index < modelCount) {
         qreal start = 0.0;
-        if (haveHighlightRange && highlightRangeMode != QQuickPathView::NoHighlightRange)
+        if (haveHighlightRange && (highlightRangeMode != QQuickPathView::NoHighlightRange
+                                   || snapMode != QQuickPathView::NoSnap))
             start = highlightRangeStart;
         qreal globalPos = index + offset;
         globalPos = qmlMod(globalPos, qreal(modelCount)) / modelCount;
@@ -698,7 +699,7 @@ void QQuickPathView::setCurrentIndex(int idx)
         if (d->modelCount) {
             d->createCurrentItem();
             if (d->haveHighlightRange && d->highlightRangeMode == QQuickPathView::StrictlyEnforceRange)
-                d->snapToCurrent();
+                d->snapToIndex(d->currentIndex);
             d->currentItemOffset = d->positionOfIndex(d->currentIndex);
             d->updateHighlight();
         }
@@ -889,7 +890,7 @@ void QQuickPathView::setPreferredHighlightBegin(qreal start)
     if (d->highlightRangeStart == start || start < 0 || start > 1.0)
         return;
     d->highlightRangeStart = start;
-    d->haveHighlightRange = d->highlightRangeMode != NoHighlightRange && d->highlightRangeStart <= d->highlightRangeEnd;
+    d->haveHighlightRange = d->highlightRangeStart <= d->highlightRangeEnd;
     refill();
     emit preferredHighlightBeginChanged();
 }
@@ -906,7 +907,7 @@ void QQuickPathView::setPreferredHighlightEnd(qreal end)
     if (d->highlightRangeEnd == end || end < 0 || end > 1.0)
         return;
     d->highlightRangeEnd = end;
-    d->haveHighlightRange = d->highlightRangeMode != NoHighlightRange && d->highlightRangeStart <= d->highlightRangeEnd;
+    d->haveHighlightRange = d->highlightRangeStart <= d->highlightRangeEnd;
     refill();
     emit preferredHighlightEndChanged();
 }
@@ -923,10 +924,12 @@ void QQuickPathView::setHighlightRangeMode(HighlightRangeMode mode)
     if (d->highlightRangeMode == mode)
         return;
     d->highlightRangeMode = mode;
-    d->haveHighlightRange = d->highlightRangeMode != NoHighlightRange && d->highlightRangeStart <= d->highlightRangeEnd;
+    d->haveHighlightRange = d->highlightRangeStart <= d->highlightRangeEnd;
     if (d->haveHighlightRange) {
         d->regenerate();
-        d->snapToCurrent();
+        int index = d->highlightRangeMode != NoHighlightRange ? d->currentIndex : d->calcCurrentIndex();
+        if (index >= 0)
+            d->snapToIndex(index);
     }
     emit highlightRangeModeChanged();
 }
@@ -1175,6 +1178,41 @@ void QQuickPathView::setPathItemCount(int i)
     emit pathItemCountChanged();
 }
 
+/*!
+    \qmlproperty enumeration QtQuick2::PathView::snapMode
+
+    This property determines how the items will settle following a drag or flick.
+    The possible values are:
+
+    \list
+    \li PathView.NoSnap (default) - the items stop anywhere along the path.
+    \li PathView.SnapToItem - the items settle with an item aligned with the \l preferredHighlightBegin.
+    \li PathView.SnapOneItem - the items settle no more than one item away from the item nearest
+        \l preferredHighlightBegin at the time the press is released.  This mode is particularly
+        useful for moving one page at a time.
+    \endlist
+
+    \c snapMode does not affect the \l currentIndex.  To update the
+    \l currentIndex as the view is moved, set \l highlightRangeMode
+    to \c PathView.StrictlyEnforceRange (default for PathView).
+
+    \sa highlightRangeMode
+*/
+QQuickPathView::SnapMode QQuickPathView::snapMode() const
+{
+    Q_D(const QQuickPathView);
+    return d->snapMode;
+}
+
+void QQuickPathView::setSnapMode(SnapMode mode)
+{
+    Q_D(QQuickPathView);
+    if (mode == d->snapMode)
+        return;
+    d->snapMode = mode;
+    emit snapModeChanged();
+}
+
 QPointF QQuickPathViewPrivate::pointNear(const QPointF &point, qreal *nearPercent) const
 {
     qreal samples = qMin(path->path().length()/5, qreal(500.0));
@@ -1236,6 +1274,15 @@ qreal QQuickPathViewPrivate::calcVelocity() const
     return velocity;
 }
 
+qint64 QQuickPathViewPrivate::computeCurrentTime(QInputEvent *event)
+{
+    if (0 != event->timestamp() && QQuickItemPrivate::consistentTime == -1) {
+        return event->timestamp();
+    }
+
+    return QQuickItemPrivate::elapsed(timer);
+}
+
 void QQuickPathView::mousePressEvent(QMouseEvent *event)
 {
     Q_D(QQuickPathView);
@@ -1277,9 +1324,8 @@ void QQuickPathViewPrivate::handleMousePressEvent(QMouseEvent *event)
     else
         stealMouse = false;
 
-    lastElapsed = 0;
-    lastDist = 0;
-    QQuickItemPrivate::start(lastPosTime);
+    QQuickItemPrivate::start(timer);
+    lastPosTime = computeCurrentTime(event);
     tl.clear();
 }
 
@@ -1299,7 +1345,7 @@ void QQuickPathView::mouseMoveEvent(QMouseEvent *event)
 void QQuickPathViewPrivate::handleMouseMoveEvent(QMouseEvent *event)
 {
     Q_Q(QQuickPathView);
-    if (!interactive || !lastPosTime.isValid() || !model || !modelCount)
+    if (!interactive || !timer.isValid() || !model || !modelCount)
         return;
 
     qreal newPc;
@@ -1308,10 +1354,10 @@ void QQuickPathViewPrivate::handleMouseMoveEvent(QMouseEvent *event)
         QPointF delta = pathPoint - startPoint;
         if (qAbs(delta.x()) > qApp->styleHints()->startDragDistance() || qAbs(delta.y()) > qApp->styleHints()->startDragDistance()) {
             stealMouse = true;
-            startPc = newPc;
         }
     }
 
+    qint64 currentTimestamp = computeCurrentTime(event);
     if (stealMouse) {
         moveReason = QQuickPathViewPrivate::Mouse;
         qreal diff = (newPc - startPc)*modelCount*mappedRange;
@@ -1323,10 +1369,9 @@ void QQuickPathViewPrivate::handleMouseMoveEvent(QMouseEvent *event)
             else if (diff < -modelCount/2)
                 diff += modelCount;
 
-            lastElapsed = QQuickItemPrivate::restart(lastPosTime);
-            lastDist = diff;
-            startPc = newPc;
-            addVelocitySample(diff / (qreal(lastElapsed) / 1000.));
+            qint64 elapsed = currentTimestamp - lastPosTime;
+            if (elapsed > 0)
+                addVelocitySample(diff / (qreal(elapsed) / 1000.));
         }
         if (!moving) {
             moving = true;
@@ -1334,6 +1379,8 @@ void QQuickPathViewPrivate::handleMouseMoveEvent(QMouseEvent *event)
             emit q->movementStarted();
         }
     }
+    startPc = newPc;
+    lastPosTime = currentTimestamp;
 }
 
 void QQuickPathView::mouseReleaseEvent(QMouseEvent *event)
@@ -1353,8 +1400,8 @@ void QQuickPathViewPrivate::handleMouseReleaseEvent(QMouseEvent *)
     Q_Q(QQuickPathView);
     stealMouse = false;
     q->setKeepMouseGrab(false);
-    if (!interactive || !lastPosTime.isValid() || !model || !modelCount) {
-        lastPosTime.invalidate();
+    if (!interactive || !timer.isValid() || !model || !modelCount) {
+        timer.invalidate();
         if (!tl.isActive())
             q->movementEnding();
         return;
@@ -1364,7 +1411,7 @@ void QQuickPathViewPrivate::handleMouseReleaseEvent(QMouseEvent *)
     qreal count = modelCount*mappedRange;
     qreal pixelVelocity = (path->path().length()/count) * velocity;
     if (qAbs(pixelVelocity) > MinimumFlickVelocity) {
-        if (qAbs(pixelVelocity) > maximumFlickVelocity) {
+        if (qAbs(pixelVelocity) > maximumFlickVelocity || snapMode == QQuickPathView::SnapOneItem) {
             // limit velocity
             qreal maxVel = velocity < 0 ? -maximumFlickVelocity : maximumFlickVelocity;
             velocity = maxVel / (path->path().length()/count);
@@ -1373,14 +1420,24 @@ void QQuickPathViewPrivate::handleMouseReleaseEvent(QMouseEvent *)
         qreal v2 = velocity*velocity;
         qreal accel = deceleration/10;
         qreal dist = 0;
-        if (haveHighlightRange && highlightRangeMode == QQuickPathView::StrictlyEnforceRange) {
-            // + 0.25 to encourage moving at least one item in the flick direction
-            dist = qMin(qreal(modelCount-1), qreal(v2 / (accel * 2.0) + 0.25));
-            // round to nearest item.
-            if (velocity > 0.)
-                dist = qRound(dist + offset) - offset;
-            else
-                dist = qRound(dist - offset) + offset;
+        if (haveHighlightRange && (highlightRangeMode == QQuickPathView::StrictlyEnforceRange
+                || snapMode != QQuickPathView::NoSnap)) {
+            if (snapMode == QQuickPathView::SnapOneItem) {
+                // encourage snapping one item in direction of motion
+                if (velocity > 0.)
+                    dist = qRound(0.5 + offset) - offset;
+                else
+                    dist = qRound(0.5 - offset) + offset;
+            } else {
+                // + 0.25 to encourage moving at least one item in the flick direction
+                dist = qMin(qreal(modelCount-1), qreal(v2 / (accel * 2.0) + 0.25));
+
+                // round to nearest item.
+                if (velocity > 0.)
+                    dist = qRound(dist + offset) - offset;
+                else
+                    dist = qRound(dist - offset) + offset;
+            }
             // Calculate accel required to stop on item boundary
             if (dist <= 0.) {
                 dist = 0.;
@@ -1405,7 +1462,7 @@ void QQuickPathViewPrivate::handleMouseReleaseEvent(QMouseEvent *)
         fixOffset();
     }
 
-    lastPosTime.invalidate();
+    timer.invalidate();
     if (!tl.isActive())
         q->movementEnding();
 }
@@ -1441,8 +1498,8 @@ bool QQuickPathView::sendMouseEvent(QMouseEvent *event)
             grabMouse();
 
         return d->stealMouse;
-    } else if (d->lastPosTime.isValid()) {
-        d->lastPosTime.invalidate();
+    } else if (d->timer.isValid()) {
+        d->timer.invalidate();
         d->fixOffset();
     }
     if (event->type() == QEvent::MouseButtonRelease)
@@ -1476,7 +1533,7 @@ void QQuickPathView::mouseUngrabEvent()
         // fix our state
         d->stealMouse = false;
         setKeepMouseGrab(false);
-        d->lastPosTime.invalidate();
+        d->timer.invalidate();
         d->fixOffset();
         if (!d->tl.isActive())
             movementEnding();
@@ -1557,7 +1614,8 @@ void QQuickPathView::refill()
         if (d->items.count() < count) {
             int idx = qRound(d->modelCount - d->offset) % d->modelCount;
             qreal startPos = 0.0;
-            if (d->haveHighlightRange && d->highlightRangeMode != QQuickPathView::NoHighlightRange)
+            if (d->haveHighlightRange && (d->highlightRangeMode != QQuickPathView::NoHighlightRange
+                                          || d->snapMode != QQuickPathView::NoSnap))
                 startPos = d->highlightRangeStart;
             if (d->firstIndex >= 0) {
                 startPos = d->positionOfIndex(d->firstIndex);
@@ -1833,22 +1891,23 @@ void QQuickPathViewPrivate::fixOffset()
 {
     Q_Q(QQuickPathView);
     if (model && items.count()) {
-        if (haveHighlightRange && highlightRangeMode == QQuickPathView::StrictlyEnforceRange) {
+        if (haveHighlightRange && (highlightRangeMode == QQuickPathView::StrictlyEnforceRange
+                || snapMode != QQuickPathView::NoSnap)) {
             int curr = calcCurrentIndex();
-            if (curr != currentIndex)
+            if (curr != currentIndex && highlightRangeMode == QQuickPathView::StrictlyEnforceRange)
                 q->setCurrentIndex(curr);
             else
-                snapToCurrent();
+                snapToIndex(curr);
         }
     }
 }
 
-void QQuickPathViewPrivate::snapToCurrent()
+void QQuickPathViewPrivate::snapToIndex(int index)
 {
     if (!model || modelCount <= 0)
         return;
 
-    qreal targetOffset = qmlMod(modelCount - currentIndex, modelCount);
+    qreal targetOffset = qmlMod(modelCount - index, modelCount);
 
     if (offset == targetOffset)
         return;
index 8b15e85..2c0c106 100644 (file)
@@ -83,8 +83,10 @@ class Q_AUTOTEST_EXPORT QQuickPathView : public QQuickItem
     Q_PROPERTY(int count READ count NOTIFY countChanged)
     Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged)
     Q_PROPERTY(int pathItemCount READ pathItemCount WRITE setPathItemCount NOTIFY pathItemCountChanged)
+    Q_PROPERTY(SnapMode snapMode READ snapMode WRITE setSnapMode NOTIFY snapModeChanged)
 
     Q_ENUMS(HighlightRangeMode)
+    Q_ENUMS(SnapMode)
 
 public:
     QQuickPathView(QQuickItem *parent=0);
@@ -144,6 +146,10 @@ public:
     int pathItemCount() const;
     void setPathItemCount(int);
 
+    enum SnapMode { NoSnap, SnapToItem, SnapOneItem };
+    SnapMode snapMode() const;
+    void setSnapMode(SnapMode mode);
+
     static QQuickPathViewAttached *qmlAttachedProperties(QObject *);
 
 public Q_SLOTS:
@@ -176,6 +182,7 @@ Q_SIGNALS:
     void movementEnded();
     void flickStarted();
     void flickEnded();
+    void snapModeChanged();
 
 protected:
     virtual void updatePolish();
index ac74e9a..3285b40 100644 (file)
@@ -122,10 +122,11 @@ public:
     void setAdjustedOffset(qreal offset);
     void regenerate();
     void updateItem(QQuickItem *, qreal);
-    void snapToCurrent();
+    void snapToIndex(int index);
     QPointF pointNear(const QPointF &point, qreal *nearPercent=0) const;
     void addVelocitySample(qreal v);
     qreal calcVelocity() const;
+    qint64 computeCurrentTime(QInputEvent *event);
 
     QQuickPath *path;
     int currentIndex;
@@ -133,8 +134,6 @@ public:
     qreal currentItemOffset;
     qreal startPc;
     QPointF startPoint;
-    qreal lastDist;
-    int lastElapsed;
     qreal offset;
     qreal offsetAdj;
     qreal mappedRange;
@@ -149,7 +148,8 @@ public:
     bool flicking : 1;
     bool requestedOnPath : 1;
     bool inRequest : 1;
-    QElapsedTimer lastPosTime;
+    QElapsedTimer timer;
+    qint64 lastPosTime;
     QPointF lastPos;
     qreal dragMargin;
     qreal deceleration;
@@ -180,6 +180,7 @@ public:
     int highlightMoveDuration;
     int modelCount;
     QPODVector<qreal,10> velocityBuffer;
+    QQuickPathView::SnapMode snapMode;
 };
 
 QT_END_NAMESPACE
diff --git a/tests/auto/quick/qquickpathview/data/panels.qml b/tests/auto/quick/qquickpathview/data/panels.qml
new file mode 100644 (file)
index 0000000..a111e45
--- /dev/null
@@ -0,0 +1,44 @@
+import QtQuick 2.0
+
+Item {
+    id: root
+    property bool snapOne: false
+    property bool enforceRange: false
+    width: 320; height: 480
+
+    VisualItemModel {
+        id: itemModel
+
+        Rectangle {
+            width: root.width
+            height: root.height
+            color: "blue"
+        }
+        Rectangle {
+            width: root.width
+            height: root.height
+            color: "yellow"
+        }
+        Rectangle {
+            width: root.width
+            height: root.height
+            color: "green"
+        }
+    }
+
+    PathView {
+        id: view
+        objectName: "view"
+        anchors.fill: parent
+        model: itemModel
+        preferredHighlightBegin: 0.5
+        preferredHighlightEnd: 0.5
+        flickDeceleration: 30
+        highlightRangeMode: enforceRange ? PathView.StrictlyEnforceRange : PathView.NoHighlightRange
+        snapMode: root.snapOne ? PathView.SnapOneItem : PathView.SnapToItem
+        path:  Path {
+            startX: -root.width; startY: root.height/2
+            PathLine { x: root.width*2; y: root.height/2 }
+        }
+    }
+}
index 85d2c3b..fbe96bf 100644 (file)
@@ -125,6 +125,10 @@ private slots:
     void asynchronous();
     void cancelDrag();
     void maximumFlickVelocity();
+    void snapToItem();
+    void snapToItem_data();
+    void snapOneItem();
+    void snapOneItem_data();
 };
 
 class TestObject : public QObject
@@ -1541,6 +1545,89 @@ void tst_QQuickPathView::maximumFlickVelocity()
     delete canvas;
 }
 
+void tst_QQuickPathView::snapToItem()
+{
+    QFETCH(bool, enforceRange);
+
+    QQuickView *canvas = createView();
+    canvas->setSource(testFileUrl("panels.qml"));
+    QQuickPathView *pathview = canvas->rootObject()->findChild<QQuickPathView*>("view");
+    QVERIFY(pathview != 0);
+
+    canvas->rootObject()->setProperty("enforceRange", enforceRange);
+    QTRY_VERIFY(!pathview->isMoving()); // ensure stable
+
+    int currentIndex = pathview->currentIndex();
+
+    QSignalSpy snapModeSpy(pathview, SIGNAL(snapModeChanged()));
+
+    flick(canvas, QPoint(200,10), QPoint(10,10), 180);
+
+    QVERIFY(pathview->isMoving());
+    QTRY_VERIFY(!pathview->isMoving());
+
+    QVERIFY(pathview->offset() == qFloor(pathview->offset()));
+
+    if (enforceRange)
+        QVERIFY(pathview->currentIndex() != currentIndex);
+    else
+        QVERIFY(pathview->currentIndex() == currentIndex);
+}
+
+void tst_QQuickPathView::snapToItem_data()
+{
+    QTest::addColumn<bool>("enforceRange");
+
+    QTest::newRow("no enforce range") << false;
+    QTest::newRow("enforce range") << true;
+}
+
+void tst_QQuickPathView::snapOneItem()
+{
+    QFETCH(bool, enforceRange);
+
+    QQuickView *canvas = createView();
+    canvas->setSource(testFileUrl("panels.qml"));
+    canvas->show();
+    canvas->requestActivateWindow();
+    QTest::qWaitForWindowShown(canvas);
+    QTRY_COMPARE(canvas, qGuiApp->focusWindow());
+
+    QQuickPathView *pathview = canvas->rootObject()->findChild<QQuickPathView*>("view");
+    QVERIFY(pathview != 0);
+
+    canvas->rootObject()->setProperty("enforceRange", enforceRange);
+
+    QSignalSpy snapModeSpy(pathview, SIGNAL(snapModeChanged()));
+
+    canvas->rootObject()->setProperty("snapOne", true);
+    QVERIFY(snapModeSpy.count() == 1);
+    QTRY_VERIFY(!pathview->isMoving()); // ensure stable
+
+    int currentIndex = pathview->currentIndex();
+
+    double startOffset = pathview->offset();
+    flick(canvas, QPoint(200,10), QPoint(10,10), 180);
+
+    QVERIFY(pathview->isMoving());
+    QTRY_VERIFY(!pathview->isMoving());
+
+    // must have moved only one item
+    QCOMPARE(pathview->offset(), fmodf(3.0 + startOffset - 1.0, 3.0));
+
+    if (enforceRange)
+        QVERIFY(pathview->currentIndex() == currentIndex+1);
+    else
+        QVERIFY(pathview->currentIndex() == currentIndex);
+}
+
+void tst_QQuickPathView::snapOneItem_data()
+{
+    QTest::addColumn<bool>("enforceRange");
+
+    QTest::newRow("no enforce range") << false;
+    QTest::newRow("enforce range") << true;
+}
 
 
 QTEST_MAIN(tst_QQuickPathView)