Files
kdeconnect-kde-mirror/urlhandler/kdeconnect-handler.cpp
Andrii Dokhniak b81e537816 kdeconnect-handler: support multiple URLs / files as arguments
The only new visual change is when you select multiple things from the commandline
2026-03-01 20:49:47 +01:00

201 lines
8.1 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SPDX-FileCopyrightText: 2017 Aleix Pol Gonzalez <aleixpol@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include <QAbstractButton>
#include <QAbstractListModel>
#include <QApplication>
#include <QComboBox>
#include <QCommandLineParser>
#include <QDBusMessage>
#include <QDialog>
#include <QFileDialog>
#include <QIcon>
#include <QListWidget>
#include <QMessageBox>
#include <QQuickStyle>
#include <QTextStream>
#include <QUrl>
#include <QtContainerFwd>
#include <KAboutData>
#include <KColorSchemeManager>
#include <KCrash>
#include <KDBusService>
#include <KLocalizedString>
#include <KUrlRequester>
#include <dbushelper.h>
#include "dbusinterfaces/dbushelpers.h"
#include "kdeconnect-version.h"
#include "models/devicesmodel.h"
#include "models/devicespluginfilterproxymodel.h"
#include "ui_dialog.h"
enum class HandlerMode {
UserInput,
SingleArg,
MultiArg,
};
/**
* Show only devices that can be shared to
*/
int main(int argc, char **argv)
{
QApplication app(argc, argv);
app.setWindowIcon(QIcon::fromTheme(QStringLiteral("kdeconnect")));
const QString description = i18n("KDE Connect URL handler");
KAboutData aboutData(QStringLiteral("kdeconnect.handler"),
description,
QStringLiteral(KDECONNECT_VERSION_STRING),
description,
KAboutLicense::GPL,
i18n("© 2015–2025 KDE Connect Team"));
aboutData.addAuthor(i18n("Aleix Pol Gonzalez"), {}, QStringLiteral("aleixpol@kde.org"));
aboutData.addAuthor(i18n("Albert Vaca Cintora"), {}, QStringLiteral("albertvaka@gmail.org"));
aboutData.setProgramLogo(QIcon::fromTheme(QStringLiteral("kdeconnect")));
KAboutData::setApplicationData(aboutData);
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
// Ensure we have a suitable color theme set for light/dark mode. KColorSchemeManager implicitly applies
// a suitable default theme.
KColorSchemeManager::instance();
// Force breeze style to ensure coloring works consistently in dark mode. Specifically tab colors have
// troubles on windows.
QApplication::setStyle(QStringLiteral("breeze"));
// Force breeze icon theme to ensure we can correctly adapt icons to color changes WRT dark/light mode.
// Without this we may end up with hicolor and fail to support icon recoloring.
QIcon::setThemeName(QStringLiteral("breeze"));
#else
QIcon::setFallbackThemeName(QStringLiteral("breeze"));
#endif
// Default to org.kde.desktop style unless the user forces another style
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
}
KCrash::initialize();
KDBusService dbusService(KDBusService::Unique);
enum HandlerMode mode = HandlerMode::UserInput;
QList<QUrl> toSend;
bool open;
QString deviceId;
{
QCommandLineParser parser;
parser.addPositionalArgument(QStringLiteral("url"), i18n("URL to share"));
parser.addOption(QCommandLineOption(QStringLiteral("device"), i18n("Select a device"), i18n("id")));
parser.addOption(QCommandLineOption(QStringLiteral("open"), i18n("Automatically open the file on the remote device, when supported")));
aboutData.setupCommandLine(&parser);
parser.process(app);
aboutData.processCommandLine(&parser);
open = parser.isSet(QStringLiteral("open"));
deviceId = parser.value(QStringLiteral("device"));
for (auto arg : parser.positionalArguments()) {
toSend += QUrl::fromUserInput(arg, QDir::currentPath(), QUrl::AssumeLocalFile);
}
if (parser.positionalArguments().count() > 1) {
mode = HandlerMode::MultiArg;
} else if (parser.positionalArguments().count() == 1) {
mode = HandlerMode::SingleArg;
}
}
DevicesModel model;
model.setDisplayFilter(DevicesModel::Paired | DevicesModel::Reachable);
DevicesPluginFilterProxyModel proxyModel;
proxyModel.setPluginFilter(QStringLiteral("kdeconnect_share"));
proxyModel.setSourceModel(&model);
QDialog dialog;
Ui::Dialog uidialog;
uidialog.setupUi(&dialog);
uidialog.devicePicker->setModel(&proxyModel);
if (!deviceId.isEmpty()) {
// This is done on rowsInserted because the model isn't populated yet
QObject::connect(&model, &QAbstractItemModel::rowsInserted, &app, [&uidialog, &deviceId, &proxyModel](const QModelIndex &parent, int first) {
Q_UNUSED(parent);
/**
* We want to run this only once, but on startup the data is populated, removed then repopulated (rowsInserted() -> rowsRemoved() ->
* rowsInserted()). Thus by running only when there were no devices previously, it ensures that the user's selection doesn't change without them
* realising.
*/
if (first == 0) {
uidialog.devicePicker->setCurrentIndex(proxyModel.rowForDevice(deviceId));
}
});
}
KUrlRequester *urlRequester;
if (mode == HandlerMode::UserInput) {
urlRequester = new KUrlRequester(&dialog);
urlRequester->setStartDir(QUrl::fromLocalFile(QDir::homePath()));
urlRequester->setFocus();
uidialog.urlPickerLayout->addWidget(urlRequester);
urlRequester->setPlaceholderText(i18nc("Placeholder for input field that should contain a file/URL to share", "Local file or web URL"));
} else if (mode == HandlerMode::SingleArg) {
uidialog.urlPickerLabel->setVisible(false);
const QUrl &urlToShare = toSend.constFirst();
QString displayUrl;
if (urlToShare.scheme() == QLatin1String("tel")) {
displayUrl = urlToShare.toDisplayString(QUrl::RemoveScheme);
uidialog.devicePickerLabel->setText(i18n("Device to call %1 with:", displayUrl));
} else if (urlToShare.isLocalFile() && open) {
displayUrl = urlToShare.toDisplayString(QUrl::PreferLocalFile);
uidialog.devicePickerLabel->setText(i18n("Device to open %1 on:", displayUrl));
} else if (urlToShare.scheme() == QLatin1String("sms")) {
displayUrl = urlToShare.toDisplayString(QUrl::PreferLocalFile);
uidialog.devicePickerLabel->setText(i18n("Device to send a SMS with:"));
} else {
displayUrl = urlToShare.toDisplayString(QUrl::PreferLocalFile);
uidialog.devicePickerLabel->setText(i18n("Device to send %1 to:", displayUrl));
}
} else if (mode == HandlerMode::MultiArg) {
QListWidget *listView = new QListWidget;
listView->addItems(QUrl::toStringList(toSend, QUrl::PreferLocalFile));
uidialog.urlPickerLayout->addWidget(listView);
uidialog.urlPickerLabel->setText(i18nc("Label, for a list of items like files or urls", "Shared content:"));
}
if (dialog.exec() != QDialog::Accepted || toSend.length() == 0) {
return 1;
}
if (mode == HandlerMode::UserInput) {
toSend.append(urlRequester->url());
}
QString failed;
for (const auto &url : toSend) {
const int currentDeviceIndex = uidialog.devicePicker->currentIndex();
if (!url.isEmpty() && currentDeviceIndex >= 0) {
const QString device = proxyModel.index(currentDeviceIndex, 0).data(DevicesModel::IdModelRole).toString();
const QString action = open && url.isLocalFile() ? QStringLiteral("openFile") : QStringLiteral("shareUrl");
QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"),
QLatin1String("/modules/kdeconnect/devices/%1/share").arg(device),
QStringLiteral("org.kde.kdeconnect.device.share"),
action);
msg.setArguments({url.toString()});
blockOnReply(QDBusConnection::sessionBus().asyncCall(msg));
} else {
QMessageBox::critical(nullptr, description, i18n("Couldn't share %1", url.toString()));
return 1;
}
}
}