diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 574dc39671..33c079e404 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -282,6 +282,10 @@ Custom + + None + + ApplicationSettingsWidgetGeneral @@ -616,6 +620,34 @@ Auto-generate password for new entries + + Keep remote desktop connection open after performing Auto-Type + + + + Remote desktop mode: + + + + Never remember session + + + + Remember session until exit + + + + Remember session until revoked by desktop + + + + None + + + + Configure... + + ApplicationSettingsWidgetSecurity @@ -810,6 +842,29 @@ + + AutoTypePlatformWayland + + Session closed + + + + User cancelled the interaction + + + + User interaction was canceled for unknown reason + + + + No symbol found for key: '%1' + + + + No symbol found for character: '%1' + + + AutoTypePlatformX11 @@ -6658,6 +6713,22 @@ This version is not meant for production use. Could not register global shortcut + + The XDG Desktop Portal for global shortcuts is not available on this system. + + + + Trigger global Auto-Type + + + + KeePassXC - Global Shortcuts + + + + Global Auto-Type shortcut is already configured. To change it, open your system settings and navigate to the keyboard or application shortcuts section. + + OpData01 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8ede82980c..d39b2cbbb2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -257,6 +257,22 @@ if(UNIX AND NOT APPLE) quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml polkit_dbus ) + qt6_add_dbus_interface(core_SOURCES + gui/osutils/nixutils/dbus/org.freedesktop.portal.Request.xml + xdp_request + ) + qt6_add_dbus_interface(core_SOURCES + gui/osutils/nixutils/dbus/org.freedesktop.portal.Session.xml + xdp_session + ) + qt6_add_dbus_interface(core_SOURCES + gui/osutils/nixutils/dbus/org.freedesktop.portal.GlobalShortcuts.xml + xdp_globalshortcuts + ) + qt6_add_dbus_interface(core_SOURCES + gui/osutils/nixutils/dbus/org.freedesktop.portal.RemoteDesktop.xml + xdp_remotedesktop + ) endif() if(WIN32) diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index b6f1428b17..871f29ffc5 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -155,6 +155,11 @@ AutoType::AutoType(QObject* parent, bool test) } connect(this, SIGNAL(autotypeFinished()), SLOT(resetAutoTypeState())); + connect(this, &AutoType::autotypeFinished, this, [this] { + if (m_plugin) { + m_plugin->finishAutoType(); + } + }); connect(qApp, SIGNAL(aboutToQuit()), SLOT(unloadPlugin())); } @@ -398,6 +403,10 @@ void AutoType::performAutoTypeWithSequence(const Entry* entry, const QString& se void AutoType::startGlobalAutoType(const QString& search) { + if (!m_plugin) { + return; + } + // Never Auto-Type into KeePassXC itself if (getMainWindow() && (qApp->activeWindow() || qApp->activeModalWidget())) { return; @@ -472,9 +481,10 @@ void AutoType::performGlobalAutoType(const QList>& dbLi qWarning() << "Auto-Type: Window title was empty from the operating system"; } - // Show the selection dialog if we always ask, have multiple matches, or no matches + // Show the selection dialog if we always ask, have multiple matches, no matches, or the window title was empty if (getMainWindow() - && (config()->get(Config::Security_AutoTypeAsk).toBool() || matchList.size() > 1 || matchList.isEmpty())) { + && (config()->get(Config::Security_AutoTypeAsk).toBool() || matchList.size() > 1 || matchList.isEmpty() + || m_windowTitleForGlobal.isEmpty())) { // Close any open modal windows that would interfere with the process getMainWindow()->closeModalWindow(); @@ -486,18 +496,19 @@ void AutoType::performGlobalAutoType(const QList>& dbLi } connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject); - connect(selectDialog, - &AutoTypeSelectDialog::matchActivated, - this, - [this](const AutoTypeMatch& match, bool virtualMode) { - m_lastMatch = match; - m_lastMatchRetypeTimer.start(config()->get(Config::GlobalAutoTypeRetypeTime).toInt() * 1000); - executeAutoTypeActions(match.first, - match.second, - m_windowForGlobal, - virtualMode ? AutoTypeExecutor::Mode::VIRTUAL - : AutoTypeExecutor::Mode::NORMAL); - }); + connect( + selectDialog, + &AutoTypeSelectDialog::matchActivated, + this, + [this](const AutoTypeMatch& match, bool virtualMode) { + m_lastMatch = match; + m_lastMatchRetypeTimer.start(config()->get(Config::GlobalAutoTypeRetypeTime).toInt() * 1000); + executeAutoTypeActions(match.first, + match.second, + m_windowForGlobal, + virtualMode ? AutoTypeExecutor::Mode::VIRTUAL : AutoTypeExecutor::Mode::NORMAL); + }, + Qt::QueuedConnection); connect(selectDialog, &QDialog::rejected, this, [this] { restoreWindowState(); emit autotypeFinished(); @@ -510,6 +521,7 @@ void AutoType::performGlobalAutoType(const QList>& dbLi selectDialog->show(); selectDialog->raise(); selectDialog->activateWindow(); + m_plugin->prepareAutoType(); } else if (!matchList.isEmpty()) { // Only one match and not asking, do it! executeAutoTypeActions(matchList.first().first, matchList.first().second, m_windowForGlobal); diff --git a/src/autotype/AutoType.h b/src/autotype/AutoType.h index 174f956222..8fed7a7a42 100644 --- a/src/autotype/AutoType.h +++ b/src/autotype/AutoType.h @@ -28,6 +28,7 @@ #include "AutoTypeAction.h" #include "AutoTypeMatch.h" +#include "autotype/AutoTypePlatformPlugin.h" #include "core/Database.h" #include "core/Entry.h" @@ -52,6 +53,11 @@ class AutoType : public QObject return m_plugin; } + inline bool hasWindowAccess() + { + return m_plugin && m_plugin->hasWindowAccess(); + } + static AutoType* instance(); static void createTestInstance(); diff --git a/src/autotype/AutoTypePlatformPlugin.h b/src/autotype/AutoTypePlatformPlugin.h index 1e1e7b8465..f99e4a8bf3 100644 --- a/src/autotype/AutoTypePlatformPlugin.h +++ b/src/autotype/AutoTypePlatformPlugin.h @@ -31,10 +31,23 @@ class AutoTypePlatformInterface virtual WId activeWindow() = 0; virtual QString activeWindowTitle() = 0; virtual bool raiseWindow(WId window) = 0; + virtual bool hasWindowAccess() + { + return true; + } + virtual void unload() { } + virtual void prepareAutoType() + { + } + + virtual void finishAutoType() + { + } + virtual AutoTypeExecutor* createExecutor() = 0; #if defined(Q_OS_MACOS) diff --git a/src/autotype/CMakeLists.txt b/src/autotype/CMakeLists.txt index c00bbde34f..8e76e679cb 100644 --- a/src/autotype/CMakeLists.txt +++ b/src/autotype/CMakeLists.txt @@ -23,6 +23,8 @@ if(UNIX AND NOT APPLE AND NOT HAIKU) add_subdirectory(xcb) endif() + + 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..a3b3b0f06b --- /dev/null +++ b/src/autotype/wayland/AutoTypeWayland.cpp @@ -0,0 +1,427 @@ +/* + * Copyright (C) 2026 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 "core/Config.h" +#include "xdp_remotedesktop.h" +#include "xdp_session.h" + +#include "AutoTypeWayland.h" +#include "autotype/AutoTypeAction.h" +#include "core/Tools.h" +#include "gui/osutils/nixutils/NixUtils.h" + +#include +#include +#include + +#include + +static xkb_keysym_t qtKeyToXkbKeysym(Qt::Key key) +{ + switch (key) { + case Qt::Key_Tab: + return XKB_KEY_Tab; + case Qt::Key_Enter: + return XKB_KEY_Return; + case Qt::Key_Space: + return XKB_KEY_space; + case Qt::Key_Up: + return XKB_KEY_Up; + case Qt::Key_Down: + return XKB_KEY_Down; + case Qt::Key_Left: + return XKB_KEY_Left; + case Qt::Key_Right: + return XKB_KEY_Right; + case Qt::Key_Insert: + return XKB_KEY_Insert; + case Qt::Key_Delete: + return XKB_KEY_Delete; + case Qt::Key_Home: + return XKB_KEY_Home; + case Qt::Key_End: + return XKB_KEY_End; + case Qt::Key_PageUp: + return XKB_KEY_Page_Up; + case Qt::Key_PageDown: + return XKB_KEY_Page_Down; + case Qt::Key_Backspace: + return XKB_KEY_BackSpace; + case Qt::Key_Pause: + return XKB_KEY_Break; + case Qt::Key_CapsLock: + return XKB_KEY_Caps_Lock; + case Qt::Key_Escape: + return XKB_KEY_Escape; + case Qt::Key_Help: + return XKB_KEY_Help; + case Qt::Key_NumLock: + return XKB_KEY_Num_Lock; + case Qt::Key_Print: + return XKB_KEY_Print; + case Qt::Key_ScrollLock: + return XKB_KEY_Scroll_Lock; + case Qt::Key_Shift: + return XKB_KEY_Shift_L; + case Qt::Key_Control: + return XKB_KEY_Control_L; + case Qt::Key_Alt: + return XKB_KEY_Alt_L; + default: + if (key >= Qt::Key_F1 && key <= Qt::Key_F16) { + return XKB_KEY_F1 + (key - Qt::Key_F1); + } else if (key >= Qt::Key_Space && key <= Qt::Key_AsciiTilde) { + return key & 0xff; + } else { + return XKB_KEY_NoSymbol; + } + } +} + +Q_GLOBAL_STATIC_WITH_ARGS(OrgFreedesktopPortalRemoteDesktopInterface, + s_remoteDesktopInterface, + ("org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + QDBusConnection::sessionBus())); + +AutoTypePlatformWayland::AutoTypePlatformWayland() +{ +} + +void AutoTypePlatformWayland::prepareAutoType() +{ + tryStartSession(); +} + +void AutoTypePlatformWayland::finishAutoType() +{ + if (!config()->get(Config::AutoTypeDesktopPortalPersistConnection).toBool()) { + closeSession(); + } +} + +void AutoTypePlatformWayland::closeSession() +{ + if (m_remoteDesktopSession) { + m_remoteDesktopSession->Close(); + m_remoteDesktopSession = nullptr; + } +} + +void AutoTypePlatformWayland::waitForSession() +{ + tryStartSession(); + + if (m_sessionStarting) { + QEventLoop loop; + connect(this, &AutoTypePlatformWayland::sessionStartupFinished, &loop, &QEventLoop::quit); + loop.exec(); + } +} + +const QString AutoTypePlatformWayland::errorString() const +{ + return m_error; +} + +void AutoTypePlatformWayland::tryStartSession() +{ + if (m_sessionStarting || m_remoteDesktopSession) { + return; + } + + m_error.clear(); + m_sessionStarting = true; + + // create the remote desktop session + auto handleToken = request([this](const QVariantMap& results) { + m_remoteDesktopSession = new OrgFreedesktopPortalSessionInterface("org.freedesktop.portal.Desktop", + results["session_handle"].toString(), + QDBusConnection::sessionBus(), + this); + connect(m_remoteDesktopSession, &OrgFreedesktopPortalSessionInterface::Closed, this, [this]() { + if (m_error.isEmpty()) { + m_error = tr("Session closed"); + } + m_remoteDesktopSession = nullptr; + if (m_sessionStarting) { + m_sessionStarting = false; + emit sessionStartupFinished(); + } + }); + connect(m_remoteDesktopSession, + &OrgFreedesktopPortalSessionInterface::Closed, + m_remoteDesktopSession, + &QObject::deleteLater); + selectDevices(); + }); + auto sessionHandleToken = "keepassxc_" + QString::number(QRandomGenerator::system()->generate()); + auto reply = s_remoteDesktopInterface->CreateSession({ + {QLatin1String("session_handle_token"), sessionHandleToken}, + {QLatin1String("handle_token"), handleToken}, + }); + reply.waitForFinished(); + if (reply.isError()) { + m_error = "Failed to create remote desktop session:" + reply.error().message(); + m_sessionStarting = false; + emit sessionStartupFinished(); + } +} + +QString AutoTypePlatformWayland::request(const std::function handler) +{ + return nixUtils()->portalRequest([this, handler](uint response, const QVariantMap& result) { + switch (response) { + case 0: + handler(result); + return; + case 1: + m_error = tr("User cancelled the interaction"); + break; + default: + m_error = tr("User interaction was canceled for unknown reason"); + break; + } + + m_sessionStarting = false; + if (m_remoteDesktopSession) { + m_remoteDesktopSession->Close(); + m_remoteDesktopSession = nullptr; + } + emit sessionStartupFinished(); + }); +} + +void AutoTypePlatformWayland::selectDevices() +{ + auto handleToken = request([this](const QVariantMap&) { startSession(); }); + + auto persistMode = config()->get(Config::AutoTypeDesktopPortalPersistMode).toUInt(); + auto options = QVariantMap{ + {QLatin1String("handle_token"), handleToken}, + {QLatin1String("types"), uint(1)}, + {QLatin1String("persist_mode"), persistMode}, + }; + + auto restoreToken = config()->get(Config::AutoTypeDesktopPortalRestoreToken).toString(); + if (persistMode < 2 && !restoreToken.isEmpty()) { + config()->set(Config::AutoTypeDesktopPortalRestoreToken, ""); + } else if (!restoreToken.isEmpty()) { + options["restore_token"] = restoreToken; + } else if (!m_restoreToken.isEmpty()) { + options["restore_token"] = m_restoreToken; + } + + auto reply = s_remoteDesktopInterface->SelectDevices(QDBusObjectPath(m_remoteDesktopSession->path()), options); + reply.waitForFinished(); + if (reply.isError()) { + m_error = "Failed to select remote desktop devices: " + reply.error().message(); + m_sessionStarting = false; + if (m_remoteDesktopSession) { + m_remoteDesktopSession->Close(); + m_remoteDesktopSession = nullptr; + } + emit sessionStartupFinished(); + } +} + +void AutoTypePlatformWayland::startSession() +{ + auto handleToken = request([this](const QVariantMap& results) { + auto persistMode = config()->get(Config::AutoTypeDesktopPortalPersistMode).toUInt(); + if (persistMode >= 2) { + m_restoreToken = ""; + config()->set(Config::AutoTypeDesktopPortalRestoreToken, results["restore_token"].toString()); + } else if (persistMode == 1) { + m_restoreToken = results["restore_token"].toString(); + } else { + m_restoreToken = ""; + config()->set(Config::AutoTypeDesktopPortalRestoreToken, ""); + } + m_error.clear(); + m_sessionStarting = false; + emit sessionStartupFinished(); + }); + + auto reply = s_remoteDesktopInterface->Start(QDBusObjectPath(m_remoteDesktopSession->path()), + "", + { + {QLatin1String("handle_token"), handleToken}, + }); + reply.waitForFinished(); + if (reply.isError()) { + m_error = "Failed to start remote desktop session: " + reply.error().message(); + m_sessionStarting = false; + emit sessionStartupFinished(); + } +} + +bool AutoTypePlatformWayland::isAvailable() +{ + if (!s_remoteDesktopInterface->isValid()) { + qWarning() << "XDG Remote Desktop portal is not available, Auto-Type disabled"; + return false; + } + if ((s_remoteDesktopInterface->availableDeviceTypes() & 1) == 0) { + qWarning() << "XDG Remote Desktop portal does not support keyboard input, Auto-Type disabled"; + return false; + } + return true; +} + +void AutoTypePlatformWayland::unload() +{ + closeSession(); +} + +QStringList AutoTypePlatformWayland::windowTitles() +{ + return {}; +} + +WId AutoTypePlatformWayland::activeWindow() +{ + // return a different value if the focus changes to our own window to stop sequence + if (qApp->activeWindow()) { + return -2; + } + + // return non-zero value to avoid minimizing our own window if it's not in focus + return -1; +} + +QString AutoTypePlatformWayland::activeWindowTitle() +{ + return {}; +} + +bool AutoTypePlatformWayland::raiseWindow(WId window) +{ + Q_UNUSED(window); + + return true; +} + +AutoTypeAction::Result AutoTypePlatformWayland::sendKey(const AutoTypeKey* action) +{ + xkb_keysym_t keysym; + if (action->key != Qt::Key_unknown) { + keysym = qtKeyToXkbKeysym(action->key); + if (keysym == XKB_KEY_NoSymbol) { + return AutoTypeAction::Result::Failed(tr("No symbol found for key: '%1'").arg(action->key)); + } + } else { + keysym = xkb_utf32_to_keysym(action->character.unicode()); + if (keysym == XKB_KEY_NoSymbol) { + return AutoTypeAction::Result::Failed(tr("No symbol found for character: '%1'").arg(action->character)); + } + } + + QVector modKeys{}; + if (action->modifiers & Qt::ShiftModifier) { + modKeys.append(XKB_KEY_Shift_L); + } + if (action->modifiers & Qt::ControlModifier) { + modKeys.append(XKB_KEY_Control_L); + } + if (action->modifiers & Qt::AltModifier) { + modKeys.append(XKB_KEY_Alt_L); + } + if (action->modifiers & Qt::MetaModifier) { + modKeys.append(XKB_KEY_Meta_L); + } + + QDBusPendingReply<> reply; + + for (auto modifier : modKeys) { + reply = s_remoteDesktopInterface->NotifyKeyboardKeysym( + QDBusObjectPath(m_remoteDesktopSession->path()), {}, modifier, uint(1)); + reply.waitForFinished(); + if (reply.isError()) { + return AutoTypeAction::Result::Failed(reply.error().message()); + } + } + + reply = s_remoteDesktopInterface->NotifyKeyboardKeysym( + QDBusObjectPath(m_remoteDesktopSession->path()), {}, keysym, uint(1)); + reply.waitForFinished(); + if (reply.isError()) { + return AutoTypeAction::Result::Failed(reply.error().message()); + } + + reply = s_remoteDesktopInterface->NotifyKeyboardKeysym( + QDBusObjectPath(m_remoteDesktopSession->path()), {}, keysym, uint(0)); + reply.waitForFinished(); + if (reply.isError()) { + return AutoTypeAction::Result::Failed(reply.error().message()); + } + + for (auto modifier : modKeys) { + reply = s_remoteDesktopInterface->NotifyKeyboardKeysym( + QDBusObjectPath(m_remoteDesktopSession->path()), {}, modifier, uint(0)); + reply.waitForFinished(); + if (reply.isError()) { + return AutoTypeAction::Result::Failed(reply.error().message()); + } + } + + return AutoTypeAction::Result::Ok(); +} + +AutoTypeExecutor* AutoTypePlatformWayland::createExecutor() +{ + return new AutoTypeExecutorWayland(this); +} + +AutoTypeExecutorWayland::AutoTypeExecutorWayland(AutoTypePlatformWayland* platform) + : m_platform(platform) +{ +} + +AutoTypeAction::Result AutoTypeExecutorWayland::execBegin(const AutoTypeBegin* action) +{ + Q_UNUSED(action); + + m_platform->waitForSession(); + + auto error = m_platform->errorString(); + if (!error.isEmpty()) { + return AutoTypeAction::Result::Failed(error); + } + + return AutoTypeAction::Result::Ok(); +} + +AutoTypeAction::Result AutoTypeExecutorWayland::execType(const AutoTypeKey* action) +{ + auto result = m_platform->sendKey(action); + + if (result.isOk()) { + Tools::sleep(execDelayMs); + } + + return result; +} + +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..2969f8eb22 --- /dev/null +++ b/src/autotype/wayland/AutoTypeWayland.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2026 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_AUTOTYPEWAYLAND_H +#define KEEPASSXC_AUTOTYPEWAYLAND_H + +#include +#include +#include +#include +#include + +#include "autotype/AutoTypePlatformPlugin.h" +#include "xdp_session.h" + +class OrgFreedesktopPortalRequestInterface; + +class AutoTypePlatformWayland : public QObject, public AutoTypePlatformInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.keepassx.AutoTypePlatformWayland") + Q_INTERFACES(AutoTypePlatformInterface) + +signals: + void sessionStartupFinished(); + +public: + AutoTypePlatformWayland(); + bool isAvailable() override; + QStringList windowTitles() override; + WId activeWindow() override; + QString activeWindowTitle() override; + bool raiseWindow(WId window) override; + bool hasWindowAccess() override + { + return false; + }; + void unload() override; + void prepareAutoType() override; + void finishAutoType() override; + + AutoTypeExecutor* createExecutor() override; + AutoTypeAction::Result sendKey(const AutoTypeKey*); + void waitForSession(); + void closeSession(); + const QString errorString() const; + +private: + QString request(const std::function handler); + void tryStartSession(); + void selectDevices(); + void startSession(); + + bool m_sessionStarting = false; + QString m_restoreToken = QString(); + OrgFreedesktopPortalSessionInterface* m_remoteDesktopSession = nullptr; + QString m_error = QString(); +}; + +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 // KEEPASSXC_AUTOTYPEWAYLAND_H diff --git a/src/autotype/wayland/CMakeLists.txt b/src/autotype/wayland/CMakeLists.txt new file mode 100644 index 0000000000..464c48cbf6 --- /dev/null +++ b/src/autotype/wayland/CMakeLists.txt @@ -0,0 +1,10 @@ +find_package(PkgConfig REQUIRED) +pkg_check_modules(Xkbcommon REQUIRED IMPORTED_TARGET xkbcommon) + +set(autotype_WAYLAND_SOURCES AutoTypeWayland.cpp) + +add_library(keepassxc-autotype-wayland MODULE ${autotype_WAYLAND_SOURCES}) +target_link_libraries(keepassxc-autotype-wayland keepassxc_gui Qt6::Core Qt6::DBus PkgConfig::Xkbcommon) +install(TARGETS keepassxc-autotype-wayland + BUNDLE DESTINATION . COMPONENT Runtime + LIBRARY DESTINATION ${PLUGIN_INSTALL_DIR} COMPONENT Runtime) diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 261a92af1d..9667a47915 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -83,6 +83,9 @@ static const QHash configStrings = { {Config::AutoTypeHideExpiredEntry,{QS("AutoTypeHideExpiredEntry"), Roaming, false}}, {Config::AutoTypeDialogSortColumn,{QS("AutoTypeDialogSortColumn"), Roaming, 0}}, {Config::AutoTypeDialogSortOrder,{QS("AutoTypeDialogSortOrder"), Roaming, Qt::AscendingOrder}}, + {Config::AutoTypeDesktopPortalPersistConnection,{QS("AutoTypeDesktopPortalPersistConnection"), Roaming, false}}, + {Config::AutoTypeDesktopPortalPersistMode,{QS("AutoTypeDesktopPortalPersistMode"), Roaming, 1}}, + {Config::AutoTypeDesktopPortalRestoreToken,{QS("AutoTypeDesktopPortalRestoreToken"), Local, ""}}, {Config::GlobalAutoTypeKey,{QS("GlobalAutoTypeKey"), Roaming, 0}}, {Config::GlobalAutoTypeModifiers,{QS("GlobalAutoTypeModifiers"), Roaming, 0}}, {Config::GlobalAutoTypeRetypeTime,{QS("GlobalAutoTypeRetypeTime"), Roaming, 15}}, @@ -125,6 +128,7 @@ static const QHash configStrings = { {Config::GUI_ShowExpiredEntriesOnDatabaseUnlock, {QS("GUI/ShowExpiredEntriesOnDatabaseUnlock"), Roaming, true}}, {Config::GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays, {QS("GUI/ShowExpiredEntriesOnDatabaseUnlockOffsetDays"), Roaming, 3}}, {Config::GUI_FontSizeOffset, {QS("GUI/FontSizeOffset"), Local, 0}}, + {Config::GUI_XDPGlobalShortcutsConfigured, {QS("GUI/XDPGlobalShortcutsConfigured"), Local, false}}, {Config::GUI_MainWindowGeometry, {QS("GUI/MainWindowGeometry"), Local, {}}}, {Config::GUI_MainWindowState, {QS("GUI/MainWindowState"), Local, {}}}, diff --git a/src/core/Config.h b/src/core/Config.h index 8f54f9c013..12b3370031 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -64,6 +64,9 @@ class Config : public QObject AutoTypeHideExpiredEntry, AutoTypeDialogSortColumn, AutoTypeDialogSortOrder, + AutoTypeDesktopPortalPersistConnection, + AutoTypeDesktopPortalPersistMode, + AutoTypeDesktopPortalRestoreToken, GlobalAutoTypeKey, GlobalAutoTypeModifiers, GlobalAutoTypeRetypeTime, @@ -104,6 +107,7 @@ class Config : public QObject GUI_ShowExpiredEntriesOnDatabaseUnlock, GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays, GUI_FontSizeOffset, + GUI_XDPGlobalShortcutsConfigured, GUI_MainWindowGeometry, GUI_MainWindowState, diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index 432525ad6c..7fa28d5a5d 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -154,6 +154,11 @@ void Application::bootstrap(const QString& uiLanguage) Bootstrap::bootstrap(uiLanguage); osUtils->registerNativeEventFilter(); + +#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) + nixUtils()->initGlobalShortcutsSession(); +#endif + MessageBox::initializeButtonDefs(); #ifdef Q_OS_MACOS diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index bb66b2aa95..5b0ac323e3 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -17,10 +17,13 @@ */ #include "ApplicationSettingsWidget.h" +#include "gui/osutils/OSUtilsBase.h" #include "ui_ApplicationSettingsWidgetGeneral.h" #include "ui_ApplicationSettingsWidgetSecurity.h" #include #include +#include +#include #include #include "config-keepassx.h" @@ -86,6 +89,42 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) if (!autoType()->isAvailable()) { m_generalUi->generalSettingsTabWidget->removeTab(1); + } else { + if (autoType()->hasWindowAccess()) { + m_generalUi->autoTypeEntryTitleMatchCheckBox->setVisible(true); + m_generalUi->autoTypeEntryURLMatchCheckBox->setVisible(true); + m_generalUi->autoTypeAskCheckBox->setDisabled(false); + + m_generalUi->autoTypeDesktopPortalPersistConnectionCheckBox->setVisible(false); + m_generalUi->autoTypeDesktopPortalPersistModeLabel->setVisible(false); + m_generalUi->autoTypeDesktopPortalPersistModeComboBox->setVisible(false); + } else { + m_generalUi->autoTypeEntryTitleMatchCheckBox->setVisible(false); + m_generalUi->autoTypeEntryURLMatchCheckBox->setVisible(false); + m_generalUi->autoTypeAskCheckBox->setChecked(true); + m_generalUi->autoTypeAskCheckBox->setDisabled(true); + + // effectively these only get shown with the Wayland plugin enabled + m_generalUi->autoTypeDesktopPortalPersistConnectionCheckBox->setVisible(true); + m_generalUi->autoTypeDesktopPortalPersistModeLabel->setVisible(true); + m_generalUi->autoTypeDesktopPortalPersistModeComboBox->setVisible(true); + } + + if (osUtils->externalGlobalShortcutsConfigurator()) { + m_generalUi->autoTypeShortcutWidget->setVisible(false); + m_generalUi->autoTypeShortcutConfigureWidget->setVisible(true); + + connect(osUtils, &OSUtilsBase::globalShortcutChanged, this, [this](const QString& description) { + m_generalUi->autoTypeShortcutConfigureLabel->setText(description.isEmpty() ? tr("None") : description); + }); + connect(m_generalUi->autoTypeShortcutConfigureButton, + &QPushButton::clicked, + osUtils, + &OSUtilsBase::configureGlobalShortcut); + } else { + m_generalUi->autoTypeShortcutWidget->setVisible(true); + m_generalUi->autoTypeShortcutConfigureWidget->setVisible(false); + } } connect(this, SIGNAL(accepted()), SLOT(saveSettings())); @@ -226,6 +265,10 @@ void ApplicationSettingsWidget::loadSettings() m_generalUi->autoTypeEntryTitleMatchCheckBox->setChecked(config()->get(Config::AutoTypeEntryTitleMatch).toBool()); m_generalUi->autoTypeEntryURLMatchCheckBox->setChecked(config()->get(Config::AutoTypeEntryURLMatch).toBool()); m_generalUi->autoTypeHideExpiredEntryCheckBox->setChecked(config()->get(Config::AutoTypeHideExpiredEntry).toBool()); + m_generalUi->autoTypeDesktopPortalPersistConnectionCheckBox->setChecked( + config()->get(Config::AutoTypeDesktopPortalPersistConnection).toBool()); + m_generalUi->autoTypeDesktopPortalPersistModeComboBox->setCurrentIndex( + config()->get(Config::AutoTypeDesktopPortalPersistMode).toUInt()); m_generalUi->faviconTimeoutSpinBox->setValue(config()->get(Config::FaviconDownloadTimeout).toInt()); m_generalUi->ConfirmMoveEntryToRecycleBinCheckBox->setChecked( !config()->get(Config::Security_NoConfirmMoveEntryToRecycleBin).toBool()); @@ -303,7 +346,8 @@ void ApplicationSettingsWidget::loadSettings() m_globalAutoTypeKey = static_cast(config()->get(Config::GlobalAutoTypeKey).toInt()); m_globalAutoTypeModifiers = static_cast(config()->get(Config::GlobalAutoTypeModifiers).toInt()); - if (m_globalAutoTypeKey > 0 && m_globalAutoTypeModifiers > 0) { + if (!osUtils->externalGlobalShortcutsConfigurator() && m_globalAutoTypeKey > 0 + && m_globalAutoTypeModifiers > 0) { m_generalUi->autoTypeShortcutWidget->setShortcut(m_globalAutoTypeKey, m_globalAutoTypeModifiers); } m_generalUi->autoTypeRetypeTimeSpinBox->setValue(config()->get(Config::GlobalAutoTypeRetypeTime).toInt()); @@ -402,6 +446,10 @@ void ApplicationSettingsWidget::saveSettings() config()->set(Config::AutoTypeEntryTitleMatch, m_generalUi->autoTypeEntryTitleMatchCheckBox->isChecked()); config()->set(Config::AutoTypeEntryURLMatch, m_generalUi->autoTypeEntryURLMatchCheckBox->isChecked()); config()->set(Config::AutoTypeHideExpiredEntry, m_generalUi->autoTypeHideExpiredEntryCheckBox->isChecked()); + config()->set(Config::AutoTypeDesktopPortalPersistConnection, + m_generalUi->autoTypeDesktopPortalPersistConnectionCheckBox->isChecked()); + config()->set(Config::AutoTypeDesktopPortalPersistMode, + m_generalUi->autoTypeDesktopPortalPersistModeComboBox->currentIndex()); config()->set(Config::FaviconDownloadTimeout, m_generalUi->faviconTimeoutSpinBox->value()); config()->set(Config::Security_NoConfirmMoveEntryToRecycleBin, !m_generalUi->ConfirmMoveEntryToRecycleBinCheckBox->isChecked()); diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui index 42ce4acc16..e7c56bdd92 100644 --- a/src/gui/ApplicationSettingsWidgetGeneral.ui +++ b/src/gui/ApplicationSettingsWidgetGeneral.ui @@ -42,13 +42,13 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Plain + QFrame::Shadow::Plain - Qt::ScrollBarAlwaysOff + Qt::ScrollBarPolicy::ScrollBarAlwaysOff true @@ -58,8 +58,8 @@ 0 0 - 568 - 1226 + 604 + 1502 @@ -143,7 +143,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -161,15 +161,15 @@ 0 - QLayout::SetMaximumSize + QLayout::SizeConstraint::SetMaximumSize - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -197,15 +197,15 @@ 0 - QLayout::SetMaximumSize + QLayout::SizeConstraint::SetMaximumSize - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -242,10 +242,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -273,7 +273,7 @@ 6 - QLayout::SetMaximumSize + QLayout::SizeConstraint::SetMaximumSize @@ -288,7 +288,7 @@ true - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus On database unlock, show entries that will expire within @@ -310,7 +310,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -374,10 +374,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -406,10 +406,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -435,10 +435,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -475,10 +475,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -494,7 +494,7 @@ false - QComboBox::AdjustToContents + QComboBox::SizeAdjustPolicy::AdjustToContents @@ -511,7 +511,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -568,13 +568,13 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Double-click action for URL field - QComboBox::AdjustToContents + QComboBox::SizeAdjustPolicy::AdjustToContents @@ -596,7 +596,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -640,10 +640,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -673,10 +673,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -716,7 +716,7 @@ true - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Website icon download timeout in seconds @@ -738,7 +738,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -767,7 +767,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -789,7 +789,7 @@ Language: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter languageComboBox @@ -821,13 +821,13 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Toolbar button style - QComboBox::AdjustToContents + QComboBox::SizeAdjustPolicy::AdjustToContents @@ -840,13 +840,13 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Language selection - QComboBox::AdjustToContents + QComboBox::SizeAdjustPolicy::AdjustToContents @@ -865,7 +865,7 @@ Toolbar button style: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter toolButtonStyleComboBox @@ -885,7 +885,7 @@ Font size: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter fontSizeComboBox @@ -895,13 +895,13 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Font size selection - QComboBox::AdjustToContents + QComboBox::SizeAdjustPolicy::AdjustToContents @@ -955,7 +955,7 @@ - QLayout::SetMaximumSize + QLayout::SizeConstraint::SetMaximumSize 0 @@ -963,10 +963,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -988,13 +988,13 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Tray icon type - QComboBox::AdjustToContents + QComboBox::SizeAdjustPolicy::AdjustToContents @@ -1014,7 +1014,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -1027,10 +1027,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -1063,15 +1063,15 @@ 0 - QLayout::SetMaximumSize + QLayout::SizeConstraint::SetMaximumSize - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -1111,7 +1111,7 @@ 6 - QLayout::SetMaximumSize + QLayout::SizeConstraint::SetMaximumSize @@ -1123,7 +1123,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -1152,7 +1152,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -1217,10 +1217,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -1259,13 +1259,20 @@ + + + + Keep remote desktop connection open after performing Auto-Type + + + - Qt::Vertical + Qt::Orientation::Vertical - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -1283,33 +1290,7 @@ 8 - - - - Auto-Type start delay: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - autoTypeStartDelaySpinBox - - - - - - - Global Auto-Type shortcut: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - autoTypeShortcutWidget - - - - + @@ -1341,45 +1322,42 @@ - + - Auto-Type typing delay: + Auto-Type start delay: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - autoTypeDelaySpinBox + autoTypeStartDelaySpinBox - - - - Qt::Horizontal + + + + Remote desktop mode: - - - 0 - 20 - + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - + - - - - - 0 - 0 - + + + + Auto-Type typing delay: - - Global auto-type shortcut + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + autoTypeDelaySpinBox - + @@ -1404,17 +1382,122 @@ - + + + + Global Auto-Type shortcut: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + autoTypeShortcutWidget + + + + + + + + 0 + 0 + + + + + Never remember session + + + + + Remember session until exit + + + + + Remember session until revoked by desktop + + + + + Remember last typed entry for: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - + + + + + + + 0 + 0 + + + + Global auto-type shortcut + + + + + + + + + + true + + + + 20 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + 0 + 0 + + + + None + + + + + + + Configure... + + + + + + + + + sec @@ -1430,12 +1513,38 @@ + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + - Qt::Vertical + Qt::Orientation::Vertical @@ -1510,7 +1619,10 @@ autoTypeAskCheckBox autoTypeHideExpiredEntryCheckBox autoTypeRelockDatabaseCheckBox + autoTypeDesktopPortalPersistConnectionCheckBox autoTypeShortcutWidget + autoTypeShortcutConfigureButton + autoTypeDesktopPortalPersistModeComboBox autoTypeRetypeTimeSpinBox autoTypeStartDelaySpinBox autoTypeDelaySpinBox diff --git a/src/gui/osutils/OSUtilsBase.h b/src/gui/osutils/OSUtilsBase.h index 11d739fde6..36a947d26a 100644 --- a/src/gui/osutils/OSUtilsBase.h +++ b/src/gui/osutils/OSUtilsBase.h @@ -72,8 +72,19 @@ class OSUtilsBase : public QObject virtual bool canPreventScreenCapture() const = 0; virtual bool setPreventScreenCapture(QWindow* window, bool allow) const; + virtual bool externalGlobalShortcutsConfigurator() + { + return false; + } + +public slots: + virtual void configureGlobalShortcut() + { + } + signals: void globalShortcutTriggered(const QString& name, const QString& search = {}); + void globalShortcutChanged(const QString& description); /** * Indicates platform UI theme change (light mode to dark mode). diff --git a/src/gui/osutils/nixutils/NixUtils.cpp b/src/gui/osutils/nixutils/NixUtils.cpp index c2901cc62b..451f9d2567 100644 --- a/src/gui/osutils/nixutils/NixUtils.cpp +++ b/src/gui/osutils/nixutils/NixUtils.cpp @@ -17,6 +17,12 @@ #include "NixUtils.h" +#include "gui/MainWindow.h" +#include "gui/MessageBox.h" +#include "xdp_globalshortcuts.h" +#include "xdp_request.h" +#include "xdp_session.h" + #include "config-keepassx.h" #include "core/Config.h" #include "core/Global.h" @@ -31,6 +37,7 @@ #include #include #include +#include #ifdef WITH_X11 #include @@ -52,6 +59,15 @@ namespace } // namespace #endif +Q_GLOBAL_STATIC_WITH_ARGS(OrgFreedesktopPortalGlobalShortcutsInterface, + s_shortcutsInterface, + ("org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + QDBusConnection::sessionBus())); + +using XdpShortcut = QPair; +using XdpShortcuts = QList; + QPointer NixUtils::m_instance = nullptr; NixUtils* NixUtils::instance() @@ -90,6 +106,55 @@ NixUtils::NixUtils(QObject* parent) } } +void NixUtils::initGlobalShortcutsSession() +{ + if (!externalGlobalShortcutsConfigurator() || !s_shortcutsInterface->isValid()) { + return; + } + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + connect(s_shortcutsInterface, + &OrgFreedesktopPortalGlobalShortcutsInterface::Activated, + this, + [this](const QDBusObjectPath& session_handle, + const QString& shortcut_id, + qulonglong timestamp, + const QVariantMap& options) { + Q_UNUSED(timestamp) + Q_UNUSED(options) + + if (!m_globalShortcutsSession) { + qWarning() << "Global shortcut activated without session"; + } else if (m_globalShortcutsSession->path() != session_handle.path()) { + qWarning() << "Global shortcut activated for wrong session:" << session_handle.path() + << "!=" << m_globalShortcutsSession->path(); + } else if (shortcut_id == "autotype") { + emit globalShortcutTriggered(shortcut_id); + } else { + qWarning() << "Unknown global shortcut activated:" << shortcut_id; + } + }); + + connect(s_shortcutsInterface, + &OrgFreedesktopPortalGlobalShortcutsInterface::ShortcutsChanged, + this, + [this](const QDBusObjectPath& session_handle, const XdpShortcuts& shortcuts) { + if (!m_globalShortcutsSession || session_handle.path() != m_globalShortcutsSession->path()) { + return; + } + for (const auto& [id, map] : shortcuts) { + if (id == QLatin1String("autotype")) { + emit globalShortcutChanged(map.value(QStringLiteral("trigger_description")).toString()); + return; + } + } + }); + + createGlobalShortcutsSession(); +} + NixUtils::~NixUtils() = default; bool NixUtils::isDarkMode() const @@ -293,6 +358,10 @@ bool NixUtils::triggerGlobalShortcut(uint keycode, uint modifiers) bool NixUtils::registerGlobalShortcut(const QString& name, Qt::Key key, Qt::KeyboardModifiers modifiers, QString* error) { #ifdef WITH_X11 + if (QApplication::platformName() != "xcb") { + return true; + } + auto keycode = XKeysymToKeycode(dpy, qtToNativeKeyCode(key)); auto modifierscode = qtToNativeModifiers(modifiers); @@ -409,3 +478,197 @@ quint64 NixUtils::getProcessStartTime() const qDebug() << "nixutils: failed to find ')' in " << processStatPath; return 0; } + +// Implements only 0.9+ requests where the path is known before making the call +QString NixUtils::portalRequest(const std::function handler) +{ + static uint next; + auto bus = QDBusConnection::sessionBus(); + auto token = QString("request_%1_%2").arg(++next).arg(QRandomGenerator::system()->generate()); + auto sender = bus.baseService().remove(0, 1).replace(".", "_"); + + QStringList path; + path << "" << "org" << "freedesktop" << "portal" << "desktop" << "request" << sender << token; + + auto req = new OrgFreedesktopPortalRequestInterface("org.freedesktop.portal.Desktop", path.join('/'), bus, this); + connect(req, &OrgFreedesktopPortalRequestInterface::Response, req, handler); + connect(req, &OrgFreedesktopPortalRequestInterface::Response, req, &QObject::deleteLater); + + auto timer = new QTimer(req); + timer->setSingleShot(true); + connect(timer, &QTimer::timeout, req, [req]() { + qWarning() << "NixUtils::portalRequest: timed out waiting for portal response, closing request"; + req->Close(); + req->deleteLater(); + }); + connect(req, &OrgFreedesktopPortalRequestInterface::Response, timer, &QTimer::stop); + timer->start(30000); + + return token; +} + +bool NixUtils::externalGlobalShortcutsConfigurator() +{ + return QApplication::platformName() == "wayland"; +} + +void NixUtils::bindShortcutsToCurrentSession() +{ + // Only attempt to restore bindings if previously configured + if (!config()->get(Config::GUI_XDPGlobalShortcutsConfigured).toBool()) { + return; + } + + // Use ListShortcuts to check if shortcuts are already active (e.g. KDE session restoration), on GNOME we need to + // rebind them every time + auto listToken = portalRequest([this](uint listResponse, const QVariantMap& listResults) { + if (listResponse != 0) { + qWarning() << "NixUtils::bindShortcutsToCurrentSession ListShortcuts failed with response:" << listResponse; + return; + } + + auto existing = qdbus_cast(listResults.value("shortcuts")); + if (!existing.isEmpty()) { + for (const auto& [id, map] : existing) { + if (id == QLatin1String("autotype")) { + emit globalShortcutChanged(map.value(QStringLiteral("trigger_description")).toString()); + break; + } + } + return; // already active, nothing to do + } + + if (!m_globalShortcutsSession) { + qWarning() << "NixUtils::bindShortcutsToCurrentSession: session was closed, aborting bind"; + return; + } + + callBindShortcuts(); + }); + + auto listReply = s_shortcutsInterface->ListShortcuts(QDBusObjectPath(m_globalShortcutsSession->path()), + {{QLatin1String("handle_token"), listToken}}); + listReply.waitForFinished(); + if (listReply.isError()) { + qWarning() << "NixUtils::bindShortcutsToCurrentSession ListShortcuts failed:" << listReply.error().message(); + } +} + +void NixUtils::createGlobalShortcutsSession() +{ + auto handleToken = portalRequest([this](uint createSessionResponse, const QVariantMap& results) { + if (createSessionResponse != 0) { + qWarning() << "NixUtils::createGlobalShortcutsSession CreateSession got unexpected response from portal:" + << createSessionResponse; + return; + } + + m_globalShortcutsSession = new OrgFreedesktopPortalSessionInterface("org.freedesktop.portal.Desktop", + results["session_handle"].toString(), + QDBusConnection::sessionBus(), + this); + connect(m_globalShortcutsSession, + &OrgFreedesktopPortalSessionInterface::Closed, + m_globalShortcutsSession, + &QObject::deleteLater); + connect(m_globalShortcutsSession, &OrgFreedesktopPortalSessionInterface::Closed, this, [this]() { + m_globalShortcutsSession = nullptr; + QTimer::singleShot(1000, this, &NixUtils::createGlobalShortcutsSession); + }); + + bindShortcutsToCurrentSession(); + }); + + auto sessionHandleToken = "keepassxc_" + QString::number(QRandomGenerator::global()->generate()); + auto reply = s_shortcutsInterface->CreateSession({ + {QLatin1String("session_handle_token"), sessionHandleToken}, + {QLatin1String("handle_token"), handleToken}, + }); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << "Failed to create Global Shortcuts session" << reply.error().message(); + } +} + +void NixUtils::configureGlobalShortcut() +{ + if (!s_shortcutsInterface->isValid() || !m_globalShortcutsSession) { + MessageBox::warning(getMainWindow(), + tr("KeePassXC - Global Shortcuts"), + tr("The XDG Desktop Portal for global shortcuts is not available on this system.")); + return; + } + + // Use the config flag to determine if shortcuts have been configured before. + // We cannot use ListShortcuts here because on GNOME, ListShortcuts always returns empty + // even after BindShortcuts was called (newer GNOME auto-applies previously saved shortcuts + // silently without populating the session's shortcuts array). Relying on ListShortcuts + // would cause BindShortcuts to be called again, but the portal rejects it (bound=true), + // resulting in no dialog being shown. On KDE, if this flag gets out-of-sync it doesn't matter because on startup + // we'll call BindShortcuts if ListShortcuts returns an empty list so the shortcuts will be bound before we get + // here. + if (config()->get(Config::GUI_XDPGlobalShortcutsConfigured).toBool()) { + // Already configured: portal v2+ can open a reconfiguration dialog directly + if (s_shortcutsInterface->version() >= 2) { + auto handleToken = portalRequest([](uint response, const QVariantMap&) { Q_UNUSED(response); }); + auto reply = s_shortcutsInterface->ConfigureShortcuts( + QDBusObjectPath(m_globalShortcutsSession->path()), "", {{QLatin1String("handle_token"), handleToken}}); + reply.waitForFinished(); + if (!reply.isError()) { + return; + } + + // Additionally, the frontend portal may expose v2 but the backend implementation doesn't support it so the + // method call can fail anyway + qWarning() << "NixUtils::configureGlobalShortcuts ConfigureShortcuts failed, falling through to dialog:" + << reply.error().message(); + } + + // Portal v1 (e.g. GNOME): no reconfiguration API, direct the user to system settings + MessageBox::information(getMainWindow(), + tr("KeePassXC - Global Shortcuts"), + tr("Global Auto-Type shortcut is already configured. " + "To change it, open your system settings and navigate to the " + "keyboard or application shortcuts section.")); + return; + } + + // First-time setup: use BindShortcuts to present the user with a system dialog. + // bindShortcutsToCurrentSession() skips BindShortcuts when the config flag is false, + // so the session's bound flag is still false and this call will succeed. + callBindShortcuts(); +} + +void NixUtils::callBindShortcuts() +{ + XdpShortcuts shortcuts = { + {QLatin1String("autotype"), {{QStringLiteral("description"), tr("Trigger global Auto-Type")}}}}; + + auto bindToken = portalRequest([this](uint bindResponse, const QVariantMap& bindResults) { + if (bindResponse != 0) { + qWarning() << "NixUtils: BindShortcuts returned response" << bindResponse + << "(shortcut may still be active; known GNOME desktop portal bug in older versions)"; + } else { + auto bound = qdbus_cast(bindResults.value("shortcuts")); + if (!bound.isEmpty()) { + for (const auto& [id, map] : bound) { + if (id == QLatin1String("autotype")) { + emit globalShortcutChanged(map.value(QStringLiteral("trigger_description")).toString()); + break; + } + } + } + } + + // Setting this unconditionally because if the config is out-of-sync with system settings this'll ensure we + // rebind them on startup + config()->set(Config::GUI_XDPGlobalShortcutsConfigured, true); + }); + + auto reply = s_shortcutsInterface->BindShortcuts( + QDBusObjectPath(m_globalShortcutsSession->path()), shortcuts, "", {{QLatin1String("handle_token"), bindToken}}); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << "NixUtils::callBindShortcuts BindShortcuts failed:" << reply.error().message(); + } +} diff --git a/src/gui/osutils/nixutils/NixUtils.h b/src/gui/osutils/nixutils/NixUtils.h index 96602ec7d7..94dba062b2 100644 --- a/src/gui/osutils/nixutils/NixUtils.h +++ b/src/gui/osutils/nixutils/NixUtils.h @@ -22,6 +22,9 @@ #include #include #include +#include + +class OrgFreedesktopPortalSessionInterface; class NixUtils : public OSUtilsBase, QAbstractNativeEventFilter { @@ -52,6 +55,14 @@ class NixUtils : public OSUtilsBase, QAbstractNativeEventFilter quint64 getProcessStartTime() const; + bool externalGlobalShortcutsConfigurator() override; + + void initGlobalShortcutsSession(); + QString portalRequest(const std::function handler); + +public slots: + void configureGlobalShortcut() override; + private slots: void handleColorSchemeChanged(QString ns, QString key, QDBusVariant value); void launchAtStartupRequested(uint response, const QVariantMap& results); @@ -86,6 +97,11 @@ private slots: bool m_systemColorschemePrefExists = false; void setColorScheme(QDBusVariant value); + void createGlobalShortcutsSession(); + void bindShortcutsToCurrentSession(); + void callBindShortcuts(); + + OrgFreedesktopPortalSessionInterface* m_globalShortcutsSession = nullptr; Q_DISABLE_COPY(NixUtils) }; diff --git a/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.GlobalShortcuts.xml b/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.GlobalShortcuts.xml new file mode 100644 index 0000000000..26b2fb753b --- /dev/null +++ b/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.GlobalShortcuts.xml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.RemoteDesktop.xml b/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.RemoteDesktop.xml new file mode 100644 index 0000000000..4fe410530e --- /dev/null +++ b/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.RemoteDesktop.xml @@ -0,0 +1,477 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.Request.xml b/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.Request.xml new file mode 100644 index 0000000000..e8a26484e2 --- /dev/null +++ b/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.Request.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.Session.xml b/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.Session.xml new file mode 100644 index 0000000000..648cbc0790 --- /dev/null +++ b/src/gui/osutils/nixutils/dbus/org.freedesktop.portal.Session.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main.cpp b/src/main.cpp index c7e82c0527..1e851f0778 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -63,6 +63,7 @@ int main(int argc, char** argv) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + QGuiApplication::setDesktopFileName("org.keepassxc.KeePassXC"); #if defined(Q_OS_WIN) QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); #endif @@ -191,8 +192,6 @@ int main(int argc, char** argv) // Apply the configured theme before creating any GUI elements app.applyTheme(); - QGuiApplication::setDesktopFileName(app.property("KPXC_QUALIFIED_APPNAME").toString() + QStringLiteral(".desktop")); - Application::bootstrap(config()->get(Config::GUI_Language).toString()); MainWindow mainWindow; diff --git a/vcpkg.json b/vcpkg.json index d0a4880a00..468558cf1b 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -19,6 +19,10 @@ { "name": "libxtst", "platform": "linux | freebsd" + }, + { + "name": "libxkbcommon", + "platform": "linux | freebsd" } ] }