blob: ee9fa110e4b13c7e0c43076860f27783b6f9b1a4 [file] [log] [blame]
/****************************************************************************
**
** 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 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<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()->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<uint> 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"