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);