mirror of
https://github.com/vsariola/sointu.git
synced 2026-06-13 17:29:10 -04:00
feat(tracker): add a preset explorer with search and filters
Closes #91
This commit is contained in:
parent
3f365707c2
commit
2336a135c6
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
|
- Preset explorer, whichs allows 1) searching the presets by name; 2) filtering
|
||||||
|
them by category (directory); 3) filtering them by being builtin vs. user;
|
||||||
|
4) filtering them if they need gm.dls (for Linux/Mac users, who don't have
|
||||||
|
it); and 5) saving and deleting user presets.
|
||||||
- Panic the synth if it outputs NaN or Inf, and handle these more gracefully in
|
- Panic the synth if it outputs NaN or Inf, and handle these more gracefully in
|
||||||
the loudness and peak detector. ([#210][i210])
|
the loudness and peak detector. ([#210][i210])
|
||||||
- More presets from Reaby, and all new and existing presets were normalized
|
- More presets from Reaby, and all new and existing presets were normalized
|
||||||
|
|||||||
@@ -11,22 +11,24 @@ type (
|
|||||||
SetValue(bool)
|
SetValue(bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
Panic Model
|
Panic Model
|
||||||
IsRecording Model
|
IsRecording Model
|
||||||
Playing Model
|
Playing Model
|
||||||
InstrEnlarged Model
|
InstrEnlarged Model
|
||||||
Effect Model
|
Effect Model
|
||||||
TrackMidiIn Model
|
TrackMidiIn Model
|
||||||
CommentExpanded Model
|
Follow Model
|
||||||
Follow Model
|
UnitSearching Model
|
||||||
UnitSearching Model
|
UnitDisabled Model
|
||||||
UnitDisabled Model
|
LoopToggle Model
|
||||||
LoopToggle Model
|
UniquePatterns Model
|
||||||
UniquePatterns Model
|
Mute Model
|
||||||
Mute Model
|
Solo Model
|
||||||
Solo Model
|
LinkInstrTrack Model
|
||||||
LinkInstrTrack Model
|
Oversampling Model
|
||||||
Oversampling Model
|
InstrEditor Model
|
||||||
|
InstrPresets Model
|
||||||
|
InstrComment Model
|
||||||
)
|
)
|
||||||
|
|
||||||
func MakeBool(valueEnabler interface {
|
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) Value() bool { return m.instrEnlarged }
|
||||||
func (m *InstrEnlarged) SetValue(val bool) { m.instrEnlarged = val }
|
func (m *InstrEnlarged) SetValue(val bool) { m.instrEnlarged = val }
|
||||||
|
|
||||||
// CommentExpanded methods
|
// InstrEditor methods
|
||||||
|
|
||||||
func (m *Model) CommentExpanded() Bool { return MakeEnabledBool((*CommentExpanded)(m)) }
|
func (m *Model) InstrEditor() Bool { return MakeEnabledBool((*InstrEditor)(m)) }
|
||||||
func (m *CommentExpanded) Value() bool { return m.commentExpanded }
|
func (m *InstrEditor) Value() bool { return m.d.InstrumentTab == InstrumentEditorTab }
|
||||||
func (m *CommentExpanded) SetValue(val bool) { m.commentExpanded = val }
|
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
|
// Follow methods
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ type (
|
|||||||
// corresponding part of the model changes.
|
// corresponding part of the model changes.
|
||||||
derivedModelData struct {
|
derivedModelData struct {
|
||||||
// map Unit by ID, other entities by their respective index
|
// map Unit by ID, other entities by their respective index
|
||||||
patch []derivedInstrument
|
patch []derivedInstrument
|
||||||
tracks []derivedTrack
|
tracks []derivedTrack
|
||||||
railError RailError
|
railError RailError
|
||||||
|
presetSearch derivedPresetSearch
|
||||||
}
|
}
|
||||||
|
|
||||||
derivedInstrument struct {
|
derivedInstrument struct {
|
||||||
|
|||||||
@@ -186,8 +186,5 @@ success:
|
|||||||
instrument.NumVoices = clamp(instrument.NumVoices, 1, 32-numVoices)
|
instrument.NumVoices = clamp(instrument.NumVoices, 1, 32-numVoices)
|
||||||
m.assignUnitIDs(instrument.Units)
|
m.assignUnitIDs(instrument.Units)
|
||||||
m.d.Song.Patch[m.d.InstrIndex] = instrument
|
m.d.Song.Patch[m.d.InstrIndex] = instrument
|
||||||
if m.d.Song.Patch[m.d.InstrIndex].Comment != "" {
|
|
||||||
m.commentExpanded = true
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ type (
|
|||||||
Button
|
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 is a button with an icon.
|
||||||
IconButton struct {
|
IconButton struct {
|
||||||
Theme *Theme
|
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 {
|
func IconBtn(th *Theme, st *IconButtonStyle, c *Clickable, icon []byte, tip string) IconButton {
|
||||||
return IconButton{
|
return IconButton{
|
||||||
Theme: th,
|
Theme: th,
|
||||||
@@ -288,6 +308,26 @@ func (b *ToggleIconButton) Layout(gtx C) D {
|
|||||||
return b.IconButton.Layout(gtx)
|
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.
|
// Click executes a simple programmatic click.
|
||||||
func (b *Clickable) Click() {
|
func (b *Clickable) Click() {
|
||||||
b.requestClicks++
|
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() {
|
if e.widgetEditor.Text() != str.Value() {
|
||||||
e.widgetEditor.SetText(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 := material.Editor(&th.Material, &e.widgetEditor, hint)
|
||||||
me.Font = style.Font
|
me.Font = style.Font
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TagYieldFunc func(level int, tag event.Tag) bool
|
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
|
// 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
|
// 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
|
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"
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
@@ -16,6 +18,8 @@ import (
|
|||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
"gioui.org/op/paint"
|
"gioui.org/op/paint"
|
||||||
"gioui.org/text"
|
"gioui.org/text"
|
||||||
|
"gioui.org/unit"
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
@@ -23,6 +27,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
InstrumentEditor struct {
|
||||||
|
unitList UnitList
|
||||||
|
unitEditor UnitEditor
|
||||||
|
}
|
||||||
|
|
||||||
|
UnitList struct {
|
||||||
|
dragList *DragList
|
||||||
|
searchEditor *Editor
|
||||||
|
addUnitBtn *Clickable
|
||||||
|
addUnitAction tracker.Action
|
||||||
|
}
|
||||||
|
|
||||||
UnitEditor struct {
|
UnitEditor struct {
|
||||||
paramTable *ScrollTable
|
paramTable *ScrollTable
|
||||||
searchList *DragList
|
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 {
|
func NewUnitEditor(m *tracker.Model) *UnitEditor {
|
||||||
ret := &UnitEditor{
|
ret := &UnitEditor{
|
||||||
DeleteUnitBtn: new(Clickable),
|
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 {
|
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||||
switch e.Name {
|
switch e.Name {
|
||||||
case key.NameLeftArrow:
|
case key.NameLeftArrow:
|
||||||
t.PatchPanel.unitList.dragList.Focus()
|
t.PatchPanel.instrEditor.unitList.dragList.Focus()
|
||||||
case key.NameDeleteBackward:
|
case key.NameDeleteBackward:
|
||||||
t.ClearUnit().Do()
|
t.ClearUnit().Do()
|
||||||
t.UnitSearch().SetValue("")
|
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"
|
"strings"
|
||||||
|
|
||||||
"gioui.org/io/clipboard"
|
"gioui.org/io/clipboard"
|
||||||
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
@@ -208,8 +209,6 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
|||||||
t.InstrEnlarged().Toggle()
|
t.InstrEnlarged().Toggle()
|
||||||
case "LinkInstrTrackToggle":
|
case "LinkInstrTrackToggle":
|
||||||
t.LinkInstrTrack().Toggle()
|
t.LinkInstrTrack().Toggle()
|
||||||
case "CommentExpandedToggle":
|
|
||||||
t.CommentExpanded().Toggle()
|
|
||||||
case "FollowToggle":
|
case "FollowToggle":
|
||||||
t.Follow().Toggle()
|
t.Follow().Toggle()
|
||||||
case "UnitDisabledToggle":
|
case "UnitDisabledToggle":
|
||||||
@@ -259,13 +258,20 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
|||||||
case "Paste":
|
case "Paste":
|
||||||
gtx.Execute(clipboard.ReadCmd{Tag: t})
|
gtx.Execute(clipboard.ReadCmd{Tag: t})
|
||||||
case "OrderEditorFocus":
|
case "OrderEditorFocus":
|
||||||
|
t.InstrEnlarged().SetValue(false)
|
||||||
gtx.Execute(key.FocusCmd{Tag: t.OrderEditor.scrollTable})
|
gtx.Execute(key.FocusCmd{Tag: t.OrderEditor.scrollTable})
|
||||||
case "TrackEditorFocus":
|
case "TrackEditorFocus":
|
||||||
|
t.InstrEnlarged().SetValue(false)
|
||||||
gtx.Execute(key.FocusCmd{Tag: t.TrackEditor.scrollTable})
|
gtx.Execute(key.FocusCmd{Tag: t.TrackEditor.scrollTable})
|
||||||
case "InstrumentListFocus":
|
case "InstrumentListFocus":
|
||||||
gtx.Execute(key.FocusCmd{Tag: t.PatchPanel.instrList.instrumentDragList})
|
gtx.Execute(key.FocusCmd{Tag: t.PatchPanel.instrList.instrumentDragList})
|
||||||
case "UnitListFocus":
|
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":
|
case "FocusPrev":
|
||||||
t.FocusPrev(gtx, false)
|
t.FocusPrev(gtx, false)
|
||||||
case "FocusPrevInto":
|
case "FocusPrevInto":
|
||||||
|
|||||||
@@ -6,32 +6,42 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gioui.org/io/clipboard"
|
"gioui.org/io/clipboard"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op"
|
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
"gioui.org/text"
|
"gioui.org/text"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"github.com/vsariola/sointu"
|
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
PatchPanel struct {
|
PatchPanel struct {
|
||||||
instrList InstrumentList
|
instrList InstrumentList
|
||||||
tools InstrumentTools
|
tools InstrumentTools
|
||||||
unitList UnitList
|
instrProps InstrumentProperties
|
||||||
unitEditor UnitEditor
|
instrPresets InstrumentPresets
|
||||||
|
instrEditor InstrumentEditor
|
||||||
|
*tracker.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
InstrumentList struct {
|
InstrumentList struct {
|
||||||
instrumentDragList *DragList
|
instrumentDragList *DragList
|
||||||
nameEditor *Editor
|
nameEditor *Editor
|
||||||
|
}
|
||||||
|
|
||||||
|
InstrumentTools struct {
|
||||||
|
EditorTab *Clickable
|
||||||
|
PresetsTab *Clickable
|
||||||
|
CommentTab *Clickable
|
||||||
|
|
||||||
|
saveInstrumentBtn *Clickable
|
||||||
|
loadInstrumentBtn *Clickable
|
||||||
|
copyInstrumentBtn *Clickable
|
||||||
|
deleteInstrumentBtn *Clickable
|
||||||
|
|
||||||
octave *NumericUpDownState
|
octave *NumericUpDownState
|
||||||
enlargeBtn *Clickable
|
enlargeBtn *Clickable
|
||||||
@@ -43,69 +53,58 @@ type (
|
|||||||
linkEnabledHint string
|
linkEnabledHint string
|
||||||
enlargeHint, shrinkHint string
|
enlargeHint, shrinkHint string
|
||||||
addInstrumentHint 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
|
deleteInstrumentHint string
|
||||||
}
|
}
|
||||||
|
|
||||||
UnitList struct {
|
|
||||||
dragList *DragList
|
|
||||||
searchEditor *Editor
|
|
||||||
addUnitBtn *Clickable
|
|
||||||
addUnitAction tracker.Action
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PatchPanel methods
|
// PatchPanel methods
|
||||||
|
|
||||||
func NewPatchPanel(model *tracker.Model) *PatchPanel {
|
func NewPatchPanel(model *tracker.Model) *PatchPanel {
|
||||||
return &PatchPanel{
|
return &PatchPanel{
|
||||||
instrList: MakeInstrList(model),
|
instrEditor: MakeInstrumentEditor(model),
|
||||||
tools: MakeInstrumentTools(model),
|
instrList: MakeInstrList(model),
|
||||||
unitList: MakeUnitList(model),
|
tools: MakeInstrumentTools(model),
|
||||||
unitEditor: *NewUnitEditor(model),
|
instrProps: *NewInstrumentProperties(),
|
||||||
|
instrPresets: *NewInstrumentPresets(model),
|
||||||
|
Model: model,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *PatchPanel) Layout(gtx C) D {
|
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,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(pp.instrList.Layout),
|
layout.Rigid(pp.instrList.Layout),
|
||||||
layout.Rigid(pp.tools.Layout),
|
layout.Rigid(pp.tools.Layout),
|
||||||
layout.Flexed(1, func(gtx C) D {
|
layout.Flexed(1, bottom),
|
||||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
)
|
||||||
layout.Rigid(pp.unitList.Layout),
|
}
|
||||||
layout.Flexed(1, pp.unitEditor.Layout),
|
|
||||||
)
|
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 {
|
func (pp *PatchPanel) Tags(level int, yield TagYieldFunc) bool {
|
||||||
return pp.instrList.Tags(level, yield) &&
|
return pp.instrList.Tags(level, yield) &&
|
||||||
pp.tools.Tags(level, yield) &&
|
pp.tools.Tags(level, yield) &&
|
||||||
pp.unitList.Tags(level, yield) &&
|
pp.BottomTags(level, yield)
|
||||||
pp.unitEditor.Tags(level, yield)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TreeFocused returns true if any of the tags in the patch panel is focused
|
// 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 {
|
func MakeInstrumentTools(m *tracker.Model) InstrumentTools {
|
||||||
ret := InstrumentTools{
|
ret := InstrumentTools{
|
||||||
Voices: NewNumericUpDownState(),
|
EditorTab: new(Clickable),
|
||||||
|
PresetsTab: new(Clickable),
|
||||||
|
CommentTab: new(Clickable),
|
||||||
deleteInstrumentBtn: new(Clickable),
|
deleteInstrumentBtn: new(Clickable),
|
||||||
splitInstrumentBtn: new(Clickable),
|
|
||||||
copyInstrumentBtn: new(Clickable),
|
copyInstrumentBtn: new(Clickable),
|
||||||
saveInstrumentBtn: new(Clickable),
|
saveInstrumentBtn: new(Clickable),
|
||||||
loadInstrumentBtn: 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"),
|
deleteInstrumentHint: makeHint("Delete\ninstrument", "\n(%s)", "DeleteInstrument"),
|
||||||
muteHint: makeHint("Mute", " (%s)", "MuteToggle"),
|
octave: NewNumericUpDownState(),
|
||||||
unmuteHint: makeHint("Unmute", " (%s)", "MuteToggle"),
|
enlargeBtn: new(Clickable),
|
||||||
soloHint: makeHint("Solo", " (%s)", "SoloToggle"),
|
linkInstrTrackBtn: new(Clickable),
|
||||||
unsoloHint: makeHint("Unsolo", " (%s)", "SoloToggle"),
|
newInstrumentBtn: new(Clickable),
|
||||||
splitInstrumentHint: makeHint("Split instrument", " (%s)", "SplitInstrument"),
|
octaveHint: makeHint("Octave down", " (%s)", "OctaveNumberInputSubtract") + makeHint(" or up", " (%s)", "OctaveNumberInputAdd"),
|
||||||
}
|
linkDisabledHint: makeHint("Instrument-Track\nlinking disabled", "\n(%s)", "LinkInstrTrackToggle"),
|
||||||
for index, name := range m.IterateInstrumentPresets {
|
linkEnabledHint: makeHint("Instrument-Track\nlinking enabled", "\n(%s)", "LinkInstrTrackToggle"),
|
||||||
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem(m.LoadPreset(index), name, "", icons.ImageAudiotrack))
|
enlargeHint: makeHint("Enlarge", " (%s)", "InstrEnlargedToggle"),
|
||||||
|
shrinkHint: makeHint("Shrink", " (%s)", "InstrEnlargedToggle"),
|
||||||
|
addInstrumentHint: makeHint("Add\ninstrument", "\n(%s)", "AddInstrument"),
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
@@ -150,55 +143,38 @@ func MakeInstrumentTools(m *tracker.Model) InstrumentTools {
|
|||||||
func (it *InstrumentTools) Layout(gtx C) D {
|
func (it *InstrumentTools) Layout(gtx C) D {
|
||||||
t := TrackerFromContext(gtx)
|
t := TrackerFromContext(gtx)
|
||||||
it.update(gtx, t)
|
it.update(gtx, t)
|
||||||
voicesLabel := Label(t.Theme, &t.Theme.InstrumentEditor.Voices, "Voices")
|
editorBtn := TabBtn(t.Model.InstrEditor(), t.Theme, it.EditorTab, "Editor", "")
|
||||||
splitInstrumentBtn := ActionIconBtn(t.SplitInstrument(), t.Theme, it.splitInstrumentBtn, icons.CommunicationCallSplit, it.splitInstrumentHint)
|
presetsBtn := TabBtn(t.Model.InstrPresets(), t.Theme, it.PresetsTab, "Presets", "")
|
||||||
commentExpandedBtn := ToggleIconBtn(t.CommentExpanded(), t.Theme, it.commentExpandBtn, icons.NavigationExpandMore, icons.NavigationExpandLess, it.expandCommentHint, it.collapseCommentHint)
|
commentBtn := TabBtn(t.Model.InstrComment(), t.Theme, it.CommentTab, "Properties", "")
|
||||||
soloBtn := ToggleIconBtn(t.Solo(), t.Theme, it.soloBtn, icons.SocialGroup, icons.SocialPerson, it.soloHint, it.unsoloHint)
|
octave := NumUpDown(t.Model.Octave(), t.Theme, t.OctaveNumberInput, "Octave")
|
||||||
muteBtn := ToggleIconBtn(t.Mute(), t.Theme, it.muteBtn, icons.AVVolumeUp, icons.AVVolumeOff, it.muteHint, it.unmuteHint)
|
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")
|
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")
|
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")
|
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)
|
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 {
|
btns := func(gtx C) D {
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Rigid(layout.Spacer{Width: 6}.Layout),
|
layout.Rigid(layout.Spacer{Width: 6}.Layout),
|
||||||
layout.Rigid(voicesLabel.Layout),
|
layout.Rigid(editorBtn.Layout),
|
||||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
layout.Rigid(presetsBtn.Layout),
|
||||||
layout.Rigid(instrumentVoices.Layout),
|
layout.Rigid(commentBtn.Layout),
|
||||||
layout.Rigid(splitInstrumentBtn.Layout),
|
|
||||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||||
layout.Rigid(commentExpandedBtn.Layout),
|
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||||
layout.Rigid(soloBtn.Layout),
|
layout.Rigid(Label(t.Theme, &t.Theme.InstrumentEditor.Octave, "Octave").Layout),
|
||||||
layout.Rigid(muteBtn.Layout),
|
layout.Rigid(octave.Layout),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(linkInstrTrackBtn.Layout),
|
||||||
presetBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, it.presetMenuBtn, icons.NavigationMenu, "Load preset")
|
layout.Rigid(instrEnlargedBtn.Layout),
|
||||||
dims := presetBtn.Layout(gtx)
|
layout.Rigid(copyInstrumentBtn.Layout),
|
||||||
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(saveInstrumentBtn.Layout),
|
layout.Rigid(saveInstrumentBtn.Layout),
|
||||||
layout.Rigid(loadInstrumentBtn.Layout),
|
layout.Rigid(loadInstrumentBtn.Layout),
|
||||||
layout.Rigid(copyInstrumentBtn.Layout),
|
|
||||||
layout.Rigid(deleteInstrumentBtn.Layout),
|
layout.Rigid(deleteInstrumentBtn.Layout),
|
||||||
|
layout.Rigid(addInstrumentBtn.Layout),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
comment := func(gtx C) D {
|
return Surface{Gray: 37, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, btns)
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *InstrumentTools) update(gtx C, tr *Tracker) {
|
func (it *InstrumentTools) update(gtx C, tr *Tracker) {
|
||||||
@@ -222,18 +198,9 @@ func (it *InstrumentTools) update(gtx C, tr *Tracker) {
|
|||||||
}
|
}
|
||||||
tr.LoadInstrument(reader)
|
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 {
|
func (it *InstrumentTools) Tags(level int, yield TagYieldFunc) bool {
|
||||||
if it.commentExpanded.Value() {
|
|
||||||
return yield(level+1, &it.commentEditor.widgetEditor)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,41 +210,12 @@ func MakeInstrList(model *tracker.Model) InstrumentList {
|
|||||||
return InstrumentList{
|
return InstrumentList{
|
||||||
instrumentDragList: NewDragList(model.Instruments().List(), layout.Horizontal),
|
instrumentDragList: NewDragList(model.Instruments().List(), layout.Horizontal),
|
||||||
nameEditor: NewEditor(true, true, text.Middle),
|
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 {
|
func (il *InstrumentList) Layout(gtx C) D {
|
||||||
t := TrackerFromContext(gtx)
|
t := TrackerFromContext(gtx)
|
||||||
il.update(gtx, t)
|
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.Max.Y = gtx.Dp(36)
|
||||||
gtx.Constraints.Min.Y = gtx.Dp(36)
|
gtx.Constraints.Min.Y = gtx.Dp(36)
|
||||||
element := func(gtx C, i int) D {
|
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 {
|
if e, ok := event.(key.Event); ok && e.State == key.Press {
|
||||||
switch e.Name {
|
switch e.Name {
|
||||||
case key.NameDownArrow:
|
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:
|
case key.NameReturn, key.NameEnter:
|
||||||
il.nameEditor.Focus()
|
il.nameEditor.Focus()
|
||||||
}
|
}
|
||||||
@@ -351,132 +300,3 @@ func (il *InstrumentList) update(gtx C, t *Tracker) {
|
|||||||
func (il *InstrumentList) Tags(level int, yield TagYieldFunc) bool {
|
func (il *InstrumentList) Tags(level int, yield TagYieldFunc) bool {
|
||||||
return yield(level, il.instrumentDragList)
|
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
|
Text ButtonStyle
|
||||||
Disabled ButtonStyle
|
Disabled ButtonStyle
|
||||||
Menu ButtonStyle
|
Menu ButtonStyle
|
||||||
|
Tab struct {
|
||||||
|
Active ButtonStyle
|
||||||
|
Inactive ButtonStyle
|
||||||
|
IndicatorHeight unit.Dp
|
||||||
|
IndicatorColor color.NRGBA
|
||||||
|
}
|
||||||
}
|
}
|
||||||
IconButton struct {
|
IconButton struct {
|
||||||
Enabled IconButtonStyle
|
Enabled IconButtonStyle
|
||||||
@@ -64,8 +70,10 @@ type Theme struct {
|
|||||||
Preset MenuStyle
|
Preset MenuStyle
|
||||||
}
|
}
|
||||||
InstrumentEditor struct {
|
InstrumentEditor struct {
|
||||||
Octave LabelStyle
|
Octave LabelStyle
|
||||||
Voices LabelStyle
|
Properties struct {
|
||||||
|
Label LabelStyle
|
||||||
|
}
|
||||||
InstrumentComment EditorStyle
|
InstrumentComment EditorStyle
|
||||||
UnitComment EditorStyle
|
UnitComment EditorStyle
|
||||||
InstrumentList struct {
|
InstrumentList struct {
|
||||||
@@ -83,6 +91,15 @@ type Theme struct {
|
|||||||
Warning color.NRGBA
|
Warning color.NRGBA
|
||||||
Error color.NRGBA
|
Error color.NRGBA
|
||||||
}
|
}
|
||||||
|
Presets struct {
|
||||||
|
SearchBg color.NRGBA
|
||||||
|
Directory LabelStyle
|
||||||
|
Results struct {
|
||||||
|
Builtin LabelStyle
|
||||||
|
User LabelStyle
|
||||||
|
UserDir LabelStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
UnitEditor struct {
|
UnitEditor struct {
|
||||||
Name LabelStyle
|
Name LabelStyle
|
||||||
|
|||||||
@@ -54,6 +54,17 @@ button:
|
|||||||
cornerradius: 0
|
cornerradius: 0
|
||||||
height: *buttonheight
|
height: *buttonheight
|
||||||
inset: *buttoninset
|
inset: *buttoninset
|
||||||
|
tab:
|
||||||
|
active: *textbutton
|
||||||
|
inactive:
|
||||||
|
background: *transparentcolor
|
||||||
|
color: *highemphasis
|
||||||
|
textsize: *buttontextsize
|
||||||
|
cornerradius: *buttoncornerradius
|
||||||
|
height: *buttonheight
|
||||||
|
inset: *buttoninset
|
||||||
|
indicatorheight: 2
|
||||||
|
indicatorcolor: *primarycolor
|
||||||
iconbutton:
|
iconbutton:
|
||||||
enabled:
|
enabled:
|
||||||
color: *primarycolor
|
color: *primarycolor
|
||||||
@@ -153,7 +164,8 @@ menu:
|
|||||||
height: 300
|
height: 300
|
||||||
instrumenteditor:
|
instrumenteditor:
|
||||||
octave: { textsize: 14, color: *disabled }
|
octave: { textsize: 14, color: *disabled }
|
||||||
voices: { textsize: 14, color: *disabled }
|
properties:
|
||||||
|
label: { textsize: 14, color: *highemphasis }
|
||||||
instrumentcomment:
|
instrumentcomment:
|
||||||
{ textsize: 14, color: *highemphasis, hintcolor: *disabled }
|
{ textsize: 14, color: *highemphasis, hintcolor: *disabled }
|
||||||
unitcomment: { textsize: 14, color: *highemphasis, hintcolor: *disabled }
|
unitcomment: { textsize: 14, color: *highemphasis, hintcolor: *disabled }
|
||||||
@@ -178,6 +190,13 @@ instrumenteditor:
|
|||||||
disabled: { textsize: 12, color: *disabled }
|
disabled: { textsize: 12, color: *disabled }
|
||||||
warning: *warningcolor
|
warning: *warningcolor
|
||||||
error: *errorcolor
|
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:
|
cursor:
|
||||||
active: { r: 100, g: 140, b: 255, a: 48 }
|
active: { r: 100, g: 140, b: 255, a: 48 }
|
||||||
activealt: { r: 255, g: 100, b: 140, 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()),
|
DialogBtn("Close", t.Cancel()),
|
||||||
)
|
)
|
||||||
dialog.Layout(gtx)
|
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
|
OrderRows Model // OrderRows is a list of all the order rows, implementing ListData & MutableListData interfaces
|
||||||
NoteRows Model // NoteRows is a list of all the note rows, implementing ListData & MutableListData interfaces
|
NoteRows Model // NoteRows is a list of all the note rows, implementing ListData & MutableListData interfaces
|
||||||
SearchResults Model // SearchResults is a unmutable list of all the search results, implementing ListData interface
|
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
|
// Model methods
|
||||||
|
|||||||
@@ -36,14 +36,15 @@ type (
|
|||||||
RecoveryFilePath string
|
RecoveryFilePath string
|
||||||
ChangedSinceRecovery bool
|
ChangedSinceRecovery bool
|
||||||
SendSource int
|
SendSource int
|
||||||
|
InstrumentTab InstrumentTab
|
||||||
|
PresetSearchString string
|
||||||
}
|
}
|
||||||
|
|
||||||
Model struct {
|
Model struct {
|
||||||
d modelData
|
d modelData
|
||||||
derived derivedModelData
|
derived derivedModelData
|
||||||
|
|
||||||
instrEnlarged bool
|
instrEnlarged bool
|
||||||
commentExpanded bool
|
|
||||||
|
|
||||||
prevUndoKind string
|
prevUndoKind string
|
||||||
undoSkipCounter int
|
undoSkipCounter int
|
||||||
@@ -84,6 +85,9 @@ type (
|
|||||||
broker *Broker
|
broker *Broker
|
||||||
|
|
||||||
MIDI MIDIContext
|
MIDI MIDIContext
|
||||||
|
|
||||||
|
presets Presets
|
||||||
|
presetIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor identifies a row and a track in a song score.
|
// Cursor identifies a row and a track in a song score.
|
||||||
@@ -129,6 +133,8 @@ type (
|
|||||||
String() string
|
String() string
|
||||||
Open() error
|
Open() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InstrumentTab int
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -159,6 +165,14 @@ const (
|
|||||||
QuitChanges
|
QuitChanges
|
||||||
QuitSaveExplorer
|
QuitSaveExplorer
|
||||||
License
|
License
|
||||||
|
DeleteUserPresetDialog
|
||||||
|
OverwriteUserPresetDialog
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
InstrumentEditorTab InstrumentTab = iota
|
||||||
|
InstrumentPresetsTab
|
||||||
|
InstrumentCommentTab
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxUndo = 64
|
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
|
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.signalAnalyzer = NewScopeModel(broker, m.d.Song.BPM)
|
||||||
m.updateDeriveData(SongChange)
|
m.updateDeriveData(SongChange)
|
||||||
|
m.presets.load()
|
||||||
|
m.updateDerivedPresetSearch()
|
||||||
return m
|
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("OrderRows", s.model.OrderRows().List(), yield, seed)
|
||||||
s.IterateList("NoteRows", s.model.NoteRows().List(), yield, seed)
|
s.IterateList("NoteRows", s.model.NoteRows().List(), yield, seed)
|
||||||
s.IterateList("UnitSearchResults", s.model.SearchResults().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("Panic", s.model.Panic(), yield, seed)
|
||||||
s.IterateBool("Recording", s.model.IsRecording(), yield, seed)
|
s.IterateBool("Recording", s.model.IsRecording(), yield, seed)
|
||||||
s.IterateBool("Playing", s.model.Playing(), yield, seed)
|
s.IterateBool("Playing", s.model.Playing(), yield, seed)
|
||||||
s.IterateBool("InstrEnlarged", s.model.InstrEnlarged(), yield, seed)
|
s.IterateBool("InstrEnlarged", s.model.InstrEnlarged(), yield, seed)
|
||||||
s.IterateBool("Effect", s.model.Effect(), 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("Follow", s.model.Follow(), yield, seed)
|
||||||
s.IterateBool("UniquePatterns", s.model.UniquePatterns(), yield, seed)
|
s.IterateBool("UniquePatterns", s.model.UniquePatterns(), yield, seed)
|
||||||
s.IterateBool("LinkInstrTrack", s.model.LinkInstrTrack(), 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("DeleteOrderRowBackward", s.model.DeleteOrderRow(true), yield, seed)
|
||||||
s.IterateAction("SplitInstrument", s.model.SplitInstrument(), yield, seed)
|
s.IterateAction("SplitInstrument", s.model.SplitInstrument(), yield, seed)
|
||||||
s.IterateAction("SplitTrack", s.model.SplitTrack(), 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
|
// Tables
|
||||||
s.IterateTable("Order", s.model.Order().Table(), yield, seed)
|
s.IterateTable("Order", s.model.Order().Table(), yield, seed)
|
||||||
s.IterateTable("Notes", s.model.Notes().Table(), yield, seed)
|
s.IterateTable("Notes", s.model.Notes().Table(), yield, seed)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -17,6 +18,9 @@ import (
|
|||||||
//go:generate go run generate/gmdls_entries.go
|
//go:generate go run generate/gmdls_entries.go
|
||||||
//go:generate go run generate/clean_presets.go
|
//go:generate go run generate/clean_presets.go
|
||||||
|
|
||||||
|
//go:embed presets/*
|
||||||
|
var instrumentPresetFS embed.FS
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// GmDlsEntry is a single sample entry from the gm.dls file
|
// GmDlsEntry is a single sample entry from the gm.dls file
|
||||||
GmDlsEntry struct {
|
GmDlsEntry struct {
|
||||||
@@ -27,13 +31,390 @@ type (
|
|||||||
Name string // sample Name
|
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)
|
InstrumentPresetYieldFunc func(index int, item string) (ok bool)
|
||||||
LoadPreset struct {
|
LoadPreset struct {
|
||||||
Index int
|
Index int
|
||||||
*Model
|
*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
|
// 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.
|
// GmDlsEntries list based on the sample offset. Do not modify during runtime.
|
||||||
var gmDlsEntryMap = make(map[vm.SampleOffset]int)
|
var gmDlsEntryMap = make(map[vm.SampleOffset]int)
|
||||||
@@ -130,97 +511,6 @@ type delayPreset struct {
|
|||||||
varArgs []int
|
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 {
|
func splitPath(path string) []string {
|
||||||
subPath := path
|
subPath := path
|
||||||
var result []string
|
var result []string
|
||||||
@@ -246,6 +536,11 @@ func splitPath(path string) []string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p instrumentPresetsSlice) Len() int { return len(p) }
|
func (p Presets) Len() int { return len(p.Presets) }
|
||||||
func (p instrumentPresetsSlice) Less(i, j int) bool { return p[i].Name < p[j].Name }
|
func (p Presets) Less(i, j int) bool {
|
||||||
func (p instrumentPresetsSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
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