From 718dcbc52416ec779fbe5deb5499b590f76acfab Mon Sep 17 00:00:00 2001 From: Zhenyu FU Date: Wed, 21 Oct 2026 07:18:47 +0800 Subject: [PATCH] feat: expose localized desktop entry keywords over D-Bus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add a Keywords property mapping each locale to its decoded word list. 2. Preserve locale keys and escaped separators when parsing desktop entries. 3. Register the D-Bus map type in application manager entry points and add parser regression tests. Log: Expose desktop entry keywords for application search consumers Influence: Consumers select a locale from the Keywords map instead of merging translations from unrelated locales. feat: 通过 D-Bus 暴露本地化的桌面应用关键词 1. 新增 Keywords 属性,将各 locale 映射到解码后的关键词列表。 2. 解析桌面文件时保留 locale 键,并正确处理转义的分隔符。 3. 在应用管理器各入口注册 D-Bus 映射类型,补充解析回归测试。 Log: 为应用搜索提供桌面文件中的关键词 Influence: 消费者从 Keywords 映射中选择一个 locale,避免合并无关语言的翻译。 --- ...opspec.ApplicationManager1.Application.xml | 12 +++ apps/dde-am/src/commandexecutor.cpp | 2 + apps/dde-am/src/launcher.cpp | 2 + apps/dde-application-manager/src/main.cpp | 2 + src/dbus/applicationservice.cpp | 11 +++ src/dbus/applicationservice.h | 6 ++ src/desktopentry.cpp | 77 +++++++++++++++++++ src/desktopentry.h | 6 ++ src/global.h | 5 ++ tests/main.cpp | 2 + tests/ut_desktopentry.cpp | 53 +++++++++++++ 11 files changed, 178 insertions(+) diff --git a/api/dbus/org.desktopspec.ApplicationManager1.Application.xml b/api/dbus/org.desktopspec.ApplicationManager1.Application.xml index f2695bf0..d0785f72 100644 --- a/api/dbus/org.desktopspec.ApplicationManager1.Application.xml +++ b/api/dbus/org.desktopspec.ApplicationManager1.Application.xml @@ -117,6 +117,18 @@ + + + + + (); qDBusRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); qRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); diff --git a/apps/dde-am/src/launcher.cpp b/apps/dde-am/src/launcher.cpp index 34e8f1ba..e0fc9a9c 100644 --- a/apps/dde-am/src/launcher.cpp +++ b/apps/dde-am/src/launcher.cpp @@ -20,6 +20,8 @@ void registerComplexDbusType() qDBusRegisterMetaType(); qDBusRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); qRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); diff --git a/apps/dde-application-manager/src/main.cpp b/apps/dde-application-manager/src/main.cpp index 413ac138..f463183f 100644 --- a/apps/dde-application-manager/src/main.cpp +++ b/apps/dde-application-manager/src/main.cpp @@ -21,6 +21,8 @@ void registerComplexDbusType() qDBusRegisterMetaType(); qDBusRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); qRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); diff --git a/src/dbus/applicationservice.cpp b/src/dbus/applicationservice.cpp index 86ac9dbe..a35d725b 100644 --- a/src/dbus/applicationservice.cpp +++ b/src/dbus/applicationservice.cpp @@ -794,6 +794,16 @@ QStringMap ApplicationService::genericName() const noexcept return value->get().value(); } +QStringListMap ApplicationService::keywords() const noexcept +{ + auto value = m_entry->value(fromStaticRaw(DesktopFileEntryKey), fromStaticRaw(DesktopEntryKeywords)); + if (!value) { + return {}; + } + + return toKeywordMap(value->get()); +} + QStringMap ApplicationService::icons() const noexcept { QStringMap ret; @@ -1457,6 +1467,7 @@ void ApplicationService::resetEntry(DesktopEntry *newEntry) noexcept emit iconsChanged(); emit nameChanged(); emit genericNameChanged(); + emit keywordsChanged(); emit actionNameChanged(); emit actionsChanged(); emit categoriesChanged(); diff --git a/src/dbus/applicationservice.h b/src/dbus/applicationservice.h index 438c3be7..4cb980b8 100644 --- a/src/dbus/applicationservice.h +++ b/src/dbus/applicationservice.h @@ -58,6 +58,10 @@ class ApplicationService : public QObject, protected QDBusContext Q_PROPERTY(QStringMap GenericName READ genericName NOTIFY genericNameChanged) [[nodiscard]] QStringMap genericName() const noexcept; + // Expose parsed Keywords by locale so clients can search them without rereading desktop files. + Q_PROPERTY(QStringListMap Keywords READ keywords NOTIFY keywordsChanged) + [[nodiscard]] QStringListMap keywords() const noexcept; + Q_PROPERTY(QStringMap Icons READ icons NOTIFY iconsChanged) [[nodiscard]] QStringMap icons() const noexcept; @@ -183,6 +187,8 @@ public Q_SLOTS: void iconsChanged(); void nameChanged(); void genericNameChanged(); + // Notify consumers to refresh their search terms when desktop metadata changes. + void keywordsChanged(); void actionNameChanged(); void actionsChanged(); void categoriesChanged(); diff --git a/src/desktopentry.cpp b/src/desktopentry.cpp index 88f36315..962be702 100644 --- a/src/desktopentry.cpp +++ b/src/desktopentry.cpp @@ -477,6 +477,83 @@ QString toLocaleString(const DesktopEntry::Value &localeEntry, const QLocale &lo return {}; } +namespace { + +QStringList parseKeywordList(QStringView rawValue) noexcept +{ + QStringList keywords; + QString current; + bool escaped{false}; + + auto flushKeyword = [&keywords, ¤t]() { + const auto keyword = current.trimmed(); + if (!keyword.isEmpty() && !keywords.contains(keyword, Qt::CaseInsensitive)) { + keywords.append(keyword); + } + current.clear(); + }; + + for (const auto ch : rawValue) { + if (escaped) { + switch (ch.unicode()) { + case 's': + current.append(u' '); + break; + case 'n': + current.append(u'\n'); + break; + case 't': + current.append(u'\t'); + break; + case 'r': + current.append(u'\r'); + break; + case '\\': + current.append(u'\\'); + break; + case ';': + current.append(u';'); + break; + default: + current.append(u'\\'); + current.append(ch); + break; + } + escaped = false; + } else if (ch == u'\\') { + escaped = true; + } else if (ch == u';') { + flushKeyword(); + } else { + current.append(ch); + } + } + + if (escaped) { + current.append(u'\\'); + } + flushKeyword(); + + return keywords; +} + +} // namespace + +QStringListMap toKeywordMap(const DesktopEntry::Value &value) noexcept +{ + QStringListMap keywordMap; + if (value.userType() != QMetaTypeId::qt_metatype_id()) { + return keywordMap; + } + + const auto &localeMap = *static_cast(value.constData()); + for (auto it = localeMap.cbegin(); it != localeMap.cend(); ++it) { + keywordMap.insert(it.key(), parseKeywordList(it.value())); + } + + return keywordMap; +} + QString toIconString(const DesktopEntry::Value &value) noexcept { return toString(value); diff --git a/src/desktopentry.h b/src/desktopentry.h index 3eab2786..0012e5d0 100644 --- a/src/desktopentry.h +++ b/src/desktopentry.h @@ -6,12 +6,14 @@ #define DESKTOPENTRY_H #include +#include #include #include #include #include #include #include "iniParser.h" +#include "global.h" enum class EntryContext : uint8_t { Unknown, EntryOuter, Entry, Done }; @@ -163,6 +165,10 @@ QString toLocaleString(const DesktopEntry::Value &localeEntry, const QLocale &lo QString toString(const DesktopEntry::Value &value, bool skipUnescape = false) noexcept; +// Unlike toLocaleString(), decode each locale's Keywords list without selecting a locale. +// Preserve locale keys so consumers can apply their existing fallback policy. +QStringListMap toKeywordMap(const DesktopEntry::Value &value) noexcept; + bool toBoolean(const DesktopEntry::Value &value, bool &ok) noexcept; QString toIconString(const DesktopEntry::Value &value) noexcept; diff --git a/src/global.h b/src/global.h index 3997ecbd..09c56775 100644 --- a/src/global.h +++ b/src/global.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include // IWYU pragma: keep #include @@ -34,11 +35,15 @@ Q_DECLARE_LOGGING_CATEGORY(DDEAMUtils) using ObjectInterfaceMap = QMap; using ObjectMap = QMap; using QStringMap = QMap; +// Keywords needs a list per locale, unlike QStringMap's single string per locale. +// Keep locale keys (including "default") so clients can choose their fallback. +using QStringListMap = QMap; using PropMap = QMap; Q_DECLARE_METATYPE(ObjectInterfaceMap) Q_DECLARE_METATYPE(ObjectMap) Q_DECLARE_METATYPE(QStringMap) +Q_DECLARE_METATYPE(QStringListMap) Q_DECLARE_METATYPE(PropMap) struct SystemdUnitDBusMessage diff --git a/tests/main.cpp b/tests/main.cpp index 4ee62d5d..d453cfad 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -16,6 +16,8 @@ void registerComplexDbusType() // FIXME: test shouldn't associate with DBus qRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); + qRegisterMetaType(); + qDBusRegisterMetaType(); qRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); diff --git a/tests/ut_desktopentry.cpp b/tests/ut_desktopentry.cpp index 1e7ab050..bcbf528c 100644 --- a/tests/ut_desktopentry.cpp +++ b/tests/ut_desktopentry.cpp @@ -92,6 +92,59 @@ TEST_F(TestDesktopEntry, prase) EXPECT_TRUE(localeString == "文本编辑器"); } +TEST(DesktopFileParser, keywordsKeepLocales) +{ + QTemporaryFile file; + ASSERT_TRUE(file.open()); + ASSERT_GT(file.write("[Desktop Entry]\n" + "Type=Application\n" + "Name=Keyword Test\n" + "Keywords=Picture;Slideshow;Graphics;\n" + "Keywords[zh_CN]=图片;幻灯片;\n"), + 0); + ASSERT_TRUE(file.flush()); + ASSERT_TRUE(file.seek(0)); + + DesktopEntry entry; + ASSERT_EQ(entry.parse(file), ParserError::NoError); + + auto value = entry.value("Desktop Entry", "Keywords"); + ASSERT_TRUE(value.has_value()); + const auto &keywords = value->get().value(); + EXPECT_EQ(keywords.value("default"), "Picture;Slideshow;Graphics;"); + EXPECT_EQ(keywords.value("zh_CN"), QString::fromUtf8("图片;幻灯片;")); + + const auto keywordMap = toKeywordMap(value->get()); + EXPECT_EQ(keywordMap.size(), 2); + EXPECT_EQ(keywordMap.value("default"), + QStringList({"Picture", "Slideshow", "Graphics"})); + EXPECT_EQ(keywordMap.value("zh_CN"), + QStringList({QString::fromUtf8("图片"), QString::fromUtf8("幻灯片")})); + EXPECT_FALSE(keywordMap.contains("de")); +} + +TEST(DesktopFileParser, keywordsSplitAndUnescape) +{ + const QStringMap keywords{{"default", R"(Semi\;colon;Space\sKeyword;Backslash\\Keyword;)"}}; + const auto keywordMap = toKeywordMap(QVariant::fromValue(keywords)); + EXPECT_EQ(keywordMap.value("default"), + QStringList({"Semi;colon", "Space Keyword", R"(Backslash\Keyword)"})); +} + +TEST(DesktopFileParser, keywordsPreserveLocaleKeys) +{ + const QStringMap keywords{ + {"default", "scan;flatbed;"}, + {"de", "einlesen;Flachbett;"}, + {"zh_CN", QString::fromUtf8("扫描;扫描仪;")}, + }; + const auto keywordMap = toKeywordMap(QVariant::fromValue(keywords)); + EXPECT_EQ(keywordMap.value("default"), QStringList({"scan", "flatbed"})); + EXPECT_EQ(keywordMap.value("de"), QStringList({"einlesen", "Flachbett"})); + EXPECT_EQ(keywordMap.value("zh_CN"), + QStringList({QString::fromUtf8("扫描"), QString::fromUtf8("扫描仪")})); +} + TEST(DesktopFileParser, desktopEntryMustBeFirstGroup) { QTemporaryFile file;