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 if (favoriteA != favoriteB) {
63 return favoriteA > favoriteB;
64 }
65
66 const int typeA = sourceA.data(ResultsModel::CategoryRelevanceRole).toReal();
67 const int typeB = sourceB.data(ResultsModel::CategoryRelevanceRole).toReal();
68 return typeA < typeB;
69 }
70
71 const qreal relevanceA = sourceA.data(ResultsModel::RelevanceRole).toReal();
72 const qreal relevanceB = sourceB.data(ResultsModel::RelevanceRole).toReal();
73
74 if (!qFuzzyCompare(relevanceA, relevanceB)) {
75 return relevanceA < relevanceB;
76 }
77
78 return QSortFilterProxyModel::lessThan(sourceA, sourceB);
79 }
80
81public:
82 QStringList m_words;
83};
84
85/**
86 * Distributes the number of matches shown per category
87 *
88 * Each category may occupy a maximum of 1/(n+1) of the given @c limit,
89 * this means the further down you get, the less matches there are.
90 * There is at least one match shown per category.
91 *
92 * This model assumes the results to already be sorted
93 * descending by their relevance/score.
94 */
95class CategoryDistributionProxyModel : public QSortFilterProxyModel
96{
98
99public:
100 explicit CategoryDistributionProxyModel(QObject *parent)
102 {
103 }
104 void setSourceModel(QAbstractItemModel *sourceModel) override
105 {
106 if (this->sourceModel()) {
107 disconnect(this->sourceModel(), nullptr, this, nullptr);
108 }
109
111
112 if (sourceModel) {
116 }
117 }
118
119 int limit() const
120 {
121 return m_limit;
122 }
123
124 void setLimit(int limit)
125 {
126 if (m_limit == limit) {
127 return;
128 }
129 m_limit = limit;
131 Q_EMIT limitChanged();
132 }
133
135 void limitChanged();
136
137protected:
138 bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
139 {
140 if (m_limit <= 0) {
141 return true;
142 }
143
144 if (!sourceParent.isValid()) {
145 return true;
146 }
147
148 const int categoryCount = sourceModel()->rowCount();
149
150 int maxItemsInCategory = m_limit;
151
152 if (categoryCount > 1) {
153 int itemsBefore = 0;
154 for (int i = 0; i <= sourceParent.row(); ++i) {
155 const int itemsInCategory = sourceModel()->rowCount(sourceModel()->index(i, 0));
156
157 // Take into account that every category gets at least one item shown
158 const int availableSpace = m_limit - itemsBefore - std::ceil(m_limit / qreal(categoryCount));
159
160 // The further down the category is the less relevant it is and the less space it my occupy
161 // First category gets max half the total limit, second category a third, etc
162 maxItemsInCategory = std::min(availableSpace, int(std::ceil(m_limit / qreal(i + 2))));
163
164 // At least show one item per category
165 maxItemsInCategory = std::max(1, maxItemsInCategory);
166
167 itemsBefore += std::min(itemsInCategory, maxItemsInCategory);
168 }
169 }
170
171 if (sourceRow >= maxItemsInCategory) {
172 return false;
173 }
174
175 return true;
176 }
177
178private:
179 // if you change this, update the default in resetLimit()
180 int m_limit = 0;
181};
182
183/**
184 * This model hides the root items of data originally in a tree structure
185 *
186 * KDescendantsProxyModel collapses the items but keeps all items in tact.
187 * The root items of the RunnerMatchesModel represent the individual cateories
188 * which we don't want in the resulting flat list.
189 * This model maps the items back to the given @c treeModel and filters
190 * out any item with an invalid parent, i.e. "on the root level"
191 */
192class HideRootLevelProxyModel : public QSortFilterProxyModel
193{
195
196public:
197 explicit HideRootLevelProxyModel(QObject *parent)
199 {
200 }
201
202 QAbstractItemModel *treeModel() const
203 {
204 return m_treeModel;
205 }
206 void setTreeModel(QAbstractItemModel *treeModel)
207 {
208 m_treeModel = treeModel;
210 }
211
212protected:
213 bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
214 {
215 KModelIndexProxyMapper mapper(sourceModel(), m_treeModel);
216 const QModelIndex treeIdx = mapper.mapLeftToRight(sourceModel()->index(sourceRow, 0, sourceParent));
217 return treeIdx.parent().isValid();
218 }
219
220private:
221 QAbstractItemModel *m_treeModel = nullptr;
222};
223
224class KRunner::ResultsModelPrivate
225{
226public:
227 explicit ResultsModelPrivate(const KConfigGroup &configGroup, const KConfigGroup &stateConfigGroup, ResultsModel *q)
228 : q(q)
229 , resultsModel(new RunnerResultsModel(configGroup, stateConfigGroup, q))
230 {
231 }
232
233 ResultsModel *q;
234
235 QPointer<KRunner::AbstractRunner> runner = nullptr;
236
237 RunnerResultsModel *const resultsModel;
238 SortProxyModel *const sortModel = new SortProxyModel(q);
239 CategoryDistributionProxyModel *const distributionModel = new CategoryDistributionProxyModel(q);
240 KDescendantsProxyModel *const flattenModel = new KDescendantsProxyModel(q);
241 HideRootLevelProxyModel *const hideRootModel = new HideRootLevelProxyModel(q);
242 const KModelIndexProxyMapper mapper{q, resultsModel};
243};
244
245ResultsModel::ResultsModel(QObject *parent)
247{
248}
249ResultsModel::ResultsModel(const KConfigGroup &configGroup, const KConfigGroup &stateConfigGroup, QObject *parent)
250 : QSortFilterProxyModel(parent)
251 , d(new ResultsModelPrivate(configGroup, stateConfigGroup, this))
252{
253 connect(d->resultsModel, &RunnerResultsModel::queryStringChanged, this, &ResultsModel::queryStringChanged);
254 connect(runnerManager(), &RunnerManager::queryingChanged, this, &ResultsModel::queryingChanged);
255 connect(d->resultsModel, &RunnerResultsModel::queryStringChangeRequested, this, &ResultsModel::queryStringChangeRequested);
256 connect(d->resultsModel, &RunnerResultsModel::runnerManagerChanged, this, [this]() {
257 connect(runnerManager(), &RunnerManager::queryingChanged, this, &ResultsModel::queryingChanged);
258 });
259
260 // The matches for the old query string remain on display until the first set of matches arrive for the new query string.
261 // Therefore we must not update the query string inside RunnerResultsModel exactly when the query string changes, otherwise it would
262 // re-sort the old query string matches based on the new query string.
263 // 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.
264 connect(d->resultsModel, &RunnerResultsModel::matchesChanged, this, [this]() {
265 d->sortModel->setQueryString(queryString());
266 });
267
268 connect(d->distributionModel, &CategoryDistributionProxyModel::limitChanged, this, &ResultsModel::limitChanged);
269
270 // The data flows as follows:
271 // - RunnerResultsModel
272 // - SortProxyModel
273 // - CategoryDistributionProxyModel
274 // - KDescendantsProxyModel
275 // - HideRootLevelProxyModel
276
277 d->sortModel->setSourceModel(d->resultsModel);
278
279 d->distributionModel->setSourceModel(d->sortModel);
280
281 d->flattenModel->setSourceModel(d->distributionModel);
282
283 d->hideRootModel->setSourceModel(d->flattenModel);
284 d->hideRootModel->setTreeModel(d->resultsModel);
285
286 setSourceModel(d->hideRootModel);
287
288 // Initialize the runners, this will speed the first query up.
289 // While there were lots of optimizations, instantiating plugins, creating threads and AbstractRunner::init is still heavy work
290 QTimer::singleShot(0, this, [this]() {
291 runnerManager()->runners();
292 });
293}
294
295ResultsModel::~ResultsModel() = default;
296
298{
299 d->resultsModel->m_favoriteIds = ids;
300 Q_EMIT favoriteIdsChanged();
301}
302
303QStringList ResultsModel::favoriteIds() const
304{
305 return d->resultsModel->m_favoriteIds;
306}
307
309{
310 return d->resultsModel->queryString();
311}
312
313void ResultsModel::setQueryString(const QString &queryString)
314{
315 d->resultsModel->setQueryString(queryString, singleRunner());
316}
317
318int ResultsModel::limit() const
319{
320 return d->distributionModel->limit();
321}
322
323void ResultsModel::setLimit(int limit)
324{
325 d->distributionModel->setLimit(limit);
326}
327
328void ResultsModel::resetLimit()
329{
330 setLimit(0);
331}
332
333bool ResultsModel::querying() const
334{
335 return runnerManager()->querying();
336}
337
338QString ResultsModel::singleRunner() const
339{
340 return d->runner ? d->runner->id() : QString();
341}
342
343void ResultsModel::setSingleRunner(const QString &runnerId)
344{
345 if (runnerId == singleRunner()) {
346 return;
347 }
348 if (runnerId.isEmpty()) {
349 d->runner = nullptr;
350 } else {
351 d->runner = runnerManager()->runner(runnerId);
352 }
353 Q_EMIT singleRunnerChanged();
354}
355
356KPluginMetaData ResultsModel::singleRunnerMetaData() const
357{
358 return d->runner ? d->runner->metadata() : KPluginMetaData();
359}
360
361QHash<int, QByteArray> ResultsModel::roleNames() const
362{
363 auto names = QAbstractProxyModel::roleNames();
364 names[IdRole] = QByteArrayLiteral("matchId"); // "id" is QML-reserved
365 names[EnabledRole] = QByteArrayLiteral("enabled");
366 names[CategoryRole] = QByteArrayLiteral("category");
367 names[SubtextRole] = QByteArrayLiteral("subtext");
368 names[UrlsRole] = QByteArrayLiteral("urls");
369 names[ActionsRole] = QByteArrayLiteral("actions");
370 names[MultiLineRole] = QByteArrayLiteral("multiLine");
371 return names;
372}
373
375{
376 d->resultsModel->clear();
377}
378
380{
381 KModelIndexProxyMapper mapper(this, d->resultsModel);
382 const QModelIndex resultsIdx = mapper.mapLeftToRight(idx);
383 if (!resultsIdx.isValid()) {
384 return false;
385 }
386 return d->resultsModel->run(resultsIdx);
387}
388
389bool ResultsModel::runAction(const QModelIndex &idx, int actionNumber)
390{
391 KModelIndexProxyMapper mapper(this, d->resultsModel);
392 const QModelIndex resultsIdx = mapper.mapLeftToRight(idx);
393 if (!resultsIdx.isValid()) {
394 return false;
395 }
396 return d->resultsModel->runAction(resultsIdx, actionNumber);
397}
398
400{
401 if (auto resultIdx = d->mapper.mapLeftToRight(idx); resultIdx.isValid()) {
402 return runnerManager()->mimeDataForMatch(d->resultsModel->fetchMatch(resultIdx));
403 }
404 return nullptr;
405}
406
407KRunner::RunnerManager *ResultsModel::runnerManager() const
408{
409 return d->resultsModel->runnerManager();
410}
411
413{
414 const QModelIndex resultIdx = d->mapper.mapLeftToRight(idx);
415 return resultIdx.isValid() ? d->resultsModel->fetchMatch(resultIdx) : QueryMatch();
416}
417
419{
420 d->resultsModel->setRunnerManager(manager);
422}
423
424#include "moc_resultsmodel.cpp"
425#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.
void queryingChanged()
Emitted when the querying status has changed.
QAbstractItemModel(QObject *parent)
virtual QModelIndex parent(const QModelIndex &index) const const=0
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
QObject(QObject *parent)
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)
QSortFilterProxyModel(QObject *parent)
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-2025 The KDE developers.
Generated on Fri Apr 11 2025 11:58:55 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.