diff options
Diffstat (limited to 'src/quick/items/qquickflickable.cpp')
-rw-r--r-- | src/quick/items/qquickflickable.cpp | 956 |
1 files changed, 659 insertions, 297 deletions
diff --git a/src/quick/items/qquickflickable.cpp b/src/quick/items/qquickflickable.cpp index 7e1f54f07e..5a9a0ff846 100644 --- a/src/quick/items/qquickflickable.cpp +++ b/src/quick/items/qquickflickable.cpp @@ -1,48 +1,15 @@ -/**************************************************************************** -** -** Copyright (C) 2019 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,40 +19,26 @@ #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 <QtCore/qmath.h> -#include "qplatformdefs.h" +#include <qpa/qplatformtheme.h> #include <math.h> #include <cmath> QT_BEGIN_NAMESPACE -Q_DECLARE_LOGGING_CATEGORY(lcHandlerParent) - -// FlickThreshold determines how far the "mouse" must have moved -// before we perform a flick. -static const int FlickThreshold = 15; +Q_STATIC_LOGGING_CATEGORY(lcFlickable, "qt.quick.flickable") +Q_STATIC_LOGGING_CATEGORY(lcFilter, "qt.quick.flickable.filter") +Q_STATIC_LOGGING_CATEGORY(lcReplay, "qt.quick.flickable.replay") +Q_STATIC_LOGGING_CATEGORY(lcWheel, "qt.quick.flickable.wheel") +Q_STATIC_LOGGING_CATEGORY(lcVel, "qt.quick.flickable.velocity") // 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); } @@ -128,8 +81,14 @@ void QQuickFlickableVisibleArea::updateVisible() // Vertical const qreal viewheight = flickable->height(); const qreal maxyextent = -flickable->maxYExtent() + flickable->minYExtent(); - qreal pagePos = (-p->vData.move.value() + flickable->minYExtent()) / (maxyextent + viewheight); - qreal pageSize = viewheight / (maxyextent + viewheight); + const qreal maxYBounds = maxyextent + viewheight; + qreal pagePos = 0; + qreal pageSize = 0; + if (!qFuzzyIsNull(maxYBounds)) { + qreal y = p->pixelAligned ? std::round(p->vData.move.value()) : p->vData.move.value(); + pagePos = (-y + flickable->minYExtent()) / maxYBounds; + pageSize = viewheight / maxYBounds; + } if (pageSize != m_heightRatio) { m_heightRatio = pageSize; @@ -143,8 +102,15 @@ void QQuickFlickableVisibleArea::updateVisible() // Horizontal const qreal viewwidth = flickable->width(); const qreal maxxextent = -flickable->maxXExtent() + flickable->minXExtent(); - pagePos = (-p->hData.move.value() + flickable->minXExtent()) / (maxxextent + viewwidth); - pageSize = viewwidth / (maxxextent + viewwidth); + const qreal maxXBounds = maxxextent + viewwidth; + if (!qFuzzyIsNull(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; + pageSize = 0; + } if (pageSize != m_widthRatio) { m_widthRatio = pageSize; @@ -237,9 +203,27 @@ QQuickFlickablePrivate::AxisData::~AxisData() delete transitionToBounds; } +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) @@ -249,15 +233,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() @@ -266,26 +255,29 @@ void QQuickFlickablePrivate::init() QQml_setParent_noEvent(contentItem, q); contentItem->setParentItem(q); qmlobject_connect(&timeline, QQuickTimeLine, SIGNAL(completed()), - q, QQuickFlickable, SLOT(timelineCompleted())) + q, QQuickFlickable, SLOT(timelineCompleted())); qmlobject_connect(&velocityTimeline, QQuickTimeLine, SIGNAL(completed()), - q, QQuickFlickable, SLOT(velocityTimelineCompleted())) + q, QQuickFlickable, SLOT(velocityTimelineCompleted())); q->setAcceptedMouseButtons(Qt::LeftButton); - q->setAcceptTouchEvents(false); // rely on mouse events synthesized from touch + 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) @@ -312,17 +304,23 @@ 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) { - Qt::Orientations orient = nullptr; + Qt::Orientations orient; if (change.xChange()) 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) @@ -330,20 +328,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; @@ -365,13 +364,14 @@ 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)); @@ -422,6 +422,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); @@ -465,6 +473,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) { @@ -475,18 +493,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; @@ -504,6 +522,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); @@ -513,33 +540,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) { @@ -580,8 +615,6 @@ void QQuickFlickablePrivate::updateBeginningEnd() This signal is emitted when the view starts to be dragged due to user interaction. - - The corresponding handler is \c onDragStarted. */ /*! @@ -591,8 +624,6 @@ void QQuickFlickablePrivate::updateBeginningEnd() If the velocity of the drag is sufficient at the time the touch/mouse button is released then a flick will start. - - The corresponding handler is \c onDragEnded. */ /*! @@ -678,8 +709,6 @@ void QQuickFlickablePrivate::updateBeginningEnd() This signal is emitted when the view begins moving due to user interaction or a generated flick(). - - The corresponding handler is \c onMovementStarted. */ /*! @@ -690,8 +719,6 @@ void QQuickFlickablePrivate::updateBeginningEnd() be emitted once the flick stops. If a flick was not active, this signal will be emitted when the user stops dragging - i.e. a mouse or touch release. - - The corresponding handler is \c onMovementEnded. */ /*! @@ -700,16 +727,13 @@ void QQuickFlickablePrivate::updateBeginningEnd() This signal is emitted when the view is flicked. A flick starts from the point that the mouse or touch is released, while still in motion. - - The corresponding handler is \c onFlickStarted. */ /*! \qmlsignal QtQuick::Flickable::flickEnded() - This signal is emitted when the view stops moving due to a flick. - - The corresponding handler is \c onFlickEnded. + This signal is emitted when the view stops moving after a flick + or a series of flicks. */ /*! @@ -731,8 +755,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) @@ -786,8 +808,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 @@ -804,8 +829,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; + } } /*! @@ -1019,6 +1047,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()) @@ -1033,7 +1072,7 @@ qreal QQuickFlickablePrivate::devicePixelRatio() const return (window ? window->effectiveDevicePixelRatio() : qApp->devicePixelRatio()); } -void QQuickFlickablePrivate::handleMousePressEvent(QMouseEvent *event) +void QQuickFlickablePrivate::handlePressEvent(QPointerEvent *event) { Q_Q(QQuickFlickable); timer.start(); @@ -1061,14 +1100,17 @@ void QQuickFlickablePrivate::handleMousePressEvent(QMouseEvent *event) } q->setKeepMouseGrab(stealMouse); - maybeBeginDrag(computeCurrentTime(event), event->localPos()); + 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(); @@ -1090,7 +1132,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(); @@ -1129,6 +1173,10 @@ void QQuickFlickablePrivate::drag(qint64 currentTimestamp, QEvent::Type eventTyp bool prevVMoved = vMoved; qint64 elapsedSincePress = currentTimestamp - lastPressTime; + qCDebug(lcFlickable).nospace() << currentTimestamp << ' ' << eventType << " drag @ " << localPos.x() << ',' << localPos.y() + << " \u0394 " << deltas.x() << ',' << deltas.y() << " vel " << velocity.x() << ',' << velocity.y() + << " thrsld? " << overThreshold << " momentum? " << momentum << " velSens? " << velocitySensitiveOverBounds + << " sincePress " << elapsedSincePress; if (q->yflick()) { qreal dy = deltas.y(); @@ -1153,9 +1201,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) { @@ -1166,7 +1214,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 { @@ -1182,7 +1230,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 { @@ -1226,9 +1274,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) { @@ -1239,7 +1287,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 { @@ -1255,7 +1303,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 { @@ -1312,34 +1360,33 @@ void QQuickFlickablePrivate::drag(qint64 currentTimestamp, QEvent::Type eventTyp lastPos = localPos; } -void QQuickFlickablePrivate::handleMouseMoveEvent(QMouseEvent *event) +void QQuickFlickablePrivate::handleMoveEvent(QPointerEvent *event) { Q_Q(QQuickFlickable); - if (!interactive || lastPosTime == -1 || event->buttons() == Qt::NoButton) + if (!interactive || lastPosTime == -1 || + (event->isSinglePointEvent() && !static_cast<QSinglePointEvent *>(event)->buttons().testFlag(Qt::LeftButton))) return; qint64 currentTimestamp = computeCurrentTime(event); - QVector2D deltas = QVector2D(event->localPos() - pressPos); + const auto &firstPoint = event->points().first(); + const auto &pos = firstPoint.position(); + const QVector2D deltas = QVector2D(pos - q->mapFromGlobal(firstPoint.globalPressPosition())); + const QVector2D velocity = firstPointLocalVelocity(event); bool overThreshold = false; - QVector2D velocity = QGuiApplicationPrivate::mouseEventVelocity(event); - // TODO guarantee that events always have velocity so that it never needs to be computed here - if (!(QGuiApplicationPrivate::mouseEventCaps(event) & QTouchDevice::Velocity)) { - qint64 lastTimestamp = (lastPos.isNull() ? lastPressTime : lastPosTime); - if (currentTimestamp == lastTimestamp) - return; // events are too close together: velocity would be infinite - qreal elapsed = qreal(currentTimestamp - lastTimestamp) / 1000.; - velocity = QVector2D(event->localPos() - (lastPos.isNull() ? pressPos : lastPos)) / elapsed; - } - if (q->yflick()) - overThreshold |= QQuickWindowPrivate::dragOverThreshold(deltas.y(), Qt::YAxis, event); - if (q->xflick()) - overThreshold |= QQuickWindowPrivate::dragOverThreshold(deltas.x(), Qt::XAxis, event); + 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(), event->localPos(), deltas, overThreshold, false, false, velocity); + drag(currentTimestamp, event->type(), pos, deltas, overThreshold, false, false, velocity); } -void QQuickFlickablePrivate::handleMouseReleaseEvent(QMouseEvent *event) +void QQuickFlickablePrivate::handleReleaseEvent(QPointerEvent *event) { Q_Q(QQuickFlickable); stealMouse = false; @@ -1360,11 +1407,15 @@ void QQuickFlickablePrivate::handleMouseReleaseEvent(QMouseEvent *event) hData.vTime = vData.vTime = timeline.time(); bool canBoost = false; + const auto pos = event->points().first().position(); + 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 = (QGuiApplicationPrivate::mouseEventCaps(event) & QTouchDevice::Velocity) - ? QGuiApplicationPrivate::mouseEventVelocity(event).y() : vData.velocity; + vVelocity = (event->device()->capabilities().testFlag(QInputDevice::Capability::Velocity) + ? eventVelocity.y() : vData.velocity); } if ((vData.atBeginning && vVelocity > 0.) || (vData.atEnd && vVelocity < 0.)) { vVelocity /= 2; @@ -1378,8 +1429,8 @@ void QQuickFlickablePrivate::handleMouseReleaseEvent(QMouseEvent *event) qreal hVelocity = 0; if (elapsed < 100 && hData.velocity != 0.) { - hVelocity = (QGuiApplicationPrivate::mouseEventCaps(event) & QTouchDevice::Velocity) - ? QGuiApplicationPrivate::mouseEventVelocity(event).x() : hData.velocity; + hVelocity = (event->device()->capabilities().testFlag(QInputDevice::Capability::Velocity) + ? eventVelocity.x() : hData.velocity); } if ((hData.atBeginning && hVelocity > 0.) || (hData.atEnd && hVelocity < 0.)) { hVelocity /= 2; @@ -1392,23 +1443,32 @@ void QQuickFlickablePrivate::handleMouseReleaseEvent(QMouseEvent *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(event->localPos().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(event->localPos().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) @@ -1418,16 +1478,23 @@ void QQuickFlickablePrivate::handleMouseReleaseEvent(QMouseEvent *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) { Q_D(QQuickFlickable); - if (d->interactive) { + if (d->interactive && !d->replayingPressEvent && d->wantsPointerEvent(event)) { if (!d->pressed) - d->handleMousePressEvent(event); + d->handlePressEvent(event); event->accept(); } else { QQuickItem::mousePressEvent(event); @@ -1437,8 +1504,8 @@ void QQuickFlickable::mousePressEvent(QMouseEvent *event) void QQuickFlickable::mouseMoveEvent(QMouseEvent *event) { Q_D(QQuickFlickable); - if (d->interactive) { - d->handleMouseMoveEvent(event); + if (d->interactive && d->wantsPointerEvent(event)) { + d->handleMoveEvent(event); event->accept(); } else { QQuickItem::mouseMoveEvent(event); @@ -1448,15 +1515,18 @@ void QQuickFlickable::mouseMoveEvent(QMouseEvent *event) void QQuickFlickable::mouseReleaseEvent(QMouseEvent *event) { Q_D(QQuickFlickable); - if (d->interactive) { + if (d->interactive && d->wantsPointerEvent(event)) { if (d->delayedPressEvent) { d->replayDelayedPress(); - // Now send the release - if (window() && window()->mouseGrabberItem()) { - QPointF localPos = window()->mouseGrabberItem()->mapFromScene(event->windowPos()); - QScopedPointer<QMouseEvent> mouseEvent(QQuickWindowPrivate::cloneMouseEvent(event, &localPos)); - QCoreApplication::sendEvent(window(), mouseEvent.data()); + 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 @@ -1465,21 +1535,87 @@ void QQuickFlickable::mouseReleaseEvent(QMouseEvent *event) return; } - d->handleMouseReleaseEvent(event); + d->handleReleaseEvent(event); event->accept(); } else { QQuickItem::mouseReleaseEvent(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()) { + case QEventPoint::State::Pressed: + if (d->interactive && !d->replayingPressEvent && d->wantsPointerEvent(event)) { + if (!d->pressed) + d->handlePressEvent(event); + event->accept(); + } else { + unhandled = true; + } + break; + case QEventPoint::State::Updated: + if (d->interactive && d->wantsPointerEvent(event)) { + d->handleMoveEvent(event); + event->accept(); + } else { + unhandled = true; + } + break; + case QEventPoint::State::Released: + if (d->interactive && d->wantsPointerEvent(event)) { + if (d->delayedPressEvent) { + d->replayDelayedPress(); + + 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()); + } + + // And the event has been consumed + d->stealMouse = false; + d->pressed = false; + return; + } + + d->handleReleaseEvent(event); + event->accept(); + } else { + unhandled = true; + } + break; + case QEventPoint::State::Stationary: + case QEventPoint::State::Unknown: + break; + } + if (unhandled) + QQuickItem::touchEvent(event); +} + #if QT_CONFIG(wheelevent) void QQuickFlickable::wheelEvent(QWheelEvent *event) { Q_D(QQuickFlickable); - if (!d->interactive) { + if (!d->interactive || !d->wantsPointerEvent(event)) { QQuickItem::wheelEvent(event); return; } + qCDebug(lcWheel) << event->device() << event << event->source(); event->setAccepted(false); qint64 currentTimestamp = d->computeCurrentTime(event); switch (event->phase()) { @@ -1489,7 +1625,8 @@ void QQuickFlickable::wheelEvent(QWheelEvent *event) d->vData.velocity = 0; d->hData.velocity = 0; d->timer.start(); - d->maybeBeginDrag(currentTimestamp, event->posF()); + d->maybeBeginDrag(currentTimestamp, event->position()); + d->lastPosTime = -1; break; case Qt::NoScrollPhase: // default phase with an ordinary wheel mouse case Qt::ScrollUpdate: @@ -1500,7 +1637,8 @@ void QQuickFlickable::wheelEvent(QWheelEvent *event) d->pressed = false; d->scrollingPhase = false; d->draggingEnding(); - event->accept(); + if (isMoving()) + event->accept(); d->lastPosTime = -1; break; case Qt::ScrollEnd: @@ -1516,64 +1654,153 @@ 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(); - 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 (valid) { - d->flickY(d->vData.velocity); - d->flickingStarted(false, true); - if (d->vData.flicking) { - d->vMoved = true; + + 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(); } - } - 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) { - d->hMoved = 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 (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(); + } + event->accept(); + } + } + if (xflick() && xDelta != 0) { + 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 { - // use pixelDelta (probably from a trackpad) + // 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()); - d->drag(currentTimestamp, event->type(), event->posF(), d->accumulatedWheelPixelDelta, true, !d->scrollingPhase, true, velocity); - event->accept(); + // Try to drag if 1) we already are dragging or flicking, or + // 2) the flickable is free to flick both directions, or + // 3) the movement so far has been mostly horizontal AND it's free to flick horizontally, or + // 4) the movement so far has been mostly vertical AND it's free to flick vertically. + // Otherwise, wait until the next event. Wheel events with pixel deltas tend to come frequently. + if (isMoving() || isFlicking() || (yflick() && xflick()) + || (xflick() && qAbs(d->accumulatedWheelPixelDelta.x()) > qAbs(d->accumulatedWheelPixelDelta.y() * 2)) + || (yflick() && qAbs(d->accumulatedWheelPixelDelta.y()) > qAbs(d->accumulatedWheelPixelDelta.x() * 2))) { + d->drag(currentTimestamp, event->type(), event->position(), d->accumulatedWheelPixelDelta, + true, !d->scrollingPhase, true, velocity); + event->accept(); + } else { + qCDebug(lcWheel) << "not dragging: accumulated deltas" << d->accumulatedWheelPixelDelta << + "moving?" << isMoving() << "can flick horizontally?" << xflick() << "vertically?" << yflick(); + } } + d->lastPosTime = currentTimestamp; if (!event->isAccepted()) QQuickItem::wheelEvent(event); @@ -1595,7 +1822,7 @@ bool QQuickFlickablePrivate::isInnermostPressDelay(QQuickItem *i) const return false; } -void QQuickFlickablePrivate::captureDelayedPress(QQuickItem *item, QMouseEvent *event) +void QQuickFlickablePrivate::captureDelayedPress(QQuickItem *item, QPointerEvent *event) { Q_Q(QQuickFlickable); if (!q->window() || pressDelay <= 0) @@ -1606,15 +1833,17 @@ void QQuickFlickablePrivate::captureDelayedPress(QQuickItem *item, QMouseEvent * if (!isInnermostPressDelay(item)) return; - delayedPressEvent = QQuickWindowPrivate::cloneMouseEvent(event); + delayedPressEvent = QQuickDeliveryAgentPrivate::clonePointerEvent(event); delayedPressEvent->setAccepted(false); delayedPressTimer.start(pressDelay, q); + qCDebug(lcReplay) << "begin press delay" << pressDelay << "ms with" << delayedPressEvent; } void QQuickFlickablePrivate::clearDelayedPress() { if (delayedPressEvent) { delayedPressTimer.stop(); + qCDebug(lcReplay) << "clear delayed press" << delayedPressEvent; delete delayedPressEvent; delayedPressEvent = nullptr; } @@ -1625,31 +1854,55 @@ void QQuickFlickablePrivate::replayDelayedPress() Q_Q(QQuickFlickable); if (delayedPressEvent) { // Losing the grab will clear the delayed press event; take control of it here - QScopedPointer<QMouseEvent> mouseEvent(delayedPressEvent); + QScopedPointer<QPointerEvent> event(delayedPressEvent); delayedPressEvent = nullptr; delayedPressTimer.stop(); // If we have the grab, release before delivering the event - if (QQuickWindow *w = q->window()) { - QQuickWindowPrivate *wpriv = QQuickWindowPrivate::get(w); - wpriv->allowChildEventFiltering = false; // don't allow re-filtering during replay + if (QQuickWindow *window = q->window()) { + auto da = deliveryAgentPrivate(); + da->allowChildEventFiltering = false; // don't allow re-filtering during replay replayingPressEvent = true; - if (w->mouseGrabberItem() == q) - q->ungrabMouse(); - - // Use the event handler that will take care of finding the proper item to propagate the event - QCoreApplication::sendEvent(w, mouseEvent.data()); + auto &firstPoint = event->point(0); + // At first glance, it's weird for delayedPressEvent to already have a grabber; + // but on press, filterMouseEvent() took the exclusive grab, and that's stored + // in the device-specific EventPointData instance in QPointingDevicePrivate::activePoints, + // not in the event itself. If this Flickable is still the grabber of that point on that device, + // that's the reason; but now it doesn't need that grab anymore. + if (event->exclusiveGrabber(firstPoint) == q) + event->setExclusiveGrabber(firstPoint, nullptr); + + qCDebug(lcReplay) << "replaying" << event.data(); + // Put scenePosition into position, for the sake of QQuickWindowPrivate::translateTouchEvent() + // TODO remove this if we remove QQuickWindowPrivate::translateTouchEvent() + 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. + QCoreApplication::sendEvent(window, event.data()); + qCDebug(lcReplay) << "replay done"; + + // We're done with replay, go back to normal delivery behavior replayingPressEvent = false; - wpriv->allowChildEventFiltering = true; + da->allowChildEventFiltering = true; } } } //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(); @@ -1673,10 +1926,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(); @@ -1744,19 +2005,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)) { @@ -1765,18 +2030,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); - 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); + } } } @@ -1788,7 +2057,7 @@ 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) timeline.accel(data.move, -data.smoothVelocity.value(), deceleration*QML_FLICK_OVERSHOOTFRICTION, maxDistance); @@ -1799,17 +2068,16 @@ void QQuickFlickablePrivate::viewportAxisMoved(AxisData &data, qreal minExtent, data.vTime = timeline.time(); } -void QQuickFlickable::geometryChanged(const QRectF &newGeometry, - const QRectF &oldGeometry) +void QQuickFlickable::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) { Q_D(QQuickFlickable); - QQuickItem::geometryChanged(newGeometry, oldGeometry); + QQuickItem::geometryChange(newGeometry, oldGeometry); bool changed = false; if (newGeometry.width() != oldGeometry.width()) { changed = true; // we must update visualArea.widthRatio if (d->hData.viewSize < 0) - d->contentItem->setWidth(width()); + d->contentItem->setWidth(width() - d->hData.startMargin - d->hData.endMargin); // Make sure that we're entirely in view. if (!d->pressed && !d->hData.moving && !d->vData.moving) { d->fixupMode = QQuickFlickablePrivate::Immediate; @@ -1819,7 +2087,7 @@ void QQuickFlickable::geometryChanged(const QRectF &newGeometry, if (newGeometry.height() != oldGeometry.height()) { changed = true; // we must update visualArea.heightRatio if (d->vData.viewSize < 0) - d->contentItem->setHeight(height()); + d->contentItem->setHeight(height() - d->vData.startMargin - d->vData.endMargin); // Make sure that we're entirely in view. if (!d->pressed && !d->hData.moving && !d->vData.moving) { d->fixupMode = QQuickFlickablePrivate::Immediate; @@ -1837,7 +2105,7 @@ void QQuickFlickable::geometryChanged(const QRectF &newGeometry, 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) @@ -1849,8 +2117,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; @@ -1897,6 +2165,9 @@ void QQuickFlickable::cancelFlick() void QQuickFlickablePrivate::data_append(QQmlListProperty<QObject> *prop, QObject *o) { + if (!prop || !prop->data) + return; + if (QQuickItem *i = qmlobject_cast<QQuickItem *>(o)) { i->setParentItem(static_cast<QQuickFlickablePrivate*>(prop->data)->contentItem); } else if (QQuickPointerHandler *pointerHandler = qmlobject_cast<QQuickPointerHandler *>(o)) { @@ -1906,13 +2177,13 @@ void QQuickFlickablePrivate::data_append(QQmlListProperty<QObject> *prop, QObjec } } -int QQuickFlickablePrivate::data_count(QQmlListProperty<QObject> *) +qsizetype QQuickFlickablePrivate::data_count(QQmlListProperty<QObject> *) { // XXX todo return 0; } -QObject *QQuickFlickablePrivate::data_at(QQmlListProperty<QObject> *, int) +QObject *QQuickFlickablePrivate::data_at(QQmlListProperty<QObject> *, qsizetype) { // XXX todo return nullptr; @@ -2077,7 +2348,7 @@ void QQuickFlickable::setContentWidth(qreal w) return; d->hData.viewSize = w; if (w < 0) - d->contentItem->setWidth(width()); + d->contentItem->setWidth(width() - d->hData.startMargin - d->hData.endMargin); else d->contentItem->setWidth(w); d->hData.markExtentsDirty(); @@ -2106,7 +2377,7 @@ void QQuickFlickable::setContentHeight(qreal h) return; d->vData.viewSize = h; if (h < 0) - d->contentItem->setHeight(height()); + d->contentItem->setHeight(height() - d->vData.startMargin - d->vData.endMargin); else d->contentItem->setHeight(h); d->vData.markExtentsDirty(); @@ -2315,23 +2586,39 @@ 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); - if ((d->flickableDirection & QQuickFlickable::AutoFlickIfNeeded) && (vWidth() > width())) + const int contentWidthWithMargins = d->contentItem->width() + d->hData.startMargin + d->hData.endMargin; + if ((d->flickableDirection & QQuickFlickable::AutoFlickIfNeeded) && (contentWidthWithMargins > width())) return true; if (d->flickableDirection == QQuickFlickable::AutoFlickDirection) - return std::floor(qAbs(vWidth() - width())); + return std::floor(qAbs(contentWidthWithMargins - width())); 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); - if ((d->flickableDirection & QQuickFlickable::AutoFlickIfNeeded) && (vHeight() > height())) + const int contentHeightWithMargins = d->contentItem->height() + d->vData.startMargin + d->vData.endMargin; + if ((d->flickableDirection & QQuickFlickable::AutoFlickIfNeeded) && (contentHeightWithMargins > height())) return true; if (d->flickableDirection == QQuickFlickable::AutoFlickDirection) - return std::floor(qAbs(vHeight() - height())); + return std::floor(qAbs(contentHeightWithMargins - height())); return d->flickableDirection & QQuickFlickable::VerticalFlick; } @@ -2368,56 +2655,99 @@ void QQuickFlickablePrivate::addPointerHandler(QQuickPointerHandler *h) QQuickItemPrivate::get(contentItem)->addPointerHandler(h); } -/*! - QQuickFlickable::filterMouseEvent checks filtered mouse events and potentially steals them. +/*! \internal + QQuickFlickable::filterPointerEvent filters pointer events intercepted on the way + to the child \a receiver, and potentially steals the exclusive grab. - This is how flickable takes over events from other items (\a receiver) that are on top of it. - It filters their events and may take over (grab) the \a event. - Return true if the mouse event will be stolen. - \internal + This is how flickable takes over the handling of events from child items. + + Returns true if the event will be stolen and should <em>not</em> be delivered to the \a receiver. */ -bool QQuickFlickable::filterMouseEvent(QQuickItem *receiver, QMouseEvent *event) +bool QQuickFlickable::filterPointerEvent(QQuickItem *receiver, QPointerEvent *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"); Q_D(QQuickFlickable); - QPointF localPos = mapFromScene(event->windowPos()); + // 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; + } - Q_ASSERT_X(receiver != this, "", "Flickable received a filter event for itself"); - if (receiver == this && d->stealMouse) { - // we are already the grabber and we do want the mouse event to ourselves. + 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()); - if ((stealThisEvent || contains(localPos)) && (!receiver || !receiverKeepsGrab || receiverDisabled)) { - QScopedPointer<QMouseEvent> mouseEvent(QQuickWindowPrivate::cloneMouseEvent(event, &localPos)); - mouseEvent->setAccepted(false); + 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; + } + } - switch (mouseEvent->type()) { - case QEvent::MouseMove: - d->handleMouseMoveEvent(mouseEvent.data()); + if ((stealThisEvent || contains(localPos)) && (!receiver || !receiverKeepsGrab || receiverDisabled)) { + QScopedPointer<QPointerEvent> localizedEvent(QQuickDeliveryAgentPrivate::clonePointerEvent(event, localPos)); + localizedEvent->setAccepted(false); + switch (firstPoint.state()) { + case QEventPoint::State::Updated: + d->handleMoveEvent(localizedEvent.data()); break; - case QEvent::MouseButtonPress: - d->handleMousePressEvent(mouseEvent.data()); + 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 QEvent::MouseButtonRelease: - d->handleMouseReleaseEvent(mouseEvent.data()); + case QEventPoint::State::Released: + d->handleReleaseEvent(localizedEvent.data()); stealThisEvent = d->stealMouse; break; - default: + case QEventPoint::State::Stationary: + case QEventPoint::State::Unknown: break; } if ((receiver && stealThisEvent && !receiverKeepsGrab && receiver != this) || receiverDisabled) { d->clearDelayedPress(); - grabMouse(); + event->setExclusiveGrabber(firstPoint, this); } else if (d->delayedPressEvent) { - grabMouse(); + event->setExclusiveGrabber(firstPoint, this); } - const bool filtered = stealThisEvent || d->delayedPressEvent || receiverDisabled; + const bool filtered = !receiverRelinquishGrab && (stealThisEvent || d->delayedPressEvent || receiverDisabled); if (filtered) { event->setAccepted(true); } @@ -2426,7 +2756,7 @@ bool QQuickFlickable::filterMouseEvent(QQuickItem *receiver, QMouseEvent *event) d->lastPosTime = -1; returnToBounds(); } - if (event->type() == QEvent::MouseButtonRelease || (receiverKeepsGrab && !receiverDisabled)) { + if (firstPoint.state() == QEventPoint::State::Released || (receiverKeepsGrab && !receiverDisabled)) { // mouse released, or another item has claimed the grab d->lastPosTime = -1; d->clearDelayedPress(); @@ -2436,28 +2766,39 @@ bool QQuickFlickable::filterMouseEvent(QQuickItem *receiver, QMouseEvent *event) return false; } - +/*! \internal + Despite the name, this function filters all pointer events on their way to any child within. + Returns true if the event will be stolen and should <em>not</em> be delivered to the \a receiver. +*/ bool QQuickFlickable::childMouseEventFilter(QQuickItem *i, QEvent *e) { Q_D(QQuickFlickable); - if (!isVisible() || !isEnabled() || !isInteractive()) { + 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() || + (pointerEvent && !wantsPointerEvent_helper())) { d->cancelInteraction(); return QQuickItem::childMouseEventFilter(i, e); } - switch (e->type()) { - case QEvent::MouseButtonPress: - case QEvent::MouseMove: - case QEvent::MouseButtonRelease: - return filterMouseEvent(i, static_cast<QMouseEvent *>(e)); - case QEvent::UngrabMouse: - if (d->window && d->window->mouseGrabberItem() && d->window->mouseGrabberItem() != this) { - // The grab has been taken away from a child and given to some other item. - mouseUngrabEvent(); - } - break; - default: - break; + if (e->type() == QEvent::UngrabMouse) { + Q_ASSERT(e->isSinglePointEvent()); + auto spe = static_cast<QSinglePointEvent *>(e); + const QObject *grabber = spe->exclusiveGrabber(spe->points().first()); + qCDebug(lcFilter) << "filtering UngrabMouse" << spe->points().first() << "for" << i << "grabber is" << grabber; + if (grabber != this) + mouseUngrabEvent(); // A child has been ungrabbed + } else if (pointerEvent) { + return filterPointerEvent(i, pointerEvent); } return QQuickItem::childMouseEventFilter(i, e); @@ -2486,9 +2827,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 { @@ -2501,7 +2845,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(); } @@ -2711,6 +3055,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 } } @@ -2724,7 +3074,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(); @@ -2736,6 +3086,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 @@ -2755,6 +3109,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) { @@ -2882,4 +3242,6 @@ void QQuickFlickable::setBoundsMovement(BoundsMovement movement) QT_END_NAMESPACE +#include "moc_qquickflickable_p_p.cpp" + #include "moc_qquickflickable_p.cpp" |