mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-06 06:02:53 -04:00
feat(tracker): add a preset explorer with search and filters
Closes #91
This commit is contained in:
parent
3f365707c2
commit
2336a135c6
@ -11,22 +11,24 @@ 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
|
||||
Oversampling Model
|
||||
Panic Model
|
||||
IsRecording Model
|
||||
Playing Model
|
||||
InstrEnlarged Model
|
||||
Effect Model
|
||||
TrackMidiIn Model
|
||||
Follow Model
|
||||
UnitSearching Model
|
||||
UnitDisabled Model
|
||||
LoopToggle Model
|
||||
UniquePatterns Model
|
||||
Mute Model
|
||||
Solo Model
|
||||
LinkInstrTrack Model
|
||||
Oversampling Model
|
||||
InstrEditor Model
|
||||
InstrPresets Model
|
||||
InstrComment Model
|
||||
)
|
||||
|
||||
func MakeBool(valueEnabler interface {
|
||||
@ -101,11 +103,31 @@ func (m *Model) InstrEnlarged() Bool { return MakeEnabledBool((*InstrEnlar
|
||||
func (m *InstrEnlarged) Value() bool { return m.instrEnlarged }
|
||||
func (m *InstrEnlarged) SetValue(val bool) { m.instrEnlarged = val }
|
||||
|
||||
// CommentExpanded methods
|
||||
// InstrEditor methods
|
||||
|
||||
func (m *Model) CommentExpanded() Bool { return MakeEnabledBool((*CommentExpanded)(m)) }
|
||||
func (m *CommentExpanded) Value() bool { return m.commentExpanded }
|
||||
func (m *CommentExpanded) SetValue(val bool) { m.commentExpanded = val }
|
||||
func (m *Model) InstrEditor() Bool { return MakeEnabledBool((*InstrEditor)(m)) }
|
||||
func (m *InstrEditor) Value() bool { return m.d.InstrumentTab == InstrumentEditorTab }
|
||||
func (m *InstrEditor) SetValue(val bool) {
|
||||
if val {
|
||||
m.d.InstrumentTab = InstrumentEditorTab
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) InstrComment() Bool { return MakeEnabledBool((*InstrComment)(m)) }
|
||||
func (m *InstrComment) Value() bool { return m.d.InstrumentTab == InstrumentCommentTab }
|
||||
func (m *InstrComment) SetValue(val bool) {
|
||||
if val {
|
||||
m.d.InstrumentTab = InstrumentCommentTab
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) InstrPresets() Bool { return MakeEnabledBool((*InstrPresets)(m)) }
|
||||
func (m *InstrPresets) Value() bool { return m.d.InstrumentTab == InstrumentPresetsTab }
|
||||
func (m *InstrPresets) SetValue(val bool) {
|
||||
if val {
|
||||
m.d.InstrumentTab = InstrumentPresetsTab
|
||||
}
|
||||
}
|
||||
|
||||
// Follow methods
|
||||
|
||||
|
||||
@ -33,9 +33,10 @@ type (
|
||||
// corresponding part of the model changes.
|
||||
derivedModelData struct {
|
||||
// map Unit by ID, other entities by their respective index
|
||||
patch []derivedInstrument
|
||||
tracks []derivedTrack
|
||||
railError RailError
|
||||
patch []derivedInstrument
|
||||
tracks []derivedTrack
|
||||
railError RailError
|
||||
presetSearch derivedPresetSearch
|
||||
}
|
||||
|
||||
derivedInstrument struct {
|
||||
|
||||
@ -186,8 +186,5 @@ success:
|
||||
instrument.NumVoices = clamp(instrument.NumVoices, 1, 32-numVoices)
|
||||
m.assignUnitIDs(instrument.Units)
|
||||
m.d.Song.Patch[m.d.InstrIndex] = instrument
|
||||
if m.d.Song.Patch[m.d.InstrIndex].Comment != "" {
|
||||
m.commentExpanded = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -73,6 +73,13 @@ type (
|
||||
Button
|
||||
}
|
||||
|
||||
// TabButton is a button used in a tab bar.
|
||||
TabButton struct {
|
||||
IndicatorHeight unit.Dp
|
||||
IndicatorColor color.NRGBA
|
||||
ToggleButton
|
||||
}
|
||||
|
||||
// IconButton is a button with an icon.
|
||||
IconButton struct {
|
||||
Theme *Theme
|
||||
@ -126,6 +133,19 @@ func ToggleBtn(b tracker.Bool, th *Theme, c *Clickable, text string, tip string)
|
||||
}
|
||||
}
|
||||
|
||||
func TabBtn(b tracker.Bool, th *Theme, c *Clickable, text string, tip string) TabButton {
|
||||
return TabButton{
|
||||
IndicatorHeight: th.Button.Tab.IndicatorHeight,
|
||||
IndicatorColor: th.Button.Tab.IndicatorColor,
|
||||
ToggleButton: ToggleButton{
|
||||
Bool: b,
|
||||
DisabledStyle: &th.Button.Disabled,
|
||||
OffStyle: &th.Button.Tab.Inactive,
|
||||
Button: Btn(th, &th.Button.Tab.Active, c, text, tip),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IconBtn(th *Theme, st *IconButtonStyle, c *Clickable, icon []byte, tip string) IconButton {
|
||||
return IconButton{
|
||||
Theme: th,
|
||||
@ -288,6 +308,26 @@ func (b *ToggleIconButton) Layout(gtx C) D {
|
||||
return b.IconButton.Layout(gtx)
|
||||
}
|
||||
|
||||
func (b *TabButton) Layout(gtx C) D {
|
||||
return layout.Stack{Alignment: layout.S}.Layout(gtx,
|
||||
layout.Stacked(b.ToggleButton.Layout),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
if !b.ToggleButton.Bool.Value() {
|
||||
return D{}
|
||||
}
|
||||
w := gtx.Constraints.Min.X
|
||||
h := gtx.Dp(b.IndicatorHeight)
|
||||
r := clip.RRect{
|
||||
Rect: image.Rect(0, 0, w, h),
|
||||
NE: h, NW: h, SE: 0, SW: 0,
|
||||
}
|
||||
defer r.Push(gtx.Ops).Pop()
|
||||
paint.Fill(gtx.Ops, b.IndicatorColor)
|
||||
return layout.Dimensions{Size: image.Pt(w, h)}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Click executes a simple programmatic click.
|
||||
func (b *Clickable) Click() {
|
||||
b.requestClicks++
|
||||
|
||||
@ -67,6 +67,8 @@ func (e *Editor) Layout(gtx C, str tracker.String, th *Theme, style *EditorStyle
|
||||
}
|
||||
if e.widgetEditor.Text() != str.Value() {
|
||||
e.widgetEditor.SetText(str.Value())
|
||||
l := len(e.widgetEditor.Text())
|
||||
e.widgetEditor.SetCaret(l, l)
|
||||
}
|
||||
me := material.Editor(&th.Material, &e.widgetEditor, hint)
|
||||
me.Font = style.Font
|
||||
|
||||
@ -8,6 +8,9 @@ import (
|
||||
)
|
||||
|
||||
type TagYieldFunc func(level int, tag event.Tag) bool
|
||||
type Tagged interface {
|
||||
Tags(level int, yield TagYieldFunc) bool
|
||||
}
|
||||
|
||||
// FocusNext navigates to the next focusable tag in the tracker. If stepInto is
|
||||
// true, it will focus the next tag regardless of its depth; otherwise it will
|
||||
@ -77,3 +80,12 @@ func (t *Tracker) findFocusedLevel(gtx C) (level int, ok bool) {
|
||||
})
|
||||
return level, ok
|
||||
}
|
||||
|
||||
func firstTag(t Tagged) (tag event.Tag, ok bool) {
|
||||
t.Tags(0, func(level int, t event.Tag) bool {
|
||||
tag = t
|
||||
ok = true
|
||||
return false
|
||||
})
|
||||
return tag, ok
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@ import (
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/f32"
|
||||
@ -16,6 +18,8 @@ import (
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"golang.org/x/text/cases"
|
||||
@ -23,6 +27,18 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
InstrumentEditor struct {
|
||||
unitList UnitList
|
||||
unitEditor UnitEditor
|
||||
}
|
||||
|
||||
UnitList struct {
|
||||
dragList *DragList
|
||||
searchEditor *Editor
|
||||
addUnitBtn *Clickable
|
||||
addUnitAction tracker.Action
|
||||
}
|
||||
|
||||
UnitEditor struct {
|
||||
paramTable *ScrollTable
|
||||
searchList *DragList
|
||||
@ -43,6 +59,154 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func MakeInstrumentEditor(model *tracker.Model) InstrumentEditor {
|
||||
return InstrumentEditor{
|
||||
unitList: MakeUnitList(model),
|
||||
unitEditor: *NewUnitEditor(model),
|
||||
}
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layout(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(ie.unitList.Layout),
|
||||
layout.Flexed(1, ie.unitEditor.Layout),
|
||||
)
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Tags(level int, yield TagYieldFunc) bool {
|
||||
return ie.unitList.Tags(level, yield) &&
|
||||
ie.unitEditor.Tags(level, yield)
|
||||
}
|
||||
|
||||
// UnitList methods
|
||||
|
||||
func MakeUnitList(m *tracker.Model) UnitList {
|
||||
ret := UnitList{
|
||||
dragList: NewDragList(m.Units().List(), layout.Vertical),
|
||||
addUnitBtn: new(Clickable),
|
||||
searchEditor: NewEditor(true, true, text.Start),
|
||||
}
|
||||
ret.addUnitAction = tracker.MakeEnabledAction(tracker.DoFunc(func() {
|
||||
m.AddUnit(false).Do()
|
||||
ret.searchEditor.Focus()
|
||||
}))
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ul *UnitList) Layout(gtx C) D {
|
||||
t := TrackerFromContext(gtx)
|
||||
ul.update(gtx, t)
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints.Max.Y = gtx.Dp(20)
|
||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||
u := t.Units().Item(i)
|
||||
editorStyle := t.Theme.InstrumentEditor.UnitList.Name
|
||||
signalError := t.RailError()
|
||||
switch {
|
||||
case u.Disabled:
|
||||
editorStyle = t.Theme.InstrumentEditor.UnitList.NameDisabled
|
||||
case signalError.Err != nil && signalError.UnitIndex == i:
|
||||
editorStyle.Color = t.Theme.InstrumentEditor.UnitList.Error
|
||||
}
|
||||
unitName := func(gtx C) D {
|
||||
if i == ul.dragList.TrackerList.Selected() {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
return ul.searchEditor.Layout(gtx, t.Model.UnitSearch(), t.Theme, &editorStyle, "---")
|
||||
} else {
|
||||
text := u.Type
|
||||
if text == "" {
|
||||
text = "---"
|
||||
}
|
||||
l := editorStyle.AsLabelStyle()
|
||||
return Label(t.Theme, &l, text).Layout(gtx)
|
||||
}
|
||||
}
|
||||
stackText := strconv.FormatInt(int64(u.Signals.StackAfter()), 10)
|
||||
commentLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Comment, u.Comment)
|
||||
stackLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Stack, stackText)
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(unitName),
|
||||
layout.Rigid(layout.Spacer{Width: 5}.Layout),
|
||||
layout.Flexed(1, commentLabel.Layout),
|
||||
layout.Rigid(stackLabel.Layout),
|
||||
layout.Rigid(layout.Spacer{Width: 10}.Layout),
|
||||
)
|
||||
}
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
unitList := FilledDragList(t.Theme, ul.dragList)
|
||||
surface := func(gtx C) D {
|
||||
return layout.Stack{Alignment: layout.SE}.Layout(gtx,
|
||||
layout.Expanded(func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(140), gtx.Constraints.Max.Y))
|
||||
dims := unitList.Layout(gtx, element, nil)
|
||||
unitList.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)}
|
||||
addUnitBtn := IconBtn(t.Theme, &t.Theme.IconButton.Emphasis, ul.addUnitBtn, icons.ContentAdd, "Add unit (Enter)")
|
||||
return margin.Layout(gtx, addUnitBtn.Layout)
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Surface{Gray: 30, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, surface)
|
||||
}
|
||||
|
||||
func (ul *UnitList) update(gtx C, t *Tracker) {
|
||||
for ul.addUnitBtn.Clicked(gtx) {
|
||||
ul.addUnitAction.Do()
|
||||
t.UnitSearching().SetValue(true)
|
||||
ul.searchEditor.Focus()
|
||||
}
|
||||
for {
|
||||
event, ok := gtx.Event(
|
||||
key.Filter{Focus: ul.dragList, Name: key.NameRightArrow},
|
||||
key.Filter{Focus: ul.dragList, Name: key.NameEnter, Optional: key.ModCtrl},
|
||||
key.Filter{Focus: ul.dragList, Name: key.NameReturn, Optional: key.ModCtrl},
|
||||
key.Filter{Focus: ul.dragList, Name: key.NameDeleteBackward},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := event.(key.Event); ok && e.State == key.Press {
|
||||
switch e.Name {
|
||||
case key.NameRightArrow:
|
||||
t.PatchPanel.instrEditor.unitEditor.paramTable.RowTitleList.Focus()
|
||||
case key.NameDeleteBackward:
|
||||
t.Units().SetSelectedType("")
|
||||
t.UnitSearching().SetValue(true)
|
||||
ul.searchEditor.Focus()
|
||||
case key.NameEnter, key.NameReturn:
|
||||
t.Model.AddUnit(e.Modifiers.Contain(key.ModCtrl)).Do()
|
||||
t.UnitSearching().SetValue(true)
|
||||
ul.searchEditor.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
str := t.Model.UnitSearch()
|
||||
for ev := ul.searchEditor.Update(gtx, str); ev != EditorEventNone; ev = ul.searchEditor.Update(gtx, str) {
|
||||
if ev == EditorEventSubmit {
|
||||
if str.Value() != "" {
|
||||
for _, n := range sointu.UnitNames {
|
||||
if strings.HasPrefix(n, str.Value()) {
|
||||
t.Units().SetSelectedType(n)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Units().SetSelectedType("")
|
||||
}
|
||||
}
|
||||
ul.dragList.Focus()
|
||||
t.UnitSearching().SetValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (ul *UnitList) Tags(curLevel int, yield TagYieldFunc) bool {
|
||||
return yield(curLevel, ul.dragList) && yield(curLevel+1, &ul.searchEditor.widgetEditor)
|
||||
}
|
||||
|
||||
func NewUnitEditor(m *tracker.Model) *UnitEditor {
|
||||
ret := &UnitEditor{
|
||||
DeleteUnitBtn: new(Clickable),
|
||||
@ -159,7 +323,7 @@ func (pe *UnitEditor) update(gtx C, t *Tracker) {
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
switch e.Name {
|
||||
case key.NameLeftArrow:
|
||||
t.PatchPanel.unitList.dragList.Focus()
|
||||
t.PatchPanel.instrEditor.unitList.dragList.Focus()
|
||||
case key.NameDeleteBackward:
|
||||
t.ClearUnit().Do()
|
||||
t.UnitSearch().SetValue("")
|
||||
211
tracker/gioui/instrument_presets.go
Normal file
211
tracker/gioui/instrument_presets.go
Normal file
@ -0,0 +1,211 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type (
|
||||
InstrumentPresets struct {
|
||||
searchEditor *Editor
|
||||
gmDlsBtn *Clickable
|
||||
userPresetsBtn *Clickable
|
||||
builtinPresetsBtn *Clickable
|
||||
clearSearchBtn *Clickable
|
||||
saveUserPreset *Clickable
|
||||
deleteUserPreset *Clickable
|
||||
dirList *DragList
|
||||
resultList *DragList
|
||||
}
|
||||
)
|
||||
|
||||
func NewInstrumentPresets(m *tracker.Model) *InstrumentPresets {
|
||||
return &InstrumentPresets{
|
||||
searchEditor: NewEditor(true, true, text.Start),
|
||||
gmDlsBtn: new(Clickable),
|
||||
clearSearchBtn: new(Clickable),
|
||||
userPresetsBtn: new(Clickable),
|
||||
builtinPresetsBtn: new(Clickable),
|
||||
saveUserPreset: new(Clickable),
|
||||
deleteUserPreset: new(Clickable),
|
||||
dirList: NewDragList(m.PresetDirList().List(), layout.Vertical),
|
||||
resultList: NewDragList(m.PresetResultList().List(), layout.Vertical),
|
||||
}
|
||||
}
|
||||
|
||||
func (ip *InstrumentPresets) Tags(level int, yield TagYieldFunc) bool {
|
||||
return yield(level, &ip.searchEditor.widgetEditor) &&
|
||||
yield(level+1, ip.clearSearchBtn) &&
|
||||
yield(level+1, ip.builtinPresetsBtn) &&
|
||||
yield(level+1, ip.userPresetsBtn) &&
|
||||
yield(level+1, ip.gmDlsBtn) &&
|
||||
yield(level, ip.dirList) &&
|
||||
yield(level, ip.resultList) &&
|
||||
yield(level+1, ip.saveUserPreset) &&
|
||||
yield(level+1, ip.deleteUserPreset)
|
||||
}
|
||||
|
||||
func (ip *InstrumentPresets) update(gtx C) {
|
||||
for {
|
||||
event, ok := gtx.Event(
|
||||
key.Filter{Focus: ip.resultList, Name: key.NameLeftArrow},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := event.(key.Event); ok && e.State == key.Press {
|
||||
switch e.Name {
|
||||
case key.NameLeftArrow:
|
||||
ip.dirList.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
for {
|
||||
event, ok := gtx.Event(
|
||||
key.Filter{Focus: ip.dirList, Name: key.NameRightArrow},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := event.(key.Event); ok && e.State == key.Press {
|
||||
switch e.Name {
|
||||
case key.NameRightArrow:
|
||||
ip.resultList.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ip *InstrumentPresets) layout(gtx C) D {
|
||||
ip.update(gtx)
|
||||
// get tracker from values
|
||||
tr := TrackerFromContext(gtx)
|
||||
gmDlsBtn := ToggleBtn(tr.NoGmDls(), tr.Theme, ip.gmDlsBtn, "No gm.dls", "Exclude presets using gm.dls")
|
||||
userPresetsFilterBtn := ToggleBtn(tr.UserPresetFilter(), tr.Theme, ip.userPresetsBtn, "User", "Show only user presets")
|
||||
builtinPresetsFilterBtn := ToggleBtn(tr.BuiltinPresetsFilter(), tr.Theme, ip.builtinPresetsBtn, "Builtin", "Show only builtin presets")
|
||||
saveUserPresetBtn := ActionIconBtn(tr.SaveAsUserPreset(), tr.Theme, ip.saveUserPreset, icons.ContentSave, "Save instrument as user preset")
|
||||
deleteUserPresetBtn := ActionIconBtn(tr.TryDeleteUserPreset(), tr.Theme, ip.deleteUserPreset, icons.ActionDelete, "Delete user preset")
|
||||
dirElem := func(gtx C, i int) D {
|
||||
return Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Directory, tr.Model.PresetDirList().Value(i)).Layout(gtx)
|
||||
}
|
||||
dirs := func(gtx C) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(140), gtx.Constraints.Max.Y))
|
||||
fdl := FilledDragList(tr.Theme, ip.dirList)
|
||||
dims := fdl.Layout(gtx, dirElem, nil)
|
||||
fdl.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}
|
||||
dirSurface := func(gtx C) D {
|
||||
return Surface{Gray: 36, Focus: tr.PatchPanel.TreeFocused(gtx)}.Layout(gtx, dirs)
|
||||
}
|
||||
resultElem := func(gtx C, i int) D {
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
n, d, u := tr.Model.PresetResultList().Value(i)
|
||||
if u {
|
||||
ln := Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Results.User, n)
|
||||
ld := Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Results.UserDir, d)
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(ln.Layout),
|
||||
layout.Rigid(layout.Spacer{Width: 6}.Layout),
|
||||
layout.Rigid(ld.Layout),
|
||||
)
|
||||
}
|
||||
return Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Results.Builtin, n).Layout(gtx)
|
||||
}
|
||||
floatButtons := func(gtx C) D {
|
||||
if tr.Model.DeleteUserPreset().Enabled() {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(deleteUserPresetBtn.Layout),
|
||||
layout.Rigid(saveUserPresetBtn.Layout),
|
||||
layout.Rigid(layout.Spacer{Width: 10}.Layout),
|
||||
)
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(saveUserPresetBtn.Layout),
|
||||
layout.Rigid(layout.Spacer{Width: 10}.Layout),
|
||||
)
|
||||
}
|
||||
results := func(gtx C) D {
|
||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||
fdl := FilledDragList(tr.Theme, ip.resultList)
|
||||
dims := fdl.Layout(gtx, resultElem, nil)
|
||||
layout.SE.Layout(gtx, floatButtons)
|
||||
fdl.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}
|
||||
resultSurface := func(gtx C) D {
|
||||
return Surface{Gray: 30, Focus: tr.PatchPanel.TreeFocused(gtx)}.Layout(gtx, results)
|
||||
}
|
||||
bottom := func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(dirSurface),
|
||||
layout.Flexed(1, resultSurface),
|
||||
)
|
||||
}
|
||||
// layout
|
||||
f := func(gtx C) D {
|
||||
m := gtx.Constraints.Max
|
||||
gtx.Constraints.Max.X = min(gtx.Dp(360), gtx.Constraints.Max.X)
|
||||
layout.Flex{Axis: layout.Vertical, Alignment: layout.Start}.Layout(gtx,
|
||||
layout.Rigid(ip.layoutSearch),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(userPresetsFilterBtn.Layout),
|
||||
layout.Rigid(builtinPresetsFilterBtn.Layout),
|
||||
layout.Rigid(gmDlsBtn.Layout),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(bottom),
|
||||
)
|
||||
return D{Size: m}
|
||||
}
|
||||
return Surface{Gray: 24, Focus: tr.PatchPanel.TreeFocused(gtx)}.Layout(gtx, f)
|
||||
}
|
||||
|
||||
func (ip *InstrumentPresets) layoutSearch(gtx C) D {
|
||||
// draw search icon on left and clear button on right
|
||||
// return ip.searchEditor.Layout(gtx, tr.Model.PresetSearchString(), tr.Theme, &tr.Theme.InstrumentEditor.InstrumentComment, "Search presets")
|
||||
tr := TrackerFromContext(gtx)
|
||||
bg := func(gtx C) D {
|
||||
rr := gtx.Dp(18)
|
||||
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
|
||||
paint.Fill(gtx.Ops, tr.Theme.InstrumentEditor.Presets.SearchBg)
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
// icon, search editor, clear button
|
||||
icon := func(gtx C) D {
|
||||
return tr.Theme.IconButton.Enabled.Inset.Layout(gtx, func(gtx C) D {
|
||||
return tr.Theme.Icon(icons.ActionSearch).Layout(gtx, tr.Theme.Material.Fg)
|
||||
})
|
||||
}
|
||||
ed := func(gtx C) D {
|
||||
return ip.searchEditor.Layout(gtx, tr.Model.PresetSearchString(), tr.Theme, &tr.Theme.InstrumentEditor.UnitComment, "Search presets")
|
||||
}
|
||||
clr := func(gtx C) D {
|
||||
btn := ActionIconBtn(tr.ClearPresetSearch(), tr.Theme, ip.clearSearchBtn, icons.ContentClear, "Clear search")
|
||||
return btn.Layout(gtx)
|
||||
}
|
||||
w := func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(icon),
|
||||
layout.Flexed(1, ed),
|
||||
layout.Rigid(clr),
|
||||
)
|
||||
}
|
||||
return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Expanded(bg),
|
||||
layout.Stacked(w),
|
||||
)
|
||||
})
|
||||
}
|
||||
105
tracker/gioui/instrument_properties.go
Normal file
105
tracker/gioui/instrument_properties.go
Normal file
@ -0,0 +1,105 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type (
|
||||
InstrumentProperties struct {
|
||||
nameEditor *Editor
|
||||
commentEditor *Editor
|
||||
list *layout.List
|
||||
soloBtn *Clickable
|
||||
muteBtn *Clickable
|
||||
soloHint string
|
||||
unsoloHint string
|
||||
muteHint string
|
||||
unmuteHint string
|
||||
voices *NumericUpDownState
|
||||
splitInstrumentBtn *Clickable
|
||||
splitInstrumentHint string
|
||||
}
|
||||
)
|
||||
|
||||
func NewInstrumentProperties() *InstrumentProperties {
|
||||
ret := &InstrumentProperties{
|
||||
list: &layout.List{Axis: layout.Vertical},
|
||||
nameEditor: NewEditor(true, true, text.Start),
|
||||
commentEditor: NewEditor(false, false, text.Start),
|
||||
soloBtn: new(Clickable),
|
||||
muteBtn: new(Clickable),
|
||||
voices: NewNumericUpDownState(),
|
||||
splitInstrumentBtn: new(Clickable),
|
||||
}
|
||||
ret.soloHint = makeHint("Solo", " (%s)", "SoloToggle")
|
||||
ret.unsoloHint = makeHint("Unsolo", " (%s)", "SoloToggle")
|
||||
ret.muteHint = makeHint("Mute", " (%s)", "MuteToggle")
|
||||
ret.unmuteHint = makeHint("Unmute", " (%s)", "MuteToggle")
|
||||
ret.splitInstrumentHint = makeHint("Split instrument", " (%s)", "SplitInstrument")
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ip *InstrumentProperties) Tags(level int, yield TagYieldFunc) bool {
|
||||
return yield(level, &ip.commentEditor.widgetEditor)
|
||||
}
|
||||
|
||||
// layout
|
||||
func (ip *InstrumentProperties) layout(gtx C) D {
|
||||
// get tracker from values
|
||||
tr := TrackerFromContext(gtx)
|
||||
voiceLine := func(gtx C) D {
|
||||
splitInstrumentBtn := ActionIconBtn(tr.SplitInstrument(), tr.Theme, ip.splitInstrumentBtn, icons.CommunicationCallSplit, ip.splitInstrumentHint)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
instrumentVoices := NumUpDown(tr.Model.InstrumentVoices(), tr.Theme, ip.voices, "Number of voices for this instrument")
|
||||
return instrumentVoices.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(splitInstrumentBtn.Layout),
|
||||
)
|
||||
}
|
||||
|
||||
return ip.list.Layout(gtx, 9, func(gtx C, index int) D {
|
||||
switch index {
|
||||
case 0:
|
||||
return layoutInstrumentPropertyLine(gtx, "Name", func(gtx C) D {
|
||||
return ip.nameEditor.Layout(gtx, tr.InstrumentName(), tr.Theme, &tr.Theme.InstrumentEditor.InstrumentComment, "Instr")
|
||||
})
|
||||
case 2:
|
||||
return layoutInstrumentPropertyLine(gtx, "Voices", voiceLine)
|
||||
case 4:
|
||||
muteBtn := ToggleIconBtn(tr.Mute(), tr.Theme, ip.muteBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.muteHint, ip.unmuteHint)
|
||||
return layoutInstrumentPropertyLine(gtx, "Mute", muteBtn.Layout)
|
||||
case 6:
|
||||
soloBtn := ToggleIconBtn(tr.Solo(), tr.Theme, ip.soloBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.soloHint, ip.unsoloHint)
|
||||
return layoutInstrumentPropertyLine(gtx, "Solo", soloBtn.Layout)
|
||||
case 8:
|
||||
return layout.UniformInset(unit.Dp(6)).Layout(gtx, func(gtx C) D {
|
||||
return ip.commentEditor.Layout(gtx, tr.InstrumentComment(), tr.Theme, &tr.Theme.InstrumentEditor.InstrumentComment, "Comment")
|
||||
})
|
||||
default: // odd valued list items are dividers
|
||||
px := max(gtx.Dp(unit.Dp(1)), 1)
|
||||
paint.FillShape(gtx.Ops, color.NRGBA{255, 255, 255, 3}, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, px)).Op())
|
||||
return D{Size: image.Pt(gtx.Constraints.Max.X, px)}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func layoutInstrumentPropertyLine(gtx C, text string, content layout.Widget) D {
|
||||
tr := TrackerFromContext(gtx)
|
||||
gtx.Constraints.Max.X = min(gtx.Dp(unit.Dp(200)), gtx.Constraints.Max.X)
|
||||
label := Label(tr.Theme, &tr.Theme.InstrumentEditor.Properties.Label, text)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(layout.Spacer{Width: 6, Height: 36}.Layout),
|
||||
layout.Rigid(label.Layout),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(content),
|
||||
)
|
||||
}
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"gopkg.in/yaml.v2"
|
||||
@ -208,8 +209,6 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
||||
t.InstrEnlarged().Toggle()
|
||||
case "LinkInstrTrackToggle":
|
||||
t.LinkInstrTrack().Toggle()
|
||||
case "CommentExpandedToggle":
|
||||
t.CommentExpanded().Toggle()
|
||||
case "FollowToggle":
|
||||
t.Follow().Toggle()
|
||||
case "UnitDisabledToggle":
|
||||
@ -259,13 +258,20 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
||||
case "Paste":
|
||||
gtx.Execute(clipboard.ReadCmd{Tag: t})
|
||||
case "OrderEditorFocus":
|
||||
t.InstrEnlarged().SetValue(false)
|
||||
gtx.Execute(key.FocusCmd{Tag: t.OrderEditor.scrollTable})
|
||||
case "TrackEditorFocus":
|
||||
t.InstrEnlarged().SetValue(false)
|
||||
gtx.Execute(key.FocusCmd{Tag: t.TrackEditor.scrollTable})
|
||||
case "InstrumentListFocus":
|
||||
gtx.Execute(key.FocusCmd{Tag: t.PatchPanel.instrList.instrumentDragList})
|
||||
case "UnitListFocus":
|
||||
gtx.Execute(key.FocusCmd{Tag: t.PatchPanel.unitList.dragList})
|
||||
var tag event.Tag
|
||||
t.PatchPanel.BottomTags(0, func(level int, t event.Tag) bool {
|
||||
tag = t
|
||||
return false
|
||||
})
|
||||
gtx.Execute(key.FocusCmd{Tag: tag})
|
||||
case "FocusPrev":
|
||||
t.FocusPrev(gtx, false)
|
||||
case "FocusPrevInto":
|
||||
|
||||
@ -6,32 +6,42 @@ import (
|
||||
"image/color"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type (
|
||||
PatchPanel struct {
|
||||
instrList InstrumentList
|
||||
tools InstrumentTools
|
||||
unitList UnitList
|
||||
unitEditor UnitEditor
|
||||
instrList InstrumentList
|
||||
tools InstrumentTools
|
||||
instrProps InstrumentProperties
|
||||
instrPresets InstrumentPresets
|
||||
instrEditor InstrumentEditor
|
||||
*tracker.Model
|
||||
}
|
||||
|
||||
InstrumentList struct {
|
||||
instrumentDragList *DragList
|
||||
nameEditor *Editor
|
||||
}
|
||||
|
||||
InstrumentTools struct {
|
||||
EditorTab *Clickable
|
||||
PresetsTab *Clickable
|
||||
CommentTab *Clickable
|
||||
|
||||
saveInstrumentBtn *Clickable
|
||||
loadInstrumentBtn *Clickable
|
||||
copyInstrumentBtn *Clickable
|
||||
deleteInstrumentBtn *Clickable
|
||||
|
||||
octave *NumericUpDownState
|
||||
enlargeBtn *Clickable
|
||||
@ -43,69 +53,58 @@ type (
|
||||
linkEnabledHint string
|
||||
enlargeHint, shrinkHint string
|
||||
addInstrumentHint string
|
||||
}
|
||||
|
||||
InstrumentTools struct {
|
||||
Voices *NumericUpDownState
|
||||
splitInstrumentBtn *Clickable
|
||||
commentExpandBtn *Clickable
|
||||
commentEditor *Editor
|
||||
soloBtn *Clickable
|
||||
muteBtn *Clickable
|
||||
presetMenuBtn *Clickable
|
||||
presetMenu MenuState
|
||||
presetMenuItems []ActionMenuItem
|
||||
saveInstrumentBtn *Clickable
|
||||
loadInstrumentBtn *Clickable
|
||||
copyInstrumentBtn *Clickable
|
||||
deleteInstrumentBtn *Clickable
|
||||
|
||||
commentExpanded tracker.Bool
|
||||
|
||||
muteHint, unmuteHint string
|
||||
soloHint, unsoloHint string
|
||||
expandCommentHint string
|
||||
collapseCommentHint string
|
||||
splitInstrumentHint string
|
||||
deleteInstrumentHint string
|
||||
}
|
||||
|
||||
UnitList struct {
|
||||
dragList *DragList
|
||||
searchEditor *Editor
|
||||
addUnitBtn *Clickable
|
||||
addUnitAction tracker.Action
|
||||
}
|
||||
)
|
||||
|
||||
// PatchPanel methods
|
||||
|
||||
func NewPatchPanel(model *tracker.Model) *PatchPanel {
|
||||
return &PatchPanel{
|
||||
instrList: MakeInstrList(model),
|
||||
tools: MakeInstrumentTools(model),
|
||||
unitList: MakeUnitList(model),
|
||||
unitEditor: *NewUnitEditor(model),
|
||||
instrEditor: MakeInstrumentEditor(model),
|
||||
instrList: MakeInstrList(model),
|
||||
tools: MakeInstrumentTools(model),
|
||||
instrProps: *NewInstrumentProperties(),
|
||||
instrPresets: *NewInstrumentPresets(model),
|
||||
Model: model,
|
||||
}
|
||||
}
|
||||
|
||||
func (pp *PatchPanel) Layout(gtx C) D {
|
||||
tr := TrackerFromContext(gtx)
|
||||
bottom := func(gtx C) D {
|
||||
switch {
|
||||
case tr.InstrComment().Value():
|
||||
return pp.instrProps.layout(gtx)
|
||||
case tr.InstrPresets().Value():
|
||||
return pp.instrPresets.layout(gtx)
|
||||
default: // editor
|
||||
return pp.instrEditor.layout(gtx)
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(pp.instrList.Layout),
|
||||
layout.Rigid(pp.tools.Layout),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(pp.unitList.Layout),
|
||||
layout.Flexed(1, pp.unitEditor.Layout),
|
||||
)
|
||||
}))
|
||||
layout.Flexed(1, bottom),
|
||||
)
|
||||
}
|
||||
|
||||
func (pp *PatchPanel) BottomTags(level int, yield TagYieldFunc) bool {
|
||||
switch {
|
||||
case pp.InstrComment().Value():
|
||||
return pp.instrProps.Tags(level, yield)
|
||||
case pp.InstrPresets().Value():
|
||||
return pp.instrPresets.Tags(level, yield)
|
||||
default: // editor
|
||||
return pp.instrEditor.Tags(level, yield)
|
||||
}
|
||||
}
|
||||
|
||||
func (pp *PatchPanel) Tags(level int, yield TagYieldFunc) bool {
|
||||
return pp.instrList.Tags(level, yield) &&
|
||||
pp.tools.Tags(level, yield) &&
|
||||
pp.unitList.Tags(level, yield) &&
|
||||
pp.unitEditor.Tags(level, yield)
|
||||
pp.BottomTags(level, yield)
|
||||
}
|
||||
|
||||
// TreeFocused returns true if any of the tags in the patch panel is focused
|
||||
@ -119,30 +118,24 @@ func (pp *PatchPanel) TreeFocused(gtx C) bool {
|
||||
|
||||
func MakeInstrumentTools(m *tracker.Model) InstrumentTools {
|
||||
ret := InstrumentTools{
|
||||
Voices: NewNumericUpDownState(),
|
||||
EditorTab: new(Clickable),
|
||||
PresetsTab: new(Clickable),
|
||||
CommentTab: new(Clickable),
|
||||
deleteInstrumentBtn: new(Clickable),
|
||||
splitInstrumentBtn: new(Clickable),
|
||||
copyInstrumentBtn: new(Clickable),
|
||||
saveInstrumentBtn: new(Clickable),
|
||||
loadInstrumentBtn: new(Clickable),
|
||||
commentExpandBtn: new(Clickable),
|
||||
presetMenuBtn: new(Clickable),
|
||||
soloBtn: new(Clickable),
|
||||
muteBtn: new(Clickable),
|
||||
presetMenuItems: []ActionMenuItem{},
|
||||
commentEditor: NewEditor(false, false, text.Start),
|
||||
commentExpanded: m.CommentExpanded(),
|
||||
expandCommentHint: makeHint("Expand comment", " (%s)", "CommentExpandedToggle"),
|
||||
collapseCommentHint: makeHint("Collapse comment", " (%s)", "CommentExpandedToggle"),
|
||||
deleteInstrumentHint: makeHint("Delete\ninstrument", "\n(%s)", "DeleteInstrument"),
|
||||
muteHint: makeHint("Mute", " (%s)", "MuteToggle"),
|
||||
unmuteHint: makeHint("Unmute", " (%s)", "MuteToggle"),
|
||||
soloHint: makeHint("Solo", " (%s)", "SoloToggle"),
|
||||
unsoloHint: makeHint("Unsolo", " (%s)", "SoloToggle"),
|
||||
splitInstrumentHint: makeHint("Split instrument", " (%s)", "SplitInstrument"),
|
||||
}
|
||||
for index, name := range m.IterateInstrumentPresets {
|
||||
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem(m.LoadPreset(index), name, "", icons.ImageAudiotrack))
|
||||
octave: NewNumericUpDownState(),
|
||||
enlargeBtn: new(Clickable),
|
||||
linkInstrTrackBtn: new(Clickable),
|
||||
newInstrumentBtn: new(Clickable),
|
||||
octaveHint: makeHint("Octave down", " (%s)", "OctaveNumberInputSubtract") + makeHint(" or up", " (%s)", "OctaveNumberInputAdd"),
|
||||
linkDisabledHint: makeHint("Instrument-Track\nlinking disabled", "\n(%s)", "LinkInstrTrackToggle"),
|
||||
linkEnabledHint: makeHint("Instrument-Track\nlinking enabled", "\n(%s)", "LinkInstrTrackToggle"),
|
||||
enlargeHint: makeHint("Enlarge", " (%s)", "InstrEnlargedToggle"),
|
||||
shrinkHint: makeHint("Shrink", " (%s)", "InstrEnlargedToggle"),
|
||||
addInstrumentHint: makeHint("Add\ninstrument", "\n(%s)", "AddInstrument"),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@ -150,55 +143,38 @@ func MakeInstrumentTools(m *tracker.Model) InstrumentTools {
|
||||
func (it *InstrumentTools) Layout(gtx C) D {
|
||||
t := TrackerFromContext(gtx)
|
||||
it.update(gtx, t)
|
||||
voicesLabel := Label(t.Theme, &t.Theme.InstrumentEditor.Voices, "Voices")
|
||||
splitInstrumentBtn := ActionIconBtn(t.SplitInstrument(), t.Theme, it.splitInstrumentBtn, icons.CommunicationCallSplit, it.splitInstrumentHint)
|
||||
commentExpandedBtn := ToggleIconBtn(t.CommentExpanded(), t.Theme, it.commentExpandBtn, icons.NavigationExpandMore, icons.NavigationExpandLess, it.expandCommentHint, it.collapseCommentHint)
|
||||
soloBtn := ToggleIconBtn(t.Solo(), t.Theme, it.soloBtn, icons.SocialGroup, icons.SocialPerson, it.soloHint, it.unsoloHint)
|
||||
muteBtn := ToggleIconBtn(t.Mute(), t.Theme, it.muteBtn, icons.AVVolumeUp, icons.AVVolumeOff, it.muteHint, it.unmuteHint)
|
||||
editorBtn := TabBtn(t.Model.InstrEditor(), t.Theme, it.EditorTab, "Editor", "")
|
||||
presetsBtn := TabBtn(t.Model.InstrPresets(), t.Theme, it.PresetsTab, "Presets", "")
|
||||
commentBtn := TabBtn(t.Model.InstrComment(), t.Theme, it.CommentTab, "Properties", "")
|
||||
octave := NumUpDown(t.Model.Octave(), t.Theme, t.OctaveNumberInput, "Octave")
|
||||
linkInstrTrackBtn := ToggleIconBtn(t.Model.LinkInstrTrack(), t.Theme, it.linkInstrTrackBtn, icons.NotificationSyncDisabled, icons.NotificationSync, it.linkDisabledHint, it.linkEnabledHint)
|
||||
instrEnlargedBtn := ToggleIconBtn(t.Model.InstrEnlarged(), t.Theme, it.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, it.enlargeHint, it.shrinkHint)
|
||||
addInstrumentBtn := ActionIconBtn(t.Model.AddInstrument(), t.Theme, it.newInstrumentBtn, icons.ContentAdd, it.addInstrumentHint)
|
||||
|
||||
saveInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, it.saveInstrumentBtn, icons.ContentSave, "Save instrument")
|
||||
loadInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, it.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument")
|
||||
copyInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, it.copyInstrumentBtn, icons.ContentContentCopy, "Copy instrument")
|
||||
deleteInstrumentBtn := ActionIconBtn(t.DeleteInstrument(), t.Theme, it.deleteInstrumentBtn, icons.ActionDelete, it.deleteInstrumentHint)
|
||||
instrumentVoices := NumUpDown(t.Model.InstrumentVoices(), t.Theme, it.Voices, "Number of voices for this instrument")
|
||||
btns := func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(layout.Spacer{Width: 6}.Layout),
|
||||
layout.Rigid(voicesLabel.Layout),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Rigid(instrumentVoices.Layout),
|
||||
layout.Rigid(splitInstrumentBtn.Layout),
|
||||
layout.Rigid(editorBtn.Layout),
|
||||
layout.Rigid(presetsBtn.Layout),
|
||||
layout.Rigid(commentBtn.Layout),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(commentExpandedBtn.Layout),
|
||||
layout.Rigid(soloBtn.Layout),
|
||||
layout.Rigid(muteBtn.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
presetBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, it.presetMenuBtn, icons.NavigationMenu, "Load preset")
|
||||
dims := presetBtn.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
m := Menu(t.Theme, &it.presetMenu)
|
||||
m.Style = &t.Theme.Menu.Preset
|
||||
m.Layout(gtx, it.presetMenuItems...)
|
||||
return dims
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Rigid(Label(t.Theme, &t.Theme.InstrumentEditor.Octave, "Octave").Layout),
|
||||
layout.Rigid(octave.Layout),
|
||||
layout.Rigid(linkInstrTrackBtn.Layout),
|
||||
layout.Rigid(instrEnlargedBtn.Layout),
|
||||
layout.Rigid(copyInstrumentBtn.Layout),
|
||||
layout.Rigid(saveInstrumentBtn.Layout),
|
||||
layout.Rigid(loadInstrumentBtn.Layout),
|
||||
layout.Rigid(copyInstrumentBtn.Layout),
|
||||
layout.Rigid(deleteInstrumentBtn.Layout),
|
||||
layout.Rigid(addInstrumentBtn.Layout),
|
||||
)
|
||||
}
|
||||
comment := func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
ret := layout.UniformInset(unit.Dp(6)).Layout(gtx, func(gtx C) D {
|
||||
return it.commentEditor.Layout(gtx, t.InstrumentComment(), t.Theme, &t.Theme.InstrumentEditor.InstrumentComment, "Comment")
|
||||
})
|
||||
return ret
|
||||
}
|
||||
return Surface{Gray: 37, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D {
|
||||
if t.CommentExpanded().Value() {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(btns), layout.Rigid(comment))
|
||||
}
|
||||
return btns(gtx)
|
||||
})
|
||||
return Surface{Gray: 37, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, btns)
|
||||
}
|
||||
|
||||
func (it *InstrumentTools) update(gtx C, tr *Tracker) {
|
||||
@ -222,18 +198,9 @@ func (it *InstrumentTools) update(gtx C, tr *Tracker) {
|
||||
}
|
||||
tr.LoadInstrument(reader)
|
||||
}
|
||||
for it.presetMenuBtn.Clicked(gtx) {
|
||||
it.presetMenu.visible = true
|
||||
}
|
||||
for it.commentEditor.Update(gtx, tr.InstrumentComment()) != EditorEventNone {
|
||||
tr.PatchPanel.instrList.instrumentDragList.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
func (it *InstrumentTools) Tags(level int, yield TagYieldFunc) bool {
|
||||
if it.commentExpanded.Value() {
|
||||
return yield(level+1, &it.commentEditor.widgetEditor)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -243,41 +210,12 @@ func MakeInstrList(model *tracker.Model) InstrumentList {
|
||||
return InstrumentList{
|
||||
instrumentDragList: NewDragList(model.Instruments().List(), layout.Horizontal),
|
||||
nameEditor: NewEditor(true, true, text.Middle),
|
||||
octave: NewNumericUpDownState(),
|
||||
enlargeBtn: new(Clickable),
|
||||
linkInstrTrackBtn: new(Clickable),
|
||||
newInstrumentBtn: new(Clickable),
|
||||
octaveHint: makeHint("Octave down", " (%s)", "OctaveNumberInputSubtract") + makeHint(" or up", " (%s)", "OctaveNumberInputAdd"),
|
||||
linkDisabledHint: makeHint("Instrument-Track\nlinking disabled", "\n(%s)", "LinkInstrTrackToggle"),
|
||||
linkEnabledHint: makeHint("Instrument-Track\nlinking enabled", "\n(%s)", "LinkInstrTrackToggle"),
|
||||
enlargeHint: makeHint("Enlarge", " (%s)", "InstrEnlargedToggle"),
|
||||
shrinkHint: makeHint("Shrink", " (%s)", "InstrEnlargedToggle"),
|
||||
addInstrumentHint: makeHint("Add\ninstrument", "\n(%s)", "AddInstrument"),
|
||||
}
|
||||
}
|
||||
|
||||
func (il *InstrumentList) Layout(gtx C) D {
|
||||
t := TrackerFromContext(gtx)
|
||||
il.update(gtx, t)
|
||||
octave := NumUpDown(t.Model.Octave(), t.Theme, t.OctaveNumberInput, "Octave")
|
||||
linkInstrTrackBtn := ToggleIconBtn(t.Model.LinkInstrTrack(), t.Theme, il.linkInstrTrackBtn, icons.NotificationSyncDisabled, icons.NotificationSync, il.linkDisabledHint, il.linkEnabledHint)
|
||||
instrEnlargedBtn := ToggleIconBtn(t.Model.InstrEnlarged(), t.Theme, il.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, il.enlargeHint, il.shrinkHint)
|
||||
addInstrumentBtn := ActionIconBtn(t.Model.AddInstrument(), t.Theme, il.newInstrumentBtn, icons.ContentAdd, il.addInstrumentHint)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(
|
||||
gtx,
|
||||
layout.Flexed(1, il.actualLayout),
|
||||
layout.Rigid(layout.Spacer{Width: 10}.Layout),
|
||||
layout.Rigid(Label(t.Theme, &t.Theme.InstrumentEditor.Octave, "Octave").Layout),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Rigid(octave.Layout),
|
||||
layout.Rigid(linkInstrTrackBtn.Layout),
|
||||
layout.Rigid(instrEnlargedBtn.Layout),
|
||||
layout.Rigid(addInstrumentBtn.Layout),
|
||||
)
|
||||
}
|
||||
|
||||
func (il *InstrumentList) actualLayout(gtx C) D {
|
||||
t := TrackerFromContext(gtx)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(36)
|
||||
gtx.Constraints.Min.Y = gtx.Dp(36)
|
||||
element := func(gtx C, i int) D {
|
||||
@ -340,7 +278,18 @@ func (il *InstrumentList) update(gtx C, t *Tracker) {
|
||||
if e, ok := event.(key.Event); ok && e.State == key.Press {
|
||||
switch e.Name {
|
||||
case key.NameDownArrow:
|
||||
t.PatchPanel.unitList.dragList.Focus()
|
||||
var tagged Tagged
|
||||
switch {
|
||||
case t.InstrComment().Value():
|
||||
tagged = &t.PatchPanel.instrProps
|
||||
case t.InstrPresets().Value():
|
||||
tagged = &t.PatchPanel.instrPresets
|
||||
default: // editor
|
||||
tagged = &t.PatchPanel.instrEditor
|
||||
}
|
||||
if tag, ok := firstTag(tagged); ok {
|
||||
gtx.Execute(key.FocusCmd{Tag: tag})
|
||||
}
|
||||
case key.NameReturn, key.NameEnter:
|
||||
il.nameEditor.Focus()
|
||||
}
|
||||
@ -351,132 +300,3 @@ func (il *InstrumentList) update(gtx C, t *Tracker) {
|
||||
func (il *InstrumentList) Tags(level int, yield TagYieldFunc) bool {
|
||||
return yield(level, il.instrumentDragList)
|
||||
}
|
||||
|
||||
// UnitList methods
|
||||
|
||||
func MakeUnitList(m *tracker.Model) UnitList {
|
||||
ret := UnitList{
|
||||
dragList: NewDragList(m.Units().List(), layout.Vertical),
|
||||
addUnitBtn: new(Clickable),
|
||||
searchEditor: NewEditor(true, true, text.Start),
|
||||
}
|
||||
ret.addUnitAction = tracker.MakeEnabledAction(tracker.DoFunc(func() {
|
||||
m.AddUnit(false).Do()
|
||||
ret.searchEditor.Focus()
|
||||
}))
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ul *UnitList) Layout(gtx C) D {
|
||||
t := TrackerFromContext(gtx)
|
||||
ul.update(gtx, t)
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints.Max.Y = gtx.Dp(20)
|
||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||
u := t.Units().Item(i)
|
||||
editorStyle := t.Theme.InstrumentEditor.UnitList.Name
|
||||
signalError := t.RailError()
|
||||
switch {
|
||||
case u.Disabled:
|
||||
editorStyle = t.Theme.InstrumentEditor.UnitList.NameDisabled
|
||||
case signalError.Err != nil && signalError.UnitIndex == i:
|
||||
editorStyle.Color = t.Theme.InstrumentEditor.UnitList.Error
|
||||
}
|
||||
unitName := func(gtx C) D {
|
||||
if i == ul.dragList.TrackerList.Selected() {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
return ul.searchEditor.Layout(gtx, t.Model.UnitSearch(), t.Theme, &editorStyle, "---")
|
||||
} else {
|
||||
text := u.Type
|
||||
if text == "" {
|
||||
text = "---"
|
||||
}
|
||||
l := editorStyle.AsLabelStyle()
|
||||
return Label(t.Theme, &l, text).Layout(gtx)
|
||||
}
|
||||
}
|
||||
stackText := strconv.FormatInt(int64(u.Signals.StackAfter()), 10)
|
||||
commentLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Comment, u.Comment)
|
||||
stackLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Stack, stackText)
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(unitName),
|
||||
layout.Rigid(layout.Spacer{Width: 5}.Layout),
|
||||
layout.Flexed(1, commentLabel.Layout),
|
||||
layout.Rigid(stackLabel.Layout),
|
||||
layout.Rigid(layout.Spacer{Width: 10}.Layout),
|
||||
)
|
||||
}
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
unitList := FilledDragList(t.Theme, ul.dragList)
|
||||
surface := func(gtx C) D {
|
||||
return layout.Stack{Alignment: layout.SE}.Layout(gtx,
|
||||
layout.Expanded(func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(140), gtx.Constraints.Max.Y))
|
||||
dims := unitList.Layout(gtx, element, nil)
|
||||
unitList.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)}
|
||||
addUnitBtn := IconBtn(t.Theme, &t.Theme.IconButton.Emphasis, ul.addUnitBtn, icons.ContentAdd, "Add unit (Enter)")
|
||||
return margin.Layout(gtx, addUnitBtn.Layout)
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Surface{Gray: 30, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, surface)
|
||||
}
|
||||
|
||||
func (ul *UnitList) update(gtx C, t *Tracker) {
|
||||
for ul.addUnitBtn.Clicked(gtx) {
|
||||
ul.addUnitAction.Do()
|
||||
t.UnitSearching().SetValue(true)
|
||||
ul.searchEditor.Focus()
|
||||
}
|
||||
for {
|
||||
event, ok := gtx.Event(
|
||||
key.Filter{Focus: ul.dragList, Name: key.NameRightArrow},
|
||||
key.Filter{Focus: ul.dragList, Name: key.NameEnter, Optional: key.ModCtrl},
|
||||
key.Filter{Focus: ul.dragList, Name: key.NameReturn, Optional: key.ModCtrl},
|
||||
key.Filter{Focus: ul.dragList, Name: key.NameDeleteBackward},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := event.(key.Event); ok && e.State == key.Press {
|
||||
switch e.Name {
|
||||
case key.NameRightArrow:
|
||||
t.PatchPanel.unitEditor.paramTable.RowTitleList.Focus()
|
||||
case key.NameDeleteBackward:
|
||||
t.Units().SetSelectedType("")
|
||||
t.UnitSearching().SetValue(true)
|
||||
ul.searchEditor.Focus()
|
||||
case key.NameEnter, key.NameReturn:
|
||||
t.Model.AddUnit(e.Modifiers.Contain(key.ModCtrl)).Do()
|
||||
t.UnitSearching().SetValue(true)
|
||||
ul.searchEditor.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
str := t.Model.UnitSearch()
|
||||
for ev := ul.searchEditor.Update(gtx, str); ev != EditorEventNone; ev = ul.searchEditor.Update(gtx, str) {
|
||||
if ev == EditorEventSubmit {
|
||||
if str.Value() != "" {
|
||||
for _, n := range sointu.UnitNames {
|
||||
if strings.HasPrefix(n, str.Value()) {
|
||||
t.Units().SetSelectedType(n)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Units().SetSelectedType("")
|
||||
}
|
||||
}
|
||||
ul.dragList.Focus()
|
||||
t.UnitSearching().SetValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (ul *UnitList) Tags(curLevel int, yield TagYieldFunc) bool {
|
||||
return yield(curLevel, ul.dragList) && yield(curLevel+1, &ul.searchEditor.widgetEditor)
|
||||
}
|
||||
|
||||
@ -19,6 +19,12 @@ type Theme struct {
|
||||
Text ButtonStyle
|
||||
Disabled ButtonStyle
|
||||
Menu ButtonStyle
|
||||
Tab struct {
|
||||
Active ButtonStyle
|
||||
Inactive ButtonStyle
|
||||
IndicatorHeight unit.Dp
|
||||
IndicatorColor color.NRGBA
|
||||
}
|
||||
}
|
||||
IconButton struct {
|
||||
Enabled IconButtonStyle
|
||||
@ -64,8 +70,10 @@ type Theme struct {
|
||||
Preset MenuStyle
|
||||
}
|
||||
InstrumentEditor struct {
|
||||
Octave LabelStyle
|
||||
Voices LabelStyle
|
||||
Octave LabelStyle
|
||||
Properties struct {
|
||||
Label LabelStyle
|
||||
}
|
||||
InstrumentComment EditorStyle
|
||||
UnitComment EditorStyle
|
||||
InstrumentList struct {
|
||||
@ -83,6 +91,15 @@ type Theme struct {
|
||||
Warning color.NRGBA
|
||||
Error color.NRGBA
|
||||
}
|
||||
Presets struct {
|
||||
SearchBg color.NRGBA
|
||||
Directory LabelStyle
|
||||
Results struct {
|
||||
Builtin LabelStyle
|
||||
User LabelStyle
|
||||
UserDir LabelStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
UnitEditor struct {
|
||||
Name LabelStyle
|
||||
|
||||
@ -54,6 +54,17 @@ button:
|
||||
cornerradius: 0
|
||||
height: *buttonheight
|
||||
inset: *buttoninset
|
||||
tab:
|
||||
active: *textbutton
|
||||
inactive:
|
||||
background: *transparentcolor
|
||||
color: *highemphasis
|
||||
textsize: *buttontextsize
|
||||
cornerradius: *buttoncornerradius
|
||||
height: *buttonheight
|
||||
inset: *buttoninset
|
||||
indicatorheight: 2
|
||||
indicatorcolor: *primarycolor
|
||||
iconbutton:
|
||||
enabled:
|
||||
color: *primarycolor
|
||||
@ -153,7 +164,8 @@ menu:
|
||||
height: 300
|
||||
instrumenteditor:
|
||||
octave: { textsize: 14, color: *disabled }
|
||||
voices: { textsize: 14, color: *disabled }
|
||||
properties:
|
||||
label: { textsize: 14, color: *highemphasis }
|
||||
instrumentcomment:
|
||||
{ textsize: 14, color: *highemphasis, hintcolor: *disabled }
|
||||
unitcomment: { textsize: 14, color: *highemphasis, hintcolor: *disabled }
|
||||
@ -178,6 +190,13 @@ instrumenteditor:
|
||||
disabled: { textsize: 12, color: *disabled }
|
||||
warning: *warningcolor
|
||||
error: *errorcolor
|
||||
presets:
|
||||
searchbg: { r: 255, g: 255, b: 255, a: 6 }
|
||||
directory: { textsize: 12, color: *white, maxlines: 1 }
|
||||
results:
|
||||
builtin: { textsize: 12, color: *white, maxlines: 1 }
|
||||
user: { textsize: 12, color: *secondarycolor, maxlines: 1 }
|
||||
userdir: { textsize: 12, color: *mediumemphasis, maxlines: 1 }
|
||||
cursor:
|
||||
active: { r: 100, g: 140, b: 255, a: 48 }
|
||||
activealt: { r: 255, g: 100, b: 140, a: 48 }
|
||||
|
||||
@ -316,6 +316,18 @@ func (t *Tracker) showDialog(gtx C) {
|
||||
DialogBtn("Close", t.Cancel()),
|
||||
)
|
||||
dialog.Layout(gtx)
|
||||
case tracker.DeleteUserPresetDialog:
|
||||
dialog := MakeDialog(t.Theme, t.DialogState, "Delete user preset?", "Are you sure you want to delete the selected user preset?\nThis action cannot be undone.",
|
||||
DialogBtn("Delete", t.DeleteUserPreset()),
|
||||
DialogBtn("Cancel", t.Cancel()),
|
||||
)
|
||||
dialog.Layout(gtx)
|
||||
case tracker.OverwriteUserPresetDialog:
|
||||
dialog := MakeDialog(t.Theme, t.DialogState, "Overwrite user preset?", "Are you sure you want to overwrite the existing user preset with the same name?",
|
||||
DialogBtn("Save", t.OverwriteUserPreset()),
|
||||
DialogBtn("Cancel", t.Cancel()),
|
||||
)
|
||||
dialog.Layout(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -55,7 +55,6 @@ type (
|
||||
OrderRows Model // OrderRows is a list of all the order rows, implementing ListData & MutableListData interfaces
|
||||
NoteRows Model // NoteRows is a list of all the note rows, implementing ListData & MutableListData interfaces
|
||||
SearchResults Model // SearchResults is a unmutable list of all the search results, implementing ListData interface
|
||||
Presets Model // Presets is a unmutable list of all the presets, implementing ListData interface
|
||||
)
|
||||
|
||||
// Model methods
|
||||
|
||||
@ -36,14 +36,15 @@ type (
|
||||
RecoveryFilePath string
|
||||
ChangedSinceRecovery bool
|
||||
SendSource int
|
||||
InstrumentTab InstrumentTab
|
||||
PresetSearchString string
|
||||
}
|
||||
|
||||
Model struct {
|
||||
d modelData
|
||||
derived derivedModelData
|
||||
|
||||
instrEnlarged bool
|
||||
commentExpanded bool
|
||||
instrEnlarged bool
|
||||
|
||||
prevUndoKind string
|
||||
undoSkipCounter int
|
||||
@ -84,6 +85,9 @@ type (
|
||||
broker *Broker
|
||||
|
||||
MIDI MIDIContext
|
||||
|
||||
presets Presets
|
||||
presetIndex int
|
||||
}
|
||||
|
||||
// Cursor identifies a row and a track in a song score.
|
||||
@ -129,6 +133,8 @@ type (
|
||||
String() string
|
||||
Open() error
|
||||
}
|
||||
|
||||
InstrumentTab int
|
||||
)
|
||||
|
||||
const (
|
||||
@ -159,6 +165,14 @@ const (
|
||||
QuitChanges
|
||||
QuitSaveExplorer
|
||||
License
|
||||
DeleteUserPresetDialog
|
||||
OverwriteUserPresetDialog
|
||||
)
|
||||
|
||||
const (
|
||||
InstrumentEditorTab InstrumentTab = iota
|
||||
InstrumentPresetsTab
|
||||
InstrumentCommentTab
|
||||
)
|
||||
|
||||
const maxUndo = 64
|
||||
@ -193,6 +207,8 @@ func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext
|
||||
TrySend(broker.ToPlayer, any(m.d.Song.Copy())) // we should be non-blocking in the constructor
|
||||
m.signalAnalyzer = NewScopeModel(broker, m.d.Song.BPM)
|
||||
m.updateDeriveData(SongChange)
|
||||
m.presets.load()
|
||||
m.updateDerivedPresetSearch()
|
||||
return m
|
||||
}
|
||||
|
||||
|
||||
@ -50,12 +50,14 @@ func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)
|
||||
s.IterateList("OrderRows", s.model.OrderRows().List(), yield, seed)
|
||||
s.IterateList("NoteRows", s.model.NoteRows().List(), yield, seed)
|
||||
s.IterateList("UnitSearchResults", s.model.SearchResults().List(), yield, seed)
|
||||
s.IterateList("PresetDirs", s.model.PresetDirList().List(), yield, seed)
|
||||
s.IterateList("PresetResults", s.model.PresetResultList().List(), yield, seed)
|
||||
// Bools
|
||||
s.IterateBool("Panic", s.model.Panic(), yield, seed)
|
||||
s.IterateBool("Recording", s.model.IsRecording(), yield, seed)
|
||||
s.IterateBool("Playing", s.model.Playing(), yield, seed)
|
||||
s.IterateBool("InstrEnlarged", s.model.InstrEnlarged(), yield, seed)
|
||||
s.IterateBool("Effect", s.model.Effect(), yield, seed)
|
||||
s.IterateBool("CommentExpanded", s.model.CommentExpanded(), yield, seed)
|
||||
s.IterateBool("Follow", s.model.Follow(), yield, seed)
|
||||
s.IterateBool("UniquePatterns", s.model.UniquePatterns(), yield, seed)
|
||||
s.IterateBool("LinkInstrTrack", s.model.LinkInstrTrack(), yield, seed)
|
||||
@ -88,8 +90,6 @@ func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)
|
||||
s.IterateAction("DeleteOrderRowBackward", s.model.DeleteOrderRow(true), yield, seed)
|
||||
s.IterateAction("SplitInstrument", s.model.SplitInstrument(), yield, seed)
|
||||
s.IterateAction("SplitTrack", s.model.SplitTrack(), yield, seed)
|
||||
// just test loading one of the presets
|
||||
s.IterateAction("LoadPreset", s.model.LoadPreset(seed%tracker.NumPresets()), yield, seed)
|
||||
// Tables
|
||||
s.IterateTable("Order", s.model.Order().Table(), yield, seed)
|
||||
s.IterateTable("Notes", s.model.Notes().Table(), yield, seed)
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@ -17,6 +18,9 @@ import (
|
||||
//go:generate go run generate/gmdls_entries.go
|
||||
//go:generate go run generate/clean_presets.go
|
||||
|
||||
//go:embed presets/*
|
||||
var instrumentPresetFS embed.FS
|
||||
|
||||
type (
|
||||
// GmDlsEntry is a single sample entry from the gm.dls file
|
||||
GmDlsEntry struct {
|
||||
@ -27,13 +31,390 @@ type (
|
||||
Name string // sample Name
|
||||
}
|
||||
|
||||
Preset struct {
|
||||
Directory string
|
||||
User bool
|
||||
NeedsGmDls bool
|
||||
Instr sointu.Instrument
|
||||
}
|
||||
|
||||
Presets struct {
|
||||
Presets []Preset
|
||||
Dirs []string
|
||||
}
|
||||
|
||||
InstrumentPresetYieldFunc func(index int, item string) (ok bool)
|
||||
LoadPreset struct {
|
||||
Index int
|
||||
*Model
|
||||
}
|
||||
|
||||
PresetSearchString Model
|
||||
NoGmDlsFilter Model
|
||||
BuiltinPresetsFilter Model
|
||||
UserPresetsFilter Model
|
||||
PresetDirectory Model
|
||||
PresetKind Model
|
||||
ClearPresetSearch Model
|
||||
PresetDirList Model
|
||||
PresetResultList Model
|
||||
SaveUserPreset Model
|
||||
TryDeleteUserPreset Model
|
||||
DeleteUserPreset Model
|
||||
|
||||
ConfirmDeleteUserPresetAction Model
|
||||
OverwriteUserPreset Model
|
||||
|
||||
derivedPresetSearch struct {
|
||||
dir string
|
||||
dirIndex int
|
||||
noGmDls bool
|
||||
kind PresetKindEnum
|
||||
searchStrings []string
|
||||
results []Preset
|
||||
}
|
||||
|
||||
PresetKindEnum int
|
||||
)
|
||||
|
||||
const (
|
||||
BuiltinPresets PresetKindEnum = -1
|
||||
AllPresets PresetKindEnum = 0
|
||||
UserPresets PresetKindEnum = 1
|
||||
)
|
||||
|
||||
func (m *Model) updateDerivedPresetSearch() {
|
||||
// reset derived data, keeping the
|
||||
str := m.derived.presetSearch.searchStrings[:0]
|
||||
m.derived.presetSearch = derivedPresetSearch{searchStrings: str, dirIndex: -1}
|
||||
// parse filters from the search string. in: dir, gmdls: yes/no, kind: builtin/user/all
|
||||
search := strings.TrimSpace(m.d.PresetSearchString)
|
||||
parts := strings.Fields(search)
|
||||
// parse parts to see if they contain :
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "d:") && len(part) > 2 {
|
||||
dir := strings.TrimSpace(part[2:])
|
||||
m.derived.presetSearch.dir = dir
|
||||
ind := slices.IndexFunc(m.presets.Dirs, func(c string) bool { return c == dir })
|
||||
m.derived.presetSearch.dirIndex = ind
|
||||
} else if strings.HasPrefix(part, "g:n") {
|
||||
m.derived.presetSearch.noGmDls = true
|
||||
} else if strings.HasPrefix(part, "t:") && len(part) > 2 {
|
||||
val := strings.TrimSpace(part[2:3])
|
||||
switch val {
|
||||
case "b":
|
||||
m.derived.presetSearch.kind = BuiltinPresets
|
||||
case "u":
|
||||
m.derived.presetSearch.kind = UserPresets
|
||||
}
|
||||
} else {
|
||||
m.derived.presetSearch.searchStrings = append(m.derived.presetSearch.searchStrings, strings.ToLower(part))
|
||||
}
|
||||
}
|
||||
// update results
|
||||
m.derived.presetSearch.results = m.derived.presetSearch.results[:0]
|
||||
for _, p := range m.presets.Presets {
|
||||
if m.derived.presetSearch.kind == BuiltinPresets && p.User {
|
||||
continue
|
||||
}
|
||||
if m.derived.presetSearch.kind == UserPresets && !p.User {
|
||||
continue
|
||||
}
|
||||
if m.derived.presetSearch.dir != "" && p.Directory != m.derived.presetSearch.dir {
|
||||
continue
|
||||
}
|
||||
if m.derived.presetSearch.noGmDls && p.NeedsGmDls {
|
||||
continue
|
||||
}
|
||||
if len(m.derived.presetSearch.searchStrings) == 0 {
|
||||
goto found
|
||||
}
|
||||
for _, s := range m.derived.presetSearch.searchStrings {
|
||||
if strings.Contains(strings.ToLower(p.Instr.Name), s) {
|
||||
goto found
|
||||
}
|
||||
}
|
||||
continue
|
||||
found:
|
||||
m.derived.presetSearch.results = append(m.derived.presetSearch.results, p)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Presets) load() {
|
||||
*m = Presets{}
|
||||
seenDir := make(map[string]bool)
|
||||
m.loadPresetsFromFs(instrumentPresetFS, false, seenDir)
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
userPresets := filepath.Join(configDir, "sointu")
|
||||
m.loadPresetsFromFs(os.DirFS(userPresets), true, seenDir)
|
||||
}
|
||||
sort.Sort(m)
|
||||
m.Dirs = make([]string, 0, len(seenDir))
|
||||
for k := range seenDir {
|
||||
m.Dirs = append(m.Dirs, k)
|
||||
}
|
||||
sort.Strings(m.Dirs)
|
||||
}
|
||||
|
||||
func (m *Presets) loadPresetsFromFs(fsys fs.FS, userDefined bool, seenDir map[string]bool) {
|
||||
fs.WalkDir(fsys, "presets", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := fs.ReadFile(fsys, path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var instr sointu.Instrument
|
||||
if yaml.UnmarshalStrict(data, &instr) == nil {
|
||||
noExt := path[:len(path)-len(filepath.Ext(path))]
|
||||
splitted := splitPath(noExt)
|
||||
splitted = splitted[1:] // remove "presets" from the path
|
||||
instr.Name = filenameToInstrumentName(splitted[len(splitted)-1])
|
||||
dir := strings.Join(splitted[:len(splitted)-1], "/")
|
||||
preset := Preset{
|
||||
Directory: dir,
|
||||
User: userDefined,
|
||||
Instr: instr,
|
||||
NeedsGmDls: checkNeedsGmDls(instr),
|
||||
}
|
||||
if dir != "" {
|
||||
seenDir[dir] = true
|
||||
}
|
||||
m.Presets = append(m.Presets, preset)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func filenameToInstrumentName(filename string) string {
|
||||
return strings.ReplaceAll(filename, "_", " ")
|
||||
}
|
||||
|
||||
func instrumentNameToFilename(name string) string {
|
||||
// remove all special characters
|
||||
reg, _ := regexp.Compile("[^a-zA-Z0-9 _]+")
|
||||
name = reg.ReplaceAllString(name, "")
|
||||
name = strings.ReplaceAll(name, " ", "_")
|
||||
return name
|
||||
}
|
||||
|
||||
func checkNeedsGmDls(instr sointu.Instrument) bool {
|
||||
for _, u := range instr.Units {
|
||||
if u.Type == "oscillator" {
|
||||
if u.Parameters["type"] == sointu.Sample {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Model) PresetSearchString() String { return MakeString((*PresetSearchString)(m)) }
|
||||
func (m *PresetSearchString) Value() string { return m.d.PresetSearchString }
|
||||
func (m *PresetSearchString) SetValue(value string) bool {
|
||||
if m.d.PresetSearchString == value {
|
||||
return false
|
||||
}
|
||||
m.d.PresetSearchString = value
|
||||
(*Model)(m).updateDerivedPresetSearch()
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Model) NoGmDls() Bool { return MakeBool((*NoGmDlsFilter)(m)) }
|
||||
func (m *NoGmDlsFilter) Value() bool { return m.derived.presetSearch.noGmDls }
|
||||
func (m *NoGmDlsFilter) SetValue(val bool) {
|
||||
if m.derived.presetSearch.noGmDls == val {
|
||||
return
|
||||
}
|
||||
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "g:")
|
||||
if val {
|
||||
m.d.PresetSearchString = "g:n " + m.d.PresetSearchString
|
||||
}
|
||||
(*Model)(m).updateDerivedPresetSearch()
|
||||
}
|
||||
func (m *NoGmDlsFilter) Enabled() bool { return true }
|
||||
|
||||
func (m *Model) UserPresetFilter() Bool { return MakeBool((*UserPresetsFilter)(m)) }
|
||||
func (m *UserPresetsFilter) Value() bool { return m.derived.presetSearch.kind == UserPresets }
|
||||
func (m *UserPresetsFilter) SetValue(val bool) {
|
||||
if (m.derived.presetSearch.kind == UserPresets) == val {
|
||||
return
|
||||
}
|
||||
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "t:")
|
||||
if val {
|
||||
m.d.PresetSearchString = "t:u " + m.d.PresetSearchString
|
||||
}
|
||||
(*Model)(m).updateDerivedPresetSearch()
|
||||
}
|
||||
func (m *UserPresetsFilter) Enabled() bool { return true }
|
||||
|
||||
func (m *Model) BuiltinPresetsFilter() Bool { return MakeBool((*BuiltinPresetsFilter)(m)) }
|
||||
func (m *BuiltinPresetsFilter) Value() bool { return m.derived.presetSearch.kind == BuiltinPresets }
|
||||
func (m *BuiltinPresetsFilter) SetValue(val bool) {
|
||||
if (m.derived.presetSearch.kind == BuiltinPresets) == val {
|
||||
return
|
||||
}
|
||||
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "t:")
|
||||
if val {
|
||||
m.d.PresetSearchString = "t:b " + m.d.PresetSearchString
|
||||
}
|
||||
(*Model)(m).updateDerivedPresetSearch()
|
||||
}
|
||||
func (m *BuiltinPresetsFilter) Enabled() bool { return true }
|
||||
|
||||
func (m *Model) ClearPresetSearch() Action { return MakeAction((*ClearPresetSearch)(m)) }
|
||||
func (m *ClearPresetSearch) Enabled() bool { return len(m.d.PresetSearchString) > 0 }
|
||||
func (m *ClearPresetSearch) Do() {
|
||||
m.d.PresetSearchString = ""
|
||||
(*Model)(m).updateDerivedPresetSearch()
|
||||
}
|
||||
|
||||
func (m *Model) PresetDirList() *PresetDirList { return (*PresetDirList)(m) }
|
||||
func (v *PresetDirList) List() List { return List{v} }
|
||||
func (m *PresetDirList) Count() int { return len(m.presets.Dirs) + 1 }
|
||||
func (m *PresetDirList) Selected() int { return m.derived.presetSearch.dirIndex + 1 }
|
||||
func (m *PresetDirList) Selected2() int { return m.derived.presetSearch.dirIndex + 1 }
|
||||
func (m *PresetDirList) SetSelected2(i int) {}
|
||||
func (m *PresetDirList) Value(i int) string {
|
||||
if i < 1 || i > len(m.presets.Dirs) {
|
||||
return "---"
|
||||
}
|
||||
return m.presets.Dirs[i-1]
|
||||
}
|
||||
func (m *PresetDirList) SetSelected(i int) {
|
||||
i = min(max(i, 0), len(m.presets.Dirs))
|
||||
if i < 0 || i > len(m.presets.Dirs) {
|
||||
return
|
||||
}
|
||||
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "d:")
|
||||
if i > 0 {
|
||||
m.d.PresetSearchString = "d:" + m.presets.Dirs[i-1] + " " + m.d.PresetSearchString
|
||||
}
|
||||
(*Model)(m).updateDerivedPresetSearch()
|
||||
}
|
||||
|
||||
func (m *Model) PresetResultList() *PresetResultList { return (*PresetResultList)(m) }
|
||||
func (v *PresetResultList) List() List { return List{v} }
|
||||
func (m *PresetResultList) Count() int { return len(m.derived.presetSearch.results) }
|
||||
func (m *PresetResultList) Selected() int {
|
||||
return min(max(m.presetIndex, 0), len(m.derived.presetSearch.results)-1)
|
||||
}
|
||||
func (m *PresetResultList) Selected2() int { return m.Selected() }
|
||||
func (m *PresetResultList) SetSelected2(i int) {}
|
||||
func (m *PresetResultList) Value(i int) (name string, dir string, user bool) {
|
||||
if i < 0 || i >= len(m.derived.presetSearch.results) {
|
||||
return "", "", false
|
||||
}
|
||||
p := m.derived.presetSearch.results[i]
|
||||
return p.Instr.Name, p.Directory, p.User
|
||||
}
|
||||
func (m *PresetResultList) SetSelected(i int) {
|
||||
i = min(max(i, 0), len(m.derived.presetSearch.results)-1)
|
||||
if i < 0 || i >= len(m.derived.presetSearch.results) {
|
||||
return
|
||||
}
|
||||
m.presetIndex = i
|
||||
defer (*Model)(m).change("LoadPreset", PatchChange, MinorChange)()
|
||||
if m.d.InstrIndex < 0 {
|
||||
m.d.InstrIndex = 0
|
||||
}
|
||||
m.d.InstrIndex2 = m.d.InstrIndex
|
||||
for m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
|
||||
}
|
||||
newInstr := m.derived.presetSearch.results[i].Instr.Copy()
|
||||
newInstr.NumVoices = clamp(m.d.Song.Patch[m.d.InstrIndex].NumVoices, 1, vm.MAX_VOICES)
|
||||
(*Model)(m).assignUnitIDs(newInstr.Units)
|
||||
m.d.Song.Patch[m.d.InstrIndex] = newInstr
|
||||
}
|
||||
|
||||
func removeFilters(str string, prefix string) string {
|
||||
parts := strings.Split(str, " ")
|
||||
newParts := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if !strings.HasPrefix(strings.ToLower(part), prefix) {
|
||||
newParts = append(newParts, part)
|
||||
}
|
||||
}
|
||||
return strings.Join(newParts, " ")
|
||||
}
|
||||
|
||||
func (m *Model) SaveAsUserPreset() Action { return MakeAction((*SaveUserPreset)(m)) }
|
||||
func (m *SaveUserPreset) Enabled() bool {
|
||||
return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch)
|
||||
}
|
||||
func (m *SaveUserPreset) Do() {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
userPresetsDir := filepath.Join(configDir, "sointu", "presets", m.derived.presetSearch.dir)
|
||||
instr := m.d.Song.Patch[m.d.InstrIndex]
|
||||
name := instrumentNameToFilename(instr.Name)
|
||||
fileName := filepath.Join(userPresetsDir, name+".yml")
|
||||
// if exists, do not overwrite
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
m.dialog = OverwriteUserPresetDialog
|
||||
return
|
||||
}
|
||||
(*Model)(m).OverwriteUserPreset().Do()
|
||||
}
|
||||
|
||||
func (m *Model) OverwriteUserPreset() Action { return MakeAction((*OverwriteUserPreset)(m)) }
|
||||
func (m *OverwriteUserPreset) Enabled() bool { return true }
|
||||
func (m *OverwriteUserPreset) Do() {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
userPresetsDir := filepath.Join(configDir, "sointu", "presets", m.derived.presetSearch.dir)
|
||||
instr := m.d.Song.Patch[m.d.InstrIndex]
|
||||
name := instrumentNameToFilename(instr.Name)
|
||||
fileName := filepath.Join(userPresetsDir, name+".yml")
|
||||
os.MkdirAll(userPresetsDir, 0755)
|
||||
data, err := yaml.Marshal(&instr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
os.WriteFile(fileName, data, 0644)
|
||||
m.dialog = NoDialog
|
||||
(*Model)(m).presets.load()
|
||||
(*Model)(m).updateDerivedPresetSearch()
|
||||
}
|
||||
|
||||
func (m *Model) TryDeleteUserPreset() Action { return MakeAction((*TryDeleteUserPreset)(m)) }
|
||||
func (m *TryDeleteUserPreset) Do() { m.dialog = DeleteUserPresetDialog }
|
||||
func (m *TryDeleteUserPreset) Enabled() bool {
|
||||
if m.presetIndex < 0 || m.presetIndex >= len(m.derived.presetSearch.results) {
|
||||
return false
|
||||
}
|
||||
return m.derived.presetSearch.results[m.presetIndex].User
|
||||
}
|
||||
|
||||
func (m *Model) DeleteUserPreset() Action { return MakeAction((*DeleteUserPreset)(m)) }
|
||||
func (m *DeleteUserPreset) Enabled() bool { return (*Model)(m).TryDeleteUserPreset().Enabled() }
|
||||
func (m *DeleteUserPreset) Do() {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p := m.derived.presetSearch.results[m.presetIndex]
|
||||
userPresetsDir := filepath.Join(configDir, "sointu", "presets")
|
||||
if p.Directory != "" {
|
||||
userPresetsDir = filepath.Join(userPresetsDir, p.Directory)
|
||||
}
|
||||
name := instrumentNameToFilename(p.Instr.Name)
|
||||
fileName := filepath.Join(userPresetsDir, name+".yml")
|
||||
os.Remove(fileName)
|
||||
m.dialog = NoDialog
|
||||
(*Model)(m).presets.load()
|
||||
(*Model)(m).updateDerivedPresetSearch()
|
||||
}
|
||||
|
||||
// gmDlsEntryMap is a reverse map, to find the index of the GmDlsEntry in the
|
||||
// GmDlsEntries list based on the sample offset. Do not modify during runtime.
|
||||
var gmDlsEntryMap = make(map[vm.SampleOffset]int)
|
||||
@ -130,97 +511,6 @@ type delayPreset struct {
|
||||
varArgs []int
|
||||
}
|
||||
|
||||
func (m *Model) IterateInstrumentPresets(yield InstrumentPresetYieldFunc) {
|
||||
for index, instr := range instrumentPresets {
|
||||
if !yield(index, instr.Name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NumPresets() int {
|
||||
return len(instrumentPresets)
|
||||
}
|
||||
|
||||
// LoadPreset loads a preset from the list of instrument presets. The index
|
||||
// should be within the range of 0 to NumPresets()-1.
|
||||
|
||||
func (m *Model) LoadPreset(index int) Action {
|
||||
return MakeEnabledAction(LoadPreset{Index: index, Model: m})
|
||||
}
|
||||
func (m LoadPreset) Do() {
|
||||
defer m.change("LoadPreset", PatchChange, MajorChange)()
|
||||
if m.d.InstrIndex < 0 {
|
||||
m.d.InstrIndex = 0
|
||||
}
|
||||
m.d.InstrIndex2 = m.d.InstrIndex
|
||||
for m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
|
||||
}
|
||||
newInstr := instrumentPresets[m.Index].Copy()
|
||||
newInstr.NumVoices = clamp(m.d.Song.Patch[m.d.InstrIndex].NumVoices, 1, vm.MAX_VOICES)
|
||||
m.Model.assignUnitIDs(newInstr.Units)
|
||||
m.d.Song.Patch[m.d.InstrIndex] = newInstr
|
||||
}
|
||||
|
||||
type instrumentPresetsSlice []sointu.Instrument
|
||||
|
||||
//go:embed presets/*
|
||||
var instrumentPresetFS embed.FS
|
||||
var instrumentPresets instrumentPresetsSlice
|
||||
|
||||
func init() {
|
||||
fs.WalkDir(instrumentPresetFS, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := fs.ReadFile(instrumentPresetFS, path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var instr sointu.Instrument
|
||||
if yaml.UnmarshalStrict(data, &instr) == nil {
|
||||
noExt := path[:len(path)-len(filepath.Ext(path))]
|
||||
splitted := splitPath(noExt)
|
||||
splitted = splitted[1:] // remove "presets" from the path
|
||||
instr.Name = strings.Join(splitted, " ")
|
||||
instrumentPresets = append(instrumentPresets, instr)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
userPresets := filepath.Join(configDir, "sointu", "presets")
|
||||
filepath.WalkDir(userPresets, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var instr sointu.Instrument
|
||||
if yaml.Unmarshal(data, &instr) == nil {
|
||||
if len(userPresets)+1 > len(path) {
|
||||
return nil
|
||||
}
|
||||
subPath := path[len(userPresets)+1:]
|
||||
noExt := subPath[:len(subPath)-len(filepath.Ext(subPath))]
|
||||
splitted := splitPath(noExt)
|
||||
instr.Name = strings.Join(splitted, " ")
|
||||
instrumentPresets = append(instrumentPresets, instr)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
sort.Sort(instrumentPresets)
|
||||
}
|
||||
|
||||
func splitPath(path string) []string {
|
||||
subPath := path
|
||||
var result []string
|
||||
@ -246,6 +536,11 @@ func splitPath(path string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
func (p instrumentPresetsSlice) Len() int { return len(p) }
|
||||
func (p instrumentPresetsSlice) Less(i, j int) bool { return p[i].Name < p[j].Name }
|
||||
func (p instrumentPresetsSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
func (p Presets) Len() int { return len(p.Presets) }
|
||||
func (p Presets) Less(i, j int) bool {
|
||||
if p.Presets[i].Instr.Name == p.Presets[j].Instr.Name {
|
||||
return p.Presets[i].User && !p.Presets[j].User
|
||||
}
|
||||
return p.Presets[i].Instr.Name < p.Presets[j].Instr.Name
|
||||
}
|
||||
func (p Presets) Swap(i, j int) { p.Presets[i], p.Presets[j] = p.Presets[j], p.Presets[i] }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user