diff --git a/src/plugins/platforms/wayland/qwaylandclipboard.cpp b/src/plugins/platforms/wayland/qwaylandclipboard.cpp index 369c6ec07db..81f48e05ee3 100644 --- a/src/plugins/platforms/wayland/qwaylandclipboard.cpp +++ b/src/plugins/platforms/wayland/qwaylandclipboard.cpp @@ -93,8 +93,10 @@ QMimeData *QWaylandClipboard::mimeData(QClipboard::Mode mode) void QWaylandClipboard::setMimeData(QMimeData *data, QClipboard::Mode mode) { auto *seat = mDisplay->currentInputDevice(); - if (!seat) + if (!seat) { + qCWarning(lcQpaWayland) << "Can't set clipboard contents with no wl_seats available"; return; + } static const QString plain = QStringLiteral("text/plain"); static const QString utf8 = QStringLiteral("text/plain;charset=utf-8"); @@ -135,14 +137,20 @@ bool QWaylandClipboard::supportsMode(QClipboard::Mode mode) const bool QWaylandClipboard::ownsMode(QClipboard::Mode mode) const { - if (mode != QClipboard::Clipboard) + QWaylandInputDevice *seat = mDisplay->currentInputDevice(); + if (!seat) return false; - QWaylandInputDevice *inputDevice = mDisplay->currentInputDevice(); - if (!inputDevice || !inputDevice->dataDevice()) + switch (mode) { + case QClipboard::Clipboard: + return seat->dataDevice() && seat->dataDevice()->selectionSource() != nullptr; +#if QT_CONFIG(wayland_client_primary_selection) + case QClipboard::Selection: + return seat->primarySelectionDevice() && seat->primarySelectionDevice()->selectionSource() != nullptr; +#endif + default: return false; - - return inputDevice->dataDevice()->selectionSource() != nullptr; + } } } diff --git a/src/plugins/platforms/wayland/qwaylanddatadevice.cpp b/src/plugins/platforms/wayland/qwaylanddatadevice.cpp index fc3c7077a84..f7d135e49f3 100644 --- a/src/plugins/platforms/wayland/qwaylanddatadevice.cpp +++ b/src/plugins/platforms/wayland/qwaylanddatadevice.cpp @@ -81,7 +81,14 @@ QWaylandDataOffer *QWaylandDataDevice::selectionOffer() const void QWaylandDataDevice::invalidateSelectionOffer() { + if (m_selectionOffer.isNull()) + return; + m_selectionOffer.reset(); + +#if QT_CONFIG(clipboard) + QGuiApplicationPrivate::platformIntegration()->clipboard()->emitChanged(QClipboard::Clipboard); +#endif } QWaylandDataSource *QWaylandDataDevice::selectionSource() const diff --git a/src/plugins/platforms/wayland/qwaylandprimaryselectionv1.cpp b/src/plugins/platforms/wayland/qwaylandprimaryselectionv1.cpp index 3ddf6dac376..832f967804a 100644 --- a/src/plugins/platforms/wayland/qwaylandprimaryselectionv1.cpp +++ b/src/plugins/platforms/wayland/qwaylandprimaryselectionv1.cpp @@ -96,6 +96,15 @@ QWaylandPrimarySelectionDeviceV1::~QWaylandPrimarySelectionDeviceV1() destroy(); } +void QWaylandPrimarySelectionDeviceV1::invalidateSelectionOffer() +{ + if (!m_selectionOffer) + return; + + m_selectionOffer.reset(); + QGuiApplicationPrivate::platformIntegration()->clipboard()->emitChanged(QClipboard::Selection); +} + void QWaylandPrimarySelectionDeviceV1::setSelectionSource(QWaylandPrimarySelectionSourceV1 *source) { if (source) { diff --git a/src/plugins/platforms/wayland/qwaylandprimaryselectionv1_p.h b/src/plugins/platforms/wayland/qwaylandprimaryselectionv1_p.h index b165c51b8c6..3f0a42a6702 100644 --- a/src/plugins/platforms/wayland/qwaylandprimaryselectionv1_p.h +++ b/src/plugins/platforms/wayland/qwaylandprimaryselectionv1_p.h @@ -125,7 +125,7 @@ class QWaylandPrimarySelectionDeviceV1 : public QObject, public QtWayland::zwp_p public: ~QWaylandPrimarySelectionDeviceV1() override; QWaylandPrimarySelectionOfferV1 *selectionOffer() const { return m_selectionOffer.data(); } - void invalidateSelectionOffer() { m_selectionOffer.reset(); } + void invalidateSelectionOffer(); QWaylandPrimarySelectionSourceV1 *selectionSource() const { return m_selectionSource.data(); } void setSelectionSource(QWaylandPrimarySelectionSourceV1 *source); diff --git a/src/plugins/platforms/wayland/qwaylandwindow.cpp b/src/plugins/platforms/wayland/qwaylandwindow.cpp index 2704705e7e7..bffb5701543 100644 --- a/src/plugins/platforms/wayland/qwaylandwindow.cpp +++ b/src/plugins/platforms/wayland/qwaylandwindow.cpp @@ -1081,25 +1081,6 @@ QVariant QWaylandWindow::property(const QString &name, const QVariant &defaultVa void QWaylandWindow::timerEvent(QTimerEvent *event) { - if (event->timerId() == mFallbackUpdateTimerId) { - killTimer(mFallbackUpdateTimerId); - mFallbackUpdateTimerId = -1; - qCDebug(lcWaylandBackingstore) << "mFallbackUpdateTimer timed out"; - - if (!isExposed()) { - qCDebug(lcWaylandBackingstore) << "Fallback update timer: Window not exposed," - << "not delivering update request."; - return; - } - - if (mWaitingForUpdate && hasPendingUpdateRequest() && !mWaitingForFrameCallback) { - qCWarning(lcWaylandBackingstore) << "Delivering update request through fallback timer," - << "may not be in sync with display"; - deliverUpdateRequest(); - } - } - - if (mFrameCallbackTimerId.testAndSetOrdered(event->timerId(), -1)) { killTimer(event->timerId()); qCDebug(lcWaylandBackingstore) << "Didn't receive frame callback in time, window should now be inexposed"; @@ -1111,6 +1092,7 @@ void QWaylandWindow::timerEvent(QTimerEvent *event) void QWaylandWindow::requestUpdate() { + qCDebug(lcWaylandBackingstore) << "requestUpdate"; Q_ASSERT(hasPendingUpdateRequest()); // should be set by QPA // If we have a frame callback all is good and will be taken care of there @@ -1118,20 +1100,17 @@ void QWaylandWindow::requestUpdate() return; // If we've already called deliverUpdateRequest(), but haven't seen any attach+commit/swap yet - if (mWaitingForUpdate) { - // Ideally, we should just have returned here, but we're not guaranteed that the client - // will actually update, so start this timer to deliver another request update after a while - // *IF* the client doesn't update. - int fallbackTimeout = 100; - mFallbackUpdateTimerId = startTimer(fallbackTimeout); - return; - } + // This is a somewhat redundant behavior and might indicate a bug in the calling code, so log + // here so we can get this information when debugging update/frame callback issues. + // Continue as nothing happened, though. + if (mWaitingForUpdate) + qCDebug(lcWaylandBackingstore) << "requestUpdate called twice without committing anything"; // Some applications (such as Qt Quick) depend on updates being delivered asynchronously, // so use invokeMethod to delay the delivery a bit. QMetaObject::invokeMethod(this, [this] { // Things might have changed in the meantime - if (hasPendingUpdateRequest() && !mWaitingForUpdate && !mWaitingForFrameCallback) + if (hasPendingUpdateRequest() && !mWaitingForFrameCallback) deliverUpdateRequest(); }, Qt::QueuedConnection); } @@ -1141,6 +1120,7 @@ void QWaylandWindow::requestUpdate() // Can be called from the render thread (without locking anything) so make sure to not make races in this method. void QWaylandWindow::handleUpdate() { + qCDebug(lcWaylandBackingstore) << "handleUpdate" << QThread::currentThread(); // TODO: Should sync subsurfaces avoid requesting frame callbacks? QReadLocker lock(&mSurfaceLock); if (!mSurface) @@ -1151,15 +1131,6 @@ void QWaylandWindow::handleUpdate() mFrameCallback = nullptr; } - if (mFallbackUpdateTimerId != -1) { - // Ideally, we would stop the fallback timer here, but since we're on another thread, - // it's not allowed. Instead we set mFallbackUpdateTimer to -1 here, so we'll just - // ignore it if it times out before it's cleaned up by the invokeMethod call. - int id = mFallbackUpdateTimerId; - mFallbackUpdateTimerId = -1; - QMetaObject::invokeMethod(this, [this, id] { killTimer(id); }, Qt::QueuedConnection); - } - mFrameCallback = mSurface->frame(); wl_callback_add_listener(mFrameCallback, &QWaylandWindow::callbackListener, this); mWaitingForFrameCallback = true; @@ -1179,6 +1150,7 @@ void QWaylandWindow::handleUpdate() void QWaylandWindow::deliverUpdateRequest() { + qCDebug(lcWaylandBackingstore) << "deliverUpdateRequest"; mWaitingForUpdate = true; QPlatformWindow::deliverUpdateRequest(); } diff --git a/src/plugins/platforms/wayland/qwaylandwindow_p.h b/src/plugins/platforms/wayland/qwaylandwindow_p.h index 8d582eb7e4c..c488c2e3f42 100644 --- a/src/plugins/platforms/wayland/qwaylandwindow_p.h +++ b/src/plugins/platforms/wayland/qwaylandwindow_p.h @@ -230,7 +230,6 @@ protected: // True when we have called deliverRequestUpdate, but the client has not yet attached a new buffer bool mWaitingForUpdate = false; - int mFallbackUpdateTimerId = -1; // Started when waiting for app to commit QMutex mResizeLock; bool mWaitingToApplyConfigure = false; diff --git a/src/plugins/platforms/wayland/qwaylandwindowmanagerintegration.cpp b/src/plugins/platforms/wayland/qwaylandwindowmanagerintegration.cpp index 17ae8a5ae85..dd1acaf7215 100644 --- a/src/plugins/platforms/wayland/qwaylandwindowmanagerintegration.cpp +++ b/src/plugins/platforms/wayland/qwaylandwindowmanagerintegration.cpp @@ -110,13 +110,17 @@ void QWaylandWindowManagerIntegration::windowmanager_quit() void QWaylandWindowManagerIntegration::openUrl_helper(const QUrl &url) { Q_ASSERT(isInitialized()); - QByteArray data = url.toString().toUtf8(); + QString data = url.toString(); static const int chunkSize = 128; while (!data.isEmpty()) { - QByteArray chunk = data.left(chunkSize); + QString chunk = data.left(chunkSize); data = data.mid(chunkSize); - open_url(!data.isEmpty(), QString::fromUtf8(chunk)); + if (chunk.at(chunk.size() - 1).isHighSurrogate() && !data.isEmpty()) { + chunk.append(data.at(0)); + data = data.mid(1); + } + open_url(!data.isEmpty(), chunk); } } diff --git a/tests/auto/wayland/client/tst_client.cpp b/tests/auto/wayland/client/tst_client.cpp index e19acff7c0a..499a93a1dde 100644 --- a/tests/auto/wayland/client/tst_client.cpp +++ b/tests/auto/wayland/client/tst_client.cpp @@ -515,7 +515,8 @@ void tst_WaylandClient::longWindowTitleWithUtf16Characters() int main(int argc, char **argv) { - setenv("XDG_RUNTIME_DIR", ".", 1); + QTemporaryDir tmpRuntimeDir; + setenv("XDG_RUNTIME_DIR", tmpRuntimeDir.path().toLocal8Bit(), 1); setenv("QT_QPA_PLATFORM", "wayland", 1); // force QGuiApplication to use wayland plugin MockCompositor compositor; diff --git a/tests/auto/wayland/datadevicev1/tst_datadevicev1.cpp b/tests/auto/wayland/datadevicev1/tst_datadevicev1.cpp index e3babceb53e..1568b3b9680 100644 --- a/tests/auto/wayland/datadevicev1/tst_datadevicev1.cpp +++ b/tests/auto/wayland/datadevicev1/tst_datadevicev1.cpp @@ -59,6 +59,7 @@ private slots: void pasteUtf8(); void destroysPreviousSelection(); void destroysSelectionWithSurface(); + void destroysSelectionOnLeave(); void dragWithoutFocus(); }; @@ -215,6 +216,35 @@ void tst_datadevicev1::destroysSelectionWithSurface() QCOMPOSITOR_TRY_COMPARE(dataDevice()->m_sentSelectionOffers.size(), 0); } +void tst_datadevicev1::destroysSelectionOnLeave() +{ + QRasterWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + exec([&] { + auto *offer = dataDevice()->sendDataOffer(client(), {"text/plain"}); + dataDevice()->sendSelection(offer); + + auto *surface = xdgSurface()->m_surface; + keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol + }); + + QTRY_VERIFY(QGuiApplication::clipboard()->mimeData(QClipboard::Clipboard)); + QTRY_VERIFY(QGuiApplication::clipboard()->mimeData(QClipboard::Clipboard)->hasText()); + + QSignalSpy dataChangedSpy(QGuiApplication::clipboard(), &QClipboard::dataChanged); + + exec([&] { + auto *surface = xdgSurface()->m_surface; + keyboard()->sendLeave(surface); + }); + + QTRY_COMPARE(dataChangedSpy.count(), 1); + QVERIFY(!QGuiApplication::clipboard()->mimeData(QClipboard::Clipboard)->hasText()); +} + // The application should not crash if it attempts to start a drag operation // when it doesn't have input focus (QTBUG-76368) void tst_datadevicev1::dragWithoutFocus() diff --git a/tests/auto/wayland/fullscreenshellv1/tst_fullscreenshellv1.cpp b/tests/auto/wayland/fullscreenshellv1/tst_fullscreenshellv1.cpp index f93d9fbc5d2..55158474c3e 100644 --- a/tests/auto/wayland/fullscreenshellv1/tst_fullscreenshellv1.cpp +++ b/tests/auto/wayland/fullscreenshellv1/tst_fullscreenshellv1.cpp @@ -93,7 +93,8 @@ void tst_WaylandClientFullScreenShellV1::createDestroyWindow() int main(int argc, char **argv) { - setenv("XDG_RUNTIME_DIR", ".", 1); + QTemporaryDir tmpRuntimeDir; + setenv("XDG_RUNTIME_DIR", tmpRuntimeDir.path().toLocal8Bit(), 1); setenv("QT_QPA_PLATFORM", "wayland", 1); // force QGuiApplication to use wayland plugin setenv("QT_WAYLAND_SHELL_INTEGRATION", "fullscreen-shell-v1", 1); setenv("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1", 1); // window decorations don't make much sense here diff --git a/tests/auto/wayland/inputcontext/tst_inputcontext.cpp b/tests/auto/wayland/inputcontext/tst_inputcontext.cpp index 7c0132e35a1..1f07eb5203a 100644 --- a/tests/auto/wayland/inputcontext/tst_inputcontext.cpp +++ b/tests/auto/wayland/inputcontext/tst_inputcontext.cpp @@ -171,7 +171,8 @@ void tst_inputcontext::inputContextReconfigurationWhenTogglingTextInputExtension int main(int argc, char *argv[]) { - qputenv("XDG_RUNTIME_DIR", "."); + QTemporaryDir tmpRuntimeDir; + qputenv("XDG_RUNTIME_DIR", tmpRuntimeDir.path().toLocal8Bit()); qputenv("QT_QPA_PLATFORM", "wayland"); tst_inputcontext tc; diff --git a/tests/auto/wayland/primaryselectionv1/tst_primaryselectionv1.cpp b/tests/auto/wayland/primaryselectionv1/tst_primaryselectionv1.cpp index 216db85cd6b..ee9fa110e4b 100644 --- a/tests/auto/wayland/primaryselectionv1/tst_primaryselectionv1.cpp +++ b/tests/auto/wayland/primaryselectionv1/tst_primaryselectionv1.cpp @@ -261,6 +261,7 @@ private slots: void pasteAscii(); void pasteUtf8(); void destroysPreviousSelection(); + void destroysSelectionOnLeave(); void copy(); }; @@ -411,6 +412,35 @@ void tst_primaryselectionv1::destroysPreviousSelection() QCOMPOSITOR_TRY_COMPARE(primarySelectionDevice()->m_sentSelectionOffers.size(), 1); } +void tst_primaryselectionv1::destroysSelectionOnLeave() +{ + QRasterWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + exec([&] { + auto *surface = xdgSurface()->m_surface; + keyboard()->sendEnter(surface); // Need to set keyboard focus according to protocol + + auto *offer = primarySelectionDevice()->sendDataOffer({"text/plain"}); + primarySelectionDevice()->sendSelection(offer); + }); + + QTRY_VERIFY(QGuiApplication::clipboard()->mimeData(QClipboard::Selection)); + QTRY_VERIFY(QGuiApplication::clipboard()->mimeData(QClipboard::Selection)->hasText()); + + QSignalSpy selectionChangedSpy(QGuiApplication::clipboard(), &QClipboard::selectionChanged); + + exec([&] { + auto *surface = xdgSurface()->m_surface; + keyboard()->sendLeave(surface); + }); + + QTRY_COMPARE(selectionChangedSpy.count(), 1); + QVERIFY(!QGuiApplication::clipboard()->mimeData(QClipboard::Selection)->hasText()); +} + void tst_primaryselectionv1::copy() { class Window : public QRasterWindow { @@ -442,6 +472,7 @@ void tst_primaryselectionv1::copy() }); QCOMPOSITOR_TRY_VERIFY(primarySelectionDevice()->m_selectionSource); QCOMPOSITOR_TRY_VERIFY(mouseSerials.contains(primarySelectionDevice()->m_serial)); + QVERIFY(QGuiApplication::clipboard()->ownsSelection()); QByteArray pastedBuf; exec([&](){ auto *source = primarySelectionDevice()->m_selectionSource; diff --git a/tests/auto/wayland/shared/corecompositor.cpp b/tests/auto/wayland/shared/corecompositor.cpp index 7edb1c2d434..5c6c83baa88 100644 --- a/tests/auto/wayland/shared/corecompositor.cpp +++ b/tests/auto/wayland/shared/corecompositor.cpp @@ -43,6 +43,7 @@ CoreCompositor::CoreCompositor() } }) { + qputenv("WAYLAND_DISPLAY", m_socketName); m_timer.start(); Q_ASSERT(isClean()); } diff --git a/tests/auto/wayland/shared/datadevice.h b/tests/auto/wayland/shared/datadevice.h index a96da86f081..98e780b2283 100644 --- a/tests/auto/wayland/shared/datadevice.h +++ b/tests/auto/wayland/shared/datadevice.h @@ -65,7 +65,6 @@ public: ~DataDevice() override; void send_data_offer(::wl_resource *resource) = delete; DataOffer *sendDataOffer(::wl_client *client, const QStringList &mimeTypes = {}); - DataOffer *sendDataOffer(const QStringList &mimeTypes = {}); void send_selection(::wl_resource *resource) = delete; void sendSelection(DataOffer *offer); diff --git a/tests/auto/wayland/shared/mockcompositor.h b/tests/auto/wayland/shared/mockcompositor.h index fc4d7cc4640..c7d85b958f6 100644 --- a/tests/auto/wayland/shared/mockcompositor.h +++ b/tests/auto/wayland/shared/mockcompositor.h @@ -84,7 +84,8 @@ public: #define QCOMPOSITOR_TEST_MAIN(test) \ int main(int argc, char **argv) \ { \ - setenv("XDG_RUNTIME_DIR", ".", 1); \ + QTemporaryDir tmpRuntimeDir; \ + setenv("XDG_RUNTIME_DIR", tmpRuntimeDir.path().toLocal8Bit(), 1); \ setenv("XDG_CURRENT_DESKTOP", "qtwaylandtests", 1); \ setenv("QT_QPA_PLATFORM", "wayland", 1); \ test tc; \ diff --git a/tests/auto/wayland/xdgshellv6/tst_xdgshellv6.cpp b/tests/auto/wayland/xdgshellv6/tst_xdgshellv6.cpp index 027e1dfa807..e44475de71d 100644 --- a/tests/auto/wayland/xdgshellv6/tst_xdgshellv6.cpp +++ b/tests/auto/wayland/xdgshellv6/tst_xdgshellv6.cpp @@ -422,7 +422,8 @@ void tst_WaylandClientXdgShellV6::dontSpamExposeEvents() int main(int argc, char **argv) { - setenv("XDG_RUNTIME_DIR", ".", 1); + QTemporaryDir tmpRuntimeDir; + setenv("XDG_RUNTIME_DIR", tmpRuntimeDir.path().toLocal8Bit(), 1); setenv("QT_QPA_PLATFORM", "wayland", 1); // force QGuiApplication to use wayland plugin setenv("QT_WAYLAND_SHELL_INTEGRATION", "xdg-shell-v6", 1);