Disable all MouseEvents on the Children of a Pane - java

I have a Pane in which i add and remove nodes during a computation. Therefor i save a boolean which is set to true if the computation is running. of course i do some handling on starting and terminating a computation.
What i want to do now is: disable all MouseEvents on the children of the Pane if the computation starts and reenable them if the computation is terminated.
My tries until now where limited to completly remove the EventHandlers, but then i can't add them again later.
unfortunately i couldn't find a way to do this, so i hope for help here :)
Thanks in advance

Assuming you have implemented the long-running computation as a Task or Service (and if you haven't, you should probably consider doing so), you can just do something along the following lines:
Pane pane ;
// ...
Task<ResultType> computation = ... ;
pane.disableProperty().bind(computation.runningProperty());
new Thread(computation).start();
Calling setDisable(true) on a node will disable all its child nodes, so this will disable all the children of the pane, and re-enable them when the task is no longer running.
Here's an SSCCE:
import javafx.application.Application;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
public class ComputationSimulation extends Application {
#Override
public void start(Stage primaryStage) {
// text fields for input:
TextField xInput = new TextField();
TextField yInput = new TextField();
// Service for performing the computation.
// (For demo here, the computation just computes the sum of
// the two input values. Obviously this doesn't take long, so
// a random pause is inserted.)
Service<Integer> service = new Service<Integer>() {
#Override
protected Task<Integer> createTask() {
final int x = readTextField(xInput);
final int y = readTextField(yInput);
return new Task<Integer>() {
#Override
public Integer call() throws Exception {
// simulate long-running computation...
Thread.sleep((int)(Math.random() * 2000) + 1000);
// this doesn't really take much time(!):
return x + y ;
}
};
}
};
// Label to show result. Just use binding to bind to value of computation:
Label result = new Label();
result.textProperty().bind(service.valueProperty().asString());
// Button starts computation by restarting service:
Button compute = new Button("Compute");
compute.setOnAction(e -> service.restart());
// Pane to hold controls:
GridPane pane = new GridPane();
// Disable pane (and consequently all its children) when computation is running:
pane.disableProperty().bind(service.runningProperty());
// layout etc:
pane.setHgap(5);
pane.setVgap(10);
pane.addRow(0, new Label("x:"), xInput);
pane.addRow(1, new Label("y:"), yInput);
pane.addRow(2, new Label("Total:"), result);
pane.add(compute, 1, 3);
ColumnConstraints left = new ColumnConstraints();
left.setHalignment(HPos.RIGHT);
left.setHgrow(Priority.NEVER);
pane.getColumnConstraints().addAll(left, new ColumnConstraints());
pane.setPadding(new Insets(10));
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
// converts text in text field to an int if possible
// returns 0 if not valid text, and sets text accordingly
private int readTextField(TextField text) {
try {
return Integer.parseInt(text.getText());
} catch (NumberFormatException e) {
text.setText("0");
return 0 ;
}
}
public static void main(String[] args) {
launch(args);
}
}

Related

JavaFX Slider Tick Marks Initialized via CSS Disappear

I am using CSS to configure my JavaFX Sliders, then applying the style in code with:
cssSlider.getStyleClass().add("slider-style");
When I first open my window, the tick marks are present on the CSS configured Slider(s). When I close and reopen the window, the tick marks are no longer present.
This following example demonstrates the anomaly using 2 Sliders, one configured directly, the other via CSS. Click the button to hide the window for 2 seconds. Notice that the Slider in which I directly configure the attributes works fine after hiding and re-showing, but the CSS configured Slider loses its tick marks after hiding and re-showing.
Does anyone have any ideas why showing, hiding, and re-showing the window causes the tick marks to vanish from the CSS configured Slider? Am I doing something wrong, or is this a JavaFX bug?
sample.css:
.slider-style {
-fx-show-tick-marks: true;
-fx-snap-to-ticks: true;
-fx-major-tick-unit: 5;
-fx-minor-tick-count: 5;
}
CssExample.java:
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.URL;
/**
* This simple example demonstrates that JavaFX Sliders configured with CSS only show their tick marks the first time
* they are shown. If the Slider is hidden, then shown again, the tick marks are gone forever.
*/
public class CssExample extends Application {
#Override
public void start(Stage stage) throws InterruptedException, IOException {
Group root = new Group();
Scene scene = new Scene(root, 400, 200);
stage.setScene(scene);
stage.setTitle("Slider Sample");
scene.setFill(Color.BLACK);
GridPane grid = new GridPane();
grid.setPadding(new Insets(10, 10, 10, 10));
grid.setVgap(10);
grid.setHgap(70);
scene.setRoot(grid);
int rowNumber = 1;
Label directLabel = new Label("Slider from attribute assignment");
GridPane.setConstraints(directLabel, 1, rowNumber++);
grid.getChildren().add(directLabel);
Slider directSlider = new Slider();
GridPane.setConstraints(directSlider, 1, rowNumber++);
grid.getChildren().add(directSlider);
directSlider.setShowTickMarks(true);
directSlider.setSnapToTicks(true);
directSlider.setMajorTickUnit(5);
directSlider.setMinorTickCount(5);
Label cssLabel = new Label("Slider from CSS (tick marks disappear after hidden)");
GridPane.setConstraints(cssLabel, 1, rowNumber++);
grid.getChildren().add(cssLabel);
Slider cssSlider = new Slider();
GridPane.setConstraints(cssSlider, 1, rowNumber++);
grid.getChildren().add(cssSlider);
URL url = getClass().getResource("sample.css");
String cssString = url.toExternalForm();
scene.getStylesheets().add(cssString);
cssSlider.getStyleClass().add("slider-style");
Button button = new Button("Hide for 2 Seconds");
GridPane.setConstraints(button, 1, rowNumber++);
grid.getChildren().add(button);
button.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
stage.hide();
stage.show();
}
});
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
is this a JavaFX bug?
Yes.
See: https://github.com/openjdk/jfx/blob/fdc88341f1df8fb9c99356ada54b25124b77ea6e/modules/javafx.controls/src/main/java/javafx/scene/control/skin/SliderSkin.java#L398
It is a bug in the internal implementation of the setShowTickMarks method of SliderSkin (verified in JavaFX 18.0.1).
Test case:
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.io.IOException;
public class CssExample extends Application {
private static final String CSS = // language=CSS
"""
.slider-style {
-fx-show-tick-marks: true;
-fx-snap-to-ticks: true;
-fx-major-tick-unit: 5;
-fx-minor-tick-count: 5;
}
""";
private static final String CSS_INLINE = "data:text/css," + CSS;
#Override
public void start(Stage stage) throws InterruptedException, IOException {
Platform.setImplicitExit(false);
Slider cssSlider = new Slider();
cssSlider.showTickMarksProperty().addListener((observable, oldValue, newValue) ->
System.out.println(cssSlider.showTickMarksProperty())
);
cssSlider.getStyleClass().add("slider-style");
PauseTransition hideAnimation = new PauseTransition(Duration.seconds(2));
hideAnimation.setOnFinished(e -> stage.show());
Button hideWindow = new Button("Hide for 2 Seconds");
hideWindow.setOnAction(e -> {
stage.hide();
hideAnimation.play();
});
Button closeApp = new Button("Close app");
closeApp.setOnAction(e -> Platform.exit());
VBox layout = new VBox(
10,
cssSlider, hideWindow, closeApp
);
layout.setPadding(new Insets(10));
layout.setPrefSize(400, 120);
Scene scene = new Scene(layout);
scene.getStylesheets().add(CSS_INLINE);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Test output:
BooleanProperty [bean: Slider#132908b9[styleClass=slider slider-style], name: showTickMarks, value: true]
BooleanProperty [bean: Slider#132908b9[styleClass=slider slider-style], name: showTickMarks, value: false]
BooleanProperty [bean: Slider#132908b9[styleClass=slider slider-style], name: showTickMarks, value: true]
It switches showTicks from true to false, and back to true, which triggers the bug.
In the current implementation for the setShowTicks method:
private void setShowTickMarks(boolean ticksVisible, boolean labelsVisible) {
showTickMarks = (ticksVisible || labelsVisible);
Slider slider = getSkinnable();
if (showTickMarks) {
if (tickLine == null) {
tickLine = new NumberAxis();
tickLine.setAutoRanging(false);
tickLine.setSide(slider.getOrientation() == Orientation.VERTICAL ? Side.RIGHT : (slider.getOrientation() == null) ? Side.RIGHT: Side.BOTTOM);
tickLine.setUpperBound(slider.getMax());
tickLine.setLowerBound(slider.getMin());
tickLine.setTickUnit(slider.getMajorTickUnit());
tickLine.setTickMarkVisible(ticksVisible);
tickLine.setTickLabelsVisible(labelsVisible);
tickLine.setMinorTickVisible(ticksVisible);
// add 1 to the slider minor tick count since the axis draws one
// less minor ticks than the number given.
tickLine.setMinorTickCount(Math.max(slider.getMinorTickCount(),0) + 1);
if (slider.getLabelFormatter() != null) {
tickLine.setTickLabelFormatter(stringConverterWrapper);
}
getChildren().clear();
getChildren().addAll(tickLine, track, thumb);
} else {
tickLine.setTickLabelsVisible(labelsVisible);
tickLine.setTickMarkVisible(ticksVisible);
tickLine.setMinorTickVisible(ticksVisible);
}
}
else {
getChildren().clear();
getChildren().addAll(track, thumb);
// tickLine = null;
}
getSkinnable().requestLayout();
}
The first time it shows the ticks it will do this:
getChildren().clear();
getChildren().addAll(tickLine, track, thumb);
Then, when the ticks are hidden, it will do this:
getChildren().clear();
getChildren().addAll(track, thumb);
Then, when the ticks are supposed to be shown again, the tickLine is not added back to the children, so it never shows the ticks again.

UI not updating when nodes are being removed

I'm trying to remove all the nodes from my pane sequentially 1 by 1 so I can see each line being removed.To do this I have made a new thread and used the task class and wrapped the method delWalls() in a Platform.runLater() . I then used Thread.sleep thinking it would slow the loop slow so I could see the UI updating as each line is removed However what happens is the whole UI freezes up and then after the loop is done all the nodes have disappeared? Is there a way around this ... thanks
*all nodes are lines btw
//loop calls delWalls() 1458 times to delete all 1458 nodes sequentailly
Task task = new Task<Void>() {
#Override
public Void call() {
Platform.runLater(() -> {
try {
for (int i = 0; i <= 1458 - 1; i++) {
Thread.sleep(2);
delWalls();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
return null;
}
};
new Thread(task).start();
}
//delWalls method deletes one node each time it is called.
public void delWalls() throws InterruptedException {
pane.getChildren().remove(0);
}
As #MadProgrammer said, you need to work with Timeline to get the desired effect.
Below is a quick sample demo of how it can be done. Click "Add" to add nodes sequentially, and once all 10 nodes are added, click "remove" to remove them one by one.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class RemoveNodes_Demo extends Application {
#Override
public void start(Stage stage) throws Exception {
FlowPane pane = new FlowPane();
pane.setVgap(10);
pane.setHgap(10);
Button button1 = new Button("Add Nodes");
button1.setOnAction(e->{
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(400), x -> {
StackPane sp = new StackPane();
sp.setMinSize(100,100);
sp.setStyle("-fx-background-color:black,red;-fx-background-insets:0,2;");
pane.getChildren().add(sp);
}));
timeline.setCycleCount(10);
timeline.play();
});
Button button2 = new Button("Remove Nodes");
button2.setOnAction(e->{
if(!pane.getChildren().isEmpty()){
int count = pane.getChildren().size();
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(400), x -> {
if(!pane.getChildren().isEmpty()){
pane.getChildren().remove(0);
}
}));
timeline.setCycleCount(count);
timeline.play();
}
});
VBox root = new VBox(button1, button2,pane);
root.setSpacing(10);
Scene sc = new Scene(root, 600, 600);
stage.setScene(sc);
stage.show();
}
public static void main(String... a) {
Application.launch(a);
}
}

How to properly implement the Action Listener in a bullseye animation

This program first displays a bullseye created by three different sized circles.
Once the animate me button is clicked, the function animation() will make the existing circles shrink inwards until the size of the circles is zero.
Once the user presses the button named "Press to stop", the animation will then stop. If the user presses the button again, it will then keep going from the state it was stopped from, so on so forth.
Currently, this is not working as intended. It only creates about 9 circles (including the nine circles that the program began with). I know I will need to use the action listener in order to make the program run, but I'm having a hard time in terms of the documentation of the action listener. What am I supposed to put in the parameters of the listener? If you see any other ways around this, please feel free to let me know.
package target;
import javafx.animation.ScaleTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Target extends Application
{
Circle[] cir = new Circle[7];
Button btn = new Button("Animate me!");
StackPane root = new StackPane();
public static void main(String[] args)
{
launch(args);
}
/**
* start method will create the target and the start button first
* displayed on-screen to the user
*/
#Override
public void start(Stage primaryStage)
{
root.setStyle("-fx-border-color:black;");
cir[0] = new Circle(400, 250, 200);
cir[0].setFill(Color.RED);
cir[0].setStyle("-fx-border-color:black;");
cir[1] = new Circle(315, 165, 115);
cir[1].setFill(Color.WHITE);
cir[1].setStyle("-fx-border-color:black;");
cir[2] = new Circle(230, 80, 30);
cir[2].setFill(Color.RED);
cir[2].setStyle("-fx-border-color:black;");
root.getChildren().addAll(cir[0], cir[1], cir[2]);
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root));
primaryStage.show();
btn.setOnAction(e ->
{
animation();
btn.setText("Press to Stop");
});
}
public void animation()
{
//Timeline animation = new Timeline(
//)
ScaleTransition[] st = new ScaleTransition[7];
boolean recycleCircles = false;
st[0]= new ScaleTransition(Duration.seconds(7), cir[0]);
st[0].setToX(0.0f);
st[0].setToY(0.0f);
st[0].play();
st[1] = new ScaleTransition(Duration.seconds(5.5), cir[1]);
st[1].setToX(0.0f);
st[1].setToY(0.0f);
st[1].play();
st[2] = new ScaleTransition(Duration.seconds(4), cir[2]);
st[2].setToX(0.0f);
st[2].setToY(0.0f);
st[2].play();
// int delayInc = 1;
int delay = 1;
//will create circles (will rotate between white and red) and then add
//to scaleTransitions
//while(btn.isPressed() == false)
{
for(int i = 3; i<st.length; i++)
{
if(recycleCircles == true)
{
i = 0;
recycleCircles = false;
}
if(i % 2 == 1)
{
cir[i] = new Circle(400,250,200);
cir[i].setFill(Color.WHITE);
cir[i].setStyle("-fx-border-color:black;");
root.getChildren().add(cir[i]);
cir[i].toBack();
st[i] = new ScaleTransition(Duration.seconds(7), cir[i]);
st[i].setDelay(Duration.seconds(delay));
delay++;
st[i].setToX(0.0f);
st[i].setToY(0.0f);
st[i].play();
}
else if(i%2==0)
{
cir[i] = new Circle(400, 250, 200);
cir[i].setFill(Color.RED);
cir[i].setStyle("-fx-border-color:black;");
root.getChildren().add(cir[i]);
cir[i].toBack();
st[i] = new ScaleTransition(Duration.seconds(7), cir[i]);
st[i].setDelay(Duration.seconds(delay));
delay++;
st[i].setToX(0.0f);
st[i].setToY(0.0f);
st[i].play();
}
if(i == 6)
recycleCircles = true;
}
}
//btn.pressedProperty().addListener(listener);
btn.setOnMousePressed(event ->
{
});
btn.setOnMouseReleased(event ->
{
for(int y = 0; y<st.length;y++)
{
}
});
}
}
Not sure whether you have any specific use case with each circle. If your are using the circles only for the purpose of alternating row colors, then you can get similar effect with radial gradient's repeat option.
To the extent I understand the question, below program is what I can think of. May be this can help you.
Just to let you know, the overall effect is slightly different from your program. The main difference in effects is, your program gives an effect/impression that each circle are shrinking towards center, as the distance between each circle is always same till it shrinked completely.
My program gives the effect/.impression like the entire board is moving away from your sight till it vanishes. In my program the distance between each circle decreases proportianally till it shrinks.
import javafx.animation.ScaleTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TargetAnimation extends Application {
Button btn = new Button("Animate me!");
StackPane root = new StackPane();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
root.setPrefSize(400, 400);
root.setStyle("-fx-border-color:black;");
Circle board = new Circle();
board.setRadius(200);
board.setStyle("-fx-fill:radial-gradient(focus-angle 0deg , focus-distance 0% , center 50% 50% , radius 21% , repeat, red 44% , white 46% );-fx-stroke-width:1px;-fx-stroke:black;");
root.getChildren().addAll(board, btn);
primaryStage.setScene(new Scene(root));
primaryStage.show();
ScaleTransition transition = new ScaleTransition(Duration.seconds(7), board);
transition.setToX(0);
transition.setToY(0);
btn.setOnAction(e -> {
switch (transition.getStatus()) {
case RUNNING:
transition.pause();
break;
case PAUSED:
transition.play();
break;
default:
board.setScaleX(1);
board.setScaleY(1);
transition.playFromStart();
}
});
}
}
The code given to setOnAction is an EventHandler, which is a #FunctionalInterface with the single method handle. That means that you can give it a lambda expression instead. The method takes an argument, which is the ActionEvent of clicking the button (created for you by JavaFX), and runs the code you give it.
If you want to pause the animation, call Animation#pause, and if you want to resume it, call Animation#play. I suggest that you create a ParallelTransition with all of your ScaleTransitions as its children. Then call the above methods on the ParallelTransition in the event handler.
That means that the setup code, like naming the button and creates the animations, goes outside of the event handler.

Get single stage resize event when user releases left mouse button

I've googled enough but still can find solution to get only single resize event when user releases left mouse button. For example the following solution from here
stage.titleProperty().bind(
scene.widthProperty().asString().
concat(" : ").
concat(scene.heightProperty().asString()));
When user clicks mouse left button and starts resizing the stage we will get very many events (using property listeners) while he does resizing. However, I want to get only one event - when the user completes resizing and releases mouse left button.
Another solution is here This solution significantly decreases amount of events but still doesn't let to get only one.
How to get only one resize event after user releases mouse button?
As far as I know, the mouse event handlers that resize the stage are managed natively, and so there is no way to access those purely in JavaFX - to do this the way you describe would require writing native libraries and hooking into them.
If you are doing some heavy computation (or other work that takes a long time) in response to the change in size of the stage, your best bet is probably to write code that only processes one change at a time, and just processes the last known change when it can.
An example of this is:
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class StageResizeThrottling extends Application {
private Random rng = new Random();
#Override
public void start(Stage primaryStage) {
BlockingQueue<Point2D> dimensionChangeQueue = new ArrayBlockingQueue<>(1);
ChangeListener<Number> dimensionChangeListener = (obs, oldValue, newValue) -> {
dimensionChangeQueue.clear();
dimensionChangeQueue.add(new Point2D(primaryStage.getWidth(), primaryStage.getHeight()));
};
primaryStage.widthProperty().addListener(dimensionChangeListener);
primaryStage.heightProperty().addListener(dimensionChangeListener);
Thread processDimensionChangeThread = new Thread(() -> {
try {
while (true) {
System.out.println("Waiting for change in size");
Point2D size = dimensionChangeQueue.take();
System.out.printf("Detected change in size to [%.1f, %.1f]: processing%n", size.getX(), size.getY());
process(size, primaryStage);
System.out.println("Done processing");
}
} catch (InterruptedException letThreadExit) { }
});
processDimensionChangeThread.setDaemon(true);
processDimensionChangeThread.start();
Scene scene = new Scene(new StackPane(), 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void process(Point2D stageDimension, Stage stage) throws InterruptedException {
// simulate slow process:
Thread.sleep(500 + rng.nextInt(1000));
final String title = String.format("Width: %.0f Height: %.0f", stageDimension.getX(), stageDimension.getY());
Platform.runLater(() -> stage.setTitle(title));
}
public static void main(String[] args) {
launch(args);
}
}
Note that this will always process the very first change immediately, and then process the latest change when each previously-processed change has finished processing. If no further changes have occurred, it will wait until one does occur and then process it immediately. If you like, you can combine this with the timer-based technique you linked for coalescing the changes in the listener, which will typically remove the very first change that is processed (which is usually redundant as it is almost always followed by subsequent changes). The following changes will wait until no resizes have occurred for 300ms before submitting one to the queue for processing (the thread still behaves the same way - it will process the latest change, and when that processing is complete, wait for another one):
BlockingQueue<Point2D> dimensionChangeQueue = new ArrayBlockingQueue<>(1);
PauseTransition coalesceChanges = new PauseTransition(Duration.millis(300));
coalesceChanges.setOnFinished(e -> {
dimensionChangeQueue.clear();
dimensionChangeQueue.add(new Point2D(primaryStage.getWidth(), primaryStage.getHeight()));
});
ChangeListener<Number> dimensionChangeListener = (obs, oldValue, newValue) ->
coalesceChanges.playFromStart();
primaryStage.widthProperty().addListener(dimensionChangeListener);
primaryStage.heightProperty().addListener(dimensionChangeListener);
There's some tuning here, which is a tradeoff between latency and over-eagerness in processing changes. You probably want the pause transition to last something shorter than the average processing time of the change in screen size, but not an order of magnitude shorter.
The code guarantees that no more than one change will be processed at a time and that the latest change will eventually be processed if no more changes occur. This is probably about as good as you can get without accessing native user events. (And it would also handle programmatic changes in the stage size, which a mouse handler would not handle.)
I tried to create an example to achieve what you are looking for, I ended up with this, it is not perfect but when I tested it, it looked like it could help:
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class OneEventJavaFX extends Application{
double originalWidth = 400; // the initial width of Scene when the program starts
double originalHeight = 400; // the initial height of Scene when the program starts
// boolean property to be observed in order to know the completion of stage resize
BooleanProperty completedProperty = new SimpleBooleanProperty(false);
Timeline timeline;
#Override
public void start(Stage stage) throws Exception {
Pane root = new Pane(); // simple root as example just for test purpose
Scene scene = new Scene(root, 400,400);
stage.setScene(scene);
stage.setTitle("OneEventJavaFX");
stage.show();
// because I could not find a way to implement MouseEvent.MOUSE_RELEASED
// on the stage to notify the completion on resizing, I had to use a TimeLine
// the duration should consider the time the user usually take to finish every resize
// duration is tricky, Very Slow Resizing V.S Very Fast Resizing!
timeline = new Timeline(new KeyFrame(Duration.seconds(1), e ->{
System.out.println("Resizing Should Be Completed By Now!");
originalWidth = scene.getWidth(); // record the new scene size
originalHeight = scene.getHeight();
completedProperty.setValue(false);
}));
// change listener, to be added to and removed from the scene
ChangeListener<Number> changeListener= (observable, oldValue, newValue) ->{
System.out.println("I am Detecting an Event!"); // test
// once the size changed
if(originalWidth-scene.getWidth()>1 || scene.getWidth()-originalWidth>1 ||
originalHeight-scene.getHeight()>1 || scene.getHeight()-originalHeight>1){
completedProperty.set(true); // notify that completion should be considered
System.out.println("I Stopped! No More Events!");
timeline.play(); // and start counting the time
}};
// add the change listener when the program starts up
scene.widthProperty().addListener(changeListener);
scene.heightProperty().addListener(changeListener);
System.out.println("ChangeListener Added At Startup!");
// now listen to the change of the boolean property value
// instead of the size changes, it should NOT take a lot of work
// then accordingly add and remove change listener!
completedProperty.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean notComplete, Boolean complete) {
if (complete) {
scene.widthProperty().removeListener(changeListener);
scene.heightProperty().removeListener(changeListener);
System.out.println("ChangeListener Removed!");
}
else{
scene.widthProperty().addListener(changeListener);
scene.heightProperty().addListener(changeListener);
System.out.println("ChangeListener Added Back!");
}
}
});
}
public static void main(String[] args) {
launch();
}
}
Test While Resizing
ChangeListener Added At Startup!
I am Detecting an Event!
I am Detecting an Event!
ChangeListener Removed!
I Stopped! No More Events!
Resizing Should Be Completed By Now!
ChangeListener Added Back!
UPDATE:
I have been working on solving this question, I believe this approach can achieve what you want.
The idea is as follows:
Create UNDECORATED Stage and Make it Resizable.
Create a Title Bar and add it to the Stage.
Now the Mouse Events can be detected on the Border of the Stage (because basically it happens on the Scene).
Create Double Property for both the Width and Height of Stage and add Change Listener to listen to the Changes.
The changes in the Stage Width & Height will only be recorded at the beginning of the drag and when user RELEASES the Mouse.
Explanations in Comments.
The whole solution can be found here as an archive file (Why? Because I tried to post it here fully but the Body Limit is 30000 Character!) .
OneEventStage Class:
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/**
* This class customize a given Stage to record the changes
* of its size only when user starts and finishes resizing (recording one event)
* #author Yahya Almardeny
* #version 28/05/2017
*/
public class OneEventStage{
private double originalWidth; // the initial width of Scene when the program starts
private double originalHeight; // the initial height of Scene when the program starts
private TitleBar titleBar; // can be customized by the setter method (by default I made it for Windows 10 style)
private boolean started, alreadyFullScreen;
private DoubleProperty widthChange, heightChange; // record the changes in size
public Scene s;
public BorderPane scene; // this will be considered as a Scene when used in the program
public OneEventStage(Stage stage, double width, double height){
originalWidth = width; originalHeight = height;
widthChange = new SimpleDoubleProperty(originalWidth);
heightChange = new SimpleDoubleProperty(originalHeight);
started = false;
titleBar = new TitleBar("");
scene = new BorderPane();
scene.setTop(titleBar.getTitleBar());
s = new Scene(scene, originalWidth,originalHeight);
stage.initStyle(StageStyle.UNDECORATED);
stage.setScene(s);
ResizeHelper.addResizeListener(stage);
Task<Void> task = new Task<Void>(){
#Override
protected Void call() throws Exception {
Platform.runLater(new Runnable(){
#Override
public void run() {
// change listener, to be added to and removed from the scene
ChangeListener<Number> changeListener= (observable, oldValue, newValue) ->{
if(isFullScreen()){
widthChange.setValue(stage.getWidth());
heightChange.setValue(stage.getHeight());
alreadyFullScreen=true;
}
else if (alreadyFullScreen){ // coming from full screen mode
widthChange.setValue(Screen.getPrimary().getVisualBounds().getWidth());
heightChange.setValue(Screen.getPrimary().getVisualBounds().getHeight());
widthChange.setValue(originalWidth);
heightChange.setValue(originalHeight);
alreadyFullScreen = false;
}
else if(!alreadyFullScreen && !started){
started = true; // to inform the detecting Mouse Release Event is required
}
};
s.setOnMouseReleased(e->{
if(started){ // if this happens particularly after changing the size/dragging
originalWidth = stage.getWidth(); // record the new scene size
originalHeight = stage.getHeight();
widthChange.setValue(originalWidth); // add it
heightChange.setValue(originalHeight);
started = false;
}
});
// add the change listener when the program starts up
s.widthProperty().addListener(changeListener);
s.heightProperty().addListener(changeListener);
}
});
return null;
}};
new Thread(task).start();
}
/*
* to detected if user clicked on maximize button or double click on the title bar
*/
private boolean isFullScreen(){
return this.s.getWindow().getWidth()==Screen.getPrimary().getVisualBounds().getWidth() &&
this.s.getWindow().getHeight()==Screen.getPrimary().getVisualBounds().getHeight();
}
public DoubleProperty getWidthChange() {
return widthChange;
}
public DoubleProperty getHeightChange() {
return heightChange;
}
public TitleBar getTitleBar() {
return titleBar;
}
public void setTitleBar(TitleBar titleBar) {
this.titleBar = titleBar;
}
public void setTitle(String title){
titleBar.getTitle().setText(title);
}
}
OneEventStageTest Class:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* Implementing an Example of OneEventStage to test it
* #author Yahya Almardeny
* #version 28/05/2017
*/
public class OneEventStageTest extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
// create stage
OneEventStage stage = new OneEventStage(primaryStage, 400,400);
stage.setTitle("One Event Stage");
// simple containers and its components for testing purpose
VBox container = new VBox();
container.setAlignment(Pos.CENTER);
HBox widthInfoContainer = new HBox();
widthInfoContainer.setAlignment(Pos.CENTER);
Label widthChangeL = new Label("Width Changes");
TextField widthChangeV = new TextField();
widthChangeV.setEditable(false);
widthInfoContainer.getChildren().addAll(widthChangeL, widthChangeV);
HBox.setMargin(widthChangeL, new Insets(10));
HBox.setMargin(widthChangeV, new Insets(10));
HBox heightInfoContainer = new HBox();
heightInfoContainer.setAlignment(Pos.CENTER);
Label heightChangeL = new Label("Height Changes");
TextField heightChangeV = new TextField();
heightChangeV.setEditable(false);
heightInfoContainer.getChildren().addAll(heightChangeL, heightChangeV);
HBox.setMargin(heightChangeL, new Insets(10));
HBox.setMargin(heightChangeV, new Insets(10));
container.getChildren().addAll(widthInfoContainer, heightInfoContainer);
//////////////////////////////////////////////////////////////////////////
DoubleProperty widthChange = stage.getWidthChange();
DoubleProperty heightChange = stage.getHeightChange();
// listen to the changes (Testing)
widthChange.addListener((obs, old, newV)->{
Platform.runLater(new Runnable(){
#Override
public void run() {
widthChangeV.setText("From(" + old.doubleValue() + ") To(" + newV.doubleValue() + ")");
}
});
});
heightChange.addListener((obs, old, newV)->{
Platform.runLater(new Runnable(){
#Override
public void run() {
heightChangeV.setText("From(" + old.doubleValue() + ") To(" + newV.doubleValue() + ")");
}
});
});
//////////////////////////////////////////////////////////////////////////////////////
// represent a root but in fact it's inside the real root (BorderPane in the OneEventStage Class!).
StackPane root = new StackPane();
root.setAlignment(Pos.CENTER);
root.getChildren().add(container);
stage.scene.setCenter(root);
primaryStage.show();
}
public static void main(String[] args) {
launch();
}
}
TitleBar Class:
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.stage.Screen;
import javafx.stage.Stage;
/**
* This class to create a default/customized Title Bar
* to be added to Undecorated Stage in JavaFX Application
* #author Yahya Almardeny
* #version 27/05/2017
*/
public class TitleBar {
private HBox titleBar;
private ImageView icon;
private StackPane close, minimize, maximize; // represent customized components for the title bar (by using the second constructor)
private Image maximizeBefore, maximizeAfter; // for changing maximize icon when it's full screen
private Label title;
private double height, stageWidth, stageHeight, x,y, offsetX, offsetY;
private double screenWidth = Screen.getPrimary().getVisualBounds().getWidth(),
screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
private Color backgroundColor;
private StackPane maximizeButton; // for default title bar
private Label minimizeButton, closeButton; // for default title bar
private Stage stage;
private boolean intialized = false, fromMax = false;
public static enum Components {ICON,TITLE,MINIMIZE,MAXIMIZE,CLOSE;}
/**
* the default constructor, appearance of Windows 10
* #param title
*/
public TitleBar(String title){
titleBar = new HBox();
icon = new ImageView(new Image(TitleBar.class.getResourceAsStream("/icon/icon.png")));
icon.setFitWidth(15); this.icon.setFitHeight(13);
closeButton = new Label("×");
closeButton.setFont(Font.font("Times New Roman", 25));
closeButton.setPrefWidth(46);
closeButton.setAlignment(Pos.CENTER);
minimizeButton = new Label("—");
minimizeButton.setFont(Font.font(10));
minimizeButton.setPrefWidth(46);
minimizeButton.setPrefHeight(29);
minimizeButton.setAlignment(Pos.CENTER);
maximizeButton = maximiazeButton();
this.title = new Label(title);
final Pane space = new Pane();
HBox.setHgrow(space,Priority.ALWAYS);
titleBar.getChildren().addAll(this.icon, this.title,space,this.minimizeButton, this.maximizeButton, this.closeButton);
titleBar.setAlignment(Pos.CENTER_RIGHT);
HBox.setMargin(this.icon, new Insets(0,5,0,10)); // top,right, bottom, left
initalize(); // private method to get the Stage for first time
setDefaultControlsFunctionality(); // private method to add the default controls functionality
}
/**
* This is constructor to create a custom title bar
* #param icon
* #param minimize
* #param maximize
* #param close
* #param title
*/
public TitleBar(Image icon, Image minimize, Image maximizeBefore, Image maximizeAfter, Image close, String title){
titleBar = new HBox();
this.icon = new ImageView(icon);
this.icon.setFitWidth(15); this.icon.setFitHeight(14); // values can be changed via setters
this.close = new StackPane();
this.close.setPrefSize(25, 20);
this.close.getChildren().add(new ImageView(close));
((ImageView) this.close.getChildren().get(0)).setFitWidth(20);
((ImageView) this.close.getChildren().get(0)).setFitHeight(20);
this.minimize = new StackPane();
this.minimize.setPrefSize(25, 20);
this.minimize.getChildren().add(new ImageView(minimize));
((ImageView) this.minimize.getChildren().get(0)).setFitWidth(20);
((ImageView) this.minimize.getChildren().get(0)).setFitHeight(20);
this.maximizeBefore = maximizeBefore;
this.maximize = new StackPane();
this.maximize.setPrefSize(25, 20);
this.maximize.getChildren().add(new ImageView(maximizeBefore));
((ImageView) this.maximize.getChildren().get(0)).setFitWidth(20);
((ImageView) this.maximize.getChildren().get(0)).setFitHeight(20);
this.maximizeAfter = maximizeAfter;
this.title = new Label(title);
final Pane space = new Pane();
HBox.setHgrow(space,Priority.ALWAYS);
titleBar.getChildren().addAll(this.icon, this.title,space,this.minimize, this.maximize, this.close);
titleBar.setAlignment(Pos.CENTER_RIGHT);
HBox.setMargin(this.icon, new Insets(0,5,0,10)); // top,right, bottom, left
HBox.setMargin(this.close, new Insets(0,5,0,0));
initalize();
setCustomizedControlsFunctionality();
}
/**
* create the default maximize button
* #return container
*/
private StackPane maximiazeButton(){
StackPane container = new StackPane();
Rectangle rect = new Rectangle(8,8);
rect.setFill(Color.TRANSPARENT);
rect.setStroke(Color.BLACK);
container.setPrefWidth(46);
container.getChildren().add(rect);
return container;
}
/**
* To get the Stage of the application for one time only
* as well as adding listener to iconifiedProperty()
*/
private void initalize(){
titleBar.setOnMouseEntered(e->{ // the entire block will be executed only once
if(!intialized){
// get the stage and assign it to the Stage field
stage = ((Stage)titleBar.getScene().getWindow());
// add listener toiconifiedProperty()
stage.iconifiedProperty().addListener(ee->{
if(!stage.isIconified()){
stage.setMaximized(true);
if(fromMax){ // if already maximized
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.setX(0);
stage.setY(0);
}
else{
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
}
try { // to remove the flash
Thread.sleep(10);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
stage.setOpacity(1.0);
}
});
intialized=true;
}
});
}
/**
* To add functionality to title bar controls
* via event listeners
*/
private void setDefaultControlsFunctionality(){
// Double-Click on Title Bar
titleBar.setOnMouseClicked(e->{
if(e.getClickCount()==2){
maximizefunctonality();
}
});
//Maximize Control
maximizeButton.setOnMouseEntered(e->{// highlight when hover
maximizeButton.setBackground(
new Background(new BackgroundFill(Color.LIGHTGRAY,null,null)));
((Rectangle)maximizeButton.getChildren().get(0)).setFill(Color.LIGHTGRAY);
if(maximizeButton.getChildren().size()==2){
((Rectangle)maximizeButton.getChildren().get(1)).setFill(Color.LIGHTGRAY);
}
});
maximizeButton.setOnMouseExited(e->{ // remove highlight
maximizeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
((Rectangle)maximizeButton.getChildren().get(0)).setFill(Color.TRANSPARENT);
if(maximizeButton.getChildren().size()==2){
((Rectangle)maximizeButton.getChildren().get(1)).setFill(Color.WHITE);
}
});
maximizeButton.setOnMouseClicked(e->{
maximizefunctonality();
});
//Close Control
closeButton.setOnMouseEntered(e->{
closeButton.setBackground(
new Background(new BackgroundFill(Color.CRIMSON,null,null)));
closeButton.setTextFill(Color.WHITE);
});
closeButton.setOnMouseExited(e->{
closeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
closeButton.setTextFill(Color.BLACK);
});
closeButton.setOnMouseClicked(e->{
stage.close();
});
//Minimize Control
minimizeButton.setOnMouseEntered(e->{
minimizeButton.setBackground(
new Background(new BackgroundFill(Color.LIGHTGRAY,null,null)));
});
minimizeButton.setOnMouseExited(e->{
minimizeButton.setBackground(
new Background(new BackgroundFill(Color.TRANSPARENT,null,null)));
});
minimizeButton.setOnMouseClicked(e->{
if(!stage.isIconified()){ // if it's not minimized
if(fromMax){ // check if it's already full screen(maximized)
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
else{ // if it's not -> record the size and position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
}
});
// to make title bar movable
titleBar.setOnMousePressed(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
offsetX = e.getScreenX() - stage.getX();
offsetY = e.getScreenY() - stage.getY();
}
});
titleBar.setOnMouseDragged(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
stage.setX(e.getScreenX() - offsetX);
stage.setY(e.getScreenY() - offsetY);
}
});
}
private void maximizefunctonality(){
Rectangle rect = (Rectangle) maximizeButton.getChildren().get(0);
if(stage.getWidth()<screenWidth||stage.getHeight()<screenHeight){
// get the previous size + position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
// maximize it
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.centerOnScreen();
// change the maximize button appearance
rect.setTranslateX(2);
rect.setTranslateY(-2);
Rectangle rect1 = new Rectangle(8,8);
rect1.setFill(Color.WHITE);
rect1.setStroke(Color.BLACK);
maximizeButton.getChildren().add(rect1);
fromMax = true;
}
else{ // if already maximized -> return to previous size + position
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
fromMax = false;
// change the maximize button appearance
rect.setTranslateX(0);
rect.setTranslateY(0);
maximizeButton.getChildren().remove(1);
}
}
private void setCustomizedControlsFunctionality(){
//Maximize Control
maximize.setOnMouseClicked(e->{
if(stage.getWidth()<screenWidth||stage.getHeight()<screenHeight){
// get the previous size + position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
// maximize it
stage.setWidth(screenWidth);
stage.setHeight(screenHeight);
stage.centerOnScreen();
// change the maximize button appearance
((ImageView) maximize.getChildren().get(0)).setImage(maximizeAfter);
fromMax = true;
}
else{ // if already maximized -> return to previous size + position
stage.setWidth(stageWidth);
stage.setHeight(stageHeight);
stage.setX(x);
stage.setY(y);
fromMax = false;
// change the maximize button appearance
((ImageView) maximize.getChildren().get(0)).setImage(maximizeBefore);
}
});
close.setOnMouseClicked(e->{
stage.close();
});
//Minimize Control
minimize.setOnMouseClicked(e->{
if(!stage.isIconified()){ // if it's not minimized
if(fromMax){ // check if it's already full screen(maximized)
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
else{ // if it's not -> record the size and position
stageWidth = stage.getWidth();
stageHeight = stage.getHeight();
x = stage.getX();
y = stage.getY();
stage.setOpacity(0.0);
stage.setIconified(true); // minimize it
}
}
});
// to make title bar movable
titleBar.setOnMousePressed(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
offsetX = e.getScreenX() - stage.getX();
offsetY = e.getScreenY() - stage.getY();
}
});
titleBar.setOnMouseDragged(e->{
if(stage.getWidth()<screenWidth || stage.getHeight()<screenHeight){
stage.setX(e.getScreenX() - offsetX);
stage.setY(e.getScreenY() - offsetY);
}
});
}
/**
* To change margins/insets to the Title Bar components
* #param component
* #param top
* #param right
* #param bottom
* #param left
*/
public void setInsets(Components component, double top, double right, double bottom, double left){
switch(component){
case TITLE:
HBox.setMargin(title, new Insets(top, right, bottom ,left));
break;
case ICON:
HBox.setMargin(icon, new Insets(top, right, bottom ,left));
break;
case CLOSE:
HBox.setMargin(close, new Insets(top, right, bottom ,left));
break;
case MAXIMIZE:
HBox.setMargin(maximize, new Insets(top, right, bottom ,left));
break;
case MINIMIZE:
HBox.setMargin(minimize, new Insets(top, right, bottom ,left));
break;
}
}
public void setControlsSpace(Components component, double width, double height){
switch(component){
case CLOSE:
close.setPrefSize(width, height);
break;
case MAXIMIZE:
maximize.setPrefSize(width, height);
break;
case MINIMIZE:
minimize.setPrefSize(width, height);
break;
case TITLE:
//do nothing
break;
case ICON:
// do nothing
break;
}
}
public void addHoverEffect(Components component, Color defaultColor, Color onHover, Cursor cursor){
}
//reset of the class
{...}
}
ResizeHelper Class:
{....}
Test

Node.snapshot(null, null) changes size of Scene

I have Scene which is set to the Scene of my primaryStage that - amongst other nodes - contains a VBox with a TableView and some buttons. When I take a snapshot on a row in the table using TableRow.snapshot(null, null), the size of the Scene is changed. The width is changed by about 10 pixels while the height is changed by about 40 - sometimes more than 600 (!) - pixels.
This happens because Node.snapshot(null, null) invokes Scene.doCSSLayoutSyncForSnapshot(Node node) which seems to get the preferred size of all nodes in the size and recalculate the size using that. This somehow returns the wrong values since my nodes only has preferred sizes specified and looks great before this method is invoked. Is there any way to prevent this?
The size change is a problem, but it is also a problem that the primary stage doesn't change size with the Scene that it contains.
I have tried to create an MCVE reproducing the issue, but after a few days of trying to do this, I am still unable to reproduce the problem. The original program contains around 2000 lines of code that I don't want to post here.
Why would Scene.doCSSLayoutSyncForSnapshot(Node node) compromise my layout when it is properly laid out in the first place? Can I somehow make sure that the layout is properly synced before this method is invoked to make sure that it doesn't change anything?
Solved the issue. Had to copy my whole project and then remove parts of the code until the issue disappeared.
Anyway. I basically had three components in my application. A navigation component, a table compontent, and a status bar compontent. It looked like this:
The problem I had was that the width of the status bar and the width and height of the table component was increased whenever I took a snapshot of a row in the table.
Apparently, this was due to the padding of the status bar compontent. It had a right and left padding of 5 pixels, and once I removed the padding, the problem disappeared.
The added 10 pixels in width made the BorderPane that contained all of this expand with the same amount of pixels, and since the table width was bound to the BorderPane width, it increased by the same amount. What I still don't understand though, is why the Stage that contains the BorderPane doesn't adjust to the new width.
The component was properly padded before Scene.doCSSLayoutSyncForSnapshot(Node node) was invoked, so I don't understand why the extra width of ten pixels is added.
Anyhow: Removing the padding from the status bar component and instead padding the components inside the status bar fixed the issue. If someone has a good explanation for this, I'm all ears.
Here's a MCVE where you can reproduce the issue by dragging a row in the table:
import java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class MCVE extends Application {
private Stage primaryStage;
private BorderPane rootLayout;
private VBox detailsView;
private StatusBar statusBar;
public void start(Stage primaryStage) throws SQLException {
this.primaryStage = primaryStage;
this.primaryStage.setTitle("MCVE");
initRootLayout();
showStatusBar();
showDetailsView();
detailsView.prefWidthProperty().bind(rootLayout.widthProperty());
detailsView.prefHeightProperty().bind(rootLayout.heightProperty());
}
#Override
public void init() throws Exception {
super.init();
}
public void initRootLayout() {
rootLayout = new BorderPane();
primaryStage.setWidth(1000);
primaryStage.setHeight(600);
Scene scene = new Scene(rootLayout);
primaryStage.setScene(scene);
primaryStage.show();
}
public void showStatusBar() {
statusBar = new StatusBar();
rootLayout.setBottom(statusBar);
}
public void showDetailsView() {
detailsView = new VBox();
rootLayout.setCenter(detailsView);
setDetailsView(new Table(this));
detailsView.prefHeightProperty().bind(primaryStage.heightProperty());
detailsView.setMaxHeight(Region.USE_PREF_SIZE);
}
public static void main(String[] args) {
launch(args);
}
public VBox getDetailsView() {
return detailsView;
}
public void setDetailsView(Node content) {
detailsView.getChildren().add(0, content);
}
public StatusBar getStatusBar() {
return statusBar;
}
class StatusBar extends HBox {
public StatusBar() {
setPadding(new Insets(0, 5, 0, 5));
HBox leftBox = new HBox(10);
getChildren().addAll(leftBox);
/**
* CONTROL SIZES
*/
setPrefHeight(28);
setMinHeight(28);
setMaxHeight(28);
// Leftbox takes all the space not occupied by the helpbox.
leftBox.prefWidthProperty().bind(widthProperty());
setStyle("-fx-border-color: black;");
}
}
class Table extends TableView<ObservableList<String>> {
private ObservableList<ObservableList<String>> data;
public Table(MCVE app) {
prefWidthProperty().bind(app.getDetailsView().widthProperty());
prefHeightProperty()
.bind(app.getDetailsView().heightProperty());
widthProperty().addListener((obs, oldValue, newValue) -> {
System.out.println("Table width: " + newValue);
});
setRowFactory(r -> {
TableRow<ObservableList<String>> row = new TableRow<ObservableList<String>>();
row.setOnDragDetected(e -> {
Dragboard db = row.startDragAndDrop(TransferMode.ANY);
db.setDragView(row.snapshot(null, null));
ArrayList<File> files = new ArrayList<File>();
// We create a clipboard and put all of the files that
// was selected into the clipboard.
ClipboardContent filesToCopyClipboard = new ClipboardContent();
filesToCopyClipboard.putFiles(files);
db.setContent(filesToCopyClipboard);
});
row.setOnDragDone(e -> {
e.consume();
});
return row;
});
ObservableList<String> columnNames = FXCollections.observableArrayList("Col1", "col2", "Col3", "Col4");
data = FXCollections.observableArrayList();
for (int i = 0; i < columnNames.size(); i++) {
final int colIndex = i;
TableColumn<ObservableList<String>, String> column = new TableColumn<ObservableList<String>, String>(
columnNames.get(i));
column.setCellValueFactory((param) -> new SimpleStringProperty(param.getValue().get(colIndex).toString()));
getColumns().add(column);
}
// Adds all of the data from the rows the data list.
for (int i = 0; i < 100; i++) {
// Each column from the row is a String in the list.
ObservableList<String> row = FXCollections.observableArrayList();
row.add("Column 1");
row.add("Column 2");
row.add("Column 3");
row.add("Column 4");
// Adds the row to data.
data.add(row);
}
// Adds all of the rows in data to the table.
setItems(data);
}
}
}
This answer talks about it a little bit
Set scene width and height
but after diving into the source code I found that the resizing in snapshot is conditional on the scene never having a size set by one of its constructors.
You can only set a scene's size in its constructors and never again. That makes a little bit of sense, since its otherwise only used to size the window that contains it. It is unfortunate that the snapshot code is not smart enough to use the window's dimensions when set by the user in addition to the scene's possible user settings.
None of this prevents resizing later, so if you depend on taking snapshots, you may want to make a best practice out of using the Scene constructors which take a width and height and sending them something above 0

Categories

Resources