Commit 400c800d authored by Nicolas Fella's avatar Nicolas Fella
Browse files

[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.
parent b88927a6
Pipeline #33610 passed with stage
in 50 minutes and 47 seconds
......@@ -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()
......@@ -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 {
......
/*
* SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
*
* 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
})
}
}
}
/*
* SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
*
* 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
})
}
}
}
]
}
}
}
......@@ -11,5 +11,7 @@
<file>qml/runcommand.qml</file>
<file>qml/volume.qml</file>
<file>qml/MprisSlider.qml</file>
<file>qml/PluginSettings.qml</file>
<file>qml/PluginInfoPage.qml</file>
</qresource>
</RCC>
......@@ -9,6 +9,8 @@
#include <QDir>
#include <QSettings>
#include <QDBusMessage>
#include <QDBusConnection>
#include <QDebug>
#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).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);
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();
}
......@@ -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<typename T> T get(const QString& key, const T& defaultValue = {}) {
return get(key, QVariant(defaultValue)).template value<T>(); //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<KdeConnectPluginConfigPrivate> d;
QString m_deviceId;
QString m_pluginName;
};
#endif
......@@ -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)
......
......@@ -19,6 +19,9 @@
#include "interfaces/notificationsmodel.h"
#include <remotecommandsmodel.h>
#include <remotesinksmodel.h>
#include <pluginmodel.h>
#include "core/kdeconnectpluginconfig.h"
#include "interfaces/commandsmodel.h"
QObject* createDBusResponse()
{
......@@ -43,6 +46,9 @@ void KdeConnectDeclarativePlugin::registerTypes(const char* uri)
qmlRegisterType<DBusAsyncResponse>(uri, 1, 0, "DBusAsyncResponse");
qmlRegisterType<DevicesSortProxyModel>(uri, 1, 0, "DevicesSortProxyModel");
qmlRegisterType<RemoteSinksModel>(uri, 1, 0, "RemoteSinksModel");
qmlRegisterType<PluginModel>(uri, 1, 0, "PluginModel");
qmlRegisterType<KdeConnectPluginConfig>(uri, 1, 0, "KdeConnectPluginConfig");
qmlRegisterType<CommandsModel>(uri, 1, 0, "CommandsModel");
qmlRegisterUncreatableType<MprisDbusInterface>(uri, 1, 0, "MprisDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterUncreatableType<LockDeviceDbusInterface>(uri, 1, 0, "LockDeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterUncreatableType<FindMyPhoneDeviceDbusInterface>(uri, 1, 0, "FindMyPhoneDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
......
......@@ -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)
/**
* SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "commandsmodel.h"
#include <QDebug>
#include <QJsonObject>
#include <QJsonDocument>
#include <QUuid>
#include <dbushelper.h>
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<int, QByteArray> CommandsModel::roleNames() const
{
//Role names for QML
QHash<int, QByteArray> 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();
}
/**
* SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#ifndef COMMANDSMODEL_H
#define COMMANDSMODEL_H
#include <QAbstractListModel>
#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<int, QByteArray> 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<CommandEntry> m_commandList;
QString m_deviceId;
KdeConnectPluginConfig m_config;
};
#endif // DEVICESMODEL_H
/**
* SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "pluginmodel.h"
#include <QDebug>
#include <KPluginMetaData>
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)