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.
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
KGuiItem clear()
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-2024 The KDE developers.
Generated on Fri Nov 29 2024 11:47:31 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.