kwin/autotests/wayland/server/test_tablet_interface.cpp
Aki Sakurai 115ea7454b Send tablet events to every bound wl_resource
Some apps, notably Tablet KCM, will rebind the tablet interface and consume all events that should be sent to the GUI toolkit.
This commit sends events to every consumer and also stores the latest cursor indexed by wl_client instead of wl_resource.
BUG:473126
2023-10-24 11:22:27 +00:00

387 lines
12 KiB
C++

/*
SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
// Qt
#include <QHash>
#include <QSignalSpy>
#include <QTest>
#include <QThread>
// WaylandServer
#include "wayland/compositor.h"
#include "wayland/display.h"
#include "wayland/seat.h"
#include "wayland/tablet_v2.h"
#include "KWayland/Client/compositor.h"
#include "KWayland/Client/connection_thread.h"
#include "KWayland/Client/event_queue.h"
#include "KWayland/Client/registry.h"
#include "KWayland/Client/seat.h"
#include "KWayland/Client/surface.h"
#include "qwayland-tablet-unstable-v2.h"
using namespace KWin;
using namespace std::literals;
class Tablet : public QtWayland::zwp_tablet_v2
{
public:
Tablet(::zwp_tablet_v2 *t)
: QtWayland::zwp_tablet_v2(t)
{
}
};
class TabletPad : public QObject, public QtWayland::zwp_tablet_pad_v2
{
Q_OBJECT
public:
TabletPad(::zwp_tablet_pad_v2 *t)
: QtWayland::zwp_tablet_pad_v2(t)
{
}
void zwp_tablet_pad_v2_done() override
{
Q_ASSERT(!doneCalled);
doneCalled = true;
}
void zwp_tablet_pad_v2_buttons(uint32_t buttons) override
{
Q_ASSERT(buttons == 1);
}
void zwp_tablet_pad_v2_enter(uint32_t /*serial*/, struct ::zwp_tablet_v2 * /*tablet*/, struct ::wl_surface *surface) override
{
m_currentSurface = surface;
}
void zwp_tablet_pad_v2_button(uint32_t /*time*/, uint32_t button, uint32_t state) override
{
buttonStates[m_currentSurface][button] = state;
Q_EMIT buttonReceived();
}
::wl_surface *m_currentSurface = nullptr;
bool doneCalled = false;
QHash<::wl_surface *, QHash<uint32_t, uint32_t>> buttonStates;
Q_SIGNALS:
void buttonReceived();
};
class Tool : public QObject, public QtWayland::zwp_tablet_tool_v2
{
Q_OBJECT
public:
Tool(::zwp_tablet_tool_v2 *t)
: QtWayland::zwp_tablet_tool_v2(t)
{
}
void zwp_tablet_tool_v2_proximity_in(uint32_t /*serial*/, struct ::zwp_tablet_v2 * /*tablet*/, struct ::wl_surface *surface) override
{
surfaceApproximated[surface]++;
}
void zwp_tablet_tool_v2_frame(uint32_t time) override
{
Q_EMIT frame(time);
}
QHash<struct ::wl_surface *, int> surfaceApproximated;
Q_SIGNALS:
void frame(quint32 time);
};
class TabletSeat : public QObject, public QtWayland::zwp_tablet_seat_v2
{
Q_OBJECT
public:
TabletSeat(::zwp_tablet_seat_v2 *seat)
: QtWayland::zwp_tablet_seat_v2(seat)
{
}
void zwp_tablet_seat_v2_tablet_added(struct ::zwp_tablet_v2 *id) override
{
m_tablets << new Tablet(id);
Q_EMIT tabletAdded();
}
void zwp_tablet_seat_v2_tool_added(struct ::zwp_tablet_tool_v2 *id) override
{
m_tools << new Tool(id);
Q_EMIT toolAdded();
}
void zwp_tablet_seat_v2_pad_added(struct ::zwp_tablet_pad_v2 *id) override
{
m_pads << new TabletPad(id);
Q_EMIT padAdded();
}
QList<Tablet *> m_tablets;
QList<TabletPad *> m_pads;
QList<Tool *> m_tools;
Q_SIGNALS:
void padAdded();
void toolAdded();
void tabletAdded();
};
class TestTabletInterface : public QObject
{
Q_OBJECT
public:
TestTabletInterface()
{
}
~TestTabletInterface() override;
private Q_SLOTS:
void initTestCase();
void testAdd();
void testAddPad();
void testInteractSimple_data();
void testInteractSimple();
void testInteractSurfaceChange_data();
void testInteractSurfaceChange();
private:
KWayland::Client::ConnectionThread *m_connection;
KWayland::Client::EventQueue *m_queue;
KWayland::Client::Compositor *m_clientCompositor;
KWayland::Client::Seat *m_clientSeat = nullptr;
QThread *m_thread;
KWin::Display m_display;
SeatInterface *m_seat;
CompositorInterface *m_serverCompositor;
TabletSeat *m_tabletSeatClient = nullptr;
TabletSeat *m_tabletSeatClient2 = nullptr;
TabletManagerV2Interface *m_tabletManager;
QList<KWayland::Client::Surface *> m_surfacesClient;
TabletV2Interface *m_tablet;
TabletPadV2Interface *m_tabletPad = nullptr;
TabletToolV2Interface *m_tool;
QList<SurfaceInterface *> m_surfaces;
};
static const QString s_socketName = QStringLiteral("kwin-wayland-server-tablet-test-0");
void TestTabletInterface::initTestCase()
{
m_display.addSocketName(s_socketName);
m_display.start();
QVERIFY(m_display.isRunning());
m_seat = new SeatInterface(&m_display, this);
m_serverCompositor = new CompositorInterface(&m_display, this);
m_tabletManager = new TabletManagerV2Interface(&m_display, this);
connect(m_serverCompositor, &CompositorInterface::surfaceCreated, this, [this](SurfaceInterface *surface) {
m_surfaces += surface;
});
// setup connection
m_connection = new KWayland::Client::ConnectionThread;
QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected);
m_connection->setSocketName(s_socketName);
m_thread = new QThread(this);
m_connection->moveToThread(m_thread);
m_thread->start();
m_connection->initConnection();
QVERIFY(connectedSpy.wait());
QVERIFY(!m_connection->connections().isEmpty());
m_queue = new KWayland::Client::EventQueue(this);
QVERIFY(!m_queue->isValid());
m_queue->setup(m_connection);
QVERIFY(m_queue->isValid());
auto registry = new KWayland::Client::Registry(this);
connect(registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, registry](const QByteArray &interface, quint32 name, quint32 version) {
if (interface == "zwp_tablet_manager_v2") {
auto tabletClient = new QtWayland::zwp_tablet_manager_v2(registry->registry(), name, version);
auto _seat = tabletClient->get_tablet_seat(*m_clientSeat);
m_tabletSeatClient = new TabletSeat(_seat);
auto _seat2 = tabletClient->get_tablet_seat(*m_clientSeat);
m_tabletSeatClient2 = new TabletSeat(_seat2);
}
});
connect(registry, &KWayland::Client::Registry::seatAnnounced, this, [this, registry](quint32 name, quint32 version) {
m_clientSeat = registry->createSeat(name, version);
});
registry->setEventQueue(m_queue);
QSignalSpy compositorSpy(registry, &KWayland::Client::Registry::compositorAnnounced);
registry->create(m_connection->display());
QVERIFY(registry->isValid());
registry->setup();
wl_display_flush(m_connection->display());
QVERIFY(compositorSpy.wait());
m_clientCompositor = registry->createCompositor(compositorSpy.first().first().value<quint32>(), compositorSpy.first().last().value<quint32>(), this);
QVERIFY(m_clientCompositor->isValid());
QSignalSpy surfaceSpy(m_serverCompositor, &CompositorInterface::surfaceCreated);
for (int i = 0; i < 3; ++i) {
m_surfacesClient += m_clientCompositor->createSurface(this);
}
QVERIFY(surfaceSpy.count() < 3 && surfaceSpy.wait(200));
QVERIFY(m_surfaces.count() == 3);
QVERIFY(m_tabletSeatClient);
}
TestTabletInterface::~TestTabletInterface()
{
if (m_queue) {
delete m_queue;
m_queue = nullptr;
}
if (m_thread) {
m_thread->quit();
m_thread->wait();
delete m_thread;
m_thread = nullptr;
}
delete m_tabletSeatClient;
delete m_tabletSeatClient2;
m_connection->deleteLater();
m_connection = nullptr;
}
void TestTabletInterface::testAdd()
{
TabletSeatV2Interface *seatInterface = m_tabletManager->seat(m_seat);
QVERIFY(seatInterface);
QSignalSpy tabletSpy(m_tabletSeatClient, &TabletSeat::tabletAdded);
m_tablet = seatInterface->addTablet(1, 2, QStringLiteral("event33"), QStringLiteral("my tablet"), {QStringLiteral("/test/event33")});
QVERIFY(m_tablet);
QVERIFY(tabletSpy.wait() || tabletSpy.count() == 1);
QCOMPARE(m_tabletSeatClient->m_tablets.count(), 1);
QSignalSpy toolSpy(m_tabletSeatClient, &TabletSeat::toolAdded);
m_tool = seatInterface->addTool(KWin::TabletToolV2Interface::Pen, 0, 0, {TabletToolV2Interface::Tilt, TabletToolV2Interface::Pressure}, "my tablet");
QVERIFY(m_tool);
QVERIFY(toolSpy.wait() || toolSpy.count() == 1);
QCOMPARE(m_tabletSeatClient->m_tools.count(), 1);
QVERIFY(!m_tool->isClientSupported()); // There's no surface in it yet
m_tool->setCurrentSurface(nullptr);
QVERIFY(!m_tool->isClientSupported()); // There's no surface in it
QCOMPARE(m_surfaces.count(), 3);
for (SurfaceInterface *surface : m_surfaces) {
m_tool->setCurrentSurface(surface);
}
m_tool->setCurrentSurface(nullptr);
}
void TestTabletInterface::testAddPad()
{
TabletSeatV2Interface *seatInterface = m_tabletManager->seat(m_seat);
QVERIFY(seatInterface);
QSignalSpy tabletPadSpy(m_tabletSeatClient, &TabletSeat::padAdded);
m_tabletPad =
seatInterface->addTabletPad(QStringLiteral("my tablet pad"), QStringLiteral("tabletpad"), {QStringLiteral("/test/event33")}, 1, 1, 1, 1, 0, m_tablet);
QVERIFY(m_tabletPad);
QVERIFY(tabletPadSpy.wait() || tabletPadSpy.count() == 1);
QCOMPARE(m_tabletSeatClient->m_pads.count(), 1);
QVERIFY(m_tabletSeatClient->m_pads[0]);
QVERIFY(m_tabletPad->ring(0));
QVERIFY(m_tabletPad->strip(0));
QCOMPARE(m_surfaces.count(), 3);
QVERIFY(m_tabletSeatClient->m_pads[0]->buttonStates.isEmpty());
QSignalSpy buttonSpy(m_tabletSeatClient->m_pads[0], &TabletPad::buttonReceived);
m_tabletPad->setCurrentSurface(m_surfaces[0], m_tablet);
m_tabletPad->sendButton(123ms, 0, QtWayland::zwp_tablet_pad_v2::button_state_pressed);
QVERIFY(buttonSpy.count() || buttonSpy.wait(100));
QCOMPARE(m_tabletSeatClient->m_pads[0]->doneCalled, true);
QCOMPARE(m_tabletSeatClient->m_pads[0]->buttonStates.count(), 1);
QCOMPARE(m_tabletSeatClient->m_pads[0]->buttonStates[*m_surfacesClient[0]][0], QtWayland::zwp_tablet_pad_v2::button_state_pressed);
}
static uint s_serial = 0;
void TestTabletInterface::testInteractSimple_data()
{
QTest::addColumn<TabletSeat *>("tabletSeatClient");
QTest::newRow("first client") << m_tabletSeatClient;
QTest::newRow("second client") << m_tabletSeatClient2;
}
void TestTabletInterface::testInteractSimple()
{
QFETCH(TabletSeat *, tabletSeatClient);
tabletSeatClient->m_tools[0]->surfaceApproximated.clear();
QSignalSpy frameSpy(tabletSeatClient->m_tools[0], &Tool::frame);
QVERIFY(!m_tool->isClientSupported());
m_tool->setCurrentSurface(m_surfaces[0]);
QVERIFY(m_tool->isClientSupported() && m_tablet->isSurfaceSupported(m_surfaces[0]));
m_tool->sendProximityIn(m_tablet);
m_tool->sendPressure(0);
m_tool->sendFrame(s_serial++);
m_tool->sendMotion({3, 3});
m_tool->sendFrame(s_serial++);
m_tool->sendProximityOut();
QVERIFY(m_tool->isClientSupported());
m_tool->sendFrame(s_serial++);
QVERIFY(!m_tool->isClientSupported());
QVERIFY(frameSpy.wait(500));
QCOMPARE(tabletSeatClient->m_tools[0]->surfaceApproximated.count(), 1);
}
void TestTabletInterface::testInteractSurfaceChange_data()
{
QTest::addColumn<TabletSeat *>("tabletSeatClient");
QTest::newRow("first client") << m_tabletSeatClient;
QTest::newRow("second client") << m_tabletSeatClient2;
}
void TestTabletInterface::testInteractSurfaceChange()
{
QFETCH(TabletSeat *, tabletSeatClient);
tabletSeatClient->m_tools[0]->surfaceApproximated.clear();
QSignalSpy frameSpy(tabletSeatClient->m_tools[0], &Tool::frame);
QVERIFY(!m_tool->isClientSupported());
m_tool->setCurrentSurface(m_surfaces[0]);
QVERIFY(m_tool->isClientSupported() && m_tablet->isSurfaceSupported(m_surfaces[0]));
m_tool->sendProximityIn(m_tablet);
m_tool->sendPressure(0);
m_tool->sendFrame(s_serial++);
m_tool->setCurrentSurface(m_surfaces[1]);
QVERIFY(m_tool->isClientSupported());
m_tool->sendMotion({3, 3});
m_tool->sendFrame(s_serial++);
m_tool->sendProximityOut();
QVERIFY(m_tool->isClientSupported());
m_tool->sendFrame(s_serial++);
QVERIFY(!m_tool->isClientSupported());
QVERIFY(frameSpy.wait(500));
QCOMPARE(tabletSeatClient->m_tools[0]->surfaceApproximated.count(), 2);
}
QTEST_GUILESS_MAIN(TestTabletInterface)
#include "test_tablet_interface.moc"