mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-20 22:14:35 -04:00
refactor(tracker, gioui): get rid of EditMode, use gio focus instead
This commit is contained in:
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
449
tracker/gioui/instrumenteditor.go
Normal file
449
tracker/gioui/instrumenteditor.go
Normal 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)))
|
||||
})
|
||||
}
|
@ -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))
|
||||
})
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
247
tracker/gioui/ordereditor.go
Normal file
247
tracker/gioui/ordereditor.go
Normal 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))
|
||||
}
|
240
tracker/gioui/parameditor.go
Normal file
240
tracker/gioui/parameditor.go
Normal 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, ¶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)
|
||||
}),
|
||||
)
|
||||
}
|
@ -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))
|
||||
}
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
@ -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)
|
||||
|
@ -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)
|
||||
}),
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user