diff --git a/autotests/unittests/courseresource/test_courseresource.h b/autotests/unittests/courseresource/test_courseresource.h index e9cd0e3..d170fbc 100644 --- a/autotests/unittests/courseresource/test_courseresource.h +++ b/autotests/unittests/courseresource/test_courseresource.h @@ -1,70 +1,69 @@ /* * 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 TESTCOURSERESOURCE_H #define TESTCOURSERESOURCE_H #include #include class TestCourseResource : public QObject { Q_OBJECT public: TestCourseResource(); private slots: /** * @brief Called before every test case. */ void init(); /** * @brief Called after every test case. */ void cleanup(); /** * @brief Test simple loading of course resource XML file */ void loadCourseResource(); - /** * @brief Test simple loading of course resource XML file and skip all incomplete units/phrases */ void loadCourseResourceSkipIncomplete(); /** * @brief Test handling of unit insertions (specifically, the signals) */ void unitAddAndRemoveHandling(); /** * @brief Test of all course property changes except unit handling */ void coursePropertyChanges(); private: bool m_systemUseCourseRepositoryValue; }; #endif diff --git a/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp b/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp index 09ff9de..ddb3bd4 100644 --- a/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp +++ b/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp @@ -1,397 +1,397 @@ /* * 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 "../mocks/coursestub.h" #include "../mocks/languagestub.h" #include "core/language.h" #include "core/phrase.h" #include "core/resources/courseparser.h" #include "core/resources/editablecourseresource.h" #include "core/unit.h" #include "resourcerepositorystub.h" #include #include #include #include #include #include #include #include #include #include TestEditableCourseResource::TestEditableCourseResource() { qRegisterMetaType>("std::shared_ptr"); } 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); 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); 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->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 auto unit = Unit::create(); 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); } void TestEditableCourseResource::phraseAddAndRemoveHandling() { - //TODO simplify test by using empty course + // TODO simplify test by using empty course // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = EditableCourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); auto unit = Unit::create(); unit->setId("testunit"); course->addUnit(unit); auto testPhrase = Phrase::create(); testPhrase->setId("testphrase"); unit->addPhrase(testPhrase, 0); QCOMPARE(unit->phrases().count(), 1); QCOMPARE(testPhrase->unit()->id(), "testunit"); // begin of test { QSignalSpy spy(course.get(), &IEditableCourse::unitChanged); QVERIFY(course->createPhraseAfter(testPhrase.get())); QCOMPARE(spy.count(), 1); QCOMPARE(unit->phrases().count(), 2); } { QSignalSpy spy(course.get(), &IEditableCourse::unitChanged); QVERIFY(course->deletePhrase(testPhrase.get())); QCOMPARE(spy.count(), 1); QCOMPARE(unit->phrases().count(), 1); } } 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(), &CourseResource::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()); QCOMPARE(loadedCourse->units().count(), course->units().count()); auto testUnit = course->units().constFirst(); auto compareUnit = loadedCourse->units().constFirst(); QCOMPARE(compareUnit->id(), testUnit->id()); QCOMPARE(compareUnit->foreignId(), testUnit->foreignId()); QCOMPARE(compareUnit->title(), testUnit->title()); QCOMPARE(compareUnit->phrases().count(), testUnit->phrases().count()); 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 (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); auto unit = Unit::create(); 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 = Phrase::create(); importPhrase->setId("importPhraseId"); importPhrase->setText("phraseText"); importPhrase->setType(IPhrase::Type::Sentence); auto importUnit = Unit::create(); importUnit->setId("importId"); importUnit->addPhrase(importPhrase, importUnit->phrases().size()); 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->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 8793ed3..30b0b32 100644 --- a/autotests/unittests/editorsession/test_editorsession.cpp +++ b/autotests/unittests/editorsession/test_editorsession.cpp @@ -1,291 +1,291 @@ /* * 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 "../mocks/editablecoursestub.h" #include "../mocks/languagestub.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/trainingaction.h" #include "src/core/unit.h" #include -#include #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 = EditableCourseStub::create(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); } void TestEditorSession::nonSkeletonSwitchingBehavior() { auto languageGerman = std::make_shared("de"); auto languageEnglish = std::make_shared("en"); std::shared_ptr courseGerman = EditableCourseStub::create(languageGerman, QVector>()); courseGerman->setId("course-german"); std::shared_ptr courseEnglish = EditableCourseStub::create(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 = EditableCourseStub::create(languageGerman, QVector>()); courseGermanA->setId("course-german"); courseGermanA->setForeignId("testskeletonA"); std::shared_ptr courseGermanB = EditableCourseStub::create(languageGerman, QVector>()); courseGermanB->setId("course-german"); courseGermanB->setForeignId("testskeletonB"); std::shared_ptr courseEnglishA = EditableCourseStub::create(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.setCourse(courseGermanA.get()); 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.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 auto unitA = Unit::create(); auto unitB = Unit::create(); std::shared_ptr phraseA1 = Phrase::create(); std::shared_ptr phraseA2 = Phrase::create(); std::shared_ptr phraseB1 = Phrase::create(); std::shared_ptr phraseB2 = Phrase::create(); // note: phrases without soundfiles are skipped in session generation phraseA1->setId("A1"); phraseA2->setId("A2"); phraseB1->setId("B1"); phraseB2->setId("B2"); phraseA1->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseA2->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseB1->setSound(QUrl::fromLocalFile("/tmp/b1.ogg")); phraseB2->setSound(QUrl::fromLocalFile("/tmp/b2.ogg")); unitA->addPhrase(phraseA1, unitA->phrases().size()); unitA->addPhrase(phraseA2, unitA->phrases().size()); unitB->addPhrase(phraseB1, unitB->phrases().size()); unitB->addPhrase(phraseB2, unitB->phrases().size()); auto course = EditableCourseStub::create(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()->self(), phraseA1); QVERIFY(course.get() == session.course()); // test direct unit setters session.setActiveUnit(unitA.get()); QCOMPARE(session.activeUnit()->self(), unitA); session.setActiveUnit(unitB.get()); QCOMPARE(session.activeUnit()->self(), unitB); // test direct phrase setters session.setActivePhrase(phraseA1.get()); QCOMPARE(session.activePhrase()->self(), phraseA1); QCOMPARE(session.activeUnit()->self(), unitA); session.setActivePhrase(phraseB1.get()); QCOMPARE(session.activePhrase()->self(), phraseB1); QCOMPARE(session.activeUnit()->self(), unitB); // test phrase forward iterators session.setActivePhrase(phraseA1.get()); QCOMPARE(session.activeUnit()->self(), unitA); QCOMPARE(session.activePhrase()->id(), phraseA1->id()); QVERIFY(session.hasNextPhrase()); session.switchToNextPhrase(); QCOMPARE(session.activeUnit()->self(), unitA); QCOMPARE(session.activePhrase()->id(), phraseA2->id()); session.switchToNextPhrase(); QCOMPARE(session.activePhrase()->self(), phraseB1); session.switchToNextPhrase(); QCOMPARE(session.activePhrase()->self(), phraseB2); QVERIFY(!session.hasNextPhrase()); // at the end, do not iterate further session.switchToNextPhrase(); QCOMPARE(session.activePhrase()->self(), phraseB2); // test phrase backward iterators QVERIFY(session.hasPreviousPhrase()); session.switchToPreviousPhrase(); QCOMPARE(session.activePhrase()->self(), 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()); } void TestEditorSession::updateActionsBehavior() { // test preparation auto language = std::make_shared("de"); auto unitA = Unit::create(); auto unitB = Unit::create(); unitA->setTitle("titleA"); unitB->setTitle("titleB"); std::shared_ptr phraseA1 = Phrase::create(); std::shared_ptr phraseA2 = Phrase::create(); std::shared_ptr phraseB1 = Phrase::create(); std::shared_ptr phraseB2 = Phrase::create(); phraseA1->setId("A1"); phraseA2->setId("A2"); phraseB1->setId("B1"); phraseB2->setId("B2"); phraseA1->setText("A1"); phraseA2->setText("A2"); phraseB1->setText("B1"); phraseB2->setText("B2"); unitA->addPhrase(phraseA1, unitA->phrases().size()); unitA->addPhrase(phraseA2, unitA->phrases().size()); unitB->addPhrase(phraseB1, unitB->phrases().size()); unitB->addPhrase(phraseB2, unitB->phrases().size()); auto course = EditableCourseStub::create(language, QVector>({unitA, unitB})); EditableRepositoryStub repository { {language}, // languages {}, // skeletons {course} // courses }; EditorSession session; session.setRepository(&repository); QVERIFY(session.activeAction() == nullptr); QCOMPARE(session.trainingActions().count(), 0); { QSignalSpy spy(&session, &EditorSession::actionsChanged); session.setCourse(course.get()); // setting the course shall trigger the action update QVERIFY(spy.count() > 0); } QVERIFY(session.activeAction() != nullptr); QCOMPARE(session.trainingActions().count(), 2); // 2 units QCOMPARE(session.trainingActions().at(0)->text(), "titleA"); QCOMPARE(session.trainingActions().at(1)->text(), "titleB"); QCOMPARE(session.trainingActions().at(0)->children().count(), 2); QCOMPARE(session.trainingActions().at(1)->children().count(), 2); - auto phraseA1Object = qobject_cast(session.trainingActions().at(0)->children().first()); + auto phraseA1Object = qobject_cast(session.trainingActions().at(0)->children().first()); QVERIFY(phraseA1Object != nullptr); QCOMPARE(phraseA1Object->text(), phraseA1->text()); // test update of unit { qDebug() << "CHECK " << session.trainingActions().last(); QCOMPARE(session.trainingActions().last()->text(), unitB->title()); // ensure that correct action is selected QSignalSpy spy(session.trainingActions().last(), &TrainingAction::actionsChanged); unitB->removePhrase(phraseB1->self()); // note: event has to be trigger explicitly course->triggerUnitChanged(unitB); QVERIFY(spy.count() > 0); } } QTEST_GUILESS_MAIN(TestEditorSession) diff --git a/autotests/unittests/unit/test_unit.cpp b/autotests/unittests/unit/test_unit.cpp index c8c787d..412df8f 100644 --- a/autotests/unittests/unit/test_unit.cpp +++ b/autotests/unittests/unit/test_unit.cpp @@ -1,123 +1,122 @@ /* * 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_unit.h" #include "../mocks/languagestub.h" #include "core/language.h" #include "core/phonemegroup.h" #include "core/phrase.h" #include "core/unit.h" #include "resourcerepositorystub.h" #include #include TestUnit::TestUnit() = default; void TestUnit::init() { qRegisterMetaType>("std::shared_ptr"); } void TestUnit::cleanup() { } void TestUnit::unitPropertyChanges() { auto unit = Unit::create(); QVERIFY(unit); { QSignalSpy spy(unit.get(), &Unit::idChanged); QString id("testId"); unit->setId(id); QCOMPARE(unit->id(), id); QCOMPARE(spy.count(), 1); } { QString foreignId("foreignId"); unit->setForeignId(foreignId); QCOMPARE(unit->foreignId(), foreignId); } { QSignalSpy spy(unit.get(), &Unit::titleChanged); QString title("title"); unit->setTitle(title); QCOMPARE(unit->title(), title); QCOMPARE(spy.count(), 1); } } void TestUnit::addAndRemovePhrases() { auto unit = Unit::create(); - QString unitId{"unitId"}; + QString unitId {"unitId"}; unit->setId(unitId); QVERIFY(unit); auto phraseA = Phrase::create(); - QString phraseIdA{"phraseIdA"}; + QString phraseIdA {"phraseIdA"}; phraseA->setId(phraseIdA); auto phraseB = Phrase::create(); - QString phraseIdB{"phraseIdB"}; + QString phraseIdB {"phraseIdB"}; phraseB->setId(phraseIdB); - { QSignalSpy aboutSpy(unit.get(), &Unit::phraseAboutToBeAdded); QSignalSpy addedSpy(unit.get(), &Unit::phraseAdded); QCOMPARE(unit->phrases().count(), 0); unit->addPhrase(phraseA, 0); QCOMPARE(unit->phrases().count(), 1); QCOMPARE(aboutSpy.count(), 1); auto parameters = aboutSpy.takeFirst(); // TODO check object parameter QCOMPARE(parameters.at(1).toInt(), 0); QCOMPARE(addedSpy.count(), 1); QCOMPARE(phraseA->unit()->id(), unitId); } { // prepend second phrase QSignalSpy aboutSpy(unit.get(), &Unit::phraseAboutToBeAdded); QSignalSpy addedSpy(unit.get(), &Unit::phraseAdded); QCOMPARE(unit->phrases().count(), 1); unit->addPhrase(phraseB, 0); QCOMPARE(unit->phrases().count(), 2); QCOMPARE(aboutSpy.count(), 1); auto parameters = aboutSpy.takeFirst(); // TODO check object parameter QCOMPARE(parameters.at(1).toInt(), 0); QCOMPARE(addedSpy.count(), 1); QCOMPARE(phraseB->unit()->id(), unitId); } { QSignalSpy aboutSpy(unit.get(), &Unit::phraseAboutToBeRemoved); QSignalSpy removedSpy(unit.get(), &Unit::phraseRemoved); QCOMPARE(unit->phrases().count(), 2); unit->removePhrase(phraseA); QCOMPARE(unit->phrases().count(), 1); QCOMPARE(aboutSpy.count(), 1); QCOMPARE(removedSpy.count(), 1); } } QTEST_GUILESS_MAIN(TestUnit) diff --git a/src/core/editorsession.cpp b/src/core/editorsession.cpp index 810174f..bc26413 100644 --- a/src/core/editorsession.cpp +++ b/src/core/editorsession.cpp @@ -1,285 +1,285 @@ /* * 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 "artikulate_debug.h" #include "core/contributorrepository.h" #include "core/iunit.h" #include "core/language.h" #include "core/phrase.h" #include "core/resources/editablecourseresource.h" #include "core/resources/skeletonresource.h" #include "core/trainingaction.h" #include "core/unit.h" EditorSession::EditorSession(QObject *parent) : ISessionActions(parent) { connect(this, &EditorSession::courseChanged, this, &EditorSession::skeletonModeChanged); } void EditorSession::setRepository(IEditableRepository *repository) { m_repository = repository; } bool EditorSession::skeletonMode() const { for (const auto &skeleton : m_repository->skeletons()) { if (skeleton->id() == m_course->id()) { return true; } } return false; } ILanguage *EditorSession::language() const { if (m_course && m_course->language()) { return m_course->language().get(); } return nullptr; } IEditableCourse *EditorSession::course() const { return m_course; } void EditorSession::setCourse(IEditableCourse *course) { if (m_course == course) { return; } m_course = course; connect(course, &IEditableCourse::unitChanged, this, [=](std::shared_ptr unit) { this->updateActions(unit); - emit actionsChanged(); //TODO much too global effect + emit actionsChanged(); // TODO much too global effect }); updateTrainingActions(); if (m_course && m_course->units().count() > 0) { setActiveUnit(m_course->units().first().get()); } emit languageChanged(); emit courseChanged(); } IUnit *EditorSession::activeUnit() const { if (auto phrase = activePhrase()) { return phrase->unit().get(); } return nullptr; } void EditorSession::setActiveUnit(IUnit *unit) { // checking phrases in increasing order ensures that always the first phrase is selected for (int i = 0; i < m_actions.count(); ++i) { for (int j = 0; j < m_actions.at(i)->actions().count(); ++j) { const auto testPhrase = qobject_cast(m_actions.at(i)->actions().at(j))->phrase(); if (unit == testPhrase->unit().get()) { if (auto action = activeAction()) { action->setChecked(false); } m_indexUnit = i; m_indexPhrase = j; if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); return; } } } } void EditorSession::setActivePhrase(IPhrase *phrase) { for (int i = 0; i < m_actions.count(); ++i) { for (int j = 0; j < m_actions.at(i)->actions().count(); ++j) { const auto testPhrase = qobject_cast(m_actions.at(i)->actions().at(j))->phrase(); if (phrase == testPhrase) { if (auto action = activeAction()) { action->setChecked(false); } m_indexUnit = i; m_indexPhrase = j; if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); return; } } } } IPhrase *EditorSession::activePhrase() const { if (const auto action = activeAction()) { return action->phrase(); } return nullptr; } void EditorSession::switchToPreviousPhrase() { if (hasPreviousPhrase()) { if (m_indexPhrase == 0) { qCDebug(ARTIKULATE_CORE()) << "switching to previous unit"; if (m_indexUnit > 0) { --m_indexUnit; m_indexPhrase = m_actions.at(m_indexUnit)->actions().count() - 1; } } else { --m_indexPhrase; } if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); } else { qCWarning(ARTIKULATE_CORE()) << "The is no previous phrase, aborting"; } } void EditorSession::switchToNextPhrase() { if (hasNextPhrase()) { if (m_indexPhrase >= m_actions.at(m_indexUnit)->actions().count() - 1) { qCDebug(ARTIKULATE_CORE()) << "switching to next unit"; if (m_indexUnit < m_actions.count() - 1) { ++m_indexUnit; m_indexPhrase = 0; } } else { ++m_indexPhrase; } if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); } else { qCWarning(ARTIKULATE_CORE()) << "The is no next phrase, aborting"; } } bool EditorSession::hasPreviousPhrase() const { return m_indexUnit > 0 || m_indexPhrase > 0; } bool EditorSession::hasNextPhrase() 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 EditorSession::updateCourseFromSkeleton() { if (!m_course) { qCritical() << "Not updating course from skeleton, no one set."; return; } m_repository->updateCourseFromSkeleton(m_course->self()); } TrainingAction *EditorSession::activeAction() const { if (m_indexUnit < 0 || m_indexPhrase < 0) { return nullptr; } return qobject_cast(m_actions.at(m_indexUnit)->actions().at(m_indexPhrase)); } void EditorSession::updateTrainingActions() { for (const auto &action : qAsConst(m_actions)) { action->clearActions(); action->deleteLater(); } m_actions.clear(); if (!m_course) { m_indexUnit = -1; m_indexPhrase = -1; return; } const auto unitList = m_course->units(); for (const auto &unit : qAsConst(unitList)) { auto action = new TrainingAction(unit->title(), this); const auto phraseList = unit->phrases(); for (const auto &phrase : qAsConst(phraseList)) { action->appendAction(new TrainingAction(phrase, this, unit.get())); } if (action->actions().count() > 0) { m_actions.append(action); } else { action->deleteLater(); } } // update indices m_indexUnit = -1; m_indexPhrase = -1; if (m_course->units().count() > 0) { m_indexUnit = 0; if (m_course->units().constFirst()->phrases().count() > 0) { m_indexPhrase = 0; } } emit actionsChanged(); } void EditorSession::updateActions(std::shared_ptr changedUnit) { int unitIndex = -1; for (int i = 0; i < m_course->units().size(); ++i) { if (changedUnit == m_course->units().at(i)) { unitIndex = i; break; } } Q_ASSERT(unitIndex >= 0); auto unitAction = m_actions.at(unitIndex); // TODO this is a heavy operation if only one phrase was changed unitAction->clearActions(); for (int i = 0; i < changedUnit->phrases().size(); ++i) { unitAction->appendAction(new TrainingAction(changedUnit->phrases().at(i), this, changedUnit.get())); } emit unitAction->actionsChanged(); } QVector EditorSession::trainingActions() const { return m_actions; } diff --git a/src/core/resources/courseparser.cpp b/src/core/resources/courseparser.cpp index 83afa5a..01f1973 100644 --- a/src/core/resources/courseparser.cpp +++ b/src/core/resources/courseparser.cpp @@ -1,412 +1,412 @@ /* * 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 "artikulate_debug.h" #include "core/ieditablecourse.h" #include "core/language.h" #include "core/phoneme.h" #include "core/phrase.h" #include "core/unit.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, bool skipIncomplete) { 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, skipIncomplete, 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::shared_ptr CourseParser::parseUnit(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool skipIncomplete, bool &ok) { std::shared_ptr unit = Unit::create(); 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 && (!skipIncomplete || !phrase->soundFileUrl().isEmpty())) { unit->addPhrase(phrase, unit->phrases().size()); } 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; } std::shared_ptr CourseParser::parsePhrase(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, bool &ok) { 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") { QString fileName = parseElement(xml, elementOk); if (!fileName.isEmpty()) { phrase->setSound(QUrl::fromLocalFile(path.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path() + '/' + fileName)); } 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(IPhrase::Type::Word); } else if (type == "expression") { phrase->setType(IPhrase::Type::Expression); } else if (type == "sentence") { phrase->setType(IPhrase::Type::Sentence); } else if (type == "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(); + // qCDebug(ARTIKULATE_PARSER()) << "parsed: " << elementName << " / " << xml.text().toString(); return xml.text().toString(); } QDomDocument CourseParser::serializedDocument(std::shared_ptr 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 (auto &phrase : unit->phrases()) { if (trainingExport && phrase->soundFileUrl().isEmpty()) { continue; } unitPhraseListElement.appendChild(serializedPhrase(std::static_pointer_cast(phrase), document)); } 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(std::shared_ptr 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 for (auto &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); return phraseElement; } bool CourseParser::exportCourseToGhnsPackage(std::shared_ptr 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->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/trainingaction.cpp b/src/core/trainingaction.cpp index e957a8c..1d2e5e6 100644 --- a/src/core/trainingaction.cpp +++ b/src/core/trainingaction.cpp @@ -1,193 +1,196 @@ /* * 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 "drawertrainingactions.h" #include "trainingactionicon.h" #include "trainingsession.h" #include TrainingAction::TrainingAction(QObject *parent) : QAbstractListModel(parent) , m_text(QString()) , m_icon(nullptr, QString()) // TODO "rating-unrated" vs. "rating" { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); } TrainingAction::TrainingAction(const QString &text, QObject *parent) : QAbstractListModel(parent) , m_text(text) , m_icon(nullptr, QString()) // TODO "rating-unrated" vs. "rating" { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); } TrainingAction::TrainingAction(std::shared_ptr phrase, ISessionActions *session, QObject *parent) : QAbstractListModel(parent) , m_icon(nullptr, QString()) , m_phrase(phrase) , m_session(session) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); if (m_phrase) { m_text = phrase->text(); } - connect(phrase.get(), &IPhrase::textChanged, this, [=](){ + connect(phrase.get(), &IPhrase::textChanged, this, [=]() { m_text = phrase->text(); emit textChanged(m_text); }); } QHash TrainingAction::roleNames() const { QHash roles; roles[ModelDataRole] = "modelData"; return roles; } int TrainingAction::columnCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return 1; } int TrainingAction::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return actionsCount(); } QVariant TrainingAction::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= actionsCount()) { return QVariant(); } switch (role) { - case ModelDataRole: return QVariant::fromValue(m_actions.at(index.row())); - case Qt::DisplayRole: return m_text; - default: return QVariant(); + case ModelDataRole: + return QVariant::fromValue(m_actions.at(index.row())); + case Qt::DisplayRole: + return m_text; + default: + return QVariant(); } } void TrainingAction::trigger() { if (m_phrase && m_session) { m_session->setActivePhrase(m_phrase.get()); } } bool TrainingAction::enabled() const { return m_enabled; } void TrainingAction::setEnabled(bool enabled) { if (enabled == m_enabled) { return; } m_enabled = enabled; emit enabledChanged(m_enabled); } QString TrainingAction::text() const { return m_text; } void TrainingAction::setText(QString text) { if (text == m_text) { return; } m_text = std::move(text); emit textChanged(m_text); } 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() +QObject *TrainingAction::icon() { - return qobject_cast(&m_icon); + return qobject_cast(&m_icon); } -IPhrase * TrainingAction::phrase() const +IPhrase *TrainingAction::phrase() const { return m_phrase.get(); } QVector TrainingAction::actions() const { return m_actions; } -QAbstractListModel * TrainingAction::actionModel() +QAbstractListModel *TrainingAction::actionModel() { return this; } int TrainingAction::actionsCount() const { return m_actions.count(); } -TrainingAction * TrainingAction::action(int index) const +TrainingAction *TrainingAction::action(int index) const { if (index < 0 || index >= m_actions.count()) { qWarning() << "index not in range, aborting"; return nullptr; } return m_actions.at(index); } void TrainingAction::appendAction(TrainingAction *action) { beginInsertRows(QModelIndex(), m_actions.count(), m_actions.count()); m_actions.append(action); endInsertRows(); emit actionsChanged(); } void TrainingAction::clearActions() { beginResetModel(); m_actions.clear(); qDeleteAll(m_actions); endResetModel(); emit actionsChanged(); } diff --git a/src/core/trainingaction.h b/src/core/trainingaction.h index aeb619b..e618ccd 100644 --- a/src/core/trainingaction.h +++ b/src/core/trainingaction.h @@ -1,96 +1,97 @@ /* * 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 "iphrase.h" #include "trainingactionicon.h" #include "trainingsession.h" -#include #include +#include #include class DrawerTrainingActions; class ARTIKULATECORE_EXPORT TrainingAction : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) Q_PROPERTY(QString title READ text WRITE setText NOTIFY textChanged) 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(QAbstractItemModel * children READ actionModel NOTIFY actionsChanged) + Q_PROPERTY(QAbstractItemModel *children READ actionModel NOTIFY actionsChanged) Q_PROPERTY(bool checkable MEMBER m_checkable CONSTANT) Q_PROPERTY(int length READ actionsCount NOTIFY actionsChanged) public: - enum ModelRoles { - ModelDataRole = Qt::UserRole + 1 - }; + enum ModelRoles { ModelDataRole = Qt::UserRole + 1 }; TrainingAction(QObject *parent = nullptr); TrainingAction(const QString &text, QObject *parent = nullptr); TrainingAction(std::shared_ptr phrase, ISessionActions *session, QObject *parent = nullptr); Q_INVOKABLE void trigger(); bool enabled() const; void setEnabled(bool enabled); QString text() const; void setText(QString text); void setChecked(bool checked); bool checked() const; QObject *icon(); IPhrase *phrase() const; - QAbstractListModel * actionModel(); - QVector actions() const; + QAbstractListModel *actionModel(); + QVector actions() const; int actionsCount() const; - bool hasActions() { return m_actions.count() > 0; } - TrainingAction * action(int index) const; + bool hasActions() + { + return m_actions.count() > 0; + } + TrainingAction *action(int index) const; void appendAction(TrainingAction *action); void clearActions(); QHash roleNames() const override; int columnCount(const QModelIndex &parent) const override; int rowCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; Q_SIGNALS: void actionsChanged(); void enabledChanged(bool enabled); void checkedChanged(bool checked); void textChanged(QString text); private: QVector m_actions; QString m_text; TrainingActionIcon m_icon; bool m_visible {true}; bool m_enabled {true}; bool m_checked {false}; bool m_checkable {false}; QString m_tooltip {QString()}; std::shared_ptr m_phrase; ISessionActions *m_session {nullptr}; }; #endif diff --git a/src/core/unit.cpp b/src/core/unit.cpp index 52b60eb..d6be53c 100644 --- a/src/core/unit.cpp +++ b/src/core/unit.cpp @@ -1,166 +1,166 @@ /* * 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 "artikulate_debug.h" #include #include Unit::Unit(QObject *parent) : IEditableUnit(parent) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); } Unit::~Unit() = default; std::shared_ptr Unit::create() { std::shared_ptr unit(new Unit); unit->setSelf(unit); std::weak_ptr unitParameter = unit; - connect(unit.get(), &IUnit::phraseAdded, unit.get(), [unitParameter](){ + connect(unit.get(), &IUnit::phraseAdded, unit.get(), [unitParameter]() { if (auto unit = unitParameter.lock()) { unit->emitPhrasesChanged(unit); } }); - connect(unit.get(), &IUnit::phraseRemoved, unit.get(), [unitParameter](){ + connect(unit.get(), &IUnit::phraseRemoved, unit.get(), [unitParameter]() { if (auto unit = unitParameter.lock()) { unit->emitPhrasesChanged(unit); } }); return unit; } void Unit::setSelf(std::shared_ptr self) { m_self = self; } std::shared_ptr Unit::self() const { return m_self.lock(); } 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; } std::shared_ptr Unit::course() const { return m_course.lock(); } void Unit::setCourse(std::shared_ptr course) { if (course == m_course.lock()) { 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(); } } QVector> Unit::phrases() const { return m_phrases; } void Unit::addPhrase(std::shared_ptr phrase, int index) { 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(m_self.lock()); emit phraseAboutToBeAdded(phrase, index); m_phrases.insert(index, phrase); qDebug() << "append phrase to unit" << index << id() << phrase->id(); qDebug() << "new count" << m_phrases.count(); emit phraseAdded(phrase); connect(phrase.get(), &Phrase::modified, this, &Unit::modified); emit modified(); } void Unit::removePhrase(std::shared_ptr phrase) { int index = -1; for (int i = 0; i < m_phrases.count(); ++i) { if (m_phrases.at(i)->id() == phrase->id()) { index = i; break; } } Q_ASSERT(index >= 0); emit phraseAboutToBeRemoved(index); m_phrases.removeAt(index); emit phraseRemoved(); } void Unit::emitPhrasesChanged(std::shared_ptr unit) { emit phrasesChanged(unit); }