diff --git a/src/models/categorizedresourcesortfilterproxymodel.cpp b/src/models/categorizedresourcesortfilterproxymodel.cpp index ff3ad03..ff74981 100644 --- a/src/models/categorizedresourcesortfilterproxymodel.cpp +++ b/src/models/categorizedresourcesortfilterproxymodel.cpp @@ -1,111 +1,131 @@ /* * Copyright 2012 Sebastian Gottfried * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "categorizedresourcesortfilterproxymodel.h" #include CategorizedResourceSortFilterProxyModel::CategorizedResourceSortFilterProxyModel(QObject *parent) : KCategorizedSortFilterProxyModel(parent), m_resourceTypeFilter(ResourceModel::CourseItem | ResourceModel::KeyboardLayoutItem), + m_invertedKeyboardLayoutNameFilter(false), m_resourceModel(0) { setDynamicSortFilter(true); } ResourceModel::ResourceItemTypes CategorizedResourceSortFilterProxyModel::resourceTypeFilter() const { return m_resourceTypeFilter; } void CategorizedResourceSortFilterProxyModel::setResourceTypeFilter(ResourceModel::ResourceItemTypes types) { if (types != m_resourceTypeFilter) { m_resourceTypeFilter = types; invalidateFilter(); invalidate(); sort(0); emit resourceTypeFilterChanged(); } } QString CategorizedResourceSortFilterProxyModel::keyboardLayoutNameFilter() const { return m_keyboardLayoutNameFilter; } void CategorizedResourceSortFilterProxyModel::setKeyboardLayoutNameFilter(const QString &name) { if (name != m_keyboardLayoutNameFilter) { m_keyboardLayoutNameFilter = name; invalidateFilter(); invalidate(); sort(0); emit keyboardLayoutNameFilterChanged(); } } +bool CategorizedResourceSortFilterProxyModel::invertedKeyboardLayoutNameFilter() const +{ + return m_invertedKeyboardLayoutNameFilter; +} + +void CategorizedResourceSortFilterProxyModel::setInvertedKeyboardLayoutNameFilter(bool inverted) +{ + if (inverted != m_invertedKeyboardLayoutNameFilter) + { + m_invertedKeyboardLayoutNameFilter = inverted; + invalidateFilter(); + invalidate(); + sort(0); + emit invertedKeyboardLayoutNameFilterChanged(); + } +} + + ResourceModel* CategorizedResourceSortFilterProxyModel::resourceModel() const { return m_resourceModel; } + void CategorizedResourceSortFilterProxyModel::setResourceModel(ResourceModel* resourceModel) { if (resourceModel != m_resourceModel) { m_resourceModel = resourceModel; setSourceModel(m_resourceModel); sort(0); emit resourceModelChanged(); } } bool CategorizedResourceSortFilterProxyModel::subSortLessThan(const QModelIndex& left, const QModelIndex& right) const { const QString leftStr = sourceModel()->data(left, Qt::DisplayRole).toString(); const QString rightStr = sourceModel()->data(right, Qt::DisplayRole).toString(); QCollator locater; locater.setCaseSensitivity(Qt::CaseInsensitive); const int difference = locater.compare(leftStr, rightStr); if (difference == 0) { return left.row() < right.row(); } return difference < 0; } bool CategorizedResourceSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); const int resourceType = sourceModel()->data(index, ResourceModel::ResourceTypeRole).toInt(); if ((m_resourceTypeFilter & resourceType) == 0) return false; if (m_keyboardLayoutNameFilter.isEmpty()) return true; const QString name = sourceModel()->data(index, ResourceModel::KeyboardLayoutNameRole).toString(); - return name == m_keyboardLayoutNameFilter; + return m_invertedKeyboardLayoutNameFilter ^ (name == m_keyboardLayoutNameFilter); } diff --git a/src/models/categorizedresourcesortfilterproxymodel.h b/src/models/categorizedresourcesortfilterproxymodel.h index d28574e..8daf08d 100644 --- a/src/models/categorizedresourcesortfilterproxymodel.h +++ b/src/models/categorizedresourcesortfilterproxymodel.h @@ -1,53 +1,58 @@ /* * Copyright 2012 Sebastian Gottfried * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef CATEGORIZEDRESOURCESORTFILTERPROXYMODEL_H #define CATEGORIZEDRESOURCESORTFILTERPROXYMODEL_H #include "KCategorizedSortFilterProxyModel" #include "models/resourcemodel.h" class CategorizedResourceSortFilterProxyModel : public KCategorizedSortFilterProxyModel { Q_OBJECT Q_PROPERTY(ResourceModel::ResourceItemTypes resourceTypeFilter READ resourceTypeFilter WRITE setResourceTypeFilter NOTIFY resourceTypeFilterChanged) Q_PROPERTY(QString keyboardLayoutNameFilter READ keyboardLayoutNameFilter WRITE setKeyboardLayoutNameFilter NOTIFY keyboardLayoutNameFilterChanged) + Q_PROPERTY(bool invertedKeyboardLayoutNameFilter READ invertedKeyboardLayoutNameFilter WRITE setInvertedKeyboardLayoutNameFilter NOTIFY invertedKeyboardLayoutNameFilterChanged) Q_PROPERTY(ResourceModel* resourceModel READ resourceModel WRITE setResourceModel NOTIFY resourceModelChanged) public: explicit CategorizedResourceSortFilterProxyModel(QObject* parent = 0); ResourceModel::ResourceItemTypes resourceTypeFilter() const; void setResourceTypeFilter(ResourceModel::ResourceItemTypes types); QString keyboardLayoutNameFilter() const; void setKeyboardLayoutNameFilter(const QString& name); + bool invertedKeyboardLayoutNameFilter() const; + void setInvertedKeyboardLayoutNameFilter(bool inverted); ResourceModel* resourceModel() const; void setResourceModel(ResourceModel* resourceModel); signals: void resourceTypeFilterChanged(); void keyboardLayoutNameFilterChanged(); + void invertedKeyboardLayoutNameFilterChanged(); void resourceModelChanged(); protected: bool subSortLessThan(const QModelIndex& left, const QModelIndex& right) const; bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; private: ResourceModel::ResourceItemTypes m_resourceTypeFilter; QString m_keyboardLayoutNameFilter; + bool m_invertedKeyboardLayoutNameFilter; ResourceModel* m_resourceModel; }; #endif // CATEGORIZEDRESOURCESORTFILTERPROXYMODEL_H diff --git a/src/qml/homescreen/CourseSelector.qml b/src/qml/homescreen/CourseSelector.qml index 983f3e9..88c6f31 100644 --- a/src/qml/homescreen/CourseSelector.qml +++ b/src/qml/homescreen/CourseSelector.qml @@ -1,178 +1,157 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.1 import ktouch 1.0 import "../common" FocusScope { id: root - property CategorizedResourceSortFilterProxyModel courseModel property Profile profile property KeyboardLayout keyboardLayout - property string keyboardLayoutName + property string currentKeyboardLayoutName + property string selectedKeyboardLayoutName property DataIndexCourse selectedCourse signal lessonSelected(variant course, variant lesson) signal courseSelectec(Course course) function selectLastUsedCourse() { if (!profile) { return } var courseId = profile.lastUsedCourseId; if (courseId === "custom_lessons") { - selectCourse(courseRepeater.count, true) + // selectCourse(courseRepeater.count, true) return } for (var i = 0; i < courseModel.rowCount(); i++) { var dataIndexCourse = courseModel.data(courseModel.index(i, 0), ResourceModel.DataRole); if (dataIndexCourse.id === courseId) { - selectCourse(i, true) + root.selectedCourse = dataIndexCourse return } } - selectCourse(0, true) - } - - function selectCourse(index, automaticSelection) { - if (index === priv.currentIndex) { - return - } - - var direction = index > priv.currentIndex? Item.Left: Item.Right - var dataIndexCourse = index < courseModel.rowCount()? - courseModel.data(courseModel.index(index, 0), ResourceModel.DataRole): - null; - var targetPage = automaticSelection? coursePageContainer.activePage: coursePageContainer.inactivePage - - priv.currentIndex = index; - targetPage.dataIndexCourse = dataIndexCourse - - if (!automaticSelection) { - coursePageContainer.inactivePage = coursePageContainer.activePage - coursePageContainer.activePage = targetPage - coursePageContainer.inactivePage.hide(direction) - coursePageContainer.activePage.show(direction) - - saveLastUsedCourse(dataIndexCourse? dataIndexCourse.id: "custom_lessons") + if (coursdeModel.rowCount() > 0) { + var dataIndexCourse = courseModel.data(courseModel.index(i, 0), ResourceModel.DataRole); + root.selectedCourse = dataIndexCourse } } - function saveLastUsedCourse(courseId) { - profile.lastUsedCourseId = courseId; - profileDataAccess.updateProfile(profileDataAccess.indexOfProfile(profile)); + onSelectedCourseChanged: { + root.selectedKeyboardLayoutName = root.selectedCourse.keyboardLayoutName; + if (profile.lastUsedCourseId != root.selectedCourse.id) { + profile.lastUsedCourseId = root.selectedCourse.id; + profileDataAccess.updateProfile(profileDataAccess.indexOfProfile(profile)); + } } onProfileChanged: selectLastUsedCourse() - Connections { - target: courseModel + ResourceModel { + id: resourceModel + dataIndex: ktouch.globalDataIndex onRowsRemoved: { - nextButton.visible = previousButton.visible = courseModel.rowCount() > 1 selectLastUsedCourse() } onRowsInserted: { - nextButton.visible = previousButton.visible = courseModel.rowCount() > 1 selectLastUsedCourse() } } - ResourceModel { - id: resourceModel - dataIndex: ktouch.globalDataIndex + CategorizedResourceSortFilterProxyModel { + id: currentKeyboardLayoutsModel + resourceModel: resourceModel + resourceTypeFilter: ResourceModel.KeyboardLayoutItem + keyboardLayoutNameFilter: root.currentKeyboardLayoutName } CategorizedResourceSortFilterProxyModel { - id: allKeyboardLayoutsModel + id: otherKeyboardLayoutsModel resourceModel: resourceModel resourceTypeFilter: ResourceModel.KeyboardLayoutItem + keyboardLayoutNameFilter: root.currentKeyboardLayoutName + invertedKeyboardLayoutNameFilter: true } KColorScheme { id: courseSelectorColorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.View } Rectangle { id: bg anchors.fill: parent color: courseSelectorColorScheme.normalBackground } Flickable { clip: true anchors.fill: parent contentWidth: width contentHeight: content.height Column { id: content width: parent.width - ListItem { + CourseSelectorKeyboardLayoutList { width: parent.width - text: i18n('Courses For Your Keyboard Layout') - font.bold: true - bg.color: courseSelectorColorScheme.alternateBackground - bg.opacity: 1 - label.opacity: 0.7 + title: i18n('Courses For Your Keyboard Layout') + model: currentKeyboardLayoutsModel + resourceModel: resourceModel + colorScheme: courseSelectorColorScheme + selectedKeyboardLayoutName: root.selectedKeyboardLayoutName + selectedCourse: root.selectedCourse + onCourseSelected: { + root.selectedCourse = course + } } - ListItem { + CourseSelectorKeyboardLayoutList { width: parent.width - text: i18n('Other Courses') - font.bold: true - bg.color: courseSelectorColorScheme.alternateBackground - bg.opacity: 1 - label.opacity: 0.7 - } - - Repeater { - model: allKeyboardLayoutsModel - CourseSelectorKeyboardLayoutItem { - width: parent.width - name: keyboardLayoutName - title: display - resourceModel: allKeyboardLayoutsModel.resourceModel - selectedCourse: root.selectedCourse - onCourseSelected: { - root.selectedCourse = course - } + title: i18n('Other courses') + model: otherKeyboardLayoutsModel + resourceModel: resourceModel + colorScheme: courseSelectorColorScheme + selectedKeyboardLayoutName: root.selectedKeyboardLayoutName + selectedCourse: root.selectedCourse + onCourseSelected: { + root.selectedCourse = course } } - } + ScrollBar.vertical: ScrollBar { } } } diff --git a/src/qml/homescreen/CourseSelectorKeyboardLayoutItem.qml b/src/qml/homescreen/CourseSelectorKeyboardLayoutItem.qml index 5105f41..2783d4e 100644 --- a/src/qml/homescreen/CourseSelectorKeyboardLayoutItem.qml +++ b/src/qml/homescreen/CourseSelectorKeyboardLayoutItem.qml @@ -1,95 +1,103 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import ktouch 1.0 import "../common" Column { id: root property string name property alias title: keyboardLayoutItem.text property ResourceModel resourceModel + property string selectedKeyboardLayoutName property DataIndexCourse selectedCourse signal courseSelected(DataIndexCourse course) height: keyboardLayoutItem.height + (loader.active? loader.height: 0) clip: true + onSelectedKeyboardLayoutNameChanged: { + if (selectedKeyboardLayoutName == root.name) { + loader.active = true + } + } + + CategorizedResourceSortFilterProxyModel { + id: courseModel + resourceModel: root.resourceModel + resourceTypeFilter: ResourceModel.CourseItem + keyboardLayoutNameFilter: loader.keyboardLayoutNameFilter + } + ListItem { id: keyboardLayoutItem icon: "input-keyboard" width: parent.width onClicked: { loader.active = !loader.active + if (loader.active) { + if (courseModel.rowCount()) { + courseSelected(courseModel.data(courseModel.index(0, 0), ResourceModel.DataRole)) + } + } } } Loader { id: loader width: parent.width active: false property string keyboardLayoutNameFilter: root.name sourceComponent: Component { id: courseSelectionComponent Column { - CategorizedResourceSortFilterProxyModel { - id: courseModel - resourceModel: root.resourceModel - resourceTypeFilter: ResourceModel.CourseItem - keyboardLayoutNameFilter: loader.keyboardLayoutNameFilter - } Repeater { id: courseRepeater model: courseModel ListItem { text: display width: parent.width reserveSpaceForIcon: true highlighted: root.selectedCourse == dataRole onClicked: { courseSelected(dataRole) } } } ListItem { text: i18n("Custom Lessons") id: ownLessonsItem reserveSpaceForIcon: true width: parent.width } - Component.onCompleted: { - if (courseModel.rowCount()) { - courseSelected(courseModel.data(courseModel.index(0, 0), ResourceModel.DataRole)) - } - } } } } Behavior on height { NumberAnimation { duration: 150 easing.type: Easing.InOutQuad } } } diff --git a/src/qml/homescreen/CourseSelectorKeyboardLayoutList.qml b/src/qml/homescreen/CourseSelectorKeyboardLayoutList.qml new file mode 100644 index 0000000..709616b --- /dev/null +++ b/src/qml/homescreen/CourseSelectorKeyboardLayoutList.qml @@ -0,0 +1,57 @@ +/* + * Copyright 2017 Sebastian Gottfried + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.4 +import ktouch 1.0 +import '../common' + +Column { + id: root + property alias title: header.text + property CategorizedResourceSortFilterProxyModel model + property ResourceModel resourceModel + property string selectedKeyboardLayoutName + property DataIndexCourse selectedCourse: null + property KColorScheme colorScheme + signal courseSelected(DataIndexCourse course) + + ListItem { + id: header + width: parent.width + font.bold: true + bg.color: colorScheme.alternateBackground + bg.opacity: 1 + label.opacity: 0.7 + } + + Repeater { + id: repeater + model: root.model + CourseSelectorKeyboardLayoutItem { + width: parent.width + name: keyboardLayoutName + title: display + resourceModel: root.resourceModel + selectedKeyboardLayoutName: root.selectedKeyboardLayoutName + selectedCourse: root.selectedCourse + onCourseSelected: { + root.courseSelected(course) + } + } + } + +} diff --git a/src/qml/homescreen/HomeScreen.qml b/src/qml/homescreen/HomeScreen.qml index a6a8ac7..e361cef 100644 --- a/src/qml/homescreen/HomeScreen.qml +++ b/src/qml/homescreen/HomeScreen.qml @@ -1,202 +1,201 @@ /* * Copyright 2012 Sebastian Gottfried * Copyright 2015 Sebastian Gottfried * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.4 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.1 import ktouch 1.0 import "../common" FocusScope { id: screen property CategorizedResourceSortFilterProxyModel courseModel property KeyboardLayout keyboardLayout property string keyboardLayoutName signal lessonSelected(variant course, variant lesson, variant profile) QtObject { id: d property Profile profile property int profileCount: profileDataAccess.profileCount onProfileCountChanged: findCurrentProfile() } function start() {} function reset() { profileDataAccess.loadProfiles(); } function findCurrentProfile() { d.profile = null var lastProfileId = preferences.lastUsedProfileId for (var i = 0; i < profileDataAccess.profileCount; i++) { var profile = profileDataAccess.profile(i) if (profile.id === lastProfileId) { d.profile = profile return; } } if (profileDataAccess.profileCount > 0) { d.profile = profileDataAccess.profile(0) preferences.lastUsedProfileId = d.profile.id preferences.writeConfig() } } function switchToProfile(profile) { d.profile = profile preferences.lastUsedProfileId = profile.id preferences.writeConfig() } ColumnLayout { anchors.fill: parent spacing: 0 ToolBar { KColorScheme { id: toolbarColorScheme colorGroup: KColorScheme.Active colorSet: KColorScheme.Complementary property color toolbarBackground: Qt.darker(toolbarColorScheme.shade(toolbarColorScheme.hoverDecoration, KColorScheme.MidShade, toolbarColorScheme.contrast, -0.2), 1.3) } visible: courseSelector.opacity > 0 id: header Layout.fillWidth: true height: 60 background: Rectangle { color: toolbarColorScheme.toolbarBackground } RowLayout { anchors.fill: parent spacing: 5 IconToolButton { id: profileButton icon: "user-identity" text: d.profile !== null? d.profile.name: "" color: toolbarColorScheme.normalText backgroundColor: toolbarColorScheme.normalBackground Layout.fillHeight: true Layout.preferredWidth: 300 onClicked: { if (checked) { profileSelectorSheet.open() } else { profileSelectorSheet.close() } } checkable: true } Item { Layout.fillWidth: true } IconToolButton { id: configureButton icon: "application-menu" color: toolbarColorScheme.normalText backgroundColor: toolbarColorScheme.normalBackground Layout.fillHeight: true Layout.preferredWidth: header.height onClicked: { var position = mapToItem(null, 0, height) ktouch.showMenu(position.x, position.y) } } } } Item { id: content Layout.fillWidth: true Layout.fillHeight: true RowLayout { anchors.fill: parent CourseSelector { id: courseSelector Layout.fillHeight: true Layout.preferredWidth: 300 opacity: 1 - initialProfileForm.opacity - courseModel: screen.courseModel profile: d.profile keyboardLayout: screen.keyboardLayout - keyboardLayoutName: screen.keyboardLayoutName + currentKeyboardLayoutName: screen.keyboardLayoutName onLessonSelected: screen.lessonSelected(course, lesson, d.profile) } Item { id: filler Layout.fillHeight: true Layout.fillWidth: true } } InitialProfileForm { id: initialProfileForm opacity: profileDataAccess.profileCount == 0? 1: 0 anchors.fill: parent anchors.margins: 5 Behavior on opacity { NumberAnimation { duration: screen.visible? 500: 0 easing.type: Easing.InOutCubic } } } SheetDialog { id: profileSelectorSheet anchors.fill: parent onOpened: { if (d.profile) { var index = profileDataAccess.indexOfProfile(d.profile) profileSelector.selectProfile(index) } } onClosed: { profileButton.checked = false; } content: ProfileSelector { id: profileSelector anchors.fill: parent onProfileChosen: { screen.switchToProfile(profile) profileSelectorSheet.close() } } } } } } diff --git a/src/qml/qml.qrc b/src/qml/qml.qrc index 6c2c915..1de2311 100644 --- a/src/qml/qml.qrc +++ b/src/qml/qml.qrc @@ -1,48 +1,49 @@ main.qml meters/AccuracyMeter.qml meters/CharactersPerMinuteMeter.qml meters/ElapsedTimeMeter.qml meters/Meter.qml meters/StatBox.qml common/DetailedRadioButton.qml common/InfoItem.qml common/InformationTable.qml common/InlineToolbar.qml common/LearningProgressChart.qml common/ListItem.qml common/MessageBox.qml common/SelectionGrip.qml common/SelectionRectangle.qml common/SheetDialog.qml common/Balloon.qml scorescreen/ScoreScreen.qml trainingscreen/KeyboardUnavailableNotice.qml trainingscreen/TrainingScreenMenuOverlay.qml trainingscreen/TrainingScreen.qml trainingscreen/TrainingScreenToolbar.qml trainingscreen/TrainingWidget.qml homescreen/CourseDescriptionItem.qml homescreen/CoursePage.qml homescreen/CourseSelector.qml homescreen/InitialProfileForm.qml homescreen/LessonLockedNotice.qml homescreen/LessonPreview.qml homescreen/LessonSelectorBase.qml homescreen/HomeScreen.qml homescreen/CustomLessonSelector.qml homescreen/ProfileForm.qml homescreen/LessonSelector.qml homescreen/ProfileSelector.qml homescreen/ProfileDetailsItem.qml keyboard/Keyboard.qml keyboard/KeyItem.qml keyboard/KeyLabel.qml keyboard/KeyboardLayoutEditor.qml common/IconToolButton.qml homescreen/CourseSelectorKeyboardLayoutItem.qml common/IconLabel.qml common/MonochromeIcon.qml + homescreen/CourseSelectorKeyboardLayoutList.qml