macOS: Teach QWheelEvent to handle a new ScrollMomentum phase

We detect if there's an upcoming momentum phase using the same trick
used by e.g. Mozilla in their event handling: https://tinyurl.com/yd8lcs4l,
and as recommended by an Apple engineer: https://tinyurl.com/y8yytlgv

The event is not guaranteed to be in the queue, but in practice it seems
to be. If this assumption fails we can add a wait timeout to the event
search instead of using [NSDate distantPast] as a timeout (which only
looks at queued events).

When the momentum phase is detected, QWheelEvent::phase will have the
new ScrollMomentum value, and the phase transitions will be
ScrollBegin -> ScrollUpdate -> ScrollMomentum -> ScrollEnd.

We no longer send ScrollEnd to signify that the user's fingers have
been lifted off the trackpad; rather, the first event with ScrollMomentum
phase means that the fingers have been lifted and macOS is now sending
simulated-momentum events.

This means ScrollEnd is a reliable indicator that the entire scroll
gesture (both the user interaction and the momentum) has ended.

If the ScrollMomentum phase is skipped, it means the user's fingers
came to rest before being lifted, so there is no momentum. In that case
the transitions will be ScrollBegin -> ScrollUpdate -> ScrollEnd.

Task-number: QTBUG-63026
Task-number: QTBUG-65160
Change-Id: I80191a472f6fa892387004c199166a6350124274
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
This commit is contained in:
Tor Arne Vestbø 2018-08-17 13:30:38 +02:00 committed by Shawn Rutledge
parent a4a730f4cb
commit e09f5b1786
4 changed files with 51 additions and 31 deletions

View File

@ -1704,7 +1704,8 @@ public:
NoScrollPhase = 0,
ScrollBegin,
ScrollUpdate,
ScrollEnd
ScrollEnd,
ScrollMomentum
};
enum MouseEventSource {

View File

@ -783,7 +783,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::NoScrollPhase), src(Qt::MouseEventNotSynthesized), invertedScrolling(false)
src(Qt::MouseEventNotSynthesized), invertedScrolling(false), ph(Qt::NoScrollPhase)
{
g = QCursor::pos();
if (orient == Qt::Vertical)
@ -818,7 +818,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::NoScrollPhase), src(Qt::MouseEventNotSynthesized), invertedScrolling(false)
src(Qt::MouseEventNotSynthesized), invertedScrolling(false), ph(Qt::NoScrollPhase)
{
if (orient == Qt::Vertical)
angleD = QPoint(0, delta);
@ -959,8 +959,8 @@ QWheelEvent::QWheelEvent(const QPointF &pos, const QPointF& globalPos,
QPoint pixelDelta, QPoint angleDelta, int qt4Delta, Qt::Orientation qt4Orientation,
Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers, Qt::ScrollPhase phase, Qt::MouseEventSource source, bool inverted)
: QInputEvent(Wheel, modifiers), p(pos), g(globalPos), pixelD(pixelDelta),
angleD(angleDelta), qt4D(qt4Delta), qt4O(qt4Orientation), mouseState(buttons), ph(phase), src(source),
invertedScrolling(inverted)
angleD(angleDelta), qt4D(qt4Delta), qt4O(qt4Orientation), mouseState(buttons), src(source),
invertedScrolling(inverted), ph(phase)
{}
/*!
@ -997,7 +997,7 @@ QWheelEvent::QWheelEvent(QPointF pos, QPointF globalPos, QPoint pixelDelta, QPoi
bool inverted, Qt::MouseEventSource source)
: QInputEvent(Wheel, modifiers), p(pos), g(globalPos), pixelD(pixelDelta), angleD(angleDelta),
qt4O(qAbs(angleDelta.x()) > qAbs(angleDelta.y()) ? Qt::Horizontal : Qt::Vertical),
mouseState(buttons), ph(phase), src(source), invertedScrolling(inverted)
mouseState(buttons), src(source), invertedScrolling(inverted), ph(phase)
{
qt4D = (qt4O == Qt::Horizontal ? angleDelta.x() : angleDelta.y());
}

View File

@ -232,10 +232,11 @@ protected:
int qt4D = 0;
Qt::Orientation qt4O = Qt::Vertical;
Qt::MouseButtons mouseState;
uint ph : 2;
uint _unused_ : 2; // Kept for binary compatibility
uint src: 2;
bool invertedScrolling : 1;
int reserved : 27;
uint ph : 3;
int reserved : 24;
friend class QApplication;
};

View File

@ -566,6 +566,42 @@
NSTimeInterval timestamp = [theEvent timestamp];
ulong qt_timestamp = timestamp * 1000;
Qt::ScrollPhase phase = Qt::NoScrollPhase;
if (theEvent.phase == NSEventPhaseMayBegin || theEvent.phase == NSEventPhaseBegan) {
// MayBegin is likely to happen. We treat it the same as an actual begin,
// and follow it with an update when the actual begin is delivered.
phase = m_scrolling ? Qt::ScrollUpdate : Qt::ScrollBegin;
m_scrolling = true;
} else if (theEvent.phase == NSEventPhaseStationary || theEvent.phase == NSEventPhaseChanged) {
phase = Qt::ScrollUpdate;
} else if (theEvent.phase == NSEventPhaseEnded) {
// A scroll event phase may be followed by a momentum phase after the user releases
// the finger, and in that case we don't want to send a Qt::ScrollEnd until after
// the momentum phase has ended. Unfortunately there isn't any guaranteed way of
// knowing whether or not a NSEventPhaseEnded will be followed by a momentum phase.
// The best we can do is to look at the event queue and hope that the system has
// had time to emit a momentum phase event.
if ([NSApp nextEventMatchingMask:NSScrollWheelMask untilDate:[NSDate distantPast]
inMode:@"QtMomementumEventSearchMode" dequeue:NO].momentumPhase == NSEventPhaseBegan) {
Q_ASSERT(pixelDelta.isNull() && angleDelta.isNull());
return; // Ignore this event, as it has a delta of 0,0
}
phase = Qt::ScrollEnd;
m_scrolling = false;
} else if (theEvent.momentumPhase == NSEventPhaseBegan) {
Q_ASSERT(!pixelDelta.isNull() && !angleDelta.isNull());
phase = Qt::ScrollUpdate; // Send as update, it has a delta
} else if (theEvent.momentumPhase == NSEventPhaseChanged) {
phase = Qt::ScrollMomentum;
} else if (theEvent.phase == NSEventPhaseCancelled
|| theEvent.momentumPhase == NSEventPhaseEnded
|| theEvent.momentumPhase == NSEventPhaseCancelled) {
phase = Qt::ScrollEnd;
m_scrolling = false;
} else {
Q_ASSERT(theEvent.momentumPhase != NSEventPhaseStationary);
}
// Prevent keyboard modifier state from changing during scroll event streams.
// A two-finger trackpad flick generates a stream of scroll events. We want
// the keyboard modifier state to be the state at the beginning of the
@ -573,34 +609,16 @@
// mid-stream. One example of this happening would be when pressing cmd
// after scrolling in Qt Creator: not taking the phase into account causes
// the end of the event stream to be interpreted as font size changes.
NSEventPhase momentumPhase = [theEvent momentumPhase];
if (momentumPhase == NSEventPhaseNone)
if (theEvent.momentumPhase == NSEventPhaseNone)
m_currentWheelModifiers = [QNSView convertKeyModifiers:[theEvent modifierFlags]];
NSEventPhase phase = [theEvent phase];
Qt::ScrollPhase ph = Qt::ScrollUpdate;
// MayBegin is likely to happen. We treat it the same as an actual begin.
if (phase == NSEventPhaseMayBegin) {
m_scrolling = true;
ph = Qt::ScrollBegin;
} else if (phase == NSEventPhaseBegan) {
// If MayBegin did not happen, Began is the actual beginning.
if (!m_scrolling)
ph = Qt::ScrollBegin;
m_scrolling = true;
} else if (phase == NSEventPhaseEnded || phase == NSEventPhaseCancelled ||
momentumPhase == NSEventPhaseEnded || momentumPhase == NSEventPhaseCancelled) {
ph = Qt::ScrollEnd;
m_scrolling = false;
} else if (phase == NSEventPhaseNone && momentumPhase == NSEventPhaseNone) {
ph = Qt::NoScrollPhase;
}
// "isInverted": natural OS X scrolling, inverted from the Qt/other platform/Jens perspective.
bool isInverted = [theEvent isDirectionInvertedFromDevice];
qCDebug(lcQpaMouse) << "scroll wheel @ window pos" << qt_windowPoint << "delta px" << pixelDelta << "angle" << angleDelta << "phase" << ph << (isInverted ? "inverted" : "");
QWindowSystemInterface::handleWheelEvent(m_platformWindow->window(), qt_timestamp, qt_windowPoint, qt_screenPoint, pixelDelta, angleDelta, m_currentWheelModifiers, ph, source, isInverted);
qCDebug(lcQpaMouse) << "scroll wheel @ window pos" << qt_windowPoint << "delta px" << pixelDelta
<< "angle" << angleDelta << "phase" << phase << (isInverted ? "inverted" : "");
QWindowSystemInterface::handleWheelEvent(m_platformWindow->window(), qt_timestamp, qt_windowPoint,
qt_screenPoint, pixelDelta, angleDelta, m_currentWheelModifiers, phase, source, isInverted);
}
#endif // QT_CONFIG(wheelevent)