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"