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"