2
\$\begingroup\$

I have a button on screen, and I want the user to be able to dismiss it by either clicking on it, or hitting space. The button is constructed as follows:

button = new TextButton("RESUME", skin);
button.setVisible(false);

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

  @Override
  public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
    button.addAction(
      Actions.sequence(
        // send out of the screen
        Actions.moveTo(WORLD_WIDTH / 2 - button.getWidth() / 2, -button.getHeight(), 0.2f),
        Actions.hide(),
        Actions.run( () -> {
          resumeGame();
        }),
        Actions.removeActor()
      )
    );
  }
});

...
table.add(button);
stage.AddActor(table);
...

So if I make the button appear and click on it, everything is fine. I can repeat the sequence "button appear/click" at will, and each time the button will move offscreen to the bottom and will be removed from the table. I want the button to do exactly the same when the user hits space, so I did:

private void clickButton(TextButton button){
    InputEvent inputEvent = new InputEvent();
    inputEvent.setRelatedActor(button);

    inputEvent.setType(InputEvent.Type.touchDown);
    button.fire(inputEvent);

    inputEvent.setType(InputEvent.Type.touchUp);
    button.fire(inputEvent);
}

// then:

stage.addListener(new InputListener(){
  @Override
  public boolean keyDown(InputEvent event, int keycode) {

    if (keycode == Input.Keys.SPACE){
      clickButton(button);
      return true;
    }
}

Now when the button is shown and I hit space, it looks like it works, but if I make the button appear again, it's shown briefly then it disappears (it's removed from the table). It's like the final Actions.removeActor() in the button listener is not executed when the button is "clicked" using space, and it's then executed the next time when the button appears again, making it disappear immediately.

EDIT: this is indeed the case. I put a debug print statement right before the Actions.removeActor() action, and upon mouse click the text is printed, but using clickButton() it's not (and it's printed instead when the button is shown again and readded to the table).

Am I doing something wrong here? What's the "correct" way to click a button programmatically (yes, I've already seen https://stackoverflow.com/questions/25870645/programatically-perform-click-on-actor-libgdx)

\$\endgroup\$
  • \$\begingroup\$ Perhaps try moving Actions.removeActor() to before Actions.hide(). Otherwise see my answer for another possible solution. \$\endgroup\$ – Charanor Jul 28 '17 at 13:30
  • \$\begingroup\$ I tried that, but then it looks that all actions after removeActor() are not executed. The solution in your answer works fine. \$\endgroup\$ – persson Jul 28 '17 at 13:42
  • \$\begingroup\$ Spoke too soon. While space now works, with ChangeListener it looks like the button isn't getting mouse input. Checking to see if I'm missing something. \$\endgroup\$ – persson Jul 28 '17 at 14:01
  • \$\begingroup\$ You can have both the InputListener and the ChangeListener :) Then it should catch both just fine. \$\endgroup\$ – Charanor Jul 28 '17 at 14:10
  • \$\begingroup\$ Isn't that way too cumbersome for such a simple thing? \$\endgroup\$ – persson Jul 28 '17 at 14:13
2
\$\begingroup\$

You should use the Button#toggle() method together with Button#setProgrammaticChangeEvents(true). From the docs:

Toggles the checked state. This method changes the checked state, which fires a ChangeEvent (if programmatic change events are enabled), so can be used to simulate a button click.

Your code would be:

// When you create the button
button.setProgrammaticChangeEvents(true);

// In your listener
if (keycode == Input.Keys.SPACE){
    button.toggle();
    return true;
}

Note that you might need to change your InputListener to a ChangeListener. In that case your listener code will be this:

new ChangeListener() {
    @Override
    public void changed(ChangeEvent event, Actor actor) {
        // Your code in here.
    }
};

Looking at the docs it seems like toggle() fires a ChangeEvent but actual mouse input fires an InputEvent. There is no built-in listener that catches both of these events so we have to create one ourselves. This can be really simple as you don't need all of the advanced parameters:

public abstract class InputChangeListener implements EventListener {
    @Override
    public boolean handle(final Event event) {
        // We catch InputEvent and ChangeEvent
        if (!(event instanceof InputEvent || event instanceof ChangeEvent))
            return false;
        return handleEvent(event);
    }

    public abstract boolean handleEvent(final Event event);
}

Then do:

new InputChangeListener() {
    @Override
    public boolean handleEvent(Event event) {
        // Code here
    }
};

This should now work for both manual toggle and mouse input.

\$\endgroup\$
  • \$\begingroup\$ Doing BOTH changes (setting setProgrammaticChangeEvents(true) and changing the listener to a ChangeListener seems to do the trick. Still, I wonder why it didn't work originally. Thanks! \$\endgroup\$ – persson Jul 28 '17 at 13:41
  • \$\begingroup\$ I'm afraid the check for event instanceof InputEvent has to be refined, otherwise it would fire even just for hovering the mouse pointer over the button. I'm working on a solution. \$\endgroup\$ – persson Jul 28 '17 at 14:43
0
\$\begingroup\$

So after another round of adding features, here's what I came up with. Basically, when the "pause" button is shown on screen, I want the user to be able to do the following:

  • Click on it to dismiss the button and resume game
  • hit SPACE to dismiss the button and resume game
  • hit ESC to dismiss the button and go back to main menu

To get the same button movement regardless of whether the user clicks or hits a key, when the user hits a key I simulate a programmatic button click. However, if the key is ESC, the action taken has to be different, so the listener has to be modifiable at run time.

Skeleton code:

// listener that catches mouse clicks and artificially generated ChangeEvent
// action taken can be set dynamically with a Runnable
private class ModifiableListener implements EventListener {

  private Runnable runnable;
  private TextButton button;

  public ModifiableListener(TextButton button, Runnable runnable){
    super();
    this.button = button;
    this.runnable = runnable;
  }

  public void setRunnable(Runnable runnable){
    this.runnable = runnable;
  }

  @Override
  public boolean handle(final Event event) {
    // We catch InputEvent and ChangeEvent
    if ((event instanceof InputEvent && (((InputEvent)event).getType() == InputEvent.Type.touchDown  || ((InputEvent)event).getType() == InputEvent.Type.touchUp))
      || event instanceof ChangeListener.ChangeEvent) {

      if (event instanceof InputEvent && ((InputEvent)event).getType() == InputEvent.Type.touchDown){
        return true;
      }
      this.button.addAction(getDisappearAction());
      return true;
    }
    return false;
  }

  private Action getDisappearAction(){
    return Actions.sequence(
      Actions.moveTo(WORLD_WIDTH / 2 - this.button.getWidth() / 2, -this.button.getHeight(), 0.2f),
      Actions.hide(),
      Actions.run(this.runnable),
      Actions.touchable(Touchable.disabled),
      Actions.removeActor()
    );
  }
}

// then in the screen code...

Runnable resumeAfterPause = () -> { resumeGame(); };
Runnable goToStartScreen = () -> { saveGame(); game.setScreen(menuScreen); };

TextButton pauseButton = new TextButton("PAUSED", skin);
pauseButton.setVisible(false);
pauseButton.setTouchable(Touchable.disabled);
ModifiableListener pausedListener = new ModifiableListener(pauseButton, resumeAfterPause);
pauseButton.addListener(pausedListener);
pauseButton.setProgrammaticChangeEvents(true);

table.add(pauseButton);
stage.add(table);

stage.addListener(new InputListener(){

  public boolean keyDown(InputEvent event, int keycode) {

    if (isPaused) {
      switch (keycode) {
        case Input.Keys.ESCAPE:
          // wait until button is shown and usable
          if (pauseButton.isTouchable()) {
            pausedListener.setRunnable(goToStartScreen);
            pauseButton.toggle();
            return true;
          }
          break;

        case Input.Keys.SPACE:
          if (pauseButton.isTouchable()) {
            pausedListener.setRunnable(resumeAfterPause);
            pauseButton.toggle();
            return true;
          }
          break;

// rest of code...

The key bit is that the action to run in the listener can be modified just before the simulated button click is fired, depending on what the user requests. Everything works fine, however the button doesn't "click" (does not draw itself pressed/clicked upon touchDown).

This still looks quite bulky and cumbersome to me, if someone has better suggestions, I'm all ears.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.