diff options
author | Johan Klokkhammer Helsing <johan.helsing@qt.io> | 2018-11-13 15:40:14 +0100 |
---|---|---|
committer | Johan Helsing <johan.helsing@qt.io> | 2019-05-10 12:35:00 +0000 |
commit | 5ec182df699041699f514d164a161c299fde5d19 (patch) | |
tree | 5f646078ba391c4327414f532991f8e1f687de59 /tests/auto/client | |
parent | 811e38787536c65122d780ad3d7b16757bb7d0bb (diff) |
Client: Implement primary-selection-unstable-v1
[ChangeLog][QPA plugin] Added support for middle mouse pasting through the
primary-selection-unstable-v1 protocol.
Fixes: QTBUG-66008
Change-Id: I7c8fb9aa2c856f5b6794aeab1ee75d80cad05dcd
Reviewed-by: Paul Olav Tvete <paul.tvete@qt.io>
Diffstat (limited to 'tests/auto/client')
-rw-r--r-- | tests/auto/client/client.pro | 1 | ||||
-rw-r--r-- | tests/auto/client/primaryselectionv1/primaryselectionv1.pro | 7 | ||||
-rw-r--r-- | tests/auto/client/primaryselectionv1/tst_primaryselectionv1.cpp | 466 | ||||
-rw-r--r-- | tests/auto/client/shared/corecompositor.h | 17 | ||||
-rw-r--r-- | tests/auto/client/shared/coreprotocol.cpp | 2 | ||||
-rw-r--r-- | tests/auto/client/shared/coreprotocol.h | 3 | ||||
-rw-r--r-- | tests/auto/client/shared/mockcompositor.h | 8 |
7 files changed, 502 insertions, 2 deletions
diff --git a/tests/auto/client/client.pro b/tests/auto/client/client.pro index 5e0dcab8c..61bf67853 100644 --- a/tests/auto/client/client.pro +++ b/tests/auto/client/client.pro @@ -6,6 +6,7 @@ SUBDIRS += \ fullscreenshellv1 \ iviapplication \ output \ + primaryselectionv1 \ seatv4 \ seatv5 \ surface \ diff --git a/tests/auto/client/primaryselectionv1/primaryselectionv1.pro b/tests/auto/client/primaryselectionv1/primaryselectionv1.pro new file mode 100644 index 000000000..9d00562df --- /dev/null +++ b/tests/auto/client/primaryselectionv1/primaryselectionv1.pro @@ -0,0 +1,7 @@ +include (../shared/shared.pri) + +WAYLANDSERVERSOURCES += \ + $$PWD/../../../../src/3rdparty/protocol/wp-primary-selection-unstable-v1.xml + +TARGET = tst_primaryselectionv1 +SOURCES += tst_primaryselectionv1.cpp diff --git a/tests/auto/client/primaryselectionv1/tst_primaryselectionv1.cpp b/tests/auto/client/primaryselectionv1/tst_primaryselectionv1.cpp new file mode 100644 index 000000000..281e4c5d1 --- /dev/null +++ b/tests/auto/client/primaryselectionv1/tst_primaryselectionv1.cpp @@ -0,0 +1,466 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "mockcompositor.h" + +#include <qwayland-server-wp-primary-selection-unstable-v1.h> + +#include <QtGui/QRasterWindow> +#include <QtGui/QOpenGLWindow> +#include <QtGui/QClipboard> +#include <QtCore/private/qcore_unix_p.h> + +#include <fcntl.h> + +using namespace MockCompositor; + +constexpr int primarySelectionVersion = 1; // protocol VERSION, not the name suffix (_v1) + +class PrimarySelectionDeviceV1; +class PrimarySelectionDeviceManagerV1; + +class PrimarySelectionOfferV1 : public QObject, public QtWaylandServer::zwp_primary_selection_offer_v1 +{ + Q_OBJECT +public: + explicit PrimarySelectionOfferV1(PrimarySelectionDeviceV1 *device, wl_client *client, int version) + : zwp_primary_selection_offer_v1(client, 0, version) + , m_device(device) + {} + void send_offer() = delete; + void sendOffer(const QString &offer) + { + zwp_primary_selection_offer_v1::send_offer(offer); + m_mimeTypes << offer; + } + + PrimarySelectionDeviceV1 *m_device = nullptr; + QStringList m_mimeTypes; + +signals: + void receive(QString mimeType, int fd); + +protected: + void zwp_primary_selection_offer_v1_destroy_resource(Resource *resource) override + { + Q_UNUSED(resource); + delete this; + } + + void zwp_primary_selection_offer_v1_receive(Resource *resource, const QString &mime_type, int32_t fd) override + { + Q_UNUSED(resource); + QTRY_VERIFY(m_mimeTypes.contains(mime_type)); + emit receive(mime_type, fd); + } + + void zwp_primary_selection_offer_v1_destroy(Resource *resource) override; +}; + +class PrimarySelectionSourceV1 : public QObject, public QtWaylandServer::zwp_primary_selection_source_v1 +{ + Q_OBJECT +public: + explicit PrimarySelectionSourceV1(wl_client *client, int id, int version) + : zwp_primary_selection_source_v1(client, id, version) + { + } + QStringList m_offers; +protected: + void zwp_primary_selection_source_v1_destroy_resource(Resource *resource) override + { + Q_UNUSED(resource); + delete this; + } + void zwp_primary_selection_source_v1_offer(Resource *resource, const QString &mime_type) override + { + Q_UNUSED(resource); + m_offers << mime_type; + } + void zwp_primary_selection_source_v1_destroy(Resource *resource) override + { + wl_resource_destroy(resource->handle); + } +}; + +class PrimarySelectionDeviceV1 : public QObject, public QtWaylandServer::zwp_primary_selection_device_v1 +{ + Q_OBJECT +public: + explicit PrimarySelectionDeviceV1(PrimarySelectionDeviceManagerV1 *manager, Seat *seat) + : m_manager(manager) + , m_seat(seat) + {} + + void send_data_offer(::wl_resource *resource) = delete; + + PrimarySelectionOfferV1 *sendDataOffer(::wl_client *client, const QStringList &mimeTypes = {}); + + PrimarySelectionOfferV1 *sendDataOffer(const QStringList &mimeTypes = {}) // creates a new offer for the focused surface and sends it + { + Q_ASSERT(m_seat->m_capabilities & Seat::capability_keyboard); + Q_ASSERT(m_seat->m_keyboard->m_enteredSurface); + auto *client = m_seat->m_keyboard->m_enteredSurface->resource()->client(); + return sendDataOffer(client, mimeTypes); + } + + void send_selection(::wl_resource *resource) = delete; + void sendSelection(PrimarySelectionOfferV1 *offer) + { + auto *client = offer->resource()->client(); + for (auto *resource : resourceMap().values(client)) + zwp_primary_selection_device_v1::send_selection(resource->handle, offer->resource()->handle); + m_sentSelectionOffers << offer; + } + + PrimarySelectionDeviceManagerV1 *m_manager = nullptr; + Seat *m_seat = nullptr; + QVector<PrimarySelectionOfferV1 *> m_sentSelectionOffers; + PrimarySelectionSourceV1 *m_selectionSource = nullptr; + uint m_serial = 0; + +protected: + void zwp_primary_selection_device_v1_set_selection(Resource *resource, ::wl_resource *source, uint32_t serial) override + { + Q_UNUSED(resource); + m_selectionSource = fromResource<PrimarySelectionSourceV1>(source); + m_serial = serial; + } + void zwp_primary_selection_device_v1_destroy(Resource *resource) override + { + wl_resource_destroy(resource->handle); + } + void zwp_primary_selection_device_v1_destroy_resource(Resource *resource) override + { + Q_UNUSED(resource); + delete this; + } +}; + +class PrimarySelectionDeviceManagerV1 : public Global, public QtWaylandServer::zwp_primary_selection_device_manager_v1 +{ + Q_OBJECT +public: + explicit PrimarySelectionDeviceManagerV1(CoreCompositor *compositor, int version = 1) + : QtWaylandServer::zwp_primary_selection_device_manager_v1(compositor->m_display, version) + , m_version(version) + {} + bool isClean() override + { + for (auto *device : qAsConst(m_devices)) { + // The client should not leak selection offers, i.e. if this fails, there is a missing + // zwp_primary_selection_offer_v1.destroy request + if (!device->m_sentSelectionOffers.empty()) + return false; + } + return true; + } + + PrimarySelectionDeviceV1 *deviceFor(Seat *seat) + { + Q_ASSERT(seat); + if (auto *device = m_devices.value(seat, nullptr)) + return device; + + auto *device = new PrimarySelectionDeviceV1(this, seat); + m_devices[seat] = device; + return device; + } + + int m_version = 1; // TODO: Remove on libwayland upgrade + QMap<Seat *, PrimarySelectionDeviceV1 *> m_devices; + QVector<PrimarySelectionSourceV1 *> m_sources; +protected: + void zwp_primary_selection_device_manager_v1_destroy(Resource *resource) override + { + // The protocol doesn't say whether managed objects should be destroyed as well, + // so leave them alone, they'll be cleaned up in the destructor anyway + wl_resource_destroy(resource->handle); + } + + void zwp_primary_selection_device_manager_v1_create_source(Resource *resource, uint32_t id) override + { + int version = m_version; + m_sources << new PrimarySelectionSourceV1(resource->client(), id, version); + } + void zwp_primary_selection_device_manager_v1_get_device(Resource *resource, uint32_t id, ::wl_resource *seatResource) override + { + auto *seat = fromResource<Seat>(seatResource); + QVERIFY(seat); + auto *device = deviceFor(seat); + device->add(resource->client(), id, resource->version()); + } +}; + +PrimarySelectionOfferV1 *PrimarySelectionDeviceV1::sendDataOffer(wl_client *client, const QStringList &mimeTypes) +{ + Q_ASSERT(client); + auto *offer = new PrimarySelectionOfferV1(this, client, m_manager->m_version); + for (auto *resource : resourceMap().values(client)) + zwp_primary_selection_device_v1::send_data_offer(resource->handle, offer->resource()->handle); + for (const auto &mimeType : mimeTypes) + offer->sendOffer(mimeType); + return offer; +} + +void PrimarySelectionOfferV1::zwp_primary_selection_offer_v1_destroy(QtWaylandServer::zwp_primary_selection_offer_v1::Resource *resource) +{ + bool removed = m_device->m_sentSelectionOffers.removeOne(this); + QVERIFY(removed); + wl_resource_destroy(resource->handle); +} + +class PrimarySelectionCompositor : public DefaultCompositor { +public: + explicit PrimarySelectionCompositor() + { + exec([this] { + m_config.autoConfigure = true; + add<PrimarySelectionDeviceManagerV1>(primarySelectionVersion); + }); + } + PrimarySelectionDeviceV1 *primarySelectionDevice(int i = 0) { + return get<PrimarySelectionDeviceManagerV1>()->deviceFor(get<Seat>(i)); + } +}; + +class tst_primaryselectionv1 : public QObject, private PrimarySelectionCompositor +{ + Q_OBJECT +private slots: + void cleanup() { QTRY_VERIFY2(isClean(), qPrintable(dirtyMessage())); } + void initTestCase(); + void bindsToManager(); + void createsPrimaryDevice(); + void createsPrimaryDeviceForNewSeats(); + void pasteAscii(); + void pasteUtf8(); + void destroysPreviousSelection(); + void copy(); +}; + +void tst_primaryselectionv1::initTestCase() +{ + QCOMPOSITOR_TRY_VERIFY(pointer()); + QCOMPOSITOR_TRY_VERIFY(!pointer()->resourceMap().empty()); + QCOMPOSITOR_TRY_COMPARE(pointer()->resourceMap().first()->version(), 4); + + QCOMPOSITOR_TRY_VERIFY(keyboard()); +} + +void tst_primaryselectionv1::bindsToManager() +{ + QCOMPOSITOR_TRY_COMPARE(get<PrimarySelectionDeviceManagerV1>()->resourceMap().size(), 1); + QCOMPOSITOR_TRY_COMPARE(get<PrimarySelectionDeviceManagerV1>()->resourceMap().first()->version(), primarySelectionVersion); +} + +void tst_primaryselectionv1::createsPrimaryDevice() +{ + QCOMPOSITOR_TRY_VERIFY(primarySelectionDevice()); + QCOMPOSITOR_TRY_VERIFY(primarySelectionDevice()->resourceMap().contains(client())); + QCOMPOSITOR_TRY_COMPARE(primarySelectionDevice()->resourceMap().value(client())->version(), primarySelectionVersion); + QTRY_VERIFY(QGuiApplication::clipboard()->supportsSelection()); +} + +void tst_primaryselectionv1::createsPrimaryDeviceForNewSeats() +{ + exec([=] { add<Seat>(); }); + QCOMPOSITOR_TRY_VERIFY(primarySelectionDevice(1)); +} + +void tst_primaryselectionv1::pasteAscii() +{ + class Window : public QRasterWindow { + public: + void mousePressEvent(QMouseEvent *event) override + { + Q_UNUSED(event); + auto *mimeData = QGuiApplication::clipboard()->mimeData(QClipboard::Selection); + m_formats = mimeData->formats(); + m_text = QGuiApplication::clipboard()->text(QClipboard::Selection); + } + QStringList m_formats; + QString m_text; + }; + + Window window; + window.resize(64, 64); + window.show(); + + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + exec([&] { + auto *surface = xdgSurface()->m_surface; + keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol + + auto *device = primarySelectionDevice(); + auto *offer = device->sendDataOffer({"text/plain"}); + connect(offer, &PrimarySelectionOfferV1::receive, [](QString mimeType, int fd) { + QFile file; + file.open(fd, QIODevice::WriteOnly, QFile::FileHandleFlag::AutoCloseHandle); + QCOMPARE(mimeType, "text/plain"); + file.write(QByteArray("normal ascii")); + file.close(); + }); + device->sendSelection(offer); + + pointer()->sendEnter(surface, {32, 32}); + pointer()->sendButton(client(), BTN_MIDDLE, 1); + pointer()->sendButton(client(), BTN_MIDDLE, 0); + }); + QTRY_COMPARE(window.m_formats, QStringList{"text/plain"}); + QTRY_COMPARE(window.m_text, "normal ascii"); +} + +void tst_primaryselectionv1::pasteUtf8() +{ + class Window : public QRasterWindow { + public: + void mousePressEvent(QMouseEvent *event) override + { + Q_UNUSED(event); + auto *mimeData = QGuiApplication::clipboard()->mimeData(QClipboard::Selection); + m_formats = mimeData->formats(); + m_text = QGuiApplication::clipboard()->text(QClipboard::Selection); + } + QStringList m_formats; + QString m_text; + }; + + Window window; + window.resize(64, 64); + window.show(); + + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + exec([&] { + auto *surface = xdgSurface()->m_surface; + keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol + + auto *device = primarySelectionDevice(); + auto *offer = device->sendDataOffer({"text/plain", "text/plain;charset=utf-8"}); + connect(offer, &PrimarySelectionOfferV1::receive, [](QString mimeType, int fd) { + QFile file; + file.open(fd, QIODevice::WriteOnly, QFile::FileHandleFlag::AutoCloseHandle); + QCOMPARE(mimeType, "text/plain;charset=utf-8"); + file.write(QByteArray("face with tears of joy: 😂")); + file.close(); + }); + device->sendSelection(offer); + + pointer()->sendEnter(surface, {32, 32}); + pointer()->sendButton(client(), BTN_MIDDLE, 1); + pointer()->sendButton(client(), BTN_MIDDLE, 0); + }); + QTRY_COMPARE(window.m_formats, QStringList({"text/plain", "text/plain;charset=utf-8"})); + QTRY_COMPARE(window.m_text, "face with tears of joy: 😂"); +} + +void tst_primaryselectionv1::destroysPreviousSelection() +{ + QRasterWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + // When the client receives a selection event, it is required to destroy the previous offer + exec([&] { + auto *surface = xdgSurface()->m_surface; + keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol + + auto *offer = primarySelectionDevice()->sendDataOffer({"text/plain"}); + primarySelectionDevice()->sendSelection(offer); + }); + + exec([&] { + auto *offer = primarySelectionDevice()->sendDataOffer({"text/plain"}); + primarySelectionDevice()->sendSelection(offer); + QCOMPARE(primarySelectionDevice()->m_sentSelectionOffers.size(), 2); + }); + + // Verify the first offer gets destroyed + QCOMPOSITOR_TRY_COMPARE(primarySelectionDevice()->m_sentSelectionOffers.size(), 1); +} + +void tst_primaryselectionv1::copy() +{ + class Window : public QRasterWindow { + public: + void mousePressEvent(QMouseEvent *event) override + { + Q_UNUSED(event); + QGuiApplication::clipboard()->setText("face with tears of joy: 😂", QClipboard::Selection); + } + QStringList m_formats; + QString m_text; + }; + + Window window; + window.resize(64, 64); + window.show(); + + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + QVector<uint> mouseSerials; + exec([&] { + auto *surface = xdgSurface()->m_surface; + keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol + pointer()->sendEnter(surface, {32, 32}); + mouseSerials << pointer()->sendButton(client(), BTN_MIDDLE, 1); + mouseSerials << pointer()->sendButton(client(), BTN_MIDDLE, 0); + }); + QCOMPOSITOR_TRY_VERIFY(primarySelectionDevice()->m_selectionSource); + QCOMPOSITOR_TRY_VERIFY(mouseSerials.contains(primarySelectionDevice()->m_serial)); + QByteArray pastedBuf; + exec([&](){ + auto *source = primarySelectionDevice()->m_selectionSource; + QCOMPARE(source->m_offers, QStringList({"text/plain", "text/plain;charset=utf-8"})); + int fd[2]; + if (pipe(fd) == -1) + QSKIP("Failed to create pipe"); + fcntl(fd[0], F_SETFL, fcntl(fd[0], F_GETFL, 0) | O_NONBLOCK); + source->send_send("text/plain;charset=utf-8", fd[1]); + auto *notifier = new QSocketNotifier(fd[0], QSocketNotifier::Read, this); + connect(notifier, &QSocketNotifier::activated, this, [&](int fd) { + exec([&]{ + static char buf[1024]; + int n = QT_READ(fd, buf, sizeof buf); + if (n <= 0) { + delete notifier; + close(fd); + } else { + pastedBuf.append(buf, n); + } + }); + }); + }); + + QCOMPOSITOR_TRY_VERIFY(pastedBuf.size()); // this assumes we got everything in one read + auto pasted = QString::fromUtf8(pastedBuf); + QCOMPARE(pasted, "face with tears of joy: 😂"); +} + +QCOMPOSITOR_TEST_MAIN(tst_primaryselectionv1) +#include "tst_primaryselectionv1.moc" diff --git a/tests/auto/client/shared/corecompositor.h b/tests/auto/client/shared/corecompositor.h index 875b7d050..254465ee6 100644 --- a/tests/auto/client/shared/corecompositor.h +++ b/tests/auto/client/shared/corecompositor.h @@ -125,6 +125,23 @@ public: } /*! + * \brief Returns the nth global with the given type, if any + */ + template<typename global_type> + global_type *get(int index) + { + warnIfNotLockedByThread(Q_FUNC_INFO); + for (auto *global : qAsConst(m_globals)) { + if (auto *casted = qobject_cast<global_type *>(global)) { + if (index--) + continue; + return casted; + } + } + return nullptr; + } + + /*! * \brief Returns all globals with the given type, if any */ template<typename global_type> diff --git a/tests/auto/client/shared/coreprotocol.cpp b/tests/auto/client/shared/coreprotocol.cpp index b10377c2c..f9335e783 100644 --- a/tests/auto/client/shared/coreprotocol.cpp +++ b/tests/auto/client/shared/coreprotocol.cpp @@ -378,6 +378,7 @@ uint Keyboard::sendEnter(Surface *surface) const auto pointerResources = resourceMap().values(client); for (auto *r : pointerResources) send_enter(r->handle, serial, surface->resource()->handle, QByteArray()); + m_enteredSurface = surface; return serial; } @@ -388,6 +389,7 @@ uint Keyboard::sendLeave(Surface *surface) const auto pointerResources = resourceMap().values(client); for (auto *r : pointerResources) send_leave(r->handle, serial, surface->resource()->handle); + m_enteredSurface = nullptr; return serial; } diff --git a/tests/auto/client/shared/coreprotocol.h b/tests/auto/client/shared/coreprotocol.h index 3563331c7..264c5f694 100644 --- a/tests/auto/client/shared/coreprotocol.h +++ b/tests/auto/client/shared/coreprotocol.h @@ -236,7 +236,7 @@ class Seat : public Global, public QtWaylandServer::wl_seat { Q_OBJECT public: - explicit Seat(CoreCompositor *compositor, uint capabilities, int version = 4); + explicit Seat(CoreCompositor *compositor, uint capabilities = Seat::capability_pointer | Seat::capability_keyboard, int version = 4); ~Seat() override; void send_capabilities(Resource *resource, uint capabilities) = delete; // Use wrapper instead void send_capabilities(uint capabilities) = delete; // Use wrapper instead @@ -317,6 +317,7 @@ public: uint sendLeave(Surface *surface); uint sendKey(wl_client *client, uint key, uint state); Seat *m_seat = nullptr; + Surface *m_enteredSurface = nullptr; }; class Shm : public Global, public QtWaylandServer::wl_shm diff --git a/tests/auto/client/shared/mockcompositor.h b/tests/auto/client/shared/mockcompositor.h index 75ef1eaea..aa85a4aea 100644 --- a/tests/auto/client/shared/mockcompositor.h +++ b/tests/auto/client/shared/mockcompositor.h @@ -36,10 +36,16 @@ #include <QtGui/QGuiApplication> -#ifndef BTN_LEFT // As defined in linux/input-event-codes.h +#ifndef BTN_LEFT #define BTN_LEFT 0x110 #endif +#ifndef BTN_RIGHT +#define BTN_RIGHT 0x111 +#endif +#ifndef BTN_MIDDLE +#define BTN_MIDDLE 0x112 +#endif namespace MockCompositor { |