From c1550ebb73372ee25333aa3c059b2b2da6ad59da Mon Sep 17 00:00:00 2001 From: zhaoyingzhen Date: Mon, 12 Oct 2026 15:59:49 +0800 Subject: [PATCH] fix: restore hardware sleep shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register Sleep and Suspend as independent fixed hardware shortcuts that invoke dde-shortcut-tool power suspend, so an unavailable X11 keysym does not disable the other binding. Accept Qt::Key_Sleep in shared validation and check suspend availability before locking the session. Reject bare Sleep before queuing Wayland bindings while the current Treeland validator does not support it, preserving the startup shortcut and gesture batch. Add converter checks and Xvfb regression coverage for keymaps containing either sleep variant, both, or neither. 将 Sleep 和 Suspend 注册为独立的固定硬件快捷键,统一调用 dde-shortcut-tool power suspend,避免 X11 缺少一种按键符号时 连带禁用另一种绑定。公共校验接受 Qt::Key_Sleep,并在锁屏前 检查待机能力。 当前 Treeland 校验尚不支持无修饰的 Sleep,因此在 Wayland 请求 入队前拒绝该绑定,保护启动时的快捷键和手势批次。补充按键转换 测试及 Xvfb 回归测试,覆盖仅有一种睡眠键、两者都有和都没有的映射。 Log: restore hardware sleep shortcuts Pms: BUG-378959 Change-Id: I662ac9d99245210ddf0b5b5ee9b7de7ccb0b9a60 --- .../configs/org.deepin.dde.keybinding.ini | 2 + .../org.deepin.shortcut.json | 92 +++++++++++++ .../org.deepin.shortcut.json | 92 +++++++++++++ .../src/backend/wayland/waylandkeyhandler.cpp | 9 ++ .../src/core/qkeysequenceconverter.cpp | 5 +- .../tests/tst_qkeysequenceconverter.cpp | 31 ++++- .../tests/tst_x11grabresilientshortcuts.cpp | 124 ++++++++++++++++++ .../dde-shortcut-tool/powercontroller.cpp | 15 ++- 8 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.shortcut.sleep/org.deepin.shortcut.json create mode 100644 src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.shortcut.suspend/org.deepin.shortcut.json diff --git a/src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.ini b/src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.ini index 21892c8c..d8cd092e 100644 --- a/src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.ini +++ b/src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.ini @@ -30,7 +30,9 @@ org.deepin.dde.keybinding.shortcut.music,\ org.deepin.dde.keybinding.shortcut.numlock,\ org.deepin.dde.keybinding.shortcut.pictures,\ org.deepin.dde.keybinding.shortcut.power,\ +org.deepin.dde.keybinding.shortcut.sleep,\ org.deepin.dde.keybinding.shortcut.speechtotext,\ +org.deepin.dde.keybinding.shortcut.suspend,\ org.deepin.dde.keybinding.shortcut.texttospeech,\ org.deepin.dde.keybinding.shortcut.tools,\ org.deepin.dde.keybinding.shortcut.touchpadoff,\ diff --git a/src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.shortcut.sleep/org.deepin.shortcut.json b/src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.shortcut.sleep/org.deepin.shortcut.json new file mode 100644 index 00000000..7b0b5c85 --- /dev/null +++ b/src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.shortcut.sleep/org.deepin.shortcut.json @@ -0,0 +1,92 @@ +{ + "magic": "dsg.config.meta", + "version": "1.0", + "contents": { + "appId": { + "value": "org.deepin.dde.keybinding", + "serial": 0, + "flags": [], + "name": "appId", + "name[zh_CN]": "提供快捷键应用Id", + "description": "provide shortcut app id", + "permissions": "", + "visibility": "private" + }, + "displayName": { + "value": "Sleep", + "serial": 0, + "flags": [], + "name": "displayName", + "name[zh_CN]": "睡眠", + "description": "睡眠快捷键", + "permissions": "", + "visibility": "private" + }, + "hotkeys": { + "value": [ + "Sleep" + ], + "serial": 0, + "flags": [], + "name": "hotkeys", + "name[zh_CN]": "快捷键组合", + "description": "快捷键组合", + "permissions": "", + "visibility": "private" + }, + "triggerType": { + "value": 1, + "serial": 0, + "flags": [], + "name": "triggerType", + "name[zh_CN]": "快捷键触发动作类型", + "description": "appId类型", + "permissions": "", + "visibility": "private" + }, + "triggerValue": { + "value": [ + "/usr/bin/dde-shortcut-tool", + "power", + "suspend" + ], + "serial": 0, + "flags": [], + "name": "triggerValue", + "name[zh_CN]": "快捷键触发动作", + "description": "触发动作", + "permissions": "", + "visibility": "private" + }, + "category": { + "value": "System", + "serial": 1, + "flags": [], + "name": "category", + "name[zh_CN]": "快捷键类别(System)", + "description": "System shortcut category", + "permissions": "", + "visibility": "private" + }, + "enabled": { + "value": true, + "serial": 0, + "flags": [], + "name": "enabled", + "name[zh_CN]": "使能", + "description": "是否启用快捷键", + "permissions": "", + "visibility": "private" + }, + "modifiable": { + "value": false, + "serial": 0, + "flags": [], + "name": "modifiable", + "name[zh_CN]": "能否修改快捷键", + "description": "不能修改快捷键", + "permissions": "", + "visibility": "private" + } + } +} diff --git a/src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.shortcut.suspend/org.deepin.shortcut.json b/src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.shortcut.suspend/org.deepin.shortcut.json new file mode 100644 index 00000000..1128718d --- /dev/null +++ b/src/plugin-qt/shortcut/configs/org.deepin.dde.keybinding.shortcut.suspend/org.deepin.shortcut.json @@ -0,0 +1,92 @@ +{ + "magic": "dsg.config.meta", + "version": "1.0", + "contents": { + "appId": { + "value": "org.deepin.dde.keybinding", + "serial": 0, + "flags": [], + "name": "appId", + "name[zh_CN]": "提供快捷键应用Id", + "description": "provide shortcut app id", + "permissions": "", + "visibility": "private" + }, + "displayName": { + "value": "Suspend", + "serial": 0, + "flags": [], + "name": "displayName", + "name[zh_CN]": "睡眠", + "description": "睡眠快捷键", + "permissions": "", + "visibility": "private" + }, + "hotkeys": { + "value": [ + "Suspend" + ], + "serial": 0, + "flags": [], + "name": "hotkeys", + "name[zh_CN]": "快捷键组合", + "description": "快捷键组合", + "permissions": "", + "visibility": "private" + }, + "triggerType": { + "value": 1, + "serial": 0, + "flags": [], + "name": "triggerType", + "name[zh_CN]": "快捷键触发动作类型", + "description": "appId类型", + "permissions": "", + "visibility": "private" + }, + "triggerValue": { + "value": [ + "/usr/bin/dde-shortcut-tool", + "power", + "suspend" + ], + "serial": 0, + "flags": [], + "name": "triggerValue", + "name[zh_CN]": "快捷键触发动作", + "description": "触发动作", + "permissions": "", + "visibility": "private" + }, + "category": { + "value": "System", + "serial": 1, + "flags": [], + "name": "category", + "name[zh_CN]": "快捷键类别(System)", + "description": "System shortcut category", + "permissions": "", + "visibility": "private" + }, + "enabled": { + "value": true, + "serial": 0, + "flags": [], + "name": "enabled", + "name[zh_CN]": "使能", + "description": "是否启用快捷键", + "permissions": "", + "visibility": "private" + }, + "modifiable": { + "value": false, + "serial": 0, + "flags": [], + "name": "modifiable", + "name[zh_CN]": "能否修改快捷键", + "description": "不能修改快捷键", + "permissions": "", + "visibility": "private" + } + } +} diff --git a/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.cpp b/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.cpp index e8bfd9f2..c13d83ff 100644 --- a/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.cpp +++ b/src/plugin-qt/shortcut/src/backend/wayland/waylandkeyhandler.cpp @@ -44,6 +44,15 @@ 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. + if (config.hotkeys.contains(QStringLiteral("Sleep"))) { + qCInfo(logShortcut) << "WaylandKeyHandler: Sleep is not supported by Treeland:" + << config.getId(); + return false; + } + if (m_idToNames.contains(config.getId())) { unregisterKey(config.getId()); } diff --git a/src/plugin-qt/shortcut/src/core/qkeysequenceconverter.cpp b/src/plugin-qt/shortcut/src/core/qkeysequenceconverter.cpp index 591c4ebd..9934ba3e 100644 --- a/src/plugin-qt/shortcut/src/core/qkeysequenceconverter.cpp +++ b/src/plugin-qt/shortcut/src/core/qkeysequenceconverter.cpp @@ -246,7 +246,10 @@ bool QKeySequenceConverter::isMultimediaKey(Qt::Key key) { return (key >= Qt::Key_Back && key <= Qt::Key_Calculator) || (key >= Qt::Key_AudioRewind && key <= Qt::Key_MediaLast) || - (key >= Qt::Key_KeyboardLightOnOff && key <= Qt::Key_KeyboardBrightnessUp); + (key >= Qt::Key_KeyboardLightOnOff && key <= Qt::Key_KeyboardBrightnessUp) || + // Qt places Key_Sleep outside the multimedia ranges (0x01020004), + // but hardware sleep keys (XF86Sleep) must still validate as hotkeys. + key == Qt::Key_Sleep; } QString QKeySequenceConverter::multimediaKeyToQKeySequenceName(Qt::Key key) diff --git a/src/plugin-qt/shortcut/tests/tst_qkeysequenceconverter.cpp b/src/plugin-qt/shortcut/tests/tst_qkeysequenceconverter.cpp index 9e40c416..d699e914 100644 --- a/src/plugin-qt/shortcut/tests/tst_qkeysequenceconverter.cpp +++ b/src/plugin-qt/shortcut/tests/tst_qkeysequenceconverter.cpp @@ -13,6 +13,8 @@ class TestQKeySequenceConverter : public QObject private slots: void symbolKeysUseXkbNames_data(); void symbolKeysUseXkbNames(); + void multimediaKeysAreRecognized_data(); + void multimediaKeysAreRecognized(); }; void TestQKeySequenceConverter::symbolKeysUseXkbNames_data() @@ -29,7 +31,11 @@ void TestQKeySequenceConverter::symbolKeysUseXkbNames_data() QTest::newRow("display") << QStringLiteral("Display") << QStringLiteral("XF86Display"); QTest::newRow("webcam") << QStringLiteral("WebCam") - << QStringLiteral("XF86WebCam"); + << QStringLiteral("XF86WebCam"); + QTest::newRow("sleep") << QStringLiteral("Sleep") + << QStringLiteral("XF86Sleep"); + QTest::newRow("suspend") << QStringLiteral("Suspend") + << QStringLiteral("XF86Suspend"); } void TestQKeySequenceConverter::symbolKeysUseXkbNames() @@ -41,6 +47,29 @@ void TestQKeySequenceConverter::symbolKeysUseXkbNames() QCOMPARE(QKeySequenceConverter::xkbToQKeySequence(xkbSequence), qtSequence); } +void TestQKeySequenceConverter::multimediaKeysAreRecognized_data() +{ + QTest::addColumn("qtKey"); + QTest::addColumn("expected"); + + // Regression: Qt::Key_Sleep (0x01020004) sits outside the classic + // multimedia ranges but hardware sleep keys (XF86Sleep) must validate, + // otherwise KeybindingManager::registerShortcut() drops the binding. + QTest::newRow("sleep") << static_cast(Qt::Key_Sleep) << true; + QTest::newRow("suspend") << static_cast(Qt::Key_Suspend) << true; + QTest::newRow("launch-mail") << static_cast(Qt::Key_LaunchMail) << true; + QTest::newRow("mic-mute") << static_cast(Qt::Key_MicMute) << true; + QTest::newRow("plain-a-is-not-multimedia") << static_cast(Qt::Key_A) << false; +} + +void TestQKeySequenceConverter::multimediaKeysAreRecognized() +{ + QFETCH(int, qtKey); + QFETCH(bool, expected); + + QCOMPARE(QKeySequenceConverter::isMultimediaKey(static_cast(qtKey)), expected); +} + QTEST_GUILESS_MAIN(TestQKeySequenceConverter) #include "tst_qkeysequenceconverter.moc" diff --git a/src/plugin-qt/shortcut/tests/tst_x11grabresilientshortcuts.cpp b/src/plugin-qt/shortcut/tests/tst_x11grabresilientshortcuts.cpp index dbe1263e..d0ccf0ed 100644 --- a/src/plugin-qt/shortcut/tests/tst_x11grabresilientshortcuts.cpp +++ b/src/plugin-qt/shortcut/tests/tst_x11grabresilientshortcuts.cpp @@ -4,13 +4,23 @@ #include "backend/x11/x11keyhandler.h" +#include +#include +#include +#include +#include #include #include +#include #include #include #include +#include +#include +#include + namespace { void sendKey(Display *display, KeySym keysym, bool pressed) @@ -72,6 +82,8 @@ private slots: void activeCaptureRejectsReplacement(); void captureReportsTimeout(); void explicitEndCaptureDoesNotReportResult(); + void sleepVariantsRegisterIndependently_data(); + void sleepVariantsRegisterIndependently(); }; void TestX11GrabResilientShortcuts::legacyShortcutsActivateExactlyOnce() @@ -258,6 +270,118 @@ void TestX11GrabResilientShortcuts::explicitEndCaptureDoesNotReportResult() QCOMPARE(resultSpy.size(), 0); } +void TestX11GrabResilientShortcuts::sleepVariantsRegisterIndependently_data() +{ + QTest::addColumn("hasSleep"); + QTest::addColumn("hasSuspend"); + + QTest::newRow("sleep-only") << true << false; + QTest::newRow("suspend-only") << false << true; + QTest::newRow("both") << true << true; + QTest::newRow("neither") << false << false; +} + +void TestX11GrabResilientShortcuts::sleepVariantsRegisterIndependently() +{ + QFETCH(bool, hasSleep); + QFETCH(bool, hasSuspend); + + const std::unique_ptr display(XOpenDisplay(nullptr), + &XCloseDisplay); + if (!display) + QSKIP("No X server is available"); + + int firstKeycode = 0; + int lastKeycode = 0; + XDisplayKeycodes(display.get(), &firstKeycode, &lastKeycode); + const int keycodeCount = lastKeycode - firstKeycode + 1; + int keysymsPerKeycode = 0; + const std::unique_ptr original( + XGetKeyboardMapping(display.get(), firstKeycode, keycodeCount, &keysymsPerKeycode), + &XFree); + QVERIFY(original); + QVERIFY(keycodeCount >= 2); + QVERIFY(keysymsPerKeycode > 0); + + // CTest runs this executable under Xvfb. Restore its map even if an assertion fails. + const auto restoreKeymap = qScopeGuard([&] { + XChangeKeyboardMapping(display.get(), firstKeycode, keysymsPerKeycode, original.get(), + keycodeCount); + XSync(display.get(), False); + }); + std::vector keymap(original.get(), + original.get() + keycodeCount * keysymsPerKeycode); + std::replace(keymap.begin(), keymap.end(), KeySym(XF86XK_Sleep), KeySym(NoSymbol)); + std::replace(keymap.begin(), keymap.end(), KeySym(XF86XK_Suspend), KeySym(NoSymbol)); + const KeyCode sleepKeycode = lastKeycode - 1; + const KeyCode suspendKeycode = lastKeycode; + const int sleepOffset = (sleepKeycode - firstKeycode) * keysymsPerKeycode; + const int suspendOffset = (suspendKeycode - firstKeycode) * keysymsPerKeycode; + std::fill(keymap.begin() + sleepOffset, keymap.end(), NoSymbol); + keymap[sleepOffset] = hasSleep ? XF86XK_Sleep : NoSymbol; + keymap[suspendOffset] = hasSuspend ? XF86XK_Suspend : NoSymbol; + XChangeKeyboardMapping(display.get(), firstKeycode, keysymsPerKeycode, keymap.data(), + keycodeCount); + XSync(display.get(), False); + + X11KeyHandler handler; + QVERIFY(handler.isAvailable()); + QSignalSpy activationSpy(&handler, &X11KeyHandler::keyActivated); + QVERIFY(activationSpy.isValid()); + const QString unrelatedId = QStringLiteral("test.unrelated"); + QVERIFY(handler.registerKey(shortcut(unrelatedId, QStringLiteral("Ctrl+Alt+B")))); + + // Read the shipped configs so combining the variants again reproduces the failure. + const QString iniPath = QFINDTESTDATA("../configs/org.deepin.dde.keybinding.ini"); + QVERIFY(!iniPath.isEmpty()); + const QString configDir = QFileInfo(iniPath).absolutePath(); + const QString prefix = QStringLiteral("org.deepin.dde.keybinding.shortcut."); + const QStringList names{QStringLiteral("sleep"), QStringLiteral("suspend")}; + for (const QString &name : names) { + QFile file(configDir + QLatin1Char('/') + prefix + name + + QStringLiteral("/org.deepin.shortcut.json")); + QVERIFY(file.open(QIODevice::ReadOnly)); + QJsonParseError error; + const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &error); + QCOMPARE(error.error, QJsonParseError::NoError); + const QJsonObject contents = document.object().value("contents").toObject(); + const auto value = [&contents](const QString &key) { + return contents.value(key).toObject().value("value").toVariant(); + }; + KeyConfig config = shortcut(prefix + name, QString()); + config.hotkeys = value("hotkeys").toStringList(); + config.triggerType = value("triggerType").toInt(); + config.triggerValue = value("triggerValue").toStringList(); + QCOMPARE(config.triggerType, static_cast(TriggerType::Command)); + QCOMPARE(config.triggerValue, (QStringList{QStringLiteral("/usr/bin/dde-shortcut-tool"), + QStringLiteral("power"), + QStringLiteral("suspend")})); + QCOMPARE(handler.registerKey(config), + name == QLatin1String("sleep") ? hasSleep : hasSuspend); + } + + // Observe backend activation only; no executor is connected, so the test never suspends. + if (hasSleep) { + XTestFakeKeyEvent(display.get(), sleepKeycode, True, CurrentTime); + XTestFakeKeyEvent(display.get(), sleepKeycode, False, CurrentTime); + XFlush(display.get()); + QTRY_COMPARE_WITH_TIMEOUT(activationSpy.size(), 1, 2000); + QCOMPARE(activationSpy.takeFirst().constFirst().toString(), + prefix + QStringLiteral("sleep")); + } + if (hasSuspend) { + XTestFakeKeyEvent(display.get(), suspendKeycode, True, CurrentTime); + XTestFakeKeyEvent(display.get(), suspendKeycode, False, CurrentTime); + XFlush(display.get()); + QTRY_COMPARE_WITH_TIMEOUT(activationSpy.size(), 1, 2000); + QCOMPARE(activationSpy.takeFirst().constFirst().toString(), + prefix + QStringLiteral("suspend")); + } + sendTestChord(display.get()); + QTRY_COMPARE_WITH_TIMEOUT(activationSpy.size(), 1, 2000); + QCOMPARE(activationSpy.constFirst().constFirst().toString(), unrelatedId); +} + QTEST_MAIN(TestX11GrabResilientShortcuts) #include "tst_x11grabresilientshortcuts.moc" diff --git a/src/plugin-qt/shortcut/tools/dde-shortcut-tool/powercontroller.cpp b/src/plugin-qt/shortcut/tools/dde-shortcut-tool/powercontroller.cpp index b465409b..40305764 100644 --- a/src/plugin-qt/shortcut/tools/dde-shortcut-tool/powercontroller.cpp +++ b/src/plugin-qt/shortcut/tools/dde-shortcut-tool/powercontroller.cpp @@ -49,7 +49,7 @@ PowerController::~PowerController() = default; QStringList PowerController::commandActions() { - return QStringList{ "button", "switch-mode", "system-away", "show-ui" }; + return QStringList{ "button", "switch-mode", "system-away", "show-ui", "suspend" }; } QMap PowerController::commandActionHelp() @@ -58,7 +58,8 @@ QMap PowerController::commandActionHelp() {"button", "Handle power button press event"}, {"switch-mode", "Switch power performance mode"}, {"system-away", "Lock the system"}, - {"show-ui", "Show shutdown interface"} + {"show-ui", "Show shutdown interface"}, + {"suspend", "Suspend the system"} }; } @@ -83,6 +84,9 @@ bool PowerController::execute(const QString &action, const QStringList &args) } else if (action == "show-ui") { showShutdownUI(); return true; + } else if (action == "suspend") { + handleSystemSuspend(); + return true; } qWarning() << "PowerController: unknown action" << action; @@ -120,6 +124,13 @@ void PowerController::handlePowerButton() void PowerController::handleSystemSuspend() { + // Check availability before locking: otherwise Sleep would lock the + // session (and stall 500 ms for the lock UI) on machines where suspend + // is unavailable, while systemSuspend() bails out right afterwards. + if (!canSuspend()) { + qInfo() << "PowerController: suspend not available"; + return; + } DConfig *config = createPowerConfig(this); if (shouldLockOnSleep(config)) { doLock(true);