Search code examples
javaandroidinputlibgdx

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.


Solution

  • 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.