From df531a6f3c74dc666036f6171610d292415d81ad Mon Sep 17 00:00:00 2001 From: Robin Burchell Date: Sun, 30 Jul 2017 21:50:41 +0200 Subject: [PATCH] Client: Implement wl_pointer version 5 Version 5 adds frame events, which groups multiple pointer events together, enabling diagonal scrolling, leave and enter without an intermediate state, and together with the new axis_source, axis_discrete and axis_stop also adds what needed to differentiate between wheel events and touchpad scrolling. This patch adds scroll phases and pixel deltas to QWaylandInputDevice, and makes sure handleWheelEvent is called accordingly. [ChangeLog][QPA plugin] Pixel delta is now set for mouse scrolling events if originating from an appropriate device such as a touch pad (requires compositor support for wl_seat version 5 or later). Fixes: QTBUG-69876 Fixes: QTBUG-63720 Change-Id: I094a1ef0365893bee135cae7e6df89fafdafa2f2 Reviewed-by: Giulio Camuffo --- .../platforms/wayland/qwaylandinputdevice.cpp | 338 +++++++++++++-- .../platforms/wayland/qwaylandinputdevice_p.h | 80 +++- .../platforms/wayland/qwaylandwindow.cpp | 42 +- .../platforms/wayland/qwaylandwindow_p.h | 1 - tests/auto/wayland/client.pro | 1 + tests/auto/wayland/seatv5/seatv5.pro | 4 + tests/auto/wayland/seatv5/tst_seatv5.cpp | 387 ++++++++++++++++++ tests/auto/wayland/shared/coreprotocol.cpp | 33 ++ tests/auto/wayland/shared/coreprotocol.h | 4 + 9 files changed, 816 insertions(+), 74 deletions(-) create mode 100644 tests/auto/wayland/seatv5/seatv5.pro create mode 100644 tests/auto/wayland/seatv5/tst_seatv5.cpp diff --git a/src/plugins/platforms/wayland/qwaylandinputdevice.cpp b/src/plugins/platforms/wayland/qwaylandinputdevice.cpp index 31495a45324..23a84e4bd1d 100644 --- a/src/plugins/platforms/wayland/qwaylandinputdevice.cpp +++ b/src/plugins/platforms/wayland/qwaylandinputdevice.cpp @@ -78,6 +78,8 @@ QT_BEGIN_NAMESPACE namespace QtWaylandClient { +Q_LOGGING_CATEGORY(lcQpaWaylandInput, "qt.qpa.wayland.input"); + QWaylandInputDevice::Keyboard::Keyboard(QWaylandInputDevice *p) : mParent(p) { @@ -200,10 +202,10 @@ QWaylandInputDevice::Touch::~Touch() } QWaylandInputDevice::QWaylandInputDevice(QWaylandDisplay *display, int version, uint32_t id) - : QtWayland::wl_seat(display->wl_registry(), id, qMin(version, 4)) + : QtWayland::wl_seat(display->wl_registry(), id, qMin(version, 5)) , mQDisplay(display) , mDisplay(display->wl_display()) - , mVersion(qMin(version, 4)) + , mVersion(qMin(version, 5)) { #if QT_CONFIG(wayland_datadevice) if (mQDisplay->dndSelectionHandler()) { @@ -446,8 +448,9 @@ void QWaylandInputDevice::setCursor(const QSharedPointer &buffer class EnterEvent : public QWaylandPointerEvent { public: - EnterEvent(const QPointF &l, const QPointF &g) - : QWaylandPointerEvent(QWaylandPointerEvent::Enter, 0, l, g, nullptr, Qt::NoModifier) + EnterEvent(QWaylandWindow *surface, const QPointF &local, const QPointF &global) + : QWaylandPointerEvent(QWaylandPointerEvent::Enter, Qt::NoScrollPhase, surface, 0, + local, global, nullptr, Qt::NoModifier) {} }; @@ -471,12 +474,19 @@ void QWaylandInputDevice::Pointer::pointer_enter(uint32_t serial, struct wl_surf #endif QWaylandWindow *grab = QWaylandWindow::mouseGrab(); - if (!grab) { - EnterEvent evt(mSurfacePos, mGlobalPos); - window->handleMouse(mParent, evt); - } + if (!grab) + setFrameEvent(new EnterEvent(window, mSurfacePos, mGlobalPos)); } +class LeaveEvent : public QWaylandPointerEvent +{ +public: + LeaveEvent(QWaylandWindow *surface, const QPointF &localPos, const QPointF &globalPos) + : QWaylandPointerEvent(QWaylandPointerEvent::Leave, Qt::NoScrollPhase, surface, 0, + localPos, globalPos, nullptr, Qt::NoModifier) + {} +}; + void QWaylandInputDevice::Pointer::pointer_leave(uint32_t time, struct wl_surface *surface) { // The event may arrive after destroying the window, indicated by @@ -486,7 +496,7 @@ void QWaylandInputDevice::Pointer::pointer_leave(uint32_t time, struct wl_surfac if (!QWaylandWindow::mouseGrab()) { QWaylandWindow *window = QWaylandWindow::fromWlSurface(surface); - window->handleMouseLeave(mParent); + setFrameEvent(new LeaveEvent(window, mSurfacePos, mGlobalPos)); } mFocus = nullptr; mButtons = Qt::NoButton; @@ -497,8 +507,10 @@ void QWaylandInputDevice::Pointer::pointer_leave(uint32_t time, struct wl_surfac class MotionEvent : public QWaylandPointerEvent { public: - MotionEvent(ulong t, const QPointF &l, const QPointF &g, Qt::MouseButtons b, Qt::KeyboardModifiers m) - : QWaylandPointerEvent(QWaylandPointerEvent::Motion, t, l, g, b, m) + MotionEvent(QWaylandWindow *surface, ulong timestamp, const QPointF &localPos, + const QPointF &globalPos, Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers) + : QWaylandPointerEvent(QWaylandPointerEvent::Motion, Qt::NoScrollPhase, surface, + timestamp, localPos, globalPos, buttons, modifiers) { } }; @@ -527,14 +539,33 @@ void QWaylandInputDevice::Pointer::pointer_motion(uint32_t time, wl_fixed_t surf // so we just set it outside of the window boundaries. pos = QPointF(-1, -1); global = grab->window()->mapToGlobal(pos.toPoint()); - MotionEvent e(time, pos, global, mButtons, mParent->modifiers()); - grab->handleMouse(mParent, e); - } else { - MotionEvent e(time, mSurfacePos, mGlobalPos, mButtons, mParent->modifiers()); - window->handleMouse(mParent, e); + window = grab; } + setFrameEvent(new MotionEvent(window, time, pos, global, mButtons, mParent->modifiers())); } +class PressEvent : public QWaylandPointerEvent +{ +public: + PressEvent(QWaylandWindow *surface, ulong timestamp, const QPointF &localPos, + const QPointF &globalPos, Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers) + : QWaylandPointerEvent(QWaylandPointerEvent::Press, Qt::NoScrollPhase, surface, + timestamp, localPos, globalPos, buttons, modifiers) + { + } +}; + +class ReleaseEvent : public QWaylandPointerEvent +{ +public: + ReleaseEvent(QWaylandWindow *surface, ulong timestamp, const QPointF &localPos, + const QPointF &globalPos, Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers) + : QWaylandPointerEvent(QWaylandPointerEvent::Release, Qt::NoScrollPhase, surface, + timestamp, localPos, globalPos, buttons, modifiers) + { + } +}; + void QWaylandInputDevice::Pointer::pointer_button(uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { @@ -580,21 +611,26 @@ void QWaylandInputDevice::Pointer::pointer_button(uint32_t serial, uint32_t time mParent->mQDisplay->setLastInputDevice(mParent, serial, window); QWaylandWindow *grab = QWaylandWindow::mouseGrab(); + + QPointF pos = mSurfacePos; + QPointF global = mGlobalPos; if (grab && grab != mFocus) { - QPointF pos = QPointF(-1, -1); - QPointF global = grab->window()->mapToGlobal(pos.toPoint()); - MotionEvent e(time, pos, global, mButtons, mParent->modifiers()); - grab->handleMouse(mParent, e); - } else if (window) { - MotionEvent e(time, mSurfacePos, mGlobalPos, mButtons, mParent->modifiers()); - window->handleMouse(mParent, e); + pos = QPointF(-1, -1); + global = grab->window()->mapToGlobal(pos.toPoint()); + + window = grab; } + + if (state) + setFrameEvent(new PressEvent(window, time, pos, global, mButtons, mParent->modifiers())); + else + setFrameEvent(new ReleaseEvent(window, time, pos, global, mButtons, mParent->modifiers())); } void QWaylandInputDevice::Pointer::releaseButtons() { mButtons = Qt::NoButton; - MotionEvent e(mParent->mTime, mSurfacePos, mGlobalPos, mButtons, mParent->modifiers()); + MotionEvent e(mFocus, mParent->mTime, mSurfacePos, mGlobalPos, mButtons, mParent->modifiers()); if (mFocus) mFocus->handleMouse(mParent, e); } @@ -602,8 +638,11 @@ void QWaylandInputDevice::Pointer::releaseButtons() class WheelEvent : public QWaylandPointerEvent { public: - WheelEvent(ulong t, const QPointF &l, const QPointF &g, const QPoint &pd, const QPoint &ad, Qt::KeyboardModifiers m) - : QWaylandPointerEvent(QWaylandPointerEvent::Wheel, t, l, g, pd, ad, m) + WheelEvent(QWaylandWindow *surface, Qt::ScrollPhase phase, ulong timestamp, const QPointF &local, + const QPointF &global, const QPoint &pixelDelta, const QPoint &angleDelta, + Qt::MouseEventSource source, Qt::KeyboardModifiers modifiers) + : QWaylandPointerEvent(QWaylandPointerEvent::Wheel, phase, surface, timestamp, + local, global, pixelDelta, angleDelta, source, modifiers) { } }; @@ -611,28 +650,247 @@ public: void QWaylandInputDevice::Pointer::pointer_axis(uint32_t time, uint32_t axis, int32_t value) { QWaylandWindow *window = mFocus; + if (!window) { // We destroyed the pointer focus surface, but the server didn't get the message yet... // or the server didn't send an enter event first. In either case, ignore the event. return; } - QPoint pixelDelta; - QPoint angleDelta; - - //normalize value and inverse axis - int valueDelta = wl_fixed_to_int(value) * -12; - - if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) { - pixelDelta = QPoint(); - angleDelta.setX(valueDelta); - } else { - pixelDelta = QPoint(); - angleDelta.setY(valueDelta); + // Get the delta and convert it into the expected range + switch (axis) { + case WL_POINTER_AXIS_VERTICAL_SCROLL: + mFrameData.delta.ry() += wl_fixed_to_double(value); + qCDebug(lcQpaWaylandInput) << "wl_pointer.axis vertical:" << mFrameData.delta.y(); + break; + case WL_POINTER_AXIS_HORIZONTAL_SCROLL: + mFrameData.delta.rx() += wl_fixed_to_double(value); + qCDebug(lcQpaWaylandInput) << "wl_pointer.axis horizontal:" << mFrameData.delta.x(); + break; + default: + //TODO: is this really needed? + qCWarning(lcQpaWaylandInput) << "wl_pointer.axis: Unknown axis:" << axis; + return; } - WheelEvent e(time, mSurfacePos, mGlobalPos, pixelDelta, angleDelta, mParent->modifiers()); - window->handleMouse(mParent, e); + mParent->mTime = time; + + if (mParent->mVersion < WL_POINTER_FRAME_SINCE_VERSION) { + qCDebug(lcQpaWaylandInput) << "Flushing new event; no frame event in this version"; + flushFrameEvent(); + } +} + +void QWaylandInputDevice::Pointer::pointer_frame() +{ + flushFrameEvent(); +} + +void QWaylandInputDevice::Pointer::pointer_axis_source(uint32_t source) +{ + switch (source) { + case axis_source_wheel: + qCDebug(lcQpaWaylandInput) << "Axis source wheel"; + break; + case axis_source_finger: + qCDebug(lcQpaWaylandInput) << "Axis source finger"; + break; + case axis_source_continuous: + qCDebug(lcQpaWaylandInput) << "Axis source continuous"; + break; + } + mFrameData.axisSource = axis_source(source); +} + +void QWaylandInputDevice::Pointer::pointer_axis_stop(uint32_t time, uint32_t axis) +{ + QWaylandWindow *window = mFocus; + if (window == nullptr) + return; + + mParent->mTime = time; + switch (axis) { + case axis_vertical_scroll: + qCDebug(lcQpaWaylandInput) << "Received vertical wl_pointer.axis_stop"; + mFrameData.delta.setY(0); //TODO: what's the point of doing this? + break; + case axis_horizontal_scroll: + qCDebug(lcQpaWaylandInput) << "Received horizontal wl_pointer.axis_stop"; + mFrameData.delta.setX(0); + break; + default: + qCWarning(lcQpaWaylandInput) << "wl_pointer.axis_stop: Unknown axis: " << axis + << "This is most likely a compositor bug"; + return; + } + + // May receive axis_stop for events we haven't sent a ScrollBegin for because + // most axis_sources do not mandate an axis_stop event to be sent. + if (!mScrollBeginSent) { + // TODO: For now, we just ignore these events, but we could perhaps take this as an + // indication that this compositor will in fact send axis_stop events for these sources + // and send a ScrollBegin the next time an axis_source event with this type is encountered. + return; + } + + QWaylandWindow *target = QWaylandWindow::mouseGrab(); + if (!target) + target = mFocus; + Qt::KeyboardModifiers mods = mParent->modifiers(); + WheelEvent wheelEvent(mFocus, Qt::ScrollEnd, mParent->mTime, mSurfacePos, mGlobalPos, + QPoint(), QPoint(), Qt::MouseEventNotSynthesized, mods); + target->handleMouse(mParent, wheelEvent); + mScrollBeginSent = false; + mScrollDeltaRemainder = QPointF(); +} + +void QWaylandInputDevice::Pointer::pointer_axis_discrete(uint32_t axis, int32_t value) +{ + QWaylandWindow *window = mFocus; + if (window == nullptr) + return; + + switch (axis) { + case axis_vertical_scroll: + qCDebug(lcQpaWaylandInput) << "wl_pointer.axis_discrete vertical:" << value; + mFrameData.discreteDelta.ry() += value; + break; + case axis_horizontal_scroll: + qCDebug(lcQpaWaylandInput) << "wl_pointer.axis_discrete horizontal:" << value; + mFrameData.discreteDelta.rx() += value; + break; + default: + //TODO: is this really needed? + qCWarning(lcQpaWaylandInput) << "wl_pointer.axis_discrete: Unknown axis:" << axis; + return; + } +} + +void QWaylandInputDevice::Pointer::setFrameEvent(QWaylandPointerEvent *event) +{ + qCDebug(lcQpaWaylandInput) << "Setting frame event " << event->type; + if (mFrameData.event && mFrameData.event->type != event->type) { + qCDebug(lcQpaWaylandInput) << "Flushing; previous was " << mFrameData.event->type; + flushFrameEvent(); + } + + mFrameData.event = event; + + if (mParent->mVersion < WL_POINTER_FRAME_SINCE_VERSION) { + qCDebug(lcQpaWaylandInput) << "Flushing new event; no frame event in this version"; + flushFrameEvent(); + } +} + +void QWaylandInputDevice::Pointer::FrameData::resetScrollData() +{ + discreteDelta = QPoint(); + delta = QPointF(); + axisSource = axis_source_wheel; +} + +bool QWaylandInputDevice::Pointer::FrameData::hasPixelDelta() const +{ + switch (axisSource) { + case axis_source_wheel_tilt: // sideways tilt of the wheel + case axis_source_wheel: + // In the case of wheel events, a pixel delta doesn't really make sense, + // and will make Qt think this is a continuous scroll event when it isn't, + // so just ignore it. + return false; + case axis_source_finger: + case axis_source_continuous: + return !delta.isNull(); + } +} + +QPoint QWaylandInputDevice::Pointer::FrameData::pixelDeltaAndError(QPointF *accumulatedError) const +{ + if (!hasPixelDelta()) + return QPoint(); + + Q_ASSERT(accumulatedError); + // Add accumulated rounding error before rounding again + QPoint pixelDelta = (delta + *accumulatedError).toPoint(); + *accumulatedError += delta - pixelDelta; + Q_ASSERT(qAbs(accumulatedError->x()) < 1.0); + Q_ASSERT(qAbs(accumulatedError->y()) < 1.0); + return pixelDelta; +} + +QPoint QWaylandInputDevice::Pointer::FrameData::angleDelta() const +{ + if (discreteDelta.isNull()) { + // If we didn't get any discrete events, then we need to fall back to + // the continuous information. + return (delta * -12).toPoint(); //TODO: why multiply by 12? + } + + // The angle delta is in eights of degrees, and our docs says most mice have + // 1 click = 15 degrees. It's also in the opposite direction of surface space. + return -discreteDelta * 15 * 8; +} + +Qt::MouseEventSource QWaylandInputDevice::Pointer::FrameData::wheelEventSource() const +{ + switch (axisSource) { + case axis_source_wheel_tilt: // sideways tilt of the wheel + case axis_source_wheel: + return Qt::MouseEventNotSynthesized; + case axis_source_finger: + case axis_source_continuous: + default: // Whatever other sources might be added are probably not mouse wheels + return Qt::MouseEventSynthesizedBySystem; + } +} + +void QWaylandInputDevice::Pointer::flushScrollEvent() +{ + QPoint angleDelta = mFrameData.angleDelta(); + + // Angle delta is required for Qt wheel events, so don't try to send events if it's zero + if (!angleDelta.isNull()) { + QWaylandWindow *target = QWaylandWindow::mouseGrab(); + if (!target) + target = mFocus; + + if (isDefinitelyTerminated(mFrameData.axisSource) && !mScrollBeginSent) { + qCDebug(lcQpaWaylandInput) << "Flushing scroll event sending ScrollBegin"; + target->handleMouse(mParent, WheelEvent(mFocus, Qt::ScrollBegin, mParent->mTime, + mSurfacePos, mGlobalPos, QPoint(), QPoint(), + Qt::MouseEventNotSynthesized, + mParent->modifiers())); + mScrollBeginSent = true; + mScrollDeltaRemainder = QPointF(); + } + + Qt::ScrollPhase phase = mScrollBeginSent ? Qt::ScrollUpdate : Qt::NoScrollPhase; + QPoint pixelDelta = mFrameData.pixelDeltaAndError(&mScrollDeltaRemainder); + Qt::MouseEventSource source = mFrameData.wheelEventSource(); + + qCDebug(lcQpaWaylandInput) << "Flushing scroll event" << phase << pixelDelta << angleDelta; + target->handleMouse(mParent, WheelEvent(mFocus, phase, mParent->mTime, mSurfacePos, mGlobalPos, + pixelDelta, angleDelta, source, mParent->modifiers())); + } + + mFrameData.resetScrollData(); +} + +void QWaylandInputDevice::Pointer::flushFrameEvent() +{ + if (mFrameData.event) { + mFrameData.event->surface->handleMouse(mParent, *mFrameData.event); + delete mFrameData.event; + mFrameData.event = nullptr; + } + + //TODO: do modifiers get passed correctly here? + flushScrollEvent(); +} + +bool QWaylandInputDevice::Pointer::isDefinitelyTerminated(QtWayland::wl_pointer::axis_source source) const +{ + return source == axis_source_finger; } void QWaylandInputDevice::Keyboard::keyboard_keymap(uint32_t format, int32_t fd, uint32_t size) diff --git a/src/plugins/platforms/wayland/qwaylandinputdevice_p.h b/src/plugins/platforms/wayland/qwaylandinputdevice_p.h index c2fd57bb0df..0da73e5d59e 100644 --- a/src/plugins/platforms/wayland/qwaylandinputdevice_p.h +++ b/src/plugins/platforms/wayland/qwaylandinputdevice_p.h @@ -262,6 +262,10 @@ public: void pointer_axis(uint32_t time, uint32_t axis, wl_fixed_t value) override; + void pointer_axis_source(uint32_t source) override; + void pointer_axis_stop(uint32_t time, uint32_t axis) override; + void pointer_axis_discrete(uint32_t axis, int32_t value) override; + void pointer_frame() override; void releaseButtons(); @@ -278,6 +282,30 @@ public: wl_buffer *mCursorBuffer = nullptr; Qt::CursorShape mCursorShape = Qt::BitmapCursor; #endif + + struct FrameData { + QWaylandPointerEvent *event = nullptr; + + QPointF delta; + QPoint discreteDelta; + axis_source axisSource = axis_source_wheel; + + void resetScrollData(); + bool hasPixelDelta() const; + QPoint pixelDeltaAndError(QPointF *accumulatedError) const; + QPoint pixelDelta() const { return hasPixelDelta() ? delta.toPoint() : QPoint(); } + QPoint angleDelta() const; + Qt::MouseEventSource wheelEventSource() const; + } mFrameData; + + bool mScrollBeginSent = false; + QPointF mScrollDeltaRemainder; + + void setFrameEvent(QWaylandPointerEvent *event); + void flushScrollEvent(); + void flushFrameEvent(); +private: //TODO: should other methods be private as well? + bool isDefinitelyTerminated(axis_source source) const; }; class Q_WAYLAND_CLIENT_EXPORT QWaylandInputDevice::Touch : public QtWayland::wl_touch @@ -313,38 +341,58 @@ public: class QWaylandPointerEvent { + Q_GADGET public: enum Type { Enter, + Leave, Motion, + Press, + Release, Wheel }; - inline QWaylandPointerEvent(Type t, ulong ts, const QPointF &l, const QPointF &g, Qt::MouseButtons b, Qt::KeyboardModifiers m) - : type(t) - , timestamp(ts) - , local(l) - , global(g) - , buttons(b) - , modifiers(m) + Q_ENUM(Type) + + inline QWaylandPointerEvent(Type type, Qt::ScrollPhase phase, QWaylandWindow *surface, + ulong timestamp, const QPointF &localPos, const QPointF &globalPos, + Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers) + : type(type) + , phase(phase) + , timestamp(timestamp) + , local(localPos) + , global(globalPos) + , buttons(buttons) + , modifiers(modifiers) + , surface(surface) {} - inline QWaylandPointerEvent(Type t, ulong ts, const QPointF &l, const QPointF &g, const QPoint &pd, const QPoint &ad, Qt::KeyboardModifiers m) - : type(t) - , timestamp(ts) - , local(l) - , global(g) - , modifiers(m) - , pixelDelta(pd) - , angleDelta(ad) + inline QWaylandPointerEvent(Type type, Qt::ScrollPhase phase, QWaylandWindow *surface, + ulong timestamp, const QPointF &local, const QPointF &global, + const QPoint &pixelDelta, const QPoint &angleDelta, + Qt::MouseEventSource source, + Qt::KeyboardModifiers modifiers) + : type(type) + , phase(phase) + , timestamp(timestamp) + , local(local) + , global(global) + , modifiers(modifiers) + , pixelDelta(pixelDelta) + , angleDelta(angleDelta) + , source(source) + , surface(surface) {} Type type; - ulong timestamp; + Qt::ScrollPhase phase = Qt::NoScrollPhase; + ulong timestamp = 0; QPointF local; QPointF global; Qt::MouseButtons buttons; Qt::KeyboardModifiers modifiers; QPoint pixelDelta; QPoint angleDelta; + Qt::MouseEventSource source = Qt::MouseEventNotSynthesized; + QWaylandWindow *surface = nullptr; }; } diff --git a/src/plugins/platforms/wayland/qwaylandwindow.cpp b/src/plugins/platforms/wayland/qwaylandwindow.cpp index 9e95b306867..abbea6291bd 100644 --- a/src/plugins/platforms/wayland/qwaylandwindow.cpp +++ b/src/plugins/platforms/wayland/qwaylandwindow.cpp @@ -846,6 +846,18 @@ QWaylandWindow *QWaylandWindow::transientParent() const void QWaylandWindow::handleMouse(QWaylandInputDevice *inputDevice, const QWaylandPointerEvent &e) { + if (e.type == QWaylandPointerEvent::Leave) { + if (mWindowDecoration) { + if (mMouseEventsInContentArea) + QWindowSystemInterface::handleLeaveEvent(window()); + } else { + QWindowSystemInterface::handleLeaveEvent(window()); + } +#if QT_CONFIG(cursor) + restoreMouseCursor(inputDevice); +#endif + return; + } if (mWindowDecoration) { handleMouseEventWithDecoration(inputDevice, e); @@ -854,11 +866,15 @@ void QWaylandWindow::handleMouse(QWaylandInputDevice *inputDevice, const QWaylan case QWaylandPointerEvent::Enter: QWindowSystemInterface::handleEnterEvent(window(), e.local, e.global); break; + case QWaylandPointerEvent::Press: + case QWaylandPointerEvent::Release: case QWaylandPointerEvent::Motion: QWindowSystemInterface::handleMouseEvent(window(), e.timestamp, e.local, e.global, e.buttons, e.modifiers); break; case QWaylandPointerEvent::Wheel: - QWindowSystemInterface::handleWheelEvent(window(), e.timestamp, e.local, e.global, e.pixelDelta, e.angleDelta, e.modifiers); + QWindowSystemInterface::handleWheelEvent(window(), e.timestamp, e.local, e.global, + e.pixelDelta, e.angleDelta, e.modifiers, + e.phase, e.source, false); break; } } @@ -872,20 +888,6 @@ void QWaylandWindow::handleMouse(QWaylandInputDevice *inputDevice, const QWaylan #endif } -void QWaylandWindow::handleMouseLeave(QWaylandInputDevice *inputDevice) -{ - if (mWindowDecoration) { - if (mMouseEventsInContentArea) { - QWindowSystemInterface::handleLeaveEvent(window()); - } - } else { - QWindowSystemInterface::handleLeaveEvent(window()); - } -#if QT_CONFIG(cursor) - restoreMouseCursor(inputDevice); -#endif -} - bool QWaylandWindow::touchDragDecoration(QWaylandInputDevice *inputDevice, const QPointF &local, const QPointF &global, Qt::TouchPointState state, Qt::KeyboardModifiers mods) { if (!mWindowDecoration) @@ -927,12 +929,18 @@ void QWaylandWindow::handleMouseEventWithDecoration(QWaylandInputDevice *inputDe case QWaylandPointerEvent::Enter: QWindowSystemInterface::handleEnterEvent(window(), localTranslated, globalTranslated); break; + case QWaylandPointerEvent::Press: + case QWaylandPointerEvent::Release: case QWaylandPointerEvent::Motion: QWindowSystemInterface::handleMouseEvent(window(), e.timestamp, localTranslated, globalTranslated, e.buttons, e.modifiers); break; - case QWaylandPointerEvent::Wheel: - QWindowSystemInterface::handleWheelEvent(window(), e.timestamp, localTranslated, globalTranslated, e.pixelDelta, e.angleDelta, e.modifiers); + case QWaylandPointerEvent::Wheel: { + QWindowSystemInterface::handleWheelEvent(window(), e.timestamp, + localTranslated, globalTranslated, + e.pixelDelta, e.angleDelta, e.modifiers, + e.phase, e.source, false); break; + } } mMouseEventsInContentArea = true; diff --git a/src/plugins/platforms/wayland/qwaylandwindow_p.h b/src/plugins/platforms/wayland/qwaylandwindow_p.h index 0e573f350eb..19c71627b56 100644 --- a/src/plugins/platforms/wayland/qwaylandwindow_p.h +++ b/src/plugins/platforms/wayland/qwaylandwindow_p.h @@ -158,7 +158,6 @@ public: QWaylandAbstractDecoration *decoration() const; void handleMouse(QWaylandInputDevice *inputDevice, const QWaylandPointerEvent &e); - void handleMouseLeave(QWaylandInputDevice *inputDevice); bool touchDragDecoration(QWaylandInputDevice *inputDevice, const QPointF &local, const QPointF &global, Qt::TouchPointState state, Qt::KeyboardModifiers mods); diff --git a/tests/auto/wayland/client.pro b/tests/auto/wayland/client.pro index e99db20ba4f..97d99bd3248 100644 --- a/tests/auto/wayland/client.pro +++ b/tests/auto/wayland/client.pro @@ -7,6 +7,7 @@ SUBDIRS += \ iviapplication \ output \ seatv4 \ + seatv5 \ surface \ wl_connect \ xdgoutput \ diff --git a/tests/auto/wayland/seatv5/seatv5.pro b/tests/auto/wayland/seatv5/seatv5.pro new file mode 100644 index 00000000000..2081845efa8 --- /dev/null +++ b/tests/auto/wayland/seatv5/seatv5.pro @@ -0,0 +1,4 @@ +include (../shared/shared.pri) + +TARGET = tst_seatv5 +SOURCES += tst_seatv5.cpp diff --git a/tests/auto/wayland/seatv5/tst_seatv5.cpp b/tests/auto/wayland/seatv5/tst_seatv5.cpp new file mode 100644 index 00000000000..5b9235d9aa3 --- /dev/null +++ b/tests/auto/wayland/seatv5/tst_seatv5.cpp @@ -0,0 +1,387 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "mockcompositor.h" +#include +#include + +using namespace MockCompositor; + +class SeatV5Compositor : public DefaultCompositor { +public: + explicit SeatV5Compositor() + { + exec([this] { + m_config.autoConfigure = true; + + removeAll(); + + uint capabilities = MockCompositor::Seat::capability_pointer; + int version = 5; + add(capabilities, version); + }); + } + + Pointer *pointer() + { + auto *seat = get(); + Q_ASSERT(seat); + return seat->m_pointer; + } +}; + +class tst_seatv5 : public QObject, private SeatV5Compositor +{ + Q_OBJECT +private slots: + void cleanup() { QTRY_VERIFY2(isClean(), qPrintable(dirtyMessage())); } + void bindsToSeat(); + void createsPointer(); + void setsCursorOnEnter(); + void usesEnterSerial(); + void simpleAxis_data(); + void simpleAxis(); + void fingerScroll(); + void fingerScrollSlow(); + void wheelDiscreteScroll(); +}; + +void tst_seatv5::bindsToSeat() +{ + QCOMPOSITOR_COMPARE(get()->resourceMap().size(), 1); + QCOMPOSITOR_COMPARE(get()->resourceMap().first()->version(), 5); +} + +void tst_seatv5::createsPointer() +{ + QCOMPOSITOR_TRY_COMPARE(pointer()->resourceMap().size(), 1); + QCOMPOSITOR_TRY_COMPARE(pointer()->resourceMap().first()->version(), 5); +} + +void tst_seatv5::setsCursorOnEnter() +{ + QRasterWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + exec([=] { + auto *surface = xdgSurface()->m_surface; + pointer()->sendEnter(surface, {0, 0}); + pointer()->sendFrame(surface->resource()->client()); + }); + + QCOMPOSITOR_TRY_VERIFY(pointer()->cursorSurface()); +} + +void tst_seatv5::usesEnterSerial() +{ + QSignalSpy setCursorSpy(exec([=] { return pointer(); }), &Pointer::setCursor); + QRasterWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + uint enterSerial = exec([=] { + return pointer()->sendEnter(xdgSurface()->m_surface, {0, 0}); + }); + QCOMPOSITOR_TRY_VERIFY(pointer()->cursorSurface()); + + QTRY_COMPARE(setCursorSpy.count(), 1); + QCOMPARE(setCursorSpy.takeFirst().at(0).toUInt(), enterSerial); +} + +class WheelWindow : QRasterWindow { +public: + WheelWindow() + { + resize(64, 64); + show(); + } + void wheelEvent(QWheelEvent *event) override + { + QRasterWindow::wheelEvent(event); +// qDebug() << event << "angleDelta" << event->angleDelta() << "pixelDelta" << event->pixelDelta(); + + if (event->phase() == Qt::ScrollUpdate || event->phase() == Qt::NoScrollPhase) { + // Angle delta should always be provided (says docs, but QPA sends compatibility events + // for Qt4 with zero angleDelta, and with a delta) + QVERIFY(!event->angleDelta().isNull() || event->delta()); + } else { + // Shouldn't have deltas in the other phases + QCOMPARE(event->angleDelta(), QPoint(0, 0)); + QCOMPARE(event->pixelDelta(), QPoint(0, 0)); + } + + // The axis vector of the event is already in surface space, so there is now way to tell + // whether it is inverted or not. + QCOMPARE(event->inverted(), false); + + // We didn't press any buttons + QCOMPARE(event->buttons(), Qt::NoButton); + + if (!event->angleDelta().isNull()) { + if (event->orientation() == Qt::Horizontal) + QCOMPARE(event->delta(), event->angleDelta().x()); + else + QCOMPARE(event->delta(), event->angleDelta().y()); + } + + m_events.append(Event{event}); + } + struct Event // Because I didn't find a convenient way to copy it entirely + { + explicit Event() = default; + explicit Event(const QWheelEvent *event) + : phase(event->phase()) + , pixelDelta(event->pixelDelta()) + , angleDelta(event->angleDelta()) + , orientation(event->orientation()) + , source(event->source()) + { + } + const Qt::ScrollPhase phase{}; + const QPoint pixelDelta; + const QPoint angleDelta; // eights of a degree, positive is upwards, left + const Qt::Orientation orientation{}; + const Qt::MouseEventSource source{}; + }; + QVector m_events; +}; + +void tst_seatv5::simpleAxis_data() +{ + QTest::addColumn("axis"); + QTest::addColumn("value"); + QTest::addColumn("orientation"); + QTest::addColumn("angleDelta"); + + // Directions in regular windows/linux terms (no "natural" scrolling) + QTest::newRow("down") << uint(Pointer::axis_vertical_scroll) << 1.0 << Qt::Vertical << QPoint{0, -12}; + QTest::newRow("up") << uint(Pointer::axis_vertical_scroll) << -1.0 << Qt::Vertical << QPoint{0, 12}; + QTest::newRow("left") << uint(Pointer::axis_horizontal_scroll) << 1.0 << Qt::Horizontal << QPoint{-12, 0}; + QTest::newRow("right") << uint(Pointer::axis_horizontal_scroll) << -1.0 << Qt::Horizontal << QPoint{12, 0}; + QTest::newRow("up big") << uint(Pointer::axis_vertical_scroll) << -10.0 << Qt::Vertical << QPoint{0, 120}; +} + +void tst_seatv5::simpleAxis() +{ + QFETCH(uint, axis); + QFETCH(qreal, value); + QFETCH(Qt::Orientation, orientation); + QFETCH(QPoint, angleDelta); + + WheelWindow window; + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + exec([=] { + auto *p = pointer(); + p->sendEnter(xdgToplevel()->surface(), {32, 32}); + p->sendFrame(client()); + p->sendAxis( + client(), + Pointer::axis(axis), + value // Length of vector in surface-local space. i.e. positive is downwards + ); + p->sendFrame(client()); + }); + + QTRY_VERIFY(!window.m_events.empty()); + { + auto e = window.m_events.takeFirst(); + QCOMPARE(e.phase, Qt::NoScrollPhase); + // Pixel delta should only be set if we know it's a high-res input device (which we don't) + QCOMPARE(e.pixelDelta, QPoint(0, 0)); + // There has been no information about what created the event. + // Documentation says not synthesized is appropriate in such cases + QCOMPARE(e.source, Qt::MouseEventNotSynthesized); + QCOMPARE(e.orientation, orientation); + QCOMPARE(e.angleDelta, angleDelta); + } + + // Sending axis_stop is not mandatory when axis source != finger +} + +void tst_seatv5::fingerScroll() +{ + WheelWindow window; + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + exec([=] { + auto *p = pointer(); + auto *c = client(); + p->sendEnter(xdgToplevel()->surface(), {32, 32}); + p->sendFrame(c); + p->sendAxisSource(c, Pointer::axis_source_finger); + p->sendAxis(c, Pointer::axis_vertical_scroll, 10); + p->sendFrame(c); + }); + + QTRY_VERIFY(!window.m_events.empty()); + { + auto e = window.m_events.takeFirst(); + QCOMPARE(e.phase, Qt::ScrollBegin); + QCOMPARE(e.angleDelta, QPoint()); + QCOMPARE(e.pixelDelta, QPoint()); + } + + QTRY_VERIFY(!window.m_events.empty()); + // For some reason we send two ScrollBegins, one for each direction, not sure if this is really + // necessary, (could be removed from QtBase, hence the conditional below. + if (window.m_events.first().phase == Qt::ScrollBegin) { + auto e = window.m_events.takeFirst(); + QCOMPARE(e.angleDelta, QPoint()); + QCOMPARE(e.pixelDelta, QPoint()); + } + + QTRY_VERIFY(!window.m_events.empty()); + { + auto e = window.m_events.takeFirst(); + QCOMPARE(e.phase, Qt::ScrollUpdate); + QCOMPARE(e.orientation, Qt::Vertical); +// QCOMPARE(e.angleDelta, angleDelta); // TODO: what should this be? + QCOMPARE(e.pixelDelta, QPoint(0, 10)); + QCOMPARE(e.source, Qt::MouseEventSynthesizedBySystem); // A finger is not a wheel + } + + QTRY_VERIFY(window.m_events.empty()); + + // Scroll horizontally as well + exec([=] { + pointer()->sendAxisSource(client(), Pointer::axis_source_finger); + pointer()->sendAxis(client(), Pointer::axis_horizontal_scroll, 10); + pointer()->sendFrame(client()); + }); + QTRY_VERIFY(!window.m_events.empty()); + { + auto e = window.m_events.takeFirst(); + QCOMPARE(e.phase, Qt::ScrollUpdate); + QCOMPARE(e.orientation, Qt::Horizontal); + QCOMPARE(e.pixelDelta, QPoint(10, 0)); + QCOMPARE(e.source, Qt::MouseEventSynthesizedBySystem); // A finger is not a wheel + } + + // Scroll diagonally + exec([=] { + pointer()->sendAxisSource(client(), Pointer::axis_source_finger); + pointer()->sendAxis(client(), Pointer::axis_horizontal_scroll, 10); + pointer()->sendAxis(client(), Pointer::axis_vertical_scroll, 10); + pointer()->sendFrame(client()); + }); + QTRY_VERIFY(!window.m_events.empty()); + { + auto e = window.m_events.takeFirst(); + QCOMPARE(e.phase, Qt::ScrollUpdate); + QCOMPARE(e.pixelDelta, QPoint(10, 10)); + QCOMPARE(e.source, Qt::MouseEventSynthesizedBySystem); // A finger is not a wheel + } + + // For diagonal events, Qt sends an additional compatibility ScrollUpdate event + if (window.m_events.first().phase == Qt::ScrollUpdate) { + auto e = window.m_events.takeFirst(); + QCOMPARE(e.angleDelta, QPoint()); + QCOMPARE(e.pixelDelta, QPoint()); + } + + QVERIFY(window.m_events.empty()); + + // Sending axis_stop is mandatory when axis source == finger + exec([=] { + pointer()->sendAxisStop(client(), Pointer::axis_vertical_scroll); + pointer()->sendFrame(client()); + }); + + QTRY_VERIFY(!window.m_events.empty()); + { + auto e = window.m_events.takeFirst(); + QCOMPARE(e.phase, Qt::ScrollEnd); + } +} + + +void tst_seatv5::fingerScrollSlow() +{ + WheelWindow window; + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + exec([=] { + auto *p = pointer(); + auto *c = client(); + p->sendEnter(xdgToplevel()->surface(), {32, 32}); + p->sendFrame(c); + // Send 10 really small updates + for (int i = 0; i < 10; ++i) { + p->sendAxisSource(c, Pointer::axis_source_finger); + p->sendAxis(c, Pointer::axis_vertical_scroll, 0.1); + p->sendFrame(c); + } + p->sendAxisStop(c, Pointer::axis_vertical_scroll); + p->sendFrame(c); + }); + + QTRY_VERIFY(!window.m_events.empty()); + QPoint accumulated; + while (window.m_events.first().phase != Qt::ScrollEnd) { + auto e = window.m_events.takeFirst(); + accumulated += e.pixelDelta; + QTRY_VERIFY(!window.m_events.empty()); + } + QCOMPARE(accumulated.y(), 1); +} +void tst_seatv5::wheelDiscreteScroll() +{ + WheelWindow window; + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + exec([=] { + auto *p = pointer(); + auto *c = client(); + p->sendEnter(xdgToplevel()->surface(), {32, 32}); + p->sendFrame(c); + p->sendAxisSource(c, Pointer::axis_source_wheel); + p->sendAxisDiscrete(c, Pointer::axis_vertical_scroll, 1); // 1 click downwards + p->sendAxis(c, Pointer::axis_vertical_scroll, 1.0); + p->sendFrame(c); + }); + + QTRY_VERIFY(!window.m_events.empty()); + { + auto e = window.m_events.takeFirst(); + QCOMPARE(e.phase, Qt::NoScrollPhase); + QCOMPARE(e.orientation, Qt::Vertical); + // According to the docs the angle delta is in eights of a degree and most mice have + // 1 click = 15 degrees. The angle delta should therefore be: + // 15 degrees / (1/8 eights per degrees) = 120 eights of degrees. + QCOMPARE(e.angleDelta, QPoint(0, -120)); + // Click scrolls are not continuous and should not have a pixel delta + QCOMPARE(e.pixelDelta, QPoint(0, 0)); + } +} + +QCOMPOSITOR_TEST_MAIN(tst_seatv5) +#include "tst_seatv5.moc" diff --git a/tests/auto/wayland/shared/coreprotocol.cpp b/tests/auto/wayland/shared/coreprotocol.cpp index 6f51a9793c9..b37f7f29cc5 100644 --- a/tests/auto/wayland/shared/coreprotocol.cpp +++ b/tests/auto/wayland/shared/coreprotocol.cpp @@ -312,6 +312,39 @@ void Pointer::sendAxis(wl_client *client, axis axis, qreal value) send_axis(r->handle, time, axis, val); } +void Pointer::sendAxisDiscrete(wl_client *client, QtWaylandServer::wl_pointer::axis axis, int discrete) +{ + // TODO: assert v5 or newer + const auto pointerResources = resourceMap().values(client); + for (auto *r : pointerResources) + send_axis_discrete(r->handle, axis, discrete); +} + +void Pointer::sendAxisSource(wl_client *client, QtWaylandServer::wl_pointer::axis_source source) +{ + // TODO: assert v5 or newer + const auto pointerResources = resourceMap().values(client); + for (auto *r : pointerResources) + send_axis_source(r->handle, source); +} + +void Pointer::sendAxisStop(wl_client *client, QtWaylandServer::wl_pointer::axis axis) +{ + // TODO: assert v5 or newer + auto time = m_seat->m_compositor->currentTimeMilliseconds(); + const auto pointerResources = resourceMap().values(client); + for (auto *r : pointerResources) + send_axis_stop(r->handle, time, axis); +} + +void Pointer::sendFrame(wl_client *client) +{ + //TODO: assert version 5 or newer? + const auto pointerResources = resourceMap().values(client); + for (auto *r : pointerResources) + send_frame(r->handle); +} + void Pointer::pointer_set_cursor(Resource *resource, uint32_t serial, wl_resource *surface, int32_t hotspot_x, int32_t hotspot_y) { Q_UNUSED(resource); diff --git a/tests/auto/wayland/shared/coreprotocol.h b/tests/auto/wayland/shared/coreprotocol.h index 249c16f428b..94876f15f04 100644 --- a/tests/auto/wayland/shared/coreprotocol.h +++ b/tests/auto/wayland/shared/coreprotocol.h @@ -282,6 +282,10 @@ public: void sendMotion(wl_client *client, const QPointF &position); uint sendButton(wl_client *client, uint button, uint state); void sendAxis(wl_client *client, axis axis, qreal value); + void sendAxisDiscrete(wl_client *client, axis axis, int discrete); + void sendAxisSource(wl_client *client, axis_source source); + void sendAxisStop(wl_client *client, axis axis); + void sendFrame(wl_client *client); Seat *m_seat = nullptr; uint m_enterSerial = 0;