diff --git a/debian/control b/debian/control index 5213888b..0a63cb37 100644 --- a/debian/control +++ b/debian/control @@ -17,6 +17,7 @@ Build-Depends: qt6-tools-dev-tools, qt6-wayland-dev, qt6-wayland-private-dev, + wayland-protocols, libdtkcommon-dev, libdtk6core-dev (>= 6.0.43), # v-- provides qdbusxml2cpp-fix binary diff --git a/desktopintegration.cpp b/desktopintegration.cpp index 49531866..da037b21 100644 --- a/desktopintegration.cpp +++ b/desktopintegration.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -15,6 +15,11 @@ #include #include +#ifdef HAVE_WAYLAND_XDG_ACTIVATION +#include +#include +#endif + #include #include "appwiz.h" @@ -50,7 +55,13 @@ void DesktopIntegration::openSystemSettings() void DesktopIntegration::launchByDesktopId(const QString &desktopId) { qCInfo(logDesktopIntegration) << "Launching app by desktop ID:" << desktopId; - if (!AppMgr::launchApp(desktopId)) { + QString token; +#ifdef HAVE_WAYLAND_XDG_ACTIVATION + auto xdgActivation = DDEIntegration::XdgActivationV1::instance(); + if (xdgActivation->isActive()) + token = xdgActivation->requestToken(QGuiApplication::focusWindow(), desktopId); +#endif + if (!AppMgr::launchApp(desktopId, token)) { qCDebug(logDesktopIntegration) << "AppMgr launch failed, trying AppInfo launch"; AppInfo::launchByDesktopId(desktopId); } @@ -259,6 +270,19 @@ DesktopIntegration::DesktopIntegration(QObject *parent) m_iconScaleFactor = dconfig->value("iconScaleFactor", 1.0).toReal(); qCInfo(logDesktopIntegration) << "Icon scale factor loaded:" << m_iconScaleFactor; +#ifdef HAVE_WAYLAND_XDG_ACTIVATION + if (DTK_GUI_NAMESPACE::DGuiApplicationHelper::testAttribute( + DTK_GUI_NAMESPACE::DGuiApplicationHelper::IsWaylandPlatform)) { + auto *xdgActivation = DDEIntegration::XdgActivationV1::instance(); + connect(xdgActivation, &DDEIntegration::XdgActivationV1::activeChanged, this, []() { + if (DDEIntegration::XdgActivationV1::instance()->isActive()) + qCInfo(logDesktopIntegration) << "XdgActivationV1: ready, XDG activation token support enabled"; + else + qCWarning(logDesktopIntegration) << "XdgActivationV1: compositor did not advertise xdg_activation_v1, token requests will be skipped"; + }); + } +#endif + connect(m_dockIntegration, &DdeDock::directionChanged, this, &DesktopIntegration::dockPositionChanged); connect(m_dockIntegration, &DdeDock::geometryChanged, this, &DesktopIntegration::dockGeometryChanged); connect(m_appearanceIntegration, &Appearance::wallpaperBlurhashChanged, this, &DesktopIntegration::backgroundUrlChanged); diff --git a/src/ddeintegration/CMakeLists.txt b/src/ddeintegration/CMakeLists.txt index 4173c115..0d2e8635 100644 --- a/src/ddeintegration/CMakeLists.txt +++ b/src/ddeintegration/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +# SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. # # SPDX-License-Identifier: CC0-1.0 @@ -150,3 +150,55 @@ if (HAVE_DDE_API_EVENTLOGGER) target_compile_definitions(dde-integration-dbus PRIVATE HAVE_DDE_API_EVENTLOGGER) target_link_libraries(dde-integration-dbus PRIVATE DDEAPI::EventLogger) endif() + +# Optional Wayland XDG activation support: request a token from the compositor +# before launching applications so they can raise their window on first map. +# Requires Qt6WaylandClient and wayland-protocols (for xdg-activation-v1.xml). +find_package(Qt6 COMPONENTS WaylandClient QUIET) +find_package(PkgConfig QUIET) +if(Qt6WaylandClient_FOUND AND PkgConfig_FOUND) + pkg_check_modules(WaylandProtocols QUIET wayland-protocols) + if(WaylandProtocols_FOUND) + pkg_get_variable(WAYLAND_PROTOCOLS_DATADIR wayland-protocols pkgdatadir) + set(XDG_ACTIVATION_XML + "${WAYLAND_PROTOCOLS_DATADIR}/staging/xdg-activation/xdg-activation-v1.xml") + endif() +endif() + +if(Qt6WaylandClient_FOUND AND DEFINED XDG_ACTIVATION_XML AND EXISTS "${XDG_ACTIVATION_XML}") + if(Qt6_VERSION VERSION_GREATER_EQUAL 6.10) + find_package(Qt6 COMPONENTS WaylandClientPrivate QUIET) + endif() + + qt_generate_wayland_protocol_client_sources(dde-integration-dbus + FILES + ${XDG_ACTIVATION_XML} + ) + + target_sources(dde-integration-dbus PRIVATE + xdgactivation.cpp + ) + + target_sources(dde-integration-dbus PUBLIC + FILE_SET HEADERS FILES xdgactivation.h + ) + + # The Wayland protocol generator emits headers into the binary dir; + # expose it publicly so consumers (e.g. launchpadcommon) can find them. + target_include_directories(dde-integration-dbus PUBLIC + ${CMAKE_CURRENT_BINARY_DIR} + ) + + target_link_libraries(dde-integration-dbus PUBLIC + Qt6::WaylandClient + Qt6::WaylandClientPrivate + ${DTK_NS}::Gui + ) + + target_compile_definitions(dde-integration-dbus PUBLIC + HAVE_WAYLAND_XDG_ACTIVATION + ) + message(STATUS "XDG activation support enabled (${XDG_ACTIVATION_XML})") +else() + message(STATUS "XDG activation support disabled (Qt6WaylandClient or wayland-protocols not found)") +endif() diff --git a/src/ddeintegration/appmgr.cpp b/src/ddeintegration/appmgr.cpp index 071f5a81..8abca924 100644 --- a/src/ddeintegration/appmgr.cpp +++ b/src/ddeintegration/appmgr.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023-2026 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -172,7 +172,7 @@ AppManager1Application * createAM1AppIface(const QString &desktopId) // if return false, it means the launch is not even started. // if return true, it means we attempted to launch it via AM, but not sure if it's succeed. -bool AppMgr::launchApp(const QString &desktopId) +bool AppMgr::launchApp(const QString &desktopId, const QString &activationToken) { qCInfo(logDdeIntegration) << "Launching app:" << desktopId; AppManager1Application * amAppIface = createAM1AppIface(desktopId); @@ -184,11 +184,18 @@ bool AppMgr::launchApp(const QString &desktopId) const auto path = amAppIface->path(); QProcess process; process.setProcessChannelMode(QProcess::MergedChannels); + + QStringList args = {"--by-user", path}; #ifdef HAVE_DDE_API_EVENTLOGGER - process.start("dde-am", {"--by-user", "--launch-type", "dde-launchpad", path}); -#else - process.start("dde-am", {"--by-user", path}); + args << "--launch-type" << "dde-launchpad"; #endif + + if (!activationToken.isEmpty()) { + qCDebug(logDdeIntegration) << "Passing XDG_ACTIVATION_TOKEN to dde-am for:" << desktopId; + args << "--env" << (QLatin1String("XDG_ACTIVATION_TOKEN=") + activationToken); + } + + process.start("dde-am", args); if (!process.waitForFinished()) { qCWarning(logDdeIntegration) << "Failed to launch the desktopId:" << desktopId << process.errorString(); return false; diff --git a/src/ddeintegration/appmgr.h b/src/ddeintegration/appmgr.h index 1f060883..04a267f0 100644 --- a/src/ddeintegration/appmgr.h +++ b/src/ddeintegration/appmgr.h @@ -41,7 +41,7 @@ class AppMgr : public QObject static AppMgr *instance(); - static bool launchApp(const QString & desktopId); + static bool launchApp(const QString & desktopId, const QString & activationToken = {}); static bool autoStart(const QString & desktopId); static void setAutoStart(const QString & desktopId, bool autoStart); static bool disableScale(const QString & desktopId); diff --git a/src/ddeintegration/xdgactivation.cpp b/src/ddeintegration/xdgactivation.cpp new file mode 100644 index 00000000..25365330 --- /dev/null +++ b/src/ddeintegration/xdgactivation.cpp @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "xdgactivation.h" + +#include +#include +#include +#include + +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(logDdeIntegration) + +namespace DDEIntegration { + +// --------------------------------------------------------------------------- +// XdgActivationTokenV1 +// --------------------------------------------------------------------------- + +XdgActivationTokenV1::~XdgActivationTokenV1() +{ + destroy(); +} + +void XdgActivationTokenV1::xdg_activation_token_v1_done(const QString &token) +{ + Q_EMIT done(token); +} + +// --------------------------------------------------------------------------- +// XdgActivationV1 +// --------------------------------------------------------------------------- + +XdgActivationV1 *XdgActivationV1::instance() +{ + static XdgActivationV1 s_instance; + return &s_instance; +} + +XdgActivationV1::XdgActivationV1() + : QWaylandClientExtensionTemplate(1) +{ +} + +XdgActivationV1::~XdgActivationV1() +{ + if (isInitialized()) + destroy(); +} + +QString XdgActivationV1::requestToken(QWindow *window, const QString &appId) +{ + if (!isActive()) { + qCWarning(logDdeIntegration) << "xdg_activation_v1 is not active, cannot request token"; + return {}; + } + + auto *provider = new XdgActivationTokenV1; + provider->init(get_activation_token()); + + // Attach the surface and input serial of the requesting window so the + // compositor can verify focus and apply focus-stealing-prevention rules. + if (window) { + if (auto *waylandWindow = + dynamic_cast(window->handle())) { + if (auto *surface = waylandWindow->wlSurface()) { + provider->set_surface(surface); + } + // set_serial tells the compositor which input event triggered this + // launch request; without it the compositor may deny focus for the + // new window (focus-stealing prevention). + if (auto *inputDevice = waylandWindow->display()->lastInputDevice()) { + provider->set_serial(inputDevice->serial(), inputDevice->wl_seat()); + } + } + } + + if (!appId.isEmpty()) + provider->set_app_id(appId); + + provider->commit(); + + // Block until the compositor delivers the token or the timeout fires. + QString token; + QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); + timeout.setInterval(2000); + + connect(provider, &XdgActivationTokenV1::done, &loop, + [&token, &loop](const QString &t) { + token = t; + loop.quit(); + }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + + timeout.start(); + loop.exec(); + + if (token.isEmpty()) + qCWarning(logDdeIntegration) << "XDG activation token request timed out"; + else + qCDebug(logDdeIntegration) << "Received XDG activation token for app:" << appId; + + provider->deleteLater(); + return token; +} + +} // namespace DDEIntegration diff --git a/src/ddeintegration/xdgactivation.h b/src/ddeintegration/xdgactivation.h new file mode 100644 index 00000000..837c16ec --- /dev/null +++ b/src/ddeintegration/xdgactivation.h @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "qwayland-xdg-activation-v1.h" + +class QWindow; + +namespace DDEIntegration { + +// Token provider: wraps the xdg_activation_token_v1 object and emits done() +// when the compositor delivers the token. +class XdgActivationTokenV1 : public QObject, public QtWayland::xdg_activation_token_v1 +{ + Q_OBJECT +public: + ~XdgActivationTokenV1() override; + +Q_SIGNALS: + void done(const QString &token); + +protected: + void xdg_activation_token_v1_done(const QString &token) override; +}; + +// Client extension: binds to the xdg_activation_v1 global and allows +// requesting activation tokens. +class XdgActivationV1 : public QWaylandClientExtensionTemplate, + public QtWayland::xdg_activation_v1 +{ + Q_OBJECT +public: + // Returns the process-wide singleton instance (created on first call). + static XdgActivationV1 *instance(); + + ~XdgActivationV1() override; + + // Synchronously request a token (blocks with a nested event loop until the + // compositor delivers it or the 2-second timeout elapses). + // Returns an empty string when not running on Wayland or when the + // compositor does not expose the extension. + QString requestToken(QWindow *window, const QString &appId); + +private: + explicit XdgActivationV1(); +}; + +} // namespace DDEIntegration