[network/kdeconnect-kde] fileitemactionplugin: fileitemactionplugin: Make it fully async
Kai Uwe Broulik <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit abd78ef70d0101dfbfb52581adf41ccf701baed0 by Kai Uwe Broulik.
Committed on 26/07/2026 at 15:17.
Pushed by broulik into branch 'master'.
fileitemactionplugin: Make it fully async
Don't block the file manager waiting for KDE Connect.
For clearer lifetime the handling is done in a separate job class
because the async process might outliff the actual file item plugin.
While at it, make use of "sendUrls".
M +1 -1 fileitemactionplugin/CMakeLists.txt
A +171 -0 fileitemactionplugin/deviceactionjob.cpp [License: GPL(v2.0+)]
A +55 -0 fileitemactionplugin/deviceactionjob.h [License: GPL(v2.0+)]
M +22 -50 fileitemactionplugin/sendfileitemaction.cpp
https://invent.kde.org/network/kdeconnect-kde/-/commit/abd78ef70d0101dfbfb52581adf41ccf701baed0
diff --git a/fileitemactionplugin/CMakeLists.txt b/fileitemactionplugin/CMakeLists.txt
index b0e888976..a5bfc841e 100644
--- a/fileitemactionplugin/CMakeLists.txt
+++ b/fileitemactionplugin/CMakeLists.txt
@@ -1,6 +1,6 @@
add_definitions(-DTRANSLATION_DOMAIN="kdeconnect-fileitemaction")
-add_library(kdeconnectfileitemaction MODULE sendfileitemaction.cpp)
+add_library(kdeconnectfileitemaction MODULE sendfileitemaction.cpp deviceactionjob.cpp)
ecm_qt_declare_logging_category(kdeconnectfileitemaction
HEADER kdeconnect_fileitemaction_debug.h
diff --git a/fileitemactionplugin/deviceactionjob.cpp b/fileitemactionplugin/deviceactionjob.cpp
new file mode 100644
index 000000000..8c5ec3993
--- /dev/null
+++ b/fileitemactionplugin/deviceactionjob.cpp
@@ -0,0 +1,171 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Kai Uwe Broulik <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "deviceactionjob.h"
+
+#include <QAction>
+#include <QDBusPendingCallWatcher>
+#include <QIcon>
+#include <QUrl>
+#include <QVariantList>
+#include <QWidget>
+
+#include "dbusinterfaces/dbusinterfaces.h"
+#include "models/devicesmodel.h"
+
+#include <dbushelper.h>
+
+#include "kdeconnect_fileitemaction_debug.h"
+
+DeviceActionJob::DeviceActionJob(const QList<QUrl> &urls, QObject *parent)
+ : QObject(parent)
+ , m_urls(urls)
+{
+ connect(this, &DeviceActionJob::finished, this, &QObject::deleteLater);
+}
+
+QList<QAction *> DeviceActionJob::actions() const
+{
+ return m_actions;
+}
+
+void DeviceActionJob::start(QObject *actionParent)
+{
+ m_actionParent = actionParent;
+ // Must only ever call start() once.
+ Q_ASSERT(m_actions.isEmpty());
+
+ DaemonDbusInterface iface;
+ auto reply = iface.devices(true, true);
+ auto *watcher = new QDBusPendingCallWatcher(reply, this);
+ connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher] {
+ watcher->deleteLater();
+ QDBusPendingReply<QStringList> reply = *watcher;
+ if (reply.isError()) {
+ qCWarning(KDECONNECT_FILEITEMACTION) << "Failed to get list of devices:" << reply.error().message();
+ Q_EMIT finished({});
+ return;
+ }
+
+ const QStringList deviceIds = reply.value();
+ for (const QString &deviceId : deviceIds) {
+ DeviceDbusInterface deviceIface(deviceId);
+
+ auto pluginReply = deviceIface.hasPlugin(QStringLiteral("kdeconnect_share"));
+ ++m_pendingCalls;
+ auto *pluginWatcher = new QDBusPendingCallWatcher(pluginReply, this);
+ connect(pluginWatcher, &QDBusPendingCallWatcher::finished, this, [this, pluginWatcher, deviceId] {
+ onPluginReplyFinished(pluginWatcher, deviceId);
+ });
+ }
+ });
+}
+
+void DeviceActionJob::onPluginReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId)
+{
+ QDBusPendingReply<bool> reply = *watcher;
+
+ if (reply.isError()) {
+ qCWarning(KDECONNECT_FILEITEMACTION) << "Failed to get check whether device" << deviceId << "supports plugin:" << reply.error().message();
+ } else {
+ if (reply.value()) {
+ m_supportedDeviceIds.append(deviceId);
+ }
+ }
+
+ if (--m_pendingCalls == 0) {
+ fetchDeviceInfo();
+ }
+ watcher->deleteLater();
+}
+
+void DeviceActionJob::fetchDeviceInfo()
+{
+ if (m_supportedDeviceIds.isEmpty()) {
+ Q_EMIT finished({});
+ return;
+ }
+
+ const QString propertiesIface = QStringLiteral("org.freedesktop.DBus.Properties");
+ const QString getMethod = QStringLiteral("Get");
+
+ for (const QString &deviceId : std::as_const(m_supportedDeviceIds)) {
+ DeviceDbusInterface deviceIface(deviceId);
+
+ {
+ // Unfortunately no easy built-in async property read on QDBusAbstractInterface.
+ auto nameMessage = QDBusMessage::createMethodCall(deviceIface.service(), deviceIface.path(), propertiesIface, getMethod);
+ nameMessage << deviceIface.interface() << QStringLiteral("name");
+
+ auto nameReply = deviceIface.connection().asyncCall(nameMessage);
+ ++m_pendingCalls;
+ auto *nameWatcher = new QDBusPendingCallWatcher(nameReply, this);
+ connect(nameWatcher, &QDBusPendingCallWatcher::finished, this, [this, nameWatcher, deviceId] {
+ onDeviceNameReplyFinished(nameWatcher, deviceId);
+ });
+ }
+
+ {
+ // Unfortunately no easy built-in async property read on QDBusAbstractInterface.
+ auto iconNameMessage = QDBusMessage::createMethodCall(deviceIface.service(), deviceIface.path(), propertiesIface, getMethod);
+ iconNameMessage << deviceIface.interface() << QStringLiteral("iconName");
+
+ auto iconNameReply = deviceIface.connection().asyncCall(iconNameMessage);
+ ++m_pendingCalls;
+ auto *iconNameWatcher = new QDBusPendingCallWatcher(iconNameReply, this);
+ connect(iconNameWatcher, &QDBusPendingCallWatcher::finished, this, [this, iconNameWatcher, deviceId] {
+ onDeviceIconNameReplyFinished(iconNameWatcher, deviceId);
+ });
+ }
+ }
+}
+
+void DeviceActionJob::onDeviceNameReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId)
+{
+ QDBusPendingReply<QDBusVariant> reply = *watcher;
+ if (reply.isError()) {
+ qCWarning(KDECONNECT_FILEITEMACTION).nospace() << "Failed to get name of" << deviceId << ": " << reply.error().message();
+ } else {
+ m_devices[deviceId].name = reply.value().variant().toString();
+ }
+
+ if (--m_pendingCalls == 0) {
+ finalizeActions();
+ }
+ watcher->deleteLater();
+}
+
+void DeviceActionJob::onDeviceIconNameReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId)
+{
+ QDBusPendingReply<QDBusVariant> reply = *watcher;
+ if (reply.isError()) {
+ qCWarning(KDECONNECT_FILEITEMACTION).nospace() << "Failed to get icon name of" << deviceId << ": " << reply.error().message();
+ } else {
+ m_devices[deviceId].iconName = reply.value().variant().toString();
+ }
+
+ if (--m_pendingCalls == 0) {
+ finalizeActions();
+ }
+ watcher->deleteLater();
+}
+
+void DeviceActionJob::finalizeActions()
+{
+ for (const auto &[deviceId, device] : std::as_const(m_devices).asKeyValueRange()) {
+ QAction *action = new QAction(QIcon::fromTheme(device.iconName), device.name, m_actionParent);
+ action->setProperty("id", deviceId);
+ connect(action, &QAction::triggered, action, [deviceId, urls = m_urls] {
+ ShareDbusInterface shareIface(deviceId);
+ shareIface.shareUrls(QUrl::toStringList(urls));
+ });
+
+ m_actions.append(action);
+ }
+ Q_EMIT finished(m_actions);
+}
+
+#include "moc_deviceactionjob.cpp"
diff --git a/fileitemactionplugin/deviceactionjob.h b/fileitemactionplugin/deviceactionjob.h
new file mode 100644
index 000000000..48135d296
--- /dev/null
+++ b/fileitemactionplugin/deviceactionjob.h
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Kai Uwe Broulik <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef DEVICEACTIONJOB_H
+#define DEVICEACTIONJOB_H
+
+#include <QList>
+#include <QMap>
+#include <QObject>
+#include <QString>
+#include <QUrl>
+
+class QAction;
+class QDBusPendingCallWatcher;
+
+struct DeviceInfo {
+ QString name;
+ QString iconName;
+};
+
+class DeviceActionJob : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit DeviceActionJob(const QList<QUrl> &urls, QObject *parent);
+
+ [[nodiscard]] QList<QAction *> actions() const;
+
+ void start(QObject *actionParent);
+
+Q_SIGNALS:
+ void finished(const QList<QAction *> &actions);
+
+private:
+ void onPluginReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId);
+ void onDeviceNameReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId);
+ void onDeviceIconNameReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId);
+
+ void fetchDeviceInfo();
+ void finalizeActions();
+
+ QList<QUrl> m_urls;
+ QList<QAction *> m_actions;
+ QObject *m_actionParent = nullptr;
+
+ int m_pendingCalls = 0;
+ QStringList m_supportedDeviceIds;
+ QMap<QString, DeviceInfo> m_devices;
+};
+
+#endif // DEVICEACTIONJOB_H
diff --git a/fileitemactionplugin/sendfileitemaction.cpp b/fileitemactionplugin/sendfileitemaction.cpp
index 9e6ec60c2..47d01118b 100644
--- a/fileitemactionplugin/sendfileitemaction.cpp
+++ b/fileitemactionplugin/sendfileitemaction.cpp
@@ -1,6 +1,7 @@
/*
* SPDX-FileCopyrightText: 2011 Alejandro Fiestas Olivares <[email protected]>
* SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <[email protected]>
+ * SPDX-FileCopyrightText: 2026 Kai Uwe Broulik <[email protected]>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
@@ -18,12 +19,7 @@
#include <KLocalizedString>
#include <KPluginFactory>
-#include "dbusinterfaces/dbusinterfaces.h"
-#include "models/devicesmodel.h"
-
-#include <dbushelper.h>
-
-#include "kdeconnect_fileitemaction_debug.h"
+#include "deviceactionjob.h"
K_PLUGIN_CLASS_WITH_JSON(SendFileItemAction, "kdeconnectsendfile.json")
@@ -34,52 +30,28 @@ SendFileItemAction::SendFileItemAction(QObject *parent, const QVariantList &)
QList<QAction *> SendFileItemAction::actions(const KFileItemListProperties &fileItemInfos, QWidget *parentWidget)
{
- QList<QAction *> actions;
-
- DaemonDbusInterface iface;
- if (!iface.isValid()) {
- return actions;
- }
-
- QDBusPendingReply<QStringList> reply = iface.devices(true, true);
- reply.waitForFinished();
- const QStringList devices = reply.value();
- for (const QString &id : devices) {
- DeviceDbusInterface deviceIface(id);
- if (!deviceIface.isValid()) {
- continue;
- }
- if (!deviceIface.hasPlugin(QStringLiteral("kdeconnect_share"))) {
- continue;
+ // We have to return an action right away.
+ // Return a placeholder that we then asynchronously populate.
+ auto *action = new QAction(QIcon::fromTheme(QStringLiteral("kdeconnect")), i18n("Send via KDE Connect"), parentWidget);
+ action->setVisible(false);
+
+ auto *job = new DeviceActionJob(fileItemInfos.urlList(), parentWidget);
+ connect(job, &DeviceActionJob::finished, parentWidget, [action, parentWidget](const QList<QAction *> &actions) {
+ if (actions.count() > 1) {
+ QMenu *menu = new QMenu(parentWidget);
+ menu->addActions(actions);
+ action->setMenu(menu);
+ action->setVisible(true);
+ } else if (actions.count() == 1) {
+ auto *firstAction = actions.first();
+ action->setText(i18n("Send to '%1' via KDE Connect", firstAction->text()));
+ connect(action, &QAction::triggered, firstAction, &QAction::trigger);
+ action->setVisible(true);
}
- QAction *action = new QAction(QIcon::fromTheme(deviceIface.iconName()), deviceIface.name(), parentWidget);
- action->setProperty("id", id);
- const QList<QUrl> urls = fileItemInfos.urlList();
- connect(action, &QAction::triggered, this, [id, urls]() {
- for (const QUrl &url : urls) {
- QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"),
- QLatin1String("/modules/kdeconnect/devices/%1/share").arg(id),
- QStringLiteral("org.kde.kdeconnect.device.share"),
- QStringLiteral("shareUrl"));
- msg.setArguments(QVariantList{url.toString()});
- QDBusConnection::sessionBus().asyncCall(msg);
- }
- });
- actions += action;
- }
+ });
+ job->start(parentWidget);
- if (actions.count() > 1) {
- QAction *menuAction = new QAction(QIcon::fromTheme(QStringLiteral("kdeconnect")), i18n("Send via KDE Connect"), parentWidget);
- QMenu *menu = new QMenu(parentWidget);
- menu->addActions(actions);
- menuAction->setMenu(menu);
- return QList<QAction *>() << menuAction;
- } else {
- if (actions.count() == 1) {
- actions.first()->setText(i18n("Send to '%1' via KDE Connect", actions.first()->text()));
- }
- return actions;
- }
+ return {action};
}
#include "moc_sendfileitemaction.cpp"