diff options
Diffstat (limited to 'src/quick/items/qquickflickable.cpp')
-rw-r--r-- | src/quick/items/qquickflickable.cpp | 739 |
1 files changed, 458 insertions, 281 deletions
diff --git a/src/quick/items/qquickflickable.cpp b/src/quick/items/qquickflickable.cpp index b02840208d..715f75cde7 100644 --- a/src/quick/items/qquickflickable.cpp +++ b/src/quick/items/qquickflickable.cpp @@ -1,48 +1,15 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtQuick module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qquickflickable_p.h" #include "qquickflickable_p_p.h" #include "qquickflickablebehavior_p.h" #include "qquickwindow.h" #include "qquickwindow_p.h" -#include "qquickevents_p_p.h" +#include "qquickmousearea_p.h" +#if QT_CONFIG(quick_draganddrop) +#include "qquickdrag_p.h" +#endif #include <QtQuick/private/qquickpointerhandler_p.h> #include <QtQuick/private/qquicktransition_p.h> @@ -52,10 +19,10 @@ #include <QtGui/qevent.h> #include <QtGui/qguiapplication.h> #include <QtGui/private/qguiapplication_p.h> +#include <QtGui/private/qeventpoint_p.h> #include <QtGui/qstylehints.h> -#include <QtGui/qpa/qplatformtheme.h> #include <QtCore/qmath.h> -#include "qplatformdefs.h" +#include <qpa/qplatformtheme.h> #include <math.h> #include <cmath> @@ -69,29 +36,10 @@ Q_LOGGING_CATEGORY(lcReplay, "qt.quick.flickable.replay") Q_LOGGING_CATEGORY(lcWheel, "qt.quick.flickable.wheel") Q_LOGGING_CATEGORY(lcVel, "qt.quick.flickable.velocity") -// FlickThreshold determines how far the "mouse" must have moved -// before we perform a flick. -static const int FlickThreshold = 15; - // RetainGrabVelocity is the maxmimum instantaneous velocity that // will ensure the Flickable retains the grab on consecutive flicks. static const int RetainGrabVelocity = 100; -// Currently std::round can't be used on Android when using ndk g++, so -// use C version instead. We could just define two versions of Round, one -// for float and one for double, but then only one of them would be used -// and compiler would trigger a warning about unused function. -// -// See https://code.google.com/p/android/issues/detail?id=54418 -template<typename T> -static T Round(T t) { - return round(t); -} -template<> -Q_DECL_UNUSED float Round<float>(float f) { - return roundf(f); -} - static qreal EaseOvershoot(qreal t) { return qAtan(t); } @@ -138,7 +86,8 @@ void QQuickFlickableVisibleArea::updateVisible() qreal pagePos = 0; qreal pageSize = 0; if (!qFuzzyIsNull(maxYBounds)) { - pagePos = (-p->vData.move.value() + flickable->minYExtent()) / maxYBounds; + qreal y = p->pixelAligned ? std::round(p->vData.move.value()) : p->vData.move.value(); + pagePos = (-y + flickable->minYExtent()) / maxYBounds; pageSize = viewheight / maxYBounds; } @@ -156,7 +105,8 @@ void QQuickFlickableVisibleArea::updateVisible() const qreal maxxextent = -flickable->maxXExtent() + flickable->minXExtent(); const qreal maxXBounds = maxxextent + viewwidth; if (!qFuzzyIsNull(maxXBounds)) { - pagePos = (-p->hData.move.value() + flickable->minXExtent()) / maxXBounds; + qreal x = p->pixelAligned ? std::round(p->hData.move.value()) : p->hData.move.value(); + pagePos = (-x + flickable->minXExtent()) / maxXBounds; pageSize = viewwidth / maxXBounds; } else { pagePos = 0; @@ -254,11 +204,27 @@ QQuickFlickablePrivate::AxisData::~AxisData() delete transitionToBounds; } - -QVarLengthArray<const QPointingDevice *, 4> QQuickFlickablePrivate::nonClickyWheelMice; +class QQuickFlickableContentItem : public QQuickItem +{ + /*! + \internal + The flickable area inside the viewport can be bigger than the bounds of the + content item itself, if the flickable is using non-zero extents (as returned + by e.g minXExtent()). Since the default implementation in QQuickItem::contains() + only checks if the point is inside the bounds of the item, we need to override it + to check the extents as well. The easist way to do this is to simply check if the + point is inside the bounds of the flickable rather than the content item. + */ + bool contains(const QPointF &point) const override + { + const QQuickItem *flickable = parentItem(); + const QPointF posInFlickable = flickable->mapFromItem(this, point); + return flickable->contains(posInFlickable); + } +}; QQuickFlickablePrivate::QQuickFlickablePrivate() - : contentItem(new QQuickItem) + : contentItem(new QQuickFlickableContentItem) , hData(this, &QQuickFlickablePrivate::setViewportX) , vData(this, &QQuickFlickablePrivate::setViewportY) , hMoved(false), vMoved(false) @@ -268,15 +234,20 @@ QQuickFlickablePrivate::QQuickFlickablePrivate() , syncDrag(false) , lastPosTime(-1) , lastPressTime(0) - , deceleration(QML_FLICK_DEFAULTDECELERATION) - , maxVelocity(QML_FLICK_DEFAULTMAXVELOCITY), reportedVelocitySmoothing(100) + , deceleration(QGuiApplicationPrivate::platformTheme()->themeHint(QPlatformTheme::FlickDeceleration).toReal()) + , wheelDeceleration(15000) + , maxVelocity(QGuiApplicationPrivate::platformTheme()->themeHint(QPlatformTheme::FlickMaximumVelocity).toReal()) , delayedPressEvent(nullptr), pressDelay(0), fixupDuration(400) - , flickBoost(1.0), fixupMode(Normal), vTime(0), visibleArea(nullptr) + , flickBoost(1.0), initialWheelFlickDistance(qApp->styleHints()->wheelScrollLines() * 24) + , fixupMode(Normal), vTime(0), visibleArea(nullptr) , flickableDirection(QQuickFlickable::AutoFlickDirection) , boundsBehavior(QQuickFlickable::DragAndOvershootBounds) , boundsMovement(QQuickFlickable::FollowBoundsBehavior) , rebound(nullptr) { + const int wheelDecelerationEnv = qEnvironmentVariableIntValue("QT_QUICK_FLICKABLE_WHEEL_DECELERATION"); + if (wheelDecelerationEnv > 0) + wheelDeceleration = wheelDecelerationEnv; } void QQuickFlickablePrivate::init() @@ -291,20 +262,23 @@ void QQuickFlickablePrivate::init() q->setAcceptedMouseButtons(Qt::LeftButton); q->setAcceptTouchEvents(true); q->setFiltersChildMouseEvents(true); + q->setFlag(QQuickItem::ItemIsViewport); QQuickItemPrivate *viewportPrivate = QQuickItemPrivate::get(contentItem); viewportPrivate->addItemChangeListener(this, QQuickItemPrivate::Geometry); + setSizePolicy(QLayoutPolicy::Preferred, QLayoutPolicy::Preferred); } -/* - Returns the amount to overshoot by given a velocity. - Will be roughly in range 0 - size/4 +/*! + \internal + Returns the distance to overshoot, given \a velocity. + Will be in range 0 - velocity / 3, but limited to a max of QML_FLICK_OVERSHOOT */ -qreal QQuickFlickablePrivate::overShootDistance(qreal size) const +qreal QQuickFlickablePrivate::overShootDistance(qreal velocity) const { if (maxVelocity <= 0) - return 0.0; + return 0; - return qMin(qreal(QML_FLICK_OVERSHOOT), size/3); + return qMin(qreal(QML_FLICK_OVERSHOOT), velocity / 3); } void QQuickFlickablePrivate::AxisData::addVelocitySample(qreal v, qreal maxVelocity) @@ -331,7 +305,7 @@ void QQuickFlickablePrivate::AxisData::updateVelocity() } } -void QQuickFlickablePrivate::itemGeometryChanged(QQuickItem *item, QQuickGeometryChange change, const QRectF &) +void QQuickFlickablePrivate::itemGeometryChanged(QQuickItem *item, QQuickGeometryChange change, const QRectF &oldGeom) { Q_Q(QQuickFlickable); if (item == contentItem) { @@ -340,8 +314,14 @@ void QQuickFlickablePrivate::itemGeometryChanged(QQuickItem *item, QQuickGeometr orient |= Qt::Horizontal; if (change.yChange()) orient |= Qt::Vertical; - if (orient) + if (orient) { q->viewportMoved(orient); + const QPointF deltaMoved = item->position() - oldGeom.topLeft(); + if (hData.contentPositionChangedExternallyDuringDrag) + hData.pressPos += deltaMoved.x(); + if (vData.contentPositionChangedExternallyDuringDrag) + vData.pressPos += deltaMoved.y(); + } if (orient & Qt::Horizontal) emit q->contentXChanged(); if (orient & Qt::Vertical) @@ -349,20 +329,21 @@ void QQuickFlickablePrivate::itemGeometryChanged(QQuickItem *item, QQuickGeometr } } -bool QQuickFlickablePrivate::flickX(qreal velocity) +bool QQuickFlickablePrivate::flickX(QEvent::Type eventType, qreal velocity) { Q_Q(QQuickFlickable); - return flick(hData, q->minXExtent(), q->maxXExtent(), q->width(), fixupX_callback, velocity); + return flick(hData, q->minXExtent(), q->maxXExtent(), q->width(), fixupX_callback, eventType, velocity); } -bool QQuickFlickablePrivate::flickY(qreal velocity) +bool QQuickFlickablePrivate::flickY(QEvent::Type eventType, qreal velocity) { Q_Q(QQuickFlickable); - return flick(vData, q->minYExtent(), q->maxYExtent(), q->height(), fixupY_callback, velocity); + return flick(vData, q->minYExtent(), q->maxYExtent(), q->height(), fixupY_callback, eventType, velocity); } bool QQuickFlickablePrivate::flick(AxisData &data, qreal minExtent, qreal maxExtent, qreal, - QQuickTimeLineCallback::Callback fixupCallback, qreal velocity) + QQuickTimeLineCallback::Callback fixupCallback, + QEvent::Type eventType, qreal velocity) { Q_Q(QQuickFlickable); qreal maxDistance = -1; @@ -384,25 +365,23 @@ bool QQuickFlickablePrivate::flick(AxisData &data, qreal minExtent, qreal maxExt v = maxVelocity; } + qreal accel = eventType == QEvent::Wheel ? wheelDeceleration : deceleration; + qCDebug(lcFlickable) << "choosing deceleration" << accel << "for" << eventType; // adjust accel so that we hit a full pixel - qreal accel = deceleration; qreal v2 = v * v; qreal dist = v2 / (accel * 2.0); if (v > 0) dist = -dist; - qreal target = -Round(-(data.move.value() - dist)); + qreal target = std::round(data.move.value() - dist); dist = -target + data.move.value(); accel = v2 / (2.0f * qAbs(dist)); resetTimeline(data); if (!data.inOvershoot) { - if (boundsBehavior & QQuickFlickable::OvershootBounds) { - qCDebug(lcVel) << "timeline.accel(" << data.move << v << accel << ')'; + if (boundsBehavior & QQuickFlickable::OvershootBounds) timeline.accel(data.move, v, accel); - } else { - qCDebug(lcVel) << "timeline.accel(" << data.move << v << accel << "maxDist" << maxDistance << ')'; + else timeline.accel(data.move, v, accel, maxDistance); - } } timeline.callback(QQuickTimeLineCallback(&data.move, fixupCallback, this)); @@ -444,6 +423,14 @@ void QQuickFlickablePrivate::fixupY() fixup(vData, q->minYExtent(), q->maxYExtent()); } +/*! + \internal + + Adjusts the contentItem's position via the timeline. + This function is used by QQuickFlickablePrivate::fixup in order to + position the contentItem back into the viewport, in case flicking, + dragging or geometry adjustments moved it outside of bounds. +*/ void QQuickFlickablePrivate::adjustContentPos(AxisData &data, qreal toPos) { Q_Q(QQuickFlickable); @@ -487,6 +474,16 @@ void QQuickFlickablePrivate::clearTimeline() vData.transitionToBounds->stopTransition(); } +/*! + \internal + + This function should be called after the contentItem has been moved, either programmatically, + or by the timeline (as a result of a flick). + It ensures that the contentItem will be moved back into bounds, + in case it was flicked outside of the visible area. + + The positional adjustment will usually be animated by the timeline, unless the fixupMode is set to Immediate. +*/ void QQuickFlickablePrivate::fixup(AxisData &data, qreal minExtent, qreal maxExtent) { if (data.move.value() >= minExtent || maxExtent > minExtent) { @@ -497,18 +494,18 @@ void QQuickFlickablePrivate::fixup(AxisData &data, qreal minExtent, qreal maxExt } else if (data.move.value() <= maxExtent) { resetTimeline(data); adjustContentPos(data, maxExtent); - } else if (-Round(-data.move.value()) != data.move.value()) { + } else if (-std::round(-data.move.value()) != data.move.value()) { // We could animate, but since it is less than 0.5 pixel it's probably not worthwhile. resetTimeline(data); qreal val = data.move.value(); - if (std::abs(-Round(-val) - val) < 0.25) // round small differences - val = -Round(-val); + if (std::abs(std::round(val) - val) < 0.25) // round small differences + val = std::round(val); else if (data.smoothVelocity.value() > 0) // continue direction of motion for larger - val = -std::floor(-val); + val = std::ceil(val); else if (data.smoothVelocity.value() < 0) - val = -std::ceil(-val); + val = std::floor(val); else // otherwise round - val = -Round(-val); + val = std::round(val); timeline.set(data.move, val); } data.inOvershoot = false; @@ -526,6 +523,15 @@ static bool fuzzyLessThanOrEqualTo(qreal a, qreal b) return a <= b || qFuzzyCompare(a, b); } +/*! + \internal + + This function's main purpose is to update the atBeginning and atEnd flags + in hData and vData. It should be called when the contentItem has moved, + to ensure that hData and vData are up to date. + + The origin will also be updated, if AxisData::markExtentsDirty has been called +*/ void QQuickFlickablePrivate::updateBeginningEnd() { Q_Q(QQuickFlickable); @@ -535,33 +541,41 @@ void QQuickFlickablePrivate::updateBeginningEnd() // Vertical const qreal maxyextent = -q->maxYExtent(); const qreal minyextent = -q->minYExtent(); - const qreal ypos = -vData.move.value(); - bool atBeginning = fuzzyLessThanOrEqualTo(ypos, minyextent); - bool atEnd = fuzzyLessThanOrEqualTo(maxyextent, ypos); + const qreal ypos = pixelAligned ? -std::round(vData.move.value()) : -vData.move.value(); + bool atBeginning = fuzzyLessThanOrEqualTo(ypos, std::ceil(minyextent)); + bool atEnd = fuzzyLessThanOrEqualTo(std::floor(maxyextent), ypos); if (atBeginning != vData.atBeginning) { vData.atBeginning = atBeginning; atYBeginningChange = true; + if (!vData.moving && atBeginning) + vData.smoothVelocity.setValue(0); } if (atEnd != vData.atEnd) { vData.atEnd = atEnd; atYEndChange = true; + if (!vData.moving && atEnd) + vData.smoothVelocity.setValue(0); } // Horizontal const qreal maxxextent = -q->maxXExtent(); const qreal minxextent = -q->minXExtent(); - const qreal xpos = -hData.move.value(); - atBeginning = fuzzyLessThanOrEqualTo(xpos, minxextent); - atEnd = fuzzyLessThanOrEqualTo(maxxextent, xpos); + const qreal xpos = pixelAligned ? -std::round(hData.move.value()) : -hData.move.value(); + atBeginning = fuzzyLessThanOrEqualTo(xpos, std::ceil(minxextent)); + atEnd = fuzzyLessThanOrEqualTo(std::floor(maxxextent), xpos); if (atBeginning != hData.atBeginning) { hData.atBeginning = atBeginning; atXBeginningChange = true; + if (!hData.moving && atBeginning) + hData.smoothVelocity.setValue(0); } if (atEnd != hData.atEnd) { hData.atEnd = atEnd; atXEndChange = true; + if (!hData.moving && atEnd) + hData.smoothVelocity.setValue(0); } if (vData.extentsChanged) { @@ -719,7 +733,8 @@ void QQuickFlickablePrivate::updateBeginningEnd() /*! \qmlsignal QtQuick::Flickable::flickEnded() - This signal is emitted when the view stops moving due to a flick. + This signal is emitted when the view stops moving after a flick + or a series of flicks. */ /*! @@ -741,8 +756,6 @@ void QQuickFlickablePrivate::updateBeginningEnd() \snippet qml/flickableScrollbar.qml 0 \dots 8 \snippet qml/flickableScrollbar.qml 1 - - \sa {customitems/scrollbar}{UI Components: Scrollbar Example} */ QQuickFlickable::QQuickFlickable(QQuickItem *parent) : QQuickItem(*(new QQuickFlickablePrivate), parent) @@ -796,8 +809,11 @@ void QQuickFlickable::setContentX(qreal pos) d->hData.vTime = d->timeline.time(); if (isMoving() || isFlicking()) movementEnding(true, false); - if (!qFuzzyCompare(-pos, d->hData.move.value())) + if (!qFuzzyCompare(-pos, d->hData.move.value())) { + d->hData.contentPositionChangedExternallyDuringDrag = d->hData.dragging; d->hData.move.setValue(-pos); + d->hData.contentPositionChangedExternallyDuringDrag = false; + } } qreal QQuickFlickable::contentY() const @@ -814,8 +830,11 @@ void QQuickFlickable::setContentY(qreal pos) d->vData.vTime = d->timeline.time(); if (isMoving() || isFlicking()) movementEnding(false, true); - if (!qFuzzyCompare(-pos, d->vData.move.value())) + if (!qFuzzyCompare(-pos, d->vData.move.value())) { + d->vData.contentPositionChangedExternallyDuringDrag = d->vData.dragging; d->vData.move.setValue(-pos); + d->vData.contentPositionChangedExternallyDuringDrag = false; + } } /*! @@ -1029,6 +1048,17 @@ void QQuickFlickable::setSynchronousDrag(bool v) } } +/*! \internal + Take the velocity of the first point from the given \a event and transform + it to the local coordinate system (taking scale and rotation into account). +*/ +QVector2D QQuickFlickablePrivate::firstPointLocalVelocity(QPointerEvent *event) +{ + QTransform transform = windowToItemTransform(); + // rotate and scale the velocity vector from scene to local + return QVector2D(transform.map(event->point(0).velocity().toPointF()) - transform.map(QPointF())); +} + qint64 QQuickFlickablePrivate::computeCurrentTime(QInputEvent *event) const { if (0 != event->timestamp()) @@ -1071,14 +1101,17 @@ void QQuickFlickablePrivate::handlePressEvent(QPointerEvent *event) } q->setKeepMouseGrab(stealMouse); - maybeBeginDrag(computeCurrentTime(event), event->points().first().position()); + maybeBeginDrag(computeCurrentTime(event), event->points().first().position(), + event->isSinglePointEvent() ? static_cast<QSinglePointEvent *>(event)->buttons() + : Qt::NoButton); } -void QQuickFlickablePrivate::maybeBeginDrag(qint64 currentTimestamp, const QPointF &pressPosn) +void QQuickFlickablePrivate::maybeBeginDrag(qint64 currentTimestamp, const QPointF &pressPosn, Qt::MouseButtons buttons) { Q_Q(QQuickFlickable); clearDelayedPress(); - pressed = true; + // consider dragging only when event is left mouse button or touch event which has no button + pressed = buttons.testFlag(Qt::LeftButton) || (buttons == Qt::NoButton); if (hData.transitionToBounds) hData.transitionToBounds->stopTransition(); @@ -1100,7 +1133,9 @@ void QQuickFlickablePrivate::maybeBeginDrag(qint64 currentTimestamp, const QPoin pressPos = pressPosn; hData.pressPos = hData.move.value(); vData.pressPos = vData.move.value(); - bool wasFlicking = hData.flicking || vData.flicking; + const bool wasFlicking = hData.flicking || vData.flicking; + hData.flickingWhenDragBegan = hData.flicking; + vData.flickingWhenDragBegan = vData.flicking; if (hData.flicking) { hData.flicking = false; emit q->flickingHorizontallyChanged(); @@ -1167,9 +1202,9 @@ void QQuickFlickablePrivate::drag(qint64 currentTimestamp, QEvent::Type eventTyp } else { qreal vel = velocity.y() / QML_FLICK_OVERSHOOTFRICTION; if (vel > 0. && vel > vData.velocity) - vData.velocity = qMin(velocity.y() / QML_FLICK_OVERSHOOTFRICTION, float(QML_FLICK_DEFAULTMAXVELOCITY)); + vData.velocity = qMin(velocity.y() / QML_FLICK_OVERSHOOTFRICTION, maxVelocity); else if (vel < 0. && vel < vData.velocity) - vData.velocity = qMax(velocity.y() / QML_FLICK_OVERSHOOTFRICTION, -float(QML_FLICK_DEFAULTMAXVELOCITY)); + vData.velocity = qMax(velocity.y() / QML_FLICK_OVERSHOOTFRICTION, -maxVelocity); if (newY > minY) { // Overshoot beyond the top. But don't wait for momentum phase to end before returning to bounds. if (momentum && vData.atBeginning) { @@ -1180,7 +1215,7 @@ void QQuickFlickablePrivate::drag(qint64 currentTimestamp, QEvent::Type eventTyp return; } if (velocitySensitiveOverBounds) { - qreal overshoot = (newY - minY) * vData.velocity / QML_FLICK_DEFAULTMAXVELOCITY / QML_FLICK_OVERSHOOTFRICTION; + qreal overshoot = (newY - minY) * vData.velocity / maxVelocity / QML_FLICK_OVERSHOOTFRICTION; overshoot = QML_FLICK_OVERSHOOT * devicePixelRatio() * EaseOvershoot(overshoot / QML_FLICK_OVERSHOOT / devicePixelRatio()); newY = minY + overshoot; } else { @@ -1196,7 +1231,7 @@ void QQuickFlickablePrivate::drag(qint64 currentTimestamp, QEvent::Type eventTyp return; } if (velocitySensitiveOverBounds) { - qreal overshoot = (newY - maxY) * vData.velocity / QML_FLICK_DEFAULTMAXVELOCITY / QML_FLICK_OVERSHOOTFRICTION; + qreal overshoot = (newY - maxY) * vData.velocity / maxVelocity / QML_FLICK_OVERSHOOTFRICTION; overshoot = QML_FLICK_OVERSHOOT * devicePixelRatio() * EaseOvershoot(overshoot / QML_FLICK_OVERSHOOT / devicePixelRatio()); newY = maxY - overshoot; } else { @@ -1240,9 +1275,9 @@ void QQuickFlickablePrivate::drag(qint64 currentTimestamp, QEvent::Type eventTyp } else { qreal vel = velocity.x() / QML_FLICK_OVERSHOOTFRICTION; if (vel > 0. && vel > hData.velocity) - hData.velocity = qMin(velocity.x() / QML_FLICK_OVERSHOOTFRICTION, float(QML_FLICK_DEFAULTMAXVELOCITY)); + hData.velocity = qMin(velocity.x() / QML_FLICK_OVERSHOOTFRICTION, maxVelocity); else if (vel < 0. && vel < hData.velocity) - hData.velocity = qMax(velocity.x() / QML_FLICK_OVERSHOOTFRICTION, -float(QML_FLICK_DEFAULTMAXVELOCITY)); + hData.velocity = qMax(velocity.x() / QML_FLICK_OVERSHOOTFRICTION, -maxVelocity); if (newX > minX) { // Overshoot beyond the left. But don't wait for momentum phase to end before returning to bounds. if (momentum && hData.atBeginning) { @@ -1253,7 +1288,7 @@ void QQuickFlickablePrivate::drag(qint64 currentTimestamp, QEvent::Type eventTyp return; } if (velocitySensitiveOverBounds) { - qreal overshoot = (newX - minX) * hData.velocity / QML_FLICK_DEFAULTMAXVELOCITY / QML_FLICK_OVERSHOOTFRICTION; + qreal overshoot = (newX - minX) * hData.velocity / maxVelocity / QML_FLICK_OVERSHOOTFRICTION; overshoot = QML_FLICK_OVERSHOOT * devicePixelRatio() * EaseOvershoot(overshoot / QML_FLICK_OVERSHOOT / devicePixelRatio()); newX = minX + overshoot; } else { @@ -1269,7 +1304,7 @@ void QQuickFlickablePrivate::drag(qint64 currentTimestamp, QEvent::Type eventTyp return; } if (velocitySensitiveOverBounds) { - qreal overshoot = (newX - maxX) * hData.velocity / QML_FLICK_DEFAULTMAXVELOCITY / QML_FLICK_OVERSHOOTFRICTION; + qreal overshoot = (newX - maxX) * hData.velocity / maxVelocity / QML_FLICK_OVERSHOOTFRICTION; overshoot = QML_FLICK_OVERSHOOT * devicePixelRatio() * EaseOvershoot(overshoot / QML_FLICK_OVERSHOOT / devicePixelRatio()); newX = maxX - overshoot; } else { @@ -1330,20 +1365,24 @@ void QQuickFlickablePrivate::handleMoveEvent(QPointerEvent *event) { Q_Q(QQuickFlickable); if (!interactive || lastPosTime == -1 || - (event->isSinglePointEvent() && static_cast<QSinglePointEvent *>(event)->buttons() == Qt::NoButton)) + (event->isSinglePointEvent() && !static_cast<QSinglePointEvent *>(event)->buttons().testFlag(Qt::LeftButton))) return; qint64 currentTimestamp = computeCurrentTime(event); const auto &firstPoint = event->points().first(); const auto &pos = firstPoint.position(); - QVector2D deltas = QVector2D(pos - q->mapFromGlobal(firstPoint.globalPressPosition())); + const QVector2D deltas = QVector2D(pos - q->mapFromGlobal(firstPoint.globalPressPosition())); + const QVector2D velocity = firstPointLocalVelocity(event); bool overThreshold = false; - QVector2D velocity = event->point(0).velocity(); - if (q->yflick()) - overThreshold |= QQuickDeliveryAgentPrivate::dragOverThreshold(deltas.y(), Qt::YAxis, firstPoint); - if (q->xflick()) - overThreshold |= QQuickDeliveryAgentPrivate::dragOverThreshold(deltas.x(), Qt::XAxis, firstPoint); + if (event->pointCount() == 1) { + if (q->yflick()) + overThreshold |= QQuickDeliveryAgentPrivate::dragOverThreshold(deltas.y(), Qt::YAxis, firstPoint); + if (q->xflick()) + overThreshold |= QQuickDeliveryAgentPrivate::dragOverThreshold(deltas.x(), Qt::XAxis, firstPoint); + } else { + qCDebug(lcFilter) << q->objectName() << "ignoring multi-touch" << event; + } drag(currentTimestamp, event->type(), pos, deltas, overThreshold, false, false, velocity); } @@ -1370,12 +1409,14 @@ void QQuickFlickablePrivate::handleReleaseEvent(QPointerEvent *event) bool canBoost = false; const auto pos = event->points().first().position(); - const auto pressPos = event->points().first().pressPosition(); + const auto pressPos = q->mapFromGlobal(event->points().first().globalPressPosition()); + const QVector2D eventVelocity = firstPointLocalVelocity(event); + qCDebug(lcVel) << event->deviceType() << event->type() << "velocity" << event->points().first().velocity() << "transformed to local" << eventVelocity; qreal vVelocity = 0; if (elapsed < 100 && vData.velocity != 0.) { vVelocity = (event->device()->capabilities().testFlag(QInputDevice::Capability::Velocity) - ? event->point(0).velocity().y() : vData.velocity); + ? eventVelocity.y() : vData.velocity); } if ((vData.atBeginning && vVelocity > 0.) || (vData.atEnd && vVelocity < 0.)) { vVelocity /= 2; @@ -1390,7 +1431,7 @@ void QQuickFlickablePrivate::handleReleaseEvent(QPointerEvent *event) qreal hVelocity = 0; if (elapsed < 100 && hData.velocity != 0.) { hVelocity = (event->device()->capabilities().testFlag(QInputDevice::Capability::Velocity) - ? event->point(0).velocity().x() : hData.velocity); + ? eventVelocity.x() : hData.velocity); } if ((hData.atBeginning && hVelocity > 0.) || (hData.atEnd && hVelocity < 0.)) { hVelocity /= 2; @@ -1403,23 +1444,32 @@ void QQuickFlickablePrivate::handleReleaseEvent(QPointerEvent *event) } flickBoost = canBoost ? qBound(1.0, flickBoost+0.25, QML_FLICK_MULTIFLICK_MAXBOOST) : 1.0; + const int flickThreshold = QGuiApplicationPrivate::platformTheme()->themeHint(QPlatformTheme::FlickStartDistance).toInt(); + + bool anyPointGrabbed = event->points().constEnd() != + std::find_if(event->points().constBegin(),event->points().constEnd(), + [q, event](const QEventPoint &point) { return event->exclusiveGrabber(point) == q; }); bool flickedVertically = false; vVelocity *= flickBoost; - bool isVerticalFlickAllowed = q->yflick() && qAbs(vVelocity) > MinimumFlickVelocity && qAbs(pos.y() - pressPos.y()) > FlickThreshold; + const bool isVerticalFlickAllowed = anyPointGrabbed && + q->yflick() && qAbs(vVelocity) > _q_MinimumFlickVelocity && + qAbs(pos.y() - pressPos.y()) > flickThreshold; if (isVerticalFlickAllowed) { velocityTimeline.reset(vData.smoothVelocity); vData.smoothVelocity.setValue(-vVelocity); - flickedVertically = flickY(vVelocity); + flickedVertically = flickY(event->type(), vVelocity); } bool flickedHorizontally = false; hVelocity *= flickBoost; - bool isHorizontalFlickAllowed = q->xflick() && qAbs(hVelocity) > MinimumFlickVelocity && qAbs(pos.x() - pressPos.x()) > FlickThreshold; + const bool isHorizontalFlickAllowed = anyPointGrabbed && + q->xflick() && qAbs(hVelocity) > _q_MinimumFlickVelocity && + qAbs(pos.x() - pressPos.x()) > flickThreshold; if (isHorizontalFlickAllowed) { velocityTimeline.reset(hData.smoothVelocity); hData.smoothVelocity.setValue(-hVelocity); - flickedHorizontally = flickX(hVelocity); + flickedHorizontally = flickX(event->type(), hVelocity); } if (!isVerticalFlickAllowed) @@ -1429,8 +1479,15 @@ void QQuickFlickablePrivate::handleReleaseEvent(QPointerEvent *event) fixupX(); flickingStarted(flickedHorizontally, flickedVertically); - if (!isViewMoving()) + if (!isViewMoving()) { q->movementEnding(); + } else { + if (flickedVertically) + vMoved = true; + if (flickedHorizontally) + hMoved = true; + q->movementStarting(); + } } void QQuickFlickable::mousePressEvent(QMouseEvent *event) @@ -1463,14 +1520,14 @@ void QQuickFlickable::mouseReleaseEvent(QMouseEvent *event) if (d->delayedPressEvent) { d->replayDelayedPress(); - // Now send the release - if (auto grabber = qmlobject_cast<QQuickItem *>(event->exclusiveGrabber(event->point(0)))) { - // not copying or detaching anything, so make sure we return the original event unchanged - QMutableSinglePointEvent *localized = QMutableSinglePointEvent::from(event); - const auto oldPosition = localized->mutablePoint().position(); - localized->mutablePoint().setPosition(grabber->mapFromScene(localized->scenePosition())); - QCoreApplication::sendEvent(window(), localized); - localized->mutablePoint().setPosition(oldPosition); + auto &firstPoint = event->point(0); + if (const auto *grabber = event->exclusiveGrabber(firstPoint); grabber && grabber->isQuickItemType()) { + // Since we sent the delayed press to the window, we need to resend the release to the window too. + // We're not copying or detaching, so restore the original event position afterwards. + const auto oldPosition = firstPoint.position(); + QMutableEventPoint::setPosition(firstPoint, event->scenePosition()); + QCoreApplication::sendEvent(window(), event); + QMutableEventPoint::setPosition(firstPoint, oldPosition); } // And the event has been consumed @@ -1489,6 +1546,15 @@ void QQuickFlickable::mouseReleaseEvent(QMouseEvent *event) void QQuickFlickable::touchEvent(QTouchEvent *event) { Q_D(QQuickFlickable); + + if (event->type() == QEvent::TouchCancel) { + if (d->interactive && d->wantsPointerEvent(event)) + d->cancelInteraction(); + else + QQuickItem::touchEvent(event); + return; + } + bool unhandled = false; const auto &firstPoint = event->points().first(); switch (firstPoint.state()) { @@ -1514,11 +1580,11 @@ void QQuickFlickable::touchEvent(QTouchEvent *event) if (d->delayedPressEvent) { d->replayDelayedPress(); - // Now send the release - auto &firstPoint = event->point(0); - if (auto grabber = qmlobject_cast<QQuickItem *>(event->exclusiveGrabber(firstPoint))) { - const auto localPos = grabber->mapFromScene(firstPoint.scenePosition()); - QScopedPointer<QPointerEvent> localizedEvent(QQuickDeliveryAgentPrivate::clonePointerEvent(event, localPos)); + const auto &firstPoint = event->point(0); + if (const auto *grabber = event->exclusiveGrabber(firstPoint); grabber && grabber->isQuickItemType()) { + // Since we sent the delayed press to the window, we need to resend the release to the window too. + QScopedPointer<QPointerEvent> localizedEvent( + QQuickDeliveryAgentPrivate::clonePointerEvent(event, firstPoint.scenePosition())); QCoreApplication::sendEvent(window(), localizedEvent.data()); } @@ -1542,21 +1608,6 @@ void QQuickFlickable::touchEvent(QTouchEvent *event) QQuickItem::touchEvent(event); } -enum class WheelMomentumSetting { - Default = -1, - Disabled = 0, - Enabled -}; - -static WheelMomentumSetting wheelMomentumEnabled() -{ - bool envIsSet = true; - const bool wheelMomentumEnabled = qEnvironmentVariableIntValue("QT_QUICK_FLICKABLE_WHEEL_MOMENTUM_ENABLED", &envIsSet); - if (!envIsSet) - return WheelMomentumSetting::Default; - return wheelMomentumEnabled ? WheelMomentumSetting::Enabled : WheelMomentumSetting::Disabled; -} - #if QT_CONFIG(wheelevent) void QQuickFlickable::wheelEvent(QWheelEvent *event) { @@ -1576,6 +1627,7 @@ void QQuickFlickable::wheelEvent(QWheelEvent *event) d->hData.velocity = 0; d->timer.start(); d->maybeBeginDrag(currentTimestamp, event->position()); + d->lastPosTime = -1; break; case Qt::NoScrollPhase: // default phase with an ordinary wheel mouse case Qt::ScrollUpdate: @@ -1603,55 +1655,105 @@ void QQuickFlickable::wheelEvent(QWheelEvent *event) return; } - if (event->source() == Qt::MouseEventNotSynthesized || event->pixelDelta().isNull()) { - // physical mouse wheel, so use angleDelta + qreal elapsed = qreal(currentTimestamp - d->lastPosTime) / qreal(1000); + if (elapsed <= 0) { + d->lastPosTime = currentTimestamp; + qCDebug(lcWheel) << "insufficient elapsed time: can't calculate velocity" << elapsed; + return; + } + + if (event->source() == Qt::MouseEventNotSynthesized || event->pixelDelta().isNull() || event->phase() == Qt::NoScrollPhase) { + // no pixel delta (physical mouse wheel, or "dumb" touchpad), so use angleDelta int xDelta = event->angleDelta().x(); int yDelta = event->angleDelta().y(); - /*! \internal - Will we get smooth scrolling with momentum, or not? - - |env var|enabled |delta|momentum| - |-------|--------|-----|--------| - |not set|Default |120x |y | - |not set|Default |other|n | - |0 |Disabled|120x |n | - |0 |Disabled|other|n | - |1 |Enabled |120x |y | - |1 |Enabled |other|y | - */ - static const WheelMomentumSetting momentumEnvEnabled = wheelMomentumEnabled(); - bool momentumEnabled = (momentumEnvEnabled != WheelMomentumSetting::Disabled); - if (momentumEnvEnabled == WheelMomentumSetting::Default) { - momentumEnabled = !d->nonClickyWheelMice.contains(event->pointingDevice()); - if (momentumEnabled) { - // If a particular mouse ever generates deltas that are not a multiple of 120, - // momentum scrolling is disabled. On a laptop, the "core pointer" might sometimes - // be the touchpad and sometimes a USB mouse (until Qt's multi-mouse support is - // better developed to keep them separate); but if you miss momentum, you can set - // QT_QUICK_FLICKABLE_WHEEL_MOMENTUM_ENABLED=1 to opt back in again. - if (xDelta % 120 || yDelta % 120) { - d->nonClickyWheelMice.append(event->pointingDevice()); - qCDebug(lcWheel) << event->pointingDevice() << "doesn't have a 'clicky' mouse wheel"; - momentumEnabled = false; + + if (d->wheelDeceleration > _q_MaximumWheelDeceleration) { + const qreal wheelScroll = -qApp->styleHints()->wheelScrollLines() * 24; + // If wheelDeceleration is very large, i.e. the user or the platform does not want to have any mouse wheel + // acceleration behavior, we want to move a distance proportional to QStyleHints::wheelScrollLines() + if (yflick() && yDelta != 0) { + d->moveReason = QQuickFlickablePrivate::Mouse; // ItemViews will set fixupMode to Immediate in fixup() without this. + d->vMoved = true; + qreal scrollPixel = (-yDelta / 120.0 * wheelScroll); + if (scrollPixel > 0) { // Forward direction (away from user) + if (d->vData.move.value() >= minYExtent()) + d->vMoved = false; + } else { // Backward direction (towards user) + if (d->vData.move.value() <= maxYExtent()) + d->vMoved = false; } + if (d->vMoved) { + if (d->boundsBehavior == QQuickFlickable::StopAtBounds) { + const qreal estContentPos = scrollPixel + d->vData.move.value(); + if (scrollPixel > 0) { // Forward direction (away from user) + if (estContentPos > minYExtent()) + scrollPixel = minYExtent() - d->vData.move.value(); + } else { // Backward direction (towards user) + if (estContentPos < maxYExtent()) + scrollPixel = maxYExtent() - d->vData.move.value(); + } + } + d->resetTimeline(d->vData); + movementStarting(); + d->timeline.moveBy(d->vData.move, scrollPixel, QEasingCurve(QEasingCurve::OutExpo), 3*d->fixupDuration/4); + d->vData.fixingUp = true; + d->timeline.callback(QQuickTimeLineCallback(&d->vData.move, QQuickFlickablePrivate::fixupY_callback, d)); + } + event->accept(); } - } - qCDebug(lcWheel) << "momentum env:" << int(momentumEnvEnabled) << "this device:" << momentumEnabled; - if (momentumEnabled) { - // engage the velocity timeline for smooth scrolling - if (yflick() && yDelta != 0) { - bool valid = false; - if (yDelta > 0 && contentY() > -minYExtent()) { - d->vData.velocity = qMax(yDelta*2 - d->vData.smoothVelocity.value(), qreal(d->maxVelocity/4)); - valid = true; - } else if (yDelta < 0 && contentY() < -maxYExtent()) { - d->vData.velocity = qMin(yDelta*2 - d->vData.smoothVelocity.value(), qreal(-d->maxVelocity/4)); - valid = true; + if (xflick() && xDelta != 0) { + d->moveReason = QQuickFlickablePrivate::Mouse; // ItemViews will set fixupMode to Immediate in fixup() without this. + d->hMoved = true; + qreal scrollPixel = (-xDelta / 120.0 * wheelScroll); + if (scrollPixel > 0) { // Forward direction (away from user) + if (d->hData.move.value() >= minXExtent()) + d->hMoved = false; + } else { // Backward direction (towards user) + if (d->hData.move.value() <= maxXExtent()) + d->hMoved = false; } - if (valid) { - d->flickY(d->vData.velocity); - d->flickingStarted(false, true); - if (d->vData.flicking) { + if (d->hMoved) { + if (d->boundsBehavior == QQuickFlickable::StopAtBounds) { + const qreal estContentPos = scrollPixel + d->hData.move.value(); + if (scrollPixel > 0) { // Forward direction (away from user) + if (estContentPos > minXExtent()) + scrollPixel = minXExtent() - d->hData.move.value(); + } else { // Backward direction (towards user) + if (estContentPos < maxXExtent()) + scrollPixel = maxXExtent() - d->hData.move.value(); + } + } + d->resetTimeline(d->hData); + movementStarting(); + d->timeline.moveBy(d->hData.move, scrollPixel, QEasingCurve(QEasingCurve::OutExpo), 3*d->fixupDuration/4); + d->hData.fixingUp = true; + d->timeline.callback(QQuickTimeLineCallback(&d->hData.move, QQuickFlickablePrivate::fixupX_callback, d)); + } + event->accept(); + } + } else { + // wheelDeceleration is set to some reasonable value: the user or the platform wants to have + // the classic Qt Quick mouse wheel acceleration behavior. + // For a single "clicky" wheel event (angleDelta +/- 120), + // we want flick() to end up moving a distance proportional to QStyleHints::wheelScrollLines(). + // The decel algo from there is + // qreal dist = v2 / (accel * 2.0); + // i.e. initialWheelFlickDistance = (120 / dt)^2 / (deceleration * 2) + // now solve for dt: + // dt = 120 / sqrt(deceleration * 2 * initialWheelFlickDistance) + if (!isMoving()) + elapsed = 120 / qSqrt(d->wheelDeceleration * 2 * d->initialWheelFlickDistance); + if (yflick() && yDelta != 0) { + qreal instVelocity = yDelta / elapsed; + // if the direction has changed, start over with filtering, to allow instant movement in the opposite direction + if ((instVelocity < 0 && d->vData.velocity > 0) || (instVelocity > 0 && d->vData.velocity < 0)) + d->vData.velocityBuffer.clear(); + d->vData.addVelocitySample(instVelocity, d->maxVelocity); + d->vData.updateVelocity(); + if ((yDelta > 0 && contentY() > -minYExtent()) || (yDelta < 0 && contentY() < -maxYExtent())) { + const bool newFlick = d->flickY(event->type(), d->vData.velocity); + if (newFlick && (d->vData.atBeginning != (yDelta > 0) || d->vData.atEnd != (yDelta < 0))) { + d->flickingStarted(false, true); d->vMoved = true; movementStarting(); } @@ -1659,60 +1761,29 @@ void QQuickFlickable::wheelEvent(QWheelEvent *event) } } if (xflick() && xDelta != 0) { - bool valid = false; - if (xDelta > 0 && contentX() > -minXExtent()) { - d->hData.velocity = qMax(xDelta*2 - d->hData.smoothVelocity.value(), qreal(d->maxVelocity/4)); - valid = true; - } else if (xDelta < 0 && contentX() < -maxXExtent()) { - d->hData.velocity = qMin(xDelta*2 - d->hData.smoothVelocity.value(), qreal(-d->maxVelocity/4)); - valid = true; - } - if (valid) { - d->flickX(d->hData.velocity); - d->flickingStarted(true, false); - if (d->hData.flicking) { + qreal instVelocity = xDelta / elapsed; + // if the direction has changed, start over with filtering, to allow instant movement in the opposite direction + if ((instVelocity < 0 && d->hData.velocity > 0) || (instVelocity > 0 && d->hData.velocity < 0)) + d->hData.velocityBuffer.clear(); + d->hData.addVelocitySample(instVelocity, d->maxVelocity); + d->hData.updateVelocity(); + if ((xDelta > 0 && contentX() > -minXExtent()) || (xDelta < 0 && contentX() < -maxXExtent())) { + const bool newFlick = d->flickX(event->type(), d->hData.velocity); + if (newFlick && (d->hData.atBeginning != (xDelta > 0) || d->hData.atEnd != (xDelta < 0))) { + d->flickingStarted(true, false); d->hMoved = true; movementStarting(); } event->accept(); } } - } else { - // if it's a laptop touchpad, ignore velocity and make scroll distance directly proportional to angleDelta - if (yDelta > 0) - yDelta = qMin(yDelta, int(contentY() + minYExtent())); - else if (yDelta < 0) - yDelta = qMax(yDelta, int(contentY() + maxYExtent())); - else if (xDelta > 0) - xDelta = qMin(xDelta, int(contentX() + minXExtent())); - else if (xDelta < 0) - xDelta = qMax(xDelta, int(contentX() + maxXExtent())); - - if (xDelta != 0 || yDelta != 0) { - // if QStyleHints::wheelScrollLines() != 3, scale it accordingly - // (Flickable has no idea how many "lines" are being scrolled: depends on font size etc.) - static const float defaultWheelScrollLines = QPlatformTheme::defaultThemeHint(QPlatformTheme::WheelScrollLines).toFloat(); - const float configuredWheelScrollLines = qGuiApp->styleHints()->wheelScrollLines(); - const float deltaRatio = configuredWheelScrollLines / defaultWheelScrollLines; - d->accumulatedWheelPixelDelta += QVector2D(xDelta * deltaRatio, yDelta * deltaRatio); - qCDebug(lcWheel) << "scroll lines: default" << defaultWheelScrollLines - << "style hint" << configuredWheelScrollLines << "scaled acc delta" << d->accumulatedWheelPixelDelta; - d->drag(currentTimestamp, event->type(), event->position(), d->accumulatedWheelPixelDelta, true, false, false, {}); - event->accept(); - } } } else { // use pixelDelta (probably from a trackpad): this is where we want to be on most platforms eventually int xDelta = event->pixelDelta().x(); int yDelta = event->pixelDelta().y(); - qreal elapsed = qreal(currentTimestamp - d->lastPosTime) / 1000.; - if (elapsed <= 0) { - d->lastPosTime = currentTimestamp; - return; - } QVector2D velocity(xDelta / elapsed, yDelta / elapsed); - d->lastPosTime = currentTimestamp; d->accumulatedWheelPixelDelta += QVector2D(event->pixelDelta()); // Try to drag if 1) we already are dragging or flicking, or // 2) the flickable is free to flick both directions, or @@ -1730,6 +1801,7 @@ void QQuickFlickable::wheelEvent(QWheelEvent *event) "moving?" << isMoving() << "can flick horizontally?" << xflick() << "vertically?" << yflick(); } } + d->lastPosTime = currentTimestamp; if (!event->isAccepted()) QQuickItem::wheelEvent(event); @@ -1804,7 +1876,7 @@ void QQuickFlickablePrivate::replayDelayedPress() qCDebug(lcReplay) << "replaying" << event.data(); // Put scenePosition into position, for the sake of QQuickWindowPrivate::translateTouchEvent() // TODO remove this if we remove QQuickWindowPrivate::translateTouchEvent() - QMutableEventPoint::from(firstPoint).setPosition(firstPoint.scenePosition()); + QMutableEventPoint::setPosition(firstPoint, firstPoint.scenePosition()); // Send it through like a fresh press event, and let QQuickWindow // (more specifically, QQuickWindowPrivate::deliverPressOrReleaseEvent) // find the item or handler that should receive it, as usual. @@ -1819,10 +1891,19 @@ void QQuickFlickablePrivate::replayDelayedPress() } //XXX pixelAligned ignores the global position of the Flickable, i.e. assumes Flickable itself is pixel aligned. + +/*! + \internal + + This function is called from the timeline, + when advancement in the timeline is modifying the hData.move value. + The \a x argument is the newly updated value in hData.move. + The purpose of the function is to update the x position of the contentItem. +*/ void QQuickFlickablePrivate::setViewportX(qreal x) { Q_Q(QQuickFlickable); - qreal effectiveX = pixelAligned ? -Round(-x) : x; + qreal effectiveX = pixelAligned ? -std::round(-x) : x; const qreal maxX = q->maxXExtent(); const qreal minX = q->minXExtent(); @@ -1846,10 +1927,18 @@ void QQuickFlickablePrivate::setViewportX(qreal x) } } +/*! + \internal + + This function is called from the timeline, + when advancement in the timeline is modifying the vData.move value. + The \a y argument is the newly updated value in vData.move. + The purpose of the function is to update the y position of the contentItem. +*/ void QQuickFlickablePrivate::setViewportY(qreal y) { Q_Q(QQuickFlickable); - qreal effectiveY = pixelAligned ? -Round(-y) : y; + qreal effectiveY = pixelAligned ? -std::round(-y) : y; const qreal maxY = q->maxYExtent(); const qreal minY = q->minYExtent(); @@ -1917,19 +2006,23 @@ void QQuickFlickable::componentComplete() setContentX(-minXExtent()); if (!d->vData.explicitValue && d->vData.startMargin != 0.) setContentY(-minYExtent()); + if (lcWheel().isDebugEnabled() || lcVel().isDebugEnabled()) { + d->timeline.setObjectName(QLatin1String("timeline for Flickable ") + objectName()); + d->velocityTimeline.setObjectName(QLatin1String("velocity timeline for Flickable ") + objectName()); + } } void QQuickFlickable::viewportMoved(Qt::Orientations orient) { Q_D(QQuickFlickable); if (orient & Qt::Vertical) - d->viewportAxisMoved(d->vData, minYExtent(), maxYExtent(), height(), d->fixupY_callback); + d->viewportAxisMoved(d->vData, minYExtent(), maxYExtent(), d->fixupY_callback); if (orient & Qt::Horizontal) - d->viewportAxisMoved(d->hData, minXExtent(), maxXExtent(), width(), d->fixupX_callback); + d->viewportAxisMoved(d->hData, minXExtent(), maxXExtent(), d->fixupX_callback); d->updateBeginningEnd(); } -void QQuickFlickablePrivate::viewportAxisMoved(AxisData &data, qreal minExtent, qreal maxExtent, qreal vSize, +void QQuickFlickablePrivate::viewportAxisMoved(AxisData &data, qreal minExtent, qreal maxExtent, QQuickTimeLineCallback::Callback fixupCallback) { if (!scrollingPhase && (pressed || calcVelocity)) { @@ -1938,21 +2031,22 @@ void QQuickFlickablePrivate::viewportAxisMoved(AxisData &data, qreal minExtent, qreal velocity = (data.lastPos - data.move.value()) * 1000 / elapsed; if (qAbs(velocity) > 0) { velocityTimeline.reset(data.smoothVelocity); - if (calcVelocity) - velocityTimeline.set(data.smoothVelocity, velocity); - else - velocityTimeline.move(data.smoothVelocity, velocity, reportedVelocitySmoothing); - velocityTimeline.move(data.smoothVelocity, 0, reportedVelocitySmoothing); + velocityTimeline.set(data.smoothVelocity, velocity); qCDebug(lcVel) << "touchpad scroll phase: velocity" << velocity; } } } else { if (timeline.time() > data.vTime) { velocityTimeline.reset(data.smoothVelocity); - qreal velocity = (data.lastPos - data.move.value()) * 1000 / (timeline.time() - data.vTime); - if (!qFuzzyCompare(data.smoothVelocity.value(), velocity)) - qCDebug(lcVel) << "velocity" << data.smoothVelocity.value() << "->" << velocity; - data.smoothVelocity.setValue(velocity); + int dt = timeline.time() - data.vTime; + if (dt > 2) { + qreal velocity = (data.lastPos - data.move.value()) * 1000 / dt; + if (!qFuzzyCompare(data.smoothVelocity.value(), velocity)) + qCDebug(lcVel) << "velocity" << data.smoothVelocity.value() << "->" << velocity + << "computed as (" << data.lastPos << "-" << data.move.value() << ") * 1000 / (" + << timeline.time() << "-" << data.vTime << ")"; + data.smoothVelocity.setValue(velocity); + } } } @@ -1964,13 +2058,10 @@ void QQuickFlickablePrivate::viewportAxisMoved(AxisData &data, qreal minExtent, ? data.move.value() - minExtent : maxExtent - data.move.value(); data.inOvershoot = true; - qreal maxDistance = overShootDistance(vSize) - overBound; + qreal maxDistance = overShootDistance(qAbs(data.smoothVelocity.value())) - overBound; resetTimeline(data); - if (maxDistance > 0) { - qCDebug(lcVel) << "timeline.accel(" << data.move << -data.smoothVelocity.value() - << deceleration*QML_FLICK_OVERSHOOTFRICTION << "maxDist" << maxDistance << ')'; + if (maxDistance > 0) timeline.accel(data.move, -data.smoothVelocity.value(), deceleration*QML_FLICK_OVERSHOOTFRICTION, maxDistance); - } timeline.callback(QQuickTimeLineCallback(&data.move, fixupCallback, this)); } @@ -2015,7 +2106,7 @@ void QQuickFlickable::geometryChange(const QRectF &newGeometry, const QRectF &ol Flicks the content with \a xVelocity horizontally and \a yVelocity vertically in pixels/sec. Calling this method will update the corresponding moving and flicking properties and signals, - just like a real flick. + just like a real touchscreen flick. */ void QQuickFlickable::flick(qreal xVelocity, qreal yVelocity) @@ -2027,8 +2118,8 @@ void QQuickFlickable::flick(qreal xVelocity, qreal yVelocity) d->vData.velocity = yVelocity; d->hData.vTime = d->vData.vTime = d->timeline.time(); - const bool flickedX = xflick() && !qFuzzyIsNull(xVelocity) && d->flickX(xVelocity); - const bool flickedY = yflick() && !qFuzzyIsNull(yVelocity) && d->flickY(yVelocity); + const bool flickedX = xflick() && !qFuzzyIsNull(xVelocity) && d->flickX(QEvent::TouchUpdate, xVelocity); + const bool flickedY = yflick() && !qFuzzyIsNull(yVelocity) && d->flickY(QEvent::TouchUpdate, yVelocity); if (flickedX) d->hMoved = true; @@ -2496,6 +2587,13 @@ qreal QQuickFlickable::vHeight() const return d->vData.viewSize; } +/*! + \internal + + The setFlickableDirection function can be used to set constraints on which axis the contentItem can be flicked along. + + \return true if the flickable is allowed to flick in the horizontal direction, otherwise returns false +*/ bool QQuickFlickable::xflick() const { Q_D(const QQuickFlickable); @@ -2507,6 +2605,13 @@ bool QQuickFlickable::xflick() const return d->flickableDirection & QQuickFlickable::HorizontalFlick; } +/*! + \internal + + The setFlickableDirection function can be used to set constraints on which axis the contentItem can be flicked along. + + \return true if the flickable is allowed to flick in the vertical direction, otherwise returns false. +*/ bool QQuickFlickable::yflick() const { Q_D(const QQuickFlickable); @@ -2561,21 +2666,59 @@ void QQuickFlickablePrivate::addPointerHandler(QQuickPointerHandler *h) */ bool QQuickFlickable::filterPointerEvent(QQuickItem *receiver, QPointerEvent *event) { - const bool isTouch = QQuickWindowPrivate::isTouchEvent(event); - if (!(QQuickWindowPrivate::isMouseEvent(event) || isTouch || - QQuickWindowPrivate::isTabletEvent(event))) + const bool isTouch = QQuickDeliveryAgentPrivate::isTouchEvent(event); + if (!(QQuickDeliveryAgentPrivate::isMouseEvent(event) || isTouch || + QQuickDeliveryAgentPrivate::isTabletEvent(event))) return false; // don't filter hover events or wheel events, for example Q_ASSERT_X(receiver != this, "", "Flickable received a filter event for itself"); - qCDebug(lcFilter) << objectName() << "filtering" << event << "for" << receiver; Q_D(QQuickFlickable); // If a touch event contains a new press point, don't steal right away: watch the movements for a while if (isTouch && static_cast<QTouchEvent *>(event)->touchPointStates().testFlag(QEventPoint::State::Pressed)) d->stealMouse = false; + // If multiple touchpoints are within bounds, don't grab: it's probably meant for multi-touch interaction in some child + if (event->pointCount() > 1) { + qCDebug(lcFilter) << objectName() << "ignoring multi-touch" << event << "for" << receiver; + d->stealMouse = false; + } else { + qCDebug(lcFilter) << objectName() << "filtering" << event << "for" << receiver; + } + const auto &firstPoint = event->points().first(); + + if (event->pointCount() == 1 && event->exclusiveGrabber(firstPoint) == this) { + // We have an exclusive grab (since we're e.g dragging), but at the same time, we have + // a child with a passive grab (which is why this filter is being called). And because + // of that, we end up getting the same pointer events twice; First in our own event + // handlers (because of the grab), then once more in here, since we filter the child. + // To avoid processing the event twice (e.g avoid calling handleReleaseEvent once more + // from below), we mark the event as filtered, and simply return. + event->setAccepted(true); + return true; + } + QPointF localPos = mapFromScene(firstPoint.scenePosition()); bool receiverDisabled = receiver && !receiver->isEnabled(); bool stealThisEvent = d->stealMouse; bool receiverKeepsGrab = receiver && (receiver->keepMouseGrab() || receiver->keepTouchGrab()); + bool receiverRelinquishGrab = false; + + // Special case for MouseArea, try to guess what it does with the event + if (auto *mouseArea = qmlobject_cast<QQuickMouseArea *>(receiver)) { + bool preventStealing = mouseArea->preventStealing(); +#if QT_CONFIG(quick_draganddrop) + if (mouseArea->drag() && mouseArea->drag()->target()) + preventStealing = true; +#endif + if (!preventStealing && receiverKeepsGrab) { + receiverRelinquishGrab = !receiverDisabled + || (QQuickDeliveryAgentPrivate::isMouseEvent(event) + && firstPoint.state() == QEventPoint::State::Pressed + && (receiver->acceptedMouseButtons() & static_cast<QMouseEvent *>(event)->button())); + if (receiverRelinquishGrab) + receiverKeepsGrab = false; + } + } + if ((stealThisEvent || contains(localPos)) && (!receiver || !receiverKeepsGrab || receiverDisabled)) { QScopedPointer<QPointerEvent> localizedEvent(QQuickDeliveryAgentPrivate::clonePointerEvent(event, localPos)); localizedEvent->setAccepted(false); @@ -2586,7 +2729,9 @@ bool QQuickFlickable::filterPointerEvent(QQuickItem *receiver, QPointerEvent *ev case QEventPoint::State::Pressed: d->handlePressEvent(localizedEvent.data()); d->captureDelayedPress(receiver, event); - stealThisEvent = d->stealMouse; // Update stealThisEvent in case changed by function call above + // never grab the pointing device on press during filtering: do it later, during a move + d->stealMouse = false; + stealThisEvent = false; break; case QEventPoint::State::Released: d->handleReleaseEvent(localizedEvent.data()); @@ -2603,7 +2748,7 @@ bool QQuickFlickable::filterPointerEvent(QQuickItem *receiver, QPointerEvent *ev event->setExclusiveGrabber(firstPoint, this); } - const bool filtered = stealThisEvent || d->delayedPressEvent || receiverDisabled; + const bool filtered = !receiverRelinquishGrab && (stealThisEvent || d->delayedPressEvent || receiverDisabled); if (filtered) { event->setAccepted(true); } @@ -2629,8 +2774,19 @@ bool QQuickFlickable::filterPointerEvent(QQuickItem *receiver, QPointerEvent *ev bool QQuickFlickable::childMouseEventFilter(QQuickItem *i, QEvent *e) { Q_D(QQuickFlickable); + QPointerEvent *pointerEvent = e->isPointerEvent() ? static_cast<QPointerEvent *>(e) : nullptr; + + auto wantsPointerEvent_helper = [this, d, i, pointerEvent]() { + Q_ASSERT(pointerEvent); + QQuickDeliveryAgentPrivate::localizePointerEvent(pointerEvent, this); + const bool wants = d->wantsPointerEvent(pointerEvent); + // re-localize event back to \a i before returning + QQuickDeliveryAgentPrivate::localizePointerEvent(pointerEvent, i); + return wants; + }; + if (!isVisible() || !isEnabled() || !isInteractive() || - (e->isPointerEvent() && !d->wantsPointerEvent(static_cast<QPointerEvent *>(e)))) { + (pointerEvent && !wantsPointerEvent_helper())) { d->cancelInteraction(); return QQuickItem::childMouseEventFilter(i, e); } @@ -2642,8 +2798,8 @@ bool QQuickFlickable::childMouseEventFilter(QQuickItem *i, QEvent *e) qCDebug(lcFilter) << "filtering UngrabMouse" << spe->points().first() << "for" << i << "grabber is" << grabber; if (grabber != this) mouseUngrabEvent(); // A child has been ungrabbed - } else if (e->isPointerEvent()) { - return filterPointerEvent(i, static_cast<QPointerEvent *>(e)); + } else if (pointerEvent) { + return filterPointerEvent(i, pointerEvent); } return QQuickItem::childMouseEventFilter(i, e); @@ -2672,9 +2828,12 @@ void QQuickFlickable::setMaximumFlickVelocity(qreal v) /*! \qmlproperty real QtQuick::Flickable::flickDeceleration - This property holds the rate at which a flick will decelerate. + This property holds the rate at which a flick will decelerate: + the higher the number, the faster it slows down when the user stops + flicking via touch. For example 0.0001 is nearly + "frictionless", and 10000 feels quite "sticky". - The default value is platform dependent. + The default value is platform dependent. Values of zero or less are not allowed. */ qreal QQuickFlickable::flickDeceleration() const { @@ -2687,7 +2846,7 @@ void QQuickFlickable::setFlickDeceleration(qreal deceleration) Q_D(QQuickFlickable); if (deceleration == d->deceleration) return; - d->deceleration = deceleration; + d->deceleration = qMax(0.001, deceleration); emit flickDecelerationChanged(); } @@ -2897,6 +3056,12 @@ void QQuickFlickable::movementStarting() if (!wasMoving && (d->hData.moving || d->vData.moving)) { emit movingChanged(); emit movementStarted(); +#if QT_CONFIG(accessibility) + if (QAccessible::isActive()) { + QAccessibleEvent ev(this, QAccessible::ScrollingStart); + QAccessible::updateAccessibility(&ev); + } +#endif } } @@ -2910,7 +3075,7 @@ void QQuickFlickable::movementEnding(bool hMovementEnding, bool vMovementEnding) Q_D(QQuickFlickable); // emit flicking signals - bool wasFlicking = d->hData.flicking || d->vData.flicking; + const bool wasFlicking = d->hData.flicking || d->vData.flicking; if (hMovementEnding && d->hData.flicking) { d->hData.flicking = false; emit flickingHorizontallyChanged(); @@ -2922,6 +3087,10 @@ void QQuickFlickable::movementEnding(bool hMovementEnding, bool vMovementEnding) if (wasFlicking && (!d->hData.flicking || !d->vData.flicking)) { emit flickingChanged(); emit flickEnded(); + } else if (d->hData.flickingWhenDragBegan || d->vData.flickingWhenDragBegan) { + d->hData.flickingWhenDragBegan = !hMovementEnding; + d->vData.flickingWhenDragBegan = !vMovementEnding; + emit flickEnded(); } // emit moving signals @@ -2941,6 +3110,12 @@ void QQuickFlickable::movementEnding(bool hMovementEnding, bool vMovementEnding) if (wasMoving && !isMoving()) { emit movingChanged(); emit movementEnded(); +#if QT_CONFIG(accessibility) + if (QAccessible::isActive()) { + QAccessibleEvent ev(this, QAccessible::ScrollingEnd); + QAccessible::updateAccessibility(&ev); + } +#endif } if (hMovementEnding) { @@ -3068,4 +3243,6 @@ void QQuickFlickable::setBoundsMovement(BoundsMovement movement) QT_END_NAMESPACE +#include "moc_qquickflickable_p_p.cpp" + #include "moc_qquickflickable_p.cpp" |