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" "strconv"
"strings" "strings"
"gioui.org/app"
"gioui.org/io/key" "gioui.org/io/key"
"gopkg.in/yaml.v3"
) )
var noteMap = map[string]int{ 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. // 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 e.State == key.Press {
if t.InstrumentNameEditor.Focused() { if t.InstrumentNameEditor.Focused() {
return false return false
} }
switch e.Name { 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": case "Z":
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModShortcut) {
t.Undo() t.Undo()
return true return true
} }
case "Y": case "Y":
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModShortcut) {
t.Redo() t.Redo()
return true return true
} }
@ -118,14 +133,14 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case key.NameUpArrow: case key.NameUpArrow:
switch t.EditMode { switch t.EditMode {
case EditPatterns: case EditPatterns:
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.SongRow = SongRow{} t.Cursor.SongRow = SongRow{}
} else { } else {
t.Cursor.Row -= t.song.RowsPerPattern t.Cursor.Row -= t.song.RowsPerPattern
} }
t.NoteTracking = false t.NoteTracking = false
case EditTracks: case EditTracks:
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Row -= t.song.RowsPerPattern t.Cursor.Row -= t.song.RowsPerPattern
} else { } else {
t.Cursor.Row-- t.Cursor.Row--
@ -144,14 +159,14 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case key.NameDownArrow: case key.NameDownArrow:
switch t.EditMode { switch t.EditMode {
case EditPatterns: case EditPatterns:
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Row = t.song.TotalRows() - 1 t.Cursor.Row = t.song.TotalRows() - 1
} else { } else {
t.Cursor.Row += t.song.RowsPerPattern t.Cursor.Row += t.song.RowsPerPattern
} }
t.NoteTracking = false t.NoteTracking = false
case EditTracks: case EditTracks:
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Row += t.song.RowsPerPattern t.Cursor.Row += t.song.RowsPerPattern
} else { } else {
t.Cursor.Row++ t.Cursor.Row++
@ -170,13 +185,13 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case key.NameLeftArrow: case key.NameLeftArrow:
switch t.EditMode { switch t.EditMode {
case EditPatterns: case EditPatterns:
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Track = 0 t.Cursor.Track = 0
} else { } else {
t.Cursor.Track-- t.Cursor.Track--
} }
case EditTracks: 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.Cursor.Track--
t.CursorColumn = 1 t.CursorColumn = 1
} else { } else {
@ -199,13 +214,13 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case key.NameRightArrow: case key.NameRightArrow:
switch t.EditMode { switch t.EditMode {
case EditPatterns: case EditPatterns:
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModShortcut) {
t.Cursor.Track = len(t.song.Tracks) - 1 t.Cursor.Track = len(t.song.Tracks) - 1
} else { } else {
t.Cursor.Track++ t.Cursor.Track++
} }
case EditTracks: 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.Cursor.Track++
t.CursorColumn = 0 t.CursorColumn = 0
} else { } else {
@ -228,7 +243,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case "+": case "+":
switch t.EditMode { switch t.EditMode {
case EditTracks: case EditTracks:
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModShortcut) {
t.AdjustSelectionPitch(12) t.AdjustSelectionPitch(12)
} else { } else {
t.AdjustSelectionPitch(1) t.AdjustSelectionPitch(1)
@ -238,7 +253,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
case "-": case "-":
switch t.EditMode { switch t.EditMode {
case EditTracks: case EditTracks:
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModShortcut) {
t.AdjustSelectionPitch(-12) t.AdjustSelectionPitch(-12)
} else { } else {
t.AdjustSelectionPitch(-1) t.AdjustSelectionPitch(-1)
@ -285,7 +300,7 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
name = strings.ToLower(name) name = strings.ToLower(name)
} }
if val, ok := unitKeyMap[name]; ok { if val, ok := unitKeyMap[name]; ok {
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModShortcut) {
t.SetUnit(val) t.SetUnit(val)
return true return true
} }

View File

@ -4,6 +4,7 @@ import (
"os" "os"
"gioui.org/app" "gioui.org/app"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/layout" "gioui.org/layout"
@ -21,7 +22,12 @@ func (t *Tracker) Run(w *app.Window) error {
case system.DestroyEvent: case system.DestroyEvent:
return e.Err return e.Err
case key.Event: 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() w.Invalidate()
} }
case system.FrameEvent: case system.FrameEvent:

View File

@ -5,6 +5,7 @@ import (
"math" "math"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
@ -12,6 +13,7 @@ import (
"gioui.org/unit" "gioui.org/unit"
"gioui.org/widget/material" "gioui.org/widget/material"
"golang.org/x/exp/shiny/materialdesign/icons" "golang.org/x/exp/shiny/materialdesign/icons"
"gopkg.in/yaml.v3"
) )
func (t *Tracker) layoutSongPanel(gtx C) D { func (t *Tracker) layoutSongPanel(gtx C) D {
@ -41,6 +43,12 @@ func (t *Tracker) layoutSongButtons(gtx C) D {
t.SaveSongFile() 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 := material.IconButton(t.Theme, t.NewSongFileBtn, widgetForIcon(icons.ContentClear))
newBtnStyle.Background = transparent newBtnStyle.Background = transparent
newBtnStyle.Inset = layout.UniformInset(unit.Dp(6)) 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.Inset = layout.UniformInset(unit.Dp(6))
loadBtnStyle.Color = primaryColor 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 { menuContents := func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(newBtnStyle.Layout), layout.Rigid(newBtnStyle.Layout),
layout.Rigid(loadBtnStyle.Layout), layout.Rigid(loadBtnStyle.Layout),
layout.Rigid(copySongBtnStyle.Layout),
) )
} }

View File

@ -1,6 +1,8 @@
package tracker package tracker
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
@ -11,6 +13,7 @@ import (
"gioui.org/widget" "gioui.org/widget"
"gioui.org/widget/material" "gioui.org/widget/material"
"github.com/vsariola/sointu" "github.com/vsariola/sointu"
"gopkg.in/yaml.v3"
) )
type EditMode int type EditMode int
@ -50,6 +53,7 @@ type Tracker struct {
TrackVoices *NumberInput TrackVoices *NumberInput
InstrumentNameEditor *widget.Editor InstrumentNameEditor *widget.Editor
NewTrackBtn *widget.Clickable NewTrackBtn *widget.Clickable
CopySongBtn *widget.Clickable
NewInstrumentBtn *widget.Clickable NewInstrumentBtn *widget.Clickable
DeleteInstrumentBtn *widget.Clickable DeleteInstrumentBtn *widget.Clickable
LoadSongFileBtn *widget.Clickable LoadSongFileBtn *widget.Clickable
@ -107,6 +111,20 @@ func (t *Tracker) LoadSong(song sointu.Song) error {
return nil 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 { func clamp(a, min, max int) int {
if a < min { if a < min {
return min return min
@ -631,6 +649,7 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr
NewInstrumentBtn: new(widget.Clickable), NewInstrumentBtn: new(widget.Clickable),
DeleteInstrumentBtn: new(widget.Clickable), DeleteInstrumentBtn: new(widget.Clickable),
NewSongFileBtn: new(widget.Clickable), NewSongFileBtn: new(widget.Clickable),
CopySongBtn: new(widget.Clickable),
FileMenuBtn: new(widget.Clickable), FileMenuBtn: new(widget.Clickable),
LoadSongFileBtn: new(widget.Clickable), LoadSongFileBtn: new(widget.Clickable),
SaveSongFileBtn: new(widget.Clickable), SaveSongFileBtn: new(widget.Clickable),