app, io: [wasm, android] add support for numeric/email keyboard mode

Previously, the on-screen keyboard always displays the text keyboard,
(QWERTY or equivalent).

For optimal user experience, it's possible to specify the keyboard type
using `InputHint`. The on-screen keyboard will provide shortcuts or
restrict what the user can input.

Due to some limitations (gio#116), only numeric and text keyboards are
supported on Android.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
This commit is contained in:
Inkeliz
2021-04-30 13:03:17 +01:00
committed by Elias Naur
parent e68ee35c86
commit 9b4b91fec0
14 changed files with 128 additions and 5 deletions
+13 -1
View File
@@ -36,6 +36,7 @@ import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.text.InputType;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@@ -47,6 +48,7 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
private final InputMethodManager imm; private final InputMethodManager imm;
private final float scrollXScale; private final float scrollXScale;
private final float scrollYScale; private final float scrollYScale;
private int keyboardHint;
private long nhandle; private long nhandle;
@@ -247,10 +249,20 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
} }
} }
@Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { @Override public InputConnection onCreateInputConnection(EditorInfo editor) {
editor.inputType = this.keyboardHint;
editor.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
return new InputConnection(this); return new InputConnection(this);
} }
void setInputHint(int hint) {
if (hint == this.keyboardHint) {
return;
}
this.keyboardHint = hint;
imm.restartInput(this);
}
void showTextInput() { void showTextInput() {
GioView.this.requestFocus(); GioView.this.requestFocus();
imm.showSoftInput(GioView.this, 0); imm.showSoftInput(GioView.this, 0);
+24
View File
@@ -152,6 +152,7 @@ var gioView struct {
getFontScale C.jmethodID getFontScale C.jmethodID
showTextInput C.jmethodID showTextInput C.jmethodID
hideTextInput C.jmethodID hideTextInput C.jmethodID
setInputHint C.jmethodID
postFrameCallback C.jmethodID postFrameCallback C.jmethodID
setCursor C.jmethodID setCursor C.jmethodID
setOrientation C.jmethodID setOrientation C.jmethodID
@@ -281,6 +282,7 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
m.getFontScale = getMethodID(env, class, "getFontScale", "()F") m.getFontScale = getMethodID(env, class, "getFontScale", "()F")
m.showTextInput = getMethodID(env, class, "showTextInput", "()V") m.showTextInput = getMethodID(env, class, "showTextInput", "()V")
m.hideTextInput = getMethodID(env, class, "hideTextInput", "()V") m.hideTextInput = getMethodID(env, class, "hideTextInput", "()V")
m.setInputHint = getMethodID(env, class, "setInputHint", "(I)V")
m.postFrameCallback = getMethodID(env, class, "postFrameCallback", "()V") m.postFrameCallback = getMethodID(env, class, "postFrameCallback", "()V")
m.setCursor = getMethodID(env, class, "setCursor", "(I)V") m.setCursor = getMethodID(env, class, "setCursor", "(I)V")
m.setOrientation = getMethodID(env, class, "setOrientation", "(II)V") m.setOrientation = getMethodID(env, class, "setOrientation", "(II)V")
@@ -609,6 +611,28 @@ func (w *window) ShowTextInput(show bool) {
}) })
} }
func (w *window) SetInputHint(mode key.InputHint) {
// Constants defined at https://developer.android.com/reference/android/text/InputType.
const (
TYPE_NULL = 0
TYPE_CLASS_NUMBER = 2
TYPE_NUMBER_FLAG_DECIMAL = 8192
TYPE_NUMBER_FLAG_SIGNED = 4096
)
runInJVM(javaVM(), func(env *C.JNIEnv) {
var m jvalue
switch mode {
case key.HintNumeric:
m = TYPE_CLASS_NUMBER | TYPE_NUMBER_FLAG_DECIMAL | TYPE_NUMBER_FLAG_SIGNED
default:
// TYPE_NULL, since TYPE_CLASS_TEXT isn't currently supported (gio#116), so TYPE_NULL is used instead.
m = TYPE_NULL
}
callVoidMethod(env, w.view, gioView.setInputHint, m)
})
}
func javaString(env *C.JNIEnv, str string) C.jstring { func javaString(env *C.JNIEnv, str string) C.jstring {
if str == "" { if str == "" {
return 0 return 0
+2
View File
@@ -297,6 +297,8 @@ func (w *window) ShowTextInput(show bool) {
} }
} }
func (w *window) SetInputHint(_ key.InputHint) {}
// Close the window. Not implemented for iOS. // Close the window. Not implemented for iOS.
func (w *window) Close() {} func (w *window) Close() {}
+25
View File
@@ -313,6 +313,27 @@ func (w *window) focus() {
w.requestFocus = true w.requestFocus = true
} }
func (w *window) keyboard(hint key.InputHint) {
var m string
switch hint {
case key.HintAny:
m = "text"
case key.HintText:
m = "text"
case key.HintNumeric:
m = "decimal"
case key.HintEmail:
m = "email"
case key.HintURL:
m = "url"
case key.HintTelephone:
m = "tel"
default:
m = "text"
}
w.tarea.Set("inputMode", m)
}
func (w *window) keyEvent(e js.Value, ks key.State) { func (w *window) keyEvent(e js.Value, ks key.State) {
k := e.Get("key").String() k := e.Get("key").String()
if n, ok := translateKey(k); ok { if n, ok := translateKey(k); ok {
@@ -524,6 +545,10 @@ func (w *window) ShowTextInput(show bool) {
}() }()
} }
func (w *window) SetInputHint(mode key.InputHint) {
w.keyboard(mode)
}
// Close the window. Not implemented for js. // Close the window. Not implemented for js.
func (w *window) Close() {} func (w *window) Close() {}
+2
View File
@@ -187,6 +187,8 @@ func (w *window) SetCursor(name pointer.CursorName) {
func (w *window) ShowTextInput(show bool) {} func (w *window) ShowTextInput(show bool) {}
func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) SetAnimating(anim bool) { func (w *window) SetAnimating(anim bool) {
if anim { if anim {
w.displayLink.Start() w.displayLink.Start()
+2
View File
@@ -1462,6 +1462,8 @@ func (w *window) surface() (*C.struct_wl_surface, int, int) {
func (w *window) ShowTextInput(show bool) {} func (w *window) ShowTextInput(show bool) {}
func (w *window) SetInputHint(_ key.InputHint) {}
// Close the window. Not implemented for Wayland. // Close the window. Not implemented for Wayland.
func (w *window) Close() {} func (w *window) Close() {}
+2
View File
@@ -665,6 +665,8 @@ func loadCursor(name pointer.CursorName) (syscall.Handle, error) {
func (w *window) ShowTextInput(show bool) {} func (w *window) ShowTextInput(show bool) {}
func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) HDC() syscall.Handle { func (w *window) HDC() syscall.Handle {
return w.hdc return w.hdc
} }
+2
View File
@@ -223,6 +223,8 @@ func (w *x11Window) SetWindowMode(mode WindowMode) {
func (w *x11Window) ShowTextInput(show bool) {} func (w *x11Window) ShowTextInput(show bool) {}
func (w *x11Window) SetInputHint(_ key.InputHint) {}
// Close the window. // Close the window.
func (w *x11Window) Close() { func (w *x11Window) Close() {
var xev C.XEvent var xev C.XEvent
+5
View File
@@ -6,6 +6,7 @@ package wm
import ( import (
"errors" "errors"
"gioui.org/io/key"
"image/color" "image/color"
"gioui.org/gpu" "gioui.org/gpu"
@@ -84,8 +85,12 @@ type Driver interface {
// SetAnimating sets the animation flag. When the window is animating, // SetAnimating sets the animation flag. When the window is animating,
// FrameEvents are delivered as fast as the display can handle them. // FrameEvents are delivered as fast as the display can handle them.
SetAnimating(anim bool) SetAnimating(anim bool)
// ShowTextInput updates the virtual keyboard state. // ShowTextInput updates the virtual keyboard state.
ShowTextInput(show bool) ShowTextInput(show bool)
SetInputHint(mode key.InputHint)
NewContext() (Context, error) NewContext() (Context, error)
// ReadClipboard requests the clipboard content. // ReadClipboard requests the clipboard content.
+3
View File
@@ -184,6 +184,9 @@ func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op.
case router.TextInputClose: case router.TextInputClose:
go w.driverRun(func(d wm.Driver) { d.ShowTextInput(false) }) go w.driverRun(func(d wm.Driver) { d.ShowTextInput(false) })
} }
if hint, ok := w.queue.q.TextInputHint(); ok {
go w.driverRun(func(d wm.Driver) { d.SetInputHint(hint) })
}
if txt, ok := w.queue.q.WriteClipboard(); ok { if txt, ok := w.queue.q.WriteClipboard(); ok {
go w.WriteClipboard(txt) go w.WriteClipboard(txt)
} }
+2 -2
View File
@@ -50,8 +50,8 @@ const (
TypePassLen = 1 + 1 TypePassLen = 1 + 1
TypeClipboardReadLen = 1 TypeClipboardReadLen = 1
TypeClipboardWriteLen = 1 TypeClipboardWriteLen = 1
TypeKeyInputLen = 1 TypeKeyInputLen = 1 + 1
TypeKeyFocusLen = 1 TypeKeyFocusLen = 1 + 1
TypeKeySoftKeyboardLen = 1 + 1 TypeKeySoftKeyboardLen = 1 + 1
TypeSaveLen = 1 + 4 TypeSaveLen = 1 + 4
TypeLoadLen = 1 + 1 + 4 TypeLoadLen = 1 + 1 + 4
+22 -1
View File
@@ -22,7 +22,8 @@ import (
// Key events are in general only delivered to the // Key events are in general only delivered to the
// focused key handler. // focused key handler.
type InputOp struct { type InputOp struct {
Tag event.Tag Tag event.Tag
Hint InputHint
} }
// SoftKeyboardOp shows or hide the on-screen keyboard, if available. // SoftKeyboardOp shows or hide the on-screen keyboard, if available.
@@ -64,6 +65,25 @@ type EditEvent struct {
Text string Text string
} }
// InputHint changes the on-screen-keyboard type. That hints the
// type of data that might be entered by the user.
type InputHint uint8
const (
// HintAny hints that any input is expected.
HintAny InputHint = iota
// HintText hints that text input is expected. It may activate auto-correction and suggestions.
HintText
// HintNumeric hints that numeric input is expected. It may activate shortcuts for 0-9, "." and ",".
HintNumeric
// HintEmail hints that email input is expected. It may activate shortcuts for common email characters, such as "@" and ".com".
HintEmail
// HintURL hints that URL input is expected. It may activate shortcuts for common URL fragments such as "/" and ".com".
HintURL
// HintTelephone hints that telephone number input is expected. It may activate shortcuts for 0-9, "#" and "*".
HintTelephone
)
// State is the state of a key during an event. // State is the state of a key during an event.
type State uint8 type State uint8
@@ -127,6 +147,7 @@ func (h InputOp) Add(o *op.Ops) {
} }
data := o.Write1(opconst.TypeKeyInputLen, h.Tag) data := o.Write1(opconst.TypeKeyInputLen, h.Tag)
data[0] = byte(opconst.TypeKeyInput) data[0] = byte(opconst.TypeKeyInput)
data[1] = byte(h.Hint)
} }
func (h SoftKeyboardOp) Add(o *op.Ops) { func (h SoftKeyboardOp) Add(o *op.Ops) {
+19 -1
View File
@@ -17,6 +17,7 @@ type keyQueue struct {
handlers map[event.Tag]*keyHandler handlers map[event.Tag]*keyHandler
reader ops.Reader reader ops.Reader
state TextInputState state TextInputState
hint key.InputHint
} }
type keyHandler struct { type keyHandler struct {
@@ -24,6 +25,7 @@ type keyHandler struct {
// in the current frame. // in the current frame.
visible bool visible bool
new bool new bool
hint key.InputHint
} }
const ( const (
@@ -38,6 +40,20 @@ func (q *keyQueue) InputState() TextInputState {
return q.state return q.state
} }
// InputHint returns the input mode from the most recent key.InputOp.
func (q *keyQueue) InputHint() (key.InputHint, bool) {
if q.focus == nil {
return q.hint, false
}
focused, ok := q.handlers[q.focus]
if !ok {
return q.hint, false
}
old := q.hint
q.hint = focused.hint
return q.hint, old != q.hint
}
func (q *keyQueue) Frame(root *op.Ops, events *handlerEvents) { func (q *keyQueue) Frame(root *op.Ops, events *handlerEvents) {
if q.handlers == nil { if q.handlers == nil {
q.handlers = make(map[event.Tag]*keyHandler) q.handlers = make(map[event.Tag]*keyHandler)
@@ -108,6 +124,7 @@ func (q *keyQueue) resolveFocus(events *handlerEvents) (focus event.Tag, changed
q.handlers[op.Tag] = h q.handlers[op.Tag] = h
} }
h.visible = true h.visible = true
h.hint = op.Hint
} }
} }
return return
@@ -118,7 +135,8 @@ func decodeKeyInputOp(d []byte, refs []interface{}) key.InputOp {
panic("invalid op") panic("invalid op")
} }
return key.InputOp{ return key.InputOp{
Tag: refs[0].(event.Tag), Tag: refs[0].(event.Tag),
Hint: key.InputHint(d[1]),
} }
} }
+5
View File
@@ -102,6 +102,11 @@ func (q *Router) TextInputState() TextInputState {
return q.kqueue.InputState() return q.kqueue.InputState()
} }
// TextInputHint returns the input mode from the most recent key.InputOp.
func (q *Router) TextInputHint() (key.InputHint, bool) {
return q.kqueue.InputHint()
}
// WriteClipboard returns the most recent text to be copied // WriteClipboard returns the most recent text to be copied
// to the clipboard, if any. // to the clipboard, if any.
func (q *Router) WriteClipboard() (string, bool) { func (q *Router) WriteClipboard() (string, bool) {