/**************************************************************************** ** ** 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 #include #include #include #include #include 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 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(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 m_devices; QVector 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(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(primarySelectionVersion); }); } PrimarySelectionDeviceV1 *primarySelectionDevice(int i = 0) { return get()->deviceFor(get(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 destroysSelectionOnLeave(); void copy(); }; void tst_primaryselectionv1::initTestCase() { QCOMPOSITOR_TRY_VERIFY(pointer()); QCOMPOSITOR_TRY_VERIFY(!pointer()->resourceMap().empty()); QCOMPOSITOR_TRY_COMPARE(pointer()->resourceMap().first()->version(), 5); QCOMPOSITOR_TRY_VERIFY(keyboard()); } void tst_primaryselectionv1::bindsToManager() { QCOMPOSITOR_TRY_COMPARE(get()->resourceMap().size(), 1); QCOMPOSITOR_TRY_COMPARE(get()->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(); }); 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()->sendFrame(client()); pointer()->sendButton(client(), BTN_MIDDLE, 1); pointer()->sendFrame(client()); pointer()->sendButton(client(), BTN_MIDDLE, 0); pointer()->sendFrame(client()); }); 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()->sendFrame(client()); pointer()->sendButton(client(), BTN_MIDDLE, 1); pointer()->sendFrame(client()); pointer()->sendButton(client(), BTN_MIDDLE, 0); pointer()->sendFrame(client()); }); 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::destroysSelectionOnLeave() { QRasterWindow 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 *offer = primarySelectionDevice()->sendDataOffer({"text/plain"}); primarySelectionDevice()->sendSelection(offer); }); QTRY_VERIFY(QGuiApplication::clipboard()->mimeData(QClipboard::Selection)); QTRY_VERIFY(QGuiApplication::clipboard()->mimeData(QClipboard::Selection)->hasText()); QSignalSpy selectionChangedSpy(QGuiApplication::clipboard(), &QClipboard::selectionChanged); exec([&] { auto *surface = xdgSurface()->m_surface; keyboard()->sendLeave(surface); }); QTRY_COMPARE(selectionChangedSpy.count(), 1); QVERIFY(!QGuiApplication::clipboard()->mimeData(QClipboard::Selection)->hasText()); } 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 mouseSerials; exec([&] { auto *surface = xdgSurface()->m_surface; keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol pointer()->sendEnter(surface, {32, 32}); pointer()->sendFrame(client()); mouseSerials << pointer()->sendButton(client(), BTN_MIDDLE, 1); pointer()->sendFrame(client()); mouseSerials << pointer()->sendButton(client(), BTN_MIDDLE, 0); pointer()->sendFrame(client()); }); QCOMPOSITOR_TRY_VERIFY(primarySelectionDevice()->m_selectionSource); QCOMPOSITOR_TRY_VERIFY(mouseSerials.contains(primarySelectionDevice()->m_serial)); QVERIFY(QGuiApplication::clipboard()->ownsSelection()); 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"