summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRobert Griebl <robert.griebl@qt.io>2024-03-14 12:50:11 +0100
committerRobert Griebl <robert.griebl@qt.io>2024-03-18 16:56:50 +0100
commit0970e3f1d21c6bfaf0e2e6f897b38aa99c4d6f7e (patch)
treeb1404ff10fa98ec5fd8a3e539df4599957437176
parent7295ea86053e1f64fd6895a5630521453a986808 (diff)
Also support CBOR as a wire format for window properties on Wayland
Our extension now supports both the old style serialization via QDataStream for Qt clients (version 1) and via CBOR for easy use in non-Qt clients (version 2). Change-Id: I757fda61af126fb273264133552b86f3e0127a6a Reviewed-by: Bernd Weimer <bernd.weimer@qt.io>
-rw-r--r--src/application-main-lib/waylandqtamclientextension.cpp32
-rw-r--r--src/wayland-extensions/qtam-extension.xml23
-rw-r--r--src/window-lib/waylandqtamserverextension.cpp46
-rw-r--r--src/window-lib/window.cpp4
-rw-r--r--tests/auto/qml/CMakeLists.txt1
-rw-r--r--tests/auto/qml/windowproperties/CMakeLists.txt13
-rw-r--r--tests/auto/qml/windowproperties/am-config.yaml8
-rw-r--r--tests/auto/qml/windowproperties/apps/wp-app-native/CMakeLists.txt26
-rw-r--r--tests/auto/qml/windowproperties/apps/wp-app-native/info.yaml6
-rw-r--r--tests/auto/qml/windowproperties/apps/wp-app-native/waylandqtamclientextension_v2.cpp127
-rw-r--r--tests/auto/qml/windowproperties/apps/wp-app-native/waylandqtamclientextension_v2_p.h45
-rw-r--r--tests/auto/qml/windowproperties/apps/wp-app-native/wp-app.cpp33
-rw-r--r--tests/auto/qml/windowproperties/apps/wp-app-qml-inprocess/info.yaml6
-rw-r--r--tests/auto/qml/windowproperties/apps/wp-app-qml-inprocess/wp-app.qml23
-rw-r--r--tests/auto/qml/windowproperties/apps/wp-app-qml/info.yaml6
-rw-r--r--tests/auto/qml/windowproperties/apps/wp-app-qml/wp-app.qml23
-rw-r--r--tests/auto/qml/windowproperties/tst_windowproperties.qml66
17 files changed, 462 insertions, 26 deletions
diff --git a/src/application-main-lib/waylandqtamclientextension.cpp b/src/application-main-lib/waylandqtamclientextension.cpp
index 92caa10b..1f4190c7 100644
--- a/src/application-main-lib/waylandqtamclientextension.cpp
+++ b/src/application-main-lib/waylandqtamclientextension.cpp
@@ -67,11 +67,16 @@ QVariantMap WaylandQtAMClientExtension::windowProperties(QWindow *window) const
void WaylandQtAMClientExtension::sendPropertyToServer(struct ::wl_surface *surface, const QString &name,
const QVariant &value)
{
- QByteArray byteValue;
- QDataStream ds(&byteValue, QDataStream::WriteOnly);
+ if (int(qtam_extension::version()) != QWaylandClientExtension::version()) {
+ qCWarning(LogWaylandDebug) << "Unsupported qtam_extension version:" << qtam_extension::version();
+ return;
+ }
+ QByteArray data;
+ QDataStream ds(&data, QDataStream::WriteOnly);
ds << value;
+
qCDebug(LogWaylandDebug) << "window property: client send:" << surface << name << value;
- set_window_property(surface, name, byteValue);
+ set_window_property(surface, name, data);
}
bool WaylandQtAMClientExtension::setWindowProperty(QWindow *window, const QString &name, const QVariant &value)
@@ -110,17 +115,20 @@ void WaylandQtAMClientExtension::clearWindowPropertyCache(QWindow *window)
void WaylandQtAMClientExtension::qtam_extension_window_property_changed(wl_surface *surface, const QString &name,
wl_array *value)
{
- const QByteArray data = QByteArray::fromRawData(static_cast<char *>(value->data), int(value->size));
- QDataStream ds(data);
- QVariant variantValue;
- ds >> variantValue;
-
- QWindow *window = m_windowToSurface.key(surface);
- qCDebug(LogWaylandDebug) << "window property: client receive" << window << name << variantValue;
- if (!window)
+ if (int(qtam_extension::version()) != QWaylandClientExtension::version()) {
+ qCWarning(LogWaylandDebug) << "Unsupported qtam_extension version:" << qtam_extension::version();
return;
+ }
+
+ if (QWindow *window = m_windowToSurface.key(surface)) {
+ const auto data = QByteArray::fromRawData(static_cast<const char *>(value->data), qsizetype(value->size));
+ QDataStream ds(data);
+ QVariant variantValue;
+ ds >> variantValue;
- setWindowPropertyHelper(window, name, variantValue);
+ qCDebug(LogWaylandDebug) << "window property: client receive" << window << name << variantValue;
+ setWindowPropertyHelper(window, name, variantValue);
+ }
}
QT_END_NAMESPACE_AM
diff --git a/src/wayland-extensions/qtam-extension.xml b/src/wayland-extensions/qtam-extension.xml
index e4a80889..2b3d679a 100644
--- a/src/wayland-extensions/qtam-extension.xml
+++ b/src/wayland-extensions/qtam-extension.xml
@@ -1,19 +1,38 @@
<protocol name="qtam_extension">
<copyright>
- Copyright (C) 2021 The Qt Company Ltd.
+ Copyright (C) 2024 The Qt Company Ltd.
Copyright (C) 2019 Luxoft Sweden AB
Copyright (C) 2018 Pelagicore AG
SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
</copyright>
- <interface name="qtam_extension" version="1">
+ <interface name="qtam_extension" version="2">
+ <description summary="notify client of window property change">
+ This interface is a way to keep window properties in sync between the server and a
+ client. Those properties are represented as QVariants on the Qt side. The wire
+ protocol (the 'value' parameter below) is a byte array containing the serialized
+ variant.
+ Version 1 of this protocol uses serialization via QDataStream into a QByteArray.
+ Version 2 of this protocol uses CBOR serialization via QCborValue::toCbor().
+
+ Please note that this extension uses the built-in Wayland versioning mechanism.
+ There is only one XML description for both versions of the extension.
+ </description>
<event name="window_property_changed">
+ <description summary="notify client of window property change">
+ The server sends this event, when it has set the window property identified by
+ 'name' to the given 'value' on the window 'surface'.
+ </description>
<arg name="surface" type="object" interface="wl_surface"/>
<arg name="name" type="string"/>
<arg name="value" type="array"/>
</event>
<request name="set_window_property">
+ <description summary="notify server of window property change">
+ A client sends this request, when it has set the window property identified by
+ 'name' to the given 'value' on the window 'surface'.
+ </description>
<arg name="surface" type="object" interface="wl_surface"/>
<arg name="name" type="string"/>
<arg name="value" type="array"/>
diff --git a/src/window-lib/waylandqtamserverextension.cpp b/src/window-lib/waylandqtamserverextension.cpp
index a6c17204..57c645df 100644
--- a/src/window-lib/waylandqtamserverextension.cpp
+++ b/src/window-lib/waylandqtamserverextension.cpp
@@ -6,6 +6,7 @@
#include "waylandqtamserverextension_p.h"
#include <QDataStream>
+#include <QCborValue>
#include <QtWaylandCompositor/QWaylandCompositor>
#include <QtWaylandCompositor/QWaylandResource>
#include <QtWaylandCompositor/QWaylandSurface>
@@ -16,7 +17,7 @@ QT_BEGIN_NAMESPACE_AM
WaylandQtAMServerExtension::WaylandQtAMServerExtension(QWaylandCompositor *compositor)
: QWaylandCompositorExtensionTemplate(compositor)
- , QtWaylandServer::qtam_extension(compositor->display(), 1)
+ , QtWaylandServer::qtam_extension(compositor->display(), 2)
{ }
QVariantMap WaylandQtAMServerExtension::windowProperties(const QWaylandSurface *surface) const
@@ -27,14 +28,25 @@ QVariantMap WaylandQtAMServerExtension::windowProperties(const QWaylandSurface *
void WaylandQtAMServerExtension::setWindowProperty(QWaylandSurface *surface, const QString &name, const QVariant &value)
{
if (setWindowPropertyHelper(surface, name, value)) {
- QByteArray byteValue;
- QDataStream ds(&byteValue, QDataStream::WriteOnly);
- ds << value;
+ if (Resource *target = resourceMap().value(surface->waylandClient())) {
+ QByteArray data;
- Resource *target = resourceMap().value(surface->waylandClient());
- if (target) {
- qDebug(LogWaylandDebug) << "window property: server send" << surface << name << value;
- send_window_property_changed(target->handle, surface->resource(), name, byteValue);
+ switch (target->version()) {
+ case 1: {
+ QDataStream ds(&data, QDataStream::WriteOnly);
+ ds << value;
+ break;
+ }
+ case 2:
+ data = QCborValue::fromVariant(value).toCbor();
+ break;
+ default:
+ qCWarning(LogWaylandDebug) << "Unsupported qtam_extension version:" << target->version();
+ return;
+ }
+
+ qCDebug(LogWaylandDebug) << "window property: server send" << surface << name << value;
+ send_window_property_changed(target->handle, surface->resource(), name, data);
}
}
}
@@ -59,13 +71,23 @@ bool WaylandQtAMServerExtension::setWindowPropertyHelper(QWaylandSurface *surfac
void WaylandQtAMServerExtension::qtam_extension_set_window_property(QtWaylandServer::qtam_extension::Resource *resource, wl_resource *surface_resource, const QString &name, wl_array *value)
{
- Q_UNUSED(resource)
QWaylandSurface *surface = QWaylandSurface::fromResource(surface_resource);
- const QByteArray byteValue(static_cast<const char *>(value->data), static_cast<int>(value->size));
- QDataStream ds(byteValue);
+ const auto data = QByteArray::fromRawData(static_cast<const char *>(value->data), qsizetype(value->size));
QVariant variantValue;
- ds >> variantValue;
+ switch (resource->version()) {
+ case 1: {
+ QDataStream ds(data);
+ ds >> variantValue;
+ break;
+ }
+ case 2:
+ variantValue = QCborValue::fromCbor(data).toVariant();
+ break;
+ default:
+ qCWarning(LogWaylandDebug) << "Unsupported qtam_extension version:" << resource->version();
+ return;
+ }
qCDebug(LogWaylandDebug) << "window property: server receive" << surface << name << variantValue;
setWindowPropertyHelper(surface, name, variantValue);
}
diff --git a/src/window-lib/window.cpp b/src/window-lib/window.cpp
index cedbf4de..1e071aa1 100644
--- a/src/window-lib/window.cpp
+++ b/src/window-lib/window.cpp
@@ -31,6 +31,10 @@
surface Wayland extension. Changes from the client side are notified by the
windowPropertyChanged() signal.
+ Starting with version 6.8, non-Qt Wayland clients can use version 2 of the extension, which
+ uses CBOR as a wire format, but only \l{QCborValue::fromVariant()}{types supported by CBOR}
+ can be used as values for these clients.
+
See ApplicationManagerWindow for the client side API.
\sa windowProperty(), windowProperties(), windowPropertyChanged()
diff --git a/tests/auto/qml/CMakeLists.txt b/tests/auto/qml/CMakeLists.txt
index 6da3dcee..b89a6b1d 100644
--- a/tests/auto/qml/CMakeLists.txt
+++ b/tests/auto/qml/CMakeLists.txt
@@ -22,6 +22,7 @@ add_subdirectory(resources)
add_subdirectory(keyinput)
add_subdirectory(monitoring)
add_subdirectory(notifications)
+add_subdirectory(windowproperties)
if (QT_FEATURE_am_multi_process)
add_subdirectory(crash)
add_subdirectory(processtitle)
diff --git a/tests/auto/qml/windowproperties/CMakeLists.txt b/tests/auto/qml/windowproperties/CMakeLists.txt
new file mode 100644
index 00000000..d3fa1b87
--- /dev/null
+++ b/tests/auto/qml/windowproperties/CMakeLists.txt
@@ -0,0 +1,13 @@
+
+qt_am_internal_add_qml_test(tst_windowproperties
+ CONFIG_YAML am-config.yaml
+ EXTRA_FILES apps
+ TEST_FILE tst_windowproperties.qml
+ CONFIGURATIONS
+ CONFIG NAME single-process ARGS --force-single-process
+ CONFIG NAME multi-process CONDITION QT_FEATURE_am_multi_process ARGS --force-multi-process
+)
+
+if (QT_FEATURE_am_multi_process)
+ add_subdirectory(apps/wp-app-native)
+endif()
diff --git a/tests/auto/qml/windowproperties/am-config.yaml b/tests/auto/qml/windowproperties/am-config.yaml
new file mode 100644
index 00000000..61b2bfb2
--- /dev/null
+++ b/tests/auto/qml/windowproperties/am-config.yaml
@@ -0,0 +1,8 @@
+formatVersion: 1
+formatType: am-configuration
+---
+applications:
+ builtinAppsManifestDir: "${CONFIG_PWD}/apps"
+
+flags:
+ noUiWatchdog: yes
diff --git a/tests/auto/qml/windowproperties/apps/wp-app-native/CMakeLists.txt b/tests/auto/qml/windowproperties/apps/wp-app-native/CMakeLists.txt
new file mode 100644
index 00000000..8bc20ddf
--- /dev/null
+++ b/tests/auto/qml/windowproperties/apps/wp-app-native/CMakeLists.txt
@@ -0,0 +1,26 @@
+
+qt_internal_add_executable(wp-app-native
+ OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/"
+ SOURCES
+ wp-app.cpp
+ waylandqtamclientextension_v2.cpp
+ waylandqtamclientextension_v2_p.h
+ LIBRARIES
+ Qt::Core
+ Qt::Gui
+ Qt::Quick
+ Qt::Qml
+ Qt::WaylandClient
+ Qt::WaylandClientPrivate
+)
+
+qt_internal_set_exceptions_flags(wp-app-native TRUE)
+
+qt_autogen_tools_initial_setup(wp-app-native)
+
+qt6_generate_wayland_protocol_client_sources(wp-app-native
+ FILES
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../src/wayland-extensions/qtam-extension.xml
+)
+
+qt_internal_add_autogen_sync_header_dependencies(wp-app-native WaylandClient)
diff --git a/tests/auto/qml/windowproperties/apps/wp-app-native/info.yaml b/tests/auto/qml/windowproperties/apps/wp-app-native/info.yaml
new file mode 100644
index 00000000..c145f0c1
--- /dev/null
+++ b/tests/auto/qml/windowproperties/apps/wp-app-native/info.yaml
@@ -0,0 +1,6 @@
+formatVersion: 1
+formatType: am-application
+---
+id: 'wp-app-native'
+code: 'wp-app-native'
+runtime: 'native'
diff --git a/tests/auto/qml/windowproperties/apps/wp-app-native/waylandqtamclientextension_v2.cpp b/tests/auto/qml/windowproperties/apps/wp-app-native/waylandqtamclientextension_v2.cpp
new file mode 100644
index 00000000..62f122da
--- /dev/null
+++ b/tests/auto/qml/windowproperties/apps/wp-app-native/waylandqtamclientextension_v2.cpp
@@ -0,0 +1,127 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "waylandqtamclientextension_v2_p.h"
+
+#include <QWindow>
+#include <QGuiApplication>
+#include <QEvent>
+#include <QExposeEvent>
+#include <qpa/qplatformnativeinterface.h>
+#include <QJSValue>
+#include <QCborValue>
+
+
+// This is a copy of the built-in class WaylandQtAMClientExtension (version 1). Not all
+// functionality is actually used for the test, but it makes it easier to keep the code in sync
+// this way, as they share ~90% of the implementation.
+
+WaylandQtAMClientExtensionV2::WaylandQtAMClientExtensionV2()
+ : QWaylandClientExtensionTemplate(2)
+{
+ qApp->installEventFilter(this);
+}
+
+WaylandQtAMClientExtensionV2::~WaylandQtAMClientExtensionV2()
+{
+ qApp->removeEventFilter(this);
+}
+
+bool WaylandQtAMClientExtensionV2::eventFilter(QObject *o, QEvent *e)
+{
+ if (e->type() == QEvent::Expose) {
+ if (!isActive()) {
+ qWarning() << "WaylandQtAMClientExtensionV2 is not active";
+ } else {
+ QWindow *window = qobject_cast<QWindow *>(o);
+ Q_ASSERT(window);
+
+ // we're only interested in the first expose to setup our mapping
+ if (!m_windowToSurface.contains(window)) {
+ auto surface = static_cast<struct ::wl_surface *>
+ (QGuiApplication::platformNativeInterface()->nativeResourceForWindow("surface", window));
+ if (surface) {
+ m_windowToSurface.insert(window, surface);
+ const QVariantMap wp = windowProperties(window);
+ for (auto it = wp.cbegin(); it != wp.cend(); ++it)
+ sendPropertyToServer(surface, it.key(), it.value());
+ }
+ // pointers can be reused, so we have to remove the old mappings
+ connect(window, &QObject::destroyed, this, [this, window]() {
+ m_windowToSurface.remove(window);
+ m_windowProperties.remove(window);
+ });
+ }
+ }
+ } else if (e->type() == QEvent::Hide) {
+ m_windowToSurface.remove(qobject_cast<QWindow *>(o));
+ }
+
+ return QWaylandClientExtensionTemplate<WaylandQtAMClientExtensionV2>::eventFilter(o, e);
+}
+
+QVariantMap WaylandQtAMClientExtensionV2::windowProperties(QWindow *window) const
+{
+ return m_windowProperties.value(window);
+}
+
+void WaylandQtAMClientExtensionV2::sendPropertyToServer(struct ::wl_surface *surface, const QString &name,
+ const QVariant &value)
+{
+ if (int(qtam_extension::version()) != QWaylandClientExtension::version()) {
+ qWarning() << "Unsupported qtam_extension version:" << qtam_extension::version();
+ return;
+ }
+ const QByteArray data = QCborValue::fromVariant(value).toCbor();
+ set_window_property(surface, name, data);
+}
+
+bool WaylandQtAMClientExtensionV2::setWindowProperty(QWindow *window, const QString &name, const QVariant &value)
+{
+ if (setWindowPropertyHelper(window, name, value) && m_windowToSurface.contains(window)) {
+ auto surface = static_cast<struct ::wl_surface *>
+ (QGuiApplication::platformNativeInterface()->nativeResourceForWindow("surface", window));
+ if (surface) {
+ sendPropertyToServer(surface, name, value);
+ return true;
+ }
+ }
+ return false;
+}
+
+bool WaylandQtAMClientExtensionV2::setWindowPropertyHelper(QWindow *window, const QString &name, const QVariant &value)
+{
+ auto it = m_windowProperties.find(window);
+ if ((it == m_windowProperties.end()) || (it.value().value(name) != value)) {
+ if (it == m_windowProperties.end())
+ m_windowProperties[window].insert(name, value);
+ else
+ it.value().insert(name, value);
+
+ emit windowPropertyChanged(window, name, value);
+ return true;
+ }
+ return false;
+}
+
+void WaylandQtAMClientExtensionV2::clearWindowPropertyCache(QWindow *window)
+{
+ m_windowProperties.remove(window);
+}
+
+void WaylandQtAMClientExtensionV2::qtam_extension_window_property_changed(wl_surface *surface, const QString &name,
+ wl_array *value)
+{
+ if (int(qtam_extension::version()) != QWaylandClientExtension::version()) {
+ qWarning() << "Unsupported qtam_extension version:" << qtam_extension::version();
+ return;
+ }
+
+ if (QWindow *window = m_windowToSurface.key(surface)) {
+ const auto data = QByteArray::fromRawData(static_cast<const char *>(value->data), qsizetype(value->size));
+ const QVariant variantValue = QCborValue::fromCbor(data).toVariant();
+ setWindowPropertyHelper(window, name, variantValue);
+ }
+}
+
+#include "moc_waylandqtamclientextension_v2_p.cpp"
diff --git a/tests/auto/qml/windowproperties/apps/wp-app-native/waylandqtamclientextension_v2_p.h b/tests/auto/qml/windowproperties/apps/wp-app-native/waylandqtamclientextension_v2_p.h
new file mode 100644
index 00000000..04251073
--- /dev/null
+++ b/tests/auto/qml/windowproperties/apps/wp-app-native/waylandqtamclientextension_v2_p.h
@@ -0,0 +1,45 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef WAYLANDQTAMCLIENTEXTENSION_V2_P_H
+#define WAYLANDQTAMCLIENTEXTENSION_V2_P_H
+
+#include <QVariantMap>
+#include <QtWaylandClient/QWaylandClientExtensionTemplate>
+#include "qwayland-qtam-extension.h"
+
+QT_FORWARD_DECLARE_CLASS(QWindow)
+
+// This is a copy of the built-in class WaylandQtAMClientExtension (version 1). Not all
+// functionality is actually used for the test, but it makes it easier to keep the code in sync
+// this way, as they share ~90% of the implementation.
+
+class WaylandQtAMClientExtensionV2 : public QWaylandClientExtensionTemplate<WaylandQtAMClientExtensionV2>,
+ public ::QtWayland::qtam_extension
+{
+ Q_OBJECT
+
+public:
+ WaylandQtAMClientExtensionV2();
+ ~WaylandQtAMClientExtensionV2() override;
+
+ QVariantMap windowProperties(QWindow *window) const;
+ bool setWindowProperty(QWindow *window, const QString &name, const QVariant &value);
+ void clearWindowPropertyCache(QWindow *window);
+
+signals:
+ void windowPropertyChanged(QWindow *window, const QString &name, const QVariant &value);
+
+protected:
+ bool eventFilter(QObject *o, QEvent *e) override;
+
+private:
+ bool setWindowPropertyHelper(QWindow *window, const QString &name, const QVariant &value);
+ void sendPropertyToServer(::wl_surface *surface, const QString &name, const QVariant &value);
+ void qtam_extension_window_property_changed(wl_surface *surface, const QString &name, wl_array *value) override;
+
+ QMap<QWindow *, QVariantMap> m_windowProperties; // AXIVION Line Qt-QMapWithPointerKey: cleared on destroyed signal
+ QMap<QWindow *, ::wl_surface *> m_windowToSurface; // AXIVION Line Qt-QMapWithPointerKey: cleared on destroyed signal
+};
+
+#endif // WAYLANDQTAMCLIENTEXTENSION_V2_P_H
diff --git a/tests/auto/qml/windowproperties/apps/wp-app-native/wp-app.cpp b/tests/auto/qml/windowproperties/apps/wp-app-native/wp-app.cpp
new file mode 100644
index 00000000..207aef6b
--- /dev/null
+++ b/tests/auto/qml/windowproperties/apps/wp-app-native/wp-app.cpp
@@ -0,0 +1,33 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QGuiApplication>
+#include <QQuickView>
+
+#include "waylandqtamclientextension_v2_p.h"
+
+using namespace Qt::StringLiterals;
+
+int main(int argc, char **argv)
+{
+ QGuiApplication app(argc, argv);
+ auto qtamExtension = new WaylandQtAMClientExtensionV2();
+ QQuickView w;
+ w.setGeometry(0, 0, 100, 100);
+ w.show();
+
+ qsizetype expectedSize = 0;
+
+ const auto args = app.arguments();
+ if (auto pos = args.indexOf(u"--start-argument"_s); pos >= 0)
+ expectedSize = args.at(pos + 1).toInt();
+
+ QObject::connect(qtamExtension, &WaylandQtAMClientExtensionV2::windowPropertyChanged,
+ &app, [&](QWindow *, const QString &, const QVariant &) {
+ auto allProperties = qtamExtension->windowProperties(&w);
+ if (allProperties.size() == expectedSize)
+ qtamExtension->setWindowProperty(&w, u"BACKCHANNEL"_s, allProperties);
+ });
+
+ return app.exec();
+}
diff --git a/tests/auto/qml/windowproperties/apps/wp-app-qml-inprocess/info.yaml b/tests/auto/qml/windowproperties/apps/wp-app-qml-inprocess/info.yaml
new file mode 100644
index 00000000..68c9447e
--- /dev/null
+++ b/tests/auto/qml/windowproperties/apps/wp-app-qml-inprocess/info.yaml
@@ -0,0 +1,6 @@
+formatVersion: 1
+formatType: am-application
+---
+id: 'wp-app-qml-inprocess'
+code: '../wp-app-qml/wp-app.qml'
+runtime: 'qml-inprocess'
diff --git a/tests/auto/qml/windowproperties/apps/wp-app-qml-inprocess/wp-app.qml b/tests/auto/qml/windowproperties/apps/wp-app-qml-inprocess/wp-app.qml
new file mode 100644
index 00000000..e076ba34
--- /dev/null
+++ b/tests/auto/qml/windowproperties/apps/wp-app-qml-inprocess/wp-app.qml
@@ -0,0 +1,23 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtApplicationManager.Application
+import QtQuick
+
+ApplicationManagerWindow {
+ id: root
+ property int expectedSize: 0
+
+ Connections {
+ target: ApplicationInterface
+ function onOpenDocument(str) {
+ expectedSize = str
+ }
+ }
+
+ onWindowPropertyChanged: {
+ const allProperties = windowProperties()
+ if (Object.keys(allProperties).length === expectedSize)
+ setWindowProperty("BACKCHANNEL", allProperties)
+ }
+}
diff --git a/tests/auto/qml/windowproperties/apps/wp-app-qml/info.yaml b/tests/auto/qml/windowproperties/apps/wp-app-qml/info.yaml
new file mode 100644
index 00000000..cc20adbc
--- /dev/null
+++ b/tests/auto/qml/windowproperties/apps/wp-app-qml/info.yaml
@@ -0,0 +1,6 @@
+formatVersion: 1
+formatType: am-application
+---
+id: 'wp-app-qml'
+code: 'wp-app.qml'
+runtime: 'qml'
diff --git a/tests/auto/qml/windowproperties/apps/wp-app-qml/wp-app.qml b/tests/auto/qml/windowproperties/apps/wp-app-qml/wp-app.qml
new file mode 100644
index 00000000..e076ba34
--- /dev/null
+++ b/tests/auto/qml/windowproperties/apps/wp-app-qml/wp-app.qml
@@ -0,0 +1,23 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtApplicationManager.Application
+import QtQuick
+
+ApplicationManagerWindow {
+ id: root
+ property int expectedSize: 0
+
+ Connections {
+ target: ApplicationInterface
+ function onOpenDocument(str) {
+ expectedSize = str
+ }
+ }
+
+ onWindowPropertyChanged: {
+ const allProperties = windowProperties()
+ if (Object.keys(allProperties).length === expectedSize)
+ setWindowProperty("BACKCHANNEL", allProperties)
+ }
+}
diff --git a/tests/auto/qml/windowproperties/tst_windowproperties.qml b/tests/auto/qml/windowproperties/tst_windowproperties.qml
new file mode 100644
index 00000000..3c05d453
--- /dev/null
+++ b/tests/auto/qml/windowproperties/tst_windowproperties.qml
@@ -0,0 +1,66 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtTest
+import QtApplicationManager.SystemUI
+import QtApplicationManager.Test
+
+TestCase {
+ id: testCase
+ when: windowShown
+ name: "Native"
+
+ WindowItem {
+ id: windowItem
+ }
+
+ property int timeout: 5000 * AmTest.timeoutFactor
+
+ function test_waylandExtension_data() {
+ let data = [
+ { tag: "qml-inprocess", appId: "wp-app-qml-inprocess" },
+ ]
+ if (!ApplicationManager.singleProcess) {
+ data.push({ tag: "qml", appId: "wp-app-qml" },
+ { tag: "native", appId: "wp-app-native" })
+ }
+ return data
+ }
+
+ function test_waylandExtension(data) {
+ let app = ApplicationManager.application(data.appId);
+
+ WindowManager.windowAdded.connect(function(window) {
+ windowItem.window = window
+ })
+ // keep this sorted alphabetically ... otherwise the JSON.stringify comparison will fail
+ let props = {
+ "ARRAY": [ 1, 2, "3" ],
+ "COLOR": Qt.color("blue"),
+ "DATE": new Date(),
+ "INT": 42,
+ "OBJECT": { "a": 1, "b": 2 },
+ "STRING": "str"
+ }
+
+ if (data.tag === "native") {
+ // cannot serialize QColor via CBOR
+ props["COLOR"] = props["COLOR"].toString()
+ }
+
+ verify(app.start("" + Object.keys(props).length))
+ tryVerify(() => { return app.runState === Am.Running }, timeout)
+ tryVerify(() => { return windowItem.window !== null }, timeout)
+ for (const prop in props)
+ windowItem.window.setWindowProperty(prop, props[prop])
+
+ tryVerify(() => {
+ return JSON.stringify(windowItem.window.windowProperty("BACKCHANNEL"))
+ === JSON.stringify(props)
+ }, timeout)
+ app.stop();
+ tryVerify(() => { return app.runState === Am.NotRunning }, timeout)
+ windowItem.window = null
+ }
+}