diff --git a/.travis.yml b/.travis.yml index cd2eb406e..56496ea85 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,12 @@ compiler: git: depth: 3 - + +matrix: + include: + - env: BUILD=1 + services: [docker] + before_install: - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get -qq update; fi - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get -qq install cmake libmicrohttpd10 libmicrohttpd-dev libxi-dev qtbase5-dev libqt5x11extras5-dev qttools5-dev qttools5-dev-tools libgcrypt20-dev zlib1g-dev libxtst-dev xvfb; fi @@ -20,14 +25,18 @@ before_install: - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew ls | grep -wq cmake || brew install cmake; fi - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew ls | grep -wq qt5 || brew install qt5; fi - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew ls | grep -wq libgcrypt || brew install libgcrypt; fi - + before_script: - if [ "$TRAVIS_OS_NAME" = "osx" ]; then CMAKE_ARGS="-DCMAKE_PREFIX_PATH=/usr/local/opt/qt5"; fi - mkdir build && pushd build - + script: - cmake -DCMAKE_BUILD_TYPE=Release -DWITH_GUI_TESTS=ON $CMAKE_ARGS .. - make - if [ "$TRAVIS_OS_NAME" = "linux" ]; then make test ARGS+="-E testgui --output-on-failure"; fi - if [ "$TRAVIS_OS_NAME" = "linux" ]; then xvfb-run -a --server-args="-screen 0 800x600x24" make test ARGS+="-R testgui --output-on-failure"; fi - if [ "$TRAVIS_OS_NAME" = "osx" ]; then make test ARGS+="--output-on-failure"; fi + +after_success: + - popd + - if [ ! -z "$BUILD" ]; then docker run -v $(pwd):/cwd snapcore/snapcraft sh -c 'cd /cwd && apt update && snapcraft'; fi diff --git a/.tx/config b/.tx/config index 84f80cc6f..3fcd5b969 100644 --- a/.tx/config +++ b/.tx/config @@ -1,7 +1,7 @@ [main] host = https://www.transifex.com -[keepassx-reboot.keepassx_ents] +[keepassxc.keepassx_ents] source_file = share/translations/keepassx_en.ts file_filter = share/translations/keepassx_.ts source_lang = en diff --git a/CMakeLists.txt b/CMakeLists.txt index 56128fe2b..cbef4bc45 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,7 +142,7 @@ if(MINGW) set(DATA_INSTALL_DIR "share") elseif(APPLE) set(BIN_INSTALL_DIR ".") - set(PLUGIN_INSTALL_DIR ".") + set(PLUGIN_INSTALL_DIR "${PROGNAME}.app/Contents/PlugIns") set(DATA_INSTALL_DIR "${PROGNAME}.app/Contents/Resources") else() include(GNUInstallDirs) diff --git a/setup/gui/keepassxc.desktop b/setup/gui/keepassxc.desktop new file mode 100644 index 000000000..bad5a8735 --- /dev/null +++ b/setup/gui/keepassxc.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Name=KeePassXC +GenericName=Community Password Manager +GenericName[de]=Passwortverwaltung +GenericName[es]=Gestor de contraseñas +GenericName[fr]=Gestionnaire de mot de passe +GenericName[ru]=менеджер паролей +Exec=keepassxc %f +Icon=${SNAP}/share/icons/hicolor/256x256/apps/keepassxc.png +Terminal=false +Type=Application +Categories=Qt;Utility; +MimeType=application/x-keepass2; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3d3d3f5f8..eebe06f74 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -105,6 +105,7 @@ set(keepassx_SOURCES gui/SearchWidget.cpp gui/SortFilterHideProxyModel.cpp gui/UnlockDatabaseWidget.cpp + gui/UnlockDatabaseDialog.cpp gui/WelcomeWidget.cpp gui/entry/AutoTypeAssociationsModel.cpp gui/entry/EditEntryWidget.cpp @@ -216,16 +217,26 @@ if(APPLE) set(CPACK_PACKAGE_FILE_NAME "${PROGNAME}-${KEEPASSXC_VERSION_NUM}") include(CPack) - include(DeployQt4) - install_qt4_executable(${PROGNAME}.app "qjpeg;qgif;qico;qtaccessiblewidgets") + if(NOT DEFINED QT_BINARY_DIR) + set(QT_BINARY_DIR "/usr/local/opt/qt5/bin" CACHE PATH "QT binary folder") + endif() + add_custom_command(TARGET ${PROGNAME} + POST_BUILD + COMMAND ${QT_BINARY_DIR}/macdeployqt ${PROGNAME}.app + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/src + COMMENT "Deploying app bundle") endif() -if(MINGW ) +if(MINGW) set(CPACK_GENERATOR "ZIP") set(CPACK_STRIP_FILES ON) set(CPACK_PACKAGE_FILE_NAME "${PROGNAME}-${KEEPASSXC_VERSION_NUM}") include(CPack) + install(CODE " + set(gp_tool \"objdump\") + " COMPONENT Runtime) + include(DeployQt4) install_qt4_executable(${PROGNAME}.exe "qjpeg;qgif;qico;qtaccessiblewidgets") endif() diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index 7f14e5b09..696f5e1ba 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -147,7 +147,11 @@ void AutoType::performAutoType(const Entry* entry, QWidget* hideWindow, const QS } if (hideWindow) { +#if defined(Q_OS_MAC) + m_plugin->raiseLastActiveWindow(); +#else hideWindow->showMinimized(); +#endif } Tools::wait(m_plugin->initialTimeout()); @@ -188,9 +192,6 @@ void AutoType::performGlobalAutoType(const QList& dbList) QList entryList; QHash sequenceHash; - // TODO: Check if there are any active databases here, if not do nothing - // TODO: Check if all databases are locked, if so ask to unlock them - for (Database* db : dbList) { const QList dbEntries = db->rootGroup()->entriesRecursive(); for (Entry* entry : dbEntries) { @@ -220,6 +221,10 @@ void AutoType::performGlobalAutoType(const QList& dbList) SLOT(performAutoTypeFromGlobal(Entry*,QString))); connect(selectDialog, SIGNAL(rejected()), SLOT(resetInAutoType())); selectDialog->setEntries(entryList, sequenceHash); +#if defined(Q_OS_MAC) + m_plugin->raiseOwnWindow(); + Tools::wait(500); +#endif selectDialog->show(); // necessary when the main window is minimized selectDialog->activateWindow(); @@ -308,7 +313,7 @@ bool AutoType::parseActions(const QString& sequence, const Entry* entry, QList AutoType::createActionFromTemplate(const QString& tmpl, c else if (tmplName == "ptrsc") { list.append(new AutoTypeKey(Qt::Key_Print)); } - else if (tmplName == "scolllock") { + else if (tmplName == "scrolllock") { list.append(new AutoTypeKey(Qt::Key_ScrollLock)); } // Qt doesn't know about keypad keys so use the normal ones instead diff --git a/src/autotype/AutoTypePlatformPlugin.h b/src/autotype/AutoTypePlatformPlugin.h index dadc7a0d2..2945e98c5 100644 --- a/src/autotype/AutoTypePlatformPlugin.h +++ b/src/autotype/AutoTypePlatformPlugin.h @@ -39,6 +39,11 @@ public: virtual AutoTypeExecutor* createExecutor() = 0; +#if defined(Q_OS_MAC) + virtual bool raiseLastActiveWindow() = 0; + virtual bool raiseOwnWindow() = 0; +#endif + // implementations should also provide a globalShortcutTriggered() signal }; diff --git a/src/autotype/CMakeLists.txt b/src/autotype/CMakeLists.txt index 417e7af4c..40dd449db 100644 --- a/src/autotype/CMakeLists.txt +++ b/src/autotype/CMakeLists.txt @@ -10,11 +10,12 @@ if(UNIX AND NOT APPLE) if(X11_FOUND AND X11_Xi_FOUND AND X11_XTest_FOUND AND Qt5X11Extras_FOUND) add_subdirectory(xcb) endif() +elseif(APPLE) + add_subdirectory(mac) +elseif(WIN32) + add_subdirectory(windows) endif() if(WITH_TESTS) - # autotype non supported on Windows right now - if(UNIX) - add_subdirectory(test) - endif() -endif() \ No newline at end of file + add_subdirectory(test) +endif() diff --git a/src/autotype/mac/AppKit.h b/src/autotype/mac/AppKit.h new file mode 100644 index 000000000..114038d63 --- /dev/null +++ b/src/autotype/mac/AppKit.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 Lennart Glauer + * + * 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_APPKIT_H +#define KEEPASSX_APPKIT_H + +#include + +extern "C" { + +class AppKit +{ +public: + AppKit(); + ~AppKit(); + + pid_t lastActiveProcessId(); + pid_t activeProcessId(); + pid_t ownProcessId(); + bool activateProcess(pid_t pid); + +private: + void *self; +}; + +} // extern "C" + +#endif // KEEPASSX_APPKIT_H diff --git a/src/autotype/mac/AppKitImpl.h b/src/autotype/mac/AppKitImpl.h new file mode 100644 index 000000000..1b57d7b14 --- /dev/null +++ b/src/autotype/mac/AppKitImpl.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016 Lennart Glauer + * + * 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 . + */ + +#import "AppKit.h" + +#import +#import + +@interface AppKitImpl : NSObject + +@property (strong) NSRunningApplication *lastActiveApplication; + +- (pid_t) activeProcessId; +- (pid_t) ownProcessId; +- (bool) activateProcess:(pid_t) pid; + +@end diff --git a/src/autotype/mac/AppKitImpl.mm b/src/autotype/mac/AppKitImpl.mm new file mode 100644 index 000000000..ad600cb95 --- /dev/null +++ b/src/autotype/mac/AppKitImpl.mm @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2016 Lennart Glauer + * + * 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 . + */ + +#import "AppKitImpl.h" + +#import + +@implementation AppKitImpl + +AppKit::AppKit() +{ + self = [[AppKitImpl alloc] init]; + [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:static_cast(self) + selector:@selector(didDeactivateApplicationObserver:) + name:NSWorkspaceDidDeactivateApplicationNotification + object:nil]; +} + +AppKit::~AppKit() +{ + [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:static_cast(self)]; + [static_cast(self) dealloc]; +} + +// +// Update last active application property +// +- (void) didDeactivateApplicationObserver:(NSNotification *) notification +{ + NSDictionary *userInfo = notification.userInfo; + NSRunningApplication *app = userInfo[NSWorkspaceApplicationKey]; + + if (app.processIdentifier != [self ownProcessId]) { + self.lastActiveApplication = app; + } +} + +// +// Get process id of frontmost application (-> keyboard input) +// +- (pid_t) activeProcessId +{ + return [NSWorkspace sharedWorkspace].frontmostApplication.processIdentifier; +} + +// +// Get process id of own process +// +- (pid_t) ownProcessId +{ + return [NSProcessInfo processInfo].processIdentifier; +} + +// +// Activate application by process id +// +- (bool) activateProcess:(pid_t) pid +{ + NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:pid]; + + return [app activateWithOptions:NSApplicationActivateIgnoringOtherApps]; +} + +// +// ------------------------- C++ Trampolines ------------------------- +// + +pid_t AppKit::lastActiveProcessId() +{ + return [static_cast(self) lastActiveApplication].processIdentifier; +} + +pid_t AppKit::activeProcessId() +{ + return [static_cast(self) activeProcessId]; +} + +pid_t AppKit::ownProcessId() +{ + return [static_cast(self) ownProcessId]; +} + +bool AppKit::activateProcess(pid_t pid) +{ + return [static_cast(self) activateProcess:pid]; +} + +@end diff --git a/src/autotype/mac/AutoTypeMac.cpp b/src/autotype/mac/AutoTypeMac.cpp new file mode 100644 index 000000000..90563a23a --- /dev/null +++ b/src/autotype/mac/AutoTypeMac.cpp @@ -0,0 +1,488 @@ +/* + * Copyright (C) 2016 Lennart Glauer + * + * 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 + +#define HOTKEY_ID 1 +#define MAX_WINDOW_TITLE_LENGTH 1024 +#define INVALID_KEYCODE 0xFFFF + +AutoTypePlatformMac::AutoTypePlatformMac() + : m_appkit(new AppKit()) + , m_hotkeyRef(nullptr) + , m_hotkeyId({ 'kpx2', HOTKEY_ID }) +{ + EventTypeSpec eventSpec; + eventSpec.eventClass = kEventClassKeyboard; + eventSpec.eventKind = kEventHotKeyPressed; + + ::InstallApplicationEventHandler(AutoTypePlatformMac::hotkeyHandler, 1, &eventSpec, this, nullptr); +} + +// +// Keepassx requires mac os 10.7 +// +bool AutoTypePlatformMac::isAvailable() +{ + return true; +} + +// +// Get list of visible window titles +// see: Quartz Window Services +// +QStringList AutoTypePlatformMac::windowTitles() +{ + QStringList list; + + CFArrayRef windowList = ::CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID); + if (windowList != nullptr) { + CFIndex count = ::CFArrayGetCount(windowList); + + for (CFIndex i = 0; i < count; i++) { + CFDictionaryRef window = static_cast(::CFArrayGetValueAtIndex(windowList, i)); + if (windowLayer(window) != 0) { + continue; + } + + QString title = windowTitle(window); + if (!title.isEmpty()) { + list.append(title); + } + } + + ::CFRelease(windowList); + } + + return list; +} + +// +// Get active window process id +// +WId AutoTypePlatformMac::activeWindow() +{ + return m_appkit->activeProcessId(); +} + +// +// Get active window title +// see: Quartz Window Services +// +QString AutoTypePlatformMac::activeWindowTitle() +{ + QString title; + + CFArrayRef windowList = ::CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID); + if (windowList != nullptr) { + CFIndex count = ::CFArrayGetCount(windowList); + + for (CFIndex i = 0; i < count; i++) { + CFDictionaryRef window = static_cast(::CFArrayGetValueAtIndex(windowList, i)); + if (windowLayer(window) == 0) { + // First toplevel window in list (front to back order) + title = windowTitle(window); + break; + } + } + + ::CFRelease(windowList); + } + + return title; +} + +// +// Register global hotkey +// +bool AutoTypePlatformMac::registerGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) +{ + uint16 nativeKeyCode = qtToNativeKeyCode(key); + if (nativeKeyCode == INVALID_KEYCODE) { + qWarning("Invalid key code"); + return false; + } + uint16 nativeModifiers = qtToNativeModifiers(modifiers); + if (::RegisterEventHotKey(nativeKeyCode, nativeModifiers, m_hotkeyId, GetApplicationEventTarget(), 0, &m_hotkeyRef) != noErr) { + qWarning("Register hotkey failed"); + return false; + } + + return true; +} + +// +// Unregister global hotkey +// +void AutoTypePlatformMac::unregisterGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) +{ + Q_UNUSED(key); + Q_UNUSED(modifiers); + + ::UnregisterEventHotKey(m_hotkeyRef); +} + +int AutoTypePlatformMac::platformEventFilter(void* event) +{ + Q_UNUSED(event); + Q_ASSERT(false); + + return -1; +} + +AutoTypeExecutor* AutoTypePlatformMac::createExecutor() +{ + return new AutoTypeExecutorMac(this); +} + +int AutoTypePlatformMac::initialTimeout() +{ + return 500; +} + +// +// Activate window by process id +// +bool AutoTypePlatformMac::raiseWindow(WId pid) +{ + return m_appkit->activateProcess(pid); +} + +// +// Activate last active window +// +bool AutoTypePlatformMac::raiseLastActiveWindow() +{ + return m_appkit->activateProcess(m_appkit->lastActiveProcessId()); +} + +// +// Activate keepassx window +// +bool AutoTypePlatformMac::raiseOwnWindow() +{ + return m_appkit->activateProcess(m_appkit->ownProcessId()); +} + +// +// Send unicode character to active window +// see: Quartz Event Services +// +void AutoTypePlatformMac::sendChar(const QChar& ch, bool isKeyDown) +{ + CGEventRef keyEvent = ::CGEventCreateKeyboardEvent(nullptr, 0, isKeyDown); + if (keyEvent != nullptr) { + UniChar unicode = ch.unicode(); + ::CGEventKeyboardSetUnicodeString(keyEvent, 1, &unicode); + ::CGEventPost(kCGSessionEventTap, keyEvent); + ::CFRelease(keyEvent); + } +} + +// +// Send key code to active window +// see: Quartz Event Services +// +void AutoTypePlatformMac::sendKey(Qt::Key key, bool isKeyDown) +{ + uint16 keyCode = qtToNativeKeyCode(key); + if (keyCode == INVALID_KEYCODE) { + return; + } + + CGEventRef keyEvent = ::CGEventCreateKeyboardEvent(nullptr, keyCode, isKeyDown); + if (keyEvent != nullptr) { + ::CGEventPost(kCGSessionEventTap, keyEvent); + ::CFRelease(keyEvent); + } +} + +// +// Translate qt key code to mac os key code +// see: HIToolbox/Events.h +// +uint16 AutoTypePlatformMac::qtToNativeKeyCode(Qt::Key key) +{ + switch (key) { + case Qt::Key_A: + return kVK_ANSI_A; + case Qt::Key_B: + return kVK_ANSI_B; + case Qt::Key_C: + return kVK_ANSI_C; + case Qt::Key_D: + return kVK_ANSI_D; + case Qt::Key_E: + return kVK_ANSI_E; + case Qt::Key_F: + return kVK_ANSI_F; + case Qt::Key_G: + return kVK_ANSI_G; + case Qt::Key_H: + return kVK_ANSI_H; + case Qt::Key_I: + return kVK_ANSI_I; + case Qt::Key_J: + return kVK_ANSI_J; + case Qt::Key_K: + return kVK_ANSI_K; + case Qt::Key_L: + return kVK_ANSI_L; + case Qt::Key_M: + return kVK_ANSI_M; + case Qt::Key_N: + return kVK_ANSI_N; + case Qt::Key_O: + return kVK_ANSI_O; + case Qt::Key_P: + return kVK_ANSI_P; + case Qt::Key_Q: + return kVK_ANSI_Q; + case Qt::Key_R: + return kVK_ANSI_R; + case Qt::Key_S: + return kVK_ANSI_S; + case Qt::Key_T: + return kVK_ANSI_T; + case Qt::Key_U: + return kVK_ANSI_U; + case Qt::Key_V: + return kVK_ANSI_V; + case Qt::Key_W: + return kVK_ANSI_W; + case Qt::Key_X: + return kVK_ANSI_X; + case Qt::Key_Y: + return kVK_ANSI_Y; + case Qt::Key_Z: + return kVK_ANSI_Z; + + case Qt::Key_0: + return kVK_ANSI_0; + case Qt::Key_1: + return kVK_ANSI_1; + case Qt::Key_2: + return kVK_ANSI_2; + case Qt::Key_3: + return kVK_ANSI_3; + case Qt::Key_4: + return kVK_ANSI_4; + case Qt::Key_5: + return kVK_ANSI_5; + case Qt::Key_6: + return kVK_ANSI_6; + case Qt::Key_7: + return kVK_ANSI_7; + case Qt::Key_8: + return kVK_ANSI_8; + case Qt::Key_9: + return kVK_ANSI_9; + + case Qt::Key_Equal: + return kVK_ANSI_Equal; + case Qt::Key_Minus: + return kVK_ANSI_Minus; + case Qt::Key_BracketRight: + return kVK_ANSI_RightBracket; + case Qt::Key_BracketLeft: + return kVK_ANSI_LeftBracket; + case Qt::Key_QuoteDbl: + return kVK_ANSI_Quote; + case Qt::Key_Semicolon: + return kVK_ANSI_Semicolon; + case Qt::Key_Backslash: + return kVK_ANSI_Backslash; + case Qt::Key_Comma: + return kVK_ANSI_Comma; + case Qt::Key_Slash: + return kVK_ANSI_Slash; + case Qt::Key_Period: + return kVK_ANSI_Period; + + case Qt::Key_Backspace: + return kVK_Delete; + case Qt::Key_Tab: + case Qt::Key_Backtab: + return kVK_Tab; + case Qt::Key_Enter: + case Qt::Key_Return: + return kVK_Return; + case Qt::Key_CapsLock: + return kVK_CapsLock; + case Qt::Key_Escape: + return kVK_Escape; + case Qt::Key_Space: + return kVK_Space; + case Qt::Key_PageUp: + return kVK_PageUp; + case Qt::Key_PageDown: + return kVK_PageDown; + case Qt::Key_End: + return kVK_End; + case Qt::Key_Home: + return kVK_Home; + case Qt::Key_Left: + return kVK_LeftArrow; + case Qt::Key_Up: + return kVK_UpArrow; + case Qt::Key_Right: + return kVK_RightArrow; + case Qt::Key_Down: + return kVK_DownArrow; + case Qt::Key_Delete: + return kVK_ForwardDelete; + case Qt::Key_Help: + return kVK_Help; + + case Qt::Key_F1: + return kVK_F1; + case Qt::Key_F2: + return kVK_F2; + case Qt::Key_F3: + return kVK_F3; + case Qt::Key_F4: + return kVK_F4; + case Qt::Key_F5: + return kVK_F5; + case Qt::Key_F6: + return kVK_F6; + case Qt::Key_F7: + return kVK_F7; + case Qt::Key_F8: + return kVK_F8; + case Qt::Key_F9: + return kVK_F9; + case Qt::Key_F10: + return kVK_F10; + case Qt::Key_F11: + return kVK_F11; + case Qt::Key_F12: + return kVK_F12; + case Qt::Key_F13: + return kVK_F13; + case Qt::Key_F14: + return kVK_F14; + case Qt::Key_F15: + return kVK_F15; + case Qt::Key_F16: + return kVK_F16; + + default: + Q_ASSERT(false); + return INVALID_KEYCODE; + } +} + +// +// Translate qt key modifiers to mac os modifiers +// see: https://doc.qt.io/qt-5/osx-issues.html#special-keys +// +uint16 AutoTypePlatformMac::qtToNativeModifiers(Qt::KeyboardModifiers modifiers) +{ + uint16 nativeModifiers = 0; + + if (modifiers & Qt::ShiftModifier) { + nativeModifiers |= shiftKey; + } + if (modifiers & Qt::ControlModifier) { + nativeModifiers |= cmdKey; + } + if (modifiers & Qt::AltModifier) { + nativeModifiers |= optionKey; + } + if (modifiers & Qt::MetaModifier) { + nativeModifiers |= controlKey; + } + + return nativeModifiers; +} + +// +// Get window layer/level +// +int AutoTypePlatformMac::windowLayer(CFDictionaryRef window) +{ + int layer; + + CFNumberRef layerRef = static_cast(::CFDictionaryGetValue(window, kCGWindowLayer)); + if (layerRef != nullptr + && ::CFNumberGetValue(layerRef, kCFNumberIntType, &layer)) { + return layer; + } + + return -1; +} + +// +// Get window title +// +QString AutoTypePlatformMac::windowTitle(CFDictionaryRef window) +{ + char buffer[MAX_WINDOW_TITLE_LENGTH]; + QString title; + + CFStringRef titleRef = static_cast(::CFDictionaryGetValue(window, kCGWindowName)); + if (titleRef != nullptr + && ::CFStringGetCString(titleRef, buffer, MAX_WINDOW_TITLE_LENGTH, kCFStringEncodingUTF8)) { + title = QString::fromUtf8(buffer); + } + + return title; +} + +// +// Carbon hotkey handler +// +OSStatus AutoTypePlatformMac::hotkeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) +{ + Q_UNUSED(nextHandler); + + AutoTypePlatformMac *self = static_cast(userData); + EventHotKeyID hotkeyId; + + if (::GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, nullptr, sizeof(hotkeyId), nullptr, &hotkeyId) == noErr + && hotkeyId.id == HOTKEY_ID) { + Q_EMIT self->globalShortcutTriggered(); + } + + return noErr; +} + +// +// ------------------------------ AutoTypeExecutorMac ------------------------------ +// + +AutoTypeExecutorMac::AutoTypeExecutorMac(AutoTypePlatformMac* platform) + : m_platform(platform) +{ +} + +void AutoTypeExecutorMac::execChar(AutoTypeChar* action) +{ + m_platform->sendChar(action->character, true); + m_platform->sendChar(action->character, false); + usleep(25 * 1000); +} + +void AutoTypeExecutorMac::execKey(AutoTypeKey* action) +{ + m_platform->sendKey(action->key, true); + m_platform->sendKey(action->key, false); + usleep(25 * 1000); +} diff --git a/src/autotype/mac/AutoTypeMac.h b/src/autotype/mac/AutoTypeMac.h new file mode 100644 index 000000000..475a4b99b --- /dev/null +++ b/src/autotype/mac/AutoTypeMac.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 Lennart Glauer + * + * 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 + +#include "AppKit.h" +#include "autotype/AutoTypePlatformPlugin.h" +#include "autotype/AutoTypeAction.h" + +class AutoTypePlatformMac : public QObject, public AutoTypePlatformInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.keepassx.AutoTypePlatformMac") + Q_INTERFACES(AutoTypePlatformInterface) + +public: + AutoTypePlatformMac(); + bool isAvailable() override; + QStringList windowTitles() override; + WId activeWindow() override; + QString activeWindowTitle() override; + bool registerGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) override; + void unregisterGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) override; + int platformEventFilter(void* event) override; + int initialTimeout() override; + bool raiseWindow(WId pid) override; + AutoTypeExecutor* createExecutor() override; + + bool raiseLastActiveWindow() override; + bool raiseOwnWindow() override; + + void sendChar(const QChar& ch, bool isKeyDown); + void sendKey(Qt::Key key, bool isKeyDown); + +Q_SIGNALS: + void globalShortcutTriggered(); + +private: + std::unique_ptr m_appkit; + EventHotKeyRef m_hotkeyRef; + EventHotKeyID m_hotkeyId; + + static uint16 qtToNativeKeyCode(Qt::Key key); + static uint16 qtToNativeModifiers(Qt::KeyboardModifiers modifiers); + static int windowLayer(CFDictionaryRef window); + static QString windowTitle(CFDictionaryRef window); + static OSStatus hotkeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData); +}; + +class AutoTypeExecutorMac : public AutoTypeExecutor +{ +public: + explicit AutoTypeExecutorMac(AutoTypePlatformMac* platform); + + void execChar(AutoTypeChar* action) override; + void execKey(AutoTypeKey* action) override; + +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..076dd5038 --- /dev/null +++ b/src/autotype/mac/CMakeLists.txt @@ -0,0 +1,20 @@ +set(autotype_mac_SOURCES + AutoTypeMac.cpp +) + +set(autotype_mac_mm_SOURCES + AppKitImpl.mm +) + +add_library(keepassx-autotype-cocoa MODULE ${autotype_mac_SOURCES} ${autotype_mac_mm_SOURCES}) +set_target_properties(keepassx-autotype-cocoa PROPERTIES LINK_FLAGS "-framework Foundation -framework AppKit -framework Carbon") +target_link_libraries(keepassx-autotype-cocoa ${PROGNAME} Qt5::Core Qt5::Widgets) +if(NOT DEFINED QT_BINARY_DIR) + set(QT_BINARY_DIR "/usr/local/opt/qt5/bin" CACHE PATH "QT binary folder") +endif() +add_custom_command(TARGET keepassx-autotype-cocoa + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/libkeepassx-autotype-cocoa.so ${PLUGIN_INSTALL_DIR} + COMMAND ${QT_BINARY_DIR}/macdeployqt ${PROGNAME}.app -executable=${PLUGIN_INSTALL_DIR}/libkeepassx-autotype-cocoa.so -no-plugins + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/src +COMMENT "Deploying autotype plugin") diff --git a/src/autotype/test/AutoTypeTest.cpp b/src/autotype/test/AutoTypeTest.cpp index 979af8bde..1ac815b0e 100644 --- a/src/autotype/test/AutoTypeTest.cpp +++ b/src/autotype/test/AutoTypeTest.cpp @@ -115,6 +115,18 @@ bool AutoTypePlatformTest::raiseWindow(WId window) return false; } +#if defined(Q_OS_MAC) +bool AutoTypePlatformTest::raiseLastActiveWindow() +{ + return false; +} + +bool AutoTypePlatformTest::raiseOwnWindow() +{ + return false; +} +#endif + AutoTypeExecturorTest::AutoTypeExecturorTest(AutoTypePlatformTest* platform) : m_platform(platform) { diff --git a/src/autotype/test/AutoTypeTest.h b/src/autotype/test/AutoTypeTest.h index 8c6e52443..33487778a 100644 --- a/src/autotype/test/AutoTypeTest.h +++ b/src/autotype/test/AutoTypeTest.h @@ -46,6 +46,11 @@ public: bool raiseWindow(WId window) override; AutoTypeExecutor* createExecutor() override; +#if defined(Q_OS_MAC) + bool raiseLastActiveWindow() override; + bool raiseOwnWindow() override; +#endif + void setActiveWindowTitle(const QString& title) override; QString actionChars() override; diff --git a/src/autotype/windows/AutoTypeWindows.cpp b/src/autotype/windows/AutoTypeWindows.cpp new file mode 100644 index 000000000..481caa83f --- /dev/null +++ b/src/autotype/windows/AutoTypeWindows.cpp @@ -0,0 +1,529 @@ +/* + * Copyright (C) 2016 Lennart Glauer + * + * 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 "AutoTypeWindows.h" + +#include + +#define HOTKEY_ID 1 +#define MAX_WINDOW_TITLE_LENGTH 1024 + +#define MOD_NOREPEAT 0x4000 // Missing in MinGW + +// +// Test if os version is Windows 7 or later +// +bool AutoTypePlatformWin::isAvailable() +{ + return IsWindows7OrGreater(); +} + +// +// Get list of all visible window titles +// +QStringList AutoTypePlatformWin::windowTitles() +{ + QStringList list; + + ::EnumWindows(AutoTypePlatformWin::windowTitleEnumProc, reinterpret_cast(&list)); + + return list; +} + +// +// Get foreground window hwnd +// +WId AutoTypePlatformWin::activeWindow() +{ + return reinterpret_cast(::GetForegroundWindow()); +} + +// +// Get foreground window title +// +QString AutoTypePlatformWin::activeWindowTitle() +{ + return windowTitle(::GetForegroundWindow()); +} + +// +// Register global hotkey +// +bool AutoTypePlatformWin::registerGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) +{ + DWORD nativeKeyCode = qtToNativeKeyCode(key); + if (nativeKeyCode < 1 || nativeKeyCode > 254) { + return false; + } + DWORD nativeModifiers = qtToNativeModifiers(modifiers); + if (!::RegisterHotKey(nullptr, HOTKEY_ID, nativeModifiers | MOD_NOREPEAT, nativeKeyCode)) { + return false; + } + + return true; +} + +// +// Unregister global hotkey +// +void AutoTypePlatformWin::unregisterGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) +{ + Q_UNUSED(key); + Q_UNUSED(modifiers); + + ::UnregisterHotKey(nullptr, HOTKEY_ID); +} + +// +// Native event filter +// +int AutoTypePlatformWin::platformEventFilter(void* event) +{ + MSG *msg = static_cast(event); + + if (msg->message == WM_HOTKEY && msg->wParam == HOTKEY_ID) { + Q_EMIT globalShortcutTriggered(); + return 1; + } + + return -1; +} + +AutoTypeExecutor* AutoTypePlatformWin::createExecutor() +{ + return new AutoTypeExecutorWin(this); +} + +int AutoTypePlatformWin::initialTimeout() +{ + return 500; +} + +// +// Set foreground window +// +bool AutoTypePlatformWin::raiseWindow(WId window) +{ + HWND hwnd = reinterpret_cast(window); + + return ::BringWindowToTop(hwnd) && ::SetForegroundWindow(hwnd); +} + +// +// Send unicode character to foreground window +// +void AutoTypePlatformWin::sendChar(const QChar& ch, bool isKeyDown) +{ + DWORD nativeFlags = KEYEVENTF_UNICODE; + if (!isKeyDown) { + nativeFlags |= KEYEVENTF_KEYUP; + } + + INPUT in; + in.type = INPUT_KEYBOARD; + in.ki.wVk = 0; + in.ki.wScan = ch.unicode(); + in.ki.dwFlags = nativeFlags; + in.ki.time = 0; + in.ki.dwExtraInfo = ::GetMessageExtraInfo(); + + ::SendInput(1, &in, sizeof(INPUT)); +} + +// +// Send virtual key code to foreground window +// +void AutoTypePlatformWin::sendKey(Qt::Key key, bool isKeyDown) +{ + DWORD nativeKeyCode = qtToNativeKeyCode(key); + if (nativeKeyCode < 1 || nativeKeyCode > 254) { + return; + } + DWORD nativeFlags = 0; + if (isExtendedKey(nativeKeyCode)) { + nativeFlags |= KEYEVENTF_EXTENDEDKEY; + } + if (!isKeyDown) { + nativeFlags |= KEYEVENTF_KEYUP; + } + + INPUT in; + in.type = INPUT_KEYBOARD; + in.ki.wVk = LOWORD(nativeKeyCode); + in.ki.wScan = LOWORD(::MapVirtualKeyW(nativeKeyCode, MAPVK_VK_TO_VSC)); + in.ki.dwFlags = nativeFlags; + in.ki.time = 0; + in.ki.dwExtraInfo = ::GetMessageExtraInfo(); + + ::SendInput(1, &in, sizeof(INPUT)); +} + +// +// Translate qt key code to windows virtual key code +// see: https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx +// +DWORD AutoTypePlatformWin::qtToNativeKeyCode(Qt::Key key) +{ + switch (key) { + case Qt::Key_Backspace: + return VK_BACK; // 0x08 + case Qt::Key_Tab: + case Qt::Key_Backtab: + return VK_TAB; // 0x09 + case Qt::Key_Clear: + return VK_CLEAR; // 0x0C + case Qt::Key_Enter: + case Qt::Key_Return: + return VK_RETURN; // 0x0D + case Qt::Key_Pause: + return VK_PAUSE; // 0x13 + case Qt::Key_CapsLock: + return VK_CAPITAL; // 0x14 + case Qt::Key_Escape: + return VK_ESCAPE; // 0x1B + case Qt::Key_Space: + return VK_SPACE; // 0x20 + case Qt::Key_PageUp: + return VK_PRIOR; // 0x21 + case Qt::Key_PageDown: + return VK_NEXT; // 0x22 + case Qt::Key_End: + return VK_END; // 0x23 + case Qt::Key_Home: + return VK_HOME; // 0x24 + case Qt::Key_Left: + return VK_LEFT; // 0x25 + case Qt::Key_Up: + return VK_UP; // 0x26 + case Qt::Key_Right: + return VK_RIGHT; // 0x27 + case Qt::Key_Down: + return VK_DOWN; // 0x28 + case Qt::Key_Print: + return VK_SNAPSHOT; // 0x2C + case Qt::Key_Insert: + return VK_INSERT; // 0x2D + case Qt::Key_Delete: + return VK_DELETE; // 0x2E + case Qt::Key_Help: + return VK_HELP; // 0x2F + + case Qt::Key_0: + return 0x30; // 0x30 + case Qt::Key_1: + return 0x31; // 0x31 + case Qt::Key_2: + return 0x32; // 0x32 + case Qt::Key_3: + return 0x33; // 0x33 + case Qt::Key_4: + return 0x34; // 0x34 + case Qt::Key_5: + return 0x35; // 0x35 + case Qt::Key_6: + return 0x36; // 0x36 + case Qt::Key_7: + return 0x37; // 0x37 + case Qt::Key_8: + return 0x38; // 0x38 + case Qt::Key_9: + return 0x39; // 0x39 + + case Qt::Key_A: + return 0x41; // 0x41 + case Qt::Key_B: + return 0x42; // 0x42 + case Qt::Key_C: + return 0x43; // 0x43 + case Qt::Key_D: + return 0x44; // 0x44 + case Qt::Key_E: + return 0x45; // 0x45 + case Qt::Key_F: + return 0x46; // 0x46 + case Qt::Key_G: + return 0x47; // 0x47 + case Qt::Key_H: + return 0x48; // 0x48 + case Qt::Key_I: + return 0x49; // 0x49 + case Qt::Key_J: + return 0x4A; // 0x4A + case Qt::Key_K: + return 0x4B; // 0x4B + case Qt::Key_L: + return 0x4C; // 0x4C + case Qt::Key_M: + return 0x4D; // 0x4D + case Qt::Key_N: + return 0x4E; // 0x4E + case Qt::Key_O: + return 0x4F; // 0x4F + case Qt::Key_P: + return 0x50; // 0x50 + case Qt::Key_Q: + return 0x51; // 0x51 + case Qt::Key_R: + return 0x52; // 0x52 + case Qt::Key_S: + return 0x53; // 0x53 + case Qt::Key_T: + return 0x54; // 0x54 + case Qt::Key_U: + return 0x55; // 0x55 + case Qt::Key_V: + return 0x56; // 0x56 + case Qt::Key_W: + return 0x57; // 0x57 + case Qt::Key_X: + return 0x58; // 0x58 + case Qt::Key_Y: + return 0x59; // 0x59 + case Qt::Key_Z: + return 0x5A; // 0x5A + + case Qt::Key_F1: + return VK_F1; // 0x70 + case Qt::Key_F2: + return VK_F2; // 0x71 + case Qt::Key_F3: + return VK_F3; // 0x72 + case Qt::Key_F4: + return VK_F4; // 0x73 + case Qt::Key_F5: + return VK_F5; // 0x74 + case Qt::Key_F6: + return VK_F6; // 0x75 + case Qt::Key_F7: + return VK_F7; // 0x76 + case Qt::Key_F8: + return VK_F8; // 0x77 + case Qt::Key_F9: + return VK_F9; // 0x78 + case Qt::Key_F10: + return VK_F10; // 0x79 + case Qt::Key_F11: + return VK_F11; // 0x7A + case Qt::Key_F12: + return VK_F12; // 0x7B + case Qt::Key_F13: + return VK_F13; // 0x7C + case Qt::Key_F14: + return VK_F14; // 0x7D + case Qt::Key_F15: + return VK_F15; // 0x7E + case Qt::Key_F16: + return VK_F16; // 0x7F + case Qt::Key_F17: + return VK_F17; // 0x80 + case Qt::Key_F18: + return VK_F18; // 0x81 + case Qt::Key_F19: + return VK_F19; // 0x82 + case Qt::Key_F20: + return VK_F20; // 0x83 + case Qt::Key_F21: + return VK_F21; // 0x84 + case Qt::Key_F22: + return VK_F22; // 0x85 + case Qt::Key_F23: + return VK_F23; // 0x86 + case Qt::Key_F24: + return VK_F24; // 0x87 + + case Qt::Key_NumLock: + return VK_NUMLOCK; // 0x90 + case Qt::Key_ScrollLock: + return VK_SCROLL; // 0x91 + + case Qt::Key_Exclam: // ! + case Qt::Key_QuoteDbl: // " + case Qt::Key_NumberSign: // # + case Qt::Key_Dollar: // $ + case Qt::Key_Percent: // % + case Qt::Key_Ampersand: // & + case Qt::Key_Apostrophe: // ' + case Qt::Key_ParenLeft: // ( + case Qt::Key_ParenRight: // ) + case Qt::Key_Asterisk: // * + case Qt::Key_Plus: // + + case Qt::Key_Comma: // , + case Qt::Key_Minus: // - + case Qt::Key_Period: // . + case Qt::Key_Slash: // / + case Qt::Key_Colon: // : + case Qt::Key_Semicolon: // ; + case Qt::Key_Less: // < + case Qt::Key_Equal: // = + case Qt::Key_Greater: // > + case Qt::Key_Question: // ? + case Qt::Key_BracketLeft: // [ + case Qt::Key_Backslash: // '\' + case Qt::Key_BracketRight: // ] + case Qt::Key_AsciiCircum: // ^ + case Qt::Key_Underscore: // _ + case Qt::Key_QuoteLeft: // ` + case Qt::Key_BraceLeft: // { + case Qt::Key_Bar: // | + case Qt::Key_BraceRight: // } + case Qt::Key_AsciiTilde: // ~ + return LOBYTE(::VkKeyScanExW(key, ::GetKeyboardLayout(0))); + + default: + Q_ASSERT(false); + return 0; + } +} + +// +// The extended-key flag indicates whether the keystroke message originated +// from one of the additional keys on the enhanced keyboard +// see: https://msdn.microsoft.com/en-us/library/windows/desktop/ms646267%28v=vs.85%29.aspx#EXTENDED_KEY_FLAG +// +BOOL AutoTypePlatformWin::isExtendedKey(DWORD nativeKeyCode) +{ + switch (nativeKeyCode) { + case VK_RMENU: + case VK_RCONTROL: + case VK_INSERT: + case VK_DELETE: + case VK_HOME: + case VK_END: + case VK_PRIOR: + case VK_NEXT: + case VK_LEFT: + case VK_UP: + case VK_RIGHT: + case VK_DOWN: + case VK_NUMLOCK: + case VK_CANCEL: + case VK_SNAPSHOT: + case VK_DIVIDE: + case VK_LWIN: + case VK_RWIN: + case VK_APPS: + return TRUE; + default: + return FALSE; + } +} + +// +// Translate qt key modifiers to windows modifiers +// +DWORD AutoTypePlatformWin::qtToNativeModifiers(Qt::KeyboardModifiers modifiers) +{ + DWORD nativeModifiers = 0; + + if (modifiers & Qt::ShiftModifier) { + nativeModifiers |= MOD_SHIFT; + } + if (modifiers & Qt::ControlModifier) { + nativeModifiers |= MOD_CONTROL; + } + if (modifiers & Qt::AltModifier) { + nativeModifiers |= MOD_ALT; + } + if (modifiers & Qt::MetaModifier) { + nativeModifiers |= MOD_WIN; + } + + return nativeModifiers; +} + +// +// Test if window is in Alt+Tab list +// see: https://blogs.msdn.microsoft.com/oldnewthing/20071008-00/?p=24863 +// +BOOL AutoTypePlatformWin::isAltTabWindow(HWND hwnd) +{ + if (!::IsWindowVisible(hwnd)) { + return FALSE; + } + + // Start at the root owner + HWND hwndWalk = ::GetAncestor(hwnd, GA_ROOTOWNER); + HWND hwndTry; + + // See if we are the last active visible popup + while ((hwndTry = ::GetLastActivePopup(hwndWalk)) != hwndWalk) { + if (::IsWindowVisible(hwndTry)) { + break; + } + hwndWalk = hwndTry; + } + + return hwndWalk == hwnd; +} + +// +// Window title enum proc +// +BOOL CALLBACK AutoTypePlatformWin::windowTitleEnumProc( + _In_ HWND hwnd, + _In_ LPARAM lParam +) +{ + if (!isAltTabWindow(hwnd)) { + // Skip window + return TRUE; + } + + QStringList *list = reinterpret_cast(lParam); + QString title = windowTitle(hwnd); + + if (!title.isEmpty()) { + list->append(title); + } + + return TRUE; +} + +// +// Get window title +// +QString AutoTypePlatformWin::windowTitle(HWND hwnd) +{ + wchar_t title[MAX_WINDOW_TITLE_LENGTH]; + int count = ::GetWindowTextW(hwnd, title, MAX_WINDOW_TITLE_LENGTH); + + return QString::fromUtf16(reinterpret_cast(title), count); +} + +// +// ------------------------------ AutoTypeExecutorWin ------------------------------ +// + +AutoTypeExecutorWin::AutoTypeExecutorWin(AutoTypePlatformWin* platform) + : m_platform(platform) +{ +} + +void AutoTypeExecutorWin::execChar(AutoTypeChar* action) +{ + m_platform->sendChar(action->character, true); + m_platform->sendChar(action->character, false); + ::Sleep(25); +} + +void AutoTypeExecutorWin::execKey(AutoTypeKey* action) +{ + m_platform->sendKey(action->key, true); + m_platform->sendKey(action->key, false); + ::Sleep(25); +} + diff --git a/src/autotype/windows/AutoTypeWindows.h b/src/autotype/windows/AutoTypeWindows.h new file mode 100644 index 000000000..7a8c4bcab --- /dev/null +++ b/src/autotype/windows/AutoTypeWindows.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 Lennart Glauer + * + * 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_AUTOTYPEWINDOWS_H +#define KEEPASSX_AUTOTYPEWINDOWS_H + +#include +#include + +#include "autotype/AutoTypePlatformPlugin.h" +#include "autotype/AutoTypeAction.h" + +class AutoTypePlatformWin : public QObject, public AutoTypePlatformInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.keepassx.AutoTypePlatformWindows") + Q_INTERFACES(AutoTypePlatformInterface) + +public: + bool isAvailable() override; + QStringList windowTitles() override; + WId activeWindow() override; + QString activeWindowTitle() override; + bool registerGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) override; + void unregisterGlobalShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) override; + int platformEventFilter(void* event) override; + int initialTimeout() override; + bool raiseWindow(WId window) override; + AutoTypeExecutor* createExecutor() override; + + void sendChar(const QChar& ch, bool isKeyDown); + void sendKey(Qt::Key key, bool isKeyDown); + +Q_SIGNALS: + void globalShortcutTriggered(); + +private: + static DWORD qtToNativeKeyCode(Qt::Key key); + static DWORD qtToNativeModifiers(Qt::KeyboardModifiers modifiers); + static BOOL isExtendedKey(DWORD nativeKeyCode); + static BOOL isAltTabWindow(HWND hwnd); + static BOOL CALLBACK windowTitleEnumProc(_In_ HWND hwnd, _In_ LPARAM lParam); + static QString windowTitle(HWND hwnd); +}; + +class AutoTypeExecutorWin : public AutoTypeExecutor +{ +public: + explicit AutoTypeExecutorWin(AutoTypePlatformWin* platform); + + void execChar(AutoTypeChar* action) override; + void execKey(AutoTypeKey* action) override; + +private: + AutoTypePlatformWin* const m_platform; +}; + +#endif // KEEPASSX_AUTOTYPEWINDOWS_H + diff --git a/src/autotype/windows/CMakeLists.txt b/src/autotype/windows/CMakeLists.txt new file mode 100644 index 000000000..dac8a7867 --- /dev/null +++ b/src/autotype/windows/CMakeLists.txt @@ -0,0 +1,9 @@ +set(autotype_win_SOURCES + AutoTypeWindows.cpp +) + +add_library(keepassx-autotype-windows MODULE ${autotype_win_SOURCES}) +target_link_libraries(keepassx-autotype-windows ${PROGNAME} Qt5::Core Qt5::Widgets) +install(TARGETS keepassx-autotype-windows + BUNDLE DESTINATION . COMPONENT Runtime +LIBRARY DESTINATION ${PLUGIN_INSTALL_DIR} COMPONENT Runtime) \ No newline at end of file diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index d90a33fe8..6862fc9d8 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -185,6 +185,35 @@ QString Entry::defaultAutoTypeSequence() const return m_data.defaultAutoTypeSequence; } +QString Entry::effectiveAutoTypeSequence() const +{ + if (!m_data.defaultAutoTypeSequence.isEmpty()) { + return m_data.defaultAutoTypeSequence; + } + QString sequence; + + const Group* grp = group(); + if(grp) { + sequence = grp->effectiveAutoTypeSequence(); + } else { + return QString(); + } + + if (sequence.isEmpty() && (!username().isEmpty() || !password().isEmpty())) { + if (username().isEmpty()) { + sequence = "{PASSWORD}{ENTER}"; + } + else if (password().isEmpty()) { + sequence = "{USERNAME}{ENTER}"; + } + else { + sequence = "{USERNAME}{TAB}{PASSWORD}{ENTER}"; + } + } + + return sequence; +} + AutoTypeAssociations* Entry::autoTypeAssociations() { return m_autoTypeAssociations; diff --git a/src/core/Entry.h b/src/core/Entry.h index 0b4d4829f..910146968 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -70,6 +70,7 @@ public: bool autoTypeEnabled() const; int autoTypeObfuscation() const; QString defaultAutoTypeSequence() const; + QString effectiveAutoTypeSequence() const; AutoTypeAssociations* autoTypeAssociations(); const AutoTypeAssociations* autoTypeAssociations() const; QString title() const; diff --git a/src/core/FilePath.cpp b/src/core/FilePath.cpp index 021234a11..24c9e3528 100644 --- a/src/core/FilePath.cpp +++ b/src/core/FilePath.cpp @@ -49,6 +49,10 @@ QString FilePath::pluginPath(const QString& name) // for TestAutoType pluginPaths << QCoreApplication::applicationDirPath() + "/../src/autotype/test"; +#if defined(Q_OS_MAC) + pluginPaths << QCoreApplication::applicationDirPath() + "/../PlugIns"; +#endif + pluginPaths << QCoreApplication::applicationDirPath(); QString configuredPluginDir = KEEPASSX_PLUGIN_DIR; diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 70260170a..eb293a935 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -187,6 +187,23 @@ QString Group::defaultAutoTypeSequence() const return m_data.defaultAutoTypeSequence; } +QString Group::effectiveAutoTypeSequence() const +{ + QString sequence; + + const Group* group = this; + do { + if (group->autoTypeEnabled() == Group::Disable) { + return QString(); + } + + sequence = group->defaultAutoTypeSequence(); + group = group->parentGroup(); + } while (group && sequence.isEmpty()); + + return sequence; +} + Group::TriState Group::autoTypeEnabled() const { return m_data.autoTypeEnabled; diff --git a/src/core/Group.h b/src/core/Group.h index 025814b6c..3c054f976 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -66,6 +66,7 @@ public: TimeInfo timeInfo() const; bool isExpanded() const; QString defaultAutoTypeSequence() const; + QString effectiveAutoTypeSequence() const; Group::TriState autoTypeEnabled() const; Group::TriState searchingEnabled() const; Group::MergeMode mergeMode() const; diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index 70550c557..d982f22ca 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -41,6 +41,25 @@ public: return false; } }; +#elif defined(Q_OS_WIN) +class WinEventFilter : public QAbstractNativeEventFilter +{ +public: + bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override + { + Q_UNUSED(result); + + if (eventType == QByteArrayLiteral("windows_generic_MSG") + || eventType == QByteArrayLiteral("windows_dispatcher_MSG")) { + int retCode = autoType()->callEventFilter(message); + if (retCode == 1) { + return true; + } + } + + return false; + } +}; #endif Application::Application(int& argc, char** argv) @@ -49,6 +68,8 @@ Application::Application(int& argc, char** argv) { #if defined(Q_OS_UNIX) && !defined(Q_OS_OSX) installNativeEventFilter(new XcbEventFilter()); +#elif defined(Q_OS_WIN) + installNativeEventFilter(new WinEventFilter()); #endif } diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 6e8a7b744..d4001501d 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -37,6 +37,7 @@ #include "gui/MessageBox.h" #include "gui/entry/EntryView.h" #include "gui/group/GroupView.h" +#include "gui/UnlockDatabaseDialog.h" DatabaseManagerStruct::DatabaseManagerStruct() : dbWidget(nullptr) @@ -235,6 +236,7 @@ bool DatabaseTabWidget::closeDatabase(Database* db) int index = databaseIndex(db); Q_ASSERT(index != -1); + dbStruct.dbWidget->closeUnlockDialog(); QString dbName = tabText(index); if (dbName.right(1) == "*") { dbName.chop(1); @@ -813,5 +815,9 @@ void DatabaseTabWidget::performGlobalAutoType() } } - autoType()->performGlobalAutoType(unlockedDatabases); + if (unlockedDatabases.size() > 0) { + autoType()->performGlobalAutoType(unlockedDatabases); + } else if (m_dbList.size() > 0){ + indexDatabaseManagerStruct(0).dbWidget->showUnlockDialog(); + } } diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index f9b635af0..bf620b6d5 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -46,6 +46,7 @@ #include "gui/KeePass1OpenWidget.h" #include "gui/MessageBox.h" #include "gui/UnlockDatabaseWidget.h" +#include "gui/UnlockDatabaseDialog.h" #include "gui/entry/EditEntryWidget.h" #include "gui/entry/EntryView.h" #include "gui/group/EditGroupWidget.h" @@ -127,6 +128,8 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) m_keepass1OpenWidget->setObjectName("keepass1OpenWidget"); m_unlockDatabaseWidget = new UnlockDatabaseWidget(); m_unlockDatabaseWidget->setObjectName("unlockDatabaseWidget"); + m_unlockDatabaseDialog = new UnlockDatabaseDialog(); + m_unlockDatabaseDialog->setObjectName("unlockDatabaseDialog"); addWidget(m_mainWidget); addWidget(m_editEntryWidget); addWidget(m_editGroupWidget); @@ -156,6 +159,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) connect(m_databaseOpenMergeWidget, SIGNAL(editFinished(bool)), SLOT(mergeDatabase(bool))); connect(m_keepass1OpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool))); connect(m_unlockDatabaseWidget, SIGNAL(editFinished(bool)), SLOT(unlockDatabase(bool))); + connect(m_unlockDatabaseDialog, SIGNAL(unlockDone(bool)), SLOT(unlockDatabase(bool))); connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged())); connect(&m_fileWatchTimer, SIGNAL(timeout()), this, SLOT(reloadDatabaseFile())); connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged())); @@ -180,7 +184,8 @@ DatabaseWidget::Mode DatabaseWidget::currentMode() const else if (currentWidget() == m_mainWidget) { return DatabaseWidget::ViewMode; } - else if (currentWidget() == m_unlockDatabaseWidget) { + else if (currentWidget() == m_unlockDatabaseWidget || + currentWidget() == m_databaseOpenWidget) { return DatabaseWidget::LockedMode; } else { @@ -705,7 +710,14 @@ void DatabaseWidget::unlockDatabase(bool accepted) return; } - replaceDatabase(static_cast(sender())->database()); + Database *db = Q_NULLPTR; + if (sender() == m_unlockDatabaseDialog) { + db = m_unlockDatabaseDialog->database(); + } else if (sender() == m_unlockDatabaseWidget) { + db = m_unlockDatabaseWidget->database(); + } + + replaceDatabase(db); const QList groups = m_db->rootGroup()->groupsRecursive(true); for (Group* group : groups) { @@ -719,6 +731,12 @@ void DatabaseWidget::unlockDatabase(bool accepted) setCurrentWidget(m_mainWidget); m_unlockDatabaseWidget->clearForms(); Q_EMIT unlockedDatabase(); + + if (sender() == m_unlockDatabaseDialog) { + QList dbList; + dbList.append(m_db); + autoType()->performGlobalAutoType(dbList); + } } void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column) @@ -1066,3 +1084,16 @@ GroupView* DatabaseWidget::groupView() { EntryView* DatabaseWidget::entryView() { return m_entryView; } + +void DatabaseWidget::showUnlockDialog() +{ + m_unlockDatabaseDialog->clearForms(); + m_unlockDatabaseDialog->setDBFilename(m_filename); + m_unlockDatabaseDialog->show(); + m_unlockDatabaseDialog->activateWindow(); +} + +void DatabaseWidget::closeUnlockDialog() +{ + m_unlockDatabaseDialog->close(); +} diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 11b5d3963..0f52ea08d 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -43,6 +43,7 @@ class QMenu; class QSplitter; class QLabel; class UnlockDatabaseWidget; +class UnlockDatabaseDialog; class QFileSystemWatcher; class DatabaseWidget : public QStackedWidget @@ -89,6 +90,8 @@ public: bool currentEntryHasNotes(); GroupView* groupView(); EntryView* entryView(); + void showUnlockDialog(); + void closeUnlockDialog(); Q_SIGNALS: void closeRequest(); @@ -174,6 +177,7 @@ private: DatabaseOpenWidget* m_databaseOpenMergeWidget; KeePass1OpenWidget* m_keepass1OpenWidget; UnlockDatabaseWidget* m_unlockDatabaseWidget; + UnlockDatabaseDialog* m_unlockDatabaseDialog; QSplitter* m_splitter; GroupView* m_groupView; EntryView* m_entryView; diff --git a/src/gui/UnlockDatabaseDialog.cpp b/src/gui/UnlockDatabaseDialog.cpp new file mode 100644 index 000000000..679493903 --- /dev/null +++ b/src/gui/UnlockDatabaseDialog.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 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 "UnlockDatabaseDialog.h" +#include "UnlockDatabaseWidget.h" + +#include "autotype/AutoType.h" +#include "gui/DragTabBar.h" +#include "core/Database.h" + + +UnlockDatabaseDialog::UnlockDatabaseDialog(QWidget *parent) + : QDialog(parent) + , m_view(new UnlockDatabaseWidget(this)) +{ + connect(m_view, SIGNAL(editFinished(bool)), this, SLOT(complete(bool))); +} + +void UnlockDatabaseDialog::setDBFilename(const QString &filename) +{ + m_view->load(filename); +} + +void UnlockDatabaseDialog::clearForms() +{ + m_view->clearForms(); +} + +Database *UnlockDatabaseDialog::database() +{ + return m_view->database(); +} + +void UnlockDatabaseDialog::complete(bool r) +{ + if (r) { + accept(); + Q_EMIT unlockDone(true); + } else { + reject(); + } +} diff --git a/src/gui/UnlockDatabaseDialog.h b/src/gui/UnlockDatabaseDialog.h new file mode 100644 index 000000000..1ba6d2e06 --- /dev/null +++ b/src/gui/UnlockDatabaseDialog.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016 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 KEEPASSX_AUTOTYPEUNLOCKDIALOG_H +#define KEEPASSX_AUTOTYPEUNLOCKDIALOG_H + +#include + +//#include + +#include "core/Global.h" + +class UnlockDatabaseWidget; +class Database; + +class UnlockDatabaseDialog : public QDialog +{ + Q_OBJECT +public: + explicit UnlockDatabaseDialog(QWidget *parent = Q_NULLPTR); + void setDBFilename(const QString& filename); + void clearForms(); + Database* database(); + +Q_SIGNALS: + void unlockDone(bool); + +public Q_SLOTS: + void complete(bool r); + +private: + UnlockDatabaseWidget* const m_view; +}; + +#endif // KEEPASSX_AUTOTYPEUNLOCKDIALOG_H diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index eb5ebd476..46b59df60 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -354,12 +354,11 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore) m_autoTypeUi->enableButton->setChecked(entry->autoTypeEnabled()); if (entry->defaultAutoTypeSequence().isEmpty()) { m_autoTypeUi->inheritSequenceButton->setChecked(true); - m_autoTypeUi->sequenceEdit->setText(""); } else { m_autoTypeUi->customSequenceButton->setChecked(true); - m_autoTypeUi->sequenceEdit->setText(entry->defaultAutoTypeSequence()); } + m_autoTypeUi->sequenceEdit->setText(entry->effectiveAutoTypeSequence()); m_autoTypeUi->windowTitleCombo->lineEdit()->clear(); m_autoTypeUi->defaultWindowSequenceButton->setChecked(true); m_autoTypeUi->windowSequenceEdit->setText(""); diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp index 1c900a578..d692c23c6 100644 --- a/src/gui/entry/EntryView.cpp +++ b/src/gui/entry/EntryView.cpp @@ -55,6 +55,10 @@ void EntryView::keyPressEvent(QKeyEvent* event) { if ((event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) && currentIndex().isValid()) { emitEntryActivated(currentIndex()); +#ifdef Q_OS_MAC + // Pressing return does not emit the QTreeView::activated signal on mac os + Q_EMIT activated(currentIndex()); +#endif } QTreeView::keyPressEvent(event); diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp index a5b426b92..177c62bb0 100644 --- a/src/gui/group/EditGroupWidget.cpp +++ b/src/gui/group/EditGroupWidget.cpp @@ -82,7 +82,7 @@ void EditGroupWidget::loadGroup(Group* group, bool create, Database* database) else { m_mainUi->autoTypeSequenceCustomRadio->setChecked(true); } - m_mainUi->autoTypeSequenceCustomEdit->setText(group->defaultAutoTypeSequence()); + m_mainUi->autoTypeSequenceCustomEdit->setText(group->effectiveAutoTypeSequence()); IconStruct iconStruct; iconStruct.uuid = group->iconUuid();