diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index 039fd83f9e..9330134f21 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -854,6 +854,13 @@
+
+ AutoTypePlatformMac
+
+ Unable to get valid keycode for key:
+
+
+
AutoTypePlatformWayland
diff --git a/src/autotype/mac/AutoTypeMac.cpp b/src/autotype/mac/AutoTypeMac.cpp
index 56b1c81092..57460098bb 100644
--- a/src/autotype/mac/AutoTypeMac.cpp
+++ b/src/autotype/mac/AutoTypeMac.cpp
@@ -17,6 +17,7 @@
*/
#include "AutoTypeMac.h"
+#include "AutoTypeMac_p.h"
#include "core/Tools.h"
#include "gui/osutils/macutils/MacUtils.h"
#include "gui/MessageBox.h"
@@ -25,6 +26,85 @@
#define INVALID_KEYCODE 0xFFFF
+//
+// Resolve macOS virtual key code and modifier flags for a unicode character
+// using the current keyboard layout via UCKeyTranslate.
+// Returns false if no single-keystroke mapping is found.
+//
+bool charToNativeKeyCode(const UCKeyboardLayout* layout,
+ UInt32 keyboardType,
+ const QChar& ch,
+ uint16_t& outKeyCode,
+ CGEventFlags& outFlags)
+{
+ UInt32 deadKeyState = 0;
+ UniCharCount actualLen = 0;
+ UniChar unicode[4];
+
+ static const UInt32 kUCTShift = shiftKey >> 8;
+ static const UInt32 kUCTOption = optionKey >> 8;
+
+ static const UInt32 modifierCombinations[] = {
+ 0,
+ kUCTShift,
+ kUCTOption,
+ kUCTShift | kUCTOption,
+ };
+
+ for (uint16_t keyCode = 0; keyCode < 128; keyCode++) {
+ for (UInt32 mods : modifierCombinations) {
+ deadKeyState = 0;
+ OSStatus status = UCKeyTranslate(layout,
+ keyCode,
+ kUCKeyActionDown,
+ mods,
+ keyboardType,
+ kUCKeyTranslateNoDeadKeysBit,
+ &deadKeyState,
+ 4,
+ &actualLen,
+ unicode);
+ if (status == noErr && actualLen == 1 && unicode[0] == ch.unicode()) {
+ outKeyCode = keyCode;
+ outFlags = 0;
+ if (mods & kUCTShift) {
+ outFlags |= kCGEventFlagMaskShift;
+ }
+ if (mods & kUCTOption) {
+ outFlags |= kCGEventFlagMaskAlternate;
+ }
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+static bool charToNativeKeyCode(const QChar& ch, uint16_t& outKeyCode, CGEventFlags& outFlags)
+{
+ TISInputSourceRef keyboard = TISCopyCurrentKeyboardLayoutInputSource();
+ if (!keyboard) {
+ keyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
+ }
+ if (!keyboard) {
+ return false;
+ }
+
+ CFDataRef layoutData =
+ static_cast(TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData));
+ if (!layoutData) {
+ CFRelease(keyboard);
+ return false;
+ }
+
+ const UCKeyboardLayout* layout = reinterpret_cast(CFDataGetBytePtr(layoutData));
+ bool result = charToNativeKeyCode(layout, LMGetKbdType(), ch, outKeyCode, outFlags);
+
+ CFRelease(keyboard);
+ return result;
+}
+
AutoTypePlatformMac::AutoTypePlatformMac()
{
MessageBox::initializeButtonDefs();
@@ -158,6 +238,18 @@ bool AutoTypePlatformMac::raiseOwnWindow()
return m_macUtils->raiseOwnWindow();
}
+//
+// Send raw keycode event
+//
+void AutoTypePlatformMac::sendRawKey(uint16_t keyCode, bool isKeyDown)
+{
+ CGEventRef keyEvent = ::CGEventCreateKeyboardEvent(nullptr, keyCode, isKeyDown);
+ if (keyEvent != nullptr) {
+ ::CGEventPost(kCGSessionEventTap, keyEvent);
+ ::CFRelease(keyEvent);
+ }
+}
+
//
// Send unicode character to active window
// see: Quartz Event Services
@@ -173,6 +265,53 @@ void AutoTypePlatformMac::sendChar(const QChar& ch, bool isKeyDown)
}
}
+//
+// Send unicode character as native keycode + modifiers
+// see: Quartz Event Services
+//
+bool AutoTypePlatformMac::sendCharVirtual(const QChar& ch, bool isKeyDown)
+{
+ uint16_t keyCode = 0;
+ CGEventFlags flags = 0;
+
+ if (!charToNativeKeyCode(ch, keyCode, flags)) {
+ // Couldn't determine which keys to press to make the character
+ return false;
+ }
+
+ // Send physical modifier key events for VNC/Screen Sharing compatibility
+ if (isKeyDown) {
+ if (flags & kCGEventFlagMaskShift) {
+ sendRawKey(kVK_Shift, true);
+ }
+ if (flags & kCGEventFlagMaskAlternate) {
+ sendRawKey(kVK_Option, true);
+ }
+ }
+
+ CGEventRef keyEvent = ::CGEventCreateKeyboardEvent(nullptr, keyCode, isKeyDown);
+ if (keyEvent != nullptr) {
+ if (flags != 0) {
+ ::CGEventSetFlags(keyEvent, flags);
+ }
+ UniChar unicode = ch.unicode();
+ ::CGEventKeyboardSetUnicodeString(keyEvent, 1, &unicode);
+ ::CGEventPost(kCGSessionEventTap, keyEvent);
+ ::CFRelease(keyEvent);
+ }
+
+ // Release modifier keys
+ if (!isKeyDown) {
+ if (flags & kCGEventFlagMaskAlternate) {
+ sendRawKey(kVK_Option, false);
+ }
+ if (flags & kCGEventFlagMaskShift) {
+ sendRawKey(kVK_Shift, false);
+ }
+ }
+ return true;
+}
+
//
// Send key code to active window
// see: Quartz Event Services
@@ -243,8 +382,6 @@ AutoTypeAction::Result AutoTypeExecutorMac::execBegin(const AutoTypeBegin* actio
AutoTypeAction::Result AutoTypeExecutorMac::execType(const AutoTypeKey* action)
{
-
-
if (action->key != Qt::Key_unknown) {
m_platform->sendKey(action->key, true, action->modifiers);
m_platform->sendKey(action->key, false, action->modifiers);
@@ -256,9 +393,11 @@ AutoTypeAction::Result AutoTypeExecutorMac::execType(const AutoTypeKey* action)
m_platform->sendKey(static_cast(ch), true, action->modifiers);
m_platform->sendKey(static_cast(ch), false, action->modifiers);
} else if (mode == Mode::VIRTUAL) {
- int ch = action->character.toLatin1();
- m_platform->sendKey(static_cast(ch), true, action->modifiers);
- m_platform->sendKey(static_cast(ch), false, action->modifiers);
+ if (!m_platform->sendCharVirtual(action->character, true)) {
+ return AutoTypeAction::Result::Failed(AutoTypePlatformMac::tr("Unable to get valid keycode for key: ")
+ + action->character);
+ }
+ m_platform->sendCharVirtual(action->character, false);
} else {
m_platform->sendChar(action->character, true);
m_platform->sendChar(action->character, false);
diff --git a/src/autotype/mac/AutoTypeMac.h b/src/autotype/mac/AutoTypeMac.h
index 62a8669369..0f9df00421 100644
--- a/src/autotype/mac/AutoTypeMac.h
+++ b/src/autotype/mac/AutoTypeMac.h
@@ -48,6 +48,8 @@ class AutoTypePlatformMac : public QObject, public AutoTypePlatformInterface
bool raiseOwnWindow() override;
void sendChar(const QChar& ch, bool isKeyDown);
+ bool sendCharVirtual(const QChar& ch, bool isKeyDown);
+ void sendRawKey(uint16_t keyCode, bool isKeyDown);
void sendKey(Qt::Key key, bool isKeyDown, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
private:
diff --git a/src/autotype/mac/AutoTypeMac_p.h b/src/autotype/mac/AutoTypeMac_p.h
new file mode 100644
index 0000000000..08f2131e93
--- /dev/null
+++ b/src/autotype/mac/AutoTypeMac_p.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2025 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_AUTOTYPEMAC_P_H
+#define KEEPASSXC_AUTOTYPEMAC_P_H
+
+#include
+#include
+#include
+
+/* Internal helper for reverse-looking a character to a native keycode. */
+
+bool charToNativeKeyCode(const UCKeyboardLayout* layout,
+ UInt32 keyboardType,
+ const QChar& ch,
+ uint16_t& outKeyCode,
+ CGEventFlags& outFlags);
+
+#endif // KEEPASSXC_AUTOTYPEMAC_P_H
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index d11e5660b5..d36af2913e 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -92,6 +92,12 @@ add_unit_test(NAME testautotype SOURCES TestAutoType.cpp
LIBS testsupport ${TEST_LIBRARIES})
set_target_properties(testautotype PROPERTIES ENABLE_EXPORTS ON)
+if(APPLE)
+ add_unit_test(NAME testautotypemac SOURCES TestAutoTypeMac.cpp
+ ${CMAKE_SOURCE_DIR}/src/autotype/mac/AutoTypeMac.cpp
+ LIBS ${TEST_LIBRARIES} "-framework Carbon" "-framework ApplicationServices" "-framework CoreFoundation")
+endif()
+
add_unit_test(NAME testentry SOURCES TestEntry.cpp
LIBS ${TEST_LIBRARIES})
@@ -177,7 +183,7 @@ if(KPXC_FEATURE_NETWORK)
add_unit_test(NAME testupdatecheck SOURCES TestUpdateCheck.cpp
LIBS ${TEST_LIBRARIES})
- add_unit_test(NAME testicondownloader SOURCES TestIconDownloader.cpp
+ add_unit_test(NAME testicondownloader SOURCES TestIconDownloader.cpp
LIBS ${TEST_LIBRARIES})
endif()
diff --git a/tests/TestAutoTypeMac.cpp b/tests/TestAutoTypeMac.cpp
new file mode 100644
index 0000000000..7c85305ca0
--- /dev/null
+++ b/tests/TestAutoTypeMac.cpp
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2025 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "TestAutoTypeMac.h"
+
+#include "autotype/mac/AutoTypeMac_p.h"
+
+#include
+#include
+
+void TestAutoTypeMac::initTestCase()
+{
+ QLocale::setDefault(QLocale::c());
+}
+
+// Forward lookup: produce a glyph from a keycode + UCKeyTranslate modifier state
+static bool
+translateKeyCode(const UCKeyboardLayout* layout, UInt32 keyboardType, uint16_t keyCode, UInt32 mods, QChar& outGlyph)
+{
+ UInt32 deadKeyState = 0;
+ UniCharCount actualLen = 0;
+ UniChar unicode[4];
+
+ OSStatus status = UCKeyTranslate(layout,
+ keyCode,
+ kUCKeyActionDown,
+ mods,
+ keyboardType,
+ kUCKeyTranslateNoDeadKeysBit,
+ &deadKeyState,
+ 4,
+ &actualLen,
+ unicode);
+ if (status != noErr || actualLen != 1) {
+ return false;
+ }
+ outGlyph = QChar(unicode[0]);
+ return true;
+}
+
+// Load a specific keyboard layout by its input source ID (e.g. "com.apple.keylayout.US")
+static const UCKeyboardLayout* layoutById(const char* sourceId)
+{
+ CFStringRef idRef = CFStringCreateWithCString(kCFAllocatorDefault, sourceId, kCFStringEncodingUTF8);
+ CFStringRef key = kTISPropertyInputSourceID;
+ CFDictionaryRef filter = CFDictionaryCreate(kCFAllocatorDefault,
+ reinterpret_cast(&key),
+ reinterpret_cast(&idRef),
+ 1,
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks);
+ CFArrayRef sources = TISCreateInputSourceList(filter, true);
+ CFRelease(idRef);
+ CFRelease(filter);
+ if (!sources || CFArrayGetCount(sources) == 0) {
+ if (sources)
+ CFRelease(sources);
+ return nullptr;
+ }
+
+ TISInputSourceRef keyboard = static_cast(const_cast(CFArrayGetValueAtIndex(sources, 0)));
+ CFDataRef layoutData =
+ static_cast(TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData));
+ const UCKeyboardLayout* layout = nullptr;
+ if (layoutData) {
+ layout = reinterpret_cast(CFDataGetBytePtr(layoutData));
+ }
+ CFRelease(sources);
+ return layout;
+}
+
+void TestAutoTypeMac::testReverseLookupRoundTrip()
+{
+ auto layout = layoutById("com.apple.keylayout.US");
+ QVERIFY2(layout, "U.S. keyboard layout not available");
+ UInt32 kbType = LMGetKbdType();
+
+ static const UInt32 kUCTShift = shiftKey >> 8;
+ static const UInt32 kUCTOption = optionKey >> 8;
+ static const UInt32 mods[] = {0, kUCTShift, kUCTOption, kUCTShift | kUCTOption};
+
+ for (uint16_t keyCode = 0; keyCode < 128; keyCode++) {
+ for (UInt32 mod : mods) {
+ QChar glyph;
+ if (!translateKeyCode(layout, kbType, keyCode, mod, glyph)) {
+ continue;
+ }
+
+ // Reverse-lookup the glyph
+ uint16_t resolvedKeyCode = 0;
+ CGEventFlags resolvedFlags = 0;
+ QVERIFY2(charToNativeKeyCode(layout, kbType, glyph, resolvedKeyCode, resolvedFlags),
+ qPrintable(QString("charToNativeKeyCode returned false for '%1' (U+%2)")
+ .arg(glyph)
+ .arg(static_cast(glyph.unicode()), 4, 16, QChar('0'))));
+
+ // Feed the resolved keycode+flags back through UCKeyTranslate
+ // and verify we get the same glyph
+ UInt32 resolvedMods = 0;
+ if (resolvedFlags & kCGEventFlagMaskShift) {
+ resolvedMods |= kUCTShift;
+ }
+ if (resolvedFlags & kCGEventFlagMaskAlternate) {
+ resolvedMods |= kUCTOption;
+ }
+
+ QChar roundTripGlyph;
+ QVERIFY2(translateKeyCode(layout, kbType, resolvedKeyCode, resolvedMods, roundTripGlyph),
+ qPrintable(QString("Round-trip UCKeyTranslate failed for '%1'").arg(glyph)));
+ QCOMPARE(roundTripGlyph, glyph);
+ }
+ }
+}
+
+void TestAutoTypeMac::testUnmappableReturnsFalse()
+{
+ auto layout = layoutById("com.apple.keylayout.US");
+ QVERIFY2(layout, "U.S. keyboard layout not available");
+ UInt32 kbType = LMGetKbdType();
+
+ uint16_t keyCode = 0;
+ CGEventFlags flags = 0;
+ // U+4E2D (中, CJK ideograph) — not a single keystroke on U.S. layout
+ QVERIFY(!charToNativeKeyCode(layout, kbType, QChar(0x4E2D), keyCode, flags));
+}
+
+void TestAutoTypeMac::testModifierMapping()
+{
+ auto layout = layoutById("com.apple.keylayout.US");
+ QVERIFY2(layout, "U.S. keyboard layout not available");
+ UInt32 kbType = LMGetKbdType();
+
+ // 'A' (uppercase) should require shift
+ uint16_t keyCode = 0;
+ CGEventFlags flags = 0;
+ QVERIFY(charToNativeKeyCode(layout, kbType, QChar('A'), keyCode, flags));
+ QVERIFY2(flags & kCGEventFlagMaskShift,
+ qPrintable(QString("Expected Shift flag for 'A', got flags=0x%1").arg(flags, 0, 16)));
+
+ // 'a' (lowercase) should not require shift
+ flags = 0;
+ QVERIFY(charToNativeKeyCode(layout, kbType, QChar('a'), keyCode, flags));
+ QVERIFY2(!(flags & kCGEventFlagMaskShift),
+ qPrintable(QString("Expected no Shift flag for 'a', got flags=0x%1").arg(flags, 0, 16)));
+}
+
+QTEST_GUILESS_MAIN(TestAutoTypeMac)
diff --git a/tests/TestAutoTypeMac.h b/tests/TestAutoTypeMac.h
new file mode 100644
index 0000000000..839f62d68e
--- /dev/null
+++ b/tests/TestAutoTypeMac.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_TESTAUTOTYPEMAC_H
+#define KEEPASSXC_TESTAUTOTYPEMAC_H
+
+#include
+
+class TestAutoTypeMac : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase();
+ void testReverseLookupRoundTrip();
+ void testUnmappableReturnsFalse();
+ void testModifierMapping();
+};
+
+#endif // KEEPASSXC_TESTAUTOTYPEMAC_H