diff --git a/src/activities/piano_composition/Piano_composition.qml b/src/activities/piano_composition/Piano_composition.qml index cae0c0092..5c4147399 100644 --- a/src/activities/piano_composition/Piano_composition.qml +++ b/src/activities/piano_composition/Piano_composition.qml @@ -1,439 +1,442 @@ /* GCompris - Piano_composition.qml * * Copyright (C) 2016 Johnny Jazeix * 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 QtQuick.Controls 1.5 import GCompris 1.0 import "../../core" import "qrc:/gcompris/src/core/core.js" as Core import "piano_composition.js" as Activity import "melodies.js" as Dataset ActivityBase { id: activity onStart: focus = true onStop: {} property bool horizontalLayout: background.width > background.height pageComponent: Rectangle { id: background anchors.fill: parent color: "#ABCDEF" signal start signal stop Component.onCompleted: { activity.start.connect(start) activity.stop.connect(stop) } Keys.onPressed: { var keyboardBindings = {} keyboardBindings[Qt.Key_1] = 0 keyboardBindings[Qt.Key_2] = 1 keyboardBindings[Qt.Key_3] = 2 keyboardBindings[Qt.Key_4] = 3 keyboardBindings[Qt.Key_5] = 4 keyboardBindings[Qt.Key_6] = 5 keyboardBindings[Qt.Key_7] = 6 keyboardBindings[Qt.Key_F1] = 1 keyboardBindings[Qt.Key_F2] = 2 keyboardBindings[Qt.Key_F3] = 3 keyboardBindings[Qt.Key_F4] = 4 keyboardBindings[Qt.Key_F5] = 5 if(event.key >= Qt.Key_1 && event.key <= Qt.Key_7) { piano.keyRepeater.itemAt(keyboardBindings[event.key]).whiteKey.keyPressed() } else if(event.key >= Qt.Key_F1 && event.key <= Qt.Key_F5) { if(piano.blackKeysEnabled) findBlackKey(keyboardBindings[event.key]) } if(event.key === Qt.Key_Left && shiftKeyboardLeft.visible) { piano.currentOctaveNb-- } if(event.key === Qt.Key_Right && shiftKeyboardRight.visible) { piano.currentOctaveNb++ } if(event.key === Qt.Key_Delete) { optionsRow.clearButtonClicked() } if(event.key === Qt.Key_Backspace) { optionsRow.undoButtonClicked() } if(event.key === Qt.Key_Space) { multipleStaff.play() } } function findBlackKey(keyNumber) { for(var i = 0; keyNumber; i++) { if(piano.keyRepeater.itemAt(i) == undefined) break if(piano.keyRepeater.itemAt(i).blackKey.visible) keyNumber-- if(keyNumber == 0) piano.keyRepeater.itemAt(i).blackKey.keyPressed() } } // 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 bar: bar property alias bonus: bonus property alias multipleStaff: multipleStaff property string staffLength: "short" property alias melodyList: melodyList property alias file: file property alias piano: piano property alias optionsRow: optionsRow property alias lyricsArea: lyricsArea } onStart: { Activity.start(items) } onStop: { Activity.stop() } property string currentType: "Quarter" property string restType: "Quarter" property string clefType: bar.level == 2 ? "Bass" : "Treble" property bool isLyricsMode: (optionsRow.lyricsOrPianoModeIndex === 1) && optionsRow.lyricsOrPianoModeOptionVisible File { id: file onError: console.error("File error: " + msg) } Item { id: clickedOptionMessage signal show(string message) onShow: { messageText.text = message messageAnimation.stop() messageAnimation.start() } width: horizontalLayout ? parent.width / 12 : parent.width / 6 height: width * 0.4 visible: false anchors.top: optionsRow.bottom anchors.horizontalCenter: optionsRow.horizontalCenter z: 5 Rectangle { id: messageRectangle width: messageText.contentWidth + 5 height: messageText.height + 5 anchors.centerIn: messageText color: "black" opacity: 0.5 border.width: 3 border.color: "black" radius: 15 } GCText { id: messageText anchors.fill: parent anchors.rightMargin: parent.width * 0.02 anchors.leftMargin: parent.width * 0.02 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter fontSizeMode: Text.Fit color: "white" } SequentialAnimation { id: messageAnimation onStarted: clickedOptionMessage.visible = true PauseAnimation { duration: 1000 } NumberAnimation { targets: [messageRectangle, messageText] property: "opacity" to: 0 duration: 200 } onStopped: { clickedOptionMessage.visible = false messageRectangle.opacity = 0.5 messageText.opacity = 1 } } } MelodyList { id: melodyList onClose: { visible = false piano.enabled = true bar.visible = true focus = false activity.focus = true } } Rectangle { id: instructionBox radius: 10 width: background.width * 0.98 height: background.height / 9 anchors.horizontalCenter: parent.horizontalCenter opacity: 0.8 border.width: 6 color: "white" border.color: "#87A6DD" GCText { id: instructionText 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: Activity.instructions[bar.level - 1].text } } MultipleStaff { id: multipleStaff width: horizontalLayout ? parent.width * 0.50 : parent.width * 0.8 height: horizontalLayout ? parent.height * 0.58 : parent.height * 0.3 nbStaves: 2 clef: clefType coloredNotes: ['C','D', 'E', 'F', 'G', 'A', 'B'] anchors.right: horizontalLayout ? parent.right: undefined anchors.horizontalCenter: horizontalLayout ? undefined : parent.horizontalCenter anchors.top: instructionBox.bottom anchors.topMargin: parent.height * 0.1 anchors.rightMargin: parent.width * 0.043 noteHoverEnabled: true onNoteClicked: { if(selectedIndex === noteIndex) selectedIndex = -1 else { selectedIndex = noteIndex background.clefType = musicElementModel.get(selectedIndex).soundPitch_ playNoteAudio(musicElementModel.get(selectedIndex).noteName_, musicElementModel.get(selectedIndex).noteType_, background.clefType, musicElementRepeater.itemAt(selectedIndex).duration) } } } GCButtonScroll { id: multipleStaffFlickButton anchors.right: parent.right anchors.rightMargin: 5 * ApplicationInfo.ratio anchors.verticalCenter: multipleStaff.verticalCenter width: horizontalLayout ? parent.width * 0.033 : parent.width * 0.06 height: width * heightRatio onUp: multipleStaff.flickableStaves.flick(0, multipleStaff.height * 1.3) onDown: multipleStaff.flickableStaves.flick(0, -multipleStaff.height * 1.3) upVisible: multipleStaff.flickableStaves.visibleArea.yPosition > 0 downVisible: (multipleStaff.flickableStaves.visibleArea.yPosition + multipleStaff.flickableStaves.visibleArea.heightRatio) < 1 } PianoOctaveKeyboard { id: piano width: horizontalLayout ? parent.width * 0.34 : parent.width * 0.7 height: horizontalLayout ? parent.height * 0.40 : parent.width * 0.26 anchors.horizontalCenter: horizontalLayout ? undefined : parent.horizontalCenter anchors.left: horizontalLayout ? parent.left : undefined anchors.leftMargin: parent.width * 0.04 anchors.top: horizontalLayout ? multipleStaff.top : multipleStaff.bottom anchors.topMargin: horizontalLayout ? parent.height * 0.08 : parent.height * 0.025 blackLabelsVisible: [3, 4, 5, 6, 7, 8].indexOf(items.bar.level) == -1 ? false : true useSharpNotation: bar.level != 4 blackKeysEnabled: bar.level > 2 visible: !background.isLyricsMode currentOctaveNb: (background.clefType === "Bass") ? 0 : 1 onNoteClicked: { parent.addMusicElementAndPushToStack(note, currentType) } } function addMusicElementAndPushToStack(noteName, noteType, elementType) { if(noteType === "Rest") elementType = "rest" else if(elementType == undefined) elementType = "note" var tempModel = multipleStaff.createNotesBackup() Activity.pushToStack(tempModel) multipleStaff.addMusicElement(elementType, noteName, noteType, false, true, background.clefType) } Image { id: shiftKeyboardLeft source: "qrc:/gcompris/src/core/resource/bar_previous.svg" sourceSize.width: piano.width / 7 width: sourceSize.width height: width fillMode: Image.PreserveAspectFit visible: (piano.currentOctaveNb > 0) && piano.visible 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) && piano.visible anchors { verticalCenter: piano.verticalCenter left: piano.right } MouseArea { anchors.fill: parent onClicked: piano.currentOctaveNb++ } } LyricsArea { id: lyricsArea } GCCreationHandler { id: creationHandler activityName: "piano_composition" + onFileLoaded: { + multipleStaff.redraw(data) + } } OptionsRow { id: optionsRow anchors.top: instructionBox.bottom anchors.topMargin: 10 anchors.horizontalCenter: parent.horizontalCenter noteOptionsVisible: bar.level > 4 playButtonVisible: true clefButtonVisible: bar.level > 2 clearButtonVisible: true undoButtonVisible: true openButtonVisible: bar.level > 6 saveButtonVisible: bar.level > 6 changeAccidentalStyleButtonVisible: bar.level >= 4 lyricsOrPianoModeOptionVisible: bar.level > 6 restOptionsVisible: bar.level > 5 bpmVisible: true onUndoButtonClicked: { Activity.undoChange() } onClearButtonClicked: { if((multipleStaff.musicElementModel.count > 1) && multipleStaff.selectedIndex === -1) { Core.showMessageDialog(main, qsTr("You have not selected any note. Do you want to erase all the notes?"), qsTr("Yes"), function() { Activity.undoStack = [] lyricsArea.resetLyricsArea() multipleStaff.eraseAllNotes() multipleStaff.nbStaves = 2 }, qsTr("No"), null, null ) } else if((multipleStaff.musicElementModel.count > 1) && !multipleStaff.musicElementModel.get(multipleStaff.selectedIndex).isDefaultClef_) { var noteIndex = multipleStaff.selectedIndex var tempModel = multipleStaff.createNotesBackup() Activity.pushToStack(tempModel) multipleStaff.eraseNote(noteIndex) } } 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: { var notesToSave = multipleStaff.createNotesBackup() - creationHandler.loadWindow() + creationHandler.saveWindow(notesToSave) } onClefAdded: { var insertingIndex = multipleStaff.selectedIndex === -1 ? multipleStaff.musicElementModel.count : multipleStaff.selectedIndex var tempModel = multipleStaff.createNotesBackup() Activity.pushToStack(tempModel) multipleStaff.addMusicElement("clef", "", "", false, false, background.clefType) if(background.clefType === "Bass") piano.currentOctaveNb = 0 else piano.currentOctaveNb = 1 } onBpmDecreased: { if(multipleStaff.bpmValue - 1 >= 1) multipleStaff.bpmValue-- } onBpmIncreased: { multipleStaff.bpmValue++ } onEmitOptionMessage: clickedOptionMessage.show(message) } DialogHelp { id: dialogHelp onClose: home() } Bar { id: bar content: BarEnumContent { value: help | home | level } onHelpClicked: { displayDialog(dialogHelp) } onPreviousLevelClicked: Activity.previousLevel() onNextLevelClicked: Activity.nextLevel() onHomeClicked: activity.home() } Bonus { id: bonus Component.onCompleted: win.connect(Activity.nextLevel) } } } diff --git a/src/core/File.cpp b/src/core/File.cpp index c18270d23..4be3727de 100644 --- a/src/core/File.cpp +++ b/src/core/File.cpp @@ -1,156 +1,161 @@ /* GCompris - File.cpp * * Copyright (C) 2014,2015 Holger Kaelberer * * Authors: * Holger Kaelberer * * 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 . */ #include "File.h" #include #include #include #include #include File::File(QObject *parent) : QObject(parent) { } QString File::name() const { return m_name; } QString File::sanitizeUrl(const QString &str) { QString target(str); // make sure we strip off invalid URL schemes: if (target.startsWith(QLatin1String("file://"))) target.remove(0, 7); else if (target.startsWith(QLatin1String("qrc:/"))) target.remove(0, 3); return target; } void File::setName(const QString &str) { QString target = sanitizeUrl(str); if (target != m_name) { m_name = target; emit nameChanged(); } } QString File::read(const QString& name) { if (!name.isEmpty()) setName(name); if (m_name.isEmpty()){ emit error("source is empty"); return QString(); } QFile file(m_name); QString fileContent; if (file.open(QIODevice::ReadOnly) ) { QString line; QTextStream t(&file); /* Force utf-8 : for some languages, it seems to be loaded in other encoding even if the file is in utf-8 */ t.setCodec("UTF-8"); do { line = t.readLine(); fileContent += line; } while (!line.isNull()); file.close(); } else { emit error("Unable to open the file"); return QString(); } return fileContent; } bool File::write(const QString& data, const QString& name) { if (!name.isEmpty()) setName(name); if (m_name.isEmpty()) { emit error("source is empty"); return false; } QFile file(m_name); if (!file.open(QFile::WriteOnly | QFile::Truncate)) { emit error("could not open file " + m_name); return false; } QTextStream out(&file); out << data; file.close(); return true; } bool File::append(const QString& data, const QString& name) { if (!name.isEmpty()) setName(name); if (m_name.isEmpty()) { emit error("source is empty"); return false; } QFile file(m_name); if (!file.open(QFile::WriteOnly | QFile::Append)) { emit error("could not open file " + m_name); return false; } QTextStream out(&file); out << data; file.close(); return true; } void File::init() { qmlRegisterType("GCompris", 1, 0, "File"); } bool File::exists(const QString& path) { return QFile::exists(sanitizeUrl(path)); } bool File::mkpath(const QString& path) { QDir dir; return dir.mkpath(dir.filePath(sanitizeUrl(path))); } + +bool File::rmpath(const QString& path) +{ + return QFile::remove(sanitizeUrl(path)); +} diff --git a/src/core/File.h b/src/core/File.h index bb58834df..423bd3fa1 100644 --- a/src/core/File.h +++ b/src/core/File.h @@ -1,126 +1,134 @@ /* GCompris - File.h * * Copyright (C) 2014,2015 Holger Kaelberer * * Authors: * Holger Kaelberer * * 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 . */ #ifndef FILE_H #define FILE_H #include #include /** * @class File * @short A helper component for accessing local files from QML. * @ingroup components * */ class File : public QObject { Q_OBJECT /** * Filename * * Accepted are absolute paths and URLs starting with the schemes * 'file://' and 'qrc://'. */ Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) public: /** * Constructor */ explicit File(QObject *parent = 0); /** * Reads contents of a file. * * @param name [optional] Filename to read from. If omitted reads from * the file specified by the member name. * @returns Whole file contents. * @sa name */ Q_INVOKABLE QString read(const QString& name = QString()); /** * Writes @p data to a file. * * @param data Text data to write. * @param name [optional] Filename to write to. If omitted writes to * the file specified by the member name. * @returns success of the operation. * @sa name */ Q_INVOKABLE bool write(const QString& data, const QString& name = QString()); /** * Apends @p data to a file. * * @param data Text data to appends. * @param name [optional] Filename to append to. If omitted writes to * the file specified by the member name. * @returns success of the operation. * @sa name */ Q_INVOKABLE bool append(const QString& data, const QString& name = QString()); /** * Checks whether file @p path exists. * * @param path Filename to check. * @returns @c true if @p path exists, @c false otherwise. */ Q_INVOKABLE static bool exists(const QString& path); /** * Creates directory @p path. * * Creates also all parent directories necessary to create the directory. * * @param path Directory to create. * @returns success */ Q_INVOKABLE static bool mkpath(const QString& path); + /** + * Deletes a file @p path. + * + * @param path file to delete. + * @returns success + */ + Q_INVOKABLE static bool rmpath(const QString& path); + /// @cond INTERNAL_DOCS static void init(); QString name() const; void setName(const QString &str); /// @endcond signals: /** * Emitted when the name changes. */ void nameChanged(); /** * Emitted when an error occurs. * * @param msg Error message. */ void error(const QString& msg); private: QString m_name; static QString sanitizeUrl(const QString& url); }; #endif diff --git a/src/core/GCCreationHandler.qml b/src/core/GCCreationHandler.qml index 5198753e5..006125b65 100644 --- a/src/core/GCCreationHandler.qml +++ b/src/core/GCCreationHandler.qml @@ -1,294 +1,350 @@ /* GCompris - GCCreationHandler.qml * * Copyright (C) 2018 Aman Kumar Gupta * * Authors: * Aman Kumar Gupta * * 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 QtQuick.Controls 1.5 import GCompris 1.0 import "qrc:/gcompris/src/core/core.js" as Core Rectangle { id: creationHandler width: parent.width height: parent.height color: "yellow" border.color: "black" border.width: 2 radius: 20 visible: false z: 2000 signal close signal fileLoaded(var data) onClose: { visible = false + viewContainer.selectedFileIndex = -1 fileNameInput.text = qsTr("Enter file name") } + MouseArea { + anchors.fill: parent + onClicked: viewContainer.selectedFileIndex = -1 + } + property string activityName: "" property var dataToSave property bool isSaveMode: false readonly property string sharedDirectoryPath: ApplicationInfo.getSharedWritablePath() + "/" + activityName + "/" + readonly property string fileSavePath: "file://" + sharedDirectoryPath + '/' + fileNameInput.text + ".json" ListModel { id: fileNames } Directory { id: directory } File { id: file onError: console.error("File error: " + msg); } JsonParser { id: parser onError: console.error("Error parsing JSON: " + msg); } Loader { id: replaceFileDialog sourceComponent: GCDialog { parent: activity.main message: qsTr("A file with this name already exists. Do you want to replace it?") button1Text: qsTr("Yes") button2Text: qsTr("No") onButton1Hit: { writeData() active = false } onButton2Hit: active = false } anchors.fill: parent focus: true active: false onStatusChanged: if (status == Loader.Ready) item.start() } function refreshWindow() { var pathExists = file.exists(sharedDirectoryPath) if(!pathExists) return fileNames.clear() var files = directory.getFiles(sharedDirectoryPath) for(var i = 2; i < files.length; i++) { fileNames.append({ "name": files[i] }) } } function loadWindow() { creationHandler.visible = true creationHandler.isSaveMode = false refreshWindow() } function loadFile(fileName) { - // Will be modified. - var data = parser.parseFromUrl("file://" + sharedDirectoryPath + fileName) + var filePath = "file://" + sharedDirectoryPath + fileNames.get(viewContainer.selectedFileIndex).name + var data = parser.parseFromUrl(filePath) creationHandler.fileLoaded(data) + creationHandler.close() + } + + function deleteFile() { + var filePath = "file://" + sharedDirectoryPath + fileNames.get(viewContainer.selectedFileIndex).name + if(file.rmpath(filePath)) { + Core.showMessageDialog(creationHandler, + qsTr("Deleted successfully!"), + "", null, "", null, null); + } + else { + Core.showMessageDialog(creationHandler, + qsTr("Unable to delete!"), + "", null, "", null, null); + } + + viewContainer.selectedFileIndex = -1 + refreshWindow() } function saveWindow(data) { creationHandler.visible = true creationHandler.isSaveMode = true creationHandler.dataToSave = data refreshWindow() } function saveFile() { if(activityName === "" || fileNameInput.text === "") return if(!file.exists(sharedDirectoryPath)) file.mkpath(sharedDirectoryPath) - if(file.exists("file://" + sharedDirectoryPath + '/' + fileNameInput.text + ".json")) { + if(file.exists(fileSavePath)) { replaceFileDialog.active = true } else writeData() } function writeData() { - file.write(JSON.stringify(creationHandler.dataToSave), "file://" + sharedDirectoryPath + '/' + fileNameInput.text + ".json") + file.write(JSON.stringify(creationHandler.dataToSave), fileSavePath) Core.showMessageDialog(creationHandler, qsTr("Saved successfully!"), "", null, "", null, null); refreshWindow() } function searchFiles() { + viewContainer.selectedFileIndex = -1 if(fileNameInput.text === "") { refreshWindow() return } var pathExists = file.exists(sharedDirectoryPath) if(!pathExists) return fileNames.clear() var files = directory.getFiles(sharedDirectoryPath) var textToSearch = fileNameInput.text.toLowerCase() for(var i = 2; i < files.length; i++) { if((files[i].toLowerCase()).indexOf(textToSearch) !== -1) fileNames.append({ "name": files[i] }) } } Rectangle { id: textField width: parent.width / 2 height: saveButton.height anchors.verticalCenter: saveButton.verticalCenter anchors.left: parent.left anchors.leftMargin: 20 border.width: 1 border.color: "black" TextInput { id: fileNameInput text: qsTr("Enter file name") font.pointSize: 28 anchors.fill: parent verticalAlignment: TextInput.AlignVCenter visible: textField.visible leftPadding: 10 selectByMouse: true maximumLength: 15 onTextChanged: { if(!creationHandler.isSaveMode) searchFiles() } } } Button { id: saveButton width: 50 * ApplicationInfo.ratio height: creationHandler.height / 15 visible: creationHandler.isSaveMode text: qsTr("Save") style: GCButtonStyle { theme: "highContrast" } anchors.top: parent.top anchors.topMargin: 30 anchors.left: textField.right anchors.leftMargin: 20 onClicked: saveFile() } property real cellWidth: 50 * ApplicationInfo.ratio property real cellHeight: cellWidth Rectangle { id: viewContainer anchors.top: cancelButton.bottom anchors.bottom: buttonRow.top anchors.margins: 20 border.color: "black" border.width: 2 radius: 20 anchors.left: parent.left anchors.right: parent.right + property int selectedFileIndex: -1 + + MouseArea { + anchors.fill: parent + onClicked: viewContainer.selectedFileIndex = -1 + } + GridView { id: creationsList model: fileNames width: parent.width - height: parent.height + height: parent.height - 10 interactive: true cellHeight: creationHandler.cellHeight cellWidth: creationHandler.cellWidth + anchors.top: parent.top + anchors.topMargin: 10 + clip: true delegate: Item { height: creationHandler.cellHeight width: creationHandler.cellWidth readonly property string fileName: fileName.text + Rectangle { + anchors.fill: parent + visible: index === viewContainer.selectedFileIndex + color: "red" + opacity: 0.4 + radius: 10 + anchors.top: parent.top + anchors.topMargin: -5 + } + Item { id: fileIcon width: creationHandler.cellWidth height: parent.height / 1.4 Image { source: "qrc:/gcompris/src/core/resource/FileIcon.svg" anchors.fill: parent } } GCText { id: fileName anchors.top: fileIcon.bottom height: parent.height - parent.height / 1.4 width: creationHandler.cellWidth font.pointSize: tinySize fontSizeMode: Text.Fit wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter // Exclude ".json" while displaying file name text: name.slice(0, name.length - 5) } + + MouseArea { + anchors.fill: parent + enabled: !creationHandler.isSaveMode + onClicked: viewContainer.selectedFileIndex = index + } } } } Row { id: buttonRow x: parent.width / 20 spacing: 20 anchors.bottom: parent.bottom anchors.bottomMargin: 10 visible: !creationHandler.isSaveMode Button { id: loadButton width: 70 * ApplicationInfo.ratio height: creationHandler.height / 15 text: "Load" + enabled: viewContainer.selectedFileIndex != -1 style: GCButtonStyle { theme: "highContrast" } + onClicked: creationHandler.loadFile() } Button { id: deleteButton width: 70 * ApplicationInfo.ratio height: creationHandler.height / 15 text: "Delete" + enabled: viewContainer.selectedFileIndex != -1 style: GCButtonStyle { theme: "highContrast" } + onClicked: deleteFile() } } GCButtonCancel { id: cancelButton onClose: { parent.close() } } }