Skip to content

Commit c96be68

Browse files
author
Allen Benz
committed
Support {MODE=VIRTUAL} on macOS
1 parent feb8c24 commit c96be68

7 files changed

Lines changed: 392 additions & 6 deletions

File tree

share/translations/keepassxc_en.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,13 @@
854854
<translation type="unfinished"></translation>
855855
</message>
856856
</context>
857+
<context>
858+
<name>AutoTypePlatformMac</name>
859+
<message>
860+
<source>Unable to get valid keycode for key: </source>
861+
<translation type="unfinished"></translation>
862+
</message>
863+
</context>
857864
<context>
858865
<name>AutoTypePlatformWayland</name>
859866
<message>

src/autotype/mac/AutoTypeMac.cpp

Lines changed: 144 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818

1919
#include "AutoTypeMac.h"
20+
#include "AutoTypeMac_p.h"
2021
#include "core/Tools.h"
2122
#include "gui/osutils/macutils/MacUtils.h"
2223
#include "gui/MessageBox.h"
@@ -25,6 +26,85 @@
2526

2627
#define INVALID_KEYCODE 0xFFFF
2728

29+
//
30+
// Resolve macOS virtual key code and modifier flags for a unicode character
31+
// using the current keyboard layout via UCKeyTranslate.
32+
// Returns false if no single-keystroke mapping is found.
33+
//
34+
bool charToNativeKeyCode(const UCKeyboardLayout* layout,
35+
UInt32 keyboardType,
36+
const QChar& ch,
37+
uint16_t& outKeyCode,
38+
CGEventFlags& outFlags)
39+
{
40+
UInt32 deadKeyState = 0;
41+
UniCharCount actualLen = 0;
42+
UniChar unicode[4];
43+
44+
static const UInt32 kUCTShift = shiftKey >> 8;
45+
static const UInt32 kUCTOption = optionKey >> 8;
46+
47+
static const UInt32 modifierCombinations[] = {
48+
0,
49+
kUCTShift,
50+
kUCTOption,
51+
kUCTShift | kUCTOption,
52+
};
53+
54+
for (uint16_t keyCode = 0; keyCode < 128; keyCode++) {
55+
for (UInt32 mods : modifierCombinations) {
56+
deadKeyState = 0;
57+
OSStatus status = UCKeyTranslate(layout,
58+
keyCode,
59+
kUCKeyActionDown,
60+
mods,
61+
keyboardType,
62+
kUCKeyTranslateNoDeadKeysBit,
63+
&deadKeyState,
64+
4,
65+
&actualLen,
66+
unicode);
67+
if (status == noErr && actualLen == 1 && unicode[0] == ch.unicode()) {
68+
outKeyCode = keyCode;
69+
outFlags = 0;
70+
if (mods & kUCTShift) {
71+
outFlags |= kCGEventFlagMaskShift;
72+
}
73+
if (mods & kUCTOption) {
74+
outFlags |= kCGEventFlagMaskAlternate;
75+
}
76+
return true;
77+
}
78+
}
79+
}
80+
81+
return false;
82+
}
83+
84+
static bool charToNativeKeyCode(const QChar& ch, uint16_t& outKeyCode, CGEventFlags& outFlags)
85+
{
86+
TISInputSourceRef keyboard = TISCopyCurrentKeyboardLayoutInputSource();
87+
if (!keyboard) {
88+
keyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
89+
}
90+
if (!keyboard) {
91+
return false;
92+
}
93+
94+
CFDataRef layoutData =
95+
static_cast<CFDataRef>(TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData));
96+
if (!layoutData) {
97+
CFRelease(keyboard);
98+
return false;
99+
}
100+
101+
const UCKeyboardLayout* layout = reinterpret_cast<const UCKeyboardLayout*>(CFDataGetBytePtr(layoutData));
102+
bool result = charToNativeKeyCode(layout, LMGetKbdType(), ch, outKeyCode, outFlags);
103+
104+
CFRelease(keyboard);
105+
return result;
106+
}
107+
28108
AutoTypePlatformMac::AutoTypePlatformMac()
29109
{
30110
MessageBox::initializeButtonDefs();
@@ -158,6 +238,18 @@ bool AutoTypePlatformMac::raiseOwnWindow()
158238
return m_macUtils->raiseOwnWindow();
159239
}
160240

241+
//
242+
// Send raw keycode event
243+
//
244+
void AutoTypePlatformMac::sendRawKey(uint16_t keyCode, bool isKeyDown)
245+
{
246+
CGEventRef keyEvent = ::CGEventCreateKeyboardEvent(nullptr, keyCode, isKeyDown);
247+
if (keyEvent != nullptr) {
248+
::CGEventPost(kCGSessionEventTap, keyEvent);
249+
::CFRelease(keyEvent);
250+
}
251+
}
252+
161253
//
162254
// Send unicode character to active window
163255
// see: Quartz Event Services
@@ -173,6 +265,53 @@ void AutoTypePlatformMac::sendChar(const QChar& ch, bool isKeyDown)
173265
}
174266
}
175267

268+
//
269+
// Send unicode character as native keycode + modifiers
270+
// see: Quartz Event Services
271+
//
272+
bool AutoTypePlatformMac::sendCharVirtual(const QChar& ch, bool isKeyDown)
273+
{
274+
uint16_t keyCode = 0;
275+
CGEventFlags flags = 0;
276+
277+
if (!charToNativeKeyCode(ch, keyCode, flags)) {
278+
// Couldn't determine which keys to press to make the character
279+
return false;
280+
}
281+
282+
// Send physical modifier key events for VNC/Screen Sharing compatibility
283+
if (isKeyDown) {
284+
if (flags & kCGEventFlagMaskShift) {
285+
sendRawKey(kVK_Shift, true);
286+
}
287+
if (flags & kCGEventFlagMaskAlternate) {
288+
sendRawKey(kVK_Option, true);
289+
}
290+
}
291+
292+
CGEventRef keyEvent = ::CGEventCreateKeyboardEvent(nullptr, keyCode, isKeyDown);
293+
if (keyEvent != nullptr) {
294+
if (flags != 0) {
295+
::CGEventSetFlags(keyEvent, flags);
296+
}
297+
UniChar unicode = ch.unicode();
298+
::CGEventKeyboardSetUnicodeString(keyEvent, 1, &unicode);
299+
::CGEventPost(kCGSessionEventTap, keyEvent);
300+
::CFRelease(keyEvent);
301+
}
302+
303+
// Release modifier keys
304+
if (!isKeyDown) {
305+
if (flags & kCGEventFlagMaskAlternate) {
306+
sendRawKey(kVK_Option, false);
307+
}
308+
if (flags & kCGEventFlagMaskShift) {
309+
sendRawKey(kVK_Shift, false);
310+
}
311+
}
312+
return true;
313+
}
314+
176315
//
177316
// Send key code to active window
178317
// see: Quartz Event Services
@@ -243,8 +382,6 @@ AutoTypeAction::Result AutoTypeExecutorMac::execBegin(const AutoTypeBegin* actio
243382

244383
AutoTypeAction::Result AutoTypeExecutorMac::execType(const AutoTypeKey* action)
245384
{
246-
247-
248385
if (action->key != Qt::Key_unknown) {
249386
m_platform->sendKey(action->key, true, action->modifiers);
250387
m_platform->sendKey(action->key, false, action->modifiers);
@@ -256,9 +393,11 @@ AutoTypeAction::Result AutoTypeExecutorMac::execType(const AutoTypeKey* action)
256393
m_platform->sendKey(static_cast<Qt::Key>(ch), true, action->modifiers);
257394
m_platform->sendKey(static_cast<Qt::Key>(ch), false, action->modifiers);
258395
} else if (mode == Mode::VIRTUAL) {
259-
int ch = action->character.toLatin1();
260-
m_platform->sendKey(static_cast<Qt::Key>(ch), true, action->modifiers);
261-
m_platform->sendKey(static_cast<Qt::Key>(ch), false, action->modifiers);
396+
if (!m_platform->sendCharVirtual(action->character, true)) {
397+
return AutoTypeAction::Result::Failed(AutoTypePlatformMac::tr("Unable to get valid keycode for key: ")
398+
+ action->character);
399+
}
400+
m_platform->sendCharVirtual(action->character, false);
262401
} else {
263402
m_platform->sendChar(action->character, true);
264403
m_platform->sendChar(action->character, false);

src/autotype/mac/AutoTypeMac.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ class AutoTypePlatformMac : public QObject, public AutoTypePlatformInterface
4848
bool raiseOwnWindow() override;
4949

5050
void sendChar(const QChar& ch, bool isKeyDown);
51+
bool sendCharVirtual(const QChar& ch, bool isKeyDown);
52+
void sendRawKey(uint16_t keyCode, bool isKeyDown);
5153
void sendKey(Qt::Key key, bool isKeyDown, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
5254

5355
private:

src/autotype/mac/AutoTypeMac_p.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 2 or (at your option)
7+
* version 3 of the License.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
#ifndef KEEPASSXC_AUTOTYPEMAC_P_H
19+
#define KEEPASSXC_AUTOTYPEMAC_P_H
20+
21+
#include <ApplicationServices/ApplicationServices.h>
22+
#include <Carbon/Carbon.h>
23+
#include <QChar>
24+
25+
/* Internal helper for reverse-looking a character to a native keycode. */
26+
27+
bool charToNativeKeyCode(const UCKeyboardLayout* layout,
28+
UInt32 keyboardType,
29+
const QChar& ch,
30+
uint16_t& outKeyCode,
31+
CGEventFlags& outFlags);
32+
33+
#endif // KEEPASSXC_AUTOTYPEMAC_P_H

tests/CMakeLists.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ add_unit_test(NAME testautotype SOURCES TestAutoType.cpp
9292
LIBS testsupport ${TEST_LIBRARIES})
9393
set_target_properties(testautotype PROPERTIES ENABLE_EXPORTS ON)
9494

95+
if(APPLE)
96+
add_unit_test(NAME testautotypemac SOURCES TestAutoTypeMac.cpp
97+
${CMAKE_SOURCE_DIR}/src/autotype/mac/AutoTypeMac.cpp
98+
LIBS ${TEST_LIBRARIES} "-framework Carbon" "-framework ApplicationServices" "-framework CoreFoundation")
99+
endif()
100+
95101
add_unit_test(NAME testentry SOURCES TestEntry.cpp
96102
LIBS ${TEST_LIBRARIES})
97103

@@ -177,7 +183,7 @@ if(KPXC_FEATURE_NETWORK)
177183
add_unit_test(NAME testupdatecheck SOURCES TestUpdateCheck.cpp
178184
LIBS ${TEST_LIBRARIES})
179185

180-
add_unit_test(NAME testicondownloader SOURCES TestIconDownloader.cpp
186+
add_unit_test(NAME testicondownloader SOURCES TestIconDownloader.cpp
181187
LIBS ${TEST_LIBRARIES})
182188
endif()
183189

0 commit comments

Comments
 (0)