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(sender: m_manager, signal: &RunnerManager::matchesChanged, context: this, slot: &RunnerResultsModel::onMatchesChanged);
24 connect(sender: m_manager, signal: &RunnerManager::queryFinished, context: this, slot: [this] {
25 setQuerying(false);
26 });
27 connect(sender: m_manager, signal: &RunnerManager::requestUpdateQueryString, context: this, slot: &RunnerResultsModel::queryStringChangeRequested);
28}
29
30KRunner::QueryMatch RunnerResultsModel::fetchMatch(const QModelIndex &idx) const
31{
32 const QString category = m_categories.value(i: int(idx.internalId() - 1));
33 return m_matches.value(key: category).value(i: 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(value: category);
48 newMatches[category].append(t: 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(first: m_categories.begin(), last: it));
55
56 if (!newCategories.contains(value: *it)) {
57 beginRemoveRows(parent: QModelIndex(), first: categoryNumber, last: categoryNumber);
58 m_matches.remove(key: *it);
59 it = m_categories.erase(pos: 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(first: m_categories.constBegin(), last: it));
72 const QModelIndex categoryIdx = index(row: categoryNumber, column: 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(key: *it);
77 Q_ASSERT(oldCategoryIt != m_matches.end());
78
79 auto &oldMatchesInCategory = *oldCategoryIt;
80 const auto newMatchesInCategory = newMatches.value(key: *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(a: oldCount, b: 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(topLeft: index(row: 0, column: 0, parent: categoryIdx), bottomRight: index(row: countCeiling - 1, column: 0, parent: categoryIdx));
105 }
106
107 // Signal insertions for any new items
108 if (newCount > oldCount) {
109 beginInsertRows(parent: categoryIdx, first: oldCount, last: newCount - 1);
110 oldMatchesInCategory = newMatchesInCategory;
111 endInsertRows();
112 } else if (newCount < oldCount) {
113 beginRemoveRows(parent: categoryIdx, first: newCount, last: 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(value: *it);
120 }
121
122 // Finally add all the new categories
123 if (!newCategories.isEmpty()) {
124 beginInsertRows(parent: QModelIndex(), first: m_categories.count(), last: m_categories.count() + newCategories.count() - 1);
125
126 for (const QString &newCategory : newCategories) {
127 const auto matchesInNewCategory = newMatches.value(key: newCategory);
128
129 m_matches[newCategory] = matchesInNewCategory;
130 m_categories.append(t: 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(term: queryString, runnerId: 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
187 beginResetModel();
188 m_categories.clear();
189 m_matches.clear();
190 endResetModel();
191
192 m_hasMatches = false;
193}
194
195bool RunnerResultsModel::run(const QModelIndex &idx)
196{
197 KRunner::QueryMatch match = fetchMatch(idx);
198 if (match.isValid() && match.isEnabled()) {
199 return m_manager->run(match);
200 }
201 return false;
202}
203
204bool RunnerResultsModel::runAction(const QModelIndex &idx, int actionNumber)
205{
206 KRunner::QueryMatch match = fetchMatch(idx);
207 if (!match.isValid() || !match.isEnabled()) {
208 return false;
209 }
210
211 if (actionNumber < 0 || actionNumber >= match.actions().count()) {
212 return false;
213 }
214
215 return m_manager->run(match, action: match.actions().at(i: actionNumber));
216}
217
218int RunnerResultsModel::columnCount(const QModelIndex &parent) const
219{
220 Q_UNUSED(parent);
221 return 1;
222}
223
224int RunnerResultsModel::rowCount(const QModelIndex &parent) const
225{
226 if (parent.column() > 0) {
227 return 0;
228 }
229
230 if (!parent.isValid()) { // root level
231 return m_categories.count();
232 }
233
234 if (parent.internalId()) {
235 return 0;
236 }
237
238 const QString category = m_categories.value(i: parent.row());
239 return m_matches.value(key: category).count();
240}
241
242QVariant RunnerResultsModel::data(const QModelIndex &index, int role) const
243{
244 if (!index.isValid()) {
245 return QVariant();
246 }
247
248 if (index.internalId()) { // runner match
249 if (int(index.internalId() - 1) >= m_categories.count()) {
250 return QVariant();
251 }
252
253 KRunner::QueryMatch match = fetchMatch(idx: index);
254 if (!match.isValid()) {
255 return QVariant();
256 }
257
258 switch (role) {
259 case Qt::DisplayRole:
260 return match.text();
261 case Qt::DecorationRole:
262 if (!match.iconName().isEmpty()) {
263 return match.iconName();
264 }
265 return match.icon();
266 case ResultsModel::CategoryRelevanceRole:
267 return match.categoryRelevance();
268 case ResultsModel::RelevanceRole:
269 return match.relevance();
270 case ResultsModel::IdRole:
271 return match.id();
272 case ResultsModel::EnabledRole:
273 return match.isEnabled();
274 case ResultsModel::CategoryRole:
275 return match.matchCategory();
276 case ResultsModel::SubtextRole:
277 return match.subtext();
278 case ResultsModel::UrlsRole:
279 return QVariant::fromValue(value: match.urls());
280 case ResultsModel::MultiLineRole:
281 return match.isMultiLine();
282 case ResultsModel::ActionsRole: {
283 const auto actions = match.actions();
284 QVariantList actionsList;
285 actionsList.reserve(asize: actions.size());
286
287 for (const KRunner::Action &action : actions) {
288 actionsList.append(t: QVariant::fromValue(value: action));
289 }
290
291 return actionsList;
292 }
293 case ResultsModel::QueryMatchRole:
294 return QVariant::fromValue(value: match);
295 }
296
297 return QVariant();
298 }
299
300 // category
301 if (index.row() >= m_categories.count()) {
302 return QVariant();
303 }
304
305 switch (role) {
306 case Qt::DisplayRole:
307 return m_categories.at(i: index.row());
308
309 case ResultsModel::FavoriteIndexRole: {
310 for (int i = 0; i < rowCount(parent: index); ++i) {
311 auto match = this->index(row: i, column: 0, parent: index).data(arole: ResultsModel::QueryMatchRole).value<KRunner::QueryMatch>();
312 if (match.isValid()) {
313 const QString id = match.runner()->id();
314 int idx = m_favoriteIds.indexOf(str: id);
315 return idx;
316 }
317 }
318 // Any match that is not a favorite will have a greater index than an actual favorite
319 return m_favoriteIds.size();
320 }
321 case ResultsModel::FavoriteCountRole:
322 return m_favoriteIds.size();
323 // Returns the highest type/role within the group
324 case ResultsModel::CategoryRelevanceRole: {
325 int highestType = 0;
326 for (int i = 0; i < rowCount(parent: index); ++i) {
327 const int type = this->index(row: i, column: 0, parent: index).data(arole: ResultsModel::CategoryRelevanceRole).toInt();
328 if (type > highestType) {
329 highestType = type;
330 }
331 }
332 return highestType;
333 }
334 case ResultsModel::RelevanceRole: {
335 qreal highestRelevance = 0.0;
336 for (int i = 0; i < rowCount(parent: index); ++i) {
337 const qreal relevance = this->index(row: i, column: 0, parent: index).data(arole: ResultsModel::RelevanceRole).toReal();
338 if (relevance > highestRelevance) {
339 highestRelevance = relevance;
340 }
341 }
342 return highestRelevance;
343 }
344 }
345
346 return QVariant();
347}
348
349QModelIndex RunnerResultsModel::index(int row, int column, const QModelIndex &parent) const
350{
351 if (row < 0 || column != 0) {
352 return QModelIndex();
353 }
354
355 if (parent.isValid()) {
356 const QString category = m_categories.value(i: parent.row());
357 const auto matches = m_matches.value(key: category);
358 if (row < matches.count()) {
359 return createIndex(arow: row, acolumn: column, aid: int(parent.row() + 1));
360 }
361
362 return QModelIndex();
363 }
364
365 if (row < m_categories.count()) {
366 return createIndex(arow: row, acolumn: column, adata: nullptr);
367 }
368
369 return QModelIndex();
370}
371
372QModelIndex RunnerResultsModel::parent(const QModelIndex &child) const
373{
374 if (child.internalId()) {
375 return createIndex(arow: int(child.internalId() - 1), acolumn: 0, adata: nullptr);
376 }
377
378 return QModelIndex();
379}
380
381KRunner::RunnerManager *RunnerResultsModel::runnerManager() const
382{
383 return m_manager;
384}
385
386}
387
388#include "moc_runnerresultsmodel_p.cpp"
389

source code of krunner/src/model/runnerresultsmodel.cpp