diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Qml Quick Widgets Test) -find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS CoreAddons I18n Declarative Service Plasma Runner) +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS CoreAddons I18n Declarative ItemModels Service Plasma Runner) include(FeatureSummary) include(KDEInstallDirs) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -9,6 +9,8 @@ set (lib_SRCS preview.cpp previewplugin.cpp + resultsmodel.cpp + runnerresultsmodel.cpp sourcesmodel.cpp draghelper.cpp mousehelper.cpp @@ -21,6 +23,7 @@ Qt5::Qml Qt5::Quick Qt5::Widgets # for QAction... + KF5::ItemModels KF5::Service KF5::Plasma KF5::Runner diff --git a/lib/qml/ResultDelegate.qml b/lib/qml/ResultDelegate.qml --- a/lib/qml/ResultDelegate.qml +++ b/lib/qml/ResultDelegate.qml @@ -113,9 +113,10 @@ onPositionChanged: { if (__pressX != -1 && typeof dragHelper !== "undefined" && dragHelper.isDrag(__pressX, __pressY, mouse.x, mouse.y)) { - var mimeData = ListView.view.model.getMimeData(index); + var resultsModel = ListView.view.model; + var mimeData = resultsModel.getMimeData(resultsModel.index(index, 0)); if (mimeData) { - dragHelper.startDrag(root, mimeData, model.decoration); + dragHelper.startDrag(resultDelegate, mimeData, model.decoration); __pressed = false; __pressX = -1; __pressY = -1; @@ -215,7 +216,10 @@ PlasmaComponents.Label { id: subtextLabel - text: model.isDuplicate > 1 || resultDelegate.isCurrent ? String(model.subtext || "") : "" + + // SourcesModel returns number of duplicates in this property + // ResultsModel just has it as a boolean as you would expect from the name of the property + text: model.isDuplicate === true || model.isDuplicate > 1 || resultDelegate.isCurrent ? String(model.subtext || "") : "" color: theme.textColor // HACK If displayLabel is too long it will shift this label outside boundaries diff --git a/lib/qml/ResultsView.qml b/lib/qml/ResultsView.qml --- a/lib/qml/ResultsView.qml +++ b/lib/qml/ResultsView.qml @@ -24,14 +24,16 @@ import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.milou 0.2 as Milou +import org.kde.milou 0.3 as Milou ListView { id: listView property alias queryString: resultModel.queryString property alias runner: resultModel.runner + property alias runnerName: resultModel.runnerName property alias runnerIcon: resultModel.runnerIcon + property alias querying: resultModel.querying property bool reversed signal activated signal updateQueryString(string text, int cursorPosition) @@ -46,7 +48,7 @@ section { criteria: ViewSection.FullString - property: "type" + property: "category" } // This is used to keep track if the user has pressed enter before @@ -66,12 +68,12 @@ dragIconSize: units.iconSizes.medium } - model: Milou.SourcesModel { + model: Milou.ResultsModel { id: resultModel - queryLimit: 20 - - - onUpdateSearchTerm: listView.updateQueryString(text, pos) + limit: 20 + onQueryStringChangeRequested:{ + listView.updateQueryString(queryString, pos) + } } // Internally when the query string changes, the model is reset @@ -116,15 +118,15 @@ return } - if (resultModel.run(currentIndex)) { + if (resultModel.run(resultModel.index(currentIndex, 0))) { activated() } runAutomatically = false } } function runAction(index) { - if (resultModel.runAction(currentIndex, index)) { + if (resultModel.runAction(resultModel.index(currentIndex, 0), index)) { activated() } } @@ -185,8 +187,8 @@ resultModel.loadSettings() } - function setQueryString(string) { - resultModel.queryString = string + function setQueryString(queryString) { + resultModel.queryString = queryString runAutomatically = false } } diff --git a/lib/qml/qmlplugins.cpp b/lib/qml/qmlplugins.cpp --- a/lib/qml/qmlplugins.cpp +++ b/lib/qml/qmlplugins.cpp @@ -23,6 +23,7 @@ #include "qmlplugins.h" #include "sourcesmodel.h" +#include "resultsmodel.h" #include "preview.h" #include "draghelper.h" #include "mousehelper.h" @@ -36,6 +37,7 @@ void QmlPlugins::registerTypes(const char *uri) { qmlRegisterType (uri, 0, 1, "SourcesModel"); + qmlRegisterType(uri, 0, 3, "ResultsModel"); qmlRegisterType (uri, 0, 1, "Preview"); qmlRegisterType (uri, 0, 2, "DragHelper"); qmlRegisterSingletonType (uri, 0, 1, "MouseHelper", diff --git a/lib/resultsmodel.h b/lib/resultsmodel.h new file mode 100644 --- /dev/null +++ b/lib/resultsmodel.h @@ -0,0 +1,136 @@ +/* + * This file is part of the KDE Milou Project + * Copyright (C) 2019 Kai Uwe Broulik + * + * 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 . + * + */ + +#pragma once + +#include +#include +#include + +#include "milou_export.h" + +namespace Milou { + +class MILOU_EXPORT ResultsModel : public QSortFilterProxyModel +{ + Q_OBJECT + + /** + * The query string to run + */ + Q_PROPERTY(QString queryString READ queryString WRITE setQueryString NOTIFY queryStringChanged) + /** + * The preferred maximum number of matches in the model + * + * If there are lots of results from different catergories, + * the limit can be slightly exceeded. + * + * Default is 0, which means no limit. + */ + Q_PROPERTY(int limit READ limit WRITE setLimit RESET resetLimit NOTIFY limitChanged) + /** + * Whether the query is currently being run + * + * This can be used to show a busy indicator + */ + Q_PROPERTY(bool querying READ querying NOTIFY queryingChanged) + + /** + * The single runner to use for querying in single runner mode + * + * Defaults to empty string which means all runners + */ + Q_PROPERTY(QString runner READ runner WRITE setRunner NOTIFY runnerChanged) + // FIXME rename to singleModeRunnerName or something + Q_PROPERTY(QString runnerName READ runnerName NOTIFY runnerChanged) + Q_PROPERTY(QIcon runnerIcon READ runnerIcon NOTIFY runnerChanged) + +public: + explicit ResultsModel(QObject *parent = nullptr); + ~ResultsModel() override; + + enum Roles { + IdRole = Qt::UserRole + 1, + TypeRole, + RelevanceRole, + EnabledRole, + CategoryRole, + SubtextRole, + DuplicateRole, + ActionsRole + }; + Q_ENUM(Roles) + + QString queryString() const; + void setQueryString(const QString &queryString); + Q_SIGNAL void queryStringChanged(); + + int limit() const; + void setLimit(int limit); + void resetLimit(); + Q_SIGNAL void limitChanged(); + + bool querying() const; + Q_SIGNAL void queryingChanged(); + + QString runner() const; + void setRunner(const QString &runner); + Q_SIGNAL void runnerChanged(); + + QString runnerName() const; + QIcon runnerIcon() const; + + QHash roleNames() const override; + + /** + * Clears the model content and resets the runner context, i.e. no new items will appear. + */ + Q_INVOKABLE void clear(); + + /** + * Run the result at the given model index @p idx + */ + Q_INVOKABLE bool run(const QModelIndex &idx); + /** + * Run the action @p actionNumber at given model index @p idx + */ + Q_INVOKABLE bool runAction(const QModelIndex &idx, int actionNumber); + + /** + * Get mime data for the result at given model index @p idx + */ + Q_INVOKABLE QMimeData *getMimeData(const QModelIndex &idx) const; + +Q_SIGNALS: + /** + * This signal is emitted when a an InformationalMatch is run, and it is advised + * to update the search term, e.g. used for calculator runner results + */ + void queryStringChangeRequested(const QString &queryString, int pos); + +private: + class Private; + QScopedPointer d; + +}; + +} // namespace Milou diff --git a/lib/resultsmodel.cpp b/lib/resultsmodel.cpp new file mode 100644 --- /dev/null +++ b/lib/resultsmodel.cpp @@ -0,0 +1,376 @@ +/* + * This file is part of the KDE Milou Project + * Copyright (C) 2019 Kai Uwe Broulik + * + * 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 "resultsmodel.h" + +#include "runnerresultsmodel.h" + +#include + +#include +#include + +#include + +using namespace Milou; + +class SortProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + SortProxyModel(QObject *parent) : QSortFilterProxyModel(parent) + { + setDynamicSortFilter(true); + sort(0, Qt::DescendingOrder); + } + ~SortProxyModel() override = default; + +protected: + bool lessThan(const QModelIndex &sourceA, const QModelIndex &sourceB) const override + { + const int typeA = sourceA.data(ResultsModel::TypeRole).toInt(); + const int typeB = sourceB.data(ResultsModel::TypeRole).toInt(); + + if (typeA != typeB) { + return typeA < typeB; + } + + const qreal relevanceA = sourceA.data(ResultsModel::RelevanceRole).toReal(); + const qreal relevanceB = sourceB.data(ResultsModel::RelevanceRole).toReal(); + + if (!qFuzzyCompare(relevanceA, relevanceB)) { + return relevanceA < relevanceB; + } + + return QSortFilterProxyModel::lessThan(sourceA, sourceB); + } +}; + +class CategoryDistributionProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + CategoryDistributionProxyModel(QObject *parent) : QSortFilterProxyModel(parent) + { + + } + ~CategoryDistributionProxyModel() override = default; + + void setSourceModel(QAbstractItemModel *sourceModel) override + { + if (this->sourceModel()) { + disconnect(this->sourceModel(), nullptr, this, nullptr); + } + + QSortFilterProxyModel::setSourceModel(sourceModel); + + if (sourceModel) { + connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &CategoryDistributionProxyModel::invalidateFilter); + connect(sourceModel, &QAbstractItemModel::rowsMoved, this, &CategoryDistributionProxyModel::invalidateFilter); + connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &CategoryDistributionProxyModel::invalidateFilter); + } + } + + int limit() const { return m_limit; } + void setLimit(int limit) { + if (m_limit == limit) { + return; + } + m_limit = limit; + invalidateFilter(); + emit limitChanged(); + } + +Q_SIGNALS: + void limitChanged(); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override + { + if (m_limit <= 0) { + return true; + } + + if (!sourceParent.isValid()) { + return true; + } + + const int categoryCount = sourceModel()->rowCount(); + + int maxItemsInCategory = m_limit; + + if (categoryCount > 1) { + int itemsBefore = 0; + for (int i = 0; i <= sourceParent.row(); ++i) { + const int itemsInCategory = sourceModel()->rowCount(sourceModel()->index(i, 0)); + + // Take into account that every category gets at least one item shown + const int availableSpace = m_limit - itemsBefore - std::ceil(m_limit / qreal(categoryCount)); + + // The further down the category is the less relevant it is and the less space it my occupy + // First category gets max half the total limit, second category a third, etc + maxItemsInCategory = std::min(availableSpace, int(std::ceil(m_limit / qreal(i + 2)))); + + // At least show one item per category + maxItemsInCategory = std::max(1, maxItemsInCategory); + + itemsBefore += std::min(itemsInCategory, maxItemsInCategory); + } + } + + if (sourceRow >= maxItemsInCategory) { + return false; + } + + return true; + } + +private: + // if you change this, update the default in resetLimit() + int m_limit = 0; +}; + +class HideRootLevelProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + HideRootLevelProxyModel(QObject *parent) : QSortFilterProxyModel(parent) + { + + } + ~HideRootLevelProxyModel() override = default; + + QAbstractItemModel *treeModel() const { + return m_treeModel; + } + void setTreeModel(QAbstractItemModel *treeModel) { + m_treeModel = treeModel; + invalidateFilter(); + } + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override + { + KModelIndexProxyMapper mapper(sourceModel(), m_treeModel); + const QModelIndex treeIdx = mapper.mapLeftToRight(sourceModel()->index(sourceRow, 0, sourceParent)); + return treeIdx.parent().isValid(); + } + +private: + QAbstractItemModel *m_treeModel = nullptr; +}; + +class DuplicateDetectorProxyModel : public QIdentityProxyModel +{ + Q_OBJECT + +public: + DuplicateDetectorProxyModel(QObject *parent) : QIdentityProxyModel(parent) + { + + } + ~DuplicateDetectorProxyModel() override = default; + + QVariant data(const QModelIndex &index, int role) const override + { + if (role != ResultsModel::DuplicateRole) { + return QIdentityProxyModel::data(index, role); + } + + int duplicatesCount = 0; + const QString display = index.data(Qt::DisplayRole).toString(); + + for (int i = 0; i < sourceModel()->rowCount(); ++i) { + if (sourceModel()->index(i, 0).data(Qt::DisplayRole) == display) { + ++duplicatesCount; + + if (duplicatesCount == 2) { + return true; + } + } + } + + return false; + } + +}; + +class Q_DECL_HIDDEN ResultsModel::Private +{ +public: + Private(ResultsModel *q); + + ResultsModel *q; + + QString runner; + + RunnerResultsModel *resultsModel; + SortProxyModel *sortModel; + CategoryDistributionProxyModel *distributionModel; + KDescendantsProxyModel *flattenModel; + HideRootLevelProxyModel *hideRootModel; + DuplicateDetectorProxyModel *duplicateDetectorModel; + +}; + +ResultsModel::Private::Private(ResultsModel *q) + : q(q) + , resultsModel(new RunnerResultsModel(q)) + , sortModel(new SortProxyModel(q)) + , distributionModel(new CategoryDistributionProxyModel(q)) + , flattenModel(new KDescendantsProxyModel(q)) + , hideRootModel(new HideRootLevelProxyModel(q)) + , duplicateDetectorModel(new DuplicateDetectorProxyModel(q)) +{ + +} + +ResultsModel::ResultsModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d(new Private(this)) +{ + connect(d->resultsModel, &RunnerResultsModel::queryStringChanged, this, &ResultsModel::queryStringChanged); + connect(d->resultsModel, &RunnerResultsModel::queryingChanged, this, &ResultsModel::queryingChanged); + connect(d->resultsModel, &RunnerResultsModel::runnerChanged, this, &ResultsModel::runnerChanged); + connect(d->resultsModel, &RunnerResultsModel::queryStringChangeRequested, this, &ResultsModel::queryStringChangeRequested); + + connect(d->distributionModel, &CategoryDistributionProxyModel::limitChanged, this, &ResultsModel::limitChanged); + + d->sortModel->setSourceModel(d->resultsModel); + + d->distributionModel->setSourceModel(d->sortModel); + + d->flattenModel->setSourceModel(d->distributionModel); + + d->hideRootModel->setSourceModel(d->flattenModel); + d->hideRootModel->setTreeModel(d->resultsModel); + + d->duplicateDetectorModel->setSourceModel(d->hideRootModel); + + setSourceModel(d->duplicateDetectorModel); +} + +ResultsModel::~ResultsModel() = default; + +QString ResultsModel::queryString() const +{ + return d->resultsModel->queryString(); +} + +void ResultsModel::setQueryString(const QString &queryString) +{ + d->resultsModel->setQueryString(queryString); +} + +int ResultsModel::limit() const +{ + return d->distributionModel->limit(); +} + +void ResultsModel::setLimit(int limit) +{ + d->distributionModel->setLimit(limit); +} + +void ResultsModel::resetLimit() +{ + setLimit(0); +} + +bool ResultsModel::querying() const +{ + return d->resultsModel->querying(); +} + +QString ResultsModel::runner() const +{ + return d->resultsModel->runner(); +} + +void ResultsModel::setRunner(const QString &runner) +{ + d->resultsModel->setRunner(runner); +} + +QString ResultsModel::runnerName() const +{ + return d->resultsModel->runnerName(); +} + +QIcon ResultsModel::runnerIcon() const +{ + return d->resultsModel->runnerIcon(); +} + +QHash ResultsModel::roleNames() const +{ + auto names = QAbstractItemModel::roleNames(); + names[IdRole] = QByteArrayLiteral("matchId"); // "id" is QML-reserved + names[EnabledRole] = QByteArrayLiteral("enabled"); + names[TypeRole] = QByteArrayLiteral("type"); + names[RelevanceRole] = QByteArrayLiteral("relevance"); + names[CategoryRole] = QByteArrayLiteral("category"); + names[SubtextRole] = QByteArrayLiteral("subtext"); + names[DuplicateRole] = QByteArrayLiteral("isDuplicate"); + names[ActionsRole] = QByteArrayLiteral("actions"); + return names; +} + +void ResultsModel::clear() +{ + +} + +bool ResultsModel::run(const QModelIndex &idx) +{ + KModelIndexProxyMapper mapper(this, d->resultsModel); + const QModelIndex resultsIdx = mapper.mapLeftToRight(idx); + if (!resultsIdx.isValid()) { + return false; + } + return d->resultsModel->run(resultsIdx); +} + +bool ResultsModel::runAction(const QModelIndex &idx, int actionNumber) +{ + KModelIndexProxyMapper mapper(this, d->resultsModel); + const QModelIndex resultsIdx = mapper.mapLeftToRight(idx); + if (!resultsIdx.isValid()) { + return false; + } + return d->resultsModel->runAction(resultsIdx, actionNumber); +} + +QMimeData *ResultsModel::getMimeData(const QModelIndex &idx) const +{ + KModelIndexProxyMapper mapper(this, d->resultsModel); + const QModelIndex resultsIdx = mapper.mapLeftToRight(idx); + if (!resultsIdx.isValid()) { + return nullptr; + } + return d->resultsModel->mimeData({resultsIdx}); +} + +#include "resultsmodel.moc" diff --git a/lib/runnerresultsmodel.h b/lib/runnerresultsmodel.h new file mode 100644 --- /dev/null +++ b/lib/runnerresultsmodel.h @@ -0,0 +1,103 @@ +/* + * This file is part of the KDE Milou Project + * Copyright (C) 2019 Kai Uwe Broulik + * + * 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 . + * + */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace Plasma { +class RunnerManager; +} + +namespace Milou { + +class RunnerResultsModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit RunnerResultsModel(QObject *parent = nullptr); + ~RunnerResultsModel() override; + + QString queryString() const; + void setQueryString(const QString &queryString); + Q_SIGNAL void queryStringChanged(); + + bool querying() const; + Q_SIGNAL void queryingChanged(); + + QString runner() const; + void setRunner(const QString &runner); + Q_SIGNAL void runnerChanged(); + + QString runnerName() const; + QIcon runnerIcon() const; + + /** + * Clears the model content and resets the runner context, i.e. no new items will appear. + */ + void clear(); + + bool run(const QModelIndex &idx); + bool runAction(const QModelIndex &idx, int actionNumber); + + int columnCount(const QModelIndex &parent) const override; + int rowCount(const QModelIndex &parent) const override; + + QVariant data(const QModelIndex &index, int role) const override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + + QMimeData *mimeData(const QModelIndexList &indexes) const override; + +Q_SIGNALS: + void queryStringChangeRequested(const QString &queryString, int pos); + +private: + void setQuerying(bool querying); + + Plasma::QueryMatch fetchMatch(const QModelIndex &idx) const; + + void onMatchesChanged(const QList &matches); + + Plasma::RunnerManager *m_manager; + + QString m_queryString; + bool m_querying = false; + + QString m_runner; + + QTimer m_resetTimer; + bool m_hasMatches = false; + + QStringList m_categories; + QHash> m_matches; + +}; + +} // namespace Milou diff --git a/lib/runnerresultsmodel.cpp b/lib/runnerresultsmodel.cpp new file mode 100644 --- /dev/null +++ b/lib/runnerresultsmodel.cpp @@ -0,0 +1,469 @@ +/* + * This file is part of the KDE Milou Project + * Copyright (C) 2019 Kai Uwe Broulik + * + * 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 "runnerresultsmodel.h" + +#include +#include + +#include + +#include "resultsmodel.h" + +using namespace Milou; +using namespace Plasma; + +RunnerResultsModel::RunnerResultsModel(QObject *parent) + : QAbstractItemModel(parent) + , m_manager(new RunnerManager(this)) +{ + connect(m_manager, &RunnerManager::matchesChanged, this, &RunnerResultsModel::onMatchesChanged); + connect(m_manager, &RunnerManager::queryFinished, this, [this] { + setQuerying(false); + }); + + m_resetTimer.setSingleShot(true); + m_resetTimer.setInterval(500); + connect(&m_resetTimer, &QTimer::timeout, this, [this] { + // Clear the old matches if any + if (!m_hasMatches) { + clear(); + } + }); +} + +RunnerResultsModel::~RunnerResultsModel() = default; + +Plasma::QueryMatch RunnerResultsModel::fetchMatch(const QModelIndex &idx) const +{ + const QString category = m_categories.value(idx.internalId() - 1); + return m_matches.value(category).value(idx.row()); +} + +void RunnerResultsModel::onMatchesChanged(const QList &matches) +{ + // We clear the model ourselves in the reset timer, ignore any empty matchset + if (matches.isEmpty() && m_resetTimer.isActive() && !m_hasMatches) { + return; + } + + // Build the list of new categories and matches + QSet newCategories; + // here we use QString as key since at this point we don't care about the order + // of categories but just what matches we have for each one. + // Below when we populate the actual m_matches we'll make sure to keep the order + // of existing categories to avoid pointless model changes. + QHash> newMatches; + for (const auto &match : matches) { + const QString category = match.matchCategory(); + newCategories.insert(category); + newMatches[category].append(match); + } + + // Get rid of all categories that are no longer present + auto it = m_categories.begin(); + while (it != m_categories.end()) { + const int categoryNumber = std::distance(m_categories.begin(), it); + + if (!newCategories.contains(*it)) { + beginRemoveRows(QModelIndex(), categoryNumber, categoryNumber); + m_matches.remove(*it); + it = m_categories.erase(it); + endRemoveRows(); + } else { + ++it; + } + } + + // Update the existing categories by adding/removing new/removed rows and + // updating changed ones + for (auto it = m_categories.constBegin(), end = m_categories.constEnd(); it != end; ++it) { + Q_ASSERT(newCategories.contains(*it)); + + const int categoryNumber = std::distance(m_categories.constBegin(), it); + const QModelIndex categoryIdx = index(categoryNumber, 0); + + // don't use operator[] as to not insert an empty list + // TODO why? shouldn't m_categories and m_matches be in sync? + auto oldCategoryIt = m_matches.find(*it); + Q_ASSERT(oldCategoryIt != m_matches.end()); + + auto &oldMatchesInCategory = *oldCategoryIt; + const auto newMatchesInCategory = newMatches.value(*it); + + Q_ASSERT(!oldMatchesInCategory.isEmpty()); + Q_ASSERT(!newMatches.isEmpty()); + + // Emit a change for all existing matches if any of them changed + // TODO only emit a change for the ones that changed + bool signalDataChanged = false; + + const int oldCount = oldMatchesInCategory.count(); + const int newCount = newMatchesInCategory.count(); + + const int endOfUpdateableRange = qMin(oldCount, newCount) - 1; + + for (int i = 0; i <= endOfUpdateableRange; ++i) { + if (oldMatchesInCategory.at(i) != newMatchesInCategory.at(i)) { + signalDataChanged = true; + break; + } + } + + // Signal insertions for any new items + if (newCount > oldCount) { + beginInsertRows(categoryIdx, oldCount, newCount - 1); + oldMatchesInCategory = newMatchesInCategory; + endInsertRows(); + } else if (newCount < oldCount) { + beginRemoveRows(categoryIdx, newCount, oldCount - 1); + oldMatchesInCategory = newMatchesInCategory; + endRemoveRows(); + } else { + // Important to still update the matches, even if the count hasn't changed :) + oldMatchesInCategory = newMatchesInCategory; + } + + // Now that the source data has been updated, emit the data changes we noted down earlier + if (signalDataChanged) { + emit dataChanged(index(0, 0, categoryIdx), index(endOfUpdateableRange, 0, categoryIdx)); + } + + // Remove it from the "new" categories so in the next step we can add all genuinely new categories in one go + newCategories.remove(*it); + } + + // Finally add all the new categories + if (!newCategories.isEmpty()) { + beginInsertRows(QModelIndex(), m_categories.count(), m_categories.count() + newCategories.count() - 1); + + for (const QString &newCategory : newCategories) { + const auto matchesInNewCategory = newMatches.value(newCategory); + + m_matches[newCategory] = matchesInNewCategory; + m_categories.append(newCategory); + } + + endInsertRows(); + } + + Q_ASSERT(m_categories.count() == m_matches.count()); + + m_hasMatches = !m_matches.isEmpty(); +} + +QString RunnerResultsModel::queryString() const +{ + return m_queryString; +} + +void RunnerResultsModel::setQueryString(const QString &queryString) +{ + if (m_queryString.trimmed() == queryString.trimmed()) { + return; + } + + m_queryString = queryString; + m_hasMatches = false; + if (queryString.isEmpty()) { + clear(); + } else { + m_resetTimer.start(); + m_manager->launchQuery(queryString); + setQuerying(true); + } + emit queryStringChanged(); +} + +bool RunnerResultsModel::querying() const +{ + return m_querying; +} + +void RunnerResultsModel::setQuerying(bool querying) +{ + if (m_querying != querying) { + m_querying = querying; + emit queryingChanged(); + } +} + +QString RunnerResultsModel::runner() const +{ + return m_runner; +} + +void RunnerResultsModel::setRunner(const QString &runner) +{ + if (m_runner == runner) { + return; + } + + m_runner = runner; + m_manager->setSingleModeRunnerId(runner); + m_manager->setSingleMode(!runner.isEmpty()); + emit runnerChanged(); +} + +QString RunnerResultsModel::runnerName() const +{ + if (auto *singleRunner = m_manager->singleModeRunner()) { + return singleRunner->name(); + } + return QString(); +} + +QIcon RunnerResultsModel::runnerIcon() const +{ + if (auto *singleRunner = m_manager->singleModeRunner()) { + return singleRunner->icon(); + } + return QIcon(); +} + +void RunnerResultsModel::clear() +{ + setQuerying(false); + + beginResetModel(); + m_hasMatches = false; + m_categories.clear(); + m_matches.clear(); + m_manager->reset(); + m_manager->matchSessionComplete(); + endResetModel(); +} + +bool RunnerResultsModel::run(const QModelIndex &idx) +{ + Plasma::QueryMatch match = fetchMatch(idx); + if (!match.isValid()) { + return false; + } + + 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()) { + // 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 queryStringChangeRequested(info, editPos); + return false; + } + } + + m_manager->run(match); + return true; +} + +bool RunnerResultsModel::runAction(const QModelIndex &idx, int actionNumber) +{ + Plasma::QueryMatch match = fetchMatch(idx); + if (!match.isValid()) { + return false; + } + + const auto actions = m_manager->actionsForMatch(match); + if (actionNumber < 0 || actionNumber >= actions.count()) { + return false; + } + + QAction *action = actions.at(actionNumber); + match.setSelectedAction(action); + m_manager->run(match); + return true; +} + +int RunnerResultsModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return 1; +} + +int RunnerResultsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.column() > 0) { + return 0; + } + + if (!parent.isValid()) { // root level + return m_categories.count(); + } + + if (parent.internalId()) { + return 0; + } + + const QString category = m_categories.value(parent.row()); + return m_matches.value(category).count(); +} + +QVariant RunnerResultsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (index.internalId()) { // runner match + if (int(index.internalId() - 1) >= m_categories.count()) { + return QVariant(); + } + + Plasma::QueryMatch match = fetchMatch(index); + if (!match.isValid()) { + return QVariant(); + } + + switch (role) { + case Qt::DisplayRole: + return match.text(); + case Qt::DecorationRole: + if (!match.iconName().isEmpty()) { + return match.iconName(); + } + return match.icon(); + case ResultsModel::TypeRole: + return match.type(); + case ResultsModel::RelevanceRole: + return match.relevance(); + case ResultsModel::IdRole: + return match.id(); + case ResultsModel::EnabledRole: + return match.isEnabled(); + case ResultsModel::CategoryRole: + return match.matchCategory(); + case ResultsModel::SubtextRole: + return match.subtext(); + case ResultsModel::ActionsRole: { + const auto actions = m_manager->actionsForMatch(match); + if (actions.isEmpty()) { + return QVariantList(); + } + + QVariantList actionsList; + actionsList.reserve(actions.size()); + + for (QAction *action : actions) { + actionsList.append(QVariant::fromValue(action)); + } + + return actionsList; + } + + } + + return QVariant(); + } + + // category + if (index.row() >= m_categories.count()) { + return QVariant(); + } + + switch (role) { + case Qt::DisplayRole: + return m_categories.at(index.row()); + + // Returns the highest type/role within the group + case ResultsModel::TypeRole: { + int highestType = 0; + for (int i = 0; i < rowCount(index); ++i) { + const int type = this->index(i, 0, index).data(ResultsModel::TypeRole).toInt(); + if (type > highestType) { + highestType = type; + } + } + return highestType; + } + case ResultsModel::RelevanceRole: { + qreal highestRelevance = 0.0; + for (int i = 0; i < rowCount(index); ++i) { + const qreal relevance = this->index(i, 0, index).data(ResultsModel::RelevanceRole).toReal(); + if (relevance > highestRelevance) { + highestRelevance = relevance; + } + } + return highestRelevance; + } + + } + + return QVariant(); +} + +QModelIndex RunnerResultsModel::index(int row, int column, const QModelIndex &parent) const +{ + if (row < 0 || column != 0) { + return QModelIndex(); + } + + if (parent.isValid()) { + const QString category = m_categories.value(parent.row()); + const auto matches = m_matches.value(category); + if (row < matches.count()) { + return createIndex(row, column, parent.row() + 1); + } + + return QModelIndex(); + } + + if (row < m_categories.count()) { + return createIndex(row, column, nullptr); + } + + return QModelIndex(); +} + +QModelIndex RunnerResultsModel::parent(const QModelIndex &child) const +{ + if (child.internalId()) { + return createIndex(child.internalId() - 1, 0, nullptr); + } + + return QModelIndex(); +} + +QMimeData *RunnerResultsModel::mimeData(const QModelIndexList &indexes) const +{ + if (indexes.isEmpty()) { + return nullptr; + } + + Plasma::QueryMatch match = fetchMatch(indexes.first()); + if (!match.isValid()) { + return nullptr; + } + + return m_manager->mimeDataForMatch(match); + return nullptr; +}