This is my code:
public class TestPanel extends ScrollPane {
final int SPACING = 5;
final int ROW_MAX = 6;
public TestPanel(ArrayList<Item> items) {
VBox root = new VBox();
root.setSpacing(SPACING);
HBox row = null;
int count = 0;
for (Item item: items){
if (count == ROW_MAX || row == null){
row = new HBox();
row.setSpacing(SPACING);
root.getChildren().add(row);
count = 0;
}
CustomBox box = new customBox(item);
row.getChildren().add(box);
HBox.setHgrow(box, Priority.ALWAYS);
//box.prefWidthProperty().bind(row.widthProperty()); // Worked for GridPane
count++;
}
setFitToWidth(true);
setContent(root);
}
And this is the custom box node element thing that I'm placing in the V and H Boxes
public class CustomBox extends StackPane {
private Items item;
private Rectangle square;
private int size = 20; // Irrelevent
public CustomBox(NewAirbnbListing item) {
this.item= item;
setPadding(new Insets(5, 5, 5, 5));
square = new Rectangle(size, size, Color.RED);
square.widthProperty().bind(widthProperty());
//square.heightProperty().bind(heightProperty());
minHeightProperty().bind(widthProperty()); // Maintains aspect ratio
getChildren().add(square);
}
The Error: Pink is box, grey is background colour of the StackPane for visual testing.
What I want to do:
What I want: I want the rectangle inside the CustomBox (as well as other components I'll add later) to fill up the size of the StackPane which they sit in and alter their size when the window is resized. I basically want them to mimic the size of that Pane.
Now to try explain whats going on, basically I have a class that's basically a square (There's going to be more stuff later) and I want to fill my "grid" with. When I first did this I used a GridPane to extend my class, I also used the commented out code to bind the properties and it worked "perfectly", it resized how I wanted with no issues, except the fact it lagged like crazy because the items ArrayList contains thousands of items so this will be a very long list. And then later when I was implementing the boxes to store images, that's when the massive lag problems started. My solution to this was to replace the GridPane with the HBox and VBox combo, and it very much fixed the lag issue, it also lets me do things I can't do with the GridPane. However, the problem in the gif I linked is what I'm facing. I've tried every combination of binding properties that I can think of but it still expands like crazy and I just have no idea whats causing it so I really hope someone here can help me out. I don't know tonnes about JavaFX but I'm here to learn, any help is greatly appreciated.
I believe the root cause for your problem lies at: the square width binding with CustomBox width. You might wonder how this is a problem. You are allowing the CustomBox width to be relied on its content (a.k.a square) and your square width is relied on its parent width.. as now these two widths are interdependent to each other.. the width is increased exponentially..
One possible way to fix this, is to calculate the CustomBox size manually based on the ScrollPane viewport width. This way you are controlling the parent width manully and the contents width is handled by binding.
The code in the TestPanel will be:
private DoubleProperty size = new SimpleDoubleProperty();
double padding = 4; // 2px on either side
viewportBoundsProperty().addListener((obs, old, bounds) -> {
size.setValue((bounds.getWidth() - padding - ((ROW_MAX - 1) * SPACING)) / ROW_MAX);
});
CustomBox box = new CustomBox(item);
box.minWidthProperty().bind(size);
A complete working demo is below:
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
public class ScrollPaneContentDemo extends Application {
#Override
public void start(Stage stage) throws Exception {
List<Item> items = new ArrayList<>();
IntStream.range(1, 1000).forEach(i -> items.add(new Item()));
TestPanel root = new TestPanel(items);
Scene scene = new Scene(root, 500, 500);
stage.setScene(scene);
stage.setTitle("ScrollPaneContent Demo");
stage.show();
}
class TestPanel extends ScrollPane {
private final int SPACING = 5;
private final int ROW_MAX = 6;
private DoubleProperty size = new SimpleDoubleProperty();
public TestPanel(List<Item> items) {
final VBox root = new VBox();
root.setSpacing(SPACING);
HBox row = null;
int count = 0;
for (Item item : items) {
if (count == ROW_MAX || row == null) {
row = new HBox();
row.setSpacing(SPACING);
root.getChildren().add(row);
count = 0;
}
CustomBox box = new CustomBox(item);
box.minWidthProperty().bind(size);
row.getChildren().add(box);
HBox.setHgrow(box, Priority.ALWAYS);
count++;
}
setFitToWidth(true);
setContent(root);
double padding = 4;
viewportBoundsProperty().addListener((obs, old, bounds) -> {
size.setValue((bounds.getWidth() - padding - ((ROW_MAX - 1) * SPACING)) / ROW_MAX);
});
}
}
class CustomBox extends StackPane {
private Item item;
private Rectangle square;
private int size = 20;
public CustomBox(Item item) {
setStyle("-fx-background-color:#99999950;");
this.item = item;
setPadding(new Insets(5, 5, 5, 5));
square = new Rectangle(size, size, Color.RED);
square.widthProperty().bind(widthProperty());
square.heightProperty().bind(heightProperty());
maxHeightProperty().bind(minWidthProperty());
maxWidthProperty().bind(minWidthProperty());
minHeightProperty().bind(minWidthProperty());
getChildren().add(square);
}
}
class Item {
}
}
Related
I have an 4x4 gridpane object consists of Tiles, extends ImageView, and i want to create method for changing places of connected Tiles by mouse drag and drop. I've figured out how to take first element which drag started but I couldn't get the referances of the ImageView which in drag dropped.
Tile Class
public class Tile extends ImageView {
protected int xCoordinate;
protected int yCoordinate;
protected boolean isMoveable;
protected ArrayList<Point2D> points = new ArrayList<>();
public Tile(int xCoordinate, int yCoordinate) {
this.xCoordinate = xCoordinate;
this.yCoordinate = yCoordinate;
super.setFitHeight(125);
super.setFitWidth(125);
}}
GridPane codes
GridPane gridPane = new GridPane();
for (Tile tile : tiles){
gridPane.add(tile,tile.getXCoordinate(),tile.getYCoordinate());
}
StackPane centerPane = new StackPane();
centerPane.setStyle("-fx-background-color: white;");
centerPane.getChildren().add(gridPane);
centerPane.setPadding(new Insets(0,50,0,50));
I have tried this but I don't know how to get referance of connected Tile
gridPane.setOnMouseDragged(e->{
System.out.println(e.getTarget());
gridPane.setOnMouseReleased(e1->{
System.out.println(e1.getTarget());
});
});
I have created the codes for changing places but I should get the referance of the connected Tile when mouse released.
Oracle provides an excellent tutorial on using drag and drop in JavaFX.
You probably want to make use of the Dragboard, which is a special kind of Clipboard.
Points to note
You may not actually need to move the tiles that you have created. They are ImageViews.
You can place the image associated with the source in the dragboard and change the image in the target view when it is dropped.
You can set the dragView on the dragboard for visual feedback of the drag operation.
You can use a custom content type for the dragboard rather than an image, this is explained in the linked Oracle tutorial.
private static final DataFormat customFormat =
new DataFormat("helloworld.custom");
When putting a custom data onto a dragboard, specify the data type. Note that the data must be serializable.
When reading the data from the dragboard, a proper casting is needed.
Potential Approaches
There are two ways you can handle the tile display.
Tile view and model.
Create a separate tile view and tile model interface.
When the tiles change, don't change the view, only change the model instance backing the view. The view observes its model for changes and automatically updates itself. New nodes are not created and existing nodes are not moved.
That is the approach in the example below. The view is an ImageView and the model is an Image.
Encapsulate view and model together.
In this case, you place the information about the model in the view.
When a node is placed in the grid, you record it's grid position, for example by member values in the node or by setting user data on the node.
When a node is dragged to a new position, you query the source node for its position, then you swap the source and target nodes in the grid using:
GridPane.setConstraints(node, columnIndex, rowIndex)
This is essentially the approach you propose in your question.
I do not provide an implementation for this second potential approach.
Example
The images are images of the grid before and after manually dragging the tiles to reorder them.
The example is not going to be exactly what you want, it is purely provided as an example and you will need to adapt it if you wish to make use of some of the concepts in it.
The ImageViewFactory is just to create test images, you can ignore that portion.
Dragging specific stuff is in the DragUtil class.
Code uses Java 18 and newer Java language features, so to compile it you will need to enable the appropriate language level.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Label;
import javafx.scene.effect.*;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class TileDragApp extends Application {
private static final String TEXT = "abcdefghijklmnop";
private static final int GRID_SIZE = 4;
private static final int TILE_SIZE = 60;
#Override
public void start(Stage stage) {
ImageViewFactory imageViewFactory = new ImageViewFactory();
ImageView[] imageViews = imageViewFactory.makeImageViews(
TEXT,
TILE_SIZE
);
DragUtil dragUtil = new DragUtil();
GridPane grid = new GridPane();
for (int i = 0; i < TEXT.length(); i++) {
dragUtil.makeDraggable(imageViews[i]);
grid.add(imageViews[i], i % GRID_SIZE, i / GRID_SIZE);
}
grid.setGridLinesVisible(true);
grid.setPadding(new Insets(20));
stage.setScene(new Scene(grid));
stage.setResizable(false);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
class ImageViewFactory {
private static final String CSS =
"""
data:text/css,
""" +
// language=CSS
"""
.root {
-fx-background-color: azure;
}
.label {
-fx-font-size: 40px;
-fx-text-fill: navy;
}
""";
public ImageView[] makeImageViews(String text, int tileSize) {
List<Character> chars =
text.chars()
.mapToObj(
c -> (char) c
).collect(
Collectors.toList()
);
Collections.shuffle(chars);
return chars.stream()
.map(
c -> makeImageView(c, tileSize)
).toArray(
ImageView[]::new
);
}
private ImageView makeImageView(char c, int tileSize) {
Label label = new Label(Character.toString(c));
StackPane layout = new StackPane(label);
layout.setPrefSize(tileSize, tileSize);
Scene scene = new Scene(layout);
scene.getStylesheets().add(CSS);
SnapshotParameters snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.AZURE);
Image image = layout.snapshot(snapshotParameters,null);
return new ImageView(image);
}
}
class DragUtil {
public void makeDraggable(ImageView imageView) {
Effect highlight = createHighlightEffect(imageView);
imageView.setOnDragDetected(e -> {
Dragboard db = imageView.startDragAndDrop(TransferMode.MOVE);
ClipboardContent content = new ClipboardContent();
content.putImage(imageView.getImage());
db.setContent(content);
db.setDragView(makeSmaller(imageView.getImage()));
e.consume();
});
imageView.setOnDragOver(e -> {
if (e.getGestureSource() != imageView
&& e.getDragboard().hasImage()
) {
e.acceptTransferModes(TransferMode.MOVE);
}
imageView.setEffect(highlight);
e.consume();
});
imageView.setOnDragExited(e -> {
imageView.setEffect(null);
e.consume();
});
imageView.setOnDragDropped(e -> {
Dragboard db = e.getDragboard();
boolean success = false;
if (db.hasImage() && e.getGestureSource() instanceof ImageView source) {
source.setImage(imageView.getImage());
imageView.setImage(db.getImage());
success = true;
}
e.setDropCompleted(success);
e.consume();
});
}
private Image makeSmaller(Image image) {
ImageView resizeView = new ImageView(image);
resizeView.setFitHeight(image.getHeight() * 3 / 4);
resizeView.setFitWidth(image.getWidth() * 3 / 4);
SnapshotParameters snapshotParameters = new SnapshotParameters();
return resizeView.snapshot(snapshotParameters, null);
}
private Effect createHighlightEffect(Node n) {
ColorAdjust monochrome = new ColorAdjust();
monochrome.setSaturation(-1.0);
return new Blend(
BlendMode.MULTIPLY,
monochrome,
new ColorInput(
0,
0,
n.getLayoutBounds().getWidth(),
n.getLayoutBounds().getHeight(),
Color.PALEGREEN
)
);
}
}
I have a list of animations and I want to be able to play them by clicking on a "next" button and playing them back by clicking a "previous" button. So I can play the first animation, then play the 2nd animation, then play the 2nd animation backwards and reach the position like after playing the first animation only.
My problem is that I can't reverse the animation after it's finished. I know that I can set autoReverse but then each animation will reverse immediately.
Here is an example for one animation:
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class AnimTest extends Application {
#Override
public void start(Stage stage) throws Exception {
Circle c = new Circle(5, Color.RED);
TranslateTransition move = new TranslateTransition(Duration.seconds(2), c);
move.setByX(10);
move.setByY(10);
Button next = new Button("Next");
Button previous = new Button("Previous");
next.setOnAction(e -> {
move.setRate(1);
move.play();
});
previous.setOnAction(e -> {
move.setRate(-1);
move.play();
});
Pane p = new Pane(c);
p.setPrefSize(50, 50);
HBox buttons = new HBox(next, previous);
VBox root = new VBox(p, buttons);
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
After pressing "next" I want "previous" to move the ball back to its original position (so effectively x by -10 and y by -10) and not playing the "following" animation in reverse.
In practice, my animations animate different objects in the scenegraph and they can be parallel/sequential transitions. For the list I keep a current location index i and doing:
next.setOnAction(e -> {
Animation move = list.get(i);
move.setRate(1);
move.play();
i++;
});
previous.setOnAction(e -> {
i--;
Animation move = list.get(i);
move.setRate(-1);
move.play();
});
in an attempt to reverse the previous animation.
How can I do this?
To clarify, my list is of Animation. The TranslateTransition was just an example.
The issue here is using "relative" movement instead of absolute movement.
If you set byX = 10 the animation moves the node 10 to the right when played forward which means the proper way of reversing the animation would be to place the node at the end position immediately and then moving the node back to the original location before starting the animation.
Since you don't want to use the same animation over and over again finding the correct way to invert different animations could be difficult for animations using "relative" values. If you instead use absolute ones this shouldn't simply playing the animations backwards shouldn't cause any issues.
Example
#Override
public void start(Stage stage) {
Circle c = new Circle(5, Color.RED);
// create alternating right/down movement animations with absolute movement
List<Animation> animations = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
TranslateTransition move = new TranslateTransition(Duration.seconds(1), c);
animations.add(move);
int step = i >> 1;
if ((i & 1) == 0) {
move.setFromX(step * 10);
move.setToX((step + 1) * 10);
} else {
move.setFromY(step * 10);
move.setToY((step + 1) * 10);
}
}
final ListIterator<Animation> iterator = animations.listIterator();
Button next = new Button("Next");
Button previous = new Button("Previous");
previous.setDisable(true);
next.setOnAction(e -> {
Animation move = iterator.next();
next.setDisable(!iterator.hasNext());
previous.setDisable(false);
move.setRate(1);
move.play();
});
previous.setOnAction(e -> {
Animation move = iterator.previous();
next.setDisable(false);
previous.setDisable(!iterator.hasPrevious());
move.setRate(-1);
move.play();
});
Pane p = new Pane(c);
p.setPrefSize(100, 100);
HBox buttons = new HBox(next, previous);
VBox root = new VBox(p, buttons);
stage.setScene(new Scene(root));
stage.show();
}
I managed to trick my way into "storing" the reverse cycle for later use using a PauseTransition that pauses the animation after the forward cycle. Then the animation can be played from the second cycle and it will reverse. Not the pretiest solution but it works (except for when you press the buttons too quickly. I tried to solve it with the comment code but it didn't quite get there so if anyone has a solution please tell)
import java.util.ArrayList;
import java.util.List;
import javafx.animation.Animation;
import javafx.animation.ParallelTransition;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class AnimTest extends Application {
int current = 0;
final int size = 5;
#Override
public void start(Stage stage) throws Exception {
Circle c = new Circle(10, Color.RED);
List<Animation> animations = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
TranslateTransition move = new TranslateTransition(Duration.seconds(1), c);
move.setByX(20);
move.setByY(20);
PauseTransition pauser = new PauseTransition(move.getCycleDuration());
ParallelTransition parallel = new ParallelTransition(move, pauser);
pauser.setOnFinished(e -> parallel.pause());
parallel.setCycleCount(2);
parallel.setAutoReverse(true);
animations.add(parallel);
}
Button next = new Button("Next");
Button previous = new Button("Previous");
previous.setDisable(true);
Label l = new Label(current + "");
next.setOnAction(e -> {
next.setDisable(current == size - 1);
previous.setDisable(false);
/* if (current > 0) {
Animation last = animations.get(current - 1);
last.jumpTo(last.getCycleDuration());
}*/
Animation cur = animations.get(current);
cur.playFromStart();
current++;
l.setText(current + "");
});
previous.setOnAction(e -> {
current--;
l.setText(current + "");
next.setDisable(false);
previous.setDisable(current == 0);
/* if (current < size - 1) {
Animation last = animations.get(current + 1);
last.stop();
}*/
Animation cur = animations.get(current);
cur.play();
});
Pane p = new Pane(c);
p.setPrefSize(200, 200);
HBox buttons = new HBox(5, next, previous, l);
VBox root = new VBox(p, buttons);
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I used the disable/enable buttons code from fabian (+1).
I found the quickest solution is to add a listener to the currentRateProperty, listen for it to change to -1 and pause the transition (after the first cycle completes).
There is no cycleCompleted listener or any similar listener, but listening for the cycle to complete can be achieved that way.
Note that the cycleCount must be set to 2, and autoReverse must be set to true.
Node node = ...;
double byX = ...;
TranslateTransition transition = new TranslateTransition();
transition.setNode(node);
transition.setCycleCount(2);
transition.setAutoReverse(true);
transition.setByX(byX);
transition.currentRateProperty().addListener((obs, old, now) -> {
if (now.intValue() == -1) {
transition.pause();
}
});
Button play = new Button("Play");
play.setOnAction(event -> transition.play());
Note that both the forward and backward translation are triggered by the same method call transition.play(), hence there is only one button for both motions, but this of course can be changed. But personally, i like it that way.
In your case, it would look like this:
public class AnimTest extends Application {
#Override
public void start(Stage stage) throws Exception {
Circle c = new Circle(5, Color.RED);
TranslateTransition move = new TranslateTransition(Duration.seconds(2), c);
move.setAutoReverse(true);
move.setCycleCount(2);
move.setByX(10);
move.setByY(10);
move.currentRateProperty().addListener((obs, old, now) -> {
if (now.intValue() == -1) {
move.pause();
}
});
Button next = new Button("Next/Previous");
next.setOnAction(e -> {
move.play();
});
Pane p = new Pane(c);
p.setPrefSize(50, 50);
HBox buttons = new HBox(next);
VBox root = new VBox(p, buttons);
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
To reverse an animation in java, first you have to set the animation's autoReverse property to true and also set the cycleCount to 2.
Below is a simple code snippet i wrote earlier that makes use of the things stated above.
ScaleTransition scaleTransition = new ScaleTransition(duration, btn);
scaleTransition.setByX(1.2);
scaleTransition.setByY(1.2);
scaleTransition.setAutoReverse(true);
scaleTransition.setCycleCount(2);
scaleTransition.play();
I am Trying to build a cinema booker, where I have to select the seats. I was thinking about make placing all my rectangles in an array so i could use it for later, when clicking on a seat it should check if left and right seats are booked. Here the array index should help me. However I cant figure out how to get to this stage. (See picture.)
Take this scenario:
You click on a rectangle (representing a seat). It changes color only it is not Red colored. So Seats[][].checkNeighbourColor or something like that.See picture
package sample;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.awt.*;
import java.util.ArrayList;
public class Main extends Application {
private int seats = 12;
private int rows = 8;
private static Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); //gets screen resolution data
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("");
Group root = new Group();
Scene scene = new Scene(root, screenSize.getWidth()/4, screenSize.getHeight()/3, Color.WHITE);
for (int i = 0; i<= seats; i++)
{
Rectangle r = new Rectangle();
r.setFill(Color.GREEN);
r.setX(scene.getWidth()/5+i*30);
r.setY(scene.getHeight()/5);
r.setWidth(screenSize.getHeight()/80);
r.setHeight(screenSize.getHeight()/80);
root.getChildren().add(r);
for (int q = 0; q<=rows; q++)
{
Rectangle s = new Rectangle();
s.setFill(Color.GREEN);
s.setX(scene.getWidth()/5+i*30);
s.setY(scene.getHeight()/5+q*30);
s.setWidth(screenSize.getHeight()/80);
s.setHeight(screenSize.getHeight()/80);
root.getChildren().add(s);
s.setOnMouseClicked(event ->{
s.setFill(Color.BLACK);
});
}
}
primaryStage.setScene(scene);
primaryStage.show();
}
}
It's not entirely clear to me what you're asking, but can't you just do
private Rectangle[][] rectangles = new Rectangle[seats][rows];
and then just do
rectangles[i][q] = s ;
For your listener you can do
final int row = i ;
final int seat = q ;
s.setOnMouseClicked(event -> {
// check rectangles[row-1][seat] and rectangles[row+1][seat] as needed,
// checking for range of row first
});
Aside: don't mix AWT and JavaFX. Use the Screen API to get the dimension of the physical screen(s) in JavaFX.
Okay I found my mistake. It was a simple ArrayOutOfBound (The statement in my for-loops was wrong), but thank you all for your help :)
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
I use certain events to place small rectangles on an HBox. Their placement is perfect when the window has not been resized, but when you for example go from small to fullscreen, their placement is wrong (of course, because they get a certain value for X at the time of placement - this is measured by getting the width of the HBox at that specific moment).
Question:
How can I make these positions dynamic, so when I resize the window, they stay in proportion?
Pictures:
Code:
#FXML HBox tagLine; // initializes the HBox
...
public void addTag(String sort) {
Rectangle rect = new Rectangle(20, tagLine.getHeight());
double pos = timeSlider.getValue() / 100 * tagLine.getWidth(); // retrieves position at the moment of being called
rect.setTranslateX(pos);
rect.setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
showNotification("Gemarkeerde gebeurtenis: " + sort);
}
});
rect.setOnMouseExited(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
notificationHide.play();
}
});
tagLine.getChildren().add(rect);
}
Few things that you need to take into account while translating a shape with accountable size is that you need to :
Translate the shape from its center
If the translation is dependent on the width of a Node, listen to the changes made to the with of that particular node and make changes to the translate property accordingly
Both of the above points seem to be missing in your implementation. You are never listening to width property of HBox. Neither is your calculation for pos taking the center of Rectangle into account.
Here is an example which try to keep the Rectangle at the center no matter what the size of your HBox is.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
private static final int SIDE = 40;
private static final double DEFAULT_WIDTH = 200;
private static final double DEFAULT_POSITION = 100;
#Override
public void start(Stage primaryStage) {
Rectangle rectangle = new Rectangle(SIDE, SIDE);
HBox root = new HBox(rectangle);
root.setPrefWidth(DEFAULT_WIDTH);
rectangle.translateXProperty().bind(root.widthProperty().multiply(DEFAULT_POSITION/DEFAULT_WIDTH).subtract(SIDE/2));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}