diff --git a/CMakeLists.txt b/CMakeLists.txt index 044aa3e..ab1c3f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,87 +1,86 @@ ### # Copyright 2013-2016 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. ### project(artikulate) cmake_minimum_required(VERSION 3.0.0) find_package(ECM 5.15.0 REQUIRED NO_MODULE) find_package(KF5DocTools) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) include(KDEInstallDirs) include(ECMAddTests) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMInstallIcons) include(ECMOptionalAddSubdirectory) include(ECMSetupVersion) include(FeatureSummary) include(GenerateExportHeader) find_package(Qt5 5.10 REQUIRED COMPONENTS Widgets Sql XmlPatterns Qml Quick QuickWidgets Test ) find_package(KF5 5.57 REQUIRED COMPONENTS Archive Config Crash I18n NewStuff XmlGui Kirigami2 ) # options option(BUILD_QTMULTIMEDIA_PLUGIN "Build QtMultimedia sound backend" ON) option(BUILD_GSTREAMER_PLUGIN "Build GStreamer sound backend" OFF) add_definitions( -DQT_NO_URL_CAST_FROM_STRING ) # subdirectories to build ecm_optional_add_subdirectory(data) -ecm_optional_add_subdirectory(schemes) ecm_optional_add_subdirectory(doc) ecm_optional_add_subdirectory(src) ecm_optional_add_subdirectory(sounds) ecm_optional_add_subdirectory(images) ecm_optional_add_subdirectory(icons) ecm_optional_add_subdirectory(libsound) ecm_optional_add_subdirectory(liblearnerprofile) ecm_optional_add_subdirectory(autotests) # files to install in the artikulate project root directory install(PROGRAMS org.kde.artikulate.desktop DESTINATION ${XDG_APPS_INSTALL_DIR}) install(FILES org.kde.artikulate.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 5342f07..a80240e 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,78 +1,77 @@ ### # 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 ../schemes/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/schemes) # copy test files -file(COPY ../data/languages DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data) # copy test files -file(COPY testcourses/de.xml DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/courses) # copy test files -file(COPY testcourses/fr.xml DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/courses) # copy test files +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 set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) # repository tests -set(TestRepository_SRCS repository/test_repository.cpp) -add_executable(test_repository ${TestRepository_SRCS} ) -target_link_libraries(test_repository +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_repository test_repository) -ecm_mark_as_test(test_repository) +add_test(test_resourcerepository test_resourcerepository) +ecm_mark_as_test(test_resourcerepository) # training session tests set(TestTrainingSession_SRCS trainingsession/test_trainingsession.cpp) -add_executable(test_trainingsession ${TestTrainingSession_SRCS} ) +add_executable(test_trainingsession ${TestTrainingSession_SRCS}) target_link_libraries(test_trainingsession artikulatecore Qt5::Test ) -add_test(test_repository test_repository) -ecm_mark_as_test(test_repository) +add_test(test_trainingsession test_trainingsession) +ecm_mark_as_test(test_trainingsession) set(TestCourseFiles_SRCS testcoursefiles.cpp) kconfig_add_kcfg_files(TestCourseFiles_SRCS ../src/settings.kcfgc) add_executable(TestCourseFiles ${TestCourseFiles_SRCS} ) target_link_libraries(TestCourseFiles artikulatecore Qt5::Test ) add_test(TestCourseFiles TestCourseFiles) ecm_mark_as_test(TestCourseFiles) # 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/course/test_course.cpp b/autotests/course/test_course.cpp new file mode 100644 index 0000000..023411a --- /dev/null +++ b/autotests/course/test_course.cpp @@ -0,0 +1,136 @@ +/* + * 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_course.h" +#include "src/core/icourse.h" +#include "src/core/course.h" +#include "src/core/resources/courseresource.h" +#include "src/core/language.h" +#include "src/core/unit.h" +#include +#include +/* +class CourseStub : public ICourse +{ +public: + CourseStub(Language *language, QVector units) + : m_language(language) + , m_units(units) + { + } + QString id() const override + { + return "courseid"; + } + QString title() const override + { + return "title"; + } + QString i18nTitle() const override + { + return "i18n title"; + } + QString description() const override + { + return "description of the course"; + } + Language * language() const override + { + return m_language; + } + QList unitList() const override + { + return m_units.toList(); + } + QUrl file() const override + { + return QUrl(); + } + +private: + Language *m_language{nullptr}; + QVector m_units; +};*/ + +void TestCourse::init() +{ + // TODO initialization of test case +} + +void TestCourse::cleanup() +{ + // TODO cleanup after test run +} + +void TestCourse::createCourseWithoutUnits() +{ +// CourseResource resource(nullptr); + + Course course(); +//TODO +// virtual ~ICourse() = default; +// virtual QString id() const = 0; +// virtual QString title() const = 0; +// virtual QString i18nTitle() const = 0; +// virtual QString description() const = 0; +// virtual Language * language() const = 0; +// virtual QList unitList() const = 0; +// virtual QUrl file() const = 0; + +// Language language; +// CourseStub course(&language, QVector()); +// LearnerProfile::ProfileManager manager; +// TrainingSession session(&manager); +// session.setCourse(&course); +// QVERIFY(&course == session.course()); +} + +void TestCourse::createCourseWithoutPhrases() +{ +// Language language; +// Unit unitA; +// Unit unitB; +// Phrase phraseA1; +// Phrase phraseA2; +// Phrase phraseB1; +// Phrase phraseB2; +// // note: phrases without soundfiles are skipped in session generation +// phraseA1.setId("A1"); +// phraseA2.setId("A2"); +// phraseB1.setId("B1"); +// phraseB2.setId("B2"); +// phraseA1.setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); +// unitA.addPhrase(&phraseA1); +// unitA.addPhrase(&phraseA2); +// unitB.addPhrase(&phraseB1); +// unitB.addPhrase(&phraseB2); + +// CourseStub course(&language, QVector({&unitA, &unitB})); +// LearnerProfile::ProfileManager manager; +// TrainingSession session(&manager); +// session.setCourse(&course); + +// // test number of actions +// auto actions = session.trainingActions(); +// QCOMPARE(actions.count(), 1); +// QCOMPARE(actions.at(0)->actions().count(), 1); +} + +QTEST_GUILESS_MAIN(TestCourse) diff --git a/autotests/repository/test_repository.h b/autotests/course/test_course.h similarity index 75% rename from autotests/repository/test_repository.h rename to autotests/course/test_course.h index 991fea5..3ca82d1 100644 --- a/autotests/repository/test_repository.h +++ b/autotests/course/test_course.h @@ -1,47 +1,55 @@ /* * 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_REPOSITORY_H -#define TEST_REPOSITORY_H +#ifndef TEST_COURSE_H +#define TEST_COURSE_H #include -class TestRepository : public QObject +class TestCourse : public QObject { Q_OBJECT public: - TestRepository() = default; + TestCourse() = default; private Q_SLOTS: /** * Called before every test case. */ void init(); /** * Called after every test case. */ void cleanup(); - void createRepository(); + /** + * @brief Construct and destruct course without units + */ + void createCourseWithoutUnits(); + + /** + * @brief Construct training session and check that phrases without sound file paths are skipped + */ + void createCourseWithoutPhrases(); }; #endif diff --git a/autotests/resourcerepository/test_resourcerepository.cpp b/autotests/resourcerepository/test_resourcerepository.cpp new file mode 100644 index 0000000..abd89f5 --- /dev/null +++ b/autotests/resourcerepository/test_resourcerepository.cpp @@ -0,0 +1,86 @@ +/* + * 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_resourcerepository.h" +#include +#include +#include + +#include "src/core/resourcerepository.h" +#include "src/core/language.h" + +void TestResourceRepository::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 TestResourceRepository::cleanup() +{ + // TODO cleanup after test run +} + +void TestResourceRepository::createRepository() +{ + ResourceRepository repository(QUrl::fromLocalFile("data/courses/")); + QCOMPARE(repository.storageLocation(), "data/courses/"); + repository.reloadCourses(); + QCOMPARE(repository.courses().count(), 2); +} + +void TestResourceRepository::iResourceRepositoryCompatability() +{ + ResourceRepository repository(QUrl::fromLocalFile("data/courses/")); + IResourceRepository *interface = &repository; + + QCOMPARE(interface->storageLocation(), "data/courses/"); + QVERIFY(interface->languages().count() > 0); + QCOMPARE(interface->courses().count(), 0); + + // 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); + repository.reloadCourses(); + QCOMPARE(interface->courses().count(), 2); + QCOMPARE(spyAboutToBeAdded.count(), 2); + QCOMPARE(spyAdded.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(TestResourceRepository) diff --git a/autotests/repository/test_repository.cpp b/autotests/resourcerepository/test_resourcerepository.h similarity index 58% rename from autotests/repository/test_repository.cpp rename to autotests/resourcerepository/test_resourcerepository.h index 489d1a5..ca3ec07 100644 --- a/autotests/repository/test_repository.cpp +++ b/autotests/resourcerepository/test_resourcerepository.h @@ -1,41 +1,56 @@ /* * 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_repository.h" -#include +#ifndef TEST_RESOURCEREPOSITORY_H +#define TEST_RESOURCEREPOSITORY_H -#include "src/core/resourcemanager.h" +#include -void TestRepository::init() +class TestResourceRepository : public QObject { - // TODO initialization of test case -} + Q_OBJECT -void TestRepository::cleanup() -{ - // TODO cleanup after test run -} +public: + TestResourceRepository() = default; -void TestRepository::createRepository() -{ +private Q_SLOTS: + /** + * Called before every test case. + */ + void init(); + + /** + * Called after every test case. + */ + void cleanup(); + + /** + * Create and load repository with simple testdata + */ + void createRepository(); -} + /** + * @brief integration test for IResourceRepository interface + * Test expectations of the IResourceRepository interface. + */ + void iResourceRepositoryCompatability(); +}; -QTEST_GUILESS_MAIN(TestRepository) +#endif diff --git a/autotests/testcourses/de.xml b/autotests/testdata/courses/de.xml similarity index 100% rename from autotests/testcourses/de.xml rename to autotests/testdata/courses/de.xml diff --git a/autotests/testcourses/de_01.ogg b/autotests/testdata/courses/de_01.ogg similarity index 100% rename from autotests/testcourses/de_01.ogg rename to autotests/testdata/courses/de_01.ogg diff --git a/autotests/testcourses/de_02.ogg b/autotests/testdata/courses/de_02.ogg similarity index 100% rename from autotests/testcourses/de_02.ogg rename to autotests/testdata/courses/de_02.ogg diff --git a/autotests/testcourses/fr.xml b/autotests/testdata/courses/fr.xml similarity index 100% rename from autotests/testcourses/fr.xml rename to autotests/testdata/courses/fr.xml diff --git a/autotests/trainingsession/test_trainingsession.cpp b/autotests/trainingsession/test_trainingsession.cpp index b29345f..690b71f 100644 --- a/autotests/trainingsession/test_trainingsession.cpp +++ b/autotests/trainingsession/test_trainingsession.cpp @@ -1,248 +1,248 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_trainingsession.h" #include "src/core/trainingsession.h" #include "src/core/icourse.h" #include "src/core/language.h" #include "src/core/unit.h" #include "src/core/trainingaction.h" #include "liblearnerprofile/src/profilemanager.h" #include #include // assumption: during a training session the units and phrases of a course do not change // any change of such a course shall result in a reload of a training session class CourseStub : public ICourse { public: CourseStub(Language *language, QVector units) : m_language(language) , m_units(units) { } QString id() const override { return "courseid"; } QString title() const override { return "title"; } QString i18nTitle() const override { return "i18n title"; } QString description() const override { return "description of the course"; } Language * language() const override { return m_language; } - QList unitList() const override + QList unitList() override { return m_units.toList(); } QUrl file() const override { return QUrl(); } private: Language *m_language{nullptr}; QVector m_units; }; void TestTrainingSession::init() { // TODO initialization of test case } void TestTrainingSession::cleanup() { // TODO cleanup after test run } void TestTrainingSession::createTrainingSessionWithoutUnits() { Language language; CourseStub course(&language, QVector()); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::createTrainingSessionWithEmptySounds() { Language language; Unit unitA; Unit unitB; Phrase phraseA1; Phrase phraseA2; Phrase phraseB1; Phrase phraseB2; // note: phrases without soundfiles are skipped in session generation phraseA1.setId("A1"); phraseA2.setId("A2"); phraseB1.setId("B1"); phraseB2.setId("B2"); phraseA1.setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); unitA.addPhrase(&phraseA1); unitA.addPhrase(&phraseA2); unitB.addPhrase(&phraseB1); unitB.addPhrase(&phraseB2); CourseStub course(&language, QVector({&unitA, &unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); // test number of actions auto actions = session.trainingActions(); QCOMPARE(actions.count(), 1); QCOMPARE(actions.at(0)->actions().count(), 1); } void TestTrainingSession::createTrainingSessionWithEmptyUnits() { Language language; Unit firstUnit; Unit secondUnit; CourseStub course(&language, QVector({&firstUnit, &secondUnit})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::createTrainingSessionWithUnitsAndPhrases() { Language language; Unit unit; Phrase firstPhrase; Phrase secondPhrase; unit.addPhrase(&firstPhrase); unit.addPhrase(&secondPhrase); CourseStub course(&language, QVector({&unit})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); QVERIFY(&course == session.course()); } void TestTrainingSession::iterateCourse() { Language language; Unit unitA; Unit unitB; Phrase phraseA1; Phrase phraseA2; Phrase phraseB1; Phrase phraseB2; // note: phrases without soundfiles are skipped in session generation phraseA1.setId("A1"); phraseA2.setId("A2"); phraseB1.setId("B1"); phraseB2.setId("B2"); phraseA1.setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseA2.setSound(QUrl::fromLocalFile("/tmp/a1.ogg")); phraseB1.setSound(QUrl::fromLocalFile("/tmp/b1.ogg")); phraseB2.setSound(QUrl::fromLocalFile("/tmp/b2.ogg")); unitA.addPhrase(&phraseA1); unitA.addPhrase(&phraseA2); unitB.addPhrase(&phraseB1); unitB.addPhrase(&phraseB2); CourseStub course(&language, QVector({&unitA, &unitB})); LearnerProfile::ProfileManager manager; TrainingSession session(&manager); session.setCourse(&course); // session assumed to initialize with first units's first phrase QCOMPARE(session.activeUnit(), &unitA); QCOMPARE(session.activePhrase(), &phraseA1); QVERIFY(&course == session.course()); // test direct unit setters session.setUnit(&unitA); QCOMPARE(session.activeUnit(), &unitA); session.setUnit(&unitB); QCOMPARE(session.activeUnit(), &unitB); // test direct phrase setters session.setPhrase(&phraseA1); QCOMPARE(session.activePhrase(), &phraseA1); QCOMPARE(session.activeUnit(), &unitA); session.setPhrase(&phraseB1); QCOMPARE(session.activePhrase(), &phraseB1); QCOMPARE(session.activeUnit(), &unitB); // test number of actions auto actions = session.trainingActions(); QCOMPARE(actions.count(), 2); QCOMPARE(actions.at(0)->actions().count(), 2); QCOMPARE(actions.at(1)->actions().count(), 2); // test phrase iterators: accept iterator session.setPhrase(&phraseA1); QCOMPARE(session.activeUnit(), &unitA); QCOMPARE(session.activePhrase(), &phraseA1); QVERIFY(session.hasNext()); session.accept(); QCOMPARE(session.activeUnit(), &unitA); QCOMPARE(session.activePhrase(), &phraseA2); session.accept(); QCOMPARE(session.activePhrase(), &phraseB1); session.accept(); QCOMPARE(session.activePhrase(), &phraseB2); QVERIFY(!session.hasNext()); // test phrase iterators: skip iterator session.setPhrase(&phraseA1); QCOMPARE(session.activeUnit(), &unitA); QCOMPARE(session.activePhrase(), &phraseA1); QVERIFY(!session.hasPrevious()); QVERIFY(session.hasNext()); session.skip(); QCOMPARE(session.activeUnit(), &unitA); QCOMPARE(session.activePhrase(), &phraseA2); session.skip(); QCOMPARE(session.activePhrase(), &phraseB1); session.skip(); QCOMPARE(session.activePhrase(), &phraseB2); QVERIFY(session.hasPrevious()); QVERIFY(!session.hasNext()); // test completed signal QSignalSpy spy(&session, SIGNAL(completed())); session.setPhrase(&phraseB1); session.accept(); QCOMPARE(spy.count(), 0); session.accept(); QCOMPARE(spy.count(), 1); } QTEST_GUILESS_MAIN(TestTrainingSession) diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt deleted file mode 100644 index c4673a8..0000000 --- a/data/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -### -# Copyright 2013 Andreas Cord-Landwehr -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -### - -ecm_optional_add_subdirectory(languages) - diff --git a/data/languages.qrc b/data/languages.qrc new file mode 100644 index 0000000..33f3d33 --- /dev/null +++ b/data/languages.qrc @@ -0,0 +1,20 @@ + + + languages/ba.xml + languages/ca.xml + languages/de.xml + languages/du.xml + languages/en_BE.xml + languages/en_US.xml + languages/fr.xml + languages/gr.xml + languages/hi.xml + languages/it.xml + languages/mr.xml + languages/sp.xml + languages/pl.xml + schemes/course.xsd + schemes/language.xsd + schemes/skeleton.xsd + + diff --git a/data/languages/CMakeLists.txt b/data/languages/CMakeLists.txt deleted file mode 100644 index 0568079..0000000 --- a/data/languages/CMakeLists.txt +++ /dev/null @@ -1,43 +0,0 @@ -### -# Copyright 2013 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. -### - -set(languages - ba.xml - ca.xml - de.xml - du.xml - en_BE.xml - en_US.xml - fr.xml - gr.xml - hi.xml - it.xml - mr.xml - sp.xml - pl.xml -) - -install(FILES ${languages} DESTINATION ${DATA_INSTALL_DIR}/artikulate/languages) - diff --git a/schemes/course.xsd b/data/schemes/course.xsd similarity index 100% rename from schemes/course.xsd rename to data/schemes/course.xsd diff --git a/schemes/language.xsd b/data/schemes/language.xsd similarity index 100% rename from schemes/language.xsd rename to data/schemes/language.xsd diff --git a/schemes/skeleton.xsd b/data/schemes/skeleton.xsd similarity index 100% rename from schemes/skeleton.xsd rename to data/schemes/skeleton.xsd diff --git a/schemes/CMakeLists.txt b/schemes/CMakeLists.txt deleted file mode 100644 index 509db66..0000000 --- a/schemes/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -### -# Copyright 2013 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. -### - -FILE(GLOB schemes *.xsd) -install(FILES ${schemes} DESTINATION ${DATA_INSTALL_DIR}/artikulate/schemes) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ce8297b..60d4c84 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,198 +1,201 @@ ### # Copyright 2013-2015 Andreas Cord-Landwehr # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ### ecm_setup_version(0.99.90 VARIABLE_PREFIX ARTIKULATE VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/ArtikulateConfigVersion.cmake" ) ecm_optional_add_subdirectory(qml) # set include directories include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${artikulate_SOURCE_DIR} ) # set the source code files from which Artikulate is compiled set(artikulateCore_SRCS core/icourse.h core/drawertrainingactions.cpp core/course.cpp core/resourcemanager.cpp + core/iresourcerepository.h + core/resourcerepository.cpp core/language.cpp core/phrase.cpp core/phoneme.cpp core/phonemegroup.cpp core/unit.cpp core/skeleton.cpp core/editorsession.cpp core/trainingaction.cpp core/trainingactionicon.cpp core/trainingsession.cpp core/resources/resourceinterface.cpp core/resources/languageresource.cpp core/resources/courseresource.cpp core/resources/skeletonresource.cpp core/player.cpp core/recorder.cpp qmlcontrols/iconitem.cpp qmlcontrols/imagetexturescache.cpp qmlcontrols/managedtexturenode.cpp artikulate_debug.cpp ) kconfig_add_kcfg_files (artikulateCore_SRCS settings.kcfgc) add_library(artikulatecore SHARED ${artikulateCore_SRCS}) generate_export_header(artikulatecore BASE_NAME artikulatecore) target_link_libraries(artikulatecore LINK_PUBLIC artikulatelearnerprofile artikulatesound Qt5::XmlPatterns Qt5::Quick KF5::Archive KF5::ConfigGui ) # internal library without any API or ABI guarantee set(GENERIC_LIB_VERSION "0") set(GENERIC_LIB_SOVERSION "0") set_target_properties( artikulatecore PROPERTIES VERSION ${GENERIC_LIB_VERSION} SOVERSION ${GENERIC_LIB_SOVERSION} ) install( TARGETS artikulatecore DESTINATION ${INSTALL_TARGETS_DEFAULT_ARGS} ) install(FILES artikulate.knsrc DESTINATION ${CONFIG_INSTALL_DIR}) # set the source code files from which Artikulate is compiled set(artikulate_SRCS main.cpp mainwindow.cpp application.cpp artikulate_debug.cpp models/coursemodel.cpp models/coursefiltermodel.cpp models/languagemodel.cpp models/languageresourcemodel.cpp # models/learningprogressmodel.cpp //TODO must be adapted to new trainingsession models/unitmodel.cpp models/unitfiltermodel.cpp models/phrasemodel.cpp models/phraselistmodel.cpp models/phrasefiltermodel.cpp models/phonememodel.cpp models/phonemegroupmodel.cpp models/phonemeunitmodel.cpp models/profilemodel.cpp models/skeletonmodel.cpp ui/sounddevicedialogpage.cpp ui/appearencedialogpage.cpp ui/resourcesdialogpage.cpp ) ki18n_wrap_ui (artikulate_SRCS ui/resourcesdialogpage.ui ui/sounddevicedialogpage.ui ui/appearencedialogpage.ui ) qt5_add_resources(artikulate_SRCS resources.qrc) +qt5_add_resources(artikulate_SRCS ../data/languages.qrc) kconfig_add_kcfg_files (artikulate_SRCS settings.kcfgc) set(artikulate_editor_SRCS main_editor.cpp mainwindow_editor.cpp application.cpp artikulate_debug.cpp models/coursemodel.cpp models/coursefiltermodel.cpp models/languagemodel.cpp models/languageresourcemodel.cpp # models/learningprogressmodel.cpp //TODO must be adapted to new trainingsession models/unitmodel.cpp models/unitfiltermodel.cpp models/phrasemodel.cpp models/phraselistmodel.cpp models/phrasefiltermodel.cpp models/phonememodel.cpp models/phonemegroupmodel.cpp models/phonemeunitmodel.cpp models/profilemodel.cpp models/skeletonmodel.cpp ui/sounddevicedialogpage.cpp ui/appearencedialogpage.cpp ui/resourcesdialogpage.cpp ui/exportghnsdialog.cpp ) ki18n_wrap_ui(artikulate_editor_SRCS ui/appearencedialogpage.ui ui/exportghnsdialog.ui ui/resourcesdialogpage.ui ui/sounddevicedialogpage.ui ) qt5_add_resources(artikulate_editor_SRCS resources.qrc) kconfig_add_kcfg_files (artikulate_editor_SRCS settings.kcfgc) # executables add_executable(artikulate ${artikulate_SRCS}) target_link_libraries(artikulate LINK_PUBLIC artikulatelearnerprofile artikulatesound artikulatecore Qt5::Qml Qt5::Quick KF5::Crash KF5::NewStuff KF5::XmlGui ) qt5_add_resources(artikulate_editor_SRCS editor.qrc) add_executable(artikulate_editor ${artikulate_editor_SRCS}) target_link_libraries(artikulate_editor LINK_PUBLIC artikulatesound artikulatecore Qt5::Qml Qt5::Quick Qt5::QuickWidgets KF5::Crash KF5::NewStuff KF5::XmlGui ) install(FILES artikulate.kcfg DESTINATION ${KCFG_INSTALL_DIR}) install(TARGETS artikulate ${INSTALL_TARGETS_DEFAULT_ARGS}) install(TARGETS artikulate_editor ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/application.cpp b/src/application.cpp index b9254be..4c1522a 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -1,122 +1,138 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "application.h" #include "core/course.h" +#include "core/iresourcerepository.h" #include "core/drawertrainingactions.h" #include "core/trainingaction.h" #include "core/editorsession.h" #include "core/language.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/phrase.h" #include "core/player.h" #include "core/recorder.h" #include "core/resourcemanager.h" #include "core/skeleton.h" #include "core/trainingsession.h" #include "core/unit.h" #include "models/coursefiltermodel.h" #include "models/coursemodel.h" #include "models/languagemodel.h" #include "models/languageresourcemodel.h" #include "models/learningprogressmodel.h" #include "models/phonemegroupmodel.h" #include "models/phonememodel.h" #include "models/phonemeunitmodel.h" #include "models/phrasefiltermodel.h" #include "models/phraselistmodel.h" #include "models/phrasemodel.h" #include "models/profilemodel.h" #include "models/skeletonmodel.h" #include "models/unitfiltermodel.h" #include "models/unitmodel.h" #include "qmlcontrols/iconitem.h" #include "liblearnerprofile/src/learner.h" #include "liblearnerprofile/src/profilemanager.h" #include "liblearnerprofile/src/learninggoal.h" #include "liblearnerprofile/src/models/learninggoalmodel.h" #include #include #include #include Application::Application(int& argc, char** argv) : QApplication(argc, argv) { registerQmlTypes(); } +IResourceRepository * Application::resourceRepository() const +{ + return m_resourceRepository; +} + +void Application::installResourceRepository(IResourceRepository *resourceRepository) +{ + m_resourceRepository = resourceRepository; +} + void Application::registerQmlTypes() { qmlRegisterUncreatableType( "artikulate", 1, 0, "TrainingSession", QStringLiteral("TrainingSession is unique object provided by the backend")); qmlRegisterUncreatableType( "artikulate", 1, 0, "EditorSession", QStringLiteral("EditorSession is unique object provided by the backend")); qmlRegisterUncreatableType( "artikulate", 1, 0, "ResourceManager", QStringLiteral("ResourceManager is unique object provided by the backend")); qmlRegisterUncreatableType( "artikulate", 1, 0, "ProfileManager", QStringLiteral("ProfileManager is unique object provided by the backend")); - qmlRegisterUncreatableType("artikulate", 1, 0, "ICourse", "Courses are managed by repository"); + // interfaces + qmlRegisterInterface("IResourceRepository"); + qmlRegisterInterface("ICourse"); + + // concrete instantiable types qmlRegisterType("artikulate", 1, 0, "Learner"); qmlRegisterType("artikulate", 1, 0, "LearningGoal"); qmlRegisterType("artikulate", 1, 0, "Unit"); qmlRegisterType("artikulate", 1, 0, "Skeleton"); qmlRegisterType("artikulate", 1, 0, "Course"); qmlRegisterType("artikulate", 1, 0, "Language"); qmlRegisterType("artikulate", 1, 0, "ResourceManager"); qmlRegisterType("artikulate", 1, 0, "Phrase"); qmlRegisterType("artikulate", 1, 0, "Phoneme"); qmlRegisterType("artikulate", 1, 0, "PhonemeGroup"); qmlRegisterType("artikulate", 1, 0, "Player"); qmlRegisterType("artikulate", 1, 0, "Recorder"); qmlRegisterType("artikulate", 1, 0, "Icon"); qmlRegisterType("artikulate", 1, 0, "DrawerTrainingActions"); qmlRegisterType("artikulate", 1, 0, "TrainingAction"); + // models qmlRegisterType("artikulate", 1, 0, "CourseModel"); qmlRegisterType("artikulate", 1, 0, "CourseFilterModel"); qmlRegisterType("artikulate", 1, 0, "LanguageModel"); qmlRegisterType("artikulate", 1, 0, "LanguageResourceModel"); // qmlRegisterType("artikulate", 1, 0, "LearningProgressModel");//TODO must be ported to new trainingsession qmlRegisterType("artikulate", 1, 0, "UnitModel"); qmlRegisterType("artikulate", 1, 0, "UnitFilterModel"); qmlRegisterType("artikulate", 1, 0, "PhraseModel"); qmlRegisterType("artikulate", 1, 0, "PhraseListModel"); qmlRegisterType("artikulate", 1, 0, "PhraseFilterModel"); qmlRegisterType("artikulate", 1, 0, "PhonemeModel"); qmlRegisterType("artikulate", 1, 0, "PhonemeGroupModel"); qmlRegisterType("artikulate", 1, 0, "PhonemeUnitModel"); qmlRegisterType("artikulate", 1, 0, "ProfileModel"); qmlRegisterType("artikulate", 1, 0, "SkeletonModel"); qmlRegisterType("artikulate", 1, 0, "LearningGoalModel"); } diff --git a/src/application.h b/src/application.h index 558d88c..de18d5f 100644 --- a/src/application.h +++ b/src/application.h @@ -1,38 +1,59 @@ /* * 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 APPLICATION_H #define APPLICATION_H #include +class IResourceRepository; +class Application; + +#if defined(artikulateApp) +#undef artikulateApp +#endif +#define artikulateApp (static_cast(QCoreApplication::instance())) + class Application : public QApplication { Q_OBJECT public: explicit Application(int &argc, char **argv); + /** + * @brief install global course data repository to application + * @param repository the concrete resource repository to install + */ + void installResourceRepository(IResourceRepository *repository); + + /** + * @brief getter for global resource repository + * @return the repository + */ + IResourceRepository * resourceRepository() const; + private: void registerQmlTypes(); + IResourceRepository *m_resourceRepository{ nullptr }; }; #endif diff --git a/src/artikulate_debug.cpp b/src/artikulate_debug.cpp index 3a93ba5..8ab69db 100644 --- a/src/artikulate_debug.cpp +++ b/src/artikulate_debug.cpp @@ -1,19 +1,20 @@ /* * Copyright 2015 Andreas Cord-Landwehr * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "artikulate_debug.h" Q_LOGGING_CATEGORY(ARTIKULATE_LOG, "log_artikulate") +Q_LOGGING_CATEGORY(ARTIKULATE_CORE, "articulate.core") diff --git a/src/artikulate_debug.h b/src/artikulate_debug.h index 9237494..ce780d2 100644 --- a/src/artikulate_debug.h +++ b/src/artikulate_debug.h @@ -1,24 +1,25 @@ /* * Copyright 2015 Andreas Cord-Landwehr * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef ARTIKULATE_DEBUG_H #define ARTIKULATE_DEBUG_H #include Q_DECLARE_LOGGING_CATEGORY(ARTIKULATE_LOG) +Q_DECLARE_LOGGING_CATEGORY(ARTIKULATE_CORE) #endif diff --git a/src/core/course.cpp b/src/core/course.cpp index 0026faf..def6416 100644 --- a/src/core/course.cpp +++ b/src/core/course.cpp @@ -1,340 +1,340 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "course.h" #include "unit.h" #include "language.h" #include "resources/resourceinterface.h" #include "resources/courseresource.h" #include "resourcemanager.h" #include "phonemegroup.h" #include "artikulate_debug.h" #include #include #include #include Course::Course(ResourceInterface *resource) : ICourse(resource) , m_resource(qobject_cast(resource)) , m_language(nullptr) , m_modified(false) { } Course::~Course() { foreach (Unit *unit, m_unitList) { unit->deleteLater(); } m_unitList.clear(); // clear phonom units QMultiMap< PhonemeGroup*, QList< QPair > >::iterator groupIter = m_phonemeUnitList.begin(); while (groupIter != m_phonemeUnitList.end()) { QList< QPair >::iterator itemIter = groupIter->begin(); while (itemIter != groupIter->end()) { itemIter->first->deleteLater(); // delete phoneme itemIter->second->deleteLater(); // delete unit ++itemIter; } groupIter->clear(); ++groupIter; } m_phonemeUnitList.clear(); m_phonemeGroupList.clear(); } QString Course::id() const { return m_id; } void Course::setId(const QString &id) { if (id != m_id) { m_id = id; emit idChanged(); setModified(); } } QString Course::foreignId() const { return m_foreignId; } void Course::setForeignId(const QString &id) { m_foreignId = id; } QString Course::title() const { return m_title; } QString Course::i18nTitle() const { return m_resource->i18nTitle(); } void Course::setTitle(const QString &title) { if (QString::compare(title, m_title) != 0) { m_title = title; emit titleChanged(); setModified(); } } QString Course::description() const { return m_description; } void Course::setDescription(const QString &description) { m_description = description; emit descriptionChanged(); } Language * Course::language() const { return m_language; } void Course::setLanguage(Language *language) { Q_ASSERT(language); // TODO this should happen in the ctor foreach (PhonemeGroup *group, language->phonemeGroups()) { addPhonemeGroup(group); } m_language = language; emit languageChanged(); } QUrl Course::file() const { return m_file; } void Course::setFile(const QUrl &file) { m_file = file; } -QList< Unit* > Course::unitList() const +QList< Unit* > Course::unitList() { return m_unitList; } void Course::addUnit(Unit *unit) { QList::ConstIterator iter = m_unitList.constBegin(); while (iter != m_unitList.constEnd()) { if (unit->id() == (*iter)->id()) { qCWarning(ARTIKULATE_LOG) << "Unit already contained in this course, aborting"; return; } ++iter; } emit unitAboutToBeAdded(unit, m_unitList.length()); m_unitList.append(unit); connect(unit, &Unit::modified, this, [=]() { setModified(true); }); // these connections are only present for "normal units" and take care to register // there phrases also at phoneme units // TODO: removing of phrase and upading of phonemes for that case is not implemented connect(unit, &Unit::phraseAdded, this, &Course::registerPhrasePhonemes); emit unitAdded(); setModified(true); } Unit * Course::createUnit() { // find first unused id QStringList unitIds; foreach (Unit *unit, m_unitList) { unitIds.append(unit->id()); } QString id = QUuid::createUuid().toString(); while (unitIds.contains(id)) { id = QUuid::createUuid().toString(); qCWarning(ARTIKULATE_LOG) << "Unit id generator has found a collision, recreating id."; } // create unit Unit *unit = new Unit(this); unit->setCourse(this); unit->setId(id); unit->setTitle(i18n("New Unit")); addUnit(unit); return unit; } Phrase * Course::createPhrase(Unit *unit) { // find globally unique phrase id inside course QStringList phraseIds; foreach (Unit *unit, m_unitList) { foreach (Phrase *phrase, unit->phraseList()) { phraseIds.append(phrase->id()); } } QString id = QUuid::createUuid().toString(); while (phraseIds.contains(id)) { id = QUuid::createUuid().toString(); qCWarning(ARTIKULATE_LOG) << "Phrase id generator has found a collision, recreating id."; } // create unit Phrase *phrase = new Phrase(this); phrase->setId(id); phrase->setText(QLatin1String("")); phrase->setType(Phrase::Word); unit->addPhrase(phrase); return phrase; } QList< Unit* > Course::phonemeUnitList(PhonemeGroup *phonemeGroup) const { QList list; for (const auto &group : m_phonemeUnitList.value(phonemeGroup)) { list.append(group.second); } return list; } Unit * Course::phonemeUnit(Phoneme *phoneme) const { for (auto group = m_phonemeUnitList.keyBegin(); group != m_phonemeUnitList.keyEnd(); ++group) { m_phonemeUnitList.value(*group); for (const auto &phonemeUnit : m_phonemeUnitList.value(*group)) { if (phonemeUnit.first == phoneme) { return phonemeUnit.second; } } } return nullptr; } PhonemeGroup * Course::phonemeGroup(Unit *unit) const { for (auto group = m_phonemeUnitList.keyBegin(); group != m_phonemeUnitList.keyEnd(); ++group) { m_phonemeUnitList.value(*group); for (const auto &phonemeUnit : m_phonemeUnitList.value(*group)) { if (phonemeUnit.second == unit) { return *group; } } } return nullptr; } void Course::addPhonemeGroup(PhonemeGroup *phonemeGroup) { if (m_phonemeUnitList.contains(phonemeGroup)) { qCWarning(ARTIKULATE_LOG) << "Phoneme group already contained in this course, aborting"; return; } emit phonemeGroupAboutToBeAdded(phonemeGroup, m_phonemeGroupList.count()); // add to phoneme list m_phonemeGroupList.append(phonemeGroup); m_phonemeUnitList.insert(phonemeGroup, QList< QPair >()); emit phonemeGroupAdded(); setModified(); } QList Course::phonemeGroupList() const { return m_phonemeGroupList; } bool Course::modified() const { return m_modified; } void Course::setModified(bool modified) { if (m_modified == modified) { return; } m_modified = modified; emit modifiedChanged(); } void Course::sync() { if (!m_file.isValid() || m_file.isEmpty() || m_resource == nullptr) { qCritical() << "Path" << m_file.toLocalFile() << "not valid, aborting sync operation."; return; } m_resource->sync(); setModified(false); } -bool Course::isContributorResource() const -{ - return m_resource->isContributorResource(); -} +//bool Course::isContributorResource() const +//{ +// return m_resource->isContributorResource(); +//} void Course::registerPhrasePhonemes(Phrase *phrase) { // iterate over all phonemes of this phrase foreach (Phoneme *phoneme, phrase->phonemes()) { // try to find corresponding phonem groups (phonem groups are registered on course creation) foreach (PhonemeGroup *group, m_phonemeGroupList) { if (!group->contains(phoneme)) { continue; } // either add phrase to existing unit or register a new one bool phraseRegistered = false; for (const auto &phonemeUnit : m_phonemeUnitList.value(group)) { if (phonemeUnit.first->id() == phoneme->id()) { phonemeUnit.second->addPhrase(phrase); phraseRegistered = true; } } // otherwise, need to create a new unit if (phraseRegistered == false) { // create unit based on the phoneme group Unit *unit = new Unit(this); unit->setId(phoneme->id()); unit->setTitle(phoneme->title()); unit->setCourse(this); m_phonemeUnitList[group].append(qMakePair(phoneme, unit)); unit->addPhrase(phrase); } } } } diff --git a/src/core/course.h b/src/core/course.h index ce75ec3..4d9e1b7 100644 --- a/src/core/course.h +++ b/src/core/course.h @@ -1,140 +1,140 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef COURSE_H #define COURSE_H #include "artikulatecore_export.h" #include "icourse.h" #include #include #include class ResourceInterface; class CourseResource; class QString; class Language; class Unit; class Phrase; class PhonemeGroup; class Phoneme; class ARTIKULATECORE_EXPORT Course : public ICourse { Q_OBJECT Q_INTERFACES(ICourse) Q_PROPERTY(QString id READ id WRITE setId NOTIFY idChanged) Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) Q_PROPERTY(QString i18nTitle READ i18nTitle NOTIFY titleChanged) Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged) Q_PROPERTY(bool modified READ modified WRITE setModified NOTIFY modifiedChanged) Q_PROPERTY(Language * language READ language NOTIFY languageChanged) public: explicit Course(ResourceInterface *resource = nullptr); ~Course() override; QString id() const override; void setId(const QString &id); QString foreignId() const; void setForeignId(const QString &id); QString title() const override; QString i18nTitle() const override; void setTitle(const QString &title); Language * language() const override; void setLanguage(Language *language); QString description() const override; void setDescription(const QString &description); QUrl file() const override; void setFile(const QUrl &file); - QList unitList() const override; + QList unitList() override; QList phonemeUnitList(PhonemeGroup *phonemeGroup) const; /** * \return the corresponding unit for phoneme \p phoneme */ Unit * phonemeUnit(Phoneme *phoneme) const; /** * \return the phoneme group containing the phoneme corresponding to \p unit */ PhonemeGroup * phonemeGroup(Unit *unit) const; void addUnit(Unit *unit); QList phonemeGroupList() const; void addPhonemeGroup(PhonemeGroup *phonemeGroup); /** * Create and add a new unit to course. * * \return pointer to the created unit */ Q_INVOKABLE Unit * createUnit(); /** * Create and add a new phrase and add it to the specified unit. The type of the created phrase * is initially Phrase::Word. * * \param unit the unit to that the created hprase shall be added * \return pointer to the created phrase */ Q_INVOKABLE Phrase * createPhrase(Unit *unit); /** * \return true if the course was modified after the last sync, otherwise false */ virtual bool modified() const; /** * Writes course object back to file and set \ref modified state to false. * If no file is set, no operation is performed. */ virtual Q_INVOKABLE void sync(); bool isContributorResource() const; public Q_SLOTS: void setModified(bool modified = true); void registerPhrasePhonemes(Phrase *phrase); Q_SIGNALS: void modifiedChanged(); void unitAdded(); void unitAboutToBeAdded(Unit*,int); void unitsRemoved(); void unitsAboutToBeRemoved(int,int); void phonemeGroupAdded(); void phonemeGroupAboutToBeAdded(PhonemeGroup*,int); void phonemeGroupRemoved(); void phonemeGroupAboutToBeRemoved(int,int); private: Q_DISABLE_COPY(Course) CourseResource * const m_resource; QString m_id; QString m_foreignId; QString m_title; QString m_description; Language *m_language; QUrl m_file; bool m_modified; QList m_unitList; QList m_phonemeGroupList; QMap>> m_phonemeUnitList; }; #endif // COURSE_H diff --git a/src/core/icourse.h b/src/core/icourse.h index a26dc47..85981e9 100644 --- a/src/core/icourse.h +++ b/src/core/icourse.h @@ -1,68 +1,73 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef ICOURSE_H #define ICOURSE_H #include "artikulatecore_export.h" #include #include #include class QString; class Language; class Unit; class ARTIKULATECORE_EXPORT ICourse : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id NOTIFY idChanged) Q_PROPERTY(QString title READ title NOTIFY titleChanged) Q_PROPERTY(QString i18nTitle NOTIFY titleChanged) Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) Q_PROPERTY(Language * language NOTIFY languageChanged) public: ICourse(QObject *parent = nullptr) : QObject(parent) { } virtual ~ICourse() = default; virtual QString id() const = 0; virtual QString title() const = 0; virtual QString i18nTitle() const = 0; virtual QString description() const = 0; virtual Language * language() const = 0; - virtual QList unitList() const = 0; + /** + * @brief Lazy loading unit list + * @return list of units in course + */ + virtual QList unitList() = 0; virtual QUrl file() const = 0; Q_SIGNALS: void idChanged(); void titleChanged(); void descriptionChanged(); void languageChanged(); void unitAdded(); void unitAboutToBeAdded(Unit*,int); void unitsRemoved(); void unitsAboutToBeRemoved(int,int); }; + Q_DECLARE_INTERFACE(ICourse, "com.kde.artikulate.ICourse/1.0") #endif // COURSE_H diff --git a/src/core/iresourcerepository.h b/src/core/iresourcerepository.h new file mode 100644 index 0000000..0ef8332 --- /dev/null +++ b/src/core/iresourcerepository.h @@ -0,0 +1,77 @@ +/* + * 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 IRESOURCEREPOSITORY_H +#define IRESOURCEREPOSITORY_H + +#include "artikulatecore_export.h" +#include + +class ICourse; +class Language; + +/** + * \class IResourceRepository + * This interface provides a generic interface that provides just the methods and signals needed + * to integrade a repository into the training part of Artikulate. + */ +class ARTIKULATECORE_EXPORT IResourceRepository : public QObject +{ + Q_OBJECT +public: + virtual ~IResourceRepository() = default; + + /** + * \return path to working repository, if one is set + */ + virtual QString storageLocation() const = 0; + + /** + * @return list of all loaded courses + */ + virtual QVector courses() const = 0; + + /** + * @param language to use for filtering + * @return list of all loaded courses filtered by the named language + */ + virtual QVector courses(Language *language) const = 0; + + /** + * @brief Requests a refresh of all resources + * + * Typical reasons to call this are GHNS signals + */ + virtual void reloadCourses() = 0; + + /** + * @return list of all loaded languages + */ + virtual QVector languages() const = 0; + +Q_SIGNALS: + virtual void courseAboutToBeAdded(ICourse*,int) = 0; + virtual void courseAdded() = 0; + virtual void courseAboutToBeRemoved(int) = 0; + virtual void courseRemoved() = 0; +}; +Q_DECLARE_INTERFACE(IResourceRepository, "IResourceRepository") + +#endif diff --git a/src/core/resourcemanager.cpp b/src/core/resourcemanager.cpp index 406d651..013e885 100644 --- a/src/core/resourcemanager.cpp +++ b/src/core/resourcemanager.cpp @@ -1,474 +1,474 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "resourcemanager.h" #include "language.h" #include "course.h" #include "skeleton.h" #include "unit.h" #include "phrase.h" #include "phoneme.h" #include "phonemegroup.h" #include "resources/languageresource.h" #include "resources/courseresource.h" #include "resources/skeletonresource.h" #include "settings.h" #include "liblearnerprofile/src/profilemanager.h" #include "liblearnerprofile/src/learninggoal.h" #include #include #include #include #include #include #include #include #include #include "artikulate_debug.h" #include #include ResourceManager::ResourceManager(QObject *parent) : QObject(parent) { } void ResourceManager::loadCourseResources() { //TODO fix this method such that it may be called many times of e.g. updating // reload config, could be changed in dialogs Settings::self()->load(); // register skeleton resources QDir skeletonRepository = QDir(Settings::courseRepositoryPath()); skeletonRepository.setFilter(QDir::Files | QDir::Hidden); if (!skeletonRepository.cd(QStringLiteral("skeletons"))) { qCritical() << "There is no subdirectory \"skeletons\" in directory " << skeletonRepository.path() << " cannot load skeletons."; } else { // read skeletons QFileInfoList list = skeletonRepository.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); addSkeleton(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); } } // register contributor course files QDir courseRepository = QDir(Settings::courseRepositoryPath()); if (!courseRepository.cd(QStringLiteral("courses"))) { qCritical() << "There is no subdirectory \"courses\" in directory " << courseRepository.path() << " cannot load courses."; } else { // find courses courseRepository.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QFileInfoList courseDirList = courseRepository.entryInfoList(); // traverse all course directories foreach (const QFileInfo &info, courseDirList) { QDir courseDir = QDir(info.absoluteFilePath()); courseDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QFileInfoList courseLangDirList = courseDir.entryInfoList(); // traverse all language directories for each course foreach (const QFileInfo &langInfo, courseLangDirList) { QDir courseLangDir = QDir(langInfo.absoluteFilePath()); courseLangDir.setFilter(QDir::Files); QStringList nameFilters; nameFilters.append(QStringLiteral("*.xml")); QFileInfoList courses = courseLangDir.entryInfoList(nameFilters); // find and add course files foreach (const QFileInfo &courseInfo, courses) { CourseResource * course = addCourse(QUrl::fromLocalFile(courseInfo.filePath())); if (course != nullptr) { - course->setContributorResource(true); +// course->setContributorResource(true); } } } } } // register GHNS course resources QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::DataLocation); foreach (const QString &testdir, dirs) { QDirIterator it(testdir + "/courses/", QDirIterator::Subdirectories); while (it.hasNext()) { QDir dir(it.next()); dir.setFilter(QDir::Files | QDir::NoSymLinks); QFileInfoList list = dir.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); if (fileInfo.completeSuffix() != QLatin1String("xml")) { continue; } addCourse(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); } } } //TODO this signal should only be emitted when repository was added/removed // yet the call to this method is very seldom and emitting it too often is not that harmful emit repositoryChanged(); } void ResourceManager::loadLanguageResources() { // load language resources // all other resources are only loaded on demand QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); foreach (const QString &testdir, dirs) { QDir dir(testdir + "/artikulate/languages/"); dir.setFilter(QDir::Files | QDir::NoSymLinks); QFileInfoList list = dir.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); if (fileInfo.completeSuffix() != QLatin1String("xml")) { continue; } addLanguage(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); } } } void ResourceManager::sync() { QMap< QString, QList< CourseResource* > >::iterator iter; for (iter = m_courseResources.begin(); iter != m_courseResources.end(); ++iter) { foreach (auto const &courseRes, iter.value()) { courseRes->sync(); } } foreach (auto const &courseRes, m_skeletonResources) { courseRes->sync(); } } bool ResourceManager::modified() const { QMap< QString, QList< CourseResource* > >::const_iterator iter; for (iter = m_courseResources.constBegin(); iter != m_courseResources.constEnd(); ++iter) { foreach (auto const &courseRes, iter.value()) { if (courseRes->isOpen() && courseRes->course()->modified()) { return true; } } } foreach (auto const &courseRes, m_skeletonResources) { if (courseRes->isOpen() && courseRes->skeleton()->modified()) { return true; } } return false; } void ResourceManager::addLanguage(const QUrl &languageFile) { if (m_loadedResources.contains(languageFile.toLocalFile())) { return; } LanguageResource *resource = new LanguageResource(this, languageFile); emit languageResourceAboutToBeAdded(resource, m_languageResources.count()); m_languageResources.append(resource); m_loadedResources.append(languageFile.toLocalFile()); m_courseResources.insert(resource->identifier(), QList()); emit languageResourceAdded(); } bool ResourceManager::isRepositoryManager() const { return !Settings::courseRepositoryPath().isEmpty(); } QString ResourceManager::repositoryUrl() const { return Settings::courseRepositoryPath(); } QList< LanguageResource* > ResourceManager::languageResources() const { return m_languageResources; } Language * ResourceManager::language(int index) const { Q_ASSERT(index >= 0 && index < m_languageResources.count()); return m_languageResources.at(index)->language(); } Language * ResourceManager::language(LearnerProfile::LearningGoal *learningGoal) const { if (!learningGoal) { return nullptr; } if (learningGoal->category() != LearnerProfile::LearningGoal::Language) { qCritical() << "Cannot translate non-language learning goal to language"; return nullptr; } foreach (LanguageResource *resource, m_languageResources) { if (resource->identifier() == learningGoal->identifier()) { return resource->language(); } } qCritical() << "No language registered with identifier " << learningGoal->identifier() << ": aborting"; return nullptr; } QList< CourseResource* > ResourceManager::courseResources(Language *language) { if (!language) { QList courses; for (auto iter = m_courseResources.constBegin(); iter != m_courseResources.constEnd(); ++iter) { courses.append(iter.value()); } return courses; } // return empty list if no course available for language if (!m_courseResources.contains(language->id())) { return QList< CourseResource* >(); } return m_courseResources[language->id()]; } Course * ResourceManager::course(Language *language, int index) const { Q_ASSERT(m_courseResources.contains(language->id())); Q_ASSERT(index >= 0 && index < m_courseResources[language->id()].count()); return m_courseResources[language->id()].at(index)->course(); } void ResourceManager::reloadCourseOrSkeleton(Course *courseOrSkeleton) { if (!courseOrSkeleton) { qCritical() << "Cannot reload non-existing course"; return; } if (!courseOrSkeleton->file().isValid()) { qCritical() << "Cannot reload temporary file, aborting."; return; } // figure out if this is a course or a skeleton if (courseOrSkeleton->language()) { // only course files have a language //TODO better add a check if this is contained in the course list // to catch possible errors QUrl file = courseOrSkeleton->file(); m_loadedResources.removeOne(courseOrSkeleton->file().toLocalFile()); removeCourse(courseOrSkeleton); addCourse(file); } else { foreach (SkeletonResource *resource, m_skeletonResources) { if (resource->identifier() == courseOrSkeleton->id()) { resource->reload(); return; } } } } void ResourceManager::updateCourseFromSkeleton(Course *course) { //TODO implement status information that are shown at mainwindow if (course->foreignId().isEmpty()) { qCritical() << "No skeleton ID specified, aborting update."; return; } Course *skeleton = nullptr; QList::ConstIterator iter = m_skeletonResources.constBegin(); while (iter != m_skeletonResources.constEnd()) { if ((*iter)->identifier() == course->foreignId()) { skeleton = (*iter)->skeleton(); break; } ++iter; } if (!skeleton) { qCritical() << "Could not find skeleton with id " << course->foreignId() << ", aborting update."; return; } // update now foreach (Unit *unitSkeleton, skeleton->unitList()) { // import unit if not exists Unit *currentUnit = nullptr; bool found = false; foreach (Unit *unit, course->unitList()) { if (unit->foreignId() == unitSkeleton->id()) { found = true; currentUnit = unit; break; } } if (found == false) { currentUnit = new Unit(course); currentUnit->setId(QUuid::createUuid().toString()); currentUnit->setTitle(unitSkeleton->title()); currentUnit->setForeignId(unitSkeleton->id()); currentUnit->setCourse(course); course->addUnit(currentUnit); course->setModified(true); } // update phrases foreach (Phrase *phraseSkeleton, unitSkeleton->phraseList()) { bool found = false; foreach (Phrase *phrase, currentUnit->phraseList()) { if (phrase->foreignId() == phraseSkeleton->id()) { if (phrase->i18nText() != phraseSkeleton->text()) { phrase->setEditState(Phrase::Unknown); phrase->seti18nText(phraseSkeleton->text()); } found = true; break; } } if (found == false) { Phrase *newPhrase = new Phrase(course); newPhrase->setForeignId(phraseSkeleton->id()); newPhrase->setId(QUuid::createUuid().toString()); newPhrase->setText(phraseSkeleton->text()); newPhrase->seti18nText(phraseSkeleton->text()); newPhrase->setType(phraseSkeleton->type()); newPhrase->setUnit(currentUnit); currentUnit->addPhrase(newPhrase); course->setModified(true); } } } // FIXME deassociate removed phrases qCDebug(ARTIKULATE_LOG) << "Update performed!"; } CourseResource * ResourceManager::addCourse(const QUrl &courseFile) -{ - CourseResource *resource = new CourseResource(this, courseFile); - if (resource->language().isEmpty()) { +{ + CourseResource *resource = new CourseResource(courseFile, nullptr); //TODO + if (resource->language() == nullptr) { delete resource; qCritical() << "Could not load course, language unknown:" << courseFile.toLocalFile(); return nullptr; } // skip already loaded resources if (m_loadedResources.contains(courseFile.toLocalFile())) { delete resource; return nullptr; } m_loadedResources.append(courseFile.toLocalFile()); addCourseResource(resource); emit languageCoursesChanged(); return resource; } void ResourceManager::addCourseResource(CourseResource *resource) { - Q_ASSERT(m_courseResources.contains(resource->language())); - - if (m_courseResources.contains(resource->language())) { - emit courseResourceAboutToBeAdded(resource, m_courseResources[resource->language()].count()); - } - else { - emit courseResourceAboutToBeAdded(resource, 0); - m_courseResources.insert(resource->language(), QList()); - } - m_courseResources[resource->language()].append(resource); - emit courseResourceAdded(); +// Q_ASSERT(m_courseResources.contains(resource->language())); + +// if (m_courseResources.contains(resource->language())) { +// emit courseResourceAboutToBeAdded(resource, m_courseResources[resource->language()].count()); +// } +// else { +// emit courseResourceAboutToBeAdded(resource, 0); +// m_courseResources.insert(resource->language(), QList()); +// } +// m_courseResources[resource->language()].append(resource); +// emit courseResourceAdded(); } void ResourceManager::removeCourse(Course *course) { for (int index = 0; index < m_courseResources[course->language()->id()].length(); ++index) { if (m_courseResources[course->language()->id()].at(index)->course() == course) { emit courseResourceAboutToBeRemoved(index); m_courseResources[course->language()->id()].removeAt(index); course->deleteLater(); return; } } } Course * ResourceManager::createCourse(Language *language, Skeleton *skeleton) { // set path QString path = QStringLiteral("%1/%2/%3/%4/%4.xml") .arg(Settings::courseRepositoryPath(), QStringLiteral("courses"), skeleton->id(), language->id()); - CourseResource * courseRes = new CourseResource(this, QUrl::fromLocalFile(path)); + CourseResource * courseRes = new CourseResource(QUrl::fromLocalFile(path), nullptr); //TODO Q_ASSERT(courseRes); Course *course = courseRes->course(); Q_ASSERT(course); course->setId(QUuid::createUuid().toString()); course->setTitle(skeleton->title()); course->setDescription(skeleton->description()); course->setFile(QUrl::fromLocalFile(path)); course->setLanguage(language); // set skeleton course->setForeignId(skeleton->id()); addCourseResource(courseRes); return course; } void ResourceManager::addSkeleton(const QUrl &skeletonFile) { SkeletonResource *resource = new SkeletonResource(this, skeletonFile); addSkeletonResource(resource); } void ResourceManager::addSkeletonResource(SkeletonResource *resource) { // skip already loaded resources if (m_loadedResources.contains(resource->path().toLocalFile())) { return; } m_loadedResources.append(resource->path().toLocalFile()); emit skeletonAboutToBeAdded(resource->skeleton(), m_skeletonResources.count()); m_skeletonResources.append(resource); emit skeletonAdded(); } void ResourceManager::removeSkeleton(Skeleton *skeleton) { for (int index = 0; index < m_skeletonResources.length(); ++index) { if (m_skeletonResources.at(index)->identifier() == skeleton->id()) { emit skeletonAboutToBeRemoved(index, index); m_skeletonResources.removeAt(index); emit skeletonRemoved(); skeleton->deleteLater(); return; } } } QList< SkeletonResource* > ResourceManager::skeletonResources() { return m_skeletonResources; } diff --git a/src/core/resourcerepository.cpp b/src/core/resourcerepository.cpp new file mode 100644 index 0000000..cc251cd --- /dev/null +++ b/src/core/resourcerepository.cpp @@ -0,0 +1,160 @@ +/* + * 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()); + } +} + +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), languages(), 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(nullptr, QUrl::fromLocalFile(resourceFile)); //TODO hacky adapter code + 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 new file mode 100644 index 0000000..8e17b94 --- /dev/null +++ b/src/core/resourcerepository.h @@ -0,0 +1,103 @@ +/* + * 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); + + /** + * @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 9d1ac69..af67bd0 100644 --- a/src/core/resources/courseresource.cpp +++ b/src/core/resources/courseresource.cpp @@ -1,488 +1,551 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * Copyright 2013 Oindrila Gupta * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "courseresource.h" #include "core/resourcemanager.h" #include "core/language.h" #include "core/course.h" #include "core/unit.h" #include "core/phoneme.h" #include "core/phonemegroup.h" -#include "core/resources/languageresource.h" +#include "core/language.h" +#include "core/iresourcerepository.h" +#include "application.h" #include #include +#include +#include #include #include #include #include #include #include #include "artikulate_debug.h" class CourseResourcePrivate { public: - CourseResourcePrivate(ResourceManager *resourceManager) - : m_resourceManager(resourceManager) - , m_courseResource(nullptr) - , m_type(ResourceInterface::CourseResourceType) - { - } - - ~CourseResourcePrivate() - { - } - - ResourceManager *m_resourceManager; + CourseResourcePrivate() = default; + + void loadCourse(); + Phrase * parsePhrase(QDomElement phraseNode, Unit* parentUnit) const; + + /** + * Load XSD file given by its file name (without ".xsd" suffix). The method searches exclusively + * the standard install dir for XSD files in subdirectory "schemes/". + * + * \param schemeName name of the Xml schema without suffix + * \return loaded XML Schema + */ + QXmlSchema loadXmlSchema(const QString &schemeName) const; + + /** + * Load XML file given by \p file that confirms with XML schema \p scheme. + * + * \param path is the path to the XML file to be loaded + * \param scheme is the XML schema describing the DOM + * \return the loaded DOM document + */ + QDomDocument loadDomDocument(const QUrl &path, const QXmlSchema &schema) const; + + IResourceRepository *m_repository{ nullptr }; QUrl m_path; QString m_identifier; QString m_title; - QString m_language; + QString m_languageId; + Language *m_language{ nullptr }; QString m_i18nTitle; - Course *m_courseResource; - ResourceInterface::Type m_type; + Course *m_loadedCourse{ nullptr }; }; -CourseResource::CourseResource(ResourceManager *resourceManager, const QUrl &path) - : ResourceInterface(resourceManager) - , d(new CourseResourcePrivate(resourceManager)) +QXmlSchema CourseResourcePrivate::loadXmlSchema(const QString &schemeName) const +{ + QString relPath = QStringLiteral(":/artikulate/schemes/%1.xsd").arg(schemeName); + QUrl file = QUrl::fromLocalFile(relPath); + + QXmlSchema schema; + if (file.isEmpty() || schema.load(file) == false) { + qCWarning(ARTIKULATE_LOG) << "Schema at file " << file.toLocalFile() << " is invalid."; + } + return schema; +} + +QDomDocument CourseResourcePrivate::loadDomDocument(const QUrl &path, const QXmlSchema &schema) const +{ + QDomDocument document; + QXmlSchemaValidator validator(schema); + if (!validator.validate(path)) { + qCWarning(ARTIKULATE_LOG) << "Schema is not valid, aborting loading of XML document:" << path.toLocalFile(); + return document; + } + + QString errorMsg; + QFile file(path.toLocalFile()); + if (file.open(QIODevice::ReadOnly)) { + if (!document.setContent(&file, &errorMsg)) { + qCWarning(ARTIKULATE_LOG) << errorMsg; + } + } else { + qCWarning(ARTIKULATE_LOG) << "Could not open XML document " << path.toLocalFile() << " for reading, aborting."; + } + return document; +} + + +CourseResource::CourseResource(const QUrl &path, IResourceRepository *repository) + : ICourse(repository) + , d(new CourseResourcePrivate()) { d->m_path = 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() == "title") { d->m_title = xml.readElementText(); d->m_i18nTitle = d->m_title; continue; } //TODO i18nTitle must be implemented, currently missing and hence not parsed if (xml.name() == "language") { - d->m_language = xml.readElementText(); + 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_language.isEmpty() + && !d->m_languageId.isEmpty() ) { break; } } if (xml.hasError()) { qCritical() << "Error occurred when reading Course XML file:" << path.toLocalFile(); } } xml.clear(); file.close(); } +CourseResource::CourseResource(const QUrl &path, const QVector &languages, IResourceRepository *repository) + : CourseResource(path, repository) +{ + for (const auto &language : languages) { + if (language == nullptr) { + continue; + } + if (language->id() == d->m_languageId) { + d->m_language = language; + } + } +} + CourseResource::~CourseResource() { } -QString CourseResource::identifier() +QString CourseResource::id() const { - if (d->m_courseResource) { - return d->m_courseResource->id(); - } return d->m_identifier; } -QString CourseResource::title() +QString CourseResource::title() const { - if (d->m_courseResource) { - return d->m_courseResource->title(); - } return d->m_title; } -QString CourseResource::i18nTitle() +QString CourseResource::i18nTitle() const { - if (d->m_courseResource) { - return d->m_courseResource->title(); //TODO - } return d->m_i18nTitle; } -QString CourseResource::language() const +QString CourseResource::description() const { - if (d->m_courseResource) { - return d->m_courseResource->language()->id(); + if (d->m_loadedCourse != nullptr) { + return d->m_loadedCourse->description(); } + return QString(); +} + +Language * CourseResource::language() const +{ return d->m_language; } -ResourceInterface::Type CourseResource::type() const +QList CourseResource::unitList() { - return d->m_type; + if (d->m_loadedCourse == nullptr) { + d->loadCourse(); + } + if (d->m_loadedCourse != nullptr) { + return d->m_loadedCourse->unitList(); + } + return QList(); } void CourseResource::sync() { - Q_ASSERT(path().isValid()); - Q_ASSERT(path().isLocalFile()); - Q_ASSERT(!path().isEmpty()); + Q_ASSERT(d->m_path.isValid()); + Q_ASSERT(d->m_path.isLocalFile()); + Q_ASSERT(!d->m_path.isEmpty()); // if resource was never loaded, it cannot be changed - if (d->m_courseResource == nullptr) { - qCDebug(ARTIKULATE_LOG) << "Aborting sync, course was not parsed."; + if (d->m_loadedCourse == nullptr) { + qCDebug(ARTIKULATE_LOG()) << "Aborting sync, course was not parsed."; return; } -//TODO -// // not writing back if not modified -// if (!d->m_courseResource->modified()) { -// qCDebug(ARTIKULATE_LOG) << "Aborting sync, course was not modified."; -// return; -// } + // not writing back if not modified + if (!d->m_loadedCourse->modified()) { + qCDebug(ARTIKULATE_LOG()) << "Aborting sync, course was not modified."; + return; + } // write back to file // create directories if necessary - QFileInfo info(path().adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); + QFileInfo info(d->m_path.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); if (!info.exists()) { qCDebug(ARTIKULATE_LOG) << "create xml output file directory, not existing"; QDir dir; - dir.mkpath(path().adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); + dir.mkpath(d->m_path.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } //TODO port to KSaveFile - QFile file(path().toLocalFile()); + QFile file(d->m_path.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { qCWarning(ARTIKULATE_LOG) << "Unable to open file " << file.fileName() << " in write mode, aborting."; return; } file.write(serializedDocument().toByteArray()); return; } void CourseResource::exportGhns(const QString &path) { - // ensure that course is loaded before exporting it - Course *course = CourseResource::course(); + if (d->m_loadedCourse == nullptr) { + d->loadCourse(); + } // filename - const QString fileName = identifier() + ".tar.bz2"; + const QString fileName = id() + ".tar.bz2"; KTar tar = KTar(path + '/' + fileName, QStringLiteral("application/x-bzip")); if (!tar.open(QIODevice::WriteOnly)) { qCWarning(ARTIKULATE_LOG) << "Unable to open tar file" << path + '/' + fileName << "in write mode, aborting."; return; } - foreach (auto *unit, course->unitList()) { - foreach (auto *phrase, unit->phraseList()) { + for (auto *unit : unitList()) { + for (auto *phrase : unit->phraseList()) { if (QFile::exists(phrase->soundFileUrl())) { tar.addLocalFile(phrase->soundFileUrl(), phrase->id() + ".ogg"); } } } - tar.writeFile(identifier() + ".xml", serializedDocument(true).toByteArray()); + tar.writeFile(id() + ".xml", serializedDocument(true).toByteArray()); tar.close(); } void CourseResource::close() { - d->m_courseResource->deleteLater(); - d->m_courseResource = nullptr; + d->m_loadedCourse->deleteLater(); + d->m_loadedCourse = nullptr; } bool CourseResource::isOpen() const { - return (d->m_courseResource != nullptr); + return (d->m_loadedCourse != nullptr); } -QUrl CourseResource::path() const +QUrl CourseResource::file() const { - if (d->m_courseResource) { - return d->m_courseResource->file(); + if (d->m_loadedCourse) { + return d->m_loadedCourse->file(); } return d->m_path; } -QObject * CourseResource::resource() +void CourseResourcePrivate::loadCourse() { - if (d->m_courseResource != nullptr) { - return d->m_courseResource; + if (m_loadedCourse != nullptr) { + qCWarning(ARTIKULATE_CORE()) << "Skipping loading of course, no reloading implemented yet"; + return; } - // if file does not exist, create new course - QFileInfo info(d->m_path.toLocalFile()); + QFileInfo info(m_path.toLocalFile()); if (!info.exists()) { - d->m_courseResource = new Course(this); - d->m_courseResource->setId(d->m_identifier); - d->m_courseResource->setTitle(d->m_title); - return d->m_courseResource; + qCCritical(ARTIKULATE_LOG()) << "No course file available at location" << m_path.toLocalFile(); + return; } // load existing file QXmlSchema schema = loadXmlSchema(QStringLiteral("course")); if (!schema.isValid()) { - return nullptr; + qCWarning(ARTIKULATE_CORE()) << "Scheme not valid, aborting"; + return; } - QDomDocument document = loadDomDocument(path(), schema); + QDomDocument document = loadDomDocument(m_path, schema); if (document.isNull()) { - qCWarning(ARTIKULATE_LOG) << "Could not parse document " << path().toLocalFile() << ", aborting."; - return nullptr; + qCWarning(ARTIKULATE_CORE()) << "Could not parse document " << m_path.toLocalFile() << ", aborting."; + return; } // create course QDomElement root(document.documentElement()); - d->m_courseResource = new Course(this); + m_loadedCourse = new Course(nullptr); - d->m_courseResource->setFile(d->m_path); - d->m_courseResource->setId(root.firstChildElement(QStringLiteral("id")).text()); - d->m_courseResource->setTitle(root.firstChildElement(QStringLiteral("title")).text()); - d->m_courseResource->setDescription(root.firstChildElement(QStringLiteral("description")).text()); + m_loadedCourse->setFile(m_path); + m_loadedCourse->setId(root.firstChildElement(QStringLiteral("id")).text()); + m_loadedCourse->setTitle(root.firstChildElement(QStringLiteral("title")).text()); + m_loadedCourse->setDescription(root.firstChildElement(QStringLiteral("description")).text()); if (!root.firstChildElement(QStringLiteral("foreignId")).isNull()) { - d->m_courseResource->setForeignId(root.firstChildElement(QStringLiteral("foreignId")).text()); + m_loadedCourse->setForeignId(root.firstChildElement(QStringLiteral("foreignId")).text()); } // set language //TODO not efficient to load completely every language for this comparison - QString language = root.firstChildElement(QStringLiteral("language")).text(); - foreach(LanguageResource * resource, d->m_resourceManager->languageResources()) { - if (resource->language()->id() == language) { - d->m_courseResource->setLanguage(resource->language()); + QString languageId = root.firstChildElement(QStringLiteral("language")).text(); + for(const auto &language : m_repository->languages()) { + if (language->id() == languageId) { + m_loadedCourse->setLanguage(language); break; } } - if (d->m_courseResource->language() == nullptr) { - qCWarning(ARTIKULATE_LOG) << "Language ID" << language << "unknown, could not register any language, aborting"; - return nullptr; + if (m_loadedCourse->language() == nullptr) { + qCWarning(ARTIKULATE_LOG) << "Language ID" << languageId << "unknown, could not register any language, aborting"; + return; } // create units for (QDomElement unitNode = root.firstChildElement(QStringLiteral("units")).firstChildElement(); !unitNode.isNull(); unitNode = unitNode.nextSiblingElement()) { - Unit *unit = new Unit(d->m_courseResource); + Unit *unit = new Unit(nullptr); unit->setId(unitNode.firstChildElement(QStringLiteral("id")).text()); - unit->setCourse(d->m_courseResource); + unit->setCourse(m_loadedCourse); unit->setTitle(unitNode.firstChildElement(QStringLiteral("title")).text()); if (!unitNode.firstChildElement(QStringLiteral("foreignId")).isNull()) { unit->setForeignId(unitNode.firstChildElement(QStringLiteral("foreignId")).text()); } - d->m_courseResource->addUnit(unit); + m_loadedCourse->addUnit(unit); // create phrases for (QDomElement phraseNode = unitNode.firstChildElement(QStringLiteral("phrases")).firstChildElement(); !phraseNode.isNull(); phraseNode = phraseNode.nextSiblingElement()) { unit->addPhrase(parsePhrase(phraseNode, unit)); // add to unit at last step to produce only one signal //FIXME phrase does not cause unit signals that phonemes list is changed } } - d->m_courseResource->setModified(false); - - return d->m_courseResource; + m_loadedCourse->setModified(false); } Course * CourseResource::course() { - return qobject_cast(resource()); + return nullptr; } -Phrase* CourseResource::parsePhrase(QDomElement phraseNode, Unit* parentUnit) const +Phrase* CourseResourcePrivate::parsePhrase(QDomElement phraseNode, Unit* parentUnit) const { Phrase *phrase = new Phrase(parentUnit); phrase->setId(phraseNode.firstChildElement(QStringLiteral("id")).text()); phrase->setText(phraseNode.firstChildElement(QStringLiteral("text")).text()); phrase->seti18nText(phraseNode.firstChildElement(QStringLiteral("i18nText")).text()); phrase->setUnit(parentUnit); if (!phraseNode.firstChildElement(QStringLiteral("soundFile")).text().isEmpty()) { phrase->setSound(QUrl::fromLocalFile( - path().adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path() + m_path.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path() + '/' + phraseNode.firstChildElement(QStringLiteral("soundFile")).text()) ); } phrase->setType(phraseNode.firstChildElement(QStringLiteral("type")).text()); phrase->setEditState(phraseNode.firstChildElement(QStringLiteral("editState")).text()); if (!phraseNode.firstChildElement(QStringLiteral("foreignId")).isNull()) { phrase->setForeignId(phraseNode.firstChildElement(QStringLiteral("foreignId")).text()); } // add phonemes - QList phonemes = d->m_courseResource->language()->phonemes(); + QList phonemes = m_loadedCourse->language()->phonemes(); for (QDomElement phonemeID = phraseNode.firstChildElement(QStringLiteral("phonemes")).firstChildElement(); !phonemeID.isNull(); phonemeID = phonemeID.nextSiblingElement()) { QString id = phonemeID.text(); if (id.isEmpty()) { qCritical() << "Phoneme ID string is empty for phrase "<< phrase->id() <<", aborting."; continue; } foreach (Phoneme *phoneme, phonemes) { if (phoneme->id() == id) { phrase->addPhoneme(phoneme); break; } } } if (!phraseNode.firstChildElement(QStringLiteral("excluded")).isNull() && phraseNode.firstChildElement(QStringLiteral("excluded")).text() == QLatin1String("true")) { phrase->setExcluded(true); } return phrase; } QDomDocument CourseResource::serializedDocument(bool trainingExport) const { QDomDocument document; // prepare xml header QDomProcessingInstruction header = document.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\"")); document.appendChild(header); // create main element QDomElement root = document.createElement(QStringLiteral("course")); document.appendChild(root); QDomElement idElement = document.createElement(QStringLiteral("id")); QDomElement titleElement = document.createElement(QStringLiteral("title")); QDomElement descriptionElement = document.createElement(QStringLiteral("description")); QDomElement languageElement = document.createElement(QStringLiteral("language")); - idElement.appendChild(document.createTextNode(d->m_courseResource->id())); - titleElement.appendChild(document.createTextNode(d->m_courseResource->title())); - descriptionElement.appendChild(document.createTextNode(d->m_courseResource->description())); - languageElement.appendChild(document.createTextNode(d->m_courseResource->language()->id())); + idElement.appendChild(document.createTextNode(d->m_loadedCourse->id())); + titleElement.appendChild(document.createTextNode(d->m_loadedCourse->title())); + descriptionElement.appendChild(document.createTextNode(d->m_loadedCourse->description())); + languageElement.appendChild(document.createTextNode(d->m_loadedCourse->language()->id())); QDomElement unitListElement = document.createElement(QStringLiteral("units")); // create units - foreach (Unit *unit, d->m_courseResource->unitList()) { + foreach (Unit *unit, d->m_loadedCourse->unitList()) { QDomElement unitElement = document.createElement(QStringLiteral("unit")); QDomElement unitIdElement = document.createElement(QStringLiteral("id")); QDomElement unitTitleElement = document.createElement(QStringLiteral("title")); QDomElement unitPhraseListElement = document.createElement(QStringLiteral("phrases")); unitIdElement.appendChild(document.createTextNode(unit->id())); unitTitleElement.appendChild(document.createTextNode(unit->title())); // construct phrases foreach (Phrase *phrase, unit->phraseList()) { if (trainingExport && phrase->soundFileUrl().isEmpty()) { continue; } unitPhraseListElement.appendChild(serializedPhrase(phrase, document)); } if (trainingExport && unitPhraseListElement.childNodes().count() == 0) { continue; } // construct the unit element unitElement.appendChild(unitIdElement); if (!unit->foreignId().isEmpty()) { QDomElement unitForeignIdElement = document.createElement(QStringLiteral("foreignId")); unitForeignIdElement.appendChild(document.createTextNode(unit->foreignId())); unitElement.appendChild(unitForeignIdElement); } unitElement.appendChild(unitTitleElement); unitElement.appendChild(unitPhraseListElement); unitListElement.appendChild(unitElement); } root.appendChild(idElement); - if (!d->m_courseResource->foreignId().isEmpty()) { + if (!d->m_loadedCourse->foreignId().isEmpty()) { QDomElement courseForeignIdElement = document.createElement(QStringLiteral("foreignId")); - courseForeignIdElement.appendChild(document.createTextNode(d->m_courseResource->foreignId())); + courseForeignIdElement.appendChild(document.createTextNode(d->m_loadedCourse->foreignId())); root.appendChild(courseForeignIdElement); } root.appendChild(titleElement); root.appendChild(descriptionElement); root.appendChild(languageElement); root.appendChild(unitListElement); return document; } QDomElement CourseResource::serializedPhrase(Phrase *phrase, QDomDocument &document) const { QDomElement phraseElement = document.createElement(QStringLiteral("phrase")); QDomElement phraseIdElement = document.createElement(QStringLiteral("id")); QDomElement phraseTextElement = document.createElement(QStringLiteral("text")); QDomElement phrasei18nTextElement = document.createElement(QStringLiteral("i18nText")); QDomElement phraseSoundFileElement = document.createElement(QStringLiteral("soundFile")); QDomElement phraseTypeElement = document.createElement(QStringLiteral("type")); QDomElement phraseEditStateElement = document.createElement(QStringLiteral("editState")); QDomElement phrasePhonemeListElement = document.createElement(QStringLiteral("phonemes")); phraseIdElement.appendChild(document.createTextNode(phrase->id())); phraseTextElement.appendChild(document.createTextNode(phrase->text())); phrasei18nTextElement.appendChild(document.createTextNode(phrase->i18nText())); phraseSoundFileElement.appendChild(document.createTextNode(phrase->sound().fileName())); phraseTypeElement.appendChild(document.createTextNode(phrase->typeString())); phraseEditStateElement.appendChild(document.createTextNode(phrase->editStateString())); // add phonemes foreach (Phoneme *phoneme, phrase->phonemes()) { QDomElement phonemeElement = document.createElement(QStringLiteral("phonemeID")); phonemeElement.appendChild(document.createTextNode(phoneme->id())); phrasePhonemeListElement.appendChild(phonemeElement); } phraseElement.appendChild(phraseIdElement); if (!phrase->foreignId().isEmpty()) { QDomElement phraseForeignIdElement = document.createElement(QStringLiteral("foreignId")); phraseForeignIdElement.appendChild(document.createTextNode(phrase->foreignId())); phraseElement.appendChild(phraseForeignIdElement); } phraseElement.appendChild(phraseTextElement); phraseElement.appendChild(phrasei18nTextElement); phraseElement.appendChild(phraseSoundFileElement); phraseElement.appendChild(phraseTypeElement); phraseElement.appendChild(phraseEditStateElement); phraseElement.appendChild(phrasePhonemeListElement); if (phrase->isExcluded()) { QDomElement phraseIsExcludedElement = document.createElement(QStringLiteral("excluded")); phraseIsExcludedElement.appendChild(document.createTextNode(QStringLiteral("true"))); phraseElement.appendChild(phraseIsExcludedElement); } return phraseElement; } diff --git a/src/core/resources/courseresource.h b/src/core/resources/courseresource.h index 02861df..1c491ef 100644 --- a/src/core/resources/courseresource.h +++ b/src/core/resources/courseresource.h @@ -1,120 +1,125 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef COURSERESOURCE_H #define COURSERESOURCE_H #include "artikulatecore_export.h" -#include "resourceinterface.h" +#include "core/icourse.h" #include +#include class QDomElement; class QString; class CourseResourcePrivate; class Course; class Unit; class Phrase; +class IResourceRepository; -class ARTIKULATECORE_EXPORT CourseResource : public ResourceInterface +//TODO move to private +class QXmlSchema; +class QJSonDocument; +class QDomDocument; + +class ARTIKULATECORE_EXPORT CourseResource : public ICourse { Q_OBJECT + Q_INTERFACES(ICourse) void course(QString text); public: /** * Create course resource from file. */ - explicit CourseResource(ResourceManager *resourceManager, const QUrl &path); + explicit CourseResource(const QUrl &path, IResourceRepository *repository); - virtual ~CourseResource(); + /** + * @brief convenience constructor for porting to language access by dedicated model + */ + explicit Q_DECL_DEPRECATED CourseResource(const QUrl &path, const QVector &languages, IResourceRepository *repository); + + ~CourseResource() override; /** * \return unique identifier */ - QString identifier() override; + QString id() const override; /** * \return human readable localized title */ - QString title() override; + QString title() const override; /** * \return human readable title in English */ - QString i18nTitle() override; + QString i18nTitle() const override; /** - * \return language identifier of this course + * \return description text for course */ - QString language() const; + QString description() const override; /** - * \return type of resource + * \return language identifier of this course */ - Type type() const override; + Language * language() const override; /** * \return true if resource is loaded, otherwise false */ - bool isOpen() const override; + bool isOpen() const; - void sync() override; + void sync(); /** * export course as .tar.bz2 file in the specified folder. */ void exportGhns(const QString &path); /** * close resource without writing changes back to file */ - void close() override; + void close(); - /** - * \return path to resource file - */ - QUrl path() const override; + QUrl file() const override; - /** - * \return reference to the loaded resource - * if resource is not open yet, it will be loaded - */ - QObject * resource() override; + QList unitList() override; /** * \return reference to the loaded course resource - * Same behavior as \see resource() but casted to Course */ - Course * course(); + Q_DECL_DEPRECATED Course * course(); private: Phrase * parsePhrase(QDomElement phraseNode, Unit *parentUnit) const; /** * \return serialized course as DOM document * \param trainingExport if true phrases without recording and empty units are excluded */ QDomDocument serializedDocument(bool trainingExport=false) const; QDomElement serializedPhrase(Phrase * phrase, QDomDocument &document) const; const QScopedPointer d; }; #endif diff --git a/src/core/resources/languageresource.cpp b/src/core/resources/languageresource.cpp index 3152a81..c3eda69 100644 --- a/src/core/resources/languageresource.cpp +++ b/src/core/resources/languageresource.cpp @@ -1,190 +1,185 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "languageresource.h" #include "core/resourcemanager.h" #include "core/language.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include #include #include #include #include #include "artikulate_debug.h" class LanguageResourcePrivate { public: LanguageResourcePrivate(ResourceManager *resourceManager) : m_resourceManager(resourceManager) , m_languageResource(nullptr) , m_type(ResourceInterface::LanguageResourceType) { } ~LanguageResourcePrivate() { } ResourceManager *m_resourceManager; Language *m_languageResource; QString m_identifier; QUrl m_path; QString m_title; QString m_i18nTitle; ResourceInterface::Type m_type; }; LanguageResource::LanguageResource(ResourceManager *resourceManager, const QUrl &path) : ResourceInterface(resourceManager) , d(new LanguageResourcePrivate(resourceManager)) { d->m_path = path; // load basic information from language file, but does not parse everything QXmlStreamReader xml; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { xml.setDevice(&file); xml.readNextStartElement(); while (xml.readNext() && !xml.atEnd()) { if (xml.name() == "id") { d->m_identifier = xml.readElementText(); } if (xml.name() == "title") { d->m_title = xml.readElementText(); } if (xml.name() == "i18nTitle") { d->m_i18nTitle = xml.readElementText(); } // quit reading when basic elements are read if (!d->m_identifier.isEmpty() && !d->m_title.isEmpty() && !d->m_i18nTitle.isEmpty() ) { break; } } if (xml.hasError()) { qCritical() << "Error occurred when reading Language XML file:" << path.toLocalFile(); } } xml.clear(); file.close(); } LanguageResource::~LanguageResource() { } QString LanguageResource::identifier() { return d->m_identifier; } QString LanguageResource::title() { return d->m_title; } QString LanguageResource::i18nTitle() { return d->m_i18nTitle; } -ResourceInterface::Type LanguageResource::type() const -{ - return d->m_type; -} - void LanguageResource::close() { // do nothing // language files are never closed } bool LanguageResource::isOpen() const { return (d->m_languageResource != nullptr); } QUrl LanguageResource::path() const { return d->m_path; } QObject * LanguageResource::resource() { if (d->m_languageResource != nullptr) { return d->m_languageResource; } if (!d->m_path.isLocalFile()) { qCWarning(ARTIKULATE_LOG) << "Cannot open language file at " << d->m_path.toLocalFile() << ", aborting."; return nullptr; } QXmlSchema schema = loadXmlSchema(QStringLiteral("language")); if (!schema.isValid()) { return nullptr; } QDomDocument document = loadDomDocument(d->m_path, schema); if (document.isNull()) { qCWarning(ARTIKULATE_LOG) << "Could not parse document " << d->m_path.toLocalFile() << ", aborting."; return nullptr; } QDomElement root(document.documentElement()); d->m_languageResource = new Language(this); d->m_languageResource->setFile(d->m_path); d->m_languageResource->setId(root.firstChildElement(QStringLiteral("id")).text()); d->m_languageResource->setTitle(root.firstChildElement(QStringLiteral("title")).text()); d->m_languageResource->seti18nTitle(root.firstChildElement(QStringLiteral("i18nTitle")).text()); // create phoneme groups for (QDomElement groupNode = root.firstChildElement(QStringLiteral("phonemeGroups")).firstChildElement(); !groupNode.isNull(); groupNode = groupNode.nextSiblingElement()) { PhonemeGroup *group = d->m_languageResource->addPhonemeGroup( groupNode.firstChildElement(QStringLiteral("id")).text(), groupNode.firstChildElement(QStringLiteral("title")).text()); group->setDescription(groupNode.attribute(QStringLiteral("description"))); // register phonemes for (QDomElement phonemeNode = groupNode.firstChildElement(QStringLiteral("phonemes")).firstChildElement(); !phonemeNode.isNull(); phonemeNode = phonemeNode.nextSiblingElement()) { group->addPhoneme(phonemeNode.firstChildElement(QStringLiteral("id")).text(), phonemeNode.firstChildElement(QStringLiteral("title")).text()); } } return d->m_languageResource; } Language * LanguageResource::language() { return qobject_cast(resource()); } diff --git a/src/core/resources/languageresource.h b/src/core/resources/languageresource.h index d9756a7..6544bd2 100644 --- a/src/core/resources/languageresource.h +++ b/src/core/resources/languageresource.h @@ -1,93 +1,88 @@ /* * 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 LANGUAGERESOURCE_H #define LANGUAGERESOURCE_H #include "artikulatecore_export.h" #include "resourceinterface.h" #include class LanguageResourcePrivate; class Language; class ARTIKULATECORE_EXPORT LanguageResource : public ResourceInterface { Q_OBJECT public: explicit LanguageResource(ResourceManager *resourceManager, const QUrl &path); virtual ~LanguageResource(); /** * \return unique identifier */ QString identifier() override; /** * \return human readable localized title */ QString title() override; /** * \return human readable title in English */ QString i18nTitle() override; - /** - * \return type of resource - */ - Type type() const override; - /** * \return true if resource is loaded, otherwise false */ bool isOpen() const override; /** * close resource without writing changes back to file */ void close() override; /** * \return path to resource file */ QUrl path() const override; /** * \return reference to the loaded resource * if resource is not open yet, it will be loaded */ QObject * resource() override; /** * \return reference to the loaded language resource * Same behavior as \see resource() but casted to Language */ Language * language(); private: const QScopedPointer d; }; #endif diff --git a/src/core/resources/resourceinterface.cpp b/src/core/resources/resourceinterface.cpp index f63566e..d0a242b 100644 --- a/src/core/resources/resourceinterface.cpp +++ b/src/core/resources/resourceinterface.cpp @@ -1,95 +1,94 @@ /* * 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 "resourceinterface.h" #include "artikulate_debug.h" #include #include #include #include #include #include #include ResourceInterface::ResourceInterface(ResourceManager *resourceManager) - : QObject() - , m_contributorResource(false) + : m_contributorResource(false) { Q_UNUSED(resourceManager) } ResourceInterface::~ResourceInterface() { } void ResourceInterface::setContributorResource(bool contributorResource) { m_contributorResource = contributorResource; } bool ResourceInterface::isContributorResource() const { return m_contributorResource; } void ResourceInterface::sync() { qCWarning(ARTIKULATE_LOG) << "Resource does not implement syncing."; } void ResourceInterface::reload() { qCWarning(ARTIKULATE_LOG) << "Resource does not implement reloading."; } QXmlSchema ResourceInterface::loadXmlSchema(const QString &schemeName) const { - QString relPath = QStringLiteral("schemes/%1.xsd").arg(schemeName); - QUrl file = QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, "artikulate/" + relPath)); + QString relPath = QStringLiteral(":/artikulate/schemes/%1.xsd").arg(schemeName); + QUrl file = QUrl::fromLocalFile(relPath); QXmlSchema schema; if (file.isEmpty() || schema.load(file) == false) { qCWarning(ARTIKULATE_LOG) << "Schema at file " << file.toLocalFile() << " is invalid."; } return schema; } QDomDocument ResourceInterface::loadDomDocument(const QUrl &path, const QXmlSchema &schema) const { QDomDocument document; QXmlSchemaValidator validator(schema); if (!validator.validate(path)) { qCWarning(ARTIKULATE_LOG) << "Schema is not valid, aborting loading of XML document:" << path.toLocalFile(); return document; } QString errorMsg; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { if (!document.setContent(&file, &errorMsg)) { qCWarning(ARTIKULATE_LOG) << errorMsg; } } else { qCWarning(ARTIKULATE_LOG) << "Could not open XML document " << path.toLocalFile() << " for reading, aborting."; } return document; } diff --git a/src/core/resources/resourceinterface.h b/src/core/resources/resourceinterface.h index 90c5b22..a36c51f 100644 --- a/src/core/resources/resourceinterface.h +++ b/src/core/resources/resourceinterface.h @@ -1,133 +1,127 @@ /* * 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 RESOURCEINTERFACE_H #define RESOURCEINTERFACE_H #include "artikulatecore_export.h" #include class ResourceManager; class QUrl; class QXmlSchema; class QDomDocument; class ARTIKULATECORE_EXPORT ResourceInterface : public QObject { - Q_OBJECT public: enum Type { LanguageResourceType, CourseResourceType, SkeletonResourceType }; explicit ResourceInterface(ResourceManager *resourceManager); virtual ~ResourceInterface(); /** * \return unique identifier */ virtual QString identifier() = 0; /** * \return human readable localized title */ virtual QString title() = 0; /** * \return human readable title in English */ virtual QString i18nTitle() = 0; /** * Set resource to be read-only. */ virtual void setContributorResource(bool contributorResource=true); /** * \returns true if resource is readonly, otherwise false */ virtual bool isContributorResource() const; - /** - * \return type of resource - */ - virtual Type type() const = 0; - /** * \return true if resource is loaded, otherwise false */ virtual bool isOpen() const = 0; /** * close resource without writing changes back to file */ virtual void close() = 0; /** * \return path to resource file */ virtual QUrl path() const = 0; /** * Write changes to resource back to file. * This operation does _not_ close the file. */ virtual void sync(); /** * Reload resource from file. */ virtual void reload(); /** * \return reference to the loaded resource * if resource is not open yet, it will be loaded */ virtual QObject * resource() = 0; /** * Load XSD file given by its file name (without ".xsd" suffix). The method searches exclusively * the standard install dir for XSD files in subdirectory "schemes/". * * \param schemeName name of the Xml schema without suffix * \return loaded XML Schema */ QXmlSchema loadXmlSchema(const QString &schemeName) const; /** * Load XML file given by \p file that confirms with XML schema \p scheme. * * \param path is the path to the XML file to be loaded * \param scheme is the XML schema describing the DOM * \return the loaded DOM document */ QDomDocument loadDomDocument(const QUrl &path, const QXmlSchema &schema) const; private: bool m_contributorResource; //!< identifies this resource as an editable resource }; #endif diff --git a/src/core/resources/skeletonresource.cpp b/src/core/resources/skeletonresource.cpp index e50d049..c1ece20 100644 --- a/src/core/resources/skeletonresource.cpp +++ b/src/core/resources/skeletonresource.cpp @@ -1,323 +1,323 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "skeletonresource.h" #include "core/resourcemanager.h" #include "core/language.h" #include "core/skeleton.h" #include "core/unit.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include "core/resources/languageresource.h" #include #include #include #include #include #include "artikulate_debug.h" class SkeletonResourcePrivate { public: SkeletonResourcePrivate(ResourceManager *resourceManager) : m_resourceManager(resourceManager) , m_skeletonResource(nullptr) , m_type(ResourceInterface::SkeletonResourceType) { } ~SkeletonResourcePrivate() = default; ResourceManager *m_resourceManager; QUrl m_path; QString m_identifier; QString m_title; QString m_i18nTitle; Skeleton *m_skeletonResource; ResourceInterface::Type m_type; }; SkeletonResource::SkeletonResource(ResourceManager *resourceManager, const QUrl &path) : ResourceInterface(resourceManager) , d(new SkeletonResourcePrivate(resourceManager)) { d->m_path = path; // load basic information from language file, but does not parse everything QXmlStreamReader xml; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { xml.setDevice(&file); xml.readNextStartElement(); while (xml.readNext() && !xml.atEnd()) { if (xml.name() == "id") { d->m_identifier = xml.readElementText(); continue; } if (xml.name() == "title") { d->m_title = xml.readElementText(); d->m_i18nTitle = d->m_title; continue; } //TODO i18nTitle must be implemented, currently missing and hence not parsed // quit reading when basic elements are read if (!d->m_identifier.isEmpty() && !d->m_title.isEmpty() && !d->m_i18nTitle.isEmpty() ) { break; } } if (xml.hasError()) { qCritical() << "Error occurred when reading Skeleton XML file:" << path.toLocalFile(); } } xml.clear(); file.close(); } SkeletonResource::SkeletonResource(ResourceManager* resourceManager, Skeleton *skeleton) : ResourceInterface(resourceManager) , d(new SkeletonResourcePrivate(resourceManager)) { d->m_type = ResourceInterface::SkeletonResourceType; d->m_path = skeleton->file(); d->m_identifier = skeleton->id(); d->m_title = skeleton->title(); d->m_skeletonResource = skeleton; } SkeletonResource::~SkeletonResource() { } QString SkeletonResource::identifier() { if (d->m_skeletonResource) { return d->m_skeletonResource->id(); } return d->m_identifier; } QString SkeletonResource::title() { if (d->m_skeletonResource) { return d->m_skeletonResource->title(); } return d->m_title; } QString SkeletonResource::i18nTitle() { if (d->m_skeletonResource) { return d->m_skeletonResource->title(); //TODO } return d->m_i18nTitle; } -ResourceInterface::Type SkeletonResource::type() const -{ - return d->m_type; -} +//ResourceInterface::Type SkeletonResource::type() const +//{ +// return d->m_type; +//} void SkeletonResource::close() { d->m_skeletonResource->deleteLater(); d->m_skeletonResource = nullptr; } void SkeletonResource::sync() { Q_ASSERT(path().isValid()); Q_ASSERT(path().isLocalFile()); Q_ASSERT(!path().isEmpty()); // if resource was never loaded, it cannot be changed if (!d->m_skeletonResource) { qCDebug(ARTIKULATE_LOG) << "Aborting sync, skeleton was not parsed."; return; } // // not writing back if not modified // if (!d->m_skeletonResource->modified()) { // qCDebug(ARTIKULATE_LOG) << "Aborting sync, skeleton was not modified."; // return; // } QDomDocument document; // prepare xml header QDomProcessingInstruction header = document.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\"")); document.appendChild(header); // create main element QDomElement root = document.createElement(QStringLiteral("skeleton")); document.appendChild(root); QDomElement idElement = document.createElement(QStringLiteral("id")); QDomElement titleElement = document.createElement(QStringLiteral("title")); QDomElement descriptionElement = document.createElement(QStringLiteral("description")); idElement.appendChild(document.createTextNode(d->m_skeletonResource->id())); titleElement.appendChild(document.createTextNode(d->m_skeletonResource->title())); descriptionElement.appendChild(document.createTextNode(d->m_skeletonResource->description())); QDomElement unitListElement = document.createElement(QStringLiteral("units")); // create units foreach (Unit *unit, d->m_skeletonResource->unitList()) { QDomElement unitElement = document.createElement(QStringLiteral("unit")); QDomElement unitIdElement = document.createElement(QStringLiteral("id")); QDomElement unitTitleElement = document.createElement(QStringLiteral("title")); QDomElement unitPhraseListElement = document.createElement(QStringLiteral("phrases")); unitIdElement.appendChild(document.createTextNode(unit->id())); unitTitleElement.appendChild(document.createTextNode(unit->title())); // construct phrases foreach (Phrase *phrase, unit->phraseList()) { QDomElement phraseElement = document.createElement(QStringLiteral("phrase")); QDomElement phraseIdElement = document.createElement(QStringLiteral("id")); QDomElement phraseTextElement = document.createElement(QStringLiteral("text")); QDomElement phraseTypeElement = document.createElement(QStringLiteral("type")); phraseIdElement.appendChild(document.createTextNode(phrase->id())); phraseTextElement.appendChild(document.createTextNode(phrase->text())); phraseTypeElement.appendChild(document.createTextNode(phrase->typeString())); phraseElement.appendChild(phraseIdElement); phraseElement.appendChild(phraseTextElement); phraseElement.appendChild(phraseTypeElement); unitPhraseListElement.appendChild(phraseElement); } // construct the unit element unitElement.appendChild(unitIdElement); unitElement.appendChild(unitTitleElement); unitElement.appendChild(unitPhraseListElement); unitListElement.appendChild(unitElement); } root.appendChild(idElement); root.appendChild(titleElement); root.appendChild(descriptionElement); root.appendChild(unitListElement); // write back to file //TODO port to KSaveFile QFile file(path().toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { qCWarning(ARTIKULATE_LOG) << "Unable to open file " << file.fileName() << " in write mode, aborting."; return; } file.write(document.toByteArray()); return; } void SkeletonResource::reload() { qCritical() << "NOT IMPLEMENTED"; } bool SkeletonResource::isOpen() const { return (d->m_skeletonResource != nullptr); } QUrl SkeletonResource::path() const { if (d->m_skeletonResource) { return d->m_skeletonResource->file(); } return d->m_path; } QObject * SkeletonResource::resource() { if (d->m_skeletonResource) { return d->m_skeletonResource; } if (!path().isLocalFile()) { qCWarning(ARTIKULATE_LOG) << "Cannot open skeleton file at " << path().toLocalFile() << ", aborting."; return nullptr; } QXmlSchema schema = loadXmlSchema(QStringLiteral("skeleton")); if (!schema.isValid()) { return nullptr; } QDomDocument document = loadDomDocument(path(), schema); if (document.isNull()) { qCWarning(ARTIKULATE_LOG) << "Could not parse document " << path().toLocalFile() << ", aborting."; return nullptr; } // create skeleton QDomElement root(document.documentElement()); d->m_skeletonResource = new Skeleton(this); d->m_skeletonResource->setFile(d->m_path); d->m_skeletonResource->setId(root.firstChildElement(QStringLiteral("id")).text()); d->m_skeletonResource->setTitle(root.firstChildElement(QStringLiteral("title")).text()); d->m_skeletonResource->setDescription(root.firstChildElement(QStringLiteral("title")).text()); // create units for (QDomElement unitNode = root.firstChildElement(QStringLiteral("units")).firstChildElement(); !unitNode.isNull(); unitNode = unitNode.nextSiblingElement()) { Unit *unit = new Unit(d->m_skeletonResource); unit->setId(unitNode.firstChildElement(QStringLiteral("id")).text()); unit->setCourse(d->m_skeletonResource); unit->setTitle(unitNode.firstChildElement(QStringLiteral("title")).text()); d->m_skeletonResource->addUnit(unit); // create phrases for (QDomElement phraseNode = unitNode.firstChildElement(QStringLiteral("phrases")).firstChildElement(); !phraseNode.isNull(); phraseNode = phraseNode.nextSiblingElement()) { Phrase *phrase = new Phrase(unit); phrase->setId(phraseNode.firstChildElement(QStringLiteral("id")).text()); phrase->setText(phraseNode.firstChildElement(QStringLiteral("text")).text()); phrase->setType(phraseNode.firstChildElement(QStringLiteral("type")).text()); phrase->setUnit(unit); unit->addPhrase(phrase); } } d->m_skeletonResource->setModified(false); return d->m_skeletonResource; } Skeleton * SkeletonResource::skeleton() { return qobject_cast(resource()); } diff --git a/src/core/resources/skeletonresource.h b/src/core/resources/skeletonresource.h index d4dd084..52b7493 100644 --- a/src/core/resources/skeletonresource.h +++ b/src/core/resources/skeletonresource.h @@ -1,105 +1,100 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef SKELETONRESOURCE_H #define SKELETONRESOURCE_H #include "artikulatecore_export.h" #include "resourceinterface.h" #include class SkeletonResourcePrivate; class Skeleton; class ARTIKULATECORE_EXPORT SkeletonResource : public ResourceInterface { Q_OBJECT void course(QString text); public: /** * Create course resource from file. */ explicit SkeletonResource(ResourceManager *resourceManager, const QUrl &path); /** * Create course resource from course. */ explicit SkeletonResource(ResourceManager *resourceManager, Skeleton *skeleton); virtual ~SkeletonResource(); /** * \return unique identifier */ QString identifier() override; /** * \return human readable localized title */ QString title() override; /** * \return human readable title in English */ QString i18nTitle() override; - /** - * \return type of resource - */ - Type type() const override; - /** * \return true if resource is loaded, otherwise false */ bool isOpen() const override; /** * close resource without writing changes back to file */ void close() override; void sync() override; void reload() override; /** * \return path to resource file */ QUrl path() const override; /** * \return reference to the loaded resource * if resource is not open yet, it will be loaded */ QObject * resource() override; /** * \return reference to the loaded skeleton resource * Same behavior as \see resource() but casted to Skeleton */ Skeleton * skeleton(); private: const QScopedPointer d; }; #endif diff --git a/src/core/trainingsession.cpp b/src/core/trainingsession.cpp index 4fadb52..b6269e1 100644 --- a/src/core/trainingsession.cpp +++ b/src/core/trainingsession.cpp @@ -1,302 +1,310 @@ /* * Copyright 2013-2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "trainingsession.h" #include "core/language.h" #include "core/icourse.h" #include "core/unit.h" #include "core/phrase.h" #include "profilemanager.h" #include "learner.h" #include "trainingaction.h" #include "artikulate_debug.h" TrainingSession::TrainingSession(LearnerProfile::ProfileManager *manager, QObject *parent) : QObject(parent) , m_profileManager(manager) , m_course(nullptr) { Q_ASSERT(m_profileManager != nullptr); } ICourse * TrainingSession::course() const { return m_course; } void TrainingSession::setCourse(ICourse *course) { if (!course) { updateTrainingActions(); return; } if (m_course == course) { return; } m_course = course; if (m_course && m_course->unitList().count() > 0) { setUnit(m_course->unitList().constFirst()); } // lazy loading of training data LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->id()); if (!goal) { goal = m_profileManager->registerGoal( LearnerProfile::LearningGoal::Language, course->language()->id(), course->language()->i18nTitle() ); } auto data = m_profileManager->progressValues(m_profileManager->activeProfile(), goal, m_course->id() ); const auto unitList = m_course->unitList(); for (Unit *unit : qAsConst(unitList)) { const auto phraseList = unit->phraseList(); for (Phrase *phrase : qAsConst(phraseList)) { auto iter = data.find(phrase->id()); if (iter != data.end()) { phrase->setProgress(iter.value()); } } } updateTrainingActions(); emit courseChanged(); } Unit * TrainingSession::activeUnit() const { if (auto phrase = activePhrase()) { return phrase->unit(); } return nullptr; } void TrainingSession::setUnit(Unit *unit) { // checking phrases in increasing order ensures that always the first phrase is selected for (int i = 0; i < m_actions.count(); ++i) { for (int j = 0; j < m_actions.at(i)->actions().count(); ++j) { const auto testPhrase = qobject_cast(m_actions.at(i)->actions().at(j))->phrase(); if (unit == testPhrase->unit()) { if (auto action = activeAction()) { action->setChecked(false); } m_indexUnit = i; m_indexPhrase = j; if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); return; } } } } TrainingAction * TrainingSession::activeAction() const { if (m_indexUnit < 0 || m_indexPhrase < 0) { return nullptr; } return qobject_cast(m_actions.at(m_indexUnit)->actions().at(m_indexPhrase)); } Phrase * TrainingSession::activePhrase() const { if (const auto action = activeAction()) { return action->phrase(); } return nullptr; } void TrainingSession::setPhrase(Phrase *phrase) { for (int i = 0; i < m_actions.count(); ++i) { for (int j = 0; j < m_actions.at(i)->actions().count(); ++j) { const auto testPhrase = qobject_cast(m_actions.at(i)->actions().at(j))->phrase(); if (phrase == testPhrase) { if (auto action = activeAction()) { action->setChecked(false); } m_indexUnit = i; m_indexPhrase = j; if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); return; } } } } void TrainingSession::accept() { Q_ASSERT(m_indexUnit >= 0); Q_ASSERT(m_indexPhrase >= 0); if (m_indexUnit < 0 || m_indexPhrase < 0) { return; } auto phrase = activePhrase(); // possibly update goals of learner updateGoal(); phrase->updateProgress(Phrase::Progress::Done); // store training activity LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->language()->id()); m_profileManager->recordProgress(m_profileManager->activeProfile(), goal, m_course->id(), phrase->id(), static_cast(LearnerProfile::ProfileManager::Skip), phrase->progress() ); selectNextPhrase(); } void TrainingSession::skip() { Q_ASSERT(m_indexUnit >= 0); Q_ASSERT(m_indexPhrase >= 0); if (m_indexUnit < 0 || m_indexPhrase < 0) { return; } // possibly update goals of learner updateGoal(); auto phrase = activePhrase(); phrase->updateProgress(Phrase::Progress::Skip); // store training activity LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->language()->id()); m_profileManager->recordProgress(m_profileManager->activeProfile(), goal, m_course->id(), phrase->id(), static_cast(LearnerProfile::ProfileManager::Skip), phrase->progress() ); selectNextPhrase(); } void TrainingSession::selectNextPhrase() { if (auto action = activeAction()) { action->setChecked(false); } // try to find next phrase, otherwise return completed if (m_indexPhrase >= m_actions.at(m_indexUnit)->actions().count() - 1) { qDebug() << "switching to next unit"; if (m_indexUnit >= m_actions.count() - 1) { emit completed(); } else { ++m_indexUnit; m_indexPhrase = 0; } } else { ++m_indexPhrase; } if (auto action = activeAction()) { action->setChecked(true); } emit phraseChanged(); } bool TrainingSession::hasPrevious() const { return m_indexUnit > 0 || m_indexPhrase > 0; } bool TrainingSession::hasNext() const { - return m_indexUnit < m_actions.count() - 1 || m_indexPhrase < m_actions.last()->actions().count() - 1; + if (m_indexUnit < m_actions.count() - 1) { + return true; + } + if (m_actions.constLast()) { + if (m_indexPhrase < m_actions.constLast()->actions().count() - 1) { + return true; + } + } + return false; } void TrainingSession::updateGoal() { if (!m_profileManager) { qCWarning(ARTIKULATE_LOG()) << "No ProfileManager registered, aborting operation"; return; } LearnerProfile::Learner *learner = m_profileManager->activeProfile(); if (!learner) { qCWarning(ARTIKULATE_LOG()) << "No active Learner registered, aborting operation"; return; } LearnerProfile::LearningGoal * goal = m_profileManager->goal( LearnerProfile::LearningGoal::Language, m_course->language()->id()); learner->addGoal(goal); learner->setActiveGoal(goal); } QVector TrainingSession::trainingActions() const { return m_actions; } void TrainingSession::updateTrainingActions() { for (const auto &action : qAsConst(m_actions)) { action->deleteLater(); } m_actions.clear(); if (!m_course) { m_indexUnit = -1; m_indexPhrase = -1; return; } const auto unitList = m_course->unitList(); for (const auto &unit : qAsConst(unitList)) { auto action = new TrainingAction(unit->title()); const auto phraseList = unit->phraseList(); for (const auto &phrase : qAsConst(phraseList)) { if (phrase->sound().isEmpty()) { continue; } action->appendChild(new TrainingAction(phrase, this, unit)); } if (action->hasChildren()) { m_actions.append(action); } else { action->deleteLater(); } } // update indices m_indexUnit = -1; m_indexPhrase = -1; if (m_course->unitList().count() > 0) { m_indexUnit = 0; if (m_course->unitList().constFirst()->phraseList().count() > 0) { m_indexPhrase = 0; } } } diff --git a/src/main.cpp b/src/main.cpp index 50b554d..5e2c084 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,65 +1,68 @@ /* * Copyright 2013-2017 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.h" #include "application.h" #include "version.h" #include #include #include #include "artikulate_debug.h" int main(int argc, char **argv) { KLocalizedString::setApplicationDomain("artikulate"); Application app(argc, argv); + ResourceRepository repository; + repository.reloadCourses(); + app.installResourceRepository(&repository); KAboutData aboutData(QStringLiteral("artikulate"), i18nc("@title Displayed program name", "Artikulate"), ARTIKULATE_VERSION_STRING, i18nc("@title KAboutData: short program description", "Artikulate Pronunciation Trainer"), KAboutLicense::GPL_V2, i18nc("@info:credit", "(c) 2013-2017 The Artikulate Developers"), i18nc("@title Short program description", "Training your pronunciation in a foreign language.") ); aboutData.addAuthor(i18nc("@info:credit Developer name", "Andreas Cord-Landwehr"), i18nc("@info:credit Role", "Original Author"), QStringLiteral("cordlandwehr@kde.org")); aboutData.addAuthor(i18nc("@info:credit Developer name", "Samikshan Bairagya"), i18nc("@info:credit Role", "Developer"), QStringLiteral("samikshan@gmail.com")); aboutData.addAuthor(i18nc("@info:credit Developer name", "Oindrila Gupta"), i18nc("@info:credit Role", "Developer and Course Data")); aboutData.addAuthor(i18nc("@info:credit Developer name", "Magdalena Konkiewicz"), i18nc("@info:credit Role", "Developer and Course Data")); aboutData.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails")); KAboutData::setApplicationData(aboutData); KCrash::initialize(); new MainWindow(); return app.exec(); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 1d2ffdd..61964d9 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,217 +1,206 @@ /* * 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.h" #include "ui/resourcesdialogpage.h" #include "ui/sounddevicedialogpage.h" #include "ui/appearencedialogpage.h" -#include "core/resourcemanager.h" +#include "core/resourcerepository.h" #include "core/trainingsession.h" #include "core/editorsession.h" #include "core/resources/courseresource.h" +#include "application.h" #include "models/languagemodel.h" #include "settings.h" #include "liblearnerprofile/src/profilemanager.h" #include "liblearnerprofile/src/learner.h" #include "libsound/src/outputdevicecontroller.h" #include "artikulate_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace LearnerProfile; MainWindow::MainWindow() : m_actionCollection(new KActionCollection(this, QStringLiteral("artikulate"))) , m_helpMenu(new KHelpMenu) - , m_resourceManager(new ResourceManager(this)) , m_profileManager(new LearnerProfile::ProfileManager(this)) , m_trainingSession(new TrainingSession(m_profileManager, this)) { rootContext()->setContextObject(new KLocalizedContext(this)); // load saved sound settings OutputDeviceController::self().setVolume(Settings::audioOutputVolume()); - // load resources - m_resourceManager->loadLanguageResources(); - if (m_resourceManager->languageResources().count() == 0) { - qFatal("No language resources found, cannot start application."); - } - m_resourceManager->loadCourseResources(); - // create menu setupActions(); // set view - rootContext()->setContextProperty(QStringLiteral("g_resourceManager"), m_resourceManager); rootContext()->setContextProperty(QStringLiteral("g_trainingSession"), m_trainingSession); rootContext()->setContextProperty(QStringLiteral("g_profileManager"), m_profileManager); rootContext()->setContextProperty(QStringLiteral("kcfg_UseContributorResources"), Settings::useCourseRepository()); rootContext()->setContextProperty(QStringLiteral("kcfg_ShowMenuBar"), Settings::showMenuBar()); // set starting screen load(QUrl(QStringLiteral("qrc:/artikulate/qml/Main.qml"))); // settings from kcfg values // updateTrainingPhraseFont(); //FIXME deactivated while porting // create training profile if none exists: if (!m_profileManager->activeProfile()) { m_profileManager->addProfile(i18n("Unnamed Identity")); } // connect to QML signals; - connect(rootObjects().constFirst(), SIGNAL(triggerSettingsDialog()), - this, SLOT(showSettingsDialog())); + connect(rootObjects().constFirst(), SIGNAL(ghnsCourseDataStatusChanged()), + this, SLOT(updateCourseResources())); connect(rootObjects().constFirst(), SIGNAL(triggerAction(QString)), this, SLOT(triggerAction(QString))); connect(rootObjects().constFirst(), SIGNAL(switchMenuBarVisibility()), this, SLOT(switchMenuBarVisibility())); // set font for the phrase in trainer to default from kcfg file QObject *phraseText = rootObjects().constFirst()->findChild(QStringLiteral("phraseText")); if (phraseText) { phraseText->setProperty("font", Settings::trainingPhraseFont()); } } MainWindow::~MainWindow() { // save current settings for case of closing Settings::self()->save(); m_profileManager->sync(); } -ResourceManager * MainWindow::resourceManager() const -{ - return m_resourceManager; -} - KActionCollection * MainWindow::actionCollection() { return m_actionCollection; } void MainWindow::setupActions() { QAction *settingsAction = new QAction(i18nc("@item:inmenu", "Configure Artikulate"), this); connect(settingsAction, &QAction::triggered, this, &MainWindow::showSettingsDialog); actionCollection()->addAction(QStringLiteral("settings"), settingsAction); settingsAction->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); QAction *configLearnerProfileAction = new QAction(i18nc("@item:inmenu", "Learner Profile"), this); connect(configLearnerProfileAction, &QAction::triggered, this, &MainWindow::configLearnerProfile); actionCollection()->addAction(QStringLiteral("config_learner_profile"), configLearnerProfileAction); configLearnerProfileAction->setIcon(QIcon::fromTheme(QStringLiteral("user-identity"))); KStandardAction::helpContents(m_helpMenu, SLOT(appHelpActivated()), actionCollection()); KStandardAction::reportBug(m_helpMenu, SLOT(reportBug()), actionCollection()); KStandardAction::aboutKDE(m_helpMenu, SLOT(aboutKDE()), actionCollection()); KStandardAction::aboutApp(m_helpMenu, SLOT(aboutApplication()), actionCollection()); KStandardAction::quit(qApp, SLOT(quit()), actionCollection()); } void MainWindow::showSettingsDialog() { if (KConfigDialog::showDialog(QStringLiteral("settings"))) { return; } QPointer dialog = new KConfigDialog(nullptr, QStringLiteral("settings"), Settings::self()); - ResourcesDialogPage *resourceDialog = new ResourcesDialogPage(m_resourceManager); SoundDeviceDialogPage *soundDialog = new SoundDeviceDialogPage(); AppearenceDialogPage *appearenceDialog = new AppearenceDialogPage(); - resourceDialog->loadSettings(); +// 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, SIGNAL(settingsChanged(const QString&)), resourceDialog, SLOT(loadSettings())); // connect(dialog, SIGNAL(settingsChanged(const QString&)), soundDialog, SLOT(loadSettings())); - connect(dialog.data(), &QDialog::accepted, resourceDialog, &ResourcesDialogPage::saveSettings); connect(dialog.data(), &QDialog::accepted, soundDialog, &SoundDeviceDialogPage::saveSettings); connect(dialog.data(), &QDialog::accepted, appearenceDialog, &AppearenceDialogPage::saveSettings); connect(dialog.data(), &QDialog::accepted, this, &MainWindow::updateTrainingPhraseFont); connect(dialog.data(), &QDialog::accepted, this, &MainWindow::updateKcfgUseContributorResources); connect(dialog.data(), &QDialog::finished, soundDialog, &SoundDeviceDialogPage::stopPlaying); connect(dialog.data(), &QDialog::finished, soundDialog, &SoundDeviceDialogPage::stopRecord); dialog->exec(); } +void MainWindow::updateCourseResources() +{ + artikulateApp->resourceRepository()->reloadCourses(); +} + void MainWindow::updateTrainingPhraseFont() { QObject *phraseText = rootObjects().constFirst()->findChild(QStringLiteral("phraseText")); if (!phraseText) { qCDebug(ARTIKULATE_LOG) << "no phraseText context object found, aborting"; return; } phraseText->setProperty("font", Settings::trainingPhraseFont()); } void MainWindow::updateKcfgUseContributorResources() { rootContext()->setContextProperty(QStringLiteral("kcfg_UseContributorResources"), Settings::useCourseRepository()); } void MainWindow::configLearnerProfile() { qCritical() << "Not implemented"; //FIXME } void MainWindow::triggerAction(const QString &actionName) { QAction * action = actionCollection()->action(actionName); if (action) { action->trigger(); } else { qCritical() << "Action is not registered:" << actionName; } } void MainWindow::switchMenuBarVisibility() { Settings::setShowMenuBar(!Settings::showMenuBar()); rootContext()->setContextProperty(QStringLiteral("kcfg_ShowMenuBar"), Settings::showMenuBar()); } bool MainWindow::queryClose() { Settings::self()->save(); // FIXME make sure all learner data is written to database return true; } diff --git a/src/mainwindow.h b/src/mainwindow.h index eae9107..47dfe47 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,72 +1,72 @@ /* * 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 MAINWINDOW_H #define MAINWINDOW_H #include #include -#include "core/resourcemanager.h" +#include "core/resourcerepository.h" class TrainingSession; class KHelpMenu; namespace LearnerProfile { class ProfileManager; } class MainWindow : public QQmlApplicationEngine { Q_OBJECT public: /** * Default Constructor */ MainWindow(); /** * Default Destructor */ - virtual ~MainWindow(); + ~MainWindow() override; - ResourceManager * resourceManager() const; + const IResourceRepository * resourceRepository() const; KActionCollection * actionCollection(); void setupActions(); bool queryClose(); public Q_SLOTS: void showSettingsDialog(); + void updateCourseResources(); void updateTrainingPhraseFont(); void updateKcfgUseContributorResources(); void configLearnerProfile(); void switchMenuBarVisibility(); void triggerAction(const QString &); private: KActionCollection *m_actionCollection; KHelpMenu *m_helpMenu; - ResourceManager *m_resourceManager; LearnerProfile::ProfileManager *m_profileManager; TrainingSession *m_trainingSession; }; #endif diff --git a/src/models/coursemodel.cpp b/src/models/coursemodel.cpp index d4045b7..f9c6968 100644 --- a/src/models/coursemodel.cpp +++ b/src/models/coursemodel.cpp @@ -1,213 +1,220 @@ /* * 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 "coursemodel.h" #include "core/language.h" #include "core/course.h" -#include "core/resourcemanager.h" +#include "core/iresourcerepository.h" #include "core/resources/courseresource.h" #include #include "artikulate_debug.h" #include #include +#include "application.h" CourseModel::CourseModel(QObject *parent) : QAbstractListModel(parent) - , m_resourceManager(nullptr) + , m_resourceRepository(nullptr) , m_language(nullptr) , m_signalMapper(new QSignalMapper(this)) { connect(m_signalMapper, static_cast(&QSignalMapper::mapped), this, &CourseModel::emitCourseChanged); connect(this, &CourseModel::resourceManagerChanged, this, &CourseModel::rowCountChanged); connect(this, &CourseModel::languageChanged, this, &CourseModel::rowCountChanged); + + setResourceRepository(artikulateApp->resourceRepository()); } QHash< int, QByteArray > CourseModel::roleNames() const { QHash roles; roles[TitleRole] = "title"; roles[DescriptionRole] = "description"; roles[IdRole] = "id"; roles[LanguageRole] = "language"; roles[DataRole] = "dataRole"; return roles; } -void CourseModel::setResourceManager(ResourceManager *resourceManager) +void CourseModel::setResourceRepository(IResourceRepository *resourceRepository) { - if (m_resourceManager == resourceManager) { + if (resourceRepository == nullptr) { + qCWarning(ARTIKULATE_CORE()) << "setting resource repository to nullptr, this shall never happen"; + } + Q_ASSERT(resourceRepository != nullptr); + + if (m_resourceRepository == resourceRepository) { return; } beginResetModel(); - if (m_resourceManager) { - m_resourceManager->disconnect(this); + if (m_resourceRepository) { + disconnect(m_resourceRepository, &IResourceRepository::courseAboutToBeAdded, this, &CourseModel::onCourseAboutToBeAdded); + disconnect(m_resourceRepository, &IResourceRepository::courseAdded, this, &CourseModel::onCourseAdded); + disconnect(m_resourceRepository, &IResourceRepository::courseAboutToBeRemoved, this, &CourseModel::onCourseAboutToBeRemoved); } - m_resourceManager = resourceManager; - m_resources.clear(); - if (m_resourceManager) { - connect(m_resourceManager, &ResourceManager::courseResourceAboutToBeAdded, - this, &CourseModel::onCourseResourceAboutToBeAdded); - connect(m_resourceManager, &ResourceManager::courseResourceAdded, - this, &CourseModel::onCourseResourceAdded); - connect(m_resourceManager, &ResourceManager::courseResourceAboutToBeRemoved, - this, &CourseModel::onCourseResourceAboutToBeRemoved); + m_resourceRepository = resourceRepository; + m_courses.clear(); + if (m_resourceRepository) { + connect(m_resourceRepository, &IResourceRepository::courseAboutToBeAdded, this, &CourseModel::onCourseAboutToBeAdded); + connect(m_resourceRepository, &IResourceRepository::courseAdded, this, &CourseModel::onCourseAdded); + connect(m_resourceRepository, &IResourceRepository::courseAboutToBeRemoved, this, &CourseModel::onCourseAboutToBeRemoved); } - if (m_resourceManager) { - m_resources = m_resourceManager->courseResources(m_language); + if (m_resourceRepository) { + m_courses = m_resourceRepository->courses(m_language); } endResetModel(); emit resourceManagerChanged(); } -ResourceManager * CourseModel::resourceManager() const +IResourceRepository * CourseModel::resourceRepository() const { - return m_resourceManager; + return m_resourceRepository; } Language * CourseModel::language() const { return m_language; } void CourseModel::setLanguage(Language *language) { beginResetModel(); m_language = language; - m_resources = m_resourceManager->courseResources(m_language); + m_courses = m_resourceRepository->courses(m_language); emit languageChanged(); endResetModel(); emit rowCountChanged(); } QVariant CourseModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } - if (index.row() >= m_resources.count()) { + if (index.row() >= m_courses.count()) { return QVariant(); } - Course * const course = m_resources.at(index.row())->course(); + ICourse * const course = m_courses.at(index.row()); switch(role) { case Qt::DisplayRole: return !course->title().isEmpty()? QVariant(course->title()): QVariant(i18nc("@item:inlistbox:", "unknown")); case Qt::ToolTipRole: return QVariant(course->title()); case TitleRole: return course->title(); case DescriptionRole: return course->description(); case IdRole: return course->id(); case ContributerResourceRole: - return m_resources.at(index.row())->isContributorResource(); + return false;// m_resources.at(index.row())->isContributorResource();//FIXME case LanguageRole: return QVariant::fromValue(course->language()); case DataRole: return QVariant::fromValue(course); default: return QVariant(); } } int CourseModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } - return m_resources.count(); + return m_courses.count(); } -void CourseModel::onCourseResourceAboutToBeAdded(CourseResource *resource, int index) +void CourseModel::onCourseAboutToBeAdded(ICourse *course, int index) { Q_UNUSED(index); - beginInsertRows(QModelIndex(), m_resources.count(), m_resources.count()); - m_resources.append(resource); + beginInsertRows(QModelIndex(), m_courses.count(), m_courses.count()); + m_courses.append(course); - connect(resource->course(), SIGNAL(titleChanged()), m_signalMapper, SLOT(map())); + connect(course, SIGNAL(titleChanged()), m_signalMapper, SLOT(map())); //TODO add missing signals } -void CourseModel::onCourseResourceAdded() +void CourseModel::onCourseAdded() { updateMappings(); endInsertRows(); emit rowCountChanged(); } -void CourseModel::onCourseResourceAboutToBeRemoved(int index) +void CourseModel::onCourseAboutToBeRemoved(int index) { - if (index >= m_resourceManager->courseResources(m_language).count()) { + if (index >= m_resourceRepository->courses(m_language).count()) { return; } - CourseResource *originalResource = m_resourceManager->courseResources(m_language).at(index); - int modelIndex = m_resources.indexOf(originalResource); + ICourse *originalCourse = m_resourceRepository->courses(m_language).at(index); + int modelIndex = m_courses.indexOf(originalCourse); if (modelIndex == -1) { qCWarning(ARTIKULATE_LOG) << "Cannot remove course from model, not registered"; return; } beginRemoveRows(QModelIndex(), modelIndex, modelIndex); - m_resources.removeAt(modelIndex); + m_courses.removeAt(modelIndex); endRemoveRows(); emit rowCountChanged(); } void CourseModel::emitCourseChanged(int row) { emit courseChanged(row); emit dataChanged(index(row, 0), index(row, 0)); } QVariant CourseModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) { return QVariant(); } if (orientation == Qt::Vertical) { return QVariant(section + 1); } return QVariant(i18nc("@title:column", "Course")); } void CourseModel::updateMappings() { - int courses = m_resources.count(); + int courses = m_courses.count(); for (int i = 0; i < courses; i++) { - m_signalMapper->setMapping(m_resources.at(i)->course(), i); + m_signalMapper->setMapping(m_courses.at(i), i); } } QVariant CourseModel::course(int row) const { return data(index(row, 0), CourseModel::DataRole); } diff --git a/src/models/coursemodel.h b/src/models/coursemodel.h index bf5bba3..f7d6f04 100644 --- a/src/models/coursemodel.h +++ b/src/models/coursemodel.h @@ -1,88 +1,88 @@ /* * 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 COURSEMODEL_H #define COURSEMODEL_H #include -class ResourceManager; -class Course; -class CourseResource; +class IResourceRepository; +class ICourse; class Language; class QSignalMapper; class CourseModel : public QAbstractListModel { Q_OBJECT - Q_PROPERTY(ResourceManager *resourceManager READ resourceManager WRITE setResourceManager NOTIFY resourceManagerChanged) Q_PROPERTY(Language *language READ language WRITE setLanguage NOTIFY languageChanged) Q_PROPERTY(int size READ rowCount NOTIFY rowCountChanged) public: enum courseRoles { TitleRole = Qt::UserRole + 1, DescriptionRole, IdRole, ContributerResourceRole, LanguageRole, DataRole }; explicit CourseModel(QObject *parent = nullptr); ~CourseModel() override = default; /** * Reimplemented from QAbstractListModel::roleNames() */ QHash roleNames() const override; - void setResourceManager(ResourceManager *resourceManager); - ResourceManager * resourceManager() const; + IResourceRepository * resourceRepository() const; void setLanguage(Language *language); Language * language() const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; Q_INVOKABLE QVariant course(int index) const; +protected: + void setResourceRepository(IResourceRepository *resourceRepository); + Q_SIGNALS: void courseChanged(int index); void resourceManagerChanged(); void languageChanged(); void rowCountChanged(); private Q_SLOTS: - void onCourseResourceAboutToBeAdded(CourseResource *resource, int index); - void onCourseResourceAdded(); - void onCourseResourceAboutToBeRemoved(int index); + void onCourseAboutToBeAdded(ICourse *resource, int index); + void onCourseAdded(); + void onCourseAboutToBeRemoved(int index); void emitCourseChanged(int row); private: /** * Updates internal mappings of course signals. */ void updateMappings(); - ResourceManager *m_resourceManager; + IResourceRepository *m_resourceRepository; Language *m_language; - QList m_resources; + QVector m_courses; QSignalMapper *m_signalMapper; }; #endif // COURSEMODEL_H diff --git a/src/models/languageresourcemodel.cpp b/src/models/languageresourcemodel.cpp index b9ce80b..a39a1a3 100644 --- a/src/models/languageresourcemodel.cpp +++ b/src/models/languageresourcemodel.cpp @@ -1,277 +1,277 @@ /* * 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 "languageresourcemodel.h" #include "core/language.h" #include "core/course.h" #include "core/resourcemanager.h" #include "core/resources/languageresource.h" #include "core/resources/courseresource.h" #include #include #include #include "artikulate_debug.h" LanguageResourceModel::LanguageResourceModel(QObject* parent) : QAbstractListModel(parent) , m_resourceManager(nullptr) , m_view(LanguageModel::NonEmptyGhnsOnlyLanguages) , m_signalMapper(new QSignalMapper(this)) { connect(m_signalMapper, SIGNAL(mapped(int)), SLOT(emitLanguageChanged(int))); } QHash< int, QByteArray > LanguageResourceModel::roleNames() const { QHash roles; roles[TitleRole] = "title"; roles[I18nTitleRole] = "i18nTitle"; roles[IdRole] = "id"; roles[DataRole] = "dataRole"; roles[CourseNumberRole] = "courseNumberRole"; return roles; } void LanguageResourceModel::setResourceManager(ResourceManager *resourceManager) { if (m_resourceManager == resourceManager) { return; } beginResetModel(); if (m_resourceManager) { m_resourceManager->disconnect(this); } m_resourceManager = resourceManager; if (m_resourceManager) { connect(m_resourceManager, &ResourceManager::languageResourceAboutToBeAdded, this, &LanguageResourceModel::onLanguageResourceAboutToBeAdded); connect(m_resourceManager, &ResourceManager::languageResourceAdded, this, &LanguageResourceModel::onLanguageResourceAdded); connect(m_resourceManager, &ResourceManager::languageResourceAboutToBeRemoved, this, &LanguageResourceModel::onLanguageResourceAboutToBeRemoved); connect(m_resourceManager, &ResourceManager::languageResourceRemoved, this, &LanguageResourceModel::onLanguageResourceRemoved); connect(m_resourceManager, &ResourceManager::languageCoursesChanged, this, &LanguageResourceModel::updateDisplayedLanguages); } updateResources(); endResetModel(); emit resourceManagerChanged(); } ResourceManager * LanguageResourceModel::resourceManager() const { return m_resourceManager; } QVariant LanguageResourceModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } if (index.row() >= m_resources.count()) { return QVariant(); } Language * const language = m_resources.at(index.row())->language(); switch(role) { case Qt::DisplayRole: return !language->title().isEmpty() ? QVariant(language->title()): QVariant(i18nc("@item:inlistbox:", "unknown")); case Qt::ToolTipRole: return QVariant(language->title()); case TitleRole: return language->title(); case I18nTitleRole: return language->i18nTitle(); case IdRole: return language->id(); case DataRole: return QVariant::fromValue(language); case CourseNumberRole: return m_resources.count(); default: return QVariant(); } } int LanguageResourceModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { return 0; } return m_resources.count(); } void LanguageResourceModel::onLanguageResourceAboutToBeAdded(LanguageResource *resource, int index) { if (!displayResource(resource)) { return; } beginInsertRows(QModelIndex(), index, index); m_resources.append(resource); connect(resource->language(), SIGNAL(titleChanged()), m_signalMapper, SLOT(map())); connect(resource->language(), SIGNAL(phonemesChanged()), m_signalMapper, SLOT(map())); connect(resource->language(), SIGNAL(phonemeGroupsChanged()), m_signalMapper, SLOT(map())); } void LanguageResourceModel::onLanguageResourceAdded() { updateMappings(); endInsertRows(); } void LanguageResourceModel::onLanguageResourceAboutToBeRemoved(int index) { if (!m_resourceManager) { return; } LanguageResource *originalResource = m_resourceManager->languageResources().at(index); int modelIndex = m_resources.indexOf(originalResource); if (modelIndex == -1) { qCWarning(ARTIKULATE_LOG) << "Cannot remove language from model, not registered"; return; } beginRemoveRows(QModelIndex(), modelIndex, modelIndex); originalResource->disconnect(m_signalMapper); m_resources.removeAt(modelIndex); } void LanguageResourceModel::onLanguageResourceRemoved() { endRemoveRows(); } void LanguageResourceModel::emitLanguageChanged(int row) { emit languageChanged(row); emit dataChanged(index(row, 0), index(row, 0)); } QVariant LanguageResourceModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) { return QVariant(); } if (orientation == Qt::Vertical) { return QVariant(section + 1); } return QVariant(i18nc("@title:column", "Language")); } void LanguageResourceModel::setView(LanguageModel::LanguageResourceView view) { if (m_view == view) { return; } beginResetModel(); m_view = view; updateResources(); endResetModel(); } void LanguageResourceModel::updateDisplayedLanguages() { beginResetModel(); updateResources(); endResetModel(); } LanguageModel::LanguageResourceView LanguageResourceModel::view() const { return m_view; } bool LanguageResourceModel::displayResource(LanguageResource* resource) const { if (m_view == LanguageModel::AllLanguages) { return true; } // otherwise compute data needed for decision QList courses = m_resourceManager->courseResources(resource->language()); int contribCount = 0; if (m_view == LanguageModel::NonEmptyLanguages && courses.count() > 0) { return true; } // compute data for determining whether language shall be shown or not - foreach (CourseResource *course, courses) { - if (course->isContributorResource()) { - ++contribCount; - } - } +// foreach (CourseResource *course, courses) { //FIXME +// if (course->isContributorResource()) { +// ++contribCount; +// } +// } if (m_view == LanguageModel::NonEmptyContributorOnlyResources && contribCount > 0) { return true; } if (m_view == LanguageModel::NonEmptyGhnsOnlyLanguages && courses.count() - contribCount > 0) { return true; } return false; } void LanguageResourceModel::updateResources() { if (!m_resourceManager) { return; } m_resources.clear(); QList resources = m_resourceManager->languageResources(); foreach (LanguageResource *language, resources) { if (displayResource(language)) { m_resources.append(language); } } updateMappings(); } void LanguageResourceModel::updateMappings() { int languages = m_resources.count(); for (int i = 0; i < languages; i++) { m_signalMapper->setMapping(m_resources.at(i), i); } } diff --git a/src/qml/DownloadPage.qml b/src/qml/DownloadPage.qml index d21d9ff..b4d749b 100644 --- a/src/qml/DownloadPage.qml +++ b/src/qml/DownloadPage.qml @@ -1,122 +1,128 @@ /* * Copyright 2018 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.1 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.1 as QQC2 import org.kde.kirigami 2.0 as Kirigami import org.kde.newstuff 1.0 as KNS Kirigami.Page { id: root + + /** + * emitted whenever GHNS resources changed + */ + signal statusChanged(); + title: i18n("Download Training Material") background: Rectangle { color: "#ffffff" } Component { id: courseDownloadItem Kirigami.AbstractListItem { id: listItem height: 50 width: parent.width text: model.name readonly property var status: model.status onStatusChanged: { - g_resourceManager.loadCourseResources(); + root.statusChanged(); } checkable: false RowLayout { id: layout spacing: Kirigami.Units.smallSpacing*2 Kirigami.Icon { height: Kirigami.Units.iconSizes.smallMedium; width: height; SequentialAnimation on opacity { loops: Animation.Infinite; running: model.status == KNS.ItemsModel.InstallingStatus || model.status == KNS.ItemsModel.UpdatingStatus NumberAnimation { to: 0; duration: 500; } NumberAnimation { to: 1; duration: 500; } onRunningChanged: { if (!running) parent.opacity = 1; } } source: { // use complete list of KNS status messages if (model.status == KNS.ItemsModel.InvalidStatus) return "emblem-error"; if (model.status == KNS.ItemsModel.DownloadableStatus) return "vcs-added"; if (model.status == KNS.ItemsModel.InstalledStatus) return "vcs-normal"; if (model.status == KNS.ItemsModel.UpdateableStatus) return "vcs-update-required"; if (model.status == KNS.ItemsModel.DeletedStatus) return "vcs-added"; if (model.status == KNS.ItemsModel.InstallingStatus) return "vcs-locally-modified"; if (model.status == KNS.ItemsModel.UpdatingStatus) return "vcs-locally-modified"; return "emblem-error"; } } QQC2.Label { id: labelItem Layout.fillWidth: true text: listItem.text color: layout.indicateActiveFocus && (listItem.highlighted || listItem.checked || listItem.pressed) ? listItem.activeTextColor : listItem.textColor elide: Text.ElideRight font: listItem.font } QQC2.Button { visible: (model.status == KNS.ItemsModel.UpdateableStatus) ? true : false; text: i18n("update") onClicked: newStuffModel.installItem(model.index) } QQC2.Button { visible: (model.status == KNS.ItemsModel.DownloadableStatus || model.status == KNS.ItemsModel.DeletedStatus) ? true : false; text: i18n("install") onClicked: newStuffModel.installItem(model.index) } QQC2.Button { visible: (model.status == KNS.ItemsModel.InstalledStatus || model.status == KNS.ItemsModel.UpdateableStatus) ? true : false; text: i18n("remove") onClicked: newStuffModel.uninstallItem(model.index) } } } } ColumnLayout { ListView { id: listView width: root.width - 40 height: 50 * listView.count delegate: courseDownloadItem model: KNS.ItemsModel { id: newStuffModel; engine: newStuffEngine.engine; } KNS.Engine { id: newStuffEngine; configFile: ":/artikulate/config/artikulate.knsrc"; onMessage: console.log("KNS Message: " + message); onIdleMessage: console.log("KNS Idle: " + message); onBusyMessage: console.log("KNS Busy: " + message); onErrorMessage: console.log("KNS Error: " + message); } } } } diff --git a/src/qml/Main.qml b/src/qml/Main.qml index f44f4f0..a31b25b 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -1,75 +1,76 @@ /* * Copyright 2013-2017 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.5 import QtQuick.Controls 2.0 as QQC2 import org.kde.kirigami 2.0 as Kirigami2 import artikulate 1.0 Kirigami2.ApplicationWindow { id: root function changePage(pageItem) { root.pageStack.clear(); root.pageStack.push(pageItem); root.pageStack.push(pageItem); } globalDrawer: ArtikulateDrawer { pageStack: root.pageStack } contextDrawer: Kirigami2.OverlayDrawer { id: contextDrawer } + signal ghnsCourseDataStatusChanged(); signal triggerSettingsDialog(); signal triggerAction(string actionName); signal switchMenuBarVisibility(); property Learner learner: g_profileManager.activeProfile - property ResourceManager resourceManager: g_resourceManager CourseModel { id: availableCourseModel - resourceManager: g_resourceManager } pageStack.initialPage: welcomePageComponent pageStack.globalToolBar.style: Kirigami2.ApplicationHeaderStyle.Titles // pages Component { id: welcomePageComponent WelcomePage { } } Component { id: trainingPageComponent TrainingPage { } } Component { id: profileSettingsPageComponent ProfileSettingsPage { } } Component { id: downloadPageComponent - DownloadPage { } + DownloadPage { + onStatusChanged: root.ghnsCourseDataStatusChanged() + } } } diff --git a/src/qml/WelcomePage.qml b/src/qml/WelcomePage.qml index 6d97752..3943db1 100644 --- a/src/qml/WelcomePage.qml +++ b/src/qml/WelcomePage.qml @@ -1,86 +1,85 @@ /* * Copyright 2015-2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.1 import QtQuick.Controls 2.1 as QQC2 import QtQuick.Layouts 1.3 import org.kde.kirigami 2.4 as Kirigami import artikulate 1.0 Kirigami.ScrollablePage { id: root title: i18n("Welcome to Artikulate") Kirigami.CardsListView { id: listView width: root.width - 40 model: CourseModel { id: courseModel - resourceManager: g_resourceManager } delegate: Kirigami.AbstractCard { contentItem: Item { implicitWidth: delegateLayout.implicitWidth implicitHeight: delegateLayout.implicitHeight GridLayout { id: delegateLayout anchors { left: parent.left top: parent.top right: parent.right } rowSpacing: Kirigami.Units.largeSpacing columnSpacing: Kirigami.Units.largeSpacing columns: width > Kirigami.Units.gridUnit * 20 ? 4 : 2 Kirigami.Icon { source: "language-artikulate" Layout.fillHeight: true Layout.maximumHeight: Kirigami.Units.iconSizes.huge Layout.preferredWidth: height } ColumnLayout { Kirigami.Heading { level: 2 text: model.language.title + " / " + model.title } Kirigami.Separator { Layout.fillWidth: true } QQC2.Label { Layout.fillWidth: true wrapMode: Text.WordWrap text: model.description } } QQC2.Button { Layout.alignment: Qt.AlignRight|Qt.AlignVCenter Layout.columnSpan: 2 text: qsTr("Start Training") onClicked: { showPassiveNotification("Starting training session for course " + model.title + "."); g_trainingSession.course = model.dataRole } } } } } } }