setOn"Fast"Click - JavaFx - javafx

I have a Button that I can move it from the screen, when clicking it has an action. The problem is, when I do Drag'n Drop the click event is called when I release the mouse on, I tried it:
setOnMouseClicked
setOnAction
setOnMousePressed
How can I do to just call the click function when it is a quick click, something like Android times that can differentiate because we have setOnLongClick, so differentiated when I have doing Drag'n Drop and when I really want to click?
Ex:
To move, do:
button.setOnMouseDragged(e -> {
//code move
});
To eventClick:
button.setOnMouseClicked/ Action / MousePressed (e -> {
//call method
});
But when I drop it, it calls setOnMouseClicked / Action / MousePressed, what I want is for it to just call in case I give a quick click, when I drop the drag'n drop do not call.

One option is to keep track of whether or not the Button was dragged; if not, only then execute the code in the onAction handler. Here's an example:
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
public class Main extends Application {
private Point2D origin;
private boolean wasDragged;
#Override
public void start(Stage primaryStage) {
Button button = new Button("Drag me!");
button.setOnAction(this::onAction);
button.setOnMousePressed(this::onMousePressed);
button.setOnMouseDragged(this::onMouseDragged);
button.setOnMouseReleased(this::onMouseReleased);
primaryStage.setScene(new Scene(new Group(button), 800, 600));
primaryStage.show();
}
private void onAction(ActionEvent event) {
event.consume();
if (!wasDragged) {
System.out.println("onAction");
}
}
private void onMousePressed(MouseEvent event) {
event.consume();
origin = new Point2D(event.getX(), event.getY());
System.out.println("onMousePressed");
}
private void onMouseDragged(MouseEvent event) {
event.consume();
wasDragged = true;
Button source = (Button) event.getSource();
source.setTranslateX(source.getTranslateX() + event.getX() - origin.getX());
source.setTranslateY(source.getTranslateY() + event.getY() - origin.getY());
}
private void onMouseReleased(MouseEvent event) {
event.consume();
origin = null;
wasDragged = false;
System.out.println("onMouseReleased");
System.out.println();
}
}
Unfortunately, I can't find documentation guaranteeing the onAction handler is always called before the onMouseReleased handler, but this worked on both Java 8u202 and JavaFX 11.0.2 when I tried it.

Related

Delegate mouse events to all children in a JavaFX StackPane

I'm trying to come up with a solution to allow multiple Pane nodes handle mouse events independently when assembled into a StackPane
StackPane
Pane 1
Pane 2
Pane 3
I'd like to be able to handle mouse events in each child, and the first child calling consume() stops the event going to the next child.
I'm also aware of setPickOnBounds(false), but this does not solve all cases as some of the overlays will be pixel based with Canvas, i.e. not involving the scene graph.
I've tried various experiments with Node.fireEvent(). However these always lead to recursion ending in stack overflow. This is because the event is propagated from the root scene and triggers the same handler again.
What I'm looking for is some method to trigger the event handlers on the child panes individually without the event travelling through its normal path.
My best workaround so far is to capture the event with a filter and manually invoke the handler. I'd need to repeat this for MouseMoved etc
parent.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
for (Node each : parent.getChildren()) {
if (!event.isConsumed()) {
each.getOnMouseClicked().handle(event);
}
}
event.consume();
});
However this only triggers listeners added with setOnMouseClicked, not addEventHandler, and only on that node, not child nodes.
Another sort of solution is just to accept JavaFX doesn't work like this, and restructure the panes like this, this will allow normal event propagation to take place.
Pane 1
Pane 2
Pane 3
Example
import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class EventsInStackPane extends Application {
public static void main(String[] args) {
launch(args);
}
private static class DebugPane extends Pane {
public DebugPane(Color color, String name) {
setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY)));
setOnMouseClicked(event -> {
System.out.println("setOnMouseClicked " + name + " " + event);
});
addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventHandler " + name + " " + event);
});
addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventFilter " + name + " " + event);
});
}
}
#Override
public void start(Stage primaryStage) throws Exception {
DebugPane red = new DebugPane(Color.RED, "red");
DebugPane green = new DebugPane(Color.GREEN, "green");
DebugPane blue = new DebugPane(Color.BLUE, "blue");
setBounds(red, 0, 0, 400, 400);
setBounds(green, 25, 25, 350, 350);
setBounds(blue, 50, 50, 300, 300);
StackPane parent = new StackPane(red, green, blue);
eventHandling(parent);
primaryStage.setScene(new Scene(parent));
primaryStage.show();
}
private void eventHandling(StackPane parent) {
parent.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (!event.isConsumed()) {
for (Node each : parent.getChildren()) {
Event copy = event.copyFor(event.getSource(), each);
parent.fireEvent(copy);
if (copy.isConsumed()) {
break;
}
}
}
event.consume();
});
}
private void setBounds(DebugPane panel, int x, int y, int width, int height) {
panel.setLayoutX(x);
panel.setLayoutY(y);
panel.setPrefWidth(width);
panel.setPrefHeight(height);
}
}
Using the hint from #jewelsea I was able to use a custom chain. I've done this from a "catcher" Pane which is added to the front of the StackPane. This then builds a chain using all the children, in reverse order, excluding itself.
private void eventHandling(StackPane parent) {
Pane catcher = new Pane() {
#Override
public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
EventDispatchChain chain = super.buildEventDispatchChain(tail);
for (int i = parent.getChildren().size() - 1; i >= 0; i--) {
Node child = parent.getChildren().get(i);
if (child != this) {
chain = chain.prepend(child.getEventDispatcher());
}
}
return chain;
}
};
parent.getChildren().add(catcher);
}

JavaFX bounds issue?

So, using JAVAfx, which I painfully downloaded, I need to be able to move this ball, but not let it leave the bounds. Right now, I have it all set up, it just leaves the area. What am i doing wrong? Any tutors don't know anything about JavaFX, so I'm having trouble getting past this. Right now I tried to work it out, but keep getting this error Cannot Make a Static Reference to Bound
'''
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.geometry.Bounds;
public class MoveBall extends Application {
// create buttons
Button left = new Button();
Button right = new Button();
Button up = new Button();
Button down = new Button();
Circle ball = new Circle(30, 30, 30);
// action event class
EventHandler<ActionEvent> event = new EventHandler<ActionEvent>() {
// handle them
#Override
public void handle(ActionEvent event) {
// movement of ball
if (event.getSource().equals(left) && ball.getLayoutX() >= (Bounds.getMaxX() + ball.getRadius())) {
ball.setCenterX(ball.getCenterX() - 5);
}
if (event.getSource().equals(right)) {
ball.setCenterX(ball.getCenterX() + 5);
}
if (event.getSource().equals(up)) {
ball.setCenterY(ball.getCenterY() - 5);
}
if (event.getSource().equals(down) && ball.getCenterY() < 400)) {
ball.setCenterY(ball.getCenterY() + 5);
}
}
};
// main method
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Ball Movement");
ball.setStroke(Color.BLACK);
ball.setFill(null);
// button location
left.setLayoutX(100);
left.setLayoutY(210);
left.setText("Left");
// button event
left.setOnAction(event);
// similar for all button
right.setLayoutX(150);
right.setLayoutY(210);
right.setText("Right");
right.setOnAction(event);
// button location
up.setLayoutX(200);
up.setLayoutY(210);
up.setText("Up");
// button event
up.setOnAction(event);
// similar for all button
down.setLayoutX(250);
down.setLayoutY(210);
down.setText("Down");
down.setOnAction(event);
Group root = new Group();
// add to a group
root.getChildren().add(left);
root.getChildren().add(right);
root.getChildren().add(up);
root.getChildren().add(down);
root.getChildren().add(ball);
primaryStage.setScene(new Scene(root, 400, 250, Color.WHITE));
// show the scene
primaryStage.show();
}
}
'''

Disable ComboBox dropdown on hitting F4

Currently JavaFX provides a feature to dropdown the comboBox on hitting F4. We want to disable that feature and process other functions for F4. On first instance I thought this is pretty straight forward. My idea is that I will add a key event filter and consume it when F4 is pressed.
But unfortunately that didnt worked !! Upon investigation, I noticed that there is a part of code in ComboBoxPopupControl to handle the key event, which is set as KeyEvent.ANY filter. The strange part is that they consume the event after showing/hiding.
The part of code is as below:
private void handleKeyEvent(KeyEvent ke, boolean doConsume) {
// When the user hits the enter or F4 keys, we respond before
// ever giving the event to the TextField.
if (ke.getCode() == KeyCode.ENTER) {
setTextFromTextFieldIntoComboBoxValue();
if (doConsume && comboBoxBase.getOnAction() != null) {
ke.consume();
} else {
forwardToParent(ke);
}
} else if (ke.getCode() == KeyCode.F4) {
if (ke.getEventType() == KeyEvent.KEY_RELEASED) {
if (comboBoxBase.isShowing()) comboBoxBase.hide();
else comboBoxBase.show();
}
ke.consume(); // we always do a consume here (otherwise unit tests fail)
}
}
This makes me totally helpless, as now I can no more control this part of event chain by merely consuming the filters/handlers. None of the below filters helped me to stop showing the dropdown.
comboBox.addEventFilter(KeyEvent.ANY, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
comboBox.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
comboBox.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
The only way I can stop it is to consume the event on its parent and not allowing to delegate to ComboBox. But this is definetly an overhead, with already tens of ComboBox(es) across the application and many more to come.
My question is:
Why did they implemented a feature which is tightly integrated not allowing the user to disable it?
Is there any alternate that I can implement on ComboBox level to stop showing/hiding the dropdown when F4 is pressed.
I tried the below approach to make it work. But I am not sure how much i can rely on Timeline based solution :(
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ComboBoxF4_Demo extends Application {
Timeline f4PressedTimeline = new Timeline(new KeyFrame(Duration.millis(100), e1 -> {
}));
#Override
public void start(Stage stage) throws Exception {
HBox root = new HBox();
root.setSpacing(15);
root.setPadding(new Insets(25));
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 600, 600);
stage.setScene(scene);
final ComboBox<String> comboBox = new ComboBox<String>() {
#Override
public void show() {
if (f4PressedTimeline.getStatus() != Animation.Status.RUNNING) {
super.show();
}
}
};
comboBox.setItems(FXCollections.observableArrayList("One", "Two", "Three"));
comboBox.addEventFilter(KeyEvent.ANY, e -> {
if (e.getCode() == KeyCode.F4) {
if (e.getEventType() == KeyEvent.KEY_RELEASED) {
f4PressedTimeline.playFromStart();
}
}
});
// NONE OF THE BELOW FILTERS WORKED :(
/*comboBox.addEventFilter(KeyEvent.ANY, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
comboBox.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
comboBox.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.F4) {
e.consume(); // Didn't stopped showing the drop down
}
});
*/
root.getChildren().addAll(comboBox);
stage.show();
}
}
As mentioned by #kleopatra in the question comments, consuming an event does not stop its propagation within the same "level" of the same phase. In other words, all the event filters registered with the ComboBox (for the EventType and its supertypes) will still be notified even if one of them consumes the event. Then there's also the problem of changing the default behavior of controls which may be unexpected, and unappreciated, by your end users.
If you still want to change the control's behavior, and you don't find consuming the event on an ancestor satisfactory, you can intercept the event in a custom EventDispatcher instead of an event filter:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage primaryStage) {
var comboBox = new ComboBox<String>();
for (int i = 0; i < 20; i++) {
comboBox.getItems().add("Item #" + i);
}
comboBox.getSelectionModel().select(0);
var oldDispatcher = comboBox.getEventDispatcher();
comboBox.setEventDispatcher((event, tail) -> {
if (event.getEventType() == KeyEvent.KEY_RELEASED
&& ((KeyEvent) event).getCode() == KeyCode.F4) {
return null; // returning null indicates the event was consumed
}
return oldDispatcher.dispatchEvent(event, tail);
});
primaryStage.setScene(new Scene(new StackPane(comboBox), 500, 300));
primaryStage.show();
}
}

How to Fix 'JavaFX Dialogbox taking user input' problem

I am creating a JavaFx dialog box and I have written to a large extent the code. My problem is how to display the error message if a user enters the invalid input. I know I have to use a while loop somewhere but not sure where because of the structure of JavaFx dialog box. Second problem is if the user enters the right input, say 1 for yes, I would want to call a function to carry out a task.
The code I have written brings up the pop up box and prints the consequence of the user input to the console.
public static void AnotherMatch() {
//creates a popUp window
Stage popUp = new Stage();
// makes sure no changes are made in the Main window while this window is open
popUp.initModality(Modality.APPLICATION_MODAL);
popUp.setTitle("New Game");
popUp.setMinWidth(400);
popUp.setHeight(200);
TextPanel textPanel2 = new TextPanel();
TextField nameInput = new TextField();
Button button = new Button("Enter");
//label explains how the game works
Label displayLabel = new Label();
displayLabel.setText("Do you want to play another match: Yes: 1 -- No: 2");
button.setOnAction(e -> isChoice(nameInput, nameInput.getText()));
//vbox stores label and is set in centre
VBox windowDisplay = new VBox();
windowDisplay.setStyle("-fx-background-color:Wheat"); //background colour is set
windowDisplay.getChildren().addAll(displayLabel,nameInput, button);
windowDisplay.setAlignment(Pos.CENTER);
Scene scene = new Scene(windowDisplay);
popUp.setScene(scene);
popUp.showAndWait(); }
Code for isChoice function
private static boolean isChoice(TextField nameInput, String message) {
// TODO Auto-generated method stub
try {
int choice = Integer.parseInt(nameInput.getText());
if(choice == 1) {
System.out.println("I want to play game again");
return true;
}
else if (choice == 2){
System.out.println("I want to stop playing");
return false;
}
else {
System.out.println("Invalid entry");
return false;
}
}
catch(NumberFormatException e){
System.out.println(message + " Invalid .Enter 1 for yes and 2 for no");
return false;
}
}
The user should be asked to enter yes or no. If the user invalid input, an error message should be displayed to the user and the answer asked again until they answer yes or no.
One way you can do is using Bindings to disable the Button unless the TextField contains Yes or No(ignore case).
Demo App using Bindings.
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class JavaFXApplication357 extends Application
{
#Override
public void start(Stage primaryStage)
{
TextField textField = new TextField();
Button btn = new Button();
btn.disableProperty().bind(Bindings.notEqualIgnoreCase("yes", textField.textProperty()).and(Bindings.notEqualIgnoreCase("no", textField.textProperty())));
btn.setText("Say 'Hello World'");
btn.setOnAction((ActionEvent event) -> {
System.out.println("Hello World!");
});
StackPane root = new StackPane(new VBox(textField, btn));
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args)
{
launch(args);
}
}

How to pass ScrollPane event KEY_PRESSED, KeyCode.LEFT to parent pane?

I have a StackPane with a single child - ScrollPane. I am trying to handle KEY_PRESSED, KeyCode.LEFT (just an example, I want to handle every arrow key) event on StackPane.
As I am concerned the particular event is consumed on ScrollPane. I would like to prevent that but cannot find any reasonable way.
import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventDispatcher;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ScrollPaneDispatcherApp extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
StackPane stackPane = new StackPane();
stackPane.setStyle("-fx-backgound-color: red");
ScrollPane scrollPane = new ScrollPane();
scrollPane.setStyle("-fx-background-color: yellow");
stackPane.getChildren().add(scrollPane);
Scene scene = new Scene(stackPane);
stage.setScene(scene);
stage.show();
scrollPane.requestFocus();
EventDispatcher sceneEventDispatcher = scene.getEventDispatcher();
EventDispatcher stackPaneEventDispatcher = stackPane.getEventDispatcher();
EventDispatcher scrollPaneEventDispatcher = scrollPane.getEventDispatcher();
scene.setEventDispatcher((event, tail) -> {
if (KeyEvent.ANY.equals(event.getEventType().getSuperType())) {
System.out.println("DISPATCH\tScene\t\tevent=" + event.getEventType());
}
return sceneEventDispatcher.dispatchEvent(event, tail);
});
stackPane.setEventDispatcher((event, tail) -> {
if (KeyEvent.ANY.equals(event.getEventType().getSuperType())) {
System.out.println("DISPATCH\tStackPane\tevent=" + event.getEventType());
}
return stackPaneEventDispatcher.dispatchEvent(event, tail);
});
scrollPane.setEventDispatcher((event, tail) -> {
if (KeyEvent.ANY.equals(event.getEventType().getSuperType())) {
System.out.println("DISPATCH\tScrollPane\tevent=" + event.getEventType());
}
Event eventToDispatch = scrollPaneEventDispatcher.dispatchEvent(event, tail);
if (KeyEvent.KEY_PRESSED.equals(event.getEventType())) {
if (KeyCode.LEFT.equals(((KeyEvent) event).getCode()) || KeyCode.RIGHT.equals(((KeyEvent) event).getCode())) {
if (eventToDispatch == null) {
return event;
}
}
}
return eventToDispatch;
});
scene.addEventFilter(KeyEvent.ANY,
event -> System.out.println("FILTER\t\tScene\t\tevent=" + event.getEventType()));
stackPane.addEventFilter(KeyEvent.ANY,
event -> System.out.println("FILTER\t\tStackPane\tevent=" + event.getEventType()));
scrollPane.addEventFilter(KeyEvent.ANY,
event -> System.out.println("FILTER\t\tScrollPane\tevent=" + event.getEventType()));
scene.addEventHandler(KeyEvent.ANY,
event -> System.out.println("HANDLER\t\tScene\t\tevent=" + event.getEventType()));
stackPane.addEventHandler(KeyEvent.ANY,
event -> System.out.println("HANDLER\t\tStackPane\tevent=" + event.getEventType()));
scrollPane.addEventHandler(KeyEvent.ANY,
event -> System.out.println("HANDLER\t\tScrollPane\tevent=" + event.getEventType()));
}
}
Proposed solution overrides LEFT, RIGHT arrows KEY_PRESSED events consumption behaviour for ScrollPane. The clue is the new EventDispatcher for ScrollPane. Rest of the code is only for debugging purposes.
By default, the ScrollPane will consume the key press event and the StackPane will not.
If you want to intercept a key press using a parent pane event handler, then you should use an event filter (https://docs.oracle.com/javafx/2/events/filters.htm) to do so (to intercept the event during the capturing phase rather than the bubbling phase).
Read up on event processing (https://docs.oracle.com/javafx/2/events/processing.htm) if you need to understand the capturing versus bubbling concepts.
From docs.oracle.com/javafx/2/events/filters.htm : Event filters enable the parent node to provide common processing for its child nodes or to intercept an event and prevent child nodes from acting on the event

Resources