diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index 52f5f2b..72bae02 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -1,73 +1,75 @@
set(minuet_SRCS
main.cpp
core.cpp
uicontroller.cpp
plugincontroller.cpp
exercisecontroller.cpp
)
qt5_add_resources(minuet_SRCS qml.qrc)
IF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android")
set(minuet_ICONS_PNG
${CMAKE_CURRENT_SOURCE_DIR}/icons/16-apps-minuet.png
${CMAKE_CURRENT_SOURCE_DIR}/icons/22-apps-minuet.png
${CMAKE_CURRENT_SOURCE_DIR}/icons/32-apps-minuet.png
${CMAKE_CURRENT_SOURCE_DIR}/icons/48-apps-minuet.png
${CMAKE_CURRENT_SOURCE_DIR}/icons/64-apps-minuet.png
${CMAKE_CURRENT_SOURCE_DIR}/icons/128-apps-minuet.png
)
set(minuet_ICONS_SVG
${CMAKE_CURRENT_SOURCE_DIR}/icons/sc-apps-minuet.svgz
${CMAKE_CURRENT_SOURCE_DIR}/icons/16-apps-minuet.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/22-apps-minuet.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/32-apps-minuet.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/48-apps-minuet.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/64-apps-minuet.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/128-apps-minuet.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/16-actions-minuet-scales.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/16-actions-minuet-intervals.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/16-actions-minuet-chords.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/16-actions-minuet-rhythms.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/22-actions-minuet-scales.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/22-actions-minuet-intervals.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/22-actions-minuet-chords.svg
${CMAKE_CURRENT_SOURCE_DIR}/icons/22-actions-minuet-rhythms.svg
)
ecm_add_app_icon(minuet_SRCS ICONS ${minuet_ICONS_PNG})
ecm_install_icons(ICONS ${minuet_ICONS_PNG} ${minuet_ICONS_SVG} DESTINATION ${ICON_INSTALL_DIR} THEME hicolor)
ENDIF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android")
add_executable(minuet ${minuet_SRCS})
target_link_libraries(minuet
Qt5::Core
Qt5::Gui
Qt5::Qml
Qt5::Quick
Qt5::QuickControls2
Qt5::Svg
Minuet::Interfaces
)
IF(${CMAKE_SYSTEM_NAME} MATCHES "Android")
target_link_libraries(minuet
Minuet::CsoundSoundController
)
ENDIF(${CMAKE_SYSTEM_NAME} MATCHES "Android")
IF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android")
target_link_libraries(minuet
KF5::CoreAddons
KF5::I18n
KF5::Crash
)
ENDIF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android")
IF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android")
install(TARGETS minuet ${INSTALL_TARGETS_DEFAULT_ARGS})
install(PROGRAMS org.kde.minuet.desktop DESTINATION ${XDG_APPS_INSTALL_DIR})
install(TARGETS minuet RUNTIME DESTINATION bin)
ENDIF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android")
+
+add_subdirectory(plugins)
\ No newline at end of file
diff --git a/src/app/plugins/CMakeLists.txt b/src/app/plugins/CMakeLists.txt
new file mode 100644
index 0000000..df2e556
--- /dev/null
+++ b/src/app/plugins/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(minuet_PLUGINS
+ PianoView.json
+)
+
+add_subdirectory(PianoView)
+
+install(FILES ${minuet_PLUGINS} DESTINATION ${KDE_INSTALL_DATADIR}/minuet/plugins)
diff --git a/src/app/plugins/PianoView.json b/src/app/plugins/PianoView.json
new file mode 100644
index 0000000..4d9a908
--- /dev/null
+++ b/src/app/plugins/PianoView.json
@@ -0,0 +1,5 @@
+{
+ "menuName": "PianoView",
+ "iconName": "group",
+ "mainPage": "PianoView.qml"
+}
diff --git a/src/app/qml/PianoView/BlackKey.qml b/src/app/plugins/PianoView/BlackKey.qml
similarity index 100%
rename from src/app/qml/PianoView/BlackKey.qml
rename to src/app/plugins/PianoView/BlackKey.qml
diff --git a/src/app/plugins/PianoView/CMakeLists.txt b/src/app/plugins/PianoView/CMakeLists.txt
new file mode 100644
index 0000000..93ecf1f
--- /dev/null
+++ b/src/app/plugins/PianoView/CMakeLists.txt
@@ -0,0 +1,8 @@
+set(minuet_PIANO
+ BlackKey.qml
+ Octave.qml
+ PianoView.qml
+ WhiteKey.qml
+)
+
+install(FILES ${minuet_PIANO} DESTINATION ${KDE_INSTALL_DATADIR}/minuet/plugins/PianoView)
diff --git a/src/app/qml/PianoView/Octave.qml b/src/app/plugins/PianoView/Octave.qml
similarity index 100%
rename from src/app/qml/PianoView/Octave.qml
rename to src/app/plugins/PianoView/Octave.qml
diff --git a/src/app/qml/PianoView/PianoView.qml b/src/app/plugins/PianoView/PianoView.qml
similarity index 100%
rename from src/app/qml/PianoView/PianoView.qml
rename to src/app/plugins/PianoView/PianoView.qml
diff --git a/src/app/qml/PianoView/WhiteKey.qml b/src/app/plugins/PianoView/WhiteKey.qml
similarity index 100%
rename from src/app/qml/PianoView/WhiteKey.qml
rename to src/app/plugins/PianoView/WhiteKey.qml
diff --git a/src/app/qml.qrc b/src/app/qml.qrc
index 395f30f..99b4a20 100644
--- a/src/app/qml.qrc
+++ b/src/app/qml.qrc
@@ -1,53 +1,50 @@
qml/Main.qml
qml/AboutDialog.qml
qml/MinuetMenu.qml
qml/MinuetMenuContainer.qml
qml/ImageItemDelegate.qml
+android/qml/MinuetMenuContainer.qml
qml/ExerciseView.qml
- qml/PianoView/BlackKey.qml
- qml/PianoView/Octave.qml
- qml/PianoView/PianoView.qml
- qml/PianoView/WhiteKey.qml
- qml/SheetMusicView/Bravura.otf
- qml/SheetMusicView/BravuraText.qml
- qml/SheetMusicView/Clef.qml
- qml/SheetMusicView/Note.qml
- qml/SheetMusicView/Score.qml
- qml/SheetMusicView/Sequence.qml
- qml/SheetMusicView/SheetMusicView.qml
+ qml/InstrumentView/InstrumentView.qml
+ qml/SheetMusicView/Bravura.otf
+ qml/SheetMusicView/BravuraText.qml
+ qml/SheetMusicView/Clef.qml
+ qml/SheetMusicView/Note.qml
+ qml/SheetMusicView/Score.qml
+ qml/SheetMusicView/Sequence.qml
+ qml/SheetMusicView/SheetMusicView.qml
qml/images/minuet-background.png
qml/images/minuet-drawer.png
qml/images/menu.png
qml/images/keyboard_arrow_left.png
qml/images/more_vert.png
icons/64-apps-minuet.png
icons/22-actions-minuet-chords.svg
icons/22-actions-minuet-intervals.svg
icons/22-actions-minuet-rhythms.svg
icons/22-actions-minuet-scales.svg
../../data/exercise-images/current-rhythm.png
../../data/exercise-images/eighth-dot-sixteenth.png
../../data/exercise-images/eighth-eighth.png
../../data/exercise-images/eighth-sixteenth-sixteenth.png
../../data/exercise-images/quarter.png
../../data/exercise-images/sixteenth-eighth-dot.png
../../data/exercise-images/sixteenth-eighth-sixteenth.png
../../data/exercise-images/sixteenth-sixteenth-eighth.png
../../data/exercise-images/sixteenth-sixteenth-sixteenth-sixteenth.png
../../data/exercise-images/unknown-rhythm.png
qtquickcontrols2.conf
+android/qtquickcontrols2.conf
+windows/qtquickcontrols2.conf
diff --git a/src/app/qml/ExerciseView.qml b/src/app/qml/ExerciseView.qml
index b4755ae..05f2b76 100644
--- a/src/app/qml/ExerciseView.qml
+++ b/src/app/qml/ExerciseView.qml
@@ -1,479 +1,481 @@
/****************************************************************************
**
** Copyright (C) 2016 by Sandro S. Andrade
**
** 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 2 of
** the License or (at your option) version 3 or any later version
** accepted by the membership of KDE e.V. (or its successor approved
** by the membership of KDE e.V.), which shall act as a proxy
** defined in Section 14 of version 3 of the license.
**
** 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.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import QtQuick.Window 2.0
Item {
id: exerciseView
visible: currentExercise != undefined
property var currentExercise
QtObject {
id: internal
property int currentAnswer
property var colors: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd", "#ccebc5", "#ffed6f", "#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a", "#ffff99", "#b15928"]
property Item rightAnswerRectangle
property variant userAnswers: []
property bool answersAreRight
property bool giveUp
property bool isTest: false
property int correctAnswers: 0
property int currentExercise: 0
property int maximumExercises: 10
onCurrentAnswerChanged: {
for (var i = 0; i < yourAnswersParent.children.length; ++i)
yourAnswersParent.children[i].destroy()
yourAnswersParent.children = ""
for (var i = 0; i < currentAnswer; ++i)
answerOption.createObject(yourAnswersParent, {"model": userAnswers[i].model, "index": userAnswers[i].index, "position": i, "color": userAnswers[i].color, "border.width": 2})
}
}
onCurrentExerciseChanged: {
clearUserAnswers()
for (var i = 0; i < answerGrid.children.length; ++i)
answerGrid.children[i].destroy()
answerGrid.children = ""
if (currentExercise != undefined) {
var currentExerciseOptions = currentExercise["options"];
if (currentExerciseOptions != undefined) {
var length = currentExerciseOptions.length
for (var i = 0; i < length; ++i)
answerOption.createObject(answerGrid, {"model": currentExerciseOptions[i], "index": i, "color": internal.colors[i%24]})
}
sheetMusicView.spaced = (currentExercise["playMode"] == "chord") ? false:true
messageText.text = i18n("Click 'New Question' to start!")
exerciseView.state = "waitingForNewQuestion"
}
}
function clearUserAnswers() {
- pianoView.clearAllMarks()
+ instrumentView.clearAllMarks()
sheetMusicView.clearAllMarks()
for (var i = 0; i < yourAnswersParent.children.length; ++i)
yourAnswersParent.children[i].destroy()
yourAnswersParent.children = ""
internal.currentAnswer = 0
internal.userAnswers = []
}
function checkAnswers() {
var rightAnswers = core.exerciseController.selectedExerciseOptions
internal.answersAreRight = true
for (var i = 0; i < currentExercise.numberOfSelectedOptions; ++i) {
if (internal.userAnswers[i].name != rightAnswers[i].name) {
yourAnswersParent.children[i].border.color = "red"
internal.answersAreRight = false
}
else {
yourAnswersParent.children[i].border.color = "green"
if (internal.isTest)
internal.correctAnswers++
}
}
messageText.text = (internal.giveUp) ? i18n("Here is the answer") : (internal.answersAreRight) ? i18n("Congratulations, you answered correctly!"):i18n("Oops, not this time! Try again!")
if (internal.currentExercise == internal.maximumExercises) {
messageText.text = i18n("You answered correctly %1%", internal.correctAnswers * 100 / internal.maximumExercises)
resetTest()
}
if (currentExercise.numberOfSelectedOptions == 1)
highlightRightAnswer()
else
exerciseView.state = "waitingForNewQuestion"
internal.giveUp = false
}
function highlightRightAnswer() {
var chosenExercises = core.exerciseController.selectedExerciseOptions
for (var i = 0; i < answerGrid.children.length; ++i) {
if (answerGrid.children[i].model.name != chosenExercises[0].name) {
answerGrid.children[i].opacity = 0.25
}
else {
internal.rightAnswerRectangle = answerGrid.children[i]
answerGrid.children[i].opacity = 1
}
}
var array = [core.exerciseController.chosenRootNote()]
internal.rightAnswerRectangle.model.sequence.split(' ').forEach(function(note) {
- pianoView.noteMark(0, core.exerciseController.chosenRootNote() + parseInt(note), 0, internal.rightAnswerRectangle.color)
+ instrumentView.noteMark(0, core.exerciseController.chosenRootNote() + parseInt(note), 0, internal.rightAnswerRectangle.color)
array.push(core.exerciseController.chosenRootNote() + parseInt(note))
})
sheetMusicView.model = array
animation.start()
}
function resetTest() {
internal.isTest = false
internal.correctAnswers = 0
internal.currentExercise = 0
}
function nextTestExercise() {
for (var i = 0; i < answerGrid.children.length; ++i)
answerGrid.children[i].opacity = 1
- pianoView.clearAllMarks()
+ instrumentView.clearAllMarks()
sheetMusicView.clearAllMarks()
clearUserAnswers()
generateNewQuestion(true)
core.soundController.play()
}
function generateNewQuestion () {
clearUserAnswers()
if (internal.isTest)
messageText.text = i18n("Question %1 out of %2", internal.currentExercise + 1, internal.maximumExercises)
else
messageText.text = ""
core.exerciseController.randomlySelectExerciseOptions()
var chosenExercises = core.exerciseController.selectedExerciseOptions
core.soundController.prepareFromExerciseOptions(chosenExercises)
if (currentExercise["playMode"] != "rhythm") {
- pianoView.noteMark(0, core.exerciseController.chosenRootNote(), 0, "white")
- pianoView.scrollToNote(core.exerciseController.chosenRootNote())
+ instrumentView.noteMark(0, core.exerciseController.chosenRootNote(), 0, "white")
+ instrumentView.scrollToNote(core.exerciseController.chosenRootNote())
sheetMusicView.model = [core.exerciseController.chosenRootNote()]
sheetMusicView.clef.type = (core.exerciseController.chosenRootNote() >= 60) ? 0:1
}
exerciseView.state = "waitingForAnswer"
if (internal.isTest)
internal.currentExercise++
}
ColumnLayout {
anchors.fill: parent
spacing: Screen.width >= 1024 ? 20:10
Text {
id: userMessage
Layout.preferredWidth: parent.width
horizontalAlignment: Text.AlignHCenter
font.pointSize: Screen.width >= 1024 ? 18:14
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
text: (currentExercise != undefined) ? i18nc("technical term, do you have a musician friend?", currentExercise["userMessage"]):""
}
Text {
id: messageText
font.pointSize: Screen.width >= 1024 ? 18:14
Layout.preferredWidth: parent.width
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 10
Button {
id: newPlayQuestionButton
width: 120; height: 40
text: (exerciseView.state == "waitingForNewQuestion") ? i18n("New Question"):i18n("Play Question")
enabled: !animation.running
onClicked: {
if (exerciseView.state == "waitingForNewQuestion") {
generateNewQuestion()
}
core.soundController.play()
}
}
Button {
id: giveUpButton
width: 120; height: 40
text: i18n("Give Up")
enabled: exerciseView.state == "waitingForAnswer" && !animation.running
onClicked: {
if (internal.isTest)
internal.correctAnswers--
internal.giveUp = true
var rightAnswers = core.exerciseController.selectedExerciseOptions
internal.userAnswers = []
for (var i = 0; i < currentExercise.numberOfSelectedOptions; ++i) {
for (var j = 0; j < answerGrid.children.length; ++j) {
if (answerGrid.children[j].model.name == rightAnswers[i].name) {
internal.userAnswers.push({"name": rightAnswers[i].name, "model": answerGrid.children[j].model, "index": j, "color": internal.colors[j]})
break
}
}
}
internal.currentAnswer = currentExercise.numberOfSelectedOptions
checkAnswers()
}
}
Button {
id: testButton
width: 120; height: 40
text: internal.isTest ? i18n("Stop Test") : i18n("Start Test")
enabled: true
onClicked: {
if (!internal.isTest) {
resetTest()
internal.isTest = true
generateNewQuestion()
if (internal.isTest)
core.soundController.play()
} else {
resetTest()
exerciseView.state = "waitingForNewQuestion"
messageText.text = i18n("Click 'New Question' to start")
}
}
}
}
GroupBox {
id: availableAnswers
title: i18n("Available Answers")
anchors.horizontalCenter: parent.horizontalCenter
Layout.preferredWidth: parent.width
Layout.fillHeight: true
Flickable {
anchors.fill: parent
contentHeight: answerGrid.height
clip: true
Grid {
id: answerGrid
anchors.centerIn: parent
spacing: 10
columns: Math.max(1, parent.width / (((currentExercise != undefined && currentExercise["playMode"] != "rhythm") ? 120:119) + spacing))
Component {
id: answerOption
Rectangle {
id: answerRectangle
property var model
property int index
property int position
width: (currentExercise != undefined && currentExercise["playMode"] != "rhythm") ? 120:119
height: (currentExercise != undefined && currentExercise["playMode"] != "rhythm") ? 40:59
Text {
id: option
property string originalText: model.name
visible: currentExercise != undefined && currentExercise["playMode"] != "rhythm"
text: i18nc("technical term, do you have a musician friend?", model.name)
width: parent.width - 4
anchors.centerIn: parent
horizontalAlignment: Qt.AlignHCenter
color: "black"
wrapMode: Text.Wrap
}
Image {
id: rhythmImage
anchors.centerIn: parent
visible: currentExercise != undefined && currentExercise["playMode"] == "rhythm"
source: (currentExercise != undefined && currentExercise["playMode"] == "rhythm") ? "exercise-images/" + model.name + ".png":""
fillMode: Image.Pad
}
MouseArea {
anchors.fill: parent
onClicked: {
if (exerciseView.state == "waitingForAnswer" && !animation.running) {
onExited()
internal.userAnswers.push({"name": option.originalText, "model": answerRectangle.model, "index": answerRectangle.index, "color": answerRectangle.color})
internal.currentAnswer++
if (internal.currentAnswer == currentExercise.numberOfSelectedOptions)
checkAnswers()
}
}
hoverEnabled: Qt.platform.os != "android" && !animation.running
onEntered: {
answerRectangle.color = Qt.darker(answerRectangle.color, 1.1)
if (currentExercise["playMode"] != "rhythm" && exerciseView.state == "waitingForAnswer") {
if (parent.parent == answerGrid) {
var array = [core.exerciseController.chosenRootNote()]
model.sequence.split(' ').forEach(function(note) {
array.push(core.exerciseController.chosenRootNote() + parseInt(note))
- pianoView.noteMark(0, core.exerciseController.chosenRootNote() + parseInt(note), 0, internal.colors[answerRectangle.index])
+ instrumentView.noteMark(0, core.exerciseController.chosenRootNote() + parseInt(note), 0, internal.colors[answerRectangle.index])
})
sheetMusicView.model = array
}
}
else {
var rightAnswers = core.exerciseController.selectedExerciseOptions
if (parent.parent == yourAnswersParent && internal.userAnswers[position].name != rightAnswers[position].name) {
parent.border.color = "green"
for (var i = 0; i < answerGrid.children.length; ++i) {
if (answerGrid.children[i].model.name == rightAnswers[position].name) {
parent.color = answerGrid.children[i].color
break
}
}
rhythmImage.source = "exercise-images/" + rightAnswers[position].name + ".png"
}
}
}
onExited: {
answerRectangle.color = internal.colors[answerRectangle.index]
if (currentExercise["playMode"] != "rhythm") {
if (parent.parent == answerGrid) {
if (!animation.running)
model.sequence.split(' ').forEach(function(note) {
- pianoView.noteUnmark(0, core.exerciseController.chosenRootNote() + parseInt(note), 0)
+ instrumentView.noteUnmark(0, core.exerciseController.chosenRootNote() + parseInt(note), 0)
})
sheetMusicView.model = [core.exerciseController.chosenRootNote()]
}
}
else {
var rightAnswers = core.exerciseController.selectedExerciseOptions
if (parent.parent == yourAnswersParent && internal.userAnswers[position].name != rightAnswers[position].name) {
parent.border.color = "red"
parent.color = internal.userAnswers[position].color
rhythmImage.source = "exercise-images/" + internal.userAnswers[position].name + ".png"
}
}
}
}
}
}
}
ScrollIndicator.vertical: ScrollIndicator { active: true }
}
}
GroupBox {
id: yourAnswers
title: i18n("Your Answer(s)")
Layout.preferredWidth: parent.width
anchors.horizontalCenter: parent.horizontalCenter
contentHeight: ((currentExercise != undefined && currentExercise["playMode"] != "rhythm") ? 40:59)
Flickable {
width: (currentExercise != undefined) ? Math.min(parent.width, internal.currentAnswer*130):0; height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
contentWidth: (currentExercise != undefined) ? internal.currentAnswer*130:0
boundsBehavior: Flickable.StopAtBounds
clip: true
Row {
id: yourAnswersParent
anchors.centerIn: parent
spacing: Screen.width >= 1024 ? 10:5
}
ScrollIndicator.horizontal: ScrollIndicator { active: true }
}
}
Button {
id: backspaceButton
text: i18n("Backspace")
anchors.horizontalCenter: parent.horizontalCenter
visible: currentExercise != undefined && currentExercise["playMode"] == "rhythm"
enabled: internal.currentAnswer > 0 && internal.currentAnswer < currentExercise.numberOfSelectedOptions
onClicked: {
internal.userAnswers.pop()
internal.currentAnswer--
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
Layout.preferredWidth: parent.width
spacing: (parent.width/2 - sheetMusicView.width)/2
- PianoView {
- id: pianoView
+
+ InstrumentView {
+ id: instrumentView
width: parent.width/2 - 10
+ height: 150
visible: currentExercise != undefined && currentExercise["playMode"] != "rhythm"
- ScrollIndicator.horizontal: ScrollIndicator { active: true }
}
+
SheetMusicView {
id: sheetMusicView
- height: pianoView.height
+ height: instrumentView.height
anchors { bottom: parent.bottom; bottomMargin: 15 }
visible: currentExercise != undefined && currentExercise["playMode"] != "rhythm"
}
}
}
states: [
State {
name: "waitingForNewQuestion"
},
State {
name: "waitingForAnswer"
StateChangeScript {
script: {
for (var i = 0; i < answerGrid.children.length; ++i) {
answerGrid.children[i].opacity = 1
}
}
}
}
]
ParallelAnimation {
id: animation
loops: 2
SequentialAnimation {
PropertyAnimation { target: internal.rightAnswerRectangle; property: "rotation"; to: -45; duration: 200 }
PropertyAnimation { target: internal.rightAnswerRectangle; property: "rotation"; to: 45; duration: 200 }
PropertyAnimation { target: internal.rightAnswerRectangle; property: "rotation"; to: 0; duration: 200 }
}
SequentialAnimation {
PropertyAnimation { target: internal.rightAnswerRectangle; property: "scale"; to: 1.2; duration: 300 }
PropertyAnimation { target: internal.rightAnswerRectangle; property: "scale"; to: 1.0; duration: 300 }
}
onStopped: {
exerciseView.state = internal.isTest ? "waitingForAnswer" : "waitingForNewQuestion"
if (internal.isTest) {
nextTestExercise()
if (internal.currentExercise == internal.maximumExercises+1)
internal.isTest = false
}
}
}
Connections {
target: core.exerciseController
- onSelectedExerciseOptionsChanged: pianoView.clearAllMarks()
+ onSelectedExerciseOptionsChanged: instrumentView.clearAllMarks()
}
Connections {
target: core.exerciseController
onSelectedExerciseOptionsChanged: sheetMusicView.clearAllMarks()
}
}
diff --git a/src/app/qml/InstrumentView/InstrumentView.qml b/src/app/qml/InstrumentView/InstrumentView.qml
new file mode 100644
index 0000000..680439e
--- /dev/null
+++ b/src/app/qml/InstrumentView/InstrumentView.qml
@@ -0,0 +1,96 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 by Stefan Toncu
+**
+** 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 2 of
+** the License or (at your option) version 3 or any later version
+** accepted by the membership of KDE e.V. (or its successor approved
+** by the membership of KDE e.V.), which shall act as a proxy
+** defined in Section 14 of version 3 of the license.
+**
+** 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.Controls 2.2
+import QtQuick 2.7
+
+Item {
+ property alias pluginMainPageLoader2: pluginMainPageLoader2
+
+ function noteOn(chan, pitch, vel) {
+ if (pluginMainPageLoader2.item)
+ pluginMainPageLoader2.item.noteOn(chan, pitch, vel)
+ }
+ function noteOff(chan, pitch, vel) {
+ if (pluginMainPageLoader2.item)
+ pluginMainPageLoader2.item.noteOff(chan, pitch, vel)
+ }
+ function noteMark(chan, pitch, vel, color) {
+ if (pluginMainPageLoader2.item)
+ pluginMainPageLoader2.item.noteMark(chan, pitch, vel, color)
+ }
+ function noteUnmark(chan, pitch, vel, color) {
+ if (pluginMainPageLoader2.item)
+ pluginMainPageLoader2.item.noteUnmark(chan, pitch, vel, color)
+ }
+ function clearAllMarks() {
+ if (pluginMainPageLoader2.item)
+ pluginMainPageLoader2.item.clearAllMarks()
+ }
+ function scrollToNote(pitch) {
+ if (pluginMainPageLoader2.item)
+ pluginMainPageLoader2.item.scrollToNote(pitch)
+ }
+ function highlightKey(pitch, color) {
+ if (pluginMainPageLoader2.item)
+ pluginMainPageLoader2.item.highlightKey(pitch, color)
+ }
+ function itemForPitch(pitch) {
+ if (pluginMainPageLoader2.item)
+ pluginMainPageLoader2.item.itemForPitch(pitch)
+ }
+
+ TabBar {
+ id: tabBar
+ anchors.top: parent.top
+ Repeater {
+ id: tabBar_repeater
+ model: contents
+ TabButton {
+ id: button
+ text: qsTr(modelData.menuName)
+ onClicked: {
+ console.log(modelData.pluginName + "/" + modelData.mainPage)
+ pluginMainPageLoader2.source = "file://" + modelData.pluginName + "/" + modelData.mainPage
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: parent.height - tabBar.height
+ anchors.bottom: parent.bottom
+ color: "grey"
+ Loader {
+ anchors.fill: parent
+ id: pluginMainPageLoader2
+ }
+ }
+
+ Component.onCompleted: {
+ //load the first available plugin instrument
+ if (!contents[0])
+ console.log("No plugin available!")
+ else
+ pluginMainPageLoader2.source = "file://" + contents[0].pluginName + "/" + contents[0].mainPage
+ }
+}
diff --git a/src/app/qml/Main.qml b/src/app/qml/Main.qml
index dfc46b6..5c272c0 100644
--- a/src/app/qml/Main.qml
+++ b/src/app/qml/Main.qml
@@ -1,207 +1,207 @@
/****************************************************************************
**
** Copyright (C) 2016 by Sandro S. Andrade
**
** 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 2 of
** the License or (at your option) version 3 or any later version
** accepted by the membership of KDE e.V. (or its successor approved
** by the membership of KDE e.V.), which shall act as a proxy
** defined in Section 14 of version 3 of the license.
**
** 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.7
-import QtQuick.Controls 2.0
+import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.0
import QtQuick.Controls.Material 2.0
ApplicationWindow {
id: applicationWindow
visible: true
width: Screen.desktopAvailableWidth; height: Screen.desktopAvailableHeight
property string titleText: "Minuet"
Component {
id: androidToolBar
ToolBar {
Material.primary: "#181818"
Material.foreground: "white"
RowLayout {
spacing: 20
anchors.fill: parent
height: parent.height / 5
ToolButton {
contentItem: Image {
fillMode: Image.Pad
horizontalAlignment: Image.AlignHCenter
verticalAlignment: Image.AlignVCenter
source: "qrc:/menu.png"
}
onClicked: drawer.open()
}
Label {
text: titleText
elide: Label.ElideRight
font { weight: Font.Bold; pixelSize: 16 }
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
Layout.fillWidth: true
}
ToolButton {
contentItem: Image {
fillMode: Image.Pad
horizontalAlignment: Image.AlignHCenter
verticalAlignment: Image.AlignVCenter
source: "qrc:/more_vert.png"
}
onClicked: optionsMenu.open()
Menu {
id: optionsMenu
x: parent.width - width
transformOrigin: Menu.TopRight
MenuItem {
text: "About"
onTriggered: aboutDialog.open()
}
}
}
}
}
}
Item {
id: mainContainer
anchors { right: parent.right; top: parent.top; bottom: parent.bottom; left: (Qt.platform.os == "android") ? parent.left:drawer.right; margins: Screen.width >= 1024 ? 20:5 }
Image {
source: "qrc:/qml/images/minuet-background.png"
anchors.fill: parent
fillMode: Image.Tile
}
ExerciseView {
id: exerciseView
anchors.fill: parent
currentExercise: minuetMenu.currentExercise
}
/* THIS IS THE DASHBOARD
Frame {
id: frame
anchors { fill: parent; margins: 15 }
Label {
id: greetings
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
text: "Hi, what kind of ear training exercise do you want to practice today?"
font { family: "Roboto" }
}
Grid {
rows: 2
columns: 2
anchors.centerIn: parent
spacing: 40
Repeater {
model: [
{ icon: "qrc:/minuet-chords.svg", title: "Chords" },
{ icon: "qrc:/minuet-intervals.svg", title: "Intervals" },
{ icon: "qrc:/minuet-rhythms.svg", title: "Rhythms" },
{ icon: "qrc:/minuet-scales.svg", title: "Scales" }
]
Column {
Image {
source: modelData.icon
fillMode: Image.PreserveAspectFit
sourceSize.width: frame.width/4;
width: frame.width/4; height: width
anchors.horizontalCenter: parent.horizontalCenter
MouseArea {
anchors.fill: parent
onClicked: {
frame.visible = true
stackView.currentExerciseMenuItem = null
exerciseController.currentExercise ={}
titleText = "Minuet"
while (stackView.depth > 1) {
stackView.pop()
minuetMenu.exerciseArray.pop()
currentExerciseParent.text = minuetMenu.exerciseArray.toString()
minuetMenu.backPressed()
}
for (var i = 0; i < exerciseController.exercises.length; ++i) {
if (exerciseController.exercises[i].name == modelData.title) {
frame.visible = true
stackView.push(categoryMenu.createObject(stackView, {model: exerciseController.exercises[i].children}))
currentExerciseParent.text = exerciseController.exercises[i].name
minuetMenu.exerciseArray.push(exerciseController.exercises[i].name)
break
}
}
drawer.open()
}
}
}
Label {
width: frame.width/4
wrapMode: Text.WordWrap
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
text: modelData.title
font { family: "Roboto" }
}
}
}
}
}
*/
}
MinuetMenuContainer {
id: drawer
MinuetMenu {
id: minuetMenu
onBackPressed: {
exerciseView.resetTest()
core.soundController.reset()
}
onCurrentExerciseChanged: if (Qt.platform.os == "android" && currentExercise != undefined) drawer.close()
}
}
AboutDialog {
id: aboutDialog
}
Binding {
target: core.exerciseController
property: "currentExercise"
value: minuetMenu.currentExercise
}
Binding {
target: core.soundController
property: "playMode"
value: (minuetMenu.currentExercise != undefined) ? minuetMenu.currentExercise["playMode"]:""
}
Component.onCompleted: if (Qt.platform.os == "android") header = androidToolBar.createObject(applicationWindow)
}
diff --git a/src/app/uicontroller.cpp b/src/app/uicontroller.cpp
index 15cd207..43d3732 100644
--- a/src/app/uicontroller.cpp
+++ b/src/app/uicontroller.cpp
@@ -1,70 +1,106 @@
/****************************************************************************
**
** Copyright (C) 2016 by Sandro S. Andrade
**
** 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 2 of
** the License or (at your option) version 3 or any later version
** accepted by the membership of KDE e.V. (or its successor approved
** by the membership of KDE e.V.), which shall act as a proxy
** defined in Section 14 of version 3 of the license.
**
** 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 "uicontroller.h"
#include "core.h"
#include
#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
#ifndef Q_OS_ANDROID
#include
#endif
namespace Minuet
{
UiController::UiController(QObject *parent)
: IUiController(parent)
{
}
UiController::~UiController()
{
}
+bool UiController::initializePlugins()
+{
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+
+ QString directoryName = "plugins";
+ QString minuet_dir = QStandardPaths::locate(QStandardPaths::AppDataLocation, directoryName, QStandardPaths::LocateDirectory);
+ QDir dir(minuet_dir);
+ qDebug()<applicationDirPath();
+ QString contents;
+ QJsonArray mergedArray;
+ foreach(const QString &fileName, dir.entryList(QStringList() << "*.json")) {
+ QFile dfile(dir.absoluteFilePath(fileName));
+ dfile.open(QIODevice::ReadOnly);
+ QJsonObject jsonObject = QJsonDocument::fromJson(dfile.readAll()).object();
+ QDir pluginDir(dir);
+ pluginDir.cd(fileName.split('.').first());
+ jsonObject["pluginName"] = pluginDir.absolutePath();
+ mergedArray.append(jsonObject);
+ dfile.close();
+ }
+ engine->rootContext()->setContextProperty("contents", mergedArray);
+}
+
bool UiController::initialize(Core *core)
{
m_errorString.clear();
- QQmlApplicationEngine *engine = new QQmlApplicationEngine(this);
+ engine = new QQmlApplicationEngine(this);
QQmlContext *rootContext = engine->rootContext();
rootContext->setContextProperty(QStringLiteral("core"), core);
#ifndef Q_OS_ANDROID
rootContext->setContextObject(new KLocalizedContext(engine));
#else
rootContext->setContextObject(new DummyAndroidLocalizer(engine));
#endif
+
+ initializePlugins();
engine->load(QUrl("qrc:/Main.qml"));
return true;
}
QString UiController::errorString() const
{
return m_errorString;
}
}
diff --git a/src/app/uicontroller.h b/src/app/uicontroller.h
index a6149c3..dec996f 100644
--- a/src/app/uicontroller.h
+++ b/src/app/uicontroller.h
@@ -1,72 +1,75 @@
/****************************************************************************
**
** Copyright (C) 2016 by Sandro S. Andrade
**
** 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 2 of
** the License or (at your option) version 3 or any later version
** accepted by the membership of KDE e.V. (or its successor approved
** by the membership of KDE e.V.), which shall act as a proxy
** defined in Section 14 of version 3 of the license.
**
** 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 MINUET_UICONTROLLER_H
#define MINUET_UICONTROLLER_H
#include
+#include
#ifdef Q_OS_ANDROID
#include
class DummyAndroidLocalizer : public QObject
{
Q_OBJECT
public:
explicit DummyAndroidLocalizer(QObject *parent = 0) : QObject(parent) { }
Q_INVOKABLE QString i18n (const QString &message, const QVariant &p1=QVariant(), const QVariant &p2=QVariant(), const QVariant &p3=QVariant(), const QVariant &p4=QVariant(), const QVariant &p5=QVariant(), const QVariant &p6=QVariant(), const QVariant &p7=QVariant(), const QVariant &p8=QVariant(), const QVariant &p9=QVariant(), const QVariant &p10=QVariant()) const
{
return message;
}
Q_INVOKABLE QString i18nc (const QString &context, const QString &message, const QVariant &p1=QVariant(), const QVariant &p2=QVariant(), const QVariant &p3=QVariant(), const QVariant &p4=QVariant(), const QVariant &p5=QVariant(), const QVariant &p6=QVariant(), const QVariant &p7=QVariant(), const QVariant &p8=QVariant(), const QVariant &p9=QVariant(), const QVariant &p10=QVariant()) const
{
return message;
}
};
#endif
namespace Minuet
{
class Core;
class UiController : public IUiController
{
Q_OBJECT
public:
UiController(QObject *parent = 0);
~UiController() override;
+ QQmlApplicationEngine *engine;
bool initialize(Core *core);
+ bool initializePlugins();
virtual QString errorString() const;
private:
QString m_errorString;
};
}
#endif