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(runnerManager(), &RunnerManager::queryingChanged, this, &ResultsModel::queryingChanged);
262 connect(d->resultsModel, &RunnerResultsModel::queryStringChangeRequested, this, &ResultsModel::queryStringChangeRequested);
263 connect(d->resultsModel, &RunnerResultsModel::runnerManagerChanged, this, [this]() {
264 connect(runnerManager(), &RunnerManager::queryingChanged, this, &ResultsModel::queryingChanged);
265 });
266
267 // The matches for the old query string remain on display until the first set of matches arrive for the new query string.
268 // Therefore we must not update the query string inside RunnerResultsModel exactly when the query string changes, otherwise it would
269 // re-sort the old query string matches based on the new query string.
270 // 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.
271 connect(d->resultsModel, &RunnerResultsModel::matchesChanged, this, [this]() {
272 d->sortModel->setQueryString(queryString());
273 });
274
275 connect(d->distributionModel, &CategoryDistributionProxyModel::limitChanged, this, &ResultsModel::limitChanged);
276
277 // The data flows as follows:
278 // - RunnerResultsModel
279 // - SortProxyModel
280 // - CategoryDistributionProxyModel
281 // - KDescendantsProxyModel
282 // - HideRootLevelProxyModel
283
284 d->sortModel->setSourceModel(d->resultsModel);
285
286 d->distributionModel->setSourceModel(d->sortModel);
287
288 d->flattenModel->setSourceModel(d->distributionModel);
289
290 d->hideRootModel->setSourceModel(d->flattenModel);
291 d->hideRootModel->setTreeModel(d->resultsModel);
292
293 setSourceModel(d->hideRootModel);
294
295 // Initialize the runners, this will speed the first query up.
296 // While there were lots of optimizations, instantiating plugins, creating threads and AbstractRunner::init is still heavy work
297 QTimer::singleShot(0, this, [this]() {
298 runnerManager()->runners();
299 });
300}
301
302ResultsModel::~ResultsModel() = default;
303
305{
306 d->resultsModel->m_favoriteIds = ids;
307 Q_EMIT favoriteIdsChanged();
308}
309
310QStringList ResultsModel::favoriteIds() const
311{
312 return d->resultsModel->m_favoriteIds;
313}
314
316{
317 return d->resultsModel->queryString();
318}
319
320void ResultsModel::setQueryString(const QString &queryString)
321{
322 d->resultsModel->setQueryString(queryString, singleRunner());
323}
324
325int ResultsModel::limit() const
326{
327 return d->distributionModel->limit();
328}
329
330void ResultsModel::setLimit(int limit)
331{
332 d->distributionModel->setLimit(limit);
333}
334
335void ResultsModel::resetLimit()
336{
337 setLimit(0);
338}
339
340bool ResultsModel::querying() const
341{
342 return runnerManager()->querying();
343}
344
346{
347 return d->runner ? d->runner->id() : QString();
348}
349
350void ResultsModel::setSingleRunner(const QString &runnerId)
351{
352 if (runnerId == singleRunner()) {
353 return;
354 }
355 if (runnerId.isEmpty()) {
356 d->runner = nullptr;
357 } else {
358 d->runner = runnerManager()->runner(runnerId);
359 }
360 Q_EMIT singleRunnerChanged();
361}
362
363KPluginMetaData ResultsModel::singleRunnerMetaData() const
364{
365 return d->runner ? d->runner->metadata() : KPluginMetaData();
366}
367
368QHash<int, QByteArray> ResultsModel::roleNames() const
369{
370 auto names = QAbstractProxyModel::roleNames();
371 names[IdRole] = QByteArrayLiteral("matchId"); // "id" is QML-reserved
372 names[EnabledRole] = QByteArrayLiteral("enabled");
373 names[CategoryRole] = QByteArrayLiteral("category");
374 names[SubtextRole] = QByteArrayLiteral("subtext");
375 names[UrlsRole] = QByteArrayLiteral("urls");
376 names[ActionsRole] = QByteArrayLiteral("actions");
377 names[MultiLineRole] = QByteArrayLiteral("multiLine");
378 return names;
379}
380
382{
383 d->resultsModel->clear();
384}
385
387{
388 KModelIndexProxyMapper mapper(this, d->resultsModel);
389 const QModelIndex resultsIdx = mapper.mapLeftToRight(idx);
390 if (!resultsIdx.isValid()) {
391 return false;
392 }
393 return d->resultsModel->run(resultsIdx);
394}
395
396bool ResultsModel::runAction(const QModelIndex &idx, int actionNumber)
397{
398 KModelIndexProxyMapper mapper(this, d->resultsModel);
399 const QModelIndex resultsIdx = mapper.mapLeftToRight(idx);
400 if (!resultsIdx.isValid()) {
401 return false;
402 }
403 return d->resultsModel->runAction(resultsIdx, actionNumber);
404}
405
407{
408 if (auto resultIdx = d->mapper.mapLeftToRight(idx); resultIdx.isValid()) {
409 return runnerManager()->mimeDataForMatch(d->resultsModel->fetchMatch(resultIdx));
410 }
411 return nullptr;
412}
413
414KRunner::RunnerManager *ResultsModel::runnerManager() const
415{
416 return d->resultsModel->runnerManager();
417}
418
420{
421 const QModelIndex resultIdx = d->mapper.mapLeftToRight(idx);
422 return resultIdx.isValid() ? d->resultsModel->fetchMatch(resultIdx) : QueryMatch();
423}
424
426{
427 d->resultsModel->setRunnerManager(manager);
429}
430
431#include "moc_resultsmodel.cpp"
432#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.
Q_SIGNAL void runnerManagerChanged()
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.
void setRunnerManager(KRunner::RunnerManager *manager)
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 queryingChanged()
Emitted when the querying status has changed.
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 Dec 6 2024 12:07:49 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.