diff options
| -rw-r--r-- | src/quicktemplates/qquickscrollbar.cpp | 36 | ||||
| -rw-r--r-- | src/quicktemplates/qquickscrollbar_p_p.h | 4 | ||||
| -rw-r--r-- | tests/auto/quickcontrols/controls/data/tst_scrollbar.qml | 106 |
3 files changed, 143 insertions, 3 deletions
diff --git a/src/quicktemplates/qquickscrollbar.cpp b/src/quicktemplates/qquickscrollbar.cpp index 58963226e3..9353563eab 100644 --- a/src/quicktemplates/qquickscrollbar.cpp +++ b/src/quicktemplates/qquickscrollbar.cpp @@ -285,13 +285,13 @@ bool QQuickScrollBarPrivate::handlePress(const QPointF &point, ulong timestamp) qreal sz = qMax(size, logicalPosition(minimumSize)); if (offset < 0 || offset > sz) offset = sz / 2; + dragPoint = point; q->setPressed(true); return true; } bool QQuickScrollBarPrivate::handleMove(const QPointF &point, ulong timestamp) { - Q_Q(QQuickScrollBar); QQuickControlPrivate::handleMove(point, timestamp); /* @@ -307,7 +307,7 @@ bool QQuickScrollBarPrivate::handleMove(const QPointF &point, ulong timestamp) qreal pos = qMax<qreal>(0.0, qMin<qreal>(positionAt(point) - offset, 1.0 - size)); if (snapMode == QQuickScrollBar::SnapAlways) pos = snapPosition(pos); - q->setPosition(pos); + setPositionFromDrag(pos, point); return true; } @@ -327,8 +327,9 @@ bool QQuickScrollBarPrivate::handleRelease(const QPointF &point, ulong timestamp qreal pos = qMax<qreal>(0.0, qMin<qreal>(positionAt(point) - offset, 1.0 - size)); if (snapMode != QQuickScrollBar::NoSnap) pos = snapPosition(pos); - q->setPosition(pos); + setPositionFromDrag(pos, point); offset = 0.0; + dragPoint = {}; q->setPressed(false); return true; } @@ -338,6 +339,7 @@ void QQuickScrollBarPrivate::handleUngrab() Q_Q(QQuickScrollBar); QQuickControlPrivate::handleUngrab(); offset = 0.0; + dragPoint = {}; q->setPressed(false); } @@ -421,6 +423,8 @@ void QQuickScrollBar::setSize(qreal size) if (d->size + d->position > 1.0) { d->setPosition(1.0 - d->size, false); } + // positionAt() depends on the size. + d->reanchorDragOffset(d->position); if (isComponentComplete()) d->resizeContent(); @@ -459,6 +463,23 @@ void QQuickScrollBar::setPosition(qreal position) void QQuickScrollBarPrivate::setPosition(qreal newPosition, bool notifyVisualChange) { + // An attached Flickable writes the drag's own position back here unchanged, and those + // writes must not touch offset - see setPositionFromDrag(). + if (qt_is_finite(newPosition) && !qFuzzyCompare(position, newPosition)) + reanchorDragOffset(newPosition); + updatePosition(newPosition, notifyVisualChange); +} + +void QQuickScrollBarPrivate::setPositionFromDrag(qreal newPosition, const QPointF &point) +{ + dragPoint = point; + // Bypass setPosition(): offset is already accounted for in newPosition, so re-anchoring it + // would fold the clamping and the snapping applied by the caller back into it. + updatePosition(newPosition, true /*notifyVisualChange*/); +} + +void QQuickScrollBarPrivate::updatePosition(qreal newPosition, bool notifyVisualChange) +{ Q_Q(QQuickScrollBar); if (!qt_is_finite(newPosition) || qFuzzyCompare(position, newPosition)) return; @@ -483,6 +504,15 @@ void QQuickScrollBarPrivate::setPosition(qreal newPosition, bool notifyVisualCha #endif } +void QQuickScrollBarPrivate::reanchorDragOffset(qreal newPosition) +{ + // offset is the distance between the handle and the pointer holding it. Moving the handle, + // or changing what positionAt() returns, makes that distance stale: measure it again, or + // the next handleMove() would put the handle back where it was (QTBUG-130890). + if (pressed) + offset = positionAt(dragPoint) - newPosition; +} + /*! \qmlproperty real QtQuick.Controls::ScrollBar::stepSize diff --git a/src/quicktemplates/qquickscrollbar_p_p.h b/src/quicktemplates/qquickscrollbar_p_p.h index 17e35f3737..5c2f63050f 100644 --- a/src/quicktemplates/qquickscrollbar_p_p.h +++ b/src/quicktemplates/qquickscrollbar_p_p.h @@ -47,6 +47,9 @@ public: qreal logicalPosition(qreal position) const; void setPosition(qreal position, bool notifyVisualChange = true); + void setPositionFromDrag(qreal position, const QPointF &point); + void updatePosition(qreal position, bool notifyVisualChange); + void reanchorDragOffset(qreal position); qreal snapPosition(qreal position) const; qreal positionAt(const QPointF &point) const; void setInteractive(bool interactive); @@ -71,6 +74,7 @@ public: qreal stepSize = 0; qreal offset = 0; qreal minimumSize = 0; + QPointF dragPoint; bool active = false; bool pressed = false; bool moving = false; diff --git a/tests/auto/quickcontrols/controls/data/tst_scrollbar.qml b/tests/auto/quickcontrols/controls/data/tst_scrollbar.qml index f3bd452a69..a663728453 100644 --- a/tests/auto/quickcontrols/controls/data/tst_scrollbar.qml +++ b/tests/auto/quickcontrols/controls/data/tst_scrollbar.qml @@ -53,6 +53,49 @@ TestCase { } } + component SalmonRect: Rectangle { color: "salmon" } + component SteelBlueRect: Rectangle { color: "steelblue" } + + // The reproducer from QTBUG-130890. + Component { + id: variableSizeDelegatesListView + ListView { + id: objectModelList + objectName: "variableSizeDelegatesListView" + width: 400 + height: 400 + ScrollBar.vertical: ScrollBar { + // The iOS style leaves the implicit width at 0, so give the bar a width of + // its own, or there is nothing to press. + width: 20 + } + model: ObjectModel { + SalmonRect { width: objectModelList.width; height: 400 } + SteelBlueRect { width: objectModelList.width; height: 100 } + SalmonRect { width: objectModelList.width; height: 600 } + SteelBlueRect { width: objectModelList.width; height: 40 } + SalmonRect { width: objectModelList.width; height: 50 } + SteelBlueRect { width: objectModelList.width; height: 10 } + SalmonRect { width: objectModelList.width; height: 50 } + SteelBlueRect { width: objectModelList.width; height: 30 } + SalmonRect { width: objectModelList.width; height: 50 } + SteelBlueRect { width: objectModelList.width; height: 100 } + SalmonRect { width: objectModelList.width; height: 200 } + SteelBlueRect { width: objectModelList.width; height: 30 } + SalmonRect { width: objectModelList.width; height: 300 } + SteelBlueRect { width: objectModelList.width; height: 30 } + SalmonRect { width: objectModelList.width; height: 600 } + SteelBlueRect { width: objectModelList.width; height: 40 } + SalmonRect { width: objectModelList.width; height: 50 } + SteelBlueRect { width: objectModelList.width; height: 10 } + SalmonRect { width: objectModelList.width; height: 50 } + SteelBlueRect { width: objectModelList.width; height: 30 } + SalmonRect { width: objectModelList.width; height: 50 } + SteelBlueRect { width: objectModelList.width; height: 100 } + } + } + } + function init() { failOnWarning(/.?/) } @@ -1004,4 +1047,67 @@ TestCase { compare(spy.count, 1) compare(control.visualSize, 0.5) } + + function test_dragHandleWithVariableSizeDelegates_data() { + return [ + { tag: "TopToBottom", verticalLayoutDirection: ListView.TopToBottom }, + { tag: "BottomToTop", verticalLayoutDirection: ListView.BottomToTop }, + ] + } + + // Dragged slowly, one pixel at a time, the way it was reported: the content must follow the + // pointer even though the drag keeps changing the contentHeight estimate, and with it the + // mapping from position to contentY. The handle itself may move and resize as the estimate + // is refined (QTBUG-130890). + function test_dragHandleWithVariableSizeDelegates(data) { + let listView = createTemporaryObject(variableSizeDelegatesListView, testCase, + { verticalLayoutDirection: data.verticalLayoutDirection }) + verify(listView) + let control = listView.ScrollBar.vertical + verify(control) + waitForRendering(listView) + + // A re-estimation can legitimately nudge contentY by a fraction of a pixel. + const tolerance = 1.0 + const steps = 80 + const x = control.width / 2 + // Drag away from whichever end the view starts at, so that there is room to move. + const direction = control.position < 0.5 ? 1 : -1 + const yStart = control.topPadding + control.position * control.availableHeight + 2 + const contentHeightAtPress = listView.contentHeight + + mousePress(control, x, yStart) + verify(control.pressed) + + let contentHeightChanged = false + let previousContentY = listView.contentY + + function verifyContentYFollowsDrag(i, sign) { + if (listView.contentHeight !== contentHeightAtPress) + contentHeightChanged = true + verify((listView.contentY - previousContentY) * sign >= -tolerance, + "step " + i + ": expected contentY to follow the drag, but it went from " + + previousContentY + " to " + listView.contentY + + " while contentHeight is " + listView.contentHeight + + " and originY is " + listView.originY) + previousContentY = listView.contentY + } + + for (let i = 1; i <= steps; ++i) { + mouseMove(control, x, yStart + direction * i) + verifyContentYFollowsDrag(i, direction) + } + + verify(contentHeightChanged, + "the test is inconclusive: contentHeight was never re-estimated during the drag") + + // Reverse the drag without releasing. + for (let i = steps - 1; i >= 0; --i) { + mouseMove(control, x, yStart + direction * i) + verifyContentYFollowsDrag(i, -direction) + } + + mouseRelease(control, x, yStart) + verify(!control.pressed) + } } |
