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{
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(int(idx.internalId() - 1));
28 return m_matches.value(category).value(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(category);
43 newMatches[category].append(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(m_categories.begin(), it));
50
51 if (!newCategories.contains(*it)) {
52 beginRemoveRows(QModelIndex(), categoryNumber, categoryNumber);
53 m_matches.remove(*it);
54 it = m_categories.erase(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(m_categories.constBegin(), it));
67 const QModelIndex categoryIdx = index(categoryNumber, 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(*it);
72 Q_ASSERT(oldCategoryIt != m_matches.end());
73
74 auto &oldMatchesInCategory = *oldCategoryIt;
75 const auto newMatchesInCategory = newMatches.value(*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(oldCount, 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(index(0, 0, categoryIdx), index(countCeiling - 1, 0, categoryIdx));
100 }
101
102 // Signal insertions for any new items
103 if (newCount > oldCount) {
104 beginInsertRows(categoryIdx, oldCount, newCount - 1);
105 oldMatchesInCategory = newMatchesInCategory;
106 endInsertRows();
107 } else if (newCount < oldCount) {
108 beginRemoveRows(categoryIdx, newCount, 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(*it);
115 }
116
117 // Finally add all the new categories
118 if (!newCategories.isEmpty()) {
119 beginInsertRows(QModelIndex(), m_categories.count(), m_categories.count() + newCategories.count() - 1);
120
121 for (const QString &newCategory : newCategories) {
122 const auto matchesInNewCategory = newMatches.value(newCategory);
123
124 m_matches[newCategory] = matchesInNewCategory;
125 m_categories.append(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(queryString, 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(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, match.actions().at(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(parent.row());
225 return m_matches.value(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(index);
240 if (!match.isValid()) {
241 return QVariant();
242 }
243
244 switch (role) {
245 case Qt::DisplayRole:
246 return match.text();
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(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(actions.size());
272
273 for (const KRunner::Action &action : actions) {
274 actionsList.append(QVariant::fromValue(action));
275 }
276
277 return actionsList;
278 }
279 case ResultsModel::QueryMatchRole:
280 return QVariant::fromValue(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(index.row());
294
295 case ResultsModel::FavoriteIndexRole: {
296 for (int i = 0; i < rowCount(index); ++i) {
297 auto match = this->index(i, 0, index).data(ResultsModel::QueryMatchRole).value<KRunner::QueryMatch>();
298 if (match.isValid()) {
299 const QString id = match.runner()->id();
300 int idx = m_favoriteIds.indexOf(id);
301 return 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 case ResultsModel::FavoriteCountRole:
308 return m_favoriteIds.size();
309 // Returns the highest type/role within the group
310 case ResultsModel::CategoryRelevanceRole: {
311 int highestType = 0;
312 for (int i = 0; i < rowCount(index); ++i) {
313 const int type = this->index(i, 0, index).data(ResultsModel::CategoryRelevanceRole).toInt();
314 if (type > highestType) {
315 highestType = type;
316 }
317 }
318 return highestType;
319 }
320 case ResultsModel::RelevanceRole: {
321 qreal highestRelevance = 0.0;
322 for (int i = 0; i < rowCount(index); ++i) {
323 const qreal relevance = this->index(i, 0, index).data(ResultsModel::RelevanceRole).toReal();
324 if (relevance > highestRelevance) {
325 highestRelevance = relevance;
326 }
327 }
328 return highestRelevance;
329 }
330 }
331
332 return QVariant();
333}
334
335QModelIndex RunnerResultsModel::index(int row, int column, const QModelIndex &parent) const
336{
337 if (row < 0 || column != 0) {
338 return QModelIndex();
339 }
340
341 if (parent.isValid()) {
342 const QString category = m_categories.value(parent.row());
343 const auto matches = m_matches.value(category);
344 if (row < matches.count()) {
345 return createIndex(row, column, int(parent.row() + 1));
346 }
347
348 return QModelIndex();
349 }
350
351 if (row < m_categories.count()) {
352 return createIndex(row, column, nullptr);
353 }
354
355 return QModelIndex();
356}
357
358QModelIndex RunnerResultsModel::parent(const QModelIndex &child) const
359{
360 if (child.internalId()) {
361 return createIndex(int(child.internalId() - 1), 0, nullptr);
362 }
363
364 return QModelIndex();
365}
366
367KRunner::RunnerManager *RunnerResultsModel::runnerManager() const
368{
369 return m_manager;
370}
371
372void RunnerResultsModel::setRunnerManager(KRunner::RunnerManager *manager)
373{
374 disconnect(m_manager);
375 m_manager = manager;
376
377 connect(m_manager, &RunnerManager::matchesChanged, this, &RunnerResultsModel::onMatchesChanged);
378 connect(m_manager, &RunnerManager::requestUpdateQueryString, this, &RunnerResultsModel::queryStringChangeRequested);
379 Q_EMIT runnerManagerChanged();
380}
381}
382
383#include "moc_runnerresultsmodel_p.cpp"
bool isValid() const
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)
QAction * clear(const QObject *recvr, const char *slot, QObject *parent)
Category category(StandardShortcut id)
iterator begin()
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-2025 The KDE developers.
Generated on Fri Jan 31 2025 12:10:47 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.