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 | |
16 | namespace KRunner |
17 | { |
18 | RunnerResultsModel::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 | |
25 | KRunner::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 | |
31 | void 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 | |
138 | QString RunnerResultsModel::queryString() const |
139 | { |
140 | return m_queryString; |
141 | } |
142 | |
143 | void 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 | |
161 | void 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 | |
181 | bool 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 | |
190 | bool 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 | |
204 | int RunnerResultsModel::columnCount(const QModelIndex &parent) const |
205 | { |
206 | Q_UNUSED(parent); |
207 | return 1; |
208 | } |
209 | |
210 | int 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 | |
228 | QVariant 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 | |
333 | QModelIndex 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 | |
356 | QModelIndex 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 | |
365 | KRunner::RunnerManager *RunnerResultsModel::runnerManager() const |
366 | { |
367 | return m_manager; |
368 | } |
369 | |
370 | void 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 | |