feat(tracker): add a preset explorer with search and filters

Closes #91
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-10-15 12:19:25 +03:00
parent 3f365707c2
commit 2336a135c6
128 changed files with 1147 additions and 405 deletions

View File

@ -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++

View File

@ -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

View File

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

View File

@ -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("")

View 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),
)
})
}

View 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),
)
}

View File

@ -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":

View File

@ -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)
}

View File

@ -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

View File

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

View File

@ -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)
}
}