Waiting a keyevent [duplicate] - java

I want to write a little game where I can move a ball on a JavaFX Panel using the W, A, S, D keys.
I have a getPosX() and setPosX() but I don't know how to write a KeyListener which will e.g. calculate setPosX(getPosX()+1) if I press D.
What do I have to do?

From a JavaRanch Forum post.
Key press and release handlers are added on the scene and update movement state variables recorded in the application. An animation timer hooks into the JavaFX pulse mechanism (which by default will be capped to fire an event 60 times a second) - so that is a kind of game "loop". In the timer the movement state variables are checked and their delta actions applied to the character position - which in effect moves the character around the screen in response to key presses.
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.*;
import javafx.scene.image.*;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
* Hold down an arrow key to have your hero move around the screen.
* Hold down the shift key to have the hero run.
*/
public class Runner extends Application {
private static final double W = 600, H = 400;
private static final String HERO_IMAGE_LOC =
"http://icons.iconarchive.com/icons/raindropmemory/legendora/64/Hero-icon.png";
private Image heroImage;
private Node hero;
boolean running, goNorth, goSouth, goEast, goWest;
#Override
public void start(Stage stage) throws Exception {
heroImage = new Image(HERO_IMAGE_LOC);
hero = new ImageView(heroImage);
Group dungeon = new Group(hero);
moveHeroTo(W / 2, H / 2);
Scene scene = new Scene(dungeon, W, H, Color.FORESTGREEN);
scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
switch (event.getCode()) {
case UP: goNorth = true; break;
case DOWN: goSouth = true; break;
case LEFT: goWest = true; break;
case RIGHT: goEast = true; break;
case SHIFT: running = true; break;
}
}
});
scene.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
switch (event.getCode()) {
case UP: goNorth = false; break;
case DOWN: goSouth = false; break;
case LEFT: goWest = false; break;
case RIGHT: goEast = false; break;
case SHIFT: running = false; break;
}
}
});
stage.setScene(scene);
stage.show();
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
int dx = 0, dy = 0;
if (goNorth) dy -= 1;
if (goSouth) dy += 1;
if (goEast) dx += 1;
if (goWest) dx -= 1;
if (running) { dx *= 3; dy *= 3; }
moveHeroBy(dx, dy);
}
};
timer.start();
}
private void moveHeroBy(int dx, int dy) {
if (dx == 0 && dy == 0) return;
final double cx = hero.getBoundsInLocal().getWidth() / 2;
final double cy = hero.getBoundsInLocal().getHeight() / 2;
double x = cx + hero.getLayoutX() + dx;
double y = cy + hero.getLayoutY() + dy;
moveHeroTo(x, y);
}
private void moveHeroTo(double x, double y) {
final double cx = hero.getBoundsInLocal().getWidth() / 2;
final double cy = hero.getBoundsInLocal().getHeight() / 2;
if (x - cx >= 0 &&
x + cx <= W &&
y - cy >= 0 &&
y + cy <= H) {
hero.relocate(x - cx, y - cy);
}
}
public static void main(String[] args) { launch(args); }
}
On filters, handlers and focus
To receive key events, the object that the event handlers are set on must be focus traversable. This example sets handlers on the scene directly, but if you were to set handlers on the pane instead of the scene, it would need to be focus traversable and have focus.
If you want a global intercept point to override or intercept events that are to be routed through the in-built event handlers which will consume events you want (e.g. buttons and text fields), you can have an event filter on the scene rather than a handler.
To better understand the difference between a handler and a filter, make sure that you study and understand the event capturing and bubbling phases as explained in the JavaFX event tutorial.
Generic input handler
Please ignore the rest of this answer if the information already provided is sufficient for your purposes.
While the above solution is sufficient to answer this question, if interested, a more sophisticated input handler (with a more general and separated, input and update handling logic), can be found in this demo breakout game:
Breakout input handler.
Example generic input handler from the sample breakout game:
class InputHandler implements EventHandler<KeyEvent> {
final private Set<KeyCode> activeKeys = new HashSet<>();
#Override
public void handle(KeyEvent event) {
if (KeyEvent.KEY_PRESSED.equals(event.getEventType())) {
activeKeys.add(event.getCode());
} else if (KeyEvent.KEY_RELEASED.equals(event.getEventType())) {
activeKeys.remove(event.getCode());
}
}
public Set<KeyCode> getActiveKeys() {
return Collections.unmodifiableSet(activeKeys);
}
}
While an ObservableSet with an appropriate set change listener could be used for the set of active keys, I have used an accessor which returns an unmodifiable set of keys which were active at a snapshot in time, because that is what I was interested in here rather than observing changes to the set of active keys in real-time.
If you want to keep track of the order in which keys are pressed, a Queue, List, or TreeSet can be used rather than a Set (for example, with a TreeSet ordering events on the time of keypress, the most recent key pressed would be the last element in the set).
Example generic input handler usage:
Scene gameScene = createGameScene();
// register the input handler to the game scene.
InputHandler inputHandler = new InputHandler();
gameScene.setOnKeyPressed(inputHandler);
gameScene.setOnKeyReleased(inputHandler);
gameLoop = createGameLoop();
// . . .
private AnimationTimer createGameLoop() {
return new AnimationTimer() {
public void handle(long now) {
update(now, inputHandler.getActiveKeys());
if (gameState.isGameOver()) {
this.stop();
}
}
};
}
public void update(long now, Set<KeyCode> activeKeys) {
applyInputToPaddle(activeKeys);
// . . . rest of logic to update game state and view.
}
// The paddle is sprite implementation with
// an in-built velocity setting that is used to
// update its position for each frame.
//
// on user input, The paddle velocity changes
// to point in the correct predefined direction.
private void applyInputToPaddle(Set<KeyCode> activeKeys) {
Point2D paddleVelocity = Point2D.ZERO;
if (activeKeys.contains(KeyCode.LEFT)) {
paddleVelocity = paddleVelocity.add(paddleLeftVelocity);
}
if (activeKeys.contains(KeyCode.RIGHT)) {
paddleVelocity = paddleVelocity.add(paddleRightVelocity);
}
gameState.getPaddle().setVelocity(paddleVelocity);
}

Scene myScene = new Scene();
KeyCombination cntrlZ = new KeyCodeCombination(KeyCode.Z, KeyCodeCombination.CONTROL_DOWN);
myScene.setOnKeyPressed(new EventHandler<KeyEvent>(){
#Override
public void handle(KeyEvent event) {
if(contrlZ.match(event)){
//Do something
}
}
});

using JNativeHook:
https://github.com/kwhat/jnativehook
<!-- https://mvnrepository.com/artifact/com.1stleg/jnativehook -->
<dependency>
<groupId>com.1stleg</groupId>
<artifactId>jnativehook</artifactId>
<version>2.1.0</version>
</dependency>
private void initKeyListener(Stage primaryStage){
/* Note: JNativeHook does *NOT* operate on the event dispatching thread.
* Because Swing components must be accessed on the event dispatching
* thread, you *MUST* wrap access to Swing components using the
* SwingUtilities.invokeLater() or EventQueue.invokeLater() methods.
*/
try {
GlobalScreen.registerNativeHook();
} catch (NativeHookException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
GlobalScreen.addNativeKeyListener(new NativeKeyListener() {
public void nativeKeyPressed(NativeKeyEvent e) {
if ( (e.getModifiers() & NativeKeyEvent.CTRL_MASK) != 0
&& (e.getModifiers() & NativeKeyEvent.ALT_MASK) != 0
&& (e.getKeyCode() == NativeKeyEvent.VC_B)){
logger.debug("key :Hide");
primaryStage.hide();
}
if ( (e.getModifiers() & NativeKeyEvent.CTRL_MASK) != 0
&& (e.getModifiers() & NativeKeyEvent.SHIFT_MASK) != 0
&& (e.getModifiers() & NativeKeyEvent.ALT_MASK) != 0
&& (e.getKeyCode() == NativeKeyEvent.VC_B)){
logger.debug("key :Show");
primaryStage.show();
}
//System.out.println("Key Pressed: " + NativeKeyEvent.getKeyText(e.getKeyCode()));
}
public void nativeKeyReleased(NativeKeyEvent e) {
//System.out.println("Key Released: " + NativeKeyEvent.getKeyText(e.getKeyCode()));
}
public void nativeKeyTyped(NativeKeyEvent e) {
//System.out.println("Key Typed: " + NativeKeyEvent.getKeyText(e.getKeyCode()));
}
});
/*
GlobalScreen.addNativeMouseListener(new NativeMouseListener() {
#Override
public void nativeMouseReleased(NativeMouseEvent arg0) {
// TODO Auto-generated method stub
System.out.println("MouseReleased()");
}
#Override
public void nativeMousePressed(NativeMouseEvent arg0) {
// TODO Auto-generated method stub
System.out.println("MousePressed()");
}
#Override
public void nativeMouseClicked(NativeMouseEvent arg0) {
// TODO Auto-generated method stub
System.out.println("MouseClicked()");
}
});
*/
}

Related

How can I get specific fields from an AnimationTimer object?

I've written a simple game using JavaFX for the GUI and AnimationTimer for the game itself, but I've found that any fields that update within the AnimationTimer object can't be found outside of the object. I've managed to continuously record how long a game runs, and the score of that game, but can't find a way to get these values out of the AnimationTimer object so I can add them to my GUI.
I've already tried java.lang.Reflect, but I can't seem to get it to work.
AnimationTimer animations = new AnimationTimer() //creates animations
{
#Override
public void handle(long now)
{
//update frames
if(jump == 0) //moves the player model to the starting position
{
playerY = 160;
}
else if(jump == 1) //moves the player model up (jumping)
{
playerY = 100;
}
if(shout == 1) //makes player immune while "shouting"
{
player.setFill(Color.GREEN);
}
if(shout == 0) //makes player not immune anymore
player.setFill(Color.BLUE);
if(obstacleX == PLAYER_X) //updates the score
{
points += 10;
score.setText("Score: " + String.valueOf(points));
}
if(player.getBoundsInParent().intersects(obstacle.getBoundsInParent())) //detects if the player model touches an obstacles
{
if(obstacle.getEndY() == 0)
{
if(player.getFill() == Color.BLUE)
player.setFill(Color.RED);
}
else if(obstacle.getEndY() == 130)
player.setFill(Color.RED);
}
if(obstacleX > 0) //moves the obstacles' reference point from the right to the left
obstacleX -= 5;
if(obstacleX == 0)
{
obstacleX = 400;
int[] array = new int[]{0, 0, 130};
Random randomNum = new Random();
int i = randomNum.nextInt(array.length);
random = array[i];
obstacle.setEndY(random);
}
//render frames
player.setCenterY(playerY);
obstacle.setStartX(obstacleX); //this and line 110 move the obstacle across the scene
obstacle.setEndX(obstacleX);
try{ //exception handler for outputs
if(player.getFill() == Color.GREEN)
{
digitalOutput6.setDutyCycle(1); //turns on green LED
digitalOutput0.setDutyCycle(0);
}
if(player.getFill() == Color.BLUE)
{
digitalOutput6.setDutyCycle(0); //turns off LEDs
digitalOutput0.setDutyCycle(0);
}
if(player.getFill() == Color.RED) //stops the animation when the player model reacts to touching an obstacle
{
endTime = System.currentTimeMillis() - startTime;
finalScore = score.getText();
this.stop(); //ends game
gameOver = 1;
digitalOutput0.setDutyCycle(1); //turns on red LED
digitalOutput6.setDutyCycle(0);
gameStage.setScene(gameEnd); //establishing scene
gameStage.setTitle("Game Over");
gameStage.show();
}
} //end of try block
catch(PhidgetException e)
{
System.out.println("Phidget Output Error");
}
} //end of animation handle itself
}; //end of animation method
I tried using
long finalTime = animations.getLong(endTime);
and
String endScore = animations.getField(finalScore);
But no luck. Any help appreciated.
AnimationTimer itself doesn't expose any attributes, make a custom class which wraps an AnimationTimer object in it, and save any attributes you want to access.
When you initialize the custom class, initialize the AnimationTimer object and override the function (as what you've done), also updates whatever attributes you want to expose.
I am not familiar with javafx, there might be other better ways, this is the one quick solution I can think of.
Did a quick search, these examples might be helpful:
https://netopyr.com/2012/06/14/using-the-javafx-animationtimer/
https://tbeernot.wordpress.com/2011/11/12/javafx-2-0-bubblemark/
First of all do not use Reflections on Anonymous classes It will work but will cause a lot of ineffeciency and dirty code.
Second of all, you try to acces fields which are in the Methodscope of
handle(long now) not in the Class scope.
AnimationTimer t = new AnimationTimer() {
int fieldInt = 0; // would be accessible
#Override
public void handle(long now) {
//do something
}
};
AnimationTimer t = new AnimationTimer() {
#Override
public void handle(long now) {
int fieldInt = 0; // is not accessible as regular field
//do something
}
};
What you can do is the following:
Use a class that extends a Animationtimer
public class Scheduler extends AnimationTimer{
private int gameEnd = 42;
private String someValue = "hello";
#Override
public void handle(long now) {
gameEnd += 1;
if(gameEnd >= 1000000) {
someValue ="Bye bye";
}
}
public String getSomeValue() {
return someValue;
}
here you can use:
Scheduler s = new Scheduler();
s.start();
//whatever you want to do
System.out.Println(s.getSomeValue());
or when you want to use the Anonymous class approach, you should create a Property that is outside of AnimationTimer and use it. (Wich is a thing that I would prefer, because you can have a changeListener on the Property and recieve a callback when the value is changed)
public void whateverMethod() {
SimpleIntegerProperty gameEndProp = new SimpleIntegerProperty(42);
AnimationTimer t = new AnimationTimer() {
#Override
public void handle(long now) {
gameEndProp.set(gameEndProp.get()+1);
}
};
t.start();
//Do something
System.out.println(gameEndProp.get());
}

How do I stop an animation once a certain barrier has been reached?

Today's question rattling my mind is such. I'm creating a program that allow the user to jump and move left or right, to problem is that whenever I try to jump the program freezes... The idea behind my code is simple, whenever the users presses space(jump) the rectangle "jumps" up and if the y of the rectangle is within a certain height above the obstacle(in this case a rectangle with dimmensions brickx,bricky,brickw, and brickh is the obstacle) then the animation is supposed to stop and the rectangle is supposed to wait for the next command while in place on top of the obstacle. To do this a method stayOnBrick is called during each iteration of the while loop and checks; during the jump, if y is within the needed range, and if y is it then sets the boolean jump = true which should break the loop on the next iteration as well as setting y to the needed value. But when space is pressed, nothing happens, or should I say the program freezes. Thoughts?
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public class KeyTest extends Core implements KeyListener{
Window w;
public int x,y;
boolean jump = true;
public int brickx, bricky, brickw, brickh;
public static void main(String args[]){
new KeyTest().run();
}
private String mess = "";
//init also called init from superclass
public void init(){
super.init();
Window w = s.getFullScreenWindow();
w.setFocusTraversalKeysEnabled(false);
w.addKeyListener(this);
y = s.getHeight()-15;
mess = "Press esc to exit";
}
#Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
e.consume();
}
#Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
int keyCode = e.getKeyCode();
if(keyCode == KeyEvent.VK_ESCAPE){
stop();
}else if(keyCode == KeyEvent.VK_SPACE){
mess = "Pressed: " + KeyEvent.getKeyText(keyCode);
while(y>s.getHeight()-200){
stayOnBrick();
if(jump==false){break;}
else{ try{
y-=20;
w.repaint();
Thread.sleep(40);
}catch(Exception jumpError){
System.out.println("Error in Jumping");
}
}
while(y<s.getHeight()-15){
stayOnBrick();
if(jump==false){
w.repaint();
break;}
else{
try{
y+=20;
Thread.sleep(40);
w.repaint();
}catch(Exception jumpError){
System.out.println("Error in Jumping");
}
}
}
}
}
else if(keyCode == e.VK_RIGHT){
if(x>=s.getWidth()-30){x=s.getWidth()-30;}
x+=20;
w.repaint();
}
else if(keyCode == e.VK_LEFT){
if(x<=0){x=0;}
x-=20;
w.repaint();
}
e.consume();
}
#Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
int keyCode = e.getKeyCode();
mess = "Released: " + KeyEvent.getKeyText(keyCode);
jump = true;
e.consume();
}
#Override
public synchronized void draw(Graphics2D g){
brickx=0; bricky=s.getHeight()-100; brickw=300; brickh=20;
Window w = s.getFullScreenWindow();
g.setColor(w.getBackground());
g.fillRect(0, 0, s.getWidth(), s.getHeight());
g.setColor(w.getForeground());
g.fillRect(x, y, 30, 15);
g.drawString(mess, 30, 30);
g.setColor(Color.BLUE);
g.fillRect(brickx, bricky, brickw, brickh);
}
public void stayOnBrick(){
if(y<bricky && y>bricky-30){
y=bricky-15;
jump = false;
}
else{jump = true;}
}
}
whenever I try to jump the program freezes
I'd start by having a look at Concurrency in Swing for the reason for the problem.
I'd suggest having a look at How to use Swing Timers for a possible solution.
I'd also recommend having a look at How to Use Key Bindings to solve the focus related issues with KeyListener
And you might find How to make sprite jump in java? and JApplet creates a ball that bounces and gets progressively less high in Java helpful

Javafx slider value at mousemoved event

I am making a media player and am trying to get the playback slider value at the cursor position when hovering over the slider bar. In an attempt to do this, i have used the following:
timeSlider.addEventFilter(MouseEvent.MOUSE_MOVED, event -> System.out.println("hovering"));
which prints "hovering" whenever the mouse changes position over the slider. Can anyone please show me how to get the value of the slider at the current cursor position? I can only figure out how to get the value at the thumb position.
Thanks in advance.
Here is a bit (maybe more than a bit) of a hack that works if you are showing the axis under the slider. It relies on looking up the axis via its css class, converting the mouse coordinates to coordinates relative to the axis, and then using API from ValueAxis to convert to the value:
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.StackPane;
import javafx.stage.Popup;
import javafx.stage.Stage;
public class TooltipOnSlider extends Application {
#Override
public void start(Stage primaryStage) {
Slider slider = new Slider(5, 25, 15);
slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
slider.setMajorTickUnit(5);
Label label = new Label();
Popup popup = new Popup();
popup.getContent().add(label);
double offset = 10 ;
slider.setOnMouseMoved(e -> {
NumberAxis axis = (NumberAxis) slider.lookup(".axis");
Point2D locationInAxis = axis.sceneToLocal(e.getSceneX(), e.getSceneY());
double mouseX = locationInAxis.getX() ;
double value = axis.getValueForDisplay(mouseX).doubleValue() ;
if (value >= slider.getMin() && value <= slider.getMax()) {
label.setText(String.format("Value: %.1f", value));
} else {
label.setText("Value: ---");
}
popup.setAnchorX(e.getScreenX());
popup.setAnchorY(e.getScreenY() + offset);
});
slider.setOnMouseEntered(e -> popup.show(slider, e.getScreenX(), e.getScreenY() + offset));
slider.setOnMouseExited(e -> popup.hide());
StackPane root = new StackPane(slider);
primaryStage.setScene(new Scene(root, 350, 80));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
This is mostly a bug-track-down: James's answer is perfect - only hampered by 2 issues:
the axis has to be visible, that is at least one of ticks or labels must be showing (in practice not a big obstacle: if you want to get the values at mouseOver you'r most probably showing the ticks anyway)
A bug in SliderSkin which introduce a slight skew of axis value vs slider value.
To see the latter, here's a slight variation of James's code. To see the asynchronicity, move the mouse over the slider then click. We expect the value of the popup to be the same as the value of the slider (shown in the label at the bottom). With core SliderSkin, they differ slightly.
public class TooltipOnSlider extends Application {
private boolean useAxis;
#Override
public void start(Stage primaryStage) {
Slider slider = new Slider(5, 25, 15);
useAxis = true;
// force an axis to be used
slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
slider.setMajorTickUnit(5);
// slider.setOrientation(Orientation.VERTICAL);
// hacking around the bugs in a custom skin
// slider.setSkin(new MySliderSkin(slider));
// slider.setSkin(new XSliderSkin(slider));
Label label = new Label();
Popup popup = new Popup();
popup.getContent().add(label);
double offset = 30 ;
slider.setOnMouseMoved(e -> {
NumberAxis axis = (NumberAxis) slider.lookup(".axis");
StackPane track = (StackPane) slider.lookup(".track");
StackPane thumb = (StackPane) slider.lookup(".thumb");
if (useAxis) {
// James: use axis to convert value/position
Point2D locationInAxis = axis.sceneToLocal(e.getSceneX(), e.getSceneY());
boolean isHorizontal = slider.getOrientation() == Orientation.HORIZONTAL;
double mouseX = isHorizontal ? locationInAxis.getX() : locationInAxis.getY() ;
double value = axis.getValueForDisplay(mouseX).doubleValue() ;
if (value >= slider.getMin() && value <= slider.getMax()) {
label.setText("" + value);
} else {
label.setText("Value: ---");
}
} else {
// this can't work because we don't know the internals of the track
Point2D locationInAxis = track.sceneToLocal(e.getSceneX(), e.getSceneY());
double mouseX = locationInAxis.getX();
double trackLength = track.getWidth();
double percent = mouseX / trackLength;
double value = slider.getMin() + ((slider.getMax() - slider.getMin()) * percent);
if (value >= slider.getMin() && value <= slider.getMax()) {
label.setText("" + value);
} else {
label.setText("Value: ---");
}
}
popup.setAnchorX(e.getScreenX());
popup.setAnchorY(e.getScreenY() + offset);
});
slider.setOnMouseEntered(e -> popup.show(slider, e.getScreenX(), e.getScreenY() + offset));
slider.setOnMouseExited(e -> popup.hide());
Label valueLabel = new Label("empty");
valueLabel.textProperty().bind(slider.valueProperty().asString());
BorderPane root = new BorderPane(slider);
root.setBottom(valueLabel);
primaryStage.setScene(new Scene(root, 350, 100));
primaryStage.show();
primaryStage.setTitle("useAxis: " + useAxis + " mySkin: " + slider.getSkin().getClass().getSimpleName());
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(TooltipOnSlider.class
.getName());
}
Note that there's an open issue which reports a similar behavior (though not so easy to see)
Looking into the code of SliderSkin, the culprit seems to be an incorrect calculation of the relative value from a mouse event on the track:
track.setOnMousePressed(me -> {
...
double relPosition = (me.getX() / trackLength);
getBehavior().trackPress(me, relPosition);
...
});
where track is positioned in the slider as:
// layout track
track.resizeRelocate((int)(trackStart - trackRadius),
trackTop ,
(int)(trackLength + trackRadius + trackRadius),
trackHeight);
Note that the active width (aka: trackLenght) of the track is offset by trackRadius, thus calculating the relative distance with the raw mousePosition on the track gives a slight error.
Below is a crude custom skin that replaces the calc simply as a test if the little application behaves as expected. Looks terrible due the need to use reflection to access super's fields/methods but now has slider and axis value in synch.
The quick hack:
/**
* Trying to work around down to the slight offset.
*/
public static class MySliderSkin extends SliderSkin {
/**
* Hook for replacing the mouse pressed handler that's installed by super.
*/
protected void installListeners() {
StackPane track = (StackPane) getSkinnable().lookup(".track");
track.setOnMousePressed(me -> {
invokeSetField("trackClicked", true);
double trackLength = invokeGetField("trackLength");
double trackStart = invokeGetField("trackStart");
// convert coordinates into slider
MouseEvent e = me.copyFor(getSkinnable(), getSkinnable());
double mouseX = e.getX();
double position;
if (mouseX < trackStart) {
position = 0;
} else if (mouseX > trackStart + trackLength) {
position = 1;
} else {
position = (mouseX - trackStart) / trackLength;
}
getBehavior().trackPress(e, position);
invokeSetField("trackClicked", false);
});
}
private double invokeGetField(String name) {
Class clazz = SliderSkin.class;
Field field;
try {
field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field.getDouble(this);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return 0.;
}
private void invokeSetField(String name, Object value) {
Class clazz = SliderSkin.class;
try {
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(this, value);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Constructor - replaces listener on track.
* #param slider
*/
public MySliderSkin(Slider slider) {
super(slider);
installListeners();
}
}
A deeper fix might be to delegate all the dirty coordinate/value transformations to the axis - that's what it is designed to do. This requires the axis to be part of the scenegraph always and only toggle its visibilty with ticks/labels showing. A first experiment looks promising.

Java slideshow image delay using paintComponent

I am putting together a slideshow program that will measure a user's time spent on each slide. The slideshow goes through several different magic tricks. Each trick is shown twice. Interim images are shown between the repetition. Transition images are shown between each trick.
On the first repetition of a trick the JPanel color flashes on the screen after a click before the next image is shown. This doesn't happen during the second repetition of the same trick. It's possible that the image is taking too long to load.
Is there an easy way to pre-load the images so that there isn't a delay the first time through?
NOTE: Original code deleted.
EDIT 1/10/2013: This code now works on slower machines. trashgod's second addendum helped the most. The mouseClick control structure periodically asks SwingWorker classes to load 40 images or less of the current trick while also setting the used images to null. I have simplified my code down for this to just two Image[]s and added a main method so it stands alone. Images are still required to run though. This is now pretty bare bones code, and if you're trying to make a slideshow with a lot of images I think it would be a good place to start.
NOTE: I think I figured out how to properly implement SwingWorker while still using multiple Image[]s. trashgod and kleopatra is this implementation in-line with what you were suggesting? I didn't end up using publish and process since I couldn't figure out how to get that to work appropriately with an indexed array, but because the StringWorker doesn't load all images in the array (only 40), and the code calls StringWorker every 20 images, there should be a pretty good buffer.
EDIT 1/10/2013 Changed out MouseListener by instead extending MouseAdapter on my Mouse class. Also fixed my paintComponent method to include a call to super.paintComponent(g).
Added publish/process methods to my SwingWorker class ImageWorker. Added a wrapper class, ArrayWrapper to allow passing imageArray[i] and its corresponding index int i with publish to process.
package slideshow3;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseAdapter;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import java.util.List;
public class SlideShow3 extends JFrame
{
//screenImage will be replaced with each new slide
private Image screenImage;
private int width;
private int height;
//Create panel for displaying images using paintComponent()
private SlideShow3.PaintPanel mainImagePanel;
//Used for keybinding
private Action escapeAction;
//Image array variables for each trick
private Image[] handCuffs; //h
private Image[] cups; //c
//Used to step through the trick arrays one image at a time
private int h = 0;
private int c = 0;
//Used by timeStamp() for documenting time per slide
private long time0 = 0;
private long time1;
public SlideShow3()
{
super();
//Create instance of each Image array
handCuffs = new Image[50];
cups = new Image[176];
//start(handCuffsString);
start("handCuffs");
try
{
screenImage = ImageIO.read(new File("images/begin1.jpg"));
}
catch (IOException nm)
{
System.out.println("begin");
System.out.println(nm.getMessage());
System.exit(0);
}
/******************************************
* Removes window framing. The next line sets fullscreen mode.
* Once fullscreen is set width and height are determined for the window
******************************************/
this.setUndecorated(true);
GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().setFullScreenWindow(this);
width = this.getWidth();
height = this.getHeight();
//Mouse click binding to slide advance control structure
addMouseListener(new Mouse());
//Create panel so that I can use key binding which requires JComponent
mainImagePanel = new PaintPanel();
add(mainImagePanel);
/******************************************
* Key Binding
* ESC will exit the slideshow
******************************************/
// Key bound AbstractAction items
escapeAction = new EscapeAction();
// Gets the mainImagePanel InputMap and pairs the key to the action
mainImagePanel.getInputMap().put(KeyStroke.getKeyStroke("ESCAPE"), "doEscapeAction");
// This line pairs the AbstractAction enterAction to the action "doEnterAction"
mainImagePanel.getActionMap().put("doEscapeAction", escapeAction);
/******************************************
* End Key Binding
******************************************/
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run()
{
SlideShow3 show = new SlideShow3();
show.setVisible(true);
}
});
}
//This method executes a specific SwingWorker class to preload images
public void start(String e)
{
if(e.equals("handCuffs"))
{
new ImageWorker(handCuffs.length, h, e).execute();
}
else if(e.equals("cups"))
{
new ImageWorker(cups.length, c, e).execute();
}
}
//Stretches and displays images in fullscreen window
private class PaintPanel extends JPanel
{
#Override
public void paintComponent(Graphics g)
{
if(screenImage != null)
{
super.paintComponent(g);
g.drawImage(screenImage, 0, 0, width, height, this);
}
}
}
/******************************************
* The following SwingWorker class Pre-loads all necessary images.
******************************************/
private class ArrayWrapper
{
private int i;
private Image image;
public ArrayWrapper(Image image, int i)
{
this.i = i;
this.image = image;
}
public int getIndex()
{
return i;
}
public Image getImage()
{
return image;
}
}
private class ImageWorker extends SwingWorker<Image[], ArrayWrapper>
{
private int currentPosition;
private int arraySize;
private String trickName;
private Image[] imageArray;
public ImageWorker(int arraySize, int currentPosition, String trick)
{
super();
this.currentPosition = currentPosition;
this.arraySize = arraySize;
this.trickName = trick;
}
#Override
public Image[] doInBackground()
{
imageArray = new Image[arraySize];
for(int i = currentPosition; i < currentPosition+40 && i < arraySize; i++)
{
try
{
imageArray[i] = ImageIO.read(new File("images/" + trickName + (i+1) + ".jpg"));
ArrayWrapper wrapArray = new ArrayWrapper(imageArray[i], i);
publish(wrapArray);
}
catch (IOException e)
{
System.out.println(trickName);
System.out.println(e.getMessage());
System.exit(0);
}
}
return imageArray;
}
#Override
public void process(List<ArrayWrapper> chunks)
{
for(ArrayWrapper element: chunks)
{
if(trickName.equals("handCuffs"))
{
handCuffs[element.getIndex()] = element.getImage();
}
else if(trickName.equals("cups"))
{
cups[element.getIndex()] = element.getImage();
}
}
}
#Override
public void done()
{
try
{
if(trickName.equals("handCuffs"))
{
handCuffs = get();
}
else if(trickName.equals("cups"))
{
cups = get();
}
}
catch(InterruptedException ignore){}
catch(java.util.concurrent.ExecutionException e)
{
String why = null;
Throwable cause = e.getCause();
if(cause != null)
{
why = cause.getMessage();
}
else
{
why = e.getMessage();
}
System.err.println("Error retrieving file: " + why);
}
}
}
/******************************************
* End SwingWorker Pre-Loading Classes
******************************************/
//Prints out time spent on each slide
public void timeStamp()
{
time1 = System.currentTimeMillis();
if(time0 != 0)
{
System.out.println(time1 - time0);
}
time0 = System.currentTimeMillis();
}
/******************************************
* User Input Classes for Key Binding Actions and Mouse Click Actions
******************************************/
private class EscapeAction extends AbstractAction
{
#Override
public void actionPerformed(ActionEvent e)
{
System.exit(0);
}
}
public class Mouse extends MouseAdapter
{
#Override
public void mouseClicked(MouseEvent e)
{
if(!(h<handCuffs.length) && !(c<cups.length))
{
timeStamp();
System.exit(0);
}
else if(h<handCuffs.length)
{
timeStamp();
screenImage = handCuffs[h];
repaint();
System.out.print("handCuffs[" + (h+1) + "]\t");
h++;
//purge used slides and refresh slide buffer
if(h == 20 || h == 40)
{
for(int i = 0; i < h; i++)
{
handCuffs[i] = null;
}
start("handCuffs");
}
if(h == 45)
{
start("cups");
}
}
else if(c<cups.length)
{
timeStamp();
screenImage = cups[c];
repaint();
System.out.print("cups[" + (c+1) + "]\t");
c++;
//purge used slides and refresh slide buffer
if(c == 20 || c == 40 || c == 60 || c == 80 || c == 100 || c == 120 || c == 140 || c == 160)
{
for(int i = 0; i < c; i++)
{
cups[i] = null;
}
start("cups");
}
}
}
}
/******************************************
* End User Input Classes for Key Binding Actions and Mouse Click Actions
******************************************/
}
This example uses a List<ImageIcon> as a cache of images returned by getImage(). Using getResource(), the delay is imperceptible. The next and previous buttons are bound to the Space key by default.
Addendum: You can control navigation by conditioning a button's setEnabled() state using an instance of javax.swing.Timer, for example.
Addendum: Your second example waits until the mouse is clicked to begin reading an image, an indeterminate process that may return a copy immediately or may not complete until after repaint(). Instead, begin reading the images in the background using ImageIO.read(), as shown here. You can process() your List<Image> and show progress, as seen here. The SwingWorker can be launched from the initial thread, running while you subsequently build your GUI on the EDT. You can display the first image as soon as it is processed.

Javafx 2 click and double click

I would like to know if it was possible to detect the double-click in JavaFX 2 ? and how ?
I would like to make different event between a click and a double click.
Thanks
Yes you can detect single, double even multiple clicks:
myNode.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2){
System.out.println("Double clicked");
}
}
}
});
MouseButton.PRIMARY is used to determine if the left (commonly) mouse button is triggered the event. Read the api of getClickCount() to conclude that there maybe multiple click counts other than single or double. However I find it hard to distinguish between single and double click events. Because the first click count of the double click will rise a single event as well.
Here is another piece of code which can be used if you have to distinguish between a single- and a double-click and have to take a specific action in either case.
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class DoubleClickDetectionTest extends Application {
boolean dragFlag = false;
int clickCounter = 0;
ScheduledThreadPoolExecutor executor;
ScheduledFuture<?> scheduledFuture;
public DoubleClickDetectionTest() {
executor = new ScheduledThreadPoolExecutor(1);
executor.setRemoveOnCancelPolicy(true);
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
StackPane root = new StackPane();
primaryStage.setScene(new Scene(root, 400, 400));
primaryStage.show();
root.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
if (e.getButton().equals(MouseButton.PRIMARY)) {
dragFlag = true;
}
}
});
root.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
if (e.getButton().equals(MouseButton.PRIMARY)) {
if (!dragFlag) {
System.out.println(++clickCounter + " " + e.getClickCount());
if (e.getClickCount() == 1) {
scheduledFuture = executor.schedule(() -> singleClickAction(), 500, TimeUnit.MILLISECONDS);
} else if (e.getClickCount() > 1) {
if (scheduledFuture != null && !scheduledFuture.isCancelled() && !scheduledFuture.isDone()) {
scheduledFuture.cancel(false);
doubleClickAction();
}
}
}
dragFlag = false;
}
}
});
}
#Override
public void stop() {
executor.shutdown();
}
private void singleClickAction() {
System.out.println("Single-click action executed.");
}
private void doubleClickAction() {
System.out.println("Double-click action executed.");
}
}
Adhering to Java SE 8 lambda expressions would look something like this:
node.setOnMouseClicked(event -> {
if(event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() == 2) {
handleSomeAction();
}
});
Once you get used to lambda expressions - they end up being more understandable than the original class instantiation and overriding (x) method. -In my opinion-
The response by P. Pandey is the simplest approach which actually distinguishes between single and double click, but it did not work for me. For one, the function "currentTimeMillis" already returns milliseconds, so dividing it by 1000 does not seem to be necessary. The version below worked for me in a more consistent fashion.
#Override
public void handle(MouseEvent t) {
long diff = 0;
currentTime=System.currentTimeMillis();
if(lastTime!=0 && currentTime!=0){
diff=currentTime-lastTime;
if( diff<=215)
isdblClicked=true;
else
isdblClicked=false;
}
lastTime=currentTime;
System.out.println("IsDblClicked()"+isdblClicked);
//use the isdblClicked flag...
}
Not sure if someone still follows this OP or refer it, but below is my version of differentiating single click to double click. While most of the answers are quite acceptable, it would be really useful if it can be done in a proper resuable way.
One of the challenge I encountered is the need to have the single-double click differentiation on multiple nodes at multiple places. I cannot do the same repetitive cumbersome logic on each and every node. It should be done in a generic way.
So I opted to implement a custom EventDispatcher and use this dispatcher on node level or I can apply it directly to Scene to make it applicable for all child nodes.
For this I created a new MouseEvent namely 'MOUSE_DOUBLE_CLICKED", so tthat I am still sticking with the standard JavaFX practises. Now I can include the double_clicked event filters/handlers just like other mouse event types.
node.addEventFilter(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e->{..<code to handle double_click>..});
node.addEventHandler(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e->{..<code to handle double_click>..});
Below is the implementation and complete working demo of this custom event dispatcher.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.*;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class DoubleClickEventDispatcherDemo extends Application {
#Override
public void start(Stage stage) throws Exception {
Rectangle box1 = new Rectangle(150, 150);
box1.setStyle("-fx-fill:red;-fx-stroke-width:2px;-fx-stroke:black;");
addEventHandlers(box1, "Red Box");
Rectangle box2 = new Rectangle(150, 150);
box2.setStyle("-fx-fill:yellow;-fx-stroke-width:2px;-fx-stroke:black;");
addEventHandlers(box2, "Yellow Box");
HBox pane = new HBox(box1, box2);
pane.setSpacing(10);
pane.setAlignment(Pos.CENTER);
addEventHandlers(pane, "HBox");
Scene scene = new Scene(new StackPane(pane), 450, 300);
stage.setScene(scene);
stage.show();
// SETTING CUSTOM EVENT DISPATCHER TO SCENE
scene.setEventDispatcher(new DoubleClickEventDispatcher(scene.getEventDispatcher()));
}
private void addEventHandlers(Node node, String nodeId) {
node.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> System.out.println("" + nodeId + " mouse clicked filter"));
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("" + nodeId + " mouse clicked handler"));
node.addEventFilter(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e -> System.out.println("" + nodeId + " mouse double clicked filter"));
node.addEventHandler(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e -> System.out.println(nodeId + " mouse double clicked handler"));
}
/**
* Custom MouseEvent
*/
interface CustomMouseEvent {
EventType<MouseEvent> MOUSE_DOUBLE_CLICKED = new EventType<>(MouseEvent.ANY, "MOUSE_DBL_CLICKED");
}
/**
* Custom EventDispatcher to differentiate from single click with double click.
*/
class DoubleClickEventDispatcher implements EventDispatcher {
/**
* Default delay to fire a double click event in milliseconds.
*/
private static final long DEFAULT_DOUBLE_CLICK_DELAY = 215;
/**
* Default event dispatcher of a node.
*/
private final EventDispatcher defaultEventDispatcher;
/**
* Timeline for dispatching mouse clicked event.
*/
private Timeline clickedTimeline;
/**
* Constructor.
*
* #param initial Default event dispatcher of a node
*/
public DoubleClickEventDispatcher(final EventDispatcher initial) {
defaultEventDispatcher = initial;
}
#Override
public Event dispatchEvent(final Event event, final EventDispatchChain tail) {
final EventType<? extends Event> type = event.getEventType();
if (type == MouseEvent.MOUSE_CLICKED) {
final MouseEvent mouseEvent = (MouseEvent) event;
final EventTarget eventTarget = event.getTarget();
if (mouseEvent.getClickCount() > 1) {
if (clickedTimeline != null) {
clickedTimeline.stop();
clickedTimeline = null;
final MouseEvent dblClickedEvent = copy(mouseEvent, CustomMouseEvent.MOUSE_DOUBLE_CLICKED);
Event.fireEvent(eventTarget, dblClickedEvent);
}
return mouseEvent;
}
if (clickedTimeline == null) {
final MouseEvent clickedEvent = copy(mouseEvent, mouseEvent.getEventType());
clickedTimeline = new Timeline(new KeyFrame(Duration.millis(DEFAULT_DOUBLE_CLICK_DELAY), e -> {
Event.fireEvent(eventTarget, clickedEvent);
clickedTimeline = null;
}));
clickedTimeline.play();
return mouseEvent;
}
}
return defaultEventDispatcher.dispatchEvent(event, tail);
}
/**
* Creates a copy of the provided mouse event type with the mouse event.
*
* #param e MouseEvent
* #param eventType Event type that need to be created
* #return New mouse event instance
*/
private MouseEvent copy(final MouseEvent e, final EventType<? extends MouseEvent> eventType) {
return new MouseEvent(eventType, e.getSceneX(), e.getSceneY(), e.getScreenX(), e.getScreenY(),
e.getButton(), e.getClickCount(), e.isShiftDown(), e.isControlDown(), e.isAltDown(),
e.isMetaDown(), e.isPrimaryButtonDown(), e.isMiddleButtonDown(),
e.isSecondaryButtonDown(), e.isSynthesized(), e.isPopupTrigger(),
e.isStillSincePress(), e.getPickResult());
}
}
}
Here is how I have implemented double click
if (e.getEventType().equals(MouseEvent.MOUSE_CLICKED) && !drag_Flag) {
long diff = 0;
if(time1==0)
time1=System.currentTimeMillis();
else
time2=System.currentTimeMillis();
if(time1!=0 && time2!=0)
diff=time2-time1;
if((diff/1000)<=215 && diff>0)
{
isdblClicked=true;
}
else
{
isdblClicked=false;
}
System.out.println("IsDblClicked()"+isdblClicked);
}
Since it is not possible to distinguish between single-click and double-click by default, we use the following approach:
On single-click, we wrap the single-click operation in an abortable runnable. This runnable waits a certain amount of time (i.e., SINGLE_CLICK_DELAY) before being executed.
In the meantime, if a second click, i.e., a double-click, occurs, the single-click operation gets aborted and only the double-click operation is performed.
This way, either the single-click or the double-click operation is performed, but never both.
Following is the full code. To use it, only the three TODO lines have to be replaced by the wanted handlers.
private static final int SINGLE_CLICK_DELAY = 250;
private ClickRunner latestClickRunner = null;
private class ClickRunner implements Runnable {
private final Runnable onSingleClick;
private boolean aborted = false;
public ClickRunner(Runnable onSingleClick) {
this.onSingleClick = onSingleClick;
}
public void abort() {
this.aborted = true;
}
#Override
public void run() {
try {
Thread.sleep(SINGLE_CLICK_DELAY);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!aborted) {
System.out.println("Execute Single Click");
Platform.runLater(() -> onSingleClick.run());
}
}
}
private void init() {
container.setOnMouseClicked(me -> {
switch (me.getButton()) {
case PRIMARY:
if (me.getClickCount() == 1) {
System.out.println("Single Click");
latestClickRunner = new ClickRunner(() -> {
// TODO: Single-left-click operation
});
CompletableFuture.runAsync(latestClickRunner);
}
if (me.getClickCount() == 2) {
System.out.println("Double Click");
if (latestClickRunner != null) {
System.out.println("-> Abort Single Click");
latestClickRunner.abort();
}
// TODO: Double-left-click operation
}
break;
case SECONDARY:
// TODO: Right-click operation
break;
default:
break;
}
});
}
A solution using PauseTransition:
PauseTransition singlePressPause = new PauseTransition(Duration.millis(500));
singlePressPause.setOnFinished(e -> {
// single press
});
node.setOnMousePressed(e -> {
if (e.isPrimaryButtonDown() && e.getClickCount() == 1) {
singlePressPause.play();
}
if (e.isPrimaryButtonDown() && e.getClickCount() == 2) {
singlePressPause.stop();
// double press
}
});
An alternative to single click vs. double click that I'm using is single click vs. press-and-hold (for about a quarter to a half second or so), then release the button. The technique can use a threaded abortable timer as in some of the code snippets above to distinguish between the two. Assuming that the actual event handling happens on the button release, this alternative has the advantage that single click works normally (i.e., without any delay), and for press-and-hold you can give the user some visual feedback when the button has been held long enough to be released (so there's never any ambiguity about which action was performed).
If you are testing how many mouse buttons (==2) are pressed, do not code it in sub-method! The next is working:
listView.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
if( mouseEvent.getButton().equals(MouseButton.SECONDARY)) {
System.out.println("isSecondaryButtonDown");
mouseEvent.consume();
// ....
}
else
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2){
System.out.println("Double clicked");
// mousePressedInListViewDC(mouseEvent);
}
else
if(mouseEvent.getClickCount() == 1){
System.out.println("1 clicked");
mousePressedInListView1C(mouseEvent);
}
}
}
})
;
I ran in the same problem, and what I noticed is that single and double click ARE distinguished with basic :
Button btn = new Button("Double click me too");
btn.setOnMousePressed(mouseEvent -> {
// CLICK catches
if (mouseEvent.getClickCount() == 1) {
System.out.println("Button clicked");
} else if (mouseEvent.getClickCount() == 2)
System.out.println("Button double clicked");
});
But a 'single' click is catched as part of the double click. So you will see on the console :
Using mainly the answer of #markus-weninger, I built up a Class extending Button to expose 2 new EventHandlers :
setOnMouseSingleClicked(EventHandler<MouseEvent> eventHandler)
setOnMouseDoubleClicked(EventHandler<MouseEvent> eventHandler)
So with the full example code bellow, when double clicking on last button, we get :
Keep in mind :
The obvious drawback is that even a single click caught with setOnMouseSingleClicked will be delayed with the singleClickDelayMillis (exposed variable which should be set accordingly to the OS, as mentioned by Kleopatra).
Another noticeable fact, is that I extended Button, and not Node where it should be : The Class where the onMouseClicked(...) is implemented.
As a last comment, I decided to add a new EventHandler rather than using the existing setOnMousePressed, setOnMouseReleased or setOnMouseClicked so that the developer can still fully implement these convenience EventHandlers. For example in order to have immediate response from a click on the button without waiting for the singleClickDelayMillis. But this means that if you implement both, the setOnMouseClicked will be fired even on a double click... beware.
Here comes the code :
import java.util.concurrent.CompletableFuture;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.beans.property.ObjectProperty;
import javafx.event.EventHandler;
import javafx.beans.property.SimpleObjectProperty;
public class DblClickCatchedWithoutSingleClick extends Application {
public class ButtonWithDblClick extends Button {
private long singleClickDelayMillis = 250;
private ClickRunner latestClickRunner = null;
private ObjectProperty<EventHandler<MouseEvent>> onMouseSingleClickedProperty = new SimpleObjectProperty<>();
private ObjectProperty<EventHandler<MouseEvent>> onMouseDoubleClickedProperty = new SimpleObjectProperty<>();
// CONSTRUCTORS
public ButtonWithDblClick() {
super();
addClickedEventHandler();
}
public ButtonWithDblClick(String text) {
super(text);
addClickedEventHandler();
}
public ButtonWithDblClick(String text, Node graphic) {
super(text, graphic);
addClickedEventHandler();
}
private class ClickRunner implements Runnable {
private final Runnable onClick;
private boolean aborted = false;
public ClickRunner(Runnable onClick) {
this.onClick = onClick;
}
public void abort() {
this.aborted = true;
}
#Override
public void run() {
try {
Thread.sleep(singleClickDelayMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!aborted) {
Platform.runLater(onClick::run);
}
}
}
private void addClickedEventHandler() {
//Handling the mouse clicked event (not using 'onMouseClicked' so it can still be used by developer).
EventHandler<MouseEvent> eventHandler = me -> {
switch (me.getButton()) {
case PRIMARY:
if (me.getClickCount() == 1) {
latestClickRunner = new ClickRunner(() -> {
System.out.println("ButtonWithDblClick : SINGLE Click fired");
onMouseSingleClickedProperty.get().handle(me);
});
CompletableFuture.runAsync(latestClickRunner);
}
if (me.getClickCount() == 2) {
if (latestClickRunner != null) {
latestClickRunner.abort();
}
System.out.println("ButtonWithDblClick : DOUBLE Click fired");
onMouseDoubleClickedProperty.get().handle(me);
}
break;
case SECONDARY:
// Right-click operation. Not implemented since usually no double RIGHT click needs to be caught.
break;
default:
break;
}
};
//Adding the event handler
addEventHandler(MouseEvent.MOUSE_CLICKED, eventHandler);
}
public void setOnMouseSingleClicked(EventHandler<MouseEvent> eventHandler) {
this.onMouseSingleClickedProperty.set(eventHandler);
}
public void setOnMouseDoubleClicked(EventHandler<MouseEvent> eventHandler) {
this.onMouseDoubleClickedProperty.set(eventHandler);
}
public long getSingleClickDelayMillis() {
return singleClickDelayMillis;
}
public void setSingleClickDelayMillis(long singleClickDelayMillis) {
this.singleClickDelayMillis = singleClickDelayMillis;
}
}
public void start(Stage stage) {
VBox root = new VBox();
Label lbl = new Label("Double click me");
lbl.setOnMouseClicked(mouseEvent -> {
// CLICK catches
if (mouseEvent.getClickCount() == 2) {
System.out.println("Label double clicked");
} else if (mouseEvent.getClickCount() == 1)
System.out.println("Label clicked");
});
Button btn = new Button("Double click me too");
btn.setOnMousePressed(mouseEvent -> {
// CLICK catches
if (mouseEvent.getClickCount() == 1) {
System.out.println("Button clicked");
} else if (mouseEvent.getClickCount() == 2)
System.out.println("Button double clicked");
});
ButtonWithDblClick btn2 = new ButtonWithDblClick("Double click me three ;-)");
btn2.setOnMouseSingleClicked(me -> {
System.out.println("BUTTON_2 : Fire SINGLE Click");
});
btn2.setOnMouseDoubleClicked(me -> {
System.out.println("BUTTON_2 : Fire DOUBLE Click");
});
root.getChildren().add(lbl);
root.getChildren().add(btn);
root.getChildren().add(btn2);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}

Categories

Resources