diff --git a/src/activities/menu/Menu.qml b/src/activities/menu/Menu.qml index 2801d6be2..3f1d64b26 100644 --- a/src/activities/menu/Menu.qml +++ b/src/activities/menu/Menu.qml @@ -1,913 +1,915 @@ /* 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 property string clickMode: "play" 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 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 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: searchTextField.height delegate: Button { id: button style: GCButtonStyle { selected: currentCategory === button.category } 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() } } 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 } } } } GridView { id: activitiesGrid anchors { top: { if(searchBar.visible) return searchBar.bottom else return categoriesGrid.bottom } bottom: bar.top left: horizontal ? parent.left : section.right margins: 4 } 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 enabled: clickMode === "play" || dialogChooseLevel.hasConfigOrDataset 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 opacity: delegateItem.enabled ? 1 : 0.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 } } DialogChooseLevel { id: dialogChooseLevel displayDatasetAtStart: hasDataset currentActivity: ActivityInfoTree.menuTree[index] + inMenu: true + onClose: { home() } onSaveData: { currentLevel = dialogChooseLevel.chosenLevel ApplicationSettings.setCurrentLevel(name, currentLevel) } onStartActivity: { clickMode = "play" // immediately pop the Dialog to load the activity // if we don't do it immediately the page is busy // and it does not load the activity pageView.pop({immediate: true}) selectCurrentItem() } } function selectCurrentItem() { if(pageView.busy || !delegateItem.enabled) return if(clickMode == "play") { particles.burst(50) ActivityInfoTree.currentActivity = ActivityInfoTree.menuTree[index] activityLoader.setSource("qrc:/gcompris/src/activities/" + ActivityInfoTree.currentActivity.name, { 'menu': activity, 'activityInfo': ActivityInfoTree.currentActivity, 'levelFolder': currentLevel }) if (activityLoader.status == Loader.Ready) loadActivity() } else { displayDialog(dialogChooseLevel); } } } 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: searchTextField.height 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 visible: activity.currentTag === "search" textColor: "black" font.pointSize: 16 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); } } Rectangle { id: activityConfigTextBar width: horizontal ? parent.width/2 : parent.width - (section.width+10) height: activitySettingsLabel.height visible: clickMode === "activityConfig" anchors { bottom: bar.top bottomMargin: height * 0.3 left: horizontal ? undefined : section.right } anchors.horizontalCenter: horizontal ? parent.horizontalCenter : undefined radius: 10 border.width: height * 0.05 border.color: "#8b66b2" color: "#eeeeee" GCText { id: activitySettingsLabel text: qsTr("Activity Settings") visible: parent.visible width: parent.width height: paintedHeight anchors.verticalCenter: parent.verticalCenter horizontalAlignment: Text.AlignHCenter color: "#232323" } } 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 | activityConfig | 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) } onActivityConfigClicked: { if(clickMode == "play") { clickMode = "activityConfig" } else { clickMode = "play" } } 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(); } onClose: { if(activity.currentTag != "search") { ActivityInfoTree.filterByTag(activity.currentTag, currentCategory) ActivityInfoTree.filterLockedActivities() ActivityInfoTree.filterEnabledActivities() } else ActivityInfoTree.filterBySearch(searchTextField.text); home() } } } } diff --git a/src/core/DialogChooseLevel.qml b/src/core/DialogChooseLevel.qml index fa6b2e247..d1d4f71d8 100644 --- a/src/core/DialogChooseLevel.qml +++ b/src/core/DialogChooseLevel.qml @@ -1,382 +1,399 @@ /* GCompris - DialogChooseLevel.qml * * Copyright (C) 2018 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 GCompris 1.0 /** * todo * @ingroup components * * todo * * @sa ApplicationSettings * @inherit QtQuick.Item */ Rectangle { id: dialogChooseLevel visible: false /* Public interface: */ /** * type:string * The name of the activity in case of per-activity config. * * Will be autogenerated unless set by the caller. */ property string activityName: currentActivity.name.split('/')[0] /// @cond INTERNAL_DOCS property bool isDialog: true /** * type:string * Title of the configuration dialog. */ readonly property string title: currentActivity ? qsTr("%1 settings").arg(currentActivity.title) : "" property var difficultiesModel: [] property QtObject currentActivity property string chosenLevel property var activityData onActivityDataChanged: loadData() /// @endcond /** * By default, we display configuration (this avoids to add code in each * activity to set it by default). */ property bool displayDatasetAtStart: !hasConfig /** * Emitted when the config dialog has been closed. */ signal close /** * Emitted when the config dialog has been started. */ signal start onStart: initialize() signal stop /** * Emitted when the settings are to be saved. * * The actual persisting of the settings in the settings file is done by * DialogActivityConfig. The activity has to take care to update its * internal state. */ signal saveData signal startActivity /** * Emitted when the config settings have been loaded. */ signal loadData property bool hasConfigOrDataset: hasConfig || hasDataset property bool hasConfig: activityConfigFile.exists("qrc:/gcompris/src/activities/"+activityName+"/ActivityConfig.qml") property bool hasDataset: currentActivity && currentActivity.levels.length !== 0 color: "#696da3" border.color: "black" border.width: 1 + property bool inMenu: false + + onVisibleChanged: { + if(visible) { + configLoader.initializePanel() + } + } + function initialize() { // dataset information chosenLevel = currentActivity.currentLevel difficultiesModel = [] if(currentActivity.levels.length == 0) { print("no levels to load for", activityName) } else { for(var level in currentActivity.levels) { objectiveLoader.dataFiles.push({"level": currentActivity.levels[level], "file": "qrc:/gcompris/src/activities/"+activityName+"/resource/"+currentActivity.levels[level]+"/Data.qml"}) } objectiveLoader.start() } // Defaults to config if in an activity else to dataset if in menu if(displayDatasetAtStart) { datasetVisibleButton.clicked() } else { optionsVisibleButton.clicked() } } Loader { id: objectiveLoader property var dataFiles: [] property var currentFile signal start signal stop onStart: { var file = dataFiles.shift() currentFile = file source = file.file.toString() } onLoaded: { difficultiesModel.push({"level": currentFile.level, "objective": item.objective, "difficulty": item.difficulty}) if(dataFiles.length != 0) { start() } else { stop() } } onStop: { difficultiesRepeater.model = difficultiesModel } } Row { visible: true spacing: 2 Item { width: 10; height: 1 } Column { spacing: 10 anchors.top: parent.top Item { width: 1; height: 10 } Rectangle { color: "#e6e6e6" radius: 6.0 width: dialogChooseLevel.width - 30 height: title.height * 1.2 border.color: "black" border.width: 2 Row { spacing: 2 padding: 8 Image { id: titleIcon anchors { left: parent.left top: parent.top margins: 4 * ApplicationInfo.ratio } } GCText { id: title text: dialogChooseLevel.title width: dialogChooseLevel.width - (30 + cancel.width) horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter color: "black" fontSize: 20 font.weight: Font.DemiBold wrapMode: Text.WordWrap } } } // Header buttons Row { id: datasetOptionsRow height: dialogChooseLevel.height / 12 width: parent.width spacing: parent.width / 4 anchors.leftMargin: parent.width / 8 Button { id: datasetVisibleButton text: qsTr("Dataset") enabled: hasDataset height: parent.height opacity: enabled ? 1 : 0 width: parent.width / 3 property bool selected: true style: GCButtonStyle { selected: datasetVisibleButton.selected } onClicked: { selected = true; } } Button { id: optionsVisibleButton text: qsTr("Options") enabled: hasConfig height: parent.height opacity: enabled ? 1 : 0 width: parent.width / 3 style: GCButtonStyle { selected: !datasetVisibleButton.selected } onClicked: { datasetVisibleButton.selected = false; } //showOptions() } } // "Dataset"/"Options" content Rectangle { color: "#e6e6e6" radius: 6.0 width: dialogChooseLevel.width - 30 height: dialogChooseLevel.height - (30 + title.height * 1.2) - saveAndPlayRow.height - datasetOptionsRow.height - 3 * parent.spacing border.color: "black" border.width: 2 Flickable { id: flick anchors.margins: 8 anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom flickableDirection: Flickable.VerticalFlick clip: true contentHeight: contentItem.childrenRect.height + 40 * ApplicationInfo.ratio Loader { id: configLoader visible: !datasetVisibleButton.selected active: optionsVisibleButton.enabled source: active ? "qrc:/gcompris/src/activities/"+activityName+"/ActivityConfig.qml" : "" - onItemChanged: { + // Load configuration at start of activity + // in the menu, it's done when the visibility property + // of the dialog changes + onItemChanged: if(!inMenu) { initializePanel(); } + + function initializePanel() { if(item) { - item.background = dialogChooseLevel - dialogChooseLevel.saveData.connect(save) + // only connect once the signal to save data + if(item.background !== dialogChooseLevel) { + item.background = dialogChooseLevel + dialogChooseLevel.saveData.connect(save) + } getInitialConfiguration() } } + function getInitialConfiguration() { activityData = Qt.binding(function() { return item.dataToSave }) if(item) { item.dataToSave = ApplicationSettings.loadActivityConfiguration(activityName) item.setDefaultValues() } } function save() { item.saveValues() ApplicationSettings.saveActivityConfiguration(activityName, item.dataToSave) } } Column { visible: datasetVisibleButton.selected spacing: 10 ExclusiveGroup { id: levelsGroup } Repeater { id: difficultiesRepeater delegate: Row { height: objective.height Image { id: difficultyIcon source: "qrc:/gcompris/src/core/resource/difficulty" + modelData.difficulty + ".svg"; sourceSize.height: objective.indicatorImageHeight anchors.verticalCenter: objective.verticalCenter } GCDialogCheckBox { id: objective width: dialogChooseLevel.width - 30 - difficultyIcon.width - 2 * flick.anchors.margins text: modelData.objective exclusiveGroup: levelsGroup checked: chosenLevel === modelData.level onClicked: chosenLevel = modelData.level } } } } } // The scroll buttons GCButtonScroll { anchors.right: parent.right anchors.rightMargin: 5 * ApplicationInfo.ratio anchors.bottom: flick.bottom anchors.bottomMargin: 5 * ApplicationInfo.ratio onUp: flick.flick(0, 1400) onDown: flick.flick(0, -1400) upVisible: flick.visibleArea.yPosition <= 0 ? false : true downVisible: flick.visibleArea.yPosition + flick.visibleArea.heightRatio >= 1 ? false : true } } // Footer buttons Row { id: saveAndPlayRow height: dialogChooseLevel.height / 12 width: parent.width spacing: parent.width / 16 Button { id: cancelButton text: qsTr("Cancel") height: parent.height width: parent.width / 4 property bool selected: true style: GCButtonStyle {} onClicked: dialogChooseLevel.close() } Button { id: saveButton text: qsTr("Save") height: parent.height width: parent.width / 4 property bool selected: true style: GCButtonStyle { } onClicked: { saveData(); close(); } } Button { id: saveAndStartButton text: qsTr("Save and start") height: parent.height width: parent.width / 3 style: GCButtonStyle { } onClicked: { saveData(); startActivity(); } } } Item { width: 1; height: 10 } } } // The cancel button GCButtonCancel { id: cancel onClose: { parent.close() } } File { id: activityConfigFile } } diff --git a/src/core/GCComboBox.qml b/src/core/GCComboBox.qml index 95ad8a67b..3d3523035 100644 --- a/src/core/GCComboBox.qml +++ b/src/core/GCComboBox.qml @@ -1,322 +1,322 @@ /* GCompris - GCComboBox.qml * * Copyright (C) 2015 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 GCompris 1.0 /** * A QML component unifying comboboxes in GCompris. * @ingroup components * * GCComboBox contains a combobox and a label. * When the combobox isn't active, it is displayed as a button containing the current value * and the combobox label (its description). * Once the button is clicked, the list of all available choices is displayed. * Also, above the list is the combobox label. * * Navigation can be done with keys and mouse/gestures. * As Qt comboboxes, you can either have a js Array or a Qml model as model. * GCComboBox should now be used wherever you'd use a QtQuick combobox. It has * been decided to implement comboboxes ourselves in GCompris because of * some integration problems on some OSes (native dialogs unavailable). */ Item { id: gccombobox focus: true width: parent.width height: flow.height /** * type:Item * Where the list containing all choices will be displayed. * Should be the dialogActivityConfig item if used on config. */ property Item background /** * type:int * Current index of the combobox. */ property int currentIndex: -1 /** * type:string * Current text displayed in the combobox when inactive. */ property string currentText /** * type:alias * Model for the list (user has to specify one). */ property alias model: gridview.model /** * type:string * Text besides the combobox, used to describe what the combobox is for. */ property string label /** * type:bool * Internal value. * If model is an js Array, we access data using modelData and [] else qml Model, we need to use model and get(). */ readonly property bool isModelArray: model.constructor === Array // start and stop trigs the animation signal start signal stop // emitted at stop animation end signal close onCurrentIndexChanged: { currentText = isModelArray ? model[currentIndex].text : (model && model.get(currentIndex) ? model.get(currentIndex).text : "") } /** * type:Flow * Combobox display when inactive: the button with current choice and its label besides. */ Flow { id: flow width: parent.width spacing: 5 * ApplicationInfo.ratio Rectangle { id: button visible: true // Add radius to add some space between text and borders implicitWidth: Math.max(200, currentTextBox.width+radius) implicitHeight: 50 * ApplicationInfo.ratio border.width: 2 border.color: "#373737" radius: 10 gradient: Gradient { GradientStop { position: 0 ; color: mouseArea.pressed ? "#C03ACAFF" : "#23373737" } GradientStop { position: 1 ; color: mouseArea.pressed ? "#803ACAFF" : "#13373737" } } // Current value of combobox GCText { id: currentTextBox anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter text: currentText fontSize: mediumSize color: "#373737" } MouseArea { id: mouseArea anchors.fill: parent onReleased: { popup.visible = true } } } Item { width: labelText.width height: button.height GCText { id: labelText text: label anchors.verticalCenter: parent.verticalCenter fontSize: mediumSize wrapMode: Text.WordWrap } } } /** * type:Item * Combobox display when active: header with the description and the gridview containing all the available choices. */ Item { id: popup visible: false - width: parent.width - height: parent.height + width: if(parent) parent.width + height: if(parent) parent.height parent: background z: 100 focus: visible // Forward event to activity if key pressed is not one of the handled key // (ctrl+F should still resize the window for example) Keys.onPressed: { if(event.key !== Qt.Key_Back) { background.currentActivity.Keys.onPressed(event) } } Keys.onReleased: { if(event.key === Qt.Key_Back) { // Keep the old value discardChange(); hidePopUpAndRestoreFocus(); event.accepted = true } } Keys.onRightPressed: gridview.moveCurrentIndexRight(); Keys.onLeftPressed: gridview.moveCurrentIndexLeft(); Keys.onDownPressed: gridview.moveCurrentIndexDown(); Keys.onUpPressed: gridview.moveCurrentIndexUp(); Keys.onEscapePressed: { // Keep the old value discardChange(); hidePopUpAndRestoreFocus(); } Keys.onEnterPressed: { acceptChange(); hidePopUpAndRestoreFocus(); } Keys.onReturnPressed: { acceptChange(); hidePopUpAndRestoreFocus(); } Keys.onSpacePressed: { acceptChange(); hidePopUpAndRestoreFocus(); } // Don't accept the list value, restore previous value function discardChange() { if(isModelArray) { for(var i = 0 ; i < model.count ; ++ i) { if(model[currentIndex].text === currentText) { currentIndex = i; break; } } } else { for(var i = 0 ; i < model.length ; ++ i) { if(model.get(currentIndex).text === currentText) { currentIndex = i; break; } } } gridview.currentIndex = currentIndex; } // Accept the change. Updates the currentIndex and text of the button function acceptChange() { currentIndex = gridview.currentIndex; currentText = isModelArray ? model[currentIndex].text : (model && model.get(currentIndex) ? model.get(currentIndex).text : "") } function hidePopUpAndRestoreFocus() { popup.visible = false; // Restore focus on previous activity for keyboard input background.forceActiveFocus(); } Rectangle { id: listBackground anchors.fill: parent radius: 10 color: "grey" Rectangle { id : headerDescription z: 10 width: gridview.width height: gridview.elementHeight GCText { text: label fontSize: mediumSize wrapMode: Text.WordWrap anchors.horizontalCenter: parent.horizontalCenter } GCButtonCancel { id: discardIcon anchors.right: headerDescription.right anchors.top: headerDescription.top MouseArea { anchors.fill: parent onClicked: { popup.acceptChange(); popup.hidePopUpAndRestoreFocus(); } } } } GridView { id: gridview z: 4 readonly property int elementHeight: 40 * ApplicationInfo.ratio // each element has a 300 width size minimum. If the screen is larger than it, // we do a grid with cases with 300px for width at minimum. // If you have a better idea/formula to have a different column number, don't hesitate, change it :). readonly property int numberOfColumns: Math.max(1, Math.floor(width / (300 * ApplicationInfo.ratio))) contentHeight: isModelArray ? elementHeight*model.count/numberOfColumns : elementHeight*model.length/numberOfColumns width: listBackground.width height: listBackground.height-headerDescription.height currentIndex: gccombobox.currentIndex flickableDirection: Flickable.VerticalFlick clip: true anchors.top: headerDescription.bottom cellWidth: width / numberOfColumns cellHeight: elementHeight delegate: Component { Rectangle { width: gridview.cellWidth height: gridview.elementHeight color: GridView.isCurrentItem ? "darkcyan" : "beige" border.width: GridView.isCurrentItem ? 3 : 2 radius: 5 Image { id: isSelectedIcon visible: parent.GridView.isCurrentItem source: "qrc:/gcompris/src/core/resource/apply.svg" anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 10 sourceSize.width: (gridview.elementHeight * 0.8) } GCText { id: textValue text: isModelArray ? modelData.text : model.text anchors.centerIn: parent } MouseArea { anchors.fill: parent onClicked: { currentIndex = index popup.acceptChange(); popup.hidePopUpAndRestoreFocus(); } } } } } } } }