diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 767e47d..8bb6f37 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,122 +1,142 @@ ### # Copyright 2013-2019 Andreas Cord-Landwehr # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ### include_directories( ../src/ ../ ${CMAKE_CURRENT_BINARY_DIR} ) # copy test data file(COPY testdata/courses/de.xml DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/courses/de/) # copy test files file(COPY testdata/courses/fr.xml DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/courses/fr/) # copy test files file(COPY testdata/contributorrepository/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/contributorrepository/) # copy test files set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) # repository tests set(TestResourceRepository_SRCS resourcerepository/test_resourcerepository.cpp) qt5_add_resources(TestResourceRepository_SRCS ../data/languages.qrc) add_executable(test_resourcerepository ${TestResourceRepository_SRCS}) target_link_libraries(test_resourcerepository artikulatecore Qt5::Test ) add_test(test_resourcerepository test_resourcerepository) ecm_mark_as_test(test_resourcerepository) + # integration tests for iresource repository interface derived classes set(TestIResourceRepository_SRCS iresourcerepository_integration/test_iresourcerepository.cpp) qt5_add_resources(TestIResourceRepository_SRCS ../data/languages.qrc) add_executable(test_iresourcerepository_integration ${TestIResourceRepository_SRCS}) target_link_libraries(test_iresourcerepository_integration artikulatecore Qt5::Test ) add_test(test_iresourcerepository_integration test_iresourcerepository_integration) ecm_mark_as_test(test_iresourcerepository_integration) + # training session tests set(TestTrainingSession_SRCS trainingsession/test_trainingsession.cpp) add_executable(test_trainingsession ${TestTrainingSession_SRCS}) target_link_libraries(test_trainingsession artikulatecore Qt5::Test ) add_test(test_trainingsession test_trainingsession) ecm_mark_as_test(test_trainingsession) + +# editor session tests +set(TestEditorSession_SRCS + editorsession/test_editorsession.cpp + editorsession/editablerepositorystub.cpp +) +add_executable(test_editorsession ${TestEditorSession_SRCS}) +target_link_libraries(test_editorsession + artikulatecore + Qt5::Test +) +add_test(test_editorsession test_editorsession) +ecm_mark_as_test(test_editorsession) + + # test course resource class set(TestCourseResource_SRCS courseresource/test_courseresource.cpp courseresource/resourcerepositorystub.cpp ) qt5_add_resources(TestCourseResource_SRCS ../data/languages.qrc) add_executable(test_courseresource ${TestCourseResource_SRCS} ) target_link_libraries(test_courseresource artikulatecore Qt5::Test ) add_test(test_courseresource test_courseresource) ecm_mark_as_test(test_courseresource) + # test skeleton resource class set(TestSkeletonResource_SRCS skeletonresource/test_skeletonresource.cpp skeletonresource/resourcerepositorystub.cpp ) qt5_add_resources(TestSkeletonResource_SRCS ../data/languages.qrc) add_executable(test_skeletonresource ${TestSkeletonResource_SRCS} ) target_link_libraries(test_skeletonresource artikulatecore Qt5::Test ) add_test(test_skeletonresource test_skeletonresource) ecm_mark_as_test(test_skeletonresource) + # test editable course resource class set(TestEditableCourseResource_SRCS editablecourseresource/test_editablecourseresource.cpp editablecourseresource/resourcerepositorystub.cpp ) qt5_add_resources(TestEditableCourseResource_SRCS ../data/languages.qrc) qt5_add_resources(TestEditableCourseResource_SRCS testdata/testdata.qrc) add_executable(test_editablecourseresource ${TestEditableCourseResource_SRCS} ) target_link_libraries(test_editablecourseresource artikulatecore Qt5::Test ) add_test(test_editablecourseresource test_editablecourseresource) ecm_mark_as_test(test_editablecourseresource) + # basic tests language files (input/output) set(TestLanguageFiles_SRCS testlanguagefiles.cpp) add_executable(TestLanguageFiles ${TestLanguageFiles_SRCS} ) target_link_libraries(TestLanguageFiles artikulatecore Qt5::Test ) add_test(TestLanguageFiles TestLanguageFiles) ecm_mark_as_test(TestLanguageFiles) diff --git a/autotests/editorsession/editablerepositorystub.cpp b/autotests/editorsession/editablerepositorystub.cpp new file mode 100644 index 0000000..3eec259 --- /dev/null +++ b/autotests/editorsession/editablerepositorystub.cpp @@ -0,0 +1,24 @@ +/* + * 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 "editablerepositorystub.h" + +// define one virtual method out of line to pin EditableRepositoryStub to this translation unit +EditableRepositoryStub::~EditableRepositoryStub() = default; diff --git a/autotests/editorsession/editablerepositorystub.h b/autotests/editorsession/editablerepositorystub.h new file mode 100644 index 0000000..b79bb76 --- /dev/null +++ b/autotests/editorsession/editablerepositorystub.h @@ -0,0 +1,87 @@ +/* + * Copyright 2019 Andreas Cord-Landwehr + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EDITABLEREPOSITORYSTUB_H +#define EDITABLEREPOSITORYSTUB_H + +#include "core/ieditablerepository.h" +#include "core/ieditablecourse.h" +#include +#include + +class Language; + +/** + * @brief The EditableRepositoryStub is simple sub class only for testing + */ +class EditableRepositoryStub : public IEditableRepository +{ + Q_OBJECT +public: + EditableRepositoryStub(QVector courses) + : m_courses{ courses } + { + } + ~EditableRepositoryStub() override; + QString storageLocation() const override + { + return QString(); + } + QVector editableCourses() const override + { + return m_courses; + } + QVector courses() const override + { + QVector courses; + for (auto course : m_courses) { + courses.push_back(course); + } + return courses; + } + QVector courses(Language *language) const override + { + Q_UNUSED(language); + return QVector(); + } + IEditableCourse * editableCourse(Language *language, int index) const override + { + Q_UNUSED(language); + Q_UNUSED(index); + return nullptr; + } + void reloadCourses() override + { + // do nothing + } + QVector languages() const override + { + return QVector(); + } +Q_SIGNALS: + void courseAboutToBeAdded(ICourse*,int) override; + void courseAdded() override; + void courseAboutToBeRemoved(int) override; + void courseRemoved() override; +private: + QVector m_courses; +}; + +#endif diff --git a/autotests/editorsession/test_editorsession.cpp b/autotests/editorsession/test_editorsession.cpp new file mode 100644 index 0000000..bd8475b --- /dev/null +++ b/autotests/editorsession/test_editorsession.cpp @@ -0,0 +1,138 @@ +/* + * Copyright 2019 Andreas Cord-Landwehr + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "test_editorsession.h" +#include "editablerepositorystub.h" +#include "src/core/editorsession.h" +#include "src/core/icourse.h" +#include "src/core/ieditablecourse.h" +#include "src/core/ieditablerepository.h" +#include "src/core/language.h" +#include "src/core/unit.h" +#include +#include + +class EditableCourseStub : public IEditableCourse +{ +public: + EditableCourseStub(Language *language, QVector units) + : m_language(language) + , m_units(units) + { + } + ~EditableCourseStub() override; + + QString id() const override + { + return m_id; + } + void setId(QString id) override + { + m_id = id; + emit idChanged(); + } + QString foreignId() const override + { + return m_foreignId; + } + void setForeignId(QString id) override + { + m_foreignId = id; + } + QString title() const override + { + return m_title; + } + void setTitle(QString title) override + { + m_title = title; + emit titleChanged(); + } + QString i18nTitle() const override + { + return m_i18nTitle; + } + void setI18nTitle(QString title) override + { + m_i18nTitle = title; + } + QString description() const override + { + return m_description; + } + void setDescription(QString description) override + { + m_description = description; + emit descriptionChanged(); + } + Language * language() const override + { + return m_language; + } + void setLanguage(Language *language) override + { + m_language = language; + emit languageChanged(); + } + QList unitList() override + { + return m_units.toList(); + } + QUrl file() const override + { + return QUrl(); + } + +private: + QString m_id{ "courseid" }; + QString m_foreignId{ "foreigncourseid" }; + QString m_title{ "title" }; + QString m_i18nTitle{ "i18n title" }; + QString m_description{ "description of the course" }; + Language *m_language{nullptr}; + QVector m_units; +}; + +// define one virtual method out of line to pin CourseStub to this translation unit +EditableCourseStub::~EditableCourseStub() = default; + + +void TestEditorSession::init() +{ + // TODO initialization of test case +} + +void TestEditorSession::cleanup() +{ + // TODO cleanup after test run +} + +void TestEditorSession::createEditorSession() +{ + Language language; + EditableCourseStub course(&language, QVector()); + EditableRepositoryStub repository{ {&course} }; + EditorSession session; + session.setRepository(&repository); + QVERIFY(session.course() != nullptr); + QCOMPARE(session.course()->id(), course.id()); +} + +QTEST_GUILESS_MAIN(TestEditorSession) diff --git a/autotests/editorsession/test_editorsession.h b/autotests/editorsession/test_editorsession.h new file mode 100644 index 0000000..4ce11b9 --- /dev/null +++ b/autotests/editorsession/test_editorsession.h @@ -0,0 +1,50 @@ +/* + * Copyright 2019 Andreas Cord-Landwehr + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TEST_EDITORSESSION_H +#define TEST_EDITORSESSION_H + +#include + +class TestEditorSession : public QObject +{ + Q_OBJECT + +public: + TestEditorSession() = default; + +private Q_SLOTS: + /** + * Called before every test case. + */ + void init(); + + /** + * Called after every test case. + */ + void cleanup(); + + /** + * @brief Construct and destruct editor session + */ + void createEditorSession(); +}; + +#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 825fe5f..066341a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,201 +1,203 @@ ### # Copyright 2013-2015 Andreas Cord-Landwehr # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ### ecm_setup_version(0.99.90 VARIABLE_PREFIX ARTIKULATE VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/ArtikulateConfigVersion.cmake" ) ecm_optional_add_subdirectory(qml) # set include directories include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${artikulate_SOURCE_DIR} ) # set the source code files from which Artikulate is compiled set(artikulateCore_SRCS core/icourse.h - core/drawertrainingactions.cpp + core/ieditablecourse.h + core/ieditablerepository.h core/iresourcerepository.h + core/drawertrainingactions.cpp core/resourcerepository.cpp core/contributorrepository.cpp core/language.cpp core/phrase.cpp core/phoneme.cpp core/phonemegroup.cpp core/unit.cpp core/editorsession.cpp core/trainingaction.cpp core/trainingactionicon.cpp core/trainingsession.cpp core/resources/resourceinterface.cpp core/resources/languageresource.cpp core/resources/courseparser.cpp core/resources/courseresource.cpp core/resources/editablecourseresource.cpp core/resources/skeletonresource.cpp core/player.cpp core/recorder.cpp qmlcontrols/iconitem.cpp qmlcontrols/imagetexturescache.cpp qmlcontrols/managedtexturenode.cpp artikulate_debug.cpp ) kconfig_add_kcfg_files (artikulateCore_SRCS settings.kcfgc) add_library(artikulatecore SHARED ${artikulateCore_SRCS}) generate_export_header(artikulatecore BASE_NAME artikulatecore) target_link_libraries(artikulatecore LINK_PUBLIC artikulatelearnerprofile artikulatesound Qt5::XmlPatterns Qt5::Quick KF5::Archive KF5::ConfigGui ) # internal library without any API or ABI guarantee set(GENERIC_LIB_VERSION "0") set(GENERIC_LIB_SOVERSION "0") set_target_properties( artikulatecore PROPERTIES VERSION ${GENERIC_LIB_VERSION} SOVERSION ${GENERIC_LIB_SOVERSION} ) install( TARGETS artikulatecore DESTINATION ${INSTALL_TARGETS_DEFAULT_ARGS} ) install(FILES artikulate.knsrc DESTINATION ${CONFIG_INSTALL_DIR}) # set the source code files from which Artikulate is compiled set(artikulate_SRCS main.cpp mainwindow.cpp application.cpp artikulate_debug.cpp models/coursemodel.cpp models/coursefiltermodel.cpp models/languagemodel.cpp models/languageresourcemodel.cpp # models/learningprogressmodel.cpp //TODO must be adapted to new trainingsession models/unitmodel.cpp models/unitfiltermodel.cpp models/phrasemodel.cpp models/phraselistmodel.cpp models/phrasefiltermodel.cpp models/phonememodel.cpp models/phonemegroupmodel.cpp models/phonemeunitmodel.cpp models/profilemodel.cpp models/skeletonmodel.cpp ui/sounddevicedialogpage.cpp ui/appearencedialogpage.cpp ui/resourcesdialogpage.cpp ) ki18n_wrap_ui (artikulate_SRCS ui/resourcesdialogpage.ui ui/sounddevicedialogpage.ui ui/appearencedialogpage.ui ) qt5_add_resources(artikulate_SRCS resources.qrc) qt5_add_resources(artikulate_SRCS ../data/languages.qrc) kconfig_add_kcfg_files (artikulate_SRCS settings.kcfgc) set(artikulate_editor_SRCS main_editor.cpp mainwindow_editor.cpp application.cpp artikulate_debug.cpp models/coursemodel.cpp models/coursefiltermodel.cpp models/languagemodel.cpp models/languageresourcemodel.cpp # models/learningprogressmodel.cpp //TODO must be adapted to new trainingsession models/unitmodel.cpp models/unitfiltermodel.cpp models/phrasemodel.cpp models/phraselistmodel.cpp models/phrasefiltermodel.cpp models/phonememodel.cpp models/phonemegroupmodel.cpp models/phonemeunitmodel.cpp models/profilemodel.cpp models/skeletonmodel.cpp ui/sounddevicedialogpage.cpp ui/appearencedialogpage.cpp ui/resourcesdialogpage.cpp ui/exportghnsdialog.cpp ) ki18n_wrap_ui(artikulate_editor_SRCS ui/appearencedialogpage.ui ui/exportghnsdialog.ui ui/resourcesdialogpage.ui ui/sounddevicedialogpage.ui ) qt5_add_resources(artikulate_editor_SRCS resources.qrc) qt5_add_resources(artikulate_editor_SRCS ../data/languages.qrc) kconfig_add_kcfg_files (artikulate_editor_SRCS settings.kcfgc) # executables add_executable(artikulate ${artikulate_SRCS}) target_link_libraries(artikulate LINK_PUBLIC artikulatelearnerprofile artikulatesound artikulatecore Qt5::Qml Qt5::Quick KF5::Crash KF5::NewStuff KF5::XmlGui ) qt5_add_resources(artikulate_editor_SRCS editor.qrc) add_executable(artikulate_editor ${artikulate_editor_SRCS}) target_link_libraries(artikulate_editor LINK_PUBLIC artikulatesound artikulatecore Qt5::Qml Qt5::Quick Qt5::QuickWidgets KF5::Crash KF5::NewStuff KF5::XmlGui ) install(FILES artikulate.kcfg DESTINATION ${KCFG_INSTALL_DIR}) install(TARGETS artikulate ${INSTALL_TARGETS_DEFAULT_ARGS}) install(TARGETS artikulate_editor ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/core/contributorrepository.cpp b/src/core/contributorrepository.cpp index c7d6d0f..164f0ad 100644 --- a/src/core/contributorrepository.cpp +++ b/src/core/contributorrepository.cpp @@ -1,466 +1,477 @@ /* * Copyright 2013-2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "contributorrepository.h" #include "artikulate_debug.h" #include "language.h" #include "unit.h" #include "phrase.h" #include "phoneme.h" #include "phonemegroup.h" #include "resources/languageresource.h" #include "resources/editablecourseresource.h" #include "resources/skeletonresource.h" #include "liblearnerprofile/src/profilemanager.h" #include "liblearnerprofile/src/learninggoal.h" #include #include #include #include #include #include ContributorRepository::ContributorRepository(QObject *parent) - : IResourceRepository() + : IEditableRepository() { loadLanguageResources(); } void ContributorRepository::loadLanguageResources() { // load language resources // all other resources are only loaded on demand QDir dir(":/artikulate/languages/"); dir.setFilter(QDir::Files | QDir::NoSymLinks); QFileInfoList list = dir.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); if (fileInfo.completeSuffix() != QLatin1String("xml")) { continue; } addLanguage(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); } } void ContributorRepository::sync() { // QMap< QString, QList< CourseResource* > >::iterator iter; // for (iter = m_courseResources.begin(); iter != m_courseResources.end(); ++iter) { // foreach (auto const &courseRes, iter.value()) { // courseRes->sync(); // } // } // foreach (auto const &courseRes, m_skeletonResources) { // courseRes->sync(); // } } bool ContributorRepository::modified() const { for (auto iter = m_courses.constBegin(); iter != m_courses.constEnd(); ++iter) { for (auto *course : iter.value()) { if (course->isModified()) { return true; } } } foreach (auto const &courseRes, m_skeletonResources) { if (courseRes->isModified()) { return true; } } return false; } void ContributorRepository::addLanguage(const QUrl &languageFile) { if (m_loadedResources.contains(languageFile.toLocalFile())) { return; } LanguageResource *resource = new LanguageResource(languageFile); emit languageResourceAboutToBeAdded(resource, m_languageResources.count()); m_languageResources.append(resource); m_loadedResources.append(languageFile.toLocalFile()); m_courses.insert(resource->identifier(), QList()); emit languageResourceAdded(); } QString ContributorRepository::storageLocation() const { return m_storageLocation; } void ContributorRepository::setStorageLocation(const QString &path) { m_storageLocation = path; } QList< LanguageResource* > ContributorRepository::languageResources() const { return m_languageResources; } QVector ContributorRepository::languages() const { QVector languages; for (auto resourse : m_languageResources) { languages.append(resourse->language()); } return languages; } Language * ContributorRepository::language(int index) const { Q_ASSERT(index >= 0 && index < m_languageResources.count()); return m_languageResources.at(index)->language(); } Language * ContributorRepository::language(LearnerProfile::LearningGoal *learningGoal) const { if (!learningGoal) { return nullptr; } if (learningGoal->category() != LearnerProfile::LearningGoal::Language) { qCritical() << "Cannot translate non-language learning goal to language"; return nullptr; } foreach (LanguageResource *resource, m_languageResources) { if (resource->identifier() == learningGoal->identifier()) { return resource->language(); } } qCritical() << "No language registered with identifier " << learningGoal->identifier() << ": aborting"; return nullptr; } QList ContributorRepository::courseResources(Language *language) { if (!language) { QList courses; for (auto iter = m_courses.constBegin(); iter != m_courses.constEnd(); ++iter) { courses.append(iter.value()); } return courses; } // return empty list if no course available for language if (!m_courses.contains(language->id())) { return QList< EditableCourseResource* >(); } return m_courses[language->id()]; } QVector ContributorRepository::courses() const { QVector courses; for (const auto &courseList : m_courses) { for (const auto &course : courseList) { courses.append(course); } } return courses; } +QVector ContributorRepository::editableCourses() const +{ + QVector courses; + for (const auto &courseList : m_courses) { + for (const auto &course : courseList) { + courses.append(course); + } + } + return courses; +} + QVector ContributorRepository::courses(Language *language) const { if (language == nullptr) { return courses(); } QVector courses; if (m_courses.contains(language->id())) { for (const auto &course : m_courses[language->id()]) { courses.append(course); } } return courses; } -EditableCourseResource * ContributorRepository::course(Language *language, int index) const +IEditableCourse * ContributorRepository::editableCourse(Language *language, int index) const { Q_ASSERT(m_courses.contains(language->id())); Q_ASSERT(index >= 0 && index < m_courses[language->id()].count()); return m_courses[language->id()].at(index); } void ContributorRepository::reloadCourseOrSkeleton(ICourse *courseOrSkeleton) { if (!courseOrSkeleton) { qCritical() << "Cannot reload non-existing course"; return; } if (!courseOrSkeleton->file().isValid()) { qCritical() << "Cannot reload temporary file, aborting."; return; } // figure out if this is a course or a skeleton if (courseOrSkeleton->language()) { // only course files have a language //TODO better add a check if this is contained in the course list // to catch possible errors QUrl file = courseOrSkeleton->file(); m_loadedResources.removeOne(courseOrSkeleton->file().toLocalFile()); removeCourse(courseOrSkeleton); addCourse(file); } else { for (SkeletonResource *resource : m_skeletonResources) { if (resource->id() == courseOrSkeleton->id()) { // TODO no reload available return; } } } } void ContributorRepository::reloadCourses() { // register skeleton resources QDir skeletonDirectory = QDir(storageLocation()); skeletonDirectory.setFilter(QDir::Files | QDir::Hidden); if (!skeletonDirectory.cd(QStringLiteral("skeletons"))) { qCritical() << "There is no subdirectory \"skeletons\" in directory " << skeletonDirectory.path() << " cannot load skeletons."; } else { // read skeletons QFileInfoList list = skeletonDirectory.entryInfoList(); for (int i = 0; i < list.size(); ++i) { QFileInfo fileInfo = list.at(i); addSkeleton(QUrl::fromLocalFile(fileInfo.absoluteFilePath())); } } // register contributor course files QDir courseDirectory(storageLocation()); if (!courseDirectory.cd(QStringLiteral("courses"))) { qCritical() << "There is no subdirectory \"courses\" in directory " << courseDirectory.path() << " cannot load courses."; } else { // find courses courseDirectory.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QFileInfoList courseDirList = courseDirectory.entryInfoList(); // traverse all course directories for (const QFileInfo &info : courseDirList) { QDir courseDir = QDir(info.absoluteFilePath()); courseDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); QFileInfoList courseLangDirList = courseDir.entryInfoList(); // traverse all language directories for each course for (const QFileInfo &langInfo : courseLangDirList) { QDir courseLangDir = QDir(langInfo.absoluteFilePath()); courseLangDir.setFilter(QDir::Files); QStringList nameFilters; nameFilters.append(QStringLiteral("*.xml")); QFileInfoList courses = courseLangDir.entryInfoList(nameFilters); // find and add course files for (const QFileInfo &courseInfo : courses) { addCourse(QUrl::fromLocalFile(courseInfo.filePath())); } } } } //TODO this signal should only be emitted when repository was added/removed // yet the call to this method is very seldom and emitting it too often is not that harmful emit repositoryChanged(); } void ContributorRepository::updateCourseFromSkeleton(EditableCourseResource *course) { //TODO implement status information that are shown at mainwindow if (course->foreignId().isEmpty()) { qCritical() << "No skeleton ID specified, aborting update."; return; } ICourse *skeleton = nullptr; for (const auto &iter : m_skeletonResources) { if (iter->id() == course->foreignId()) { skeleton = iter; break; } } if (!skeleton) { qCritical() << "Could not find skeleton with id " << course->foreignId() << ", aborting update."; return; } // update now foreach (Unit *unitSkeleton, skeleton->unitList()) { // import unit if not exists Unit *currentUnit = nullptr; bool found = false; foreach (Unit *unit, course->unitList()) { if (unit->foreignId() == unitSkeleton->id()) { found = true; currentUnit = unit; break; } } if (found == false) { currentUnit = new Unit(course); currentUnit->setId(QUuid::createUuid().toString()); currentUnit->setTitle(unitSkeleton->title()); currentUnit->setForeignId(unitSkeleton->id()); currentUnit->setCourse(course); course->addUnit(currentUnit); course->setModified(true); } // update phrases foreach (Phrase *phraseSkeleton, unitSkeleton->phraseList()) { bool found = false; foreach (Phrase *phrase, currentUnit->phraseList()) { if (phrase->foreignId() == phraseSkeleton->id()) { if (phrase->i18nText() != phraseSkeleton->text()) { phrase->setEditState(Phrase::Unknown); phrase->seti18nText(phraseSkeleton->text()); } found = true; break; } } if (found == false) { Phrase *newPhrase = new Phrase(course); newPhrase->setForeignId(phraseSkeleton->id()); newPhrase->setId(QUuid::createUuid().toString()); newPhrase->setText(phraseSkeleton->text()); newPhrase->seti18nText(phraseSkeleton->text()); newPhrase->setType(phraseSkeleton->type()); newPhrase->setUnit(currentUnit); currentUnit->addPhrase(newPhrase); course->setModified(true); } } } // FIXME deassociate removed phrases qCDebug(ARTIKULATE_LOG) << "Update performed!"; } EditableCourseResource * ContributorRepository::addCourse(const QUrl &courseFile) { EditableCourseResource *resource = new EditableCourseResource(courseFile, this); if (resource->language() == nullptr) { delete resource; qCritical() << "Could not load course, language unknown:" << courseFile.toLocalFile(); return nullptr; } // skip already loaded resources if (m_loadedResources.contains(courseFile.toLocalFile())) { delete resource; return nullptr; } m_loadedResources.append(courseFile.toLocalFile()); addCourseResource(resource); emit languageCoursesChanged(); return resource; } void ContributorRepository::addCourseResource(EditableCourseResource *resource) { Q_ASSERT(m_courses.contains(resource->language()->id())); if (!m_courses.contains(resource->language()->id())) { m_courses.insert(resource->language()->id(), QList()); } emit courseAboutToBeAdded(resource, m_courses[resource->language()->id()].count()); m_courses[resource->language()->id()].append(resource); emit courseAdded(); } void ContributorRepository::removeCourse(ICourse *course) { for (int index = 0; index < m_courses[course->language()->id()].length(); ++index) { if (m_courses[course->language()->id()].at(index) == course) { emit courseAboutToBeRemoved(index); m_courses[course->language()->id()].removeAt(index); emit courseRemoved(); course->deleteLater(); return; } } } EditableCourseResource * ContributorRepository::createCourse(Language *language, SkeletonResource *skeleton) { // set path QString path = QStringLiteral("%1/%2/%3/%4/%4.xml") .arg(storageLocation(), QStringLiteral("courses"), skeleton->id(), language->id()); EditableCourseResource * course = new EditableCourseResource(QUrl::fromLocalFile(path), this); Q_ASSERT(course); course->setId(QUuid::createUuid().toString()); course->setTitle(skeleton->title()); course->setDescription(skeleton->description()); course->setFile(QUrl::fromLocalFile(path)); course->setLanguage(language); // set skeleton course->setForeignId(skeleton->id()); addCourseResource(course); return course; } void ContributorRepository::addSkeleton(const QUrl &file) { SkeletonResource *resource = new SkeletonResource(file, this); addSkeletonResource(resource); } void ContributorRepository::addSkeletonResource(SkeletonResource *resource) { // skip already loaded resources if (m_loadedResources.contains(resource->file().toLocalFile())) { return; } m_loadedResources.append(resource->file().toLocalFile()); emit skeletonAboutToBeAdded(resource, m_skeletonResources.count()); m_skeletonResources.append(resource); emit skeletonAdded(); } void ContributorRepository::removeSkeleton(SkeletonResource *skeleton) { for (int index = 0; index < m_skeletonResources.length(); ++index) { if (m_skeletonResources.at(index)->id() == skeleton->id()) { emit skeletonAboutToBeRemoved(index, index); m_skeletonResources.removeAt(index); emit skeletonRemoved(); skeleton->deleteLater(); return; } } } QList< SkeletonResource* > ContributorRepository::skeletonResources() { return m_skeletonResources; } diff --git a/src/core/contributorrepository.h b/src/core/contributorrepository.h index b1cb02d..a9379d6 100644 --- a/src/core/contributorrepository.h +++ b/src/core/contributorrepository.h @@ -1,218 +1,217 @@ /* * Copyright 2013-2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef CONTRIBUTORREPOSITORY_H #define CONTRIBUTORREPOSITORY_H #include "artikulatecore_export.h" -#include "iresourcerepository.h" +#include "ieditablerepository.h" #include #include #include #include #include "liblearnerprofile/src/learninggoal.h" class SkeletonResource; class EditableCourseResource; class LanguageResource; class Language; class ICourse; class QUrl; /** * @class ContributorRepository * This class handles the resources of a contributor. */ -class ARTIKULATECORE_EXPORT ContributorRepository : public IResourceRepository +class ARTIKULATECORE_EXPORT ContributorRepository : public IEditableRepository { Q_OBJECT Q_INTERFACES(IResourceRepository) + Q_INTERFACES(IEditableRepository) Q_PROPERTY(QString repositoryUrl READ storageLocation NOTIFY repositoryChanged) public: explicit ContributorRepository(QObject *parent = nullptr); /** * save all changes to course resources */ void sync(); /** * \return \c true if any course or skeleton is modified, otherwise \c false */ bool modified() const; /** * \return path to working repository, if one is set */ QString storageLocation() const override; /** * Set path to central storage location * \param path the path to the storage location directory */ void setStorageLocation(const QString &path); /** * \return list of all available language specifications */ Q_DECL_DEPRECATED QList languageResources() const; - /** - * \return list of all available language specifications - */ QVector languages() const override; /** * \return language by \p index */ Q_INVOKABLE Language * language(int index) const; /** * \return language by \p learningGoal */ Q_INVOKABLE Language * language(LearnerProfile::LearningGoal* learningGoal) const; QVector courses() const override; + QVector editableCourses() const override; QVector courses(Language *language) const override; /** * \return list of all loaded courses for language \p language */ QList courseResources(Language *language); - Q_INVOKABLE EditableCourseResource * course(Language *language, int index) const; + Q_INVOKABLE IEditableCourse * editableCourse(Language *language, int index) const override; /** * Reset the file for this course or skeleton. * * \param course the course to be reloaded */ Q_INVOKABLE void reloadCourseOrSkeleton(ICourse *course); /** * @brief Implementation of course resource reloading */ void reloadCourses() override; /** * Imports units and phrases from skeleton, deassociates removed ones. * * \param course the course to be update */ void updateCourseFromSkeleton(EditableCourseResource *course); /** * Add language to resource manager by parsing the given language specification file. * * \param languageFile is the local XML file containing the language */ void addLanguage(const QUrl &languageFile); /** * Adds course to resource manager by parsing the given course specification file. * * \param courseFile is the local XML file containing the course * \return true if loaded successfully, otherwise false */ EditableCourseResource * addCourse(const QUrl &courseFile); /** * Adds course to resource manager. If the course's language is not registered, the language * is registered by this method. * * \param resource the course resource to add to resource manager */ void addCourseResource(EditableCourseResource *resource); /** * Remove course from resource manager. If the course is modified its changes are NOT * written. For writing changes, the Course::sync() method must be called directly. * * \param course is the course to be removed */ void removeCourse(ICourse *course); /** * Create new course for \p language and derived from \p skeleton. * * \return created course */ Q_INVOKABLE EditableCourseResource * createCourse(Language *language, SkeletonResource *skeleton); /** * Adds skeleton resource to resource manager * * \param resource the skeleton resource to add to resource manager */ void addSkeleton(const QUrl &skeletonFile); /** * Adds skeleton resource to resource manager * * \param resource the skeleton resource to add to resource manager */ void addSkeletonResource(SkeletonResource *resource); /** * Remove skeleton from resource manager. If the skeleton is modified its changes are NOT * written. For writing changes, the Skeleton::sync() method must be called directly. * * \param skeleton is the skeleton to be removed */ void removeSkeleton(SkeletonResource *skeleton); /** * \return list of all loaded skeletons resources */ QList skeletonResources(); Q_SIGNALS: void languageResourceAdded(); void languageResourceAboutToBeAdded(LanguageResource*,int); void languageResourceRemoved(); void languageResourceAboutToBeRemoved(int); void repositoryChanged(); void courseAdded() override; void courseAboutToBeAdded(ICourse*,int) override; void courseAboutToBeRemoved(int) override; void courseRemoved() override; void skeletonAdded(); void skeletonAboutToBeAdded(ICourse*,int); void skeletonRemoved(); void skeletonAboutToBeRemoved(int,int); void languageCoursesChanged(); private: /** * This method loads all language files that are provided in the standard directories * for this application. */ void loadLanguageResources(); QString m_storageLocation; QList m_languageResources; QMap > m_courses; //!> (language-id, course-resource) QList m_skeletonResources; QStringList m_loadedResources; }; #endif diff --git a/src/core/editorsession.cpp b/src/core/editorsession.cpp index 26e8793..d59a760 100644 --- a/src/core/editorsession.cpp +++ b/src/core/editorsession.cpp @@ -1,278 +1,281 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "editorsession.h" #include "core/language.h" #include "core/resources/editablecourseresource.h" #include "core/resources/skeletonresource.h" #include "core/resources/languageresource.h" #include "core/unit.h" #include "core/phrase.h" #include "core/contributorrepository.h" #include "artikulate_debug.h" EditorSession::EditorSession(QObject *parent) : QObject(parent) , m_repository(nullptr) , m_skeletonMode(true) , m_editSkeleton(false) , m_skeleton(nullptr) , m_language(nullptr) , m_course(nullptr) , m_tmpCourseWhileSkeletonEditing(nullptr) , m_unit(nullptr) , m_phrase(nullptr) { } -void EditorSession::setContributorRepository(ContributorRepository *repository) +void EditorSession::setRepository(IEditableRepository *repository) { m_repository = repository; + if (!repository->editableCourses().isEmpty()) { + setCourse(repository->editableCourses().first()); + } } void EditorSession::setSkeletonMode(bool enabled) { if (m_skeletonMode == enabled) { return; } m_skeletonMode = enabled; emit skeletonModeChanged(); } bool EditorSession::skeletonMode() const { return m_skeletonMode; } void EditorSession::setEditSkeleton(bool enabled) { if (m_editSkeleton == enabled) { return; } m_editSkeleton = enabled; if (enabled) { m_tmpCourseWhileSkeletonEditing = m_course; // setCourse(m_skeleton); //FIXME port skeleton for this } else { setCourse(m_tmpCourseWhileSkeletonEditing); m_tmpCourseWhileSkeletonEditing = nullptr; } emit editSkeletonChanged(); } bool EditorSession::isEditSkeleton() const { return m_editSkeleton; } SkeletonResource * EditorSession::skeleton() const { return m_skeleton; } void EditorSession::setSkeleton(SkeletonResource *skeleton) { if (m_skeleton == skeleton) { return; } m_skeleton = skeleton; Language *language = m_language; if (!m_language) { language = m_repository->languages().constFirst(); } if (m_skeleton) { bool found = false; - int resources = m_repository->courseResources(language).count(); + int resources = m_repository->courses(language).count(); for (int i=0; i < resources; ++i) { - EditableCourseResource * course = m_repository->course(language, i); + auto course = m_repository->editableCourse(language, i); if (course->foreignId() == m_skeleton->id()) { setCourse(course); found = true; break; } } if (!found) { setCourse(nullptr); } } emit skeletonChanged(); } Language * EditorSession::language() const { return m_language; } void EditorSession::setLanguage(Language *language) { if (m_language == language) { return; } m_language = language; if (m_skeletonMode) { bool found = false; if (m_skeleton) { - int resources = m_repository->courseResources(m_language).count(); + int resources = m_repository->courses(m_language).count(); for (int i=0; i < resources; ++i) { - EditableCourseResource * course = m_repository->course(m_language, i); + IEditableCourse *course = m_repository->editableCourse(m_language, i); if (course->foreignId() == m_skeleton->id()) { setCourse(course); found = true; break; } } } if (!found) { setCourse(nullptr); } } else { // not skeleton mode - if (m_repository->courseResources(m_language).count() > 0) { - setCourse(m_repository->course(m_language, 0)); + if (m_repository->courses(m_language).count() > 0) { + setCourse(m_repository->editableCourse(m_language, 0)); } } emit languageChanged(); } -EditableCourseResource * EditorSession::course() const +IEditableCourse * EditorSession::course() const { return m_course; } -void EditorSession::setCourse(EditableCourseResource *course) +void EditorSession::setCourse(IEditableCourse *course) { if (m_course == course) { return; } m_course = course; if (m_course && !m_course->unitList().isEmpty()) { setUnit(m_course->unitList().constFirst()); } else { setUnit(nullptr); } emit courseChanged(); } Unit * EditorSession::unit() const { return m_unit; } void EditorSession::setUnit(Unit *unit) { if (m_unit == unit) { return; } m_unit = unit; // different than above, do not directly enter phrases // but first show editing information for units setPhrase(nullptr); emit unitChanged(); } void EditorSession::setPhrase(Phrase *phrase) { if (m_phrase == phrase) { return; } if (phrase) { setUnit(phrase->unit()); } m_phrase = phrase; emit phraseChanged(); } Phrase * EditorSession::phrase() const { return m_phrase; } Phrase * EditorSession::previousPhrase() const { if (!m_phrase) { return nullptr; } const int index = m_phrase->unit()->phraseList().indexOf(m_phrase); if (index > 0) { return m_phrase->unit()->phraseList().at(index - 1); } else { Unit *unit = m_phrase->unit(); int uIndex = unit->course()->unitList().indexOf(unit); if (uIndex > 0) { return unit->course()->unitList().at(uIndex - 1)->phraseList().last(); } } return nullptr; } Phrase * EditorSession::nextPhrase() const { if (!m_phrase) { return nullptr; } const int index = m_phrase->unit()->phraseList().indexOf(m_phrase); if (index < m_phrase->unit()->phraseList().length() - 1) { return m_phrase->unit()->phraseList().at(index + 1); } else { Unit *unit = m_phrase->unit(); int uIndex = unit->course()->unitList().indexOf(unit); if (uIndex < unit->course()->unitList().length() - 1) { Unit *nextUnit = unit->course()->unitList().at(uIndex + 1); if (nextUnit->phraseList().isEmpty()) { return nullptr; } return nextUnit->phraseList().constFirst(); } } return nullptr; } void EditorSession::switchToPreviousPhrase() { setPhrase(previousPhrase()); } void EditorSession::switchToNextPhrase() { setPhrase(nextPhrase()); } bool EditorSession::hasPreviousPhrase() const { return previousPhrase() != nullptr; } bool EditorSession::hasNextPhrase() const { return nextPhrase() != nullptr; } void EditorSession::updateCourseFromSkeleton() { if (!m_course) { qCritical() << "Not updating course from skeleton, no one set."; return; } // m_resourceManager->updateCourseFromSkeleton(m_course); //FIXME } diff --git a/src/core/editorsession.h b/src/core/editorsession.h index 7e4f3aa..b19aadc 100644 --- a/src/core/editorsession.h +++ b/src/core/editorsession.h @@ -1,120 +1,120 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef EDITORSESSION_H #define EDITORSESSION_H #include "artikulatecore_export.h" #include "phrase.h" class QString; class Language; -class EditableCourseResource; +class IEditableCourse; class Unit; class SkeletonResource; -class ContributorRepository; +class IEditableRepository; /** * \class EditorSession * * An object of this class is used to set the current state of the editor. By this, we put all logic * how language, skeleton and course fit to each other into this class. The main concept is that * we have to fundamentally different workflows that both are modeled in this class: * * 1. Skeleton based workflow * - a skeleton is selected * - every language is available, since eventually the course should be available in every language * - for every language, there is at most one course (there is none only in case it is not created yet) * - adding new units or phrases is only possible in the skeleton course * - every course can update/sync with the skeleton * * 2. Course based workflow * - there is no skeleton from which the course is derived * - the base is a language that is selected first * - for a language there can be none to arbitrarily many courses * * The main switch is \c EditorSession::setSkeletonMode(bool) */ class ARTIKULATECORE_EXPORT EditorSession : public QObject { Q_OBJECT Q_PROPERTY(bool skeletonMode READ skeletonMode WRITE setSkeletonMode NOTIFY skeletonModeChanged) Q_PROPERTY(bool editSkeleton READ isEditSkeleton WRITE setEditSkeleton NOTIFY editSkeletonChanged) Q_PROPERTY(SkeletonResource *skeleton READ skeleton WRITE setSkeleton NOTIFY skeletonChanged) Q_PROPERTY(Language *language READ language WRITE setLanguage NOTIFY languageChanged) - Q_PROPERTY(EditableCourseResource *course READ course WRITE setCourse NOTIFY courseChanged) //TODO interface should provde ICourse + Q_PROPERTY(IEditableCourse *course READ course WRITE setCourse NOTIFY courseChanged) Q_PROPERTY(Unit *unit READ unit WRITE setUnit NOTIFY unitChanged) Q_PROPERTY(Phrase *phrase READ phrase WRITE setPhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasNextPhrase READ hasNextPhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasPreviousPhrase READ hasPreviousPhrase NOTIFY phraseChanged) public: explicit EditorSession(QObject *parent = nullptr); - void setContributorRepository(ContributorRepository *manager); + void setRepository(IEditableRepository *repository); void setSkeletonMode(bool enabled=true); bool skeletonMode() const; void setEditSkeleton(bool enabled=true); bool isEditSkeleton() const; SkeletonResource * skeleton() const; void setSkeleton(SkeletonResource *skeleton); Language * language() const; void setLanguage(Language *language); - EditableCourseResource * course() const; - void setCourse(EditableCourseResource *course); + IEditableCourse * course() const; + void setCourse(IEditableCourse *course); Unit * unit() const; void setUnit(Unit *unit); Phrase * phrase() const; void setPhrase(Phrase *phrase); Phrase::Type phraseType() const; void setPhraseType(Phrase::Type type); bool hasPreviousPhrase() const; bool hasNextPhrase() const; Q_INVOKABLE void switchToPreviousPhrase(); Q_INVOKABLE void switchToNextPhrase(); Q_INVOKABLE void updateCourseFromSkeleton(); private: Phrase * nextPhrase() const; Phrase * previousPhrase() const; Q_SIGNALS: void editSkeletonChanged(); void skeletonModeChanged(); void skeletonChanged(); void languageChanged(); void courseChanged(); void unitChanged(); void phraseChanged(); private: Q_DISABLE_COPY(EditorSession) - ContributorRepository * m_repository; + IEditableRepository * m_repository; bool m_skeletonMode; bool m_editSkeleton; SkeletonResource *m_skeleton; Language *m_language; - EditableCourseResource *m_course; - EditableCourseResource *m_tmpCourseWhileSkeletonEditing; + IEditableCourse *m_course; + IEditableCourse *m_tmpCourseWhileSkeletonEditing; Unit *m_unit; Phrase *m_phrase; }; #endif diff --git a/src/core/ieditablecourse.h b/src/core/ieditablecourse.h new file mode 100644 index 0000000..08922e8 --- /dev/null +++ b/src/core/ieditablecourse.h @@ -0,0 +1,45 @@ +/* + * 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 IEDITABLECOURSE_H +#define IEDITABLECOURSE_H + +#include "artikulatecore_export.h" +#include "icourse.h" +#include + +class QString; +class Language; + +class ARTIKULATECORE_EXPORT IEditableCourse : public ICourse +{ +public: + virtual ~IEditableCourse() = default; + virtual void setId(QString id) = 0; + virtual void setForeignId(QString foreignId) = 0; + virtual void setTitle(QString title) = 0; + virtual void setI18nTitle(QString title) = 0; + virtual void setDescription(QString description) = 0; + virtual void setLanguage(Language *language) = 0; +}; + +Q_DECLARE_INTERFACE(IEditableCourse, "com.kde.artikulate.IEditableCourse/1.0") + +#endif // EDITABLECOURSE_H diff --git a/src/core/ieditablerepository.h b/src/core/ieditablerepository.h new file mode 100644 index 0000000..511db1d --- /dev/null +++ b/src/core/ieditablerepository.h @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Andreas Cord-Landwehr + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef IEDITABLEREPOSITORY_H +#define IEDITABLEREPOSITORY_H + +#include "artikulatecore_export.h" +#include "iresourcerepository.h" + +class IEditableCourse; +class Language; + +/** + * \class IEditableRepository + * This interface provides a generic interface that provides just the methods and signals needed + * to integrade a repository into the editing part of Artikulate. + */ +class ARTIKULATECORE_EXPORT IEditableRepository : public IResourceRepository +{ + Q_OBJECT +public: + virtual ~IEditableRepository() = default; + virtual QVector editableCourses() const = 0; + virtual IEditableCourse * editableCourse(Language *language, int index) const = 0; +}; +Q_DECLARE_INTERFACE(IEditableRepository, "IEditableRepository") + +#endif diff --git a/src/core/iresourcerepository.h b/src/core/iresourcerepository.h index 0ef8332..e65ffe6 100644 --- a/src/core/iresourcerepository.h +++ b/src/core/iresourcerepository.h @@ -1,77 +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 + * \return list of all available 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/resources/editablecourseresource.cpp b/src/core/resources/editablecourseresource.cpp index 51353bb..d13ca20 100644 --- a/src/core/resources/editablecourseresource.cpp +++ b/src/core/resources/editablecourseresource.cpp @@ -1,217 +1,217 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "editablecourseresource.h" #include "courseparser.h" #include "artikulate_debug.h" #include "core/unit.h" #include "core/phrase.h" #include "core/phoneme.h" #include #include #include #include #include #include #include #include EditableCourseResource::EditableCourseResource(const QUrl &path, IResourceRepository *repository) : m_course(new CourseResource(path, repository)) { connect(m_course.get(), &ICourse::unitAboutToBeAdded, this, &ICourse::unitAboutToBeAdded); connect(m_course.get(), &ICourse::unitAdded, this, &ICourse::unitAdded); connect(m_course.get(), &CourseResource::idChanged, this, &EditableCourseResource::idChanged); connect(m_course.get(), &CourseResource::foreignIdChanged, this, &EditableCourseResource::foreignIdChanged); connect(m_course.get(), &CourseResource::titleChanged, this, &EditableCourseResource::titleChanged); connect(m_course.get(), &CourseResource::descriptionChanged, this, &EditableCourseResource::descriptionChanged); connect(m_course.get(), &CourseResource::languageChanged, this, &EditableCourseResource::languageChanged); } QString EditableCourseResource::id() const { return m_course->id(); } -void EditableCourseResource::setId(const QString &id) +void EditableCourseResource::setId(QString id) { m_course->setId(id); } QString EditableCourseResource::foreignId() const { return m_course->foreignId(); } -void EditableCourseResource::setForeignId(const QString &foreignId) +void EditableCourseResource::setForeignId(QString foreignId) { - m_course->setForeignId(foreignId); + m_course->setForeignId(std::move(foreignId)); } QString EditableCourseResource::title() const { return m_course->title(); } -void EditableCourseResource::setTitle(const QString &title) +void EditableCourseResource::setTitle(QString title) { m_course->setTitle(title); } QString EditableCourseResource::i18nTitle() const { return m_course->i18nTitle(); } -void EditableCourseResource::seti18nTitle(const QString &i18nTitle) +void EditableCourseResource::setI18nTitle(QString i18nTitle) { m_course->setI18nTitle(i18nTitle); } QString EditableCourseResource::description() const { return m_course->description(); } -void EditableCourseResource::setDescription(const QString &description) +void EditableCourseResource::setDescription(QString description) { m_course->setDescription(description); } Language * EditableCourseResource::language() const { return m_course->language(); } void EditableCourseResource::setLanguage(Language *language) { m_course->setLanguage(language); } QList EditableCourseResource::unitList() { return m_course->unitList(); } QUrl EditableCourseResource::file() const { return m_course->file(); } void EditableCourseResource::sync() { Q_ASSERT(file().isValid()); Q_ASSERT(file().isLocalFile()); Q_ASSERT(!file().isEmpty()); // not writing back if not modified if (!m_modified) { qCDebug(ARTIKULATE_LOG()) << "Aborting sync, course was not modified."; return; } exportCourse(file()); } bool EditableCourseResource::exportCourse(const QUrl &filePath) { // write back to file // create directories if necessary QFileInfo info(filePath.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); if (!info.exists()) { qCDebug(ARTIKULATE_LOG()) << "create xml output file directory, not existing"; QDir dir; dir.mkpath(filePath.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } //TODO port to KSaveFile QFile file(filePath.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { qCWarning(ARTIKULATE_LOG()) << "Unable to open file " << file.fileName() << " in write mode, aborting."; return false; } file.write(CourseParser::serializedDocument(m_course.get(), false).toByteArray()); return true; } void EditableCourseResource::addUnit(Unit *unit) { m_course->addUnit(unit); } bool EditableCourseResource::isModified() const { return m_modified; } void EditableCourseResource::setModified(bool modified) { m_modified = modified; } Unit * EditableCourseResource::createUnit() { // find first unused id QStringList unitIds; for (auto *unit : m_course->units()) { 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 * EditableCourseResource::createPhrase(Unit *unit) { // find globally unique phrase id inside course QStringList phraseIds; for (auto *unit : m_course->units()) { for (auto *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; } diff --git a/src/core/resources/editablecourseresource.h b/src/core/resources/editablecourseresource.h index e8925b1..5bd89f4 100644 --- a/src/core/resources/editablecourseresource.h +++ b/src/core/resources/editablecourseresource.h @@ -1,136 +1,138 @@ /* * 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 EDITABLECOURSERESOURCE_H #define EDITABLECOURSERESOURCE_H #include "artikulatecore_export.h" #include "courseresource.h" #include "core/icourse.h" +#include "core/ieditablecourse.h" #include #include #include class IResourceRepository; class Course; class Unit; class Phrase; class QString; class QDomDocument; /** * @brief Decorator for CourseResource * * This decorator adds functionality to modify and write back changes of a course. */ -class ARTIKULATECORE_EXPORT EditableCourseResource : public ICourse +class ARTIKULATECORE_EXPORT EditableCourseResource : public IEditableCourse { Q_OBJECT Q_INTERFACES(ICourse) + Q_INTERFACES(IEditableCourse) public: /** * Create course resource from file. */ explicit EditableCourseResource(const QUrl &path, IResourceRepository *repository); ~EditableCourseResource() override = default; /** * \return unique identifier */ QString id() const override; - void setId(const QString &id); + void setId(QString id) override; /** * \return unique identifier */ QString foreignId() const override; - void setForeignId(const QString &foreignId); + void setForeignId(QString foreignId) override; /** * \return human readable localized title */ QString title() const override; - void setTitle(const QString &title); + void setTitle(QString title) override; /** * \return human readable title in English */ QString i18nTitle() const override; - void seti18nTitle(const QString &i18nTitle); + void setI18nTitle(QString i18nTitle) override; /** * \return description text for course */ QString description() const override; - void setDescription(const QString &description); + void setDescription(QString description) override; /** * \return language identifier of this course */ Language * language() const override; - void setLanguage(Language *language); + void setLanguage(Language *language) override; void sync(); /** * @brief Export course to specified file. * @param filePath the absolute path to the export file * @return true of export finished without errors */ bool exportCourse(const QUrl &filePath); void addUnit(Unit *); bool isModified() const; void setModified(bool modified); //TODO this method should not be public API but only used internally QUrl file() const override; void setFile(const QUrl &) {} QList unitList() override; Q_INVOKABLE Unit * createUnit(); Q_INVOKABLE Phrase * createPhrase(Unit *unit); Q_SIGNALS: void idChanged(); void foreignIdChanged(); void titleChanged(); void i18nTitleChanged(); void descriptionChanged(); void languageChanged(); private: bool m_modified{ false }; const std::unique_ptr m_course; }; #endif diff --git a/src/mainwindow_editor.cpp b/src/mainwindow_editor.cpp index 2bdf73b..5ebb549 100644 --- a/src/mainwindow_editor.cpp +++ b/src/mainwindow_editor.cpp @@ -1,198 +1,198 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mainwindow_editor.h" #include "application.h" #include "ui/resourcesdialogpage.h" #include "ui/sounddevicedialogpage.h" #include "ui/appearencedialogpage.h" #include "ui/exportghnsdialog.h" #include "core/editorsession.h" #include "core/resources/courseresource.h" #include "models/languagemodel.h" #include "settings.h" #include "libsound/src/outputdevicecontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include "artikulate_debug.h" #include #include #include #include #include #include #include #include #include #include using namespace LearnerProfile; MainWindowEditor::MainWindowEditor(ContributorRepository *repository) : m_repository(repository) , m_editorSession(new EditorSession()) , m_widget(new QQuickWidget) { m_repository->setStorageLocation(Settings::courseRepositoryPath()); - m_editorSession->setContributorRepository(m_repository); + m_editorSession->setRepository(m_repository); setWindowIcon(QIcon::fromTheme(QStringLiteral("artikulate"))); setWindowTitle(qAppName()); setAutoSaveSettings(); // workaround for QTBUG-40765 qApp->setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); // load saved sound settings OutputDeviceController::self().setVolume(Settings::audioOutputVolume()); // load resources if (m_repository->languages().count() == 0) { qFatal("No language resources found, cannot start application."); } m_repository->reloadCourses(); // create menu setupActions(); // set view m_widget->resize(QSize(800, 600)); m_widget->rootContext()->setContextObject(new KLocalizedContext(m_widget)); m_widget->rootContext()->setContextProperty(QStringLiteral("g_repository"), m_repository); m_widget->rootContext()->setContextProperty(QStringLiteral("g_editorSession"), m_editorSession); // set starting screen m_widget->setSource(QUrl(QStringLiteral("qrc:/artikulate/qml/Editor.qml"))); m_widget->setResizeMode(QQuickWidget::SizeRootObjectToView); QAction *newAct = KStandardAction::save(this, SLOT(save()), actionCollection()); actionCollection()->addAction(QStringLiteral("save"), newAct); // set status bar statusBar()->setEnabled(true); QLabel *repositoryLabel = new QLabel; repositoryLabel->setText(i18n("Course Repository: %1", m_repository->storageLocation())); connect(m_repository, &ContributorRepository::repositoryChanged, this, [=]() { repositoryLabel->setText(i18n("Course Repository: %1", m_repository->storageLocation())); }); statusBar()->insertWidget(0, repositoryLabel); createGUI(QStringLiteral("artikulateui_editor.rc")); setCentralWidget(m_widget); } MainWindowEditor::~MainWindowEditor() { // save current settings for case of closing Settings::self()->save(); } ContributorRepository * MainWindowEditor::resourceRepository() const { return m_repository; } void MainWindowEditor::setupActions() { QAction *settingsAction = new QAction(i18nc("@item:inmenu", "Configure Artikulate"), this); connect(settingsAction, &QAction::triggered, this, &MainWindowEditor::showSettingsDialog); actionCollection()->addAction(QStringLiteral("settings"), settingsAction); settingsAction->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); QAction *exportAction = new QAction(i18nc("@item:inmenu", "Export GHNS Files"), this); connect(exportAction, &QAction::triggered, this, [=]() { QPointer dialog = new ExportGhnsDialog(m_repository); dialog->exec(); }); actionCollection()->addAction(QStringLiteral("export_ghns"), exportAction); exportAction->setIcon(QIcon::fromTheme(QStringLiteral("document-export"))); KStandardAction::quit(this, SLOT(quit()), actionCollection()); setupGUI(Keys | Save | Create, QStringLiteral("artikulateui_editor.rc")); } void MainWindowEditor::showSettingsDialog() { if (KConfigDialog::showDialog(QStringLiteral("settings"))) { return; } QPointer dialog = new KConfigDialog(nullptr, QStringLiteral("settings"), Settings::self()); ResourcesDialogPage *resourceDialog = new ResourcesDialogPage(m_repository); SoundDeviceDialogPage *soundDialog = new SoundDeviceDialogPage(); AppearenceDialogPage *appearenceDialog = new AppearenceDialogPage(); resourceDialog->loadSettings(); soundDialog->loadSettings(); appearenceDialog->loadSettings(); dialog->addPage(soundDialog, i18nc("@item:inmenu", "Sound Devices"), QStringLiteral("audio-headset"), i18nc("@title:tab", "Sound Device Settings"), true); dialog->addPage(appearenceDialog, i18nc("@item:inmenu", "Fonts"), QStringLiteral("preferences-desktop-font"), i18nc("@title:tab", "Training Phrase Font"), true); dialog->addPage(resourceDialog, i18nc("@item:inmenu", "Course Resources"), QStringLiteral("repository"), i18nc("@title:tab", "Resource Repository Settings"), true); connect(dialog.data(), &QDialog::accepted, resourceDialog, &ResourcesDialogPage::saveSettings); connect(dialog.data(), &QDialog::accepted, soundDialog, &SoundDeviceDialogPage::saveSettings); connect(dialog.data(), &QDialog::accepted, appearenceDialog, &AppearenceDialogPage::saveSettings); dialog->exec(); } void MainWindowEditor::save() { m_repository->sync(); } void MainWindowEditor::quit() { if (queryClose()) { qApp->quit(); } } bool MainWindowEditor::queryClose() { if (!m_repository->modified()) { return true; } int result = KMessageBox::warningYesNoCancel(nullptr, i18nc("@info", "The currently open course contains unsaved changes. Do you want to save them?")); switch(result) { case KMessageBox::Yes: m_repository->sync(); return true; case KMessageBox::No: return true; default: return false; } }