Warning, file /frameworks/krunner/src/model/runnerresultsmodel.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 * SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@broulik.de> 0003 * 0004 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0005 * 0006 */ 0007 0008 #include "runnerresultsmodel_p.h" 0009 0010 #include <QSet> 0011 0012 #include <KRunner/RunnerManager> 0013 0014 #include "resultsmodel.h" 0015 0016 namespace KRunner 0017 { 0018 RunnerResultsModel::RunnerResultsModel(const KConfigGroup &configGroup, KConfigGroup stateConfigGroup, QObject *parent) 0019 : QAbstractItemModel(parent) 0020 // Invalid groups are passed in to avoid unneeded overloads and such 0021 , m_manager(configGroup.isValid() && stateConfigGroup.isValid() ? new RunnerManager(configGroup, stateConfigGroup, this) : new RunnerManager(this)) 0022 { 0023 connect(m_manager, &RunnerManager::matchesChanged, this, &RunnerResultsModel::onMatchesChanged); 0024 connect(m_manager, &RunnerManager::queryFinished, this, [this] { 0025 setQuerying(false); 0026 }); 0027 connect(m_manager, &RunnerManager::requestUpdateQueryString, this, &RunnerResultsModel::queryStringChangeRequested); 0028 } 0029 0030 KRunner::QueryMatch RunnerResultsModel::fetchMatch(const QModelIndex &idx) const 0031 { 0032 const QString category = m_categories.value(int(idx.internalId() - 1)); 0033 return m_matches.value(category).value(idx.row()); 0034 } 0035 0036 void RunnerResultsModel::onMatchesChanged(const QList<KRunner::QueryMatch> &matches) 0037 { 0038 // Build the list of new categories and matches 0039 QSet<QString> newCategories; 0040 // here we use QString as key since at this point we don't care about the order 0041 // of categories but just what matches we have for each one. 0042 // Below when we populate the actual m_matches we'll make sure to keep the order 0043 // of existing categories to avoid pointless model changes. 0044 QHash<QString /*category*/, QList<KRunner::QueryMatch>> newMatches; 0045 for (const auto &match : matches) { 0046 const QString category = match.matchCategory(); 0047 newCategories.insert(category); 0048 newMatches[category].append(match); 0049 } 0050 0051 // Get rid of all categories that are no longer present 0052 auto it = m_categories.begin(); 0053 while (it != m_categories.end()) { 0054 const int categoryNumber = int(std::distance(m_categories.begin(), it)); 0055 0056 if (!newCategories.contains(*it)) { 0057 beginRemoveRows(QModelIndex(), categoryNumber, categoryNumber); 0058 m_matches.remove(*it); 0059 it = m_categories.erase(it); 0060 endRemoveRows(); 0061 } else { 0062 ++it; 0063 } 0064 } 0065 0066 // Update the existing categories by adding/removing new/removed rows and 0067 // updating changed ones 0068 for (auto it = m_categories.constBegin(); it != m_categories.constEnd(); ++it) { 0069 Q_ASSERT(newCategories.contains(*it)); 0070 0071 const int categoryNumber = int(std::distance(m_categories.constBegin(), it)); 0072 const QModelIndex categoryIdx = index(categoryNumber, 0); 0073 0074 // don't use operator[] as to not insert an empty list 0075 // TODO why? shouldn't m_categories and m_matches be in sync? 0076 auto oldCategoryIt = m_matches.find(*it); 0077 Q_ASSERT(oldCategoryIt != m_matches.end()); 0078 0079 auto &oldMatchesInCategory = *oldCategoryIt; 0080 const auto newMatchesInCategory = newMatches.value(*it); 0081 0082 Q_ASSERT(!oldMatchesInCategory.isEmpty()); 0083 Q_ASSERT(!newMatches.isEmpty()); 0084 0085 // Emit a change for all existing matches if any of them changed 0086 // TODO only emit a change for the ones that changed 0087 bool emitDataChanged = false; 0088 0089 const int oldCount = oldMatchesInCategory.count(); 0090 const int newCount = newMatchesInCategory.count(); 0091 0092 const int countCeiling = qMin(oldCount, newCount); 0093 0094 for (int i = 0; i < countCeiling; ++i) { 0095 auto &oldMatch = oldMatchesInCategory[i]; 0096 if (oldMatch != newMatchesInCategory.at(i)) { 0097 oldMatch = newMatchesInCategory.at(i); 0098 emitDataChanged = true; 0099 } 0100 } 0101 0102 // Now that the source data has been updated, emit the data changes we noted down earlier 0103 if (emitDataChanged) { 0104 Q_EMIT dataChanged(index(0, 0, categoryIdx), index(countCeiling - 1, 0, categoryIdx)); 0105 } 0106 0107 // Signal insertions for any new items 0108 if (newCount > oldCount) { 0109 beginInsertRows(categoryIdx, oldCount, newCount - 1); 0110 oldMatchesInCategory = newMatchesInCategory; 0111 endInsertRows(); 0112 } else if (newCount < oldCount) { 0113 beginRemoveRows(categoryIdx, newCount, oldCount - 1); 0114 oldMatchesInCategory = newMatchesInCategory; 0115 endRemoveRows(); 0116 } 0117 0118 // Remove it from the "new" categories so in the next step we can add all genuinely new categories in one go 0119 newCategories.remove(*it); 0120 } 0121 0122 // Finally add all the new categories 0123 if (!newCategories.isEmpty()) { 0124 beginInsertRows(QModelIndex(), m_categories.count(), m_categories.count() + newCategories.count() - 1); 0125 0126 for (const QString &newCategory : newCategories) { 0127 const auto matchesInNewCategory = newMatches.value(newCategory); 0128 0129 m_matches[newCategory] = matchesInNewCategory; 0130 m_categories.append(newCategory); 0131 } 0132 0133 endInsertRows(); 0134 } 0135 0136 Q_ASSERT(m_categories.count() == m_matches.count()); 0137 0138 m_hasMatches = !m_matches.isEmpty(); 0139 0140 Q_EMIT matchesChanged(); 0141 } 0142 0143 QString RunnerResultsModel::queryString() const 0144 { 0145 return m_queryString; 0146 } 0147 0148 void RunnerResultsModel::setQueryString(const QString &queryString, const QString &runner) 0149 { 0150 // If our query and runner are the same we don't need to query again 0151 if (m_queryString.trimmed() == queryString.trimmed() && m_prevRunner == runner) { 0152 return; 0153 } 0154 0155 m_prevRunner = runner; 0156 m_queryString = queryString; 0157 m_hasMatches = false; 0158 if (queryString.isEmpty()) { 0159 clear(); 0160 } else if (!queryString.trimmed().isEmpty()) { 0161 m_manager->launchQuery(queryString, runner); 0162 setQuerying(true); 0163 } 0164 Q_EMIT queryStringChanged(queryString); // NOLINT(readability-misleading-indentation) 0165 } 0166 0167 bool RunnerResultsModel::querying() const 0168 { 0169 return m_querying; 0170 } 0171 0172 void RunnerResultsModel::setQuerying(bool querying) 0173 { 0174 if (m_querying != querying) { 0175 m_querying = querying; 0176 Q_EMIT queryingChanged(); 0177 } 0178 } 0179 0180 void RunnerResultsModel::clear() 0181 { 0182 m_manager->reset(); 0183 m_manager->matchSessionComplete(); 0184 0185 setQuerying(false); 0186 0187 beginResetModel(); 0188 m_categories.clear(); 0189 m_matches.clear(); 0190 endResetModel(); 0191 0192 m_hasMatches = false; 0193 } 0194 0195 bool RunnerResultsModel::run(const QModelIndex &idx) 0196 { 0197 KRunner::QueryMatch match = fetchMatch(idx); 0198 if (match.isValid() && match.isEnabled()) { 0199 return m_manager->run(match); 0200 } 0201 return false; 0202 } 0203 0204 bool RunnerResultsModel::runAction(const QModelIndex &idx, int actionNumber) 0205 { 0206 KRunner::QueryMatch match = fetchMatch(idx); 0207 if (!match.isValid() || !match.isEnabled()) { 0208 return false; 0209 } 0210 0211 if (actionNumber < 0 || actionNumber >= match.actions().count()) { 0212 return false; 0213 } 0214 0215 return m_manager->run(match, match.actions().at(actionNumber)); 0216 } 0217 0218 int RunnerResultsModel::columnCount(const QModelIndex &parent) const 0219 { 0220 Q_UNUSED(parent); 0221 return 1; 0222 } 0223 0224 int RunnerResultsModel::rowCount(const QModelIndex &parent) const 0225 { 0226 if (parent.column() > 0) { 0227 return 0; 0228 } 0229 0230 if (!parent.isValid()) { // root level 0231 return m_categories.count(); 0232 } 0233 0234 if (parent.internalId()) { 0235 return 0; 0236 } 0237 0238 const QString category = m_categories.value(parent.row()); 0239 return m_matches.value(category).count(); 0240 } 0241 0242 QVariant RunnerResultsModel::data(const QModelIndex &index, int role) const 0243 { 0244 if (!index.isValid()) { 0245 return QVariant(); 0246 } 0247 0248 if (index.internalId()) { // runner match 0249 if (int(index.internalId() - 1) >= m_categories.count()) { 0250 return QVariant(); 0251 } 0252 0253 KRunner::QueryMatch match = fetchMatch(index); 0254 if (!match.isValid()) { 0255 return QVariant(); 0256 } 0257 0258 switch (role) { 0259 case Qt::DisplayRole: 0260 return match.text(); 0261 case Qt::DecorationRole: 0262 if (!match.iconName().isEmpty()) { 0263 return match.iconName(); 0264 } 0265 return match.icon(); 0266 case ResultsModel::CategoryRelevanceRole: 0267 return match.categoryRelevance(); 0268 case ResultsModel::RelevanceRole: 0269 return match.relevance(); 0270 case ResultsModel::IdRole: 0271 return match.id(); 0272 case ResultsModel::EnabledRole: 0273 return match.isEnabled(); 0274 case ResultsModel::CategoryRole: 0275 return match.matchCategory(); 0276 case ResultsModel::SubtextRole: 0277 return match.subtext(); 0278 case ResultsModel::UrlsRole: 0279 return QVariant::fromValue(match.urls()); 0280 case ResultsModel::MultiLineRole: 0281 return match.isMultiLine(); 0282 case ResultsModel::ActionsRole: { 0283 const auto actions = match.actions(); 0284 QVariantList actionsList; 0285 actionsList.reserve(actions.size()); 0286 0287 for (const KRunner::Action &action : actions) { 0288 actionsList.append(QVariant::fromValue(action)); 0289 } 0290 0291 return actionsList; 0292 } 0293 case ResultsModel::QueryMatchRole: 0294 return QVariant::fromValue(match); 0295 } 0296 0297 return QVariant(); 0298 } 0299 0300 // category 0301 if (index.row() >= m_categories.count()) { 0302 return QVariant(); 0303 } 0304 0305 switch (role) { 0306 case Qt::DisplayRole: 0307 return m_categories.at(index.row()); 0308 0309 case ResultsModel::FavoriteIndexRole: { 0310 for (int i = 0; i < rowCount(index); ++i) { 0311 auto match = this->index(i, 0, index).data(ResultsModel::QueryMatchRole).value<KRunner::QueryMatch>(); 0312 if (match.isValid()) { 0313 const QString id = match.runner()->id(); 0314 int idx = m_favoriteIds.indexOf(id); 0315 return idx == -1 ? m_favoriteIds.size() : idx; 0316 } 0317 } 0318 // Any match that is not a favorite will have a greater index than an actual favorite 0319 return m_favoriteIds.size(); 0320 } 0321 // Returns the highest type/role within the group 0322 case ResultsModel::CategoryRelevanceRole: { 0323 int highestType = 0; 0324 for (int i = 0; i < rowCount(index); ++i) { 0325 const int type = this->index(i, 0, index).data(ResultsModel::CategoryRelevanceRole).toInt(); 0326 if (type > highestType) { 0327 highestType = type; 0328 } 0329 } 0330 return highestType; 0331 } 0332 case ResultsModel::RelevanceRole: { 0333 qreal highestRelevance = 0.0; 0334 for (int i = 0; i < rowCount(index); ++i) { 0335 const qreal relevance = this->index(i, 0, index).data(ResultsModel::RelevanceRole).toReal(); 0336 if (relevance > highestRelevance) { 0337 highestRelevance = relevance; 0338 } 0339 } 0340 return highestRelevance; 0341 } 0342 } 0343 0344 return QVariant(); 0345 } 0346 0347 QModelIndex RunnerResultsModel::index(int row, int column, const QModelIndex &parent) const 0348 { 0349 if (row < 0 || column != 0) { 0350 return QModelIndex(); 0351 } 0352 0353 if (parent.isValid()) { 0354 const QString category = m_categories.value(parent.row()); 0355 const auto matches = m_matches.value(category); 0356 if (row < matches.count()) { 0357 return createIndex(row, column, int(parent.row() + 1)); 0358 } 0359 0360 return QModelIndex(); 0361 } 0362 0363 if (row < m_categories.count()) { 0364 return createIndex(row, column, nullptr); 0365 } 0366 0367 return QModelIndex(); 0368 } 0369 0370 QModelIndex RunnerResultsModel::parent(const QModelIndex &child) const 0371 { 0372 if (child.internalId()) { 0373 return createIndex(int(child.internalId() - 1), 0, nullptr); 0374 } 0375 0376 return QModelIndex(); 0377 } 0378 0379 KRunner::RunnerManager *RunnerResultsModel::runnerManager() const 0380 { 0381 return m_manager; 0382 } 0383 0384 } 0385 0386 #include "moc_runnerresultsmodel_p.cpp"