diff --git a/go.mod b/go.mod index 9c3a7722..e9d25e87 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module github.com/vsariola/sointu go 1.21 require ( - gioui.org v0.3.1 - gioui.org/x v0.1.0 + gioui.org v0.5.0 + gioui.org/x v0.5.0 github.com/Masterminds/sprig v2.22.0+incompatible github.com/hajimehoshi/oto v0.6.6 - golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 + golang.org/x/text v0.9.0 gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 pipelined.dev/audio/vst2 v0.10.1-0.20240223162706-41e9b65fb5c2 @@ -29,10 +29,10 @@ require ( github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/stretchr/testify v1.6.1 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/image v0.7.0 // indirect golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/sys v0.12.0 // indirect pipelined.dev/pipe v0.11.0 // indirect pipelined.dev/signal v0.10.0 // indirect ) diff --git a/go.sum b/go.sum index 9796ac5c..239625b4 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,14 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= -gioui.org v0.3.1 h1:hslYkrkIWvx28Mxe3A87opl+8s9mnWsnWmPDh11+zco= -gioui.org v0.3.1/go.mod h1:2atiYR4upH71/6ehnh6XsUELa7JZOrOHHNMDxGBZF0Q= +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= +gioui.org v0.5.0 h1:07g7/LY1MFuTncfO4A5DIKMMsQV6PkPHyx0MhDqgmYY= +gioui.org v0.5.0/go.mod h1:2atiYR4upH71/6ehnh6XsUELa7JZOrOHHNMDxGBZF0Q= gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc= gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= -gioui.org/x v0.1.0 h1:CvphvaQSroRaNEZ+JbXBkV3J3klA76U3JpieyEwHFX4= -gioui.org/x v0.1.0/go.mod h1:5qZxjtK/TVznMlcEOyn8OheiCZlArxF3IKnLqSehKXQ= +gioui.org/x v0.5.0 h1:NVKTn5AZuYhkAnF7MYcy1dIes36+U1N4gUTsgBhfr4A= +gioui.org/x v0.5.0/go.mod h1:X4UBhvanAN+8S16L3K6jDMrVo7Dii7NptgBpOLBD7E4= git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI= git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -22,6 +23,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= +github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= @@ -49,8 +51,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5U golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg= -golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0= golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -84,8 +86,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -110,8 +112,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -pipelined.dev/audio/vst2 v0.10.1-0.20231016195025-8c5c6a64c826 h1:4c7O6PJ/Zl677O2VhXHUZK7LJyVBhUI7Q39+ri+gKUs= -pipelined.dev/audio/vst2 v0.10.1-0.20231016195025-8c5c6a64c826/go.mod h1:wETLxsbBPftj6t4iVBCXvH/Xgd27ZgIC4hNnHDYNuz8= pipelined.dev/audio/vst2 v0.10.1-0.20240223162706-41e9b65fb5c2 h1:qrI7YY5ZH4pJflMfzum2TKvA1NaX+H4feaA6jweX2R8= pipelined.dev/audio/vst2 v0.10.1-0.20240223162706-41e9b65fb5c2/go.mod h1:wETLxsbBPftj6t4iVBCXvH/Xgd27ZgIC4hNnHDYNuz8= pipelined.dev/pipe v0.10.0/go.mod h1:aIt+NPlW0QLYByqYniG77lTxSvl7OtCNLws/m+Xz5ww= diff --git a/tracker/gioui/buttons.go b/tracker/gioui/buttons.go index 2d8fe71d..4e8de3c3 100644 --- a/tracker/gioui/buttons.go +++ b/tracker/gioui/buttons.go @@ -51,9 +51,9 @@ func Tooltip(th *material.Theme, tip string) component.Tooltip { return tooltip } -func ActionIcon(th *material.Theme, w *ActionClickable, icon []byte, tip string) TipIconButtonStyle { +func ActionIcon(gtx C, th *material.Theme, w *ActionClickable, icon []byte, tip string) TipIconButtonStyle { ret := TipIcon(th, &w.TipClickable, icon, tip) - for w.Clickable.Clicked() { + for w.Clickable.Clicked(gtx) { w.Action.Do() } if !w.Action.Allowed() { @@ -74,14 +74,14 @@ func TipIcon(th *material.Theme, w *TipClickable, icon []byte, tip string) TipIc } } -func ToggleIcon(th *material.Theme, w *BoolClickable, offIcon, onIcon []byte, offTip, onTip string) TipIconButtonStyle { +func ToggleIcon(gtx C, 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() { + for w.Clickable.Clicked(gtx) { w.Bool.Toggle() } ibStyle := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "") @@ -102,8 +102,8 @@ func (t *TipIconButtonStyle) Layout(gtx C) D { 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() { +func ActionButton(gtx C, th *material.Theme, w *ActionClickable, text string) material.ButtonStyle { + for w.Clickable.Clicked(gtx) { w.Action.Do() } ret := material.Button(th, &w.Clickable, text) @@ -116,8 +116,8 @@ func ActionButton(th *material.Theme, w *ActionClickable, text string) material. return ret } -func ToggleButton(th *material.Theme, b *BoolClickable, text string) material.ButtonStyle { - for b.Clickable.Clicked() { +func ToggleButton(gtx C, th *material.Theme, b *BoolClickable, text string) material.ButtonStyle { + for b.Clickable.Clicked(gtx) { b.Bool.Toggle() } ret := material.Button(th, &b.Clickable, text) diff --git a/tracker/gioui/dialog.go b/tracker/gioui/dialog.go index 1393a0f4..de1b964c 100644 --- a/tracker/gioui/dialog.go +++ b/tracker/gioui/dialog.go @@ -1,6 +1,7 @@ package gioui import ( + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/layout" "gioui.org/op/paint" @@ -11,10 +12,11 @@ import ( ) type Dialog struct { - BtnAlt *ActionClickable - BtnOk *ActionClickable - BtnCancel *ActionClickable - tag bool + BtnAlt *ActionClickable + BtnOk *ActionClickable + BtnCancel *ActionClickable + tag bool + keyFilters []event.Filter } type DialogStyle struct { @@ -30,45 +32,77 @@ type DialogStyle struct { } func NewDialog(ok, alt, cancel tracker.Action) *Dialog { - return &Dialog{ + ret := &Dialog{ BtnOk: NewActionClickable(ok), BtnAlt: NewActionClickable(alt), BtnCancel: NewActionClickable(cancel), } + + return ret } -func ConfirmDialog(th *material.Theme, dialog *Dialog, title, text string) DialogStyle { +func ConfirmDialog(gtx C, 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)}, 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"), + AltStyle: ActionButton(gtx, th, dialog.BtnAlt, "Alt"), + OkStyle: ActionButton(gtx, th, dialog.BtnOk, "Ok"), + CancelStyle: ActionButton(gtx, th, dialog.BtnCancel, "Cancel"), Shaper: th.Shaper, } return ret } +func (d *Dialog) handleKeysForButton(gtx C, btn, next, prev *ActionClickable) { + for { + e, ok := gtx.Event( + key.Filter{Focus: &btn.Clickable, Name: key.NameLeftArrow}, + key.Filter{Focus: &btn.Clickable, Name: key.NameRightArrow}, + key.Filter{Focus: &btn.Clickable, Name: key.NameEscape}, + key.Filter{Focus: &btn.Clickable, Name: key.NameTab, Optional: key.ModShift}, + ) + if !ok { + break + } + if e, ok := e.(key.Event); ok && e.State == key.Press { + switch { + case e.Name == key.NameLeftArrow || (e.Name == key.NameTab && e.Modifiers.Contain(key.ModShift)): + gtx.Execute(key.FocusCmd{Tag: &prev.Clickable}) + case e.Name == key.NameRightArrow || (e.Name == key.NameTab && !e.Modifiers.Contain(key.ModShift)): + gtx.Execute(key.FocusCmd{Tag: &next.Clickable}) + case e.Name == key.NameEscape: + d.BtnCancel.Action.Do() + } + } + } +} + +func (d *Dialog) handleKeys(gtx C) { + if d.BtnAlt.Action.Allowed() { + d.handleKeysForButton(gtx, d.BtnAlt, d.BtnCancel, d.BtnOk) + d.handleKeysForButton(gtx, d.BtnCancel, d.BtnOk, d.BtnAlt) + d.handleKeysForButton(gtx, d.BtnOk, d.BtnAlt, d.BtnCancel) + } else { + d.handleKeysForButton(gtx, d.BtnOk, d.BtnCancel, d.BtnCancel) + d.handleKeysForButton(gtx, d.BtnCancel, d.BtnOk, d.BtnOk) + } +} + func (d *DialogStyle) Layout(gtx C) D { - if !d.dialog.BtnOk.Clickable.Focused() && !d.dialog.BtnCancel.Clickable.Focused() && !d.dialog.BtnAlt.Clickable.Focused() { - d.dialog.BtnCancel.Clickable.Focus() + if !gtx.Source.Focused(&d.dialog.BtnOk.Clickable) && !gtx.Source.Focused(&d.dialog.BtnCancel.Clickable) && !gtx.Source.Focused(&d.dialog.BtnAlt.Clickable) { + gtx.Execute(key.FocusCmd{Tag: &d.dialog.BtnCancel.Clickable}) } + d.dialog.handleKeys(gtx) 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)), @@ -94,36 +128,3 @@ func (d *DialogStyle) Layout(gtx C) 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() - } - } -} diff --git a/tracker/gioui/draglist.go b/tracker/gioui/draglist.go index ea628759..7186721a 100644 --- a/tracker/gioui/draglist.go +++ b/tracker/gioui/draglist.go @@ -1,12 +1,16 @@ package gioui import ( + "bytes" "image" "image/color" + "io" "gioui.org/io/clipboard" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" + "gioui.org/io/transfer" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" @@ -27,7 +31,6 @@ type DragList struct { swapped bool focused bool requestFocus bool - mainTag bool } type FilledDragListStyle struct { @@ -72,11 +75,7 @@ func (s FilledDragListStyle) Layout(gtx C) D { 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-↓|⇞|⇟|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-→|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) + event.Op(gtx.Ops, s.dragList) if s.dragList.List.Axis == layout.Horizontal { gtx.Constraints.Min.X = gtx.Constraints.Max.X @@ -86,11 +85,39 @@ func (s FilledDragListStyle) Layout(gtx C) D { if s.dragList.requestFocus { s.dragList.requestFocus = false - key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops) + gtx.Execute(key.FocusCmd{Tag: s.dragList}) + } + + prevKey := key.NameUpArrow + nextKey := key.NameDownArrow + firstKey := key.NamePageUp + lastKey := key.NamePageDown + if s.dragList.List.Axis == layout.Horizontal { + prevKey = key.NameLeftArrow + nextKey = key.NameRightArrow + firstKey = key.NameHome + lastKey = key.NameEnd } - for _, ke := range gtx.Events(&s.dragList.mainTag) { - switch ke := ke.(type) { + for { + event, ok := gtx.Event( + key.FocusFilter{Target: s.dragList}, + transfer.TargetFilter{Target: s.dragList, Type: "application/text"}, + key.Filter{Focus: s.dragList, Name: prevKey, Optional: key.ModShift | key.ModShortcut}, + key.Filter{Focus: s.dragList, Name: nextKey, Optional: key.ModShift | key.ModShortcut}, + key.Filter{Focus: s.dragList, Name: firstKey, Optional: key.ModShift | key.ModShortcut}, + key.Filter{Focus: s.dragList, Name: lastKey, Optional: key.ModShift | key.ModShortcut}, + key.Filter{Focus: s.dragList, Name: "A", Required: key.ModShortcut}, + key.Filter{Focus: s.dragList, Name: "C", Required: key.ModShortcut}, + key.Filter{Focus: s.dragList, Name: "X", Required: key.ModShortcut}, + key.Filter{Focus: s.dragList, Name: "V", Required: key.ModShortcut}, + key.Filter{Focus: s.dragList, Name: key.NameDeleteBackward, Required: key.ModShortcut}, + key.Filter{Focus: s.dragList, Name: key.NameDeleteForward}, + ) + if !ok { + break + } + switch ke := event.(type) { case key.FocusEvent: s.dragList.focused = ke.Focus if !s.dragList.focused { @@ -101,10 +128,13 @@ func (s FilledDragListStyle) Layout(gtx C) D { break } s.dragList.command(gtx, ke) - case clipboard.Event: - s.dragList.TrackerList.PasteElements([]byte(ke.Text)) + case transfer.DataEvent: + if b, err := io.ReadAll(ke.Open()); err == nil { + s.dragList.TrackerList.PasteElements([]byte(b)) + } + } - op.InvalidateOp{}.Add(gtx.Ops) + gtx.Execute(op.InvalidateCmd{}) } _, isMutable := s.dragList.TrackerList.ListData.(tracker.MutableListData) @@ -128,12 +158,19 @@ func (s FilledDragListStyle) Layout(gtx C) D { } paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op()) - for _, ev := range gtx.Events(&s.dragList.tags[index]) { + for { + ev, ok := gtx.Event(pointer.Filter{ + Target: &s.dragList.tags[index], + Kinds: pointer.Press | pointer.Enter | pointer.Leave, + }) + if !ok { + break + } e, ok := ev.(pointer.Event) if !ok { continue } - switch e.Type { + switch e.Kind { case pointer.Enter: s.dragList.HoverItem = index case pointer.Leave: @@ -148,22 +185,28 @@ func (s FilledDragListStyle) Layout(gtx C) D { if !e.Modifiers.Contain(key.ModShift) { s.dragList.TrackerList.SetSelected2(index) } - key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops) + gtx.Execute(key.FocusCmd{Tag: s.dragList}) } } rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y) area := clip.Rect(rect).Push(gtx.Ops) - pointer.InputOp{Tag: &s.dragList.tags[index], - Types: pointer.Press | pointer.Enter | pointer.Leave, - }.Add(gtx.Ops) + event.Op(gtx.Ops, &s.dragList.tags[index]) area.Pop() if index == s.dragList.TrackerList.Selected() && isMutable { - for _, ev := range gtx.Events(&s.dragList.focused) { + for { + target := &s.dragList.focused + if s.dragList.drag { + target = nil + } + ev, ok := gtx.Event(pointer.Filter{Target: target, Kinds: pointer.Drag | pointer.Press | pointer.Release | pointer.Cancel}) + if !ok { + break + } e, ok := ev.(pointer.Event) if !ok { continue } - switch e.Type { + switch e.Kind { case pointer.Press: s.dragList.dragID = e.PointerID s.dragList.drag = true @@ -186,17 +229,12 @@ func (s FilledDragListStyle) Layout(gtx C) D { swap = 1 } } - case pointer.Release: - fallthrough - case pointer.Cancel: + case pointer.Release, pointer.Cancel: s.dragList.drag = false } } area := clip.Rect(rect).Push(gtx.Ops) - pointer.InputOp{Tag: &s.dragList.focused, - Types: pointer.Drag | pointer.Press | pointer.Release, - Grab: s.dragList.drag, - }.Add(gtx.Ops) + event.Op(gtx.Ops, &s.dragList.focused) pointer.CursorGrab.Add(gtx.Ops) area.Pop() } @@ -225,7 +263,7 @@ func (s FilledDragListStyle) Layout(gtx C) D { 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) + gtx.Execute(op.InvalidateCmd{}) } s.dragList.swapped = true } else { @@ -238,12 +276,12 @@ 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) + gtx.Execute(clipboard.ReadCmd{Tag: e}) 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) + gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(data))}) } return case "A": diff --git a/tracker/gioui/instrument_editor.go b/tracker/gioui/instrument_editor.go index 4c314e78..2b51abd8 100644 --- a/tracker/gioui/instrument_editor.go +++ b/tracker/gioui/instrument_editor.go @@ -1,14 +1,17 @@ package gioui import ( + "bytes" "fmt" "image" "image/color" + "io" "strconv" "strings" "gioui.org/font" "gioui.org/io/clipboard" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/layout" "gioui.org/op" @@ -45,6 +48,9 @@ type InstrumentEditor struct { wasFocused bool presetMenuItems []MenuItem presetMenu Menu + commentKeyFilters []event.Filter + searchkeyFilters []event.Filter + nameKeyFilters []event.Filter } func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor { @@ -72,6 +78,17 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor { ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: name, IconBytes: icons.ImageAudiotrack, Doer: model.LoadPreset(index)}) return true }) + for k := range noteMap { + ret.commentKeyFilters = append(ret.commentKeyFilters, key.Filter{Name: k, Focus: ret.commentEditor}) + ret.searchkeyFilters = append(ret.searchkeyFilters, key.Filter{Name: k, Focus: ret.searchEditor}) + ret.nameKeyFilters = append(ret.nameKeyFilters, key.Filter{Name: k, Focus: ret.nameEditor}) + } + ret.commentKeyFilters = append(ret.commentKeyFilters, key.Filter{Name: key.NameEscape, Focus: ret.commentEditor}) + ret.searchkeyFilters = append(ret.searchkeyFilters, key.Filter{Name: key.NameEscape, Focus: ret.searchEditor}) + ret.nameKeyFilters = append(ret.nameKeyFilters, key.Filter{Name: key.NameEscape, Focus: ret.nameEditor}) + ret.commentKeyFilters = append(ret.commentKeyFilters, key.Filter{Name: key.NameSpace, Focus: ret.commentEditor}) + ret.searchkeyFilters = append(ret.searchkeyFilters, key.Filter{Name: key.NameSpace, Focus: ret.searchEditor}) + ret.nameKeyFilters = append(ret.nameKeyFilters, key.Filter{Name: key.NameSpace, Focus: ret.nameEditor}) return ret } @@ -83,14 +100,16 @@ 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) childFocused(gtx C) bool { + return ie.unitEditor.sliderList.Focused() || + ie.instrumentDragList.Focused() || gtx.Source.Focused(ie.commentEditor) || gtx.Source.Focused(ie.nameEditor) || gtx.Source.Focused(ie.searchEditor) || + gtx.Source.Focused(ie.addUnitBtn.Clickable) || gtx.Source.Focused(ie.commentExpandBtn.Clickable) || gtx.Source.Focused(ie.presetMenuBtn.Clickable) || + gtx.Source.Focused(ie.deleteInstrumentBtn.Clickable) || gtx.Source.Focused(ie.copyInstrumentBtn.Clickable) } 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)") + ie.wasFocused = ie.Focused() || ie.childFocused(gtx) + fullscreenBtnStyle := ToggleIcon(gtx, 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)) @@ -99,7 +118,7 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D { return dims } - newBtnStyle := ActionIcon(t.Theme, ie.newInstrumentBtn, icons.ContentAdd, "Add\ninstrument\n(Ctrl+I)") + newBtnStyle := ActionIcon(gtx, 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( @@ -142,23 +161,23 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D { 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") + commentExpandBtnStyle := ToggleIcon(gtx, 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") + deleteInstrumentBtnStyle := ActionIcon(gtx, t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, "Delete\ninstrument") m := PopupMenu(&ie.presetMenu, t.Theme.Shaper) - for ie.copyInstrumentBtn.Clickable.Clicked() { + for ie.copyInstrumentBtn.Clickable.Clicked(gtx) { if contents, ok := t.Instruments().List().CopyElements(); ok { - clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops) + gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))}) t.Alerts().Add("Instrument copied to clipboard", tracker.Info) } } - for ie.saveInstrumentBtn.Clickable.Clicked() { + for ie.saveInstrumentBtn.Clickable.Clicked(gtx) { writer, err := t.Explorer.CreateFile(t.InstrumentName().Value() + ".yml") if err != nil { continue @@ -166,7 +185,7 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D { t.SaveInstrument(writer) } - for ie.loadInstrumentBtn.Clickable.Clicked() { + for ie.loadInstrumentBtn.Clickable.Clicked(gtx) { reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp") if err != nil { continue @@ -199,11 +218,11 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D { layout.Rigid(deleteInstrumentBtnStyle.Layout)) } - for ie.presetMenuBtn.Clickable.Clicked() { + for ie.presetMenuBtn.Clickable.Clicked(gtx) { 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.commentExpandBtn.Bool.Value() || gtx.Source.Focused(ie.commentEditor) { // we draw once the widget after it manages to lose focus if ie.commentEditor.Text() != ie.commentString.Value() { ie.commentEditor.SetText(ie.commentString.Value()) } @@ -211,8 +230,11 @@ func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D { 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) { + for { + event, ok := gtx.Event(ie.commentKeyFilters...) + if !ok { + break + } if e, ok := event.(key.Event); ok && e.State == key.Press && e.Name == key.NameEscape { ie.instrumentDragList.Focus() } @@ -249,13 +271,6 @@ func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D { 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) } @@ -264,11 +279,30 @@ func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D { editor.HintColor = instrumentNameHintColor editor.TextSize = unit.Sp(12) editor.Font = labelDefaultFont + for { + ev, ok := ie.nameEditor.Update(gtx) + if !ok { + break + } + _, ok = ev.(widget.SubmitEvent) + if ok { + ie.instrumentDragList.Focus() + continue + } + } 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) }) + for { // don't let key presses flow through from the editor + event, ok := gtx.Event(ie.nameKeyFilters...) + if !ok { + break + } + if e, ok := event.(key.Event); ok && e.State == key.Press && e.Name == key.NameEscape { + ie.instrumentDragList.Focus() + } + } ie.nameString.Set(ie.nameEditor.Text()) return dims } @@ -297,9 +331,15 @@ func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D { 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) { + for { + event, ok := gtx.Event( + key.Filter{Focus: ie.instrumentDragList, Name: key.NameDownArrow}, + key.Filter{Focus: ie.instrumentDragList, Name: key.NameReturn}, + key.Filter{Focus: ie.instrumentDragList, Name: key.NameEnter}, + ) + if !ok { + break + } switch e := event.(type) { case key.Event: switch e.State { @@ -308,7 +348,7 @@ func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D { case key.NameDownArrow: ie.unitDragList.Focus() case key.NameReturn, key.NameEnter: - ie.nameEditor.Focus() + gtx.Execute(key.FocusCmd{Tag: ie.nameEditor}) l := len(ie.nameEditor.Text()) ie.nameEditor.SetCaret(l, l) } @@ -321,9 +361,10 @@ func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D { 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 := ActionIcon(gtx, 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)) @@ -365,8 +406,26 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D { 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) + editor := material.Editor(t.Theme, ie.searchEditor, "---") + editor.Color = color + editor.HintColor = instrumentNameHintColor + editor.TextSize = unit.Sp(12) + editor.Font = f + defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() + txt := u.Type + str := tracker.String{StringData: (*tracker.UnitSearch)(t.Model)} + if t.UnitSearching().Value() { + txt = str.Value() + } + if ie.searchEditor.Text() != txt { + ie.searchEditor.SetText(txt) + } + for { + ev, ok := ie.searchEditor.Update(gtx) + if !ok { + break + } + _, ok = ev.(widget.SubmitEvent) if ok { txt := "" ie.unitDragList.Focus() @@ -383,23 +442,16 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D { continue } } - editor := material.Editor(t.Theme, ie.searchEditor, "---") - editor.Color = color - editor.HintColor = instrumentNameHintColor - editor.TextSize = unit.Sp(12) - editor.Font = f - - 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) - txt := u.Type - str := tracker.String{StringData: (*tracker.UnitSearch)(t.Model)} - if t.UnitSearching().Value() { - txt = str.Value() - } - if ie.searchEditor.Text() != txt { - ie.searchEditor.SetText(txt) - } ret := editor.Layout(gtx) + for { // don't let key presses flow through from the editor + event, ok := gtx.Event(ie.searchkeyFilters...) + if !ok { + break + } + if e, ok := event.(key.Event); ok && e.State == key.Press && e.Name == key.NameEscape { + ie.instrumentDragList.Focus() + } + } if ie.searchEditor.Text() != txt { str.Set(ie.searchEditor.Text()) } @@ -424,8 +476,18 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) 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) { + for { + event, ok := gtx.Event( + key.Filter{Focus: ie.unitDragList, Name: key.NameRightArrow}, + key.Filter{Focus: ie.unitDragList, Name: key.NameEnter, Optional: key.ModCtrl}, + key.Filter{Focus: ie.unitDragList, Name: key.NameReturn, Optional: key.ModCtrl}, + key.Filter{Focus: ie.unitDragList, Name: key.NameDeleteBackward}, + key.Filter{Focus: ie.unitDragList, Name: key.NameEscape}, + ) + if !ok { + break + } + switch e := event.(type) { case key.Event: switch e.State { @@ -437,13 +499,13 @@ func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D { ie.unitEditor.sliderList.Focus() case key.NameDeleteBackward: t.Units().SetSelectedType("") - ie.searchEditor.Focus() + gtx.Execute(key.FocusCmd{Tag: ie.searchEditor}) l := len(ie.searchEditor.Text()) ie.searchEditor.SetCaret(l, l) - case key.NameReturn: + case key.NameEnter, key.NameReturn: t.Model.AddUnit(e.Modifiers.Contain(key.ModCtrl)).Do() ie.searchEditor.SetText("") - ie.searchEditor.Focus() + gtx.Execute(key.FocusCmd{Tag: ie.searchEditor}) l := len(ie.searchEditor.Text()) ie.searchEditor.SetCaret(l, l) } diff --git a/tracker/gioui/keyevent.go b/tracker/gioui/keyevent.go index e83a9076..cc16b426 100644 --- a/tracker/gioui/keyevent.go +++ b/tracker/gioui/keyevent.go @@ -3,14 +3,9 @@ package gioui import ( "gioui.org/io/clipboard" "gioui.org/io/key" - "gioui.org/op" ) -// 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{ +var noteMap = map[key.Name]int{ "Z": -12, "S": -11, "X": -10, @@ -46,12 +41,12 @@ var noteMap = map[string]int{ } // KeyEvent handles incoming key events and returns true if repaint is needed. -func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) { +func (t *Tracker) KeyEvent(e key.Event, gtx C) { if e.State == key.Press { switch e.Name { case "V": if e.Modifiers.Contain(key.ModShortcut) { - clipboard.ReadOp{Tag: t}.Add(o) + gtx.Execute(clipboard.ReadCmd{Tag: t}) return } case "Z": diff --git a/tracker/gioui/menu.go b/tracker/gioui/menu.go index ce95e45c..96dcd65b 100644 --- a/tracker/gioui/menu.go +++ b/tracker/gioui/menu.go @@ -4,6 +4,7 @@ import ( "image" "image/color" + "gioui.org/io/event" "gioui.org/io/pointer" "gioui.org/layout" "gioui.org/op" @@ -65,12 +66,19 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D { m.Menu.tags = append(m.Menu.tags, false) } // handle pointer events for this item - for _, ev := range gtx.Events(&m.Menu.tags[i]) { + for { + ev, ok := gtx.Event(pointer.Filter{ + Target: &m.Menu.tags[i], + Kinds: pointer.Press | pointer.Enter | pointer.Leave, + }) + if !ok { + break + } e, ok := ev.(pointer.Event) if !ok { continue } - switch e.Type { + switch e.Kind { case pointer.Press: item.Doer.Do() m.Menu.Visible = false @@ -130,9 +138,7 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D { 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], - Types: pointer.Press | pointer.Enter | pointer.Leave, - }.Add(gtx.Ops) + event.Op(gtx.Ops, &m.Menu.tags[i]) area.Pop() } return dims @@ -163,8 +169,8 @@ func PopupMenu(menu *Menu, shaper *text.Shaper) MenuStyle { } } -func (tr *Tracker) layoutMenu(title string, clickable *widget.Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget { - for clickable.Clicked() { +func (tr *Tracker) layoutMenu(gtx C, title string, clickable *widget.Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget { + for clickable.Clicked(gtx) { menu.Visible = true } m := PopupMenu(menu, tr.Theme.Shaper) diff --git a/tracker/gioui/note_editor.go b/tracker/gioui/note_editor.go index 0371da07..f4b69567 100644 --- a/tracker/gioui/note_editor.go +++ b/tracker/gioui/note_editor.go @@ -83,7 +83,50 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor { } func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions { - for _, e := range gtx.Events(&te.tag) { + for { + e, ok := gtx.Event( + key.Filter{Focus: te.scrollTable, Name: "A"}, + key.Filter{Focus: te.scrollTable, Name: "B"}, + key.Filter{Focus: te.scrollTable, Name: "C"}, + key.Filter{Focus: te.scrollTable, Name: "D"}, + key.Filter{Focus: te.scrollTable, Name: "E"}, + key.Filter{Focus: te.scrollTable, Name: "F"}, + key.Filter{Focus: te.scrollTable, Name: "G"}, + key.Filter{Focus: te.scrollTable, Name: "H"}, + key.Filter{Focus: te.scrollTable, Name: "I"}, + key.Filter{Focus: te.scrollTable, Name: "J"}, + key.Filter{Focus: te.scrollTable, Name: "K"}, + key.Filter{Focus: te.scrollTable, Name: "L"}, + key.Filter{Focus: te.scrollTable, Name: "M"}, + key.Filter{Focus: te.scrollTable, Name: "N"}, + key.Filter{Focus: te.scrollTable, Name: "O"}, + key.Filter{Focus: te.scrollTable, Name: "P"}, + key.Filter{Focus: te.scrollTable, Name: "Q"}, + key.Filter{Focus: te.scrollTable, Name: "R"}, + key.Filter{Focus: te.scrollTable, Name: "S"}, + key.Filter{Focus: te.scrollTable, Name: "T"}, + key.Filter{Focus: te.scrollTable, Name: "U"}, + key.Filter{Focus: te.scrollTable, Name: "V"}, + key.Filter{Focus: te.scrollTable, Name: "W"}, + key.Filter{Focus: te.scrollTable, Name: "X"}, + key.Filter{Focus: te.scrollTable, Name: "Y"}, + key.Filter{Focus: te.scrollTable, Name: "Z"}, + key.Filter{Focus: te.scrollTable, Name: "0"}, + key.Filter{Focus: te.scrollTable, Name: "1"}, + key.Filter{Focus: te.scrollTable, Name: "2"}, + key.Filter{Focus: te.scrollTable, Name: "3"}, + key.Filter{Focus: te.scrollTable, Name: "4"}, + key.Filter{Focus: te.scrollTable, Name: "5"}, + key.Filter{Focus: te.scrollTable, Name: "6"}, + key.Filter{Focus: te.scrollTable, Name: "7"}, + key.Filter{Focus: te.scrollTable, Name: "8"}, + key.Filter{Focus: te.scrollTable, Name: "9"}, + key.Filter{Focus: te.scrollTable, Name: ","}, + key.Filter{Focus: te.scrollTable, Name: "."}, + ) + if !ok { + break + } switch e := e.(type) { case key.Event: if e.State == key.Release { @@ -99,7 +142,6 @@ func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions { 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, @@ -115,19 +157,19 @@ func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions { func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D { return Surface{Gray: 37, Focus: te.scrollTable.Focused() || 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)") + addSemitoneBtnStyle := ActionButton(gtx, t.Theme, te.AddSemitoneBtn, "+1") + subtractSemitoneBtnStyle := ActionButton(gtx, t.Theme, te.SubtractSemitoneBtn, "-1") + addOctaveBtnStyle := ActionButton(gtx, t.Theme, te.AddOctaveBtn, "+12") + subtractOctaveBtnStyle := ActionButton(gtx, t.Theme, te.SubtractOctaveBtn, "-12") + noteOffBtnStyle := ActionButton(gtx, t.Theme, te.NoteOffBtn, "Note Off") + deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, "Delete track\n(Ctrl+Shift+T)") + newTrackBtnStyle := ActionIcon(gtx, 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") + effectBtnStyle := ToggleButton(gtx, 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), @@ -304,7 +346,7 @@ func (te *NoteEditor) command(gtx C, t *Tracker, e key.Event) { } var n byte if t.Model.Notes().Effect(te.scrollTable.Table.Cursor().X) { - if nibbleValue, err := strconv.ParseInt(e.Name, 16, 8); err == nil { + if nibbleValue, err := strconv.ParseInt(string(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 diff --git a/tracker/gioui/numericupdown.go b/tracker/gioui/numericupdown.go index 33d99753..36117093 100644 --- a/tracker/gioui/numericupdown.go +++ b/tracker/gioui/numericupdown.go @@ -15,6 +15,7 @@ import ( "gioui.org/x/component" "gioui.org/gesture" + "gioui.org/io/event" "gioui.org/io/pointer" "gioui.org/layout" "gioui.org/op" @@ -160,9 +161,16 @@ func (s *NumericUpDownStyle) layoutText(gtx C) D { func (s *NumericUpDownStyle) layoutDrag(gtx layout.Context) layout.Dimensions { { // handle dragging pxPerStep := float32(gtx.Dp(s.UnitsPerStep)) - for _, ev := range gtx.Events(s.NumberInput) { + for { + ev, ok := gtx.Event(pointer.Filter{ + Target: s.NumberInput, + Kinds: pointer.Press | pointer.Drag | pointer.Release, + }) + if !ok { + break + } if e, ok := ev.(pointer.Event); ok { - switch e.Type { + switch e.Kind { case pointer.Press: s.NumberInput.dragStartValue = s.NumberInput.Int.Value() s.NumberInput.dragStartXY = e.Position.X - e.Position.Y @@ -180,10 +188,7 @@ func (s *NumericUpDownStyle) layoutDrag(gtx layout.Context) layout.Dimensions { // register for input dragRect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y) area := clip.Rect(dragRect).Push(gtx.Ops) - pointer.InputOp{ - Tag: s.NumberInput, - Types: pointer.Press | pointer.Drag | pointer.Release, - }.Add(gtx.Ops) + event.Op(gtx.Ops, s.NumberInput) area.Pop() stack.Pop() } @@ -192,9 +197,13 @@ func (s *NumericUpDownStyle) layoutDrag(gtx layout.Context) layout.Dimensions { func (s *NumericUpDownStyle) layoutClick(gtx layout.Context, delta int, click *gesture.Click) layout.Dimensions { // handle clicking - for _, e := range click.Events(gtx) { - switch e.Type { - case gesture.TypeClick: + for { + ev, ok := click.Update(gtx.Source) + if !ok { + break + } + switch ev.Kind { + case gesture.KindClick: s.NumberInput.Int.Add(delta) } } diff --git a/tracker/gioui/order_editor.go b/tracker/gioui/order_editor.go index de2a203f..750e0636 100644 --- a/tracker/gioui/order_editor.go +++ b/tracker/gioui/order_editor.go @@ -8,6 +8,7 @@ import ( "strings" "gioui.org/f32" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/layout" "gioui.org/op" @@ -19,9 +20,9 @@ import ( "github.com/vsariola/sointu/tracker" ) -const patternCellHeight = 16 -const patternCellWidth = 16 -const patternRowMarkerWidth = 30 +const patternCellHeight = unit.Dp(16) +const patternCellWidth = unit.Dp(16) +const patternRowMarkerWidth = unit.Dp(30) const orderTitleHeight = unit.Dp(52) type OrderEditor struct { @@ -57,18 +58,11 @@ func (oe *OrderEditor) Layout(gtx C, t *Tracker) D { 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) - } - } + oe.handleEvents(gtx, 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: &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) + event.Op(gtx.Ops, &oe.tag) colTitle := func(gtx C, i int) D { h := gtx.Dp(orderTitleHeight) @@ -77,13 +71,13 @@ func (oe *OrderEditor) Layout(gtx C, t *Tracker) D { 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)} + return D{Size: image.Pt(gtx.Dp(patternCellWidth), h)} } rowTitle := func(gtx C, j int) D { w := gtx.Dp(unit.Dp(30)) 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()) + paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Dp(patternCellHeight))}.Op()) } color := rowMarkerPatternTextColor if l := t.Loop(); j >= l.Start && j < l.Start+l.Length { @@ -92,7 +86,7 @@ func (oe *OrderEditor) Layout(gtx C, t *Tracker) D { paint.ColorOp{Color: color}.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)} + return D{Size: image.Pt(w, gtx.Dp(patternCellHeight))} } selection := oe.scrollTable.Table.Range() @@ -114,7 +108,7 @@ func (oe *OrderEditor) Layout(gtx C, t *Tracker) D { 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)} + return D{Size: image.Pt(gtx.Dp(patternCellWidth), gtx.Dp(patternCellHeight))} } table := FilledScrollTable(t.Theme, oe.scrollTable, cell, colTitle, rowTitle, nil, nil) @@ -123,6 +117,61 @@ func (oe *OrderEditor) Layout(gtx C, t *Tracker) D { return table.Layout(gtx) } +func (oe *OrderEditor) handleEvents(gtx C, t *Tracker) { + for { + e, ok := gtx.Event( + key.Filter{Focus: oe.scrollTable, Name: key.NameDeleteBackward, Optional: key.ModShortcut}, + key.Filter{Focus: oe.scrollTable, Name: key.NameDeleteForward, Optional: key.ModShortcut}, + key.Filter{Focus: oe.scrollTable, Name: key.NameReturn, Optional: key.ModShortcut}, + key.Filter{Focus: oe.scrollTable, Name: "0"}, + key.Filter{Focus: oe.scrollTable, Name: "1"}, + key.Filter{Focus: oe.scrollTable, Name: "2"}, + key.Filter{Focus: oe.scrollTable, Name: "3"}, + key.Filter{Focus: oe.scrollTable, Name: "4"}, + key.Filter{Focus: oe.scrollTable, Name: "5"}, + key.Filter{Focus: oe.scrollTable, Name: "6"}, + key.Filter{Focus: oe.scrollTable, Name: "7"}, + key.Filter{Focus: oe.scrollTable, Name: "8"}, + key.Filter{Focus: oe.scrollTable, Name: "9"}, + key.Filter{Focus: oe.scrollTable, Name: "A"}, + key.Filter{Focus: oe.scrollTable, Name: "B"}, + key.Filter{Focus: oe.scrollTable, Name: "C"}, + key.Filter{Focus: oe.scrollTable, Name: "D"}, + key.Filter{Focus: oe.scrollTable, Name: "E"}, + key.Filter{Focus: oe.scrollTable, Name: "F"}, + key.Filter{Focus: oe.scrollTable, Name: "G"}, + key.Filter{Focus: oe.scrollTable, Name: "H"}, + key.Filter{Focus: oe.scrollTable, Name: "I"}, + key.Filter{Focus: oe.scrollTable, Name: "J"}, + key.Filter{Focus: oe.scrollTable, Name: "K"}, + key.Filter{Focus: oe.scrollTable, Name: "L"}, + key.Filter{Focus: oe.scrollTable, Name: "M"}, + key.Filter{Focus: oe.scrollTable, Name: "N"}, + key.Filter{Focus: oe.scrollTable, Name: "O"}, + key.Filter{Focus: oe.scrollTable, Name: "P"}, + key.Filter{Focus: oe.scrollTable, Name: "Q"}, + key.Filter{Focus: oe.scrollTable, Name: "R"}, + key.Filter{Focus: oe.scrollTable, Name: "S"}, + key.Filter{Focus: oe.scrollTable, Name: "T"}, + key.Filter{Focus: oe.scrollTable, Name: "U"}, + key.Filter{Focus: oe.scrollTable, Name: "V"}, + key.Filter{Focus: oe.scrollTable, Name: "W"}, + key.Filter{Focus: oe.scrollTable, Name: "X"}, + key.Filter{Focus: oe.scrollTable, Name: "Y"}, + key.Filter{Focus: oe.scrollTable, Name: "Z"}, + ) + if !ok { + break + } + if e, ok := e.(key.Event); ok { + if e.State != key.Press { + continue + } + oe.command(gtx, t, e) + } + } +} + func (oe *OrderEditor) command(gtx C, t *Tracker, e key.Event) { switch e.Name { case key.NameDeleteBackward: @@ -140,7 +189,7 @@ func (oe *OrderEditor) command(gtx C, t *Tracker, e key.Event) { } t.Model.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut)).Do() } - if iv, err := strconv.Atoi(e.Name); err == nil { + if iv, err := strconv.Atoi(string(e.Name)); err == nil { t.Model.Order().SetValue(oe.scrollTable.Table.Cursor(), iv) oe.scrollTable.EnsureCursorVisible() } diff --git a/tracker/gioui/popup.go b/tracker/gioui/popup.go index e8312195..c4773173 100644 --- a/tracker/gioui/popup.go +++ b/tracker/gioui/popup.go @@ -4,6 +4,7 @@ import ( "image" "image/color" + "gioui.org/io/event" "gioui.org/io/pointer" "gioui.org/layout" "gioui.org/op" @@ -44,13 +45,19 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D { return D{} } - for _, ev := range gtx.Events(s.Visible) { - e, ok := ev.(pointer.Event) + for { + event, ok := gtx.Event(pointer.Filter{ + Target: s.Visible, + Kinds: pointer.Press, + }) + if !ok { + break + } + e, ok := event.(pointer.Event) if !ok { continue } - - switch e.Type { + switch e.Kind { case pointer.Press: *s.Visible = false } @@ -70,16 +77,10 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D { paint.FillShape(gtx.Ops, s.ShadowColor, rrect2.Op(gtx.Ops)) paint.FillShape(gtx.Ops, s.SurfaceColor, rrect.Op(gtx.Ops)) area := clip.Rect(image.Rect(-1e6, -1e6, 1e6, 1e6)).Push(gtx.Ops) - pointer.InputOp{Tag: s.Visible, - Types: pointer.Press, - Grab: true, - }.Add(gtx.Ops) + event.Op(gtx.Ops, s.Visible) area.Pop() area = clip.Rect(rrect2.Rect).Push(gtx.Ops) - pointer.InputOp{Tag: &dummyTag, - Types: pointer.Press, - Grab: true, - }.Add(gtx.Ops) + event.Op(gtx.Ops, &dummyTag) area.Pop() return D{Size: gtx.Constraints.Min} } diff --git a/tracker/gioui/popup_alert.go b/tracker/gioui/popup_alert.go index 0020c265..656a704a 100644 --- a/tracker/gioui/popup_alert.go +++ b/tracker/gioui/popup_alert.go @@ -31,7 +31,7 @@ func NewPopupAlert(alerts *tracker.Alerts, shaper *text.Shaper) *PopupAlert { 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) + gtx.Execute(op.InvalidateCmd{At: now.Add(50 * time.Millisecond)}) } a.prevUpdate = now diff --git a/tracker/gioui/scroll_table.go b/tracker/gioui/scroll_table.go index f1be6437..8187cf99 100644 --- a/tracker/gioui/scroll_table.go +++ b/tracker/gioui/scroll_table.go @@ -1,11 +1,15 @@ package gioui import ( + "bytes" "image" + "io" "gioui.org/io/clipboard" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" + "gioui.org/io/transfer" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" @@ -20,7 +24,6 @@ type ScrollTable struct { Table tracker.Table focused bool requestFocus bool - tag bool colTag bool rowTag bool cursorMoved bool @@ -85,65 +88,111 @@ func (st *ScrollTable) ChildFocused() bool { func (s ScrollTableStyle) Layout(gtx C) D { p := image.Pt(gtx.Dp(s.RowTitleWidth), gtx.Dp(s.ColumnTitleHeight)) + s.handleEvents(gtx) - for _, e := range gtx.Events(&s.ScrollTable.tag) { + 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() + 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) handleEvents(gtx layout.Context) { + for { + e, ok := gtx.Event( + key.FocusFilter{Target: s.ScrollTable}, + transfer.TargetFilter{Target: s.ScrollTable, Type: "application/text"}, + pointer.Filter{Target: s.ScrollTable, Kinds: pointer.Press}, + key.Filter{Focus: s.ScrollTable, Name: key.NameLeftArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt}, + key.Filter{Focus: s.ScrollTable, Name: key.NameUpArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt}, + key.Filter{Focus: s.ScrollTable, Name: key.NameRightArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt}, + key.Filter{Focus: s.ScrollTable, Name: key.NameDownArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt}, + key.Filter{Focus: s.ScrollTable, Name: key.NamePageUp, Optional: key.ModShift}, + key.Filter{Focus: s.ScrollTable, Name: key.NamePageDown, Optional: key.ModShift}, + key.Filter{Focus: s.ScrollTable, Name: key.NameHome, Optional: key.ModShift}, + key.Filter{Focus: s.ScrollTable, Name: key.NameEnd, Optional: key.ModShift}, + key.Filter{Focus: s.ScrollTable, Name: key.NameDeleteBackward}, + key.Filter{Focus: s.ScrollTable, Name: key.NameDeleteForward}, + key.Filter{Focus: s.ScrollTable, Name: "C", Required: key.ModShortcut}, + key.Filter{Focus: s.ScrollTable, Name: "V", Required: key.ModShortcut}, + key.Filter{Focus: s.ScrollTable, Name: "X", Required: key.ModShortcut}, + key.Filter{Focus: s.ScrollTable, Name: ",", Required: key.ModShift}, + key.Filter{Focus: s.ScrollTable, Name: ".", Required: key.ModShift}, + ) + if !ok { + break + } 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 + if e.Kind == pointer.Press { + gtx.Execute(key.FocusCmd{Tag: s.ScrollTable}) + } + dx := (int(e.Position.X) + s.ScrollTable.ColTitleList.List.Position.Offset) / gtx.Dp(s.CellWidth) + dy := (int(e.Position.Y) + s.ScrollTable.RowTitleList.List.Position.Offset) / 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)) + case transfer.DataEvent: + if b, err := io.ReadAll(e.Open()); err == nil { + s.ScrollTable.Table.Paste(b) + } } } - for _, e := range gtx.Events(&s.ScrollTable.rowTag) { + for { + e, ok := gtx.Event( + key.FocusFilter{ + Target: &s.ScrollTable.rowTag, + }, + key.Filter{ + Focus: &s.ScrollTable.rowTag, + Name: "→", + }, + ) + if !ok { + break + } if e, ok := e.(key.Event); ok && e.State == key.Press { s.ScrollTable.Focus() } } - for _, e := range gtx.Events(&s.ScrollTable.colTag) { + for { + e, ok := gtx.Event( + key.FocusFilter{ + Target: &s.ScrollTable.colTag, + }, + key.Filter{ + Focus: &s.ScrollTable.colTag, + Name: "↓", + }, + ) + if !ok { + break + } 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) { @@ -151,9 +200,9 @@ func (s ScrollTableStyle) layoutTable(gtx C, p image.Point) { if s.ScrollTable.requestFocus { s.ScrollTable.requestFocus = false - key.FocusOp{Tag: &s.ScrollTable.tag}.Add(gtx.Ops) + gtx.Execute(key.FocusCmd{Tag: s.ScrollTable}) } - 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) + event.Op(gtx.Ops, s.ScrollTable) cellWidth := gtx.Dp(s.CellWidth) cellHeight := gtx.Dp(s.CellHeight) @@ -162,14 +211,12 @@ func (s ScrollTableStyle) layoutTable(gtx C, p image.Point) { 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) + for x := 0; x < colP.Count; x++ { + for y := 0; y < rowP.Count; y++ { + o := op.Offset(image.Pt(cellWidth*x, cellHeight*y)).Push(gtx.Ops) + s.element(gtx, x+colP.First, y+rowP.First) + o.Pop() } - offs.Pop() - op.Offset(image.Pt(cellWidth, 0)).Add(gtx.Ops) } } @@ -179,7 +226,7 @@ func (s *ScrollTableStyle) layoutRowTitles(gtx C, p image.Point) { 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) + event.Op(gtx.Ops, &s.ScrollTable.rowTag) s.RowTitleStyle.Layout(gtx) } @@ -189,7 +236,7 @@ func (s *ScrollTableStyle) layoutColTitles(gtx C, p image.Point) { 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) + event.Op(gtx.Ops, &s.ScrollTable.colTag) s.ColTitleStyle.Layout(gtx) } @@ -210,7 +257,7 @@ func (s *ScrollTable) command(gtx C, e key.Event) { if !ok { return } - clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops) + gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))}) if e.Name == "X" { s.Table.Clear() } @@ -218,7 +265,7 @@ func (s *ScrollTable) command(gtx C, e key.Event) { } case "V": if e.Modifiers.Contain(key.ModShortcut) { - clipboard.ReadOp{Tag: &s.tag}.Add(gtx.Ops) + gtx.Execute(clipboard.ReadCmd{Tag: s}) } return case key.NameDeleteBackward, key.NameDeleteForward: diff --git a/tracker/gioui/scrollbar.go b/tracker/gioui/scrollbar.go index df39b156..0fbf20c8 100644 --- a/tracker/gioui/scrollbar.go +++ b/tracker/gioui/scrollbar.go @@ -4,6 +4,7 @@ import ( "image" "gioui.org/f32" + "gioui.org/io/event" "gioui.org/io/pointer" "gioui.org/layout" "gioui.org/op" @@ -77,19 +78,22 @@ func (s *ScrollBar) Layout(gtx C, width unit.Dp, numItems int, pos *layout.Posit rect := image.Rect(0, gtx.Constraints.Min.Y-scrWidth, gtx.Constraints.Min.X, gtx.Constraints.Min.Y) area = clip.Rect(rect).Push(gtx.Ops) } - pointer.InputOp{Tag: &s.dragStart, - Types: pointer.Drag | pointer.Press | pointer.Cancel | pointer.Release, - Grab: s.dragging, - }.Add(gtx.Ops) + event.Op(gtx.Ops, &s.dragStart) area.Pop() stack.Pop() - for _, ev := range gtx.Events(&s.dragStart) { + for { + ev, ok := gtx.Event( + pointer.Filter{Target: &s.dragStart, Kinds: pointer.Press | pointer.Cancel | pointer.Release | pointer.Drag}, + ) + if !ok { + break + } e, ok := ev.(pointer.Event) if !ok { continue } - switch e.Type { + switch e.Kind { case pointer.Press: if s.Axis == layout.Horizontal { s.dragStart = e.Position.X @@ -114,17 +118,22 @@ func (s *ScrollBar) Layout(gtx C, width unit.Dp, numItems int, pos *layout.Posit rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y) area2 := clip.Rect(rect).Push(gtx.Ops) defer pointer.PassOp{}.Push(gtx.Ops).Pop() - pointer.InputOp{Tag: &s.tag, - Types: pointer.Enter | pointer.Leave, - }.Add(gtx.Ops) + event.Op(gtx.Ops, &s.tag) area2.Pop() - for _, ev := range gtx.Events(&s.tag) { + for { + ev, ok := gtx.Event(pointer.Filter{ + Target: &s.tag, + Kinds: pointer.Enter | pointer.Leave, + }) + if !ok { + break + } e, ok := ev.(pointer.Event) if !ok { continue } - switch e.Type { + switch e.Kind { case pointer.Enter: s.hovering = true case pointer.Leave: diff --git a/tracker/gioui/songpanel.go b/tracker/gioui/songpanel.go index a06da4a1..99e9d7ca 100644 --- a/tracker/gioui/songpanel.go +++ b/tracker/gioui/songpanel.go @@ -93,8 +93,8 @@ func (t *SongPanel) layoutMenuBar(gtx C, tr *Tracker) D { gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36)) return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - 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...)), + layout.Rigid(tr.layoutMenu(gtx, "File", &t.MenuBar[0], &t.Menus[0], unit.Dp(200), t.fileMenuItems...)), + layout.Rigid(tr.layoutMenu(gtx, "Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(200), t.editMenuItems...)), ) } @@ -103,12 +103,12 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D { in := layout.UniformInset(unit.Dp(1)) - 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)") - loopBtnStyle := ToggleIcon(tr.Theme, t.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, "Loop\nOff\n(Ctrl+L)", "Loop\nOn\n(Ctrl+L)") + panicBtnStyle := ToggleButton(gtx, tr.Theme, t.PanicBtn, "Panic (F12)") + rewindBtnStyle := ActionIcon(gtx, tr.Theme, t.RewindBtn, icons.AVFastRewind, "Rewind\n(F5)") + playBtnStyle := ToggleIcon(gtx, tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F6 / Space)", "Stop (F6 / Space)") + recordBtnStyle := ToggleIcon(gtx, tr.Theme, t.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, "Record (F7)", "Stop (F7)") + noteTrackBtnStyle := ToggleIcon(gtx, tr.Theme, t.NoteTracking, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, "Follow\nOff\n(F8)", "Follow\nOn\n(F8)") + loopBtnStyle := ToggleIcon(gtx, tr.Theme, t.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, "Loop\nOff\n(Ctrl+L)", "Loop\nOn\n(Ctrl+L)") return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { diff --git a/tracker/gioui/split.go b/tracker/gioui/split.go index 490eaea0..065b4b98 100644 --- a/tracker/gioui/split.go +++ b/tracker/gioui/split.go @@ -3,6 +3,7 @@ package gioui import ( "image" + "gioui.org/io/event" "gioui.org/io/pointer" "gioui.org/layout" "gioui.org/op" @@ -48,14 +49,21 @@ func (s *Split) Layout(gtx layout.Context, first, second layout.Widget) layout.D { // handle input // Avoid affecting the input tree with pointer events. - - for _, ev := range gtx.Events(s) { + for { + ev, ok := gtx.Event(pointer.Filter{ + Target: s, + Kinds: pointer.Press | pointer.Drag | pointer.Release, + // TODO: there should be a grab; there was Grab: s.drag, + }) + if !ok { + break + } e, ok := ev.(pointer.Event) if !ok { continue } - switch e.Type { + switch e.Kind { case pointer.Press: if s.drag { break @@ -123,10 +131,7 @@ func (s *Split) Layout(gtx layout.Context, first, second layout.Widget) layout.D barRect = image.Rect(0, firstSize, gtx.Constraints.Max.X, secondOffset) } area := clip.Rect(barRect).Push(gtx.Ops) - pointer.InputOp{Tag: s, - Types: pointer.Press | pointer.Drag | pointer.Release, - Grab: s.drag, - }.Add(gtx.Ops) + event.Op(gtx.Ops, s) area.Pop() } diff --git a/tracker/gioui/tracker.go b/tracker/gioui/tracker.go index e9fc1b10..49fb709a 100644 --- a/tracker/gioui/tracker.go +++ b/tracker/gioui/tracker.go @@ -5,14 +5,14 @@ import ( "image" "io" "path/filepath" - "strings" "sync" "time" "gioui.org/app" - "gioui.org/io/clipboard" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/system" + "gioui.org/io/transfer" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" @@ -34,7 +34,7 @@ type ( TopHorizontalSplit *Split BottomHorizontalSplit *Split VerticalSplit *Split - KeyPlaying map[string]tracker.NoteID + KeyPlaying map[key.Name]tracker.NoteID PopupAlert *PopupAlert SaveChangesDialog *Dialog @@ -76,7 +76,7 @@ func NewTracker(model *tracker.Model) *Tracker { BottomHorizontalSplit: &Split{Ratio: -.6}, VerticalSplit: &Split{Axis: layout.Vertical}, - KeyPlaying: make(map[string]tracker.NoteID), + KeyPlaying: make(map[key.Name]tracker.NoteID), SaveChangesDialog: NewDialog(model.SaveSong(), model.DiscardSong(), model.Cancel()), WaveTypeDialog: NewDialog(model.ExportInt16(), model.ExportFloat(), model.Cancel()), InstrumentEditor: NewInstrumentEditor(model), @@ -107,6 +107,11 @@ func (t *Tracker) Main() { t.InstrumentEditor.Focus() recoveryTicker := time.NewTicker(time.Second * 30) t.Explorer = explorer.NewExplorer(w) + // Make a channel to read window events from. + events := make(chan event.Event) + // Make a channel to signal the end of processing a window event. + acks := make(chan struct{}) + go eventLoop(w, events, acks) var ops op.Ops for { if titleFooter != t.filePathString.Value() { @@ -121,9 +126,10 @@ func (t *Tracker) Main() { case e := <-t.PlayerMessages: t.ProcessPlayerMessage(e) w.Invalidate() - case e := <-w.Events(): + case e := <-events: switch e := e.(type) { - case system.DestroyEvent: + case app.DestroyEvent: + acks <- struct{}{} if canQuit { t.Quit().Do() } @@ -134,14 +140,18 @@ func (t *Tracker) Main() { app.Title("Sointu Tracker"), ) t.Explorer = explorer.NewExplorer(w) + go eventLoop(w, events, acks) } - case system.FrameEvent: - gtx := layout.NewContext(&ops, e) + case app.FrameEvent: + gtx := app.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) + acks <- struct{}{} + default: + acks <- struct{}{} } case <-recoveryTicker.C: t.SaveRecovery() @@ -158,6 +168,19 @@ func (t *Tracker) Main() { t.quitWG.Done() } +func eventLoop(w *app.Window, events chan<- event.Event, acks <-chan struct{}) { + // Iterate window events, sending each to the old event loop and waiting for + // a signal that processing is complete before iterating again. + for { + ev := w.NextEvent() + events <- ev + <-acks + if _, ok := ev.(app.DestroyEvent); ok { + return + } + } +} + func (t *Tracker) Exec() chan<- func() { return t.execChan } @@ -167,22 +190,6 @@ func (t *Tracker) WaitQuitted() { } 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) @@ -193,6 +200,27 @@ func (t *Tracker) Layout(gtx layout.Context, w *app.Window) { } t.PopupAlert.Layout(gtx) t.showDialog(gtx) + // 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 + for { + ev, ok := gtx.Event( + key.Filter{Name: "", Optional: key.ModAlt | key.ModCommand | key.ModShift | key.ModShortcut | key.ModSuper}, + key.Filter{Name: key.NameTab, Optional: key.ModShift}, + transfer.TargetFilter{Target: t, Type: "application/text"}, + ) + if !ok { + break + } + switch e := ev.(type) { + case key.Event: + t.KeyEvent(e, gtx) + case transfer.DataEvent: + t.ReadSong(e.Open()) + } + } + } func (t *Tracker) showDialog(gtx C) { @@ -201,12 +229,12 @@ func (t *Tracker) showDialog(gtx C) { } 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 := ConfirmDialog(gtx, 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 := ConfirmDialog(gtx, t.Theme, t.WaveTypeDialog, "", "Export .wav in int16 or float32 sample format?") dstyle.OkStyle.Text = "Int16" dstyle.AltStyle.Text = "Float32" dstyle.Layout(gtx) diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index 237a4f3e..bb408975 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -1,11 +1,14 @@ package gioui import ( + "bytes" "fmt" "image" + "io" "math" "gioui.org/io/clipboard" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/layout" @@ -29,7 +32,6 @@ type UnitEditor struct { ClearUnitBtn *ActionClickable DisableUnitBtn *BoolClickable SelectTypeBtn *widget.Clickable - tag bool caser cases.Caser } @@ -48,7 +50,15 @@ func NewUnitEditor(m *tracker.Model) *UnitEditor { } func (pe *UnitEditor) Layout(gtx C, t *Tracker) D { - for _, e := range gtx.Events(&pe.tag) { + for { + e, ok := gtx.Event( + key.Filter{Focus: pe.sliderList, Name: key.NameLeftArrow, Optional: key.ModShift}, + key.Filter{Focus: pe.sliderList, Name: key.NameRightArrow, Optional: key.ModShift}, + key.Filter{Focus: pe.sliderList, Name: key.NameEscape}, + ) + if !ok { + break + } switch e := e.(type) { case key.Event: if e.State == key.Press { @@ -58,8 +68,6 @@ func (pe *UnitEditor) Layout(gtx C, t *Tracker) D { } 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 if t.UnitSearching().Value() || pe.sliderList.TrackerList.Count() == 0 { @@ -108,15 +116,15 @@ func (pe *UnitEditor) layoutSliders(gtx C, t *Tracker) D { } func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D { - for pe.CopyUnitBtn.Clickable.Clicked() { + for pe.CopyUnitBtn.Clickable.Clicked(gtx) { if contents, ok := t.Units().List().CopyElements(); ok { - clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops) + gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))}) 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)") - disableUnitBtnStyle := ToggleIcon(t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, "Disable unit (Ctrl-D)", "Enable unit (Ctrl-D)") + deleteUnitBtnStyle := ActionIcon(gtx, t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)") + disableUnitBtnStyle := ToggleIcon(gtx, t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, "Disable unit (Ctrl-D)", "Enable unit (Ctrl-D)") text := t.Units().SelectedType() if text == "" { text = "Choose unit type" @@ -131,7 +139,7 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D { layout.Rigid(func(gtx C) D { var dims D if t.Units().SelectedType() != "" { - clearUnitBtnStyle := ActionIcon(t.Theme, pe.ClearUnitBtn, icons.ContentClear, "Clear unit") + clearUnitBtnStyle := ActionIcon(gtx, 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)} @@ -151,7 +159,7 @@ func (pe *UnitEditor) layoutUnitTypeChooser(gtx C, t *Tracker) D { 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() { + for pe.SelectTypeBtn.Clicked(gtx) { t.Units().SetSelectedType(names[i]) } return pe.SelectTypeBtn.Layout(gtx, w.Layout) @@ -232,28 +240,36 @@ func (p ParameterStyle) Layout(gtx C) D { 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 { + for p.Focus { + e, ok := gtx.Event(pointer.Filter{ + Target: &p.w.floatWidget, + Kinds: pointer.Scroll, + ScrollBounds: image.Rectangle{Min: image.Pt(0, -1e6), Max: image.Pt(0, 1e6)}, + }) + if !ok { + break + } + if ev, ok := e.(pointer.Event); ok && ev.Kind == 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)) + ra := p.w.Parameter.Range() if !p.w.floatWidget.Dragging() { - p.w.floatWidget.Value = float32(p.w.Parameter.Value()) + p.w.floatWidget.Value = (float32(p.w.Parameter.Value()) - float32(ra.Min)) / float32(ra.Max-ra.Min) } - ra := p.w.Parameter.Range() - sliderStyle := material.Slider(p.Theme, &p.w.floatWidget, float32(ra.Min), float32(ra.Max)) + sliderStyle := material.Slider(p.Theme, &p.w.floatWidget) 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) + event.Op(gtx.Ops, &p.w.floatWidget) } dims := sliderStyle.Layout(gtx) area.Pop() - tracker.Int{IntData: p.w.Parameter}.Set(int(p.w.floatWidget.Value + 0.5)) + tracker.Int{IntData: p.w.Parameter}.Set(int(p.w.floatWidget.Value*float32(ra.Max-ra.Min) + float32(ra.Min) + 0.5)) return dims case tracker.BoolParameter: gtx.Constraints.Min.X = gtx.Dp(unit.Dp(60)) @@ -305,10 +321,10 @@ func (p ParameterStyle) Layout(gtx C) D { } } return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(p.tracker.layoutMenu(instrName, &p.w.instrBtn, &p.w.instrMenu, unit.Dp(200), + layout.Rigid(p.tracker.layoutMenu(gtx, 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), + layout.Rigid(p.tracker.layoutMenu(gtx, unitName, &p.w.unitBtn, &p.w.unitMenu, unit.Dp(200), unitItems..., )), )