diff --git a/src/activities/activities.txt b/src/activities/activities.txt index 7ae3fb7b1..e2cbf8380 100644 --- a/src/activities/activities.txt +++ b/src/activities/activities.txt @@ -1,105 +1,107 @@ # The list of activities that will be loaded at GCompris start. # Keep it sorted advanced_colors algebra_by algebra_div algebra_minus algebra_plus algorithm align4 align4-2players alphabet-sequence babymatch babyshapes ballcatch braille_alphabets braille_fun canal_lock chronos -clickanddraw -clickgame click_on_letter click_on_letter_up +clickanddraw +clickgame clockgame color_mix color_mix_light colors details drawnumber enumerate erase erase_2clic erase_clic fifteen followline football geo-country geography gletters gnumch-equality gnumch-factors gnumch-inequality gnumch-multiples gnumch-primes guessnumber hanoi hanoi_real hexagon imageid imagename instruments intro_gravity leftright lightsoff louis-braille magic-hat-minus magic-hat-plus maze mazeinvisible mazerelative memory memory-enumerate memory-math-add memory-math-add-minus memory-math-add-minus-mult-div memory-math-add-minus-mult-div-tux memory-math-add-minus-tux memory-math-add-tux memory-math-div memory-math-div-tux memory-math-minus memory-math-minus-tux memory-math-mult memory-math-mult-div memory-math-mult-div-tux memory-math-mult-tux memory-sound memory-sound-tux memory-tux memory-wordnumber mining missing-letter money money_back money_back_cents money_cents mosaic numbers-odd-even penalty planegame +readingh +readingv redraw redraw_symmetrical reversecount scalesboard scalesboard_weight scalesboard_weight_avoirdupois simplepaint smallnumbers smallnumbers2 sudoku superbrain target tic_tac_toe tic_tac_toe_2players traffic wordsgame diff --git a/tools/menus/readingh.qml b/src/activities/readingh/ActivityInfo.qml similarity index 87% rename from tools/menus/readingh.qml rename to src/activities/readingh/ActivityInfo.qml index 4f7c595fd..2121b3a29 100644 --- a/tools/menus/readingh.qml +++ b/src/activities/readingh/ActivityInfo.qml @@ -1,33 +1,34 @@ /* GCompris - ActivityInfo.qml * - * Copyright (C) 2015 Your Name + * Copyright (C) 2015 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 GCompris 1.0 ActivityInfo { name: "readingh/Readingh.qml" difficulty: 2 icon: "readingh/readingh.svg" - author: "Bruno Coudoin " - demo: false + author: "Johnny Jazeix <jazeix@gmail.com>" + demo: true title: qsTr("Horizontal reading practice") - description: qsTr("Read a list of words and work out if a given word is in it") + description: qsTr("Read a list of words and work out if a given word is in it.") goal: qsTr("Reading training in a limited time") prerequisite: qsTr("Reading") manual: qsTr("A word is shown at the top right of the board. A list of words will appear and disappear on the left. Does the given word belong to the list?") credit: "" - section: "/reading" + section: "reading" } + diff --git a/src/activities/readingh/CMakeLists.txt b/src/activities/readingh/CMakeLists.txt new file mode 100644 index 000000000..7f5f7d3a9 --- /dev/null +++ b/src/activities/readingh/CMakeLists.txt @@ -0,0 +1 @@ +GCOMPRIS_ADD_RCC(activities/readingh *.qml *.svg *.js resource/*) diff --git a/src/activities/readingh/Readingh.qml b/src/activities/readingh/Readingh.qml new file mode 100644 index 000000000..6943f7823 --- /dev/null +++ b/src/activities/readingh/Readingh.qml @@ -0,0 +1,322 @@ +/* GCompris - readingh.qml + * + * Copyright (C) 2015 Johnny Jazeix + * + * Authors: + * Bruno Coudoin (GTK+ version) + * Johnny Jazeix (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 QtQuick.Controls 1.2 +import GCompris 1.0 + +import "../../core" +import "readingh.js" as Activity + +ActivityBase { + id: activity + + onStart: focus = true + onStop: {} + + /* mode of the activity, "readingh" (horizontal) or "readingv" (vertical):*/ + property string mode: "readingh" + + pageComponent: Image { + id: background + anchors.fill: parent + source: Activity.url + "reading-bg.svg" + signal start + signal stop + sourceSize.width: parent.width + fillMode: Image.Stretch + + Component.onCompleted: { + dialogActivityConfig.getInitialConfiguration() + activity.start.connect(start) + activity.stop.connect(stop) + } + + // system locale by default + property string locale: "system" + + // Add here the QML items you need to access in javascript + QtObject { + id: items + property Item main: activity.main + property alias background: background + property alias bar: bar + property alias bonus: bonus + property alias wordlist: wordlist + property alias wordDropTimer: wordDropTimer + property alias locale: background.locale + property alias iAmReady: iAmReady + property alias answerButtonFound: answerButtonFound + property alias answerButtonNotFound: answerButtonNotFound + property alias answerButtonsFlow: answerButtonsFlow + property alias wordDisplayRepeater: wordDisplayRepeater + property string textToFind + property int currentIndex + } + + onStart: { Activity.start(items, mode) } + onStop: { Activity.stop() } + + DialogActivityConfig { + id: dialogActivityConfig + currentActivity: activity + content: Component { + Item { + property alias localeBox: localeBox + property alias leftToRightBox: leftToRightBox + height: column.height + + property alias availableLangs: langs.languages + LanguageList { + id: langs + } + + Column { + id: column + spacing: 10 + width: parent.width + + Flow { + spacing: 5 + width: dialogActivityConfig.width + GCComboBox { + id: localeBox + model: langs.languages + background: dialogActivityConfig + width: dialogActivityConfig.width + label: qsTr("Select your locale") + } + } + GCDialogCheckBox { + id: leftToRightBox + width: 300 * ApplicationInfo.ratio + text: qsTr("Left to right mode") + checked: wordDisplayList.layoutDirection == Qt.LeftToRight + } + } + } + } + + onClose: home() + onLoadData: { + if(dataToSave) { + if(dataToSave["locale"]) { + background.locale = dataToSave["locale"]; + } + if(dataToSave["leftToRight"] == false) { + wordDisplayList.layoutDirection = Qt.RightToLeft; + } + else { // true or undefined (default value) + wordDisplayList.layoutDirection = Qt.LeftToRight; + } + } + } + onSaveData: { + var oldLocale = background.locale; + var newLocale = dialogActivityConfig.configItem.availableLangs[dialogActivityConfig.loader.item.localeBox.currentIndex].locale; + var leftToRightMode = dialogActivityConfig.loader.item.leftToRightBox.checked; + // Remove .UTF-8 + if(newLocale.indexOf('.') != -1) { + newLocale = newLocale.substring(0, newLocale.indexOf('.')) + } + dataToSave = { + "locale": newLocale, + "leftToRight": leftToRightMode + } + + background.locale = newLocale; + wordDisplayList.layoutDirection = leftToRightMode ? Qt.LeftToRight : Qt.RightToLeft; + + // Restart the activity with new information + if(oldLocale !== newLocale) { + background.stop(); + background.start(); + } + } + + function setDefaultValues() { + var localeUtf8 = background.locale; + if(background.locale != "system") { + localeUtf8 += ".UTF-8"; + } + + for(var i = 0 ; i < dialogActivityConfig.configItem.availableLangs.length ; i ++) { + if(dialogActivityConfig.configItem.availableLangs[i].locale === localeUtf8) { + dialogActivityConfig.loader.item.localeBox.currentIndex = i; + break; + } + } + } + } + + DialogHelp { + id: dialogHelp + onClose: home() + } + + Bar { + id: bar + content: BarEnumContent { value: help | home | level | config } + onHelpClicked: { + displayDialog(dialogHelp) + } + onPreviousLevelClicked: Activity.previousLevel() + onNextLevelClicked: Activity.nextLevel() + onHomeClicked: activity.home() + onConfigClicked: { + dialogActivityConfig.active = true + dialogActivityConfig.setDefaultValues() + displayDialog(dialogActivityConfig) + } + } + + Bonus { + id: bonus + // Do not pass automatically at next level, allowing the child to do more than one try, or add sublevels? + Component.onCompleted: { + win.connect(resetClickInProgress) + loose.connect(resetClickInProgress) + } + } + // used to know if we already click on "Yes" or "No" + property bool isClickInProgress: false + // used to avoid multiple clicks between the begin and end of bonus play + property bool isClickInProgress2: false + function resetClickInProgress() { + isClickInProgress = false; + isClickInProgress2 = false; + Activity.initLevel() + } + + Flow { + id: wordDisplayList + spacing: 20 + x: 70/800*parent.width + y: 100/600*parent.height + width: 350/800*parent.width-x + height: 520/600*parent.height-y + flow: mode == "readingh" ? Flow.LeftToRight : Flow.TopToBottom + layoutDirection: Qt.LeftToRight + + Repeater { + id: wordDisplayRepeater + model: Activity.words + property int idToHideBecauseOverflow: 0 + delegate: GCText { + text: modelData + opacity: iAmReady.visible ? false : (index == items.currentIndex ? 1 : 0) + + onOpacityChanged: { + /* Handle case where we go over the image + On these cases, we hide all above items to restart to 0 + As we don't replay the same level and always replace the model, + we do not care about restoring visible to true */ + if((x+width > wordDisplayList.width) || + (y+height > wordDisplayList.height)) { + var i = wordDisplayRepeater.idToHideBecauseOverflow; + for(; i < index; ++i) { + wordDisplayRepeater.itemAt(i).visible=false + } + wordDisplayRepeater.idToHideBecauseOverflow = i + } + } + } + } + } + + GCText { + id: wordToFindBox + x: 430/800*parent.width + y: 90/600*parent.height + text: qsTr("Check if the word
%1
is displayed").arg(items.textToFind) + color: "black" + horizontalAlignment: Text.AlignHCenter + width: background.width/3 + height: background.height/5 + fontSizeMode: Text.Fit + } + + ReadyButton { + id: iAmReady + onClicked: Activity.run() + x: background.width / 2 + y: background.height / 2 + anchors.verticalCenter: undefined + anchors.horizontalCenter: undefined + } + Flow { + id: answerButtonsFlow + x: iAmReady.x + y: iAmReady.y + width: wordToFindBox.width + AnswerButton { + id : answerButtonFound + width: Math.min(250 * ApplicationInfo.ratio, background.width/2-10) + height: 80 * ApplicationInfo.ratio + textLabel: qsTr("Yes, I saw it!") + isCorrectAnswer: Activity.words ? Activity.words.indexOf(items.textToFind) != -1 : false + onCorrectlyPressed: if(isClickInProgress && !isClickInProgress2) { bonus.good("flower"); isClickInProgress2 = true } + onIncorrectlyPressed: if(isClickInProgress && !isClickInProgress2) { bonus.bad("flower"); isClickInProgress2 = true } + onPressed: { + if(!isClickInProgress) { + isClickInProgress = true + } + } + } + + AnswerButton { + id : answerButtonNotFound + width: Math.min(250 * ApplicationInfo.ratio, background.width/2-10) + height: 80 * ApplicationInfo.ratio + textLabel: qsTr("No, it was not there!") + isCorrectAnswer: !answerButtonFound.isCorrectAnswer + onCorrectlyPressed: if(isClickInProgress && !isClickInProgress2) { bonus.good("flower"); isClickInProgress2 = true } + onIncorrectlyPressed: if(isClickInProgress && !isClickInProgress2) { bonus.bad("flower"); isClickInProgress2 = true } + + onPressed: { + if(!isClickInProgress) { + isClickInProgress = true + } + } + } + } + + Wordlist { + id: wordlist + defaultFilename: Activity.dataSetUrl + "default-en.json" + // To switch between locales: xx_XX stored in configuration and + // possibly correct xx if available (ie fr_FR for french but dataset is fr.) + useDefault: false + filename: "" + + onError: console.log("Reading: Wordlist error: " + msg); + } + + Timer { + id: wordDropTimer + repeat: true + interval: 1000 + onTriggered: Activity.dropWord(); + } + + } + +} diff --git a/src/activities/readingh/readingh.js b/src/activities/readingh/readingh.js new file mode 100644 index 000000000..d3735dbe1 --- /dev/null +++ b/src/activities/readingh/readingh.js @@ -0,0 +1,118 @@ +/* GCompris - readingh.js + * + * Copyright (C) 2015 Johnny Jazeix + * + * Authors: + * Bruno Coudoin (GTK+ version) + * Johnny Jazeix (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 //for ApplicationInfo +.import "qrc:/gcompris/src/core/core.js" as Core + +var currentLevel = 0 +var items +var maxLevel + +var url = "qrc:/gcompris/src/activities/readingh/resource/" +var dataSetUrl= "qrc:/gcompris/src/activities/wordsgame/resource/" + +// +var level +// words to display +var words + +function start(items_) { + items = items_ + currentLevel = 0 + var locale = items.locale == "system" ? "$LOCALE" : items.locale + + items.wordlist.loadFromFile(GCompris.ApplicationInfo.getLocaleFilePath( + dataSetUrl + "default-"+locale+".json")); + // If wordlist is empty, we try to load from short locale and if not present again, we switch to default one + var localeUnderscoreIndex = locale.indexOf('_') + // probably exist a better way to see if the list is empty + if(items.wordlist.maxLevel == 0) { + var localeShort; + // We will first look again for locale xx (without _XX if exist) + if(localeUnderscoreIndex > 0) { + localeShort = locale.substring(0, localeUnderscoreIndex) + } + else { + localeShort = locale; + } + // If not found, we will use the default file + items.wordlist.useDefault = true + items.wordlist.loadFromFile(GCompris.ApplicationInfo.getLocaleFilePath( + dataSetUrl + "default-"+localeShort+".json")); + // We remove the using of default file for next time we enter this function + items.wordlist.useDefault = false + } + maxLevel = items.wordlist.maxLevel; + initLevel(); +} + +function stop() { + items.wordDropTimer.stop(); +} + +function initLevel() { + items.bar.level = currentLevel + 1; + items.wordDropTimer.stop(); + items.answerButtonsFlow.visible = false; + + // initialize level + level = items.wordlist.getLevelWordList(currentLevel + 1); + items.wordlist.initRandomWord(currentLevel + 1) + items.textToFind = items.wordlist.getRandomWord() + Core.shuffle(level.words) + words = level.words.slice(0, 15) + items.currentIndex = -1 + + items.wordDisplayRepeater.model = words + items.wordDisplayRepeater.idToHideBecauseOverflow = 0 + items.answerButtonFound.isCorrectAnswer = words.indexOf(items.textToFind) != -1 + items.iAmReady.visible = true +} + +function nextLevel() { + if(maxLevel <= ++currentLevel) { + currentLevel = 0 + } + initLevel(); +} + +function previousLevel() { + if(--currentLevel < 0) { + currentLevel = maxLevel - 1 + } + initLevel(); +} + +function run() { + items.wordDropTimer.start(); +} + +function dropWord() { + if(++items.currentIndex < words.length) { + // Display next word + } + else { + items.wordDropTimer.stop(); + items.answerButtonsFlow.visible = true + } +} diff --git a/tools/menus/resource/readingh.svg b/src/activities/readingh/readingh.svg similarity index 100% rename from tools/menus/resource/readingh.svg rename to src/activities/readingh/readingh.svg diff --git a/src/activities/readingh/resource/reading-bg.svg b/src/activities/readingh/resource/reading-bg.svg new file mode 100644 index 000000000..b0cc9c1a9 --- /dev/null +++ b/src/activities/readingh/resource/reading-bg.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/tools/menus/readingv.qml b/src/activities/readingv/ActivityInfo.qml similarity index 87% rename from tools/menus/readingv.qml rename to src/activities/readingv/ActivityInfo.qml index 1923ed7a6..d93367ebd 100644 --- a/tools/menus/readingv.qml +++ b/src/activities/readingv/ActivityInfo.qml @@ -1,33 +1,34 @@ /* GCompris - ActivityInfo.qml * - * Copyright (C) 2015 Your Name + * Copyright (C) 2015 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 GCompris 1.0 ActivityInfo { name: "readingv/Readingv.qml" difficulty: 2 icon: "readingv/reading.svg" - author: "Bruno Coudoin " - demo: false + author: "Johnny Jazeix <jazeix@gmail.com>" + demo: true title: qsTr("Vertical-reading practice") - description: qsTr("Read a vertical list of words and work out if a given word is in it") + description: qsTr("Read a vertical list of words and work out if a given word is in it.") goal: qsTr("Read training in a limited time") prerequisite: qsTr("Reading") manual: qsTr("A word is shown at the top right of the board. A list of words will appear and disappear on the left. Does the given word belong to the list?") credit: "" - section: "/reading" + section: "reading" } + diff --git a/src/activities/readingv/CMakeLists.txt b/src/activities/readingv/CMakeLists.txt new file mode 100644 index 000000000..c8a37b49a --- /dev/null +++ b/src/activities/readingv/CMakeLists.txt @@ -0,0 +1 @@ +GCOMPRIS_ADD_RCC(activities/readingv *.qml *.svg) diff --git a/src/activities/readingv/Readingv.qml b/src/activities/readingv/Readingv.qml new file mode 100644 index 000000000..f4eb03b00 --- /dev/null +++ b/src/activities/readingv/Readingv.qml @@ -0,0 +1,28 @@ +/* GCompris - readingv.qml + * + * Copyright (C) 2015 Johnny Jazeix + * + * Authors: + * Bruno Coudoin (GTK+ version) + * Johnny Jazeix (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 "../readingh" + +Readingh { + mode: "readingv" +} diff --git a/tools/menus/resource/reading.svg b/src/activities/readingv/reading.svg similarity index 100% rename from tools/menus/resource/reading.svg rename to src/activities/readingv/reading.svg diff --git a/src/core/AnswerButton.qml b/src/core/AnswerButton.qml index ef8402730..4a656e772 100644 --- a/src/core/AnswerButton.qml +++ b/src/core/AnswerButton.qml @@ -1,189 +1,189 @@ /* Copyed in GCompris from Touch'n'learn Touch'n'learn - Fun and easy mobile lessons for kids Copyright (C) 2010, 2011 by Alessandro Portale http://touchandlearn.sourceforge.net This file is part of Touch'n'learn Touch'n'learn 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. Touch'n'learn 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 Touch'n'learn; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ import QtQuick 2.1 import GCompris 1.0 Item { id: button property string textLabel property bool isCorrectAnswer: false property color normalStateColor: "#fff" property color correctStateColor: "#ffa" property color wrongStateColor: "#f66" property bool blockClicks: false property int wrongAnswerShakeAmplitudeCalc: width * 0.2 property int wrongAnswerShakeAmplitudeMin: 45 property int wrongAnswerShakeAmplitude: wrongAnswerShakeAmplitudeCalc < wrongAnswerShakeAmplitudeMin ? wrongAnswerShakeAmplitudeMin : wrongAnswerShakeAmplitudeCalc // If you want the sound effects just pass the audioEffects property GCAudio audioEffects signal correctlyPressed signal incorrectlyPressed signal pressed onPressed: { if (!blockClicks) { if (isCorrectAnswer) { if(audioEffects) audioEffects.play("qrc:/gcompris/src/core/resource/sounds/win.wav") correctAnswerAnimation.start(); } else { if(audioEffects) audioEffects.play("qrc:/gcompris/src/core/resource/sounds/crash.wav") wrongAnswerAnimation.start(); } } } Rectangle { id: rect anchors.fill: parent color: normalStateColor opacity: 0.5 } ParticleSystemStarLoader { id: particles } Image { source: "qrc:/gcompris/src/core/resource/button.svg" sourceSize { height: parent.height; width: parent.width } width: sourceSize.width height: sourceSize.height smooth: false } GCText { id: label anchors.verticalCenter: parent.verticalCenter // We need to manually horizonally center the text, because in wrongAnswerAnimation, // the x of the text is changed, which would not work if we use an anchor layout. property int horizontallyCenteredX: (button.width - width) >> 1; x: horizontallyCenteredX; - fontSize: 18 + fontSizeMode: Text.Fit font.bold: true text: textLabel } MouseArea { id: mouseArea anchors.fill: parent onPressed: button.pressed() } SequentialAnimation { id: correctAnswerAnimation ScriptAction { script: { if (typeof(feedback) === "object") feedback.playCorrectSound(); blockClicks = true; if (typeof(particles) === "object") particles.burst(40); } } PropertyAction { target: rect property: "color" value: correctStateColor } PropertyAnimation { target: rect property: "color" to: normalStateColor duration: 700 } PauseAnimation { duration: 300 // Wait for particles to finish } ScriptAction { script: { blockClicks = false; correctlyPressed(); } } } SequentialAnimation { id: wrongAnswerAnimation ParallelAnimation { SequentialAnimation { PropertyAction { target: rect property: "color" value: wrongStateColor } ScriptAction { script: { if (typeof(feedback) === "object") feedback.playIncorrectSound(); } } PropertyAnimation { target: rect property: "color" to: normalStateColor duration: 600 } } SequentialAnimation { PropertyAnimation { target: label property: "x" to: label.horizontallyCenteredX - wrongAnswerShakeAmplitude easing.type: Easing.InCubic duration: 120 } PropertyAnimation { target: label property: "x" to: label.horizontallyCenteredX + wrongAnswerShakeAmplitude easing.type: Easing.InOutCubic duration: 220 } PropertyAnimation { target: label property: "x" to: label.horizontallyCenteredX easing { type: Easing.OutBack; overshoot: 3 } duration: 180 } } } PropertyAnimation { target: rect property: "color" to: normalStateColor duration: 450 } ScriptAction { script: { incorrectlyPressed(); } } } }