From 400c800deb6054f4dc445e0c902b71c7e03f434b Mon Sep 17 00:00:00 2001 From: Nicolas Fella Date: Sun, 27 Oct 2019 18:08:51 +0100 Subject: [PATCH] [app] Add plugin settings page Include a page that allows (de)selecting and configuring plugins This is one of the last missing pieces for feature parity with the KCM. --- KDEConnectMacros.cmake | 3 + app/qml/DevicePage.qml | 12 +- app/qml/PluginInfoPage.qml | 37 +++++ app/qml/PluginSettings.qml | 64 ++++++++ app/resources.qrc | 2 + core/kdeconnectpluginconfig.cpp | 90 ++++++++++- core/kdeconnectpluginconfig.h | 28 ++-- declarativeplugin/CMakeLists.txt | 1 + .../kdeconnectdeclarativeplugin.cpp | 6 + interfaces/CMakeLists.txt | 4 + interfaces/commandsmodel.cpp | 143 ++++++++++++++++++ interfaces/commandsmodel.h | 63 ++++++++ interfaces/pluginmodel.cpp | 122 +++++++++++++++ interfaces/pluginmodel.h | 58 +++++++ .../findthisdevice/findthisdevice_config.cpp | 2 +- .../findthisdevice/findthisdeviceplugin.cpp | 4 +- .../kdeconnect_findthisdevice_config.qml | 52 +++++++ .../kdeconnect_pausemusic_config.qml | 46 ++++++ plugins/pausemusic/pausemusic_config.cpp | 8 +- plugins/pausemusic/pausemusicplugin.cpp | 8 +- .../kdeconnect_runcommand_config.qml | 105 +++++++++++++ plugins/runcommand/runcommand_config.cpp | 2 +- plugins/runcommand/runcommandplugin.cpp | 4 +- .../kdeconnect_sendnotifications_config.qml | 50 ++++++ .../notificationslistener.cpp | 8 +- .../sendnotifications_config.cpp | 8 +- plugins/share/kdeconnect_share_config.qml | 53 +++++++ plugins/share/share_config.cpp | 2 +- plugins/share/shareplugin.cpp | 2 +- smsapp/qml/AttachmentViewer.qml | 18 +-- smsapp/qml/SendingArea.qml | 18 +-- 31 files changed, 956 insertions(+), 67 deletions(-) create mode 100644 app/qml/PluginInfoPage.qml create mode 100644 app/qml/PluginSettings.qml create mode 100644 interfaces/commandsmodel.cpp create mode 100644 interfaces/commandsmodel.h create mode 100644 interfaces/pluginmodel.cpp create mode 100644 interfaces/pluginmodel.h create mode 100644 plugins/findthisdevice/kdeconnect_findthisdevice_config.qml create mode 100644 plugins/pausemusic/kdeconnect_pausemusic_config.qml create mode 100644 plugins/runcommand/kdeconnect_runcommand_config.qml create mode 100644 plugins/sendnotifications/kdeconnect_sendnotifications_config.qml create mode 100644 plugins/share/kdeconnect_share_config.qml diff --git a/KDEConnectMacros.cmake b/KDEConnectMacros.cmake index 20e36cfae..b4e2ef558 100644 --- a/KDEConnectMacros.cmake +++ b/KDEConnectMacros.cmake @@ -21,5 +21,8 @@ if (SAILFISHOS) else() function(kdeconnect_add_plugin) kcoreaddons_add_plugin(${ARGN} INSTALL_NAMESPACE kdeconnect) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${ARGV0}_config.qml") + install(FILES "${ARGV0}_config.qml" DESTINATION ${DATA_INSTALL_DIR}/kdeconnect) + endif() endfunction() endif() diff --git a/app/qml/DevicePage.qml b/app/qml/DevicePage.qml index 2505397a3..259d957fc 100644 --- a/app/qml/DevicePage.qml +++ b/app/qml/DevicePage.qml @@ -31,6 +31,17 @@ Kirigami.ScrollablePage onTriggered: { root.currentDevice.pluginCall("ping", "sendPing"); } + }, + Kirigami.Action { + iconName: "settings-configure" + text: i18n("Plugin Settings") + visible: root.currentDevice.isTrusted && root.currentDevice.isReachable + onTriggered: { + pageStack.push( + Qt.resolvedUrl("PluginSettings.qml"), + {device: currentDevice.id()} + ); + } } ] @@ -119,7 +130,6 @@ Kirigami.ScrollablePage Column { visible: root.currentDevice.hasPairingRequests anchors.centerIn: parent - spacing: Kirigami.Units.largeSpacing Kirigami.PlaceholderMessage { diff --git a/app/qml/PluginInfoPage.qml b/app/qml/PluginInfoPage.qml new file mode 100644 index 000000000..87a1f61b0 --- /dev/null +++ b/app/qml/PluginInfoPage.qml @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2019 Nicolas Fella + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.2 +import QtQuick.Controls 2.2 +import org.kde.kirigami 2.0 as Kirigami + +Kirigami.Page +{ + id: root + property string configFile + property string device + + actions.main: loader.item.action + + onConfigFileChanged: { + loader.setSource(configFile, { + device: root.device + }) + } + + Loader { + anchors.fill: parent + id: loader + Component.onCompleted: { + setSource(configFile, { + device: root.device + }) + } + } +} + + + diff --git a/app/qml/PluginSettings.qml b/app/qml/PluginSettings.qml new file mode 100644 index 000000000..04be78b5b --- /dev/null +++ b/app/qml/PluginSettings.qml @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2019 Nicolas Fella + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.2 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.0 as Kirigami +import org.kde.kdeconnect 1.0 + +Kirigami.ScrollablePage +{ + id: root + title: i18n("Plugin Settings") + property string device + + ListView { + id: sinkList + + anchors.fill: parent + + model: PluginModel { + deviceId: device + } + + delegate: Kirigami.SwipeListItem { + width: parent.width + enabled: true + supportsMouseEvents: false + + CheckBox { + checked: isChecked + text: name + onClicked: { + isChecked = checked + } + } + + actions: [ + Kirigami.Action { + icon.name: "settings-configure" + visible: configSource != "" + onTriggered: { + if (pageStack.lastItem.toString().startsWith("PluginInfoPage")) { + pageStack.lastItem.configFile = configSource + pageStack.lastItem.title = name + } else { + pageStack.push(Qt.resolvedUrl("PluginInfoPage.qml"), { + title: name, + configFile: configSource, + device: root.device + }) + } + } + } + ] + } + } + +} + + diff --git a/app/resources.qrc b/app/resources.qrc index e726a941d..9bedb9e07 100644 --- a/app/resources.qrc +++ b/app/resources.qrc @@ -11,5 +11,7 @@ qml/runcommand.qml qml/volume.qml qml/MprisSlider.qml + qml/PluginSettings.qml + qml/PluginInfoPage.qml diff --git a/core/kdeconnectpluginconfig.cpp b/core/kdeconnectpluginconfig.cpp index b134c478b..fe48733da 100644 --- a/core/kdeconnectpluginconfig.cpp +++ b/core/kdeconnectpluginconfig.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include "kdeconnectconfig.h" #include "dbushelper.h" @@ -20,6 +22,12 @@ struct KdeConnectPluginConfigPrivate QDBusMessage m_signal; }; +KdeConnectPluginConfig::KdeConnectPluginConfig() + : d(new KdeConnectPluginConfigPrivate()) +{ + +} + KdeConnectPluginConfig::KdeConnectPluginConfig(const QString& deviceId, const QString& pluginName) : d(new KdeConnectPluginConfigPrivate()) { @@ -37,10 +45,44 @@ KdeConnectPluginConfig::~KdeConnectPluginConfig() delete d->m_config; } -QVariant KdeConnectPluginConfig::get(const QString& key, const QVariant& defaultValue) +QString KdeConnectPluginConfig::getString(const QString& key, const QString& defaultValue) { + if (!d->m_config) { + loadConfig(); + } + d->m_config->sync(); - return d->m_config->value(key, defaultValue); + return d->m_config->value(key, defaultValue).toString(); +} + +bool KdeConnectPluginConfig::getBool(const QString& key, const bool defaultValue) +{ + if (!d->m_config) { + loadConfig(); + } + + d->m_config->sync(); + return d->m_config->value(key, defaultValue).toBool(); +} + +int KdeConnectPluginConfig::getInt(const QString& key, const int defaultValue) +{ + if (!d->m_config) { + loadConfig(); + } + + d->m_config->sync(); + return d->m_config->value(key, defaultValue).toInt(); +} + +QByteArray KdeConnectPluginConfig::getByteArray(const QString& key, const QByteArray defaultValue) +{ + if (!d->m_config) { + loadConfig(); + } + + d->m_config->sync(); + return d->m_config->value(key, defaultValue).toByteArray(); } QVariantList KdeConnectPluginConfig::getList(const QString& key, @@ -84,3 +126,47 @@ void KdeConnectPluginConfig::slotConfigChanged() { Q_EMIT configChanged(); } + +void KdeConnectPluginConfig::setDeviceId(const QString& deviceId) +{ + if (deviceId != m_deviceId) { + m_deviceId = deviceId; + } + + if (!m_deviceId.isEmpty() && !m_pluginName.isEmpty()) { + loadConfig(); + } +} + +QString KdeConnectPluginConfig::deviceId() +{ + return m_deviceId; +} + +void KdeConnectPluginConfig::setPluginName(const QString& pluginName) +{ + if (pluginName != m_pluginName) { + m_pluginName = pluginName; + } + + if (!m_deviceId.isEmpty() && !m_pluginName.isEmpty()) { + loadConfig(); + } +} + +QString KdeConnectPluginConfig::pluginName() +{ + return m_pluginName; +} + +void KdeConnectPluginConfig::loadConfig() +{ + d->m_configDir = KdeConnectConfig::instance().pluginConfigDir(m_deviceId, m_pluginName); + QDir().mkpath(d->m_configDir.path()); + + d->m_config = new QSettings(d->m_configDir.absoluteFilePath(QStringLiteral("config")), QSettings::IniFormat); + + d->m_signal = QDBusMessage::createSignal(QStringLiteral("/kdeconnect/") + m_deviceId + QStringLiteral("/") + m_pluginName, QStringLiteral("org.kde.kdeconnect.config"), QStringLiteral("configChanged")); + QDBusConnection::sessionBus().connect(QLatin1String(""), QStringLiteral("/kdeconnect/") + m_deviceId + QStringLiteral("/") + m_pluginName, QStringLiteral("org.kde.kdeconnect.config"), QStringLiteral("configChanged"), this, SLOT(slotConfigChanged())); + Q_EMIT configChanged(); +} diff --git a/core/kdeconnectpluginconfig.h b/core/kdeconnectpluginconfig.h index 8284f1985..d1271a6d5 100644 --- a/core/kdeconnectpluginconfig.h +++ b/core/kdeconnectpluginconfig.h @@ -21,14 +21,18 @@ class KDECONNECTCORE_EXPORT KdeConnectPluginConfig : public QObject { Q_OBJECT + Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY configChanged) + Q_PROPERTY(QString pluginName READ pluginName WRITE setPluginName NOTIFY configChanged) + public: + KdeConnectPluginConfig(); KdeConnectPluginConfig(const QString& deviceId, const QString& pluginName); ~KdeConnectPluginConfig() override; /** * Store a key-value pair in this config object */ - void set(const QString& key, const QVariant& value); + Q_SCRIPTABLE void set(const QString& key, const QVariant& value); /** * Store a list of values in this config object under the array name @@ -39,17 +43,19 @@ public: /** * Read a key-value pair from this config object */ - QVariant get(const QString& key, const QVariant& defaultValue); - - /** - * Convenience method that will convert the QVariant to whatever type for you - */ - template T get(const QString& key, const T& defaultValue = {}) { - return get(key, QVariant(defaultValue)).template value(); //Important note: Awesome template syntax is awesome - } + Q_SCRIPTABLE QString getString(const QString& key, const QString& defaultValue); + Q_SCRIPTABLE bool getBool(const QString& key, const bool defaultValue); + Q_SCRIPTABLE int getInt(const QString& key, const int defaultValue); + Q_SCRIPTABLE QByteArray getByteArray(const QString& key, const QByteArray defaultValue); QVariantList getList(const QString& key, const QVariantList& defaultValue = {}); + QString deviceId(); + void setDeviceId(const QString& deviceId); + + QString pluginName(); + void setPluginName(const QString& pluginName); + private Q_SLOTS: void slotConfigChanged(); @@ -57,7 +63,11 @@ Q_SIGNALS: void configChanged(); private: + void loadConfig(); + QScopedPointer d; + QString m_deviceId; + QString m_pluginName; }; #endif diff --git a/declarativeplugin/CMakeLists.txt b/declarativeplugin/CMakeLists.txt index 9a6eb0eaf..ce6edada0 100644 --- a/declarativeplugin/CMakeLists.txt +++ b/declarativeplugin/CMakeLists.txt @@ -15,6 +15,7 @@ add_library(kdeconnectdeclarativeplugin SHARED ${kdeconnectdeclarativeplugin_SRC target_link_libraries(kdeconnectdeclarativeplugin Qt5::Qml kdeconnectinterfaces + kdeconnectcore ) install(TARGETS kdeconnectdeclarativeplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/kdeconnect) diff --git a/declarativeplugin/kdeconnectdeclarativeplugin.cpp b/declarativeplugin/kdeconnectdeclarativeplugin.cpp index 12e44c92b..6ba072cd3 100644 --- a/declarativeplugin/kdeconnectdeclarativeplugin.cpp +++ b/declarativeplugin/kdeconnectdeclarativeplugin.cpp @@ -19,6 +19,9 @@ #include "interfaces/notificationsmodel.h" #include #include +#include +#include "core/kdeconnectpluginconfig.h" +#include "interfaces/commandsmodel.h" QObject* createDBusResponse() { @@ -43,6 +46,9 @@ void KdeConnectDeclarativePlugin::registerTypes(const char* uri) qmlRegisterType(uri, 1, 0, "DBusAsyncResponse"); qmlRegisterType(uri, 1, 0, "DevicesSortProxyModel"); qmlRegisterType(uri, 1, 0, "RemoteSinksModel"); + qmlRegisterType(uri, 1, 0, "PluginModel"); + qmlRegisterType(uri, 1, 0, "KdeConnectPluginConfig"); + qmlRegisterType(uri, 1, 0, "CommandsModel"); qmlRegisterUncreatableType(uri, 1, 0, "MprisDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); qmlRegisterUncreatableType(uri, 1, 0, "LockDeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); qmlRegisterUncreatableType(uri, 1, 0, "FindMyPhoneDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces")); diff --git a/interfaces/CMakeLists.txt b/interfaces/CMakeLists.txt index 8f47b4eb2..1a2aa48f2 100644 --- a/interfaces/CMakeLists.txt +++ b/interfaces/CMakeLists.txt @@ -32,6 +32,8 @@ set(libkdeconnect_SRC remotecommandsmodel.cpp remotesinksmodel.cpp devicespluginfilterproxymodel.cpp + pluginmodel.cpp + commandsmodel.cpp # modeltest.cpp ${debug_files_SRCS} ) @@ -67,9 +69,11 @@ LINK_PUBLIC Qt5::Gui Qt5::DBus LINK_PRIVATE + kdeconnectcore KF5::ConfigCore KF5::I18n kdeconnectcore + KF5::CoreAddons ) install(TARGETS kdeconnectinterfaces EXPORT kdeconnectLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) diff --git a/interfaces/commandsmodel.cpp b/interfaces/commandsmodel.cpp new file mode 100644 index 000000000..6c49f1ba9 --- /dev/null +++ b/interfaces/commandsmodel.cpp @@ -0,0 +1,143 @@ +/** + * SPDX-FileCopyrightText: 2019 Nicolas Fella + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "commandsmodel.h" + +#include +#include +#include +#include + +#include + +CommandsModel::CommandsModel(QObject* parent) + : QAbstractListModel(parent) + , m_config() +{ + + m_config.setPluginName(QStringLiteral("kdeconnect_runcommand")); + connect(this, &QAbstractItemModel::rowsInserted, + this, &CommandsModel::rowsChanged); + connect(this, &QAbstractItemModel::rowsRemoved, + this, &CommandsModel::rowsChanged); +} + +QHash CommandsModel::roleNames() const +{ + //Role names for QML + QHash names = QAbstractItemModel::roleNames(); + names.insert(KeyRole, "key"); + names.insert(NameRole, "name"); + names.insert(CommandRole, "command"); + return names; +} + +CommandsModel::~CommandsModel() +{ +} + +QString CommandsModel::deviceId() const +{ + return m_deviceId; +} + +void CommandsModel::setDeviceId(const QString& deviceId) +{ + m_deviceId = deviceId; + m_config.setDeviceId(deviceId); + + refreshCommandList(); + + Q_EMIT deviceIdChanged(deviceId); +} + +void CommandsModel::refreshCommandList() +{ + const auto cmds = QJsonDocument::fromJson(m_config.getByteArray(QStringLiteral("commands"), QByteArray())).object(); + + beginResetModel(); + m_commandList.clear(); + + for (auto it = cmds.constBegin(), itEnd = cmds.constEnd(); it!=itEnd; ++it) { + const QJsonObject cont = it->toObject(); + CommandEntry command; + command.key = it.key(); + command.name = cont.value(QStringLiteral("name")).toString(); + command.command = cont.value(QStringLiteral("command")).toString(); + m_commandList.append(command); + } + + endResetModel(); +} + +QVariant CommandsModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() + || index.row() < 0 + || index.row() >= m_commandList.count()) + { + return QVariant(); + } + + CommandEntry command = m_commandList[index.row()]; + + switch (role) { + case KeyRole: + return command.key; + case NameRole: + return command.name; + case CommandRole: + return command.command; + default: + return QVariant(); + } +} + +int CommandsModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) { + //Return size 0 if we are a child because this is not a tree + return 0; + } + + return m_commandList.count(); +} + +void CommandsModel::removeCommand(int index) +{ + beginRemoveRows(QModelIndex(), index, index); + m_commandList.remove(index); + endRemoveRows(); + saveCommands(); +} + +void CommandsModel::saveCommands() +{ + QJsonObject jsonConfig; + for (const CommandEntry &command : m_commandList) { + QJsonObject entry; + entry[QStringLiteral("name")] = command.name; + entry[QStringLiteral("command")] = command.command; + jsonConfig[command.key] = entry; + } + QJsonDocument document; + document.setObject(jsonConfig); + m_config.set(QStringLiteral("commands"), document.toJson(QJsonDocument::Compact)); +} + +void CommandsModel::addCommand(const QString& name, const QString& command) +{ + CommandEntry entry; + QString key = QUuid::createUuid().toString(); + DBusHelper::filterNonExportableCharacters(key); + entry.key = key; + entry.name = name; + entry.command = command; + beginInsertRows(QModelIndex(), m_commandList.size(), m_commandList.size()); + m_commandList.append(entry); + endInsertRows(); + saveCommands(); +} diff --git a/interfaces/commandsmodel.h b/interfaces/commandsmodel.h new file mode 100644 index 000000000..b4eac82fd --- /dev/null +++ b/interfaces/commandsmodel.h @@ -0,0 +1,63 @@ +/** + * SPDX-FileCopyrightText: 2019 Nicolas Fella + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#ifndef COMMANDSMODEL_H +#define COMMANDSMODEL_H + +#include +#include "kdeconnectinterfaces_export.h" +#include "core/kdeconnectpluginconfig.h" + +struct CommandEntry { + QString key; + QString name; + QString command; +}; + +class KDECONNECTINTERFACES_EXPORT CommandsModel + : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged) + +public: + enum ModelRoles { + KeyRole, + NameRole, + CommandRole + }; + + explicit CommandsModel(QObject* parent = nullptr); + ~CommandsModel() override; + + QString deviceId() const; + void setDeviceId(const QString& deviceId); + + QVariant data(const QModelIndex& index, int role) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + + QHash roleNames() const override; + + Q_SCRIPTABLE void removeCommand(int index); + Q_SCRIPTABLE void addCommand(const QString& name, const QString& command); + +private Q_SLOTS: + void refreshCommandList(); + +Q_SIGNALS: + void deviceIdChanged(const QString& value); + void rowsChanged(); + +private: + void saveCommands(); + + QVector m_commandList; + QString m_deviceId; + KdeConnectPluginConfig m_config; +}; + +#endif // DEVICESMODEL_H + diff --git a/interfaces/pluginmodel.cpp b/interfaces/pluginmodel.cpp new file mode 100644 index 000000000..fa6743c8a --- /dev/null +++ b/interfaces/pluginmodel.cpp @@ -0,0 +1,122 @@ +/** + * SPDX-FileCopyrightText: 2019 Nicolas Fella + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "pluginmodel.h" + +#include +#include + +PluginModel::PluginModel(QObject *parent) + : QAbstractListModel(parent) +{ + + connect(this, &QAbstractItemModel::rowsInserted, + this, &PluginModel::rowsChanged); + connect(this, &QAbstractItemModel::rowsRemoved, + this, &PluginModel::rowsChanged); + + + beginResetModel(); + m_plugins = KPluginInfo::fromMetaData(KPluginLoader::findPlugins(QStringLiteral("kdeconnect/"))); + endResetModel(); +} + +PluginModel::~PluginModel() +{ +} + +void PluginModel::setDeviceId(const QString& deviceId) +{ + if (deviceId == m_deviceId) + return; + + m_deviceId = deviceId; + DeviceDbusInterface *device = new DeviceDbusInterface(m_deviceId); + m_config = KSharedConfig::openConfig(device->pluginsConfigFile()); +} + +QVariant PluginModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + const KPluginInfo &pluginEntry = m_plugins[index.row()]; + + switch (role) { + case Qt::CheckStateRole: + { + QString def = pluginEntry.isPluginEnabledByDefault() ? QStringLiteral("true") : QStringLiteral("false"); + return m_config->group("Plugins").readEntry(QStringLiteral("%1Enabled").arg(pluginEntry.pluginName()), def) == QStringLiteral("true"); + } + case Qt::DisplayRole: + return pluginEntry.name(); + case IconRole: + return pluginEntry.icon(); + case IdRole: + return pluginEntry.pluginName(); + case ConfigSourceRole: + { + const QString configFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kdeconnect/%1_config.qml").arg(pluginEntry.pluginName())); + if (configFile.isEmpty()) + return QUrl(); + + return QUrl::fromLocalFile(configFile); + } + default: + return QVariant(); + } +} + + +QHash PluginModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + roles[Qt::DisplayRole] = "name"; + roles[Qt::CheckStateRole] = "isChecked"; + roles[IconRole] = "iconName"; + roles[IdRole] = "pluginId"; + roles[ConfigSourceRole] = "configSource"; + return roles; +} + + +bool PluginModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) { + return false; + } + + bool ret = false; + + if (role == Qt::CheckStateRole) { + const KPluginInfo &pluginEntry = m_plugins[index.row()]; + m_config->group("Plugins").writeEntry(QStringLiteral("%1Enabled").arg(pluginEntry.pluginName()), value); + ret = true; + } + + m_config->sync(); + + if (ret) { + Q_EMIT dataChanged(index, index); + } + + return ret; +} + +int PluginModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_plugins.size(); +} + +QString PluginModel::deviceId() +{ + return m_deviceId; +} diff --git a/interfaces/pluginmodel.h b/interfaces/pluginmodel.h new file mode 100644 index 000000000..c8af85140 --- /dev/null +++ b/interfaces/pluginmodel.h @@ -0,0 +1,58 @@ +/** + * SPDX-FileCopyrightText: 2019 Nicolas Fella + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#ifndef PLUGINMODEL +#define PLUGINMODEL + +#include + +#include + +#include "interfaces/dbusinterfaces.h" +#include + +class KDECONNECTINTERFACES_EXPORT PluginModel + : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId) + +public: + + enum ExtraRoles { + IconRole = Qt::UserRole + 1, + IdRole, + ConfigSourceRole + }; + + Q_ENUM(ExtraRoles) + + explicit PluginModel(QObject *parent = nullptr); + ~PluginModel() override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + void setDeviceId(const QString& deviceId); + QString deviceId(); + +Q_SIGNALS: + void deviceIdChanged(const QString& value); + void rowsChanged(); + + +private: + QList m_plugins; + QString m_deviceId; + KSharedConfigPtr m_config; + +}; + + +#endif // KPLUGINSELECTOR_P_H + + diff --git a/plugins/findthisdevice/findthisdevice_config.cpp b/plugins/findthisdevice/findthisdevice_config.cpp index 31641337e..7a24384b5 100644 --- a/plugins/findthisdevice/findthisdevice_config.cpp +++ b/plugins/findthisdevice/findthisdevice_config.cpp @@ -57,7 +57,7 @@ void FindThisDeviceConfig::load() { KCModule::load(); - const QString ringTone = config()->get(QStringLiteral("ringtone"), defaultSound()); + const QString ringTone = config()->getString(QStringLiteral("ringtone"), defaultSound()); m_ui->soundFileRequester->setText(ringTone); Q_EMIT changed(false); diff --git a/plugins/findthisdevice/findthisdeviceplugin.cpp b/plugins/findthisdevice/findthisdeviceplugin.cpp index 547bff192..524b317ed 100644 --- a/plugins/findthisdevice/findthisdeviceplugin.cpp +++ b/plugins/findthisdevice/findthisdeviceplugin.cpp @@ -33,8 +33,10 @@ FindThisDevicePlugin::~FindThisDevicePlugin() = default; bool FindThisDevicePlugin::receivePacket(const NetworkPacket& np) { Q_UNUSED(np); - const QString soundFile = config()->get(QStringLiteral("ringtone"), defaultSound()); + + const QString soundFile = config()->getString(QStringLiteral("ringtone"), defaultSound()); const QUrl soundURL = QUrl::fromLocalFile(soundFile); + if (soundURL.isEmpty()) { qCWarning(KDECONNECT_PLUGIN_FINDTHISDEVICE) << "Not playing sound, no valid ring tone specified."; return true; diff --git a/plugins/findthisdevice/kdeconnect_findthisdevice_config.qml b/plugins/findthisdevice/kdeconnect_findthisdevice_config.qml new file mode 100644 index 000000000..e2d1b82d9 --- /dev/null +++ b/plugins/findthisdevice/kdeconnect_findthisdevice_config.qml @@ -0,0 +1,52 @@ +import QtQuick 2.2 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.5 as Kirigami +import Qt.labs.platform 1.1 +import org.kde.kdeconnect 1.0 + +Kirigami.FormLayout { + + property string device + + property var action: Kirigami.Action { + icon.name: "dialog-ok" + text: i18n("Apply") + onTriggered: config.set("ringtone", path.text) + } + + FileDialog { + id: fileDialog + currentFile: path.text + + onAccepted: { + path.text = currentFile.toString().replace("file://", "") + } + } + + KdeConnectPluginConfig { + id: config + deviceId: device + pluginName: "kdeconnect_findthisdevice" + + onConfigChanged: { + path.text = get("ringtone", StandardPaths.writableLocation(StandardPaths.DownloadsLocation).toString().replace("file://", "")) + } + } + + RowLayout { + Kirigami.FormData.label: i18n("Sound to play:") + + TextField { + id: path + } + + Button { + icon.name: "document-open" + onClicked: { + fileDialog.open() + } + } + } +} + diff --git a/plugins/pausemusic/kdeconnect_pausemusic_config.qml b/plugins/pausemusic/kdeconnect_pausemusic_config.qml new file mode 100644 index 000000000..e27c2e9e4 --- /dev/null +++ b/plugins/pausemusic/kdeconnect_pausemusic_config.qml @@ -0,0 +1,46 @@ +import QtQuick 2.2 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.5 as Kirigami +import org.kde.kdeconnect 1.0 + +Kirigami.FormLayout { + + property string device + + KdeConnectPluginConfig { + id: config + deviceId: device + pluginName: "kdeconnect_pausemusic" + } + + Component.onCompleted: { + talking.checked = config.get("conditionTalking", false) + mute.checked = config.get("actionMute", false) + pause.checked = config.get("actionPause", true) + } + + RadioButton { + text: i18n("Pause as soon as phone rings") + checked: !talking.checked + } + + RadioButton { + id: talking + onCheckedChanged: config.set("conditionTalking", checked) + text: i18n("Pause only while talking") + } + + CheckBox { + id: pause + text: i18n("Pause media players") + onClicked: config.set("actionPause", checked) + } + + CheckBox { + id: mute + text: i18n("Mute system sound") + onClicked: config.set("actionMute", checked) + } + +} diff --git a/plugins/pausemusic/pausemusic_config.cpp b/plugins/pausemusic/pausemusic_config.cpp index 44534bb98..9a4fd45fd 100644 --- a/plugins/pausemusic/pausemusic_config.cpp +++ b/plugins/pausemusic/pausemusic_config.cpp @@ -43,16 +43,16 @@ void PauseMusicConfig::defaults() void PauseMusicConfig::load() { KCModule::load(); - bool talking = config()->get(QStringLiteral("conditionTalking"), false); + bool talking = config()->getBool(QStringLiteral("conditionTalking"), false); m_ui->rad_talking->setChecked(talking); m_ui->rad_ringing->setChecked(!talking); - bool pause = config()->get(QStringLiteral("actionPause"), true); - bool mute = config()->get(QStringLiteral("actionMute"), false); + bool pause = config()->getBool(QStringLiteral("actionPause"), true); + bool mute = config()->getBool(QStringLiteral("actionMute"), false); m_ui->check_pause->setChecked(pause); m_ui->check_mute->setChecked(mute); - const bool autoResume = config()->get(QStringLiteral("actionResume"), true); + const bool autoResume = config()->getBool(QStringLiteral("actionResume"), true); m_ui->check_resume->setChecked(autoResume); Q_EMIT changed(false); diff --git a/plugins/pausemusic/pausemusicplugin.cpp b/plugins/pausemusic/pausemusicplugin.cpp index 7929cb4d3..120bb6b09 100644 --- a/plugins/pausemusic/pausemusicplugin.cpp +++ b/plugins/pausemusic/pausemusicplugin.cpp @@ -26,7 +26,7 @@ PauseMusicPlugin::PauseMusicPlugin(QObject* parent, const QVariantList& args) bool PauseMusicPlugin::receivePacket(const NetworkPacket& np) { - bool pauseOnlyWhenTalking = config()->get(QStringLiteral("conditionTalking"), false); + bool pauseOnlyWhenTalking = config()->getBool(QStringLiteral("conditionTalking"), false); if (pauseOnlyWhenTalking) { if (np.get(QStringLiteral("event")) != QLatin1String("talking")) { @@ -40,10 +40,10 @@ bool PauseMusicPlugin::receivePacket(const NetworkPacket& np) bool pauseConditionFulfilled = !np.get(QStringLiteral("isCancel")); - bool pause = config()->get(QStringLiteral("actionPause"), true); - bool mute = config()->get(QStringLiteral("actionMute"), false); + bool pause = config()->getBool(QStringLiteral("actionPause"), true); + bool mute = config()->getBool(QStringLiteral("actionMute"), false); - const bool autoResume = config()->get(QStringLiteral("actionResume"), true); + const bool autoResume = config()->getBool(QStringLiteral("actionResume"), true); if (pauseConditionFulfilled) { diff --git a/plugins/runcommand/kdeconnect_runcommand_config.qml b/plugins/runcommand/kdeconnect_runcommand_config.qml new file mode 100644 index 000000000..ff536f76a --- /dev/null +++ b/plugins/runcommand/kdeconnect_runcommand_config.qml @@ -0,0 +1,105 @@ +import QtQuick 2.2 +import QtQuick.Controls 2.5 +import org.kde.kirigami 2.4 as Kirigami +import org.kde.kdeconnect 1.0 + +ListView { + + Component.onCompleted: { + root.leftPadding = 0 + root.rightPadding = 0 + root.topPadding = 0 + root.bottomPadding = 0 + } + + property string device + + property var action: Kirigami.Action { + icon.name: "list-add" + text: i18n("Add command") + onTriggered: addDialog.open() + } + + model: CommandsModel { + id: commandModel + deviceId: device + } + + delegate: Kirigami.SwipeListItem { + width: parent.width + enabled: true + + Label { + text: i18n("%1
%2", name, command) + } + + actions: [ + Kirigami.Action { + icon.name: "delete" + onTriggered: commandModel.removeCommand(index) + } + ] + } + + Dialog { + id: addDialog + title: "Add command" + + standardButtons: Dialog.Save | Dialog.Cancel + + Kirigami.FormLayout { + TextField { + id: nameField + Kirigami.FormData.label: i18n("Name:") + } + TextField { + id: commandField + Kirigami.FormData.label: i18n("Command:") + } + + ComboBox { + Kirigami.FormData.label: i18n("Sample commands:") + textRole: "name" + model: ListModel { + id: sampleCommands + ListElement { + name: "Sample command" + command: "" + } + ListElement { + name: "Suspend" + command: "systemctl suspend" + } + ListElement { + name: "Maximum Brightness" + command: "qdbus org.kde.Solid.PowerManagement /org/kde/Solid/PowerManagement/Actions/BrightnessControl org.kde.Solid.PowerManagement.Actions.BrightnessControl.setBrightness `qdbus org.kde.Solid.PowerManagement /org/kde/Solid/PowerManagement/Actions/BrightnessControl org.kde.Solid.PowerManagement.Actions.BrightnessControl.brightnessMax`" + } + ListElement { + name: "Lock Screen" + command: "loginctl lock-session" + } + ListElement { + name: "Unlock Screen" + command: "loginctl unlock-session" + } + ListElement { + name: "Close All Vaults" + command: "qdbus org.kde.kded5 /modules/plasmavault closeAllVaults" + } + ListElement { + name: "Forcefully Close All Vaults" + command: "qdbus org.kde.kded5 /modules/plasmavault forceCloseAllVaults" + } + } + onActivated: { + if (index > 0) { + nameField.text = sampleCommands.get(index).name + commandField.text = sampleCommands.get(index).command + } + } + } + } + + onAccepted: commandModel.addCommand(nameField.text, commandField.text) + } +} diff --git a/plugins/runcommand/runcommand_config.cpp b/plugins/runcommand/runcommand_config.cpp index 6ed8f95d9..f0dbc0daf 100644 --- a/plugins/runcommand/runcommand_config.cpp +++ b/plugins/runcommand/runcommand_config.cpp @@ -88,7 +88,7 @@ void RunCommandConfig::load() { KCModule::load(); - QJsonDocument jsonDocument = QJsonDocument::fromJson(config()->get(QStringLiteral("commands"), "{}")); + QJsonDocument jsonDocument = QJsonDocument::fromJson(config()->getByteArray(QStringLiteral("commands"), "{}")); QJsonObject jsonConfig = jsonDocument.object(); const QStringList keys = jsonConfig.keys(); for (const QString& key : keys) { diff --git a/plugins/runcommand/runcommandplugin.cpp b/plugins/runcommand/runcommandplugin.cpp index 6161d6afb..f73c79260 100644 --- a/plugins/runcommand/runcommandplugin.cpp +++ b/plugins/runcommand/runcommandplugin.cpp @@ -58,7 +58,7 @@ bool RunCommandPlugin::receivePacket(const NetworkPacket& np) } if (np.has(QStringLiteral("key"))) { - QJsonDocument commandsDocument = QJsonDocument::fromJson(config()->get(QStringLiteral("commands"), "{}")); + QJsonDocument commandsDocument = QJsonDocument::fromJson(config()->getByteArray(QStringLiteral("commands"), "{}")); QJsonObject commands = commandsDocument.object(); QString key = np.get(QStringLiteral("key")); QJsonValue value = commands[key]; @@ -84,7 +84,7 @@ void RunCommandPlugin::connected() void RunCommandPlugin::sendConfig() { - QString commands = config()->get(QStringLiteral("commands"),QStringLiteral("{}")); + QString commands = config()->getString(QStringLiteral("commands"),QStringLiteral("{}")); NetworkPacket np(PACKET_TYPE_RUNCOMMAND, {{QStringLiteral("commandList"), commands}}); #if KCMUTILS_VERSION >= QT_VERSION_CHECK(5, 45, 0) diff --git a/plugins/sendnotifications/kdeconnect_sendnotifications_config.qml b/plugins/sendnotifications/kdeconnect_sendnotifications_config.qml new file mode 100644 index 000000000..a52fbe932 --- /dev/null +++ b/plugins/sendnotifications/kdeconnect_sendnotifications_config.qml @@ -0,0 +1,50 @@ +import QtQuick 2.2 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.5 as Kirigami +import org.kde.kdeconnect 1.0 + +Kirigami.FormLayout { + + property string device + + KdeConnectPluginConfig { + id: config + deviceId: device + pluginName: "kdeconnect_sendnotifications" + } + + Component.onCompleted: { + persistent.checked = config.getBool("generalPersistent", false) + includeBody.checked = config.getBool("generalIncludeBody", true) + includeIcon.checked = config.getBool("generalSynchronizeIcons", true) + urgency.value = config.getInt("generalUrgency", 0) + } + + CheckBox { + id: persistent + text: i18n("Persistent notifications only") + onClicked: config.set("generalPersistent", checked) + } + + CheckBox { + id: includeBody + text: i18n("Include body") + onClicked: config.set("generalIncludeBody", checked) + } + + CheckBox { + id: includeIcon + text: i18n("Include icon") + onClicked: config.set("generalSynchronizeIcons", checked) + } + + SpinBox { + id: urgency + Kirigami.FormData.label: i18n("Minimum urgency level:") + from: 0 + to: 2 + onValueModified: config.set("generalUrgency", value) + } + +} diff --git a/plugins/sendnotifications/notificationslistener.cpp b/plugins/sendnotifications/notificationslistener.cpp index 1d79f7eba..842384f60 100644 --- a/plugins/sendnotifications/notificationslistener.cpp +++ b/plugins/sendnotifications/notificationslistener.cpp @@ -205,7 +205,7 @@ uint NotificationsListener::Notify(const QString& appName, uint replacesId, if (!app.active) return 0; - if (timeout > 0 && m_plugin->config()->get(QStringLiteral("generalPersistent"), false)) + if (timeout > 0 && m_plugin->config()->getBool(QStringLiteral("generalPersistent"), false)) return 0; int urgency = -1; @@ -215,11 +215,11 @@ uint NotificationsListener::Notify(const QString& appName, uint replacesId, if (!ok) urgency = -1; } - if (urgency > -1 && urgency < m_plugin->config()->get(QStringLiteral("generalUrgency"), 0)) + if (urgency > -1 && urgency < m_plugin->config()->getInt(QStringLiteral("generalUrgency"), 0)) return 0; QString ticker = summary; - if (!body.isEmpty() && m_plugin->config()->get(QStringLiteral("generalIncludeBody"), true)) + if (!body.isEmpty() && m_plugin->config()->getBool(QStringLiteral("generalIncludeBody"), true)) ticker += QStringLiteral(": ") + body; if (app.blacklistExpression.isValid() && @@ -238,7 +238,7 @@ uint NotificationsListener::Notify(const QString& appName, uint replacesId, // clearability is pointless // sync any icon data? - if (m_plugin->config()->get(QStringLiteral("generalSynchronizeIcons"), true)) { + if (m_plugin->config()->getBool(QStringLiteral("generalSynchronizeIcons"), true)) { QSharedPointer iconSource; // try different image sources according to priorities in notifications- // spec version 1.2: diff --git a/plugins/sendnotifications/sendnotifications_config.cpp b/plugins/sendnotifications/sendnotifications_config.cpp index 9adb8b271..4c9ab81fa 100644 --- a/plugins/sendnotifications/sendnotifications_config.cpp +++ b/plugins/sendnotifications/sendnotifications_config.cpp @@ -74,13 +74,13 @@ void SendNotificationsConfig::loadApplications() void SendNotificationsConfig::load() { KCModule::load(); - bool persistent = config()->get(QStringLiteral("generalPersistent"), false); + bool persistent = config()->getBool(QStringLiteral("generalPersistent"), false); m_ui->check_persistent->setChecked(persistent); - bool body = config()->get(QStringLiteral("generalIncludeBody"), true); + bool body = config()->getBool(QStringLiteral("generalIncludeBody"), true); m_ui->check_body->setChecked(body); - bool icons = config()->get(QStringLiteral("generalSynchronizeIcons"), true); + bool icons = config()->getBool(QStringLiteral("generalSynchronizeIcons"), true); m_ui->check_icons->setChecked(icons); - int urgency = config()->get(QStringLiteral("generalUrgency"), 0); + int urgency = config()->getInt(QStringLiteral("generalUrgency"), 0); m_ui->spin_urgency->setValue(urgency); loadApplications(); diff --git a/plugins/share/kdeconnect_share_config.qml b/plugins/share/kdeconnect_share_config.qml new file mode 100644 index 000000000..7e074a714 --- /dev/null +++ b/plugins/share/kdeconnect_share_config.qml @@ -0,0 +1,53 @@ +import QtQuick 2.2 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 +import org.kde.kirigami 2.5 as Kirigami +import Qt.labs.platform 1.1 +import org.kde.kdeconnect 1.0 + +Kirigami.FormLayout { + + property string device + + property var action: Kirigami.Action { + icon.name: "dialog-ok" + text: i18n("Apply") + onTriggered: config.set("incoming_path", path.text) + } + + FolderDialog { + id: folderDialog + currentFolder: path.text + + onAccepted: { + path.text = currentFolder.toString().replace("file://", "") + } + } + + KdeConnectPluginConfig { + id: config + deviceId: device + pluginName: "kdeconnect_share" + + onConfigChanged: { + path.text = get("incoming_path", StandardPaths.writableLocation(StandardPaths.DownloadsLocation).toString().replace("file://", "")) + } + } + + RowLayout { + Kirigami.FormData.label: i18n("Save files in:") + + TextField { + id: path + } + + Button { + icon.name: "document-open" + onClicked: folderDialog.open() + } + } + + Label { + text: "%1 in the path will be replaced with the specific device name" + } +} diff --git a/plugins/share/share_config.cpp b/plugins/share/share_config.cpp index 6222a579e..06af1492f 100644 --- a/plugins/share/share_config.cpp +++ b/plugins/share/share_config.cpp @@ -45,7 +45,7 @@ void ShareConfig::load() KCModule::load(); const auto standardPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); - m_ui->kurlrequester->setText(config()->get(QStringLiteral("incoming_path"), standardPath)); + m_ui->kurlrequester->setText(config()->getString(QStringLiteral("incoming_path"), standardPath)); Q_EMIT changed(false); } diff --git a/plugins/share/shareplugin.cpp b/plugins/share/shareplugin.cpp index d12e39ad9..ff922d648 100644 --- a/plugins/share/shareplugin.cpp +++ b/plugins/share/shareplugin.cpp @@ -37,7 +37,7 @@ SharePlugin::SharePlugin(QObject* parent, const QVariantList& args) QUrl SharePlugin::destinationDir() const { const QString defaultDownloadPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); - QUrl dir = QUrl::fromLocalFile(config()->get(QStringLiteral("incoming_path"), defaultDownloadPath)); + QUrl dir = QUrl::fromLocalFile(config()->getString(QStringLiteral("incoming_path"), defaultDownloadPath)); if (dir.path().contains(QLatin1String("%1"))) { dir.setPath(dir.path().arg(device()->name())); diff --git a/smsapp/qml/AttachmentViewer.qml b/smsapp/qml/AttachmentViewer.qml index 10d14f5d7..1e2e40f45 100644 --- a/smsapp/qml/AttachmentViewer.qml +++ b/smsapp/qml/AttachmentViewer.qml @@ -1,21 +1,7 @@ /** - * Copyright (C) 2020 Aniket Kumar + * SPDX-FileCopyrightText: 2020 Aniket Kumar * - * 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 of - * the License or (at your option) version 3 or any later version - * accepted by the membership of KDE e.V. (or its successor approved - * by the membership of KDE e.V.), which shall act as a proxy - * defined in Section 14 of 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 . + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ import QtQuick 2.12 diff --git a/smsapp/qml/SendingArea.qml b/smsapp/qml/SendingArea.qml index c5a8fab5b..1ef1307d3 100644 --- a/smsapp/qml/SendingArea.qml +++ b/smsapp/qml/SendingArea.qml @@ -1,21 +1,7 @@ /** - * Copyright (C) 2020 Aniket Kumar + * SPDX-FileCopyrightText: 2020 Aniket Kumar * - * 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 of - * the License or (at your option) version 3 or any later version - * accepted by the membership of KDE e.V. (or its successor approved - * by the membership of KDE e.V.), which shall act as a proxy - * defined in Section 14 of 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 . + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ import QtQuick 2.1