Skip to content

Instantly share code, notes, and snippets.

@quarnster
Last active August 29, 2015 13:56
Show Gist options
  • Save quarnster/9150668 to your computer and use it in GitHub Desktop.
Save quarnster/9150668 to your computer and use it in GitHub Desktop.
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