From 1693d7ed5e3cdbb6e2eb973dd3d654f5126943cd Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:42:25 +0200 Subject: [PATCH] refactor(tracker): make Model methods return List, avoiding .List() --- tracker/action.go | 10 +- tracker/bool.go | 5 +- tracker/derived.go | 9 +- tracker/gioui/draglist.go | 2 +- tracker/gioui/instrument_editor.go | 37 +-- tracker/gioui/keybindings.go | 2 +- tracker/gioui/note_editor.go | 4 +- tracker/gioui/order_editor.go | 4 +- tracker/gioui/patch_panel.go | 6 +- tracker/list.go | 428 +++++++++++------------------ tracker/model.go | 2 + tracker/model_test.go | 12 +- tracker/string.go | 16 ++ 13 files changed, 215 insertions(+), 322 deletions(-) diff --git a/tracker/action.go b/tracker/action.go index 2ec079d..6f7bb73 100644 --- a/tracker/action.go +++ b/tracker/action.go @@ -145,7 +145,7 @@ func (m *AddTrack) Do() { func (m *Model) DeleteTrack() Action { return MakeAction((*DeleteTrack)(m)) } func (m *DeleteTrack) Enabled() bool { return len(m.d.Song.Score.Tracks) > 0 } -func (m *DeleteTrack) Do() { (*Model)(m).Tracks().List().DeleteElements(false) } +func (m *DeleteTrack) Do() { (*Model)(m).Tracks().DeleteElements(false) } // AddInstrument @@ -164,7 +164,7 @@ func (m *AddInstrument) Do() { func (m *Model) DeleteInstrument() Action { return MakeAction((*DeleteInstrument)(m)) } func (m *DeleteInstrument) Enabled() bool { return len((*Model)(m).d.Song.Patch) > 0 } -func (m *DeleteInstrument) Do() { (*Model)(m).Instruments().List().DeleteElements(false) } +func (m *DeleteInstrument) Do() { (*Model)(m).Instruments().DeleteElements(false) } // SplitTrack @@ -259,7 +259,7 @@ func (m *DeleteUnit) Enabled() bool { } func (m *DeleteUnit) Do() { defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)() - (*Model)(m).Units().List().DeleteElements(true) + (*Model)(m).Units().DeleteElements(true) } // ClearUnit @@ -271,7 +271,7 @@ func (m *ClearUnit) Enabled() bool { } func (m *ClearUnit) Do() { defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)() - l := ((*Model)(m)).Units().List() + l := ((*Model)(m)).Units() r := l.listRange() for i := r.Start; i < r.End; i++ { m.d.Song.Patch[m.d.InstrIndex].Units[i] = sointu.Unit{} @@ -426,7 +426,7 @@ func (m *PlaySelected) Enabled() bool { return !m.instrEnlarged } func (m *PlaySelected) Do() { (*Model)(m).setPanic(false) m.playing = true - l := (*Model)(m).OrderRows().List() + l := (*Model)(m).OrderRows() r := l.listRange() newLoop := Loop{r.Start, r.End - r.Start} (*Model)(m).setLoop(newLoop) diff --git a/tracker/bool.go b/tracker/bool.go index 533ea7e..70b950a 100644 --- a/tracker/bool.go +++ b/tracker/bool.go @@ -272,6 +272,7 @@ func (m *UnitSearching) SetValue(val bool) { return } m.d.UnitSearchString = m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Type + (*Model)(m).updateDerivedUnitSearch() } // UnitDisabled methods @@ -290,7 +291,7 @@ func (m *UnitDisabled) SetValue(val bool) { if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) { return } - l := ((*Model)(m)).Units().List() + l := ((*Model)(m)).Units() r := l.listRange() defer (*Model)(m).change("UnitDisabledSet", PatchChange, MajorChange)() for i := r.Start; i < r.End; i++ { @@ -315,7 +316,7 @@ func (t *LoopToggle) SetValue(val bool) { m := (*Model)(t) newLoop := Loop{} if val { - l := m.OrderRows().List() + l := m.OrderRows() r := l.listRange() newLoop = Loop{r.Start, r.End - r.Start} } diff --git a/tracker/derived.go b/tracker/derived.go index cbcc983..7a47ba6 100644 --- a/tracker/derived.go +++ b/tracker/derived.go @@ -33,10 +33,11 @@ type ( // corresponding part of the model changes. derivedModelData struct { // map Unit by ID, other entities by their respective index - patch []derivedInstrument - tracks []derivedTrack - railError RailError - presetSearch derivedPresetSearch + patch []derivedInstrument + tracks []derivedTrack + railError RailError + presetSearch derivedPresetSearch + searchResults []string } derivedInstrument struct { diff --git a/tracker/gioui/draglist.go b/tracker/gioui/draglist.go index 92f891f..3e6da3b 100644 --- a/tracker/gioui/draglist.go +++ b/tracker/gioui/draglist.go @@ -128,7 +128,7 @@ func (s FilledDragListStyle) Layout(gtx C, element, bg func(gtx C, i int) D) D { gtx.Execute(op.InvalidateCmd{}) } - _, isMutable := s.dragList.TrackerList.ListData.(tracker.MutableListData) + isMutable := s.dragList.TrackerList.Mutable() listElem := func(gtx C, index int) D { for len(s.dragList.tags) <= index { diff --git a/tracker/gioui/instrument_editor.go b/tracker/gioui/instrument_editor.go index 08548fc..af3687c 100644 --- a/tracker/gioui/instrument_editor.go +++ b/tracker/gioui/instrument_editor.go @@ -53,7 +53,7 @@ type ( func NewInstrumentEditor(m *tracker.Model) *InstrumentEditor { ret := &InstrumentEditor{ - dragList: NewDragList(m.Units().List(), layout.Vertical), + dragList: NewDragList(m.Units(), layout.Vertical), addUnitBtn: new(Clickable), searchEditor: NewEditor(true, true, text.Start), DeleteUnitBtn: new(Clickable), @@ -62,8 +62,8 @@ func NewInstrumentEditor(m *tracker.Model) *InstrumentEditor { CopyUnitBtn: new(Clickable), SelectTypeBtn: new(Clickable), commentEditor: NewEditor(true, true, text.Start), - paramTable: NewScrollTable(m.Params().Table(), m.ParamVertList().List(), m.Units().List()), - searchList: NewDragList(m.SearchResults().List(), layout.Vertical), + paramTable: NewScrollTable(m.Params().Table(), m.ParamVertList().List(), m.Units()), + searchList: NewDragList(m.SearchResults(), layout.Vertical), searching: m.UnitSearching(), } ret.caser = cases.Title(language.English) @@ -95,7 +95,7 @@ func (ul *InstrumentEditor) layoutList(gtx C) D { element := func(gtx C, i int) D { gtx.Constraints.Max.Y = gtx.Dp(20) gtx.Constraints.Min.Y = gtx.Constraints.Max.Y - u := t.Units().Item(i) + u := t.Unit(i) editorStyle := t.Theme.InstrumentEditor.UnitList.Name signalError := t.RailError() switch { @@ -169,7 +169,7 @@ func (ul *InstrumentEditor) update(gtx C) { case key.NameRightArrow: t.PatchPanel.instrEditor.paramTable.RowTitleList.Focus() case key.NameDeleteBackward: - t.Units().SetSelectedType("") + t.SetSelectedUnitType("") t.UnitSearching().SetValue(true) ul.searchEditor.Focus() case key.NameEnter, key.NameReturn: @@ -185,12 +185,12 @@ func (ul *InstrumentEditor) update(gtx C) { if str.Value() != "" { for _, n := range sointu.UnitNames { if strings.HasPrefix(n, str.Value()) { - t.Units().SetSelectedType(n) + t.SetSelectedUnitType(n) break } } } else { - t.Units().SetSelectedType("") + t.SetSelectedUnitType("") } } ul.dragList.Focus() @@ -202,7 +202,7 @@ func (ul *InstrumentEditor) update(gtx C) { ul.searchEditor.Focus() } for ul.CopyUnitBtn.Clicked(gtx) { - if contents, ok := t.Units().List().CopyElements(); ok { + if contents, ok := t.Units().CopyElements(); ok { gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))}) t.Alerts().Add("Unit(s) copied to clipboard", tracker.Info) } @@ -288,8 +288,8 @@ func (pe *InstrumentEditor) layoutTable(gtx C) D { } func (pe *InstrumentEditor) ChooseUnitType(t *Tracker) { - if ut, ok := t.SearchResults().Item(pe.searchList.TrackerList.Selected()); ok { - t.Units().SetSelectedType(ut) + if ut, ok := t.SearchResult(pe.searchList.TrackerList.Selected()); ok { + t.SetSelectedUnitType(ut) pe.paramTable.RowTitleList.Focus() } } @@ -321,7 +321,7 @@ func (pe *InstrumentEditor) layoutRack(gtx C) D { if y < 0 || y >= len(pe.Parameters) { return D{} } - item := t.Units().Item(y) + item := t.Unit(y) sr := Rail(t.Theme, item.Signals) label := Label(t.Theme, &t.Theme.UnitEditor.UnitList.Name, item.Type) switch { @@ -360,7 +360,7 @@ func (pe *InstrumentEditor) layoutRack(gtx C) D { } param := t.Model.Params().Item(point) - paramStyle := Param(param, t.Theme, pe.Parameters[y][x], pe.paramTable.Table.Cursor() == point, t.Units().Item(y).Disabled) + paramStyle := Param(param, t.Theme, pe.Parameters[y][x], pe.paramTable.Table.Cursor() == point, t.Unit(y).Disabled) paramStyle.Layout(gtx) if x == t.Model.Params().RowWidth(y) { if y == cursor.Y { @@ -373,7 +373,7 @@ func (pe *InstrumentEditor) layoutRack(gtx C) D { return pe.commentEditor.Layout(gtx, t.UnitComment(), t.Theme, &t.Theme.InstrumentEditor.UnitComment, "---") }) } else { - comment := t.Units().Item(y).Comment + comment := t.Unit(y).Comment if comment != "" { style := t.Theme.InstrumentEditor.UnitComment.AsLabelStyle() label := Label(t.Theme, &style, comment) @@ -530,16 +530,9 @@ func (pe *InstrumentEditor) layoutFooter(gtx C) D { func (pe *InstrumentEditor) layoutUnitTypeChooser(gtx C) D { t := TrackerFromContext(gtx) - var namesArray [256]string - names := namesArray[:0] - for _, item := range t.Model.SearchResults().Iterate { - names = append(names, item) - } element := func(gtx C, i int) D { - if i < 0 || i >= len(names) { - return D{} - } - w := Label(t.Theme, &t.Theme.UnitEditor.Chooser, names[i]) + name, _ := t.SearchResult(i) + w := Label(t.Theme, &t.Theme.UnitEditor.Chooser, name) if i == pe.searchList.TrackerList.Selected() { return pe.SelectTypeBtn.Layout(gtx, w.Layout) } diff --git a/tracker/gioui/keybindings.go b/tracker/gioui/keybindings.go index 97f2069..ee46d48 100644 --- a/tracker/gioui/keybindings.go +++ b/tracker/gioui/keybindings.go @@ -289,7 +289,7 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) { if err != nil { break } - instr := t.Model.Instruments().List().Selected() + instr := t.Model.Instruments().Selected() n := noteAsValue(t.Model.Octave().Value(), val-12) t.KeyNoteMap.Press(e.Name, tracker.NoteEvent{Channel: instr, Note: n}) } diff --git a/tracker/gioui/note_editor.go b/tracker/gioui/note_editor.go index aef442f..8a5572b 100644 --- a/tracker/gioui/note_editor.go +++ b/tracker/gioui/note_editor.go @@ -93,8 +93,8 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor { TrackMidiInBtn: new(Clickable), scrollTable: NewScrollTable( model.Notes().Table(), - model.Tracks().List(), - model.NoteRows().List(), + model.Tracks(), + model.NoteRows(), ), } for k, a := range keyBindingMap { diff --git a/tracker/gioui/order_editor.go b/tracker/gioui/order_editor.go index 52b8b01..04398f3 100644 --- a/tracker/gioui/order_editor.go +++ b/tracker/gioui/order_editor.go @@ -42,8 +42,8 @@ func NewOrderEditor(m *tracker.Model) *OrderEditor { return &OrderEditor{ scrollTable: NewScrollTable( m.Order().Table(), - m.Tracks().List(), - m.OrderRows().List(), + m.Tracks(), + m.OrderRows(), ), } } diff --git a/tracker/gioui/patch_panel.go b/tracker/gioui/patch_panel.go index 81c71ba..6ebc81e 100644 --- a/tracker/gioui/patch_panel.go +++ b/tracker/gioui/patch_panel.go @@ -179,7 +179,7 @@ func (it *InstrumentTools) Layout(gtx C) D { func (it *InstrumentTools) update(gtx C, tr *Tracker) { for it.copyInstrumentBtn.Clicked(gtx) { - if contents, ok := tr.Instruments().List().CopyElements(); ok { + if contents, ok := tr.Instruments().CopyElements(); ok { gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))}) tr.Alerts().Add("Instrument copied to clipboard", tracker.Info) } @@ -208,7 +208,7 @@ func (it *InstrumentTools) Tags(level int, yield TagYieldFunc) bool { func MakeInstrList(model *tracker.Model) InstrumentList { return InstrumentList{ - instrumentDragList: NewDragList(model.Instruments().List(), layout.Horizontal), + instrumentDragList: NewDragList(model.Instruments(), layout.Horizontal), nameEditor: NewEditor(true, true, text.Middle), } } @@ -221,7 +221,7 @@ func (il *InstrumentList) Layout(gtx C) D { element := func(gtx C, i int) D { grabhandle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, strconv.Itoa(i+1)) label := func(gtx C) D { - name, level, mute, ok := (*tracker.Instruments)(t.Model).Item(i) + name, level, mute, ok := t.Instrument(i) if !ok { labelStyle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, "") return layout.Center.Layout(gtx, labelStyle.Layout) diff --git a/tracker/list.go b/tracker/list.go index 5eb3a1e..a7771f8 100644 --- a/tracker/list.go +++ b/tracker/list.go @@ -6,7 +6,6 @@ import ( "iter" "math" "math/bits" - "strings" "github.com/vsariola/sointu" "github.com/vsariola/sointu/vm" @@ -15,7 +14,7 @@ import ( type ( List struct { - ListData + data ListData } ListData interface { @@ -27,49 +26,32 @@ type ( } MutableListData interface { - change(kind string, severity ChangeSeverity) func() - cancel() - move(r Range, delta int) (ok bool) - delete(r Range) (ok bool) - marshal(r Range) ([]byte, error) - unmarshal([]byte) (r Range, err error) - } - - UnitListItem struct { - Type, Comment string - Disabled bool - Signals Rail + Change(kind string, severity ChangeSeverity) func() + Cancel() + Move(r Range, delta int) (ok bool) + Delete(r Range) (ok bool) + Marshal(r Range) ([]byte, error) + Unmarshal([]byte) (r Range, err error) } // Range is used to represent a range [Start,End) of integers Range struct { Start, End int } - - UnitYieldFunc func(index int, item UnitListItem) (ok bool) - UnitSearchYieldFunc func(index int, item string) (ok bool) - - Instruments Model // Instruments is a list of instruments, implementing ListData & MutableListData interfaces - Units Model // Units is a list of all the units in the selected instrument, implementing ListData & MutableListData interfaces - Tracks Model // Tracks is a list of all the tracks, implementing ListData & MutableListData interfaces - OrderRows Model // OrderRows is a list of all the order rows, implementing ListData & MutableListData interfaces - NoteRows Model // NoteRows is a list of all the note rows, implementing ListData & MutableListData interfaces - SearchResults Model // SearchResults is a unmutable list of all the search results, implementing ListData interface ) -// Model methods +func MakeList(data ListData) List { return List{data} } -func (m *Model) Instruments() *Instruments { return (*Instruments)(m) } -func (m *Model) Units() *Units { return (*Units)(m) } -func (m *Model) Tracks() *Tracks { return (*Tracks)(m) } -func (m *Model) OrderRows() *OrderRows { return (*OrderRows)(m) } -func (m *Model) NoteRows() *NoteRows { return (*NoteRows)(m) } -func (m *Model) SearchResults() *SearchResults { return (*SearchResults)(m) } +func (l List) Selected() int { return max(min(l.data.Selected(), l.data.Count()-1), 0) } +func (l List) Selected2() int { return max(min(l.data.Selected2(), l.data.Count()-1), 0) } +func (l List) SetSelected(value int) { l.data.SetSelected(max(min(value, l.data.Count()-1), 0)) } +func (l List) SetSelected2(value int) { l.data.SetSelected2(max(min(value, l.data.Count()-1), 0)) } +func (l List) Count() int { return l.data.Count() } // MoveElements moves the selected elements in a list by delta. The list must // implement the MutableListData interface. func (v List) MoveElements(delta int) bool { - s, ok := v.ListData.(MutableListData) + s, ok := v.data.(MutableListData) if !ok { return false } @@ -77,9 +59,9 @@ func (v List) MoveElements(delta int) bool { if delta == 0 || r.Start+delta < 0 || r.End+delta > v.Count() { return false } - defer s.change("MoveElements", MajorChange)() - if !s.move(r, delta) { - s.cancel() + defer s.Change("MoveElements", MajorChange)() + if !s.Move(r, delta) { + s.Cancel() return false } v.SetSelected(v.Selected() + delta) @@ -90,7 +72,7 @@ func (v List) MoveElements(delta int) bool { // DeleteElements deletes the selected elements in a list. The list must // implement the MutableListData interface. func (v List) DeleteElements(backwards bool) bool { - d, ok := v.ListData.(MutableListData) + d, ok := v.data.(MutableListData) if !ok { return false } @@ -98,9 +80,9 @@ func (v List) DeleteElements(backwards bool) bool { if r.Len() == 0 { return false } - defer d.change("DeleteElements", MajorChange)() - if !d.delete(r) { - d.cancel() + defer d.Change("DeleteElements", MajorChange)() + if !d.Delete(r) { + d.Cancel() return false } if backwards && r.Start > 0 { @@ -115,7 +97,7 @@ func (v List) DeleteElements(backwards bool) bool { // the MutableListData interface. Returns the copied data, marshaled into byte // slice, and true if successful. func (v List) CopyElements() ([]byte, bool) { - m, ok := v.ListData.(MutableListData) + m, ok := v.data.(MutableListData) if !ok { return nil, false } @@ -123,7 +105,7 @@ func (v List) CopyElements() ([]byte, bool) { if r.Len() == 0 { return nil, false } - ret, err := m.marshal(r) + ret, err := m.Marshal(r) if err != nil { return nil, false } @@ -134,14 +116,14 @@ func (v List) CopyElements() ([]byte, bool) { // byte slice. The list must implement the MutableListData interface. Returns // true if successful. func (v List) PasteElements(data []byte) (ok bool) { - m, ok := v.ListData.(MutableListData) + m, ok := v.data.(MutableListData) if !ok { return false } - defer m.change("PasteElements", MajorChange)() - r, err := m.unmarshal(data) + defer m.Change("PasteElements", MajorChange)() + r, err := m.Unmarshal(data) if err != nil { - m.cancel() + m.Cancel() return false } v.SetSelected(r.Start) @@ -149,19 +131,23 @@ func (v List) PasteElements(data []byte) (ok bool) { return true } +func (v List) Mutable() bool { + _, ok := v.data.(MutableListData) + return ok +} + func (v *List) listRange() (r Range) { r.Start = max(min(v.Selected(), v.Selected2()), 0) r.End = min(max(v.Selected(), v.Selected2())+1, v.Count()) return } -// Instruments methods +// instruments is a list of instruments, implementing ListData & MutableListData interfaces +type instruments Model -func (v *Instruments) List() List { - return List{v} -} +func (m *Model) Instruments() List { return List{(*instruments)(m)} } -func (v *Instruments) Item(i int) (name string, maxLevel float32, mute bool, ok bool) { +func (v *Model) Instrument(i int) (name string, maxLevel float32, mute bool, ok bool) { if i < 0 || i >= len(v.d.Song.Patch) { return "", 0, false, false } @@ -182,37 +168,20 @@ func (v *Instruments) Item(i int) (name string, maxLevel float32, mute bool, ok ok = true return } -func (v *Instruments) FirstID(i int) (id int, ok bool) { - if i < 0 || i >= len(v.d.Song.Patch) { - return 0, false - } - if len(v.d.Song.Patch[i].Units) == 0 { - return 0, false - } - return v.d.Song.Patch[i].Units[0].ID, true -} -func (v *Instruments) Selected() int { - return max(min(v.d.InstrIndex, v.Count()-1), 0) -} - -func (v *Instruments) Selected2() int { - return max(min(v.d.InstrIndex2, v.Count()-1), 0) -} - -func (v *Instruments) SetSelected(value int) { - v.d.InstrIndex = max(min(value, v.Count()-1), 0) +func (v *instruments) Count() int { return len(v.d.Song.Patch) } +func (v *instruments) Selected() int { return v.d.InstrIndex } +func (v *instruments) Selected2() int { return v.d.InstrIndex2 } +func (v *instruments) SetSelected2(value int) { v.d.InstrIndex2 = value } +func (v *instruments) SetSelected(value int) { + v.d.InstrIndex = value v.d.UnitIndex = 0 v.d.UnitIndex2 = 0 v.d.UnitSearching = false v.d.UnitSearchString = "" } -func (v *Instruments) SetSelected2(value int) { - v.d.InstrIndex2 = max(min(value, v.Count()-1), 0) -} - -func (v *Instruments) move(r Range, delta int) (ok bool) { +func (v *instruments) Move(r Range, delta int) (ok bool) { voiceDelta := 0 if delta < 0 { voiceDelta = -VoiceRange(v.d.Song.Patch, Range{r.Start + delta, r.Start}).Len() @@ -226,28 +195,24 @@ func (v *Instruments) move(r Range, delta int) (ok bool) { return (*Model)(v).sliceInstrumentsTracks(true, v.linkInstrTrack, ranges[:]...) } -func (v *Instruments) delete(r Range) (ok bool) { +func (v *instruments) Delete(r Range) (ok bool) { ranges := Complement(VoiceRange(v.d.Song.Patch, r)) return (*Model)(v).sliceInstrumentsTracks(true, v.linkInstrTrack, ranges[:]...) } -func (v *Instruments) change(n string, severity ChangeSeverity) func() { +func (v *instruments) Change(n string, severity ChangeSeverity) func() { return (*Model)(v).change("Instruments."+n, SongChange, severity) } -func (v *Instruments) cancel() { +func (v *instruments) Cancel() { v.changeCancel = true } -func (v *Instruments) Count() int { - return len(v.d.Song.Patch) -} - -func (v *Instruments) marshal(r Range) ([]byte, error) { +func (v *instruments) Marshal(r Range) ([]byte, error) { return (*Model)(v).marshalVoices(VoiceRange(v.d.Song.Patch, r)) } -func (m *Instruments) unmarshal(data []byte) (r Range, err error) { +func (m *instruments) Unmarshal(data []byte) (r Range, err error) { voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex) r, _, ok := (*Model)(m).unmarshalVoices(voiceIndex, data, true, m.linkInstrTrack) if !ok { @@ -256,13 +221,37 @@ func (m *Instruments) unmarshal(data []byte) (r Range, err error) { return r, nil } -// Units methods +// units is a list of all the units in the selected instrument, implementing ListData & MutableListData interfaces +type ( + units Model + UnitListItem struct { + Type, Comment string + Disabled bool + Signals Rail + } +) -func (v *Units) List() List { - return List{v} +func (m *Model) Units() List { return List{(*units)(m)} } + +func (v *Model) Unit(index int) UnitListItem { + i := v.d.InstrIndex + if i < 0 || i >= len(v.d.Song.Patch) || index < 0 || index >= (*units)(v).Count() { + return UnitListItem{} + } + unit := v.d.Song.Patch[v.d.InstrIndex].Units[index] + signals := Rail{} + if i >= 0 && i < len(v.derived.patch) && index >= 0 && index < len(v.derived.patch[i].rails) { + signals = v.derived.patch[i].rails[index] + } + return UnitListItem{ + Type: unit.Type, + Comment: unit.Comment, + Disabled: unit.Disabled, + Signals: signals, + } } -func (m *Units) SelectedType() string { +func (m *Model) SelectedUnitType() string { if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) || m.d.UnitIndex < 0 || @@ -272,7 +261,7 @@ func (m *Units) SelectedType() string { return m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Type } -func (m *Units) SetSelectedType(t string) { +func (m *Model) SetSelectedUnitType(t string) { if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) { return @@ -293,58 +282,28 @@ func (m *Units) SetSelectedType(t string) { if oldUnit.Type == unit.Type { return } - defer m.change("SetSelectedType", MajorChange)() + defer (*units)(m).Change("SetSelectedType", MajorChange)() m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex] = unit m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].ID = oldUnit.ID // keep the ID of the replaced unit } -func (v *Units) Item(index int) UnitListItem { - i := v.d.InstrIndex - if i < 0 || i >= len(v.d.Song.Patch) || index < 0 || index >= v.Count() { - return UnitListItem{} - } - unit := v.d.Song.Patch[v.d.InstrIndex].Units[index] - signals := Rail{} - if i >= 0 && i < len(v.derived.patch) && index >= 0 && index < len(v.derived.patch[i].rails) { - signals = v.derived.patch[i].rails[index] - } - return UnitListItem{ - Type: unit.Type, - Comment: unit.Comment, - Disabled: unit.Disabled, - Signals: signals, - } -} - -func (v *Units) Selected() int { - return max(min(v.d.UnitIndex, v.Count()-1), 0) -} - -func (v *Units) Selected2() int { - return max(min(v.d.UnitIndex2, v.Count()-1), 0) -} - -func (v *Units) SetSelected(value int) { - m := (*Model)(v) - m.d.UnitIndex = max(min(value, v.Count()-1), 0) +func (v *units) Selected() int { return v.d.UnitIndex } +func (v *units) Selected2() int { return v.d.UnitIndex2 } +func (v *units) SetSelected2(value int) { v.d.UnitIndex2 = value } +func (m *units) SetSelected(value int) { + m.d.UnitIndex = value m.d.ParamIndex = 0 m.d.UnitSearching = false m.d.UnitSearchString = "" } - -func (v *Units) SetSelected2(value int) { - (*Model)(v).d.UnitIndex2 = max(min(value, v.Count()-1), 0) -} - -func (v *Units) Count() int { - m := (*Model)(v) - if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) { +func (v *units) Count() int { + if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) { return 0 } - return len(m.d.Song.Patch[(*Model)(v).d.InstrIndex].Units) + return len(v.d.Song.Patch[v.d.InstrIndex].Units) } -func (v *Units) move(r Range, delta int) (ok bool) { +func (v *units) Move(r Range, delta int) (ok bool) { m := (*Model)(v) if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) { return false @@ -356,7 +315,7 @@ func (v *Units) move(r Range, delta int) (ok bool) { return true } -func (v *Units) delete(r Range) (ok bool) { +func (v *units) Delete(r Range) (ok bool) { m := (*Model)(v) if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) { return false @@ -366,15 +325,15 @@ func (v *Units) delete(r Range) (ok bool) { return true } -func (v *Units) change(n string, severity ChangeSeverity) func() { +func (v *units) Change(n string, severity ChangeSeverity) func() { return (*Model)(v).change("UnitListView."+n, PatchChange, severity) } -func (v *Units) cancel() { +func (v *units) Cancel() { (*Model)(v).changeCancel = true } -func (v *Units) marshal(r Range) ([]byte, error) { +func (v *units) Marshal(r Range) ([]byte, error) { m := (*Model)(v) if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) { return nil, errors.New("UnitListView.marshal: no instruments") @@ -387,7 +346,7 @@ func (v *Units) marshal(r Range) ([]byte, error) { return ret, nil } -func (v *Units) unmarshal(data []byte) (r Range, err error) { +func (v *units) Unmarshal(data []byte) (r Range, err error) { m := (*Model)(v) if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) { return Range{}, errors.New("UnitListView.unmarshal: no instruments") @@ -409,29 +368,18 @@ func (v *Units) unmarshal(data []byte) (r Range, err error) { return Range{sel, sel + len(pastedUnits.Units)}, nil } -// Tracks methods +// tracks is a list of all the tracks, implementing ListData & MutableListData interfaces +type tracks Model -func (v *Tracks) List() List { - return List{v} -} +func (m *Model) Tracks() List { return List{(*tracks)(m)} } -func (v *Tracks) Selected() int { - return max(min(v.d.Cursor.Track, v.Count()-1), 0) -} +func (v *tracks) Selected() int { return v.d.Cursor.Track } +func (v *tracks) Selected2() int { return v.d.Cursor2.Track } +func (v *tracks) SetSelected(value int) { v.d.Cursor.Track = value } +func (v *tracks) SetSelected2(value int) { v.d.Cursor2.Track = value } +func (v *tracks) Count() int { return len((*Model)(v).d.Song.Score.Tracks) } -func (v *Tracks) Selected2() int { - return max(min(v.d.Cursor2.Track, v.Count()-1), 0) -} - -func (v *Tracks) SetSelected(value int) { - v.d.Cursor.Track = max(min(value, v.Count()-1), 0) -} - -func (v *Tracks) SetSelected2(value int) { - v.d.Cursor2.Track = max(min(value, v.Count()-1), 0) -} - -func (v *Tracks) move(r Range, delta int) (ok bool) { +func (v *tracks) Move(r Range, delta int) (ok bool) { voiceDelta := 0 if delta < 0 { voiceDelta = -VoiceRange(v.d.Song.Score.Tracks, Range{r.Start + delta, r.Start}).Len() @@ -445,28 +393,24 @@ func (v *Tracks) move(r Range, delta int) (ok bool) { return (*Model)(v).sliceInstrumentsTracks(v.linkInstrTrack, true, ranges[:]...) } -func (v *Tracks) delete(r Range) (ok bool) { +func (v *tracks) Delete(r Range) (ok bool) { ranges := Complement(VoiceRange(v.d.Song.Score.Tracks, r)) return (*Model)(v).sliceInstrumentsTracks(v.linkInstrTrack, true, ranges[:]...) } -func (v *Tracks) change(n string, severity ChangeSeverity) func() { +func (v *tracks) Change(n string, severity ChangeSeverity) func() { return (*Model)(v).change("TrackList."+n, SongChange, severity) } -func (v *Tracks) cancel() { +func (v *tracks) Cancel() { v.changeCancel = true } -func (v *Tracks) Count() int { - return len((*Model)(v).d.Song.Score.Tracks) -} - -func (v *Tracks) marshal(r Range) ([]byte, error) { +func (v *tracks) Marshal(r Range) ([]byte, error) { return (*Model)(v).marshalVoices(VoiceRange(v.d.Song.Score.Tracks, r)) } -func (m *Tracks) unmarshal(data []byte) (r Range, err error) { +func (m *tracks) Unmarshal(data []byte) (r Range, err error) { voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track) _, r, ok := (*Model)(m).unmarshalVoices(voiceIndex, data, m.linkInstrTrack, true) if !ok { @@ -475,37 +419,23 @@ func (m *Tracks) unmarshal(data []byte) (r Range, err error) { return r, nil } -// OrderRows methods +// orderRows is a list of all the order rows, implementing ListData & MutableListData interfaces +type orderRows Model -func (v *OrderRows) List() List { - return List{v} -} +func (m *Model) OrderRows() List { return List{(*orderRows)(m)} } -func (v *OrderRows) Selected() int { - p := v.d.Cursor.OrderRow - p = max(min(p, v.Count()-1), 0) - return p -} - -func (v *OrderRows) Selected2() int { - p := v.d.Cursor2.OrderRow - p = max(min(p, v.Count()-1), 0) - return p -} - -func (v *OrderRows) SetSelected(value int) { - y := max(min(value, v.Count()-1), 0) - if y != v.d.Cursor.OrderRow { +func (v *orderRows) Count() int { return v.d.Song.Score.Length } +func (v *orderRows) Selected() int { return v.d.Cursor.OrderRow } +func (v *orderRows) Selected2() int { return v.d.Cursor2.OrderRow } +func (v *orderRows) SetSelected2(value int) { v.d.Cursor2.OrderRow = value } +func (v *orderRows) SetSelected(value int) { + if value != v.d.Cursor.OrderRow { v.follow = false } - v.d.Cursor.OrderRow = y + v.d.Cursor.OrderRow = value } -func (v *OrderRows) SetSelected2(value int) { - v.d.Cursor2.OrderRow = max(min(value, v.Count()-1), 0) -} - -func (v *OrderRows) move(r Range, delta int) (ok bool) { +func (v *orderRows) Move(r Range, delta int) (ok bool) { swaps := r.Swaps(delta) for i, t := range v.d.Song.Score.Tracks { for a, b := range swaps { @@ -517,7 +447,7 @@ func (v *OrderRows) move(r Range, delta int) (ok bool) { return true } -func (v *OrderRows) delete(r Range) (ok bool) { +func (v *orderRows) Delete(r Range) (ok bool) { for i, t := range v.d.Song.Score.Tracks { r2 := r.Intersect(Range{0, len(t.Order)}) v.d.Song.Score.Tracks[i].Order = append(t.Order[:r2.Start], t.Order[r2.End:]...) @@ -525,23 +455,19 @@ func (v *OrderRows) delete(r Range) (ok bool) { return true } -func (v *OrderRows) change(n string, severity ChangeSeverity) func() { +func (v *orderRows) Change(n string, severity ChangeSeverity) func() { return (*Model)(v).change("OrderRowList."+n, ScoreChange, severity) } -func (v *OrderRows) cancel() { +func (v *orderRows) Cancel() { v.changeCancel = true } -func (v *OrderRows) Count() int { - return v.d.Song.Score.Length -} - type marshalOrderRows struct { Columns [][]int `yaml:",flow"` } -func (v *OrderRows) marshal(r Range) ([]byte, error) { +func (v *orderRows) Marshal(r Range) ([]byte, error) { var table marshalOrderRows for i := range v.d.Song.Score.Tracks { table.Columns = append(table.Columns, make([]int, r.Len())) @@ -552,7 +478,7 @@ func (v *OrderRows) marshal(r Range) ([]byte, error) { return yaml.Marshal(table) } -func (v *OrderRows) unmarshal(data []byte) (r Range, err error) { +func (v *orderRows) Unmarshal(data []byte) (r Range, err error) { var table marshalOrderRows err = yaml.Unmarshal(data, &table) if err != nil { @@ -581,33 +507,23 @@ func (v *OrderRows) unmarshal(data []byte) (r Range, err error) { return } -// NoteRows methods +// noteRows is a list of all the note rows, implementing ListData & MutableListData interfaces +type noteRows Model -func (v *NoteRows) List() List { - return List{v} -} +func (m *Model) NoteRows() List { return List{(*noteRows)(m)} } -func (v *NoteRows) Selected() int { - return v.d.Song.Score.SongRow(v.d.Song.Score.Clamp(v.d.Cursor.SongPos)) -} - -func (v *NoteRows) Selected2() int { - return v.d.Song.Score.SongRow(v.d.Song.Score.Clamp(v.d.Cursor2.SongPos)) -} - -func (v *NoteRows) SetSelected(value int) { - if value != v.d.Song.Score.SongRow(v.d.Cursor.SongPos) { - v.follow = false +func (n *noteRows) Count() int { return n.d.Song.Score.Length * n.d.Song.Score.RowsPerPattern } +func (n *noteRows) Selected() int { return n.d.Song.Score.SongRow(n.d.Cursor.SongPos) } +func (n *noteRows) Selected2() int { return n.d.Song.Score.SongRow(n.d.Cursor2.SongPos) } +func (n *noteRows) SetSelected2(v int) { n.d.Cursor2.SongPos = n.d.Song.Score.SongPos(v) } +func (n *noteRows) SetSelected(value int) { + if value != n.d.Song.Score.SongRow(n.d.Cursor.SongPos) { + n.follow = false } - v.d.Cursor.SongPos = v.d.Song.Score.Clamp(v.d.Song.Score.SongPos(value)) + n.d.Cursor.SongPos = n.d.Song.Score.Clamp(n.d.Song.Score.SongPos(value)) } -func (v *NoteRows) SetSelected2(value int) { - v.d.Cursor2.SongPos = v.d.Song.Score.Clamp(v.d.Song.Score.SongPos(value)) - -} - -func (v *NoteRows) move(r Range, delta int) (ok bool) { +func (v *noteRows) Move(r Range, delta int) (ok bool) { for a, b := range r.Swaps(delta) { apos := v.d.Song.Score.SongPos(a) bpos := v.d.Song.Score.SongPos(b) @@ -621,7 +537,7 @@ func (v *NoteRows) move(r Range, delta int) (ok bool) { return true } -func (v *NoteRows) delete(r Range) (ok bool) { +func (v *noteRows) Delete(r Range) (ok bool) { for _, track := range v.d.Song.Score.Tracks { for i := r.Start; i < r.End; i++ { pos := v.d.Song.Score.SongPos(i) @@ -631,23 +547,19 @@ func (v *NoteRows) delete(r Range) (ok bool) { return true } -func (v *NoteRows) change(n string, severity ChangeSeverity) func() { +func (v *noteRows) Change(n string, severity ChangeSeverity) func() { return (*Model)(v).change("NoteRowList."+n, ScoreChange, severity) } -func (v *NoteRows) cancel() { +func (v *noteRows) Cancel() { (*Model)(v).changeCancel = true } -func (v *NoteRows) Count() int { - return (*Model)(v).d.Song.Score.Length * v.d.Song.Score.RowsPerPattern -} - type marshalNoteRows struct { NoteRows [][]byte `yaml:",flow"` } -func (v *NoteRows) marshal(r Range) ([]byte, error) { +func (v *noteRows) Marshal(r Range) ([]byte, error) { var table marshalNoteRows for i, track := range v.d.Song.Score.Tracks { table.NoteRows = append(table.NoteRows, make([]byte, r.Len())) @@ -660,7 +572,7 @@ func (v *NoteRows) marshal(r Range) ([]byte, error) { return yaml.Marshal(table) } -func (v *NoteRows) unmarshal(data []byte) (r Range, err error) { +func (v *noteRows) Unmarshal(data []byte) (r Range, err error) { var table marshalNoteRows if err := yaml.Unmarshal(data, &table); err != nil { return Range{}, fmt.Errorf("NoteRowList.unmarshal: %v", err) @@ -683,57 +595,25 @@ func (v *NoteRows) unmarshal(data []byte) (r Range, err error) { return } -// SearchResults +// searchResults is a unmutable list of all the search results, implementing ListData interface +type ( + searchResults Model + UnitSearchYieldFunc func(index int, item string) (ok bool) +) -func (v *SearchResults) List() List { - return List{v} -} - -func (l *SearchResults) Iterate(yield UnitSearchYieldFunc) { - index := 0 - for _, name := range sointu.UnitNames { - if !strings.HasPrefix(name, l.d.UnitSearchString) { - continue - } - if !yield(index, name) { - break - } - index++ +func (m *Model) SearchResults() List { return List{(*searchResults)(m)} } +func (l *Model) SearchResult(i int) (name string, ok bool) { + if i < 0 || i >= len(l.derived.searchResults) { + return "", false } + return l.derived.searchResults[i], true } -func (l *SearchResults) Item(index int) (name string, ok bool) { - for i, n := range l.Iterate { - if i == index { - return n, true - } - } - return "", false -} - -func (l *SearchResults) Selected() int { - return max(min(l.d.UnitSearchIndex, l.Count()-1), 0) -} - -func (l *SearchResults) Selected2() int { - return max(min(l.d.UnitSearchIndex, l.Count()-1), 0) -} - -func (l *SearchResults) SetSelected(value int) { - l.d.UnitSearchIndex = max(min(value, l.Count()-1), 0) -} - -func (l *SearchResults) SetSelected2(value int) { -} - -func (l *SearchResults) Count() (count int) { - for _, n := range sointu.UnitNames { - if strings.HasPrefix(n, l.d.UnitSearchString) { - count++ - } - } - return -} +func (l *searchResults) Selected() int { return l.d.UnitSearchIndex } +func (l *searchResults) Selected2() int { return l.d.UnitSearchIndex } +func (l *searchResults) SetSelected(value int) { l.d.UnitSearchIndex = value } +func (l *searchResults) SetSelected2(value int) {} +func (l *searchResults) Count() (count int) { return len(l.derived.searchResults) } func (r Range) Len() int { return r.End - r.Start } diff --git a/tracker/model.go b/tracker/model.go index b26438a..fe4b149 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -216,6 +216,8 @@ func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext m.updateDeriveData(SongChange) m.presets.load() m.updateDerivedPresetSearch() + m.derived.searchResults = make([]string, 0, len(sointu.UnitNames)) + m.updateDerivedUnitSearch() return m } diff --git a/tracker/model_test.go b/tracker/model_test.go index e8ea8e1..281027c 100644 --- a/tracker/model_test.go +++ b/tracker/model_test.go @@ -44,12 +44,12 @@ func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T) s.IterateInt("Step", s.model.Step(), yield, seed) s.IterateInt("Octave", s.model.Octave(), yield, seed) // Lists - s.IterateList("Instruments", s.model.Instruments().List(), yield, seed) - s.IterateList("Units", s.model.Units().List(), yield, seed) - s.IterateList("Tracks", s.model.Tracks().List(), yield, seed) - s.IterateList("OrderRows", s.model.OrderRows().List(), yield, seed) - s.IterateList("NoteRows", s.model.NoteRows().List(), yield, seed) - s.IterateList("UnitSearchResults", s.model.SearchResults().List(), yield, seed) + s.IterateList("Instruments", s.model.Instruments(), yield, seed) + s.IterateList("Units", s.model.Units(), yield, seed) + s.IterateList("Tracks", s.model.Tracks(), yield, seed) + s.IterateList("OrderRows", s.model.OrderRows(), yield, seed) + s.IterateList("NoteRows", s.model.NoteRows(), yield, seed) + s.IterateList("UnitSearchResults", s.model.SearchResults(), yield, seed) s.IterateList("PresetDirs", s.model.PresetDirList().List(), yield, seed) s.IterateList("PresetResults", s.model.PresetResultList().List(), yield, seed) // Bools diff --git a/tracker/string.go b/tracker/string.go index 55a8b42..0198b30 100644 --- a/tracker/string.go +++ b/tracker/string.go @@ -1,5 +1,11 @@ package tracker +import ( + "strings" + + "github.com/vsariola/sointu" +) + type ( String struct { value StringValue @@ -67,8 +73,18 @@ func (v *UnitSearch) Value() string { func (v *UnitSearch) SetValue(value string) bool { v.d.UnitSearchString = value v.d.UnitSearching = true + (*Model)(v).updateDerivedUnitSearch() return true } +func (v *Model) updateDerivedUnitSearch() { + // update search results based on current search string + v.derived.searchResults = v.derived.searchResults[:0] + for _, name := range sointu.UnitNames { + if strings.HasPrefix(name, v.UnitSearch().Value()) { + v.derived.searchResults = append(v.derived.searchResults, name) + } + } +} // InstrumentNameString