diff --git a/CHANGELOG.md b/CHANGELOG.md index 76797b7..3ffcec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added +- Tabbing works more consistently, with widgets placed in a "tree", and plain + Tab moves to the next widget on the same level or more shallow in the tree, + while ctrl-Tab moves to next widget, regardless of its depth. This allows the + user to quickly move between different panels, but also tabbing into every + tiny widget if needed. Shift-* tab backwards. - Help menu, with a menu item to show the license in a dialog, and also menu items to open manual, Github Discussions & Github Issues in a browser ([#196][i196]) diff --git a/tracker/gioui/dialog.go b/tracker/gioui/dialog.go index cd572bc..bacf859 100644 --- a/tracker/gioui/dialog.go +++ b/tracker/gioui/dialog.go @@ -74,7 +74,7 @@ func DialogBtn(text string, action tracker.Action) DialogButton { func (d *Dialog) Layout(gtx C) D { anyFocused := false for i := 0; i < d.NumBtns; i++ { - anyFocused = anyFocused || gtx.Source.Focused(&d.State.Clickables[i]) + anyFocused = anyFocused || gtx.Focused(&d.State.Clickables[i]) } if !anyFocused { gtx.Execute(key.FocusCmd{Tag: &d.State.Clickables[d.NumBtns-1]}) diff --git a/tracker/gioui/draglist.go b/tracker/gioui/draglist.go index 3d33e0a..3725021 100644 --- a/tracker/gioui/draglist.go +++ b/tracker/gioui/draglist.go @@ -56,10 +56,6 @@ func (d *DragList) Focus() { d.requestFocus = true } -func (d *DragList) Focused(gtx C) bool { - return gtx.Focused(d) -} - func (s FilledDragListStyle) LayoutScrollBar(gtx C) D { return s.dragList.ScrollBar.Layout(gtx, &s.ScrollBar, s.dragList.TrackerList.Count(), &s.dragList.List.Position) } @@ -117,7 +113,7 @@ func (s FilledDragListStyle) Layout(gtx C, element, bg func(gtx C, i int) D) D { s.dragList.TrackerList.SetSelected2(s.dragList.TrackerList.Selected()) } case key.Event: - if !s.dragList.Focused(gtx) || ke.State != key.Press { + if ke.State != key.Press { break } s.dragList.command(gtx, ke) diff --git a/tracker/gioui/focus.go b/tracker/gioui/focus.go new file mode 100644 index 0000000..c722baa --- /dev/null +++ b/tracker/gioui/focus.go @@ -0,0 +1,79 @@ +package gioui + +import ( + "math" + + "gioui.org/io/event" + "gioui.org/io/key" +) + +type TagYieldFunc func(level int, tag event.Tag) 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 +// focus the next tag at the current level or shallower. +func (t *Tracker) FocusNext(gtx C, stepInto bool) { + _, next := t.findPrevNext(gtx, stepInto) + if next != nil { + gtx.Execute(key.FocusCmd{Tag: next}) + } +} + +// FocusPrev navigates to the previous focusable tag in the tracker. If stepInto +// is true, it will focus the previous tag regardless of its depth; otherwise it +// will focus the previous tag at the current level or shallower. +func (t *Tracker) FocusPrev(gtx C, stepInto bool) { + prev, _ := t.findPrevNext(gtx, stepInto) + if prev != nil { + gtx.Execute(key.FocusCmd{Tag: prev}) + } +} + +func (t *Tracker) findPrevNext(gtx C, stepInto bool) (prev, next event.Tag) { + var first, last event.Tag + found := false + maxLevel := math.MaxInt + if !stepInto { + if level, ok := t.findFocusedLevel(gtx); ok { + maxLevel = level // limit to the current focused tag's level + } + } + t.Tags(0, func(l int, t event.Tag) bool { + if l > maxLevel || t == nil { + return true // skip tags that are too deep or nils + } + if first == nil { + first = t + } + if found && next == nil { + next = t + } + if gtx.Focused(t) { + found = true + } + if !found { + prev = t + } + last = t + return true + }) + if next == nil { + next = first + } + if prev == nil { + prev = last + } + return prev, next +} + +func (t *Tracker) findFocusedLevel(gtx C) (level int, ok bool) { + t.Tags(0, func(l int, t event.Tag) bool { + if gtx.Focused(t) { + level = l + ok = true + return false // stop when we find the focused tag + } + return true // continue searching + }) + return level, ok +} diff --git a/tracker/gioui/instrument_editor.go b/tracker/gioui/instrument_editor.go deleted file mode 100644 index b06414a..0000000 --- a/tracker/gioui/instrument_editor.go +++ /dev/null @@ -1,480 +0,0 @@ -package gioui - -import ( - "bytes" - "fmt" - "image" - "image/color" - "io" - "strconv" - "strings" - - "gioui.org/io/clipboard" - "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 ( - InstrumentEditor struct { - newInstrumentBtn *Clickable - enlargeBtn *Clickable - deleteInstrumentBtn *Clickable - linkInstrTrackBtn *Clickable - splitInstrumentBtn *Clickable - copyInstrumentBtn *Clickable - saveInstrumentBtn *Clickable - loadInstrumentBtn *Clickable - addUnitBtn *Clickable - presetMenuBtn *Clickable - commentExpandBtn *Clickable - soloBtn *Clickable - muteBtn *Clickable - commentEditor *Editor - nameEditor *Editor - searchEditor *Editor - instrumentDragList *DragList - unitDragList *DragList - unitEditor *UnitEditor - wasFocused bool - presetMenuItems []ActionMenuItem - presetMenu MenuState - - addUnit tracker.Action - - enlargeHint, shrinkHint string - addInstrumentHint string - octaveHint string - expandCommentHint string - collapseCommentHint string - deleteInstrumentHint string - muteHint, unmuteHint string - soloHint, unsoloHint string - linkDisabledHint string - linkEnabledHint string - splitInstrumentHint string - } - - AddUnitThenFocus InstrumentEditor -) - -func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor { - ret := &InstrumentEditor{ - newInstrumentBtn: new(Clickable), - enlargeBtn: new(Clickable), - deleteInstrumentBtn: new(Clickable), - linkInstrTrackBtn: 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), - addUnitBtn: new(Clickable), - commentEditor: NewEditor(false, false, text.Start), - nameEditor: NewEditor(true, true, text.Middle), - searchEditor: NewEditor(true, true, text.Start), - instrumentDragList: NewDragList(model.Instruments().List(), layout.Horizontal), - unitDragList: NewDragList(model.Units().List(), layout.Vertical), - unitEditor: NewUnitEditor(model), - presetMenuItems: []ActionMenuItem{}, - } - model.IterateInstrumentPresets(func(index int, name string) bool { - ret.presetMenuItems = append(ret.presetMenuItems, ActionMenuItem{Text: name, Icon: icons.ImageAudiotrack, Action: model.LoadPreset(index)}) - return true - }) - ret.addUnit = model.AddUnit(false) - ret.enlargeHint = makeHint("Enlarge", " (%s)", "InstrEnlargedToggle") - ret.shrinkHint = makeHint("Shrink", " (%s)", "InstrEnlargedToggle") - ret.addInstrumentHint = makeHint("Add\ninstrument", "\n(%s)", "AddInstrument") - ret.octaveHint = makeHint("Octave down", " (%s)", "OctaveNumberInputSubtract") + makeHint(" or up", " (%s)", "OctaveNumberInputAdd") - ret.expandCommentHint = makeHint("Expand comment", " (%s)", "CommentExpandedToggle") - ret.collapseCommentHint = makeHint("Collapse comment", " (%s)", "CommentExpandedToggle") - ret.deleteInstrumentHint = makeHint("Delete\ninstrument", "\n(%s)", "DeleteInstrument") - ret.muteHint = makeHint("Mute", " (%s)", "MuteToggle") - ret.unmuteHint = makeHint("Unmute", " (%s)", "MuteToggle") - ret.soloHint = makeHint("Solo", " (%s)", "SoloToggle") - ret.unsoloHint = makeHint("Unsolo", " (%s)", "SoloToggle") - ret.linkDisabledHint = makeHint("Instrument-Track\nlinking disabled", "\n(%s)", "LinkInstrTrackToggle") - ret.linkEnabledHint = makeHint("Instrument-Track\nlinking enabled", "\n(%s)", "LinkInstrTrackToggle") - ret.splitInstrumentHint = makeHint("Split instrument", " (%s)", "SplitInstrument") - return ret -} - -func (ie *InstrumentEditor) AddUnitThenFocus() tracker.Action { - return tracker.MakeAction((*AddUnitThenFocus)(ie)) -} -func (a *AddUnitThenFocus) Enabled() bool { return a.addUnit.Enabled() } -func (a *AddUnitThenFocus) Do() { - a.addUnit.Do() - a.searchEditor.Focus() -} - -func (ie *InstrumentEditor) Focus() { - ie.unitDragList.Focus() -} - -func (ie *InstrumentEditor) Focused(gtx C) bool { - return gtx.Focused(ie.unitDragList) -} - -func (ie *InstrumentEditor) childFocused(gtx C) bool { - return ie.unitEditor.sliderList.Focused(gtx) || - ie.instrumentDragList.Focused(gtx) || gtx.Source.Focused(ie.commentEditor) || gtx.Source.Focused(ie.nameEditor) || gtx.Source.Focused(ie.searchEditor) || - gtx.Source.Focused(ie.addUnitBtn) || gtx.Source.Focused(ie.commentExpandBtn) || gtx.Source.Focused(ie.presetMenuBtn) || - gtx.Source.Focused(ie.deleteInstrumentBtn) || gtx.Source.Focused(ie.copyInstrumentBtn) -} - -func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D { - ie.wasFocused = ie.Focused(gtx) || ie.childFocused(gtx) - - octave := func(gtx C) D { - in := layout.UniformInset(unit.Dp(1)) - octave := NumUpDown(t.Model.Octave(), t.Theme, t.OctaveNumberInput, "Octave") - return in.Layout(gtx, octave.Layout) - } - - ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout( - gtx, - layout.Flexed(1, func(gtx C) D { - return ie.layoutInstrumentList(gtx, t) - }), - 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.Rigid(func(gtx C) D { - linkInstrTrackBtn := ToggleIconBtn(t.Model.LinkInstrTrack(), t.Theme, ie.linkInstrTrackBtn, icons.NotificationSyncDisabled, icons.NotificationSync, ie.linkDisabledHint, ie.linkEnabledHint) - return layout.E.Layout(gtx, linkInstrTrackBtn.Layout) - }), - layout.Rigid(func(gtx C) D { - instrEnlargedBtn := ToggleIconBtn(t.Model.InstrEnlarged(), t.Theme, ie.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, ie.enlargeHint, ie.shrinkHint) - return layout.E.Layout(gtx, instrEnlargedBtn.Layout) - }), - layout.Rigid(func(gtx C) D { - addInstrumentBtn := ActionIconBtn(t.Model.AddInstrument(), t.Theme, ie.newInstrumentBtn, icons.ContentAdd, ie.addInstrumentHint) - return layout.E.Layout(gtx, addInstrumentBtn.Layout) - }), - ) - }), - layout.Rigid(func(gtx C) D { - return ie.layoutInstrumentHeader(gtx, t) - }), - layout.Flexed(1, func(gtx C) D { - return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return ie.layoutUnitList(gtx, t) - }), - layout.Flexed(1, func(gtx C) D { - return ie.unitEditor.Layout(gtx, t) - }), - ) - })) - return ret -} - -func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D { - header := func(gtx C) D { - for ie.copyInstrumentBtn.Clicked(gtx) { - if contents, ok := t.Instruments().List().CopyElements(); ok { - gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))}) - t.Alerts().Add("Instrument copied to clipboard", tracker.Info) - } - } - - for ie.saveInstrumentBtn.Clicked(gtx) { - writer, err := t.Explorer.CreateFile(t.InstrumentName().Value() + ".yml") - if err != nil { - continue - } - t.SaveInstrument(writer) - } - - for ie.loadInstrumentBtn.Clicked(gtx) { - reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp") - if err != nil { - continue - } - t.LoadInstrument(reader) - } - - splitInstrumentBtn := ActionIconBtn(t.SplitInstrument(), t.Theme, ie.splitInstrumentBtn, icons.CommunicationCallSplit, ie.splitInstrumentHint) - commentExpandedBtn := ToggleIconBtn(t.CommentExpanded(), t.Theme, ie.commentExpandBtn, icons.NavigationExpandMore, icons.NavigationExpandLess, ie.expandCommentHint, ie.collapseCommentHint) - soloBtn := ToggleIconBtn(t.Solo(), t.Theme, ie.soloBtn, icons.SocialGroup, icons.SocialPerson, ie.soloHint, ie.unsoloHint) - muteBtn := ToggleIconBtn(t.Mute(), t.Theme, ie.muteBtn, icons.AVVolumeUp, icons.AVVolumeOff, ie.muteHint, ie.unmuteHint) - saveInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, ie.saveInstrumentBtn, icons.ContentSave, "Save instrument") - loadInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, ie.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument") - copyInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, ie.copyInstrumentBtn, icons.ContentContentCopy, "Copy instrument") - deleteInstrumentBtn := ActionIconBtn(t.DeleteInstrument(), t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, ie.deleteInstrumentHint) - instrumentVoices := NumUpDown(t.Model.InstrumentVoices(), t.Theme, t.InstrumentVoices, "Number of voices for this instrument") - - header := func(gtx C) D { - return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(layout.Spacer{Width: 6}.Layout), - layout.Rigid(Label(t.Theme, &t.Theme.InstrumentEditor.Voices, "Voices").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 { - presetBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, ie.presetMenuBtn, icons.NavigationMenu, "Load preset") - dims := presetBtn.Layout(gtx) - op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops) - m := Menu(t.Theme, &ie.presetMenu) - m.Style = &t.Theme.Menu.Preset - m.Layout(gtx, ie.presetMenuItems...) - return dims - }), - layout.Rigid(saveInstrumentBtn.Layout), - layout.Rigid(loadInstrumentBtn.Layout), - layout.Rigid(copyInstrumentBtn.Layout), - layout.Rigid(deleteInstrumentBtn.Layout), - ) - } - - for ie.presetMenuBtn.Clicked(gtx) { - ie.presetMenu.visible = true - } - - if t.CommentExpanded().Value() || gtx.Source.Focused(ie.commentEditor) { // we draw once the widget after it manages to lose focus - ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(header), - layout.Rigid(func(gtx C) D { - defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() - for ie.commentEditor.Update(gtx, t.InstrumentComment()) != EditorEventNone { - ie.instrumentDragList.Focus() - } - ret := layout.UniformInset(unit.Dp(6)).Layout(gtx, func(gtx C) D { - return ie.commentEditor.Layout(gtx, t.InstrumentComment(), t.Theme, &t.Theme.InstrumentEditor.InstrumentComment, "Comment") - }) - return ret - }), - ) - return ret - } - return header(gtx) - } - - return Surface{Gray: 37, Focus: ie.wasFocused}.Layout(gtx, header) -} - -func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D { - gtx.Constraints.Max.Y = gtx.Dp(36) - gtx.Constraints.Min.Y = gtx.Dp(36) - element := func(gtx C, i int) D { - grabhandle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, strconv.Itoa(i+1)) - label := func(gtx C) D { - name, level, mute, ok := (*tracker.Instruments)(t.Model).Item(i) - if !ok { - labelStyle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, "") - return layout.Center.Layout(gtx, labelStyle.Layout) - } - s := t.Theme.InstrumentEditor.InstrumentList.NameMuted - if !mute { - s = t.Theme.InstrumentEditor.InstrumentList.Name - k := byte(255 - level*127) - s.Color = color.NRGBA{R: 255, G: k, B: 255, A: 255} - } - if i == ie.instrumentDragList.TrackerList.Selected() { - for ie.nameEditor.Update(gtx, t.InstrumentName()) != EditorEventNone { - ie.instrumentDragList.Focus() - } - return layout.Center.Layout(gtx, func(gtx C) D { - defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() - return ie.nameEditor.Layout(gtx, t.InstrumentName(), t.Theme, &s, "Instr") - }) - } - if name == "" { - name = "Instr" - } - l := s.AsLabelStyle() - return layout.Center.Layout(gtx, Label(t.Theme, &l, name).Layout) - } - return layout.Center.Layout(gtx, func(gtx C) D { - return layout.Inset{Left: unit.Dp(6), Right: unit.Dp(6)}.Layout(gtx, func(gtx C) D { - return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(grabhandle.Layout), - layout.Rigid(label), - ) - }) - }) - } - - instrumentList := FilledDragList(t.Theme, ie.instrumentDragList) - instrumentList.ScrollBar = t.Theme.InstrumentEditor.InstrumentList.ScrollBar - - defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() - defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() - for { - event, ok := gtx.Event( - key.Filter{Focus: ie.instrumentDragList, Name: key.NameDownArrow}, - key.Filter{Focus: ie.instrumentDragList, Name: key.NameReturn}, - key.Filter{Focus: ie.instrumentDragList, Name: key.NameEnter}, - ) - if !ok { - break - } - switch e := event.(type) { - case key.Event: - switch e.State { - case key.Press: - switch e.Name { - case key.NameDownArrow: - ie.unitDragList.Focus() - case key.NameReturn, key.NameEnter: - ie.nameEditor.Focus() - } - } - } - } - - dims := instrumentList.Layout(gtx, element, nil) - gtx.Constraints = layout.Exact(dims.Size) - instrumentList.LayoutScrollBar(gtx) - return dims -} - -func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D { - var units [256]tracker.UnitListItem - for i, item := range (*tracker.Units)(t.Model).Iterate { - if i >= 256 { - break - } - units[i] = item - } - count := min(ie.unitDragList.TrackerList.Count(), 256) - - element := func(gtx C, i int) D { - gtx.Constraints.Max.Y = gtx.Dp(20) - gtx.Constraints.Min.Y = gtx.Constraints.Max.Y - if i < 0 || i > 255 { - return layout.Dimensions{Size: gtx.Constraints.Min} - } - u := units[i] - - editorStyle := t.Theme.InstrumentEditor.UnitList.Name - if u.Disabled { - editorStyle = t.Theme.InstrumentEditor.UnitList.NameDisabled - } - - stackText := strconv.FormatInt(int64(u.StackAfter), 10) - if u.StackNeed > u.StackBefore { - editorStyle.Color = t.Theme.InstrumentEditor.UnitList.Error - (*tracker.Alerts)(t.Model).AddNamed("UnitNeedsInputs", fmt.Sprintf("%v needs at least %v input signals, got %v", u.Type, u.StackNeed, u.StackBefore), tracker.Error) - } else if i == count-1 && u.StackAfter != 0 { - editorStyle.Color = t.Theme.InstrumentEditor.UnitList.Warning - (*tracker.Alerts)(t.Model).AddNamed("InstrumentLeavesSignals", fmt.Sprintf("Instrument leaves %v signal(s) on the stack", u.StackAfter), tracker.Warning) - } - - stackLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Stack, stackText) - - rightMargin := layout.Inset{Right: unit.Dp(10)} - return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(func(gtx C) D { - if i == ie.unitDragList.TrackerList.Selected() { - defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() - str := t.Model.UnitSearch() - for ev := ie.searchEditor.Update(gtx, str); ev != EditorEventNone; ev = ie.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("") - } - } - ie.unitDragList.Focus() - t.UnitSearching().SetValue(false) - } - return ie.searchEditor.Layout(gtx, str, t.Theme, &editorStyle, "---") - } else { - text := u.Type - if text == "" { - text = "---" - } - l := editorStyle.AsLabelStyle() - return Label(t.Theme, &l, text).Layout(gtx) - } - }), - layout.Flexed(1, func(gtx C) D { - unitNameLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Comment, u.Comment) - inset := layout.Inset{Left: unit.Dp(5)} - return inset.Layout(gtx, unitNameLabel.Layout) - }), - layout.Rigid(func(gtx C) D { - return rightMargin.Layout(gtx, stackLabel.Layout) - }), - ) - } - - defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() - unitList := FilledDragList(t.Theme, ie.unitDragList) - for { - event, ok := gtx.Event( - key.Filter{Focus: ie.unitDragList, Name: key.NameRightArrow}, - key.Filter{Focus: ie.unitDragList, Name: key.NameEnter, Optional: key.ModCtrl}, - key.Filter{Focus: ie.unitDragList, Name: key.NameReturn, Optional: key.ModCtrl}, - key.Filter{Focus: ie.unitDragList, Name: key.NameDeleteBackward}, - key.Filter{Focus: ie.unitDragList, Name: key.NameEscape}, - ) - if !ok { - break - } - switch e := event.(type) { - case key.Event: - switch e.State { - case key.Press: - switch e.Name { - case key.NameEscape: - ie.instrumentDragList.Focus() - case key.NameRightArrow: - ie.unitEditor.sliderList.Focus() - case key.NameDeleteBackward: - t.Units().SetSelectedType("") - t.UnitSearching().SetValue(true) - ie.searchEditor.Focus() - case key.NameEnter, key.NameReturn: - t.Model.AddUnit(e.Modifiers.Contain(key.ModCtrl)).Do() - t.UnitSearching().SetValue(true) - ie.searchEditor.Focus() - } - } - } - } - return Surface{Gray: 30, Focus: ie.wasFocused}.Layout(gtx, 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 { - for ie.addUnitBtn.Clicked(gtx) { - t.AddUnit(false).Do() - } - margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)} - addUnitBtn := IconBtn(t.Theme, &t.Theme.IconButton.Emphasis, ie.addUnitBtn, icons.ContentAdd, "Add unit (Enter)") - return margin.Layout(gtx, addUnitBtn.Layout) - }), - ) - }) -} diff --git a/tracker/gioui/keybindings.go b/tracker/gioui/keybindings.go index 35383dd..605f8c5 100644 --- a/tracker/gioui/keybindings.go +++ b/tracker/gioui/keybindings.go @@ -259,48 +259,28 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) { case "Paste": gtx.Execute(clipboard.ReadCmd{Tag: t}) case "OrderEditorFocus": - t.OrderEditor.scrollTable.Focus() + gtx.Execute(key.FocusCmd{Tag: t.OrderEditor.scrollTable}) case "TrackEditorFocus": - t.TrackEditor.scrollTable.Focus() - case "InstrumentEditorFocus": - t.InstrumentEditor.Focus() + 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}) case "FocusPrev": - switch { - case t.OrderEditor.scrollTable.Focused(gtx): - t.InstrumentEditor.unitEditor.sliderList.Focus() - case t.TrackEditor.scrollTable.Focused(gtx): - t.OrderEditor.scrollTable.Focus() - case t.InstrumentEditor.Focused(gtx): - if t.InstrEnlarged().Value() { - t.InstrumentEditor.unitEditor.sliderList.Focus() - } else { - t.TrackEditor.scrollTable.Focus() - } - default: - t.InstrumentEditor.Focus() - } + t.FocusPrev(gtx, false) + case "FocusPrevInto": + t.FocusPrev(gtx, true) case "FocusNext": - switch { - case t.OrderEditor.scrollTable.Focused(gtx): - t.TrackEditor.scrollTable.Focus() - case t.TrackEditor.scrollTable.Focused(gtx): - t.InstrumentEditor.Focus() - case t.InstrumentEditor.Focused(gtx): - t.InstrumentEditor.unitEditor.sliderList.Focus() - default: - if t.InstrEnlarged().Value() { - t.InstrumentEditor.Focus() - } else { - t.OrderEditor.scrollTable.Focus() - } - } + t.FocusNext(gtx, false) + case "FocusNextInto": + t.FocusNext(gtx, true) default: if action[:4] == "Note" { val, err := strconv.Atoi(string(action[4:])) if err != nil { break } - instr := t.InstrumentEditor.instrumentDragList.TrackerList.Selected() + instr := t.Model.Instruments().List().Selected() n := noteAsValue(t.Model.Octave().Value(), val-12) t.KeyNoteMap.Press(e.Name, tracker.NoteEvent{Channel: instr, Note: n}) } diff --git a/tracker/gioui/keybindings.yml b/tracker/gioui/keybindings.yml index fd54a97..3ce2061 100644 --- a/tracker/gioui/keybindings.yml +++ b/tracker/gioui/keybindings.yml @@ -6,86 +6,89 @@ # - {key: "A"} # # will stop the A key from sending NoteOff events. -- {key: "C", shortcut: true, action: "Copy"} -- {key: "V", shortcut: true, action: "Paste"} -- {key: "A", shortcut: true, action: "SelectAll"} -- {key: "X", shortcut: true, action: "Cut"} -- {key: "Z", shortcut: true, action: "Undo"} -- {key: "Y", shortcut: true, action: "Redo"} -- {key: "D", shortcut: true, action: "UnitDisabledToggle"} -- {key: "L", shortcut: true, action: "LoopToggle"} -- {key: "N", shortcut: true, action: "NewSong"} -- {key: "S", shortcut: true, action: "SaveSong"} -- {key: "M", shortcut: true, action: "MuteToggle"} -- {key: ",", shortcut: true, action: "SoloToggle"} -- {key: "O", shortcut: true, action: "OpenSong"} -- {key: "I", shortcut: true, shift: true, action: "DeleteInstrument"} -- {key: "I", shortcut: true, action: "AddInstrument"} -- {key: "I", shortcut: true, alt: true, action: "SplitInstrument"} -- {key: "T", shortcut: true, shift: true, action: "DeleteTrack"} -- {key: "T", shortcut: true, alt: true, action: "SplitTrack"} -- {key: "T", shortcut: true, action: "AddTrack"} -- {key: "E", shortcut: true, action: "InstrEnlargedToggle"} -- {key: "K", shortcut: true, action: "LinkInstrTrackToggle"} -- {key: "W", shortcut: true, action: "Quit"} -- {key: "Space", action: "PlayingToggleUnfollow"} -- {key: "Space", shift: true, action: "PlayingToggleFollow"} -- {key: "F1", action: "OrderEditorFocus"} -- {key: "F2", action: "TrackEditorFocus"} -- {key: "F3", action: "InstrumentEditorFocus"} -- {key: "F5", action: "PlayCurrentPosUnfollow"} -- {key: "F5", shift: true, action: "PlayCurrentPosFollow"} -- {key: "F5", shortcut: true, action: "PlaySongStartUnfollow"} -- {key: "F5", shortcut: true, shift: true, action: "PlaySongStartFollow"} -- {key: "F6", action: "PlaySelectedUnfollow"} -- {key: "F6", shift: true, action: "PlaySelectedFollow"} -- {key: "F6", shortcut: true, action: "PlayLoopUnfollow"} -- {key: "F6", shortcut: true, shift: true, action: "PlayLoopFollow"} -- {key: "F7", action: "RecordingToggle"} -- {key: "F8", action: "StopPlaying"} -- {key: "F9", action: "FollowToggle"} -- {key: "F12", action: "PanicToggle"} -- {key: "\\", shift: true, action: "OctaveAdd"} -- {key: "\\", action: "OctaveSubtract"} -- {key: ">", shift: true, action: "OctaveAdd"} -- {key: ">", action: "OctaveSubtract"} -- {key: "<", shift: true, action: "OctaveAdd"} -- {key: "<", action: "OctaveSubtract"} -- {key: "Tab", shift: true, action: "FocusPrev"} -- {key: "Tab", action: "FocusNext"} -- {key: "A", action: "NoteOff"} -- {key: "1", action: "NoteOff"} -- {key: "Z", action: "Note0"} -- {key: "S", action: "Note1"} -- {key: "X", action: "Note2"} -- {key: "D", action: "Note3"} -- {key: "C", action: "Note4"} -- {key: "V", action: "Note5"} -- {key: "G", action: "Note6"} -- {key: "B", action: "Note7"} -- {key: "H", action: "Note8"} -- {key: "N", action: "Note9"} -- {key: "J", action: "Note10"} -- {key: "M", action: "Note11"} -- {key: ",", action: "Note12"} -- {key: "L", action: "Note13"} -- {key: ".", action: "Note14"} -- {key: "Q", action: "Note12"} -- {key: "2", action: "Note13"} -- {key: "W", action: "Note14"} -- {key: "3", action: "Note15"} -- {key: "E", action: "Note16"} -- {key: "R", action: "Note17"} -- {key: "5", action: "Note18"} -- {key: "T", action: "Note19"} -- {key: "6", action: "Note20"} -- {key: "Y", action: "Note21"} -- {key: "7", action: "Note22"} -- {key: "U", action: "Note23"} -- {key: "I", action: "Note24"} -- {key: "9", action: "Note25"} -- {key: "O", action: "Note26"} -- {key: "0", action: "Note27"} -- {key: "P", action: "Note28"} -- {key: "+", action: "Increase"} -- {key: "-", action: "Decrease"} +- { key: "C", shortcut: true, action: "Copy" } +- { key: "V", shortcut: true, action: "Paste" } +- { key: "A", shortcut: true, action: "SelectAll" } +- { key: "X", shortcut: true, action: "Cut" } +- { key: "Z", shortcut: true, action: "Undo" } +- { key: "Y", shortcut: true, action: "Redo" } +- { key: "D", shortcut: true, action: "UnitDisabledToggle" } +- { key: "L", shortcut: true, action: "LoopToggle" } +- { key: "N", shortcut: true, action: "NewSong" } +- { key: "S", shortcut: true, action: "SaveSong" } +- { key: "M", shortcut: true, action: "MuteToggle" } +- { key: ",", shortcut: true, action: "SoloToggle" } +- { key: "O", shortcut: true, action: "OpenSong" } +- { key: "I", shortcut: true, shift: true, action: "DeleteInstrument" } +- { key: "I", shortcut: true, action: "AddInstrument" } +- { key: "I", shortcut: true, alt: true, action: "SplitInstrument" } +- { key: "T", shortcut: true, shift: true, action: "DeleteTrack" } +- { key: "T", shortcut: true, alt: true, action: "SplitTrack" } +- { key: "T", shortcut: true, action: "AddTrack" } +- { key: "E", shortcut: true, action: "InstrEnlargedToggle" } +- { key: "K", shortcut: true, action: "LinkInstrTrackToggle" } +- { key: "W", shortcut: true, action: "Quit" } +- { key: "Space", action: "PlayingToggleUnfollow" } +- { key: "Space", shift: true, action: "PlayingToggleFollow" } +- { key: "F1", action: "OrderEditorFocus" } +- { key: "F2", action: "TrackEditorFocus" } +- { key: "F3", action: "InstrumentListFocus" } +- { key: "F4", action: "UnitListFocus" } +- { key: "F5", action: "PlayCurrentPosUnfollow" } +- { key: "F5", shift: true, action: "PlayCurrentPosFollow" } +- { key: "F5", shortcut: true, action: "PlaySongStartUnfollow" } +- { key: "F5", shortcut: true, shift: true, action: "PlaySongStartFollow" } +- { key: "F6", action: "PlaySelectedUnfollow" } +- { key: "F6", shift: true, action: "PlaySelectedFollow" } +- { key: "F6", shortcut: true, action: "PlayLoopUnfollow" } +- { key: "F6", shortcut: true, shift: true, action: "PlayLoopFollow" } +- { key: "F7", action: "RecordingToggle" } +- { key: "F8", action: "StopPlaying" } +- { key: "F9", action: "FollowToggle" } +- { key: "F12", action: "PanicToggle" } +- { key: "\\", shift: true, action: "OctaveAdd" } +- { key: "\\", action: "OctaveSubtract" } +- { key: ">", shift: true, action: "OctaveAdd" } +- { key: ">", action: "OctaveSubtract" } +- { key: "<", shift: true, action: "OctaveAdd" } +- { key: "<", action: "OctaveSubtract" } +- { key: "Tab", shift: true, action: "FocusPrev" } +- { key: "Tab", action: "FocusNext" } +- { key: "Tab", shift: true, shortcut: true, action: "FocusPrevInto" } +- { key: "Tab", shortcut: true, action: "FocusNextInto" } +- { key: "A", action: "NoteOff" } +- { key: "1", action: "NoteOff" } +- { key: "Z", action: "Note0" } +- { key: "S", action: "Note1" } +- { key: "X", action: "Note2" } +- { key: "D", action: "Note3" } +- { key: "C", action: "Note4" } +- { key: "V", action: "Note5" } +- { key: "G", action: "Note6" } +- { key: "B", action: "Note7" } +- { key: "H", action: "Note8" } +- { key: "N", action: "Note9" } +- { key: "J", action: "Note10" } +- { key: "M", action: "Note11" } +- { key: ",", action: "Note12" } +- { key: "L", action: "Note13" } +- { key: ".", action: "Note14" } +- { key: "Q", action: "Note12" } +- { key: "2", action: "Note13" } +- { key: "W", action: "Note14" } +- { key: "3", action: "Note15" } +- { key: "E", action: "Note16" } +- { key: "R", action: "Note17" } +- { key: "5", action: "Note18" } +- { key: "T", action: "Note19" } +- { key: "6", action: "Note20" } +- { key: "Y", action: "Note21" } +- { key: "7", action: "Note22" } +- { key: "U", action: "Note23" } +- { key: "I", action: "Note24" } +- { key: "9", action: "Note25" } +- { key: "O", action: "Note26" } +- { key: "0", action: "Note27" } +- { key: "P", action: "Note28" } +- { key: "+", action: "Increase" } +- { key: "-", action: "Decrease" } diff --git a/tracker/gioui/note_editor.go b/tracker/gioui/note_editor.go index a8e04c0..73c02a7 100644 --- a/tracker/gioui/note_editor.go +++ b/tracker/gioui/note_editor.go @@ -117,10 +117,6 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor { return ret } -func (te *NoteEditor) Focused(gtx C) bool { - return te.scrollTable.Focused(gtx) || te.scrollTable.ChildFocused(gtx) -} - func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions { for { e, ok := gtx.Event(te.eventFilters...) @@ -137,7 +133,7 @@ func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions { } } - for te.Focused(gtx) && len(t.noteEvents) > 0 { + for gtx.Focused(te.scrollTable) && len(t.noteEvents) > 0 { ev := t.noteEvents[0] ev.IsTrack = true ev.Channel = t.Model.Notes().Cursor().X @@ -152,7 +148,7 @@ func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions { defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() - return Surface{Gray: 24, Focus: te.scrollTable.Focused(gtx)}.Layout(gtx, func(gtx C) D { + return Surface{Gray: 24, Focus: te.scrollTable.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { return te.layoutButtons(gtx, t) @@ -165,7 +161,7 @@ func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions { } func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D { - return Surface{Gray: 37, Focus: te.scrollTable.Focused(gtx) || te.scrollTable.ChildFocused(gtx)}.Layout(gtx, func(gtx C) D { + return Surface{Gray: 37, Focus: te.scrollTable.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D { addSemitoneBtn := ActionBtn(t.AddSemitone(), t.Theme, te.AddSemitoneBtn, "+1", "Add semitone") subtractSemitoneBtn := ActionBtn(t.SubtractSemitone(), t.Theme, te.SubtractSemitoneBtn, "-1", "Subtract semitone") addOctaveBtn := ActionBtn(t.AddOctave(), t.Theme, te.AddOctaveBtn, "+12", "Add octave") @@ -290,7 +286,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D { point := tracker.Point{X: x, Y: y} if drawSelection && selection.Contains(point) { color := t.Theme.Selection.Inactive - if te.scrollTable.Focused(gtx) { + if gtx.Focused(te.scrollTable) { color = t.Theme.Selection.Active } paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op()) @@ -298,7 +294,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D { // draw the cursor if point == cursor { c := t.Theme.Cursor.Inactive - if te.scrollTable.Focused(gtx) { + if gtx.Focused(te.scrollTable) { c = t.Theme.Cursor.Active } if hasTrackMidiIn { @@ -335,6 +331,12 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D { return table.Layout(gtx, cell, colTitle, rowTitle, nil, rowTitleBg) } +func (t *NoteEditor) Tags(level int, yield TagYieldFunc) bool { + return yield(level+1, t.scrollTable.RowTitleList) && + yield(level+1, t.scrollTable.ColTitleList) && + yield(level, t.scrollTable) +} + func colorOp(gtx C, c color.NRGBA) op.CallOp { macro := op.Record(gtx.Ops) paint.ColorOp{Color: c}.Add(gtx.Ops) diff --git a/tracker/gioui/order_editor.go b/tracker/gioui/order_editor.go index 6950ee9..6b03161 100644 --- a/tracker/gioui/order_editor.go +++ b/tracker/gioui/order_editor.go @@ -100,12 +100,12 @@ func (oe *OrderEditor) Layout(gtx C, t *Tracker) D { point := tracker.Point{X: x, Y: y} if selection.Contains(point) { color = t.Theme.Selection.Inactive - if oe.scrollTable.Focused(gtx) { + if gtx.Focused(oe.scrollTable) { color = t.Theme.Selection.Active } if point == oe.scrollTable.Table.Cursor() { color = t.Theme.Cursor.Inactive - if oe.scrollTable.Focused(gtx) { + if gtx.Focused(oe.scrollTable) { color = t.Theme.Cursor.Active } } @@ -200,6 +200,10 @@ func (oe *OrderEditor) command(t *Tracker, e key.Event) { } } +func (t *OrderEditor) Tags(level int, yield TagYieldFunc) bool { + return yield(level+1, t.scrollTable.RowTitleList) && yield(level+1, t.scrollTable.ColTitleList) && yield(level, t.scrollTable) +} + func patternIndexToString(index int) string { if index < 0 { return "" diff --git a/tracker/gioui/patch_panel.go b/tracker/gioui/patch_panel.go new file mode 100644 index 0000000..a570629 --- /dev/null +++ b/tracker/gioui/patch_panel.go @@ -0,0 +1,496 @@ +package gioui + +import ( + "bytes" + "fmt" + "image" + "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 + } + + InstrumentList struct { + instrumentDragList *DragList + nameEditor *Editor + + octave *NumericUpDownState + enlargeBtn *Clickable + linkInstrTrackBtn *Clickable + newInstrumentBtn *Clickable + + octaveHint string + linkDisabledHint string + 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), + } +} + +func (pp *PatchPanel) Layout(gtx C, t *Tracker) D { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx C) D { return pp.instrList.Layout(gtx, t) }), + layout.Rigid(func(gtx C) D { return pp.tools.Layout(gtx, t) }), + layout.Flexed(1, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(func(gtx C) D { return pp.unitList.Layout(gtx, t) }), + layout.Flexed(1, func(gtx C) D { return pp.unitEditor.Layout(gtx, t) }), + ) + })) +} + +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) +} + +// TreeFocused returns true if any of the tags in the patch panel is focused +func (pp *PatchPanel) TreeFocused(gtx C) bool { + return !pp.Tags(0, func(_ int, tag event.Tag) bool { + return !gtx.Focused(tag) + }) +} + +// InstrumentTools methods + +func MakeInstrumentTools(m *tracker.Model) InstrumentTools { + ret := InstrumentTools{ + Voices: NewNumericUpDownState(), + 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)) + } + return ret +} + +func (it *InstrumentTools) Layout(gtx C, t *Tracker) D { + 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) + 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.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(saveInstrumentBtn.Layout), + layout.Rigid(loadInstrumentBtn.Layout), + layout.Rigid(copyInstrumentBtn.Layout), + layout.Rigid(deleteInstrumentBtn.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) + }) +} + +func (it *InstrumentTools) update(gtx C, tr *Tracker) { + for it.copyInstrumentBtn.Clicked(gtx) { + if contents, ok := tr.Instruments().List().CopyElements(); ok { + gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))}) + tr.Alerts().Add("Instrument copied to clipboard", tracker.Info) + } + } + for it.saveInstrumentBtn.Clicked(gtx) { + writer, err := tr.Explorer.CreateFile(tr.InstrumentName().Value() + ".yml") + if err != nil { + continue + } + tr.SaveInstrument(writer) + } + for it.loadInstrumentBtn.Clicked(gtx) { + reader, err := tr.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp") + if err != nil { + continue + } + 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 +} + +// InstrumentList methods + +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, t *Tracker) D { + 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, func(gtx C) D { return il.actualLayout(gtx, t) }), + 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, t *Tracker) D { + gtx.Constraints.Max.Y = gtx.Dp(36) + gtx.Constraints.Min.Y = gtx.Dp(36) + element := func(gtx C, i int) D { + grabhandle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, strconv.Itoa(i+1)) + label := func(gtx C) D { + name, level, mute, ok := (*tracker.Instruments)(t.Model).Item(i) + if !ok { + labelStyle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, "") + return layout.Center.Layout(gtx, labelStyle.Layout) + } + s := t.Theme.InstrumentEditor.InstrumentList.NameMuted + if !mute { + s = t.Theme.InstrumentEditor.InstrumentList.Name + k := byte(255 - level*127) + s.Color = color.NRGBA{R: 255, G: k, B: 255, A: 255} + } + if i == il.instrumentDragList.TrackerList.Selected() { + for il.nameEditor.Update(gtx, t.InstrumentName()) != EditorEventNone { + il.instrumentDragList.Focus() + } + return layout.Center.Layout(gtx, func(gtx C) D { + defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() + return il.nameEditor.Layout(gtx, t.InstrumentName(), t.Theme, &s, "Instr") + }) + } + if name == "" { + name = "Instr" + } + l := s.AsLabelStyle() + return layout.Center.Layout(gtx, Label(t.Theme, &l, name).Layout) + } + return layout.Center.Layout(gtx, func(gtx C) D { + return layout.Inset{Left: unit.Dp(6), Right: unit.Dp(6)}.Layout(gtx, func(gtx C) D { + return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(grabhandle.Layout), + layout.Rigid(label), + ) + }) + }) + } + instrumentList := FilledDragList(t.Theme, il.instrumentDragList) + instrumentList.ScrollBar = t.Theme.InstrumentEditor.InstrumentList.ScrollBar + defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() + dims := instrumentList.Layout(gtx, element, nil) + gtx.Constraints = layout.Exact(dims.Size) + instrumentList.LayoutScrollBar(gtx) + return dims +} + +func (il *InstrumentList) update(gtx C, t *Tracker) { + for { + event, ok := gtx.Event( + key.Filter{Focus: il.instrumentDragList, Name: key.NameDownArrow}, + key.Filter{Focus: il.instrumentDragList, Name: key.NameReturn}, + key.Filter{Focus: il.instrumentDragList, Name: key.NameEnter}, + ) + if !ok { + break + } + if e, ok := event.(key.Event); ok && e.State == key.Press { + switch e.Name { + case key.NameDownArrow: + t.PatchPanel.unitList.dragList.Focus() + case key.NameReturn, key.NameEnter: + il.nameEditor.Focus() + } + } + } +} + +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, t *Tracker) D { + ul.update(gtx, t) + var units [256]tracker.UnitListItem + for i, item := range (*tracker.Units)(t.Model).Iterate { + if i >= 256 { + break + } + units[i] = item + } + count := min(ul.dragList.TrackerList.Count(), 256) + element := func(gtx C, i int) D { + gtx.Constraints.Max.Y = gtx.Dp(20) + gtx.Constraints.Min.Y = gtx.Constraints.Max.Y + if i < 0 || i > 255 { + return layout.Dimensions{Size: gtx.Constraints.Min} + } + u := units[i] + editorStyle := t.Theme.InstrumentEditor.UnitList.Name + if u.Disabled { + editorStyle = t.Theme.InstrumentEditor.UnitList.NameDisabled + } + stackText := strconv.FormatInt(int64(u.StackAfter), 10) + if u.StackNeed > u.StackBefore { + editorStyle.Color = t.Theme.InstrumentEditor.UnitList.Error + (*tracker.Alerts)(t.Model).AddNamed("UnitNeedsInputs", fmt.Sprintf("%v needs at least %v input signals, got %v", u.Type, u.StackNeed, u.StackBefore), tracker.Error) + } else if i == count-1 && u.StackAfter != 0 { + editorStyle.Color = t.Theme.InstrumentEditor.UnitList.Warning + (*tracker.Alerts)(t.Model).AddNamed("InstrumentLeavesSignals", fmt.Sprintf("Instrument leaves %v signal(s) on the stack", u.StackAfter), tracker.Warning) + } + stackLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Stack, stackText) + rightMargin := layout.Inset{Right: unit.Dp(10)} + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(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() + 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) + } + return ul.searchEditor.Layout(gtx, str, t.Theme, &editorStyle, "---") + } else { + text := u.Type + if text == "" { + text = "---" + } + l := editorStyle.AsLabelStyle() + return Label(t.Theme, &l, text).Layout(gtx) + } + }), + layout.Flexed(1, func(gtx C) D { + unitNameLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Comment, u.Comment) + inset := layout.Inset{Left: unit.Dp(5)} + return inset.Layout(gtx, unitNameLabel.Layout) + }), + layout.Rigid(func(gtx C) D { + return rightMargin.Layout(gtx, stackLabel.Layout) + }), + ) + } + defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() + unitList := FilledDragList(t.Theme, ul.dragList) + return Surface{Gray: 30, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, 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) + }), + ) + }) +} + +func (ul *UnitList) update(gtx C, t *Tracker) { + for ul.addUnitBtn.Clicked(gtx) { + ul.addUnitAction.Do() + } + 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}, + key.Filter{Focus: ul.dragList, Name: key.NameEscape}, + ) + if !ok { + break + } + if e, ok := event.(key.Event); ok && e.State == key.Press { + switch e.Name { + case key.NameEscape: + t.PatchPanel.instrList.instrumentDragList.Focus() + case key.NameRightArrow: + t.PatchPanel.unitEditor.sliderList.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() + } + } + } +} + +func (ul *UnitList) Tags(curLevel int, yield TagYieldFunc) bool { + return yield(curLevel, ul.dragList) +} diff --git a/tracker/gioui/scroll_table.go b/tracker/gioui/scroll_table.go index 881c874..695e9d4 100644 --- a/tracker/gioui/scroll_table.go +++ b/tracker/gioui/scroll_table.go @@ -92,8 +92,17 @@ func (st *ScrollTable) Focus() { st.requestFocus = true } -func (st *ScrollTable) Focused(gtx C) bool { - return gtx.Source.Focused(st) +func (st *ScrollTable) Tags(level int, yield TagYieldFunc) bool { + return yield(level+1, st.RowTitleList) && + yield(level+1, st.ColTitleList) && + yield(level, st) +} + +// TreeFocused return true if any of the tags in the scroll table has focus. +func (st *ScrollTable) TreeFocused(gtx C) bool { + return !st.Tags(0, func(_ int, tag event.Tag) bool { + return !gtx.Focused(tag) + }) } func (st *ScrollTable) EnsureCursorVisible() { @@ -101,10 +110,6 @@ func (st *ScrollTable) EnsureCursorVisible() { st.RowTitleList.EnsureVisible(st.Table.Cursor().Y) } -func (st *ScrollTable) ChildFocused(gtx C) bool { - return st.ColTitleList.Focused(gtx) || st.RowTitleList.Focused(gtx) -} - func (s ScrollTableStyle) Layout(gtx C, element func(gtx C, x, y int) D, colTitle, rowTitle, colTitleBg, rowTitleBg func(gtx C, i int) D) D { defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop() event.Op(gtx.Ops, s.ScrollTable) @@ -112,7 +117,7 @@ func (s ScrollTableStyle) Layout(gtx C, element func(gtx C, x, y int) D, colTitl p := image.Pt(gtx.Dp(s.RowTitleWidth), gtx.Dp(s.ColumnTitleHeight)) s.handleEvents(gtx, p) - return Surface{Gray: 24, Focus: s.ScrollTable.Focused(gtx) || s.ScrollTable.ChildFocused(gtx)}.Layout(gtx, func(gtx C) D { + return Surface{Gray: 24, Focus: s.ScrollTable.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D { defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() dims := gtx.Constraints.Max s.layoutColTitles(gtx, p, colTitle, colTitleBg) diff --git a/tracker/gioui/tracker.go b/tracker/gioui/tracker.go index bef5c56..ddc2232 100644 --- a/tracker/gioui/tracker.go +++ b/tracker/gioui/tracker.go @@ -42,13 +42,13 @@ type ( DialogState *DialogState - ModalDialog layout.Widget - InstrumentEditor *InstrumentEditor - OrderEditor *OrderEditor - TrackEditor *NoteEditor - Explorer *explorer.Explorer - Exploring bool - SongPanel *SongPanel + ModalDialog layout.Widget + PatchPanel *PatchPanel + OrderEditor *OrderEditor + TrackEditor *NoteEditor + Explorer *explorer.Explorer + Exploring bool + SongPanel *SongPanel filePathString tracker.String noteEvents []tracker.NoteEvent @@ -83,10 +83,10 @@ func NewTracker(model *tracker.Model) *Tracker { BottomHorizontalSplit: &SplitState{Ratio: -.6}, VerticalSplit: &SplitState{Axis: layout.Vertical}, - DialogState: new(DialogState), - InstrumentEditor: NewInstrumentEditor(model), - OrderEditor: NewOrderEditor(model), - TrackEditor: NewNoteEditor(model), + DialogState: new(DialogState), + PatchPanel: NewPatchPanel(model), + OrderEditor: NewOrderEditor(model), + TrackEditor: NewNoteEditor(model), Zoom: 6, @@ -118,7 +118,6 @@ func NewTracker(model *tracker.Model) *Tracker { } func (t *Tracker) Main() { - t.InstrumentEditor.Focus() recoveryTicker := time.NewTicker(time.Second * 30) var ops op.Ops titlePath := "" @@ -233,7 +232,7 @@ func (t *Tracker) Layout(gtx layout.Context, w *app.Window) { for { ev, ok := gtx.Event( key.Filter{Name: "", Optional: key.ModAlt | key.ModCommand | key.ModShift | key.ModShortcut | key.ModSuper}, - key.Filter{Name: key.NameTab, Optional: key.ModShift}, + key.Filter{Name: key.NameTab, Optional: key.ModShift | key.ModShortcut}, transfer.TargetFilter{Target: t, Type: "application/text"}, pointer.Filter{Target: t, Kinds: pointer.Scroll, ScrollY: pointer.ScrollRange{Min: -1, Max: 1}}, ) @@ -359,7 +358,7 @@ func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions { return t.SongPanel.Layout(gtx, t) }, func(gtx C) D { - return t.InstrumentEditor.Layout(gtx, t) + return t.PatchPanel.Layout(gtx, t) }, ) } @@ -392,3 +391,12 @@ func (t *Tracker) openUrl(url string) { t.Alerts().Add(err.Error(), tracker.Error) } } + +func (t *Tracker) Tags(curLevel int, yield TagYieldFunc) bool { + ret := t.PatchPanel.Tags(curLevel+1, yield) + if !t.InstrEnlarged().Value() { + ret = ret && t.OrderEditor.Tags(curLevel+1, yield) && + t.TrackEditor.Tags(curLevel+1, yield) + } + return ret +} diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index fac97a1..6ac6eb0 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -74,7 +74,7 @@ func (pe *UnitEditor) Layout(gtx C, t *Tracker) D { switch e := e.(type) { case key.Event: if e.State == key.Press { - pe.command(e, t) + pe.command(gtx, e, t) } } } @@ -85,7 +85,7 @@ func (pe *UnitEditor) Layout(gtx C, t *Tracker) D { if t.UnitSearching().Value() || pe.sliderList.TrackerList.Count() == 0 { editorFunc = pe.layoutUnitTypeChooser } - return Surface{Gray: 24, Focus: t.InstrumentEditor.wasFocused}.Layout(gtx, func(gtx C) D { + return Surface{Gray: 24, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Flexed(1, func(gtx C) D { return editorFunc(gtx, t) @@ -162,7 +162,7 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D { }), layout.Flexed(1, func(gtx C) D { for pe.commentEditor.Update(gtx, t.UnitComment()) != EditorEventNone { - t.InstrumentEditor.Focus() + t.FocusPrev(gtx, false) } return pe.commentEditor.Layout(gtx, t.UnitComment(), t.Theme, &t.Theme.InstrumentEditor.UnitComment, "---") }), @@ -195,7 +195,7 @@ func (pe *UnitEditor) layoutUnitTypeChooser(gtx C, t *Tracker) D { return dims } -func (pe *UnitEditor) command(e key.Event, t *Tracker) { +func (pe *UnitEditor) command(gtx C, e key.Event, t *Tracker) { params := t.Model.Params() switch e.State { case key.Press: @@ -215,11 +215,15 @@ func (pe *UnitEditor) command(e key.Event, t *Tracker) { i.SetValue(i.Value() + 1) } case key.NameEscape: - t.InstrumentEditor.unitDragList.Focus() + t.FocusPrev(gtx, false) } } } +func (t *UnitEditor) Tags(level int, yield TagYieldFunc) bool { + return yield(level, t.sliderList) && yield(level+1, &t.commentEditor.widgetEditor) +} + type ParameterWidget struct { floatWidget widget.Float boolWidget widget.Bool