From 4da225ec334624e8163f98170e7456abc3632524 Mon Sep 17 00:00:00 2001 From: vsariola <5684185+vsariola@users.noreply.github.com> Date: Sat, 13 Feb 2021 01:59:10 +0200 Subject: [PATCH] feat(tracker): allow copying and pasting songs to/from the window --- tracker/keyevent.go | 43 +++++++++++++++++++++++++++++-------------- tracker/run.go | 8 +++++++- tracker/songpanel.go | 14 ++++++++++++++ tracker/tracker.go | 19 +++++++++++++++++++ 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/tracker/keyevent.go b/tracker/keyevent.go index 7801c81..42e9c6d 100644 --- a/tracker/keyevent.go +++ b/tracker/keyevent.go @@ -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 } diff --git a/tracker/run.go b/tracker/run.go index 01f4a96..1df38a4 100644 --- a/tracker/run.go +++ b/tracker/run.go @@ -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: diff --git a/tracker/songpanel.go b/tracker/songpanel.go index c70acd3..68cb98f 100644 --- a/tracker/songpanel.go +++ b/tracker/songpanel.go @@ -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), ) } diff --git a/tracker/tracker.go b/tracker/tracker.go index 2e7455f..0005c4e 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -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),