From 19bd87c7524825a491a3f180c9015078263c94de Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 14 Jul 2026 17:24:41 -0400 Subject: [PATCH 1/2] wayland/window: add WaylandWindow attached object for overriding App ID of wayland toplevels --- changelog/next.md | 4 + src/wayland/CMakeLists.txt | 1 + src/wayland/module.md | 1 + src/wayland/test/manual/wayland_window.qml | 36 +++++++ src/wayland/wayland_window.cpp | 117 +++++++++++++++++++++ src/wayland/wayland_window.hpp | 71 +++++++++++++ 6 files changed, 230 insertions(+) create mode 100644 src/wayland/test/manual/wayland_window.qml create mode 100644 src/wayland/wayland_window.cpp create mode 100644 src/wayland/wayland_window.hpp diff --git a/changelog/next.md b/changelog/next.md index 585dfb67..84ca56b7 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -1,3 +1,7 @@ +## New Features + +- Added `WaylandWindow.appId` for setting the app id of toplevel windows on Wayland. + ## Bug Fixes - Fixed ScreencopyView not displaying when only lock surfaces are shown. diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 509fd91d..36c9cd18 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -80,6 +80,7 @@ qt_add_library(quickshell-wayland STATIC xdgshell.cpp util.cpp output_tracking.cpp + wayland_window.cpp ) # required for wl_proxy_safe_deref diff --git a/src/wayland/module.md b/src/wayland/module.md index 1e34988c..7e0e565f 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -9,5 +9,6 @@ headers = [ "idle_notify/monitor.hpp", "shortcuts_inhibit/inhibitor.hpp", "background_effect/qml.hpp", + "wayland_window.hpp", ] ----- diff --git a/src/wayland/test/manual/wayland_window.qml b/src/wayland/test/manual/wayland_window.qml new file mode 100644 index 00000000..17b96637 --- /dev/null +++ b/src/wayland/test/manual/wayland_window.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland + +FloatingWindow { + id: root + color: contentItem.palette.window + + WaylandWindow.appId: appIdField.text + + ColumnLayout { + anchors.centerIn: parent + + TextField { + id: appIdField + Layout.preferredWidth: 300 + text: "org.quickshell.test" + } + + Button { + text: "Hide->Show" + onClicked: { + root.visible = false; + showTimer.start(); + } + } + + Timer { + id: showTimer + interval: 200 + onTriggered: root.visible = true + } + } +} diff --git a/src/wayland/wayland_window.cpp b/src/wayland/wayland_window.cpp new file mode 100644 index 00000000..f9a5c20f --- /dev/null +++ b/src/wayland/wayland_window.cpp @@ -0,0 +1,117 @@ +#include "wayland_window.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../window/proxywindow.hpp" + +using QtWaylandClient::QWaylandWindow; + +namespace qs::wayland { + +WaylandWindow* WaylandWindow::qmlAttachedProperties(QObject* object) { + auto* proxyWindow = ProxyWindowBase::forObject(object); + + if (!proxyWindow) return nullptr; + return new WaylandWindow(proxyWindow); +} + +WaylandWindow::WaylandWindow(ProxyWindowBase* window): QObject(window), proxyWindow(window) { + QObject::connect( + window, + &ProxyWindowBase::windowConnected, + this, + &WaylandWindow::onWindowConnected + ); + + if (window->backingWindow()) { + this->onWindowConnected(); + } +} + +QString WaylandWindow::appId() const { return this->mAppId; } + +void WaylandWindow::setAppId(const QString& appId) { + if (appId == this->mAppId) return; + this->mAppId = appId; + this->applyAppId(); + emit this->appIdChanged(); +} + +void WaylandWindow::onWindowConnected() { + this->mWindow = this->proxyWindow->backingWindow(); + + QObject::connect( + this->mWindow, + &QWindow::visibleChanged, + this, + &WaylandWindow::onWindowVisibleChanged + ); + + this->onWindowVisibleChanged(); +} + +void WaylandWindow::onWindowVisibleChanged() { + if (this->mWindow->isVisible() && !this->mWindow->handle()) { + this->mWindow->create(); + } + + auto* window = dynamic_cast(this->mWindow->handle()); + if (window == this->mWaylandWindow) return; + + if (this->mWaylandWindow) { + QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr); + } + + this->mWaylandWindow = window; + if (!window) return; + + QObject::connect( + this->mWaylandWindow, + &QObject::destroyed, + this, + &WaylandWindow::onWaylandWindowDestroyed + ); + + QObject::connect( + this->mWaylandWindow, + &QWaylandWindow::surfaceRoleCreated, + this, + &WaylandWindow::onSurfaceRoleCreated + ); + + if (this->mWaylandWindow->shellSurface() && !this->mAppId.isEmpty()) { + this->applyAppId(); + } +} + +void WaylandWindow::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; } + +void WaylandWindow::onSurfaceRoleCreated() { + if (this->mAppId.isEmpty()) return; + this->applyAppId(); +} + +void WaylandWindow::applyAppId() { + if (!this->mWaylandWindow) return; + + auto* shellSurface = this->mWaylandWindow->shellSurface(); + if (!shellSurface) return; + + if (!this->mWaylandWindow->surfaceRole<::xdg_toplevel>()) { + qmlWarning(this) << "appId can only be set on toplevel windows."; + return; + } + + auto appId = this->mAppId.isEmpty() ? QGuiApplication::desktopFileName() : this->mAppId; + shellSurface->setAppId(appId); +} + +} // namespace qs::wayland diff --git a/src/wayland/wayland_window.hpp b/src/wayland/wayland_window.hpp new file mode 100644 index 00000000..c396e909 --- /dev/null +++ b/src/wayland/wayland_window.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../window/proxywindow.hpp" + +namespace qs::wayland { + +///! Wayland specific QsWindow properties. +/// Allows setting wayland specific window properties on a @@Quickshell.QsWindow or subclass, +/// as an attached object. +/// +/// #### Example +/// ```qml +/// @@Quickshell.FloatingWindow { +/// // ... +/// WaylandWindow.appId: "org.example.myapp" +/// } +/// ``` +class WaylandWindow: public QObject { + Q_OBJECT; + // clang-format off + /// The [application id] of the window, used by the compositor to identify, group, + /// and match windows against desktop entries. Only toplevel windows such as + /// @@Quickshell.FloatingWindow have an application id. + /// + /// If unset or empty, the app id of the quickshell instance is used. + /// + /// > [!NOTE] The app id can be changed while the window is visible, but compositor + /// > behaviors that were decided when the window was first shown, such as window + /// > rules, may not be re-evaluated. + /// + /// [application id]: https://wayland.app/protocols/xdg-shell#xdg_toplevel:request:set_app_id + Q_PROPERTY(QString appId READ appId WRITE setAppId NOTIFY appIdChanged); + // clang-format on + QML_ELEMENT; + QML_UNCREATABLE("WaylandWindow can only be used as an attached object."); + QML_ATTACHED(WaylandWindow); + +public: + explicit WaylandWindow(ProxyWindowBase* window); + + [[nodiscard]] QString appId() const; + void setAppId(const QString& appId); + + static WaylandWindow* qmlAttachedProperties(QObject* object); + +signals: + void appIdChanged(); + +private slots: + void onWindowConnected(); + void onWindowVisibleChanged(); + void onWaylandWindowDestroyed(); + void onSurfaceRoleCreated(); + +private: + void applyAppId(); + + ProxyWindowBase* proxyWindow = nullptr; + QWindow* mWindow = nullptr; + QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + QString mAppId; +}; + +} // namespace qs::wayland From fc66d6ac1838fbb0e8e493d3f07c16df442aedce Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 14 Jul 2026 18:01:15 -0400 Subject: [PATCH 2/2] wayland/window: fix build on QT < 6.8 --- src/wayland/wayland_window.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/wayland/wayland_window.cpp b/src/wayland/wayland_window.cpp index f9a5c20f..7539ca78 100644 --- a/src/wayland/wayland_window.cpp +++ b/src/wayland/wayland_window.cpp @@ -8,8 +8,13 @@ #include #include #include +#include #include +#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0) +#include +#endif + #include "../window/proxywindow.hpp" using QtWaylandClient::QWaylandWindow; @@ -63,6 +68,15 @@ void WaylandWindow::onWindowVisibleChanged() { this->mWindow->create(); } +#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0) + // Qt < 6.8 has no surfaceRoleCreated signal. The shell surface is created and the + // default app id set after visibleChanged is emitted, so apply ours in a queued + // slot which runs after setVisible completes and before the toplevel is mapped. + if (this->mWindow->isVisible()) { + QMetaObject::invokeMethod(this, &WaylandWindow::onSurfaceRoleCreated, Qt::QueuedConnection); + } +#endif + auto* window = dynamic_cast(this->mWindow->handle()); if (window == this->mWaylandWindow) return; @@ -80,12 +94,14 @@ void WaylandWindow::onWindowVisibleChanged() { &WaylandWindow::onWaylandWindowDestroyed ); +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) QObject::connect( this->mWaylandWindow, &QWaylandWindow::surfaceRoleCreated, this, &WaylandWindow::onSurfaceRoleCreated ); +#endif if (this->mWaylandWindow->shellSurface() && !this->mAppId.isEmpty()) { this->applyAppId();