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

0001 /*
0002  * This file is part of the KDE Milou Project
0003  * SPDX-FileCopyrightText: 2013 Vishesh Handa <me@vhanda.in>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006  *
0007  */
0008 
0009 #include "sourcesmodel.h"
0010 
0011 #include <KConfig>
0012 #include <KDirWatch>
0013 #include <KSharedConfig>
0014 
0015 #include <QAction>
0016 #include <QMimeData>
0017 #include <QModelIndex>
0018 #include <QSet>
0019 
0020 using namespace Milou;
0021 
0022 SourcesModel::SourcesModel(QObject *parent)
0023     : QAbstractListModel(parent)
0024     , m_size(0)
0025 {
0026     m_manager = new Plasma::RunnerManager(this);
0027     connect(m_manager, &Plasma::RunnerManager::matchesChanged, this, &SourcesModel::slotMatchesChanged);
0028 
0029     KDirWatch *watch = KDirWatch::self();
0030     connect(watch, &KDirWatch::created, this, &SourcesModel::slotSettingsFileChanged);
0031     connect(watch, &KDirWatch::dirty, this, &SourcesModel::slotSettingsFileChanged);
0032     watch->addFile(QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("krunnerrc")));
0033 
0034     m_resetTimer.setSingleShot(true);
0035     m_resetTimer.setInterval(500);
0036     connect(&m_resetTimer, &QTimer::timeout, this, &SourcesModel::slotResetTimeout);
0037 }
0038 
0039 SourcesModel::~SourcesModel()
0040 {
0041 }
0042 
0043 QHash<int, QByteArray> SourcesModel::roleNames() const
0044 {
0045     QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
0046     roles.insert(TypeRole, "type");
0047     roles.insert(SubtextRole, "subtext");
0048     roles.insert(ActionsRole, "actions");
0049     roles.insert(DuplicateRole, "isDuplicate");
0050     roles.insert(PreviewTypeRole, "previewType");
0051     roles.insert(PreviewUrlRole, "previewUrl");
0052     roles.insert(PreviewLabelRole, "previewLabel");
0053 
0054     return roles;
0055 }
0056 
0057 Plasma::QueryMatch SourcesModel::fetchMatch(int row) const
0058 {
0059     for (const QString &type : qAsConst(m_types)) {
0060         const TypeData data = m_matches.value(type);
0061         if (row < data.shown.size()) {
0062             return data.shown[row];
0063         } else {
0064             row -= data.shown.size();
0065             if (row < 0) {
0066                 break;
0067             }
0068         }
0069     }
0070 
0071     return Plasma::QueryMatch(nullptr);
0072 }
0073 
0074 QVariant SourcesModel::data(const QModelIndex &index, int role) const
0075 {
0076     if (!index.isValid()) {
0077         return QVariant();
0078     }
0079 
0080     if (index.row() >= m_size) {
0081         return QVariant();
0082     }
0083 
0084     Plasma::QueryMatch m = fetchMatch(index.row());
0085     Q_ASSERT(m.runner());
0086 
0087     switch (role) {
0088     case Qt::DisplayRole:
0089         return m.text();
0090 
0091     case Qt::DecorationRole:
0092         if (!m.iconName().isEmpty()) {
0093             return m.iconName();
0094         }
0095 
0096         return m.icon();
0097 
0098     case TypeRole:
0099         return m.matchCategory();
0100 
0101     case SubtextRole:
0102         return m.subtext();
0103 
0104     case ActionsRole: {
0105         const auto &actions = m_manager->actionsForMatch(m);
0106         if (actions.isEmpty()) {
0107             return QVariantList();
0108         }
0109 
0110         QVariantList actionsList;
0111         actionsList.reserve(actions.size());
0112 
0113         for (QAction *action : actions) {
0114             actionsList.append(QVariant::fromValue(action));
0115         }
0116 
0117         return actionsList;
0118     }
0119     case DuplicateRole:
0120         return m_duplicates.value(m.text());
0121 
0122         /*
0123     case PreviewTypeRole:
0124         return m.previewType();
0125 
0126     case PreviewUrlRole:
0127         return m.previewUrl();
0128 
0129     case PreviewLabelRole:
0130         return m.previewLabel();
0131         */
0132     }
0133 
0134     return QVariant();
0135 }
0136 
0137 int SourcesModel::rowCount(const QModelIndex &parent) const
0138 {
0139     if (parent.isValid()) {
0140         return 0;
0141     }
0142 
0143     return m_size;
0144 }
0145 
0146 QString SourcesModel::queryString() const
0147 {
0148     return m_queryString;
0149 }
0150 
0151 int SourcesModel::queryLimit() const
0152 {
0153     return m_queryLimit;
0154 }
0155 
0156 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81)
0157 QString SourcesModel::runner() const
0158 {
0159     return m_runner;
0160 }
0161 
0162 void SourcesModel::setRunner(const QString &runner)
0163 {
0164     if (m_runner != runner) {
0165         m_runner = runner;
0166 
0167         m_manager->setSingleModeRunnerId(m_runner);
0168         m_manager->setSingleMode(!m_runner.isEmpty());
0169 
0170         Q_EMIT runnerChanged();
0171     }
0172 }
0173 
0174 QString SourcesModel::runnerName() const
0175 {
0176     auto *singleRunner = m_manager->singleModeRunner();
0177     if (!singleRunner) {
0178         return QString();
0179     }
0180 
0181     return singleRunner->name();
0182 }
0183 
0184 QIcon SourcesModel::runnerIcon() const
0185 {
0186     auto *singleRunner = m_manager->singleModeRunner();
0187     if (!singleRunner) {
0188         return QIcon();
0189     }
0190 
0191     return singleRunner->icon();
0192 }
0193 #endif
0194 
0195 void SourcesModel::setQueryLimit(int limit)
0196 {
0197     m_queryLimit = limit;
0198     /*
0199     foreach (AbstractSource* source, m_sources)
0200         source->setQueryLimit(limit);
0201     */
0202 }
0203 
0204 void SourcesModel::setQueryString(const QString &str)
0205 {
0206     if (str.trimmed() == m_queryString.trimmed()) {
0207         return;
0208     }
0209 
0210     m_queryString = str;
0211     if (m_queryString.isEmpty()) {
0212         clear();
0213         return;
0214     }
0215 
0216     // We avoid clearing the model instantly, and instead wait for the results
0217     // to show up, and only then do we clear the model. In the event
0218     // where there are no results, we wait for a predefined time before
0219     // clearing the model
0220     m_resetTimer.start();
0221 
0222     m_modelPopulated = false;
0223     m_manager->launchQuery(m_queryString, m_runner);
0224 }
0225 
0226 void SourcesModel::slotResetTimeout()
0227 {
0228     if (!m_modelPopulated) {
0229         // The old items are still shown, get rid of them
0230         beginResetModel();
0231         m_matches.clear();
0232         m_size = 0;
0233         m_duplicates.clear();
0234         endResetModel();
0235     }
0236 }
0237 
0238 void SourcesModel::slotMatchesChanged(const QList<Plasma::QueryMatch> &l)
0239 {
0240     // We do reset handling ourselves, so ignore clears if the reset timer
0241     // is supposed to handle them (see setQueryString)
0242     if (l.length() == 0 && m_resetTimer.isActive() && !m_modelPopulated) {
0243         return;
0244     }
0245 
0246     beginResetModel();
0247     m_matches.clear();
0248     m_size = 0;
0249     m_types.clear();
0250     m_duplicates.clear();
0251 
0252     QList<Plasma::QueryMatch> list(l);
0253     std::sort(list.begin(), list.end());
0254 
0255     for (auto it = list.crbegin(), end = list.crend(); it != end; ++it) {
0256         slotMatchAdded(*it);
0257     }
0258 
0259     // Sort the result types. We give the results which contain the query
0260     // text in the user visible string a higher preference than the ones
0261     // that do not
0262     // The rest are given the same preference as given by the runners.
0263     const QString simplifiedQuery = m_queryString.simplified();
0264 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0265     const auto words = simplifiedQuery.splitRef(QLatin1Char(' '), Qt::SkipEmptyParts);
0266 #else
0267     const auto words = QStringView(simplifiedQuery).split(QLatin1Char(' '), Qt::SkipEmptyParts);
0268 #endif
0269     QSet<QString> higherTypes;
0270     for (const QString &type : qAsConst(m_types)) {
0271         const TypeData td = m_matches.value(type);
0272 
0273         for (const Plasma::QueryMatch &match : td.shown) {
0274             const QString text = match.text().simplified();
0275             bool containsAll = true;
0276 
0277             for (const auto &word : words) {
0278                 if (!text.contains(word, Qt::CaseInsensitive)) {
0279                     containsAll = false;
0280                     break;
0281                 }
0282             }
0283 
0284             // Maybe we should be giving it a higher type based on the number of matched
0285             // words in the text?
0286             if (containsAll) {
0287                 higherTypes << match.matchCategory();
0288             }
0289         }
0290     }
0291 
0292     auto sortFunc = [&](const QString &l, const QString &r) {
0293         bool lHigher = higherTypes.contains(l);
0294         bool rHigher = higherTypes.contains(r);
0295 
0296         if (lHigher == rHigher) {
0297             return false;
0298         } else {
0299             return lHigher;
0300         }
0301     };
0302     std::stable_sort(m_types.begin(), m_types.end(), sortFunc);
0303 
0304     m_modelPopulated = true;
0305     endResetModel();
0306 }
0307 
0308 //
0309 // Tries to make sure that all the types have the same number
0310 // of visible items
0311 //
0312 void SourcesModel::slotMatchAdded(const Plasma::QueryMatch &m)
0313 {
0314     if (m_queryString.isEmpty()) {
0315         return;
0316     }
0317 
0318     QString matchType = m.matchCategory();
0319 
0320     if (!m_types.contains(matchType)) {
0321         m_types << matchType;
0322     }
0323 
0324     if (m_size == m_queryLimit) {
0325         int maxShownItems = 0;
0326         QString maxShownType;
0327         for (const QString &type : qAsConst(m_types)) {
0328             TypeData data = m_matches.value(type);
0329             if (data.shown.size() >= maxShownItems) {
0330                 maxShownItems = data.shown.size();
0331                 maxShownType = type;
0332             }
0333         }
0334 
0335         if (maxShownType == matchType) {
0336             m_matches[matchType].hidden.append(m);
0337             return;
0338         }
0339 
0340         // Remove the last shown row from maxShownType
0341         // and add it to matchType
0342         Plasma::QueryMatch transferMatch = m_matches[maxShownType].shown.takeLast();
0343         m_matches[maxShownType].hidden.append(transferMatch);
0344         m_size--;
0345         m_duplicates[transferMatch.text()]--;
0346     }
0347 
0348     m_matches[matchType].shown.append(m);
0349     m_size++;
0350     m_duplicates[m.text()]++;
0351 }
0352 
0353 void SourcesModel::slotSettingsFileChanged(const QString &path)
0354 {
0355     if (!path.endsWith(QLatin1String("krunnerrc"))) {
0356         return;
0357     }
0358 
0359     reloadConfiguration();
0360 }
0361 
0362 void SourcesModel::clear()
0363 {
0364     beginResetModel();
0365     m_matches.clear();
0366     m_size = 0;
0367     m_duplicates.clear();
0368     m_queryString.clear();
0369     m_manager->reset();
0370     m_manager->matchSessionComplete();
0371     endResetModel();
0372 }
0373 
0374 bool SourcesModel::run(int index)
0375 {
0376     Plasma::QueryMatch match = fetchMatch(index);
0377     Q_ASSERT(match.runner());
0378 
0379 #if KRUNNER_ENABLE_DEPRECATED_SINCE(5, 99)
0380     if (match.type() == Plasma::QueryMatch::InformationalMatch) {
0381         QString info = match.data().toString();
0382         int editPos = info.length();
0383 
0384         if (!info.isEmpty()) {
0385             // FIXME: pretty lame way to decide if this is a query prototype
0386             // Copied from kde4 krunner interface.cpp
0387             if (match.runner() == nullptr) {
0388                 // lame way of checking to see if this is a Help Button generated match!
0389                 int index = info.indexOf(QStringLiteral(":q:"));
0390 
0391                 if (index != -1) {
0392                     editPos = index;
0393                     info.replace(QStringLiteral(":q:"), QString());
0394                 }
0395             }
0396 
0397             Q_EMIT updateSearchTerm(info, editPos);
0398             return false;
0399         }
0400     }
0401 #endif
0402 
0403     m_manager->run(match);
0404     return true;
0405 }
0406 
0407 bool SourcesModel::runAction(int index, int actionIndex)
0408 {
0409     Plasma::QueryMatch match = fetchMatch(index);
0410     Q_ASSERT(match.runner());
0411 
0412     const auto &actions = m_manager->actionsForMatch(match);
0413     if (actionIndex < 0 || actionIndex >= actions.count()) {
0414         return false;
0415     }
0416 
0417     QAction *action = actions.at(actionIndex);
0418     match.setSelectedAction(action);
0419     m_manager->run(match);
0420     return true;
0421 }
0422 
0423 void SourcesModel::reloadConfiguration()
0424 {
0425     KSharedConfig::openConfig(QStringLiteral("krunnerrc"))->reparseConfiguration();
0426     m_manager->reloadConfiguration();
0427 }
0428 
0429 QMimeData *SourcesModel::getMimeData(int index) const
0430 {
0431     Plasma::QueryMatch match = fetchMatch(index);
0432     Q_ASSERT(match.runner());
0433 
0434     // we're returning a parent-less QObject from a Q_INVOKABLE
0435     // which means the QML engine will take care of deleting it eventually
0436     QMimeData *mimeData = m_manager->mimeDataForMatch(match);
0437 
0438     return mimeData;
0439 }