1/* GCompris - main.qml
2 *
3 * SPDX-FileCopyrightText: 2014 Bruno Coudoin <bruno.coudoin@gcompris.net>
4 *
5 * Authors:
6 *   Bruno Coudoin <bruno.coudoin@gcompris.net>
7 *
8 *   SPDX-License-Identifier: GPL-3.0-or-later
9 */
10import QtQuick 2.9
11import QtQuick.Controls 1.5
12import QtQuick.Window 2.2
13import QtQml 2.2
14
15import GCompris 1.0
16import "qrc:/gcompris/src/core/core.js" as Core
17
18/**
19 * GCompris' main QML file defining the top level window.
20 * @ingroup infrastructure
21 *
22 * Handles application start (Component.onCompleted) and shutdown (onClosing)
23 * on the QML layer.
24 *
25 * Contains the central GCAudio objects audio effects and audio voices.
26 *
27 * Contains the top level StackView presenting and animating GCompris'
28 * full screen views.
29 *
30 * @sa BarButton, BarEnumContent
31 * @inherit QtQuick.Window
32 */
33Window {
34    id: main
35    // Start in window mode at full screen size
36    width: ApplicationSettings.previousWidth
37    height: ApplicationSettings.previousHeight
38    minimumWidth: 400 * ApplicationInfo.ratio
39    minimumHeight: 400 * ApplicationInfo.ratio
40    title: "GCompris"
41
42    /// @cond INTERNAL_DOCS
43
44    property var applicationState: Qt.application.state
45    property var rccBackgroundMusic: ApplicationInfo.getBackgroundMusicFromRcc()
46    property var filteredBackgroundMusic: ApplicationSettings.filteredBackgroundMusic
47    property alias backgroundMusic: backgroundMusic
48    property bool voicesDownloaded: true
49    property bool wordSetDownloaded: true
50    property bool musicDownloaded: true
51    property bool welcomePlayed: false
52    property int lastGCVersionRanCopy: ApplicationInfo.GCVersionCode
53
54    /**
55     * type: bool
56     * It tells whether a musical activity is running.
57     *
58     * It changes to true if the started activity is a musical activity and back to false when the activity is closed, allowing to play background music.
59     */
60    property bool isMusicalActivityRunning: false
61
62    /**
63     * When a musical activity is started, the backgroundMusic pauses.
64     *
65     * When returning back from the musical activity to menu, backgroundMusic resumes.
66     */
67    onIsMusicalActivityRunningChanged: {
68        if(isMusicalActivityRunning) {
69            backgroundMusic.pause();
70        }
71        else {
72            backgroundMusic.resume();
73        }
74    }
75
76    onApplicationStateChanged: {
77        if(ApplicationInfo.isMobile && applicationState !== Qt.ApplicationActive) {
78            audioVoices.stop();
79            audioEffects.stop();
80            backgroundMusic.pause();
81        } else if(ApplicationInfo.isMobile && applicationState == Qt.ApplicationActive
82                && !isMusicalActivityRunning) {
83            backgroundMusic.resume();
84        }
85    }
86
87    onClosing: Core.quit(pageView)
88
89    GCAudio {
90        id: audioVoices
91        muted: !ApplicationSettings.isAudioVoicesEnabled && !main.isMusicalActivityRunning
92
93        Timer {
94            id: delayedWelcomeTimer
95            interval: 10000 /* Make sure, that playing welcome.ogg if delayed
96                             * because of not yet registered voices, will only
97                             * happen max 10sec after startup */
98            repeat: false
99
100            onTriggered: {
101                DownloadManager.voicesRegistered.disconnect(playWelcome);
102            }
103
104            function playWelcome() {
105                if(!welcomePlayed) {
106                    audioVoices.append(ApplicationInfo.getAudioFilePath("voices-$CA/$LOCALE/misc/welcome.$CA"));
107                    welcomePlayed = true;
108                }
109            }
110        }
111
112        Component.onCompleted: {
113            if (DownloadManager.areVoicesRegistered())
114                delayedWelcomeTimer.playWelcome();
115            else {
116                DownloadManager.voicesRegistered.connect(
117                        delayedWelcomeTimer.playWelcome);
118                delayedWelcomeTimer.start();
119            }
120        }
121    }
122
123    GCSfx {
124        id: audioEffects
125        muted: !ApplicationSettings.isAudioEffectsEnabled && !main.isMusicalActivityRunning
126        volume: ApplicationSettings.audioEffectsVolume
127    }
128
129    GCAudio {
130        id: backgroundMusic
131        isBackgroundMusic: true
132        muted: !ApplicationSettings.isBackgroundMusicEnabled
133        volume: ApplicationSettings.backgroundMusicVolume
134
135        onMutedChanged: {
136            if(!hasAudio && !files.length) {
137                backgroundMusic.playBackgroundMusic()
138            }
139        }
140
141        onDone: backgroundMusic.playBackgroundMusic()
142
143        function playBackgroundMusic() {
144            rccBackgroundMusic = ApplicationInfo.getBackgroundMusicFromRcc()
145
146            for(var i = 0; i < filteredBackgroundMusic.length; i++) {
147                backgroundMusic.append(ApplicationInfo.getAudioFilePath("backgroundMusic/" + filteredBackgroundMusic[i]))
148            }
149            if(main.isMusicalActivityRunning)
150                backgroundMusic.pause()
151        }
152
153        Component.onCompleted: {
154            if(ApplicationSettings.isBackgroundMusicEnabled)
155                backgroundMusic.append(ApplicationInfo.getAudioFilePath("qrc:/gcompris/src/core/resource/intro.$CA"))
156            if(ApplicationSettings.isBackgroundMusicEnabled
157               && DownloadManager.haveLocalResource(DownloadManager.getBackgroundMusicResources())) {
158                   backgroundMusic.playBackgroundMusic()
159            }
160            else {
161                DownloadManager.backgroundMusicRegistered.connect(backgroundMusic.playBackgroundMusic)
162            }
163        }
164    }
165
166    function playIntroVoice(name) {
167        name = name.split("/")[0]
168        audioVoices.play(ApplicationInfo.getAudioFilePath("voices-$CA/$LOCALE/intro/" + name + ".$CA"))
169    }
170
171    function checkWordset() {
172        var wordset = ApplicationSettings.wordset
173        if(wordset === '')
174            // Maybe the wordset has been bundled or copied manually
175            // we have to register it if we find it.
176            wordset = 'data2/words/words.rcc'
177
178        // check for words.rcc:
179        if (DownloadManager.isDataRegistered("words")) {
180            // words.rcc is already registered -> nothing to do
181        } else if(DownloadManager.haveLocalResource(wordset)) {
182            // words.rcc is there -> register old file first
183            // then try to update in the background
184            if(DownloadManager.updateResource(wordset)) {
185                ApplicationSettings.wordset = wordset
186            }
187        } else if(ApplicationSettings.useWordset) { // Only if external wordset is enabled
188            // words.rcc has not been downloaded yet -> ask for download
189            wordSetDownloaded = false;
190        }
191
192        //disable wordset if useWordset config is false
193        if(!ApplicationSettings.useWordset) {
194            ApplicationSettings.wordset = "";
195        }
196    }
197
198    function checkBackgroundMusic() {
199        var music = DownloadManager.getBackgroundMusicResources()
200        if(rccBackgroundMusic === '') {
201            rccBackgroundMusic = ApplicationInfo.getBackgroundMusicFromRcc()
202        }
203        if(music === '') {
204            music = DownloadManager.getBackgroundMusicResources()
205        }
206        // We have local music but it is not yet registered
207        else if(!DownloadManager.isDataRegistered("backgroundMusic") && DownloadManager.haveLocalResource(music)) {
208            // We have music and automatic download is enabled. Download the music and register it
209            if(DownloadManager.updateResource(music) && DownloadManager.downloadIsRunning()) {
210                DownloadManager.registerResource(music)
211                rccBackgroundMusic = Core.shuffle(ApplicationInfo.getBackgroundMusicFromRcc())
212            }
213            else {
214                rccBackgroundMusic = ApplicationInfo.getBackgroundMusicFromRcc()
215            }
216        }
217        else if(ApplicationSettings.isBackgroundMusicEnabled && !DownloadManager.haveLocalResource(music)) {
218            musicDownloaded = false;
219        }
220    }
221
222    function checkVoices() {
223        if(!DownloadManager.haveLocalResource(DownloadManager.getVoicesResourceForLocale(ApplicationSettings.locale)))
224            voicesDownloaded = false;
225        else
226            DownloadManager.registerResource(DownloadManager.getVoicesResourceForLocale(ApplicationSettings.locale));
227    }
228
229    function initialAssetsDownload() {
230        checkVoices();
231        checkWordset();
232        checkBackgroundMusic();
233        var voicesLine = voicesDownloaded ? "" : ("<li>" + qsTr("Voices for your language") + "</li>");
234        var wordSetLine = wordSetDownloaded ? "" : ("<li>" + qsTr("Full word image set") + "</li>");
235        var musicLine = musicDownloaded ? "" : ("<li>" + qsTr("Background music") + "</li>");
236        if(!voicesDownloaded || !wordSetDownloaded || ! musicDownloaded) {
237            var dialog;
238            dialog = Core.showMessageDialog(
239                pageView.currentItem,
240                qsTr("Do you want to download the following external assets?")
241                + ("<ul>")
242                + voicesLine
243                + wordSetLine
244                + musicLine
245                + "</ul>",
246                qsTr("Yes"),
247                function() {
248                    if(!voicesDownloaded)
249                        DownloadManager.downloadResource(DownloadManager.getVoicesResourceForLocale(ApplicationSettings.locale));
250                    if(!wordSetDownloaded)
251                        DownloadManager.downloadResource('data2/words/words.rcc');
252                    if(!musicDownloaded)
253                        DownloadManager.downloadResource(DownloadManager.getBackgroundMusicResources());
254                    var downloadDialog = Core.showDownloadDialog(pageView.currentItem, {});
255                },
256                qsTr("No"), null,
257                null
258            );
259
260        }
261    }
262
263    ChangeLog {
264       id: changelog
265    }
266
267    Component.onCompleted: {
268        console.log("enter main.qml (run #" + ApplicationSettings.exeCount
269                    + ", ratio=" + ApplicationInfo.ratio
270                    + ", fontRatio=" + ApplicationInfo.fontRatio
271                    + ", dpi=" + Math.round(Screen.pixelDensity*25.4)
272                    + ", userDataPath=" + ApplicationSettings.userDataPath
273                    + ")");
274        if (ApplicationSettings.exeCount === 1 &&
275                !ApplicationSettings.isKioskMode &&
276                ApplicationInfo.isDownloadAllowed) {
277            // first run
278            var dialog;
279            dialog = Core.showMessageDialog(
280                        pageView,
281                        qsTr("Welcome to GCompris!") + ("<br>")
282                        + qsTr("You are running GCompris for the first time.") + "\n"
283                        + qsTr("You should verify that your application settings especially your language is set correctly, and that all language specific sound files are installed. You can do this in the Preferences Dialog.")
284                        + "\n"
285                        + qsTr("Have Fun!")
286                        + ("<br><br>")
287                        + qsTr("Your current language is %1 (%2).")
288                          .arg(Qt.locale(ApplicationInfo.getVoicesLocale(ApplicationSettings.locale)).nativeLanguageName)
289                          .arg(ApplicationInfo.getVoicesLocale(ApplicationSettings.locale)),
290                        "", null,
291                        "", null,
292                        function() {
293                            pageView.currentItem.focus = true;
294                            initialAssetsDownload();
295                        }
296             );
297        }
298        else {
299            // Register voices-resources for current locale, updates/downloads only if
300            // not prohibited by the settings
301            if(!DownloadManager.areVoicesRegistered()) {
302                DownloadManager.updateResource(
303                    DownloadManager.getVoicesResourceForLocale(ApplicationSettings.locale));
304            }
305            checkWordset();
306            checkBackgroundMusic();
307            if(changelog.isNewerVersion(ApplicationSettings.lastGCVersionRan, ApplicationInfo.GCVersionCode)) {
308                lastGCVersionRanCopy = ApplicationSettings.lastGCVersionRan;
309                // display log between ApplicationSettings.lastGCVersionRan and ApplicationInfo.GCVersionCode
310                Core.showMessageDialog(
311                pageView,
312                qsTr("GCompris has been updated! Here are the new changes:<br/>") + changelog.getLogBetween(ApplicationSettings.lastGCVersionRan, ApplicationInfo.GCVersionCode),
313                "", null,
314                "", null,
315                function() { pageView.currentItem.focus = true }
316                );
317                // Store new version after update
318                ApplicationSettings.lastGCVersionRan = ApplicationInfo.GCVersionCode;
319            }
320        }
321        //Store version on first run in any case
322        if(ApplicationSettings.lastGCVersionRan === 0)
323            ApplicationSettings.lastGCVersionRan = ApplicationInfo.GCVersionCode;
324    }
325
326    Loading {
327        id: loading
328    }
329
330    StackView {
331        id: pageView
332        anchors.fill: parent
333        initialItem: {
334            "item": "qrc:/gcompris/src/activities/" + ActivityInfoTree.rootMenu.name,
335            "properties": {
336                'audioVoices': audioVoices,
337                'audioEffects': audioEffects,
338                'loading': loading,
339                'backgroundMusic': backgroundMusic
340            }
341        }
342
343        focus: true
344
345        delegate: StackViewDelegate {
346            id: root
347            function getTransition(properties)
348            {
349                audioVoices.clearQueue()
350                audioVoices.stop()
351
352                if(!properties.exitItem.isDialog &&        // if coming from menu and
353                        !properties.enterItem.isDialog)    // going into an activity then
354                    playIntroVoice(properties.enterItem.activityInfo.name); // play intro
355
356                // Don't restart an activity if you click on help
357                if (!properties.exitItem.isDialog ||       // if coming from menu or
358                    properties.enterItem.alwaysStart)  // start signal enforced (for special case like transition from config-dialog to editor)
359                    properties.enterItem.start();
360
361                if(properties.name === "pushTransition") {
362                    if(properties.enterItem.isDialog) {
363                        return pushVTransition
364                    } else {
365                        if(properties.enterItem.isMusicalActivity)
366                            main.isMusicalActivityRunning = true
367                        return pushHTransition
368                    }
369                } else {
370                    if(properties.exitItem.isDialog) {
371                        return popVTransition
372                    } else {
373                        main.isMusicalActivityRunning = false
374                        return popHTransition
375                    }
376
377                }
378            }
379
380            function transitionFinished(properties)
381            {
382                properties.exitItem.opacity = 1
383                if(!properties.enterItem.isDialog) {
384                    properties.exitItem.stop()
385                }
386            }
387
388            property Component pushHTransition: StackViewTransition {
389                PropertyAnimation {
390                    target: enterItem
391                    property: "x"
392                    from: target.width
393                    to: 0
394                    duration: 500
395                    easing.type: Easing.OutSine
396                }
397                PropertyAnimation {
398                    target: exitItem
399                    property: "x"
400                    from: 0
401                    to: -target.width
402                    duration: 500
403                    easing.type: Easing.OutSine
404                }
405            }
406
407            property Component popHTransition: StackViewTransition {
408                PropertyAnimation {
409                    target: enterItem
410                    property: "x"
411                    from: -target.width
412                    to: 0
413                    duration: 500
414                    easing.type: Easing.OutSine
415                }
416                PropertyAnimation {
417                    target: exitItem
418                    property: "x"
419                    from: 0
420                    to: target.width
421                    duration: 500
422                    easing.type: Easing.OutSine
423                }
424            }
425
426            property Component pushVTransition: StackViewTransition {
427                PropertyAnimation {
428                    target: enterItem
429                    property: "y"
430                    from: -target.height
431                    to: 0
432                    duration: 500
433                    easing.type: Easing.OutSine
434                }
435                PropertyAnimation {
436                    target: exitItem
437                    property: "y"
438                    from: 0
439                    to: target.height
440                    duration: 500
441                    easing.type: Easing.OutSine
442                }
443            }
444
445            property Component popVTransition: StackViewTransition {
446                PropertyAnimation {
447                    target: enterItem
448                    property: "y"
449                    from: target.height
450                    to: 0
451                    duration: 500
452                    easing.type: Easing.OutSine
453                }
454                PropertyAnimation {
455                    target: exitItem
456                    property: "y"
457                    from: 0
458                    to: -target.height
459                    duration: 500
460                    easing.type: Easing.OutSine
461                }
462            }
463
464            property Component replaceTransition: pushHTransition
465        }
466    }
467    /// @endcond
468}
469