Wheel event widget: Harden logic an extra bit

This is quite an unlikely scenario, but not impossible.

It could be that the wheel widget is destroyed during
an update phase event. In that case, wheel_widget would
be a dangling pointer for any subsequent wheel event.
We protect against this with a QPointer.

However, that would mean that if the next wheel event
were to be an end phase event, that event would be lost.
So we go through the usual code path, except that we won't
set wheel_widget in the case of an end phase event.

Change-Id: I59a912b845dcc249e1edc60b4dc28bf308d807d9
Reviewed-by: Shawn Rutledge <shawn.rutledge@theqtcompany.com>
This commit is contained in:
Gabriel de Dietrich 2016-03-03 13:51:19 -08:00
parent 2020d2cb63
commit 85a57f7a2e
2 changed files with 52 additions and 24 deletions

View File

@ -417,7 +417,7 @@ QWidget *QApplicationPrivate::hidden_focus_widget = 0; // will get keyboard inpu
QWidget *QApplicationPrivate::active_window = 0; // toplevel with keyboard focus QWidget *QApplicationPrivate::active_window = 0; // toplevel with keyboard focus
#ifndef QT_NO_WHEELEVENT #ifndef QT_NO_WHEELEVENT
int QApplicationPrivate::wheel_scroll_lines; // number of lines to scroll int QApplicationPrivate::wheel_scroll_lines; // number of lines to scroll
QWidget *QApplicationPrivate::wheel_widget = Q_NULLPTR; QPointer<QWidget> QApplicationPrivate::wheel_widget;
#endif #endif
bool qt_in_tab_key_event = false; bool qt_in_tab_key_event = false;
int qt_antialiasing_threshold = -1; int qt_antialiasing_threshold = -1;
@ -3326,9 +3326,32 @@ bool QApplication::notify(QObject *receiver, QEvent *e)
const bool spontaneous = wheel->spontaneous(); const bool spontaneous = wheel->spontaneous();
const Qt::ScrollPhase phase = wheel->phase(); const Qt::ScrollPhase phase = wheel->phase();
if (phase == Qt::NoScrollPhase || phase == Qt::ScrollBegin // Ideally, we should lock on a widget when it starts receiving wheel
|| (phase == Qt::ScrollUpdate && !QApplicationPrivate::wheel_widget)) { // events. This avoids other widgets to start receiving those events
// as the mouse cursor hovers them. However, given the way common
// wheeled mice work, there's no certain way of connecting different
// wheel events as a stream. This results in the NoScrollPhase case,
// where we just send the event from the original receiver and up its
// hierarchy until the event gets accepted.
//
// In the case of more evolved input devices, like Apple's trackpad or
// Magic Mouse, we receive the scroll phase information. This helps us
// connect wheel events as a stream and therefore makes it easier to
// lock on the widget onto which the scrolling was initiated.
//
// We assume that, when supported, the phase cycle follows the pattern:
//
// ScrollBegin (ScrollUpdate* ScrollEnd)+
//
// This means that we can have scrolling sequences (starting with ScrollBegin)
// or partial sequences (after a ScrollEnd and starting with ScrollUpdate).
// If wheel_widget is null because it was deleted, we also take the same
// code path as an initial sequence.
if (phase == Qt::NoScrollPhase || phase == Qt::ScrollBegin || !QApplicationPrivate::wheel_widget) {
// A system-generated ScrollBegin event starts a new user scrolling
// sequence, so we reset wheel_widget in case no one accepts the event
// or if we didn't get (or missed) a ScrollEnd previously.
if (spontaneous && phase == Qt::ScrollBegin) if (spontaneous && phase == Qt::ScrollBegin)
QApplicationPrivate::wheel_widget = Q_NULLPTR; QApplicationPrivate::wheel_widget = Q_NULLPTR;
@ -3346,7 +3369,10 @@ bool QApplication::notify(QObject *receiver, QEvent *e)
res = d->notify_helper(w, &we); res = d->notify_helper(w, &we);
eventAccepted = we.isAccepted(); eventAccepted = we.isAccepted();
if (res && eventAccepted) { if (res && eventAccepted) {
if (spontaneous && phase != Qt::NoScrollPhase && QGuiApplicationPrivate::scrollNoPhaseAllowed) // 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) && QGuiApplicationPrivate::scrollNoPhaseAllowed)
QApplicationPrivate::wheel_widget = w; QApplicationPrivate::wheel_widget = w;
break; break;
} }
@ -3357,25 +3383,27 @@ bool QApplication::notify(QObject *receiver, QEvent *e)
w = w->parentWidget(); w = w->parentWidget();
} }
wheel->setAccepted(eventAccepted); wheel->setAccepted(eventAccepted);
} else if (QApplicationPrivate::wheel_widget) { } else if (!spontaneous) {
if (!spontaneous) { // wheel_widget may forward the wheel event to a delegate widget,
// wheel_widget may forward the wheel event to a delegate widget, // either directly or indirectly (e.g. QAbstractScrollArea will
// either directly or indirectly (e.g. QAbstractScrollArea will // forward to its QScrollBars through viewportEvent()). In that
// forward to its QScrollBars through viewportEvent()). In that // case, the event will not be spontaneous but synthesized, so
// case, the event will not be spontaneous but synthesized, so // we can send it straight to the receiver.
// we can send it straigth to the receiver. d->notify_helper(w, wheel);
d->notify_helper(w, wheel); } else {
} else { // The phase is either ScrollUpdate or ScrollEnd, and wheel_widget
const QPoint &relpos = QApplicationPrivate::wheel_widget->mapFromGlobal(wheel->globalPos()); // is set. Since it accepted the wheel event previously, we continue
QWheelEvent we(relpos, wheel->globalPos(), wheel->pixelDelta(), wheel->angleDelta(), wheel->delta(), wheel->orientation(), wheel->buttons(), // sending those events until we get a ScrollEnd, which signifies
wheel->modifiers(), wheel->phase(), wheel->source()); // the end of the natural scrolling sequence.
we.spont = true; const QPoint &relpos = QApplicationPrivate::wheel_widget->mapFromGlobal(wheel->globalPos());
we.ignore(); QWheelEvent we(relpos, wheel->globalPos(), wheel->pixelDelta(), wheel->angleDelta(), wheel->delta(), wheel->orientation(), wheel->buttons(),
d->notify_helper(QApplicationPrivate::wheel_widget, &we); wheel->modifiers(), wheel->phase(), wheel->source());
wheel->setAccepted(we.isAccepted()); we.spont = true;
if (phase == Qt::ScrollEnd) we.ignore();
QApplicationPrivate::wheel_widget = Q_NULLPTR; d->notify_helper(QApplicationPrivate::wheel_widget, &we);
} wheel->setAccepted(we.isAccepted());
if (phase == Qt::ScrollEnd)
QApplicationPrivate::wheel_widget = Q_NULLPTR;
} }
} }
break; break;

View File

@ -202,7 +202,7 @@ public:
static QWidget *active_window; static QWidget *active_window;
#ifndef QT_NO_WHEELEVENT #ifndef QT_NO_WHEELEVENT
static int wheel_scroll_lines; static int wheel_scroll_lines;
static QWidget *wheel_widget; static QPointer<QWidget> wheel_widget;
#endif #endif
static int enabledAnimations; // Combination of QPlatformTheme::UiEffect static int enabledAnimations; // Combination of QPlatformTheme::UiEffect