diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index b10de60..5342f07 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,67 +1,78 @@ ### -# Copyright 2013-2014 Andreas Cord-Landwehr +# 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 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 artikulatecore Qt5::Test ) add_test(test_repository test_repository) ecm_mark_as_test(test_repository) +# 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_repository test_repository) +ecm_mark_as_test(test_repository) + 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/trainingsession/test_trainingsession.cpp b/autotests/trainingsession/test_trainingsession.cpp new file mode 100644 index 0000000..b29345f --- /dev/null +++ b/autotests/trainingsession/test_trainingsession.cpp @@ -0,0 +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 + { + 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/autotests/trainingsession/test_trainingsession.h b/autotests/trainingsession/test_trainingsession.h new file mode 100644 index 0000000..e10dda5 --- /dev/null +++ b/autotests/trainingsession/test_trainingsession.h @@ -0,0 +1,70 @@ +/* + * 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_TRAININGSESSION_H +#define TEST_TRAININGSESSION_H + +#include + +class TestTrainingSession : public QObject +{ + Q_OBJECT + +public: + TestTrainingSession() = default; + +private Q_SLOTS: + /** + * Called before every test case. + */ + void init(); + + /** + * Called after every test case. + */ + void cleanup(); + + /** + * @brief Construct and destruct training session without units + */ + void createTrainingSessionWithoutUnits(); + + /** + * @brief Construct training session and check that phrases without sound file paths are skipped + */ + void createTrainingSessionWithEmptySounds(); + + /** + * @brief Construct and destruct training session only empty units + */ + void createTrainingSessionWithEmptyUnits(); + + /** + * @brief Construct and destruct training session only non-empty units + */ + void createTrainingSessionWithUnitsAndPhrases(); + + /** + * @brief Test for all iterator functionality + */ + void iterateCourse(); +}; + +#endif diff --git a/liblearnerprofile/src/profilemanager.cpp b/liblearnerprofile/src/profilemanager.cpp index bcecc03..8d2d037 100644 --- a/liblearnerprofile/src/profilemanager.cpp +++ b/liblearnerprofile/src/profilemanager.cpp @@ -1,304 +1,308 @@ /* * Copyright 2013-2014 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) 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 6 of version 3 of the license. * * This library 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 library. If not, see . */ #include "profilemanager.h" #include "storage.h" #include "learner.h" #include #include #include "liblearner_debug.h" #include #include #include #include using namespace LearnerProfile; ///BEGIN: ProfileManagerPrivate namespace LearnerProfile { class ProfileManagerPrivate { public: ProfileManagerPrivate(); ~ProfileManagerPrivate() {} void sync(); QList m_profiles; Learner *m_activeProfile; QList m_goals; KConfig *m_config; Storage m_storage; }; } LearnerProfile::ProfileManagerPrivate::ProfileManagerPrivate() : m_profiles(QList()) , m_activeProfile(nullptr) , m_config(nullptr) { // load all profiles from storage m_goals.append(m_storage.loadGoals()); m_profiles.append(m_storage.loadProfiles(m_goals)); // set last used profile m_config = new KConfig(QStringLiteral("learnerprofilerc")); KConfigGroup activeProfileGroup(m_config, "ActiveProfile"); int lastProfileId = activeProfileGroup.readEntry("profileId", "0").toInt(); QList activeGoalsCategories = activeProfileGroup.readEntry("activeGoalsCategories", QList()); QList activeGoalsIdentifiers = activeProfileGroup.readEntry("activeGoalsIdentifiers", QList()); foreach (Learner *learner, m_profiles) { if (learner->identifier() == lastProfileId) { m_activeProfile = learner; // set active goals if (activeGoalsCategories.count() == activeGoalsIdentifiers.count()) { for (int i = 0; i < activeGoalsCategories.count(); ++i) { m_activeProfile->setActiveGoal( static_cast(activeGoalsCategories.at(i)), activeGoalsIdentifiers.at(i)); } } else { - qCritical() << "Inconsistent goal category / identifier pairs found: aborting."; + qCCritical(LIBLEARNER_LOG()) << "Inconsistent goal category / identifier pairs found: aborting."; } break; } } if (m_activeProfile == nullptr) { - qCDebug(LIBLEARNER_LOG) << "No last active profile found, falling back to first found profile"; + qCDebug(LIBLEARNER_LOG()) << "No last active profile found, falling back to first found profile"; if (m_profiles.size() > 0) { m_activeProfile = m_profiles.at(0); } } } void ProfileManagerPrivate::sync() { // sync last used profile data if (m_activeProfile) { KConfigGroup activeProfileGroup(m_config, "ActiveProfile"); activeProfileGroup.writeEntry("profileId", m_activeProfile->identifier()); // compute activer learning goals by category QList goalCatogries; QList goalIdentifiers; // compute used goals foreach (LearningGoal *goal, m_activeProfile->goals()) { if (!goalCatogries.contains(static_cast(goal->category()))) { goalCatogries.append(static_cast(goal->category())); } } // compute active goals foreach (int category, goalCatogries) { goalIdentifiers.append(m_activeProfile->activeGoal(static_cast(category))->identifier()); } activeProfileGroup.writeEntry("activeGoalsCategories", goalCatogries); activeProfileGroup.writeEntry("activeGoalsIdentifiers", goalIdentifiers); } else { - qCritical() << "No active profile selected, aborting sync."; + qCCritical(LIBLEARNER_LOG()) << "No active profile selected, aborting sync."; } m_config->sync(); //TODO only sync changed learner foreach (Learner *learner, m_profiles) { m_storage.storeProfile(learner); } } ///END: ProfileManagerPrivate ProfileManager::ProfileManager(QObject *parent) : QObject(parent) , d(new ProfileManagerPrivate) { connect (this, &ProfileManager::profileAdded, this, &ProfileManager::profileCountChanged); connect (this, &ProfileManager::profileRemoved, this, &ProfileManager::profileCountChanged); foreach (Learner *learner, d->m_profiles) { connect(learner, &Learner::goalRemoved, this, &ProfileManager::removeLearningGoal); } } ProfileManager::~ProfileManager() { foreach (Learner *learner, d->m_profiles) { learner->deleteLater(); } } QList< Learner* > ProfileManager::profiles() const { return d->m_profiles; } int ProfileManager::profileCount() const { return profiles().length(); } void ProfileManager::openImageFileDialog() { const QString imagePath = QFileDialog::getOpenFileName( nullptr, i18n("Open Image"), QLatin1String(""), i18n("Image Files (*.png *.jpg *.bmp)")); d->m_activeProfile->importImage(imagePath); } Learner * ProfileManager::addProfile(const QString &name) { Learner *learner = new Learner(this); learner->setName(name); // set id int maxUsedId = 0; foreach (Learner *cpLearner, d->m_profiles) { if (cpLearner->identifier() >= maxUsedId) { maxUsedId = cpLearner->identifier(); } } learner->setIdentifier(maxUsedId + 1); d->m_profiles.append(learner); d->m_storage.storeProfile(learner); emit profileAdded(learner, d->m_profiles.count() - 1); if (activeProfile() == nullptr) { setActiveProfile(learner); } connect (learner, SIGNAL(goalRemoved(Learner*,LearningGoal*)), this, SLOT(removeLearningGoal(Learner*,LearningGoal*))); return learner; } void ProfileManager::removeProfile(Learner *learner) { int index = d->m_profiles.indexOf(learner); if (index < 0) { - qCWarning(LIBLEARNER_LOG) << "Profile was not found, aborting"; + qCWarning(LIBLEARNER_LOG()) << "Profile was not found, aborting"; return; } emit profileAboutToBeRemoved(index); d->m_profiles.removeAt(index); d->m_storage.removeProfile(learner); if (d->m_activeProfile == learner) { if (d->m_profiles.isEmpty()) { setActiveProfile(nullptr); } else { setActiveProfile(d->m_profiles.at(0)); } } emit profileRemoved(); } void ProfileManager::removeLearningGoal(Learner* learner, LearningGoal* goal) { d->m_storage.removeRelation(learner, goal); } Learner * ProfileManager::profile(int index) { if (index < 0 || index >= profiles().count()) { return nullptr; } return profiles().at(index); } QList< LearningGoal* > ProfileManager::goals() const { return d->m_goals; } LearningGoal * ProfileManager::registerGoal(LearningGoal::Category category, const QString &identifier, const QString &name) { // test whether goal is already registered foreach (LearningGoal *cmpGoal, d->m_goals) { if (cmpGoal->category() == category && cmpGoal->identifier() == identifier) { return cmpGoal; } } LearningGoal *goal = new LearningGoal(category, identifier, this); goal->setName(name); d->m_goals.append(goal); d->m_storage.storeGoal(goal); return goal; } LearnerProfile::LearningGoal * LearnerProfile::ProfileManager::goal( LearningGoal::Category category, const QString& identifier) const { foreach (LearningGoal *goal, d->m_goals) { if (goal->category() == category && goal->identifier() == identifier) { return goal; } } return nullptr; } void ProfileManager::recordProgress(Learner *learner, LearningGoal *goal, const QString &container, const QString &item, int logPayload, int valuePayload) { + if (!learner) { + qCDebug(LIBLEARNER_LOG()) << "No learner set, no data stored"; + return; + } d->m_storage.storeProgressLog(learner, goal, container, item, logPayload, QDateTime::currentDateTime()); d->m_storage.storeProgressValue(learner, goal, container, item, valuePayload); } QHash ProfileManager::progressValues(Learner *learner, LearningGoal *goal, const QString &container) const { if (!learner || !goal) { return QHash(); } return d->m_storage.readProgressValues(learner, goal, container); } void ProfileManager::sync() { d->sync(); } void ProfileManager::sync(Learner *learner) { d->m_storage.storeProfile(learner); } Learner * ProfileManager::activeProfile() const { return d->m_activeProfile; } void ProfileManager::setActiveProfile(Learner* learner) { if (learner == d->m_activeProfile) { return; } d->m_activeProfile = learner; emit activeProfileChanged(); } diff --git a/src/core/course.h b/src/core/course.h index c1fa6c3..ce75ec3 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; + QUrl file() const override; void setFile(const QUrl &file); QList unitList() const 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 4238717..a26dc47 100644 --- a/src/core/icourse.h +++ b/src/core/icourse.h @@ -1,67 +1,68 @@ /* * 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; + 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/trainingsession.cpp b/src/core/trainingsession.cpp index 9a6eca7..4fadb52 100644 --- a/src/core/trainingsession.cpp +++ b/src/core/trainingsession.cpp @@ -1,273 +1,302 @@ /* * 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(QObject *parent) +TrainingSession::TrainingSession(LearnerProfile::ProfileManager *manager, QObject *parent) : QObject(parent) - , m_profileManager(nullptr) + , m_profileManager(manager) , m_course(nullptr) - , m_unit(nullptr) { - -} - -void TrainingSession::setProfileManager(LearnerProfile::ProfileManager *manager) -{ - if (m_profileManager == manager) { - return; - } - m_profileManager = manager; + 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() ); - Q_FOREACH(Unit *unit, m_course->unitList()) { - Q_FOREACH(Phrase *phrase, unit->phraseList()) { + 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::unit() const +Unit * TrainingSession::activeUnit() const { - return m_unit; + if (auto phrase = activePhrase()) { + return phrase->unit(); + } + return nullptr; } void TrainingSession::setUnit(Unit *unit) { - if (m_unit == unit) { - return; - } - m_unit = unit; - if (m_unit && m_unit->phraseList().count() > 0) { - setPhrase(m_unit->phraseList().constFirst()); + // 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; + } + } } - emit unitChanged(); } 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; } void TrainingSession::updateGoal() { if (!m_profileManager) { - qCWarning(ARTIKULATE_LOG) << "No ProfileManager registered, aborting operation"; + 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"; + 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() +QVector TrainingSession::trainingActions() const +{ + return m_actions; +} + +void TrainingSession::updateTrainingActions() { - // cleanup for (const auto &action : qAsConst(m_actions)) { action->deleteLater(); } m_actions.clear(); if (!m_course) { - return QVector(); + 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(); } } - return m_actions; + + // 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/core/trainingsession.h b/src/core/trainingsession.h index 141ce77..5467ad7 100644 --- a/src/core/trainingsession.h +++ b/src/core/trainingsession.h @@ -1,89 +1,96 @@ /* * 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 TRAININGSESSION_H #define TRAININGSESSION_H #include "artikulatecore_export.h" #include "phrase.h" #include class QString; class Language; class ICourse; class Unit; class TrainingAction; namespace LearnerProfile { class ProfileManager; } /** * \class TrainingSession */ class ARTIKULATECORE_EXPORT TrainingSession : public QObject { Q_OBJECT Q_PROPERTY(ICourse *course READ course WRITE setCourse NOTIFY courseChanged) - Q_PROPERTY(Unit *unit READ unit WRITE setUnit NOTIFY unitChanged) + Q_PROPERTY(Unit *unit READ activeUnit WRITE setUnit NOTIFY phraseChanged) Q_PROPERTY(Phrase *phrase READ activePhrase WRITE setPhrase NOTIFY phraseChanged) Q_PROPERTY(bool hasNext READ hasNext NOTIFY phraseChanged) public: - explicit TrainingSession(QObject *parent = nullptr); + explicit TrainingSession(LearnerProfile::ProfileManager *manager, QObject *parent = nullptr); - void setProfileManager(LearnerProfile::ProfileManager *manager); ICourse * course() const; void setCourse(ICourse *course); - Unit * unit() const; + Unit * activeUnit() const; void setUnit(Unit *unit); TrainingAction * activeAction() const; Phrase * activePhrase() const; void setPhrase(Phrase *phrase); - bool hasPreviousPhrase() const; + bool hasPrevious() const; bool hasNext() const; Q_INVOKABLE void accept(); Q_INVOKABLE void skip(); - QVector trainingActions(); + /** + * @brief Return tree of training actions + * + * The return actions form a 2-level hierarchy: + * - the first level are all units + * - the unit actions may contain sub-actions, which are the phrases + * + * @note phrases without sound file paths are skipped when generating actions + */ + QVector trainingActions() const; Q_SIGNALS: void courseChanged(); - void unitChanged(); void phraseChanged(); /** * @brief Emitted when last phrase of session is skipped or marked as completed. */ void completed(); private: Q_DISABLE_COPY(TrainingSession) + void updateTrainingActions(); void selectNextPhrase(); Phrase * nextPhrase() const; void updateGoal(); LearnerProfile::ProfileManager *m_profileManager; ICourse *m_course; - Unit *m_unit; QVector m_actions; int m_indexUnit{-1}; int m_indexPhrase{-1}; }; #endif diff --git a/src/core/unit.cpp b/src/core/unit.cpp index 2c81b9f..634009c 100644 --- a/src/core/unit.cpp +++ b/src/core/unit.cpp @@ -1,164 +1,163 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * Copyright 2013 Oindrila Gupta * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "unit.h" #include "phrase.h" #include #include #include #include #include #include "artikulate_debug.h" #include #include Unit::Unit(QObject *parent) : QObject(parent) , m_course(nullptr) , m_phraseSignalMapper(new QSignalMapper(this)) { } Unit::~Unit() { m_phraseSignalMapper->deleteLater(); } QString Unit::id() const { return m_id; } void Unit::setId(const QString &id) { if (id != m_id) { m_id = id; emit idChanged(); emit modified(); } } QString Unit::foreignId() const { return m_foreignId; } void Unit::setForeignId(const QString &id) { m_foreignId = id; } ICourse *Unit::course() const { return m_course; } void Unit::setCourse(ICourse *course) { if (course == m_course) { return; } m_course = course; emit courseChanged(); } QString Unit::title() const { return m_title; } void Unit::setTitle(const QString &title) { if (QString::compare(title, m_title) != 0) { m_title = title; emit titleChanged(); emit modified(); } } QList< Phrase* > Unit::phraseList() const { return m_phraseList; } void Unit::addPhrase(Phrase *phrase) { QList::ConstIterator iter = m_phraseList.constBegin(); while (iter != m_phraseList.constEnd()) { if (phrase->id() == (*iter)->id()) { - qCWarning(ARTIKULATE_LOG) << "Phrase is already contained in this unit, aborting"; + qCWarning(ARTIKULATE_LOG()) << "Phrase is already contained in this unit, aborting"; return; } ++iter; } phrase->setUnit(this); - emit phraseAboutToBeAdded(phrase, m_phraseList.length()); m_phraseList.append(phrase); m_phraseSignalMapper->setMapping(phrase, phrase->id()); emit phraseAdded(phrase); connect(phrase, &Phrase::typeChanged, m_phraseSignalMapper, static_cast(&QSignalMapper::map)); connect(phrase, &Phrase::modified, this, &Unit::modified); emit modified(); } QList Unit::excludedSkeletonPhraseList() const { QList excludedPhraseList; QList::ConstIterator iter = m_phraseList.constBegin(); while (iter != m_phraseList.constEnd()) { if ((*iter)->isExcluded() == true) { excludedPhraseList.append(*iter); } ++iter; } return excludedPhraseList; } void Unit::excludeSkeletonPhrase(const QString &phraseId) { foreach (Phrase *phrase, m_phraseList) { if (phrase->id() == phraseId) { phrase->setExcluded(true); emit modified(); return; } } qCWarning(ARTIKULATE_LOG) << "Could not exclude phrase with ID " << phraseId << ", no phrase with this ID."; } void Unit::includeSkeletonPhrase(const QString &phraseId) { foreach (Phrase *phrase, m_phraseList) { if (phrase->id() == phraseId) { phrase->setExcluded(false); emit modified(); return; } } qCWarning(ARTIKULATE_LOG) << "Could not include phrase with ID " << phraseId << ", no phrase with this ID."; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 8fd01e5..1d2ffdd 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,218 +1,217 @@ /* * 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/trainingsession.h" #include "core/editorsession.h" #include "core/resources/courseresource.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(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(); - m_trainingSession->setProfileManager(m_profileManager); // 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(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(); 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::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; }