/* SPDX-FileCopyrightText: 2024 Evgeny Chesnokov SPDX-FileCopyrightText: 2025 Kristen McWilliam SPDX-License-Identifier: GPL-2.0-or-later */ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import QtQuick.Controls import org.kde.kirigami as Kirigami import org.kde.kitemmodels as KItemModels import org.kde.plasmasetup import org.kde.plasmasetup.components as PlasmaSetupComponents import org.kde.plasma.private.kcm_keyboard as KCMKeyboard PlasmaSetupComponents.SetupModule { id: root function onPageActivated(): void { // Temporarily disable the highlight move duration so the correct // layout is preselected when the page is activated. layoutsView.highlightMoveDuration = 0; // Set the initial current item to the system locale's layout. layoutsView.currentIndex = layoutsProxy.layoutForLocaleIndex(); // Reset the highlight move duration to the default value. layoutsView.highlightMoveDuration = -1; } KCMKeyboard.LayoutSearchModel { id: layoutSearchProxy sourceModel: KCMKeyboard.LayoutModel {} searchString: "" } KItemModels.KSortFilterProxyModel { id: layoutsProxy sourceModel: layoutSearchProxy sortRoleName: "searchScore" sortOrder: Qt.AscendingOrder filterRowCallback: function (row, parent) { const modelIndex = sourceModel.index(row, 0, parent); const currentVariantName = modelIndex.data(KItemModels.KRoleNames.role("variantName")); const description = modelIndex.data(KItemModels.KRoleNames.role("description")); if (currentVariantName !== '') { return false; } if (searchField.text.length > 0) { const score = modelIndex.data(KItemModels.KRoleNames.role("searchScore")); if (score !== 0) { return true; } const shortNameRole = KItemModels.KRoleNames.role("shortName"); const currentName = modelIndex.data(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; } // Reads the default locale and returns the index of the keyboard layout // that matches it. function layoutForLocaleIndex(): int { const locale = Qt.locale(); const localeName = locale.name; // Iterate through the rows to find the layout that matches the locale for (let i = 0; i < rowCount(); i++) { const proxyIndex = index(i, 0); const shortName = data(proxyIndex, KItemModels.KRoleNames.role("shortName")); const description = data(proxyIndex, KItemModels.KRoleNames.role("description")); const variantName = data(proxyIndex, KItemModels.KRoleNames.role("variantName")); // For now just hardcode the English (US) layout. // TODO: Add support for _actually_ finding the layout for the current locale. if (description === "English (US)" && variantName === "") { return i; // Return the index of the matching layout } } return -1; // Not found } } KItemModels.KSortFilterProxyModel { id: variantProxy sourceModel: layoutSearchProxy sortRoleName: "searchScore" sortOrder: Qt.AscendingOrder filterRowCallback: function (row, parent) { if (!layoutsView.currentItem) { return false; } const modelIndex = sourceModel.index(row, 0, parent); const currentName = modelIndex.data(KItemModels.KRoleNames.role("shortName")); const selectedName = layoutsView.currentItem.shortName; if (currentName !== selectedName) { return false; } if (searchField.text.length > 0) { const searchScore = modelIndex.data(KItemModels.KRoleNames.role("searchScore")); return searchScore > 100; } return true; } } contentItem: ColumnLayout { id: mainColumn ColumnLayout { Layout.alignment: Qt.AlignCenter spacing: Kirigami.Units.smallSpacing Label { id: titleLabel Layout.leftMargin: Kirigami.Units.gridUnit Layout.rightMargin: Kirigami.Units.gridUnit Layout.bottomMargin: Kirigami.Units.gridUnit Layout.alignment: Qt.AlignTop Layout.fillWidth: true wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter text: i18n("Select your keyboard layout and language.") // qmllint disable unqualified } Kirigami.SearchField { id: searchField Layout.fillWidth: true onAccepted: { layoutSearchProxy.searchString = searchField.text.trim(); } } RowLayout { Layout.fillWidth: true implicitHeight: mainColumn.height - titleLabel.height - searchField.height - Kirigami.Units.smallSpacing ScrollView { clip: true implicitWidth: Math.round(mainColumn.width / 2) Layout.fillWidth: true Layout.fillHeight: true Component.onCompleted: { if (background) { background.visible = true; } } contentItem: ListView { id: layoutsView model: layoutsProxy delegate: LayoutDelegate {} keyNavigationEnabled: true activeFocusOnTab: true onCurrentIndexChanged: { variantProxy.invalidateFilter(); } } } ScrollView { clip: true implicitWidth: Math.round(mainColumn.width / 2) Layout.fillWidth: true Layout.fillHeight: true Component.onCompleted: { if (background) { background.visible = true; } } contentItem: ListView { id: variantView model: layoutsView.currentItem ? variantProxy : [] delegate: LayoutDelegate {} keyNavigationEnabled: true activeFocusOnTab: true } } } } } component LayoutDelegate: ItemDelegate { id: delegate width: ListView.view.width highlighted: ListView.isCurrentItem required property string shortName required property string description required property string variantName required property int index onClicked: { ListView.view.currentIndex = index; KeyboardUtil.setLayout(shortName, variantName); } Accessible.name: delegate.description contentItem: Kirigami.IconTitleSubtitle { icon.source: KCMKeyboard.Flags.getIcon(delegate.shortName) title: delegate.description wrapMode: Text.Wrap } } }