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"
}
]
}