diff --git a/src/3rdparty/protocol/qt_attribution.json b/src/3rdparty/protocol/qt_attribution.json index 7e068f75..e6f90698 100644 --- a/src/3rdparty/protocol/qt_attribution.json +++ b/src/3rdparty/protocol/qt_attribution.json @@ -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", diff --git a/src/3rdparty/protocol/wp-primary-selection-unstable-v1.xml b/src/3rdparty/protocol/wp-primary-selection-unstable-v1.xml new file mode 100644 index 00000000..e5a39e34 --- /dev/null +++ b/src/3rdparty/protocol/wp-primary-selection-unstable-v1.xml @@ -0,0 +1,225 @@ + + + + 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. + + + + 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. + + + + + 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. + + + + + Create a new primary selection source. + + + + + + + Create a new data device for a given seat. + + + + + + + + Destroy the primary selection device manager. + + + + + + + + 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. + + + + + + + + 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. + + + + + + + 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. + + + + + + + Destroy the primary selection device. + + + + + + + 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. + + + + + 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. + + + + + + + + Destroy the primary selection offer. + + + + + + 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. + + + + + + + + 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. + + + + + This request adds a mime type to the set of mime types advertised to + targets. Can be called several times to offer multiple types. + + + + + + + Destroy the primary selection source. + + + + + + Request for the current primary selection contents from the client. + Send the specified mime type over the passed file descriptor, then + close it. + + + + + + + + This primary selection source is no longer valid. The client should + clean up and destroy this primary selection source. + + + + diff --git a/src/client/client.pro b/src/client/client.pro index 4233ac95..3793cd8e 100644 --- a/src/client/client.pro +++ b/src/client/client.pro @@ -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 @@ -116,6 +117,11 @@ qtConfig(wayland-datadevice) { qwaylanddatasource.cpp } +qtConfig(wayland-client-primary-selection) { + HEADERS += qwaylandprimaryselectionv1_p.h + SOURCES += qwaylandprimaryselectionv1.cpp +} + qtConfig(draganddrop) { HEADERS += \ qwaylanddnd_p.h diff --git a/src/client/configure.json b/src/client/configure.json index 91024c9d..b63031f2 100644 --- a/src/client/configure.json +++ b/src/client/configure.json @@ -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", diff --git a/src/client/qwaylandclipboard.cpp b/src/client/qwaylandclipboard.cpp index 60820da9..369c6ec0 100644 --- a/src/client/qwaylandclipboard.cpp +++ b/src/client/qwaylandclipboard.cpp @@ -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; } diff --git a/src/client/qwaylanddataoffer.cpp b/src/client/qwaylanddataoffer.cpp index 3da16ed0..4c06277f 100644 --- a/src/client/qwaylanddataoffer.cpp +++ b/src/client/qwaylanddataoffer.cpp @@ -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]); diff --git a/src/client/qwaylanddataoffer_p.h b/src/client/qwaylanddataoffer_p.h index 5412400a..9cf1483c 100644 --- a/src/client/qwaylanddataoffer_p.h +++ b/src/client/qwaylanddataoffer_p.h @@ -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 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 @@ class QWaylandMimeData : public QInternalMimeData { 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 m_data; }; -} +} // namespace QtWaylandClient QT_END_NAMESPACE #endif diff --git a/src/client/qwaylanddisplay.cpp b/src/client/qwaylanddisplay.cpp index 78524f6f..a1c177ec 100644 --- a/src/client/qwaylanddisplay.cpp +++ b/src/client/qwaylanddisplay.cpp @@ -51,7 +51,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 #endif @@ -68,6 +71,7 @@ #include "qwaylandqtkey_p.h" #include +#include #include @@ -307,6 +311,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)) diff --git a/src/client/qwaylanddisplay_p.h b/src/client/qwaylanddisplay_p.h index 7cfbc19b..a525817d 100644 --- a/src/client/qwaylanddisplay_p.h +++ b/src/client/qwaylanddisplay_p.h @@ -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; @@ -148,6 +151,9 @@ class Q_WAYLAND_CLIENT_EXPORT QWaylandDisplay : public QObject, public QtWayland 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(); } @@ -236,6 +242,9 @@ public slots: QScopedPointer mTouchExtension; QScopedPointer mQtKeyExtension; QScopedPointer mWindowManagerIntegration; +#if QT_CONFIG(wayland_client_primary_selection) + QScopedPointer mPrimarySelectionManager; +#endif QScopedPointer mTextInputManager; QScopedPointer mHardwareIntegration; QScopedPointer mXdgOutputManager; diff --git a/src/client/qwaylandinputdevice.cpp b/src/client/qwaylandinputdevice.cpp index 8f3df8e4..eefd0488 100644 --- a/src/client/qwaylandinputdevice.cpp +++ b/src/client/qwaylandinputdevice.cpp @@ -46,6 +46,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" @@ -364,6 +367,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()))); @@ -447,6 +456,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); @@ -937,6 +958,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(); diff --git a/src/client/qwaylandinputdevice_p.h b/src/client/qwaylandinputdevice_p.h index 143e1122..cfaa5d7b 100644 --- a/src/client/qwaylandinputdevice_p.h +++ b/src/client/qwaylandinputdevice_p.h @@ -77,11 +77,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; @@ -115,6 +121,11 @@ class Q_WAYLAND_CLIENT_EXPORT QWaylandInputDevice 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; @@ -157,6 +168,10 @@ class Q_WAYLAND_CLIENT_EXPORT QWaylandInputDevice QWaylandDataDevice *mDataDevice = nullptr; #endif +#if QT_CONFIG(wayland_client_primary_selection) + QScopedPointer mPrimarySelectionDevice; +#endif + Keyboard *mKeyboard = nullptr; Pointer *mPointer = nullptr; Touch *mTouch = nullptr; diff --git a/src/client/qwaylandprimaryselectionv1.cpp b/src/client/qwaylandprimaryselectionv1.cpp new file mode 100644 index 00000000..3ddf6dac --- /dev/null +++ b/src/client/qwaylandprimaryselectionv1.cpp @@ -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 + +#include + +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(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 diff --git a/src/client/qwaylandprimaryselectionv1_p.h b/src/client/qwaylandprimaryselectionv1_p.h new file mode 100644 index 00000000..b165c51b --- /dev/null +++ b/src/client/qwaylandprimaryselectionv1_p.h @@ -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 + +#include +#include + +#include + +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 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 m_selectionOffer; + QScopedPointer m_selectionSource; + friend class QWaylandPrimarySelectionDeviceManagerV1; +}; + +} // namespace QtWaylandClient + +QT_END_NAMESPACE + +#endif // QWAYLANDPRIMARYSELECTIONV1_P_H diff --git a/sync.profile b/sync.profile index 087bfcf5..6bbb18ef 100644 --- a/sync.profile +++ b/sync.profile @@ -27,6 +27,7 @@ "^qwayland-text-input-unstable-v2.h", "^qwayland-touch-extension.h", "^qwayland-wayland.h", + "^qwayland-wp-primary-selection-unstable-v1.h", "^qwayland-xdg-output-unstable-v1.h", "^wayland-hardware-integration-client-protocol.h", "^wayland-qt-windowmanager-client-protocol.h", @@ -36,6 +37,7 @@ "^wayland-text-input-unstable-v2-client-protocol.h", "^wayland-touch-extension-client-protocol.h", "^wayland-wayland-client-protocol.h", + "^wayland-wp-primary-selection-unstable-v1-client-protocol.h", "^wayland-xdg-output-unstable-v1-client-protocol.h", ], "$basedir/src/plugins/shellintegration/xdg-shell" => [ diff --git a/tests/auto/client/client.pro b/tests/auto/client/client.pro index 06c1cb87..80694273 100644 --- a/tests/auto/client/client.pro +++ b/tests/auto/client/client.pro @@ -6,6 +6,7 @@ SUBDIRS += \ fullscreenshellv1 \ iviapplication \ output \ + primaryselectionv1 \ seatv4 \ surface \ wl_connect \ diff --git a/tests/auto/client/primaryselectionv1/primaryselectionv1.pro b/tests/auto/client/primaryselectionv1/primaryselectionv1.pro new file mode 100644 index 00000000..9d00562d --- /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 00000000..281e4c5d --- /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 + +#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 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()->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()->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 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 875b7d05..254465ee 100644 --- a/tests/auto/client/shared/corecompositor.h +++ b/tests/auto/client/shared/corecompositor.h @@ -124,6 +124,23 @@ class CoreCompositor return nullptr; } + /*! + * \brief Returns the nth global with the given type, if any + */ + template + global_type *get(int index) + { + warnIfNotLockedByThread(Q_FUNC_INFO); + for (auto *global : qAsConst(m_globals)) { + if (auto *casted = qobject_cast(global)) { + if (index--) + continue; + return casted; + } + } + return nullptr; + } + /*! * \brief Returns all globals with the given type, if any */ diff --git a/tests/auto/client/shared/coreprotocol.cpp b/tests/auto/client/shared/coreprotocol.cpp index 729d481f..8f79124c 100644 --- a/tests/auto/client/shared/coreprotocol.cpp +++ b/tests/auto/client/shared/coreprotocol.cpp @@ -345,6 +345,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; } @@ -355,6 +356,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 5cef476c..0ee8d76c 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 @@ -313,6 +313,7 @@ class Keyboard : public QObject, public QtWaylandServer::wl_keyboard 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 05bf32c8..b8094a17 100644 --- a/tests/auto/client/shared/mockcompositor.h +++ b/tests/auto/client/shared/mockcompositor.h @@ -36,10 +36,16 @@ #include -#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 { diff --git a/include/QtWaylandClient/headers.pri b/include/QtWaylandClient/headers.pri index 635f03c..1c7f2eb 100644 --- a/include/QtWaylandClient/headers.pri +++ b/include/QtWaylandClient/headers.pri @@ -3,4 +3,4 @@ SYNCQT.GENERATED_HEADER_FILES = QWaylandClientExtension QWaylandClientExtensionT SYNCQT.PRIVATE_HEADER_FILES = qtwaylandclientglobal_p.h qwaylandabstractdecoration_p.h qwaylandbuffer_p.h qwaylandclipboard_p.h qwaylandcursor_p.h qwaylanddatadevice_p.h qwaylanddatadevicemanager_p.h qwaylanddataoffer_p.h qwaylanddatasource_p.h qwaylanddecorationfactory_p.h qwaylanddecorationplugin_p.h qwaylanddisplay_p.h qwaylanddnd_p.h qwaylandextendedsurface_p.h qwaylandinputcontext_p.h qwaylandinputdevice_p.h qwaylandintegration_p.h qwaylandnativeinterface_p.h qwaylandqtkey_p.h qwaylandscreen_p.h qwaylandshellsurface_p.h qwaylandshm_p.h qwaylandshmbackingstore_p.h qwaylandshmwindow_p.h qwaylandsubsurface_p.h qwaylandtouch_p.h qwaylandwindow_p.h qwaylandwindowmanagerintegration_p.h global/qwaylandclientextension_p.h hardwareintegration/qwaylandclientbufferintegration_p.h hardwareintegration/qwaylandclientbufferintegrationfactory_p.h hardwareintegration/qwaylandclientbufferintegrationplugin_p.h hardwareintegration/qwaylandhardwareintegration_p.h hardwareintegration/qwaylandserverbufferintegration_p.h hardwareintegration/qwaylandserverbufferintegrationfactory_p.h hardwareintegration/qwaylandserverbufferintegrationplugin_p.h inputdeviceintegration/qwaylandinputdeviceintegration_p.h inputdeviceintegration/qwaylandinputdeviceintegrationfactory_p.h inputdeviceintegration/qwaylandinputdeviceintegrationplugin_p.h shellintegration/qwaylandshellintegration_p.h shellintegration/qwaylandshellintegrationfactory_p.h shellintegration/qwaylandshellintegrationplugin_p.h SYNCQT.QPA_HEADER_FILES = SYNCQT.CLEAN_HEADER_FILES = qtwaylandclientglobal.h global/qwaylandclientextension.h -SYNCQT.INJECTIONS = src/client/qwayland-hardware-integration.h:^5.13.2/QtWaylandClient/private/qwayland-hardware-integration.h src/client/qwayland-qt-windowmanager.h:^5.13.2/QtWaylandClient/private/qwayland-qt-windowmanager.h src/client/qwayland-qt-key-unstable-v1.h:^5.13.2/QtWaylandClient/private/qwayland-qt-key-unstable-v1.h src/client/qwayland-server-buffer-extension.h:^5.13.2/QtWaylandClient/private/qwayland-server-buffer-extension.h src/client/qwayland-surface-extension.h:^5.13.2/QtWaylandClient/private/qwayland-surface-extension.h src/client/qwayland-text-input-unstable-v2.h:^5.13.2/QtWaylandClient/private/qwayland-text-input-unstable-v2.h src/client/qwayland-touch-extension.h:^5.13.2/QtWaylandClient/private/qwayland-touch-extension.h src/client/qwayland-wayland.h:^5.13.2/QtWaylandClient/private/qwayland-wayland.h src/client/qwayland-xdg-output-unstable-v1.h:^5.13.2/QtWaylandClient/private/qwayland-xdg-output-unstable-v1.h src/client/wayland-hardware-integration-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-hardware-integration-client-protocol.h src/client/wayland-qt-windowmanager-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-qt-windowmanager-client-protocol.h src/client/wayland-qt-key-unstable-v1-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-qt-key-unstable-v1-client-protocol.h src/client/wayland-server-buffer-extension-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-server-buffer-extension-client-protocol.h src/client/wayland-surface-extension-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-surface-extension-client-protocol.h src/client/wayland-text-input-unstable-v2-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-text-input-unstable-v2-client-protocol.h src/client/wayland-touch-extension-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-touch-extension-client-protocol.h src/client/wayland-wayland-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-wayland-client-protocol.h src/client/wayland-xdg-output-unstable-v1-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-xdg-output-unstable-v1-client-protocol.h +SYNCQT.INJECTIONS = src/client/qwayland-wp-primary-selection-unstable-v1.h:^5.13.2/QtWaylandClient/private/qwayland-wp-primary-selection-unstable-v1.h src/client/wayland-wp-primary-selection-unstable-v1-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-wp-primary-selection-unstable-v1-client-protocol.h src/client/qwayland-hardware-integration.h:^5.13.2/QtWaylandClient/private/qwayland-hardware-integration.h src/client/qwayland-qt-windowmanager.h:^5.13.2/QtWaylandClient/private/qwayland-qt-windowmanager.h src/client/qwayland-qt-key-unstable-v1.h:^5.13.2/QtWaylandClient/private/qwayland-qt-key-unstable-v1.h src/client/qwayland-server-buffer-extension.h:^5.13.2/QtWaylandClient/private/qwayland-server-buffer-extension.h src/client/qwayland-surface-extension.h:^5.13.2/QtWaylandClient/private/qwayland-surface-extension.h src/client/qwayland-text-input-unstable-v2.h:^5.13.2/QtWaylandClient/private/qwayland-text-input-unstable-v2.h src/client/qwayland-touch-extension.h:^5.13.2/QtWaylandClient/private/qwayland-touch-extension.h src/client/qwayland-wayland.h:^5.13.2/QtWaylandClient/private/qwayland-wayland.h src/client/qwayland-xdg-output-unstable-v1.h:^5.13.2/QtWaylandClient/private/qwayland-xdg-output-unstable-v1.h src/client/wayland-hardware-integration-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-hardware-integration-client-protocol.h src/client/wayland-qt-windowmanager-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-qt-windowmanager-client-protocol.h src/client/wayland-qt-key-unstable-v1-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-qt-key-unstable-v1-client-protocol.h src/client/wayland-server-buffer-extension-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-server-buffer-extension-client-protocol.h src/client/wayland-surface-extension-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-surface-extension-client-protocol.h src/client/wayland-text-input-unstable-v2-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-text-input-unstable-v2-client-protocol.h src/client/wayland-touch-extension-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-touch-extension-client-protocol.h src/client/wayland-wayland-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-wayland-client-protocol.h src/client/wayland-xdg-output-unstable-v1-client-protocol.h:^5.13.2/QtWaylandClient/private/wayland-xdg-output-unstable-v1-client-protocol.h