diff --git a/src/plugin-qt/shortcut/DEVELOPER_GUIDE.md b/src/plugin-qt/shortcut/DEVELOPER_GUIDE.md index 7e957779..549b1a2d 100644 --- a/src/plugin-qt/shortcut/DEVELOPER_GUIDE.md +++ b/src/plugin-qt/shortcut/DEVELOPER_GUIDE.md @@ -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` 为准。 diff --git a/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h b/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h index c28260c5..2fd7297e 100644 --- a/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h +++ b/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h @@ -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; @@ -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; @@ -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(); diff --git a/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.cpp b/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.cpp index 85ffc45c..73bdc4f4 100644 --- a/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.cpp +++ b/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.cpp @@ -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, @@ -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; @@ -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()) @@ -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(); @@ -658,17 +663,31 @@ void X11KeyHandler::handleXcbEvents() if (m_capture.active && responseType == XCB_KEY_PRESS) { const CapturedKey captured = captureKey( reinterpret_cast(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(event)); diff --git a/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.h b/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.h index ce535824..d5b6a783 100644 --- a/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.h +++ b/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.h @@ -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 @@ -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; diff --git a/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp b/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp index 53ef9ab3..0dffe8e9 100644 --- a/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp +++ b/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp @@ -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, @@ -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() @@ -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; diff --git a/src/plugin-qt/shortcut/src/core/keybindingmanager.h b/src/plugin-qt/shortcut/src/core/keybindingmanager.h index 5fff73b2..65c0ee9e 100644 --- a/src/plugin-qt/shortcut/src/core/keybindingmanager.h +++ b/src/plugin-qt/shortcut/src/core/keybindingmanager.h @@ -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. @@ -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); @@ -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(); diff --git a/src/plugin-qt/shortcut/tests/tst_x11grabresilientshortcuts.cpp b/src/plugin-qt/shortcut/tests/tst_x11grabresilientshortcuts.cpp index 8879757f..dbe1263e 100644 --- a/src/plugin-qt/shortcut/tests/tst_x11grabresilientshortcuts.cpp +++ b/src/plugin-qt/shortcut/tests/tst_x11grabresilientshortcuts.cpp @@ -31,6 +31,20 @@ void sendTestChord(Display *display) 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; @@ -54,6 +68,10 @@ class TestX11GrabResilientShortcuts : public QObject private slots: void legacyShortcutsActivateExactlyOnce(); void xcbFallbackDuringRecordRestart(); + void captureReportsExplicitResults(); + void activeCaptureRejectsReplacement(); + void captureReportsTimeout(); + void explicitEndCaptureDoesNotReportResult(); }; void TestX11GrabResilientShortcuts::legacyShortcutsActivateExactlyOnce() @@ -123,7 +141,7 @@ void TestX11GrabResilientShortcuts::xcbFallbackDuringRecordRestart() // 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); @@ -135,6 +153,111 @@ void TestX11GrabResilientShortcuts::xcbFallbackDuringRecordRestart() 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 result = resultSpy.takeFirst(); + QCOMPARE(result.at(0).toULongLong(), 10ULL); + QCOMPARE(result.at(1).toUInt(), + static_cast(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(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(AbstractKeyHandler::CaptureSuccess)); + QCOMPARE(result.at(2).toString(), QStringLiteral("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(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(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 result = resultSpy.takeFirst(); + QCOMPARE(result.at(0).toULongLong(), 20ULL); + QCOMPARE(result.at(1).toUInt(), + static_cast(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"