Purpose

alternativesmodel.cpp
1/*
2 SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
3
4 SPDX-License-Identifier: LGPL-2.1-or-later
5*/
6
7#include "alternativesmodel.h"
8#include <QDBusConnection>
9#include <QDBusConnectionInterface>
10#include <QDebug>
11#include <QDirIterator>
12#include <QIcon>
13#include <QJsonArray>
14#include <QMimeDatabase>
15#include <QMimeType>
16#include <QRegularExpression>
17#include <QStandardPaths>
18
19#include <KConfigGroup>
20#include <KJsonUtils>
21#include <KPluginMetaData>
22#include <KSharedConfig>
23
24#include "configuration.h"
25#include "helper.h"
26#include "job.h"
27
28using namespace Purpose;
29
30static const QStringList s_defaultDisabledPlugins = {QStringLiteral("saveasplugin")};
31
32typedef bool (*matchFunction)(const QString &constraint, const QJsonValue &value);
33
34static bool defaultMatch(const QString &constraint, const QJsonValue &value)
35{
36 return value == QJsonValue(constraint);
37}
38
39static bool mimeTypeMatch(const QString &constraint, const QJsonValue &value)
40{
41 if (value.isArray()) {
42 const auto array = value.toArray();
43 for (const QJsonValue &val : array) {
44 if (mimeTypeMatch(constraint, val))
45 return true;
46 }
47 return false;
48 } else if (value.isObject()) {
49 for (const QJsonValue &val : value.toObject()) {
50 if (mimeTypeMatch(constraint, val))
51 return true;
52 }
53 return false;
54 } else if (constraint.contains(QLatin1Char('*'))) {
56 return re.match(value.toString()).hasMatch();
57 } else {
59 QMimeType mime = db.mimeTypeForName(value.toString());
60 return mime.inherits(constraint);
61 }
62}
63
64static bool dbusMatch(const QString &constraint, const QJsonValue &value)
65{
66 Q_UNUSED(value)
68}
69
70static bool executablePresent(const QString &constraint, const QJsonValue &value)
71{
72 Q_UNUSED(value)
73 return !QStandardPaths::findExecutable(constraint).isEmpty();
74}
75
76static bool desktopFilePresent(const QString &constraint, const QJsonValue &value)
77{
78 Q_UNUSED(value)
80}
81
82static QMap<QString, matchFunction> s_matchFunctions = {
83 {QStringLiteral("mimeType"), mimeTypeMatch},
84 {QStringLiteral("dbus"), dbusMatch},
85 {QStringLiteral("application"), desktopFilePresent},
86 {QStringLiteral("exec"), executablePresent},
87};
88
89class Purpose::AlternativesModelPrivate
90{
91public:
92 QList<KPluginMetaData> m_plugins;
93 QJsonObject m_inputData;
94 QString m_pluginType;
95 QStringList m_disabledPlugins = s_defaultDisabledPlugins;
96 QJsonObject m_pluginTypeData;
97 const QRegularExpression constraintRx{QStringLiteral("(\\w+):(.*)")};
98
99 bool isPluginAcceptable(const KPluginMetaData &meta, const QStringList &disabledPlugins) const
100 {
101 const QJsonObject obj = meta.rawData();
102 if (!obj.value(QLatin1String("X-Purpose-PluginTypes")).toArray().contains(m_pluginType)) {
103 // qDebug() << "discarding" << meta.name() << KPluginMetaData::readStringList(meta.rawData(), QStringLiteral("X-Purpose-PluginTypes"));
104 return false;
105 }
106
107 if (disabledPlugins.contains(meta.pluginId()) || m_disabledPlugins.contains(meta.pluginId())) {
108 // qDebug() << "disabled plugin" << meta.name() << meta.pluginId();
109 return false;
110 }
111
112 // All constraints must match
113 const QJsonArray constraints = obj.value(QLatin1String("X-Purpose-Constraints")).toArray();
114 for (const QJsonValue &constraint : constraints) {
115 if (!constraintMatches(meta, constraint))
116 return false;
117 }
118 return true;
119 }
120
121 bool constraintMatches(const KPluginMetaData &meta, const QJsonValue &constraint) const
122 {
123 // Treat an array as an OR
124 if (constraint.isArray()) {
125 const QJsonArray options = constraint.toArray();
126 for (const auto &option : options) {
127 if (constraintMatches(meta, option)) {
128 return true;
129 }
130 }
131 return false;
132 }
133 Q_ASSERT(constraintRx.isValid());
134 QRegularExpressionMatch match = constraintRx.match(constraint.toString());
135 if (!match.isValid() || !match.hasMatch()) {
136 qWarning() << "wrong constraint" << constraint.toString();
137 return false;
138 }
139 const QString propertyName = match.captured(1);
140 const QString constrainedValue = match.captured(2);
141 const bool acceptable = s_matchFunctions.value(propertyName, defaultMatch)(constrainedValue, m_inputData.value(propertyName));
142 if (!acceptable) {
143 // qDebug() << "not accepted" << meta.name() << propertyName << constrainedValue << m_inputData[propertyName];
144 }
145 return acceptable;
146 }
147};
148
149AlternativesModel::AlternativesModel(QObject *parent)
150 : QAbstractListModel(parent)
151 , d_ptr(new AlternativesModelPrivate)
152{
153}
154
155AlternativesModel::~AlternativesModel()
156{
158 delete d;
159}
160
161QHash<int, QByteArray> AlternativesModel::roleNames() const
162{
164 roles.insert(IconNameRole, QByteArrayLiteral("iconName"));
165 roles.insert(PluginIdRole, QByteArrayLiteral("pluginId"));
166 roles.insert(ActionDisplayRole, QByteArrayLiteral("actionDisplay"));
167 return roles;
168}
169
170void AlternativesModel::setInputData(const QJsonObject &input)
171{
173 if (input == d->m_inputData)
174 return;
175
176 d->m_inputData = input;
177 initializeModel();
178
179 Q_EMIT inputDataChanged();
180}
181
182void AlternativesModel::setPluginType(const QString &pluginType)
183{
185 if (pluginType == d->m_pluginType)
186 return;
187
188 d->m_pluginTypeData = Purpose::readPluginType(pluginType);
189 d->m_pluginType = pluginType;
190 Q_ASSERT(d->m_pluginTypeData.isEmpty() == d->m_pluginType.isEmpty());
191
192 initializeModel();
193
194 Q_EMIT pluginTypeChanged();
195}
196
198{
199 Q_D(const AlternativesModel);
200 return d->m_disabledPlugins;
201}
202
203void AlternativesModel::setDisabledPlugins(const QStringList &pluginIds)
204{
206 if (pluginIds == d->m_disabledPlugins)
207 return;
208
209 d->m_disabledPlugins = pluginIds;
210
211 initializeModel();
212
213 Q_EMIT disabledPluginsChanged();
214}
215
217{
218 Q_D(const AlternativesModel);
219 return d->m_pluginType;
220}
221
223{
224 Q_D(const AlternativesModel);
225 return d->m_inputData;
226}
227
229{
231 const KPluginMetaData pluginData = d->m_plugins.at(row);
232 return new Configuration(d->m_inputData, d->m_pluginType, d->m_pluginTypeData, pluginData, this);
233}
234
235int AlternativesModel::rowCount(const QModelIndex &parent) const
236{
237 Q_D(const AlternativesModel);
238 return parent.isValid() ? 0 : d->m_plugins.count();
239}
240
241QVariant AlternativesModel::data(const QModelIndex &index, int role) const
242{
243 Q_D(const AlternativesModel);
244 if (!index.isValid() || index.row() > d->m_plugins.count())
245 return QVariant();
246
247 KPluginMetaData data = d->m_plugins[index.row()];
248 switch (role) {
249 case Qt::DisplayRole:
250 return data.name();
251 case Qt::ToolTip:
252 return data.description();
253 case IconNameRole:
254 return data.iconName();
256 return QIcon::fromTheme(data.iconName());
257 case PluginIdRole:
258 return data.pluginId();
259 case ActionDisplayRole: {
260 const QJsonObject pluginData = data.rawData().value(QLatin1String("KPlugin")).toObject();
261 const QString action = KJsonUtils::readTranslatedString(pluginData, QStringLiteral("X-Purpose-ActionDisplay"));
262 return action.isEmpty() ? data.name() : action;
263 }
264 }
265 return QVariant();
266}
267
268static QList<KPluginMetaData> findScriptedPackages(std::function<bool(const KPluginMetaData &)> filter)
269{
271 QSet<QString> addedPlugins;
272 const QStringList dirs =
274 for (const QString &dir : dirs) {
276
277 for (; dirIt.hasNext();) {
278 QDir dir(dirIt.next());
279 Q_ASSERT(dir.exists());
280 if (!dir.exists(QStringLiteral("metadata.json")))
281 continue;
282
283 const KPluginMetaData info = Purpose::createMetaData(dir.absoluteFilePath(QStringLiteral("metadata.json")));
284 if (!addedPlugins.contains(info.pluginId()) && filter(info)) {
285 addedPlugins << info.pluginId();
286 ret += info;
287 }
288 }
289 }
290
291 return ret;
292}
293
294void AlternativesModel::initializeModel()
295{
297 if (d->m_pluginType.isEmpty()) {
298 return;
299 }
300
301 const QJsonArray inbound = d->m_pluginTypeData.value(QLatin1String("X-Purpose-InboundArguments")).toArray();
302 for (const QJsonValue &arg : inbound) {
303 if (!d->m_inputData.contains(arg.toString())) {
304 qWarning().nospace() << "Cannot initialize model with data " << d->m_inputData << ". missing: " << arg;
305 return;
306 }
307 }
308
309 const auto config = KSharedConfig::openConfig(QStringLiteral("purposerc"));
310 const auto group = config->group(QStringLiteral("plugins"));
311 const QStringList disabledPlugins = group.readEntry("disabled", QStringList());
312 auto pluginAcceptable = [d, disabledPlugins](const KPluginMetaData &meta) {
313 return d->isPluginAcceptable(meta, disabledPlugins);
314 };
315
317 d->m_plugins.clear();
318 d->m_plugins << KPluginMetaData::findPlugins(QStringLiteral("kf6/purpose"), pluginAcceptable);
319 d->m_plugins += findScriptedPackages(pluginAcceptable);
321}
322
323#include "moc_alternativesmodel.cpp"
QString pluginId() const
QJsonObject rawData() const
static QList< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter={}, KPluginMetaDataOptions options={})
QString iconName() const
QString name() const
QString description() const
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
Interface for client applications to share data.
QJsonObject inputData
Specifies the information that will be given to the plugin once it's started.
QString pluginType
Specifies the type of the plugin we want to list.
QStringList disabledPlugins
Provides a list of plugin names to have filtered out.
Q_SCRIPTABLE Purpose::Configuration * configureJob(int row)
This shouldn't require to have the job actually running on the same process as the app.
This class will be in charge of figuring out the job configuration.
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT QString dir(const QString &fileClass)
virtual QHash< int, QByteArray > roleNames() const const
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
QDBusConnectionInterface * interface() const const
QDBusConnection sessionBus()
QDBusReply< bool > isServiceRegistered(const QString &serviceName) const const
iterator insert(const Key &key, const T &value)
QIcon fromTheme(const QString &name)
QJsonValue value(QLatin1StringView key) const const
bool isArray() const const
bool isObject() const const
QJsonArray toArray() const const
QJsonObject toObject() const const
QString toString() const const
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
bool inherits(const QString &mimeTypeName) const const
bool isValid() const const
int row() const const
Q_EMITQ_EMIT
QObject * parent() const const
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
bool isValid() const const
QString wildcardToRegularExpression(QStringView pattern, WildcardConversionOptions options)
bool contains(const QSet< T > &other) const const
QString findExecutable(const QString &executableName, const QStringList &paths)
QString locate(StandardLocation type, const QString &fileName, LocateOptions options)
QStringList locateAll(StandardLocation type, const QString &fileName, LocateOptions options)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
DisplayRole
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:05 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.