diff --git a/autotests/courseresource/test_courseresource.cpp b/autotests/courseresource/test_courseresource.cpp index 9a8cdb3..9cb635c 100644 --- a/autotests/courseresource/test_courseresource.cpp +++ b/autotests/courseresource/test_courseresource.cpp @@ -1,205 +1,205 @@ /* * Copyright 2013 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_courseresource.h" #include "resourcerepositorystub.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "core/phonemegroup.h" #include "core/resources/languageresource.h" #include "core/resources/courseresource.h" #include #include #include #include #include #include #include #include #include TestCourseResource::TestCourseResource() { } void TestCourseResource::init() { } void TestCourseResource::cleanup() { } void TestCourseResource::courseSchemeValidationTest() { QUrl schemeFile = QUrl::fromLocalFile(":/artikulate/schemes/course.xsd"); QXmlSchema courseSchema; QVERIFY(courseSchema.load(schemeFile)); QVERIFY(courseSchema.isValid()); } void TestCourseResource::loadCourseResource() { std::unique_ptr language(new Language); language->setId("de"); auto group = language->addPhonemeGroup("id", "title"); group->addPhoneme("g", "G"); group->addPhoneme("u", "U"); std::vector> languages; languages.push_back(std::move(language)); ResourceRepositoryStub repository(std::move(languages)); const QString courseDirectory = "data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; CourseResource course(QUrl::fromLocalFile(courseFile), &repository); QCOMPARE(course.file().toLocalFile(), courseFile); QCOMPARE(course.id(), "de"); QCOMPARE(course.foreignId(), "artikulate-basic"); QCOMPARE(course.title(), "Artikulate Deutsch"); QCOMPARE(course.description(), "Ein Kurs in (hoch-)deutscher Aussprache."); QVERIFY(course.language() != nullptr); QCOMPARE(course.language()->id(), "de"); - QCOMPARE(course.unitList().count(), 1); + QCOMPARE(course.units().count(), 1); - const auto unit = course.unitList().first(); + const auto unit = course.units().first(); QVERIFY(unit != nullptr); QCOMPARE(unit->id(), "1"); QCOMPARE(unit->title(), QStringLiteral("Auf der Straße")); QCOMPARE(unit->foreignId(), "{dd60f04a-eb37-44b7-9787-67aaf7d3578d}"); QCOMPARE(unit->phraseList().count(), 3); // note: this test takes the silent assumption that phrases are added to the list in same // order as they are defined in the file. This assumption should be made explicit or dropped const auto firstPhrase = unit->phraseList().first(); QVERIFY(firstPhrase != nullptr); QCOMPARE(firstPhrase->id(), "1"); QCOMPARE(firstPhrase->foreignId(), "{3a4c1926-60d7-44c6-80d1-03165a641c75}"); QCOMPARE(firstPhrase->text(), "Guten Tag."); QCOMPARE(firstPhrase->soundFileUrl(), courseDirectory + "de_01.ogg"); QCOMPARE(firstPhrase->type(), Phrase::Type::Sentence); QCOMPARE(firstPhrase->phonemes().count(), 2); } void TestCourseResource::unitAddAndRemoveHandling() { // boilerplate std::unique_ptr language(new Language); language->setId("de"); std::vector> languages; languages.push_back(std::move(language)); ResourceRepositoryStub repository(std::move(languages)); const QString courseDirectory = "data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; CourseResource course(QUrl::fromLocalFile(courseFile), &repository); // begin of test std::unique_ptr unit(new Unit); unit->setId("testunit"); - const int initialUnitNumber = course.unitList().count(); + const int initialUnitNumber = course.units().count(); QCOMPARE(initialUnitNumber, 1); QSignalSpy spyAboutToBeAdded(&course, SIGNAL(unitAboutToBeAdded(Unit*, int))); QSignalSpy spyAdded(&course, SIGNAL(unitAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); course.addUnit(std::move(unit)); - QCOMPARE(course.unitList().count(), initialUnitNumber + 1); + QCOMPARE(course.units().count(), initialUnitNumber + 1); QCOMPARE(spyAboutToBeAdded.count(), 1); QCOMPARE(spyAdded.count(), 1); } void TestCourseResource::coursePropertyChanges() { // boilerplate std::unique_ptr language(new Language); language->setId("de"); std::vector> languages; languages.push_back(std::move(language)); ResourceRepositoryStub repository(std::move(languages)); const QString courseDirectory = "data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; CourseResource course(QUrl::fromLocalFile(courseFile), &repository); // id { const QString value = "newId"; QSignalSpy spy(&course, SIGNAL(idChanged())); QCOMPARE(spy.count(), 0); course.setId(value); QCOMPARE(course.id(), value); QCOMPARE(spy.count(), 1); } // foreign id { const QString value = "newForeignId"; QSignalSpy spy(&course, SIGNAL(foreignIdChanged())); QCOMPARE(spy.count(), 0); course.setForeignId(value); QCOMPARE(course.foreignId(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newTitle"; QSignalSpy spy(&course, SIGNAL(titleChanged())); QCOMPARE(spy.count(), 0); course.setTitle(value); QCOMPARE(course.title(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newI18nTitle"; QSignalSpy spy(&course, SIGNAL(i18nTitleChanged())); QCOMPARE(spy.count(), 0); course.setI18nTitle(value); QCOMPARE(course.i18nTitle(), value); QCOMPARE(spy.count(), 1); } // description { const QString value = "newDescription"; QSignalSpy spy(&course, SIGNAL(descriptionChanged())); QCOMPARE(spy.count(), 0); course.setDescription(value); QCOMPARE(course.description(), value); QCOMPARE(spy.count(), 1); } // language { std::shared_ptr testLanguage; QSignalSpy spy(&course, SIGNAL(languageChanged())); QCOMPARE(spy.count(), 0); course.setLanguage(testLanguage); QCOMPARE(course.language(), testLanguage); QCOMPARE(spy.count(), 1); } } QTEST_GUILESS_MAIN(TestCourseResource) diff --git a/autotests/editablecourseresource/test_editablecourseresource.cpp b/autotests/editablecourseresource/test_editablecourseresource.cpp index 28d1ebe..859260a 100644 --- a/autotests/editablecourseresource/test_editablecourseresource.cpp +++ b/autotests/editablecourseresource/test_editablecourseresource.cpp @@ -1,250 +1,250 @@ /* * 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 "test_editablecourseresource.h" #include "resourcerepositorystub.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "core/resources/courseparser.h" #include "core/resources/languageresource.h" #include "core/resources/editablecourseresource.h" #include #include #include #include #include #include #include #include #include #include TestEditableCourseResource::TestEditableCourseResource() { } void TestEditableCourseResource::init() { } void TestEditableCourseResource::cleanup() { } void TestEditableCourseResource::courseSchemeValidationTest() { QUrl schemeFile = QUrl::fromLocalFile(":/artikulate/schemes/course.xsd"); QXmlSchema courseSchema; QVERIFY(courseSchema.load(schemeFile)); QVERIFY(courseSchema.isValid()); //TODO shall be used in skeleton specific test QUrl skeletonFile = QUrl::fromLocalFile(":/artikulate/schemes/skeleton.xsd"); QXmlSchema skeletonScheme; QVERIFY(skeletonScheme.load(skeletonFile)); QVERIFY(skeletonScheme.isValid()); } void TestEditableCourseResource::loadCourseResource() { std::unique_ptr language(new Language); language->setId("de"); std::vector> languages; languages.push_back(std::move(language)); ResourceRepositoryStub repository(std::move(languages)); EditableCourseResource course(QUrl::fromLocalFile(":/courses/de.xml"), &repository); QCOMPARE(course.file().toLocalFile(), ":/courses/de.xml"); QCOMPARE(course.id(), "de"); QCOMPARE(course.foreignId(), "artikulate-basic"); QCOMPARE(course.title(), "Artikulate Deutsch"); QCOMPARE(course.description(), "Ein Kurs in (hoch-)deutscher Aussprache."); QVERIFY(course.language() != nullptr); QCOMPARE(course.language()->id(), "de"); - QCOMPARE(course.unitList().count(), 1); + QCOMPARE(course.units().count(), 1); - const auto unit = course.unitList().first(); + const auto unit = course.units().first(); QVERIFY(unit != nullptr); QCOMPARE(unit->id(), "1"); QCOMPARE(unit->title(), QStringLiteral("Auf der Straße")); QCOMPARE(unit->foreignId(), "{dd60f04a-eb37-44b7-9787-67aaf7d3578d}"); QCOMPARE(unit->phraseList().count(), 3); // note: this test takes the silent assumption that phrases are added to the list in same // order as they are defined in the file. This assumption should be made explicit or dropped const auto firstPhrase = unit->phraseList().first(); QVERIFY(firstPhrase != nullptr); QCOMPARE(firstPhrase->id(), "1"); QCOMPARE(firstPhrase->foreignId(), "{3a4c1926-60d7-44c6-80d1-03165a641c75}"); QCOMPARE(firstPhrase->text(), "Guten Tag."); QCOMPARE(firstPhrase->soundFileUrl(), ":/courses/de_01.ogg"); QCOMPARE(firstPhrase->type(), Phrase::Type::Sentence); QVERIFY(firstPhrase->phonemes().isEmpty()); } void TestEditableCourseResource::unitAddAndRemoveHandling() { // boilerplate std::unique_ptr language(new Language); language->setId("de"); std::vector> languages; languages.push_back(std::move(language)); ResourceRepositoryStub repository(std::move(languages)); EditableCourseResource course(QUrl::fromLocalFile(":/courses/de.xml"), &repository); // begin of test std::unique_ptr unit(new Unit); unit->setId("testunit"); - const int initialUnitNumber = course.unitList().count(); + const int initialUnitNumber = course.units().count(); QCOMPARE(initialUnitNumber, 1); QSignalSpy spyAboutToBeAdded(&course, SIGNAL(unitAboutToBeAdded(Unit*, int))); QSignalSpy spyAdded(&course, SIGNAL(unitAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); course.addUnit(std::move(unit)); - QCOMPARE(course.unitList().count(), initialUnitNumber + 1); + QCOMPARE(course.units().count(), initialUnitNumber + 1); QCOMPARE(spyAboutToBeAdded.count(), 1); QCOMPARE(spyAdded.count(), 1); } void TestEditableCourseResource::coursePropertyChanges() { // boilerplate std::unique_ptr language(new Language); language->setId("de"); std::vector> languages; languages.push_back(std::move(language)); ResourceRepositoryStub repository(std::move(languages)); CourseResource course(QUrl::fromLocalFile(":/courses/de.xml"), &repository); // id { const QString value = "newId"; QSignalSpy spy(&course, SIGNAL(idChanged())); QCOMPARE(spy.count(), 0); course.setId(value); QCOMPARE(course.id(), value); QCOMPARE(spy.count(), 1); } // foreign id { const QString value = "newForeignId"; QSignalSpy spy(&course, SIGNAL(foreignIdChanged())); QCOMPARE(spy.count(), 0); course.setForeignId(value); QCOMPARE(course.foreignId(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newTitle"; QSignalSpy spy(&course, SIGNAL(titleChanged())); QCOMPARE(spy.count(), 0); course.setTitle(value); QCOMPARE(course.title(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newI18nTitle"; QSignalSpy spy(&course, SIGNAL(i18nTitleChanged())); QCOMPARE(spy.count(), 0); course.setI18nTitle(value); QCOMPARE(course.i18nTitle(), value); QCOMPARE(spy.count(), 1); } // description { const QString value = "newDescription"; QSignalSpy spy(&course, SIGNAL(descriptionChanged())); QCOMPARE(spy.count(), 0); course.setDescription(value); QCOMPARE(course.description(), value); QCOMPARE(spy.count(), 1); } // language { std::shared_ptr testLanguage; QSignalSpy spy(&course, SIGNAL(languageChanged())); QCOMPARE(spy.count(), 0); course.setLanguage(testLanguage); QCOMPARE(course.language(), testLanguage); QCOMPARE(spy.count(), 1); } } void TestEditableCourseResource::fileLoadSaveCompleteness() { // boilerplate std::unique_ptr language(new Language); language->setId("de"); std::vector> languages; languages.push_back(std::move(language)); ResourceRepositoryStub repository(std::move(languages)); EditableCourseResource course(QUrl::fromLocalFile(":/courses/de.xml"), &repository); QTemporaryFile outputFile; outputFile.open(); course.exportCourse(QUrl::fromLocalFile(outputFile.fileName())); // note: this only works, since the resource manager not checks uniqueness of course ids! EditableCourseResource loadedCourse(QUrl::fromLocalFile(outputFile.fileName()), &repository); // test that we actually call the different files QVERIFY(course.file().toLocalFile() != loadedCourse.file().toLocalFile()); QVERIFY(course.id() == loadedCourse.id()); QVERIFY(course.foreignId() == loadedCourse.foreignId()); QVERIFY(course.title() == loadedCourse.title()); QVERIFY(course.description() == loadedCourse.description()); QVERIFY(course.language()->id() == loadedCourse.language()->id()); - QVERIFY(course.unitList().count() == loadedCourse.unitList().count()); + QVERIFY(course.units().count() == loadedCourse.units().count()); - Unit *testUnit = course.unitList().constFirst(); - Unit *compareUnit = loadedCourse.unitList().constFirst(); + auto testUnit = course.units().constFirst(); + auto compareUnit = loadedCourse.units().constFirst(); QVERIFY(testUnit->id() == compareUnit->id()); QVERIFY(testUnit->foreignId() == compareUnit->foreignId()); QVERIFY(testUnit->title() == compareUnit->title()); QVERIFY(testUnit->phraseList().count() == compareUnit->phraseList().count()); Phrase *testPhrase = testUnit->phraseList().constFirst(); Phrase *comparePhrase = new Phrase(this); // note that this actually means that we DO NOT respect phrase orders by list order for (Phrase *phrase : compareUnit->phraseList()) { if (testPhrase->id() == phrase->id()) { comparePhrase = phrase; break; } } QVERIFY(testPhrase->id() == comparePhrase->id()); QVERIFY(testPhrase->foreignId() == comparePhrase->foreignId()); QVERIFY(testPhrase->text() == comparePhrase->text()); QVERIFY(testPhrase->type() == comparePhrase->type()); QVERIFY(testPhrase->sound().fileName() == comparePhrase->sound().fileName()); QVERIFY(testPhrase->phonemes().count() == comparePhrase->phonemes().count()); } QTEST_GUILESS_MAIN(TestEditableCourseResource) diff --git a/autotests/editorsession/test_editorsession.cpp b/autotests/editorsession/test_editorsession.cpp index 7d24e23..f18332d 100644 --- a/autotests/editorsession/test_editorsession.cpp +++ b/autotests/editorsession/test_editorsession.cpp @@ -1,242 +1,238 @@ /* * 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_editorsession.h" #include "editablerepositorystub.h" #include "src/core/editorsession.h" #include "src/core/icourse.h" #include "src/core/ieditablecourse.h" #include "src/core/ieditablerepository.h" #include "src/core/language.h" #include "src/core/resources/skeletonresource.h" #include "src/core/unit.h" #include #include class EditableCourseStub : public IEditableCourse { public: EditableCourseStub(std::shared_ptr language, QVector> units) : IEditableCourse() , m_language(language) , m_units(units) { } ~EditableCourseStub() override; QString id() const override { return m_id; } void setId(QString id) override { m_id = id; emit idChanged(); } QString foreignId() const override { return m_foreignId; } void setForeignId(QString id) override { m_foreignId = id; } QString title() const override { return m_title; } void setTitle(QString title) override { m_title = title; emit titleChanged(); } QString i18nTitle() const override { return m_i18nTitle; } void setI18nTitle(QString title) override { m_i18nTitle = title; } QString description() const override { return m_description; } void setDescription(QString description) override { m_description = description; emit descriptionChanged(); } std::shared_ptr language() const override { return m_language; } void setLanguage(std::shared_ptr language) override { m_language = language; emit languageChanged(); } - QList unitList() override + QVector> units() override { - QList rawList; - for (auto unit : m_units) { - rawList.append(unit.get()); - } - return rawList; + return m_units; } std::shared_ptr addUnit(std::unique_ptr unit) override { m_units.append(std::move(unit)); return m_units.last(); } QUrl file() const override { return QUrl(); } private: QString m_id{ "courseid" }; QString m_foreignId{ "foreigncourseid" }; QString m_title{ "title" }; QString m_i18nTitle{ "i18n title" }; QString m_description{ "description of the course" }; std::shared_ptr m_language; QVector> m_units; }; // define one virtual method out of line to pin CourseStub to this translation unit EditableCourseStub::~EditableCourseStub() = default; void TestEditorSession::init() { // TODO initialization of test case } void TestEditorSession::cleanup() { // TODO cleanup after test run } void TestEditorSession::createEditorSession() { auto languageGerman = std::make_shared(); languageGerman->setId("de"); auto languageEnglish = std::make_shared(); languageEnglish->setId("en"); std::shared_ptr course(new EditableCourseStub(languageGerman, QVector>())); course->setLanguage(languageGerman); std::shared_ptr skeleton(new SkeletonResource(QUrl(), nullptr)); EditableRepositoryStub repository{ {languageGerman, languageEnglish}, // languages {skeleton}, // skeletons {course} // courses }; EditorSession session; session.setRepository(&repository); QVERIFY(session.course() == nullptr); QVERIFY(session.language() == nullptr); QVERIFY(session.skeleton() == nullptr); } void TestEditorSession::nonSkeletonSwitchingBehavior() { auto languageGerman = std::make_shared(); languageGerman->setId("de"); auto languageEnglish = std::make_shared(); languageEnglish->setId("en"); std::shared_ptr courseGerman(new EditableCourseStub(languageGerman, QVector>())); courseGerman->setId("course-german"); std::shared_ptr courseEnglish(new EditableCourseStub(languageEnglish, QVector>())); courseEnglish->setId("course-english"); EditableRepositoryStub repository{ {languageGerman, languageEnglish}, // languages {}, // skeletons {courseGerman, courseEnglish} // courses }; EditorSession session; session.setRepository(&repository); QVERIFY(session.course() == nullptr); session.setCourse(courseGerman.get()); QCOMPARE(session.course()->id(), courseGerman->id()); QVERIFY(session.language() != nullptr); QCOMPARE(session.language()->id(), languageGerman->id()); QVERIFY(session.language() != nullptr); QCOMPARE(session.language()->id(), languageGerman->id()); session.setCourse(courseEnglish.get()); QVERIFY(session.course() != nullptr); QCOMPARE(session.course()->id(), courseEnglish->id()); QVERIFY(session.language() != nullptr); QCOMPARE(session.language()->id(), languageEnglish->id()); } void TestEditorSession::skeletonSwitchingBehavior() { auto languageGerman = std::make_shared(); languageGerman->setId("de"); auto languageEnglish = std::make_shared(); languageEnglish->setId("en"); std::shared_ptr courseGermanA(new EditableCourseStub(languageGerman, QVector>())); courseGermanA->setId("course-german"); courseGermanA->setForeignId("testskeletonA"); std::shared_ptr courseGermanB(new EditableCourseStub(languageGerman, QVector>())); courseGermanB->setId("course-german"); courseGermanB->setForeignId("testskeletonB"); std::shared_ptr courseEnglishA(new EditableCourseStub(languageEnglish, QVector>())); courseEnglishA->setId("course-english"); courseEnglishA->setForeignId("testskeletonA"); std::shared_ptr skeletonA(new SkeletonResource(QUrl(), nullptr)); skeletonA->setId("testskeletonA"); std::shared_ptr skeletonB(new SkeletonResource(QUrl(), nullptr)); skeletonB->setId("testskeletonB"); EditableRepositoryStub repository{ {languageGerman, languageEnglish}, // languages {skeletonA, skeletonB}, // skeletons {courseGermanA, courseEnglishA, courseGermanB} // courses }; EditorSession session; session.setRepository(&repository); session.setSkeleton(skeletonA.get()); Q_ASSERT(session.skeleton() != nullptr); QCOMPARE(session.skeleton()->id(), skeletonA->id()); Q_ASSERT(session.course() != nullptr); QCOMPARE(session.course()->id(), courseGermanA->id()); session.setCourse(courseEnglishA.get()); Q_ASSERT(session.course() != nullptr); QCOMPARE(session.course()->id(), courseEnglishA->id()); session.setCourse(courseGermanB.get()); QVERIFY(session.skeleton() != nullptr); QCOMPARE(session.skeleton()->id(), skeletonB->id()); QVERIFY(session.course() != nullptr); QCOMPARE(session.course()->id(), courseGermanB->id()); QVERIFY(session.language() != nullptr); QCOMPARE(session.language()->id(), languageGerman->id()); } QTEST_GUILESS_MAIN(TestEditorSession) diff --git a/autotests/skeletonresource/test_skeletonresource.cpp b/autotests/skeletonresource/test_skeletonresource.cpp index 1e624df..cc091ee 100644 --- a/autotests/skeletonresource/test_skeletonresource.cpp +++ b/autotests/skeletonresource/test_skeletonresource.cpp @@ -1,212 +1,212 @@ /* * 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_skeletonresource.h" #include "resourcerepositorystub.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "core/resources/languageresource.h" #include "core/resources/skeletonresource.h" #include #include #include #include #include #include #include #include #include TestSkeletonResource::TestSkeletonResource() { } void TestSkeletonResource::init() { } void TestSkeletonResource::cleanup() { } void TestSkeletonResource::schemeValidationTest() { QUrl skeletonFile = QUrl::fromLocalFile(":/artikulate/schemes/skeleton.xsd"); QXmlSchema skeletonScheme; QVERIFY(skeletonScheme.load(skeletonFile)); QVERIFY(skeletonScheme.isValid()); } void TestSkeletonResource::loadSkeletonResource() { std::shared_ptr language(new Language); language->setId("de"); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; SkeletonResource course(QUrl::fromLocalFile(courseFile), &repository); QCOMPARE(course.file().toLocalFile(), courseFile); QCOMPARE(course.id(), "skeleton-testdata"); QCOMPARE(course.foreignId(), "skeleton-testdata"); // always same as ID QCOMPARE(course.title(), "Artikulate Test Course Title"); QCOMPARE(course.description(), "Artikulate Test Course Description"); QVERIFY(course.language() == nullptr); // a skeleton must not have a language - QCOMPARE(course.unitList().count(), 2); + QCOMPARE(course.units().count(), 2); - const auto unit = course.unitList().first(); + const auto unit = course.units().first(); QVERIFY(unit != nullptr); QCOMPARE(unit->id(), "{11111111-b885-4833-97ff-27cb1ca2f543}"); QCOMPARE(unit->title(), QStringLiteral("Numbers")); QCOMPARE(unit->phraseList().count(), 2); // note: this test takes the silent assumption that phrases are added to the list in same // order as they are defined in the file. This assumption should be made explicit or dropped const auto firstPhrase = unit->phraseList().first(); QVERIFY(firstPhrase != nullptr); QCOMPARE(firstPhrase->id(), "{22222222-9234-4da5-a6fe-dbd5104f57d5}"); QCOMPARE(firstPhrase->text(), "0"); QCOMPARE(firstPhrase->type(), Phrase::Type::Word); const auto secondPhrase = unit->phraseList().at(1); QVERIFY(secondPhrase != nullptr); QCOMPARE(secondPhrase->id(), "{333333333-b4a9-4264-9a26-75a55aa5d302}"); QCOMPARE(secondPhrase->text(), "1"); QCOMPARE(secondPhrase->type(), Phrase::Type::Word); } void TestSkeletonResource::unitAddAndRemoveHandling() { // boilerplate std::shared_ptr language(new Language); language->setId("de"); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; SkeletonResource course(QUrl::fromLocalFile(courseFile), &repository); // begin of test std::unique_ptr unit(new Unit); unit->setId("testunit"); - const int initialUnitNumber = course.unitList().count(); + const int initialUnitNumber = course.units().count(); QCOMPARE(initialUnitNumber, 2); QSignalSpy spyAboutToBeAdded(&course, SIGNAL(unitAboutToBeAdded(Unit*, int))); QSignalSpy spyAdded(&course, SIGNAL(unitAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); course.addUnit(std::move(unit)); - QCOMPARE(course.unitList().count(), initialUnitNumber + 1); + QCOMPARE(course.units().count(), initialUnitNumber + 1); QCOMPARE(spyAboutToBeAdded.count(), 1); QCOMPARE(spyAdded.count(), 1); } void TestSkeletonResource::coursePropertyChanges() { // boilerplate std::shared_ptr language(new Language); language->setId("de"); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; SkeletonResource course(QUrl::fromLocalFile(courseFile), &repository); // id { const QString value = "newId"; QSignalSpy spy(&course, SIGNAL(idChanged())); QCOMPARE(spy.count(), 0); course.setId(value); QCOMPARE(course.id(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newTitle"; QSignalSpy spy(&course, SIGNAL(titleChanged())); QCOMPARE(spy.count(), 0); course.setTitle(value); QCOMPARE(course.title(), value); QCOMPARE(spy.count(), 1); } // description { const QString value = "newDescription"; QSignalSpy spy(&course, SIGNAL(descriptionChanged())); QCOMPARE(spy.count(), 0); course.setDescription(value); QCOMPARE(course.description(), value); QCOMPARE(spy.count(), 1); } } void TestSkeletonResource::fileLoadSaveCompleteness() { // boilerplate std::shared_ptr language(new Language); language->setId("de"); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; SkeletonResource course(QUrl::fromLocalFile(courseFile), &repository); QTemporaryFile outputFile; outputFile.open(); course.exportCourse(QUrl::fromLocalFile(outputFile.fileName())); // note: this only works, since the resource manager not checks uniqueness of course ids! SkeletonResource loadedCourse(QUrl::fromLocalFile(outputFile.fileName()), &repository); // test that we actually call the different files QVERIFY(course.file().toLocalFile() != loadedCourse.file().toLocalFile()); QCOMPARE(loadedCourse.id(), course.id()); QCOMPARE(loadedCourse.foreignId(), course.foreignId()); QCOMPARE(loadedCourse.title(), course.title()); QCOMPARE(loadedCourse.description(), course.description()); QCOMPARE(loadedCourse.language(), course.language()); - QCOMPARE(loadedCourse.unitList().count(), course.unitList().count()); + QCOMPARE(loadedCourse.units().count(), course.units().count()); - Unit *testUnit = course.unitList().constFirst(); - Unit *compareUnit = loadedCourse.unitList().constFirst(); + auto testUnit = course.units().constFirst(); + auto compareUnit = loadedCourse.units().constFirst(); QCOMPARE(testUnit->id(), compareUnit->id()); QCOMPARE(testUnit->foreignId(), compareUnit->foreignId()); QCOMPARE(testUnit->title(), compareUnit->title()); QCOMPARE(testUnit->phraseList().count(), compareUnit->phraseList().count()); Phrase *testPhrase = testUnit->phraseList().constFirst(); Phrase *comparePhrase = new Phrase(this); // note that this actually means that we DO NOT respect phrase orders by list order for (Phrase *phrase : compareUnit->phraseList()) { if (testPhrase->id() == phrase->id()) { comparePhrase = phrase; break; } } QVERIFY(testPhrase->id() == comparePhrase->id()); QVERIFY(testPhrase->foreignId() == comparePhrase->foreignId()); QVERIFY(testPhrase->text() == comparePhrase->text()); QVERIFY(testPhrase->type() == comparePhrase->type()); QVERIFY(testPhrase->sound().fileName() == comparePhrase->sound().fileName()); QVERIFY(testPhrase->phonemes().count() == comparePhrase->phonemes().count()); } QTEST_GUILESS_MAIN(TestSkeletonResource) diff --git a/autotests/trainingsession/test_trainingsession.cpp b/autotests/trainingsession/test_trainingsession.cpp index c2a83cc..ad65d32 100644 --- a/autotests/trainingsession/test_trainingsession.cpp +++ b/autotests/trainingsession/test_trainingsession.cpp @@ -1,257 +1,256 @@ /* * 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 "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 class CourseStub : public ICourse { public: - CourseStub(std::shared_ptr language, QVector units) + CourseStub(std::shared_ptr language, QVector> units) : m_language(language) , m_units(units) { } ~CourseStub() override; QString id() const override { return "courseid"; } QString foreignId() const override { return "foreigncourseid"; } QString title() const override { return "title"; } QString i18nTitle() const override { return "i18n title"; } QString description() const override { return "description of the course"; } std::shared_ptr language() const override { return m_language; } - QList unitList() override + QVector> units() override { - return m_units.toList(); + return m_units; } QUrl file() const override { return QUrl(); } private: std::shared_ptr m_language; - QVector m_units; + QVector> m_units; }; // define one virtual method out of line to pin CourseStub to this translation unit CourseStub::~CourseStub() = default; void TestTrainingSession::init() { // TODO initialization of test case } void TestTrainingSession::cleanup() { // TODO cleanup after test run } void TestTrainingSession::createTrainingSessionWithoutUnits() { auto language = std::make_shared(); - CourseStub course(language, QVector()); + 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(); - Unit unitA; - Unit unitB; + std::shared_ptr unitA(new Unit); + std::shared_ptr unitB(new Unit); Phrase *phraseA1 = new Phrase; Phrase *phraseA2 = new Phrase; Phrase *phraseB1 = new Phrase; Phrase *phraseB2 = new Phrase; // 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); + unitA->addPhrase(phraseA1); + unitA->addPhrase(phraseA2); + unitB->addPhrase(phraseB1); + unitB->addPhrase(phraseB2); - CourseStub course(language, QVector({&unitA, &unitB})); + 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(); - Unit firstUnit; - Unit secondUnit; - - CourseStub course(language, QVector({&firstUnit, &secondUnit})); + std::shared_ptr unitA(new Unit); + std::shared_ptr unitB(new Unit); + 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(); - Unit unit; + std::shared_ptr unit(new Unit); Phrase *firstPhrase = new Phrase(); Phrase *secondPhrase = new Phrase(); - unit.addPhrase(firstPhrase); - unit.addPhrase(secondPhrase); + unit->addPhrase(firstPhrase); + unit->addPhrase(secondPhrase); - CourseStub course(language, QVector({&unit})); + 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(); - Unit unitA; - Unit unitB; + std::shared_ptr unitA(new Unit); + std::shared_ptr unitB(new Unit); Phrase *phraseA1 = new Phrase; Phrase *phraseA2 = new Phrase; Phrase *phraseB1 = new Phrase; Phrase *phraseB2 = new Phrase; // 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); + unitA->addPhrase(phraseA1); + unitA->addPhrase(phraseA2); + unitB->addPhrase(phraseB1); + unitB->addPhrase(phraseB2); - CourseStub course(language, QVector({&unitA, &unitB})); + 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(), &unitA); + QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA1); QVERIFY(&course == session.course()); // test direct unit setters - session.setUnit(&unitA); - QCOMPARE(session.activeUnit(), &unitA); - session.setUnit(&unitB); - QCOMPARE(session.activeUnit(), &unitB); + 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); QCOMPARE(session.activePhrase(), phraseA1); - QCOMPARE(session.activeUnit(), &unitA); + QCOMPARE(session.activeUnit(), unitA.get()); session.setPhrase(phraseB1); QCOMPARE(session.activePhrase(), phraseB1); - QCOMPARE(session.activeUnit(), &unitB); + 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); - QCOMPARE(session.activeUnit(), &unitA); + QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA1); QVERIFY(session.hasNext()); session.accept(); - QCOMPARE(session.activeUnit(), &unitA); + QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA2); session.accept(); QCOMPARE(session.activePhrase(), phraseB1); session.accept(); QCOMPARE(session.activePhrase(), phraseB2); QVERIFY(!session.hasNext()); // test phrase iterators: skip iterator session.setPhrase(phraseA1); - QCOMPARE(session.activeUnit(), &unitA); + QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA1); QVERIFY(!session.hasPrevious()); QVERIFY(session.hasNext()); session.skip(); - QCOMPARE(session.activeUnit(), &unitA); + QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA2); session.skip(); QCOMPARE(session.activePhrase(), phraseB1); session.skip(); QCOMPARE(session.activePhrase(), phraseB2); QVERIFY(session.hasPrevious()); QVERIFY(!session.hasNext()); // test completed signal QSignalSpy spy(&session, SIGNAL(completed())); session.setPhrase(phraseB1); 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 f4a3c72..074fe5d 100644 --- a/src/core/editorsession.cpp +++ b/src/core/editorsession.cpp @@ -1,278 +1,291 @@ /* * 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/resources/languageresource.h" #include "core/unit.h" #include "core/phrase.h" #include "core/contributorrepository.h" #include "artikulate_debug.h" EditorSession::EditorSession(QObject *parent) : QObject(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(); } Language * EditorSession::language() const { return m_language; } 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; } emit languageChanged(); emit courseChanged(); } void EditorSession::setCourseByLanguage(Language *language) { if (!skeletonMode() || m_skeleton == nullptr) { 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(); - Unit * unit{ nullptr }; if (course != nullptr) { - auto units = course->unitList(); + auto units = course->units(); if (!units.isEmpty()) { - unit = units.constFirst(); + setUnit(units.constFirst().get()); + return; } } - setUnit(unit); } Unit * EditorSession::unit() const { return m_unit; } void EditorSession::setUnit(Unit *unit) { if (m_unit == unit) { return; } m_unit = unit; // different than above, do not directly enter phrases // but first show editing information for units setPhrase(nullptr); emit unitChanged(); } void EditorSession::setPhrase(Phrase *phrase) { if (m_phrase == phrase) { return; } if (phrase) { setUnit(phrase->unit()); } m_phrase = phrase; emit phraseChanged(); } Phrase * EditorSession::phrase() const { return m_phrase; } Phrase * EditorSession::previousPhrase() const { if (!m_phrase) { return nullptr; } const int index = m_phrase->unit()->phraseList().indexOf(m_phrase); if (index > 0) { return m_phrase->unit()->phraseList().at(index - 1); } else { - Unit *unit = m_phrase->unit(); - int uIndex = unit->course()->unitList().indexOf(unit); + 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->id() == unit->id()) { + uIndex = i; + break; + } + } if (uIndex > 0) { - return unit->course()->unitList().at(uIndex - 1)->phraseList().last(); + return unit->course()->units().at(uIndex - 1)->phraseList().last(); } } return nullptr; } Phrase * EditorSession::nextPhrase() const { 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) { - Unit *nextUnit = unit->course()->unitList().at(uIndex + 1); + int uIndex{ -1 }; + for (int i = 0; i < unit->course()->units().size(); ++i) { + auto testUnit = unit->course()->units().at(i); + if (testUnit->id() == unit->id()) { + uIndex = i; + break; + } + } + if (uIndex < unit->course()->units().length() - 1) { + auto nextUnit = unit->course()->units().at(uIndex + 1); if (nextUnit->phraseList().isEmpty()) { return nullptr; } return nextUnit->phraseList().constFirst(); } } return nullptr; } void EditorSession::switchToPreviousPhrase() { setPhrase(previousPhrase()); } void EditorSession::switchToNextPhrase() { setPhrase(nextPhrase()); } bool EditorSession::hasPreviousPhrase() const { return previousPhrase() != nullptr; } bool EditorSession::hasNextPhrase() const { return nextPhrase() != nullptr; } void EditorSession::updateCourseFromSkeleton() { if (!m_course) { qCritical() << "Not updating course from skeleton, no one set."; return; } //FIXME convert to interface // m_repository->updateCourseFromSkeleton(m_course); } diff --git a/src/core/icourse.h b/src/core/icourse.h index 2e063e8..c51da4b 100644 --- a/src/core/icourse.h +++ b/src/core/icourse.h @@ -1,75 +1,75 @@ /* * 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 ICOURSE_H #define ICOURSE_H #include "artikulatecore_export.h" #include #include #include #include class QString; class Language; class Unit; class Phoneme; class ARTIKULATECORE_EXPORT ICourse : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id NOTIFY idChanged) Q_PROPERTY(QString title READ title NOTIFY titleChanged) Q_PROPERTY(QString i18nTitle READ i18nTitle NOTIFY titleChanged) Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) public: ICourse(QObject *parent = nullptr) : QObject(parent) { } virtual ~ICourse() = default; virtual QString id() const = 0; virtual QString foreignId() const = 0; virtual QString title() const = 0; virtual QString i18nTitle() const = 0; virtual QString description() const = 0; virtual std::shared_ptr language() const = 0; /** * @brief Lazy loading unit list * @return list of units in course */ - virtual QList unitList() = 0; + virtual QVector> units() = 0; virtual QUrl file() const = 0; Q_SIGNALS: void idChanged(); void titleChanged(); void descriptionChanged(); void languageChanged(); void unitAdded(); - void unitAboutToBeAdded(Unit*,int); + void unitAboutToBeAdded(std::shared_ptr,int); void unitsRemoved(); void unitsAboutToBeRemoved(int,int); }; Q_DECLARE_INTERFACE(ICourse, "com.kde.artikulate.ICourse/1.0") #endif // COURSE_H diff --git a/src/core/resources/courseparser.cpp b/src/core/resources/courseparser.cpp index 4d2665b..6f0b964 100644 --- a/src/core/resources/courseparser.cpp +++ b/src/core/resources/courseparser.cpp @@ -1,425 +1,425 @@ /* * 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 "courseparser.h" #include "core/icourse.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "core/phoneme.h" #include "artikulate_debug.h" #include #include #include #include #include #include #include #include QXmlSchema CourseParser::loadXmlSchema(const QString &schemeName) { QString relPath = QStringLiteral(":/artikulate/schemes/%1.xsd").arg(schemeName); QUrl file = QUrl::fromLocalFile(relPath); QXmlSchema schema; if (file.isEmpty() || schema.load(file) == false) { qCWarning(ARTIKULATE_PARSER()) << "Schema at file " << file.toLocalFile() << " is invalid."; } return schema; } QDomDocument CourseParser::loadDomDocument(const QUrl &path, const QXmlSchema &schema) { QDomDocument document; QXmlSchemaValidator validator(schema); if (!validator.validate(path)) { qCWarning(ARTIKULATE_PARSER()) << "Schema is not valid, aborting loading of XML document:" << path.toLocalFile(); return document; } QString errorMsg; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { if (!document.setContent(&file, &errorMsg)) { qCWarning(ARTIKULATE_PARSER()) << errorMsg; } } else { qCWarning(ARTIKULATE_PARSER()) << "Could not open XML document " << path.toLocalFile() << " for reading, aborting."; } return document; } std::vector> CourseParser::parseUnits(const QUrl &path, QVector> phonemes) { std::vector> units; QFileInfo info(path.toLocalFile()); if (!info.exists()) { qCCritical(ARTIKULATE_PARSER()()) << "No course file available at location" << path.toLocalFile(); return units; } QXmlStreamReader xml; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { xml.setDevice(&file); xml.readNextStartElement(); while (!xml.atEnd() && !xml.hasError()) { bool elementOk{ false }; QXmlStreamReader::TokenType token = xml.readNext(); if (token == QXmlStreamReader::StartDocument) { continue; } if (token == QXmlStreamReader::StartElement) { if (xml.name() == "units") { continue; } else if (xml.name() == "unit") { auto unit = parseUnit(xml, path, phonemes, elementOk); if (elementOk) { units.push_back(std::move(unit)); } } } } if (xml.hasError()) { qCCritical(ARTIKULATE_PARSER()) << "Error occurred when reading Course XML file:" << path.toLocalFile(); } } else { qCCritical(ARTIKULATE_PARSER()) << "Could not open course file" << path.toLocalFile(); } xml.clear(); file.close(); return units; } std::unique_ptr CourseParser::parseUnit(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok) { std::unique_ptr unit(new Unit); ok = true; if (xml.tokenType() != QXmlStreamReader::StartElement && xml.name() == "unit") { qCWarning(ARTIKULATE_PARSER()) << "Expected to parse 'unit' element, aborting here"; return unit; } xml.readNext(); while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "unit")) { if (xml.tokenType() == QXmlStreamReader::StartElement) { bool elementOk{ false }; if (xml.name() == "id") { unit->setId(parseElement(xml, elementOk)); ok &= elementOk; } else if (xml.name() == "foreignId") { unit->setForeignId(parseElement(xml, elementOk)); ok &= elementOk; } else if (xml.name() == "title") { unit->setTitle(parseElement(xml, elementOk)); ok &= elementOk; } else if (xml.name() == "phrases") { // nothing to do } else if (xml.name() == "phrase") { auto phrase = parsePhrase(xml, path, phonemes, elementOk); if (elementOk) { unit->addPhrase(phrase); } ok &= elementOk; } else { qCWarning(ARTIKULATE_PARSER()) << "Skipping unknown token" << xml.name(); } } xml.readNext(); } if (!ok) { qCWarning(ARTIKULATE_PARSER()) << "Errors occured while parsing unit" << unit->title() << unit->id(); } return unit; } Phrase * CourseParser::parsePhrase(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok) { Phrase * phrase = new Phrase; ok = true; if (xml.tokenType() != QXmlStreamReader::StartElement && xml.name() == "phrase") { qCWarning(ARTIKULATE_PARSER()) << "Expected to parse 'phrase' element, aborting here"; ok = false; return phrase; } xml.readNext(); while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "phrase")) { if (xml.tokenType() == QXmlStreamReader::StartElement) { bool elementOk{ false }; if (xml.name() == "id") { phrase->setId(parseElement(xml, elementOk)); ok &= elementOk; } else if (xml.name() == "foreignId") { phrase->setForeignId(parseElement(xml, elementOk)); ok &= elementOk; } else if (xml.name() == "text") { phrase->setText(parseElement(xml, elementOk)); ok &= elementOk; } else if (xml.name() == "i18nText") { phrase->seti18nText(parseElement(xml, elementOk)); ok &= elementOk; } else if (xml.name() == "soundFile") { phrase->setSound(QUrl::fromLocalFile( path.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path() + '/' + parseElement(xml, elementOk))); ok &= elementOk; } else if (xml.name() == "phonemes") { auto parsedPhonemeIds = parsePhonemeIds(xml, elementOk); for (auto phoneme : phonemes) { if (parsedPhonemeIds.contains(phoneme->id())) { phrase->addPhoneme(phoneme.get()); } } ok &= elementOk; } else if (xml.name() == "type") { const QString type = parseElement(xml, elementOk); if (type == "word") { phrase->setType(Phrase::Word); } else if (type == "expression") { phrase->setType(Phrase::Expression); } else if (type == "sentence") { phrase->setType(Phrase::Sentence); } else if (type == "paragraph") { phrase->setType(Phrase::Paragraph); } ok &= elementOk; } else if (xml.name() == "editState") { const QString type = parseElement(xml, elementOk); if (type == "translated") { phrase->setEditState(Phrase::EditState::Translated); } else if (type == "completed") { phrase->setEditState(Phrase::EditState::Completed); } else if (type == "unknown") { phrase->setEditState(Phrase::EditState::Completed); } ok &= elementOk; } else { qCWarning(ARTIKULATE_PARSER()) << "Skipping unknown token" << xml.name(); } } xml.readNext(); } if (!ok) { qCWarning(ARTIKULATE_PARSER()) << "Errors occured while parsing phrase" << phrase->text() << phrase->id(); } return phrase; } QStringList CourseParser::parsePhonemeIds(QXmlStreamReader &xml, bool &ok) { QStringList ids; ok = true; if (xml.tokenType() != QXmlStreamReader::StartElement && xml.name() == "phonemes") { qCWarning(ARTIKULATE_PARSER()) << "Expected to parse 'phonemes' element, aborting here"; ok = false; return ids; } xml.readNext(); while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "phonemes")) { xml.readNext(); if (xml.tokenType() == QXmlStreamReader::StartElement) { if (xml.name() == "phonemeID") { bool elementOk{ false }; ids.append(parseElement(xml, elementOk)); ok &= elementOk; } else { qCWarning(ARTIKULATE_PARSER()) << "Skipping unknown token" << xml.name(); } } } return ids; } QString CourseParser::parseElement(QXmlStreamReader& xml, bool &ok) { ok = true; if (xml.tokenType() != QXmlStreamReader::StartElement) { qCCritical(ARTIKULATE_PARSER()) << "Parsing element that does not start with a start element"; ok = false; return QString(); } QString elementName = xml.name().toString(); xml.readNext(); qCDebug(ARTIKULATE_PARSER()) << "parsed: " << elementName << " / " << xml.text().toString(); return xml.text().toString(); } QDomDocument CourseParser::serializedDocument(ICourse *course, bool trainingExport) { QDomDocument document; // prepare xml header QDomProcessingInstruction header = document.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\"")); document.appendChild(header); // create main element QDomElement root = document.createElement(QStringLiteral("course")); document.appendChild(root); QDomElement idElement = document.createElement(QStringLiteral("id")); QDomElement titleElement = document.createElement(QStringLiteral("title")); QDomElement descriptionElement = document.createElement(QStringLiteral("description")); QDomElement languageElement = document.createElement(QStringLiteral("language")); idElement.appendChild(document.createTextNode(course->id())); titleElement.appendChild(document.createTextNode(course->title())); descriptionElement.appendChild(document.createTextNode(course->description())); languageElement.appendChild(document.createTextNode(course->id())); QDomElement unitListElement = document.createElement(QStringLiteral("units")); // create units - for (Unit *unit : course->unitList()) { + for (auto unit : course->units()) { QDomElement unitElement = document.createElement(QStringLiteral("unit")); QDomElement unitIdElement = document.createElement(QStringLiteral("id")); QDomElement unitTitleElement = document.createElement(QStringLiteral("title")); QDomElement unitPhraseListElement = document.createElement(QStringLiteral("phrases")); unitIdElement.appendChild(document.createTextNode(unit->id())); unitTitleElement.appendChild(document.createTextNode(unit->title())); // construct phrases for (Phrase *phrase : unit->phraseList()) { if (trainingExport && phrase->soundFileUrl().isEmpty()) { continue; } unitPhraseListElement.appendChild(serializedPhrase(phrase, document)); } if (trainingExport && unitPhraseListElement.childNodes().count() == 0) { continue; } // construct the unit element unitElement.appendChild(unitIdElement); if (!unit->foreignId().isEmpty()) { QDomElement unitForeignIdElement = document.createElement(QStringLiteral("foreignId")); unitForeignIdElement.appendChild(document.createTextNode(unit->foreignId())); unitElement.appendChild(unitForeignIdElement); } unitElement.appendChild(unitTitleElement); unitElement.appendChild(unitPhraseListElement); unitListElement.appendChild(unitElement); } root.appendChild(idElement); if (!course->foreignId().isEmpty()) { QDomElement courseForeignIdElement = document.createElement(QStringLiteral("foreignId")); courseForeignIdElement.appendChild(document.createTextNode(course->foreignId())); root.appendChild(courseForeignIdElement); } root.appendChild(titleElement); root.appendChild(descriptionElement); root.appendChild(languageElement); root.appendChild(unitListElement); return document; } QDomElement CourseParser::serializedPhrase(Phrase *phrase, QDomDocument &document) { QDomElement phraseElement = document.createElement(QStringLiteral("phrase")); QDomElement phraseIdElement = document.createElement(QStringLiteral("id")); QDomElement phraseTextElement = document.createElement(QStringLiteral("text")); QDomElement phrasei18nTextElement = document.createElement(QStringLiteral("i18nText")); QDomElement phraseSoundFileElement = document.createElement(QStringLiteral("soundFile")); QDomElement phraseTypeElement = document.createElement(QStringLiteral("type")); QDomElement phraseEditStateElement = document.createElement(QStringLiteral("editState")); QDomElement phrasePhonemeListElement = document.createElement(QStringLiteral("phonemes")); phraseIdElement.appendChild(document.createTextNode(phrase->id())); phraseTextElement.appendChild(document.createTextNode(phrase->text())); phrasei18nTextElement.appendChild(document.createTextNode(phrase->i18nText())); phraseSoundFileElement.appendChild(document.createTextNode(phrase->sound().fileName())); phraseTypeElement.appendChild(document.createTextNode(phrase->typeString())); phraseEditStateElement.appendChild(document.createTextNode(phrase->editStateString())); // add phonemes foreach (Phoneme *phoneme, phrase->phonemes()) { QDomElement phonemeElement = document.createElement(QStringLiteral("phonemeID")); phonemeElement.appendChild(document.createTextNode(phoneme->id())); phrasePhonemeListElement.appendChild(phonemeElement); } phraseElement.appendChild(phraseIdElement); if (!phrase->foreignId().isEmpty()) { QDomElement phraseForeignIdElement = document.createElement(QStringLiteral("foreignId")); phraseForeignIdElement.appendChild(document.createTextNode(phrase->foreignId())); phraseElement.appendChild(phraseForeignIdElement); } phraseElement.appendChild(phraseTextElement); phraseElement.appendChild(phrasei18nTextElement); phraseElement.appendChild(phraseSoundFileElement); phraseElement.appendChild(phraseTypeElement); phraseElement.appendChild(phraseEditStateElement); phraseElement.appendChild(phrasePhonemeListElement); if (phrase->isExcluded()) { QDomElement phraseIsExcludedElement = document.createElement(QStringLiteral("excluded")); phraseIsExcludedElement.appendChild(document.createTextNode(QStringLiteral("true"))); phraseElement.appendChild(phraseIsExcludedElement); } return phraseElement; } bool CourseParser::exportCourseToGhnsPackage(ICourse *course, const QString &exportPath) { // filename const QString fileName = course->id() + ".tar.bz2"; KTar tar = KTar(exportPath + '/' + fileName, QStringLiteral("application/x-bzip")); if (!tar.open(QIODevice::WriteOnly)) { qCWarning(ARTIKULATE_CORE()) << "Unable to open tar file" << exportPath + '/' + fileName << "in write mode, aborting."; return false; } - for (auto *unit : course->unitList()) { + for (auto unit : course->units()) { for (auto *phrase : unit->phraseList()) { if (QFile::exists(phrase->soundFileUrl())) { tar.addLocalFile(phrase->soundFileUrl(), phrase->id() + ".ogg"); } } } tar.writeFile(course->id() + ".xml", CourseParser::serializedDocument(course, true).toByteArray()); tar.close(); return true; } diff --git a/src/core/resources/courseresource.cpp b/src/core/resources/courseresource.cpp index eeb2d31..b02b0d4 100644 --- a/src/core/resources/courseresource.cpp +++ b/src/core/resources/courseresource.cpp @@ -1,278 +1,267 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * Copyright 2013 Oindrila Gupta * * 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 "courseresource.h" #include "courseparser.h" #include "core/language.h" #include "core/unit.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/language.h" #include "core/iresourcerepository.h" #include #include #include #include #include #include #include #include #include "artikulate_debug.h" class CourseResourcePrivate { public: CourseResourcePrivate() = default; ~CourseResourcePrivate(); void loadCourse(CourseResource *parent); IResourceRepository *m_repository{ nullptr }; QUrl m_file; QString m_identifier; QString m_foreignId; QString m_title; QString m_languageId; std::shared_ptr m_language; QString m_i18nTitle; QString m_description; QVector> m_units; bool m_courseLoaded{ false }; ///> phonemes = m_language->phonemes(); auto units = CourseParser::parseUnits(m_file, phonemes); for (auto &unit : units) { parent->addUnit(std::move(unit)); } } CourseResource::CourseResource(const QUrl &path, IResourceRepository *repository) : ICourse(repository) , d(new CourseResourcePrivate()) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); d->m_file = path; d->m_repository = repository; // load basic information from language file, but does not parse everything QXmlStreamReader xml; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { xml.setDevice(&file); xml.readNextStartElement(); while (xml.readNext() && !xml.atEnd()) { if (xml.name() == "id") { d->m_identifier = xml.readElementText(); continue; } if (xml.name() == "foreignId") { d->m_foreignId = xml.readElementText(); continue; } //TODO i18nTitle must be implemented, currently missing and hence not parsed if (xml.name() == "title") { d->m_title = xml.readElementText(); d->m_i18nTitle = d->m_title; continue; } if (xml.name() == "description") { d->m_description = xml.readElementText(); continue; } if (xml.name() == "language") { d->m_languageId = xml.readElementText(); continue; } // quit reading when basic elements are read if (!d->m_identifier.isEmpty() && !d->m_title.isEmpty() && !d->m_i18nTitle.isEmpty() && !d->m_description.isEmpty() && !d->m_languageId.isEmpty() && !d->m_foreignId.isEmpty() ) { break; } } if (xml.hasError()) { qCritical() << "Error occurred when reading Course XML file:" << path.toLocalFile(); } } else { qCCritical(ARTIKULATE_CORE()) << "Could not open course file" << path.toLocalFile(); } xml.clear(); file.close(); // find correct language if (repository != nullptr) { for (const auto &language : repository->languages()) { if (language == nullptr) { continue; } if (language->id() == d->m_languageId) { d->m_language = language; } } } if (d->m_language == nullptr) { qCCritical(ARTIKULATE_CORE()) << "A course with an unknown language was loaded"; } } CourseResource::~CourseResource() = default; QString CourseResource::id() const { return d->m_identifier; } void CourseResource::setId(const QString &id) { if (d->m_identifier == id) { return; } d->m_identifier = id; emit idChanged(); } QString CourseResource::foreignId() const { return d->m_foreignId; } void CourseResource::setForeignId(const QString &foreignId) { if (d->m_foreignId == foreignId) { return; } d->m_foreignId = foreignId; emit foreignIdChanged(); } QString CourseResource::title() const { return d->m_title; } void CourseResource::setTitle(const QString &title) { if (d->m_title == title) { return; } d->m_title = title; emit titleChanged(); } QString CourseResource::i18nTitle() const { return d->m_i18nTitle; } void CourseResource::setI18nTitle(const QString &i18nTitle) { if (d->m_i18nTitle == i18nTitle) { return; } d->m_i18nTitle = i18nTitle; emit i18nTitleChanged(); } QString CourseResource::description() const { return d->m_description; } void CourseResource::setDescription(const QString &description) { if (d->m_description == description) { return; } d->m_description = description; emit descriptionChanged(); } std::shared_ptr CourseResource::language() const { return d->m_language; } void CourseResource::setLanguage(std::shared_ptr language) { if (d->m_language == language) { return; } d->m_language = language; emit languageChanged(); } std::shared_ptr CourseResource::addUnit(std::unique_ptr unit) { - emit unitAboutToBeAdded(unit.get(), d->m_units.count() - 1); - d->m_units.append(std::move(unit)); + std::shared_ptr storedUnit(std::move(unit)); + emit unitAboutToBeAdded(storedUnit, d->m_units.count() - 1); + d->m_units.append(storedUnit); emit unitAdded(); - return d->m_units.last(); -} - -QList CourseResource::unitList() -{ - if (d->m_courseLoaded == false) { - d->loadCourse(this); - } - QList rawList; - for (auto unit : d->m_units) { - rawList.append(unit.get()); - } - return rawList; + return storedUnit; } QVector> CourseResource::units() { if (d->m_courseLoaded == false) { d->loadCourse(this); } return d->m_units; } QUrl CourseResource::file() const { return d->m_file; } diff --git a/src/core/resources/courseresource.h b/src/core/resources/courseresource.h index acd2b5f..b1d8b52 100644 --- a/src/core/resources/courseresource.h +++ b/src/core/resources/courseresource.h @@ -1,118 +1,116 @@ /* * Copyright 2013 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 COURSERESOURCE_H #define COURSERESOURCE_H #include "artikulatecore_export.h" #include "core/icourse.h" #include #include #include class QString; class CourseResourcePrivate; class Unit; class Phrase; class IResourceRepository; class ARTIKULATECORE_EXPORT CourseResource : public ICourse { Q_OBJECT Q_INTERFACES(ICourse) public: /** * Create course resource from file. */ explicit CourseResource(const QUrl &path, IResourceRepository *repository); ~CourseResource() override; /** * \return unique identifier */ QString id() const override; void setId(const QString &id); /** * \return global ID for this course */ QString foreignId() const override; void setForeignId(const QString &foreignId); /** * \return human readable localized title */ QString title() const override; void setTitle(const QString &title); /** * \return human readable title in English */ QString i18nTitle() const override; void setI18nTitle(const QString &i18nTitle); /** * \return description text for course */ QString description() const override; void setDescription(const QString &description); /** * \return language identifier of this course */ std::shared_ptr language() const override; void setLanguage(std::shared_ptr language); std::shared_ptr addUnit(std::unique_ptr unit); /** * \return true if resource is loaded, otherwise false */ bool isOpen() const; void sync(); QUrl file() const override; - QList unitList() override; - - QVector> units(); + QVector> units() override; Q_SIGNALS: void idChanged(); void foreignIdChanged(); void titleChanged(); void i18nTitleChanged(); void descriptionChanged(); void languageChanged(); private: const QScopedPointer d; }; #endif diff --git a/src/core/resources/editablecourseresource.cpp b/src/core/resources/editablecourseresource.cpp index 824af10..57d58cb 100644 --- a/src/core/resources/editablecourseresource.cpp +++ b/src/core/resources/editablecourseresource.cpp @@ -1,221 +1,221 @@ /* * 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 "editablecourseresource.h" #include "courseparser.h" #include "artikulate_debug.h" #include "core/unit.h" #include "core/phrase.h" #include "core/phoneme.h" #include #include #include #include #include #include #include #include #include EditableCourseResource::EditableCourseResource(const QUrl &path, IResourceRepository *repository) : IEditableCourse() , m_course(new CourseResource(path, repository)) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); connect(m_course.get(), &ICourse::unitAboutToBeAdded, this, &ICourse::unitAboutToBeAdded); connect(m_course.get(), &ICourse::unitAdded, this, &ICourse::unitAdded); connect(m_course.get(), &CourseResource::idChanged, this, &EditableCourseResource::idChanged); connect(m_course.get(), &CourseResource::foreignIdChanged, this, &EditableCourseResource::foreignIdChanged); connect(m_course.get(), &CourseResource::titleChanged, this, &EditableCourseResource::titleChanged); connect(m_course.get(), &CourseResource::descriptionChanged, this, &EditableCourseResource::descriptionChanged); connect(m_course.get(), &CourseResource::languageChanged, this, &EditableCourseResource::languageChanged); } QString EditableCourseResource::id() const { return m_course->id(); } void EditableCourseResource::setId(QString id) { m_course->setId(id); } QString EditableCourseResource::foreignId() const { return m_course->foreignId(); } void EditableCourseResource::setForeignId(QString foreignId) { m_course->setForeignId(std::move(foreignId)); } QString EditableCourseResource::title() const { return m_course->title(); } void EditableCourseResource::setTitle(QString title) { m_course->setTitle(title); } QString EditableCourseResource::i18nTitle() const { return m_course->i18nTitle(); } void EditableCourseResource::setI18nTitle(QString i18nTitle) { m_course->setI18nTitle(i18nTitle); } QString EditableCourseResource::description() const { return m_course->description(); } void EditableCourseResource::setDescription(QString description) { m_course->setDescription(description); } std::shared_ptr EditableCourseResource::language() const { return m_course->language(); } void EditableCourseResource::setLanguage(std::shared_ptr language) { m_course->setLanguage(language); } -QList EditableCourseResource::unitList() -{ - return m_course->unitList(); -} - QUrl EditableCourseResource::file() const { return m_course->file(); } void EditableCourseResource::sync() { Q_ASSERT(file().isValid()); Q_ASSERT(file().isLocalFile()); Q_ASSERT(!file().isEmpty()); // not writing back if not modified if (!m_modified) { qCDebug(ARTIKULATE_LOG()) << "Aborting sync, course was not modified."; return; } exportCourse(file()); } bool EditableCourseResource::exportCourse(const QUrl &filePath) { // write back to file // create directories if necessary QFileInfo info(filePath.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); if (!info.exists()) { qCDebug(ARTIKULATE_LOG()) << "create xml output file directory, not existing"; QDir dir; dir.mkpath(filePath.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } //TODO port to KSaveFile QFile file(filePath.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { qCWarning(ARTIKULATE_LOG()) << "Unable to open file " << file.fileName() << " in write mode, aborting."; return false; } file.write(CourseParser::serializedDocument(m_course.get(), false).toByteArray()); return true; } std::shared_ptr EditableCourseResource::addUnit(std::unique_ptr unit) { setModified(true); return m_course->addUnit(std::move(unit)); } +QVector> EditableCourseResource::units() +{ + return m_course->units(); +} + bool EditableCourseResource::isModified() const { return m_modified; } void EditableCourseResource::setModified(bool modified) { m_modified = modified; } Unit * EditableCourseResource::createUnit() { // find first unused id QStringList unitIds; for (auto unit : m_course->units()) { unitIds.append(unit->id()); } QString id = QUuid::createUuid().toString(); while (unitIds.contains(id)) { id = QUuid::createUuid().toString(); qCWarning(ARTIKULATE_LOG) << "Unit id generator has found a collision, recreating id."; } // create unit std::unique_ptr unit(new Unit(this)); unit->setCourse(this); unit->setId(id); unit->setTitle(i18n("New Unit")); auto sharedUnit = addUnit(std::move(unit)); return sharedUnit.get(); } Phrase * EditableCourseResource::createPhrase(Unit *unit) { // find globally unique phrase id inside course QStringList phraseIds; for (auto unit : m_course->units()) { for (auto *phrase : unit->phraseList()) { phraseIds.append(phrase->id()); } } QString id = QUuid::createUuid().toString(); while (phraseIds.contains(id)) { id = QUuid::createUuid().toString(); qCWarning(ARTIKULATE_LOG) << "Phrase id generator has found a collision, recreating id."; } // create unit Phrase *phrase = new Phrase(this); phrase->setId(id); phrase->setText(QLatin1String("")); phrase->setType(Phrase::Word); unit->addPhrase(phrase); return phrase; } diff --git a/src/core/resources/editablecourseresource.h b/src/core/resources/editablecourseresource.h index 538765a..a352d7f 100644 --- a/src/core/resources/editablecourseresource.h +++ b/src/core/resources/editablecourseresource.h @@ -1,139 +1,137 @@ /* * 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 EDITABLECOURSERESOURCE_H #define EDITABLECOURSERESOURCE_H #include "artikulatecore_export.h" #include "courseresource.h" #include "core/icourse.h" #include "core/ieditablecourse.h" #include #include #include class IResourceRepository; class Course; class Unit; class Phrase; class QString; class QDomDocument; /** * @brief Decorator for CourseResource * * This decorator adds functionality to modify and write back changes of a course. */ class ARTIKULATECORE_EXPORT EditableCourseResource : public IEditableCourse { Q_OBJECT Q_INTERFACES(ICourse) Q_INTERFACES(IEditableCourse) public: /** * Create course resource from file. */ explicit EditableCourseResource(const QUrl &path, IResourceRepository *repository); ~EditableCourseResource() override = default; /** * \return unique identifier */ QString id() const override; void setId(QString id) override; /** * \return unique identifier */ QString foreignId() const override; void setForeignId(QString foreignId) override; /** * \return human readable localized title */ QString title() const override; void setTitle(QString title) override; /** * \return human readable title in English */ QString i18nTitle() const override; void setI18nTitle(QString i18nTitle) override; /** * \return description text for course */ QString description() const override; void setDescription(QString description) override; /** * \return language identifier of this course */ std::shared_ptr language() const override; void setLanguage(std::shared_ptr language) override; void sync(); /** * @brief Export course to specified file. * @param filePath the absolute path to the export file * @return true of export finished without errors */ bool exportCourse(const QUrl &filePath); std::shared_ptr addUnit(std::unique_ptr unit) override; + QVector> units() override; bool isModified() const; - QUrl file() const override; void setFile(const QUrl &) {} - QList unitList() override; - Q_INVOKABLE Unit * createUnit(); Q_INVOKABLE Phrase * createPhrase(Unit *unit); Q_SIGNALS: void idChanged(); void foreignIdChanged(); void titleChanged(); void i18nTitleChanged(); void descriptionChanged(); void languageChanged(); private: Q_DISABLE_COPY(EditableCourseResource) void setModified(bool modified); bool m_modified{ false }; const std::unique_ptr m_course; }; #endif diff --git a/src/core/resources/skeletonresource.cpp b/src/core/resources/skeletonresource.cpp index ce22efa..632097e 100644 --- a/src/core/resources/skeletonresource.cpp +++ b/src/core/resources/skeletonresource.cpp @@ -1,317 +1,314 @@ /* * Copyright 2013 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 "skeletonresource.h" #include "courseparser.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "editablecourseresource.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/resources/languageresource.h" #include #include #include #include #include #include #include #include "artikulate_debug.h" class SkeletonResourcePrivate { public: SkeletonResourcePrivate(const QUrl &path) : m_path(path) { // load basic information from language file, but does not parse everything QXmlStreamReader xml; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { xml.setDevice(&file); xml.readNextStartElement(); while (xml.readNext() && !xml.atEnd()) { if (xml.name() == "id") { m_identifier = xml.readElementText(); continue; } if (xml.name() == "title") { m_title = xml.readElementText(); continue; } if (xml.name() == "description") { m_description = xml.readElementText(); continue; } // quit reading when basic elements are read if (!m_identifier.isEmpty() && !m_title.isEmpty() && !m_description.isEmpty() ) { break; } } if (xml.hasError()) { qCritical() << "Error occurred when reading Course XML file:" << path.toLocalFile(); } } else { qCCritical(ARTIKULATE_CORE()) << "Could not open course file" << path.toLocalFile(); } xml.clear(); file.close(); } QVector> units(); - std::shared_ptr appendUnit(std::unique_ptr unit); + std::shared_ptr appendUnit(std::shared_ptr unit); /** * @return the skeleton resource as serialized byte array */ QDomDocument serializedSkeleton(); QUrl m_path; QString m_identifier; QString m_title; QString m_description; bool m_unitsParsed{ false }; protected: QVector> m_units; ///!< the units variable is loaded lazily and shall never be access directly }; QVector> SkeletonResourcePrivate::units() { if (m_unitsParsed) { return m_units; } auto units = CourseParser::parseUnits(m_path); for (auto &unit : units) { m_units.append(std::move(unit)); } m_unitsParsed = true; return m_units; } -std::shared_ptr SkeletonResourcePrivate::appendUnit(std::unique_ptr unit) { +std::shared_ptr SkeletonResourcePrivate::appendUnit(std::shared_ptr unit) { units(); // ensure that units are parsed - m_units.append(std::move(unit)); + m_units.append(unit); return m_units.last(); } QDomDocument SkeletonResourcePrivate::serializedSkeleton() { QDomDocument document; // prepare xml header QDomProcessingInstruction header = document.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\"")); document.appendChild(header); // create main element QDomElement root = document.createElement(QStringLiteral("skeleton")); document.appendChild(root); QDomElement idElement = document.createElement(QStringLiteral("id")); QDomElement titleElement = document.createElement(QStringLiteral("title")); QDomElement descriptionElement = document.createElement(QStringLiteral("description")); idElement.appendChild(document.createTextNode(m_identifier)); titleElement.appendChild(document.createTextNode(m_title)); descriptionElement.appendChild(document.createTextNode(m_description)); QDomElement unitListElement = document.createElement(QStringLiteral("units")); // create units for (auto unit : units()) { QDomElement unitElement = document.createElement(QStringLiteral("unit")); QDomElement unitIdElement = document.createElement(QStringLiteral("id")); QDomElement unitTitleElement = document.createElement(QStringLiteral("title")); QDomElement unitPhraseListElement = document.createElement(QStringLiteral("phrases")); unitIdElement.appendChild(document.createTextNode(unit->id())); unitTitleElement.appendChild(document.createTextNode(unit->title())); // construct phrases for (Phrase *phrase : unit->phraseList()) { QDomElement phraseElement = document.createElement(QStringLiteral("phrase")); QDomElement phraseIdElement = document.createElement(QStringLiteral("id")); QDomElement phraseTextElement = document.createElement(QStringLiteral("text")); QDomElement phraseTypeElement = document.createElement(QStringLiteral("type")); phraseIdElement.appendChild(document.createTextNode(phrase->id())); phraseTextElement.appendChild(document.createTextNode(phrase->text())); phraseTypeElement.appendChild(document.createTextNode(phrase->typeString())); phraseElement.appendChild(phraseIdElement); phraseElement.appendChild(phraseTextElement); phraseElement.appendChild(phraseTypeElement); unitPhraseListElement.appendChild(phraseElement); } // construct the unit element unitElement.appendChild(unitIdElement); unitElement.appendChild(unitTitleElement); unitElement.appendChild(unitPhraseListElement); unitListElement.appendChild(unitElement); } root.appendChild(idElement); root.appendChild(titleElement); root.appendChild(descriptionElement); root.appendChild(unitListElement); return document; } SkeletonResource::SkeletonResource(const QUrl &path, IResourceRepository *repository) : IEditableCourse() , d(new SkeletonResourcePrivate(path)) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); Q_UNUSED(repository); } SkeletonResource::~SkeletonResource() = default; QString SkeletonResource::id() const { return d->m_identifier; } void SkeletonResource::setId(QString id) { if (d->m_identifier == id) { return; } d->m_identifier = id; emit idChanged(); } QString SkeletonResource::foreignId() const { return id(); } void SkeletonResource::setForeignId(QString id) { Q_UNUSED(id); Q_UNREACHABLE(); } QString SkeletonResource::title() const { return d->m_title; } void SkeletonResource::setTitle(QString title) { if (d->m_title == title) { return; } d->m_title = title; emit titleChanged(); } QString SkeletonResource::i18nTitle() const { // there are no localized titles available return title(); } void SkeletonResource::setI18nTitle(QString title) { Q_UNUSED(title); Q_UNREACHABLE(); } QString SkeletonResource::description() const { return d->m_description; } void SkeletonResource::setDescription(QString description) { if (d->m_description == description) { return; } d->m_description = description; emit descriptionChanged(); } bool SkeletonResource::exportCourse(const QUrl &filePath) { // write back to file // create directories if necessary QFileInfo info(filePath.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); if (!info.exists()) { qCDebug(ARTIKULATE_LOG()) << "create xml output file directory, not existing"; QDir dir; dir.mkpath(filePath.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } //TODO port to atomic file swap QFile file(filePath.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { qCWarning(ARTIKULATE_LOG()) << "Unable to open file " << filePath << " in write mode, aborting."; return false; } file.write(d->serializedSkeleton().toByteArray()); return true; } std::shared_ptr SkeletonResource::addUnit(std::unique_ptr unit) { - emit unitAboutToBeAdded(unit.get(), d->units().count() - 1); - auto sharedUnit = d->appendUnit(std::move(unit)); + std::shared_ptr storedUnit(std::move(unit)); + emit unitAboutToBeAdded(storedUnit, d->units().count() - 1); + d->appendUnit(storedUnit); emit unitAdded(); - return sharedUnit; + return storedUnit; } std::shared_ptr SkeletonResource::language() const { // skeleton must not have a dedicated language return std::shared_ptr(); } void SkeletonResource::setLanguage(std::shared_ptr language) { Q_UNUSED(language); Q_UNREACHABLE(); } -QList SkeletonResource::unitList() +QVector> SkeletonResource::units() { - QList rawList; - for (auto unit : d->units()) { - rawList.append(unit.get()); - } - return rawList; + return d->units(); } QUrl SkeletonResource::file() const { return d->m_path; } diff --git a/src/core/resources/skeletonresource.h b/src/core/resources/skeletonresource.h index 893b248..c6cfdc2 100644 --- a/src/core/resources/skeletonresource.h +++ b/src/core/resources/skeletonresource.h @@ -1,75 +1,75 @@ /* * Copyright 2013 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 SKELETONRESOURCE_H #define SKELETONRESOURCE_H #include "artikulatecore_export.h" #include "core/ieditablecourse.h" #include class SkeletonResourcePrivate; class IResourceRepository; /** * @brief The SkeletonResource class is a decorator for EditableCourseResource */ class ARTIKULATECORE_EXPORT SkeletonResource : public IEditableCourse { Q_OBJECT Q_INTERFACES(ICourse) public: /** * Create course resource from file. */ explicit SkeletonResource(const QUrl &path, IResourceRepository *repository); ~SkeletonResource() override; QString id() const override; void setId(QString id) override; QString foreignId() const override; void setForeignId(QString id) override; QString title() const override; void setTitle(QString title) override; QString i18nTitle() const override; void setI18nTitle(QString title) override; QString description() const override; void setDescription(QString description) override; std::shared_ptr language() const override; void setLanguage(std::shared_ptr language) override; - QList unitList() override; + QVector> units() override; QUrl file() const override; bool exportCourse(const QUrl &filePath); std::shared_ptr addUnit(std::unique_ptr unit) override; bool isModified() const { return true;} //FIXME private: const QScopedPointer d; }; #endif diff --git a/src/core/trainingsession.cpp b/src/core/trainingsession.cpp index d2a2182..bdc9862 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) : QObject(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->unitList().count() > 0) { - setUnit(m_course->unitList().constFirst()); + 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->unitList(); - for (Unit *unit : qAsConst(unitList)) { + const auto unitList = m_course->units(); + for (auto unit : qAsConst(unitList)) { const auto phraseList = unit->phraseList(); for (Phrase *phrase : qAsConst(phraseList)) { auto iter = data.find(phrase->id()); if (iter != data.end()) { phrase->setProgress(iter.value()); } } } updateTrainingActions(); emit courseChanged(); } Unit * TrainingSession::activeUnit() const { if (auto phrase = activePhrase()) { return phrase->unit(); } return nullptr; } void TrainingSession::setUnit(Unit *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()) { 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)); } Phrase * TrainingSession::activePhrase() const { if (const auto action = activeAction()) { return action->phrase(); } return nullptr; } void TrainingSession::setPhrase(Phrase *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); // 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() ); 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); // 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() ); 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->unitList(); + const auto unitList = m_course->units(); for (const auto &unit : qAsConst(unitList)) { auto action = new TrainingAction(unit->title(), this); const auto phraseList = unit->phraseList(); for (const auto &phrase : qAsConst(phraseList)) { if (phrase->sound().isEmpty()) { continue; } - action->appendChild(new TrainingAction(phrase, this, unit)); + 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->unitList().count() > 0) { + if (m_course->units().count() > 0) { m_indexUnit = 0; - if (m_course->unitList().constFirst()->phraseList().count() > 0) { + if (m_course->units().constFirst()->phraseList().count() > 0) { m_indexPhrase = 0; } } } diff --git a/src/models/phrasemodel.cpp b/src/models/phrasemodel.cpp index 21dfed1..ff0b3c1 100644 --- a/src/models/phrasemodel.cpp +++ b/src/models/phrasemodel.cpp @@ -1,338 +1,351 @@ /* * 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 "phrasemodel.h" #include "core/icourse.h" #include "core/unit.h" #include "core/phrase.h" #include #include #include #include "artikulate_debug.h" PhraseModel::PhraseModel(QObject *parent) : QAbstractItemModel(parent) , m_course(nullptr) , m_unitSignalMapper(new QSignalMapper) , m_phraseSignalMapper(new QSignalMapper) { connect(m_unitSignalMapper, static_cast(&QSignalMapper::mapped), this, &PhraseModel::onUnitChanged); connect(m_phraseSignalMapper, static_cast(&QSignalMapper::mapped), this, &PhraseModel::onPhraseChanged); } QHash< int, QByteArray > PhraseModel::roleNames() const { QHash roles; roles[TextRole] = "text"; roles[DataRole] = "dataRole"; return roles; } void PhraseModel::setCourse(ICourse *course) { if (m_course == course) { return; } beginResetModel(); if (m_course) { m_course->disconnect(this); - foreach (auto const &unit, m_course->unitList()) { + for (auto unit : m_course->units()) { unit->disconnect(this); foreach (auto const &phrase, unit->phraseList()) { phrase->disconnect(this); } } } m_course = course; if (m_course) { // connect to unit changes connect(m_course, &ICourse::unitAboutToBeAdded, this, &PhraseModel::onUnitAboutToBeAdded); connect(m_course, &ICourse::unitAdded, this, &PhraseModel::onUnitAdded); connect(m_course, &ICourse::unitsAboutToBeRemoved, this, &PhraseModel::onUnitsAboutToBeRemoved); connect(m_course, &ICourse::unitsRemoved, this, &PhraseModel::onUnitsRemoved); // initial setting of signal mappings - foreach (auto const &unit, m_course->unitList()) { + for (auto unit : m_course->units()) { // connect to phrase changes - connect(unit, &Unit::phraseAboutToBeAdded, this, &PhraseModel::onPhraseAboutToBeAdded); - connect(unit, &Unit::phraseAdded, this, &PhraseModel::onPhraseAdded); - connect(unit, &Unit::phraseAboutToBeRemoved, this, &PhraseModel::onPhrasesAboutToBeRemoved); - connect(unit, &Unit::phraseRemoved, this, &PhraseModel::onPhrasesRemoved); - connect(unit, &Unit::titleChanged, m_unitSignalMapper, static_cast(&QSignalMapper::map)); + connect(unit.get(), &Unit::phraseAboutToBeAdded, this, &PhraseModel::onPhraseAboutToBeAdded); + connect(unit.get(), &Unit::phraseAdded, this, &PhraseModel::onPhraseAdded); + connect(unit.get(), &Unit::phraseAboutToBeRemoved, this, &PhraseModel::onPhrasesAboutToBeRemoved); + connect(unit.get(), &Unit::phraseRemoved, this, &PhraseModel::onPhrasesRemoved); + connect(unit.get(), &Unit::titleChanged, m_unitSignalMapper, static_cast(&QSignalMapper::map)); // insert and connect all already existing phrases int phrases = unit->phraseList().count(); for (int i = 0; i < phrases; ++i) { onPhraseAboutToBeAdded(unit->phraseList().at(i), i); endInsertRows(); } } updateUnitMappings(); updatePhraseMappings(); } // emit done endResetModel(); emit courseChanged(); } ICourse * PhraseModel::course() const { return m_course; } QVariant PhraseModel::data(const QModelIndex &index, int role) const { Q_ASSERT(m_course); if (!index.isValid()) { return QVariant(); } if (!index.internalPointer()) { - if (!m_course || m_course->unitList().size() == 0) { + if (!m_course || m_course->units().size() == 0) { return QVariant(); } - Unit *unit = m_course->unitList().at(index.row()); + auto unit = m_course->units().at(index.row()); switch(role) { case TextRole: return unit->title(); case DataRole: - return QVariant::fromValue(unit); + return QVariant::fromValue(unit.get()); default: return QVariant(); } } else { Unit *unit = static_cast(index.internalPointer()); switch(role) { case TextRole: return unit->phraseList().at(index.row())->text(); case DataRole: return QVariant::fromValue(unit->phraseList().at(index.row())); default: return QVariant(); } } } int PhraseModel::rowCount(const QModelIndex &parent) const { if (!m_course) { return 0; } // no valid index -> must be (invisible) root if (!parent.isValid()) { - return m_course->unitList().count(); + return m_course->units().count(); } // internal pointer -> must be a phrase if (parent.internalPointer()) { return 0; } // else -> must be a unit - Unit *unit = m_course->unitList().at(parent.row()); + Unit *unit = m_course->units().at(parent.row()).get(); return unit->phraseList().count(); } int PhraseModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return 1; } QModelIndex PhraseModel::parent(const QModelIndex &child) const { if (!child.internalPointer() || !m_course) { return QModelIndex(); } Unit *parent = static_cast(child.internalPointer()); - for (int i = 0; i < m_course->unitList().count(); ++i) { - if (m_course->unitList().at(i) == parent) { + for (int i = 0; i < m_course->units().count(); ++i) { + if (m_course->units().at(i).get() == parent) { return createIndex(i, 0); } } return QModelIndex(); } QModelIndex PhraseModel::index(int row, int column, const QModelIndex &parent) const { if (!parent.isValid()) { // unit elements return createIndex(row, column); } else { // phrase elements - Unit *unit = m_course->unitList().at(parent.row()); + auto unit = m_course->units().at(parent.row()); if (unit) { - return createIndex(row, column, unit); + return createIndex(row, column, unit.get()); } } return QModelIndex(); } QModelIndex PhraseModel::indexPhrase(Phrase *phrase) const { if (!phrase) { return QModelIndex(); } Unit *unit = phrase->unit(); return createIndex(unit->phraseList().indexOf(phrase), 0, unit); } QModelIndex PhraseModel::indexUnit(Unit *unit) const { if (!unit || !m_course) { return QModelIndex(); } - return createIndex(m_course->unitList().indexOf(unit), 0); + int uIndex{ -1 }; + for (int i = 0; i < m_course->units().size(); ++i) { + if (m_course->units().at(i)->id() == unit->id()) { + uIndex = i; + break; + } + } + return createIndex(uIndex, 0); } bool PhraseModel::isUnit(const QModelIndex &index) const { return (index.internalPointer() == nullptr); } void PhraseModel::onPhraseAboutToBeAdded(Phrase *phrase, int index) { - int unitIndex = m_course->unitList().indexOf(phrase->unit()); + int uIndex{ -1 }; + for (int i = 0; i < m_course->units().size(); ++i) { + if (m_course->units().at(i)->id() == phrase->unit()->id()) { + uIndex = i; + break; + } + } connect(phrase, &Phrase::textChanged, m_phraseSignalMapper, static_cast(&QSignalMapper::map)); - beginInsertRows(createIndex(unitIndex, 0), index, index); + beginInsertRows(createIndex(uIndex, 0), index, index); } void PhraseModel::onPhraseAdded() { endInsertRows(); updatePhraseMappings(); } void PhraseModel::onPhrasesAboutToBeRemoved(int first, int last) { //TODO better solution requires access to unit //TODO remove connections from m_phraseSignalMapper beginResetModel(); } void PhraseModel::onPhrasesRemoved() { endResetModel(); } void PhraseModel::onPhraseChanged(QObject *phrase) { Phrase *changedPhrase = qobject_cast(phrase); Q_ASSERT(changedPhrase); QModelIndex index = indexPhrase(changedPhrase); emit dataChanged(index, index); } -void PhraseModel::onUnitAboutToBeAdded(Unit *unit, int index) +void PhraseModel::onUnitAboutToBeAdded(std::shared_ptr unit, int index) { Q_UNUSED(unit) beginInsertRows(QModelIndex(), index, index); - connect(unit, &Unit::titleChanged, m_unitSignalMapper, static_cast(&QSignalMapper::map)); + connect(unit.get(), &Unit::titleChanged, m_unitSignalMapper, static_cast(&QSignalMapper::map)); } void PhraseModel::onUnitAdded() { endInsertRows(); updateUnitMappings(); } void PhraseModel::onUnitsAboutToBeRemoved(int first, int last) { for (int i = first; i <= last; ++i) { - Unit *unit = m_course->unitList().at(i); - disconnect(unit, &Unit::titleChanged, m_unitSignalMapper, static_cast(&QSignalMapper::map)); + auto unit = m_course->units().at(i); + disconnect(unit.get(), &Unit::titleChanged, m_unitSignalMapper, static_cast(&QSignalMapper::map)); } beginRemoveRows(QModelIndex(), first, last); } void PhraseModel::onUnitsRemoved() { endRemoveRows(); } void PhraseModel::onUnitChanged(int index) { emit dataChanged(createIndex(index, 0), createIndex(index, 0)); } QVariant PhraseModel::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", "Phrase")); } bool PhraseModel::isPhrase(const QModelIndex &index) const { if (index.internalPointer()) { return true; } return false; } Phrase * PhraseModel::phrase(const QModelIndex &index) const { if (index.internalPointer()) { Unit *unit = static_cast(index.internalPointer()); return unit->phraseList().at(index.row()); } - if (!m_course->unitList().at(index.row())->phraseList().isEmpty()) { - return m_course->unitList().at(index.row())->phraseList().first(); + if (!m_course->units().at(index.row())->phraseList().isEmpty()) { + return m_course->units().at(index.row())->phraseList().first(); } return nullptr; } Unit * PhraseModel::unit(const QModelIndex &index) const { - return m_course->unitList().at(index.row()); + return m_course->units().at(index.row()).get(); } void PhraseModel::updateUnitMappings() { - int units = m_course->unitList().count(); + int units = m_course->units().count(); for (int i = 0; i < units; ++i) { - m_unitSignalMapper->setMapping(m_course->unitList().at(i), i); + m_unitSignalMapper->setMapping(m_course->units().at(i).get(), i); } } void PhraseModel::updatePhraseMappings() { //TODO this might be quite costly for long units // better, implement access based on index pairs - foreach (const Unit *unit, m_course->unitList()) { + for (auto unit : m_course->units()) { foreach (Phrase *phrase, unit->phraseList()) { m_phraseSignalMapper->setMapping(phrase, phrase); } } } diff --git a/src/models/phrasemodel.h b/src/models/phrasemodel.h index da1478d..eff3b7b 100644 --- a/src/models/phrasemodel.h +++ b/src/models/phrasemodel.h @@ -1,85 +1,86 @@ /* * 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 PHRASEMODEL_H #define PHRASEMODEL_H +#include #include #include "core/phrase.h" class ICourse; class QSignalMapper; class PhraseModel : public QAbstractItemModel { Q_OBJECT Q_PROPERTY(ICourse *course READ course WRITE setCourse NOTIFY courseChanged) public: enum phraseRoles { TextRole = Qt::UserRole + 1, IdRole, DataRole }; explicit PhraseModel(QObject *parent = nullptr); virtual QHash roleNames() const override; void setCourse(ICourse *course); ICourse * course() const; virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override; virtual QModelIndex parent(const QModelIndex &child) const override; virtual QModelIndex index(int row, int column, const QModelIndex &parent) const override; virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; Q_INVOKABLE bool isPhrase(const QModelIndex &index) const; Q_INVOKABLE Phrase * phrase(const QModelIndex &index) const; Q_INVOKABLE Unit * unit(const QModelIndex &index) const; Q_INVOKABLE QModelIndex indexPhrase(Phrase *phrase) const; Q_INVOKABLE QModelIndex indexUnit(Unit *unit) const; Q_INVOKABLE bool isUnit(const QModelIndex& index) const; Q_SIGNALS: void phraseChanged(int index); void courseChanged(); void typeChanged(); private Q_SLOTS: void onPhraseAboutToBeAdded(Phrase *phrase, int index); void onPhraseAdded(); void onPhrasesAboutToBeRemoved(int first, int last); void onPhrasesRemoved(); void onPhraseChanged(QObject *phrase); - void onUnitAboutToBeAdded(Unit *unit, int index); + void onUnitAboutToBeAdded(std::shared_ptr unit, int index); void onUnitAdded(); void onUnitsAboutToBeRemoved(int first, int last); void onUnitsRemoved(); void onUnitChanged(int index); private: void updateUnitMappings(); void updatePhraseMappings(); ICourse *m_course; QSignalMapper *m_unitSignalMapper; QSignalMapper *m_phraseSignalMapper; }; #endif diff --git a/src/models/unitmodel.cpp b/src/models/unitmodel.cpp index 3fd5292..ed2dbb1 100644 --- a/src/models/unitmodel.cpp +++ b/src/models/unitmodel.cpp @@ -1,181 +1,181 @@ /* * Copyright 2013-2014 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 "unitmodel.h" #include "core/icourse.h" #include "core/unit.h" #include "core/phrase.h" #include "core/language.h" #include #include #include #include "artikulate_debug.h" UnitModel::UnitModel(QObject *parent) : QAbstractListModel(parent) , m_course(nullptr) , m_signalMapper(new QSignalMapper(this)) { connect(m_signalMapper, SIGNAL(mapped(int)), SLOT(emitUnitChanged(int))); } QHash< int, QByteArray > UnitModel::roleNames() const { QHash roles; roles[TitleRole] = "title"; roles[ContainsTrainingData] = "containsTrainingData"; roles[IdRole] = "id"; roles[DataRole] = "dataRole"; return roles; } void UnitModel::setCourse(ICourse *course) { if (m_course == course) { return; } beginResetModel(); if (m_course) { m_course->disconnect(this); } m_course = course; if (m_course) { connect(m_course, &ICourse::unitAboutToBeAdded, this, &UnitModel::onUnitAboutToBeAdded); connect(m_course, &ICourse::unitAdded, this, &UnitModel::onUnitAdded); connect(m_course, &ICourse::unitsAboutToBeRemoved, this, &UnitModel::onUnitsAboutToBeRemoved); connect(m_course, &ICourse::unitsRemoved, this, &UnitModel::onUnitsRemoved); } endResetModel(); emit courseChanged(); } ICourse * UnitModel::course() const { return m_course; } QVariant UnitModel::data(const QModelIndex& index, int role) const { Q_ASSERT(m_course); if (!index.isValid()) { return QVariant(); } - if (index.row() >= m_course->unitList().count()) { + if (index.row() >= m_course->units().count()) { return QVariant(); } - Unit * const unit = m_course->unitList().at(index.row()); + auto unit = m_course->units().at(index.row()); switch(role) { case Qt::DisplayRole: return !unit->title().isEmpty()? QVariant(unit->title()): QVariant(i18nc("@item:inlistbox:", "unknown")); case Qt::ToolTipRole: return QVariant(unit->title()); case TitleRole: return unit->title(); case ContainsTrainingData: foreach (Phrase *phrase, unit->phraseList()) { if (phrase->editState() == Phrase::Completed) { return true; } } return false; case IdRole: return unit->id(); case DataRole: - return QVariant::fromValue(unit); + return QVariant::fromValue(unit.get()); default: return QVariant(); } } int UnitModel::rowCount(const QModelIndex& parent) const { if (!m_course) { return 0; } if (parent.isValid()) { return 0; } - return m_course->unitList().count(); + return m_course->units().count(); } -void UnitModel::onUnitAboutToBeAdded(Unit *unit, int index) +void UnitModel::onUnitAboutToBeAdded(std::shared_ptr unit, int index) { - connect(unit, SIGNAL(titleChanged()), m_signalMapper, SLOT(map())); + connect(unit.get(), SIGNAL(titleChanged()), m_signalMapper, SLOT(map())); //TODO add missing signals beginInsertRows(QModelIndex(), index, index); } void UnitModel::onUnitAdded() { updateMappings(); endInsertRows(); } void UnitModel::onUnitsAboutToBeRemoved(int first, int last) { beginRemoveRows(QModelIndex(), first, last); } void UnitModel::onUnitsRemoved() { endRemoveRows(); } void UnitModel::emitUnitChanged(int row) { emit unitChanged(row); emit dataChanged(index(row, 0), index(row, 0)); } QVariant UnitModel::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", "Unit")); } void UnitModel::updateMappings() { - int units = m_course->unitList().count(); + int units = m_course->units().count(); for (int i = 0; i < units; i++) { - m_signalMapper->setMapping(m_course->unitList().at(i), i); + m_signalMapper->setMapping(m_course->units().at(i).get(), i); } } diff --git a/src/models/unitmodel.h b/src/models/unitmodel.h index ba165fe..bea9911 100644 --- a/src/models/unitmodel.h +++ b/src/models/unitmodel.h @@ -1,71 +1,72 @@ /* * 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 UNITMODEL_H #define UNITMODEL_H +#include #include class ICourse; class Unit; class QSignalMapper; class UnitModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(ICourse *course READ course WRITE setCourse NOTIFY courseChanged) public: enum unitRoles { TitleRole = Qt::UserRole + 1, //!< title of unit IdRole, //!< unique identifier of unit ContainsTrainingData, //!< boolean value indicating whether unit has phrase with native recordings DataRole //!< access to Unit object }; explicit UnitModel(QObject *parent = nullptr); /** * Reimplemented from QAbstractListModel::roleNames() */ virtual QHash roleNames() const override; void setCourse(ICourse *course); ICourse * course() const; virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; Q_SIGNALS: void unitChanged(int index); void courseChanged(); private Q_SLOTS: - void onUnitAboutToBeAdded(Unit *unit, int index); + void onUnitAboutToBeAdded(std::shared_ptr unit, int index); void onUnitAdded(); void onUnitsAboutToBeRemoved(int first, int last); void onUnitsRemoved(); void emitUnitChanged(int row); private: void updateMappings(); ICourse *m_course; QSignalMapper *m_signalMapper; }; #endif // UNITMODEL_H