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 }