JavaFX: Bind to nested position - javafx

I have a Pane that contains some VBoxs and Lines. I want to bind the endpoints of the Lines to the "anchors" inside the VBoxs (basically I want to bind to the position of a Node nested arbitrarily deep in the VBox), but I can't figure out what value represents the Nodes position relative to the top Pane. I've tried layout properties, translate properties, as well as bounds in local and bounds in parent, and none of them seem to work. What am I missing?
(I can provide a code sample if needed, but I don't think it helps explain my problem any better since I can't get it to work.)
EDIT: I forgot to mention the VBox can be moved around the pane freely, which is why I need to bind the lines.
EDIT: Here's some source showing my progress. I can get the correct location, but it's not binding
public class Graph extends Application {
private double startX;
private double startY;
private ObjectBinding<Bounds> bounds;
private DoubleBinding tx;
private DoubleBinding ty;
#Override
public void start(Stage primaryStage) throws Exception {
Pane pane = new Pane();
Circle target = new Circle(5, Color.RED);
VBox node = wrap(target);
Line connector = new Line();
bounds = Bindings.createObjectBinding(() -> {
Bounds nodeLocal = target.getBoundsInLocal();
Bounds nodeScene = target.localToScene(nodeLocal);
Bounds nodePane = pane.sceneToLocal(nodeScene);
return nodePane;
},
target.boundsInLocalProperty(),
target.localToSceneTransformProperty(),
pane.localToSceneTransformProperty()
);
connector.setStartX(0);
connector.setStartY(0);
tx = Bindings.createDoubleBinding(() -> bounds.get().getMinX(), bounds);
ty = Bindings.createDoubleBinding(() -> bounds.get().getMinY(), bounds);
connector.endXProperty().bind(tx);
connector.endYProperty().bind(ty);
connector.setStroke(Color.BLACK);
pane.getChildren().add(node);
pane.getChildren().add(connector);
node.relocate(100, 100);
primaryStage.setScene(new Scene(pane, 300, 300));
primaryStage.show();
}
private VBox wrap(Circle target) {
VBox node = new VBox(new Label("Node"), new StackPane(new Rectangle(50, 50, Color.GRAY), target));
node.setOnMousePressed(event -> {
Node source = (Node) event.getSource();
startX = source.getBoundsInParent().getMinX() - event.getScreenX();
startY = source.getBoundsInParent().getMinY() - event.getScreenY();
});
node.setOnMouseDragged(event -> {
Node source = (Node) event.getSource();
double offsetX = event.getScreenX() + startX;
double offsetY = event.getScreenY() + startY;
source.relocate(offsetX, offsetY);
});
return node;
}
}

Given any Node node in a (meaning it has a parent or indirect ancestor) Pane pane, the basic idea is to do
ObjectBinding<Bounds> boundsInPaneBinding = Bindings.createObjectBinding(() -> {
Bounds nodeLocal = node.getBoundsInLocal();
Bounds nodeScene = node.localToScene(nodeLocal);
Bounds nodePane = pane.sceneToLocal(nodeScene);
return nodePane ;
}, node.boundsInLocalProperty(), node.localToSceneTransformProperty(),
pane.localToSceneTransformProperty());
Then boundsInPaneBinding is an ObservableValue<Bounds> that always contains the bounds of the Node in the Pane's coordinate system. So you can then do things like
line.startXProperty().bind(Bindings.createDoubleBinding(
() -> boundsInPaneBinding.get().getMinX(),
boundsInPaneBinding));
The tricky part here is making sure that the bindings don't get garbage collected prematurely. (See here for a discussion.) First, you need to retain a reference to the binding:
private ObjectBinding<Bounds> boundsInPaneBinding ;
and then (for reasons I can't quite figure out), the binding must actually evaluate the bound property directly:
boundsInPaneBinding = Bindings.createObjectBinding(() -> {
Bounds nodeLocal = node.getBoundsInLocal();
// note how this actually gets the value of localToSceneTransformProperty():
Bounds nodeScene = node.getLocalToSceneTransform().apply(nodeLocal);
Bounds nodePane = pane.sceneToLocal(nodeScene);
return nodePane ;
}, node.boundsInLocalProperty(), node.localToSceneTransformProperty(),
pane.localToSceneTransformProperty());

Related

FlowPane automatic horizontal space

I have a FlowPane with equally-sized rectangular children and I have this problem with the space at the right end of the flowpane, when the space is not enough to fit another column of children, I want it to be equally divided between the other columns.
An example of what I want to achieve is how the file explorer in windows behaves, see the gif bellow for reference
The default FlowPane behaviour doesn't look like this, it leaves the remaining width that can't fit a new child at the end of the region, as shown in the gif bellow
And I failed to find any API or documentation to help me achieve my goal, I thought of adding a listener on the width property and adjusting the hGap property accordingly, something like
[ flowPaneWidth - (sum of the widths of the children in one column) ] / (column count - 1)
But again, I have no idea how to figure out the column count, so any help would be appreciated.
Here is a MRE if anyone wants to try their ideas :
public class FPAutoSpace extends Application {
#Override
public void start(Stage ps) throws Exception {
FlowPane root = new FlowPane(10, 10);
root.setPadding(new Insets(10));
for(int i = 0; i < 20; i++) {
root.getChildren().add(new Rectangle(100, 100, Color.GRAY));
}
ps.setScene(new Scene(root, 600, 600));
ps.show();
}
}
After a bit of thinking, I tried to implement the idea mentioned in the question :
adding a listener on the width property and adjusting the hGap property accordingly
but I added the listener on the needsLayout property instead, as follows :
public class FPAutoSpace extends Application {
private double nodeWidth = 100;
#Override
public void start(Stage ps) throws Exception {
FlowPane root = new FlowPane();
root.setVgap(10);
for (int i = 0; i < 20; i++) {
root.getChildren().add(new Rectangle(nodeWidth, nodeWidth, Color.GRAY));
}
root.needsLayoutProperty().addListener((obs, ov, nv) -> {
int colCount = (int) (root.getWidth() / nodeWidth);
//added 4 pixels because it gets glitchy otherwise
double occupiedWidth = nodeWidth * colCount + 4;
double hGap = (root.getWidth() - occupiedWidth) / (colCount - 1);
root.setHgap(hGap);
});
StackPane preRoot = new StackPane(root);
preRoot.setPadding(new Insets(10));
preRoot.setAlignment(Pos.TOP_LEFT);
ps.setScene(new Scene(preRoot, 600, 600));
ps.show();
}
}
Not sure how this will hold up in a multi-hundred node FlowPane but it worked for the MRE
Let me know if you think there are better ways to do it.

After translating node The layout x and y stays the same

I currently have some stack panes on a pane and when dragged with a mouse they move around the pane. I do this by getting the coordinate of the mouse
and the translate x and y of the stack pane when I press the the stack pane. Then when I start to drag the stack pane I set the the translation x and y of the stack pane to the mouse coordinates when I pressed the stack pane + the difference of the new mouse coordinates and the old mouse coordinates.
My problem is after dragging the StackPane the layout x and y stays the same I want to update this as I used this else where.
My event handler when you press the StackPane:
EventHandler<MouseEvent> circleOnMousePressedEventHandler =
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
currentStackPane = ((StackPane)(t.getSource()));
orgSceneX = t.getSceneX();
orgSceneY = t.getSceneY();
layoutX = currentStackPane.getLayoutX();
layoutY = currentStackPane.getLayoutY();
}
};
My event handler when i drag the StackPane:
EventHandler<MouseEvent> circleOnMouseDraggedEventHandler =
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
double offsetX = t.getSceneX() - orgSceneX;
double offsetY = t.getSceneY() - orgSceneY;
currentStackPane.setTranslateX(offsetX);
currentStackPane.setTranslateY(offsetY);
}
};
I tried make a event handler after the drag is finished:
EventHandler<MouseEvent> circleOnMouseReleasedEventHandler =
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
currentStackPane.setLayoutX(layoutX + ((StackPane)(t.getSource())).getTranslateX());
currentStackPane.setLayoutY(layoutY + ((StackPane)(t.getSource())).getTranslateY());
currentStackPane.setTranslateX(0);
currentStackPane.setTranslateY(0);
}
};
But this doesn't seem to work. Any help would be appreciated thanks!
EDIT:
I have changed my event handlers. It seems to be updating the layout x and y correctly for the first time I drag the stack pane but when i first drag the stack pane and then release the mouse the stackpane moves to a different position then every time i drag after it messes up completely. Not sure why, any help appreciated!
EDIT2: I realised I set translate x to 0 but didnt set translate y to 0 in the mouse released event. It all works now!
To understand the problem, I would first recommend to have a look at the documentation of layoutX/layoutY properties of a Node.
public final DoubleProperty layoutXProperty
Defines the x coordinate of the translation that is added to this
Node's transform for the purpose of layout. The value should be
computed as the offset required to adjust the position of the node
from its current layoutBounds minX position (which might not be 0) to
the desired location.
For example, if textnode should be positioned at finalX
textnode.setLayoutX(finalX - textnode.getLayoutBounds().getMinX());
Failure to subtract layoutBounds minX may result in misplacement of
the node. The relocate(x, y) method will automatically do the correct
computation and should generally be used over setting layoutX
directly.
The node's final translation will be computed as layoutX + translateX,
where layoutX establishes the node's stable position and translateX
optionally makes dynamic adjustments to that position.
If the node is managed and has a Region as its parent, then the layout
region will set layoutX according to its own layout policy. If the
node is unmanaged or parented by a Group, then the application may set
layoutX directly to position it.
In short,for every node that is rendered in the scene, its position is actually a sum of its layoutX/Y and translateX/Y values in relative to its parent node. The layoutX/Y are initially updated as per its parents layout policy. For that reason, there is no point in updating/relying on layoutX/Y values of node, IF its parent (eg,.StackPane,HBox,VBox,..etc) manages it position.
Pane will not manage/decide its children layout. For that reason the default layoutX/Y values of its children is always 0.
From the above info,if we now look into your code, you are updating the translate values and setting the layout values wrongly. Instead what you have to actually do is:
Take intial values of layoutX/Y.
Update the translateX/Y while dragging.
And on mouse released recompute layoutX/Y values and reset
translateX/Y values.
Below is a quick demo of what I have described.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class PaneLayoutDemo extends Application {
double sceneX, sceneY, layoutX, layoutY;
#Override
public void start(Stage stage) throws Exception {
Pane root = new Pane();
Scene sc = new Scene(root, 600, 600);
stage.setScene(sc);
stage.show();
root.getChildren().addAll(getBox("green"), getBox("red"), getBox("yellow"));
}
private StackPane getBox(String color) {
StackPane box = new StackPane();
box.getChildren().add(new Label("Drag me !!"));
box.setStyle("-fx-background-color:" + color + ";-fx-border-width:2px;-fx-border-color:black;");
box.setPrefSize(150, 150);
box.setMaxSize(150, 150);
box.setMinSize(150, 150);
box.setOnMousePressed(e -> {
sceneX = e.getSceneX();
sceneY = e.getSceneY();
layoutX = box.getLayoutX();
layoutY = box.getLayoutY();
System.out.println(color.toUpperCase() + " Box onStart :: layoutX ::" + layoutX + ", layoutY::" + layoutY);
});
box.setOnMouseDragged(e -> {
double offsetX = e.getSceneX() - sceneX;
double offsetY = e.getSceneY() - sceneY;
box.setTranslateX(offsetX);
box.setTranslateY(offsetY);
});
box.setOnMouseReleased(e -> {
// Updating the new layout positions
box.setLayoutX(layoutX + box.getTranslateX());
box.setLayoutY(layoutY + box.getTranslateY());
// Resetting the translate positions
box.setTranslateX(0);
box.setTranslateY(0);
});
return box;
}
public static void main(String[] args) {
Application.launch(args);
}
}
Once you are familiar with the demo, try changing the root from Pane to StackPane and see the behaviour difference.

JavaFx Can't obtain stackPane position on Scene

I have a class which extends StackPane. Another one which makes a 20x20 2d Array of this class. I then translated their position somewhere on the scene. Right now, I can't obtain the position of a cell relative on the scene.
I tried Bounds, Point2D.zero and getMin..
cell.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
System.out.println("mouse click detected! " + mouseEvent.getSource());
// boundsInScene = cell.localToScene(cell.getLayoutBounds());
// System.out.println("Cell width: " + boundsInScene.getMinX());
// System.out.println("Cell height: " + boundsInScene.getMinY());
// cell.localToScene(Point2D.ZERO);
int x = (int) cell.getTranslateX();
int y = (int) cell.getTranslateY();
System.out.println("X: " + x + "y :" + y);
}
});
Part of my code:
private Parent createContent() {
root = new Pane();
root.setPrefSize(640, 480);
for (int col = 0; col < y_Tiles; col++) {
for (int row = 0; row < x_Tiles; row++) {
cell = new Cell(row, col);
grid_Array[row][col] = cell;
cell.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {});
root.getChildren().add(cell);
}
}
return root;
}
Bounds is resturning me 480 and 400. Another thing is that all cells return the same output for any of the methods I tried. Any idea how to cell the position of a cell on the scene?
PS:Sorry for bad english. Not native language
You shouldn't be using translateX/Y to set the location of your items. translateX/Y should just be used for temporary movements of an item, such as a bounce effect or a transition. layoutX/Y should be used to set the location of items. Normally layoutX/Y will be set automatically by a layout pane. If you aren't using a layout pane and are manually performing the layout in an unmanaged parent (such as a Pane or a Group), then you can manually set the layoutX/Y values as needed (relocate will do this).
Once you switch to setting the layout values rather than the translate values, then you determine the location of any node in a scene using the localToScene function.
Bounds boundsInScene = node.localToScene(node.getLayoutBounds());

Create a path transition with absolute coordinates for a StackPane object

OrangeBlock is an orange block with text inside. It is implemented as a StackPane that contains text on top of a rectangle. (This approach is demonstrated in the documentation for StackPane.)
I've placed an OrangeBlock at coordinates (100, 80) and am now trying to make it travel smoothly to some target coordinates. Unfortunately I get a nasty bump in my path:
For some reason the coordinates in the PathElements are interpreted relative to the orange block.
Why is this? And how can I make my OrangeBlock travel along a path with absolute coordinates? Minimal working example below.
import javafx.animation.PathTransition;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class PathTransitionExample extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
OrangeBlock block = new OrangeBlock(60, 40);
block.relocate(100, 80);
root.getChildren().add(block);
PathTransition transition = newPathTransitionTo(block, 460, 320);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
transition.play();
}
private static PathTransition newPathTransitionTo(OrangeBlock block,
double toX, double toY) {
double fromX = block.getLayoutX();
double fromY = block.getLayoutY();
Path path = new Path();
path.getElements().add(new MoveTo(fromX, fromY));
path.getElements().add(new LineTo(toX, toY));
PathTransition transition = new PathTransition();
transition.setPath(path);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
return transition;
}
public static void main(String[] args) {
launch(args);
}
private static class OrangeBlock extends StackPane {
public OrangeBlock(int width, int height) {
Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
Text text = new Text("Block");
getChildren().addAll(rectangle, text);
}
}
}
I debugged the JavaFX code out of curiosity. Seems like you are out of luck with a proper solution. Here's what happens:
The PathTransition code has a method interpolate(double frac) which includes:
cachedNode.setTranslateX(x - cachedNode.impl_getPivotX());
cachedNode.setTranslateY(y - cachedNode.impl_getPivotY());
The impl_getPivotX() and impl_getPivotY() methods contain this:
public final double impl_getPivotX() {
final Bounds bounds = getLayoutBounds();
return bounds.getMinX() + bounds.getWidth()/2;
}
public final double impl_getPivotY() {
final Bounds bounds = getLayoutBounds();
return bounds.getMinY() + bounds.getHeight()/2;
}
So the PathTransition always uses the center of your node for the calculation. In other words this works with e. g. a Circle node, but not with e. g. a Rectangle node. Moreover you need the layoutBounds, so the PathTransition must be created after the bounds were made available.
You can see in the PathTransition code that the calculations are all relative and already involve the layout position. So in your lineTo you have to consider this.
Worth noting is that the LineTo class has a method setAbsolut(boolean). However it doesn't solve your problem.
So my solution to your problem would be
creating the PathTransition after the primary stage was made visible
modification of the moveTo and lineTo parameters
This works for me (I added a Rectangle shape to identify the proper bounds visually):
public class PathTransitionExampleWorking2 extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
Rectangle rect = new Rectangle( 100, 80, 460-100+60, 320-80+40);
root.getChildren().add(rect);
OrangeBlock block = new OrangeBlock(60, 40);
block.relocate( 100, 80);
root.getChildren().add(block);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
// layout bounds are used in path transition => PathTransition creation must happen when they are available
PathTransition transition = newPathTransitionTo(block, 460, 320);
transition.play();
}
private static PathTransition newPathTransitionTo(OrangeBlock block, double toX, double toY) {
double fromX = block.getLayoutBounds().getWidth() / 2;
double fromY = block.getLayoutBounds().getHeight() / 2;
toX -= block.getLayoutX() - block.getLayoutBounds().getWidth() / 2;
toY -= block.getLayoutY() - block.getLayoutBounds().getHeight() / 2;
Path path = new Path();
path.getElements().add(new MoveTo(fromX, fromY));
path.getElements().add(new LineTo(toX, toY));
PathTransition transition = new PathTransition();
transition.setPath(path);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
return transition;
}
public static void main(String[] args) {
launch(args);
}
private static class OrangeBlock extends StackPane {
public OrangeBlock(int width, int height) {
Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
Text text = new Text("Block");
getChildren().addAll(rectangle, text);
}
}
}
edit: another solution would be to use this instead of MoveTo and LineTo:
public static class MoveToAbs extends MoveTo {
public MoveToAbs( Node node) {
super( node.getLayoutBounds().getWidth() / 2, node.getLayoutBounds().getHeight() / 2);
}
}
public static class LineToAbs extends LineTo {
public LineToAbs( Node node, double x, double y) {
super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
Note: You still have to create the PathTransition after the primaryStage was created.
edit: here's another example with the block moving to the position of the mouse-click:
public class PathTransitionExample extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
OrangeBlock block = new OrangeBlock(60, 40);
block.relocate(100, 80);
root.getChildren().add(block);
Label label = new Label( "Click on scene to set destination");
label.relocate(0, 0);
root.getChildren().add(label);
Scene scene = new Scene(root, 600, 400);
scene.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<Event>() {
PathTransition transition;
{
transition = new PathTransition();
transition.setNode(block);
transition.setDuration(Duration.seconds(2));
}
#Override
public void handle(Event event) {
transition.stop();
setPositionFixed(block.getLayoutX() + block.getTranslateX(), block.getLayoutY() + block.getTranslateY());
double x = ((MouseEvent) event).getX();
double y = ((MouseEvent) event).getY();
Path path = new Path();
path.getElements().add(new MoveToAbs( block));
path.getElements().add(new LineToAbs( block, x, y));
transition.setPath(path);
transition.play();
}
private void setPositionFixed( double x, double y) {
block.relocate(x, y);
block.setTranslateX(0);
block.setTranslateY(0);
}
});
primaryStage.setScene( scene);
primaryStage.show();
PathTransition transition = newPathTransitionTo(block, 460, 320);
transition.play();
}
private static PathTransition newPathTransitionTo(OrangeBlock block, double toX, double toY) {
Path path = new Path();
path.getElements().add(new MoveToAbs( block));
path.getElements().add(new LineToAbs( block, toX, toY));
PathTransition transition = new PathTransition();
transition.setPath(path);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
return transition;
}
public static void main(String[] args) {
launch(args);
}
private static class OrangeBlock extends StackPane {
public OrangeBlock(int width, int height) {
Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
Text text = new Text("Block");
getChildren().addAll(rectangle, text);
}
}
public static class MoveToAbs extends MoveTo {
public MoveToAbs( Node node) {
super( node.getLayoutBounds().getWidth() / 2, node.getLayoutBounds().getHeight() / 2);
}
}
public static class LineToAbs extends LineTo {
public LineToAbs( Node node, double x, double y) {
super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
}
The solution I'm using now is to simply offset layoutX and layoutY of the Path in the opposite direction.
private static void offsetPathForAbsoluteCoords(Path path, OrangeBlock block) {
Node rectangle = block.getChildren().iterator().next();
double width = rectangle.getLayoutBounds().getWidth();
double height = rectangle.getLayoutBounds().getHeight();
path.setLayoutX(-block.getLayoutX() + width / 2);
path.setLayoutY(-block.getLayoutY() + height / 2);
}
Inserting a call to this method immediately after the Path instantiation fixes the problem.
I'm not really satisfied with this solution. I don't understand why layoutX and layoutY need to be involved at all. Is there a neater way?
So Basically,
the relocate(x,y) method sets the layout x/y values...
the Transition uses the translateX/Y values...
I could be wrong but I believe that when either value gets invalidated the scene runs a "layout" pass on the nodes in scene.
When this happens it tries to set the node to it's known layoutX/Y values, which are set when you call relocate(x,y) (layout values are 0,0 by default).
This causes the node to be drawn in "layoutPosition" then "pathPosition" within the transition at each step, causing jitters and the node to be offset from where it should be.
#Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
OrangeBlock block = new OrangeBlock(60, 40);
System.out.println(block.getLayoutX() + " : " + block.getLayoutY());
block.relocate(100, 80);
//block.setTranslateX(100);
//block.setTranslateY(80);
System.out.println(block.getLayoutX() + " : " + block.getLayoutY());
root.getChildren().add(block);
PathTransition transition = newPathTransitionTo(block, 460, 320);
transition.currentTimeProperty().addListener(e->{
System.out.println("\nLayout Values: " + block.getLayoutX() + " : " + block.getLayoutY()
+"\nTranslate Values:" + block.getTranslateX() + " : " + block.getTranslateY()
);});
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
transition.play();
}
private static PathTransition newPathTransitionTo(OrangeBlock block,
double toX, double toY) {
double fromX = block.getLayoutX();//getTranslateX();
double fromY = block.getLayoutY();//getTranslateY();
Path path = new Path();
path.getElements().add(new MoveTo(fromX, fromY));
path.getElements().add(new LineTo(toX, toY));
PathTransition transition = new PathTransition();
transition.setPath(path);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
return transition;
}
public static void main(String[] args) {
launch(args);
}
private static class OrangeBlock extends StackPane {
public OrangeBlock(int width, int height) {
Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
Text text = new Text("Block");
getChildren().addAll(rectangle, text);
}
}
Not going to post pics, But my first result was like your first post, changing it to the above code gave me the second post result.
Usually it is always best to avoid "layout" values on a dynamic (moving) object for these reasons. If you like the convenience of the relocate method, I'd implement your own setting the translate values instead.
Cheers :)
EDIT:
I edited some code to print what happens as the transition is running so you can see what happens in your original version..
Both #jdub1581 and #Lorand have given valid points:
Transition is applied modifying block's translateXProperty() and translateYProperty().
Transition is applied on the center of the block.
I'll add one more thing:
We are mixing two different things: the global path we want the block to follow, and the local path we have to apply to the transition, so the block follows the first one.
Let's add a pathScene to the group:
Path pathScene = new Path();
pathScene.getElements().add(new MoveTo( block.getLayoutX(), block.getLayoutY()));
pathScene.getElements().add(new LineTo(460, 320));
root.getChildren().add(pathScene);
This will be our scene now (I've added two labels with the coordinates of the origin and end of the path for clarity):
Now we need to determine the local path, so we'll change pathScene elements to local coordinates of the block, and translate it to its center:
Path pathLocal = new Path();
pathScene.getElements().forEach(elem->{
if(elem instanceof MoveTo){
Point2D m = block.sceneToLocal(((MoveTo)elem).getX(),((MoveTo)elem).getY());
Point2D mc = new Point2D(m.getX()+block.getWidth()/2d,m.getY()+block.getHeight()/2d);
pathLocal.getElements().add(new MoveTo(mc.getX(),mc.getY()));
} else if(elem instanceof LineTo){
Point2D l = block.sceneToLocal(((LineTo)elem).getX(),((LineTo)elem).getY());
Point2D lc = new Point2D(l.getX()+block.getWidth()/2d,l.getY()+block.getHeight()/2d);
pathLocal.getElements().add(new LineTo(lc.getX(),lc.getY()));
}
});
As #Lorand also mentioned, this should be done after the stage is shown to compute the size of the block.
Now we can create the transition and play it.
PathTransition transition = new PathTransition();
transition.setPath(pathLocal);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
transition.play();
Finally, this is all the code we need to make the block follow the desired path:
#Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
OrangeBlock block = new OrangeBlock(60, 40);
block.relocate(100, 80);
root.getChildren().add(block);
// Path in scene coordinates, added to group
// in order to visualize the transition path for the block to follow
Path pathScene = new Path();
pathScene.getElements().add(new MoveTo(block.getLayoutX(), block.getLayoutY()));
pathScene.getElements().add(new LineTo(460, 320));
root.getChildren().add(pathScene);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
PathTransition transition = newPathTransitionTo(pathScene, block);
transition.play();
}
private PathTransition newPathTransitionTo(Path pathScene, OrangeBlock block) {
// Calculate the path in local coordinates of the block
// so transition is applied to the block without bumps
Path pathLocal = new Path();
pathScene.getElements().forEach(elem->{
if(elem instanceof MoveTo){
Point2D m = block.sceneToLocal(((MoveTo)elem).getX(),((MoveTo)elem).getY());
Point2D mc = new Point2D(m.getX()+block.getWidth()/2d,m.getY()+block.getHeight()/2d);
pathLocal.getElements().add(new MoveTo(mc.getX(),mc.getY()));
} else if(elem instanceof LineTo){
Point2D l = block.sceneToLocal(((LineTo)elem).getX(),((LineTo)elem).getY());
Point2D lc = new Point2D(l.getX()+block.getWidth()/2d,l.getY()+block.getHeight()/2d);
pathLocal.getElements().add(new LineTo(lc.getX(),lc.getY()));
}
});
PathTransition transition = new PathTransition();
transition.setPath(pathLocal);
transition.setNode(block);
transition.setDelay(Duration.seconds(1));
transition.setDuration(Duration.seconds(2));
return transition;
}
public static void main(String[] args) {
launch(args);
}
private static class OrangeBlock extends StackPane {
public OrangeBlock(int width, int height) {
Rectangle rectangle = new Rectangle(width, height, Color.ORANGE);
Text text = new Text("Block");
getChildren().addAll(rectangle, text);
}
}
Note that this solution is equivalent to the one given by #Lorand.
If we monitorize the X, Y translate properties of the block, these go from (0,0) to (360,240), which are just the relative ones on the global path.
You're using relocate function to locate your Block. relocate function makes computation on x and y for locating your object. If you used setLayoutX to locate Block and then use getLayoutX, this problem might not be happened. Same explanations is valid for y property.
You can find some informations about your problem in here: http://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html#layoutXProperty

JavaFX MouseEvent not firing after moving nodes in GridPane

I am attempting to create a "draggable" histogram UI with JavaFX. I have a ScrollPane containing a GridPane with 1 column and lots of rows. In each row is an HBox containing a label. Every 10 rows, there is also an HBox containing a Line.
I tried to make the HBoxes containing lines draggable by setting onMousePressed, onMouseDragged, and onMouseReleased event handlers (shown below). It works if I drag and release an hbox-line above its starting point - it ends up in whatever grid row I put it in, and I can click and drag it again. However, if I drag and release a line below its starting point, I can't get any more mouseEvents for that hBox. I tried adding log statements everywhere, nothing. I tried setting onMouseOver, it also was not fired.
Why would moving an hbox around the grid like this work for dragging up but not down?
lineContainer.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
EventTarget target = mouseEvent.getTarget();
lastY = mouseEvent.getSceneY();
}
});
lineContainer.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
Node target = (Node) mouseEvent.getTarget();
HBox hBox = null;
if (target instanceof HBox) {
hBox = (HBox) target;
}
else if (target instanceof Line) {
hBox = (HBox) target.getParent();
}
else { //should never happen
log.info("target not hbox or line: " + target.getClass());
}
if (mouseEvent.getSceneY() <= (lastY - 15)) {
int row = GridPane.getRowIndex(hBox);
GridPane.setRowIndex(hBox, --row);
lastY = mouseEvent.getSceneY();
lastRow = row - 1;
} else if (mouseEvent.getSceneY() >= (lastY + 15)) {
int row = GridPane.getRowIndex(hBox);
GridPane.setRowIndex(hBox, ++row);
lastRow = row - 1;
lastY = mouseEvent.getSceneY();
}
}
});
lineContainer.setOnMouseReleased(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
Node tar = (Node) mouseEvent.getTarget();
HBox hBox = null;
if (tar instanceof HBox) {
hBox = (HBox) tar;
}
else if (tar instanceof Line && tar.getParent() instanceof HBox) {
hBox = (HBox) tar.getParent();
}
else { //should never happen
log.info(mouseEvent.getTarget().getClass().toString());
}
}
});
UPDATE: I managed to get it working by creating a new HBox, resetting the onMouse... handlers, and copying its children every time the mouse is released. But I still don't know what was causing the original issue...
The following isn't a direct solution for you, but I wanted to say I have a similar problem and share my observations.
My application allows dragging in both axes (X, Y). All I've been able to figure out that some invisible element is 'obscuring' the MouseEvent hitbox. Testing it using an 'MS minesweeper' approach, shows this interfering area to extend from coords (0,0) of the root to (maxX,maxY) of another Node I have in the scene which is a layer above.
My problem was solved by changing z-order of Parent objects (let's call them layers) containing the Nodes that didn't receive their MouseEvents.
JavaFX MouseEvent doc explains that it is only the top level node which receives the event:
https://docs.oracle.com/javafx/2/api/javafx/scene/input/MouseEvent.html
Also look at the pickOnBounds property: JavaFX: How to make a Node partially mouse transparent?

Resources