fix: preserve locked modifiers on ei backend

This commit is contained in:
Charles Jones 2026-07-28 13:17:32 -04:00 committed by Chris Rizzitello
parent 0d21f54cb2
commit f8edc7b0ae
5 changed files with 160 additions and 9 deletions

View file

@ -97,8 +97,9 @@ bool EiKeyState::fakeCtrlAltDel()
KeyModifierMask EiKeyState::pollActiveModifiers() const KeyModifierMask EiKeyState::pollActiveModifiers() const
{ {
const auto xkbMask = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_EFFECTIVE); const auto effectiveMask = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_EFFECTIVE);
return convertModMask(xkbMask); 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 std::int32_t EiKeyState::pollActiveGroup() const
@ -112,7 +113,7 @@ void EiKeyState::pollPressedKeys(KeyButtonSet &) const
return; 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. // This is our own modifier mask, not xkb's.
std::uint32_t modMaskOut = 0; std::uint32_t modMaskOut = 0;
@ -170,12 +171,12 @@ std::uint32_t EiKeyState::convertModMask(xkb_mod_mask_t xkbModMaskIn) const
modMaskOut |= (1 << kKeyModifierBitAltGr); modMaskOut |= (1 << kKeyModifierBitAltGr);
else if (strcmp(XKB_VMOD_NAME_LEVEL5, name) == 0) else if (strcmp(XKB_VMOD_NAME_LEVEL5, name) == 0)
modMaskOut |= (1 << kKeyModifierBitLevel5Lock); 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); modMaskOut |= (1 << kKeyModifierBitNumLock);
else if (strcmp(XKB_VMOD_NAME_SCROLL, name) == 0) else if (strcmp(XKB_VMOD_NAME_SCROLL, name) == 0)
modMaskOut |= (1 << kKeyModifierBitScrollLock); modMaskOut |= (1 << kKeyModifierBitScrollLock);
else if ((strcmp(XKB_VMOD_NAME_META, name) == 0) || // virtual; the old meta (not the new meta/super/logo key) 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. (!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. (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); LOG_VERBOSE("modifier mask %s ignored", name);
@ -346,11 +347,17 @@ void EiKeyState::updateXkbState(uint32_t keyval, bool isPressed)
void EiKeyState::clearStaleModifiers() void EiKeyState::clearStaleModifiers()
{ {
// Recreate the XKB state to clear stuck modifiers that happen when const auto lockedMods = xkb_state_serialize_mods(m_xkbState, XKB_STATE_MODS_LOCKED);
// modifier keys are press on client and released on server 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) { if (m_xkbState) {
xkb_state_unref(m_xkbState); xkb_state_unref(m_xkbState);
} }
m_xkbState = xkb_state_new(m_xkbKeymap); m_xkbState = xkb_state_new(m_xkbKeymap);
xkb_state_update_mask(m_xkbState, 0, 0, lockedMods, 0, 0, lockedLayout);
} }
} // namespace deskflow } // namespace deskflow

View file

@ -43,7 +43,7 @@ protected:
void fakeKey(const Keystroke &keystroke) override; void fakeKey(const Keystroke &keystroke) override;
private: 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); void assignGeneratedModifiers(std::uint32_t keycode, KeyMap::KeyItem &item);
EiScreen *m_screen = nullptr; EiScreen *m_screen = nullptr;

View file

@ -25,6 +25,16 @@ elseif(APPLE)
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/platform" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/platform"
) )
elseif(UNIX) 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) if (BUILD_X11_SUPPORT)
create_test( create_test(
NAME XWindowsClipboardTests NAME XWindowsClipboardTests

View file

@ -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 <QByteArray>
#include <QTemporaryFile>
#include <cstdint>
namespace {
class TestAppUtil : public AppUtil
{
public:
int run() override
{
return 0;
}
void startNode() override
{
}
std::vector<std::string> getKeyboardLayoutList() override
{
return {"en"};
}
std::string getCurrentLanguageCode() override
{
return "en";
}
};
const char TestKeymap[] = R"XKB(xkb_keymap {
xkb_keycodes "test" {
minimum = 8;
maximum = 255;
<LFSH> = 50;
<NMLK> = 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 <LFSH> { [ Shift_L ] };
key <NMLK> { [ Num_Lock ] };
modifier_map Shift { <LFSH> };
modifier_map Mod2 { <NMLK> };
};
};)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)

View file

@ -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 <QTest>
class EiKeyStateTests : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void clearStaleModifiers_shiftDownAndNumLockOn_shiftClearedAndNumLockPreserved();
private:
Arch m_arch;
Log m_log;
};