/* SPDX-FileCopyrightText: 2015, 2016 Ivan Cukic SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL */ // Self #include "resultmodel.h" // Qt #include #include #include #include #include #include // STL #include #include // KDE #include #include // Local #include "cleaning.h" #include "plasma-activities-stats-logsettings.h" #include "plasmaactivities/consumer.h" #include "resultset.h" #include "resultwatcher.h" #include #include #include #include #include constexpr int s_defaultCacheSize = 50; #define QDBG qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "PlasmaActivitiesStats(" << (void *)this << ")" namespace KActivities { namespace Stats { using Common::Database; class ResultModelPrivate { public: ResultModelPrivate(Query query, const QString &clientId, ResultModel *parent) : cache(this, clientId, query.limit()) , query(query) , watcher(query) , hasMore(true) , database(Database::instance(Database::ResourcesDatabase, Database::ReadOnly)) , q(parent) { s_privates << this; } ~ResultModelPrivate() { s_privates.removeAll(this); } enum Fetch { FetchReset, // Remove old data and reload FetchReload, // Update all data FetchMore, // Load more data if there is any }; class Cache { //_ public: typedef QList Items; Cache(ResultModelPrivate *d, const QString &clientId, int limit) : d(d) , m_countLimit(limit) , m_clientId(clientId) { if (!m_clientId.isEmpty()) { m_configFile = KSharedConfig::openConfig(QStringLiteral("kactivitymanagerd-statsrc")); } } ~Cache() { } inline int size() const { return m_items.size(); } inline void setLinkedResultPosition(const QString &resourcePath, int position) { if (!m_orderingConfig.isValid()) { qCWarning(PLASMA_ACTIVITIES_STATS_LOG) << "We can not reorder the results, no clientId was specified"; return; } // Preconditions: // - cache is ordered properly, first on the user's desired order, // then on the query specified order // - the resource that needs to be moved is a linked resource, not // one that comes from the stats (there are overly many // corner-cases that need to be covered in order to support // reordering of the statistics-based resources) // - the new position for the resource is not outside of the cache auto resourcePosition = find(resourcePath); if (resourcePosition) { if (resourcePosition.index == position) { return; } if (resourcePosition.iterator->linkStatus() == ResultSet::Result::NotLinked) { return; } } // Lets make a list of linked items - we can only reorder them, // not others QStringList linkedItems; for (const ResultSet::Result &item : std::as_const(m_items)) { if (item.linkStatus() == ResultSet::Result::NotLinked) { break; } linkedItems << item.resource(); } // We have two options: // - we are planning to add an item to the desired position, // but the item is not yet in the model // - we want to move an existing item if (!resourcePosition || resourcePosition.iterator->linkStatus() == ResultSet::Result::NotLinked) { linkedItems.insert(position, resourcePath); m_fixedOrderedItems = linkedItems; } else { // We can not accept the new position to be outside // of the linked items area if (position >= linkedItems.size()) { position = linkedItems.size() - 1; } Q_ASSERT(resourcePosition.index == linkedItems.indexOf(resourcePath)); auto oldPosition = linkedItems.indexOf(resourcePath); kamd::utils::move_one(linkedItems.begin() + oldPosition, linkedItems.begin() + position); // When we change this, the cache is not valid anymore, // destinationFor will fail and we can not use it m_fixedOrderedItems = linkedItems; // We are prepared to reorder the cache d->repositionResult(resourcePosition, d->destinationFor(*resourcePosition)); } m_orderingConfig.writeEntry("kactivitiesLinkedItemsOrder", m_fixedOrderedItems); m_orderingConfig.sync(); // We need to notify others to reload for (const auto &other : std::as_const(s_privates)) { if (other != d && other->cache.m_clientId == m_clientId) { other->fetch(FetchReset); } } } inline void debug() const { for (const auto &item : m_items) { qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "Item: " << item; } } void loadOrderingConfig(const QString &activityTag) { if (!m_configFile) { qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "Nothing to load - the client id is empty"; return; } m_orderingConfig = KConfigGroup(m_configFile, QStringLiteral("ResultModel-OrderingFor-") + m_clientId + activityTag); if (m_orderingConfig.hasKey("kactivitiesLinkedItemsOrder")) { // If we have the ordering defined, use it m_fixedOrderedItems = m_orderingConfig.readEntry("kactivitiesLinkedItemsOrder", QStringList()); } else { // Otherwise, copy the order from the previous activity to this one m_orderingConfig.writeEntry("kactivitiesLinkedItemsOrder", m_fixedOrderedItems); m_orderingConfig.sync(); } } private: ResultModelPrivate *const d; QList m_items; int m_countLimit; QString m_clientId; KSharedConfig::Ptr m_configFile; KConfigGroup m_orderingConfig; QStringList m_fixedOrderedItems; friend QDebug operator<<(QDebug out, const Cache &cache) { for (const auto &item : cache.m_items) { out << "Cache item: " << item << "\n"; } return out; } public: inline const QStringList &fixedOrderedItems() const { return m_fixedOrderedItems; } //_ Fancy iterator, find, lowerBound struct FindCacheResult { Cache *const cache; Items::iterator iterator; int index; FindCacheResult(Cache *cache, Items::iterator iterator) : cache(cache) , iterator(iterator) , index(std::distance(cache->m_items.begin(), iterator)) { } operator bool() const { return iterator != cache->m_items.end(); } ResultSet::Result &operator*() const { return *iterator; } ResultSet::Result *operator->() const { return &(*iterator); } }; inline FindCacheResult find(const QString &resource) { using namespace kamd::utils::member_matcher; // Non-const iterator because the result is constructed from it return FindCacheResult(this, std::find_if(m_items.begin(), m_items.end(), member(&ResultSet::Result::resource) == resource)); } template inline FindCacheResult lowerBoundWithSkippedResource(Predicate &&lessThanPredicate) { using namespace kamd::utils::member_matcher; const int count = std::count_if(m_items.cbegin(), m_items.cend(), [&](const ResultSet::Result &result) { return lessThanPredicate(result, _); }); return FindCacheResult(this, m_items.begin() + count); // using namespace kamd::utils::member_matcher; // // const auto position = // std::lower_bound(m_items.begin(), m_items.end(), // _, std::forward(lessThanPredicate)); // // // We seem to have found the position for the item. // // The problem is that we might have found the same position // // we were previously at. Since this function is usually used // // to reposition the result, we might not be in a completely // // sorted collection, so the next item(s) could be less than us. // // We could do this with count_if, but it would be slower // // if (position >= m_items.cend() - 1) { // return FindCacheResult(this, position); // // } else if (lessThanPredicate(_, *(position + 1))) { // return FindCacheResult(this, position); // // } else { // return FindCacheResult( // this, std::lower_bound(position + 1, m_items.end(), // _, std::forward(lessThanPredicate))); // } } //^ inline void insertAt(const FindCacheResult &at, const ResultSet::Result &result) { m_items.insert(at.iterator, result); } inline void removeAt(const FindCacheResult &at) { m_items.removeAt(at.index); } inline const ResultSet::Result &operator[](int index) const { return m_items[index]; } inline void clear() { if (m_items.size() == 0) { return; } d->q->beginRemoveRows(QModelIndex(), 0, m_items.size() - 1); m_items.clear(); d->q->endRemoveRows(); } // Algorithm to calculate the edit operations to allow //_ replaceing items without model reset inline void replace(const Items &newItems, int from = 0) { using namespace kamd::utils::member_matcher; #if 0 QDBG << "======"; QDBG << "Old items {"; for (const auto &item : m_items) { QDBG << item; } QDBG << "}"; QDBG << "New items to be added at " << from << " {"; for (const auto &item : newItems) { QDBG << item; } QDBG << "}"; #endif // Based on 'The string to string correction problem // with block moves' paper by Walter F. Tichy // // In essence, it goes like this: // // Take the first element from the new list, and try to find // it in the old one. If you can not find it, it is a new item // item - send the 'inserted' event. // If you did find it, test whether the following items also // match. This detects blocks of items that have moved. // // In this example, we find 'b', and then detect the rest of the // moved block 'b' 'c' 'd' // // Old items: a[b c d]e f g // ^ // / // New items: [b c d]a f g // // After processing one block, just repeat until the end of the // new list is reached. // // Then remove all remaining elements from the old list. // // The main addition here compared to the original papers is that // our 'strings' can not hold two instances of the same element, // and that we support updating from arbitrary position. auto newBlockStart = newItems.cbegin(); // How many items should we add? // This should remove the need for post-replace-trimming // in the case where somebody called this with too much new items. const int maxToReplace = m_countLimit - from; if (maxToReplace <= 0) { return; } const auto newItemsEnd = newItems.size() <= maxToReplace ? newItems.cend() : newItems.cbegin() + maxToReplace; // Finding the blocks until we reach the end of the newItems list // // from = 4 // Old items: X Y Z U a b c d e f g // ^ oldBlockStart points to the first element // of the currently processed block in the old list // // New items: _ _ _ _ b c d a f g // ^ newBlockStartIndex is the index of the first // element of the block that is currently being // processed (with 'from' offset) while (newBlockStart != newItemsEnd) { const int newBlockStartIndex = from + std::distance(newItems.cbegin(), newBlockStart); const auto oldBlockStart = std::find_if(m_items.begin() + from, m_items.end(), member(&ResultSet::Result::resource) == newBlockStart->resource()); if (oldBlockStart == m_items.end()) { // This item was not found in the old cache, so we are // inserting a new item at the same position it had in // the newItems array d->q->beginInsertRows(QModelIndex(), newBlockStartIndex, newBlockStartIndex); m_items.insert(newBlockStartIndex, *newBlockStart); d->q->endInsertRows(); // This block contained only one item, move on to find // the next block - it starts from the next item ++newBlockStart; } else { // We are searching for a block of matching items. // This is a reimplementation of std::mismatch that // accepts two complete ranges that is available only // since C++14, so we can not use it. auto newBlockEnd = newBlockStart; auto oldBlockEnd = oldBlockStart; while (newBlockEnd != newItemsEnd && oldBlockEnd != m_items.end() && newBlockEnd->resource() == oldBlockEnd->resource()) { ++newBlockEnd; ++oldBlockEnd; } // We have found matching blocks // [newBlockStart, newBlockEnd) and [oldBlockStart, newBlockEnd) const int oldBlockStartIndex = std::distance(m_items.begin() + from, oldBlockStart); const int blockSize = std::distance(oldBlockStart, oldBlockEnd); if (oldBlockStartIndex != newBlockStartIndex) { // If these blocks do not have the same start, // we need to send the move event. // Note: If there is a crash here, it means we // are getting a bad query which has duplicate // results d->q->beginMoveRows(QModelIndex(), oldBlockStartIndex, oldBlockStartIndex + blockSize - 1, QModelIndex(), newBlockStartIndex); // Moving the items from the old location to the new one kamd::utils::slide(oldBlockStart, oldBlockEnd, m_items.begin() + newBlockStartIndex); d->q->endMoveRows(); } // Skip all the items in this block, and continue with // the search newBlockStart = newBlockEnd; } } // We have avoided the need for trimming for the most part, // but if the newItems list was shorter than needed, we still // need to trim the rest. trim(from + newItems.size()); // Check whether we got an item representing a non-existent file, // if so, schedule its removal from the database // we want to do this async so that we don't block QPointer model{d->q}; std::thread([model, newItems] { QList missingResources; for (const auto &item : newItems) { // QFile.exists() can be incredibly slow (eg. if resource is on remote filesystem) if (item.resource().startsWith(QLatin1Char('/')) && !QFile(item.resource()).exists()) { missingResources << item.resource(); } } if (missingResources.empty()) { return; } QTimer::singleShot(0, model, [missingResources, model] { if (model) { model->forgetResources(missingResources); } }); }).detach(); } //^ inline void trim() { trim(m_countLimit); } inline void trim(int limit) { if (m_items.size() <= limit) { return; } // Example: // limit is 5, // current cache (0, 1, 2, 3, 4, 5, 6, 7), size = 8 // We need to delete from 5 to 7 d->q->beginRemoveRows(QModelIndex(), limit, m_items.size() - 1); m_items.erase(m_items.begin() + limit, m_items.end()); d->q->endRemoveRows(); } } cache; //^ struct FixedItemsLessThan { //_ Compartor that orders the linked items by user-specified order typedef kamd::utils::member_matcher::placeholder placeholder; enum Ordering { PartialOrdering, FullOrdering, }; FixedItemsLessThan(Ordering ordering, const Cache &cache, const QString &matchResource = QString()) : cache(cache) , matchResource(matchResource) , ordering(ordering) { } bool lessThan(const QString &leftResource, const QString &rightResource) const { const auto fixedOrderedItems = cache.fixedOrderedItems(); const auto indexLeft = fixedOrderedItems.indexOf(leftResource); const auto indexRight = fixedOrderedItems.indexOf(rightResource); const bool hasLeft = indexLeft != -1; const bool hasRight = indexRight != -1; return (hasLeft && !hasRight) ? true : (!hasLeft && hasRight) ? false : (hasLeft && hasRight) ? indexLeft < indexRight : (ordering == PartialOrdering ? false : leftResource < rightResource); } template bool operator()(const T &left, placeholder) const { return lessThan(left.resource(), matchResource); } template bool operator()(placeholder, const T &right) const { return lessThan(matchResource, right.resource()); } template bool operator()(const T &left, const V &right) const { return lessThan(left.resource(), right.resource()); } const Cache &cache; const QString matchResource; Ordering ordering; //^ }; inline Cache::FindCacheResult destinationFor(const ResultSet::Result &result) { using namespace kamd::utils::member_matcher; using namespace Terms; const auto resource = result.resource(); const auto score = result.score(); const auto firstUpdate = result.firstUpdate(); const auto lastUpdate = result.lastUpdate(); const auto linkStatus = result.linkStatus(); #define FIXED_ITEMS_LESS_THAN FixedItemsLessThan(FixedItemsLessThan::PartialOrdering, cache, resource) #define ORDER_BY(Field) member(&ResultSet::Result::Field) > Field #define ORDER_BY_FULL(Field) \ (query.selection() == Terms::AllResources \ ? cache.lowerBoundWithSkippedResource(FIXED_ITEMS_LESS_THAN && ORDER_BY(linkStatus) && ORDER_BY(Field) && ORDER_BY(resource)) \ : cache.lowerBoundWithSkippedResource(FIXED_ITEMS_LESS_THAN && ORDER_BY(Field) && ORDER_BY(resource))) const auto destination = query.ordering() == HighScoredFirst ? ORDER_BY_FULL(score) : query.ordering() == RecentlyUsedFirst ? ORDER_BY_FULL(lastUpdate) : query.ordering() == RecentlyCreatedFirst ? ORDER_BY_FULL(firstUpdate) : /* otherwise */ ORDER_BY_FULL(resource); #undef ORDER_BY #undef ORDER_BY_FULL #undef FIXED_ITEMS_LESS_THAN return destination; } inline void removeResult(const Cache::FindCacheResult &result) { q->beginRemoveRows(QModelIndex(), result.index, result.index); cache.removeAt(result); q->endRemoveRows(); if (query.selection() != Terms::LinkedResources) { fetch(cache.size(), 1); } } inline void repositionResult(const Cache::FindCacheResult &result, const Cache::FindCacheResult &destination) { // We already have the resource in the cache // So, it is the time for a reshuffle const int oldPosition = result.index; int position = destination.index; Q_EMIT q->dataChanged(q->index(oldPosition), q->index(oldPosition)); if (oldPosition == position) { return; } if (position > oldPosition) { position++; } bool moving = q->beginMoveRows(QModelIndex(), oldPosition, oldPosition, QModelIndex(), position); kamd::utils::move_one(result.iterator, destination.iterator); if (moving) { q->endMoveRows(); } } void reload() { fetch(FetchReload); } void init() { using namespace std::placeholders; QObject::connect(&watcher, &ResultWatcher::resultScoreUpdated, q, std::bind(&ResultModelPrivate::onResultScoreUpdated, this, _1, _2, _3, _4)); QObject::connect(&watcher, &ResultWatcher::resultRemoved, q, std::bind(&ResultModelPrivate::onResultRemoved, this, _1)); QObject::connect(&watcher, &ResultWatcher::resultLinked, q, std::bind(&ResultModelPrivate::onResultLinked, this, _1)); QObject::connect(&watcher, &ResultWatcher::resultUnlinked, q, std::bind(&ResultModelPrivate::onResultUnlinked, this, _1)); QObject::connect(&watcher, &ResultWatcher::resourceTitleChanged, q, std::bind(&ResultModelPrivate::onResourceTitleChanged, this, _1, _2)); QObject::connect(&watcher, &ResultWatcher::resourceMimetypeChanged, q, std::bind(&ResultModelPrivate::onResourceMimetypeChanged, this, _1, _2)); QObject::connect(&watcher, &ResultWatcher::resultsInvalidated, q, std::bind(&ResultModelPrivate::reload, this)); if (query.activities().contains(CURRENT_ACTIVITY_TAG)) { QObject::connect(&activities, &KActivities::Consumer::currentActivityChanged, q, std::bind(&ResultModelPrivate::onCurrentActivityChanged, this, _1)); } fetch(FetchReset); } void fetch(const int from, int count) { using namespace Terms; if (from + count > query.limit()) { count = query.limit() - from; } if (count <= 0) { return; } // In order to see whether there are more results, we need to pass // the count increased by one ResultSet results(query | Offset(from) | Limit(count + 1)); auto it = results.begin(); Cache::Items newItems; while (count-- > 0 && it != results.end()) { newItems << *it; ++it; } hasMore = (it != results.end()); // We need to sort the new items for the linked resources // user-defined reordering. This needs only to be a partial sort, // the main sorting is done by sqlite if (query.selection() != Terms::UsedResources) { std::stable_sort(newItems.begin(), newItems.end(), FixedItemsLessThan(FixedItemsLessThan::PartialOrdering, cache)); } cache.replace(newItems, from); } void fetch(Fetch mode) { if (mode == FetchReset) { // Removing the previously cached data // and loading all from scratch cache.clear(); const QString activityTag = query.activities().contains(CURRENT_ACTIVITY_TAG) // ? (QStringLiteral("-ForActivity-") + activities.currentActivity()) : QStringLiteral("-ForAllActivities"); cache.loadOrderingConfig(activityTag); // If the user has requested less than 50 entries, only fetch those. If more, they should be fetched in subsequent batches fetch(0, qMin(s_defaultCacheSize, query.limit())); } else if (mode == FetchReload) { if (cache.size() > s_defaultCacheSize) { // If the cache is big, we are pretending // we were asked to reset the model fetch(FetchReset); } else { // We are only updating the currently // cached items, nothing more fetch(0, cache.size()); } } else { // FetchMore // Load a new batch of data fetch(cache.size(), s_defaultCacheSize); } } void onResultScoreUpdated(const QString &resource, double score, uint lastUpdate, uint firstUpdate) { QDBG << "ResultModelPrivate::onResultScoreUpdated " << "result added:" << resource << "score:" << score << "last:" << lastUpdate << "first:" << firstUpdate; // This can also be called when the resource score // has been updated, so we need to check whether // we already have it in the cache const auto result = cache.find(resource); ResultSet::Result::LinkStatus linkStatus = result ? result->linkStatus() : query.selection() != Terms::UsedResources ? ResultSet::Result::Unknown : query.selection() != Terms::LinkedResources ? ResultSet::Result::Linked : ResultSet::Result::NotLinked; if (result) { // We are only updating a result we already had, // lets fill out the data and send the update signal. // Move it if necessary. auto &item = *result.iterator; item.setScore(score); item.setLinkStatus(linkStatus); item.setLastUpdate(lastUpdate); item.setFirstUpdate(firstUpdate); repositionResult(result, destinationFor(item)); } else { // We do not have the resource in the cache, // lets fill out the data and insert it // at the desired position ResultSet::Result result; result.setResource(resource); result.setTitle(QStringLiteral(" ")); result.setMimetype(QStringLiteral(" ")); fillTitleAndMimetype(result); result.setScore(score); result.setLinkStatus(linkStatus); result.setLastUpdate(lastUpdate); result.setFirstUpdate(firstUpdate); const auto destination = destinationFor(result); q->beginInsertRows(QModelIndex(), destination.index, destination.index); cache.insertAt(destination, result); q->endInsertRows(); cache.trim(); } } void onResultRemoved(const QString &resource) { const auto result = cache.find(resource); if (!result) { return; } if (query.selection() == Terms::UsedResources || result->linkStatus() != ResultSet::Result::Linked) { removeResult(result); } } void onResultLinked(const QString &resource) { if (query.selection() != Terms::UsedResources) { onResultScoreUpdated(resource, 0, 0, 0); } } void onResultUnlinked(const QString &resource) { const auto result = cache.find(resource); if (!result) { return; } if (query.selection() == Terms::LinkedResources) { removeResult(result); } else if (query.selection() == Terms::AllResources) { // When the result is unlinked, it might go away or not // depending on its previous usage reload(); } } Query query; ResultWatcher watcher; bool hasMore; KActivities::Consumer activities; Common::Database::Ptr database; //_ Title and mimetype functions void fillTitleAndMimetype(ResultSet::Result &result) { if (!database) { return; } auto query = database->execQuery(QStringLiteral("SELECT " "title, mimetype " "FROM " "ResourceInfo " "WHERE " "targettedResource = '") + result.resource() + QStringLiteral("'")); // Only one item at most for (const auto &item : query) { result.setTitle(item[QStringLiteral("title")].toString()); result.setMimetype(item[QStringLiteral("mimetype")].toString()); } } void onResourceTitleChanged(const QString &resource, const QString &title) { const auto result = cache.find(resource); if (!result) { return; } result->setTitle(title); Q_EMIT q->dataChanged(q->index(result.index), q->index(result.index)); } void onResourceMimetypeChanged(const QString &resource, const QString &mimetype) { // TODO: This can add or remove items from the model const auto result = cache.find(resource); if (!result) { return; } result->setMimetype(mimetype); Q_EMIT q->dataChanged(q->index(result.index), q->index(result.index)); } //^ void onCurrentActivityChanged(const QString &activity) { Q_UNUSED(activity); // If the current activity has changed, and // the query lists items for the ':current' one, // reset the model (not a simple refresh this time) if (query.activities().contains(CURRENT_ACTIVITY_TAG)) { fetch(FetchReset); } } private: ResultModel *const q; static QList s_privates; }; QList ResultModelPrivate::s_privates; ResultModel::ResultModel(Query query, QObject *parent) : QAbstractListModel(parent) , d(new ResultModelPrivate(query, QString(), this)) { d->init(); } ResultModel::ResultModel(Query query, const QString &clientId, QObject *parent) : QAbstractListModel(parent) , d(new ResultModelPrivate(query, clientId, this)) { d->init(); } ResultModel::~ResultModel() { delete d; } QHash ResultModel::roleNames() const { return { {ResourceRole, "resource"}, {TitleRole, "title"}, {ScoreRole, "score"}, {FirstUpdateRole, "created"}, {LastUpdateRole, "modified"}, {LinkStatusRole, "linkStatus"}, {LinkedActivitiesRole, "linkedActivities"}, {MimeType, "mimeType"}, }; } QVariant ResultModel::data(const QModelIndex &item, int role) const { const auto row = item.row(); if (row < 0 || row >= d->cache.size()) { return QVariant(); } const auto &result = d->cache[row]; return role == Qt::DisplayRole ? QString(result.title() + QStringLiteral(" ") + result.resource() + QStringLiteral(" - ") + QString::number(result.linkStatus()) + QStringLiteral(" - ") + QString::number(result.score())) : role == ResourceRole ? result.resource() : role == TitleRole ? result.title() : role == ScoreRole ? result.score() : role == FirstUpdateRole ? result.firstUpdate() : role == LastUpdateRole ? result.lastUpdate() : role == LinkStatusRole ? result.linkStatus() : role == LinkedActivitiesRole ? result.linkedActivities() : role == MimeType ? result.mimetype() : role == Agent ? result.agent() : QVariant(); } QVariant ResultModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(section); Q_UNUSED(orientation); Q_UNUSED(role); return QVariant(); } int ResultModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : d->cache.size(); } void ResultModel::fetchMore(const QModelIndex &parent) { if (parent.isValid()) { return; } d->fetch(ResultModelPrivate::FetchMore); } bool ResultModel::canFetchMore(const QModelIndex &parent) const { return parent.isValid() ? false : d->cache.size() >= d->query.limit() ? false : d->hasMore; } void ResultModel::forgetResources(const QList &resources) { const auto lstActivities = d->query.activities(); for (const QString &activity : lstActivities) { const auto lstAgents = d->query.agents(); for (const QString &agent : lstAgents) { for (const QString &resource : resources) { Stats::forgetResource(activity, agent == CURRENT_AGENT_TAG ? QCoreApplication::applicationName() : agent, resource); } } } } void ResultModel::forgetResource(const QString &resource) { ResultModel::forgetResources({resource}); } void ResultModel::forgetResource(int row) { if (row >= d->cache.size()) { return; } const auto lstActivities = d->query.activities(); for (const QString &activity : lstActivities) { const auto lstAgents = d->query.agents(); for (const QString &agent : lstAgents) { Stats::forgetResource(activity, agent == CURRENT_AGENT_TAG ? QCoreApplication::applicationName() : agent, d->cache[row].resource()); } } } void ResultModel::forgetAllResources() { Stats::forgetResources(d->query); } void ResultModel::setResultPosition(const QString &resource, int position) { d->cache.setLinkedResultPosition(resource, position); } void ResultModel::sortItems(Qt::SortOrder sortOrder) { // TODO Q_UNUSED(sortOrder); } void ResultModel::linkToActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent) { d->watcher.linkToActivity(resource, activity, agent); } void ResultModel::unlinkFromActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent) { d->watcher.unlinkFromActivity(resource, activity, agent); } } // namespace Stats } // namespace KActivities #include "moc_resultmodel.cpp"