Last active
August 29, 2015 13:56
-
-
Save quarnster/9150668 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import QtQuick 2.0 | |
import QtQuick.Controls 1.1 | |
import QtQuick.Layouts 1.0 | |
ApplicationWindow { | |
id: window | |
property var lyrics: gocode.project.lyrics | |
width: 800 | |
height: 600 | |
property int scale: 1 | |
property var pixelscale: 100 | |
property int add: window.width/(2*window.pixelscale) | |
title: "1/" + scale + ", " + gocode.time | |
ColumnLayout { | |
anchors.fill: parent | |
width: parent.width | |
spacing: 6 | |
// A text field for editing or inserting lyrics, not visible by default. | |
TextField { | |
id: lyricField | |
property bool edit: false | |
height: 20 | |
// Visibility is bound to whether we have focus or not, | |
// and when the focus property changes so does the visibility | |
// WITHOUT any more code needed to be written other than this: | |
visible: lyricField.focus | |
z: 4 | |
Keys.onPressed: { | |
if (event.key == Qt.Key_Escape) { | |
// Set focus back to the lyrics view. | |
// Again, since this makes lyricsField.focus be false | |
// this field will automatically be hidden too at the | |
// same time! | |
lv.focus = true; | |
event.accepted = true; | |
} | |
} | |
onFocusChanged: { | |
// onXXXChanged is automatically called when the property changes, | |
// no need to explicitly bind it anywhere, it just works (tm). | |
// This function would be called when the "focus" property changes. | |
if(lyricField.focus == false) { | |
// Lost focus, reset status | |
lyricField.edit = false; | |
lyricField.text = ""; | |
} else if (lyricField.edit) { | |
// Gained focus AND the edit property is true. In other words | |
// we are editing an existing lyric, so set the text field | |
// to the lyrics of the current lyrics item. | |
lyricField.text = lyrics.lyric(lv.currentIndex).text | |
} | |
} | |
onAccepted: { | |
var idx = lv.currentIndex+1; | |
if (!lyricField.edit) { | |
// Inserting new lyrics. This is just some fancy code that splits | |
// words into separate lyrics. That way I can cut and paste the full | |
// lyrics from the web into the text box and I'll still get separate | |
// lyrics items to drag around for each word. | |
var texts = lyricField.text.replace(/([\n\r\t])/g," ").split(" "); | |
for (var i = 0; i < texts.length; i++) { | |
var str = texts[i].trim(); | |
if (str.length != 0) { | |
console.log(str, str.length); | |
lyrics.insert(idx, str); | |
idx++; | |
} | |
} | |
} else { | |
lyrics.lyric(lv.currentIndex).text = lyricField.text; | |
} | |
lv.focus = true; | |
lv.currentIndex = idx-1; | |
} | |
} | |
Row { | |
Layout.minimumWidth: parent.width | |
Text { | |
id: text | |
text: playbackspeed.value | |
width: 100 | |
} | |
// Obviously a slider controlling playback speed, do I need to say more? ;) | |
Slider { | |
id: playbackspeed | |
z: parent.z+1 | |
width: parent.width-text.width | |
minimumValue: 0.25 | |
maximumValue: 2.0 | |
states: [ | |
State { | |
// States is an interesting QML phenomenon that allow components | |
// to exhibit different behaviours under different conditions. | |
// | |
// This state in particular says that if we are not manually | |
// dragging the slider, it should be set to whatever value is | |
// provided by the "gocode.playbackspeed" property, which is | |
// a value in code written in Go (which I prefer over C/C++). | |
when: !playbackspeed.pressed | |
PropertyChanges { target: playbackspeed; value: gocode.playbackspeed } | |
} | |
] | |
stepSize: 0.25 | |
onValueChanged: { | |
if (pressed) { | |
// Kind of reversed of the state above; if we are dragging | |
// the slider manually, set the "gocode.playbackspeed" property | |
// to the value that the slider has. | |
gocode.playbackspeed = playbackspeed.value; | |
} | |
} | |
} | |
} | |
// Slider for the song position | |
Slider { | |
id: timepos | |
z: 3 | |
Layout.minimumWidth: parent.width | |
minimumValue: 0 | |
maximumValue: gocode.ogg.length() | |
states: [ | |
State { | |
when: !timepos.pressed | |
PropertyChanges { target: timepos; value: gocode.time } | |
} | |
] | |
onValueChanged: { | |
if (pressed) { | |
gocode.ogg.time = timepos.value; | |
gocode.time = gocode.ogg.time | |
} | |
} | |
} | |
// This is just defining a component, rather than actually instancing it. | |
// In short this creates a box with a lyric word in it. | |
Component { | |
id: content | |
Rectangle { | |
id: self | |
color: lv.currentIndex == index ? "#888888" : "#ffffff" ; | |
height: text.height | |
width: text.width | |
// If we are not manually dragging the box, set the x position to | |
// the scaled time location of this lyric word. | |
// | |
// "index" is an automatic property set to the integer index of this item, | |
// as instanced by it's parent container. | |
x: ma.drag.active ? self.x : lyrics.lyric(index).time*window.pixelscale; | |
y: parent.y | |
onXChanged: { | |
if (ma.drag.active) { | |
// So if we are manually dragging the item outside of the current visible | |
// area, scroll the area. | |
if ((lv.contentX+lv.contextWidth) < self.x || self.x < lv.contentX) { | |
lv.contentX = self.x; | |
} | |
} | |
} | |
Text { | |
id: text | |
property var e: lyrics.lyric(index); | |
// Set the text to red when the lyric's time has passed when playing the song, | |
// and black when it has yet to be shown. | |
color: e.time < gocode.time ? "red" : "black" | |
text: e.text | |
} | |
MouseArea { | |
id: ma | |
anchors.fill: parent | |
hoverEnabled: false | |
drag.target: self | |
drag.axis: Drag.XAxis | |
drag.minimumX: 0 | |
drag.maximumX: lv.contentWidth-parent.width | |
// When clicking this item, make it the selected one in th "lv" component. | |
onClicked: lv.currentIndex = index; | |
onReleased: { | |
// When releasing, update the lyric's time to whatever we dragged this | |
// box to. | |
lyrics.lyric(index).time = self.x/window.pixelscale | |
} | |
} | |
} | |
} | |
Flickable { | |
id: lv | |
Layout.fillHeight: true | |
Layout.preferredWidth: window.width | |
property int currentIndex: 0; | |
// Scroll this view according to the current playback position | |
contentX: (-window.add+gocode.time)*window.pixelscale | |
contentWidth: (window.add+gocode.ogg.length())*window.pixelscale | |
contentHeight: 20 | |
focus: true | |
Repeater { | |
// QML comes from a quite strong Model-View-Controller design, | |
// lyrics.len here is a Go side integer property. All we have to do | |
// Go side is to notify qml that "len" changed when it does and it'll | |
// automatically update any properties that are bound to "len". | |
model: lyrics.len | |
// The Repeater then instances "len" "content" components as children | |
// of this "Flickable" item. | |
delegate: content | |
} | |
Keys.onPressed: { | |
event.accepted = true; | |
var c = lv.currentIndex; | |
switch (event.key) { | |
case Qt.Key_Y: | |
gocode.ogg.time = gocode.lyric(lv.currentIndex).time; | |
gocode.time = gocode.ogg.time; | |
break; | |
case Qt.Key_E: | |
// Edit the current lyric. Remember what I wrote about earlier about | |
// lyricField.focus being bound to its visibility? Yeah, it all happens | |
// here just by setting the property to true. | |
lyricField.edit = true; | |
lyricField.focus = true; | |
break; | |
case Qt.Key_Plus: | |
window.scale++; | |
break; | |
case Qt.Key_Minus: | |
window.scale--; | |
break; | |
case Qt.Key_Left: | |
gocode.ogg.time = gocode.ogg.time-(1.0/Math.pow(10, window.scale-1)); | |
gocode.time = gocode.ogg.time; | |
break; | |
case Qt.Key_Right: | |
gocode.ogg.time = gocode.ogg.time+(1.0/Math.pow(10, window.scale-1)); | |
gocode.time = gocode.ogg.time; | |
break; | |
case Qt.Key_Return: | |
// Sync will just set the time of the current lyric to the current playback time, | |
// and make the next lyric word the current item. | |
// | |
// That way all I need to do to sync lyrics is to play back the song and | |
// hit enter when appropriate. | |
lyrics.sync(lv.currentIndex); | |
if (lv.currentIndex+1 < lyrics.len) { | |
lv.currentIndex = c+1; | |
} | |
break; | |
case Qt.Key_Space: | |
if (gocode.running()) { | |
gocode.pause(); | |
} else { | |
gocode.play(); | |
} | |
break; | |
case Qt.Key_Backspace: | |
lyrics.del(lv.currentIndex); | |
lv.currentIndex = c; | |
break; | |
case Qt.Key_H: | |
// vim navigation ;) | |
if (lv.currentIndex > 0) { | |
lv.currentIndex--; | |
} | |
break; | |
case Qt.Key_L: | |
// vim navigation ;) | |
if (lv.currentIndex+1 < lyrics.len) { | |
lv.currentIndex++; | |
} | |
break; | |
case Qt.Key_A: | |
// Add a lyric. Yep, that's all that's needed. | |
lyricField.focus = true; | |
break; | |
default: | |
event.accepted = false; | |
break; | |
} | |
} | |
} | |
} | |
// Just a vertical bar indicating the current song position. | |
Rectangle { | |
x: window.width/2 | |
y: lv.y | |
z: lv.z+1 | |
width: 1 | |
height: lv.height | |
color: "red" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment