mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-12 17:14:43 -04:00
feat!: rewrote the GUI and model for better testability
The Model was getting unmaintanable mess. This is an attempt to refactor/rewrite the Model so that data of certain type is exposed in standardized way, offering certain standard manipulations for that data type, and on the GUI side, certain standard widgets to tied to that data. This rewrite closes #72, #106 and #120.
This commit is contained in:
parent
6d3c65e11d
commit
d92426a100
@ -1,118 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
message string
|
||||
alertType AlertType
|
||||
duration time.Duration
|
||||
showMessage string
|
||||
showAlertType AlertType
|
||||
showDuration time.Duration
|
||||
showTime time.Time
|
||||
pos float64
|
||||
lastUpdate time.Time
|
||||
shaper *text.Shaper
|
||||
}
|
||||
|
||||
type AlertType int
|
||||
|
||||
const (
|
||||
None AlertType = iota
|
||||
Notify
|
||||
Warning
|
||||
Error
|
||||
)
|
||||
|
||||
var alertSpeed = 150 * time.Millisecond
|
||||
var alertMargin = layout.UniformInset(unit.Dp(6))
|
||||
var alertInset = layout.UniformInset(unit.Dp(6))
|
||||
|
||||
func (a *Alert) Update(message string, alertType AlertType, duration time.Duration) {
|
||||
if a.alertType < alertType {
|
||||
a.message = message
|
||||
a.alertType = alertType
|
||||
a.duration = duration
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Alert) Layout(gtx C) D {
|
||||
now := time.Now()
|
||||
if a.alertType != None {
|
||||
a.showMessage = a.message
|
||||
a.showAlertType = a.alertType
|
||||
a.showTime = now
|
||||
a.showDuration = a.duration
|
||||
}
|
||||
a.alertType = None
|
||||
var targetPos float64 = 0.0
|
||||
if now.Sub(a.showTime) <= a.showDuration {
|
||||
targetPos = 1.0
|
||||
}
|
||||
delta := float64(now.Sub(a.lastUpdate)) / float64(alertSpeed)
|
||||
if a.pos < targetPos {
|
||||
a.pos += delta
|
||||
if a.pos > targetPos {
|
||||
a.pos = targetPos
|
||||
} else {
|
||||
op.InvalidateOp{At: now.Add(50 * time.Millisecond)}.Add(gtx.Ops)
|
||||
}
|
||||
} else if a.pos > targetPos {
|
||||
a.pos -= delta
|
||||
if a.pos < targetPos {
|
||||
a.pos = targetPos
|
||||
} else {
|
||||
op.InvalidateOp{At: now.Add(50 * time.Millisecond)}.Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
a.lastUpdate = now
|
||||
var color, textColor, shadeColor color.NRGBA
|
||||
switch a.showAlertType {
|
||||
case Warning:
|
||||
color = warningColor
|
||||
textColor = black
|
||||
case Error:
|
||||
color = errorColor
|
||||
textColor = black
|
||||
default:
|
||||
color = popupSurfaceColor
|
||||
textColor = white
|
||||
shadeColor = black
|
||||
}
|
||||
bgWidget := func(gtx C) D {
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
labelStyle := LabelStyle{Text: a.showMessage, Color: textColor, ShadeColor: shadeColor, Font: labelDefaultFont, Alignment: layout.Center, FontSize: unit.Sp(16), Shaper: a.shaper}
|
||||
return alertMargin.Layout(gtx, func(gtx C) D {
|
||||
return layout.S.Layout(gtx, func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
recording := op.Record(gtx.Ops)
|
||||
dims := layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Expanded(bgWidget),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return alertInset.Layout(gtx, labelStyle.Layout)
|
||||
}),
|
||||
)
|
||||
macro := recording.Stop()
|
||||
totalY := dims.Size.Y + gtx.Dp(alertMargin.Bottom)
|
||||
op.Offset(image.Point{0, int((1 - a.pos) * float64(totalY))}).Add((gtx.Ops))
|
||||
macro.Add(gtx.Ops)
|
||||
return dims
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -6,17 +6,43 @@ import (
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type TipClickable struct {
|
||||
Clickable widget.Clickable
|
||||
TipArea component.TipArea
|
||||
type (
|
||||
TipClickable struct {
|
||||
Clickable widget.Clickable
|
||||
TipArea component.TipArea
|
||||
}
|
||||
|
||||
ActionClickable struct {
|
||||
Action tracker.Action
|
||||
TipClickable
|
||||
}
|
||||
|
||||
TipIconButtonStyle struct {
|
||||
TipArea *component.TipArea
|
||||
IconButtonStyle material.IconButtonStyle
|
||||
Tooltip component.Tooltip
|
||||
}
|
||||
|
||||
BoolClickable struct {
|
||||
Clickable widget.Clickable
|
||||
TipArea component.TipArea
|
||||
Bool tracker.Bool
|
||||
}
|
||||
)
|
||||
|
||||
func NewActionClickable(a tracker.Action) *ActionClickable {
|
||||
return &ActionClickable{
|
||||
Action: a,
|
||||
}
|
||||
}
|
||||
|
||||
type TipIconButtonStyle struct {
|
||||
IconButtonStyle material.IconButtonStyle
|
||||
Tooltip component.Tooltip
|
||||
tipArea *component.TipArea
|
||||
func NewBoolClickable(b tracker.Bool) *BoolClickable {
|
||||
return &BoolClickable{
|
||||
Bool: b,
|
||||
}
|
||||
}
|
||||
|
||||
func Tooltip(th *material.Theme, tip string) component.Tooltip {
|
||||
@ -25,24 +51,86 @@ func Tooltip(th *material.Theme, tip string) component.Tooltip {
|
||||
return tooltip
|
||||
}
|
||||
|
||||
func IconButton(th *material.Theme, w *TipClickable, icon []byte, enabled bool, tip string) TipIconButtonStyle {
|
||||
ret := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
||||
ret.Background = transparent
|
||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||
if enabled {
|
||||
ret.Color = primaryColor
|
||||
} else {
|
||||
ret.Color = disabledTextColor
|
||||
func ActionIcon(th *material.Theme, w *ActionClickable, icon []byte, tip string) TipIconButtonStyle {
|
||||
ret := TipIcon(th, &w.TipClickable, icon, tip)
|
||||
for w.Clickable.Clicked() {
|
||||
w.Action.Do()
|
||||
}
|
||||
if !w.Action.Allowed() {
|
||||
ret.IconButtonStyle.Color = disabledTextColor
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func TipIcon(th *material.Theme, w *TipClickable, icon []byte, tip string) TipIconButtonStyle {
|
||||
iconButtonStyle := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
||||
iconButtonStyle.Color = primaryColor
|
||||
iconButtonStyle.Background = transparent
|
||||
iconButtonStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
return TipIconButtonStyle{
|
||||
TipArea: &w.TipArea,
|
||||
IconButtonStyle: iconButtonStyle,
|
||||
Tooltip: Tooltip(th, tip),
|
||||
}
|
||||
}
|
||||
|
||||
func ToggleIcon(th *material.Theme, w *BoolClickable, offIcon, onIcon []byte, offTip, onTip string) TipIconButtonStyle {
|
||||
icon := offIcon
|
||||
tip := offTip
|
||||
if w.Bool.Value() {
|
||||
icon = onIcon
|
||||
tip = onTip
|
||||
}
|
||||
for w.Clickable.Clicked() {
|
||||
w.Bool.Toggle()
|
||||
}
|
||||
ibStyle := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
||||
ibStyle.Background = transparent
|
||||
ibStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
ibStyle.Color = primaryColor
|
||||
if !w.Bool.Enabled() {
|
||||
ibStyle.Color = disabledTextColor
|
||||
}
|
||||
return TipIconButtonStyle{
|
||||
IconButtonStyle: ret,
|
||||
TipArea: &w.TipArea,
|
||||
IconButtonStyle: ibStyle,
|
||||
Tooltip: Tooltip(th, tip),
|
||||
tipArea: &w.TipArea,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TipIconButtonStyle) Layout(gtx C) D {
|
||||
return t.tipArea.Layout(gtx, t.Tooltip, t.IconButtonStyle.Layout)
|
||||
return t.TipArea.Layout(gtx, t.Tooltip, t.IconButtonStyle.Layout)
|
||||
}
|
||||
|
||||
func ActionButton(th *material.Theme, w *ActionClickable, text string) material.ButtonStyle {
|
||||
for w.Clickable.Clicked() {
|
||||
w.Action.Do()
|
||||
}
|
||||
ret := material.Button(th, &w.Clickable, text)
|
||||
ret.Color = th.Palette.Fg
|
||||
if !w.Action.Allowed() {
|
||||
ret.Color = disabledTextColor
|
||||
}
|
||||
ret.Background = transparent
|
||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||
return ret
|
||||
}
|
||||
|
||||
func ToggleButton(th *material.Theme, b *BoolClickable, text string) material.ButtonStyle {
|
||||
for b.Clickable.Clicked() {
|
||||
b.Bool.Toggle()
|
||||
}
|
||||
ret := material.Button(th, &b.Clickable, text)
|
||||
ret.Background = transparent
|
||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||
if b.Bool.Value() {
|
||||
ret.Color = th.Palette.ContrastFg
|
||||
ret.Background = th.Palette.Fg
|
||||
} else {
|
||||
ret.Color = th.Palette.Fg
|
||||
ret.Background = transparent
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func LowEmphasisButton(th *material.Theme, w *widget.Clickable, text string) material.ButtonStyle {
|
||||
|
||||
@ -1,56 +1,82 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type Dialog struct {
|
||||
Visible bool
|
||||
BtnAlt widget.Clickable
|
||||
BtnOk widget.Clickable
|
||||
BtnCancel widget.Clickable
|
||||
BtnAlt *ActionClickable
|
||||
BtnOk *ActionClickable
|
||||
BtnCancel *ActionClickable
|
||||
tag bool
|
||||
}
|
||||
|
||||
type DialogStyle struct {
|
||||
dialog *Dialog
|
||||
Title string
|
||||
Text string
|
||||
Inset layout.Inset
|
||||
ShowAlt bool
|
||||
TextInset layout.Inset
|
||||
AltStyle material.ButtonStyle
|
||||
OkStyle material.ButtonStyle
|
||||
CancelStyle material.ButtonStyle
|
||||
Shaper *text.Shaper
|
||||
}
|
||||
|
||||
func ConfirmDialog(th *material.Theme, dialog *Dialog, text string, shaper *text.Shaper) DialogStyle {
|
||||
func NewDialog(ok, alt, cancel tracker.Action) *Dialog {
|
||||
return &Dialog{
|
||||
BtnOk: NewActionClickable(ok),
|
||||
BtnAlt: NewActionClickable(alt),
|
||||
BtnCancel: NewActionClickable(cancel),
|
||||
}
|
||||
}
|
||||
|
||||
func ConfirmDialog(th *material.Theme, dialog *Dialog, title, text string) DialogStyle {
|
||||
ret := DialogStyle{
|
||||
dialog: dialog,
|
||||
Title: title,
|
||||
Text: text,
|
||||
Inset: layout.Inset{Top: unit.Dp(12), Bottom: unit.Dp(12), Left: unit.Dp(20), Right: unit.Dp(20)},
|
||||
AltStyle: HighEmphasisButton(th, &dialog.BtnAlt, "Alt"),
|
||||
OkStyle: HighEmphasisButton(th, &dialog.BtnOk, "Ok"),
|
||||
CancelStyle: HighEmphasisButton(th, &dialog.BtnCancel, "Cancel"),
|
||||
Shaper: shaper,
|
||||
TextInset: layout.Inset{Top: unit.Dp(12), Bottom: unit.Dp(12)},
|
||||
AltStyle: ActionButton(th, dialog.BtnAlt, "Alt"),
|
||||
OkStyle: ActionButton(th, dialog.BtnOk, "Ok"),
|
||||
CancelStyle: ActionButton(th, dialog.BtnCancel, "Cancel"),
|
||||
Shaper: th.Shaper,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (d *DialogStyle) Layout(gtx C) D {
|
||||
if d.dialog.Visible {
|
||||
paint.Fill(gtx.Ops, dialogBgColor)
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return Popup(&d.dialog.Visible).Layout(gtx, func(gtx C) D {
|
||||
return d.Inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label(d.Text, highEmphasisTextColor, d.Shaper)),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !d.dialog.BtnOk.Clickable.Focused() && !d.dialog.BtnCancel.Clickable.Focused() && !d.dialog.BtnAlt.Clickable.Focused() {
|
||||
d.dialog.BtnCancel.Clickable.Focus()
|
||||
}
|
||||
paint.Fill(gtx.Ops, dialogBgColor)
|
||||
text := func(gtx C) D {
|
||||
return d.TextInset.Layout(gtx, LabelStyle{Text: d.Text, Color: highEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(14), Shaper: d.Shaper}.Layout)
|
||||
}
|
||||
for _, e := range gtx.Events(&d.dialog.tag) {
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
d.command(e)
|
||||
}
|
||||
}
|
||||
visible := true
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return Popup(&visible).Layout(gtx, func(gtx C) D {
|
||||
key.InputOp{Tag: &d.dialog.tag, Keys: "⎋|←|→|Tab"}.Add(gtx.Ops)
|
||||
return d.Inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label(d.Title, highEmphasisTextColor, d.Shaper)),
|
||||
layout.Rigid(text),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, func(gtx C) D {
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(120))
|
||||
if d.ShowAlt {
|
||||
if d.dialog.BtnAlt.Action.Allowed() {
|
||||
return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween}.Layout(gtx,
|
||||
layout.Rigid(d.OkStyle.Layout),
|
||||
layout.Rigid(d.AltStyle.Layout),
|
||||
@ -61,11 +87,43 @@ func (d *DialogStyle) Layout(gtx C) D {
|
||||
layout.Rigid(d.OkStyle.Layout),
|
||||
layout.Rigid(d.CancelStyle.Layout),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
return D{}
|
||||
})
|
||||
}
|
||||
|
||||
func (d *DialogStyle) command(e key.Event) {
|
||||
switch e.Name {
|
||||
case key.NameEscape:
|
||||
d.dialog.BtnCancel.Action.Do()
|
||||
case key.NameLeftArrow:
|
||||
switch {
|
||||
case d.dialog.BtnOk.Clickable.Focused():
|
||||
d.dialog.BtnCancel.Clickable.Focus()
|
||||
case d.dialog.BtnCancel.Clickable.Focused():
|
||||
if d.dialog.BtnAlt.Action.Allowed() {
|
||||
d.dialog.BtnAlt.Clickable.Focus()
|
||||
} else {
|
||||
d.dialog.BtnOk.Clickable.Focus()
|
||||
}
|
||||
case d.dialog.BtnAlt.Clickable.Focused():
|
||||
d.dialog.BtnOk.Clickable.Focus()
|
||||
}
|
||||
case key.NameRightArrow, key.NameTab:
|
||||
switch {
|
||||
case d.dialog.BtnOk.Clickable.Focused():
|
||||
if d.dialog.BtnAlt.Action.Allowed() {
|
||||
d.dialog.BtnAlt.Clickable.Focus()
|
||||
} else {
|
||||
d.dialog.BtnCancel.Clickable.Focus()
|
||||
}
|
||||
case d.dialog.BtnCancel.Clickable.Focused():
|
||||
d.dialog.BtnOk.Clickable.Focus()
|
||||
case d.dialog.BtnAlt.Clickable.Focused():
|
||||
d.dialog.BtnCancel.Clickable.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,48 +4,54 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"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/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type DragList struct {
|
||||
SelectedItem int
|
||||
SelectedItem2 int
|
||||
HoverItem int
|
||||
List *layout.List
|
||||
drag bool
|
||||
dragID pointer.ID
|
||||
tags []bool
|
||||
swapped bool
|
||||
focused bool
|
||||
requestFocus bool
|
||||
mainTag bool
|
||||
TrackerList tracker.List
|
||||
HoverItem int
|
||||
List *layout.List
|
||||
ScrollBar *ScrollBar
|
||||
drag bool
|
||||
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)
|
||||
dragList *DragList
|
||||
HoverColor color.NRGBA
|
||||
SelectedColor color.NRGBA
|
||||
CursorColor color.NRGBA
|
||||
ScrollBarWidth unit.Dp
|
||||
element, bg func(gtx C, i int) D
|
||||
}
|
||||
|
||||
func FilledDragList(th *material.Theme, dragList *DragList, count int, element func(gtx C, i int) D, swap func(i, j int)) FilledDragListStyle {
|
||||
func NewDragList(model tracker.List, axis layout.Axis) *DragList {
|
||||
return &DragList{TrackerList: model, List: &layout.List{Axis: axis}, HoverItem: -1, ScrollBar: &ScrollBar{Axis: axis}}
|
||||
}
|
||||
|
||||
func FilledDragList(th *material.Theme, dragList *DragList, element, bg func(gtx C, i int) D) FilledDragListStyle {
|
||||
return FilledDragListStyle{
|
||||
dragList: dragList,
|
||||
element: element,
|
||||
swap: swap,
|
||||
Count: count,
|
||||
HoverColor: dragListHoverColor,
|
||||
SelectedColor: dragListSelectedColor,
|
||||
CursorColor: cursorColor,
|
||||
dragList: dragList,
|
||||
element: element,
|
||||
bg: bg,
|
||||
HoverColor: dragListHoverColor,
|
||||
SelectedColor: dragListSelectedColor,
|
||||
CursorColor: cursorColor,
|
||||
ScrollBarWidth: unit.Dp(10),
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,14 +63,18 @@ func (d *DragList) Focused() bool {
|
||||
return d.focused
|
||||
}
|
||||
|
||||
func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
func (s FilledDragListStyle) LayoutScrollBar(gtx C) D {
|
||||
return s.dragList.ScrollBar.Layout(gtx, s.ScrollBarWidth, s.dragList.TrackerList.Count(), &s.dragList.List.Position)
|
||||
}
|
||||
|
||||
func (s FilledDragListStyle) Layout(gtx C) D {
|
||||
swap := 0
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
keys := key.Set("↑|↓|Ctrl-↑|Ctrl-↓|Shift-↑|Shift-↓")
|
||||
keys := key.Set("↑|↓|Ctrl-↑|Ctrl-↓|Shift-↑|Shift-↓|⇞|⇟|Ctrl-⇞|Ctrl-⇟|Ctrl-A|Ctrl-C|Ctrl-X|Ctrl-V|⌦|Ctrl-⌫")
|
||||
if s.dragList.List.Axis == layout.Horizontal {
|
||||
keys = key.Set("←|→|Ctrl-←|Ctrl-→|Shift-←|Shift-→")
|
||||
keys = key.Set("←|→|Ctrl-←|Ctrl-→|Shift-←|Shift-→|Home|End|Ctrl-Home|Ctrl-End|Ctrl-A|Ctrl-C|Ctrl-X|Ctrl-V|⌦|Ctrl-⌫")
|
||||
}
|
||||
key.InputOp{Tag: &s.dragList.mainTag, Keys: keys}.Add(gtx.Ops)
|
||||
|
||||
@ -79,65 +89,45 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
if !s.dragList.focused {
|
||||
s.dragList.SelectedItem2 = s.dragList.SelectedItem
|
||||
}
|
||||
|
||||
for _, ke := range gtx.Events(&s.dragList.mainTag) {
|
||||
switch ke := ke.(type) {
|
||||
case key.FocusEvent:
|
||||
s.dragList.focused = ke.Focus
|
||||
if !s.dragList.focused {
|
||||
s.dragList.TrackerList.SetSelected2(s.dragList.TrackerList.Selected())
|
||||
}
|
||||
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
|
||||
if !ke.Modifiers.Contain(key.ModShift) {
|
||||
s.dragList.SelectedItem2 = s.dragList.SelectedItem
|
||||
}
|
||||
}
|
||||
}
|
||||
s.dragList.command(gtx, ke)
|
||||
case clipboard.Event:
|
||||
s.dragList.TrackerList.PasteElements([]byte(ke.Text))
|
||||
}
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
_, isMutable := s.dragList.TrackerList.ListData.(tracker.MutableListData)
|
||||
|
||||
listElem := func(gtx C, index int) D {
|
||||
for len(s.dragList.tags) <= index {
|
||||
s.dragList.tags = append(s.dragList.tags, false)
|
||||
}
|
||||
bg := func(gtx C) D {
|
||||
cursorBg := func(gtx C) D {
|
||||
var color color.NRGBA
|
||||
if s.dragList.SelectedItem == index {
|
||||
if s.dragList.TrackerList.Selected() == index {
|
||||
if s.dragList.focused {
|
||||
color = s.CursorColor
|
||||
} else {
|
||||
color = s.SelectedColor
|
||||
}
|
||||
} else if between(s.dragList.SelectedItem, index, s.dragList.SelectedItem2) {
|
||||
} else if between(s.dragList.TrackerList.Selected(), index, s.dragList.TrackerList.Selected2()) {
|
||||
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.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
for _, ev := range gtx.Events(&s.dragList.tags[index]) {
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
@ -154,9 +144,9 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
if s.dragList.drag {
|
||||
break
|
||||
}
|
||||
s.dragList.SelectedItem = index
|
||||
s.dragList.TrackerList.SetSelected(index)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
s.dragList.SelectedItem2 = index
|
||||
s.dragList.TrackerList.SetSelected2(index)
|
||||
}
|
||||
key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops)
|
||||
}
|
||||
@ -167,7 +157,7 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
Types: pointer.Press | pointer.Enter | pointer.Leave,
|
||||
}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
if index == s.dragList.SelectedItem {
|
||||
if index == s.dragList.TrackerList.Selected() && isMutable {
|
||||
for _, ev := range gtx.Events(&s.dragList.focused) {
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
@ -212,29 +202,31 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
}
|
||||
return layout.Stack{Alignment: layout.W}.Layout(gtx,
|
||||
layout.Expanded(bg),
|
||||
layout.Expanded(inputFg),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return s.element(gtx, index)
|
||||
}),
|
||||
)
|
||||
}
|
||||
dims := s.dragList.List.Layout(gtx, s.Count, listElem)
|
||||
a := intMin(s.dragList.SelectedItem, s.dragList.SelectedItem2)
|
||||
b := intMax(s.dragList.SelectedItem, s.dragList.SelectedItem2)
|
||||
if !s.dragList.swapped && swap != 0 && a+swap >= 0 && b+swap < s.Count {
|
||||
if swap < 0 {
|
||||
for i := a; i <= b; i++ {
|
||||
s.swap(i, i+swap)
|
||||
}
|
||||
} else {
|
||||
for i := b; i >= a; i-- {
|
||||
s.swap(i, i+swap)
|
||||
}
|
||||
macro := op.Record(gtx.Ops)
|
||||
dims := s.element(gtx, index)
|
||||
call := macro.Stop()
|
||||
gtx.Constraints.Min = dims.Size
|
||||
if s.bg != nil {
|
||||
s.bg(gtx, index)
|
||||
}
|
||||
cursorBg(gtx)
|
||||
call.Add(gtx.Ops)
|
||||
if s.dragList.List.Axis == layout.Horizontal {
|
||||
dims.Size.Y = gtx.Constraints.Max.Y
|
||||
} else {
|
||||
dims.Size.X = gtx.Constraints.Max.X
|
||||
}
|
||||
return dims
|
||||
}
|
||||
count := s.dragList.TrackerList.Count()
|
||||
if count < 1 {
|
||||
count = 1 // draw at least one empty element to get the correct size
|
||||
}
|
||||
dims := s.dragList.List.Layout(gtx, count, listElem)
|
||||
if !s.dragList.swapped && swap != 0 {
|
||||
if s.dragList.TrackerList.MoveElements(swap) {
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
s.dragList.SelectedItem += swap
|
||||
s.dragList.SelectedItem2 += swap
|
||||
s.dragList.swapped = true
|
||||
} else {
|
||||
s.dragList.swapped = false
|
||||
@ -242,6 +234,88 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
return dims
|
||||
}
|
||||
|
||||
func (e *DragList) command(gtx layout.Context, k key.Event) {
|
||||
if k.Modifiers.Contain(key.ModShortcut) {
|
||||
switch k.Name {
|
||||
case "V":
|
||||
clipboard.ReadOp{Tag: &e.mainTag}.Add(gtx.Ops)
|
||||
return
|
||||
case "C", "X":
|
||||
data, ok := e.TrackerList.CopyElements()
|
||||
if ok && (k.Name == "C" || e.TrackerList.DeleteElements(false)) {
|
||||
clipboard.WriteOp{Text: string(data)}.Add(gtx.Ops)
|
||||
}
|
||||
return
|
||||
case "A":
|
||||
e.TrackerList.SetSelected(0)
|
||||
e.TrackerList.SetSelected2(e.TrackerList.Count() - 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
delta := 0
|
||||
switch k.Name {
|
||||
case key.NameDeleteBackward:
|
||||
if k.Modifiers.Contain(key.ModShortcut) {
|
||||
e.TrackerList.DeleteElements(true)
|
||||
}
|
||||
return
|
||||
case key.NameDeleteForward:
|
||||
e.TrackerList.DeleteElements(false)
|
||||
return
|
||||
case key.NameLeftArrow:
|
||||
delta = -1
|
||||
case key.NameRightArrow:
|
||||
delta = 1
|
||||
case key.NameHome:
|
||||
delta = -1e6
|
||||
case key.NameEnd:
|
||||
delta = 1e6
|
||||
case key.NameUpArrow:
|
||||
delta = -1
|
||||
case key.NameDownArrow:
|
||||
delta = 1
|
||||
case key.NamePageUp:
|
||||
delta = -1e6
|
||||
case key.NamePageDown:
|
||||
delta = 1e6
|
||||
}
|
||||
if k.Modifiers.Contain(key.ModShortcut) {
|
||||
e.TrackerList.MoveElements(delta)
|
||||
} else {
|
||||
e.TrackerList.SetSelected(e.TrackerList.Selected() + delta)
|
||||
if !k.Modifiers.Contain(key.ModShift) {
|
||||
e.TrackerList.SetSelected2(e.TrackerList.Selected())
|
||||
}
|
||||
}
|
||||
e.EnsureVisible(e.TrackerList.Selected())
|
||||
}
|
||||
|
||||
func (l *DragList) EnsureVisible(item int) {
|
||||
first := l.List.Position.First
|
||||
last := l.List.Position.First + l.List.Position.Count - 1
|
||||
if item < first || (item == first && l.List.Position.Offset > 0) {
|
||||
l.List.ScrollTo(item)
|
||||
}
|
||||
if item > last || (item == last && l.List.Position.OffsetLast < 0) {
|
||||
o := -l.List.Position.OffsetLast + l.List.Position.Offset
|
||||
l.List.ScrollTo(item - l.List.Position.Count + 1)
|
||||
l.List.Position.Offset = o
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DragList) CenterOn(item int) {
|
||||
lenPerChildPx := l.List.Position.Length / l.TrackerList.Count()
|
||||
if lenPerChildPx == 0 {
|
||||
return
|
||||
}
|
||||
listLengthPx := l.List.Position.Count*l.List.Position.Length/l.TrackerList.Count() + l.List.Position.OffsetLast - l.List.Position.Offset
|
||||
lenBeforeItem := (listLengthPx - lenPerChildPx) / 2
|
||||
quot := lenBeforeItem / lenPerChildPx
|
||||
rem := lenBeforeItem % lenPerChildPx
|
||||
l.List.ScrollTo(item - quot - 1)
|
||||
l.List.Position.Offset = lenPerChildPx - rem
|
||||
}
|
||||
|
||||
func between(a, b, c int) bool {
|
||||
return (a <= b && b <= c) || (c <= b && b <= a)
|
||||
}
|
||||
|
||||
@ -1,224 +0,0 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
func (t *Tracker) OpenSongFile(forced bool) {
|
||||
if !forced && t.ChangedSinceSave() {
|
||||
t.ConfirmSongActionType = ConfirmLoad
|
||||
t.ConfirmSongDialog.Visible = true
|
||||
return
|
||||
}
|
||||
reader, err := t.Explorer.ChooseFile(".yml", ".json")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.loadSong(reader)
|
||||
}
|
||||
|
||||
func (t *Tracker) SaveSongFile() bool {
|
||||
if p := t.FilePath(); p != "" {
|
||||
if f, err := os.Create(p); err == nil {
|
||||
return t.saveSong(f)
|
||||
}
|
||||
}
|
||||
t.SaveSongAsFile()
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Tracker) SaveSongAsFile() {
|
||||
p := t.FilePath()
|
||||
if p == "" {
|
||||
p = "song.yml"
|
||||
}
|
||||
writer, err := t.Explorer.CreateFile(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.saveSong(writer)
|
||||
}
|
||||
|
||||
func (t *Tracker) ExportWav(pcm16 bool) {
|
||||
filename := "song.wav"
|
||||
if p := t.FilePath(); p != "" {
|
||||
filename = p[:len(p)-len(filepath.Ext(p))] + ".wav"
|
||||
}
|
||||
writer, err := t.Explorer.CreateFile(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.exportWav(writer, pcm16)
|
||||
}
|
||||
|
||||
func (t *Tracker) LoadInstrument() {
|
||||
reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.loadInstrument(reader)
|
||||
}
|
||||
|
||||
func (t *Tracker) SaveInstrument() {
|
||||
writer, err := t.Explorer.CreateFile(t.Instrument().Name + ".yml")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.saveInstrument(writer)
|
||||
}
|
||||
|
||||
func (t *Tracker) loadSong(r io.ReadCloser) {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var song sointu.Song
|
||||
if errJSON := json.Unmarshal(b, &song); errJSON != nil {
|
||||
if errYaml := yaml.Unmarshal(b, &song); errYaml != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error, time.Second*3)
|
||||
}
|
||||
}
|
||||
if song.Score.Length <= 0 || len(song.Score.Tracks) == 0 || len(song.Patch) == 0 {
|
||||
t.Alert.Update("The song file is malformed", Error, time.Second*3)
|
||||
return
|
||||
}
|
||||
t.SetSong(song)
|
||||
path := ""
|
||||
if f, ok := r.(*os.File); ok {
|
||||
path = f.Name()
|
||||
}
|
||||
t.SetFilePath(path)
|
||||
t.ClearUndoHistory()
|
||||
t.SetChangedSinceSave(false)
|
||||
}
|
||||
|
||||
func (t *Tracker) saveSong(w io.WriteCloser) bool {
|
||||
path := ""
|
||||
if f, ok := w.(*os.File); ok {
|
||||
path = f.Name()
|
||||
}
|
||||
var extension = filepath.Ext(path)
|
||||
var contents []byte
|
||||
var err error
|
||||
if extension == ".json" {
|
||||
contents, err = json.Marshal(t.Song())
|
||||
} else {
|
||||
contents, err = yaml.Marshal(t.Song())
|
||||
}
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error marshaling a song file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
if _, err := w.Write(contents); err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error writing to file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error closing file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
t.SetFilePath(path)
|
||||
t.SetChangedSinceSave(false)
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Tracker) exportWav(w io.WriteCloser, pcm16 bool) {
|
||||
data, err := sointu.Play(t.synther, t.Song()) // render the song to calculate its length
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error rendering the song during export: %v", err), Error, time.Second*3)
|
||||
return
|
||||
}
|
||||
buffer, err := data.Wav(pcm16)
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error converting to .wav: %v", err), Error, time.Second*3)
|
||||
return
|
||||
}
|
||||
w.Write(buffer)
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func (t *Tracker) saveInstrument(w io.WriteCloser) bool {
|
||||
path := ""
|
||||
if f, ok := w.(*os.File); ok {
|
||||
path = f.Name()
|
||||
}
|
||||
var extension = filepath.Ext(path)
|
||||
var contents []byte
|
||||
var err error
|
||||
if extension == ".json" {
|
||||
contents, err = json.Marshal(t.Instrument())
|
||||
} else {
|
||||
contents, err = yaml.Marshal(t.Instrument())
|
||||
}
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error marshaling a ínstrument file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
w.Write(contents)
|
||||
w.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Tracker) loadInstrument(r io.ReadCloser) bool {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var instrument sointu.Instrument
|
||||
var errJSON, errYaml, err4ki, err4kp error
|
||||
var patch sointu.Patch
|
||||
errJSON = json.Unmarshal(b, &instrument)
|
||||
if errJSON == nil {
|
||||
goto success
|
||||
}
|
||||
errYaml = yaml.Unmarshal(b, &instrument)
|
||||
if errYaml == nil {
|
||||
goto success
|
||||
}
|
||||
patch, err4kp = sointu.Read4klangPatch(bytes.NewReader(b))
|
||||
if err4kp == nil {
|
||||
song := t.Song()
|
||||
song.Score = t.Song().Score.Copy()
|
||||
song.Patch = patch
|
||||
t.SetSong(song)
|
||||
return true
|
||||
}
|
||||
instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b))
|
||||
if err4ki == nil {
|
||||
goto success
|
||||
}
|
||||
t.Alert.Update(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v / %v / %v", errYaml, errJSON, err4ki, err4kp), Error, time.Second*3)
|
||||
return false
|
||||
success:
|
||||
if f, ok := r.(*os.File); ok {
|
||||
filename := f.Name()
|
||||
// the 4klang instrument names are junk, replace them with the filename without extension
|
||||
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
|
||||
}
|
||||
if len(instrument.Units) == 0 {
|
||||
t.Alert.Update("The instrument file is malformed", Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
t.SetInstrument(instrument)
|
||||
if t.Instrument().Comment != "" {
|
||||
t.InstrumentEditor.ExpandComment()
|
||||
}
|
||||
return true
|
||||
}
|
||||
461
tracker/gioui/instrument_editor.go
Normal file
461
tracker/gioui/instrument_editor.go
Normal file
@ -0,0 +1,461 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type InstrumentEditor struct {
|
||||
newInstrumentBtn *ActionClickable
|
||||
enlargeBtn *BoolClickable
|
||||
deleteInstrumentBtn *ActionClickable
|
||||
copyInstrumentBtn *TipClickable
|
||||
saveInstrumentBtn *TipClickable
|
||||
loadInstrumentBtn *TipClickable
|
||||
addUnitBtn *ActionClickable
|
||||
presetMenuBtn *TipClickable
|
||||
commentExpandBtn *BoolClickable
|
||||
commentEditor *widget.Editor
|
||||
commentString tracker.String
|
||||
nameEditor *widget.Editor
|
||||
nameString tracker.String
|
||||
searchEditor *widget.Editor
|
||||
instrumentDragList *DragList
|
||||
unitDragList *DragList
|
||||
presetDragList *DragList
|
||||
unitEditor *UnitEditor
|
||||
tag bool
|
||||
wasFocused bool
|
||||
presetMenuItems []MenuItem
|
||||
presetMenu Menu
|
||||
}
|
||||
|
||||
func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
|
||||
ret := &InstrumentEditor{
|
||||
newInstrumentBtn: NewActionClickable(model.AddInstrument()),
|
||||
enlargeBtn: NewBoolClickable(model.InstrEnlarged().Bool()),
|
||||
deleteInstrumentBtn: NewActionClickable(model.DeleteInstrument()),
|
||||
copyInstrumentBtn: new(TipClickable),
|
||||
saveInstrumentBtn: new(TipClickable),
|
||||
loadInstrumentBtn: new(TipClickable),
|
||||
addUnitBtn: NewActionClickable(model.AddUnit(false)),
|
||||
commentExpandBtn: NewBoolClickable(model.CommentExpanded().Bool()),
|
||||
presetMenuBtn: new(TipClickable),
|
||||
commentEditor: new(widget.Editor),
|
||||
nameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle},
|
||||
searchEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Start},
|
||||
commentString: model.InstrumentComment().String(),
|
||||
nameString: model.InstrumentName().String(),
|
||||
instrumentDragList: NewDragList(model.Instruments().List(), layout.Horizontal),
|
||||
unitDragList: NewDragList(model.Units().List(), layout.Vertical),
|
||||
unitEditor: NewUnitEditor(model),
|
||||
presetMenuItems: []MenuItem{},
|
||||
}
|
||||
model.IterateInstrumentPresets(func(index int, name string) bool {
|
||||
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: name, IconBytes: icons.ImageAudiotrack, Doer: model.LoadPreset(index)})
|
||||
return true
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Focus() {
|
||||
ie.unitDragList.Focus()
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Focused() bool {
|
||||
return ie.unitDragList.focused
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) ChildFocused() bool {
|
||||
return ie.unitEditor.sliderList.Focused() || ie.instrumentDragList.Focused() || ie.commentEditor.Focused() || ie.nameEditor.Focused() || ie.searchEditor.Focused() ||
|
||||
ie.addUnitBtn.Clickable.Focused() || ie.commentExpandBtn.Clickable.Focused() || ie.presetMenuBtn.Clickable.Focused() || ie.deleteInstrumentBtn.Clickable.Focused() || ie.copyInstrumentBtn.Clickable.Focused()
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
|
||||
ie.wasFocused = ie.Focused() || ie.ChildFocused()
|
||||
fullscreenBtnStyle := ToggleIcon(t.Theme, ie.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, "Enlarge (Ctrl+E)", "Shrink (Ctrl+E)")
|
||||
|
||||
octave := func(gtx C) D {
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
numStyle := NumericUpDown(t.Theme, t.OctaveNumberInput, "Octave down (<) or up (>)")
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
return dims
|
||||
}
|
||||
|
||||
newBtnStyle := ActionIcon(t.Theme, ie.newInstrumentBtn, icons.ContentAdd, "Add\ninstrument\n(Ctrl+I)")
|
||||
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 ie.layoutInstrumentList(gtx, t)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
inset := layout.UniformInset(unit.Dp(6))
|
||||
return inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("OCT:", white, t.Theme.Shaper)),
|
||||
layout.Rigid(octave),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, fullscreenBtnStyle.Layout)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, newBtnStyle.Layout)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return ie.layoutInstrumentHeader(gtx, t)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return ie.layoutUnitList(gtx, t)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return ie.unitEditor.Layout(gtx, t)
|
||||
}),
|
||||
)
|
||||
}))
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
|
||||
header := func(gtx C) D {
|
||||
commentExpandBtnStyle := ToggleIcon(t.Theme, ie.commentExpandBtn, icons.NavigationExpandMore, icons.NavigationExpandLess, "Expand comment", "Collapse comment")
|
||||
presetMenuBtnStyle := TipIcon(t.Theme, ie.presetMenuBtn, icons.NavigationMenu, "Load preset")
|
||||
copyInstrumentBtnStyle := TipIcon(t.Theme, ie.copyInstrumentBtn, icons.ContentContentCopy, "Copy instrument")
|
||||
saveInstrumentBtnStyle := TipIcon(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, "Save instrument")
|
||||
loadInstrumentBtnStyle := TipIcon(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument")
|
||||
deleteInstrumentBtnStyle := ActionIcon(t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, "Delete\ninstrument")
|
||||
|
||||
m := PopupMenu(&ie.presetMenu, t.Theme.Shaper)
|
||||
|
||||
for ie.copyInstrumentBtn.Clickable.Clicked() {
|
||||
if contents, ok := t.Instruments().List().CopyElements(); ok {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alerts().Add("Instrument copied to clipboard", tracker.Info)
|
||||
}
|
||||
}
|
||||
|
||||
for ie.saveInstrumentBtn.Clickable.Clicked() {
|
||||
writer, err := t.Explorer.CreateFile(t.InstrumentName().Value() + ".yml")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
t.SaveInstrument(writer)
|
||||
}
|
||||
|
||||
for ie.loadInstrumentBtn.Clickable.Clicked() {
|
||||
reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
t.LoadInstrument(reader)
|
||||
}
|
||||
|
||||
header := func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label("Voices: ", white, t.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
numStyle := NumericUpDown(t.Theme, t.InstrumentVoices, "Number of voices for this instrument")
|
||||
dims := numStyle.Layout(gtx)
|
||||
return dims
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(commentExpandBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
dims := presetMenuBtnStyle.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(500))
|
||||
gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180))
|
||||
m.Layout(gtx, ie.presetMenuItems...)
|
||||
return dims
|
||||
}),
|
||||
layout.Rigid(saveInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(loadInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(copyInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(deleteInstrumentBtnStyle.Layout))
|
||||
}
|
||||
|
||||
for ie.presetMenuBtn.Clickable.Clicked() {
|
||||
ie.presetMenu.Visible = true
|
||||
}
|
||||
|
||||
if ie.commentExpandBtn.Bool.Value() || ie.commentEditor.Focused() { // we draw once the widget after it manages to lose focus
|
||||
if ie.commentEditor.Text() != ie.commentString.Value() {
|
||||
ie.commentEditor.SetText(ie.commentString.Value())
|
||||
}
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(header),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &ie.unitDragList, Keys: globalKeys + "|⎋"}.Add(gtx.Ops)
|
||||
for _, event := range gtx.Events(&ie.unitDragList) {
|
||||
if e, ok := event.(key.Event); ok && e.State == key.Press && e.Name == key.NameEscape {
|
||||
ie.instrumentDragList.Focus()
|
||||
}
|
||||
}
|
||||
editorStyle := material.Editor(t.Theme, ie.commentEditor, "Comment")
|
||||
editorStyle.Color = highEmphasisTextColor
|
||||
return layout.UniformInset(unit.Dp(6)).Layout(gtx, editorStyle.Layout)
|
||||
}),
|
||||
)
|
||||
ie.commentString.Set(ie.commentEditor.Text())
|
||||
return ret
|
||||
}
|
||||
return header(gtx)
|
||||
}
|
||||
|
||||
return Surface{Gray: 37, Focus: ie.wasFocused}.Layout(gtx, header)
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D {
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(36))
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(30))
|
||||
grabhandle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(10), Alignment: layout.Center, Shaper: t.Theme.Shaper}
|
||||
if i == ie.instrumentDragList.TrackerList.Selected() {
|
||||
grabhandle.Text = ":::"
|
||||
}
|
||||
label := func(gtx C) D {
|
||||
name, level, ok := (*tracker.Instruments)(t.Model).Item(i)
|
||||
if !ok {
|
||||
labelStyle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
return layout.Center.Layout(gtx, labelStyle.Layout)
|
||||
}
|
||||
k := byte(255 - level*127)
|
||||
color := color.NRGBA{R: 255, G: k, B: 255, A: 255}
|
||||
if i == ie.instrumentDragList.TrackerList.Selected() {
|
||||
for _, ev := range ie.nameEditor.Events() {
|
||||
_, ok := ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
ie.instrumentDragList.Focus()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if n := 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.Sp(12)
|
||||
editor.Font = labelDefaultFont
|
||||
dims := layout.Center.Layout(gtx, func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &ie.nameEditor, Keys: globalKeys}.Add(gtx.Ops)
|
||||
return editor.Layout(gtx)
|
||||
})
|
||||
ie.nameString.Set(ie.nameEditor.Text())
|
||||
return dims
|
||||
}
|
||||
if name == "" {
|
||||
name = "Instr"
|
||||
}
|
||||
labelStyle := LabelStyle{Text: name, ShadeColor: black, Color: color, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
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, element, nil)
|
||||
instrumentList.SelectedColor = color
|
||||
instrumentList.HoverColor = instrumentHoverColor
|
||||
instrumentList.ScrollBarWidth = unit.Dp(6)
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: ie.instrumentDragList, Keys: "↓|⏎|⌤"}.Add(gtx.Ops)
|
||||
|
||||
for _, event := range gtx.Events(ie.instrumentDragList) {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameDownArrow:
|
||||
ie.unitDragList.Focus()
|
||||
case key.NameReturn, key.NameEnter:
|
||||
ie.nameEditor.Focus()
|
||||
l := len(ie.nameEditor.Text())
|
||||
ie.nameEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dims := instrumentList.Layout(gtx)
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
instrumentList.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}
|
||||
func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
|
||||
// TODO: how to ie.unitDragList.Focus()
|
||||
addUnitBtnStyle := ActionIcon(t.Theme, ie.addUnitBtn, icons.ContentAdd, "Add unit (Enter)")
|
||||
addUnitBtnStyle.IconButtonStyle.Color = t.Theme.ContrastFg
|
||||
addUnitBtnStyle.IconButtonStyle.Background = t.Theme.Fg
|
||||
addUnitBtnStyle.IconButtonStyle.Inset = layout.UniformInset(unit.Dp(4))
|
||||
|
||||
index := 0
|
||||
var units [256]tracker.UnitListItem
|
||||
(*tracker.Units)(t.Model).Iterate(func(item tracker.UnitListItem) (ok bool) {
|
||||
units[index] = item
|
||||
index++
|
||||
return index <= 256
|
||||
})
|
||||
count := intMin(ie.unitDragList.TrackerList.Count(), 256)
|
||||
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(unit.Dp(120)), gtx.Dp(unit.Dp(20))))
|
||||
if i < 0 || i >= count {
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
}
|
||||
u := units[i]
|
||||
var color color.NRGBA = white
|
||||
|
||||
var stackText string
|
||||
stackText = strconv.FormatInt(int64(u.StackAfter), 10)
|
||||
if u.StackNeed > u.StackBefore {
|
||||
color = errorColor
|
||||
(*tracker.Alerts)(t.Model).AddNamed("UnitNeedsInputs", fmt.Sprintf("%v needs at least %v input signals, got %v", u.Type, u.StackNeed, u.StackBefore), tracker.Error)
|
||||
} else if i == count-1 && u.StackAfter != 0 {
|
||||
color = warningColor
|
||||
(*tracker.Alerts)(t.Model).AddNamed("InstrumentLeavesSignals", fmt.Sprintf("Instrument leaves %v signal(s) on the stack", u.StackAfter), tracker.Warning)
|
||||
}
|
||||
|
||||
stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
rightMargin := layout.Inset{Right: unit.Dp(10)}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
if i == ie.unitDragList.TrackerList.Selected() {
|
||||
for _, ev := range ie.searchEditor.Events() {
|
||||
_, ok := ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
txt := ""
|
||||
ie.unitDragList.Focus()
|
||||
if text := ie.searchEditor.Text(); text != "" {
|
||||
for _, n := range sointu.UnitNames {
|
||||
if strings.HasPrefix(n, ie.searchEditor.Text()) {
|
||||
txt = n
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Units().SetSelectedType(txt)
|
||||
continue
|
||||
}
|
||||
}
|
||||
editor := material.Editor(t.Theme, ie.searchEditor, "---")
|
||||
editor.Color = color
|
||||
editor.HintColor = instrumentNameHintColor
|
||||
editor.TextSize = unit.Sp(12)
|
||||
editor.Font = labelDefaultFont
|
||||
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &ie.searchEditor, Keys: globalKeys}.Add(gtx.Ops)
|
||||
str := tracker.String{StringData: (*tracker.UnitSearch)(t.Model)}
|
||||
if ie.searchEditor.Text() != str.Value() {
|
||||
ie.searchEditor.SetText(str.Value())
|
||||
}
|
||||
ret := editor.Layout(gtx)
|
||||
str.Set(ie.searchEditor.Text())
|
||||
return ret
|
||||
} else {
|
||||
unitNameLabel := LabelStyle{Text: u.Type, ShadeColor: black, Color: color, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
if unitNameLabel.Text == "" {
|
||||
unitNameLabel.Text = "---"
|
||||
}
|
||||
return unitNameLabel.Layout(gtx)
|
||||
}
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return rightMargin.Layout(gtx, stackLabel.Layout)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
unitList := FilledDragList(t.Theme, ie.unitDragList, element, nil)
|
||||
return Surface{Gray: 30, Focus: ie.wasFocused}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Stack{Alignment: layout.SE}.Layout(gtx,
|
||||
layout.Expanded(func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: ie.unitDragList, Keys: "→|⏎|Ctrl-⏎|⌫|⎋"}.Add(gtx.Ops)
|
||||
for _, event := range gtx.Events(ie.unitDragList) {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameEscape:
|
||||
ie.instrumentDragList.Focus()
|
||||
case key.NameRightArrow:
|
||||
ie.unitEditor.sliderList.Focus()
|
||||
case key.NameDeleteBackward:
|
||||
t.Units().SetSelectedType("")
|
||||
ie.searchEditor.Focus()
|
||||
l := len(ie.searchEditor.Text())
|
||||
ie.searchEditor.SetCaret(l, l)
|
||||
case key.NameReturn:
|
||||
t.Model.AddUnit(e.Modifiers.Contain(key.ModCtrl)).Do()
|
||||
ie.searchEditor.SetText("")
|
||||
ie.searchEditor.Focus()
|
||||
l := len(ie.searchEditor.Text())
|
||||
ie.searchEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(unit.Dp(120)), gtx.Constraints.Max.Y))
|
||||
dims := unitList.Layout(gtx)
|
||||
unitList.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)}
|
||||
return margin.Layout(gtx, addUnitBtnStyle.Layout)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func clamp(i, min, max int) int {
|
||||
if i < min {
|
||||
return min
|
||||
}
|
||||
if i > max {
|
||||
return max
|
||||
}
|
||||
return i
|
||||
}
|
||||
@ -1,578 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/eventx"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type InstrumentEditor struct {
|
||||
newInstrumentBtn *TipClickable
|
||||
enlargeBtn *TipClickable
|
||||
deleteInstrumentBtn *TipClickable
|
||||
copyInstrumentBtn *TipClickable
|
||||
saveInstrumentBtn *TipClickable
|
||||
loadInstrumentBtn *TipClickable
|
||||
addUnitBtn *TipClickable
|
||||
commentExpandBtn *TipClickable
|
||||
presetMenuBtn *TipClickable
|
||||
commentEditor *widget.Editor
|
||||
nameEditor *widget.Editor
|
||||
unitTypeEditor *widget.Editor
|
||||
instrumentDragList *DragList
|
||||
instrumentScrollBar *ScrollBar
|
||||
unitDragList *DragList
|
||||
unitScrollBar *ScrollBar
|
||||
confirmInstrDelete *Dialog
|
||||
paramEditor *ParamEditor
|
||||
stackUse []int
|
||||
tag bool
|
||||
wasFocused bool
|
||||
commentExpanded bool
|
||||
voiceLevels [vm.MAX_VOICES]float32
|
||||
presetMenuItems []MenuItem
|
||||
presetMenu Menu
|
||||
}
|
||||
|
||||
func NewInstrumentEditor() *InstrumentEditor {
|
||||
ret := &InstrumentEditor{
|
||||
newInstrumentBtn: new(TipClickable),
|
||||
enlargeBtn: new(TipClickable),
|
||||
deleteInstrumentBtn: new(TipClickable),
|
||||
copyInstrumentBtn: new(TipClickable),
|
||||
saveInstrumentBtn: new(TipClickable),
|
||||
loadInstrumentBtn: new(TipClickable),
|
||||
addUnitBtn: new(TipClickable),
|
||||
commentExpandBtn: new(TipClickable),
|
||||
presetMenuBtn: new(TipClickable),
|
||||
commentEditor: new(widget.Editor),
|
||||
nameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle},
|
||||
unitTypeEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Start},
|
||||
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(),
|
||||
presetMenuItems: []MenuItem{},
|
||||
}
|
||||
for _, instr := range tracker.InstrumentPresets {
|
||||
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: instr.Name, IconBytes: icons.ImageAudiotrack})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
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() || ie.unitTypeEditor.Focused() ||
|
||||
ie.addUnitBtn.Clickable.Focused() || ie.commentExpandBtn.Clickable.Focused() || ie.presetMenuBtn.Clickable.Focused() || ie.deleteInstrumentBtn.Clickable.Focused() || ie.copyInstrumentBtn.Clickable.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)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &ie.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
|
||||
enlargeTip := "Enlarge"
|
||||
icon := icons.NavigationFullscreen
|
||||
if t.InstrEnlarged() {
|
||||
icon = icons.NavigationFullscreenExit
|
||||
enlargeTip = "Shrink"
|
||||
}
|
||||
|
||||
fullscreenBtnStyle := IconButton(t.Theme, ie.enlargeBtn, icon, true, enlargeTip)
|
||||
for ie.enlargeBtn.Clickable.Clicked() {
|
||||
t.SetInstrEnlarged(!t.InstrEnlarged())
|
||||
}
|
||||
for ie.newInstrumentBtn.Clickable.Clicked() {
|
||||
t.AddInstrument(true)
|
||||
}
|
||||
octave := func(gtx C) D {
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
t.OctaveNumberInput.Value = t.Octave()
|
||||
numStyle := NumericUpDown(t.Theme, t.OctaveNumberInput, 0, 9, "Octave down (<) or up (>)")
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetOctave(t.OctaveNumberInput.Value)
|
||||
return dims
|
||||
}
|
||||
newBtnStyle := IconButton(t.Theme, ie.newInstrumentBtn, icons.ContentAdd, t.CanAddInstrument(), "Add\ninstrument")
|
||||
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 {
|
||||
inset := layout.UniformInset(unit.Dp(6))
|
||||
return inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("OCT:", white, t.TextShaper)),
|
||||
layout.Rigid(octave),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, fullscreenBtnStyle.Layout)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, newBtnStyle.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
|
||||
commentTip := "Collapse comment"
|
||||
if !ie.commentExpanded {
|
||||
collapseIcon = icons.NavigationExpandMore
|
||||
commentTip = "Expand comment"
|
||||
}
|
||||
|
||||
commentExpandBtnStyle := IconButton(t.Theme, ie.commentExpandBtn, collapseIcon, true, commentTip)
|
||||
presetMenuBtnStyle := IconButton(t.Theme, ie.presetMenuBtn, icons.NavigationMenu, true, "Load preset")
|
||||
copyInstrumentBtnStyle := IconButton(t.Theme, ie.copyInstrumentBtn, icons.ContentContentCopy, true, "Copy instrument")
|
||||
saveInstrumentBtnStyle := IconButton(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, true, "Save instrument")
|
||||
loadInstrumentBtnStyle := IconButton(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, true, "Load instrument")
|
||||
deleteInstrumentBtnStyle := IconButton(t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, t.CanDeleteInstrument(), "Delete\ninstrument")
|
||||
|
||||
m := t.PopupMenu(&ie.presetMenu)
|
||||
|
||||
for item, clicked := ie.presetMenu.Clicked(); clicked; item, clicked = ie.presetMenu.Clicked() {
|
||||
t.SetInstrument(tracker.InstrumentPresets[item])
|
||||
}
|
||||
|
||||
header := func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label("Voices: ", white, t.TextShaper)),
|
||||
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, "Number of voices for this instrument")
|
||||
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(func(gtx C) D {
|
||||
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
dims := presetMenuBtnStyle.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(500))
|
||||
gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180))
|
||||
m.Layout(gtx, ie.presetMenuItems...)
|
||||
return dims
|
||||
}),
|
||||
layout.Rigid(saveInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(loadInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(copyInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(deleteInstrumentBtnStyle.Layout))
|
||||
}
|
||||
|
||||
for ie.presetMenuBtn.Clickable.Clicked() {
|
||||
ie.presetMenu.Visible = true
|
||||
}
|
||||
|
||||
for ie.commentExpandBtn.Clickable.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.Clickable.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.Clickable.Clicked() {
|
||||
if t.CanDeleteInstrument() {
|
||||
dialogStyle := ConfirmDialog(t.Theme, ie.confirmInstrDelete, "Are you sure you want to delete this instrument?", t.TextShaper)
|
||||
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.Clickable.Clicked() {
|
||||
t.SaveInstrument()
|
||||
}
|
||||
|
||||
for ie.loadInstrumentBtn.Clickable.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.Dp(unit.Dp(36))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(30))
|
||||
grabhandle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(10), Alignment: layout.Center, Shaper: t.TextShaper}
|
||||
if i == t.InstrIndex() {
|
||||
grabhandle.Text = ":::"
|
||||
}
|
||||
label := func(gtx C) D {
|
||||
c := float32(0.0)
|
||||
voice := t.Song().Patch.FirstVoiceForInstrument(i)
|
||||
loopMax := t.Song().Patch[i].NumVoices
|
||||
if loopMax > vm.MAX_VOICES {
|
||||
loopMax = vm.MAX_VOICES
|
||||
}
|
||||
for j := 0; j < loopMax; j++ {
|
||||
vc := ie.voiceLevels[voice]
|
||||
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.Sp(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), Shaper: t.TextShaper}
|
||||
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.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: ie.instrumentDragList, Keys: "↓|⏎|⌤"}.Add(gtx.Ops)
|
||||
|
||||
for _, event := range gtx.Events(ie.instrumentDragList) {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameDownArrow:
|
||||
ie.unitDragList.Focus()
|
||||
case key.NameReturn, key.NameEnter:
|
||||
ie.nameEditor.Focus()
|
||||
l := len(ie.nameEditor.Text())
|
||||
ie.nameEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dims := instrumentList.Layout(gtx)
|
||||
|
||||
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.Clickable.Clicked() {
|
||||
t.AddUnit(true)
|
||||
ie.unitDragList.Focus()
|
||||
}
|
||||
addUnitBtnStyle := IconButton(t.Theme, ie.addUnitBtn, icons.ContentAdd, true, "Add unit (Ctrl+Enter)")
|
||||
addUnitBtnStyle.IconButtonStyle.Color = t.Theme.ContrastFg
|
||||
addUnitBtnStyle.IconButtonStyle.Background = t.Theme.Fg
|
||||
addUnitBtnStyle.IconButtonStyle.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.Dp(unit.Dp(120)), gtx.Dp(unit.Dp(20))))
|
||||
u := units[i]
|
||||
var color color.NRGBA = white
|
||||
|
||||
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 {
|
||||
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 {
|
||||
color = warningColor
|
||||
t.Alert.Update(fmt.Sprintf("Instrument leaves %v signal(s) on the stack", ie.stackUse[i]), Warning, 0)
|
||||
}
|
||||
}
|
||||
|
||||
var unitName layout.Widget
|
||||
if i == t.UnitIndex() {
|
||||
for _, ev := range ie.unitTypeEditor.Events() {
|
||||
_, ok := ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
ie.unitDragList.Focus()
|
||||
if text := ie.unitTypeEditor.Text(); text != "" {
|
||||
for _, n := range sointu.UnitNames {
|
||||
if strings.HasPrefix(n, ie.unitTypeEditor.Text()) {
|
||||
t.SetUnitType(n)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.SetUnitType("")
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !ie.unitTypeEditor.Focused() && !ie.paramEditor.Focused() && ie.unitTypeEditor.Text() != t.Unit().Type {
|
||||
ie.unitTypeEditor.SetText(t.Unit().Type)
|
||||
}
|
||||
editor := material.Editor(t.Theme, ie.unitTypeEditor, "---")
|
||||
editor.Color = color
|
||||
editor.HintColor = instrumentNameHintColor
|
||||
editor.TextSize = unit.Sp(12)
|
||||
editor.Font = labelDefaultFont
|
||||
unitName = editor.Layout
|
||||
} else {
|
||||
unitNameLabel := LabelStyle{Text: u.Type, ShadeColor: black, Color: color, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.TextShaper}
|
||||
if unitNameLabel.Text == "" {
|
||||
unitNameLabel.Text = "---"
|
||||
}
|
||||
unitName = unitNameLabel.Layout
|
||||
}
|
||||
|
||||
stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.TextShaper}
|
||||
rightMargin := layout.Inset{Right: unit.Dp(10)}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Flexed(1, unitName),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return rightMargin.Layout(gtx, stackLabel.Layout)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
unitList := FilledDragList(t.Theme, ie.unitDragList, len(units), element, t.SwapUnits)
|
||||
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 {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: ie.unitDragList, Keys: "→|⏎|⌫|⌦|⎋|Ctrl-⏎|Ctrl-C|Ctrl-X"}.Add(gtx.Ops)
|
||||
for _, event := range gtx.Events(ie.unitDragList) {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameEscape:
|
||||
ie.instrumentDragList.Focus()
|
||||
case key.NameRightArrow:
|
||||
ie.paramEditor.Focus()
|
||||
case key.NameDeleteBackward:
|
||||
t.SetUnitType("")
|
||||
ie.unitTypeEditor.Focus()
|
||||
l := len(ie.unitTypeEditor.Text())
|
||||
ie.unitTypeEditor.SetCaret(l, l)
|
||||
case key.NameDeleteForward:
|
||||
t.DeleteUnits(true, ie.unitDragList.SelectedItem, ie.unitDragList.SelectedItem2)
|
||||
ie.unitDragList.SelectedItem2 = t.UnitIndex()
|
||||
case "X":
|
||||
units := t.DeleteUnits(true, ie.unitDragList.SelectedItem, ie.unitDragList.SelectedItem2)
|
||||
ie.unitDragList.SelectedItem2 = t.UnitIndex()
|
||||
contents, err := yaml.Marshal(units)
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Unit(s) cut to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
case "C":
|
||||
a := clamp(ie.unitDragList.SelectedItem, 0, len(t.Instrument().Units)-1)
|
||||
b := clamp(ie.unitDragList.SelectedItem2, 0, len(t.Instrument().Units)-1)
|
||||
if a > b {
|
||||
a, b = b, a
|
||||
}
|
||||
units := t.Instrument().Units[a : b+1]
|
||||
contents, err := yaml.Marshal(units)
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Unit(s) copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
case key.NameReturn:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.AddUnit(true)
|
||||
ie.unitDragList.SelectedItem2 = ie.unitDragList.SelectedItem
|
||||
ie.unitTypeEditor.SetText("")
|
||||
}
|
||||
ie.unitTypeEditor.Focus()
|
||||
l := len(ie.unitTypeEditor.Text())
|
||||
ie.unitTypeEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ie.unitDragList.SelectedItem = t.UnitIndex()
|
||||
dims := unitList.Layout(gtx)
|
||||
if t.UnitIndex() != ie.unitDragList.SelectedItem {
|
||||
t.SetUnitIndex(ie.unitDragList.SelectedItem)
|
||||
ie.unitTypeEditor.SetText(t.Unit().Type)
|
||||
}
|
||||
return dims
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)}
|
||||
return margin.Layout(gtx, addUnitBtnStyle.Layout)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return ie.unitScrollBar.Layout(gtx, unit.Dp(10), len(t.Instrument().Units), &ie.unitDragList.List.Position)
|
||||
}))
|
||||
}),
|
||||
layout.Rigid(ie.paramEditor.Bind(t)))
|
||||
})
|
||||
}
|
||||
|
||||
func clamp(i, min, max int) int {
|
||||
if i < min {
|
||||
return min
|
||||
}
|
||||
if i > max {
|
||||
return max
|
||||
}
|
||||
return i
|
||||
}
|
||||
@ -1,15 +1,15 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/op"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// globalKeys is a list of keys that are handled globally by the app.
|
||||
// All Editors should capture these keys to prevent them flowing to the global handler.
|
||||
var globalKeys = key.Set("Space|\\|<|>|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|1|2|3|4|5|6|7|8|9|0|,|.")
|
||||
|
||||
var noteMap = map[string]int{
|
||||
"Z": -12,
|
||||
"S": -11,
|
||||
@ -49,15 +49,6 @@ var noteMap = map[string]int{
|
||||
func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) {
|
||||
if e.State == key.Press {
|
||||
switch e.Name {
|
||||
case "C":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
contents, err := yaml.Marshal(t.Song())
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(o)
|
||||
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
return
|
||||
}
|
||||
case "V":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
clipboard.ReadOp{Tag: t}.Add(o)
|
||||
@ -65,97 +56,118 @@ func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) {
|
||||
}
|
||||
case "Z":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Undo()
|
||||
t.Model.Undo().Do()
|
||||
return
|
||||
}
|
||||
case "Y":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Redo()
|
||||
t.Model.Redo().Do()
|
||||
return
|
||||
}
|
||||
case "N":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.NewSong(false)
|
||||
t.NewSong().Do()
|
||||
return
|
||||
}
|
||||
case "S":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.SaveSongFile()
|
||||
t.SaveSong().Do()
|
||||
return
|
||||
}
|
||||
case "O":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.OpenSongFile(false)
|
||||
t.OpenSong().Do()
|
||||
return
|
||||
}
|
||||
case "I":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.DeleteInstrument().Do()
|
||||
} else {
|
||||
t.AddInstrument().Do()
|
||||
}
|
||||
return
|
||||
}
|
||||
case "T":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.DeleteTrack().Do()
|
||||
} else {
|
||||
t.AddTrack().Do()
|
||||
}
|
||||
return
|
||||
}
|
||||
case "E":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.InstrEnlarged().Bool().Toggle()
|
||||
return
|
||||
}
|
||||
case "W":
|
||||
if e.Modifiers.Contain(key.ModShortcut) && canQuit {
|
||||
t.Quit().Do()
|
||||
return
|
||||
}
|
||||
case "F1":
|
||||
t.OrderEditor.Focus()
|
||||
t.OrderEditor.scrollTable.Focus()
|
||||
return
|
||||
case "F2":
|
||||
t.TrackEditor.Focus()
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
return
|
||||
case "F3":
|
||||
t.InstrumentEditor.Focus()
|
||||
return
|
||||
case "F4":
|
||||
t.TrackEditor.Focus()
|
||||
return
|
||||
case "F5":
|
||||
t.SetNoteTracking(true)
|
||||
startRow := t.Cursor().ScoreRow
|
||||
t.PlayFromPosition(startRow)
|
||||
t.SongPanel.RewindBtn.Action.Do()
|
||||
t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl))
|
||||
return
|
||||
case "F6":
|
||||
t.SetNoteTracking(false)
|
||||
startRow := t.Cursor().ScoreRow
|
||||
t.PlayFromPosition(startRow)
|
||||
case "F6", "Space":
|
||||
t.SongPanel.PlayingBtn.Bool.Toggle()
|
||||
t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl))
|
||||
return
|
||||
case "F7":
|
||||
t.SongPanel.RecordBtn.Bool.Toggle()
|
||||
return
|
||||
case "F8":
|
||||
t.SetPlaying(false)
|
||||
t.SongPanel.NoteTracking.Bool.Toggle()
|
||||
return
|
||||
case "F12":
|
||||
t.Panic().Bool().Toggle()
|
||||
return
|
||||
case "Space":
|
||||
if !t.Playing() && !t.InstrEnlarged() {
|
||||
t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut))
|
||||
startRow := t.Cursor().ScoreRow
|
||||
t.PlayFromPosition(startRow)
|
||||
} else {
|
||||
t.SetPlaying(false)
|
||||
}
|
||||
case `\`, `<`, `>`:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetOctave(t.Octave() + 1)
|
||||
t.OctaveNumberInput.Int.Add(1)
|
||||
} else {
|
||||
t.SetOctave(t.Octave() - 1)
|
||||
t.OctaveNumberInput.Int.Add(-1)
|
||||
}
|
||||
case key.NameTab:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
switch {
|
||||
case t.OrderEditor.Focused():
|
||||
t.InstrumentEditor.paramEditor.Focus()
|
||||
case t.TrackEditor.Focused():
|
||||
t.OrderEditor.Focus()
|
||||
case t.OrderEditor.scrollTable.Focused():
|
||||
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
||||
case t.TrackEditor.scrollTable.Focused():
|
||||
t.OrderEditor.scrollTable.Focus()
|
||||
case t.InstrumentEditor.Focused():
|
||||
if t.InstrEnlarged() {
|
||||
t.InstrumentEditor.paramEditor.Focus()
|
||||
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
||||
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
||||
} else {
|
||||
t.TrackEditor.Focus()
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
}
|
||||
default:
|
||||
t.InstrumentEditor.Focus()
|
||||
}
|
||||
} else {
|
||||
switch {
|
||||
case t.OrderEditor.Focused():
|
||||
t.TrackEditor.Focus()
|
||||
case t.TrackEditor.Focused():
|
||||
case t.OrderEditor.scrollTable.Focused():
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
case t.TrackEditor.scrollTable.Focused():
|
||||
t.InstrumentEditor.Focus()
|
||||
case t.InstrumentEditor.Focused():
|
||||
t.InstrumentEditor.paramEditor.Focus()
|
||||
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
||||
default:
|
||||
if t.InstrEnlarged() {
|
||||
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
||||
t.InstrumentEditor.Focus()
|
||||
} else {
|
||||
t.OrderEditor.Focus()
|
||||
t.OrderEditor.scrollTable.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,28 +178,12 @@ func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) {
|
||||
}
|
||||
}
|
||||
|
||||
// NumberPressed handles incoming presses while in either of the hex number columns
|
||||
func (t *Tracker) NumberPressed(iv byte) {
|
||||
val := t.Note()
|
||||
if val == 1 {
|
||||
val = 0
|
||||
}
|
||||
if t.LowNibble() {
|
||||
val = (val & 0xF0) | (iv & 0xF)
|
||||
} else {
|
||||
val = ((iv & 0xF) << 4) | (val & 0xF)
|
||||
}
|
||||
t.SetNote(val)
|
||||
}
|
||||
|
||||
func (t *Tracker) JammingPressed(e key.Event) byte {
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
n := noteAsValue(t.OctaveNumberInput.Value, val)
|
||||
instr := t.InstrIndex()
|
||||
noteID := tracker.NoteIDInstr(instr, n)
|
||||
t.NoteOn(noteID)
|
||||
t.KeyPlaying[e.Name] = noteID
|
||||
n := noteAsValue(t.OctaveNumberInput.Int.Value(), val)
|
||||
instr := t.InstrumentEditor.instrumentDragList.TrackerList.Selected()
|
||||
t.KeyPlaying[e.Name] = t.InstrNoteOn(instr, n)
|
||||
return n
|
||||
}
|
||||
}
|
||||
@ -196,7 +192,7 @@ func (t *Tracker) JammingPressed(e key.Event) byte {
|
||||
|
||||
func (t *Tracker) JammingReleased(e key.Event) bool {
|
||||
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
t.NoteOff(noteID)
|
||||
noteID.NoteOff()
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -24,28 +24,25 @@ type LabelStyle struct {
|
||||
}
|
||||
|
||||
func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Stack{Alignment: l.Alignment}.Layout(gtx,
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
paint.ColorOp{Color: l.ShadeColor}.Add(gtx.Ops)
|
||||
op.Offset(image.Pt(2, 2)).Add(gtx.Ops)
|
||||
dims := widget.Label{
|
||||
Alignment: text.Start,
|
||||
MaxLines: 1,
|
||||
}.Layout(gtx, l.Shaper, l.Font, l.FontSize, l.Text, op.CallOp{})
|
||||
return layout.Dimensions{
|
||||
Size: dims.Size.Add(image.Pt(2, 2)),
|
||||
Baseline: dims.Baseline,
|
||||
}
|
||||
}),
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
|
||||
return widget.Label{
|
||||
Alignment: text.Start,
|
||||
MaxLines: 1,
|
||||
}.Layout(gtx, l.Shaper, l.Font, l.FontSize, l.Text, op.CallOp{})
|
||||
}),
|
||||
)
|
||||
return l.Alignment.Layout(gtx, func(gtx C) D {
|
||||
gtx.Constraints.Min = image.Point{}
|
||||
paint.ColorOp{Color: l.ShadeColor}.Add(gtx.Ops)
|
||||
offs := op.Offset(image.Pt(2, 2)).Push(gtx.Ops)
|
||||
widget.Label{
|
||||
Alignment: text.Start,
|
||||
MaxLines: 1,
|
||||
}.Layout(gtx, l.Shaper, l.Font, l.FontSize, l.Text, op.CallOp{})
|
||||
offs.Pop()
|
||||
paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
|
||||
dims := widget.Label{
|
||||
Alignment: text.Start,
|
||||
MaxLines: 1,
|
||||
}.Layout(gtx, l.Shaper, l.Font, l.FontSize, l.Text, op.CallOp{})
|
||||
return layout.Dimensions{
|
||||
Size: dims.Size,
|
||||
Baseline: dims.Baseline,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Label(str string, color color.NRGBA, shaper *text.Shaper) layout.Widget {
|
||||
|
||||
@ -1,121 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
|
||||
type C = layout.Context
|
||||
type D = layout.Dimensions
|
||||
|
||||
func (t *Tracker) Layout(gtx layout.Context, w *app.Window) {
|
||||
// this is the top level input handler for the whole app
|
||||
// it handles all the global key events and clipboard events
|
||||
// we need to tell gio that we handle tabs too; otherwise
|
||||
// it will steal them for focus switching
|
||||
key.InputOp{Tag: t, Keys: "Tab|Shift-Tab"}.Add(gtx.Ops)
|
||||
for _, ev := range gtx.Events(t) {
|
||||
switch e := ev.(type) {
|
||||
case key.Event:
|
||||
t.KeyEvent(e, gtx.Ops)
|
||||
case clipboard.Event:
|
||||
t.UnmarshalContent([]byte(e.Text))
|
||||
}
|
||||
}
|
||||
|
||||
paint.FillShape(gtx.Ops, backgroundColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
||||
if t.InstrEnlarged() {
|
||||
t.layoutTop(gtx)
|
||||
} else {
|
||||
t.VerticalSplit.Layout(gtx,
|
||||
t.layoutTop,
|
||||
t.layoutBottom)
|
||||
}
|
||||
t.Alert.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.", t.TextShaper)
|
||||
dstyle.ShowAlt = true
|
||||
dstyle.OkStyle.Text = "Save"
|
||||
dstyle.AltStyle.Text = "Don't save"
|
||||
dstyle.Layout(gtx)
|
||||
for t.ConfirmSongDialog.BtnOk.Clicked() {
|
||||
if t.SaveSongFile() {
|
||||
t.confirmedSongAction()
|
||||
}
|
||||
t.ConfirmSongDialog.Visible = false
|
||||
}
|
||||
for t.ConfirmSongDialog.BtnAlt.Clicked() {
|
||||
t.confirmedSongAction()
|
||||
t.ConfirmSongDialog.Visible = false
|
||||
}
|
||||
for t.ConfirmSongDialog.BtnCancel.Clicked() {
|
||||
t.ConfirmSongDialog.Visible = false
|
||||
}
|
||||
dstyle = ConfirmDialog(t.Theme, t.WaveTypeDialog, "Export .wav in int16 or float32 sample format?", t.TextShaper)
|
||||
dstyle.ShowAlt = true
|
||||
dstyle.OkStyle.Text = "Int16"
|
||||
dstyle.AltStyle.Text = "Float32"
|
||||
dstyle.Layout(gtx)
|
||||
for t.WaveTypeDialog.BtnOk.Clicked() {
|
||||
t.ExportWav(true)
|
||||
t.WaveTypeDialog.Visible = false
|
||||
}
|
||||
for t.WaveTypeDialog.BtnAlt.Clicked() {
|
||||
t.ExportWav(false)
|
||||
t.WaveTypeDialog.Visible = false
|
||||
}
|
||||
for t.WaveTypeDialog.BtnCancel.Clicked() {
|
||||
t.WaveTypeDialog.Visible = false
|
||||
}
|
||||
if t.ModalDialog != nil {
|
||||
t.ModalDialog(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) confirmedSongAction() {
|
||||
switch t.ConfirmSongActionType {
|
||||
case ConfirmLoad:
|
||||
t.OpenSongFile(true)
|
||||
case ConfirmNew:
|
||||
t.NewSong(true)
|
||||
case ConfirmQuit:
|
||||
t.Quit(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) NewSong(forced bool) {
|
||||
if !forced && t.ChangedSinceSave() {
|
||||
t.ConfirmSongActionType = ConfirmNew
|
||||
t.ConfirmSongDialog.Visible = true
|
||||
return
|
||||
}
|
||||
t.ResetSong()
|
||||
t.SetFilePath("")
|
||||
t.ClearUndoHistory()
|
||||
t.SetChangedSinceSave(false)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions {
|
||||
return t.BottomHorizontalSplit.Layout(gtx,
|
||||
func(gtx C) D {
|
||||
return t.OrderEditor.Layout(gtx, t)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return t.TrackEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions {
|
||||
return t.TopHorizontalSplit.Layout(gtx,
|
||||
t.layoutSongPanel,
|
||||
func(gtx C) D {
|
||||
return t.InstrumentEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -12,6 +12,8 @@ import (
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type Menu struct {
|
||||
@ -40,7 +42,7 @@ type MenuItem struct {
|
||||
IconBytes []byte
|
||||
Text string
|
||||
ShortcutText string
|
||||
Disabled bool
|
||||
Doer tracker.Action
|
||||
}
|
||||
|
||||
func (m *Menu) Clicked() (int, bool) {
|
||||
@ -57,7 +59,7 @@ func (m *Menu) Clicked() (int, bool) {
|
||||
|
||||
func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
contents := func(gtx C) D {
|
||||
for i := range items {
|
||||
for i, item := range items {
|
||||
// make sure we have a tag for every item
|
||||
for len(m.Menu.tags) <= i {
|
||||
m.Menu.tags = append(m.Menu.tags, false)
|
||||
@ -70,7 +72,7 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
}
|
||||
switch e.Type {
|
||||
case pointer.Press:
|
||||
m.Menu.clicks = append(m.Menu.clicks, i)
|
||||
item.Doer.Do()
|
||||
m.Menu.Visible = false
|
||||
case pointer.Enter:
|
||||
m.Menu.hover = i + 1
|
||||
@ -89,17 +91,17 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
var macro op.MacroOp
|
||||
item := &items[i]
|
||||
if i == m.Menu.hover-1 && !item.Disabled {
|
||||
if i == m.Menu.hover-1 && item.Doer.Allowed() {
|
||||
macro = op.Record(gtx.Ops)
|
||||
}
|
||||
icon := widgetForIcon(item.IconBytes)
|
||||
iconColor := m.IconColor
|
||||
if item.Disabled {
|
||||
if !item.Doer.Allowed() {
|
||||
iconColor = mediumEmphasisTextColor
|
||||
}
|
||||
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
|
||||
textLabel := LabelStyle{Text: item.Text, FontSize: m.FontSize, Color: m.TextColor, Shaper: m.Shaper}
|
||||
if item.Disabled {
|
||||
if !item.Doer.Allowed() {
|
||||
textLabel.Color = mediumEmphasisTextColor
|
||||
}
|
||||
shortcutLabel := LabelStyle{Text: item.ShortcutText, FontSize: m.FontSize, Color: m.ShortCutColor, Shaper: m.Shaper}
|
||||
@ -118,14 +120,14 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
return shortcutInset.Layout(gtx, shortcutLabel.Layout)
|
||||
}),
|
||||
)
|
||||
if i == m.Menu.hover-1 && !item.Disabled {
|
||||
if i == m.Menu.hover-1 && item.Doer.Allowed() {
|
||||
recording := macro.Stop()
|
||||
paint.FillShape(gtx.Ops, m.HoverColor, clip.Rect{
|
||||
Max: image.Pt(dims.Size.X, dims.Size.Y),
|
||||
}.Op())
|
||||
recording.Add(gtx.Ops)
|
||||
}
|
||||
if !item.Disabled {
|
||||
if item.Doer.Allowed() {
|
||||
rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &m.Menu.tags[i],
|
||||
@ -148,7 +150,7 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
return popup.Layout(gtx, contents)
|
||||
}
|
||||
|
||||
func (t *Tracker) PopupMenu(menu *Menu) MenuStyle {
|
||||
func PopupMenu(menu *Menu, shaper *text.Shaper) MenuStyle {
|
||||
return MenuStyle{
|
||||
Menu: menu,
|
||||
IconColor: white,
|
||||
@ -157,6 +159,26 @@ func (t *Tracker) PopupMenu(menu *Menu) MenuStyle {
|
||||
FontSize: unit.Sp(16),
|
||||
IconSize: unit.Dp(16),
|
||||
HoverColor: menuHoverColor,
|
||||
Shaper: t.TextShaper,
|
||||
Shaper: shaper,
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *Tracker) layoutMenu(title string, clickable *widget.Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget {
|
||||
for clickable.Clicked() {
|
||||
menu.Visible = true
|
||||
}
|
||||
m := PopupMenu(menu, tr.Theme.Shaper)
|
||||
return func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
titleBtn := material.Button(tr.Theme, clickable, title)
|
||||
titleBtn.Color = white
|
||||
titleBtn.Background = transparent
|
||||
titleBtn.CornerRadius = unit.Dp(0)
|
||||
dims := titleBtn.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.X = gtx.Dp(width)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(1000))
|
||||
m.Layout(gtx, items...)
|
||||
return dims
|
||||
}
|
||||
}
|
||||
|
||||
338
tracker/gioui/note_editor.go
Normal file
338
tracker/gioui/note_editor.go
Normal file
@ -0,0 +1,338 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/key"
|
||||
"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"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
const trackRowHeight = unit.Dp(16)
|
||||
const trackColWidth = unit.Dp(54)
|
||||
const trackColTitleHeight = unit.Dp(16)
|
||||
const trackPatMarkWidth = unit.Dp(25)
|
||||
const trackRowMarkWidth = unit.Dp(25)
|
||||
|
||||
var noteStr [256]string
|
||||
var hexStr [256]string
|
||||
|
||||
func init() {
|
||||
// initialize these strings once, so we don't have to do it every time we draw the note editor
|
||||
hexStr[0] = "--"
|
||||
hexStr[1] = ".."
|
||||
noteStr[0] = "---"
|
||||
noteStr[1] = "..."
|
||||
for i := 2; i < 256; i++ {
|
||||
hexStr[i] = fmt.Sprintf("%02x", i)
|
||||
oNote := mod(i-baseNote, 12)
|
||||
octave := (i - oNote - baseNote) / 12
|
||||
switch {
|
||||
case octave < 0:
|
||||
noteStr[i] = fmt.Sprintf("%s%s", notes[oNote], string(byte('Z'+1+octave)))
|
||||
case octave >= 10:
|
||||
noteStr[i] = fmt.Sprintf("%s%s", notes[oNote], string(byte('A'+octave-10)))
|
||||
default:
|
||||
noteStr[i] = fmt.Sprintf("%s%d", notes[oNote], octave)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type NoteEditor struct {
|
||||
TrackVoices *NumberInput
|
||||
NewTrackBtn *ActionClickable
|
||||
DeleteTrackBtn *ActionClickable
|
||||
AddSemitoneBtn *ActionClickable
|
||||
SubtractSemitoneBtn *ActionClickable
|
||||
AddOctaveBtn *ActionClickable
|
||||
SubtractOctaveBtn *ActionClickable
|
||||
NoteOffBtn *ActionClickable
|
||||
EffectBtn *BoolClickable
|
||||
|
||||
scrollTable *ScrollTable
|
||||
tag struct{}
|
||||
}
|
||||
|
||||
func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
||||
return &NoteEditor{
|
||||
TrackVoices: NewNumberInput(model.TrackVoices().Int()),
|
||||
NewTrackBtn: NewActionClickable(model.AddTrack()),
|
||||
DeleteTrackBtn: NewActionClickable(model.DeleteTrack()),
|
||||
AddSemitoneBtn: NewActionClickable(model.AddSemitone()),
|
||||
SubtractSemitoneBtn: NewActionClickable(model.SubtractSemitone()),
|
||||
AddOctaveBtn: NewActionClickable(model.AddOctave()),
|
||||
SubtractOctaveBtn: NewActionClickable(model.SubtractOctave()),
|
||||
NoteOffBtn: NewActionClickable(model.EditNoteOff()),
|
||||
EffectBtn: NewBoolClickable(model.Effect().Bool()),
|
||||
scrollTable: NewScrollTable(
|
||||
model.Notes().Table(),
|
||||
model.Tracks().List(),
|
||||
model.NoteRows().List(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions {
|
||||
for _, e := range gtx.Events(&te.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.Event:
|
||||
if e.State == key.Release {
|
||||
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
noteID.NoteOff()
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
te.command(gtx, t, e)
|
||||
}
|
||||
}
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &te.tag, Keys: "Ctrl-⌫|Ctrl-⌦|⏎|Ctrl-⏎|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|0|1|2|3|4|5|6|7|8|9|,|."}.Add(gtx.Ops)
|
||||
|
||||
return Surface{Gray: 24, Focus: te.scrollTable.Focused()}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return te.layoutButtons(gtx, t)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return te.layoutTracks(gtx, t)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
||||
return Surface{Gray: 37, Focus: te.scrollTable.Focused() || te.scrollTable.ChildFocused(), FitSize: true}.Layout(gtx, func(gtx C) D {
|
||||
addSemitoneBtnStyle := ActionButton(t.Theme, te.AddSemitoneBtn, "+1")
|
||||
subtractSemitoneBtnStyle := ActionButton(t.Theme, te.SubtractSemitoneBtn, "-1")
|
||||
addOctaveBtnStyle := ActionButton(t.Theme, te.AddOctaveBtn, "+12")
|
||||
subtractOctaveBtnStyle := ActionButton(t.Theme, te.SubtractOctaveBtn, "-12")
|
||||
noteOffBtnStyle := ActionButton(t.Theme, te.NoteOffBtn, "Note Off")
|
||||
deleteTrackBtnStyle := ActionIcon(t.Theme, te.DeleteTrackBtn, icons.ActionDelete, "Delete track\n(Ctrl+Shift+T)")
|
||||
newTrackBtnStyle := ActionIcon(t.Theme, te.NewTrackBtn, icons.ContentAdd, "Add track\n(Ctrl+T)")
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
voiceUpDown := func(gtx C) D {
|
||||
numStyle := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
|
||||
return in.Layout(gtx, numStyle.Layout)
|
||||
}
|
||||
effectBtnStyle := ToggleButton(t.Theme, te.EffectBtn, "Hex")
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
||||
layout.Rigid(addSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(subtractSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(addOctaveBtnStyle.Layout),
|
||||
layout.Rigid(subtractOctaveBtnStyle.Layout),
|
||||
layout.Rigid(noteOffBtnStyle.Layout),
|
||||
layout.Rigid(effectBtnStyle.Layout),
|
||||
layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)),
|
||||
layout.Rigid(voiceUpDown),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(deleteTrackBtnStyle.Layout),
|
||||
layout.Rigid(newTrackBtnStyle.Layout))
|
||||
})
|
||||
}
|
||||
|
||||
const baseNote = 24
|
||||
|
||||
var notes = []string{
|
||||
"C-",
|
||||
"C#",
|
||||
"D-",
|
||||
"D#",
|
||||
"E-",
|
||||
"F-",
|
||||
"F#",
|
||||
"G-",
|
||||
"G#",
|
||||
"A-",
|
||||
"A#",
|
||||
"B-",
|
||||
}
|
||||
|
||||
func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
||||
|
||||
beatMarkerDensity := t.RowsPerBeat().Value()
|
||||
switch beatMarkerDensity {
|
||||
case 0, 1, 2:
|
||||
beatMarkerDensity = 4
|
||||
}
|
||||
|
||||
playSongRow := t.PlaySongRow()
|
||||
pxWidth := gtx.Dp(trackColWidth)
|
||||
pxHeight := gtx.Dp(trackRowHeight)
|
||||
pxPatMarkWidth := gtx.Dp(trackPatMarkWidth)
|
||||
pxRowMarkWidth := gtx.Dp(trackRowMarkWidth)
|
||||
|
||||
colTitle := func(gtx C, i int) D {
|
||||
h := gtx.Dp(unit.Dp(trackColTitleHeight))
|
||||
title := ((*tracker.Order)(t.Model)).Title(i)
|
||||
gtx.Constraints = layout.Exact(image.Pt(pxWidth, h))
|
||||
LabelStyle{Alignment: layout.N, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.Theme.Shaper}.Layout(gtx)
|
||||
return D{Size: image.Pt(pxWidth, h)}
|
||||
}
|
||||
|
||||
rowTitleBg := func(gtx C, j int) D {
|
||||
if mod(j, beatMarkerDensity*2) == 0 {
|
||||
paint.FillShape(gtx.Ops, twoBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
||||
} else if mod(j, beatMarkerDensity) == 0 {
|
||||
paint.FillShape(gtx.Ops, oneBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
||||
}
|
||||
if t.SongPanel.PlayingBtn.Bool.Value() && j == playSongRow {
|
||||
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
||||
}
|
||||
return D{}
|
||||
}
|
||||
|
||||
rowTitle := func(gtx C, j int) D {
|
||||
rpp := intMax(t.RowsPerPattern().Value(), 1)
|
||||
pat := j / rpp
|
||||
row := j % rpp
|
||||
w := pxPatMarkWidth + pxRowMarkWidth
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
if row == 0 {
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", pat)), op.CallOp{})
|
||||
}
|
||||
defer op.Offset(image.Pt(pxPatMarkWidth, 0)).Push(gtx.Ops).Pop()
|
||||
paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", row)), op.CallOp{})
|
||||
return D{Size: image.Pt(w, pxHeight)}
|
||||
}
|
||||
|
||||
drawSelection := te.scrollTable.Table.Cursor() != te.scrollTable.Table.Cursor2()
|
||||
selection := te.scrollTable.Table.Range()
|
||||
|
||||
cell := func(gtx C, x, y int) D {
|
||||
// draw the background, to indicate selection
|
||||
color := transparent
|
||||
point := tracker.Point{X: x, Y: y}
|
||||
if drawSelection && selection.Contains(point) {
|
||||
color = inactiveSelectionColor
|
||||
if te.scrollTable.Focused() {
|
||||
color = selectionColor
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
||||
// draw the cursor
|
||||
if point == te.scrollTable.Table.Cursor() {
|
||||
cw := gtx.Constraints.Min.X
|
||||
cx := 0
|
||||
if t.Model.Notes().Effect(x) {
|
||||
cw /= 2
|
||||
if t.Model.Notes().LowNibble() {
|
||||
cx += cw
|
||||
}
|
||||
}
|
||||
c := inactiveSelectionColor
|
||||
if te.scrollTable.Focused() {
|
||||
c = cursorColor
|
||||
}
|
||||
paint.FillShape(gtx.Ops, c, clip.Rect{Min: image.Pt(cx, 0), Max: image.Pt(cx+cw, gtx.Constraints.Min.Y)}.Op())
|
||||
}
|
||||
// draw the pattern marker
|
||||
rpp := intMax(t.RowsPerPattern().Value(), 1)
|
||||
pat := y / rpp
|
||||
row := y % rpp
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
s := t.Model.Order().Value(tracker.Point{X: x, Y: pat})
|
||||
if row == 0 { // draw the pattern marker
|
||||
paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, patternIndexToString(s), op.CallOp{})
|
||||
}
|
||||
if row == 1 && t.Model.Notes().Unique(x, s) { // draw a * if the pattern is unique
|
||||
paint.ColorOp{Color: mediumEmphasisTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, "*", op.CallOp{})
|
||||
}
|
||||
if te.scrollTable.Table.Cursor() == point && te.scrollTable.Focused() {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops)
|
||||
}
|
||||
val := noteStr[byte(t.Model.Notes().Value(tracker.Point{X: x, Y: y}))]
|
||||
if t.Model.Notes().Effect(x) {
|
||||
val = hexStr[byte(t.Model.Notes().Value(tracker.Point{X: x, Y: y}))]
|
||||
}
|
||||
widget.Label{Alignment: text.Middle}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, val, op.CallOp{})
|
||||
return D{Size: image.Pt(pxWidth, pxHeight)}
|
||||
}
|
||||
table := FilledScrollTable(t.Theme, te.scrollTable, cell, colTitle, rowTitle, nil, rowTitleBg)
|
||||
table.RowTitleWidth = trackPatMarkWidth + trackRowMarkWidth
|
||||
table.ColumnTitleHeight = trackColTitleHeight
|
||||
table.CellWidth = trackColWidth
|
||||
table.CellHeight = trackRowHeight
|
||||
return table.Layout(gtx)
|
||||
}
|
||||
|
||||
func mod(x, d int) int {
|
||||
x = x % d
|
||||
if x >= 0 {
|
||||
return x
|
||||
}
|
||||
if d < 0 {
|
||||
return x - d
|
||||
}
|
||||
return x + d
|
||||
}
|
||||
|
||||
func noteAsValue(octave, note int) byte {
|
||||
return byte(baseNote + (octave * 12) + note)
|
||||
}
|
||||
|
||||
func (te *NoteEditor) command(gtx C, t *Tracker, e key.Event) {
|
||||
if e.Name == "A" || e.Name == "1" {
|
||||
t.Model.Notes().Table().Fill(0)
|
||||
te.scrollTable.EnsureCursorVisible()
|
||||
return
|
||||
}
|
||||
var n byte
|
||||
if t.Model.Notes().Effect(te.scrollTable.Table.Cursor().X) {
|
||||
if nibbleValue, err := strconv.ParseInt(e.Name, 16, 8); err == nil {
|
||||
n = t.Model.Notes().Value(te.scrollTable.Table.Cursor())
|
||||
t.Model.Notes().FillNibble(byte(nibbleValue), t.Model.Notes().LowNibble())
|
||||
goto validNote
|
||||
}
|
||||
} else {
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
n = noteAsValue(t.OctaveNumberInput.Int.Value(), val)
|
||||
t.Model.Notes().Table().Fill(int(n))
|
||||
goto validNote
|
||||
}
|
||||
}
|
||||
return
|
||||
validNote:
|
||||
te.scrollTable.EnsureCursorVisible()
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
trk := te.scrollTable.Table.Cursor().X
|
||||
t.KeyPlaying[e.Name] = t.TrackNoteOn(trk, n)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
case "+":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
te.AddOctaveBtn.Action.Do()
|
||||
} else {
|
||||
te.AddSemitoneBtn.Action.Do()
|
||||
}
|
||||
case "-":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
te.SubtractSemitoneBtn.Action.Do()
|
||||
} else {
|
||||
te.SubtractOctaveBtn.Action.Do()
|
||||
}
|
||||
}*/
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
|
||||
"gioui.org/font"
|
||||
@ -23,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
type NumberInput struct {
|
||||
Value int
|
||||
Int tracker.Int
|
||||
dragStartValue int
|
||||
dragStartXY float32
|
||||
clickDecrease gesture.Click
|
||||
@ -33,8 +34,6 @@ type NumberInput struct {
|
||||
|
||||
type NumericUpDownStyle struct {
|
||||
NumberInput *NumberInput
|
||||
Min int
|
||||
Max int
|
||||
Color color.NRGBA
|
||||
Font font.Font
|
||||
TextSize unit.Sp
|
||||
@ -51,15 +50,17 @@ type NumericUpDownStyle struct {
|
||||
shaper text.Shaper
|
||||
}
|
||||
|
||||
func NumericUpDown(th *material.Theme, number *NumberInput, min, max int, tooltip string) NumericUpDownStyle {
|
||||
func NewNumberInput(v tracker.Int) *NumberInput {
|
||||
return &NumberInput{Int: v}
|
||||
}
|
||||
|
||||
func NumericUpDown(th *material.Theme, number *NumberInput, tooltip string) NumericUpDownStyle {
|
||||
bgColor := th.Palette.Fg
|
||||
bgColor.R /= 4
|
||||
bgColor.G /= 4
|
||||
bgColor.B /= 4
|
||||
return NumericUpDownStyle{
|
||||
NumberInput: number,
|
||||
Min: min,
|
||||
Max: max,
|
||||
Color: white,
|
||||
BorderColor: th.Palette.Fg,
|
||||
IconColor: th.Palette.ContrastFg,
|
||||
@ -104,12 +105,6 @@ func (s *NumericUpDownStyle) actualLayout(gtx C) D {
|
||||
layout.Flexed(1, s.layoutText),
|
||||
layout.Rigid(s.button(gtx.Constraints.Max.Y, widgetForIcon(icons.NavigationArrowForward), 1, &s.NumberInput.clickIncrease)),
|
||||
)
|
||||
if s.NumberInput.Value < s.Min {
|
||||
s.NumberInput.Value = s.Min
|
||||
}
|
||||
if s.NumberInput.Value > s.Max {
|
||||
s.NumberInput.Value = s.Max
|
||||
}
|
||||
off.Pop()
|
||||
c2.Pop()
|
||||
return layout.Dimensions{Size: size}
|
||||
@ -156,7 +151,7 @@ func (s *NumericUpDownStyle) layoutText(gtx C) D {
|
||||
}),
|
||||
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
||||
paint.ColorOp{Color: s.Color}.Add(gtx.Ops)
|
||||
return widget.Label{Alignment: text.Middle}.Layout(gtx, &s.shaper, s.Font, s.TextSize, fmt.Sprintf("%v", s.NumberInput.Value), op.CallOp{})
|
||||
return widget.Label{Alignment: text.Middle}.Layout(gtx, &s.shaper, s.Font, s.TextSize, fmt.Sprintf("%v", s.NumberInput.Int.Value()), op.CallOp{})
|
||||
}),
|
||||
layout.Expanded(s.layoutDrag),
|
||||
)
|
||||
@ -169,13 +164,13 @@ func (s *NumericUpDownStyle) layoutDrag(gtx layout.Context) layout.Dimensions {
|
||||
if e, ok := ev.(pointer.Event); ok {
|
||||
switch e.Type {
|
||||
case pointer.Press:
|
||||
s.NumberInput.dragStartValue = s.NumberInput.Value
|
||||
s.NumberInput.dragStartValue = s.NumberInput.Int.Value()
|
||||
s.NumberInput.dragStartXY = e.Position.X - e.Position.Y
|
||||
|
||||
case pointer.Drag:
|
||||
var deltaCoord float32
|
||||
deltaCoord = e.Position.X - e.Position.Y - s.NumberInput.dragStartXY
|
||||
s.NumberInput.Value = s.NumberInput.dragStartValue + int(deltaCoord/pxPerStep+0.5)
|
||||
s.NumberInput.Int.Set(s.NumberInput.dragStartValue + int(deltaCoord/pxPerStep+0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -200,7 +195,7 @@ func (s *NumericUpDownStyle) layoutClick(gtx layout.Context, delta int, click *g
|
||||
for _, e := range click.Events(gtx) {
|
||||
switch e.Type {
|
||||
case gesture.TypeClick:
|
||||
s.NumberInput.Value += delta
|
||||
s.NumberInput.Int.Add(delta)
|
||||
}
|
||||
}
|
||||
// Avoid affecting the input tree with pointer events.
|
||||
|
||||
156
tracker/gioui/order_editor.go
Normal file
156
tracker/gioui/order_editor.go
Normal file
@ -0,0 +1,156 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/key"
|
||||
"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
|
||||
const orderTitleHeight = unit.Dp(52)
|
||||
|
||||
type OrderEditor struct {
|
||||
scrollTable *ScrollTable
|
||||
tag struct{}
|
||||
}
|
||||
|
||||
var patternIndexStrings [36]string
|
||||
|
||||
func init() {
|
||||
for i := 0; i < 10; i++ {
|
||||
patternIndexStrings[i] = string('0' + byte(i))
|
||||
}
|
||||
for i := 10; i < 36; i++ {
|
||||
patternIndexStrings[i] = string('A' + byte(i-10))
|
||||
}
|
||||
}
|
||||
|
||||
func NewOrderEditor(m *tracker.Model) *OrderEditor {
|
||||
return &OrderEditor{
|
||||
scrollTable: NewScrollTable(
|
||||
m.Order().Table(),
|
||||
m.Tracks().List(),
|
||||
m.OrderRows().List(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) Layout(gtx C, t *Tracker) D {
|
||||
if oe.scrollTable.CursorMoved() {
|
||||
cursor := t.TrackEditor.scrollTable.Table.Cursor()
|
||||
t.TrackEditor.scrollTable.ColTitleList.CenterOn(cursor.X)
|
||||
t.TrackEditor.scrollTable.RowTitleList.CenterOn(cursor.Y)
|
||||
}
|
||||
|
||||
for _, e := range gtx.Events(&oe.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.Event:
|
||||
if e.State != key.Press {
|
||||
continue
|
||||
}
|
||||
oe.command(gtx, t, e)
|
||||
}
|
||||
}
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &oe.tag, Keys: "Ctrl-⌫|Ctrl-⌦|⏎|Ctrl-⏎|0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z"}.Add(gtx.Ops)
|
||||
|
||||
colTitle := func(gtx C, i int) D {
|
||||
h := gtx.Dp(orderTitleHeight)
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: 0, Y: float32(h)})).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints = layout.Exact(image.Pt(1e6, 1e6))
|
||||
title := t.Model.Order().Title(i)
|
||||
LabelStyle{Alignment: layout.NW, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.Theme.Shaper}.Layout(gtx)
|
||||
return D{Size: image.Pt(patternCellWidth, h)}
|
||||
}
|
||||
|
||||
rowTitle := func(gtx C, j int) D {
|
||||
if playPos := t.PlayPosition(); t.SongPanel.PlayingBtn.Bool.Value() && j == playPos.OrderRow {
|
||||
paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op())
|
||||
}
|
||||
w := gtx.Dp(unit.Dp(30))
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)), op.CallOp{})
|
||||
return D{Size: image.Pt(w, patternCellHeight)}
|
||||
}
|
||||
|
||||
selection := oe.scrollTable.Table.Range()
|
||||
|
||||
cell := func(gtx C, x, y int) D {
|
||||
val := patternIndexToString(t.Model.Order().Value(tracker.Point{X: x, Y: y}))
|
||||
color := patternCellColor
|
||||
point := tracker.Point{X: x, Y: y}
|
||||
if selection.Contains(point) {
|
||||
color = inactiveSelectionColor
|
||||
if oe.scrollTable.Focused() {
|
||||
color = selectionColor
|
||||
if point == oe.scrollTable.Table.Cursor() {
|
||||
color = cursorColor
|
||||
}
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(1, 1), Max: image.Pt(gtx.Constraints.Min.X-1, gtx.Constraints.Min.X-1)}.Op())
|
||||
paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops)
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
widget.Label{Alignment: text.Middle}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, val, op.CallOp{})
|
||||
return D{Size: image.Pt(patternCellWidth, patternCellHeight)}
|
||||
}
|
||||
|
||||
table := FilledScrollTable(t.Theme, oe.scrollTable, cell, colTitle, rowTitle, nil, nil)
|
||||
table.ColumnTitleHeight = orderTitleHeight
|
||||
|
||||
return table.Layout(gtx)
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) command(gtx C, t *Tracker, e key.Event) {
|
||||
switch e.Name {
|
||||
case key.NameDeleteBackward:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Model.DeleteOrderRow(true).Do()
|
||||
}
|
||||
case key.NameDeleteForward:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Model.DeleteOrderRow(false).Do()
|
||||
}
|
||||
case key.NameReturn:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
oe.scrollTable.Table.MoveCursor(0, -1)
|
||||
oe.scrollTable.Table.SetCursor2(oe.scrollTable.Table.Cursor())
|
||||
}
|
||||
t.Model.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut)).Do()
|
||||
}
|
||||
if iv, err := strconv.Atoi(e.Name); err == nil {
|
||||
t.Model.Order().SetValue(oe.scrollTable.Table.Cursor(), iv)
|
||||
oe.scrollTable.EnsureCursorVisible()
|
||||
}
|
||||
if b := int(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 {
|
||||
t.Model.Order().SetValue(oe.scrollTable.Table.Cursor(), b+10)
|
||||
oe.scrollTable.EnsureCursorVisible()
|
||||
}
|
||||
}
|
||||
|
||||
func patternIndexToString(index int) string {
|
||||
if index < 0 {
|
||||
return ""
|
||||
} else if index < len(patternIndexStrings) {
|
||||
return patternIndexStrings[index]
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
@ -1,262 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"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/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
|
||||
titleList *DragList
|
||||
scrollBar *ScrollBar
|
||||
tag bool
|
||||
focused bool
|
||||
requestFocus bool
|
||||
}
|
||||
|
||||
func NewOrderEditor() *OrderEditor {
|
||||
return &OrderEditor{
|
||||
list: &layout.List{Axis: layout.Vertical},
|
||||
titleList: &DragList{List: &layout.List{Axis: layout.Horizontal}},
|
||||
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 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.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
case "Space":
|
||||
if !t.Playing() {
|
||||
t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut))
|
||||
startRow := t.Cursor().ScoreRow
|
||||
startRow.Row = 0
|
||||
t.PlayFromPosition(startRow)
|
||||
} else {
|
||||
t.SetPlaying(false)
|
||||
}
|
||||
case key.NameReturn:
|
||||
t.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut))
|
||||
case key.NameUpArrow:
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.ScoreRow = tracker.ScoreRow{}
|
||||
} 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)
|
||||
case "+":
|
||||
t.AdjustPatternNumber(1, e.Modifiers.Contain(key.ModShortcut))
|
||||
continue
|
||||
case "-":
|
||||
t.AdjustPatternNumber(-1, e.Modifiers.Contain(key.ModShortcut))
|
||||
continue
|
||||
case key.NameHome:
|
||||
cursor := t.Cursor()
|
||||
cursor.Track = 0
|
||||
t.SetCursor(cursor)
|
||||
case key.NameEnd:
|
||||
cursor := t.Cursor()
|
||||
cursor.Track = len(t.Song().Score.Tracks) - 1
|
||||
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.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.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
if oe.requestFocus {
|
||||
oe.requestFocus = false
|
||||
key.FocusOp{Tag: &oe.tag}.Add(gtx.Ops)
|
||||
}
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
pointer.InputOp{Tag: &oe.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
|
||||
key.InputOp{Tag: &oe.tag, Keys: "←|→|↑|↓|Shift-←|Shift-→|Shift-↑|Shift-↓|⏎|⇱|⇲|⌫|⌦|Ctrl-⌫|Ctrl-⌦|+|-|Space|0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z"}.Add(gtx.Ops)
|
||||
|
||||
patternRect := tracker.ScoreRect{
|
||||
Corner1: tracker.ScorePoint{ScoreRow: tracker.ScoreRow{Pattern: t.Cursor().Pattern}, Track: t.Cursor().Track},
|
||||
Corner2: tracker.ScorePoint{ScoreRow: tracker.ScoreRow{Pattern: t.SelectionCorner().Pattern}, Track: t.SelectionCorner().Track},
|
||||
}
|
||||
|
||||
// draw the single letter titles for tracks
|
||||
{
|
||||
gtx := gtx
|
||||
stack := op.Offset(image.Pt(patternRowMarkerWidth, 0)).Push(gtx.Ops)
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X-patternRowMarkerWidth, patternCellHeight))
|
||||
elem := func(gtx C, i int) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(patternCellWidth, patternCellHeight))
|
||||
instr, err := t.Song().Patch.InstrumentForVoice(t.Song().Score.FirstVoiceForTrack(i))
|
||||
var title string
|
||||
if err == nil && len(t.Song().Patch[instr].Name) > 0 {
|
||||
title = string(t.Song().Patch[instr].Name[0])
|
||||
} else {
|
||||
title = "?"
|
||||
}
|
||||
LabelStyle{Alignment: layout.N, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.TextShaper}.Layout(gtx)
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
style := FilledDragList(t.Theme, oe.titleList, len(t.Song().Score.Tracks), elem, t.SwapTracks)
|
||||
style.HoverColor = transparent
|
||||
style.SelectedColor = transparent
|
||||
style.Layout(gtx)
|
||||
stack.Pop()
|
||||
}
|
||||
op.Offset(image.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 := t.PlayPosition(); t.Playing() && 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, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)), op.CallOp{})
|
||||
stack := op.Offset(image.Pt(patternRowMarkerWidth, 0)).Push(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(image.Pt(0, -2)).Add(gtx.Ops)
|
||||
widget.Label{Alignment: text.Middle}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, patternIndexToString(track.Order[j]), op.CallOp{})
|
||||
op.Offset(image.Pt(0, 2)).Add(gtx.Ops)
|
||||
}
|
||||
point := tracker.ScorePoint{Track: i, ScoreRow: tracker.ScoreRow{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(image.Pt(patternCellWidth, 0)).Add(gtx.Ops)
|
||||
}
|
||||
stack.Pop()
|
||||
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))
|
||||
}
|
||||
@ -1,256 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"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"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type ParamEditor struct {
|
||||
list *layout.List
|
||||
scrollBar *ScrollBar
|
||||
Parameters []*ParameterWidget
|
||||
DeleteUnitBtn *TipClickable
|
||||
CopyUnitBtn *TipClickable
|
||||
ClearUnitBtn *TipClickable
|
||||
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(TipClickable),
|
||||
ClearUnitBtn: new(TipClickable),
|
||||
CopyUnitBtn: new(TipClickable),
|
||||
list: &layout.List{Axis: layout.Vertical},
|
||||
scrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
}
|
||||
for range sointu.UnitNames {
|
||||
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 pe.requestFocus {
|
||||
pe.requestFocus = false
|
||||
key.FocusOp{Tag: &pe.tag}.Add(gtx.Ops)
|
||||
}
|
||||
editorFunc := pe.layoutUnitSliders
|
||||
if y := t.Unit().Type; y == "" || y != t.InstrumentEditor.unitTypeEditor.Text() {
|
||||
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)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
|
||||
pointer.InputOp{Tag: &pe.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
key.InputOp{Tag: &pe.tag, Keys: "←|Shift-←|→|Shift-→|↑|↓|⎋"}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
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 t.ParamIndex() != index {
|
||||
t.SetParamIndex(index)
|
||||
} else {
|
||||
t.ResetParam()
|
||||
}
|
||||
pe.Focus()
|
||||
}
|
||||
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.Clickable.Clicked() {
|
||||
t.SetUnitType("")
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
t.InstrumentEditor.unitDragList.Focus()
|
||||
}
|
||||
for pe.DeleteUnitBtn.Clickable.Clicked() {
|
||||
t.DeleteUnits(false, t.UnitIndex(), t.UnitIndex())
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
t.InstrumentEditor.unitDragList.Focus()
|
||||
}
|
||||
for pe.CopyUnitBtn.Clickable.Clicked() {
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
contents, err := yaml.Marshal([]sointu.Unit{t.Unit()})
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Unit copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
}
|
||||
copyUnitBtnStyle := IconButton(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, true, "Copy unit (Ctrl+C)")
|
||||
deleteUnitBtnStyle := IconButton(t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, t.CanDeleteUnit(), "Delete unit (Del)")
|
||||
text := t.Unit().Type
|
||||
if text == "" {
|
||||
text = "Choose unit type"
|
||||
} else {
|
||||
text = strings.Title(text)
|
||||
}
|
||||
hintText := Label(text, white, t.TextShaper)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(deleteUnitBtnStyle.Layout),
|
||||
layout.Rigid(copyUnitBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
var dims D
|
||||
if t.Unit().Type != "" {
|
||||
clearUnitBtnStyle := IconButton(t.Theme, pe.ClearUnitBtn, icons.ContentClear, true, "Clear unit")
|
||||
dims = clearUnitBtnStyle.Layout(gtx)
|
||||
}
|
||||
return D{Size: image.Pt(gtx.Dp(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(sointu.UnitNames[i])
|
||||
t.InstrumentEditor.unitTypeEditor.SetText(sointu.UnitNames[i])
|
||||
}
|
||||
text := sointu.UnitNames[i]
|
||||
if t.InstrumentEditor.unitTypeEditor.Focused() && !strings.HasPrefix(text, t.InstrumentEditor.unitTypeEditor.Text()) {
|
||||
return D{}
|
||||
}
|
||||
labelStyle := LabelStyle{Text: text, ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.TextShaper}
|
||||
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 pe.ChooseUnitTypeBtns[i].Layout(gtx, func(gtx C) D {
|
||||
return leftMargin.Layout(gtx, labelStyle.Layout)
|
||||
})
|
||||
}))
|
||||
}
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return pe.list.Layout(gtx, len(sointu.UnitNames), listElem)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return pe.scrollBar.Layout(gtx, unit.Dp(10), len(sointu.UnitNames), &pe.list.Position)
|
||||
}),
|
||||
)
|
||||
}
|
||||
@ -1,218 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type ParameterWidget struct {
|
||||
floatWidget widget.Float
|
||||
boolWidget widget.Bool
|
||||
labelBtn widget.Clickable
|
||||
instrBtn widget.Clickable
|
||||
instrMenu Menu
|
||||
unitBtn widget.Clickable
|
||||
unitMenu Menu
|
||||
}
|
||||
|
||||
type ParameterStyle struct {
|
||||
tracker *Tracker
|
||||
Parameter *tracker.Parameter
|
||||
ParameterWidget *ParameterWidget
|
||||
Theme *material.Theme
|
||||
Focus bool
|
||||
}
|
||||
|
||||
func (t *Tracker) ParamStyle(th *material.Theme, param *tracker.Parameter, paramWidget *ParameterWidget) ParameterStyle {
|
||||
return ParameterStyle{
|
||||
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way
|
||||
Parameter: param,
|
||||
Theme: th,
|
||||
ParameterWidget: paramWidget,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParameterWidget) Clicked() bool {
|
||||
return p.labelBtn.Clicked()
|
||||
}
|
||||
|
||||
func (p ParameterStyle) Layout(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.ParameterWidget.labelBtn.Layout(gtx, func(gtx C) D {
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110))
|
||||
return layout.E.Layout(gtx, Label(p.Parameter.Name, white, p.tracker.TextShaper))
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
switch p.Parameter.Type {
|
||||
case tracker.IntegerParameter:
|
||||
for _, e := range gtx.Events(&p.ParameterWidget.floatWidget) {
|
||||
switch ev := e.(type) {
|
||||
case pointer.Event:
|
||||
if ev.Type == pointer.Scroll {
|
||||
delta := math.Min(math.Max(float64(ev.Scroll.Y), -1), 1)
|
||||
p.Parameter.Value += int(math.Round(delta))
|
||||
}
|
||||
}
|
||||
}
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
if !p.ParameterWidget.floatWidget.Dragging() {
|
||||
p.ParameterWidget.floatWidget.Value = float32(p.Parameter.Value)
|
||||
}
|
||||
sliderStyle := material.Slider(p.Theme, &p.ParameterWidget.floatWidget, float32(p.Parameter.Min), float32(p.Parameter.Max))
|
||||
sliderStyle.Color = p.Theme.Fg
|
||||
r := image.Rectangle{Max: gtx.Constraints.Min}
|
||||
area := clip.Rect(r).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &p.ParameterWidget.floatWidget, Types: pointer.Scroll, ScrollBounds: image.Rectangle{Min: image.Pt(0, -1e6), Max: image.Pt(0, 1e6)}}.Add(gtx.Ops)
|
||||
dims := sliderStyle.Layout(gtx)
|
||||
area.Pop()
|
||||
p.Parameter.Value = int(p.ParameterWidget.floatWidget.Value + 0.5)
|
||||
return dims
|
||||
case tracker.BoolParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(60))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
p.ParameterWidget.boolWidget.Value = p.Parameter.Value > p.Parameter.Min
|
||||
boolStyle := material.Switch(p.Theme, &p.ParameterWidget.boolWidget, "Toggle boolean parameter")
|
||||
boolStyle.Color.Disabled = p.Theme.Fg
|
||||
boolStyle.Color.Enabled = white
|
||||
dims := layout.Center.Layout(gtx, boolStyle.Layout)
|
||||
if p.ParameterWidget.boolWidget.Value {
|
||||
p.Parameter.Value = p.Parameter.Max
|
||||
} else {
|
||||
p.Parameter.Value = p.Parameter.Min
|
||||
}
|
||||
return dims
|
||||
case tracker.IDParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
for clickedItem, hasClicked := p.ParameterWidget.instrMenu.Clicked(); hasClicked; {
|
||||
p.Parameter.Value = p.tracker.Song().Patch[clickedItem].Units[0].ID
|
||||
clickedItem, hasClicked = p.ParameterWidget.instrMenu.Clicked()
|
||||
}
|
||||
instrItems := make([]MenuItem, len(p.tracker.Song().Patch))
|
||||
for i, instr := range p.tracker.Song().Patch {
|
||||
instrItems[i].Text = instr.Name
|
||||
instrItems[i].IconBytes = icons.NavigationChevronRight
|
||||
}
|
||||
var unitItems []MenuItem
|
||||
instrName := "<instr>"
|
||||
unitName := "<unit>"
|
||||
targetI, targetU, err := p.tracker.Song().Patch.FindUnit(p.Parameter.Value)
|
||||
if err == nil {
|
||||
targetInstrument := p.tracker.Song().Patch[targetI]
|
||||
instrName = targetInstrument.Name
|
||||
units := targetInstrument.Units
|
||||
unitName = fmt.Sprintf("%v: %v", targetU, units[targetU].Type)
|
||||
unitItems = make([]MenuItem, len(units))
|
||||
for clickedItem, hasClicked := p.ParameterWidget.unitMenu.Clicked(); hasClicked; {
|
||||
p.Parameter.Value = units[clickedItem].ID
|
||||
clickedItem, hasClicked = p.ParameterWidget.unitMenu.Clicked()
|
||||
}
|
||||
for j, unit := range units {
|
||||
unitItems[j].Text = fmt.Sprintf("%v: %v", j, unit.Type)
|
||||
unitItems[j].IconBytes = icons.NavigationChevronRight
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(p.tracker.layoutMenu(instrName, &p.ParameterWidget.instrBtn, &p.ParameterWidget.instrMenu, unit.Dp(200),
|
||||
instrItems...,
|
||||
)),
|
||||
layout.Rigid(p.tracker.layoutMenu(unitName, &p.ParameterWidget.unitBtn, &p.ParameterWidget.unitMenu, unit.Dp(200),
|
||||
unitItems...,
|
||||
)),
|
||||
)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if p.Parameter.Type != tracker.IDParameter {
|
||||
return Label(p.Parameter.Hint, white, p.tracker.TextShaper)(gtx)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
func (t *Tracker) layoutParameter(gtx C, index int) D {
|
||||
u := t.Unit()
|
||||
ut, _ := sointu.UnitTypes[u.Type]
|
||||
|
||||
params := u.Parameters
|
||||
var name string
|
||||
var value, min, max int
|
||||
var valueText string
|
||||
if u.Type == "oscillator" && index == len(ut) {
|
||||
name = "sample"
|
||||
key := compiler.SampleOffset{Start: uint32(params["samplestart"]), LoopStart: uint16(params["loopstart"]), LoopLength: uint16(params["looplength"])}
|
||||
if v, ok := tracker.GmDlsEntryMap[key]; ok {
|
||||
value = v + 1
|
||||
valueText = fmt.Sprintf("%v / %v", value, tracker.GmDlsEntries[v].Name)
|
||||
} else {
|
||||
value = 0
|
||||
valueText = "0 / custom"
|
||||
}
|
||||
min, max = 0, len(tracker.GmDlsEntries)
|
||||
} else {
|
||||
if ut[index].MaxValue < ut[index].MinValue {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
name = ut[index].Name
|
||||
if u.Type == "oscillator" && (name == "samplestart" || name == "loopstart" || name == "looplength") {
|
||||
if params["type"] != sointu.Sample {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
}
|
||||
value = params[name]
|
||||
min, max = ut[index].MinValue, ut[index].MaxValue
|
||||
if u.Type == "send" && name == "voice" {
|
||||
max = t.Song().Patch.NumVoices()
|
||||
} else if u.Type == "send" && name == "unit" { // set the maximum values depending on the send target
|
||||
instrIndex, _, _ := t.Song().Patch.FindSendTarget(t.Unit().Parameters["target"])
|
||||
if instrIndex != -1 {
|
||||
max = len(t.Song().Patch[instrIndex].Units) - 1
|
||||
}
|
||||
} else if u.Type == "send" && name == "port" { // set the maximum values depending on the send target
|
||||
instrIndex, unitIndex, _ := t.Song().Patch.FindSendTarget(t.Unit().Parameters["target"])
|
||||
if instrIndex != -1 && unitIndex != -1 {
|
||||
max = len(sointu.Ports[t.Song().Patch[instrIndex].Units[unitIndex].Type]) - 1
|
||||
}
|
||||
}
|
||||
hint := t.Song().Patch.ParamHintString(t.InstrIndex(), t.UnitIndex(), name)
|
||||
if hint != "" {
|
||||
valueText = fmt.Sprintf("%v / %v", value, hint)
|
||||
} else {
|
||||
valueText = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
}*/
|
||||
81
tracker/gioui/popup_alert.go
Normal file
81
tracker/gioui/popup_alert.go
Normal file
@ -0,0 +1,81 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type PopupAlert struct {
|
||||
alerts *tracker.Alerts
|
||||
prevUpdate time.Time
|
||||
shaper *text.Shaper
|
||||
}
|
||||
|
||||
var alertSpeed = 150 * time.Millisecond
|
||||
var alertMargin = layout.UniformInset(unit.Dp(6))
|
||||
var alertInset = layout.UniformInset(unit.Dp(6))
|
||||
|
||||
func NewPopupAlert(alerts *tracker.Alerts, shaper *text.Shaper) *PopupAlert {
|
||||
return &PopupAlert{alerts: alerts, shaper: shaper, prevUpdate: time.Now()}
|
||||
}
|
||||
|
||||
func (a *PopupAlert) Layout(gtx C) D {
|
||||
now := time.Now()
|
||||
if a.alerts.Update(now.Sub(a.prevUpdate)) {
|
||||
op.InvalidateOp{At: now.Add(50 * time.Millisecond)}.Add(gtx.Ops)
|
||||
}
|
||||
a.prevUpdate = now
|
||||
|
||||
var totalY float64
|
||||
a.alerts.Iterate(func(alert tracker.Alert) {
|
||||
var color, textColor, shadeColor color.NRGBA
|
||||
switch alert.Priority {
|
||||
case tracker.Warning:
|
||||
color = warningColor
|
||||
textColor = black
|
||||
case tracker.Error:
|
||||
color = errorColor
|
||||
textColor = black
|
||||
default:
|
||||
color = popupSurfaceColor
|
||||
textColor = white
|
||||
shadeColor = black
|
||||
}
|
||||
bgWidget := func(gtx C) D {
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
labelStyle := LabelStyle{Text: alert.Message, Color: textColor, ShadeColor: shadeColor, Font: labelDefaultFont, Alignment: layout.Center, FontSize: unit.Sp(16), Shaper: a.shaper}
|
||||
alertMargin.Layout(gtx, func(gtx C) D {
|
||||
return layout.S.Layout(gtx, func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
recording := op.Record(gtx.Ops)
|
||||
dims := layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Expanded(bgWidget),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return alertInset.Layout(gtx, labelStyle.Layout)
|
||||
}),
|
||||
)
|
||||
macro := recording.Stop()
|
||||
delta := float64(dims.Size.Y + gtx.Dp(alertMargin.Bottom))
|
||||
op.Offset(image.Point{0, int(-totalY*alert.FadeLevel + delta*(1-alert.FadeLevel))}).Add((gtx.Ops))
|
||||
totalY += delta
|
||||
macro.Add(gtx.Ops)
|
||||
return dims
|
||||
})
|
||||
})
|
||||
})
|
||||
return D{}
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
const rowMarkerWidth = 50
|
||||
|
||||
func (t *Tracker) layoutRowMarkers(gtx C) D {
|
||||
gtx.Constraints.Min.X = rowMarkerWidth
|
||||
paint.FillShape(gtx.Ops, rowMarkerSurfaceColor, clip.Rect{
|
||||
Max: gtx.Constraints.Max,
|
||||
}.Op())
|
||||
//defer op.Save(gtx.Ops).Load()
|
||||
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
||||
op.Offset(image.Pt(0, (gtx.Constraints.Max.Y-trackRowHeight)/2)).Add(gtx.Ops)
|
||||
cursorSongRow := t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row
|
||||
playPos := t.PlayPosition()
|
||||
playSongRow := playPos.Pattern*t.Song().Score.RowsPerPattern + playPos.Row
|
||||
op.Offset(image.Pt(0, (-1*trackRowHeight)*(cursorSongRow))).Add(gtx.Ops)
|
||||
beatMarkerDensity := t.Song().RowsPerBeat
|
||||
for beatMarkerDensity <= 2 {
|
||||
beatMarkerDensity *= 2
|
||||
}
|
||||
for i := 0; i < t.Song().Score.Length; i++ {
|
||||
for j := 0; j < t.Song().Score.RowsPerPattern; j++ {
|
||||
songRow := i*t.Song().Score.RowsPerPattern + j
|
||||
if mod(songRow, beatMarkerDensity*2) == 0 {
|
||||
paint.FillShape(gtx.Ops, twoBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
} else if mod(songRow, beatMarkerDensity) == 0 {
|
||||
paint.FillShape(gtx.Ops, oneBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
}
|
||||
if t.Playing() && songRow == playSongRow {
|
||||
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
}
|
||||
if j == 0 {
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", i)), op.CallOp{})
|
||||
}
|
||||
if t.TrackEditor.Focused() && songRow == cursorSongRow {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops)
|
||||
}
|
||||
op.Offset(image.Pt(rowMarkerWidth/2, 0)).Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)), op.CallOp{})
|
||||
op.Offset(image.Pt(-rowMarkerWidth/2, trackRowHeight)).Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
return layout.Dimensions{Size: image.Pt(rowMarkerWidth, gtx.Constraints.Max.Y)}
|
||||
}
|
||||
|
||||
func mod(a, b int) int {
|
||||
m := a % b
|
||||
if a < 0 && b < 0 {
|
||||
m -= b
|
||||
}
|
||||
if a < 0 && b > 0 {
|
||||
m += b
|
||||
}
|
||||
return m
|
||||
}
|
||||
258
tracker/gioui/scroll_table.go
Normal file
258
tracker/gioui/scroll_table.go
Normal file
@ -0,0 +1,258 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type ScrollTable struct {
|
||||
ColTitleList *DragList
|
||||
RowTitleList *DragList
|
||||
Table tracker.Table
|
||||
focused bool
|
||||
requestFocus bool
|
||||
tag bool
|
||||
colTag bool
|
||||
rowTag bool
|
||||
cursorMoved bool
|
||||
}
|
||||
|
||||
type ScrollTableStyle struct {
|
||||
RowTitleStyle FilledDragListStyle
|
||||
ColTitleStyle FilledDragListStyle
|
||||
ScrollTable *ScrollTable
|
||||
ScrollBarWidth unit.Dp
|
||||
RowTitleWidth unit.Dp
|
||||
ColumnTitleHeight unit.Dp
|
||||
CellWidth unit.Dp
|
||||
CellHeight unit.Dp
|
||||
element func(gtx C, x, y int) D
|
||||
}
|
||||
|
||||
func NewScrollTable(table tracker.Table, vertList, horizList tracker.List) *ScrollTable {
|
||||
return &ScrollTable{
|
||||
Table: table,
|
||||
ColTitleList: NewDragList(vertList, layout.Horizontal),
|
||||
RowTitleList: NewDragList(horizList, layout.Vertical),
|
||||
}
|
||||
}
|
||||
|
||||
func FilledScrollTable(th *material.Theme, scrollTable *ScrollTable, element func(gtx C, x, y int) D, colTitle, rowTitle, colTitleBg, rowTitleBg func(gtx C, i int) D) ScrollTableStyle {
|
||||
return ScrollTableStyle{
|
||||
RowTitleStyle: FilledDragList(th, scrollTable.RowTitleList, rowTitle, rowTitleBg),
|
||||
ColTitleStyle: FilledDragList(th, scrollTable.ColTitleList, colTitle, colTitleBg),
|
||||
ScrollTable: scrollTable,
|
||||
element: element,
|
||||
ScrollBarWidth: unit.Dp(14),
|
||||
RowTitleWidth: unit.Dp(30),
|
||||
ColumnTitleHeight: unit.Dp(16),
|
||||
CellWidth: unit.Dp(16),
|
||||
CellHeight: unit.Dp(16),
|
||||
}
|
||||
}
|
||||
|
||||
func (st *ScrollTable) CursorMoved() bool {
|
||||
ret := st.cursorMoved
|
||||
st.cursorMoved = false
|
||||
return ret
|
||||
}
|
||||
|
||||
func (st *ScrollTable) Focus() {
|
||||
st.requestFocus = true
|
||||
}
|
||||
|
||||
func (st *ScrollTable) Focused() bool {
|
||||
return st.focused
|
||||
}
|
||||
|
||||
func (st *ScrollTable) EnsureCursorVisible() {
|
||||
st.ColTitleList.EnsureVisible(st.Table.Cursor().X)
|
||||
st.RowTitleList.EnsureVisible(st.Table.Cursor().Y)
|
||||
}
|
||||
|
||||
func (st *ScrollTable) ChildFocused() bool {
|
||||
return st.ColTitleList.Focused() || st.RowTitleList.Focused()
|
||||
}
|
||||
|
||||
func (s ScrollTableStyle) Layout(gtx C) D {
|
||||
p := image.Pt(gtx.Dp(s.RowTitleWidth), gtx.Dp(s.ColumnTitleHeight))
|
||||
|
||||
for _, e := range gtx.Events(&s.ScrollTable.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
s.ScrollTable.focused = e.Focus
|
||||
case pointer.Event:
|
||||
if e.Position.X >= float32(p.X) && e.Position.Y >= float32(p.Y) {
|
||||
if e.Type == pointer.Press {
|
||||
key.FocusOp{Tag: &s.ScrollTable.tag}.Add(gtx.Ops)
|
||||
}
|
||||
dx := (int(e.Position.X) + s.ScrollTable.ColTitleList.List.Position.Offset - p.X) / gtx.Dp(s.CellWidth)
|
||||
dy := (int(e.Position.Y) + s.ScrollTable.RowTitleList.List.Position.Offset - p.Y) / gtx.Dp(s.CellHeight)
|
||||
x := dx + s.ScrollTable.ColTitleList.List.Position.First
|
||||
y := dy + s.ScrollTable.RowTitleList.List.Position.First
|
||||
s.ScrollTable.Table.SetCursor(
|
||||
tracker.Point{X: x, Y: y},
|
||||
)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
s.ScrollTable.Table.SetCursor2(s.ScrollTable.Table.Cursor())
|
||||
}
|
||||
s.ScrollTable.cursorMoved = true
|
||||
}
|
||||
case key.Event:
|
||||
if e.State == key.Press {
|
||||
s.ScrollTable.command(gtx, e)
|
||||
}
|
||||
case clipboard.Event:
|
||||
s.ScrollTable.Table.Paste([]byte(e.Text))
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range gtx.Events(&s.ScrollTable.rowTag) {
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
s.ScrollTable.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range gtx.Events(&s.ScrollTable.colTag) {
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
s.ScrollTable.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
return Surface{Gray: 24, Focus: s.ScrollTable.Focused() || s.ScrollTable.ChildFocused()}.Layout(gtx, func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
pointer.InputOp{
|
||||
Tag: &s.ScrollTable.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
dims := gtx.Constraints.Max
|
||||
s.layoutColTitles(gtx, p)
|
||||
s.layoutRowTitles(gtx, p)
|
||||
defer op.Offset(p).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X-p.X, gtx.Constraints.Max.Y-p.Y))
|
||||
s.layoutTable(gtx, p)
|
||||
s.RowTitleStyle.LayoutScrollBar(gtx)
|
||||
s.ColTitleStyle.LayoutScrollBar(gtx)
|
||||
return D{Size: dims}
|
||||
})
|
||||
}
|
||||
|
||||
func (s ScrollTableStyle) layoutTable(gtx C, p image.Point) {
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop()
|
||||
|
||||
if s.ScrollTable.requestFocus {
|
||||
s.ScrollTable.requestFocus = false
|
||||
key.FocusOp{Tag: &s.ScrollTable.tag}.Add(gtx.Ops)
|
||||
}
|
||||
key.InputOp{Tag: &s.ScrollTable.tag, Keys: "←|→|↑|↓|Shift-←|Shift-→|Shift-↑|Shift-↓|Ctrl-←|Ctrl-→|Ctrl-↑|Ctrl-↓|Ctrl-Shift-←|Ctrl-Shift-→|Ctrl-Shift-↑|Ctrl-Shift-↓|Alt-←|Alt-→|Alt-↑|Alt-↓|Alt-Shift-←|Alt-Shift-→|Alt-Shift-↑|Alt-Shift-↓|⇱|⇲|Shift-⇱|Shift-⇲|⌫|⌦|⇞|⇟|Shift-⇞|Shift-⇟|Ctrl-C|Ctrl-V|Ctrl-X|Shift-,|Shift-."}.Add(gtx.Ops)
|
||||
cellWidth := gtx.Dp(s.CellWidth)
|
||||
cellHeight := gtx.Dp(s.CellHeight)
|
||||
|
||||
gtx.Constraints = layout.Exact(image.Pt(cellWidth, cellHeight))
|
||||
|
||||
colP := s.ColTitleStyle.dragList.List.Position
|
||||
rowP := s.RowTitleStyle.dragList.List.Position
|
||||
defer op.Offset(image.Pt(-colP.Offset, -rowP.Offset)).Push(gtx.Ops).Pop()
|
||||
for x := colP.First; x < colP.First+colP.Count; x++ {
|
||||
offs := op.Offset(image.Point{}).Push(gtx.Ops)
|
||||
for y := rowP.First; y < rowP.First+rowP.Count; y++ {
|
||||
s.element(gtx, x, y)
|
||||
op.Offset(image.Pt(0, cellHeight)).Add(gtx.Ops)
|
||||
}
|
||||
offs.Pop()
|
||||
op.Offset(image.Pt(cellWidth, 0)).Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScrollTableStyle) layoutRowTitles(gtx C, p image.Point) {
|
||||
defer op.Offset(image.Pt(0, p.Y)).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.X = p.X
|
||||
gtx.Constraints.Max.Y -= p.Y
|
||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &s.ScrollTable.rowTag, Keys: "→"}.Add(gtx.Ops)
|
||||
s.RowTitleStyle.Layout(gtx)
|
||||
}
|
||||
|
||||
func (s *ScrollTableStyle) layoutColTitles(gtx C, p image.Point) {
|
||||
defer op.Offset(image.Pt(p.X, 0)).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.Y = p.Y
|
||||
gtx.Constraints.Max.X -= p.X
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &s.ScrollTable.colTag, Keys: "↓"}.Add(gtx.Ops)
|
||||
s.ColTitleStyle.Layout(gtx)
|
||||
}
|
||||
|
||||
func (s *ScrollTable) command(gtx C, e key.Event) {
|
||||
stepX := 1
|
||||
stepY := 1
|
||||
if e.Modifiers.Contain(key.ModAlt) {
|
||||
stepX = intMax(s.ColTitleList.List.Position.Count-3, 8)
|
||||
stepY = intMax(s.RowTitleList.List.Position.Count-3, 8)
|
||||
} else if e.Modifiers.Contain(key.ModCtrl) {
|
||||
stepX = 1e6
|
||||
stepY = 1e6
|
||||
}
|
||||
switch e.Name {
|
||||
case "X", "C":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
contents, ok := s.Table.Copy()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
if e.Name == "X" {
|
||||
s.Table.Clear()
|
||||
}
|
||||
return
|
||||
}
|
||||
case "V":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
clipboard.ReadOp{Tag: &s.tag}.Add(gtx.Ops)
|
||||
}
|
||||
return
|
||||
case key.NameDeleteBackward, key.NameDeleteForward:
|
||||
s.Table.Clear()
|
||||
return
|
||||
case key.NameUpArrow:
|
||||
if !s.Table.MoveCursor(0, -stepY) && stepY == 1 {
|
||||
s.ColTitleList.Focus()
|
||||
}
|
||||
case key.NameDownArrow:
|
||||
s.Table.MoveCursor(0, stepY)
|
||||
case key.NameLeftArrow:
|
||||
if !s.Table.MoveCursor(-stepX, 0) && stepX == 1 {
|
||||
s.RowTitleList.Focus()
|
||||
}
|
||||
case key.NameRightArrow:
|
||||
s.Table.MoveCursor(stepX, 0)
|
||||
case key.NamePageUp:
|
||||
s.Table.MoveCursor(0, -intMax(s.RowTitleList.List.Position.Count-3, 8))
|
||||
case key.NamePageDown:
|
||||
s.Table.MoveCursor(0, intMax(s.RowTitleList.List.Position.Count-3, 8))
|
||||
case key.NameHome:
|
||||
s.Table.SetCursorX(0)
|
||||
case key.NameEnd:
|
||||
s.Table.SetCursorX(s.Table.Width() - 1)
|
||||
case ".":
|
||||
s.Table.Add(1)
|
||||
case ",":
|
||||
s.Table.Add(-1)
|
||||
}
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
s.Table.SetCursor2(s.Table.Cursor())
|
||||
}
|
||||
s.ColTitleList.EnsureVisible(s.Table.Cursor().X)
|
||||
s.RowTitleList.EnsureVisible(s.Table.Cursor().Y)
|
||||
s.cursorMoved = true
|
||||
}
|
||||
@ -2,222 +2,180 @@ package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type SongPanel struct {
|
||||
MenuBar []widget.Clickable
|
||||
Menus []Menu
|
||||
BPM *NumberInput
|
||||
RowsPerPattern *NumberInput
|
||||
RowsPerBeat *NumberInput
|
||||
Step *NumberInput
|
||||
SongLength *NumberInput
|
||||
|
||||
RewindBtn *ActionClickable
|
||||
PlayingBtn *BoolClickable
|
||||
RecordBtn *BoolClickable
|
||||
NoteTracking *BoolClickable
|
||||
PanicBtn *BoolClickable
|
||||
|
||||
// File menu items
|
||||
fileMenuItems []MenuItem
|
||||
NewSong tracker.Action
|
||||
OpenSongFile tracker.Action
|
||||
SaveSongFile tracker.Action
|
||||
SaveSongAsFile tracker.Action
|
||||
ExportWav tracker.Action
|
||||
Quit tracker.Action
|
||||
|
||||
// Edit menu items
|
||||
editMenuItems []MenuItem
|
||||
}
|
||||
|
||||
func NewSongPanel(model *tracker.Model) *SongPanel {
|
||||
ret := &SongPanel{
|
||||
MenuBar: make([]widget.Clickable, 2),
|
||||
Menus: make([]Menu, 2),
|
||||
BPM: NewNumberInput(model.BPM().Int()),
|
||||
RowsPerPattern: NewNumberInput(model.RowsPerPattern().Int()),
|
||||
RowsPerBeat: NewNumberInput(model.RowsPerBeat().Int()),
|
||||
Step: NewNumberInput(model.Step().Int()),
|
||||
SongLength: NewNumberInput(model.SongLength().Int()),
|
||||
PanicBtn: NewBoolClickable(model.Panic().Bool()),
|
||||
RecordBtn: NewBoolClickable(model.IsRecording().Bool()),
|
||||
NoteTracking: NewBoolClickable(model.NoteTracking().Bool()),
|
||||
PlayingBtn: NewBoolClickable(model.Playing().Bool()),
|
||||
RewindBtn: NewActionClickable(model.Rewind()),
|
||||
}
|
||||
ret.fileMenuItems = []MenuItem{
|
||||
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N", Doer: model.NewSong()},
|
||||
{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O", Doer: model.OpenSong()},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S", Doer: model.SaveSong()},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song As...", Doer: model.SaveSongAs()},
|
||||
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav...", Doer: model.Export()},
|
||||
}
|
||||
if canQuit {
|
||||
ret.fileMenuItems = append(ret.fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit", Doer: model.Quit()})
|
||||
}
|
||||
ret.editMenuItems = []MenuItem{
|
||||
{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Doer: model.Undo()},
|
||||
{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: shortcutKey + "Y", Doer: model.Redo()},
|
||||
{IconBytes: icons.ImageCrop, Text: "Remove unused data", Doer: model.RemoveUnused()},
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
const shortcutKey = "Ctrl+"
|
||||
|
||||
var fileMenuItems []MenuItem = []MenuItem{
|
||||
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N"},
|
||||
{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O"},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S"},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song As..."},
|
||||
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav..."},
|
||||
}
|
||||
|
||||
func init() {
|
||||
if CAN_QUIT {
|
||||
fileMenuItems = append(fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit"})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutSongPanel(gtx C) D {
|
||||
func (s *SongPanel) Layout(gtx C, t *Tracker) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(t.layoutMenuBar),
|
||||
layout.Rigid(t.layoutSongOptions),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return s.layoutMenuBar(gtx, t)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return s.layoutSongOptions(gtx, t)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutMenu(title string, clickable *widget.Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget {
|
||||
for clickable.Clicked() {
|
||||
menu.Visible = true
|
||||
}
|
||||
m := t.PopupMenu(menu)
|
||||
return func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
titleBtn := material.Button(t.Theme, clickable, title)
|
||||
titleBtn.Color = white
|
||||
titleBtn.Background = transparent
|
||||
titleBtn.CornerRadius = unit.Dp(0)
|
||||
dims := titleBtn.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.X = gtx.Dp(width)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(1000))
|
||||
m.Layout(gtx, items...)
|
||||
return dims
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutMenuBar(gtx C) D {
|
||||
func (t *SongPanel) layoutMenuBar(gtx C, tr *Tracker) D {
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(36))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
|
||||
|
||||
for clickedItem, hasClicked := t.Menus[0].Clicked(); hasClicked; {
|
||||
switch clickedItem {
|
||||
case 0:
|
||||
t.NewSong(false)
|
||||
case 1:
|
||||
t.OpenSongFile(false)
|
||||
case 2:
|
||||
t.SaveSongFile()
|
||||
case 3:
|
||||
t.SaveSongAsFile()
|
||||
case 4:
|
||||
t.WaveTypeDialog.Visible = true
|
||||
case 5:
|
||||
t.Quit(false)
|
||||
}
|
||||
clickedItem, hasClicked = t.Menus[0].Clicked()
|
||||
}
|
||||
|
||||
for clickedItem, hasClicked := t.Menus[1].Clicked(); hasClicked; {
|
||||
switch clickedItem {
|
||||
case 0:
|
||||
t.Undo()
|
||||
case 1:
|
||||
t.Redo()
|
||||
case 2:
|
||||
if contents, err := yaml.Marshal(t.Song()); err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
case 3:
|
||||
clipboard.ReadOp{Tag: t}.Add(gtx.Ops)
|
||||
case 4:
|
||||
t.RemoveUnusedData()
|
||||
}
|
||||
clickedItem, hasClicked = t.Menus[1].Clicked()
|
||||
}
|
||||
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(t.layoutMenu("File", &t.MenuBar[0], &t.Menus[0], unit.Dp(200),
|
||||
fileMenuItems...,
|
||||
)),
|
||||
layout.Rigid(t.layoutMenu("Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(200),
|
||||
MenuItem{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Disabled: !t.CanUndo()},
|
||||
MenuItem{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: shortcutKey + "Y", Disabled: !t.CanRedo()},
|
||||
MenuItem{IconBytes: icons.ContentContentCopy, Text: "Copy", ShortcutText: shortcutKey + "C"},
|
||||
MenuItem{IconBytes: icons.ContentContentPaste, Text: "Paste", ShortcutText: shortcutKey + "V"},
|
||||
MenuItem{IconBytes: icons.ImageCrop, Text: "Remove unused data"},
|
||||
)),
|
||||
layout.Rigid(tr.layoutMenu("File", &t.MenuBar[0], &t.Menus[0], unit.Dp(200), t.fileMenuItems...)),
|
||||
layout.Rigid(tr.layoutMenu("Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(200), t.editMenuItems...)),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
|
||||
paint.FillShape(gtx.Ops, songSurfaceColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
||||
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
|
||||
var panicBtnStyle material.ButtonStyle
|
||||
if !t.Panic() {
|
||||
panicBtnStyle = LowEmphasisButton(t.Theme, t.PanicBtn, "Panic")
|
||||
} else {
|
||||
panicBtnStyle = HighEmphasisButton(t.Theme, t.PanicBtn, "Panic")
|
||||
}
|
||||
|
||||
for t.PanicBtn.Clicked() {
|
||||
t.SetPanic(!t.Panic())
|
||||
}
|
||||
|
||||
var recordBtnStyle material.ButtonStyle
|
||||
if !t.Recording() {
|
||||
recordBtnStyle = LowEmphasisButton(t.Theme, t.RecordBtn, "Record")
|
||||
} else {
|
||||
recordBtnStyle = HighEmphasisButton(t.Theme, t.RecordBtn, "Record")
|
||||
}
|
||||
|
||||
for t.RecordBtn.Clicked() {
|
||||
t.SetRecording(!t.Recording())
|
||||
}
|
||||
panicBtnStyle := ToggleButton(tr.Theme, t.PanicBtn, "Panic (F12)")
|
||||
rewindBtnStyle := ActionIcon(tr.Theme, t.RewindBtn, icons.AVFastRewind, "Rewind\n(F5)")
|
||||
playBtnStyle := ToggleIcon(tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F6 / Space)", "Stop (F6 / Space)")
|
||||
recordBtnStyle := ToggleIcon(tr.Theme, t.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, "Record (F7)", "Stop (F7)")
|
||||
noteTrackBtnStyle := ToggleIcon(tr.Theme, t.NoteTracking, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, "Follow\nOff\n(F8)", "Follow\nOn\n(F8)")
|
||||
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("LEN:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("LEN:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.SongLength.Value = t.Song().Score.Length
|
||||
numStyle := NumericUpDown(t.Theme, t.SongLength, 1, math.MaxInt32, "Song length")
|
||||
numStyle := NumericUpDown(tr.Theme, t.SongLength, "Song length")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetSongLength(t.SongLength.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("BPM:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("BPM:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.BPM.Value = t.Song().BPM
|
||||
numStyle := NumericUpDown(t.Theme, t.BPM, 1, 999, "Beats per minute")
|
||||
numStyle := NumericUpDown(tr.Theme, t.BPM, "Beats per minute")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetBPM(t.BPM.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("RPP:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("RPP:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.RowsPerPattern.Value = t.Song().Score.RowsPerPattern
|
||||
numStyle := NumericUpDown(t.Theme, t.RowsPerPattern, 1, 255, "Rows per pattern")
|
||||
numStyle := NumericUpDown(tr.Theme, t.RowsPerPattern, "Rows per pattern")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetRowsPerPattern(t.RowsPerPattern.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("RPB:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("RPB:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.RowsPerBeat.Value = t.Song().RowsPerBeat
|
||||
numStyle := NumericUpDown(t.Theme, t.RowsPerBeat, 1, 32, "Rows per beat")
|
||||
numStyle := NumericUpDown(tr.Theme, t.RowsPerBeat, "Rows per beat")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetRowsPerBeat(t.RowsPerBeat.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("STP:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("STP:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
numStyle := NumericUpDown(t.Theme, t.Step, 0, 8, "Cursor step")
|
||||
numStyle := NumericUpDown(tr.Theme, t.Step, "Cursor step")
|
||||
numStyle.UnitsPerStep = unit.Dp(20)
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(VuMeter{AverageVolume: tr.Model.AverageVolume(), PeakVolume: tr.Model.PeakVolume(), Range: 100}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
gtx.Constraints.Min = image.Pt(0, 0)
|
||||
return panicBtnStyle.Layout(gtx)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(rewindBtnStyle.Layout),
|
||||
layout.Rigid(playBtnStyle.Layout),
|
||||
layout.Rigid(recordBtnStyle.Layout),
|
||||
layout.Rigid(noteTrackBtnStyle.Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
gtx.Constraints.Min = image.Pt(0, 0)
|
||||
return recordBtnStyle.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(VuMeter{AverageVolume: t.lastAvgVolume, PeakVolume: t.lastPeakVolume, Range: 100}.Layout),
|
||||
layout.Rigid(panicBtnStyle.Layout),
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"image/color"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
@ -39,10 +40,13 @@ func (s Surface) Layout(gtx C, widget layout.Widget) D {
|
||||
return s.Inset.Layout(gtx, widget)
|
||||
}
|
||||
if s.FitSize {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Expanded(bg),
|
||||
layout.Stacked(fg),
|
||||
)
|
||||
macro := op.Record(gtx.Ops)
|
||||
dims := fg(gtx)
|
||||
call := macro.Stop()
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
bg(gtx)
|
||||
call.Add(gtx.Ops)
|
||||
return dims
|
||||
}
|
||||
gtxbg := gtx
|
||||
gtxbg.Constraints.Min = gtxbg.Constraints.Max
|
||||
|
||||
@ -63,7 +63,7 @@ var inactiveLightSurfaceColor = color.NRGBA{R: 37, G: 37, B: 38, A: 255}
|
||||
var activeLightSurfaceColor = color.NRGBA{R: 45, G: 45, B: 45, A: 255}
|
||||
|
||||
var cursorColor = color.NRGBA{R: 100, G: 140, B: 255, A: 48}
|
||||
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 8}
|
||||
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 12}
|
||||
var inactiveSelectionColor = color.NRGBA{R: 140, G: 140, B: 140, A: 16}
|
||||
|
||||
var errorColor = color.NRGBA{R: 207, G: 102, B: 121, A: 255}
|
||||
|
||||
@ -1,500 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"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"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
const trackRowHeight = 16
|
||||
const trackColWidth = 54
|
||||
const patmarkWidth = 16
|
||||
|
||||
type TrackEditor struct {
|
||||
TrackVoices *NumberInput
|
||||
NewTrackBtn *TipClickable
|
||||
DeleteTrackBtn *TipClickable
|
||||
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(TipClickable),
|
||||
DeleteTrackBtn: new(TipClickable),
|
||||
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 || te.ChildFocused()
|
||||
}
|
||||
|
||||
func (te *TrackEditor) ChildFocused() bool {
|
||||
return te.AddOctaveBtn.Focused() || te.AddSemitoneBtn.Focused() || te.DeleteTrackBtn.Clickable.Focused() || te.NewTrackBtn.Clickable.Focused() || te.NoteOffBtn.Focused() || te.SubtractOctaveBtn.Focused() || te.SubtractSemitoneBtn.Focused() || te.SubtractSemitoneBtn.Focused() || te.SubtractSemitoneBtn.Focused()
|
||||
}
|
||||
|
||||
var trackerEditorKeys key.Set = "+|-|←|→|↑|↓|Ctrl-←|Ctrl-→|Ctrl-↑|Ctrl-↓|Shift-←|Shift-→|Shift-↑|Shift-↓|⏎|⇱|⇲|⌫|⌦|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|0|1|2|3|4|5|6|7|8|9|,|."
|
||||
|
||||
func (te *TrackEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions {
|
||||
for _, e := range gtx.Events(te) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
te.focused = e.Focus
|
||||
case pointer.Event:
|
||||
if e.Type == pointer.Press {
|
||||
key.FocusOp{Tag: te}.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.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 := noteAsValue(t.OctaveNumberInput.Value, val)
|
||||
t.SetNote(n)
|
||||
step = true
|
||||
trk := t.Cursor().Track
|
||||
noteID := tracker.NoteIDTrack(trk, n)
|
||||
t.NoteOn(noteID)
|
||||
t.KeyPlaying[e.Name] = noteID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if step && !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
case key.Release:
|
||||
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
t.NoteOff(noteID)
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if te.requestFocus || te.ChildFocused() {
|
||||
te.requestFocus = false
|
||||
key.FocusOp{Tag: te}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
rowMarkers := layout.Rigid(t.layoutRowMarkers)
|
||||
|
||||
for te.NewTrackBtn.Clickable.Clicked() {
|
||||
t.AddTrack(true)
|
||||
}
|
||||
|
||||
for te.DeleteTrackBtn.Clickable.Clicked() {
|
||||
t.DeleteTrack(false)
|
||||
}
|
||||
|
||||
//t.TrackHexCheckBoxes[i2].Value = t.TrackShowHex[i2]
|
||||
//cbStyle := material.CheckBox(t.Theme, t.TrackHexCheckBoxes[i2], "hex")
|
||||
//cbStyle.Color = white
|
||||
//cbStyle.IconColor = t.Theme.Fg
|
||||
|
||||
for te.AddSemitoneBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(1)
|
||||
}
|
||||
|
||||
for te.SubtractSemitoneBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(-1)
|
||||
}
|
||||
|
||||
for te.NoteOffBtn.Clicked() {
|
||||
t.SetNote(0)
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
|
||||
for te.AddOctaveBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(12)
|
||||
}
|
||||
|
||||
for te.SubtractOctaveBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(-12)
|
||||
}
|
||||
|
||||
menu := func(gtx C) D {
|
||||
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(), "Delete track")
|
||||
newTrackBtnStyle := IconButton(t.Theme, te.NewTrackBtn, icons.ContentAdd, t.CanAddTrack(), "Add track")
|
||||
n := t.Song().Score.Tracks[t.Cursor().Track].NumVoices
|
||||
te.TrackVoices.Value = n
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
voiceUpDown := func(gtx C) D {
|
||||
numStyle := NumericUpDown(t.Theme, te.TrackVoices, 1, t.MaxTrackVoices(), "Number of voices for this track")
|
||||
return in.Layout(gtx, numStyle.Layout)
|
||||
}
|
||||
t.TrackHexCheckBox.Value = t.Song().Score.Tracks[t.Cursor().Track].Effect
|
||||
hexCheckBoxStyle := material.CheckBox(t.Theme, t.TrackHexCheckBox, "Hex")
|
||||
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
||||
layout.Rigid(addSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(subtractSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(addOctaveBtnStyle.Layout),
|
||||
layout.Rigid(subtractOctaveBtnStyle.Layout),
|
||||
layout.Rigid(noteOffBtnStyle.Layout),
|
||||
layout.Rigid(hexCheckBoxStyle.Layout),
|
||||
layout.Rigid(Label(" Voices:", white, t.TextShaper)),
|
||||
layout.Rigid(voiceUpDown),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
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(te.TrackVoices.Value)
|
||||
return dims
|
||||
}
|
||||
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: te,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
key.InputOp{Tag: te, Keys: trackerEditorKeys}.Add(gtx.Ops)
|
||||
|
||||
dims := 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)
|
||||
}))
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
area.Pop()
|
||||
return dims
|
||||
}
|
||||
|
||||
const baseNote = 24
|
||||
|
||||
var notes = []string{
|
||||
"C-",
|
||||
"C#",
|
||||
"D-",
|
||||
"D#",
|
||||
"E-",
|
||||
"F-",
|
||||
"F#",
|
||||
"G-",
|
||||
"G#",
|
||||
"A-",
|
||||
"A#",
|
||||
"B-",
|
||||
}
|
||||
|
||||
func noteStr(val byte) string {
|
||||
if val == 1 {
|
||||
return "..." // hold
|
||||
}
|
||||
if val == 0 {
|
||||
return "---" // release
|
||||
}
|
||||
oNote := mod(int(val-baseNote), 12)
|
||||
octave := (int(val) - oNote - baseNote) / 12
|
||||
if octave < 0 {
|
||||
return fmt.Sprintf("%s%s", notes[oNote], string(byte('Z'+1+octave)))
|
||||
}
|
||||
if octave >= 10 {
|
||||
return fmt.Sprintf("%s%s", notes[oNote], string(byte('A'+octave-10)))
|
||||
}
|
||||
return fmt.Sprintf("%s%d", notes[oNote], octave)
|
||||
}
|
||||
|
||||
func noteAsValue(octave, note int) byte {
|
||||
return byte(baseNote + (octave * 12) + note)
|
||||
}
|
||||
|
||||
func (te *TrackEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
||||
cursorSongRow := t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row
|
||||
for _, ev := range gtx.Events(&te.trackJumpPointerTag) {
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if e.Type == pointer.Press {
|
||||
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.ScorePoint{Track: track, ScoreRow: tracker.ScoreRow{Row: row}}.Clamp(t.Song().Score)
|
||||
t.SetCursor(cursor)
|
||||
t.SetSelectionCorner(cursor)
|
||||
cursorSongRow = cursor.Pattern*t.Song().Score.RowsPerPattern + cursor.Row
|
||||
}
|
||||
}
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &te.trackJumpPointerTag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
stack := op.Offset(image.Point{}).Push(gtx.Ops)
|
||||
curVoice := 0
|
||||
for _, trk := range t.Song().Score.Tracks {
|
||||
gtx := gtx
|
||||
instrName := "?"
|
||||
firstIndex, err := t.Song().Patch.InstrumentForVoice(curVoice)
|
||||
lastIndex, err2 := t.Song().Patch.InstrumentForVoice(curVoice + trk.NumVoices - 1)
|
||||
if err == nil && err2 == nil {
|
||||
switch diff := lastIndex - firstIndex; diff {
|
||||
case 0:
|
||||
instrName = t.Song().Patch[firstIndex].Name
|
||||
default:
|
||||
n1 := t.Song().Patch[firstIndex].Name
|
||||
n2 := t.Song().Patch[firstIndex+1].Name
|
||||
if len(n1) > 0 {
|
||||
n1 = string(n1[0])
|
||||
} else {
|
||||
n1 = "?"
|
||||
}
|
||||
if len(n2) > 0 {
|
||||
n2 = string(n2[0])
|
||||
} else {
|
||||
n2 = "?"
|
||||
}
|
||||
if diff > 1 {
|
||||
instrName = n1 + "/" + n2 + "..."
|
||||
} else {
|
||||
instrName = n1 + "/" + n2
|
||||
}
|
||||
}
|
||||
if len(instrName) > 7 {
|
||||
instrName = instrName[:7]
|
||||
}
|
||||
}
|
||||
gtx.Constraints.Max.X = trackColWidth
|
||||
LabelStyle{Alignment: layout.N, Text: instrName, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.TextShaper}.Layout(gtx)
|
||||
op.Offset(image.Point{trackColWidth, 0}).Add(gtx.Ops)
|
||||
curVoice += trk.NumVoices
|
||||
}
|
||||
stack.Pop()
|
||||
op.Offset(image.Point{0, (gtx.Constraints.Max.Y - trackRowHeight) / 2}).Add(gtx.Ops)
|
||||
op.Offset(image.Point{0, int((-1 * trackRowHeight) * (cursorSongRow))}).Add(gtx.Ops)
|
||||
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 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
if y1 > y2 {
|
||||
y1, y2 = y2, y1
|
||||
}
|
||||
x2++
|
||||
y2++
|
||||
x1 *= trackColWidth
|
||||
y1 *= trackRowHeight * t.Song().Score.RowsPerPattern
|
||||
x2 *= trackColWidth
|
||||
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 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 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
if y1 > y2 {
|
||||
y1, y2 = y2, y1
|
||||
}
|
||||
x2++
|
||||
y2++
|
||||
x1 *= trackColWidth
|
||||
y1 *= trackRowHeight
|
||||
x2 *= trackColWidth
|
||||
y2 *= trackRowHeight
|
||||
paint.FillShape(gtx.Ops, selectionColor, clip.Rect{Min: image.Pt(x1, y1), Max: image.Pt(x2, y2)}.Op())
|
||||
cx := t.Cursor().Track * trackColWidth
|
||||
cy := (t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row) * trackRowHeight
|
||||
cw := trackColWidth
|
||||
if t.Song().Score.Tracks[t.Cursor().Track].Effect {
|
||||
cw /= 2
|
||||
if t.LowNibble() {
|
||||
cx += cw
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{Min: image.Pt(cx, cy), Max: image.Pt(cx+cw, cy+trackRowHeight)}.Op())
|
||||
}
|
||||
delta := (gtx.Constraints.Max.Y/2 + trackRowHeight - 1) / trackRowHeight
|
||||
firstRow := cursorSongRow - delta
|
||||
lastRow := cursorSongRow + delta
|
||||
if firstRow < 0 {
|
||||
firstRow = 0
|
||||
}
|
||||
if l := t.Song().Score.LengthInRows(); lastRow >= l {
|
||||
lastRow = l - 1
|
||||
}
|
||||
op.Offset(image.Point{0, trackRowHeight * firstRow}).Add(gtx.Ops)
|
||||
for trkIndex, trk := range t.Song().Score.Tracks {
|
||||
stack := op.Offset(image.Point{}).Push(gtx.Ops)
|
||||
for row := firstRow; row <= lastRow; row++ {
|
||||
pat := row / t.Song().Score.RowsPerPattern
|
||||
patRow := row % t.Song().Score.RowsPerPattern
|
||||
s := trk.Order.Get(pat)
|
||||
if s < 0 {
|
||||
op.Offset(image.Point{0, trackRowHeight}).Add(gtx.Ops)
|
||||
continue
|
||||
}
|
||||
if s >= 0 && patRow == 0 {
|
||||
paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, patternIndexToString(s), op.CallOp{})
|
||||
}
|
||||
if s >= 0 && patRow == 1 && t.IsPatternUnique(trkIndex, s) {
|
||||
paint.ColorOp{Color: mediumEmphasisTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, "*", op.CallOp{})
|
||||
}
|
||||
op.Offset(image.Point{patmarkWidth, 0}).Add(gtx.Ops)
|
||||
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)
|
||||
}
|
||||
var c byte = 1
|
||||
if s >= 0 && s < len(trk.Patterns) {
|
||||
c = trk.Patterns[s].Get(patRow)
|
||||
}
|
||||
if trk.Effect {
|
||||
var text string
|
||||
switch c {
|
||||
case 0:
|
||||
text = "--"
|
||||
case 1:
|
||||
text = ".."
|
||||
default:
|
||||
text = fmt.Sprintf("%02x", c)
|
||||
}
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(text), op.CallOp{})
|
||||
} else {
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, noteStr(c), op.CallOp{})
|
||||
}
|
||||
op.Offset(image.Point{-patmarkWidth, trackRowHeight}).Add(gtx.Ops)
|
||||
}
|
||||
stack.Pop()
|
||||
op.Offset(image.Point{trackColWidth, 0}).Add(gtx.Ops)
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
@ -1,24 +1,63 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/explorer"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var canQuit = true // set to false in init() if plugin tag is enabled
|
||||
|
||||
type (
|
||||
Tracker struct {
|
||||
Theme *material.Theme
|
||||
OctaveNumberInput *NumberInput
|
||||
InstrumentVoices *NumberInput
|
||||
TopHorizontalSplit *Split
|
||||
BottomHorizontalSplit *Split
|
||||
VerticalSplit *Split
|
||||
KeyPlaying map[string]tracker.NoteID
|
||||
PopupAlert *PopupAlert
|
||||
|
||||
SaveChangesDialog *Dialog
|
||||
WaveTypeDialog *Dialog
|
||||
|
||||
ModalDialog layout.Widget
|
||||
InstrumentEditor *InstrumentEditor
|
||||
OrderEditor *OrderEditor
|
||||
TrackEditor *NoteEditor
|
||||
Explorer *explorer.Explorer
|
||||
Exploring bool
|
||||
SongPanel *SongPanel
|
||||
|
||||
filePathString tracker.String
|
||||
|
||||
quitWG sync.WaitGroup
|
||||
execChan chan func()
|
||||
|
||||
*tracker.Model
|
||||
}
|
||||
|
||||
C = layout.Context
|
||||
D = layout.Dimensions
|
||||
)
|
||||
|
||||
const (
|
||||
@ -27,140 +66,34 @@ const (
|
||||
ConfirmNew
|
||||
)
|
||||
|
||||
type Tracker struct {
|
||||
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
|
||||
RecordBtn *widget.Clickable
|
||||
AddUnitBtn *widget.Clickable
|
||||
TrackHexCheckBox *widget.Bool
|
||||
TopHorizontalSplit *Split
|
||||
BottomHorizontalSplit *Split
|
||||
VerticalSplit *Split
|
||||
KeyPlaying map[string]tracker.NoteID
|
||||
Alert Alert
|
||||
ConfirmSongDialog *Dialog
|
||||
WaveTypeDialog *Dialog
|
||||
ConfirmSongActionType int
|
||||
ModalDialog layout.Widget
|
||||
InstrumentEditor *InstrumentEditor
|
||||
OrderEditor *OrderEditor
|
||||
TrackEditor *TrackEditor
|
||||
Explorer *explorer.Explorer
|
||||
|
||||
TextShaper *text.Shaper
|
||||
|
||||
lastAvgVolume tracker.Volume
|
||||
lastPeakVolume tracker.Volume
|
||||
|
||||
wavFilePath string
|
||||
quitChannel chan struct{}
|
||||
quitWG sync.WaitGroup
|
||||
errorChannel chan error
|
||||
quitted bool
|
||||
unmarshalRecoveryChannel chan []byte
|
||||
marshalRecoveryChannel chan (chan []byte)
|
||||
synther sointu.Synther
|
||||
|
||||
*trackerModel
|
||||
}
|
||||
|
||||
type trackerModel = tracker.Model
|
||||
|
||||
func (t *Tracker) UnmarshalContent(bytes []byte) error {
|
||||
var units []sointu.Unit
|
||||
if errJSON := json.Unmarshal(bytes, &units); errJSON == nil {
|
||||
if len(units) == 0 {
|
||||
return nil
|
||||
}
|
||||
t.PasteUnits(units)
|
||||
// TODO: this is a bit hacky, but works for now. How to change the selection to the pasted units more elegantly?
|
||||
t.InstrumentEditor.unitDragList.SelectedItem = t.UnitIndex()
|
||||
t.InstrumentEditor.unitDragList.SelectedItem2 = t.UnitIndex() + len(units) - 1
|
||||
return nil
|
||||
}
|
||||
if errYaml := yaml.Unmarshal(bytes, &units); errYaml == nil {
|
||||
if len(units) == 0 {
|
||||
return nil
|
||||
}
|
||||
t.PasteUnits(units)
|
||||
t.InstrumentEditor.unitDragList.SelectedItem = t.UnitIndex()
|
||||
t.InstrumentEditor.unitDragList.SelectedItem2 = t.UnitIndex() + len(units) - 1
|
||||
return nil
|
||||
}
|
||||
var instr sointu.Instrument
|
||||
if errJSON := json.Unmarshal(bytes, &instr); errJSON == nil {
|
||||
if t.SetInstrument(instr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if errYaml := yaml.Unmarshal(bytes, &instr); errYaml == nil {
|
||||
if t.SetInstrument(instr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var song sointu.Song
|
||||
if errJSON := json.Unmarshal(bytes, &song); errJSON != nil {
|
||||
if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil {
|
||||
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
|
||||
}
|
||||
}
|
||||
if song.BPM > 0 {
|
||||
t.SetSong(song)
|
||||
return nil
|
||||
}
|
||||
return errors.New("was able to unmarshal a song, but the bpm was 0")
|
||||
}
|
||||
|
||||
func NewTracker(model *tracker.Model, synther sointu.Synther) *Tracker {
|
||||
func NewTracker(model *tracker.Model) *Tracker {
|
||||
t := &Tracker{
|
||||
Theme: material.NewTheme(),
|
||||
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),
|
||||
RecordBtn: new(widget.Clickable),
|
||||
TrackHexCheckBox: new(widget.Bool),
|
||||
Menus: make([]Menu, 2),
|
||||
MenuBar: make([]widget.Clickable, 2),
|
||||
quitChannel: make(chan struct{}, 1), // use non-blocking sends; no need to queue extra ticks if one is queued already
|
||||
OctaveNumberInput: NewNumberInput(model.Octave().Int()),
|
||||
InstrumentVoices: NewNumberInput(model.InstrumentVoices().Int()),
|
||||
|
||||
TopHorizontalSplit: &Split{Ratio: -.6},
|
||||
BottomHorizontalSplit: &Split{Ratio: -.6},
|
||||
VerticalSplit: &Split{Axis: layout.Vertical},
|
||||
|
||||
KeyPlaying: make(map[string]tracker.NoteID),
|
||||
ConfirmSongDialog: new(Dialog),
|
||||
WaveTypeDialog: new(Dialog),
|
||||
InstrumentEditor: NewInstrumentEditor(),
|
||||
OrderEditor: NewOrderEditor(),
|
||||
TrackEditor: NewTrackEditor(),
|
||||
SaveChangesDialog: NewDialog(model.SaveSong(), model.DiscardSong(), model.Cancel()),
|
||||
WaveTypeDialog: NewDialog(model.ExportInt16(), model.ExportFloat(), model.Cancel()),
|
||||
InstrumentEditor: NewInstrumentEditor(model),
|
||||
OrderEditor: NewOrderEditor(model),
|
||||
TrackEditor: NewNoteEditor(model),
|
||||
SongPanel: NewSongPanel(model),
|
||||
|
||||
errorChannel: make(chan error, 32),
|
||||
synther: synther,
|
||||
trackerModel: model,
|
||||
Model: model,
|
||||
|
||||
marshalRecoveryChannel: make(chan (chan []byte)),
|
||||
unmarshalRecoveryChannel: make(chan []byte),
|
||||
filePathString: model.FilePath().String(),
|
||||
execChan: make(chan func(), 1024),
|
||||
}
|
||||
t.TextShaper = text.NewShaper(text.WithCollection(fontCollection))
|
||||
t.Alert.shaper = t.TextShaper
|
||||
t.Theme.Shaper = text.NewShaper(text.WithCollection(fontCollection))
|
||||
t.PopupAlert = NewPopupAlert(model.Alerts(), t.Theme.Shaper)
|
||||
t.Theme.Palette.Fg = primaryColor
|
||||
t.Theme.Palette.ContrastFg = black
|
||||
t.TrackEditor.Focus()
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
t.quitWG.Add(1)
|
||||
return t
|
||||
}
|
||||
@ -171,19 +104,13 @@ func (t *Tracker) Main() {
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
app.Title("Sointu Tracker"),
|
||||
)
|
||||
t.InstrumentEditor.Focus()
|
||||
recoveryTicker := time.NewTicker(time.Second * 30)
|
||||
t.Explorer = explorer.NewExplorer(w)
|
||||
var ops op.Ops
|
||||
mainloop:
|
||||
for {
|
||||
if pos, playing := t.PlayPosition(), t.Playing(); t.NoteTracking() && playing {
|
||||
cursor := t.Cursor()
|
||||
cursor.ScoreRow = pos
|
||||
t.SetCursor(cursor)
|
||||
t.SetSelectionCorner(cursor)
|
||||
}
|
||||
if titleFooter != t.FilePath() {
|
||||
titleFooter = t.FilePath()
|
||||
if titleFooter != t.filePathString.Value() {
|
||||
titleFooter = t.filePathString.Value()
|
||||
if titleFooter != "" {
|
||||
w.Option(app.Title(fmt.Sprintf("Sointu Tracker - %v", titleFooter)))
|
||||
} else {
|
||||
@ -191,28 +118,16 @@ mainloop:
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-t.quitChannel:
|
||||
recoveryTicker.Stop()
|
||||
break mainloop
|
||||
case e := <-t.errorChannel:
|
||||
t.Alert.Update(e.Error(), Error, time.Second*5)
|
||||
w.Invalidate()
|
||||
case e := <-t.PlayerMessages:
|
||||
if err, ok := e.Inner.(tracker.PlayerCrashMessage); ok {
|
||||
t.Alert.Update(err.Error(), Error, time.Second*3)
|
||||
}
|
||||
if err, ok := e.Inner.(tracker.PlayerVolumeErrorMessage); ok {
|
||||
t.Alert.Update(err.Error(), Warning, time.Second*3)
|
||||
}
|
||||
t.lastAvgVolume = e.AverageVolume
|
||||
t.lastPeakVolume = e.PeakVolume
|
||||
t.InstrumentEditor.voiceLevels = e.VoiceLevels
|
||||
t.ProcessPlayerMessage(e)
|
||||
w.Invalidate()
|
||||
case e := <-w.Events():
|
||||
switch e := e.(type) {
|
||||
case system.DestroyEvent:
|
||||
if !t.Quit(false) {
|
||||
if canQuit {
|
||||
t.Quit().Do()
|
||||
}
|
||||
if !t.Quitted() {
|
||||
// TODO: uh oh, there's no way of canceling the destroyevent in gioui? so we create a new window just to show the dialog
|
||||
w = app.NewWindow(
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
@ -222,41 +137,146 @@ mainloop:
|
||||
}
|
||||
case system.FrameEvent:
|
||||
gtx := layout.NewContext(&ops, e)
|
||||
if t.SongPanel.PlayingBtn.Bool.Value() && t.SongPanel.NoteTracking.Bool.Value() {
|
||||
t.TrackEditor.scrollTable.RowTitleList.CenterOn(t.PlaySongRow())
|
||||
}
|
||||
t.Layout(gtx, w)
|
||||
e.Frame(gtx.Ops)
|
||||
}
|
||||
case <-recoveryTicker.C:
|
||||
t.SaveRecovery()
|
||||
case retChn := <-t.marshalRecoveryChannel:
|
||||
retChn <- t.MarshalRecovery()
|
||||
case bytes := <-t.unmarshalRecoveryChannel:
|
||||
t.UnmarshalRecovery(bytes)
|
||||
case f := <-t.execChan:
|
||||
f()
|
||||
}
|
||||
if t.Quitted() {
|
||||
break
|
||||
}
|
||||
}
|
||||
recoveryTicker.Stop()
|
||||
w.Perform(system.ActionClose)
|
||||
t.SaveRecovery()
|
||||
t.quitWG.Done()
|
||||
}
|
||||
|
||||
// thread safe, executed in the GUI thread
|
||||
func (t *Tracker) SafeMarshalRecovery() []byte {
|
||||
retChn := make(chan []byte)
|
||||
t.marshalRecoveryChannel <- retChn
|
||||
return <-retChn
|
||||
}
|
||||
|
||||
// thread safe, executed in the GUI thread
|
||||
func (t *Tracker) SafeUnmarshalRecovery(data []byte) {
|
||||
t.unmarshalRecoveryChannel <- data
|
||||
}
|
||||
|
||||
func (t *Tracker) sendQuit() {
|
||||
select {
|
||||
case t.quitChannel <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
func (t *Tracker) Exec() chan<- func() {
|
||||
return t.execChan
|
||||
}
|
||||
|
||||
func (t *Tracker) WaitQuitted() {
|
||||
t.quitWG.Wait()
|
||||
}
|
||||
|
||||
func (t *Tracker) Layout(gtx layout.Context, w *app.Window) {
|
||||
// this is the top level input handler for the whole app
|
||||
// it handles all the global key events and clipboard events
|
||||
// we need to tell gio that we handle tabs too; otherwise
|
||||
// it will steal them for focus switching
|
||||
key.InputOp{Tag: t, Keys: "Tab|Shift-Tab"}.Add(gtx.Ops)
|
||||
for _, ev := range gtx.Events(t) {
|
||||
switch e := ev.(type) {
|
||||
case key.Event:
|
||||
t.KeyEvent(e, gtx.Ops)
|
||||
case clipboard.Event:
|
||||
stringReader := strings.NewReader(e.Text)
|
||||
stringReadCloser := io.NopCloser(stringReader)
|
||||
t.ReadSong(stringReadCloser)
|
||||
}
|
||||
}
|
||||
|
||||
paint.FillShape(gtx.Ops, backgroundColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
||||
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
||||
t.layoutTop(gtx)
|
||||
} else {
|
||||
t.VerticalSplit.Layout(gtx,
|
||||
t.layoutTop,
|
||||
t.layoutBottom)
|
||||
}
|
||||
t.PopupAlert.Layout(gtx)
|
||||
t.showDialog(gtx)
|
||||
}
|
||||
|
||||
func (t *Tracker) showDialog(gtx C) {
|
||||
if t.Exploring {
|
||||
return
|
||||
}
|
||||
switch t.Dialog() {
|
||||
case tracker.NewSongChanges, tracker.OpenSongChanges, tracker.QuitChanges:
|
||||
dstyle := ConfirmDialog(t.Theme, t.SaveChangesDialog, "Save changes to song?", "Your changes will be lost if you don't save them.")
|
||||
dstyle.OkStyle.Text = "Save"
|
||||
dstyle.AltStyle.Text = "Don't save"
|
||||
dstyle.Layout(gtx)
|
||||
case tracker.Export:
|
||||
dstyle := ConfirmDialog(t.Theme, t.WaveTypeDialog, "", "Export .wav in int16 or float32 sample format?")
|
||||
dstyle.OkStyle.Text = "Int16"
|
||||
dstyle.AltStyle.Text = "Float32"
|
||||
dstyle.Layout(gtx)
|
||||
case tracker.OpenSongOpenExplorer:
|
||||
t.explorerChooseFile(t.ReadSong, ".yml", ".json")
|
||||
case tracker.NewSongSaveExplorer, tracker.OpenSongSaveExplorer, tracker.QuitSaveExplorer, tracker.SaveAsExplorer:
|
||||
filename := t.filePathString.Value()
|
||||
if filename == "" {
|
||||
filename = "song.yml"
|
||||
}
|
||||
t.explorerCreateFile(t.WriteSong, filename)
|
||||
case tracker.ExportFloatExplorer, tracker.ExportInt16Explorer:
|
||||
filename := "song.wav"
|
||||
if p := t.filePathString.Value(); p != "" {
|
||||
filename = p[:len(p)-len(filepath.Ext(p))] + ".wav"
|
||||
}
|
||||
t.explorerCreateFile(func(wc io.WriteCloser) {
|
||||
t.WriteWav(wc, t.Dialog() == tracker.ExportInt16Explorer, t.execChan)
|
||||
}, filename)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) explorerChooseFile(success func(io.ReadCloser), extensions ...string) {
|
||||
t.Exploring = true
|
||||
go func() {
|
||||
file, err := t.Explorer.ChooseFile(extensions...)
|
||||
t.Exec() <- func() {
|
||||
t.Exploring = false
|
||||
if err == nil {
|
||||
success(file)
|
||||
} else {
|
||||
t.Cancel().Do()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *Tracker) explorerCreateFile(success func(io.WriteCloser), filename string) {
|
||||
t.Exploring = true
|
||||
go func() {
|
||||
file, err := t.Explorer.CreateFile(filename)
|
||||
t.Exec() <- func() {
|
||||
t.Exploring = false
|
||||
if err == nil {
|
||||
success(file)
|
||||
} else {
|
||||
t.Cancel().Do()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions {
|
||||
return t.BottomHorizontalSplit.Layout(gtx,
|
||||
func(gtx C) D {
|
||||
return t.OrderEditor.Layout(gtx, t)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return t.TrackEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions {
|
||||
return t.TopHorizontalSplit.Layout(gtx,
|
||||
func(gtx C) D {
|
||||
return t.SongPanel.Layout(gtx, t)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return t.InstrumentEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
//go:build !plugin
|
||||
|
||||
package gioui
|
||||
|
||||
const CAN_QUIT = true
|
||||
|
||||
func (t *Tracker) Quit(forced bool) bool {
|
||||
if !forced && t.ChangedSinceSave() {
|
||||
t.ConfirmSongActionType = ConfirmQuit
|
||||
t.ConfirmSongDialog.Visible = true
|
||||
return false
|
||||
}
|
||||
t.sendQuit()
|
||||
return true
|
||||
}
|
||||
@ -2,11 +2,6 @@
|
||||
|
||||
package gioui
|
||||
|
||||
const CAN_QUIT = false
|
||||
|
||||
func (t *Tracker) Quit(forced bool) bool {
|
||||
if forced {
|
||||
t.sendQuit()
|
||||
}
|
||||
return forced
|
||||
func init() {
|
||||
canQuit = false
|
||||
}
|
||||
|
||||
321
tracker/gioui/unit_editor.go
Normal file
321
tracker/gioui/unit_editor.go
Normal file
@ -0,0 +1,321 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type UnitEditor struct {
|
||||
sliderList *DragList
|
||||
searchList *DragList
|
||||
Parameters []*ParameterWidget
|
||||
DeleteUnitBtn *ActionClickable
|
||||
CopyUnitBtn *TipClickable
|
||||
ClearUnitBtn *ActionClickable
|
||||
SelectTypeBtn *widget.Clickable
|
||||
tag bool
|
||||
caser cases.Caser
|
||||
}
|
||||
|
||||
func NewUnitEditor(m *tracker.Model) *UnitEditor {
|
||||
ret := &UnitEditor{
|
||||
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()),
|
||||
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
|
||||
CopyUnitBtn: new(TipClickable),
|
||||
SelectTypeBtn: new(widget.Clickable),
|
||||
sliderList: NewDragList(m.Params().List(), layout.Vertical),
|
||||
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
|
||||
}
|
||||
ret.caser = cases.Title(language.English)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) Layout(gtx C, t *Tracker) D {
|
||||
for _, e := range gtx.Events(&pe.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.Event:
|
||||
if e.State == key.Press {
|
||||
pe.command(e, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &pe.tag, Keys: "←|Shift-←|→|Shift-→|⎋"}.Add(gtx.Ops)
|
||||
|
||||
editorFunc := pe.layoutSliders
|
||||
str := tracker.String{StringData: (*tracker.UnitSearch)(t.Model)}
|
||||
if str.Value() != t.Model.Units().SelectedType() || pe.sliderList.TrackerList.Count() == 0 {
|
||||
editorFunc = pe.layoutUnitTypeChooser
|
||||
}
|
||||
return Surface{Gray: 24, Focus: t.InstrumentEditor.wasFocused}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return editorFunc(gtx, t)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return pe.layoutFooter(gtx, t)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) layoutSliders(gtx C, t *Tracker) D {
|
||||
numItems := pe.sliderList.TrackerList.Count()
|
||||
|
||||
for len(pe.Parameters) < numItems {
|
||||
pe.Parameters = append(pe.Parameters, new(ParameterWidget))
|
||||
}
|
||||
|
||||
index := 0
|
||||
t.Model.Params().Iterate(func(param tracker.Parameter) {
|
||||
pe.Parameters[index].Parameter = param
|
||||
index++
|
||||
})
|
||||
|
||||
element := func(gtx C, index int) D {
|
||||
if index < 0 || index >= numItems {
|
||||
return D{}
|
||||
}
|
||||
paramStyle := t.ParamStyle(t.Theme, pe.Parameters[index])
|
||||
paramStyle.Focus = pe.sliderList.TrackerList.Selected() == index
|
||||
dims := paramStyle.Layout(gtx)
|
||||
return D{Size: image.Pt(gtx.Constraints.Max.X, dims.Size.Y)}
|
||||
}
|
||||
|
||||
fdl := FilledDragList(t.Theme, pe.sliderList, element, nil)
|
||||
dims := fdl.Layout(gtx)
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
fdl.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
|
||||
for pe.CopyUnitBtn.Clickable.Clicked() {
|
||||
if contents, ok := t.Units().List().CopyElements(); ok {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alerts().Add("Unit copied to clipboard", tracker.Info)
|
||||
}
|
||||
}
|
||||
copyUnitBtnStyle := TipIcon(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, "Copy unit (Ctrl+C)")
|
||||
deleteUnitBtnStyle := ActionIcon(t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)")
|
||||
text := t.Units().SelectedType()
|
||||
if text == "" {
|
||||
text = "Choose unit type"
|
||||
} else {
|
||||
text = pe.caser.String(text)
|
||||
}
|
||||
hintText := Label(text, white, t.Theme.Shaper)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(deleteUnitBtnStyle.Layout),
|
||||
layout.Rigid(copyUnitBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
var dims D
|
||||
if t.Units().SelectedType() != "" {
|
||||
clearUnitBtnStyle := ActionIcon(t.Theme, pe.ClearUnitBtn, icons.ContentClear, "Clear unit")
|
||||
dims = clearUnitBtnStyle.Layout(gtx)
|
||||
}
|
||||
return D{Size: image.Pt(gtx.Dp(unit.Dp(48)), dims.Size.Y)}
|
||||
}),
|
||||
layout.Flexed(1, hintText),
|
||||
)
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) layoutUnitTypeChooser(gtx C, t *Tracker) D {
|
||||
var names [256]string
|
||||
index := 0
|
||||
t.Model.SearchResults().Iterate(func(item string) (ok bool) {
|
||||
names[index] = item
|
||||
index++
|
||||
return index <= 256
|
||||
})
|
||||
element := func(gtx C, i int) D {
|
||||
w := LabelStyle{Text: names[i], ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
if i == pe.searchList.TrackerList.Selected() {
|
||||
for pe.SelectTypeBtn.Clicked() {
|
||||
t.Units().SetSelectedType(names[i])
|
||||
}
|
||||
return pe.SelectTypeBtn.Layout(gtx, w.Layout)
|
||||
}
|
||||
return w.Layout(gtx)
|
||||
}
|
||||
fdl := FilledDragList(t.Theme, pe.searchList, element, nil)
|
||||
dims := fdl.Layout(gtx)
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
fdl.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) command(e key.Event, t *Tracker) {
|
||||
params := (*tracker.Params)(t.Model)
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameLeftArrow:
|
||||
sel := params.SelectedItem()
|
||||
if sel == nil {
|
||||
return
|
||||
}
|
||||
i := (&tracker.Int{IntData: sel})
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
i.Set(i.Value() - sel.LargeStep())
|
||||
} else {
|
||||
i.Set(i.Value() - 1)
|
||||
}
|
||||
case key.NameRightArrow:
|
||||
sel := params.SelectedItem()
|
||||
if sel == nil {
|
||||
return
|
||||
}
|
||||
i := (&tracker.Int{IntData: sel})
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
i.Set(i.Value() + sel.LargeStep())
|
||||
} else {
|
||||
i.Set(i.Value() + 1)
|
||||
}
|
||||
case key.NameEscape:
|
||||
t.InstrumentEditor.unitDragList.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ParameterWidget struct {
|
||||
floatWidget widget.Float
|
||||
boolWidget widget.Bool
|
||||
instrBtn widget.Clickable
|
||||
instrMenu Menu
|
||||
unitBtn widget.Clickable
|
||||
unitMenu Menu
|
||||
Parameter tracker.Parameter
|
||||
}
|
||||
|
||||
type ParameterStyle struct {
|
||||
tracker *Tracker
|
||||
w *ParameterWidget
|
||||
Theme *material.Theme
|
||||
Focus bool
|
||||
}
|
||||
|
||||
func (t *Tracker) ParamStyle(th *material.Theme, paramWidget *ParameterWidget) ParameterStyle {
|
||||
return ParameterStyle{
|
||||
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way
|
||||
Theme: th,
|
||||
w: paramWidget,
|
||||
}
|
||||
}
|
||||
|
||||
func (p ParameterStyle) Layout(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110))
|
||||
return layout.E.Layout(gtx, Label(p.w.Parameter.Name(), white, p.tracker.Theme.Shaper))
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
switch p.w.Parameter.Type() {
|
||||
case tracker.IntegerParameter:
|
||||
for _, e := range gtx.Events(&p.w.floatWidget) {
|
||||
if ev, ok := e.(pointer.Event); ok && ev.Type == pointer.Scroll {
|
||||
delta := math.Min(math.Max(float64(ev.Scroll.Y), -1), 1)
|
||||
tracker.Int{IntData: p.w.Parameter}.Add(int(delta))
|
||||
}
|
||||
}
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
if !p.w.floatWidget.Dragging() {
|
||||
p.w.floatWidget.Value = float32(p.w.Parameter.Value())
|
||||
}
|
||||
ra := p.w.Parameter.Range()
|
||||
sliderStyle := material.Slider(p.Theme, &p.w.floatWidget, float32(ra.Min), float32(ra.Max))
|
||||
sliderStyle.Color = p.Theme.Fg
|
||||
r := image.Rectangle{Max: gtx.Constraints.Min}
|
||||
area := clip.Rect(r).Push(gtx.Ops)
|
||||
if p.Focus {
|
||||
pointer.InputOp{Tag: &p.w.floatWidget, Types: pointer.Scroll, ScrollBounds: image.Rectangle{Min: image.Pt(0, -1e6), Max: image.Pt(0, 1e6)}}.Add(gtx.Ops)
|
||||
}
|
||||
dims := sliderStyle.Layout(gtx)
|
||||
area.Pop()
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(int(p.w.floatWidget.Value + 0.5))
|
||||
return dims
|
||||
case tracker.BoolParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(60))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
ra := p.w.Parameter.Range()
|
||||
p.w.boolWidget.Value = p.w.Parameter.Value() > ra.Min
|
||||
boolStyle := material.Switch(p.Theme, &p.w.boolWidget, "Toggle boolean parameter")
|
||||
boolStyle.Color.Disabled = p.Theme.Fg
|
||||
boolStyle.Color.Enabled = white
|
||||
dims := layout.Center.Layout(gtx, boolStyle.Layout)
|
||||
if p.w.boolWidget.Value {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(ra.Max)
|
||||
} else {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(ra.Min)
|
||||
}
|
||||
return dims
|
||||
case tracker.IDParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
instrItems := make([]MenuItem, p.tracker.Instruments().Count())
|
||||
for i := range instrItems {
|
||||
i := i
|
||||
name, _, _ := p.tracker.Instruments().Item(i)
|
||||
instrItems[i].Text = name
|
||||
instrItems[i].IconBytes = icons.NavigationChevronRight
|
||||
instrItems[i].Doer = tracker.Allow(func() {
|
||||
if id, ok := p.tracker.Instruments().FirstID(i); ok {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
var unitItems []MenuItem
|
||||
instrName := "<instr>"
|
||||
unitName := "<unit>"
|
||||
targetI, targetU, err := p.tracker.FindUnit(p.w.Parameter.Value())
|
||||
if err == nil {
|
||||
targetInstrument := p.tracker.Instrument(targetI)
|
||||
instrName = targetInstrument.Name
|
||||
units := targetInstrument.Units
|
||||
unitName = fmt.Sprintf("%v: %v", targetU, units[targetU].Type)
|
||||
unitItems = make([]MenuItem, len(units))
|
||||
for j, unit := range units {
|
||||
id := unit.ID
|
||||
unitItems[j].Text = fmt.Sprintf("%v: %v", j, unit.Type)
|
||||
unitItems[j].IconBytes = icons.NavigationChevronRight
|
||||
unitItems[j].Doer = tracker.Allow(func() {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(p.tracker.layoutMenu(instrName, &p.w.instrBtn, &p.w.instrMenu, unit.Dp(200),
|
||||
instrItems...,
|
||||
)),
|
||||
layout.Rigid(p.tracker.layoutMenu(unitName, &p.w.unitBtn, &p.w.unitMenu, unit.Dp(200),
|
||||
unitItems...,
|
||||
)),
|
||||
)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if p.w.Parameter.Type() != tracker.IDParameter {
|
||||
return Label(p.w.Parameter.Hint(), white, p.tracker.Theme.Shaper)(gtx)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user