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