diff --git a/CHANGELOG.md b/CHANGELOG.md index e5bbef9..f7e2d2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added +- A toggle button for copying non-unique patterns before editing. When enabled + and if the pattern is used in multiple places, the pattern is copied first. + ([#77][i77]) - User can define own keybindings in `os.UserConfigDir()/sointu/keybindings.yaml` ([#94][i94], [#151][i151]) - A small number above the instrument name identifies the MIDI channel / @@ -226,6 +229,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). [0.1.0]: https://github.com/vsariola/sointu/compare/4klang-3.11...v0.1.0 [i65]: https://github.com/vsariola/sointu/issues/65 [i68]: https://github.com/vsariola/sointu/issues/68 +[i77]: https://github.com/vsariola/sointu/issues/77 [i94]: https://github.com/vsariola/sointu/issues/94 [i112]: https://github.com/vsariola/sointu/issues/112 [i116]: https://github.com/vsariola/sointu/issues/116 diff --git a/song.go b/song.go index 698275e..fbe2ad0 100644 --- a/song.go +++ b/song.go @@ -137,7 +137,10 @@ func (s Track) Note(pos SongPos) byte { return s.Patterns[pat][pos.PatternRow] } -func (s *Track) SetNote(pos SongPos, note byte) { +// SetNote sets the note at the given position. If uniquePatterns is true, the +// pattern is copied to a new pattern if the pattern is used by more than one +// order row. +func (s *Track) SetNote(pos SongPos, note byte, uniquePatterns bool) { if pos.OrderRow < 0 || pos.PatternRow < 0 { return } @@ -163,13 +166,31 @@ func (s *Track) SetNote(pos SongPos, note byte) { for pat >= len(s.Patterns) { s.Patterns = append(s.Patterns, Pattern{}) } - if pos.PatternRow >= len(s.Patterns[pat]) && note == 1 { - return + if uniquePatterns { + uses := 0 + maxPat := 0 + for _, p := range s.Order { + if p == pat { + uses++ + } + if p > maxPat { + maxPat = p + } + } + if uses > 1 { + newPattern := append(Pattern{}, s.Patterns[pat]...) + pat = maxPat + 1 + if pat >= 36 { + return + } + for pat >= len(s.Patterns) { + s.Patterns = append(s.Patterns, Pattern{}) + } + s.Patterns[pat] = newPattern + s.Order.Set(pos.OrderRow, pat) + } } - for pos.PatternRow >= len(s.Patterns[pat]) { - s.Patterns[pat] = append(s.Patterns[pat], 1) - } - s.Patterns[pat][pos.PatternRow] = note + s.Patterns[pat].Set(pos.PatternRow, note) } // Get returns the value at index; or 1 is the index is out of range @@ -182,6 +203,9 @@ func (s Pattern) Get(index int) byte { // Set sets the value at index; appending 1s until the slice is long enough. func (s *Pattern) Set(index int, value byte) { + if value == 1 && index >= len(*s) { + return + } for len(*s) <= index { *s = append(*s, 1) } diff --git a/tracker/bool.go b/tracker/bool.go index 9093d0d..f050e20 100644 --- a/tracker/bool.go +++ b/tracker/bool.go @@ -21,6 +21,7 @@ type ( UnitSearching Model UnitDisabled Model LoopToggle Model + UniquePatterns Model ) func (v Bool) Toggle() { @@ -45,6 +46,7 @@ func (m *Model) Follow() *Follow { return (*Follow)(m) } func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m) } func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) } func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) } +func (m *Model) UniquePatterns() *UniquePatterns { return (*UniquePatterns)(m) } // Panic methods @@ -185,3 +187,10 @@ func (t *LoopToggle) setValue(val bool) { m.setLoop(newLoop) } func (m *LoopToggle) Enabled() bool { return true } + +// UniquePatterns methods + +func (m *UniquePatterns) Bool() Bool { return Bool{m} } +func (m *UniquePatterns) Value() bool { return m.uniquePatterns } +func (m *UniquePatterns) setValue(val bool) { m.uniquePatterns = val } +func (m *UniquePatterns) Enabled() bool { return true } diff --git a/tracker/gioui/keyevent.go b/tracker/gioui/keyevent.go index 86d3bef..d315173 100644 --- a/tracker/gioui/keyevent.go +++ b/tracker/gioui/keyevent.go @@ -220,6 +220,8 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) { t.UnitDisabled().Bool().Toggle() case "LoopToggle": t.LoopToggle().Bool().Toggle() + case "UniquePatternsToggle": + t.UniquePatterns().Bool().Toggle() // Integers case "InstrumentVoicesAdd": t.Model.InstrumentVoices().Int().Add(1) diff --git a/tracker/gioui/note_editor.go b/tracker/gioui/note_editor.go index 48d982d..59de3d6 100644 --- a/tracker/gioui/note_editor.go +++ b/tracker/gioui/note_editor.go @@ -59,13 +59,15 @@ type NoteEditor struct { SubtractOctaveBtn *ActionClickable NoteOffBtn *ActionClickable EffectBtn *BoolClickable + UniqueBtn *BoolClickable scrollTable *ScrollTable tag struct{} eventFilters []event.Filter - deleteTrackHint string - addTrackHint string + deleteTrackHint string + addTrackHint string + uniqueOffTip, uniqueOnTip string } func NewNoteEditor(model *tracker.Model) *NoteEditor { @@ -79,6 +81,7 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor { SubtractOctaveBtn: NewActionClickable(model.SubtractOctave()), NoteOffBtn: NewActionClickable(model.EditNoteOff()), EffectBtn: NewBoolClickable(model.Effect().Bool()), + UniqueBtn: NewBoolClickable(model.UniquePatterns().Bool()), scrollTable: NewScrollTable( model.Notes().Table(), model.Tracks().List(), @@ -93,6 +96,8 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor { } ret.deleteTrackHint = makeHint("Delete\ntrack", "\n(%s)", "DeleteTrack") ret.addTrackHint = makeHint("Add\ntrack", "\n(%s)", "AddTrack") + ret.uniqueOnTip = makeHint("Duplicate non-unique patterns", " (%s)", "UniquePatternsToggle") + ret.uniqueOffTip = makeHint("Allow editing non-unique patterns", " (%s)", "UniquePatternsToggle") return ret } @@ -145,6 +150,7 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D { return in.Layout(gtx, numStyle.Layout) } effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex") + uniqueBtnStyle := ToggleIcon(gtx, t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip) return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }), layout.Rigid(addSemitoneBtnStyle.Layout), @@ -153,6 +159,7 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D { layout.Rigid(subtractOctaveBtnStyle.Layout), layout.Rigid(noteOffBtnStyle.Layout), layout.Rigid(effectBtnStyle.Layout), + layout.Rigid(uniqueBtnStyle.Layout), layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)), layout.Rigid(voiceUpDown), layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), diff --git a/tracker/list.go b/tracker/list.go index 7548be5..2a7db6d 100644 --- a/tracker/list.go +++ b/tracker/list.go @@ -667,8 +667,8 @@ func (v *NoteRows) swap(i, j int) (ok bool) { for _, track := range v.d.Song.Score.Tracks { n1 := track.Note(ipos) n2 := track.Note(jpos) - track.SetNote(ipos, n2) - track.SetNote(jpos, n1) + track.SetNote(ipos, n2, v.uniquePatterns) + track.SetNote(jpos, n1, v.uniquePatterns) } return true } @@ -679,7 +679,7 @@ func (v *NoteRows) delete(i int) (ok bool) { } pos := v.d.Song.Score.SongPos(i) for _, track := range v.d.Song.Score.Tracks { - track.SetNote(pos, 1) + track.SetNote(pos, 1, v.uniquePatterns) } return true } @@ -730,7 +730,7 @@ func (v *NoteRows) unmarshal(data []byte) (from, to int, err error) { for j, note := range arr { y := j + from pos := v.d.Song.Score.SongPos(y) - v.d.Song.Score.Tracks[i].SetNote(pos, note) + v.d.Song.Score.Tracks[i].SetNote(pos, note, v.uniquePatterns) } } return diff --git a/tracker/model.go b/tracker/model.go index 6cca46a..625f95d 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -54,13 +54,14 @@ type ( changeSeverity ChangeSeverity changeType ChangeType - panic bool - recording bool - playing bool - playPosition sointu.SongPos - loop Loop - follow bool - quitted bool + panic bool + recording bool + playing bool + playPosition sointu.SongPos + loop Loop + follow bool + quitted bool + uniquePatterns bool cachePatternUseCount [][]int diff --git a/tracker/model_test.go b/tracker/model_test.go index 32ff896..8158372 100644 --- a/tracker/model_test.go +++ b/tracker/model_test.go @@ -60,6 +60,7 @@ func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T) s.IterateBool("Effect", s.model.Effect().Bool(), yield, seed) s.IterateBool("CommentExpanded", s.model.CommentExpanded().Bool(), yield, seed) s.IterateBool("Follow", s.model.Follow().Bool(), yield, seed) + s.IterateBool("UniquePatterns", s.model.UniquePatterns().Bool(), yield, seed) // Strings s.IterateString("FilePath", s.model.FilePath().String(), yield, seed) s.IterateString("InstrumentName", s.model.InstrumentName().String(), yield, seed) diff --git a/tracker/table.go b/tracker/table.go index 0d1182d..e8a38eb 100644 --- a/tracker/table.go +++ b/tracker/table.go @@ -481,7 +481,7 @@ func (v *Notes) add(rect Rect, delta int) (ok bool) { newVal = 255 } // only do all sets after all gets, so we don't accidentally adjust single note multiple times - defer v.d.Song.Score.Tracks[x].SetNote(pos, byte(newVal)) + defer v.d.Song.Score.Tracks[x].SetNote(pos, byte(newVal), v.uniquePatterns) } } return true @@ -540,7 +540,7 @@ func (v *Notes) unmarshalAtCursor(data []byte) bool { continue } pos := v.d.Song.Score.SongPos(y) - v.d.Song.Score.Tracks[x].SetNote(pos, q) + v.d.Song.Score.Tracks[x].SetNote(pos, q, v.uniquePatterns) } } return true @@ -562,7 +562,7 @@ func (v *Notes) unmarshalRange(rect Rect, data []byte) bool { continue } pos := v.d.Song.Score.SongPos(y) - v.d.Song.Score.Tracks[x].SetNote(pos, a) + v.d.Song.Score.Tracks[x].SetNote(pos, a, v.uniquePatterns) } } return true @@ -609,7 +609,7 @@ func (m *Notes) SetValue(p Point, val byte) { } track := &(m.d.Song.Score.Tracks[p.X]) pos := m.d.Song.Score.SongPos(p.Y) - (*track).SetNote(pos, val) + (*track).SetNote(pos, val, m.uniquePatterns) } func (v *Notes) FillNibble(value byte, lowNibble bool) {