diff --git a/autotests/editablecourseresource/test_editablecourseresource.cpp b/autotests/editablecourseresource/test_editablecourseresource.cpp index 46325c7..58ed1b3 100644 --- a/autotests/editablecourseresource/test_editablecourseresource.cpp +++ b/autotests/editablecourseresource/test_editablecourseresource.cpp @@ -1,241 +1,241 @@ /* * Copyright 2013-2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_editablecourseresource.h" #include "resourcerepositorystub.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "core/resources/courseparser.h" #include "core/resources/languageresource.h" #include "core/resources/editablecourseresource.h" #include #include #include #include #include #include #include #include #include TestEditableCourseResource::TestEditableCourseResource() { } void TestEditableCourseResource::init() { } void TestEditableCourseResource::cleanup() { } void TestEditableCourseResource::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 TestEditableCourseResource::loadCourseResource() { Language language; language.setId("de"); ResourceRepositoryStub repository({&language}); EditableCourseResource course(QUrl::fromLocalFile(":/courses/de.xml"), &repository); QCOMPARE(course.file().toLocalFile(), ":/courses/de.xml"); QCOMPARE(course.id(), "de"); QCOMPARE(course.foreignId(), "artikulate-basic"); QCOMPARE(course.title(), "Artikulate Deutsch"); QCOMPARE(course.description(), "Ein Kurs in (hoch-)deutscher Aussprache."); QVERIFY(course.language() != nullptr); QCOMPARE(course.language()->id(), "de"); QCOMPARE(course.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(), ":/courses/de_01.ogg"); QCOMPARE(firstPhrase->type(), Phrase::Type::Sentence); QVERIFY(firstPhrase->phonemes().isEmpty()); } void TestEditableCourseResource::unitAddAndRemoveHandling() { // boilerplate Language language; language.setId("de"); ResourceRepositoryStub repository({&language}); EditableCourseResource course(QUrl::fromLocalFile(":/courses/de.xml"), &repository); // begin of test - Unit unit; - unit.setId("testunit"); + Unit *unit = new Unit; // TODO: change to unique pointer when interface is changed to scoped pointers + 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); + course.addUnit(unit); QCOMPARE(course.unitList().count(), initialUnitNumber + 1); QCOMPARE(spyAboutToBeAdded.count(), 1); QCOMPARE(spyAdded.count(), 1); } void TestEditableCourseResource::coursePropertyChanges() { // boilerplate Language language; language.setId("de"); ResourceRepositoryStub repository({&language}); CourseResource course(QUrl::fromLocalFile(":/courses/de.xml"), &repository); // id { const QString value = "newId"; QSignalSpy spy(&course, SIGNAL(idChanged())); QCOMPARE(spy.count(), 0); course.setId(value); QCOMPARE(course.id(), value); QCOMPARE(spy.count(), 1); } // foreign id { const QString value = "newForeignId"; QSignalSpy spy(&course, SIGNAL(foreignIdChanged())); QCOMPARE(spy.count(), 0); course.setForeignId(value); QCOMPARE(course.foreignId(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newTitle"; QSignalSpy spy(&course, SIGNAL(titleChanged())); QCOMPARE(spy.count(), 0); course.setTitle(value); QCOMPARE(course.title(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newI18nTitle"; QSignalSpy spy(&course, SIGNAL(i18nTitleChanged())); QCOMPARE(spy.count(), 0); course.setI18nTitle(value); QCOMPARE(course.i18nTitle(), value); QCOMPARE(spy.count(), 1); } // description { const QString value = "newDescription"; QSignalSpy spy(&course, SIGNAL(descriptionChanged())); QCOMPARE(spy.count(), 0); course.setDescription(value); QCOMPARE(course.description(), value); QCOMPARE(spy.count(), 1); } // language { Language testLanguage; QSignalSpy spy(&course, SIGNAL(languageChanged())); QCOMPARE(spy.count(), 0); course.setLanguage(&testLanguage); QCOMPARE(course.language(), &testLanguage); QCOMPARE(spy.count(), 1); } } void TestEditableCourseResource::fileLoadSaveCompleteness() { // boilerplate Language language; language.setId("de"); ResourceRepositoryStub repository({&language}); EditableCourseResource course(QUrl::fromLocalFile(":/courses/de.xml"), &repository); QTemporaryFile outputFile; outputFile.open(); course.exportCourse(QUrl::fromLocalFile(outputFile.fileName())); // note: this only works, since the resource manager not checks uniqueness of course ids! EditableCourseResource loadedCourse(QUrl::fromLocalFile(outputFile.fileName()), &repository); // test that we actually call the different files QVERIFY(course.file().toLocalFile() != loadedCourse.file().toLocalFile()); QVERIFY(course.id() == loadedCourse.id()); QVERIFY(course.foreignId() == loadedCourse.foreignId()); QVERIFY(course.title() == loadedCourse.title()); QVERIFY(course.description() == loadedCourse.description()); QVERIFY(course.language()->id() == loadedCourse.language()->id()); QVERIFY(course.unitList().count() == loadedCourse.unitList().count()); Unit *testUnit = course.unitList().constFirst(); Unit *compareUnit = loadedCourse.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 for (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()); } QTEST_GUILESS_MAIN(TestEditableCourseResource) diff --git a/src/core/contributorrepository.cpp b/src/core/contributorrepository.cpp index 1babe4e..0831430 100644 --- a/src/core/contributorrepository.cpp +++ b/src/core/contributorrepository.cpp @@ -1,481 +1,492 @@ /* * 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(); } +ContributorRepository::~ContributorRepository() +{ + for (auto skeleton : m_skeletonResources) { + skeleton->deleteLater(); + } + m_skeletonResources.clear(); + for (auto language : m_languageResources) { + language->deleteLater(); + } +} + 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; } } } QVector ContributorRepository::skeletons() const { 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 bd064d9..f0f91e9 100644 --- a/src/core/contributorrepository.h +++ b/src/core/contributorrepository.h @@ -1,216 +1,216 @@ /* * 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); - ~ContributorRepository() override = default; + ~ContributorRepository() override; /** * 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); 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) QVector m_skeletonResources; QStringList m_loadedResources; }; #endif diff --git a/src/core/phonemegroup.cpp b/src/core/phonemegroup.cpp index 0af7e4e..a4baa08 100644 --- a/src/core/phonemegroup.cpp +++ b/src/core/phonemegroup.cpp @@ -1,125 +1,133 @@ /* * 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 "phonemegroup.h" #include "phoneme.h" #include "artikulate_debug.h" PhonemeGroup::PhonemeGroup(QObject *parent) : QObject(parent) { } +PhonemeGroup::~PhonemeGroup() +{ + for (auto phoneme : m_phonemes) { + phoneme->deleteLater(); + } + m_phonemes.clear(); +} + QString PhonemeGroup::id() const { return m_id; } void PhonemeGroup::setId(const QString &id) { if (id != m_id) { m_id = id; emit idChanged(); } } QString PhonemeGroup::title() const { return m_title; } void PhonemeGroup::setTitle(const QString &title) { if (QString::compare(title, m_title) != 0) { m_title = title; emit titleChanged(); } } QString PhonemeGroup::description() const { return m_description; } void PhonemeGroup::setDescription(const QString &description) { m_description = description; emit descriptionChanged(); } QList< Phoneme* > PhonemeGroup::phonemes() const { return m_phonemes; } bool PhonemeGroup::contains(Phoneme *phoneme) const { QList::ConstIterator iter = m_phonemes.constBegin(); while (iter != m_phonemes.constEnd()) { if (QString::compare((*iter)->id(), phoneme->id()) == 0) { return true; } ++iter; } return false; } void PhonemeGroup::addPhoneme(Phoneme *phoneme) { QList::ConstIterator iter = m_phonemes.constBegin(); while (iter != m_phonemes.constEnd()) { if (QString::compare((*iter)->id(), phoneme->id()) == 0) { qCWarning(ARTIKULATE_LOG) << "Phoneme identifier already registered in group "<< m_title <<", aborting"; return; } ++iter; } m_phonemes.append(phoneme); } Phoneme * PhonemeGroup::addPhoneme(const QString &identifier, const QString &title) { Q_ASSERT(!identifier.isEmpty()); // check that identifier is not used QList::ConstIterator iter = m_phonemes.constBegin(); while (iter != m_phonemes.constEnd()) { if (QString::compare((*iter)->id(), identifier) == 0) { qCWarning(ARTIKULATE_LOG) << "Phoneme identifier " << identifier <<" already registered in group " << m_title <<", aborting"; return nullptr; } ++iter; } // create phoneme and add it Phoneme *newPhoneme = new Phoneme(); newPhoneme->setId(identifier); newPhoneme->setTitle(title); addPhoneme(newPhoneme); return newPhoneme; } void PhonemeGroup::removePhoneme(Phoneme *phoneme) { m_phonemes.removeOne(phoneme); } diff --git a/src/core/phonemegroup.h b/src/core/phonemegroup.h index 5abef84..c60a158 100644 --- a/src/core/phonemegroup.h +++ b/src/core/phonemegroup.h @@ -1,76 +1,77 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef PHONEMEGROUP_H #define PHONEMEGROUP_H #include "artikulatecore_export.h" #include #include #include "phoneme.h" class QString; class Phoneme; /** * \class PhonemeGroup */ class ARTIKULATECORE_EXPORT PhonemeGroup : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id WRITE setId NOTIFY idChanged) Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) public: explicit PhonemeGroup(QObject *parent = 0); + ~PhonemeGroup() override; QString id() const; void setId(const QString &id); QString title() const; void setTitle(const QString &title); QString description() const; void setDescription(const QString &description); void addPhoneme(Phoneme *phoneme); Phoneme * addPhoneme(const QString &identifier, const QString &title); void removePhoneme(Phoneme *phoneme); QList phonemes() const; /** * Checks by identifier comparison whether phoneme is registered in this group. * * \param poneme is the phoneme to be checked for if registered * \return true if registered, false otherwise */ bool contains(Phoneme *phoneme) const; signals: void idChanged(); void titleChanged(); void descriptionChanged(); void phonemeAdded(const Phoneme&); void phonemeRemoved(const Phoneme&); private: Q_DISABLE_COPY(PhonemeGroup) QString m_id; QString m_title; QString m_description; QList m_phonemes; }; #endif // PHONEMEGROUP_H diff --git a/src/core/resourcerepository.cpp b/src/core/resourcerepository.cpp index 7b892b5..848ec42 100644 --- a/src/core/resourcerepository.cpp +++ b/src/core/resourcerepository.cpp @@ -1,160 +1,168 @@ /* * 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 "resourcerepository.h" #include "artikulate_debug.h" #include "resources/courseresource.h" #include "resources/languageresource.h" #include #include #include #include ResourceRepository::ResourceRepository(QObject *parent) : ResourceRepository(QUrl::fromLocalFile(QStandardPaths::standardLocations(QStandardPaths::DataLocation).constFirst() + QStringLiteral("/courses/")), parent) { } ResourceRepository::ResourceRepository(const QUrl &storageLocation, QObject *parent) : IResourceRepository() , m_storageLocation(storageLocation.toLocalFile()) { // 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; } loadLanguage(fileInfo.absoluteFilePath()); } } +ResourceRepository::~ResourceRepository() +{ + for (auto language : m_languages) { + language->deleteLater(); + } + m_languages.clear(); +} + QString ResourceRepository::storageLocation() const { return m_storageLocation; } QVector ResourceRepository::courses() const { QVector courses; for (const auto &course : m_courses) { courses.append(course); } return courses; } QVector ResourceRepository::courses(Language *language) const { QVector courses; for (const auto &course : m_courses) { if (language != nullptr && course->language() != language) { continue; } courses.append(course); } return courses; } QVector ResourceRepository::languages() const { QVector languages; for (const auto &language : m_languages) { if (language == nullptr) { continue; } languages.append(language->language()); } return languages; } Language * ResourceRepository::language(const QString &id) const { if (m_languages.contains(id)) { return m_languages.value(id)->language(); } return nullptr; } void ResourceRepository::reloadCourses() { std::function scanDirectoryForXmlCourseFiles = [this](QDir dir) { 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; } loadCourse(fileInfo.absoluteFilePath()); } }; QDir rootDirectory = QDir(m_storageLocation); QDirIterator it(rootDirectory, QDirIterator::Subdirectories); qCInfo(ARTIKULATE_CORE()) << "Loading courses from" << rootDirectory.absolutePath(); while (it.hasNext()) { scanDirectoryForXmlCourseFiles(it.next()); } } bool ResourceRepository::loadCourse(const QString &resourceFile) { qCDebug(ARTIKULATE_CORE()) << "Loading resource" << resourceFile; // skip already loaded resources if (m_loadedCourses.contains(resourceFile)) { qCWarning(ARTIKULATE_CORE()) << "Reloading of resources not yet supported, skippen course"; return false; } CourseResource *resource = new CourseResource(QUrl::fromLocalFile(resourceFile), this); if (resource->language() == nullptr) { resource->deleteLater(); qCCritical(ARTIKULATE_CORE()) << "Could not load course, language unknown:" << resourceFile; return false; } emit courseAboutToBeAdded(resource, m_courses.count() - 1); m_courses.append(resource); emit courseAdded(); m_loadedCourses.append(resourceFile); return true; } bool ResourceRepository::loadLanguage(const QString &resourceFile) { LanguageResource *resource = new LanguageResource(QUrl::fromLocalFile(resourceFile)); if (!resource) { qCWarning(ARTIKULATE_CORE()) << "Could not load language" << resourceFile; resource->deleteLater(); return false; } if (m_languages.contains(resource->identifier())) { qCWarning(ARTIKULATE_CORE()) << "Could not load language" << resourceFile; resource->deleteLater(); return false; } m_languages.insert(resource->identifier(), resource); return true; } diff --git a/src/core/resourcerepository.h b/src/core/resourcerepository.h index 8e17b94..ab1e559 100644 --- a/src/core/resourcerepository.h +++ b/src/core/resourcerepository.h @@ -1,103 +1,104 @@ /* * 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 RESOURCEREPOSITORY_H #define RESOURCEREPOSITORY_H #include "artikulatecore_export.h" #include "iresourcerepository.h" #include #include #include #include #include class QUrl; class CourseResource; class ICourse; class Language; class LanguageResource; /** * @class ResourceRepository * * This class provides data handling of all downloaded trainingdata of a user. It dervies from the repository interface * to provide a generalized access to the data. */ class ARTIKULATECORE_EXPORT ResourceRepository : public IResourceRepository { Q_OBJECT Q_INTERFACES(IResourceRepository) public: explicit ResourceRepository(QObject *parent = nullptr); + ~ResourceRepository() override; /** * @brief Construtor for ResourceRepository object with explicitly set course folder * * @param storageLocation relative or absolute path to courses/ folder (including that directory) * @param parent the parent object in the QObject hierarchy */ explicit ResourceRepository(const QUrl &storageLocation, QObject *parent = nullptr); /** * @return path to repository location */ QString storageLocation() const override; /** * @return list of available courses */ QVector courses() const override; /** * @return list of available courses */ QVector courses(Language *language) const override; /** * @return list of all available language specifications */ QVector languages() const override; Language * language(const QString &id) const; public Q_SLOTS: /** * \brief updates available resources */ void reloadCourses() override; Q_SIGNALS: void courseAboutToBeAdded(ICourse*, int) override; void courseAdded() override; void courseAboutToBeRemoved(int) override; void courseRemoved() override; private: bool loadCourse(const QString &resourceFile); bool loadLanguage(const QString &resourceFile); QVector m_courses; QHash m_languages; ///>! (language-identifier, language resource) QStringList m_loadedCourses; const QString m_storageLocation; }; #endif // RESOURCEREPOSITORY_H diff --git a/src/core/resources/courseresource.cpp b/src/core/resources/courseresource.cpp index bf0e9ce..59041f5 100644 --- a/src/core/resources/courseresource.cpp +++ b/src/core/resources/courseresource.cpp @@ -1,305 +1,314 @@ /* * 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/language.h" #include "core/unit.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/language.h" #include "core/iresourcerepository.h" #include #include #include #include #include #include #include "artikulate_debug.h" class CourseResourcePrivate { public: CourseResourcePrivate() = default; + ~CourseResourcePrivate(); void loadCourse(CourseResource *parent); IResourceRepository *m_repository{ nullptr }; 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; QVector m_units; bool m_courseLoaded{ false }; ///deleteLater(); + } + m_units.clear(); +} + void CourseResourcePrivate::loadCourse(CourseResource *parent) { if (m_courseLoaded == true) { qCWarning(ARTIKULATE_CORE()) << "Skipping loading of course, no reloading implemented yet"; return; } m_courseLoaded = true; QFileInfo info(m_file.toLocalFile()); if (!info.exists()) { qCCritical(ARTIKULATE_CORE()) << "No course file available at location" << m_file.toLocalFile(); return; } // load existing file QXmlSchema schema = CourseParser::loadXmlSchema(QStringLiteral("course")); if (!schema.isValid()) { qCWarning(ARTIKULATE_CORE()) << "Scheme not valid, aborting"; return; } QDomDocument document = CourseParser::loadDomDocument(m_file, schema); if (document.isNull()) { qCWarning(ARTIKULATE_CORE()) << "Could not parse document " << m_file.toLocalFile() << ", aborting."; return; } // load missing elements of course // TODO usage of QDomElement is quite slow and should be exchanged with the QXmlParser in the future QDomElement root(document.documentElement()); if (!root.firstChildElement(QStringLiteral("foreignId")).isNull()) { m_foreignId = root.firstChildElement(QStringLiteral("foreignId")).text(); } // 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(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 } } } CourseResource::CourseResource(const QUrl &path, IResourceRepository *repository) : ICourse(repository) , d(new CourseResourcePrivate()) { d->m_file = path; d->m_repository = repository; // load basic information from language file, but does not parse everything QXmlStreamReader xml; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { xml.setDevice(&file); xml.readNextStartElement(); while (xml.readNext() && !xml.atEnd()) { if (xml.name() == "id") { d->m_identifier = xml.readElementText(); continue; } if (xml.name() == "foreignId") { d->m_foreignId = xml.readElementText(); continue; } //TODO i18nTitle must be implemented, currently missing and hence not parsed if (xml.name() == "title") { d->m_title = xml.readElementText(); d->m_i18nTitle = d->m_title; continue; } if (xml.name() == "description") { d->m_description = xml.readElementText(); continue; } if (xml.name() == "language") { d->m_languageId = xml.readElementText(); continue; } // quit reading when basic elements are read if (!d->m_identifier.isEmpty() && !d->m_title.isEmpty() && !d->m_i18nTitle.isEmpty() && !d->m_description.isEmpty() && !d->m_languageId.isEmpty() && !d->m_foreignId.isEmpty() ) { break; } } if (xml.hasError()) { qCritical() << "Error occurred when reading Course XML file:" << path.toLocalFile(); } } else { qCCritical(ARTIKULATE_CORE()) << "Could not open course file" << path.toLocalFile(); } xml.clear(); file.close(); // find correct language if (repository != nullptr) { for (const auto &language : repository->languages()) { if (language == nullptr) { continue; } if (language->id() == d->m_languageId) { d->m_language = language; } } } if (d->m_language == nullptr) { qCCritical(ARTIKULATE_CORE()) << "A course with an unknown language was loaded"; } } CourseResource::~CourseResource() = default; QString CourseResource::id() const { return d->m_identifier; } void CourseResource::setId(const QString &id) { if (d->m_identifier == id) { return; } d->m_identifier = id; emit idChanged(); } QString CourseResource::foreignId() const { return d->m_foreignId; } void CourseResource::setForeignId(const QString &foreignId) { if (d->m_foreignId == foreignId) { return; } d->m_foreignId = foreignId; emit foreignIdChanged(); } QString CourseResource::title() const { return d->m_title; } void CourseResource::setTitle(const QString &title) { if (d->m_title == title) { return; } d->m_title = title; emit titleChanged(); } QString CourseResource::i18nTitle() const { return d->m_i18nTitle; } void CourseResource::setI18nTitle(const QString &i18nTitle) { if (d->m_i18nTitle == i18nTitle) { return; } d->m_i18nTitle = i18nTitle; emit i18nTitleChanged(); } QString CourseResource::description() const { return d->m_description; } void CourseResource::setDescription(const QString &description) { if (d->m_description == description) { return; } d->m_description = description; emit descriptionChanged(); } Language * CourseResource::language() const { return d->m_language; } void CourseResource::setLanguage(Language *language) { if (d->m_language == language) { return; } d->m_language = language; emit languageChanged(); } void CourseResource::addUnit(Unit *unit) { emit unitAboutToBeAdded(unit, d->m_units.count() - 1); d->m_units.append(unit); emit unitAdded(); } QList CourseResource::unitList() { if (d->m_courseLoaded == false) { d->loadCourse(this); } return d->m_units.toList(); } QVector CourseResource::units() { if (d->m_courseLoaded == false) { d->loadCourse(this); } return d->m_units; } QUrl CourseResource::file() const { return d->m_file; } diff --git a/src/core/unit.cpp b/src/core/unit.cpp index 634009c..4a5f68c 100644 --- a/src/core/unit.cpp +++ b/src/core/unit.cpp @@ -1,163 +1,167 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * Copyright 2013 Oindrila Gupta * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "unit.h" #include "phrase.h" #include #include #include #include #include #include "artikulate_debug.h" #include #include Unit::Unit(QObject *parent) : QObject(parent) , m_course(nullptr) , m_phraseSignalMapper(new QSignalMapper(this)) { } Unit::~Unit() { + for (auto phrase : m_phraseList) { + phrase->deleteLater(); + } + m_phraseList.clear(); m_phraseSignalMapper->deleteLater(); } QString Unit::id() const { return m_id; } void Unit::setId(const QString &id) { if (id != m_id) { m_id = id; emit idChanged(); emit modified(); } } QString Unit::foreignId() const { return m_foreignId; } void Unit::setForeignId(const QString &id) { m_foreignId = id; } ICourse *Unit::course() const { return m_course; } void Unit::setCourse(ICourse *course) { if (course == m_course) { return; } m_course = course; emit courseChanged(); } QString Unit::title() const { return m_title; } void Unit::setTitle(const QString &title) { if (QString::compare(title, m_title) != 0) { m_title = title; emit titleChanged(); emit modified(); } } QList< Phrase* > Unit::phraseList() const { return m_phraseList; } void Unit::addPhrase(Phrase *phrase) { QList::ConstIterator iter = m_phraseList.constBegin(); while (iter != m_phraseList.constEnd()) { if (phrase->id() == (*iter)->id()) { qCWarning(ARTIKULATE_LOG()) << "Phrase is already contained in this unit, aborting"; return; } ++iter; } phrase->setUnit(this); emit phraseAboutToBeAdded(phrase, m_phraseList.length()); m_phraseList.append(phrase); m_phraseSignalMapper->setMapping(phrase, phrase->id()); emit phraseAdded(phrase); connect(phrase, &Phrase::typeChanged, m_phraseSignalMapper, static_cast(&QSignalMapper::map)); connect(phrase, &Phrase::modified, this, &Unit::modified); emit modified(); } QList Unit::excludedSkeletonPhraseList() const { QList excludedPhraseList; QList::ConstIterator iter = m_phraseList.constBegin(); while (iter != m_phraseList.constEnd()) { if ((*iter)->isExcluded() == true) { excludedPhraseList.append(*iter); } ++iter; } return excludedPhraseList; } void Unit::excludeSkeletonPhrase(const QString &phraseId) { foreach (Phrase *phrase, m_phraseList) { if (phrase->id() == phraseId) { phrase->setExcluded(true); emit modified(); return; } } qCWarning(ARTIKULATE_LOG) << "Could not exclude phrase with ID " << phraseId << ", no phrase with this ID."; } void Unit::includeSkeletonPhrase(const QString &phraseId) { foreach (Phrase *phrase, m_phraseList) { if (phrase->id() == phraseId) { phrase->setExcluded(false); emit modified(); return; } } qCWarning(ARTIKULATE_LOG) << "Could not include phrase with ID " << phraseId << ", no phrase with this ID."; }