From f8edc7b0ae90da684de4461f549b7b4a4ccd1b5b Mon Sep 17 00:00:00 2001 From: Charles Jones Date: Tue, 28 Jul 2026 13:17:32 -0400 Subject: [PATCH] fix: preserve locked modifiers on ei backend --- src/lib/platform/EiKeyState.cpp | 23 +++-- src/lib/platform/EiKeyState.h | 2 +- src/unittests/platform/CMakeLists.txt | 10 ++ src/unittests/platform/EiKeyStateTests.cpp | 109 +++++++++++++++++++++ src/unittests/platform/EiKeyStateTests.h | 25 +++++ 5 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 src/unittests/platform/EiKeyStateTests.cpp create mode 100644 src/unittests/platform/EiKeyStateTests.h diff --git a/src/lib/platform/EiKeyState.cpp b/src/lib/platform/EiKeyState.cpp index ad8290edf..1d9dcb5dc 100644 --- a/src/lib/platform/EiKeyState.cpp +++ b/src/lib/platform/EiKeyState.cpp @@ -97,8 +97,9 @@ bool EiKeyState::fakeCtrlAltDel() KeyModifierMask EiKeyState::pollActiveModifiers() const { - const auto xkbMask = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_EFFECTIVE); - return convertModMask(xkbMask); + const auto effectiveMask = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_EFFECTIVE); + const auto lockedMask = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_LOCKED); + return convertModMask(effectiveMask) | convertModMask(lockedMask, true); } std::int32_t EiKeyState::pollActiveGroup() const @@ -112,7 +113,7 @@ void EiKeyState::pollPressedKeys(KeyButtonSet &) const return; } -std::uint32_t EiKeyState::convertModMask(xkb_mod_mask_t xkbModMaskIn) const +std::uint32_t EiKeyState::convertModMask(xkb_mod_mask_t xkbModMaskIn, bool mapMod2ToNumLock) const { // This is our own modifier mask, not xkb's. std::uint32_t modMaskOut = 0; @@ -170,13 +171,13 @@ std::uint32_t EiKeyState::convertModMask(xkb_mod_mask_t xkbModMaskIn) const modMaskOut |= (1 << kKeyModifierBitAltGr); else if (strcmp(XKB_VMOD_NAME_LEVEL5, name) == 0) modMaskOut |= (1 << kKeyModifierBitLevel5Lock); - else if (strcmp(XKB_VMOD_NAME_NUM, name) == 0) + else if (strcmp(XKB_VMOD_NAME_NUM, name) == 0 || (mapMod2ToNumLock && strcmp(XKB_MOD_NAME_MOD2, name) == 0)) modMaskOut |= (1 << kKeyModifierBitNumLock); else if (strcmp(XKB_VMOD_NAME_SCROLL, name) == 0) modMaskOut |= (1 << kKeyModifierBitScrollLock); else if ((strcmp(XKB_VMOD_NAME_META, name) == 0) || // virtual; the old meta (not the new meta/super/logo key) - (strcmp(XKB_MOD_NAME_MOD2, name) == 0) || // spare, sometimes mapped to num lock. - (strcmp(XKB_MOD_NAME_MOD3, name) == 0) // spare, could be mapped to alt_r, caps lock, scroll lock, etc. + (!mapMod2ToNumLock && strcmp(XKB_MOD_NAME_MOD2, name) == 0) || // spare, sometimes mapped to num lock. + (strcmp(XKB_MOD_NAME_MOD3, name) == 0) // spare, could be mapped to alt_r, caps lock, scroll lock, etc. ) LOG_VERBOSE("modifier mask %s ignored", name); else @@ -346,11 +347,17 @@ void EiKeyState::updateXkbState(uint32_t keyval, bool isPressed) void EiKeyState::clearStaleModifiers() { - // Recreate the XKB state to clear stuck modifiers that happen when - // modifier keys are press on client and released on server + const auto lockedMods = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_LOCKED); + const auto lockedLayout = xkb_state_serialize_layout(m_xkbState, XKB_STATE_LAYOUT_LOCKED); + + // Recreate the XKB state to clear stuck depressed modifiers that happen when + // modifier keys are pressed on the client and released on the server. Locked + // modifiers are real keyboard state; do not clear NumLock/CapsLock/ScrollLock + // during screen transitions. if (m_xkbState) { xkb_state_unref(m_xkbState); } m_xkbState = xkb_state_new(m_xkbKeymap); + xkb_state_update_mask(m_xkbState, 0, 0, lockedMods, 0, 0, lockedLayout); } } // namespace deskflow diff --git a/src/lib/platform/EiKeyState.h b/src/lib/platform/EiKeyState.h index 477abbdb9..aa4005fb8 100644 --- a/src/lib/platform/EiKeyState.h +++ b/src/lib/platform/EiKeyState.h @@ -43,7 +43,7 @@ protected: void fakeKey(const Keystroke &keystroke) override; private: - std::uint32_t convertModMask(xkb_mod_mask_t xkbModMaskIn) const; + std::uint32_t convertModMask(xkb_mod_mask_t xkbModMaskIn, bool mapMod2ToNumLock = false) const; void assignGeneratedModifiers(std::uint32_t keycode, KeyMap::KeyItem &item); EiScreen *m_screen = nullptr; diff --git a/src/unittests/platform/CMakeLists.txt b/src/unittests/platform/CMakeLists.txt index 9b613f0e9..35ed48100 100644 --- a/src/unittests/platform/CMakeLists.txt +++ b/src/unittests/platform/CMakeLists.txt @@ -25,6 +25,16 @@ elseif(APPLE) WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/platform" ) elseif(UNIX) + if(LIBEI_FOUND) + create_test( + NAME EiKeyStateTests + DEPENDS platform + LIBS base arch + SOURCE EiKeyStateTests.cpp + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/platform" + ) + endif() + if (BUILD_X11_SUPPORT) create_test( NAME XWindowsClipboardTests diff --git a/src/unittests/platform/EiKeyStateTests.cpp b/src/unittests/platform/EiKeyStateTests.cpp new file mode 100644 index 000000000..6ba3372c9 --- /dev/null +++ b/src/unittests/platform/EiKeyStateTests.cpp @@ -0,0 +1,109 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Deskflow Developers + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#include "EiKeyStateTests.h" + +#include "base/EventQueue.h" +#include "deskflow/AppUtil.h" +#include "deskflow/KeyTypes.h" +#include "platform/EiKeyState.h" + +#include +#include + +#include + +namespace { +class TestAppUtil : public AppUtil +{ +public: + int run() override + { + return 0; + } + + void startNode() override + { + } + + std::vector getKeyboardLayoutList() override + { + return {"en"}; + } + + std::string getCurrentLanguageCode() override + { + return "en"; + } +}; + +const char TestKeymap[] = R"XKB(xkb_keymap { +xkb_keycodes "test" { + minimum = 8; + maximum = 255; + = 50; + = 77; +}; +xkb_types "test" { + type "ONE_LEVEL" { + modifiers = none; + level_name[Level1] = "Any"; + }; +}; +xkb_compat "test" { + interpret Shift_L+AnyOfOrNone(all) { + action = SetMods(modifiers=Shift); + }; + interpret Num_Lock+AnyOfOrNone(all) { + action = LockMods(modifiers=Mod2); + }; +}; +xkb_symbols "test" { + key { [ Shift_L ] }; + key { [ Num_Lock ] }; + modifier_map Shift { }; + modifier_map Mod2 { }; +}; +};)XKB"; + +// XKB keycodes for TestKeymap. +constexpr std::uint32_t LeftShiftKeycode = 50; +constexpr std::uint32_t NumLockKeycode = 77; +} // namespace + +void EiKeyStateTests::initTestCase() +{ + m_arch.init(); + m_log.setFilter(LogLevel::Level::Verbose); +} + +void EiKeyStateTests::clearStaleModifiers_shiftDownAndNumLockOn_shiftClearedAndNumLockPreserved() +{ + TestAppUtil appUtil; + EventQueue eventQueue; + deskflow::EiKeyState keyState(nullptr, &eventQueue); + + QTemporaryFile keymapFile; + QVERIFY(keymapFile.open()); + const QByteArray keymapData = QByteArray::fromRawData(TestKeymap, sizeof(TestKeymap) - 1); + QCOMPARE(keymapFile.write(keymapData), keymapData.size()); + QVERIFY(keymapFile.flush()); + keyState.init(keymapFile.handle(), keymapFile.size()); + + keyState.updateXkbState(LeftShiftKeycode, true); + keyState.updateXkbState(NumLockKeycode, true); + keyState.updateXkbState(NumLockKeycode, false); + + QVERIFY((keyState.pollActiveModifiers() & KeyModifierShift) != 0); + QVERIFY((keyState.pollActiveModifiers() & KeyModifierNumLock) != 0); + + keyState.clearStaleModifiers(); + + QVERIFY((keyState.pollActiveModifiers() & KeyModifierShift) == 0); + QVERIFY((keyState.pollActiveModifiers() & KeyModifierNumLock) != 0); +} + +QTEST_MAIN(EiKeyStateTests) diff --git a/src/unittests/platform/EiKeyStateTests.h b/src/unittests/platform/EiKeyStateTests.h new file mode 100644 index 000000000..76b2756d8 --- /dev/null +++ b/src/unittests/platform/EiKeyStateTests.h @@ -0,0 +1,25 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Deskflow Developers + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#pragma once + +#include "arch/Arch.h" +#include "base/Log.h" + +#include + +class EiKeyStateTests : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void clearStaleModifiers_shiftDownAndNumLockOn_shiftClearedAndNumLockPreserved(); + +private: + Arch m_arch; + Log m_log; +};