Warning, /education/gcompris/src/activities/morse_code/MorseCode.qml is written in an unsupported language. File is not indexed.

0001 /* GCompris - MorseCode.qml
0002  *
0003  * SPDX-FileCopyrightText: 2016 SOURADEEP BARUA <sourad97@gmail.com>
0004  * SPDX-FileCopyrightText: 2022 Johnny Jazeix <jazeix@gmail.com>
0005  * SPDX-FileCopyrightText: 2022 Timothée Giet <animtim@gmail.com>
0006  *
0007  * Authors:
0008  *   SOURADEEP BARUA <sourad97@gmail.com>
0009  *   Johnny Jazeix <jazeix@gmail.com>
0010  *   Timothée Giet <animtim@gmail.com>
0011  *
0012  *   SPDX-License-Identifier: GPL-3.0-or-later
0013  */
0014 import QtQuick 2.12
0015 import "../../core"
0016 import "../../core/core.js" as Core
0017 import GCompris 1.0
0018 
0019 ActivityBase {
0020     id: activity
0021     property string resourcesUrl: "qrc:/gcompris/src/activities/morse_code/resource/"
0022     onStart: focus = true
0023     onStop: {}
0024 
0025     // When opening a dialog, it steals the focus and re set it to the activity.
0026     // We need to set it back to the textinput item in order to have key events.
0027     signal resetFocus
0028     onFocusChanged: {
0029        if(focus)
0030             resetFocus();
0031     }
0032 
0033     pageComponent: Image {
0034         id: background
0035         source: "qrc:/gcompris/src/activities/braille_alphabets/resource/background.svg"
0036         fillMode: Image.PreserveAspectCrop
0037         sourceSize.width: Math.max(parent.width, parent.height)
0038 
0039         signal start
0040         signal stop
0041         signal resetFocus
0042 
0043         Component.onCompleted: {
0044             dialogActivityConfig.initialize()
0045             activity.start.connect(start)
0046             activity.stop.connect(stop)
0047             activity.resetFocus.connect(resetFocus)
0048         }
0049 
0050         onResetFocus: {
0051             if (!ApplicationInfo.isMobile)
0052                 textInput.forceActiveFocus();
0053         }
0054 
0055         property int layoutMargins: 10 * ApplicationInfo.ratio
0056 
0057         // Add here the QML items you need to access in javascript
0058         QtObject {
0059             id: items
0060             property Item main: activity.main
0061             property alias background: background
0062             property GCSfx audioEffects: activity.audioEffects
0063             property int currentLevel: activity.currentLevel
0064             property alias bonus: bonus
0065             property alias score: score
0066             property alias textInput: textInput
0067             readonly property var dataset: activity.datasetLoader.data
0068             property bool toAlpha: dataset[currentLevel].toAlpha
0069             property bool audioMode: dataset[currentLevel].audioMode ? dataset[currentLevel].audioMode : false
0070             property string questionText: dataset[currentLevel].question
0071             property string questionValue
0072             property int numberOfLevel: dataset.length
0073             property bool buttonsBlocked: false
0074             readonly property string middleDot: '·'
0075             readonly property var regexSpaceReplace: new RegExp(keyboard.space, "g")
0076 
0077             onToAlphaChanged: {
0078                 textInput.text = ''
0079                 morseConverter.alpha = ''
0080                 if(toAlpha)
0081                     keyboard.populateAlpha()
0082                 else
0083                     keyboard.populateMorse()
0084             }
0085 
0086             function start() {
0087                 if (!ApplicationInfo.isMobile)
0088                     textInput.forceActiveFocus();
0089                 items.currentLevel = Core.getInitialLevel(items.numberOfLevel);
0090                 initLevel()
0091             }
0092 
0093             function initLevel() {
0094                 errorRectangle.resetState();
0095                 // Reset the values on the text fields
0096                 toAlphaChanged();
0097                 score.currentSubLevel = 0
0098                 score.numberOfSubLevels = dataset[currentLevel].values[1].length
0099                 if(dataset[currentLevel].values[0] == '_random_') {
0100                     Core.shuffle(dataset[currentLevel].values[1]);
0101                 }
0102                 initSubLevel()
0103             }
0104 
0105             function initSubLevel() {
0106                 textInput.text = ''
0107                 stopMorseSounds();
0108                 questionValue = dataset[currentLevel].values[1][score.currentSubLevel]
0109                 questionValue = questionValue.replace(/\./g, items.middleDot);
0110                 questionValue = questionValue.replace(items.regexSpaceReplace, ' ');
0111                 activity.audioVoices.clearQueue();
0112                 // Play the audio at start of the sublevel
0113                 if(items.audioMode) {
0114                     delayTimer.restart();
0115                 }
0116                 items.buttonsBlocked = false;
0117             }
0118 
0119             function nextLevel() {
0120                 score.stopWinAnimation();
0121                 currentLevel = Core.getNextLevel(currentLevel, numberOfLevel);
0122                 initLevel();
0123             }
0124 
0125             function previousLevel() {
0126                 score.stopWinAnimation();
0127                 currentLevel = Core.getPreviousLevel(currentLevel, numberOfLevel);
0128                 initLevel();
0129             }
0130 
0131             function nextSubLevel() {
0132                 if(score.currentSubLevel >= score.numberOfSubLevels) {
0133                     bonus.good('tux');
0134                 }
0135                 else {
0136                     initSubLevel();
0137                 }
0138             }
0139 
0140             function check() {
0141                 items.buttonsBlocked = true;
0142                 if(feedback.value === items.questionValue) {
0143                     stopMorseSounds();
0144                     score.currentSubLevel++;
0145                     score.playWinAnimation();
0146                     items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/completetask.wav");
0147                 }
0148                 else {
0149                     stopMorseSounds();
0150                     errorRectangle.startAnimation();
0151                     items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/crash.wav");
0152                 }
0153             }
0154 
0155             function stopMorseSounds() {
0156                 // Reset soundList and stop running sound
0157                 delayTimer.stop();
0158                 ledContainer.soundList = [];
0159                 ledContainer.phraseRunning = false;
0160                 activity.stopSounds();
0161             }
0162         }
0163 
0164         onStart: {
0165             firstScreen.visible = true
0166             items.start()
0167         }
0168         onStop: {
0169             ledContainer.soundList = []
0170             activity.audioVoices.stop()
0171             activity.audioVoices.clearQueue()
0172         }
0173 
0174         Keys.enabled: !items.buttonsBlocked
0175         Keys.onPressed: {
0176             if ((event.key === Qt.Key_Enter) || (event.key === Qt.Key_Return)) {
0177                 if(firstScreen.visible) {
0178                     firstScreen.visible = false;
0179                 }
0180                 else {
0181                     items.check();
0182                 }
0183             }
0184         }
0185 
0186         QtObject {
0187             id: morseConverter
0188             property string alpha
0189             property string morse
0190             // TODO Need to double check the values just in case...
0191             property var table: {
0192              "A" : ".-", "B" : "-...", "C" : "-.-.", "D" : "-..",  "E" : ".", "F" : "..-.", "G" : "--.",
0193              "H" : "....", "I" : "..", "J" : ".---", "K" : "-.-",  "L" : ".-..","M" : "--","N" : "-.",
0194              "O" : "---", "P" : ".--.", "Q" : "--.-", "R" : ".-.",  "S" : "...","T" : "-", "U" : "..-",
0195              "V" : "...-", "W" : ".--",  "X" : "-..-",  "Y" : "-.--","Z" : "--..","1" : ".----","2" : "..---",
0196              "3" : "...--",  "4" : "....-", "5" : ".....", "6" : "-....",  "7" : "--...",  "8" : "---..",
0197              "9" : "----." , "0" : "-----"
0198             }
0199             function morse2alpha(str) {
0200                 var letters = ""
0201                 var input = []
0202                 input = str.split(' ')
0203                 if(input[0] === "") return ''
0204 
0205                 for(var index in input) {
0206                     for(var key in table) {
0207                         if(table[key] === input[index]) {
0208                             letters += key
0209                             continue;
0210                         }
0211                     }
0212                 }
0213 
0214                 if(!letters) return ''
0215                 return letters
0216             }
0217 
0218             function alpha2morse(str) {
0219                 var code = "";
0220 
0221                 for(var index in str) {
0222                     if(table[str[index]]) {
0223                         code += table[str[index]] + " ";
0224                     }
0225                     else {
0226                         code = "";
0227                         break;
0228                     }
0229                 }
0230                 code = code.trim();
0231                 return code
0232             }
0233 
0234             onAlphaChanged: morse = alpha2morse(alpha);
0235             onMorseChanged: alpha = morse2alpha(morse);
0236         }
0237 
0238         Rectangle {
0239             id: questionArea
0240             anchors.top: background.top
0241             anchors.left: background.left
0242             anchors.right: background.right
0243             anchors.margins: background.layoutMargins
0244             color: "#f2f2f2"
0245             radius: background.layoutMargins
0246             height: questionLabel.height + 20 * ApplicationInfo.ratio
0247             Rectangle {
0248                 anchors.centerIn: parent
0249                 width: parent.width - background.layoutMargins
0250                 height: parent.height - background.layoutMargins
0251                 color: "#f2f2f2"
0252                 radius: parent.radius
0253                 border.width: 3 * ApplicationInfo.ratio
0254                 border.color: "#9fb8e3"
0255                 GCText {
0256                     id: questionLabel
0257                     anchors.centerIn: parent
0258                     wrapMode: TextEdit.WordWrap
0259                     text: items.questionValue ? items.questionText.includes("%1") ? items.questionText.arg(items.questionValue) : items.questionText : ''
0260                     color: "#373737"
0261                     width: parent.width * 0.9
0262                     horizontalAlignment: Text.AlignHCenter
0263                 }
0264             }
0265         }
0266 
0267         Rectangle {
0268             id: inputArea
0269             anchors.top: questionArea.bottom
0270             anchors.left: background.left
0271             anchors.margins: background.layoutMargins
0272             color: "#f2f2f2"
0273             radius: background.layoutMargins
0274             width: questionArea.width * 0.5 - background.layoutMargins * 0.5
0275             height: textInput.height
0276             border.width: 1 * ApplicationInfo.ratio
0277             border.color: "#9fb8e3"
0278             TextInput {
0279                 id: textInput
0280                 x: parent.width / 2
0281                 width: parent.width
0282                 color: "#373737"
0283                 enabled: !firstScreen.visible && !items.buttonsBlocked
0284                 text: ''
0285                 // At best, 5 characters when looking for a letter (4 max + 1 space)
0286                 maximumLength: items.toAlpha ? items.questionValue.split(' ').length + 1 : 5 * items.questionValue.length
0287                 horizontalAlignment: Text.AlignHCenter
0288                 verticalAlignment: TextInput.AlignVCenter
0289                 anchors.horizontalCenter: parent.horizontalCenter
0290                 font.pointSize: questionLabel.pointSize
0291                 font.weight: Font.DemiBold
0292                 font.family: GCSingletonFontLoader.fontLoader.name
0293                 font.capitalization: ApplicationSettings.fontCapitalization
0294                 font.letterSpacing: ApplicationSettings.fontLetterSpacing
0295                 cursorVisible: true
0296                 wrapMode: TextInput.Wrap
0297                 // TODO Use RegularExpressionValidator when supporting Qt5.14 minimum
0298                 validator: RegExpValidator { regExp: items.toAlpha ?
0299                                                        /^[a-zA-Z0-9 ]+$/ :
0300                                                        /[\.\-\x00B7 ]+$/
0301                                            }
0302                 onTextChanged: {
0303                     if(text) {
0304                         text = text.replace(/\./g, items.middleDot);
0305                         text = text.replace(items.regexSpaceReplace, ' ');
0306                         text = text.toUpperCase();
0307                         if(items.toAlpha) {
0308                             morseConverter.alpha = text.replace(/\W/g , '');
0309                         }
0310                         else {
0311                             morseConverter.morse = text.replace(/·/g, '.');
0312                         }
0313                     }
0314                     else {
0315                         morseConverter.morse = "";
0316                         morseConverter.alpha = "";
0317                     }
0318                 }
0319 
0320                 function appendText(car) {
0321                     if(car === keyboard.backspace) {
0322                         if(text && cursorPosition > 0) {
0323                             var oldPos = cursorPosition
0324                             text = text.substring(0, cursorPosition - 1) + text.substring(cursorPosition)
0325                             cursorPosition = oldPos - 1
0326                         }
0327                         return
0328                     }
0329                     var oldPos = cursorPosition
0330                     text = text.substring(0, cursorPosition) + car + text.substring(cursorPosition)
0331                     cursorPosition = oldPos + 1
0332                 }
0333             }
0334         }
0335 
0336         Rectangle {
0337             id: feedbackArea
0338             anchors.top: questionArea.bottom
0339             anchors.margins: background.layoutMargins
0340             anchors.right: background.right
0341             width: inputArea.width
0342             height: inputArea.height
0343             color: "#f2f2f2"
0344             radius: background.layoutMargins
0345             border.width: 1 * ApplicationInfo.ratio
0346             border.color: "#9fb8e3"
0347 
0348             GCText {
0349                 id: feedback
0350                 anchors.horizontalCenter: parent.horizontalCenter
0351                 text: items.toAlpha ?
0352                 qsTr("Morse value: %1").arg(value) :
0353                 qsTr("Alphabet/Numeric value: %1").arg(value)
0354                 color: "#373737"
0355                 property string value: items.toAlpha ?
0356                                            morseConverter.morse.replace(/\./g, items.middleDot).replace(items.regexSpaceReplace, ' ')
0357  :
0358                                            morseConverter.alpha
0359                 verticalAlignment: Text.AlignVCenter
0360                 width: parent.width * 0.9
0361                 height: parent.height * 0.9
0362                 fontSizeMode: Text.Fit
0363                 minimumPointSize: 10
0364                 fontSize: mediumSize
0365             }
0366         }
0367 
0368         Item {
0369             id: layoutArea
0370             anchors.top: feedbackArea.bottom
0371             anchors.topMargin: background.layoutMargins
0372             anchors.left: inputArea.left
0373             anchors.right: feedbackArea.right
0374             anchors.bottom: bar.top
0375             anchors.bottomMargin: bar.height * 0.2
0376         }
0377 
0378         Rectangle {
0379             id: ledContainer
0380             visible: repeatItem.visible
0381             anchors.verticalCenter: layoutArea.verticalCenter
0382             anchors.right: repeatItem.left
0383             anchors.rightMargin: background.layoutMargins
0384             height: Math.min(70 * ApplicationInfo.ratio, layoutArea.height)
0385             width: height
0386             radius:background.layoutMargins
0387             color: "#f2f2f2"
0388             border.width: 1 * ApplicationInfo.ratio
0389             border.color: "#9fb8e3"
0390             property var soundList: []
0391             property bool phraseRunning: false
0392 
0393             Rectangle {
0394                 id: ledContour
0395                 anchors.centerIn: parent
0396                 width: Math.min(parent.width, parent.height) * 0.9
0397                 height: width
0398                 radius: width * 0.5
0399                 color: "#373737"
0400             }
0401             Image {
0402                 id: ledOff
0403                 source: "qrc:/gcompris/src/activities/morse_code/resource/ledOff.svg"
0404                 anchors.centerIn: ledContour
0405                 width: ledContour.width * 0.9
0406                 height: width
0407                 sourceSize.width: width
0408                 sourceSize.height: width
0409             }
0410             Image {
0411                 id: ledOn
0412                 source: "qrc:/gcompris/src/activities/morse_code/resource/ledOn.svg"
0413                 anchors.centerIn: ledContour
0414                 width: ledContour.width * 0.9
0415                 height: width
0416                 sourceSize.width: width
0417                 sourceSize.height: width
0418                 visible: false
0419             }
0420             SequentialAnimation {
0421                 id: dotAnim
0422                 PropertyAction { target: ledOn; property: "visible"; value: true }
0423                 ScriptAction { script: activity.audioVoices.append("qrc:/gcompris/src/activities/morse_code/resource/dot.wav") }
0424                 PauseAnimation { duration: 100 }
0425                 PropertyAction { target: ledOn; property: "visible"; value: false }
0426                 PauseAnimation { duration: 400 }
0427                 ScriptAction { script: ledContainer.playLedAnim() }
0428             }
0429             SequentialAnimation {
0430                 id: dashAnim
0431                 PropertyAction { target: ledOn; property: "visible"; value: true }
0432                 ScriptAction { script: activity.audioVoices.append("qrc:/gcompris/src/activities/morse_code/resource/dash.wav") }
0433                 PauseAnimation { duration: 300 }
0434                 PropertyAction { target: ledOn; property: "visible"; value: false }
0435                 PauseAnimation { duration: 200 }
0436                 ScriptAction { script: ledContainer.playLedAnim() }
0437             }
0438             SequentialAnimation {
0439                 id: silenceAnim
0440                 PropertyAction { target: ledOn; property: "visible"; value: false }
0441                 PauseAnimation { duration: 500 }
0442                 ScriptAction { script: ledContainer.playLedAnim() }
0443             }
0444             function playLedAnim() {
0445                 if(ledContainer.soundList.length > 0) {
0446                     var soundType = soundList.shift();
0447                     if (soundType === "dot.wav") {
0448                         dotAnim.restart();
0449                     } else if (soundType === "dash.wav") {
0450                         dashAnim.restart();
0451                     } else if (soundType === "silence.wav") {
0452                         silenceAnim.restart();
0453                     }
0454                 } else {
0455                     ledContainer.phraseRunning = false;
0456                 }
0457             }
0458         }
0459 
0460         ErrorRectangle {
0461             id: errorRectangle
0462             anchors.fill: inputArea
0463             radius: inputArea.radius
0464             imageSize: height * 1.5
0465             function releaseControls() {
0466                 items.buttonsBlocked = false;
0467             }
0468         }
0469 
0470         Timer {
0471             id: delayTimer
0472             interval: 1000
0473             repeat: false
0474             running: false
0475             onTriggered: repeatItem.clicked()
0476         }
0477 
0478         MorseMap {
0479             id: morseMap
0480             visible: false
0481             onClose: home()
0482         }
0483 
0484         Score {
0485             id: score
0486             visible: !firstScreen.visible
0487             anchors.right: layoutArea.right
0488             anchors.verticalCenter: layoutArea.verticalCenter
0489             anchors.bottom: undefined
0490             currentSubLevel: 0
0491             numberOfSubLevels: 1
0492             onStop: items.nextSubLevel()
0493         }
0494 
0495 
0496         DialogChooseLevel {
0497             id: dialogActivityConfig
0498             currentActivity: activity.activityInfo
0499 
0500             onSaveData: {
0501                 levelFolder = dialogActivityConfig.chosenLevels
0502                 currentActivity.currentLevels = dialogActivityConfig.chosenLevels
0503                 ApplicationSettings.setCurrentLevels(currentActivity.name, dialogActivityConfig.chosenLevels)
0504             }
0505             onClose: {
0506                 home()
0507             }
0508             onStartActivity: {
0509                 background.stop()
0510                 background.start()
0511             }
0512         }
0513 
0514         DialogHelp {
0515             id: dialogHelp
0516             onClose: home()
0517         }
0518 
0519         VirtualKeyboard {
0520             id: keyboard
0521             anchors.bottom: parent.bottom
0522             anchors.horizontalCenter: parent.horizontalCenter
0523             visible: !firstScreen.visible
0524             enabled: visible && !items.buttonsBlocked
0525             function populateAlpha() {
0526                 layout = [ [
0527                               { label: "0" },
0528                               { label: "1" },
0529                               { label: "2" },
0530                               { label: "3" },
0531                               { label: "4" },
0532                               { label: "5" },
0533                               { label: "6" },
0534                               { label: "7" },
0535                               { label: "8" },
0536                               { label: "9" }
0537                            ],
0538                            [
0539                               { label: "A" },
0540                               { label: "B" },
0541                               { label: "C" },
0542                               { label: "D" },
0543                               { label: "E" },
0544                               { label: "F" },
0545                               { label: "G" },
0546                               { label: "H" },
0547                               { label: "I" },
0548                               { label: "J" },
0549                               { label: "K" },
0550                               { label: "L" },
0551                               { label: "M" }
0552                            ],
0553                            [
0554                               { label: "N" },
0555                               { label: "O" },
0556                               { label: "P" },
0557                               { label: "Q" },
0558                               { label: "R" },
0559                               { label: "S" },
0560                               { label: "T" },
0561                               { label: "U" },
0562                               { label: "V" },
0563                               { label: "W" },
0564                               { label: "X" },
0565                               { label: "Y" },
0566                               { label: "Z" },
0567                               { label: keyboard.space },
0568                               { label: keyboard.backspace }
0569 
0570                            ]
0571                          ]
0572             }
0573 
0574             function populateMorse() {
0575                 layout = [ [
0576                     { label: items.middleDot },
0577                     { label: "-" },
0578                     { label: keyboard.space },
0579                     { label: keyboard.backspace }
0580                 ] ]
0581             }
0582 
0583             onKeypress: {
0584                 if(!items.buttonsBlocked) {
0585                     textInput.appendText(text)
0586                 }
0587                 // Set the focus back to the InputText for keyboard input
0588                 resetFocus();
0589             }
0590             onError: console.log("VirtualKeyboard error: " + msg);
0591         }
0592 
0593         FirstScreen {
0594             id: firstScreen
0595             visible: true
0596         }
0597 
0598         Bar {
0599             id: bar
0600             level: items.currentLevel + 1
0601             anchors.bottom: keyboard.top
0602             content: BarEnumContent {
0603                 value: !firstScreen.visible ? (help | home | level | hint | activityConfig) : (help | home)
0604             }
0605             onHelpClicked: {
0606                 displayDialog(dialogHelp)
0607             }
0608             onActivityConfigClicked: {
0609                 displayDialog(dialogActivityConfig)
0610             }
0611             onPreviousLevelClicked: items.previousLevel()
0612             onNextLevelClicked: items.nextLevel()
0613             onHomeClicked: activity.home()
0614             onHintClicked: feedbackArea.visible = !feedbackArea.visible
0615         }
0616         BarButton {
0617             id: repeatItem
0618             source: "qrc:/gcompris/src/core/resource/bar_repeat.svg"
0619             height: ledContainer.height
0620             width: height
0621             sourceSize.height: height
0622             sourceSize.width: height
0623             visible: !firstScreen.visible && items.audioMode
0624             anchors {
0625                 verticalCenter: layoutArea.verticalCenter
0626                 right: showMapButton.left
0627                 rightMargin: background.layoutMargins
0628             }
0629             mouseArea.enabled: ledContainer.phraseRunning == false
0630             mouseArea.hoverEnabled: ledContainer.phraseRunning == false
0631             onClicked: {
0632                 ledContainer.soundList = []
0633                 for(var f = 0 ; f < items.questionValue.length; ++ f) {
0634                     var letter = items.questionValue[f];
0635                     // If the character to play is a letter, we convert it to morse
0636                     if(".·- ".indexOf(items.questionValue[f]) === -1) {
0637                         letter = morseConverter.alpha2morse(items.questionValue[f]);
0638                     }
0639                     // We play each character, one after the other
0640                     for(var i = 0 ; i < letter.length; ++ i) {
0641                         if(letter[i] === '-') {
0642                             ledContainer.soundList.push("dash.wav");
0643                         }
0644                         else if(letter[i] === '.' || letter[i] === '·') {
0645                             ledContainer.soundList.push("dot.wav");
0646                         }
0647                     }
0648                     // Add a silence after each letter
0649                     ledContainer.soundList.push("silence.wav")
0650                 }
0651                 ledContainer.phraseRunning = true;
0652                 ledContainer.playLedAnim();
0653             }
0654         }
0655 
0656         BarButton {
0657             id: okButton
0658             source: "qrc:/gcompris/src/core/resource/bar_ok.svg";
0659             visible: !firstScreen.visible
0660             anchors.right: score.left
0661             anchors.verticalCenter: layoutArea.verticalCenter
0662             anchors.rightMargin: background.layoutMargins
0663             enabled: !items.buttonsBlocked
0664             height: ledContainer.height
0665             width: height
0666             sourceSize.height: height
0667             sourceSize.width: height
0668             onClicked: items.check()
0669         }
0670 
0671         BarButton {
0672             id: showMapButton
0673             source: "qrc:/gcompris/src/activities/morse_code/resource/morseButton.svg"
0674             visible: !firstScreen.visible
0675             anchors.right: okButton.left
0676             anchors.verticalCenter: layoutArea.verticalCenter
0677             anchors.rightMargin: background.layoutMargins
0678             enabled: !items.buttonsBlocked
0679             height: ledContainer.height
0680             width: height
0681             sourceSize.height: height
0682             sourceSize.width: height
0683             onClicked: {
0684                 morseMap.visible = true
0685                 displayDialog(morseMap)
0686             }
0687         }
0688 
0689         Bonus {
0690             id: bonus
0691             Component.onCompleted: win.connect(items.nextLevel)
0692         }
0693     }
0694 }