feat: toggle button to duplicate non-unique patterns when changed

Closes #77.
This commit is contained in:
5684185+vsariola@users.noreply.github.com 2024-10-13 14:47:22 +03:00
parent 3a7ab0416a
commit 10f021a497
9 changed files with 72 additions and 24 deletions

View File

@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added ### 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 - User can define own keybindings in
`os.UserConfigDir()/sointu/keybindings.yaml` ([#94][i94], [#151][i151]) `os.UserConfigDir()/sointu/keybindings.yaml` ([#94][i94], [#151][i151])
- A small number above the instrument name identifies the MIDI channel / - 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 [0.1.0]: https://github.com/vsariola/sointu/compare/4klang-3.11...v0.1.0
[i65]: https://github.com/vsariola/sointu/issues/65 [i65]: https://github.com/vsariola/sointu/issues/65
[i68]: https://github.com/vsariola/sointu/issues/68 [i68]: https://github.com/vsariola/sointu/issues/68
[i77]: https://github.com/vsariola/sointu/issues/77
[i94]: https://github.com/vsariola/sointu/issues/94 [i94]: https://github.com/vsariola/sointu/issues/94
[i112]: https://github.com/vsariola/sointu/issues/112 [i112]: https://github.com/vsariola/sointu/issues/112
[i116]: https://github.com/vsariola/sointu/issues/116 [i116]: https://github.com/vsariola/sointu/issues/116

38
song.go
View File

@ -137,7 +137,10 @@ func (s Track) Note(pos SongPos) byte {
return s.Patterns[pat][pos.PatternRow] 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 { if pos.OrderRow < 0 || pos.PatternRow < 0 {
return return
} }
@ -163,13 +166,31 @@ func (s *Track) SetNote(pos SongPos, note byte) {
for pat >= len(s.Patterns) { for pat >= len(s.Patterns) {
s.Patterns = append(s.Patterns, Pattern{}) s.Patterns = append(s.Patterns, Pattern{})
} }
if pos.PatternRow >= len(s.Patterns[pat]) && note == 1 { if uniquePatterns {
return 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].Set(pos.PatternRow, note)
s.Patterns[pat] = append(s.Patterns[pat], 1)
}
s.Patterns[pat][pos.PatternRow] = note
} }
// Get returns the value at index; or 1 is the index is out of range // 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. // Set sets the value at index; appending 1s until the slice is long enough.
func (s *Pattern) Set(index int, value byte) { func (s *Pattern) Set(index int, value byte) {
if value == 1 && index >= len(*s) {
return
}
for len(*s) <= index { for len(*s) <= index {
*s = append(*s, 1) *s = append(*s, 1)
} }

View File

@ -21,6 +21,7 @@ type (
UnitSearching Model UnitSearching Model
UnitDisabled Model UnitDisabled Model
LoopToggle Model LoopToggle Model
UniquePatterns Model
) )
func (v Bool) Toggle() { 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) UnitSearching() *UnitSearching { return (*UnitSearching)(m) }
func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) } func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) }
func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) } func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) }
func (m *Model) UniquePatterns() *UniquePatterns { return (*UniquePatterns)(m) }
// Panic methods // Panic methods
@ -185,3 +187,10 @@ func (t *LoopToggle) setValue(val bool) {
m.setLoop(newLoop) m.setLoop(newLoop)
} }
func (m *LoopToggle) Enabled() bool { return true } 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 }

View File

@ -220,6 +220,8 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
t.UnitDisabled().Bool().Toggle() t.UnitDisabled().Bool().Toggle()
case "LoopToggle": case "LoopToggle":
t.LoopToggle().Bool().Toggle() t.LoopToggle().Bool().Toggle()
case "UniquePatternsToggle":
t.UniquePatterns().Bool().Toggle()
// Integers // Integers
case "InstrumentVoicesAdd": case "InstrumentVoicesAdd":
t.Model.InstrumentVoices().Int().Add(1) t.Model.InstrumentVoices().Int().Add(1)

View File

@ -59,13 +59,15 @@ type NoteEditor struct {
SubtractOctaveBtn *ActionClickable SubtractOctaveBtn *ActionClickable
NoteOffBtn *ActionClickable NoteOffBtn *ActionClickable
EffectBtn *BoolClickable EffectBtn *BoolClickable
UniqueBtn *BoolClickable
scrollTable *ScrollTable scrollTable *ScrollTable
tag struct{} tag struct{}
eventFilters []event.Filter eventFilters []event.Filter
deleteTrackHint string deleteTrackHint string
addTrackHint string addTrackHint string
uniqueOffTip, uniqueOnTip string
} }
func NewNoteEditor(model *tracker.Model) *NoteEditor { func NewNoteEditor(model *tracker.Model) *NoteEditor {
@ -79,6 +81,7 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
SubtractOctaveBtn: NewActionClickable(model.SubtractOctave()), SubtractOctaveBtn: NewActionClickable(model.SubtractOctave()),
NoteOffBtn: NewActionClickable(model.EditNoteOff()), NoteOffBtn: NewActionClickable(model.EditNoteOff()),
EffectBtn: NewBoolClickable(model.Effect().Bool()), EffectBtn: NewBoolClickable(model.Effect().Bool()),
UniqueBtn: NewBoolClickable(model.UniquePatterns().Bool()),
scrollTable: NewScrollTable( scrollTable: NewScrollTable(
model.Notes().Table(), model.Notes().Table(),
model.Tracks().List(), model.Tracks().List(),
@ -93,6 +96,8 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
} }
ret.deleteTrackHint = makeHint("Delete\ntrack", "\n(%s)", "DeleteTrack") ret.deleteTrackHint = makeHint("Delete\ntrack", "\n(%s)", "DeleteTrack")
ret.addTrackHint = makeHint("Add\ntrack", "\n(%s)", "AddTrack") 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 return ret
} }
@ -145,6 +150,7 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
return in.Layout(gtx, numStyle.Layout) return in.Layout(gtx, numStyle.Layout)
} }
effectBtnStyle := ToggleButton(gtx, t.Theme, te.EffectBtn, "Hex") 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, 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(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
layout.Rigid(addSemitoneBtnStyle.Layout), layout.Rigid(addSemitoneBtnStyle.Layout),
@ -153,6 +159,7 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
layout.Rigid(subtractOctaveBtnStyle.Layout), layout.Rigid(subtractOctaveBtnStyle.Layout),
layout.Rigid(noteOffBtnStyle.Layout), layout.Rigid(noteOffBtnStyle.Layout),
layout.Rigid(effectBtnStyle.Layout), layout.Rigid(effectBtnStyle.Layout),
layout.Rigid(uniqueBtnStyle.Layout),
layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)), layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)),
layout.Rigid(voiceUpDown), layout.Rigid(voiceUpDown),
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),

View File

@ -667,8 +667,8 @@ func (v *NoteRows) swap(i, j int) (ok bool) {
for _, track := range v.d.Song.Score.Tracks { for _, track := range v.d.Song.Score.Tracks {
n1 := track.Note(ipos) n1 := track.Note(ipos)
n2 := track.Note(jpos) n2 := track.Note(jpos)
track.SetNote(ipos, n2) track.SetNote(ipos, n2, v.uniquePatterns)
track.SetNote(jpos, n1) track.SetNote(jpos, n1, v.uniquePatterns)
} }
return true return true
} }
@ -679,7 +679,7 @@ func (v *NoteRows) delete(i int) (ok bool) {
} }
pos := v.d.Song.Score.SongPos(i) pos := v.d.Song.Score.SongPos(i)
for _, track := range v.d.Song.Score.Tracks { for _, track := range v.d.Song.Score.Tracks {
track.SetNote(pos, 1) track.SetNote(pos, 1, v.uniquePatterns)
} }
return true return true
} }
@ -730,7 +730,7 @@ func (v *NoteRows) unmarshal(data []byte) (from, to int, err error) {
for j, note := range arr { for j, note := range arr {
y := j + from y := j + from
pos := v.d.Song.Score.SongPos(y) 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 return

View File

@ -54,13 +54,14 @@ type (
changeSeverity ChangeSeverity changeSeverity ChangeSeverity
changeType ChangeType changeType ChangeType
panic bool panic bool
recording bool recording bool
playing bool playing bool
playPosition sointu.SongPos playPosition sointu.SongPos
loop Loop loop Loop
follow bool follow bool
quitted bool quitted bool
uniquePatterns bool
cachePatternUseCount [][]int cachePatternUseCount [][]int

View File

@ -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("Effect", s.model.Effect().Bool(), yield, seed)
s.IterateBool("CommentExpanded", s.model.CommentExpanded().Bool(), yield, seed) s.IterateBool("CommentExpanded", s.model.CommentExpanded().Bool(), yield, seed)
s.IterateBool("Follow", s.model.Follow().Bool(), yield, seed) s.IterateBool("Follow", s.model.Follow().Bool(), yield, seed)
s.IterateBool("UniquePatterns", s.model.UniquePatterns().Bool(), yield, seed)
// Strings // Strings
s.IterateString("FilePath", s.model.FilePath().String(), yield, seed) s.IterateString("FilePath", s.model.FilePath().String(), yield, seed)
s.IterateString("InstrumentName", s.model.InstrumentName().String(), yield, seed) s.IterateString("InstrumentName", s.model.InstrumentName().String(), yield, seed)

View File

@ -481,7 +481,7 @@ func (v *Notes) add(rect Rect, delta int) (ok bool) {
newVal = 255 newVal = 255
} }
// only do all sets after all gets, so we don't accidentally adjust single note multiple times // 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 return true
@ -540,7 +540,7 @@ func (v *Notes) unmarshalAtCursor(data []byte) bool {
continue continue
} }
pos := v.d.Song.Score.SongPos(y) 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 return true
@ -562,7 +562,7 @@ func (v *Notes) unmarshalRange(rect Rect, data []byte) bool {
continue continue
} }
pos := v.d.Song.Score.SongPos(y) 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 return true
@ -609,7 +609,7 @@ func (m *Notes) SetValue(p Point, val byte) {
} }
track := &(m.d.Song.Score.Tracks[p.X]) track := &(m.d.Song.Score.Tracks[p.X])
pos := m.d.Song.Score.SongPos(p.Y) 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) { func (v *Notes) FillNibble(value byte, lowNibble bool) {