diff --git a/src/activities/piano_composition/ActivityInfo.qml b/src/activities/piano_composition/ActivityInfo.qml --- a/src/activities/piano_composition/ActivityInfo.qml +++ b/src/activities/piano_composition/ActivityInfo.qml @@ -42,20 +42,8 @@ - enter/return: OK button - space bar: play - number keys: - - 1: C - - 2: D - - 3: E - - 4: F - - 5: G - - 6: A - - 7: B - - 8: C (higher octave) - - etc. - - F1: C# / Db - - F2: D# / Eb - - F3: F# / Gb - - F4: G# / Ab - - F5: A# / Bb + - 1 to 8: Corresponding white keys in the order on the displayed octave. + - F1 to F5: Corresponding black keys in the order on the displayed octave. ") credit: "" section: "discovery sound_group" diff --git a/src/activities/piano_composition/LyricsArea.qml b/src/activities/piano_composition/LyricsArea.qml --- a/src/activities/piano_composition/LyricsArea.qml +++ b/src/activities/piano_composition/LyricsArea.qml @@ -112,7 +112,7 @@ lyricsArea.title = "" lyricsArea.origin = "" lyricsArea.lyrics = "" - lyricsOrPianoModeOption.currentIndex = 0 + optionsRow.lyricsOrPianoModeIndex = 0 } function setLyrics(title, origin, lyrics) { @@ -120,6 +120,6 @@ lyricsArea.title = title lyricsArea.origin = origin lyricsArea.lyrics = lyrics - lyricsOrPianoModeOption.currentIndex = 1 + optionsRow.lyricsOrPianoModeIndex = 1 } } diff --git a/src/activities/piano_composition/MultipleStaff.qml b/src/activities/piano_composition/MultipleStaff.qml --- a/src/activities/piano_composition/MultipleStaff.qml +++ b/src/activities/piano_composition/MultipleStaff.qml @@ -23,7 +23,7 @@ import GCompris 1.0 import "../../core" -import "piano_composition.js" as Activity +import "qrc:/gcompris/src/activities/piano_composition/NoteNotations.js" as NoteNotations Item { id: multipleStaff @@ -39,10 +39,14 @@ // Stores the note number which is to be replaced. property int noteToReplace: -1 property bool noteIsColored + property bool noteHoverEnabled: true + property bool centerNotesPosition: false property bool isMetronomeDisplayed: false + readonly property bool isMusicPlaying: musicTimer.running property alias flickableStaves: flickableStaves property real flickableTopMargin: multipleStaff.height / 14 + distanceBetweenStaff / 2 + property bool isFlickable: true signal noteClicked(string noteName, string noteType, int noteIndex) signal pushToUndoStack(int noteIndex, string oldNoteName, string oldNoteType) @@ -54,6 +58,7 @@ Flickable { id: flickableStaves + interactive: multipleStaff.isFlickable flickableDirection: Flickable.VerticalFlick contentWidth: staffColumn.width contentHeight: staffColumn.height + 1.5 * distanceBetweenStaff @@ -78,7 +83,7 @@ } } - property real newNoteWidth: (multipleStaff.width - 15 - staves.itemAt(0).clefImageWidth) / 10 + property real noteWidth: (multipleStaff.width - 15 - staves.itemAt(0).clefImageWidth) / 10 Repeater { id: notesRepeater model: notesModel @@ -87,14 +92,16 @@ noteType: noteType_ highlightWhenPlayed: highlightWhenPlayed noteIsColored: multipleStaff.noteIsColored - width: flickableStaves.newNoteWidth + width: flickableStaves.noteWidth height: multipleStaff.height / 5 readonly property int currentStaffNb: index / nbMaxNotesPerStaff + readonly property real defaultX: staves.itemAt(0).clefImageWidth + ((index % nbMaxNotesPerStaff) * flickableStaves.noteWidth) + readonly property real centeredPosition: (multipleStaff.width / 2.5 - (flickableStaves.noteWidth * notesModel.count / 2) + defaultX) - x: staves.itemAt(0).clefImageWidth + ((index % nbMaxNotesPerStaff) * flickableStaves.newNoteWidth) + x: multipleStaff.centerNotesPosition ? centeredPosition : defaultX - noteDetails: Activity.getNoteDetails(noteName, noteType) + noteDetails: multipleStaff.getNoteDetails(noteName, noteType) MouseArea { id: noteMouseArea @@ -126,6 +133,22 @@ } } + function getNoteDetails(noteName, noteType) { + var notesDetails = NoteNotations.get() + var clef = background.clefType === 'treble' ? "Treble" : "Bass" + var noteNotation + if(noteType === "Rest") + noteNotation = noteName + noteType + else + noteNotation = clef + noteName + console.log(noteNotation) + for(var i = 0; i < notesDetails.length; i++) { + if(noteNotation === notesDetails[i].noteName) { + return notesDetails[i] + } + } + } + function calculateTimerDuration(noteType) { noteType = noteType.toLowerCase() if(noteType === "whole") @@ -240,7 +263,7 @@ else audioPitchType = "bass" var noteToPlay = "qrc:/gcompris/src/activities/piano_composition/resource/" + audioPitchType + "_pitches/" + noteName + ".wav" - console.log(noteToPlay) + console.log("Path for playing note audio --> " + noteToPlay) items.audioEffects.play(noteToPlay) } } @@ -277,6 +300,15 @@ } } + function indicateAnsweredNote(isCorrectAnswer, noteIndexAnswered) { + notesRepeater.itemAt(noteIndexAnswered).noteAnswered = true + notesRepeater.itemAt(noteIndexAnswered).isCorrectlyAnswered = isCorrectAnswer + } + + function revertAnswer(noteIndexReverting) { + notesRepeater.itemAt(noteIndexReverting).noteAnswered = false + } + function play() { musicTimer.currentNote = 0 musicTimer.interval = 500 diff --git a/src/activities/piano_composition/Note.qml b/src/activities/piano_composition/Note.qml --- a/src/activities/piano_composition/Note.qml +++ b/src/activities/piano_composition/Note.qml @@ -57,6 +57,9 @@ property var noteDetails readonly property int notesPositionBelowStaffWithBars: 6 + property bool noteAnswered: false + property bool isCorrectlyAnswered: false + rotation: { if((noteDetails === undefined) || ((noteDetails.positionOnStaff < 0) && (noteType === "Whole"))) return 0 @@ -69,12 +72,12 @@ Image { id: blackTypeImage source: blackType !== "" ? "qrc:/gcompris/src/activities/piano_composition/resource/black" + blackType + ".svg" : "" - sourceSize.width: noteImage.width / 2.5 + sourceSize.width: noteImage.width / 2 anchors.right: parent.rotation === 180 ? undefined : noteImage.left anchors.left: parent.rotation === 180 ? noteImage.right : undefined rotation: parent.rotation === 180 ? 180 : 0 - anchors.rightMargin: -12 - anchors.leftMargin: -18 + anchors.rightMargin: -noteImage.width / 4 + anchors.leftMargin: -noteImage.width / 2.5 anchors.bottom: noteImage.bottom anchors.bottomMargin: parent.height / 6 fillMode: Image.PreserveAspectFit @@ -97,7 +100,7 @@ opacity: 0.6 border.color: "white" radius: width / 6 - visible: noteMouseArea.containsMouse || highlightTimer.running + visible: (multipleStaff.noteHoverEnabled && noteMouseArea.containsMouse) || highlightTimer.running } Rectangle { @@ -121,6 +124,22 @@ height: note.height } + Image { + id: correctOrWrongAnswerIndicator + visible: noteAnswered + source: isCorrectlyAnswered ? "qrc:/gcompris/src/activities/piano_composition/resource/passed.svg" + : "qrc:/gcompris/src/activities/piano_composition/resource/failed.svg" + sourceSize.width: noteImage.width / 2.5 + anchors.right: parent.rotation === 180 ? undefined : noteImage.right + anchors.left: parent.rotation === 180 ? noteImage.left : undefined + rotation: parent.rotation === 180 ? 180 : 0 + anchors.rightMargin: 12 + anchors.bottom: noteImage.bottom + anchors.bottomMargin: parent.height / 6 + fillMode: Image.PreserveAspectFit + z: 3 + } + // If the result is not good enough maybe have a rectangle and use opacity mask with a note ColorOverlay { anchors.fill: noteImage diff --git a/src/activities/piano_composition/OptionsRow.qml b/src/activities/piano_composition/OptionsRow.qml new file mode 100644 --- /dev/null +++ b/src/activities/piano_composition/OptionsRow.qml @@ -0,0 +1,201 @@ +/* GCompris - OptionsRow.qml +* +* Copyright (C) 2018 Aman Kumar Gupta +* +* Authors: +* Beth Hadley (GTK+ version) +* Johnny Jazeix (Qt Quick port) +* Aman Kumar Gupta (Qt Quick port) +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, see . +*/ +import QtQuick 2.6 +import GCompris 1.0 + +import "../../core" + +Row { + id: optionsRow + spacing: 15 + + readonly property var noteLengthName: ["Whole", "Half", "Quarter", "Eighth"] + readonly property var staffModes: ["add", "replace", "erase"] + readonly property var lyricsOrPianoModes: ["piano", "lyrics"] + + property real iconsWidth: Math.min(50, (background.width - optionsRow.spacing * 12) / 14) + property alias noteOptionsIndex: noteOptions.currentIndex + property alias staffModeIndex: staffModesOptions.currentIndex + property alias lyricsOrPianoModeIndex: lyricsOrPianoModeOption.currentIndex + property alias restOptionIndex: restOptions.currentIndex + + property alias noteOptionsVisible: noteOptions.visible + property alias playButtonVisible: playButton.visible + property alias clefButtonVisible: clefButton.visible + property alias staffModesOptionsVisible: staffModesOptions.visible + property alias clearButtonVisible: clearButton.visible + property alias undoButtonVisible: undoButton.visible + property alias openButtonVisible: openButton.visible + property alias saveButtonVisible: saveButton.visible + property alias changeAccidentalStyleButtonVisible: changeAccidentalStyleButton.visible + property alias lyricsOrPianoModeOptionVisible: lyricsOrPianoModeOption.visible + property alias restOptionsVisible: restOptions.visible + + signal undoButtonClicked + signal clearButtonClicked + signal openButtonClicked + signal saveButtonClicked + + SwitchableOptions { + id: noteOptions + source: "qrc:/gcompris/src/activities/piano_composition/resource/genericNote%1.svg".arg(optionsRow.noteLengthName[currentIndex]) + nbOptions: optionsRow.noteLengthName.length + onClicked: background.currentType = optionsRow.noteLengthName[currentIndex] + visible: bar.level > 4 + } + + Image { + id: playButton + source: "qrc:/gcompris/src/activities/piano_composition/resource/play.svg" + sourceSize.width: optionsRow.iconsWidth + MouseArea { + anchors.fill: parent + onClicked: multipleStaff.play() + } + } + + Image { + id: clefButton + source: background.clefType == "bass" ? "qrc:/gcompris/src/activities/piano_composition/resource/bassClefButton.svg" : "qrc:/gcompris/src/activities/piano_composition/resource/trebbleClefButton.svg" + sourceSize.width: optionsRow.iconsWidth + visible: bar.level > 2 + MouseArea { + anchors.fill: parent + onClicked: { + multipleStaff.eraseAllNotes() + background.clefType = (background.clefType == "bass") ? "treble" : "bass" + lyricsArea.resetLyricsArea() + } + } + } + + SwitchableOptions { + id:staffModesOptions + nbOptions: optionsRow.staffModes.length + source: "qrc:/gcompris/src/activities/piano_composition/resource/%1.svg".arg(optionsRow.staffModes[currentIndex]) + anchors.top: parent.top + anchors.topMargin: 4 + onClicked: { + background.staffMode = optionsRow.staffModes[currentIndex] + if(background.staffMode != "replace") { + multipleStaff.noteToReplace = -1 + } + } + visible: true + } + + Image { + id: clearButton + source: "qrc:/gcompris/src/activities/piano_composition/resource/edit-clear.svg" + sourceSize.width: optionsRow.iconsWidth + MouseArea { + anchors.fill: parent + onClicked: clearButtonClicked() + } + } + + Image { + id: undoButton + source: "qrc:/gcompris/src/activities/piano_composition/resource/undo.svg" + sourceSize.width: optionsRow.iconsWidth + MouseArea { + anchors.fill: parent + onClicked: undoButtonClicked() + } + } + + Image { + id: openButton + source: "qrc:/gcompris/src/activities/piano_composition/resource/open.svg" + sourceSize.width: optionsRow.iconsWidth + visible: bar.level > 6 + MouseArea { + anchors.fill: parent + onClicked: openButtonClicked() + } + } + + Image { + id: saveButton + source: "qrc:/gcompris/src/activities/piano_composition/resource/save.svg" + sourceSize.width: optionsRow.iconsWidth + visible: bar.level == 7 + MouseArea { + anchors.fill: parent + onClicked: saveButtonClicked() + } + } + + Image { + id: changeAccidentalStyleButton + source: piano.useSharpNotation ? "qrc:/gcompris/src/activities/piano_composition/resource/blacksharp.svg" : "qrc:/gcompris/src/activities/piano_composition/resource/blackflat.svg" + visible: bar.level >= 4 + sourceSize.width: optionsRow.iconsWidth + MouseArea { + anchors.fill: parent + onClicked: piano.useSharpNotation = !piano.useSharpNotation + } + } + + SwitchableOptions { + id: lyricsOrPianoModeOption + nbOptions: optionsRow.lyricsOrPianoModes.length + source: "qrc:/gcompris/src/activities/piano_composition/resource/%1-icon.svg".arg(optionsRow.lyricsOrPianoModes[currentIndex]) + anchors.top: parent.top + anchors.topMargin: 4 + visible: bar.level > 6 + } + + // Since the half rest image is just the rotated image of whole rest image, we check if the current rest type is half, we assign the source as whole rest and rotate it by 180 degrees. + SwitchableOptions { + id: restOptions + + readonly property string restTypeImage: ((optionsRow.noteLengthName[currentIndex] === "Half") ? "Whole" : optionsRow.noteLengthName[currentIndex]).toLowerCase() + + source: "qrc:/gcompris/src/activities/piano_composition/resource/%1Rest.svg".arg(restTypeImage) + nbOptions: optionsRow.noteLengthName.length + onClicked: background.restType = optionsRow.noteLengthName[currentIndex] + rotation: optionsRow.noteLengthName[currentIndex] === "Half" ? 180 : 0 + visible: bar.level > 5 + } + + Image { + id: addRestButton + sourceSize.width: optionsRow.iconsWidth + source: "qrc:/gcompris/src/core/resource/apply.svg" + visible: restOptions.visible + anchors.top: parent.top + anchors.topMargin: 4 + MouseArea { + anchors.fill: parent + onPressed: parent.scale = 0.8 + onReleased: { + parent.scale = 1 + if(background.staffMode === "add") + multipleStaff.addNote(restType.toLowerCase(), "Rest", false, false) + else + multipleStaff.replaceNote(restType.toLowerCase(), "Rest") + } + } + } +} diff --git a/src/activities/piano_composition/Piano.qml b/src/activities/piano_composition/Piano.qml --- a/src/activities/piano_composition/Piano.qml +++ b/src/activities/piano_composition/Piano.qml @@ -140,6 +140,7 @@ property bool blackLabelsVisible: true property bool whiteLabelsVisible: true property bool blackKeysEnabled: true + property bool whiteKeysEnabled: true property bool useSharpNotation: true property int labelSquareSize: whiteWidth - 5 @@ -155,6 +156,7 @@ noteColor: colorWhiteNotes[currentOctaveNb][index] keyName: whiteNotes[currentOctaveNb][index][1] labelsVisible: whiteLabelsVisible + isKeyEnabled: piano.whiteKeysEnabled onKeyPressed: noteClicked(whiteNotes[currentOctaveNb][index][0]) } } diff --git a/src/activities/piano_composition/Piano_composition.qml b/src/activities/piano_composition/Piano_composition.qml --- a/src/activities/piano_composition/Piano_composition.qml +++ b/src/activities/piano_composition/Piano_composition.qml @@ -72,19 +72,19 @@ if(event.key === Qt.Key_8) { piano.whiteKeyRepeater.itemAt(7).keyPressed() } - if(event.key === Qt.Key_F1 && bar.level >= 4) { + if(event.key === Qt.Key_F1 && piano.blackKeysEnabled) { piano.blackKeyRepeater.itemAt(0).keyPressed() } - if(event.key === Qt.Key_F2 && bar.level >= 4) { + if(event.key === Qt.Key_F2 && piano.blackKeysEnabled) { piano.blackKeyRepeater.itemAt(1).keyPressed() } - if(event.key === Qt.Key_F3 && bar.level >= 4) { + if(event.key === Qt.Key_F3 && piano.blackKeysEnabled) { piano.blackKeyRepeater.itemAt(2).keyPressed() } - if(event.key === Qt.Key_F4 && bar.level >= 4) { + if(event.key === Qt.Key_F4 && piano.blackKeysEnabled) { piano.blackKeyRepeater.itemAt(3).keyPressed() } - if(event.key === Qt.Key_F5 && bar.level >= 4) { + if(event.key === Qt.Key_F5 && piano.blackKeysEnabled) { piano.blackKeyRepeater.itemAt(4).keyPressed() } if(event.key === Qt.Key_Delete) { @@ -108,7 +108,7 @@ property alias melodyList: melodyList property alias file: file property alias piano: piano - property alias staffModesOptions: staffModesOptions + property alias optionsRow: optionsRow property alias lyricsArea: lyricsArea } @@ -120,7 +120,7 @@ property string clefType: bar.level == 2 ? "bass" : "treble" property string staffMode: "add" property bool undidChange: false - property bool isLyricsMode: (lyricsOrPianoModeOption.currentIndex === 1) && lyricsOrPianoModeOption.visible + property bool isLyricsMode: (optionsRow.lyricsOrPianoModeIndex === 1) && optionsRow.lyricsOrPianoModeOptionVisible File { id: file @@ -269,179 +269,34 @@ id: lyricsArea } - Row { + OptionsRow { id: optionsRow anchors.top: instructionBox.bottom anchors.topMargin: 10 - spacing: 15 anchors.horizontalCenter: parent.horizontalCenter - readonly property var noteLengthName: ["Whole", "Half", "Quarter", "Eighth"] - readonly property var staffModes: ["add", "replace", "erase"] - readonly property var lyricsOrPianoModes: ["piano", "lyrics"] - readonly property real iconsWidth: Math.min(50, (background.width - optionsRow.spacing *12) / 14) - - SwitchableOptions { - id: noteOptions - source: "qrc:/gcompris/src/activities/piano_composition/resource/genericNote%1.svg".arg(optionsRow.noteLengthName[currentIndex]) - nbOptions: optionsRow.noteLengthName.length - onClicked: currentType = optionsRow.noteLengthName[currentIndex] - visible: bar.level > 4 - } - - Image { - id: playButton - source: "qrc:/gcompris/src/activities/piano_composition/resource/play.svg" - sourceSize.width: optionsRow.iconsWidth - MouseArea { - anchors.fill: parent - onClicked: multipleStaff.play() - } - } - - Image { - id: clefButton - source: clefType == "bass" ? "qrc:/gcompris/src/activities/piano_composition/resource/bassClefButton.svg" : "qrc:/gcompris/src/activities/piano_composition/resource/trebbleClefButton.svg" - sourceSize.width: optionsRow.iconsWidth - visible: bar.level > 2 - MouseArea { - anchors.fill: parent - onClicked: { - multipleStaff.eraseAllNotes() - clefType = (clefType == "bass") ? "treble" : "bass" - lyricsArea.resetLyricsArea() - } - } - } - - SwitchableOptions { - id:staffModesOptions - nbOptions: optionsRow.staffModes.length - source: "qrc:/gcompris/src/activities/piano_composition/resource/%1.svg".arg(optionsRow.staffModes[currentIndex]) - anchors.top: parent.top - anchors.topMargin: 4 - onClicked: { - background.staffMode = optionsRow.staffModes[currentIndex] - if(background.staffMode != "replace") { - multipleStaff.noteToReplace = -1 - } - } - visible: true - } - - Image { - id: clearButton - source: "qrc:/gcompris/src/activities/piano_composition/resource/edit-clear.svg" - sourceSize.width: optionsRow.iconsWidth - MouseArea { - anchors.fill: parent - onClicked: { - lyricsArea.resetLyricsArea() - Activity.undoStack = [] - multipleStaff.eraseAllNotes() - } - } - } - - Image { - id: undoButton - source: "qrc:/gcompris/src/activities/piano_composition/resource/undo.svg" - sourceSize.width: optionsRow.iconsWidth - MouseArea { - anchors.fill: parent - onClicked: { - background.undidChange = true - Activity.undoChange() - background.undidChange = false - } - } + onUndoButtonClicked: { + background.undidChange = true + Activity.undoChange() + background.undidChange = false } - - Image { - id: openButton - source: "qrc:/gcompris/src/activities/piano_composition/resource/open.svg" - sourceSize.width: optionsRow.iconsWidth - visible: bar.level > 6 - MouseArea { - anchors.fill: parent - onClicked: { - melodyList.melodiesModel.clear() - var dataset = Dataset.get() - for(var i = 0; i < dataset.length; i++) { - melodyList.melodiesModel.append(dataset[i]) - } - piano.enabled = false - bar.visible = false - melodyList.visible = true - melodyList.forceActiveFocus() - } - } - } - - Image { - id: saveButton - source: "qrc:/gcompris/src/activities/piano_composition/resource/save.svg" - sourceSize.width: optionsRow.iconsWidth - visible: bar.level == 7 - MouseArea { - anchors.fill: parent - onClicked: { - Activity.saveMelody() - } - } - } - - Image { - id: changeAccidentalStyleButton - source: piano.useSharpNotation ? "qrc:/gcompris/src/activities/piano_composition/resource/blacksharp.svg" : "qrc:/gcompris/src/activities/piano_composition/resource/blackflat.svg" - visible: bar.level >= 4 - MouseArea { - anchors.fill: parent - onClicked: piano.useSharpNotation = !piano.useSharpNotation - } - } - - SwitchableOptions { - id: lyricsOrPianoModeOption - nbOptions: optionsRow.lyricsOrPianoModes.length - source: "qrc:/gcompris/src/activities/piano_composition/resource/%1-icon.svg".arg(optionsRow.lyricsOrPianoModes[currentIndex]) - anchors.top: parent.top - anchors.topMargin: 4 - visible: bar.level > 6 - } - - // Since the half rest image is just the rotated image of whole rest image, we check if the current rest type is half, we assign the source as whole rest and rotate it by 180 degrees. - SwitchableOptions { - id: restOptions - - readonly property string restTypeImage: ((optionsRow.noteLengthName[currentIndex] === "Half") ? "Whole" : optionsRow.noteLengthName[currentIndex]).toLowerCase() - - source: "qrc:/gcompris/src/activities/piano_composition/resource/%1Rest.svg".arg(restTypeImage) - nbOptions: optionsRow.noteLengthName.length - onClicked: restType = optionsRow.noteLengthName[currentIndex] - rotation: optionsRow.noteLengthName[currentIndex] === "Half" ? 180 : 0 - visible: bar.level > 5 + onClearButtonClicked: { + lyricsArea.resetLyricsArea() + Activity.undoStack = [] + multipleStaff.eraseAllNotes() } - - Image { - id: addRestButton - sourceSize.width: optionsRow.iconsWidth - source: "qrc:/gcompris/src/core/resource/apply.svg" - visible: restOptions.visible - anchors.top: parent.top - anchors.topMargin: 4 - MouseArea { - anchors.fill: parent - onPressed: parent.scale = 0.8 - onReleased: { - parent.scale = 1 - if(background.staffMode === "add") - multipleStaff.addNote(restType.toLowerCase(), "Rest", false, false) - else - multipleStaff.replaceNote(restType.toLowerCase(), "Rest") - } + onOpenButtonClicked: { + melodyList.melodiesModel.clear() + var dataset = Dataset.get() + for(var i = 0; i < dataset.length; i++) { + melodyList.melodiesModel.append(dataset[i]) } + piano.enabled = false + bar.visible = false + melodyList.visible = true + melodyList.forceActiveFocus() } + onSaveButtonClicked: Activity.saveMelody() } DialogHelp { diff --git a/src/activities/piano_composition/Staff.qml b/src/activities/piano_composition/Staff.qml --- a/src/activities/piano_composition/Staff.qml +++ b/src/activities/piano_composition/Staff.qml @@ -23,7 +23,6 @@ import GCompris 1.0 import "../../core" -import "piano_composition.js" as Activity Item { id: staff diff --git a/src/activities/piano_composition/piano_composition.js b/src/activities/piano_composition/piano_composition.js --- a/src/activities/piano_composition/piano_composition.js +++ b/src/activities/piano_composition/piano_composition.js @@ -23,12 +23,10 @@ .import QtQuick 2.6 as Quick .import GCompris 1.0 as GCompris .import "qrc:/gcompris/src/core/core.js" as Core -.import "qrc:/gcompris/src/activities/piano_composition/NoteNotations.js" as NoteNotations var currentLevel = 0 var numberOfLevel = 7 var items -var notesDetails = NoteNotations.get() var userDir = "file://" + GCompris.ApplicationInfo.getSharedWritablePath() + "/" + "piano_composition" var userFile = userDir + "/melodies.json" var undoStack = [] @@ -105,7 +103,7 @@ items.multipleStaff.nbStaves = 2 items.background.staffMode = "add" items.multipleStaff.noteToReplace = -1 - items.staffModesOptions.currentIndex = 0 + items.optionsRow.staffModeIndex = 0 items.lyricsArea.resetLyricsArea() undoStack = [] } @@ -125,21 +123,6 @@ } } -function getNoteDetails(noteName, noteType) { - var clef = items.background.clefType === 'treble' ? "Treble" : "Bass" - var noteNotation - if(noteType === "Rest") - noteNotation = noteName + noteType - else - noteNotation = clef + noteName - console.log(noteNotation) - for(var i = 0; i < notesDetails.length; i++) { - if(noteNotation === notesDetails[i].noteName) { - return notesDetails[i] - } - } -} - function nextLevel() { items.multipleStaff.eraseAllNotes() if(numberOfLevel <= ++currentLevel) { diff --git a/src/activities/piano_composition/resource/background/1.jpg b/src/activities/piano_composition/resource/background/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@Each level has 5 sublevels. Levels 1-5 will offer treble clef to practice and levels 6-10 will offer bass clef.") + manual: qsTr("The notes you see will be played to you. Click on the corresponding keys on the keyboard that match the notes you hear and see.
Each level has 5 sublevels. Levels 1-6 will offer treble clef to practice and levels 7-11 will offer bass clef.") credit: "" section: "discovery sound_group" createdInVersion: 9500 diff --git a/src/activities/play_piano/PlayPiano.qml b/src/activities/play_piano/PlayPiano.qml --- a/src/activities/play_piano/PlayPiano.qml +++ b/src/activities/play_piano/PlayPiano.qml @@ -20,8 +20,11 @@ * along with this program; if not, see . */ import QtQuick 2.6 +import QtQuick.Controls 1.5 +import GCompris 1.0 import "../../core" +import "../piano_composition" import "play_piano.js" as Activity ActivityBase { @@ -30,34 +33,318 @@ onStart: focus = true onStop: {} - pageComponent: Rectangle { + property bool horizontalLayout: width > height + + pageComponent: Image { id: background anchors.fill: parent - color: "#ABCDEF" + fillMode: Image.PreserveAspectCrop signal start signal stop + property string backgroundImagesUrl: ":/gcompris/src/activities/piano_composition/resource/background/" + property var backgroundImages: directory.getFiles(backgroundImagesUrl) + + Directory { + id: directory + } + + source:{ + if(items.bar.level === 0) + return "qrc" + backgroundImagesUrl + backgroundImages[0] + else + return "qrc" + backgroundImagesUrl + backgroundImages[(items.bar.level - 1) % backgroundImages.length] + } + Component.onCompleted: { activity.start.connect(start) activity.stop.connect(stop) } + Keys.onPressed: { + if(event.key === Qt.Key_1) { + piano.whiteKeyRepeater.itemAt(0).keyPressed() + } + if(event.key === Qt.Key_2) { + piano.whiteKeyRepeater.itemAt(1).keyPressed() + } + if(event.key === Qt.Key_3) { + piano.whiteKeyRepeater.itemAt(2).keyPressed() + } + if(event.key === Qt.Key_4) { + piano.whiteKeyRepeater.itemAt(3).keyPressed() + } + if(event.key === Qt.Key_5) { + piano.whiteKeyRepeater.itemAt(4).keyPressed() + } + if(event.key === Qt.Key_6) { + piano.whiteKeyRepeater.itemAt(5).keyPressed() + } + if(event.key === Qt.Key_7) { + piano.whiteKeyRepeater.itemAt(6).keyPressed() + } + if(event.key === Qt.Key_8) { + piano.whiteKeyRepeater.itemAt(7).keyPressed() + } + if(event.key === Qt.Key_F1 && piano.blackKeysEnabled) { + piano.blackKeyRepeater.itemAt(0).keyPressed() + } + if(event.key === Qt.Key_F2 && piano.blackKeysEnabled) { + piano.blackKeyRepeater.itemAt(1).keyPressed() + } + if(event.key === Qt.Key_F3 && piano.blackKeysEnabled) { + piano.blackKeyRepeater.itemAt(2).keyPressed() + } + if(event.key === Qt.Key_F4 && piano.blackKeysEnabled) { + piano.blackKeyRepeater.itemAt(3).keyPressed() + } + if(event.key === Qt.Key_F5 && piano.blackKeysEnabled) { + piano.blackKeyRepeater.itemAt(4).keyPressed() + } + if(event.key === Qt.Key_Space) { + multipleStaff.play() + } + if(event.key === Qt.Key_Backspace || event.key === Qt.Key_Delete) { + Activity.undoPreviousAnswer() + } + } + // Add here the QML items you need to access in javascript QtObject { id: items property Item main: activity.main property alias background: background + property GCSfx audioEffects: activity.audioEffects + property alias multipleStaff: multipleStaff + property alias piano: piano property alias bar: bar property alias bonus: bonus + property alias score: score + property alias iAmReady: iAmReady + property string mode: "easy" } - onStart: { Activity.start(items) } + onStart: { + dialogActivityConfig.getInitialConfiguration() + Activity.start(items) + } onStop: { Activity.stop() } - GCText { - anchors.centerIn: parent - text: "play_piano activity" - fontSize: largeSize + property string clefType: (items.bar.level <= 6) ? "treble" : "bass" + readonly property bool shiftButtonsVisible: [4, 5, 6, 9, 10, 11].indexOf(items.bar.level) != -1 + + Rectangle { + anchors.fill: parent + color: "black" + opacity: 0.3 + visible: iAmReady.visible + z: 10 + MouseArea { + anchors.fill: parent + } + } + + ReadyButton { + id: iAmReady + focus: true + z: 10 + onClicked: { + Activity.initLevel() + } + } + + Score { + id: score + anchors.top: background.top + anchors.bottom: undefined + numberOfSubLevels: 5 + width: horizontalLayout ? parent.width / 10 : (parent.width - instruction.x - instruction.width - 1.5 * anchors.rightMargin) + } + + Rectangle { + id: instruction + radius: 10 + width: background.width * 0.6 + height: background.height / 9 + anchors.horizontalCenter: parent.horizontalCenter + opacity: 0.8 + border.width: 6 + color: "white" + border.color: "#87A6DD" + + GCText { + color: "black" + z: 3 + anchors.fill: parent + anchors.rightMargin: parent.width * 0.02 + anchors.leftMargin: parent.width * 0.02 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + fontSizeMode: Text.Fit + wrapMode: Text.WordWrap + text: qsTr("Click on the piano keys that match the given notes.") + } + } + + MultipleStaff { + id: multipleStaff + width: horizontalLayout ? parent.width * 0.4 : parent.width * 0.8 + height: horizontalLayout ? parent.height * 0.7 : parent.height * 0.58 + nbStaves: 1 + clef: clefType + nbMaxNotesPerStaff: 10 + noteIsColored: items.mode === "easy" + isMetronomeDisplayed: false + isFlickable: false + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: instruction.bottom + anchors.topMargin: horizontalLayout ? 0 : parent.height * 0.09 + onNoteClicked: playNoteAudio(noteName, noteType) + noteHoverEnabled: false + centerNotesPosition: true + } + + Piano { + id: piano + width: horizontalLayout ? parent.width * 0.4 : parent.width * 0.7 + height: horizontalLayout ? parent.height * 0.3 : parent.width * 0.26 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: bar.top + anchors.bottomMargin: 20 + blackLabelsVisible: ([4, 5, 6, 9, 10, 11].indexOf(items.bar.level) != -1) + useSharpNotation: (bar.level != 5) && (bar.level != 10) + blackKeysEnabled: blackLabelsVisible && !multipleStaff.isMusicPlaying + whiteKeysEnabled: !multipleStaff.isMusicPlaying + onNoteClicked: Activity.checkAnswer(note) + currentOctaveNb: 0 + } + + Image { + id: shiftKeyboardLeft + source: "qrc:/gcompris/src/core/resource/bar_next.svg" + sourceSize.width: piano.width / 7 + width: sourceSize.width + height: width + fillMode: Image.PreserveAspectFit + rotation: 180 + visible: (piano.currentOctaveNb > 0) && shiftButtonsVisible + anchors { + verticalCenter: piano.verticalCenter + right: piano.left + } + MouseArea { + anchors.fill: parent + onClicked: piano.currentOctaveNb-- + } + } + + Image { + id: shiftKeyboardRight + source: "qrc:/gcompris/src/core/resource/bar_next.svg" + sourceSize.width: piano.width / 7 + width: sourceSize.width + height: width + fillMode: Image.PreserveAspectFit + visible: (piano.currentOctaveNb < piano.maxNbOctaves - 1) && shiftButtonsVisible + anchors { + verticalCenter: piano.verticalCenter + left: piano.right + } + MouseArea { + anchors.fill: parent + onClicked: piano.currentOctaveNb++ + } + } + + Rectangle { + id: optionDeck + width: optionsRow.changeAccidentalStyleButtonVisible ? optionsRow.iconsWidth * 5 : optionsRow.iconsWidth * 4 + height: optionsRow.iconsWidth * 1.7 + border.width: 2 + border.color: "black" + color: "black" + opacity: 0.5 + radius: 10 + y: horizontalLayout ? background.height / 2 - height : instruction.height + 10 + x: horizontalLayout ? background.width - width - 25 : background.width / 2 - width / 2 + } + + OptionsRow { + id: optionsRow + anchors.top: optionDeck.top + anchors.topMargin: 15 + anchors.horizontalCenter: optionDeck.horizontalCenter + iconsWidth: horizontalLayout ? Math.min(50, (background.width - piano.x - piano.width) / 5) : 45 + + playButtonVisible: true + undoButtonVisible: true + changeAccidentalStyleButtonVisible: [6, 11].indexOf(items.bar.level) != -1 + clefButtonVisible: false + noteOptionsVisible: false + staffModesOptionsVisible: false + clearButtonVisible: false + openButtonVisible: false + saveButtonVisible: false + lyricsOrPianoModeOptionVisible: false + restOptionsVisible: false + + onUndoButtonClicked: Activity.undoPreviousAnswer() + } + + ExclusiveGroup { + id: configOptions + } + + DialogActivityConfig { + id: dialogActivityConfig + content: Component { + Column { + id: column + spacing: 5 + width: dialogActivityConfig.width + height: dialogActivityConfig.height + + property alias easyModeBox: easyModeBox + property alias expertModeBox: expertModeBox + + GCDialogCheckBox { + id: easyModeBox + width: column.width - 50 + text: qsTr("Display colored notes.") + checked: (items.mode == "easy") ? true : false + exclusiveGroup: configOptions + onCheckedChanged: { + if(easyModeBox.checked) { + items.mode = "easy" + } + } + } + + GCDialogCheckBox { + id: expertModeBox + width: easyModeBox.width + text: qsTr("Display colorless notes.") + checked: (items.mode == "expert") ? true : false + exclusiveGroup: configOptions + onCheckedChanged: { + if(expertModeBox.checked) { + items.mode = "expert" + } + } + } + } + } + onLoadData: { + if(dataToSave && dataToSave["mode"]) + items.mode = dataToSave["mode"] + } + onSaveData: dataToSave["mode"] = items.mode + onClose: { + multipleStaff.eraseAllNotes() + iAmReady.visible = true + Activity.currentLevel = 0 + home() + } } DialogHelp { @@ -67,19 +354,24 @@ Bar { id: bar - content: BarEnumContent { value: help | home | level } - onHelpClicked: { - displayDialog(dialogHelp) - } + content: BarEnumContent { value: help | home | level | config | reload } + onHelpClicked: displayDialog(dialogHelp) onPreviousLevelClicked: Activity.previousLevel() onNextLevelClicked: Activity.nextLevel() onHomeClicked: activity.home() + onConfigClicked: { + dialogActivityConfig.active = true + displayDialog(dialogActivityConfig) + } + onReloadClicked: { + multipleStaff.eraseAllNotes() + iAmReady.visible = true + } } Bonus { id: bonus - Component.onCompleted: win.connect(Activity.nextLevel) + Component.onCompleted: win.connect(Activity.nextSubLevel) } } - } diff --git a/src/activities/play_piano/dataset.js b/src/activities/play_piano/dataset.js new file mode 100644 --- /dev/null +++ b/src/activities/play_piano/dataset.js @@ -0,0 +1,103 @@ +/* GCompris - dataset.js + * + * Copyright (C) 2018 Aman Kumar Gupta + * + * Authors: + * Beth Hadley (GTK+ version) + * Aman Kumar Gupta (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 . + **/ + +function getData() { + return [ + [ + "treble G3Quarter G3Quarter F4Quarter", + "treble A3Quarter A3Quarter G4Quarter", + "treble B3Quarter E4Quarter G3Quarter", + "treble F4Quarter A3Quarter C4Quarter B3Quarter", + "treble D4Quarter A3Quarter G4Quarter E4Quarter" + ], + [ + "treble C4Quarter E4Quarter E4Quarter", + "treble A4Quarter A4Quarter G4Quarter", + "treble B4Quarter D4Quarter F4Quarter", + "treble F4Quarter A4Quarter A4Quarter F4Quarter", + "treble D4Quarter C4Quarter G4Quarter E4Quarter" + ], + [ + "treble C5Quarter E5Quarter E5Quarter", + "treble A5Quarter A5Quarter G5Quarter", + "treble B5Quarter D5Quarter F5Quarter", + "treble F5Quarter A5Quarter A5Quarter F5Quarter", + "treble D5Quarter C5Quarter G5Quarter E5Quarter" + ], + [ + "treble C#5Quarter D4Quarter D#4Quarter", + "treble A3Quarter A4Quarter G#5Quarter", + "treble B3Quarter D#5Quarter F#5Quarter", + "treble F#5Quarter A3Quarter A3Quarter F#4Quarter", + "treble D5Quarter C#4Quarter G#3Quarter D#4Quarter" + ], + [ + "treble Db5Quarter D4Quarter Bb3Quarter", + "treble A3Quarter A4Quarter Bb5Quarter", + "treble B3Quarter Db5Quarter Ab5Quarter", + "treble Ab3Quarter Ab3Quarter A3Quarter D4Quarter", + "treble D5Quarter Db5Quarter Gb4Quarter Bb3Quarter" + ], + [ + "treble C#5Quarter D4Quarter Ab3Quarter", + "treble A3Quarter A4Quarter G#5Quarter", + "treble B3Quarter D#5Quarter Ab5Quarter", + "treble Ab3Quarter G#3Quarter A3Quarter D4Quarter", + "treble Ab5Quarter C#4Quarter Gb4Quarter F#5Quarter" + ], + [ + "bass C3Quarter A3Quarter D3Quarter", + "bass G3Quarter B3Quarter E3Quarter", + "bass F3Quarter A3Quarter B3Quarter", + "bass G3Quarter E3Quarter E3Quarter F3Quarter", + "bass C3Quarter F3Quarter G3Quarter B3Quarter" + ], + [ + "bass C4Quarter F4Quarter B3Quarter", + "bass A3Quarter G4Quarter C4Quarter", + "bass D4Quarter E4Quarter B3Quarter", + "bass G3Quarter C4Quarter E4Quarter F4Quarter", + "bass G4Quarter F4Quarter A3Quarter D4Quarter" + ], + [ + "bass C3Quarter C#3Quarter D#4Quarter", + "bass A3Quarter G#3Quarter E3Quarter", + "bass F3Quarter F#4Quarter F#3Quarter", + "bass G3Quarter E4Quarter E3Quarter F#3Quarter", + "bass G4Quarter F3Quarter F#4Quarter C#4Quarter" + ], + [ + "bass A3Quarter Ab3Quarter Db4Quarter", + "bass Eb4Quarter F3Quarter F3Quarter", + "bass F3Quarter Eb4Quarter Db3Quarter", + "bass F4Quarter Gb3Quarter E4Quarter F4Quarter", + "bass Gb4Quarter A3Quarter Db4Quarter D4Quarter" + ], + [ + "bass C3Quarter Gb3Quarter D#4Quarter", + "bass Eb4Quarter F3Quarter F#3Quarter", + "bass F4Quarter F#4Quarter Db4Quarter", + "bass G3Quarter G#3Quarter C#4Quarter C4Quarter", + "bass G4Quarter F#3Quarter Gb3Quarter Eb4Quarter" + ] + ] +} diff --git a/src/activities/play_piano/play_piano.js b/src/activities/play_piano/play_piano.js --- a/src/activities/play_piano/play_piano.js +++ b/src/activities/play_piano/play_piano.js @@ -21,15 +21,21 @@ */ .pragma library .import QtQuick 2.6 as Quick +.import "dataset.js" as Dataset var currentLevel = 0 -var numberOfLevel = 4 +var currentSubLevel = 0 +var numberOfLevel = 11 +var noteIndexAnswered var items +var levels +var incorrectAnswers = [] function start(items_) { items = items_ currentLevel = 0 - initLevel() + levels = Dataset.getData() + items.piano.currentOctaveNb = 0 } function stop() { @@ -37,18 +43,87 @@ function initLevel() { items.bar.level = currentLevel + 1 + if(items.bar.level === 1 || items.bar.level === 7) + items.piano.currentOctaveNb = 0 + else if(items.bar.level === 2 || items.bar.level === 8) + items.piano.currentOctaveNb = 1 + else if(items.bar.level === 3) + items.piano.currentOctaveNb = 2 + else + items.piano.currentOctaveNb = items.piano.defaultOctaveNb + + if([5, 10].indexOf(items.bar.level) === -1) + items.piano.useSharpNotation = true + else + items.piano.useSharpNotation = false + + currentSubLevel = 0 + nextSubLevel() +} + +function initSubLevel() { + var currentSubLevelMelody = levels[currentLevel][currentSubLevel - 1] + noteIndexAnswered = -1 + items.multipleStaff.loadFromData(currentSubLevelMelody) + items.multipleStaff.play() +} + +function nextSubLevel() { + currentSubLevel++ + incorrectAnswers = [] + items.score.currentSubLevel = currentSubLevel + if(currentSubLevel > levels[currentLevel].length) + nextLevel() + else + initSubLevel() +} + +// Function will be written when activity logic will be implemented. +function undoPreviousAnswer() { + if(noteIndexAnswered >= 0) { + items.multipleStaff.revertAnswer(noteIndexAnswered) + if(incorrectAnswers.indexOf(noteIndexAnswered) != -1) + incorrectAnswers.pop() + + noteIndexAnswered-- + } +} + +function checkAnswer(noteName) { + var currentSubLevelNotes = levels[currentLevel][currentSubLevel - 1].split(' ') + if(noteIndexAnswered < (currentSubLevelNotes.length - 2)) { + noteIndexAnswered++ + var currentNote = currentSubLevelNotes[noteIndexAnswered + 1] + if((noteName + "Quarter") === currentNote) + items.multipleStaff.indicateAnsweredNote(true, noteIndexAnswered) + else { + incorrectAnswers.push(noteIndexAnswered) + items.multipleStaff.indicateAnsweredNote(false, noteIndexAnswered) + } + + if(noteIndexAnswered === (currentSubLevelNotes.length - 2)) { + if(incorrectAnswers.length === 0) + items.bonus.good("flower") + else + items.bonus.bad("flower") + } + } } function nextLevel() { - if(numberOfLevel <= ++currentLevel) { - currentLevel = 0 + if(!items.iAmReady.visible) { + if(numberOfLevel <= ++currentLevel) { + currentLevel = 0 + } + initLevel() } - initLevel(); } function previousLevel() { - if(--currentLevel < 0) { - currentLevel = numberOfLevel - 1 + if(!items.iAmReady.visible) { + if(--currentLevel < 0) { + currentLevel = numberOfLevel - 1 + } + initLevel() } - initLevel(); }