QApplication: deliver all wheel events to widget that accepts the first
For kinetic wheel events, Qt tries to make sure that all events in the stream go to the widget that accepted the first wheel event. It did so by directing all events from the stream to the widget from which the spontaneous event was returned as accepted. However, that widget might have passed the event on to some other widgets; e.g QScrollArea forwards wheel events from the viewport to the relevant scroll bar. The event might then have come back accepted only because parent propagation kicked in (the scrollbar might not accept the event, so the parents get a chance, and some parent's scrollbar ultimately accepts the event). In this scenario, the wheel widget would be the viewport under the mouse, when it should have been the scrollbar of the parent. The next events from the stream were then delivered to a widget that didn't scroll; and parent propagation is not (and should not be) implemented for the case where Qt has a wheel widget. Instead, make the first widget that accepts any initial wheel event the wheel widget, even if the event was not spontaneous. With this change, all events from the stream are delivered to the widget that actually handled the event. That has the effect that ie. a viewport of a scroll area only gets the first event; all following events are delivered directly to the scrollbar. The test case added simulates the different scenarios - nesting of scroll areas, classic wheel events and a stream of kinetic wheel events. [ChangeLog][QtWidgets][QApplication] Wheel events from a device that creates an event stream are correctly delivered to the widget that accepts the first wheel event in the stream. Change-Id: I5ebfc7789b5c32ebc8d881686f450fa05ec92cfe Fixes: QTBUG-79102 Pick-to: 5.15 Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io> Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
This commit is contained in:
parent
f6bd056803
commit
bc205d81e7
@ -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.
|
||||
|
@ -50,6 +50,8 @@
|
||||
#include <QtWidgets/QLineEdit>
|
||||
#include <QtWidgets/QLabel>
|
||||
#include <QtWidgets/QMainWindow>
|
||||
#include <QtWidgets/QScrollArea>
|
||||
#include <QtWidgets/QScrollBar>
|
||||
#include <QtWidgets/private/qapplication_p.h>
|
||||
#include <QtWidgets/QStyle>
|
||||
|
||||
@ -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<Qt::ScrollPhase>;
|
||||
|
||||
void tst_QApplication::wheelEventPropagation_data()
|
||||
{
|
||||
qRegisterMetaType<PhaseList>();
|
||||
|
||||
QTest::addColumn<bool>("innerScrolls");
|
||||
QTest::addColumn<PhaseList>("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)
|
||||
|
Loading…
Reference in New Issue
Block a user