feat(tracker): allow copying and pasting songs to/from the window

This commit is contained in:
vsariola 2021-02-13 01:59:10 +02:00
parent 11b5b5b322
commit 4da225ec33
4 changed files with 69 additions and 15 deletions

View File

@ -4,7 +4,9 @@ import (
"strconv"
"strings"
"gioui.org/app"
"gioui.org/io/key"
"gopkg.in/yaml.v3"
)
var noteMap = map[string]int{
@ -75,19 +77,32 @@ var unitKeyMap = map[string]string{
}
// KeyEvent handles incoming key events and returns true if repaint is needed.
func (t *Tracker) KeyEvent(e key.Event) bool {
func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool {
if e.State == key.Press {
if t.InstrumentNameEditor.Focused() {
return false
}
switch e.Name {
case "C":
if e.Modifiers.Contain(key.ModShortcut) {
contents, err := yaml.Marshal(t.song)
if err == nil {
w.WriteClipboard(string(contents))
}
return true
}
case "V":
if e.Modifiers.Contain(key.ModShortcut) {
w.ReadClipboard()
return true
}
case "Z":
if e.Modifiers.Contain(key.ModCtrl) {
if e.Modifiers.Contain(key.ModShortcut) {
t.Undo()
return true
}
case "Y":
if e.Modifiers.Contain(key.ModCtrl) {
if e.Modifiers.Contain(key.ModShortcut) {
t.Redo()
return true
}
@ -118,14 +133,14 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case key.NameUpArrow:
switch t.EditMode {
case EditPatterns:
if e.Modifiers.Contain(key.ModCtrl) {
if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.SongRow = SongRow{}
} else {
t.Cursor.Row -= t.song.RowsPerPattern
}
t.NoteTracking = false
case EditTracks:
if e.Modifiers.Contain(key.ModCtrl) {
if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Row -= t.song.RowsPerPattern
} else {
t.Cursor.Row--
@ -144,14 +159,14 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case key.NameDownArrow:
switch t.EditMode {
case EditPatterns:
if e.Modifiers.Contain(key.ModCtrl) {
if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Row = t.song.TotalRows() - 1
} else {
t.Cursor.Row += t.song.RowsPerPattern
}
t.NoteTracking = false
case EditTracks:
if e.Modifiers.Contain(key.ModCtrl) {
if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Row += t.song.RowsPerPattern
} else {
t.Cursor.Row++
@ -170,13 +185,13 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case key.NameLeftArrow:
switch t.EditMode {
case EditPatterns:
if e.Modifiers.Contain(key.ModCtrl) {
if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Track = 0
} else {
t.Cursor.Track--
}
case EditTracks:
if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) {
if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Track--
t.CursorColumn = 1
} else {
@ -199,13 +214,13 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case key.NameRightArrow:
switch t.EditMode {
case EditPatterns:
if e.Modifiers.Contain(key.ModCtrl) {
if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Track = len(t.song.Tracks) - 1
} else {
t.Cursor.Track++
}
case EditTracks:
if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModCtrl) {
if t.CursorColumn == 0 || !t.TrackShowHex[t.Cursor.Track] || e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Track++
t.CursorColumn = 0
} else {
@ -228,7 +243,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case "+":
switch t.EditMode {
case EditTracks:
if e.Modifiers.Contain(key.ModCtrl) {
if e.Modifiers.Contain(key.ModShortcut) {
t.AdjustSelectionPitch(12)
} else {
t.AdjustSelectionPitch(1)
@ -238,7 +253,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case "-":
switch t.EditMode {
case EditTracks:
if e.Modifiers.Contain(key.ModCtrl) {
if e.Modifiers.Contain(key.ModShortcut) {
t.AdjustSelectionPitch(-12)
} else {
t.AdjustSelectionPitch(-1)
@ -285,7 +300,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
name = strings.ToLower(name)
}
if val, ok := unitKeyMap[name]; ok {
if e.Modifiers.Contain(key.ModCtrl) {
if e.Modifiers.Contain(key.ModShortcut) {
t.SetUnit(val)
return true
}

View File

@ -4,6 +4,7 @@ import (
"os"
"gioui.org/app"
"gioui.org/io/clipboard"
"gioui.org/io/key"
"gioui.org/io/system"
"gioui.org/layout"
@ -21,7 +22,12 @@ func (t *Tracker) Run(w *app.Window) error {
case system.DestroyEvent:
return e.Err
case key.Event:
if t.KeyEvent(e) {
if t.KeyEvent(w, e) {
w.Invalidate()
}
case clipboard.Event:
err := t.UnmarshalSong([]byte(e.Text))
if err == nil {
w.Invalidate()
}
case system.FrameEvent:

View File

@ -5,6 +5,7 @@ import (
"math"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
@ -12,6 +13,7 @@ import (
"gioui.org/unit"
"gioui.org/widget/material"
"golang.org/x/exp/shiny/materialdesign/icons"
"gopkg.in/yaml.v3"
)
func (t *Tracker) layoutSongPanel(gtx C) D {
@ -41,6 +43,12 @@ func (t *Tracker) layoutSongButtons(gtx C) D {
t.SaveSongFile()
}
for t.CopySongBtn.Clicked() {
if contents, err := yaml.Marshal(t.song); err == nil {
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
}
}
newBtnStyle := material.IconButton(t.Theme, t.NewSongFileBtn, widgetForIcon(icons.ContentClear))
newBtnStyle.Background = transparent
newBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
@ -51,10 +59,16 @@ func (t *Tracker) layoutSongButtons(gtx C) D {
loadBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
loadBtnStyle.Color = primaryColor
copySongBtnStyle := material.IconButton(t.Theme, t.CopySongBtn, widgetForIcon(icons.ContentContentCopy))
copySongBtnStyle.Background = transparent
copySongBtnStyle.Inset = layout.UniformInset(unit.Dp(6))
copySongBtnStyle.Color = primaryColor
menuContents := func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(newBtnStyle.Layout),
layout.Rigid(loadBtnStyle.Layout),
layout.Rigid(copySongBtnStyle.Layout),
)
}

View File

@ -1,6 +1,8 @@
package tracker
import (
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
@ -11,6 +13,7 @@ import (
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu"
"gopkg.in/yaml.v3"
)
type EditMode int
@ -50,6 +53,7 @@ type Tracker struct {
TrackVoices *NumberInput
InstrumentNameEditor *widget.Editor
NewTrackBtn *widget.Clickable
CopySongBtn *widget.Clickable
NewInstrumentBtn *widget.Clickable
DeleteInstrumentBtn *widget.Clickable
LoadSongFileBtn *widget.Clickable
@ -107,6 +111,20 @@ func (t *Tracker) LoadSong(song sointu.Song) error {
return nil
}
func (t *Tracker) UnmarshalSong(bytes []byte) error {
var song sointu.Song
if errJSON := json.Unmarshal(bytes, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil {
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
}
}
if song.BPM > 0 {
t.LoadSong(song)
return nil
}
return errors.New("was able to unmarshal a song, but the bpm was 0")
}
func clamp(a, min, max int) int {
if a < min {
return min
@ -631,6 +649,7 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr
NewInstrumentBtn: new(widget.Clickable),
DeleteInstrumentBtn: new(widget.Clickable),
NewSongFileBtn: new(widget.Clickable),
CopySongBtn: new(widget.Clickable),
FileMenuBtn: new(widget.Clickable),
LoadSongFileBtn: new(widget.Clickable),
SaveSongFileBtn: new(widget.Clickable),