From c0cb9f9929cfca683313252ebe95a0ac2b62f6e8 Mon Sep 17 00:00:00 2001 From: Xuetian Weng Date: Wed, 18 May 2022 08:17:59 +0000 Subject: [PATCH] Improve kickoff's group for non-latin language. Use icu transliterator to convert i18n'ed name into ascii for initial character grouping. Since AppsModel does not use group(), just use group to store this value for convenience. Relevant qt feature request for having a new mode for section: https://bugreports.qt.io/browse/QTBUG-91258 Strategy used by this code mainly focused on CJK language. 1. Japanese locale will group all Han script together. Katakana will be converted to hiragana. 2. Hangul will decompose and use consonant as group name. 3. Han will use icu "Han-Latin" transliteration to convert to pinyin. BUG: 433297 --- applets/kicker/CMakeLists.txt | 7 ++++ applets/kicker/plugin/appentry.cpp | 56 +++++++++++++++++++++++++++++ applets/kicker/plugin/appentry.h | 3 ++ applets/kicker/plugin/appsmodel.cpp | 8 ++++- applets/kicker/plugin/rootmodel.cpp | 10 ++++-- 5 files changed, 81 insertions(+), 3 deletions(-) diff --git a/applets/kicker/CMakeLists.txt b/applets/kicker/CMakeLists.txt index 0df9a9044..a31ab8230 100644 --- a/applets/kicker/CMakeLists.txt +++ b/applets/kicker/CMakeLists.txt @@ -1,3 +1,5 @@ +find_package(ICU COMPONENTS i18n uc) + add_definitions( -DQT_USE_QSTRINGBUILDER -DQT_NO_CAST_TO_ASCII @@ -87,6 +89,11 @@ if (${HAVE_APPSTREAMQT}) target_link_libraries(kickerplugin AppStreamQt) endif() +if (ICU_FOUND) + target_link_libraries(kickerplugin ICU::i18n ICU::uc) + target_compile_definitions(kickerplugin PRIVATE "-DHAVE_ICU") +endif() + add_subdirectory(plugin/autotests) install(TARGETS kickerplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/plasma/private/kicker) diff --git a/applets/kicker/plugin/appentry.cpp b/applets/kicker/plugin/appentry.cpp index 8ecdf63d7..3190c5f6d 100644 --- a/applets/kicker/plugin/appentry.cpp +++ b/applets/kicker/plugin/appentry.cpp @@ -40,6 +40,56 @@ #include +#ifdef HAVE_ICU +#include +#endif + +namespace +{ + +QString groupName(const QString &name) +{ + if (name.isEmpty()) { + return QString(); + } + // Here we will apply a locale based strategy for the first character. + // If first character is hangul, run decomposition and return the choseong (consonants). + if (name[0].script() == QChar::Script_Hangul) { + auto decomposed = name[0].decomposition(); + if (decomposed.isEmpty()) { + return name.left(1); + } + return decomposed.left(1); + } + if (QLocale::system().language() == QLocale::Japanese) { + // We do this here for Japanese locale because: + // 1. it does not make much sense to have every different Kanji to have a different group. + // 2. ICU transliterator can't yet convert Kanji to Hiragana. + // https://unicode-org.atlassian.net/browse/ICU-5874 + if (name[0].script() == QChar::Script_Han) { + // Unicode Han + return QString::fromUtf8("\xe6\xbc\xa2"); + } + } +#ifdef HAVE_ICU + static auto ue = UErrorCode::U_ZERO_ERROR; + static auto transliterator = + std::unique_ptr(icu::Transliterator::createInstance("Han-Latin; " + "Katakana-Hiragana; " + "Latin-ASCII", + UTRANS_FORWARD, + ue)); + + if (ue == UErrorCode::U_ZERO_ERROR) { + icu::UnicodeString icuText(reinterpret_cast(name.data()), name.size()); + transliterator->transliterate(icuText); + return QString::fromUtf16(icuText.getBuffer(), static_cast(icuText.length())).left(1); + } +#endif + return name.left(1); +} +} + AppEntry::AppEntry(AbstractModel *owner, KService::Ptr service, NameFormat nameFormat) : AbstractEntry(owner) , m_service(service) @@ -78,6 +128,7 @@ AppEntry::AppEntry(AbstractModel *owner, const QString &id) void AppEntry::init(NameFormat nameFormat) { m_name = nameFromService(m_service, nameFormat); + m_group = groupName(m_name); if (nameFormat == GenericNameOnly) { m_description = nameFromService(m_service, NameOnly); @@ -118,6 +169,11 @@ KService::Ptr AppEntry::service() const return m_service; } +QString AppEntry::group() const +{ + return m_group; +} + QString AppEntry::id() const { if (!m_id.isEmpty()) { diff --git a/applets/kicker/plugin/appentry.h b/applets/kicker/plugin/appentry.h index 1ea466d11..5c7a54f7f 100644 --- a/applets/kicker/plugin/appentry.h +++ b/applets/kicker/plugin/appentry.h @@ -39,6 +39,7 @@ public: QString name() const override; QString description() const override; KService::Ptr service() const; + QString group() const override; QString id() const override; QUrl url() const override; @@ -59,6 +60,8 @@ private: QString m_id; QString m_name; QString m_description; + // Not an actual group name, but the first character for transliterated name. + QString m_group; mutable QIcon m_icon; KService::Ptr m_service; static MenuEntryEditor *m_menuEntryEditor; diff --git a/applets/kicker/plugin/appsmodel.cpp b/applets/kicker/plugin/appsmodel.cpp index 667afa343..63817ebb0 100644 --- a/applets/kicker/plugin/appsmodel.cpp +++ b/applets/kicker/plugin/appsmodel.cpp @@ -163,6 +163,8 @@ QVariant AppsModel::data(const QModelIndex &index, int role) const } return actionList; + } else if (role == Kicker::GroupRole) { + return entry->group(); } return QVariant(); @@ -691,7 +693,11 @@ void AppsModel::sortEntries() if (a->type() != b->type()) { return a->type() > b->type(); } else { - return c.compare(a->name(), b->name()) < 0; + if (a->group() != b->group()) { + return c.compare(a->group(), b->group()) < 0; + } else { + return c.compare(a->name(), b->name()) < 0; + } } }); } diff --git a/applets/kicker/plugin/rootmodel.cpp b/applets/kicker/plugin/rootmodel.cpp index 837108744..03811c910 100644 --- a/applets/kicker/plugin/rootmodel.cpp +++ b/applets/kicker/plugin/rootmodel.cpp @@ -370,11 +370,17 @@ void RootModel::refresh() for (int i = 0; i < model->count(); ++i) { AbstractEntry *appEntry = static_cast(model->index(i, 0).internalPointer()); - if (appEntry->name().isEmpty()) { + // App entry's group stores a transliterated first character of the name. Prefer to use that. + QString name = appEntry->group(); + if (name.isEmpty()) { + name = appEntry->name(); + } + + if (name.isEmpty()) { continue; } - const QChar &first = appEntry->name().at(0).toUpper(); + const QChar &first = name.at(0).toUpper(); m_categoryHash[first.isDigit() ? QStringLiteral("0-9") : first].append(appEntry); } }