Don Kirkby Don Kirkby - 25 days ago 12
Android Question

Enter key handling in Libgdx TextField

I set up a stage with three TextFields in my libgdx app, and I get different behaviour in the desktop mode and the Android mode. On Android, typing the enter key moves the cursor to the next TextField. On the desktop, typing the enter key does nothing.

How can I make the cursor move consistently on both platforms? I want to be able to set the focus to another field when the user types enter. On Android, whatever I set the focus to, the default enter key behaviour then jumps the focus to the field after that.

Here's the code I'm currently using to move the cursor and clear the next field:

stage.addListener(new InputListener() {
@Override
public boolean keyUp(InputEvent event, int keycode) {
if (keycode == Input.Keys.ENTER) {
nextField();
}
return false;
}
});
Gdx.input.setInputProcessor(stage);
}

private void nextField() {
TextField nextField =
stage.getKeyboardFocus() == text1
? text2
: stage.getKeyboardFocus() == text2
? text3
: text1;
nextField.setText("");
stage.setKeyboardFocus(nextField);
}


I've tried cancelling the event or returning true from the handler methods, but the focus still moves after my code finishes.

My complete sample code is on GitHub.

Answer

TextField uses a private internal InputListener, that gets initialized in the constructor and cannot be easily overwritten. The relevant code that changes the focus is during the keyTyped method of this listener:

public boolean keyTyped (InputEvent event, char character) {
     [...]
     if ((character == TAB || character == ENTER_ANDROID) && focusTraversal)
         next(Gdx.input.isKeyPressed(Keys.SHIFT_LEFT) || Gdx.input.isKeyPressed(Keys.SHIFT_RIGHT));
     [...]
}

One easy solution would be to disable focus traversals all together and set a com.badlogic.gdx.scenes.scene2d.ui.TextFieldListener that automatically does the traversals instead:

TextField textField
textField.setFocusTraversal(false);
textField.setTextFieldListener(new TextFieldListener() {
    @Override
    public void keyTyped(TextField textField, char key) {
        if ((key == '\r' || key == '\n')){
            textField.next(Gdx.input.isKeyPressed(Keys.SHIFT_LEFT) || Gdx.input.isKeyPressed(Keys.SHIFT_RIGHT));
        }
    }
});

If you need to be able to enable and disable focus traversals using TextFields setFocusTraversal method, there would also be a quite hacky solution by wrapping the internal InputListener inside your own listener when it is added to the TextField (but I would not recommend this):

class MyTextField extends TextField{

class InputWrapper extends InputListener{
    private final InputListener l;

    public InputWrapper(InputListener l) {
        super();
        this.l = l;
    }

    @Override
    public boolean handle(Event e) {
        return l.handle(e);
    }

    @Override
    public boolean touchDown(InputEvent event, float x, float y,
            int pointer, int button) {
        return l.touchDown(event, x, y, pointer, button);
    }

    @Override
    public void touchUp(InputEvent event, float x, float y,
            int pointer, int button) {
        l.touchUp(event, x, y, pointer, button);
    }

    @Override
    public void touchDragged(InputEvent event, float x, float y,
            int pointer) {
        l.touchDragged(event, x, y, pointer);
    }

    @Override
    public boolean mouseMoved(InputEvent event, float x, float y) {
        return l.mouseMoved(event, x, y);
    }

    @Override
    public void enter(InputEvent event, float x, float y, int pointer,
            Actor fromActor) {
        l.enter(event, x, y, pointer, fromActor);
    }

    @Override
    public void exit(InputEvent event, float x, float y, int pointer,
            Actor toActor) {
        l.exit(event, x, y, pointer, toActor);
    }

    @Override
    public boolean scrolled(InputEvent event, float x, float y,
            int amount) {
        return l.scrolled(event, x, y, amount);
    }

    @Override
    public boolean keyDown(InputEvent event, int keycode) {
        return l.keyDown(event, keycode);
    }

    @Override
    public boolean keyUp(InputEvent event, int keycode) {
        return l.keyUp(event, keycode);
    }
    @Override
    public boolean keyTyped(InputEvent event, char character) {
        if (isDisabled()) {
            return false;
        } else if ((character == '\r' || character == '\n')){
            next(Gdx.input.isKeyPressed(Keys.SHIFT_LEFT) || Gdx.input.isKeyPressed(Keys.SHIFT_RIGHT));
            return true;
        }
        return l.keyTyped(event, character);
    }

}

public MyTextField(String text, Skin skin, String styleName) {
    super(text, skin, styleName);
}

public MyTextField(String text, Skin skin) {
    super(text, skin);
}

public MyTextField(String text, TextFieldStyle style) {
    super(text, style);
}

boolean initialized = false;
@Override
public boolean addListener (EventListener l) {
    if (!initialized) {
        if (!(l instanceof InputListener)) {
            throw new IllegalStateException();
        }
        initialized = true;
        return super.addListener(new InputWrapper((InputListener) l));
    }
    return super.addListener(l);
}
}

edit: On a second thought, you could also do this with the first solution by simply overwriting setFocusTraversal of the TextField and enabling/disabling your own listener during calls to this method.