Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/next.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/wayland/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/wayland/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ headers = [
"idle_notify/monitor.hpp",
"shortcuts_inhibit/inhibitor.hpp",
"background_effect/qml.hpp",
"wayland_window.hpp",
]
-----
36 changes: 36 additions & 0 deletions src/wayland/test/manual/wayland_window.qml
Original file line number Diff line number Diff line change
@@ -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
}
}
}
133 changes: 133 additions & 0 deletions src/wayland/wayland_window.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#include "wayland_window.hpp"

#include <private/qwaylandshellsurface_p.h>
#include <private/qwaylandwindow_p.h>
#include <private/wayland-xdg-shell-client-protocol.h>
#include <qguiapplication.h>
#include <qobject.h>
#include <qqmlinfo.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include <qtversionchecks.h>
#include <qwindow.h>

#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
#include <qnamespace.h>
#endif

#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();
}

#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<QWaylandWindow*>(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
);

#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();
}
}

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
71 changes: 71 additions & 0 deletions src/wayland/wayland_window.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#pragma once

#include <private/qwaylandwindow_p.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include <qwindow.h>

#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
Loading