Client: Add basic support for tablet-unstable-v2

[ChangeLog][QPA plugin] Added support for drawing tablets through the
tablet-unstable-v2 protocol.

Adds support for the basic one tablet, one tool, no pads scenario.

Besides the auto tests, I tested with a Huion Kamvas Pro 16 with a simple
pressure sensitive pen with two buttons. As far as I can tell, it works the
same way as on xcb on Sway and Gnome Shell.

Also tested on a Wacom PTZ-630 with a mouse, art pen, airbrush and a stylus.

Mapped the distance event to QTabletEvent::Z and slider to
QTabletEvent::tangentialPressure.

For now we send QTabletEvents even when there's a mouse or finger on the
tablet. Those should ideally be sent as QMouseEvents/QTouchEvents, but that's
out of scope for this patch, but at least we will send synthesized mouse events
if the tablet events are not accepted.

Change-Id: I93291ffa5f00fa2bb8533eddd8d873b84a3386b8
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
This commit is contained in:
Johan Klokkhammer Helsing 2019-12-17 12:08:54 +01:00
parent 8abd3e5f55
commit 4d844d3640
13 changed files with 2664 additions and 0 deletions

View File

@ -89,6 +89,24 @@ Copyright (c) 2013 BMW Car IT GmbH"
"Copyright": "Copyright © 2013-2014 Collabora, Ltd."
},
{
"Id": "wayland-tablet-protocol",
"Name": "Wayland Tablet Protocol",
"QDocModule": "qtwaylandcompositor",
"QtUsage": "Used in the Qt Wayland platform plugin",
"Files": "tablet-unstable-v2.xml",
"Description": "",
"Homepage": "https://wayland.freedesktop.org",
"Version": "unstable v2, version 1",
"DownloadLocation": "https://cgit.freedesktop.org/wayland/wayland-protocols/plain/unstable/tablet/tablet-unstable-v2.xml",
"LicenseId": "MIT",
"License": "MIT License",
"LicenseFile": "MIT_LICENSE.txt",
"Copyright": "Copyright 2014 © Stephen "Lyude" Chandler Paul
Copyright 2015-2016 © Red Hat, Inc."
},
{
"Id": "wayland-viewporter-protocol",
"Name": "Wayland Viewporter Protocol",

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,7 @@ WAYLANDCLIENTSOURCES += \
../extensions/qt-key-unstable-v1.xml \
../extensions/qt-windowmanager.xml \
../3rdparty/protocol/wp-primary-selection-unstable-v1.xml \
../3rdparty/protocol/tablet-unstable-v2.xml \
../3rdparty/protocol/text-input-unstable-v2.xml \
../3rdparty/protocol/xdg-output-unstable-v1.xml \
../3rdparty/protocol/wayland.xml
@ -52,6 +53,7 @@ SOURCES += qwaylandintegration.cpp \
qwaylandextendedsurface.cpp \
qwaylandsubsurface.cpp \
qwaylandsurface.cpp \
qwaylandtabletv2.cpp \
qwaylandtouch.cpp \
qwaylandqtkey.cpp \
../shared/qwaylandmimehelper.cpp \
@ -77,6 +79,7 @@ HEADERS += qwaylandintegration_p.h \
qwaylandextendedsurface_p.h \
qwaylandsubsurface_p.h \
qwaylandsurface_p.h \
qwaylandtabletv2_p.h \
qwaylandtouch_p.h \
qwaylandqtkey_p.h \
qwaylandabstractdecoration_p.h \

View File

@ -69,6 +69,7 @@
#include "qwaylandextendedsurface_p.h"
#include "qwaylandsubsurface_p.h"
#include "qwaylandtouch_p.h"
#include "qwaylandtabletv2_p.h"
#include "qwaylandqtkey_p.h"
#include <QtWaylandClient/private/qwayland-text-input-unstable-v2.h>
@ -330,6 +331,8 @@ void QWaylandDisplay::registry_global(uint32_t id, const QString &interface, uin
mTouchExtension.reset(new QWaylandTouchExtension(this, id));
} else if (interface == QStringLiteral("zqt_key_v1")) {
mQtKeyExtension.reset(new QWaylandQtKeyExtension(this, id));
} else if (interface == QStringLiteral("zwp_tablet_manager_v2")) {
mTabletManager.reset(new QWaylandTabletManagerV2(this, id, qMin(1, int(version))));
#if QT_CONFIG(wayland_client_primary_selection)
} else if (interface == QStringLiteral("zwp_primary_selection_device_manager_v1")) {
mPrimarySelectionManager.reset(new QWaylandPrimarySelectionDeviceManagerV1(this, id, 1));

View File

@ -97,6 +97,7 @@ class QWaylandDataDeviceManager;
#if QT_CONFIG(wayland_client_primary_selection)
class QWaylandPrimarySelectionDeviceManagerV1;
#endif
class QWaylandTabletManagerV2;
class QWaylandTouchExtension;
class QWaylandQtKeyExtension;
class QWaylandWindow;
@ -160,6 +161,7 @@ public:
QWaylandPrimarySelectionDeviceManagerV1 *primarySelectionManager() const { return mPrimarySelectionManager.data(); }
#endif
QtWayland::qt_surface_extension *windowExtension() const { return mWindowExtension.data(); }
QWaylandTabletManagerV2 *tabletManager() const { return mTabletManager.data(); }
QWaylandTouchExtension *touchExtension() const { return mTouchExtension.data(); }
QtWayland::zwp_text_input_manager_v2 *textInputManager() const { return mTextInputManager.data(); }
QWaylandHardwareIntegration *hardwareIntegration() const { return mHardwareIntegration.data(); }
@ -247,6 +249,7 @@ private:
QScopedPointer<QWaylandTouchExtension> mTouchExtension;
QScopedPointer<QWaylandQtKeyExtension> mQtKeyExtension;
QScopedPointer<QWaylandWindowManagerIntegration> mWindowManagerIntegration;
QScopedPointer<QWaylandTabletManagerV2> mTabletManager;
#if QT_CONFIG(wayland_client_primary_selection)
QScopedPointer<QWaylandPrimarySelectionDeviceManagerV1> mPrimarySelectionManager;
#endif

View File

@ -50,6 +50,7 @@
#if QT_CONFIG(wayland_client_primary_selection)
#include "qwaylandprimaryselectionv1_p.h"
#endif
#include "qwaylandtabletv2_p.h"
#include "qwaylandtouch_p.h"
#include "qwaylandscreen_p.h"
#include "qwaylandcursor_p.h"
@ -418,6 +419,8 @@ QWaylandInputDevice::QWaylandInputDevice(QWaylandDisplay *display, int version,
if (mQDisplay->textInputManager())
mTextInput.reset(new QWaylandTextInput(mQDisplay, mQDisplay->textInputManager()->get_text_input(wl_seat())));
if (auto *tm = mQDisplay->tabletManager())
mTabletSeat.reset(new QWaylandTabletSeatV2(tm, this));
}
QWaylandInputDevice::~QWaylandInputDevice()

View File

@ -89,6 +89,7 @@ class QWaylandDisplay;
#if QT_CONFIG(wayland_client_primary_selection)
class QWaylandPrimarySelectionDeviceV1;
#endif
class QWaylandTabletSeatV2;
class QWaylandTextInput;
#if QT_CONFIG(cursor)
class QWaylandCursorTheme;
@ -127,6 +128,9 @@ public:
QWaylandPrimarySelectionDeviceV1 *primarySelectionDevice() const;
#endif
void setTabletSeat(QWaylandTabletSeatV2 *tabletSeat);
QWaylandTabletSeatV2* tabletSeat() const;
void setTextInput(QWaylandTextInput *textInput);
QWaylandTextInput *textInput() const;
@ -183,6 +187,7 @@ private:
Touch *mTouch = nullptr;
QScopedPointer<QWaylandTextInput> mTextInput;
QScopedPointer<QWaylandTabletSeatV2> mTabletSeat;
uint32_t mTime = 0;
uint32_t mSerial = 0;

View File

@ -425,6 +425,8 @@ void QWaylandIntegration::initializeShellIntegration()
qCWarning(lcQpaWayland) << "Loading shell integration failed.";
qCWarning(lcQpaWayland) << "Attempted to load the following shells" << preferredShells;
}
QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false);
}
QWaylandInputDevice *QWaylandIntegration::createInputDevice(QWaylandDisplay *display, int version, uint32_t id)

View File

@ -0,0 +1,332 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** 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-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qwaylandtabletv2_p.h"
#include "qwaylandinputdevice_p.h"
#include "qwaylanddisplay_p.h"
#include "qwaylandsurface_p.h"
QT_BEGIN_NAMESPACE
namespace QtWaylandClient {
QWaylandTabletManagerV2::QWaylandTabletManagerV2(QWaylandDisplay *display, uint id, uint version)
: zwp_tablet_manager_v2(display->wl_registry(), id, qMin(version, uint(1)))
{
// Create tabletSeats for all seats.
// This only works if we get the manager after all seats
const auto seats = display->inputDevices();
for (auto *seat : seats)
createTabletSeat(seat);
}
QWaylandTabletSeatV2 *QWaylandTabletManagerV2::createTabletSeat(QWaylandInputDevice *seat)
{
return new QWaylandTabletSeatV2(this, seat);
}
QWaylandTabletSeatV2::QWaylandTabletSeatV2(QWaylandTabletManagerV2 *manager, QWaylandInputDevice *seat)
: QtWayland::zwp_tablet_seat_v2(manager->get_tablet_seat(seat->wl_seat()))
{
}
QWaylandTabletSeatV2::~QWaylandTabletSeatV2()
{
for (auto *tablet : m_tablets)
tablet->destroy();
for (auto *tool : m_tools)
tool->destroy();
for (auto *pad : m_pads)
pad->destroy();
destroy();
}
void QWaylandTabletSeatV2::zwp_tablet_seat_v2_tablet_added(zwp_tablet_v2 *id)
{
auto *tablet = new QWaylandTabletV2(id);
m_tablets.push_back(tablet);
connect(tablet, &QWaylandTabletV2::destroyed, this, [this, tablet] { m_tablets.removeOne(tablet); });
}
void QWaylandTabletSeatV2::zwp_tablet_seat_v2_tool_added(zwp_tablet_tool_v2 *id)
{
auto *tool = new QWaylandTabletToolV2(id);
m_tools.push_back(tool);
connect(tool, &QWaylandTabletToolV2::destroyed, this, [this, tool] { m_tools.removeOne(tool); });
}
void QWaylandTabletSeatV2::zwp_tablet_seat_v2_pad_added(zwp_tablet_pad_v2 *id)
{
auto *pad = new QWaylandTabletPadV2(id);
m_pads.push_back(pad);
connect(pad, &QWaylandTabletPadV2::destroyed, this, [this, pad] { m_pads.removeOne(pad); });
}
QWaylandTabletV2::QWaylandTabletV2(::zwp_tablet_v2 *tablet)
: QtWayland::zwp_tablet_v2(tablet)
{
}
void QWaylandTabletV2::zwp_tablet_v2_removed()
{
destroy();
delete this;
}
QWaylandTabletToolV2::QWaylandTabletToolV2(::zwp_tablet_tool_v2 *tool)
: QtWayland::zwp_tablet_tool_v2(tool)
{
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_type(uint32_t tool_type)
{
m_toolType = type(tool_type);
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_hardware_serial(uint32_t hardware_serial_hi, uint32_t hardware_serial_lo)
{
m_uid = (quint64(hardware_serial_hi) << 32) + hardware_serial_lo;
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_capability(uint32_t capability)
{
if (capability == capability_rotation)
m_hasRotation = true;
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_done()
{
switch (m_toolType) {
case type::type_airbrush:
case type::type_brush:
case type::type_pencil:
case type::type_pen:
m_pointerType = QTabletEvent::PointerType::Pen;
break;
case type::type_eraser:
m_pointerType = QTabletEvent::PointerType::Eraser;
break;
case type::type_mouse:
case type::type_lens:
m_pointerType = QTabletEvent::PointerType::Cursor;
break;
case type::type_finger:
m_pointerType = QTabletEvent::PointerType::UnknownPointer;
break;
}
switch (m_toolType) {
case type::type_airbrush:
m_tabletDevice = QTabletEvent::TabletDevice::Airbrush;
break;
case type::type_brush:
case type::type_pencil:
case type::type_pen:
case type::type_eraser:
m_tabletDevice = m_hasRotation ? QTabletEvent::TabletDevice::RotationStylus : QTabletEvent::TabletDevice::Stylus;
break;
case type::type_lens:
m_tabletDevice = QTabletEvent::TabletDevice::Puck;
break;
case type::type_mouse:
case type::type_finger:
m_tabletDevice = QTabletEvent::TabletDevice::NoDevice;
break;
}
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_removed()
{
destroy();
delete this;
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_proximity_in(uint32_t serial, zwp_tablet_v2 *tablet, wl_surface *surface)
{
Q_UNUSED(tablet);
Q_UNUSED(serial);
if (Q_UNLIKELY(!surface)) {
qCDebug(lcQpaWayland) << "Ignoring zwp_tablet_tool_v2_proximity_v2 with no surface";
return;
}
m_pending.enteredSurface = true;
m_pending.proximitySurface = QWaylandSurface::fromWlSurface(surface);
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_proximity_out()
{
m_pending.enteredSurface = false;
m_pending.proximitySurface = nullptr;
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_down(uint32_t serial)
{
Q_UNUSED(serial);
m_pending.down = true;
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_up()
{
m_pending.down = false;
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_motion(wl_fixed_t x, wl_fixed_t y)
{
m_pending.surfacePosition = QPointF(wl_fixed_to_double(x), wl_fixed_to_double(y));
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_pressure(uint32_t pressure)
{
const int maxPressure = 65535;
m_pending.pressure = qreal(pressure)/maxPressure;
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_distance(uint32_t distance)
{
m_pending.distance = distance;
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_tilt(wl_fixed_t tilt_x, wl_fixed_t tilt_y)
{
m_pending.xTilt = wl_fixed_to_double(tilt_x);
m_pending.yTilt = wl_fixed_to_double(tilt_y);
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_rotation(wl_fixed_t degrees)
{
m_pending.rotation = wl_fixed_to_double(degrees);
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_slider(int32_t position)
{
m_pending.slider = qreal(position) / 65535;
}
static Qt::MouseButton mouseButtonFromTablet(uint button)
{
switch (button) {
case 0x110: return Qt::MouseButton::LeftButton; // BTN_LEFT
case 0x14b: return Qt::MouseButton::MiddleButton; // BTN_STYLUS
case 0x14c: return Qt::MouseButton::RightButton; // BTN_STYLUS2
default:
return Qt::NoButton;
}
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_button(uint32_t serial, uint32_t button, uint32_t state)
{
Q_UNUSED(serial);
Qt::MouseButton mouseButton = mouseButtonFromTablet(button);
if (state == button_state_pressed)
m_pending.buttons |= mouseButton;
else
m_pending.buttons &= ~mouseButton;
}
void QWaylandTabletToolV2::zwp_tablet_tool_v2_frame(uint32_t time)
{
if (m_pending.proximitySurface && !m_applied.proximitySurface) {
QWindowSystemInterface::handleTabletEnterProximityEvent(m_tabletDevice, m_pointerType, m_uid);
m_applied.proximitySurface = m_pending.proximitySurface;
}
if (!(m_pending == m_applied) && m_pending.proximitySurface) {
if (!m_pending.proximitySurface) {
qCWarning(lcQpaWayland) << "Can't send tablet event with no proximity surface, ignoring";
return;
}
QWaylandWindow *waylandWindow = QWaylandWindow::fromWlSurface(m_pending.proximitySurface->object());
QWindow *window = waylandWindow->window();
ulong timestamp = time;
const QPointF localPosition = waylandWindow->mapFromWlSurface(m_pending.surfacePosition);
QPointF delta = localPosition - localPosition.toPoint();
QPointF globalPosition = window->mapToGlobal(localPosition.toPoint());
globalPosition += delta;
Qt::MouseButtons buttons = m_pending.down ? Qt::MouseButton::LeftButton : Qt::MouseButton::NoButton;
buttons |= m_pending.buttons;
qreal pressure = m_pending.pressure;
int xTilt = int(m_pending.xTilt);
int yTilt = int(m_pending.yTilt);
qreal tangentialPressure = m_pending.slider;
qreal rotation = m_pending.rotation;
int z = int(m_pending.distance);
QWindowSystemInterface::handleTabletEvent(window, timestamp, localPosition, globalPosition,
m_tabletDevice, m_pointerType, buttons, pressure,
xTilt, yTilt, tangentialPressure, rotation, z, m_uid);
}
if (!m_pending.proximitySurface && m_applied.enteredSurface) {
QWindowSystemInterface::handleTabletLeaveProximityEvent(m_tabletDevice, m_pointerType, m_uid);
m_pending = State(); // Don't leave pressure etc. lying around when we enter the next surface
}
m_applied = m_pending;
}
// TODO: delete when upgrading to c++20
bool QWaylandTabletToolV2::State::operator==(const QWaylandTabletToolV2::State &o) const {
return
down == o.down &&
proximitySurface.data() == o.proximitySurface.data() &&
enteredSurface == o.enteredSurface &&
surfacePosition == o.surfacePosition &&
distance == o.distance &&
pressure == o.pressure &&
rotation == o.rotation &&
xTilt == o.xTilt &&
yTilt == o.yTilt &&
slider == o.slider &&
buttons == o.buttons;
}
QWaylandTabletPadV2::QWaylandTabletPadV2(::zwp_tablet_pad_v2 *pad)
: QtWayland::zwp_tablet_pad_v2(pad)
{
}
void QWaylandTabletPadV2::zwp_tablet_pad_v2_removed()
{
destroy();
delete this;
}
} // namespace QtWaylandClient
QT_END_NAMESPACE

View File

@ -0,0 +1,191 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** 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-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef QWAYLANDTABLETV2_P_H
#define QWAYLANDTABLETV2_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtWaylandClient/private/qwayland-tablet-unstable-v2.h>
#include <QtWaylandClient/private/qtwaylandclientglobal_p.h>
#include <QtGui/QTabletEvent>
#include <QtCore/QObject>
#include <QtCore/QPointer>
#include <QtCore/QPointF>
QT_BEGIN_NAMESPACE
namespace QtWaylandClient {
class QWaylandDisplay;
class QWaylandInputDevice;
class QWaylandSurface;
class QWaylandTabletSeatV2;
class QWaylandTabletV2;
class QWaylandTabletToolV2;
class QWaylandTabletPadV2;
class Q_WAYLAND_CLIENT_EXPORT QWaylandTabletManagerV2 : public QtWayland::zwp_tablet_manager_v2
{
public:
explicit QWaylandTabletManagerV2(QWaylandDisplay *display, uint id, uint version);
QWaylandTabletSeatV2 *createTabletSeat(QWaylandInputDevice *seat);
};
class Q_WAYLAND_CLIENT_EXPORT QWaylandTabletSeatV2 : public QObject, public QtWayland::zwp_tablet_seat_v2
{
Q_OBJECT
public:
explicit QWaylandTabletSeatV2(QWaylandTabletManagerV2 *manager, QWaylandInputDevice *seat);
~QWaylandTabletSeatV2() override;
protected:
void zwp_tablet_seat_v2_tablet_added(struct ::zwp_tablet_v2 *id) override;
void zwp_tablet_seat_v2_tool_added(struct ::zwp_tablet_tool_v2 *id) override;
void zwp_tablet_seat_v2_pad_added(struct ::zwp_tablet_pad_v2 *id) override;
private:
QVector<QWaylandTabletV2 *> m_tablets;
QVector<QWaylandTabletToolV2 *> m_tools;
QVector<QWaylandTabletPadV2 *> m_pads;
};
class Q_WAYLAND_CLIENT_EXPORT QWaylandTabletV2 : public QObject, public QtWayland::zwp_tablet_v2
{
Q_OBJECT
public:
explicit QWaylandTabletV2(::zwp_tablet_v2 *tablet);
protected:
// void zwp_tablet_v2_name(const QString &name) override;
// void zwp_tablet_v2_id(uint32_t vid, uint32_t pid) override;
// void zwp_tablet_v2_path(const QString &path) override;
// void zwp_tablet_v2_done() override;
void zwp_tablet_v2_removed() override;
};
class Q_WAYLAND_CLIENT_EXPORT QWaylandTabletToolV2 : public QObject, public QtWayland::zwp_tablet_tool_v2
{
Q_OBJECT
public:
explicit QWaylandTabletToolV2(::zwp_tablet_tool_v2 *tool);
protected:
void zwp_tablet_tool_v2_type(uint32_t tool_type) override;
void zwp_tablet_tool_v2_hardware_serial(uint32_t hardware_serial_hi, uint32_t hardware_serial_lo) override;
// void zwp_tablet_tool_v2_hardware_id_wacom(uint32_t hardware_id_hi, uint32_t hardware_id_lo) override;
void zwp_tablet_tool_v2_capability(uint32_t capability) override;
void zwp_tablet_tool_v2_done() override;
void zwp_tablet_tool_v2_removed() override;
void zwp_tablet_tool_v2_proximity_in(uint32_t serial, struct ::zwp_tablet_v2 *tablet, struct ::wl_surface *surface) override;
void zwp_tablet_tool_v2_proximity_out() override;
void zwp_tablet_tool_v2_down(uint32_t serial) override;
void zwp_tablet_tool_v2_up() override;
void zwp_tablet_tool_v2_motion(wl_fixed_t x, wl_fixed_t y) override;
void zwp_tablet_tool_v2_pressure(uint32_t pressure) override;
void zwp_tablet_tool_v2_distance(uint32_t distance) override;
void zwp_tablet_tool_v2_tilt(wl_fixed_t tilt_x, wl_fixed_t tilt_y) override;
void zwp_tablet_tool_v2_rotation(wl_fixed_t degrees) override;
void zwp_tablet_tool_v2_slider(int32_t position) override;
// void zwp_tablet_tool_v2_wheel(wl_fixed_t degrees, int32_t clicks) override;
void zwp_tablet_tool_v2_button(uint32_t serial, uint32_t button, uint32_t state) override;
void zwp_tablet_tool_v2_frame(uint32_t time) override;
private:
// Static state (sent before done event)
QTabletEvent::PointerType m_pointerType = QTabletEvent::PointerType::UnknownPointer;
QTabletEvent::TabletDevice m_tabletDevice = QTabletEvent::TabletDevice::NoDevice;
type m_toolType = type_pen;
bool m_hasRotation = false;
quint64 m_uid = 0;
// Accumulated state (applied on frame event)
struct State {
bool down = false;
QPointer<QWaylandSurface> proximitySurface;
bool enteredSurface = false; // Not enough with just proximitySurface, if the surface is deleted, we still want to send a leave event
QPointF surfacePosition;
uint distance = 0;
qreal pressure = 0;
qreal rotation = 0;
qreal xTilt = 0;
qreal yTilt = 0;
qreal slider = 0;
Qt::MouseButtons buttons = Qt::MouseButton::NoButton; // Actual buttons, down state -> left mouse is mapped inside the frame handler
//auto operator<=>(const Point&) const = default; // TODO: use this when upgrading to C++20
bool operator==(const State &o) const;
} m_pending, m_applied;
};
// We don't actually use this, but need to handle the "removed" event to comply with the protocol
class Q_WAYLAND_CLIENT_EXPORT QWaylandTabletPadV2 : public QObject, public QtWayland::zwp_tablet_pad_v2
{
Q_OBJECT
public:
explicit QWaylandTabletPadV2(::zwp_tablet_pad_v2 *pad);
protected:
// void zwp_tablet_pad_v2_group(struct ::zwp_tablet_pad_group_v2 *pad_group) override;
// void zwp_tablet_pad_v2_path(const QString &path) override;
// void zwp_tablet_pad_v2_buttons(uint32_t buttons) override;
// void zwp_tablet_pad_v2_done() override;
// void zwp_tablet_pad_v2_button(uint32_t time, uint32_t button, uint32_t state) override;
// void zwp_tablet_pad_v2_enter(uint32_t serial, struct ::zwp_tablet_v2 *tablet, struct ::wl_surface *surface) override;
// void zwp_tablet_pad_v2_leave(uint32_t serial, struct ::wl_surface *surface) override;
void zwp_tablet_pad_v2_removed() override;
};
} // namespace QtWaylandClient
QT_END_NAMESPACE
#endif // QWAYLANDTABLETV2_P_H

View File

@ -11,6 +11,7 @@ SUBDIRS += \
seatv4 \
seatv5 \
surface \
tabletv2 \
wl_connect \
xdgdecorationv1 \
xdgoutput \

View File

@ -0,0 +1,7 @@
include (../shared/shared.pri)
WAYLANDSERVERSOURCES += \
$$PWD/../../../../src/3rdparty/protocol/tablet-unstable-v2.xml
TARGET = tst_tabletv2
SOURCES += tst_tabletv2.cpp

View File

@ -0,0 +1,918 @@
/****************************************************************************
**
** Copyright (C) 2019 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 <qwayland-server-tablet-unstable-v2.h>
#include <QtGui/QRasterWindow>
using namespace MockCompositor;
constexpr int tabletVersion = 1; // protocol VERSION, not the name suffix (_v2)
class TabletManagerV2;
class TabletSeatV2;
class TabletV2 : public QObject, public QtWaylandServer::zwp_tablet_v2
{
Q_OBJECT
public:
explicit TabletV2(TabletSeatV2 *tabletSeat)
: m_tabletSeat(tabletSeat)
{
}
void send_removed() = delete;
void send_removed(struct ::wl_resource *resource) = delete;
void sendRemoved();
QPointer<TabletSeatV2> m_tabletSeat; // destroy order is not guaranteed
protected:
void zwp_tablet_v2_destroy(Resource *resource) override;
};
class TabletToolV2 : public QObject, public QtWaylandServer::zwp_tablet_tool_v2
{
Q_OBJECT
public:
using ToolType = QtWaylandServer::zwp_tablet_tool_v2::type;
explicit TabletToolV2(TabletSeatV2 *tabletSeat, ToolType toolType, quint64 hardwareSerial)
: m_tabletSeat(tabletSeat)
, m_toolType(toolType)
, m_hardwareSerial(hardwareSerial)
{
}
wl_resource *toolResource() // for convenience
{
Q_ASSERT(resourceMap().size() == 1);
// Strictly speaking, there may be more than one resource for the tool, for intsance if
// if there are multiple clients, or a client has called get_tablet_seat multiple times.
// For now we'll pretend there can only be one resource.
return resourceMap().first()->handle;
}
void send_removed() = delete;
void send_removed(struct ::wl_resource *resource) = delete;
void sendRemoved();
uint sendProximityIn(TabletV2 *tablet, Surface *surface);
void sendProximityOut();
void sendMotion(QPointF position)
{
Q_ASSERT(m_proximitySurface);
for (auto *resource : resourceMap())
send_motion(resource->handle, wl_fixed_from_double(position.x()), wl_fixed_from_double(position.y()));
}
uint sendDown();
void sendUp() { send_up(toolResource()); }
void sendPressure(uint pressure);
void sendTilt(qreal tiltX, qreal tiltY) { send_tilt(toolResource(), wl_fixed_from_double(tiltX), wl_fixed_from_double(tiltY)); }
void sendRotation(qreal rotation) { send_rotation(toolResource(), wl_fixed_from_double(rotation)); }
uint sendButton(uint button, bool pressed);
uint sendFrame();
QPointer<TabletSeatV2> m_tabletSeat; // destruction order is not guaranteed
ToolType m_toolType = ToolType::type_pen;
quint64 m_hardwareSerial = 0;
QPointer<Surface> m_proximitySurface;
protected:
void zwp_tablet_tool_v2_destroy(Resource *resource) override;
};
class TabletPadV2 : public QObject, public QtWaylandServer::zwp_tablet_pad_v2
{
Q_OBJECT
public:
explicit TabletPadV2(TabletSeatV2 *tabletSeat)
: m_tabletSeat(tabletSeat)
{
}
void send_removed() = delete;
void send_removed(struct ::wl_resource *resource) = delete;
void sendRemoved();
QPointer<TabletSeatV2> m_tabletSeat; // destroy order is not guaranteed
protected:
void zwp_tablet_pad_v2_destroy(Resource *resource) override;
};
class TabletSeatV2 : public QObject, public QtWaylandServer::zwp_tablet_seat_v2
{
Q_OBJECT
public:
explicit TabletSeatV2(TabletManagerV2 *manager, Seat *seat)
: m_manager(manager)
, m_seat(seat)
{}
TabletV2 *addTablet()
{
auto *tablet = new TabletV2(this);
m_tablets.append(tablet);
for (auto *resource : resourceMap())
sendTabletAdded(resource, tablet);
return tablet;
}
void sendTabletAdded(Resource *resource, TabletV2 *tablet)
{
// Although, not necessarily correct, assuming just one tablet_seat per client
auto *tabletResource = tablet->add(resource->client(), resource->version());
zwp_tablet_seat_v2::send_tablet_added(resource->handle, tabletResource->handle);
// TODO: send extra stuff before done?
tablet->send_done(tabletResource->handle);
}
using ToolType = QtWaylandServer::zwp_tablet_tool_v2::type;
TabletToolV2 *addTool(ToolType toolType = ToolType::type_pen, quint64 hardwareSerial = 0)
{
auto *tool = new TabletToolV2(this, toolType, hardwareSerial);
m_tools.append(tool);
for (auto *resource : resourceMap())
sendToolAdded(resource, tool);
return tool;
}
void sendToolAdded(Resource *resource, TabletToolV2 *tool)
{
// Although, not necessarily correct, assuming just one tablet_seat per client
auto *toolResource = tool->add(resource->client(), resource->version())->handle;
zwp_tablet_seat_v2::send_tool_added(resource->handle, toolResource);
tool->send_type(toolResource, tool->m_toolType);
if (tool->m_hardwareSerial) {
const uint hi = tool->m_hardwareSerial >> 32;
const uint lo = tool->m_hardwareSerial & 0xffffffff;
tool->send_hardware_serial(toolResource, hi, lo);
}
tool->send_done(toolResource);
}
TabletPadV2 *addPad()
{
auto *pad = new TabletPadV2(this);
m_pads.append(pad);
for (auto *resource : resourceMap())
sendPadAdded(resource, pad);
return pad;
}
void sendPadAdded(Resource *resource, TabletPadV2 *pad)
{
// Although, not necessarily correct, assuming just one tablet_seat per client
auto *padResource = pad->add(resource->client(), resource->version())->handle;
zwp_tablet_seat_v2::send_pad_added(resource->handle, padResource);
pad->send_done(padResource);
}
void removeAll()
{
const auto tools = m_tools;
for (auto *tool : tools)
tool->sendRemoved();
const auto tablets = m_tablets;
for (auto *tablet : tablets)
tablet->sendRemoved();
const auto pads = m_pads;
for (auto *pad : pads)
pad->sendRemoved();
}
TabletManagerV2 *m_manager = nullptr;
Seat *m_seat = nullptr;
QVector<TabletV2 *> m_tablets;
QVector<TabletV2 *> m_tabletsWaitingForDestroy;
QVector<TabletToolV2 *> m_tools;
QVector<TabletToolV2 *> m_toolsWaitingForDestroy;
QVector<TabletPadV2 *> m_pads;
QVector<TabletPadV2 *> m_padsWaitingForDestroy;
protected:
void zwp_tablet_seat_v2_bind_resource(Resource *resource)
{
for (auto *tablet : m_tablets)
sendTabletAdded(resource, tablet);
for (auto *tool : m_tools)
sendToolAdded(resource, tool);
for (auto *pad : m_pads)
sendPadAdded(resource, pad);
}
};
class TabletManagerV2 : public Global, public QtWaylandServer::zwp_tablet_manager_v2
{
Q_OBJECT
public:
explicit TabletManagerV2(CoreCompositor *compositor, int version = 1)
: QtWaylandServer::zwp_tablet_manager_v2(compositor->m_display, version)
, m_version(version)
{}
bool isClean() override
{
for (auto *seat : m_tabletSeats) {
if (!seat->m_tabletsWaitingForDestroy.empty())
return false;
if (!seat->m_toolsWaitingForDestroy.empty())
return false;
if (!seat->m_padsWaitingForDestroy.empty())
return false;
}
return true;
}
TabletSeatV2 *tabletSeatFor(Seat *seat)
{
Q_ASSERT(seat);
if (auto *tabletSeat = m_tabletSeats.value(seat, nullptr))
return tabletSeat;
auto *tabletSeat = new TabletSeatV2(this, seat);
m_tabletSeats[seat] = tabletSeat;
return tabletSeat;
}
int m_version = 1; // TODO: Remove on libwayland upgrade
QMap<Seat *, TabletSeatV2 *> m_tabletSeats;
protected:
void zwp_tablet_manager_v2_destroy(Resource *resource) override
{
// tablet_seats created from this object are unaffected and should be destroyed separately.
wl_resource_destroy(resource->handle);
}
void zwp_tablet_manager_v2_get_tablet_seat(Resource *resource, uint32_t id, ::wl_resource *seatResource) override
{
auto *seat = fromResource<Seat>(seatResource);
QVERIFY(seat);
auto *tabletSeat = tabletSeatFor(seat);
tabletSeat->add(resource->client(), id, resource->version());
}
};
void TabletV2::sendRemoved()
{
for (auto *resource : resourceMap())
zwp_tablet_v2_send_removed(resource->handle);
bool removed = m_tabletSeat->m_tablets.removeOne(this);
QVERIFY(removed);
m_tabletSeat->m_tabletsWaitingForDestroy.append(this);
}
void TabletV2::zwp_tablet_v2_destroy(QtWaylandServer::zwp_tablet_v2::Resource *resource)
{
Q_UNUSED(resource)
if (m_tabletSeat) {
bool removed = m_tabletSeat->m_tabletsWaitingForDestroy.removeOne(this);
QVERIFY(removed);
}
wl_resource_destroy(resource->handle);
}
void TabletToolV2::sendRemoved()
{
for (auto *resource : resourceMap())
zwp_tablet_tool_v2_send_removed(resource->handle);
bool removed = m_tabletSeat->m_tools.removeOne(this);
QVERIFY(removed);
m_tabletSeat->m_toolsWaitingForDestroy.append(this);
}
uint TabletToolV2::sendProximityIn(TabletV2 *tablet, Surface *surface)
{
Q_ASSERT(!m_proximitySurface);
m_proximitySurface = surface;
uint serial = m_tabletSeat->m_seat->m_compositor->nextSerial();
auto *client = surface->resource()->client();
auto tabletResource = tablet->resourceMap().value(client)->handle;
send_proximity_in(toolResource(), serial, tabletResource, surface->resource()->handle);
return serial;
}
void TabletToolV2::sendProximityOut()
{
Q_ASSERT(m_proximitySurface);
send_proximity_out(toolResource());
m_proximitySurface = nullptr;
}
uint TabletToolV2::sendDown()
{
uint serial = m_tabletSeat->m_seat->m_compositor->nextSerial();
send_down(toolResource(), serial);
return serial;
}
void TabletToolV2::sendPressure(uint pressure)
{
Q_ASSERT(m_proximitySurface);
auto *client = m_proximitySurface->resource()->client();
auto toolResource = resourceMap().value(client)->handle;
send_pressure(toolResource, pressure);
}
uint TabletToolV2::sendButton(uint button, bool pressed)
{
button_state state = pressed ? button_state_pressed : button_state_released;
uint serial = m_tabletSeat->m_seat->m_compositor->nextSerial();
send_button(toolResource(), serial, button, state);
return serial;
}
uint TabletToolV2::sendFrame()
{
uint time = m_tabletSeat->m_seat->m_compositor->currentTimeMilliseconds();
for (auto *resource : resourceMap())
send_frame(resource->handle, time);
return time;
}
void TabletToolV2::zwp_tablet_tool_v2_destroy(QtWaylandServer::zwp_tablet_tool_v2::Resource *resource)
{
if (m_tabletSeat) {
bool removed = m_tabletSeat->m_toolsWaitingForDestroy.removeOne(this);
QVERIFY(removed);
}
wl_resource_destroy(resource->handle);
}
void TabletPadV2::sendRemoved()
{
for (auto *resource : resourceMap())
zwp_tablet_pad_v2_send_removed(resource->handle);
bool removed = m_tabletSeat->m_pads.removeOne(this);
QVERIFY(removed);
m_tabletSeat->m_padsWaitingForDestroy.append(this);
}
void TabletPadV2::zwp_tablet_pad_v2_destroy(QtWaylandServer::zwp_tablet_pad_v2::Resource *resource)
{
if (m_tabletSeat) {
bool removed = m_tabletSeat->m_padsWaitingForDestroy.removeOne(this);
QVERIFY(removed);
}
wl_resource_destroy(resource->handle);
}
class TabletCompositor : public DefaultCompositor {
public:
explicit TabletCompositor()
{
exec([this] {
m_config.autoConfigure = true;
add<TabletManagerV2>(tabletVersion);
});
}
TabletSeatV2 *tabletSeat(int i = 0)
{
return get<TabletManagerV2>()->tabletSeatFor(get<Seat>(i));
}
TabletV2 *tablet(int i = 0, int iSeat = 0)
{
if (auto *ts = tabletSeat(iSeat))
return ts->m_tablets.value(i, nullptr);
return nullptr;
}
TabletToolV2 *tabletTool(int i = 0, int iSeat = 0)
{
if (auto *ts = tabletSeat(iSeat))
return ts->m_tools.value(i, nullptr);
return nullptr;
}
TabletPadV2 *tabletPad(int i = 0, int iSeat = 0)
{
if (auto *ts = tabletSeat(iSeat))
return ts->m_pads.value(i, nullptr);
return nullptr;
}
};
Q_DECLARE_METATYPE(QtWaylandServer::zwp_tablet_tool_v2::type);
Q_DECLARE_METATYPE(QTabletEvent::PointerType);
Q_DECLARE_METATYPE(Qt::MouseButton);
class tst_tabletv2 : public QObject, private TabletCompositor
{
using ToolType = QtWaylandServer::zwp_tablet_tool_v2::type;
Q_OBJECT
private slots:
void cleanup();
void bindsToManager();
void createsTabletSeat();
void destroysTablet();
void destroysTool();
void destroysPad();
void proximityEvents();
void moveEvent();
void pointerType_data();
void pointerType();
void hardwareSerial();
void buttons_data();
void buttons();
void tabletEvents();
};
class ProximityFilter : public QObject {
Q_OBJECT
public:
ProximityFilter() { qApp->installEventFilter(this); }
~ProximityFilter() override { qDeleteAll(m_events); }
QVector<QTabletEvent *> m_events;
int nextEventIndex = 0;
int numEvents() const { return m_events.size() - nextEventIndex; }
QTabletEvent *popEvent()
{
auto *event = m_events.value(nextEventIndex, nullptr);
if (event)
++nextEventIndex;
return event;
}
protected:
bool eventFilter(QObject *object, QEvent *event) override
{
Q_UNUSED(object);
switch (event->type()) {
case QEvent::TabletEnterProximity:
case QEvent::TabletLeaveProximity: {
auto *e = static_cast<QTabletEvent *>(event);
auto *ev = new QTabletEvent(e->type(), e->posF(), e->globalPosF(), e->device(),
e->pointerType(), e->pressure(), e->xTilt(), e->yTilt(),
e->tangentialPressure(), e->rotation(), e->z(),
Qt::KeyboardModifier::NoModifier, e->uniqueId(),
e->button(), e->buttons());
m_events << ev;
break;
}
default:
break;
}
return false;
}
};
void tst_tabletv2::cleanup()
{
exec([&] {
tabletSeat()->removeAll();
});
QCOMPOSITOR_COMPARE(get<TabletManagerV2>()->m_tabletSeats.size(), 1);
QCOMPOSITOR_COMPARE(tabletSeat()->m_tablets.size(), 0);
QCOMPOSITOR_COMPARE(tabletSeat()->m_tools.size(), 0);
QCOMPOSITOR_COMPARE(tabletSeat()->m_pads.size(), 0);
QTRY_VERIFY2(isClean(), qPrintable(dirtyMessage()));
}
void tst_tabletv2::bindsToManager()
{
QCOMPOSITOR_TRY_COMPARE(get<TabletManagerV2>()->resourceMap().size(), 1);
QCOMPOSITOR_TRY_COMPARE(get<TabletManagerV2>()->resourceMap().first()->version(), tabletVersion);
}
void tst_tabletv2::createsTabletSeat()
{
QCOMPOSITOR_TRY_VERIFY(tabletSeat());
QCOMPOSITOR_TRY_VERIFY(tabletSeat()->resourceMap().contains(client()));
QCOMPOSITOR_TRY_COMPARE(tabletSeat()->resourceMap().value(client())->version(), tabletVersion);
//TODO: Maybe also assert some capability reported though qt APIs?
}
void tst_tabletv2::destroysTablet()
{
QCOMPOSITOR_TRY_VERIFY(tabletSeat());
exec([&] {
tabletSeat()->addTablet();
});
QCOMPOSITOR_TRY_VERIFY(tablet());
exec([&] {
tablet()->sendRemoved();
});
QCOMPOSITOR_TRY_VERIFY(!tablet());
QCOMPOSITOR_TRY_VERIFY(tabletSeat()->m_tabletsWaitingForDestroy.empty());
}
void tst_tabletv2::destroysTool()
{
QCOMPOSITOR_TRY_VERIFY(tabletSeat());
exec([&] {
tabletSeat()->addTool();
});
QCOMPOSITOR_TRY_VERIFY(tabletTool());
exec([&] {
tabletTool()->sendRemoved();
});
QCOMPOSITOR_TRY_VERIFY(!tabletTool());
QCOMPOSITOR_TRY_VERIFY(tabletSeat()->m_toolsWaitingForDestroy.empty());
}
void tst_tabletv2::destroysPad()
{
QCOMPOSITOR_TRY_VERIFY(tabletSeat());
exec([&] {
tabletSeat()->addPad();
});
QCOMPOSITOR_TRY_VERIFY(tabletPad());
exec([&] {
tabletPad()->sendRemoved();
});
QCOMPOSITOR_TRY_VERIFY(!tabletPad());
QCOMPOSITOR_TRY_VERIFY(tabletSeat()->m_padsWaitingForDestroy.empty());
}
void tst_tabletv2::proximityEvents()
{
ProximityFilter filter;
QCOMPOSITOR_TRY_VERIFY(tabletSeat());
exec([&] {
tabletSeat()->addTablet();
tabletSeat()->addTool();
});
QRasterWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
QCOMPOSITOR_TRY_VERIFY(tablet());
exec([&] {
auto *surface = xdgSurface()->m_surface;
auto *tool = tabletTool();
tool->sendProximityIn(tablet(), surface);
tool->sendFrame();
});
QTRY_COMPARE(filter.numEvents(), 1);
QTabletEvent *enterEvent = filter.popEvent();
QCOMPARE(enterEvent->type(), QEvent::TabletEnterProximity);
exec([&] {
auto *tool = tabletTool();
tool->sendProximityOut();
tool->sendFrame();
});
QTRY_COMPARE(filter.numEvents(), 1);
QTabletEvent *leaveEvent = filter.popEvent();
QCOMPARE(leaveEvent->type(), QEvent::TabletLeaveProximity);
}
class TabletWindow : public QRasterWindow {
Q_OBJECT
public:
~TabletWindow() override { qDeleteAll(m_events); }
void tabletEvent(QTabletEvent *e) override
{
m_events << new QTabletEvent(e->type(), e->posF(), e->globalPosF(), e->device(),
e->pointerType(), e->pressure(), e->xTilt(), e->yTilt(),
e->tangentialPressure(), e->rotation(), e->z(),
Qt::KeyboardModifier::NoModifier, e->uniqueId(), e->button(),
e->buttons());
emit tabletEventReceived(m_events.last());
}
int nextEventIndex = 0;
int numEvents() const { return m_events.size() - nextEventIndex; }
QTabletEvent *popEvent()
{
auto *event = m_events.value(nextEventIndex, nullptr);
if (event)
++nextEventIndex;
return event;
}
signals:
void tabletEventReceived(QTabletEvent *event);
private:
QVector<QTabletEvent *> m_events;
};
void tst_tabletv2::moveEvent()
{
QCOMPOSITOR_TRY_VERIFY(tabletSeat());
exec([&] {
tabletSeat()->addTablet();
tabletSeat()->addTool();
});
TabletWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
QCOMPOSITOR_TRY_VERIFY(tablet());
exec([&] {
auto *surface = xdgSurface()->m_surface;
auto *tool = tabletTool();
tool->sendProximityIn(tablet(), surface);
QMargins margins = window.frameMargins();
tool->sendMotion(QPointF(12 + margins.left(), 34 + margins.top()));
tool->sendFrame();
});
QTRY_VERIFY(window.numEvents());
QTabletEvent *event = window.popEvent();
QCOMPARE(event->type(), QEvent::TabletMove);
QCOMPARE(event->pressure(), 0);
QCOMPARE(event->posF(), QPointF(12, 34));
}
void tst_tabletv2::pointerType_data()
{
QTest::addColumn<ToolType>("toolType");
QTest::addColumn<QTabletEvent::PointerType>("pointerType");
QTest::addColumn<QTabletEvent::TabletDevice>("tabletDevice");
QTest::newRow("pen") << ToolType::type_pen << QTabletEvent::PointerType::Pen << QTabletEvent::TabletDevice::Stylus;
QTest::newRow("eraser") << ToolType::type_eraser << QTabletEvent::PointerType::Eraser << QTabletEvent::TabletDevice::Stylus;
QTest::newRow("pencil") << ToolType::type_pencil << QTabletEvent::PointerType::Pen << QTabletEvent::TabletDevice::Stylus;
QTest::newRow("airbrush") << ToolType::type_airbrush << QTabletEvent::PointerType::Pen << QTabletEvent::TabletDevice::Airbrush;
QTest::newRow("brush") << ToolType::type_brush << QTabletEvent::PointerType::Pen << QTabletEvent::TabletDevice::Stylus; // TODO: is TabletDevice::Stylus the right thing?
QTest::newRow("lens") << ToolType::type_lens << QTabletEvent::PointerType::Cursor << QTabletEvent::TabletDevice::Puck;
// TODO: also add tests for FourDMouse and RotationStylus (also need to send capabilities)
// TODO: should these rather be mapped to touch/mouse events?
QTest::newRow("finger") << ToolType::type_finger << QTabletEvent::PointerType::UnknownPointer << QTabletEvent::TabletDevice::NoDevice;
QTest::newRow("mouse") << ToolType::type_mouse << QTabletEvent::PointerType::Cursor << QTabletEvent::TabletDevice::NoDevice;
}
void tst_tabletv2::pointerType()
{
using ToolType = QtWaylandServer::zwp_tablet_tool_v2::type;
QFETCH(ToolType, toolType);
QFETCH(QTabletEvent::PointerType, pointerType);
QFETCH(QTabletEvent::TabletDevice, tabletDevice);
ProximityFilter filter;
QCOMPOSITOR_TRY_VERIFY(tabletSeat());
exec([&] {
tabletSeat()->addTablet();
tabletSeat()->addTool(toolType);
});
TabletWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
QCOMPOSITOR_TRY_VERIFY(tablet());
exec([&] {
auto *surface = xdgSurface()->m_surface;
auto *tool = tabletTool();
tool->sendProximityIn(tablet(), surface);
QMargins margins = window.frameMargins();
tool->sendMotion(QPointF(12 + margins.left(), 34 + margins.top()));
tool->sendFrame();
});
QTRY_COMPARE(filter.numEvents(), 1);
QTabletEvent *event = filter.popEvent();
QCOMPARE(event->pointerType(), pointerType);
QCOMPARE(event->device(), tabletDevice);
QTRY_VERIFY(window.numEvents());
event = window.popEvent();
QCOMPARE(event->pointerType(), pointerType);
QCOMPARE(event->device(), tabletDevice);
exec([&] {
tabletTool()->sendProximityOut();
tabletTool()->sendFrame();
});
QTRY_VERIFY(filter.numEvents());
event = filter.popEvent();
QCOMPARE(event->pointerType(), pointerType);
QCOMPARE(event->device(), tabletDevice);
}
void tst_tabletv2::hardwareSerial()
{
ProximityFilter filter;
const quint64 uid = 0xbaba15dead15f00d;
QCOMPOSITOR_TRY_VERIFY(tabletSeat());
exec([&] {
tabletSeat()->addTablet();
tabletSeat()->addTool(ToolType::type_pen, uid);
});
TabletWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
QCOMPOSITOR_TRY_VERIFY(tablet());
exec([&] {
auto *surface = xdgSurface()->m_surface;
auto *tool = tabletTool();
tool->sendProximityIn(tablet(), surface);
QMargins margins = window.frameMargins();
tool->sendMotion(QPointF(12 + margins.left(), 34 + margins.top()));
tool->sendFrame();
});
QTRY_COMPARE(filter.numEvents(), 1);
QTabletEvent *event = filter.popEvent();
QCOMPARE(event->uniqueId(), uid);
QTRY_VERIFY(window.numEvents());
event = window.popEvent();
QCOMPARE(event->uniqueId(), uid);
exec([&] {
tabletTool()->sendProximityOut();
tabletTool()->sendFrame();
});
QTRY_VERIFY(filter.numEvents());
event = filter.popEvent();
QCOMPARE(event->uniqueId(), uid);
}
// As defined in linux/input-event-codes.h
#ifndef BTN_STYLUS
#define BTN_STYLUS 0x14b
#endif
#ifndef BTN_STYLUS2
#define BTN_STYLUS2 0x14c
#endif
void tst_tabletv2::buttons_data()
{
QTest::addColumn<uint>("tabletButton");
QTest::addColumn<Qt::MouseButton>("mouseButton");
QTest::newRow("BTN_STYLUS2") << uint(BTN_STYLUS2) << Qt::MouseButton::RightButton;
QTest::newRow("BTN_STYLUS") << uint(BTN_STYLUS) << Qt::MouseButton::MiddleButton;
}
void tst_tabletv2::buttons()
{
QFETCH(uint, tabletButton);
QFETCH(Qt::MouseButton, mouseButton);
QCOMPOSITOR_TRY_VERIFY(tabletSeat());
exec([&] {
tabletSeat()->addTablet();
tabletSeat()->addTool();
});
TabletWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
QCOMPOSITOR_TRY_VERIFY(tablet());
exec([&] {
tabletTool()->sendProximityIn(tablet(), xdgSurface()->m_surface);
QMargins margins = window.frameMargins();
tabletTool()->sendMotion(QPointF(12 + margins.left(), 34 + margins.top()));
tabletTool()->sendFrame();
});
QTRY_VERIFY(window.numEvents());
window.popEvent();
QCOMPOSITOR_TRY_VERIFY(tablet());
exec([&] {
tabletTool()->sendButton(tabletButton, true);
tabletTool()->sendFrame();
tabletTool()->sendButton(tabletButton, false);
tabletTool()->sendFrame();
});
QTRY_VERIFY(window.numEvents());
QTabletEvent *event = window.popEvent();
QCOMPARE(event->buttons(), mouseButton);
exec([&] {
tabletTool()->sendProximityOut();
tabletTool()->sendFrame();
});
}
void tst_tabletv2::tabletEvents()
{
QCOMPOSITOR_TRY_VERIFY(tabletSeat());
exec([&] {
tabletSeat()->addTablet();
tabletSeat()->addTool();
});
TabletWindow window;
window.resize(64, 64);
window.show();
QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);
const QPointF insideDecorations(window.frameMargins().left(), window.frameMargins().top());
QCOMPOSITOR_TRY_VERIFY(tablet());
exec([&] {
auto *surface = xdgSurface()->m_surface;
auto *tool = tabletTool();
// TODO: encapsulate this into a helper function?
tool->sendProximityIn(tablet(), surface);
tool->sendMotion(QPointF(12, 34) + insideDecorations);
tool->sendDown();
tool->sendPressure(65535);
tool->sendFrame();
});
QTRY_VERIFY(window.numEvents());
QTabletEvent *event = window.popEvent();
QCOMPARE(event->type(), QEvent::TabletPress);
QCOMPARE(event->pressure(), 1.0);
QCOMPARE(event->posF(), QPointF(12, 34));
// Values we didn't send should be 0
QCOMPARE(event->rotation(), 0);
QCOMPARE(event->xTilt(), 0);
QCOMPARE(event->yTilt(), 0);
exec([&] {
tabletTool()->sendMotion(QPointF(45, 56) + insideDecorations);
tabletTool()->sendPressure(65535/2);
tabletTool()->sendRotation(90);
tabletTool()->sendTilt(13, 37);
tabletTool()->sendFrame();
});
QTRY_VERIFY(window.numEvents());
event = window.popEvent();
QCOMPARE(event->type(), QEvent::TabletMove);
QVERIFY(qAbs(event->pressure() - 0.5) < 0.01);
QVERIFY(qAbs(event->rotation() - 90) < 0.01);
QVERIFY(qAbs(event->xTilt() - 13) < 0.01);
QVERIFY(qAbs(event->yTilt() - 37) < 0.01);
QCOMPARE(event->posF(), QPointF(45, 56));
// Verify that the values stay the same if we don't update them
exec([&] {
tabletTool()->sendMotion(QPointF(10, 11) + insideDecorations); // Change position only
tabletTool()->sendFrame();
});
QTRY_VERIFY(window.numEvents());
event = window.popEvent();
QCOMPARE(event->type(), QEvent::TabletMove);
QVERIFY(qAbs(event->pressure() - 0.5) < 0.01);
QVERIFY(qAbs(event->rotation() - 90) < 0.01);
QVERIFY(qAbs(event->xTilt() - 13) < 0.01);
QVERIFY(qAbs(event->yTilt() - 37) < 0.01);
QCOMPARE(event->posF(), QPointF(10, 11));
exec([&] {
tabletTool()->sendPressure(0);
tabletTool()->sendUp();
tabletTool()->sendFrame();
tabletTool()->sendProximityOut();
tabletTool()->sendFrame();
});
QTRY_VERIFY(window.numEvents());
event = window.popEvent();
QCOMPARE(event->type(), QEvent::TabletRelease);
QCOMPARE(event->pressure(), 0);
QCOMPARE(event->posF(), QPointF(10, 11));
}
QCOMPOSITOR_TEST_MAIN(tst_tabletv2)
#include "tst_tabletv2.moc"