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;