From b87097a7abbc676c46ff0a84c341e381506a9fcc Mon Sep 17 00:00:00 2001 From: Keith Bennett Date: Sat, 22 Mar 2014 16:29:16 +0000 Subject: [PATCH] Added global autotype support for OSX. --- src/autotype/CMakeLists.txt | 4 + src/autotype/mac/AutoTypeMac.cpp | 551 +++++++++++++++++++++++++++++++ src/autotype/mac/AutoTypeMac.h | 105 ++++++ src/autotype/mac/CMakeLists.txt | 18 + 4 files changed, 678 insertions(+) create mode 100644 src/autotype/mac/AutoTypeMac.cpp create mode 100644 src/autotype/mac/AutoTypeMac.h create mode 100644 src/autotype/mac/CMakeLists.txt diff --git a/src/autotype/CMakeLists.txt b/src/autotype/CMakeLists.txt index 0bf5fc252..37cb2f917 100644 --- a/src/autotype/CMakeLists.txt +++ b/src/autotype/CMakeLists.txt @@ -9,6 +9,10 @@ if(Q_WS_X11) endif() endif() +if(Q_WS_MAC) + add_subdirectory(mac) +endif() + if(WITH_TESTS) add_subdirectory(test) endif() diff --git a/src/autotype/mac/AutoTypeMac.cpp b/src/autotype/mac/AutoTypeMac.cpp new file mode 100644 index 000000000..d6f83b569 --- /dev/null +++ b/src/autotype/mac/AutoTypeMac.cpp @@ -0,0 +1,551 @@ +/* + * Copyright (C) 2012 Felix Geyer + * + * 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 "AutoTypeMac.h" +#include + +static pid_t keepassxPID2; + +#define UNICODE_BUFFER_SIZE 1 +static UniChar unicodeBuffer[UNICODE_BUFFER_SIZE]; +static UniCharCount unicodePtr = 0; +// reusable events +static CGEventRef unicodeEvent = CGEventCreateKeyboardEvent(NULL, 0, true); +CGEventRef AutoTypePlatformMac::keyEvent = + CGEventCreateKeyboardEvent(NULL, 0, true); + +bool AutoTypePlatformMac::inHotKeyEvent = false; + +static const KeycodeWithMods NoKeycodeWithMods = + (KeycodeWithMods){ NoKeycode, 0 }; + +static uint orderedModifiers[] = { + 0, + ( shiftKey ) >> 8, + (controlKey ) >> 8, + ( optionKey ) >> 8, + ( cmdKey ) >> 8, + ( shiftKey | controlKey ) >> 8, + ( shiftKey | optionKey ) >> 8, + ( shiftKey | cmdKey ) >> 8, + (controlKey | optionKey ) >> 8, + (controlKey | cmdKey ) >> 8, + ( optionKey | cmdKey ) >> 8, + ( shiftKey | controlKey | optionKey ) >> 8, + ( shiftKey | controlKey | cmdKey ) >> 8, + ( shiftKey | optionKey | cmdKey ) >> 8, + (controlKey | optionKey | cmdKey ) >> 8, + ( shiftKey | controlKey | optionKey | cmdKey) >> 8 +}; + +static std::map unicodeToKeycodeWithModsMap; + + +AutoTypePlatformMac::AutoTypePlatformMac() : first(true), keepassxPID(0) +{ + globalKey = 0; + globalMod = 0; + inGlobalAutoType = false; + + // initialize hot key handling + hotKeyRef = NULL; + hotKeyID.signature = 'kpsx'; + hotKeyID.id = 1; + EventTypeSpec eventType; + eventType.eventClass = kEventClassKeyboard; + eventType.eventKind = kEventHotKeyPressed; + InstallApplicationEventHandler(&hotKeyHandler, 1, &eventType, this, NULL); + AutoTypePlatformMac::initUnicodeToKeycodeWithModsMap(); +} + + +QStringList AutoTypePlatformMac::windowTitles() +{ + pid_t pid; + return getTargetWindowInfo(&pid, NULL); +} + + +WId AutoTypePlatformMac::activeWindow() +{ + WId mid; + pid_t pid; + getTargetWindowInfo(&pid, &mid); + AutoTypePlatformMac::processToFront(pid); + if (first) + first = false; + + return mid; +} + + +QString AutoTypePlatformMac::activeWindowTitle() +{ + QStringList sl = getTargetWindowInfo(NULL, NULL); + return sl[0]; +} + + +bool AutoTypePlatformMac::registerGlobalShortcut(Qt::Key key, + Qt::KeyboardModifiers modifiers) +{ + if (key == 0) + return false; + + //KeycodeWithMods kc = keyToKeycodeWithMods(key); + KeycodeWithMods kc = AutoTypePlatformMac::keysymToKeycodeWithMods2(static_cast(key)); + int code = kc.keycode; + uint mod = qtToNativeModifiers(modifiers); + + if (code==globalKey && mod==globalMod) + return true; + + // need to unregister old before registering new + unregisterGlobalShortcut(key, modifiers); + OSStatus status = RegisterEventHotKey(code, mod, hotKeyID, + GetApplicationEventTarget(), 0, &hotKeyRef); + + if (noErr == status) { + globalKey = code; + globalMod = mod; + return true; + } else { + qWarning("Error registering global shortcut: %d", status); + RegisterEventHotKey(globalKey, globalMod, hotKeyID, + GetApplicationEventTarget(), 0, &hotKeyRef); + return false; + } +} + + +void AutoTypePlatformMac::unregisterGlobalShortcut(Qt::Key, + Qt::KeyboardModifiers) +{ + globalKey = 0; + globalMod = 0; + UnregisterEventHotKey(hotKeyRef); +} + + +int AutoTypePlatformMac::platformEventFilter(void* event) +{ + return -1; +} + + +int AutoTypePlatformMac::initialTimeout() +{ + first = true; + return 500; +} + + +AutoTypeExecutor* AutoTypePlatformMac::createExecutor() +{ + return new AutoTypeExecturorMac(this); +} + + +void AutoTypePlatformMac::sendUnicode(KeySym keysym) +{ + if (onlySendKeycodes) { + KeycodeWithMods keycodeWithMods = + AutoTypePlatformMac::keysymToKeycodeWithMods2(keysym); + if (NoKeycode == keycodeWithMods.keycode) return; + sendKeycode(keycodeWithMods); + return; + } + unicodeBuffer[unicodePtr++] = keysym; + if (UNICODE_BUFFER_SIZE == unicodePtr) flushUnicode(); +} + + +void AutoTypePlatformMac::sendKeycode(KeycodeWithMods keycodeWithMods) +{ + flushUnicode(); + uint keycode = keycodeWithMods.keycode; + uint mods = keycodeWithMods.mods << 8; + uint flags = 0; + if (0 != ( shiftKey & mods)) flags |= kCGEventFlagMaskShift; + if (0 != (controlKey & mods)) flags |= kCGEventFlagMaskControl; + if (0 != ( optionKey & mods)) flags |= kCGEventFlagMaskAlternate; + if (0 != ( cmdKey & mods)) flags |= kCGEventFlagMaskCommand; + CGEventSetIntegerValueField(keyEvent, kCGKeyboardEventKeycode, keycode); + CGEventSetFlags(AutoTypePlatformMac::keyEvent, flags); + keyDownUp(AutoTypePlatformMac::keyEvent); + sleepKeyStrokeDelay(); +} + + +KeycodeWithMods AutoTypePlatformMac::keyToKeycodeWithMods(Qt::Key key) +{ + KeycodeWithMods kc; + kc.mods = 0; + + switch (key) { + case Qt::Key_Tab: + kc.keycode = kVK_Tab; + break; + case Qt::Key_Enter: + kc.keycode = kVK_Return; + break; + case Qt::Key_Up: + kc.keycode = kVK_UpArrow; + break; + case Qt::Key_Down: + kc.keycode = kVK_DownArrow; + break; + case Qt::Key_Left: + kc.keycode = kVK_LeftArrow; + break; + case Qt::Key_Right: + kc.keycode = kVK_RightArrow; + break; + case Qt::Key_Insert: + kc.keycode = kVK_Help; + break; + case Qt::Key_Delete: + kc.keycode = kVK_ForwardDelete; + break; + case Qt::Key_Home: + kc.keycode = kVK_Home; + break; + case Qt::Key_End: + kc.keycode = kVK_End; + break; + case Qt::Key_PageUp: + kc.keycode = kVK_PageUp; + break; + case Qt::Key_PageDown: + kc.keycode = kVK_PageDown; + break; + case Qt::Key_Backspace: + kc.keycode = kVK_Delete; + break; + //case Qt::Key_Pause: + // return XK_Break; + case Qt::Key_CapsLock: + kc.keycode = kVK_CapsLock; + break; + case Qt::Key_Escape: + kc.keycode = kVK_Escape; + break; + case Qt::Key_Help: + kc.keycode = kVK_Help; + break; + case Qt::Key_NumLock: + kc.keycode = kVK_ANSI_KeypadClear; + break; + case Qt::Key_Print: + kc.keycode = kVK_F13; + break; + //case Qt::Key_ScrollLock: + // return XK_Scroll_Lock; + default: + if (key >= Qt::Key_F1 && key <= Qt::Key_F16) + kc.keycode = kVK_F1 + key - Qt::Key_F1; + else + kc.keycode = NoSymbol; + } + + return kc; +} + +// Private + +QStringList AutoTypePlatformMac::getTargetWindowInfo(pid_t *pidPtr, + WId *windowNumberPtr) +{ + QStringList titles; + + const int maxWindowNameSize = 512; + char windowName[maxWindowNameSize]; + + onlySendKeycodes = false; + + CFArrayRef windowInfo = CGWindowListCopyWindowInfo( + kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, + 0); + CFIndex windowCount = CFArrayGetCount(windowInfo); + + for (CFIndex i = 0; i < windowCount; i++) { + CFDictionaryRef window = static_cast + (CFArrayGetValueAtIndex(windowInfo, i)); + + // only want windows in layer 0 + CFNumberRef windowLayerRef = static_cast + (CFDictionaryGetValue(window, kCGWindowLayer)); + + int windowLayer = -1; + CFNumberGetValue(windowLayerRef, kCFNumberIntType, &windowLayer); + if (0 != windowLayer) continue; + + // get the pid owning this window + CFNumberRef pidRef = static_cast + (CFDictionaryGetValue(window, kCGWindowOwnerPID)); + pid_t pid = -1; + CFNumberGetValue(pidRef, kCFNumberIntType, &pid); + + // skip KeePassX windows + if (getKeepassxPID() == pid) continue; + + // get window name; continue if no name + CFStringRef windowNameRef = static_cast + (CFDictionaryGetValue(window, kCGWindowName)); + if (!windowNameRef) continue; + + windowName[0] = 0; + if (!CFStringGetCString(windowNameRef, windowName, + maxWindowNameSize, kCFStringEncodingUTF8) || + (0 == windowName[0])) continue; + + if (NULL != pidPtr) + *pidPtr = pid; + + if (NULL != windowNumberPtr) { + CGWindowID wid; + CFNumberRef windowNumberRef = static_cast + (CFDictionaryGetValue(window, kCGWindowNumber)); + CFNumberGetValue(windowNumberRef, kCGWindowIDCFNumberType, &wid); + *windowNumberPtr = wid; + return titles; + } + titles.append(QString(windowName)); + } + + return titles; +} + + +uint AutoTypePlatformMac::qtToNativeModifiers(Qt::KeyboardModifiers modifiers) +{ + uint nativeModifiers = 0; + + if (modifiers & Qt::ShiftModifier) { + nativeModifiers |= shiftKey; + } + if (modifiers & Qt::ControlModifier) { + nativeModifiers |= controlKey; + } + if (modifiers & Qt::AltModifier) { + nativeModifiers |= optionKey; + } + if (modifiers & Qt::MetaModifier) { + nativeModifiers |= cmdKey; + } + + return nativeModifiers; +} + + +void AutoTypePlatformMac::flushUnicode() +{ + if (0 == unicodePtr) return; + CGEventKeyboardSetUnicodeString(unicodeEvent, unicodePtr, unicodeBuffer); + keyDownUp(unicodeEvent); + unicodePtr = 0; + sleepKeyStrokeDelay(); +} + + +void AutoTypePlatformMac::keyDownUp(CGEventRef theEvent) +{ + // posting Key Down/Up events also annoyingly sets mouse location so mus + // current mouse location and set it in the event + CGEventRef eventLocation = CGEventCreate(NULL); + CGEventSetLocation(theEvent, CGEventGetLocation(eventLocation)); + CFRelease(eventLocation); + + CGEventSetType(theEvent, kCGEventKeyDown); + CGEventPost(kCGHIDEventTap, theEvent); + CGEventSetType(theEvent, kCGEventKeyUp); + CGEventPost(kCGHIDEventTap, theEvent); +} + + +void AutoTypePlatformMac::sleepTime(int msec) +{ + if (msec == 0) return; + + timespec timeOut, remains; + timeOut.tv_sec = msec/1000; + timeOut.tv_nsec = (msec%1000)*1000000; + nanosleep(&timeOut, &remains); +} + + +OSStatus AutoTypePlatformMac::hotKeyHandler(EventHandlerCallRef, EventRef, + void *userData) +{ + // ignore nextHandler - should not be called + if ((inHotKeyEvent) || + AutoTypePlatformMac::isFrontProcess(AutoTypePlatformMac::getKeepassxPID2())) return noErr; + + AutoTypePlatformMac::inHotKeyEvent = true; + Q_EMIT static_cast(userData)->globalShortcutTriggered(); + AutoTypePlatformMac::inHotKeyEvent = false; + return noErr; +} + + +pid_t AutoTypePlatformMac::getKeepassxPID() { + + if (0 == keepassxPID) { + ProcessSerialNumber processSerialNumber; + GetCurrentProcess(&processSerialNumber); + GetProcessPID(&processSerialNumber, &keepassxPID); + } + + return keepassxPID; +} + + +Boolean AutoTypePlatformMac::isFrontProcess(pid_t pid) +{ + Boolean result; + ProcessSerialNumber pidPSN; + ProcessSerialNumber frontPSN; + OSStatus status = GetProcessForPID(pid, &pidPSN); + if (noErr != status) { + qWarning("AutoTypePlatformMac::isFrontProcess: GetProcessForPID " + "error for pid %d: %d", pid, status); + return false; + } + GetFrontProcess(&frontPSN); + SameProcess(&pidPSN, &frontPSN, &result); + return result; +} + + +void AutoTypePlatformMac::initUnicodeToKeycodeWithModsMap() +{ + unicodeToKeycodeWithModsMap.clear(); + TISInputSourceRef inputSourceRef = TISCopyCurrentKeyboardInputSource(); + + if (NULL == inputSourceRef) { + qWarning("AutoTypePlatformMac::initUnicodeToKeycodeWithModsMap: " + "inputSourceRef is NULL"); + return; + } + + CFDataRef unicodeKeyLayoutDataRef = static_cast + (TISGetInputSourceProperty(inputSourceRef, + kTISPropertyUnicodeKeyLayoutData)); + + if (NULL == unicodeKeyLayoutDataRef) { + qWarning("AutoTypePlatformMac::initUnicodeToKeycodeWithModsMap: " + "unicodeKeyLayoutDataRef is NULL"); + return; + } + const UCKeyboardLayout *unicodeKeyLayoutDataPtr = + reinterpret_cast + (CFDataGetBytePtr(unicodeKeyLayoutDataRef)); + + UInt32 deadKeyState; + UniChar unicodeString[8]; + UniCharCount len; + for (int m = 0; m < 16; m++) { + uint mods = orderedModifiers[m]; + for (uint keycode = 0; keycode < 0x80; keycode++) { + deadKeyState = 0; + len = 0; + OSStatus status = UCKeyTranslate(unicodeKeyLayoutDataPtr, keycode, + kUCKeyActionDown, mods, LMGetKbdType(), + kUCKeyTranslateNoDeadKeysMask, &deadKeyState, + sizeof(unicodeString), &len, unicodeString); + + if (noErr != status) { + qWarning("AutoTypePlatformMac::initUnicodeToKeycodeWithModsMap: " + "UCKeyTranslate error: %d keycode 0x%02X modifiers 0x%02X", + status, keycode, mods); + continue; + } + + // store if only one char and not already in store + if ((1 != len) || + (0 < unicodeToKeycodeWithModsMap.count(unicodeString[0]))) + continue; + + KeycodeWithMods kc; + kc.keycode = keycode; + kc.mods = mods; + unicodeToKeycodeWithModsMap[unicodeString[0]] = kc; + } + } +} + + +void AutoTypePlatformMac::processToFront(pid_t pid) +{ + OSStatus status; + ProcessSerialNumber processSerialNumber; + status = GetProcessForPID(pid, &processSerialNumber); + + if (noErr != status) { + qWarning("AutoTypePlatformMac::processToFront: GetProcessForPID " + "error for pid %d: %d", pid, status); + return; + } + + status = SetFrontProcessWithOptions(&processSerialNumber, + kSetFrontProcessFrontWindowOnly); + + if (noErr != status) { + qWarning("AutoTypePlatformMac::processToFront: " + "SetFrontProcessWithOptions for pid %d: %d", pid, status); + return; + } +} + + +KeycodeWithMods AutoTypePlatformMac::keysymToKeycodeWithMods2(KeySym keysym) +{ + return 0 == unicodeToKeycodeWithModsMap.count(keysym) + ? NoKeycodeWithMods : unicodeToKeycodeWithModsMap[keysym]; +} + + +pid_t AutoTypePlatformMac::getKeepassxPID2() +{ + if (0 == keepassxPID2) { + ProcessSerialNumber processSerialNumber; + GetCurrentProcess(&processSerialNumber); + GetProcessPID(&processSerialNumber, &keepassxPID2); + } + return keepassxPID2; +} + + +AutoTypeExecturorMac::AutoTypeExecturorMac(AutoTypePlatformMac* platform) + : m_platform(platform) +{ +} + + +void AutoTypeExecturorMac::execChar(AutoTypeChar* action) +{ + m_platform->sendUnicode(action->character.unicode()); +} + + +void AutoTypeExecturorMac::execKey(AutoTypeKey* action) +{ + m_platform->sendKeycode(m_platform->keyToKeycodeWithMods(action->key)); +} + + +Q_EXPORT_PLUGIN2(keepassx-autotype-mac, AutoTypePlatformMac) diff --git a/src/autotype/mac/AutoTypeMac.h b/src/autotype/mac/AutoTypeMac.h new file mode 100644 index 000000000..28eee1dcd --- /dev/null +++ b/src/autotype/mac/AutoTypeMac.h @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2012 Felix Geyer + * + * 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 KEEPASSX_AUTOTYPEMAC_H +#define KEEPASSX_AUTOTYPEMAC_H + +#include +#include + +#include "autotype/AutoTypePlatformPlugin.h" +#include "autotype/AutoTypeAction.h" +#include "core/Global.h" + +typedef quint32 KeySym; +#define NoSymbol static_cast(0) +#define NoKeycode static_cast(-1) + +struct KeycodeWithMods { + uint16 keycode; + uint16 mods; +}; + +class AutoTypePlatformMac : public QObject, public AutoTypePlatformInterface +{ + Q_OBJECT + Q_INTERFACES(AutoTypePlatformInterface) + +public: + AutoTypePlatformMac(); + + QStringList windowTitles(); + WId activeWindow(); + QString activeWindowTitle(); + bool registerGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers); + void unregisterGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers); + int platformEventFilter(void* event); + int initialTimeout(); + AutoTypeExecutor* createExecutor(); + + void sendUnicode(KeySym keysym); + void sendKeycode(KeycodeWithMods keycodeWithMods); + KeycodeWithMods keyToKeycodeWithMods(Qt::Key key); + +Q_SIGNALS: + void globalShortcutTriggered(); + +private: + QStringList getTargetWindowInfo(pid_t *pidPtr, WId *windowNumberPtr); + uint qtToNativeModifiers(Qt::KeyboardModifiers modifiers); + void flushUnicode(); + void keyDownUp(CGEventRef theEvent); + void sleepTime(int msec); + inline void sleepKeyStrokeDelay(){ sleepTime(5); }; + static OSStatus hotKeyHandler(EventHandlerCallRef, + EventRef, void *userData); + pid_t getKeepassxPID(); + + bool first; + bool onlySendKeycodes; + bool inGlobalAutoType; + int globalKey; + uint globalMod; + pid_t keepassxPID; + EventHotKeyRef hotKeyRef; + EventHotKeyID hotKeyID; + + static bool inHotKeyEvent; + static CGEventRef keyEvent; + +private: + static void initUnicodeToKeycodeWithModsMap(); + static void processToFront(pid_t pid); + static KeycodeWithMods keysymToKeycodeWithMods2(KeySym keysym); + static Boolean isFrontProcess(pid_t pid); + static pid_t getKeepassxPID2(); +}; + + +class AutoTypeExecturorMac : public AutoTypeExecutor +{ +public: + explicit AutoTypeExecturorMac(AutoTypePlatformMac* platform); + + void execChar(AutoTypeChar* action); + void execKey(AutoTypeKey* action); + +private: + AutoTypePlatformMac* const m_platform; +}; + +#endif // KEEPASSX_AUTOTYPEMAC_H diff --git a/src/autotype/mac/CMakeLists.txt b/src/autotype/mac/CMakeLists.txt new file mode 100644 index 000000000..c85e40e17 --- /dev/null +++ b/src/autotype/mac/CMakeLists.txt @@ -0,0 +1,18 @@ +set(autotype_mac_SOURCES + AutoTypeMac.cpp +) + +set(autotype_mac_MOC + AutoTypeMac.h +) + +qt4_wrap_cpp(autotype_mac_SOURCES ${autotype_mac_MOC}) + +add_library(keepassx-autotype-mac MODULE ${autotype_mac_SOURCES}) +if(APPLE) + set_target_properties(keepassx-autotype-mac PROPERTIES LINK_FLAGS "-framework Foundation -framework AppKit -framework Carbon") +endif() +target_link_libraries(keepassx-autotype-mac testautotype ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY}) +install(TARGETS keepassx-autotype-mac + BUNDLE DESTINATION . COMPONENT Runtime + LIBRARY DESTINATION ${PLUGIN_INSTALL_DIR} COMPONENT Runtime)