KRunner

runnerresultsmodel.cpp
1/*
2 * SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@broulik.de>
3 *
4 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5 *
6 */
7
8#include "runnerresultsmodel_p.h"
9
10#include <QSet>
11
12#include <KRunner/RunnerManager>
13
14#include "resultsmodel.h"
15
16namespace KRunner
17{
18RunnerResultsModel::RunnerResultsModel(const KConfigGroup &configGroup, const KConfigGroup &stateConfigGroup, QObject *parent)
19 : QAbstractItemModel(parent)
20 // Invalid groups are passed in to avoid unneeded overloads and such
21 , m_manager(configGroup.isValid() && stateConfigGroup.isValid() ? new RunnerManager(configGroup, stateConfigGroup, this) : new RunnerManager(this))
22{
23 connect(m_manager, &RunnerManager::matchesChanged, this, &RunnerResultsModel::onMatchesChanged);
24 connect(m_manager, &RunnerManager::queryFinished, this, [this] {
25 setQuerying(false);
26 });
27 connect(m_manager, &RunnerManager::requestUpdateQueryString, this, &RunnerResultsModel::queryStringChangeRequested);
28}
29
30KRunner::QueryMatch RunnerResultsModel::fetchMatch(const QModelIndex &idx) const
31{
32 const QString category = m_categories.value(int(idx.internalId() - 1));
33 return m_matches.value(category).value(idx.row());
34}
35
36void RunnerResultsModel::onMatchesChanged(const QList<KRunner::QueryMatch> &matches)
37{
38 // Build the list of new categories and matches
39 QSet<QString> newCategories;
40 // here we use QString as key since at this point we don't care about the order
41 // of categories but just what matches we have for each one.
42 // Below when we populate the actual m_matches we'll make sure to keep the order
43 // of existing categories to avoid pointless model changes.
44 QHash<QString /*category*/, QList<KRunner::QueryMatch>> newMatches;
45 for (const auto &match : matches) {
46 const QString category = match.matchCategory();
47 newCategories.insert(category);
48 newMatches[category].append(match);
49 }
50
51 // Get rid of all categories that are no longer present
52 auto it = m_categories.begin();
53 while (it != m_categories.end()) {
54 const int categoryNumber = int(std::distance(m_categories.begin(), it));
55
56 if (!newCategories.contains(*it)) {
57 beginRemoveRows(QModelIndex(), categoryNumber, categoryNumber);
58 m_matches.remove(*it);
59 it = m_categories.erase(it);
60 endRemoveRows();
61 } else {
62 ++it;
63 }
64 }
65
66 // Update the existing categories by adding/removing new/removed rows and
67 // updating changed ones
68 for (auto it = m_categories.constBegin(); it != m_categories.constEnd(); ++it) {
69 Q_ASSERT(newCategories.contains(*it));
70
71 const int categoryNumber = int(std::distance(m_categories.constBegin(), it));
72 const QModelIndex categoryIdx = index(categoryNumber, 0);
73
74 // don't use operator[] as to not insert an empty list
75 // TODO why? shouldn't m_categories and m_matches be in sync?
76 auto oldCategoryIt = m_matches.find(*it);
77 Q_ASSERT(oldCategoryIt != m_matches.end());
78
79 auto &oldMatchesInCategory = *oldCategoryIt;
80 const auto newMatchesInCategory = newMatches.value(*it);
81
82 Q_ASSERT(!oldMatchesInCategory.isEmpty());
83 Q_ASSERT(!newMatches.isEmpty());
84
85 // Emit a change for all existing matches if any of them changed
86 // TODO only emit a change for the ones that changed
87 bool emitDataChanged = false;
88
89 const int oldCount = oldMatchesInCategory.count();
90 const int newCount = newMatchesInCategory.count();
91
92 const int countCeiling = qMin(oldCount, newCount);
93
94 for (int i = 0; i < countCeiling; ++i) {
95 auto &oldMatch = oldMatchesInCategory[i];
96 if (oldMatch != newMatchesInCategory.at(i)) {
97 oldMatch = newMatchesInCategory.at(i);
98 emitDataChanged = true;
99 }
100 }
101
102 // Now that the source data has been updated, emit the data changes we noted down earlier
103 if (emitDataChanged) {
104 Q_EMIT dataChanged(index(0, 0, categoryIdx), index(countCeiling - 1, 0, categoryIdx));
105 }
106
107 // Signal insertions for any new items
108 if (newCount > oldCount) {
109 beginInsertRows(categoryIdx, oldCount, newCount - 1);
110 oldMatchesInCategory = newMatchesInCategory;
111 endInsertRows();
112 } else if (newCount < oldCount) {
113 beginRemoveRows(categoryIdx, newCount, oldCount - 1);
114 oldMatchesInCategory = newMatchesInCategory;
115 endRemoveRows();
116 }
117
118 // Remove it from the "new" categories so in the next step we can add all genuinely new categories in one go
119 newCategories.remove(*it);
120 }
121
122 // Finally add all the new categories
123 if (!newCategories.isEmpty()) {
124 beginInsertRows(QModelIndex(), m_categories.count(), m_categories.count() + newCategories.count() - 1);
125
126 for (const QString &newCategory : newCategories) {
127 const auto matchesInNewCategory = newMatches.value(newCategory);
128
129 m_matches[newCategory] = matchesInNewCategory;
130 m_categories.append(newCategory);
131 }
132
133 endInsertRows();
134 }
135
136 Q_ASSERT(m_categories.count() == m_matches.count());
137
138 m_hasMatches = !m_matches.isEmpty();
139
140 Q_EMIT matchesChanged();
141}
142
143QString RunnerResultsModel::queryString() const
144{
145 return m_queryString;
146}
147
148void RunnerResultsModel::setQueryString(const QString &queryString, const QString &runner)
149{
150 // If our query and runner are the same we don't need to query again
151 if (m_queryString.trimmed() == queryString.trimmed() && m_prevRunner == runner) {
152 return;
153 }
154
155 m_prevRunner = runner;
156 m_queryString = queryString;
157 m_hasMatches = false;
158 if (queryString.isEmpty()) {
159 clear();
160 } else if (!queryString.trimmed().isEmpty()) {
161 m_manager->launchQuery(queryString, runner);
162 setQuerying(true);
163 }
164 Q_EMIT queryStringChanged(queryString); // NOLINT(readability-misleading-indentation)
165}
166
167bool RunnerResultsModel::querying() const
168{
169 return m_querying;
170}
171
172void RunnerResultsModel::setQuerying(bool querying)
173{
174 if (m_querying != querying) {
175 m_querying = querying;
176 Q_EMIT queryingChanged();
177 }
178}
179
180void RunnerResultsModel::clear()
181{
182 m_manager->reset();
183 m_manager->matchSessionComplete();
184
185 setQuerying(false);
186 // When our session is over, the term is also no longer relevant
187 // If the same term is used again, the RunnerManager should be asked again
188 if (!m_queryString.isEmpty()) {
189 m_queryString.clear();
190 Q_EMIT queryStringChanged(m_queryString);
191 }
192
193 beginResetModel();
194 m_categories.clear();
195 m_matches.clear();
196 endResetModel();
197
198 m_hasMatches = false;
199}
200
201bool RunnerResultsModel::run(const QModelIndex &idx)
202{
203 KRunner::QueryMatch match = fetchMatch(idx);
204 if (match.isValid() && match.isEnabled()) {
205 return m_manager->run(match);
206 }
207 return false;
208}
209
210bool RunnerResultsModel::runAction(const QModelIndex &idx, int actionNumber)
211{
212 KRunner::QueryMatch match = fetchMatch(idx);
213 if (!match.isValid() || !match.isEnabled()) {
214 return false;
215 }
216
217 if (actionNumber < 0 || actionNumber >= match.actions().count()) {
218 return false;
219 }
220
221 return m_manager->run(match, match.actions().at(actionNumber));
222}
223
224int RunnerResultsModel::columnCount(const QModelIndex &parent) const
225{
226 Q_UNUSED(parent);
227 return 1;
228}
229
230int RunnerResultsModel::rowCount(const QModelIndex &parent) const
231{
232 if (parent.column() > 0) {
233 return 0;
234 }
235
236 if (!parent.isValid()) { // root level
237 return m_categories.count();
238 }
239
240 if (parent.internalId()) {
241 return 0;
242 }
243
244 const QString category = m_categories.value(parent.row());
245 return m_matches.value(category).count();
246}
247
248QVariant RunnerResultsModel::data(const QModelIndex &index, int role) const
249{
250 if (!index.isValid()) {
251 return QVariant();
252 }
253
254 if (index.internalId()) { // runner match
255 if (int(index.internalId() - 1) >= m_categories.count()) {
256 return QVariant();
257 }
258
259 KRunner::QueryMatch match = fetchMatch(index);
260 if (!match.isValid()) {
261 return QVariant();
262 }
263
264 switch (role) {
265 case Qt::DisplayRole:
266 return match.text();
268 if (!match.iconName().isEmpty()) {
269 return match.iconName();
270 }
271 return match.icon();
272 case ResultsModel::CategoryRelevanceRole:
273 return match.categoryRelevance();
274 case ResultsModel::RelevanceRole:
275 return match.relevance();
276 case ResultsModel::IdRole:
277 return match.id();
278 case ResultsModel::EnabledRole:
279 return match.isEnabled();
280 case ResultsModel::CategoryRole:
281 return match.matchCategory();
282 case ResultsModel::SubtextRole:
283 return match.subtext();
284 case ResultsModel::UrlsRole:
285 return QVariant::fromValue(match.urls());
286 case ResultsModel::MultiLineRole:
287 return match.isMultiLine();
288 case ResultsModel::ActionsRole: {
289 const auto actions = match.actions();
290 QVariantList actionsList;
291 actionsList.reserve(actions.size());
292
293 for (const KRunner::Action &action : actions) {
294 actionsList.append(QVariant::fromValue(action));
295 }
296
297 return actionsList;
298 }
299 case ResultsModel::QueryMatchRole:
300 return QVariant::fromValue(match);
301 }
302
303 return QVariant();
304 }
305
306 // category
307 if (index.row() >= m_categories.count()) {
308 return QVariant();
309 }
310
311 switch (role) {
312 case Qt::DisplayRole:
313 return m_categories.at(index.row());
314
315 case ResultsModel::FavoriteIndexRole: {
316 for (int i = 0; i < rowCount(index); ++i) {
317 auto match = this->index(i, 0, index).data(ResultsModel::QueryMatchRole).value<KRunner::QueryMatch>();
318 if (match.isValid()) {
319 const QString id = match.runner()->id();
320 int idx = m_favoriteIds.indexOf(id);
321 return idx;
322 }
323 }
324 // Any match that is not a favorite will have a greater index than an actual favorite
325 return m_favoriteIds.size();
326 }
327 case ResultsModel::FavoriteCountRole:
328 return m_favoriteIds.size();
329 // Returns the highest type/role within the group
330 case ResultsModel::CategoryRelevanceRole: {
331 int highestType = 0;
332 for (int i = 0; i < rowCount(index); ++i) {
333 const int type = this->index(i, 0, index).data(ResultsModel::CategoryRelevanceRole).toInt();
334 if (type > highestType) {
335 highestType = type;
336 }
337 }
338 return highestType;
339 }
340 case ResultsModel::RelevanceRole: {
341 qreal highestRelevance = 0.0;
342 for (int i = 0; i < rowCount(index); ++i) {
343 const qreal relevance = this->index(i, 0, index).data(ResultsModel::RelevanceRole).toReal();
344 if (relevance > highestRelevance) {
345 highestRelevance = relevance;
346 }
347 }
348 return highestRelevance;
349 }
350 }
351
352 return QVariant();
353}
354
355QModelIndex RunnerResultsModel::index(int row, int column, const QModelIndex &parent) const
356{
357 if (row < 0 || column != 0) {
358 return QModelIndex();
359 }
360
361 if (parent.isValid()) {
362 const QString category = m_categories.value(parent.row());
363 const auto matches = m_matches.value(category);
364 if (row < matches.count()) {
365 return createIndex(row, column, int(parent.row() + 1));
366 }
367
368 return QModelIndex();
369 }
370
371 if (row < m_categories.count()) {
372 return createIndex(row, column, nullptr);
373 }
374
375 return QModelIndex();
376}
377
378QModelIndex RunnerResultsModel::parent(const QModelIndex &child) const
379{
380 if (child.internalId()) {
381 return createIndex(int(child.internalId() - 1), 0, nullptr);
382 }
383
384 return QModelIndex();
385}
386
387KRunner::RunnerManager *RunnerResultsModel::runnerManager() const
388{
389 return m_manager;
390}
391
392}
393
394#include "moc_runnerresultsmodel_p.cpp"
This class represents an action that will be shown next to a match.
Definition action.h:23
A match returned by an AbstractRunner in response to a given RunnerContext.
Definition querymatch.h:32
The RunnerManager class decides what installed runners are runnable, and their ratings.
Type type(const QSqlDatabase &db)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
bool isValid(QStringView ifopt)
QAction * clear(const QObject *recvr, const char *slot, QObject *parent)
Category category(StandardShortcut id)
qsizetype count() const const
T value(qsizetype i) const const
int column() const const
QVariant data(int role) const const
quintptr internalId() const const
bool isValid() const const
int row() const const
bool contains(const QSet< T > &other) const const
qsizetype count() const const
iterator insert(const T &value)
bool isEmpty() const const
bool remove(const T &value)
bool isEmpty() const const
QString trimmed() const const
DisplayRole
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QVariant fromValue(T &&value)
int toInt(bool *ok) const const
qreal toReal(bool *ok) const const
T value() 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.