diff --git a/src/core/trainingaction.cpp b/src/core/trainingaction.cpp index 79d3c0a..0ba7e5f 100644 --- a/src/core/trainingaction.cpp +++ b/src/core/trainingaction.cpp @@ -1,86 +1,110 @@ /* * Copyright 2018-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 . */ #include "trainingaction.h" #include "trainingactionicon.h" #include "drawertrainingactions.h" #include "trainingsession.h" TrainingAction::TrainingAction(QObject *parent) : QObject(parent) , m_text(QString()) , m_icon(new TrainingActionIcon(this, QString())) //TODO "rating-unrated" vs. "rating" { } TrainingAction::TrainingAction(const QString &text, QObject *parent) : QObject(parent) , m_text(text) , m_icon(new TrainingActionIcon(this, QString())) //TODO "rating-unrated" vs. "rating" { } TrainingAction::TrainingAction(Phrase *phrase, TrainingSession *session, QObject* parent) : QObject(parent) , m_icon(new TrainingActionIcon(this, QString())) , m_phrase(phrase) , m_trainingSession(session) { if (m_phrase) { m_text = phrase->text(); } } void TrainingAction::appendChild(QObject* child) { - m_children.append(child); - emit childrenChanged(); + m_actions.append(child); + emit actionsChanged(); } bool TrainingAction::hasChildren() const { - return m_children.count() > 0; + return m_actions.count() > 0; } void TrainingAction::trigger() { if (m_phrase && m_trainingSession) { m_trainingSession->setPhrase(m_phrase); } } bool TrainingAction::enabled() const { return m_enabled; } void TrainingAction::setEnabled(bool enabled) { if (enabled == m_enabled) { return; } m_enabled = enabled; emit enabledChanged(m_enabled); } +bool TrainingAction::checked() const +{ + return m_checked; +} + +void TrainingAction::setChecked(bool checked) +{ + if (checked == m_checked) { + return; + } + m_checked = checked; + emit checkedChanged(m_checked); +} + QObject * TrainingAction::icon() const { return m_icon; } + +Phrase * TrainingAction::phrase() const +{ + return m_phrase; +} + +QList TrainingAction::actions() const +{ + return m_actions; +} diff --git a/src/core/trainingaction.h b/src/core/trainingaction.h index deb8ca5..e22d8e4 100644 --- a/src/core/trainingaction.h +++ b/src/core/trainingaction.h @@ -1,74 +1,79 @@ /* * Copyright 2018-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 . */ #ifndef TRAININGACTION_H #define TRAININGACTION_H #include "artikulatecore_export.h" #include "trainingactionicon.h" #include "phrase.h" #include "trainingsession.h" #include #include class DrawerTrainingActions; class ARTIKULATECORE_EXPORT TrainingAction : public QObject { Q_OBJECT Q_PROPERTY(QString text MEMBER m_text CONSTANT) Q_PROPERTY(QObject* icon READ icon CONSTANT) Q_PROPERTY(bool visible MEMBER m_visible CONSTANT) Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) - Q_PROPERTY(bool checked MEMBER m_checked CONSTANT) + Q_PROPERTY(bool checked READ checked NOTIFY checkedChanged) Q_PROPERTY(QString tooltip MEMBER m_tooltip CONSTANT) - Q_PROPERTY(QList children MEMBER m_children NOTIFY childrenChanged) + Q_PROPERTY(QList children READ actions NOTIFY actionsChanged) Q_PROPERTY(bool checkable MEMBER m_checkable CONSTANT) -Q_SIGNALS: - void changed(); - void childrenChanged(); - void enabledChanged(bool enabled); - public: TrainingAction(QObject *parent = nullptr); TrainingAction(const QString &text, QObject *parent = nullptr); TrainingAction(Phrase *phrase, TrainingSession *session, QObject *parent = nullptr); void appendChild(QObject *child); bool hasChildren() const; Q_INVOKABLE void trigger(); bool enabled() const; void setEnabled(bool enabled); + void setChecked(bool checked); + bool checked() const; QObject * icon() const; + Phrase * phrase() const; + QList actions() const; + +Q_SIGNALS: + void changed(); + void actionsChanged(); + void enabledChanged(bool enabled); + void checkedChanged(bool checked); private: QString m_text; TrainingActionIcon *m_icon{nullptr}; bool m_visible{true}; bool m_enabled{true}; bool m_checked{false}; bool m_checkable{false}; QString m_tooltip{QString()}; - QList m_children; + QList m_actions; Phrase *m_phrase{nullptr}; TrainingSession * m_trainingSession{nullptr}; }; #endif diff --git a/src/core/trainingsession.cpp b/src/core/trainingsession.cpp index 6ba9b8b..36bd919 100644 --- a/src/core/trainingsession.cpp +++ b/src/core/trainingsession.cpp @@ -1,232 +1,271 @@ /* * 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 . */ #include "trainingsession.h" #include "core/language.h" #include "core/course.h" #include "core/unit.h" #include "core/phrase.h" #include "profilemanager.h" #include "learner.h" #include "trainingaction.h" #include "artikulate_debug.h" TrainingSession::TrainingSession(QObject *parent) : QObject(parent) , m_profileManager(nullptr) , m_course(nullptr) , m_unit(nullptr) - , m_phrase(nullptr) { } void TrainingSession::setProfileManager(LearnerProfile::ProfileManager *manager) { if (m_profileManager == manager) { return; } m_profileManager = manager; } Course * TrainingSession::course() const { return m_course; } void TrainingSession::setCourse(Course *course) { if (!course) { return; } if (m_course == course) { return; } m_course = course; if (m_course && m_course->unitList().count() > 0) { setUnit(m_course->unitList().first()); } // lazy loading of training data LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->id()); if (!goal) { goal = m_profileManager->registerGoal( LearnerProfile::LearningGoal::Language, course->language()->id(), course->language()->i18nTitle() ); } auto data = m_profileManager->progressValues(m_profileManager->activeProfile(), goal, m_course->id() ); Q_FOREACH(Unit *unit, m_course->unitList()) { Q_FOREACH(Phrase *phrase, unit->phraseList()) { auto iter = data.find(phrase->id()); if (iter != data.end()) { phrase->setProgress(iter.value()); } } } emit courseChanged(); } Unit * TrainingSession::unit() const { return m_unit; } void TrainingSession::setUnit(Unit *unit) { if (m_unit == unit) { return; } m_unit = unit; if (m_unit && m_unit->phraseList().count() > 0) { setPhrase(m_unit->phraseList().first()); } return unitChanged(); } -Phrase * TrainingSession::phrase() const +TrainingAction * TrainingSession::activeAction() const { - return m_phrase; + if (m_indexUnit < 0 || m_indexPhrase < 0) { + return nullptr; + } + return qobject_cast(m_actions.at(m_indexUnit)->actions().at(m_indexPhrase)); } -void TrainingSession::setPhrase(Phrase *phrase) +Phrase * TrainingSession::activePhrase() const { - if (m_phrase == phrase) { - return; + if (const auto action = activeAction()) { + return action->phrase(); } - setUnit(phrase->unit()); - m_phrase = phrase; - return phraseChanged(); + return nullptr; } -Phrase * TrainingSession::nextPhrase() const +void TrainingSession::setPhrase(Phrase *phrase) { - if (!m_phrase) { - return nullptr; - } - const int index = m_phrase->unit()->phraseList().indexOf(m_phrase); - if (index < m_phrase->unit()->phraseList().length() - 1) { - return m_phrase->unit()->phraseList().at(index + 1); - } else { - Unit *unit = m_phrase->unit(); - int uIndex = unit->course()->unitList().indexOf(unit); - if (uIndex < unit->course()->unitList().length() - 1) { - return unit->course()->unitList().at(uIndex + 1)->phraseList().first(); + for (int i = 0; i < m_actions.count(); ++i) { + for (int j = 0; j < m_actions.at(i)->actions().count(); ++j) { + const auto testPhrase = qobject_cast(m_actions.at(i)->actions().at(j))->phrase(); + if (phrase == testPhrase) { + if (auto action = activeAction()) { + action->setChecked(false); + } + m_indexUnit = i; + m_indexPhrase = j; + if (auto action = activeAction()) { + action->setChecked(true); + } + emit phraseChanged(); + return; + } } } - return nullptr; } -void TrainingSession::showNextPhrase() +void TrainingSession::accept() { + Q_ASSERT(m_indexUnit >= 0); + Q_ASSERT(m_indexPhrase >= 0); + if (m_indexUnit < 0 || m_indexPhrase < 0) { + return; + } + auto phrase = activePhrase(); + // possibly update goals of learner updateGoal(); - m_phrase->updateProgress(Phrase::Progress::Done); + phrase->updateProgress(Phrase::Progress::Done); // store training activity LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->language()->id()); m_profileManager->recordProgress(m_profileManager->activeProfile(), goal, m_course->id(), - m_phrase->id(), + phrase->id(), static_cast(LearnerProfile::ProfileManager::Skip), - m_phrase->progress() + phrase->progress() ); - setPhrase(nextPhrase()); + selectNextPhrase(); } -void TrainingSession::skipPhrase() +void TrainingSession::skip() { + Q_ASSERT(m_indexUnit >= 0); + Q_ASSERT(m_indexPhrase >= 0); + if (m_indexUnit < 0 || m_indexPhrase < 0) { + return; + } + // possibly update goals of learner updateGoal(); - m_phrase->updateProgress(Phrase::Progress::Skip); + auto phrase = activePhrase(); + phrase->updateProgress(Phrase::Progress::Skip); // store training activity LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->language()->id()); m_profileManager->recordProgress(m_profileManager->activeProfile(), goal, m_course->id(), - m_phrase->id(), + phrase->id(), static_cast(LearnerProfile::ProfileManager::Skip), - m_phrase->progress() + phrase->progress() ); - setPhrase(nextPhrase()); + selectNextPhrase(); +} + +void TrainingSession::selectNextPhrase() +{ + if (auto action = activeAction()) { + action->setChecked(false); + } + // try to find next phrase, otherwise return completed + if (m_indexPhrase >= m_actions.at(m_indexUnit)->actions().count() - 1) { + if (m_indexUnit >= m_actions.count() - 1) { + emit completed(); + } else { + ++m_indexUnit; + m_indexPhrase = 0; + } + } else { + ++m_indexPhrase; + } + if (auto action = activeAction()) { + action->setChecked(true); + } + emit phraseChanged(); } -bool TrainingSession::hasNextPhrase() const +bool TrainingSession::hasNext() const { - return nextPhrase() != nullptr; + return m_indexUnit < m_actions.count() - 1 || m_indexPhrase < m_actions.last()->actions().count() - 1; } void TrainingSession::updateGoal() { if (!m_profileManager) { qCWarning(ARTIKULATE_LOG) << "No ProfileManager registered, aborting operation"; return; } LearnerProfile::Learner *learner = m_profileManager->activeProfile(); if (!learner) { qCWarning(ARTIKULATE_LOG) << "No active Learner registered, aborting operation"; return; } LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->language()->id()); learner->addGoal(goal); learner->setActiveGoal(goal); } QVector TrainingSession::trainingActions() { // cleanup for (const auto &action : m_actions) { action->deleteLater(); } m_actions.clear(); if (!m_course) { return QVector(); } for (const auto &unit : m_course->unitList()) { auto action = new TrainingAction(unit->title()); for (const auto &phrase : unit->phraseList()) { if (phrase->sound().isEmpty()) { continue; } action->appendChild(new TrainingAction(phrase, this, unit)); } if (action->hasChildren()) { m_actions.append(action); } else { action->deleteLater(); } } return m_actions; } diff --git a/src/core/trainingsession.h b/src/core/trainingsession.h index 33f3682..81fc26b 100644 --- a/src/core/trainingsession.h +++ b/src/core/trainingsession.h @@ -1,84 +1,90 @@ /* * 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 . */ #ifndef TRAININGSESSION_H #define TRAININGSESSION_H #include "artikulatecore_export.h" #include "course.h" #include "phrase.h" #include class QString; class Language; class Course; class Unit; class TrainingAction; namespace LearnerProfile { class ProfileManager; } /** * \class TrainingSession */ class ARTIKULATECORE_EXPORT TrainingSession : public QObject { Q_OBJECT Q_PROPERTY(Course *course READ course WRITE setCourse NOTIFY courseChanged) Q_PROPERTY(Unit *unit READ unit WRITE setUnit NOTIFY unitChanged) - Q_PROPERTY(Phrase *phrase READ phrase WRITE setPhrase NOTIFY phraseChanged) - Q_PROPERTY(bool hasNextPhrase READ hasNextPhrase NOTIFY phraseChanged) + Q_PROPERTY(Phrase *phrase READ activePhrase WRITE setPhrase NOTIFY phraseChanged) + Q_PROPERTY(bool hasNext READ hasNext NOTIFY phraseChanged) public: explicit TrainingSession(QObject *parent = nullptr); void setProfileManager(LearnerProfile::ProfileManager *manager); Course * course() const; void setCourse(Course *course); Unit * unit() const; void setUnit(Unit *unit); - Phrase::Type phraseType() const; - void setPhraseType(Phrase::Type type); - Phrase * phrase() const; + TrainingAction * activeAction() const; + Phrase * activePhrase() const; void setPhrase(Phrase *phrase); bool hasPreviousPhrase() const; - bool hasNextPhrase() const; - Q_INVOKABLE void showNextPhrase(); - Q_INVOKABLE void skipPhrase(); + bool hasNext() const; + Q_INVOKABLE void accept(); + Q_INVOKABLE void skip(); QVector trainingActions(); Q_SIGNALS: void courseChanged(); void unitChanged(); void phraseChanged(); + /** + * @brief Emitted when last phrase of session is skipped or marked as completed. + */ + void completed(); private: Q_DISABLE_COPY(TrainingSession) + void selectNextPhrase(); Phrase * nextPhrase() const; void updateGoal(); LearnerProfile::ProfileManager *m_profileManager; Course *m_course; Unit *m_unit; - Phrase *m_phrase; QVector m_actions; + + int m_indexUnit{-1}; + int m_indexPhrase{-1}; }; #endif diff --git a/src/qml/TrainingPage.qml b/src/qml/TrainingPage.qml index 9d31c78..12ab098 100644 --- a/src/qml/TrainingPage.qml +++ b/src/qml/TrainingPage.qml @@ -1,159 +1,161 @@ /* * 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.0 as Kirigami2 import artikulate 1.0 Kirigami2.Page { id: root readonly property color colorTask: "#1dbf4e" readonly property color colorAnswer: "#7e48a5" title: { var titleString = ""; if (g_trainingSession.unit === null) { titleString += i18n("Category: no category selected"); } else { titleString += i18n("Category: ") + g_trainingSession.unit.title } if (g_trainingSession.unit !== null && g_trainingSession.course !== null) { titleString += " / " + g_trainingSession.course.i18nTitle } return titleString } actions { main: Kirigami2.Action { text: i18n("Next") + tooltip: i18n("Mark current phrase as completed and proceed with next one.") iconName: "dialog-ok" - onTriggered: g_trainingSession.showNextPhrase() + onTriggered: g_trainingSession.accept() } right: Kirigami2.Action { text: i18n("Skip") + tooltip: i18n("Skip current phrase and proceed with next one.") iconName: "go-next" - enabled: g_trainingSession.hasNextPhrase - onTriggered: g_trainingSession.skipPhrase() + 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: 10 horizontalCenter: taskTriangle.right } 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: 150 } 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: 10 horizontalCenter: answerTriangle.left } } SoundPlayer { id: player anchors { centerIn: parent } fileUrl: recorder.outputFileUrl } } }