~eliasnaur/gio-patches

gio: Make widget.Editor keyboard shortcuts extensible v1 PROPOSED

~whereswaldon
This change changes the material.Editor so that it emits an even
whenever it received a non-text-input event that isn't one of its
builtin keyboard shortcuts. This makes it easy for applications to add
their own keyboard shortcut behavior while the editor widget has focus
(which was previously impossible).

I modified the kitchen demo so
that it can accept ctrl+v to paste into the editors, though I left the
old paste-by-clicking-a-button behavior in there. I don't mind removing
that if it's preferred.

I mostly want this feature to support pasting
and dismissing the editor (by pressing escape) in my applications.
Feedback welcome!
Chris

Chris Waldon (3):
  widget: make Editor emit events for unhandled key.Events
  example: dummy commit to make next patch build
  example/kitchen: paste into editors with ctrl+v

 example/go.mod             |  2 ++
 example/kitchen/kitchen.go | 41 ++++++++++++++++++++++++++++++++++----
 widget/editor.go           | 15 ++++++++++++--
 widget/editor_test.go      | 39 ++++++++++++++++++++++++++++++++++++
 4 files changed, 91 insertions(+), 6 deletions(-)

-- 
2.26.2
#326969 apple.yml failed
#328684 freebsd.yml failed
#326971 linux.yml success
#326972 openbsd.yml success
builds.sr.ht
gio/patches: FAILED in 9m7s

[Make widget.Editor keyboard shortcuts extensible][0] from [~whereswaldon][1]

[0]: https://lists.sr.ht/~eliasnaur/gio-patches/patches/14507
[1]: mailto:christopher.waldon.dev@gmail.com

✓ #326971 SUCCESS gio/patches/linux.yml   https://builds.sr.ht/~eliasnaur/job/326971
✗ #326970 FAILED  gio/patches/freebsd.yml https://builds.sr.ht/~eliasnaur/job/326970
✗ #326969 FAILED  gio/patches/apple.yml   https://builds.sr.ht/~eliasnaur/job/326969
✓ #326972 SUCCESS gio/patches/openbsd.yml https://builds.sr.ht/~eliasnaur/job/326972
(I haven't looked at Chris's code.)

How would the Spy know which events were processed and which not?
What if you wrap a Spy around a Spy (around a Spy, around a Spy ...)?
Let's try not to reinvent the Javascript event model, if possible
(bubbling and cancelling and whatnot), which seems very complex.  (If
that's *not* possible, let's try to learn from it and/or other
toolkits' event models.)
I seem to recall (it's been a little while) that In my own code, I
have an ugly hack for this sort of thing for keystrokes, where
basically I mark keys I've consumed/processes as such, and then ignore
already-processed keys at the top of every event processing loop.
(I'm afraid it didn't occur to me to try to generalize this, much less
submit it, more fool I.  Also I admit it probably would've been better
if I'd just removed the key from the queue instead of having to modify
all my event loops, but oh well.  All too often I *Want It To Work
Tonight, Dammit!* and this, uh, impacts my code quality.  :)  I'd love
if this sort of thing were generalized and part of the core API.

-- L

On Mon, Oct 26, 2020 at 4:30 AM Elias Naur <mail@eliasnaur.com> wrote:
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/14507/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH gio 1/3] widget: make Editor emit events for unhandled key.Events Export this patch

~whereswaldon
From: Chris Waldon <christopher.waldon.dev@gmail.com>

This enables higher-level application logic to interpret the keypresses,
which can be used (for instance) to detect ctrl+v and request a keyboard
paste.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
---
 widget/editor.go      | 15 +++++++++++++--
 widget/editor_test.go | 39 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 52 insertions(+), 2 deletions(-)

diff --git a/widget/editor.go b/widget/editor.go
index 57784cb..3b29e25 100644
--- a/widget/editor.go
+++ b/widget/editor.go
@@ -142,6 +142,14 @@ type SubmitEvent struct {
	Text string
}

// UnhandledKeyEvent is generated when a non-text-input keypress is
// not recognized by the Editor. Higher-level application logic can
// consume this event to implement other keyboard shortcuts while the
// editor has focus.
type UnhandledKeyEvent struct {
	key.Event
}

type line struct {
	offset f32.Point
	clip   op.CallOp
@@ -251,7 +259,9 @@ func (e *Editor) processKey(gtx layout.Context) {
			if e.command(ke) {
				e.caret.scroll = true
				e.scroller.Stop()
				break
			}
			e.events = append(e.events, UnhandledKeyEvent{ke})
		case key.EditEvent:
			e.caret.scroll = true
			e.scroller.Stop()
@@ -932,5 +942,6 @@ func nullLayout(r io.Reader) ([]text.Line, error) {
	}, rerr
}

func (s ChangeEvent) isEditorEvent() {}
func (s SubmitEvent) isEditorEvent() {}
func (s ChangeEvent) isEditorEvent()       {}
func (s SubmitEvent) isEditorEvent()       {}
func (s UnhandledKeyEvent) isEditorEvent() {}
diff --git a/widget/editor_test.go b/widget/editor_test.go
index 3b003ad..409f91b 100644
--- a/widget/editor_test.go
+++ b/widget/editor_test.go
@@ -300,3 +300,42 @@ func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
	t := editMutation(rand.Intn(int(moveLast)))
	return reflect.ValueOf(t)
}

func TestEditorUnhandledKeyEvent(t *testing.T) {
	e := new(Editor)
	e.Focus()
	keyEvent := key.Event{Name: "V", Modifiers: key.ModCtrl}
	handledKeyEvent := key.Event{Name: key.NameUpArrow}
	tq := &testQueue{
		events: []event.Event{
			key.FocusEvent{Focus: true},
			key.EditEvent{Text: "A"},
			keyEvent,
			handledKeyEvent,
		},
	}
	gtx := layout.Context{
		Ops:         new(op.Ops),
		Constraints: layout.Constraints{Max: image.Pt(100, 100)},
		Queue:       tq,
	}
	cache := text.NewCache(gofont.Collection())
	fontSize := unit.Px(10)
	font := text.Font{}
	e.Layout(gtx, cache, font, fontSize)
	foundUnhandled := false
	for _, event := range e.Events() {
		switch event := event.(type) {
		case UnhandledKeyEvent:
			if event.Event == keyEvent {
				foundUnhandled = true
			}
			if event.Event == handledKeyEvent {
				t.Errorf("should not generate UnhandledKeyEvent for handled event: %v", handledKeyEvent)
			}
		}
	}
	if !foundUnhandled {
		t.Errorf("editor did not emit UnhandledKeyEvent")
	}
}
-- 
2.26.2

[PATCH gio 2/3] example: dummy commit to make next patch build Export this patch

~whereswaldon
From: Chris Waldon <christopher.waldon.dev@gmail.com>

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
---
 example/go.mod | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/example/go.mod b/example/go.mod
index fa0a357..0f11b09 100644
--- a/example/go.mod
+++ b/example/go.mod
@@ -11,3 +11,5 @@ require (
	golang.org/x/image v0.0.0-20200618115811-c13761719519
	golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
)

replace gioui.org => ../
-- 
2.26.2

[PATCH gio 3/3] example/kitchen: paste into editors with ctrl+v Export this patch

~whereswaldon
From: Chris Waldon <christopher.waldon.dev@gmail.com>

Uses command+V on macOS.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
---
 example/kitchen/kitchen.go | 41 ++++++++++++++++++++++++++++++++++----
 1 file changed, 37 insertions(+), 4 deletions(-)

diff --git a/example/kitchen/kitchen.go b/example/kitchen/kitchen.go
index f607493..7584b7a 100644
--- a/example/kitchen/kitchen.go
+++ b/example/kitchen/kitchen.go
@@ -15,12 +15,14 @@ import (
	"log"
	"math"
	"os"
	"runtime"
	"time"

	"gioui.org/app"
	"gioui.org/app/headless"
	"gioui.org/f32"
	"gioui.org/font/gofont"
	"gioui.org/io/key"
	"gioui.org/io/router"
	"gioui.org/io/system"
	"gioui.org/layout"
@@ -116,10 +118,19 @@ func loop(w *app.Window) error {
		case e := <-w.Events():
			switch e := e.(type) {
			case system.ClipboardEvent:
				lineEditor.SetText(e.Text)
				if pasteRequestingEditor == nil {
					lineEditor.Insert(e.Text)
				} else {
					pasteRequestingEditor.Insert(e.Text)
					pasteRequestingEditor = nil
				}
			case system.DestroyEvent:
				return e.Err
			case system.FrameEvent:
				if pasteRequested {
					w.ReadClipboard()
					pasteRequested = false
				}
				gtx := layout.NewContext(&ops, e)
				for iconButton.Clicked() {
					w.WriteClipboard(lineEditor.Text())
@@ -176,8 +187,10 @@ func transformedKitchen(gtx layout.Context, th *material.Theme) layout.Dimension
}

var (
	editor     = new(widget.Editor)
	lineEditor = &widget.Editor{
	pasteRequested        bool
	pasteRequestingEditor *widget.Editor
	editor                = new(widget.Editor)
	lineEditor            = &widget.Editor{
		SingleLine: true,
		Submit:     true,
	}
@@ -240,11 +253,31 @@ func (b iconAndTextButton) Layout(gtx layout.Context) layout.Dimensions {
	})
}

func processUnhandledKey(event key.Event, editor *widget.Editor) {
	modifier := key.ModCtrl
	if runtime.GOOS == "darwin" {
		modifier = key.ModCommand
	}
	if event.Modifiers == modifier && event.Name == "V" {
		pasteRequestingEditor = editor
		pasteRequested = true
	}
}

func kitchen(gtx layout.Context, th *material.Theme) layout.Dimensions {
	for _, e := range lineEditor.Events() {
		if e, ok := e.(widget.SubmitEvent); ok {
		switch e := e.(type) {
		case widget.SubmitEvent:
			topLabel = e.Text
			lineEditor.SetText("")
		case widget.UnhandledKeyEvent:
			processUnhandledKey(e.Event, lineEditor)
		}
	}
	for _, e := range editor.Events() {
		switch e := e.(type) {
		case widget.UnhandledKeyEvent:
			processUnhandledKey(e.Event, editor)
		}
	}
	widgets := []layout.Widget{
-- 
2.26.2
builds.sr.ht
gio/patches: FAILED in 9m7s

[Make widget.Editor keyboard shortcuts extensible][0] from [~whereswaldon][1]

[0]: https://lists.sr.ht/~eliasnaur/gio-patches/patches/14507
[1]: mailto:christopher.waldon.dev@gmail.com

✓ #326971 SUCCESS gio/patches/linux.yml   https://builds.sr.ht/~eliasnaur/job/326971
✗ #326970 FAILED  gio/patches/freebsd.yml https://builds.sr.ht/~eliasnaur/job/326970
✗ #326969 FAILED  gio/patches/apple.yml   https://builds.sr.ht/~eliasnaur/job/326969
✓ #326972 SUCCESS gio/patches/openbsd.yml https://builds.sr.ht/~eliasnaur/job/326972