From 4440fe8e5eca23dff305581b536d7095076a40d5 Mon Sep 17 00:00:00 2001 From: Zachary Freed Date: Sat, 15 Jun 2024 21:59:25 +0100 Subject: [PATCH] WIP: Wayland autotype implementation (using xdg-desktop-portal) --- cmake/FindXkbcommon.cmake | 6 + src/autotype/CMakeLists.txt | 2 + src/autotype/wayland/AutoTypeWayland.cpp | 246 +++++++++ src/autotype/wayland/AutoTypeWayland.h | 86 +++ src/autotype/wayland/CMakeLists.txt | 7 + .../org.freedesktop.portal.RemoteDesktop.xml | 493 ++++++++++++++++++ src/gui/MainWindow.cpp | 5 + src/gui/MainWindow.h | 1 + .../org.keepassxc.KeePassXC.MainWindow.xml | 5 + vcpkg.json | 4 + 10 files changed, 855 insertions(+) create mode 100644 cmake/FindXkbcommon.cmake create mode 100644 src/autotype/wayland/AutoTypeWayland.cpp create mode 100644 src/autotype/wayland/AutoTypeWayland.h create mode 100644 src/autotype/wayland/CMakeLists.txt create mode 100644 src/autotype/wayland/org.freedesktop.portal.RemoteDesktop.xml diff --git a/cmake/FindXkbcommon.cmake b/cmake/FindXkbcommon.cmake new file mode 100644 index 0000000000..75a55649d6 --- /dev/null +++ b/cmake/FindXkbcommon.cmake @@ -0,0 +1,6 @@ + +find_package(PkgConfig) +pkg_check_modules(Xkbcommon xkbcommon) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Xkbcommon DEFAULT_MSG Xkbcommon_LIBRARIES Xkbcommon_INCLUDE_DIRS) diff --git a/src/autotype/CMakeLists.txt b/src/autotype/CMakeLists.txt index 79bb503722..0fa1f0d03b 100644 --- a/src/autotype/CMakeLists.txt +++ b/src/autotype/CMakeLists.txt @@ -2,6 +2,7 @@ if(WITH_XC_AUTOTYPE) if(UNIX AND NOT APPLE AND NOT HAIKU) find_package(X11 REQUIRED COMPONENTS Xi XTest) find_package(Qt5X11Extras 5.2 REQUIRED) + find_package(Xkbcommon REQUIRED) if(PRINT_SUMMARY) add_feature_info(libXi X11_Xi_FOUND "The X11 Xi Protocol library is required for auto-type") add_feature_info(libXtst X11_XTest_FOUND "The X11 XTEST Protocol library is required for auto-type") @@ -9,6 +10,7 @@ if(WITH_XC_AUTOTYPE) endif() add_subdirectory(xcb) + add_subdirectory(wayland) elseif(APPLE) add_subdirectory(mac) elseif(WIN32) diff --git a/src/autotype/wayland/AutoTypeWayland.cpp b/src/autotype/wayland/AutoTypeWayland.cpp new file mode 100644 index 0000000000..75109174b0 --- /dev/null +++ b/src/autotype/wayland/AutoTypeWayland.cpp @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2012 Felix Geyer + * Copyright (C) 2000-2008 Tom Sato + * Copyright (C) 2017 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 "AutoTypeWayland.h" +#include "autotype/AutoTypeAction.h" +#include "core/Tools.h" +#include "gui/osutils/nixutils/X11Funcs.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +QString generateToken() +{ + static uint next = 0; + return QString("keepassxc_%1_%2").arg(next++).arg(QRandomGenerator::system()->generate()); +} + +AutoTypePlatformWayland::AutoTypePlatformWayland() + : m_bus(QDBusConnection::sessionBus()) + , m_remote_desktop("org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.RemoteDesktop", + m_bus, + this) +{ + m_bus.connect("org.freedesktop.portal.Desktop", + "", + "org.freedesktop.portal.Request", + "Response", + this, + SLOT(portalResponse(uint, QVariantMap, QDBusMessage))); + + createSession(); +} + +void AutoTypePlatformWayland::createSession() +{ + QString requestHandle = generateToken(); + + m_handlers.insert(requestHandle, + [this](uint _response, QVariantMap _result) { handleCreateSession(_response, _result); }); + + m_remote_desktop.call("CreateSession", + QVariantMap{{"handle_token", requestHandle}, {"session_handle_token", generateToken()}}); +} + +void AutoTypePlatformWayland::handleCreateSession(uint response, QVariantMap result) +{ + qDebug() << "Got response and result" << response << result; + if (response == 0) { + m_session_handle = QDBusObjectPath(result["session_handle"].toString()); + + QString selectDevicesRequestHandle = generateToken(); + m_handlers.insert(selectDevicesRequestHandle, + [this](uint _response, QVariantMap _result) { handleSelectDevices(_response, _result); }); + + QVariantMap selectDevicesOptions{ + {"handle_token", selectDevicesRequestHandle}, + {"types", uint(1)}, + {"persist_mode", uint(2)}, + }; + + // TODO: Store restore token in database/some other persistent data so the dialog doesn't appear every launch + if (!m_restore_token.isEmpty()) { + selectDevicesOptions.insert("restore_token", m_restore_token); + } + + m_remote_desktop.call("SelectDevices", m_session_handle, selectDevicesOptions); + + QString startRequestHandle = generateToken(); + m_handlers.insert(startRequestHandle, + [this](uint _response, QVariantMap _result) { handleStart(_response, _result); }); + + QVariantMap startOptions{ + {"handle_token", startRequestHandle}, + }; + + // TODO: Pass window identifier here instead of empty string if we want the dialog to appear on top of the + // application window, need to be able to get active window and handle from Wayland + m_remote_desktop.call("Start", m_session_handle, "", startOptions); + } +} + +void AutoTypePlatformWayland::handleSelectDevices(uint response, QVariantMap result) +{ + Q_UNUSED(result); + qDebug() << "Select Devices: " << response << result; + + if (response == 0) { + } +} + +void AutoTypePlatformWayland::handleStart(uint response, QVariantMap result) +{ + qDebug() << "Start: " << response << result; + if (response == 0) { + m_session_started = true; + m_restore_token = result["restore_token"].toString(); + } +} + +void AutoTypePlatformWayland::portalResponse(uint response, QVariantMap results, QDBusMessage message) +{ + Q_UNUSED(response); + Q_UNUSED(results); + qDebug() << "Recieved message: " << message; + auto index = message.path().lastIndexOf("/"); + auto handle = message.path().right(message.path().length() - index - 1); + if (m_handlers.contains(handle)) { + m_handlers.take(handle)(response, results); + } +} + +AutoTypeAction::Result AutoTypePlatformWayland::sendKey(xkb_keysym_t keysym, QVector modifiers) +{ + for (auto modifier : modifiers) { + m_remote_desktop.call("NotifyKeyboardKeysym", m_session_handle, QVariantMap(), int(modifier), uint(1)); + } + + m_remote_desktop.call("NotifyKeyboardKeysym", m_session_handle, QVariantMap(), int(keysym), uint(1)); + + m_remote_desktop.call("NotifyKeyboardKeysym", m_session_handle, QVariantMap(), int(keysym), uint(0)); + + for (auto modifier : modifiers) { + m_remote_desktop.call("NotifyKeyboardKeysym", m_session_handle, QVariantMap(), int(modifier), uint(0)); + } + return AutoTypeAction::Result::Ok(); +}; + +bool AutoTypePlatformWayland::isAvailable() +{ + return true; +} + +void AutoTypePlatformWayland::unload() +{ +} + +QString AutoTypePlatformWayland::activeWindowTitle() +{ + return QString(""); +} + +WId AutoTypePlatformWayland::activeWindow() +{ + return 0; +}; + +AutoTypeExecutor* AutoTypePlatformWayland::createExecutor() +{ + return new AutoTypeExecutorWayland(this); +} + +bool AutoTypePlatformWayland::raiseWindow(WId window) +{ + Q_UNUSED(window); + return false; +} + +QStringList AutoTypePlatformWayland::windowTitles() +{ + return QStringList{}; +} + +AutoTypeExecutorWayland::AutoTypeExecutorWayland(AutoTypePlatformWayland* platform) + : m_platform(platform) +{ +} + +AutoTypeAction::Result AutoTypeExecutorWayland::execBegin(const AutoTypeBegin* action) +{ + Q_UNUSED(action); + return AutoTypeAction::Result::Ok(); +} + +AutoTypeAction::Result AutoTypeExecutorWayland::execType(const AutoTypeKey* action) +{ + Q_UNUSED(action); + + QVector modifiers{}; + + if (action->modifiers.testFlag(Qt::ShiftModifier)) { + modifiers.append(XKB_KEY_Shift_L); + } + if (action->modifiers.testFlag(Qt::ControlModifier)) { + modifiers.append(XKB_KEY_Control_L); + } + if (action->modifiers.testFlag(Qt::AltModifier)) { + modifiers.append(XKB_KEY_Alt_L); + } + if (action->modifiers.testFlag(Qt::MetaModifier)) { + modifiers.append(XKB_KEY_Meta_L); + } + + // TODO: Replace these with proper lookups to xkbcommon keysyms instead of just reusing the X11 ones + // They're mostly the same for most things, but strictly speaking differ slightly + if (action->key != Qt::Key_unknown) { + m_platform->sendKey(qtToNativeKeyCode(action->key), modifiers); + } else { + m_platform->sendKey(qcharToNativeKeyCode(action->character), modifiers); + } + + Tools::sleep(execDelayMs); + + return AutoTypeAction::Result::Ok(); +} + +AutoTypeAction::Result AutoTypeExecutorWayland::execClearField(const AutoTypeClearField* action) +{ + Q_UNUSED(action); + execType(new AutoTypeKey(Qt::Key_Home)); + execType(new AutoTypeKey(Qt::Key_End, Qt::ShiftModifier)); + execType(new AutoTypeKey(Qt::Key_Backspace)); + + return AutoTypeAction::Result::Ok(); +} diff --git a/src/autotype/wayland/AutoTypeWayland.h b/src/autotype/wayland/AutoTypeWayland.h new file mode 100644 index 0000000000..41b430634c --- /dev/null +++ b/src/autotype/wayland/AutoTypeWayland.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2012 Felix Geyer + * Copyright (C) 2000-2008 Tom Sato + * Copyright (C) 2017 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_AUTOTYPEWAYLAND_H +#define KEEPASSX_AUTOTYPEWAYLAND_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "autotype/AutoTypePlatformPlugin.h" + +class AutoTypePlatformWayland : public QObject, public AutoTypePlatformInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.keepassx.AutoTypePlatformWaylnd") + Q_INTERFACES(AutoTypePlatformInterface) + +public: + AutoTypePlatformWayland(); + bool isAvailable() override; + void unload() override; + QStringList windowTitles() override; + WId activeWindow() override; + QString activeWindowTitle() override; + bool raiseWindow(WId window) override; + AutoTypeExecutor* createExecutor() override; + + AutoTypeAction::Result sendKey(xkb_keysym_t keysym, QVector modifiers = {}); + void createSession(); + +private: + bool m_loaded; + QDBusConnection m_bus; + QMap> m_handlers; + QDBusInterface m_remote_desktop; + QDBusObjectPath m_session_handle; + QString m_restore_token; + bool m_session_started = false; + + void handleCreateSession(uint response, QVariantMap results); + void handleSelectDevices(uint response, QVariantMap results); + void handleStart(uint response, QVariantMap results); +private slots: + void portalResponse(uint response, QVariantMap results, QDBusMessage message); +}; + +class AutoTypeExecutorWayland : public AutoTypeExecutor +{ +public: + explicit AutoTypeExecutorWayland(AutoTypePlatformWayland* platform); + + AutoTypeAction::Result execBegin(const AutoTypeBegin* action) override; + AutoTypeAction::Result execType(const AutoTypeKey* action) override; + AutoTypeAction::Result execClearField(const AutoTypeClearField* action) override; + +private: + AutoTypePlatformWayland* const m_platform; +}; + +#endif // KEEPASSX_AUTOTYPEWAYLAND_H diff --git a/src/autotype/wayland/CMakeLists.txt b/src/autotype/wayland/CMakeLists.txt new file mode 100644 index 0000000000..355f5c3b3e --- /dev/null +++ b/src/autotype/wayland/CMakeLists.txt @@ -0,0 +1,7 @@ +set(autotype_WAYLAND_SOURCES AutoTypeWayland.cpp) + +add_library(keepassxc-autotype-wayland MODULE ${autotype_WAYLAND_SOURCES}) +target_link_libraries(keepassxc-autotype-wayland keepassxc_gui Qt5::Core Qt5::Widgets Qt5::DBus ${Xkbcommon_LIBRARIES}) +install(TARGETS keepassxc-autotype-wayland + BUNDLE DESTINATION . COMPONENT Runtime + LIBRARY DESTINATION ${PLUGIN_INSTALL_DIR} COMPONENT Runtime) diff --git a/src/autotype/wayland/org.freedesktop.portal.RemoteDesktop.xml b/src/autotype/wayland/org.freedesktop.portal.RemoteDesktop.xml new file mode 100644 index 0000000000..9ef6671a05 --- /dev/null +++ b/src/autotype/wayland/org.freedesktop.portal.RemoteDesktop.xml @@ -0,0 +1,493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 6d0c510e0d..1fa0b7fa42 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -1885,6 +1885,11 @@ void MainWindow::lockAllDatabases() lockDatabasesAfterInactivity(); } +void MainWindow::requestGlobalAutoType(const QString& search) +{ + emit osUtils->globalShortcutTriggered("autotype", search); +} + void MainWindow::displayDesktopNotification(const QString& msg, QString title, int msTimeoutHint) { if (!m_trayIcon || !QSystemTrayIcon::supportsMessages()) { diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 51a9f79424..5de50f081b 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -91,6 +91,7 @@ public slots: void toggleWindow(); void bringToFront(); void closeAllDatabases(); + void requestGlobalAutoType(const QString& search = ""); void lockAllDatabases(); void closeModalWindow(); void displayDesktopNotification(const QString& msg, QString title = "", int msTimeoutHint = 10000); diff --git a/src/gui/org.keepassxc.KeePassXC.MainWindow.xml b/src/gui/org.keepassxc.KeePassXC.MainWindow.xml index 6510181494..1be9da42c0 100644 --- a/src/gui/org.keepassxc.KeePassXC.MainWindow.xml +++ b/src/gui/org.keepassxc.KeePassXC.MainWindow.xml @@ -21,6 +21,11 @@ + + + + + diff --git a/vcpkg.json b/vcpkg.json index baa40686c3..7080229ba5 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -76,6 +76,10 @@ { "name": "zlib", "version>=": "1.3" + }, + { + "name": "libxkbcommon", + "version>=": "1.4.1" } ] }