diff --git a/src/activities/melody/Melody.qml b/src/activities/melody/Melody.qml index 2c69abaa3..af54b43d3 100644 --- a/src/activities/melody/Melody.qml +++ b/src/activities/melody/Melody.qml @@ -1,256 +1,254 @@ /* GCompris - melody.qml * * Copyright (C) 2015 Bruno Coudoin * * Authors: * Jose JORGE (GTK+ version) * Bruno Coudoin (Qt Quick port) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import QtQuick 2.6 import GCompris 1.0 import "../../core" ActivityBase { id: activity onStart: focus = true onStop: {} isMusicalActivity: true - isMusicalActivity: true - pageComponent: Image { id: background anchors.fill: parent source: items.url + 'xylofon_background.svg' sourceSize.width: Math.max(parent.width, parent.height) fillMode: Image.PreserveAspectCrop signal start signal stop Component.onCompleted: { activity.start.connect(start) activity.stop.connect(stop) } // Add here the QML items you need to access in javascript QtObject { id: items property string url: "qrc:/gcompris/src/activities/melody/resource/" property var question property var questionToPlay property var answer property alias questionInterval: questionPlayer.interval property int numberOfLevel: 10 property bool running: false } onStart: { bar.level = 1 score.numberOfSubLevels = 5 score.currentSubLevel = 1 initLevel() items.running = true } onStop: { knock.stop() questionPlayer.stop() items.running = false } Image { id: xylofon anchors { fill: parent margins: 10 * ApplicationInfo.ratio } source: items.url + 'xylofon.svg' sourceSize.width: parent.width * 0.7 fillMode: Image.PreserveAspectFit } Repeater { id: parts model: 4 Image { id: part source: items.url + 'xylofon_part' + (index + 1) + '.svg' rotation: - 80 anchors.horizontalCenter: xylofon.horizontalCenter anchors.horizontalCenterOffset: (- xylofon.paintedWidth) * 0.3 + xylofon.paintedWidth * index * 0.22 anchors.verticalCenter: xylofon.verticalCenter anchors.verticalCenterOffset: - xylofon.paintedHeight * 0.1 sourceSize.width: xylofon.paintedWidth * 0.5 fillMode: Image.PreserveAspectFit property alias anim: anim SequentialAnimation { id: anim NumberAnimation { target: part property: "scale" from: 1; to: 0.95 duration: 150 easing.type: Easing.InOutQuad } NumberAnimation { target: part property: "scale" from: 0.95; to: 1 duration: 150 easing.type: Easing.OutElastic } } MouseArea { anchors.fill: parent enabled: !questionPlayer.running onClicked: { anim.start() background.playNote(index) items.answer.push(index) background.checkAnswer() } } } } function playNote(index) { activity.audioEffects.play(ApplicationInfo.getAudioFilePath(items.url + 'xylofon_son' + (index + 1) + ".wav")) } Timer { id: knock interval: 1000 repeat: false onTriggered: { questionPlayer.start() } } Timer { id: questionPlayer onTriggered: { var partIndex = items.questionToPlay.shift() if(partIndex !== undefined) { parts.itemAt(partIndex).anim.start() background.playNote(partIndex) start() } } } DialogHelp { id: dialogHelp onClose: home() } Bar { id: bar content: BarEnumContent { value: help | home | level | repeat } onHelpClicked: { displayDialog(dialogHelp) } onPreviousLevelClicked: { score.currentSubLevel = 1 if(bar.level == 1) { bar.level = items.numberOfLevel } else { bar.level-- } initLevel(); } onNextLevelClicked: parent.nextLevel() onHomeClicked: activity.home() onRepeatClicked: parent.repeat() } Bonus { id: bonus onWin: { parent.nextSubLevel() parent.repeat() } onLoose: parent.repeat() } Score { id: score anchors.bottom: undefined anchors.right: parent.right anchors.rightMargin: 10 * ApplicationInfo.ratio anchors.top: parent.top } function initLevel() { items.question = [] questionPlayer.stop() var numberOfParts = 4 if(bar.level < 3) numberOfParts = 2 else if(bar.level < 5) numberOfParts = 3 for(var i = 0; i < bar.level + 2; ++i) { items.question.push(Math.floor(Math.random() * numberOfParts)) } items.questionInterval = 1200 - Math.min(500, 100 * bar.level) items.answer = [] } function nextSubLevel() { if(score.currentSubLevel < score.numberOfSubLevels) { score.currentSubLevel++ initLevel() return } nextLevel() } function nextLevel() { score.currentSubLevel = 1 if(items.numberOfLevel === bar.level ) { bar.level = 1 } else { bar.level++ } initLevel(); } function repeat() { if(items.running == true) { questionPlayer.stop() activity.audioEffects.play(ApplicationInfo.getAudioFilePath(items.url + 'knock.wav')) items.questionToPlay = items.question.slice() items.answer = [] knock.start() } } function checkAnswer() { if(items.answer.join() == items.question.join()) bonus.good('lion') else if(items.answer.length >= items.question.length) bonus.bad('lion') } } } diff --git a/src/activities/menu/BackgroundMusicList.qml b/src/activities/menu/BackgroundMusicList.qml new file mode 100644 index 000000000..b63340cfd --- /dev/null +++ b/src/activities/menu/BackgroundMusicList.qml @@ -0,0 +1,161 @@ +/* GCompris - BackgroundMusicList.qml + * + * Copyright (C) 2019 Aman Kumar Gupta + * + * Authors: + * Aman Kumar Gupta (Qt Quick) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +import QtQuick 2.6 +import QtQuick.Controls 1.5 +import GCompris 1.0 + +import "../../core" + +Rectangle { + id: dialogBackground + color: "#696da3" + border.color: "black" + border.width: 1 + z: 10000 + anchors.fill: parent + visible: false + + Keys.onEscapePressed: close() + + signal close + + property bool horizontalLayout: dialogBackground.width >= dialogBackground.height + + Row { + spacing: 2 + Item { width: 10; height: 1 } + + Column { + spacing: 10 + anchors.top: parent.top + Item { width: 1; height: 10 } + Rectangle { + id: titleRectangle + color: "#e6e6e6" + radius: 6.0 + width: dialogBackground.width - 30 + height: title.height * 1.2 + border.color: "black" + border.width: 2 + + GCText { + id: title + text: qsTr("Background Musics") + width: dialogBackground.width - 30 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: "black" + fontSize: 20 + font.weight: Font.DemiBold + wrapMode: Text.WordWrap + } + } + + Rectangle { + color: "#e6e6e6" + radius: 6.0 + width: dialogBackground.width - 30 + height: dialogBackground.height - 100 + border.color: "black" + border.width: 2 + anchors.margins: 100 + + Flickable { + id: flickableList + anchors.fill: parent + anchors.topMargin: 10 + anchors.leftMargin: 20 + contentWidth: parent.width + contentHeight: musicGrid.height + flickableDirection: Flickable.VerticalFlick + clip: true + + Flow { + id: musicGrid + width: parent.width + spacing: 40 + anchors.horizontalCenter: parent.horizontalCenter + + Repeater { + model: dialogActivityConfig.configItem ? dialogActivityConfig.configItem.allBackgroundMusic : 0 + + Item { + width: dialogBackground.horizontalLayout ? dialogBackground.width / 5 : dialogBackground.width / 4 + height: dialogBackground.height / 5 + + Button { + text: modelData + onClicked: { + if(dialogActivityConfig.configItem.filteredBackgroundMusic.indexOf(modelData) == -1) + dialogActivityConfig.configItem.filteredBackgroundMusic.push(modelData) + else + dialogActivityConfig.configItem.filteredBackgroundMusic.splice(dialogActivityConfig.configItem.filteredBackgroundMusic.indexOf(modelData), 1) + + selectedIcon.visible = !selectedIcon.visible + } + width: parent.width + height: parent.height * 0.8 + style: GCButtonStyle { + theme: "dark" + } + + Image { + id: selectedIcon + source: "qrc:/gcompris/src/core/resource/apply.svg" + sourceSize.width: height + sourceSize.height: height + width: height + height: parent.height / 4 + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 2 + visible: dialogActivityConfig.configItem.filteredBackgroundMusic ? dialogActivityConfig.configItem.filteredBackgroundMusic.indexOf(modelData) != -1 : false + } + } + } + } + } + } + // The scroll buttons + GCButtonScroll { + anchors.right: parent.right + anchors.rightMargin: 5 * ApplicationInfo.ratio + anchors.bottom: flickableList.bottom + anchors.bottomMargin: 30 * ApplicationInfo.ratio + width: parent.width / 20 + height: width * heightRatio + onUp: flickableList.flick(0, 1400) + onDown: flickableList.flick(0, -1400) + upVisible: (flickableList.visibleArea.yPosition <= 0) ? false : true + downVisible: ((flickableList.visibleArea.yPosition + flickableList.visibleArea.heightRatio) >= 1) ? false : true + } + } + Item { width: 1; height: 10 } + } + } + + GCButtonCancel { + onClose: { + parent.close() + } + } +} diff --git a/src/activities/menu/ConfigurationItem.qml b/src/activities/menu/ConfigurationItem.qml index 012ac5140..c5358ed52 100644 --- a/src/activities/menu/ConfigurationItem.qml +++ b/src/activities/menu/ConfigurationItem.qml @@ -1,824 +1,846 @@ /* GCompris - ConfigurationItem.qml * * Copyright (C) 2014-2016 Johnny Jazeix * * Authors: * Johnny Jazeix * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import QtQuick 2.6 import QtQuick.Controls 1.5 import QtQuick.Controls.Styles 1.4 import GCompris 1.0 import QtMultimedia 5.0 import "../../core" import "qrc:/gcompris/src/core/core.js" as Core Item { id: dialogConfig property var languages: allLangs.languages height: column.height LanguageList { id: allLangs } Column { id: column spacing: 10 width: parent.width move: Transition { NumberAnimation { properties: "x,y"; duration: 120 } } // Put configuration here Row { id: demoModeBox width: parent.width spacing: 10 property bool checked: !ApplicationSettings.isDemoMode Image { sourceSize.height: 50 * ApplicationInfo.ratio source: demoModeBox.checked ? "qrc:/gcompris/src/core/resource/apply.svg" : "qrc:/gcompris/src/core/resource/cancel.svg" MouseArea { anchors.fill: parent onClicked: { if(ApplicationSettings.isDemoMode) ApplicationSettings.isDemoMode = false } } } Button { width: parent.parent.width - 50 * ApplicationInfo.ratio - 10 * 2 height: parent.height enabled: ApplicationSettings.isDemoMode anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter text: demoModeBox.checked ? qsTr("You have the full version") : qsTr("Buy the full version").toUpperCase() style: ButtonStyle { background: Rectangle { implicitWidth: 100 implicitHeight: 25 border.width: control.activeFocus ? 4 : 2 border.color: "black" radius: 10 gradient: Gradient { GradientStop { position: 0 ; color: control.pressed ? "#87ff5c" : ApplicationSettings.isDemoMode ? "#ffe85c" : "#EEEEEE"} GradientStop { position: 1 ; color: control.pressed ? "#44ff00" : ApplicationSettings.isDemoMode ? "#f8d600" : "#AAAAAA"} } } label: GCText { text: control.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter wrapMode: Text.WordWrap } } onClicked: { if(ApplicationSettings.activationMode == 1) { if(ApplicationSettings.isDemoMode) ApplicationSettings.isDemoMode = false } else if(ApplicationSettings.activationMode == 2) { activationCodeEntry.visible = !activationCodeEntry.visible } } } } Column { id: activationCodeEntry width: parent.width spacing: 10 visible: false opacity: 0 Behavior on opacity { NumberAnimation { duration: 200 } } onVisibleChanged: { if(visible) { activationInput.forceActiveFocus() activationInput.cursorPosition = 0 opacity = 1 } else { activationInput.focus = false opacity = 0 } } GCText { id: activationInstruction fontSize: regularSize color: "black" style: Text.Outline styleColor: "white" horizontalAlignment: Text.AlignHCenter width: parent.width wrapMode: TextEdit.WordWrap text: qsTr("On https://gcompris.net " + "you will find the instructions to obtain an activation code.") Component.onCompleted: ApplicationInfo.isDownloadAllowed ? linkActivated.connect(Qt.openUrlExternally) : null } TextInput { id: activationInput width: parent.width focus: true font.weight: Font.DemiBold font.pointSize: ApplicationSettings.baseFontSize + 14 * ApplicationInfo.fontRatio color: 'black' horizontalAlignment: Text.AlignHCenter inputMask: '>HHHH-HHHH-HHHH;#' text: ApplicationSettings.codeKey onTextChanged: { var code = text.replace(/-/g,'') var codeValidity = ApplicationSettings.checkActivationCode(code); switch (codeValidity) { case 0: activationMsg.text = qsTr('Enter your activation code'); break; case 1: activationMsg.text = qsTr('Sorry, your code is too old for this version of GCompris'); break; case 2: activationMsg.text = qsTr('Your code is valid, thanks a lot for your support'); activationCodeEntry.visible = false ApplicationSettings.codeKey = code break; } } } GCText { id: activationMsg width: parent.width color: "black" fontSize: regularSize horizontalAlignment: TextInput.AlignHCenter wrapMode: TextEdit.WordWrap } } GCDialogCheckBox { id: displayLockedActivitiesBox text: qsTr("Show locked activities") visible: ApplicationSettings.isDemoMode checked: showLockedActivities onCheckedChanged: { showLockedActivities = checked; } } GCDialogCheckBox { id: enableAudioVoicesBox text: qsTr("Enable audio voices") checked: isAudioVoicesEnabled onCheckedChanged: { isAudioVoicesEnabled = checked; } } GCDialogCheckBox { id: enableAudioEffectsBox text: qsTr("Enable audio effects") checked: isAudioEffectsEnabled onCheckedChanged: { isAudioEffectsEnabled = checked; } } GCDialogCheckBox { id: enableBackgroundMusicBox text: qsTr("Enable background music") checked: isBackgroundMusicEnabled onCheckedChanged: { isBackgroundMusicEnabled = checked; } } Flow { width: parent.width spacing: 5 * ApplicationInfo.ratio GCText { text: qsTr("Background Music") fontSize: mediumSize height: 50 * ApplicationInfo.ratio } // Padding Item { height: 1 width: 10 * ApplicationInfo.ratio } Button { id: backgroundMusicName height: 30 * ApplicationInfo.ratio text: { if(backgroundMusic.playbackState != Audio.PlayingState) return qsTr("Not playing") var musicDirectoryPath = ApplicationInfo.getAudioFilePath("backgroundMusic/") var musicName = String(backgroundMusic.source) musicName = musicName.slice(musicDirectoryPath.length, musicName.length) - print("Music name: " + musicName) return musicName.slice(0, musicName.lastIndexOf('.')) } style: GCButtonStyle {} + onClicked: { + dialogConfig.visible = false + backgroundMusicList.visible = true + } } // Padding Item { height: 1 width: 10 * ApplicationInfo.ratio } Image { source: "qrc:/gcompris/src/core/resource/bar_next.svg" sourceSize.height: Math.min(50 * ApplicationInfo.ratio, parent.width / 8) MouseArea { anchors.fill: parent enabled: (backgroundMusic.playbackState == Audio.PlayingState && !backgroundMusic.muted) onClicked: backgroundMusic.nextAudio() } } } GCDialogCheckBox { id: enableFullscreenBox text: qsTr("Fullscreen") checked: isFullscreen onCheckedChanged: { isFullscreen = checked; } visible: !ApplicationInfo.isMobile } GCDialogCheckBox { id: enableVirtualKeyboardBox text: qsTr("Virtual Keyboard") checked: isVirtualKeyboard onCheckedChanged: { isVirtualKeyboard = checked; } } GCDialogCheckBox { id: enableAutomaticDownloadsBox checked: isAutomaticDownloadsEnabled text: qsTr("Enable automatic downloads/updates of sound files") visible: ApplicationInfo.isDownloadAllowed onCheckedChanged: { isAutomaticDownloadsEnabled = checked; } } /* Technically wordset config is a string that holds the wordset name or '' for the * internal wordset. But as we support only internal and words its best to show the * user a boolean choice. */ GCDialogCheckBox { id: wordsetBox checked: DownloadManager.isDataRegistered("words") text: enabled ? qsTr("Use full word image set") : qsTr("Download full word image set") visible: ApplicationInfo.isDownloadAllowed enabled: !DownloadManager.isDataRegistered("words") onCheckedChanged: { wordset = checked ? 'data2/words/words.rcc' : ''; } } GCDialogCheckBox { id: sectionVisibleBox checked: sectionVisible text: qsTr("The activity section menu is visible") onCheckedChanged: { sectionVisible = checked; } } Flow { spacing: 5 width: parent.width GCComboBox { id: fontBox model: fonts background: dialogActivityConfig label: qsTr("Font selector") } } Flow { spacing: 5 width: parent.width GCSlider { id: baseFontSizeSlider width: 250 * ApplicationInfo.ratio maximumValue: ApplicationSettings.baseFontSizeMax minimumValue: ApplicationSettings.baseFontSizeMin value: baseFontSize onValueChanged: ApplicationSettings.baseFontSize = value; scrollEnabled: false } GCText { id: baseFontSizeText text: qsTr("Font size") fontSize: mediumSize wrapMode: Text.WordWrap } Button { height: 30 * ApplicationInfo.ratio text: qsTr("Default"); style: GCButtonStyle {} onClicked: baseFontSizeSlider.value = 0.0 } } Flow { spacing: 5 width: parent.width GCComboBox { id: fontCapitalizationBox model: fontCapitalizationModel background: dialogActivityConfig label: qsTr("Font Capitalization") } } Flow { spacing: 5 width: parent.width GCSlider { id: fontLetterSpacingSlider width: 250 * ApplicationInfo.ratio maximumValue: ApplicationSettings.fontLetterSpacingMax minimumValue: ApplicationSettings.fontLetterSpacingMin value: fontLetterSpacing onValueChanged: ApplicationSettings.fontLetterSpacing = value; scrollEnabled: false } GCText { id: fontLetterSpacingText text: qsTr("Font letter spacing") fontSize: mediumSize wrapMode: Text.WordWrap } Button { height: 30 * ApplicationInfo.ratio text: qsTr("Default"); style: GCButtonStyle {} onClicked: fontLetterSpacingSlider.value = ApplicationSettings.fontLetterSpacingMin } } Flow { spacing: 5 width: parent.width GCComboBox { id: languageBox model: dialogConfig.languages background: dialogActivityConfig onCurrentIndexChanged: voicesRow.localeChanged(); label: qsTr("Language selector") } } Flow { id: voicesRow width: parent.width spacing: 5 * ApplicationInfo.ratio property bool haveLocalResource: false function localeChanged() { var language = dialogConfig.languages[languageBox.currentIndex].text; voicesRow.haveLocalResource = DownloadManager.isDataRegistered( "voices-" + ApplicationInfo.CompressedAudio + "/" + ApplicationInfo.getVoicesLocale(dialogConfig.languages[languageBox.currentIndex].locale) ) } Connections { target: DownloadManager onDownloadFinished: voicesRow.localeChanged() } GCText { id: voicesText text: qsTr("Localized voices") fontSize: mediumSize wrapMode: Text.WordWrap } Image { id: voicesImage sourceSize.height: 30 * ApplicationInfo.ratio source: voicesRow.haveLocalResource ? "qrc:/gcompris/src/core/resource/apply.svg" : "qrc:/gcompris/src/core/resource/cancel.svg" } Button { id: voicesButton height: 30 * ApplicationInfo.ratio visible: ApplicationInfo.isDownloadAllowed text: voicesRow.haveLocalResource ? qsTr("Check for updates") : qsTr("Download") style: GCButtonStyle {} onClicked: { if (DownloadManager.downloadResource( DownloadManager.getVoicesResourceForLocale(dialogConfig.languages[languageBox.currentIndex].locale))) { var downloadDialog = Core.showDownloadDialog(dialogConfig.parent.rootItem, {}); } } } } Flow { width: parent.width spacing: 5 * ApplicationInfo.ratio GCText { text: qsTr("Difficulty filter:") fontSize: mediumSize height: 50 * ApplicationInfo.ratio } // Padding Item { height: 1 width: 10 * ApplicationInfo.ratio } Image { source: "qrc:/gcompris/src/core/resource/bar_next.svg" sourceSize.height: Math.min(50 * ApplicationInfo.ratio, parent.width / 8) MouseArea { anchors.fill: parent onClicked: { filterRepeater.setMin(filterRepeater.min + 1) } } } // Padding Item { height: 1 width: 5 * ApplicationInfo.ratio } // Level filtering Repeater { id: filterRepeater model: 6 property int min: ApplicationSettings.filterLevelMin property int max: ApplicationSettings.filterLevelMax function setMin(value) { var newMin if(min < 1) newMin = 1 else if(min > 6) newMin = 6 else if(max >= value) newMin = value if(newMin) ApplicationSettings.filterLevelMin = newMin } function setMax(value) { var newMax if(max < 1) newMax = 1 else if(max > 6) newMax = 6 else if(min <= value) newMax = value if(newMax) ApplicationSettings.filterLevelMax = newMax } Image { source: "qrc:/gcompris/src/core/resource/difficulty" + (modelData + 1) + ".svg"; sourceSize.width: Math.min(50 * ApplicationInfo.ratio, parent.width / 8) opacity: modelData + 1 >= filterRepeater.min && modelData + 1 <= filterRepeater.max ? 1 : 0.4 property int value: modelData + 1 MouseArea { anchors.fill: parent onClicked: { if(parent.value < filterRepeater.max) { if(parent.opacity == 1) filterRepeater.setMin(parent.value + 1) else filterRepeater.setMin(parent.value) } else if(parent.value > filterRepeater.min) { if(parent.opacity == 1) filterRepeater.setMax(parent.value - 1) else filterRepeater.setMax(parent.value) } } } } } // Padding Item { height: 1 width: 5 * ApplicationInfo.ratio } Image { source: "qrc:/gcompris/src/core/resource/bar_previous.svg" sourceSize.height: Math.min(50 * ApplicationInfo.ratio, parent.width / 8) MouseArea { anchors.fill: parent onClicked: { filterRepeater.setMax(filterRepeater.max - 1) } } } } } property bool showLockedActivities: ApplicationSettings.showLockedActivities property bool isAudioVoicesEnabled: ApplicationSettings.isAudioVoicesEnabled property bool isAudioEffectsEnabled: ApplicationSettings.isAudioEffectsEnabled property bool isBackgroundMusicEnabled: ApplicationSettings.isBackgroundMusicEnabled property bool isFullscreen: ApplicationSettings.isFullscreen property bool isVirtualKeyboard: ApplicationSettings.isVirtualKeyboard property bool isAutomaticDownloadsEnabled: ApplicationSettings.isAutomaticDownloadsEnabled property bool sectionVisible: ApplicationSettings.sectionVisible property string wordset: ApplicationSettings.wordset + property var filteredBackgroundMusic: ApplicationSettings.filteredBackgroundMusic + property var allBackgroundMusic: ApplicationInfo.getBackgroundMusicFromRcc() property int baseFontSize // don't bind to ApplicationSettings.baseFontSize property real fontLetterSpacing // don't bind to ApplicationSettings.fontLetterSpacing // or we get a binding loop warning function loadFromConfig() { // Synchronize settings with data showLockedActivities = ApplicationSettings.showLockedActivities isAudioVoicesEnabled = ApplicationSettings.isAudioVoicesEnabled enableAudioVoicesBox.checked = isAudioVoicesEnabled isAudioEffectsEnabled = ApplicationSettings.isAudioEffectsEnabled enableAudioEffectsBox.checked = isAudioEffectsEnabled isBackgroundMusicEnabled = ApplicationSettings.isBackgroundMusicEnabled enableBackgroundMusicBox.checked = isBackgroundMusicEnabled isFullscreen = ApplicationSettings.isFullscreen enableFullscreenBox.checked = isFullscreen isVirtualKeyboard = ApplicationSettings.isVirtualKeyboard enableVirtualKeyboardBox.checked = isVirtualKeyboard isAutomaticDownloadsEnabled = ApplicationSettings.isAutomaticDownloadsEnabled enableAutomaticDownloadsBox.checked = isAutomaticDownloadsEnabled sectionVisible = ApplicationSettings.sectionVisible sectionVisibleBox.checked = sectionVisible wordset = ApplicationSettings.wordset wordsetBox.checked = DownloadManager.isDataRegistered("words") || ApplicationSettings.wordset == 'data2/words/words.rcc' wordsetBox.enabled = !DownloadManager.isDataRegistered("words") - baseFontSize = ApplicationSettings.baseFontSize; - fontLetterSpacing = ApplicationSettings.fontLetterSpacing; + baseFontSize = ApplicationSettings.baseFontSize + fontLetterSpacing = ApplicationSettings.fontLetterSpacing + filteredBackgroundMusic = ApplicationSettings.filteredBackgroundMusic + allBackgroundMusic = ApplicationInfo.getBackgroundMusicFromRcc() + if(filteredBackgroundMusic.length === 0) + filteredBackgroundMusic = allBackgroundMusic // Set locale for(var i = 0 ; i < dialogConfig.languages.length ; i ++) { if(dialogConfig.languages[i].locale === ApplicationSettings.locale) { languageBox.currentIndex = i; break; } } // Set font for(var i = 0 ; i < fonts.count ; i ++) { if(fonts.get(i).text == ApplicationSettings.font) { fontBox.currentIndex = i; break; } } // Set font capitalization for(var i = 0 ; i < fontCapitalizationModel.length ; i ++) { if(fontCapitalizationModel[i].value == ApplicationSettings.fontCapitalization) { fontCapitalizationBox.currentIndex = i; break; } } } function save() { ApplicationSettings.showLockedActivities = showLockedActivities ApplicationSettings.isAudioVoicesEnabled = isAudioVoicesEnabled ApplicationSettings.isAudioEffectsEnabled = isAudioEffectsEnabled ApplicationSettings.isBackgroundMusicEnabled = isBackgroundMusicEnabled + ApplicationSettings.filteredBackgroundMusic = filteredBackgroundMusic ApplicationSettings.isFullscreen = isFullscreen ApplicationSettings.isVirtualKeyboard = isVirtualKeyboard ApplicationSettings.isAutomaticDownloadsEnabled = isAutomaticDownloadsEnabled ApplicationSettings.sectionVisible = sectionVisible ApplicationSettings.wordset = wordset ApplicationSettings.isEmbeddedFont = fonts.get(fontBox.currentIndex).isLocalResource; ApplicationSettings.font = fonts.get(fontBox.currentIndex).text ApplicationSettings.fontCapitalization = fontCapitalizationModel[fontCapitalizationBox.currentIndex].value ApplicationSettings.saveBaseFontSize(); ApplicationSettings.notifyFontLetterSpacingChanged(); if (ApplicationSettings.locale != dialogConfig.languages[languageBox.currentIndex].locale) { ApplicationSettings.locale = dialogConfig.languages[languageBox.currentIndex].locale if(ApplicationInfo.isDownloadAllowed && !DownloadManager.isDataRegistered( "voices-" + ApplicationInfo.CompressedAudio + "/" + ApplicationInfo.getVoicesLocale(dialogConfig.languages[languageBox.currentIndex].locale) )) { // ask for downloading new voices Core.showMessageDialog(main, qsTr("You selected a new locale. You need to restart GCompris to play in your new locale.
Do you want to download the corresponding sound files now?"), qsTr("Yes"), function() { // yes -> start download if (DownloadManager.downloadResource( DownloadManager.getVoicesResourceForLocale(ApplicationSettings.locale))) var downloadDialog = Core.showDownloadDialog(main, {}); }, qsTr("No"), null, null ); } else { // check for updates or/and register new voices DownloadManager.updateResource( DownloadManager.getVoicesResourceForLocale(ApplicationSettings.locale)) } } // download words.rcc if needed if(ApplicationSettings.wordset != "") { // we want to use the external dataset, it is either in // words/words.rcc or full-${CA}.rcc if(DownloadManager.isDataRegistered("words")) { // we either have it, we try to update in the background // or we are downloading it if(DownloadManager.haveLocalResource(wordset)) DownloadManager.updateResource(wordset) } else { // download automatically if automatic download else ask for download if(isAutomaticDownloadsEnabled) { var prevAutomaticDownload = ApplicationSettings.isAutomaticDownloadsEnabled ApplicationSettings.isAutomaticDownloadsEnabled = true; DownloadManager.updateResource(wordset); ApplicationSettings.isAutomaticDownloadsEnabled = prevAutomaticDownload } else { Core.showMessageDialog(main, qsTr("The images for several activities are not yet installed. ") + qsTr("Do you want to download them now?"), qsTr("Yes"), function() { if (DownloadManager.downloadResource(wordset)) var downloadDialog = Core.showDownloadDialog(pageView.currentItem, {}); }, qsTr("No"), function() { ApplicationSettings.wordset = '' }, null ); } } } // download backgroundMusic.rcc if needed if(DownloadManager.isDataRegistered("backgroundMusic")) { // we either have it, we try to update in the background // or we are downloading it if(DownloadManager.haveLocalResource(DownloadManager.getBackgroundMusicResources())) DownloadManager.updateResource(DownloadManager.getBackgroundMusicResources()) } else { // download automatically if automatic download else ask for download if(isAutomaticDownloadsEnabled) { var prevAutomaticDownload = ApplicationSettings.isAutomaticDownloadsEnabled ApplicationSettings.isAutomaticDownloadsEnabled = true; DownloadManager.updateResource(DownloadManager.getBackgroundMusicResources()); ApplicationSettings.isAutomaticDownloadsEnabled = prevAutomaticDownload } else { Core.showMessageDialog(main, qsTr("The background music is not yet installed. ") + qsTr("Do you want to download it now?"), qsTr("Yes"), function() { if (DownloadManager.downloadResource(DownloadManager.getBackgroundMusicResources())) var downloadDialog = Core.showDownloadDialog(pageView.currentItem, {}); }, qsTr("No"),null ); } } } ListModel { id: fonts Component.onCompleted: { var systemFonts = Qt.fontFamilies(); var rccFonts = ApplicationInfo.getFontsFromRcc(); // Remove explicitly all *symbol* and *ding* fonts var excludedFonts = ApplicationInfo.getSystemExcludedFonts(); excludedFonts.push("ding"); excludedFonts.push("symbol"); // first display fonts from rcc for(var i = 0 ; i < rccFonts.length ; ++ i) { // Append fonts from resources fonts.append({ "text": rccFonts[i], "isLocalResource": true }); } for(var i = 0 ; i < systemFonts.length ; ++ i) { var isExcluded = false; var systemFont = systemFonts[i].toLowerCase(); // Remove symbol fonts for(var j = 0 ; j < excludedFonts.length ; ++ j) { if(systemFont.indexOf(excludedFonts[j].toLowerCase()) != -1) { isExcluded = true; break; } } // Remove fonts from rcc (if you have a default font from rcc, Qt will add it to systemFonts) for(var j = 0 ; j < rccFonts.length ; ++ j) { if(rccFonts[j].toLowerCase().indexOf(systemFont) != -1) { isExcluded = true; break; } } // Finally, we know if we add this font or not if(!isExcluded) { fonts.append({ "text": systemFonts[i], "isLocalResource": false }); } } } } property var fontCapitalizationModel: [ { text: qsTr("Mixed case (default)"), value: Font.MixedCase }, { text: qsTr("All uppercase"), value: Font.AllUppercase }, { text: qsTr("All lowercase"), value: Font.AllLowercase } ] + + function isFilteredBackgroundMusicChanged() { + initialFilteredMusic = ApplicationSettings.filteredBackgroundMusic + if(initialFilteredMusic.length != filteredBackgroundMusic.length) + return true + for(var i = 0; i < initialFilteredMusic.length; i++) + if(filteredBackgroundMusic.indexOf(initialFilteredMusic[i]) == -1) + return true + + return false + } function hasConfigChanged() { return (ApplicationSettings.locale !== dialogConfig.languages[languageBox.currentIndex].locale || (ApplicationSettings.sectionVisible != sectionVisible) || (ApplicationSettings.wordset != wordset) || (ApplicationSettings.font != fonts.get(fontBox.currentIndex).text) || (ApplicationSettings.isEmbeddedFont != fonts.get(fontBox.currentIndex).isLocalResource) || (ApplicationSettings.isEmbeddedFont != fonts.get(fontBox.currentIndex).isLocalResource) || (ApplicationSettings.fontCapitalization != fontCapitalizationModel[(fontcapitalizationBox.currentIndex)].value) || (ApplicationSettings.fontLetterSpacing != fontLetterSpacing) || (ApplicationSettings.isAudioVoicesEnabled != isAudioVoicesEnabled) || (ApplicationSettings.isAudioEffectsEnabled != isAudioEffectsEnabled) || (ApplicationSettings.isBackgroundMusicEnabled != isBackgroundMusicEnabled) || (ApplicationSettings.isFullscreen != isFullscreen) || (ApplicationSettings.isVirtualKeyboard != isVirtualKeyboard) || (ApplicationSettings.isAutomaticDownloadsEnabled != isAutomaticDownloadsEnabled) || (ApplicationSettings.baseFontSize != baseFontSize) || - (ApplicationSettings.showLockedActivities != showLockedActivities) + (ApplicationSettings.showLockedActivities != showLockedActivities) || + isFilteredBackgroundMusicChanged() ); } } diff --git a/src/activities/menu/Menu.qml b/src/activities/menu/Menu.qml index c9644a693..d9a0a0430 100644 --- a/src/activities/menu/Menu.qml +++ b/src/activities/menu/Menu.qml @@ -1,863 +1,871 @@ /* GCompris - Menu.qml * * Copyright (C) 2014 Bruno Coudoin * * Authors: * Bruno Coudoin (Qt Quick port) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import QtQuick 2.6 import "../../core" import GCompris 1.0 import QtGraphicalEffects 1.0 import "qrc:/gcompris/src/core/core.js" as Core import QtQuick.Controls 1.5 import QtQuick.Controls.Styles 1.4 /** * GCompris' top level menu screen. * * Displays a grid of available activities divided subdivided in activity * categories/sections. * * The visibility of the section row is toggled by the setting * ApplicationSettings.sectionVisible. * * The list of available activities depends on the following settings: * * * ApplicationSettings.showLockedActivities * * ApplicationSettings.filterLevelMin * * ApplicationSettings.filterLevelMax * * @inherit QtQuick.Item */ ActivityBase { id: activity focus: true activityInfo: ActivityInfoTree.rootMenu onBack: { pageView.pop(to); // Restore focus that has been taken by the loaded activity if(pageView.currentItem == activity) focus = true; } onHome: { if(pageView.depth === 1 && !ApplicationSettings.isKioskMode) { Core.quit(main); } else { pageView.pop(); // Restore focus that has been taken by the loaded activity if(pageView.currentItem == activity) focus = true; } } onDisplayDialog: pageView.push(dialog) onDisplayDialogs: { var toPush = new Array(); for (var i = 0; i < dialogs.length; i++) { toPush.push({item: dialogs[i]}); } pageView.push(toPush); } Connections { // At the launch of the application, box2d check is performed after we // first initialize the menu. This connection is to refresh // automatically the menu at start. target: ApplicationInfo onIsBox2DInstalledChanged: { ActivityInfoTree.filterByTag(activity.currentTag, currentCategory) ActivityInfoTree.filterLockedActivities() ActivityInfoTree.filterEnabledActivities() } } // @cond INTERNAL_DOCS property string url: "qrc:/gcompris/src/activities/menu/resource/" property var sections: [ { icon: activity.url + "all.svg", tag: "favorite" }, { icon: activity.url + "computer.svg", tag: "computer" }, { icon: activity.url + "discovery.svg", tag: "discovery", categories: [{ "logic": qsTr("Logic") }, { "arts": qsTr("Fine Arts") }, { "music": qsTr("Music") } ] }, { icon: activity.url + "sciences.svg", tag: "sciences", categories: [{ "experiment": qsTr("Experiment") }, { "history": qsTr("History") }, { "geography": qsTr("Geography") } ] }, { icon: activity.url + "fun.svg", tag: "fun" }, { icon: activity.url + "math.svg", tag: "math", categories: [{ "numeration": qsTr("Numeration") }, { "arithmetic": qsTr("Arithmetic") }, { "measures": qsTr("Measures") } ] }, { icon: activity.url + "puzzle.svg", tag: "puzzle" }, { icon: activity.url + "reading.svg", tag: "reading", categories: [{ "letters": qsTr("Letters") }, { "words": qsTr("Words") }, { "vocabulary": qsTr("Vocabulary") } ] }, { icon: activity.url + "strategy.svg", tag: "strategy" }, { icon: activity.url + "search-icon.svg", tag: "search" } ] property string currentTag: sections[0].tag property var currentTagCategories: [] property string currentCategory: "" /// @endcond pageComponent: Image { id: background source: activity.url + "background.svg" sourceSize.width: Math.max(parent.width, parent.height) height: main.height fillMode: Image.PreserveAspectCrop Timer { // triggered once at startup to populate the keyboard id: keyboardFiller interval: 1000; running: true; onTriggered: { keyboard.populate(); } } function loadActivity() { // @TODO init of item would be better in setsource but it crashes on Qt5.6 // https://bugreports.qt.io/browse/QTBUG-49793 activityLoader.item.audioVoices = audioVoices activityLoader.item.audioEffects = audioEffects activityLoader.item.loading = loading //take the focus away from textField before starting an activity searchTextField.focus = false pageView.push(activityLoader.item) } Loader { id: activityLoader asynchronous: true onStatusChanged: { if (status == Loader.Loading) { loading.start(); } else if (status == Loader.Ready) { loading.stop(); loadActivity(); } else if (status == Loader.Error) loading.stop(); } } // Filters property bool horizontal: main.width >= main.height property int sectionIconWidth: { if(horizontal) return Math.min(100 * ApplicationInfo.ratio, main.width / (sections.length + 1)) else if(activity.currentTag === "search" && ApplicationSettings.isVirtualKeyboard) return Math.min(100 * ApplicationInfo.ratio, (background.height - (bar.height+keyboard.height)) / (sections.length + 1)) else return Math.min(100 * ApplicationInfo.ratio, (background.height - bar.height) / (sections.length + 1)) } property int sectionIconHeight: sectionIconWidth property int sectionCellWidth: sectionIconWidth * 1.1 property int sectionCellHeight: sectionIconHeight * 1.1 property int categoriesHeight: currentCategory == "" ? 0 : sectionCellHeight - 2 property var currentActiveGrid: activitiesGrid property bool keyboardMode: false Keys.onPressed: { // Ctrl-modifiers should never be handled by the search-field if (event.modifiers === Qt.ControlModifier) { if (event.key === Qt.Key_S) { // Ctrl+S toggle show / hide section ApplicationSettings.sectionVisible = !ApplicationSettings.sectionVisible } } else if(currentTag === "search") { // forward to the virtual keyboard the pressed keys if(event.key == Qt.Key_Backspace) keyboard.keypress(keyboard.backspace); else keyboard.keypress(event.text); } else if(event.key === Qt.Key_Space && currentActiveGrid.currentItem) { currentActiveGrid.currentItem.selectCurrentItem() } } Keys.onReleased: { keyboardMode = true event.accepted = false } Keys.onTabPressed: { if(currentActiveGrid == section) { if(currentTagCategories && currentTagCategories.length != 0) { currentActiveGrid = categoriesGrid; } else { currentActiveGrid = activitiesGrid; } } else if(currentActiveGrid == categoriesGrid) { currentActiveGrid = activitiesGrid; } else { currentActiveGrid = section; } } Keys.onEnterPressed: if(currentActiveGrid.currentItem) currentActiveGrid.currentItem.selectCurrentItem(); Keys.onReturnPressed: if(currentActiveGrid.currentItem) currentActiveGrid.currentItem.selectCurrentItem(); Keys.onRightPressed: if(currentActiveGrid.currentItem) currentActiveGrid.moveCurrentIndexRight(); Keys.onLeftPressed: if(currentActiveGrid.currentItem) currentActiveGrid.moveCurrentIndexLeft(); Keys.onDownPressed: if(currentActiveGrid.currentItem && !currentActiveGrid.atYEnd) currentActiveGrid.moveCurrentIndexDown(); Keys.onUpPressed: if(currentActiveGrid.currentItem && !currentActiveGrid.atYBeginning) currentActiveGrid.moveCurrentIndexUp(); GridView { id: section model: sections width: horizontal ? main.width : sectionCellWidth height: { if(horizontal) return sectionCellHeight else if(activity.currentTag === "search" && ApplicationSettings.isVirtualKeyboard) return sectionCellHeight * (sections.length+1) else return main.height - bar.height } x: ApplicationSettings.sectionVisible ? section.initialX : -sectionCellWidth y: ApplicationSettings.sectionVisible ? section.initialY : -sectionCellHeight visible: ApplicationSettings.sectionVisible cellWidth: sectionCellWidth cellHeight: sectionCellHeight interactive: false keyNavigationWraps: true property int initialX: 4 property int initialY: 4 Component { id: sectionDelegate Item { id: backgroundSection width: sectionCellWidth height: sectionCellHeight Image { source: modelData.icon sourceSize.height: sectionIconHeight anchors.margins: 5 anchors.horizontalCenter: parent.horizontalCenter } ParticleSystemStarLoader { id: particles anchors.fill: backgroundSection clip: false } MouseArea { anchors.fill: backgroundSection onClicked: { selectCurrentItem() } } function selectCurrentItem() { section.currentIndex = index activity.currentTag = modelData.tag activity.currentTagCategories = modelData.categories if(modelData.categories != undefined) { currentCategory = Object.keys(modelData.categories[0])[0]; } else { currentCategory = "" } particles.burst(10) if(modelData.tag === "search") { ActivityInfoTree.filterBySearch(searchTextField.text); } else { ActivityInfoTree.filterByTag(modelData.tag, currentCategory) ActivityInfoTree.filterLockedActivities() ActivityInfoTree.filterEnabledActivities() } } } } delegate: sectionDelegate highlight: Item { width: sectionCellWidth height: sectionCellHeight Rectangle { anchors.fill: parent color: "#5AFFFFFF" } Image { source: "qrc:/gcompris/src/core/resource/button.svg" anchors.fill: parent } Behavior on x { SpringAnimation { spring: 2; damping: 0.2 } } Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } } } } // Activities property int iconWidth: 120 * ApplicationInfo.ratio property int activityCellWidth: horizontal ? background.width / Math.floor(background.width / iconWidth) : (background.width - section.width) / Math.floor((background.width - section.width) / iconWidth) property int activityCellHeight: iconWidth * 1.7 Loader { id: warningOverlay anchors { top: horizontal ? section.bottom : parent.top bottom: parent.bottom left: horizontal ? parent.left : section.right right: parent.right margins: 4 } active: (ActivityInfoTree.menuTree.length === 0) && (currentTag === "favorite") sourceComponent: Item { anchors.fill: parent GCText { id: instructionTxt fontSize: smallSize y: height * 0.2 x: (parent.width - width) / 2 z: 2 width: parent.width * 0.6 horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap font.weight: Font.DemiBold color: 'white' text: qsTr("Put your favorite activities here by selecting the " + "sun at the top right of that activity.") } Rectangle { anchors.fill: instructionTxt anchors.margins: -6 z: 1 opacity: 0.5 radius: 10 border.width: 2 border.color: "black" gradient: Gradient { GradientStop { position: 0.0; color: "#000" } GradientStop { position: 0.9; color: "#666" } GradientStop { position: 1.0; color: "#AAA" } } } } } GridView { id: categoriesGrid model: currentTagCategories anchors.top: horizontal ? section.bottom : parent.top topMargin: 5 interactive: false keyNavigationWraps: true width: horizontal ? main.width : main.width - section.width visible: activity.currentTag !== "search" x: { if(currentTagCategories) { if(horizontal) { return categoriesGrid.width / (4 * (currentTagCategories.length+1)) } else { return categoriesGrid.width / (4 * (currentTagCategories.length+1)) + section.width } } else { return 0 } } cellWidth: currentTagCategories ? categoriesGrid.width / currentTagCategories.length : 0 cellHeight: height height: horizontal ? categoriesHeight * 0.5 : categoriesHeight delegate: Button { id: button style: GCButtonStyle { selected: currentCategory === button.category theme: "categories" textSize: "regular" haveIconRight: horizontal } width: categoriesGrid.width / (currentTagCategories.length + 1) height: categoriesGrid.cellHeight text: Object.values(modelData)[0] property string category: Object.keys(modelData)[0] onClicked: { selectCurrentItem() } function selectCurrentItem() { categoriesGrid.currentIndex = index currentCategory = Object.keys(modelData)[0] ActivityInfoTree.filterByTag(currentTag, currentCategory) ActivityInfoTree.filterLockedActivities() ActivityInfoTree.filterEnabledActivities() } Image { visible: horizontal source: "qrc:/gcompris/src/activities/menu/resource/category-" + button.category + ".svg"; height: Math.round(parent.height * 0.8) sourceSize.height: height width: height anchors { verticalCenter: parent.verticalCenter right: parent.right rightMargin: parent.height * 0.1 } } } highlight: Rectangle { z: 10 width: activityCellWidth - activitiesGrid.spacing height: activityCellHeight - activitiesGrid.spacing color: "#00FFFFFF" radius: 10 border.width: 5 border.color: "#FF87A6DD" visible: true Behavior on x { SpringAnimation { spring: 2; damping: 0.2 } } Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } } } } GridView { id: activitiesGrid anchors { top: { if(activity.currentTag === "search") return searchBar.bottom else return categoriesGrid.bottom } bottom: bar.top left: horizontal ? parent.left : section.right margins: 4 topMargin: currentCategory == "" ? 4 : 10 } width: background.width cellWidth: activityCellWidth cellHeight: activityCellHeight clip: true model: ActivityInfoTree.menuTree keyNavigationWraps: true property int spacing: 10 delegate: Item { id: delegateItem width: activityCellWidth - activitiesGrid.spacing height: activityCellHeight - activitiesGrid.spacing Rectangle { id: activityBackground width: parent.width height: parent.height anchors.horizontalCenter: parent.horizontalCenter color: "white" opacity: 0.5 } Image { source: "qrc:/gcompris/src/activities/" + icon; anchors.top: activityBackground.top anchors.horizontalCenter: parent.horizontalCenter width: iconWidth - activitiesGrid.spacing height: width sourceSize.width: width fillMode: Image.PreserveAspectFit anchors.margins: 5 Image { source: "qrc:/gcompris/src/core/resource/difficulty" + ActivityInfoTree.menuTree[index].difficulty + ".svg"; anchors.top: parent.top sourceSize.width: iconWidth * 0.15 x: 5 } Image { anchors { horizontalCenter: parent.horizontalCenter top: parent.top rightMargin: 4 } source: demo || !ApplicationSettings.isDemoMode ? "" : activity.url + "lock.svg" sourceSize.width: 30 * ApplicationInfo.ratio } Image { anchors { left: parent.left bottom: parent.bottom } source: ActivityInfoTree.menuTree[index].createdInVersion == ApplicationInfo.GCVersionCode ? activity.url + "new.svg" : "" sourceSize.width: 30 * ApplicationInfo.ratio } GCText { id: title anchors.top: parent.bottom anchors.horizontalCenter: parent.horizontalCenter horizontalAlignment: Text.AlignHCenter width: activityBackground.width fontSizeMode: Text.Fit minimumPointSize: 7 fontSize: regularSize elide: Text.ElideRight maximumLineCount: 2 wrapMode: Text.WordWrap text: ActivityInfoTree.menuTree[index].title } // If we have enough room at the bottom display the description GCText { id: description visible: delegateItem.height - (title.y + title.height) > description.height ? 1 : 0 anchors.top: title.bottom anchors.horizontalCenter: parent.horizontalCenter horizontalAlignment: Text.AlignHCenter width: activityBackground.width fontSizeMode: Text.Fit minimumPointSize: 7 fontSize: regularSize elide: Text.ElideRight maximumLineCount: 3 wrapMode: Text.WordWrap text: ActivityInfoTree.menuTree[index].description } } ParticleSystemStarLoader { id: particles anchors.fill: activityBackground } MouseArea { anchors.fill: activityBackground onClicked: selectCurrentItem() } Image { source: activity.url + (favorite ? "all.svg" : "all_disabled.svg"); anchors { top: parent.top right: parent.right rightMargin: 4 * ApplicationInfo.ratio } sourceSize.width: iconWidth * 0.25 visible: ApplicationSettings.sectionVisible MouseArea { anchors.fill: parent onClicked: favorite = !favorite } } function selectCurrentItem() { if(pageView.busy) return particles.burst(50) ActivityInfoTree.currentActivity = ActivityInfoTree.menuTree[index] activityLoader.setSource("qrc:/gcompris/src/activities/" + ActivityInfoTree.menuTree[index].name, { 'menu': activity, 'activityInfo': ActivityInfoTree.currentActivity }) if (activityLoader.status == Loader.Ready) loadActivity() } } highlight: Rectangle { width: activityCellWidth - activitiesGrid.spacing height: activityCellHeight - activitiesGrid.spacing color: "#AAFFFFFF" border.width: 3 border.color: "black" visible: background.keyboardMode Behavior on x { SpringAnimation { spring: 2; damping: 0.2 } } Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } } } Rectangle { id: activitiesMask visible: false anchors.fill: activitiesGrid gradient: Gradient { GradientStop { position: 0.0; color: "#FFFFFFFF" } GradientStop { position: 0.92; color: "#FFFFFFFF" } GradientStop { position: 0.96; color: "#00FFFFFF"} } } layer.enabled: ApplicationInfo.useOpenGL layer.effect: OpacityMask { id: activitiesOpacity source: activitiesGrid maskSource: activitiesMask anchors.fill: activitiesGrid } } // The scroll buttons GCButtonScroll { visible: !ApplicationInfo.useOpenGL anchors.right: parent.right anchors.rightMargin: 5 * ApplicationInfo.ratio anchors.bottom: activitiesGrid.bottom anchors.bottomMargin: 30 * ApplicationInfo.ratio onUp: activitiesGrid.flick(0, 1127) onDown: activitiesGrid.flick(0, -1127) upVisible: activitiesGrid.visibleArea.yPosition <= 0 ? false : true downVisible: activitiesGrid.visibleArea.yPosition >= 1 ? false : true } Rectangle { id: categories width: horizontal ? parent.width : parent.width - (section.width+10) height: searchTextField.height visible: sections[activity.currentTag] === "search" anchors { top: horizontal ? section.bottom : categoriesGrid.top left: horizontal ? undefined : section.right } } Rectangle { id: searchBar width: horizontal ? parent.width/2 : parent.width - (section.width+10) height: horizontal ? sectionCellHeight * 0.5 : sectionCellHeight visible: activity.currentTag === "search" anchors { top: horizontal ? section.bottom : parent.top left: horizontal ? undefined : section.right } anchors.topMargin: horizontal ? 0 : 4 anchors.bottomMargin: horizontal ? 0 : 4 anchors.horizontalCenter: horizontal ? parent.horizontalCenter : undefined opacity: 0.5 radius: 10 border.width: 2 border.color: "black" gradient: Gradient { GradientStop { position: 0.3; color: "#000" } GradientStop { position: 0.9; color: "#666" } GradientStop { position: 1.0; color: "#AAA" } } Connections { // On mobile with GCompris' virtual keyboard activated: // Force invisibility of Androids virtual keyboard: target: (ApplicationInfo.isMobile && activity.currentTag === "search" && ApplicationSettings.isVirtualKeyboard) ? Qt.inputMethod : null onVisibleChanged: { if (ApplicationSettings.isVirtualKeyboard && visible) Qt.inputMethod.hide(); } onAnimatingChanged: { // note: seems to be never fired! if (ApplicationSettings.isVirtualKeyboard && Qt.inputMethod.visible) Qt.inputMethod.hide(); } } Connections { target: activity onCurrentTagChanged: { if (activity.currentTag === 'search') { searchTextField.focus = true; } else activity.focus = true; } } TextField { id: searchTextField width: parent.width height: parent.height textColor: "black" font.pointSize: 32 font.bold: true horizontalAlignment: TextInput.AlignHCenter verticalAlignment: TextInput.AlignVCenter font.family: GCSingletonFontLoader.fontLoader.name inputMethodHints: Qt.ImhNoPredictiveText // Note: we give focus to the textfield also in case // isMobile && !ApplicationSettings.isVirtualKeyboard // in conjunction with auto-hiding the inputMethod to always get // an input-cursor: activeFocusOnPress: true //ApplicationInfo.isMobile ? !ApplicationSettings.isVirtualKeyboard : true Keys.onReturnPressed: { if (ApplicationInfo.isMobile && !ApplicationSettings.isVirtualKeyboard) Qt.inputMethod.hide(); activity.focus = true; } onEditingFinished: { if (ApplicationInfo.isMobile && !ApplicationSettings.isVirtualKeyboard) Qt.inputMethod.hide(); activity.focus = true; } style: TextFieldStyle { placeholderTextColor: "black" } placeholderText: qsTr("Search specific activities") onTextChanged: ActivityInfoTree.filterBySearch(searchTextField.text); } } VirtualKeyboard { id: keyboard readonly property var letter: ActivityInfoTree.characters width: parent.width visible: activity.currentTag === "search" && ApplicationSettings.isVirtualKeyboard anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter onKeypress: { if(text == keyboard.backspace) { searchTextField.text = searchTextField.text.slice(0, -1); } else if(text == keyboard.space) { searchTextField.text = searchTextField.text.concat(" "); } else { searchTextField.text = searchTextField.text.concat(text); } } function populate() { var tmplayout = []; var row = 0; var offset = 0; var cols; while(offset < letter.length-1) { if(letter.length <= 100) { cols = Math.ceil((letter.length-offset) / (3 - row)); } else { cols = background.horizontal ? (Math.ceil((letter.length-offset) / (15 - row))) :(Math.ceil((letter.length-offset) / (22 - row))) if(row == 0) { tmplayout[row] = new Array(); tmplayout[row].push({ label: keyboard.backspace }); tmplayout[row].push({ label: keyboard.space }); row ++; } } tmplayout[row] = new Array(); for (var j = 0; j < cols; j++) tmplayout[row][j] = { label: letter[j+offset] }; offset += j; row ++; } if(letter.length <= 100) { tmplayout[0].push({ label: keyboard.space }); tmplayout[row-1].push({ label: keyboard.backspace }); } keyboard.layout = tmplayout } } Bar { id: bar // No exit button on mobile, UI Guidelines prohibits it content: BarEnumContent { value: help | config | about | (ApplicationInfo.isMobile ? 0 : exit) } anchors.bottom: keyboard.visible ? keyboard.top : parent.bottom onAboutClicked: { searchTextField.focus = false displayDialog(dialogAbout) } onHelpClicked: { searchTextField.focus = false displayDialog(dialogHelp) } onConfigClicked: { searchTextField.focus = false dialogActivityConfig.active = true dialogActivityConfig.loader.item.loadFromConfig() displayDialog(dialogActivityConfig) } } DialogAbout { id: dialogAbout onClose: home() } DialogHelp { id: dialogHelp onClose: home() activityInfo: ActivityInfoTree.rootMenu } DialogActivityConfig { id: dialogActivityConfig currentActivity: activity content: Component { ConfigurationItem { id: configItem width: dialogActivityConfig.width - 50 * ApplicationInfo.ratio } } onSaveData: { - dialogActivityConfig.configItem.save(); + dialogActivityConfig.configItem.save() } onClose: { if(activity.currentTag != "search") { ActivityInfoTree.filterByTag(activity.currentTag, currentCategory) ActivityInfoTree.filterLockedActivities() ActivityInfoTree.filterEnabledActivities() } else ActivityInfoTree.filterBySearch(searchTextField.text); home() } + + BackgroundMusicList { + id: backgroundMusicList + onClose: { + visible = false + dialogActivityConfig.configItem.visible = true + } + } } } } diff --git a/src/core/ApplicationSettings.cpp b/src/core/ApplicationSettings.cpp index b65f3d4c1..f6da5b541 100644 --- a/src/core/ApplicationSettings.cpp +++ b/src/core/ApplicationSettings.cpp @@ -1,514 +1,524 @@ /* GCompris - ApplicationSettings.cpp * * Copyright (C) 2014-2016 Johnny Jazeix * * Authors: * Johnny Jazeix * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include "ApplicationSettings.h" #include "ApplicationInfo.h" #include "DownloadManager.h" #include #include #include #include #include #include #include #include #define GC_DEFAULT_FONT QLatin1String("Andika-R.otf") #define GC_DEFAULT_FONT_CAPITALIZATION 0 // Font.MixedCase #define GC_DEFAULT_FONT_LETTER_SPACING 0 static const char *GENERAL_GROUP_KEY = "General"; static const char *ADMIN_GROUP_KEY = "Admin"; static const char *INTERNAL_GROUP_KEY = "Internal"; static const char *FAVORITE_GROUP_KEY = "Favorite"; static const char *FULLSCREEN_KEY = "fullscreen"; static const char *PREVIOUS_HEIGHT_KEY = "previousHeight"; static const char *PREVIOUS_WIDTH_KEY = "previousWidth"; static const char *SHOW_LOCKED_ACTIVITIES_KEY = "showLockedActivities"; static const char *ENABLE_AUDIO_VOICES_KEY = "enableAudioVoices"; static const char *ENABLE_AUDIO_EFFECTS_KEY = "enableAudioEffects"; -static const char * ENABLE_BACKGROUND_MUSIC_KEY = "enableBackgroundMusic"; +static const char *ENABLE_BACKGROUND_MUSIC_KEY = "enableBackgroundMusic"; static const char *VIRTUALKEYBOARD_KEY = "virtualKeyboard"; static const char *LOCALE_KEY = "locale"; static const char *FONT_KEY = "font"; static const char *IS_CURRENT_FONT_EMBEDDED = "isCurrentFontEmbedded"; static const char *ENABLE_AUTOMATIC_DOWNLOADS = "enableAutomaticDownloads"; +static const char *FILTERED_BACKGROUND_MUSIC_KEY = "filteredBackgroundMusic"; static const char *DOWNLOAD_SERVER_URL_KEY = "downloadServerUrl"; static const char *CACHE_PATH_KEY = "cachePath"; static const char *USERDATA_PATH_KEY = "userDataPath"; static const char *RENDERER_KEY = "renderer"; static const char *EXE_COUNT_KEY = "exeCount"; static const char *LAST_GC_VERSION_RAN = "lastGCVersionRan"; static const char *FILTER_LEVEL_MIN = "filterLevelMin"; static const char *FILTER_LEVEL_MAX = "filterLevelMax"; static const char *BASE_FONT_SIZE_KEY = "baseFontSize"; static const char *FONT_CAPITALIZATION = "fontCapitalization"; static const char *FONT_LETTER_SPACING = "fontLetterSpacing"; static const char *DEFAULT_CURSOR = "defaultCursor"; static const char *NO_CURSOR = "noCursor"; static const char *DEMO_KEY = "demo"; static const char *CODE_KEY = "key"; static const char *KIOSK_KEY = "kiosk"; static const char *SECTION_VISIBLE = "sectionVisible"; static const char *WORDSET = "wordset"; static const char *PROGRESS_KEY = "progress"; static const char *DEFAULT_DOWNLOAD_SERVER = "https://cdn.kde.org/gcompris"; ApplicationSettings *ApplicationSettings::m_instance = nullptr; ApplicationSettings::ApplicationSettings(const QString &configPath, QObject *parent): QObject(parent), m_baseFontSizeMin(-7), m_baseFontSizeMax(7), m_fontLetterSpacingMin(0.0), m_fontLetterSpacingMax(8.0), m_config(configPath, QSettings::IniFormat) { const QRect &screenSize = QGuiApplication::screens().at(0)->availableGeometry(); // initialize from settings file or default // general group m_config.beginGroup(GENERAL_GROUP_KEY); m_isAudioEffectsEnabled = m_config.value(ENABLE_AUDIO_EFFECTS_KEY, true).toBool(); m_isBackgroundMusicEnabled = m_config.value(ENABLE_BACKGROUND_MUSIC_KEY, true).toBool(); m_isFullscreen = m_config.value(FULLSCREEN_KEY, true).toBool(); m_previousHeight = m_config.value(PREVIOUS_HEIGHT_KEY, screenSize.height()).toUInt(); m_previousWidth = m_config.value(PREVIOUS_WIDTH_KEY, screenSize.width()).toUInt(); m_isAudioVoicesEnabled = m_config.value(ENABLE_AUDIO_VOICES_KEY, true).toBool(); m_isVirtualKeyboard = m_config.value(VIRTUALKEYBOARD_KEY, ApplicationInfo::getInstance()->isMobile()).toBool(); m_locale = m_config.value(LOCALE_KEY, GC_DEFAULT_LOCALE).toString(); m_font = m_config.value(FONT_KEY, GC_DEFAULT_FONT).toString(); if(m_font == QLatin1String("Andika-R.ttf")) m_font = "Andika-R.otf"; m_fontCapitalization = m_config.value(FONT_CAPITALIZATION, GC_DEFAULT_FONT_CAPITALIZATION).toUInt(); m_fontLetterSpacing = m_config.value(FONT_LETTER_SPACING, GC_DEFAULT_FONT_LETTER_SPACING).toReal(); m_isEmbeddedFont = m_config.value(IS_CURRENT_FONT_EMBEDDED, true).toBool(); + m_filteredBackgroundMusic = m_config.value(FILTERED_BACKGROUND_MUSIC_KEY, ApplicationInfo::getInstance()->getBackgroundMusicFromRcc()).toStringList(); // Init the activation mode if(QLatin1String(ACTIVATION_MODE) == "no") m_activationMode = 0; else if(QLatin1String(ACTIVATION_MODE) == "inapp") m_activationMode = 1; else if(QLatin1String(ACTIVATION_MODE) == "internal") m_activationMode = 2; else qFatal("Unknown activation mode"); // Set the demo mode if(QLatin1String(ACTIVATION_MODE) != "no") m_isDemoMode = m_config.value(DEMO_KEY, true).toBool(); else m_isDemoMode = false; m_codeKey = m_config.value(CODE_KEY, "").toString(); #if defined(WITH_KIOSK_MODE) m_isKioskMode = m_config.value(KIOSK_KEY, true).toBool(); #else m_isKioskMode = m_config.value(KIOSK_KEY, false).toBool(); #endif // Option only useful if we are in demo mode (else all the activities are available and unlocked) // By default, all the activities are displayed (even locked ones) m_showLockedActivities = m_config.value(SHOW_LOCKED_ACTIVITIES_KEY, m_isDemoMode).toBool(); m_sectionVisible = m_config.value(SECTION_VISIBLE, true).toBool(); m_wordset = m_config.value(WORDSET, "").toString(); m_isAutomaticDownloadsEnabled = m_config.value(ENABLE_AUTOMATIC_DOWNLOADS, !ApplicationInfo::getInstance()->isMobile() && ApplicationInfo::isDownloadAllowed()).toBool(); m_filterLevelMin = m_config.value(FILTER_LEVEL_MIN, 1).toUInt(); m_filterLevelMax = m_config.value(FILTER_LEVEL_MAX, 6).toUInt(); m_defaultCursor = m_config.value(DEFAULT_CURSOR, false).toBool(); m_noCursor = m_config.value(NO_CURSOR, false).toBool(); m_baseFontSize = m_config.value(BASE_FONT_SIZE_KEY, 0).toInt(); m_config.sync(); // make sure all defaults are written back m_config.endGroup(); // admin group m_config.beginGroup(ADMIN_GROUP_KEY); m_downloadServerUrl = m_config.value(DOWNLOAD_SERVER_URL_KEY, QLatin1String(DEFAULT_DOWNLOAD_SERVER)).toString(); if(m_downloadServerUrl == "http://gcompris.net") { setDownloadServerUrl(DEFAULT_DOWNLOAD_SERVER); } m_cachePath = m_config.value(CACHE_PATH_KEY, QStandardPaths::writableLocation(QStandardPaths::CacheLocation)).toString(); m_userDataPath = m_config.value(USERDATA_PATH_KEY, QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/GCompris")).toString(); m_renderer = m_config.value(RENDERER_KEY, GRAPHICAL_RENDERER).toString(); m_config.endGroup(); // internal group m_config.beginGroup(INTERNAL_GROUP_KEY); m_exeCount = m_config.value(EXE_COUNT_KEY, 0).toUInt(); m_lastGCVersionRan = m_config.value(LAST_GC_VERSION_RAN, 0).toUInt(); m_config.endGroup(); // no group m_isBarHidden = false; connect(this, &ApplicationSettings::showLockedActivitiesChanged, this, &ApplicationSettings::notifyShowLockedActivitiesChanged); connect(this, &ApplicationSettings::audioVoicesEnabledChanged, this, &ApplicationSettings::notifyAudioVoicesEnabledChanged); connect(this, &ApplicationSettings::audioEffectsEnabledChanged, this, &ApplicationSettings::notifyAudioEffectsEnabledChanged); connect(this, &ApplicationSettings::backgroundMusicEnabledChanged, this, &ApplicationSettings::notifyBackgroundMusicEnabledChanged); + connect(this, &ApplicationSettings::filteredBackgroundMusicChanged, this, &ApplicationSettings::notifyFilteredBackgroundMusicChanged); connect(this, &ApplicationSettings::fullscreenChanged, this, &ApplicationSettings::notifyFullscreenChanged); connect(this, &ApplicationSettings::previousHeightChanged, this, &ApplicationSettings::notifyPreviousHeightChanged); connect(this, &ApplicationSettings::previousWidthChanged, this, &ApplicationSettings::notifyPreviousWidthChanged); connect(this, &ApplicationSettings::localeChanged, this, &ApplicationSettings::notifyLocaleChanged); connect(this, &ApplicationSettings::fontChanged, this, &ApplicationSettings::notifyFontChanged); connect(this, &ApplicationSettings::virtualKeyboardChanged, this, &ApplicationSettings::notifyVirtualKeyboardChanged); connect(this, &ApplicationSettings::automaticDownloadsEnabledChanged, this, &ApplicationSettings::notifyAutomaticDownloadsEnabledChanged); connect(this, &ApplicationSettings::filterLevelMinChanged, this, &ApplicationSettings::notifyFilterLevelMinChanged); connect(this, &ApplicationSettings::filterLevelMaxChanged, this, &ApplicationSettings::notifyFilterLevelMaxChanged); connect(this, &ApplicationSettings::sectionVisibleChanged, this, &ApplicationSettings::notifySectionVisibleChanged); connect(this, &ApplicationSettings::wordsetChanged, this, &ApplicationSettings::notifyWordsetChanged); connect(this, &ApplicationSettings::demoModeChanged, this, &ApplicationSettings::notifyDemoModeChanged); connect(this, &ApplicationSettings::codeKeyChanged, this, &ApplicationSettings::notifyCodeKeyChanged); connect(this, &ApplicationSettings::kioskModeChanged, this, &ApplicationSettings::notifyKioskModeChanged); connect(this, &ApplicationSettings::downloadServerUrlChanged, this, &ApplicationSettings::notifyDownloadServerUrlChanged); connect(this, &ApplicationSettings::cachePathChanged, this, &ApplicationSettings::notifyCachePathChanged); connect(this, &ApplicationSettings::userDataPathChanged, this, &ApplicationSettings::notifyUserDataPathChanged); connect(this, &ApplicationSettings::rendererChanged, this, &ApplicationSettings::notifyRendererChanged); connect(this, &ApplicationSettings::exeCountChanged, this, &ApplicationSettings::notifyExeCountChanged); connect(this, &ApplicationSettings::barHiddenChanged, this, &ApplicationSettings::notifyBarHiddenChanged); connect(this, &ApplicationSettings::lastGCVersionRanChanged, this, &ApplicationSettings::notifyLastGCVersionRanChanged); } ApplicationSettings::~ApplicationSettings() { // make sure settings file is up2date: // general group m_config.beginGroup(GENERAL_GROUP_KEY); m_config.setValue(SHOW_LOCKED_ACTIVITIES_KEY, m_showLockedActivities); m_config.setValue(ENABLE_AUDIO_VOICES_KEY, m_isAudioVoicesEnabled); m_config.setValue(ENABLE_BACKGROUND_MUSIC_KEY, m_isBackgroundMusicEnabled); + m_config.setValue(FILTERED_BACKGROUND_MUSIC_KEY, m_filteredBackgroundMusic); m_config.setValue(LOCALE_KEY, m_locale); m_config.setValue(FONT_KEY, m_font); m_config.setValue(IS_CURRENT_FONT_EMBEDDED, m_isEmbeddedFont); m_config.setValue(FULLSCREEN_KEY, m_isFullscreen); m_config.setValue(PREVIOUS_HEIGHT_KEY, m_previousHeight); m_config.setValue(PREVIOUS_WIDTH_KEY, m_previousWidth); m_config.setValue(VIRTUALKEYBOARD_KEY, m_isVirtualKeyboard); m_config.setValue(ENABLE_AUTOMATIC_DOWNLOADS, m_isAutomaticDownloadsEnabled); m_config.setValue(FILTER_LEVEL_MIN, m_filterLevelMin); m_config.setValue(FILTER_LEVEL_MAX, m_filterLevelMax); m_config.setValue(DEMO_KEY, m_isDemoMode); m_config.setValue(CODE_KEY, m_codeKey); m_config.setValue(KIOSK_KEY, m_isKioskMode); m_config.setValue(SECTION_VISIBLE, m_sectionVisible); m_config.setValue(WORDSET, m_wordset); m_config.setValue(DEFAULT_CURSOR, m_defaultCursor); m_config.setValue(NO_CURSOR, m_noCursor); m_config.setValue(BASE_FONT_SIZE_KEY, m_baseFontSize); m_config.setValue(FONT_CAPITALIZATION, m_fontCapitalization); m_config.setValue(FONT_LETTER_SPACING, m_fontLetterSpacing); m_config.endGroup(); // admin group m_config.beginGroup(ADMIN_GROUP_KEY); m_config.setValue(DOWNLOAD_SERVER_URL_KEY, m_downloadServerUrl); m_config.setValue(CACHE_PATH_KEY, m_cachePath); m_config.setValue(USERDATA_PATH_KEY, m_userDataPath); m_config.setValue(RENDERER_KEY, m_renderer); m_config.endGroup(); // internal group m_config.beginGroup(INTERNAL_GROUP_KEY); m_config.setValue(EXE_COUNT_KEY, m_exeCount); m_config.setValue(LAST_GC_VERSION_RAN, m_lastGCVersionRan); m_config.endGroup(); m_config.sync(); m_instance = nullptr; } void ApplicationSettings::notifyShowLockedActivitiesChanged() { updateValueInConfig(GENERAL_GROUP_KEY, SHOW_LOCKED_ACTIVITIES_KEY, m_showLockedActivities); qDebug() << "notifyShowLockedActivitiesChanged: " << m_showLockedActivities; } void ApplicationSettings::notifyAudioVoicesEnabledChanged() { updateValueInConfig(GENERAL_GROUP_KEY, ENABLE_AUDIO_VOICES_KEY, m_isAudioVoicesEnabled); qDebug() << "notifyAudioVoices: " << m_isAudioVoicesEnabled; } void ApplicationSettings::notifyAudioEffectsEnabledChanged() { updateValueInConfig(GENERAL_GROUP_KEY, ENABLE_AUDIO_EFFECTS_KEY, m_isAudioEffectsEnabled); qDebug() << "notifyAudioEffects: " << m_isAudioEffectsEnabled; } void ApplicationSettings::notifyBackgroundMusicEnabledChanged() { updateValueInConfig(GENERAL_GROUP_KEY, ENABLE_BACKGROUND_MUSIC_KEY, m_isBackgroundMusicEnabled); qDebug() << "notifyBackgroundMusic: " << m_isBackgroundMusicEnabled; } +void ApplicationSettings::notifyFilteredBackgroundMusicChanged() +{ + updateValueInConfig(GENERAL_GROUP_KEY, FILTERED_BACKGROUND_MUSIC_KEY, m_filteredBackgroundMusic); + qDebug()<<"filteredBackgroundMusic: " << m_filteredBackgroundMusic; +} + void ApplicationSettings::notifyLocaleChanged() { updateValueInConfig(GENERAL_GROUP_KEY, LOCALE_KEY, m_locale); qDebug() << "new locale: " << m_locale; } void ApplicationSettings::notifyFontChanged() { updateValueInConfig(GENERAL_GROUP_KEY, FONT_KEY, m_font); qDebug() << "new font: " << m_font; } void ApplicationSettings::notifyEmbeddedFontChanged() { updateValueInConfig(GENERAL_GROUP_KEY, IS_CURRENT_FONT_EMBEDDED, m_isEmbeddedFont); qDebug() << "new font is embedded: " << m_isEmbeddedFont; } void ApplicationSettings::notifyFontCapitalizationChanged() { updateValueInConfig(GENERAL_GROUP_KEY, FONT_CAPITALIZATION, m_fontCapitalization); qDebug() << "new fontCapitalization: " << m_fontCapitalization; } void ApplicationSettings::notifyFontLetterSpacingChanged() { updateValueInConfig(GENERAL_GROUP_KEY, FONT_LETTER_SPACING, m_fontLetterSpacing); qDebug() << "new fontLetterSpacing: " << m_fontLetterSpacing; } void ApplicationSettings::notifyFullscreenChanged() { updateValueInConfig(GENERAL_GROUP_KEY, FULLSCREEN_KEY, m_isFullscreen); qDebug() << "fullscreen set to: " << m_isFullscreen; } void ApplicationSettings::notifyPreviousHeightChanged() { updateValueInConfig(GENERAL_GROUP_KEY, PREVIOUS_HEIGHT_KEY, m_previousHeight); qDebug() << "previous height set to: " << m_previousHeight; } void ApplicationSettings::notifyPreviousWidthChanged() { updateValueInConfig(GENERAL_GROUP_KEY, PREVIOUS_WIDTH_KEY, m_previousWidth); qDebug() << "previous width set to: " << m_previousWidth; } void ApplicationSettings::notifyVirtualKeyboardChanged() { updateValueInConfig(GENERAL_GROUP_KEY, VIRTUALKEYBOARD_KEY, m_isVirtualKeyboard); qDebug() << "virtualkeyboard set to: " << m_isVirtualKeyboard; } bool ApplicationSettings::isAutomaticDownloadsEnabled() const { return m_isAutomaticDownloadsEnabled && ApplicationInfo::isDownloadAllowed(); } void ApplicationSettings::setIsAutomaticDownloadsEnabled(const bool newIsAutomaticDownloadsEnabled) { if(ApplicationInfo::isDownloadAllowed()) { m_isAutomaticDownloadsEnabled = newIsAutomaticDownloadsEnabled; emit automaticDownloadsEnabledChanged(); } } void ApplicationSettings::notifyAutomaticDownloadsEnabledChanged() { updateValueInConfig(GENERAL_GROUP_KEY, ENABLE_AUTOMATIC_DOWNLOADS, m_isAutomaticDownloadsEnabled); qDebug() << "enableAutomaticDownloads set to: " << m_isAutomaticDownloadsEnabled; } void ApplicationSettings::notifyFilterLevelMinChanged() { updateValueInConfig(GENERAL_GROUP_KEY, FILTER_LEVEL_MIN, m_filterLevelMin); qDebug() << "filterLevelMin set to: " << m_filterLevelMin; } void ApplicationSettings::notifyFilterLevelMaxChanged() { updateValueInConfig(GENERAL_GROUP_KEY, FILTER_LEVEL_MAX, m_filterLevelMax); qDebug() << "filterLevelMax set to: " << m_filterLevelMax; } void ApplicationSettings::notifyDemoModeChanged() { updateValueInConfig(GENERAL_GROUP_KEY, DEMO_KEY, m_isDemoMode); qDebug() << "notifyDemoMode: " << m_isDemoMode; } void ApplicationSettings::notifyCodeKeyChanged() { checkPayment(); if(!m_isDemoMode) updateValueInConfig(GENERAL_GROUP_KEY, CODE_KEY, m_codeKey); qDebug() << "notifyCodeKey: " << m_codeKey; } void ApplicationSettings::notifyKioskModeChanged() { updateValueInConfig(GENERAL_GROUP_KEY, KIOSK_KEY, m_isKioskMode); qDebug() << "notifyKioskMode: " << m_isKioskMode; } void ApplicationSettings::notifySectionVisibleChanged() { updateValueInConfig(GENERAL_GROUP_KEY, SECTION_VISIBLE, m_sectionVisible); qDebug() << "notifySectionVisible: " << m_sectionVisible; } void ApplicationSettings::notifyWordsetChanged() { if(!m_wordset.isEmpty() && DownloadManager::getInstance()->haveLocalResource(m_wordset) && !DownloadManager::getInstance()->isDataRegistered("words")) { // words.rcc is there -> register old file first // then try to update in the background DownloadManager::getInstance()->updateResource(m_wordset); } updateValueInConfig(GENERAL_GROUP_KEY, WORDSET, m_wordset); qDebug() << "notifyWordset: " << m_wordset; } void ApplicationSettings::notifyDownloadServerUrlChanged() { updateValueInConfig(ADMIN_GROUP_KEY, DOWNLOAD_SERVER_URL_KEY, m_downloadServerUrl); qDebug() << "downloadServerUrl set to: " << m_downloadServerUrl; } void ApplicationSettings::notifyCachePathChanged() { updateValueInConfig(ADMIN_GROUP_KEY, CACHE_PATH_KEY, m_cachePath); qDebug() << "cachePath set to: " << m_cachePath; } void ApplicationSettings::notifyUserDataPathChanged() { updateValueInConfig(ADMIN_GROUP_KEY, USERDATA_PATH_KEY, m_userDataPath); qDebug() << "userDataPath set to: " << m_userDataPath; } void ApplicationSettings::notifyRendererChanged() { updateValueInConfig(ADMIN_GROUP_KEY, RENDERER_KEY, m_renderer); qDebug() << "renderer set to: " << m_renderer; } void ApplicationSettings::notifyExeCountChanged() { updateValueInConfig(INTERNAL_GROUP_KEY, EXE_COUNT_KEY, m_exeCount); qDebug() << "exeCount set to: " << m_exeCount; } void ApplicationSettings::notifyLastGCVersionRanChanged() { updateValueInConfig(INTERNAL_GROUP_KEY, LAST_GC_VERSION_RAN, m_lastGCVersionRan); qDebug() << "lastVersionRan set to: " << m_lastGCVersionRan; } void ApplicationSettings::notifyBarHiddenChanged() { qDebug() << "is bar hidden: " << m_isBarHidden; } void ApplicationSettings::saveBaseFontSize() { updateValueInConfig(GENERAL_GROUP_KEY, BASE_FONT_SIZE_KEY, m_baseFontSize); } void ApplicationSettings::saveActivityConfiguration(const QString &activity, const QVariantMap &data) { qDebug() << "save configuration for:" << activity; QMapIterator i(data); while (i.hasNext()) { i.next(); updateValueInConfig(activity, i.key(), i.value()); } } QVariantMap ApplicationSettings::loadActivityConfiguration(const QString &activity) { qDebug() << "load configuration for:" << activity; m_config.beginGroup(activity); QStringList keys = m_config.childKeys(); QVariantMap data; for(const QString &key : keys) { data[key] = m_config.value(key); } m_config.endGroup(); return data; } void ApplicationSettings::setFavorite(const QString &activity, bool favorite) { updateValueInConfig(FAVORITE_GROUP_KEY, activity, favorite); } bool ApplicationSettings::isFavorite(const QString &activity) { m_config.beginGroup(FAVORITE_GROUP_KEY); bool favorite = m_config.value(activity, false).toBool(); m_config.endGroup(); return favorite; } template void ApplicationSettings::updateValueInConfig(const QString& group, const QString& key, const T& value) { m_config.beginGroup(group); m_config.setValue(key, value); m_config.endGroup(); m_config.sync(); } int ApplicationSettings::loadActivityProgress(const QString &activity) { int progress = 0; m_config.beginGroup(activity); progress = m_config.value(PROGRESS_KEY, 0).toInt(); m_config.endGroup(); qDebug() << "loaded progress for activity" << activity << ":" << progress; return progress; } void ApplicationSettings::saveActivityProgress(const QString &activity, int progress) { updateValueInConfig(activity, PROGRESS_KEY, progress); } bool ApplicationSettings::useExternalWordset() { return !m_wordset.isEmpty() && DownloadManager::getInstance()->isDataRegistered("words"); } QObject *ApplicationSettings::applicationSettingsProvider(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) Q_UNUSED(scriptEngine) ApplicationSettings* appSettings = getInstance(); return appSettings; } diff --git a/src/core/ApplicationSettings.h b/src/core/ApplicationSettings.h index fae11eabd..64e95844a 100644 --- a/src/core/ApplicationSettings.h +++ b/src/core/ApplicationSettings.h @@ -1,666 +1,680 @@ /* GCompris - ApplicationSettings.h * * Copyright (C) 2014 Johnny Jazeix * * Authors: * Johnny Jazeix * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #ifndef APPLICATIONSETTINGS_H #define APPLICATIONSETTINGS_H #include #include #include #include #include #include #include #define GC_DEFAULT_LOCALE "system" /** * @class ApplicationSettings * @short Singleton that contains GCompris' persistent settings. * @ingroup infrastructure * * Settings are persisted using QSettings, which stores them in platform * specific locations. * * The settings are subdivided in different groups of settings. * * [General] settings are mostly changeable by users in the DialogConfig * dialog. * * [Admin] and [Internal] settings are not changeable by the * user and used for internal purposes. Should only be changed if you really know * what you are doing. * * The [Favorite] group is auto-generated from the favorite activities * selected by a user. * * Besides these global settings there is one group for each activity that * stores persistent settings. * * Settings defaults are defined in the source code. * * @sa DialogActivityConfig */ class ApplicationSettings : public QObject { Q_OBJECT /* General group */ /** * Whether to show locked activities. * False if in Demo mode, true otherwise. */ Q_PROPERTY(bool showLockedActivities READ showLockedActivities WRITE setShowLockedActivities NOTIFY showLockedActivitiesChanged) /** * Whether audio voices/speech should be enabled. */ Q_PROPERTY(bool isAudioVoicesEnabled READ isAudioVoicesEnabled WRITE setIsAudioVoicesEnabled NOTIFY audioVoicesEnabledChanged) /** * Whether audio effects should be enabled. */ Q_PROPERTY(bool isAudioEffectsEnabled READ isAudioEffectsEnabled WRITE setIsAudioEffectsEnabled NOTIFY audioEffectsEnabledChanged) /** Whether background music should be enabled. */ Q_PROPERTY(bool isBackgroundMusicEnabled READ isBackgroundMusicEnabled WRITE setIsBackgroundMusicEnabled NOTIFY backgroundMusicEnabledChanged) /** * Whether GCompris should run in fullscreen mode. */ Q_PROPERTY(bool isFullscreen READ isFullscreen WRITE setFullscreen NOTIFY fullscreenChanged) /** * Window Height on Application's Startup */ Q_PROPERTY(quint32 previousHeight READ previousHeight WRITE setPreviousHeight NOTIFY previousHeightChanged) /** * Window Width on Application's Startup */ Q_PROPERTY(quint32 previousWidth READ previousWidth WRITE setPreviousWidth NOTIFY previousWidthChanged) /** * Whether on-screen keyboard should be enabled per default in activities * that use it. */ Q_PROPERTY(bool isVirtualKeyboard READ isVirtualKeyboard WRITE setVirtualKeyboard NOTIFY virtualKeyboardChanged) /** * Locale string for currently active language. */ Q_PROPERTY(QString locale READ locale WRITE setLocale NOTIFY localeChanged) /** * Currently selected font. */ Q_PROPERTY(QString font READ font WRITE setFont NOTIFY fontChanged) /** * Whether currently active font is a shipped font (or a system font). * * Updated automatically. * @sa font */ Q_PROPERTY(bool isEmbeddedFont READ isEmbeddedFont WRITE setIsEmbeddedFont NOTIFY embeddedFontChanged) /** * Font Capitalization * * Force all texts to be rendered in UpperCase, LowerCase or MixedCase (default) * @sa font */ Q_PROPERTY(quint32 fontCapitalization READ fontCapitalization WRITE setFontCapitalization NOTIFY fontCapitalizationChanged) /** * Font letter spacing * * Change the letter spacing of all the texts * @sa font */ Q_PROPERTY(qreal fontLetterSpacing READ fontLetterSpacing WRITE setFontLetterSpacing NOTIFY fontLetterSpacingChanged) /** * Minimum allowed value for font spacing letter. * * Constant value: +0.0 */ Q_PROPERTY(qreal fontLetterSpacingMin READ fontLetterSpacingMin CONSTANT) /** * Maximum allowed value for font spacing letter. * * Constant value: +8.0 */ Q_PROPERTY(qreal fontLetterSpacingMax READ fontLetterSpacingMax CONSTANT) /** * Whether downloads/updates of resource files should be done automatically, * without user-interaction. * * Note, that on Android GCompris currently can't distinguish Wifi * from mobile data connections (cf. Qt ticket #30394). */ Q_PROPERTY(bool isAutomaticDownloadsEnabled READ isAutomaticDownloadsEnabled WRITE setIsAutomaticDownloadsEnabled NOTIFY automaticDownloadsEnabledChanged) /** * Minimum value for difficulty level filter. */ Q_PROPERTY(quint32 filterLevelMin READ filterLevelMin WRITE setFilterLevelMin NOTIFY filterLevelMinChanged) /** * Maximum value for difficulty level filter. */ Q_PROPERTY(quint32 filterLevelMax READ filterLevelMax WRITE setFilterLevelMax NOTIFY filterLevelMaxChanged) /** * Whether in demo mode. */ Q_PROPERTY(bool isDemoMode READ isDemoMode WRITE setDemoMode NOTIFY demoModeChanged) /** * Activation code key. */ Q_PROPERTY(QString codeKey READ codeKey WRITE setCodeKey NOTIFY codeKeyChanged) /** * Activation mode. */ Q_PROPERTY(quint32 activationMode READ activationMode CONSTANT) /** * Whether kiosk mode is currently active. */ Q_PROPERTY(bool isKioskMode READ isKioskMode WRITE setKioskMode NOTIFY kioskModeChanged) /** * Whether the section selection row is visible in the menu view. */ Q_PROPERTY(bool sectionVisible READ sectionVisible WRITE setSectionVisible NOTIFY sectionVisibleChanged) /** * The name of the default wordset to use. If empty then the internal sample wordset is used. */ Q_PROPERTY(QString wordset READ wordset WRITE setWordset NOTIFY wordsetChanged) /** * Current base font-size used for font scaling. * * This setting is the basis for application-wide font-scaling. A value * of 0 means to use the font-size as set by the application. Other values * between @ref baseFontSizeMin and @ref baseFontSizeMax enforce * font-scaling. * * @sa GCText.fontSize baseFontSizeMin baseFontSizeMax */ Q_PROPERTY(int baseFontSize READ baseFontSize WRITE setBaseFontSize NOTIFY baseFontSizeChanged) /** * Minimum allowed value for font-scaling. * * Constant value: -7 */ Q_PROPERTY(int baseFontSizeMin READ baseFontSizeMin CONSTANT) /** * Maximum allowed value for font-scaling. * * Constant value: +7 */ Q_PROPERTY(int baseFontSizeMax READ baseFontSizeMax CONSTANT) // admin group /** * Base-URL for resource downloads. * * @sa DownloadManager */ Q_PROPERTY(QString downloadServerUrl READ downloadServerUrl WRITE setDownloadServerUrl NOTIFY downloadServerUrlChanged) /** * Path where resources are downloaded and stored. * * @sa DownloadManager */ Q_PROPERTY(QString cachePath READ cachePath WRITE setCachePath NOTIFY cachePathChanged) /** * Return the platform specific path for storing data shared between apps * * On Android: /storage/emulated/0/GCompris (>= Android 4.2), * /storage/sdcard0/GCompris (< Android 4.2) * On Linux: $HOME/local/share/GCompris */ Q_PROPERTY(QString userDataPath READ userDataPath WRITE setUserDataPath NOTIFY userDataPathChanged) /** * Define the renderer used. * Either openGL or software renderer (only for Qt >= 5.8) */ Q_PROPERTY(QString renderer READ renderer WRITE setRenderer NOTIFY rendererChanged) + + /** + * Stores the filtered background music playlist by the user. + */ + Q_PROPERTY(QStringList filteredBackgroundMusic READ filteredBackgroundMusic WRITE setFilteredBackgroundMusic NOTIFY filteredBackgroundMusicChanged) // internal group Q_PROPERTY(quint32 exeCount READ exeCount WRITE setExeCount NOTIFY exeCountChanged) // keep last version ran. If different than ApplicationInfo.GCVersionCode(), it means a new version is running Q_PROPERTY(int lastGCVersionRan READ lastGCVersionRan WRITE setLastGCVersionRan NOTIFY lastGCVersionRanChanged) // no group Q_PROPERTY(bool isBarHidden READ isBarHidden WRITE setBarHidden NOTIFY barHiddenChanged) public: /// @cond INTERNAL_DOCS explicit ApplicationSettings(const QString &configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/gcompris/" + GCOMPRIS_APPLICATION_NAME + ".conf", QObject *parent = 0); virtual ~ApplicationSettings(); // It is not recommended to create a singleton of Qml Singleton registered // object but we could not found a better way to let us access ApplicationInfo // on the C++ side. All our test shows that it works. static ApplicationSettings *getInstance() { if(!m_instance) { m_instance = new ApplicationSettings(); } return m_instance; } static QObject *applicationSettingsProvider(QQmlEngine *engine, QJSEngine *scriptEngine); bool showLockedActivities() const { return m_showLockedActivities; } void setShowLockedActivities(const bool newMode) { m_showLockedActivities = newMode; emit showLockedActivitiesChanged(); } bool isAudioVoicesEnabled() const { return m_isAudioVoicesEnabled; } void setIsAudioVoicesEnabled(const bool newMode) { m_isAudioVoicesEnabled = newMode; emit audioVoicesEnabledChanged(); } bool isAudioEffectsEnabled() const { return m_isAudioEffectsEnabled; } void setIsAudioEffectsEnabled(const bool newMode) { m_isAudioEffectsEnabled = newMode; emit audioEffectsEnabledChanged(); } bool isBackgroundMusicEnabled() const { return m_isBackgroundMusicEnabled; } void setIsBackgroundMusicEnabled(const bool newMode) { m_isBackgroundMusicEnabled = newMode; emit backgroundMusicEnabledChanged(); } bool isFullscreen() const { return m_isFullscreen; } void setFullscreen(const bool newMode) { if(m_isFullscreen != newMode) { m_isFullscreen = newMode; emit fullscreenChanged(); } } quint32 previousHeight() const { return m_previousHeight; } void setPreviousHeight(quint32 height) { if(m_previousHeight != height) { m_previousHeight = height; emit previousHeightChanged(); } } quint32 previousWidth() const { return m_previousWidth; } void setPreviousWidth(quint32 width) { if(m_previousWidth != width) { m_previousWidth = width; emit previousWidthChanged(); } } bool isVirtualKeyboard() const { return m_isVirtualKeyboard; } void setVirtualKeyboard(const bool newMode) { m_isVirtualKeyboard = newMode; emit virtualKeyboardChanged(); } QString locale() const { return m_locale; } void setLocale(const QString &newLocale) { m_locale = newLocale; emit localeChanged(); } QString font() const { return m_font; } void setFont(const QString &newFont) { m_font = newFont; emit fontChanged(); } bool isEmbeddedFont() const { return m_isEmbeddedFont; } void setIsEmbeddedFont(const bool newIsEmbeddedFont) { m_isEmbeddedFont = newIsEmbeddedFont; emit embeddedFontChanged(); } quint32 fontCapitalization() const { return m_fontCapitalization; } void setFontCapitalization(quint32 newFontCapitalization) { m_fontCapitalization = newFontCapitalization; emit fontCapitalizationChanged(); } qreal fontLetterSpacing() const { return m_fontLetterSpacing; } void setFontLetterSpacing(qreal newFontLetterSpacing) { m_fontLetterSpacing = newFontLetterSpacing; emit fontLetterSpacingChanged(); } qreal fontLetterSpacingMin() const { return m_fontLetterSpacingMin; } qreal fontLetterSpacingMax() const { return m_fontLetterSpacingMax; } bool isAutomaticDownloadsEnabled() const; void setIsAutomaticDownloadsEnabled(const bool newIsAutomaticDownloadsEnabled); quint32 filterLevelMin() const { return m_filterLevelMin; } void setFilterLevelMin(const quint32 newFilterLevelMin) { m_filterLevelMin = newFilterLevelMin; emit filterLevelMinChanged(); } quint32 filterLevelMax() const { return m_filterLevelMax; } void setFilterLevelMax(const quint32 newFilterLevelMax) { m_filterLevelMax = newFilterLevelMax; emit filterLevelMaxChanged(); } bool isDemoMode() const { return m_isDemoMode; } void setDemoMode(const bool newMode); QString codeKey() const { return m_codeKey; } void setCodeKey(const QString &newCodeKey) { m_codeKey = newCodeKey; emit codeKeyChanged(); } /** * @brief activationMode * @return 0: no, 1: inapp, 2: internal */ quint32 activationMode() const { return m_activationMode; } bool isKioskMode() const { return m_isKioskMode; } void setKioskMode(const bool newMode) { m_isKioskMode = newMode; emit kioskModeChanged(); } /** * Check validity of the activation code * @param code An activation code to check * @returns 0 if the code is not valid or we don't know yet * 1 if the code is valid but out of date * 2 if the code is valid and under 2 years */ Q_INVOKABLE uint checkActivationCode(const QString &code); /** * Check Payment API * Call a payment system to sync our demoMode state with it */ void checkPayment(); // Called by the payment system void bought(const bool isBought) { if(m_isDemoMode != !isBought) { m_isDemoMode = !isBought; emit demoModeChanged(); } } bool sectionVisible() const { return m_sectionVisible; } void setSectionVisible(const bool newMode) { qDebug() << "c++ setSectionVisible=" << newMode; m_sectionVisible = newMode; emit sectionVisibleChanged(); } QString wordset() const { return m_wordset; } void setWordset(const QString &newWordset) { m_wordset = newWordset; emit wordsetChanged(); } QString downloadServerUrl() const { return m_downloadServerUrl; } void setDownloadServerUrl(const QString &newDownloadServerUrl) { m_downloadServerUrl = newDownloadServerUrl; emit downloadServerUrlChanged(); } QString cachePath() const { return m_cachePath; } void setCachePath(const QString &newCachePath) { m_cachePath = newCachePath; emit cachePathChanged(); } QString userDataPath() const { return m_userDataPath; } void setUserDataPath(const QString &newUserDataPath) { m_userDataPath = newUserDataPath; emit userDataPathChanged(); } quint32 exeCount() const { return m_exeCount; } void setExeCount(const quint32 newExeCount) { m_exeCount = newExeCount; emit exeCountChanged(); } bool isBarHidden() const { return m_isBarHidden; } void setBarHidden(const bool newBarHidden) { m_isBarHidden = newBarHidden; emit barHiddenChanged(); } int baseFontSize() const { return m_baseFontSize; } void setBaseFontSize(const int newBaseFontSize) { m_baseFontSize = qMax(qMin(newBaseFontSize, baseFontSizeMax()), baseFontSizeMin()); emit baseFontSizeChanged(); } int baseFontSizeMin() const { return m_baseFontSizeMin; } int baseFontSizeMax() const { return m_baseFontSizeMax; } int lastGCVersionRan() const { return m_lastGCVersionRan; } void setLastGCVersionRan(const int newLastGCVersionRan) { m_lastGCVersionRan = newLastGCVersionRan; emit lastGCVersionRanChanged(); } QString renderer() const { return m_renderer; } void setRenderer(const QString &newRenderer) { m_renderer = newRenderer; emit rendererChanged(); } + + QStringList filteredBackgroundMusic() const { return m_filteredBackgroundMusic; } + void setFilteredBackgroundMusic(const QStringList &newFilteredBackgroundMusic) { + m_filteredBackgroundMusic = newFilteredBackgroundMusic; + emit filteredBackgroundMusicChanged(); + } /** * Check if we use the external wordset for activity based on lang_api * @returns true if wordset is loaded * false if wordset is not loaded */ Q_INVOKABLE bool useExternalWordset(); protected slots: Q_INVOKABLE void notifyShowLockedActivitiesChanged(); Q_INVOKABLE void notifyAudioVoicesEnabledChanged(); Q_INVOKABLE void notifyAudioEffectsEnabledChanged(); Q_INVOKABLE void notifyBackgroundMusicEnabledChanged(); Q_INVOKABLE void notifyFullscreenChanged(); Q_INVOKABLE void notifyPreviousHeightChanged(); Q_INVOKABLE void notifyPreviousWidthChanged(); Q_INVOKABLE void notifyVirtualKeyboardChanged(); Q_INVOKABLE void notifyLocaleChanged(); Q_INVOKABLE void notifyFontChanged(); Q_INVOKABLE void notifyFontCapitalizationChanged(); Q_INVOKABLE void notifyFontLetterSpacingChanged(); Q_INVOKABLE void notifyEmbeddedFontChanged(); Q_INVOKABLE void notifyAutomaticDownloadsEnabledChanged(); Q_INVOKABLE void notifyFilterLevelMinChanged(); Q_INVOKABLE void notifyFilterLevelMaxChanged(); Q_INVOKABLE void notifyDemoModeChanged(); Q_INVOKABLE void notifyCodeKeyChanged(); Q_INVOKABLE void notifyKioskModeChanged(); Q_INVOKABLE void notifySectionVisibleChanged(); Q_INVOKABLE void notifyWordsetChanged(); + Q_INVOKABLE void notifyFilteredBackgroundMusicChanged(); Q_INVOKABLE void notifyDownloadServerUrlChanged(); Q_INVOKABLE void notifyCachePathChanged(); Q_INVOKABLE void notifyUserDataPathChanged(); Q_INVOKABLE void notifyExeCountChanged(); Q_INVOKABLE void notifyLastGCVersionRanChanged(); Q_INVOKABLE void notifyRendererChanged(); Q_INVOKABLE void notifyBarHiddenChanged(); public slots: Q_INVOKABLE bool isFavorite(const QString &activity); Q_INVOKABLE void setFavorite(const QString &activity, bool favorite); Q_INVOKABLE void saveBaseFontSize(); /// @endcond /** * Stores per-activity configuration @p data for @p activity. * * @param activity Name of the activity that wants to persist settings. * @param data Map of configuration data so save. */ Q_INVOKABLE void saveActivityConfiguration(const QString &activity, const QVariantMap &data); /** * Loads per-activity configuration data for @p activity. * * @param activity Name of the activity that wants to persist settings. * @returns Map of configuration items. */ Q_INVOKABLE QVariantMap loadActivityConfiguration(const QString &activity); /** * Loads per-activity progress using the default "progress" key. * * @param activity Name of the activity to load progress for. * @returns Last started level of the activity, 0 if none saved. */ Q_INVOKABLE int loadActivityProgress(const QString &activity); /** * Saves per-activity progress using the default "progress" key. * * @param activity Name of the activity that wants to persist settings. * @param progress Last started level to save as progress value. */ Q_INVOKABLE void saveActivityProgress(const QString &activity, int progress); signals: void showLockedActivitiesChanged(); void audioVoicesEnabledChanged(); void audioEffectsEnabledChanged(); void backgroundMusicEnabledChanged(); void fullscreenChanged(); void previousHeightChanged(); void previousWidthChanged(); void virtualKeyboardChanged(); void localeChanged(); void fontChanged(); void fontCapitalizationChanged(); void fontLetterSpacingChanged(); void embeddedFontChanged(); void automaticDownloadsEnabledChanged(); void filterLevelMinChanged(); void filterLevelMaxChanged(); void demoModeChanged(); void codeKeyChanged(); void kioskModeChanged(); void sectionVisibleChanged(); void wordsetChanged(); void baseFontSizeChanged(); + void filteredBackgroundMusicChanged(); void downloadServerUrlChanged(); void cachePathChanged(); void userDataPathChanged(); void exeCountChanged(); void lastGCVersionRanChanged(); void rendererChanged(); void barHiddenChanged(); protected: static ApplicationSettings *m_instance; private: // Update in configuration the couple {key, value} in the group. template void updateValueInConfig(const QString& group, const QString& key, const T& value); bool m_showLockedActivities; bool m_isAudioVoicesEnabled; bool m_isAudioEffectsEnabled; bool m_isBackgroundMusicEnabled; bool m_isFullscreen; quint32 m_previousHeight; quint32 m_previousWidth; bool m_isVirtualKeyboard; bool m_isAutomaticDownloadsEnabled; bool m_isEmbeddedFont; quint32 m_fontCapitalization; qreal m_fontLetterSpacing; quint32 m_filterLevelMin; quint32 m_filterLevelMax; bool m_defaultCursor; bool m_noCursor; QString m_locale; QString m_font; bool m_isDemoMode; QString m_codeKey; quint32 m_activationMode; bool m_isKioskMode; bool m_sectionVisible; QString m_wordset; + QStringList m_filteredBackgroundMusic; int m_baseFontSize; const int m_baseFontSizeMin; const int m_baseFontSizeMax; const qreal m_fontLetterSpacingMin; const qreal m_fontLetterSpacingMax; QString m_downloadServerUrl; QString m_cachePath; QString m_userDataPath; quint32 m_exeCount; int m_lastGCVersionRan; QString m_renderer; bool m_isBarHidden; QSettings m_config; }; #endif // APPLICATIONSETTINGS_H diff --git a/src/core/GCAudio.qml b/src/core/GCAudio.qml index eb12ae74f..21e7148d8 100644 --- a/src/core/GCAudio.qml +++ b/src/core/GCAudio.qml @@ -1,250 +1,250 @@ /* GCompris - GCAudio.qml * * Copyright (C) 2014 Johnny Jazeix * * Authors: * Johnny Jazeix * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import QtQuick 2.6 import QtMultimedia 5.0 import GCompris 1.0 /** * A QML component for audio playback. * @ingroup components * * Wrapper component around QtQuick's Audio element, handling all audio * playback in GCompris uniformly. * * It maintains a queue of audio-sources (@ref files) that are played back * sequentially, to which the user can enqueue files that should be scheduled * for playback. * * To make sure an audio voice will be localized, replace the locale part * of the file by '$LOCALE'. * * To makes sure that all audio sources are normalized with respect to * ApplicationInfo.CompressedAudio replace the 'ogg' part of the file by * '$CA'. * * @inherit QtQuick.Item */ Item { id: gcaudio /** * type:bool * Whether audio should be muted. */ property bool muted /** * type:url * URL to the audio source to be played back. */ property alias source: audio.source /** * type: positive real number less than 1 * Determines intensity of the audio. */ property alias volume: audio.volume /** * type:bool * Whether the audio element contains audio. */ property bool hasAudio: audio.hasAudio /** * type:string * Detailed error message in case of playback errors. */ property alias errorString: audio.errorString /** * Trigger this signal externally to play the next audio in the "files". This, in turn, stops the currently playing audio and check the necessary * conditions (see onStopped signal in "audio" element) and decides what needs to be done for the next audio. */ signal nextAudio() - onNextAudio: audio.stop() + onNextAudio: stop() /** * type:var * Current playback state. * * Possible values taken from Audio.status */ property var playbackState: (audio.error == Audio.NoError) ? audio.playbackState : Audio.StoppedState; /** * type:list * Playback queue. */ property var files: [] /** * Emitted in case of error. */ signal error /** * Emitted when playback of all scheduled audio sources has finished. */ signal done //Pauses the currently playing audio function pause() { if(playbackState === Audio.PlayingState) audio.pause() } //Resumes the current audio if it had been paused function resume() { if(playbackState === Audio.PausedState || playbackState === Audio.StoppedState) audio.play() } /** * Plays back the audio resource @p file. * * @param type:string file [optional] URL to an audio source. * @returns @c true if playback has been started, @c false if file does not * exist or audio is muted */ function play(file) { if(!fileId.exists(file) || muted) return false if(file) { // Setting the source to "" on Linux fix a case where the sound is no more played if you play twice the same sound in a row source = "" source = file } if(!muted) { audio.play() } return true } /** * Stops audio playback. */ function stop() { if(audio.playbackState != Audio.StoppedState) audio.stop() } /** * Schedules a @p file for audio playback. * * If there is no playback currently running, the new source will be * played back immediately. Otherwise it is appended to the file queue of * sources. * * @param type:string file File to the audio file to be played back. * @returns @c true upon success, or @c false if @p file does not exist or * audio is muted */ function append(file) { if(!fileId.exists(file) || muted) return false if(audio.playbackState !== Audio.PlayingState || audio.status === Audio.EndOfMedia || audio.status === Audio.NoMedia || audio.status === Audio.InvalidMedia) { // Setting the source to "" on Linux fix a case where the sound is no more played source = "" source = file files.push(file) silenceTimer.start() } else { files.push(file) } return true } /** * Adds a pause of the given duration in ms before playing of the next file. * * @param type:int duration_ms Pause in milliseconds. */ function silence(duration_ms) { silenceTimer.interval = duration_ms } /** * Flushes the list of scheduled files. * @sa files */ function clearQueue() { while(files.length > 0) { files.pop(); } } /// @cond INTERNAL_DOCS function _playNextFile() { var nextFile = files.shift() if(nextFile.length === 0) { audio.source = "" gcaudio.done() } else { audio.source = "" audio.source = nextFile if(!muted) audio.play() } } Audio { id: audio muted: gcaudio.muted onError: { // This file cannot be played, remove it from the source asap source = "" if(files.length) silenceTimer.start() else gcaudio.error() } onStopped: { if(files.length) silenceTimer.start() else gcaudio.done() } } Timer { id: silenceTimer repeat: false onTriggered: { interval = 0 _playNextFile() } } File { id: fileId } /// @endcond } diff --git a/src/core/main.qml b/src/core/main.qml index 34f1c35d3..8ce8a9770 100644 --- a/src/core/main.qml +++ b/src/core/main.qml @@ -1,461 +1,464 @@ /* GCompris - main.qml * * Copyright (C) 2014 Bruno Coudoin * * Authors: * Bruno Coudoin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import QtQuick 2.6 import QtQuick.Controls 1.5 import QtQuick.Window 2.2 import QtQml 2.2 import GCompris 1.0 import "qrc:/gcompris/src/core/core.js" as Core /** * GCompris' main QML file defining the top level window. * @ingroup infrastructure * * Handles application start (Component.onCompleted) and shutdown (onClosing) * on the QML layer. * * Contains the central GCAudio objects audio effects and audio voices. * * Contains the top level StackView presenting and animating GCompris' * full screen views. * * @sa BarButton, BarEnumContent * @inherit QtQuick.Window */ Window { id: main // Start in window mode at full screen size width: ApplicationSettings.previousWidth height: ApplicationSettings.previousHeight minimumWidth: 400 * ApplicationInfo.ratio minimumHeight: 400 * ApplicationInfo.ratio title: "GCompris" /// @cond INTERNAL_DOCS property var applicationState: Qt.application.state property var rccBackgroundMusic: ApplicationInfo.getBackgroundMusicFromRcc() + property var filteredBackgroundMusic: ApplicationSettings.filteredBackgroundMusic property alias backgroundMusic: backgroundMusic /** * type: bool * It tells whether a musical activity is running. * * 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. */ property bool isMusicalActivityRunning: false /** * When a musical activity is started, the backgroundMusic pauses. * * When returning back from the musical activity to menu, backgroundMusic resumes. */ onIsMusicalActivityRunningChanged: { if(isMusicalActivityRunning) { backgroundMusic.pause() } else { backgroundMusic.resume() } } onApplicationStateChanged: { if (ApplicationInfo.isMobile && applicationState != Qt.ApplicationActive) { audioVoices.stop(); audioEffects.stop(); } } - + onClosing: Core.quit(main) GCAudio { id: audioVoices muted: !ApplicationSettings.isAudioVoicesEnabled Timer { id: delayedWelcomeTimer interval: 10000 /* Make sure, that playing welcome.ogg if delayed * because of not yet registered voices, will only * happen max 10sec after startup */ repeat: false onTriggered: { DownloadManager.voicesRegistered.disconnect(playWelcome); } function playWelcome() { audioVoices.append(ApplicationInfo.getAudioFilePath("voices-$CA/$LOCALE/misc/welcome.$CA")); } } Component.onCompleted: { if(ApplicationSettings.isAudioEffectsEnabled) audioVoices.append(ApplicationInfo.getAudioFilePath("qrc:/gcompris/src/core/resource/intro.$CA")) if (DownloadManager.areVoicesRegistered()) delayedWelcomeTimer.playWelcome(); else { DownloadManager.voicesRegistered.connect( delayedWelcomeTimer.playWelcome); delayedWelcomeTimer.start(); } } } GCSfx { id: audioEffects muted: !ApplicationSettings.isAudioEffectsEnabled && !main.isMusicalActivityRunning } GCAudio { id: backgroundMusic muted: !ApplicationSettings.isBackgroundMusicEnabled readonly property real backgroundMusicVolume: 0.5 volume: backgroundMusicVolume onMutedChanged: { if(!hasAudio && !delayedbackgroundMusic.running && !files.length) { delayedbackgroundMusic.playBackgroundMusic() } } onDone: delayedbackgroundMusic.playBackgroundMusic() Timer { id: delayedbackgroundMusic interval: (ApplicationSettings.isAudioVoicesEnabled && !ApplicationSettings.isAudioEffectsEnabled) ? 2000 : 20000 repeat: false onTriggered: { delayedbackgroundMusic.playBackgroundMusic(); } function playBackgroundMusic() { rccBackgroundMusic = ApplicationInfo.getBackgroundMusicFromRcc() - Core.shuffle(rccBackgroundMusic) - for(var i = 0; i < rccBackgroundMusic.length; i++) - backgroundMusic.append(ApplicationInfo.getAudioFilePath("backgroundMusic/" + rccBackgroundMusic[i])) - if(!main.isBackgroundMusicEnabledInActivity) + + for(var i = 0; i < filteredBackgroundMusic.length; i++) { + backgroundMusic.append(ApplicationInfo.getAudioFilePath("backgroundMusic/" + filteredBackgroundMusic[i])) + } + + if(main.isMusicalActivityRunning) backgroundMusic.pause() } } Component.onCompleted: { if(ApplicationSettings.isBackgroundMusicEnabled && DownloadManager.haveLocalResource(DownloadManager.getBackgroundMusicResources())) { if(!ApplicationSettings.isAudioEffectsEnabled && !ApplicationSettings.isAudioVoicesEnabled) { delayedbackgroundMusic.playBackgroundMusic() } else { delayedbackgroundMusic.start() } } else { DownloadManager.backgroundMusicRegistered.connect(delayedbackgroundMusic.playBackgroundMusic) } } } function playIntroVoice(name) { name = name.split("/")[0] audioVoices.play(ApplicationInfo.getAudioFilePath("voices-$CA/$LOCALE/intro/" + name + ".$CA")) } function checkWordset() { var wordset = ApplicationSettings.wordset if(wordset == '') // Maybe the wordset has been bundled or copied manually // we have to register it if we find it. wordset = 'data2/words/words.rcc' // check for words.rcc: if (DownloadManager.isDataRegistered("words")) { // words.rcc is already registered -> nothing to do } else if(DownloadManager.haveLocalResource(wordset)) { // words.rcc is there -> register old file first // then try to update in the background if(DownloadManager.updateResource(wordset)) { ApplicationSettings.wordset = wordset } } else if(ApplicationSettings.wordset) { // Only if wordset specified // words.rcc has not been downloaded yet -> ask for download Core.showMessageDialog( main, qsTr("The images for several activities are not yet installed. " + "Do you want to download them now?"), qsTr("Yes"), function() { if (DownloadManager.downloadResource(wordset)) var downloadDialog = Core.showDownloadDialog(pageView.currentItem, {}); }, qsTr("No"), null, function() { pageView.currentItem.focus = true } ); } } function checkBackgroundMusic() { var music = DownloadManager.getBackgroundMusicResources() if(rccBackgroundMusic == '') { rccBackgroundMusic = ApplicationInfo.getBackgroundMusicFromRcc() } if(music == '') { music = DownloadManager.getBackgroundMusicResources() } // We have local music but it is not yet registered else if(!DownloadManager.isDataRegistered("backgroundMusic") && DownloadManager.haveLocalResource(music)) { // We have music and automatic download is enabled. Download the music and register it if(DownloadManager.updateResource(music) && DownloadManager.downloadIsRunning()) { DownloadManager.registerResource(music) rccBackgroundMusic = Core.shuffle(ApplicationInfo.getBackgroundMusicFromRcc()) } else { rccBackgroundMusic = ApplicationInfo.getBackgroundMusicFromRcc() } } else if(!DownloadManager.haveLocalResource(music)) { Core.showMessageDialog( main, qsTr("The background music is not yet downloaded. ") + qsTr("Do you want to download it now?"), qsTr("Yes"), function() { if(DownloadManager.downloadResource(DownloadManager.getBackgroundMusicResources())) { var downloadDialog = Core.showDownloadDialog(pageView.currentItem, {}); } }, qsTr("No"), null, function() { pageView.currentItem.focus = true } ); } } ChangeLog { id: changelog } Component.onCompleted: { console.log("enter main.qml (run #" + ApplicationSettings.exeCount + ", ratio=" + ApplicationInfo.ratio + ", fontRatio=" + ApplicationInfo.fontRatio + ", dpi=" + Math.round(Screen.pixelDensity*25.4) + ", userDataPath=" + ApplicationSettings.userDataPath + ")"); if (ApplicationSettings.exeCount === 1 && !ApplicationSettings.isKioskMode && ApplicationInfo.isDownloadAllowed) { // first run var dialog; dialog = Core.showMessageDialog( main, qsTr("Welcome to GCompris!") + '\n' + qsTr("You are running GCompris for the first time.") + '\n' + 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.") + "\n" + qsTr("Have Fun!") + "\n" + qsTr("Your current language is %1 (%2).") .arg(Qt.locale(ApplicationInfo.getVoicesLocale(ApplicationSettings.locale)).nativeLanguageName) .arg(ApplicationInfo.getVoicesLocale(ApplicationSettings.locale)) + "\n" + qsTr("Do you want to download the corresponding sound files now?"), qsTr("Yes"), function() { if (DownloadManager.downloadResource( DownloadManager.getVoicesResourceForLocale(ApplicationSettings.locale))) var downloadDialog = Core.showDownloadDialog(pageView.currentItem, {}); }, qsTr("No"), null, function() { pageView.currentItem.focus = true checkWordset() checkBackgroundMusic() } ); } else { // Register voices-resources for current locale, updates/downloads only if // not prohibited by the settings if (!DownloadManager.areVoicesRegistered()) { DownloadManager.updateResource( DownloadManager.getVoicesResourceForLocale(ApplicationSettings.locale)); } checkWordset() checkBackgroundMusic() if(changelog.isNewerVersion(ApplicationSettings.lastGCVersionRan, ApplicationInfo.GCVersionCode)) { // display log between ApplicationSettings.lastGCVersionRan and ApplicationInfo.GCVersionCode Core.showMessageDialog( main, qsTr("GCompris has been updated! Here are the new changes:
") + changelog.getLogBetween(ApplicationSettings.lastGCVersionRan, ApplicationInfo.GCVersionCode), "", null, "", null, function() { pageView.currentItem.focus = true } ); // Store new version ApplicationSettings.lastGCVersionRan = ApplicationInfo.GCVersionCode; } } } Loading { id: loading } StackView { id: pageView anchors.fill: parent initialItem: { "item": "qrc:/gcompris/src/activities/" + ActivityInfoTree.rootMenu.name, "properties": { 'audioVoices': audioVoices, 'audioEffects': audioEffects, 'loading': loading, 'backgroundMusic': backgroundMusic } } focus: true delegate: StackViewDelegate { id: root function getTransition(properties) { audioVoices.clearQueue() audioVoices.stop() if(!properties.exitItem.isDialog && // if coming from menu and !properties.enterItem.isDialog) // going into an activity then playIntroVoice(properties.enterItem.activityInfo.name); // play intro if (!properties.exitItem.isDialog || // if coming from menu or properties.enterItem.alwaysStart) // start signal enforced (for special case like transition from config-dialog to editor) properties.enterItem.start(); if(properties.name === "pushTransition") { if(properties.enterItem.isDialog) { return pushVTransition } else { if(properties.enterItem.isMusicalActivity) main.isMusicalActivityRunning = true return pushHTransition } } else { if(properties.exitItem.isDialog) { return popVTransition } else { main.isMusicalActivityRunning = false return popHTransition } } } function transitionFinished(properties) { properties.exitItem.opacity = 1 if(!properties.enterItem.isDialog) { properties.exitItem.stop() } } property Component pushHTransition: StackViewTransition { PropertyAnimation { target: enterItem property: "x" from: target.width to: 0 duration: 500 easing.type: Easing.OutSine } PropertyAnimation { target: exitItem property: "x" from: 0 to: -target.width duration: 500 easing.type: Easing.OutSine } } property Component popHTransition: StackViewTransition { PropertyAnimation { target: enterItem property: "x" from: -target.width to: 0 duration: 500 easing.type: Easing.OutSine } PropertyAnimation { target: exitItem property: "x" from: 0 to: target.width duration: 500 easing.type: Easing.OutSine } } property Component pushVTransition: StackViewTransition { PropertyAnimation { target: enterItem property: "y" from: -target.height to: 0 duration: 500 easing.type: Easing.OutSine } PropertyAnimation { target: exitItem property: "y" from: 0 to: target.height duration: 500 easing.type: Easing.OutSine } } property Component popVTransition: StackViewTransition { PropertyAnimation { target: enterItem property: "y" from: target.height to: 0 duration: 500 easing.type: Easing.OutSine } PropertyAnimation { target: exitItem property: "y" from: 0 to: -target.height duration: 500 easing.type: Easing.OutSine } } property Component replaceTransition: pushHTransition } } /// @endcond }