[plasma/plasma-setup] modules: Improve language search in Plasma Setup

Kristen McWilliam <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit b5a0a068ff74f4c69cfcc664dd454fc19312d53e by Kristen McWilliam, on behalf of Tiziano Gaia.
Committed on 21/07/2026 at 17:23.
Pushed by merritt into branch 'master'.

Improve language search in Plasma Setup

Use QSortFilterProxyModel for language filtering

M  +21   -28   modules/language/contents/ui/main.qml
M  +1    -0    modules/languageutil/CMakeLists.txt
A  +56   -0    modules/languageutil/languagesortfilterproxymodel.cpp     [License: LGPL(v2.1+)]
A  +32   -0    modules/languageutil/languagesortfilterproxymodel.h     [License: LGPL(v2.1+)]
M  +28   -0    modules/languageutil/languageutil.cpp
M  +28   -0    modules/languageutil/languageutil.h

https://invent.kde.org/plasma/plasma-setup/-/commit/b5a0a068ff74f4c69cfcc664dd454fc19312d53e

diff --git a/modules/language/contents/ui/main.qml b/modules/language/contents/ui/main.qml
index 3d7f767..a52eaa0 100644
--- a/modules/language/contents/ui/main.qml
+++ b/modules/language/contents/ui/main.qml
@@ -1,4 +1,5 @@
 // SPDX-FileCopyrightText: 2025 Kristen McWilliam <[email protected]>
+// SPDX-FileCopyrightText: 2026 Tiziano Gaia <[email protected]>
 // SPDX-License-Identifier: LGPL-2.1-or-later
 
 pragma ComponentBehavior: Bound
@@ -49,10 +50,9 @@ PlasmaSetupComponents.SetupModule {
                 id: searchField
                 Layout.fillWidth: true
                 placeholderText: i18n("Search languages…")
-                property string filterString: ""
 
                 onTextChanged: {
-                    filterString = text.toLowerCase();
+                    Language.LanguageUtil.languageFilter = text;
                 }
             }
 
@@ -69,45 +69,35 @@ PlasmaSetupComponents.SetupModule {
                 ListView {
                     id: languageListView
                     clip: true
-                    model: Language.LanguageUtil.availableLanguages
-
-                    // Filter languages based on search text
-                    function matchesFilter(language) {
-                        if (!searchField.filterString) {
-                            return true;
-                        }
-
-                        // Get language name from locale code
-                        const localeName = Qt.locale(language).nativeLanguageName;
-                        return language.toLowerCase().includes(searchField.filterString) || localeName.toLowerCase().includes(searchField.filterString);
-                    }
+                    model: Language.LanguageUtil.languageModel
 
                     currentIndex: -1 // Ensure focus is not on the listview
 
                     delegate: RadioDelegate {
-                        required property string modelData
+                        required property string languageCode
 
-                        // Show and hide based on filter
-                        readonly property bool matchesFilter: languageListView.matchesFilter(modelData)
-                        height: matchesFilter ? implicitHeight : 0
                         width: ListView.view.width
-                        visible: matchesFilter
 
                         // Get language name from locale code (e.g., "en_US" -> "English (United States)")
                         text: {
-                            const locale = Qt.locale(modelData);
+                            const locale = Qt.locale(languageCode);
                             const localeName = locale.nativeLanguageName;
+
                             // First letter to uppercase
-                            return localeName.charAt(0).toUpperCase() + localeName.slice(1) + " (" + locale.nativeCountryName + ")";
+                            return localeName.charAt(0).toUpperCase()
+                                + localeName.slice(1)
+                                + " ("
+                                + locale.nativeCountryName
+                                + ")";
                         }
 
-                        checked: Language.LanguageUtil.currentLanguage === modelData
+                        checked: Language.LanguageUtil.currentLanguage === languageCode
 
                         onToggled: {
-                            if (checked && modelData !== Language.LanguageUtil.currentLanguage) {
-                                Language.LanguageUtil.currentLanguage = modelData;
+                            if (checked && languageCode !== Language.LanguageUtil.currentLanguage) {
+                                Language.LanguageUtil.currentLanguage = languageCode;
                                 Language.LanguageUtil.applyLanguage();
-                                checked = Qt.binding(() => Language.LanguageUtil.currentLanguage === modelData);
+                                checked = Qt.binding(() => Language.LanguageUtil.currentLanguage === languageCode);
                             }
                         }
                     }
@@ -115,10 +105,13 @@ PlasmaSetupComponents.SetupModule {
                     function scrollToCurrentLanguage() {
                         // Find the index of the current language
                         const currentLang = Language.LanguageUtil.currentLanguage;
-                        for (let i = 0; i < model.length; i++) {
-                            if (model[i] === currentLang) {
+
+                        for (let i = 0; i < languageListView.count; i++) {
+                            const item = languageListView.itemAtIndex(i);
+
+                            if (item && item.languageCode === currentLang) {
                                 // Position the view at the current language with some offset
-                                positionViewAtIndex(i, ListView.Center);
+                                languageListView.positionViewAtIndex(i, ListView.Center);
                                 break;
                             }
                         }
diff --git a/modules/languageutil/CMakeLists.txt b/modules/languageutil/CMakeLists.txt
index 73fda76..af807bf 100644
--- a/modules/languageutil/CMakeLists.txt
+++ b/modules/languageutil/CMakeLists.txt
@@ -16,6 +16,7 @@ ecm_add_qml_module(plasmasetup_languageutil
     GENERATE_PLUGIN_SOURCE
     SOURCES
         languageutil.cpp
+        languagesortfilterproxymodel.cpp
         ${logging_SRCS}
 )
 
diff --git a/modules/languageutil/languagesortfilterproxymodel.cpp b/modules/languageutil/languagesortfilterproxymodel.cpp
new file mode 100644
index 0000000..fc4f1b3
--- /dev/null
+++ b/modules/languageutil/languagesortfilterproxymodel.cpp
@@ -0,0 +1,56 @@
+// SPDX-FileCopyrightText: 2026 Tiziano Gaia <[email protected]>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "languagesortfilterproxymodel.h"
+
+#include <QLocale>
+
+LanguageSortFilterProxyModel::LanguageSortFilterProxyModel(QObject *parent)
+    : QSortFilterProxyModel(parent)
+{
+}
+
+void LanguageSortFilterProxyModel::setFilterString(const QString &filter)
+{
+    if (m_filterString == filter) {
+        return;
+    }
+
+    beginFilterChange();
+    m_filterString = filter;
+    endFilterChange();
+}
+
+bool LanguageSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+    if (m_filterString.isEmpty()) {
+        return true;
+    }
+
+    const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
+    const QString language = sourceModel()->data(index, Qt::DisplayRole).toString();
+
+    const QLocale locale(language);
+
+    const QString nativeName = locale.nativeLanguageName();
+    const QString englishName = QLocale::languageToString(locale.language());
+
+    return language.contains(m_filterString, Qt::CaseInsensitive) || nativeName.contains(m_filterString, Qt::CaseInsensitive)
+        || englishName.contains(m_filterString, Qt::CaseInsensitive);
+}
+
+QHash<int, QByteArray> LanguageSortFilterProxyModel::roleNames() const
+{
+    auto roles = QSortFilterProxyModel::roleNames();
+    roles[Qt::UserRole + 1] = "languageCode";
+    return roles;
+}
+
+QVariant LanguageSortFilterProxyModel::data(const QModelIndex &index, int role) const
+{
+    if (role == Qt::UserRole + 1) {
+        return sourceModel()->data(mapToSource(index), Qt::DisplayRole);
+    }
+
+    return QSortFilterProxyModel::data(index, role);
+}
diff --git a/modules/languageutil/languagesortfilterproxymodel.h b/modules/languageutil/languagesortfilterproxymodel.h
new file mode 100644
index 0000000..0089b98
--- /dev/null
+++ b/modules/languageutil/languagesortfilterproxymodel.h
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2026 Tiziano Gaia <[email protected]>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QSortFilterProxyModel>
+#include <QString>
+
+class QModelIndex;
+class QVariant;
+
+/**
+ * A proxy model that filters the list of available languages
+ * by locale code, native language name, and English language name.
+ */
+class LanguageSortFilterProxyModel : public QSortFilterProxyModel
+{
+    Q_OBJECT
+
+public:
+    explicit LanguageSortFilterProxyModel(QObject *parent = nullptr);
+
+    void setFilterString(const QString &filter);
+
+protected:
+    bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
+    QHash<int, QByteArray> roleNames() const override;
+    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+private:
+    QString m_filterString;
+};
diff --git a/modules/languageutil/languageutil.cpp b/modules/languageutil/languageutil.cpp
index 9d3f01e..6bcd1b3 100644
--- a/modules/languageutil/languageutil.cpp
+++ b/modules/languageutil/languageutil.cpp
@@ -1,4 +1,5 @@
 // SPDX-FileCopyrightText: 2025 Kristen McWilliam <[email protected]>
+// SPDX-FileCopyrightText: 2026 Tiziano Gaia <[email protected]>
 // SPDX-License-Identifier: LGPL-2.1-or-later
 
 #include "languageutil.h"
@@ -9,12 +10,17 @@
 #include <QCoreApplication>
 #include <QDBusConnection>
 #include <QDBusReply>
+#include <QLocale>
 #include <QTimer>
 
 LanguageUtil::LanguageUtil(QObject *parent)
     : QObject(parent)
 {
     loadAvailableLanguages();
+
+    m_languageModel.setStringList(m_availableLanguages);
+    m_languageProxyModel.setSourceModel(&m_languageModel);
+
     m_currentLanguage = QLocale::system().name();
     qCInfo(PlasmaSetupLanguageUtil) << "System language detected as:" << m_currentLanguage;
     overrideInitialLanguageIfNeeded();
@@ -25,6 +31,28 @@ QStringList LanguageUtil::availableLanguages() const
     return m_availableLanguages;
 }
 
+QString LanguageUtil::languageFilter() const
+{
+    return m_languageFilter;
+}
+
+QAbstractItemModel *LanguageUtil::languageModel()
+{
+    return &m_languageProxyModel;
+}
+
+void LanguageUtil::setLanguageFilter(const QString &filter)
+{
+    if (m_languageFilter == filter) {
+        return;
+    }
+
+    m_languageFilter = filter;
+    m_languageProxyModel.setFilterString(filter);
+
+    Q_EMIT languageFilterChanged();
+}
+
 QString LanguageUtil::currentLanguage() const
 {
     return m_currentLanguage;
diff --git a/modules/languageutil/languageutil.h b/modules/languageutil/languageutil.h
index 51c967f..bbf4954 100644
--- a/modules/languageutil/languageutil.h
+++ b/modules/languageutil/languageutil.h
@@ -1,10 +1,14 @@
 // SPDX-FileCopyrightText: 2025 Kristen McWilliam <[email protected]>
+// SPDX-FileCopyrightText: 2026 Tiziano Gaia <[email protected]>
 // SPDX-License-Identifier: LGPL-2.1-or-later
 
 #pragma once
 
 #include <QObject>
 #include <QQmlEngine>
+#include <QStringListModel>
+
+#include "languagesortfilterproxymodel.h"
 
 /**
  * Handles language choice.
@@ -28,6 +32,22 @@ class LanguageUtil : public QObject
      */
     Q_PROPERTY(QStringList availableLanguages READ availableLanguages NOTIFY availableLanguagesChanged)
 
+    /**
+     * Model containing the available languages.
+     *
+     * This model supports filtering through languageFilter and is exposed
+     * directly to QML views that display the available language choices.
+     */
+    Q_PROPERTY(QAbstractItemModel *languageModel READ languageModel CONSTANT)
+
+    /**
+     * Search filter applied to the language model.
+     *
+     * Updating this value filters languages by their code, native name,
+     * or English name.
+     */
+    Q_PROPERTY(QString languageFilter READ languageFilter WRITE setLanguageFilter NOTIFY languageFilterChanged)
+
     /**
      * The language code of the currently selected language.
      */
@@ -37,6 +57,10 @@ public:
     explicit LanguageUtil(QObject *parent = nullptr);
 
     QStringList availableLanguages() const;
+    QAbstractItemModel *languageModel();
+    QString languageFilter() const;
+    void setLanguageFilter(const QString &filter);
+
     QString currentLanguage() const;
     void setCurrentLanguage(const QString &language);
 
@@ -47,6 +71,7 @@ public:
 
 Q_SIGNALS:
     void availableLanguagesChanged();
+    void languageFilterChanged();
     void currentLanguageChanged();
     void initialLanguageOverrideApplied();
 
@@ -84,5 +109,8 @@ private:
     void overrideInitialLanguageIfNeeded();
 
     QStringList m_availableLanguages;
+    QStringListModel m_languageModel;
+    LanguageSortFilterProxyModel m_languageProxyModel;
+    QString m_languageFilter;
     QString m_currentLanguage;
 };
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.