File indexing completed on 2024-04-21 16:18:22

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 }