refactor(tracker, gioui): get rid of EditMode, use gio focus instead

This commit is contained in:
vsariola
2021-04-24 22:07:56 +03:00
parent e544e955cb
commit b2b15f825d
16 changed files with 1325 additions and 1086 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, &param, 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)
}),
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, &param, 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)
}),
)
}

View File

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