[education/gcompris] /: core, initial idea of sequence
Johnny Jazeix
null at kde.org
Wed Apr 8 10:39:09 BST 2026
Git commit 2bfc8f690c7b5e1ca7a119e490dadc10c7942969 by Johnny Jazeix.
Committed on 06/04/2026 at 16:17.
Pushed by jjazeix into branch 'master'.
core, initial idea of sequence
M +4 -0 docs/docbook/index.docbook
M +20 -0 src/activities/menu/Menu.qml
M +36 -2 src/core/ActivityBase.qml
M +4 -0 src/core/ActivityInfo.cpp
M +5 -0 src/core/ActivityInfo.h
M +33 -0 src/core/ActivityInfoTree.cpp
M +14 -0 src/core/ActivityInfoTree.h
M +12 -5 src/core/Bar.qml
M +2 -0 src/core/CMakeLists.txt
M +2 -1 src/core/ChangeLog.qml
A +34 -0 src/core/Sequence.cpp [License: GPL(v3.0+)]
A +59 -0 src/core/Sequence.h [License: GPL(v3.0+)]
M +15 -0 src/core/main.cpp
M +3 -0 src/core/main.qml
https://invent.kde.org/education/gcompris/-/commit/2bfc8f690c7b5e1ca7a119e490dadc10c7942969
diff --git a/docs/docbook/index.docbook b/docs/docbook/index.docbook
index 5f087f6fa..95708ad53 100644
--- a/docs/docbook/index.docbook
+++ b/docs/docbook/index.docbook
@@ -340,6 +340,10 @@ or make it show real car images instead of filled rectangles (traffic).</para>
<entry>--locale=locale</entry>
<entry>Run &gcompris; with the specified locale.</entry>
</row>
+<row>
+<entry>--with-sequence=sequence.json</entry>
+<entry>Run &gcompris; with the specific sequence. A sequence is a list of activities with specific datasets, forcing GCompris to run them in the specified order.</entry>
+</row>
</tbody>
</tgroup>
</informaltable>
diff --git a/src/activities/menu/Menu.qml b/src/activities/menu/Menu.qml
index 0116a9ed6..e07856c85 100644
--- a/src/activities/menu/Menu.qml
+++ b/src/activities/menu/Menu.qml
@@ -168,6 +168,8 @@ ActivityBase {
signal startActivity(string activityName, int level)
+ signal startNextActivityInSequence
+
pageComponent: Image {
id: activityBackground
source: activity.url + "background.svg"
@@ -744,6 +746,7 @@ ActivityBase {
function onStartActivity(activityName: string, level: int) {
ActivityInfoTree.setCurrentActivityFromName(activityName)
+ print(activityName, ActivityInfoTree.currentActivity.name)
var currentLevels = ActivityInfoTree.currentActivity.currentLevels
activityLoader.setSource("qrc:/gcompris/src/activities/" + ActivityInfoTree.currentActivity.name,
{
@@ -755,6 +758,23 @@ ActivityBase {
})
if (activityLoader.status == Loader.Ready) loadActivity()
}
+
+ function onStartNextActivityInSequence() {
+ // First go back to the menu
+ while(pageView.depth > 1) {
+ home()
+ }
+ print("next activity in sequence!")
+ // always start at level 0, we should load dataset in the order of the sequence!
+ var nextActivity = ActivityInfoTree.nextActivityInSequence();
+ if(nextActivity) {
+ activity.startActivity(nextActivity, 0);
+ }
+ else {
+ print("end of sequence!")
+ ActivityInfoTree.resetAfterSequence();
+ }
+ }
}
Connections {
diff --git a/src/core/ActivityBase.qml b/src/core/ActivityBase.qml
index 342f218d0..192b8c75f 100644
--- a/src/core/ActivityBase.qml
+++ b/src/core/ActivityBase.qml
@@ -209,14 +209,48 @@ Item {
}
onBack: (to) => menu ? menu.back(to) : ""
- onHome: menu ? menu.home() : ""
+ onHome: {
+ // if we are in a sequence and in an activity (not inside a dialog)
+ // and not in the menu (the menu activity is the only one where 'menu'
+ // is undefined, we display the dialog to confirm leaving the sequence
+ if(ActivityInfoTree.isInSequence && pageView.depth === 2 && menu) {
+ Core.showMessageDialog(activity.item,
+ qsTr("You are currently in a sequence, going back to the menu will cancel it, do you confirm?"),
+ qsTr("Yes"), function() {
+ // yes -> leave sequence and go back to menu
+ ActivityInfoTree.resetAfterSequence();
+ menu.home();
+ },
+ qsTr("No"), null,
+ function() { pageView.focus = true }
+ );
+ }
+ else {
+ if(menu) menu.home()
+ }
+ }
onDisplayDialog: (dialog) => menu ? menu.displayDialog(dialog) : ""
onDisplayDialogs: (dialogs) => menu ? menu.displayDialogs(dialogs) : ""
signal activityNextLevel
signal nextLevel
+
onNextLevel: {
- activityNextLevel();
+ if(ActivityInfoTree.isInSequence) {
+ print(activityInfo.name, "in sequence");
+ if(currentLevel+1 >= numberOfLevel) {
+ menu.startNextActivityInSequence();
+ }
+ else {
+ activityNextLevel();
+ }
+ // check if last level of sequence
+ // if yes, menu.loadNext()
+ // if no, go to next level. Maybe only load the datasets in the sequence, and ignore the other. To know if we are the last one, check the next level number and if 0, then change activity. We may need to move the number of levels in the ActivityBase instead of js
+ }
+ else {
+ activityNextLevel();
+ }
}
Keys.forwardTo: activity.children
diff --git a/src/core/ActivityInfo.cpp b/src/core/ActivityInfo.cpp
index 79535a8fe..69daed962 100644
--- a/src/core/ActivityInfo.cpp
+++ b/src/core/ActivityInfo.cpp
@@ -421,4 +421,8 @@ void ActivityInfo::resetLevels()
enableDatasetsBetweenDifficulties(1, 6);
}
+void ActivityInfo::restoreInitialLevels() {
+ setCurrentLevels();
+}
+
#include "moc_ActivityInfo.cpp"
diff --git a/src/core/ActivityInfo.h b/src/core/ActivityInfo.h
index 492877f61..7d42611e5 100644
--- a/src/core/ActivityInfo.h
+++ b/src/core/ActivityInfo.h
@@ -214,6 +214,11 @@ public:
*/
Q_INVOKABLE void resetLevels();
+ /*
+ * Restore the initial state of levels.
+ */
+ void restoreInitialLevels();
+
/*
* Remove the dataset from the activity
*/
diff --git a/src/core/ActivityInfoTree.cpp b/src/core/ActivityInfoTree.cpp
index 6c11cc42d..162b41104 100644
--- a/src/core/ActivityInfoTree.cpp
+++ b/src/core/ActivityInfoTree.cpp
@@ -445,6 +445,39 @@ QVariantList ActivityInfoTree::allCharacters()
return keyboardCharacters;
}
+void ActivityInfoTree::initializeSequence(const QByteArray &jsonContent) {
+ m_sessionSequence.loadFromJson(jsonContent);
+}
+
+QString ActivityInfoTree::nextActivityInSequence() {
+ QString nextActivity = "";
+ if (m_currentActivityInSequence < m_sessionSequence.size()) {
+ const auto &[activityName, datasets] = m_sessionSequence.getNextActivity();
+ // Initialize the datasets to only have the needed ones!
+ for (auto *activity: m_menuTreeFull) {
+ if (activity->shortName() == activityName) {
+ activity->setCurrentLevels(datasets);
+ nextActivity = activity->name();
+ break;
+ }
+ }
+ }
+ return nextActivity;
+}
+
+void ActivityInfoTree::resetAfterSequence() {
+ for(const auto &[activityName, datasets] : m_sessionSequence) {
+ for (auto *activity: m_menuTreeFull) {
+ if (activity->shortName() == activityName) {
+ activity->restoreInitialLevels();
+ break;
+ }
+ }
+ }
+ m_sessionSequence.clear();
+ Q_EMIT isInSequenceChanged();
+}
+
void ActivityInfoTree::createDataset(const QJsonObject &dataset)
{
const QString datasetName(dataset["internal_name"].toString());
diff --git a/src/core/ActivityInfoTree.h b/src/core/ActivityInfoTree.h
index 1361affc9..94dd4f8a1 100644
--- a/src/core/ActivityInfoTree.h
+++ b/src/core/ActivityInfoTree.h
@@ -11,6 +11,7 @@
#define ACTIVITYINFOTREE_H
#include "ActivityInfo.h"
+#include "Sequence.h"
#include <QQmlEngine>
#include <QList>
#include <QDir>
@@ -35,6 +36,8 @@ class ActivityInfoTree : public QObject
*/
Q_PROPERTY(QString startingActivity MEMBER m_startingActivity NOTIFY startingActivityChanged)
Q_PROPERTY(int startingLevel MEMBER m_startingLevel NOTIFY startingLevelChanged)
+ Q_PROPERTY(bool isInSequence READ isInSequence NOTIFY isInSequenceChanged)
+
public:
static ActivityInfoTree *getInstance()
{
@@ -66,6 +69,11 @@ public:
void removeDataset(const QJsonObject &dataset, bool clearCache = true);
void removeAllLocalDatasets();
+ void initializeSequence(const QByteArray &jsonContent);
+ bool isInSequence() {
+ return !m_sessionSequence.empty() && m_currentActivityInSequence <= m_sessionSequence.size();
+ }
+
protected:
static ActivityInfoTree *m_instance;
@@ -82,6 +90,8 @@ protected Q_SLOTS:
Q_INVOKABLE void filterBySearch(const QString &text);
Q_INVOKABLE void filterByDifficulty(quint32 levelMin, quint32 levelMax);
Q_INVOKABLE void setCurrentActivityFromName(const QString &name);
+ Q_INVOKABLE void resetAfterSequence();
+ Q_INVOKABLE QString nextActivityInSequence();
Q_SIGNALS:
void menuTreeChanged();
@@ -90,6 +100,7 @@ Q_SIGNALS:
void startingActivityChanged();
void startingLevelChanged();
void activitiesWithoutDatasets(QList<ActivityInfo *> activitiesWithoutActiveDatasets);
+ void isInSequenceChanged();
private:
explicit ActivityInfoTree(QObject *parent = nullptr);
@@ -111,6 +122,9 @@ private:
QStringList getActivityList();
+ int m_currentActivityInSequence = 0;
+ Sequence m_sessionSequence;
+
public:
static void registerResources();
static ActivityInfoTree *create(QQmlEngine *engine, QJSEngine *scriptEngine);
diff --git a/src/core/Bar.qml b/src/core/Bar.qml
index d9f057521..cb88be6cc 100644
--- a/src/core/Bar.qml
+++ b/src/core/Bar.qml
@@ -37,6 +37,7 @@ import "qrc:/gcompris/src/core/core.js" as Core
*/
Item {
id: bar
+
/**
* type: real
* Keeps track of the number of buttons that are displayed
@@ -180,17 +181,17 @@ Item {
{
'bid': previous,
'contentId': content.level,
- 'allowed': true
+ 'allowed': !ActivityInfoTree.isInSequence
},
{
'bid': levelText,
'contentId': content.level,
- 'allowed': true
+ 'allowed': !ActivityInfoTree.isInSequence
},
{
'bid': next,
'contentId': content.level,
- 'allowed': true
+ 'allowed': !ActivityInfoTree.isInSequence
},
{
'bid': repeat,
@@ -205,7 +206,7 @@ Item {
{
'bid': config,
'contentId': content.config,
- 'allowed': !ApplicationSettings.isKioskMode
+ 'allowed': !ApplicationSettings.isKioskMode && !ActivityInfoTree.isInSequence
},
{
'bid': hint,
@@ -215,7 +216,7 @@ Item {
{
'bid': activityConfigImage,
'contentId': content.activityConfig,
- 'allowed': !ApplicationSettings.isKioskMode
+ 'allowed': !ApplicationSettings.isKioskMode && !ActivityInfoTree.isInSequence
},
{
'bid': downloadImage,
@@ -290,6 +291,12 @@ Item {
buttonModel = newButtonModel;
}
+ Connections {
+ target: ActivityInfoTree
+ function onIsInSequenceChanged() {
+ bar.updateContent();
+ }
+ }
Connections {
target: bar.content
function onValueChanged() {
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index bf2655c58..d0be8f6e1 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -38,6 +38,8 @@ set(gcompris_SRCS
ClientNetworkMessages.cpp
ClientNetworkMessages.h
netconst.h
+ Sequence.cpp
+ Sequence.h
synth/ADSRenvelope.cpp
synth/ADSRenvelope.h
GSynth.cpp
diff --git a/src/core/ChangeLog.qml b/src/core/ChangeLog.qml
index 96abc88f4..70f9aed36 100644
--- a/src/core/ChangeLog.qml
+++ b/src/core/ChangeLog.qml
@@ -26,7 +26,8 @@ QtObject {
*
*/
property var changelog: [
- { "versionCode": 270000, "content": [ // versionCode TBD...
+ { "versionCode": 270000, "content": [
+ qsTr("New command-line option to run a list of activities with specific datasets in sequence (--with-sequence sequence.json)"),
qsTr("Many usability improvements"),
qsTr("Many bug fixes")
],
diff --git a/src/core/Sequence.cpp b/src/core/Sequence.cpp
new file mode 100644
index 000000000..9c352fd4e
--- /dev/null
+++ b/src/core/Sequence.cpp
@@ -0,0 +1,34 @@
+/* GCompris - Sequence.cpp
+ *
+ * SPDX-FileCopyrightText: 2026 Johnny Jazeix <jazeix at gmail.com>
+ *
+ * Authors:
+ * Johnny Jazeix <jazeix at gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+#include "Sequence.h"
+#include <QJsonDocument>
+#include <QJsonValue>
+#include <QJsonArray>
+#include <QJsonObject>
+
+void Sequence::loadFromJson(const QByteArray &jsonContent) {
+ m_currentActivityIndex = 0;
+ m_sequences.clear();
+ QJsonParseError error;
+ QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonContent, &error);
+
+ if(jsonDocument.isEmpty()) {
+ qWarning() << "Error parsing the sequence" << error.errorString();
+ }
+ if (const QJsonValue v = jsonDocument["sequence"]; v.isArray()) {
+ const QJsonArray sequences = v.toArray();
+ for (int i = 0; i < sequences.size(); i++) {
+ const QJsonObject sequence = sequences[i].toObject();
+ const QString activity = sequence["activity"].toString();
+ const QStringList datasets = sequence["datasets"].toVariant().toStringList();
+ m_sequences.push_back({activity, datasets});
+ }
+ }
+}
diff --git a/src/core/Sequence.h b/src/core/Sequence.h
new file mode 100644
index 000000000..bbcd2eca1
--- /dev/null
+++ b/src/core/Sequence.h
@@ -0,0 +1,59 @@
+/* GCompris - Sequence.h
+ *
+ * SPDX-FileCopyrightText: 2026 Johnny Jazeix <jazeix at gmail.com>
+ *
+ * Authors:
+ * Johnny Jazeix <jazeix at gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+#ifndef SEQUENCE_H
+#define SEQUENCE_H
+
+#include <QList>
+#include <QString>
+#include <utility>
+
+// activity name, dataset names
+using SequenceElement = std::pair<QString, QStringList>;
+
+class Sequence
+{
+public:
+ const int currentActivityIndex() {
+ return m_currentActivityIndex;
+ }
+
+ const SequenceElement& getNextActivity() {
+ return m_sequences[m_currentActivityIndex++];
+ }
+
+ const qsizetype size() {
+ return m_sequences.size();
+ }
+ const SequenceElement &operator[](qsizetype index) const {
+ return m_sequences[index];
+ }
+ void clear() {
+ m_sequences.clear();
+ }
+ bool empty() const {
+ return m_sequences.empty();
+ }
+
+ // For c++17 for range loop
+ QList<SequenceElement>::const_iterator begin() const {
+ return m_sequences.begin();
+ }
+ QList<SequenceElement>::const_iterator end() const {
+ return m_sequences.end();
+ }
+
+ void loadFromJson(const QByteArray &jsonContent);
+
+private:
+ int m_currentActivityIndex;
+ QList<SequenceElement> m_sequences;
+};
+
+#endif // SEQUENCE_H
diff --git a/src/core/main.cpp b/src/core/main.cpp
index 7b6b25ac3..f76ac44ac 100644
--- a/src/core/main.cpp
+++ b/src/core/main.cpp
@@ -145,6 +145,10 @@ int main(int argc, char *argv[])
QObject::tr("Specify on which level to start the activity. Only used when --launch option is used."), "startLevel");
parser.addOption(clStartOnLevel);
+ QCommandLineOption clWithSequence("with-sequence",
+ QObject::tr("Start the sequence of activities passed in the json file."), "jsonSequence");
+ parser.addOption(clWithSequence);
+
QCommandLineOption clLocale("locale",
QObject::tr("Specify the locale when starting GCompris."), "locale");
parser.addOption(clLocale);
@@ -238,6 +242,17 @@ int main(int argc, char *argv[])
ApplicationSettings::getInstance()->setLocale(locale);
}
}
+ if (parser.isSet(clWithSequence)) {
+ QString sequenceFilename = parser.value(clWithSequence);
+ QFile sequenceFile(sequenceFilename);
+ if(!sequenceFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ qWarning() << QString("Error: Unable to open the sequence file %1.").arg(sequenceFilename);
+ }
+ else {
+ QByteArray jsonContent = sequenceFile.readAll();
+ ActivityInfoTree::getInstance()->initializeSequence(jsonContent);
+ }
+ }
// This will be removed later, for now, it is ignored if --renderer option is set
if (parser.isSet(clSoftwareRenderer)) {
ApplicationSettings::getInstance()->setRenderer(QStringLiteral("software"));
diff --git a/src/core/main.qml b/src/core/main.qml
index b6b44b295..13f0251c4 100644
--- a/src/core/main.qml
+++ b/src/core/main.qml
@@ -443,6 +443,9 @@ Window {
welcomePlayed = true;
startApplicationTimer.start();
}
+ else if(ActivityInfoTree.isInSequence) {
+ pageView.currentItem.startNextActivityInSequence();
+ }
else if (DownloadManager.areVoicesRegistered(ApplicationSettings.locale)) {
delayedWelcomeTimer.playWelcome();
}
More information about the kde-doc-english
mailing list