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