diff --git a/autotests/courseresource/test_courseresource.cpp b/autotests/courseresource/test_courseresource.cpp index fec8f1c..1c92582 100644 --- a/autotests/courseresource/test_courseresource.cpp +++ b/autotests/courseresource/test_courseresource.cpp @@ -1,167 +1,193 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_courseresource.h" #include "resourcerepositorystub.h" #include "core/course.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "core/resources/languageresource.h" #include "core/resources/courseresource.h" #include #include #include - +#include #include #include #include #include #include TestCourseResource::TestCourseResource() { } void TestCourseResource::init() { } void TestCourseResource::cleanup() { } void TestCourseResource::courseSchemeValidationTest() { QUrl schemeFile = QUrl::fromLocalFile(QStringLiteral("schemes/course.xsd")); QXmlSchema courseSchema; QVERIFY(courseSchema.load(schemeFile)); QVERIFY(courseSchema.isValid()); //TODO shall be used in skeleton specific test QUrl skeletonFile = QUrl::fromLocalFile(QStringLiteral("schemes/skeleton.xsd")); QXmlSchema skeletonScheme; QVERIFY(skeletonScheme.load(skeletonFile)); QVERIFY(skeletonScheme.isValid()); } void TestCourseResource::loadCourseResource() { Language language; language.setId("de"); ResourceRepositoryStub repository({&language}); const QString courseDirectory = "data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; CourseResource course(QUrl::fromLocalFile(courseFile), &repository); QCOMPARE(course.file().toLocalFile(), courseFile); QCOMPARE(course.id(), "de"); + QCOMPARE(course.foreignId(), "artikulate-basic"); QCOMPARE(course.title(), "Artikulate Deutsch"); QCOMPARE(course.description(), "Ein Kurs in (hoch-)deutscher Aussprache."); QVERIFY(course.language() != nullptr); QCOMPARE(course.language()->id(), "de"); QCOMPARE(course.unitList().count(), 1); const auto unit = course.unitList().first(); QVERIFY(unit != nullptr); QCOMPARE(unit->id(), "1"); QCOMPARE(unit->title(), QStringLiteral("Auf der Straße")); QCOMPARE(unit->foreignId(), "{dd60f04a-eb37-44b7-9787-67aaf7d3578d}"); QCOMPARE(unit->phraseList().count(), 3); // note: this test takes the silent assumption that phrases are added to the list in same // order as they are defined in the file. This assumption should be made explicit or dropped const auto firstPhrase = unit->phraseList().first(); QVERIFY(firstPhrase != nullptr); QCOMPARE(firstPhrase->id(), "1"); QCOMPARE(firstPhrase->foreignId(), "{3a4c1926-60d7-44c6-80d1-03165a641c75}"); QCOMPARE(firstPhrase->text(), "Guten Tag."); QCOMPARE(firstPhrase->soundFileUrl(), courseDirectory + "de_01.ogg"); QCOMPARE(firstPhrase->type(), Phrase::Type::Sentence); QVERIFY(firstPhrase->phonemes().isEmpty()); } +void TestCourseResource::addUnitHandling() +{ + // boilerplate + Language language; + language.setId("de"); + ResourceRepositoryStub repository({&language}); + const QString courseDirectory = "data/courses/de/"; + const QString courseFile = courseDirectory + "de.xml"; + CourseResource course(QUrl::fromLocalFile(courseFile), &repository); + + // begin of test + Unit unit; + unit.setId("testunit"); + const int initialUnitNumber = course.unitList().count(); + QCOMPARE(initialUnitNumber, 1); + QSignalSpy spyAboutToBeAdded(&course, SIGNAL(unitAboutToBeAdded(Unit*, int))); + QSignalSpy spyAdded(&course, SIGNAL(unitAdded())); + QCOMPARE(spyAboutToBeAdded.count(), 0); + QCOMPARE(spyAdded.count(), 0); + course.addUnit(&unit); + QCOMPARE(course.unitList().count(), initialUnitNumber + 1); + QCOMPARE(spyAboutToBeAdded.count(), 1); + QCOMPARE(spyAdded.count(), 1); +} + //TODO test signals // FIXME porting break void TestCourseResource::fileLoadSaveCompleteness() { // ResourceManager manager; // manager.addLanguage(QUrl::fromLocalFile(QStringLiteral("data/languages/de.xml"))); // manager.addCourse(QUrl::fromLocalFile(QStringLiteral("data/courses/de.xml"))); // // test to encure further logic // QVERIFY(manager.courseResources(manager.languageResources().constFirst()->language()).count() == 1); // Course *testCourse = manager.courseResources(manager.languageResources().constFirst()->language()).constFirst()->course(); // QTemporaryFile outputFile; // outputFile.open(); // QUrl oldFileName = testCourse->file(); // testCourse->setFile(QUrl::fromLocalFile(outputFile.fileName())); // testCourse->setLanguage(manager.languageResources().constFirst()->language()); // testCourse->sync(); // testCourse->setFile(oldFileName); // restore for later tests // QFile file(outputFile.fileName()); // if (!file.open(QIODevice::ReadOnly)) { // qCritical() << "Could not open file to read."; // } // //TODO this only works, since the resource manager not checks uniqueness of course ids! // manager.addCourse(QUrl::fromLocalFile(outputFile.fileName())); // Course *compareCourse = manager.courseResources(manager.languageResources().constFirst()->language()).constLast()->course(); // // test that we actually call the different files // QVERIFY(testCourse->file().toLocalFile() != compareCourse->file().toLocalFile()); // QVERIFY(testCourse->id() == compareCourse->id()); // QVERIFY(testCourse->foreignId() == compareCourse->foreignId()); // QVERIFY(testCourse->title() == compareCourse->title()); // QVERIFY(testCourse->description() == compareCourse->description()); // QVERIFY(testCourse->language()->id() == compareCourse->language()->id()); // QVERIFY(testCourse->unitList().count() == compareCourse->unitList().count()); // Unit *testUnit = testCourse->unitList().constFirst(); // Unit *compareUnit = compareCourse->unitList().constFirst(); // QVERIFY(testUnit->id() == compareUnit->id()); // QVERIFY(testUnit->foreignId() == compareUnit->foreignId()); // QVERIFY(testUnit->title() == compareUnit->title()); // QVERIFY(testUnit->phraseList().count() == compareUnit->phraseList().count()); // Phrase *testPhrase = testUnit->phraseList().constFirst(); // Phrase *comparePhrase = new Phrase(this); // // Note that this actually means that we DO NOT respect phrase orders by list order! // foreach (Phrase *phrase, compareUnit->phraseList()) { // if (testPhrase->id() == phrase->id()) { // comparePhrase = phrase; // break; // } // } // QVERIFY(testPhrase->id() == comparePhrase->id()); // QVERIFY(testPhrase->foreignId() == comparePhrase->foreignId()); // QVERIFY(testPhrase->text() == comparePhrase->text()); // QVERIFY(testPhrase->type() == comparePhrase->type()); // QVERIFY(testPhrase->sound().fileName() == comparePhrase->sound().fileName()); // QVERIFY(testPhrase->phonemes().count() == comparePhrase->phonemes().count()); // //FIXME implement phoneme checks after phonemes are fully implemented } QTEST_GUILESS_MAIN(TestCourseResource) diff --git a/autotests/courseresource/test_courseresource.h b/autotests/courseresource/test_courseresource.h index ac745a7..650049a 100644 --- a/autotests/courseresource/test_courseresource.h +++ b/autotests/courseresource/test_courseresource.h @@ -1,62 +1,70 @@ /* * Copyright 2013-2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef TESTCOURSERESOURCE_H #define TESTCOURSERESOURCE_H #include #include class TestCourseResource : public QObject { Q_OBJECT public: TestCourseResource(); private slots: /** * Called before every test case. */ void init(); /** * Called after every test case. */ void cleanup(); /** * Test if course XSD specification is valid. */ void courseSchemeValidationTest(); + /** + * Test simple loading of course resource XML file + */ void loadCourseResource(); + /** + * Test handling of unit insertions (specifically, the signals) + */ + void addUnitHandling(); + /** * Test if serialization of unserialized file gives original file. * TODO this is a test by only string equality and should improved to test on a data level */ void fileLoadSaveCompleteness(); private: bool m_systemUseCourseRepositoryValue; }; #endif diff --git a/autotests/trainingsession/test_trainingsession.cpp b/autotests/trainingsession/test_trainingsession.cpp index 03406c0..dce0278 100644 --- a/autotests/trainingsession/test_trainingsession.cpp +++ b/autotests/trainingsession/test_trainingsession.cpp @@ -1,253 +1,257 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_trainingsession.h" #include "src/core/trainingsession.h" #include "src/core/icourse.h" #include "src/core/language.h" #include "src/core/unit.h" #include "src/core/trainingaction.h" #include "liblearnerprofile/src/profilemanager.h" #include #include // assumption: during a training session the units and phrases of a course do not change // any change of such a course shall result in a reload of a training session class CourseStub : public ICourse { public: CourseStub(Language *language, QVector units) : m_language(language) , m_units(units) { } ~CourseStub() override; QString id() const override { return "courseid"; } + QString foreignId() const override + { + return "foreigncourseid"; + } QString title() const override { return "title"; } QString i18nTitle() const override { return "i18n title"; } QString description() const override { return "description of the course"; } Language * language() const override { return m_language; } QList unitList() override { return m_units.toList(); } QUrl file() const override { return QUrl(); } private: Language *m_language{nullptr}; QVector m_units; }; // define one virtual method out of line to pin CourseStub to this translation unit CourseStub::~CourseStub() = default; void TestTrainingSession::init() { // TODO initialization of test case } void TestTrainingSession::cleanup() { // TODO cleanup after test run } void TestTrainingSession::createTrainingSessionWithoutUnits() { Language language; CourseStub course(&language, QVector()); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::createTrainingSessionWithEmptySounds() { Language language; Unit unitA; Unit unitB; Phrase phraseA1; Phrase phraseA2; Phrase phraseB1; Phrase phraseB2; // note: phrases without soundfiles are skipped in session generation phraseA1.setId("A1"); phraseA2.setId("A2"); phraseB1.setId("B1"); phraseB2.setId("B2"); phraseA1.setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); unitA.addPhrase(&phraseA1); unitA.addPhrase(&phraseA2); unitB.addPhrase(&phraseB1); unitB.addPhrase(&phraseB2); CourseStub course(&language, QVector({&unitA, &unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); // test number of actions auto actions = session.trainingActions(); QCOMPARE(actions.count(), 1); QCOMPARE(actions.at(0)->actions().count(), 1); } void TestTrainingSession::createTrainingSessionWithEmptyUnits() { Language language; Unit firstUnit; Unit secondUnit; CourseStub course(&language, QVector({&firstUnit, &secondUnit})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::createTrainingSessionWithUnitsAndPhrases() { Language language; Unit unit; Phrase firstPhrase; Phrase secondPhrase; unit.addPhrase(&firstPhrase); unit.addPhrase(&secondPhrase); CourseStub course(&language, QVector({&unit})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::iterateCourse() { Language language; Unit unitA; Unit unitB; Phrase phraseA1; Phrase phraseA2; Phrase phraseB1; Phrase phraseB2; // note: phrases without soundfiles are skipped in session generation phraseA1.setId("A1"); phraseA2.setId("A2"); phraseB1.setId("B1"); phraseB2.setId("B2"); phraseA1.setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseA2.setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseB1.setSound(QUrl::fromLocalFile("/tmp/b1.ogg")); phraseB2.setSound(QUrl::fromLocalFile("/tmp/b2.ogg")); unitA.addPhrase(&phraseA1); unitA.addPhrase(&phraseA2); unitB.addPhrase(&phraseB1); unitB.addPhrase(&phraseB2); CourseStub course(&language, QVector({&unitA, &unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); // session assumed to initialize with first units's first phrase QCOMPARE(session.activeUnit(), &unitA); QCOMPARE(session.activePhrase(), &phraseA1); QVERIFY(&course == session.course()); // test direct unit setters session.setUnit(&unitA); QCOMPARE(session.activeUnit(), &unitA); session.setUnit(&unitB); QCOMPARE(session.activeUnit(), &unitB); // test direct phrase setters session.setPhrase(&phraseA1); QCOMPARE(session.activePhrase(), &phraseA1); QCOMPARE(session.activeUnit(), &unitA); session.setPhrase(&phraseB1); QCOMPARE(session.activePhrase(), &phraseB1); QCOMPARE(session.activeUnit(), &unitB); // test number of actions auto actions = session.trainingActions(); QCOMPARE(actions.count(), 2); QCOMPARE(actions.at(0)->actions().count(), 2); QCOMPARE(actions.at(1)->actions().count(), 2); // test phrase iterators: accept iterator session.setPhrase(&phraseA1); QCOMPARE(session.activeUnit(), &unitA); QCOMPARE(session.activePhrase(), &phraseA1); QVERIFY(session.hasNext()); session.accept(); QCOMPARE(session.activeUnit(), &unitA); QCOMPARE(session.activePhrase(), &phraseA2); session.accept(); QCOMPARE(session.activePhrase(), &phraseB1); session.accept(); QCOMPARE(session.activePhrase(), &phraseB2); QVERIFY(!session.hasNext()); // test phrase iterators: skip iterator session.setPhrase(&phraseA1); QCOMPARE(session.activeUnit(), &unitA); QCOMPARE(session.activePhrase(), &phraseA1); QVERIFY(!session.hasPrevious()); QVERIFY(session.hasNext()); session.skip(); QCOMPARE(session.activeUnit(), &unitA); QCOMPARE(session.activePhrase(), &phraseA2); session.skip(); QCOMPARE(session.activePhrase(), &phraseB1); session.skip(); QCOMPARE(session.activePhrase(), &phraseB2); QVERIFY(session.hasPrevious()); QVERIFY(!session.hasNext()); // test completed signal QSignalSpy spy(&session, SIGNAL(completed())); session.setPhrase(&phraseB1); session.accept(); QCOMPARE(spy.count(), 0); session.accept(); QCOMPARE(spy.count(), 1); } QTEST_GUILESS_MAIN(TestTrainingSession) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 60d4c84..e962d0b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,201 +1,203 @@ ### # Copyright 2013-2015 Andreas Cord-Landwehr # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ### ecm_setup_version(0.99.90 VARIABLE_PREFIX ARTIKULATE VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/ArtikulateConfigVersion.cmake" ) ecm_optional_add_subdirectory(qml) # set include directories include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${artikulate_SOURCE_DIR} ) # set the source code files from which Artikulate is compiled set(artikulateCore_SRCS core/icourse.h core/drawertrainingactions.cpp core/course.cpp core/resourcemanager.cpp core/iresourcerepository.h core/resourcerepository.cpp core/language.cpp core/phrase.cpp core/phoneme.cpp core/phonemegroup.cpp core/unit.cpp core/skeleton.cpp core/editorsession.cpp core/trainingaction.cpp core/trainingactionicon.cpp core/trainingsession.cpp core/resources/resourceinterface.cpp core/resources/languageresource.cpp + core/resources/courseparser.cpp core/resources/courseresource.cpp + core/resources/editablecourseresource.cpp core/resources/skeletonresource.cpp core/player.cpp core/recorder.cpp qmlcontrols/iconitem.cpp qmlcontrols/imagetexturescache.cpp qmlcontrols/managedtexturenode.cpp artikulate_debug.cpp ) kconfig_add_kcfg_files (artikulateCore_SRCS settings.kcfgc) add_library(artikulatecore SHARED ${artikulateCore_SRCS}) generate_export_header(artikulatecore BASE_NAME artikulatecore) target_link_libraries(artikulatecore LINK_PUBLIC artikulatelearnerprofile artikulatesound Qt5::XmlPatterns Qt5::Quick KF5::Archive KF5::ConfigGui ) # internal library without any API or ABI guarantee set(GENERIC_LIB_VERSION "0") set(GENERIC_LIB_SOVERSION "0") set_target_properties( artikulatecore PROPERTIES VERSION ${GENERIC_LIB_VERSION} SOVERSION ${GENERIC_LIB_SOVERSION} ) install( TARGETS artikulatecore DESTINATION ${INSTALL_TARGETS_DEFAULT_ARGS} ) install(FILES artikulate.knsrc DESTINATION ${CONFIG_INSTALL_DIR}) # set the source code files from which Artikulate is compiled set(artikulate_SRCS main.cpp mainwindow.cpp application.cpp artikulate_debug.cpp models/coursemodel.cpp models/coursefiltermodel.cpp models/languagemodel.cpp models/languageresourcemodel.cpp # models/learningprogressmodel.cpp //TODO must be adapted to new trainingsession models/unitmodel.cpp models/unitfiltermodel.cpp models/phrasemodel.cpp models/phraselistmodel.cpp models/phrasefiltermodel.cpp models/phonememodel.cpp models/phonemegroupmodel.cpp models/phonemeunitmodel.cpp models/profilemodel.cpp models/skeletonmodel.cpp ui/sounddevicedialogpage.cpp ui/appearencedialogpage.cpp ui/resourcesdialogpage.cpp ) ki18n_wrap_ui (artikulate_SRCS ui/resourcesdialogpage.ui ui/sounddevicedialogpage.ui ui/appearencedialogpage.ui ) qt5_add_resources(artikulate_SRCS resources.qrc) qt5_add_resources(artikulate_SRCS ../data/languages.qrc) kconfig_add_kcfg_files (artikulate_SRCS settings.kcfgc) set(artikulate_editor_SRCS main_editor.cpp mainwindow_editor.cpp application.cpp artikulate_debug.cpp models/coursemodel.cpp models/coursefiltermodel.cpp models/languagemodel.cpp models/languageresourcemodel.cpp # models/learningprogressmodel.cpp //TODO must be adapted to new trainingsession models/unitmodel.cpp models/unitfiltermodel.cpp models/phrasemodel.cpp models/phraselistmodel.cpp models/phrasefiltermodel.cpp models/phonememodel.cpp models/phonemegroupmodel.cpp models/phonemeunitmodel.cpp models/profilemodel.cpp models/skeletonmodel.cpp ui/sounddevicedialogpage.cpp ui/appearencedialogpage.cpp ui/resourcesdialogpage.cpp ui/exportghnsdialog.cpp ) ki18n_wrap_ui(artikulate_editor_SRCS ui/appearencedialogpage.ui ui/exportghnsdialog.ui ui/resourcesdialogpage.ui ui/sounddevicedialogpage.ui ) qt5_add_resources(artikulate_editor_SRCS resources.qrc) kconfig_add_kcfg_files (artikulate_editor_SRCS settings.kcfgc) # executables add_executable(artikulate ${artikulate_SRCS}) target_link_libraries(artikulate LINK_PUBLIC artikulatelearnerprofile artikulatesound artikulatecore Qt5::Qml Qt5::Quick KF5::Crash KF5::NewStuff KF5::XmlGui ) qt5_add_resources(artikulate_editor_SRCS editor.qrc) add_executable(artikulate_editor ${artikulate_editor_SRCS}) target_link_libraries(artikulate_editor LINK_PUBLIC artikulatesound artikulatecore Qt5::Qml Qt5::Quick Qt5::QuickWidgets KF5::Crash KF5::NewStuff KF5::XmlGui ) install(FILES artikulate.kcfg DESTINATION ${KCFG_INSTALL_DIR}) install(TARGETS artikulate ${INSTALL_TARGETS_DEFAULT_ARGS}) install(TARGETS artikulate_editor ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/core/course.cpp b/src/core/course.cpp index def6416..97401e8 100644 --- a/src/core/course.cpp +++ b/src/core/course.cpp @@ -1,340 +1,325 @@ /* * 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 "course.h" #include "unit.h" #include "language.h" #include "resources/resourceinterface.h" #include "resources/courseresource.h" #include "resourcemanager.h" #include "phonemegroup.h" #include "artikulate_debug.h" #include #include #include #include Course::Course(ResourceInterface *resource) : ICourse(resource) , m_resource(qobject_cast(resource)) , m_language(nullptr) , m_modified(false) { } Course::~Course() { foreach (Unit *unit, m_unitList) { unit->deleteLater(); } m_unitList.clear(); // clear phonom units QMultiMap< PhonemeGroup*, QList< QPair > >::iterator groupIter = m_phonemeUnitList.begin(); while (groupIter != m_phonemeUnitList.end()) { QList< QPair >::iterator itemIter = groupIter->begin(); while (itemIter != groupIter->end()) { itemIter->first->deleteLater(); // delete phoneme itemIter->second->deleteLater(); // delete unit ++itemIter; } groupIter->clear(); ++groupIter; } m_phonemeUnitList.clear(); m_phonemeGroupList.clear(); } QString Course::id() const { return m_id; } void Course::setId(const QString &id) { if (id != m_id) { m_id = id; emit idChanged(); setModified(); } } QString Course::foreignId() const { return m_foreignId; } void Course::setForeignId(const QString &id) { m_foreignId = id; } QString Course::title() const { return m_title; } QString Course::i18nTitle() const { return m_resource->i18nTitle(); } void Course::setTitle(const QString &title) { if (QString::compare(title, m_title) != 0) { m_title = title; emit titleChanged(); setModified(); } } QString Course::description() const { return m_description; } void Course::setDescription(const QString &description) { m_description = description; emit descriptionChanged(); } Language * Course::language() const { return m_language; } void Course::setLanguage(Language *language) { Q_ASSERT(language); // TODO this should happen in the ctor foreach (PhonemeGroup *group, language->phonemeGroups()) { addPhonemeGroup(group); } m_language = language; emit languageChanged(); } QUrl Course::file() const { return m_file; } void Course::setFile(const QUrl &file) { m_file = file; } QList< Unit* > Course::unitList() { return m_unitList; } void Course::addUnit(Unit *unit) { QList::ConstIterator iter = m_unitList.constBegin(); while (iter != m_unitList.constEnd()) { if (unit->id() == (*iter)->id()) { qCWarning(ARTIKULATE_LOG) << "Unit already contained in this course, aborting"; return; } ++iter; } emit unitAboutToBeAdded(unit, m_unitList.length()); m_unitList.append(unit); connect(unit, &Unit::modified, this, [=]() { setModified(true); }); // these connections are only present for "normal units" and take care to register // there phrases also at phoneme units // TODO: removing of phrase and upading of phonemes for that case is not implemented connect(unit, &Unit::phraseAdded, this, &Course::registerPhrasePhonemes); emit unitAdded(); setModified(true); } Unit * Course::createUnit() { // find first unused id QStringList unitIds; foreach (Unit *unit, m_unitList) { 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 Unit *unit = new Unit(this); unit->setCourse(this); unit->setId(id); unit->setTitle(i18n("New Unit")); addUnit(unit); return unit; } Phrase * Course::createPhrase(Unit *unit) { // find globally unique phrase id inside course QStringList phraseIds; foreach (Unit *unit, m_unitList) { foreach (Phrase *phrase, unit->phraseList()) { phraseIds.append(phrase->id()); } } QString id = QUuid::createUuid().toString(); while (phraseIds.contains(id)) { id = QUuid::createUuid().toString(); qCWarning(ARTIKULATE_LOG) << "Phrase id generator has found a collision, recreating id."; } // create unit Phrase *phrase = new Phrase(this); phrase->setId(id); phrase->setText(QLatin1String("")); phrase->setType(Phrase::Word); unit->addPhrase(phrase); return phrase; } QList< Unit* > Course::phonemeUnitList(PhonemeGroup *phonemeGroup) const { QList list; for (const auto &group : m_phonemeUnitList.value(phonemeGroup)) { list.append(group.second); } return list; } Unit * Course::phonemeUnit(Phoneme *phoneme) const { for (auto group = m_phonemeUnitList.keyBegin(); group != m_phonemeUnitList.keyEnd(); ++group) { m_phonemeUnitList.value(*group); for (const auto &phonemeUnit : m_phonemeUnitList.value(*group)) { if (phonemeUnit.first == phoneme) { return phonemeUnit.second; } } } return nullptr; } PhonemeGroup * Course::phonemeGroup(Unit *unit) const { for (auto group = m_phonemeUnitList.keyBegin(); group != m_phonemeUnitList.keyEnd(); ++group) { m_phonemeUnitList.value(*group); for (const auto &phonemeUnit : m_phonemeUnitList.value(*group)) { if (phonemeUnit.second == unit) { return *group; } } } return nullptr; } void Course::addPhonemeGroup(PhonemeGroup *phonemeGroup) { if (m_phonemeUnitList.contains(phonemeGroup)) { qCWarning(ARTIKULATE_LOG) << "Phoneme group already contained in this course, aborting"; return; } emit phonemeGroupAboutToBeAdded(phonemeGroup, m_phonemeGroupList.count()); // add to phoneme list m_phonemeGroupList.append(phonemeGroup); m_phonemeUnitList.insert(phonemeGroup, QList< QPair >()); emit phonemeGroupAdded(); setModified(); } QList Course::phonemeGroupList() const { return m_phonemeGroupList; } bool Course::modified() const { return m_modified; } void Course::setModified(bool modified) { if (m_modified == modified) { return; } m_modified = modified; emit modifiedChanged(); } -void Course::sync() -{ - if (!m_file.isValid() || m_file.isEmpty() || m_resource == nullptr) { - qCritical() << "Path" << m_file.toLocalFile() << "not valid, aborting sync operation."; - return; - } - m_resource->sync(); - setModified(false); -} - -//bool Course::isContributorResource() const -//{ -// return m_resource->isContributorResource(); -//} - void Course::registerPhrasePhonemes(Phrase *phrase) { // iterate over all phonemes of this phrase foreach (Phoneme *phoneme, phrase->phonemes()) { // try to find corresponding phonem groups (phonem groups are registered on course creation) foreach (PhonemeGroup *group, m_phonemeGroupList) { if (!group->contains(phoneme)) { continue; } // either add phrase to existing unit or register a new one bool phraseRegistered = false; for (const auto &phonemeUnit : m_phonemeUnitList.value(group)) { if (phonemeUnit.first->id() == phoneme->id()) { phonemeUnit.second->addPhrase(phrase); phraseRegistered = true; } } // otherwise, need to create a new unit if (phraseRegistered == false) { // create unit based on the phoneme group Unit *unit = new Unit(this); unit->setId(phoneme->id()); unit->setTitle(phoneme->title()); unit->setCourse(this); m_phonemeUnitList[group].append(qMakePair(phoneme, unit)); unit->addPhrase(phrase); } } } } diff --git a/src/core/course.h b/src/core/course.h index 4d9e1b7..a25edeb 100644 --- a/src/core/course.h +++ b/src/core/course.h @@ -1,140 +1,142 @@ /* * 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 COURSE_H #define COURSE_H #include "artikulatecore_export.h" #include "icourse.h" #include #include #include class ResourceInterface; class CourseResource; class QString; class Language; class Unit; class Phrase; class PhonemeGroup; class Phoneme; class ARTIKULATECORE_EXPORT Course : public ICourse { Q_OBJECT Q_INTERFACES(ICourse) Q_PROPERTY(QString id READ id WRITE setId NOTIFY idChanged) Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) Q_PROPERTY(QString i18nTitle READ i18nTitle NOTIFY titleChanged) Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged) Q_PROPERTY(bool modified READ modified WRITE setModified NOTIFY modifiedChanged) Q_PROPERTY(Language * language READ language NOTIFY languageChanged) public: explicit Course(ResourceInterface *resource = nullptr); ~Course() override; QString id() const override; void setId(const QString &id); QString foreignId() const; void setForeignId(const QString &id); QString title() const override; QString i18nTitle() const override; void setTitle(const QString &title); Language * language() const override; void setLanguage(Language *language); QString description() const override; void setDescription(const QString &description); QUrl file() const override; void setFile(const QUrl &file); QList unitList() override; QList phonemeUnitList(PhonemeGroup *phonemeGroup) const; /** * \return the corresponding unit for phoneme \p phoneme */ Unit * phonemeUnit(Phoneme *phoneme) const; /** * \return the phoneme group containing the phoneme corresponding to \p unit */ PhonemeGroup * phonemeGroup(Unit *unit) const; void addUnit(Unit *unit); QList phonemeGroupList() const; void addPhonemeGroup(PhonemeGroup *phonemeGroup); /** * Create and add a new unit to course. * * \return pointer to the created unit */ Q_INVOKABLE Unit * createUnit(); /** * Create and add a new phrase and add it to the specified unit. The type of the created phrase * is initially Phrase::Word. * * \param unit the unit to that the created hprase shall be added * \return pointer to the created phrase */ Q_INVOKABLE Phrase * createPhrase(Unit *unit); /** * \return true if the course was modified after the last sync, otherwise false */ virtual bool modified() const; /** * Writes course object back to file and set \ref modified state to false. * If no file is set, no operation is performed. */ - virtual Q_INVOKABLE void sync(); + virtual Q_INVOKABLE void sync(){ + //FIXME + }; bool isContributorResource() const; public Q_SLOTS: void setModified(bool modified = true); void registerPhrasePhonemes(Phrase *phrase); Q_SIGNALS: void modifiedChanged(); void unitAdded(); void unitAboutToBeAdded(Unit*,int); void unitsRemoved(); void unitsAboutToBeRemoved(int,int); void phonemeGroupAdded(); void phonemeGroupAboutToBeAdded(PhonemeGroup*,int); void phonemeGroupRemoved(); void phonemeGroupAboutToBeRemoved(int,int); private: Q_DISABLE_COPY(Course) CourseResource * const m_resource; QString m_id; QString m_foreignId; QString m_title; QString m_description; Language *m_language; QUrl m_file; bool m_modified; QList m_unitList; QList m_phonemeGroupList; QMap>> m_phonemeUnitList; }; #endif // COURSE_H diff --git a/src/core/icourse.h b/src/core/icourse.h index 85981e9..d319ac5 100644 --- a/src/core/icourse.h +++ b/src/core/icourse.h @@ -1,73 +1,74 @@ /* * 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 class QString; class Language; class Unit; 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 NOTIFY titleChanged) Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) Q_PROPERTY(Language * language NOTIFY languageChanged) public: ICourse(QObject *parent = nullptr) : QObject(parent) { } virtual ~ICourse() = default; virtual QString id() const = 0; + virtual QString foreignId() const = 0; virtual QString title() const = 0; virtual QString i18nTitle() const = 0; virtual QString description() const = 0; virtual Language * language() const = 0; /** * @brief Lazy loading unit list * @return list of units in course */ virtual QList unitList() = 0; virtual QUrl file() const = 0; Q_SIGNALS: void idChanged(); void titleChanged(); void descriptionChanged(); void languageChanged(); void unitAdded(); void unitAboutToBeAdded(Unit*,int); void unitsRemoved(); void unitsAboutToBeRemoved(int,int); }; Q_DECLARE_INTERFACE(ICourse, "com.kde.artikulate.ICourse/1.0") #endif // COURSE_H diff --git a/src/core/resourcemanager.cpp b/src/core/resourcemanager.cpp index 013e885..b2d7beb 100644 --- a/src/core/resourcemanager.cpp +++ b/src/core/resourcemanager.cpp @@ -1,474 +1,474 @@ /* * 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 "resourcemanager.h" #include "language.h" #include "course.h" #include "skeleton.h" #include "unit.h" #include "phrase.h" #include "phoneme.h" #include "phonemegroup.h" #include "resources/languageresource.h" #include "resources/courseresource.h" #include "resources/skeletonresource.h" #include "settings.h" #include "liblearnerprofile/src/profilemanager.h" #include "liblearnerprofile/src/learninggoal.h" #include #include #include #include #include #include #include #include #include #include "artikulate_debug.h" #include #include ResourceManager::ResourceManager(QObject *parent) : QObject(parent) { } void ResourceManager::loadCourseResources() { //TODO fix this method such that it may be called many times of e.g. updating // reload config, could be changed in dialogs Settings::self()->load(); // register skeleton resources QDir skeletonRepository = QDir(Settings::courseRepositoryPath()); skeletonRepository.setFilter(QDir::Files | QDir::Hidden); if (!skeletonRepository.cd(QStringLiteral("skeletons"))) { qCritical() << "There is no subdirectory \"skeletons\" in directory " << skeletonRepository.path() << " cannot load skeletons."; } else { // read skeletons QFileInfoList list = skeletonRepository.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); addSkeleton(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); } } // register contributor course files QDir courseRepository = QDir(Settings::courseRepositoryPath()); if (!courseRepository.cd(QStringLiteral("courses"))) { qCritical() << "There is no subdirectory \"courses\" in directory " << courseRepository.path() << " cannot load courses."; } else { // find courses courseRepository.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QFileInfoList courseDirList = courseRepository.entryInfoList(); // traverse all course directories foreach (const QFileInfo &info, courseDirList) { QDir courseDir = QDir(info.absoluteFilePath()); courseDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QFileInfoList courseLangDirList = courseDir.entryInfoList(); // traverse all language directories for each course foreach (const QFileInfo &langInfo, courseLangDirList) { QDir courseLangDir = QDir(langInfo.absoluteFilePath()); courseLangDir.setFilter(QDir::Files); QStringList nameFilters; nameFilters.append(QStringLiteral("*.xml")); QFileInfoList courses = courseLangDir.entryInfoList(nameFilters); // find and add course files foreach (const QFileInfo &courseInfo, courses) { CourseResource * course = addCourse(QUrl::fromLocalFile(courseInfo.filePath())); if (course != nullptr) { // course->setContributorResource(true); } } } } } // register GHNS course resources QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::DataLocation); foreach (const QString &testdir, dirs) { QDirIterator it(testdir + "/courses/", QDirIterator::Subdirectories); while (it.hasNext()) { QDir dir(it.next()); dir.setFilter(QDir::Files | QDir::NoSymLinks); QFileInfoList list = dir.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); if (fileInfo.completeSuffix() != QLatin1String("xml")) { continue; } addCourse(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); } } } //TODO this signal should only be emitted when repository was added/removed // yet the call to this method is very seldom and emitting it too often is not that harmful emit repositoryChanged(); } void ResourceManager::loadLanguageResources() { // load language resources // all other resources are only loaded on demand QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); foreach (const QString &testdir, dirs) { QDir dir(testdir + "/artikulate/languages/"); dir.setFilter(QDir::Files | QDir::NoSymLinks); QFileInfoList list = dir.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); if (fileInfo.completeSuffix() != QLatin1String("xml")) { continue; } addLanguage(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); } } } void ResourceManager::sync() { - QMap< QString, QList< CourseResource* > >::iterator iter; - for (iter = m_courseResources.begin(); iter != m_courseResources.end(); ++iter) { - foreach (auto const &courseRes, iter.value()) { - courseRes->sync(); - } - } - foreach (auto const &courseRes, m_skeletonResources) { - courseRes->sync(); - } +// QMap< QString, QList< CourseResource* > >::iterator iter; +// for (iter = m_courseResources.begin(); iter != m_courseResources.end(); ++iter) { +// foreach (auto const &courseRes, iter.value()) { +// courseRes->sync(); +// } +// } +// foreach (auto const &courseRes, m_skeletonResources) { +// courseRes->sync(); +// } } bool ResourceManager::modified() const { QMap< QString, QList< CourseResource* > >::const_iterator iter; for (iter = m_courseResources.constBegin(); iter != m_courseResources.constEnd(); ++iter) { foreach (auto const &courseRes, iter.value()) { if (courseRes->isOpen() && courseRes->course()->modified()) { return true; } } } foreach (auto const &courseRes, m_skeletonResources) { if (courseRes->isOpen() && courseRes->skeleton()->modified()) { return true; } } return false; } void ResourceManager::addLanguage(const QUrl &languageFile) { if (m_loadedResources.contains(languageFile.toLocalFile())) { return; } LanguageResource *resource = new LanguageResource(this, languageFile); emit languageResourceAboutToBeAdded(resource, m_languageResources.count()); m_languageResources.append(resource); m_loadedResources.append(languageFile.toLocalFile()); m_courseResources.insert(resource->identifier(), QList()); emit languageResourceAdded(); } bool ResourceManager::isRepositoryManager() const { return !Settings::courseRepositoryPath().isEmpty(); } QString ResourceManager::repositoryUrl() const { return Settings::courseRepositoryPath(); } QList< LanguageResource* > ResourceManager::languageResources() const { return m_languageResources; } Language * ResourceManager::language(int index) const { Q_ASSERT(index >= 0 && index < m_languageResources.count()); return m_languageResources.at(index)->language(); } Language * ResourceManager::language(LearnerProfile::LearningGoal *learningGoal) const { if (!learningGoal) { return nullptr; } if (learningGoal->category() != LearnerProfile::LearningGoal::Language) { qCritical() << "Cannot translate non-language learning goal to language"; return nullptr; } foreach (LanguageResource *resource, m_languageResources) { if (resource->identifier() == learningGoal->identifier()) { return resource->language(); } } qCritical() << "No language registered with identifier " << learningGoal->identifier() << ": aborting"; return nullptr; } QList< CourseResource* > ResourceManager::courseResources(Language *language) { if (!language) { QList courses; for (auto iter = m_courseResources.constBegin(); iter != m_courseResources.constEnd(); ++iter) { courses.append(iter.value()); } return courses; } // return empty list if no course available for language if (!m_courseResources.contains(language->id())) { return QList< CourseResource* >(); } return m_courseResources[language->id()]; } Course * ResourceManager::course(Language *language, int index) const { Q_ASSERT(m_courseResources.contains(language->id())); Q_ASSERT(index >= 0 && index < m_courseResources[language->id()].count()); return m_courseResources[language->id()].at(index)->course(); } void ResourceManager::reloadCourseOrSkeleton(Course *courseOrSkeleton) { if (!courseOrSkeleton) { qCritical() << "Cannot reload non-existing course"; return; } if (!courseOrSkeleton->file().isValid()) { qCritical() << "Cannot reload temporary file, aborting."; return; } // figure out if this is a course or a skeleton if (courseOrSkeleton->language()) { // only course files have a language //TODO better add a check if this is contained in the course list // to catch possible errors QUrl file = courseOrSkeleton->file(); m_loadedResources.removeOne(courseOrSkeleton->file().toLocalFile()); removeCourse(courseOrSkeleton); addCourse(file); } else { foreach (SkeletonResource *resource, m_skeletonResources) { if (resource->identifier() == courseOrSkeleton->id()) { resource->reload(); return; } } } } void ResourceManager::updateCourseFromSkeleton(Course *course) { //TODO implement status information that are shown at mainwindow if (course->foreignId().isEmpty()) { qCritical() << "No skeleton ID specified, aborting update."; return; } Course *skeleton = nullptr; QList::ConstIterator iter = m_skeletonResources.constBegin(); while (iter != m_skeletonResources.constEnd()) { if ((*iter)->identifier() == course->foreignId()) { skeleton = (*iter)->skeleton(); break; } ++iter; } if (!skeleton) { qCritical() << "Could not find skeleton with id " << course->foreignId() << ", aborting update."; return; } // update now foreach (Unit *unitSkeleton, skeleton->unitList()) { // import unit if not exists Unit *currentUnit = nullptr; bool found = false; foreach (Unit *unit, course->unitList()) { if (unit->foreignId() == unitSkeleton->id()) { found = true; currentUnit = unit; break; } } if (found == false) { currentUnit = new Unit(course); currentUnit->setId(QUuid::createUuid().toString()); currentUnit->setTitle(unitSkeleton->title()); currentUnit->setForeignId(unitSkeleton->id()); currentUnit->setCourse(course); course->addUnit(currentUnit); course->setModified(true); } // update phrases foreach (Phrase *phraseSkeleton, unitSkeleton->phraseList()) { bool found = false; foreach (Phrase *phrase, currentUnit->phraseList()) { if (phrase->foreignId() == phraseSkeleton->id()) { if (phrase->i18nText() != phraseSkeleton->text()) { phrase->setEditState(Phrase::Unknown); phrase->seti18nText(phraseSkeleton->text()); } found = true; break; } } if (found == false) { Phrase *newPhrase = new Phrase(course); newPhrase->setForeignId(phraseSkeleton->id()); newPhrase->setId(QUuid::createUuid().toString()); newPhrase->setText(phraseSkeleton->text()); newPhrase->seti18nText(phraseSkeleton->text()); newPhrase->setType(phraseSkeleton->type()); newPhrase->setUnit(currentUnit); currentUnit->addPhrase(newPhrase); course->setModified(true); } } } // FIXME deassociate removed phrases qCDebug(ARTIKULATE_LOG) << "Update performed!"; } CourseResource * ResourceManager::addCourse(const QUrl &courseFile) -{ +{ CourseResource *resource = new CourseResource(courseFile, nullptr); //TODO if (resource->language() == nullptr) { delete resource; qCritical() << "Could not load course, language unknown:" << courseFile.toLocalFile(); return nullptr; } // skip already loaded resources if (m_loadedResources.contains(courseFile.toLocalFile())) { delete resource; return nullptr; } m_loadedResources.append(courseFile.toLocalFile()); addCourseResource(resource); emit languageCoursesChanged(); return resource; } void ResourceManager::addCourseResource(CourseResource *resource) { // Q_ASSERT(m_courseResources.contains(resource->language())); // if (m_courseResources.contains(resource->language())) { // emit courseResourceAboutToBeAdded(resource, m_courseResources[resource->language()].count()); // } // else { // emit courseResourceAboutToBeAdded(resource, 0); // m_courseResources.insert(resource->language(), QList()); // } // m_courseResources[resource->language()].append(resource); // emit courseResourceAdded(); } void ResourceManager::removeCourse(Course *course) { for (int index = 0; index < m_courseResources[course->language()->id()].length(); ++index) { if (m_courseResources[course->language()->id()].at(index)->course() == course) { emit courseResourceAboutToBeRemoved(index); m_courseResources[course->language()->id()].removeAt(index); course->deleteLater(); return; } } } Course * ResourceManager::createCourse(Language *language, Skeleton *skeleton) { // set path QString path = QStringLiteral("%1/%2/%3/%4/%4.xml") .arg(Settings::courseRepositoryPath(), QStringLiteral("courses"), skeleton->id(), language->id()); CourseResource * courseRes = new CourseResource(QUrl::fromLocalFile(path), nullptr); //TODO Q_ASSERT(courseRes); Course *course = courseRes->course(); Q_ASSERT(course); course->setId(QUuid::createUuid().toString()); course->setTitle(skeleton->title()); course->setDescription(skeleton->description()); course->setFile(QUrl::fromLocalFile(path)); course->setLanguage(language); // set skeleton course->setForeignId(skeleton->id()); addCourseResource(courseRes); return course; } void ResourceManager::addSkeleton(const QUrl &skeletonFile) { SkeletonResource *resource = new SkeletonResource(this, skeletonFile); addSkeletonResource(resource); } void ResourceManager::addSkeletonResource(SkeletonResource *resource) { // skip already loaded resources if (m_loadedResources.contains(resource->path().toLocalFile())) { return; } m_loadedResources.append(resource->path().toLocalFile()); emit skeletonAboutToBeAdded(resource->skeleton(), m_skeletonResources.count()); m_skeletonResources.append(resource); emit skeletonAdded(); } void ResourceManager::removeSkeleton(Skeleton *skeleton) { for (int index = 0; index < m_skeletonResources.length(); ++index) { if (m_skeletonResources.at(index)->identifier() == skeleton->id()) { emit skeletonAboutToBeRemoved(index, index); m_skeletonResources.removeAt(index); emit skeletonRemoved(); skeleton->deleteLater(); return; } } } QList< SkeletonResource* > ResourceManager::skeletonResources() { return m_skeletonResources; } diff --git a/src/core/resources/courseparser.cpp b/src/core/resources/courseparser.cpp new file mode 100644 index 0000000..ee1a7d2 --- /dev/null +++ b/src/core/resources/courseparser.cpp @@ -0,0 +1,263 @@ +/* + * Copyright 2013-2019 Andreas Cord-Landwehr + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "courseparser.h" +#include "core/icourse.h" +#include "core/language.h" +#include "core/unit.h" +#include "core/phrase.h" +#include "core/phoneme.h" +#include "artikulate_debug.h" + +#include +#include +#include +#include +#include +#include +#include + + +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_LOG) << "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_LOG) << "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_LOG) << errorMsg; + } + } else { + qCWarning(ARTIKULATE_LOG) << "Could not open XML document " << path.toLocalFile() << " for reading, aborting."; + } + return document; +} + +Phrase * CourseParser::parsePhrase(QDomElement phraseNode, Unit* parentUnit) +{ + const ICourse *course = parentUnit->course(); + Q_ASSERT(course != nullptr); + + Phrase *phrase = new Phrase(parentUnit); + phrase->setId(phraseNode.firstChildElement(QStringLiteral("id")).text()); + phrase->setText(phraseNode.firstChildElement(QStringLiteral("text")).text()); + phrase->seti18nText(phraseNode.firstChildElement(QStringLiteral("i18nText")).text()); + phrase->setUnit(parentUnit); + if (!phraseNode.firstChildElement(QStringLiteral("soundFile")).text().isEmpty()) { + phrase->setSound(QUrl::fromLocalFile( + course->file().adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path() + + '/' + phraseNode.firstChildElement(QStringLiteral("soundFile")).text()) + ); + } + phrase->setType(phraseNode.firstChildElement(QStringLiteral("type")).text()); + phrase->setEditState(phraseNode.firstChildElement(QStringLiteral("editState")).text()); + if (!phraseNode.firstChildElement(QStringLiteral("foreignId")).isNull()) { + phrase->setForeignId(phraseNode.firstChildElement(QStringLiteral("foreignId")).text()); + } + + // add phonemes + QList phonemes = course->language()->phonemes(); + for (QDomElement phonemeID = phraseNode.firstChildElement(QStringLiteral("phonemes")).firstChildElement(); + !phonemeID.isNull(); + phonemeID = phonemeID.nextSiblingElement()) + { + QString id = phonemeID.text(); + if (id.isEmpty()) { + qCritical() << "Phoneme ID string is empty for phrase "<< phrase->id() <<", aborting."; + continue; + } + for (Phoneme *phoneme : phonemes) { + if (phoneme->id() == id) { + phrase->addPhoneme(phoneme); + break; + } + } + } + + if (!phraseNode.firstChildElement(QStringLiteral("excluded")).isNull() && + phraseNode.firstChildElement(QStringLiteral("excluded")).text() == QLatin1String("true")) + { + phrase->setExcluded(true); + } + + return phrase; +} + +QDomDocument CourseParser::serializedDocument(ICourse *course, bool trainingExport) +{ + QDomDocument document; + // prepare xml header + QDomProcessingInstruction header = document.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\"")); + document.appendChild(header); + + // create main element + QDomElement root = document.createElement(QStringLiteral("course")); + document.appendChild(root); + + QDomElement idElement = document.createElement(QStringLiteral("id")); + QDomElement titleElement = document.createElement(QStringLiteral("title")); + QDomElement descriptionElement = document.createElement(QStringLiteral("description")); + QDomElement languageElement = document.createElement(QStringLiteral("language")); + + idElement.appendChild(document.createTextNode(course->id())); + titleElement.appendChild(document.createTextNode(course->title())); + descriptionElement.appendChild(document.createTextNode(course->description())); + languageElement.appendChild(document.createTextNode(course->id())); + + QDomElement unitListElement = document.createElement(QStringLiteral("units")); + // create units + for (Unit *unit : course->unitList()) { + QDomElement unitElement = document.createElement(QStringLiteral("unit")); + + QDomElement unitIdElement = document.createElement(QStringLiteral("id")); + QDomElement unitTitleElement = document.createElement(QStringLiteral("title")); + QDomElement unitPhraseListElement = document.createElement(QStringLiteral("phrases")); + unitIdElement.appendChild(document.createTextNode(unit->id())); + unitTitleElement.appendChild(document.createTextNode(unit->title())); + + // construct phrases + for (Phrase *phrase : unit->phraseList()) { + if (trainingExport && phrase->soundFileUrl().isEmpty()) { + continue; + } + unitPhraseListElement.appendChild(serializedPhrase(phrase, document)); + } + + if (trainingExport && unitPhraseListElement.childNodes().count() == 0) { + continue; + } + + // construct the unit element + unitElement.appendChild(unitIdElement); + if (!unit->foreignId().isEmpty()) { + QDomElement unitForeignIdElement = document.createElement(QStringLiteral("foreignId")); + unitForeignIdElement.appendChild(document.createTextNode(unit->foreignId())); + unitElement.appendChild(unitForeignIdElement); + } + unitElement.appendChild(unitTitleElement); + unitElement.appendChild(unitPhraseListElement); + + unitListElement.appendChild(unitElement); + } + + root.appendChild(idElement); + if (!course->foreignId().isEmpty()) { + QDomElement courseForeignIdElement = document.createElement(QStringLiteral("foreignId")); + courseForeignIdElement.appendChild(document.createTextNode(course->foreignId())); + root.appendChild(courseForeignIdElement); + } + root.appendChild(titleElement); + root.appendChild(descriptionElement); + root.appendChild(languageElement); + root.appendChild(unitListElement); + + return document; +} + +QDomElement CourseParser::serializedPhrase(Phrase *phrase, QDomDocument &document) +{ + QDomElement phraseElement = document.createElement(QStringLiteral("phrase")); + QDomElement phraseIdElement = document.createElement(QStringLiteral("id")); + QDomElement phraseTextElement = document.createElement(QStringLiteral("text")); + QDomElement phrasei18nTextElement = document.createElement(QStringLiteral("i18nText")); + QDomElement phraseSoundFileElement = document.createElement(QStringLiteral("soundFile")); + QDomElement phraseTypeElement = document.createElement(QStringLiteral("type")); + QDomElement phraseEditStateElement = document.createElement(QStringLiteral("editState")); + QDomElement phrasePhonemeListElement = document.createElement(QStringLiteral("phonemes")); + + phraseIdElement.appendChild(document.createTextNode(phrase->id())); + phraseTextElement.appendChild(document.createTextNode(phrase->text())); + phrasei18nTextElement.appendChild(document.createTextNode(phrase->i18nText())); + phraseSoundFileElement.appendChild(document.createTextNode(phrase->sound().fileName())); + phraseTypeElement.appendChild(document.createTextNode(phrase->typeString())); + phraseEditStateElement.appendChild(document.createTextNode(phrase->editStateString())); + + // add phonemes + foreach (Phoneme *phoneme, phrase->phonemes()) { + QDomElement phonemeElement = document.createElement(QStringLiteral("phonemeID")); + phonemeElement.appendChild(document.createTextNode(phoneme->id())); + phrasePhonemeListElement.appendChild(phonemeElement); + } + + phraseElement.appendChild(phraseIdElement); + if (!phrase->foreignId().isEmpty()) { + QDomElement phraseForeignIdElement = document.createElement(QStringLiteral("foreignId")); + phraseForeignIdElement.appendChild(document.createTextNode(phrase->foreignId())); + phraseElement.appendChild(phraseForeignIdElement); + } + phraseElement.appendChild(phraseTextElement); + phraseElement.appendChild(phrasei18nTextElement); + phraseElement.appendChild(phraseSoundFileElement); + phraseElement.appendChild(phraseTypeElement); + phraseElement.appendChild(phraseEditStateElement); + phraseElement.appendChild(phrasePhonemeListElement); + + if (phrase->isExcluded()) { + QDomElement phraseIsExcludedElement = document.createElement(QStringLiteral("excluded")); + phraseIsExcludedElement.appendChild(document.createTextNode(QStringLiteral("true"))); + phraseElement.appendChild(phraseIsExcludedElement); + } + + return phraseElement; +} + +bool CourseParser::exportCourseToGhnsPackage(ICourse *course, const QString &exportPath) +{ + // filename + const QString fileName = course->id() + ".tar.bz2"; + KTar tar = KTar(exportPath + '/' + fileName, QStringLiteral("application/x-bzip")); + if (!tar.open(QIODevice::WriteOnly)) { + qCWarning(ARTIKULATE_CORE()) << "Unable to open tar file" + << exportPath + '/' + fileName + << "in write mode, aborting."; + return false; + } + + for (auto *unit : course->unitList()) { + for (auto *phrase : unit->phraseList()) { + if (QFile::exists(phrase->soundFileUrl())) { + tar.addLocalFile(phrase->soundFileUrl(), phrase->id() + ".ogg"); + } + } + } + + tar.writeFile(course->id() + ".xml", CourseParser::serializedDocument(course, true).toByteArray()); + + tar.close(); + return true; +} diff --git a/src/core/resources/courseparser.h b/src/core/resources/courseparser.h new file mode 100644 index 0000000..0ffb468 --- /dev/null +++ b/src/core/resources/courseparser.h @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2019 Andreas Cord-Landwehr + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef COURSEPARSER_H +#define COURSEPARSER_H + +#include "artikulatecore_export.h" + +class ICourse; +class Unit; +class Phrase; +class QXmlSchema; +class QJSonDocument; +class QDomDocument; +class QDomElement; +class QString; +class QUrl; + +class ARTIKULATECORE_EXPORT CourseParser +{ + +public: + /** + * Load XSD file given by its file name (without ".xsd" suffix). The method searches exclusively + * the standard install dir for XSD files in subdirectory "schemes/". + * + * \param schemeName name of the Xml schema without suffix + * \return loaded XML Schema + */ + static QXmlSchema loadXmlSchema(const QString &schemeName); + + /** + * Load XML file given by \p file that confirms with XML schema \p scheme. + * + * \param path is the path to the XML file to be loaded + * \param scheme is the XML schema describing the DOM + * \return the loaded DOM document + */ + static QDomDocument loadDomDocument(const QUrl &path, const QXmlSchema &schema); + + + static Phrase * parsePhrase(QDomElement phraseNode, Unit* parentUnit); + static QDomDocument serializedDocument(ICourse *course, bool trainingExport); + static QDomElement serializedPhrase(Phrase *phrase, QDomDocument &document); + static bool exportCourseToGhnsPackage(ICourse *course, const QString &exportPath); +}; + +#endif diff --git a/src/core/resources/courseresource.cpp b/src/core/resources/courseresource.cpp index 39680e1..20bfb99 100644 --- a/src/core/resources/courseresource.cpp +++ b/src/core/resources/courseresource.cpp @@ -1,571 +1,281 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * Copyright 2013 Oindrila Gupta * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "courseresource.h" +#include "courseparser.h" #include "core/resourcemanager.h" #include "core/language.h" #include "core/course.h" #include "core/unit.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/language.h" #include "core/iresourcerepository.h" #include "application.h" #include #include -#include #include #include #include #include #include #include #include #include "artikulate_debug.h" class CourseResourcePrivate { public: CourseResourcePrivate() = default; - void loadCourse(); - Phrase * parsePhrase(QDomElement phraseNode, Unit* parentUnit) const; - - /** - * Load XSD file given by its file name (without ".xsd" suffix). The method searches exclusively - * the standard install dir for XSD files in subdirectory "schemes/". - * - * \param schemeName name of the Xml schema without suffix - * \return loaded XML Schema - */ - QXmlSchema loadXmlSchema(const QString &schemeName) const; - - /** - * Load XML file given by \p file that confirms with XML schema \p scheme. - * - * \param path is the path to the XML file to be loaded - * \param scheme is the XML schema describing the DOM - * \return the loaded DOM document - */ - QDomDocument loadDomDocument(const QUrl &path, const QXmlSchema &schema) const; + void loadCourse(CourseResource *parent); IResourceRepository *m_repository{ nullptr }; - QUrl m_path; + QUrl m_file; QString m_identifier; + QString m_foreignId; QString m_title; QString m_languageId; Language *m_language{ nullptr }; QString m_i18nTitle; QString m_description; - Course *m_loadedCourse{ nullptr }; + QVector m_units; + bool m_courseLoaded{ false }; ///setId(unitNode.firstChildElement(QStringLiteral("id")).text()); + unit->setCourse(parent); + unit->setTitle(unitNode.firstChildElement(QStringLiteral("title")).text()); + if (!unitNode.firstChildElement(QStringLiteral("foreignId")).isNull()) { + unit->setForeignId(unitNode.firstChildElement(QStringLiteral("foreignId")).text()); + } + parent->addUnit(unit); + + // create phrases + for (QDomElement phraseNode = unitNode.firstChildElement(QStringLiteral("phrases")).firstChildElement(); + !phraseNode.isNull(); + phraseNode = phraseNode.nextSiblingElement()) + { + unit->addPhrase(CourseParser::parsePhrase(phraseNode, unit)); // add to unit at last step to produce only one signal + //FIXME phrase does not cause unit signals that phonemes list is changed } - } else { - qCWarning(ARTIKULATE_LOG) << "Could not open XML document " << path.toLocalFile() << " for reading, aborting."; } - return document; } - CourseResource::CourseResource(const QUrl &path, IResourceRepository *repository) : ICourse(repository) , d(new CourseResourcePrivate()) { - d->m_path = path; + 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(const QUrl &path, const QVector &languages, IResourceRepository *repository) : CourseResource(path, repository) { for (const auto &language : languages) { if (language == nullptr) { continue; } if (language->id() == d->m_languageId) { d->m_language = language; } } } CourseResource::~CourseResource() { } QString CourseResource::id() const { return d->m_identifier; } +QString CourseResource::foreignId() const +{ + return d->m_foreignId; +} + QString CourseResource::title() const { return d->m_title; } QString CourseResource::i18nTitle() const { return d->m_i18nTitle; } QString CourseResource::description() const { return d->m_description; } Language * CourseResource::language() const { return d->m_language; } -QList CourseResource::unitList() +void CourseResource::addUnit(Unit *unit) { - if (d->m_loadedCourse == nullptr) { - d->loadCourse(); - } - if (d->m_loadedCourse != nullptr) { - return d->m_loadedCourse->unitList(); - } - return QList(); -} - -void CourseResource::sync() -{ - Q_ASSERT(d->m_path.isValid()); - Q_ASSERT(d->m_path.isLocalFile()); - Q_ASSERT(!d->m_path.isEmpty()); - - // if resource was never loaded, it cannot be changed - if (d->m_loadedCourse == nullptr) { - qCDebug(ARTIKULATE_LOG()) << "Aborting sync, course was not parsed."; - return; - } - - // not writing back if not modified - if (!d->m_loadedCourse->modified()) { - qCDebug(ARTIKULATE_LOG()) << "Aborting sync, course was not modified."; - return; - } - - // write back to file - // create directories if necessary - QFileInfo info(d->m_path.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); - if (!info.exists()) { - qCDebug(ARTIKULATE_LOG) << "create xml output file directory, not existing"; - QDir dir; - dir.mkpath(d->m_path.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); - } - - //TODO port to KSaveFile - QFile file(d->m_path.toLocalFile()); - if (!file.open(QIODevice::WriteOnly)) { - qCWarning(ARTIKULATE_LOG) << "Unable to open file " << file.fileName() << " in write mode, aborting."; - return; - } - - file.write(serializedDocument().toByteArray()); - return; + emit unitAboutToBeAdded(unit, d->m_units.count() - 1); + d->m_units.append(unit); + emit unitAdded(); } -void CourseResource::exportGhns(const QString &path) +QList CourseResource::unitList() { - if (d->m_loadedCourse == nullptr) { - d->loadCourse(); - } - - // filename - const QString fileName = id() + ".tar.bz2"; - KTar tar = KTar(path + '/' + fileName, QStringLiteral("application/x-bzip")); - if (!tar.open(QIODevice::WriteOnly)) { - qCWarning(ARTIKULATE_LOG) << "Unable to open tar file" - << path + '/' + fileName - << "in write mode, aborting."; - return; - } - - for (auto *unit : unitList()) { - for (auto *phrase : unit->phraseList()) { - if (QFile::exists(phrase->soundFileUrl())) { - tar.addLocalFile(phrase->soundFileUrl(), phrase->id() + ".ogg"); - } - } + if (d->m_courseLoaded == false) { + d->loadCourse(this); } - - tar.writeFile(id() + ".xml", serializedDocument(true).toByteArray()); - - tar.close(); + return d->m_units.toList(); } void CourseResource::close() { - d->m_loadedCourse->deleteLater(); - d->m_loadedCourse = nullptr; + //TODO cleanup } bool CourseResource::isOpen() const { - return (d->m_loadedCourse != nullptr); + return true; + //TODO cleanaup } QUrl CourseResource::file() const { - if (d->m_loadedCourse) { - return d->m_loadedCourse->file(); - } - return d->m_path; -} - -void CourseResourcePrivate::loadCourse() -{ - if (m_loadedCourse != nullptr) { - qCWarning(ARTIKULATE_CORE()) << "Skipping loading of course, no reloading implemented yet"; - return; - } - - QFileInfo info(m_path.toLocalFile()); - if (!info.exists()) { - qCCritical(ARTIKULATE_LOG()) << "No course file available at location" << m_path.toLocalFile(); - return; - } - - // load existing file - QXmlSchema schema = loadXmlSchema(QStringLiteral("course")); - if (!schema.isValid()) { - qCWarning(ARTIKULATE_CORE()) << "Scheme not valid, aborting"; - return; - } - QDomDocument document = loadDomDocument(m_path, schema); - if (document.isNull()) { - qCWarning(ARTIKULATE_CORE()) << "Could not parse document " << m_path.toLocalFile() << ", aborting."; - return; - } - - // create course - QDomElement root(document.documentElement()); - m_loadedCourse = new Course(nullptr); - - m_loadedCourse->setFile(m_path); - m_loadedCourse->setId(root.firstChildElement(QStringLiteral("id")).text()); - m_loadedCourse->setTitle(root.firstChildElement(QStringLiteral("title")).text()); - m_loadedCourse->setDescription(root.firstChildElement(QStringLiteral("description")).text()); - if (!root.firstChildElement(QStringLiteral("foreignId")).isNull()) { - m_loadedCourse->setForeignId(root.firstChildElement(QStringLiteral("foreignId")).text()); - } - - // set language - //TODO not efficient to load completely every language for this comparison - QString languageId = root.firstChildElement(QStringLiteral("language")).text(); - for(const auto &language : m_repository->languages()) { - if (language->id() == languageId) { - m_loadedCourse->setLanguage(language); - break; - } - } - if (m_loadedCourse->language() == nullptr) { - qCWarning(ARTIKULATE_LOG) << "Language ID" << languageId << "unknown, could not register any language, aborting"; - return; - } - - // create units - for (QDomElement unitNode = root.firstChildElement(QStringLiteral("units")).firstChildElement(); - !unitNode.isNull(); - unitNode = unitNode.nextSiblingElement()) - { - Unit *unit = new Unit(nullptr); - unit->setId(unitNode.firstChildElement(QStringLiteral("id")).text()); - unit->setCourse(m_loadedCourse); - unit->setTitle(unitNode.firstChildElement(QStringLiteral("title")).text()); - if (!unitNode.firstChildElement(QStringLiteral("foreignId")).isNull()) { - unit->setForeignId(unitNode.firstChildElement(QStringLiteral("foreignId")).text()); - } - m_loadedCourse->addUnit(unit); - - // create phrases - for (QDomElement phraseNode = unitNode.firstChildElement(QStringLiteral("phrases")).firstChildElement(); - !phraseNode.isNull(); - phraseNode = phraseNode.nextSiblingElement()) - { - unit->addPhrase(parsePhrase(phraseNode, unit)); // add to unit at last step to produce only one signal - //FIXME phrase does not cause unit signals that phonemes list is changed - } - } - m_loadedCourse->setModified(false); + return d->m_file; } Course * CourseResource::course() { return nullptr; } - -Phrase* CourseResourcePrivate::parsePhrase(QDomElement phraseNode, Unit* parentUnit) const -{ - Phrase *phrase = new Phrase(parentUnit); - phrase->setId(phraseNode.firstChildElement(QStringLiteral("id")).text()); - phrase->setText(phraseNode.firstChildElement(QStringLiteral("text")).text()); - phrase->seti18nText(phraseNode.firstChildElement(QStringLiteral("i18nText")).text()); - phrase->setUnit(parentUnit); - if (!phraseNode.firstChildElement(QStringLiteral("soundFile")).text().isEmpty()) { - phrase->setSound(QUrl::fromLocalFile( - m_path.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path() - + '/' + phraseNode.firstChildElement(QStringLiteral("soundFile")).text()) - ); - } - phrase->setType(phraseNode.firstChildElement(QStringLiteral("type")).text()); - phrase->setEditState(phraseNode.firstChildElement(QStringLiteral("editState")).text()); - if (!phraseNode.firstChildElement(QStringLiteral("foreignId")).isNull()) { - phrase->setForeignId(phraseNode.firstChildElement(QStringLiteral("foreignId")).text()); - } - - // add phonemes - QList phonemes = m_loadedCourse->language()->phonemes(); - for (QDomElement phonemeID = phraseNode.firstChildElement(QStringLiteral("phonemes")).firstChildElement(); - !phonemeID.isNull(); - phonemeID = phonemeID.nextSiblingElement()) - { - QString id = phonemeID.text(); - if (id.isEmpty()) { - qCritical() << "Phoneme ID string is empty for phrase "<< phrase->id() <<", aborting."; - continue; - } - foreach (Phoneme *phoneme, phonemes) { - if (phoneme->id() == id) { - phrase->addPhoneme(phoneme); - break; - } - } - } - - if (!phraseNode.firstChildElement(QStringLiteral("excluded")).isNull() && - phraseNode.firstChildElement(QStringLiteral("excluded")).text() == QLatin1String("true")) - { - phrase->setExcluded(true); - } - - return phrase; -} - -QDomDocument CourseResource::serializedDocument(bool trainingExport) const -{ - 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(d->m_loadedCourse->id())); - titleElement.appendChild(document.createTextNode(d->m_loadedCourse->title())); - descriptionElement.appendChild(document.createTextNode(d->m_loadedCourse->description())); - languageElement.appendChild(document.createTextNode(d->m_loadedCourse->language()->id())); - - QDomElement unitListElement = document.createElement(QStringLiteral("units")); - // create units - foreach (Unit *unit, d->m_loadedCourse->unitList()) { - 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 - foreach (Phrase *phrase, unit->phraseList()) { - if (trainingExport && phrase->soundFileUrl().isEmpty()) { - continue; - } - unitPhraseListElement.appendChild(serializedPhrase(phrase, document)); - } - - if (trainingExport && unitPhraseListElement.childNodes().count() == 0) { - continue; - } - - // construct the unit element - unitElement.appendChild(unitIdElement); - if (!unit->foreignId().isEmpty()) { - QDomElement unitForeignIdElement = document.createElement(QStringLiteral("foreignId")); - unitForeignIdElement.appendChild(document.createTextNode(unit->foreignId())); - unitElement.appendChild(unitForeignIdElement); - } - unitElement.appendChild(unitTitleElement); - unitElement.appendChild(unitPhraseListElement); - - unitListElement.appendChild(unitElement); - } - - root.appendChild(idElement); - if (!d->m_loadedCourse->foreignId().isEmpty()) { - QDomElement courseForeignIdElement = document.createElement(QStringLiteral("foreignId")); - courseForeignIdElement.appendChild(document.createTextNode(d->m_loadedCourse->foreignId())); - root.appendChild(courseForeignIdElement); - } - root.appendChild(titleElement); - root.appendChild(descriptionElement); - root.appendChild(languageElement); - root.appendChild(unitListElement); - - return document; -} - -QDomElement CourseResource::serializedPhrase(Phrase *phrase, QDomDocument &document) const -{ - QDomElement phraseElement = document.createElement(QStringLiteral("phrase")); - QDomElement phraseIdElement = document.createElement(QStringLiteral("id")); - QDomElement phraseTextElement = document.createElement(QStringLiteral("text")); - QDomElement phrasei18nTextElement = document.createElement(QStringLiteral("i18nText")); - QDomElement phraseSoundFileElement = document.createElement(QStringLiteral("soundFile")); - QDomElement phraseTypeElement = document.createElement(QStringLiteral("type")); - QDomElement phraseEditStateElement = document.createElement(QStringLiteral("editState")); - QDomElement phrasePhonemeListElement = document.createElement(QStringLiteral("phonemes")); - - phraseIdElement.appendChild(document.createTextNode(phrase->id())); - phraseTextElement.appendChild(document.createTextNode(phrase->text())); - phrasei18nTextElement.appendChild(document.createTextNode(phrase->i18nText())); - phraseSoundFileElement.appendChild(document.createTextNode(phrase->sound().fileName())); - phraseTypeElement.appendChild(document.createTextNode(phrase->typeString())); - phraseEditStateElement.appendChild(document.createTextNode(phrase->editStateString())); - - // add phonemes - foreach (Phoneme *phoneme, phrase->phonemes()) { - QDomElement phonemeElement = document.createElement(QStringLiteral("phonemeID")); - phonemeElement.appendChild(document.createTextNode(phoneme->id())); - phrasePhonemeListElement.appendChild(phonemeElement); - } - - phraseElement.appendChild(phraseIdElement); - if (!phrase->foreignId().isEmpty()) { - QDomElement phraseForeignIdElement = document.createElement(QStringLiteral("foreignId")); - phraseForeignIdElement.appendChild(document.createTextNode(phrase->foreignId())); - phraseElement.appendChild(phraseForeignIdElement); - } - phraseElement.appendChild(phraseTextElement); - phraseElement.appendChild(phrasei18nTextElement); - phraseElement.appendChild(phraseSoundFileElement); - phraseElement.appendChild(phraseTypeElement); - phraseElement.appendChild(phraseEditStateElement); - phraseElement.appendChild(phrasePhonemeListElement); - - if (phrase->isExcluded()) { - QDomElement phraseIsExcludedElement = document.createElement(QStringLiteral("excluded")); - phraseIsExcludedElement.appendChild(document.createTextNode(QStringLiteral("true"))); - phraseElement.appendChild(phraseIsExcludedElement); - } - - return phraseElement; -} diff --git a/src/core/resources/courseresource.h b/src/core/resources/courseresource.h index c580be6..d6a22ef 100644 --- a/src/core/resources/courseresource.h +++ b/src/core/resources/courseresource.h @@ -1,124 +1,131 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef COURSERESOURCE_H #define COURSERESOURCE_H #include "artikulatecore_export.h" #include "core/icourse.h" #include #include class QDomElement; class QString; class CourseResourcePrivate; class Course; class Unit; class Phrase; class IResourceRepository; //TODO move to private class QXmlSchema; class QJSonDocument; class QDomDocument; class ARTIKULATECORE_EXPORT CourseResource : public ICourse { Q_OBJECT Q_INTERFACES(ICourse) public: /** * Create course resource from file. */ explicit CourseResource(const QUrl &path, IResourceRepository *repository); /** * @brief convenience constructor for porting to language access by dedicated model */ explicit Q_DECL_DEPRECATED CourseResource(const QUrl &path, const QVector &languages, IResourceRepository *repository); ~CourseResource() override; /** * \return unique identifier */ QString id() const override; + /** + * \return global ID for this course + */ + QString foreignId() const override; + /** * \return human readable localized title */ QString title() const override; /** * \return human readable title in English */ QString i18nTitle() const override; /** * \return description text for course */ QString description() const override; /** * \return language identifier of this course */ Language * language() const override; + void addUnit(Unit *unit); + /** * \return true if resource is loaded, otherwise false */ bool isOpen() const; void sync(); /** * export course as .tar.bz2 file in the specified folder. */ void exportGhns(const QString &path); /** * close resource without writing changes back to file */ void close(); QUrl file() const override; QList unitList() override; /** * \return reference to the loaded course resource */ Q_DECL_DEPRECATED Course * course(); private: Phrase * parsePhrase(QDomElement phraseNode, Unit *parentUnit) const; /** * \return serialized course as DOM document * \param trainingExport if true phrases without recording and empty units are excluded */ QDomDocument serializedDocument(bool trainingExport=false) const; QDomElement serializedPhrase(Phrase * phrase, QDomDocument &document) const; const QScopedPointer d; }; #endif diff --git a/src/core/resources/editablecourseresource.cpp b/src/core/resources/editablecourseresource.cpp new file mode 100644 index 0000000..9b506df --- /dev/null +++ b/src/core/resources/editablecourseresource.cpp @@ -0,0 +1,109 @@ +/* + * Copyright 2019 Andreas Cord-Landwehr + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "editablecourseresource.h" +#include "courseparser.h" +#include "artikulate_debug.h" +#include "core/unit.h" +#include "core/phrase.h" +#include "core/phoneme.h" + +#include +#include +#include +#include +#include + +EditableCourseResource::EditableCourseResource(const QUrl &path, IResourceRepository *repository) + : m_course(new CourseResource(path, repository)) +{ +} + +QString EditableCourseResource::id() const +{ + return m_course->id(); +} + +QString EditableCourseResource::foreignId() const +{ + return m_course->foreignId(); +} + +QString EditableCourseResource::title() const +{ + return m_course->title(); +} + +QString EditableCourseResource::i18nTitle() const +{ + return m_course->i18nTitle(); +} + +QString EditableCourseResource::description() const +{ + return m_course->description(); +} + +Language * EditableCourseResource::language() const +{ + return m_course->language(); +} + +QList EditableCourseResource::unitList() +{ + return m_course->unitList(); +} + +QUrl EditableCourseResource::file() const +{ + return m_course->file(); +} + +void EditableCourseResource::sync() +{ + Q_ASSERT(file().isValid()); + Q_ASSERT(file().isLocalFile()); + Q_ASSERT(!file().isEmpty()); + + // not writing back if not modified + if (!m_modified) { + qCDebug(ARTIKULATE_LOG()) << "Aborting sync, course was not modified."; + return; + } + + // write back to file + // create directories if necessary + QFileInfo info(file().adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); + if (!info.exists()) { + qCDebug(ARTIKULATE_LOG) << "create xml output file directory, not existing"; + QDir dir; + dir.mkpath(file().adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); + } + + //TODO port to KSaveFile + QFile file(EditableCourseResource::file().toLocalFile()); + if (!file.open(QIODevice::WriteOnly)) { + qCWarning(ARTIKULATE_LOG) << "Unable to open file " << file.fileName() << " in write mode, aborting."; + return; + } + + file.write(CourseParser::serializedDocument(m_course.get(), false).toByteArray()); + return; +} diff --git a/src/core/resources/courseresource.h b/src/core/resources/editablecourseresource.h similarity index 60% copy from src/core/resources/courseresource.h copy to src/core/resources/editablecourseresource.h index c580be6..2a248e4 100644 --- a/src/core/resources/courseresource.h +++ b/src/core/resources/editablecourseresource.h @@ -1,124 +1,108 @@ /* - * Copyright 2013 Andreas Cord-Landwehr + * 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 COURSERESOURCE_H -#define COURSERESOURCE_H +#ifndef EDITABLECOURSERESOURCE_H +#define EDITABLECOURSERESOURCE_H #include "artikulatecore_export.h" +#include "courseresource.h" #include "core/icourse.h" +#include #include #include -class QDomElement; -class QString; -class CourseResourcePrivate; +class IResourceRepository; class Course; class Unit; class Phrase; -class IResourceRepository; - -//TODO move to private -class QXmlSchema; -class QJSonDocument; +class QString; class QDomDocument; -class ARTIKULATECORE_EXPORT CourseResource : public ICourse +/** + * @brief Decorator for CourseResource + * + * This decorator adds functionality to modify and write back changes of a course. + */ +class ARTIKULATECORE_EXPORT EditableCourseResource : public ICourse { Q_OBJECT Q_INTERFACES(ICourse) public: /** * Create course resource from file. */ - explicit CourseResource(const QUrl &path, IResourceRepository *repository); + explicit EditableCourseResource(const QUrl &path, IResourceRepository *repository); + + ~EditableCourseResource() override = default; /** - * @brief convenience constructor for porting to language access by dedicated model + * \return unique identifier */ - explicit Q_DECL_DEPRECATED CourseResource(const QUrl &path, const QVector &languages, IResourceRepository *repository); - - ~CourseResource() override; + QString id() const override; /** * \return unique identifier */ - QString id() const override; + QString foreignId() const override; /** * \return human readable localized title */ QString title() const override; /** * \return human readable title in English */ QString i18nTitle() const override; /** * \return description text for course */ QString description() const override; /** * \return language identifier of this course */ Language * language() const override; /** * \return true if resource is loaded, otherwise false */ bool isOpen() const; void sync(); - /** - * export course as .tar.bz2 file in the specified folder. - */ - void exportGhns(const QString &path); - /** * close resource without writing changes back to file */ void close(); QUrl file() const override; QList unitList() override; - /** - * \return reference to the loaded course resource - */ - Q_DECL_DEPRECATED Course * course(); - private: - Phrase * parsePhrase(QDomElement phraseNode, Unit *parentUnit) const; - /** - * \return serialized course as DOM document - * \param trainingExport if true phrases without recording and empty units are excluded - */ - QDomDocument serializedDocument(bool trainingExport=false) const; - QDomElement serializedPhrase(Phrase * phrase, QDomDocument &document) const; - - const QScopedPointer d; + bool m_modified{ false }; //FIXME modify this state + const std::unique_ptr m_course; }; #endif diff --git a/src/ui/exportghnsdialog.cpp b/src/ui/exportghnsdialog.cpp index bfd0114..2727577 100644 --- a/src/ui/exportghnsdialog.cpp +++ b/src/ui/exportghnsdialog.cpp @@ -1,84 +1,86 @@ /* * Copyright 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 "exportghnsdialog.h" #include "core/resourcemanager.h" #include "core/resources/languageresource.h" -#include "core/resources/courseresource.h" +#include "core/resources/editablecourseresource.h" +#include "core/resources/courseparser.h" +#include "core/icourse.h" #include "artikulate_debug.h" #include #include #include #include #include ExportGhnsDialog::ExportGhnsDialog(ResourceManager *manager) : m_manager(manager) { ui = new Ui::ExportGhnsDialog; ui->setupUi(this); // require to set a proper directory ui->buttonBox->button(QDialogButtonBox::Apply)->setDisabled(true); ui->buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Export")); connect(ui->exportDirectory, &QLineEdit::textChanged, this, [=](){ const bool directorySet = !ui->exportDirectory->text().isEmpty(); ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(directorySet); }); connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &ExportGhnsDialog::onExportCourse); // directory selection dialog connect(ui->selectDirectoryButton, &QToolButton::clicked, this, [=]() { // TODO save last path in config file const QString dir = QFileDialog::getExistingDirectory( this, i18n("Export Directory"), QString(), QFileDialog::ShowDirsOnly); ui->exportDirectory->setText(dir); }); // add courses to combo box int counter = 0; foreach (auto *languageRes, manager->languageResources()) { foreach (auto *courseRes, manager->courseResources(languageRes->language())) { ui->courseListCombo->insertItem(counter, courseRes->i18nTitle(), QVariant::fromValue(courseRes)); ++counter; } } connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); } ExportGhnsDialog::~ExportGhnsDialog() { delete ui; } void ExportGhnsDialog::onExportCourse() { - CourseResource *res = qobject_cast( + ICourse *res = qobject_cast( ui->courseListCombo->currentData().value()); qCDebug(ARTIKULATE_LOG) << res << "export GHNS file for" << res->i18nTitle(); - res->exportGhns(ui->exportDirectory->text()); + CourseParser::exportCourseToGhnsPackage(res, ui->exportDirectory->text()); }