diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,9 @@ # Dependencies set(REQUIRED_QT_VERSION 5.12.0) -find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Concurrent Gui) +find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Concurrent Gui WaylandClient) + +find_package(QtWaylandScanner REQUIRED) find_package(Wayland 1.15 COMPONENTS Client Server) set_package_properties(Wayland PROPERTIES @@ -64,6 +66,8 @@ if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/po") ecm_install_po_files_as_qm(po) endif() + +find_package(QtWaylandScanner REQUIRED) add_subdirectory(src) if (BUILD_TESTING) diff --git a/autotests/server/CMakeLists.txt b/autotests/server/CMakeLists.txt --- a/autotests/server/CMakeLists.txt +++ b/autotests/server/CMakeLists.txt @@ -45,3 +45,15 @@ target_link_libraries( testNoXdgRuntimeDir Qt5::Test KF5::WaylandServer) add_test(NAME kwayland-testNoXdgRuntimeDir COMMAND testNoXdgRuntimeDir) ecm_mark_as_test(testNoXdgRuntimeDir) + +######################################################## +# Test Tablet Interface +######################################################## +ecm_add_qtwayland_client_protocol(TABLET_SRCS + PROTOCOL ${WaylandProtocols_DATADIR}/unstable/tablet/tablet-unstable-v2.xml + BASENAME tablet-unstable-v2 +) +add_executable(testTabletInterface test_tablet_interface.cpp ${TABLET_SRCS}) +target_link_libraries( testTabletInterface Qt5::Test KF5::WaylandServer KF5::WaylandClient Wayland::Client) +add_test(NAME kwayland-testTabletInterface COMMAND testTabletInterface) +ecm_mark_as_test(testTabletInterface) diff --git a/autotests/server/test_tablet_interface.cpp b/autotests/server/test_tablet_interface.cpp new file mode 100644 --- /dev/null +++ b/autotests/server/test_tablet_interface.cpp @@ -0,0 +1,296 @@ +/******************************************************************** +Copyright 2020 Aleix Pol Gonzalez + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) version 3, or any +later version accepted by the membership of KDE e.V. (or its +successor approved by the membership of KDE e.V.), which shall +act as a proxy defined in Section 6 of version 3 of the license. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ +// Qt +#include +#include +#include +// WaylandServer +#include "../../src/server/compositor_interface.h" +#include "../../src/server/display.h" +#include "../../src/server/seat_interface.h" +#include "../../src/server/tablet_interface.h" + +#include "../../src/client/compositor.h" +#include "../../src/client/connection_thread.h" +#include "../../src/client/event_queue.h" +#include "../../src/client/registry.h" +#include "../../src/client/seat.h" + +#include "qwayland-tablet-unstable-v2.h" + +using namespace KWayland::Server; + +class Tablet : public QtWayland::zwp_tablet_v2 +{ +public: + Tablet(::zwp_tablet_v2 *t) + : QtWayland::zwp_tablet_v2(t) + { + } +}; + +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 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(); + } + + QVector m_tablets; + QVector m_tools; + +Q_SIGNALS: + void toolAdded(); + void tabletAdded(); +}; + +class TestTabletInterface : public QObject +{ + Q_OBJECT +public: + TestTabletInterface() + { + } + ~TestTabletInterface() override; + +private Q_SLOTS: + void initTestCase(); + void testAdd(); + void testInteractSimple(); + 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; + Display m_display; + SeatInterface *m_seat; + CompositorInterface *m_serverCompositor; + + TabletSeat *m_tabletSeatClient = nullptr; + TabletManagerInterface *m_tabletManager; + + TabletInterface *m_tablet; + TabletToolInterface *m_tool; + + QVector m_surfaces; +}; + +static const QString s_socketName = QStringLiteral("kwin-wayland-server-tablet-test-0"); + +void TestTabletInterface::initTestCase() +{ + m_display.setSocketName(s_socketName); + m_display.start(); + QVERIFY(m_display.isRunning()); + + m_seat = m_display.createSeat(this); + m_seat->create(); + m_serverCompositor = m_display.createCompositor(this); + m_serverCompositor->create(); + m_tabletManager = m_display.createTabletManagerInterface(this); + QVERIFY(m_serverCompositor->isValid()); + 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); + } + }); + 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(), compositorSpy.first().last().value(), this); + QVERIFY(m_clientCompositor->isValid()); + + QSignalSpy surfaceSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + for (int i = 0; i < 3; ++i) { + 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; + m_connection->deleteLater(); + m_connection = nullptr; +} + +void TestTabletInterface::testAdd() +{ + TabletSeatInterface *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(KWayland::Server::TabletToolInterface::Pen, 0, 0, {TabletToolInterface::Tilt, TabletToolInterface::Pressure}); + 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); +} + +static uint s_serial = 0; +void TestTabletInterface::testInteractSimple() +{ + QSignalSpy frameSpy(m_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(m_tabletSeatClient->m_tools[0]->surfaceApproximated.count(), 1); +} + +void TestTabletInterface::testInteractSurfaceChange() +{ + m_tabletSeatClient->m_tools[0]->surfaceApproximated.clear(); + QSignalSpy frameSpy(m_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(m_tabletSeatClient->m_tools[0]->surfaceApproximated.count(), 2); +} + +QTEST_GUILESS_MAIN(TestTabletInterface) +#include "test_tablet_interface.moc" diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -50,6 +50,7 @@ subcompositor_interface.cpp surface_interface.cpp surfacerole.cpp + tablet_interface.cpp textinput_interface.cpp textinput_interface_v0.cpp textinput_interface_v2.cpp @@ -228,6 +229,11 @@ BASENAME linux-dmabuf-unstable-v1 ) +ecm_add_qtwayland_server_protocol(SERVER_LIB_SRCS + PROTOCOL ${WaylandProtocols_DATADIR}/unstable/tablet/tablet-unstable-v2.xml + BASENAME tablet-unstable-v2 +) + set(SERVER_GENERATED_SRCS ${CMAKE_CURRENT_BINARY_DIR}/wayland-blur-client-protocol.h ${CMAKE_CURRENT_BINARY_DIR}/wayland-blur-server-protocol.h @@ -285,6 +291,8 @@ ${CMAKE_CURRENT_BINARY_DIR}/wayland-xdg-shell-server-protocol.h ${CMAKE_CURRENT_BINARY_DIR}/wayland-xdg-shell-v6-client-protocol.h ${CMAKE_CURRENT_BINARY_DIR}/wayland-xdg-shell-v6-server-protocol.h + ${CMAKE_CURRENT_BINARY_DIR}/qwayland-server-tablet-unstable-v2.h + ${CMAKE_CURRENT_BINARY_DIR}/qwayland-server-tablet-unstable-v2.cpp ) set_source_files_properties(${SERVER_GENERATED_SRCS} PROPERTIES SKIP_AUTOMOC ON) @@ -369,6 +377,7 @@ slide_interface.h subcompositor_interface.h surface_interface.h + tablet_interface.h textinput_interface.h touch_interface.h xdgdecoration_interface.h diff --git a/src/server/display.h b/src/server/display.h --- a/src/server/display.h +++ b/src/server/display.h @@ -79,6 +79,7 @@ class EglStreamControllerInterface; class KeyStateInterface; class LinuxDmabufUnstableV1Interface; +class TabletManagerInterface; /** * @brief Class holding the Wayland server display loop. @@ -310,6 +311,13 @@ */ EglStreamControllerInterface *createEglStreamControllerInterface(QObject *parent = nullptr); + /** + * Creates the entry point to support wacom-like tablets and pens. + * + * @since 5.67 + */ + TabletManagerInterface *createTabletManagerInterface(QObject *parent = nullptr); + /** * Gets the ClientConnection for the given @p client. * If there is no ClientConnection yet for the given @p client, it will be created. diff --git a/src/server/display.cpp b/src/server/display.cpp --- a/src/server/display.cpp +++ b/src/server/display.cpp @@ -5,45 +5,46 @@ SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL */ #include "display.h" +#include "appmenu_interface.h" +#include "blur_interface.h" #include "compositor_interface.h" +#include "contrast_interface.h" #include "datadevicemanager_interface.h" #include "dpms_interface.h" -#include "outputconfiguration_interface.h" -#include "outputmanagement_interface.h" -#include "outputdevice_interface.h" +#include "eglstream_controller_interface.h" +#include "fakeinput_interface.h" #include "idle_interface.h" #include "idleinhibit_interface_p.h" -#include "remote_access_interface.h" -#include "fakeinput_interface.h" +#include "keystate_interface.h" +#include "linuxdmabuf_v1_interface.h" #include "logging.h" #include "output_interface.h" +#include "outputconfiguration_interface.h" +#include "outputdevice_interface.h" +#include "outputmanagement_interface.h" #include "plasmashell_interface.h" +#include "plasmavirtualdesktop_interface.h" #include "plasmawindowmanagement_interface.h" #include "pointerconstraints_interface_p.h" #include "pointergestures_interface_p.h" #include "qtsurfaceextension_interface.h" -#include "seat_interface.h" -#include "shadow_interface.h" -#include "blur_interface.h" -#include "contrast_interface.h" #include "relativepointer_interface_p.h" +#include "remote_access_interface.h" +#include "seat_interface.h" #include "server_decoration_interface.h" -#include "slide_interface.h" +#include "server_decoration_palette_interface.h" +#include "shadow_interface.h" #include "shell_interface.h" +#include "slide_interface.h" #include "subcompositor_interface.h" +#include "tablet_interface.h" #include "textinput_interface_p.h" -#include "xdgshell_v5_interface_p.h" +#include "xdgdecoration_interface.h" #include "xdgforeign_interface.h" -#include "xdgshell_v6_interface_p.h" -#include "xdgshell_stable_interface_p.h" -#include "appmenu_interface.h" -#include "server_decoration_palette_interface.h" -#include "plasmavirtualdesktop_interface.h" #include "xdgoutput_interface.h" -#include "xdgdecoration_interface.h" -#include "eglstream_controller_interface.h" -#include "keystate_interface.h" -#include "linuxdmabuf_v1_interface.h" +#include "xdgshell_stable_interface_p.h" +#include "xdgshell_v5_interface_p.h" +#include "xdgshell_v6_interface_p.h" #include #include @@ -523,6 +524,13 @@ return d; } +TabletManagerInterface *Display::createTabletManagerInterface(QObject *parent) +{ + auto d = new TabletManagerInterface(this, parent); + connect(this, &Display::aboutToTerminate, d, [d] { delete d; }); + return d; +} + void Display::createShm() { Q_ASSERT(d->display); diff --git a/src/server/tablet_interface.h b/src/server/tablet_interface.h new file mode 100644 --- /dev/null +++ b/src/server/tablet_interface.h @@ -0,0 +1,194 @@ +/******************************************************************** +Copyright 2019 Aleix Pol Gonzalez + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) version 3, or any +later version accepted by the membership of KDE e.V. (or its +successor approved by the membership of KDE e.V.), which shall +act as a proxy defined in Section 6 of version 3 of the license. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ +#ifndef WAYLAND_SERVER_TABLET_INTERFACE_H +#define WAYLAND_SERVER_TABLET_INTERFACE_H + +#include +#include + +#include "resource.h" + +namespace KWayland +{ +namespace Server +{ +class TabletSeatInterface; +class Display; +class SeatInterface; +class SurfaceInterface; +class TabletInterface; +class TabletCursor; + +/** + * This is an implementation of wayland-protocols/unstable/tablet/tablet-unstable-v2.xml + * + * This class is just the means to get a @class TabletSeatInterface, which is + * the class that will have all of the information we need. + * + * @since 5.69 + */ + +class KWAYLANDSERVER_EXPORT TabletManagerInterface : public QObject +{ + Q_OBJECT +public: + virtual ~TabletManagerInterface(); + + TabletSeatInterface *seat(SeatInterface *seat) const; + +private: + friend class Display; + explicit TabletManagerInterface(Display *d, QObject *parent); + class Private; + QScopedPointer d; +}; + +class KWAYLANDSERVER_EXPORT TabletToolInterface : public QObject +{ + Q_OBJECT +public: + virtual ~TabletToolInterface(); + + enum Type { + Pen = 0x140, ///< Pen + Eraser = 0x141, ///< Eraser + Brush = 0x142, ///< Brush + Pencil = 0x143, ///< Pencil + Airbrush = 0x144, ///< Airbrush + Finger = 0x145, ///< Finger + Mouse = 0x146, ///< Mouse + Lens = 0x147, ///< Lens + Totem + }; + Q_ENUM(Type) + + enum Capability { + Tilt = 1, ///< Tilt axeis + Pressure = 2, ///< Pressure axis + Distance = 3, ///< Distance axis + Rotation = 4, ///< Z-rotation axis + Slider = 5, ///< Slider axis + Wheel = 6 ///< Wheel axis + }; + Q_ENUM(Capability) + + /** + * Sets the surface the events will be sent to. + * + * Make sure the surface supports being sent events to. + * + * @see TabletInterface::isSurfaceSupported + */ + void setCurrentSurface(SurfaceInterface *surface); + bool isClientSupported() const; + + void sendRemoved(); + void sendProximityIn(TabletInterface *tablet); + void sendProximityOut(); + void sendUp(); + void sendDown(); + void sendPressure(quint32 pressure); + void sendDistance(quint32 distance); + void sendTilt(qreal degreesX, qreal degreesY); + void sendRotation(qreal degrees); + void sendSlider(qint32 position); + void sendWheel(qint32 degrees, qint32 clicks); + void sendButton(quint32 button, bool pressed); + void sendFrame(quint32 time); + void sendMotion(const QPointF &pos); + +Q_SIGNALS: + void cursorChanged(TabletCursor* cursor) const; + +private: + friend class TabletSeatInterface; + explicit TabletToolInterface(Display *display, Type type, quint32 hsh, quint32 hsl, quint32 hih, quint32 hil, const QVector &capability, QObject *parent); + class Private; + QScopedPointer d; +}; + +class KWAYLANDSERVER_EXPORT TabletCursor : public QObject +{ + Q_OBJECT +public: + ~TabletCursor() override; + QPoint hotspot() const; + quint32 enteredSerial() const; + SurfaceInterface* surface() const; + +Q_SIGNALS: + void changed(); + +private: + friend class TabletToolInterface; + TabletCursor(); + class Private; + const QScopedPointer d; +}; + +class KWAYLANDSERVER_EXPORT TabletInterface : public QObject +{ + Q_OBJECT +public: + virtual ~TabletInterface(); + + /** + * @returns true if the surface has been bound to the tablet. + */ + bool isSurfaceSupported(SurfaceInterface *surface) const; + + void sendRemoved(); + +private: + friend class TabletSeatInterface; + friend class TabletToolInterface; + explicit TabletInterface(quint32 vendorId, quint32 productId, const QString &name, const QStringList &paths, QObject *parent); + class Private; + QScopedPointer d; +}; + +class KWAYLANDSERVER_EXPORT TabletSeatInterface : public QObject +{ + Q_OBJECT +public: + virtual ~TabletSeatInterface(); + + TabletInterface *addTablet(quint32 vendorId, quint32 productId, const QString &sysname, const QString &name, const QStringList &paths); + TabletToolInterface *addTool(TabletToolInterface::Type type, quint64 hardwareSerial, quint64 hardwareId, const QVector &capabilities); + + TabletToolInterface *toolByHardwareId(quint64 hardwareId) const; + TabletToolInterface *toolByHardwareSerial(quint64 hardwareSerial) const; + TabletInterface *tabletByName(const QString &sysname) const; + + void removeTablet(const QString &sysname); + +private: + friend class TabletManagerInterface; + explicit TabletSeatInterface(Display *display, QObject *parent); + class Private; + QScopedPointer d; +}; + +} +} + +Q_DECLARE_METATYPE(KWayland::Server::TabletSeatInterface *) + +#endif diff --git a/src/server/tablet_interface.cpp b/src/server/tablet_interface.cpp new file mode 100644 --- /dev/null +++ b/src/server/tablet_interface.cpp @@ -0,0 +1,502 @@ +/******************************************************************** +Copyright 2019 Aleix Pol Gonzalez + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) version 3, or any +later version accepted by the membership of KDE e.V. (or its +successor approved by the membership of KDE e.V.), which shall +act as a proxy defined in Section 6 of version 3 of the license. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ +#include "tablet_interface.h" +#include "display.h" +#include "resource_p.h" +#include "seat_interface.h" +#include "surface_interface.h" + +#include "qwayland-server-tablet-unstable-v2.h" +#include + +using namespace KWayland; +using namespace Server; + +static int s_version = 1; + +class TabletInterface::Private : public QtWaylandServer::zwp_tablet_v2 +{ +public: + Private(TabletInterface* q, uint32_t vendorId, uint32_t productId, const QString name, const QStringList &paths) + : zwp_tablet_v2() + , q(q) + , m_vendorId(vendorId) + , m_productId(productId) + , m_name(name) + , m_paths(paths) + { + } + + wl_resource *resourceForSurface(SurfaceInterface *surface) const + { + ClientConnection *client = surface->client(); + QtWaylandServer::zwp_tablet_v2::Resource *r = resourceMap().value(*client); + return r ? r->handle : nullptr; + } + + void zwp_tablet_v2_destroy_resource(QtWaylandServer::zwp_tablet_v2::Resource * resource) override + { + Q_UNUSED(resource); + if (removed && resourceMap().isEmpty()) { + delete q; + } + } + + TabletInterface *const q; + const uint32_t m_vendorId; + const uint32_t m_productId; + const QString m_name; + const QStringList m_paths; + bool removed = false; +}; + +TabletInterface::TabletInterface(uint32_t vendorId, uint32_t productId, + const QString &name, const QStringList &paths, + QObject *parent) + : QObject(parent) + , d(new Private(this, vendorId, productId, name, paths)) +{ +} + +TabletInterface::~TabletInterface() = default; + +bool TabletInterface::isSurfaceSupported(SurfaceInterface *surface) const +{ + return d->resourceForSurface(surface); +} + +void TabletInterface::sendRemoved() +{ + d->removed = true; + for (QtWaylandServer::zwp_tablet_v2::Resource *resource : d->resourceMap()) { + d->send_removed(resource->handle); + } +} + +class TabletCursor::Private +{ +public: + Private(TabletCursor* q) : q(q) {} + + void update(quint32 serial, SurfaceInterface *surface, const QPoint &hotspot) + { + const bool diff = m_serial != serial && m_surface != surface && m_hotspot != hotspot; + m_serial = serial; + m_surface = surface; + m_hotspot = hotspot; + if (diff) + Q_EMIT q->changed(); + } + + TabletCursor* const q; + + quint32 m_serial = 0; + SurfaceInterface* m_surface = nullptr; + QPoint m_hotspot; +}; + +TabletCursor::TabletCursor() + : QObject() + , d(new Private(this)) +{ +} + +TabletCursor::~TabletCursor() = default; + +QPoint TabletCursor::hotspot() const +{ + return d->m_hotspot; +} + +quint32 TabletCursor::enteredSerial() const +{ + return d->m_serial; +} + +SurfaceInterface* TabletCursor::surface() const +{ + return d->m_surface; +} + +class TabletToolInterface::Private : public QtWaylandServer::zwp_tablet_tool_v2 +{ +public: + Private(TabletToolInterface* q, Display *display, Type type, uint32_t hsh, uint32_t hsl, uint32_t hih, + uint32_t hil, const QVector& capabilities) + : zwp_tablet_tool_v2() + , m_display(display) + , m_type(type) + , m_hardwareSerialHigh(hsh) + , m_hardwareSerialLow(hsl) + , m_hardwareIdHigh(hih) + , m_hardwareIdLow(hil) + , m_capabilities(capabilities) + , q(q) + { + } + + + wl_resource *targetResource() + { + ClientConnection *client = m_surface->client(); + const Resource *r = resourceMap().value(*client); + return r ? r->handle : nullptr; + } + + quint64 hardwareId() const + { + return quint64(quint64(m_hardwareIdHigh) << 32) + m_hardwareIdLow; + } + quint64 hardwareSerial() const + { + return quint64(quint64(m_hardwareSerialHigh) << 32) + m_hardwareSerialLow; + } + + void zwp_tablet_tool_v2_bind_resource(QtWaylandServer::zwp_tablet_tool_v2::Resource * resource) override + { + TabletCursor *&c = m_cursors[resource->handle]; + if (!c) + c = new TabletCursor; + } + + void zwp_tablet_tool_v2_set_cursor(Resource * resource, uint32_t serial, struct ::wl_resource * _surface, int32_t hotspot_x, int32_t hotspot_y) override + { + TabletCursor *c = m_cursors[resource->handle]; + c->d->update(serial, SurfaceInterface::get(_surface), {hotspot_x, hotspot_y}); + if (resource->handle == targetResource()) + q->cursorChanged(c); + } + + void zwp_tablet_tool_v2_destroy_resource(Resource * resource) override { + delete m_cursors.take(resource->handle); + } + + Display *const m_display; + bool m_cleanup = false; + QPointer m_surface; + QPointer m_lastTablet; + const uint32_t m_type; + const uint32_t m_hardwareSerialHigh, m_hardwareSerialLow; + const uint32_t m_hardwareIdHigh, m_hardwareIdLow; + const QVector m_capabilities; + QHash m_cursors; + TabletToolInterface *const q; +}; + +TabletToolInterface::TabletToolInterface(Display *display, Type type, uint32_t hsh, + uint32_t hsl, uint32_t hih, uint32_t hil, + const QVector& capabilities, + QObject *parent) + : QObject(parent) + , d(new Private(this, display, type, hsh, hsl, hih, hil, capabilities)) +{ +} + +TabletToolInterface::~TabletToolInterface() = default; + +void TabletToolInterface::setCurrentSurface(SurfaceInterface *surface) +{ + if (d->m_surface == surface) + return; + + TabletInterface *const lastTablet = d->m_lastTablet; + if (d->m_surface && d->resourceMap().contains(*d->m_surface->client())) { + sendProximityOut(); + sendFrame(0); + } + + d->m_surface = surface; + + if (lastTablet && lastTablet->d->resourceForSurface(surface)) { + sendProximityIn(lastTablet); + } else { + d->m_lastTablet = lastTablet; + } + + Q_EMIT cursorChanged(d->m_cursors.value(d->targetResource())); +} + +bool TabletToolInterface::isClientSupported() const +{ + return d->m_surface && d->targetResource(); +} + +void TabletToolInterface::sendButton(uint32_t button, bool pressed) +{ + d->send_button(d->targetResource(), d->m_display->nextSerial(), button, + pressed ? QtWaylandServer::zwp_tablet_tool_v2::button_state_pressed + : QtWaylandServer::zwp_tablet_tool_v2::button_state_released); +} + +void TabletToolInterface::sendMotion(const QPointF &pos) +{ + d->send_motion(d->targetResource(), wl_fixed_from_double(pos.x()), + wl_fixed_from_double(pos.y())); +} + +void TabletToolInterface::sendDistance(uint32_t distance) +{ + d->send_distance(d->targetResource(), distance); +} + +void TabletToolInterface::sendFrame(uint32_t time) +{ + d->send_frame(d->targetResource(), time); + + if (d->m_cleanup) { + d->m_surface = nullptr; + d->m_lastTablet = nullptr; + d->m_cleanup = false; + } +} + +void TabletToolInterface::sendPressure(uint32_t pressure) +{ + d->send_pressure(d->targetResource(), pressure); +} + +void TabletToolInterface::sendRotation(qreal rotation) +{ + d->send_rotation(d->targetResource(), wl_fixed_from_double(rotation)); +} + +void TabletToolInterface::sendSlider(int32_t position) +{ + d->send_slider(d->targetResource(), position); +} + +void TabletToolInterface::sendTilt(qreal degreesX, qreal degreesY) +{ + d->send_tilt(d->targetResource(), wl_fixed_from_double(degreesX), + wl_fixed_from_double(degreesY)); +} + +void TabletToolInterface::sendWheel(int32_t degrees, int32_t clicks) +{ + d->send_wheel(d->targetResource(), degrees, clicks); +} + +void TabletToolInterface::sendProximityIn(TabletInterface *tablet) +{ + wl_resource* tabletResource = tablet->d->resourceForSurface(d->m_surface); + d->send_proximity_in(d->targetResource(), d->m_display->nextSerial(), + tabletResource, d->m_surface->resource()); + d->m_lastTablet = tablet; +} + +void TabletToolInterface::sendProximityOut() +{ + d->send_proximity_out(d->targetResource()); + d->m_cleanup = true; +} + +void TabletToolInterface::sendDown() +{ + d->send_down(d->targetResource(), d->m_display->nextSerial()); +} + +void TabletToolInterface::sendUp() +{ + d->send_up(d->targetResource()); +} + +void TabletToolInterface::sendRemoved() +{ + for (QtWaylandServer::zwp_tablet_tool_v2::Resource *resource : d->resourceMap()) { + d->send_removed(resource->handle); + } +} + +class TabletSeatInterface::Private : public QtWaylandServer::zwp_tablet_seat_v2 +{ +public: + Private(Display *display, TabletSeatInterface *q) + : zwp_tablet_seat_v2() + , q(q) + , m_display(display) + { + } + + void zwp_tablet_seat_v2_bind_resource(Resource *resource) override + { + for (auto iface : qAsConst(m_tablets)) { + sendTabletAdded(resource, iface); + } + + for (auto *tool : qAsConst(m_tools)) { + sendToolAdded(resource, tool); + } + } + + void sendToolAdded(Resource *resource, TabletToolInterface *tool) + { + wl_resource *toolResource = tool->d->add(resource->client(), resource->version())->handle; + send_tool_added(resource->handle, toolResource); + + tool->d->send_type(toolResource, tool->d->m_type); + tool->d->send_hardware_serial(toolResource, tool->d->m_hardwareSerialHigh, + tool->d->m_hardwareSerialLow); + tool->d->send_hardware_id_wacom(toolResource, tool->d->m_hardwareIdHigh, + tool->d->m_hardwareIdLow); + for (uint32_t cap : qAsConst(tool->d->m_capabilities)) { + tool->d->send_capability(toolResource, cap); + } + tool->d->send_done(toolResource); + } + void sendTabletAdded(Resource *resource, TabletInterface *tablet) + { + wl_resource *tabletResource = tablet->d->add(resource->client(), resource->version())->handle; + send_tablet_added(resource->handle, tabletResource); + + tablet->d->send_name(tabletResource, tablet->d->m_name); + if (tablet->d->m_vendorId && tablet->d->m_productId) { + tablet->d->send_id(tabletResource, tablet->d->m_vendorId, tablet->d->m_productId); + } + for (const QString &path : qAsConst(tablet->d->m_paths)) { + tablet->d->send_path(tabletResource, path); + } + tablet->d->send_done(tabletResource); + } + + TabletSeatInterface *const q; + QVector m_tools; + QHash m_tablets; + Display *const m_display; +}; + +TabletSeatInterface::TabletSeatInterface(Display *display, QObject *parent) + : QObject(parent) + , d(new Private(display, this)) +{ +} + +TabletSeatInterface::~TabletSeatInterface() = default; + +TabletToolInterface *TabletSeatInterface::addTool(TabletToolInterface::Type type, + quint64 hardwareSerial, + quint64 hardwareId, + const QVector &capabilities) +{ + constexpr auto MAX_UINT_32 = std::numeric_limits::max(); + auto tool = new TabletToolInterface(d->m_display, + type, hardwareSerial >> 32, hardwareSerial & MAX_UINT_32, + hardwareId >> 32, hardwareId & MAX_UINT_32, capabilities, this); + for (QtWaylandServer::zwp_tablet_seat_v2::Resource *resource : d->resourceMap()) { + d->sendToolAdded(resource, tool); + } + + d->m_tools.append(tool); + QObject::connect(tool, &QObject::destroyed, this, [this](QObject *object) { + auto tti = static_cast(object); + tti->d->send_removed(); + d->m_tools.removeAll(tti); + }); + return tool; +} + +TabletInterface *TabletSeatInterface::addTablet(uint32_t vendorId, uint32_t productId, + const QString &sysname, + const QString &name, + const QStringList &paths) +{ + auto iface = new TabletInterface(vendorId, productId, name, paths, this); + + for (QtWaylandServer::zwp_tablet_seat_v2::Resource *r : d->resourceMap()) { + d->sendTabletAdded(r, iface); + } + + d->m_tablets[sysname] = iface; + return iface; +} + +void TabletSeatInterface::removeTablet(const QString &sysname) +{ + auto tablet = d->m_tablets.take(sysname); + if (tablet) { + tablet->sendRemoved(); + } +} + +TabletToolInterface *TabletSeatInterface::toolByHardwareId(quint64 hardwareId) const +{ + for (TabletToolInterface *tool : d->m_tools) { + if (tool->d->hardwareId() == hardwareId) + return tool; + } + return nullptr; +} + +TabletToolInterface *TabletSeatInterface::toolByHardwareSerial(quint64 hardwareSerial) const +{ + for (TabletToolInterface *tool : d->m_tools) { + if (tool->d->hardwareSerial() == hardwareSerial) + return tool; + } + return nullptr; +} + +TabletInterface *TabletSeatInterface::tabletByName(const QString &name) const +{ + return d->m_tablets.value(name); +} + +class TabletManagerInterface::Private : public QtWaylandServer::zwp_tablet_manager_v2 +{ +public: + Private(Display *display, TabletManagerInterface *q) + : zwp_tablet_manager_v2(*display, s_version) + , q(q) + , m_display(display) + { + } + + void zwp_tablet_manager_v2_get_tablet_seat(Resource *resource, uint32_t tablet_seat, + struct ::wl_resource *seat_resource) override { + SeatInterface* seat = SeatInterface::get(seat_resource); + TabletSeatInterface *tsi = get(seat); + tsi->d->add(resource->client(), tablet_seat, s_version); + } + + TabletSeatInterface *get(SeatInterface *seat) + { + TabletSeatInterface *&tabletSeat = m_seats[seat]; + if (!tabletSeat) { + tabletSeat = new TabletSeatInterface(m_display, q); + } + return tabletSeat; + } + + TabletManagerInterface *const q; + Display *const m_display; + QHash m_seats; +}; + +TabletManagerInterface::TabletManagerInterface(Display *display, QObject *parent) + : QObject(parent) + , d(new Private(display, this)) +{ +} + +TabletSeatInterface *TabletManagerInterface::seat(SeatInterface *seat) const +{ + return d->get(seat); +} + +TabletManagerInterface::~TabletManagerInterface() = default;