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

0001 /* GCompris - core.js
0002  *
0003  * Copyright (C) 2014
0004  * Authors:
0005  *   Bruno Coudoin <bruno.coudoin@gcompris.net>
0006  *   Holger Kaelberer <holger.k@elberer.de>
0007  *   Johnny Jazeix <jazeix@gmail.com>
0008  *
0009  *   SPDX-License-Identifier: GPL-3.0-or-later
0010  */
0011 
0012 /**
0013  * @file
0014  * Contains commonly used javascript methods.
0015  * @ingroup components
0016  *
0017  * FIXME: how to include this file in kgenapidox's output?
0018  */
0019 .pragma library
0020 .import QtQml 2.12 as Qml
0021 .import GCompris 1.0 as GCompris
0022 
0023 /**
0024  * Shuffle the array @p o and returns it.
0025  *
0026  * @param o Array to shuffle.
0027  * @returns A shuffled array.
0028  */
0029 function shuffle(o) {
0030     for(var j, x, i = o.length; i;
0031         j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
0032     return o;
0033 }
0034 
0035 /**
0036  * Get the starting level. This is used with --start-level option.
0037  *
0038  * @param numberOfLevel the number of levels for this activity.
0039  * @returns 0 if no --start-level or invalid one (request of a level higher than
0040  * the level count of the activity), else the level of --start-level option.
0041  */
0042 function getInitialLevel(numberOfLevel) {
0043     var level = 0;
0044     if(GCompris.ActivityInfoTree.startingLevel > 0 && GCompris.ActivityInfoTree.startingLevel < numberOfLevel) {
0045         level = GCompris.ActivityInfoTree.startingLevel;
0046     }
0047     return level;
0048 }
0049 
0050 /**
0051  * Get the next level.
0052  *
0053  * @param currentLevel the current level.
0054  * @param numberOfLevel the number of levels for this activity.
0055  * @returns If the next level is after the maximum, returns 0 else return currentLevel + 1.
0056  */
0057 function getNextLevel(currentLevel, numberOfLevel) {
0058     var level = currentLevel + 1;
0059     if(numberOfLevel <= level) {
0060         level = 0;
0061     }
0062     return level;
0063 }
0064 
0065 /**
0066  * Get the previous level.
0067  *
0068  * @param currentLevel the current level.
0069  * @param numberOfLevel the number of levels for this activity.
0070  * @returns If the previous level is 0, returns the last level else return currentLevel - 1.
0071  */
0072 function getPreviousLevel(currentLevel, numberOfLevel) {
0073     var level = currentLevel - 1;
0074     if(level < 0) {
0075         level = numberOfLevel - 1;
0076     }
0077     return level;
0078 }
0079 /**
0080  * Return the filename of the audio voices file for the passed letters
0081  * (or a number) @p c
0082  *
0083  * A letter maybe a digraph or more characters (a pair of characters used
0084  * to write one phoneme). In this case the result have the form U0066U0068.ogg
0085  *
0086  * The returned audio file. has the suffix .ogg
0087  *
0088  * @param c Letter or number character.
0089  * @return A filename for the audio file for the passed letter @p c of the
0090  *         form U0033.ogg
0091  */
0092 function getSoundFilenamForChar(c)
0093 {
0094     var results = ''
0095     for(var i = 0; i < c.length; ++i) {
0096         var result = "U";
0097         var codeHex = c.toLowerCase().charCodeAt(i).toString(16).toUpperCase();
0098         while (codeHex.length < 4) {
0099             codeHex = "0" + codeHex;
0100         }
0101         results += "U" + codeHex
0102     }
0103     results += ".$CA";
0104     return results;
0105 }
0106 
0107 /**
0108  * Create and present a GCDialog with the given parameters
0109  *
0110  * Instantiates a GCDialog object dynamically as child of the  passed
0111  * parent object. After one of the buttons passed in the buttonHandler parameter
0112  * has been pressed, the dialog will be closed and destroyed automatically.
0113  *
0114  * @param parent            QML parent object
0115  * @param informativeText   Informative text
0116  * @param button1Text the   Label of the first button
0117  * @param button1Callback   Callback handler for the first button
0118  * @param button2Text       Label of the second button
0119  * @param button2Callback   Callback handler for the second button
0120  * @param closeCallback     Callback handler for the close button
0121  * @returns The GCDialog object upon success, null otherwise
0122  *
0123  * @sa GCDialog
0124  */
0125 function showMessageDialog(parent, informativeText,
0126                            button1Text, button1Callback,
0127                            button2Text, button2Callback,
0128                            closeCallback) {
0129     //console.debug("Core.showMessageDialog: parent=" + parent + " backtrace="); console.trace();
0130     var qmlStr =
0131           'import QtQuick 2.12\n'
0132         + 'GCDialog {\n'
0133         + '    message: "' + informativeText + '"\n'
0134         + '    button1Text: "' + button1Text + '"\n'
0135         + '    button2Text: "' + button2Text + '"\n'
0136         + ' }\n';
0137 
0138     var dialog = null;
0139     try {
0140         dialog = Qt.createQmlObject(qmlStr, parent);
0141         if(button1Callback)
0142             dialog.button1Hit.connect(button1Callback);
0143         if(button2Callback)
0144             dialog.button2Hit.connect(button2Callback);
0145         if(closeCallback)
0146             dialog.close.connect(closeCallback);
0147         dialog.start();
0148     } catch (e) {
0149         console.error("core.js: Error creating a MessageDialog: " + e);
0150         if (dialog)
0151             dialog.destroy();
0152         return null;
0153     }
0154     return dialog;
0155 }
0156 
0157 /**
0158  * Destroy dialog @p dialog
0159  *
0160  * @param dialog A dynamically created GCDialog or DownloadDialog
0161  */
0162 function destroyDialog(dialog) {
0163     if (dialog) {
0164         dialog.stop();
0165         dialog.destroy();
0166     }
0167 }
0168 
0169 var downloadDialogComponent = null;
0170 
0171 /**
0172  * Create and present a DownloadDialog with the given parameters.
0173  *
0174  * Instantiates a DownloadDialog object dynamically as child of the  passed
0175  * parent object. The DownloadDialog.dynamic property will be set, and the
0176  * dialog will be destroyed dynamically after closing.
0177  *
0178  * @param parent     QML parent object
0179  * @param properties Object with property-value pairs used to parametrize the new
0180  *                   object. Used directly as properties parameter of
0181  *                   Component.createObject()
0182  * @returns          A newly created DownloadDialog object upon success, null
0183  *                   otherwise.
0184  */
0185 function showDownloadDialog(parent, properties) {
0186     var dialog = null;
0187     try {
0188         if (!downloadDialogComponent) {
0189             downloadDialogComponent = Qt.createComponent("qrc:/gcompris/src/core/DownloadDialog.qml");
0190             if (downloadDialogComponent.status != Qml.Component.Ready) {
0191                 throw new Error("Error creating DownloadDialog component: "
0192                         + downloadDialogComponent.errorString());
0193                 downloadDialogComponent = null;
0194             }
0195         }
0196         properties.dynamic = true;
0197         dialog = downloadDialogComponent.createObject(parent, properties);
0198         dialog.main = parent
0199         dialog.start();
0200     } catch (e) {
0201         console.error("core.js: Error creating a DownloadDialog: " + e);
0202         if (dialog)
0203             dialog.destroy();
0204         return null;
0205     }
0206     //console.log("created DownloadDialog " + dialog);
0207     return dialog;
0208 }
0209 
0210 /**
0211  * Helper checking for availability of audio voices for the current locale and
0212  * informing the user in case they're missing.
0213  *
0214  * Can be used by acitivities that depend on audio voices to inform the user
0215  * of missing resources during startup.
0216  *
0217  * @param parent Parent QML object.
0218  */
0219 function checkForVoices(parent)
0220 {
0221     var messageText = qsTr("Missing sound files!") + '\n'
0222                 + qsTr("This activity uses language sound files, that are not yet installed on your system.")
0223                 + '\n';
0224     if(GCompris.ApplicationInfo.isDownloadAllowed) {
0225         messageText += qsTr("For downloading the needed sound files go to the Configuration dialog.");
0226     }
0227     if (!GCompris.DownloadManager.areVoicesRegistered(GCompris.ApplicationSettings.locale)) {
0228         showMessageDialog(parent,
0229                 messageText,
0230                 "", null,
0231                 "", null,
0232                 null);
0233     }
0234 }
0235 
0236 var aboutToQuit = false;
0237 /**
0238  * Central function for quitting GCompris.
0239  *
0240  * Should be used everywhere instead of Qt.quit(), warning in case of running
0241  * downloadloads and showing a confirmation dialog on mobile devices.
0242  * Call Qt.quit() itself upon confirmation.
0243  *
0244  * @param parent QML parent object used for the dynamic dialog.
0245  */
0246 function quit(parent)
0247 {
0248     if (aboutToQuit)  // don't execute concurrently
0249         return;
0250     aboutToQuit = true;
0251     GCompris.ApplicationSettings.previousHeight = parent.height;
0252     GCompris.ApplicationSettings.previousWidth = parent.width;
0253     GCompris.ApplicationInfo.abandonAudioFocus()
0254 
0255     if (GCompris.DownloadManager.downloadIsRunning()) {
0256         var dialog = showDownloadDialog(parent, {
0257             text: qsTr("Download in progress")
0258                   + '\n'
0259                   + qsTr("Download in progress.<br/>'Abort' it to quit immediately."),
0260             autohide: true,
0261             reportError: false,
0262             reportSuccess: false,
0263             backgroundButtonVisible: false
0264         });
0265         dialog.finished.connect(function() {Qt.quit();});
0266     } else if (GCompris.ApplicationSettings.exitConfirmation) {
0267         // prevent the user from quitting accidentially by clicking back too often:
0268         showMessageDialog(parent,
0269                 qsTr("Quit?") +
0270                 '\n' +
0271                 qsTr("Do you really want to quit GCompris?"),
0272                 qsTr("Yes"), function() { Qt.quit(); },
0273                 qsTr("No"), function() { aboutToQuit = false; },
0274                 function() { aboutToQuit = false; } );
0275     } else {
0276         Qt.quit();
0277     }
0278 }
0279 
0280 function isLeftToRightLocale(locale) {
0281     var localeShort = GCompris.ApplicationInfo.getVoicesLocale(locale)
0282     return (localeShort != "ar" && localeShort != "he");
0283 }
0284 
0285 function localeNeedSpaces(locale) {
0286     // Returns false for languages with no spaces between words
0287     var localeShort = GCompris.ApplicationInfo.getVoicesLocale(locale)
0288     var noSpacesLanguages = [ "zh_TW", "zh_CN" ]
0289     return (noSpacesLanguages.indexOf(localeShort) === -1)
0290 }
0291 
0292 function resolveLocale(localeToSet) {
0293     // Resolve the locale used to a real one
0294     // if Gcompris' locale selected, we want the real locale
0295     // and if GCompris' locale is system, then resolve again
0296     var newLocale = localeToSet
0297     if(newLocale === "system") {
0298         newLocale = GCompris.ApplicationSettings.locale
0299         // Remove .UTF-8
0300         if(newLocale.indexOf('.') != -1) {
0301             newLocale = newLocale.substring(0, newLocale.indexOf('.'))
0302         }
0303         return newLocale
0304     }
0305     else {
0306         return localeToSet
0307     }
0308 }
0309 
0310 function convertNumberToLocaleString(decimalNumber, locale = GCompris.ApplicationInfo.localeShort, format = 'f', precision = 0) {
0311     // Special case for Arabic, we still want to use Arabic numerals, not Eastern Arabic numerals
0312     // For now, we consider dot separated numbers for Arabic
0313     var localeToConvertTo = (locale.startsWith("ar") ? "he" : locale);
0314     return decimalNumber.toLocaleString(Qt.locale(localeToConvertTo), format, precision);
0315 }
0316 
0317 function convertNumberToLocaleCurrencyString(number, locale) {
0318     // Special case for Arabic, we still want to use Arabic numerals, not Eastern Arabic numerals but we want to keep the Arabic currency
0319     var localeToConvertTo = (locale.startsWith("ar") ? "he" : locale);
0320     return number.toLocaleCurrencyString(Qt.locale(localeToConvertTo), Qt.locale(locale).currencySymbol(Qml.Locale.CurrencySymbol));
0321 }
0322 
0323 /**
0324  * Function that returns the best cell size for a grid of a given
0325  * width and height and a number of items.
0326  *
0327  * Formula inspired from https://math.stackexchange.com/q/2570649
0328  *
0329  * @param x_: grid width
0330  * @param y_: grid height
0331  * @param n_: number of items to place in the grid
0332  * @param extra_: optional extra number of items to add to sides calculation
0333  */
0334 function fitItems(x_, y_, n_, extra_) {
0335     if(x_ <= 0 || y_ <= 0 || n_ <= 0)
0336         return 10; // return default value that will be erased later, to avoid crash on Android
0337 
0338     if(extra_ === undefined)
0339         extra_ = 0;
0340 
0341     // Compute number of rows and columns, and cell size
0342     var ratio = x_ / y_;
0343     var ncols_float = Math.sqrt(n_ * ratio);
0344     var nrows_float = n_ / ncols_float;
0345     
0346     // Find best option filling the whole height
0347     var nrows1 = Math.ceil(nrows_float) + extra_;
0348     var ncols1 = Math.ceil(n_ / nrows1) + extra_;
0349     while (nrows1 * ratio < ncols1) {
0350         nrows1++;
0351         ncols1 = Math.ceil(n_ / nrows1) + extra_;
0352     }
0353     var cell_size1 = y_ / nrows1;
0354     
0355     // Find best option filling the whole width
0356     var ncols2 = Math.ceil(ncols_float) + extra_;
0357     var nrows2 = Math.ceil(n_ / ncols2) + extra_;
0358     while (ncols2 < nrows2 * ratio) {
0359         ncols2++;
0360         nrows2 = Math.ceil(n_ / ncols2) + extra_;
0361     }
0362     var cell_size2 = x_ / ncols2;
0363     
0364     return Math.max(cell_size1, cell_size2);
0365 }