diff --git a/src/3rdparty/wayland/protocols/cursor-shape-v1.xml b/src/3rdparty/wayland/protocols/cursor-shape-v1.xml new file mode 100644 index 00000000000..b6fbe08b78f --- /dev/null +++ b/src/3rdparty/wayland/protocols/cursor-shape-v1.xml @@ -0,0 +1,146 @@ + + + + Copyright 2018 The Chromium Authors + Copyright 2023 Simon Ser + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + This global allows clients to set cursor images by name instead of + creating and attaching buffers. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + Destroy the cursor shape manager. + + + + + + Obtain a wp_cursor_shape_device_v1 for a wl_pointer object. + + + + + + + + Obtain a wp_cursor_shape_device_v1 for a zwp_tablet_tool_v2 object. + + + + + + + + + This interface advertises the list of supported cursor shapes for a + device, and allows clients to set the cursor shape. + + + + + This enum describes cursor shapes. + + The names are taken from the CSS W3C specification: + https://w3c.github.io/csswg-drafts/css-ui/#cursor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Destroy the cursor shape device. + + The device cursor shape remains unchanged. + + + + + + Sets the device cursor to the specified shape. The compositor will + change the cursor image based on the specified shape. + + The cursor actually changes only if the input device focus is one of + the requesting client's surfaces. If any, the previous cursor image + (surface or shape) is replaced. + + The "shape" argument must be a valid enum entry, otherwise the + invalid_shape protocol error is raised. + + This is similar to the wl_pointer.set_cursor and + zwp_tablet_tool_v2.set_cursor requests, but this request accepts a + shape instead of contents in the form of a surface. Clients can mix + set_cursor and set_shape requests. + + The serial parameter must match the latest wl_pointer.enter or + zwp_tablet_tool_v2.proximity_in serial number sent to the client. + Otherwise the request will be ignored. + + + + + + diff --git a/src/plugins/platforms/wayland/CMakeLists.txt b/src/plugins/platforms/wayland/CMakeLists.txt index 47312010fed..d42259fc0c0 100644 --- a/src/plugins/platforms/wayland/CMakeLists.txt +++ b/src/plugins/platforms/wayland/CMakeLists.txt @@ -82,6 +82,7 @@ qt_internal_add_module(WaylandClient qt6_generate_wayland_protocol_client_sources(WaylandClient FILES + ${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/protocol/cursor-shape-v1.xml ${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/protocol/pointer-gestures-unstable-v1.xml ${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/protocol/tablet-unstable-v2.xml ${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/protocol/text-input-unstable-v1.xml diff --git a/src/plugins/platforms/wayland/qwaylandcursor.cpp b/src/plugins/platforms/wayland/qwaylandcursor.cpp index ec17ed21813..83b240ce5c7 100644 --- a/src/plugins/platforms/wayland/qwaylandcursor.cpp +++ b/src/plugins/platforms/wayland/qwaylandcursor.cpp @@ -1,4 +1,5 @@ // Copyright (C) 2016 The Qt Company Ltd. +// Copyright (C) 2023 David Edmundson // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qwaylandcursor_p.h" @@ -206,6 +207,77 @@ wl_cursor *QWaylandCursorTheme::requestCursor(WaylandCursor shape) return waylandCursor; } +QWaylandCursorShape::QWaylandCursorShape(::wp_cursor_shape_device_v1 *object) + : QtWayland::wp_cursor_shape_device_v1(object) +{} + +QWaylandCursorShape::~QWaylandCursorShape() +{ + destroy(); +} + +static QtWayland::wp_cursor_shape_device_v1::shape qtCursorShapeToWaylandShape(Qt::CursorShape cursorShape) +{ + using QtWayland::wp_cursor_shape_device_v1; + + switch (cursorShape) { + case Qt::BlankCursor: + case Qt::CustomCursor: + case Qt::BitmapCursor: + // these should have been handled separately before using the shape protocol + Q_ASSERT(false); + break; + case Qt::ArrowCursor: + return wp_cursor_shape_device_v1::shape_default; + case Qt::SizeVerCursor: + return wp_cursor_shape_device_v1::shape_ns_resize; + case Qt::UpArrowCursor: + return wp_cursor_shape_device_v1::shape_n_resize; + case Qt::SizeHorCursor: + return wp_cursor_shape_device_v1::shape_ew_resize; + case Qt::CrossCursor: + return wp_cursor_shape_device_v1::shape_crosshair; + case Qt::SizeBDiagCursor: + return wp_cursor_shape_device_v1::shape_nesw_resize; + case Qt::IBeamCursor: + return wp_cursor_shape_device_v1::shape_text; + case Qt::SizeFDiagCursor: + return wp_cursor_shape_device_v1::shape_nwse_resize; + case Qt::WaitCursor: + return wp_cursor_shape_device_v1::shape_progress; + case Qt::SizeAllCursor: + return wp_cursor_shape_device_v1::shape_all_scroll; + case Qt::BusyCursor: + return wp_cursor_shape_device_v1::shape_wait; + case Qt::SplitVCursor: + return wp_cursor_shape_device_v1::shape_row_resize; + case Qt::ForbiddenCursor: + return wp_cursor_shape_device_v1::shape_not_allowed; + case Qt::SplitHCursor: + return wp_cursor_shape_device_v1::shape_col_resize; + case Qt::PointingHandCursor: + return wp_cursor_shape_device_v1::shape_pointer; + case Qt::OpenHandCursor: + return wp_cursor_shape_device_v1::shape_grab; + case Qt::WhatsThisCursor: + return wp_cursor_shape_device_v1::shape_help; + case Qt::ClosedHandCursor: + return wp_cursor_shape_device_v1::shape_grabbing; + case Qt::DragMoveCursor: + case Qt::DragCopyCursor: + case Qt::DragLinkCursor: + // drags on wayland are different, the compositor knows + // the drag type and can do something custom + return wp_cursor_shape_device_v1::shape_grab; + } + return wp_cursor_shape_device_v1::shape_default; +} + +void QWaylandCursorShape::setShape(uint32_t serial, Qt::CursorShape shape) +{ + set_shape(serial, qtCursorShapeToWaylandShape(shape)); +} + QWaylandCursor::QWaylandCursor(QWaylandDisplay *display) : mDisplay(display) { diff --git a/src/plugins/platforms/wayland/qwaylandcursor_p.h b/src/plugins/platforms/wayland/qwaylandcursor_p.h index 2334c88d902..8f4a5b7e570 100644 --- a/src/plugins/platforms/wayland/qwaylandcursor_p.h +++ b/src/plugins/platforms/wayland/qwaylandcursor_p.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #if QT_CONFIG(cursor) @@ -87,6 +88,14 @@ protected: wl_cursor *m_cursors[NumWaylandCursors] = {}; }; +class Q_WAYLANDCLIENT_EXPORT QWaylandCursorShape : public QtWayland::wp_cursor_shape_device_v1 +{ +public: + QWaylandCursorShape(struct ::wp_cursor_shape_device_v1 *object); + ~QWaylandCursorShape(); + void setShape(uint32_t serial, Qt::CursorShape shape); +}; + class Q_WAYLANDCLIENT_EXPORT QWaylandCursor : public QPlatformCursor { public: diff --git a/src/plugins/platforms/wayland/qwaylanddisplay.cpp b/src/plugins/platforms/wayland/qwaylanddisplay.cpp index 911004f8b35..c49cb428a4f 100644 --- a/src/plugins/platforms/wayland/qwaylanddisplay.cpp +++ b/src/plugins/platforms/wayland/qwaylanddisplay.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include @@ -755,6 +756,8 @@ void QWaylandDisplay::registry_global(uint32_t id, const QString &interface, uin mFractionalScaleManager.reset(new QtWayland::wp_fractional_scale_manager_v1(registry, id, 1)); } else if (interface == QLatin1String("wp_viewporter")) { mViewporter.reset(new QtWayland::wp_viewporter(registry, id, qMin(1u, version))); + } else if (interface == QLatin1String(QtWayland::wp_cursor_shape_manager_v1::interface()->name)) { + mCursorShapeManager.reset(new QtWayland::wp_cursor_shape_manager_v1(registry, id, std::min(1u, version))); } mGlobals.append(RegistryGlobal(id, interface, version, registry)); diff --git a/src/plugins/platforms/wayland/qwaylanddisplay_p.h b/src/plugins/platforms/wayland/qwaylanddisplay_p.h index 93d4b1d90dc..877ff9692a5 100644 --- a/src/plugins/platforms/wayland/qwaylanddisplay_p.h +++ b/src/plugins/platforms/wayland/qwaylanddisplay_p.h @@ -51,8 +51,9 @@ namespace QtWayland { class zwp_text_input_manager_v2; class zwp_text_input_manager_v4; class qt_text_input_method_manager_v1; - class wp_viewporter; + class wp_cursor_shape_manager_v1; class wp_fractional_scale_manager_v1; + class wp_viewporter; } namespace QtWaylandClient { @@ -152,6 +153,7 @@ public: QWaylandXdgOutputManagerV1 *xdgOutputManager() const { return mXdgOutputManager.data(); } QtWayland::wp_fractional_scale_manager_v1 *fractionalScaleManager() const { return mFractionalScaleManager.data(); } QtWayland::wp_viewporter *viewporter() const { return mViewporter.data(); } + QtWayland::wp_cursor_shape_manager_v1 *cursorShapeManager() const { return mCursorShapeManager.data();} struct RegistryGlobal { uint32_t id; @@ -277,6 +279,7 @@ private: QScopedPointer mXdgOutputManager; QScopedPointer mViewporter; QScopedPointer mFractionalScaleManager; + QScopedPointer mCursorShapeManager; int mFd = -1; int mWritableNotificationFd = -1; QList mGlobals; diff --git a/src/plugins/platforms/wayland/qwaylandinputdevice.cpp b/src/plugins/platforms/wayland/qwaylandinputdevice.cpp index 24c86a0cf70..b39e3a9755d 100644 --- a/src/plugins/platforms/wayland/qwaylandinputdevice.cpp +++ b/src/plugins/platforms/wayland/qwaylandinputdevice.cpp @@ -127,6 +127,10 @@ QWaylandInputDevice::Pointer::Pointer(QWaylandInputDevice *seat) { init(seat->get_pointer()); #if QT_CONFIG(cursor) + if (auto cursorShapeManager = seat->mQDisplay->cursorShapeManager()) { + mCursor.shape.reset(new QWaylandCursorShape(cursorShapeManager->get_pointer(object()))); + } + mCursor.frameTimer.setSingleShot(true); mCursor.frameTimer.callOnTimeout([&]() { cursorTimerCallback(); @@ -303,6 +307,11 @@ void QWaylandInputDevice::Pointer::updateCursor() return; } + if (mCursor.shape) { + mCursor.shape->setShape(mEnterSerial, shape); + return; + } + if (!mCursor.theme || idealCursorScale() != mCursor.themeBufferScale) updateCursorTheme(); diff --git a/src/plugins/platforms/wayland/qwaylandinputdevice_p.h b/src/plugins/platforms/wayland/qwaylandinputdevice_p.h index 115b39a8896..d41d885f3d5 100644 --- a/src/plugins/platforms/wayland/qwaylandinputdevice_p.h +++ b/src/plugins/platforms/wayland/qwaylandinputdevice_p.h @@ -64,6 +64,7 @@ class QWaylandTextInputInterface; class QWaylandTextInputMethod; #if QT_CONFIG(cursor) class QWaylandCursorTheme; +class QWaylandCursorShape; class CursorSurface; #endif @@ -323,6 +324,7 @@ public: uint32_t mEnterSerial = 0; #if QT_CONFIG(cursor) struct { + QScopedPointer shape; QWaylandCursorTheme *theme = nullptr; int themeBufferScale = 0; QScopedPointer surface; diff --git a/tests/auto/wayland/CMakeLists.txt b/tests/auto/wayland/CMakeLists.txt index 5ae005eaafe..79bcd442e4a 100644 --- a/tests/auto/wayland/CMakeLists.txt +++ b/tests/auto/wayland/CMakeLists.txt @@ -10,6 +10,7 @@ add_subdirectory(shared) if (NOT WEBOS) add_subdirectory(client) add_subdirectory(clientextension) + add_subdirectory(cursor) add_subdirectory(datadevicev1) add_subdirectory(fullscreenshellv1) add_subdirectory(iviapplication) diff --git a/tests/auto/wayland/cursor/CMakeLists.txt b/tests/auto/wayland/cursor/CMakeLists.txt new file mode 100644 index 00000000000..93783994e77 --- /dev/null +++ b/tests/auto/wayland/cursor/CMakeLists.txt @@ -0,0 +1,11 @@ +##################################################################### +## tst_cursor Test: +##################################################################### + +qt_internal_add_test(tst_cursor + SOURCES + tst_cursor.cpp + cursorshapev1.cpp + LIBRARIES + SharedClientTest +) diff --git a/tests/auto/wayland/cursor/cursorshapev1.cpp b/tests/auto/wayland/cursor/cursorshapev1.cpp new file mode 100644 index 00000000000..7fd93ed1d3b --- /dev/null +++ b/tests/auto/wayland/cursor/cursorshapev1.cpp @@ -0,0 +1,47 @@ +// Copyright (C) 2023 David Edmundson +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "cursorshapev1.h" + +namespace MockCompositor { + +CursorShapeManager::CursorShapeManager(CoreCompositor *compositor, int version) + : QtWaylandServer::wp_cursor_shape_manager_v1(compositor->m_display, version) +{ +} + +void CursorShapeManager::wp_cursor_shape_manager_v1_get_pointer(Resource *resource, uint32_t id, wl_resource *pointer) +{ + auto *p = fromResource(pointer); + auto *cursorShape = new CursorShapeDevice(p, resource->client(), id, resource->version()); + connect(cursorShape, &QObject::destroyed, this, [this, cursorShape]() { + m_cursorDevices.removeOne(cursorShape); + }); + m_cursorDevices << cursorShape; +} + +CursorShapeDevice::CursorShapeDevice(Pointer *pointer, wl_client *client, int id, int version) + : QtWaylandServer::wp_cursor_shape_device_v1(client, id, version) + , m_pointer(pointer) +{ +} + +void CursorShapeDevice::wp_cursor_shape_device_v1_destroy_resource(Resource *resource) +{ + Q_UNUSED(resource) + delete this; +} + +void CursorShapeDevice::wp_cursor_shape_device_v1_destroy(Resource *resource) +{ + wl_resource_destroy(resource->handle); +} + +void CursorShapeDevice::wp_cursor_shape_device_v1_set_shape(Resource *resource, uint32_t serial, uint32_t shape) +{ + Q_UNUSED(resource); + m_currentShape = static_cast(shape); + emit setCursor(serial); +} + +} diff --git a/tests/auto/wayland/cursor/cursorshapev1.h b/tests/auto/wayland/cursor/cursorshapev1.h new file mode 100644 index 00000000000..a8c2376aee7 --- /dev/null +++ b/tests/auto/wayland/cursor/cursorshapev1.h @@ -0,0 +1,44 @@ +// Copyright (C) 2023 David Edmundson +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef MOCKCOMPOSITOR_CURSORSHAPE_H +#define MOCKCOMPOSITOR_CURSORSHAPE_H + +#include "coreprotocol.h" +#include + +namespace MockCompositor { + +class CursorShapeDevice; + +class CursorShapeManager : public Global, public QtWaylandServer::wp_cursor_shape_manager_v1 +{ + Q_OBJECT +public: + explicit CursorShapeManager(CoreCompositor *compositor, int version = 1); + QList m_cursorDevices; + +protected: + void wp_cursor_shape_manager_v1_get_pointer(Resource *resource, uint32_t id, wl_resource *pointer) override; +}; + +class CursorShapeDevice : public QObject, public QtWaylandServer::wp_cursor_shape_device_v1 +{ + Q_OBJECT +public: + explicit CursorShapeDevice(Pointer *pointer, wl_client *client, int id, int version); + Pointer *m_pointer; + shape m_currentShape = shape_default; + +Q_SIGNALS: + void setCursor(uint serial); + +protected: + void wp_cursor_shape_device_v1_destroy_resource(Resource *resource) override; + void wp_cursor_shape_device_v1_destroy(Resource *resource) override; + void wp_cursor_shape_device_v1_set_shape(Resource *resource, uint32_t serial, uint32_t shape) override; +}; + +} + +#endif diff --git a/tests/auto/wayland/cursor/tst_cursor.cpp b/tests/auto/wayland/cursor/tst_cursor.cpp new file mode 100644 index 00000000000..65bafb23eca --- /dev/null +++ b/tests/auto/wayland/cursor/tst_cursor.cpp @@ -0,0 +1,89 @@ +// Copyright (C) 2023 David Edmundson +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "mockcompositor.h" +#include +#include +#include +#include + +#include "cursorshapev1.h" + +using namespace MockCompositor; + +class tst_cursor : public QObject, private DefaultCompositor +{ + Q_OBJECT +public: + tst_cursor(); + CursorShapeDevice* cursorShape(); +private slots: + void init(); + void cleanup() { QTRY_VERIFY2(isClean(), qPrintable(dirtyMessage())); } + void setCursor(); +}; + +tst_cursor::tst_cursor() +{ + exec([this] { + m_config.autoConfigure = true; + add(1); + }); +} + +CursorShapeDevice* tst_cursor::cursorShape() +{ + auto manager = get(); + if (!manager->m_cursorDevices.count()) + return nullptr; + return manager->m_cursorDevices[0]; +} + +void tst_cursor::init() +{ + setenv("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1", 1); +} + +void tst_cursor::setCursor() +{ + QCOMPOSITOR_TRY_VERIFY(cursorShape()); + QSignalSpy setCursorSpy(exec([&] { return pointer(); }), &Pointer::setCursor); + QSignalSpy setCursorShapeSpy(exec([&] { return cursorShape(); }), &CursorShapeDevice::setCursor); + + QRasterWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + uint enterSerial = exec([&] { + return pointer()->sendEnter(xdgSurface()->m_surface, {32, 32}); + }); + setCursorShapeSpy.wait(); + // verify we got given a cursor on enter + QCOMPOSITOR_COMPARE(cursorShape()->m_currentShape, CursorShapeDevice::shape_default); + QVERIFY(setCursorSpy.isEmpty()); + QCOMPARE(setCursorShapeSpy.takeFirst().at(0).toUInt(), enterSerial); + + // client sets a different shape + window.setCursor(QCursor(Qt::BusyCursor)); + QCOMPOSITOR_TRY_COMPARE(cursorShape()->m_currentShape, CursorShapeDevice::shape_wait); + + setCursorShapeSpy.clear(); + + // client hides the cursor + // CursorShape will not be used, instead, it uses the old path + window.setCursor(QCursor(Qt::BlankCursor)); + QVERIFY(setCursorSpy.wait()); + QVERIFY(setCursorShapeSpy.isEmpty()); + QCOMPOSITOR_VERIFY(!pointer()->cursorSurface()); + + // same for bitmaps + QPixmap myCustomPixmap(10, 10); + myCustomPixmap.fill(Qt::red); + window.setCursor(QCursor(myCustomPixmap)); + QVERIFY(setCursorSpy.wait()); + QVERIFY(setCursorShapeSpy.isEmpty()); +} + +QCOMPOSITOR_TEST_MAIN(tst_cursor) +#include "tst_cursor.moc" diff --git a/tests/auto/wayland/shared/CMakeLists.txt b/tests/auto/wayland/shared/CMakeLists.txt index a1f150c2245..ee81b4d6866 100644 --- a/tests/auto/wayland/shared/CMakeLists.txt +++ b/tests/auto/wayland/shared/CMakeLists.txt @@ -39,6 +39,7 @@ add_library(SharedClientTest qt6_generate_wayland_protocol_server_sources(SharedClientTest FILES + ${PROJECT_SOURCE_DIR}/src/3rdparty/protocol/cursor-shape-v1.xml ${PROJECT_SOURCE_DIR}/src/3rdparty/protocol/fullscreen-shell-unstable-v1.xml ${PROJECT_SOURCE_DIR}/src/3rdparty/protocol/ivi-application.xml ${PROJECT_SOURCE_DIR}/src/3rdparty/protocol/wp-primary-selection-unstable-v1.xml diff --git a/tests/auto/wayland/shared/coreprotocol.cpp b/tests/auto/wayland/shared/coreprotocol.cpp index 15be62ccb9f..64586d413de 100644 --- a/tests/auto/wayland/shared/coreprotocol.cpp +++ b/tests/auto/wayland/shared/coreprotocol.cpp @@ -435,15 +435,19 @@ void Pointer::sendAxisValue120(wl_client *client, QtWaylandServer::wl_pointer::a void Pointer::pointer_set_cursor(Resource *resource, uint32_t serial, wl_resource *surface, int32_t hotspot_x, int32_t hotspot_y) { Q_UNUSED(resource); - auto *s = fromResource(surface); - QVERIFY(s); - if (s->m_role) { - m_cursorRole = CursorRole::fromSurface(s); - QVERIFY(m_cursorRole); + if (!surface) { + m_cursorRole = nullptr; } else { - m_cursorRole = new CursorRole(s); //TODO: make sure we don't leak CursorRole - s->m_role = m_cursorRole; + auto *s = fromResource(surface); + QVERIFY(s); + if (s->m_role) { + m_cursorRole = CursorRole::fromSurface(s); + QVERIFY(m_cursorRole); + } else { + m_cursorRole = new CursorRole(s); //TODO: make sure we don't leak CursorRole + s->m_role = m_cursorRole; + } } // Directly checking the last serial would be racy, we may just have sent leaves/enters which