diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index b1647f2..15acaa4 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,96 +1,108 @@ ### # Copyright 2013-2019 Andreas Cord-Landwehr # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ### include_directories( ../src/ ../ ${CMAKE_CURRENT_BINARY_DIR} ) # copy test data file(COPY testdata/courses/de.xml DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/courses/de/) # copy test files file(COPY testdata/courses/fr.xml DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/courses/fr/) # copy test files +file(COPY testdata/contributorrepository/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/contributorrepository/) # copy test files set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) # repository tests set(TestResourceRepository_SRCS resourcerepository/test_resourcerepository.cpp) qt5_add_resources(TestResourceRepository_SRCS ../data/languages.qrc) add_executable(test_resourcerepository ${TestResourceRepository_SRCS}) target_link_libraries(test_resourcerepository artikulatecore Qt5::Test ) add_test(test_resourcerepository test_resourcerepository) ecm_mark_as_test(test_resourcerepository) +# integration tests for iresource repository interface derived classes +set(TestIResourceRepository_SRCS iresourcerepository_integration/test_iresourcerepository.cpp) +qt5_add_resources(TestIResourceRepository_SRCS ../data/languages.qrc) +add_executable(test_iresourcerepository_integration ${TestIResourceRepository_SRCS}) +target_link_libraries(test_iresourcerepository_integration + artikulatecore + Qt5::Test +) +add_test(test_iresourcerepository_integration test_iresourcerepository_integration) +ecm_mark_as_test(test_iresourcerepository_integration) + # training session tests set(TestTrainingSession_SRCS trainingsession/test_trainingsession.cpp) add_executable(test_trainingsession ${TestTrainingSession_SRCS}) target_link_libraries(test_trainingsession artikulatecore Qt5::Test ) add_test(test_trainingsession test_trainingsession) ecm_mark_as_test(test_trainingsession) # test course resource class set(TestCourseResource_SRCS courseresource/test_courseresource.cpp courseresource/resourcerepositorystub.cpp ) qt5_add_resources(TestCourseResource_SRCS ../data/languages.qrc) add_executable(test_courseresource ${TestCourseResource_SRCS} ) target_link_libraries(test_courseresource artikulatecore Qt5::Test ) add_test(test_courseresource test_courseresource) ecm_mark_as_test(test_courseresource) # test editable course resource class set(TestEditableCourseResource_SRCS editablecourseresource/test_editablecourseresource.cpp editablecourseresource/resourcerepositorystub.cpp ) qt5_add_resources(TestEditableCourseResource_SRCS ../data/languages.qrc) qt5_add_resources(TestEditableCourseResource_SRCS testdata/testdata.qrc) add_executable(test_editablecourseresource ${TestEditableCourseResource_SRCS} ) target_link_libraries(test_editablecourseresource artikulatecore Qt5::Test ) add_test(test_editablecourseresource test_editablecourseresource) ecm_mark_as_test(test_editablecourseresource) # basic tests language files (input/output) set(TestLanguageFiles_SRCS testlanguagefiles.cpp) add_executable(TestLanguageFiles ${TestLanguageFiles_SRCS} ) target_link_libraries(TestLanguageFiles artikulatecore Qt5::Test ) add_test(TestLanguageFiles TestLanguageFiles) ecm_mark_as_test(TestLanguageFiles) diff --git a/autotests/iresourcerepository_integration/test_iresourcerepository.cpp b/autotests/iresourcerepository_integration/test_iresourcerepository.cpp new file mode 100644 index 0000000..829cbfb --- /dev/null +++ b/autotests/iresourcerepository_integration/test_iresourcerepository.cpp @@ -0,0 +1,89 @@ +/* + * 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_iresourcerepository.h" +#include +#include +#include + +#include "src/core/resourcerepository.h" +#include "src/core/contributorrepository.h" +#include "src/core/language.h" +#include "../src/settings.h" + +void TestIResourceRepository::init() +{ + // check that test data is deployed at the expected location + QVERIFY(QFile::exists("data/courses/de/de.xml")); + QVERIFY(QFile::exists("data/courses/fr/fr.xml")); +} + +void TestIResourceRepository::resourceRepository() +{ + ResourceRepository repository(QUrl::fromLocalFile("data/courses/")); + QCOMPARE(repository.storageLocation(), "data/courses/"); + performInterfaceTests(&repository); +} + +void TestIResourceRepository::contributorRepository() +{ + ContributorRepository repository; + repository.setStorageLocation("data/contributorrepository/"); // contributor repository requires subdirectory "courses" + QCOMPARE(repository.storageLocation(), "data/contributorrepository/"); + performInterfaceTests(&repository); +} + +void TestIResourceRepository::performInterfaceTests(IResourceRepository *interface) +{ + QVERIFY(interface->languages().count() > 0); // automatically load languages + QCOMPARE(interface->courses().count(), 0); // load courses only on demand + + // test adding + QSignalSpy spyAboutToBeAdded(dynamic_cast(interface), SIGNAL(courseAboutToBeAdded(ICourse*, int))); + QSignalSpy spyAdded(dynamic_cast(interface), SIGNAL(courseAdded())); + QCOMPARE(spyAboutToBeAdded.count(), 0); + QCOMPARE(spyAdded.count(), 0); + interface->reloadCourses(); // initial loading of courses + QCOMPARE(interface->courses().count(), 2); + QCOMPARE(spyAboutToBeAdded.count(), 2); + QCOMPARE(spyAdded.count(), 2); + + // test reloading of courses + interface->reloadCourses(); // initial loading of courses + QCOMPARE(interface->courses().count(), 2); + + // test removal + // note: repository does not provide removal of courses, yet + + // test access of courses grouped by language + auto languages = interface->languages(); + Language *german = nullptr; + for (auto language : interface->languages()) { + if (language->id() == "de") { + german = language; + break; + } + } + QVERIFY(german != nullptr); // ensure that German language was found + QCOMPARE(interface->courses(german).count(), 1); // there is exactly one German course + QCOMPARE(interface->courses(nullptr).count(), 2); // all courses in total are 2 +} + +QTEST_GUILESS_MAIN(TestIResourceRepository) diff --git a/autotests/iresourcerepository_integration/test_iresourcerepository.h b/autotests/iresourcerepository_integration/test_iresourcerepository.h new file mode 100644 index 0000000..955ed06 --- /dev/null +++ b/autotests/iresourcerepository_integration/test_iresourcerepository.h @@ -0,0 +1,59 @@ +/* + * 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_IRESOURCEREPOSITORY_H +#define TEST_IRESOURCEREPOSITORY_H + +#include + +class IResourceRepository; + +class TestIResourceRepository : public QObject +{ + Q_OBJECT + +public: + TestIResourceRepository() = default; + +private Q_SLOTS: + /** + * Called before every test case. + */ + void init(); + + /** + * @brief integration test of ResourceRepository for IResourceRepository interface contract + * Test that expectations of the IResourceRepository interface are implemented by + * ResourceRepository class. + */ + void resourceRepository(); + + /** + * @brief integration test of ContributorRepository for IResourceRepository interface contract + * Test that expectations of the IResourceRepository interface are implemented by + * ContributorRepository class. + */ + void contributorRepository(); + +private: + void performInterfaceTests(IResourceRepository *repository); +}; + +#endif diff --git a/autotests/testdata/contributorrepository/courses/coursename/de/de.xml b/autotests/testdata/contributorrepository/courses/coursename/de/de.xml new file mode 100644 index 0000000..66f5e2a --- /dev/null +++ b/autotests/testdata/contributorrepository/courses/coursename/de/de.xml @@ -0,0 +1,41 @@ + + + de + artikulate-basic + Artikulate Deutsch + Ein Kurs in (hoch-)deutscher Aussprache. + de + + + 1 + {dd60f04a-eb37-44b7-9787-67aaf7d3578d} + Auf der Straße + + + 1 + {3a4c1926-60d7-44c6-80d1-03165a641c75} + Guten Tag. + de_01.ogg + sentence + + + + 2 + + Auf Wiedersehen. + de_02.ogg + sentence + + + + 3 + {56b0d0a2-8505-4a9e-89f7-a933824fac89} + Wie geht es dir? + + sentence + + + + + + diff --git a/autotests/testdata/contributorrepository/courses/coursename/fr/fr.xml b/autotests/testdata/contributorrepository/courses/coursename/fr/fr.xml new file mode 100644 index 0000000..3ed0e11 --- /dev/null +++ b/autotests/testdata/contributorrepository/courses/coursename/fr/fr.xml @@ -0,0 +1,38 @@ + + + fr + ArtiKulate Français + Course française. + fr + + + 1 + Cuisine Français + + + 1 + Qu'est-ce que vous avez choisi? + + sentence + + + + 2 + Moi, comme entrée, une salade exotique. + + sentence + + + + 3 + eau + + word + + oh + + + + + + diff --git a/src/core/contributorrepository.cpp b/src/core/contributorrepository.cpp index e826ba5..fae26f3 100644 --- a/src/core/contributorrepository.cpp +++ b/src/core/contributorrepository.cpp @@ -1,484 +1,474 @@ /* * 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 "language.h" #include "skeleton.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 "settings.h" #include "liblearnerprofile/src/profilemanager.h" #include "liblearnerprofile/src/learninggoal.h" #include #include #include #include #include #include #include #include #include #include "artikulate_debug.h" #include #include ContributorRepository::ContributorRepository(QObject *parent) : IResourceRepository() { -} - -void ContributorRepository::loadCourseResources() -{ - //TODO fix this method such that it may be called many times of e.g. updating - - // reload config, could be changed in dialogs - Settings::self()->load(); - - // register skeleton resources - QDir skeletonRepository = QDir(Settings::courseRepositoryPath()); - skeletonRepository.setFilter(QDir::Files | QDir::Hidden); - if (!skeletonRepository.cd(QStringLiteral("skeletons"))) { - qCritical() << "There is no subdirectory \"skeletons\" in directory " << skeletonRepository.path() - << " cannot load skeletons."; - } else { - // read skeletons - QFileInfoList list = skeletonRepository.entryInfoList(); - for (int i = 0; i < list.size(); ++i) { - QFileInfo fileInfo = list.at(i); - addSkeleton(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); - } - } - - // register contributor course files - QDir courseRepository = QDir(Settings::courseRepositoryPath()); - if (!courseRepository.cd(QStringLiteral("courses"))) { - qCritical() << "There is no subdirectory \"courses\" in directory " << courseRepository.path() - << " cannot load courses."; - } else { - // find courses - courseRepository.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); - QFileInfoList courseDirList = courseRepository.entryInfoList(); - - // traverse all course directories - foreach (const QFileInfo &info, courseDirList) { - QDir courseDir = QDir(info.absoluteFilePath()); - courseDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); - QFileInfoList courseLangDirList = courseDir.entryInfoList(); - - // traverse all language directories for each course - foreach (const QFileInfo &langInfo, courseLangDirList) { - QDir courseLangDir = QDir(langInfo.absoluteFilePath()); - courseLangDir.setFilter(QDir::Files); - QStringList nameFilters; - nameFilters.append(QStringLiteral("*.xml")); - QFileInfoList courses = courseLangDir.entryInfoList(nameFilters); - - // find and add course files - foreach (const QFileInfo &courseInfo, courses) { - addCourse(QUrl::fromLocalFile(courseInfo.filePath())); - } - } - } - } - - // register GHNS course resources - QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::DataLocation); - foreach (const QString &testdir, dirs) { - QDirIterator it(testdir + "/courses/", QDirIterator::Subdirectories); - while (it.hasNext()) { - QDir dir(it.next()); - dir.setFilter(QDir::Files | QDir::NoSymLinks); - QFileInfoList list = dir.entryInfoList(); - for (int i = 0; i < list.size(); ++i) { - QFileInfo fileInfo = list.at(i); - if (fileInfo.completeSuffix() != QLatin1String("xml")) { - continue; - } - addCourse(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); - } - } - } - - //TODO this signal should only be emitted when repository was added/removed - // yet the call to this method is very seldom and emitting it too often is not that harmful - emit repositoryChanged(); + 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->isOpen() && courseRes->skeleton()->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(); } -bool ContributorRepository::isRepositoryManager() const +QString ContributorRepository::storageLocation() const { - return !Settings::courseRepositoryPath().isEmpty(); + return m_storageLocation; } -QString ContributorRepository::storageLocation() const +void ContributorRepository::setStorageLocation(const QString &path) { - return Settings::courseRepositoryPath(); + 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 { - return QVector(); //TODO and check if overload for editable is needed + QVector courses; + for (const auto &courseList : m_courses) { + for (const auto &course : courseList) { + courses.append(course); + } + } + return courses; } QVector ContributorRepository::courses(Language *language) const { - return QVector(); //TODO and check if overload for editable is needed + 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; } EditableCourseResource * ContributorRepository::course(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 { foreach (SkeletonResource *resource, m_skeletonResources) { if (resource->identifier() == courseOrSkeleton->id()) { resource->reload(); 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; QList::ConstIterator iter = m_skeletonResources.constBegin(); while (iter != m_skeletonResources.constEnd()) { if ((*iter)->identifier() == course->foreignId()) { skeleton = (*iter)->skeleton(); break; } ++iter; } if (!skeleton) { qCritical() << "Could not find skeleton with id " << course->foreignId() << ", aborting update."; return; } // update now foreach (Unit *unitSkeleton, skeleton->unitList()) { // import unit if not exists Unit *currentUnit = nullptr; bool found = false; foreach (Unit *unit, course->unitList()) { if (unit->foreignId() == unitSkeleton->id()) { found = true; currentUnit = unit; break; } } if (found == false) { currentUnit = new Unit(course); currentUnit->setId(QUuid::createUuid().toString()); currentUnit->setTitle(unitSkeleton->title()); currentUnit->setForeignId(unitSkeleton->id()); currentUnit->setCourse(course); course->addUnit(currentUnit); course->setModified(true); } // update phrases foreach (Phrase *phraseSkeleton, unitSkeleton->phraseList()) { bool found = false; foreach (Phrase *phrase, currentUnit->phraseList()) { if (phrase->foreignId() == phraseSkeleton->id()) { if (phrase->i18nText() != phraseSkeleton->text()) { phrase->setEditState(Phrase::Unknown); phrase->seti18nText(phraseSkeleton->text()); } found = true; break; } } if (found == false) { Phrase *newPhrase = new Phrase(course); newPhrase->setForeignId(phraseSkeleton->id()); newPhrase->setId(QUuid::createUuid().toString()); newPhrase->setText(phraseSkeleton->text()); newPhrase->seti18nText(phraseSkeleton->text()); newPhrase->setType(phraseSkeleton->type()); newPhrase->setUnit(currentUnit); currentUnit->addPhrase(newPhrase); course->setModified(true); } } } // FIXME deassociate removed phrases qCDebug(ARTIKULATE_LOG) << "Update performed!"; } 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())) { -// emit courseResourceAboutToBeAdded(resource, m_courses[resource->language()].count()); //FIXME - } - else { - emit courseAboutToBeAdded(resource, 0); + 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, Skeleton *skeleton) { // set path QString path = QStringLiteral("%1/%2/%3/%4/%4.xml") - .arg(Settings::courseRepositoryPath(), + .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 &skeletonFile) { SkeletonResource *resource = new SkeletonResource(skeletonFile); addSkeletonResource(resource); } void ContributorRepository::addSkeletonResource(SkeletonResource *resource) { // skip already loaded resources if (m_loadedResources.contains(resource->path().toLocalFile())) { return; } m_loadedResources.append(resource->path().toLocalFile()); emit skeletonAboutToBeAdded(resource->skeleton(), m_skeletonResources.count()); m_skeletonResources.append(resource); emit skeletonAdded(); } void ContributorRepository::removeSkeleton(Skeleton *skeleton) { for (int index = 0; index < m_skeletonResources.length(); ++index) { if (m_skeletonResources.at(index)->identifier() == skeleton->id()) { emit skeletonAboutToBeRemoved(index, index); m_skeletonResources.removeAt(index); emit skeletonRemoved(); skeleton->deleteLater(); return; } } } QList< SkeletonResource* > ContributorRepository::skeletonResources() { return m_skeletonResources; } diff --git a/src/core/contributorrepository.h b/src/core/contributorrepository.h index 0d4b9ae..4caebbe 100644 --- a/src/core/contributorrepository.h +++ b/src/core/contributorrepository.h @@ -1,230 +1,219 @@ /* * 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 "iresourcerepository.h" #include #include #include #include #include "liblearnerprofile/src/learninggoal.h" class SkeletonResource; class EditableCourseResource; class LanguageResource; class Skeleton; class Language; class ICourse; -class ProfileManager; class QUrl; -namespace LearnerProfile { - class ProfileManager; -} - /** * @class ContributorRepository * This class handles the resources of a contributor. */ class ARTIKULATECORE_EXPORT ContributorRepository : public IResourceRepository { Q_OBJECT Q_INTERFACES(IResourceRepository) Q_PROPERTY(QString repositoryUrl READ storageLocation NOTIFY repositoryChanged) public: explicit ContributorRepository(QObject *parent = nullptr); - /** - * Load all course resources. - * This loading is very fast, since course files are only partly (~20 top lines) parsed and - * the complete parsing is postponed until first access. - * - * This method is safe to be called several times for incremental updates. - */ - Q_INVOKABLE void loadCourseResources(); - - /** - * This method loads all language files that are provided in the standard directories - * for this application. - */ - void loadLanguageResources(); - /** * 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 \c true if a repository is used, else \c false + * \return path to working repository, if one is set */ - Q_INVOKABLE bool isRepositoryManager() const; + QString storageLocation() const override; /** - * \return path to working repository, if one is set + * Set path to central storage location + * \param path the path to the storage location directory */ - QString storageLocation() const override; + void setStorageLocation(const QString &path); /** * \return list of all available language specifications */ Q_DECL_DEPRECATED QList languageResources() const; /** * \return list of all available language specifications */ QVector languages() const; /** * \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 courses(Language *language) const override; /** * \return list of all loaded courses for language \p language */ QList courseResources(Language *language); Q_INVOKABLE EditableCourseResource * course(Language *language, int index) const; /** * Reset the file for this course or skeleton. * * \param course the course to be reloaded */ Q_INVOKABLE void reloadCourseOrSkeleton(ICourse *course); - //TODO implement some logic - void reloadCourses() override {} + /** + * @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, Skeleton *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(Skeleton *skeleton); /** * \return list of all loaded skeletons resources */ QList skeletonResources(); 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; QStringList m_loadedResources; }; #endif diff --git a/src/mainwindow_editor.cpp b/src/mainwindow_editor.cpp index 57e8916..976656d 100644 --- a/src/mainwindow_editor.cpp +++ b/src/mainwindow_editor.cpp @@ -1,198 +1,197 @@ /* * 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 "mainwindow_editor.h" #include "application.h" #include "ui/resourcesdialogpage.h" #include "ui/sounddevicedialogpage.h" #include "ui/appearencedialogpage.h" #include "ui/exportghnsdialog.h" #include "core/editorsession.h" #include "core/resources/courseresource.h" #include "models/languagemodel.h" #include "settings.h" #include "libsound/src/outputdevicecontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include "artikulate_debug.h" #include #include #include #include #include #include #include #include #include #include using namespace LearnerProfile; MainWindowEditor::MainWindowEditor(ContributorRepository *repository) : m_repository(repository) , m_editorSession(new EditorSession()) , m_widget(new QQuickWidget) { m_editorSession->setContributorRepository(m_repository); setWindowIcon(QIcon::fromTheme(QStringLiteral("artikulate"))); setWindowTitle(qAppName()); setAutoSaveSettings(); // workaround for QTBUG-40765 qApp->setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); // load saved sound settings OutputDeviceController::self().setVolume(Settings::audioOutputVolume()); // load resources - m_repository->loadLanguageResources(); - if (m_repository->languageResources().count() == 0) { + if (m_repository->languages().count() == 0) { qFatal("No language resources found, cannot start application."); } - m_repository->loadCourseResources(); + m_repository->reloadCourses(); // create menu setupActions(); // set view m_widget->resize(QSize(800, 600)); m_widget->rootContext()->setContextObject(new KLocalizedContext(m_widget)); m_widget->rootContext()->setContextProperty(QStringLiteral("g_resourceManager"), m_repository); m_widget->rootContext()->setContextProperty(QStringLiteral("editorSession"), m_repository); // set starting screen m_widget->setSource(QUrl(QStringLiteral("qrc:/artikulate/qml/Editor.qml"))); m_widget->setResizeMode(QQuickWidget::SizeRootObjectToView); QAction *newAct = KStandardAction::save(this, SLOT(save()), actionCollection()); actionCollection()->addAction(QStringLiteral("save"), newAct); // set status bar statusBar()->setEnabled(true); QLabel *repositoryLabel = new QLabel; repositoryLabel->setText(i18n("Course Repository: %1", m_repository->storageLocation())); connect(m_repository, &ContributorRepository::repositoryChanged, this, [=]() { repositoryLabel->setText(i18n("Course Repository: %1", m_repository->storageLocation())); }); statusBar()->insertWidget(0, repositoryLabel); createGUI(QStringLiteral("artikulateui_editor.rc")); setCentralWidget(m_widget); } MainWindowEditor::~MainWindowEditor() { // save current settings for case of closing Settings::self()->save(); } ContributorRepository * MainWindowEditor::resourceRepository() const { return m_repository; } void MainWindowEditor::setupActions() { QAction *settingsAction = new QAction(i18nc("@item:inmenu", "Configure Artikulate"), this); connect(settingsAction, &QAction::triggered, this, &MainWindowEditor::showSettingsDialog); actionCollection()->addAction(QStringLiteral("settings"), settingsAction); settingsAction->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); QAction *exportAction = new QAction(i18nc("@item:inmenu", "Export GHNS Files"), this); connect(exportAction, &QAction::triggered, this, [=]() { QPointer dialog = new ExportGhnsDialog(m_repository); dialog->exec(); }); actionCollection()->addAction(QStringLiteral("export_ghns"), exportAction); exportAction->setIcon(QIcon::fromTheme(QStringLiteral("document-export"))); KStandardAction::quit(this, SLOT(quit()), actionCollection()); setupGUI(Keys | Save | Create, QStringLiteral("artikulateui_editor.rc")); } void MainWindowEditor::showSettingsDialog() { if (KConfigDialog::showDialog(QStringLiteral("settings"))) { return; } QPointer dialog = new KConfigDialog(nullptr, QStringLiteral("settings"), Settings::self()); ResourcesDialogPage *resourceDialog = new ResourcesDialogPage(m_repository); SoundDeviceDialogPage *soundDialog = new SoundDeviceDialogPage(); AppearenceDialogPage *appearenceDialog = new AppearenceDialogPage(); resourceDialog->loadSettings(); soundDialog->loadSettings(); appearenceDialog->loadSettings(); dialog->addPage(soundDialog, i18nc("@item:inmenu", "Sound Devices"), QStringLiteral("audio-headset"), i18nc("@title:tab", "Sound Device Settings"), true); dialog->addPage(appearenceDialog, i18nc("@item:inmenu", "Fonts"), QStringLiteral("preferences-desktop-font"), i18nc("@title:tab", "Training Phrase Font"), true); dialog->addPage(resourceDialog, i18nc("@item:inmenu", "Course Resources"), QStringLiteral("repository"), i18nc("@title:tab", "Resource Repository Settings"), true); connect(dialog.data(), &QDialog::accepted, resourceDialog, &ResourcesDialogPage::saveSettings); connect(dialog.data(), &QDialog::accepted, soundDialog, &SoundDeviceDialogPage::saveSettings); connect(dialog.data(), &QDialog::accepted, appearenceDialog, &AppearenceDialogPage::saveSettings); dialog->exec(); } void MainWindowEditor::save() { m_repository->sync(); } void MainWindowEditor::quit() { if (queryClose()) { qApp->quit(); } } bool MainWindowEditor::queryClose() { if (!m_repository->modified()) { return true; } int result = KMessageBox::warningYesNoCancel(nullptr, i18nc("@info", "The currently open course contains unsaved changes. Do you want to save them?")); switch(result) { case KMessageBox::Yes: m_repository->sync(); return true; case KMessageBox::No: return true; default: return false; } } diff --git a/src/ui/resourcesdialogpage.cpp b/src/ui/resourcesdialogpage.cpp index 07edd8e..93a0aa7 100644 --- a/src/ui/resourcesdialogpage.cpp +++ b/src/ui/resourcesdialogpage.cpp @@ -1,69 +1,69 @@ /* * 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 "resourcesdialogpage.h" #include "core/contributorrepository.h" #include "core/language.h" #include "settings.h" #include #include #include #include #include ResourcesDialogPage::ResourcesDialogPage(ContributorRepository *repository) : QWidget(nullptr) , m_repository(repository) , m_restartNeeded(false) { ui = new Ui::ResourcesDialogPage; ui->setupUi(this); connect(ui->buttonSelectCourseRepository, &QToolButton::clicked, this, [=](){ const QString dir = QFileDialog::getExistingDirectory(this, i18n("Open Repository Directory"), QString(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); ui->kcfg_CourseRepositoryPath->setText(dir); }); } ResourcesDialogPage::~ResourcesDialogPage() { delete ui; } void ResourcesDialogPage::loadSettings() { // setup Ui with stored settings ui->kcfg_CourseRepositoryPath->setText(Settings::courseRepositoryPath()); ui->kcfg_UseCourseRepository->setChecked(Settings::useCourseRepository()); } void ResourcesDialogPage::saveSettings() { // save settings Settings::setUseCourseRepository(ui->kcfg_UseCourseRepository->isChecked()); Settings::setCourseRepositoryPath(ui->kcfg_CourseRepositoryPath->text()); Settings::self()->save(); // reloading resources - m_repository->loadCourseResources(); + m_repository->reloadCourses(); }