Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/plugin-qt/shortcut/DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1117,13 +1117,17 @@ iface.call("Reset");
| 分类元数据 | `ListCategories` |
| 修改 | `ModifyHotkeys`、`Disable`、`SwapHotkeys`、`ReplaceHotkey`、`Reset` |
| 自定义快捷键 | `AddCustomShortcut`、`ModifyCustomShortcut`、`DeleteCustomShortcut` |
| 录入 | `BeginCapture`、`EndCapture` 和 `KeyEvent` 信号 |
| 录入 | `BeginCapture`、`EndCapture`、`KeyEvent` 和 `CaptureFinished` 信号 |
| 手势 | `ListAllGestures`、`ModifyGesture` |

`category` 是提供方拥有的自由字符串,不是固定整数枚举。客户端通过
`ListCategories` 获取分类的显示名、顺序和 `isCustom` 标记,不应硬编码分类键。
`GestureInfo.availableActions` 是当前后端动作能力的来源。

新版 `BeginCapture(captureId, timeoutMs)` 接收客户端生成的 `captureId`,
`CaptureFinished` 会原样返回该标识,客户端只处理与当前录入请求匹配的结果。
为兼容旧客户端,服务仍保留 `BeginCapture(timeoutMs)` 签名;旧接口使用标识 `0`。

D-Bus 结构体必须在调用前注册 Qt 元类型。字段顺序和完整方法签名以
`src/core/keybindingmanager.h`、`src/core/gesturemanager.h` 为准。

Expand Down
12 changes: 11 additions & 1 deletion src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ class AbstractKeyHandler : public QObject
{
Q_OBJECT
public:
enum CaptureResult : uint {
CaptureSuccess = 0,
CaptureInvalid = 1,
CaptureCanceled = 2,
CaptureTimedOut = 3,
};
Q_ENUM(CaptureResult)

explicit AbstractKeyHandler(QObject *parent = nullptr) : QObject(parent) {}
virtual ~AbstractKeyHandler() = default;

Expand All @@ -26,8 +34,9 @@ class AbstractKeyHandler : public QObject
virtual bool commit() { return true; }
virtual bool commitSync() { return commit(); }

virtual bool beginCapture(uint timeoutMs, const QString &owner)
virtual bool beginCapture(quint64 captureId, uint timeoutMs, const QString &owner)
{
Q_UNUSED(captureId);
Q_UNUSED(timeoutMs);
Q_UNUSED(owner);
return false;
Expand All @@ -50,6 +59,7 @@ class AbstractKeyHandler : public QObject
void capsLockStateChanged(bool on);
void captureStarted();
void captureKeyEvent(bool pressed, const QString &keystroke);
void captureResult(quint64 captureId, uint result, const QString &keystroke);
void captureFinished();
void keymapAboutToChange();
void keymapChanged();
Expand Down
37 changes: 28 additions & 9 deletions src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,10 @@ X11KeyHandler::X11KeyHandler(QObject *parent)
m_capture.timer = new QTimer(this);
m_capture.ownerWatcher = new QDBusServiceWatcher(this);
m_capture.timer->setSingleShot(true);
connect(m_capture.timer, &QTimer::timeout, this, [this] { finishCapture(); });
connect(m_capture.timer, &QTimer::timeout, this, [this] {
emit captureResult(m_capture.id, CaptureTimedOut, QString());
finishCapture();
});
m_capture.ownerWatcher->setConnection(QDBusConnection::sessionBus());
m_capture.ownerWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
connect(m_capture.ownerWatcher, &QDBusServiceWatcher::serviceUnregistered,
Expand Down Expand Up @@ -208,14 +211,10 @@ bool X11KeyHandler::isAvailable() const
&& !xcb_connection_has_error(m_connection);
}

bool X11KeyHandler::beginCapture(uint timeoutMs, const QString &owner)
bool X11KeyHandler::beginCapture(quint64 captureId, uint timeoutMs, const QString &owner)
{
if (m_capture.active) {
if (!owner.isEmpty() && owner != m_capture.owner)
return false;
m_capture.timer->start(qBound(1000u, timeoutMs, 60000u));
return true;
}
if (m_capture.active)
return false;
if (!isAvailable())
return false;

Expand Down Expand Up @@ -260,7 +259,10 @@ bool X11KeyHandler::beginCapture(uint timeoutMs, const QString &owner)
m_recordPendingReleases.clear();
m_recordPressedBindings.clear();
m_recordObservedPresses.clear();
m_capture.id = captureId;
m_capture.keystroke.clear();
m_capture.candidateKeystroke.clear();
m_capture.candidateValid = false;
m_capture.owner = owner;
m_capture.active = true;
if (!m_capture.owner.isEmpty())
Expand Down Expand Up @@ -296,8 +298,11 @@ void X11KeyHandler::finishCapture(bool notify)
m_capture.timer->stop();
if (!m_capture.owner.isEmpty())
m_capture.ownerWatcher->removeWatchedService(m_capture.owner);
m_capture.id = 0;
m_capture.owner.clear();
m_capture.keystroke.clear();
m_capture.candidateKeystroke.clear();
m_capture.candidateValid = false;
m_capture.active = false;
if (m_modifierMonitor)
m_modifierMonitor->start();
Expand Down Expand Up @@ -658,17 +663,31 @@ void X11KeyHandler::handleXcbEvents()
if (m_capture.active && responseType == XCB_KEY_PRESS) {
const CapturedKey captured = captureKey(
reinterpret_cast<xcb_key_press_event_t *>(event));
m_capture.keystroke = isCapturedKeyValid(captured)
m_capture.candidateKeystroke = captured.keystroke;
m_capture.candidateValid = isCapturedKeyValid(captured);
m_capture.keystroke = m_capture.candidateValid
? captured.keystroke : QString();
emit captureKeyEvent(true, captured.keystroke);
} else if (m_capture.active && responseType == XCB_KEY_RELEASE) {
emit captureKeyEvent(false, m_capture.keystroke);
const bool canceled = m_capture.candidateKeystroke == QLatin1String("Escape")
|| m_capture.candidateKeystroke == QLatin1String("Esc");
if (canceled) {
emit captureResult(m_capture.id, CaptureCanceled, QString());
} else if (m_capture.candidateValid) {
emit captureResult(m_capture.id, CaptureSuccess, m_capture.keystroke);
} else {
emit captureResult(m_capture.id, CaptureInvalid, m_capture.candidateKeystroke);
}
finishCapture();
} else if (m_capture.active && responseType == XCB_BUTTON_PRESS) {
m_capture.keystroke.clear();
m_capture.candidateKeystroke.clear();
m_capture.candidateValid = false;
emit captureKeyEvent(true, QString());
} else if (m_capture.active && responseType == XCB_BUTTON_RELEASE) {
emit captureKeyEvent(false, QString());
emit captureResult(m_capture.id, CaptureCanceled, QString());
finishCapture();
} else if (responseType == XCB_KEY_PRESS) {
handleKeyPress(reinterpret_cast<xcb_key_press_event_t *>(event));
Expand Down
5 changes: 4 additions & 1 deletion src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class X11KeyHandler : public AbstractKeyHandler
bool registerKey(const KeyConfig &config) override;
bool unregisterKey(const QString &appId) override;
bool isAvailable() const override;
bool beginCapture(uint timeoutMs, const QString &owner) override;
bool beginCapture(quint64 captureId, uint timeoutMs, const QString &owner) override;
bool endCapture(const QString &owner) override;

// Lock key state operations
Expand Down Expand Up @@ -141,8 +141,11 @@ private slots:
struct CaptureState {
QTimer *timer = nullptr;
QDBusServiceWatcher *ownerWatcher = nullptr;
quint64 id = 0;
QString owner;
QString keystroke;
QString candidateKeystroke;
bool candidateValid = false;
bool active = false;
};
CaptureState m_capture;
Expand Down
14 changes: 13 additions & 1 deletion src/plugin-qt/shortcut/src/core/keybindingmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ KeybindingManager::KeybindingManager(ConfigLoader *loader, ActionExecutor *execu
this, [this] { m_specialKeyHandler->setEnabled(false); });
connect(m_keyHandler, &AbstractKeyHandler::captureKeyEvent,
this, &KeybindingManager::onCaptureKeyEvent);
connect(m_keyHandler, &AbstractKeyHandler::captureResult,
this, &KeybindingManager::onCaptureResult);
connect(m_keyHandler, &AbstractKeyHandler::captureFinished,
this, [this] { m_specialKeyHandler->setEnabled(true); });
connect(m_keyHandler, &AbstractKeyHandler::keymapAboutToChange,
Expand All @@ -357,12 +359,17 @@ KeybindingManager::KeybindingManager(ConfigLoader *loader, ActionExecutor *execu
}

bool KeybindingManager::BeginCapture(uint timeoutMs)
{
return BeginCapture(0, timeoutMs);
}

bool KeybindingManager::BeginCapture(quint64 captureId, uint timeoutMs)
{
if (m_isWayland)
return true;

const QString owner = calledFromDBus() ? message().service() : QString();
return m_keyHandler->beginCapture(timeoutMs, owner);
return m_keyHandler->beginCapture(captureId, timeoutMs, owner);
}

void KeybindingManager::EndCapture()
Expand Down Expand Up @@ -1359,6 +1366,11 @@ void KeybindingManager::onCaptureKeyEvent(bool pressed, const QString &keystroke
emit KeyEvent(pressed, keystroke);
}

void KeybindingManager::onCaptureResult(quint64 captureId, uint result, const QString &keystroke)
{
emit CaptureFinished(captureId, result, keystroke);
}

void KeybindingManager::updateNumLockState(bool on)
{
const uint state = on ? 1U : 0U;
Expand Down
3 changes: 3 additions & 0 deletions src/plugin-qt/shortcut/src/core/keybindingmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public slots:
const QString &expectedConflictId);
Q_SCRIPTABLE bool DeleteCustomShortcut(const QString &id);
Q_SCRIPTABLE bool BeginCapture(uint timeoutMs = 30000);
Q_SCRIPTABLE bool BeginCapture(quint64 captureId, uint timeoutMs);
Q_SCRIPTABLE void EndCapture();

// Atomically swap the hotkeys of two shortcuts in a single compositor commit.
Expand All @@ -119,6 +120,7 @@ public slots:
Q_SCRIPTABLE void ShortcutActivated(const QString &id, const QStringList &triggerValue);
Q_SCRIPTABLE void ShortcutRemoved(const QString &id);
Q_SCRIPTABLE void KeyEvent(bool pressed, const QString &keystroke);
Q_SCRIPTABLE void CaptureFinished(quint64 captureId, uint result, const QString &keystroke);

// Lock key state signals (0=off, 1=on)
Q_SCRIPTABLE void NumLockStateChanged(uint state);
Expand All @@ -131,6 +133,7 @@ private slots:
void onKeyActivated(const QString &shortcutId);
void onSpecialKeyActivated(const QString &shortcutId);
void onCaptureKeyEvent(bool pressed, const QString &keystroke);
void onCaptureResult(quint64 captureId, uint result, const QString &keystroke);
void updateNumLockState(bool on);
void updateCapsLockState(bool on);
void onBackendKeymapAboutToChange();
Expand Down
125 changes: 124 additions & 1 deletion src/plugin-qt/shortcut/tests/tst_x11grabresilientshortcuts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@
XFlush(display);
}

void sendKeyStroke(Display *display, KeySym keysym)
{
sendKey(display, keysym, true);
sendKey(display, keysym, false);
XFlush(display);
}

void clickPointer(Display *display)
{
XTestFakeButtonEvent(display, 1, True, CurrentTime);
XTestFakeButtonEvent(display, 1, False, CurrentTime);
XFlush(display);
}

KeyConfig shortcut(const QString &id, const QString &hotkey)
{
KeyConfig config;
Expand All @@ -51,9 +65,13 @@
{
Q_OBJECT

private slots:

Check warning on line 68 in src/plugin-qt/shortcut/tests/tst_x11grabresilientshortcuts.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

There is an unknown macro here somewhere. Configuration is required. If slots is a macro then please configure it.
void legacyShortcutsActivateExactlyOnce();
void xcbFallbackDuringRecordRestart();
void captureReportsExplicitResults();
void activeCaptureRejectsReplacement();
void captureReportsTimeout();
void explicitEndCaptureDoesNotReportResult();
};

void TestX11GrabResilientShortcuts::legacyShortcutsActivateExactlyOnce()
Expand Down Expand Up @@ -123,7 +141,7 @@

// Ending capture restarts RECORD asynchronously. A shortcut pressed in
// that interval must fall back to XCB and still activate exactly once.
QVERIFY(handler.beginCapture(30000, QStringLiteral("test-owner")));
QVERIFY(handler.beginCapture(1, 30000, QStringLiteral("test-owner")));
QVERIFY(handler.endCapture(QStringLiteral("test-owner")));
sendTestChord(display);
QTRY_COMPARE_WITH_TIMEOUT(activationSpy.size(), 1, 2000);
Expand All @@ -135,6 +153,111 @@
XCloseDisplay(display);
}

void TestX11GrabResilientShortcuts::captureReportsExplicitResults()
{
Display *display = XOpenDisplay(nullptr);
if (!display)
QSKIP("No X server is available");

X11KeyHandler handler;
if (!handler.isAvailable()) {
XCloseDisplay(display);
QSKIP("X11 shortcut backend is unavailable");
}

QSignalSpy resultSpy(&handler, &X11KeyHandler::captureResult);
QVERIFY(resultSpy.isValid());

QVERIFY(handler.beginCapture(10, 30000, QStringLiteral("test-owner")));
sendKeyStroke(display, XK_Shift_L);
QTRY_COMPARE_WITH_TIMEOUT(resultSpy.size(), 1, 2000);
QList<QVariant> result = resultSpy.takeFirst();
QCOMPARE(result.at(0).toULongLong(), 10ULL);
QCOMPARE(result.at(1).toUInt(),
static_cast<uint>(AbstractKeyHandler::CaptureInvalid));

QVERIFY(handler.beginCapture(11, 30000, QStringLiteral("test-owner")));
sendKeyStroke(display, XK_Escape);
QTRY_COMPARE_WITH_TIMEOUT(resultSpy.size(), 1, 2000);
result = resultSpy.takeFirst();
QCOMPARE(result.at(0).toULongLong(), 11ULL);
QCOMPARE(result.at(1).toUInt(),
static_cast<uint>(AbstractKeyHandler::CaptureCanceled));

QVERIFY(handler.beginCapture(12, 30000, QStringLiteral("test-owner")));
sendTestChord(display);
QTRY_COMPARE_WITH_TIMEOUT(resultSpy.size(), 1, 2000);
result = resultSpy.takeFirst();
QCOMPARE(result.at(0).toULongLong(), 12ULL);
QCOMPARE(result.at(1).toUInt(), static_cast<uint>(AbstractKeyHandler::CaptureSuccess));
QCOMPARE(result.at(2).toString(), QStringLiteral("<Control><Alt>B"));

QVERIFY(handler.beginCapture(13, 30000, QStringLiteral("test-owner")));
clickPointer(display);
QTRY_COMPARE_WITH_TIMEOUT(resultSpy.size(), 1, 2000);
result = resultSpy.takeFirst();
QCOMPARE(result.at(0).toULongLong(), 13ULL);
QCOMPARE(result.at(1).toUInt(),
static_cast<uint>(AbstractKeyHandler::CaptureCanceled));

XCloseDisplay(display);
}

void TestX11GrabResilientShortcuts::activeCaptureRejectsReplacement()
{
Display *display = XOpenDisplay(nullptr);
if (!display)
QSKIP("No X server is available");

X11KeyHandler handler;
if (!handler.isAvailable()) {
XCloseDisplay(display);
QSKIP("X11 shortcut backend is unavailable");
}

QSignalSpy resultSpy(&handler, &X11KeyHandler::captureResult);
QVERIFY(resultSpy.isValid());

const QString owner = QStringLiteral("test-owner");
QVERIFY(handler.beginCapture(14, 30000, owner));
QVERIFY(!handler.beginCapture(15, 30000, owner));
sendTestChord(display);
QTRY_COMPARE_WITH_TIMEOUT(resultSpy.size(), 1, 2000);
QCOMPARE(resultSpy.constFirst().at(0).toULongLong(), 14ULL);
QCOMPARE(resultSpy.constFirst().at(1).toUInt(),
static_cast<uint>(AbstractKeyHandler::CaptureSuccess));

XCloseDisplay(display);
}

void TestX11GrabResilientShortcuts::captureReportsTimeout()
{
X11KeyHandler handler;
if (!handler.isAvailable())
QSKIP("X11 shortcut backend is unavailable");

QSignalSpy resultSpy(&handler, &X11KeyHandler::captureResult);
QVERIFY(handler.beginCapture(20, 1, QStringLiteral("test-owner")));
QTRY_COMPARE_WITH_TIMEOUT(resultSpy.size(), 1, 2000);
const QList<QVariant> result = resultSpy.takeFirst();
QCOMPARE(result.at(0).toULongLong(), 20ULL);
QCOMPARE(result.at(1).toUInt(),
static_cast<uint>(AbstractKeyHandler::CaptureTimedOut));
}

void TestX11GrabResilientShortcuts::explicitEndCaptureDoesNotReportResult()
{
X11KeyHandler handler;
if (!handler.isAvailable())
QSKIP("X11 shortcut backend is unavailable");

QSignalSpy resultSpy(&handler, &X11KeyHandler::captureResult);
QVERIFY(handler.beginCapture(30, 30000, QStringLiteral("test-owner")));
QVERIFY(handler.endCapture(QStringLiteral("test-owner")));
QTest::qWait(50);
QCOMPARE(resultSpy.size(), 0);
}

QTEST_MAIN(TestX11GrabResilientShortcuts)

#include "tst_x11grabresilientshortcuts.moc"

Check warning on line 263 in src/plugin-qt/shortcut/tests/tst_x11grabresilientshortcuts.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "tst_x11grabresilientshortcuts.moc" not found.
Loading