Skip to content

Commit efabb22

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 1abad99 commit efabb22

8 files changed

Lines changed: 252 additions & 8 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: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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,12 @@ 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+
if (auto *xdgActivation = instance().m_xdgActivation)
61+
token = xdgActivation->requestToken(QGuiApplication::focusWindow(), desktopId);
62+
#endif
63+
if (!AppMgr::launchApp(desktopId, token)) {
5464
qCDebug(logDesktopIntegration) << "AppMgr launch failed, trying AppInfo launch";
5565
AppInfo::launchByDesktopId(desktopId);
5666
}
@@ -259,6 +269,19 @@ DesktopIntegration::DesktopIntegration(QObject *parent)
259269
m_iconScaleFactor = dconfig->value("iconScaleFactor", 1.0).toReal();
260270
qCInfo(logDesktopIntegration) << "Icon scale factor loaded:" << m_iconScaleFactor;
261271

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

desktopintegration.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
class AppWiz;
1313
class DdeDock;
1414
class Appearance;
15+
#ifdef HAVE_WAYLAND_XDG_ACTIVATION
16+
namespace DDEIntegration { class XdgActivationV1; }
17+
#endif
1518
class DesktopIntegration : public QObject
1619
{
1720
Q_OBJECT
@@ -90,4 +93,7 @@ class DesktopIntegration : public QObject
9093
DdeDock * m_dockIntegration;
9194
Appearance * m_appearanceIntegration;
9295
qreal m_iconScaleFactor;
96+
#ifdef HAVE_WAYLAND_XDG_ACTIVATION
97+
DDEIntegration::XdgActivationV1 *m_xdgActivation = nullptr;
98+
#endif
9399
};

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: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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()
39+
: QWaylandClientExtensionTemplate<XdgActivationV1>(1)
40+
{
41+
}
42+
43+
XdgActivationV1::~XdgActivationV1()
44+
{
45+
if (isInitialized())
46+
destroy();
47+
}
48+
49+
QString XdgActivationV1::requestToken(QWindow *window, const QString &appId)
50+
{
51+
if (!isActive()) {
52+
qCWarning(logDdeIntegration) << "xdg_activation_v1 is not active, cannot request token";
53+
return {};
54+
}
55+
56+
auto *provider = new XdgActivationTokenV1;
57+
provider->init(get_activation_token());
58+
59+
// Attach the surface and input serial of the requesting window so the
60+
// compositor can verify focus and apply focus-stealing-prevention rules.
61+
if (window) {
62+
if (auto *waylandWindow =
63+
dynamic_cast<QtWaylandClient::QWaylandWindow *>(window->handle())) {
64+
if (auto *surface = waylandWindow->wlSurface()) {
65+
provider->set_surface(surface);
66+
}
67+
// set_serial tells the compositor which input event triggered this
68+
// launch request; without it the compositor may deny focus for the
69+
// new window (focus-stealing prevention).
70+
if (auto *inputDevice = waylandWindow->display()->lastInputDevice()) {
71+
provider->set_serial(inputDevice->serial(), inputDevice->wl_seat());
72+
}
73+
}
74+
}
75+
76+
if (!appId.isEmpty())
77+
provider->set_app_id(appId);
78+
79+
provider->commit();
80+
81+
// Block until the compositor delivers the token or the timeout fires.
82+
QString token;
83+
QEventLoop loop;
84+
QTimer timeout;
85+
timeout.setSingleShot(true);
86+
timeout.setInterval(2000);
87+
88+
connect(provider, &XdgActivationTokenV1::done, &loop,
89+
[&token, &loop](const QString &t) {
90+
token = t;
91+
loop.quit();
92+
});
93+
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
94+
95+
timeout.start();
96+
loop.exec();
97+
98+
if (token.isEmpty())
99+
qCWarning(logDdeIntegration) << "XDG activation token request timed out";
100+
else
101+
qCDebug(logDdeIntegration) << "Received XDG activation token for app:" << appId;
102+
103+
provider->deleteLater();
104+
return token;
105+
}
106+
107+
} // namespace DDEIntegration

src/ddeintegration/xdgactivation.h

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
explicit XdgActivationV1();
39+
~XdgActivationV1() override;
40+
41+
// Synchronously request a token (blocks with a nested event loop until the
42+
// compositor delivers it or the 2-second timeout elapses).
43+
// Returns an empty string when not running on Wayland or when the
44+
// compositor does not expose the extension.
45+
QString requestToken(QWindow *window, const QString &appId);
46+
};
47+
48+
} // namespace DDEIntegration

0 commit comments

Comments
 (0)