diff --git a/autotests/unittests/courseresource/test_courseresource.cpp b/autotests/unittests/courseresource/test_courseresource.cpp index d0e9aca..fd245ea 100644 --- a/autotests/unittests/courseresource/test_courseresource.cpp +++ b/autotests/unittests/courseresource/test_courseresource.cpp @@ -1,192 +1,192 @@ /* * 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/courseresource.h" #include "../mocks/languagestub.h" #include #include #include #include #include #include #include #include #include TestCourseResource::TestCourseResource() { } void TestCourseResource::init() { } void TestCourseResource::cleanup() { } void TestCourseResource::loadCourseResource() { std::shared_ptr language(new LanguageStub("de")); auto group = std::static_pointer_cast(language)->addPhonemeGroup("id", "title"); group->addPhoneme("g", "G"); group->addPhoneme("u", "U"); std::vector> languages; languages.push_back(language); ResourceRepositoryStub repository(languages); const QString courseDirectory = "data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; auto course = CourseResource::create(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->units().count(), 1); QCOMPARE(course->units().first()->course(), course.get()); 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->course(), course.get()); - QCOMPARE(unit->phraseList().count(), 3); + QCOMPARE(unit->phrases().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(); + const auto firstPhrase = unit->phrases().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::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; auto course = CourseResource::create(QUrl::fromLocalFile(courseFile), &repository); // begin of test std::unique_ptr unit(new Unit); unit->setId("testunit"); const int initialUnitNumber = course->units().count(); QCOMPARE(initialUnitNumber, 1); QSignalSpy spyAboutToBeAdded(course.get(), SIGNAL(unitAboutToBeAdded(std::shared_ptr, int))); QSignalSpy spyAdded(course.get(), SIGNAL(unitAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); auto sharedUnit = course->addUnit(std::move(unit)); QCOMPARE(course->units().count(), initialUnitNumber + 1); QCOMPARE(spyAboutToBeAdded.count(), 1); QCOMPARE(spyAdded.count(), 1); QCOMPARE(sharedUnit->course(), course.get()); } void TestCourseResource::coursePropertyChanges() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; auto course = CourseResource::create(QUrl::fromLocalFile(courseFile), &repository); // id { const QString value = "newId"; QSignalSpy spy(course.get(), 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.get(), 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.get(), 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.get(), 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.get(), 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.get(), 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/unittests/editablecourseresource/test_editablecourseresource.cpp b/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp index 4d815a4..638bbeb 100644 --- a/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp +++ b/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp @@ -1,364 +1,364 @@ /* * 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/editablecourseresource.h" #include "../mocks/languagestub.h" #include "../mocks/coursestub.h" #include #include #include #include #include #include #include #include #include #include TestEditableCourseResource::TestEditableCourseResource() { } void TestEditableCourseResource::init() { } void TestEditableCourseResource::cleanup() { } void TestEditableCourseResource::loadCourseResource() { std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = EditableCourseResource::create(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->units().count(), 1); QCOMPARE(course->units().first()->course(), course.get()); 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->course(), course.get()); - QCOMPARE(unit->phraseList().count(), 3); + QCOMPARE(unit->phrases().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(); + const auto firstPhrase = unit->phrases().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::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = EditableCourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); // begin of test std::unique_ptr unit(new Unit); unit->setId("testunit"); const int initialUnitNumber = course->units().count(); QCOMPARE(initialUnitNumber, 1); QSignalSpy spyAboutToBeAdded(course.get(), SIGNAL(unitAboutToBeAdded(std::shared_ptr, int))); QSignalSpy spyAdded(course.get(), SIGNAL(unitAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); auto sharedUnit = course->addUnit(std::move(unit)); QCOMPARE(course->units().count(), initialUnitNumber + 1); QCOMPARE(spyAboutToBeAdded.count(), 1); QCOMPARE(spyAdded.count(), 1); QCOMPARE(sharedUnit->course(), course.get()); } void TestEditableCourseResource::coursePropertyChanges() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = CourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); // id { const QString value = "newId"; QSignalSpy spy(course.get(), 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.get(), 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.get(), 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.get(), 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.get(), 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.get(), SIGNAL(languageChanged())); QCOMPARE(spy.count(), 0); course->setLanguage(testLanguage); QCOMPARE(course->language(), testLanguage); QCOMPARE(spy.count(), 1); } } void TestEditableCourseResource::fileLoadSaveCompleteness() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = EditableCourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); // note: this only works, since the resource manager not checks uniqueness of course ids! auto loadedCourse = EditableCourseResource::create(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->units().count() == loadedCourse->units().count()); 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()); + QVERIFY(testUnit->phrases().count() == compareUnit->phrases().count()); - Phrase *testPhrase = testUnit->phraseList().constFirst(); - Phrase *comparePhrase = new Phrase(this); + std::shared_ptr testPhrase = testUnit->phrases().constFirst(); + std::shared_ptr comparePhrase = Phrase::create(); // note that this actually means that we DO NOT respect phrase orders by list order - for (Phrase *phrase : compareUnit->phraseList()) { + for (const auto &phrase : compareUnit->phrases()) { 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()); } void TestEditableCourseResource::modifiedStatus() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = EditableCourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); { // initial file loading QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); } { // set ID QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); loadedCourse->setId("ASDF"); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } { // set title QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); loadedCourse->setTitle("ASDF"); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } { // set i18n title QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); loadedCourse->setI18nTitle("ASDF"); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } { // set description QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); loadedCourse->setDescription("ASDF"); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } { // set language QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); std::shared_ptr newLanguage(new LanguageStub("en")); loadedCourse->setLanguage(newLanguage); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } { // add unit QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); std::unique_ptr unit(new Unit); unit->setId("testunit"); loadedCourse->addUnit(std::move(unit)); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } } void TestEditableCourseResource::skeletonUpdate() { std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = EditableCourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); QCOMPARE(course->units().count(), 1); // create skeleton stub - auto importPhrase = new Phrase; + auto importPhrase = Phrase::create(); importPhrase->setId("importPhraseId"); importPhrase->setText("phraseText"); - importPhrase->setType(Phrase::Sentence); + importPhrase->setType(IPhrase::Type::Sentence); auto importUnit = std::shared_ptr(new Unit); importUnit->setId("importId"); importUnit->addPhrase(importPhrase); auto skeleton = CourseStub::create(language, {importUnit}); // test import course->updateFrom(skeleton); QCOMPARE(course->units().count(), 2); { std::shared_ptr importedUnit; for (auto unit : course->units()) { if (unit->foreignId() == importUnit->id()) { importedUnit = unit; break; } } QVERIFY(importedUnit != nullptr); QCOMPARE(importedUnit->foreignId(), importUnit->id()); QCOMPARE(importedUnit->id(), importUnit->id()); QCOMPARE(importedUnit->title(), importUnit->title()); - QCOMPARE(importedUnit->phraseList().count(), 1); - auto importedPhrase = importedUnit->phraseList().first(); + QCOMPARE(importedUnit->phrases().count(), 1); + auto importedPhrase = importedUnit->phrases().first(); QCOMPARE(importedPhrase->id(), importPhrase->id()); QCOMPARE(importedPhrase->foreignId(), importPhrase->id()); QCOMPARE(importedPhrase->text(), importPhrase->text()); QCOMPARE(importedPhrase->type(), importPhrase->type()); } // test that re-import does not change course course->updateFrom(skeleton); QCOMPARE(course->units().count(), 2); } QTEST_GUILESS_MAIN(TestEditableCourseResource) diff --git a/autotests/unittests/editorsession/test_editorsession.cpp b/autotests/unittests/editorsession/test_editorsession.cpp index ac836a6..584ed96 100644 --- a/autotests/unittests/editorsession/test_editorsession.cpp +++ b/autotests/unittests/editorsession/test_editorsession.cpp @@ -1,231 +1,231 @@ /* * 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 "../mocks/editablecoursestub.h" #include "../mocks/languagestub.h" #include #include void TestEditorSession::init() { // no initialization of test case } void TestEditorSession::cleanup() { // no cleanup after test run } void TestEditorSession::createEditorSession() { auto languageGerman = std::make_shared("de"); auto languageEnglish = std::make_shared("en"); std::shared_ptr course(new EditableCourseStub(languageGerman, QVector>())); course->setLanguage(languageGerman); auto skeleton = SkeletonResource::create(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("de"); auto languageEnglish = std::make_shared("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("de"); auto languageEnglish = std::make_shared("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"); auto skeletonA = SkeletonResource::create(QUrl(), nullptr); skeletonA->setId("testskeletonA"); auto skeletonB = SkeletonResource::create(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()); } void TestEditorSession::iterateCourse() { // language auto language = std::make_shared("de"); // course 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; + std::shared_ptr phraseA1 = Phrase::create(); + std::shared_ptr phraseA2 = Phrase::create(); + std::shared_ptr phraseB1 = Phrase::create(); + std::shared_ptr phraseB2 = Phrase::create(); // note: phrases without soundfiles are skipped in session generation phraseA1->setId("A1"); phraseA2->setId("A2"); phraseB1->setId("B1"); phraseB2->setId("B2"); phraseA1->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseA2->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseB1->setSound(QUrl::fromLocalFile("/tmp/b1.ogg")); phraseB2->setSound(QUrl::fromLocalFile("/tmp/b2.ogg")); unitA->addPhrase(phraseA1); unitA->addPhrase(phraseA2); unitB->addPhrase(phraseB1); unitB->addPhrase(phraseB2); auto course = std::make_shared(language, QVector>({unitA, unitB})); EditableRepositoryStub repository{ {language}, // languages {}, // skeletons {course} // courses }; EditorSession session; session.setRepository(&repository); session.setCourse(course.get()); // session assumed to initialize with first units's first phrase QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA1); QVERIFY(course.get() == session.course()); // test direct unit setters session.setUnit(unitA.get()); QCOMPARE(session.activeUnit(), unitA.get()); session.setUnit(unitB.get()); QCOMPARE(session.activeUnit(), unitB.get()); // test direct phrase setters session.setPhrase(phraseA1); QCOMPARE(session.activePhrase(), phraseA1); QCOMPARE(session.activeUnit(), unitA.get()); session.setPhrase(phraseB1); QCOMPARE(session.activePhrase(), phraseB1); QCOMPARE(session.activeUnit(), unitB.get()); // test phrase forward iterators session.setPhrase(phraseA1); QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase()->id(), phraseA1->id()); QVERIFY(session.hasNextPhrase()); session.switchToNextPhrase(); QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase()->id(), phraseA2->id()); session.switchToNextPhrase(); QCOMPARE(session.activePhrase(), phraseB1); session.switchToNextPhrase(); QCOMPARE(session.activePhrase(), phraseB2); QVERIFY(!session.hasNextPhrase()); // at the end, do not iterate further session.switchToNextPhrase(); QCOMPARE(session.activePhrase(), phraseB2); // test phrase backward iterators QVERIFY(session.hasPreviousPhrase()); session.switchToPreviousPhrase(); QCOMPARE(session.activePhrase(), phraseB1); session.switchToPreviousPhrase(); QCOMPARE(session.activePhrase()->id(), phraseA2->id()); session.switchToPreviousPhrase(); QCOMPARE(session.activePhrase()->id(), phraseA1->id()); QVERIFY(!session.hasPreviousPhrase()); // at the end, do not iterate further session.switchToPreviousPhrase(); QCOMPARE(session.activePhrase()->id(), phraseA1->id()); } QTEST_GUILESS_MAIN(TestEditorSession) diff --git a/autotests/unittests/skeletonresource/test_skeletonresource.cpp b/autotests/unittests/skeletonresource/test_skeletonresource.cpp index af1bf6c..10f69a1 100644 --- a/autotests/unittests/skeletonresource/test_skeletonresource.cpp +++ b/autotests/unittests/skeletonresource/test_skeletonresource.cpp @@ -1,204 +1,204 @@ /* * 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/skeletonresource.h" #include "../mocks/languagestub.h" #include #include #include #include #include #include #include #include #include TestSkeletonResource::TestSkeletonResource() { } void TestSkeletonResource::init() { } void TestSkeletonResource::cleanup() { } void TestSkeletonResource::loadSkeletonResource() { std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; auto skeleton = SkeletonResource::create(QUrl::fromLocalFile(courseFile), &repository); QCOMPARE(skeleton->file().toLocalFile(), courseFile); QCOMPARE(skeleton->id(), "skeleton-testdata"); QCOMPARE(skeleton->foreignId(), "skeleton-testdata"); // always same as ID QCOMPARE(skeleton->title(), "Artikulate Test Course Title"); QCOMPARE(skeleton->description(), "Artikulate Test Course Description"); QVERIFY(skeleton->language() == nullptr); // a skeleton must not have a language QCOMPARE(skeleton->units().count(), 2); const auto unit = skeleton->units().first(); QVERIFY(unit != nullptr); QCOMPARE(unit->id(), "{11111111-b885-4833-97ff-27cb1ca2f543}"); QCOMPARE(unit->title(), QStringLiteral("Numbers")); - QCOMPARE(unit->phraseList().count(), 2); + QCOMPARE(unit->phrases().count(), 2); QVERIFY(unit->course() != nullptr); QCOMPARE(unit->course(), skeleton.get()); // 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(); + const auto firstPhrase = unit->phrases().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); + const auto secondPhrase = unit->phrases().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 LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; auto skeleton = SkeletonResource::create(QUrl::fromLocalFile(courseFile), &repository); // begin of test std::unique_ptr unit(new Unit); unit->setId("testunit"); const int initialUnitNumber = skeleton->units().count(); QCOMPARE(initialUnitNumber, 2); QSignalSpy spyAboutToBeAdded(skeleton.get(), SIGNAL(unitAboutToBeAdded(std::shared_ptr, int))); QSignalSpy spyAdded(skeleton.get(), SIGNAL(unitAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); auto sharedUnit = skeleton->addUnit(std::move(unit)); QCOMPARE(skeleton->units().count(), initialUnitNumber + 1); QCOMPARE(spyAboutToBeAdded.count(), 1); QCOMPARE(spyAdded.count(), 1); QCOMPARE(sharedUnit->course(), skeleton.get()); } void TestSkeletonResource::coursePropertyChanges() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; auto skeleton = SkeletonResource::create(QUrl::fromLocalFile(courseFile), &repository); // id { const QString value = "newId"; QSignalSpy spy(skeleton.get(), SIGNAL(idChanged())); QCOMPARE(spy.count(), 0); skeleton->setId(value); QCOMPARE(skeleton->id(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newTitle"; QSignalSpy spy(skeleton.get(), SIGNAL(titleChanged())); QCOMPARE(spy.count(), 0); skeleton->setTitle(value); QCOMPARE(skeleton->title(), value); QCOMPARE(spy.count(), 1); } // description { const QString value = "newDescription"; QSignalSpy spy(skeleton.get(), SIGNAL(descriptionChanged())); QCOMPARE(spy.count(), 0); skeleton->setDescription(value); QCOMPARE(skeleton->description(), value); QCOMPARE(spy.count(), 1); } } void TestSkeletonResource::fileLoadSaveCompleteness() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; auto skeleton = SkeletonResource::create(QUrl::fromLocalFile(courseFile), &repository); QTemporaryFile outputFile; outputFile.open(); skeleton->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); // note: this only works, since the resource manager not checks uniqueness of course ids! auto loadedSkeleton = SkeletonResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); // test that we actually call the different files QVERIFY(skeleton->file().toLocalFile() != loadedSkeleton->file().toLocalFile()); QCOMPARE(loadedSkeleton->id(), skeleton->id()); QCOMPARE(loadedSkeleton->foreignId(), skeleton->foreignId()); QCOMPARE(loadedSkeleton->title(), skeleton->title()); QCOMPARE(loadedSkeleton->description(), skeleton->description()); QCOMPARE(loadedSkeleton->language(), skeleton->language()); QCOMPARE(loadedSkeleton->units().count(), skeleton->units().count()); auto testUnit = skeleton->units().constFirst(); auto compareUnit = loadedSkeleton->units().constFirst(); QCOMPARE(testUnit->id(), compareUnit->id()); QCOMPARE(testUnit->foreignId(), compareUnit->foreignId()); QCOMPARE(testUnit->title(), compareUnit->title()); - QCOMPARE(testUnit->phraseList().count(), compareUnit->phraseList().count()); + QCOMPARE(testUnit->phrases().count(), compareUnit->phrases().count()); - Phrase *testPhrase = testUnit->phraseList().constFirst(); - Phrase *comparePhrase = new Phrase(this); + std::shared_ptr testPhrase = testUnit->phrases().constFirst(); + std::shared_ptr comparePhrase = Phrase::create(); // note that this actually means that we DO NOT respect phrase orders by list order - for (Phrase *phrase : compareUnit->phraseList()) { + for (const auto &phrase : compareUnit->phrases()) { 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/unittests/trainingsession/test_trainingsession.cpp b/autotests/unittests/trainingsession/test_trainingsession.cpp index 2411013..05d5f38 100644 --- a/autotests/unittests/trainingsession/test_trainingsession.cpp +++ b/autotests/unittests/trainingsession/test_trainingsession.cpp @@ -1,207 +1,207 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_trainingsession.h" #include "src/core/trainingsession.h" #include "src/core/icourse.h" #include "src/core/language.h" #include "src/core/unit.h" #include "src/core/trainingaction.h" #include "../mocks/coursestub.h" #include "../mocks/languagestub.h" #include "liblearnerprofile/src/profilemanager.h" #include #include // assumption: during a training session the units and phrases of a course do not change // any change of such a course shall result in a reload of a training session void TestTrainingSession::init() { // no initialization of test case } void TestTrainingSession::cleanup() { // no cleanup after test run } void TestTrainingSession::createTrainingSessionWithoutUnits() { auto language = std::make_shared("de"); CourseStub course(language, QVector>()); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::createTrainingSessionWithEmptySounds() { auto language = std::make_shared("de"); 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; + std::shared_ptr phraseA1 = Phrase::create(); + std::shared_ptr phraseA2 = Phrase::create(); + std::shared_ptr phraseB1 = Phrase::create(); + std::shared_ptr phraseB2 = Phrase::create(); // note: phrases without soundfiles are skipped in session generation phraseA1->setId("A1"); phraseA2->setId("A2"); phraseB1->setId("B1"); phraseB2->setId("B2"); phraseA1->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); unitA->addPhrase(phraseA1); unitA->addPhrase(phraseA2); unitB->addPhrase(phraseB1); unitB->addPhrase(phraseB2); CourseStub course(language, QVector>({unitA, unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); // test number of actions auto actions = session.trainingActions(); QCOMPARE(actions.count(), 1); QCOMPARE(actions.at(0)->actions().count(), 1); } void TestTrainingSession::createTrainingSessionWithEmptyUnits() { auto language = std::make_shared("de"); 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("de"); std::shared_ptr unit(new Unit); - Phrase *firstPhrase = new Phrase(); - Phrase *secondPhrase = new Phrase(); + std::shared_ptr firstPhrase = Phrase::create(); + std::shared_ptr secondPhrase = Phrase::create(); unit->addPhrase(firstPhrase); unit->addPhrase(secondPhrase); CourseStub course(language, QVector>({unit})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::iterateCourse() { auto language = std::make_shared("de"); 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; + std::shared_ptr phraseA1 = Phrase::create(); + std::shared_ptr phraseA2 = Phrase::create(); + std::shared_ptr phraseB1 = Phrase::create(); + std::shared_ptr phraseB2 = Phrase::create(); // note: phrases without soundfiles are skipped in session generation phraseA1->setId("A1"); phraseA2->setId("A2"); phraseB1->setId("B1"); phraseB2->setId("B2"); phraseA1->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseA2->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseB1->setSound(QUrl::fromLocalFile("/tmp/b1.ogg")); phraseB2->setSound(QUrl::fromLocalFile("/tmp/b2.ogg")); unitA->addPhrase(phraseA1); unitA->addPhrase(phraseA2); unitB->addPhrase(phraseB1); unitB->addPhrase(phraseB2); CourseStub course(language, QVector>({unitA, unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); // session assumed to initialize with first units's first phrase QCOMPARE(session.activeUnit(), unitA.get()); - QCOMPARE(session.activePhrase(), phraseA1); + QCOMPARE(session.activePhrase(), phraseA1.get()); QVERIFY(&course == session.course()); // test direct unit setters session.setUnit(unitA.get()); QCOMPARE(session.activeUnit(), unitA.get()); session.setUnit(unitB.get()); QCOMPARE(session.activeUnit(), unitB.get()); // test direct phrase setters - session.setPhrase(phraseA1); - QCOMPARE(session.activePhrase(), phraseA1); + session.setPhrase(phraseA1.get()); + QCOMPARE(session.activePhrase(), phraseA1.get()); QCOMPARE(session.activeUnit(), unitA.get()); - session.setPhrase(phraseB1); - QCOMPARE(session.activePhrase(), phraseB1); + session.setPhrase(phraseB1.get()); + QCOMPARE(session.activePhrase(), phraseB1.get()); QCOMPARE(session.activeUnit(), unitB.get()); // test number of actions auto actions = session.trainingActions(); QCOMPARE(actions.count(), 2); QCOMPARE(actions.at(0)->actions().count(), 2); QCOMPARE(actions.at(1)->actions().count(), 2); // test phrase iterators: accept iterator - session.setPhrase(phraseA1); + session.setPhrase(phraseA1.get()); QCOMPARE(session.activeUnit(), unitA.get()); - QCOMPARE(session.activePhrase(), phraseA1); + QCOMPARE(session.activePhrase(), phraseA1.get()); QVERIFY(session.hasNext()); session.accept(); QCOMPARE(session.activeUnit(), unitA.get()); - QCOMPARE(session.activePhrase(), phraseA2); + QCOMPARE(session.activePhrase(), phraseA2.get()); session.accept(); - QCOMPARE(session.activePhrase(), phraseB1); + QCOMPARE(session.activePhrase(), phraseB1.get()); session.accept(); - QCOMPARE(session.activePhrase(), phraseB2); + QCOMPARE(session.activePhrase(), phraseB2.get()); QVERIFY(!session.hasNext()); // test phrase iterators: skip iterator - session.setPhrase(phraseA1); + session.setPhrase(phraseA1.get()); QCOMPARE(session.activeUnit(), unitA.get()); - QCOMPARE(session.activePhrase(), phraseA1); + QCOMPARE(session.activePhrase(), phraseA1.get()); QVERIFY(!session.hasPrevious()); QVERIFY(session.hasNext()); session.skip(); QCOMPARE(session.activeUnit(), unitA.get()); - QCOMPARE(session.activePhrase(), phraseA2); + QCOMPARE(session.activePhrase(), phraseA2.get()); session.skip(); - QCOMPARE(session.activePhrase(), phraseB1); + QCOMPARE(session.activePhrase(), phraseB1.get()); session.skip(); - QCOMPARE(session.activePhrase(), phraseB2); + QCOMPARE(session.activePhrase(), phraseB2.get()); QVERIFY(session.hasPrevious()); QVERIFY(!session.hasNext()); // test completed signal QSignalSpy spy(&session, SIGNAL(completed())); - session.setPhrase(phraseB1); + session.setPhrase(phraseB1.get()); session.accept(); QCOMPARE(spy.count(), 0); session.accept(); QCOMPARE(spy.count(), 1); } QTEST_GUILESS_MAIN(TestTrainingSession) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 66029b7..74f105d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,183 +1,185 @@ ### # Copyright 2013-2019 Andreas Cord-Landwehr # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ### ecm_setup_version(0.99.90 VARIABLE_PREFIX ARTIKULATE VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/ArtikulateConfigVersion.cmake" ) ecm_optional_add_subdirectory(qml) # set include directories include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${artikulate_SOURCE_DIR} ) # set the source code files from which Artikulate is compiled set(artikulateCore_SRCS application.cpp artikulate_debug.cpp core/icourse.h core/ieditablecourse.h + core/ieditablephrase.h core/ieditablerepository.h core/ilanguage.h + core/iphrase.h core/iresourcerepository.h core/drawertrainingactions.cpp core/resourcerepository.cpp core/contributorrepository.cpp core/language.cpp core/phrase.cpp core/phoneme.cpp core/phonemegroup.cpp core/unit.cpp core/editorsession.cpp core/trainingaction.cpp core/trainingactionicon.cpp core/trainingsession.cpp core/resources/courseparser.cpp core/resources/courseresource.cpp core/resources/editablecourseresource.cpp core/resources/skeletonresource.cpp core/player.cpp core/recorder.cpp models/coursemodel.cpp models/coursefiltermodel.cpp models/languagemodel.cpp models/languageresourcemodel.cpp # models/learningprogressmodel.cpp //TODO must be adapted to new trainingsession models/unitmodel.cpp models/unitfiltermodel.cpp models/phrasemodel.cpp models/phraselistmodel.cpp models/phrasefiltermodel.cpp models/phonememodel.cpp models/phonemegroupmodel.cpp models/phonemeunitmodel.cpp models/profilemodel.cpp models/skeletonmodel.cpp qmlcontrols/iconitem.cpp qmlcontrols/imagetexturescache.cpp qmlcontrols/managedtexturenode.cpp ) kconfig_add_kcfg_files (artikulateCore_SRCS settings.kcfgc) add_library(artikulatecore SHARED ${artikulateCore_SRCS}) generate_export_header(artikulatecore BASE_NAME artikulatecore) target_link_libraries(artikulatecore LINK_PUBLIC artikulatelearnerprofile artikulatesound Qt5::XmlPatterns Qt5::Qml Qt5::Quick KF5::Archive KF5::ConfigGui ) # internal library without any API or ABI guarantee set(GENERIC_LIB_VERSION "0") set(GENERIC_LIB_SOVERSION "0") set_target_properties( artikulatecore PROPERTIES VERSION ${GENERIC_LIB_VERSION} SOVERSION ${GENERIC_LIB_SOVERSION} ) install( TARGETS artikulatecore LIBRARY NAMELINK_SKIP DESTINATION ${INSTALL_TARGETS_DEFAULT_ARGS} ) install(FILES artikulate.knsrc DESTINATION ${CONFIG_INSTALL_DIR}) # set the source code files from which Artikulate is compiled set(artikulate_SRCS main.cpp mainwindow.cpp artikulate_debug.cpp ui/sounddevicedialogpage.cpp ui/appearencedialogpage.cpp ui/resourcesdialogpage.cpp ) ki18n_wrap_ui (artikulate_SRCS ui/resourcesdialogpage.ui ui/sounddevicedialogpage.ui ui/appearencedialogpage.ui ) qt5_add_resources(artikulate_SRCS resources.qrc) qt5_add_resources(artikulate_SRCS ../data/languages.qrc) kconfig_add_kcfg_files (artikulate_SRCS settings.kcfgc) set(artikulate_editor_SRCS main_editor.cpp mainwindow_editor.cpp artikulate_debug.cpp ui/sounddevicedialogpage.cpp ui/appearencedialogpage.cpp ui/resourcesdialogpage.cpp ui/exportghnsdialog.cpp ) ki18n_wrap_ui(artikulate_editor_SRCS ui/appearencedialogpage.ui ui/exportghnsdialog.ui ui/resourcesdialogpage.ui ui/sounddevicedialogpage.ui ) qt5_add_resources(artikulate_editor_SRCS resources.qrc) qt5_add_resources(artikulate_editor_SRCS ../data/languages.qrc) kconfig_add_kcfg_files (artikulate_editor_SRCS settings.kcfgc) # executables add_executable(artikulate ${artikulate_SRCS}) target_link_libraries(artikulate LINK_PUBLIC artikulatelearnerprofile artikulatesound artikulatecore Qt5::Qml Qt5::Quick KF5::Crash KF5::NewStuff KF5::XmlGui ) qt5_add_resources(artikulate_editor_SRCS editor.qrc) add_executable(artikulate_editor ${artikulate_editor_SRCS}) target_link_libraries(artikulate_editor LINK_PUBLIC artikulatesound artikulatecore Qt5::Qml Qt5::Quick Qt5::QuickWidgets KF5::Crash KF5::NewStuff KF5::XmlGui ) install(FILES artikulate.kcfg DESTINATION ${KCFG_INSTALL_DIR}) install(TARGETS artikulate ${INSTALL_TARGETS_DEFAULT_ARGS}) install(TARGETS artikulate_editor ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/application.cpp b/src/application.cpp index b0ec6b4..38c2e81 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -1,153 +1,157 @@ /* * 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 "application.h" #include "core/iresourcerepository.h" #include "core/ieditablerepository.h" #include "core/ilanguage.h" #include "core/language.h" #include "core/contributorrepository.h" #include "core/drawertrainingactions.h" #include "core/trainingaction.h" #include "core/editorsession.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/phrase.h" #include "core/player.h" #include "core/recorder.h" #include "core/trainingsession.h" #include "core/unit.h" #include "core/resources/editablecourseresource.h" #include "core/resources/skeletonresource.h" #include "models/coursefiltermodel.h" #include "models/coursemodel.h" #include "models/languagemodel.h" #include "models/languageresourcemodel.h" #include "models/learningprogressmodel.h" #include "models/phonemegroupmodel.h" #include "models/phonememodel.h" #include "models/phonemeunitmodel.h" #include "models/phrasefiltermodel.h" #include "models/phraselistmodel.h" #include "models/phrasemodel.h" #include "models/profilemodel.h" #include "models/skeletonmodel.h" #include "models/unitfiltermodel.h" #include "models/unitmodel.h" #include "qmlcontrols/iconitem.h" #include "liblearnerprofile/src/learner.h" #include "liblearnerprofile/src/profilemanager.h" #include "liblearnerprofile/src/learninggoal.h" #include "liblearnerprofile/src/models/learninggoalmodel.h" #include #include #include #include Application::Application(int& argc, char** argv) : QApplication(argc, argv) { registerQmlTypes(); } IResourceRepository * Application::resourceRepository() const { return m_resourceRepository; } IEditableRepository * Application::editableRepository() const { return qobject_cast(m_resourceRepository); } void Application::installResourceRepository(IResourceRepository *resourceRepository) { m_resourceRepository = resourceRepository; } void Application::registerQmlTypes() { qmlRegisterUncreatableType( "artikulate", 1, 0, "TrainingSession", QStringLiteral("TrainingSession is unique object provided by the backend")); qmlRegisterUncreatableType( "artikulate", 1, 0, "EditorSession", QStringLiteral("EditorSession is unique object provided by the backend")); qmlRegisterUncreatableType( "artikulate", 1, 0, "ContributorRepository", QStringLiteral("ContributorRepository is unique object provided by the backend")); qmlRegisterUncreatableType( "artikulate", 1, 0, "ProfileManager", QStringLiteral("ProfileManager is unique object provided by the backend")); qmlRegisterUncreatableType( "artikulate", 1, 0, "EditableCourseResource", QStringLiteral("EditableCourseResource objects are backend objects")); qmlRegisterUncreatableType( "artikulate", 1, 0, "SkeletonResource", QStringLiteral("SkeletonResource objects are backend objects")); + qmlRegisterUncreatableType( + "artikulate", 1, 0, + "Phrase", + QStringLiteral("Phrase objects are backend objects")); // interfaces qmlRegisterInterface("IResourceRepository"); qmlRegisterInterface("IEditableRepository"); qmlRegisterInterface("ICourse"); qmlRegisterInterface("IEditableCourse"); qmlRegisterInterface("ILanguage"); + qmlRegisterInterface("IPhrase"); // concrete instantiable types qmlRegisterType("artikulate", 1, 0, "Learner"); qmlRegisterType("artikulate", 1, 0, "LearningGoal"); qmlRegisterType("artikulate", 1, 0, "Unit"); qmlRegisterType("artikulate", 1, 0, "Language"); - qmlRegisterType("artikulate", 1, 0, "Phrase"); qmlRegisterType("artikulate", 1, 0, "Phoneme"); qmlRegisterType("artikulate", 1, 0, "PhonemeGroup"); qmlRegisterType("artikulate", 1, 0, "Player"); qmlRegisterType("artikulate", 1, 0, "Recorder"); qmlRegisterType("artikulate", 1, 0, "Icon"); qmlRegisterType("artikulate", 1, 0, "DrawerTrainingActions"); qmlRegisterType("artikulate", 1, 0, "TrainingAction"); // models qmlRegisterType("artikulate", 1, 0, "CourseModel"); qmlRegisterType("artikulate", 1, 0, "CourseFilterModel"); qmlRegisterType("artikulate", 1, 0, "LanguageModel"); qmlRegisterType("artikulate", 1, 0, "LanguageResourceModel"); // qmlRegisterType("artikulate", 1, 0, "LearningProgressModel");//TODO must be ported to new trainingsession qmlRegisterType("artikulate", 1, 0, "UnitModel"); qmlRegisterType("artikulate", 1, 0, "UnitFilterModel"); qmlRegisterType("artikulate", 1, 0, "PhraseModel"); qmlRegisterType("artikulate", 1, 0, "PhraseListModel"); qmlRegisterType("artikulate", 1, 0, "PhraseFilterModel"); qmlRegisterType("artikulate", 1, 0, "PhonemeModel"); qmlRegisterType("artikulate", 1, 0, "PhonemeGroupModel"); qmlRegisterType("artikulate", 1, 0, "PhonemeUnitModel"); qmlRegisterType("artikulate", 1, 0, "ProfileModel"); qmlRegisterType("artikulate", 1, 0, "SkeletonModel"); qmlRegisterType("artikulate", 1, 0, "LearningGoalModel"); } diff --git a/src/core/editorsession.cpp b/src/core/editorsession.cpp index 70b4457..15eeee4 100644 --- a/src/core/editorsession.cpp +++ b/src/core/editorsession.cpp @@ -1,338 +1,333 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "editorsession.h" #include "core/language.h" #include "core/resources/editablecourseresource.h" #include "core/resources/skeletonresource.h" #include "core/unit.h" #include "core/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(); } ILanguage * 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(ILanguage *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(); if (course != nullptr) { auto units = course->units(); if (!units.isEmpty()) { setUnit(units.constFirst().get()); return; } } } Unit * EditorSession::unit() const { return m_unit; } Unit * EditorSession::activeUnit() const { return m_unit; } void EditorSession::setUnit(Unit *unit) { if (m_unit == unit) { return; } m_unit = unit; - if (!m_unit->phraseList().isEmpty()) { - setPhrase(m_unit->phraseList().first()); + if (!m_unit->phrases().isEmpty()) { + setPhrase(m_unit->phrases().first()); } else { setPhrase(nullptr); } emit unitChanged(); } -void EditorSession::setPhrase(Phrase *phrase) +void EditorSession::setPhrase(std::shared_ptr phrase) { if (m_phrase == phrase) { return; } if (phrase) { setUnit(phrase->unit()); } m_phrase = phrase; emit phraseChanged(); } -Phrase * EditorSession::activePhrase() const +std::shared_ptr EditorSession::activePhrase() const { return m_phrase; } -Phrase * EditorSession::phrase() const -{ - return m_phrase; -} - -Phrase * EditorSession::previousPhrase() const +std::shared_ptr EditorSession::previousPhrase() const { if (!m_phrase) { return nullptr; } - const int index = m_phrase->unit()->phraseList().indexOf(m_phrase); + const int index = m_phrase->unit()->phrases().indexOf(m_phrase); if (index > 0) { - return m_phrase->unit()->phraseList().at(index - 1); + return m_phrase->unit()->phrases().at(index - 1); } else { auto unit = m_phrase->unit(); int uIndex{ -1 }; for (int i = 0; i < unit->course()->units().size(); ++i) { auto testUnit = unit->course()->units().at(i); if (testUnit.get() == unit) { uIndex = i; break; } } if (uIndex > 0) { - return unit->course()->units().at(uIndex - 1)->phraseList().last(); + return unit->course()->units().at(uIndex - 1)->phrases().last(); } } return m_phrase; } -Phrase * EditorSession::nextPhrase() const +std::shared_ptr 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); + const int index = m_phrase->unit()->phrases().indexOf(m_phrase); + if (index < m_phrase->unit()->phrases().length() - 1) { + return m_phrase->unit()->phrases().at(index + 1); } else { Unit *unit = m_phrase->unit(); int uIndex{ -1 }; for (int i = 0; i < unit->course()->units().size(); ++i) { auto testUnit = unit->course()->units().at(i); if (testUnit.get() == unit) { uIndex = i; break; } } if (uIndex < unit->course()->units().length() - 1) { auto nextUnit = unit->course()->units().at(uIndex + 1); - if (nextUnit->phraseList().isEmpty()) { + if (nextUnit->phrases().isEmpty()) { return nullptr; } - return nextUnit->phraseList().constFirst(); + return nextUnit->phrases().constFirst(); } } return m_phrase; } void EditorSession::switchToPreviousPhrase() { if (hasPreviousPhrase()) { setPhrase(previousPhrase()); } } void EditorSession::switchToNextPhrase() { if (hasNextPhrase()) { setPhrase(nextPhrase()); } } bool EditorSession::hasPreviousPhrase() const { if (!m_unit || !m_phrase) { return false; } Q_ASSERT(m_unit->course() != nullptr); - const int phraseIndex = m_phrase->unit()->phraseList().indexOf(m_phrase); + const int phraseIndex = m_phrase->unit()->phrases().indexOf(m_phrase); int unitIndex = -1; for (int i = 0; i < m_unit->course()->units().size(); ++i) { if (m_unit->course()->units().at(i).get() == m_unit) { unitIndex = i; break; } } if (unitIndex > 0 || phraseIndex > 0) { return true; } return false; } bool EditorSession::hasNextPhrase() const { if (!m_unit || !m_phrase) { return false; } Q_ASSERT(m_unit->course() != nullptr); - const int phraseIndex = m_phrase->unit()->phraseList().indexOf(m_phrase); + const int phraseIndex = m_phrase->unit()->phrases().indexOf(m_phrase); int unitIndex = -1; for (int i = 0; i < m_unit->course()->units().size(); ++i) { if (m_unit->course()->units().at(i).get() == m_unit) { unitIndex = i; break; } } if ((unitIndex >= 0 && unitIndex < m_course->units().size() - 1) - || (phraseIndex >= 0 && phraseIndex < m_unit->phraseList().size() - 1)) { + || (phraseIndex >= 0 && phraseIndex < m_unit->phrases().size() - 1)) { return true; } return false; } void EditorSession::updateCourseFromSkeleton() { if (!m_course) { qCritical() << "Not updating course from skeleton, no one set."; return; } m_repository->updateCourseFromSkeleton(m_course->self()); } diff --git a/src/core/editorsession.h b/src/core/editorsession.h index 722c8e1..6f487b4 100644 --- a/src/core/editorsession.h +++ b/src/core/editorsession.h @@ -1,133 +1,133 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef EDITORSESSION_H #define EDITORSESSION_H #include "artikulatecore_export.h" #include "phrase.h" class ILanguage; class IEditableCourse; class Unit; +class IPhrase; class SkeletonResource; class IEditableRepository; /** * \class EditorSession * * An object of this class is used to set the current state of the editor. By this, we put all logic * how language, skeleton and course fit to each other into this class. The main concept is that * we have to fundamentally different workflows that both are modeled in this class: * * 1. Skeleton based workflow * - a skeleton is selected * - every language is available, since eventually the course should be available in every language * - for every language, there is at most one course (there is none only in case it is not created yet) * - adding new units or phrases is only possible in the skeleton course * - every course can update/sync with the skeleton * * 2. Course based workflow * - there is no skeleton from which the course is derived * - the base is a language that is selected first * - for a language there can be none to arbitrarily many courses * * The main switch is \c EditorSession::setSkeletonMode(bool) */ class ARTIKULATECORE_EXPORT EditorSession : public QObject { Q_OBJECT Q_PROPERTY(bool skeletonMode READ skeletonMode NOTIFY skeletonModeChanged) Q_PROPERTY(bool editSkeleton READ isEditSkeleton WRITE setEditSkeleton NOTIFY editSkeletonChanged) Q_PROPERTY(IEditableCourse *skeleton READ skeleton WRITE setSkeleton NOTIFY skeletonChanged) Q_PROPERTY(IEditableCourse *course READ course WRITE setCourse NOTIFY courseChanged) // editor elements depending on curently selected mode, skeleton and course /** * @brief the displayed course (skeleton or course) depending on the user selection */ Q_PROPERTY(IEditableCourse *displayedCourse READ displayedCourse NOTIFY displayedCourseChanged) Q_PROPERTY(ILanguage *language READ language NOTIFY languageChanged) Q_PROPERTY(Unit *unit READ unit WRITE setUnit NOTIFY unitChanged) - Q_PROPERTY(Phrase *phrase READ phrase WRITE setPhrase NOTIFY phraseChanged) +// Q_PROPERTY(IPhrase *phrase READ phrase WRITE setPhrase NOTIFY phraseChanged) //FIXME Q_PROPERTY(bool hasNextPhrase READ hasNextPhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasPreviousPhrase READ hasPreviousPhrase NOTIFY phraseChanged) public: explicit EditorSession(QObject *parent = nullptr); void setRepository(IEditableRepository *repository); bool skeletonMode() const; void setEditSkeleton(bool enabled=true); bool isEditSkeleton() const; IEditableCourse * skeleton() const; void setSkeleton(IEditableCourse *skeleton); ILanguage * language() const; IEditableCourse * course() const; void setCourse(IEditableCourse *course); /** * @brief Open course resource by specifying the language * @param language the target language */ Q_INVOKABLE void setCourseByLanguage(ILanguage *language); IEditableCourse * displayedCourse() const; Q_DECL_DEPRECATED Unit * unit() const; Unit * activeUnit() const; void setUnit(Unit *unit); - Q_DECL_DEPRECATED Phrase * phrase() const; - Phrase * activePhrase() const; - void setPhrase(Phrase *phrase); - Phrase::Type phraseType() const; - void setPhraseType(Phrase::Type type); + std::shared_ptr activePhrase() const; + void setPhrase(std::shared_ptr phrase); + IPhrase::Type phraseType() const; + void setPhraseType(IPhrase::Type type); bool hasPreviousPhrase() const; bool hasNextPhrase() const; Q_INVOKABLE void switchToPreviousPhrase(); Q_INVOKABLE void switchToNextPhrase(); Q_INVOKABLE void updateCourseFromSkeleton(); private: - Phrase * nextPhrase() const; - Phrase * previousPhrase() const; + std::shared_ptr nextPhrase() const; + std::shared_ptr previousPhrase() const; private Q_SLOTS: void updateDisplayedUnit(); Q_SIGNALS: void editSkeletonChanged(); void skeletonModeChanged(); void skeletonChanged(); void languageChanged(); void courseChanged(); void displayedCourseChanged(); void unitChanged(); void phraseChanged(); private: Q_DISABLE_COPY(EditorSession) IEditableRepository * m_repository{ nullptr }; bool m_editSkeleton{ false }; IEditableCourse *m_skeleton{ nullptr }; ILanguage *m_language{ nullptr }; IEditableCourse *m_course{ nullptr }; Unit *m_unit{ nullptr }; - Phrase *m_phrase{ nullptr }; + std::shared_ptr m_phrase; }; #endif diff --git a/src/core/ieditablephrase.h b/src/core/ieditablephrase.h new file mode 100644 index 0000000..4112d4e --- /dev/null +++ b/src/core/ieditablephrase.h @@ -0,0 +1,81 @@ +/* + * 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 IEDITABLEPHRASE_H +#define IEDITABLEPHRASE_H + +#include "artikulatecore_export.h" +#include "iphrase.h" +#include +#include +#include +#include + +class QString; +class Unit; +class Phoneme; + +class ARTIKULATECORE_EXPORT IEditablePhrase : public IPhrase +{ + Q_OBJECT + Q_PROPERTY(QString id READ id NOTIFY idChanged) + Q_PROPERTY(QString text READ text NOTIFY textChanged) + Q_PROPERTY(QString i18nText READ i18nText NOTIFY i18nTextChanged) + Q_PROPERTY(QString soundFileUrl READ soundFileUrl NOTIFY soundChanged) + Q_PROPERTY(IPhrase::Type type READ type NOTIFY typeChanged) + Q_PROPERTY(Unit *unit READ unit NOTIFY unitChanged) + +public: + enum class EditState { + Unknown, + Translated, + Completed + }; + Q_ENUM(EditState) + + virtual ~IEditablePhrase() = default; + + virtual void setId(QString id) = 0; + virtual void setForeignId(QString id) = 0; + virtual void setText(QString text) = 0; + virtual void seti18nText(QString text) = 0; + virtual void setUnit(Unit *unit) = 0; //TODO check why this is not part of the constructor + virtual void setType(IPhrase::Type type) = 0; + virtual void setSoundFileUrl() = 0; //TODO revisit as a setter should have an argument + virtual IEditablePhrase::EditState editState() const = 0; + virtual QString editStateString() const = 0; + virtual void setEditState(IEditablePhrase::EditState state) = 0; + virtual void setEditState(const QString &stateString) = 0; + virtual void setSound(QUrl soundFile) = 0; + +protected: + IEditablePhrase() + : IPhrase() + { + } + +Q_SIGNALS: + void modified(); + void editStateChanged(); +}; + +Q_DECLARE_INTERFACE(IEditablePhrase, "com.kde.artikulate.IEditablePhrase/1.0") + +#endif // IEDITABLEPHRASE_H diff --git a/src/core/iphrase.h b/src/core/iphrase.h new file mode 100644 index 0000000..cfab15f --- /dev/null +++ b/src/core/iphrase.h @@ -0,0 +1,86 @@ +/* + * 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 IPHRASE_H +#define IPHRASE_H + +#include "artikulatecore_export.h" +#include +#include +#include +#include + +class QString; +class Unit; +class Phoneme; + +class ARTIKULATECORE_EXPORT IPhrase : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString id READ id NOTIFY idChanged) + Q_PROPERTY(QString text READ text NOTIFY textChanged) + Q_PROPERTY(QString i18nText READ i18nText NOTIFY i18nTextChanged) + Q_PROPERTY(QString soundFileUrl READ soundFileUrl NOTIFY soundChanged) + Q_PROPERTY(IPhrase::Type type READ type NOTIFY typeChanged) + Q_PROPERTY(Unit *unit READ unit NOTIFY unitChanged) + +public: + enum class Type { + Word, + Expression, + Sentence, + Paragraph, + AllTypes + }; + Q_ENUM(Type) + + virtual ~IPhrase() = default; + + virtual QString id() const = 0; + virtual QString foreignId() const = 0; + virtual QString text() const = 0; + virtual QString i18nText() const = 0; + virtual Unit * unit() const = 0; + virtual IPhrase::Type type() const = 0; + virtual QString typeString() const = 0; + virtual QString soundFileUrl() const = 0; + virtual QUrl sound() const = 0; + virtual QVector phonemes() const = 0; + +protected: + IPhrase() + : QObject() + { + } + virtual void setSelf(std::shared_ptr phrase) = 0; + +Q_SIGNALS: + void idChanged(); + void unitChanged(); + void textChanged(); + void i18nTextChanged(); + void typeChanged(); + void soundChanged(); + void phonemesChanged(); +}; + +Q_DECLARE_INTERFACE(IPhrase, "com.kde.artikulate.IPhrase/1.0") + +#endif // IPHRASE_H diff --git a/src/core/phrase.cpp b/src/core/phrase.cpp index 363209b..52711e8 100644 --- a/src/core/phrase.cpp +++ b/src/core/phrase.cpp @@ -1,338 +1,347 @@ /* * 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 "phrase.h" -#include "libsound/src/capturedevicecontroller.h" -#include "libsound/src/outputdevicecontroller.h" #include "unit.h" #include "icourse.h" -#include "settings.h" - #include "artikulate_debug.h" #include #include -Phrase::Phrase(QObject *parent) - : QObject(parent) - , m_type(Phrase::AllTypes) - , m_editState(Unknown) +Phrase::Phrase() + : IEditablePhrase() + , m_type(IPhrase::Type::AllTypes) + , m_editState(IEditablePhrase::EditState::Unknown) , m_unit(nullptr) , m_trainingProgress(0) , m_skipCounter(0) , m_excludedFromUnit(false) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); connect(this, &Phrase::idChanged, this, &Phrase::modified); connect(this, &Phrase::typeChanged, this, &Phrase::modified); connect(this, &Phrase::textChanged, this, &Phrase::modified); connect(this, &Phrase::soundChanged, this, &Phrase::modified); connect(this, &Phrase::editStateChanged, this, &Phrase::modified); connect(this, &Phrase::i18nTextChanged, this, &Phrase::modified); connect(this, &Phrase::phonemesChanged, this, &Phrase::modified); connect(this, &Phrase::excludedChanged, this, &Phrase::modified); } Phrase::~Phrase() = default; +std::shared_ptr Phrase::create() +{ + std::shared_ptr phrase(new Phrase); + phrase->setSelf(phrase); + return phrase; +} + + +void Phrase::setSelf(std::shared_ptr self) +{ + m_self = self; +} + QString Phrase::id() const { return m_id; } -void Phrase::setId(const QString &id) +void Phrase::setId(QString id) { if (id != m_id) { - m_id = id; + m_id = std::move(id); emit idChanged(); } } QString Phrase::foreignId() const { return m_foreignId; } -void Phrase::setForeignId(const QString &id) +void Phrase::setForeignId(QString id) { - m_foreignId = id; + m_foreignId = std::move(id); } QString Phrase::text() const { return m_text; } -void Phrase::setText(const QString &text) +void Phrase::setText(QString text) { if (QString::compare(text, m_text) != 0) { m_text = text.trimmed(); emit textChanged(); } } QString Phrase::i18nText() const { return m_i18nText; } -void Phrase::seti18nText(const QString &text) +void Phrase::seti18nText(QString text) { if (QString::compare(text, m_i18nText) != 0) { // copy unmodified original text string - m_i18nText = text; + m_i18nText = std::move(text); emit i18nTextChanged(); } } Phrase::Type Phrase::type() const { return m_type; } QString Phrase::typeString() const { switch(m_type) { - case Word: + case IPhrase::Type::Word: return QStringLiteral("word"); - case Expression: + case IPhrase::Type::Expression: return QStringLiteral("expression"); - case Sentence: + case IPhrase::Type::Sentence: return QStringLiteral("sentence"); - case Paragraph: + case IPhrase::Type::Paragraph: return QStringLiteral("paragraph"); default: return QStringLiteral("ERROR_UNKNOWN_TYPE"); } } void Phrase::setType(Phrase::Type type) { if (m_type == type) { return; } m_type = type; emit typeChanged(); } void Phrase::setType(const QString &typeString) { if (typeString == QLatin1String("word")) { - setType(Word); + setType(IPhrase::Type::Word); return; } if (typeString == QLatin1String("expression")) { - setType(Expression); + setType(IPhrase::Type::Expression); return; } if (typeString == QLatin1String("sentence")) { - setType(Sentence); + setType(IPhrase::Type::Sentence); return; } if (typeString == QLatin1String("paragraph")) { - setType(Paragraph); + setType(IPhrase::Type::Paragraph); return; } - qCWarning(ARTIKULATE_LOG) << "Cannot set type from unknown identifier, aborting"; + qCWarning(ARTIKULATE_CORE()) << "Cannot set type from unknown identifier, aborting"; return; } Phrase::EditState Phrase::editState() const { return m_editState; } QString Phrase::editStateString() const { switch(m_editState) { - case Unknown: + case IEditablePhrase::EditState::Unknown: return QStringLiteral("unknown"); - case Translated: + case IEditablePhrase::EditState::Translated: return QStringLiteral("translated"); - case Completed: + case IEditablePhrase::EditState::Completed: return QStringLiteral("completed"); } Q_UNREACHABLE(); } void Phrase::setEditState(Phrase::EditState state) { if (m_editState == state) { return; } m_editState = state; emit editStateChanged(); } void Phrase::setEditState(const QString &stateString) { if (stateString.isEmpty()) { return; } if (stateString == QLatin1String("unknown")) { - setEditState(Unknown); + setEditState(IEditablePhrase::EditState::Unknown); return; } if (stateString == QLatin1String("translated")) { - setEditState(Translated); + setEditState(IEditablePhrase::EditState::Translated); return; } if (stateString == QLatin1String("completed")) { - setEditState(Completed); + setEditState(IEditablePhrase::EditState::Completed); return; } qCWarning(ARTIKULATE_LOG) << "Cannot set edit state from unknown identifier " << stateString << ", aborting"; return; } Unit * Phrase::unit() const { return m_unit; } void Phrase::setUnit(Unit *unit) { if (unit == m_unit) { return; } m_unit = unit; emit unitChanged(); } QUrl Phrase::sound() const { return m_nativeSoundFile; } -void Phrase::setSound(const QUrl &soundFile) +void Phrase::setSound(QUrl soundFile) { if (!soundFile.isValid() || soundFile.isEmpty()) { qCWarning(ARTIKULATE_LOG) << "Not setting empty sound file path."; return; } - m_nativeSoundFile = soundFile; + m_nativeSoundFile = std::move(soundFile); emit soundChanged(); } QString Phrase::soundFileUrl() const { return m_nativeSoundFile.toLocalFile(); } QString Phrase::soundFileOutputPath() const { if (m_nativeSoundFile.isEmpty()) { QString outputDir = m_unit->course()->file().path() + '/'; //TODO take care that this is proper ASCII return outputDir + id() + ".ogg"; } else { return soundFileUrl(); } } void Phrase::setSoundFileUrl() { if (soundFileOutputPath() != m_nativeSoundFile.toLocalFile()) { m_nativeSoundFile = QUrl::fromLocalFile(soundFileOutputPath()); emit soundChanged(); emit modified(); } } bool Phrase::isExcluded() const { return m_excludedFromUnit; } void Phrase::setExcluded(bool excluded) { if (excluded == m_excludedFromUnit) { return; } m_excludedFromUnit = excluded; emit excludedChanged(); } int Phrase::progress() const { return static_cast(m_trainingProgress); } void Phrase::setProgress(int value) { Q_ASSERT(value >= 0); if (value < 0) { value = 0; } if (m_trainingProgress == static_cast(value)) { return; } m_trainingProgress = static_cast(value); emit progressChanged(); } void Phrase::updateProgress(Phrase::Progress progress) { // logic of progress computation: // a) if skipped 3 times in a row, decrease progress // b) if done and skipped less than two times in a row, increase progress if (progress == Progress::Done) { m_skipCounter = 0; if (m_trainingProgress < 3) { ++m_trainingProgress; emit progressChanged(); } return; } if (progress == Progress::Skip) { ++m_skipCounter; if (m_skipCounter > 2 && m_trainingProgress > 0) { --m_trainingProgress; emit progressChanged(); } return; } } QVector Phrase::phonemes() const { return m_phonemes; } bool Phrase::hasPhoneme(Phoneme* phoneme) { return m_phonemes.contains(phoneme); } void Phrase::addPhoneme(Phoneme *phoneme) { if (!m_phonemes.contains(phoneme)) { m_phonemes.append(phoneme); emit phonemesChanged(); //FIXME tell Unit to also send corresponding signal! } } void Phrase::removePhoneme(Phoneme *phoneme) { if (m_phonemes.removeOne(phoneme)) { emit phonemesChanged(); //FIXME tell Unit to also send corresponding signal! } } diff --git a/src/core/phrase.h b/src/core/phrase.h index 48db14d..65bcf0f 100644 --- a/src/core/phrase.h +++ b/src/core/phrase.h @@ -1,140 +1,117 @@ /* * 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 . */ #ifndef PHRASE_H #define PHRASE_H #include "artikulatecore_export.h" - -#include +#include "iphrase.h" +#include "ieditablephrase.h" #include #include #include -#include +#include class QString; class Phoneme; class Unit; class QUrl; -class ARTIKULATECORE_EXPORT Phrase : public QObject +class ARTIKULATECORE_EXPORT Phrase : public IEditablePhrase { Q_OBJECT Q_PROPERTY(QString id READ id WRITE setId NOTIFY idChanged) Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) Q_PROPERTY(QString i18nText READ i18nText WRITE seti18nText NOTIFY i18nTextChanged) Q_PROPERTY(QString soundFileUrl READ soundFileUrl NOTIFY soundChanged) - Q_PROPERTY(Phrase::Type type READ type WRITE setType NOTIFY typeChanged) + Q_PROPERTY(IPhrase::Type type READ type WRITE setType NOTIFY typeChanged) Q_PROPERTY(Phrase::EditState editState READ editState WRITE setEditState NOTIFY editStateChanged) Q_PROPERTY(Unit *unit READ unit NOTIFY unitChanged) Q_PROPERTY(bool excluded READ isExcluded NOTIFY excludedChanged) Q_PROPERTY(int progress READ progress NOTIFY progressChanged) public: - Q_ENUMS(EditState) - Q_ENUMS(TrainingState) - Q_ENUMS(Type) - enum EditState { - Unknown, - Translated, - Completed - }; - enum TrainingState { //TODO not needed anymore with statistics - Trained, - Untrained - }; enum class Progress { Skip, Done }; - enum Type { - Word, - Expression, - Sentence, - Paragraph, - AllTypes - }; + Q_ENUM(Progress) - explicit Phrase(QObject *parent = nullptr); - ~Phrase(); + static std::shared_ptr create(); - QString id() const; - void setId(const QString &id); - QString foreignId() const; - void setForeignId(const QString &id); - QString text() const; - void setText(const QString &text); - QString i18nText() const; - void seti18nText(const QString &text); - Unit * unit() const; - void setUnit(Unit *unit); - Phrase::Type type() const; - QString typeString() const; - void setType(Phrase::Type type); + ~Phrase() override; + + QString id() const override; + void setId(QString id) override; + QString foreignId() const override; + void setForeignId(QString id) override; + QString text() const override; + void setText(QString text) override; + QString i18nText() const override; + void seti18nText(QString text) override; + Unit * unit() const override; + void setUnit(Unit *unit) override; + IPhrase::Type type() const override; + QString typeString() const override; + void setType(IPhrase::Type type) override; void setType(const QString &typeString); - QString soundFileUrl() const; + QString soundFileUrl() const override; Q_INVOKABLE QString soundFileOutputPath() const; - Q_INVOKABLE void setSoundFileUrl(); - Phrase::EditState editState() const; - QString editStateString() const; - void setEditState(Phrase::EditState state); - void setEditState(const QString &stateString); - QUrl sound() const; - void setSound(const QUrl &soundFile); - QVector phonemes() const; + Q_INVOKABLE void setSoundFileUrl() override; + IEditablePhrase::EditState editState() const override; + QString editStateString() const override; + void setEditState(IEditablePhrase::EditState state) override; + void setEditState(const QString &stateString) override; + QUrl sound() const override; + void setSound(QUrl soundFile) override; + QVector phonemes() const override; bool isExcluded() const; void setExcluded(bool excluded = false); int progress() const; void setProgress(int value); void updateProgress(Phrase::Progress progress); Q_INVOKABLE bool hasPhoneme(Phoneme *phoneme); Q_INVOKABLE void addPhoneme(Phoneme *phoneme); Q_INVOKABLE void removePhoneme(Phoneme *phoneme); Q_SIGNALS: - void idChanged(); - void unitChanged(); - void textChanged(); - void i18nTextChanged(); - void typeChanged(); - void editStateChanged(); - void soundChanged(); - void excludedChanged(); - void phonemesChanged(); - void modified(); void progressChanged(); + void excludedChanged(); private: Q_DISABLE_COPY(Phrase) + explicit Phrase(); + void setSelf(std::shared_ptr self) override; + std::weak_ptr m_self; QString m_id; QString m_foreignId; QString m_text; QString m_i18nText; - Type m_type; + IPhrase::Type m_type; EditState m_editState; Unit *m_unit; unsigned m_trainingProgress; int m_skipCounter; // count how many skips occurred since last progress update bool m_excludedFromUnit; QVector m_phonemes; QUrl m_nativeSoundFile; }; #endif // PHRASE_H diff --git a/src/core/resources/courseparser.cpp b/src/core/resources/courseparser.cpp index b550489..33e9945 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 occurred while parsing unit" << unit->title() << unit->id(); } return unit; } -Phrase * CourseParser::parsePhrase(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok) +std::shared_ptr CourseParser::parsePhrase(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok) { - Phrase * phrase = new Phrase; + std::shared_ptr phrase = Phrase::create(); 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); + phrase->setType(IPhrase::Type::Word); } else if (type == "expression") { - phrase->setType(Phrase::Expression); + phrase->setType(IPhrase::Type::Expression); } else if (type == "sentence") { - phrase->setType(Phrase::Sentence); + phrase->setType(IPhrase::Type::Sentence); } else if (type == "paragraph") { - phrase->setType(Phrase::Paragraph); + phrase->setType(IPhrase::Type::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 occurred 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 (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()) { + for (auto &phrase : unit->phrases()) { if (trainingExport && phrase->soundFileUrl().isEmpty()) { continue; } - unitPhraseListElement.appendChild(serializedPhrase(phrase, document)); +// unitPhraseListElement.appendChild(serializedPhrase(phrase, document)); //FIXME } if (trainingExport && unitPhraseListElement.childNodes().isEmpty()) { 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->units()) { - for (auto *phrase : unit->phraseList()) { + for (auto &phrase : unit->phrases()) { 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/courseparser.h b/src/core/resources/courseparser.h index fdcfb81..2c0c931 100644 --- a/src/core/resources/courseparser.h +++ b/src/core/resources/courseparser.h @@ -1,76 +1,76 @@ /* * 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 . */ #ifndef COURSEPARSER_H #define COURSEPARSER_H #include "artikulatecore_export.h" #include #include class ICourse; class Unit; class Phrase; class Phoneme; class IResourceRepository; class QXmlSchema; class QJSonDocument; class QDomDocument; class QDomElement; class QXmlStreamReader; class QString; class QUrl; class ARTIKULATECORE_EXPORT CourseParser { public: /** * Load XSD file given by its file name (without ".xsd" suffix). The method searches exclusively * the standard install dir for XSD files in subdirectory "schemes/". * * \param schemeName name of the Xml schema without suffix * \return loaded XML Schema */ static QXmlSchema loadXmlSchema(const QString &schemeName); /** * Load XML file given by \p file that confirms with XML schema \p scheme. * * \param path is the path to the XML file to be loaded * \param scheme is the XML schema describing the DOM * \return the loaded DOM document */ static QDomDocument loadDomDocument(const QUrl &path, const QXmlSchema &schema); static std::vector> parseUnits(const QUrl &path, QVector> phonemes = QVector>()); static QDomDocument serializedDocument(ICourse *course, bool trainingExport); static QDomElement serializedPhrase(Phrase *phrase, QDomDocument &document); static bool exportCourseToGhnsPackage(ICourse *course, const QString &exportPath); private: static std::unique_ptr parseUnit(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok); - static Phrase * parsePhrase(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok); + static std::shared_ptr parsePhrase(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok); static QStringList parsePhonemeIds(QXmlStreamReader &xml, bool &ok); static QString parseElement(QXmlStreamReader &xml, bool &ok); }; #endif diff --git a/src/core/resources/editablecourseresource.cpp b/src/core/resources/editablecourseresource.cpp index 1a4156f..7809f4d 100644 --- a/src/core/resources/editablecourseresource.cpp +++ b/src/core/resources/editablecourseresource.cpp @@ -1,288 +1,288 @@ /* * 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 "artikulate_debug.h" #include "core/phoneme.h" #include "core/phrase.h" #include "core/unit.h" #include "courseparser.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); for (auto unit : m_course->units()) { unit->setCourse(this); } 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); } std::shared_ptr EditableCourseResource::create( const QUrl &path, IResourceRepository *repository) { std::shared_ptr course(new EditableCourseResource(path, repository)); course->setSelf(course); return course; } void EditableCourseResource::setSelf(std::shared_ptr self) { m_course->setSelf(self); } QString EditableCourseResource::id() const { return m_course->id(); } void EditableCourseResource::setId(QString id) { if (m_course->id() != id) { m_course->setId(id); m_modified = true; } } 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) { if (m_course->title() != title) { m_course->setTitle(title); m_modified = true; } } QString EditableCourseResource::i18nTitle() const { return m_course->i18nTitle(); } void EditableCourseResource::setI18nTitle(QString i18nTitle) { if (m_course->i18nTitle() != i18nTitle) { m_course->setI18nTitle(i18nTitle); m_modified = true; } } QString EditableCourseResource::description() const { return m_course->description(); } void EditableCourseResource::setDescription(QString description) { if (m_course->description() != description) { m_course->setDescription(description); m_modified = true; } } std::shared_ptr EditableCourseResource::language() const { return m_course->language(); } void EditableCourseResource::setLanguage(std::shared_ptr language) { if (m_course->language() != language) { m_course->setLanguage(language); m_modified = true; } } QUrl EditableCourseResource::file() const { return m_course->file(); } std::shared_ptr EditableCourseResource::self() const { return std::static_pointer_cast(m_course->self()); } bool 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 false; } bool ok = exportToFile(file()); if (ok) { m_modified = false; } return ok; } bool EditableCourseResource::exportToFile(const QUrl &filePath) const { // 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) { m_modified = true; auto sharedUnit = m_course->addUnit(std::move(unit)); sharedUnit->setCourse(this); return sharedUnit; } QVector> EditableCourseResource::units() { return m_course->units(); } void EditableCourseResource::updateFrom(std::shared_ptr skeleton) { for (auto skeletonUnit : skeleton->units()) { // find matching unit or create one std::shared_ptr matchingUnit; auto it = std::find_if(m_course->units().cbegin(), m_course->units().cend(), [skeletonUnit](std::shared_ptr compareUnit) { return compareUnit->foreignId() == skeletonUnit->id(); }); if (it == m_course->units().cend()) { // import complete unit auto importUnit = std::unique_ptr(new Unit); importUnit->setId(skeletonUnit->id()); importUnit->setForeignId(skeletonUnit->id()); importUnit->setTitle(skeletonUnit->title()); matchingUnit = m_course->addUnit(std::move(importUnit)); } else { matchingUnit = *it; } // import phrases - for (auto skeletonPhrase : skeletonUnit->phraseList()) { - auto it = std::find_if(matchingUnit->phraseList().cbegin(), - matchingUnit->phraseList().cend(), [skeletonPhrase](Phrase *comparePhrase) { + for (auto skeletonPhrase : skeletonUnit->phrases()) { + auto it = std::find_if(matchingUnit->phrases().cbegin(), + matchingUnit->phrases().cend(), [skeletonPhrase](std::shared_ptr comparePhrase) { return comparePhrase->foreignId() == skeletonPhrase->id(); }); - if (it == matchingUnit->phraseList().cend()) { + if (it == matchingUnit->phrases().cend()) { // import complete Phrase - Phrase *importPhrase = new Phrase(matchingUnit.get()); + std::shared_ptr importPhrase = Phrase::create(); importPhrase->setId(skeletonPhrase->id()); importPhrase->setForeignId(skeletonPhrase->id()); importPhrase->setText(skeletonPhrase->text()); importPhrase->seti18nText(skeletonPhrase->i18nText()); importPhrase->setType(skeletonPhrase->type()); importPhrase->setUnit(matchingUnit.get()); matchingUnit->addPhrase(importPhrase); } } } qCInfo(ARTIKULATE_LOG()) << "Update performed!"; } bool EditableCourseResource::isModified() const { return m_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) +std::shared_ptr EditableCourseResource::createPhrase(Unit *unit) { // find globally unique phrase id inside course QStringList phraseIds; for (auto unit : m_course->units()) { - for (auto *phrase : unit->phraseList()) { + for (auto &phrase : unit->phrases()) { 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); + std::shared_ptr phrase = Phrase::create(); phrase->setId(id); phrase->setText(QLatin1String("")); - phrase->setType(Phrase::Word); + phrase->setType(IPhrase::Type::Word); unit->addPhrase(phrase); return phrase; } diff --git a/src/core/resources/editablecourseresource.h b/src/core/resources/editablecourseresource.h index 06af212..49b3ea5 100644 --- a/src/core/resources/editablecourseresource.h +++ b/src/core/resources/editablecourseresource.h @@ -1,126 +1,126 @@ /* * 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: static std::shared_ptr create(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; bool sync() override; bool exportToFile(const QUrl &filePath) const override; std::shared_ptr addUnit(std::unique_ptr unit) override; QVector> units() override; void updateFrom(std::shared_ptr course) override; bool isModified() const override; QUrl file() const override; std::shared_ptr self() const override; Q_INVOKABLE Unit * createUnit(); - Q_INVOKABLE Phrase * createPhrase(Unit *unit); + Q_INVOKABLE std::shared_ptr createPhrase(Unit *unit); Q_SIGNALS: void idChanged(); void foreignIdChanged(); void titleChanged(); void i18nTitleChanged(); void descriptionChanged(); void languageChanged(); private: Q_DISABLE_COPY(EditableCourseResource) /** * Create course resource from file. */ explicit EditableCourseResource(const QUrl &path, IResourceRepository *repository); void setSelf(std::shared_ptr self) override; 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 cce07c0..85fbf0d 100644 --- a/src/core/resources/skeletonresource.cpp +++ b/src/core/resources/skeletonresource.cpp @@ -1,370 +1,370 @@ /* * 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 #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(); m_modified = false; } QVector> units(); std::shared_ptr appendUnit(std::shared_ptr unit); /** * @return the skeleton resource as serialized byte array */ QDomDocument serializedSkeleton(); std::weak_ptr m_self; QUrl m_path; QString m_identifier; QString m_title; QString m_description; bool m_unitsParsed{ false }; bool m_modified{ 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) { Q_ASSERT(m_self.lock() != nullptr); unit->setCourse(m_self.lock().get()); m_units.append(std::move(unit)); } m_unitsParsed = true; return m_units; } std::shared_ptr SkeletonResourcePrivate::appendUnit(std::shared_ptr unit) { units(); // ensure that units are parsed m_units.append(unit); m_modified = true; Q_ASSERT(m_self.lock() != nullptr); unit->setCourse(m_self.lock().get()); 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()) { + for (auto &phrase : unit->phrases()) { 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; } std::shared_ptr SkeletonResource::create(const QUrl &path, IResourceRepository *repository) { std::shared_ptr course(new SkeletonResource(path, repository)); course->setSelf(course); return course; } SkeletonResource::SkeletonResource(const QUrl &path, IResourceRepository *repository) : IEditableCourse() , d(new SkeletonResourcePrivate(path)) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); connect(this, &SkeletonResource::idChanged, this, [=]() { d->m_modified = true; }); connect(this, &SkeletonResource::titleChanged, this, [=]() { d->m_modified = true; }); connect(this, &SkeletonResource::descriptionChanged, this, [=]() { d->m_modified = true; }); Q_UNUSED(repository); } SkeletonResource::~SkeletonResource() = default; void SkeletonResource::setSelf(std::shared_ptr self) { d->m_self = self; } std::shared_ptr SkeletonResource::self() const { return std::static_pointer_cast(d->m_self.lock()); } 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::exportToFile(const QUrl &filePath) const { // 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) { std::shared_ptr storedUnit(std::move(unit)); emit unitAboutToBeAdded(storedUnit, d->units().count() - 1); d->appendUnit(storedUnit); emit unitAdded(); return storedUnit; } bool SkeletonResource::sync() { if (!d->m_modified) { qCDebug(ARTIKULATE_LOG()) << "Aborting sync, skeleton was not modified."; return false; } bool ok = exportToFile(file()); if (ok) { d->m_modified = false; } return ok; } void SkeletonResource::updateFrom(std::shared_ptr) { // not supported } bool SkeletonResource::isModified() const { return d->m_modified; } 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(); } QVector> SkeletonResource::units() { return d->units(); } QUrl SkeletonResource::file() const { return d->m_path; } diff --git a/src/core/trainingaction.cpp b/src/core/trainingaction.cpp index 0ba7e5f..4bc57a0 100644 --- a/src/core/trainingaction.cpp +++ b/src/core/trainingaction.cpp @@ -1,110 +1,110 @@ /* * Copyright 2018-2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "trainingaction.h" #include "trainingactionicon.h" #include "drawertrainingactions.h" #include "trainingsession.h" TrainingAction::TrainingAction(QObject *parent) : QObject(parent) , m_text(QString()) , m_icon(new TrainingActionIcon(this, QString())) //TODO "rating-unrated" vs. "rating" { } TrainingAction::TrainingAction(const QString &text, QObject *parent) : QObject(parent) , m_text(text) , m_icon(new TrainingActionIcon(this, QString())) //TODO "rating-unrated" vs. "rating" { } -TrainingAction::TrainingAction(Phrase *phrase, TrainingSession *session, QObject* parent) +TrainingAction::TrainingAction(std::shared_ptr phrase, TrainingSession *session, QObject* parent) : QObject(parent) , m_icon(new TrainingActionIcon(this, QString())) , m_phrase(phrase) , m_trainingSession(session) { if (m_phrase) { m_text = phrase->text(); } } void TrainingAction::appendChild(QObject* child) { m_actions.append(child); emit actionsChanged(); } bool TrainingAction::hasChildren() const { return m_actions.count() > 0; } void TrainingAction::trigger() { if (m_phrase && m_trainingSession) { - m_trainingSession->setPhrase(m_phrase); + m_trainingSession->setPhrase(m_phrase.get()); } } bool TrainingAction::enabled() const { return m_enabled; } void TrainingAction::setEnabled(bool enabled) { if (enabled == m_enabled) { return; } m_enabled = enabled; emit enabledChanged(m_enabled); } bool TrainingAction::checked() const { return m_checked; } void TrainingAction::setChecked(bool checked) { if (checked == m_checked) { return; } m_checked = checked; emit checkedChanged(m_checked); } QObject * TrainingAction::icon() const { return m_icon; } -Phrase * TrainingAction::phrase() const +IPhrase * TrainingAction::phrase() const { - return m_phrase; + return m_phrase.get(); //TODO } QList TrainingAction::actions() const { return m_actions; } diff --git a/src/core/trainingaction.h b/src/core/trainingaction.h index e22d8e4..0a561ad 100644 --- a/src/core/trainingaction.h +++ b/src/core/trainingaction.h @@ -1,79 +1,79 @@ /* * Copyright 2018-2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef TRAININGACTION_H #define TRAININGACTION_H #include "artikulatecore_export.h" #include "trainingactionicon.h" -#include "phrase.h" +#include "iphrase.h" #include "trainingsession.h" #include #include class DrawerTrainingActions; class ARTIKULATECORE_EXPORT TrainingAction : public QObject { Q_OBJECT Q_PROPERTY(QString text MEMBER m_text CONSTANT) Q_PROPERTY(QObject* icon READ icon CONSTANT) Q_PROPERTY(bool visible MEMBER m_visible CONSTANT) Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) Q_PROPERTY(bool checked READ checked NOTIFY checkedChanged) Q_PROPERTY(QString tooltip MEMBER m_tooltip CONSTANT) Q_PROPERTY(QList children READ actions NOTIFY actionsChanged) Q_PROPERTY(bool checkable MEMBER m_checkable CONSTANT) public: TrainingAction(QObject *parent = nullptr); TrainingAction(const QString &text, QObject *parent = nullptr); - TrainingAction(Phrase *phrase, TrainingSession *session, QObject *parent = nullptr); + TrainingAction(std::shared_ptr phrase, TrainingSession *session, QObject *parent = nullptr); void appendChild(QObject *child); bool hasChildren() const; Q_INVOKABLE void trigger(); bool enabled() const; void setEnabled(bool enabled); void setChecked(bool checked); bool checked() const; QObject * icon() const; - Phrase * phrase() const; + IPhrase * phrase() const; QList actions() const; Q_SIGNALS: void changed(); void actionsChanged(); void enabledChanged(bool enabled); void checkedChanged(bool checked); private: QString m_text; TrainingActionIcon *m_icon{nullptr}; bool m_visible{true}; bool m_enabled{true}; bool m_checked{false}; bool m_checkable{false}; QString m_tooltip{QString()}; QList m_actions; - Phrase *m_phrase{nullptr}; + std::shared_ptr m_phrase; TrainingSession * m_trainingSession{nullptr}; }; #endif diff --git a/src/core/trainingsession.cpp b/src/core/trainingsession.cpp index bdc9862..578c493 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->units().count() > 0) { setUnit(m_course->units().constFirst().get()); } // lazy loading of training data LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->id()); if (!goal) { goal = m_profileManager->registerGoal( LearnerProfile::LearningGoal::Language, course->language()->id(), course->language()->i18nTitle() ); } auto data = m_profileManager->progressValues(m_profileManager->activeProfile(), goal, m_course->id() ); const auto unitList = m_course->units(); for (auto unit : qAsConst(unitList)) { - const auto phraseList = unit->phraseList(); - for (Phrase *phrase : qAsConst(phraseList)) { + const auto phrases = unit->phrases(); + for (auto &phrase : phrases) { auto iter = data.find(phrase->id()); if (iter != data.end()) { - phrase->setProgress(iter.value()); +// phrase->setProgress(iter.value()); //FIXME add a decorator? } } } 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 +IPhrase * TrainingSession::activePhrase() const { if (const auto action = activeAction()) { return action->phrase(); } return nullptr; } -void TrainingSession::setPhrase(Phrase *phrase) +void TrainingSession::setPhrase(IPhrase *phrase) { for (int i = 0; i < m_actions.count(); ++i) { for (int j = 0; j < m_actions.at(i)->actions().count(); ++j) { const auto testPhrase = qobject_cast(m_actions.at(i)->actions().at(j))->phrase(); if (phrase == testPhrase) { if (auto action = activeAction()) { action->setChecked(false); } m_indexUnit = i; m_indexPhrase = j; if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); return; } } } } void TrainingSession::accept() { Q_ASSERT(m_indexUnit >= 0); Q_ASSERT(m_indexPhrase >= 0); if (m_indexUnit < 0 || m_indexPhrase < 0) { return; } auto phrase = activePhrase(); // possibly update goals of learner updateGoal(); - phrase->updateProgress(Phrase::Progress::Done); +// phrase->updateProgress(Phrase::Progress::Done); //FIXME // store training activity LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->language()->id()); - m_profileManager->recordProgress(m_profileManager->activeProfile(), - goal, - m_course->id(), - phrase->id(), - static_cast(LearnerProfile::ProfileManager::Skip), - phrase->progress() - ); +// m_profileManager->recordProgress(m_profileManager->activeProfile(), //FIXME +// goal, +// m_course->id(), +// phrase->id(), +// static_cast(LearnerProfile::ProfileManager::Skip), +// phrase->progress() +// ); selectNextPhrase(); } void TrainingSession::skip() { Q_ASSERT(m_indexUnit >= 0); Q_ASSERT(m_indexPhrase >= 0); if (m_indexUnit < 0 || m_indexPhrase < 0) { return; } // possibly update goals of learner updateGoal(); auto phrase = activePhrase(); - phrase->updateProgress(Phrase::Progress::Skip); +// phrase->updateProgress(Phrase::Progress::Skip); //FIXME // store training activity LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->language()->id()); - m_profileManager->recordProgress(m_profileManager->activeProfile(), - goal, - m_course->id(), - phrase->id(), - static_cast(LearnerProfile::ProfileManager::Skip), - phrase->progress() - ); +// m_profileManager->recordProgress(m_profileManager->activeProfile(), +// goal, +// m_course->id(), +// phrase->id(), +// static_cast(LearnerProfile::ProfileManager::Skip), +// phrase->progress() +// ); // FIXME selectNextPhrase(); } void TrainingSession::selectNextPhrase() { if (auto action = activeAction()) { action->setChecked(false); } // try to find next phrase, otherwise return completed if (m_indexPhrase >= m_actions.at(m_indexUnit)->actions().count() - 1) { qDebug() << "switching to next unit"; if (m_indexUnit >= m_actions.count() - 1) { emit completed(); } else { ++m_indexUnit; m_indexPhrase = 0; } } else { ++m_indexPhrase; } if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); } bool TrainingSession::hasPrevious() const { return m_indexUnit > 0 || m_indexPhrase > 0; } bool TrainingSession::hasNext() const { if (m_indexUnit < m_actions.count() - 1) { return true; } if (m_actions.constLast()) { if (m_indexPhrase < m_actions.constLast()->actions().count() - 1) { return true; } } return false; } void TrainingSession::updateGoal() { if (!m_profileManager) { qCWarning(ARTIKULATE_LOG()) << "No ProfileManager registered, aborting operation"; return; } LearnerProfile::Learner *learner = m_profileManager->activeProfile(); if (!learner) { qCWarning(ARTIKULATE_LOG()) << "No active Learner registered, aborting operation"; return; } LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->language()->id()); learner->addGoal(goal); learner->setActiveGoal(goal); } QVector TrainingSession::trainingActions() const { return m_actions; } void TrainingSession::updateTrainingActions() { for (const auto &action : qAsConst(m_actions)) { action->deleteLater(); } m_actions.clear(); if (!m_course) { m_indexUnit = -1; m_indexPhrase = -1; return; } const auto unitList = m_course->units(); for (const auto &unit : qAsConst(unitList)) { auto action = new TrainingAction(unit->title(), this); - const auto phraseList = unit->phraseList(); + const auto phraseList = unit->phrases(); for (const auto &phrase : qAsConst(phraseList)) { if (phrase->sound().isEmpty()) { continue; } action->appendChild(new TrainingAction(phrase, this, unit.get())); } if (action->hasChildren()) { m_actions.append(action); } else { action->deleteLater(); } } // update indices m_indexUnit = -1; m_indexPhrase = -1; if (m_course->units().count() > 0) { m_indexUnit = 0; - if (m_course->units().constFirst()->phraseList().count() > 0) { + if (m_course->units().constFirst()->phrases().count() > 0) { m_indexPhrase = 0; } } } diff --git a/src/core/trainingsession.h b/src/core/trainingsession.h index e457be4..ef4f813 100644 --- a/src/core/trainingsession.h +++ b/src/core/trainingsession.h @@ -1,94 +1,94 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef TRAININGSESSION_H #define TRAININGSESSION_H #include "artikulatecore_export.h" #include "phrase.h" #include class Language; class ICourse; class Unit; class TrainingAction; namespace LearnerProfile { class ProfileManager; } /** * \class TrainingSession */ class ARTIKULATECORE_EXPORT TrainingSession : public QObject { Q_OBJECT Q_PROPERTY(ICourse *course READ course WRITE setCourse NOTIFY courseChanged) Q_PROPERTY(Unit *unit READ activeUnit WRITE setUnit NOTIFY phraseChanged) - Q_PROPERTY(Phrase *phrase READ activePhrase WRITE setPhrase NOTIFY phraseChanged) + Q_PROPERTY(IPhrase *phrase READ activePhrase WRITE setPhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasNext READ hasNext NOTIFY phraseChanged) public: explicit TrainingSession(LearnerProfile::ProfileManager *manager, QObject *parent = nullptr); ICourse * course() const; void setCourse(ICourse *course); Unit * activeUnit() const; void setUnit(Unit *unit); TrainingAction * activeAction() const; - Phrase * activePhrase() const; - void setPhrase(Phrase *phrase); + IPhrase * activePhrase() const; + void setPhrase(IPhrase *phrase); bool hasPrevious() const; bool hasNext() const; Q_INVOKABLE void accept(); Q_INVOKABLE void skip(); /** * @brief Return tree of training actions * * The return actions form a 2-level hierarchy: * - the first level are all units * - the unit actions may contain sub-actions, which are the phrases * * @note phrases without sound file paths are skipped when generating actions */ QVector trainingActions() const; Q_SIGNALS: void courseChanged(); void phraseChanged(); /** * @brief Emitted when last phrase of session is skipped or marked as completed. */ void completed(); private: Q_DISABLE_COPY(TrainingSession) void updateTrainingActions(); void selectNextPhrase(); void updateGoal(); LearnerProfile::ProfileManager *m_profileManager; ICourse *m_course; QVector m_actions; int m_indexUnit{-1}; int m_indexPhrase{-1}; }; #endif diff --git a/src/core/unit.cpp b/src/core/unit.cpp index a7a9956..7460311 100644 --- a/src/core/unit.cpp +++ b/src/core/unit.cpp @@ -1,167 +1,168 @@ /* * 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 "unit.h" #include "phrase.h" #include #include #include #include #include #include #include "artikulate_debug.h" #include #include Unit::Unit(QObject *parent) : QObject(parent) , m_course(nullptr) , m_phraseSignalMapper(new QSignalMapper(this)) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); } Unit::~Unit() { for (auto phrase : m_phrases) { phrase->deleteLater(); } m_phrases.clear(); m_phraseSignalMapper->deleteLater(); } QString Unit::id() const { return m_id; } void Unit::setId(const QString &id) { if (id != m_id) { m_id = id; emit idChanged(); emit modified(); } } QString Unit::foreignId() const { return m_foreignId; } void Unit::setForeignId(const QString &id) { m_foreignId = id; } ICourse *Unit::course() const { return m_course; } void Unit::setCourse(ICourse *course) { if (course == m_course) { return; } m_course = course; emit courseChanged(); } QString Unit::title() const { return m_title; } void Unit::setTitle(const QString &title) { if (QString::compare(title, m_title) != 0) { m_title = title; emit titleChanged(); emit modified(); } } -QList Unit::phraseList() const +QVector> Unit::phrases() const { return m_phrases; } -void Unit::addPhrase(Phrase *phrase) +void Unit::addPhrase(std::shared_ptr phrase) { auto iter = m_phrases.constBegin(); while (iter != m_phrases.constEnd()) { if (phrase->id() == (*iter)->id()) { qCWarning(ARTIKULATE_LOG()) << "Phrase is already contained in this unit, aborting"; return; } ++iter; } phrase->setUnit(this); - emit phraseAboutToBeAdded(phrase, m_phrases.length()); + emit phraseAboutToBeAdded(phrase.get(), m_phrases.length()); m_phrases.append(phrase); - m_phraseSignalMapper->setMapping(phrase, phrase->id()); + m_phraseSignalMapper->setMapping(phrase.get(), phrase->id()); - emit phraseAdded(phrase); + emit phraseAdded(phrase.get()); - connect(phrase, &Phrase::typeChanged, m_phraseSignalMapper, + connect(phrase.get(), &Phrase::typeChanged, m_phraseSignalMapper, static_cast(&QSignalMapper::map)); - connect(phrase, &Phrase::modified, this, &Unit::modified); + connect(phrase.get(), &Phrase::modified, this, &Unit::modified); emit modified(); } -QList Unit::excludedSkeletonPhraseList() const +QList Unit::excludedSkeletonPhraseList() const { - QList excludedPhraseList; - for (auto phrase : m_phrases) { - if (phrase->isExcluded() == true) { - excludedPhraseList.append(phrase); - } - } + QList excludedPhraseList; +//TODO this should not be handled on unit level +// for (auto phrase : m_phrases) { +// if (phrase->isExcluded() == true) { +// excludedPhraseList.append(phrase); +// } +// } return excludedPhraseList; } void Unit::excludeSkeletonPhrase(const QString &phraseId) { - for (auto phrase : m_phrases) { - if (phrase->id() == phraseId) { - phrase->setExcluded(true); - emit modified(); - return; - } - } +// for (auto phrase : m_phrases) { +// if (phrase->id() == phraseId) { +// phrase->setExcluded(true); +// emit modified(); +// return; +// } +// } qCWarning(ARTIKULATE_LOG) << "Could not exclude phrase with ID " << phraseId << ", no phrase with this ID."; } void Unit::includeSkeletonPhrase(const QString &phraseId) { - for (auto phrase : m_phrases) { - if (phrase->id() == phraseId) { - phrase->setExcluded(false); - emit modified(); - return; - } - } +// for (auto phrase : m_phrases) { +// if (phrase->id() == phraseId) { +// phrase->setExcluded(false); +// emit modified(); +// return; +// } +// } qCWarning(ARTIKULATE_LOG) << "Could not include phrase with ID " << phraseId << ", no phrase with this ID."; } diff --git a/src/core/unit.h b/src/core/unit.h index 529f4e3..bc99deb 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -1,89 +1,89 @@ /* * 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 UNIT_H #define UNIT_H #include "artikulatecore_export.h" #include #include -#include #include #include class QSignalMapper; class QString; class Phrase; +class IPhrase; class ICourse; class ARTIKULATECORE_EXPORT Unit : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id WRITE setId NOTIFY idChanged) Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) Q_PROPERTY(ICourse *course READ course WRITE setCourse NOTIFY courseChanged) public: explicit Unit(QObject *parent = nullptr); ~Unit(); QString id() const; void setId(const QString &id); QString foreignId() const; void setForeignId(const QString &id); ICourse * course() const; void setCourse(ICourse* course); QString title() const; void setTitle(const QString &title); - QList phraseList() const; - void addPhrase(Phrase *phrase); - QList excludedSkeletonPhraseList() const; + QVector> phrases() const; + void addPhrase(std::shared_ptr phrase); + QList excludedSkeletonPhraseList() const; /** * Removes phrase with ID \p phraseId from unit and adds ID to set * of excluded IDs. * * \param phraseId is the UID of the to be excluded phrase */ Q_INVOKABLE void excludeSkeletonPhrase(const QString &phraseId); Q_INVOKABLE void includeSkeletonPhrase(const QString &phraseId); Q_SIGNALS: void idChanged(); void titleChanged(); void courseChanged(); void displayPhraseTypeChanged(); void modified(); - void phraseAdded(Phrase*); - void phraseAboutToBeAdded(Phrase*,int); - void phraseRemoved(Phrase*); + void phraseAdded(IPhrase*); //TODO + void phraseAboutToBeAdded(IPhrase*,int);//TODO + void phraseRemoved(IPhrase*);//TODO void phraseAboutToBeRemoved(int,int); private: Q_DISABLE_COPY(Unit) QString m_id; QString m_foreignId; ICourse *m_course; QString m_title; - QList m_phrases; + QVector> m_phrases; QSignalMapper *m_phraseSignalMapper; }; #endif // UNIT_H diff --git a/src/models/phraselistmodel.cpp b/src/models/phraselistmodel.cpp index e7c15e5..82a4c2b 100644 --- a/src/models/phraselistmodel.cpp +++ b/src/models/phraselistmodel.cpp @@ -1,210 +1,210 @@ /* * 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 "phraselistmodel.h" #include "core/unit.h" #include "core/phrase.h" #include #include #include PhraseListModel::PhraseListModel(QObject *parent) : QAbstractListModel(parent) , m_unit(nullptr) , m_signalMapper(new QSignalMapper(this)) { connect(m_signalMapper, SIGNAL(mapped(int)), SLOT(emitPhraseChanged(int))); // connect all phrase number operations to single signal connect(this, &PhraseListModel::typeChanged, this, &PhraseListModel::countChanged); connect(this, &PhraseListModel::unitChanged, this, &PhraseListModel::countChanged); } QHash< int, QByteArray > PhraseListModel::roleNames() const { QHash roles; roles[TextRole] = "text"; roles[SoundFileRole] = "soundFile"; roles[IdRole] = "id"; roles[TypeRole] = "type"; roles[ExcludedRole] = "excludedRole"; roles[DataRole] = "dataRole"; return roles; } void PhraseListModel::setUnit(Unit *unit) { if (m_unit == unit) { return; } beginResetModel(); if (m_unit) { m_unit->disconnect(this); - foreach (Phrase *phrase, m_unit->phraseList()) { + for (auto &phrase : m_unit->phrases()) { phrase->disconnect(this); } } m_unit = unit; if (m_unit) { // initial setting of signal mappings connect(m_unit, &Unit::phraseAboutToBeAdded, this, &PhraseListModel::onPhraseAboutToBeAdded); connect(m_unit, &Unit::phraseAdded, this, &PhraseListModel::onPhraseAdded); connect(m_unit, &Unit::phraseAboutToBeRemoved, this, &PhraseListModel::onPhrasesAboutToBeRemoved); connect(m_unit, &Unit::phraseRemoved, this, &PhraseListModel::onPhrasesRemoved); // insert and connect all already existing phrases - int phrases = m_unit->phraseList().count(); - for (int i=0; i < phrases; ++i) { - onPhraseAboutToBeAdded(m_unit->phraseList().at(i), i); + int phrases = m_unit->phrases().count(); + for (int i = 0; i < phrases; ++i) { + onPhraseAboutToBeAdded(m_unit->phrases().at(i).get(), i); endInsertRows(); emit countChanged(); } updateMappings(); } // emit done endResetModel(); emit unitChanged(); } Unit * PhraseListModel::unit() const { return m_unit; } QVariant PhraseListModel::data(const QModelIndex &index, int role) const { Q_ASSERT(m_unit); if (!index.isValid()) { return QVariant(); } - if (index.row() >= m_unit->phraseList().count()) { + if (index.row() >= m_unit->phrases().count()) { return QVariant(); } - Phrase * const phrase = m_unit->phraseList().at(index.row()); + std::shared_ptr const phrase = m_unit->phrases().at(index.row()); switch(role) { case Qt::DisplayRole: return !phrase->text().isEmpty()? QVariant(phrase->text()): QVariant(i18nc("@item:inlistbox:", "unknown")); case Qt::ToolTipRole: return QVariant(phrase->text()); case TextRole: return phrase->text(); case SoundFileRole: return phrase->sound(); case IdRole: return phrase->id(); case TypeRole: - return phrase->type(); - case ExcludedRole: - return phrase->isExcluded(); + return QVariant::fromValue(phrase->type()); +// case ExcludedRole: //FIXME +// return phrase->isExcluded(); case DataRole: - return QVariant::fromValue(phrase); + return QVariant::fromValue(phrase.get()); default: return QVariant(); } } int PhraseListModel::rowCount(const QModelIndex &parent) const { if (!m_unit) { return 0; } if (parent.isValid()) { return 0; } - return m_unit->phraseList().count(); + return m_unit->phrases().count(); } -void PhraseListModel::onPhraseAboutToBeAdded(Phrase *phrase, int index) +void PhraseListModel::onPhraseAboutToBeAdded(IPhrase *phrase, int index) { connect(phrase, SIGNAL(textChanged()), m_signalMapper, SLOT(map())); connect(phrase, SIGNAL(typeChanged()), m_signalMapper, SLOT(map())); connect(phrase, SIGNAL(excludedChanged()), m_signalMapper, SLOT(map())); beginInsertRows(QModelIndex(), index, index); } void PhraseListModel::onPhraseAdded() { updateMappings(); endInsertRows(); emit countChanged(); } void PhraseListModel::onPhrasesAboutToBeRemoved(int first, int last) { beginRemoveRows(QModelIndex(), first, last); } void PhraseListModel::onPhrasesRemoved() { endRemoveRows(); emit countChanged(); } void PhraseListModel::emitPhraseChanged(int row) { beginResetModel(); endResetModel(); //FIXME very inefficient, but workaround to force new filtering in phrasefiltermodel // to exclude possible new excluded phrases emit phraseChanged(row); emit dataChanged(index(row, 0), index(row, 0)); } QVariant PhraseListModel::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")); } int PhraseListModel::count() const { if (!m_unit) { return 0; } - return m_unit->phraseList().count(); + return m_unit->phrases().count(); } void PhraseListModel::updateMappings() { if (!m_unit) { return; } - int phrases = m_unit->phraseList().count(); + int phrases = m_unit->phrases().count(); for (int i = 0; i < phrases; ++i) { - m_signalMapper->setMapping(m_unit->phraseList().at(i), i); + m_signalMapper->setMapping(m_unit->phrases().at(i).get(), i); } } diff --git a/src/models/phraselistmodel.h b/src/models/phraselistmodel.h index 01bffc3..da50fa9 100644 --- a/src/models/phraselistmodel.h +++ b/src/models/phraselistmodel.h @@ -1,84 +1,84 @@ /* * 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 PHRASELISTMODEL_H #define PHRASELISTMODEL_H #include #include "core/phrase.h" class Unit; class QSignalMapper; class PhraseListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(Unit *unit READ unit WRITE setUnit NOTIFY unitChanged) Q_PROPERTY(int count READ count NOTIFY countChanged) public: enum phraseRoles { TextRole = Qt::UserRole + 1, IdRole, TypeRole, SoundFileRole, ExcludedRole, DataRole }; explicit PhraseListModel(QObject *parent = nullptr); /** * Reimplemented from QAbstractListModel::roleNames() */ virtual QHash roleNames() const override; void setUnit(Unit *unit); Unit * unit() 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; /** * Count phrases in current model view. If this number is changed, signal * countChanged() is emitted. * * \return number of phrases */ int count() const; Q_SIGNALS: void phraseChanged(int index); void unitChanged(); void typeChanged(); void countChanged(); private Q_SLOTS: - void onPhraseAboutToBeAdded(Phrase *unit, int index); + void onPhraseAboutToBeAdded(IPhrase *unit, int index); void onPhraseAdded(); void onPhrasesAboutToBeRemoved(int first, int last); void onPhrasesRemoved(); void emitPhraseChanged(int row); private: void updateMappings(); Unit *m_unit; QSignalMapper *m_signalMapper; }; #endif diff --git a/src/models/phrasemodel.cpp b/src/models/phrasemodel.cpp index ff0b3c1..a636a7e 100644 --- a/src/models/phrasemodel.cpp +++ b/src/models/phrasemodel.cpp @@ -1,351 +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); for (auto unit : m_course->units()) { unit->disconnect(this); - foreach (auto const &phrase, unit->phraseList()) { + for (auto &phrase : unit->phrases()) { 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 for (auto unit : m_course->units()) { // connect to phrase changes 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(); + int phrases = unit->phrases().count(); for (int i = 0; i < phrases; ++i) { - onPhraseAboutToBeAdded(unit->phraseList().at(i), i); + onPhraseAboutToBeAdded(unit->phrases().at(i).get(), 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->units().size() == 0) { return QVariant(); } auto unit = m_course->units().at(index.row()); switch(role) { case TextRole: return unit->title(); case DataRole: 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(); + return unit->phrases().at(index.row())->text(); case DataRole: - return QVariant::fromValue(unit->phraseList().at(index.row())); + return QVariant::fromValue(unit->phrases().at(index.row()).get()); 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->units().count(); } // internal pointer -> must be a phrase if (parent.internalPointer()) { return 0; } // else -> must be a unit Unit *unit = m_course->units().at(parent.row()).get(); - return unit->phraseList().count(); + return unit->phrases().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->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 auto unit = m_course->units().at(parent.row()); if (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); +// if (!phrase) { + return QModelIndex(); //FIXME +// } +// Unit *unit = phrase->unit(); +// return createIndex(unit->phrases().indexOf(phrase), 0, unit); } QModelIndex PhraseModel::indexUnit(Unit *unit) const { if (!unit || !m_course) { return QModelIndex(); } 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) +void PhraseModel::onPhraseAboutToBeAdded(IPhrase *phrase, int index) { 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)); + connect(phrase, &IPhrase::textChanged, m_phraseSignalMapper, static_cast(&QSignalMapper::map)); 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(std::shared_ptr unit, int index) { Q_UNUSED(unit) beginInsertRows(QModelIndex(), index, index); 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) { 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 +IPhrase * PhraseModel::phrase(const QModelIndex &index) const { if (index.internalPointer()) { Unit *unit = static_cast(index.internalPointer()); - return unit->phraseList().at(index.row()); + return unit->phrases().at(index.row()).get(); } - if (!m_course->units().at(index.row())->phraseList().isEmpty()) { - return m_course->units().at(index.row())->phraseList().first(); + if (!m_course->units().at(index.row())->phrases().isEmpty()) { + return m_course->units().at(index.row())->phrases().first().get(); } return nullptr; } Unit * PhraseModel::unit(const QModelIndex &index) const { return m_course->units().at(index.row()).get(); } void PhraseModel::updateUnitMappings() { int units = m_course->units().count(); for (int i = 0; i < units; ++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 for (auto unit : m_course->units()) { - foreach (Phrase *phrase, unit->phraseList()) { - m_phraseSignalMapper->setMapping(phrase, phrase); + for (const auto &phrase : unit->phrases()) { + m_phraseSignalMapper->setMapping(phrase.get(), phrase.get()); } } } diff --git a/src/models/phrasemodel.h b/src/models/phrasemodel.h index eff3b7b..e347a25 100644 --- a/src/models/phrasemodel.h +++ b/src/models/phrasemodel.h @@ -1,86 +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 IPhrase * 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 onPhraseAboutToBeAdded(IPhrase *phrase, int index); void onPhraseAdded(); void onPhrasesAboutToBeRemoved(int first, int last); void onPhrasesRemoved(); void onPhraseChanged(QObject *phrase); 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 ed2dbb1..a2bccd8 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->units().count()) { return QVariant(); } 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; - } + for (const auto &phrase : unit->phrases()) { +// if (phrase->editState() == Phrase::Completed) { //TODO introduce editablephrase +// return true; +// } } return false; case IdRole: return unit->id(); case DataRole: 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->units().count(); } void UnitModel::onUnitAboutToBeAdded(std::shared_ptr unit, int index) { 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->units().count(); for (int i = 0; i < units; i++) { m_signalMapper->setMapping(m_course->units().at(i).get(), i); } }