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 | // 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 | |
30 | KRunner::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 | |
36 | void 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 | |
143 | QString RunnerResultsModel::queryString() const |
144 | { |
145 | return m_queryString; |
146 | } |
147 | |
148 | void 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 | |
167 | bool RunnerResultsModel::querying() const |
168 | { |
169 | return m_querying; |
170 | } |
171 | |
172 | void RunnerResultsModel::setQuerying(bool querying) |
173 | { |
174 | if (m_querying != querying) { |
175 | m_querying = querying; |
176 | Q_EMIT queryingChanged(); |
177 | } |
178 | } |
179 | |
180 | void 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 | |
195 | bool 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 | |
204 | bool 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 | |
218 | int RunnerResultsModel::columnCount(const QModelIndex &parent) const |
219 | { |
220 | Q_UNUSED(parent); |
221 | return 1; |
222 | } |
223 | |
224 | int 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 | |
242 | QVariant 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 | |
349 | QModelIndex 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 | |
372 | QModelIndex 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 | |
381 | KRunner::RunnerManager *RunnerResultsModel::runnerManager() const |
382 | { |
383 | return m_manager; |
384 | } |
385 | |
386 | } |
387 | |
388 | #include "moc_runnerresultsmodel_p.cpp" |
389 | |