diff --git a/autotests/editorsession/editablerepositorystub.h b/autotests/editorsession/editablerepositorystub.h index b79bb76..afa54de 100644 --- a/autotests/editorsession/editablerepositorystub.h +++ b/autotests/editorsession/editablerepositorystub.h @@ -1,87 +1,99 @@ /* * 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 EDITABLEREPOSITORYSTUB_H #define EDITABLEREPOSITORYSTUB_H #include "core/ieditablerepository.h" #include "core/ieditablecourse.h" #include #include class Language; +class SkeletonResource; /** * @brief The EditableRepositoryStub is simple sub class only for testing */ class EditableRepositoryStub : public IEditableRepository { Q_OBJECT public: - EditableRepositoryStub(QVector courses) - : m_courses{ courses } + EditableRepositoryStub( + QVector languages, + QVector skeletons, + QVector courses) + : m_languages{ languages } + , m_skeletons{ skeletons } + , m_courses{ courses } { } ~EditableRepositoryStub() override; QString storageLocation() const override { return QString(); } + QVector skeletons() const override + { + return m_skeletons; + } QVector editableCourses() const override { return m_courses; } QVector courses() const override { QVector courses; for (auto course : m_courses) { courses.push_back(course); } return courses; } QVector courses(Language *language) const override { Q_UNUSED(language); return QVector(); } IEditableCourse * editableCourse(Language *language, int index) const override { Q_UNUSED(language); Q_UNUSED(index); return nullptr; } void reloadCourses() override { // do nothing } QVector languages() const override { - return QVector(); + return m_languages; } Q_SIGNALS: void courseAboutToBeAdded(ICourse*,int) override; void courseAdded() override; void courseAboutToBeRemoved(int) override; void courseRemoved() override; private: + QVector m_languages; + QVector m_skeletons; QVector m_courses; }; #endif diff --git a/autotests/editorsession/test_editorsession.cpp b/autotests/editorsession/test_editorsession.cpp index bd8475b..29c1893 100644 --- a/autotests/editorsession/test_editorsession.cpp +++ b/autotests/editorsession/test_editorsession.cpp @@ -1,138 +1,232 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_editorsession.h" #include "editablerepositorystub.h" #include "src/core/editorsession.h" #include "src/core/icourse.h" #include "src/core/ieditablecourse.h" #include "src/core/ieditablerepository.h" #include "src/core/language.h" +#include "src/core/resources/skeletonresource.h" #include "src/core/unit.h" #include #include class EditableCourseStub : public IEditableCourse { public: EditableCourseStub(Language *language, QVector units) : m_language(language) , m_units(units) { } ~EditableCourseStub() override; QString id() const override { return m_id; } void setId(QString id) override { m_id = id; emit idChanged(); } QString foreignId() const override { return m_foreignId; } void setForeignId(QString id) override { m_foreignId = id; } QString title() const override { return m_title; } void setTitle(QString title) override { m_title = title; emit titleChanged(); } QString i18nTitle() const override { return m_i18nTitle; } void setI18nTitle(QString title) override { m_i18nTitle = title; } QString description() const override { return m_description; } void setDescription(QString description) override { m_description = description; emit descriptionChanged(); } Language * language() const override { return m_language; } void setLanguage(Language *language) override { m_language = language; emit languageChanged(); } QList unitList() override { return m_units.toList(); } QUrl file() const override { return QUrl(); } private: QString m_id{ "courseid" }; QString m_foreignId{ "foreigncourseid" }; QString m_title{ "title" }; QString m_i18nTitle{ "i18n title" }; QString m_description{ "description of the course" }; Language *m_language{nullptr}; QVector m_units; }; // define one virtual method out of line to pin CourseStub to this translation unit EditableCourseStub::~EditableCourseStub() = default; void TestEditorSession::init() { // TODO initialization of test case } void TestEditorSession::cleanup() { // TODO cleanup after test run } void TestEditorSession::createEditorSession() { - Language language; - EditableCourseStub course(&language, QVector()); - EditableRepositoryStub repository{ {&course} }; + Language languageGerman; + languageGerman.setId("de"); + Language languageEnglish; + languageEnglish.setId("en"); + EditableCourseStub course(&languageGerman, QVector()); + course.setLanguage(&languageGerman); + SkeletonResource skeleton(QUrl(), nullptr); + + EditableRepositoryStub repository{ + {&languageGerman, &languageEnglish}, // languages + {&skeleton}, + {&course} // courses + }; + EditorSession session; + session.setRepository(&repository); + QVERIFY(session.course() == nullptr); + QVERIFY(session.language() == nullptr); + QVERIFY(session.skeleton() == nullptr); +} + +void TestEditorSession::nonSkeletonSwitchingBehavior() +{ + Language languageGerman; + languageGerman.setId("de"); + Language languageEnglish; + languageEnglish.setId("en"); + EditableCourseStub courseGerman(&languageGerman, QVector()); + courseGerman.setId("course-german"); + EditableCourseStub courseEnglish(&languageEnglish, QVector()); + courseEnglish.setId("course-english"); + + EditableRepositoryStub repository{ + {&languageGerman, &languageEnglish}, // languages + {}, // skeletons + {&courseGerman, &courseEnglish} // courses + }; + EditorSession session; + session.setRepository(&repository); + + QVERIFY(session.course() == nullptr); + session.setCourse(&courseGerman); + QCOMPARE(session.course()->id(), courseGerman.id()); + QVERIFY(session.language() != nullptr); + QCOMPARE(session.language()->id(), languageGerman.id()); + + QVERIFY(session.language() != nullptr); + QCOMPARE(session.language()->id(), languageGerman.id()); + session.setCourse(&courseEnglish); + QVERIFY(session.course() != nullptr); + QCOMPARE(session.course()->id(), courseEnglish.id()); + QVERIFY(session.language() != nullptr); + QCOMPARE(session.language()->id(), languageEnglish.id()); + +} + +void TestEditorSession::skeletonSwitchingBehavior() +{ + Language languageGerman; + languageGerman.setId("de"); + Language languageEnglish; + languageEnglish.setId("en"); + EditableCourseStub courseGermanA(&languageGerman, QVector()); + courseGermanA.setId("course-german"); + courseGermanA.setForeignId("testskeletonA"); + EditableCourseStub courseGermanB(&languageGerman, QVector()); + courseGermanB.setId("course-german"); + courseGermanB.setForeignId("testskeletonB"); + EditableCourseStub courseEnglishA(&languageEnglish, QVector()); + courseEnglishA.setId("course-english"); + courseEnglishA.setForeignId("testskeletonA"); + SkeletonResource skeletonA(QUrl(), nullptr); + skeletonA.setId("testskeletonA"); + SkeletonResource skeletonB(QUrl(), nullptr); + skeletonB.setId("testskeletonB"); + + EditableRepositoryStub repository{ + {&languageGerman, &languageEnglish}, // languages + {&skeletonA, &skeletonB}, // skeletons + {&courseGermanA, &courseEnglishA, &courseGermanB} // courses + }; EditorSession session; session.setRepository(&repository); + + session.setSkeleton(&skeletonA); + Q_ASSERT(session.skeleton() != nullptr); + QCOMPARE(session.skeleton()->id(), skeletonA.id()); + Q_ASSERT(session.course() != nullptr); + QCOMPARE(session.course()->id(), courseGermanA.id()); + session.setCourse(&courseEnglishA); + Q_ASSERT(session.course() != nullptr); + QCOMPARE(session.course()->id(), courseEnglishA.id()); + + session.setCourse(&courseGermanB); + QVERIFY(session.skeleton() != nullptr); + QCOMPARE(session.skeleton()->id(), skeletonB.id()); QVERIFY(session.course() != nullptr); - QCOMPARE(session.course()->id(), course.id()); + QCOMPARE(session.course()->id(), courseGermanB.id()); + QVERIFY(session.language() != nullptr); + QCOMPARE(session.language()->id(), languageGerman.id()); } + QTEST_GUILESS_MAIN(TestEditorSession) diff --git a/autotests/editorsession/test_editorsession.h b/autotests/editorsession/test_editorsession.h index 4ce11b9..c600c5a 100644 --- a/autotests/editorsession/test_editorsession.h +++ b/autotests/editorsession/test_editorsession.h @@ -1,50 +1,60 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef TEST_EDITORSESSION_H #define TEST_EDITORSESSION_H #include class TestEditorSession : public QObject { Q_OBJECT public: TestEditorSession() = default; private Q_SLOTS: /** * Called before every test case. */ void init(); /** * Called after every test case. */ void cleanup(); /** - * @brief Construct and destruct editor session + * @brief Construct and destruct editor session and test initial values */ void createEditorSession(); + + /** + * @brief Test switching behavior for courses without skeleton. + */ + void nonSkeletonSwitchingBehavior(); + + /** + * @brief Test handling of skeletons and respective course switching + */ + void skeletonSwitchingBehavior(); }; #endif diff --git a/src/application.cpp b/src/application.cpp index a81ee33..26e1028 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -1,143 +1,145 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "application.h" #include "core/iresourcerepository.h" #include "core/contributorrepository.h" #include "core/drawertrainingactions.h" #include "core/trainingaction.h" #include "core/editorsession.h" #include "core/language.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/phrase.h" #include "core/player.h" #include "core/recorder.h" #include "core/trainingsession.h" #include "core/unit.h" #include "core/resources/editablecourseresource.h" #include "core/resources/skeletonresource.h" #include "models/coursefiltermodel.h" #include "models/coursemodel.h" #include "models/languagemodel.h" #include "models/languageresourcemodel.h" #include "models/learningprogressmodel.h" #include "models/phonemegroupmodel.h" #include "models/phonememodel.h" #include "models/phonemeunitmodel.h" #include "models/phrasefiltermodel.h" #include "models/phraselistmodel.h" #include "models/phrasemodel.h" #include "models/profilemodel.h" #include "models/skeletonmodel.h" #include "models/unitfiltermodel.h" #include "models/unitmodel.h" #include "qmlcontrols/iconitem.h" #include "liblearnerprofile/src/learner.h" #include "liblearnerprofile/src/profilemanager.h" #include "liblearnerprofile/src/learninggoal.h" #include "liblearnerprofile/src/models/learninggoalmodel.h" #include #include #include #include Application::Application(int& argc, char** argv) : QApplication(argc, argv) { registerQmlTypes(); } IResourceRepository * Application::resourceRepository() const { return m_resourceRepository; } void Application::installResourceRepository(IResourceRepository *resourceRepository) { m_resourceRepository = resourceRepository; } void Application::registerQmlTypes() { qmlRegisterUncreatableType( "artikulate", 1, 0, "TrainingSession", QStringLiteral("TrainingSession is unique object provided by the backend")); qmlRegisterUncreatableType( "artikulate", 1, 0, "EditorSession", QStringLiteral("EditorSession is unique object provided by the backend")); qmlRegisterUncreatableType( "artikulate", 1, 0, "ContributorRepository", QStringLiteral("ContributorRepository is unique object provided by the backend")); qmlRegisterUncreatableType( "artikulate", 1, 0, "ProfileManager", QStringLiteral("ProfileManager is unique object provided by the backend")); qmlRegisterUncreatableType( "artikulate", 1, 0, "EditableCourseResource", QStringLiteral("EditableCourseResource objects are backend objects")); qmlRegisterUncreatableType( "artikulate", 1, 0, "SkeletonResource", QStringLiteral("SkeletonResource objects are backend objects")); // interfaces qmlRegisterInterface("IResourceRepository"); + qmlRegisterInterface("IEditableRepository"); qmlRegisterInterface("ICourse"); + qmlRegisterInterface("IEditableCourse"); // concrete instantiable types qmlRegisterType("artikulate", 1, 0, "Learner"); qmlRegisterType("artikulate", 1, 0, "LearningGoal"); qmlRegisterType("artikulate", 1, 0, "Unit"); qmlRegisterType("artikulate", 1, 0, "Language"); qmlRegisterType("artikulate", 1, 0, "Phrase"); qmlRegisterType("artikulate", 1, 0, "Phoneme"); qmlRegisterType("artikulate", 1, 0, "PhonemeGroup"); qmlRegisterType("artikulate", 1, 0, "Player"); qmlRegisterType("artikulate", 1, 0, "Recorder"); qmlRegisterType("artikulate", 1, 0, "Icon"); qmlRegisterType("artikulate", 1, 0, "DrawerTrainingActions"); qmlRegisterType("artikulate", 1, 0, "TrainingAction"); // models qmlRegisterType("artikulate", 1, 0, "CourseModel"); qmlRegisterType("artikulate", 1, 0, "CourseFilterModel"); qmlRegisterType("artikulate", 1, 0, "LanguageModel"); qmlRegisterType("artikulate", 1, 0, "LanguageResourceModel"); // qmlRegisterType("artikulate", 1, 0, "LearningProgressModel");//TODO must be ported to new trainingsession qmlRegisterType("artikulate", 1, 0, "UnitModel"); qmlRegisterType("artikulate", 1, 0, "UnitFilterModel"); qmlRegisterType("artikulate", 1, 0, "PhraseModel"); qmlRegisterType("artikulate", 1, 0, "PhraseListModel"); qmlRegisterType("artikulate", 1, 0, "PhraseFilterModel"); qmlRegisterType("artikulate", 1, 0, "PhonemeModel"); qmlRegisterType("artikulate", 1, 0, "PhonemeGroupModel"); qmlRegisterType("artikulate", 1, 0, "PhonemeUnitModel"); qmlRegisterType("artikulate", 1, 0, "ProfileModel"); qmlRegisterType("artikulate", 1, 0, "SkeletonModel"); qmlRegisterType("artikulate", 1, 0, "LearningGoalModel"); } diff --git a/src/core/contributorrepository.cpp b/src/core/contributorrepository.cpp index 164f0ad..1babe4e 100644 --- a/src/core/contributorrepository.cpp +++ b/src/core/contributorrepository.cpp @@ -1,477 +1,481 @@ /* * 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 "contributorrepository.h" #include "artikulate_debug.h" #include "language.h" #include "unit.h" #include "phrase.h" #include "phoneme.h" #include "phonemegroup.h" #include "resources/languageresource.h" #include "resources/editablecourseresource.h" #include "resources/skeletonresource.h" #include "liblearnerprofile/src/profilemanager.h" #include "liblearnerprofile/src/learninggoal.h" #include #include #include #include #include #include ContributorRepository::ContributorRepository(QObject *parent) : IEditableRepository() { loadLanguageResources(); } void ContributorRepository::loadLanguageResources() { // load language resources // all other resources are only loaded on demand QDir dir(":/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 ContributorRepository::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 ContributorRepository::modified() const { for (auto iter = m_courses.constBegin(); iter != m_courses.constEnd(); ++iter) { for (auto *course : iter.value()) { if (course->isModified()) { return true; } } } foreach (auto const &courseRes, m_skeletonResources) { if (courseRes->isModified()) { return true; } } return false; } void ContributorRepository::addLanguage(const QUrl &languageFile) { if (m_loadedResources.contains(languageFile.toLocalFile())) { return; } LanguageResource *resource = new LanguageResource(languageFile); emit languageResourceAboutToBeAdded(resource, m_languageResources.count()); m_languageResources.append(resource); m_loadedResources.append(languageFile.toLocalFile()); m_courses.insert(resource->identifier(), QList()); emit languageResourceAdded(); } QString ContributorRepository::storageLocation() const { return m_storageLocation; } void ContributorRepository::setStorageLocation(const QString &path) { m_storageLocation = path; } QList< LanguageResource* > ContributorRepository::languageResources() const { return m_languageResources; } QVector ContributorRepository::languages() const { QVector languages; for (auto resourse : m_languageResources) { languages.append(resourse->language()); } return languages; } Language * ContributorRepository::language(int index) const { Q_ASSERT(index >= 0 && index < m_languageResources.count()); return m_languageResources.at(index)->language(); } Language * ContributorRepository::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 ContributorRepository::courseResources(Language *language) { if (!language) { QList courses; for (auto iter = m_courses.constBegin(); iter != m_courses.constEnd(); ++iter) { courses.append(iter.value()); } return courses; } // return empty list if no course available for language if (!m_courses.contains(language->id())) { return QList< EditableCourseResource* >(); } return m_courses[language->id()]; } QVector ContributorRepository::courses() const { QVector courses; for (const auto &courseList : m_courses) { for (const auto &course : courseList) { courses.append(course); } } return courses; } QVector ContributorRepository::editableCourses() const { QVector courses; for (const auto &courseList : m_courses) { for (const auto &course : courseList) { courses.append(course); } } return courses; } QVector ContributorRepository::courses(Language *language) const { if (language == nullptr) { return courses(); } QVector courses; if (m_courses.contains(language->id())) { for (const auto &course : m_courses[language->id()]) { courses.append(course); } } return courses; } IEditableCourse * ContributorRepository::editableCourse(Language *language, int index) const { Q_ASSERT(m_courses.contains(language->id())); Q_ASSERT(index >= 0 && index < m_courses[language->id()].count()); return m_courses[language->id()].at(index); } void ContributorRepository::reloadCourseOrSkeleton(ICourse *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 { for (SkeletonResource *resource : m_skeletonResources) { if (resource->id() == courseOrSkeleton->id()) { // TODO no reload available return; } } } } void ContributorRepository::reloadCourses() { // register skeleton resources QDir skeletonDirectory = QDir(storageLocation()); skeletonDirectory.setFilter(QDir::Files | QDir::Hidden); if (!skeletonDirectory.cd(QStringLiteral("skeletons"))) { qCritical() << "There is no subdirectory \"skeletons\" in directory " << skeletonDirectory.path() << " cannot load skeletons."; } else { // read skeletons QFileInfoList list = skeletonDirectory.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); addSkeleton(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); } } // register contributor course files QDir courseDirectory(storageLocation()); if (!courseDirectory.cd(QStringLiteral("courses"))) { qCritical() << "There is no subdirectory \"courses\" in directory " << courseDirectory.path() << " cannot load courses."; } else { // find courses courseDirectory.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QFileInfoList courseDirList = courseDirectory.entryInfoList(); // traverse all course directories for (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 for (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 for (const QFileInfo &courseInfo : courses) { addCourse(QUrl::fromLocalFile(courseInfo.filePath())); } } } } //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 ContributorRepository::updateCourseFromSkeleton(EditableCourseResource *course) { //TODO implement status information that are shown at mainwindow if (course->foreignId().isEmpty()) { qCritical() << "No skeleton ID specified, aborting update."; return; } ICourse *skeleton = nullptr; for (const auto &iter : m_skeletonResources) { if (iter->id() == course->foreignId()) { skeleton = iter; break; } } 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!"; } EditableCourseResource * ContributorRepository::addCourse(const QUrl &courseFile) { EditableCourseResource *resource = new EditableCourseResource(courseFile, this); 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 ContributorRepository::addCourseResource(EditableCourseResource *resource) { Q_ASSERT(m_courses.contains(resource->language()->id())); if (!m_courses.contains(resource->language()->id())) { m_courses.insert(resource->language()->id(), QList()); } emit courseAboutToBeAdded(resource, m_courses[resource->language()->id()].count()); m_courses[resource->language()->id()].append(resource); emit courseAdded(); } void ContributorRepository::removeCourse(ICourse *course) { for (int index = 0; index < m_courses[course->language()->id()].length(); ++index) { if (m_courses[course->language()->id()].at(index) == course) { emit courseAboutToBeRemoved(index); m_courses[course->language()->id()].removeAt(index); emit courseRemoved(); course->deleteLater(); return; } } } EditableCourseResource * ContributorRepository::createCourse(Language *language, SkeletonResource *skeleton) { // set path QString path = QStringLiteral("%1/%2/%3/%4/%4.xml") .arg(storageLocation(), QStringLiteral("courses"), skeleton->id(), language->id()); EditableCourseResource * course = new EditableCourseResource(QUrl::fromLocalFile(path), this); 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(course); return course; } void ContributorRepository::addSkeleton(const QUrl &file) { SkeletonResource *resource = new SkeletonResource(file, this); addSkeletonResource(resource); } void ContributorRepository::addSkeletonResource(SkeletonResource *resource) { // skip already loaded resources if (m_loadedResources.contains(resource->file().toLocalFile())) { return; } m_loadedResources.append(resource->file().toLocalFile()); emit skeletonAboutToBeAdded(resource, m_skeletonResources.count()); m_skeletonResources.append(resource); emit skeletonAdded(); } void ContributorRepository::removeSkeleton(SkeletonResource *skeleton) { for (int index = 0; index < m_skeletonResources.length(); ++index) { if (m_skeletonResources.at(index)->id() == skeleton->id()) { emit skeletonAboutToBeRemoved(index, index); m_skeletonResources.removeAt(index); emit skeletonRemoved(); skeleton->deleteLater(); return; } } } -QList< SkeletonResource* > ContributorRepository::skeletonResources() +QVector ContributorRepository::skeletons() const { - return m_skeletonResources; + QVector skeletonList; + for (const auto &skeleton : m_skeletonResources) { + skeletonList.append(skeleton); + } + return skeletonList; } diff --git a/src/core/contributorrepository.h b/src/core/contributorrepository.h index a9379d6..4650f08 100644 --- a/src/core/contributorrepository.h +++ b/src/core/contributorrepository.h @@ -1,217 +1,215 @@ /* * 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 CONTRIBUTORREPOSITORY_H #define CONTRIBUTORREPOSITORY_H #include "artikulatecore_export.h" #include "ieditablerepository.h" #include #include #include +#include #include #include "liblearnerprofile/src/learninggoal.h" class SkeletonResource; class EditableCourseResource; class LanguageResource; class Language; class ICourse; class QUrl; /** * @class ContributorRepository * This class handles the resources of a contributor. */ class ARTIKULATECORE_EXPORT ContributorRepository : public IEditableRepository { Q_OBJECT Q_INTERFACES(IResourceRepository) Q_INTERFACES(IEditableRepository) Q_PROPERTY(QString repositoryUrl READ storageLocation NOTIFY repositoryChanged) public: explicit ContributorRepository(QObject *parent = nullptr); /** * save all changes to course resources */ void sync(); /** * \return \c true if any course or skeleton is modified, otherwise \c false */ bool modified() const; /** * \return path to working repository, if one is set */ QString storageLocation() const override; /** * Set path to central storage location * \param path the path to the storage location directory */ void setStorageLocation(const QString &path); /** * \return list of all available language specifications */ Q_DECL_DEPRECATED QList languageResources() const; QVector languages() const override; /** * \return language by \p index */ Q_INVOKABLE Language * language(int index) const; /** * \return language by \p learningGoal */ Q_INVOKABLE Language * language(LearnerProfile::LearningGoal* learningGoal) const; QVector courses() const override; QVector editableCourses() const override; QVector courses(Language *language) const override; /** * \return list of all loaded courses for language \p language */ QList courseResources(Language *language); Q_INVOKABLE IEditableCourse * editableCourse(Language *language, int index) const override; /** * Reset the file for this course or skeleton. * * \param course the course to be reloaded */ Q_INVOKABLE void reloadCourseOrSkeleton(ICourse *course); /** * @brief Implementation of course resource reloading */ void reloadCourses() override; /** * Imports units and phrases from skeleton, deassociates removed ones. * * \param course the course to be update */ void updateCourseFromSkeleton(EditableCourseResource *course); /** * Add language to resource manager by parsing the given language specification file. * * \param languageFile is the local XML file containing the language */ void addLanguage(const QUrl &languageFile); /** * Adds course to resource manager by parsing the given course specification file. * * \param courseFile is the local XML file containing the course * \return true if loaded successfully, otherwise false */ EditableCourseResource * addCourse(const QUrl &courseFile); /** * Adds course to resource manager. If the course's language is not registered, the language * is registered by this method. * * \param resource the course resource to add to resource manager */ void addCourseResource(EditableCourseResource *resource); /** * Remove course from resource manager. If the course is modified its changes are NOT * written. For writing changes, the Course::sync() method must be called directly. * * \param course is the course to be removed */ void removeCourse(ICourse *course); /** * Create new course for \p language and derived from \p skeleton. * * \return created course */ Q_INVOKABLE EditableCourseResource * createCourse(Language *language, SkeletonResource *skeleton); /** * Adds skeleton resource to resource manager * * \param resource the skeleton resource to add to resource manager */ void addSkeleton(const QUrl &skeletonFile); /** * Adds skeleton resource to resource manager * * \param resource the skeleton resource to add to resource manager */ void addSkeletonResource(SkeletonResource *resource); /** * Remove skeleton from resource manager. If the skeleton is modified its changes are NOT * written. For writing changes, the Skeleton::sync() method must be called directly. * * \param skeleton is the skeleton to be removed */ void removeSkeleton(SkeletonResource *skeleton); - /** - * \return list of all loaded skeletons resources - */ - QList skeletonResources(); + QVector skeletons() const override; Q_SIGNALS: void languageResourceAdded(); void languageResourceAboutToBeAdded(LanguageResource*,int); void languageResourceRemoved(); void languageResourceAboutToBeRemoved(int); void repositoryChanged(); void courseAdded() override; void courseAboutToBeAdded(ICourse*,int) override; void courseAboutToBeRemoved(int) override; void courseRemoved() override; void skeletonAdded(); void skeletonAboutToBeAdded(ICourse*,int); void skeletonRemoved(); void skeletonAboutToBeRemoved(int,int); void languageCoursesChanged(); private: /** * This method loads all language files that are provided in the standard directories * for this application. */ void loadLanguageResources(); QString m_storageLocation; QList m_languageResources; QMap > m_courses; //!> (language-id, course-resource) - QList m_skeletonResources; + QVector m_skeletonResources; QStringList m_loadedResources; }; #endif diff --git a/src/core/editorsession.cpp b/src/core/editorsession.cpp index d59a760..55c04f4 100644 --- a/src/core/editorsession.cpp +++ b/src/core/editorsession.cpp @@ -1,281 +1,240 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "editorsession.h" #include "core/language.h" #include "core/resources/editablecourseresource.h" #include "core/resources/skeletonresource.h" #include "core/resources/languageresource.h" #include "core/unit.h" #include "core/phrase.h" #include "core/contributorrepository.h" #include "artikulate_debug.h" EditorSession::EditorSession(QObject *parent) : QObject(parent) - , m_repository(nullptr) - , m_skeletonMode(true) - , m_editSkeleton(false) - , m_skeleton(nullptr) - , m_language(nullptr) - , m_course(nullptr) - , m_tmpCourseWhileSkeletonEditing(nullptr) - , m_unit(nullptr) - , m_phrase(nullptr) { } void EditorSession::setRepository(IEditableRepository *repository) { m_repository = repository; - if (!repository->editableCourses().isEmpty()) { - setCourse(repository->editableCourses().first()); - } -} - -void EditorSession::setSkeletonMode(bool enabled) -{ - if (m_skeletonMode == enabled) { - return; - } - m_skeletonMode = enabled; - emit skeletonModeChanged(); } bool EditorSession::skeletonMode() const { return m_skeletonMode; } void EditorSession::setEditSkeleton(bool enabled) { if (m_editSkeleton == enabled) { return; } m_editSkeleton = enabled; if (enabled) { m_tmpCourseWhileSkeletonEditing = m_course; // setCourse(m_skeleton); //FIXME port skeleton for this } else { setCourse(m_tmpCourseWhileSkeletonEditing); m_tmpCourseWhileSkeletonEditing = nullptr; } emit editSkeletonChanged(); } bool EditorSession::isEditSkeleton() const { return m_editSkeleton; } -SkeletonResource * EditorSession::skeleton() const +IEditableCourse * EditorSession::skeleton() const { return m_skeleton; } -void EditorSession::setSkeleton(SkeletonResource *skeleton) +void EditorSession::setSkeleton(IEditableCourse *skeleton) { if (m_skeleton == skeleton) { return; } m_skeleton = skeleton; - Language *language = m_language; - if (!m_language) { - language = m_repository->languages().constFirst(); + if (m_skeletonMode != true) { + m_skeletonMode = true; + emit skeletonModeChanged(); } - - if (m_skeleton) { - bool found = false; - int resources = m_repository->courses(language).count(); - for (int i=0; i < resources; ++i) { - auto course = m_repository->editableCourse(language, i); + IEditableCourse *newCourse{ nullptr }; + if (m_skeleton && m_repository) { + for (const auto &course : m_repository->editableCourses()) { if (course->foreignId() == m_skeleton->id()) { - setCourse(course); - found = true; + newCourse = course; break; } } - if (!found) { - setCourse(nullptr); - } } + setCourse(newCourse); emit skeletonChanged(); } Language * EditorSession::language() const { return m_language; } -void EditorSession::setLanguage(Language *language) -{ - if (m_language == language) { - return; - } - m_language = language; - if (m_skeletonMode) { - bool found = false; - if (m_skeleton) { - int resources = m_repository->courses(m_language).count(); - for (int i=0; i < resources; ++i) { - IEditableCourse *course = m_repository->editableCourse(m_language, i); - if (course->foreignId() == m_skeleton->id()) { - setCourse(course); - found = true; - break; - } - } - } - if (!found) { - setCourse(nullptr); - } - } - else { // not skeleton mode - if (m_repository->courses(m_language).count() > 0) { - setCourse(m_repository->editableCourse(m_language, 0)); - } - } - emit languageChanged(); -} - IEditableCourse * EditorSession::course() const { return m_course; } void EditorSession::setCourse(IEditableCourse *course) { if (m_course == course) { return; } m_course = course; + if (m_skeleton == nullptr || m_skeleton->id() != course->foreignId()) { + for (const auto &skeleton : m_repository->skeletons()) { + if (skeleton->id() == course->foreignId()) { + m_skeleton = skeleton; + emit skeletonChanged(); + break; + } + } + } + + if (m_course != nullptr) { + m_language = m_course->language(); + } else { + m_language = nullptr; + } + emit languageChanged(); + if (m_course && !m_course->unitList().isEmpty()) { setUnit(m_course->unitList().constFirst()); } else { setUnit(nullptr); } emit courseChanged(); } Unit * EditorSession::unit() const { return m_unit; } void EditorSession::setUnit(Unit *unit) { if (m_unit == unit) { return; } m_unit = unit; // different than above, do not directly enter phrases // but first show editing information for units setPhrase(nullptr); emit unitChanged(); } void EditorSession::setPhrase(Phrase *phrase) { if (m_phrase == phrase) { return; } if (phrase) { setUnit(phrase->unit()); } m_phrase = phrase; emit phraseChanged(); } Phrase * EditorSession::phrase() const { return m_phrase; } Phrase * EditorSession::previousPhrase() const { if (!m_phrase) { return nullptr; } const int index = m_phrase->unit()->phraseList().indexOf(m_phrase); if (index > 0) { return m_phrase->unit()->phraseList().at(index - 1); } else { Unit *unit = m_phrase->unit(); int uIndex = unit->course()->unitList().indexOf(unit); if (uIndex > 0) { return unit->course()->unitList().at(uIndex - 1)->phraseList().last(); } } return nullptr; } Phrase * EditorSession::nextPhrase() const { if (!m_phrase) { return nullptr; } const int index = m_phrase->unit()->phraseList().indexOf(m_phrase); if (index < m_phrase->unit()->phraseList().length() - 1) { return m_phrase->unit()->phraseList().at(index + 1); } else { Unit *unit = m_phrase->unit(); int uIndex = unit->course()->unitList().indexOf(unit); if (uIndex < unit->course()->unitList().length() - 1) { Unit *nextUnit = unit->course()->unitList().at(uIndex + 1); if (nextUnit->phraseList().isEmpty()) { return nullptr; } return nextUnit->phraseList().constFirst(); } } return nullptr; } void EditorSession::switchToPreviousPhrase() { setPhrase(previousPhrase()); } void EditorSession::switchToNextPhrase() { setPhrase(nextPhrase()); } bool EditorSession::hasPreviousPhrase() const { return previousPhrase() != nullptr; } bool EditorSession::hasNextPhrase() const { return nextPhrase() != nullptr; } void EditorSession::updateCourseFromSkeleton() { if (!m_course) { qCritical() << "Not updating course from skeleton, no one set."; return; } // m_resourceManager->updateCourseFromSkeleton(m_course); //FIXME } diff --git a/src/core/editorsession.h b/src/core/editorsession.h index b19aadc..fb6cb59 100644 --- a/src/core/editorsession.h +++ b/src/core/editorsession.h @@ -1,120 +1,118 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef EDITORSESSION_H #define EDITORSESSION_H #include "artikulatecore_export.h" #include "phrase.h" class QString; class Language; class IEditableCourse; class Unit; class SkeletonResource; class IEditableRepository; /** * \class EditorSession * * An object of this class is used to set the current state of the editor. By this, we put all logic * how language, skeleton and course fit to each other into this class. The main concept is that * we have to fundamentally different workflows that both are modeled in this class: * * 1. Skeleton based workflow * - a skeleton is selected * - every language is available, since eventually the course should be available in every language * - for every language, there is at most one course (there is none only in case it is not created yet) * - adding new units or phrases is only possible in the skeleton course * - every course can update/sync with the skeleton * * 2. Course based workflow * - there is no skeleton from which the course is derived * - the base is a language that is selected first * - for a language there can be none to arbitrarily many courses * * The main switch is \c EditorSession::setSkeletonMode(bool) */ class ARTIKULATECORE_EXPORT EditorSession : public QObject { Q_OBJECT - Q_PROPERTY(bool skeletonMode READ skeletonMode WRITE setSkeletonMode NOTIFY skeletonModeChanged) + Q_PROPERTY(bool skeletonMode READ skeletonMode NOTIFY skeletonModeChanged) Q_PROPERTY(bool editSkeleton READ isEditSkeleton WRITE setEditSkeleton NOTIFY editSkeletonChanged) - Q_PROPERTY(SkeletonResource *skeleton READ skeleton WRITE setSkeleton NOTIFY skeletonChanged) - Q_PROPERTY(Language *language READ language WRITE setLanguage NOTIFY languageChanged) + Q_PROPERTY(IEditableCourse *skeleton READ skeleton WRITE setSkeleton NOTIFY skeletonChanged) + Q_PROPERTY(Language *language READ language NOTIFY languageChanged) Q_PROPERTY(IEditableCourse *course READ course WRITE setCourse NOTIFY courseChanged) Q_PROPERTY(Unit *unit READ unit WRITE setUnit NOTIFY unitChanged) Q_PROPERTY(Phrase *phrase READ phrase WRITE setPhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasNextPhrase READ hasNextPhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasPreviousPhrase READ hasPreviousPhrase NOTIFY phraseChanged) public: explicit EditorSession(QObject *parent = nullptr); void setRepository(IEditableRepository *repository); - void setSkeletonMode(bool enabled=true); bool skeletonMode() const; void setEditSkeleton(bool enabled=true); bool isEditSkeleton() const; - SkeletonResource * skeleton() const; - void setSkeleton(SkeletonResource *skeleton); + IEditableCourse * skeleton() const; + void setSkeleton(IEditableCourse *skeleton); Language * language() const; - void setLanguage(Language *language); IEditableCourse * course() const; void setCourse(IEditableCourse *course); Unit * unit() const; void setUnit(Unit *unit); Phrase * phrase() const; void setPhrase(Phrase *phrase); Phrase::Type phraseType() const; void setPhraseType(Phrase::Type type); bool hasPreviousPhrase() const; bool hasNextPhrase() const; Q_INVOKABLE void switchToPreviousPhrase(); Q_INVOKABLE void switchToNextPhrase(); Q_INVOKABLE void updateCourseFromSkeleton(); private: Phrase * nextPhrase() const; Phrase * previousPhrase() const; Q_SIGNALS: void editSkeletonChanged(); void skeletonModeChanged(); void skeletonChanged(); void languageChanged(); void courseChanged(); void unitChanged(); void phraseChanged(); private: Q_DISABLE_COPY(EditorSession) - IEditableRepository * m_repository; - bool m_skeletonMode; - bool m_editSkeleton; - SkeletonResource *m_skeleton; - Language *m_language; - IEditableCourse *m_course; - IEditableCourse *m_tmpCourseWhileSkeletonEditing; - Unit *m_unit; - Phrase *m_phrase; + IEditableRepository * m_repository{ nullptr }; + bool m_skeletonMode{ false }; + bool m_editSkeleton{ false }; + IEditableCourse *m_skeleton{ nullptr }; + Language *m_language{ nullptr }; + IEditableCourse *m_course{ nullptr }; + IEditableCourse *m_tmpCourseWhileSkeletonEditing{ nullptr }; + Unit *m_unit{ nullptr }; + Phrase *m_phrase{ nullptr }; }; #endif diff --git a/src/core/ieditablerepository.h b/src/core/ieditablerepository.h index 511db1d..eeb7ceb 100644 --- a/src/core/ieditablerepository.h +++ b/src/core/ieditablerepository.h @@ -1,45 +1,46 @@ /* * 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 IEDITABLEREPOSITORY_H #define IEDITABLEREPOSITORY_H #include "artikulatecore_export.h" #include "iresourcerepository.h" class IEditableCourse; class Language; /** * \class IEditableRepository * This interface provides a generic interface that provides just the methods and signals needed * to integrade a repository into the editing part of Artikulate. */ class ARTIKULATECORE_EXPORT IEditableRepository : public IResourceRepository { Q_OBJECT public: virtual ~IEditableRepository() = default; virtual QVector editableCourses() const = 0; virtual IEditableCourse * editableCourse(Language *language, int index) const = 0; + virtual QVector skeletons() const = 0; }; Q_DECLARE_INTERFACE(IEditableRepository, "IEditableRepository") #endif diff --git a/src/core/resources/skeletonresource.cpp b/src/core/resources/skeletonresource.cpp index d208404..c7a5342 100644 --- a/src/core/resources/skeletonresource.cpp +++ b/src/core/resources/skeletonresource.cpp @@ -1,287 +1,305 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "skeletonresource.h" #include "courseparser.h" #include "core/language.h" #include "core/unit.h" #include "editablecourseresource.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/resources/languageresource.h" #include #include #include #include #include #include #include "artikulate_debug.h" class SkeletonResourcePrivate { public: SkeletonResourcePrivate(const QUrl &path) : m_path(path) { // load basic information from language file, but does not parse everything QXmlStreamReader xml; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { xml.setDevice(&file); xml.readNextStartElement(); while (xml.readNext() && !xml.atEnd()) { if (xml.name() == "id") { m_identifier = xml.readElementText(); continue; } if (xml.name() == "title") { m_title = xml.readElementText(); continue; } if (xml.name() == "description") { m_description = xml.readElementText(); continue; } // quit reading when basic elements are read if (!m_identifier.isEmpty() && !m_title.isEmpty() && !m_description.isEmpty() ) { break; } } if (xml.hasError()) { qCritical() << "Error occurred when reading Course XML file:" << path.toLocalFile(); } } else { qCCritical(ARTIKULATE_CORE()) << "Could not open course file" << path.toLocalFile(); } xml.clear(); file.close(); } QVector units(); void appendUnit(Unit *unit); /** * @return the skeleton resource as serialized byte array */ QDomDocument serializedSkeleton(); QUrl m_path; QString m_identifier; QString m_title; QString m_description; bool m_unitsParsed{ false }; protected: QVector m_units; ///!< the units variable is loaded lazily and shall never be access directly }; QVector SkeletonResourcePrivate::units() { if (m_unitsParsed) { return m_units; } m_units = CourseParser::parseUnits(m_path); m_unitsParsed = true; return m_units; } void SkeletonResourcePrivate::appendUnit(Unit *unit) { units(); // ensure that units are parsed m_units.append(unit); } QDomDocument SkeletonResourcePrivate::serializedSkeleton() { QDomDocument document; // prepare xml header QDomProcessingInstruction header = document.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\"")); document.appendChild(header); // create main element QDomElement root = document.createElement(QStringLiteral("skeleton")); document.appendChild(root); QDomElement idElement = document.createElement(QStringLiteral("id")); QDomElement titleElement = document.createElement(QStringLiteral("title")); QDomElement descriptionElement = document.createElement(QStringLiteral("description")); idElement.appendChild(document.createTextNode(m_identifier)); titleElement.appendChild(document.createTextNode(m_title)); descriptionElement.appendChild(document.createTextNode(m_description)); QDomElement unitListElement = document.createElement(QStringLiteral("units")); // create units for (auto unit : units()) { QDomElement unitElement = document.createElement(QStringLiteral("unit")); QDomElement unitIdElement = document.createElement(QStringLiteral("id")); QDomElement unitTitleElement = document.createElement(QStringLiteral("title")); QDomElement unitPhraseListElement = document.createElement(QStringLiteral("phrases")); unitIdElement.appendChild(document.createTextNode(unit->id())); unitTitleElement.appendChild(document.createTextNode(unit->title())); // construct phrases for (Phrase *phrase : unit->phraseList()) { QDomElement phraseElement = document.createElement(QStringLiteral("phrase")); QDomElement phraseIdElement = document.createElement(QStringLiteral("id")); QDomElement phraseTextElement = document.createElement(QStringLiteral("text")); QDomElement phraseTypeElement = document.createElement(QStringLiteral("type")); phraseIdElement.appendChild(document.createTextNode(phrase->id())); phraseTextElement.appendChild(document.createTextNode(phrase->text())); phraseTypeElement.appendChild(document.createTextNode(phrase->typeString())); phraseElement.appendChild(phraseIdElement); phraseElement.appendChild(phraseTextElement); phraseElement.appendChild(phraseTypeElement); unitPhraseListElement.appendChild(phraseElement); } // construct the unit element unitElement.appendChild(unitIdElement); unitElement.appendChild(unitTitleElement); unitElement.appendChild(unitPhraseListElement); unitListElement.appendChild(unitElement); } root.appendChild(idElement); root.appendChild(titleElement); root.appendChild(descriptionElement); root.appendChild(unitListElement); return document; } SkeletonResource::SkeletonResource(const QUrl &path, IResourceRepository *repository) - : ICourse() + : IEditableCourse() , d(new SkeletonResourcePrivate(path)) { Q_UNUSED(repository); } SkeletonResource::~SkeletonResource() = default; QString SkeletonResource::id() const { return d->m_identifier; } -void SkeletonResource::setId(const QString &id) +void SkeletonResource::setId(QString id) { if (d->m_identifier == id) { return; } d->m_identifier = id; emit idChanged(); } QString SkeletonResource::foreignId() const { return id(); } +void SkeletonResource::setForeignId(QString id) +{ + Q_UNUSED(id); + Q_UNREACHABLE(); +} + QString SkeletonResource::title() const { return d->m_title; } -void SkeletonResource::setTitle(const QString &title) +void SkeletonResource::setTitle(QString title) { if (d->m_title == title) { return; } d->m_title = title; emit titleChanged(); } QString SkeletonResource::i18nTitle() const { // there are no localized titles available return title(); } +void SkeletonResource::setI18nTitle(QString title) +{ + Q_UNUSED(title); + Q_UNREACHABLE(); +} + QString SkeletonResource::description() const { return d->m_description; } -void SkeletonResource::setDescription(const QString &description) +void SkeletonResource::setDescription(QString description) { if (d->m_description == description) { return; } d->m_description = description; emit descriptionChanged(); } bool SkeletonResource::exportCourse(const QUrl &filePath) { // write back to file // create directories if necessary QFileInfo info(filePath.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); if (!info.exists()) { qCDebug(ARTIKULATE_LOG()) << "create xml output file directory, not existing"; QDir dir; dir.mkpath(filePath.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } //TODO port to atomic file swap QFile file(filePath.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { qCWarning(ARTIKULATE_LOG()) << "Unable to open file " << filePath << " in write mode, aborting."; return false; } file.write(d->serializedSkeleton().toByteArray()); return true; } void SkeletonResource::addUnit(Unit *unit) { emit unitAboutToBeAdded(unit, d->units().count() - 1); d->appendUnit(unit); emit unitAdded(); } Language * SkeletonResource::language() const { // skeleton must not have a dedicated language return nullptr; } +void SkeletonResource::setLanguage(Language *language) +{ + Q_UNUSED(language); + Q_UNREACHABLE(); +} + QList SkeletonResource::unitList() { return d->units().toList(); } QUrl SkeletonResource::file() const { return d->m_path; } diff --git a/src/core/resources/skeletonresource.h b/src/core/resources/skeletonresource.h index 98075b1..3906785 100644 --- a/src/core/resources/skeletonresource.h +++ b/src/core/resources/skeletonresource.h @@ -1,76 +1,75 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef SKELETONRESOURCE_H #define SKELETONRESOURCE_H #include "artikulatecore_export.h" -#include "core/icourse.h" +#include "core/ieditablecourse.h" #include class SkeletonResourcePrivate; class IResourceRepository; /** * @brief The SkeletonResource class is a decorator for EditableCourseResource */ -class ARTIKULATECORE_EXPORT SkeletonResource : public ICourse +class ARTIKULATECORE_EXPORT SkeletonResource : public IEditableCourse { Q_OBJECT Q_INTERFACES(ICourse) public: /** * Create course resource from file. */ explicit SkeletonResource(const QUrl &path, IResourceRepository *repository); ~SkeletonResource() override; QString id() const override; - void setId(const QString &id); + void setId(QString id) override; QString foreignId() const override; + void setForeignId(QString id) override; QString title() const override; - - void setTitle(const QString &title); - + void setTitle(QString title) override; QString i18nTitle() const override; + void setI18nTitle(QString title) override; QString description() const override; - - void setDescription(const QString &description); - + void setDescription(QString description) override; Language * language() const override; + void setLanguage(Language *language) override; QList unitList() override; QUrl file() const override; bool exportCourse(const QUrl &filePath); void addUnit(Unit *unit); bool isModified() const { return true;} //FIXME private: const QScopedPointer d; }; #endif diff --git a/src/models/skeletonmodel.cpp b/src/models/skeletonmodel.cpp index 5cef041..b5e706e 100644 --- a/src/models/skeletonmodel.cpp +++ b/src/models/skeletonmodel.cpp @@ -1,185 +1,185 @@ /* * 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 "skeletonmodel.h" #include "core/icourse.h" #include "core/contributorrepository.h" #include "core/resources/skeletonresource.h" #include #include #include #include "artikulate_debug.h" SkeletonModel::SkeletonModel(QObject *parent) : QAbstractListModel(parent) , m_repository(nullptr) , m_signalMapper(new QSignalMapper(this)) { connect(m_signalMapper, SIGNAL(mapped(int)), SLOT(emitSkeletonChanged(int))); } QHash< int, QByteArray > SkeletonModel::roleNames() const { QHash roles; roles[TitleRole] = "title"; roles[DescriptionRole] = "description"; roles[IdRole] = "id"; roles[DataRole] = "dataRole"; return roles; } void SkeletonModel::setResourceRepository(ContributorRepository *repository) { if (m_repository == repository) { return; } beginResetModel(); if (m_repository) { m_repository->disconnect(this); } m_repository = repository; if (m_repository) { //FIXME // connect(m_resourceManager, &ResourceManager::skeletonAboutToBeAdded, this, &SkeletonModel::onSkeletonAboutToBeAdded); // connect(m_resourceManager, &ResourceManager::skeletonAdded, this, &SkeletonModel::onSkeletonAdded); // connect(m_resourceManager, &ResourceManager::skeletonAboutToBeRemoved, this, &SkeletonModel::onSkeletonsAboutToBeRemoved); // connect(m_resourceManager, &ResourceManager::skeletonRemoved, this, &SkeletonModel::onSkeletonsRemoved); } endResetModel(); emit resourceRepositoryChanged(); } ContributorRepository * SkeletonModel::resourceRepository() const { return m_repository; } QVariant SkeletonModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } - if (index.row() >= m_repository->skeletonResources().count()) { + if (index.row() >= m_repository->skeletons().count()) { return QVariant(); } - ICourse * const skeleton = m_repository->skeletonResources().at(index.row()); + ICourse * const skeleton = m_repository->skeletons().at(index.row()); switch(role) { case Qt::DisplayRole: return !skeleton->title().isEmpty() ? QVariant(skeleton->title()): QVariant(i18nc("@item:inlistbox:", "unknown")); case Qt::ToolTipRole: return QVariant(skeleton->title()); case TitleRole: return skeleton->title(); case DescriptionRole: return skeleton->description(); case IdRole: return skeleton->id(); case DataRole: return QVariant::fromValue(skeleton); default: return QVariant(); } } int SkeletonModel::rowCount(const QModelIndex &parent) const { if (!m_repository) { return 0; } if (parent.isValid()) { return 0; } - return m_repository->skeletonResources().count(); + return m_repository->skeletons().count(); } void SkeletonModel::onSkeletonAboutToBeAdded(ICourse *skeleton, int index) { connect(skeleton, SIGNAL(titleChanged()), m_signalMapper, SLOT(map())); //TODO add missing signals beginInsertRows(QModelIndex(), index, index); } void SkeletonModel::onSkeletonAdded() { updateMappings(); endInsertRows(); emit countChanged(); } void SkeletonModel::onSkeletonsAboutToBeRemoved(int first, int last) { beginRemoveRows(QModelIndex(), first, last); } void SkeletonModel::onSkeletonsRemoved() { endRemoveRows(); emit countChanged(); } void SkeletonModel::emitSkeletonChanged(int row) { emit skeletonChanged(row); emit dataChanged(index(row, 0), index(row, 0)); } QVariant SkeletonModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) { return QVariant(); } if (orientation == Qt::Vertical) { return QVariant(section + 1); } return QVariant(i18nc("@title:column", "Skeleton")); } int SkeletonModel::count() const { - return m_repository->skeletonResources().count(); + return m_repository->skeletons().count(); } void SkeletonModel::updateMappings() { - int skeletons = m_repository->skeletonResources().count(); + int skeletons = m_repository->skeletons().count(); for (int i = 0; i < skeletons; ++i) { - m_signalMapper->setMapping(m_repository->skeletonResources().at(i), i); + m_signalMapper->setMapping(m_repository->skeletons().at(i), i); } } QVariant SkeletonModel::skeleton(int row) const { return data(index(row, 0), SkeletonModel::DataRole); }