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"