File indexing completed on 2024-04-21 14:43:50

0001 /* GCompris - main.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 <QtDebug>
0011 #include <QApplication>
0012 #include <QQuickWindow>
0013 #include <QQmlApplicationEngine>
0014 #include <QQmlComponent>
0015 #include <QStandardPaths>
0016 #include <QObject>
0017 #include <QTranslator>
0018 #include <QCommandLineParser>
0019 #include <QCursor>
0020 #include <QPixmap>
0021 #include <QSettings>
0022 
0023 #include "GComprisPlugin.h"
0024 #include "ApplicationInfo.h"
0025 #include "ActivityInfoTree.h"
0026 #include "DownloadManager.h"
0027 
0028 bool loadAndroidTranslation(QTranslator &translator, const QString &locale)
0029 {
0030     QFile file("assets:/share/GCompris/gcompris_" + locale + ".qm");
0031 
0032     file.open(QIODevice::ReadOnly);
0033     QDataStream in(&file);
0034     uchar *data = (uchar *)malloc(file.size());
0035 
0036     if (!file.exists())
0037         qDebug() << "file assets:/share/GCompris/gcompris_" << locale << ".qm does not exist";
0038 
0039     in.readRawData((char *)data, file.size());
0040 
0041     if (!translator.load(data, file.size())) {
0042         qDebug() << "Unable to load translation for locale " << locale << ", use en_US by default";
0043         free(data);
0044         return false;
0045     }
0046     // Do not free data, it is still needed by translator
0047     return true;
0048 }
0049 
0050 /**
0051  * Checks if the locale is supported. Locale may have been removed because
0052  * translation progress was not enough or invalid language put in configuration.
0053  */
0054 bool isSupportedLocale(const QString &locale)
0055 {
0056     bool isSupported = false;
0057     QQmlEngine engine;
0058     QQmlComponent component(&engine, QUrl("qrc:/gcompris/src/core/LanguageList.qml"));
0059     QObject *object = component.create();
0060     if (!object) {
0061         qWarning() << "isSupportedLocale:" << component.errors();
0062         return false;
0063     }
0064     QVariant variant = object->property("languages");
0065     QJSValue languagesList = variant.value<QJSValue>();
0066     const int length = languagesList.property("length").toInt();
0067     for (int i = 0; i < length; ++i) {
0068         if (languagesList.property(i).property("locale").toString() == locale) {
0069             isSupported = true;
0070         }
0071     }
0072     delete object;
0073     return isSupported;
0074 }
0075 // Return the locale
0076 QString loadTranslation(const QSettings &config, QTranslator &translator)
0077 {
0078     QString locale = config.value("General/locale", GC_DEFAULT_LOCALE).toString();
0079 
0080     if (!isSupportedLocale(locale)) {
0081         qDebug() << "locale" << locale << "not supported, defaulting to" << GC_DEFAULT_LOCALE;
0082         locale = GC_DEFAULT_LOCALE;
0083         ApplicationSettings::getInstance()->setLocale(locale);
0084     }
0085 
0086     if (locale == GC_DEFAULT_LOCALE)
0087         locale = QString(QLocale::system().name() + ".UTF-8");
0088 
0089     if (locale == "C.UTF-8")
0090         locale = "en_US.UTF-8";
0091 
0092     // Load translation
0093     // Remove .UTF8
0094     locale.remove(".UTF-8");
0095 
0096 #if defined(Q_OS_ANDROID)
0097     if (!loadAndroidTranslation(translator, locale))
0098         loadAndroidTranslation(translator, ApplicationInfo::localeShort(locale));
0099 #else
0100 
0101 #if (defined(Q_OS_LINUX) || defined(Q_OS_UNIX))
0102     // only useful for translators: load from $application_dir/../share/... if exists as it is where kde scripts install translations
0103     if (translator.load("gcompris_qt.qm", QString("%1/../share/locale/%2/LC_MESSAGES").arg(QCoreApplication::applicationDirPath(), locale))) {
0104         qDebug() << "load translation for locale " << locale << " in " << QString("%1/../share/locale/%2/LC_MESSAGES").arg(QCoreApplication::applicationDirPath(), locale);
0105     }
0106     else if (translator.load("gcompris_qt.qm", QString("%1/../share/locale/%2/LC_MESSAGES").arg(QCoreApplication::applicationDirPath(), locale.split('_')[0]))) {
0107         qDebug() << "load translation for locale " << locale << " in " << QString("%1/../share/locale/%2/LC_MESSAGES").arg(QCoreApplication::applicationDirPath(), locale.split('_')[0]);
0108     }
0109     else
0110 #endif
0111         if (!translator.load("gcompris_" + locale, QString("%1/%2/translations").arg(QCoreApplication::applicationDirPath(), GCOMPRIS_DATA_FOLDER))) {
0112         qDebug() << "Unable to load translation for locale " << locale << ", use en_US by default";
0113     }
0114 #endif
0115     return locale;
0116 }
0117 
0118 int main(int argc, char *argv[])
0119 {
0120     // Disable it because we already support HDPI display natively
0121     qunsetenv("QT_DEVICE_PIXEL_RATIO");
0122 
0123     QApplication app(argc, argv);
0124 #if defined(UBUNTUTOUCH)
0125     app.setOrganizationName("org.kde.gcompris");
0126 #else
0127     app.setOrganizationName("KDE");
0128 #endif
0129     app.setApplicationName(GCOMPRIS_APPLICATION_NAME);
0130     app.setOrganizationDomain("kde.org");
0131     app.setApplicationVersion(ApplicationInfo::GCVersion());
0132     // Set desktop file name, as the built-in (orgDomain + appName) is not
0133     // the one we use (because appName is gcompris-qt, not gcompris)
0134     QGuiApplication::setDesktopFileName("org.kde.gcompris");
0135 
0136     // add a variable to disable default fullscreen on Mac, see below..
0137 #if defined(Q_OS_MAC)
0138     // Sandboxing on MacOSX as documented in:
0139     // https://doc.qt.io/qt-5/osx-deployment.html
0140     QDir dir(QGuiApplication::applicationDirPath());
0141     dir.cdUp();
0142     dir.cd("Plugins");
0143     QGuiApplication::setLibraryPaths(QStringList(dir.absolutePath()));
0144 #endif
0145 
0146     // Local scope for config
0147 #if defined(UBUNTUTOUCH)
0148     QSettings config(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + "/gcompris/" + GCOMPRIS_APPLICATION_NAME + ".conf",
0149                      QSettings::IniFormat);
0150 #else
0151     QSettings config(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/gcompris/" + GCOMPRIS_APPLICATION_NAME + ".conf",
0152                      QSettings::IniFormat);
0153 
0154 #endif
0155 
0156     QCommandLineParser parser;
0157     parser.setApplicationDescription("GCompris is an educational software for children 2 to 10");
0158     parser.addHelpOption();
0159     parser.addVersionOption();
0160     QCommandLineOption exportActivitiesAsSQL("export-activities-as-sql", "Export activities as SQL");
0161     parser.addOption(exportActivitiesAsSQL);
0162     QCommandLineOption clDefaultCursor(QStringList() << "c"
0163                                                      << "cursor",
0164                                        QObject::tr("Run GCompris with the default system cursor."));
0165     parser.addOption(clDefaultCursor);
0166     QCommandLineOption clNoCursor(QStringList() << "C"
0167                                                 << "nocursor",
0168                                   QObject::tr("Run GCompris without cursor (touch screen mode)."));
0169     parser.addOption(clNoCursor);
0170     QCommandLineOption clFullscreen(QStringList() << "f"
0171                                                   << "fullscreen",
0172                                     QObject::tr("Run GCompris in fullscreen mode."));
0173     parser.addOption(clFullscreen);
0174     QCommandLineOption clWindow(QStringList() << "w"
0175                                               << "window",
0176                                 QObject::tr("Run GCompris in window mode."));
0177     parser.addOption(clWindow);
0178     QCommandLineOption clSound(QStringList() << "s"
0179                                              << "sound",
0180                                QObject::tr("Run GCompris with sound enabled."));
0181     parser.addOption(clSound);
0182     QCommandLineOption clMute(QStringList() << "m"
0183                                             << "mute",
0184                               QObject::tr("Run GCompris without sound."));
0185     parser.addOption(clMute);
0186     QCommandLineOption clWithoutKioskMode(QStringList() << "disable-kioskmode",
0187                                           QObject::tr("Disable the kiosk mode (default)."));
0188     parser.addOption(clWithoutKioskMode);
0189     QCommandLineOption clWithKioskMode(QStringList() << "enable-kioskmode",
0190                                        QObject::tr("Enable the kiosk mode."));
0191     parser.addOption(clWithKioskMode);
0192 
0193     QCommandLineOption clSoftwareRenderer(QStringList() << "software-renderer",
0194                                           QObject::tr("Use software renderer instead of openGL (slower but should run with any graphical card)."));
0195     parser.addOption(clSoftwareRenderer);
0196     QCommandLineOption clOpenGLRenderer(QStringList() << "opengl-renderer",
0197                                         QObject::tr("Use openGL renderer instead of software (faster but crash potentially depending on your graphical card)."));
0198     parser.addOption(clOpenGLRenderer);
0199 
0200     QCommandLineOption clStartOnActivity("launch",
0201                                          QObject::tr("Specify the activity when starting GCompris."), "activity");
0202     parser.addOption(clStartOnActivity);
0203 
0204     QCommandLineOption clListActivities(QStringList() << "l"
0205                                                       << "list-activities",
0206                                         QObject::tr("Outputs all the available activities on the standard output."));
0207     parser.addOption(clListActivities);
0208 
0209     QCommandLineOption clDifficultyRange("difficulty",
0210                                          QObject::tr("Specify the range of activity difficulty to display for the session. Either a single value (2), or a range (3-6). Values must be between 1 and 6."), "difficulty");
0211     parser.addOption(clDifficultyRange);
0212 
0213     QCommandLineOption clStartOnLevel("start-level",
0214                                       QObject::tr("Specify on which level to start the activity. Only used when --launch option is used."), "startLevel");
0215     parser.addOption(clStartOnLevel);
0216 
0217     parser.process(app);
0218 
0219     GComprisPlugin plugin;
0220     plugin.registerTypes("GCompris");
0221     ActivityInfoTree::registerResources();
0222 
0223     // Load translations
0224     QTranslator translator;
0225     loadTranslation(config, translator);
0226     // Apply translation
0227     app.installTranslator(&translator);
0228 
0229     // Tell media players to stop playing, it's GCompris time
0230     ApplicationInfo::getInstance()->requestAudioFocus();
0231 
0232     // For android, request the permissions if not already allowed
0233     ApplicationInfo::getInstance()->checkPermissions();
0234 
0235     // Disable default fullscreen launch on Mac as it's a bit broken, window is behind desktop bars
0236 #if defined(Q_OS_MAC)
0237     bool isFullscreen = false;
0238 #else
0239     // for other platforms, fullscreen is the default value
0240     bool isFullscreen = true;
0241 #endif
0242     {
0243         isFullscreen = config.value("General/fullscreen", isFullscreen).toBool();
0244 
0245         // Set the cursor image
0246         bool defaultCursor = config.value("General/defaultCursor", false).toBool();
0247 
0248         if (!defaultCursor && !parser.isSet(clDefaultCursor))
0249             QGuiApplication::setOverrideCursor(
0250                 QCursor(QPixmap(":/gcompris/src/core/resource/cursor.svg"),
0251                         0, 0));
0252 
0253         // Hide the cursor
0254         bool noCursor = config.value("General/noCursor", false).toBool();
0255 
0256         if (noCursor || parser.isSet(clNoCursor))
0257             QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
0258     }
0259 
0260     // Update execution counter
0261     ApplicationSettings::getInstance()->setExeCount(ApplicationSettings::getInstance()->exeCount() + 1);
0262 
0263     if (parser.isSet(clFullscreen)) {
0264         isFullscreen = true;
0265     }
0266     if (parser.isSet(clWindow)) {
0267         isFullscreen = false;
0268     }
0269     if (parser.isSet(clMute)) {
0270         ApplicationSettings::getInstance()->setIsAudioEffectsEnabled(false);
0271         ApplicationSettings::getInstance()->setIsAudioVoicesEnabled(false);
0272     }
0273     if (parser.isSet(clSound)) {
0274         ApplicationSettings::getInstance()->setIsAudioEffectsEnabled(true);
0275         ApplicationSettings::getInstance()->setIsAudioVoicesEnabled(true);
0276     }
0277     if (parser.isSet(clWithoutKioskMode)) {
0278         ApplicationSettings::getInstance()->setKioskMode(false);
0279     }
0280     if (parser.isSet(clWithKioskMode)) {
0281         ApplicationSettings::getInstance()->setKioskMode(true);
0282     }
0283     if (parser.isSet(clSoftwareRenderer)) {
0284         ApplicationSettings::getInstance()->setRenderer(QStringLiteral("software"));
0285     }
0286     if (parser.isSet(clOpenGLRenderer)) {
0287         ApplicationSettings::getInstance()->setRenderer(QStringLiteral("opengl"));
0288     }
0289 
0290     // Set the renderer used
0291     const QString &renderer = ApplicationSettings::getInstance()->renderer();
0292     ApplicationInfo::getInstance()->setUseOpenGL(renderer != QLatin1String("software"));
0293 
0294     if (renderer == QLatin1String("software")) {
0295 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0296         QQuickWindow::setGraphicsApi(QSGRendererInterface::Software);
0297 #else
0298         QQuickWindow::setSceneGraphBackend(QSGRendererInterface::Software);
0299 #endif
0300     }
0301     else if (renderer == QLatin1String("opengl")) {
0302 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0303         QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
0304 #else
0305         QQuickWindow::setSceneGraphBackend(QSGRendererInterface::OpenGL);
0306 #endif
0307     }
0308 
0309     // Start on specific activity
0310     if (parser.isSet(clStartOnActivity)) {
0311         QString startingActivity = parser.value(clStartOnActivity);
0312         int startingLevel = 0;
0313         if (parser.isSet(clStartOnLevel)) {
0314             bool isNumber = false;
0315             startingLevel = parser.value(clStartOnLevel).toInt(&isNumber);
0316             if (!isNumber || startingLevel < 0) {
0317                 startingLevel = 0;
0318             }
0319         }
0320         // internally, levels start at 0
0321         ActivityInfoTree::setStartingActivity(startingActivity, startingLevel - 1);
0322     }
0323 
0324     QQmlApplicationEngine engine(QUrl("qrc:/gcompris/src/core/main.qml"));
0325     QObject::connect(&engine, &QQmlApplicationEngine::quit, DownloadManager::getInstance(),
0326                      &DownloadManager::shutdown);
0327     // add import path for shipped qml modules:
0328     engine.addImportPath(QStringLiteral("%1/../lib/qml")
0329                              .arg(QCoreApplication::applicationDirPath()));
0330 
0331 #if __ANDROID__ && QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
0332     // Find box2d
0333     engine.addImportPath(QStringLiteral("assets:/"));
0334 #endif
0335 
0336     ApplicationInfo::getInstance()->setBox2DInstalled(engine);
0337 
0338     if (parser.isSet(exportActivitiesAsSQL)) {
0339         ActivityInfoTree *menuTree(qobject_cast<ActivityInfoTree *>(ActivityInfoTree::menuTreeProvider(&engine, nullptr)));
0340         menuTree->exportAsSQL();
0341         return 0;
0342     }
0343 
0344     if (parser.isSet(clListActivities)) {
0345         ActivityInfoTree *menuTree(qobject_cast<ActivityInfoTree *>(ActivityInfoTree::menuTreeProvider(&engine, nullptr)));
0346         menuTree->listActivities();
0347         return 0;
0348     }
0349     // Start on specific difficulties
0350     if (parser.isSet(clDifficultyRange)) {
0351         QString difficultyRange = parser.value(clDifficultyRange);
0352         QStringList levels = difficultyRange.split(QStringLiteral("-"));
0353         quint32 minDifficulty = levels[0].toUInt();
0354         quint32 maxDifficulty = minDifficulty;
0355         // If we have a range, take the second value as max difficulty
0356         if (levels.size() > 1) {
0357             maxDifficulty = levels[1].toUInt();
0358         }
0359         if (minDifficulty > maxDifficulty) {
0360             qWarning() << "Minimal level must be lower than maximum level";
0361             return -1;
0362         }
0363         if (minDifficulty < 1 || minDifficulty > 6) {
0364             qWarning() << "Minimal level must between 1 and 6";
0365             return -1;
0366         }
0367         if (maxDifficulty < 1 || maxDifficulty > 6) {
0368             qWarning() << "Maximal level must between 1 and 6";
0369             return -1;
0370         }
0371 
0372         qDebug() << QStringLiteral("Setting difficulty between %1 and %2").arg(minDifficulty).arg(maxDifficulty);
0373         ApplicationSettings::getInstance()->setDifficultyFromCommandLine(minDifficulty, maxDifficulty);
0374         ActivityInfoTree::getInstance()->minMaxFiltersChanged(minDifficulty, maxDifficulty, false);
0375         ActivityInfoTree::getInstance()->filterByTag("favorite");
0376     }
0377 
0378     QObject *topLevel = engine.rootObjects().value(0);
0379 
0380     QQuickWindow *window = qobject_cast<QQuickWindow *>(topLevel);
0381     if (window == nullptr) {
0382         qWarning("Error: Your root item has to be a Window.");
0383         return -1;
0384     }
0385     ApplicationInfo::setWindow(window);
0386 
0387     window->setIcon(QIcon(QPixmap(QString::fromUtf8(":/gcompris/src/core/resource/gcompris-icon.png"))));
0388 
0389     if (isFullscreen) {
0390         window->showFullScreen();
0391     }
0392     else {
0393         window->show();
0394     }
0395 
0396     return app.exec();
0397 }