File indexing completed on 2024-05-12 05:29:04

0001 /*
0002  *   SPDX-FileCopyrightText: 2010 Jonathan Thomas <echidnaman@kubuntu.org>
0003  *   SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0004  *
0005  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006  */
0007 
0008 #include "ResourcesProxyModel.h"
0009 
0010 #include "libdiscover_debug.h"
0011 #include <QMetaProperty>
0012 #include <cmath>
0013 #include <qnamespace.h>
0014 #include <utils.h>
0015 
0016 #include "ResourcesModel.h"
0017 #include <Category/CategoryModel.h>
0018 #include <KLocalizedString>
0019 #include <ReviewsBackend/Rating.h>
0020 #include <Transaction/TransactionModel.h>
0021 
0022 const QHash<int, QByteArray> ResourcesProxyModel::s_roles = {{NameRole, "name"},
0023                                                              {IconRole, "icon"},
0024                                                              {CommentRole, "comment"},
0025                                                              {StateRole, "state"},
0026                                                              {RatingRole, "rating"},
0027                                                              {RatingPointsRole, "ratingPoints"},
0028                                                              {RatingCountRole, "ratingCount"},
0029                                                              {SortableRatingRole, "sortableRating"},
0030                                                              {SearchRelevanceRole, "searchRelevance"},
0031                                                              {InstalledRole, "isInstalled"},
0032                                                              {ApplicationRole, "application"},
0033                                                              {OriginRole, "origin"},
0034                                                              {DisplayOriginRole, "displayOrigin"},
0035                                                              {CanUpgrade, "canUpgrade"},
0036                                                              {PackageNameRole, "packageName"},
0037                                                              {CategoryRole, "category"},
0038                                                              {SectionRole, "section"},
0039                                                              {MimeTypes, "mimetypes"},
0040                                                              {LongDescriptionRole, "longDescription"},
0041                                                              {SourceIconRole, "sourceIcon"},
0042                                                              {SizeRole, "size"},
0043                                                              {ReleaseDateRole, "releaseDate"}};
0044 
0045 int levenshteinDistance(const QString &source, const QString &target)
0046 {
0047     if (source == target) {
0048         return 0;
0049     }
0050 
0051     // Do a case insensitive version of it
0052     const QString &sourceUp = source.toUpper();
0053     const QString &targetUp = target.toUpper();
0054 
0055     if (sourceUp == targetUp) {
0056         return 0;
0057     }
0058 
0059     const int sourceCount = sourceUp.size();
0060     const int targetCount = targetUp.size();
0061 
0062     if (sourceUp.isEmpty()) {
0063         return targetCount;
0064     }
0065 
0066     if (targetUp.isEmpty()) {
0067         return sourceCount;
0068     }
0069 
0070     if (sourceCount > targetCount) {
0071         return levenshteinDistance(targetUp, sourceUp);
0072     }
0073 
0074     QVector<int> column;
0075     column.fill(0, targetCount + 1);
0076     QVector<int> previousColumn;
0077     previousColumn.reserve(targetCount + 1);
0078     for (int i = 0; i < targetCount + 1; i++) {
0079         previousColumn.append(i);
0080     }
0081 
0082     for (int i = 0; i < sourceCount; i++) {
0083         column[0] = i + 1;
0084         for (int j = 0; j < targetCount; j++) {
0085             column[j + 1] = std::min({1 + column.at(j), 1 + previousColumn.at(1 + j), previousColumn.at(j) + ((sourceUp.at(i) == targetUp.at(j)) ? 0 : 1)});
0086         }
0087         column.swap(previousColumn);
0088     }
0089 
0090     return previousColumn.at(targetCount);
0091 }
0092 
0093 ResourcesProxyModel::ResourcesProxyModel(QObject *parent)
0094     : QAbstractListModel(parent)
0095     , m_sortRole(NameRole)
0096     , m_sortOrder(Qt::AscendingOrder)
0097     , m_currentStream(nullptr)
0098 {
0099     // new QAbstractItemModelTester(this, this);
0100 
0101     connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, this, &ResourcesProxyModel::invalidateFilter);
0102     connect(ResourcesModel::global(), &ResourcesModel::backendDataChanged, this, &ResourcesProxyModel::refreshBackend);
0103     connect(ResourcesModel::global(), &ResourcesModel::resourceDataChanged, this, &ResourcesProxyModel::refreshResource);
0104     connect(ResourcesModel::global(), &ResourcesModel::resourceRemoved, this, &ResourcesProxyModel::removeResource);
0105 
0106     m_countTimer.setInterval(10);
0107     m_countTimer.setSingleShot(true);
0108     connect(&m_countTimer, &QTimer::timeout, this, &ResourcesProxyModel::countChanged);
0109 
0110     connect(this, &QAbstractItemModel::modelReset, &m_countTimer, qOverload<>(&QTimer::start));
0111     connect(this, &QAbstractItemModel::rowsInserted, &m_countTimer, qOverload<>(&QTimer::start));
0112     connect(this, &QAbstractItemModel::rowsRemoved, &m_countTimer, qOverload<>(&QTimer::start));
0113 
0114     connect(this, &ResourcesProxyModel::busyChanged, &m_countTimer, qOverload<>(&QTimer::start));
0115 }
0116 
0117 void ResourcesProxyModel::componentComplete()
0118 {
0119     m_setup = true;
0120     invalidateFilter();
0121 }
0122 
0123 QHash<int, QByteArray> ResourcesProxyModel::roleNames() const
0124 {
0125     return s_roles;
0126 }
0127 
0128 void ResourcesProxyModel::setSortRole(Roles sortRole)
0129 {
0130     if (sortRole != m_sortRole) {
0131         Q_ASSERT(roleNames().contains(sortRole));
0132 
0133         m_sortRole = sortRole;
0134         Q_EMIT sortRoleChanged(sortRole);
0135         invalidateSorting();
0136     }
0137 }
0138 
0139 void ResourcesProxyModel::setSortOrder(Qt::SortOrder sortOrder)
0140 {
0141     if (sortOrder != m_sortOrder) {
0142         m_sortOrder = sortOrder;
0143         Q_EMIT sortOrderChanged(sortOrder);
0144         invalidateSorting();
0145     }
0146 }
0147 
0148 void ResourcesProxyModel::setSearch(const QString &_searchText)
0149 {
0150     // 1-character searches are painfully slow. >= 2 chars are fine, though
0151     const QString searchText = _searchText.size() <= 1 ? QString() : _searchText;
0152 
0153     const bool diff = searchText != m_filters.search;
0154 
0155     if (diff) {
0156         m_filters.search = searchText;
0157         invalidateFilter();
0158         Q_EMIT searchChanged(m_filters.search);
0159     }
0160 }
0161 
0162 void ResourcesProxyModel::removeDuplicates(QVector<StreamResult> &resources)
0163 {
0164     const auto cab = ResourcesModel::global()->currentApplicationBackend();
0165     QHash<QString, QString> aliases;
0166     QHash<QString, QVector<StreamResult>::iterator> storedIds;
0167     for (auto it = m_displayedResources.begin(); it != m_displayedResources.end(); ++it) {
0168         const auto appstreamid = it->resource->appstreamId();
0169         if (appstreamid.isEmpty()) {
0170             continue;
0171         }
0172         auto at = storedIds.find(appstreamid);
0173         if (at == storedIds.end()) {
0174             storedIds[appstreamid] = it;
0175         } else {
0176             qCWarning(LIBDISCOVER_LOG) << "We should have sanitized the displayed resources. There is a bug";
0177             Q_UNREACHABLE();
0178         }
0179 
0180         const auto alts = it->resource->alternativeAppstreamIds();
0181         for (const auto &alias : alts) {
0182             aliases[alias] = appstreamid;
0183         }
0184     }
0185 
0186     QHash<QString, QVector<StreamResult>::iterator> ids;
0187     for (auto it = resources.begin(); it != resources.end();) {
0188         const auto appstreamid = it->resource->appstreamId();
0189         if (appstreamid.isEmpty()) {
0190             ++it;
0191             continue;
0192         }
0193         auto at = storedIds.find(appstreamid);
0194         if (at == storedIds.end()) {
0195             auto aliased = aliases.constFind(appstreamid);
0196             if (aliased != aliases.constEnd()) {
0197                 at = storedIds.find(aliased.value());
0198             }
0199         }
0200 
0201         if (at == storedIds.end()) {
0202             const auto alts = it->resource->alternativeAppstreamIds();
0203             for (const auto &alt : alts) {
0204                 at = storedIds.find(alt);
0205                 if (at == storedIds.end())
0206                     break;
0207 
0208                 auto aliased = aliases.constFind(alt);
0209                 if (aliased != aliases.constEnd()) {
0210                     at = storedIds.find(aliased.value());
0211                     if (at != storedIds.end())
0212                         break;
0213                 }
0214             }
0215         }
0216         if (at == storedIds.end()) {
0217             auto at = ids.find(appstreamid);
0218             if (at == ids.end()) {
0219                 auto aliased = aliases.constFind(appstreamid);
0220                 if (aliased != aliases.constEnd()) {
0221                     at = ids.find(aliased.value());
0222                 }
0223             }
0224             if (at == ids.end()) {
0225                 const auto alts = it->resource->alternativeAppstreamIds();
0226                 for (const auto &alt : alts) {
0227                     at = ids.find(alt);
0228                     if (at != ids.end())
0229                         break;
0230 
0231                     auto aliased = aliases.constFind(appstreamid);
0232                     if (aliased != aliases.constEnd()) {
0233                         at = ids.find(aliased.value());
0234                         if (at != ids.end())
0235                             break;
0236                     }
0237                 }
0238             }
0239             if (at == ids.end()) {
0240                 ids[appstreamid] = it;
0241                 const auto alts = it->resource->alternativeAppstreamIds();
0242                 for (const auto &alias : alts) {
0243                     aliases[alias] = appstreamid;
0244                 }
0245                 ++it;
0246             } else {
0247                 if (it->resource->backend() == cab && it->resource->backend() != (**at).resource->backend()) {
0248                     qSwap(*it, **at);
0249                 }
0250                 it = resources.erase(it);
0251             }
0252         } else {
0253             if (it->resource->backend() == cab) {
0254                 **at = *it;
0255                 auto pos = index(*at - m_displayedResources.begin(), 0);
0256                 Q_EMIT dataChanged(pos, pos);
0257             }
0258             it = resources.erase(it);
0259         }
0260     }
0261 }
0262 
0263 void ResourcesProxyModel::addResources(const QVector<StreamResult> &_res)
0264 {
0265     auto res = _res;
0266     m_filters.filterJustInCase(res);
0267 
0268     if (res.isEmpty())
0269         return;
0270 
0271     std::sort(res.begin(), res.end(), [this](const auto &left, const auto &right) {
0272         return orderedLessThan(left, right);
0273     });
0274 
0275     sortedInsertion(res);
0276     fetchSubcategories();
0277 }
0278 
0279 void ResourcesProxyModel::invalidateSorting()
0280 {
0281     if (m_displayedResources.isEmpty())
0282         return;
0283 
0284     beginResetModel();
0285     std::sort(m_displayedResources.begin(), m_displayedResources.end(), [this](const auto &left, const auto &right) {
0286         return orderedLessThan(left, right);
0287     });
0288     endResetModel();
0289 }
0290 
0291 QString ResourcesProxyModel::lastSearch() const
0292 {
0293     return m_filters.search;
0294 }
0295 
0296 void ResourcesProxyModel::setOriginFilter(const QString &origin)
0297 {
0298     if (origin == m_filters.origin)
0299         return;
0300 
0301     m_filters.origin = origin;
0302 
0303     invalidateFilter();
0304 }
0305 
0306 QString ResourcesProxyModel::originFilter() const
0307 {
0308     return m_filters.origin;
0309 }
0310 
0311 QString ResourcesProxyModel::filteredCategoryName() const
0312 {
0313     return m_categoryName;
0314 }
0315 
0316 void ResourcesProxyModel::setFilteredCategoryName(const QString &cat)
0317 {
0318     if (cat == m_categoryName)
0319         return;
0320 
0321     m_categoryName = cat;
0322 
0323     auto category = CategoryModel::global()->findCategoryByName(cat);
0324     if (category) {
0325         setFiltersFromCategory(category);
0326     } else {
0327         qDebug() << "looking up wrong category or too early" << m_categoryName;
0328         auto f = [this, cat] {
0329             auto category = CategoryModel::global()->findCategoryByName(cat);
0330             setFiltersFromCategory(category);
0331         };
0332         auto one = new OneTimeAction(f, this);
0333         connect(CategoryModel::global(), &CategoryModel::rootCategoriesChanged, one, &OneTimeAction::trigger);
0334     }
0335 }
0336 
0337 void ResourcesProxyModel::setFiltersFromCategory(Category *category)
0338 {
0339     if (category == m_filters.category)
0340         return;
0341 
0342     m_filters.category = category;
0343     invalidateFilter();
0344     Q_EMIT categoryChanged();
0345 }
0346 
0347 void ResourcesProxyModel::fetchSubcategories()
0348 {
0349     auto cats = kToSet(m_filters.category ? m_filters.category->subCategories() : CategoryModel::global()->rootCategories());
0350 
0351     const int count = rowCount();
0352     QSet<Category *> done;
0353     for (int i = 0; i < count && !cats.isEmpty(); ++i) {
0354         AbstractResource *res = m_displayedResources[i].resource;
0355         const auto found = res->categoryObjects(kSetToVector(cats));
0356         done.unite(found);
0357         cats.subtract(found);
0358     }
0359 
0360     const QVariantList ret = kTransform<QVariantList>(done, [](Category *cat) {
0361         return QVariant::fromValue<QObject *>(cat);
0362     });
0363     if (ret != m_subcategories) {
0364         m_subcategories = ret;
0365         Q_EMIT subcategoriesChanged(m_subcategories);
0366     }
0367 }
0368 
0369 QVariantList ResourcesProxyModel::subcategories() const
0370 {
0371     return m_subcategories;
0372 }
0373 
0374 void ResourcesProxyModel::invalidateFilter()
0375 {
0376     if (!m_setup || ResourcesModel::global()->backends().isEmpty()) {
0377         return;
0378     }
0379 
0380     if (!m_categoryName.isEmpty() && m_filters.category == nullptr) {
0381         return;
0382     }
0383 
0384     if (m_currentStream) {
0385         qCWarning(LIBDISCOVER_LOG) << "last stream isn't over yet" << m_filters << this;
0386         delete m_currentStream;
0387     }
0388 
0389     m_currentStream = m_filters.backend ? m_filters.backend->search(m_filters) : ResourcesModel::global()->search(m_filters);
0390     Q_EMIT busyChanged();
0391 
0392     if (!m_displayedResources.isEmpty()) {
0393         beginResetModel();
0394         m_displayedResources.clear();
0395         endResetModel();
0396     }
0397 
0398     connect(m_currentStream, &ResultsStream::resourcesFound, this, &ResourcesProxyModel::addResources);
0399     connect(m_currentStream, &ResultsStream::destroyed, this, [this]() {
0400         m_currentStream = nullptr;
0401         Q_EMIT busyChanged();
0402     });
0403 }
0404 
0405 int ResourcesProxyModel::rowCount(const QModelIndex &parent) const
0406 {
0407     return parent.isValid() ? 0 : m_displayedResources.count();
0408 }
0409 
0410 // This comparator takes m_sortRole and m_sortOrder into account. It has a
0411 // fallback mechanism to use secondary sort role and order.
0412 bool ResourcesProxyModel::orderedLessThan(const StreamResult &left, const StreamResult &right) const
0413 {
0414     // (role, order) pair
0415     using SortCombination = std::pair<Roles, Qt::SortOrder>;
0416     const std::array<SortCombination, 2> sortFallbackChain = {{
0417         {m_sortRole, m_sortOrder},
0418         {NameRole, Qt::AscendingOrder},
0419     }};
0420 
0421     for (const auto &[role, order] : sortFallbackChain) {
0422         QVariant leftValue = roleToOrderedValue(left, role);
0423         QVariant rightValue = roleToOrderedValue(right, role);
0424 
0425         if (leftValue == rightValue) {
0426             continue;
0427         }
0428 
0429         const auto result = QVariant::compare(leftValue, rightValue);
0430 
0431         // Should not happen, but it's better to skip than assert
0432         if (!(result == QPartialOrdering::Less || result == QPartialOrdering::Greater)) {
0433             continue;
0434         }
0435 
0436         // Yes, there is a shorter but incomprehensible way of rewriting this
0437         return (order == Qt::AscendingOrder) ? (result == QPartialOrdering::Less) : (result == QPartialOrdering::Greater);
0438     }
0439 
0440     // They compared equal, so it is definitely not a "less than" relation
0441     return false;
0442 }
0443 
0444 Category *ResourcesProxyModel::filteredCategory() const
0445 {
0446     return m_filters.category;
0447 }
0448 
0449 void ResourcesProxyModel::setStateFilter(AbstractResource::State s)
0450 {
0451     if (s != m_filters.state) {
0452         m_filters.state = s;
0453         invalidateFilter();
0454         Q_EMIT stateFilterChanged();
0455     }
0456 }
0457 
0458 AbstractResource::State ResourcesProxyModel::stateFilter() const
0459 {
0460     return m_filters.state;
0461 }
0462 
0463 QString ResourcesProxyModel::mimeTypeFilter() const
0464 {
0465     return m_filters.mimetype;
0466 }
0467 
0468 void ResourcesProxyModel::setMimeTypeFilter(const QString &mime)
0469 {
0470     if (m_filters.mimetype != mime) {
0471         m_filters.mimetype = mime;
0472         invalidateFilter();
0473     }
0474 }
0475 
0476 QString ResourcesProxyModel::extends() const
0477 {
0478     return m_filters.extends;
0479 }
0480 
0481 void ResourcesProxyModel::setExtends(const QString &extends)
0482 {
0483     if (m_filters.extends != extends) {
0484         m_filters.extends = extends;
0485         invalidateFilter();
0486     }
0487 }
0488 
0489 void ResourcesProxyModel::setFilterMinimumState(bool filterMinimumState)
0490 {
0491     if (filterMinimumState != m_filters.filterMinimumState) {
0492         m_filters.filterMinimumState = filterMinimumState;
0493         invalidateFilter();
0494         Q_EMIT filterMinimumStateChanged(m_filters.filterMinimumState);
0495     }
0496 }
0497 
0498 bool ResourcesProxyModel::filterMinimumState() const
0499 {
0500     return m_filters.filterMinimumState;
0501 }
0502 
0503 QUrl ResourcesProxyModel::resourcesUrl() const
0504 {
0505     return m_filters.resourceUrl;
0506 }
0507 
0508 void ResourcesProxyModel::setResourcesUrl(const QUrl &resourcesUrl)
0509 {
0510     if (m_filters.resourceUrl != resourcesUrl) {
0511         m_filters.resourceUrl = resourcesUrl;
0512         invalidateFilter();
0513     }
0514 }
0515 
0516 bool ResourcesProxyModel::allBackends() const
0517 {
0518     return m_filters.allBackends;
0519 }
0520 
0521 void ResourcesProxyModel::setAllBackends(bool allBackends)
0522 {
0523     m_filters.allBackends = allBackends;
0524 }
0525 
0526 AbstractResourcesBackend *ResourcesProxyModel::backendFilter() const
0527 {
0528     return m_filters.backend;
0529 }
0530 
0531 void ResourcesProxyModel::setBackendFilter(AbstractResourcesBackend *filtered)
0532 {
0533     m_filters.backend = filtered;
0534 }
0535 
0536 QVariant ResourcesProxyModel::data(const QModelIndex &index, int role) const
0537 {
0538     if (!index.isValid()) {
0539         return QVariant();
0540     }
0541     const auto result = m_displayedResources[index.row()];
0542     return roleToValue(result, role);
0543 }
0544 
0545 QVariant ResourcesProxyModel::roleToValue(const StreamResult &result, int role) const
0546 {
0547     AbstractResource *resource = result.resource;
0548     switch (role) {
0549     case ApplicationRole:
0550         return QVariant::fromValue<QObject *>(resource);
0551     case RatingPointsRole:
0552     case RatingRole:
0553     case RatingCountRole:
0554     case SortableRatingRole: {
0555         Rating *const rating = resource->rating();
0556         const int idx = Rating::staticMetaObject.indexOfProperty(roleNames().value(role).constData());
0557         Q_ASSERT(idx >= 0);
0558         auto prop = Rating::staticMetaObject.property(idx);
0559         if (rating) {
0560             return prop.readOnGadget(rating);
0561         } else {
0562             QVariant val(0);
0563             val.convert(prop.metaType());
0564             return val;
0565         }
0566     }
0567     case SearchRelevanceRole: {
0568         qreal rating = roleToValue(result, SortableRatingRole).value<qreal>();
0569 
0570         qreal reverseDistance = 0;
0571         for (const QString &word : resource->name().split(QLatin1Char(' '))) {
0572             const qreal maxLength = std::max(word.length(), m_filters.search.length());
0573             reverseDistance =
0574                 std::max(reverseDistance, (maxLength - std::min(reverseDistance, qreal(levenshteinDistance(word, m_filters.search)))) / maxLength * 10.0);
0575         }
0576 
0577         qreal exactMatch = 0.0;
0578         if (resource->name().toUpper() == m_filters.search.toUpper()) {
0579             exactMatch = 10.0;
0580         } else if (resource->name().contains(m_filters.search, Qt::CaseInsensitive)) {
0581             exactMatch = 5.0;
0582         }
0583         return qreal(result.sortScore) / 100 + rating + reverseDistance + exactMatch;
0584     }
0585     case Qt::DecorationRole:
0586     case Qt::DisplayRole:
0587     case Qt::StatusTipRole:
0588     case Qt::ToolTipRole:
0589         return QVariant();
0590     default: {
0591         QByteArray roleText = roleNames().value(role);
0592         if (Q_UNLIKELY(roleText.isEmpty())) {
0593             qCDebug(LIBDISCOVER_LOG) << "unsupported role" << role;
0594             return {};
0595         }
0596         static const QMetaObject *m = &AbstractResource::staticMetaObject;
0597         int propidx = roleText.isEmpty() ? -1 : m->indexOfProperty(roleText.constData());
0598 
0599         if (Q_UNLIKELY(propidx < 0)) {
0600             qCWarning(LIBDISCOVER_LOG) << "unknown role:" << role << roleText;
0601             return QVariant();
0602         } else
0603             return m->property(propidx).read(resource);
0604     }
0605     }
0606 }
0607 
0608 // Wraps roleToValue with additional features for sorting/comparison.
0609 QVariant ResourcesProxyModel::roleToOrderedValue(const StreamResult &result, int role) const
0610 {
0611     AbstractResource *resource = result.resource;
0612     switch (role) {
0613     case NameRole:
0614         return QVariant::fromValue(resource->nameSortKey());
0615     default:
0616         return roleToValue(result, role);
0617     }
0618 }
0619 
0620 bool ResourcesProxyModel::isSorted(const QVector<StreamResult> &resources)
0621 {
0622     auto last = resources.constFirst();
0623     for (auto it = resources.constBegin() + 1, itEnd = resources.constEnd(); it != itEnd; ++it) {
0624         auto v1 = roleToValue(last, m_sortRole), v2 = roleToValue(*it, m_sortRole);
0625         if (!orderedLessThan(last, *it) && v1 != v2) {
0626             qDebug() << "faulty sort" << last.resource->name() << (*it).resource->name() << last.resource << (*it).resource;
0627             return false;
0628         }
0629         last = *it;
0630     }
0631     return true;
0632 }
0633 
0634 void ResourcesProxyModel::sortedInsertion(const QVector<StreamResult> &_res)
0635 {
0636     Q_ASSERT(_res.size() == QSet(_res.constBegin(), _res.constEnd()).size());
0637 
0638     auto resources = _res;
0639     Q_ASSERT(!resources.isEmpty());
0640 
0641     if (!m_filters.allBackends) {
0642         removeDuplicates(resources);
0643         if (resources.isEmpty())
0644             return;
0645     }
0646 
0647     if (m_displayedResources.isEmpty()) {
0648         int rows = rowCount();
0649         beginInsertRows({}, rows, rows + resources.count() - 1);
0650         m_displayedResources += resources;
0651         endInsertRows();
0652         return;
0653     }
0654 
0655     for (auto result : std::as_const(resources)) {
0656         const auto finder = [this](const StreamResult &result, const StreamResult &res) {
0657             return orderedLessThan(result, res);
0658         };
0659         const auto it = std::upper_bound(m_displayedResources.constBegin(), m_displayedResources.constEnd(), result, finder);
0660         const auto newIdx = it == m_displayedResources.constEnd() ? m_displayedResources.count() : (it - m_displayedResources.constBegin());
0661 
0662         if ((it - 1) != m_displayedResources.constEnd() && (it - 1)->resource == result.resource)
0663             continue;
0664 
0665         beginInsertRows({}, newIdx, newIdx);
0666         m_displayedResources.insert(newIdx, result);
0667         endInsertRows();
0668         // Q_ASSERT(isSorted(resources));
0669     }
0670 }
0671 
0672 void ResourcesProxyModel::refreshResource(AbstractResource *resource, const QVector<QByteArray> &properties)
0673 {
0674     const auto residx = indexOf(resource);
0675     if (residx < 0) {
0676         return;
0677     }
0678 
0679     if (!m_filters.shouldFilter(resource)) {
0680         beginRemoveRows({}, residx, residx);
0681         m_displayedResources.removeAt(residx);
0682         endRemoveRows();
0683         return;
0684     }
0685 
0686     const QModelIndex idx = index(residx, 0);
0687     Q_ASSERT(idx.isValid());
0688     const auto roles = propertiesToRoles(properties);
0689     if (roles.contains(m_sortRole)) {
0690         beginRemoveRows({}, residx, residx);
0691         m_displayedResources.removeAt(residx);
0692         endRemoveRows();
0693 
0694         sortedInsertion({{resource, 0}});
0695     } else
0696         Q_EMIT dataChanged(idx, idx, roles);
0697 }
0698 
0699 void ResourcesProxyModel::removeResource(AbstractResource *resource)
0700 {
0701     const auto residx = indexOf(resource);
0702     if (residx < 0)
0703         return;
0704     beginRemoveRows({}, residx, residx);
0705     m_displayedResources.removeAt(residx);
0706     endRemoveRows();
0707 }
0708 
0709 void ResourcesProxyModel::refreshBackend(AbstractResourcesBackend *backend, const QVector<QByteArray> &properties)
0710 {
0711     auto roles = propertiesToRoles(properties);
0712     const int count = m_displayedResources.count();
0713 
0714     bool found = false;
0715 
0716     for (int i = 0; i < count; ++i) {
0717         if (backend != m_displayedResources[i].resource->backend())
0718             continue;
0719 
0720         int j = i + 1;
0721         for (; j < count && backend == m_displayedResources[j].resource->backend(); ++j) { }
0722 
0723         Q_EMIT dataChanged(index(i, 0), index(j - 1, 0), roles);
0724         i = j;
0725         found = true;
0726     }
0727 
0728     if (found && properties.contains(s_roles.value(m_sortRole))) {
0729         invalidateSorting();
0730     }
0731 }
0732 
0733 QVector<int> ResourcesProxyModel::propertiesToRoles(const QVector<QByteArray> &properties) const
0734 {
0735     QVector<int> roles = kTransform<QVector<int>>(properties, [this](const QByteArray &arr) {
0736         return roleNames().key(arr, -1);
0737     });
0738     roles.removeAll(-1);
0739     return roles;
0740 }
0741 
0742 int ResourcesProxyModel::indexOf(AbstractResource *res)
0743 {
0744     return kIndexOf(m_displayedResources, [res](auto result) {
0745         return result.resource == res;
0746     });
0747 }
0748 
0749 AbstractResource *ResourcesProxyModel::resourceAt(int row) const
0750 {
0751     return m_displayedResources[row].resource;
0752 }
0753 
0754 bool ResourcesProxyModel::canFetchMore(const QModelIndex &parent) const
0755 {
0756     Q_ASSERT(!parent.isValid());
0757     return m_currentStream;
0758 }
0759 
0760 void ResourcesProxyModel::fetchMore(const QModelIndex &parent)
0761 {
0762     Q_ASSERT(!parent.isValid());
0763     if (!m_currentStream)
0764         return;
0765     Q_EMIT m_currentStream->fetchMore();
0766 }
0767 
0768 ResourcesCount ResourcesProxyModel::count() const
0769 {
0770     const int rows = rowCount();
0771     if (isBusy()) {
0772         // We return an empty string because it's evidently confusing
0773         if (rows == 0) {
0774             return ResourcesCount();
0775         }
0776 
0777         // We convert rows=1234 into round=1000
0778         const int round = std::pow(10, std::floor(std::log10(rows)));
0779         if (round >= 1) {
0780             const int roughCount = (rows / round) * round;
0781             const auto string = i18nc("an approximation number, like 3000+", "%1+", roughCount);
0782             return ResourcesCount(roughCount, string);
0783         }
0784     }
0785     return ResourcesCount(rows);
0786 }
0787 
0788 ResourcesCount::ResourcesCount()
0789     : m_valid(false)
0790     , m_number(0)
0791     , m_string()
0792 {
0793 }
0794 
0795 ResourcesCount::ResourcesCount(int number)
0796     : m_valid(true)
0797     , m_number(number)
0798     , m_string(QString::number(number))
0799 {
0800 }
0801 
0802 ResourcesCount::ResourcesCount(int number, const QString &string)
0803     : m_valid(true)
0804     , m_number(number)
0805     , m_string(string)
0806 {
0807 }