Skip to content

Commit

Permalink
Wayland autotype implementation (using xdg-desktop-portal)
Browse files Browse the repository at this point in the history
  • Loading branch information
TheConfuZzledDude authored and droidmonkey committed Nov 9, 2024
1 parent d03ffc2 commit d37e8b1
Show file tree
Hide file tree
Showing 12 changed files with 870 additions and 16 deletions.
6 changes: 6 additions & 0 deletions cmake/FindXkbcommon.cmake
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion src/autotype/AutoType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbLi
return;
}

if (m_windowTitleForGlobal.isEmpty()) {
if (m_windowTitleForGlobal.isEmpty() && QApplication::platformName().compare("wayland", Qt::CaseInsensitive) == 0) {
m_inGlobalAutoTypeDialog.unlock();
return;
}
Expand Down
9 changes: 1 addition & 8 deletions src/autotype/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,14 +1,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)
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")
add_feature_info(Qt5X11Extras Qt5X11Extras_FOUND "The Qt5X11Extras library is required for auto-type")
endif()

add_subdirectory(xcb)
add_subdirectory(wayland)
elseif(APPLE)
add_subdirectory(mac)
elseif(WIN32)
Expand Down
247 changes: 247 additions & 0 deletions src/autotype/wayland/AutoTypeWayland.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*
* Copyright (C) 2024 KeePassXC Team <[email protected]>
*
* 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 <http://www.gnu.org/licenses/>.
*/

#include "AutoTypeWayland.h"

#include "autotype/AutoTypeAction.h"
#include "core/Tools.h"
#include "gui/osutils/nixutils/X11Funcs.h"

#include <qcbormap.h>
#include <qcoreapplication.h>
#include <qdbusconnection.h>
#include <qdbusinterface.h>
#include <qdbusmessage.h>
#include <qdebug.h>
#include <qglobal.h>
#include <qguiapplication.h>
#include <qlist.h>
#include <qmetaobject.h>
#include <qnamespace.h>
#include <qrandom.h>
#include <qscopedpointer.h>
#include <qsocketnotifier.h>
#include <qvariant.h>
#include <qvector.h>

#include <xkbcommon/xkbcommon.h>

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<xkb_keysym_t> 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<xkb_keysym_t> 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();
}
81 changes: 81 additions & 0 deletions src/autotype/wayland/AutoTypeWayland.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2024 KeePassXC Team <[email protected]>
*
* 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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <qdbusconnection.h>
#include <qdbusinterface.h>
#include <qdbusmessage.h>
#include <qnamespace.h>
#include <qpointer.h>
#include <qscopedpointer.h>
#include <qsocketnotifier.h>

#include <xkbcommon/xkbcommon.h>

#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<xkb_keysym_t> modifiers = {});
void createSession();

private slots:
void portalResponse(uint response, QVariantMap results, QDBusMessage message);

private:
bool m_loaded;
QDBusConnection m_bus;
QMap<QString, std::function<void(uint, QVariantMap)>> 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);
};

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
9 changes: 9 additions & 0 deletions src/autotype/wayland/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
find_package(Xkbcommon REQUIRED)

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)
Loading

0 comments on commit d37e8b1

Please sign in to comment.