diff --git a/tracker/gioui/draglist.go b/tracker/gioui/draglist.go index c4aec10..b036034 100644 --- a/tracker/gioui/draglist.go +++ b/tracker/gioui/draglist.go @@ -4,6 +4,7 @@ import ( "image" "image/color" + "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/layout" "gioui.org/op" @@ -20,12 +21,16 @@ type DragList struct { dragID pointer.ID tags []bool swapped bool + focused bool + requestFocus bool + mainTag bool } type FilledDragListStyle struct { dragList *DragList HoverColor color.NRGBA SelectedColor color.NRGBA + CursorColor color.NRGBA Count int element func(gtx C, i int) D swap func(i, j int) @@ -39,9 +44,18 @@ func FilledDragList(th *material.Theme, dragList *DragList, count int, element f Count: count, HoverColor: dragListHoverColor, SelectedColor: dragListSelectedColor, + CursorColor: cursorColor, } } +func (d *DragList) Focus() { + d.requestFocus = true +} + +func (d *DragList) Focused() bool { + return d.focused +} + func (s *FilledDragListStyle) Layout(gtx C) D { swap := 0 @@ -53,6 +67,40 @@ func (s *FilledDragListStyle) Layout(gtx C) D { gtx.Constraints.Min.Y = gtx.Constraints.Max.Y } + if s.dragList.requestFocus { + s.dragList.requestFocus = false + key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops) + } + + for _, ke := range gtx.Events(&s.dragList.mainTag) { + switch ke := ke.(type) { + case key.FocusEvent: + s.dragList.focused = ke.Focus + case key.Event: + if !s.dragList.focused || ke.State != key.Press { + break + } + delta := 0 + switch { + case s.dragList.List.Axis == layout.Horizontal && ke.Name == key.NameLeftArrow && s.dragList.SelectedItem > 0: + delta = -1 + case s.dragList.List.Axis == layout.Horizontal && ke.Name == key.NameRightArrow && s.dragList.SelectedItem < s.Count-1: + delta = 1 + case s.dragList.List.Axis == layout.Vertical && ke.Name == key.NameUpArrow && s.dragList.SelectedItem > 0: + delta = -1 + case s.dragList.List.Axis == layout.Vertical && ke.Name == key.NameDownArrow && s.dragList.SelectedItem < s.Count-1: + delta = 1 + } + if delta != 0 { + if ke.Modifiers.Contain(key.ModShortcut) { + swap = delta + } else { + s.dragList.SelectedItem += delta + } + } + } + } + listElem := func(gtx C, index int) D { for len(s.dragList.tags) <= index { s.dragList.tags = append(s.dragList.tags, false) @@ -60,13 +108,18 @@ func (s *FilledDragListStyle) Layout(gtx C) D { bg := func(gtx C) D { var color color.NRGBA if s.dragList.SelectedItem == index { - color = s.SelectedColor + if s.dragList.focused { + color = s.CursorColor + } else { + color = s.SelectedColor + } } else if s.dragList.HoverItem == index { color = s.HoverColor } paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op()) return D{Size: gtx.Constraints.Min} } + inputFg := func(gtx C) D { defer op.Save(gtx.Ops).Load() for _, ev := range gtx.Events(&s.dragList.tags[index]) { @@ -86,6 +139,7 @@ func (s *FilledDragListStyle) Layout(gtx C) D { break } s.dragList.SelectedItem = index + key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops) } } rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y) @@ -141,6 +195,7 @@ func (s *FilledDragListStyle) Layout(gtx C) D { }), layout.Expanded(inputFg)) } + key.InputOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops) dims := s.dragList.List.Layout(gtx, s.Count, listElem) if !s.dragList.swapped && swap != 0 && s.dragList.SelectedItem+swap >= 0 && s.dragList.SelectedItem+swap < s.Count { s.swap(s.dragList.SelectedItem, s.dragList.SelectedItem+swap) diff --git a/tracker/gioui/files.go b/tracker/gioui/files.go index 8fc79a3..2e9e9ff 100644 --- a/tracker/gioui/files.go +++ b/tracker/gioui/files.go @@ -175,7 +175,7 @@ func (t *Tracker) loadInstrument(filename string) bool { } t.SetInstrument(instrument) if t.Instrument().Comment != "" { - t.InstrumentExpanded = true + t.InstrumentEditor.ExpandComment() } return true } diff --git a/tracker/gioui/instrumenteditor.go b/tracker/gioui/instrumenteditor.go new file mode 100644 index 0000000..0d2aad2 --- /dev/null +++ b/tracker/gioui/instrumenteditor.go @@ -0,0 +1,449 @@ +package gioui + +import ( + "fmt" + "image" + "image/color" + "math" + "strconv" + "strings" + "time" + + "gioui.org/io/clipboard" + "gioui.org/io/key" + "gioui.org/io/pointer" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/text" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" + "gioui.org/x/eventx" + "golang.org/x/exp/shiny/materialdesign/icons" + "gopkg.in/yaml.v3" +) + +type InstrumentEditor struct { + newInstrumentBtn *widget.Clickable + deleteInstrumentBtn *widget.Clickable + copyInstrumentBtn *widget.Clickable + saveInstrumentBtn *widget.Clickable + loadInstrumentBtn *widget.Clickable + addUnitBtn *widget.Clickable + commentExpandBtn *widget.Clickable + commentEditor *widget.Editor + nameEditor *widget.Editor + instrumentDragList *DragList + instrumentScrollBar *ScrollBar + unitDragList *DragList + unitScrollBar *ScrollBar + confirmInstrDelete *Dialog + paramEditor *ParamEditor + stackUse []int + tag bool + wasFocused bool + commentExpanded bool +} + +func NewInstrumentEditor() *InstrumentEditor { + return &InstrumentEditor{ + newInstrumentBtn: new(widget.Clickable), + deleteInstrumentBtn: new(widget.Clickable), + copyInstrumentBtn: new(widget.Clickable), + saveInstrumentBtn: new(widget.Clickable), + loadInstrumentBtn: new(widget.Clickable), + addUnitBtn: new(widget.Clickable), + commentExpandBtn: new(widget.Clickable), + commentEditor: new(widget.Editor), + nameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle}, + instrumentDragList: &DragList{List: &layout.List{Axis: layout.Horizontal}, HoverItem: -1}, + instrumentScrollBar: &ScrollBar{Axis: layout.Horizontal}, + unitDragList: &DragList{List: &layout.List{Axis: layout.Vertical}, HoverItem: -1}, + unitScrollBar: &ScrollBar{Axis: layout.Vertical}, + confirmInstrDelete: new(Dialog), + paramEditor: NewParamEditor(), + } +} + +func (t *InstrumentEditor) ExpandComment() { + t.commentExpanded = true +} + +func (ie *InstrumentEditor) Focus() { + ie.unitDragList.Focus() +} + +func (ie *InstrumentEditor) Focused() bool { + return ie.unitDragList.focused +} + +func (ie *InstrumentEditor) ChildFocused() bool { + return ie.paramEditor.Focused() || ie.instrumentDragList.Focused() || ie.commentEditor.Focused() || ie.nameEditor.Focused() +} + +func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D { + ie.wasFocused = ie.Focused() || ie.ChildFocused() + for _, e := range gtx.Events(&ie.tag) { + switch e.(type) { + case pointer.Event: + ie.unitDragList.Focus() + } + } + rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y) + pointer.Rect(rect).Add(gtx.Ops) + pointer.InputOp{Tag: &ie.tag, + Types: pointer.Press, + }.Add(gtx.Ops) + for ie.newInstrumentBtn.Clicked() { + t.AddInstrument(true) + } + btnStyle := IconButton(t.Theme, ie.newInstrumentBtn, icons.ContentAdd, t.CanAddInstrument()) + ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx C) D { + return layout.Flex{}.Layout( + gtx, + layout.Flexed(1, func(gtx C) D { + return layout.Stack{}.Layout(gtx, + layout.Stacked(func(gtx C) D { + return ie.layoutInstrumentNames(gtx, t) + }), + layout.Expanded(func(gtx C) D { + return ie.instrumentScrollBar.Layout(gtx, unit.Dp(6), len(t.Song().Patch), &ie.instrumentDragList.List.Position) + }), + ) + }), + layout.Rigid(func(gtx C) D { + return layout.E.Layout(gtx, btnStyle.Layout) + }), + ) + }), + layout.Rigid(func(gtx C) D { + return ie.layoutInstrumentHeader(gtx, t) + }), + layout.Flexed(1, func(gtx C) D { + return ie.layoutInstrumentEditor(gtx, t) + })) + return ret +} + +func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D { + header := func(gtx C) D { + collapseIcon := icons.NavigationExpandLess + if ie.commentExpanded { + collapseIcon = icons.NavigationExpandMore + } + + commentExpandBtnStyle := IconButton(t.Theme, ie.commentExpandBtn, collapseIcon, true) + copyInstrumentBtnStyle := IconButton(t.Theme, ie.copyInstrumentBtn, icons.ContentContentCopy, true) + saveInstrumentBtnStyle := IconButton(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, true) + loadInstrumentBtnStyle := IconButton(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, true) + deleteInstrumentBtnStyle := IconButton(t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, t.CanDeleteInstrument()) + + header := func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(Label("Voices: ", white)), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + maxRemain := t.MaxInstrumentVoices() + t.InstrumentVoices.Value = t.Instrument().NumVoices + numStyle := NumericUpDown(t.Theme, t.InstrumentVoices, 0, maxRemain) + gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20)) + gtx.Constraints.Min.X = gtx.Px(unit.Dp(70)) + dims := numStyle.Layout(gtx) + t.SetInstrumentVoices(t.InstrumentVoices.Value) + return dims + }), + layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), + layout.Rigid(commentExpandBtnStyle.Layout), + layout.Rigid(saveInstrumentBtnStyle.Layout), + layout.Rigid(loadInstrumentBtnStyle.Layout), + layout.Rigid(copyInstrumentBtnStyle.Layout), + layout.Rigid(deleteInstrumentBtnStyle.Layout)) + } + for ie.commentExpandBtn.Clicked() { + ie.commentExpanded = !ie.commentExpanded + if !ie.commentExpanded { + key.FocusOp{Tag: &ie.tag}.Add(gtx.Ops) // clear focus + } + } + if ie.commentExpanded || ie.commentEditor.Focused() { // we draw once the widget after it manages to lose focus + if ie.commentEditor.Text() != t.Instrument().Comment { + ie.commentEditor.SetText(t.Instrument().Comment) + } + editorStyle := material.Editor(t.Theme, ie.commentEditor, "Comment") + editorStyle.Color = highEmphasisTextColor + + ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(header), + layout.Rigid(func(gtx C) D { + spy, spiedGtx := eventx.Enspy(gtx) + ret := layout.UniformInset(unit.Dp(6)).Layout(spiedGtx, editorStyle.Layout) + for _, group := range spy.AllEvents() { + for _, event := range group.Items { + switch e := event.(type) { + case key.Event: + if e.Name == key.NameEscape { + ie.instrumentDragList.Focus() + } + } + } + } + return ret + }), + ) + t.SetInstrumentComment(ie.commentEditor.Text()) + return ret + } + return header(gtx) + } + for ie.copyInstrumentBtn.Clicked() { + contents, err := yaml.Marshal(t.Instrument()) + if err == nil { + clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops) + t.Alert.Update("Instrument copied to clipboard", Notify, time.Second*3) + } + } + for ie.deleteInstrumentBtn.Clicked() && t.ModalDialog == nil { + dialogStyle := ConfirmDialog(t.Theme, ie.confirmInstrDelete, "Are you sure you want to delete this instrument?") + ie.confirmInstrDelete.Visible = true + t.ModalDialog = dialogStyle.Layout + } + for ie.confirmInstrDelete.BtnOk.Clicked() { + t.DeleteInstrument(false) + t.ModalDialog = nil + } + for ie.confirmInstrDelete.BtnCancel.Clicked() { + t.ModalDialog = nil + } + for ie.saveInstrumentBtn.Clicked() { + t.SaveInstrument() + } + + for ie.loadInstrumentBtn.Clicked() { + t.LoadInstrument() + } + return Surface{Gray: 37, Focus: ie.wasFocused}.Layout(gtx, header) +} + +func (ie *InstrumentEditor) layoutInstrumentNames(gtx C, t *Tracker) D { + element := func(gtx C, i int) D { + gtx.Constraints.Min.Y = gtx.Px(unit.Dp(36)) + gtx.Constraints.Min.X = gtx.Px(unit.Dp(30)) + grabhandle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(10), Alignment: layout.Center} + if i == t.InstrIndex() { + grabhandle.Text = ":::" + } + label := func(gtx C) D { + c := 0.0 + voice := t.Song().Patch.FirstVoiceForInstrument(i) + for j := 0; j < t.Song().Patch[i].NumVoices; j++ { + released, event := t.player.VoiceState(voice) + vc := math.Exp(-float64(event)/15000) * .5 + if !released { + vc += .5 + } + if c < vc { + c = vc + } + voice++ + } + k := byte(255 - c*127) + color := color.NRGBA{R: 255, G: k, B: 255, A: 255} + if i == t.InstrIndex() { + for _, ev := range ie.nameEditor.Events() { + _, ok := ev.(widget.SubmitEvent) + if ok { + ie.instrumentDragList.Focus() + continue + } + } + if n := t.Instrument().Name; n != ie.nameEditor.Text() { + ie.nameEditor.SetText(n) + } + editor := material.Editor(t.Theme, ie.nameEditor, "Instr") + editor.Color = color + editor.HintColor = instrumentNameHintColor + editor.TextSize = unit.Dp(12) + dims := layout.Center.Layout(gtx, editor.Layout) + t.SetInstrumentName(ie.nameEditor.Text()) + return dims + } + text := t.Song().Patch[i].Name + if text == "" { + text = "Instr" + } + labelStyle := LabelStyle{Text: text, ShadeColor: black, Color: color, FontSize: unit.Sp(12)} + return layout.Center.Layout(gtx, labelStyle.Layout) + } + 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), + ) + }) + } + + color := inactiveLightSurfaceColor + if ie.wasFocused { + color = activeLightSurfaceColor + } + instrumentList := FilledDragList(t.Theme, ie.instrumentDragList, len(t.Song().Patch), element, t.SwapInstruments) + instrumentList.SelectedColor = color + instrumentList.HoverColor = instrumentHoverColor + + ie.instrumentDragList.SelectedItem = t.InstrIndex() + defer op.Save(gtx.Ops).Load() + pointer.PassOp{Pass: true}.Add(gtx.Ops) + spy, spiedGtx := eventx.Enspy(gtx) + dims := instrumentList.Layout(spiedGtx) + for _, group := range spy.AllEvents() { + for _, event := range group.Items { + switch e := event.(type) { + case key.Event: + if e.Modifiers.Contain(key.ModShortcut) { + continue + } + if !ie.nameEditor.Focused() { + switch e.State { + case key.Press: + switch e.Name { + case key.NameDownArrow: + ie.unitDragList.Focus() + case key.NameReturn, key.NameEnter: + ie.nameEditor.Focus() + } + t.JammingPressed(e) + case key.Release: + t.JammingReleased(e) + } + } + } + } + } + if t.InstrIndex() != ie.instrumentDragList.SelectedItem { + t.SetInstrIndex(ie.instrumentDragList.SelectedItem) + op.InvalidateOp{}.Add(gtx.Ops) + } + return dims +} +func (ie *InstrumentEditor) layoutInstrumentEditor(gtx C, t *Tracker) D { + for ie.addUnitBtn.Clicked() { + t.AddUnit(true) + ie.unitDragList.Focus() + } + addUnitBtnStyle := material.IconButton(t.Theme, ie.addUnitBtn, widgetForIcon(icons.ContentAdd)) + addUnitBtnStyle.Color = t.Theme.ContrastFg + addUnitBtnStyle.Background = t.Theme.Fg + addUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(4)) + + units := t.Instrument().Units + for len(ie.stackUse) < len(units) { + ie.stackUse = append(ie.stackUse, 0) + } + + stackHeight := 0 + for i, u := range units { + stackHeight += u.StackChange() + ie.stackUse[i] = stackHeight + } + + element := func(gtx C, i int) D { + gtx.Constraints = layout.Exact(image.Pt(gtx.Px(unit.Dp(120)), gtx.Px(unit.Dp(20)))) + u := units[i] + unitNameLabel := LabelStyle{Text: u.Type, ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12)} + if unitNameLabel.Text == "" { + unitNameLabel.Text = "---" + unitNameLabel.Alignment = layout.Center + } + var stackText string + if i < len(ie.stackUse) { + stackText = strconv.FormatInt(int64(ie.stackUse[i]), 10) + var prevStackUse int + if i > 0 { + prevStackUse = ie.stackUse[i-1] + } + if stackNeed := u.StackNeed(); stackNeed > prevStackUse { + unitNameLabel.Color = errorColor + typeString := u.Type + if u.Parameters["stereo"] == 1 { + typeString += " (stereo)" + } + t.Alert.Update(fmt.Sprintf("%v needs at least %v input signals, got %v", typeString, stackNeed, prevStackUse), Error, 0) + } else if i == len(units)-1 && ie.stackUse[i] != 0 { + unitNameLabel.Color = warningColor + t.Alert.Update(fmt.Sprintf("Instrument leaves %v signal(s) on the stack", ie.stackUse[i]), Warning, 0) + } + } + stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12)} + rightMargin := layout.Inset{Right: unit.Dp(10)} + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Flexed(1, unitNameLabel.Layout), + layout.Rigid(func(gtx C) D { + return rightMargin.Layout(gtx, stackLabel.Layout) + }), + ) + } + + unitList := FilledDragList(t.Theme, ie.unitDragList, len(units), element, t.SwapUnits) + ie.unitDragList.SelectedItem = t.UnitIndex() + return Surface{Gray: 30, Focus: ie.wasFocused}.Layout(gtx, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(func(gtx C) D { + return layout.Stack{Alignment: layout.SE}.Layout(gtx, + layout.Expanded(func(gtx C) D { + spy, spiedGtx := eventx.Enspy(gtx) + dims := unitList.Layout(spiedGtx) + prevUnitIndex := t.UnitIndex() + if t.UnitIndex() != ie.unitDragList.SelectedItem { + t.SetUnitIndex(ie.unitDragList.SelectedItem) + } + for _, group := range spy.AllEvents() { + for _, event := range group.Items { + switch e := event.(type) { + case key.Event: + switch e.State { + case key.Press: + switch e.Name { + case key.NameUpArrow: + if prevUnitIndex == 0 { + ie.instrumentDragList.Focus() + } + case key.NameRightArrow: + ie.paramEditor.Focus() + case key.NameDeleteForward, key.NameDeleteBackward: + t.DeleteUnit(e.Name == key.NameDeleteForward) + case key.NameReturn: + t.AddUnit(!e.Modifiers.Contain(key.ModShortcut)) + } + name := e.Name + if !e.Modifiers.Contain(key.ModShift) { + name = strings.ToLower(name) + } + if val, ok := unitKeyMap[name]; ok { + if e.Modifiers.Contain(key.ModShortcut) { + t.SetUnitType(val) + continue + } + } + if e.Modifiers.Contain(key.ModShortcut) { + continue + } + t.JammingPressed(e) + case key.Release: + t.JammingReleased(e) + } + } + } + } + return dims + }), + layout.Expanded(func(gtx C) D { + return ie.unitScrollBar.Layout(gtx, unit.Dp(10), len(t.Instrument().Units), &ie.unitDragList.List.Position) + }), + layout.Stacked(func(gtx C) D { + margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)} + return margin.Layout(gtx, addUnitBtnStyle.Layout) + })) + }), + layout.Rigid(ie.paramEditor.Bind(t))) + }) +} diff --git a/tracker/gioui/instruments.go b/tracker/gioui/instruments.go deleted file mode 100644 index 3457465..0000000 --- a/tracker/gioui/instruments.go +++ /dev/null @@ -1,338 +0,0 @@ -package gioui - -import ( - "fmt" - "image" - "image/color" - "math" - "strconv" - "time" - - "gioui.org/io/clipboard" - "gioui.org/io/key" - "gioui.org/io/pointer" - "gioui.org/layout" - "gioui.org/op" - "gioui.org/text" - "gioui.org/unit" - "gioui.org/widget" - "gioui.org/widget/material" - "gioui.org/x/eventx" - "github.com/vsariola/sointu/tracker" - "golang.org/x/exp/shiny/materialdesign/icons" - "gopkg.in/yaml.v3" -) - -var instrumentPointerTag = false - -func (t *Tracker) layoutInstruments(gtx C) D { - for _, ev := range gtx.Events(&instrumentPointerTag) { - e, ok := ev.(pointer.Event) - if !ok { - continue - } - if e.Type == pointer.Press && (t.EditMode() != tracker.EditUnits && t.EditMode() != tracker.EditParameters) { - t.SetEditMode(tracker.EditUnits) - } - } - rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y) - pointer.Rect(rect).Add(gtx.Ops) - pointer.InputOp{Tag: &instrumentPointerTag, - Types: pointer.Press, - }.Add(gtx.Ops) - for t.NewInstrumentBtn.Clicked() { - t.AddInstrument(true) - } - btnStyle := IconButton(t.Theme, t.NewInstrumentBtn, icons.ContentAdd, t.CanAddInstrument()) - spy, spiedGtx := eventx.Enspy(gtx) - ret := layout.Flex{Axis: layout.Vertical}.Layout(spiedGtx, - layout.Rigid(func(gtx C) D { - return layout.Flex{}.Layout( - gtx, - layout.Flexed(1, func(gtx C) D { - return layout.Stack{}.Layout(gtx, - layout.Stacked(t.layoutInstrumentNames), - layout.Expanded(func(gtx C) D { - return t.InstrumentScrollBar.Layout(gtx, unit.Dp(6), len(t.Song().Patch), &t.InstrumentDragList.List.Position) - }), - ) - }), - layout.Rigid(func(gtx C) D { - return layout.E.Layout(gtx, btnStyle.Layout) - }), - ) - }), - layout.Rigid(t.layoutInstrumentHeader), - layout.Flexed(1, t.layoutInstrumentEditor)) - for _, group := range spy.AllEvents() { - for _, event := range group.Items { - switch e := event.(type) { - case key.Event: - if e.Name == key.NameEscape { - key.FocusOp{}.Add(gtx.Ops) - } - } - } - } - return ret -} - -func (t *Tracker) layoutInstrumentHeader(gtx C) D { - header := func(gtx C) D { - collapseIcon := icons.NavigationExpandLess - if t.InstrumentExpanded { - collapseIcon = icons.NavigationExpandMore - } - - instrumentExpandBtnStyle := IconButton(t.Theme, t.InstrumentExpandBtn, collapseIcon, true) - copyInstrumentBtnStyle := IconButton(t.Theme, t.CopyInstrumentBtn, icons.ContentContentCopy, true) - saveInstrumentBtnStyle := IconButton(t.Theme, t.SaveInstrumentBtn, icons.ContentSave, true) - loadInstrumentBtnStyle := IconButton(t.Theme, t.LoadInstrumentBtn, icons.FileFolderOpen, true) - deleteInstrumentBtnStyle := IconButton(t.Theme, t.DeleteInstrumentBtn, icons.ActionDelete, t.CanDeleteInstrument()) - - header := func(gtx C) D { - return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(Label("Voices: ", white)), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - maxRemain := t.MaxInstrumentVoices() - t.InstrumentVoices.Value = t.Instrument().NumVoices - numStyle := NumericUpDown(t.Theme, t.InstrumentVoices, 0, maxRemain) - gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20)) - gtx.Constraints.Min.X = gtx.Px(unit.Dp(70)) - dims := numStyle.Layout(gtx) - t.SetInstrumentVoices(t.InstrumentVoices.Value) - return dims - }), - layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }), - layout.Rigid(instrumentExpandBtnStyle.Layout), - layout.Rigid(saveInstrumentBtnStyle.Layout), - layout.Rigid(loadInstrumentBtnStyle.Layout), - layout.Rigid(copyInstrumentBtnStyle.Layout), - layout.Rigid(deleteInstrumentBtnStyle.Layout)) - } - for t.InstrumentExpandBtn.Clicked() { - t.InstrumentExpanded = !t.InstrumentExpanded - if !t.InstrumentExpanded { - key.FocusOp{Tag: nil}.Add(gtx.Ops) // clear focus - } - } - if t.InstrumentExpanded || t.InstrumentCommentEditor.Focused() { // we draw once the widget after it manages to lose focus - if t.InstrumentCommentEditor.Text() != t.Instrument().Comment { - t.InstrumentCommentEditor.SetText(t.Instrument().Comment) - } - editorStyle := material.Editor(t.Theme, t.InstrumentCommentEditor, "Comment") - editorStyle.Color = highEmphasisTextColor - - spy, spiedGtx := eventx.Enspy(gtx) - ret := layout.Flex{Axis: layout.Vertical}.Layout(spiedGtx, - layout.Rigid(header), - layout.Rigid(func(gtx C) D { - return layout.UniformInset(unit.Dp(6)).Layout(gtx, editorStyle.Layout) - }), - ) - for _, group := range spy.AllEvents() { - for _, event := range group.Items { - switch e := event.(type) { - case key.Event: - if e.Name == key.NameEscape { - key.FocusOp{}.Add(gtx.Ops) - } - } - } - } - t.SetInstrumentComment(t.InstrumentCommentEditor.Text()) - return ret - } - return header(gtx) - } - for t.CopyInstrumentBtn.Clicked() { - contents, err := yaml.Marshal(t.Instrument()) - if err == nil { - clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops) - t.Alert.Update("Instrument copied to clipboard", Notify, time.Second*3) - } - } - for t.DeleteInstrumentBtn.Clicked() { - t.ConfirmInstrDelete.Visible = true - } - for t.ConfirmInstrDelete.BtnOk.Clicked() { - t.DeleteInstrument(false) - t.ConfirmInstrDelete.Visible = false - } - for t.ConfirmInstrDelete.BtnCancel.Clicked() { - t.ConfirmInstrDelete.Visible = false - } - for t.SaveInstrumentBtn.Clicked() { - t.SaveInstrument() - } - - for t.LoadInstrumentBtn.Clicked() { - t.LoadInstrument() - } - return Surface{Gray: 37, Focus: t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.EditParameters}.Layout(gtx, header) -} - -func (t *Tracker) layoutInstrumentNames(gtx C) D { - element := func(gtx C, i int) D { - gtx.Constraints.Min.Y = gtx.Px(unit.Dp(36)) - gtx.Constraints.Min.X = gtx.Px(unit.Dp(30)) - grabhandle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(10), Alignment: layout.Center} - if i == t.InstrIndex() { - grabhandle.Text = ":::" - } - label := func(gtx C) D { - c := 0.0 - voice := t.Song().Patch.FirstVoiceForInstrument(i) - for j := 0; j < t.Song().Patch[i].NumVoices; j++ { - released, event := t.player.VoiceState(voice) - vc := math.Exp(-float64(event)/15000) * .5 - if !released { - vc += .5 - } - if c < vc { - c = vc - } - voice++ - } - k := byte(255 - c*127) - color := color.NRGBA{R: 255, G: k, B: 255, A: 255} - if i == t.InstrIndex() { - for _, ev := range t.InstrumentNameEditor.Events() { - _, ok := ev.(widget.SubmitEvent) - if ok { - t.InstrumentNameEditor = &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle} // TODO: is there any other way to defocus the editor - break - } - } - if n := t.Instrument().Name; n != t.InstrumentNameEditor.Text() { - t.InstrumentNameEditor.SetText(n) - } - editor := material.Editor(t.Theme, t.InstrumentNameEditor, "Instr") - editor.Color = color - editor.HintColor = instrumentNameHintColor - editor.TextSize = unit.Dp(12) - dims := layout.Center.Layout(gtx, editor.Layout) - t.SetInstrumentName(t.InstrumentNameEditor.Text()) - return dims - } - text := t.Song().Patch[i].Name - if text == "" { - text = "Instr" - } - labelStyle := LabelStyle{Text: text, ShadeColor: black, Color: color, FontSize: unit.Sp(12)} - return layout.Center.Layout(gtx, labelStyle.Layout) - } - 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), - ) - }) - } - - color := inactiveLightSurfaceColor - if t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.EditParameters { - color = activeLightSurfaceColor - } - instrumentList := FilledDragList(t.Theme, t.InstrumentDragList, len(t.Song().Patch), element, t.SwapInstruments) - instrumentList.SelectedColor = color - instrumentList.HoverColor = instrumentHoverColor - - t.InstrumentDragList.SelectedItem = t.InstrIndex() - defer op.Save(gtx.Ops).Load() - pointer.PassOp{Pass: true}.Add(gtx.Ops) - dims := instrumentList.Layout(gtx) - if t.InstrIndex() != t.InstrumentDragList.SelectedItem { - t.SetInstrIndex(t.InstrumentDragList.SelectedItem) - op.InvalidateOp{}.Add(gtx.Ops) - } - return dims -} -func (t *Tracker) layoutInstrumentEditor(gtx C) D { - for t.AddUnitBtn.Clicked() { - t.AddUnit(true) - } - addUnitBtnStyle := material.IconButton(t.Theme, t.AddUnitBtn, widgetForIcon(icons.ContentAdd)) - addUnitBtnStyle.Color = t.Theme.ContrastFg - addUnitBtnStyle.Background = t.Theme.Fg - addUnitBtnStyle.Inset = layout.UniformInset(unit.Dp(4)) - - units := t.Instrument().Units - for len(t.StackUse) < len(units) { - t.StackUse = append(t.StackUse, 0) - } - - stackHeight := 0 - for i, u := range units { - stackHeight += u.StackChange() - t.StackUse[i] = stackHeight - } - - element := func(gtx C, i int) D { - gtx.Constraints = layout.Exact(image.Pt(gtx.Px(unit.Dp(120)), gtx.Px(unit.Dp(20)))) - u := units[i] - unitNameLabel := LabelStyle{Text: u.Type, ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12)} - if unitNameLabel.Text == "" { - unitNameLabel.Text = "---" - unitNameLabel.Alignment = layout.Center - } - var stackText string - if i < len(t.StackUse) { - stackText = strconv.FormatInt(int64(t.StackUse[i]), 10) - var prevStackUse int - if i > 0 { - prevStackUse = t.StackUse[i-1] - } - if stackNeed := u.StackNeed(); stackNeed > prevStackUse { - unitNameLabel.Color = errorColor - typeString := u.Type - if u.Parameters["stereo"] == 1 { - typeString += " (stereo)" - } - t.Alert.Update(fmt.Sprintf("%v needs at least %v input signals, got %v", typeString, stackNeed, prevStackUse), Error, 0) - } else if i == len(units)-1 && t.StackUse[i] != 0 { - unitNameLabel.Color = warningColor - t.Alert.Update(fmt.Sprintf("Instrument leaves %v signal(s) on the stack", t.StackUse[i]), Warning, 0) - } - } - stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12)} - rightMargin := layout.Inset{Right: unit.Dp(10)} - return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Flexed(1, unitNameLabel.Layout), - layout.Rigid(func(gtx C) D { - return rightMargin.Layout(gtx, stackLabel.Layout) - }), - ) - } - - unitList := FilledDragList(t.Theme, t.UnitDragList, len(units), element, t.SwapUnits) - - if t.EditMode() == tracker.EditUnits { - unitList.SelectedColor = cursorColor - } - - t.UnitDragList.SelectedItem = t.UnitIndex() - return Surface{Gray: 30, Focus: t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.EditParameters}.Layout(gtx, func(gtx C) D { - return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return layout.Stack{Alignment: layout.SE}.Layout(gtx, - layout.Expanded(func(gtx C) D { - dims := unitList.Layout(gtx) - if t.UnitIndex() != t.UnitDragList.SelectedItem { - t.SetUnitIndex(t.UnitDragList.SelectedItem) - t.SetEditMode(tracker.EditUnits) - op.InvalidateOp{}.Add(gtx.Ops) - } - return dims - }), - layout.Expanded(func(gtx C) D { - return t.UnitScrollBar.Layout(gtx, unit.Dp(10), len(t.Instrument().Units), &t.UnitDragList.List.Position) - }), - layout.Stacked(func(gtx C) D { - margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)} - return margin.Layout(gtx, addUnitBtnStyle.Layout) - })) - }), - layout.Rigid(t.layoutUnitEditor)) - }) -} diff --git a/tracker/gioui/keyevent.go b/tracker/gioui/keyevent.go index f16b913..14ef722 100644 --- a/tracker/gioui/keyevent.go +++ b/tracker/gioui/keyevent.go @@ -1,11 +1,8 @@ package gioui import ( - "strconv" - "strings" "time" - "gioui.org/app" "gioui.org/io/key" "github.com/vsariola/sointu/tracker" "gopkg.in/yaml.v3" @@ -79,11 +76,9 @@ var unitKeyMap = map[string]string{ } // KeyEvent handles incoming key events and returns true if repaint is needed. -func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool { +func (t *Tracker) KeyEvent(e key.Event) bool { if e.State == key.Press { - if t.InstrumentNameEditor.Focused() || - t.InstrumentCommentEditor.Focused() || - t.OpenSongDialog.Visible || + if t.OpenSongDialog.Visible || t.SaveSongDialog.Visible || t.SaveInstrumentDialog.Visible || t.OpenInstrumentDialog.Visible || @@ -95,14 +90,14 @@ func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool { if e.Modifiers.Contain(key.ModShortcut) { contents, err := yaml.Marshal(t.Song()) if err == nil { - w.WriteClipboard(string(contents)) + t.window.WriteClipboard(string(contents)) t.Alert.Update("Song copied to clipboard", Notify, time.Second*3) } return true } case "V": if e.Modifiers.Contain(key.ModShortcut) { - w.ReadClipboard() + t.window.ReadClipboard() return true } case "Z": @@ -131,21 +126,21 @@ func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool { return true } case "F1": - t.SetEditMode(tracker.EditPatterns) + t.OrderEditor.Focus() return true case "F2": - t.SetEditMode(tracker.EditTracks) + t.TrackEditor.Focus() return true case "F3": - t.SetEditMode(tracker.EditUnits) + t.InstrumentEditor.Focus() return true case "F4": - t.SetEditMode(tracker.EditParameters) + t.TrackEditor.Focus() return true case "F5": t.SetNoteTracking(true) startRow := t.Cursor().SongRow - if t.EditMode() == tracker.EditPatterns { + if t.OrderEditor.Focused() { startRow.Row = 0 } t.player.Play(startRow) @@ -153,7 +148,7 @@ func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool { case "F6": t.SetNoteTracking(false) startRow := t.Cursor().SongRow - if t.EditMode() == tracker.EditPatterns { + if t.OrderEditor.Focused() { startRow.Row = 0 } t.player.Play(startRow) @@ -161,299 +156,48 @@ func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool { case "F8": t.player.Stop() return true - case key.NameDeleteForward, key.NameDeleteBackward: - switch t.EditMode() { - case tracker.EditPatterns: - if e.Modifiers.Contain(key.ModShortcut) { - t.DeleteOrderRow(e.Name == key.NameDeleteForward) - } else { - t.DeletePatternSelection() - if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 { - t.SetCursor(t.Cursor().AddPatterns(1)) - t.SetSelectionCorner(t.Cursor()) - } - } - return true - case tracker.EditTracks: - t.DeleteSelection() - if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 { - t.SetCursor(t.Cursor().AddRows(t.Step.Value)) - t.SetSelectionCorner(t.Cursor()) - } - return true - case tracker.EditUnits: - t.DeleteUnit(e.Name == key.NameDeleteForward) - return true - } case "Space": _, playing := t.player.Position() if !playing { t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut)) startRow := t.Cursor().SongRow - if t.EditMode() == tracker.EditPatterns { - startRow.Row = 0 - } t.player.Play(startRow) } else { t.player.Stop() } - return true case `\`, `<`, `>`: if e.Modifiers.Contain(key.ModShift) { - return t.SetOctave(t.Octave() + 1) + t.SetOctave(t.Octave() + 1) + } else { + t.SetOctave(t.Octave() - 1) } - return t.SetOctave(t.Octave() - 1) case key.NameTab: if e.Modifiers.Contain(key.ModShift) { - t.SetEditMode((t.EditMode() - 1 + 4) % 4) - } else { - t.SetEditMode((t.EditMode() + 1) % 4) - } - return true - case key.NameReturn: - switch t.EditMode() { - case tracker.EditPatterns: - t.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut)) - case tracker.EditUnits: - t.AddUnit(!e.Modifiers.Contain(key.ModShortcut)) - } - case key.NameUpArrow: - cursor := t.Cursor() - switch t.EditMode() { - case tracker.EditPatterns: - if e.Modifiers.Contain(key.ModShortcut) { - cursor.SongRow = tracker.SongRow{} - } else { - cursor.Row -= t.Song().Score.RowsPerPattern - } - t.SetNoteTracking(false) - case tracker.EditTracks: - if e.Modifiers.Contain(key.ModShortcut) { - cursor.Row -= t.Song().Score.RowsPerPattern - } else { - if t.Step.Value > 0 { - cursor.Row -= t.Step.Value - } else { - cursor.Row-- - } - } - t.SetNoteTracking(false) - case tracker.EditUnits: - t.SetUnitIndex(t.UnitIndex() - 1) - case tracker.EditParameters: - t.SetParamIndex(t.ParamIndex() - 1) - } - t.SetCursor(cursor) - if !e.Modifiers.Contain(key.ModShift) { - t.SetSelectionCorner(t.Cursor()) - } - scrollToView(t.PatternOrderList, t.Cursor().Pattern, t.Song().Score.Length) - return true - case key.NameDownArrow: - cursor := t.Cursor() - switch t.EditMode() { - case tracker.EditPatterns: - if e.Modifiers.Contain(key.ModShortcut) { - cursor.Row = t.Song().Score.LengthInRows() - 1 - } else { - cursor.Row += t.Song().Score.RowsPerPattern - } - t.SetNoteTracking(false) - case tracker.EditTracks: - if e.Modifiers.Contain(key.ModShortcut) { - cursor.Row += t.Song().Score.RowsPerPattern - } else { - if t.Step.Value > 0 { - cursor.Row += t.Step.Value - } else { - cursor.Row++ - } - } - t.SetNoteTracking(false) - case tracker.EditUnits: - t.SetUnitIndex(t.UnitIndex() + 1) - case tracker.EditParameters: - t.SetParamIndex(t.ParamIndex() + 1) - } - t.SetCursor(cursor) - if !e.Modifiers.Contain(key.ModShift) { - t.SetSelectionCorner(t.Cursor()) - } - scrollToView(t.PatternOrderList, t.Cursor().Pattern, t.Song().Score.Length) - return true - case key.NameLeftArrow: - cursor := t.Cursor() - switch t.EditMode() { - case tracker.EditPatterns: - if e.Modifiers.Contain(key.ModShortcut) { - cursor.Track = 0 - } else { - cursor.Track-- - } - case tracker.EditTracks: - if !t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) { - cursor.Track-- - t.SetLowNibble(true) - } else { - t.SetLowNibble(false) - } - case tracker.EditUnits: - t.SetInstrIndex(t.InstrIndex() - 1) - case tracker.EditParameters: - param, _ := t.Param(t.ParamIndex()) - if e.Modifiers.Contain(key.ModShift) { - p, err := t.Param(t.ParamIndex()) - if err == nil { - t.SetParam(param.Value - p.LargeStep) - } - } else { - t.SetParam(param.Value - 1) - } - } - t.SetCursor(cursor) - if !e.Modifiers.Contain(key.ModShift) { - t.SetSelectionCorner(t.Cursor()) - } - return true - case key.NameRightArrow: - switch t.EditMode() { - case tracker.EditPatterns: - cursor := t.Cursor() - if e.Modifiers.Contain(key.ModShortcut) { - cursor.Track = len(t.Song().Score.Tracks) - 1 - } else { - cursor.Track++ - } - t.SetCursor(cursor) - case tracker.EditTracks: - if t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) { - cursor := t.Cursor() - cursor.Track++ - t.SetCursor(cursor) - t.SetLowNibble(false) - } else { - t.SetLowNibble(true) - } - case tracker.EditUnits: - t.SetInstrIndex(t.InstrIndex() + 1) - case tracker.EditParameters: - param, _ := t.Param(t.ParamIndex()) - if e.Modifiers.Contain(key.ModShift) { - p, err := t.Param(t.ParamIndex()) - if err == nil { - t.SetParam(param.Value + p.LargeStep) - } - } else { - t.SetParam(param.Value + 1) - } - } - if !e.Modifiers.Contain(key.ModShift) { - t.SetSelectionCorner(t.Cursor()) - } - return true - case "+": - switch t.EditMode() { - case tracker.EditTracks: - if e.Modifiers.Contain(key.ModShortcut) { - t.AdjustSelectionPitch(12) - } else { - t.AdjustSelectionPitch(1) - } - return true - } - case "-": - switch t.EditMode() { - case tracker.EditTracks: - if e.Modifiers.Contain(key.ModShortcut) { - t.AdjustSelectionPitch(-12) - } else { - t.AdjustSelectionPitch(-1) - } - return true - } - } - switch t.EditMode() { - case tracker.EditPatterns: - if iv, err := strconv.Atoi(e.Name); err == nil { - t.SetCurrentPattern(iv) - if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 { - t.SetCursor(t.Cursor().AddPatterns(1)) - t.SetSelectionCorner(t.Cursor()) - } - return true - } - if b := int(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 { - t.SetCurrentPattern(b + 10) - if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 { - t.SetCursor(t.Cursor().AddPatterns(1)) - t.SetSelectionCorner(t.Cursor()) - } - return true - } - case tracker.EditTracks: - step := false - if t.Song().Score.Tracks[t.Cursor().Track].Effect { - if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil { - t.NumberPressed(byte(iv)) - step = true + switch { + case t.OrderEditor.Focused(): + t.InstrumentEditor.paramEditor.Focus() + case t.TrackEditor.Focused(): + t.OrderEditor.Focus() + case t.InstrumentEditor.Focused(): + t.TrackEditor.Focus() + default: + t.InstrumentEditor.Focus() } } else { - if e.Name == "A" || e.Name == "1" { - t.SetNote(0) - step = true - } else { - if val, ok := noteMap[e.Name]; ok { - if _, ok := t.KeyPlaying[e.Name]; !ok { - n := tracker.NoteAsValue(t.OctaveNumberInput.Value, val) - t.SetNote(n) - step = true - trk := t.Cursor().Track - start := t.Song().Score.FirstVoiceForTrack(trk) - end := start + t.Song().Score.Tracks[trk].NumVoices - t.KeyPlaying[e.Name] = t.player.Trigger(start, end, n) - } - } - } - } - if step && !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 { - t.SetCursor(t.Cursor().AddRows(t.Step.Value)) - t.SetSelectionCorner(t.Cursor()) - } - return true - case tracker.EditUnits: - name := e.Name - if !e.Modifiers.Contain(key.ModShift) { - name = strings.ToLower(name) - } - if val, ok := unitKeyMap[name]; ok { - if e.Modifiers.Contain(key.ModShortcut) { - t.SetUnitType(val) - return true - } - } - fallthrough - case tracker.EditParameters: - if val, ok := noteMap[e.Name]; ok { - if _, ok := t.KeyPlaying[e.Name]; !ok { - n := tracker.NoteAsValue(t.OctaveNumberInput.Value, val) - instr := t.InstrIndex() - start := t.Song().Patch.FirstVoiceForInstrument(instr) - end := start + t.Instrument().NumVoices - t.KeyPlaying[e.Name] = t.player.Trigger(start, end, n) - return false + switch { + case t.OrderEditor.Focused(): + t.TrackEditor.Focus() + case t.TrackEditor.Focused(): + t.InstrumentEditor.Focus() + case t.InstrumentEditor.Focused(): + t.InstrumentEditor.paramEditor.Focus() + default: + t.OrderEditor.Focus() } } } } - if e.State == key.Release { - if ID, ok := t.KeyPlaying[e.Name]; ok { - t.player.Release(ID) - delete(t.KeyPlaying, e.Name) - if _, playing := t.player.Position(); t.EditMode() == tracker.EditTracks && playing && t.Note() == 1 && t.NoteTracking() { - t.SetNote(0) - } - } - } + return false } @@ -470,3 +214,25 @@ func (t *Tracker) NumberPressed(iv byte) { } t.SetNote(val) } + +func (t *Tracker) JammingPressed(e key.Event) { + if val, ok := noteMap[e.Name]; ok { + if _, ok := t.KeyPlaying[e.Name]; !ok { + n := tracker.NoteAsValue(t.OctaveNumberInput.Value, val) + instr := t.InstrIndex() + start := t.Song().Patch.FirstVoiceForInstrument(instr) + end := start + t.Instrument().NumVoices + t.KeyPlaying[e.Name] = t.player.Trigger(start, end, n) + } + } +} + +func (t *Tracker) JammingReleased(e key.Event) { + if ID, ok := t.KeyPlaying[e.Name]; ok { + t.player.Release(ID) + delete(t.KeyPlaying, e.Name) + if _, playing := t.player.Position(); t.TrackEditor.focused && playing && t.Note() == 1 && t.NoteTracking() { + t.SetNote(0) + } + } +} diff --git a/tracker/gioui/layout.go b/tracker/gioui/layout.go index 8b4b4d6..235b272 100644 --- a/tracker/gioui/layout.go +++ b/tracker/gioui/layout.go @@ -8,7 +8,6 @@ import ( "gioui.org/layout" "gioui.org/op/clip" "gioui.org/op/paint" - "github.com/vsariola/sointu/tracker" ) type C = layout.Context @@ -20,9 +19,7 @@ func (t *Tracker) Layout(gtx layout.Context) { t.layoutTop, t.layoutBottom) t.Alert.Layout(gtx) - dstyle := ConfirmDialog(t.Theme, t.ConfirmInstrDelete, "Are you sure you want to delete this instrument?") - dstyle.Layout(gtx) - dstyle = ConfirmDialog(t.Theme, t.ConfirmSongDialog, "Do you want to save your changes to the song? Your changes will be lost if you don't save them.") + dstyle := ConfirmDialog(t.Theme, t.ConfirmSongDialog, "Do you want to save your changes to the song? Your changes will be lost if you don't save them.") dstyle.ShowAlt = true dstyle.OkStyle.Text = "Save" dstyle.AltStyle.Text = "Don't save" @@ -93,6 +90,9 @@ func (t *Tracker) Layout(gtx layout.Context) { t.loadInstrument(file) } fstyle.Layout(gtx) + if t.ModalDialog != nil { + t.ModalDialog(gtx) + } } func (t *Tracker) confirmedSongAction() { @@ -122,10 +122,10 @@ func (t *Tracker) NewSong(forced bool) { func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions { return t.BottomHorizontalSplit.Layout(gtx, func(gtx C) D { - return Surface{Gray: 24, Focus: t.EditMode() == tracker.EditPatterns}.Layout(gtx, t.layoutPatterns) + return t.OrderEditor.Layout(gtx, t) }, func(gtx C) D { - return Surface{Gray: 24, Focus: t.EditMode() == tracker.EditTracks}.Layout(gtx, t.layoutTracker) + return t.TrackEditor.Layout(gtx, t) }, ) } @@ -133,6 +133,8 @@ func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions { func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions { return t.TopHorizontalSplit.Layout(gtx, t.layoutSongPanel, - t.layoutInstruments, + func(gtx C) D { + return t.InstrumentEditor.Layout(gtx, t) + }, ) } diff --git a/tracker/gioui/ordereditor.go b/tracker/gioui/ordereditor.go new file mode 100644 index 0000000..8704f6c --- /dev/null +++ b/tracker/gioui/ordereditor.go @@ -0,0 +1,247 @@ +package gioui + +import ( + "fmt" + "image" + "strconv" + "strings" + + "gioui.org/f32" + "gioui.org/io/key" + "gioui.org/io/pointer" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/text" + "gioui.org/unit" + "gioui.org/widget" + "github.com/vsariola/sointu/tracker" +) + +const patternCellHeight = 16 +const patternCellWidth = 16 +const patternRowMarkerWidth = 30 + +type OrderEditor struct { + list *layout.List + scrollBar *ScrollBar + tag bool + focused bool + requestFocus bool +} + +func NewOrderEditor() *OrderEditor { + return &OrderEditor{ + list: &layout.List{Axis: layout.Vertical}, + scrollBar: &ScrollBar{Axis: layout.Vertical}, + } +} + +func (oe *OrderEditor) Focus() { + oe.requestFocus = true +} + +func (oe *OrderEditor) Focused() bool { + return oe.focused +} + +func (oe *OrderEditor) Layout(gtx C, t *Tracker) D { + return Surface{Gray: 24, Focus: oe.focused}.Layout(gtx, func(gtx C) D { + return oe.doLayout(gtx, t) + }) +} + +func (oe *OrderEditor) doLayout(gtx C, t *Tracker) D { + for _, e := range gtx.Events(&oe.tag) { + switch e := e.(type) { + case key.FocusEvent: + oe.focused = e.Focus + case pointer.Event: + if e.Type == pointer.Press { + key.FocusOp{Tag: &oe.tag}.Add(gtx.Ops) + } + case key.Event: + if !oe.focused || e.State != key.Press { + continue + } + switch e.Name { + case key.NameDeleteForward, key.NameDeleteBackward: + if e.Modifiers.Contain(key.ModShortcut) { + t.DeleteOrderRow(e.Name == key.NameDeleteForward) + } else { + t.DeletePatternSelection() + if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 { + t.SetCursor(t.Cursor().AddPatterns(1)) + t.SetSelectionCorner(t.Cursor()) + } + } + case "Space": + _, playing := t.player.Position() + if !playing { + t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut)) + startRow := t.Cursor().SongRow + startRow.Row = 0 + t.player.Play(startRow) + } else { + t.player.Stop() + } + case key.NameReturn: + t.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut)) + case key.NameUpArrow: + cursor := t.Cursor() + if e.Modifiers.Contain(key.ModShortcut) { + cursor.SongRow = tracker.SongRow{} + } else { + cursor.Row -= t.Song().Score.RowsPerPattern + } + t.SetNoteTracking(false) + t.SetCursor(cursor) + case key.NameDownArrow: + cursor := t.Cursor() + if e.Modifiers.Contain(key.ModShortcut) { + cursor.Row = t.Song().Score.LengthInRows() - 1 + } else { + cursor.Row += t.Song().Score.RowsPerPattern + } + t.SetNoteTracking(false) + t.SetCursor(cursor) + case key.NameLeftArrow: + cursor := t.Cursor() + if e.Modifiers.Contain(key.ModShortcut) { + cursor.Track = 0 + } else { + cursor.Track-- + } + t.SetCursor(cursor) + case key.NameRightArrow: + cursor := t.Cursor() + if e.Modifiers.Contain(key.ModShortcut) { + cursor.Track = len(t.Song().Score.Tracks) - 1 + } else { + cursor.Track++ + } + t.SetCursor(cursor) + } + if (e.Name != key.NameLeftArrow && + e.Name != key.NameRightArrow && + e.Name != key.NameUpArrow && + e.Name != key.NameDownArrow) || + !e.Modifiers.Contain(key.ModShift) { + t.SetSelectionCorner(t.Cursor()) + } + if e.Modifiers.Contain(key.ModShortcut) { + continue + } + if iv, err := strconv.Atoi(e.Name); err == nil { + t.SetCurrentPattern(iv) + if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 { + t.SetCursor(t.Cursor().AddPatterns(1)) + t.SetSelectionCorner(t.Cursor()) + } + } + if b := int(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 { + t.SetCurrentPattern(b + 10) + if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 { + t.SetCursor(t.Cursor().AddPatterns(1)) + t.SetSelectionCorner(t.Cursor()) + } + } + } + } + defer op.Save(gtx.Ops).Load() + if oe.requestFocus { + oe.requestFocus = false + key.FocusOp{Tag: &oe.tag}.Add(gtx.Ops) + } + clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops) + rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y) + pointer.Rect(rect).Add(gtx.Ops) + pointer.InputOp{Tag: &oe.tag, + Types: pointer.Press, + }.Add(gtx.Ops) + key.InputOp{Tag: &oe.tag}.Add(gtx.Ops) + patternRect := tracker.SongRect{ + Corner1: tracker.SongPoint{SongRow: tracker.SongRow{Pattern: t.Cursor().Pattern}, Track: t.Cursor().Track}, + Corner2: tracker.SongPoint{SongRow: tracker.SongRow{Pattern: t.SelectionCorner().Pattern}, Track: t.SelectionCorner().Track}, + } + + // draw the single letter titles for tracks + { + gtx := gtx + curVoice := 0 + stack := op.Save(gtx.Ops) + op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops) + gtx.Constraints = layout.Exact(image.Pt(patternCellWidth, patternCellHeight)) + for _, track := range t.Song().Score.Tracks { + instr, err := t.Song().Patch.InstrumentForVoice(curVoice) + var title string + if err == nil && len(t.Song().Patch[instr].Name) > 0 { + title = string(t.Song().Patch[instr].Name[0]) + } else { + title = "I" + } + LabelStyle{Alignment: layout.N, Text: title, FontSize: unit.Dp(12), Color: mediumEmphasisTextColor}.Layout(gtx) + op.Offset(f32.Pt(patternCellWidth, 0)).Add(gtx.Ops) + curVoice += track.NumVoices + } + stack.Load() + } + op.Offset(f32.Pt(0, patternCellHeight)).Add(gtx.Ops) + gtx.Constraints.Max.Y -= patternCellHeight + gtx.Constraints.Min.Y -= patternCellHeight + element := func(gtx C, j int) D { + if playPos, ok := t.player.Position(); ok && j == playPos.Pattern { + paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op()) + } + paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops) + widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j))) + stack := op.Save(gtx.Ops) + op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops) + for i, track := range t.Song().Score.Tracks { + paint.FillShape(gtx.Ops, patternCellColor, clip.Rect{Min: image.Pt(1, 1), Max: image.Pt(patternCellWidth-1, patternCellHeight-1)}.Op()) + paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops) + if j >= 0 && j < len(track.Order) && track.Order[j] >= 0 { + gtx := gtx + gtx.Constraints.Max.X = patternCellWidth + op.Offset(f32.Pt(0, -2)).Add(gtx.Ops) + widget.Label{Alignment: text.Middle}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(track.Order[j])) + op.Offset(f32.Pt(0, 2)).Add(gtx.Ops) + } + point := tracker.SongPoint{Track: i, SongRow: tracker.SongRow{Pattern: j}} + if oe.focused || t.TrackEditor.Focused() { + if patternRect.Contains(point) { + color := inactiveSelectionColor + if oe.focused { + color = selectionColor + if point.Pattern == t.Cursor().Pattern && point.Track == t.Cursor().Track { + color = cursorColor + } + } + paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(patternCellWidth, patternCellHeight)}.Op()) + } + } + op.Offset(f32.Pt(patternCellWidth, 0)).Add(gtx.Ops) + } + stack.Load() + return D{Size: image.Pt(gtx.Constraints.Max.X, patternCellHeight)} + } + + return layout.Stack{Alignment: layout.NE}.Layout(gtx, + layout.Expanded(func(gtx C) D { + return oe.list.Layout(gtx, t.Song().Score.Length, element) + }), + layout.Expanded(func(gtx C) D { + return oe.scrollBar.Layout(gtx, unit.Dp(10), t.Song().Score.Length, &oe.list.Position) + }), + ) +} + +func patternIndexToString(index int) string { + if index < 0 { + return "" + } else if index < 10 { + return string('0' + byte(index)) + } + return string('A' + byte(index-10)) +} diff --git a/tracker/gioui/parameditor.go b/tracker/gioui/parameditor.go new file mode 100644 index 0000000..2b7ac0d --- /dev/null +++ b/tracker/gioui/parameditor.go @@ -0,0 +1,240 @@ +package gioui + +import ( + "image" + "image/color" + "strings" + + "gioui.org/io/key" + "gioui.org/io/pointer" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" + "github.com/vsariola/sointu/tracker" + "golang.org/x/exp/shiny/materialdesign/icons" +) + +type ParamEditor struct { + list *layout.List + scrollBar *ScrollBar + Parameters []*ParameterWidget + DeleteUnitBtn *widget.Clickable + ClearUnitBtn *widget.Clickable + ChooseUnitTypeBtns []*widget.Clickable + tag bool + focused bool + requestFocus bool +} + +func (pe *ParamEditor) Focus() { + pe.requestFocus = true +} + +func (pe *ParamEditor) Focused() bool { + return pe.focused +} + +func NewParamEditor() *ParamEditor { + ret := &ParamEditor{ + DeleteUnitBtn: new(widget.Clickable), + ClearUnitBtn: new(widget.Clickable), + list: &layout.List{Axis: layout.Vertical}, + scrollBar: &ScrollBar{Axis: layout.Vertical}, + } + for range tracker.UnitTypeNames { + ret.ChooseUnitTypeBtns = append(ret.ChooseUnitTypeBtns, new(widget.Clickable)) + } + return ret +} + +func (pe *ParamEditor) Bind(t *Tracker) layout.Widget { + return func(gtx C) D { + for _, e := range gtx.Events(&pe.tag) { + switch e := e.(type) { + case key.FocusEvent: + pe.focused = e.Focus + case pointer.Event: + if e.Type == pointer.Press { + key.FocusOp{Tag: &pe.tag}.Add(gtx.Ops) + } + case key.Event: + if e.Modifiers.Contain(key.ModShortcut) { + continue + } + switch e.State { + case key.Press: + switch e.Name { + case key.NameUpArrow: + t.SetParamIndex(t.ParamIndex() - 1) + case key.NameDownArrow: + t.SetParamIndex(t.ParamIndex() + 1) + case key.NameLeftArrow: + p, err := t.Param(t.ParamIndex()) + if err != nil { + break + } + if e.Modifiers.Contain(key.ModShift) { + t.SetParam(p.Value - p.LargeStep) + } else { + t.SetParam(p.Value - 1) + } + case key.NameRightArrow: + p, err := t.Param(t.ParamIndex()) + if err != nil { + break + } + if e.Modifiers.Contain(key.ModShift) { + t.SetParam(p.Value + p.LargeStep) + } else { + t.SetParam(p.Value + 1) + } + case key.NameEscape: + t.InstrumentEditor.unitDragList.Focus() + } + if e.Modifiers.Contain(key.ModShortcut) { + continue + } + t.JammingPressed(e) + case key.Release: + t.JammingReleased(e) + } + } + } + if pe.requestFocus { + pe.requestFocus = false + key.FocusOp{Tag: &pe.tag}.Add(gtx.Ops) + } + editorFunc := pe.layoutUnitSliders + if t.Unit().Type == "" { + editorFunc = pe.layoutUnitTypeChooser + } + return Surface{Gray: 24, Focus: t.InstrumentEditor.wasFocused}.Layout(gtx, func(gtx C) D { + ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Flexed(1, func(gtx C) D { + return editorFunc(gtx, t) + }), + layout.Rigid(pe.layoutUnitFooter(t))) + rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y) + pointer.PassOp{Pass: true}.Add(gtx.Ops) + pointer.Rect(rect).Add(gtx.Ops) + pointer.InputOp{Tag: &pe.tag, + Types: pointer.Press, + }.Add(gtx.Ops) + key.InputOp{Tag: &pe.tag}.Add(gtx.Ops) + return ret + }) + } +} + +func (pe *ParamEditor) layoutUnitSliders(gtx C, t *Tracker) D { + numItems := t.NumParams() + + for len(pe.Parameters) <= numItems { + pe.Parameters = append(pe.Parameters, new(ParameterWidget)) + } + + listItem := func(gtx C, index int) D { + for pe.Parameters[index].Clicked() { + if !pe.focused || t.ParamIndex() != index { + pe.Focus() + t.SetParamIndex(index) + } else { + t.ResetParam() + } + } + param, err := t.Param(index) + if err != nil { + return D{} + } + oldVal := param.Value + paramStyle := t.ParamStyle(t.Theme, ¶m, pe.Parameters[index]) + paramStyle.Focus = pe.focused && t.ParamIndex() == index + dims := paramStyle.Layout(gtx) + if oldVal != param.Value { + pe.Focus() + t.SetParamIndex(index) + t.SetParam(param.Value) + } + return dims + } + + return layout.Stack{}.Layout(gtx, + layout.Stacked(func(gtx C) D { + return pe.list.Layout(gtx, numItems, listItem) + }), + layout.Stacked(func(gtx C) D { + gtx.Constraints.Min = gtx.Constraints.Max + return pe.scrollBar.Layout(gtx, unit.Dp(10), numItems, &pe.list.Position) + })) +} + +func (pe *ParamEditor) layoutUnitFooter(t *Tracker) layout.Widget { + return func(gtx C) D { + for pe.ClearUnitBtn.Clicked() { + t.SetUnitType("") + op.InvalidateOp{}.Add(gtx.Ops) + t.InstrumentEditor.unitDragList.Focus() + } + for pe.DeleteUnitBtn.Clicked() { + t.DeleteUnit(false) + op.InvalidateOp{}.Add(gtx.Ops) + t.InstrumentEditor.unitDragList.Focus() + } + deleteUnitBtnStyle := IconButton(t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, t.CanDeleteUnit()) + text := t.Unit().Type + if text == "" { + text = "Choose unit type" + } else { + text = strings.Title(text) + } + hintText := Label(text, white) + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(deleteUnitBtnStyle.Layout), + layout.Rigid(func(gtx C) D { + var dims D + if t.Unit().Type != "" { + clearUnitBtnStyle := IconButton(t.Theme, pe.ClearUnitBtn, icons.ContentClear, true) + dims = clearUnitBtnStyle.Layout(gtx) + } + return D{Size: image.Pt(gtx.Px(unit.Dp(48)), dims.Size.Y)} + }), + layout.Flexed(1, hintText), + ) + } +} + +func (pe *ParamEditor) layoutUnitTypeChooser(gtx C, t *Tracker) D { + listElem := func(gtx C, i int) D { + for pe.ChooseUnitTypeBtns[i].Clicked() { + t.SetUnitType(tracker.UnitTypeNames[i]) + } + labelStyle := LabelStyle{Text: tracker.UnitTypeNames[i], ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12)} + bg := func(gtx C) D { + gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X, 20)) + var color color.NRGBA + if pe.ChooseUnitTypeBtns[i].Hovered() { + color = unitTypeListHighlightColor + } + paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op()) + return D{Size: gtx.Constraints.Min} + } + leftMargin := layout.Inset{Left: unit.Dp(10)} + return layout.Stack{Alignment: layout.W}.Layout(gtx, + layout.Stacked(bg), + layout.Expanded(func(gtx C) D { + return leftMargin.Layout(gtx, labelStyle.Layout) + }), + layout.Expanded(pe.ChooseUnitTypeBtns[i].Layout)) + } + return layout.Stack{}.Layout(gtx, + layout.Stacked(func(gtx C) D { + return pe.list.Layout(gtx, len(tracker.UnitTypeNames), listElem) + }), + layout.Expanded(func(gtx C) D { + return pe.scrollBar.Layout(gtx, unit.Dp(10), len(tracker.UnitTypeNames), &pe.list.Position) + }), + ) +} diff --git a/tracker/gioui/patterns.go b/tracker/gioui/patterns.go deleted file mode 100644 index fc7f93c..0000000 --- a/tracker/gioui/patterns.go +++ /dev/null @@ -1,125 +0,0 @@ -package gioui - -import ( - "fmt" - "image" - "strings" - - "gioui.org/f32" - "gioui.org/io/pointer" - "gioui.org/layout" - "gioui.org/op" - "gioui.org/op/clip" - "gioui.org/op/paint" - "gioui.org/text" - "gioui.org/unit" - "gioui.org/widget" - "github.com/vsariola/sointu/tracker" -) - -const patternCellHeight = 16 -const patternCellWidth = 16 -const patternRowMarkerWidth = 30 - -var patternPointerTag = false - -func (t *Tracker) layoutPatterns(gtx C) D { - defer op.Save(gtx.Ops).Load() - clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops) - for _, ev := range gtx.Events(&patternPointerTag) { - e, ok := ev.(pointer.Event) - if !ok { - continue - } - if e.Type == pointer.Press { - t.SetEditMode(tracker.EditPatterns) - } - } - rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y) - pointer.Rect(rect).Add(gtx.Ops) - pointer.InputOp{Tag: &patternPointerTag, - Types: pointer.Press, - }.Add(gtx.Ops) - patternRect := tracker.SongRect{ - Corner1: tracker.SongPoint{SongRow: tracker.SongRow{Pattern: t.Cursor().Pattern}, Track: t.Cursor().Track}, - Corner2: tracker.SongPoint{SongRow: tracker.SongRow{Pattern: t.SelectionCorner().Pattern}, Track: t.SelectionCorner().Track}, - } - - // draw the single letter titles for tracks - { - gtx := gtx - curVoice := 0 - stack := op.Save(gtx.Ops) - op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops) - gtx.Constraints = layout.Exact(image.Pt(patternCellWidth, patternCellHeight)) - for _, track := range t.Song().Score.Tracks { - instr, err := t.Song().Patch.InstrumentForVoice(curVoice) - var title string - if err == nil && len(t.Song().Patch[instr].Name) > 0 { - title = string(t.Song().Patch[instr].Name[0]) - } else { - title = "I" - } - LabelStyle{Alignment: layout.N, Text: title, FontSize: unit.Dp(12), Color: mediumEmphasisTextColor}.Layout(gtx) - op.Offset(f32.Pt(patternCellWidth, 0)).Add(gtx.Ops) - curVoice += track.NumVoices - } - stack.Load() - } - op.Offset(f32.Pt(0, patternCellHeight)).Add(gtx.Ops) - gtx.Constraints.Max.Y -= patternCellHeight - gtx.Constraints.Min.Y -= patternCellHeight - element := func(gtx C, j int) D { - if playPos, ok := t.player.Position(); ok && j == playPos.Pattern { - paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op()) - } - paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops) - widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j))) - stack := op.Save(gtx.Ops) - op.Offset(f32.Pt(patternRowMarkerWidth, 0)).Add(gtx.Ops) - for i, track := range t.Song().Score.Tracks { - paint.FillShape(gtx.Ops, patternCellColor, clip.Rect{Min: image.Pt(1, 1), Max: image.Pt(patternCellWidth-1, patternCellHeight-1)}.Op()) - paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops) - if j >= 0 && j < len(track.Order) && track.Order[j] >= 0 { - gtx := gtx - gtx.Constraints.Max.X = patternCellWidth - op.Offset(f32.Pt(0, -2)).Add(gtx.Ops) - widget.Label{Alignment: text.Middle}.Layout(gtx, textShaper, trackerFont, trackerFontSize, patternIndexToString(track.Order[j])) - op.Offset(f32.Pt(0, 2)).Add(gtx.Ops) - } - point := tracker.SongPoint{Track: i, SongRow: tracker.SongRow{Pattern: j}} - if t.EditMode() == tracker.EditPatterns || t.EditMode() == tracker.EditTracks { - if patternRect.Contains(point) { - color := inactiveSelectionColor - if t.EditMode() == tracker.EditPatterns { - color = selectionColor - if point.Pattern == t.Cursor().Pattern && point.Track == t.Cursor().Track { - color = cursorColor - } - } - paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(patternCellWidth, patternCellHeight)}.Op()) - } - } - op.Offset(f32.Pt(patternCellWidth, 0)).Add(gtx.Ops) - } - stack.Load() - return D{Size: image.Pt(gtx.Constraints.Max.X, patternCellHeight)} - } - return layout.Stack{Alignment: layout.NE}.Layout(gtx, - layout.Expanded(func(gtx C) D { - return t.PatternOrderList.Layout(gtx, t.Song().Score.Length, element) - }), - layout.Expanded(func(gtx C) D { - return t.PatternOrderScrollBar.Layout(gtx, unit.Dp(10), t.Song().Score.Length, &t.PatternOrderList.Position) - }), - ) -} - -func patternIndexToString(index int) string { - if index < 0 { - return "" - } else if index < 10 { - return string('0' + byte(index)) - } - return string('A' + byte(index-10)) -} diff --git a/tracker/gioui/rowmarkers.go b/tracker/gioui/rowmarkers.go index 3f2c663..4901d95 100644 --- a/tracker/gioui/rowmarkers.go +++ b/tracker/gioui/rowmarkers.go @@ -11,7 +11,6 @@ import ( "gioui.org/op/clip" "gioui.org/op/paint" "gioui.org/widget" - "github.com/vsariola/sointu/tracker" ) const rowMarkerWidth = 50 @@ -47,7 +46,7 @@ func (t *Tracker) layoutRowMarkers(gtx C) D { paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops) widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", i))) } - if t.EditMode() == tracker.EditTracks && songRow == cursorSongRow { + if t.TrackEditor.Focused() && songRow == cursorSongRow { paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops) } else { paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops) diff --git a/tracker/gioui/run.go b/tracker/gioui/run.go index 1fc30f0..232f5f1 100644 --- a/tracker/gioui/run.go +++ b/tracker/gioui/run.go @@ -44,7 +44,7 @@ func (t *Tracker) Run(w *app.Window) error { ) } case key.Event: - if t.KeyEvent(w, e) { + if t.KeyEvent(e) { w.Invalidate() } case clipboard.Event: diff --git a/tracker/gioui/scrollbar.go b/tracker/gioui/scrollbar.go index 1e35be2..60e14e9 100644 --- a/tracker/gioui/scrollbar.go +++ b/tracker/gioui/scrollbar.go @@ -17,6 +17,7 @@ type ScrollBar struct { dragStart float32 hovering bool dragging bool + tag bool } func (s *ScrollBar) Layout(gtx C, width unit.Value, numItems int, pos *layout.Position) D { @@ -107,11 +108,11 @@ func (s *ScrollBar) Layout(gtx C, width unit.Value, numItems int, pos *layout.Po pointer.PassOp{Pass: true}.Add(gtx.Ops) rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y) pointer.Rect(rect).Add(gtx.Ops) - pointer.InputOp{Tag: s, + pointer.InputOp{Tag: &s.tag, Types: pointer.Enter | pointer.Leave, }.Add(gtx.Ops) - for _, ev := range gtx.Events(s) { + for _, ev := range gtx.Events(&s.tag) { e, ok := ev.(pointer.Event) if !ok { continue diff --git a/tracker/gioui/track.go b/tracker/gioui/trackeditor.go similarity index 56% rename from tracker/gioui/track.go rename to tracker/gioui/trackeditor.go index 12bc7bd..f552a6f 100644 --- a/tracker/gioui/track.go +++ b/tracker/gioui/trackeditor.go @@ -3,9 +3,11 @@ package gioui import ( "fmt" "image" + "strconv" "strings" "gioui.org/f32" + "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/layout" "gioui.org/op" @@ -22,17 +24,172 @@ const trackRowHeight = 16 const trackColWidth = 54 const patmarkWidth = 16 -var trackPointerTag bool -var trackJumpPointerTag bool +type TrackEditor struct { + TrackVoices *NumberInput + NewTrackBtn *widget.Clickable + DeleteTrackBtn *widget.Clickable + AddSemitoneBtn *widget.Clickable + SubtractSemitoneBtn *widget.Clickable + AddOctaveBtn *widget.Clickable + SubtractOctaveBtn *widget.Clickable + NoteOffBtn *widget.Clickable + trackPointerTag bool + trackJumpPointerTag bool + tag bool + focused bool + requestFocus bool +} + +func NewTrackEditor() *TrackEditor { + return &TrackEditor{ + TrackVoices: new(NumberInput), + NewTrackBtn: new(widget.Clickable), + DeleteTrackBtn: new(widget.Clickable), + AddSemitoneBtn: new(widget.Clickable), + SubtractSemitoneBtn: new(widget.Clickable), + AddOctaveBtn: new(widget.Clickable), + SubtractOctaveBtn: new(widget.Clickable), + NoteOffBtn: new(widget.Clickable), + } +} + +func (te *TrackEditor) Focus() { + te.requestFocus = true +} + +func (te *TrackEditor) Focused() bool { + return te.focused +} + +func (te *TrackEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions { + for _, e := range gtx.Events(&te.tag) { + switch e := e.(type) { + case key.FocusEvent: + te.focused = e.Focus + case pointer.Event: + if e.Type == pointer.Press { + key.FocusOp{Tag: &te.tag}.Add(gtx.Ops) + } + case key.Event: + switch e.State { + case key.Press: + switch e.Name { + case key.NameDeleteForward, key.NameDeleteBackward: + t.DeleteSelection() + if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 { + t.SetCursor(t.Cursor().AddRows(t.Step.Value)) + t.SetSelectionCorner(t.Cursor()) + } + case key.NameUpArrow, key.NameDownArrow: + sign := -1 + if e.Name == key.NameDownArrow { + sign = 1 + } + cursor := t.Cursor() + if e.Modifiers.Contain(key.ModShortcut) { + cursor.Row += t.Song().Score.RowsPerPattern * sign + } else { + if t.Step.Value > 0 { + cursor.Row += t.Step.Value * sign + } else { + cursor.Row += sign + } + } + t.SetNoteTracking(false) + t.SetCursor(cursor) + if !e.Modifiers.Contain(key.ModShift) { + t.SetSelectionCorner(t.Cursor()) + } + //scrollToView(t.PatternOrderList, t.Cursor().Pattern, t.Song().Score.Length) + case key.NameLeftArrow: + cursor := t.Cursor() + if !t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) { + cursor.Track-- + t.SetLowNibble(true) + } else { + t.SetLowNibble(false) + } + t.SetCursor(cursor) + if !e.Modifiers.Contain(key.ModShift) { + t.SetSelectionCorner(t.Cursor()) + } + case key.NameRightArrow: + if t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) { + cursor := t.Cursor() + cursor.Track++ + t.SetCursor(cursor) + t.SetLowNibble(false) + } else { + t.SetLowNibble(true) + } + + if !e.Modifiers.Contain(key.ModShift) { + t.SetSelectionCorner(t.Cursor()) + } + case "+": + if e.Modifiers.Contain(key.ModShortcut) { + t.AdjustSelectionPitch(12) + } else { + t.AdjustSelectionPitch(1) + } + case "-": + if e.Modifiers.Contain(key.ModShortcut) { + t.AdjustSelectionPitch(-12) + } else { + t.AdjustSelectionPitch(-1) + } + } + if e.Modifiers.Contain(key.ModShortcut) { + continue + } + step := false + if t.Song().Score.Tracks[t.Cursor().Track].Effect { + if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil { + t.NumberPressed(byte(iv)) + step = true + } + } else { + if e.Name == "A" || e.Name == "1" { + t.SetNote(0) + step = true + } else { + if val, ok := noteMap[e.Name]; ok { + if _, ok := t.KeyPlaying[e.Name]; !ok { + n := tracker.NoteAsValue(t.OctaveNumberInput.Value, val) + t.SetNote(n) + step = true + trk := t.Cursor().Track + start := t.Song().Score.FirstVoiceForTrack(trk) + end := start + t.Song().Score.Tracks[trk].NumVoices + t.KeyPlaying[e.Name] = t.player.Trigger(start, end, n) + } + } + } + } + if step && !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 { + t.SetCursor(t.Cursor().AddRows(t.Step.Value)) + t.SetSelectionCorner(t.Cursor()) + } + + t.JammingPressed(e) + case key.Release: + t.JammingReleased(e) + } + } + } + + if te.requestFocus { + te.requestFocus = false + key.FocusOp{Tag: &te.tag}.Add(gtx.Ops) + } -func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { rowMarkers := layout.Rigid(t.layoutRowMarkers) - for t.NewTrackBtn.Clicked() { + for te.NewTrackBtn.Clicked() { t.AddTrack(true) } - for t.DeleteTrackBtn.Clicked() { + for te.DeleteTrackBtn.Clicked() { t.DeleteTrack(false) } @@ -41,15 +198,15 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { //cbStyle.Color = white //cbStyle.IconColor = t.Theme.Fg - for t.AddSemitoneBtn.Clicked() { + for te.AddSemitoneBtn.Clicked() { t.AdjustSelectionPitch(1) } - for t.SubtractSemitoneBtn.Clicked() { + for te.SubtractSemitoneBtn.Clicked() { t.AdjustSelectionPitch(-1) } - for t.NoteOffBtn.Clicked() { + for te.NoteOffBtn.Clicked() { t.SetNote(0) if !(t.NoteTracking() && t.player.Playing()) && t.Step.Value > 0 { t.SetCursor(t.Cursor().AddRows(t.Step.Value)) @@ -57,22 +214,22 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { } } - for t.AddOctaveBtn.Clicked() { + for te.AddOctaveBtn.Clicked() { t.AdjustSelectionPitch(12) } - for t.SubtractOctaveBtn.Clicked() { + for te.SubtractOctaveBtn.Clicked() { t.AdjustSelectionPitch(-12) } menu := func(gtx C) D { - addSemitoneBtnStyle := LowEmphasisButton(t.Theme, t.AddSemitoneBtn, "+1") - subtractSemitoneBtnStyle := LowEmphasisButton(t.Theme, t.SubtractSemitoneBtn, "-1") - addOctaveBtnStyle := LowEmphasisButton(t.Theme, t.AddOctaveBtn, "+12") - subtractOctaveBtnStyle := LowEmphasisButton(t.Theme, t.SubtractOctaveBtn, "-12") - noteOffBtnStyle := LowEmphasisButton(t.Theme, t.NoteOffBtn, "Note Off") - deleteTrackBtnStyle := IconButton(t.Theme, t.DeleteTrackBtn, icons.ActionDelete, t.CanDeleteTrack()) - newTrackBtnStyle := IconButton(t.Theme, t.NewTrackBtn, icons.ContentAdd, t.CanAddTrack()) + addSemitoneBtnStyle := LowEmphasisButton(t.Theme, te.AddSemitoneBtn, "+1") + subtractSemitoneBtnStyle := LowEmphasisButton(t.Theme, te.SubtractSemitoneBtn, "-1") + addOctaveBtnStyle := LowEmphasisButton(t.Theme, te.AddOctaveBtn, "+12") + subtractOctaveBtnStyle := LowEmphasisButton(t.Theme, te.SubtractOctaveBtn, "-12") + noteOffBtnStyle := LowEmphasisButton(t.Theme, te.NoteOffBtn, "Note Off") + deleteTrackBtnStyle := IconButton(t.Theme, te.DeleteTrackBtn, icons.ActionDelete, t.CanDeleteTrack()) + newTrackBtnStyle := IconButton(t.Theme, te.NewTrackBtn, icons.ContentAdd, t.CanAddTrack()) in := layout.UniformInset(unit.Dp(1)) octave := func(gtx C) D { t.OctaveNumberInput.Value = t.Octave() @@ -84,9 +241,9 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { return dims } n := t.Song().Score.Tracks[t.Cursor().Track].NumVoices - t.TrackVoices.Value = n + te.TrackVoices.Value = n voiceUpDown := func(gtx C) D { - numStyle := NumericUpDown(t.Theme, t.TrackVoices, 1, t.MaxTrackVoices()) + numStyle := NumericUpDown(t.Theme, te.TrackVoices, 1, t.MaxTrackVoices()) gtx.Constraints.Min.Y = gtx.Px(unit.Dp(20)) gtx.Constraints.Min.X = gtx.Px(unit.Dp(70)) return in.Layout(gtx, numStyle.Layout) @@ -109,48 +266,44 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { layout.Rigid(deleteTrackBtnStyle.Layout), layout.Rigid(newTrackBtnStyle.Layout)) t.Song().Score.Tracks[t.Cursor().Track].Effect = t.TrackHexCheckBox.Value // TODO: we should not modify the model, but how should this be done - t.SetTrackVoices(t.TrackVoices.Value) + t.SetTrackVoices(te.TrackVoices.Value) return dims } - for _, ev := range gtx.Events(&trackPointerTag) { - e, ok := ev.(pointer.Event) - if !ok { - continue - } - if e.Type == pointer.Press { - t.SetEditMode(tracker.EditTracks) - } - } rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y) pointer.Rect(rect).Add(gtx.Ops) - pointer.InputOp{Tag: &trackPointerTag, + pointer.InputOp{Tag: &te.tag, Types: pointer.Press, }.Add(gtx.Ops) + key.InputOp{Tag: &te.tag}.Add(gtx.Ops) - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return Surface{Gray: 37, Focus: t.EditMode() == tracker.EditTracks, FitSize: true}.Layout(gtx, menu) - }), - layout.Flexed(1, func(gtx C) D { - return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - rowMarkers, - layout.Flexed(1, t.layoutTracks)) - }), - ) + return Surface{Gray: 24, Focus: te.focused}.Layout(gtx, func(gtx C) D { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx C) D { + return Surface{Gray: 37, Focus: te.focused, FitSize: true}.Layout(gtx, menu) + }), + layout.Flexed(1, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + rowMarkers, + layout.Flexed(1, func(gtx C) D { + return te.layoutTracks(gtx, t) + })) + }), + ) + }) } -func (t *Tracker) layoutTracks(gtx C) D { +func (te *TrackEditor) layoutTracks(gtx C, t *Tracker) D { defer op.Save(gtx.Ops).Load() clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops) cursorSongRow := t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row - for _, ev := range gtx.Events(&trackJumpPointerTag) { + for _, ev := range gtx.Events(&te.trackJumpPointerTag) { e, ok := ev.(pointer.Event) if !ok { continue } if e.Type == pointer.Press { - t.SetEditMode(tracker.EditTracks) + te.Focus() track := int(e.Position.X) / trackColWidth row := int((e.Position.Y-float32(gtx.Constraints.Max.Y-trackRowHeight)/2)/trackRowHeight + float32(cursorSongRow)) cursor := tracker.SongPoint{Track: track, SongRow: tracker.SongRow{Row: row}}.Clamp(t.Song().Score) @@ -161,7 +314,7 @@ func (t *Tracker) layoutTracks(gtx C) D { } rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y) pointer.Rect(rect).Add(gtx.Ops) - pointer.InputOp{Tag: &trackJumpPointerTag, + pointer.InputOp{Tag: &te.trackJumpPointerTag, Types: pointer.Press, }.Add(gtx.Ops) stack := op.Save(gtx.Ops) @@ -192,7 +345,7 @@ func (t *Tracker) layoutTracks(gtx C) D { stack.Load() op.Offset(f32.Pt(0, float32(gtx.Constraints.Max.Y-trackRowHeight)/2)).Add(gtx.Ops) op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorSongRow))).Add(gtx.Ops) - if t.EditMode() == tracker.EditPatterns || t.EditMode() == tracker.EditTracks { + if te.focused || t.OrderEditor.Focused() { x1, y1 := t.Cursor().Track, t.Cursor().Pattern x2, y2 := t.SelectionCorner().Track, t.SelectionCorner().Pattern if x1 > x2 { @@ -209,7 +362,7 @@ func (t *Tracker) layoutTracks(gtx C) D { y2 *= trackRowHeight * t.Song().Score.RowsPerPattern paint.FillShape(gtx.Ops, inactiveSelectionColor, clip.Rect{Min: image.Pt(x1, y1), Max: image.Pt(x2, y2)}.Op()) } - if t.EditMode() == tracker.EditTracks { + if te.focused { x1, y1 := t.Cursor().Track, t.Cursor().Pattern*t.Song().Score.RowsPerPattern+t.Cursor().Row x2, y2 := t.SelectionCorner().Track, t.SelectionCorner().Pattern*t.Song().Score.RowsPerPattern+t.SelectionCorner().Row if x1 > x2 { @@ -268,7 +421,7 @@ func (t *Tracker) layoutTracks(gtx C) D { widget.Label{}.Layout(gtx, textShaper, trackerFont, trackerFontSize, "*") } op.Offset(f32.Pt(patmarkWidth, 0)).Add(gtx.Ops) - if t.EditMode() == tracker.EditTracks && t.Cursor().Row == patRow && t.Cursor().Pattern == pat { + if te.focused && t.Cursor().Row == patRow && t.Cursor().Pattern == pat { paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops) } else { paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops) diff --git a/tracker/gioui/tracker.go b/tracker/gioui/tracker.go index 5692f11..c02e3b1 100644 --- a/tracker/gioui/tracker.go +++ b/tracker/gioui/tracker.go @@ -8,7 +8,6 @@ import ( "gioui.org/app" "gioui.org/font/gofont" "gioui.org/layout" - "gioui.org/text" "gioui.org/widget" "gioui.org/widget/material" "github.com/vsariola/sointu" @@ -23,66 +22,37 @@ const ( ) type Tracker struct { - Theme *material.Theme - MenuBar []widget.Clickable - Menus []Menu - OctaveNumberInput *NumberInput - BPM *NumberInput - RowsPerPattern *NumberInput - RowsPerBeat *NumberInput - Step *NumberInput - InstrumentVoices *NumberInput - TrackVoices *NumberInput - InstrumentNameEditor *widget.Editor - NewTrackBtn *widget.Clickable - DeleteTrackBtn *widget.Clickable - NewInstrumentBtn *widget.Clickable - DeleteInstrumentBtn *widget.Clickable - AddSemitoneBtn *widget.Clickable - SubtractSemitoneBtn *widget.Clickable - AddOctaveBtn *widget.Clickable - SubtractOctaveBtn *widget.Clickable - NoteOffBtn *widget.Clickable - SongLength *NumberInput - PanicBtn *widget.Clickable - CopyInstrumentBtn *widget.Clickable - SaveInstrumentBtn *widget.Clickable - LoadInstrumentBtn *widget.Clickable - ParameterList *layout.List - ParameterScrollBar *ScrollBar - Parameters []*ParameterWidget - UnitDragList *DragList - UnitScrollBar *ScrollBar - DeleteUnitBtn *widget.Clickable - ClearUnitBtn *widget.Clickable - ChooseUnitTypeList *layout.List - ChooseUnitScrollBar *ScrollBar - ChooseUnitTypeBtns []*widget.Clickable - AddUnitBtn *widget.Clickable - InstrumentDragList *DragList - InstrumentScrollBar *ScrollBar - TrackHexCheckBox *widget.Bool - TopHorizontalSplit *Split - BottomHorizontalSplit *Split - VerticalSplit *Split - StackUse []int - KeyPlaying map[string]uint32 - Alert Alert - PatternOrderList *layout.List - PatternOrderScrollBar *ScrollBar - ConfirmInstrDelete *Dialog - ConfirmSongDialog *Dialog - WaveTypeDialog *Dialog - OpenSongDialog *FileDialog - SaveSongDialog *FileDialog - OpenInstrumentDialog *FileDialog - SaveInstrumentDialog *FileDialog - ExportWavDialog *FileDialog - InstrumentCommentEditor *widget.Editor - InstrumentExpandBtn *widget.Clickable - InstrumentExpanded bool - ConfirmSongActionType int - window *app.Window + Theme *material.Theme + MenuBar []widget.Clickable + Menus []Menu + OctaveNumberInput *NumberInput + BPM *NumberInput + RowsPerPattern *NumberInput + RowsPerBeat *NumberInput + Step *NumberInput + InstrumentVoices *NumberInput + SongLength *NumberInput + PanicBtn *widget.Clickable + AddUnitBtn *widget.Clickable + TrackHexCheckBox *widget.Bool + TopHorizontalSplit *Split + BottomHorizontalSplit *Split + VerticalSplit *Split + KeyPlaying map[string]uint32 + Alert Alert + ConfirmSongDialog *Dialog + WaveTypeDialog *Dialog + OpenSongDialog *FileDialog + SaveSongDialog *FileDialog + OpenInstrumentDialog *FileDialog + SaveInstrumentDialog *FileDialog + ExportWavDialog *FileDialog + ConfirmSongActionType int + window *app.Window + ModalDialog layout.Widget + InstrumentEditor *InstrumentEditor + OrderEditor *OrderEditor + TrackEditor *TrackEditor lastVolume tracker.Volume volumeChan chan tracker.Volume @@ -131,62 +101,38 @@ func (t *Tracker) Close() { func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syncChannel chan<- []float32, window *app.Window) *Tracker { t := &Tracker{ - Theme: material.NewTheme(gofont.Collection()), - audioContext: audioContext, - BPM: new(NumberInput), - OctaveNumberInput: &NumberInput{Value: 4}, - SongLength: new(NumberInput), - RowsPerPattern: new(NumberInput), - RowsPerBeat: new(NumberInput), - Step: &NumberInput{Value: 1}, - InstrumentVoices: new(NumberInput), - TrackVoices: new(NumberInput), - InstrumentNameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle}, - NewTrackBtn: new(widget.Clickable), - DeleteTrackBtn: new(widget.Clickable), - NewInstrumentBtn: new(widget.Clickable), - DeleteInstrumentBtn: new(widget.Clickable), - AddSemitoneBtn: new(widget.Clickable), - SubtractSemitoneBtn: new(widget.Clickable), - AddOctaveBtn: new(widget.Clickable), - SubtractOctaveBtn: new(widget.Clickable), - NoteOffBtn: new(widget.Clickable), - AddUnitBtn: new(widget.Clickable), - DeleteUnitBtn: new(widget.Clickable), - ClearUnitBtn: new(widget.Clickable), - PanicBtn: new(widget.Clickable), - CopyInstrumentBtn: new(widget.Clickable), - SaveInstrumentBtn: new(widget.Clickable), - LoadInstrumentBtn: new(widget.Clickable), - TrackHexCheckBox: new(widget.Bool), - Menus: make([]Menu, 2), - MenuBar: make([]widget.Clickable, 2), - UnitDragList: &DragList{List: &layout.List{Axis: layout.Vertical}, HoverItem: -1}, - UnitScrollBar: &ScrollBar{Axis: layout.Vertical}, - refresh: make(chan struct{}, 1), // use non-blocking sends; no need to queue extra ticks if one is queued already - InstrumentDragList: &DragList{List: &layout.List{Axis: layout.Horizontal}, HoverItem: -1}, - InstrumentScrollBar: &ScrollBar{Axis: layout.Horizontal}, - ParameterList: &layout.List{Axis: layout.Vertical}, - ParameterScrollBar: &ScrollBar{Axis: layout.Vertical}, - TopHorizontalSplit: &Split{Ratio: -.6}, - BottomHorizontalSplit: &Split{Ratio: -.6}, - VerticalSplit: &Split{Axis: layout.Vertical}, - ChooseUnitTypeList: &layout.List{Axis: layout.Vertical}, - ChooseUnitScrollBar: &ScrollBar{Axis: layout.Vertical}, - KeyPlaying: make(map[string]uint32), - volumeChan: make(chan tracker.Volume, 1), - playerCloser: make(chan struct{}), - PatternOrderList: &layout.List{Axis: layout.Vertical}, - PatternOrderScrollBar: &ScrollBar{Axis: layout.Vertical}, - ConfirmInstrDelete: new(Dialog), - ConfirmSongDialog: new(Dialog), - WaveTypeDialog: new(Dialog), - OpenSongDialog: NewFileDialog(), - SaveSongDialog: NewFileDialog(), - OpenInstrumentDialog: NewFileDialog(), - SaveInstrumentDialog: NewFileDialog(), - InstrumentCommentEditor: new(widget.Editor), - InstrumentExpandBtn: new(widget.Clickable), + Theme: material.NewTheme(gofont.Collection()), + audioContext: audioContext, + BPM: new(NumberInput), + OctaveNumberInput: &NumberInput{Value: 4}, + SongLength: new(NumberInput), + RowsPerPattern: new(NumberInput), + RowsPerBeat: new(NumberInput), + Step: &NumberInput{Value: 1}, + InstrumentVoices: new(NumberInput), + + PanicBtn: new(widget.Clickable), + TrackHexCheckBox: new(widget.Bool), + Menus: make([]Menu, 2), + MenuBar: make([]widget.Clickable, 2), + refresh: make(chan struct{}, 1), // use non-blocking sends; no need to queue extra ticks if one is queued already + + TopHorizontalSplit: &Split{Ratio: -.6}, + BottomHorizontalSplit: &Split{Ratio: -.6}, + VerticalSplit: &Split{Axis: layout.Vertical}, + + KeyPlaying: make(map[string]uint32), + volumeChan: make(chan tracker.Volume, 1), + playerCloser: make(chan struct{}), + ConfirmSongDialog: new(Dialog), + WaveTypeDialog: new(Dialog), + OpenSongDialog: NewFileDialog(), + SaveSongDialog: NewFileDialog(), + OpenInstrumentDialog: NewFileDialog(), + SaveInstrumentDialog: NewFileDialog(), + InstrumentEditor: NewInstrumentEditor(), + OrderEditor: NewOrderEditor(), + TrackEditor: NewTrackEditor(), ExportWavDialog: NewFileDialog(), errorChannel: make(chan error, 32), @@ -198,10 +144,7 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syn go tracker.VuAnalyzer(0.3, 1e-4, 1, -100, 20, vuBufferObserver, t.volumeChan, t.errorChannel) t.Theme.Palette.Fg = primaryColor t.Theme.Palette.ContrastFg = black - t.SetEditMode(tracker.EditTracks) - for range tracker.UnitTypeNames { - t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable)) - } + t.TrackEditor.Focus() t.SetOctave(4) patchObserver := make(chan sointu.Patch, 16) t.AddPatchObserver(patchObserver) diff --git a/tracker/gioui/uniteditor.go b/tracker/gioui/uniteditor.go deleted file mode 100644 index 126088a..0000000 --- a/tracker/gioui/uniteditor.go +++ /dev/null @@ -1,135 +0,0 @@ -package gioui - -import ( - "image" - "image/color" - "strings" - - "gioui.org/layout" - "gioui.org/op" - "gioui.org/op/clip" - "gioui.org/op/paint" - "gioui.org/unit" - "github.com/vsariola/sointu/tracker" - "golang.org/x/exp/shiny/materialdesign/icons" -) - -func (t *Tracker) layoutUnitEditor(gtx C) D { - editorFunc := t.layoutUnitSliders - if t.Unit().Type == "" { - editorFunc = t.layoutUnitTypeChooser - } - return Surface{Gray: 24, Focus: t.EditMode() == tracker.EditUnits || t.EditMode() == tracker.EditParameters}.Layout(gtx, func(gtx C) D { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Flexed(1, editorFunc), - layout.Rigid(t.layoutUnitFooter())) - }) -} - -func (t *Tracker) layoutUnitSliders(gtx C) D { - numItems := t.NumParams() - - for len(t.Parameters) <= numItems { - t.Parameters = append(t.Parameters, new(ParameterWidget)) - } - - listItem := func(gtx C, index int) D { - for t.Parameters[index].Clicked() { - if t.EditMode() != tracker.EditParameters || t.ParamIndex() != index { - t.SetEditMode(tracker.EditParameters) - t.SetParamIndex(index) - } else { - t.ResetParam() - } - } - param, err := t.Param(index) - if err != nil { - return D{} - } - oldVal := param.Value - paramStyle := t.ParamStyle(t.Theme, ¶m, t.Parameters[index]) - paramStyle.Focus = t.EditMode() == tracker.EditParameters && t.ParamIndex() == index - dims := paramStyle.Layout(gtx) - if oldVal != param.Value { - t.SetEditMode(tracker.EditParameters) - t.SetParamIndex(index) - t.SetParam(param.Value) - } - return dims - } - - return layout.Stack{}.Layout(gtx, - layout.Stacked(func(gtx C) D { - return t.ParameterList.Layout(gtx, numItems, listItem) - }), - layout.Stacked(func(gtx C) D { - gtx.Constraints.Min = gtx.Constraints.Max - return t.ParameterScrollBar.Layout(gtx, unit.Dp(10), numItems, &t.ParameterList.Position) - })) -} - -func (t *Tracker) layoutUnitFooter() layout.Widget { - return func(gtx C) D { - for t.ClearUnitBtn.Clicked() { - t.SetUnitType("") - op.InvalidateOp{}.Add(gtx.Ops) - } - for t.DeleteUnitBtn.Clicked() { - t.DeleteUnit(false) - op.InvalidateOp{}.Add(gtx.Ops) - } - deleteUnitBtnStyle := IconButton(t.Theme, t.DeleteUnitBtn, icons.ActionDelete, t.CanDeleteUnit()) - text := t.Unit().Type - if text == "" { - text = "Choose unit type" - } else { - text = strings.Title(text) - } - hintText := Label(text, white) - return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(deleteUnitBtnStyle.Layout), - layout.Rigid(func(gtx C) D { - var dims D - if t.Unit().Type != "" { - clearUnitBtnStyle := IconButton(t.Theme, t.ClearUnitBtn, icons.ContentClear, true) - dims = clearUnitBtnStyle.Layout(gtx) - } - return D{Size: image.Pt(gtx.Px(unit.Dp(48)), dims.Size.Y)} - }), - layout.Flexed(1, hintText), - ) - } -} - -func (t *Tracker) layoutUnitTypeChooser(gtx C) D { - listElem := func(gtx C, i int) D { - for t.ChooseUnitTypeBtns[i].Clicked() { - t.SetUnitType(tracker.UnitTypeNames[i]) - } - labelStyle := LabelStyle{Text: tracker.UnitTypeNames[i], ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12)} - bg := func(gtx C) D { - gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X, 20)) - var color color.NRGBA - if t.ChooseUnitTypeBtns[i].Hovered() { - color = unitTypeListHighlightColor - } - paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op()) - return D{Size: gtx.Constraints.Min} - } - leftMargin := layout.Inset{Left: unit.Dp(10)} - return layout.Stack{Alignment: layout.W}.Layout(gtx, - layout.Stacked(bg), - layout.Expanded(func(gtx C) D { - return leftMargin.Layout(gtx, labelStyle.Layout) - }), - layout.Expanded(t.ChooseUnitTypeBtns[i].Layout)) - } - return layout.Stack{}.Layout(gtx, - layout.Stacked(func(gtx C) D { - return t.ChooseUnitTypeList.Layout(gtx, len(tracker.UnitTypeNames), listElem) - }), - layout.Expanded(func(gtx C) D { - return t.ChooseUnitScrollBar.Layout(gtx, unit.Dp(10), len(tracker.UnitTypeNames), &t.ChooseUnitTypeList.Position) - }), - ) -} diff --git a/tracker/model.go b/tracker/model.go index a4d3aa0..3885f7d 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -18,7 +18,6 @@ import ( // protected. type Model struct { song sointu.Song - editMode EditMode selectionCorner SongPoint cursor SongPoint lowNibble bool @@ -54,17 +53,8 @@ type Parameter struct { LargeStep int } -type EditMode int - type ParameterType int -const ( - EditPatterns EditMode = iota - EditTracks - EditUnits - EditParameters -) - const ( IntegerParameter ParameterType = iota BoolParameter @@ -697,10 +687,6 @@ func (m *Model) DeletePatternSelection() { m.notifyScoreChange() } -func (m *Model) SetEditMode(value EditMode) { - m.editMode = value -} - func (m *Model) Undo() { if !m.CanUndo() { return @@ -758,10 +744,6 @@ func (m *Model) Song() sointu.Song { return m.song } -func (m *Model) EditMode() EditMode { - return m.editMode -} - func (m *Model) SelectionCorner() SongPoint { return m.selectionCorner }