feat: first draft of multi-unit view

This commit is contained in:
qm210 2024-11-10 23:23:06 +01:00
parent 8d71cf3ca7
commit 5be1743204
14 changed files with 497 additions and 296 deletions

View File

@ -26,6 +26,7 @@ type (
Mute Model Mute Model
Solo Model Solo Model
LinkInstrTrack Model LinkInstrTrack Model
EnableMultiUnits Model
) )
func (v Bool) Toggle() { func (v Bool) Toggle() {
@ -55,6 +56,7 @@ func (m *Model) UniquePatterns() *UniquePatterns { return (*UniquePatterns)(m)
func (m *Model) Mute() *Mute { return (*Mute)(m) } func (m *Model) Mute() *Mute { return (*Mute)(m) }
func (m *Model) Solo() *Solo { return (*Solo)(m) } func (m *Model) Solo() *Solo { return (*Solo)(m) }
func (m *Model) LinkInstrTrack() *LinkInstrTrack { return (*LinkInstrTrack)(m) } func (m *Model) LinkInstrTrack() *LinkInstrTrack { return (*LinkInstrTrack)(m) }
func (m *Model) EnableMultiUnits() *EnableMultiUnits { return (*EnableMultiUnits)(m) }
// Panic methods // Panic methods
@ -267,3 +269,10 @@ func (m *LinkInstrTrack) Bool() Bool { return Bool{m} }
func (m *LinkInstrTrack) Value() bool { return m.linkInstrTrack } func (m *LinkInstrTrack) Value() bool { return m.linkInstrTrack }
func (m *LinkInstrTrack) setValue(val bool) { m.linkInstrTrack = val } func (m *LinkInstrTrack) setValue(val bool) { m.linkInstrTrack = val }
func (m *LinkInstrTrack) Enabled() bool { return true } func (m *LinkInstrTrack) Enabled() bool { return true }
// EnableMultiUnits methods
func (m *EnableMultiUnits) Bool() Bool { return Bool{m} }
func (m *EnableMultiUnits) Value() bool { return m.enableMultiUnits }
func (m *EnableMultiUnits) setValue(val bool) { m.enableMultiUnits = val }
func (m *EnableMultiUnits) Enabled() bool { return true }

View File

@ -168,6 +168,9 @@ type Clickable struct {
history []widget.Press history []widget.Press
requestClicks int requestClicks int
// optional callback for custom interactions
OnClick func()
} }
// Click executes a simple programmatic click. // Click executes a simple programmatic click.
@ -177,7 +180,11 @@ func (b *Clickable) Click() {
// Clicked calls Update and reports whether a click was registered. // Clicked calls Update and reports whether a click was registered.
func (b *Clickable) Clicked(gtx layout.Context) bool { func (b *Clickable) Clicked(gtx layout.Context) bool {
return b.clicked(b, gtx) clicked := b.clicked(b, gtx)
if clicked && b.OnClick != nil {
b.OnClick()
}
return clicked
} }
func (b *Clickable) clicked(t event.Tag, gtx layout.Context) bool { func (b *Clickable) clicked(t event.Tag, gtx layout.Context) bool {

View File

@ -31,6 +31,7 @@ type DragList struct {
swapped bool swapped bool
focused bool focused bool
requestFocus bool requestFocus bool
onSelect func(index int)
} }
type FilledDragListStyle struct { type FilledDragListStyle struct {
@ -185,6 +186,9 @@ func (s FilledDragListStyle) Layout(gtx C) D {
if !e.Modifiers.Contain(key.ModShift) { if !e.Modifiers.Contain(key.ModShift) {
s.dragList.TrackerList.SetSelected2(index) s.dragList.TrackerList.SetSelected2(index)
} }
if s.dragList.onSelect != nil {
s.dragList.onSelect(index)
}
gtx.Execute(key.FocusCmd{Tag: s.dragList}) gtx.Execute(key.FocusCmd{Tag: s.dragList})
} }
} }

View File

@ -105,6 +105,11 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
ret.linkDisabledHint = makeHint("Instrument-Track\nlinking disabled", "\n(%s)", "LinkInstrTrackToggle") ret.linkDisabledHint = makeHint("Instrument-Track\nlinking disabled", "\n(%s)", "LinkInstrTrackToggle")
ret.linkEnabledHint = makeHint("Instrument-Track\nlinking enabled", "\n(%s)", "LinkInstrTrackToggle") ret.linkEnabledHint = makeHint("Instrument-Track\nlinking enabled", "\n(%s)", "LinkInstrTrackToggle")
ret.splitInstrumentHint = makeHint("Split instrument", " (%s)", "SplitInstrument") ret.splitInstrumentHint = makeHint("Split instrument", " (%s)", "SplitInstrument")
ret.unitDragList.onSelect = func(index int) {
if model.EnableMultiUnits().Value() {
ret.unitEditor.ScrollToUnit(index)
}
}
return ret return ret
} }
@ -117,7 +122,7 @@ func (ie *InstrumentEditor) Focused() bool {
} }
func (ie *InstrumentEditor) childFocused(gtx C) bool { func (ie *InstrumentEditor) childFocused(gtx C) bool {
return ie.unitEditor.sliderList.Focused() || return ie.unitEditor.sliderColumns.Focused() ||
ie.instrumentDragList.Focused() || gtx.Source.Focused(ie.commentEditor) || gtx.Source.Focused(ie.nameEditor) || gtx.Source.Focused(ie.searchEditor) || ie.instrumentDragList.Focused() || gtx.Source.Focused(ie.commentEditor) || gtx.Source.Focused(ie.nameEditor) || gtx.Source.Focused(ie.searchEditor) ||
gtx.Source.Focused(ie.addUnitBtn.Clickable) || gtx.Source.Focused(ie.commentExpandBtn.Clickable) || gtx.Source.Focused(ie.presetMenuBtn.Clickable) || gtx.Source.Focused(ie.addUnitBtn.Clickable) || gtx.Source.Focused(ie.commentExpandBtn.Clickable) || gtx.Source.Focused(ie.presetMenuBtn.Clickable) ||
gtx.Source.Focused(ie.deleteInstrumentBtn.Clickable) || gtx.Source.Focused(ie.copyInstrumentBtn.Clickable) gtx.Source.Focused(ie.deleteInstrumentBtn.Clickable) || gtx.Source.Focused(ie.copyInstrumentBtn.Clickable)
@ -277,7 +282,7 @@ func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D {
element := func(gtx C, i int) D { element := func(gtx C, i int) D {
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36)) gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(30)) gtx.Constraints.Min.X = gtx.Dp(unit.Dp(30))
grabhandle := LabelStyle{Text: strconv.Itoa(i + 1), ShadeColor: black, Color: mediumEmphasisTextColor, FontSize: unit.Sp(10), Alignment: layout.Center, Shaper: t.Theme.Shaper} grabhandle := LabelStyle{Text: strconv.Itoa(i + 1), ShadeColor: black, Color: mediumEmphasisTextColor, FontSize: unit.Sp(10), Direction: layout.Center, Shaper: t.Theme.Shaper}
label := func(gtx C) D { label := func(gtx C) D {
name, level, mute, ok := (*tracker.Instruments)(t.Model).Item(i) name, level, mute, ok := (*tracker.Instruments)(t.Model).Item(i)
if !ok { if !ok {
@ -375,9 +380,10 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
addUnitBtnStyle.IconButtonStyle.Inset = layout.UniformInset(unit.Dp(4)) addUnitBtnStyle.IconButtonStyle.Inset = layout.UniformInset(unit.Dp(4))
var units [256]tracker.UnitListItem var units [256]tracker.UnitListItem
for i, item := range (*tracker.Units)(t.Model).Iterate { for i, item := range t.Model.Units().Iterate {
if i >= 256 { if i >= 256 {
break break
} }
units[i] = item units[i] = item
} }
@ -386,7 +392,7 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
if ie.searchEditor.requestFocus { if ie.searchEditor.requestFocus {
// for now, only the searchEditor has its requestFocus flag // for now, only the searchEditor has its requestFocus flag
ie.searchEditor.requestFocus = false ie.searchEditor.requestFocus = false
gtx.Execute(key.FocusCmd{Tag: &ie.searchEditor.Editor}) gtx.Execute(key.FocusCmd{Tag: ie.searchEditor.Editor})
} }
element := func(gtx C, i int) D { element := func(gtx C, i int) D {
@ -486,7 +492,7 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
case key.NameEscape: case key.NameEscape:
ie.instrumentDragList.Focus() ie.instrumentDragList.Focus()
case key.NameRightArrow: case key.NameRightArrow:
ie.unitEditor.sliderList.Focus() ie.unitEditor.sliderColumns.Focus()
case key.NameDeleteBackward: case key.NameDeleteBackward:
t.Units().SetSelectedType("") t.Units().SetSelectedType("")
t.UnitSearching().Bool().Set(true) t.UnitSearching().Bool().Set(true)

View File

@ -285,12 +285,12 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
case "FocusPrev": case "FocusPrev":
switch { switch {
case t.OrderEditor.scrollTable.Focused(): case t.OrderEditor.scrollTable.Focused():
t.InstrumentEditor.unitEditor.sliderList.Focus() t.InstrumentEditor.unitEditor.sliderColumns.Focus()
case t.TrackEditor.scrollTable.Focused(): case t.TrackEditor.scrollTable.Focused():
t.OrderEditor.scrollTable.Focus() t.OrderEditor.scrollTable.Focus()
case t.InstrumentEditor.Focused(): case t.InstrumentEditor.Focused():
if t.InstrumentEditor.enlargeBtn.Bool.Value() { if t.InstrumentEditor.enlargeBtn.Bool.Value() {
t.InstrumentEditor.unitEditor.sliderList.Focus() t.InstrumentEditor.unitEditor.sliderColumns.Focus()
} else { } else {
t.TrackEditor.scrollTable.Focus() t.TrackEditor.scrollTable.Focus()
} }
@ -304,7 +304,7 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
case t.TrackEditor.scrollTable.Focused(): case t.TrackEditor.scrollTable.Focused():
t.InstrumentEditor.Focus() t.InstrumentEditor.Focus()
case t.InstrumentEditor.Focused(): case t.InstrumentEditor.Focused():
t.InstrumentEditor.unitEditor.sliderList.Focus() t.InstrumentEditor.unitEditor.sliderColumns.Focus()
default: default:
if t.InstrumentEditor.enlargeBtn.Bool.Value() { if t.InstrumentEditor.enlargeBtn.Bool.Value() {
t.InstrumentEditor.Focus() t.InstrumentEditor.Focus()

View File

@ -17,14 +17,14 @@ type LabelStyle struct {
Text string Text string
Color color.NRGBA Color color.NRGBA
ShadeColor color.NRGBA ShadeColor color.NRGBA
Alignment layout.Direction Direction layout.Direction
Font font.Font Font font.Font
FontSize unit.Sp FontSize unit.Sp
Shaper *text.Shaper Shaper *text.Shaper
} }
func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions { func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
return l.Alignment.Layout(gtx, func(gtx C) D { return l.Direction.Layout(gtx, func(gtx C) D {
gtx.Constraints.Min = image.Point{} gtx.Constraints.Min = image.Point{}
paint.ColorOp{Color: l.ShadeColor}.Add(gtx.Ops) paint.ColorOp{Color: l.ShadeColor}.Add(gtx.Ops)
offs := op.Offset(image.Pt(2, 2)).Push(gtx.Ops) offs := op.Offset(image.Pt(2, 2)).Push(gtx.Ops)
@ -46,5 +46,13 @@ func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
} }
func Label(str string, color color.NRGBA, shaper *text.Shaper) layout.Widget { func Label(str string, color color.NRGBA, shaper *text.Shaper) layout.Widget {
return LabelStyle{Text: str, Color: color, ShadeColor: black, Font: labelDefaultFont, FontSize: labelDefaultFontSize, Alignment: layout.W, Shaper: shaper}.Layout return LabelStyle{
Text: str,
Color: color,
ShadeColor: black,
Font: labelDefaultFont,
FontSize: labelDefaultFontSize,
Direction: layout.W,
Shaper: shaper,
}.Layout
} }

View File

@ -223,7 +223,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
h := gtx.Dp(trackColTitleHeight) h := gtx.Dp(trackColTitleHeight)
gtx.Constraints = layout.Exact(image.Pt(pxWidth, h)) gtx.Constraints = layout.Exact(image.Pt(pxWidth, h))
LabelStyle{ LabelStyle{
Alignment: layout.N, Direction: layout.N,
Text: t.Model.TrackTitle(i), Text: t.Model.TrackTitle(i),
FontSize: unit.Sp(12), FontSize: unit.Sp(12),
Color: mediumEmphasisTextColor, Color: mediumEmphasisTextColor,

View File

@ -69,7 +69,7 @@ func (oe *OrderEditor) Layout(gtx C, t *Tracker) D {
defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: 0, Y: float32(h)})).Push(gtx.Ops).Pop() defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: 0, Y: float32(h)})).Push(gtx.Ops).Pop()
gtx.Constraints = layout.Exact(image.Pt(1e6, 1e6)) gtx.Constraints = layout.Exact(image.Pt(1e6, 1e6))
LabelStyle{ LabelStyle{
Alignment: layout.NW, Direction: layout.NW,
Text: t.Model.TrackTitle(i), Text: t.Model.TrackTitle(i),
FontSize: unit.Sp(12), FontSize: unit.Sp(12),
Color: mediumEmphasisTextColor, Color: mediumEmphasisTextColor,

View File

@ -55,7 +55,7 @@ func (a *PopupAlert) Layout(gtx C) D {
}.Op()) }.Op())
return D{Size: gtx.Constraints.Min} return D{Size: gtx.Constraints.Min}
} }
labelStyle := LabelStyle{Text: alert.Message, Color: textColor, ShadeColor: shadeColor, Font: labelDefaultFont, Alignment: layout.Center, FontSize: unit.Sp(16), Shaper: a.shaper} labelStyle := LabelStyle{Text: alert.Message, Color: textColor, ShadeColor: shadeColor, Font: labelDefaultFont, Direction: layout.Center, FontSize: unit.Sp(16), Shaper: a.shaper}
alertMargin.Layout(gtx, func(gtx C) D { alertMargin.Layout(gtx, func(gtx C) D {
return layout.S.Layout(gtx, func(gtx C) D { return layout.S.Layout(gtx, func(gtx C) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()

View File

@ -25,20 +25,26 @@ import (
) )
type UnitEditor struct { type UnitEditor struct {
sliderList *DragList sliderRows []*DragList
sliderColumns *DragList
searchList *DragList searchList *DragList
Parameters []*ParameterWidget Parameters [][]*ParameterWidget
DeleteUnitBtn *ActionClickable DeleteUnitBtn *ActionClickable
CopyUnitBtn *TipClickable CopyUnitBtn *TipClickable
ClearUnitBtn *ActionClickable ClearUnitBtn *ActionClickable
DisableUnitBtn *BoolClickable DisableUnitBtn *BoolClickable
SelectTypeBtn *widget.Clickable SelectTypeBtn *widget.Clickable
MultiUnitsBtn *BoolClickable
commentEditor *Editor commentEditor *Editor
caser cases.Caser caser cases.Caser
copyHint string copyHint string
disableUnitHint string disableUnitHint string
enableUnitHint string enableUnitHint string
multiUnitsHint string
totalWidthForUnit map[int]int
paramWidthForUnit map[int]int
} }
func NewUnitEditor(m *tracker.Model) *UnitEditor { func NewUnitEditor(m *tracker.Model) *UnitEditor {
@ -46,25 +52,34 @@ func NewUnitEditor(m *tracker.Model) *UnitEditor {
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()), DeleteUnitBtn: NewActionClickable(m.DeleteUnit()),
ClearUnitBtn: NewActionClickable(m.ClearUnit()), ClearUnitBtn: NewActionClickable(m.ClearUnit()),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()), DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
MultiUnitsBtn: NewBoolClickable(m.EnableMultiUnits().Bool()),
CopyUnitBtn: new(TipClickable), CopyUnitBtn: new(TipClickable),
SelectTypeBtn: new(widget.Clickable), SelectTypeBtn: new(widget.Clickable),
commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}), commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}),
sliderList: NewDragList(m.Params().List(), layout.Vertical), sliderColumns: NewDragList(m.Units().List(), layout.Horizontal),
searchList: NewDragList(m.SearchResults().List(), layout.Vertical), searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
totalWidthForUnit: make(map[int]int),
paramWidthForUnit: make(map[int]int),
} }
ret.caser = cases.Title(language.English) ret.caser = cases.Title(language.English)
ret.copyHint = makeHint("Copy unit", " (%s)", "Copy") ret.copyHint = makeHint("Copy unit", " (%s)", "Copy")
ret.disableUnitHint = makeHint("Disable unit", " (%s)", "UnitDisabledToggle") ret.disableUnitHint = makeHint("Disable unit", " (%s)", "UnitDisabledToggle")
ret.enableUnitHint = makeHint("Enable unit", " (%s)", "UnitDisabledToggle") ret.enableUnitHint = makeHint("Enable unit", " (%s)", "UnitDisabledToggle")
ret.multiUnitsHint = "Toggle Multi-Unit View"
ret.MultiUnitsBtn.Clickable.OnClick = func() {
ret.ScrollToUnit(m.Units().Selected())
}
return ret return ret
} }
func (pe *UnitEditor) Layout(gtx C, t *Tracker) D { func (pe *UnitEditor) Layout(gtx C, t *Tracker) D {
for { for {
e, ok := gtx.Event( e, ok := gtx.Event(
key.Filter{Focus: pe.sliderList, Name: key.NameLeftArrow, Optional: key.ModShift}, key.Filter{Focus: pe.sliderColumns, Name: key.NameLeftArrow, Optional: key.ModShift},
key.Filter{Focus: pe.sliderList, Name: key.NameRightArrow, Optional: key.ModShift}, key.Filter{Focus: pe.sliderColumns, Name: key.NameRightArrow, Optional: key.ModShift},
key.Filter{Focus: pe.sliderList, Name: key.NameEscape}, key.Filter{Focus: pe.sliderColumns, Name: key.NameEscape},
) )
if !ok { if !ok {
break break
@ -78,9 +93,9 @@ func (pe *UnitEditor) Layout(gtx C, t *Tracker) D {
} }
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
editorFunc := pe.layoutSliders editorFunc := pe.layoutColumns
if t.UnitSearching().Value() || pe.sliderList.TrackerList.Count() == 0 { if t.UnitSearching().Value() || pe.sliderColumns.TrackerList.Count() == 0 {
editorFunc = pe.layoutUnitTypeChooser editorFunc = pe.layoutUnitTypeChooser
} }
return Surface{Gray: 24, Focus: t.InstrumentEditor.wasFocused}.Layout(gtx, func(gtx C) D { return Surface{Gray: 24, Focus: t.InstrumentEditor.wasFocused}.Layout(gtx, func(gtx C) D {
@ -95,35 +110,85 @@ func (pe *UnitEditor) Layout(gtx C, t *Tracker) D {
}) })
} }
func (pe *UnitEditor) layoutSliders(gtx C, t *Tracker) D { func (pe *UnitEditor) layoutColumns(gtx C, t *Tracker) D {
numItems := pe.sliderList.TrackerList.Count() numUnits := pe.sliderColumns.TrackerList.Count()
for len(pe.Parameters) < numUnits {
for len(pe.Parameters) < numItems { pe.Parameters = append(pe.Parameters, []*ParameterWidget{})
pe.Parameters = append(pe.Parameters, new(ParameterWidget)) }
for u := len(pe.sliderRows); u < numUnits; u++ {
paramList := NewDragList(t.ParamsForUnit(u).List(), layout.Vertical)
pe.sliderRows = append(pe.sliderRows, paramList)
} }
index := 0 if !t.Model.EnableMultiUnits().Value() {
for param := range t.Model.Params().Iterate { return pe.layoutSliderColumn(gtx, t, t.Model.Units().Selected(), false)
pe.Parameters[index].Parameter = param
index++
} }
element := func(gtx C, index int) D {
if index < 0 || index >= numItems { column := func(gtx C, index int) D {
if index < 0 || index > numUnits {
return D{} return D{}
} }
paramStyle := t.ParamStyle(t.Theme, pe.Parameters[index]) dims := pe.layoutSliderColumn(gtx, t, index, true)
paramStyle.Focus = pe.sliderList.TrackerList.Selected() == index return D{Size: image.Pt(dims.Size.X, gtx.Constraints.Max.Y)}
dims := paramStyle.Layout(gtx)
return D{Size: image.Pt(gtx.Constraints.Max.X, dims.Size.Y)}
} }
fdl := FilledDragList(t.Theme, pe.sliderColumns, column, nil)
fdl := FilledDragList(t.Theme, pe.sliderList, element, nil)
dims := fdl.Layout(gtx) dims := fdl.Layout(gtx)
gtx.Constraints = layout.Exact(dims.Size) gtx.Constraints = layout.Exact(dims.Size)
fdl.LayoutScrollBar(gtx) fdl.LayoutScrollBar(gtx)
return dims return dims
} }
func (pe *UnitEditor) layoutSliderColumn(gtx C, t *Tracker, u int, multiUnits bool) D {
numParams := 0
for param := range t.Model.ParamsForUnit(u).Iterate {
for len(pe.Parameters[u]) < numParams+1 {
pe.Parameters[u] = append(pe.Parameters[u], new(ParameterWidget))
}
pe.Parameters[u][numParams].Parameter = param
numParams++
}
unitId := t.Model.Units().CurrentInstrumentUnitAt(u).ID
columnWidth := gtx.Constraints.Max.X
if multiUnits {
columnWidth = pe.totalWidthForUnit[unitId]
}
element := func(gtx C, index int) D {
if index < 0 || index >= numParams {
return D{}
}
paramStyle := t.ParamStyle(t.Theme, pe.Parameters[u][index])
paramStyle.Focus = pe.sliderRows[u].TrackerList.Selected() == index
dims := paramStyle.Layout(gtx, pe.paramWidthForUnit, unitId)
if multiUnits && pe.totalWidthForUnit[unitId] < dims.Size.X {
pe.totalWidthForUnit[unitId] = dims.Size.X
}
return D{Size: image.Pt(columnWidth, dims.Size.Y)}
}
fdl := FilledDragList(t.Theme, pe.sliderRows[u], element, nil)
var dims D
if multiUnits {
name := buildUnitName(t.Model.Units().CurrentInstrumentUnitAt(u))
dims = layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, fdl.Layout),
layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = columnWidth
gtx.Constraints.Min.Y = gtx.Sp(t.Theme.TextSize * 3)
return layout.Center.Layout(gtx, Label(name, primaryColor, t.Theme.Shaper))
}),
)
dims.Size.Y -= gtx.Dp(fdl.ScrollBarWidth)
} else {
dims = fdl.Layout(gtx)
}
gtx.Constraints = layout.Exact(dims.Size)
fdl.LayoutScrollBar(gtx)
return D{Size: image.Pt(columnWidth, dims.Size.Y)}
}
func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D { func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
for pe.CopyUnitBtn.Clickable.Clicked(gtx) { for pe.CopyUnitBtn.Clickable.Clicked(gtx) {
if contents, ok := t.Units().List().CopyElements(); ok { if contents, ok := t.Units().List().CopyElements(); ok {
@ -134,6 +199,7 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
copyUnitBtnStyle := TipIcon(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, pe.copyHint) copyUnitBtnStyle := TipIcon(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, pe.copyHint)
deleteUnitBtnStyle := ActionIcon(gtx, t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)") deleteUnitBtnStyle := ActionIcon(gtx, t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)")
disableUnitBtnStyle := ToggleIcon(gtx, t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, pe.disableUnitHint, pe.enableUnitHint) disableUnitBtnStyle := ToggleIcon(gtx, t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, pe.disableUnitHint, pe.enableUnitHint)
multiUnitsBtnStyle := ToggleIcon(gtx, t.Theme, pe.MultiUnitsBtn, icons.ActionViewWeek, icons.ActionViewWeek, pe.multiUnitsHint, pe.multiUnitsHint)
text := t.Units().SelectedType() text := t.Units().SelectedType()
if text == "" { if text == "" {
text = "Choose unit type" text = "Choose unit type"
@ -172,6 +238,7 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
s.Set(pe.commentEditor.Text()) s.Set(pe.commentEditor.Text())
return ret return ret
}), }),
layout.Rigid(multiUnitsBtnStyle.Layout),
) )
} }
@ -210,7 +277,7 @@ func (pe *UnitEditor) command(e key.Event, t *Tracker) {
if sel == nil { if sel == nil {
return return
} }
i := (&tracker.Int{IntData: sel}) i := &tracker.Int{IntData: sel}
if e.Modifiers.Contain(key.ModShift) { if e.Modifiers.Contain(key.ModShift) {
i.Set(i.Value() - sel.LargeStep()) i.Set(i.Value() - sel.LargeStep())
} else { } else {
@ -221,7 +288,7 @@ func (pe *UnitEditor) command(e key.Event, t *Tracker) {
if sel == nil { if sel == nil {
return return
} }
i := (&tracker.Int{IntData: sel}) i := &tracker.Int{IntData: sel}
if e.Modifiers.Contain(key.ModShift) { if e.Modifiers.Contain(key.ModShift) {
i.Set(i.Value() + sel.LargeStep()) i.Set(i.Value() + sel.LargeStep())
} else { } else {
@ -267,13 +334,25 @@ func (t *Tracker) ParamStyle(th *material.Theme, paramWidget *ParameterWidget) P
} }
} }
func (p ParameterStyle) Layout(gtx C) D { func spacer(px int) layout.FlexChild {
isSendTarget, info := p.tryDerivedParameterInfo() return layout.Rigid(func(gtx C) D {
return D{Size: image.Pt(px, px)}
})
}
func (p ParameterStyle) Layout(gtx C, paramWidthMap map[int]int, unitId int) D {
isSendTarget, info := p.tryDerivedParameterInfo(unitId)
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
spacer(24),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110)) dims := layout.E.Layout(gtx, Label(p.w.Parameter.Name(), white, p.tracker.Theme.Shaper))
return layout.E.Layout(gtx, Label(p.w.Parameter.Name(), white, p.tracker.Theme.Shaper)) if paramWidthMap[unitId] < dims.Size.X {
paramWidthMap[unitId] = dims.Size.X
}
dims.Size.X = paramWidthMap[unitId]
return dims
}), }),
spacer(8),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
switch p.w.Parameter.Type() { switch p.w.Parameter.Type() {
case tracker.IntegerParameter: case tracker.IntegerParameter:
@ -371,6 +450,7 @@ func (p ParameterStyle) Layout(gtx C) D {
} }
return D{} return D{}
}), }),
spacer(8),
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
if p.w.Parameter.Type() != tracker.IDParameter { if p.w.Parameter.Type() != tracker.IDParameter {
color := white color := white
@ -387,22 +467,31 @@ func (p ParameterStyle) Layout(gtx C) D {
} }
return D{} return D{}
}), }),
spacer(24),
) )
} }
func buildUnitLabel(index int, u sointu.Unit) string { func buildUnitLabel(index int, u sointu.Unit) string {
text := u.Type return fmt.Sprintf("%d: %s", index, buildUnitName(u))
if u.Comment != "" {
text = fmt.Sprintf("%s \"%s\"", text, u.Comment)
}
return fmt.Sprintf("%d: %s", index, text)
} }
func (p ParameterStyle) tryDerivedParameterInfo() (isSendTarget bool, sendInfo string) { func buildUnitName(u sointu.Unit) string {
if u.Comment != "" {
return fmt.Sprintf("%s \"%s\"", u.Type, u.Comment)
}
return u.Type
}
func (p ParameterStyle) tryDerivedParameterInfo(unitId int) (isSendTarget bool, sendInfo string) {
param, ok := (p.w.Parameter).(tracker.NamedParameter) param, ok := (p.w.Parameter).(tracker.NamedParameter)
if !ok { if !ok {
return false, "" return false, ""
} }
isSendTarget, sendInfo, _ = p.tracker.ParameterInfo(param.Unit().ID, param.Name()) isSendTarget, sendInfo, _ = p.tracker.ParameterInfo(unitId, param.Name())
return isSendTarget, sendInfo return isSendTarget, sendInfo
} }
func (pe *UnitEditor) ScrollToUnit(index int) {
pe.sliderColumns.List.Position.First = index
pe.sliderColumns.List.Position.Offset = 0
}

View File

@ -35,22 +35,12 @@ type (
unmarshal([]byte) (r Range, err error) unmarshal([]byte) (r Range, err error)
} }
UnitListItem struct {
Type, Comment string
Disabled bool
StackNeed, StackBefore, StackAfter int
}
// Range is used to represent a range [Start,End) of integers // Range is used to represent a range [Start,End) of integers
Range struct { Range struct {
Start, End int 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 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 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 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 NoteRows Model // NoteRows is a list of all the note rows, implementing ListData & MutableListData interfaces
@ -61,7 +51,6 @@ type (
// Model methods // Model methods
func (m *Model) Instruments() *Instruments { return (*Instruments)(m) } 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) Tracks() *Tracks { return (*Tracks)(m) }
func (m *Model) OrderRows() *OrderRows { return (*OrderRows)(m) } func (m *Model) OrderRows() *OrderRows { return (*OrderRows)(m) }
func (m *Model) NoteRows() *NoteRows { return (*NoteRows)(m) } func (m *Model) NoteRows() *NoteRows { return (*NoteRows)(m) }
@ -257,162 +246,6 @@ func (m *Instruments) unmarshal(data []byte) (r Range, err error) {
return r, nil return r, nil
} }
// Units methods
func (v *Units) List() List {
return List{v}
}
func (m *Units) SelectedType() string {
if m.d.InstrIndex < 0 ||
m.d.InstrIndex >= len(m.d.Song.Patch) ||
m.d.UnitIndex < 0 ||
m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
return ""
}
return m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Type
}
func (m *Units) SetSelectedType(t string) {
if m.d.InstrIndex < 0 ||
m.d.InstrIndex >= len(m.d.Song.Patch) {
return
}
if m.d.UnitIndex < 0 {
m.d.UnitIndex = 0
}
for len(m.d.Song.Patch[m.d.InstrIndex].Units) <= m.d.UnitIndex {
m.d.Song.Patch[m.d.InstrIndex].Units = append(m.d.Song.Patch[m.d.InstrIndex].Units, sointu.Unit{})
}
unit, ok := defaultUnits[t]
if !ok { // if the type is invalid, we just set it to empty unit
unit = sointu.Unit{Parameters: make(map[string]int)}
} else {
unit = unit.Copy()
}
oldUnit := m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex]
if oldUnit.Type == unit.Type {
return
}
defer 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) Iterate(yield UnitYieldFunc) {
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
return
}
stackBefore := 0
for i, unit := range v.d.Song.Patch[v.d.InstrIndex].Units {
stackAfter := stackBefore + unit.StackChange()
if !yield(i, UnitListItem{
Type: unit.Type,
Comment: unit.Comment,
Disabled: unit.Disabled,
StackNeed: unit.StackNeed(),
StackBefore: stackBefore,
StackAfter: stackAfter,
}) {
break
}
stackBefore = stackAfter
}
}
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)
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) {
return 0
}
return len(m.d.Song.Patch[(*Model)(v).d.InstrIndex].Units)
}
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
}
units := m.d.Song.Patch[m.d.InstrIndex].Units
for i, j := range r.Swaps(delta) {
units[i], units[j] = units[j], units[i]
}
return true
}
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
}
u := m.d.Song.Patch[m.d.InstrIndex].Units
m.d.Song.Patch[m.d.InstrIndex].Units = append(u[:r.Start], u[r.End:]...)
return true
}
func (v *Units) change(n string, severity ChangeSeverity) func() {
return (*Model)(v).change("UnitListView."+n, PatchChange, severity)
}
func (v *Units) cancel() {
(*Model)(v).changeCancel = true
}
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")
}
units := m.d.Song.Patch[m.d.InstrIndex].Units[r.Start:r.End]
ret, err := yaml.Marshal(struct{ Units []sointu.Unit }{units})
if err != nil {
return nil, fmt.Errorf("UnitListView.marshal: %v", err)
}
return ret, nil
}
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")
}
var pastedUnits struct{ Units []sointu.Unit }
if err := yaml.Unmarshal(data, &pastedUnits); err != nil {
return Range{}, fmt.Errorf("UnitListView.unmarshal: %v", err)
}
if len(pastedUnits.Units) == 0 {
return Range{}, errors.New("UnitListView.unmarshal: no units")
}
m.assignUnitIDs(pastedUnits.Units)
sel := v.Selected()
var ok bool
m.d.Song.Patch[m.d.InstrIndex].Units, ok = Insert(m.d.Song.Patch[m.d.InstrIndex].Units, sel, pastedUnits.Units...)
if !ok {
return Range{}, errors.New("UnitListView.unmarshal: insert failed")
}
return Range{sel, sel + len(pastedUnits.Units)}, nil
}
// Tracks methods // Tracks methods
func (v *Tracks) List() List { func (v *Tracks) List() List {

View File

@ -67,6 +67,7 @@ type (
// the track. when true, editing an instrument changes the tracks (e.g. // the track. when true, editing an instrument changes the tracks (e.g.
// reordering or deleting instrument can delete track) // reordering or deleting instrument can delete track)
linkInstrTrack bool linkInstrTrack bool
enableMultiUnits bool
voiceLevels [vm.MAX_VOICES]float32 voiceLevels [vm.MAX_VOICES]float32

View File

@ -41,6 +41,11 @@ type (
Params Model Params Model
ParamsForUnit struct {
*Params
unitIndex int
}
ParamYieldFunc func(param Parameter) bool ParamYieldFunc func(param Parameter) bool
ParameterType int ParameterType int
@ -61,6 +66,13 @@ const (
func (m *Model) Params() *Params { return (*Params)(m) } func (m *Model) Params() *Params { return (*Params)(m) }
func (m *Model) ParamsForUnit(u int) *ParamsForUnit {
return &ParamsForUnit{
Params: m.Params(),
unitIndex: u,
}
}
// parameter methods // parameter methods
func (p parameter) change(kind string) func() { func (p parameter) change(kind string) func() {
@ -80,14 +92,22 @@ func (pl *Params) change(n string, severity ChangeSeverity) func() {
return (*Model)(pl).change("ParamList."+n, PatchChange, severity) return (*Model)(pl).change("ParamList."+n, PatchChange, severity)
} }
func (pl *Params) Count() int { func count(iterator func(yield ParamYieldFunc)) int {
count := 0 count := 0
for range pl.Iterate { for _ = range iterator {
count++ count++
} }
return count return count
} }
func (pl *Params) Count() int {
return count(pl.Iterate)
}
func (pl *Params) CountInUnit(unitIndex int) int {
return count(pl.IterateInUnit(unitIndex))
}
func (pl *Params) SelectedItem() (ret Parameter) { func (pl *Params) SelectedItem() (ret Parameter) {
index := pl.Selected() index := pl.Selected()
for param := range pl.Iterate { for param := range pl.Iterate {
@ -100,13 +120,18 @@ func (pl *Params) SelectedItem() (ret Parameter) {
} }
func (pl *Params) Iterate(yield ParamYieldFunc) { func (pl *Params) Iterate(yield ParamYieldFunc) {
if pl.d.InstrIndex < 0 || pl.d.InstrIndex >= len(pl.d.Song.Patch) {
return
}
if pl.d.UnitIndex < 0 || pl.d.UnitIndex >= len(pl.d.Song.Patch[pl.d.InstrIndex].Units) { if pl.d.UnitIndex < 0 || pl.d.UnitIndex >= len(pl.d.Song.Patch[pl.d.InstrIndex].Units) {
return return
} }
unit := &pl.d.Song.Patch[pl.d.InstrIndex].Units[pl.d.UnitIndex] pl.IterateInUnit(pl.d.UnitIndex)(yield)
}
func (pl *Params) IterateInUnit(unitIndex int) func(yield ParamYieldFunc) {
return func(yield ParamYieldFunc) {
if pl.d.InstrIndex < 0 || pl.d.InstrIndex >= len(pl.d.Song.Patch) {
return
}
unit := &pl.d.Song.Patch[pl.d.InstrIndex].Units[unitIndex]
unitType, ok := sointu.UnitTypes[unit.Type] unitType, ok := sointu.UnitTypes[unit.Type]
if !ok { if !ok {
return return
@ -148,6 +173,34 @@ func (pl *Params) Iterate(yield ParamYieldFunc) {
} }
} }
} }
}
// ParamsForUnit
func (pu *ParamsForUnit) List() List { return List{pu} }
func (pu *ParamsForUnit) Selected2() int { return pu.Selected() }
func (pu *ParamsForUnit) SetSelected2(int) {}
func (pu *ParamsForUnit) Selected() int {
if pu.unitIndex != pu.d.UnitIndex {
return -1
}
return pu.d.ParamIndex
}
func (pu *ParamsForUnit) SetSelected(value int) {
pu.d.ParamIndex = max(min(value, pu.Count()-1), 0)
pu.d.UnitIndex = pu.unitIndex
pu.d.UnitIndex2 = pu.unitIndex
}
func (pu *ParamsForUnit) Count() int {
return count(pu.Iterate)
}
func (pu *ParamsForUnit) Iterate(yield ParamYieldFunc) {
pu.Params.IterateInUnit(pu.unitIndex)(yield)
}
// NamedParameter // NamedParameter

191
tracker/units.go Normal file
View File

@ -0,0 +1,191 @@
package tracker
import (
"errors"
"fmt"
"github.com/vsariola/sointu"
"gopkg.in/yaml.v2"
)
type (
UnitListItem struct {
Type, Comment string
Disabled bool
StackNeed, StackBefore, StackAfter int
}
UnitYieldFunc func(index int, item UnitListItem) (ok bool)
UnitSearchYieldFunc func(index int, item string) (ok bool)
Units Model // Units is a list of all the units in the selected instrument, implementing ListData & MutableListData interfaces
)
// Model methods
func (m *Model) Units() *Units { return (*Units)(m) }
// Units methods
func (ul *Units) List() List {
return List{ul}
}
func (ul *Units) SelectedType() string {
if ul.d.InstrIndex < 0 ||
ul.d.InstrIndex >= len(ul.d.Song.Patch) ||
ul.d.UnitIndex < 0 ||
ul.d.UnitIndex >= len(ul.d.Song.Patch[ul.d.InstrIndex].Units) {
return ""
}
return ul.d.Song.Patch[ul.d.InstrIndex].Units[ul.d.UnitIndex].Type
}
func (ul *Units) SetSelectedType(t string) {
if ul.d.InstrIndex < 0 ||
ul.d.InstrIndex >= len(ul.d.Song.Patch) {
return
}
if ul.d.UnitIndex < 0 {
ul.d.UnitIndex = 0
}
for len(ul.d.Song.Patch[ul.d.InstrIndex].Units) <= ul.d.UnitIndex {
ul.d.Song.Patch[ul.d.InstrIndex].Units = append(ul.d.Song.Patch[ul.d.InstrIndex].Units, sointu.Unit{})
}
unit, ok := defaultUnits[t]
if !ok { // if the type is invalid, we just set it to empty unit
unit = sointu.Unit{Parameters: make(map[string]int)}
} else {
unit = unit.Copy()
}
oldUnit := ul.d.Song.Patch[ul.d.InstrIndex].Units[ul.d.UnitIndex]
if oldUnit.Type == unit.Type {
return
}
defer ul.change("SetSelectedType", MajorChange)()
ul.d.Song.Patch[ul.d.InstrIndex].Units[ul.d.UnitIndex] = unit
ul.d.Song.Patch[ul.d.InstrIndex].Units[ul.d.UnitIndex].ID = oldUnit.ID // keep the ID of the replaced unit
}
func (ul *Units) Iterate(yield UnitYieldFunc) {
if ul.d.InstrIndex < 0 || ul.d.InstrIndex >= len(ul.d.Song.Patch) {
return
}
stackBefore := 0
for i, unit := range ul.d.Song.Patch[ul.d.InstrIndex].Units {
stackAfter := stackBefore + unit.StackChange()
if !yield(i, UnitListItem{
Type: unit.Type,
Comment: unit.Comment,
Disabled: unit.Disabled,
StackNeed: unit.StackNeed(),
StackBefore: stackBefore,
StackAfter: stackAfter,
}) {
break
}
stackBefore = stackAfter
}
}
func (ul *Units) Selected() int {
return max(min(ul.d.UnitIndex, ul.Count()-1), 0)
}
func (ul *Units) Selected2() int {
return max(min(ul.d.UnitIndex2, ul.Count()-1), 0)
}
func (ul *Units) SetSelected(value int) {
m := (*Model)(ul)
m.d.UnitIndex = max(min(value, ul.Count()-1), 0)
m.d.ParamIndex = 0
m.d.UnitSearching = false
m.d.UnitSearchString = ""
}
func (ul *Units) SetSelected2(value int) {
(*Model)(ul).d.UnitIndex2 = max(min(value, ul.Count()-1), 0)
}
func (ul *Units) Count() int {
m := (*Model)(ul)
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return 0
}
return len(m.d.Song.Patch[(*Model)(ul).d.InstrIndex].Units)
}
func (ul *Units) move(r Range, delta int) (ok bool) {
m := (*Model)(ul)
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return false
}
units := m.d.Song.Patch[m.d.InstrIndex].Units
for i, j := range r.Swaps(delta) {
units[i], units[j] = units[j], units[i]
}
return true
}
func (ul *Units) delete(r Range) (ok bool) {
m := (*Model)(ul)
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return false
}
u := m.d.Song.Patch[m.d.InstrIndex].Units
m.d.Song.Patch[m.d.InstrIndex].Units = append(u[:r.Start], u[r.End:]...)
return true
}
func (ul *Units) change(n string, severity ChangeSeverity) func() {
return (*Model)(ul).change("UnitListView."+n, PatchChange, severity)
}
func (ul *Units) cancel() {
(*Model)(ul).changeCancel = true
}
func (ul *Units) marshal(r Range) ([]byte, error) {
m := (*Model)(ul)
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return nil, errors.New("UnitListView.marshal: no instruments")
}
units := m.d.Song.Patch[m.d.InstrIndex].Units[r.Start:r.End]
ret, err := yaml.Marshal(struct{ Units []sointu.Unit }{units})
if err != nil {
return nil, fmt.Errorf("UnitListView.marshal: %v", err)
}
return ret, nil
}
func (ul *Units) unmarshal(data []byte) (r Range, err error) {
m := (*Model)(ul)
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
return Range{}, errors.New("UnitListView.unmarshal: no instruments")
}
var pastedUnits struct{ Units []sointu.Unit }
if err := yaml.Unmarshal(data, &pastedUnits); err != nil {
return Range{}, fmt.Errorf("UnitListView.unmarshal: %v", err)
}
if len(pastedUnits.Units) == 0 {
return Range{}, errors.New("UnitListView.unmarshal: no units")
}
m.assignUnitIDs(pastedUnits.Units)
sel := ul.Selected()
var ok bool
m.d.Song.Patch[m.d.InstrIndex].Units, ok = Insert(m.d.Song.Patch[m.d.InstrIndex].Units, sel, pastedUnits.Units...)
if !ok {
return Range{}, errors.New("UnitListView.unmarshal: insert failed")
}
return Range{sel, sel + len(pastedUnits.Units)}, nil
}
func (ul *Units) CurrentInstrumentUnitAt(index int) sointu.Unit {
units := ul.d.Song.Patch[ul.d.InstrIndex].Units
if index < 0 || index >= len(units) {
return sointu.Unit{}
}
return units[index]
}