Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ Build-Depends: debhelper (>= 11),
libxcb-xtest0-dev,
libxtst-dev,
dbus-daemon <!nocheck>,
qt6-wayland <!nocheck>,
xauth <!nocheck>,
xvfb <!nocheck>,
libwayland-dev,
treeland-protocols (>> 0.5.7),
treeland-protocols (>= 0.6.0),
python3,
Standards-Version: 4.1.3
Homepage: https://github.com/linuxdeepin
Expand Down
10 changes: 5 additions & 5 deletions src/plugin-qt/shortcut/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
35 changes: 27 additions & 8 deletions src/plugin-qt/shortcut/DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 配置文件编写

快捷键服务使用两种配置文件协同工作:
Expand Down Expand Up @@ -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`)
Expand All @@ -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 调试工具

Expand Down Expand Up @@ -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` 和
Expand Down
4 changes: 3 additions & 1 deletion src/plugin-qt/shortcut/src/backend/abstractgesturehandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
9 changes: 5 additions & 4 deletions src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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(); }

Expand All @@ -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();
Expand Down
151 changes: 87 additions & 64 deletions src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,53 @@

#include "treelandshortcutwrapper.h"

#include <QDebug>

Check warning on line 9 in src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDebug> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QEventLoop>

Check warning on line 10 in src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QEventLoop> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QGuiApplication>

Check warning on line 11 in src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QGuiApplication> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QPointer>

Check warning on line 12 in src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QPointer> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QTimer>

Check warning on line 13 in src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QTimer> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QtCore/qnativeinterface.h>

Check warning on line 14 in src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtCore/qnativeinterface.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.

extern "C" {
#include <wayland-client-core.h>
}
#include <utility>

Check warning on line 16 in src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <utility> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <wayland-client.h>

Check warning on line 17 in src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <wayland-client.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.

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<QNativeInterface::QWaylandApplication>()) {
wl_display_flush(wlApp->display());
if (qGuiApp) {
if (auto *wlApp = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>())
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";
}
}
} // namespace

struct TreelandShortcutWrapper::PendingSync {
QEventLoop loop;
QSet<QString> bindings;
bool failed = false;
bool done = false;
};

TreelandShortcutWrapper::TreelandShortcutWrapper(QObject *parent)
: QWaylandClientExtensionTemplate<TreelandShortcutWrapper>(2)
, QtWayland::treeland_shortcut_manager_v2()
: QWaylandClientExtensionTemplate<TreelandShortcutWrapper>(1)
, QtWayland::treeland_shortcut_manager_v3()
{
setParent(parent);

Expand All @@ -61,6 +65,8 @@
} else {
qCWarning(logShortcut) << "TreelandShortcutManager protocol is now inactive!";
m_boundObject = nullptr;
m_pendingBindings.clear();
m_pendingBindFailed = false;
emit protocolInactive();
}
});
Expand All @@ -86,6 +92,7 @@
return false;
}

m_pendingBindings.insert(name);
bind_key(name, key, flags, action);
return true;
}
Expand All @@ -96,6 +103,7 @@
return false;
}

m_pendingBindings.insert(name);
bind_swipe_gesture(name, finger, direction, action);
return true;
}
Expand All @@ -106,6 +114,7 @@
return false;
}

m_pendingBindings.insert(name);
bind_hold_gesture(name, finger, action);
return true;
}
Expand All @@ -122,82 +131,96 @@
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()

Check warning on line 149 in src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'flushDeferred' is never used.
{
// 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<PendingSync *>(data);
pending->done = true;
pending->loop.quit();
}
};
wl_callback_add_listener(callback, &listener, &sync);

QPointer<TreelandShortcutWrapper> 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)

Check warning on line 198 in src/plugin-qt/shortcut/src/backend/wayland/treelandshortcutwrapper.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Condition '!guard' is always false
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);
}
Loading
Loading