From a074de1b7ccab92f2e71ce85e409ea9d79272074 Mon Sep 17 00:00:00 2001 From: fruitriin Date: Mon, 10 Aug 2026 22:27:37 +0900 Subject: [PATCH] fix(macos): build key map with UCKeyTranslate to honor keyboard type --- src/lib/platform/CMakeLists.txt | 4 +- src/lib/platform/OSXKeyLayoutResource.cpp | 93 ++++++++ src/lib/platform/OSXKeyLayoutResource.h | 34 +++ src/lib/platform/OSXKeyState.cpp | 13 +- src/lib/platform/OSXUchrKeyResource.cpp | 257 ---------------------- src/lib/platform/OSXUchrKeyResource.h | 43 ---- 6 files changed, 135 insertions(+), 309 deletions(-) create mode 100644 src/lib/platform/OSXKeyLayoutResource.cpp create mode 100644 src/lib/platform/OSXKeyLayoutResource.h delete mode 100644 src/lib/platform/OSXUchrKeyResource.cpp delete mode 100644 src/lib/platform/OSXUchrKeyResource.h diff --git a/src/lib/platform/CMakeLists.txt b/src/lib/platform/CMakeLists.txt index 52ffe0c55..d1bf54789 100644 --- a/src/lib/platform/CMakeLists.txt +++ b/src/lib/platform/CMakeLists.txt @@ -84,6 +84,8 @@ elseif(APPLE) OSXCocoaApp.m OSXEventQueueBuffer.cpp OSXEventQueueBuffer.h + OSXKeyLayoutResource.cpp + OSXKeyLayoutResource.h OSXKeyState.cpp OSXKeyState.h OSXMediaKeySupport.h @@ -99,8 +101,6 @@ elseif(APPLE) OSXScreenSaverControl.h OSXScreenSaverUtil.h OSXScreenSaverUtil.m - OSXUchrKeyResource.cpp - OSXUchrKeyResource.h ) elseif(UNIX) set(PLATFORM_SOURCES diff --git a/src/lib/platform/OSXKeyLayoutResource.cpp b/src/lib/platform/OSXKeyLayoutResource.cpp new file mode 100644 index 000000000..9ce73ffc0 --- /dev/null +++ b/src/lib/platform/OSXKeyLayoutResource.cpp @@ -0,0 +1,93 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Deskflow Developers + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#include "platform/OSXKeyLayoutResource.h" + +// there are 128 virtual key codes and 32 modifier combinations (the +// right-handed modifier bits are ignored, matching the uchr parser). +static const uint32_t s_numButtons = 128; +static const uint32_t s_numModifierCombinations = 32; + +OSXKeyLayoutResource::OSXKeyLayoutResource(const void *resource, uint32_t keyboardType) + : m_layout(static_cast(resource)), + m_keyboardType(keyboardType) +{ +} + +bool OSXKeyLayoutResource::isValid() const +{ + return m_layout != nullptr; +} + +uint32_t OSXKeyLayoutResource::getNumModifierCombinations() const +{ + return s_numModifierCombinations; +} + +uint32_t OSXKeyLayoutResource::getNumTables() const +{ + // one table per modifier combination + return s_numModifierCombinations; +} + +uint32_t OSXKeyLayoutResource::getNumButtons() const +{ + return s_numButtons; +} + +uint32_t OSXKeyLayoutResource::getTableForModifier(uint32_t mask) const +{ + // the modifier combination is the table + return (mask < s_numModifierCombinations) ? mask : 0; +} + +KeyID OSXKeyLayoutResource::getKey(uint32_t table, uint32_t button) const +{ + if (m_layout == nullptr || button >= s_numButtons) { + return kKeyNone; + } + + // UCKeyTranslate's modifier key state uses the same low five bits as our + // table index (cmd, shift, caps, option, control). + const UInt32 modifierState = table; + + UInt32 deadKeyState = 0; + UniChar chars[4] = {0}; + UniCharCount count = 0; + OSStatus status = UCKeyTranslate( + m_layout, static_cast(button), kUCKeyActionDown, modifierState, m_keyboardType, 0, &deadKeyState, + sizeof(chars) / sizeof(chars[0]), &count, chars + ); + if (status != noErr) { + return kKeyNone; + } + + if (count == 0) { + // no output. a non-zero dead-key state means this is a dead key: resolve + // it to its spacing character by following it with a space, then convert + // to the dead KeyID. + if (deadKeyState == 0) { + return kKeyNone; + } + UniChar spaceChars[4] = {0}; + UniCharCount spaceCount = 0; + status = UCKeyTranslate( + m_layout, kVK_Space, kUCKeyActionDown, 0, m_keyboardType, 0, &deadKeyState, + sizeof(spaceChars) / sizeof(spaceChars[0]), &spaceCount, spaceChars + ); + if (status != noErr || spaceCount != 1) { + return kKeyNone; + } + return deskflow::KeyMap::getDeadKey(unicharToKeyID(spaceChars[0])); + } + + // no support for multi-character output (matches the uchr parser). + if (count != 1) { + return kKeyNone; + } + + return unicharToKeyID(chars[0]); +} diff --git a/src/lib/platform/OSXKeyLayoutResource.h b/src/lib/platform/OSXKeyLayoutResource.h new file mode 100644 index 000000000..9403f9ea5 --- /dev/null +++ b/src/lib/platform/OSXKeyLayoutResource.h @@ -0,0 +1,34 @@ +/* + * 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 "platform/IOSXKeyResource.h" + +#include + +//! Key resource for a keyboard layout, backed by UCKeyTranslate +/*! +Builds the key map for a keyboard layout using UCKeyTranslate. Each modifier +combination is treated as its own table. +*/ +class OSXKeyLayoutResource : public IOSXKeyResource +{ +public: + OSXKeyLayoutResource(const void *resource, uint32_t keyboardType); + + // IOSXKeyResource overrides + bool isValid() const override; + uint32_t getNumModifierCombinations() const override; + uint32_t getNumTables() const override; + uint32_t getNumButtons() const override; + uint32_t getTableForModifier(uint32_t mask) const override; + KeyID getKey(uint32_t table, uint32_t button) const override; + +private: + const UCKeyboardLayout *m_layout; + uint32_t m_keyboardType; +}; diff --git a/src/lib/platform/OSXKeyState.cpp b/src/lib/platform/OSXKeyState.cpp index 796e95f9a..f7ec83320 100644 --- a/src/lib/platform/OSXKeyState.cpp +++ b/src/lib/platform/OSXKeyState.cpp @@ -8,8 +8,8 @@ #include "platform/OSXKeyState.h" #include "arch/Arch.h" #include "base/Log.h" +#include "platform/OSXKeyLayoutResource.h" #include "platform/OSXMediaKeySupport.h" -#include "platform/OSXUchrKeyResource.h" #include #include @@ -527,8 +527,7 @@ void OSXKeyState::getKeyMap(deskflow::KeyMap &keyMap) const void *resource; bool layoutValid = false; - // add regular keys - // try uchr resource first + // add regular keys from the layout's key data TISInputSourceRef keyboardLayout = (TISInputSourceRef)CFArrayGetValueAtIndex(m_groups.get(), g); CFDataRef resourceRef = nullptr; { @@ -541,10 +540,10 @@ void OSXKeyState::getKeyMap(deskflow::KeyMap &keyMap) resource = CFDataGetBytePtr(resourceRef); if (layoutValid) { - OSXUchrKeyResource uchr(resource, keyboardType); - if (uchr.isValid()) { - LOG_VERBOSE("using uchr resource for group %d", g); - getKeyMap(keyMap, g, uchr); + OSXKeyLayoutResource keyResource(resource, keyboardType); + if (keyResource.isValid()) { + LOG_VERBOSE("using key layout for group %d", g); + getKeyMap(keyMap, g, keyResource); continue; } } diff --git a/src/lib/platform/OSXUchrKeyResource.cpp b/src/lib/platform/OSXUchrKeyResource.cpp deleted file mode 100644 index 08c3a360a..000000000 --- a/src/lib/platform/OSXUchrKeyResource.cpp +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2016 Synergy App Ltd - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#include "platform/OSXUchrKeyResource.h" - -#include - -// -// OSXUchrKeyResource -// - -OSXUchrKeyResource::OSXUchrKeyResource(const void *resource, uint32_t keyboardType) - : m_m(nullptr), - m_cti(nullptr), - m_sdi(nullptr), - m_sri(nullptr), - m_st(nullptr) -{ - m_resource = static_cast(resource); - if (m_resource == nullptr) { - return; - } - - // find the keyboard info for the current keyboard type - const UCKeyboardTypeHeader *th = nullptr; - const UCKeyboardLayout *r = m_resource; - for (ItemCount i = 0; i < r->keyboardTypeCount; ++i) { - if (keyboardType >= r->keyboardTypeList[i].keyboardTypeFirst && - keyboardType <= r->keyboardTypeList[i].keyboardTypeLast) { - th = r->keyboardTypeList + i; - break; - } - if (r->keyboardTypeList[i].keyboardTypeFirst == 0) { - // found the default. use it unless we find a match. - th = r->keyboardTypeList + i; - } - } - if (th == nullptr) { - // cannot find a suitable keyboard type - return; - } - - // get tables for keyboard type - const uint8_t *const base = reinterpret_cast(m_resource); - m_m = reinterpret_cast(base + th->keyModifiersToTableNumOffset); - m_cti = reinterpret_cast(base + th->keyToCharTableIndexOffset); - m_sdi = reinterpret_cast(base + th->keySequenceDataIndexOffset); - if (th->keyStateRecordsIndexOffset != 0) { - m_sri = reinterpret_cast(base + th->keyStateRecordsIndexOffset); - } - if (th->keyStateTerminatorsOffset != 0) { - m_st = reinterpret_cast(base + th->keyStateTerminatorsOffset); - } - - // find the space key, but only if it can combine with dead keys. - // a dead key followed by a space yields the non-dead version of - // the dead key. - m_spaceOutput = 0xffffu; - uint32_t table = getTableForModifier(0); - for (uint32_t button = 0, n = getNumButtons(); button < n; ++button) { - KeyID id = getKey(table, button); - if (id == 0x20) { - UCKeyOutput c = reinterpret_cast(base + m_cti->keyToCharTableOffsets[table])[button]; - if ((c & kUCKeyOutputTestForIndexMask) == kUCKeyOutputStateIndexMask) { - m_spaceOutput = (c & kUCKeyOutputGetIndexMask); - break; - } - } - } -} - -bool OSXUchrKeyResource::isValid() const -{ - return (m_m != nullptr); -} - -uint32_t OSXUchrKeyResource::getNumModifierCombinations() const -{ - // only 32 (not 256) because the righthanded modifier bits are ignored - return 32; -} - -uint32_t OSXUchrKeyResource::getNumTables() const -{ - return m_cti->keyToCharTableCount; -} - -uint32_t OSXUchrKeyResource::getNumButtons() const -{ - return m_cti->keyToCharTableSize; -} - -uint32_t OSXUchrKeyResource::getTableForModifier(uint32_t mask) const -{ - if (mask >= m_m->modifiersCount) { - return m_m->defaultTableNum; - } else { - return m_m->tableNum[mask]; - } -} - -KeyID OSXUchrKeyResource::getKey(uint32_t table, uint32_t button) const -{ - assert(table < getNumTables()); - assert(button < getNumButtons()); - - const uint8_t *const base = reinterpret_cast(m_resource); - const UCKeyOutput *cPtr = reinterpret_cast(base + m_cti->keyToCharTableOffsets[table]); - - const UCKeyOutput c = cPtr[button]; - - KeySequence keys; - switch (c & kUCKeyOutputTestForIndexMask) { - case kUCKeyOutputStateIndexMask: - if (!getDeadKey(keys, c & kUCKeyOutputGetIndexMask)) { - return kKeyNone; - } - break; - - case kUCKeyOutputSequenceIndexMask: - default: - if (!addSequence(keys, c)) { - return kKeyNone; - } - break; - } - - // XXX -- no support for multiple characters - if (keys.size() != 1) { - return kKeyNone; - } - - return keys.front(); -} - -bool OSXUchrKeyResource::getDeadKey(KeySequence &keys, uint16_t index) const -{ - if (m_sri == nullptr || index >= m_sri->keyStateRecordCount) { - // XXX -- should we be using some other fallback? - return false; - } - - uint16_t state = 0; - if (!getKeyRecord(keys, index, state)) { - return false; - } - if (state == 0) { - // not a dead key - return true; - } - - // no dead keys if we couldn't find the space key - if (m_spaceOutput == 0xffffu) { - return false; - } - - // the dead key should not have put anything in the key list - if (!keys.empty()) { - return false; - } - - // get the character generated by pressing the space key after the - // dead key. if we're still in a compose state afterwards then we're - // confused so we bail. - if (!getKeyRecord(keys, m_spaceOutput, state) || state != 0) { - return false; - } - - // convert keys to their dead counterparts - for (KeySequence::iterator i = keys.begin(); i != keys.end(); ++i) { - *i = deskflow::KeyMap::getDeadKey(*i); - } - - return true; -} - -bool OSXUchrKeyResource::getKeyRecord(KeySequence &keys, uint16_t index, uint16_t &state) const -{ - const uint8_t *const base = reinterpret_cast(m_resource); - const UCKeyStateRecord *sr = reinterpret_cast(base + m_sri->keyStateRecordOffsets[index]); - const UCKeyStateEntryTerminal *kset = reinterpret_cast(sr->stateEntryData); - - uint16_t nextState = 0; - bool found = false; - if (state == 0) { - found = true; - nextState = sr->stateZeroNextState; - if (!addSequence(keys, sr->stateZeroCharData)) { - return false; - } - } else { - // we have a next entry - switch (sr->stateEntryFormat) { - case kUCKeyStateEntryTerminalFormat: - for (uint16_t j = 0; j < sr->stateEntryCount; ++j) { - if (kset[j].curState == state) { - if (!addSequence(keys, kset[j].charData)) { - return false; - } - nextState = 0; - found = true; - break; - } - } - break; - - case kUCKeyStateEntryRangeFormat: - // XXX -- not supported yet - break; - - default: - // XXX -- unknown format - return false; - } - } - if (!found) { - // use a terminator - if (m_st != nullptr && state < m_st->keyStateTerminatorCount) { - if (!addSequence(keys, m_st->keyStateTerminators[state - 1])) { - return false; - } - } - nextState = sr->stateZeroNextState; - if (!addSequence(keys, sr->stateZeroCharData)) { - return false; - } - } - - // next - state = nextState; - - return true; -} - -bool OSXUchrKeyResource::addSequence(KeySequence &keys, UCKeyCharSeq c) const -{ - if ((c & kUCKeyOutputTestForIndexMask) == kUCKeyOutputSequenceIndexMask) { - uint16_t index = (c & kUCKeyOutputGetIndexMask); - if (index < m_sdi->charSequenceCount && - m_sdi->charSequenceOffsets[index] != m_sdi->charSequenceOffsets[index + 1]) { - // XXX -- sequences not supported yet - return false; - } - } - - if (c != 0xfffe && c != 0xffff) { - KeyID id = unicharToKeyID(c); - if (id != kKeyNone) { - keys.push_back(id); - } - } - - return true; -} diff --git a/src/lib/platform/OSXUchrKeyResource.h b/src/lib/platform/OSXUchrKeyResource.h deleted file mode 100644 index d7eed132a..000000000 --- a/src/lib/platform/OSXUchrKeyResource.h +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2016 Synergy App Ltd - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#pragma once - -#include "platform/IOSXKeyResource.h" - -#include - -using KeyLayout = TISInputSourceRef; - -class OSXUchrKeyResource : public IOSXKeyResource -{ -public: - OSXUchrKeyResource(const void *, uint32_t keyboardType); - - // KeyResource overrides - bool isValid() const override; - uint32_t getNumModifierCombinations() const override; - uint32_t getNumTables() const override; - uint32_t getNumButtons() const override; - uint32_t getTableForModifier(uint32_t mask) const override; - KeyID getKey(uint32_t table, uint32_t button) const override; - -private: - using KeySequence = std::vector; - - bool getDeadKey(KeySequence &keys, uint16_t index) const; - bool getKeyRecord(KeySequence &keys, uint16_t index, uint16_t &state) const; - bool addSequence(KeySequence &keys, UCKeyCharSeq c) const; - -private: - const UCKeyboardLayout *m_resource; - const UCKeyModifiersToTableNum *m_m; - const UCKeyToCharTableIndex *m_cti; - const UCKeySequenceDataIndex *m_sdi; - const UCKeyStateRecordsIndex *m_sri; - const UCKeyStateTerminators *m_st; - uint16_t m_spaceOutput; -};