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

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