diff --git a/src/widgets/kernel/qapplication.cpp b/src/widgets/kernel/qapplication.cpp index d0d2ab22ce4..1567f7173b3 100644 --- a/src/widgets/kernel/qapplication.cpp +++ b/src/widgets/kernel/qapplication.cpp @@ -3010,7 +3010,7 @@ bool QApplication::notify(QObject *receiver, QEvent *e) // // We assume that, when supported, the phase cycle follows the pattern: // - // ScrollBegin (ScrollUpdate* ScrollEnd)+ + // ScrollBegin (ScrollUpdate* ScrollMomentum* ScrollEnd)+ // // This means that we can have scrolling sequences (starting with ScrollBegin) // or partial sequences (after a ScrollEnd and starting with ScrollUpdate). @@ -3024,7 +3024,7 @@ bool QApplication::notify(QObject *receiver, QEvent *e) if (spontaneous && phase == Qt::ScrollBegin) QApplicationPrivate::wheel_widget = nullptr; - QPoint relpos = wheel->position().toPoint(); + const QPoint relpos = wheel->position().toPoint(); if (spontaneous && (phase == Qt::NoScrollPhase || phase == Qt::ScrollUpdate)) QApplicationPrivate::giveFocusAccordingToFocusPolicy(w, e, relpos); @@ -3050,7 +3050,7 @@ QT_WARNING_POP // A new scrolling sequence or partial sequence starts and w has accepted // the event. Therefore, we can set wheel_widget, but only if it's not // the end of a sequence. - if (spontaneous && (phase == Qt::ScrollBegin || phase == Qt::ScrollUpdate)) + if (QApplicationPrivate::wheel_widget == nullptr && (phase == Qt::ScrollBegin || phase == Qt::ScrollUpdate)) QApplicationPrivate::wheel_widget = w; break; } @@ -3069,7 +3069,7 @@ QT_WARNING_POP // we can send it straight to the receiver. d->notify_helper(w, wheel); } else { - // The phase is either ScrollUpdate or ScrollEnd, and wheel_widget + // The phase is either ScrollUpdate, ScrollMomentum, or ScrollEnd, and wheel_widget // is set. Since it accepted the wheel event previously, we continue // sending those events until we get a ScrollEnd, which signifies // the end of the natural scrolling sequence. diff --git a/tests/auto/widgets/kernel/qapplication/tst_qapplication.cpp b/tests/auto/widgets/kernel/qapplication/tst_qapplication.cpp index 245e107170a..e341e0b7563 100644 --- a/tests/auto/widgets/kernel/qapplication/tst_qapplication.cpp +++ b/tests/auto/widgets/kernel/qapplication/tst_qapplication.cpp @@ -50,6 +50,8 @@ #include #include #include +#include +#include #include #include @@ -132,6 +134,8 @@ private slots: void setAttribute(); void touchEventPropagation(); + void wheelEventPropagation_data(); + void wheelEventPropagation(); void qtbug_12673(); void noQuitOnHide(); @@ -2118,6 +2122,125 @@ void tst_QApplication::touchEventPropagation() } } +/*! + Test that wheel events are propagated correctly. + + The event propagation of wheel events is complex: generally, they are propagated + up the parent tree like other input events, until a widget accepts the event. However, + wheel events are ignored by default (unlike mouse events, which are accepted by default, + and ignored in the default implementation of the event handler of QWidget). + + And Qt tries to make sure that wheel events that "belong together" are going to the same + widget. However, for low-precision events as generated by an old-fashioned + mouse wheel, each event is a distinct event, so Qt has no choice than to deliver the event + to the widget under the mouse. + High-precision events, as generated by track pads or other kinetic scrolling devices, come + in a continuous stream, with different phases. Qt tries to make sure that all events in the + same stream go to the widget that accepted the first event. + + Also, QAbstractScrollArea forwards wheel events from the viewport to the relevant scrollbar, + which adds more complexity to the handling. + + This tests two scenarios: + 1) a large widget inside a scrollarea that scrolls, inside a scrollarea that also scrolls + 2) a large widget inside a scrollarea that doesn't scroll, within a scrollarea that does + + For scenario 1 "inner", the expectation is that the inner scrollarea handles all wheel + events. + For scenario 2 "outer", the expectation is that the outer scrollarea handles all wheel + events. +*/ +using PhaseList = QList; + +void tst_QApplication::wheelEventPropagation_data() +{ + qRegisterMetaType(); + + QTest::addColumn("innerScrolls"); + QTest::addColumn("phases"); + + QTest::addRow("inner, classic") + << true + << PhaseList{Qt::NoScrollPhase, Qt::NoScrollPhase, Qt::NoScrollPhase}; + QTest::addRow("outer, classic") + << false + << PhaseList{Qt::NoScrollPhase, Qt::NoScrollPhase, Qt::NoScrollPhase}; + QTest::addRow("inner, kinetic") + << true + << PhaseList{Qt::ScrollBegin, Qt::ScrollUpdate, Qt::ScrollMomentum, Qt::ScrollEnd}; + QTest::addRow("outer, kinetic") + << false + << PhaseList{Qt::ScrollBegin, Qt::ScrollUpdate, Qt::ScrollMomentum, Qt::ScrollEnd}; +} + +void tst_QApplication::wheelEventPropagation() +{ + QFETCH(bool, innerScrolls); + QFETCH(PhaseList, phases); + + const QSize baseSize(500, 500); + const QPointF center(baseSize.width() / 2, baseSize.height() / 2); + int scrollStep = 50; + + int argc = 1; + QApplication app(argc, &argv0); + + QScrollArea outerArea; + outerArea.setObjectName("outerArea"); + outerArea.viewport()->setObjectName("outerArea_viewport"); + QScrollArea innerArea; + innerArea.setObjectName("innerArea"); + innerArea.viewport()->setObjectName("innerArea_viewport"); + QWidget largeWidget; + largeWidget.setObjectName("largeWidget"); + QScrollBar trap(Qt::Vertical, &largeWidget); + trap.setObjectName("It's a trap!"); + + largeWidget.setFixedSize(baseSize * 8); + + // classic wheel events will be grabbed by the widget under the mouse, so don't place a trap + if (phases.at(0) == Qt::NoScrollPhase) + trap.hide(); + // kinetic wheel events should all go to the first widget; place a trap + else + trap.setGeometry(center.x() - 50, center.y() + scrollStep, 100, baseSize.height()); + + // if the inner area is large enough to host the widget, then it won't scroll + innerArea.setWidget(&largeWidget); + innerArea.setFixedSize(innerScrolls ? baseSize * 4 + : largeWidget.minimumSize() + QSize(100, 100)); + // the outer area always scrolls + outerArea.setFixedSize(baseSize); + outerArea.setWidget(&innerArea); + outerArea.show(); + + if (!QTest::qWaitForWindowExposed(&outerArea)) + QSKIP("Window failed to show, can't run test"); + + auto innerVBar = innerArea.verticalScrollBar(); + innerVBar->setObjectName("innerArea_vbar"); + QCOMPARE(innerVBar->isVisible(), innerScrolls); + auto outerVBar = outerArea.verticalScrollBar(); + outerVBar->setObjectName("outerArea_vbar"); + QVERIFY(outerVBar->isVisible()); + + const QPointF global(outerArea.mapToGlobal(center.toPoint())); + + QSignalSpy innerSpy(innerVBar, &QAbstractSlider::valueChanged); + QSignalSpy outerSpy(outerVBar, &QAbstractSlider::valueChanged); + + int count = 0; + for (const auto &phase : qAsConst(phases)) { + QWindowSystemInterface::handleWheelEvent(outerArea.windowHandle(), center, global, + QPoint(0, -scrollStep), QPoint(0, -120), Qt::NoModifier, + phase); + ++count; + QCoreApplication::processEvents(); + QCOMPARE(innerSpy.count(), innerScrolls ? count : 0); + QCOMPARE(outerSpy.count(), innerScrolls ? 0 : count); + } +} + void tst_QApplication::qtbug_12673() { #if QT_CONFIG(process)