diff --git a/kcms/keyboard/CMakeLists.txt b/kcms/keyboard/CMakeLists.txt index dc4b4a9302..5241a197d1 100644 --- a/kcms/keyboard/CMakeLists.txt +++ b/kcms/keyboard/CMakeLists.txt @@ -19,6 +19,7 @@ set( kcm_keyboard_PART_SRCS xkboptionsmodel.cpp userlayoutmodel.cpp layoutsearchmodel.cpp + variantsmodel.cpp ) ecm_qt_declare_logging_category(kcm_keyboard_PART_SRCS diff --git a/kcms/keyboard/layoutmodel.cpp b/kcms/keyboard/layoutmodel.cpp index e71708edec..1fa4d6a8b8 100644 --- a/kcms/keyboard/layoutmodel.cpp +++ b/kcms/keyboard/layoutmodel.cpp @@ -38,7 +38,7 @@ QVariant LayoutModel::data(const QModelIndex &index, int role) const const auto &layout = m_data.at(index.row()); - if (role == Roles::DescripionRole || role == Qt::DisplayRole) { + if (role == Roles::DescriptionRole || role == Qt::DisplayRole) { return layout.description; } else if (role == Roles::ShortNameRole) { return layout.name; @@ -54,7 +54,7 @@ QHash LayoutModel::roleNames() const return { {Qt::DisplayRole, QByteArrayLiteral("display")}, {Roles::ShortNameRole, QByteArrayLiteral("shortName")}, - {Roles::DescripionRole, QByteArrayLiteral("description")}, + {Roles::DescriptionRole, QByteArrayLiteral("description")}, {Roles::VariantNameRole, QByteArrayLiteral("variantName")}, }; } diff --git a/kcms/keyboard/layoutmodel.h b/kcms/keyboard/layoutmodel.h index 5bfaf2ef64..77dd95e4cc 100644 --- a/kcms/keyboard/layoutmodel.h +++ b/kcms/keyboard/layoutmodel.h @@ -35,7 +35,7 @@ public: QHash roleNames() const override; enum Roles { ShortNameRole = Qt::UserRole + 1, - DescripionRole, + DescriptionRole, VariantNameRole, }; diff --git a/kcms/keyboard/layoutsearchmodel.cpp b/kcms/keyboard/layoutsearchmodel.cpp index 6e77446d80..2e731a3b83 100644 --- a/kcms/keyboard/layoutsearchmodel.cpp +++ b/kcms/keyboard/layoutsearchmodel.cpp @@ -1,16 +1,11 @@ /* SPDX-FileCopyrightText: 2025 Bharadwaj Raju - + SPDX-FileCopyrightText: 2026 Tobias Fella SPDX-License-Identifier: GPL-2.0-or-later */ #include "layoutsearchmodel.h" -#include - -#include -#include - #include #include "layoutmodel.h" @@ -20,53 +15,78 @@ using namespace Qt::Literals::StringLiterals; LayoutSearchModel::LayoutSearchModel(QObject *parent) : QSortFilterProxyModel(parent) { - connect(this, &QSortFilterProxyModel::sourceModelChanged, this, [this]() { - const auto originalRoles = sourceModel()->roleNames().keys(); - m_searchScoreRole = *std::max_element(originalRoles.begin(), originalRoles.end()) + 1; - }); + setSortRole(LayoutModel::Roles::DescriptionRole); + sort(0); } -void LayoutSearchModel::setSearchString(QStringView searchString) +QString LayoutSearchModel::searchString() const { - beginResetModel(); - m_searchString = searchString.toString(); - endResetModel(); + return m_searchString; +} + +void LayoutSearchModel::setSearchString(const QString &searchString) +{ + if (searchString == m_searchString) { + return; + } + + beginFilterChange(); + m_searchString = searchString; + endFilterChange(); + + sort(-1); + sort(0); + Q_EMIT searchStringChanged(); } QString LayoutSearchModel::getFullName(const QModelIndex &idx) const { - const auto shortName = sourceModel()->data(idx, LayoutModel::Roles::ShortNameRole).toString(); - const auto description = sourceModel()->data(idx, LayoutModel::Roles::DescripionRole).toString(); - const auto variantName = sourceModel()->data(idx, LayoutModel::Roles::VariantNameRole).toString(); + const auto shortName = idx.data(LayoutModel::Roles::ShortNameRole).toString(); + const auto description = idx.data(LayoutModel::Roles::DescriptionRole).toString(); + const auto variantName = idx.data(LayoutModel::Roles::VariantNameRole).toString(); return shortName + " - "_L1 + description + " - "_L1 + variantName; } -QVariant LayoutSearchModel::data(const QModelIndex &idx, int role) const -{ - if (!idx.isValid()) - return QVariant(); - - if (role == m_searchScoreRole) - return QVariant(KFuzzyMatcher::match(m_searchString, getFullName(idx)).score); - - return sourceModel()->data(idx, role); -} - bool LayoutSearchModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { - return true; - const auto idx = sourceModel()->index(sourceRow, 0, sourceParent); - const auto res = KFuzzyMatcher::match(m_searchString, getFullName(idx)); - return res.matched; + const auto index = sourceModel()->index(sourceRow, 0, sourceParent); + + if (!index.data(LayoutModel::Roles::VariantNameRole).toString().trimmed().isEmpty()) { + return false; + } + + if (m_searchString.isEmpty()) { + return true; + } + + if (getFullName(index).contains(m_searchString, Qt::CaseInsensitive)) { + return true; + } + + const auto shortName = index.data(LayoutModel::ShortNameRole).toString(); + + for (auto i = 0; i < sourceModel()->rowCount(); i++) { + const auto sourceIndex = sourceModel()->index(i, 0, sourceParent); + const auto sourceShortName = sourceIndex.data(LayoutModel::ShortNameRole).toString(); + if (sourceShortName == shortName && getFullName(sourceIndex).contains(m_searchString, Qt::CaseInsensitive)) { + return true; + } + } + return false; } -QHash LayoutSearchModel::roleNames() const +bool LayoutSearchModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { - QHash newRoles(sourceModel()->roleNames()); - newRoles.insert(m_searchScoreRole, "searchScore"_ba); - return newRoles; + auto leftScore = KFuzzyMatcher::match(m_searchString, getFullName(left)).score; + auto rightScore = KFuzzyMatcher::match(m_searchString, getFullName(right)).score; + + if (leftScore == rightScore) { + return QSortFilterProxyModel::lessThan(left, right); + } + + return leftScore > rightScore; } #include "moc_layoutsearchmodel.cpp" diff --git a/kcms/keyboard/layoutsearchmodel.h b/kcms/keyboard/layoutsearchmodel.h index d9f3df0512..8da6e3e3a3 100644 --- a/kcms/keyboard/layoutsearchmodel.h +++ b/kcms/keyboard/layoutsearchmodel.h @@ -1,6 +1,6 @@ /* SPDX-FileCopyrightText: 2025 Bharadwaj Raju - + SPDX-FileCopyrightText: 2026 Tobias Fella SPDX-License-Identifier: GPL-2.0-or-later */ @@ -12,27 +12,23 @@ class LayoutSearchModel : public QSortFilterProxyModel { Q_OBJECT -public: - LayoutSearchModel(QObject *parent = nullptr); - - Q_INVOKABLE QString searchString() const - { - return m_searchString; - }; - Q_INVOKABLE void setSearchString(QStringView searchString); - Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged) -signals: +public: + explicit LayoutSearchModel(QObject *parent = nullptr); + + QString searchString() const; + void setSearchString(const QString &searchString); + + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +Q_SIGNALS: void searchStringChanged(); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; - QVariant data(const QModelIndex &idx, int role) const override; - QHash roleNames() const override; private: - QString m_searchString; + QString m_searchString = QStringLiteral(""); QString getFullName(const QModelIndex &idx) const; - int m_searchScoreRole; }; diff --git a/kcms/keyboard/types.h b/kcms/keyboard/types.h index 58efb394ec..fd20be4fdd 100644 --- a/kcms/keyboard/types.h +++ b/kcms/keyboard/types.h @@ -5,6 +5,7 @@ #include "flags.h" #include "layoutsearchmodel.h" +#include "variantsmodel.h" struct FlagsForeign { Q_GADGET @@ -18,3 +19,9 @@ struct LayoutSearchModelForeign { QML_NAMED_ELEMENT(LayoutSearchModel) QML_FOREIGN(LayoutSearchModel) }; + +struct VariantsModelForeign { + Q_GADGET + QML_NAMED_ELEMENT(VariantsModel) + QML_FOREIGN(VariantsModel) +}; diff --git a/kcms/keyboard/ui/KeyboardLayoutsModel.qml b/kcms/keyboard/ui/KeyboardLayoutsModel.qml deleted file mode 100644 index a42d269c1b..0000000000 --- a/kcms/keyboard/ui/KeyboardLayoutsModel.qml +++ /dev/null @@ -1,47 +0,0 @@ -/* - SPDX-FileCopyrightText: 2024 Evgeny Chesnokov - - SPDX-License-Identifier: GPL-2.0-or-later -*/ - -import QtQuick - -import org.kde.kitemmodels as KItemModels - -KItemModels.KSortFilterProxyModel { - id: layoutsProxy - sourceModel: layoutSearchProxy - sortRoleName: "searchScore" - sortOrder: Qt.DescendingOrder - - filterRowCallback: function (row, parent): bool { - const modelIndex = sourceModel.index(row, 0, parent); - const currentVariantName = sourceModel.data(modelIndex, KItemModels.KRoleNames.role("variantName")); - const description = sourceModel.data(modelIndex, KItemModels.KRoleNames.role("description")); - - if (currentVariantName !== '') { - return false; - } - - if (searchField.text.length > 0) { - const score = sourceModel.data(modelIndex, KItemModels.KRoleNames.role("searchScore")); - if (score !== 0) { - return true; - } - const shortNameRole = KItemModels.KRoleNames.role("shortName"); - const currentName = sourceModel.data(modelIndex, shortNameRole); - const searchScoreRole = KItemModels.KRoleNames.role("searchScore"); - for (let i = 0; i < sourceModel.rowCount(); i++) { - const index = sourceModel.index(i, 0, parent); - const name = sourceModel.data(index, shortNameRole); - const variantSearchScore = sourceModel.data(index, searchScoreRole); - if (name === currentName && variantSearchScore > 100) { - return true; - } - } - return false; - } - - return true; - } -} diff --git a/kcms/keyboard/ui/KeyboardVariantsModel.qml b/kcms/keyboard/ui/KeyboardVariantsModel.qml deleted file mode 100644 index 5dd59f6a4a..0000000000 --- a/kcms/keyboard/ui/KeyboardVariantsModel.qml +++ /dev/null @@ -1,37 +0,0 @@ -/* - SPDX-FileCopyrightText: 2024 Evgeny Chesnokov - - SPDX-License-Identifier: GPL-2.0-or-later -*/ - -import QtQuick - -import org.kde.kitemmodels as KItemModels - -KItemModels.KSortFilterProxyModel { - id: variantProxy - sourceModel: layoutSearchProxy - sortRoleName: "searchScore" - sortOrder: Qt.DescendingOrder - - filterRowCallback: function (row, parent): bool { - if (!layoutsView.currentItem) { - return false; - } - - const modelIndex = sourceModel.index(row, 0, parent); - const currentName = sourceModel.data(modelIndex, KItemModels.KRoleNames.role("shortName")); - const selectedName = layoutsView.currentItem.shortName; - - if (currentName !== selectedName) { - return false; - } - - if (searchField.text.length > 0) { - const searchScore = sourceModel.data(modelIndex, KItemModels.KRoleNames.role("searchScore")); - return searchScore > 100; - } - - return true; - } -} diff --git a/kcms/keyboard/ui/LayoutDialog.qml b/kcms/keyboard/ui/LayoutDialog.qml index f9f3a8dc85..db3f1a515b 100644 --- a/kcms/keyboard/ui/LayoutDialog.qml +++ b/kcms/keyboard/ui/LayoutDialog.qml @@ -47,18 +47,15 @@ Kirigami.Dialog { } } - KCMKeyboard.LayoutSearchModel { - id: layoutSearchProxy - sourceModel: KCMKeyboard.LayoutModel {} - searchString: "" + KCMKeyboard.LayoutModel { + id: layoutModel } - KeyboardLayoutsModel { - id: layoutsProxy - } - - KeyboardVariantsModel { + KCMKeyboard.VariantsModel { id: variantProxy + searchString: searchField.text + shortName: layoutsView.currentItem?.shortName ?? "" + sourceModel: layoutModel } contentItem: ColumnLayout { @@ -67,8 +64,10 @@ Kirigami.Dialog { Kirigami.SearchField { id: searchField Layout.fillWidth: true - onAccepted: { - layoutSearchProxy.searchString = searchField.text.trim(); + + onTextChanged: { + layoutsView.currentIndex = 0; + variantView.currentIndex = 0; } } @@ -84,12 +83,13 @@ Kirigami.Dialog { contentItem: ListView { id: layoutsView currentIndex: 0 - model: layoutsProxy + model: KCMKeyboard.LayoutSearchModel { + sourceModel: layoutModel + searchString: searchField.text + } delegate: LayoutDelegate {} keyNavigationEnabled: true activeFocusOnTab: true - - onCurrentItemChanged: variantProxy.invalidateFilter() } } diff --git a/kcms/keyboard/variantsmodel.cpp b/kcms/keyboard/variantsmodel.cpp new file mode 100644 index 0000000000..bf8cf9e522 --- /dev/null +++ b/kcms/keyboard/variantsmodel.cpp @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2026 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "variantsmodel.h" + +#include + +#include "layoutmodel.h" + +using namespace Qt::Literals::StringLiterals; + +VariantsModel::VariantsModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ + setSortRole(LayoutModel::DescriptionRole); + sort(0); +} + +QString VariantsModel::searchString() const +{ + return m_searchString; +} + +void VariantsModel::setSearchString(const QString &searchString) +{ + if (searchString == m_searchString) { + return; + } + + beginFilterChange(); + m_searchString = searchString; + endFilterChange(); + + sort(-1); + sort(0); + + Q_EMIT searchStringChanged(); +} + +QString VariantsModel::shortName() const +{ + return m_shortName; +} + +void VariantsModel::setShortName(const QString &shortName) +{ + if (shortName == m_shortName) { + return; + } + + beginFilterChange(); + m_shortName = shortName; + endFilterChange(); + + Q_EMIT shortNameChanged(); +} + +QString VariantsModel::getFullName(const QModelIndex &idx) const +{ + const auto shortName = idx.data(LayoutModel::Roles::ShortNameRole).toString(); + const auto description = idx.data(LayoutModel::Roles::DescriptionRole).toString(); + const auto variantName = idx.data(LayoutModel::Roles::VariantNameRole).toString(); + + return shortName + " - "_L1 + description + " - "_L1 + variantName; +} + +bool VariantsModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + if (m_shortName.isEmpty()) { + return false; + } + + const auto index = sourceModel()->index(sourceRow, 0, sourceParent); + if (index.data(LayoutModel::ShortNameRole).toString() != m_shortName) { + return false; + } + + if (!m_searchString.isEmpty()) { + return getFullName(index).contains(m_searchString, Qt::CaseInsensitive); + } + + return true; +} + +bool VariantsModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + auto leftScore = KFuzzyMatcher::match(m_searchString, getFullName(left)).score; + auto rightScore = KFuzzyMatcher::match(m_searchString, getFullName(right)).score; + + if (leftScore != rightScore) { + return leftScore > rightScore; + } + + const auto leftIsBase = left.data(LayoutModel::VariantNameRole).toString().isEmpty(); + const auto rightIsBase = right.data(LayoutModel::VariantNameRole).toString().isEmpty(); + if (leftIsBase != rightIsBase) { + return leftIsBase; + } + + return QSortFilterProxyModel::lessThan(left, right); +} + +#include "moc_variantsmodel.cpp" diff --git a/kcms/keyboard/variantsmodel.h b/kcms/keyboard/variantsmodel.h new file mode 100644 index 0000000000..73ffd9d426 --- /dev/null +++ b/kcms/keyboard/variantsmodel.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +class VariantsModel : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged) + Q_PROPERTY(QString shortName READ shortName WRITE setShortName NOTIFY shortNameChanged) + +public: + explicit VariantsModel(QObject *parent = nullptr); + + QString searchString() const; + void setSearchString(const QString &searchString); + + QString shortName() const; + void setShortName(const QString &shortName); + + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +Q_SIGNALS: + void searchStringChanged(); + void shortNameChanged(); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + +private: + QString m_searchString = QStringLiteral(""); + QString getFullName(const QModelIndex &idx) const; + QString m_shortName; +};