From 57aef3bed3de194172b9733f456708228455bda7 Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Wed, 15 Oct 2025 12:19:25 +0300 Subject: [PATCH] drafting preset explorer --- tracker/bool.go | 62 ++++++++---- tracker/files.go | 3 - tracker/gioui/buttons.go | 40 ++++++++ tracker/gioui/keybindings.go | 2 - tracker/gioui/patch_panel.go | 187 +++++------------------------------ tracker/gioui/theme.go | 6 ++ tracker/gioui/theme.yml | 11 +++ tracker/gioui/unit_editor.go | 140 ++++++++++++++++++++++++++ tracker/model.go | 12 ++- tracker/model_test.go | 1 - 10 files changed, 274 insertions(+), 190 deletions(-) diff --git a/tracker/bool.go b/tracker/bool.go index 3cca3ca..46b91b8 100644 --- a/tracker/bool.go +++ b/tracker/bool.go @@ -11,22 +11,24 @@ type ( SetValue(bool) } - Panic Model - IsRecording Model - Playing Model - InstrEnlarged Model - Effect Model - TrackMidiIn Model - CommentExpanded Model - Follow Model - UnitSearching Model - UnitDisabled Model - LoopToggle Model - UniquePatterns Model - Mute Model - Solo Model - LinkInstrTrack Model - Oversampling Model + Panic Model + IsRecording Model + Playing Model + InstrEnlarged Model + Effect Model + TrackMidiIn Model + Follow Model + UnitSearching Model + UnitDisabled Model + LoopToggle Model + UniquePatterns Model + Mute Model + Solo Model + LinkInstrTrack Model + Oversampling Model + InstrEditor Model + InstrPresets Model + InstrComment Model ) func MakeBool(valueEnabler interface { @@ -101,11 +103,31 @@ func (m *Model) InstrEnlarged() Bool { return MakeEnabledBool((*InstrEnlar func (m *InstrEnlarged) Value() bool { return m.instrEnlarged } func (m *InstrEnlarged) SetValue(val bool) { m.instrEnlarged = val } -// CommentExpanded methods +// InstrEditor methods -func (m *Model) CommentExpanded() Bool { return MakeEnabledBool((*CommentExpanded)(m)) } -func (m *CommentExpanded) Value() bool { return m.commentExpanded } -func (m *CommentExpanded) SetValue(val bool) { m.commentExpanded = val } +func (m *Model) InstrEditor() Bool { return MakeEnabledBool((*InstrEditor)(m)) } +func (m *InstrEditor) Value() bool { return m.d.InstrumentTab == InstrumentEditorTab } +func (m *InstrEditor) SetValue(val bool) { + if val { + m.d.InstrumentTab = InstrumentEditorTab + } +} + +func (m *Model) InstrComment() Bool { return MakeEnabledBool((*InstrComment)(m)) } +func (m *InstrComment) Value() bool { return m.d.InstrumentTab == InstrumentCommentTab } +func (m *InstrComment) SetValue(val bool) { + if val { + m.d.InstrumentTab = InstrumentCommentTab + } +} + +func (m *Model) InstrPresets() Bool { return MakeEnabledBool((*InstrPresets)(m)) } +func (m *InstrPresets) Value() bool { return m.d.InstrumentTab == InstrumentPresetsTab } +func (m *InstrPresets) SetValue(val bool) { + if val { + m.d.InstrumentTab = InstrumentPresetsTab + } +} // Follow methods diff --git a/tracker/files.go b/tracker/files.go index 8eb6347..0d1c4d3 100644 --- a/tracker/files.go +++ b/tracker/files.go @@ -186,8 +186,5 @@ success: instrument.NumVoices = clamp(instrument.NumVoices, 1, 32-numVoices) m.assignUnitIDs(instrument.Units) m.d.Song.Patch[m.d.InstrIndex] = instrument - if m.d.Song.Patch[m.d.InstrIndex].Comment != "" { - m.commentExpanded = true - } return true } diff --git a/tracker/gioui/buttons.go b/tracker/gioui/buttons.go index cc73fe8..1420487 100644 --- a/tracker/gioui/buttons.go +++ b/tracker/gioui/buttons.go @@ -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++ diff --git a/tracker/gioui/keybindings.go b/tracker/gioui/keybindings.go index 605f8c5..a2115dc 100644 --- a/tracker/gioui/keybindings.go +++ b/tracker/gioui/keybindings.go @@ -208,8 +208,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": diff --git a/tracker/gioui/patch_panel.go b/tracker/gioui/patch_panel.go index 55b19ae..ec7db4d 100644 --- a/tracker/gioui/patch_panel.go +++ b/tracker/gioui/patch_panel.go @@ -6,17 +6,14 @@ 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" ) @@ -46,6 +43,10 @@ type ( } InstrumentTools struct { + EditorTab *Clickable + PresetsTab *Clickable + CommentTab *Clickable + Voices *NumericUpDownState splitInstrumentBtn *Clickable commentExpandBtn *Clickable @@ -60,8 +61,6 @@ type ( copyInstrumentBtn *Clickable deleteInstrumentBtn *Clickable - commentExpanded tracker.Bool - muteHint, unmuteHint string soloHint, unsoloHint string expandCommentHint string @@ -69,13 +68,6 @@ type ( splitInstrumentHint string deleteInstrumentHint string } - - UnitList struct { - dragList *DragList - searchEditor *Editor - addUnitBtn *Clickable - addUnitAction tracker.Action - } ) // PatchPanel methods @@ -119,6 +111,9 @@ func (pp *PatchPanel) TreeFocused(gtx C) bool { func MakeInstrumentTools(m *tracker.Model) InstrumentTools { ret := InstrumentTools{ + EditorTab: new(Clickable), + PresetsTab: new(Clickable), + CommentTab: new(Clickable), Voices: NewNumericUpDownState(), deleteInstrumentBtn: new(Clickable), splitInstrumentBtn: new(Clickable), @@ -131,7 +126,6 @@ func MakeInstrumentTools(m *tracker.Model) InstrumentTools { 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"), @@ -151,27 +145,33 @@ func (it *InstrumentTools) Layout(gtx C) D { t := TrackerFromContext(gtx) it.update(gtx, t) voicesLabel := Label(t.Theme, &t.Theme.InstrumentEditor.Voices, "Voices") + + 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, "Comment", "") + 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) - 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") + // 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(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(voicesLabel.Layout), layout.Rigid(layout.Spacer{Width: 4}.Layout), layout.Rigid(instrumentVoices.Layout), layout.Rigid(splitInstrumentBtn.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 { + /*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) @@ -179,26 +179,21 @@ func (it *InstrumentTools) Layout(gtx C) D { m.Style = &t.Theme.Menu.Preset m.Layout(gtx, it.presetMenuItems...) return dims - }), - layout.Rigid(saveInstrumentBtn.Layout), - layout.Rigid(loadInstrumentBtn.Layout), + }),*/ + // layout.Rigid(saveInstrumentBtn.Layout), + // layout.Rigid(loadInstrumentBtn.Layout), layout.Rigid(copyInstrumentBtn.Layout), layout.Rigid(deleteInstrumentBtn.Layout), ) } - comment := func(gtx C) D { + /*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) { @@ -231,9 +226,6 @@ func (it *InstrumentTools) update(gtx C, tr *Tracker) { } func (it *InstrumentTools) Tags(level int, yield TagYieldFunc) bool { - if it.commentExpanded.Value() { - return yield(level+1, &it.commentEditor.widgetEditor) - } return true } @@ -351,132 +343,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) -} diff --git a/tracker/gioui/theme.go b/tracker/gioui/theme.go index 4b5f788..3c06194 100644 --- a/tracker/gioui/theme.go +++ b/tracker/gioui/theme.go @@ -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 diff --git a/tracker/gioui/theme.yml b/tracker/gioui/theme.yml index 1ad253c..ce53f97 100644 --- a/tracker/gioui/theme.yml +++ b/tracker/gioui/theme.yml @@ -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 diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index 25067ec..588667b 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -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,13 @@ import ( ) type ( + UnitList struct { + dragList *DragList + searchEditor *Editor + addUnitBtn *Clickable + addUnitAction tracker.Action + } + UnitEditor struct { paramTable *ScrollTable searchList *DragList @@ -43,6 +54,135 @@ type ( } ) +// 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) +} + func NewUnitEditor(m *tracker.Model) *UnitEditor { ret := &UnitEditor{ DeleteUnitBtn: new(Clickable), diff --git a/tracker/model.go b/tracker/model.go index 92d5719..d3a0948 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -36,14 +36,14 @@ type ( RecoveryFilePath string ChangedSinceRecovery bool SendSource int + InstrumentTab InstrumentTab } Model struct { d modelData derived derivedModelData - instrEnlarged bool - commentExpanded bool + instrEnlarged bool prevUndoKind string undoSkipCounter int @@ -129,6 +129,8 @@ type ( String() string Open() error } + + InstrumentTab int ) const ( @@ -161,6 +163,12 @@ const ( License ) +const ( + InstrumentEditorTab InstrumentTab = iota + InstrumentPresetsTab + InstrumentCommentTab +) + const maxUndo = 64 func (m *Model) PlayPosition() sointu.SongPos { return m.playerStatus.SongPos } diff --git a/tracker/model_test.go b/tracker/model_test.go index ab8de5e..41d85eb 100644 --- a/tracker/model_test.go +++ b/tracker/model_test.go @@ -55,7 +55,6 @@ func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T) s.IterateBool("Playing", s.model.Playing(), yield, seed) s.IterateBool("InstrEnlarged", s.model.InstrEnlarged(), yield, seed) s.IterateBool("Effect", s.model.Effect(), yield, seed) - s.IterateBool("CommentExpanded", s.model.CommentExpanded(), yield, seed) s.IterateBool("Follow", s.model.Follow(), yield, seed) s.IterateBool("UniquePatterns", s.model.UniquePatterns(), yield, seed) s.IterateBool("LinkInstrTrack", s.model.LinkInstrTrack(), yield, seed)