Warning, /education/gcompris/src/activities/railroad/Railroad.qml is written in an unsupported language. File is not indexed.
0001 /* GCompris - railroad.qml
0002 *
0003 * SPDX-FileCopyrightText: 2016 Utkarsh Tiwari <iamutkarshtiwari@kde.org>
0004 * SPDX-FileCopyrightText: 2018 Amit Sagtani <asagtani06@gmail.com>
0005 *
0006 * Authors:
0007 * Pascal Georges (GTK+ version)
0008 * Utkarsh Tiwari <iamutkarshtiwari@kde.org> (Qt Quick port)
0009 * Amit Sagtani <asagtani06@gmail.com> (Qt Quick port)
0010 * Timothée Giet <animtim@gmail.com> (controls refactoring and bugfixes)
0011 *
0012 * SPDX-License-Identifier: GPL-3.0-or-later
0013 */
0014 import QtQuick 2.12
0015 import GCompris 1.0
0016 import "../../core"
0017 import "railroad.js" as Activity
0018
0019 ActivityBase {
0020 id: activity
0021
0022 onStart: focus = true
0023 onStop: {}
0024 property bool isHorizontal: background.width >= background.height
0025
0026 pageComponent: Image {
0027 id: background
0028 source: Activity.resourceURL + "railroad-bg.svg"
0029 sourceSize.height: background.height
0030 fillMode: Image.PreserveAspectCrop
0031 anchors.horizontalCenter: parent.horizontalCenter
0032
0033 signal start
0034 signal stop
0035
0036 Component.onCompleted: {
0037 activity.start.connect(start)
0038 activity.stop.connect(stop)
0039 }
0040
0041 // Add here the QML items you need to access in javascript
0042 QtObject {
0043 id: items
0044 property Item main: activity.main
0045 property alias background: background
0046 property int currentLevel: activity.currentLevel
0047 property alias bonus: bonus
0048 property alias score: score
0049 property GCSfx audioEffects: activity.audioEffects
0050 property alias trainAnimationTimer: trainAnimationTimer
0051 property alias sampleList: sampleList
0052 property alias listModel: listModel
0053 property alias answerZone: answerZone
0054 property alias animateFlow: animateFlow
0055 property alias introMessage: introMessage
0056 property alias errorRectangle: errorRectangle
0057 property bool memoryMode: false
0058 property bool mouseEnabled: true
0059 property bool controlsEnabled: false
0060 property var currentKeyZone: sampleList
0061 property bool keyNavigationMode: false
0062 // stores height of sampleGrid images to set rail bar support position
0063 property int sampleImageHeight: 0
0064 property int sampleModel: Activity.dataset["noOfLocos"][currentLevel] + Activity.dataset["noOfWagons"][currentLevel]
0065 property var uniqueId: []
0066 }
0067
0068 onStart: { Activity.start(items) }
0069 onStop: { Activity.stop() }
0070 // Needed to get keyboard focus on IntroMessage
0071 Keys.forwardTo: introMessage
0072
0073 Keys.onPressed: {
0074 items.keyNavigationMode = true;
0075 items.currentKeyZone.handleKeys(event);
0076 }
0077
0078 function playSoundFX() {
0079 activity.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav');
0080 }
0081
0082 // Countdown timer
0083 Timer {
0084 id: trainAnimationTimer
0085 repeat: false
0086 interval: 4000
0087 onTriggered: {
0088 items.animateFlow.start()
0089 activity.audioEffects.play(Activity.resourceURL + 'sounds/train.wav')
0090 }
0091 }
0092
0093 // Intro message
0094 IntroMessage {
0095 id: introMessage
0096 y: background.height / 4.7
0097 anchors {
0098 right: parent.right
0099 rightMargin: 5
0100 left: parent.left
0101 leftMargin: 5
0102 }
0103 z: score.z + 1
0104 onIntroDone: {
0105 trainAnimationTimer.start()
0106 }
0107 intro: [
0108 qsTr("Observe and remember the train before the timer ends and then drag the items to set up a similar train."),
0109 qsTr("If you forgot the model, you can click on the Hint button to view it again.")
0110 ]
0111 }
0112
0113 // Top Display Area
0114 Rectangle {
0115 id: topDisplayArea
0116 width: background.width
0117 height: background.height * 0.2
0118 anchors.bottom: sampleList.top
0119 color: 'transparent'
0120 z: 1
0121
0122 GridView {
0123 id: answerZone
0124 readonly property int levelCellWidth: isHorizontal ? background.width / (listModel.count > 5 ? 7.2 : 5.66) :
0125 background.width / ((listModel.count > 5) ? 7.1 : 5)
0126 readonly property int levelCellHeight: levelCellWidth * 0.42
0127 width: parent.width
0128 height: levelCellHeight
0129 cellWidth: levelCellWidth
0130 cellHeight: levelCellHeight
0131 anchors.bottom: parent.bottom
0132 interactive: false
0133 model: listModel
0134
0135 delegate: Image {
0136 id: wagon
0137 source: Activity.resourceURL + modelData + ".svg"
0138 fillMode: Image.PreserveAspectFit
0139 width: answerZone.cellWidth
0140 sourceSize.width: wagon.width
0141 function checkDrop(dragItem) {
0142 // Checks the drop location of this wagon
0143 var globalCoordinates = dragItem.mapToItem(answerZone, 0, 0)
0144 if(globalCoordinates.y <= ((background.height / 12.5) + (background.height / 8))) {
0145 var dropIndex = Activity.getDropIndex(globalCoordinates.x)
0146
0147 if(dropIndex > (listModel.count - 1)) {
0148 // Handles index overflow
0149 dropIndex = listModel.count - 1
0150 }
0151 listModel.move(listModel.count - 1, dropIndex, 1)
0152 opacity = 1
0153 }
0154 if(globalCoordinates.y > (background.height / 8)) {
0155 // Remove it if dropped in the lower section
0156 activity.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav')
0157 listModel.remove(listModel.count - 1)
0158 }
0159 }
0160
0161 function createNewItem() {
0162 var component = Qt.createComponent("Loco.qml");
0163 if(component.status === Component.Ready) {
0164 var newItem = component.createObject(parent, {"x": x, "y": y, "z": 10, "imageURL": source});
0165 }
0166 return newItem
0167 }
0168
0169 MouseArea {
0170 id: displayWagonMouseArea
0171 hoverEnabled: true
0172 enabled: !introMessage.visible && items.mouseEnabled
0173 anchors.fill: parent
0174
0175 onPressed: {
0176 if(items.memoryMode) {
0177 drag.target = parent.createNewItem();
0178 parent.opacity = 0
0179 listModel.move(index, listModel.count - 1, 1)
0180 }
0181 answerZone.selectedSwapIndex = -1;
0182 }
0183 onReleased: {
0184 if(items.memoryMode) {
0185 var dragItem = drag.target
0186 parent.checkDrop(dragItem)
0187 dragItem.destroy();
0188 parent.Drag.cancel()
0189 }
0190 }
0191
0192 onClicked: {
0193 // skips memorization time
0194 if(!items.memoryMode) {
0195 bar.hintClicked()
0196 }
0197 else {
0198 items.currentKeyZone = answerZone
0199 if(items.keyNavigationMode) {
0200 answerZone.currentIndex = index
0201 }
0202 }
0203 answerZone.selectedSwapIndex = -1;
0204 }
0205 }
0206 states: State {
0207 name: "wagonHover"
0208 when: displayWagonMouseArea.containsMouse && (items.memoryMode === true)
0209 PropertyChanges {
0210 target: wagon
0211 scale: 1.1
0212 }
0213 }
0214 }
0215
0216 onXChanged: {
0217 if(answerZone.x >= background.width) {
0218 trainAnimationTimer.stop()
0219 animateFlow.stop();
0220 listModel.clear();
0221 items.memoryMode = true;
0222 items.controlsEnabled = true;
0223 }
0224 }
0225
0226 PropertyAnimation {
0227 id: animateFlow
0228 target: answerZone
0229 properties: "x"
0230 from: answerZone.x
0231 to: background.width
0232 duration: 4000
0233 easing.type: Easing.InExpo
0234 loops: 1
0235 onStopped: answerZone.x = 2;
0236 }
0237
0238 function handleKeys(event) {
0239 // Checks answer.
0240 if(event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
0241 okButton.clicked();
0242 }
0243 if(trainAnimationTimer.running || animateFlow.running) {
0244 return;
0245 }
0246 if(event.key === Qt.Key_Tab)
0247 bar.hintClicked();
0248 if(!items.controlsEnabled)
0249 return;
0250 if(event.key === Qt.Key_Down) {
0251 playSoundFX();
0252 items.currentKeyZone = sampleList;
0253 answerZone.currentIndex = -1;
0254 sampleList.currentIndex = 0;
0255 }
0256 if(event.key === Qt.Key_Up) {
0257 playSoundFX();
0258 items.currentKeyZone = sampleList;
0259 answerZone.currentIndex = -1;
0260 sampleList.currentIndex = 0;
0261 }
0262 if(event.key === Qt.Key_Left) {
0263 playSoundFX();
0264 answerZone.moveCurrentIndexLeft();
0265 }
0266 if(event.key === Qt.Key_Right) {
0267 playSoundFX();
0268 answerZone.moveCurrentIndexRight();
0269 }
0270 // Remove a wagon via Delete/Return key.
0271 if((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) && listModel.count > 0) {
0272 playSoundFX();
0273 listModel.remove(answerZone.currentIndex);
0274 if(listModel.count < 2) {
0275 answerZone.selectedSwapIndex = -1;
0276 }
0277 }
0278 // Swaps two wagons with help of Space/Enter keys.
0279 if(event.key === Qt.Key_Space) {
0280 if(selectedSwapIndex === -1 && listModel.count > 1) {
0281 playSoundFX();
0282 answerZone.selectedSwapIndex = answerZone.currentIndex;
0283 swapHighlight.x = answerZone.currentItem.x;
0284 swapHighlight.anchors.top = answerZone.top;
0285 }
0286 else if(answerZone.currentIndex != selectedSwapIndex && listModel.count > 1){
0287 playSoundFX();
0288 var min = Math.min(selectedSwapIndex, answerZone.currentIndex);
0289 var max = Math.max(selectedSwapIndex, answerZone.currentIndex);
0290 listModel.move(min, max, 1);
0291 listModel.move(max-1, min, 1);
0292 answerZone.selectedSwapIndex = -1;
0293 }
0294 }
0295 }
0296 // variable for storing the index of wagons to be swapped via key navigations.
0297 property int selectedSwapIndex: -1
0298
0299 Keys.enabled: true
0300 focus: true
0301 keyNavigationWraps: true
0302 highlightRangeMode: GridView.ApplyRange
0303 highlight: Rectangle {
0304 width: answerZone.cellWidth
0305 height: answerZone.cellHeight
0306 color: "blue"
0307 opacity: 0.3
0308 radius: 5
0309 visible: (items.currentKeyZone === answerZone) && (!trainAnimationTimer.running && !animateFlow.running)
0310 && items.keyNavigationMode && items.controlsEnabled
0311 x: (visible && answerZone.currentItem) ? answerZone.currentItem.x : 0
0312 y: (visible && answerZone.currentItem) ? answerZone.currentItem.y : 0
0313 Behavior on x {
0314 SpringAnimation {
0315 spring: 3
0316 damping: 0.2
0317 }
0318 }
0319 Behavior on y {
0320 SpringAnimation {
0321 spring: 3
0322 damping: 0.2
0323 }
0324 }
0325 }
0326 highlightFollowsCurrentItem: false
0327 }
0328
0329 // Used to highlight a wagon selected for swaping via key navigations
0330 Rectangle {
0331 id: swapHighlight
0332 width: answerZone.cellWidth
0333 height: answerZone.cellHeight
0334 visible: answerZone.selectedSwapIndex != -1 ? true : false
0335 color: "#AA41AAC4"
0336 opacity: 0.8
0337 radius: 5
0338 }
0339
0340 ListModel {
0341 id: listModel
0342 }
0343 }
0344
0345 // Lower Sample Wagon Display Area
0346 GridView {
0347 id: sampleList
0348 visible: items.memoryMode
0349 y: background.height * 0.2
0350 z: 5
0351 width: background.width
0352 height: background.height * 0.8
0353 anchors.margins: 20
0354 cellWidth: width / columnCount
0355 cellHeight: isHorizontal ? background.height / 7 : background.height / 7.5
0356 model: items.uniqueId
0357 interactive: false
0358
0359 // No. of wagons in a row
0360 readonly property int columnCount: isHorizontal ? Activity.dataset["columnsInHorizontalMode"][items.currentLevel] :
0361 Activity.dataset["columsInVerticalMode"][items.currentLevel]
0362
0363 readonly property int rowCount: columnCount > 0 ? model.length / columnCount : 0
0364
0365 delegate: Image {
0366 id: loco
0367 readonly property string uniqueID: modelData
0368 property real originX
0369 property real originY
0370 source: Activity.resourceURL + uniqueID + ".svg"
0371 width: isHorizontal ? background.width / 5.66 : background.width / 4.2
0372 sourceSize.width: width
0373 fillMode: Image.PreserveAspectFit
0374 visible: true
0375 onHeightChanged: items.sampleImageHeight = height
0376 onVisibleChanged: items.sampleImageHeight = height
0377 function initDrag() {
0378 originX = x
0379 originY = y
0380 }
0381
0382 function replace() {
0383 x = originX
0384 y = originY
0385 }
0386
0387 function checkDrop() {
0388 // Checks the drop location of this wagon
0389 var globalCoordinates = loco.mapToItem(answerZone, 0, 0)
0390 // checks if the wagon is dropped in correct zone and no. of wagons in answer row are less than
0391 // total no. of wagons in correct answer + 2, before dropping the wagon
0392 if(globalCoordinates.y <= (background.height / 12.5) &&
0393 listModel.count < Activity.dataset["WagonsInCorrectAnswers"][items.currentLevel] + 2) {
0394 activity.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav')
0395 var dropIndex = Activity.getDropIndex(globalCoordinates.x)
0396 Activity.addWagon(uniqueID, dropIndex);
0397 }
0398 }
0399
0400 MouseArea {
0401 id: mouseArea
0402 hoverEnabled: true
0403 anchors.fill: parent
0404 drag.target: parent
0405 enabled: items.mouseEnabled
0406 onClicked: {
0407 items.currentKeyZone = sampleList
0408 if(items.keyNavigationMode) {
0409 sampleList.currentIndex = index
0410 }
0411 }
0412 onPressed: {
0413 parent.initDrag()
0414 }
0415 onReleased: {
0416 parent.Drag.cancel()
0417 parent.checkDrop()
0418 parent.replace()
0419 }
0420 }
0421
0422 Component.onCompleted: initDrag();
0423
0424 states: State {
0425 name: "carHover"
0426 when: mouseArea.containsMouse
0427 PropertyChanges {
0428 target: loco
0429 scale: 1.1
0430 }
0431 }
0432 }
0433
0434 function handleKeys(event) {
0435 if(event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
0436 okButton.clicked();
0437 }
0438 if(trainAnimationTimer.running || animateFlow.running) {
0439 return;
0440 }
0441 if(event.key === Qt.Key_Tab)
0442 bar.hintClicked();
0443 if(!items.controlsEnabled)
0444 return;
0445
0446 if(event.key === Qt.Key_Up) {
0447 playSoundFX();
0448 // Checks if current highlighted element is in first row of the grid.
0449 if(sampleList.currentIndex < columnCount && listModel.count > 0) {
0450 items.currentKeyZone = answerZone;
0451 answerZone.currentIndex = 0;
0452 sampleList.currentIndex = -1;
0453 }
0454 else {
0455 sampleList.moveCurrentIndexUp();
0456 }
0457 }
0458 if(event.key === Qt.Key_Down) {
0459 playSoundFX();
0460 // Checks if current highlighted element is in last row of the grid.
0461 if(sampleList.model - columnCount <= sampleList.currentIndex && listModel.count > 0) {
0462 items.currentKeyZone = answerZone;
0463 answerZone.currentIndex = 0;
0464 sampleList.currentIndex = -1;
0465 }
0466 else {
0467 sampleList.moveCurrentIndexDown();
0468 }
0469 }
0470 if(event.key === Qt.Key_Left) {
0471 playSoundFX();
0472 sampleList.moveCurrentIndexLeft();
0473 }
0474 if(event.key === Qt.Key_Right) {
0475 playSoundFX();
0476 sampleList.moveCurrentIndexRight();
0477 }
0478 if(event.key === Qt.Key_Space) {
0479 var imageId = items.uniqueId[sampleList.currentIndex];
0480 // At most (current level + 2) wagons are allowed in answer row at a time.
0481 if(listModel.count < Activity.dataset["WagonsInCorrectAnswers"][items.currentLevel] + 2) {
0482 playSoundFX();
0483 Activity.addWagon(imageId, listModel.count);
0484 }
0485 }
0486 }
0487
0488 Keys.enabled: true
0489 focus: true
0490 keyNavigationWraps: true
0491 highlightRangeMode: GridView.ApplyRange
0492 highlight: Rectangle {
0493 width: isHorizontal ? background.width / 5.66 : background.width / 4.2
0494 height: isHorizontal ? sampleList.cellHeight : sampleList.cellHeight / 1.65
0495 color: "#AA41AAC4"
0496 opacity: 0.8
0497 radius: 5
0498 visible: items.currentKeyZone === sampleList && items.keyNavigationMode
0499 x: (sampleList.currentIndex >= 0 && sampleList.currentItem) ? sampleList.currentItem.x : 0
0500 y: (sampleList.currentIndex >= 0 && sampleList.currentItem) ? sampleList.currentItem.y : 0
0501 Behavior on x {
0502 SpringAnimation {
0503 spring: 3
0504 damping: 0.2
0505 }
0506 }
0507 Behavior on y {
0508 SpringAnimation {
0509 spring: 3
0510 damping: 0.2
0511 }
0512 }
0513 }
0514 highlightFollowsCurrentItem: false
0515 }
0516
0517 // Lower level wagons shelves
0518 Repeater {
0519 id: railSupporter
0520 model: sampleList.rowCount
0521 Rectangle {
0522 x: 0
0523 y: sampleList.y + (sampleList.cellHeight * (index + 1)) - (sampleList.cellHeight - items.sampleImageHeight)
0524 z: 1
0525 width: background.width
0526 height: isHorizontal ? 6 : 3
0527 border.color: "#808180"
0528 color: "transparent"
0529 border.width: 4
0530 }
0531 }
0532
0533 ErrorRectangle {
0534 id: errorRectangle
0535 anchors.fill: topDisplayArea
0536 z: score.z
0537 imageSize: okButton.width
0538 function releaseControls() { items.mouseEnabled = true; }
0539 }
0540
0541 // Answer Submission button
0542 BarButton {
0543 id: okButton
0544 source: "qrc:/gcompris/src/core/resource/bar_ok.svg"
0545 height: score.height
0546 width: height
0547 sourceSize.width: width
0548 sourceSize.height: height
0549 anchors.top: score.top
0550 z: score.z
0551 enabled: items.mouseEnabled
0552 anchors {
0553 right: score.left
0554 rightMargin: 10
0555 }
0556 ParticleSystemStarLoader {
0557 id: okButtonParticles
0558 clip: false
0559 }
0560 onClicked: {
0561 if(trainAnimationTimer.running || animateFlow.running)
0562 bar.hintClicked();
0563 else if(listModel.count > 0 && visible)
0564 Activity.checkAnswer();
0565 }
0566 }
0567
0568 DialogHelp {
0569 id: dialogHelp
0570 onClose: home()
0571 }
0572
0573 Score {
0574 id: score
0575 height: bar.height * 0.8
0576 width: height
0577 anchors.top: parent.top
0578 anchors.topMargin: 10 * ApplicationInfo.ratio
0579 anchors.right: parent.right
0580 anchors.leftMargin: 10 * ApplicationInfo.ratio
0581 anchors.bottom: undefined
0582 anchors.left: undefined
0583 onStop: Activity.nextSubLevel()
0584 }
0585
0586 Bar {
0587 id: bar
0588 level: items.currentLevel + 1
0589 content: BarEnumContent { value: help | home | level | hint }
0590 onHelpClicked: {
0591 displayDialog(dialogHelp)
0592 }
0593 z: introMessage.z
0594 onPreviousLevelClicked: Activity.previousLevel()
0595 onNextLevelClicked: Activity.nextLevel()
0596 onHomeClicked: home()
0597 onHintClicked: {
0598 if(!introMessage.visible && items.mouseEnabled) {
0599 if(items.memoryMode == false) {
0600 trainAnimationTimer.stop()
0601 animateFlow.stop();
0602 listModel.clear();
0603 for(var index = 0; index < Activity.backupListModel.length; index++) {
0604 Activity.addWagon(Activity.backupListModel[index], index);
0605 }
0606 items.memoryMode = true;
0607 okButton.visible = true;
0608 items.controlsEnabled = true;
0609 } else {
0610 Activity.restoreLevel();
0611 okButton.visible = false;
0612 items.controlsEnabled = false;
0613 }
0614 }
0615 }
0616 }
0617
0618 Bonus {
0619 id: bonus
0620 onWin: Activity.nextLevel()
0621 }
0622 }
0623 }