diff --git a/src/models/appsmodel.cpp b/src/models/appsmodel.cpp index f2086062..2abfd761 100644 --- a/src/models/appsmodel.cpp +++ b/src/models/appsmodel.cpp @@ -33,6 +33,7 @@ constexpr auto LaunchedTimesRoleName = "launchedTimes"; constexpr auto AutoStartRoleName = "autoStart"; constexpr auto VendorRoleName = "vendor"; constexpr auto GenericNameRoleName = "genericName"; +constexpr auto KeywordsRoleName = "keywords"; } AppsModel::AppsModel(QObject *parent) @@ -194,6 +195,8 @@ QVariant AppsModel::data(const QModelIndex &index, int role) const return sourceData(sourceIndex, VendorRoleName); case AppsModel::GenericNameRole: return sourceData(sourceIndex, GenericNameRoleName); + case AppsModel::KeywordsRole: + return sourceData(sourceIndex, KeywordsRoleName); case AppsModel::TransliteratedRole: { const auto decodedDisplay = Dtk::Core::pinyin(data(index, Qt::DisplayRole).toString(), Dtk::Core::TS_NoneTone); if (decodedDisplay.isEmpty()) @@ -230,6 +233,7 @@ QHash AppsModel::roleNames() const { AppsModel::IsAutoStartRole, QByteArrayLiteral("autoStart") }, { AppsModel::VendorRole, QByteArrayLiteral("vendor") }, { AppsModel::GenericNameRole, QByteArrayLiteral("genericName") }, + { AppsModel::KeywordsRole, QByteArrayLiteral("keywords") }, { AppsModel::TransliteratedRole, QByteArrayLiteral("transliterated") }, { AppsModel::AllTransliteratedRole, QByteArrayLiteral("allTransliterated") }, }; @@ -279,6 +283,7 @@ QList AppsModel::mappedRoles(const QList &sourceRoles) const appendIfChanged(AppsModel::IsAutoStartRole, AutoStartRoleName); appendIfChanged(AppsModel::VendorRole, VendorRoleName); appendIfChanged(AppsModel::GenericNameRole, GenericNameRoleName); + appendIfChanged(AppsModel::KeywordsRole, KeywordsRoleName); if (sourceRoles.contains(sourceRole(NameRoleName))) { result << Qt::DisplayRole << AppsModel::NameRole << AppsModel::TransliteratedRole << AppsModel::AllTransliteratedRole; diff --git a/src/models/appsmodel.h b/src/models/appsmodel.h index aa44e377..9cd46cf8 100644 --- a/src/models/appsmodel.h +++ b/src/models/appsmodel.h @@ -39,6 +39,7 @@ class AppsModel : public QAbstractListModel IsAutoStartRole, VendorRole, GenericNameRole, + KeywordsRole, // Forward shell's selected Keywords so search needs no desktop-file reader. TransliteratedRole = 0x1000, AllTransliteratedRole, ProxyModelExtendedRole = 0x10000 diff --git a/src/models/searchfilterproxymodel.cpp b/src/models/searchfilterproxymodel.cpp index 548e17b6..5f32c062 100644 --- a/src/models/searchfilterproxymodel.cpp +++ b/src/models/searchfilterproxymodel.cpp @@ -89,6 +89,7 @@ int SearchFilterProxyModel::calculateWeight(const QModelIndex &modelIndex) const const QString & name = modelIndex.data(AppsModel::NameRole).toString(); const QString & vendor = modelIndex.data(AppsModel::VendorRole).toString(); const QString & genericName = modelIndex.data(AppsModel::GenericNameRole).toString(); + const QStringList keywords = modelIndex.data(AppsModel::KeywordsRole).toStringList(); const QString & transliterated = modelIndex.data(AppsModel::AllTransliteratedRole).toString(); const QString & jianpin = Dtk::Core::firstLetters(displayName, TS_NoneTone).join(','); @@ -127,6 +128,13 @@ int SearchFilterProxyModel::calculateWeight(const QModelIndex &modelIndex) const QString searchPatternLower = searchPatternDelBlank.toLower(); QString displayNameLower = displayName.toLower().remove(" "); QString targetNameLower = targetName.toLower().remove(" "); + const QString genericNameLower = genericName.toLower().remove(" "); + QStringList keywordsLower; + for (const QString &keyword : keywords) { + const QString normalized = keyword.toLower().remove(" "); + if (!normalized.isEmpty() && !keywordsLower.contains(normalized)) + keywordsLower.append(normalized); + } QString transliteratedLower = transliterated.toLower(); QString jianpinLower = jianpin.toLower(); QString nameFirstLettersLower = nameFirstLetters.toLower(); @@ -144,6 +152,11 @@ int SearchFilterProxyModel::calculateWeight(const QModelIndex &modelIndex) const return (targetNameLower == searchPatternLower); })); + // GenericName 是独立于 Name 的检索字段,对所有应用都参与匹配 + matchTypes.push_back(qMakePair(QString("genericName_exact"), [&]() -> bool { + return !genericNameLower.isEmpty() && genericNameLower == searchPatternLower; + })); + // 中文拼音匹配 matchTypes.push_back(qMakePair(QString("transliterated_start"), [&]() -> bool { return transliteratedLower.startsWith(searchPatternLower); @@ -205,6 +218,15 @@ int SearchFilterProxyModel::calculateWeight(const QModelIndex &modelIndex) const return false; })); + matchTypes.push_back(qMakePair(QString("genericName_word_start"), [&]() -> bool { + const QStringList words = genericName.split(" ", Qt::SkipEmptyParts); + for (const QString &word : words) { + if (word.toLower().startsWith(searchPatternLower)) + return true; + } + return false; + })); + matchTypes.push_back(qMakePair(QString("displayName_middle"), [&]() -> bool { if (displayNameLower.contains(searchPatternLower)) { QStringList displayWords = displayName.split(" ", Qt::SkipEmptyParts); @@ -238,6 +260,10 @@ int SearchFilterProxyModel::calculateWeight(const QModelIndex &modelIndex) const return (targetNameLower.contains(searchPatternLower)); })); + matchTypes.push_back(qMakePair(QString("genericName_middle"), [&]() -> bool { + return !genericNameLower.isEmpty() && genericNameLower.contains(searchPatternLower); + })); + matchTypes.push_back(qMakePair(QString("transliterated_middle"), [&]() -> bool { return transliteratedLower.contains(searchPatternLower); })); @@ -251,6 +277,30 @@ int SearchFilterProxyModel::calculateWeight(const QModelIndex &modelIndex) const return jianpinNormalized.contains(searchPatternLower); })); + // Keywords 是规范中用于辅助搜索的字段,优先级低于名称类匹配 + matchTypes.push_back(qMakePair(QString("keyword_exact"), [&]() -> bool { + return keywordsLower.contains(searchPatternLower); + })); + + matchTypes.push_back(qMakePair(QString("keyword_word_start"), [&]() -> bool { + for (const QString &keyword : keywords) { + const QStringList words = keyword.split(" ", Qt::SkipEmptyParts); + for (const QString &word : words) { + if (word.toLower().startsWith(searchPatternLower)) + return true; + } + } + return false; + })); + + matchTypes.push_back(qMakePair(QString("keyword_middle"), [&]() -> bool { + for (const QString &keywordLower : keywordsLower) { + if (keywordLower.contains(searchPatternLower)) + return true; + } + return false; + })); + // 英文搜索特殊情况处理 auto getCapitalizedWords = [&]() -> QString { QRegularExpression capitalizedWordRegex("\\b[A-Z][A-Za-z0-9]*"); diff --git a/tests/searchfilterproxymodeltest.cpp b/tests/searchfilterproxymodeltest.cpp index 06fa5695..e45f3f9e 100644 --- a/tests/searchfilterproxymodeltest.cpp +++ b/tests/searchfilterproxymodeltest.cpp @@ -30,6 +30,7 @@ enum SourceRoles { SourceCategoriesRole, SourceVendorRole, SourceGenericNameRole, + SourceKeywordsRole, }; } @@ -47,6 +48,7 @@ private slots: void testJianpinSearch(); void testSorting(); void testSpecialCharacters(); + void testGenericNameAndKeywords(); void testSharedModelAdapter(); void testCategorySortSignalOrder(); @@ -57,7 +59,8 @@ private slots: const QString &displayName, const QString &genericName = QString(), const QString &vendor = QString(), - int launchedTimes = 0); + int launchedTimes = 0, + const QStringList &keywords = {}); QStandardItemModel m_sourceModel; }; @@ -94,6 +97,7 @@ void TestSearchFilterProxyModel::setupTestData() { SourceCategoriesRole, QByteArrayLiteral("categories") }, { SourceVendorRole, QByteArrayLiteral("vendor") }, { SourceGenericNameRole, QByteArrayLiteral("genericName") }, + { SourceKeywordsRole, QByteArrayLiteral("keywords") }, }); // 创建测试数据 @@ -112,7 +116,17 @@ void TestSearchFilterProxyModel::setupTestData() testItems.append(createTestAppItem("org.deepin.reader", "阅读器", "文档阅读器", "阅读", "deepin", 7)); testItems.append(createTestAppItem("org.deepin.calendar", "日历", "日历", "日历", "deepin", 4)); testItems.append(createTestAppItem("org.deepin.defender", "安全中心", "安全中心", "安全中心", "deepin", 4)); - + + // GenericName 与 Keywords 的检索样本 + testItems.append(createTestAppItem("org.gnome.eog", + "Image Viewer", + "Image Viewer", + QString::fromUtf8("图像查看器"), + "gnome", + 0, + { QString::fromUtf8("图片"), + QString::fromUtf8("幻灯片") })); + // 包含特殊符号的应用 testItems.append(createTestAppItem("org.special.app1", "App@Name", "App@Name", "", "special", 1)); testItems.append(createTestAppItem("org.special.app2", "App-Name", "App-Name", "", "special", 2)); @@ -142,7 +156,8 @@ QStandardItem *TestSearchFilterProxyModel::createTestAppItem(const QString &desk const QString &displayName, const QString &genericName, const QString &vendor, - int launchedTimes) + int launchedTimes, + const QStringList &keywords) { qCDebug(logTest) << "Creating test app item:" << desktopId << "name:" << name << "launched times:" << launchedTimes; auto item = new QStandardItem; @@ -150,6 +165,7 @@ QStandardItem *TestSearchFilterProxyModel::createTestAppItem(const QString &desk item->setData(displayName.isEmpty() ? name : displayName, SourceNameRole); item->setData(QStringLiteral("application-default-icon"), SourceIconNameRole); item->setData(genericName, SourceGenericNameRole); + item->setData(keywords, SourceKeywordsRole); item->setData(vendor, SourceVendorRole); item->setData(launchedTimes, SourceLaunchedTimesRole); item->setData(launchedTimes % 11, SourceDDECategoryRole); @@ -362,6 +378,36 @@ void TestSearchFilterProxyModel::testSpecialCharacters() qCInfo(logTest) << "Special characters search tests completed successfully"; } +void TestSearchFilterProxyModel::testGenericNameAndKeywords() +{ + qCInfo(logTest) << "Starting GenericName and Keywords search tests"; + auto &model = SearchFilterProxyModel::instance(); + + const auto search = [&model](const QString &query) { + model.setFilterRegularExpression(QRegularExpression(query)); + return model.rowCount(); + }; + + QCOMPARE(search(QStringLiteral("Slideshow")), 0); + QCOMPARE(search(QStringLiteral("Picture")), 0); + QCOMPARE(search(QStringLiteral("DefaultOnlyGeneric")), 0); + QCOMPARE(search(QStringLiteral("Image Viewer")), 1); // Existing Name search is unchanged. + QCOMPARE(search(QString::fromUtf8("幻灯片")), 1); + QCOMPARE(search(QString::fromUtf8("图像查看器")), 1); + QCOMPARE(search(QString::fromUtf8("查看器")), 1); // Scalar GenericName substring. + QCOMPARE(search(QStringLiteral("Bildwerkzeug")), 0); + + const QModelIndex index = AppsModel::instance().indexFromDesktopId("org.gnome.eog"); + QVERIFY(index.isValid()); + QCOMPARE(index.data(AppsModel::KeywordsRole).toStringList(), + QStringList({ QString::fromUtf8("图片"), + QString::fromUtf8("幻灯片") })); + QCOMPARE(index.data(AppsModel::GenericNameRole).toString(), + QString::fromUtf8("图像查看器")); + + qCInfo(logTest) << "GenericName and Keywords search tests completed successfully"; +} + void TestSearchFilterProxyModel::testSharedModelAdapter() { auto &appsModel = AppsModel::instance();