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); } }