diff --git a/liblearnerprofile/src/models/learninggoalmodel.cpp b/liblearnerprofile/src/models/learninggoalmodel.cpp index 0eca6d4..f30c70d 100644 --- a/liblearnerprofile/src/models/learninggoalmodel.cpp +++ b/liblearnerprofile/src/models/learninggoalmodel.cpp @@ -1,243 +1,243 @@ /* * Copyright 2013-2016 Andreas Cord-Landwehr * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "learninggoalmodel.h" #include "profilemanager.h" #include "learner.h" #include #include #include #include #include "liblearner_debug.h" using namespace LearnerProfile; // private class LearningGoalModelPrivate class LearningGoalModelPrivate { public: LearningGoalModelPrivate() : m_profileManager(nullptr) , m_learner(nullptr) , m_signalMapper(new QSignalMapper()) { } ~LearningGoalModelPrivate() { delete m_signalMapper; } void updateGoals(); void updateMappings(); ProfileManager *m_profileManager; Learner *m_learner; QList m_goals; QSignalMapper *m_signalMapper; }; void LearningGoalModelPrivate::updateGoals() { m_goals.clear(); // set all registered goals from profile manager if (m_profileManager) { foreach (LearningGoal *goal, m_profileManager->goals()) { m_goals.append(goal); } } // TODO add learner status information } void LearningGoalModelPrivate::updateMappings() { if (!m_profileManager) { return; } int goals = m_goals.count(); for (int i = 0; i < goals; ++i) { m_signalMapper->setMapping(m_goals.at(i), i); } } // class LearningGoalModel LearningGoalModel::LearningGoalModel(QObject *parent) : QAbstractListModel(parent) , d(new LearningGoalModelPrivate) { connect(d->m_signalMapper, static_cast(&QSignalMapper::mapped), this, &LearningGoalModel::emitLearningGoalChanged); } LearningGoalModel::~LearningGoalModel() { } QHash< int, QByteArray > LearningGoalModel::roleNames() const { QHash roles; roles[TitleRole] = "title"; roles[IdRole] = "id"; roles[DataRole] = "dataRole"; return roles; } void LearningGoalModel::setProfileManager(ProfileManager *profileManager) { if (d->m_profileManager == profileManager) { return; } beginResetModel(); if (d->m_profileManager) { d->m_profileManager->disconnect(this); } d->m_profileManager = profileManager; d->updateGoals(); d->updateMappings(); endResetModel(); emit profileManagerChanged(); } ProfileManager * LearningGoalModel::profileManager() const { return d->m_profileManager; } Learner * LearningGoalModel::learner() const { return d->m_learner; } void LearningGoalModel::setLearner(Learner *learner) { if (!learner) { return; } beginResetModel(); if (d->m_learner) { learner->disconnect(this); } d->m_learner = learner; d->updateGoals(); d->updateMappings(); connect(learner, &Learner::goalAboutToBeAdded, this, &LearningGoalModel::onLearningGoalAboutToBeAdded); connect(learner, &Learner::goalAdded, this, &LearningGoalModel::onLearningGoalAdded); connect(learner, &Learner::goalAboutToBeRemoved, this, &LearningGoalModel::onLearningGoalAboutToBeRemoved); emit learnerChanged(); endResetModel(); } QVariant LearningGoalModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } if (index.row() >= d->m_goals.count()) { return QVariant(); } LearningGoal * const goal = d->m_goals.at(index.row()); switch(role) { case Qt::DisplayRole: return !goal->name().isEmpty()? - QVariant(goal->name()): QVariant(i18nc("@item:inlistbox:", "unknown")); + QVariant(goal->name()): QVariant(i18nc("@item:inlistbox unknown learning goal", "unknown")); case Qt::ToolTipRole: return QVariant(goal->name()); case TitleRole: return goal->name(); case IdRole: return goal->identifier(); case DataRole: return QVariant::fromValue(goal); default: return QVariant(); } } int LearningGoalModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return d->m_goals.count(); } void LearningGoalModel::onLearningGoalAboutToBeAdded(LearningGoal *goal, int index) { Q_UNUSED(index) beginInsertRows(QModelIndex(), d->m_goals.count(), d->m_goals.count()); d->m_goals.append(goal); d->updateMappings(); } void LearningGoalModel::onLearningGoalAdded() { endInsertRows(); } void LearningGoalModel::onLearningGoalAboutToBeRemoved(int index) { if (!d->m_learner) { return; } if (index < 0 || d->m_goals.count() <= index) { qCWarning(LIBLEARNER_LOG) << "Cannot remove learning goal from model, not registered"; return; } beginRemoveRows(QModelIndex(), index, index); d->m_goals.removeAt(index); d->updateMappings(); endRemoveRows(); } void LearningGoalModel::emitLearningGoalChanged(int row) { emit learningGoalChanged(row); emit dataChanged(index(row, 0), index(row, 0)); } QVariant LearningGoalModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) { return QVariant(); } if (orientation == Qt::Vertical) { return QVariant(section + 1); } return QVariant(i18nc("@title:column", "Learning Goal")); } QVariant LearningGoalModel::learningGoal(int row) const { return data(index(row, 0), LearningGoalModel::DataRole); } diff --git a/src/qml/DownloadPage.qml b/src/qml/DownloadPage.qml index b4d749b..d538cea 100644 --- a/src/qml/DownloadPage.qml +++ b/src/qml/DownloadPage.qml @@ -1,128 +1,128 @@ /* * Copyright 2018 Andreas Cord-Landwehr * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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.1 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.1 as QQC2 import org.kde.kirigami 2.0 as Kirigami import org.kde.newstuff 1.0 as KNS Kirigami.Page { id: root /** * emitted whenever GHNS resources changed */ signal statusChanged(); title: i18n("Download Training Material") background: Rectangle { color: "#ffffff" } Component { id: courseDownloadItem Kirigami.AbstractListItem { id: listItem height: 50 width: parent.width text: model.name readonly property var status: model.status onStatusChanged: { root.statusChanged(); } checkable: false RowLayout { id: layout spacing: Kirigami.Units.smallSpacing*2 Kirigami.Icon { height: Kirigami.Units.iconSizes.smallMedium; width: height; SequentialAnimation on opacity { loops: Animation.Infinite; running: model.status == KNS.ItemsModel.InstallingStatus || model.status == KNS.ItemsModel.UpdatingStatus NumberAnimation { to: 0; duration: 500; } NumberAnimation { to: 1; duration: 500; } onRunningChanged: { if (!running) parent.opacity = 1; } } source: { // use complete list of KNS status messages if (model.status == KNS.ItemsModel.InvalidStatus) return "emblem-error"; if (model.status == KNS.ItemsModel.DownloadableStatus) return "vcs-added"; if (model.status == KNS.ItemsModel.InstalledStatus) return "vcs-normal"; if (model.status == KNS.ItemsModel.UpdateableStatus) return "vcs-update-required"; if (model.status == KNS.ItemsModel.DeletedStatus) return "vcs-added"; if (model.status == KNS.ItemsModel.InstallingStatus) return "vcs-locally-modified"; if (model.status == KNS.ItemsModel.UpdatingStatus) return "vcs-locally-modified"; return "emblem-error"; } } QQC2.Label { id: labelItem Layout.fillWidth: true text: listItem.text color: layout.indicateActiveFocus && (listItem.highlighted || listItem.checked || listItem.pressed) ? listItem.activeTextColor : listItem.textColor elide: Text.ElideRight font: listItem.font } QQC2.Button { visible: (model.status == KNS.ItemsModel.UpdateableStatus) ? true : false; - text: i18n("update") + text: i18nc("@action:button", "Update") onClicked: newStuffModel.installItem(model.index) } QQC2.Button { visible: (model.status == KNS.ItemsModel.DownloadableStatus || model.status == KNS.ItemsModel.DeletedStatus) ? true : false; - text: i18n("install") + text: i18nc("@action:button", "Install") onClicked: newStuffModel.installItem(model.index) } QQC2.Button { visible: (model.status == KNS.ItemsModel.InstalledStatus || model.status == KNS.ItemsModel.UpdateableStatus) ? true : false; - text: i18n("remove") + text: i18nc("@action:button", "Remove") onClicked: newStuffModel.uninstallItem(model.index) } } } } ColumnLayout { ListView { id: listView width: root.width - 40 height: 50 * listView.count delegate: courseDownloadItem model: KNS.ItemsModel { id: newStuffModel; engine: newStuffEngine.engine; } KNS.Engine { id: newStuffEngine; configFile: ":/artikulate/config/artikulate.knsrc"; onMessage: console.log("KNS Message: " + message); onIdleMessage: console.log("KNS Idle: " + message); onBusyMessage: console.log("KNS Busy: " + message); onErrorMessage: console.log("KNS Error: " + message); } } } } diff --git a/src/qml/PhraseEditor.qml b/src/qml/PhraseEditor.qml index 9d044d9..49b3030 100644 --- a/src/qml/PhraseEditor.qml +++ b/src/qml/PhraseEditor.qml @@ -1,186 +1,186 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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.10 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import artikulate 1.0 Item { id: root property Phrase phrase property bool isSkeletonPhrase: false // use for saving property int __changedPhraseType property string __changedPhraseText width: 500 height: editLoader.height Component { id: editComponent Row { width: root.width height: { if (!root.isSkeletonPhrase) textEdit.height + phonemeGrid.height + phraseEditStateSetter.height + phraseRecorder.height + phraseTypeSetter.height; else { // height if only editing skeleton textEdit.height + phraseTypeSetter.height; } } ColumnLayout { id: textEdit height: inputLine.height + originalPhraseInfo.height width: parent.width spacing: 5 Row { id: originalPhraseInfo property string originalPhrase : (root.phrase != null) ? root.phrase.i18nText : "" spacing: 10 visible: { root.phrase != null && originalPhrase != "" && !root.isSkeletonPhrase} Text { - text: i18n("Original Phrase:") + " " + originalPhraseInfo.originalPhrase + "" + text: i18n("Original Phrase: %1", originalPhraseInfo.originalPhrase) width: root.width - 70 wrapMode: Text.WordWrap } } RowLayout { // controls for setting phrase id: inputLine TextArea { id: phraseInput property Phrase phrase: root.phrase Layout.fillWidth: true Layout.maximumHeight: 100 text: root.phrase.text onTextChanged: { if (root.phrase == null) { return } root.phrase.text = text } onPhraseChanged: { if (root.phrase != null) text = root.phrase.text else text = "" } } } PhraseEditorTypeComponent { id: phraseTypeSetter phrase: root.phrase } PhraseEditorSoundComponent { id: phraseRecorder visible: !root.isSkeletonPhrase phrase: root.phrase } Component { id: phonemeItem Text { Button { width: 100 text: model.title checkable: true checked: { phrase != null && phrase.hasPhoneme(model.dataRole) } onClicked: { //TODO this button has no undo operation yet if (checked) { phrase.addPhoneme(model.dataRole) } else { phrase.removePhoneme(model.dataRole) } } } } } GridView { id: phonemeGrid property int columns : width / cellWidth width: root.width height: 30 * count / columns + 60 cellWidth: 100 cellHeight: 30 model: PhonemeModel { language: { (phrase != null) ? root.phrase.unit.course.language : null } } delegate: phonemeItem } RowLayout { id: controls anchors { left: parent.left right: parent.right } PhraseEditorEditStateComponent { id: phraseEditStateSetter visible: !root.isSkeletonPhrase phrase: root.phrase } Label { // dummy Layout.fillWidth: true } ToolButton { Layout.alignment: Qt.AlignBottom width: 48 height: 48 enabled: g_editorSession.hasPreviousPhrase icon.name: "go-previous" onClicked: { g_editorSession.switchToPreviousPhrase() } } ToolButton { Layout.alignment: Qt.AlignBottom width: 48 height: 48 enabled: g_editorSession.hasNextPhrase icon.name: "go-next" onClicked: { g_editorSession.switchToNextPhrase() } } } } } } ColumnLayout { id: phraseRow Loader { id: editLoader sourceComponent: (phrase != null) ? editComponent : undefined onSourceComponentChanged: { if (sourceComponent == undefined) height = 0 else height = editComponent.height } } } } diff --git a/src/qml/PhraseEditorEditStateComponent.qml b/src/qml/PhraseEditorEditStateComponent.qml index 807fbd4..0f88306 100644 --- a/src/qml/PhraseEditorEditStateComponent.qml +++ b/src/qml/PhraseEditorEditStateComponent.qml @@ -1,94 +1,94 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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.1 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.2 import artikulate 1.0 Item { id: root property Phrase phrase width: buttons.width height: buttons.height Component.onCompleted: { updateCheckedStates(); } onPhraseChanged: { updateCheckedStates() } function updateCheckedStates() { if (root.phrase == null) { return; } switch (root.phrase.editState) { case Phrase.Unknown: buttonUnknown.checked = true; break; case Phrase.Translated: buttonTranslated.checked = true; break; case Phrase.Completed: buttonCompleted.checked = true; break; } } GroupBox { id: buttons title: i18n("Edit State:") RowLayout { ExclusiveGroup { id: editStateGroup } RadioButton { id: buttonUnknown - text: i18n("Unknown") + text: i18nc("state", "Unknown") onCheckedChanged: { if (!checked) return root.phrase.editState = Phrase.Unknown } exclusiveGroup: editStateGroup } RadioButton { id: buttonTranslated - text: i18n("Translated") + text: i18nc("state", "Translated") onCheckedChanged: { if (!checked) return root.phrase.editState = Phrase.Translated } exclusiveGroup: editStateGroup } RadioButton { id: buttonCompleted - text: i18n("Completed") + text: i18nc("state", "Completed") onCheckedChanged: { if (!checked) return root.phrase.editState = Phrase.Completed } exclusiveGroup: editStateGroup } } } } diff --git a/src/qml/PhraseEditorSoundComponent.qml b/src/qml/PhraseEditorSoundComponent.qml index 300ab62..ab7ca15 100644 --- a/src/qml/PhraseEditorSoundComponent.qml +++ b/src/qml/PhraseEditorSoundComponent.qml @@ -1,89 +1,89 @@ /* * Copyright 2013-2019 Andreas Cord-Landwehr * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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.5 import QtQuick.Controls 2.3 import artikulate 1.0 Item { id: root property Phrase phrase width: mediaController.width height: mediaController.height Column { id: mediaController Text { id: componentTitle text: i18n("Native Speaker Recording") font.pointSize: 14; } Row { anchors { left: componentTitle.left; leftMargin: 30 } height: 48 Text { anchors.verticalCenter: parent.verticalCenter text: i18n("Existing Recording:") } SoundPlayer { fileUrl: root.phrase == null ? "" : phrase.soundFileUrl } } Row { anchors { left: componentTitle.left; leftMargin: 30 } Text { anchors.verticalCenter: parent.verticalCenter text: i18n("Create New Recording:") } SoundRecorder { id: recorder } SoundPlayer { fileUrl: recorder.outputFileUrl } } Row { anchors { left: componentTitle.left; leftMargin: 30 } visible: recorder.outputFileUrl != "" ToolButton { anchors.verticalCenter: parent.verticalCenter icon.name: "dialog-ok-apply" - text: i18n("Replace existing recording") + text: i18n("Replace Existing Recording") onClicked: { recorder.storeToFile(phrase.soundFileOutputPath()) } } ToolButton { anchors.verticalCenter: parent.verticalCenter icon.name: "dialog-cancel" text: i18n("Dismiss") onClicked: { recorder.clearBuffer() } } } } } diff --git a/src/qml/TrainingPage.qml b/src/qml/TrainingPage.qml index d4e6638..c6a59b2 100644 --- a/src/qml/TrainingPage.qml +++ b/src/qml/TrainingPage.qml @@ -1,168 +1,168 @@ /* * Copyright 2013-2019 Andreas Cord-Landwehr * Copyright 2013 Magdalena Konkiewicz * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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.5 import QtQuick.Shapes 1.10 import QtQuick.Controls 2.3 as QQC2 import org.kde.kirigami 2.7 as Kirigami import artikulate 1.0 Kirigami.Page { id: root readonly property color colorTask: "#1dbf4e" readonly property color colorAnswer: "#7e48a5" background: Rectangle { color: "#ffffff" } title: { var titleString = ""; if (g_trainingSession.unit === null) { titleString += i18n("Category: no category selected"); } else { - titleString += i18n("Category: ") + g_trainingSession.unit.title + titleString += i18n("Category: %1", g_trainingSession.unit.title) } if (g_trainingSession.unit !== null && g_trainingSession.course !== null) { titleString += " / " + g_trainingSession.course.i18nTitle } return titleString } actions { main: Kirigami.Action { text: i18n("Next") tooltip: i18n("Mark current phrase as completed and proceed with next one.") iconName: "dialog-ok" onTriggered: g_trainingSession.accept() } right: Kirigami.Action { text: i18n("Skip") tooltip: i18n("Skip current phrase and proceed with next one.") iconName: "go-next" enabled: g_trainingSession.hasNext onTriggered: g_trainingSession.skip() } } Rectangle { id: trainingTextRect width: Math.min(0.7 * parent.width, parent.width - 80) height: Math.max(200, phraseText.implicitHeight) anchors { left: parent.left top: parent.top leftMargin: 20 topMargin: 20 } color: root.colorTask Shape { id: taskTriangle width: 50 height: 40 anchors { top: parent.bottom horizontalCenter: parent.horizontalCenter horizontalCenterOffset: parent.width / 10 } ShapePath { fillColor: colorTask strokeColor: colorTask PathLine { x: 0; y: 0 } PathLine { x: taskTriangle.width; y: taskTriangle.height } PathLine { x: taskTriangle.width; y: 0 } } } QQC2.TextArea { id: phraseText anchors.fill: parent objectName: "phraseText" text: (g_trainingSession.phrase !== null) ? g_trainingSession.phrase.text : "" font.pointSize: 24 wrapMode: Text.WordWrap readOnly: true background: Item {} horizontalAlignment: Text.AlignHCenter verticalAlignment: TextEdit.AlignVCenter } SoundPlayer { id: buttonNativePlay anchors { top: taskTriangle.bottom topMargin: 20 horizontalCenter: taskTriangle.right } text: i18n("Play original") fileUrl: g_trainingSession.phrase === null ? "" : g_trainingSession.phrase.soundFileUrl } } Rectangle { id: trainingUserRect width: 200 height: 0.65 * width anchors { right: parent.right top: trainingTextRect.bottom rightMargin: 20 topMargin: 154 } color: root.colorAnswer Shape { id: answerTriangle width: 50 height: 40 anchors { bottom: parent.top horizontalCenter: parent.horizontalCenter horizontalCenterOffset: -parent.width / 10 } ShapePath { fillColor: root.colorAnswer strokeColor: root.colorAnswer PathLine { x: 0; y: 0 } PathLine { x: 0; y: taskTriangle.height } PathLine { x: taskTriangle.width; y: taskTriangle.height } } } SoundRecorder { id: recorder anchors { bottom: answerTriangle.top bottomMargin: 20 horizontalCenter: answerTriangle.left } text: i18n("Record yourself") } SoundPlayer { id: player anchors { centerIn: parent } text: i18n("Play yourself") fileUrl: recorder.outputFileUrl } } } diff --git a/src/qml/WelcomePage.qml b/src/qml/WelcomePage.qml index 3943db1..d57db56 100644 --- a/src/qml/WelcomePage.qml +++ b/src/qml/WelcomePage.qml @@ -1,85 +1,85 @@ /* * Copyright 2015-2019 Andreas Cord-Landwehr * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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.1 import QtQuick.Controls 2.1 as QQC2 import QtQuick.Layouts 1.3 import org.kde.kirigami 2.4 as Kirigami import artikulate 1.0 Kirigami.ScrollablePage { id: root title: i18n("Welcome to Artikulate") Kirigami.CardsListView { id: listView width: root.width - 40 model: CourseModel { id: courseModel } delegate: Kirigami.AbstractCard { contentItem: Item { implicitWidth: delegateLayout.implicitWidth implicitHeight: delegateLayout.implicitHeight GridLayout { id: delegateLayout anchors { left: parent.left top: parent.top right: parent.right } rowSpacing: Kirigami.Units.largeSpacing columnSpacing: Kirigami.Units.largeSpacing columns: width > Kirigami.Units.gridUnit * 20 ? 4 : 2 Kirigami.Icon { source: "language-artikulate" Layout.fillHeight: true Layout.maximumHeight: Kirigami.Units.iconSizes.huge Layout.preferredWidth: height } ColumnLayout { Kirigami.Heading { level: 2 - text: model.language.title + " / " + model.title + text: i18nc("@title:window language / course name", "%1 / %2", model.language.title, model.title) } Kirigami.Separator { Layout.fillWidth: true } QQC2.Label { Layout.fillWidth: true wrapMode: Text.WordWrap text: model.description } } QQC2.Button { Layout.alignment: Qt.AlignRight|Qt.AlignVCenter Layout.columnSpan: 2 - text: qsTr("Start Training") + text: i18nc("@action:button", "Start Training") onClicked: { showPassiveNotification("Starting training session for course " + model.title + "."); g_trainingSession.course = model.dataRole } } } } } } }