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.
- 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
([#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
- 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
floating point. This might be important if OS sound drivers apply some 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]
### Added

View File

@ -11,21 +11,22 @@ type (
setValue(bool)
}
Panic Model
IsRecording Model
Playing Model
InstrEnlarged Model
Effect Model
TrackMidiIn Model
CommentExpanded Model
Follow Model
UnitSearching Model
UnitDisabled Model
LoopToggle Model
UniquePatterns Model
Mute Model
Solo Model
LinkInstrTrack Model
Panic Model
IsRecording Model
Playing Model
InstrEnlarged Model
Effect Model
TrackMidiIn Model
CommentExpanded Model
Follow Model
UnitSearching Model
UnitDisabled Model
LoopToggle Model
UniquePatterns Model
Mute Model
Solo Model
LinkInstrTrack Model
EnableMultiUnits Model
)
func (v Bool) Toggle() {
@ -40,21 +41,22 @@ func (v Bool) Set(value bool) {
// Model methods
func (m *Model) Panic() *Panic { return (*Panic)(m) }
func (m *Model) IsRecording() *IsRecording { return (*IsRecording)(m) }
func (m *Model) Playing() *Playing { return (*Playing)(m) }
func (m *Model) InstrEnlarged() *InstrEnlarged { return (*InstrEnlarged)(m) }
func (m *Model) Effect() *Effect { return (*Effect)(m) }
func (m *Model) TrackMidiIn() *TrackMidiIn { return (*TrackMidiIn)(m) }
func (m *Model) CommentExpanded() *CommentExpanded { return (*CommentExpanded)(m) }
func (m *Model) Follow() *Follow { return (*Follow)(m) }
func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m) }
func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) }
func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) }
func (m *Model) UniquePatterns() *UniquePatterns { return (*UniquePatterns)(m) }
func (m *Model) Mute() *Mute { return (*Mute)(m) }
func (m *Model) Solo() *Solo { return (*Solo)(m) }
func (m *Model) LinkInstrTrack() *LinkInstrTrack { return (*LinkInstrTrack)(m) }
func (m *Model) Panic() *Panic { return (*Panic)(m) }
func (m *Model) IsRecording() *IsRecording { return (*IsRecording)(m) }
func (m *Model) Playing() *Playing { return (*Playing)(m) }
func (m *Model) InstrEnlarged() *InstrEnlarged { return (*InstrEnlarged)(m) }
func (m *Model) Effect() *Effect { return (*Effect)(m) }
func (m *Model) TrackMidiIn() *TrackMidiIn { return (*TrackMidiIn)(m) }
func (m *Model) CommentExpanded() *CommentExpanded { return (*CommentExpanded)(m) }
func (m *Model) Follow() *Follow { return (*Follow)(m) }
func (m *Model) UnitSearching() *UnitSearching { return (*UnitSearching)(m) }
func (m *Model) UnitDisabled() *UnitDisabled { return (*UnitDisabled)(m) }
func (m *Model) LoopToggle() *LoopToggle { return (*LoopToggle)(m) }
func (m *Model) UniquePatterns() *UniquePatterns { return (*UniquePatterns)(m) }
func (m *Model) Mute() *Mute { return (*Mute)(m) }
func (m *Model) Solo() *Solo { return (*Solo)(m) }
func (m *Model) LinkInstrTrack() *LinkInstrTrack { return (*LinkInstrTrack)(m) }
func (m *Model) EnableMultiUnits() *EnableMultiUnits { return (*EnableMultiUnits)(m) }
// 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) setValue(val bool) { m.linkInstrTrack = val }
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
requestClicks int
// optional callback for custom interactions
OnClick func()
}
// Click executes a simple programmatic click.
@ -177,7 +180,11 @@ func (b *Clickable) Click() {
// Clicked calls Update and reports whether a click was registered.
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 {

View File

@ -31,6 +31,7 @@ type DragList struct {
swapped bool
focused bool
requestFocus bool
onSelect func(index int)
}
type FilledDragListStyle struct {
@ -185,6 +186,9 @@ func (s FilledDragListStyle) Layout(gtx C) D {
if !e.Modifiers.Contain(key.ModShift) {
s.dragList.TrackerList.SetSelected2(index)
}
if s.dragList.onSelect != nil {
s.dragList.onSelect(index)
}
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.linkEnabledHint = makeHint("Instrument-Track\nlinking enabled", "\n(%s)", "LinkInstrTrackToggle")
ret.splitInstrumentHint = makeHint("Split instrument", " (%s)", "SplitInstrument")
ret.unitDragList.onSelect = func(index int) {
if model.EnableMultiUnits().Value() {
ret.unitEditor.ScrollToUnit(index)
}
}
return ret
}
@ -117,7 +122,7 @@ func (ie *InstrumentEditor) Focused() 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) ||
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)
@ -277,7 +282,7 @@ func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D {
element := func(gtx C, i int) D {
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
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 {
name, level, mute, ok := (*tracker.Instruments)(t.Model).Item(i)
if !ok {
@ -375,9 +380,10 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
addUnitBtnStyle.IconButtonStyle.Inset = layout.UniformInset(unit.Dp(4))
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 {
break
}
units[i] = item
}
@ -386,7 +392,7 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
if ie.searchEditor.requestFocus {
// for now, only the searchEditor has its requestFocus flag
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 {
@ -486,7 +492,7 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
case key.NameEscape:
ie.instrumentDragList.Focus()
case key.NameRightArrow:
ie.unitEditor.sliderList.Focus()
ie.unitEditor.sliderColumns.Focus()
case key.NameDeleteBackward:
t.Units().SetSelectedType("")
t.UnitSearching().Bool().Set(true)

View File

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

View File

@ -17,14 +17,14 @@ type LabelStyle struct {
Text string
Color color.NRGBA
ShadeColor color.NRGBA
Alignment layout.Direction
Direction layout.Direction
Font font.Font
FontSize unit.Sp
Shaper *text.Shaper
}
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{}
paint.ColorOp{Color: l.ShadeColor}.Add(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 {
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)
gtx.Constraints = layout.Exact(image.Pt(pxWidth, h))
LabelStyle{
Alignment: layout.N,
Direction: layout.N,
Text: t.Model.TrackTitle(i),
FontSize: unit.Sp(12),
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()
gtx.Constraints = layout.Exact(image.Pt(1e6, 1e6))
LabelStyle{
Alignment: layout.NW,
Direction: layout.NW,
Text: t.Model.TrackTitle(i),
FontSize: unit.Sp(12),
Color: mediumEmphasisTextColor,

View File

@ -55,7 +55,7 @@ func (a *PopupAlert) Layout(gtx C) D {
}.Op())
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 {
return layout.S.Layout(gtx, func(gtx C) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()

View File

@ -25,46 +25,61 @@ import (
)
type UnitEditor struct {
sliderList *DragList
sliderRows []*DragList
sliderColumns *DragList
searchList *DragList
Parameters []*ParameterWidget
Parameters [][]*ParameterWidget
DeleteUnitBtn *ActionClickable
CopyUnitBtn *TipClickable
ClearUnitBtn *ActionClickable
DisableUnitBtn *BoolClickable
SelectTypeBtn *widget.Clickable
MultiUnitsBtn *BoolClickable
commentEditor *Editor
caser cases.Caser
copyHint string
disableUnitHint string
enableUnitHint string
multiUnitsHint string
totalWidthForUnit map[int]int
paramWidthForUnit map[int]int
}
func NewUnitEditor(m *tracker.Model) *UnitEditor {
ret := &UnitEditor{
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()),
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
CopyUnitBtn: new(TipClickable),
SelectTypeBtn: new(widget.Clickable),
commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}),
sliderList: NewDragList(m.Params().List(), layout.Vertical),
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()),
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
DisableUnitBtn: NewBoolClickable(m.UnitDisabled().Bool()),
MultiUnitsBtn: NewBoolClickable(m.EnableMultiUnits().Bool()),
CopyUnitBtn: new(TipClickable),
SelectTypeBtn: new(widget.Clickable),
commentEditor: NewEditor(widget.Editor{SingleLine: true, Submit: true}),
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.copyHint = makeHint("Copy unit", " (%s)", "Copy")
ret.disableUnitHint = makeHint("Disable 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
}
func (pe *UnitEditor) Layout(gtx C, t *Tracker) D {
for {
e, ok := gtx.Event(
key.Filter{Focus: pe.sliderList, Name: key.NameLeftArrow, Optional: key.ModShift},
key.Filter{Focus: pe.sliderList, Name: key.NameRightArrow, Optional: key.ModShift},
key.Filter{Focus: pe.sliderList, Name: key.NameEscape},
key.Filter{Focus: pe.sliderColumns, Name: key.NameLeftArrow, Optional: key.ModShift},
key.Filter{Focus: pe.sliderColumns, Name: key.NameRightArrow, Optional: key.ModShift},
key.Filter{Focus: pe.sliderColumns, Name: key.NameEscape},
)
if !ok {
break
@ -78,9 +93,9 @@ func (pe *UnitEditor) Layout(gtx C, t *Tracker) D {
}
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()
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
}
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 {
numItems := pe.sliderList.TrackerList.Count()
for len(pe.Parameters) < numItems {
pe.Parameters = append(pe.Parameters, new(ParameterWidget))
func (pe *UnitEditor) layoutColumns(gtx C, t *Tracker) D {
numUnits := pe.sliderColumns.TrackerList.Count()
for len(pe.Parameters) < numUnits {
pe.Parameters = append(pe.Parameters, []*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
for param := range t.Model.Params().Iterate {
pe.Parameters[index].Parameter = param
index++
if !t.Model.EnableMultiUnits().Value() {
return pe.layoutSliderColumn(gtx, t, t.Model.Units().Selected(), false)
}
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{}
}
paramStyle := t.ParamStyle(t.Theme, pe.Parameters[index])
paramStyle.Focus = pe.sliderList.TrackerList.Selected() == index
dims := paramStyle.Layout(gtx)
return D{Size: image.Pt(gtx.Constraints.Max.X, dims.Size.Y)}
dims := pe.layoutSliderColumn(gtx, t, index, true)
return D{Size: image.Pt(dims.Size.X, gtx.Constraints.Max.Y)}
}
fdl := FilledDragList(t.Theme, pe.sliderList, element, nil)
fdl := FilledDragList(t.Theme, pe.sliderColumns, column, nil)
dims := fdl.Layout(gtx)
gtx.Constraints = layout.Exact(dims.Size)
fdl.LayoutScrollBar(gtx)
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 {
for pe.CopyUnitBtn.Clickable.Clicked(gtx) {
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)
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)
multiUnitsBtnStyle := ToggleIcon(gtx, t.Theme, pe.MultiUnitsBtn, icons.ActionViewWeek, icons.ActionViewWeek, pe.multiUnitsHint, pe.multiUnitsHint)
text := t.Units().SelectedType()
if text == "" {
text = "Choose unit type"
@ -172,6 +238,7 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
s.Set(pe.commentEditor.Text())
return ret
}),
layout.Rigid(multiUnitsBtnStyle.Layout),
)
}
@ -210,7 +277,7 @@ func (pe *UnitEditor) command(e key.Event, t *Tracker) {
if sel == nil {
return
}
i := (&tracker.Int{IntData: sel})
i := &tracker.Int{IntData: sel}
if e.Modifiers.Contain(key.ModShift) {
i.Set(i.Value() - sel.LargeStep())
} else {
@ -221,7 +288,7 @@ func (pe *UnitEditor) command(e key.Event, t *Tracker) {
if sel == nil {
return
}
i := (&tracker.Int{IntData: sel})
i := &tracker.Int{IntData: sel}
if e.Modifiers.Contain(key.ModShift) {
i.Set(i.Value() + sel.LargeStep())
} else {
@ -267,13 +334,25 @@ func (t *Tracker) ParamStyle(th *material.Theme, paramWidget *ParameterWidget) P
}
}
func (p ParameterStyle) Layout(gtx C) D {
isSendTarget, info := p.tryDerivedParameterInfo()
func spacer(px int) layout.FlexChild {
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,
spacer(24),
layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110))
return layout.E.Layout(gtx, Label(p.w.Parameter.Name(), white, p.tracker.Theme.Shaper))
dims := 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 {
switch p.w.Parameter.Type() {
case tracker.IntegerParameter:
@ -371,6 +450,7 @@ func (p ParameterStyle) Layout(gtx C) D {
}
return D{}
}),
spacer(8),
layout.Rigid(func(gtx C) D {
if p.w.Parameter.Type() != tracker.IDParameter {
color := white
@ -387,22 +467,31 @@ func (p ParameterStyle) Layout(gtx C) D {
}
return D{}
}),
spacer(24),
)
}
func buildUnitLabel(index int, u sointu.Unit) string {
text := u.Type
if u.Comment != "" {
text = fmt.Sprintf("%s \"%s\"", text, u.Comment)
}
return fmt.Sprintf("%d: %s", index, text)
return fmt.Sprintf("%d: %s", index, buildUnitName(u))
}
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)
if !ok {
return false, ""
}
isSendTarget, sendInfo, _ = p.tracker.ParameterInfo(param.Unit().ID, param.Name())
isSendTarget, sendInfo, _ = p.tracker.ParameterInfo(unitId, param.Name())
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)
}
UnitListItem struct {
Type, Comment string
Disabled bool
StackNeed, StackBefore, StackAfter int
}
// 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
@ -61,7 +51,6 @@ type (
// Model methods
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) }
@ -257,162 +246,6 @@ func (m *Instruments) unmarshal(data []byte) (r Range, err error) {
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
func (v *Tracks) List() List {

View File

@ -66,7 +66,8 @@ type (
// when linkInstrTrack is false, editing an instrument does not change
// the track. when true, editing an instrument changes the tracks (e.g.
// reordering or deleting instrument can delete track)
linkInstrTrack bool
linkInstrTrack bool
enableMultiUnits bool
voiceLevels [vm.MAX_VOICES]float32

View File

@ -41,6 +41,11 @@ type (
Params Model
ParamsForUnit struct {
*Params
unitIndex int
}
ParamYieldFunc func(param Parameter) bool
ParameterType int
@ -61,6 +66,13 @@ const (
func (m *Model) Params() *Params { return (*Params)(m) }
func (m *Model) ParamsForUnit(u int) *ParamsForUnit {
return &ParamsForUnit{
Params: m.Params(),
unitIndex: u,
}
}
// parameter methods
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)
}
func (pl *Params) Count() int {
func count(iterator func(yield ParamYieldFunc)) int {
count := 0
for range pl.Iterate {
for _ = range iterator {
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) {
index := pl.Selected()
for param := range pl.Iterate {
@ -100,55 +120,88 @@ func (pl *Params) SelectedItem() (ret Parameter) {
}
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) {
return
}
unit := &pl.d.Song.Patch[pl.d.InstrIndex].Units[pl.d.UnitIndex]
unitType, ok := sointu.UnitTypes[unit.Type]
if !ok {
return
}
for i := range unitType {
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],
}) {
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
}
}
if unit.Type == "oscillator" && unit.Parameters["type"] == sointu.Sample {
if !yield(GmDlsEntryParameter{parameter: parameter{m: (*Model)(pl), unit: unit}}) {
unit := &pl.d.Song.Patch[pl.d.InstrIndex].Units[unitIndex]
unitType, ok := sointu.UnitTypes[unit.Type]
if !ok {
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}) {
for i := range unitType {
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
}
}
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
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]
}