fix(macos): build key map with UCKeyTranslate to honor keyboard type

This commit is contained in:
fruitriin 2026-08-10 22:27:37 +09:00 committed by Nick Bolton
parent abcd4278fe
commit a074de1b7c
6 changed files with 135 additions and 309 deletions

View file

@ -84,6 +84,8 @@ elseif(APPLE)
OSXCocoaApp.m OSXCocoaApp.m
OSXEventQueueBuffer.cpp OSXEventQueueBuffer.cpp
OSXEventQueueBuffer.h OSXEventQueueBuffer.h
OSXKeyLayoutResource.cpp
OSXKeyLayoutResource.h
OSXKeyState.cpp OSXKeyState.cpp
OSXKeyState.h OSXKeyState.h
OSXMediaKeySupport.h OSXMediaKeySupport.h
@ -99,8 +101,6 @@ elseif(APPLE)
OSXScreenSaverControl.h OSXScreenSaverControl.h
OSXScreenSaverUtil.h OSXScreenSaverUtil.h
OSXScreenSaverUtil.m OSXScreenSaverUtil.m
OSXUchrKeyResource.cpp
OSXUchrKeyResource.h
) )
elseif(UNIX) elseif(UNIX)
set(PLATFORM_SOURCES set(PLATFORM_SOURCES

View file

@ -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<const UCKeyboardLayout *>(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<UInt16>(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]);
}

View file

@ -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 <Carbon/Carbon.h>
//! 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;
};

View file

@ -8,8 +8,8 @@
#include "platform/OSXKeyState.h" #include "platform/OSXKeyState.h"
#include "arch/Arch.h" #include "arch/Arch.h"
#include "base/Log.h" #include "base/Log.h"
#include "platform/OSXKeyLayoutResource.h"
#include "platform/OSXMediaKeySupport.h" #include "platform/OSXMediaKeySupport.h"
#include "platform/OSXUchrKeyResource.h"
#include <Carbon/Carbon.h> #include <Carbon/Carbon.h>
#include <IOKit/hidsystem/IOHIDLib.h> #include <IOKit/hidsystem/IOHIDLib.h>
@ -527,8 +527,7 @@ void OSXKeyState::getKeyMap(deskflow::KeyMap &keyMap)
const void *resource; const void *resource;
bool layoutValid = false; bool layoutValid = false;
// add regular keys // add regular keys from the layout's key data
// try uchr resource first
TISInputSourceRef keyboardLayout = (TISInputSourceRef)CFArrayGetValueAtIndex(m_groups.get(), g); TISInputSourceRef keyboardLayout = (TISInputSourceRef)CFArrayGetValueAtIndex(m_groups.get(), g);
CFDataRef resourceRef = nullptr; CFDataRef resourceRef = nullptr;
{ {
@ -541,10 +540,10 @@ void OSXKeyState::getKeyMap(deskflow::KeyMap &keyMap)
resource = CFDataGetBytePtr(resourceRef); resource = CFDataGetBytePtr(resourceRef);
if (layoutValid) { if (layoutValid) {
OSXUchrKeyResource uchr(resource, keyboardType); OSXKeyLayoutResource keyResource(resource, keyboardType);
if (uchr.isValid()) { if (keyResource.isValid()) {
LOG_VERBOSE("using uchr resource for group %d", g); LOG_VERBOSE("using key layout for group %d", g);
getKeyMap(keyMap, g, uchr); getKeyMap(keyMap, g, keyResource);
continue; continue;
} }
} }

View file

@ -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 <Carbon/Carbon.h>
//
// 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<const UCKeyboardLayout *>(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<const uint8_t *>(m_resource);
m_m = reinterpret_cast<const UCKeyModifiersToTableNum *>(base + th->keyModifiersToTableNumOffset);
m_cti = reinterpret_cast<const UCKeyToCharTableIndex *>(base + th->keyToCharTableIndexOffset);
m_sdi = reinterpret_cast<const UCKeySequenceDataIndex *>(base + th->keySequenceDataIndexOffset);
if (th->keyStateRecordsIndexOffset != 0) {
m_sri = reinterpret_cast<const UCKeyStateRecordsIndex *>(base + th->keyStateRecordsIndexOffset);
}
if (th->keyStateTerminatorsOffset != 0) {
m_st = reinterpret_cast<const UCKeyStateTerminators *>(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<const UCKeyOutput *>(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<const uint8_t *>(m_resource);
const UCKeyOutput *cPtr = reinterpret_cast<const UCKeyOutput *>(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<const uint8_t *>(m_resource);
const UCKeyStateRecord *sr = reinterpret_cast<const UCKeyStateRecord *>(base + m_sri->keyStateRecordOffsets[index]);
const UCKeyStateEntryTerminal *kset = reinterpret_cast<const UCKeyStateEntryTerminal *>(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;
}

View file

@ -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 <Carbon/Carbon.h>
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<KeyID>;
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;
};