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

KDE's Doxygen guidelines are available online.