Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/models/appsmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -230,6 +233,7 @@ QHash<int, QByteArray> 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") },
};
Expand Down Expand Up @@ -279,6 +283,7 @@ QList<int> AppsModel::mappedRoles(const QList<int> &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;
Expand Down
1 change: 1 addition & 0 deletions src/models/appsmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions src/models/searchfilterproxymodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(',');

Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}));
Expand All @@ -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]*");
Expand Down
52 changes: 49 additions & 3 deletions tests/searchfilterproxymodeltest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ enum SourceRoles {
SourceCategoriesRole,
SourceVendorRole,
SourceGenericNameRole,
SourceKeywordsRole,
};
}

Expand All @@ -47,6 +48,7 @@ private slots:
void testJianpinSearch();
void testSorting();
void testSpecialCharacters();
void testGenericNameAndKeywords();
void testSharedModelAdapter();
void testCategorySortSignalOrder();

Expand All @@ -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;
};
Expand Down Expand Up @@ -94,6 +97,7 @@ void TestSearchFilterProxyModel::setupTestData()
{ SourceCategoriesRole, QByteArrayLiteral("categories") },
{ SourceVendorRole, QByteArrayLiteral("vendor") },
{ SourceGenericNameRole, QByteArrayLiteral("genericName") },
{ SourceKeywordsRole, QByteArrayLiteral("keywords") },
});

// 创建测试数据
Expand All @@ -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));
Expand Down Expand Up @@ -142,14 +156,16 @@ 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;
item->setData(desktopId, SourceDesktopIdRole);
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);
Expand Down Expand Up @@ -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();
Expand Down
Loading