File indexing completed on 2024-03-24 15:14:39

0001 /* GCompris - ActivityInfoTree.cpp
0002  *
0003  * SPDX-FileCopyrightText: 2014 Bruno Coudoin <bruno.coudoin@gcompris.net>
0004  *
0005  * Authors:
0006  *   Bruno Coudoin <bruno.coudoin@gcompris.net>
0007  *
0008  *   SPDX-License-Identifier: GPL-3.0-or-later
0009  */
0010 #include "ActivityInfoTree.h"
0011 #include "ApplicationInfo.h"
0012 
0013 #include <QtDebug>
0014 #include <QQmlProperty>
0015 #include <QQmlComponent>
0016 #include <QResource>
0017 #include <QStandardPaths>
0018 #include <QCoreApplication>
0019 #include <QTextStream>
0020 
0021 QString ActivityInfoTree::m_startingActivity = "";
0022 int ActivityInfoTree::m_startingLevel = -1;
0023 ActivityInfoTree *ActivityInfoTree::m_instance = nullptr;
0024 
0025 ActivityInfoTree::ActivityInfoTree(QObject *parent) :
0026     QObject(parent),
0027     m_rootMenu(nullptr),
0028     m_currentActivity(nullptr)
0029 {
0030 }
0031 
0032 void ActivityInfoTree::setRootMenu(ActivityInfo *rootMenu)
0033 {
0034     m_rootMenu = rootMenu;
0035 }
0036 
0037 ActivityInfo *ActivityInfoTree::getRootMenu() const
0038 {
0039     return m_rootMenu;
0040 }
0041 
0042 QQmlListProperty<ActivityInfo> ActivityInfoTree::menuTree()
0043 {
0044     return { this, nullptr, &menuTreeCount, &menuTreeAt };
0045 }
0046 
0047 QList<ActivityInfo>::size_type ActivityInfoTree::menuTreeCount(QQmlListProperty<ActivityInfo> *property)
0048 {
0049     ActivityInfoTree *obj = qobject_cast<ActivityInfoTree *>(property->object);
0050     if (obj != nullptr)
0051         return obj->m_menuTree.count();
0052 
0053     return 0;
0054 }
0055 
0056 ActivityInfo *ActivityInfoTree::menuTreeAt(QQmlListProperty<ActivityInfo> *property, QList<ActivityInfo>::size_type index)
0057 {
0058     ActivityInfoTree *obj = qobject_cast<ActivityInfoTree *>(property->object);
0059     if (obj != nullptr)
0060         return obj->m_menuTree.at(index);
0061 
0062     return nullptr;
0063 }
0064 
0065 QQmlListProperty<ActivityInfo> ActivityInfoTree::getMenuTreeFull()
0066 {
0067     return { this, nullptr, &menuTreeFullCount, &menuTreeFullAt };
0068 }
0069 
0070 QList<ActivityInfo>::size_type ActivityInfoTree::menuTreeFullCount(QQmlListProperty<ActivityInfo> *property)
0071 {
0072     ActivityInfoTree *obj = qobject_cast<ActivityInfoTree *>(property->object);
0073     if (obj != nullptr)
0074         return obj->m_menuTreeFull.count();
0075 
0076     return 0;
0077 }
0078 
0079 ActivityInfo *ActivityInfoTree::menuTreeFullAt(QQmlListProperty<ActivityInfo> *property, QList<ActivityInfo>::size_type index)
0080 {
0081     ActivityInfoTree *obj = qobject_cast<ActivityInfoTree *>(property->object);
0082     if (obj != nullptr)
0083         return obj->m_menuTreeFull.at(index);
0084 
0085     return nullptr;
0086 }
0087 
0088 ActivityInfo *ActivityInfoTree::menuTree(int index) const
0089 {
0090     return m_menuTree.at(index);
0091 }
0092 
0093 void ActivityInfoTree::setCurrentActivityFromName(const QString &name)
0094 {
0095     const auto &constMenuTreeFull = m_menuTreeFull;
0096     for (const auto &activity: constMenuTreeFull) {
0097         if (activity->name() == name) {
0098             m_currentActivity = activity;
0099             Q_EMIT currentActivityChanged();
0100             break;
0101         }
0102     }
0103 }
0104 
0105 void ActivityInfoTree::setCurrentActivity(ActivityInfo *currentActivity)
0106 {
0107     m_currentActivity = currentActivity;
0108     Q_EMIT currentActivityChanged();
0109 }
0110 
0111 ActivityInfo *ActivityInfoTree::getCurrentActivity() const
0112 {
0113     return m_currentActivity;
0114 }
0115 
0116 void ActivityInfoTree::menuTreeAppend(ActivityInfo *menu)
0117 {
0118     m_menuTreeFull.append(menu);
0119 }
0120 
0121 void ActivityInfoTree::menuTreeAppend(QQmlEngine *engine,
0122                                       const QDir &menuDir, const QString &menuFile)
0123 {
0124     QQmlComponent component(engine,
0125                             QUrl::fromLocalFile(menuDir.absolutePath() + '/' + menuFile));
0126     QObject *object = component.create();
0127     if (component.isReady()) {
0128         if (QQmlProperty::read(object, "section").toString() == "/") {
0129             menuTreeAppend(qobject_cast<ActivityInfo *>(object));
0130         }
0131     }
0132     else {
0133         qDebug() << menuFile << ": Failed to load";
0134     }
0135 }
0136 
0137 void ActivityInfoTree::sortByDifficultyThenName(bool emitChanged)
0138 {
0139     std::sort(m_menuTree.begin(), m_menuTree.end(),
0140               [](const ActivityInfo *a, const ActivityInfo *b) {
0141                   /* clang-format off */
0142                   return (a->minimalDifficulty() < b->minimalDifficulty()) ||
0143                          (a->minimalDifficulty() == b->minimalDifficulty() && (a->name() < b->name()));
0144                   /* clang-format on */
0145               });
0146     if (emitChanged)
0147         Q_EMIT menuTreeChanged();
0148 }
0149 
0150 // Filter the current activity list by the given tag
0151 // the tag 'all' means no filter
0152 // the tag 'favorite' means only marked as favorite
0153 // The level is also filtered based on the global property
0154 void ActivityInfoTree::filterByTag(const QString &tag, const QString &category, bool emitChanged)
0155 {
0156     m_menuTree.clear();
0157     // https://www.kdab.com/goodbye-q_foreach/, for loops on QList may cause detach
0158     const auto constMenuTreeFull = m_menuTreeFull;
0159     for (const auto &activity: constMenuTreeFull) {
0160         // filter on category if given else on tag
0161         /* clang-format off */
0162         if(((!category.isEmpty() && activity->section().indexOf(category) != -1) ||
0163             (category.isEmpty() && activity->section().indexOf(tag) != -1) ||
0164             tag == "all" ||
0165             (tag == "favorite" && activity->favorite())) &&
0166             (activity->maximalDifficulty() >= ApplicationSettings::getInstance()->filterLevelMin() &&
0167              activity->minimalDifficulty() <= ApplicationSettings::getInstance()->filterLevelMax())) {
0168             m_menuTree.push_back(activity);
0169         }
0170         /* clang-format on */
0171     }
0172     sortByDifficultyThenName();
0173     if (emitChanged)
0174         Q_EMIT menuTreeChanged();
0175 }
0176 
0177 void ActivityInfoTree::filterByDifficulty(quint32 levelMin, quint32 levelMax)
0178 {
0179     auto it = std::remove_if(m_menuTree.begin(), m_menuTree.end(),
0180                              [&](const ActivityInfo *activity) {
0181                                  return activity->minimalDifficulty() < levelMin || activity->maximalDifficulty() > levelMax;
0182                              });
0183     m_menuTree.erase(it, m_menuTree.end());
0184 }
0185 
0186 void ActivityInfoTree::filterEnabledActivities(bool emitChanged)
0187 {
0188     auto it = std::remove_if(m_menuTree.begin(), m_menuTree.end(),
0189                              [](const ActivityInfo *activity) { return !activity->enabled(); });
0190     m_menuTree.erase(it, m_menuTree.end());
0191     if (emitChanged)
0192         Q_EMIT menuTreeChanged();
0193 }
0194 
0195 void ActivityInfoTree::filterCreatedWithinVersions(int firstVersion,
0196                                                    int lastVersion,
0197                                                    bool emitChanged)
0198 {
0199     m_menuTree.clear();
0200     const auto constMenuTreeFull = m_menuTreeFull;
0201     for (const auto &activity: constMenuTreeFull) {
0202         if (firstVersion < activity->createdInVersion() && activity->createdInVersion() <= lastVersion) {
0203             m_menuTree.push_back(activity);
0204         }
0205     }
0206     if (emitChanged)
0207         Q_EMIT menuTreeChanged();
0208 }
0209 
0210 void ActivityInfoTree::resetLevels(const QString &activityName)
0211 {
0212     auto activityIterator = std::find_if(m_menuTreeFull.begin(), m_menuTreeFull.end(), [&activityName](const ActivityInfo *value) {
0213         return activityName == value->name();
0214     });
0215     if (activityIterator == m_menuTreeFull.end()) {
0216         // We didn't find the activity
0217         return;
0218     }
0219     ActivityInfo *activity = *activityIterator;
0220     activity->resetLevels();
0221 }
0222 
0223 void ActivityInfoTree::exportAsSQL()
0224 {
0225     QTextStream qtOut(stdout);
0226 
0227     ApplicationSettings::getInstance()->setFilterLevelMin(1);
0228     ApplicationSettings::getInstance()->setFilterLevelMax(6);
0229     filterByTag("all");
0230 
0231     qtOut << "CREATE TABLE activities ("
0232           << "id INT UNIQUE, "
0233           << "name TEXT,"
0234           << "section TEXT,"
0235           << "author TEXT,"
0236           << "difficulty INT,"
0237           << "icon TEXT,"
0238           << "title TEXT,"
0239           << "description TEXT,"
0240           << "prerequisite TEXT,"
0241           << "goal TEXT,"
0242           << "manual TEXT,"
0243           << "credit TEXT);\n";
0244     qtOut << "DELETE FROM activities\n";
0245 
0246     int i(0);
0247     const auto constMenuTree = m_menuTree;
0248     for (const auto &activity: constMenuTree) {
0249         qtOut << "INSERT INTO activities VALUES(" << i++ << ", "
0250               << "'" << activity->name() << "', "
0251               << "'" << activity->section() << "', "
0252               << "'" << activity->author() << "', " << activity->difficulty() << ", "
0253               << "'" << activity->icon() << "', "
0254               << "\"" << activity->title() << "\", "
0255               << "\"" << activity->description() << "\", "
0256               << "\"" << activity->prerequisite() << "\", "
0257               << "\"" << activity->goal().toHtmlEscaped() << "\", "
0258               << "\"" << activity->manual().toHtmlEscaped() << "\", "
0259               << "\"" << activity->credit() << ");\n";
0260     }
0261     qtOut.flush();
0262 }
0263 
0264 void ActivityInfoTree::listActivities()
0265 {
0266     QTextStream qtOut(stdout);
0267     const QStringList list = ActivityInfoTree::getActivityList();
0268     for (const QString &activity: list) {
0269         qtOut << activity << '\n';
0270     }
0271     qtOut.flush();
0272 }
0273 
0274 QStringList ActivityInfoTree::getActivityList()
0275 {
0276     QStringList list;
0277     QFile file(":/gcompris/src/activities/activities_out.txt");
0278     if (!file.open(QFile::ReadOnly)) {
0279         qDebug() << "Failed to load the activity list";
0280         return list;
0281     }
0282     QTextStream in(&file);
0283     while (!in.atEnd()) {
0284         QString line = in.readLine();
0285         if (!line.startsWith(QLatin1String("#"))) {
0286             list << line;
0287         }
0288     }
0289     file.close();
0290     return list;
0291 }
0292 
0293 QObject *ActivityInfoTree::menuTreeProvider(QQmlEngine *engine, QJSEngine *scriptEngine)
0294 {
0295     Q_UNUSED(scriptEngine)
0296 
0297     ActivityInfoTree *menuTree = getInstance();
0298     QQmlComponent componentRoot(engine,
0299                                 QUrl("qrc:/gcompris/src/activities/menu/ActivityInfo.qml"));
0300     QObject *objectRoot = componentRoot.create();
0301     menuTree->setRootMenu(qobject_cast<ActivityInfo *>(objectRoot));
0302 
0303     const QStringList activities = getActivityList();
0304     QString startingActivity = m_startingActivity;
0305     for (const QString &line: activities) {
0306         QString url = QString("qrc:/gcompris/src/activities/%1/ActivityInfo.qml").arg(line);
0307         if (!QResource::registerResource(
0308                 ApplicationInfo::getFilePath(line + ".rcc")))
0309             qDebug() << "Failed to load the resource file " << line + ".rcc";
0310 
0311         QQmlComponent activityComponentRoot(engine, QUrl(url));
0312         QObject *activityObjectRoot = activityComponentRoot.create();
0313         if (activityObjectRoot != nullptr) {
0314             ActivityInfo *activityInfo = qobject_cast<ActivityInfo *>(activityObjectRoot);
0315             activityInfo->fillDatasets(engine);
0316             menuTree->menuTreeAppend(activityInfo);
0317 
0318             // Check if the activity is the one we want to start in and set the full name
0319             if (!startingActivity.isEmpty() && startingActivity == line) {
0320                 startingActivity = activityInfo->name();
0321             }
0322         }
0323         else {
0324             qDebug() << "ERROR: failed to load " << line << " " << activityComponentRoot.errors();
0325         }
0326     }
0327 
0328     // In case we have asked for a specific activity to start but the activity does not exist, we reinitialise the value
0329     if (m_startingActivity == startingActivity) {
0330         m_startingActivity = "";
0331     }
0332     else {
0333         m_startingActivity = startingActivity;
0334     }
0335 
0336     menuTree->filterByTag("favorite");
0337     menuTree->filterEnabledActivities();
0338     return menuTree;
0339 }
0340 
0341 void ActivityInfoTree::registerResources()
0342 {
0343     if (!QResource::registerResource(ApplicationInfo::getFilePath("core.rcc")))
0344         qDebug() << "Failed to load the resource file " << ApplicationInfo::getFilePath("core.rcc");
0345 
0346     if (!QResource::registerResource(ApplicationInfo::getFilePath("menu.rcc")))
0347         qDebug() << "Failed to load the resource file menu.rcc";
0348 
0349     if (!QResource::registerResource(ApplicationInfo::getFilePath("activities.rcc")))
0350         qDebug() << "Failed to load the resource file activities.rcc";
0351 }
0352 
0353 void ActivityInfoTree::filterBySearch(const QString &text)
0354 {
0355     m_menuTree.clear();
0356     if (!text.trimmed().isEmpty()) {
0357         // perform search on each word entered in the searchField
0358 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
0359         const QStringList wordsList = text.split(' ', Qt::SkipEmptyParts);
0360 #else
0361         const QStringList wordsList = text.split(' ', QString::SkipEmptyParts);
0362 #endif
0363         for (const QString &searchTerm: wordsList) {
0364             const QString trimmedText = searchTerm.trimmed();
0365             const auto &constMenuTreeFull = m_menuTreeFull;
0366             for (const auto &activity: constMenuTreeFull) {
0367                 /* clang-format off */
0368                 if (activity->title().remove(QChar::SoftHyphen).contains(trimmedText, Qt::CaseInsensitive) ||
0369                     activity->name().remove(QChar::SoftHyphen).contains(trimmedText, Qt::CaseInsensitive) ||
0370                     activity->description().remove(QChar::SoftHyphen).contains(trimmedText, Qt::CaseInsensitive)) {
0371                     /* clang-format on */
0372                     // add the activity only if it's not added
0373                     if (m_menuTree.indexOf(activity) == -1)
0374                         m_menuTree.push_back(activity);
0375                 }
0376             }
0377         }
0378     }
0379     else {
0380         m_menuTree = m_menuTreeFull;
0381     }
0382 
0383     filterEnabledActivities(false);
0384     filterByDifficulty(ApplicationSettings::getInstance()->filterLevelMin(), ApplicationSettings::getInstance()->filterLevelMax());
0385     sortByDifficultyThenName(false);
0386     Q_EMIT menuTreeChanged();
0387 }
0388 
0389 void ActivityInfoTree::minMaxFiltersChanged(quint32 levelMin, quint32 levelMax, bool doSynchronize)
0390 {
0391     for (ActivityInfo *activity: qAsConst(m_menuTreeFull)) {
0392         activity->enableDatasetsBetweenDifficulties(levelMin, levelMax);
0393     }
0394     if (doSynchronize) {
0395         ApplicationSettings::getInstance()->sync();
0396     }
0397 }
0398 
0399 QVariantList ActivityInfoTree::allCharacters()
0400 {
0401     QSet<QChar> keyboardChars;
0402     const auto constMenuTreeFull = m_menuTreeFull;
0403     for (const auto &tree: constMenuTreeFull) {
0404         const QString &title = tree->title();
0405         for (const QChar &letter: title) {
0406             if (letter.isLetterOrNumber() || letter == QLatin1Char('-')) {
0407                 keyboardChars.insert(letter.toLower());
0408             }
0409         }
0410     }
0411     for (const QChar &letters: keyboardChars) {
0412         m_keyboardCharacters.push_back(letters);
0413     }
0414     std::sort(m_keyboardCharacters.begin(), m_keyboardCharacters.end(), [](const QVariant &v1, const QVariant &v2) {
0415         return ApplicationInfo::getInstance()->localeCompare(v1.toString(), v2.toString()) < 0;
0416     });
0417 
0418     return m_keyboardCharacters;
0419 }
0420 
0421 #include "moc_ActivityInfoTree.cpp"