aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/quicktemplates/qquickscrollbar.cpp36
-rw-r--r--src/quicktemplates/qquickscrollbar_p_p.h4
-rw-r--r--tests/auto/quickcontrols/controls/data/tst_scrollbar.qml106
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)
+ }
}