KRunner

resultsmodel.cpp
1/*
2 * This file is part of the KDE Milou Project
3 * SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@broulik.de>
4 * SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
5 *
6 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
7 *
8 */
9
10#include "resultsmodel.h"
11
12#include "runnerresultsmodel_p.h"
13
14#include <QIdentityProxyModel>
15#include <QPointer>
16
17#include <KConfigGroup>
18#include <KDescendantsProxyModel>
19#include <KModelIndexProxyMapper>
20#include <KRunner/AbstractRunner>
21#include <QTimer>
22#include <cmath>
23
24using namespace KRunner;
25
26/**
27 * Sorts the matches and categories by their type and relevance
28 *
29 * A category gets type and relevance of the highest
30 * scoring match within.
31 */
32class SortProxyModel : public QSortFilterProxyModel
33{
35
36public:
37 explicit SortProxyModel(QObject *parent)
39 {
42 }
43
44 void setQueryString(const QString &queryString)
45 {
46 const QStringList words = queryString.split(QLatin1Char(' '), Qt::SkipEmptyParts);
47 if (m_words != words) {
48 m_words = words;
49 invalidate();
50 }
51 }
52
53protected:
54 bool lessThan(const QModelIndex &sourceA, const QModelIndex &sourceB) const override
55 {
56 bool isCategoryComparison = !sourceA.internalId() && !sourceB.internalId();
57 Q_ASSERT((bool)sourceA.internalId() == (bool)sourceB.internalId());
58 // Only check the favorite index if we compare categories. For individual matches, they will always be the same
59 if (isCategoryComparison) {
60 const int favoriteA = sourceA.data(ResultsModel::FavoriteIndexRole).toInt();
61 const int favoriteB = sourceB.data(ResultsModel::FavoriteIndexRole).toInt();
62 bool isFavoriteA = favoriteA != -1;
63 bool isFavoriteB = favoriteB != -1;
64 if (isFavoriteA && !isFavoriteB) {
65 return false;
66 } else if (!isFavoriteA && isFavoriteB) {
67 return true;
68 }
69
70 const int favoritesCount = sourceA.data(ResultsModel::FavoriteCountRole).toInt();
71 const double favoriteAMultiplicationFactor = (favoriteA ? 1 + ((favoritesCount - favoriteA) * 0.2) : 1);
72 const double typeA = sourceA.data(ResultsModel::CategoryRelevanceRole).toReal() * favoriteAMultiplicationFactor;
73 const double favoriteBMultiplicationFactor = (favoriteB ? 1 + ((favoritesCount - favoriteB) * 0.2) : 1);
74 const double typeB = sourceB.data(ResultsModel::CategoryRelevanceRole).toReal() * favoriteBMultiplicationFactor;
75 return typeA < typeB;
76 }
77
78 const qreal relevanceA = sourceA.data(ResultsModel::RelevanceRole).toReal();
79 const qreal relevanceB = sourceB.data(ResultsModel::RelevanceRole).toReal();
80
81 if (!qFuzzyCompare(relevanceA, relevanceB)) {
82 return relevanceA < relevanceB;
83 }
84
85 return QSortFilterProxyModel::lessThan(sourceA, sourceB);
86 }
87
88public:
89 QStringList m_words;
90};
91
92/**
93 * Distributes the number of matches shown per category
94 *
95 * Each category may occupy a maximum of 1/(n+1) of the given @c limit,
96 * this means the further down you get, the less matches there are.
97 * There is at least one match shown per category.
98 *
99 * This model assumes the results to already be sorted
100 * descending by their relevance/score.
101 */
102class CategoryDistributionProxyModel : public QSortFilterProxyModel
103{
105
106public:
107 explicit CategoryDistributionProxyModel(QObject *parent)
109 {
110 }
111 void setSourceModel(QAbstractItemModel *sourceModel) override
112 {
113 if (this->sourceModel()) {
114 disconnect(this->sourceModel(), nullptr, this, nullptr);
115 }
116
118
119 if (sourceModel) {
123 }
124 }
125
126 int limit() const
127 {
128 return m_limit;
129 }
130
131 void setLimit(int limit)
132 {
133 if (m_limit == limit) {
134 return;
135 }
136 m_limit = limit;
138 Q_EMIT limitChanged();
139 }
140
142 void limitChanged();
143
144protected:
145 bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
146 {
147 if (m_limit <= 0) {
148 return true;
149 }
150
151 if (!sourceParent.isValid()) {
152 return true;
153 }
154
155 const int categoryCount = sourceModel()->rowCount();
156
157 int maxItemsInCategory = m_limit;
158
159 if (categoryCount > 1) {
160 int itemsBefore = 0;
161 for (int i = 0; i <= sourceParent.row(); ++i) {
162 const int itemsInCategory = sourceModel()->rowCount(sourceModel()->index(i, 0));
163
164 // Take into account that every category gets at least one item shown
165 const int availableSpace = m_limit - itemsBefore - std::ceil(m_limit / qreal(categoryCount));
166
167 // The further down the category is the less relevant it is and the less space it my occupy
168 // First category gets max half the total limit, second category a third, etc
169 maxItemsInCategory = std::min(availableSpace, int(std::ceil(m_limit / qreal(i + 2))));
170
171 // At least show one item per category
172 maxItemsInCategory = std::max(1, maxItemsInCategory);
173
174 itemsBefore += std::min(itemsInCategory, maxItemsInCategory);
175 }
176 }
177
178 if (sourceRow >= maxItemsInCategory) {
179 return false;
180 }
181
182 return true;
183 }
184
185private:
186 // if you change this, update the default in resetLimit()
187 int m_limit = 0;
188};
189
190/**
191 * This model hides the root items of data originally in a tree structure
192 *
193 * KDescendantsProxyModel collapses the items but keeps all items in tact.
194 * The root items of the RunnerMatchesModel represent the individual cateories
195 * which we don't want in the resulting flat list.
196 * This model maps the items back to the given @c treeModel and filters
197 * out any item with an invalid parent, i.e. "on the root level"
198 */
199class HideRootLevelProxyModel : public QSortFilterProxyModel
200{
202
203public:
204 explicit HideRootLevelProxyModel(QObject *parent)
206 {
207 }
208
209 QAbstractItemModel *treeModel() const
210 {
211 return m_treeModel;
212 }
213 void setTreeModel(QAbstractItemModel *treeModel)
214 {
215 m_treeModel = treeModel;
217 }
218
219protected:
220 bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
221 {
222 KModelIndexProxyMapper mapper(sourceModel(), m_treeModel);
223 const QModelIndex treeIdx = mapper.mapLeftToRight(sourceModel()->index(sourceRow, 0, sourceParent));
224 return treeIdx.parent().isValid();
225 }
226
227private:
228 QAbstractItemModel *m_treeModel = nullptr;
229};
230
231class KRunner::ResultsModelPrivate
232{
233public:
234 explicit ResultsModelPrivate(const KConfigGroup &configGroup, const KConfigGroup &stateConfigGroup, ResultsModel *q)
235 : q(q)
236 , resultsModel(new RunnerResultsModel(configGroup, stateConfigGroup, q))
237 {
238 }
239
240 ResultsModel *q;
241
242 QPointer<KRunner::AbstractRunner> runner = nullptr;
243
244 RunnerResultsModel *const resultsModel;
245 SortProxyModel *const sortModel = new SortProxyModel(q);
246 CategoryDistributionProxyModel *const distributionModel = new CategoryDistributionProxyModel(q);
247 KDescendantsProxyModel *const flattenModel = new KDescendantsProxyModel(q);
248 HideRootLevelProxyModel *const hideRootModel = new HideRootLevelProxyModel(q);
249 const KModelIndexProxyMapper mapper{q, resultsModel};
250};
251
252ResultsModel::ResultsModel(QObject *parent)
254{
255}
256ResultsModel::ResultsModel(const KConfigGroup &configGroup, const KConfigGroup &stateConfigGroup, QObject *parent)
257 : QSortFilterProxyModel(parent)
258 , d(new ResultsModelPrivate(configGroup, stateConfigGroup, this))
259{
260 connect(d->resultsModel, &RunnerResultsModel::queryStringChanged, this, &ResultsModel::queryStringChanged);
261 connect(d->resultsModel, &RunnerResultsModel::queryingChanged, this, &ResultsModel::queryingChanged);
262 connect(d->resultsModel, &RunnerResultsModel::queryStringChangeRequested, this, &ResultsModel::queryStringChangeRequested);
263
264 // The matches for the old query string remain on display until the first set of matches arrive for the new query string.
265 // Therefore we must not update the query string inside RunnerResultsModel exactly when the query string changes, otherwise it would
266 // re-sort the old query string matches based on the new query string.
267 // So we only make it aware of the query string change at the time when we receive the first set of matches for the new query string.
268 connect(d->resultsModel, &RunnerResultsModel::matchesChanged, this, [this]() {
269 d->sortModel->setQueryString(queryString());
270 });
271
272 connect(d->distributionModel, &CategoryDistributionProxyModel::limitChanged, this, &ResultsModel::limitChanged);
273
274 // The data flows as follows:
275 // - RunnerResultsModel
276 // - SortProxyModel
277 // - CategoryDistributionProxyModel
278 // - KDescendantsProxyModel
279 // - HideRootLevelProxyModel
280
281 d->sortModel->setSourceModel(d->resultsModel);
282
283 d->distributionModel->setSourceModel(d->sortModel);
284
285 d->flattenModel->setSourceModel(d->distributionModel);
286
287 d->hideRootModel->setSourceModel(d->flattenModel);
288 d->hideRootModel->setTreeModel(d->resultsModel);
289
290 setSourceModel(d->hideRootModel);
291
292 // Initialize the runners, this will speed the first query up.
293 // While there were lots of optimizations, instantiating plugins, creating threads and AbstractRunner::init is still heavy work
294 QTimer::singleShot(0, this, [this]() {
295 runnerManager()->runners();
296 });
297}
298
299ResultsModel::~ResultsModel() = default;
300
302{
303 d->resultsModel->m_favoriteIds = ids;
304 Q_EMIT favoriteIdsChanged();
305}
306
307QStringList ResultsModel::favoriteIds() const
308{
309 return d->resultsModel->m_favoriteIds;
310}
311
313{
314 return d->resultsModel->queryString();
315}
316
317void ResultsModel::setQueryString(const QString &queryString)
318{
319 d->resultsModel->setQueryString(queryString, singleRunner());
320}
321
322int ResultsModel::limit() const
323{
324 return d->distributionModel->limit();
325}
326
327void ResultsModel::setLimit(int limit)
328{
329 d->distributionModel->setLimit(limit);
330}
331
332void ResultsModel::resetLimit()
333{
334 setLimit(0);
335}
336
337bool ResultsModel::querying() const
338{
339 return d->resultsModel->querying();
340}
341
343{
344 return d->runner ? d->runner->id() : QString();
345}
346
347void ResultsModel::setSingleRunner(const QString &runnerId)
348{
349 if (runnerId == singleRunner()) {
350 return;
351 }
352 if (runnerId.isEmpty()) {
353 d->runner = nullptr;
354 } else {
355 d->runner = runnerManager()->runner(runnerId);
356 }
357 Q_EMIT singleRunnerChanged();
358}
359
360KPluginMetaData ResultsModel::singleRunnerMetaData() const
361{
362 return d->runner ? d->runner->metadata() : KPluginMetaData();
363}
364
365QHash<int, QByteArray> ResultsModel::roleNames() const
366{
367 auto names = QAbstractProxyModel::roleNames();
368 names[IdRole] = QByteArrayLiteral("matchId"); // "id" is QML-reserved
369 names[EnabledRole] = QByteArrayLiteral("enabled");
370 names[CategoryRole] = QByteArrayLiteral("category");
371 names[SubtextRole] = QByteArrayLiteral("subtext");
372 names[UrlsRole] = QByteArrayLiteral("urls");
373 names[ActionsRole] = QByteArrayLiteral("actions");
374 names[MultiLineRole] = QByteArrayLiteral("multiLine");
375 return names;
376}
377
379{
380 d->resultsModel->clear();
381}
382
384{
385 KModelIndexProxyMapper mapper(this, d->resultsModel);
386 const QModelIndex resultsIdx = mapper.mapLeftToRight(idx);
387 if (!resultsIdx.isValid()) {
388 return false;
389 }
390 return d->resultsModel->run(resultsIdx);
391}
392
393bool ResultsModel::runAction(const QModelIndex &idx, int actionNumber)
394{
395 KModelIndexProxyMapper mapper(this, d->resultsModel);
396 const QModelIndex resultsIdx = mapper.mapLeftToRight(idx);
397 if (!resultsIdx.isValid()) {
398 return false;
399 }
400 return d->resultsModel->runAction(resultsIdx, actionNumber);
401}
402
404{
405 if (auto resultIdx = d->mapper.mapLeftToRight(idx); resultIdx.isValid()) {
406 return runnerManager()->mimeDataForMatch(d->resultsModel->fetchMatch(resultIdx));
407 }
408 return nullptr;
409}
410
411KRunner::RunnerManager *ResultsModel::runnerManager() const
412{
413 return d->resultsModel->runnerManager();
414}
415
417{
418 const QModelIndex resultIdx = d->mapper.mapLeftToRight(idx);
419 return resultIdx.isValid() ? d->resultsModel->fetchMatch(resultIdx) : QueryMatch();
420}
421
422#include "moc_resultsmodel.cpp"
423#include "resultsmodel.moc"
QModelIndex mapLeftToRight(const QModelIndex &index) const
A match returned by an AbstractRunner in response to a given RunnerContext.
Definition querymatch.h:32
A model that exposes and sorts results for a given query.
QString singleRunner
The single runner to use for querying in single runner mode.
KRunner::QueryMatch getQueryMatch(const QModelIndex &idx) const
Get match for the result at given model index idx.
Q_INVOKABLE bool runAction(const QModelIndex &idx, int actionNumber)
Run the action actionNumber at given model index idx.
bool querying
Whether the query is currently being run.
int limit
The preferred maximum number of matches in the model.
Q_INVOKABLE void clear()
Clears the model content and resets the runner context, i.e.
QString queryString
The query string to run.
void queryStringChangeRequested(const QString &queryString, int pos)
This signal is emitted when a an InformationalMatch is run, and it is advised to update the search te...
Q_INVOKABLE bool run(const QModelIndex &idx)
Run the result at the given model index idx.
Q_INVOKABLE QMimeData * getMimeData(const QModelIndex &idx) const
Get mime data for the result at given model index idx.
void setFavoriteIds(const QStringList &ids)
IDs of favorite plugins.
The RunnerManager class decides what installed runners are runnable, and their ratings.
QMimeData * mimeDataForMatch(const QueryMatch &match) const
AbstractRunner * runner(const QString &pluginId) const
Finds and returns a loaded runner or a nullptr.
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow)
void rowsRemoved(const QModelIndex &parent, int first, int last)
virtual QHash< int, QByteArray > roleNames() const const override
QVariant data(int role) const const
quintptr internalId() const const
bool isValid() const const
QModelIndex parent() const const
int row() const const
Q_EMITQ_EMIT
Q_OBJECTQ_OBJECT
Q_SIGNALSQ_SIGNALS
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QObject * parent() const const
void setDynamicSortFilter(bool enable)
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
virtual bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const const
virtual void setSourceModel(QAbstractItemModel *sourceModel) override
virtual void sort(int column, Qt::SortOrder order) override
bool isEmpty() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
DescendingOrder
SkipEmptyParts
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
int toInt(bool *ok) const const
qreal toReal(bool *ok) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 24 2024 12:01:02 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.