Lisek
Lisek

Reputation: 783

Detect single key press in JavaFX

I have a problem of detecting a single key press in JavaFX. I have to detect arrow keys but each time I press any of those keys part of my code is being called multiple times. I realize that it is because AnimationTimer() is a loop thus this is the reason but I have no idea how to detect single key tap. Anyways, here is the code:

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

import java.util.HashSet;

public class Main extends Application {

    Stage window;
    static Scene scene;

    private static char[][] mapa = {
            {'X','X','X','X','X'},
            {'X','.','.','.','X'},
            {'X','.','M','.','X'},
            {'X','.','.','.','X'},
            {'X','X','X','X','X'}
    };

    private final int sizeX = 16;
    private final int sizeY = 16;

    static HashSet<String> currentlyActiveKeys;

    @Override
    public void start(Stage primaryStage) throws Exception {

        window = primaryStage;
        window.setTitle("Hello World");

        Group root = new Group();
        scene = new Scene(root);

        window.setScene(scene);

        Canvas canvas = new Canvas(512 - 64, 256);
        root.getChildren().add(canvas);

        prepareActionHandlers();

        GraphicsContext gc = canvas.getGraphicsContext2D();

        new AnimationTimer() {
            @Override
            public void handle(long now) {
                gc.clearRect(0,0,512,512);

                for(int i = 0; i < mapa.length; i++) {
                    for(int j = 0; j < mapa[i].length; j++) {
                        if(mapa[i][j] == 'X') {
                            gc.setLineWidth(5);
                            gc.setFill(Color.RED);
                            gc.fillRect(sizeX + i*sizeX, sizeY + j*sizeY, sizeX, sizeY);
                        } else if(mapa[i][j] == '.') {
                            gc.setLineWidth(5);
                            gc.setFill(Color.GREEN);
                            gc.fillRect(sizeX + i*sizeX, sizeY + j*sizeY, sizeX, sizeY);
                        } else if(mapa[i][j] == 'M') {
                            gc.setLineWidth(5);
                            gc.setFill(Color.BLACK);
                            gc.fillRect(sizeX + i*sizeX, sizeY + j*sizeY, sizeX, sizeY);
                        }
                    }
                }

                if(currentlyActiveKeys.contains("LEFT")) {
                    System.out.println("left");
                }

                if(currentlyActiveKeys.contains("RIGHT")) {
                    System.out.println("right");
                }

                if(currentlyActiveKeys.contains("UP")) {
                    System.out.println("up");
                }

                if(currentlyActiveKeys.contains("DOWN")) {
                    System.out.println("down");
                }
            }
        }.start();

        window.show();
    }

    private static void prepareActionHandlers()
    {
        currentlyActiveKeys = new HashSet<String>();
        scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event)
            {
                currentlyActiveKeys.add(event.getCode().toString());

            }
        });
        scene.setOnKeyReleased(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event)
            {
                currentlyActiveKeys.remove(event.getCode().toString());

            }
        });
    }

    public static void main(String[] args) {
        launch(args);
    }
}

When I press down arrow button (same goes with other keys of course), in the scope I am getting results like:

down
down
down
down
down
down

Obviously this happens as long as I press the button. Once I stop pressing it, printing ends. What I would love to achieve is that once I press a button (no matter how long I hold it) I will get down just once. I need this because I would like to update the color of my rectangles in the canvas.

Upvotes: 5

Views: 16310

Answers (3)

Arwan Khoiruddin
Arwan Khoiruddin

Reputation: 454

I know this is already a long age question. I just wanna share my finding after hours of struggling. I finally found one simple solution i.e. by adding clear function. Here's what I meant

if(currentlyActiveKeys.contains("LEFT")) {
    System.out.println("left");
}

if(currentlyActiveKeys.contains("RIGHT")) {
    System.out.println("right");
}

if(currentlyActiveKeys.contains("UP")) {
    System.out.println("up");
}

if(currentlyActiveKeys.contains("DOWN")) {
    System.out.println("down");
}

currentlyActiveKeys.clear();

This last line will clear the currently active key, thus it will prevent duplicate as mentioned in the original post

Upvotes: 1

jewelsea
jewelsea

Reputation: 159281

What is happening is that the OS has a kind of automatic typing function, such that when you hold a key down, it keeps generating key press events, even though you aren't really pressing the key more than once.

By adding both key press and key release handlers and a boolean state for each key, you can keep track of whether you have processed the key since it was pressed. You can then reset that processed state whenever the key is released so that the next time it is really pressed you can handle it.

Sample Application

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.util.HashMap;

public class Main extends Application {
    private HashMap<String, Boolean> currentlyActiveKeys = new HashMap<>();

    @Override
    public void start(Stage stage) throws Exception {
        final Scene scene = new Scene(new Group(), 100, 100);
        stage.setScene(scene);

        scene.setOnKeyPressed(event -> {
            String codeString = event.getCode().toString();
            if (!currentlyActiveKeys.containsKey(codeString)) {
                currentlyActiveKeys.put(codeString, true);
            }
        });
        scene.setOnKeyReleased(event -> 
            currentlyActiveKeys.remove(event.getCode().toString())
        );

        new AnimationTimer() {
            @Override
            public void handle(long now) {
                if (removeActiveKey("LEFT")) {
                    System.out.println("left");
                }

                if (removeActiveKey("RIGHT")) {
                    System.out.println("right");
                }

                if (removeActiveKey("UP")) {
                    System.out.println("up");
                }

                if (removeActiveKey("DOWN")) {
                    System.out.println("down");
                }
            }
        }.start();

        stage.show();
    }

    private boolean removeActiveKey(String codeString) {
        Boolean isActive = currentlyActiveKeys.get(codeString);

        if (isActive != null && isActive) {
            currentlyActiveKeys.put(codeString, false);
            return true;
        } else {
            return false;
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Upvotes: 5

Aaron
Aaron

Reputation: 175

You could declare a global boolean and toogle it like press = !press. Then every 2 seconds reset it back to true or something to that effect.

if(currentlyActiveKeys.contains("DOWN") && press) {
       press = !press;//sets it false
       System.out.println("down");
 }

Upvotes: 1

Related Questions