diff --git a/autotests/unittests/trainingsession/test_trainingsession.cpp b/autotests/unittests/trainingsession/test_trainingsession.cpp index b91435f..7c018cb 100644 --- a/autotests/unittests/trainingsession/test_trainingsession.cpp +++ b/autotests/unittests/trainingsession/test_trainingsession.cpp @@ -1,207 +1,207 @@ /* * Copyright 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 "test_trainingsession.h" #include "src/core/trainingsession.h" #include "src/core/icourse.h" #include "src/core/language.h" #include "src/core/unit.h" #include "src/core/trainingaction.h" #include "../mocks/coursestub.h" #include "../mocks/languagestub.h" #include "liblearnerprofile/src/profilemanager.h" #include #include // assumption: during a training session the units and phrases of a course do not change // any change of such a course shall result in a reload of a training session void TestTrainingSession::init() { // no initialization of test case } void TestTrainingSession::cleanup() { // no cleanup after test run } void TestTrainingSession::createTrainingSessionWithoutUnits() { auto language = std::make_shared("de"); CourseStub course(language, QVector>()); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::createTrainingSessionWithEmptySounds() { auto language = std::make_shared("de"); auto unitA = Unit::create(); auto unitB = Unit::create(); std::shared_ptr phraseA1 = Phrase::create(); std::shared_ptr phraseA2 = Phrase::create(); std::shared_ptr phraseB1 = Phrase::create(); std::shared_ptr phraseB2 = Phrase::create(); // note: phrases without soundfiles are skipped in session generation phraseA1->setId("A1"); phraseA2->setId("A2"); phraseB1->setId("B1"); phraseB2->setId("B2"); phraseA1->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); unitA->addPhrase(phraseA1); unitA->addPhrase(phraseA2); unitB->addPhrase(phraseB1); unitB->addPhrase(phraseB2); CourseStub course(language, QVector>({unitA, unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); // test number of actions auto actions = session.trainingActions(); QCOMPARE(actions.count(), 1); QCOMPARE(actions.at(0)->actions().count(), 1); } void TestTrainingSession::createTrainingSessionWithEmptyUnits() { auto language = std::make_shared("de"); auto unitA = Unit::create(); auto unitB = Unit::create(); CourseStub course(language, QVector>({unitA, unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::createTrainingSessionWithUnitsAndPhrases() { auto language = std::make_shared("de"); auto unit = Unit::create(); std::shared_ptr firstPhrase = Phrase::create(); std::shared_ptr secondPhrase = Phrase::create(); unit->addPhrase(firstPhrase); unit->addPhrase(secondPhrase); CourseStub course(language, QVector>({unit})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::iterateCourse() { auto language = std::make_shared("de"); auto unitA = Unit::create(); auto unitB = Unit::create(); std::shared_ptr phraseA1 = Phrase::create(); std::shared_ptr phraseA2 = Phrase::create(); std::shared_ptr phraseB1 = Phrase::create(); std::shared_ptr phraseB2 = Phrase::create(); // note: phrases without soundfiles are skipped in session generation phraseA1->setId("A1"); phraseA2->setId("A2"); phraseB1->setId("B1"); phraseB2->setId("B2"); phraseA1->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseA2->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseB1->setSound(QUrl::fromLocalFile("/tmp/b1.ogg")); phraseB2->setSound(QUrl::fromLocalFile("/tmp/b2.ogg")); unitA->addPhrase(phraseA1); unitA->addPhrase(phraseA2); unitB->addPhrase(phraseB1); unitB->addPhrase(phraseB2); CourseStub course(language, QVector>({unitA, unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); // session assumed to initialize with first units's first phrase QCOMPARE(session.activeUnit()->self(), unitA); QCOMPARE(session.activePhrase()->self(), phraseA1); QVERIFY(&course == session.course()); // test direct unit setters session.setUnit(unitA.get()); QCOMPARE(session.activeUnit(), unitA.get()); session.setUnit(unitB.get()); QCOMPARE(session.activeUnit(), unitB.get()); // test direct phrase setters - session.setPhrase(phraseA1.get()); + session.setActivePhrase(phraseA1.get()); QCOMPARE(session.activePhrase(), phraseA1.get()); QCOMPARE(session.activeUnit(), unitA.get()); - session.setPhrase(phraseB1.get()); + session.setActivePhrase(phraseB1.get()); QCOMPARE(session.activePhrase(), phraseB1.get()); QCOMPARE(session.activeUnit(), unitB.get()); // test number of actions auto actions = session.trainingActions(); QCOMPARE(actions.count(), 2); QCOMPARE(actions.at(0)->actions().count(), 2); QCOMPARE(actions.at(1)->actions().count(), 2); // test phrase iterators: accept iterator - session.setPhrase(phraseA1.get()); + session.setActivePhrase(phraseA1.get()); QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA1.get()); QVERIFY(session.hasNext()); session.accept(); QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA2.get()); session.accept(); QCOMPARE(session.activePhrase(), phraseB1.get()); session.accept(); QCOMPARE(session.activePhrase(), phraseB2.get()); QVERIFY(!session.hasNext()); // test phrase iterators: skip iterator - session.setPhrase(phraseA1.get()); + session.setActivePhrase(phraseA1.get()); QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA1.get()); QVERIFY(!session.hasPrevious()); QVERIFY(session.hasNext()); session.skip(); QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA2.get()); session.skip(); QCOMPARE(session.activePhrase(), phraseB1.get()); session.skip(); QCOMPARE(session.activePhrase(), phraseB2.get()); QVERIFY(session.hasPrevious()); QVERIFY(!session.hasNext()); // test completed signal QSignalSpy spy(&session, SIGNAL(completed())); - session.setPhrase(phraseB1.get()); + session.setActivePhrase(phraseB1.get()); session.accept(); QCOMPARE(spy.count(), 0); session.accept(); QCOMPARE(spy.count(), 1); } QTEST_GUILESS_MAIN(TestTrainingSession) diff --git a/src/core/editorsession.cpp b/src/core/editorsession.cpp index e165600..9d5abb8 100644 --- a/src/core/editorsession.cpp +++ b/src/core/editorsession.cpp @@ -1,340 +1,391 @@ /* * 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 . */ #include "editorsession.h" #include "core/language.h" #include "core/resources/editablecourseresource.h" #include "core/resources/skeletonresource.h" #include "core/unit.h" #include "core/iunit.h" #include "core/phrase.h" +#include "core/trainingaction.h" #include "core/contributorrepository.h" #include "artikulate_debug.h" EditorSession::EditorSession(QObject *parent) - : QObject(parent) + : ISessionActions(parent) { connect(this, &EditorSession::skeletonChanged, this, &EditorSession::displayedCourseChanged); connect(this, &EditorSession::courseChanged, this, &EditorSession::displayedCourseChanged); connect(this, &EditorSession::editSkeletonChanged, this, &EditorSession::displayedCourseChanged); connect(this, &EditorSession::displayedCourseChanged, this, &EditorSession::updateDisplayedUnit); connect(this, &EditorSession::courseChanged, this, &EditorSession::skeletonModeChanged); } void EditorSession::setRepository(IEditableRepository *repository) { m_repository = repository; } bool EditorSession::skeletonMode() const { return m_skeleton != nullptr; } void EditorSession::setEditSkeleton(bool enabled) { if (m_editSkeleton == enabled) { return; } m_editSkeleton = enabled; emit editSkeletonChanged(); } bool EditorSession::isEditSkeleton() const { return m_editSkeleton; } IEditableCourse * EditorSession::skeleton() const { return m_skeleton; } void EditorSession::setSkeleton(IEditableCourse *skeleton) { if (m_skeleton == skeleton) { return; } m_skeleton = skeleton; IEditableCourse *newCourse{ nullptr }; if (m_skeleton && m_repository) { for (const auto &course : m_repository->editableCourses()) { if (course->foreignId() == m_skeleton->id()) { newCourse = course.get(); break; } } } setCourse(newCourse); emit skeletonChanged(); } ILanguage * EditorSession::language() const { return m_language; } -IEditableCourse * EditorSession::course() const +IEditableCourse *EditorSession::course() const { return m_course; } void EditorSession::setCourse(IEditableCourse *course) { if (m_course == course) { return; } m_course = course; if (m_course != nullptr) { // update skeleton IEditableCourse * newSkeleton{ nullptr }; if (m_skeleton == nullptr || m_skeleton->id() != course->foreignId()) { for (const auto &skeleton : m_repository->skeletons()) { if (skeleton->id() == course->foreignId()) { newSkeleton = skeleton.get(); break; } } m_skeleton = newSkeleton; emit skeletonChanged(); } // update language m_language = m_course->language().get(); } else { m_language = nullptr; } + updateTrainingActions(); emit languageChanged(); emit courseChanged(); } void EditorSession::setCourseByLanguage(ILanguage *language) { if (!skeletonMode()) { qDebug() << "Course selection by language is only available in skeleton mode"; return; } if (language == nullptr || m_repository == nullptr) { return; } IEditableCourse *newCourse{ nullptr }; QString languageId; if (language) { languageId = language->id(); } for (auto course : m_repository->editableCourses()) { if (course->foreignId() == m_skeleton->id() && course->language()->id() == language->id()) { newCourse = course.get(); break; } } setCourse(newCourse); } IEditableCourse * EditorSession::displayedCourse() const { IEditableCourse * course{ nullptr }; if (m_editSkeleton) { course = m_skeleton; } else { course = m_course; } return course; } void EditorSession::updateDisplayedUnit() { auto course = displayedCourse(); if (course != nullptr) { auto units = course->units(); if (!units.isEmpty()) { setUnit(units.constFirst().get()); return; } } } IUnit * EditorSession::unit() const { return m_unit.get(); } IUnit * EditorSession::activeUnit() const { return m_unit.get(); } void EditorSession::setUnit(IUnit *unit) { if (unit != m_unit.get()) { if (!unit) { m_unit.reset(); } else { m_unit = unit->self(); } if (m_unit && !m_unit->phrases().isEmpty()) { setActivePhrase(m_unit->phrases().first().get()); } else { setActivePhrase(nullptr); } emit unitChanged(); } } void EditorSession::setActivePhrase(IPhrase * phrase) { if (phrase && m_phrase == phrase->self()) { return; } if (phrase == nullptr) { m_phrase = nullptr; } else { setUnit(phrase->unit().get()); m_phrase = phrase->self(); } emit phraseChanged(); } IPhrase * EditorSession::activePhrase() const { return m_phrase.get(); } std::shared_ptr EditorSession::previousPhrase() const { if (!m_phrase) { return nullptr; } const int index = m_phrase->unit()->phrases().indexOf(m_phrase); if (index > 0) { return m_phrase->unit()->phrases().at(index - 1); } else { auto unit = m_phrase->unit(); int uIndex{ -1 }; for (int i = 0; i < unit->course()->units().size(); ++i) { auto testUnit = unit->course()->units().at(i); if (testUnit.get() == unit.get()) { uIndex = i; break; } } if (uIndex > 0) { return unit->course()->units().at(uIndex - 1)->phrases().last(); } } return m_phrase; } std::shared_ptr EditorSession::nextPhrase() const { if (!m_phrase) { return nullptr; } const int index = m_phrase->unit()->phrases().indexOf(m_phrase); if (index < m_phrase->unit()->phrases().length() - 1) { return m_phrase->unit()->phrases().at(index + 1); } else { auto unit = m_phrase->unit(); int uIndex{ -1 }; for (int i = 0; i < unit->course()->units().size(); ++i) { auto testUnit = unit->course()->units().at(i); if (testUnit.get() == unit.get()) { uIndex = i; break; } } if (uIndex < unit->course()->units().length() - 1) { auto nextUnit = unit->course()->units().at(uIndex + 1); if (nextUnit->phrases().isEmpty()) { return nullptr; } return nextUnit->phrases().constFirst(); } } return m_phrase; } void EditorSession::switchToPreviousPhrase() { if (hasPreviousPhrase()) { setActivePhrase(previousPhrase().get()); } } void EditorSession::switchToNextPhrase() { if (hasNextPhrase()) { setActivePhrase(nextPhrase().get()); } } bool EditorSession::hasPreviousPhrase() const { if (!m_unit || !m_phrase) { return false; } Q_ASSERT(m_unit->course() != nullptr); const int phraseIndex = m_phrase->unit()->phrases().indexOf(m_phrase); int unitIndex = -1; for (int i = 0; i < m_unit->course()->units().size(); ++i) { if (m_unit->course()->units().at(i) == m_unit) { unitIndex = i; break; } } if (unitIndex > 0 || phraseIndex > 0) { return true; } return false; } bool EditorSession::hasNextPhrase() const { if (!m_unit || !m_phrase) { return false; } Q_ASSERT(m_unit->course() != nullptr); const int phraseIndex = m_phrase->unit()->phrases().indexOf(m_phrase); int unitIndex = -1; for (int i = 0; i < m_unit->course()->units().size(); ++i) { if (m_unit->course()->units().at(i) == m_unit) { unitIndex = i; break; } } if ((unitIndex >= 0 && unitIndex < m_course->units().size() - 1) || (phraseIndex >= 0 && phraseIndex < m_unit->phrases().size() - 1)) { return true; } return false; } void EditorSession::updateCourseFromSkeleton() { if (!m_course) { qCritical() << "Not updating course from skeleton, no one set."; return; } m_repository->updateCourseFromSkeleton(m_course->self()); } + +TrainingAction * EditorSession::activeAction() const +{ + if (!m_phrase) { + return nullptr; + } + return nullptr; +// return qobject_cast(m_actions.at(m_indexUnit)->actions().at(m_indexPhrase)); +} + +void EditorSession::updateTrainingActions() +{ + for (const auto &action : qAsConst(m_actions)) { + action->deleteLater(); + } + m_actions.clear(); + + const auto unitList = m_course->units(); + for (const auto &unit : qAsConst(unitList)) { + auto action = new TrainingAction(unit->title(), this); + const auto phraseList = unit->phrases(); + for (const auto &phrase : qAsConst(phraseList)) { + if (phrase->sound().isEmpty()) { + continue; + } + action->appendChild(new TrainingAction(phrase, this, unit.get())); + } + if (action->hasChildren()) { + m_actions.append(action); + } else { + action->deleteLater(); + } + } + + // update indices +// m_indexUnit = -1; +// m_indexPhrase = -1; +// if (m_course->units().count() > 0) { +// m_indexUnit = 0; +// if (m_course->units().constFirst()->phrases().count() > 0) { +// m_indexPhrase = 0; +// } +// } +} + +QVector EditorSession::trainingActions() const +{ + return m_actions; +} diff --git a/src/core/editorsession.h b/src/core/editorsession.h index 6004417..3cd6304 100644 --- a/src/core/editorsession.h +++ b/src/core/editorsession.h @@ -1,133 +1,137 @@ /* * 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 EDITORSESSION_H #define EDITORSESSION_H #include "artikulatecore_export.h" #include "phrase.h" +#include "isessionactions.h" class ILanguage; class IEditableCourse; class Unit; class IPhrase; class SkeletonResource; class IEditableRepository; /** * \class EditorSession * * An object of this class is used to set the current state of the editor. By this, we put all logic * how language, skeleton and course fit to each other into this class. The main concept is that * we have to fundamentally different workflows that both are modeled in this class: * * 1. Skeleton based workflow * - a skeleton is selected * - every language is available, since eventually the course should be available in every language * - for every language, there is at most one course (there is none only in case it is not created yet) * - adding new units or phrases is only possible in the skeleton course * - every course can update/sync with the skeleton * * 2. Course based workflow * - there is no skeleton from which the course is derived * - the base is a language that is selected first * - for a language there can be none to arbitrarily many courses * * The main switch is \c EditorSession::setSkeletonMode(bool) */ -class ARTIKULATECORE_EXPORT EditorSession : public QObject +class ARTIKULATECORE_EXPORT EditorSession : public ISessionActions { Q_OBJECT + Q_INTERFACES(ISessionActions) Q_PROPERTY(bool skeletonMode READ skeletonMode NOTIFY skeletonModeChanged) Q_PROPERTY(bool editSkeleton READ isEditSkeleton WRITE setEditSkeleton NOTIFY editSkeletonChanged) Q_PROPERTY(IEditableCourse *skeleton READ skeleton WRITE setSkeleton NOTIFY skeletonChanged) Q_PROPERTY(IEditableCourse *course READ course WRITE setCourse NOTIFY courseChanged) // editor elements depending on currently selected mode, skeleton and course /** * @brief the displayed course (skeleton or course) depending on the user selection */ Q_PROPERTY(IEditableCourse *displayedCourse READ displayedCourse NOTIFY displayedCourseChanged) Q_PROPERTY(ILanguage *language READ language NOTIFY languageChanged) Q_PROPERTY(IUnit *unit READ unit WRITE setUnit NOTIFY unitChanged) Q_PROPERTY(IPhrase *phrase READ activePhrase WRITE setActivePhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasNextPhrase READ hasNextPhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasPreviousPhrase READ hasPreviousPhrase NOTIFY phraseChanged) public: explicit EditorSession(QObject *parent = nullptr); void setRepository(IEditableRepository *repository); bool skeletonMode() const; void setEditSkeleton(bool enabled=true); bool isEditSkeleton() const; IEditableCourse * skeleton() const; void setSkeleton(IEditableCourse *skeleton); ILanguage * language() const; IEditableCourse * course() const; void setCourse(IEditableCourse *course); /** * @brief Open course resource by specifying the language * @param language the target language */ Q_INVOKABLE void setCourseByLanguage(ILanguage *language); IEditableCourse * displayedCourse() const; Q_DECL_DEPRECATED IUnit * unit() const; IUnit * activeUnit() const; void setUnit(IUnit *unit); IPhrase * activePhrase() const; - void setActivePhrase(IPhrase * phrase); + void setActivePhrase(IPhrase *phrase) override; IPhrase::Type phraseType() const; void setPhraseType(IPhrase::Type type); bool hasPreviousPhrase() const; bool hasNextPhrase() const; Q_INVOKABLE void switchToPreviousPhrase(); Q_INVOKABLE void switchToNextPhrase(); Q_INVOKABLE void updateCourseFromSkeleton(); + TrainingAction * activeAction() const override; + QVector trainingActions() const override; private: std::shared_ptr nextPhrase() const; std::shared_ptr previousPhrase() const; private Q_SLOTS: void updateDisplayedUnit(); Q_SIGNALS: void editSkeletonChanged(); void skeletonModeChanged(); void skeletonChanged(); void languageChanged(); - void courseChanged(); void displayedCourseChanged(); void unitChanged(); - void phraseChanged(); private: Q_DISABLE_COPY(EditorSession) + void updateTrainingActions(); IEditableRepository * m_repository{ nullptr }; bool m_editSkeleton{ false }; IEditableCourse *m_skeleton{ nullptr }; ILanguage *m_language{ nullptr }; IEditableCourse *m_course{ nullptr }; std::shared_ptr m_unit{ nullptr }; std::shared_ptr m_phrase; + QVector m_actions; }; #endif diff --git a/src/core/isessionactions.h b/src/core/isessionactions.h index 7797961..a2b0aea 100644 --- a/src/core/isessionactions.h +++ b/src/core/isessionactions.h @@ -1,66 +1,64 @@ /* * Copyright 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 ISESSIONACTIONS_H #define ISESSIONACTIONS_H #include "artikulatecore_export.h" #include #include class ICourse; class IPhrase; class TrainingAction; /** * \class ISessionActions * * Interface for both training and editor sessions that exposes simple iterator functionalities for a selecte course. * The interface provides all properties that are needed to create a navigatible menu. */ class ARTIKULATECORE_EXPORT ISessionActions : public QObject { Q_OBJECT public: ISessionActions(QObject *parent) : QObject(parent) { } virtual ~ISessionActions() = default; - virtual ICourse * course() const = 0; virtual TrainingAction * activeAction() const = 0; - virtual IPhrase * activePhrase() const = 0; - virtual void setPhrase(IPhrase *phrase) = 0; + virtual void setActivePhrase(IPhrase *phrase) = 0; /** * \brief Return tree of training actions * * The return actions form a 2-level hierarchy: * - the first level are all units * - the unit actions may contain sub-actions, which are the phrases */ virtual QVector trainingActions() const = 0; Q_SIGNALS: void courseChanged(); void phraseChanged(); }; Q_DECLARE_INTERFACE(ISessionActions, "ISessionActions") #endif diff --git a/src/core/trainingaction.cpp b/src/core/trainingaction.cpp index 4bc57a0..ecae445 100644 --- a/src/core/trainingaction.cpp +++ b/src/core/trainingaction.cpp @@ -1,110 +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(std::shared_ptr phrase, TrainingSession *session, QObject* parent) +TrainingAction::TrainingAction(std::shared_ptr phrase, ISessionActions *session, QObject* parent) : QObject(parent) , m_icon(new TrainingActionIcon(this, QString())) , m_phrase(phrase) - , m_trainingSession(session) + , m_session(session) { if (m_phrase) { m_text = phrase->text(); } } void TrainingAction::appendChild(QObject* child) { m_actions.append(child); emit actionsChanged(); } bool TrainingAction::hasChildren() const { return m_actions.count() > 0; } void TrainingAction::trigger() { - if (m_phrase && m_trainingSession) { - m_trainingSession->setPhrase(m_phrase.get()); + if (m_phrase && m_session) { + m_session->setActivePhrase(m_phrase.get()); } } 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; } IPhrase * TrainingAction::phrase() const { - return m_phrase.get(); //TODO + return m_phrase.get(); } QList TrainingAction::actions() const { return m_actions; } diff --git a/src/core/trainingaction.h b/src/core/trainingaction.h index 0a561ad..0fbc7f7 100644 --- a/src/core/trainingaction.h +++ b/src/core/trainingaction.h @@ -1,79 +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 "iphrase.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 READ checked NOTIFY checkedChanged) Q_PROPERTY(QString tooltip MEMBER m_tooltip CONSTANT) Q_PROPERTY(QList children READ actions NOTIFY actionsChanged) Q_PROPERTY(bool checkable MEMBER m_checkable CONSTANT) public: TrainingAction(QObject *parent = nullptr); TrainingAction(const QString &text, QObject *parent = nullptr); - TrainingAction(std::shared_ptr phrase, TrainingSession *session, QObject *parent = nullptr); + TrainingAction(std::shared_ptr phrase, ISessionActions *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; IPhrase * 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_actions; std::shared_ptr m_phrase; - TrainingSession * m_trainingSession{nullptr}; + ISessionActions * m_session{nullptr}; }; #endif diff --git a/src/core/trainingsession.cpp b/src/core/trainingsession.cpp index fe87273..46760cc 100644 --- a/src/core/trainingsession.cpp +++ b/src/core/trainingsession.cpp @@ -1,310 +1,310 @@ /* * 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/icourse.h" #include "core/unit.h" #include "core/phrase.h" #include "profilemanager.h" #include "learner.h" #include "trainingaction.h" #include "artikulate_debug.h" TrainingSession::TrainingSession(LearnerProfile::ProfileManager *manager, QObject *parent) : ISessionActions(parent) , m_profileManager(manager) , m_course(nullptr) { Q_ASSERT(m_profileManager != nullptr); } ICourse * TrainingSession::course() const { return m_course; } void TrainingSession::setCourse(ICourse *course) { if (!course) { updateTrainingActions(); return; } if (m_course == course) { return; } m_course = course; if (m_course && m_course->units().count() > 0) { setUnit(m_course->units().constFirst().get()); } // 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() ); const auto unitList = m_course->units(); for (auto unit : qAsConst(unitList)) { const auto phrases = unit->phrases(); for (auto &phrase : phrases) { auto iter = data.find(phrase->id()); if (iter != data.end()) { // phrase->setProgress(iter.value()); //FIXME add a decorator? } } } updateTrainingActions(); emit courseChanged(); } IUnit * TrainingSession::activeUnit() const { if (auto phrase = activePhrase()) { return phrase->unit().get(); } return nullptr; } void TrainingSession::setUnit(IUnit *unit) { // checking phrases in increasing order ensures that always the first phrase is selected 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 (unit == testPhrase->unit().get()) { if (auto action = activeAction()) { action->setChecked(false); } m_indexUnit = i; m_indexPhrase = j; if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); return; } } } } TrainingAction * TrainingSession::activeAction() const { if (m_indexUnit < 0 || m_indexPhrase < 0) { return nullptr; } return qobject_cast(m_actions.at(m_indexUnit)->actions().at(m_indexPhrase)); } IPhrase * TrainingSession::activePhrase() const { if (const auto action = activeAction()) { return action->phrase(); } return nullptr; } -void TrainingSession::setPhrase(IPhrase *phrase) +void TrainingSession::setActivePhrase(IPhrase *phrase) { 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; } } } } 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(); // phrase->updateProgress(Phrase::Progress::Done); //FIXME // store training activity LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->language()->id()); // m_profileManager->recordProgress(m_profileManager->activeProfile(), //FIXME // goal, // m_course->id(), // phrase->id(), // static_cast(LearnerProfile::ProfileManager::Skip), // phrase->progress() // ); selectNextPhrase(); } 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(); auto phrase = activePhrase(); // phrase->updateProgress(Phrase::Progress::Skip); //FIXME // 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(), // phrase->id(), // static_cast(LearnerProfile::ProfileManager::Skip), // phrase->progress() // ); // FIXME 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) { qDebug() << "switching to next unit"; 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::hasPrevious() const { return m_indexUnit > 0 || m_indexPhrase > 0; } bool TrainingSession::hasNext() const { if (m_indexUnit < m_actions.count() - 1) { return true; } if (m_actions.constLast()) { if (m_indexPhrase < m_actions.constLast()->actions().count() - 1) { return true; } } return false; } 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() const { return m_actions; } void TrainingSession::updateTrainingActions() { for (const auto &action : qAsConst(m_actions)) { action->deleteLater(); } m_actions.clear(); if (!m_course) { m_indexUnit = -1; m_indexPhrase = -1; return; } const auto unitList = m_course->units(); for (const auto &unit : qAsConst(unitList)) { auto action = new TrainingAction(unit->title(), this); const auto phraseList = unit->phrases(); for (const auto &phrase : qAsConst(phraseList)) { if (phrase->sound().isEmpty()) { continue; } action->appendChild(new TrainingAction(phrase, this, unit.get())); } if (action->hasChildren()) { m_actions.append(action); } else { action->deleteLater(); } } // update indices m_indexUnit = -1; m_indexPhrase = -1; if (m_course->units().count() > 0) { m_indexUnit = 0; if (m_course->units().constFirst()->phrases().count() > 0) { m_indexPhrase = 0; } } } diff --git a/src/core/trainingsession.h b/src/core/trainingsession.h index a00b9e2..f953df3 100644 --- a/src/core/trainingsession.h +++ b/src/core/trainingsession.h @@ -1,93 +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 . */ #ifndef TRAININGSESSION_H #define TRAININGSESSION_H #include "artikulatecore_export.h" #include "isessionactions.h" #include "phrase.h" #include class Language; class ICourse; class Unit; class TrainingAction; namespace LearnerProfile { class ProfileManager; } /** * \class TrainingSession */ class ARTIKULATECORE_EXPORT TrainingSession : public ISessionActions { Q_OBJECT + Q_INTERFACES(ISessionActions) Q_PROPERTY(ICourse *course READ course WRITE setCourse NOTIFY courseChanged) Q_PROPERTY(IUnit *unit READ activeUnit WRITE setUnit NOTIFY phraseChanged) - Q_PROPERTY(IPhrase *phrase READ activePhrase WRITE setPhrase NOTIFY phraseChanged) + Q_PROPERTY(IPhrase *phrase READ activePhrase WRITE setActivePhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasNext READ hasNext NOTIFY phraseChanged) public: explicit TrainingSession(LearnerProfile::ProfileManager *manager, QObject *parent = nullptr); - ICourse * course() const override; + ICourse * course() const; void setCourse(ICourse *course); IUnit * activeUnit() const; void setUnit(IUnit *unit); TrainingAction * activeAction() const override; - IPhrase * activePhrase() const override; - void setPhrase(IPhrase *phrase) override; + IPhrase * activePhrase() const; + void setActivePhrase(IPhrase *phrase) override; bool hasPrevious() const; bool hasNext() const; Q_INVOKABLE void accept(); Q_INVOKABLE void skip(); /** * @brief Return tree of training actions * * The return actions form a 2-level hierarchy: * - the first level are all units * - the unit actions may contain sub-actions, which are the phrases * * @note phrases without sound file paths are skipped when generating actions */ QVector trainingActions() const override; Q_SIGNALS: /** * @brief Emitted when last phrase of session is skipped or marked as completed. */ void completed(); private: Q_DISABLE_COPY(TrainingSession) void updateTrainingActions(); void selectNextPhrase(); void updateGoal(); LearnerProfile::ProfileManager *m_profileManager; ICourse *m_course; QVector m_actions; int m_indexUnit{-1}; int m_indexPhrase{-1}; }; #endif