Skip to content

Commit 6d6b094

Browse files
committed
feat: add Wayland XDG activation token support for app launch
Implement XDG activation token v1 protocol support to enable window focus-stealing prevention when launching applications on Wayland. This allows the active window to pass its activation token to `dde-am`, ensuring the newly launched application can properly raise its window on first map. Key changes: 1. Add wayland-protocols dependency for xdg-activation-v1.xml 2. Implement XdgActivationV1 client extension with token request mechanism 3. Integrate token retrieval into AppMgr::launchApp() for Wayland sessions 4. Pass the token via XDG_ACTIVATION_TOKEN environment variable Log: Added Wayland XDG activation support for window management during app launches Influence: 1. Test app launch on Wayland session with focus window present 2. Verify token is properly passed to dde-am process 3. Test fallback behavior when compositor doesn't support xdg- activation-v1 4. Verify no regression on X11 session 5. Test timeout handling when compositor delays token delivery 6. Verify multiple rapid app launches work correctly feat: 为 Wayland 环境添加 XDG 激活令牌支持 在 Wayland 下启动应用时实现 XDG 激活令牌 v1 协议支持,用于窗口焦点防盗预 防。通过将激活窗口的令牌传递给 `dde-am`,确保新启动的应用能在首次映射时 正确弹出窗口。 关键改动: 1. 添加 wayland-protocols 依赖,获取 xdg-activation-v1.xml 2. 实现 XdgActivationV1 客户端扩展,包含令牌请求机制 3. 在 AppMgr::launchApp() 中集成 Wayland 会话的令牌获取 4. 通过 XDG_ACTIVATION_TOKEN 环境变量传递令牌 Log: 新增 Wayland 下应用启动的 XDG 激活支持 Influence: 1. 在 Wayland 会话中测试有焦点窗口时的应用启动 2. 验证令牌是否正确传递给 dde-am 进程 3. 测试当合成器不支持 xdg-activation-v1 时的回退行为 4. 验证 X11 会话无回归问题 5. 测试合成器延迟传递令牌时的超时处理 6. 验证快速连续启动多个应用的正确性 PMS: BUG-345417
1 parent 2c2921f commit 6d6b094

7 files changed

Lines changed: 259 additions & 9 deletions

File tree

debian/control

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Build-Depends:
1717
qt6-tools-dev-tools,
1818
qt6-wayland-dev,
1919
qt6-wayland-private-dev,
20+
wayland-protocols,
2021
libdtkcommon-dev,
2122
libdtk6core-dev (>= 6.0.43),
2223
# v-- provides qdbusxml2cpp-fix binary

desktopintegration.cpp

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
1+
// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
22
//
33
// SPDX-License-Identifier: GPL-3.0-or-later
44

@@ -15,6 +15,11 @@
1515
#include <appinfo.h>
1616
#include <appmgr.h>
1717

18+
#ifdef HAVE_WAYLAND_XDG_ACTIVATION
19+
#include <xdgactivation.h>
20+
#include <DGuiApplicationHelper>
21+
#endif
22+
1823
#include <AppStreamQt/pool.h>
1924

2025
#include "appwiz.h"
@@ -50,7 +55,13 @@ void DesktopIntegration::openSystemSettings()
5055
void DesktopIntegration::launchByDesktopId(const QString &desktopId)
5156
{
5257
qCInfo(logDesktopIntegration) << "Launching app by desktop ID:" << desktopId;
53-
if (!AppMgr::launchApp(desktopId)) {
58+
QString token;
59+
#ifdef HAVE_WAYLAND_XDG_ACTIVATION
60+
auto xdgActivation = DDEIntegration::XdgActivationV1::instance();
61+
if (xdgActivation->isActive())
62+
token = xdgActivation->requestToken(QGuiApplication::focusWindow(), desktopId);
63+
#endif
64+
if (!AppMgr::launchApp(desktopId, token)) {
5465
qCDebug(logDesktopIntegration) << "AppMgr launch failed, trying AppInfo launch";
5566
AppInfo::launchByDesktopId(desktopId);
5667
}
@@ -259,6 +270,19 @@ DesktopIntegration::DesktopIntegration(QObject *parent)
259270
m_iconScaleFactor = dconfig->value("iconScaleFactor", 1.0).toReal();
260271
qCInfo(logDesktopIntegration) << "Icon scale factor loaded:" << m_iconScaleFactor;
261272

273+
#ifdef HAVE_WAYLAND_XDG_ACTIVATION
274+
if (DTK_GUI_NAMESPACE::DGuiApplicationHelper::testAttribute(
275+
DTK_GUI_NAMESPACE::DGuiApplicationHelper::IsWaylandPlatform)) {
276+
auto *xdgActivation = DDEIntegration::XdgActivationV1::instance();
277+
connect(xdgActivation, &DDEIntegration::XdgActivationV1::activeChanged, this, []() {
278+
if (DDEIntegration::XdgActivationV1::instance()->isActive())
279+
qCInfo(logDesktopIntegration) << "XdgActivationV1: ready, XDG activation token support enabled";
280+
else
281+
qCWarning(logDesktopIntegration) << "XdgActivationV1: compositor did not advertise xdg_activation_v1, token requests will be skipped";
282+
});
283+
}
284+
#endif
285+
262286
connect(m_dockIntegration, &DdeDock::directionChanged, this, &DesktopIntegration::dockPositionChanged);
263287
connect(m_dockIntegration, &DdeDock::geometryChanged, this, &DesktopIntegration::dockGeometryChanged);
264288
connect(m_appearanceIntegration, &Appearance::wallpaperBlurhashChanged, this, &DesktopIntegration::backgroundUrlChanged);

src/ddeintegration/CMakeLists.txt

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
1+
# SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
22
#
33
# SPDX-License-Identifier: CC0-1.0
44

@@ -150,3 +150,55 @@ if (HAVE_DDE_API_EVENTLOGGER)
150150
target_compile_definitions(dde-integration-dbus PRIVATE HAVE_DDE_API_EVENTLOGGER)
151151
target_link_libraries(dde-integration-dbus PRIVATE DDEAPI::EventLogger)
152152
endif()
153+
154+
# Optional Wayland XDG activation support: request a token from the compositor
155+
# before launching applications so they can raise their window on first map.
156+
# Requires Qt6WaylandClient and wayland-protocols (for xdg-activation-v1.xml).
157+
find_package(Qt6 COMPONENTS WaylandClient QUIET)
158+
find_package(PkgConfig QUIET)
159+
if(Qt6WaylandClient_FOUND AND PkgConfig_FOUND)
160+
pkg_check_modules(WaylandProtocols QUIET wayland-protocols)
161+
if(WaylandProtocols_FOUND)
162+
pkg_get_variable(WAYLAND_PROTOCOLS_DATADIR wayland-protocols pkgdatadir)
163+
set(XDG_ACTIVATION_XML
164+
"${WAYLAND_PROTOCOLS_DATADIR}/staging/xdg-activation/xdg-activation-v1.xml")
165+
endif()
166+
endif()
167+
168+
if(Qt6WaylandClient_FOUND AND DEFINED XDG_ACTIVATION_XML AND EXISTS "${XDG_ACTIVATION_XML}")
169+
if(Qt6_VERSION VERSION_GREATER_EQUAL 6.10)
170+
find_package(Qt6 COMPONENTS WaylandClientPrivate QUIET)
171+
endif()
172+
173+
qt_generate_wayland_protocol_client_sources(dde-integration-dbus
174+
FILES
175+
${XDG_ACTIVATION_XML}
176+
)
177+
178+
target_sources(dde-integration-dbus PRIVATE
179+
xdgactivation.cpp
180+
)
181+
182+
target_sources(dde-integration-dbus PUBLIC
183+
FILE_SET HEADERS FILES xdgactivation.h
184+
)
185+
186+
# The Wayland protocol generator emits headers into the binary dir;
187+
# expose it publicly so consumers (e.g. launchpadcommon) can find them.
188+
target_include_directories(dde-integration-dbus PUBLIC
189+
${CMAKE_CURRENT_BINARY_DIR}
190+
)
191+
192+
target_link_libraries(dde-integration-dbus PUBLIC
193+
Qt6::WaylandClient
194+
Qt6::WaylandClientPrivate
195+
${DTK_NS}::Gui
196+
)
197+
198+
target_compile_definitions(dde-integration-dbus PUBLIC
199+
HAVE_WAYLAND_XDG_ACTIVATION
200+
)
201+
message(STATUS "XDG activation support enabled (${XDG_ACTIVATION_XML})")
202+
else()
203+
message(STATUS "XDG activation support disabled (Qt6WaylandClient or wayland-protocols not found)")
204+
endif()

src/ddeintegration/appmgr.cpp

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2023-2026 UnionTech Software Technology Co., Ltd.
1+
// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
22
//
33
// SPDX-License-Identifier: GPL-3.0-or-later
44

@@ -172,7 +172,7 @@ AppManager1Application * createAM1AppIface(const QString &desktopId)
172172

173173
// if return false, it means the launch is not even started.
174174
// if return true, it means we attempted to launch it via AM, but not sure if it's succeed.
175-
bool AppMgr::launchApp(const QString &desktopId)
175+
bool AppMgr::launchApp(const QString &desktopId, const QString &activationToken)
176176
{
177177
qCInfo(logDdeIntegration) << "Launching app:" << desktopId;
178178
AppManager1Application * amAppIface = createAM1AppIface(desktopId);
@@ -184,11 +184,18 @@ bool AppMgr::launchApp(const QString &desktopId)
184184
const auto path = amAppIface->path();
185185
QProcess process;
186186
process.setProcessChannelMode(QProcess::MergedChannels);
187+
188+
QStringList args = {"--by-user", path};
187189
#ifdef HAVE_DDE_API_EVENTLOGGER
188-
process.start("dde-am", {"--by-user", "--launch-type", "dde-launchpad", path});
189-
#else
190-
process.start("dde-am", {"--by-user", path});
190+
args << "--launch-type" << "dde-launchpad";
191191
#endif
192+
193+
if (!activationToken.isEmpty()) {
194+
qCDebug(logDdeIntegration) << "Passing XDG_ACTIVATION_TOKEN to dde-am for:" << desktopId;
195+
args << "--env" << (QLatin1String("XDG_ACTIVATION_TOKEN=") + activationToken);
196+
}
197+
198+
process.start("dde-am", args);
192199
if (!process.waitForFinished()) {
193200
qCWarning(logDdeIntegration) << "Failed to launch the desktopId:" << desktopId << process.errorString();
194201
return false;

src/ddeintegration/appmgr.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class AppMgr : public QObject
4141

4242
static AppMgr *instance();
4343

44-
static bool launchApp(const QString & desktopId);
44+
static bool launchApp(const QString & desktopId, const QString & activationToken = {});
4545
static bool autoStart(const QString & desktopId);
4646
static void setAutoStart(const QString & desktopId, bool autoStart);
4747
static bool disableScale(const QString & desktopId);
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
#include "xdgactivation.h"
6+
7+
#include <QEventLoop>
8+
#include <QTimer>
9+
#include <QWindow>
10+
#include <QLoggingCategory>
11+
12+
#include <private/qwaylandwindow_p.h>
13+
#include <private/qwaylanddisplay_p.h>
14+
#include <private/qwaylandinputdevice_p.h>
15+
16+
Q_DECLARE_LOGGING_CATEGORY(logDdeIntegration)
17+
18+
namespace DDEIntegration {
19+
20+
// ---------------------------------------------------------------------------
21+
// XdgActivationTokenV1
22+
// ---------------------------------------------------------------------------
23+
24+
XdgActivationTokenV1::~XdgActivationTokenV1()
25+
{
26+
destroy();
27+
}
28+
29+
void XdgActivationTokenV1::xdg_activation_token_v1_done(const QString &token)
30+
{
31+
Q_EMIT done(token);
32+
}
33+
34+
// ---------------------------------------------------------------------------
35+
// XdgActivationV1
36+
// ---------------------------------------------------------------------------
37+
38+
XdgActivationV1 *XdgActivationV1::instance()
39+
{
40+
static XdgActivationV1 s_instance;
41+
return &s_instance;
42+
}
43+
44+
XdgActivationV1::XdgActivationV1()
45+
: QWaylandClientExtensionTemplate<XdgActivationV1>(1)
46+
{
47+
}
48+
49+
XdgActivationV1::~XdgActivationV1()
50+
{
51+
if (isInitialized())
52+
destroy();
53+
}
54+
55+
QString XdgActivationV1::requestToken(QWindow *window, const QString &appId)
56+
{
57+
if (!isActive()) {
58+
qCWarning(logDdeIntegration) << "xdg_activation_v1 is not active, cannot request token";
59+
return {};
60+
}
61+
62+
auto *provider = new XdgActivationTokenV1;
63+
provider->init(get_activation_token());
64+
65+
// Attach the surface and input serial of the requesting window so the
66+
// compositor can verify focus and apply focus-stealing-prevention rules.
67+
if (window) {
68+
if (auto *waylandWindow =
69+
dynamic_cast<QtWaylandClient::QWaylandWindow *>(window->handle())) {
70+
if (auto *surface = waylandWindow->wlSurface()) {
71+
provider->set_surface(surface);
72+
}
73+
// set_serial tells the compositor which input event triggered this
74+
// launch request; without it the compositor may deny focus for the
75+
// new window (focus-stealing prevention).
76+
if (auto *inputDevice = waylandWindow->display()->lastInputDevice()) {
77+
provider->set_serial(inputDevice->serial(), inputDevice->wl_seat());
78+
}
79+
}
80+
}
81+
82+
if (!appId.isEmpty())
83+
provider->set_app_id(appId);
84+
85+
provider->commit();
86+
87+
// Block until the compositor delivers the token or the timeout fires.
88+
QString token;
89+
QEventLoop loop;
90+
QTimer timeout;
91+
timeout.setSingleShot(true);
92+
timeout.setInterval(2000);
93+
94+
connect(provider, &XdgActivationTokenV1::done, &loop,
95+
[&token, &loop](const QString &t) {
96+
token = t;
97+
loop.quit();
98+
});
99+
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
100+
101+
timeout.start();
102+
loop.exec();
103+
104+
if (token.isEmpty())
105+
qCWarning(logDdeIntegration) << "XDG activation token request timed out";
106+
else
107+
qCDebug(logDdeIntegration) << "Received XDG activation token for app:" << appId;
108+
109+
provider->deleteLater();
110+
return token;
111+
}
112+
113+
} // namespace DDEIntegration

src/ddeintegration/xdgactivation.h

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
#pragma once
6+
7+
#include <QObject>
8+
#include <QtWaylandClient/QWaylandClientExtension>
9+
10+
#include "qwayland-xdg-activation-v1.h"
11+
12+
class QWindow;
13+
14+
namespace DDEIntegration {
15+
16+
// Token provider: wraps the xdg_activation_token_v1 object and emits done()
17+
// when the compositor delivers the token.
18+
class XdgActivationTokenV1 : public QObject, public QtWayland::xdg_activation_token_v1
19+
{
20+
Q_OBJECT
21+
public:
22+
~XdgActivationTokenV1() override;
23+
24+
Q_SIGNALS:
25+
void done(const QString &token);
26+
27+
protected:
28+
void xdg_activation_token_v1_done(const QString &token) override;
29+
};
30+
31+
// Client extension: binds to the xdg_activation_v1 global and allows
32+
// requesting activation tokens.
33+
class XdgActivationV1 : public QWaylandClientExtensionTemplate<XdgActivationV1>,
34+
public QtWayland::xdg_activation_v1
35+
{
36+
Q_OBJECT
37+
public:
38+
// Returns the process-wide singleton instance (created on first call).
39+
static XdgActivationV1 *instance();
40+
41+
~XdgActivationV1() override;
42+
43+
// Synchronously request a token (blocks with a nested event loop until the
44+
// compositor delivers it or the 2-second timeout elapses).
45+
// Returns an empty string when not running on Wayland or when the
46+
// compositor does not expose the extension.
47+
QString requestToken(QWindow *window, const QString &appId);
48+
49+
private:
50+
explicit XdgActivationV1();
51+
};
52+
53+
} // namespace DDEIntegration

0 commit comments

Comments
 (0)