Add Flickable.acceptedButtons property

Presumably in Qt 4 times, before multi-touch was commonplace, someone
assumed that touch events could be handled the same as mouse events.
And until d7623d79ef Flickable relied upon
touch->mouse synthesis anyway. In Qt 5, it was possible to make a
distinction by checking QMouseEvent::source() (although if a mouse event
is synthesized, it might or might not have come from a touchscreen);
but we didn't use it in Flickable.

Now, the ability to scroll a Flickable by dragging the mouse has
become a pure anachronism, not really useful in any UI where a mouse
would normally be used (except perhaps for testing touch-like behavior
when the developer doesn't have a touchscreen). And if the developer
wants the mouse-drag gesture to do something else with content inside
Flickable, the fact that Flickable is always trying to take over the
grab (at least in one direction) interferes. So let's make it an
opt-out feature in Qt 6. Perhaps it should be opt-in in Qt 7, but
this is not yet done.

[ChangeLog][QtQuick][Flickable] Flickable.acceptedButtons has been added
with default Qt.LeftButton; set it to Qt.NoButton to disallow scrolling
by dragging the mouse.

Fixes: QTBUG-97111
Change-Id: I46026edf4cd35d2b945659e814ee6ca3c892ce9d
Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
This commit is contained in:
Shawn Rutledge 2024-06-18 00:13:30 -07:00
parent 24d51ce29e
commit cbc694491b
4 changed files with 103 additions and 20 deletions

View File

@ -231,6 +231,7 @@ QQuickFlickablePrivate::QQuickFlickablePrivate()
, scrollingPhase(false), interactive(true), calcVelocity(false)
, pixelAligned(false)
, syncDrag(false)
, acceptedButtons(Qt::LeftButton)
, lastPosTime(-1)
, lastPressTime(0)
, deceleration(QGuiApplicationPrivate::platformTheme()->themeHint(QPlatformTheme::FlickDeceleration).toReal())
@ -258,7 +259,7 @@ void QQuickFlickablePrivate::init()
q, QQuickFlickable, SLOT(timelineCompleted()));
qmlobject_connect(&velocityTimeline, QQuickTimeLine, SIGNAL(completed()),
q, QQuickFlickable, SLOT(velocityTimelineCompleted()));
q->setAcceptedMouseButtons(Qt::LeftButton);
q->setAcceptedMouseButtons(acceptedButtons);
q->setAcceptTouchEvents(true);
q->setFiltersChildMouseEvents(true);
q->setFlag(QQuickItem::ItemIsViewport);
@ -1047,6 +1048,38 @@ void QQuickFlickable::setSynchronousDrag(bool v)
}
}
/*!
\qmlproperty flags QtQuick::Flickable::acceptedButtons
\since 6.9
The mouse buttons that can be used to scroll this Flickable by dragging.
By default, this property is set to \l {QtQuick::MouseEvent::button} {Qt.LeftButton},
which provides the same behavior as in previous Qt versions; but in most
user interfaces, this behavior is unexpected. Users expect to flick only on
a touchscreen, and to use the mouse wheel, touchpad gestures or a scroll
bar with mouse or touchpad. Set it to \c Qt.NoButton to disable dragging.
It can be set to an OR combination of mouse buttons, and will ignore events
from other buttons.
*/
Qt::MouseButtons QQuickFlickable::acceptedButtons() const
{
Q_D(const QQuickFlickable);
return d->acceptedButtons;
}
void QQuickFlickable::setAcceptedButtons(Qt::MouseButtons buttons)
{
Q_D(QQuickFlickable);
if (d->acceptedButtons == buttons)
return;
d->acceptedButtons = buttons;
setAcceptedMouseButtons(buttons);
emit acceptedButtonsChanged();
}
/*! \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).
@ -1109,8 +1142,8 @@ void QQuickFlickablePrivate::maybeBeginDrag(qint64 currentTimestamp, const QPoin
{
Q_Q(QQuickFlickable);
clearDelayedPress();
// consider dragging only when event is left mouse button or touch event which has no button
pressed = buttons.testFlag(Qt::LeftButton) || (buttons == Qt::NoButton);
// consider dragging only when buttons intersect acceptedButtons, or it's a touch event which has no button
pressed = (buttons == Qt::NoButton) || (acceptedButtons != Qt::NoButton && (buttons & acceptedButtons) != 0);
if (hData.transitionToBounds)
hData.transitionToBounds->stopTransition();
@ -1364,7 +1397,7 @@ void QQuickFlickablePrivate::handleMoveEvent(QPointerEvent *event)
{
Q_Q(QQuickFlickable);
if (!interactive || lastPosTime == -1 ||
(event->isSinglePointEvent() && !static_cast<QSinglePointEvent *>(event)->buttons().testFlag(Qt::LeftButton)))
(event->isSinglePointEvent() && !buttonsAccepted(static_cast<QSinglePointEvent *>(event))))
return;
qint64 currentTimestamp = computeCurrentTime(event);
@ -1489,10 +1522,15 @@ void QQuickFlickablePrivate::handleReleaseEvent(QPointerEvent *event)
}
}
bool QQuickFlickablePrivate::buttonsAccepted(const QSinglePointEvent *event)
{
return !((event->button() & acceptedButtons) == 0 && (event->buttons() & acceptedButtons) == 0);
}
void QQuickFlickable::mousePressEvent(QMouseEvent *event)
{
Q_D(QQuickFlickable);
if (d->interactive && !d->replayingPressEvent && d->wantsPointerEvent(event)) {
if (d->interactive && !d->replayingPressEvent && d->buttonsAccepted(event) && d->wantsPointerEvent(event)) {
if (!d->pressed)
d->handlePressEvent(event);
event->accept();
@ -1504,7 +1542,7 @@ void QQuickFlickable::mousePressEvent(QMouseEvent *event)
void QQuickFlickable::mouseMoveEvent(QMouseEvent *event)
{
Q_D(QQuickFlickable);
if (d->interactive && d->wantsPointerEvent(event)) {
if (d->interactive && d->buttonsAccepted(event) && d->wantsPointerEvent(event)) {
d->handleMoveEvent(event);
event->accept();
} else {
@ -1515,7 +1553,7 @@ void QQuickFlickable::mouseMoveEvent(QMouseEvent *event)
void QQuickFlickable::mouseReleaseEvent(QMouseEvent *event)
{
Q_D(QQuickFlickable);
if (d->interactive && d->wantsPointerEvent(event)) {
if (d->interactive && d->buttonsAccepted(event) && d->wantsPointerEvent(event)) {
if (d->delayedPressEvent) {
d->replayDelayedPress();
@ -2664,12 +2702,16 @@ void QQuickFlickablePrivate::addPointerHandler(QQuickPointerHandler *h)
*/
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);
const bool isTouch = QQuickDeliveryAgentPrivate::isTouchEvent(event);
const bool isMouse = QQuickDeliveryAgentPrivate::isMouseEvent(event);
if (isMouse || QQuickDeliveryAgentPrivate::isTabletEvent(event)) {
if (!d->buttonsAccepted(static_cast<QSinglePointEvent *>(event)))
return QQuickItem::childMouseEventFilter(receiver, event);
} else if (!isTouch) {
return false; // don't filter hover events or wheel events, for example
}
Q_ASSERT_X(receiver != this, "", "Flickable received a filter event for itself");
// 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;
@ -2708,8 +2750,7 @@ bool QQuickFlickable::filterPointerEvent(QQuickItem *receiver, QPointerEvent *ev
preventStealing = true;
#endif
if (!preventStealing && receiverKeepsGrab) {
receiverRelinquishGrab = !receiverDisabled
|| (QQuickDeliveryAgentPrivate::isMouseEvent(event)
receiverRelinquishGrab = !receiverDisabled || (isMouse
&& firstPoint.state() == QEventPoint::State::Pressed
&& (receiver->acceptedMouseButtons() & static_cast<QMouseEvent *>(event)->button()));
if (receiverRelinquishGrab)

View File

@ -75,6 +75,8 @@ class Q_QUICK_EXPORT QQuickFlickable : public QQuickItem
Q_PROPERTY(qreal horizontalOvershoot READ horizontalOvershoot NOTIFY horizontalOvershootChanged REVISION(2, 9))
Q_PROPERTY(qreal verticalOvershoot READ verticalOvershoot NOTIFY verticalOvershootChanged REVISION(2, 9))
Q_PROPERTY(Qt::MouseButtons acceptedButtons READ acceptedButtons WRITE setAcceptedButtons NOTIFY acceptedButtonsChanged REVISION(6, 9) FINAL)
Q_PROPERTY(QQmlListProperty<QObject> flickableData READ flickableData)
Q_PROPERTY(QQmlListProperty<QQuickItem> flickableChildren READ flickableChildren)
Q_CLASSINFO("DefaultProperty", "flickableData")
@ -183,6 +185,9 @@ public:
bool synchronousDrag() const;
void setSynchronousDrag(bool v);
Qt::MouseButtons acceptedButtons() const;
void setAcceptedButtons(Qt::MouseButtons buttons);
qreal horizontalOvershoot() const;
qreal verticalOvershoot() const;
@ -239,6 +244,8 @@ Q_SIGNALS:
void atXBeginningChanged();
void atYBeginningChanged();
Q_REVISION(6, 9) void acceptedButtonsChanged();
protected:
bool childMouseEventFilter(QQuickItem *, QEvent *) override;
void mousePressEvent(QMouseEvent *event) override;

View File

@ -192,6 +192,7 @@ public:
bool calcVelocity : 1;
bool pixelAligned : 1;
bool syncDrag : 1;
Qt::MouseButtons acceptedButtons = Qt::LeftButton;
QElapsedTimer timer;
qint64 lastPosTime;
qint64 lastPressTime;
@ -229,6 +230,7 @@ public:
void handlePressEvent(QPointerEvent *);
void handleMoveEvent(QPointerEvent *);
void handleReleaseEvent(QPointerEvent *);
bool buttonsAccepted(const QSinglePointEvent *event);
void maybeBeginDrag(qint64 currentTimestamp, const QPointF &pressPosn,
Qt::MouseButtons buttons = Qt::NoButton);

View File

@ -1123,23 +1123,48 @@ void tst_qquickflickable::nestedTrackpad()
void tst_qquickflickable::movingAndFlicking_data()
{
const QPointingDevice *constTouchDevice = touchDevice;
QTest::addColumn<bool>("verticalEnabled");
QTest::addColumn<bool>("horizontalEnabled");
QTest::addColumn<Qt::MouseButtons>("acceptedButtons");
QTest::addColumn<Qt::MouseButton>("testButton");
QTest::addColumn<const QPointingDevice *>("device");
QTest::addColumn<QPoint>("flickToWithoutSnapBack");
QTest::addColumn<QPoint>("flickToWithSnapBack");
QTest::newRow("vertical")
<< true << false
<< true << false << Qt::MouseButtons(Qt::LeftButton) << Qt::LeftButton << mouseDevice
<< QPoint(50, 100)
<< QPoint(50, 300);
QTest::newRow("horizontal")
<< false << true
<< false << true << Qt::MouseButtons(Qt::LeftButton) << Qt::LeftButton << mouseDevice
<< QPoint(-50, 200)
<< QPoint(150, 200);
QTest::newRow("both")
<< true << true
<< true << true << Qt::MouseButtons(Qt::LeftButton) << Qt::LeftButton << mouseDevice
<< QPoint(-50, 100)
<< QPoint(150, 300);
QTest::newRow("mouse disabled")
<< true << true << Qt::MouseButtons(Qt::NoButton) << Qt::LeftButton << mouseDevice
<< QPoint(-50, 100)
<< QPoint(150, 300);
QTest::newRow("wrong button")
<< true << true << Qt::MouseButtons(Qt::RightButton) << Qt::LeftButton << mouseDevice
<< QPoint(-50, 100)
<< QPoint(150, 300);
QTest::newRow("right button")
<< true << true << Qt::MouseButtons(Qt::RightButton) << Qt::RightButton << mouseDevice
<< QPoint(-50, 100)
<< QPoint(150, 300);
QTest::newRow("touch")
<< true << true << Qt::MouseButtons(Qt::NoButton) << Qt::LeftButton << constTouchDevice
<< QPoint(-50, 100)
<< QPoint(150, 300);
}
@ -1148,9 +1173,11 @@ void tst_qquickflickable::movingAndFlicking()
{
QFETCH(bool, verticalEnabled);
QFETCH(bool, horizontalEnabled);
QFETCH(Qt::MouseButtons, acceptedButtons);
QFETCH(Qt::MouseButton, testButton);
QFETCH(QPoint, flickToWithoutSnapBack);
QFETCH(QPoint, flickToWithSnapBack);
auto device = mouseDevice;
QFETCH(const QPointingDevice *, device);
const QPoint flickFrom(50, 200); // centre
@ -1165,6 +1192,7 @@ void tst_qquickflickable::movingAndFlicking()
QQuickFlickable *flickable = qobject_cast<QQuickFlickable*>(window->rootObject());
QVERIFY(flickable != nullptr);
flickable->setAcceptedButtons(acceptedButtons);
QSignalSpy vMoveSpy(flickable, SIGNAL(movingVerticallyChanged()));
QSignalSpy hMoveSpy(flickable, SIGNAL(movingHorizontallyChanged()));
@ -1179,7 +1207,12 @@ void tst_qquickflickable::movingAndFlicking()
QSignalSpy flickEndSpy(flickable, SIGNAL(flickEnded()));
// do a flick that keeps the view within the bounds
QQuickTest::pointerFlick(device, window.data(), 0, flickFrom, flickToWithoutSnapBack, 200);
QQuickTest::pointerFlick(device, window.data(), 0, flickFrom, flickToWithoutSnapBack, 200, testButton);
if (!(acceptedButtons & testButton) && device->type() != QInputDevice::DeviceType::TouchScreen) {
QVERIFY(!flickable->isMoving());
return;
}
QTRY_VERIFY(flickable->isMoving());
QCOMPARE(flickable->isMovingHorizontally(), horizontalEnabled);
@ -1238,7 +1271,7 @@ void tst_qquickflickable::movingAndFlicking()
flickable->setContentX(0);
flickable->setContentY(0);
QTRY_VERIFY(!flickable->isMoving());
QQuickTest::pointerFlick(device, window.data(), 0, flickFrom, flickToWithSnapBack, 10);
QQuickTest::pointerFlick(device, window.data(), 0, flickFrom, flickToWithSnapBack, 10, testButton);
QTRY_VERIFY(flickable->isMoving());
QCOMPARE(flickable->isMovingHorizontally(), horizontalEnabled);