I'm developing a challenge in which I must add a new circle to the screen every time the user presses a button. In addition to allowing the user to choose the size and color of the circle to be added. I managed to develop this functionality!
The next step is to select a circle, highlight it from the others and allow you to move it around the interface using the arrow keys. The first part I managed to develop, every time the user selects a circle with the mouse, a border appears on it, highlighting it. However, I am unable to develop the move functionality. Whenever I click an arrow key, the circle doesn't move, instead, what changes is the focus between the buttons.
Could you help me, please?
This is my FXMLDocumentController.java:
package addcircleapp;
import java.net.URL;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.Slider;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.ResourceBundle;
public class FXMLDocumentController implements Initializable {
@FXML
private Pane containerPane;
@FXML
private Button addCircleButton;
@FXML
private ColorPicker colorPicker;
@FXML
private Slider sizeSlider;
private List<Circle> circles = new ArrayList<>();
private Circle selectedCircle = null;
@FXML
private void handleAddCircleButton(ActionEvent event) {
Random rand = new Random();
int x = rand.nextInt(500);
int y = rand.nextInt(200);
Color selectedColor = colorPicker.getValue();
double selectedSize = sizeSlider.getValue();
Circle circle = new Circle(x, y, selectedSize, selectedColor);
circle.setOnMouseClicked((MouseEvent e) -> {
handleCircleClick(circle);
});
circle.setOnKeyPressed((KeyEvent e) -> {
handleCircleMovement(e, circle);
});
circles.add(circle);
containerPane.getChildren().add(circle);
System.out.println("New circle was created!");
}
@Override
public void initialize(URL url, ResourceBundle rb) {
// TODO
}
private void handleCircleClick(Circle clickedCircle) {
/*circles.forEach((circle) -> {
circle.setStroke(Color.TRANSPARENT);
});*/
if(selectedCircle != null){
selectedCircle.setStroke(Color.TRANSPARENT);
}
selectedCircle = clickedCircle;
selectedCircle.setStroke(Color.BLACK);
selectedCircle.setStrokeWidth(2.0);
}
private void handleCircleMovement(KeyEvent event, Circle circle){
if(event.getCode() == KeyCode.LEFT){
circle.setCenterX(circle.getCenterX() - 10);
} else if(event.getCode() == KeyCode.RIGHT){
circle.setCenterX(circle.getCenterX() + 10);
} else if(event.getCode() == KeyCode.UP){
circle.setCenterY(circle.getCenterY() - 10);
} else if(event.getCode() == KeyCode.DOWN){
circle.setCenterY(circle.getCenterY() + 10);
}
}
}
This is my FXMLDocument.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ColorPicker?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.layout.Pane?>
<Pane fx:id="containerPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="addcircleapp.FXMLDocumentController">
<children>
<Button fx:id="addCircleButton" layoutX="345.0" layoutY="362.0" mnemonicParsing="false" onAction="#handleAddCircleButton" text="AddCircle" />
<ColorPicker fx:id="colorPicker" layoutX="14.0" layoutY="361.0" />
<Slider fx:id="sizeSlider" blockIncrement="1.0" layoutX="180.0" layoutY="367.0" min="1.0" value="50.0" />
</children>
</Pane>
This is my AddCircleApp.java:
package addcircleapp;
import java.util.ArrayList;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AddCircleApp extends Application {
@Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
This is a screenshot of the interface: Interface
This is a gif of the problem: Problem
I want to move the selected circle around the interface, and not change the focus between the inputs present in the interface. I already tried to find the solution using ChatGPT, but I was unsuccessful.
Keyboard events are only delivered to the node with the keyboard focus. So when the circle is selected, you need to request that the circle has focus:
private void handleCircleClick(Circle clickedCircle) {
/*circles.forEach((circle) -> {
circle.setStroke(Color.TRANSPARENT);
});*/
if(selectedCircle != null){
selectedCircle.setStroke(Color.TRANSPARENT);
}
selectedCircle = clickedCircle;
selectedCircle.setStroke(Color.BLACK);
selectedCircle.setStrokeWidth(2.0);
selectedCircle.requestFocus();
}
Additionally, the default keyboard handlers on the scene will move focus to other nodes for certain key presses. So you probably want to consume the events for keys you are handling to prevent keyboard focus from moving:
private void handleCircleMovement(KeyEvent event, Circle circle){
if(event.getCode() == KeyCode.LEFT){
circle.setCenterX(circle.getCenterX() - 10);
event.consume();
} else if(event.getCode() == KeyCode.RIGHT){
circle.setCenterX(circle.getCenterX() + 10);
event.consume();
} else if(event.getCode() == KeyCode.UP){
circle.setCenterY(circle.getCenterY() - 10);
event.consume();
} else if(event.getCode() == KeyCode.DOWN){
circle.setCenterY(circle.getCenterY() + 10);
event.consume();
}
}
As an alternative solution, you might also consider adding the key listener to the scene, instead of to the individual circles. First, define a method in the controller to move the selected circle, and remove the existing key handler code:
public class FXMLDocumentController implements Initializable {
@FXML
private Pane containerPane;
@FXML
private Button addCircleButton;
@FXML
private ColorPicker colorPicker;
@FXML
private Slider sizeSlider;
private List<Circle> circles = new ArrayList<>();
private Circle selectedCircle = null;
@FXML
private void handleAddCircleButton(ActionEvent event) {
Random rand = new Random();
int x = rand.nextInt(500);
int y = rand.nextInt(200);
Color selectedColor = colorPicker.getValue();
double selectedSize = sizeSlider.getValue();
Circle circle = new Circle(x, y, selectedSize, selectedColor);
circle.setOnMouseClicked((MouseEvent e) -> {
handleCircleClick(circle);
});
// circle.setOnKeyPressed((KeyEvent e) -> {
// handleCircleMovement(e, circle);
// });
circles.add(circle);
containerPane.getChildren().add(circle);
System.out.println("New circle was created!");
}
@Override
public void initialize(URL url, ResourceBundle rb) {
// TODO
}
private void handleCircleClick(Circle clickedCircle) {
/*circles.forEach((circle) -> {
circle.setStroke(Color.TRANSPARENT);
});*/
if(selectedCircle != null){
selectedCircle.setStroke(Color.TRANSPARENT);
}
selectedCircle = clickedCircle;
selectedCircle.setStroke(Color.BLACK);
selectedCircle.setStrokeWidth(2.0);
selectedCircle.requestFocus();
}
public void moveSelectedCircle(double deltaX, double deltaY) {
if (selectedCircle != null) {
selectedCircle.setCenterX(selectedCircle.getCenterX() + deltaX);
selectedCircle.setCenterY(selectedCircle.getCenterY() + deltaY);
}
}
// private void handleCircleMovement(KeyEvent event, Circle circle){
// if(event.getCode() == KeyCode.LEFT){
// circle.setCenterX(circle.getCenterX() - 10);
// event.consume();
// } else if(event.getCode() == KeyCode.RIGHT){
// circle.setCenterX(circle.getCenterX() + 10);
// event.consume();
// } else if(event.getCode() == KeyCode.UP){
// circle.setCenterY(circle.getCenterY() - 10);
// event.consume();
// } else if(event.getCode() == KeyCode.DOWN){
// circle.setCenterY(circle.getCenterY() + 10);
// event.consume();
// }
//
// }
}
Then, modify the application code to retrieve a reference to the controller. Add a key pressed handler to the scene and invoke the moveSelectedCircle(...)
method on the controller with the appropriate parameter values:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AddCircleApp extends Application {
@Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("FXMLDocument.fxml"));
Parent root = loader.load();
FXMLDocumentController controller = loader.getController();
Scene scene = new Scene(root);
scene.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
double deltaX = switch (e.getCode()) {
case LEFT -> -10;
case RIGHT -> 10;
default -> 0;
};
double deltaY = switch (e.getCode()) {
case UP -> -10;
case DOWN -> 10;
default -> 0;
};
switch (e.getCode()) {
case LEFT, RIGHT, UP, DOWN -> {
controller.moveSelectedCircle(deltaX, deltaY);
e.consume();
}
}
});
stage.setScene(scene);
stage.show();
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}