From eb25ddd86499c209a0b8290561ac2c075b29a59a Mon Sep 17 00:00:00 2001 From: vsariola Date: Fri, 8 Jan 2021 22:00:15 +0200 Subject: [PATCH] feat(tracker): implement simple undo / redo --- tracker/keyevent.go | 33 ++++++++++++++++++++++----------- tracker/tracker.go | 22 ++++++++++++++++++++++ tracker/undo.go | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 tracker/undo.go diff --git a/tracker/keyevent.go b/tracker/keyevent.go index 4978274..b7e2c85 100644 --- a/tracker/keyevent.go +++ b/tracker/keyevent.go @@ -45,18 +45,17 @@ var noteMap = map[string]int{ // KeyEvent handles incoming key events and returns true if repaint is needed. func (t *Tracker) KeyEvent(e key.Event) bool { if e.State == key.Press { - if t.CursorColumn == 0 { - if val, ok := noteMap[e.Name]; ok { - t.NotePressed(val) - return true - } - } else { - if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil { - t.NumberPressed(byte(iv)) - return true - } - } switch e.Name { + case "Z": + if e.Modifiers.Contain(key.ModCtrl) { + t.Undo() + return true + } + case "Y": + if e.Modifiers.Contain(key.ModCtrl) { + t.Redo() + return true + } case "A": t.setCurrent(0) return true @@ -114,6 +113,17 @@ func (t *Tracker) KeyEvent(e key.Event) bool { t.CursorColumn = 0 return true } + if t.CursorColumn == 0 { + if val, ok := noteMap[e.Name]; ok { + t.NotePressed(val) + return true + } + } else { + if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil { + t.NumberPressed(byte(iv)) + return true + } + } } return false } @@ -135,6 +145,7 @@ func (t *Tracker) moveCursor(delta int) { // setCurrent sets the (note) value in current pattern under cursor to iv func (t *Tracker) setCurrent(iv byte) { + t.SaveUndo() t.song.Tracks[t.ActiveTrack].Patterns[t.song.Tracks[t.ActiveTrack].Sequence[t.DisplayPattern]][t.CursorRow] = iv } diff --git a/tracker/tracker.go b/tracker/tracker.go index 66878c9..6753f0a 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -44,6 +44,8 @@ type Tracker struct { synth sointu.Synth playBuffer []float32 closer chan struct{} + undoStack []sointu.Song + redoStack []sointu.Song } func (t *Tracker) LoadSong(song sointu.Song) error { @@ -59,6 +61,21 @@ func (t *Tracker) LoadSong(song sointu.Song) error { } else { t.synth = synth } + if t.DisplayPattern >= song.SequenceLength() { + t.DisplayPattern = song.SequenceLength() - 1 + } + if t.CursorRow >= song.PatternRows() { + t.CursorRow = song.PatternRows() - 1 + } + if t.PlayPattern >= song.SequenceLength() { + t.PlayPattern = song.SequenceLength() - 1 + } + if t.PlayRow >= song.PatternRows() { + t.PlayRow = song.PatternRows() - 1 + } + if t.ActiveTrack >= len(song.Tracks) { + t.ActiveTrack = len(song.Tracks) - 1 + } return nil } @@ -153,6 +170,7 @@ func (t *Tracker) ChangeOctave(delta int) bool { } func (t *Tracker) ChangeBPM(delta int) bool { + t.SaveUndo() newBPM := t.song.BPM + delta if newBPM < 1 { newBPM = 1 @@ -169,6 +187,7 @@ func (t *Tracker) ChangeBPM(delta int) bool { } func (t *Tracker) AddTrack() { + t.SaveUndo() if t.song.TotalTrackVoices() < t.song.Patch.TotalVoices() { seq := make([]byte, t.song.SequenceLength()) patterns := [][]byte{make([]byte, t.song.PatternRows())} @@ -181,6 +200,7 @@ func (t *Tracker) AddTrack() { } func (t *Tracker) AddInstrument() { + t.SaveUndo() if t.song.Patch.TotalVoices() < 32 { units := make([]sointu.Unit, len(defaultInstrument.Units)) for i, defUnit := range defaultInstrument.Units { @@ -220,6 +240,8 @@ func New(audioContext sointu.AudioContext) *Tracker { patternJump: make(chan int), ticked: make(chan struct{}), closer: make(chan struct{}), + undoStack: []sointu.Song{}, + redoStack: []sointu.Song{}, } t.Theme.Color.Primary = color.RGBA{R: 64, G: 64, B: 64, A: 255} go t.sequencerLoop(t.closer) diff --git a/tracker/undo.go b/tracker/undo.go new file mode 100644 index 0000000..cf1fb86 --- /dev/null +++ b/tracker/undo.go @@ -0,0 +1,37 @@ +package tracker + +var undoSkip = map[string]int{ + "setNote": 10, +} + +const maxUndo = 256 + +func (t *Tracker) SaveUndo() { + if len(t.undoStack) >= maxUndo { + t.undoStack = t.undoStack[1:] + } + t.undoStack = append(t.undoStack, t.song.Copy()) + t.redoStack = t.redoStack[:0] +} + +func (t *Tracker) Undo() { + if len(t.undoStack) > 0 { + if len(t.redoStack) >= maxUndo { + t.redoStack = t.redoStack[1:] + } + t.redoStack = append(t.redoStack, t.song.Copy()) + t.LoadSong(t.undoStack[len(t.undoStack)-1]) + t.undoStack = t.undoStack[:len(t.undoStack)-1] + } +} + +func (t *Tracker) Redo() { + if len(t.redoStack) > 0 { + if len(t.undoStack) >= maxUndo { + t.undoStack = t.undoStack[1:] + } + t.undoStack = append(t.undoStack, t.song.Copy()) + t.LoadSong(t.redoStack[len(t.redoStack)-1]) + t.redoStack = t.redoStack[:len(t.redoStack)-1] + } +}