diff --git a/autotests/mocks/editablecoursestub.h b/autotests/mocks/editablecoursestub.h index 809f913..536dbde 100644 --- a/autotests/mocks/editablecoursestub.h +++ b/autotests/mocks/editablecoursestub.h @@ -1,162 +1,178 @@ /* * 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 EDITABLECOURSESTUB_H #define EDITABLECOURSESTUB_H #include "src/core/ieditablecourse.h" #include "src/core/ilanguage.h" #include "src/core/unit.h" #include class EditableCourseStub : public IEditableCourse { public: EditableCourseStub(std::shared_ptr language, QVector> units) : IEditableCourse() , m_language(language) , m_units(units) { } ~EditableCourseStub() override; - static std::shared_ptr create(std::shared_ptr language, QVector> units) + static std::shared_ptr create(std::shared_ptr language, QVector> units) { auto course = std::make_shared(language, units); course->setSelf(course); for (auto &unit : units) { unit->setCourse(course); } - return std::static_pointer_cast(course); + return course; } void setSelf(std::shared_ptr self) override { m_self = self; } std::shared_ptr self() const override { return std::static_pointer_cast(m_self.lock()); } QString id() const override { return m_id; } void setId(QString id) override { m_id = id; emit idChanged(); } QString foreignId() const override { return m_foreignId; } void setForeignId(QString id) override { m_foreignId = id; } QString title() const override { return m_title; } void setTitle(QString title) override { m_title = title; emit titleChanged(); } QString i18nTitle() const override { return m_i18nTitle; } void setI18nTitle(QString title) override { m_i18nTitle = title; } QString description() const override { return m_description; } void setDescription(QString description) override { m_description = description; emit descriptionChanged(); } std::shared_ptr language() const override { return m_language; } QString languageTitle() const override { if (m_language) { m_language->title(); } return QString(); } void setLanguage(std::shared_ptr language) override { m_language = language; emit languageChanged(); } QVector> units() override { return m_units; } std::shared_ptr addUnit(std::shared_ptr unit) override { m_units.append(std::move(unit)); auto unitPtr = m_units.last(); unitPtr->setCourse(self()); return unitPtr; } + bool createPhraseAfter(IPhrase *previousPhrase) override + { + Q_UNUSED(previousPhrase) + // not implemented + return false; + } + bool deletePhrase(IPhrase *phrase) override + { + Q_UNUSED(phrase) + // not implemented + return false; + } QUrl file() const override { return QUrl(); } bool sync() override { return false; } void updateFrom(std::shared_ptr) override { // not implemented } bool isModified() const override { return false; } bool exportToFile(const QUrl &) const override { // do nothing return false; } + void triggerUnitChanged(std::shared_ptr unit) + { + emit unitChanged(unit); + } private: std::weak_ptr m_self; QString m_id {"courseid"}; QString m_foreignId {"foreigncourseid"}; QString m_title {"title"}; QString m_i18nTitle {"i18n title"}; QString m_description {"description of the course"}; std::shared_ptr m_language; QVector> m_units; }; #endif diff --git a/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp b/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp index a2a9f25..09ff9de 100644 --- a/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp +++ b/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp @@ -1,363 +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 + + // 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(), SIGNAL(idChanged())); + 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->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/editablecourseresource/test_editablecourseresource.h b/autotests/unittests/editablecourseresource/test_editablecourseresource.h index a9debb2..fdecbb5 100644 --- a/autotests/unittests/editablecourseresource/test_editablecourseresource.h +++ b/autotests/unittests/editablecourseresource/test_editablecourseresource.h @@ -1,76 +1,81 @@ /* * 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 TESTEDITABLECOURSERESOURCE_H #define TESTEDITABLECOURSERESOURCE_H #include #include class TestEditableCourseResource : public QObject { Q_OBJECT public: TestEditableCourseResource(); private Q_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 handling of unit insertions (specifically, the signals) */ void unitAddAndRemoveHandling(); + /** + * @brief Test handling of phrase insertions (specifically, the signals) + */ + void phraseAddAndRemoveHandling(); + /** * @brief Test of all course property changes except unit handling */ void coursePropertyChanges(); /** * Test if serialization of unserialized file gives original file. */ void fileLoadSaveCompleteness(); /** * Test if the modified status is correctly set. */ void modifiedStatus(); /** * Test correct update of course from a skeleton */ void skeletonUpdate(); }; #endif diff --git a/autotests/unittests/editorsession/test_editorsession.cpp b/autotests/unittests/editorsession/test_editorsession.cpp index d33477f..8793ed3 100644 --- a/autotests/unittests/editorsession/test_editorsession.cpp +++ b/autotests/unittests/editorsession/test_editorsession.cpp @@ -1,224 +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 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->addPhrase(phraseA2); - unitB->addPhrase(phraseB1); - unitB->addPhrase(phraseB2); + 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()); + 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/editorsession/test_editorsession.h b/autotests/unittests/editorsession/test_editorsession.h index 6d12c9f..078761d 100644 --- a/autotests/unittests/editorsession/test_editorsession.h +++ b/autotests/unittests/editorsession/test_editorsession.h @@ -1,65 +1,70 @@ /* * 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 TEST_EDITORSESSION_H #define TEST_EDITORSESSION_H #include class TestEditorSession : public QObject { Q_OBJECT public: TestEditorSession() = default; private Q_SLOTS: /** * Called before every test case. */ void init(); /** * Called after every test case. */ void cleanup(); /** * @brief Construct and destruct editor session and test initial values */ void createEditorSession(); /** * @brief Test switching behavior for courses without skeleton. */ void nonSkeletonSwitchingBehavior(); /** * @brief Test handling of skeletons and respective course switching */ void skeletonSwitchingBehavior(); /** * @brief Test for all iterator functionality */ void iterateCourse(); + + /** + * @brief Test that the actions are correctly updated when course changes; + */ + void updateActionsBehavior(); }; #endif diff --git a/autotests/unittests/trainingsession/test_trainingsession.cpp b/autotests/unittests/trainingsession/test_trainingsession.cpp index d21cd84..dc1fe3b 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 "../mocks/coursestub.h" #include "../mocks/languagestub.h" #include "liblearnerprofile/src/profilemanager.h" #include "src/core/icourse.h" #include "src/core/language.h" #include "src/core/trainingaction.h" #include "src/core/trainingsession.h" #include "src/core/unit.h" #include #include // assumption: during a training session the units and phrases of a course do not change // any change of such a course shall result in a reload of a training session void TestTrainingSession::init() { // no initialization of test case } void TestTrainingSession::cleanup() { // no cleanup after test run } void TestTrainingSession::createTrainingSessionWithoutUnits() { auto language = std::make_shared("de"); CourseStub course(language, QVector>()); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::createTrainingSessionWithEmptySounds() { auto language = std::make_shared("de"); auto unitA = Unit::create(); auto unitB = Unit::create(); std::shared_ptr phraseA1 = Phrase::create(); std::shared_ptr phraseA2 = Phrase::create(); std::shared_ptr phraseB1 = Phrase::create(); std::shared_ptr phraseB2 = Phrase::create(); // note: phrases without soundfiles are skipped in session generation phraseA1->setId("A1"); phraseA2->setId("A2"); phraseB1->setId("B1"); phraseB2->setId("B2"); phraseA1->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); - unitA->addPhrase(phraseA1); - unitA->addPhrase(phraseA2); - unitB->addPhrase(phraseB1); - unitB->addPhrase(phraseB2); + unitA->addPhrase(phraseA1, unitA->phrases().size()); + unitA->addPhrase(phraseA2, unitA->phrases().size()); + unitB->addPhrase(phraseB1, unitB->phrases().size()); + unitB->addPhrase(phraseB2, unitB->phrases().size()); CourseStub course(language, QVector>({unitA, unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); // test number of actions auto actions = session.trainingActions(); QCOMPARE(actions.count(), 1); QCOMPARE(actions.at(0)->actions().count(), 1); } void TestTrainingSession::createTrainingSessionWithEmptyUnits() { auto language = std::make_shared("de"); auto unitA = Unit::create(); auto unitB = Unit::create(); CourseStub course(language, QVector>({unitA, unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::createTrainingSessionWithUnitsAndPhrases() { auto language = std::make_shared("de"); auto unit = Unit::create(); std::shared_ptr firstPhrase = Phrase::create(); std::shared_ptr secondPhrase = Phrase::create(); - unit->addPhrase(firstPhrase); - unit->addPhrase(secondPhrase); + unit->addPhrase(firstPhrase, unit->phrases().size()); + unit->addPhrase(secondPhrase, unit->phrases().size()); CourseStub course(language, QVector>({unit})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::iterateCourse() { auto language = std::make_shared("de"); auto unitA = Unit::create(); auto unitB = Unit::create(); std::shared_ptr phraseA1 = Phrase::create(); std::shared_ptr phraseA2 = Phrase::create(); std::shared_ptr phraseB1 = Phrase::create(); std::shared_ptr phraseB2 = Phrase::create(); // note: phrases without soundfiles are skipped in session generation phraseA1->setId("A1"); phraseA2->setId("A2"); phraseB1->setId("B1"); phraseB2->setId("B2"); phraseA1->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseA2->setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseB1->setSound(QUrl::fromLocalFile("/tmp/b1.ogg")); phraseB2->setSound(QUrl::fromLocalFile("/tmp/b2.ogg")); - unitA->addPhrase(phraseA1); - unitA->addPhrase(phraseA2); - unitB->addPhrase(phraseB1); - unitB->addPhrase(phraseB2); + unitA->addPhrase(phraseA1, unitA->phrases().size()); + unitA->addPhrase(phraseA2, unitA->phrases().size()); + unitB->addPhrase(phraseB1, unitB->phrases().size()); + unitB->addPhrase(phraseB2, unitB->phrases().size()); CourseStub course(language, QVector>({unitA, unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); // session assumed to initialize with first units's first phrase QCOMPARE(session.activeUnit()->self(), unitA); QCOMPARE(session.activePhrase()->self(), phraseA1); QVERIFY(&course == session.course()); // test direct unit setters session.setUnit(unitA.get()); QCOMPARE(session.activeUnit(), unitA.get()); session.setUnit(unitB.get()); QCOMPARE(session.activeUnit(), unitB.get()); // test direct phrase setters session.setActivePhrase(phraseA1.get()); QCOMPARE(session.activePhrase(), phraseA1.get()); QCOMPARE(session.activeUnit(), unitA.get()); session.setActivePhrase(phraseB1.get()); QCOMPARE(session.activePhrase(), phraseB1.get()); QCOMPARE(session.activeUnit(), unitB.get()); // test number of actions auto actions = session.trainingActions(); QCOMPARE(actions.count(), 2); QCOMPARE(actions.at(0)->actions().count(), 2); QCOMPARE(actions.at(1)->actions().count(), 2); // test phrase iterators: accept iterator session.setActivePhrase(phraseA1.get()); QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA1.get()); QVERIFY(session.hasNext()); session.accept(); QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA2.get()); session.accept(); QCOMPARE(session.activePhrase(), phraseB1.get()); session.accept(); QCOMPARE(session.activePhrase(), phraseB2.get()); QVERIFY(!session.hasNext()); // test phrase iterators: skip iterator session.setActivePhrase(phraseA1.get()); QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA1.get()); QVERIFY(!session.hasPrevious()); QVERIFY(session.hasNext()); session.skip(); QCOMPARE(session.activeUnit(), unitA.get()); QCOMPARE(session.activePhrase(), phraseA2.get()); session.skip(); QCOMPARE(session.activePhrase(), phraseB1.get()); session.skip(); QCOMPARE(session.activePhrase(), phraseB2.get()); QVERIFY(session.hasPrevious()); QVERIFY(!session.hasNext()); // test completed signal QSignalSpy spy(&session, SIGNAL(completed())); session.setActivePhrase(phraseB1.get()); session.accept(); QCOMPARE(spy.count(), 0); session.accept(); QCOMPARE(spy.count(), 1); } QTEST_GUILESS_MAIN(TestTrainingSession) diff --git a/src/core/drawertrainingactions.cpp b/src/core/drawertrainingactions.cpp index 7f9b3af..e05fd60 100644 --- a/src/core/drawertrainingactions.cpp +++ b/src/core/drawertrainingactions.cpp @@ -1,70 +1,72 @@ /* * 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 "drawertrainingactions.h" #include "trainingaction.h" #include #include #include DrawerTrainingActions::DrawerTrainingActions(QObject *parent) : QObject {parent} , m_defaultAction {new TrainingAction(i18n("Please select a course"), this)} { } void DrawerTrainingActions::setSession(ISessionActions *session) { if (session == m_session) { return; } if (m_session) { - disconnect(m_session, &TrainingSession::courseChanged, this, &DrawerTrainingActions::actionsChanged); - disconnect(m_session, &TrainingSession::phraseChanged, this, &DrawerTrainingActions::triggerTrainingView); + disconnect(m_session, &ISessionActions::courseChanged, this, &DrawerTrainingActions::actionsChanged); + disconnect(m_session, &ISessionActions::actionsChanged, this, &DrawerTrainingActions::actionsChanged); + disconnect(m_session, &ISessionActions::phraseChanged, this, &DrawerTrainingActions::triggerPhraseView); } m_session = session; - connect(m_session, &TrainingSession::courseChanged, this, &DrawerTrainingActions::actionsChanged); - connect(m_session, &TrainingSession::phraseChanged, this, &DrawerTrainingActions::triggerTrainingView); + connect(m_session, &ISessionActions::courseChanged, this, &DrawerTrainingActions::actionsChanged); + connect(m_session, &ISessionActions::actionsChanged, this, &DrawerTrainingActions::actionsChanged); + connect(m_session, &ISessionActions::phraseChanged, this, &DrawerTrainingActions::triggerPhraseView); emit sessionChanged(); emit actionsChanged(); } ISessionActions *DrawerTrainingActions::session() const { return m_session; } QList DrawerTrainingActions::actions() const { if (!m_session || m_session->trainingActions().isEmpty()) { QList list; list << qobject_cast(m_defaultAction); return list; } QList actions; const auto trainingActions = m_session->trainingActions(); for (const auto &action : qAsConst(trainingActions)) { actions.append(qobject_cast(action)); } return actions; } diff --git a/src/core/drawertrainingactions.h b/src/core/drawertrainingactions.h index c52de95..c35561d 100644 --- a/src/core/drawertrainingactions.h +++ b/src/core/drawertrainingactions.h @@ -1,55 +1,55 @@ /* * 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 DRAWERTRAININGACTIONS_H #define DRAWERTRAININGACTIONS_H #include "artikulatecore_export.h" #include "isessionactions.h" #include class Course; class ARTIKULATECORE_EXPORT DrawerTrainingActions : public QObject { Q_OBJECT Q_PROPERTY(ISessionActions *session READ session WRITE setSession NOTIFY sessionChanged) Q_PROPERTY(QList actions READ actions NOTIFY actionsChanged) public: DrawerTrainingActions(QObject *parent = nullptr); void setSession(ISessionActions *session); ISessionActions *session() const; QList actions() const; Q_SIGNALS: void actionsChanged(); void sessionChanged(); /** - * Notify that training view shall be displayed. + * Notify that course view shall be displayed. */ - void triggerTrainingView(); + void triggerPhraseView(); private: ISessionActions *m_session {nullptr}; TrainingAction *m_defaultAction {nullptr}; }; #endif diff --git a/src/core/editorsession.cpp b/src/core/editorsession.cpp index 9307a65..2e27a9c 100644 --- a/src/core/editorsession.cpp +++ b/src/core/editorsession.cpp @@ -1,255 +1,299 @@ /* * 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 + }); 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->appendChild(new TrainingAction(phrase, this, unit.get())); + action->appendAction(new TrainingAction(phrase, this, unit.get())); } - if (action->hasChildren()) { + 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 + qDeleteAll(unitAction->actions()); + unitAction->actions().clear(); + for (int i = 0; i < changedUnit->phrases().size(); ++i) { + unitAction->appendAction(new TrainingAction(changedUnit->phrases().at(i), this, changedUnit.get())); + } + + qDebug() << "unit action changed" << unitAction << unitAction->text(); + emit unitAction->actionsChanged(); + emit unitAction->changed(); + + // update indices + //TODO update indexes according to changed phrase action +// m_indexUnit = -1; +// m_indexPhrase = -1; +// if (m_course->units().count() > 0) { +// m_indexUnit = 0; +// if (m_course->units().constFirst()->phrases().count() > 0) { +// m_indexPhrase = 0; +// } +// } } QVector EditorSession::trainingActions() const { return m_actions; } diff --git a/src/core/editorsession.h b/src/core/editorsession.h index 0c63de1..3cc2a63 100644 --- a/src/core/editorsession.h +++ b/src/core/editorsession.h @@ -1,89 +1,92 @@ /* * 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 "isessionactions.h" #include "phrase.h" +#include class ILanguage; class IEditableCourse; +class IEditableUnit; class Unit; class IPhrase; class SkeletonResource; class IEditableRepository; /** * \class EditorSession */ class ARTIKULATECORE_EXPORT EditorSession : public ISessionActions { Q_OBJECT Q_INTERFACES(ISessionActions) Q_PROPERTY(bool skeletonMode READ skeletonMode NOTIFY skeletonModeChanged) /** * @brief language of the currently selected course or null if skeleton is selected */ Q_PROPERTY(ILanguage *language READ language NOTIFY languageChanged) Q_PROPERTY(IEditableCourse *course READ course WRITE setCourse NOTIFY courseChanged) Q_PROPERTY(IUnit *unit READ activeUnit NOTIFY unitChanged) Q_PROPERTY(IPhrase *phrase READ activePhrase WRITE setActivePhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasNextPhrase READ hasNextPhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasPreviousPhrase READ hasPreviousPhrase NOTIFY phraseChanged) public: explicit EditorSession(QObject *parent = nullptr); void setRepository(IEditableRepository *repository); bool skeletonMode() const; ILanguage *language() const; IEditableCourse *course() const; void setCourse(IEditableCourse *course); IUnit *activeUnit() const; void setActiveUnit(IUnit *unit); IPhrase *activePhrase() const; void setActivePhrase(IPhrase *phrase) override; bool hasPreviousPhrase() const; bool hasNextPhrase() const; Q_INVOKABLE void switchToPreviousPhrase(); Q_INVOKABLE void switchToNextPhrase(); Q_INVOKABLE void updateCourseFromSkeleton(); TrainingAction *activeAction() const override; QVector trainingActions() const override; Q_SIGNALS: void skeletonModeChanged(); void languageChanged(); void unitChanged(); private: Q_DISABLE_COPY(EditorSession) void updateTrainingActions(); + void updateActions(std::shared_ptr unit); IEditableRepository *m_repository {nullptr}; bool m_editSkeleton {false}; IEditableCourse *m_course {nullptr}; QVector m_actions; int m_indexUnit {-1}; int m_indexPhrase {-1}; }; #endif diff --git a/src/core/icourse.h b/src/core/icourse.h index 7d35a55..a5330f4 100644 --- a/src/core/icourse.h +++ b/src/core/icourse.h @@ -1,80 +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 ICOURSE_H #define ICOURSE_H #include "artikulatecore_export.h" #include #include #include #include class QString; class ILanguage; +class IUnit; class Unit; class Phoneme; class ARTIKULATECORE_EXPORT ICourse : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id NOTIFY idChanged) Q_PROPERTY(QString title READ title NOTIFY titleChanged) Q_PROPERTY(QString i18nTitle READ i18nTitle NOTIFY titleChanged) Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) Q_PROPERTY(QString languageTitle READ languageTitle CONSTANT) public: virtual ~ICourse() = default; virtual QString id() const = 0; virtual QString foreignId() const = 0; virtual QString title() const = 0; virtual QString i18nTitle() const = 0; virtual QString description() const = 0; virtual QString languageTitle() const = 0; virtual std::shared_ptr language() const = 0; /** * @brief Lazy loading unit list * @return list of units in course */ virtual QVector> units() = 0; virtual QUrl file() const = 0; protected: ICourse() : QObject() { } virtual void setSelf(std::shared_ptr course) = 0; Q_SIGNALS: void idChanged(); void titleChanged(); void descriptionChanged(); void languageChanged(); void unitAdded(); - void unitAboutToBeAdded(std::shared_ptr, int); + void unitAboutToBeAdded(std::shared_ptr unit, int index); void unitsRemoved(); void unitsAboutToBeRemoved(int, int); }; Q_DECLARE_INTERFACE(ICourse, "com.kde.artikulate.ICourse/1.0") #endif // COURSE_H diff --git a/src/core/ieditablecourse.h b/src/core/ieditablecourse.h index ba30938..c230a06 100644 --- a/src/core/ieditablecourse.h +++ b/src/core/ieditablecourse.h @@ -1,76 +1,83 @@ /* * 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 IEDITABLECOURSE_H #define IEDITABLECOURSE_H #include "artikulatecore_export.h" #include "icourse.h" #include #include class QString; class ILanguage; +class IEditableUnit; +class IPhrase; class ARTIKULATECORE_EXPORT IEditableCourse : public ICourse { Q_OBJECT Q_INTERFACES(ICourse) public: virtual ~IEditableCourse() = default; virtual void setId(QString id) = 0; virtual void setForeignId(QString foreignId) = 0; virtual void setTitle(QString title) = 0; virtual void setI18nTitle(QString title) = 0; virtual void setDescription(QString description) = 0; virtual void setLanguage(std::shared_ptr language) = 0; virtual std::shared_ptr addUnit(std::shared_ptr unit) = 0; + Q_INVOKABLE virtual bool createPhraseAfter(IPhrase *phrase) = 0; + Q_INVOKABLE virtual bool deletePhrase(IPhrase *phrase) = 0; /** * @brief Export course to specified file. * @param filePath the absolute path to the export file * @return true of export finished without errors */ virtual bool exportToFile(const QUrl &filePath) const = 0; /** * @brief store editable course in file and set modified to false * @return true if no errors occurred */ virtual bool sync() = 0; /** * @brief Update course from skeleton * This method imports all units and phrases from the specified skeleton * * @param skeleton */ virtual void updateFrom(std::shared_ptr skeleton) = 0; virtual bool isModified() const = 0; virtual std::shared_ptr self() const = 0; +Q_SIGNALS: + void unitChanged(std::shared_ptr unit); + protected: IEditableCourse() : ICourse() { } }; Q_DECLARE_INTERFACE(IEditableCourse, "com.kde.artikulate.IEditableCourse/1.0") #endif // EDITABLECOURSE_H diff --git a/src/core/ieditableunit.h b/src/core/ieditableunit.h index b0c410b..70c0dff 100644 --- a/src/core/ieditableunit.h +++ b/src/core/ieditableunit.h @@ -1,62 +1,67 @@ /* * 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 IEDITABLEUNIT_H #define IEDITABLEUNIT_H #include "artikulatecore_export.h" #include "iunit.h" #include #include #include class QString; class ICourse; class IPhrase; class IEditablePhrase; class Phoneme; class ARTIKULATECORE_EXPORT IEditableUnit : public IUnit { Q_OBJECT Q_PROPERTY(QString id READ id WRITE setId NOTIFY idChanged) Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) public: virtual ~IEditableUnit() = default; virtual void setId(const QString &id) = 0; virtual void setForeignId(const QString &id) = 0; virtual void setCourse(std::shared_ptr course) = 0; virtual void setTitle(const QString &title) = 0; - virtual void addPhrase(std::shared_ptr phrase) = 0; + virtual void addPhrase(std::shared_ptr phrase, int index) = 0; + virtual void removePhrase(std::shared_ptr phrase) = 0; Q_SIGNALS: void modified(); + /** + * @brief combines any change (rename, phrases) + */ + void phrasesChanged(std::shared_ptr unit); protected: IEditableUnit(QObject *parent = nullptr) : IUnit(parent) { } }; Q_DECLARE_INTERFACE(IEditableUnit, "com.kde.artikulate.IEditableUnit/1.0") #endif // IEDITABLEUNIT_H diff --git a/src/core/isessionactions.h b/src/core/isessionactions.h index 6472537..84161bb 100644 --- a/src/core/isessionactions.h +++ b/src/core/isessionactions.h @@ -1,64 +1,65 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef ISESSIONACTIONS_H #define ISESSIONACTIONS_H #include "artikulatecore_export.h" #include #include class ICourse; class IPhrase; class TrainingAction; /** * \class ISessionActions * * Interface for both training and editor sessions that exposes simple iterator functionalities for a selecte course. * The interface provides all properties that are needed to create a navigatible menu. */ class ARTIKULATECORE_EXPORT ISessionActions : public QObject { Q_OBJECT public: ISessionActions(QObject *parent) : QObject(parent) { } virtual ~ISessionActions() = default; virtual TrainingAction *activeAction() const = 0; virtual void setActivePhrase(IPhrase *phrase) = 0; /** * \brief Return tree of training actions * * The return actions form a 2-level hierarchy: * - the first level are all units * - the unit actions may contain sub-actions, which are the phrases */ virtual QVector trainingActions() const = 0; Q_SIGNALS: void courseChanged(); + void actionsChanged(); void phraseChanged(); }; Q_DECLARE_INTERFACE(ISessionActions, "ISessionActions") #endif diff --git a/src/core/iunit.h b/src/core/iunit.h index f6e80dd..a9b97e4 100644 --- a/src/core/iunit.h +++ b/src/core/iunit.h @@ -1,71 +1,71 @@ /* * 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 IUNIT_H #define IUNIT_H #include "artikulatecore_export.h" #include #include #include #include class QString; class ICourse; class IPhrase; class Phoneme; class ARTIKULATECORE_EXPORT IUnit : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id NOTIFY idChanged) Q_PROPERTY(QString title READ title NOTIFY titleChanged) public: virtual ~IUnit() = default; virtual QString id() const = 0; virtual QString foreignId() const = 0; virtual std::shared_ptr course() const = 0; virtual QString title() const = 0; virtual QVector> phrases() const = 0; virtual std::shared_ptr self() const = 0; Q_SIGNALS: void idChanged(); void titleChanged(); void courseChanged(); void displayPhraseTypeChanged(); void modified(); - void phraseAdded(std::shared_ptr); void phraseAboutToBeAdded(std::shared_ptr, int); - void phraseRemoved(std::shared_ptr); - void phraseAboutToBeRemoved(int, int); + void phraseAdded(std::shared_ptr); + void phraseAboutToBeRemoved(int); + void phraseRemoved(); protected: IUnit(QObject *parent = nullptr) : QObject(parent) { } virtual void setSelf(std::shared_ptr unit) = 0; }; Q_DECLARE_INTERFACE(IUnit, "com.kde.artikulate.IUnit/1.0") #endif // IUNIT_H diff --git a/src/core/resources/courseparser.cpp b/src/core/resources/courseparser.cpp index 332c182..bbd0acb 100644 --- a/src/core/resources/courseparser.cpp +++ b/src/core/resources/courseparser.cpp @@ -1,409 +1,409 @@ /* * 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) { 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::shared_ptr CourseParser::parseUnit(QXmlStreamReader &xml, const QUrl &path, QVector> phonemes, 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) { - unit->addPhrase(phrase); + 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") { 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(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(); 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/resources/courseresource.cpp b/src/core/resources/courseresource.cpp index ac468da..05c6cbe 100644 --- a/src/core/resources/courseresource.cpp +++ b/src/core/resources/courseresource.cpp @@ -1,288 +1,287 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * Copyright 2013 Oindrila Gupta * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "courseresource.h" #include "core/iresourcerepository.h" #include "core/language.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/unit.h" #include "courseparser.h" #include #include #include #include #include #include #include #include #include "artikulate_debug.h" class CourseResourcePrivate { public: CourseResourcePrivate() = default; ~CourseResourcePrivate(); void loadCourse(CourseResource *parent); std::weak_ptr m_self; IResourceRepository *m_repository {nullptr}; QUrl m_file; QString m_identifier; QString m_foreignId; QString m_title; QString m_languageId; std::shared_ptr m_language; QString m_i18nTitle; QString m_description; QVector> m_units; bool m_courseLoaded {false}; ///> phonemes = m_language->phonemes(); auto units = CourseParser::parseUnits(m_file, phonemes); for (auto &unit : units) { parent->addUnit(std::move(unit)); } } std::shared_ptr CourseResource::create(const QUrl &path, IResourceRepository *repository) { std::shared_ptr course(new CourseResource(path, repository)); course->setSelf(course); return course; } void CourseResource::setSelf(std::shared_ptr self) { Q_ASSERT(d->m_self.expired()); d->m_self = self; } std::shared_ptr CourseResource::self() const { - Q_ASSERT(!d->m_self.expired()); return d->m_self.lock(); } CourseResource::CourseResource(const QUrl &path, IResourceRepository *repository) : ICourse() , d(new CourseResourcePrivate()) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); d->m_file = path; d->m_repository = repository; // load basic information from language file, but does not parse everything QXmlStreamReader xml; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { xml.setDevice(&file); xml.readNextStartElement(); while (xml.readNext() && !xml.atEnd()) { if (xml.name() == "id") { d->m_identifier = xml.readElementText(); continue; } if (xml.name() == "foreignId") { d->m_foreignId = xml.readElementText(); continue; } // TODO i18nTitle must be implemented, currently missing and hence not parsed if (xml.name() == "title") { d->m_title = xml.readElementText(); d->m_i18nTitle = d->m_title; continue; } if (xml.name() == "description") { d->m_description = xml.readElementText(); continue; } if (xml.name() == "language") { d->m_languageId = xml.readElementText(); continue; } // quit reading when basic elements are read if (!d->m_identifier.isEmpty() && !d->m_title.isEmpty() && !d->m_i18nTitle.isEmpty() && !d->m_description.isEmpty() && !d->m_languageId.isEmpty() && !d->m_foreignId.isEmpty()) { break; } } if (xml.hasError()) { qCritical() << "Error occurred when reading Course XML file:" << path.toLocalFile(); } } else { qCCritical(ARTIKULATE_CORE()) << "Could not open course file" << path.toLocalFile(); } xml.clear(); file.close(); // find correct language if (repository != nullptr) { for (const auto &language : repository->languages()) { if (language == nullptr) { continue; } if (language->id() == d->m_languageId) { d->m_language = language; } } } if (d->m_language == nullptr) { qCCritical(ARTIKULATE_CORE()) << "A course with an unknown language was loaded"; } } CourseResource::~CourseResource() = default; QString CourseResource::id() const { return d->m_identifier; } void CourseResource::setId(const QString &id) { if (d->m_identifier == id) { return; } d->m_identifier = id; emit idChanged(); } QString CourseResource::foreignId() const { return d->m_foreignId; } void CourseResource::setForeignId(const QString &foreignId) { if (d->m_foreignId == foreignId) { return; } d->m_foreignId = foreignId; emit foreignIdChanged(); } QString CourseResource::title() const { return d->m_title; } void CourseResource::setTitle(const QString &title) { if (d->m_title == title) { return; } d->m_title = title; emit titleChanged(); } QString CourseResource::i18nTitle() const { return d->m_i18nTitle; } void CourseResource::setI18nTitle(const QString &i18nTitle) { if (d->m_i18nTitle == i18nTitle) { return; } d->m_i18nTitle = i18nTitle; emit i18nTitleChanged(); } QString CourseResource::description() const { return d->m_description; } void CourseResource::setDescription(const QString &description) { if (d->m_description == description) { return; } d->m_description = description; emit descriptionChanged(); } std::shared_ptr CourseResource::language() const { return d->m_language; } QString CourseResource::languageTitle() const { if (d->m_language) { return d->m_language->title(); } return QString(); } void CourseResource::setLanguage(std::shared_ptr language) { if (d->m_language == language) { return; } d->m_language = language; emit languageChanged(); } std::shared_ptr CourseResource::addUnit(std::shared_ptr unit) { std::shared_ptr storedUnit(std::move(unit)); storedUnit->setCourse(self()); emit unitAboutToBeAdded(storedUnit, d->m_units.count() - 1); d->m_units.append(storedUnit); emit unitAdded(); return storedUnit; } QVector> CourseResource::units() { if (d->m_courseLoaded == false) { d->loadCourse(this); } return d->m_units; } QUrl CourseResource::file() const { return d->m_file; } diff --git a/src/core/resources/editablecourseresource.cpp b/src/core/resources/editablecourseresource.cpp index 4960098..5bc9b98 100644 --- a/src/core/resources/editablecourseresource.cpp +++ b/src/core/resources/editablecourseresource.cpp @@ -1,303 +1,344 @@ /* * 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 #include EditableCourseResource::EditableCourseResource(const QUrl &path, IResourceRepository *repository) : IEditableCourse() , m_course(new CourseResource(path, repository)) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); connect(m_course.get(), &ICourse::unitAboutToBeAdded, this, &ICourse::unitAboutToBeAdded); connect(m_course.get(), &ICourse::unitAdded, this, &ICourse::unitAdded); connect(m_course.get(), &CourseResource::idChanged, this, &EditableCourseResource::idChanged); connect(m_course.get(), &CourseResource::foreignIdChanged, this, &EditableCourseResource::foreignIdChanged); connect(m_course.get(), &CourseResource::titleChanged, this, &EditableCourseResource::titleChanged); connect(m_course.get(), &CourseResource::descriptionChanged, this, &EditableCourseResource::descriptionChanged); connect(m_course.get(), &CourseResource::languageChanged, this, &EditableCourseResource::languageChanged); + + for (auto &unit : m_course->units()) { + connect(unit.get(), &Unit::phrasesChanged, this, &IEditableCourse::unitChanged); + } } 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(); } QString EditableCourseResource::languageTitle() const { return m_course->languageTitle(); } 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(self(), false).toByteArray()); return true; } std::shared_ptr EditableCourseResource::addUnit(std::shared_ptr unit) { m_modified = true; - auto sharedUnit = m_course->addUnit(std::move(unit)); - sharedUnit->setCourse(self()); - return sharedUnit; + m_course->addUnit(unit); + unit->setCourse(self()); + connect(unit.get(), &Unit::phrasesChanged, this, &IEditableCourse::unitChanged); + return unit; } QVector> EditableCourseResource::units() { if (!m_unitsLoaded) { for (auto &unit : m_course->units()) { unit->setCourse(self()); } m_unitsLoaded = true; } 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 = Unit::create(); 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->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->phrases().cend()) { // import complete Phrase 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); - matchingUnit->addPhrase(importPhrase); + matchingUnit->addPhrase(importPhrase, matchingUnit->phrases().size()); } } } 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::shared_ptr unit = Unit::create(); unit->setCourse(self()); unit->setId(id); unit->setTitle(i18n("New Unit")); auto sharedUnit = addUnit(std::move(unit)); return sharedUnit.get(); } -std::shared_ptr EditableCourseResource::createPhrase(Unit *unit) +bool EditableCourseResource::createPhraseAfter(IPhrase *previousPhrase) { + std::shared_ptr parentUnit = units().last(); + if (previousPhrase) { + for (const auto &unit : units()) { + if (previousPhrase->unit()->id() == unit->id()) { + parentUnit = unit; + break; + } + } + } + + // find index + int index = parentUnit->phrases().size(); + for (int i = 0; i < parentUnit->phrases().size(); ++i) { + if (parentUnit->phrases().at(i)->id() == previousPhrase->id()) { + index = i; + break; + } + } + // find globally unique phrase id inside course QStringList phraseIds; for (auto unit : m_course->units()) { 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 std::shared_ptr phrase = Phrase::create(); phrase->setId(id); phrase->setText(QLatin1String("")); phrase->setType(IPhrase::Type::Word); + parentUnit->addPhrase(phrase, index); - unit->addPhrase(phrase); + qCDebug(ARTIKULATE_CORE()) << "Created phrase at index" << index; - return phrase; + return true; +} + +bool EditableCourseResource::deletePhrase(IPhrase *phrase) +{ + Q_ASSERT(phrase); + if (!phrase) { + return false; + } + auto unitId = phrase->unit()->id(); + for (auto &unit : units()) { + if (unit->id() == unitId) { + unit->removePhrase(phrase->self()); + return true; + } + } + return false; } diff --git a/src/core/resources/editablecourseresource.h b/src/core/resources/editablecourseresource.h index 9207f00..393bf22 100644 --- a/src/core/resources/editablecourseresource.h +++ b/src/core/resources/editablecourseresource.h @@ -1,128 +1,129 @@ /* * 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 "core/icourse.h" #include "core/ieditablecourse.h" +#include "core/ieditableunit.h" #include "courseresource.h" - #include #include #include class IResourceRepository; class Course; class Unit; -class Phrase; +class IPhrase; 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; QString languageTitle() const override; void setLanguage(std::shared_ptr language) override; bool sync() override; bool exportToFile(const QUrl &filePath) const override; std::shared_ptr addUnit(std::shared_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 std::shared_ptr createPhrase(Unit *unit); + Q_INVOKABLE bool createPhraseAfter(IPhrase *previousPhrase) override; + Q_INVOKABLE bool deletePhrase(IPhrase *phrase) override; 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; mutable bool m_unitsLoaded {false}; ///< parsing of all units is postponed until needed, this variable indicates if they are read 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 8763704..228aa1f 100644 --- a/src/core/resources/skeletonresource.cpp +++ b/src/core/resources/skeletonresource.cpp @@ -1,365 +1,426 @@ /* * 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 "artikulate_debug.h" #include "core/language.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/phrase.h" #include "core/unit.h" #include "courseparser.h" #include "editablecourseresource.h" #include #include #include #include #include #include +#include #include 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()); 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()); 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 (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); + 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_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_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; } +bool SkeletonResource::createPhraseAfter(IPhrase *previousPhrase) +{ + std::shared_ptr parentUnit = units().last(); + if (previousPhrase) { + for (const auto &unit : units()) { + if (previousPhrase->unit()->id() == unit->id()) { + parentUnit = unit; + break; + } + } + } + + // find index + int index = parentUnit->phrases().size(); + for (int i = 0; i < parentUnit->phrases().size(); ++i) { + if (parentUnit->phrases().at(i)->id() == previousPhrase->id()) { + index = i; + break; + } + } + + // find globally unique phrase id inside course + QStringList phraseIds; + for (auto unit : units()) { + 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 + std::shared_ptr phrase = Phrase::create(); + phrase->setId(id); + phrase->setText(QString()); + phrase->setType(IPhrase::Type::Word); + parentUnit->addPhrase(phrase, index); + + return true; +} + +bool SkeletonResource::deletePhrase(IPhrase *phrase) +{ + Q_ASSERT(phrase); + if (!phrase) { + return false; + } + auto unitId = phrase->unit()->id(); + for (auto &unit : d->units()) { + if (unit->id() == unitId) { + unit->removePhrase(phrase->self()); + return true; + } + } + return false; +} + std::shared_ptr SkeletonResource::addUnit(std::shared_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(); } QString SkeletonResource::languageTitle() const { return QString(); } void SkeletonResource::setLanguage(std::shared_ptr language) { - Q_UNUSED(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/resources/skeletonresource.h b/src/core/resources/skeletonresource.h index a84cfc9..d44fac2 100644 --- a/src/core/resources/skeletonresource.h +++ b/src/core/resources/skeletonresource.h @@ -1,75 +1,77 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef SKELETONRESOURCE_H #define SKELETONRESOURCE_H #include "artikulatecore_export.h" #include "core/ieditablecourse.h" #include class SkeletonResourcePrivate; class IResourceRepository; /** * @brief The SkeletonResource class is a decorator for EditableCourseResource */ class ARTIKULATECORE_EXPORT SkeletonResource : public IEditableCourse { Q_OBJECT Q_INTERFACES(ICourse) public: static std::shared_ptr create(const QUrl &path, IResourceRepository *repository); ~SkeletonResource() override; QString id() const override; void setId(QString id) override; QString foreignId() const override; void setForeignId(QString id) override; QString title() const override; void setTitle(QString title) override; QString i18nTitle() const override; void setI18nTitle(QString title) override; QString description() const override; void setDescription(QString description) override; std::shared_ptr language() const override; QString languageTitle() const override; void setLanguage(std::shared_ptr language) override; QVector> units() override; QUrl file() const override; bool exportToFile(const QUrl &filePath) const override; + Q_INVOKABLE bool createPhraseAfter(IPhrase *previousPhrase) override; + Q_INVOKABLE bool deletePhrase(IPhrase *phrase) override; std::shared_ptr addUnit(std::shared_ptr unit) override; bool sync() override; void updateFrom(std::shared_ptr) override; bool isModified() const override; std::shared_ptr self() const override; private: /** * Create course resource from file. */ explicit SkeletonResource(const QUrl &path, IResourceRepository *repository); void setSelf(std::shared_ptr self) override; const QScopedPointer d; }; #endif diff --git a/src/core/trainingaction.cpp b/src/core/trainingaction.cpp index e82e622..6902a46 100644 --- a/src/core/trainingaction.cpp +++ b/src/core/trainingaction.cpp @@ -1,110 +1,188 @@ /* * 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) - : QObject(parent) + : QAbstractListModel(parent) , m_text(QString()) - , m_icon(new TrainingActionIcon(this, QString())) // TODO "rating-unrated" vs. "rating" + , m_icon(nullptr, QString()) // TODO "rating-unrated" vs. "rating" { + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); } TrainingAction::TrainingAction(const QString &text, QObject *parent) - : QObject(parent) + : QAbstractListModel(parent) , m_text(text) - , m_icon(new TrainingActionIcon(this, QString())) // TODO "rating-unrated" vs. "rating" + , m_icon(nullptr, QString()) // TODO "rating-unrated" vs. "rating" { + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); } TrainingAction::TrainingAction(std::shared_ptr phrase, ISessionActions *session, QObject *parent) - : QObject(parent) - , m_icon(new TrainingActionIcon(this, QString())) + : QAbstractListModel(parent) + , m_icon(nullptr, QString()) , m_phrase(phrase) , m_session(session) { + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); if (m_phrase) { m_text = phrase->text(); } } -void TrainingAction::appendChild(QObject *child) +QHash TrainingAction::roleNames() const { - m_actions.append(child); - emit actionsChanged(); + QHash roles; + roles[ModelDataRole] = "modelData"; + return roles; +} + +int TrainingAction::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return 1; } -bool TrainingAction::hasChildren() const +int TrainingAction::rowCount(const QModelIndex &parent) const { - return m_actions.count() > 0; + 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(); + } } 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() const +QObject * TrainingAction::icon() { - return m_icon; + return qobject_cast(&m_icon); } -IPhrase *TrainingAction::phrase() const +IPhrase * TrainingAction::phrase() const { return m_phrase.get(); } -QList TrainingAction::actions() const +QVector TrainingAction::actions() const { return m_actions; } + +QAbstractListModel * TrainingAction::actionModel() +{ + return this; +} + +int TrainingAction::actionsCount() const +{ + return m_actions.count(); +} + +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(); + endResetModel(); + emit actionsChanged(); +} diff --git a/src/core/trainingaction.h b/src/core/trainingaction.h index 44d7ea0..8be93fe 100644 --- a/src/core/trainingaction.h +++ b/src/core/trainingaction.h @@ -1,79 +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 class DrawerTrainingActions; -class ARTIKULATECORE_EXPORT TrainingAction : public QObject +class ARTIKULATECORE_EXPORT TrainingAction : public QAbstractListModel { Q_OBJECT - Q_PROPERTY(QString text MEMBER m_text CONSTANT) + 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(QList children READ actions 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 + }; + TrainingAction(QObject *parent = nullptr); TrainingAction(const QString &text, QObject *parent = nullptr); TrainingAction(std::shared_ptr phrase, ISessionActions *session, QObject *parent = nullptr); - void appendChild(QObject *child); - bool hasChildren() const; Q_INVOKABLE void trigger(); bool enabled() const; void setEnabled(bool enabled); + QString text() const; + void setText(QString text); void setChecked(bool checked); bool checked() const; - QObject *icon() const; + QObject *icon(); IPhrase *phrase() const; - QList actions() const; + QAbstractListModel * actionModel(); + QVector actions() const; + int actionsCount() 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 changed(); void actionsChanged(); void enabledChanged(bool enabled); void checkedChanged(bool checked); + void textChanged(QString text); private: + QVector m_actions; QString m_text; - TrainingActionIcon *m_icon {nullptr}; + TrainingActionIcon m_icon; bool m_visible {true}; bool m_enabled {true}; bool m_checked {false}; bool m_checkable {false}; QString m_tooltip {QString()}; - QList m_actions; std::shared_ptr m_phrase; ISessionActions *m_session {nullptr}; }; #endif diff --git a/src/core/trainingsession.cpp b/src/core/trainingsession.cpp index 7bfccfd..d5e1f39 100644 --- a/src/core/trainingsession.cpp +++ b/src/core/trainingsession.cpp @@ -1,298 +1,298 @@ /* * 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 "artikulate_debug.h" #include "core/icourse.h" #include "core/language.h" #include "core/phrase.h" #include "core/unit.h" #include "learner.h" #include "profilemanager.h" #include "trainingaction.h" TrainingSession::TrainingSession(LearnerProfile::ProfileManager *manager, QObject *parent) : ISessionActions(parent) , m_profileManager(manager) , m_course(nullptr) { Q_ASSERT(m_profileManager != nullptr); } ICourse *TrainingSession::course() const { return m_course; } void TrainingSession::setCourse(ICourse *course) { if (!course) { updateTrainingActions(); return; } if (m_course == course) { return; } m_course = course; if (m_course && m_course->units().count() > 0) { setUnit(m_course->units().constFirst().get()); } // lazy loading of training data LearnerProfile::LearningGoal *goal = m_profileManager->goal(LearnerProfile::LearningGoal::Language, m_course->id()); if (!goal) { goal = m_profileManager->registerGoal(LearnerProfile::LearningGoal::Language, course->language()->id(), course->language()->i18nTitle()); } auto data = m_profileManager->progressValues(m_profileManager->activeProfile(), goal, m_course->id()); const auto unitList = m_course->units(); for (auto unit : qAsConst(unitList)) { const auto phrases = unit->phrases(); for (auto &phrase : phrases) { auto iter = data.find(phrase->id()); if (iter != data.end()) { // phrase->setProgress(iter.value()); //FIXME add a decorator? } } } updateTrainingActions(); emit courseChanged(); } IUnit *TrainingSession::activeUnit() const { if (auto phrase = activePhrase()) { return phrase->unit().get(); } return nullptr; } void TrainingSession::setUnit(IUnit *unit) { // checking phrases in increasing order ensures that always the first phrase is selected for (int i = 0; i < m_actions.count(); ++i) { for (int j = 0; j < m_actions.at(i)->actions().count(); ++j) { const auto testPhrase = qobject_cast(m_actions.at(i)->actions().at(j))->phrase(); if (unit == testPhrase->unit().get()) { if (auto action = activeAction()) { action->setChecked(false); } m_indexUnit = i; m_indexPhrase = j; if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); return; } } } } TrainingAction *TrainingSession::activeAction() const { if (m_indexUnit < 0 || m_indexPhrase < 0) { return nullptr; } return qobject_cast(m_actions.at(m_indexUnit)->actions().at(m_indexPhrase)); } IPhrase *TrainingSession::activePhrase() const { if (const auto action = activeAction()) { return action->phrase(); } return nullptr; } void TrainingSession::setActivePhrase(IPhrase *phrase) { for (int i = 0; i < m_actions.count(); ++i) { for (int j = 0; j < m_actions.at(i)->actions().count(); ++j) { const auto testPhrase = qobject_cast(m_actions.at(i)->actions().at(j))->phrase(); if (phrase == testPhrase) { if (auto action = activeAction()) { action->setChecked(false); } m_indexUnit = i; m_indexPhrase = j; if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); return; } } } } void TrainingSession::accept() { Q_ASSERT(m_indexUnit >= 0); Q_ASSERT(m_indexPhrase >= 0); if (m_indexUnit < 0 || m_indexPhrase < 0) { return; } auto phrase = activePhrase(); // possibly update goals of learner updateGoal(); // phrase->updateProgress(Phrase::Progress::Done); //FIXME // store training activity LearnerProfile::LearningGoal *goal = m_profileManager->goal(LearnerProfile::LearningGoal::Language, m_course->language()->id()); // m_profileManager->recordProgress(m_profileManager->activeProfile(), //FIXME // goal, // m_course->id(), // phrase->id(), // static_cast(LearnerProfile::ProfileManager::Skip), // phrase->progress() // ); selectNextPhrase(); } void TrainingSession::skip() { Q_ASSERT(m_indexUnit >= 0); Q_ASSERT(m_indexPhrase >= 0); if (m_indexUnit < 0 || m_indexPhrase < 0) { return; } // possibly update goals of learner updateGoal(); auto phrase = activePhrase(); // phrase->updateProgress(Phrase::Progress::Skip); //FIXME // store training activity LearnerProfile::LearningGoal *goal = m_profileManager->goal(LearnerProfile::LearningGoal::Language, m_course->language()->id()); // m_profileManager->recordProgress(m_profileManager->activeProfile(), // goal, // m_course->id(), // phrase->id(), // static_cast(LearnerProfile::ProfileManager::Skip), // phrase->progress() // ); // FIXME selectNextPhrase(); } void TrainingSession::selectNextPhrase() { if (auto action = activeAction()) { action->setChecked(false); } // try to find next phrase, otherwise return completed if (m_indexPhrase >= m_actions.at(m_indexUnit)->actions().count() - 1) { qDebug() << "switching to next unit"; if (m_indexUnit >= m_actions.count() - 1) { emit completed(); } else { ++m_indexUnit; m_indexPhrase = 0; } } else { ++m_indexPhrase; } if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); } bool TrainingSession::hasPrevious() const { return m_indexUnit > 0 || m_indexPhrase > 0; } bool TrainingSession::hasNext() const { if (m_indexUnit < m_actions.count() - 1) { return true; } if (m_actions.constLast()) { if (m_indexPhrase < m_actions.constLast()->actions().count() - 1) { return true; } } return false; } void TrainingSession::updateGoal() { if (!m_profileManager) { qCWarning(ARTIKULATE_LOG()) << "No ProfileManager registered, aborting operation"; return; } LearnerProfile::Learner *learner = m_profileManager->activeProfile(); if (!learner) { qCWarning(ARTIKULATE_LOG()) << "No active Learner registered, aborting operation"; return; } LearnerProfile::LearningGoal *goal = m_profileManager->goal(LearnerProfile::LearningGoal::Language, m_course->language()->id()); learner->addGoal(goal); learner->setActiveGoal(goal); } QVector TrainingSession::trainingActions() const { return m_actions; } void TrainingSession::updateTrainingActions() { for (const auto &action : qAsConst(m_actions)) { action->deleteLater(); } m_actions.clear(); if (!m_course) { m_indexUnit = -1; m_indexPhrase = -1; return; } const auto unitList = m_course->units(); for (const auto &unit : qAsConst(unitList)) { auto action = new TrainingAction(unit->title(), this); const auto phraseList = unit->phrases(); for (const auto &phrase : qAsConst(phraseList)) { if (phrase->sound().isEmpty()) { continue; } - action->appendChild(new TrainingAction(phrase, this, unit.get())); + action->appendAction(new TrainingAction(phrase, this, unit.get())); } - if (action->hasChildren()) { + 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; } } } diff --git a/src/core/unit.cpp b/src/core/unit.cpp index 2d44e33..437abce 100644 --- a/src/core/unit.cpp +++ b/src/core/unit.cpp @@ -1,171 +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 "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](){ + if (auto unit = unitParameter.lock()) { + qDebug() << "emit due to added" << unit.get(); + unit->emitPhrasesChanged(unit); + } + }); + connect(unit.get(), &IUnit::phraseRemoved, unit.get(), [unitParameter](){ + if (auto unit = unitParameter.lock()) { + qDebug() << "emit due to removed" << unit.get(); + 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) +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, m_phrases.length()); - m_phrases.append(phrase); - + 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(); } -QList Unit::excludedSkeletonPhraseList() const +void Unit::removePhrase(std::shared_ptr 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; - // } - // } - qCWarning(ARTIKULATE_LOG) << "Could not exclude phrase with ID " << phraseId << ", no phrase with this ID."; + 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::includeSkeletonPhrase(const QString &phraseId) +void Unit::emitPhrasesChanged(std::shared_ptr unit) { - // 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."; + emit phrasesChanged(unit); } diff --git a/src/core/unit.h b/src/core/unit.h index 96c13a6..d84ae08 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -1,82 +1,74 @@ /* * 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 "ieditableunit.h" #include #include #include #include class QString; class Phrase; class IPhrase; class ICourse; class ARTIKULATECORE_EXPORT Unit : public IEditableUnit { Q_OBJECT Q_PROPERTY(QString id READ id WRITE setId NOTIFY idChanged) Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) public: static std::shared_ptr create(); ~Unit() override; QString id() const override; void setId(const QString &id) override; QString foreignId() const override; void setForeignId(const QString &id) override; std::shared_ptr course() const override; void setCourse(std::shared_ptr course) override; QString title() const override; void setTitle(const QString &title) override; QVector> phrases() const override; - void addPhrase(std::shared_ptr phrase) override; - QList excludedSkeletonPhraseList() const; + void addPhrase(std::shared_ptr phrase, int index) override; + void removePhrase(std::shared_ptr phrase) override; std::shared_ptr self() const override; - - /** - * 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); + void emitPhrasesChanged(std::shared_ptr unit); protected: explicit Unit(QObject *parent = nullptr); private: void setSelf(std::shared_ptr self) override; Q_DISABLE_COPY(Unit) std::weak_ptr m_self; QString m_id; QString m_foreignId; std::weak_ptr m_course; QString m_title; QVector> m_phrases; }; #endif // UNIT_H diff --git a/src/models/phraselistmodel.cpp b/src/models/phraselistmodel.cpp index c78e833..587d2fb 100644 --- a/src/models/phraselistmodel.cpp +++ b/src/models/phraselistmodel.cpp @@ -1,208 +1,208 @@ /* * 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/phrase.h" #include "core/unit.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 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); 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::phraseAboutToBeRemoved, this, &PhraseListModel::onPhraseAboutToBeRemoved); connect(m_unit, &Unit::phraseRemoved, this, &PhraseListModel::onPhrasesRemoved); // insert and connect all already existing phrases int phrases = m_unit->phrases().count(); for (int i = 0; i < phrases; ++i) { onPhraseAboutToBeAdded(m_unit->phrases().at(i), 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->phrases().count()) { return QVariant(); } 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 QVariant::fromValue(phrase->type()); // case ExcludedRole: //FIXME // return phrase->isExcluded(); case DataRole: 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->phrases().count(); } void PhraseListModel::onPhraseAboutToBeAdded(std::shared_ptr phrase, int index) { connect(phrase.get(), SIGNAL(textChanged()), m_signalMapper, SLOT(map())); connect(phrase.get(), SIGNAL(typeChanged()), m_signalMapper, SLOT(map())); connect(phrase.get(), SIGNAL(excludedChanged()), m_signalMapper, SLOT(map())); beginInsertRows(QModelIndex(), index, index); } void PhraseListModel::onPhraseAdded() { updateMappings(); endInsertRows(); emit countChanged(); } -void PhraseListModel::onPhrasesAboutToBeRemoved(int first, int last) +void PhraseListModel::onPhraseAboutToBeRemoved(int index) { - beginRemoveRows(QModelIndex(), first, last); + beginRemoveRows(QModelIndex(), index, index); } 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->phrases().count(); } void PhraseListModel::updateMappings() { if (!m_unit) { return; } int phrases = m_unit->phrases().count(); for (int i = 0; i < phrases; ++i) { m_signalMapper->setMapping(m_unit->phrases().at(i).get(), i); } } diff --git a/src/models/phraselistmodel.h b/src/models/phraselistmodel.h index 49a9713..a260aa8 100644 --- a/src/models/phraselistmodel.h +++ b/src/models/phraselistmodel.h @@ -1,77 +1,77 @@ /* * 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 "core/phrase.h" #include 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(std::shared_ptr unit, int index); void onPhraseAdded(); - void onPhrasesAboutToBeRemoved(int first, int last); + void onPhraseAboutToBeRemoved(int index); 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 496d750..1485342 100644 --- a/src/models/phrasemodel.cpp +++ b/src/models/phrasemodel.cpp @@ -1,346 +1,347 @@ /* * 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 "artikulate_debug.h" #include "core/icourse.h" #include "core/phrase.h" #include "core/unit.h" #include #include #include 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 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); 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::phraseAboutToBeRemoved, this, &PhraseModel::onPhraseAboutToBeRemoved); 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->phrases().count(); for (int i = 0; i < phrases; ++i) { onPhraseAboutToBeAdded(unit->phrases().at(i), i); endInsertRows(); } } updateUnitMappings(); updatePhraseMappings(); } // emit done endResetModel(); emit courseChanged(); } ICourse *PhraseModel::course() const { return m_course; } QVariant PhraseModel::data(const QModelIndex &index, int role) const { Q_ASSERT(m_course); if (!index.isValid()) { return QVariant(); } if (!index.internalPointer()) { if (!m_course || m_course->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->phrases().at(index.row())->text(); case DataRole: 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->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(); } auto unit = phrase->unit(); return createIndex(unit->phrases().indexOf(phrase->self()), 0, unit.get()); } 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(std::shared_ptr 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.get(), &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) +void PhraseModel::onPhraseAboutToBeRemoved(int index) { + Q_UNUSED(index) // 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; } IPhrase *PhraseModel::phrase(const QModelIndex &index) const { if (index.internalPointer()) { Unit *unit = static_cast(index.internalPointer()); return unit->phrases().at(index.row()).get(); } 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()) { 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 1495059..25e19c8 100644 --- a/src/models/phrasemodel.h +++ b/src/models/phrasemodel.h @@ -1,82 +1,82 @@ /* * 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 "core/phrase.h" #include #include 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 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(std::shared_ptr phrase, int index); void onPhraseAdded(); - void onPhrasesAboutToBeRemoved(int first, int last); + void onPhraseAboutToBeRemoved(int index); 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/qml/ArtikulateDrawer.qml b/src/qml/ArtikulateDrawer.qml index 8198e83..03dea1d 100644 --- a/src/qml/ArtikulateDrawer.qml +++ b/src/qml/ArtikulateDrawer.qml @@ -1,154 +1,154 @@ /* * Copyright 2018 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 . */ import QtQuick 2.5 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.0 as QQC2 import org.kde.kirigami 2.7 as Kirigami import artikulate 1.0 Kirigami.GlobalDrawer { id: root title: "Artikulate" titleIcon: "artikulate" resetMenuOnTriggered: false bottomPadding: 0 property QtObject pageStack // enforce drawer always to be open modal: false handleVisible: false topContent: [ ColumnLayout { spacing: 0 Layout.fillWidth: true Layout.leftMargin: -root.leftPadding Layout.rightMargin: -root.rightPadding ActionListItem { action: Kirigami.Action { text: i18n("Training") iconName: "artikulate" onTriggered: { root.pageStack.clear(); root.pageStack.push(welcomePageComponent); } } } Kirigami.Separator { Layout.fillWidth: true } } ] // ordinary Kirigami actions are filled from training units/phrases - actions: trainingActions.actions + actions: sessionActions.actions DrawerTrainingActions { - id: trainingActions + id: sessionActions session: g_trainingSession - onTriggerTrainingView: { + onTriggerPhraseView: { root.pageStack.clear(); root.pageStack.push(trainingPageComponent); } } //TODO integrate again // [ // Kirigami.Action { // text: i18n("Help") // iconName: "help-about" // Kirigami.Action { // text: i18n("Artikulate Handbook") // iconName: "help-contents" // onTriggered: { // triggerAction("help_contents"); // globalDrawer.resetMenu(); // } // } // Kirigami.Action { // text: i18n("Report Bug") // iconName: "tools-report-bug" // onTriggered: { // triggerAction("help_report_bug"); // globalDrawer.resetMenu(); // } // } // Kirigami.Action { // text: i18n("About KDE") // iconName: "help-about" // onTriggered: { // triggerAction("help_about_kde") // globalDrawer.resetMenu(); // } // } // } // ] ColumnLayout { spacing: 0 Layout.fillWidth: true Layout.leftMargin: -root.leftPadding Layout.rightMargin: -root.rightPadding Kirigami.Separator { Layout.fillWidth: true } //TODO currently disabled while contents have to be ported // ActionListItem { // action: Kirigami.Action { // text: i18n("Statistics") // iconName: "user-properties" // onTriggered: { // root.pageStack.pop(); // root.pageStack.push(profileSettingsPageComponent); // } // } // } // ActionListItem { // action: Kirigami.Action { // text: i18n("Settings") // iconName: "settings-configure" // onTriggered: triggerSettingsDialog() // } // } ActionListItem { action: Kirigami.Action { text: i18n("Download Training") iconName: "get-hot-new-stuff" onTriggered: { root.pageStack.pop(); root.pageStack.push(downloadPageComponent); } } } ActionListItem { action: Kirigami.Action { text: i18n("About") iconName: "help-about" onTriggered: { root.pageStack.pop(); root.pageStack.push(aboutPageComponent); } } } } } diff --git a/src/qml/EditCoursePage.qml b/src/qml/EditCoursePage.qml index 6a675d7..217fe5b 100644 --- a/src/qml/EditCoursePage.qml +++ b/src/qml/EditCoursePage.qml @@ -1,57 +1,75 @@ /* * 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 . */ import QtQuick 2.5 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQml.Models 2.2 import org.kde.kirigami 2.7 as Kirigami import artikulate 1.0 Kirigami.ScrollablePage { id: root title: i18n("Edit Course") actions { left: Kirigami.Action { text: i18n("Previous") tooltip: i18n("Switch to previous phrase.") iconName: "go-previous" enabled: g_editorSession.hasPreviousPhrase onTriggered: g_editorSession.switchToPreviousPhrase() } - right: Kirigami.Action { + main: Kirigami.Action { text: i18n("Next") tooltip: i18n("Switch to next phrase.") iconName: "go-next" enabled: g_editorSession.hasNextPhrase onTriggered: g_editorSession.switchToNextPhrase() } +//TODO backend behavior is broken +// contextualActions: [ +// Kirigami.Action { +// text: i18n("Delete") +// tooltip: i18n("Delete this phrase.") +// iconName: "edit-delete-remove" +// onTriggered: g_editorSession.course.deletePhrase(g_editorSession.phrase) +// }, +// Kirigami.Action { +// separator: true +// }, +// Kirigami.Action { +// text: i18n("Create Phrase") +// tooltip: i18n("Create phrase after current phrase.") +// iconName: "list-add" +// onTriggered: g_editorSession.course.createPhraseAfter(g_editorSession.phrase) +// } +// ] } ColumnLayout { PhraseEditor { visible: g_editorSession.phrase !== null phrase: g_editorSession.phrase isSkeletonPhrase: g_editorSession.skeletonMode Layout.fillHeight: true } } } diff --git a/src/qml/Editor.qml b/src/qml/Editor.qml index 4f4d94a..821ddd7 100644 --- a/src/qml/Editor.qml +++ b/src/qml/Editor.qml @@ -1,69 +1,73 @@ /* * 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 . */ import QtQuick 2.5 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import org.kde.kirigami 2.7 as Kirigami import artikulate 1.0 Kirigami.ApplicationWindow { id: root function changePage(pageItem) { root.pageStack.clear(); root.pageStack.push(pageItem); } globalDrawer: EditorDrawer { pageStack: root.pageStack } + contextDrawer: Kirigami.ContextDrawer { + id: mainFlickable //ID is workaround for Kirigami's assumption that this is the name of the drawer's main flickable + } + pageStack.initialPage: [ editorCourseSelectionPage, editorSkeletonSelectionPage ] pageStack.globalToolBar.style: pageStack.depth > 1 ? Kirigami.ApplicationHeaderStyle.TabBar :Kirigami.ApplicationHeaderStyle.Titles // pages Component { id: editorCourseSelectionPage EditorCourseSelectionPage { } } Component { id: editorSkeletonSelectionPage EditorSkeletonSelectionPage { } } Component { id: editCoursePageComponent EditCoursePage { } } Component { id: courseConfigurationPageComponent CourseConfigurationPage { } } Component { id: repositoryPageComponent RepositoryConfigurationPage { } } Component { id: aboutPageComponent Kirigami.AboutPage { aboutData: g_artikulateAboutData } } } diff --git a/src/qml/EditorDrawer.qml b/src/qml/EditorDrawer.qml index d1a4eee..362d30e 100644 --- a/src/qml/EditorDrawer.qml +++ b/src/qml/EditorDrawer.qml @@ -1,159 +1,159 @@ /* * Copyright 2018 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 . */ import QtQuick 2.5 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.0 as QQC2 import org.kde.kirigami 2.7 as Kirigami import artikulate 1.0 Kirigami.GlobalDrawer { id: root title: "Editor" titleIcon: "artikulate" resetMenuOnTriggered: false bottomPadding: 0 property QtObject pageStack // enforce drawer always to be open modal: false handleVisible: false topContent: [ ColumnLayout { spacing: 0 Layout.fillWidth: true Layout.leftMargin: -root.leftPadding Layout.rightMargin: -root.rightPadding ActionListItem { action: Kirigami.Action { text: i18n("Courses") iconName: "artikulate" onTriggered: { root.pageStack.clear(); root.pageStack.push(editorCourseSelectionPage); root.pageStack.push(editorSkeletonSelectionPage); } } } ActionListItem { action: Kirigami.Action { text: i18n("Repository") iconName: "folder-sync" onTriggered: { root.pageStack.clear(); root.pageStack.push(repositoryPageComponent); } } } Kirigami.Separator { Layout.fillWidth: true } ActionListItem { action: Kirigami.Action { text: i18n("Course Configuration") iconName: "document-properties" enabled: g_editorSession.course !== null onTriggered: { root.pageStack.clear(); root.pageStack.push(courseConfigurationPageComponent); } } } } ] // ordinary Kirigami actions are filled from training units/phrases actions: trainingActions.actions DrawerTrainingActions { id: trainingActions session: g_editorSession - onTriggerTrainingView: { + onTriggerPhraseView: { root.pageStack.clear(); root.pageStack.push(editCoursePageComponent); } } //TODO integrate again // [ // Kirigami.Action { // text: i18n("Help") // iconName: "help-about" // Kirigami.Action { // text: i18n("Artikulate Handbook") // iconName: "help-contents" // onTriggered: { // triggerAction("help_contents"); // globalDrawer.resetMenu(); // } // } // Kirigami.Action { // text: i18n("Report Bug") // iconName: "tools-report-bug" // onTriggered: { // triggerAction("help_report_bug"); // globalDrawer.resetMenu(); // } // } // Kirigami.Action { // text: i18n("About KDE") // iconName: "help-about" // onTriggered: { // triggerAction("help_about_kde") // globalDrawer.resetMenu(); // } // } // } // ] ColumnLayout { spacing: 0 Layout.fillWidth: true Layout.leftMargin: -root.leftPadding Layout.rightMargin: -root.rightPadding Kirigami.Separator { Layout.fillWidth: true } //TODO planned but not implemented // ActionListItem { // action: Kirigami.Action { // text: i18n("Upload Training") // iconName: "get-hot-new-stuff" // onTriggered: { // root.pageStack.pop(); // root.pageStack.push(downloadPageComponent); // } // } // } ActionListItem { action: Kirigami.Action { text: i18n("About") iconName: "help-about" onTriggered: { root.pageStack.pop(); root.pageStack.push(aboutPageComponent); } } } } } diff --git a/src/qml/PhraseEditor.qml b/src/qml/PhraseEditor.qml index c48a934..b2f1755 100644 --- a/src/qml/PhraseEditor.qml +++ b/src/qml/PhraseEditor.qml @@ -1,183 +1,174 @@ /* * 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 . */ import QtQuick 2.10 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import artikulate 1.0 Item { id: root property Phrase phrase property bool isSkeletonPhrase: false // use for saving property int __changedPhraseType property string __changedPhraseText width: 500 height: editLoader.height Component { id: editComponent Row { width: root.width height: { if (!root.isSkeletonPhrase) textEdit.height + phonemeGrid.height + phraseEditStateSetter.height + phraseRecorder.height + phraseTypeSetter.height; else { // height if only editing skeleton textEdit.height + phraseTypeSetter.height; } } ColumnLayout { id: textEdit height: inputLine.height + originalPhraseInfo.height width: parent.width spacing: 5 Row { id: originalPhraseInfo property string originalPhrase : (root.phrase != null) ? root.phrase.i18nText : "" spacing: 10 visible: { root.phrase != null && originalPhrase != "" && !root.isSkeletonPhrase} Text { - text: i18n("Original Phrase: %1", originalPhraseInfo.originalPhrase) + text: i18n("Original Phrase:") + " " + originalPhraseInfo.originalPhrase width: root.width - 70 wrapMode: Text.WordWrap } } RowLayout { // controls for setting phrase id: inputLine TextArea { id: phraseInput property Phrase phrase: root.phrase Layout.fillWidth: true Layout.maximumHeight: 100 text: root.phrase.text onTextChanged: { if (root.phrase == null) { return } root.phrase.text = text } onPhraseChanged: { if (root.phrase != null) text = root.phrase.text else text = "" } } } PhraseEditorTypeComponent { id: phraseTypeSetter phrase: root.phrase } PhraseEditorSoundComponent { id: phraseRecorder visible: !root.isSkeletonPhrase phrase: root.phrase } Component { id: phonemeItem Text { Button { width: 100 text: model.title checkable: true checked: { phrase != null && phrase.hasPhoneme(model.dataRole) } onClicked: { //TODO this button has no undo operation yet if (checked) { phrase.addPhoneme(model.dataRole) } else { phrase.removePhoneme(model.dataRole) } } } } } GridView { id: phonemeGrid property int columns : width / cellWidth width: root.width height: 30 * count / columns + 60 cellWidth: 100 cellHeight: 30 model: PhonemeModel { language: g_editorSession.language } delegate: phonemeItem } RowLayout { id: controls PhraseEditorEditStateComponent { id: phraseEditStateSetter visible: !root.isSkeletonPhrase phrase: root.phrase } Label { // dummy Layout.fillWidth: true } } } } } ColumnLayout { id: phraseRow Row { Text { text: i18n("Unit Title:") } TextField { width: 300 text: g_editorSession.unit.title onEditingFinished: { g_editorSession.unit.title = text } } - Button { // add units only if skeleton - id: newUnitButton - icon.name: "list-add" - text: i18n("Create Phrase") - onClicked: phraseModel.course.createPhrase(unit) - } - Item { //dummy - Layout.fillHeight: true - } } Loader { id: editLoader sourceComponent: (phrase != null) ? editComponent : undefined onSourceComponentChanged: { if (sourceComponent == undefined) height = 0 else height = editComponent.height } } } }