diff --git a/libdiscover/resources/ResourcesProxyModel.cpp b/libdiscover/resources/ResourcesProxyModel.cpp index 7311bb05..5828ab56 100644 --- a/libdiscover/resources/ResourcesProxyModel.cpp +++ b/libdiscover/resources/ResourcesProxyModel.cpp @@ -1,602 +1,601 @@ /*************************************************************************** * Copyright © 2010 Jonathan Thomas * * Copyright © 2012 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 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 14 of version 3 of the license. * * * * This program 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 General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "ResourcesProxyModel.h" #include "libdiscover_debug.h" #include #include #include "ResourcesModel.h" #include "AbstractResource.h" #include "AbstractResourcesBackend.h" #include #include #include ResourcesProxyModel::ResourcesProxyModel(QObject *parent) : QAbstractListModel(parent) , m_sortRole(NameRole) , m_sortOrder(Qt::AscendingOrder) , m_sortByRelevancy(false) , m_roles({ { NameRole, "name" }, { IconRole, "icon" }, { CommentRole, "comment" }, { StateRole, "state" }, { RatingRole, "rating" }, { RatingPointsRole, "ratingPoints" }, { RatingCountRole, "ratingCount" }, { SortableRatingRole, "sortableRating" }, { InstalledRole, "isInstalled" }, { ApplicationRole, "application" }, { OriginRole, "origin" }, { DisplayOriginRole, "displayOrigin" }, { CanUpgrade, "canUpgrade" }, { PackageNameRole, "packageName" }, - { IsTechnicalRole, "isTechnical" }, { CategoryRole, "category" }, { CategoryDisplayRole, "categoryDisplay" }, { SectionRole, "section" }, { MimeTypes, "mimetypes" }, { LongDescriptionRole, "longDescription" }, { SourceIconRole, "sourceIcon" }, { SizeRole, "size" }, { ReleaseDateRole, "releaseDate" } }) , m_currentStream(nullptr) { // new ModelTest(this, this); connect(ResourcesModel::global(), &ResourcesModel::backendsChanged, this, &ResourcesProxyModel::invalidateFilter); connect(ResourcesModel::global(), &ResourcesModel::backendDataChanged, this, &ResourcesProxyModel::refreshBackend); connect(ResourcesModel::global(), &ResourcesModel::resourceDataChanged, this, &ResourcesProxyModel::refreshResource); connect(ResourcesModel::global(), &ResourcesModel::resourceRemoved, this, &ResourcesProxyModel::removeResource); connect(this, &QAbstractItemModel::modelReset, this, &ResourcesProxyModel::countChanged); connect(this, &QAbstractItemModel::rowsInserted, this, &ResourcesProxyModel::countChanged); connect(this, &QAbstractItemModel::rowsRemoved, this, &ResourcesProxyModel::countChanged); } void ResourcesProxyModel::componentComplete() { m_setup = true; invalidateFilter(); } QHash ResourcesProxyModel::roleNames() const { return m_roles; } void ResourcesProxyModel::setSortRole(Roles sortRole) { if (sortRole != m_sortRole) { Q_ASSERT(roleNames().contains(sortRole)); m_sortRole = sortRole; Q_EMIT sortRoleChanged(sortRole); invalidateSorting(); } } void ResourcesProxyModel::setSortOrder(Qt::SortOrder sortOrder) { if (sortOrder != m_sortOrder) { m_sortOrder = sortOrder; Q_EMIT sortRoleChanged(sortOrder); invalidateSorting(); } } void ResourcesProxyModel::setSearch(const QString &_searchText) { // 1-character searches are painfully slow. >= 2 chars are fine, though const QString searchText = _searchText.count() <= 1 ? QString() : _searchText; const bool diff = searchText != m_filters.search; if (diff) { m_filters.search = searchText; if (m_sortByRelevancy == searchText.isEmpty()) { m_sortByRelevancy = !searchText.isEmpty(); Q_EMIT sortByRelevancyChanged(m_sortByRelevancy); } invalidateFilter(); Q_EMIT searchChanged(m_filters.search); } } void ResourcesProxyModel::removeDuplicates(QVector& resources) { const auto cab = ResourcesModel::global()->currentApplicationBackend(); QHash::iterator> storedIds; for(auto it = m_displayedResources.begin(); it != m_displayedResources.end(); ++it) { const auto appstreamid = (*it)->appstreamId(); if (appstreamid.isEmpty()) { continue; } auto at = storedIds.find(appstreamid); if (at == storedIds.end()) { storedIds[appstreamid] = it; } else { qCWarning(LIBDISCOVER_LOG) << "We should have sanitized the displayed resources. There is a bug"; Q_UNREACHABLE(); } } QHash::iterator> ids; for(auto it = resources.begin(); it != resources.end(); ) { const auto appstreamid = (*it)->appstreamId(); if (appstreamid.isEmpty()) { ++it; continue; } auto at = storedIds.find(appstreamid); if (at == storedIds.end()) { auto at = ids.find(appstreamid); if (at == ids.end()) { ids[appstreamid] = it; ++it; } else { if ((*it)->backend() == cab) { qSwap(*it, **at); } it = resources.erase(it); } } else { if ((*it)->backend() == cab) { **at = *it; auto pos = index(*at - m_displayedResources.begin(), 0); Q_EMIT dataChanged(pos, pos); } it = resources.erase(it); } } } void ResourcesProxyModel::addResources(const QVector& _res) { auto res = _res; m_filters.filterJustInCase(res); if (res.isEmpty()) return; if (!m_sortByRelevancy) qSort(res.begin(), res.end(), [this](AbstractResource* res, AbstractResource* res2){ return lessThan(res, res2); }); sortedInsertion(res); fetchSubcategories(); } void ResourcesProxyModel::invalidateSorting() { if (m_displayedResources.isEmpty()) return; if (!m_sortByRelevancy) { beginResetModel(); qSort(m_displayedResources.begin(), m_displayedResources.end(), [this](AbstractResource* res, AbstractResource* res2){ return lessThan(res, res2); }); endResetModel(); } } QString ResourcesProxyModel::lastSearch() const { return m_filters.search; } void ResourcesProxyModel::setOriginFilter(const QString &origin) { if (origin == m_filters.origin) return; m_filters.origin = origin; invalidateFilter(); } QString ResourcesProxyModel::originFilter() const { return m_filters.origin; } void ResourcesProxyModel::setFiltersFromCategory(Category *category) { if(category==m_filters.category) return; m_filters.category = category; invalidateFilter(); emit categoryChanged(); } void ResourcesProxyModel::fetchSubcategories() { auto cats = kVectorToSet(m_filters.category ? m_filters.category->subCategories() : CategoryModel::global()->rootCategories()); const int count = rowCount(); QSet done; for (int i=0; icategoryObjects(kSetToVector(cats)); done.unite(found); cats.subtract(found); } const QVariantList ret = kTransform(done, [](Category* cat) { return QVariant::fromValue(cat); }); if (ret != m_subcategories) { m_subcategories = ret; Q_EMIT subcategoriesChanged(m_subcategories); } } QVariantList ResourcesProxyModel::subcategories() const { return m_subcategories; } void ResourcesProxyModel::invalidateFilter() { if (!m_setup || ResourcesModel::global()->backends().isEmpty()) { return; } if (m_currentStream) { qCWarning(LIBDISCOVER_LOG) << "last stream isn't over yet" << m_filters << this; delete m_currentStream; } m_currentStream = ResourcesModel::global()->search(m_filters); Q_EMIT busyChanged(true); if (!m_displayedResources.isEmpty()) { beginResetModel(); m_displayedResources.clear(); endResetModel(); } connect(m_currentStream, &AggregatedResultsStream::resourcesFound, this, &ResourcesProxyModel::addResources); connect(m_currentStream, &AggregatedResultsStream::finished, this, [this]() { m_currentStream = nullptr; Q_EMIT busyChanged(false); }); } int ResourcesProxyModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_displayedResources.count(); } bool ResourcesProxyModel::lessThan(AbstractResource* leftPackage, AbstractResource* rightPackage) const { auto role = m_sortRole; Qt::SortOrder order = m_sortOrder; QVariant leftValue; QVariant rightValue; //if we're comparing two equal values, we want the model sorted by application name if(role != NameRole) { leftValue = roleToValue(leftPackage, role); rightValue = roleToValue(rightPackage, role); if (leftValue == rightValue) { role = NameRole; order = Qt::AscendingOrder; } } bool ret; if(role == NameRole) { ret = leftPackage->nameSortKey().compare(rightPackage->nameSortKey()) < 0; } else if(role == CanUpgrade) { ret = leftValue.toBool(); } else { ret = leftValue < rightValue; } return ret != (order != Qt::AscendingOrder); } Category* ResourcesProxyModel::filteredCategory() const { return m_filters.category; } void ResourcesProxyModel::setStateFilter(AbstractResource::State s) { if (s != m_filters.state) { m_filters.state = s; invalidateFilter(); emit stateFilterChanged(); } } AbstractResource::State ResourcesProxyModel::stateFilter() const { return m_filters.state; } QString ResourcesProxyModel::mimeTypeFilter() const { return m_filters.mimetype; } void ResourcesProxyModel::setMimeTypeFilter(const QString& mime) { if (m_filters.mimetype != mime) { m_filters.mimetype = mime; invalidateFilter(); } } QString ResourcesProxyModel::extends() const { return m_filters.extends; } void ResourcesProxyModel::setExtends(const QString& extends) { if (m_filters.extends != extends) { m_filters.extends = extends; invalidateFilter(); } } void ResourcesProxyModel::setFilterMinimumState(bool filterMinimumState) { if (filterMinimumState != m_filters.filterMinimumState) { m_filters.filterMinimumState = filterMinimumState; invalidateFilter(); Q_EMIT filterMinimumStateChanged(m_filters.filterMinimumState); } } bool ResourcesProxyModel::filterMinimumState() const { return m_filters.filterMinimumState; } QUrl ResourcesProxyModel::resourcesUrl() const { return m_filters.resourceUrl; } void ResourcesProxyModel::setResourcesUrl(const QUrl& resourcesUrl) { if (m_filters.resourceUrl != resourcesUrl) { m_filters.resourceUrl = resourcesUrl; invalidateFilter(); } } bool ResourcesProxyModel::allBackends() const { return m_filters.allBackends; } void ResourcesProxyModel::setAllBackends(bool allBackends) { m_filters.allBackends = allBackends; } QVariant ResourcesProxyModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } AbstractResource* const resource = m_displayedResources[index.row()]; return roleToValue(resource, role); } QVariant ResourcesProxyModel::roleToValue(AbstractResource* resource, int role) const { switch(role) { case ApplicationRole: return qVariantFromValue(resource); case RatingPointsRole: case RatingRole: case RatingCountRole: case SortableRatingRole: { Rating* const rating = resource->rating(); const int idx = Rating::staticMetaObject.indexOfProperty(roleNames().value(role).constData()); Q_ASSERT(idx >= 0); auto prop = Rating::staticMetaObject.property(idx); if (rating) return prop.read(rating); else { QVariant val(0); val.convert(prop.type()); return val; } } case Qt::DecorationRole: case Qt::DisplayRole: case Qt::StatusTipRole: case Qt::ToolTipRole: return QVariant(); default: { QByteArray roleText = roleNames().value(role); if(Q_UNLIKELY(roleText.isEmpty())) { qCDebug(LIBDISCOVER_LOG) << "unsupported role" << role; return {}; } static const QMetaObject* m = &AbstractResource::staticMetaObject; int propidx = roleText.isEmpty() ? -1 : m->indexOfProperty(roleText.constData()); if(Q_UNLIKELY(propidx < 0)) { qCWarning(LIBDISCOVER_LOG) << "unknown role:" << role << roleText; return QVariant(); } else return m->property(propidx).read(resource); } } } bool ResourcesProxyModel::isSorted(const QVector & resources) { auto last = resources.constFirst(); for(auto it = resources.constBegin()+1, itEnd = resources.constEnd(); it != itEnd; ++it) { if(!lessThan(last, *it)) { return false; } last = *it; } return true; } void ResourcesProxyModel::sortedInsertion(const QVector & _res) { auto resources = _res; Q_ASSERT(!resources.isEmpty()); if (!m_filters.allBackends) { removeDuplicates(resources); if (resources.isEmpty()) return; } if (m_sortByRelevancy || m_displayedResources.isEmpty()) { // Q_ASSERT(m_sortByRelevancy || isSorted(resources)); int rows = rowCount(); beginInsertRows({}, rows, rows+resources.count()-1); m_displayedResources += resources; endInsertRows(); return; } for(auto resource: qAsConst(resources)) { const auto finder = [this](AbstractResource* resource, AbstractResource* res){ return lessThan(resource, res); }; const auto it = std::upper_bound(m_displayedResources.constBegin(), m_displayedResources.constEnd(), resource, finder); const auto newIdx = it == m_displayedResources.constEnd() ? m_displayedResources.count() : (it - m_displayedResources.constBegin()); if ((it-1) != m_displayedResources.constEnd() && *(it-1) == resource) continue; beginInsertRows({}, newIdx, newIdx); m_displayedResources.insert(newIdx, resource); endInsertRows(); // Q_ASSERT(isSorted(resources)); } } void ResourcesProxyModel::refreshResource(AbstractResource* resource, const QVector& properties) { const auto residx = m_displayedResources.indexOf(resource); if (residx<0) { if (!m_sortByRelevancy && m_filters.shouldFilter(resource)) { sortedInsertion({resource}); } return; } if (!m_filters.shouldFilter(resource)) { beginRemoveRows({}, residx, residx); m_displayedResources.removeAt(residx); endRemoveRows(); return; } const QModelIndex idx = index(residx, 0); Q_ASSERT(idx.isValid()); const auto roles = propertiesToRoles(properties); if (!m_sortByRelevancy && roles.contains(m_sortRole)) { beginRemoveRows({}, residx, residx); m_displayedResources.removeAt(residx); endRemoveRows(); sortedInsertion({resource}); } else emit dataChanged(idx, idx, roles); } void ResourcesProxyModel::removeResource(AbstractResource* resource) { const auto residx = m_displayedResources.indexOf(resource); if (residx < 0) return; beginRemoveRows({}, residx, residx); m_displayedResources.removeAt(residx); endRemoveRows(); } void ResourcesProxyModel::refreshBackend(AbstractResourcesBackend* backend, const QVector& properties) { auto roles = propertiesToRoles(properties); const int count = m_displayedResources.count(); bool found = false; for(int i = 0; ibackend()) continue; int j = i+1; for(; jbackend(); ++j) {} Q_EMIT dataChanged(index(i, 0), index(j-1, 0), roles); i = j; found = true; } if (found && properties.contains(m_roles.value(m_sortRole))) { invalidateSorting(); } } QVector ResourcesProxyModel::propertiesToRoles(const QVector& properties) const { QVector roles = kTransform>(properties, [this](const QByteArray& arr) { return roleNames().key(arr, -1); }); roles.removeAll(-1); return roles; } int ResourcesProxyModel::indexOf(AbstractResource* res) { return m_displayedResources.indexOf(res); } AbstractResource * ResourcesProxyModel::resourceAt(int row) const { return m_displayedResources[row]; } bool ResourcesProxyModel::canFetchMore(const QModelIndex& parent) const { Q_ASSERT(!parent.isValid()); return m_currentStream; } void ResourcesProxyModel::fetchMore(const QModelIndex& parent) { Q_ASSERT(!parent.isValid()); if (!m_currentStream) return; m_currentStream->fetchMore(); } bool ResourcesProxyModel::sortByRelevancy() const { return m_sortByRelevancy; } diff --git a/libdiscover/resources/ResourcesProxyModel.h b/libdiscover/resources/ResourcesProxyModel.h index 83e45a7e..9483ea44 100644 --- a/libdiscover/resources/ResourcesProxyModel.h +++ b/libdiscover/resources/ResourcesProxyModel.h @@ -1,179 +1,178 @@ /*************************************************************************** * Copyright © 2010 Jonathan Thomas * * Copyright © 2012 Aleix Pol Gonzalez * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 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 14 of version 3 of the license. * * * * This program 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 General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef RESOURCESPROXYMODEL_H #define RESOURCESPROXYMODEL_H #include #include #include #include #include #include "discovercommon_export.h" #include "AbstractResource.h" #include "AbstractResourcesBackend.h" class Transaction; class AggregatedResultsStream; class DISCOVERCOMMON_EXPORT ResourcesProxyModel : public QAbstractListModel, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(Roles sortRole READ sortRole WRITE setSortRole NOTIFY sortRoleChanged) Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) Q_PROPERTY(Category* filteredCategory READ filteredCategory WRITE setFiltersFromCategory NOTIFY categoryChanged) Q_PROPERTY(QString originFilter READ originFilter WRITE setOriginFilter) Q_PROPERTY(AbstractResource::State stateFilter READ stateFilter WRITE setStateFilter NOTIFY stateFilterChanged) Q_PROPERTY(bool filterMinimumState READ filterMinimumState WRITE setFilterMinimumState NOTIFY filterMinimumStateChanged) Q_PROPERTY(QString mimeTypeFilter READ mimeTypeFilter WRITE setMimeTypeFilter) Q_PROPERTY(QString search READ lastSearch WRITE setSearch NOTIFY searchChanged) Q_PROPERTY(QUrl resourcesUrl READ resourcesUrl WRITE setResourcesUrl NOTIFY resourcesUrlChanged) Q_PROPERTY(QString extending READ extends WRITE setExtends) Q_PROPERTY(bool allBackends READ allBackends WRITE setAllBackends) Q_PROPERTY(QVariantList subcategories READ subcategories NOTIFY subcategoriesChanged) Q_PROPERTY(bool isBusy READ isBusy NOTIFY busyChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(bool sortByRelevancy READ sortByRelevancy NOTIFY sortByRelevancyChanged) public: explicit ResourcesProxyModel(QObject* parent = nullptr); enum Roles { NameRole = Qt::UserRole, IconRole, CommentRole, StateRole, RatingRole, RatingPointsRole, RatingCountRole, SortableRatingRole, InstalledRole, ApplicationRole, OriginRole, DisplayOriginRole, CanUpgrade, PackageNameRole, - IsTechnicalRole, CategoryRole, CategoryDisplayRole, SectionRole, MimeTypes, SizeRole, LongDescriptionRole, SourceIconRole, ReleaseDateRole }; Q_ENUM(Roles) QHash roleNames() const override; void setSearch(const QString &text); QString lastSearch() const; void setOriginFilter(const QString &origin); QString originFilter() const; void setFiltersFromCategory(Category *category); void setStateFilter(AbstractResource::State s); AbstractResource::State stateFilter() const; void setSortRole(Roles sortRole); Roles sortRole() const { return m_sortRole; } void setSortOrder(Qt::SortOrder sortOrder); Qt::SortOrder sortOrder() const { return m_sortOrder; } void setFilterMinimumState(bool filterMinimumState); bool filterMinimumState() const; Category* filteredCategory() const; QString mimeTypeFilter() const; void setMimeTypeFilter(const QString& mime); QString extends() const; void setExtends(const QString &extends); QUrl resourcesUrl() const; void setResourcesUrl(const QUrl& resourcesUrl); bool allBackends() const; void setAllBackends(bool allBackends); QVariantList subcategories() const; QVariant data(const QModelIndex & index, int role) const override; int rowCount(const QModelIndex & parent = {}) const override; Q_SCRIPTABLE int indexOf(AbstractResource* res); Q_SCRIPTABLE AbstractResource* resourceAt(int row) const; bool isBusy() const { return m_currentStream != nullptr; } bool lessThan(AbstractResource* rl, AbstractResource* rr) const; Q_SCRIPTABLE void invalidateFilter(); void invalidateSorting(); bool canFetchMore(const QModelIndex & parent) const override; void fetchMore(const QModelIndex & parent) override; bool sortByRelevancy() const; void classBegin() override {} void componentComplete() override; private Q_SLOTS: void refreshBackend(AbstractResourcesBackend* backend, const QVector& properties); void refreshResource(AbstractResource* resource, const QVector& properties); void removeResource(AbstractResource* resource); private: void sortedInsertion(const QVector &res); QVariant roleToValue(AbstractResource* res, int role) const; QVector propertiesToRoles(const QVector& properties) const; void addResources(const QVector &res); void fetchSubcategories(); void removeDuplicates(QVector& newResources); bool isSorted(const QVector & resources); Roles m_sortRole; Qt::SortOrder m_sortOrder; bool m_sortByRelevancy; bool m_setup = false; AbstractResourcesBackend::Filters m_filters; QVariantList m_subcategories; QVector m_displayedResources; const QHash m_roles; AggregatedResultsStream* m_currentStream; Q_SIGNALS: void busyChanged(bool isBusy); void sortRoleChanged(int sortRole); void sortOrderChanged(Qt::SortOrder order); void categoryChanged(); void stateFilterChanged(); void searchChanged(const QString &search); void subcategoriesChanged(const QVariantList &subcategories); void resourcesUrlChanged(const QUrl &url); void countChanged(); void filterMinimumStateChanged(bool filterMinimumState); void sortByRelevancyChanged(bool sortByRelevancy); }; #endif