diff --git a/src/activities/superbrain/Superbrain.qml b/src/activities/superbrain/Superbrain.qml index 030f4e7a1..57375874c 100644 --- a/src/activities/superbrain/Superbrain.qml +++ b/src/activities/superbrain/Superbrain.qml @@ -1,599 +1,626 @@ /* GCompris - Superbrain.qml * * Copyright (C) 2015 Holger Kaelberer * * Authors: * Bruno Coudoin (GTK+ version) * Holger Kaelberer (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.1 import "../../core" import "superbrain.js" as Activity import GCompris 1.0 ActivityBase { id: activity onStart: focus = true onStop: {} pageComponent: Image { id: background source: Activity.baseUrl + "background.svg" sourceSize.width: parent.width fillMode: Image.PreserveAspectCrop focus: true readonly property double scaleFactor: Math.max(1, Math.min(background.width / 800, background.height / 520)) readonly property bool isPortrait: (height > width) signal start signal stop MouseArea { anchors.fill: parent onClicked: showChooser(false); } Component.onCompleted: { activity.start.connect(start) activity.stop.connect(stop) } QtObject { id: items property Item main: activity.main property alias background: background property alias bar: bar property alias bonus: bonus property alias score: score property alias colorsRepeater: colorsRepeater property alias chooserGrid: chooserGrid property alias guessModel: guessModel property alias guessColumn: guessColumn property alias currentRepeater: currentRepeater } onStart: { Activity.start(items) } onStop: { Activity.stop() } Column { id: colorsColumn anchors.left: parent.left anchors.leftMargin: 5 * background.scaleFactor * ApplicationInfo.ratio anchors.top: parent.top anchors.topMargin: 5 * background.scaleFactor * ApplicationInfo.ratio spacing: 3 * background.scaleFactor * ApplicationInfo.ratio width: guessColumn.guessSize height: guessColumn.guessSize add: Transition { NumberAnimation { properties: "y"; duration: 1000; easing.type: Easing.OutBounce } } Repeater { id: colorsRepeater model: ListModel {} delegate: Rectangle { width: 40 * background.scaleFactor * ApplicationInfo.ratio height: 40 * background.scaleFactor * ApplicationInfo.ratio radius: width * 0.5 border.width: 2 border.color: "white" color: col } } } Rectangle { id: tooltipRect width: 100 height: tooltipText.font.pixelSize + 10 radius: 4 x: 0 y: 0 color: "lightgray" opacity: 0 z: 10 property alias text: tooltipText.text GCText { id: tooltipText anchors.centerIn: parent fontSize: NaN font.pixelSize: 11 * ApplicationInfo.ratio text: "" color: "black" onTextChanged: parent.width = width + 10 } Behavior on opacity { NumberAnimation { duration: 100 } } } function showTooltip(visible, status, mouseArea) { if (!visible || status === Activity.STATUS_UNKNOWN) { tooltipRect.opacity = 0; return; } showChooser(false); var obj = background.mapFromItem(mouseArea, mouseArea.mouseX, mouseArea.mouseY); if (status === Activity.STATUS_CORRECT) tooltipRect.text = qsTr("This item is well placed"); if (status === Activity.STATUS_MISPLACED) tooltipRect.text = qsTr("This item is misplaced"); tooltipRect.x = obj.x - 5 - tooltipRect.width; tooltipRect.y = obj.y - 5 - tooltipRect.height; tooltipRect.opacity = 0.9; } function showChooser(visible, guessIndex, item) { if (!visible) { chooserTimer.stop(); chooser.scale = 0; return; } var modelObj = guessModel.get(0).guess.get(guessIndex); var absolute = currentRow.mapToItem(background, item.x, item.y); chooserGrid.colIndex = modelObj.colIndex; chooserGrid.guessIndex = guessIndex; var chooserOffset = 0.5*chooser.width - item.width/2; var arrowOffset = 0; var targetX = item.x - chooserOffset; // beyond left screen border: if (absolute.x - chooserOffset < 0) { arrowOffset = absolute.x - chooserOffset; targetX -= arrowOffset; } // beyond right screen border: if (absolute.x + chooserOffset + item.width > background.width) { arrowOffset = absolute.x + chooserOffset + item.width - background.width; targetX -= arrowOffset; } chooser.x = targetX; chooser.arrowOffset = arrowOffset; var targetY = item.y - chooser.height - 15; var targetAbove = true; /* //only on top-level, at window border: if (targetY < 0) { targetY = item.y + guessColumn.guessSize + 10; targetAbove = false; }*/ chooser.y = targetY; chooser.above = targetAbove; chooser.scale = 1; chooser.visible = true; chooserTimer.restart(); //console.log("XXX chooser at item.x=" + item.x + " absolute.x=" + absolute.x + " chooser.x/w=" + chooser.x + "/" + chooser.width + " background.width=" + background.width + " currentRow.x/y/w/h=" + currentRow.x + "/" + currentRow.y + "/" + currentRow.width + "/" + currentRow.height + " guessIdx=" + guessIndex + " arrowOff=" + arrowOffset); } Item { id: currentWrapper width: currentRow.width height: currentRow.height z: 8 anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: ApplicationSettings.isBarHidden ? parent.bottom : bar.top anchors.bottomMargin: 20 * ApplicationInfo.ratio Rectangle { id: chooser width: chooserGrid.width + 15 height: chooserGrid.height + 15 color: "darkgray" border.width: 0 border.color: "white" opacity: 1 scale: 0 visible: false z: 10 property bool above: true property real arrowOffset: 0 Rectangle { id: chooserArrow width: 10 height: 10 x: chooser.width / 2 - 5 + chooser.arrowOffset y: chooser.above ? (chooser.height - 5) : (-5) color: chooser.color z: chooser.z transform: Rotation { origin.x: 5; origin.y: 5; angle: 45} } GridView { id: chooserGrid cellWidth: guessColumn.guessSize * 2 cellHeight: guessColumn.guessSize * 2 width: Math.ceil(count / 2) * cellWidth// * 1.2 height: 2 * cellHeight// * 1.2 anchors.centerIn: parent z: 11 clip: false interactive: false verticalLayoutDirection: GridView.TopToBottom layoutDirection: Qt.LeftToRight flow: GridView.FlowLeftToRight property int colIndex: 0 property int guessIndex: 0 Timer { id: chooserTimer interval: 5000 onTriggered: showChooser(false); } model: new Array() delegate: Rectangle { width: chooserGrid.cellWidth height: chooserGrid.cellWidth radius: 5 * background.scaleFactor border.width: index == chooserGrid.colIndex ? 3 : 1 border.color: index == chooserGrid.colIndex ? "white" : "darkgray" color: modelData MouseArea { id: chooserMouseArea anchors.fill: parent acceptedButtons: Qt.LeftButton z: 11 hoverEnabled: ApplicationInfo.isMobile ? false : true onClicked: { chooserGrid.colIndex = index; var obj = items.guessModel.get(0); obj.guess.setProperty(chooserGrid.guessIndex, "colIndex", chooserGrid.colIndex); showChooser(false); } } } } Behavior on scale { NumberAnimation { duration: 100 } } } Row { id: currentRow visible: true property double factor: 1.9 anchors.left: parent.left anchors.top: parent.top spacing: guessColumn.horizSpacing * factor height: guessColumn.guessSize * factor scale: 1 z: 9 Repeater { id: currentRepeater delegate: Rectangle { id: currentGuess width: guessColumn.guessSize * currentRow.factor height: guessColumn.guessSize * currentRow.factor radius: width * 0.5 border.width: 2 * currentRow.factor border.color: "lightgray" color: Activity.colors[colIndex] opacity: 1.0 z: 2 MouseArea { id: mouseArea anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton enabled: true z: 3 hoverEnabled: ApplicationInfo.isMobile ? false : true onPressAndHold: { if (guessColumn.count > 1) guessModel.get(0).guess.get(index).colIndex = guessModel.get(1).guess.get(index).colIndex; } onClicked: { var obj = items.guessModel.get(0).guess.get(index); if(chooserTimer.running && chooserGrid.guessIndex === index) { if (mouse.button == Qt.LeftButton) obj.colIndex = (obj.colIndex == Activity.currentColors.length - 1) ? 0 : obj.colIndex + 1; else obj.colIndex = (obj.colIndex == 0) ? Activity.currentColors.length - 1 : obj.colIndex - 1; } showChooser(true, index, parent); } } states: State { name: "scaled"; when: mouseArea.containsMouse PropertyChanges { target: currentGuess scale: 1.1 } } transitions: Transition { NumberAnimation { properties: "scale"; easing.type: Easing.OutCubic } } } } BarButton { id: okButton source: "qrc:/gcompris/src/core/resource/bar_ok.svg" sourceSize.width: 66 * bar.barZoom width: guessColumn.guessSize * currentRow.factor height: guessColumn.guessSize * currentRow.factor visible: true z: 8 onClicked: { showChooser(false); Activity.checkGuess(); } } } } ListModel { id: guessModel dynamicRoles: true } ListView { id: guessColumn anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: currentWrapper.top anchors.bottomMargin: 10 * ApplicationInfo.ratio boundsBehavior: Flickable.DragOverBounds verticalLayoutDirection: ListView.BottomToTop readonly property int guessSize: 30 * background.scaleFactor * ApplicationInfo.ratio readonly property int vertSpacing: 15 * background.scaleFactor * ApplicationInfo.ratio readonly property int horizSpacing: 15 * background.scaleFactor * ApplicationInfo.ratio readonly property int statusMargin: 5 * background.scaleFactor * ApplicationInfo.ratio readonly property int resultSize: 10 * background.scaleFactor * ApplicationInfo.ratio readonly property int guessColWidth: Activity.maxPieces * (guessSize + (2 * guessColumn.statusMargin)) + (Activity.maxPieces-1) * horizSpacing; readonly property int resultColWidth: Activity.maxPieces * resultSize + (Activity.maxPieces-1) * 2; spacing: vertSpacing width: guessColWidth + 10 + (2 * horizSpacing) + resultColWidth height: count * (guessSize + vertSpacing) model: guessModel delegate: Row { id: guessRow width: guessColumn.width height: guessColumn.guessSize spacing: guessColumn.horizSpacing property int rowIndex: index visible: index != 0 Item { id: guessRowSpacer width: guessColumn.guessColWidth - (guessRepeater.count * (guessColumn.guessSize + (2 * guessColumn.statusMargin) + guessColumn.horizSpacing)) height: parent.height } Repeater { id: guessRepeater anchors.left: parent.left anchors.top: parent.top model: guess delegate: Item { // wrapper needed for singleGuessStatusRect's opacity id: singleGuessWrapper width: guessColumn.guessSize + (2 * guessColumn.statusMargin); height: guessColumn.guessSize + (2 * guessColumn.statusMargin); Rectangle { id: singleGuessStatusRect border.width: 2 border.color: (status == Activity.STATUS_CORRECT) ? "white" : "black"; anchors.fill: parent radius: 3 color: (status == Activity.STATUS_CORRECT) ? "black" : "white"; opacity: (status == Activity.STATUS_UNKNOWN) ? 0 : 0.9 z: 1 MouseArea { id: mouseAreaRect anchors.fill: parent acceptedButtons: Qt.LeftButton enabled: guessRow.rowIndex > 0 z: 4 hoverEnabled: ApplicationInfo.isMobile ? false : true Timer { id: tooltipTimer repeat: false interval: 300 onTriggered: showTooltip(true, status, mouseAreaRect) } onEntered: tooltipTimer.restart() onExited: { tooltipTimer.stop() showTooltip(false) } - onClicked: showTooltip(true, status, mouseAreaRect) // for mobile + onPressAndHold: showTooltip(true, status, mouseAreaRect); + + onDoubleClicked: Activity.ackColor(index, colIndex); + } } Rectangle { id: singleGuess width: guessColumn.guessSize height: guessColumn.guessSize anchors.left: parent.left anchors.top: parent.top anchors.leftMargin: guessColumn.statusMargin anchors.topMargin: guessColumn.statusMargin radius: width * 0.5 border.width: 2 border.color: "lightgray" color: Activity.colors[colIndex] opacity: 1.0 z: 2 + + Image { + id: okImage + visible: isAcked + + width: parent.width / 2 + height: parent.height / 2 + + anchors.centerIn: parent + + source: Activity.baseUrl + "apply.svg" + } + + MouseArea { + id: ackMouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + enabled: status == Activity.STATUS_UNKNOWN + visible: status == Activity.STATUS_UNKNOWN + z: 3 + hoverEnabled: ApplicationInfo.isMobile ? false : true + + onDoubleClicked: Activity.ackColor(index, colIndex); + } } } } Item { id: guessRowSpacer2 width: 10 height: guessColumn.guessSize } Column { id: guessResultColumn width: guessColumn.resultColWidth height: guessColumn.guessSize spacing: 2 Item { id: guessResultColSpacer width: guessResultColumn.width height: (guessResultColumn.height - 2 * (guessColumn.resultSize)) } Row { id: guessResultCorrectRow width: guessResultColumn.width height: guessColumn.resultSize spacing: 2 Repeater { id: guessResultCorrectRepeater model: result.correct delegate: Rectangle { id: singleCorrectResult width: guessColumn.resultSize height: guessColumn.resultSize radius: width * 0.5 border.width: 1 border.color: "white" color: "black" } } } Row { id: guessResultMisplacedRow width: guessResultColumn.width height: guessColumn.resultSize spacing: 2 Repeater { id: guessResultMisplacedRepeater model: result.misplaced delegate: Rectangle { id: singleMisplacedResult width: guessColumn.resultSize height: guessColumn.resultSize radius: width * 0.5 border.width: 1 border.color: "black" color: "white" } } } } } } DialogHelp { id: dialogHelp onClose: home() } Bar { id: bar content: BarEnumContent { value: help | home | level } onHelpClicked: { displayDialog(dialogHelp) } onPreviousLevelClicked: Activity.previousLevel() onNextLevelClicked: Activity.nextLevel() onHomeClicked: activity.home() } Bonus { id: bonus Component.onCompleted: win.connect(Activity.nextSubLevel) } Score { id: score anchors.bottom: undefined anchors.rightMargin: 10 * ApplicationInfo.ratio anchors.topMargin: 10 * ApplicationInfo.ratio anchors.left: undefined anchors.top: parent.top anchors.right: parent.right } } } diff --git a/src/activities/superbrain/resource/apply.svg b/src/activities/superbrain/resource/apply.svg new file mode 100644 index 000000000..01212ee82 --- /dev/null +++ b/src/activities/superbrain/resource/apply.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/activities/superbrain/superbrain.js b/src/activities/superbrain/superbrain.js index 72ce147a2..ee53c6350 100644 --- a/src/activities/superbrain/superbrain.js +++ b/src/activities/superbrain/superbrain.js @@ -1,203 +1,218 @@ /* GCompris - superbrain.js * * Copyright (C) 2015 Holger Kaelberer * * Authors: * Bruno Coudoin (GTK+ version) * Holger Kaelberer (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 . */ .pragma library .import QtQuick 2.0 as Quick .import GCompris 1.0 as GCompris /* Todo/possible improvements: * * - select colors to guess instead of cycling through * - (> 6 levels with duplicate colors) * - improve layout for smartphones (too small, stretch horizontally/vertically * in landscape/portrait orientation) */ var currentLevel = 0; var maxLevel = 6; var currentSubLevel = 0; var maxSubLevel = 6; var items; var baseUrl = "qrc:/gcompris/src/activities/superbrain/resource/"; var maxLevelForHelp = 4; // after this level, we provide less feedback to the user var numberOfPieces = 0; var numberOfColors = 0; var maxPieces = 5; var solution = new Array(maxPieces); var colors = [ "#FF0000FF", "#FF00FF00", "#FFFF0000", "#FF00FFFF", "#FFFF00FF", "#FFFFFF00", "#FF8e7016", "#FF04611a", "#FFa0174b", "#FF7F007F" ]; +var ackColors = new Array(); var currentColors = new Array(); var maxColors = colors.length; var STATUS_UNKNOWN = 0; var STATUS_MISPLACED = 1; var STATUS_CORRECT = 2; function start(items_) { items = items_; currentLevel = 0; currentSubLevel = 0; initLevel(); } function stop() { } function initLevel() { if (currentSubLevel == 0) { // init level items.bar.level = currentLevel + 1; if(currentLevel + 1 < maxLevelForHelp) { numberOfPieces = currentLevel + 3; numberOfColors = currentLevel + 5; } else { numberOfPieces = currentLevel - maxLevelForHelp + 4; numberOfColors = currentLevel - maxLevelForHelp + 6; } } // init sublevel + ackColors = new Array(numberOfPieces); items.score.numberOfSubLevels = maxSubLevel; items.score.currentSubLevel = currentSubLevel + 1; var selectedColors = new Array(maxColors); solution = new Array(numberOfPieces); for (var i = 0; i < maxColors; ++i) selectedColors[i] = false; // generate solution: for(var i = 0; i < numberOfPieces; ++i) { var j; do j = Math.floor(Math.random() * numberOfColors); while (selectedColors[j]); solution[i] = j; selectedColors[j] = true; } //console.log("XXX solution: " + JSON.stringify(solution)); // populate currentColors: items.colorsRepeater.model.clear(); items.currentRepeater.model = new Array(); currentColors = new Array(); for (var i = 0; i < numberOfColors; ++i) { currentColors[i] = colors[i]; items.colorsRepeater.model.append({"col": colors[i]}); } items.chooserGrid.model = currentColors; // add first guess row: items.guessModel.clear(); appendGuessRow(); } function appendGuessRow() { var guessRow = new Array(); for (var i = 0; i < numberOfPieces; ++i) { + var col = guessRow.push({ - colIndex: 0, - status: STATUS_UNKNOWN + index: i, + colIndex: (ackColors[i] === undefined) ? 0 : ackColors[i], + status: STATUS_UNKNOWN, + isAcked: (ackColors[i] !== undefined) }); } items.guessModel.insert(0, { guess: guessRow, result: {correct: 0, misplaced: 0} }); var obj = items.guessModel.get(0); items.currentRepeater.model = obj.guess; } +function ackColor(column, colIndex) +{ + ackColors[column] = (ackColors[column] == colIndex) ? undefined : colIndex; + for (var i = 0; i < items.guessModel.count; i++) { + var obj = items.guessModel.get(i).guess.get(column); + obj.isAcked = (ackColors[column] == obj.colIndex); + } + items.currentRepeater.model.get(column).colIndex = colIndex; + items.currentRepeater.model.get(column).isAcked = (ackColors[column] !== undefined); +} + function checkGuess() { var remainingIndeces = solution.slice(); var obj = items.guessModel.get(0); var correctCount = 0; var misplacedCount = 0; // check for exact matches first: for (var i = 0; i < numberOfPieces; i++) { var guessIndex = obj.guess.get(i).colIndex; var newStatus; if (solution[i] == guessIndex) { // correct remainingIndeces.splice(remainingIndeces.indexOf(guessIndex), 1); if (currentLevel + 1 < maxLevelForHelp) obj.guess.setProperty(i, "status", STATUS_CORRECT); correctCount++; } } obj.result = ({ correct: correctCount }); if (remainingIndeces.length == 0) { items.bonus.good("smiley"); - return; } for (var i = 0; i < numberOfPieces; i++) { if (obj.guess.get(i).status == STATUS_CORRECT) continue; var guessIndex = obj.guess.get(i).colIndex; var newStatus = STATUS_UNKNOWN; if (solution.indexOf(guessIndex) != -1 && remainingIndeces.indexOf(guessIndex) != -1) { // misplaced remainingIndeces.splice(remainingIndeces.indexOf(guessIndex), 1); if (currentLevel + 1 < maxLevelForHelp) obj.guess.setProperty(i, "status", STATUS_MISPLACED); misplacedCount++; } } obj.result = ({ misplaced: misplacedCount, correct: correctCount }); appendGuessRow(); } function nextLevel() { if(maxLevel <= ++currentLevel ) { currentLevel = 0; } currentSubLevel = 0; initLevel(); } function previousLevel() { if(--currentLevel < 0) { currentLevel = maxLevel - 1 } currentSubLevel = 0; initLevel(); } function nextSubLevel() { if( ++currentSubLevel >= maxSubLevel) { currentSubLevel = 0 nextLevel() } initLevel(); }