From e09f5b17865a09dac41d0f30ef2ea238f38873eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Fri, 17 Aug 2018 13:30:38 +0200 Subject: [PATCH] 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 --- src/corelib/global/qnamespace.h | 3 +- src/gui/kernel/qevent.cpp | 10 +-- src/gui/kernel/qevent.h | 5 +- src/plugins/platforms/cocoa/qnsview_mouse.mm | 64 +++++++++++++------- 4 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/corelib/global/qnamespace.h b/src/corelib/global/qnamespace.h index 288c966eec..dec2c44637 100644 --- a/src/corelib/global/qnamespace.h +++ b/src/corelib/global/qnamespace.h @@ -1704,7 +1704,8 @@ public: NoScrollPhase = 0, ScrollBegin, ScrollUpdate, - ScrollEnd + ScrollEnd, + ScrollMomentum }; enum MouseEventSource { diff --git a/src/gui/kernel/qevent.cpp b/src/gui/kernel/qevent.cpp index 49f1ba3048..f5527354a2 100644 --- a/src/gui/kernel/qevent.cpp +++ b/src/gui/kernel/qevent.cpp @@ -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()); } diff --git a/src/gui/kernel/qevent.h b/src/gui/kernel/qevent.h index 8d2f225809..2b1c6a6e31 100644 --- a/src/gui/kernel/qevent.h +++ b/src/gui/kernel/qevent.h @@ -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; }; diff --git a/src/plugins/platforms/cocoa/qnsview_mouse.mm b/src/plugins/platforms/cocoa/qnsview_mouse.mm index 1de256825a..65bc9f837d 100644 --- a/src/plugins/platforms/cocoa/qnsview_mouse.mm +++ b/src/plugins/platforms/cocoa/qnsview_mouse.mm @@ -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)