Javafx slider value at mousemoved event - java

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.

Related

JavaFx Group contents position getting changed upon transformation

I'm simulating a compass where I need to display current angular position upon reception of angular data from another source(via network). But somehow the circle group is getting shifted upon applying transformation to Text node based upon angular position. This is a sample code
MainApp.java
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.stage.Screen;
import javafx.stage.Stage;
public class MainApp extends Application {
static BorderPane borderPane;
static Group clock;
static Group angStatus;
public static Circle clockCircle;
public static Line angHand;
public static Text angText;
public static Rotate angRotate;
public static Label angLabel;
public static void main(String[] args) {
borderPane = new BorderPane();
clock = new Group();
angStatus = new Group();
clockCircle = new Circle();
clockCircle.centerXProperty().bind(borderPane.layoutXProperty());
clockCircle.centerYProperty().bind(borderPane.layoutYProperty());
clockCircle.setRadius(360);
clockCircle.setFill(Color.RED);
angHand = new Line();
angHand.startXProperty().bind(clockCircle.centerXProperty());
angHand.startYProperty().bind(clockCircle.centerYProperty());
angText = new Text();
angRotate = new Rotate();
angRotate.pivotXProperty().bind(angText.xProperty());
angRotate.pivotYProperty().bind(angText.yProperty());
angText.getTransforms().addAll(angRotate);
angLabel = new Label();
angLabel.layoutXProperty().bind(borderPane.layoutXProperty());
angLabel.layoutYProperty().bind(borderPane.layoutYProperty());
clock.getChildren().addAll(clockCircle, angHand, angText);
angStatus.getChildren().addAll(angLabel);
borderPane.setCenter(clock);
borderPane.setBottom(angStatus);
DataReceiver objDataReceiver = new DataReceiver();
Thread dataRecvThread = new Thread(objDataReceiver, "DATARECVR");
dataRecvThread.start();
launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("CLOCK");
primaryStage.setMaximized(true);
primaryStage.setScene(new Scene(borderPane, Screen.getPrimary().getVisualBounds().getWidth(), Screen.getPrimary().getVisualBounds().getHeight()));
primaryStage.show();
}
}
DataReceiver.java
import javafx.application.Platform;
import java.time.Instant;
import static java.lang.Thread.sleep;
public class DataReceiver implements Runnable {
public static int deg;
DataReceiver() {
deg = 0;
}
public void run() {
while(true) {
long startTime = Instant.now().toEpochMilli();
deg++;
while(deg >= 360)
deg -= 360;
while(deg < 0)
deg += 360;
long endTime = Instant.now().toEpochMilli();
Platform.runLater(() -> {
MainApp.angHand.endXProperty().bind(MainApp.clockCircle.radiusProperty().multiply(Math.sin(Math.toRadians(deg))));
MainApp.angHand.endYProperty().bind(MainApp.clockCircle.radiusProperty().multiply(-Math.cos(Math.toRadians(deg))));
MainApp.angText.xProperty().bind(MainApp.angHand.endXProperty().add(10 * Math.sin(Math.toRadians(deg))));
MainApp.angText.yProperty().bind(MainApp.angHand.endYProperty().subtract(10 * Math.cos(Math.toRadians(deg))));
MainApp.angText.setText(String.valueOf(deg));
MainApp.angRotate.setAngle(deg);
MainApp.angLabel.setText("Angle: " + deg);
});
try {
sleep(1000 - (endTime - startTime));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
I'm using jdk-13.0.1 and javafx-sdk-11.0.2
The problem here is the fact that Group itself is non-resizeable, but changes it's own size to fit the contents. Modifying the transformations of the text results in the bounds of the Group changing horizontally and/or vertically which in turn makes the parent layout reposition the group to keep it centered. Depending on your needs you may want wrap the clock in a parent not doing this (Pane). If you want to keep the center of the circle centered, you'll probably need to implement your own layout.
Notes:
I don't recommend doing this using a Thread. Instead do this using AnimationTimer or similar utilities provided by JavaFX.
Bindings are only necessary for dynamic updates. If you just want to set values, it's the wrong choice.
There is the remainder operator (%) which could deal with the logic of one of the while loops. Furthermore the second loop is not actually needed, since the value of deg is never decreased below 0.
I wouldn't recommend putting the logic using threads in the main method. The way you implement the logic could result in an exception, if Platform.runLater is called before the toolkit has been started. Better initialize this kind of logic from Application.start.
static should be avoided, if possible, since it makes controlling the flow of data more complicated. And what if you want to display 2 clocks for different timezones? There's no way of reusing the class, if it relies on static data in the way your classes do.
A binding like the following makes no sense: borderPane is the root of the scene and therefore will keep the position (0,0); furthermore angLabel is a descendant of the borderPane. I recommend not wrapping the label in a group any use the static BorderPane.alignment property to tell borderPane how to position the node.
angLabel.layoutXProperty().bind(borderPane.layoutXProperty());
angLabel.layoutYProperty().bind(borderPane.layoutYProperty());
The following example makes Clock a Control with a property of type LocalTime and calculates the positions of the children itself:
public class Clock extends Control {
private final ObjectProperty<LocalTime> time = new SimpleObjectProperty<>(this, "time", LocalTime.MIDNIGHT) {
#Override
public void set(LocalTime newValue) {
// make sure the value is non-null
super.set(newValue == null ? LocalTime.MIDNIGHT : newValue);
}
};
public ObjectProperty<LocalTime> timeProperty() {
return time;
}
public LocalTime getTime() {
return time.get();
}
public void setTime(LocalTime value) {
time.set(value);
}
#Override
protected Skin<?> createDefaultSkin() {
return new ClockSkin(this);
}
}
public class ClockSkin extends SkinBase<Clock> {
private static final double SPACE = 20;
private static final double FACTOR = 360d / 60;
private final Circle face;
private final Line secondsHand;
private final Rotate rotate;
private final Text secondsText;
public ClockSkin(Clock control) {
super(control);
face = new Circle(360, Color.RED);
// line straight up from center to circle border
secondsHand = new Line();
secondsHand.endXProperty().bind(face.centerXProperty());
secondsHand.endYProperty().bind(face.centerYProperty().subtract(face.getRadius()));
secondsHand.startXProperty().bind(face.centerXProperty());
secondsHand.startYProperty().bind(face.centerYProperty());
secondsText = new Text();
rotate = new Rotate();
rotate.pivotXProperty().bind(face.centerXProperty());
rotate.pivotYProperty().bind(face.centerYProperty());
secondsHand.getTransforms().add(rotate);
secondsText.getTransforms().add(rotate);
registerChangeListener(control.timeProperty(), (observable) -> {
LocalTime value = (LocalTime) observable.getValue();
update(value);
control.requestLayout();
});
getChildren().addAll(face, secondsHand, secondsText);
update(control.getTime());
}
protected void update(LocalTime time) {
int seconds = time.getSecond();
secondsText.setText(Integer.toString(seconds));
rotate.setAngle(seconds * FACTOR);
}
#Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
// center face
face.setCenterX(contentX + SPACE + contentWidth / 2);
face.setCenterY(contentY + SPACE + contentHeight / 2);
// position text
secondsText.setX(contentX + SPACE + (contentWidth - secondsText.prefWidth(-1)) / 2);
secondsText.setY(face.getCenterY() - face.getRadius() - SPACE / 2);
}
#Override
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset,
double leftInset) {
return 2 * (SPACE + face.getRadius()) + leftInset + rightInset;
}
#Override
protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
return 2 * (SPACE + face.getRadius()) + topInset + bottomInset;
}
#Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset,
double leftInset) {
return computeMinWidth(height, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
return computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
public void dispose() {
getChildren().clear();
super.dispose();
}
}
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane borderPane = new BorderPane();
Clock clock = new Clock();
clock.setTime(LocalTime.now());
Label angLabel = new Label();
BorderPane.setAlignment(angLabel, Pos.TOP_LEFT);
borderPane.setCenter(clock);
borderPane.setBottom(angLabel);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
LocalTime time = LocalTime.now();
clock.setTime(time);
angLabel.setText(formatter.format(time));
}
};
timer.start();
primaryStage.setTitle("CLOCK");
primaryStage.setMaximized(true);
primaryStage.setScene(new Scene(borderPane));
primaryStage.show();
}
Note that here the separation between the logic for updating the visuals and updating the data is done much more cleanly than in your code. In general you want to prevent access of other classes to internal logic, since this prevents outside interference that could possibly break your control.

Waiting a keyevent [duplicate]

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()");
}
});
*/
}

How to retrieve the final Slider value when snapToTicks==true?

I have the following JavaFX scene (note the setting of snapToTicks):
package com.example.javafx;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.stage.Stage;
public class SliderExample extends Application {
public static void main(String[] args) { launch(args); }
#Override
public void start(Stage primaryStage) {
Slider slider = new Slider(0.25, 2.0, 1.0);
slider.setShowTickLabels(true);
slider.setShowTickMarks(true);
slider.setMajorTickUnit(0.25);
slider.setMinorTickCount(0);
slider.setSnapToTicks(true); // !!!!!!!!!!
Scene scene = new Scene(slider, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
}
which renders a slider like this:
Since snapToTicks is set to true the slider will finally move to the nearest value once the mouse button is released.
How can that final value be retrieved?
I tried
slider.valueProperty().addListener( n -> {
if (!slider.isValueChanging()) {
System.err.println(n);
}
});
which works well except for the minimum and maximum values - if the mouse is already at a position left to the slider or at a position right to the slider, the listener will not be called at all anymore since the final value has already been set.
I have also tried to use the valueChangingProperty:
slider.valueChangingProperty().addListener( (prop, oldVal, newVal) -> {
// NOT the final value when newVal == false!!!!!!!
System.err.println(prop + "/" + oldVal + "/" + newVal);
});
but the problem is that JavaFX will still change the value to the snapped value after that listener has been called with newVal equal to false (which I would even consider a bug, but probably I missed something). So its not possible to access the final, snapped value in that method.
I finally came up with the below solution, based on the proposal from #ItachiUchiha. Essentially, the solution uses both, a valueProperty and a valueChangingProperty listener, and uses some flags to track the current state. At the end, the perform() method is called exactly once when the slider movement is done and the final value is available. This works when the slider is moved either with the mouse or through the keyboard.
A reusable class implemented as subclass of Slider is available at https://github.com/afester/FranzXaver/blob/master/FranzXaver/src/main/java/afester/javafx/components/SnapSlider.java.
package com.example.javafx;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.stage.Stage;
public class SliderExample extends Application {
public static void main(String[] args) { launch(args); }
private boolean isFinal = true; // assumption: no dragging - clicked value is the final one.
// variable changes to "false" once dragging starts.
private Double finalValue = null;
#Override
public void start(Stage primaryStage) {
final Slider slider = new Slider(0.25, 2.0, 1.0);
slider.setShowTickLabels(true);
slider.setShowTickMarks(true);
slider.setMajorTickUnit(0.25);
slider.setMinorTickCount(0);
slider.setSnapToTicks(true);
slider.valueProperty().addListener(new ChangeListener<Number>() {
final double minCompare = slider.getMin() + Math.ulp(slider.getMin());
final double maxCompare = slider.getMax() - Math.ulp(slider.getMax());
#Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue) {
if (isFinal) { // either dragging of knob has stopped or
// no dragging was done at all (direct click or
// keyboard navigation)
perform((Double) newValue);
finalValue = null;
} else { // dragging in progress
double val = (double) newValue;
if (val > maxCompare || val < minCompare) {
isFinal = true; // current value will be treated as final value
// once the valueChangingProperty goes to false
finalValue = (Double) newValue; // remember current value
} else {
isFinal = false; // no final value anymore - slider
finalValue = null; // has been dragged to a position within
// minimum and maximum
}
}
}
});
slider.valueChangingProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) {
if (newValue == true) { // dragging of knob started.
isFinal = false; // captured values are not the final ones.
} else { // dragging of knob stopped.
if (isFinal) { // captured value is already the final one
// since it is either the minimum or the maximum value
perform(finalValue);
finalValue = null;
} else {
isFinal = true; // next captured value will be the final one
}
}
}
});
Scene scene = new Scene(slider, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void perform(double value) {
System.err.printf("FINAL: %s\n", value);
}
}

Javafx - Bounds are zero until seen

In an attempt to fix a problem with printing within the margins, I'm trying to scale my forms so that they'd shrink to the size of the paper it will be printed to.
Inside Printable.java that extends VBox
public void scaleToFit(){
double maxWidth = 497.0;
this.requestLayout();
double width = this.getWidth();
if(width > maxWidth){
double widthFrac = maxWidth / width;
this.setScaleX(widthFrac);
}
System.out.println(this.getWidth());
//edited out same process for height
}
Printable will be kept in a HashMap data.table. When my printing window is loaded I run scaleToFit for each of them. main is a ScrollPane.
Inside ModPrintCycle.java that extends VBox
//inside constructor
main.sceneProperty().addListener(new ChangeListener<Scene>(){
#Override
public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) {
System.out.println("new Scene");
newValue.windowProperty().addListener(new ChangeListener<Window>(){
public void changed(ObservableValue<? extends Window> arg0, Window arg1, Window arg2) {
System.out.println("new Window");
arg2.setOnShown(new EventHandler<WindowEvent>(){
#Override
public void handle(WindowEvent event) {
Platform.runLater(new Runnable(){
#Override
public void run() {
System.out.println(event.toString());
lookingAt = data.tables.size();
while(lookingAt-1 >= 0 ){
showNext(-1);
}
}
});
}
});
}
});
}
});
Just for this example, I also added scaleToFit() in the button that changes between these Printables. [EDIT: Added the scripts that explicitly show the use of scaleToFit()] Note that data.tables is a HashMap containing the Printables.
Inside ModPrintCycle.java continuation
private void showNext(int move){
boolean k = false;
if(move > 0 && lookingAt+move < data.tables.size()){
k = true;
}
else if(move < 0 && lookingAt+move >=0){
k = true;
}
if(k){
lookingAt+= move;
}
show();
}
private void show(){
if(data.tables.size() > 0){
if(lookingAt >= 0 && lookingAt < data.tables.size()){
//tableOrder is an ArrayList<String> for the arrangement of data.tables
if(tableOrder.size() > 0){
Printable pt = data.tables.get(tableOrder.get(lookingAt));
main.setContent(pt);
pt.scaleToFit();
}
}
else{
if(lookingAt < 0){
lookingAt = 0;
show();
}
else if(lookingAt >= data.tables.size()){
lookingAt = data.tables.size()-1;
show();
}
}
txtStatus.setText((lookingAt+1) + " / " + data.tables.size());
}else{
main.setContent(null);
txtStatus.setText("Empty");
}
}
public void printAll(ArrayList<String> pageList){
//PrinterJob pj is global
//conditions and try declarations
pj = PrinterJob.createPrinterJob(curP);
PageLayout pp = curP.createPageLayout(Paper.LEGAL, PageOrientation.PORTRAIT, MarginType.DEFAULT);
PageLayout pl = curP.createPageLayout(Paper.LEGAL, PageOrientation.LANDSCAPE, MarginType.DEFAULT);
for(String p : pageList){
Printable pt = data.tables.get(p);
pt.scaleToFit();
if(pt.isLandscape()){
pj.printPage(pl,pt);
}
else{
pj.printPage(pp,pt);
}
}
pj.endJob();
// catch statements
}
However, whenever scaleToFit() is called for the first time (when ModPrintCycle is loaded), it tells me that the widths are 0, thus will not scale yet. The second time it is called (when I change between them for the first time), it's still 0. When it finally runs a 3rd time (when I look back at the Printable), it finally works and changes the widths as needed.
Since these Printables need to be printed, I cannot ensure that the forms are scaled until someone looks through all of them twice.
How do I force the forms to take their bounds before having to load them?
Since the code you posted is not fully executable (i.e. not MCVE or SSCCE), the problem cannot be reproduced. It is also difficult to guess the cause. But I see your purpose, so I suggest instead of scaling it manually, let the printable scale itself automatically through listener:
#Override
public void start( Stage stage )
{
ScrollPane scrollPane = new ScrollPane( new Printable( new Label( "looong loooong loooooong looooong loooong text" ) ) );
stage.setScene( new Scene( scrollPane ) );
stage.show();
}
class Printable extends VBox
{
public Printable( Node... children )
{
super( children );
Printable me = this;
this.widthProperty().addListener( new ChangeListener<Number>()
{
#Override
public void changed( ObservableValue<? extends Number> observable, Number oldValue, Number newValue )
{
double maxWidth = 100.0;
double width = newValue.doubleValue();
if ( width > maxWidth )
{
double widthFrac = maxWidth / width;
me.setScaleX( widthFrac );
}
}
} );
}
}

ScrollPane is not refreshing its contents

RESUME
Good day StackOverflow community.
I've been trying for some time to develop a program that enables users to put objects in an area, allowing this area to be moved by the mouse. For this type of program, I decided to use a ScrollPane, because the user can add various contents in the area which I call the canvas. For some reason, something strange is happening in my program.
EXPLANATION OF PROGRAM
What I basically did was create a group of objects, and define this group as the ScrollPane content. Within the group, there is a Rectangle object that was added to serve as canvas boundaries. This object has larger dimensions (such as 1500 x 1000, for example), and is used in calculations that prevent nodes from moving beyond its limits. This is just the logical behind the existing large rectangle in my program, but in reality, there is no Node object with the mouse movement. What exists is the random distribution of Shape objects by the rectangle area.
For ScrollPane has its scrollbars moved, I use the setHvalue setVvalue methods. Unfortunately for my purposes, this method does not change the position of the ScrollPane's viewport with pixel values​​, but values ​​that are in a range between 0f and 1f. So I can move the viewport with the mouse, I used a equation known as Rule of 3 (here in my Country, as I know), which we equate values ​​and cross multiply.
For example, say I want to move the viewport of the ScrollPane with the mouse horizontally, and that my canvas area has a width of 2000 pixels. Finding how far (in pixels) the mouse was dragged from one point to another, I need to know how this value represents in a range 0f to 1f. Suppose I have dragged the mouse in 3 pixels, I could find the representation of the 0f to 1f with the following comparison:
2000 px ---- 1f
3 px ---- xf
Multiplying crossed, I'll get the following result:
xf = 3 / 2000
xf = 0.0015
Note: I believe you all know that. I'm not teaching math to anyone,
just want to explain the logic of my problem.
SOURCE CODE
Here is my program class:
import testes.util.TestesUtil;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
public class ScrollTest4 extends Application
{
// #########################################################################################################
// MAIN
// #########################################################################################################
public static void main(String[] args)
{
Application.launch(args);
}
// #########################################################################################################
// INSTÂNCIAS
// #########################################################################################################
// OUTSIDE
private BorderPane root;
private Button but_moreH;
private Button but_lessH;
private Button but_moreV;
private Button but_lessV;
// LOG
private VBox vbox_south;
private Label lab_hValue;
private Label lab_vValue;
private Label lab_viewport;
// INSIDE
private Rectangle rec_canvas;
private ScrollPane scroll;
private Group grp_objects;
// MOUSE
private double mouse_x = 0;
private double mouse_y = 0;
// MISC
private AnimationTimer timer;
// EDITED - 08/02/2014
private boolean moreH = false;
private boolean moreV = false; // Purposely unused.
private boolean lessH = false;
private boolean lessV = false; // Purposely unused.
// #########################################################################################################
// INÍCIO FX
// #########################################################################################################
#Override public void start(Stage estagio) throws Exception
{
this.iniFX();
this.confFX();
this.adFX();
this.evFX();
Scene cenario = new Scene(this.root , 640 , 480);
estagio.setScene(cenario);
estagio.setTitle("Programa JavaFX");
estagio.show();
}
protected void iniFX()
{
// OUTSIDE
this.root = new BorderPane();
this.but_moreH = new Button();
this.but_lessH = new Button();
this.but_moreV = new Button();
this.but_lessV = new Button();
// LOG
this.vbox_south = new VBox();
this.lab_hValue = new Label();
this.lab_vValue = new Label();
this.lab_viewport = new Label();
// INSIDE
this.scroll = new ScrollPane();
this.grp_objects = new Group();
this.rec_canvas = new Rectangle();
// MISC
this.timer = new AnimationTimer()
{
#Override public void handle(long now)
{
// EDITED - 08/02/2014
if(but_moreH.isArmed() || moreH)
{
// scroll.hvalueProperty().set(scroll.hvalueProperty().get() + 0.003f);
scroll.setHvalue(scroll.getHvalue() + 0.003f);
}
// EDITED - 08/02/2014
if(but_lessH.isArmed() || lessH)
{
// scroll.hvalueProperty().set(scroll.hvalueProperty().get() - 0.003f);
scroll.setHvalue(scroll.getHvalue() - 0.003f);
}
if(but_moreV.isArmed())
{
scroll.setVvalue(scroll.getVvalue() + 0.003f);
}
if(but_lessV.isArmed())
{
scroll.setVvalue(scroll.getVvalue() - 0.003f);
}
}
};
this.timer.start();
}
protected void confFX()
{
// OUTSIDE
this.but_moreH.setText("More H");
this.but_moreH.setMaxHeight(Double.MAX_VALUE);
this.but_lessH.setText("Less H");
this.but_lessH.setMaxHeight(Double.MAX_VALUE);
this.but_moreV.setText("More V");
this.but_moreV.setMaxWidth(Double.MAX_VALUE);
this.but_lessV.setText("Less V");
this.but_lessV.setMaxWidth(Double.MAX_VALUE);
// LOG
this.updateHvalue();
this.updateVvalue();
this.updateViewport();
// INSIDE
this.rec_canvas.setWidth(1200);
this.rec_canvas.setHeight(1000);
this.rec_canvas.setFill(Color.INDIANRED);
this.rec_canvas.setStroke(Color.RED);
this.rec_canvas.setStrokeType(StrokeType.INSIDE);
this.rec_canvas.setStrokeWidth(1);
}
protected void adFX()
{
// LOG
this.vbox_south.getChildren().add(this.but_moreV);
this.vbox_south.getChildren().addAll(this.lab_hValue , this.lab_vValue , this.lab_viewport);
// OUTSIDE
this.root.setCenter(this.scroll);
this.root.setTop(this.but_lessV);
this.root.setBottom(this.vbox_south);
this.root.setRight(this.but_moreH);
this.root.setLeft(this.but_lessH);
// INSIDE
this.grp_objects.getChildren().add(this.rec_canvas);
this.scroll.setContent(this.grp_objects);
// MISC
StrokeType[] strokes = {StrokeType.CENTERED , StrokeType.INSIDE , StrokeType.OUTSIDE};
for(int cont = 0 ; cont < 20 ; cont++)
{
Rectangle node = new Rectangle(Math.random() * 100 + 50 , Math.random() * 100 + 50);
node.setFill(TestesUtil.getCorAleatoria(false));
node.setStroke(TestesUtil.getCorAleatoria(false));
node.setStrokeType(strokes[(int) (Math.random() * 2)]);
node.setStrokeWidth(Math.random() * 9 + 1);
node.setRotate(Math.random() * 360);
node.setMouseTransparent(true);
// EDITED - 08/02/2014
TestsUtil.putRandomlyIn(
node ,
rec_canvas.getBoundsInParent().getMinY() ,
rec_canvas.getBoundsInParent().getMinY() + rec_canvas.getBoundsInParent().getHeight() ,
rec_canvas.getBoundsInParent().getMinX() + rec_canvas.getBoundsInParent().getWidth() ,
rec_canvas.getBoundsInParent().getMinX() );
this.grp_objects.getChildren().add(node);
}
}
protected void evFX()
{
// ##########################
// SCROLL PROPERTIES
// ##########################
this.scroll.hvalueProperty().addListener(new ChangeListener<Number>()
{
#Override public void changed(ObservableValue<? extends Number> observable,Number oldValue, Number newValue)
{
updateHvalue();
updateViewport();
}
});
this.scroll.vvalueProperty().addListener(new ChangeListener<Number>()
{
#Override public void changed(ObservableValue<? extends Number> observable,Number oldValue, Number newValue)
{
updateVvalue();
updateViewport();
}
});
this.scroll.setOnKeyPressed(new EventHandler<KeyEvent>()
{
#Override public void handle(KeyEvent e)
{
if(e.getCode() == KeyCode.RIGHT)
{
moreH = true;
}
else if(e.getCode() == KeyCode.LEFT)
{
lessH = true;
}
}
});
this.scroll.setOnKeyReleased(new EventHandler<KeyEvent>()
{
#Override public void handle(KeyEvent e)
{
if(e.getCode() == KeyCode.RIGHT)
{
moreH = false;
}
else if(e.getCode() == KeyCode.LEFT)
{
lessH = false;
}
}
});
// ##########################
// CANVAS
// ##########################
this.rec_canvas.setOnMousePressed(new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent e)
{
// The XY distance from the upper left corner of the canvas.
mouse_x = e.getX();
mouse_y = e.getY();
}
});
this.rec_canvas.setOnMouseDragged(new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent e)
{
// ##########################
// PIXELS
// ##########################
// The distance between mouse movements (drag events).
double xPixelsMoved = e.getX() - mouse_x;
// double yPixelsMoved = e.getY() - mouse_y;
// ##########################
// TO 1F
// ##########################
double h_of_1f = xPixelsMoved / rec_canvas.getBoundsInParent().getWidth();
double h_of_1f_inverted = h_of_1f * -1;
double currentH = scroll.getHvalue();
scroll.setHvalue(currentH + h_of_1f);
// scroll.hvalueProperty().set(scroll.getHvalue() + h_de_x);
// scroll.vvalueProperty().set(scroll.getVvalue() + v_de_y);
// ##########################
// DEBUG
// ##########################
System.out.printf("xPixelsMoved: %f , h_of_1f: %f , h_of_1f_inverted: %f %n",
xPixelsMoved , h_of_1f , h_of_1f_inverted);
// ##########################
// UPDATE FROM
// EVENT TO EVENT
// ##########################
// Writes last mouse position to update on new motion event.
mouse_x = e.getX();
mouse_y = e.getY();
}
});
}
// #########################################################################################################
// MISC.
// #########################################################################################################
protected void updateViewport()
{
Bounds vport = this.scroll.getViewportBounds();
this.lab_viewport.setText(String.format("Viewport - [X: %f , Y: %f , W: %f , H: %f]",
vport.getMinX() , vport.getMinY() , vport.getWidth() , vport.getHeight() ));
}
protected void updateHvalue()
{
this.lab_hValue.setText("H value: " + this.scroll.getHvalue() );
}
protected void updateVvalue()
{
this.lab_vValue.setText("V value: " + this.scroll.getVvalue() );
}
}
THE PROBLEM
Clicking the mouse button on the canvas area and drag it, you can see that the program moves the ScrollPane viewport horizontally. The program seems to work perfectly (or not). However, something goes wrong at the time when the mouse is dragged sometimes abruptly (...or not!). At certain times the ScrollPane Viewport is not visually updated. This is a strange behavior, because even if viewport is not visually updated, the scrollbars are still updated.
I put other ways to move the ScrollPane viewport horizontally using the same method, and for some reason, only the approach using the mouse makes it happen. I thought this could be solved by making a request for layout using requestLayout, also causing a request to a pulse, but it does not work.
THE TEST OUTPUT
The odd thing is that everything returns to normal when the window of my application is resized. Here's a video that shows what happens to my program:
VIDEO & MIRROR 1
I no longer know what else to do. Can anyone help me with this please?
EDIT (08/02/2014 10:08 AM GMT - 3:00)
The original source code of my application is found written in Portuguese, so you may be seeing something unknown. Basically TestesUtil is a utility class with static methods that define shortcuts to other client classes. I changed the call from my source code shown here previously and am now putting some methods of my class TestesUtil, translated into English as TestsUtil:
public static void putRandomlyIn(Node node , double northPoint , double southPoint , double eastPoint , double westPoint)
{
node.setLayoutX(Math.random() * pontoLeste);
node.setLayoutY(Math.random() * pontoSul);
fixEasternBoundary(node , eastPoint);
fixNorthernBoundary(node , northPoint);
fixWesternBoundary(node , westPoint);
fixSouthernBoundary(node , southPoint);
}
There is no mystery here. This method simply calculates a value from an interval, and defines the LayoutXY properties for the Node argument. Methods "fix ..." just check the boundsInParent bounds of the node compared to the point in the argument, and then adjust the layoutXYproperties from the Node object. Even if I remove the random distribution of objects, the problem still happens. So I'm sure this problem is not being caused by this.
The source code of the original post was changed with the addition of the ability to move the scroll bars with the arrow keys. Even if it is already an existing function of ScrollPane, adding that could reproduce the error seen with the mouse (now with arrows). Some things were also translated into English for better understanding by the community.
Please, I ask for help. I'm getting dizzy not knowing what to do. This type of situation could be happening because of some bug in JavaFX? Ahhrr... Please somebody help me in this. :'(
Thank you for your attention anyway.
EDIT (09/02/2014 10:50 AM GMT - 3:00)
Forgot to mention... My program was initially written and tested using JDK 8 b123. Currently I installed the JDK 8 b128 version and am still getting the same problem. My operating system is Windows 7 64x.
I am almost certain that this is a bug. Are you guys getting the same result as me? Or am I the only one to find this kind of problem? If this is a bug, which procedure should be taken?
Thank you for your attention.
EDIT (10/02/2014 09:45 AM GMT - 3:00)
A bounty was started.
UPDATE
This bug has now been fixed for JavaFX 8u20.
Bug description
This is a bug, that can be easily verified by executing the following code with JavaFx JRE 8:
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
final ScrollPane sp = new ScrollPane();
final Image[] images = new Image[5];
final ImageView[] pics = new ImageView[5];
final VBox vb = new VBox();
final Label fileName = new Label();
final String [] imageNames = new String [] {"fw1.jpg", "fw2.jpg",
"fw3.jpg", "fw4.jpg", "fw5.jpg"};
#Override
public void start(Stage stage) {
VBox box = new VBox();
Scene scene = new Scene(box, 180, 180);
stage.setScene(scene);
stage.setTitle("Scroll Pane");
box.getChildren().addAll(sp, fileName);
VBox.setVgrow(sp, Priority.ALWAYS);
fileName.setLayoutX(30);
fileName.setLayoutY(160);
for (int i = 0; i < 5; i++) {
images[i] = new Image(getClass().getResourceAsStream(imageNames[i]));
pics[i] = new ImageView(images[i]);
pics[i].setFitWidth(100);
pics[i].setPreserveRatio(true);
vb.getChildren().add(pics[i]);
}
sp.setVmax(440);
sp.setPrefSize(115, 150);
sp.setContent(vb);
sp.vvalueProperty().addListener(new ChangeListener<Number>() {
public void changed(ObservableValue<? extends Number> ov,
Number old_val, Number new_val) {
fileName.setText(imageNames[(new_val.intValue() - 1)/100]);
}
});
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
This code comes directly from the JavaFX ScrollPane Tutorial.
If one randomly moves the vertical scroll bar with the mouse very rapidly, then at some time the screen will freeze and no longer get updated. Although one is still able to move the scroll bar around, the displayed images will stay fixed. Only if one resizes the frame, the display of the images will be updated and the ScrollPane reverts to its previous state. Note, that this bug will only happen in JRE 8, it is not reproducible in JRE 7.
The only workaround for the problem, that I could find, is adding
sp.snapshot(new SnapshotParameters(), new WritableImage(1, 1));
to the listener:
sp.vvalueProperty().addListener(new ChangeListener<Number>() {
public void changed(ObservableValue<? extends Number> ov,
Number old_val, Number new_val) {
fileName.setText(imageNames[(new_val.intValue() - 1)/100]);
sp.snapshot(new SnapshotParameters(), new WritableImage(1, 1));
}
});
Calling snapshot on the ScrollPane seems to force the update every time the vvalueProperty changes. This seems to be a known workaround for several update problems with JavaFX - see here.

Categories

Resources