This commit is contained in:
qm210 2024-11-10 22:30:17 +00:00 committed by GitHub
commit baea1e0a46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 503 additions and 296 deletions

View File

@ -35,6 +35,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
the command line tools. the command line tools.
- If a parameter is controlled by a `send`, the slider is now colored differently - If a parameter is controlled by a `send`, the slider is now colored differently
and there's a tooltip over the value to see where it comes from and its amount and there's a tooltip over the value to see where it comes from and its amount
([#176][p176])
- If a parameter has an invalid value (for now only `port` of a `send`),
value is printed grey ([#176][p176])
- "Multi-Unit View" to see all units as column next to each other ([#173][i173])
### Fixed ### Fixed
- We try to honor the MIDI event time stamps, so that the timing between MIDI - We try to honor the MIDI event time stamps, so that the timing between MIDI
@ -75,6 +79,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
matches the compiled output better, as usually compiled intros output sound in matches the compiled output better, as usually compiled intros output sound in
floating point. This might be important if OS sound drivers apply some audio floating point. This might be important if OS sound drivers apply some audio
enhancemenets e.g. compressors to the audio. enhancemenets e.g. compressors to the audio.
- Performance improvement: derived model that is useful for the UI is cached
on each score/patch change instead of evaluated on each draw ([#176][p176])
## [0.4.1] ## [0.4.1]
### Added ### Added

View File

@ -11,21 +11,22 @@ type (
setValue(bool) setValue(bool)
} }
Panic Model Panic Model
IsRecording Model IsRecording Model
Playing Model Playing Model
InstrEnlarged Model InstrEnlarged Model
Effect Model Effect Model
TrackMidiIn Model TrackMidiIn Model
CommentExpanded Model CommentExpanded Model
Follow Model Follow Model
UnitSearching Model UnitSearching Model
UnitDisabled Model UnitDisabled Model
LoopToggle Model LoopToggle Model
UniquePatterns Model UniquePatterns Model
Mute Model Mute Model
Solo Model Solo Model
LinkInstrTrack Model LinkInstrTrack Model
EnableMultiUnits Model
) )
func (v Bool) Toggle() { func (v Bool) Toggle() {
@ -40,21 +41,22 @@ func (v Bool) Set(value bool) {
// Model methods // Model methods
func (m *Model) Panic() *Panic { return (*Panic)(m) } func (m *Model) Panic() *Panic { return (*Panic)(m) }
func (m *Model) IsRecording() *IsRecording { return (*IsRecording)(m) } func (m *Model) IsRecording() *IsRecording { return (*IsRecording)(m) }
func (m *Model) Playing() *Playing { return (*Playing)(m) } func (m *Model) Playing() *Playing { return (*Playing)(m) }
func (m *Model) InstrEnlarged() *InstrEnlarged { return (*InstrEnlarged)(m) } func (m *Model) InstrEnlarged() *InstrEnlarged { return (*InstrEnlarged)(m) }
func (m *Model) Effect() *Effect { return (*Effect)(m) } func (m *Model) Effect() *Effect { return (*Effect)(m) }
func (m *Model) TrackMidiIn() *TrackMidiIn { return (*TrackMidiIn)(m) } func (m *Model) TrackMidiIn() *TrackMidiIn { return (*TrackMidiIn)(m) }
func (m *Model) CommentExpanded() *CommentExpanded { return (*CommentExpanded)(m) } func (m *Model) CommentExpanded() *CommentExpanded { return (*CommentExpanded)(m) }
func (m *Model) Follow() *Follow { return (*Follow)(m) } 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) } 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,46 +25,61 @@ 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 {
ret := &UnitEditor{ ret := &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()),
CopyUnitBtn: new(TipClickable), MultiUnitsBtn: NewBoolClickable(m.EnableMultiUnits().Bool()),
SelectTypeBtn: new(widget.Clickable), CopyUnitBtn: new(TipClickable),
commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}), SelectTypeBtn: new(widget.Clickable),
sliderList: NewDragList(m.Params().List(), layout.Vertical), commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}),
searchList: NewDragList(m.SearchResults().List(), layout.Vertical), sliderColumns: NewDragList(m.Units().List(), layout.Horizontal),
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

@ -66,7 +66,8 @@ type (
// when linkInstrTrack is false, editing an instrument does not change // when linkInstrTrack is false, editing an instrument does not change
// 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,55 +120,88 @@ 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)
unitType, ok := sointu.UnitTypes[unit.Type] }
if !ok {
return func (pl *Params) IterateInUnit(unitIndex int) func(yield ParamYieldFunc) {
} return func(yield ParamYieldFunc) {
for i := range unitType { if pl.d.InstrIndex < 0 || pl.d.InstrIndex >= len(pl.d.Song.Patch) {
if !unitType[i].CanSet {
continue
}
if unit.Type == "oscillator" && unit.Parameters["type"] != sointu.Sample && i >= 11 {
break // don't show the sample related params unless necessary
}
if !yield(NamedParameter{
parameter: parameter{m: (*Model)(pl), unit: unit},
up: &unitType[i],
}) {
return return
} }
} unit := &pl.d.Song.Patch[pl.d.InstrIndex].Units[unitIndex]
if unit.Type == "oscillator" && unit.Parameters["type"] == sointu.Sample { unitType, ok := sointu.UnitTypes[unit.Type]
if !yield(GmDlsEntryParameter{parameter: parameter{m: (*Model)(pl), unit: unit}}) { if !ok {
return return
} }
} for i := range unitType {
switch { if !unitType[i].CanSet {
case unit.Type == "delay": continue
if unit.Parameters["stereo"] == 1 && len(unit.VarArgs)%2 == 1 { }
unit.VarArgs = append(unit.VarArgs, 1) if unit.Type == "oscillator" && unit.Parameters["type"] != sointu.Sample && i >= 11 {
} break // don't show the sample related params unless necessary
if !yield(ReverbParameter{parameter: parameter{m: (*Model)(pl), unit: unit}}) { }
return if !yield(NamedParameter{
} parameter: parameter{m: (*Model)(pl), unit: unit},
if !yield(DelayLinesParameter{parameter: parameter{m: (*Model)(pl), unit: unit}}) { up: &unitType[i],
return }) {
}
for i := range unit.VarArgs {
if !yield(DelayTimeParameter{parameter: parameter{m: (*Model)(pl), unit: unit}, index: i}) {
return return
} }
} }
if unit.Type == "oscillator" && unit.Parameters["type"] == sointu.Sample {
if !yield(GmDlsEntryParameter{parameter: parameter{m: (*Model)(pl), unit: unit}}) {
return
}
}
switch {
case unit.Type == "delay":
if unit.Parameters["stereo"] == 1 && len(unit.VarArgs)%2 == 1 {
unit.VarArgs = append(unit.VarArgs, 1)
}
if !yield(ReverbParameter{parameter: parameter{m: (*Model)(pl), unit: unit}}) {
return
}
if !yield(DelayLinesParameter{parameter: parameter{m: (*Model)(pl), unit: unit}}) {
return
}
for i := range unit.VarArgs {
if !yield(DelayTimeParameter{parameter: parameter{m: (*Model)(pl), unit: unit}, index: i}) {
return
}
}
}
} }
} }
// 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
func (p NamedParameter) Name() string { return p.up.Name } func (p NamedParameter) Name() string { return p.up.Name }

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]
}