diff --git a/lib/sourcesmodel.cpp b/lib/sourcesmodel.cpp index ad89fc5..82f52e7 100644 --- a/lib/sourcesmodel.cpp +++ b/lib/sourcesmodel.cpp @@ -1,440 +1,445 @@ /* * This file is part of the KDE Milou Project * Copyright (C) 2013 Vishesh Handa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "sourcesmodel.h" #include #include #include #include #include #include #include using namespace Milou; SourcesModel::SourcesModel(QObject* parent) : QAbstractListModel(parent) , m_size(0) { m_manager = new Plasma::RunnerManager(this); connect(m_manager, SIGNAL(matchesChanged(QList)), this, SLOT(slotMatchesChanged(QList))); KDirWatch* watch = KDirWatch::self(); connect(watch, &KDirWatch::created, this, &SourcesModel::slotSettingsFileChanged); connect(watch, &KDirWatch::dirty, this, &SourcesModel::slotSettingsFileChanged); watch->addFile(QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("krunnerrc"))); m_resetTimer.setSingleShot(true); m_resetTimer.setInterval(500); connect(&m_resetTimer, SIGNAL(timeout()), this, SLOT(slotResetTimeout())); } SourcesModel::~SourcesModel() { } QHash SourcesModel::roleNames() const { QHash roles = QAbstractListModel::roleNames(); roles.insert(TypeRole, "type"); roles.insert(SubtextRole, "subtext"); roles.insert(ActionsRole, "actions"); roles.insert(DuplicateRole, "isDuplicate"); roles.insert(PreviewTypeRole, "previewType"); roles.insert(PreviewUrlRole, "previewUrl"); roles.insert(PreviewLabelRole, "previewLabel"); return roles; } Plasma::QueryMatch SourcesModel::fetchMatch(int row) const { foreach (const QString& type, m_types) { const TypeData data = m_matches.value(type); if (row < data.shown.size()) { return data.shown[row]; } else { row -= data.shown.size(); if (row < 0) { break; } } } return Plasma::QueryMatch(nullptr); } QVariant SourcesModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() >= m_size) return QVariant(); Plasma::QueryMatch m = fetchMatch(index.row()); Q_ASSERT(m.runner()); switch(role) { case Qt::DisplayRole: return m.text(); case Qt::DecorationRole: if (!m.iconName().isEmpty()) { return m.iconName(); } return m.icon(); case TypeRole: return m.matchCategory(); case SubtextRole: return m.subtext(); case ActionsRole: { const auto &actions = m_manager->actionsForMatch(m); if (actions.isEmpty()) { return QVariantList(); } QVariantList actionsList; actionsList.reserve(actions.size()); for (QAction *action : actions) { actionsList.append(QVariant::fromValue(action)); } return actionsList; } case DuplicateRole: return m_duplicates.value(m.text()); /* case PreviewTypeRole: return m.previewType(); case PreviewUrlRole: return m.previewUrl(); case PreviewLabelRole: return m.previewLabel(); */ } return QVariant(); } int SourcesModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_size; } QString SourcesModel::queryString() const { return m_queryString; } int SourcesModel::queryLimit() const { return m_queryLimit; } QString SourcesModel::runner() const { return m_runner; } void SourcesModel::setRunner(const QString& runner) { if (m_runner != runner) { m_runner = runner; m_manager->setSingleModeRunnerId(m_runner); m_manager->setSingleMode(!m_runner.isEmpty()); emit runnerChanged(); } } QString SourcesModel::runnerName() const { auto *singleRunner = m_manager->singleModeRunner(); if (!singleRunner) { return QString(); } return singleRunner->name(); } QIcon SourcesModel::runnerIcon() const { auto *singleRunner = m_manager->singleModeRunner(); if (!singleRunner) { return QIcon(); } return singleRunner->icon(); } void SourcesModel::setQueryLimit(int limit) { m_queryLimit = limit; /* foreach (AbstractSource* source, m_sources) source->setQueryLimit(limit); */ } void SourcesModel::setQueryString(const QString& str) { if (str.trimmed() == m_queryString.trimmed()) { return; } m_queryString = str; if (m_queryString.isEmpty()) { clear(); return; } // We avoid clearing the model instantly, and instead wait for the results // to show up, and only then do we clear the model. In the event // where there are no results, we wait for a predefined time before // clearing the model m_resetTimer.start(); m_modelPopulated = false; m_manager->launchQuery(m_queryString, m_runner); } void SourcesModel::slotResetTimeout() { if (!m_modelPopulated) { - clear(); + // The old items are still shown, get rid of them + beginResetModel(); + m_matches.clear(); + m_size = 0; + m_duplicates.clear(); + endResetModel(); } } void SourcesModel::slotMatchesChanged(const QList& l) { // We do reset handling ourselves, so ignore clears if the reset timer // is supposed to handle them (see setQueryString) if(l.length() == 0 && m_resetTimer.isActive() && !m_modelPopulated) { return; } beginResetModel(); m_matches.clear(); m_size = 0; m_types.clear(); m_duplicates.clear(); QList list(l); qSort(list); for (auto it = list.crbegin(), end = list.crend(); it != end; ++it) { slotMatchAdded(*it); } // Sort the result types. We give the results which contain the query // text in the user visible string a higher preference than the ones // that do not // The rest are given the same preference as given by the runners. const QString simplifiedQuery = m_queryString.simplified(); const auto words = simplifiedQuery.splitRef(QLatin1Char(' '), QString::SkipEmptyParts); QSet higherTypes; foreach (const QString &type, m_types) { const TypeData td = m_matches.value(type); for (const Plasma::QueryMatch &match : td.shown) { const QString text = match.text().simplified(); bool containsAll = true; for (const auto &word : words) { if (!text.contains(word, Qt::CaseInsensitive)) { containsAll = false; break; } } // Maybe we should be giving it a higher type based on the number of matched // words in the text? if (containsAll) { higherTypes << match.matchCategory(); } } } auto sortFunc = [&](const QString& l, const QString& r) { bool lHigher = higherTypes.contains(l); bool rHigher = higherTypes.contains(r); if (lHigher == rHigher) { return false; } else { return lHigher; } }; qStableSort(m_types.begin(), m_types.end(), sortFunc); m_modelPopulated = true; endResetModel(); } // // Tries to make sure that all the types have the same number // of visible items // void SourcesModel::slotMatchAdded(const Plasma::QueryMatch& m) { if (m_queryString.isEmpty()) return; QString matchType = m.matchCategory(); if (!m_types.contains(matchType)) { m_types << matchType; } if (m_size == m_queryLimit) { int maxShownItems = 0; QString maxShownType; foreach (const QString& type, m_types) { TypeData data = m_matches.value(type); if (data.shown.size() >= maxShownItems) { maxShownItems = data.shown.size(); maxShownType = type; } } if (maxShownType == matchType) { m_matches[matchType].hidden.append(m); return; } // Remove the last shown row from maxShownType // and add it to matchType Plasma::QueryMatch transferMatch = m_matches[maxShownType].shown.takeLast(); m_matches[maxShownType].hidden.append(transferMatch); m_size--; m_duplicates[transferMatch.text()]--; } m_matches[matchType].shown.append(m); m_size++; m_duplicates[m.text()]++; } void SourcesModel::slotSettingsFileChanged(const QString &path) { if (!path.endsWith(QLatin1String("krunnerrc"))) { return; } reloadConfiguration(); } void SourcesModel::clear() { beginResetModel(); m_matches.clear(); m_size = 0; m_duplicates.clear(); m_queryString.clear(); m_manager->reset(); m_manager->matchSessionComplete(); endResetModel(); } bool SourcesModel::run(int index) { Plasma::QueryMatch match = fetchMatch(index); Q_ASSERT(match.runner()); if (match.type() == Plasma::QueryMatch::InformationalMatch) { QString info = match.data().toString(); int editPos = info.length(); if (!info.isEmpty()) { // FIXME: pretty lame way to decide if this is a query prototype // Copied from kde4 krunner interface.cpp if (match.runner() == nullptr) { // lame way of checking to see if this is a Help Button generated match! int index = info.indexOf(QStringLiteral(":q:")); if (index != -1) { editPos = index; info.replace(QStringLiteral(":q:"), QString()); } } emit updateSearchTerm(info, editPos); return false; } } m_manager->run(match); return true; } bool SourcesModel::runAction(int index, int actionIndex) { Plasma::QueryMatch match = fetchMatch(index); Q_ASSERT(match.runner()); const auto &actions = m_manager->actionsForMatch(match); if (actionIndex < 0 || actionIndex >= actions.count()) { return false; } QAction *action = actions.at(actionIndex); match.setSelectedAction(action); m_manager->run(match); return true; } void SourcesModel::reloadConfiguration() { KSharedConfig::openConfig(QStringLiteral("krunnerrc"))->reparseConfiguration(); m_manager->reloadConfiguration(); } QMimeData *SourcesModel::getMimeData(int index) const { Plasma::QueryMatch match = fetchMatch(index); Q_ASSERT(match.runner()); // we're returning a parent-less QObject from a Q_INVOKABLE // which means the QML engine will take care of deleting it eventually QMimeData *mimeData = m_manager->mimeDataForMatch(match); return mimeData; } diff --git a/lib/sourcesmodel.h b/lib/sourcesmodel.h index 5ad6955..e5eeafc 100644 --- a/lib/sourcesmodel.h +++ b/lib/sourcesmodel.h @@ -1,137 +1,140 @@ /* * This file is part of the KDE Milou Project * Copyright (C) 2013 Vishesh Handa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #ifndef SOURCESMODEL_H #define SOURCESMODEL_H #include #include #include #include #include "milou_export.h" namespace Milou { class MILOU_EXPORT SourcesModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString queryString READ queryString WRITE setQueryString) Q_PROPERTY(int queryLimit READ queryLimit WRITE setQueryLimit) Q_PROPERTY(QString runner READ runner WRITE setRunner NOTIFY runnerChanged) Q_PROPERTY(QString runnerName READ runnerName NOTIFY runnerChanged) Q_PROPERTY(QIcon runnerIcon READ runnerIcon NOTIFY runnerChanged) public: explicit SourcesModel(QObject* parent = nullptr); ~SourcesModel() override; enum Roles { TypeRole = Qt::UserRole + 1, SubtextRole, ActionsRole, DuplicateRole, PreviewTypeRole, PreviewUrlRole, PreviewLabelRole }; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; bool hasChildren(const QModelIndex&) const override { return false; } QString runner() const; void setRunner(const QString& runner); QString runnerName() const; QIcon runnerIcon() const; QString queryString() const; int queryLimit() const; QHash roleNames() const override; Q_SIGNALS: /** * This signal is emitted when a an InformationalMatch is run, and it is advised * to update the search term. * Eg - Calculator runner */ void updateSearchTerm(const QString& text, int pos); void runnerChanged(); public Q_SLOTS: void reloadConfiguration(); void setQueryString(const QString& str); void setQueryLimit(int limit); + /** + * Clears the model content and resets the runner context, i.e. no new items will appear. + */ void clear(); bool run(int index); bool runAction(int index, int actionIndex); Q_INVOKABLE QString getType(int index) const { return data(createIndex(index, 0), TypeRole).toString(); } Q_INVOKABLE QMimeData *getMimeData(int index) const; private Q_SLOTS: void slotMatchesChanged(const QList& list); void slotMatchAdded(const Plasma::QueryMatch& match); void slotResetTimeout(); void slotSettingsFileChanged(const QString &path); public: // A list of all the types that are being shown QList m_types; struct TypeData { QList shown; QList hidden; }; QHash m_matches; int m_size; /// Counts the number of results for each visible Plasma::QueryMatch::text /// We use this to show additional info when there are multiple visible /// results with the same text QHash m_duplicates; QString m_queryString; int m_queryLimit; QString m_runner; Plasma::RunnerManager* m_manager; bool m_modelPopulated; QTimer m_resetTimer; Plasma::QueryMatch fetchMatch(int row) const; }; } #endif // SOURCESMODEL_H