From 8ac804aad6b68b6b1ff581b42367ca1d2211fb61 Mon Sep 17 00:00:00 2001 From: zhaoyingzhen Date: Tue, 27 Oct 2026 14:19:49 +0800 Subject: [PATCH] fix: adapt shortcuts to the Treeland v3 protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Treeland now exposes shortcut-manager v3, so the v2 client cannot register shortcuts and legacy action IDs must no longer be sent as protocol values. Generate v3 bindings and preserve configuration and D-Bus action IDs through an explicit mapping. Replace commit acknowledgements with flush/display sync, handle individual and deferred binding failures, clear partial registrations, and restore old bindings when edits fail. Enable the implemented tiling, minimize and workspace-move actions, and keep unsupported actions unregistered. Add protocol regression coverage, declare the Qt Wayland test runtime package, and document the new lifecycle. The 13 shortcut test suites and six live Treeland scenarios passed; bulk startup registered and cleaned 100 bindings. Treeland 已改为提供 shortcut-manager v3,旧版客户端无法注册快捷键, 原有业务动作 ID 也不能再直接作为协议枚举发送。 切换到 v3 生成代码,通过显式映射保留配置和 D-Bus 动作 ID。 使用 flush/display sync 替代提交确认,处理逐条及延迟注册失败, 清理部分成功的绑定,并在修改失败时恢复旧绑定。启用已实现的平铺、 最小化和移动到相邻工作区动作,继续拒绝尚不支持的动作。 补充协议回归测试、Qt Wayland 测试运行依赖及生命周期文档。 13 组快捷键测试和 6 个真实 Treeland 场景通过,批量启动完成了 100 条绑定的注册和清理。 Log: adapt shortcuts to the Treeland v3 protocol Pms: TASK-397267 Change-Id: I9c2eb32345ee7003cac893449dc414309fbee347 --- debian/control | 3 +- src/plugin-qt/shortcut/CMakeLists.txt | 10 +- src/plugin-qt/shortcut/DEVELOPER_GUIDE.md | 35 +- .../src/backend/abstractgesturehandler.h | 4 +- .../shortcut/src/backend/abstractkeyhandler.h | 9 +- .../wayland/treelandshortcutwrapper.cpp | 151 +++--- .../backend/wayland/treelandshortcutwrapper.h | 32 +- .../backend/wayland/waylandgesturehandler.cpp | 43 +- .../backend/wayland/waylandgesturehandler.h | 6 +- .../src/backend/wayland/waylandkeyhandler.cpp | 36 +- .../src/backend/wayland/waylandkeyhandler.h | 2 + .../src/core/gestureactioncatalog.cpp | 5 +- .../shortcut/src/core/gesturemanager.cpp | 35 +- .../shortcut/src/core/gesturemanager.h | 2 + .../shortcut/src/core/keybindingmanager.cpp | 17 +- .../shortcut/src/core/keybindingmanager.h | 5 +- .../shortcut/src/core/shortcutmanager.cpp | 7 +- .../shortcut/src/core/shortcutmanager.h | 2 +- .../src/core/triggeractioncatalog.cpp | 92 ++-- .../shortcut/src/core/triggeractioncatalog.h | 11 +- src/plugin-qt/shortcut/tests/CMakeLists.txt | 57 +++ .../tests/tst_gestureactioncatalog.cpp | 6 +- .../tests/tst_triggeractioncatalog.cpp | 54 ++- .../shortcut/tests/tst_waylandshortcuts.cpp | 443 ++++++++++++++++++ 24 files changed, 880 insertions(+), 187 deletions(-) create mode 100644 src/plugin-qt/shortcut/tests/tst_waylandshortcuts.cpp diff --git a/debian/control b/debian/control index 203898b2..51a0878b 100644 --- a/debian/control +++ b/debian/control @@ -32,10 +32,11 @@ Build-Depends: debhelper (>= 11), libxcb-xtest0-dev, libxtst-dev, dbus-daemon , + qt6-wayland , xauth , xvfb , libwayland-dev, - treeland-protocols (>> 0.5.7), + treeland-protocols (>= 0.6.0), python3, Standards-Version: 4.1.3 Homepage: https://github.com/linuxdeepin diff --git a/src/plugin-qt/shortcut/CMakeLists.txt b/src/plugin-qt/shortcut/CMakeLists.txt index 0b183aad..89f39a48 100644 --- a/src/plugin-qt/shortcut/CMakeLists.txt +++ b/src/plugin-qt/shortcut/CMakeLists.txt @@ -14,7 +14,7 @@ set(CMAKE_AUTOMOC ON) find_package(Qt6 REQUIRED COMPONENTS Core DBus Gui WaylandClient) find_package(Dtk6 REQUIRED COMPONENTS Core DConfig Gui) find_package(PkgConfig REQUIRED) -find_package(TreelandProtocols REQUIRED) +find_package(TreelandProtocols 0.6.0 REQUIRED) pkg_check_modules(XCB REQUIRED xcb xcb-keysyms) pkg_check_modules(XCB_XINPUT REQUIRED xcb-xinput) @@ -84,12 +84,12 @@ add_library(${PLUGIN_NAME} SHARED ${PLUGIN_SOURCES}) qt6_generate_wayland_protocol_client_sources( ${PLUGIN_NAME} FILES - ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-v2.xml + ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-unstable-v3.xml ) # Add generated protocol files target_sources(${PLUGIN_NAME} PRIVATE - ${CMAKE_CURRENT_BINARY_DIR}/wayland-treeland-shortcut-manager-v2-protocol.c + ${CMAKE_CURRENT_BINARY_DIR}/wayland-treeland-shortcut-manager-unstable-v3-protocol.c ) target_link_libraries(${PLUGIN_NAME} PRIVATE @@ -224,8 +224,8 @@ if(BUILD_SHORTCUT_DEBUG_BINARY) ${SHORTCUT_SRC_DIR}/main.cpp ${SHORTCUT_COMMON_SOURCES} # Reuse Wayland protocol files generated for plugin - ${CMAKE_CURRENT_BINARY_DIR}/wayland-treeland-shortcut-manager-v2-protocol.c - ${CMAKE_CURRENT_BINARY_DIR}/qwayland-treeland-shortcut-manager-v2.cpp + ${CMAKE_CURRENT_BINARY_DIR}/wayland-treeland-shortcut-manager-unstable-v3-protocol.c + ${CMAKE_CURRENT_BINARY_DIR}/qwayland-treeland-shortcut-manager-unstable-v3.cpp ) # Enable AUTOMOC for debug binary diff --git a/src/plugin-qt/shortcut/DEVELOPER_GUIDE.md b/src/plugin-qt/shortcut/DEVELOPER_GUIDE.md index 549b1a2d..ba783308 100644 --- a/src/plugin-qt/shortcut/DEVELOPER_GUIDE.md +++ b/src/plugin-qt/shortcut/DEVELOPER_GUIDE.md @@ -45,10 +45,18 @@ Deepin 快捷键服务是一个基于 DConfig 的动态快捷键管理框架, 和本地化由公共管理层处理,输入后端按桌面环境选择: - X11 使用 `X11KeyHandler` 抓取按键,并通过 system bus 手势采集服务接收触控板事件。 -- Wayland 使用 Treeland shortcut 协议注册按键和手势。 +- Wayland 使用 `treeland-shortcut-manager-unstable-v3`(接口版本 1)注册按键和手势, + 构建依赖 `treeland-protocols >= 0.6.0`。 应用使用相同的 DConfig 格式和 D-Bus 接口,不应自行维护 X11/Wayland 两套配置。 +Treeland v3 的 bind/unbind 请求逐条生效,没有协议 `commit` 或成功事件。 +后端的 `commit()` 负责 flush,`commitSync()` 用 `wl_display.sync` 等待处理并汇总 +`bind_failure`,不提供原子提交。修改失败时服务显式删除部分成功的新绑定并恢复旧绑定。 +非活动会话的绑定会延后应用,失败可能在 sync 返回后通知;服务收到后将对应配置的 +运行时绑定移除,保存的配置仍可查询。每次注册使用独立的协议名称,避免迟到失败影响 +已经替换的绑定。 + ## 2. 配置文件编写 快捷键服务使用两种配置文件协同工作: @@ -1326,8 +1334,9 @@ dbus-monitor --system "interface='org.deepin.dde.KeyEvent1'" #### 问题 6:单 modifier 键(Meta / Ctrl 等)作为 hotkey 行为不确定 -**说明**: X11 和 Treeland 后端都接受单 modifier 配置,但它与 `Meta+X` 等组合键 -同时存在时依赖后端的按下/释放派发语义,应用不能只验证单按场景。 +**说明**: Treeland 只接受单独的 `Meta`,不接受单独的 Ctrl、Alt、Shift 等修饰键。 +X11 的支持范围不同。单独 `Meta` 与 `Meta+X` 同时存在时,应用还需验证组合键的 +按下/释放场景。 **适配建议**: - 优先用 modifier + 非 modifier 实键的组合(如 `Ctrl+Alt+T`、`Meta+E`) @@ -1338,11 +1347,15 @@ dbus-monitor --system "interface='org.deepin.dde.KeyEvent1'" **症状**: 安装/卸载带快捷键配置的包后,`ReloadConfigs` 被调,journal 里能看到 `Config added` / `Parsed KeyConfig`,但用户按对应快捷键长时间无响应,直到某次 D-Bus 调用(d-feet、控制中心打开等)介入后才突然生效。 -**根因**: `TreelandShortcutWrapper::commit / commitDeferred` 调用底层 wayland commit 后没有 `wl_display_flush`,wayland 协议消息卡在 libwayland 客户端 outgoing 缓冲。dde-services 是非 GUI 进程,与 compositor 之间没有常规 wayland 往来,缓冲要等到 Qt event loop 因为其它原因(D-Bus 调用等)跑一轮才被 Qt wayland 平台插件附带 flush。这之前 Treeland 根本没收到 bind/commit,binding 自然不生效。 +**根因**: 注册请求只写入 libwayland 客户端的 outgoing 缓冲。快捷键服务没有窗口, +不能依靠窗口的常规 Wayland 往来及时发送这些请求。 -**修复(2026-05-25 已合入)**: `commit / commitDeferred` 调底层 commit 后通过 `QNativeInterface::QWaylandApplication::display()` 拿 `wl_display*` 并显式 `wl_display_flush`,确保协议消息立即送达。 +**处理**: `TreelandShortcutWrapper::flush / flushDeferred` 通过 +`QNativeInterface::QWaylandApplication::display()` 获取连接,显式调用 +`wl_display_flush`。需要同步结果时,`syncAndWait` 发送 display sync 并 flush, +等待此前绑定的失败事件和 sync 回调;v3 不再发送协议 commit。 -**对应代码**: `src/backend/wayland/treelandshortcutwrapper.cpp`(`commit`、`commitDeferred`、`flushWaylandDisplay`) +**对应代码**: `src/backend/wayland/treelandshortcutwrapper.cpp`。 ### 6.5 调试工具 @@ -1404,8 +1417,14 @@ Qt 没有定义的硬件键可以使用十进制或十六进制 Linux input keyc `triggerValue` 保存数字 action ID: - `0` 表示禁用。 -- `1-99` 与 Treeland shortcut 协议枚举保持一致。 -- `100+` 是 dde-services 的 X11 扩展动作。 +- 其余已定义的 ID 是稳定的业务编号,与 Treeland v3 协议编号分开维护;升级协议不改 + JSON、用户已保存的 `triggerValue` 或 D-Bus 返回的动作 ID。 +- 后端通过 `TriggerActionCatalog::treelandActionId()` 转换。例如配置 `10`(显示桌面) + 对应 v3 的 `24`,配置 `100/101`(左右平铺)对应 `32/33`,配置 `1`(Notify)对应 + 协议 `0`;禁用动作不发送为 Notify。 +- Treeland 支持左右平铺、最小化和窗口移到前后工作区。调整窗口大小和屏幕缩放虽已在 + v3 XML 定义,当前合成器尚未实现,服务继续将其视为不支持;旧 Quit/TaskSwitchEnter + 也不再映射为协议动作。 动作 ID、名称和后端支持状态的权威定义在 `src/core/triggeractioncatalog.h`、`src/core/triggeractioncatalog.cpp` 和 diff --git a/src/plugin-qt/shortcut/src/backend/abstractgesturehandler.h b/src/plugin-qt/shortcut/src/backend/abstractgesturehandler.h index 4070934b..2e56c7ba 100644 --- a/src/plugin-qt/shortcut/src/backend/abstractgesturehandler.h +++ b/src/plugin-qt/shortcut/src/backend/abstractgesturehandler.h @@ -19,11 +19,13 @@ class AbstractGestureHandler : public QObject virtual bool unregisterGesture(const QString &appId) = 0; virtual bool isAvailable() const { return true; } - // commit() async (may be debounced); commitSync() returns compositor ack. + // Flush requests / wait for processing, without transaction guarantees. + // Inactive Treeland sessions can report registrationFailed after the sync. virtual bool commit() { return true; } virtual bool commitSync() { return commit(); } signals: void activated(const QString &name); + void registrationFailed(const QString &id); void availabilityChanged(bool available); }; diff --git a/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h b/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h index 2fd7297e..22da5c26 100644 --- a/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h +++ b/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h @@ -27,10 +27,10 @@ class AbstractKeyHandler : public QObject virtual bool unregisterKey(const QString &shortcutId) = 0; virtual bool isAvailable() const { return true; } - // commit(): async; backend may debounce. Returns true on scheduling, not - // on compositor ack. X11 backend is a no-op (XGrabKey is immediate). - // commitSync(): synchronous; returns whether the compositor accepted the - // pending changes. Use when you need to roll back on failure. + // commit() flushes queued requests; commitSync() waits for their processing + // and reports binding failures. Neither provides an atomic transaction. + // Treeland may defer inactive-session requests past the sync, reporting + // registrationFailed later. X11 is immediate and these are no-ops. virtual bool commit() { return true; } virtual bool commitSync() { return commit(); } @@ -55,6 +55,7 @@ class AbstractKeyHandler : public QObject signals: void keyActivated(const QString &shortcutId); + void registrationFailed(const QString &shortcutId); void numLockStateChanged(bool on); void capsLockStateChanged(bool on); void captureStarted(); diff --git a/src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp b/src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp index bb3d3d16..516ceb46 100644 --- a/src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp +++ b/src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp @@ -9,36 +9,33 @@ #include #include #include +#include #include #include -extern "C" { -#include -} +#include +#include namespace { -// commit() only queues the bind/unbind/commit messages into libwayland's -// internal outgoing buffer. With no other wayland traffic on this client -// (dde-services has no window), the buffer can sit unflushed for minutes -// until some D-Bus call wakes the Qt event loop. Force the flush so the -// compositor sees the change as soon as the caller intended. -inline void flushWaylandDisplay() +wl_display *waylandDisplay() { - if (auto *wlApp = qGuiApp->nativeInterface()) { - wl_display_flush(wlApp->display()); + if (qGuiApp) { + if (auto *wlApp = qGuiApp->nativeInterface()) + return wlApp->display(); } + return nullptr; } const char *bindErrorName(uint32_t error) { switch (error) { - case 1: + case QtWayland::treeland_shortcut_manager_v3::bind_error_name_conflict: return "name_conflict"; - case 2: + case QtWayland::treeland_shortcut_manager_v3::bind_error_duplicate_binding: return "duplicate_binding"; - case 3: + case QtWayland::treeland_shortcut_manager_v3::bind_error_invalid_argument: return "invalid_argument"; - case 4: + case QtWayland::treeland_shortcut_manager_v3::bind_error_internal_error: return "internal_error"; default: return "unknown"; @@ -46,9 +43,16 @@ const char *bindErrorName(uint32_t error) } } // namespace +struct TreelandShortcutWrapper::PendingSync { + QEventLoop loop; + QSet bindings; + bool failed = false; + bool done = false; +}; + TreelandShortcutWrapper::TreelandShortcutWrapper(QObject *parent) - : QWaylandClientExtensionTemplate(2) - , QtWayland::treeland_shortcut_manager_v2() + : QWaylandClientExtensionTemplate(1) + , QtWayland::treeland_shortcut_manager_v3() { setParent(parent); @@ -61,6 +65,8 @@ TreelandShortcutWrapper::TreelandShortcutWrapper(QObject *parent) } else { qCWarning(logShortcut) << "TreelandShortcutManager protocol is now inactive!"; m_boundObject = nullptr; + m_pendingBindings.clear(); + m_pendingBindFailed = false; emit protocolInactive(); } }); @@ -86,6 +92,7 @@ bool TreelandShortcutWrapper::bindKey(const QString &name, const QString &key, u return false; } + m_pendingBindings.insert(name); bind_key(name, key, flags, action); return true; } @@ -96,6 +103,7 @@ bool TreelandShortcutWrapper::bindSwipeGesture(const QString &name, int finger, return false; } + m_pendingBindings.insert(name); bind_swipe_gesture(name, finger, direction, action); return true; } @@ -106,6 +114,7 @@ bool TreelandShortcutWrapper::bindHoldGesture(const QString &name, int finger, i return false; } + m_pendingBindings.insert(name); bind_hold_gesture(name, finger, action); return true; } @@ -122,82 +131,96 @@ bool TreelandShortcutWrapper::unbind(const QString &name) return false; } - QtWayland::treeland_shortcut_manager_v2::unbind(name); + QtWayland::treeland_shortcut_manager_v3::unbind(name); return true; } -void TreelandShortcutWrapper::commit() +void TreelandShortcutWrapper::flush() { if (!isActive()) { return; } - QtWayland::treeland_shortcut_manager_v2::commit(); - flushWaylandDisplay(); + // The service has no window traffic to flush queued Wayland requests. + if (auto *display = waylandDisplay()) + wl_display_flush(display); } -void TreelandShortcutWrapper::commitDeferred() +void TreelandShortcutWrapper::flushDeferred() { - // Subsequent calls in this tick are folded into the queued one — - // their bind/unbind ops already sit in the same wire-protocol queue. - if (m_commitPending) { + if (m_flushPending) { return; } - m_commitPending = true; + m_flushPending = true; QTimer::singleShot(0, this, [this]() { - m_commitPending = false; - if (!isActive()) { - return; - } - QtWayland::treeland_shortcut_manager_v2::commit(); - flushWaylandDisplay(); + m_flushPending = false; + flush(); }); } -bool TreelandShortcutWrapper::commitAndWait(int timeoutMs) +bool TreelandShortcutWrapper::syncAndWait(int timeoutMs) { - if (!isActive()) { + auto *display = waylandDisplay(); + if (!isActive() || !display) { return false; } - QEventLoop loop; - bool success = false; - bool responded = false; - - connect(this, &TreelandShortcutWrapper::commitStatus, &loop, [&](bool status) { - loop.quit(); - success = status; - responded = true; - qCDebug(logShortcut) << "TreelandShortcutWrapper::commitAndWait response:" << status; - }, Qt::SingleShotConnection); - - QTimer::singleShot(timeoutMs, &loop, [&]() { - if (!responded) { - qCWarning(logShortcut) << "TreelandShortcutWrapper::commitAndWait timeout after" << timeoutMs << "ms"; - loop.quit(); - } - }); + auto *callback = wl_display_sync(display); + if (!callback) + return false; + + PendingSync sync; + sync.bindings = std::exchange(m_pendingBindings, {}); + sync.failed = std::exchange(m_pendingBindFailed, false); + m_syncs.append(&sync); - QtWayland::treeland_shortcut_manager_v2::commit(); - flushWaylandDisplay(); - loop.exec(); - return success; + static const wl_callback_listener listener = { + [](void *data, wl_callback *, uint32_t) { + auto *pending = static_cast(data); + pending->done = true; + pending->loop.quit(); + } + }; + wl_callback_add_listener(callback, &listener, &sync); + + QPointer guard(this); + const auto *boundObject = object(); + connect(this, &TreelandShortcutWrapper::protocolInactive, &sync.loop, &QEventLoop::quit); + connect(this, &QObject::destroyed, &sync.loop, &QEventLoop::quit); + QTimer timer; + timer.setSingleShot(true); + connect(&timer, &QTimer::timeout, &sync.loop, &QEventLoop::quit); + timer.start(timeoutMs); + flush(); + sync.loop.exec(); + wl_callback_destroy(callback); + + if (!guard) + return false; + m_syncs.removeOne(&sync); + if (!sync.done) + qCWarning(logShortcut) << "Treeland shortcut sync did not complete within" << timeoutMs << "ms"; + return sync.done && !sync.failed && isActive() && object() == boundObject; } -void TreelandShortcutWrapper::treeland_shortcut_manager_v2_activated(const QString &name, uint32_t flags) +void TreelandShortcutWrapper::treeland_shortcut_manager_v3_activated(const QString &name, uint32_t flags) { qCDebug(logShortcut) << "Treeland Shortcut Manager Activated:" << name << "flags:" << flags; emit activated(name, flags); } -void TreelandShortcutWrapper::treeland_shortcut_manager_v2_commit_success() +void TreelandShortcutWrapper::treeland_shortcut_manager_v3_bind_failure(const QString &name, uint32_t error) { - emit commitStatus(true); -} - -void TreelandShortcutWrapper::treeland_shortcut_manager_v2_commit_failure(const QString &name, uint32_t error) -{ - qCWarning(logShortcut) << "Treeland Shortcut Manager Commit Failed:" << name + qCWarning(logShortcut) << "Treeland shortcut binding failed:" << name << "Error:" << error << bindErrorName(error); - emit commitStatus(false); + // Keep failures received before syncAndWait as well as those received while + // waiting. A late failure from an inactive session must not fail a later, + // unrelated batch, but it must still invalidate the affected binding. + if (m_pendingBindings.contains(name)) + m_pendingBindFailed = true; + for (PendingSync *sync : std::as_const(m_syncs)) { + if (sync->bindings.contains(name)) + sync->failed = true; + } + emit bindFailed(name, error); } diff --git a/src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.h b/src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.h index 80fe0b8a..df593834 100644 --- a/src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.h +++ b/src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.h @@ -5,12 +5,13 @@ #pragma once #include +#include #include -#include "qwayland-treeland-shortcut-manager-v2.h" +#include "qwayland-treeland-shortcut-manager-unstable-v3.h" class TreelandShortcutWrapper : public QWaylandClientExtensionTemplate, - public QtWayland::treeland_shortcut_manager_v2 + public QtWayland::treeland_shortcut_manager_v3 { Q_OBJECT public: @@ -21,13 +22,13 @@ class TreelandShortcutWrapper : public QWaylandClientExtensionTemplate m_pendingBindings; + QList m_syncs; + bool m_pendingBindFailed = false; + bool m_flushPending = false; }; diff --git a/src/plugin-qt/shortcut/src/backend/wayland/waylandgesturehandler.cpp b/src/plugin-qt/shortcut/src/backend/wayland/waylandgesturehandler.cpp index 170f1307..f8fa0ba8 100644 --- a/src/plugin-qt/shortcut/src/backend/wayland/waylandgesturehandler.cpp +++ b/src/plugin-qt/shortcut/src/backend/wayland/waylandgesturehandler.cpp @@ -6,6 +6,7 @@ #include "waylandgesturehandler.h" #include "core/shortcutconfig.h" +#include "core/triggeractioncatalog.h" #include "treelandshortcutwrapper.h" #include @@ -15,6 +16,7 @@ WaylandGestureHandler::WaylandGestureHandler(TreelandShortcutWrapper *wrapper, Q , m_wrapper(wrapper) { connect(m_wrapper, &TreelandShortcutWrapper::activated, this, &WaylandGestureHandler::onActivated); + connect(m_wrapper, &TreelandShortcutWrapper::bindFailed, this, &WaylandGestureHandler::onBindFailed); connect(m_wrapper, &TreelandShortcutWrapper::protocolInactive, this, [this]() { qCWarning(logShortcut) << "WaylandGestureHandler: Protocol inactive, clearing all bindings"; m_bindings.clear(); @@ -33,15 +35,15 @@ bool WaylandGestureHandler::registerGesture(const GestureConfig &config) unregisterGesture(config.getId()); } - // use id as name - QString name = config.getId(); - int action = (int)QtWayland::treeland_shortcut_manager_v2::action_notify; // notify + const QString name = config.getId() + "#" + QString::number(++m_bindingSerial); + int action = QtWayland::treeland_shortcut_manager_v3::action_notify; - if (config.triggerType == (int)TriggerType::Action && !config.triggerValue.isEmpty()) { - const QString &first = config.triggerValue.first(); - bool ok; - int val = first.toInt(&ok); - if (ok) action = val; + if (config.triggerType == int(TriggerType::Action)) { + const auto actionId = TriggerActionCatalog::resolve(config.triggerValue.value(0)); + const auto treelandAction = TriggerActionCatalog::treelandActionId(actionId); + if (!treelandAction) + return false; + action = *treelandAction; } bool success = false; @@ -52,7 +54,7 @@ bool WaylandGestureHandler::registerGesture(const GestureConfig &config) } if (success) { - m_bindings.append(name); + m_bindings.insert(config.getId(), name); } else { qCWarning(logShortcut) << "Failed to bind gesture for" << config.getId(); } @@ -64,21 +66,20 @@ bool WaylandGestureHandler::unregisterGesture(const QString &id) { if (!m_bindings.contains(id)) return false; - m_bindings.removeAll(id); - m_wrapper->unbind(id); + m_wrapper->unbind(m_bindings.take(id)); return true; } bool WaylandGestureHandler::commit() { - m_wrapper->commitDeferred(); + m_wrapper->flushDeferred(); return true; } bool WaylandGestureHandler::commitSync() { - bool success = m_wrapper->commitAndWait(); + bool success = m_wrapper->syncAndWait(); if (!success) { qCWarning(logShortcut) << "WaylandGestureHandler::commitSync failed"; } @@ -90,8 +91,20 @@ void WaylandGestureHandler::onActivated(const QString &name, uint32_t flags) { Q_UNUSED(flags); // Forward the activation signal to GestureHandler - if (!m_bindings.contains(name)) return; + const QString id = m_bindings.key(name); + if (id.isEmpty()) return; qCDebug(logShortcut) << "WaylandGestureHandler::onActivated name:" << name << "flags:" << flags; - emit activated(name); + emit activated(id); +} + +void WaylandGestureHandler::onBindFailed(const QString &name, uint32_t error) +{ + Q_UNUSED(error); + const QString id = m_bindings.key(name); + if (id.isEmpty()) + return; + + m_bindings.remove(id); + emit registrationFailed(id); } diff --git a/src/plugin-qt/shortcut/src/backend/wayland/waylandgesturehandler.h b/src/plugin-qt/shortcut/src/backend/wayland/waylandgesturehandler.h index 43d663e6..4ef6ad71 100644 --- a/src/plugin-qt/shortcut/src/backend/wayland/waylandgesturehandler.h +++ b/src/plugin-qt/shortcut/src/backend/wayland/waylandgesturehandler.h @@ -6,6 +6,8 @@ #include "backend/abstractgesturehandler.h" +#include + class TreelandShortcutWrapper; class WaylandGestureHandler : public AbstractGestureHandler @@ -22,9 +24,11 @@ class WaylandGestureHandler : public AbstractGestureHandler private slots: void onActivated(const QString &name, uint32_t flags); + void onBindFailed(const QString &name, uint32_t error); private: TreelandShortcutWrapper *m_wrapper = nullptr; - QStringList m_bindings; // List of binding names + QMap m_bindings; // Config id -> unique wire name + quint64 m_bindingSerial = 0; }; diff --git a/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.cpp b/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.cpp index c13d83ff..1db75bd5 100644 --- a/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.cpp +++ b/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.cpp @@ -20,6 +20,7 @@ WaylandKeyHandler::WaylandKeyHandler(TreelandShortcutWrapper *wrapper, QObject * , m_wrapper(wrapper) { connect(m_wrapper, &TreelandShortcutWrapper::activated, this, &WaylandKeyHandler::onActivated); + connect(m_wrapper, &TreelandShortcutWrapper::bindFailed, this, &WaylandKeyHandler::onBindFailed); connect(m_wrapper, &TreelandShortcutWrapper::protocolInactive, this, [this]() { qCWarning(logShortcut) << "WaylandKeyHandler: Protocol inactive, clearing all bindings"; m_nameToId.clear(); @@ -31,9 +32,8 @@ WaylandKeyHandler::~WaylandKeyHandler() { } -// Lock keys (NumLock, CapsLock) are rejected by Treeland at commit time. -// Skip bindKey() for these so the bulk commit does not fail; the config -// remains visible on D-Bus and the physical keys keep working. +// Lock keys are handled by the compositor rather than registered shortcuts. +// Keep their configuration visible while leaving the physical keys to it. static bool isLockKey(const QString &hotkey) { static const QSet lockKeys = { "NumLock", "CapsLock", "Num_Lock", "Caps_Lock" }; @@ -44,9 +44,8 @@ bool WaylandKeyHandler::registerKey(const KeyConfig &config) { if (!m_wrapper) return false; - // Treeland rejects bare Qt::Key_Sleep and rolls back the entire commit. - // Reject it before queuing any bindings. Remove this compatibility guard - // once the supported Treeland versions accept Sleep. + // Bare Sleep is still rejected by Treeland. Return failure before queuing + // bindings so fixed hardware shortcuts can use their raw-keycode fallback. if (config.hotkeys.contains(QStringLiteral("Sleep"))) { qCInfo(logShortcut) << "WaylandKeyHandler: Sleep is not supported by Treeland:" << config.getId(); @@ -57,7 +56,7 @@ bool WaylandKeyHandler::registerKey(const KeyConfig &config) unregisterKey(config.getId()); } - int action = QtWayland::treeland_shortcut_manager_v2::action_notify; + int action = QtWayland::treeland_shortcut_manager_v3::action_notify; if (config.triggerType == static_cast(TriggerType::Action)) { const TriggerActionId actionId = config.triggerValue.isEmpty() ? TriggerActionId::Invalid @@ -83,7 +82,10 @@ bool WaylandKeyHandler::registerKey(const KeyConfig &config) continue; } - QString name = config.getId() + "_" + hotkey; + // A deferred failure from an older registration must not invalidate a + // replacement for the same config/hotkey after a session switch. + const QString name = config.getId() + "_" + hotkey + + "#" + QString::number(++m_bindingSerial); // Use keyEventFlags from config, default to key_release (0x2) int flags = config.keyEventFlags; @@ -119,13 +121,13 @@ bool WaylandKeyHandler::unregisterKey(const QString &id) bool WaylandKeyHandler::commit() { - m_wrapper->commitDeferred(); + m_wrapper->flushDeferred(); return true; } bool WaylandKeyHandler::commitSync() { - bool success = m_wrapper->commitAndWait(); + bool success = m_wrapper->syncAndWait(); if (!success) { qCWarning(logShortcut) << "WaylandKeyHandler::commitSync failed"; } @@ -146,6 +148,20 @@ void WaylandKeyHandler::onActivated(const QString &name, uint32_t flags) } } +void WaylandKeyHandler::onBindFailed(const QString &name, uint32_t error) +{ + Q_UNUSED(error); + const QString id = m_nameToId.value(name); + if (id.isEmpty()) + return; + + // A multi-hotkey config is registered as a unit by the service. V3 keeps + // its other successful binds, so explicitly remove those on failure. + unregisterKey(id); + m_wrapper->flushDeferred(); + emit registrationFailed(id); +} + // Wayland does not expose CapsLock / NumLock state to regular clients, and // the old org_kde_kwin_keystate dependency has been removed. These methods // remain part of the AbstractKeyHandler contract for the X11 backend; on diff --git a/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.h b/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.h index a0670ee5..a789ecf5 100644 --- a/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.h +++ b/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.h @@ -35,10 +35,12 @@ class WaylandKeyHandler : public AbstractKeyHandler private slots: void onActivated(const QString &name, uint32_t flags); + void onBindFailed(const QString &name, uint32_t error); private: TreelandShortcutWrapper *m_wrapper = nullptr; QMap m_nameToId; // Map binding name -> id QMap m_idToNames; // Map id -> list of binding names + quint64 m_bindingSerial = 0; }; diff --git a/src/plugin-qt/shortcut/src/core/gestureactioncatalog.cpp b/src/plugin-qt/shortcut/src/core/gestureactioncatalog.cpp index 66cd91f6..be942f6c 100644 --- a/src/plugin-qt/shortcut/src/core/gestureactioncatalog.cpp +++ b/src/plugin-qt/shortcut/src/core/gestureactioncatalog.cpp @@ -61,6 +61,8 @@ QList treelandActionIds(const GestureConfig &config) static const QList ThreeFingerActions = { GestureActionId::MaximizeWindow, GestureActionId::RestoreWindow, + GestureActionId::SplitWindowLeft, + GestureActionId::SplitWindowRight, GestureActionId::Disable, }; static const QList FourFingerActions = { @@ -94,9 +96,6 @@ QList treelandActionIds(const GestureConfig &config) else if (config.fingerCount == 4) result = FourFingerActions; - // TODO: expose SplitWindowLeft and SplitWindowRight after Treeland - // provides native compositor actions for them. Until then they must - // remain unregistered and absent from the control-center choices. return result; } diff --git a/src/plugin-qt/shortcut/src/core/gesturemanager.cpp b/src/plugin-qt/shortcut/src/core/gesturemanager.cpp index 637fb5dc..950dda0d 100644 --- a/src/plugin-qt/shortcut/src/core/gesturemanager.cpp +++ b/src/plugin-qt/shortcut/src/core/gesturemanager.cpp @@ -55,6 +55,8 @@ GestureManager::GestureManager(ConfigLoader *loader, ActionExecutor *executor, if (m_gestureHandler) { connect(m_gestureHandler, &AbstractGestureHandler::activated, this, &GestureManager::onGestureActivated); + connect(m_gestureHandler, &AbstractGestureHandler::registrationFailed, + this, &GestureManager::onGestureRegistrationFailed); connect(m_gestureHandler, &AbstractGestureHandler::availabilityChanged, this, &GestureManager::onHandlerAvailabilityChanged); } @@ -190,12 +192,11 @@ bool GestureManager::ModifyGesture(const QString &id, const QStringList &action) if (registered && !disabled) setGestureActive(newConfig); if (!registered) { - if (oldWasActive && registerGesture(oldConfig)) - setGestureActive(oldConfig); - m_gestureHandler->commitSync(); + restoreGesture(oldConfig, oldWasActive); return false; } if (!m_gestureHandler->commitSync()) { + restoreGesture(oldConfig, oldWasActive); return false; } @@ -204,11 +205,7 @@ bool GestureManager::ModifyGesture(const QString &id, const QStringList &action) if (!m_loader->updateValue(id, QStringLiteral("triggerValue"), QVariantList{int(newActionId)})) { m_configuredGestures.insert(id, oldConfig); - m_gestureHandler->unregisterGesture(id); - setGestureInactive(id); - if (oldWasActive && registerGesture(oldConfig)) - setGestureActive(oldConfig); - m_gestureHandler->commitSync(); + restoreGesture(oldConfig, oldWasActive); return false; } @@ -216,6 +213,28 @@ bool GestureManager::ModifyGesture(const QString &id, const QStringList &action) return true; } +void GestureManager::restoreGesture(const GestureConfig &config, bool wasActive) +{ + const QString id = config.getId(); + m_gestureHandler->unregisterGesture(id); + setGestureInactive(id); + if (wasActive && registerGesture(config)) + setGestureActive(config); + if (!m_gestureHandler->commitSync()) { + m_gestureHandler->unregisterGesture(id); + setGestureInactive(id); + m_gestureHandler->commit(); + qCWarning(logShortcut) << "GestureManager: failed to restore gesture:" << id; + } +} + +void GestureManager::onGestureRegistrationFailed(const QString &id) +{ + setGestureInactive(id); + qCWarning(logShortcut) << "GestureManager: gesture registration failed:" << id; + emit GestureInfosChanged(); +} + void GestureManager::onGestureConfigAdded(const GestureConfig &config) { m_configuredGestures.insert(config.getId(), config); diff --git a/src/plugin-qt/shortcut/src/core/gesturemanager.h b/src/plugin-qt/shortcut/src/core/gesturemanager.h index 43227eab..27784424 100644 --- a/src/plugin-qt/shortcut/src/core/gesturemanager.h +++ b/src/plugin-qt/shortcut/src/core/gesturemanager.h @@ -82,10 +82,12 @@ private slots: void onGestureConfigChanged(const GestureConfig &config); void onGestureConfigRemoved(const QString &id); void onGestureActivated(const QString &gestureId); + void onGestureRegistrationFailed(const QString &id); void onHandlerAvailabilityChanged(bool available); private: bool registerGesture(const GestureConfig &config); + void restoreGesture(const GestureConfig &config, bool wasActive); void setGestureActive(const GestureConfig &config); bool setGestureInactive(const QString &id); QString LookupConflictGesture(int gestureType, int fingerCount, int direction); diff --git a/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp b/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp index fb7e5050..9b1eae92 100644 --- a/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp +++ b/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp @@ -326,6 +326,8 @@ KeybindingManager::KeybindingManager(ConfigLoader *loader, ActionExecutor *execu // Connect signals from key handler connect(m_keyHandler, &AbstractKeyHandler::keyActivated, this, &KeybindingManager::onKeyActivated); + connect(m_keyHandler, &AbstractKeyHandler::registrationFailed, + this, &KeybindingManager::onKeyRegistrationFailed); connect(m_keyHandler, &AbstractKeyHandler::capsLockStateChanged, this, &KeybindingManager::updateCapsLockState); m_lastCapsLockState = GetCapsLockState(); @@ -591,10 +593,11 @@ bool KeybindingManager::ModifyHotkeys(const QString &id, const QStringList &newH } } - // Save old state for X11 rollback or persistence recovery. + // Keep the old bindings for compensation: Treeland v3 applies each request + // immediately, so a rejected replacement does not restore the old state. const QStringList oldHotkeys = config.hotkeys; - // Phase 1: queue unbind + rebind + // Phase 1: unbind + rebind unregisterShortcut(id); config.hotkeys = normalized; @@ -606,7 +609,7 @@ bool KeybindingManager::ModifyHotkeys(const QString &id, const QStringList &newH return false; } - // Phase 2: commit to compositor (sync — must know whether to roll back). + // Phase 2: wait for processing and check whether compensation is needed. if (!m_keyHandler->commitSync()) { qCWarning(logShortcut) << "Shortcut commit failed for ModifyHotkeys:" << id; unregisterShortcut(id); @@ -1522,6 +1525,14 @@ void KeybindingManager::unregisterShortcut(const QString &id) m_activeShortcutIds.remove(id); } +void KeybindingManager::onKeyRegistrationFailed(const QString &id) +{ + // Failure can arrive after a sync when an inactive session becomes active. + // Preserve the configured value, but remove all runtime channels for it. + unregisterShortcut(id); + qCWarning(logShortcut) << "KeybindingManager: shortcut registration failed:" << id; +} + QString KeybindingManager::lookupRuntimeConflict(const QString &hotkey, const QStringList &excludeIds) const { diff --git a/src/plugin-qt/shortcut/src/core/keybindingmanager.h b/src/plugin-qt/shortcut/src/core/keybindingmanager.h index 65c0ee9e..3e9ca879 100644 --- a/src/plugin-qt/shortcut/src/core/keybindingmanager.h +++ b/src/plugin-qt/shortcut/src/core/keybindingmanager.h @@ -101,9 +101,9 @@ public slots: Q_SCRIPTABLE bool BeginCapture(quint64 captureId, uint timeoutMs); Q_SCRIPTABLE void EndCapture(); - // Atomically swap the hotkeys of two shortcuts in a single compositor commit. + // Swap both shortcuts, restoring their old bindings if registration fails. Q_SCRIPTABLE bool SwapHotkeys(const QString &id1, const QString &id2); - // Remove newHotkey from conflictId and assign it to targetId in one commit. + // Reassign a conflicting hotkey, compensating runtime/persistence failures. Q_SCRIPTABLE bool ReplaceHotkey(const QString &targetId, const QString &newHotkey, const QString &conflictId); Q_SCRIPTABLE void ReloadConfigs(); Q_SCRIPTABLE void Reset(); @@ -131,6 +131,7 @@ private slots: void onKeyConfigChanged(const KeyConfig &config); void onConfigRemoved(const QString &id); void onKeyActivated(const QString &shortcutId); + void onKeyRegistrationFailed(const QString &shortcutId); void onSpecialKeyActivated(const QString &shortcutId); void onCaptureKeyEvent(bool pressed, const QString &keystroke); void onCaptureResult(quint64 captureId, uint result, const QString &keystroke); diff --git a/src/plugin-qt/shortcut/src/core/shortcutmanager.cpp b/src/plugin-qt/shortcut/src/core/shortcutmanager.cpp index dee71893..e855d0ea 100644 --- a/src/plugin-qt/shortcut/src/core/shortcutmanager.cpp +++ b/src/plugin-qt/shortcut/src/core/shortcutmanager.cpp @@ -146,11 +146,12 @@ void ShortcutManager::registerAll() } if (m_isWayland) { - bool success = m_treelandShortcutWrapper->commitAndWait(); + bool success = m_treelandShortcutWrapper->syncAndWait(); if (success) { - qCInfo(logShortcut) << "ShortcutManager: All shortcuts and gestures registered successfully"; + qCInfo(logShortcut) << "ShortcutManager: Shortcut and gesture requests processed;" + << "inactive-session bindings may be deferred"; } else { - qCWarning(logShortcut) << "ShortcutManager: Commit failed"; + qCWarning(logShortcut) << "ShortcutManager: Some bindings failed or synchronization did not complete"; } } } diff --git a/src/plugin-qt/shortcut/src/core/shortcutmanager.h b/src/plugin-qt/shortcut/src/core/shortcutmanager.h index 72273f0a..252f2288 100644 --- a/src/plugin-qt/shortcut/src/core/shortcutmanager.h +++ b/src/plugin-qt/shortcut/src/core/shortcutmanager.h @@ -24,7 +24,7 @@ class SessionActiveMonitor; * Responsibilities: * 1. Create and manage all sub-components * 2. Handle platform differences (Wayland/X11) - * 3. Coordinate protocol lifecycle, unified commit + * 3. Coordinate protocol lifecycle and request synchronization * 4. Register DBus services */ class ShortcutManager : public QObject diff --git a/src/plugin-qt/shortcut/src/core/triggeractioncatalog.cpp b/src/plugin-qt/shortcut/src/core/triggeractioncatalog.cpp index 631025bd..aa714443 100644 --- a/src/plugin-qt/shortcut/src/core/triggeractioncatalog.cpp +++ b/src/plugin-qt/shortcut/src/core/triggeractioncatalog.cpp @@ -4,6 +4,8 @@ #include "triggeractioncatalog.h" +#include "wayland-treeland-shortcut-manager-unstable-v3-client-protocol.h" + namespace { struct ActionBackendMapping { @@ -17,48 +19,72 @@ struct ActionBackendMapping { const ActionBackendMapping *mappingFor(TriggerActionId actionId) { static constexpr ActionBackendMapping mappings[] = { - {TriggerActionId::Disable, nullptr, 0}, - {TriggerActionId::Notify, nullptr, 1}, - {TriggerActionId::Workspace1, "switchToWorkspace1", 2}, - {TriggerActionId::Workspace2, "switchToWorkspace2", 3}, - {TriggerActionId::Workspace3, "switchToWorkspace3", 4}, - {TriggerActionId::Workspace4, "switchToWorkspace4", 5}, - {TriggerActionId::Workspace5, "switchToWorkspace5", 6}, - {TriggerActionId::Workspace6, "switchToWorkspace6", 7}, - {TriggerActionId::PreviousWorkspace, "switchToWorkspaceLeft", 8}, - {TriggerActionId::NextWorkspace, "switchToWorkspaceRight", 9}, - {TriggerActionId::ShowDesktop, "showDesktop", 10}, - {TriggerActionId::MaximizeWindow, "maximize", 11}, - {TriggerActionId::RestoreWindow, "unmaximize", 12}, - {TriggerActionId::MoveWindow, "beginMove", 13}, - {TriggerActionId::CloseWindow, "close", 14}, - {TriggerActionId::ShowWindowMenu, "activateWindowMenu", 15}, - {TriggerActionId::ShowMultitask, nullptr, 16}, - {TriggerActionId::HideMultitask, nullptr, 17}, - {TriggerActionId::ToggleMultitask, "previewWorkspace", 18}, - {TriggerActionId::ToggleFpsDisplay, nullptr, 19}, - {TriggerActionId::LockScreen, nullptr, 20}, - {TriggerActionId::ShutdownMenu, nullptr, 21}, - {TriggerActionId::Quit, nullptr, 22}, - {TriggerActionId::TaskSwitchEnter, nullptr, 23}, - {TriggerActionId::TaskSwitchNext, "switchApplications", 24}, - {TriggerActionId::TaskSwitchPrevious, "switchApplicationsBackward", 25}, - {TriggerActionId::TaskSwitchSameAppNext, "switchGroup", 26}, - {TriggerActionId::TaskSwitchSameAppPrevious, "switchGroupBackward", 27}, - {TriggerActionId::SplitWindowLeft, "toggleToLeft", -1}, - {TriggerActionId::SplitWindowRight, "toggleToRight", -1}, + {TriggerActionId::Disable, nullptr, -1}, + {TriggerActionId::Notify, nullptr, TREELAND_SHORTCUT_MANAGER_V3_ACTION_NOTIFY}, + {TriggerActionId::Workspace1, "switchToWorkspace1", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_WORKSPACE_1}, + {TriggerActionId::Workspace2, "switchToWorkspace2", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_WORKSPACE_2}, + {TriggerActionId::Workspace3, "switchToWorkspace3", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_WORKSPACE_3}, + {TriggerActionId::Workspace4, "switchToWorkspace4", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_WORKSPACE_4}, + {TriggerActionId::Workspace5, "switchToWorkspace5", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_WORKSPACE_5}, + {TriggerActionId::Workspace6, "switchToWorkspace6", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_WORKSPACE_6}, + {TriggerActionId::PreviousWorkspace, "switchToWorkspaceLeft", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_PREV_WORKSPACE}, + {TriggerActionId::NextWorkspace, "switchToWorkspaceRight", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_NEXT_WORKSPACE}, + {TriggerActionId::ShowDesktop, "showDesktop", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_SHOW_DESKTOP}, + {TriggerActionId::MaximizeWindow, "maximize", TREELAND_SHORTCUT_MANAGER_V3_ACTION_MAXIMIZE}, + {TriggerActionId::RestoreWindow, "unmaximize", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_CANCEL_MAXIMIZE}, + {TriggerActionId::MoveWindow, "beginMove", TREELAND_SHORTCUT_MANAGER_V3_ACTION_MOVE_WINDOW}, + {TriggerActionId::CloseWindow, "close", TREELAND_SHORTCUT_MANAGER_V3_ACTION_CLOSE_WINDOW}, + {TriggerActionId::ShowWindowMenu, "activateWindowMenu", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_SHOW_WINDOW_MENU}, + {TriggerActionId::ShowMultitask, nullptr, + TREELAND_SHORTCUT_MANAGER_V3_ACTION_OPEN_MULTITASK_VIEW}, + {TriggerActionId::HideMultitask, nullptr, + TREELAND_SHORTCUT_MANAGER_V3_ACTION_CLOSE_MULTITASK_VIEW}, + {TriggerActionId::ToggleMultitask, "previewWorkspace", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_TOGGLE_MULTITASK_VIEW}, + {TriggerActionId::ToggleFpsDisplay, nullptr, + TREELAND_SHORTCUT_MANAGER_V3_ACTION_TOGGLE_FPS_DISPLAY}, + {TriggerActionId::LockScreen, nullptr, TREELAND_SHORTCUT_MANAGER_V3_ACTION_LOCKSCREEN}, + {TriggerActionId::ShutdownMenu, nullptr, TREELAND_SHORTCUT_MANAGER_V3_ACTION_SHUTDOWN_MENU}, + {TriggerActionId::Quit, nullptr, -1}, + {TriggerActionId::TaskSwitchEnter, nullptr, -1}, + {TriggerActionId::TaskSwitchNext, "switchApplications", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_TASKSWITCH_NEXT}, + {TriggerActionId::TaskSwitchPrevious, "switchApplicationsBackward", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_TASKSWITCH_PREV}, + {TriggerActionId::TaskSwitchSameAppNext, "switchGroup", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_TASKSWITCH_SAMEAPP_NEXT}, + {TriggerActionId::TaskSwitchSameAppPrevious, "switchGroupBackward", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_TASKSWITCH_SAMEAPP_PREV}, + {TriggerActionId::SplitWindowLeft, "toggleToLeft", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_TILE_LEFT}, + {TriggerActionId::SplitWindowRight, "toggleToRight", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_TILE_RIGHT}, {TriggerActionId::HideDesktop, nullptr, -1}, {TriggerActionId::ToggleGrandSearch, nullptr, -1}, {TriggerActionId::ToggleLauncher, nullptr, -1}, {TriggerActionId::ToggleClipboard, nullptr, -1}, {TriggerActionId::ToggleNotifications, nullptr, -1}, - {TriggerActionId::MinimizeWindow, "minimize", -1}, + {TriggerActionId::MinimizeWindow, "minimize", TREELAND_SHORTCUT_MANAGER_V3_ACTION_MINIMIZE}, + // Defined by v3, but not implemented by Treeland's action executor yet. {TriggerActionId::ResizeWindow, "beginResize", -1}, {TriggerActionId::ZoomIn, "viewZoomIn", -1}, {TriggerActionId::ZoomOut, "viewZoomOut", -1}, {TriggerActionId::ZoomReset, "viewActualSize", -1}, - {TriggerActionId::MoveToLeftWorkspace, "moveToWorkspaceLeft", -1}, - {TriggerActionId::MoveToRightWorkspace, "moveToWorkspaceRight", -1}, + {TriggerActionId::MoveToLeftWorkspace, "moveToWorkspaceLeft", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_MOVE_WINDOW_TO_PREV_WORKSPACE}, + {TriggerActionId::MoveToRightWorkspace, "moveToWorkspaceRight", + TREELAND_SHORTCUT_MANAGER_V3_ACTION_MOVE_WINDOW_TO_NEXT_WORKSPACE}, }; for (const ActionBackendMapping &mapping : mappings) { diff --git a/src/plugin-qt/shortcut/src/core/triggeractioncatalog.h b/src/plugin-qt/shortcut/src/core/triggeractioncatalog.h index ef8cc2f3..1703f2d9 100644 --- a/src/plugin-qt/shortcut/src/core/triggeractioncatalog.h +++ b/src/plugin-qt/shortcut/src/core/triggeractioncatalog.h @@ -8,9 +8,8 @@ #include -// IDs used by action-triggered shortcut configurations. The numeric values -// are part of the public configuration format; values above the Treeland -// protocol range are service/X11 extensions. +// Stable IDs used by shortcut/gesture configurations and D-Bus. These values +// must not follow protocol enum renumbering; translate them at the backend. enum class TriggerActionId { Invalid = -1, Disable = 0, @@ -71,8 +70,8 @@ class TriggerActionCatalog // X11. An empty result means that the action is not a KWin shortcut. static QString x11WmShortcutId(TriggerActionId actionId); - // Returns the action value accepted by treeland-shortcut-manager-v2. - // Unsupported actions intentionally return no value: their configuration - // remains visible, but no invalid protocol request is sent. + // Returns the v3 wire value for actions implemented by Treeland. Disabled, + // removed and unimplemented actions return no value. In particular, the + // protocol's notify = 0 must not be confused with the configuration's Disable. static std::optional treelandActionId(TriggerActionId actionId); }; diff --git a/src/plugin-qt/shortcut/tests/CMakeLists.txt b/src/plugin-qt/shortcut/tests/CMakeLists.txt index 33fc83c5..f612b730 100644 --- a/src/plugin-qt/shortcut/tests/CMakeLists.txt +++ b/src/plugin-qt/shortcut/tests/CMakeLists.txt @@ -47,8 +47,11 @@ add_executable(tst-triggeractioncatalog target_include_directories(tst-triggeractioncatalog PRIVATE ../src + ${CMAKE_CURRENT_BINARY_DIR}/.. ) +add_dependencies(tst-triggeractioncatalog plugin-dde-shortcut) + target_link_libraries(tst-triggeractioncatalog PRIVATE Qt6::Core Qt6::Test @@ -64,8 +67,11 @@ add_executable(tst-gestureactioncatalog target_include_directories(tst-gestureactioncatalog PRIVATE ../src + ${CMAKE_CURRENT_BINARY_DIR}/.. ) +add_dependencies(tst-gestureactioncatalog plugin-dde-shortcut) + target_link_libraries(tst-gestureactioncatalog PRIVATE Qt6::Core Qt6::Test @@ -73,6 +79,57 @@ target_link_libraries(tst-gestureactioncatalog PRIVATE add_test(NAME shortcut-gestureactioncatalog COMMAND tst-gestureactioncatalog) +pkg_check_modules(WAYLAND_SERVER REQUIRED IMPORTED_TARGET wayland-server) +find_program(WAYLAND_SCANNER_EXECUTABLE NAMES wayland-scanner REQUIRED) +set(SHORTCUT_PROTOCOL_XML ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-unstable-v3.xml) +set(SHORTCUT_SERVER_HEADER ${CMAKE_CURRENT_BINARY_DIR}/wayland-treeland-shortcut-manager-v3-server-protocol.h) +set(SHORTCUT_TEST_PROTOCOL_CODE ${CMAKE_CURRENT_BINARY_DIR}/wayland-shortcut-test-protocol.c) +pkg_get_variable(WAYLAND_PROTOCOLS_DATA_DIR wayland-protocols pkgdatadir) +set(SHORTCUT_TEST_XDG_XML ${WAYLAND_PROTOCOLS_DATA_DIR}/stable/xdg-shell/xdg-shell.xml) +set(SHORTCUT_TEST_XDG_HEADER ${CMAKE_CURRENT_BINARY_DIR}/wayland-shortcut-test-xdg-shell-server-protocol.h) +set(SHORTCUT_TEST_XDG_CODE ${CMAKE_CURRENT_BINARY_DIR}/wayland-shortcut-test-xdg-shell-protocol.c) +add_custom_command( + OUTPUT ${SHORTCUT_SERVER_HEADER} ${SHORTCUT_TEST_PROTOCOL_CODE} + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} server-header ${SHORTCUT_PROTOCOL_XML} ${SHORTCUT_SERVER_HEADER} + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code ${SHORTCUT_PROTOCOL_XML} ${SHORTCUT_TEST_PROTOCOL_CODE} + DEPENDS ${SHORTCUT_PROTOCOL_XML} + VERBATIM +) +add_custom_command( + OUTPUT ${SHORTCUT_TEST_XDG_HEADER} ${SHORTCUT_TEST_XDG_CODE} + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} server-header ${SHORTCUT_TEST_XDG_XML} ${SHORTCUT_TEST_XDG_HEADER} + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code ${SHORTCUT_TEST_XDG_XML} ${SHORTCUT_TEST_XDG_CODE} + DEPENDS ${SHORTCUT_TEST_XDG_XML} + VERBATIM +) +add_executable(tst-waylandshortcuts + tst_waylandshortcuts.cpp + ${SHORTCUT_SERVER_HEADER} + ${SHORTCUT_TEST_PROTOCOL_CODE} + ${SHORTCUT_TEST_XDG_HEADER} + ${SHORTCUT_TEST_XDG_CODE} +) +target_include_directories(tst-waylandshortcuts PRIVATE + ../src + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_BINARY_DIR}/.. +) +target_link_libraries(tst-waylandshortcuts PRIVATE + plugin-dde-shortcut + Qt6::Core + Qt6::Gui + Qt6::Test + Qt6::DBus + Qt6::WaylandClient + Dtk6::Core + PkgConfig::WAYLAND_SERVER +) +find_program(DBUS_RUN_SESSION_EXECUTABLE NAMES dbus-run-session REQUIRED) +add_test(NAME shortcut-waylandshortcuts + COMMAND ${DBUS_RUN_SESSION_EXECUTABLE} -- $ +) +set_tests_properties(shortcut-waylandshortcuts PROPERTIES TIMEOUT 30) + add_executable(tst-qkeysequenceconverter tst_qkeysequenceconverter.cpp ../src/core/qkeysequenceconverter.cpp diff --git a/src/plugin-qt/shortcut/tests/tst_gestureactioncatalog.cpp b/src/plugin-qt/shortcut/tests/tst_gestureactioncatalog.cpp index 0ea50269..b433f5ab 100644 --- a/src/plugin-qt/shortcut/tests/tst_gestureactioncatalog.cpp +++ b/src/plugin-qt/shortcut/tests/tst_gestureactioncatalog.cpp @@ -31,6 +31,8 @@ void TestGestureActionCatalog::swipeActionsFollowFingerClassification() const QList treelandThreeFinger = { GestureActionId::MaximizeWindow, GestureActionId::RestoreWindow, + GestureActionId::SplitWindowLeft, + GestureActionId::SplitWindowRight, GestureActionId::Disable, }; const QList x11FourFinger = { @@ -149,9 +151,9 @@ void TestGestureActionCatalog::hidesUnsupportedTreelandActions() threeFingerSwipe.gestureType = int(GestureType::Swipe); threeFingerSwipe.fingerCount = 3; - QVERIFY(!GestureActionCatalog::find( + QVERIFY(GestureActionCatalog::find( threeFingerSwipe, GestureActionId::SplitWindowLeft, GestureBackend::Treeland)); - QVERIFY(!GestureActionCatalog::find( + QVERIFY(GestureActionCatalog::find( threeFingerSwipe, GestureActionId::SplitWindowRight, GestureBackend::Treeland)); QVERIFY(!GestureActionCatalog::find( threeFingerSwipe, GestureActionId::ToggleLauncher, GestureBackend::Treeland)); diff --git a/src/plugin-qt/shortcut/tests/tst_triggeractioncatalog.cpp b/src/plugin-qt/shortcut/tests/tst_triggeractioncatalog.cpp index 5fa64ac2..4c7ab4f3 100644 --- a/src/plugin-qt/shortcut/tests/tst_triggeractioncatalog.cpp +++ b/src/plugin-qt/shortcut/tests/tst_triggeractioncatalog.cpp @@ -13,6 +13,8 @@ class TestTriggerActionCatalog : public QObject private slots: void resolvesStableActionIds(); void mapsX11ActionsByLogicalId(); + void mapsStableIdsToTreelandV3_data(); + void mapsStableIdsToTreelandV3(); void rejectsUnsupportedTreelandExtensions(); }; @@ -39,10 +41,56 @@ void TestTriggerActionCatalog::mapsX11ActionsByLogicalId() void TestTriggerActionCatalog::rejectsUnsupportedTreelandExtensions() { - QCOMPARE(TriggerActionCatalog::treelandActionId(TriggerActionId::ShowWindowMenu), - std::optional(15)); + QVERIFY(!TriggerActionCatalog::treelandActionId(TriggerActionId::Disable)); + QVERIFY(!TriggerActionCatalog::treelandActionId(TriggerActionId::Quit)); + QVERIFY(!TriggerActionCatalog::treelandActionId(TriggerActionId::TaskSwitchEnter)); QVERIFY(!TriggerActionCatalog::treelandActionId(TriggerActionId::ResizeWindow)); - QVERIFY(!TriggerActionCatalog::treelandActionId(TriggerActionId::MoveToRightWorkspace)); + QVERIFY(!TriggerActionCatalog::treelandActionId(TriggerActionId::ZoomIn)); + QVERIFY(!TriggerActionCatalog::treelandActionId(TriggerActionId::ZoomOut)); + QVERIFY(!TriggerActionCatalog::treelandActionId(TriggerActionId::ZoomReset)); + QVERIFY(!TriggerActionCatalog::treelandActionId(TriggerActionId::ToggleLauncher)); + QVERIFY(!TriggerActionCatalog::treelandActionId(TriggerActionId::Invalid)); +} + +void TestTriggerActionCatalog::mapsStableIdsToTreelandV3_data() +{ + QTest::addColumn("configuredId"); + QTest::addColumn("wireValue"); + QTest::newRow("notify-is-zero") << QStringLiteral("1") << 0; + QTest::newRow("workspace-1") << QStringLiteral("2") << 1; + QTest::newRow("workspace-6") << QStringLiteral("7") << 6; + QTest::newRow("previous-workspace") << QStringLiteral("8") << 13; + QTest::newRow("next-workspace") << QStringLiteral("9") << 14; + QTest::newRow("desktop-is-not-workspace-10") << QStringLiteral("10") << 24; + QTest::newRow("maximize") << QStringLiteral("11") << 15; + QTest::newRow("restore") << QStringLiteral("12") << 16; + QTest::newRow("move") << QStringLiteral("13") << 18; + QTest::newRow("close") << QStringLiteral("14") << 20; + QTest::newRow("window-menu") << QStringLiteral("15") << 21; + QTest::newRow("multitask-open") << QStringLiteral("16") << 25; + QTest::newRow("multitask-close") << QStringLiteral("17") << 26; + QTest::newRow("multitask-toggle") << QStringLiteral("18") << 27; + QTest::newRow("fps") << QStringLiteral("19") << 43; + QTest::newRow("lock") << QStringLiteral("20") << 44; + QTest::newRow("shutdown-menu") << QStringLiteral("21") << 45; + QTest::newRow("task-next") << QStringLiteral("24") << 28; + QTest::newRow("task-previous") << QStringLiteral("25") << 29; + QTest::newRow("same-app-next") << QStringLiteral("26") << 30; + QTest::newRow("same-app-previous") << QStringLiteral("27") << 31; + QTest::newRow("tile-left") << QStringLiteral("100") << 32; + QTest::newRow("tile-right") << QStringLiteral("101") << 33; + QTest::newRow("minimize") << QStringLiteral("110") << 17; + QTest::newRow("move-to-left-workspace") << QStringLiteral("120") << 22; + QTest::newRow("move-to-right-workspace") << QStringLiteral("121") << 23; +} + +void TestTriggerActionCatalog::mapsStableIdsToTreelandV3() +{ + QFETCH(QString, configuredId); + QFETCH(int, wireValue); + const auto action = TriggerActionCatalog::resolve(configuredId); + QCOMPARE(TriggerActionCatalog::idString(action), configuredId); + QCOMPARE(TriggerActionCatalog::treelandActionId(action), std::optional(wireValue)); } QTEST_GUILESS_MAIN(TestTriggerActionCatalog) diff --git a/src/plugin-qt/shortcut/tests/tst_waylandshortcuts.cpp b/src/plugin-qt/shortcut/tests/tst_waylandshortcuts.cpp new file mode 100644 index 00000000..7f625be0 --- /dev/null +++ b/src/plugin-qt/shortcut/tests/tst_waylandshortcuts.cpp @@ -0,0 +1,443 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "backend/wayland/treelandshortcutwrapper.h" +#include "backend/wayland/waylandgesturehandler.h" +#include "backend/wayland/waylandkeyhandler.h" +#include "config/configloader.h" +#include "core/gesturemanager.h" +#include "core/keybindingmanager.h" +#include "core/sessionactivemonitor.h" +#include "core/translationmanager.h" +#include "wayland-treeland-shortcut-manager-v3-server-protocol.h" +#include "wayland-shortcut-test-xdg-shell-server-protocol.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Binding { + QString name; + QString key; + uint32_t flags = 0; + uint32_t action = 0; + uint32_t finger = 0; + uint32_t direction = 0; +}; + +// A private, windowless Wayland server. It exercises the real Qt platform and +// generated client bindings without acquiring the desktop's shortcut manager. +class ShortcutServer +{ +public: + ShortcutServer() + { + m_display = wl_display_create(); + int sockets[2]; + if (!m_display || socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) != 0) + qFatal("Cannot create the private Wayland connection"); + if (!wl_client_create(m_display, sockets[0])) + qFatal("Cannot create the test Wayland client"); + qputenv("WAYLAND_SOCKET", QByteArray::number(sockets[1])); + + wl_display_init_shm(m_display); + wl_global_create(m_display, &wl_compositor_interface, 4, nullptr, + [](wl_client *client, void *, uint32_t version, uint32_t id) { + // No surfaces are created by these windowless tests. + wl_resource_create(client, &wl_compositor_interface, version, id); + }); + wl_global_create(m_display, &xdg_wm_base_interface, 1, nullptr, + [](wl_client *client, void *, uint32_t version, uint32_t id) { + static const struct xdg_wm_base_interface shellRequests = { + [](wl_client *, wl_resource *resource) { wl_resource_destroy(resource); }, + nullptr, + nullptr, + [](wl_client *, wl_resource *, uint32_t) {}, + }; + auto *resource = wl_resource_create(client, &xdg_wm_base_interface, version, id); + wl_resource_set_implementation(resource, &shellRequests, nullptr, nullptr); + }); + wl_global_create(m_display, &treeland_shortcut_manager_v3_interface, 1, this, + [](wl_client *client, void *data, uint32_t version, uint32_t id) { + auto *resource = wl_resource_create(client, &treeland_shortcut_manager_v3_interface, + version, id); + wl_resource_set_implementation(resource, &requests, data, destroyResource); + }); + m_thread = std::thread([this] { + while (!m_stopping) { + wl_event_loop_dispatch(wl_display_get_event_loop(m_display), 5); + { + std::lock_guard lock(m_mutex); + if (m_releaseFailures) { + for (const QString &name : std::as_const(m_deferredFailures)) + sendFailure(name); + m_deferredFailures.clear(); + m_releaseFailures = false; + } + for (const QString &name : std::as_const(m_activations)) { + if (m_owner && m_bindings.contains(name)) { + treeland_shortcut_manager_v3_send_activated( + m_owner, name.toUtf8().constData(), 2); + } + } + m_activations.clear(); + } + wl_display_flush_clients(m_display); + } + }); + } + + ~ShortcutServer() + { + m_stopping = true; + m_thread.join(); + wl_display_destroy_clients(m_display); + wl_display_destroy(m_display); + } + + QMap bindings() + { + std::lock_guard lock(m_mutex); + return m_bindings; + } + + void rejectNext(bool defer = false) + { + std::lock_guard lock(m_mutex); + m_rejectNext = true; + m_deferNextFailure = defer; + } + + void releaseFailures() + { + std::lock_guard lock(m_mutex); + m_releaseFailures = true; + } + + void activate(const QString &name) + { + std::lock_guard lock(m_mutex); + m_activations.append(name); + } + +private: + static ShortcutServer *server(wl_resource *resource) + { + return static_cast(wl_resource_get_user_data(resource)); + } + + static void destroyResource(wl_resource *resource) + { + auto *self = server(resource); + std::lock_guard lock(self->m_mutex); + if (self->m_owner == resource) { + self->m_owner = nullptr; + self->m_deferredFailures.clear(); + } + } + + void sendFailure(const QString &name) + { + if (m_owner) { + treeland_shortcut_manager_v3_send_bind_failure( + m_owner, name.toUtf8().constData(), + TREELAND_SHORTCUT_MANAGER_V3_BIND_ERROR_INVALID_ARGUMENT); + } + } + + void bind(wl_resource *resource, const Binding &binding) + { + std::lock_guard lock(m_mutex); + if (m_owner != resource) { + wl_resource_post_error(resource, TREELAND_SHORTCUT_MANAGER_V3_ERROR_NOT_ACQUIRED, + "bind before acquire"); + return; + } + if (m_rejectNext) { + m_rejectNext = false; + if (m_deferNextFailure) + m_deferredFailures.append(binding.name); + else + sendFailure(binding.name); + return; + } + m_bindings.insert(binding.name, binding); + } + + static const struct treeland_shortcut_manager_v3_interface requests; + wl_display *m_display = nullptr; + wl_resource *m_owner = nullptr; + std::thread m_thread; + std::atomic_bool m_stopping = false; + std::mutex m_mutex; + QMap m_bindings; + QStringList m_deferredFailures; + QStringList m_activations; + bool m_rejectNext = false; + bool m_deferNextFailure = false; + bool m_releaseFailures = false; +}; + +const struct treeland_shortcut_manager_v3_interface ShortcutServer::requests = { + [](wl_client *, wl_resource *resource) { wl_resource_destroy(resource); }, + [](wl_client *, wl_resource *resource) { + auto *self = server(resource); + std::lock_guard lock(self->m_mutex); + if (self->m_owner) { + wl_resource_post_error(resource, TREELAND_SHORTCUT_MANAGER_V3_ERROR_OCCUPIED, + "manager already acquired"); + return; + } + self->m_owner = resource; + self->m_bindings.clear(); + }, + [](wl_client *, wl_resource *resource, const char *name, const char *key, + uint32_t flags, uint32_t action) { + server(resource)->bind(resource, {QString::fromUtf8(name), QString::fromUtf8(key), + flags, action, 0, 0}); + }, + [](wl_client *, wl_resource *resource, const char *name, uint32_t finger, + uint32_t direction, uint32_t action) { + server(resource)->bind(resource, {QString::fromUtf8(name), {}, 0, action, finger, direction}); + }, + [](wl_client *, wl_resource *resource, const char *name, uint32_t finger, uint32_t action) { + server(resource)->bind(resource, {QString::fromUtf8(name), {}, 0, action, finger, 0}); + }, + [](wl_client *, wl_resource *resource, const char *name) { + auto *self = server(resource); + std::lock_guard lock(self->m_mutex); + self->m_bindings.remove(QString::fromUtf8(name)); + }, + nullptr, // capture_next_shortcut is owned by the GUI client, not this service. +}; + +KeyConfig keyConfig(const QString &id, const QStringList &hotkeys) +{ + KeyConfig config; + config.appId = QStringLiteral("org.deepin.dde.keybinding"); + config.subPath = id; + config.displayName = id; + config.enabled = true; + config.modifiable = true; + config.triggerType = int(TriggerType::Command); + config.hotkeys = hotkeys; + return config; +} + +GestureConfig gestureConfig(const QString &id, int action) +{ + GestureConfig config; + config.appId = QStringLiteral("org.deepin.dde.keybinding"); + config.subPath = id; + config.displayName = id; + config.enabled = true; + config.modifiable = true; + config.triggerType = int(TriggerType::Action); + config.triggerValue = {QString::number(action)}; + config.gestureType = int(GestureType::Swipe); + config.fingerCount = 3; + config.direction = 3; + return config; +} + +} // namespace + +class TestWaylandShortcuts : public QObject +{ + Q_OBJECT +public: + explicit TestWaylandShortcuts(ShortcutServer &server) : m_server(server) {} + +private slots: + void init() + { + m_wrapper = std::make_unique(); + m_wrapper->initProtocol(); + QTRY_VERIFY(m_wrapper->isActive()); + QVERIFY(m_wrapper->syncAndWait()); + } + + void cleanup() + { + m_wrapper.reset(); + } + + void bindsAndActivatesWithoutCommit() + { + WaylandKeyHandler handler(m_wrapper.get()); + QSignalSpy activated(&handler, &AbstractKeyHandler::keyActivated); + QVERIFY(handler.registerKey(keyConfig("command", {"Ctrl+Alt+A"}))); + QVERIFY(handler.commitSync()); + const auto bindings = m_server.bindings(); + QCOMPARE(bindings.size(), 1); + QCOMPARE(bindings.first().action, 0U); + QCOMPARE(bindings.first().flags, 2U); + m_server.activate(bindings.firstKey()); + QTRY_COMPARE(activated.size(), 1); + QCOMPARE(activated.first().first().toString(), QStringLiteral("command")); + QVERIFY(handler.unregisterKey("command")); + QVERIFY(handler.commitSync()); + QVERIFY(m_server.bindings().isEmpty()); + } + + void mapsGestureBusinessIdsToWireValues() + { + WaylandGestureHandler handler(m_wrapper.get()); + QVERIFY(handler.registerGesture(gestureConfig("maximize", 11))); + QVERIFY(handler.registerGesture(gestureConfig("tile-left", 100))); + auto notify = gestureConfig("service", 1); + notify.gestureType = int(GestureType::Hold); + QVERIFY(handler.registerGesture(notify)); + QVERIFY(handler.commitSync()); + QList actions; + for (const Binding &binding : m_server.bindings()) + actions.append(binding.action); + std::sort(actions.begin(), actions.end()); + QCOMPARE(actions, QList({0, 15, 32})); + QVERIFY(!handler.registerGesture(gestureConfig("disabled", 0))); + } + + void remembersFailureReceivedBeforeSync() + { + QSignalSpy failed(m_wrapper.get(), &TreelandShortcutWrapper::bindFailed); + m_server.rejectNext(); + QVERIFY(m_wrapper->bindKey("rejected", "Ctrl+Alt+B", 2, 0)); + m_wrapper->flush(); + QTRY_COMPARE(failed.size(), 1); + QVERIFY(!m_wrapper->syncAndWait()); + QVERIFY(m_wrapper->bindKey("accepted", "Ctrl+Alt+C", 2, 0)); + QVERIFY(m_wrapper->syncAndWait()); + QCOMPARE(m_server.bindings().size(), 1); + } + + void failedConfigRemovesItsPartialBindings() + { + WaylandKeyHandler handler(m_wrapper.get()); + QSignalSpy failed(&handler, &AbstractKeyHandler::registrationFailed); + QVERIFY(handler.registerKey(keyConfig("unrelated", {"Ctrl+Alt+D"}))); + QVERIFY(handler.commitSync()); + m_server.rejectNext(); + QVERIFY(handler.registerKey(keyConfig("partial", {"Ctrl+Alt+E", "Ctrl+Alt+F"}))); + QVERIFY(!handler.commitSync()); + QCOMPARE(failed.size(), 1); + QCOMPARE(failed.first().first().toString(), QStringLiteral("partial")); + QVERIFY(handler.commitSync()); // Also wait for the compensating unbinds. + const auto bindings = m_server.bindings(); + QCOMPARE(bindings.size(), 1); + QCOMPARE(bindings.first().key, QStringLiteral("Ctrl+Alt+D")); + } + + void lateFailureInvalidatesOnlyItsConfig() + { + WaylandKeyHandler handler(m_wrapper.get()); + QSignalSpy failed(&handler, &AbstractKeyHandler::registrationFailed); + m_server.rejectNext(true); + QVERIFY(handler.registerKey(keyConfig("deferred", {"Ctrl+Alt+G"}))); + QVERIFY(handler.commitSync()); + m_server.releaseFailures(); + QTRY_COMPARE(failed.size(), 1); + QVERIFY(handler.registerKey(keyConfig("unrelated", {"Ctrl+Alt+H"}))); + QVERIFY(handler.commitSync()); + QCOMPARE(m_server.bindings().size(), 1); + } + + void lateFailureDoesNotInvalidateReplacement() + { + WaylandGestureHandler handler(m_wrapper.get()); + QSignalSpy failed(&handler, &AbstractGestureHandler::registrationFailed); + QSignalSpy activated(&handler, &AbstractGestureHandler::activated); + m_server.rejectNext(true); + QVERIFY(handler.registerGesture(gestureConfig("replaced", 11))); + QVERIFY(handler.commitSync()); + QVERIFY(handler.registerGesture(gestureConfig("replaced", 12))); + QVERIFY(handler.commitSync()); + QSignalSpy wireFailure(m_wrapper.get(), &TreelandShortcutWrapper::bindFailed); + m_server.releaseFailures(); + QTRY_COMPARE(wireFailure.size(), 1); + QCOMPARE(failed.size(), 0); + QVERIFY(handler.commitSync()); + const auto bindings = m_server.bindings(); + QCOMPARE(bindings.size(), 1); + QCOMPARE(bindings.first().action, 16U); + m_server.activate(bindings.firstKey()); + QTRY_COMPARE(activated.size(), 1); + QCOMPARE(activated.first().first().toString(), QStringLiteral("replaced")); + } + + void gestureModificationRestoresOldBindingAfterRejection() + { + ConfigLoader loader; + TranslationManager translations; + WaylandGestureHandler handler(m_wrapper.get()); + GestureManager manager(&loader, nullptr, &translations, &handler, nullptr, nullptr); + const GestureConfig original = gestureConfig("editable", 11); + emit loader.gestureConfigAdded(original); + QCOMPARE(m_server.bindings().size(), 1); + m_server.rejectNext(); + QVERIFY(!manager.ModifyGesture("editable", {"12"})); + const auto bindings = m_server.bindings(); + QCOMPARE(bindings.size(), 1); + QCOMPARE(bindings.first().action, 15U); + QCOMPARE(manager.ListAllGestures().first().triggerValue, original.triggerValue); + } + + void keyModificationRestoresOldBindingAfterRejection() + { + ConfigLoader loader; + TranslationManager translations; + SessionActiveMonitor session; + WaylandKeyHandler handler(m_wrapper.get()); + KeybindingManager manager(&loader, nullptr, &translations, &handler, &session); + const KeyConfig original = keyConfig("editable-key", {"Ctrl+Alt+I"}); + emit loader.keyConfigAdded(original); + QCOMPARE(m_server.bindings().size(), 1); + QSignalSpy failed(&handler, &AbstractKeyHandler::registrationFailed); + m_server.rejectNext(); + QVERIFY(!manager.ModifyHotkeys(original.getId(), {"Ctrl+Alt+J"})); + QCOMPARE(failed.size(), 1); + const auto bindings = m_server.bindings(); + QCOMPARE(bindings.size(), 1); + QCOMPARE(bindings.first().key, original.hotkeys.first()); + QCOMPARE(manager.GetShortcut(original.getId()).hotkeys, + QStringList({"I"})); + } + +private: + ShortcutServer &m_server; + std::unique_ptr m_wrapper; +}; + +int main(int argc, char **argv) +{ + QTemporaryDir runtime; + if (!runtime.isValid()) + return 1; + qputenv("XDG_RUNTIME_DIR", runtime.path().toUtf8()); + qputenv("XDG_CONFIG_HOME", runtime.filePath("config").toUtf8()); + qputenv("QT_QPA_PLATFORM", "wayland"); + qputenv("QT_QPA_PLATFORMTHEME", "generic"); + qputenv("QT_NO_XDG_DESKTOP_PORTAL", "1"); + qputenv("QT_WAYLAND_CLIENT_BUFFER_INTEGRATION", "shm"); + ShortcutServer server; + QGuiApplication app(argc, argv); + QStandardPaths::setTestModeEnabled(true); + TestWaylandShortcuts test(server); + return QTest::qExec(&test, argc, argv); +} + +#include "tst_waylandshortcuts.moc"