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>
This commit is contained in:
Johan Klokkhammer Helsing 2018-11-13 15:40:14 +01:00 committed by Johan Helsing
parent 50d2c36a57
commit 72cae9c58c
20 changed files with 1205 additions and 35 deletions

View File

@ -55,6 +55,23 @@ Copyright © 2012-2013 Collabora, Ltd."
Copyright (c) 2013 BMW Car IT GmbH"
},
{
"Id": "wayland-primary-selection-protocol",
"Name": "Wayland Primary Selection Protocol",
"QDocModule": "qtwaylandcompositor",
"QtUsage": "Used in the Qt Wayland platform plugin",
"Files": "wp-primary-selection-unstable-v1.xml",
"Description": "The primary selection extension allows copying text by selecting it and pasting it with the middle mouse button.",
"Homepage": "https://wayland.freedesktop.org",
"Version": "1",
"DownloadLocation": "https://cgit.freedesktop.org/wayland/wayland-protocols/plain/unstable/primary-selection/primary-selection-unstable-v1.xml",
"LicenseId": "MIT",
"License": "MIT License",
"LicenseFile": "MIT_LICENSE.txt",
"Copyright": "Copyright © 2015 2016 Red Hat"
},
{
"Id": "wayland-scaler-protocol",
"Name": "Wayland Scaler Protocol",

View File

@ -0,0 +1,225 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wp_primary_selection_unstable_v1">
<copyright>
Copyright © 2015, 2016 Red Hat
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.
</copyright>
<description summary="Primary selection protocol">
This protocol provides the ability to have a primary selection device to
match that of the X server. This primary selection is a shortcut to the
common clipboard selection, where text just needs to be selected in order
to allow copying it elsewhere. The de facto way to perform this action
is the middle mouse button, although it is not limited to this one.
Clients wishing to honor primary selection should create a primary
selection source and set it as the selection through
wp_primary_selection_device.set_selection whenever the text selection
changes. In order to minimize calls in pointer-driven text selection,
it should happen only once after the operation finished. Similarly,
a NULL source should be set when text is unselected.
wp_primary_selection_offer objects are first announced through the
wp_primary_selection_device.data_offer event. Immediately after this event,
the primary data offer will emit wp_primary_selection_offer.offer events
to let know of the mime types being offered.
When the primary selection changes, the client with the keyboard focus
will receive wp_primary_selection_device.selection events. Only the client
with the keyboard focus will receive such events with a non-NULL
wp_primary_selection_offer. Across keyboard focus changes, previously
focused clients will receive wp_primary_selection_device.events with a
NULL wp_primary_selection_offer.
In order to request the primary selection data, the client must pass
a recent serial pertaining to the press event that is triggering the
operation, if the compositor deems the serial valid and recent, the
wp_primary_selection_source.send event will happen in the other end
to let the transfer begin. The client owning the primary selection
should write the requested data, and close the file descriptor
immediately.
If the primary selection owner client disappeared during the transfer,
the client reading the data will receive a
wp_primary_selection_device.selection event with a NULL
wp_primary_selection_offer, the client should take this as a hint
to finish the reads related to the no longer existing offer.
The primary selection owner should be checking for errors during
writes, merely cancelling the ongoing transfer if any happened.
</description>
<interface name="zwp_primary_selection_device_manager_v1" version="1">
<description summary="X primary selection emulation">
The primary selection device manager is a singleton global object that
provides access to the primary selection. It allows to create
wp_primary_selection_source objects, as well as retrieving the per-seat
wp_primary_selection_device objects.
</description>
<request name="create_source">
<description summary="create a new primary selection source">
Create a new primary selection source.
</description>
<arg name="id" type="new_id" interface="zwp_primary_selection_source_v1"/>
</request>
<request name="get_device">
<description summary="create a new primary selection device">
Create a new data device for a given seat.
</description>
<arg name="id" type="new_id" interface="zwp_primary_selection_device_v1"/>
<arg name="seat" type="object" interface="wl_seat"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the primary selection device manager">
Destroy the primary selection device manager.
</description>
</request>
</interface>
<interface name="zwp_primary_selection_device_v1" version="1">
<request name="set_selection">
<description summary="set the primary selection">
Replaces the current selection. The previous owner of the primary
selection will receive a wp_primary_selection_source.cancelled event.
To unset the selection, set the source to NULL.
</description>
<arg name="source" type="object" interface="zwp_primary_selection_source_v1" allow-null="true"/>
<arg name="serial" type="uint" summary="serial of the event that triggered this request"/>
</request>
<event name="data_offer">
<description summary="introduce a new wp_primary_selection_offer">
Introduces a new wp_primary_selection_offer object that may be used
to receive the current primary selection. Immediately following this
event, the new wp_primary_selection_offer object will send
wp_primary_selection_offer.offer events to describe the offered mime
types.
</description>
<arg name="offer" type="new_id" interface="zwp_primary_selection_offer_v1"/>
</event>
<event name="selection">
<description summary="advertise a new primary selection">
The wp_primary_selection_device.selection event is sent to notify the
client of a new primary selection. This event is sent after the
wp_primary_selection.data_offer event introducing this object, and after
the offer has announced its mimetypes through
wp_primary_selection_offer.offer.
The data_offer is valid until a new offer or NULL is received
or until the client loses keyboard focus. The client must destroy the
previous selection data_offer, if any, upon receiving this event.
</description>
<arg name="id" type="object" interface="zwp_primary_selection_offer_v1" allow-null="true"/>
</event>
<request name="destroy" type="destructor">
<description summary="destroy the primary selection device">
Destroy the primary selection device.
</description>
</request>
</interface>
<interface name="zwp_primary_selection_offer_v1" version="1">
<description summary="offer to transfer primary selection contents">
A wp_primary_selection_offer represents an offer to transfer the contents
of the primary selection clipboard to the client. Similar to
wl_data_offer, the offer also describes the mime types that the data can
be converted to and provides the mechanisms for transferring the data
directly to the client.
</description>
<request name="receive">
<description summary="request that the data is transferred">
To transfer the contents of the primary selection clipboard, the client
issues this request and indicates the mime type that it wants to
receive. The transfer happens through the passed file descriptor
(typically created with the pipe system call). The source client writes
the data in the mime type representation requested and then closes the
file descriptor.
The receiving client reads from the read end of the pipe until EOF and
closes its end, at which point the transfer is complete.
</description>
<arg name="mime_type" type="string"/>
<arg name="fd" type="fd"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the primary selection offer">
Destroy the primary selection offer.
</description>
</request>
<event name="offer">
<description summary="advertise offered mime type">
Sent immediately after creating announcing the
wp_primary_selection_offer through
wp_primary_selection_device.data_offer. One event is sent per offered
mime type.
</description>
<arg name="mime_type" type="string"/>
</event>
</interface>
<interface name="zwp_primary_selection_source_v1" version="1">
<description summary="offer to replace the contents of the primary selection">
The source side of a wp_primary_selection_offer, it provides a way to
describe the offered data and respond to requests to transfer the
requested contents of the primary selection clipboard.
</description>
<request name="offer">
<description summary="add an offered mime type">
This request adds a mime type to the set of mime types advertised to
targets. Can be called several times to offer multiple types.
</description>
<arg name="mime_type" type="string"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the primary selection source">
Destroy the primary selection source.
</description>
</request>
<event name="send">
<description summary="send the primary selection contents">
Request for the current primary selection contents from the client.
Send the specified mime type over the passed file descriptor, then
close it.
</description>
<arg name="mime_type" type="string"/>
<arg name="fd" type="fd"/>
</event>
<event name="cancelled">
<description summary="request for primary selection contents was canceled">
This primary selection source is no longer valid. The client should
clean up and destroy this primary selection source.
</description>
</event>
</interface>
</protocol>

View File

@ -31,6 +31,7 @@ WAYLANDCLIENTSOURCES += \
../extensions/touch-extension.xml \
../extensions/qt-key-unstable-v1.xml \
../extensions/qt-windowmanager.xml \
../3rdparty/protocol/wp-primary-selection-unstable-v1.xml \
../3rdparty/protocol/text-input-unstable-v2.xml \
../3rdparty/protocol/xdg-output-unstable-v1.xml \
../3rdparty/protocol/wayland.xml
@ -118,6 +119,11 @@ qtConfig(wayland-datadevice) {
qwaylanddatasource.cpp
}
qtConfig(wayland-client-primary-selection) {
HEADERS += qwaylandprimaryselectionv1_p.h
SOURCES += qwaylandprimaryselectionv1.cpp
}
qtConfig(draganddrop) {
HEADERS += \
qwaylanddnd_p.h

View File

@ -93,6 +93,11 @@
"condition": "features.draganddrop || features.clipboard",
"output": [ "privateFeature" ]
},
"wayland-client-primary-selection": {
"label": "primary-selection clipboard",
"condition": "features.clipboard",
"output": [ "privateFeature" ]
},
"wayland-client-fullscreen-shell-v1": {
"label": "fullscreen-shell-v1",
"condition": "features.wayland-client",

View File

@ -43,6 +43,9 @@
#include "qwaylanddataoffer_p.h"
#include "qwaylanddatasource_p.h"
#include "qwaylanddatadevice_p.h"
#if QT_CONFIG(wayland_client_primary_selection)
#include "qwaylandprimaryselectionv1_p.h"
#endif
QT_BEGIN_NAMESPACE
@ -59,44 +62,74 @@ QWaylandClipboard::~QWaylandClipboard()
QMimeData *QWaylandClipboard::mimeData(QClipboard::Mode mode)
{
if (mode != QClipboard::Clipboard)
auto *seat = mDisplay->currentInputDevice();
if (!seat)
return &m_emptyData;
QWaylandInputDevice *inputDevice = mDisplay->currentInputDevice();
if (!inputDevice || !inputDevice->dataDevice())
switch (mode) {
case QClipboard::Clipboard:
if (auto *dataDevice = seat->dataDevice()) {
if (auto *source = dataDevice->selectionSource())
return source->mimeData();
if (auto *offer = dataDevice->selectionOffer())
return offer->mimeData();
}
return &m_emptyData;
case QClipboard::Selection:
#if QT_CONFIG(wayland_client_primary_selection)
if (auto *selectionDevice = seat->primarySelectionDevice()) {
if (auto *source = selectionDevice->selectionSource())
return source->mimeData();
if (auto *offer = selectionDevice->selectionOffer())
return offer->mimeData();
}
#endif
return &m_emptyData;
default:
return &m_emptyData;
QWaylandDataSource *source = inputDevice->dataDevice()->selectionSource();
if (source) {
return source->mimeData();
}
if (inputDevice->dataDevice()->selectionOffer())
return inputDevice->dataDevice()->selectionOffer()->mimeData();
return &m_emptyData;
}
void QWaylandClipboard::setMimeData(QMimeData *data, QClipboard::Mode mode)
{
if (mode != QClipboard::Clipboard)
return;
QWaylandInputDevice *inputDevice = mDisplay->currentInputDevice();
if (!inputDevice || !inputDevice->dataDevice())
auto *seat = mDisplay->currentInputDevice();
if (!seat)
return;
static const QString plain = QStringLiteral("text/plain");
static const QString utf8 = QStringLiteral("text/plain;charset=utf-8");
if (data && data->hasFormat(plain) && !data->hasFormat(utf8))
data->setData(utf8, data->data(plain));
inputDevice->dataDevice()->setSelectionSource(data ? new QWaylandDataSource(mDisplay->dndSelectionHandler(), data) : nullptr);
emitChanged(mode);
switch (mode) {
case QClipboard::Clipboard:
if (auto *dataDevice = seat->dataDevice()) {
dataDevice->setSelectionSource(data ? new QWaylandDataSource(mDisplay->dndSelectionHandler(), data) : nullptr);
emitChanged(mode);
}
break;
case QClipboard::Selection:
#if QT_CONFIG(wayland_client_primary_selection)
if (auto *selectionDevice = seat->primarySelectionDevice()) {
selectionDevice->setSelectionSource(data ? new QWaylandPrimarySelectionSourceV1(mDisplay->primarySelectionManager(), data) : nullptr);
emitChanged(mode);
}
#endif
break;
default:
break;
}
}
bool QWaylandClipboard::supportsMode(QClipboard::Mode mode) const
{
#if QT_CONFIG(wayland_client_primary_selection)
if (mode == QClipboard::Selection) {
auto *seat = mDisplay->currentInputDevice();
return seat && seat->primarySelectionDevice();
}
#endif
return mode == QClipboard::Clipboard;
}

View File

@ -58,7 +58,8 @@ static QString utf8Text()
QWaylandDataOffer::QWaylandDataOffer(QWaylandDisplay *display, struct ::wl_data_offer *offer)
: QtWayland::wl_data_offer(offer)
, m_mimeData(new QWaylandMimeData(this, display))
, m_display(display)
, m_mimeData(new QWaylandMimeData(this))
{
}
@ -81,14 +82,19 @@ QMimeData *QWaylandDataOffer::mimeData()
return m_mimeData.data();
}
void QWaylandDataOffer::startReceiving(const QString &mimeType, int fd)
{
receive(mimeType, fd);
wl_display_flush(m_display->wl_display());
}
void QWaylandDataOffer::data_offer_offer(const QString &mime_type)
{
m_mimeData->appendFormat(mime_type);
}
QWaylandMimeData::QWaylandMimeData(QWaylandDataOffer *dataOffer, QWaylandDisplay *display)
QWaylandMimeData::QWaylandMimeData(QWaylandAbstractDataOffer *dataOffer)
: m_dataOffer(dataOffer)
, m_display(display)
{
}
@ -140,8 +146,7 @@ QVariant QWaylandMimeData::retrieveData_sys(const QString &mimeType, QVariant::T
return QVariant();
}
m_dataOffer->receive(mime, pipefd[1]);
wl_display_flush(m_display->wl_display());
m_dataOffer->startReceiving(mime, pipefd[1]);
close(pipefd[1]);

View File

@ -65,27 +65,40 @@ namespace QtWaylandClient {
class QWaylandDisplay;
class QWaylandMimeData;
class Q_WAYLAND_CLIENT_EXPORT QWaylandDataOffer : public QtWayland::wl_data_offer
class QWaylandAbstractDataOffer
{
public:
virtual void startReceiving(const QString &mimeType, int fd) = 0;
virtual QMimeData *mimeData() = 0;
virtual ~QWaylandAbstractDataOffer() = default;
};
class Q_WAYLAND_CLIENT_EXPORT QWaylandDataOffer
: public QtWayland::wl_data_offer // needs to be the first because we do static casts from the user pointer to the wrapper
, public QWaylandAbstractDataOffer
{
public:
explicit QWaylandDataOffer(QWaylandDisplay *display, struct ::wl_data_offer *offer);
~QWaylandDataOffer() override;
QMimeData *mimeData() override;
QString firstFormat() const;
QMimeData *mimeData();
void startReceiving(const QString &mimeType, int fd) override;
protected:
void data_offer_offer(const QString &mime_type) override;
private:
QWaylandDisplay *m_display = nullptr;
QScopedPointer<QWaylandMimeData> m_mimeData;
};
class QWaylandMimeData : public QInternalMimeData {
public:
explicit QWaylandMimeData(QWaylandDataOffer *dataOffer, QWaylandDisplay *display);
explicit QWaylandMimeData(QWaylandAbstractDataOffer *dataOffer);
~QWaylandMimeData() override;
void appendFormat(const QString &mimeType);
@ -98,13 +111,12 @@ protected:
private:
int readData(int fd, QByteArray &data) const;
mutable QWaylandDataOffer *m_dataOffer = nullptr;
QWaylandDisplay *m_display = nullptr;
QWaylandAbstractDataOffer *m_dataOffer = nullptr;
mutable QStringList m_types;
mutable QHash<QString, QByteArray> m_data;
};
}
} // namespace QtWaylandClient
QT_END_NAMESPACE
#endif

View File

@ -52,7 +52,10 @@
#if QT_CONFIG(wayland_datadevice)
#include "qwaylanddatadevicemanager_p.h"
#include "qwaylanddatadevice_p.h"
#endif
#endif // QT_CONFIG(wayland_datadevice)
#if QT_CONFIG(wayland_client_primary_selection)
#include "qwaylandprimaryselectionv1_p.h"
#endif // QT_CONFIG(wayland_client_primary_selection)
#if QT_CONFIG(cursor)
#include <wayland-cursor.h>
#endif
@ -69,6 +72,7 @@
#include "qwaylandqtkey_p.h"
#include <QtWaylandClient/private/qwayland-text-input-unstable-v2.h>
#include <QtWaylandClient/private/qwayland-wp-primary-selection-unstable-v1.h>
#include <QtCore/private/qcore_unix_p.h>
@ -318,6 +322,10 @@ void QWaylandDisplay::registry_global(uint32_t id, const QString &interface, uin
mTouchExtension.reset(new QWaylandTouchExtension(this, id));
} else if (interface == QStringLiteral("zqt_key_v1")) {
mQtKeyExtension.reset(new QWaylandQtKeyExtension(this, id));
#if QT_CONFIG(wayland_client_primary_selection)
} else if (interface == QStringLiteral("zwp_primary_selection_device_manager_v1")) {
mPrimarySelectionManager.reset(new QWaylandPrimarySelectionDeviceManagerV1(this, id, 1));
#endif
} else if (interface == QStringLiteral("zwp_text_input_manager_v2") && !mClientSideInputContextRequested) {
mTextInputManager.reset(new QtWayland::zwp_text_input_manager_v2(registry, id, 1));
for (QWaylandInputDevice *inputDevice : qAsConst(mInputDevices))

View File

@ -93,6 +93,9 @@ class QWaylandScreen;
class QWaylandClientBufferIntegration;
class QWaylandWindowManagerIntegration;
class QWaylandDataDeviceManager;
#if QT_CONFIG(wayland_client_primary_selection)
class QWaylandPrimarySelectionDeviceManagerV1;
#endif
class QWaylandTouchExtension;
class QWaylandQtKeyExtension;
class QWaylandWindow;
@ -149,6 +152,9 @@ public:
QWaylandInputDevice *currentInputDevice() const { return defaultInputDevice(); }
#if QT_CONFIG(wayland_datadevice)
QWaylandDataDeviceManager *dndSelectionHandler() const { return mDndSelectionHandler.data(); }
#endif
#if QT_CONFIG(wayland_client_primary_selection)
QWaylandPrimarySelectionDeviceManagerV1 *primarySelectionManager() const { return mPrimarySelectionManager.data(); }
#endif
QtWayland::qt_surface_extension *windowExtension() const { return mWindowExtension.data(); }
QWaylandTouchExtension *touchExtension() const { return mTouchExtension.data(); }
@ -238,6 +244,9 @@ private:
QScopedPointer<QWaylandTouchExtension> mTouchExtension;
QScopedPointer<QWaylandQtKeyExtension> mQtKeyExtension;
QScopedPointer<QWaylandWindowManagerIntegration> mWindowManagerIntegration;
#if QT_CONFIG(wayland_client_primary_selection)
QScopedPointer<QWaylandPrimarySelectionDeviceManagerV1> mPrimarySelectionManager;
#endif
QScopedPointer<QtWayland::zwp_text_input_manager_v2> mTextInputManager;
QScopedPointer<QWaylandHardwareIntegration> mHardwareIntegration;
QScopedPointer<QtWayland::zxdg_output_manager_v1> mXdgOutputManager;

View File

@ -47,6 +47,9 @@
#include "qwaylanddatadevice_p.h"
#include "qwaylanddatadevicemanager_p.h"
#endif
#if QT_CONFIG(wayland_client_primary_selection)
#include "qwaylandprimaryselectionv1_p.h"
#endif
#include "qwaylandtouch_p.h"
#include "qwaylandscreen_p.h"
#include "qwaylandcursor_p.h"
@ -363,6 +366,12 @@ QWaylandInputDevice::QWaylandInputDevice(QWaylandDisplay *display, int version,
}
#endif
#if QT_CONFIG(wayland_client_primary_selection)
// TODO: Could probably decouple this more if there was a signal for new seat added
if (auto *psm = mQDisplay->primarySelectionManager())
setPrimarySelectionDevice(psm->createDevice(this));
#endif
if (mQDisplay->textInputManager())
mTextInput.reset(new QWaylandTextInput(mQDisplay, mQDisplay->textInputManager()->get_text_input(wl_seat())));
@ -446,6 +455,18 @@ QWaylandDataDevice *QWaylandInputDevice::dataDevice() const
}
#endif
#if QT_CONFIG(wayland_client_primary_selection)
void QWaylandInputDevice::setPrimarySelectionDevice(QWaylandPrimarySelectionDeviceV1 *primarySelectionDevice)
{
mPrimarySelectionDevice.reset(primarySelectionDevice);
}
QWaylandPrimarySelectionDeviceV1 *QWaylandInputDevice::primarySelectionDevice() const
{
return mPrimarySelectionDevice.data();
}
#endif
void QWaylandInputDevice::setTextInput(QWaylandTextInput *textInput)
{
mTextInput.reset(textInput);
@ -1189,6 +1210,10 @@ void QWaylandInputDevice::Keyboard::handleFocusLost()
#if QT_CONFIG(clipboard)
if (auto *dataDevice = mParent->dataDevice())
dataDevice->invalidateSelectionOffer();
#endif
#if QT_CONFIG(wayland_client_primary_selection)
if (auto *device = mParent->primarySelectionDevice())
device->invalidateSelectionOffer();
#endif
mParent->mQDisplay->handleKeyboardFocusChanged(mParent);
mRepeatTimer.stop();

View File

@ -78,11 +78,17 @@ struct wl_cursor_image;
QT_BEGIN_NAMESPACE
namespace QtWayland {
class zwp_primary_selection_device_v1;
} //namespace QtWayland
namespace QtWaylandClient {
class QWaylandWindow;
class QWaylandDisplay;
class QWaylandDataDevice;
class QWaylandDisplay;
#if QT_CONFIG(wayland_client_primary_selection)
class QWaylandPrimarySelectionDeviceV1;
#endif
class QWaylandTextInput;
#if QT_CONFIG(cursor)
class QWaylandCursorTheme;
@ -116,6 +122,11 @@ public:
QWaylandDataDevice *dataDevice() const;
#endif
#if QT_CONFIG(wayland_client_primary_selection)
void setPrimarySelectionDevice(QWaylandPrimarySelectionDeviceV1 *primarySelectionDevice);
QWaylandPrimarySelectionDeviceV1 *primarySelectionDevice() const;
#endif
void setTextInput(QWaylandTextInput *textInput);
QWaylandTextInput *textInput() const;
@ -159,6 +170,10 @@ private:
QWaylandDataDevice *mDataDevice = nullptr;
#endif
#if QT_CONFIG(wayland_client_primary_selection)
QScopedPointer<QWaylandPrimarySelectionDeviceV1> mPrimarySelectionDevice;
#endif
Keyboard *mKeyboard = nullptr;
Pointer *mPointer = nullptr;
Touch *mTouch = nullptr;

View File

@ -0,0 +1,162 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** 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-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qwaylandprimaryselectionv1_p.h"
#include "qwaylandinputdevice_p.h"
#include "qwaylanddisplay_p.h"
#include "qwaylandmimehelper_p.h"
#include <QtGui/private/qguiapplication_p.h>
#include <qpa/qplatformclipboard.h>
QT_BEGIN_NAMESPACE
namespace QtWaylandClient {
QWaylandPrimarySelectionDeviceManagerV1::QWaylandPrimarySelectionDeviceManagerV1(QWaylandDisplay *display, uint id, uint version)
: zwp_primary_selection_device_manager_v1(display->wl_registry(), id, qMin(version, uint(1)))
, m_display(display)
{
// Create devices for all seats.
// This only works if we get the global before all devices
const auto seats = m_display->inputDevices();
for (auto *seat : seats)
seat->setPrimarySelectionDevice(createDevice(seat));
}
QWaylandPrimarySelectionDeviceV1 *QWaylandPrimarySelectionDeviceManagerV1::createDevice(QWaylandInputDevice *seat)
{
return new QWaylandPrimarySelectionDeviceV1(this, seat);
}
QWaylandPrimarySelectionOfferV1::QWaylandPrimarySelectionOfferV1(QWaylandDisplay *display, ::zwp_primary_selection_offer_v1 *offer)
: zwp_primary_selection_offer_v1(offer)
, m_display(display)
, m_mimeData(new QWaylandMimeData(this))
{}
void QWaylandPrimarySelectionOfferV1::startReceiving(const QString &mimeType, int fd)
{
receive(mimeType, fd);
wl_display_flush(m_display->wl_display());
}
void QWaylandPrimarySelectionOfferV1::zwp_primary_selection_offer_v1_offer(const QString &mime_type)
{
m_mimeData->appendFormat(mime_type);
}
QWaylandPrimarySelectionDeviceV1::QWaylandPrimarySelectionDeviceV1(
QWaylandPrimarySelectionDeviceManagerV1 *manager, QWaylandInputDevice *seat)
: QtWayland::zwp_primary_selection_device_v1(manager->get_device(seat->wl_seat()))
, m_display(manager->display())
, m_seat(seat)
{
}
QWaylandPrimarySelectionDeviceV1::~QWaylandPrimarySelectionDeviceV1()
{
destroy();
}
void QWaylandPrimarySelectionDeviceV1::setSelectionSource(QWaylandPrimarySelectionSourceV1 *source)
{
if (source) {
connect(source, &QWaylandPrimarySelectionSourceV1::cancelled, this, [this]() {
m_selectionSource.reset();
QGuiApplicationPrivate::platformIntegration()->clipboard()->emitChanged(QClipboard::Selection);
});
}
set_selection(source ? source->object() : nullptr, m_seat->serial());
m_selectionSource.reset(source);
}
void QWaylandPrimarySelectionDeviceV1::zwp_primary_selection_device_v1_data_offer(zwp_primary_selection_offer_v1 *offer)
{
new QWaylandPrimarySelectionOfferV1(m_display, offer);
}
void QWaylandPrimarySelectionDeviceV1::zwp_primary_selection_device_v1_selection(zwp_primary_selection_offer_v1 *id)
{
if (id)
m_selectionOffer.reset(static_cast<QWaylandPrimarySelectionOfferV1 *>(zwp_primary_selection_offer_v1_get_user_data(id)));
else
m_selectionOffer.reset();
QGuiApplicationPrivate::platformIntegration()->clipboard()->emitChanged(QClipboard::Selection);
}
QWaylandPrimarySelectionSourceV1::QWaylandPrimarySelectionSourceV1(QWaylandPrimarySelectionDeviceManagerV1 *manager, QMimeData *mimeData)
: QtWayland::zwp_primary_selection_source_v1(manager->create_source())
, m_mimeData(mimeData)
{
if (!mimeData)
return;
for (auto &format : mimeData->formats())
offer(format);
}
QWaylandPrimarySelectionSourceV1::~QWaylandPrimarySelectionSourceV1()
{
destroy();
}
void QWaylandPrimarySelectionSourceV1::zwp_primary_selection_source_v1_send(const QString &mime_type, int32_t fd)
{
QByteArray content = QWaylandMimeHelper::getByteArray(m_mimeData, mime_type);
if (!content.isEmpty()) {
// Create a sigpipe handler that does nothing, or clients may be forced to terminate
// if the pipe is closed in the other end.
struct sigaction action, oldAction;
action.sa_handler = SIG_IGN;
sigemptyset (&action.sa_mask);
action.sa_flags = 0;
sigaction(SIGPIPE, &action, &oldAction);
write(fd, content.constData(), size_t(content.size()));
sigaction(SIGPIPE, &oldAction, nullptr);
}
close(fd);
}
} // namespace QtWaylandClient
QT_END_NAMESPACE

View File

@ -0,0 +1,148 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** 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-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef QWAYLANDPRIMARYSELECTIONV1_P_H
#define QWAYLANDPRIMARYSELECTIONV1_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtWaylandClient/private/qwayland-wp-primary-selection-unstable-v1.h>
#include <QtWaylandClient/private/qtwaylandclientglobal_p.h>
#include <QtWaylandClient/private/qwaylanddataoffer_p.h>
#include <QtCore/QObject>
QT_REQUIRE_CONFIG(wayland_client_primary_selection);
QT_BEGIN_NAMESPACE
class QMimeData;
namespace QtWaylandClient {
class QWaylandInputDevice;
class QWaylandPrimarySelectionDeviceV1;
class QWaylandPrimarySelectionDeviceManagerV1 : public QtWayland::zwp_primary_selection_device_manager_v1
{
public:
explicit QWaylandPrimarySelectionDeviceManagerV1(QWaylandDisplay *display, uint id, uint version);
QWaylandPrimarySelectionDeviceV1 *createDevice(QWaylandInputDevice *seat);
QWaylandDisplay *display() const { return m_display; }
private:
QWaylandDisplay *m_display = nullptr;
};
class QWaylandPrimarySelectionOfferV1 : public QtWayland::zwp_primary_selection_offer_v1, public QWaylandAbstractDataOffer
{
public:
explicit QWaylandPrimarySelectionOfferV1(QWaylandDisplay *display, ::zwp_primary_selection_offer_v1 *offer);
~QWaylandPrimarySelectionOfferV1() override { destroy(); }
void startReceiving(const QString &mimeType, int fd) override;
QMimeData *mimeData() override { return m_mimeData.data(); }
protected:
void zwp_primary_selection_offer_v1_offer(const QString &mime_type) override;
private:
QWaylandDisplay *m_display = nullptr;
QScopedPointer<QWaylandMimeData> m_mimeData;
};
class Q_WAYLAND_CLIENT_EXPORT QWaylandPrimarySelectionSourceV1 : public QObject, public QtWayland::zwp_primary_selection_source_v1
{
Q_OBJECT
public:
explicit QWaylandPrimarySelectionSourceV1(QWaylandPrimarySelectionDeviceManagerV1 *manager, QMimeData *mimeData);
~QWaylandPrimarySelectionSourceV1() override;
QMimeData *mimeData() const { return m_mimeData; }
signals:
void cancelled();
protected:
void zwp_primary_selection_source_v1_send(const QString &mime_type, int32_t fd) override;
void zwp_primary_selection_source_v1_cancelled() override { emit cancelled(); }
private:
QWaylandDisplay *m_display = nullptr;
QMimeData *m_mimeData = nullptr;
};
class QWaylandPrimarySelectionDeviceV1 : public QObject, public QtWayland::zwp_primary_selection_device_v1
{
Q_OBJECT
QWaylandPrimarySelectionDeviceV1(QWaylandPrimarySelectionDeviceManagerV1 *manager, QWaylandInputDevice *seat);
public:
~QWaylandPrimarySelectionDeviceV1() override;
QWaylandPrimarySelectionOfferV1 *selectionOffer() const { return m_selectionOffer.data(); }
void invalidateSelectionOffer() { m_selectionOffer.reset(); }
QWaylandPrimarySelectionSourceV1 *selectionSource() const { return m_selectionSource.data(); }
void setSelectionSource(QWaylandPrimarySelectionSourceV1 *source);
protected:
void zwp_primary_selection_device_v1_data_offer(struct ::zwp_primary_selection_offer_v1 *offer) override;
void zwp_primary_selection_device_v1_selection(struct ::zwp_primary_selection_offer_v1 *id) override;
private:
QWaylandDisplay *m_display = nullptr;
QWaylandInputDevice *m_seat = nullptr;
QScopedPointer<QWaylandPrimarySelectionOfferV1> m_selectionOffer;
QScopedPointer<QWaylandPrimarySelectionSourceV1> m_selectionSource;
friend class QWaylandPrimarySelectionDeviceManagerV1;
};
} // namespace QtWaylandClient
QT_END_NAMESPACE
#endif // QWAYLANDPRIMARYSELECTIONV1_P_H

View File

@ -6,6 +6,7 @@ SUBDIRS += \
fullscreenshellv1 \
iviapplication \
output \
primaryselectionv1 \
seatv4 \
seatv5 \
surface \

View File

@ -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

View File

@ -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"

View File

@ -124,6 +124,23 @@ public:
return nullptr;
}
/*!
* \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
*/

View File

@ -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;
}

View File

@ -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

View File

@ -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 {