Track target widget when wheel events are received

This issue is reproducible on OS X when using a Magic Mouse
or a combination of Magic Trackpad and regular mouse. In these
cases it's possible to start a scrolling gesture on one widget
and move the mouse cursor over another widget.

Although we send the wheel event phase information, we never
made any use of it. This means that a widget would start
scrolling even though it never received a ScrollBegin event.

In this patch, we make sure the scrolling cycle is respected
and that once a widget starts scrolling, it'll be recieving
all the wheel events until a ScrollEnd event reaches the
application.

For those input devices not supporting a proper phase cycle,
we introduce a new (undocumented) phase value, NoScrollPhase.
If the wheel event phase is NoScrollPhase, then we ignore
the current scroll widget and proceed as usual. This value
is the default for wheel events. It's up to the platform
plugin to set the proper phase value according to the data
received from the OS.

Finally, we fix a few of QWheelEvent constructors to properly
initialize the phase and source properties.

Task-number: QTBUG-50199
Change-Id: I3773729a9c757e2d2fcc5100dcd79f0ed26cb808
Reviewed-by: Shawn Rutledge <shawn.rutledge@theqtcompany.com>
This commit is contained in:
Gabriel de Dietrich 2016-02-10 11:30:33 -08:00 committed by Shawn Rutledge
parent a17a7d37c2
commit f253f4c331
9 changed files with 74 additions and 24 deletions

View File

@ -1626,7 +1626,8 @@ public:
};
enum ScrollPhase {
ScrollBegin = 1,
NoScrollPhase = 0, // Make public in 5.7 or asap
ScrollBegin,
ScrollUpdate,
ScrollEnd
};

View File

@ -3070,6 +3070,8 @@
This enum describes the phase of scrolling.
\omitvalue NoScrollPhase The input device doesn't support scroll phase.
\value ScrollBegin Scrolling is about to begin, but the scrolling
distance did not yet change.

View File

@ -727,7 +727,7 @@ QWheelEvent::QWheelEvent(const QPointF &pos, int delta,
Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers,
Qt::Orientation orient)
: QInputEvent(Wheel, modifiers), p(pos), qt4D(delta), qt4O(orient), mouseState(buttons),
ph(Qt::ScrollUpdate), src(Qt::MouseEventNotSynthesized)
ph(Qt::NoScrollPhase), src(Qt::MouseEventNotSynthesized)
{
g = QCursor::pos();
if (orient == Qt::Vertical)
@ -762,7 +762,7 @@ QWheelEvent::QWheelEvent(const QPointF &pos, const QPointF& globalPos, int delta
Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers,
Qt::Orientation orient)
: QInputEvent(Wheel, modifiers), p(pos), g(globalPos), qt4D(delta), qt4O(orient), mouseState(buttons),
ph(Qt::ScrollUpdate), src(Qt::MouseEventNotSynthesized)
ph(Qt::NoScrollPhase), src(Qt::MouseEventNotSynthesized)
{
if (orient == Qt::Vertical)
angleD = QPoint(0, delta);
@ -798,7 +798,7 @@ QWheelEvent::QWheelEvent(const QPointF &pos, const QPointF& globalPos,
QPoint pixelDelta, QPoint angleDelta, int qt4Delta, Qt::Orientation qt4Orientation,
Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers)
: QInputEvent(Wheel, modifiers), p(pos), g(globalPos), pixelD(pixelDelta),
angleD(angleDelta), qt4D(qt4Delta), qt4O(qt4Orientation), mouseState(buttons), ph(Qt::ScrollUpdate),
angleD(angleDelta), qt4D(qt4Delta), qt4O(qt4Orientation), mouseState(buttons), ph(Qt::NoScrollPhase),
src(Qt::MouseEventNotSynthesized)
{}

View File

@ -219,6 +219,8 @@ protected:
uint ph : 2;
uint src: 2;
int reserved : 28;
friend class QApplication;
};
#endif

View File

@ -94,8 +94,16 @@ public:
quint32 nativeModifiers,
const QString& text = QString(), bool autorep = false,
ushort count = 1, bool tryShortcutOverride = true);
static void handleWheelEvent(QWindow *w, const QPointF & local, const QPointF & global, QPoint pixelDelta, QPoint angleDelta, Qt::KeyboardModifiers mods = Qt::NoModifier, Qt::ScrollPhase phase = Qt::ScrollUpdate, Qt::MouseEventSource source = Qt::MouseEventNotSynthesized);
static void handleWheelEvent(QWindow *w, ulong timestamp, const QPointF & local, const QPointF & global, QPoint pixelDelta, QPoint angleDelta, Qt::KeyboardModifiers mods = Qt::NoModifier, Qt::ScrollPhase phase = Qt::ScrollUpdate, Qt::MouseEventSource source = Qt::MouseEventNotSynthesized);
static void handleWheelEvent(QWindow *w, const QPointF & local, const QPointF & global,
QPoint pixelDelta, QPoint angleDelta,
Qt::KeyboardModifiers mods = Qt::NoModifier,
Qt::ScrollPhase phase = Qt::NoScrollPhase,
Qt::MouseEventSource source = Qt::MouseEventNotSynthesized);
static void handleWheelEvent(QWindow *w, ulong timestamp, const QPointF & local, const QPointF & global,
QPoint pixelDelta, QPoint angleDelta,
Qt::KeyboardModifiers mods = Qt::NoModifier,
Qt::ScrollPhase phase = Qt::NoScrollPhase,
Qt::MouseEventSource source = Qt::MouseEventNotSynthesized);
// Wheel event compatibility functions. Will be removed: do not use.
static void handleWheelEvent(QWindow *w, const QPointF & local, const QPointF & global, int d, Qt::Orientation o, Qt::KeyboardModifiers mods = Qt::NoModifier);

View File

@ -236,7 +236,7 @@ public:
class WheelEvent : public InputEvent {
public:
WheelEvent(QWindow *w, ulong time, const QPointF & local, const QPointF & global, QPoint pixelD, QPoint angleD, int qt4D, Qt::Orientation qt4O,
Qt::KeyboardModifiers mods, Qt::ScrollPhase phase = Qt::ScrollUpdate, Qt::MouseEventSource src = Qt::MouseEventNotSynthesized)
Qt::KeyboardModifiers mods, Qt::ScrollPhase phase = Qt::NoScrollPhase, Qt::MouseEventSource src = Qt::MouseEventNotSynthesized)
: InputEvent(w, time, Wheel, mods), pixelDelta(pixelD), angleDelta(angleD), qt4Delta(qt4D), qt4Orientation(qt4O), localPos(local), globalPos(global), phase(phase), source(src) { }
QPoint pixelDelta;
QPoint angleDelta;

View File

@ -1405,6 +1405,8 @@ static QTabletEvent::TabletDevice wacomTabletDevice(NSEvent *theEvent)
momentumPhase == NSEventPhaseEnded || momentumPhase == NSEventPhaseCancelled) {
ph = Qt::ScrollEnd;
m_scrolling = false;
} else if (phase == NSEventPhaseNone && momentumPhase == NSEventPhaseNone) {
ph = Qt::NoScrollPhase;
}
QWindowSystemInterface::handleWheelEvent(m_window, qt_timestamp, qt_windowPoint, qt_screenPoint, pixelDelta, angleDelta, currentWheelModifiers, ph, source);

View File

@ -417,6 +417,7 @@ QWidget *QApplicationPrivate::hidden_focus_widget = 0; // will get keyboard inpu
QWidget *QApplicationPrivate::active_window = 0; // toplevel with keyboard focus
#ifndef QT_NO_WHEELEVENT
int QApplicationPrivate::wheel_scroll_lines; // number of lines to scroll
QWidget *QApplicationPrivate::wheel_widget = Q_NULLPTR;
#endif
bool qt_in_tab_key_event = false;
int qt_antialiasing_threshold = -1;
@ -3309,7 +3310,6 @@ bool QApplication::notify(QObject *receiver, QEvent *e)
case QEvent::Wheel:
{
QWidget* w = static_cast<QWidget *>(receiver);
QWheelEvent* wheel = static_cast<QWheelEvent*>(e);
// QTBUG-40656, QTBUG-42731: ignore wheel events when a popup (QComboBox) is open.
if (const QWidget *popup = QApplication::activePopupWidget()) {
@ -3317,27 +3317,61 @@ bool QApplication::notify(QObject *receiver, QEvent *e)
return true;
}
QPoint relpos = wheel->pos();
bool eventAccepted = wheel->isAccepted();
QWheelEvent* wheel = static_cast<QWheelEvent*>(e);
const bool spontaneous = wheel->spontaneous();
const Qt::ScrollPhase phase = wheel->phase();
if (e->spontaneous() && wheel->phase() == Qt::ScrollUpdate)
if (phase == Qt::NoScrollPhase || phase == Qt::ScrollBegin
|| (phase == Qt::ScrollUpdate && !QApplicationPrivate::wheel_widget)) {
if (spontaneous && phase == Qt::ScrollBegin)
QApplicationPrivate::wheel_widget = Q_NULLPTR;
const QPoint &relpos = wheel->pos();
if (spontaneous && (phase == Qt::NoScrollPhase || phase == Qt::ScrollUpdate))
QApplicationPrivate::giveFocusAccordingToFocusPolicy(w, e, relpos);
while (w) {
QWheelEvent we(relpos, wheel->globalPos(), wheel->pixelDelta(), wheel->angleDelta(), wheel->delta(), wheel->orientation(), wheel->buttons(),
wheel->modifiers(), wheel->phase(), wheel->source());
we.spont = wheel->spontaneous();
res = d->notify_helper(w, w == receiver ? wheel : &we);
eventAccepted = ((w == receiver) ? wheel : &we)->isAccepted();
e->spont = false;
if ((res && eventAccepted)
|| w->isWindow() || w->testAttribute(Qt::WA_NoMousePropagation))
wheel->modifiers(), phase, wheel->source());
bool eventAccepted;
while (w) {
we.spont = spontaneous && w == receiver;
we.ignore();
res = d->notify_helper(w, &we);
eventAccepted = we.isAccepted();
if (res && eventAccepted) {
if (spontaneous && phase != Qt::NoScrollPhase)
QApplicationPrivate::wheel_widget = w;
break;
}
if (w->isWindow() || w->testAttribute(Qt::WA_NoMousePropagation))
break;
relpos += w->pos();
we.p += w->pos();
w = w->parentWidget();
}
wheel->setAccepted(eventAccepted);
} else if (QApplicationPrivate::wheel_widget) {
if (!spontaneous) {
// wheel_widget may forward the wheel event to a delegate widget,
// either directly or indirectly (e.g. QAbstractScrollArea will
// forward to its QScrollBars through viewportEvent()). In that
// case, the event will not be spontaneous but synthesized, so
// we can send it straigth to the receiver.
d->notify_helper(w, wheel);
} else {
const QPoint &relpos = QApplicationPrivate::wheel_widget->mapFromGlobal(wheel->globalPos());
QWheelEvent we(relpos, wheel->globalPos(), wheel->pixelDelta(), wheel->angleDelta(), wheel->delta(), wheel->orientation(), wheel->buttons(),
wheel->modifiers(), wheel->phase(), wheel->source());
we.spont = true;
we.ignore();
d->notify_helper(QApplicationPrivate::wheel_widget, &we);
wheel->setAccepted(we.isAccepted());
if (phase == Qt::ScrollEnd)
QApplicationPrivate::wheel_widget = Q_NULLPTR;
}
}
}
break;
#endif

View File

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