/* * SPDX-FileCopyrightText: 2010 Jonathan Thomas * SPDX-FileCopyrightText: 2012 Aleix Pol Gonzalez * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "ResourcesProxyModel.h" #include "libdiscover_debug.h" #include #include #include #include #include "ResourcesModel.h" #include #include #include #include const QHash ResourcesProxyModel::s_roles = {{NameRole, "name"}, {IconRole, "icon"}, {CommentRole, "comment"}, {StateRole, "state"}, {RatingRole, "rating"}, {RatingPointsRole, "ratingPoints"}, {RatingCountRole, "ratingCount"}, {SortableRatingRole, "sortableRating"}, {SearchRelevanceRole, "searchRelevance"}, {InstalledRole, "isInstalled"}, {ApplicationRole, "application"}, {OriginRole, "origin"}, {DisplayOriginRole, "displayOrigin"}, {CanUpgrade, "canUpgrade"}, {PackageNameRole, "packageName"}, {CategoryRole, "category"}, {SectionRole, "section"}, {MimeTypes, "mimetypes"}, {LongDescriptionRole, "longDescription"}, {SourceIconRole, "sourceIcon"}, {SizeRole, "size"}, {ReleaseDateRole, "releaseDate"}}; int levenshteinDistance(const QString &source, const QString &target) { if (source == target) { return 0; } // Do a case insensitive version of it const QString &sourceUp = source.toUpper(); const QString &targetUp = target.toUpper(); if (sourceUp == targetUp) { return 0; } const int sourceCount = sourceUp.size(); const int targetCount = targetUp.size(); if (sourceUp.isEmpty()) { return targetCount; } if (targetUp.isEmpty()) { return sourceCount; } if (sourceCount > targetCount) { return levenshteinDistance(targetUp, sourceUp); } QVector column; column.fill(0, targetCount + 1); QVector previousColumn; previousColumn.reserve(targetCount + 1); for (int i = 0; i < targetCount + 1; i++) { previousColumn.append(i); } for (int i = 0; i < sourceCount; i++) { column[0] = i + 1; for (int j = 0; j < targetCount; j++) { 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)}); } column.swap(previousColumn); } return previousColumn.at(targetCount); } ResourcesProxyModel::ResourcesProxyModel(QObject *parent) : QAbstractListModel(parent) , m_sortRole(NameRole) , m_sortOrder(Qt::AscendingOrder) , m_currentStream(nullptr) { // new QAbstractItemModelTester(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); m_countTimer.setInterval(10); m_countTimer.setSingleShot(true); connect(&m_countTimer, &QTimer::timeout, this, &ResourcesProxyModel::countChanged); connect(this, &QAbstractItemModel::modelReset, &m_countTimer, qOverload<>(&QTimer::start)); connect(this, &QAbstractItemModel::rowsInserted, &m_countTimer, qOverload<>(&QTimer::start)); connect(this, &QAbstractItemModel::rowsRemoved, &m_countTimer, qOverload<>(&QTimer::start)); connect(this, &ResourcesProxyModel::busyChanged, &m_countTimer, qOverload<>(&QTimer::start)); } void ResourcesProxyModel::componentComplete() { m_setup = true; invalidateFilter(); } QHash ResourcesProxyModel::roleNames() const { return s_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 sortOrderChanged(sortOrder); invalidateSorting(); } } void ResourcesProxyModel::setSearch(const QString &_searchText) { // 1-character searches are painfully slow. >= 2 chars are fine, though const QString searchText = _searchText.size() <= 1 ? QString() : _searchText; if (m_filters.search != searchText) { m_filters.search = searchText; invalidateFilter(); Q_EMIT searchChanged(m_filters.search); } } void ResourcesProxyModel::removeDuplicates(QVector &resources) { const auto currentApplicationBackend = ResourcesModel::global()->currentApplicationBackend(); QHash aliases; QHash::iterator> storedIds; for (auto it = m_displayedResources.begin(); it != m_displayedResources.end(); ++it) { const auto appstreamid = it->resource->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(); } const auto alts = it->resource->alternativeAppstreamIds(); for (const auto &alias : alts) { aliases[alias] = appstreamid; } } QHash::iterator> ids; for (auto it = resources.begin(); it != resources.end();) { const auto appstreamid = it->resource->appstreamId(); if (appstreamid.isEmpty()) { ++it; continue; } auto at = storedIds.find(appstreamid); if (at == storedIds.end()) { auto aliased = aliases.constFind(appstreamid); if (aliased != aliases.constEnd()) { at = storedIds.find(aliased.value()); } } if (at == storedIds.end()) { const auto alts = it->resource->alternativeAppstreamIds(); for (const auto &alt : alts) { at = storedIds.find(alt); if (at == storedIds.end()) { break; } auto aliased = aliases.constFind(alt); if (aliased != aliases.constEnd()) { at = storedIds.find(aliased.value()); if (at != storedIds.end()) { break; } } } } if (at == storedIds.end()) { auto at = ids.find(appstreamid); if (at == ids.end()) { auto aliased = aliases.constFind(appstreamid); if (aliased != aliases.constEnd()) { at = ids.find(aliased.value()); } } if (at == ids.end()) { const auto alts = it->resource->alternativeAppstreamIds(); for (const auto &alt : alts) { at = ids.find(alt); if (at != ids.end()) { break; } auto aliased = aliases.constFind(appstreamid); if (aliased != aliases.constEnd()) { at = ids.find(aliased.value()); if (at != ids.end()) { break; } } } } if (at == ids.end()) { ids[appstreamid] = it; const auto alts = it->resource->alternativeAppstreamIds(); for (const auto &alias : alts) { aliases[alias] = appstreamid; } ++it; } else { if (it->resource->backend() == currentApplicationBackend && it->resource->backend() != (**at).resource->backend()) { qSwap(*it, **at); } it = resources.erase(it); } } else { if (it->resource->backend() == currentApplicationBackend) { **at = *it; auto pos = index(*at - m_displayedResources.begin(), 0); Q_EMIT dataChanged(pos, pos); } it = resources.erase(it); } } } void ResourcesProxyModel::addResources(const QVector &results) { auto resultsCopy = results; m_filters.filterJustInCase(resultsCopy); if (resultsCopy.isEmpty()) { return; } std::sort(resultsCopy.begin(), resultsCopy.end(), [this](const auto &left, const auto &right) { return orderedLessThan(left, right); }); sortedInsertion(resultsCopy); fetchSubcategories(); } void ResourcesProxyModel::invalidateSorting() { if (m_displayedResources.isEmpty()) { return; } beginResetModel(); std::sort(m_displayedResources.begin(), m_displayedResources.end(), [this](const auto &left, const auto &right) { return orderedLessThan(left, right); }); endResetModel(); } QString ResourcesProxyModel::lastSearch() const { return m_filters.search; } void ResourcesProxyModel::setOriginFilter(const QString &origin) { if (m_filters.origin != origin) { m_filters.origin = origin; invalidateFilter(); } } QString ResourcesProxyModel::originFilter() const { return m_filters.origin; } QString ResourcesProxyModel::filteredCategoryName() const { return m_categoryName; } void ResourcesProxyModel::setFilteredCategoryName(const QString &categoryName) { if (categoryName == m_categoryName) { return; } m_categoryName = categoryName; if (auto category = CategoryModel::global()->findCategoryByName(categoryName)) { setFiltersFromCategory(category); } else { qDebug() << "looking up wrong category or too early" << m_categoryName; auto f = [this, categoryName] { auto category = CategoryModel::global()->findCategoryByName(categoryName); setFiltersFromCategory(category); }; auto action = new OneTimeAction(f, this); connect(CategoryModel::global(), &CategoryModel::rootCategoriesChanged, action, &OneTimeAction::trigger); } } void ResourcesProxyModel::setFiltersFromCategory(Category *category) { if (m_filters.category != category) { m_filters.category = category; invalidateFilter(); Q_EMIT categoryChanged(); } } void ResourcesProxyModel::fetchSubcategories() { auto categories = kToSet(m_filters.category ? m_filters.category->subCategories() : CategoryModel::global()->rootCategories()); const int count = rowCount(); QSet done; for (int i = 0; i < count && !categories.isEmpty(); ++i) { const auto resource = m_displayedResources[i].resource; const auto found = resource->categoryObjects(kSetToVector(categories)); done.unite(found); categories.subtract(found); } const QVariantList ret = kTransform(done, [](Category *category) { return QVariant::fromValue(category); }); if (m_subcategories != ret) { 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_categoryName.isEmpty() && m_filters.category == nullptr) { return; } if (m_currentStream) { qCWarning(LIBDISCOVER_LOG) << "last stream isn't over yet" << m_filters << this; delete m_currentStream; } m_currentStream = m_filters.backend ? m_filters.backend->search(m_filters) : ResourcesModel::global()->search(m_filters); Q_EMIT busyChanged(); if (!m_displayedResources.isEmpty()) { beginResetModel(); m_displayedResources.clear(); endResetModel(); } connect(m_currentStream, &ResultsStream::resourcesFound, this, &ResourcesProxyModel::addResources); connect(m_currentStream, &ResultsStream::destroyed, this, [this]() { m_currentStream = nullptr; Q_EMIT busyChanged(); }); } int ResourcesProxyModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : m_displayedResources.count(); } // This comparator takes m_sortRole and m_sortOrder into account. It has a // fallback mechanism to use secondary sort role and order. bool ResourcesProxyModel::orderedLessThan(const StreamResult &left, const StreamResult &right) const { // (role, order) pair using SortCombination = std::pair; const std::array sortFallbackChain = {{ {m_sortRole, m_sortOrder}, {NameRole, Qt::AscendingOrder}, }}; for (const auto &[role, order] : sortFallbackChain) { QVariant leftValue = roleToOrderedValue(left, role); QVariant rightValue = roleToOrderedValue(right, role); if (leftValue == rightValue) { continue; } const auto result = QVariant::compare(leftValue, rightValue); // Should not happen, but it's better to skip than assert if (!(result == QPartialOrdering::Less || result == QPartialOrdering::Greater)) { continue; } // Yes, there is a shorter but incomprehensible way of rewriting this return (order == Qt::AscendingOrder) ? (result == QPartialOrdering::Less) : (result == QPartialOrdering::Greater); } // They compared equal, so it is definitely not a "less than" relation return false; } Category *ResourcesProxyModel::filteredCategory() const { return m_filters.category; } void ResourcesProxyModel::setStateFilter(AbstractResource::State s) { if (s != m_filters.state) { m_filters.state = s; invalidateFilter(); Q_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; } AbstractResourcesBackend *ResourcesProxyModel::backendFilter() const { return m_filters.backend; } void ResourcesProxyModel::setBackendFilter(AbstractResourcesBackend *filtered) { m_filters.backend = filtered; } QVariant ResourcesProxyModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } const auto result = m_displayedResources[index.row()]; return roleToValue(result, role); } QHash ResourcesProxyModel::createRoleToProperty() { QHash ret; static const QMetaObject *m = &AbstractResource::staticMetaObject; for (const auto &[role, roleText] : ResourcesProxyModel::s_roles.asKeyValueRange()) { int propidx = roleText.isEmpty() ? -1 : m->indexOfProperty(roleText.constData()); ret.insert(role, propidx); } return ret; } QVariant ResourcesProxyModel::roleToValue(const StreamResult &result, int role) const { const auto resource = result.resource; switch (role) { case ApplicationRole: return QVariant::fromValue(resource); case RatingPointsRole: { const auto rating = resource->rating(); return rating.ratingPoints(); } case RatingRole: { const auto rating = resource->rating(); return rating.rating(); } case RatingCountRole: { const auto rating = resource->rating(); return rating.ratingCount(); } case SortableRatingRole: { const auto rating = resource->rating(); return rating.sortableRating(); } case SearchRelevanceRole: { qreal rating = roleToValue(result, SortableRatingRole).value(); qreal reverseDistance = 0; for (const QString &word : resource->name().split(QLatin1Char(' '))) { const qreal maxLength = std::max(word.length(), m_filters.search.length()); reverseDistance = std::max(reverseDistance, (maxLength - std::min(reverseDistance, qreal(levenshteinDistance(word, m_filters.search)))) / maxLength * 10.0); } qreal exactMatch = 0.0; if (resource->name().toUpper() == m_filters.search.toUpper()) { exactMatch = 10.0; } else if (resource->name().contains(m_filters.search, Qt::CaseInsensitive)) { exactMatch = 5.0; } return qreal(result.sortScore) / 100 + rating + reverseDistance + exactMatch; } case Qt::DecorationRole: case Qt::DisplayRole: case Qt::StatusTipRole: case Qt::ToolTipRole: return {}; default: { static QHash s_roleToProperty = createRoleToProperty(); const int propidx = s_roleToProperty[role]; if (Q_UNLIKELY(propidx < 0)) { qCWarning(LIBDISCOVER_LOG) << "unknown role:" << role << roleNames()[role]; return {}; } else { return AbstractResource::staticMetaObject.property(propidx).read(resource); } } } } // Wraps roleToValue with additional features for sorting/comparison. QVariant ResourcesProxyModel::roleToOrderedValue(const StreamResult &result, int role) const { AbstractResource *resource = result.resource; switch (role) { case NameRole: return QVariant::fromValue(resource->nameSortKey()); default: return roleToValue(result, role); } } bool ResourcesProxyModel::isSorted(const QVector &results) { auto last = results.constFirst(); for (auto it = results.constBegin() + 1, itEnd = results.constEnd(); it != itEnd; ++it) { auto v1 = roleToValue(last, m_sortRole); auto v2 = roleToValue(*it, m_sortRole); if (!orderedLessThan(last, *it) && v1 != v2) { qCDebug(LIBDISCOVER_LOG) << "Faulty sort" << last.resource->name() << (*it).resource->name() << last.resource << (*it).resource; return false; } last = *it; } return true; } void ResourcesProxyModel::sortedInsertion(const QVector &results) { Q_ASSERT(results.size() == QSet(results.constBegin(), results.constEnd()).size()); auto resultsCopy = results; Q_ASSERT(!resultsCopy.isEmpty()); if (!m_filters.allBackends) { removeDuplicates(resultsCopy); if (resultsCopy.isEmpty()) { return; } } if (m_displayedResources.isEmpty()) { int rows = rowCount(); beginInsertRows({}, rows, rows + resultsCopy.count() - 1); m_displayedResources += resultsCopy; endInsertRows(); return; } for (const auto &result : std::as_const(resultsCopy)) { const auto finder = [this](const StreamResult &left, const StreamResult &right) { return orderedLessThan(left, right); }; const auto it = std::upper_bound(m_displayedResources.constBegin(), m_displayedResources.constEnd(), result, finder); const auto prev = it - 1; const auto newIdx = it == m_displayedResources.constEnd() ? m_displayedResources.count() : (it - m_displayedResources.constBegin()); if (prev != m_displayedResources.constEnd() && prev->resource == result.resource) { continue; } beginInsertRows({}, newIdx, newIdx); m_displayedResources.insert(newIdx, result); endInsertRows(); // Q_ASSERT(isSorted(resultsCopy)); } } void ResourcesProxyModel::refreshResource(AbstractResource *resource, const QVector &properties) { const auto row = indexOf(resource); if (row < 0) { return; } if (!m_filters.shouldFilter(resource)) { beginRemoveRows({}, row, row); m_displayedResources.removeAt(row); endRemoveRows(); return; } const QModelIndex idx = index(row, 0); Q_ASSERT(idx.isValid()); const auto roles = propertiesToRoles(properties); if (roles.contains(m_sortRole)) { beginRemoveRows({}, row, row); m_displayedResources.removeAt(row); endRemoveRows(); sortedInsertion({{resource, 0}}); } else { Q_EMIT dataChanged(idx, idx, roles); } } void ResourcesProxyModel::removeResource(AbstractResource *resource) { const auto residx = 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; i < count; ++i) { if (backend != m_displayedResources[i].resource->backend()) { continue; } int j = i + 1; while (j < count && backend == m_displayedResources[j].resource->backend()) { j++; } Q_EMIT dataChanged(index(i, 0), index(j - 1, 0), roles); i = j; found = true; } if (found && properties.contains(s_roles.value(m_sortRole))) { invalidateSorting(); } } QVector ResourcesProxyModel::propertiesToRoles(const QVector &properties) const { return kFilterTransform>(properties, [this](const QByteArray &property) { const auto role = roleNames().key(property, -1); return role == -1 ? std::nullopt : std::optional(role); }); } int ResourcesProxyModel::indexOf(AbstractResource *resource) { return kIndexOf(m_displayedResources, [resource](const StreamResult &result) { return result.resource == resource; }); } AbstractResource *ResourcesProxyModel::resourceAt(int row) const { return m_displayedResources[row].resource; } 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) { Q_EMIT m_currentStream->fetchMore(); } } ResourcesCount ResourcesProxyModel::count() const { const int rows = rowCount(); if (isBusy()) { // We return an empty string because it's evidently confusing if (rows == 0) { return ResourcesCount(); } // We convert rows=1234 into round=1000 const int round = std::pow(10, std::floor(std::log10(rows))); if (round >= 1) { const int roughCount = (rows / round) * round; const auto string = i18nc("an approximation number, like 3000+", "%1+", roughCount); return ResourcesCount(roughCount, string); } } return ResourcesCount(rows); } ResourcesCount::ResourcesCount() : m_valid(false) , m_number(0) , m_string() { } ResourcesCount::ResourcesCount(int number) : m_valid(true) , m_number(number) , m_string(QString::number(number)) { } ResourcesCount::ResourcesCount(int number, const QString &string) : m_valid(true) , m_number(number) , m_string(string) { } #include "moc_ResourcesProxyModel.cpp"