Showing texts over the face of a Box based on the visible area on zooming in/out - javafx

I have a sample 3D application (built by taking reference from the Javafx sample 3DViewer) which has a table created by laying out Boxes and Panes:
The table is centered wrt (0,0,0) coordinates and camera is at -z position initially.
It has the zoom-in/out based on the camera z position from the object.
On zooming in/out the object's boundsInParent increases/decreases i.e. area of the face increases/decreases. So the idea is to put more text when we have more area (always confining within the face) and lesser text or no text when the face area is too less. I am able to to do that using this node hierarchy:
and resizing the Pane (and managing the vBox and number of texts in it) as per Box on each zoom-in/out.
Now the issue is that table boundsInParent is giving incorrect results (table image showing the boundingBox off at the top) whenever a text is added to the vBox for the first time only. On further zooming-in/out gives correct boundingBox and does not go off.
Below is the UIpane3D class:
public class UIPane3D extends Pane
{
VBox textPane;
ArrayList<String> infoTextKeys = new ArrayList<>();
ArrayList<Text> infoTextValues = new ArrayList<>();
Rectangle bgCanvasRect = null;
final double fontSize = 16.0;
public UIPane3D() {
setMouseTransparent(true);
textPane = new VBox(2.0)
}
public void updateContent() {
textPane.getChildren().clear();
getChildren().clear();
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
textPane.setTranslateY(getHeight() / 2 - textPane.getHeight() / 2.0);
bgCanvasRect = new Rectangle(getWidth(), getHeight());
bgCanvasRect.setFill(Color.web(Color.BURLYWOOD.toString(), 0.10));
bgCanvasRect.setVisible(true);
getChildren().addAll(bgCanvasRect, textPane);
}
public void resetInfoTextMap()
{
if (infoTextKeys != null || infoTextValues != null)
{
try
{
infoTextKeys.clear();
infoTextValues.clear();
} catch (Exception e){e.printStackTrace();}
}
}
public void updateInfoTextMap(String pKey, String pValue)
{
int index = -1;
boolean objectFound = false;
for (String string : infoTextKeys)
{
index++;
if(string.equals(pKey))
{
objectFound = true;
break;
}
}
if(objectFound)
{
infoTextValues.get(index).setText(pValue.toUpperCase());
}
else
{
if (pValue != null)
{
Text textNode = new Text(pValue.toUpperCase());
textNode.setFont(Font.font("Consolas", FontWeight.BLACK, FontPosture.REGULAR, fontSize));
textNode.wrappingWidthProperty().bind(widthProperty());
textNode.setTextAlignment(TextAlignment.CENTER);
infoTextKeys.add(pKey);
infoTextValues.add(textNode);
}
}
}
}
The code which get called at the last after all the manipulations:
public void refreshBoundingBox()
{
if(boundingBox != null)
{
root3D.getChildren().remove(boundingBox);
}
PhongMaterial blueMaterial = new PhongMaterial();
blueMaterial.setDiffuseColor(Color.web(Color.CRIMSON.toString(), 0.25));
Bounds tableBounds = table.getBoundsInParent();
boundingBox = new Box(tableBounds.getWidth(), tableBounds.getHeight(), tableBounds.getDepth());
boundingBox.setMaterial(blueMaterial);
boundingBox.setTranslateX(tableBounds.getMinX() + tableBounds.getWidth()/2.0);
boundingBox.setTranslateY(tableBounds.getMinY() + tableBounds.getHeight()/2.0);
boundingBox.setTranslateZ(tableBounds.getMinZ() + tableBounds.getDepth()/2.0);
boundingBox.setMouseTransparent(true);
root3D.getChildren().add(boundingBox);
}
Two things:
The table3D's boundsInParent is not updated properly when texts are added for the first time.
What would be the right way of putting texts on 3D nodes? I am having to manipulate a whole lot to bring the texts as required.
Sharing code here.

For the first question, about the "jump" that can be noticed just when after scrolling a new text item is laid out:
After digging into the code, I noticed that the UIPane3D has a VBox textPane that contains the different Text nodes. Every time updateContent is called, it tries to add a text node, but it checks that the vbox's height is always lower than the pane's height, or else the node will be removed:
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
While this is basically correct, when you add a node to the scene, you can't get textPane.getHeight() immediately, as it hasn't been laid out yet, and you have to wait until the next pulse. This is why the next time you scroll, the height is correct and the bounding box is well placed.
One way to force the layout and get the correct height of the textNode is by forcing css and a layout pass:
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
// force css and layout
textPane.applyCss();
textPane.layout();
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
Note that:
This method [applyCss] does not normally need to be invoked directly but may be used in conjunction with Parent.layout() to size a Node before the next pulse, or if the Scene is not in a Stage.
For the second question, about a different solution to add Text to 3D Shape.
Indeed, placing a (2D) text on top of a 3D shape is quite difficult, and requires complex maths (that are done quite nicely in the project, by the way).
There is an alternative avoiding the use of 2D nodes directly.
Precisely in a previous question, I "wrote" into an image, that later on I used as the material diffuse map of a 3D shape.
The built-in 3D Box places the same image into every face, so that wouldn't work. We can implement a 3D prism, or we can make use of the CuboidMesh node from the FXyz3D library.
Replacing the Box in UIPaneBoxGroup:
final CuboidMesh contentShape;
UIPane3D displaypane = null;
PhongMaterial shader = new PhongMaterial();
final Color pColor;
public UIPaneBoxGroup(final double pWidth, final double pHeight, final double pDepth, final Color pColor) {
contentShape = new CuboidMesh(pWidth, pHeight, pDepth);
this.pColor = pColor;
contentShape.setMaterial(shader);
getChildren().add(contentShape);
addInfoUIPane();
}
and adding the generateNet method:
private Image generateNet(String string) {
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
Label label5 = new Label(string);
label5.setFont(Font.font("Consolas", FontWeight.BLACK, FontPosture.REGULAR, 40));
GridPane.setHalignment(label5, HPos.CENTER);
grid.add(label5, 3, 1);
double w = contentShape.getWidth() * 10; // more resolution
double h = contentShape.getHeight() * 10;
double d = contentShape.getDepth() * 10;
final double W = 2 * d + 2 * w;
final double H = 2 * d + h;
ColumnConstraints col1 = new ColumnConstraints();
col1.setPercentWidth(d * 100 / W);
ColumnConstraints col2 = new ColumnConstraints();
col2.setPercentWidth(w * 100 / W);
ColumnConstraints col3 = new ColumnConstraints();
col3.setPercentWidth(d * 100 / W);
ColumnConstraints col4 = new ColumnConstraints();
col4.setPercentWidth(w * 100 / W);
grid.getColumnConstraints().addAll(col1, col2, col3, col4);
RowConstraints row1 = new RowConstraints();
row1.setPercentHeight(d * 100 / H);
RowConstraints row2 = new RowConstraints();
row2.setPercentHeight(h * 100 / H);
RowConstraints row3 = new RowConstraints();
row3.setPercentHeight(d * 100 / H);
grid.getRowConstraints().addAll(row1, row2, row3);
grid.setPrefSize(W, H);
grid.setBackground(new Background(new BackgroundFill(pColor, CornerRadii.EMPTY, Insets.EMPTY)));
new Scene(grid);
return grid.snapshot(null, null);
}
Now all the 2D related code can be removed (including displaypane), and after a scrolling event get the image:
public void refreshBomUIPane() {
Image net = generateNet(displaypane.getText());
shader.setDiffuseMap(net);
}
where in UIPane3D:
public String getText() {
return infoTextKeys.stream().collect(Collectors.joining("\n"));
}
I've also removed the bounding box to get this picture:
I haven't played around with the number of text nodes that can be added to the VBox, the font size nor with an strategy to avoid generating images on every scroll: only when the text changes this should be done. So with the current approach is quite slow, but it can be improved notably as there are only three possible images for each box.

Related

Scaling start screen with fullscreen JavaFX

I'm stuck on full scaling for my JavaFX application. I'm in the process of making a full screen feature for the application and I'm running into issues on trying to get the aspect ratio and positioning right without manually editing the values.
With the way I've been trying, the values butcher the game's start screen making the positioning change making the designs of the game offset from the center of the application. I can understand the reasoning behind it with the way I set it up. My problem is wondering how to scale the start screen and keep it's original position without having to manually edit the values.
What I thought of was trying to input the value and having it scale according to that value then putting the result in the position of objects X and Y.
if (fullscreen) {
WIDTH = (Enter aspect ratio here) * 1.5;
HEIGHT = (Enter aspect ratio here) * 1.5;
} else {
WIDTH = 990;
HEIGHT = 525;
}
with Obvious flaws this butchers the start screen.
My solution was to make a double() that you just enter the value of the application WIDTH/HEIGHT then entering the amount you want to divide by (since I couldn't come up with exact cords, I grabbed the WIDTH and divided by specific value for it to align in the center) following with a boolean to state whether it's full screened or not. Though my only issue with this theory is that it'll only work with 1920x1080 monitors so I'd assume I would have to manually enter all types of aspect ratios to make it fit otherwise the start screen would be butchered.
I've seen a way of scaling here:
JavaFX fullscreen - resizing elements based upon screen size
Though I'm not sure how to correctly implement it.
public static boolean fullscreen = false;
public static double WIDTH = 990;
public static double HEIGHT = 525;
public static Pane pane = new Pane();
public static void StartScreen() {
pane.setPrefSize(WIDTH, (HEIGHT - 25)); // the 25 is for the text field/input.
pane.setStyle("-fx-background-color: transparent;");
Group sGroup = new Group();
Image i = new Image("file:start/So7AA.png");
ImageView outer = new ImageView(i);
// outer.setX(Ce.WIDTH/4.75); //4.75 // The functioning code for the snippit
// outer.setY(-10); //-10
outer.setX(Ce.WIDTH/position(3.60, fullscreen)); //4.75 // The non functioning code.
outer.setY(position(-1, Ce.fullscreen)); //-10
outer.setFitWidth(550);
outer.setFitHeight(550);
outer.setOpacity(.3);
GaussianBlur gBlur = new GaussianBlur();
gBlur.setRadius(50);
ImageView seal = new ImageView(i);
// seal.setX(Ce.WIDTH/3.83); //247.5 - 3.83
// seal.setY(39); //39
seal.setX(Ce.WIDTH/position(3.83, fullscreen)); //247.5 - 3.83
seal.setY(position(32, Ce.fullscreen)); //39
seal.setFitWidth(450);
seal.setFitHeight(450);
ImageView sealBlur = new ImageView(i);
// sealBlur.setX(Ce.WIDTH/3.83); //247.5 - 3.83
// sealBlur.setY(39); //39
sealBlur.setX(Ce.WIDTH/position(3.83, fullscreen)); //247.5 - 3.83
sealBlur.setY(position(32, Ce.fullscreen));
sealBlur.setFitWidth(450);
sealBlur.setFitHeight(450);
sealBlur.setEffect(gBlur);
}
For getting the values of the WIDTH and HEIGHT:
public static double getWidth(double W, boolean fs) {
if (fs) {
return WIDTH = Screen.getPrimary().getBounds().getMaxX();
} else {
return WIDTH = W;
}
}
public static double getHeight(double H, boolean fs) {
if (fs) {
return HEIGHT = Screen.getPrimary().getBounds().getMaxY();
} else {
return HEIGHT = H;
}
}
I know there's a way around this, I'm just not sure how to pull it off.
I'm not sure exactly what the requirements are here, but it looks like you have three images, which you want centered, and you want them all scaled by the same amount so that one of the images fills the available space in its container. (Then, you just need to make sure its container grows to fill all the space, and you can call stage.setFullScreen(true) or stage.setMaximized(true) as needed.)
You can do this with a pretty simple custom pane that manages the layout in the layoutChildren() method:
public class ImagePane extends Region {
private final Image image1;
private final ImageView imageView1;
private final Image image2;
private final ImageView imageView2;
private final Image image3;
private final ImageView imageView3;
public ImagePane(Image image1, Image image2, Image image3) {
this.image1 = image1;
this.image2 = image2;
this.image3 = image3;
imageView1 = new ImageView(image1);
imageView2 = new ImageView(image2);
imageView3 = new ImageView(image3);
getChildren().addAll(imageView1, imageView2, imageView3);
}
#Override
protected void layoutChildren() {
double xScale = getWidth() / image1.getWidth();
double yScale = getHeight() / image1.getHeight();
double scale = Math.min(xScale, yScale);
for (ImageView view : List.of(imageView1, imageView2, imageView3) {
scaleAndCenter(view, scale);
}
}
private void scaleAndCenter(ImageView view, scale) {
double w = scale * view.getImage().getWidth();
double h = scale * view.getImage().getHeight();
view.setFitWidth(w);
view.setFitHeight(h);
view.relocate((getWidth()-w) / 2, (getHeight()-h) / 2);
}
}
The rest of your layout looks something like:
Label label = new Label("Type in 'start'.\nType in 'options' for options.\n(Demo)");
TextField textField = new TextField();
ImagePane imagePane = new ImagePane(new Image(...), new Image(...), new Image(...));
AnchorPane anchor = new AnchorPane(imagePane, label);
AnchorPane.setTopAnchor(imagePane, 0.0);
AnchorPane.setRightAnchor(imagePane, 0.0);
AnchorPane.setBottomAnchor(imagePane, 0.0);
AnchorPane.setLeftAnchor(imagePane, 0.0);
AnchorPane.setTopAnchor(label, 5.0);
AnchorPane.setLeftAnchor(label, 5.0);
BorderPane root = new BorderPane();
root.setCenter(anchor);
root.setBottom(textField);
Now everything should just respond to whatever size is assigned to the root pane, so setting full screen mode should "just work".

Preventing overlapping shapes while dragging on a Pane

I've looked at similar questions but they all are concerned with collision detection rather than preventing overlap. I've gotten most of it to work with the below code:
private final EventHandler<MouseEvent> onPress = mouseEvent -> {
xDrag = this.getCenterX() - mouseEvent.getX();
yDrag = this.getCenterY() - mouseEvent.getY();
};
private final EventHandler<MouseEvent> onDrag = mouseEvent -> {
for (Shape shape : getAllShapes()) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
return;
}
}
}
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
};
However, the problem is, once there is a tiniest bit of overlap, the Shape is no longer draggable at all. Meaning, if I drag a shape to another, once they become essentially tangent, neither of them are draggable anymore. What I want to happen is just that, for example, if you try to drag a circle onto another, the circle won't follow the mouse position as long as the future position of the drag will cause an overlap.
I can't figure out exactly how to accomplish this.
EDIT: Minimum Reproducible Example:
Main.java
public class Main extends Application {
static Circle circle1 = new DraggableCircle(100, 200);
static Circle circle2 = new DraggableCircle(200, 300);
static Circle[] circleList = new Circle[]{circle1, circle2};
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World");
Pane pane = new Pane();
primaryStage.setScene(new Scene(pane, 300, 275));
primaryStage.show();
pane.getChildren().addAll(circle1, circle2);
}
public static void main(String[] args) {
launch(args);
}
}
DraggableCircle.java
public class DraggableCircle extends Circle {
private double xDrag, yDrag;
public DraggableCircle(double x, double y) {
super(x, y, 30);
this.setFill(Color.WHITE);
this.setStroke(Color.BLACK);
this.setStrokeWidth(1.5);
this.addEventHandler(MouseEvent.MOUSE_PRESSED, onPress);
this.addEventHandler(MouseEvent.MOUSE_DRAGGED, onDrag);
}
private final EventHandler<MouseEvent> onPress = (mouseEvent) -> {
xDrag = this.getCenterX() - mouseEvent.getX();
yDrag = this.getCenterY() - mouseEvent.getY();
};
private final EventHandler<MouseEvent> onDrag = mouseEvent -> {
for (Shape shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
return;
}
}
}
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
};
}
This also has an issue where dragging too quickly causes a noticeable overlap between the circles, before the drag detection ends.
A simple (imperfect) solution
The following algorithm will allow a node to continue to be dragged after an intersection has occurred:
Record the current draggable shape position.
Set the new position.
Check the intersection.
If an intersection is detected, reset the position to the original position.
An implementation replaces the drag handler in the supplied minimal example code from the question.
private final EventHandler<MouseEvent> onDrag = (mouseEvent) -> {
double priorCenterX = getCenterX();
double priorCenterY = getCenterY();
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
for (Shape shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
this.setCenterX(priorCenterX);
this.setCenterY(priorCenterY);
return;
}
}
}
};
This handler does work better than what you had, it does at least allow you to continue dragging after the intersection.
But, yes, if you drag quickly it will leave a visible space between nodes when it has detected that the drag operation would cause an intersection, which isn't ideal.
Also, the additional requirement you added in your comment about having the dragged shape glide along a border would require a more sophisticated solution.
Other potential solutions
I don't offer code for these more sophisticated solutions here.
One potential brute force solution is to interpolate the prior center with the new center and then, in a loop, slowly move the dragged object along the interpolated line until an intersection is detected, then just back it out to the last interpolated value to prevent the intersection. You can do this by calculating and applying a normalized (1 unit distance) movement vector. That might fix space between intersected nodes.
Similarly to get the gliding, on the intersection, you could just update either the interpolated x or y value rather than both.
There may be more sophisticated methods with geometry math applied, especially if you know shape geometry along with movement vectors and surface normals.
+1 for #jewelsea answer.
On top of #jewelsea answer, I would like to provide a fix for the "space between nodes" issue.
So you might have already observed that when you drag fast, it will not cover each and every pixel in the drag path. It varies with the speed of the drag. So when you decide to move it to the previous recorded point, we will do a quick math, to see if there is any gap between the two nodes, if yes:
We will do a math "to determine a point along a line which is at distance d" and move the drag circle to that point. Here..
start point of line is : previous recorded point
end point of line is : the intersected shape center
d is : the gap between the two shapes.
So the updated code to the #jewelsea answer is as below:
private final EventHandler<MouseEvent> onDrag = (mouseEvent) -> {
double priorCenterX = getCenterX();
double priorCenterY = getCenterY();
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
for (Circle shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
Point2D cx = new Point2D(priorCenterX, priorCenterY);
Point2D px = new Point2D(shape.getCenterX(), shape.getCenterY());
double d = cx.distance(px);
if (d > getRadius() + shape.getRadius()) {
cx = pointAtDistance(cx, px, d - getRadius() - shape.getRadius());
}
this.setCenterX(cx.getX());
this.setCenterY(cx.getY());
return;
}
}
}
};
private Point2D pointAtDistance(Point2D p1, Point2D p2, double distance) {
double lineLength = p1.distance(p2);
double t = distance / lineLength;
double dx = ((1 - t) * p1.getX()) + (t * p2.getX());
double dy = ((1 - t) * p1.getY()) + (t * p2.getY());
return new Point2D(dx, dy);
}

Slider Puzzle in javaFX - Swapping a button if it borders on the blank button

I am coding a slider puzzle, using a 2-D array of buttons where 1 button is blank at all times, and the remaining buttons can only move if they are next to the blank button. Would anyone be able to offer a way to check the surrounding buttons and determine if it borders on the blank one? (keeping in mind boundaries)
Create fields blankX and blankY that contain the current position of the blank.
For each button save the current position in the properties. This allows you to retrieve the coordinates of the button, check, if they are adjacent to the blank and swap the positions, if that's the case.
Example:
private int blankX = 0;
private int blankY = 0;
private static final int SIZE = 50;
private static final String X_KEY = "TileLocationX";
private static final String Y_KEY = "TileLocationY";
private void move(Node n, int x, int y) {
// adjust position
n.setLayoutX(blankX * SIZE);
n.setLayoutY(blankY * SIZE);
// save coordinates to property
n.getProperties().put(X_KEY, blankX);
n.getProperties().put(Y_KEY, blankY);
// save node pos as blank pos
blankX = x;
blankY = y;
}
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
EventHandler<ActionEvent> handler = evt -> {
Node n = (Node) evt.getSource();
int x = (Integer) n.getProperties().get(X_KEY);
int y = (Integer) n.getProperties().get(Y_KEY);
if (Math.abs(x - blankX) + Math.abs(y - blankY) == 1) {
// move button, if it's adjacent to the blank
move(n, x, y);
}
};
int count = 1;
// create grid
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if (i != blankX || j != blankY) {
Button button = new Button(Integer.toString(count++));
button.setPrefSize(SIZE, SIZE);
button.setLayoutX(SIZE * i);
button.setLayoutY(SIZE * j);
button.getProperties().put(X_KEY, i);
button.getProperties().put(Y_KEY, j);
button.setOnAction(handler);
pane.getChildren().add(button);
}
}
}
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
2D arrays make it easy. Check for null above, below, right, and left, making sure that you aren't going out of bounds (aka checking boxes off the actual game grid).
I'm not sure if JavaFX is different from Java, so I can only give pseudo code:
//initialize int[5][5] (position) with tile classes (Tile), setting one to null
//checking tile at position[a][b]
//check position[a-1][b], [a+1][b], [a][b-1], [a][b+1] for null...
//...also making sure the value is between 0 and 5
//if any of those is true, square is moveable!
There might be an even more efficient way to do this, however. If you add another variable (boolean isMoveable) to each tile, then you can start each turn by finding the empty square and simply checking off every tile it borders as moveable. This would even allow you to highlight them somehow at the beginning of the turn, so the player knows what he can move.
And if you don't want to search through the entire board for the empty square, you can simply do it once at the beginning and save it's position to an array. Next turn, look to each square bordering that position to find the empty one.
If you have a 2D array, then you have the x and y location of each button. Keep the location of the blank button in variables, say blankX and blankY.
Then the condition for a button to be adjacent to the blank button is
Either x==blankX and y and blankY differ by 1, or
y==blankY and x and blankX differ by 1
So if you do
int deltaX = Math.abs(x - blankX) ;
int deltaY = Math.abs(y - blankY) ;
the condition is
deltaX==0 && deltaY==1, or
deltaX==1 && deltaY==0
but since deltaX >= 0 and deltaY >= 0, this is just equivalent to
deltaX + deltaY == 1
If you make blankX and blankY JavaFX properties:
private final IntegerProperty blankX = new SimpleIntegerProperty();
private final IntegerProperty blankY = new SimpleIntegerProperty();
then you can easily disable the button when it's not adjacent to the blank buttons:
// creates a button with coordinates x,y and initializes it
private Button createButton(int x, int y) {
Button button = new Button(Integer.toString(x + y * numColumns + 1));
// disable if not adjacent to blank button:
button.disableProperty().bind(Bindings.createBooleanBinding(
() -> Math.abs(x - blankX.get()) + Math.abs(y - blankY.get()) != 1,
blankX, blankY));
// when pressed, swap with blank button and update blankX, blankY:
button.setOnAction(e -> {
buttonArray[blankX.get()][blankY.get()].setText(button.getText());
button.setText("");
blankX.set(x);
blankY.set(y);
});
return button ;
}

How to set position of a Node relative to another Node in JavaFX?

I have two controls in my JavaFx Application
1. TextField
2. ListView
I want to place ListView just below the TextField using co-ordinates of TextField.
Because position of TextField changes.
Details :
I am creating auto suggestion text field for my project which requires to pop up a list when users type something in text field. that text field may exist anywhere in the scene it can be in another list view also. So what I need precisely is how to obtain co-ordinates of text field on screen so that I can place list view just below the text field.
I see multiple options here:
I think you might be better of using a ContextMenu. When you show a ContextMenu you can define a view that it should be shown relative to and a side of the view to show it.
Should a ContextMenu not fulfill your demands, the easiest idea that I have is that you replace your TextField with a VBox and input the TextField and the ListView in the VBox. That will make them be together and you can just position the VBox like you would position your TextField.
Last if you want to lay it out yourself, the TextField contains Properties like layoutX and layoutY and height and width. Using those you can calculate where to position your ListView (e.g. Y position of ListView would be textField.layoutY + textField.height). To keep this position up to date I see two options:
To position the TextField you have probably overwritten the layoutChildrens method of some Parent, so you can probably put this code there.
If not, you can also register ChangeListeners on those Properties to get notified when they change and reposition your ListView accordingly.
To get the location of a text field in the screen coordinates, you can do
Bounds boundsInScreen = textField.localToScreen(textField.getBoundsInLocal());
This returns a Bounds object, from which you can get the various coordinates, e.g.
Popup popup = ... ;
popup.setWidth(boundsInScreen.getWidth());
popup.show(textField, boundsInScreen.getMinX(), boundsInScreen.getMaxY());
I Created this helper class that does just that.
public class PositionUtils {
public static void position(Region movable, Region reference, Anchor movableAnchor, Anchor referenceAnchor, float xOffset, float yOffset) {
double x = reference.localToScene(reference.getBoundsInLocal()).getMaxX();
double y = reference.localToScene(reference.getBoundsInLocal()).getMaxY();
Point referencePoint = referenceAnchor.referencePoint;
Point movablePoint = movableAnchor.movablePoint;
x -= (0.5 * movable.getWidth()) * movablePoint.x;
y += (0.5 * movable.getHeight()) * movablePoint.y;
x += (0.5 * reference.getWidth()) * referencePoint.x;
y -= (0.5 * reference.getHeight()) * referencePoint.y;
movable.setLayoutX(x + xOffset);
movable.setLayoutY(y + yOffset);
}
public static void position(Pane movable, Pane reference, Anchor movableAnchor, Anchor referenceAnchor) {
position(movable, reference, movableAnchor, referenceAnchor, 0, 0);
}
public static void position(Pane movable, Pane reference, Side side) {
position(movable, reference, side, 0, 0);
}
public static void position(Pane movable, Pane reference, Side side, float xOffset, float yOffset) {
Anchor movableAnchor = null;
Anchor referenceAnchor = null;
switch (side) {
case TOP:
movableAnchor = Anchor.BOTTOM_CENTER;
referenceAnchor = Anchor.TOP_CENTER;
break;
case BOTTOM:
movableAnchor = Anchor.TOP_CENTER;
referenceAnchor = Anchor.BOTTOM_CENTER;
break;
case RIGHT:
movableAnchor = Anchor.CENTER_LEFT;
referenceAnchor = Anchor.CENTER_RIGHT;
break;
case LEFT:
movableAnchor = Anchor.CENTER_RIGHT;
referenceAnchor = Anchor.CENTER_LEFT;
break;
}
position(movable, reference, movableAnchor, referenceAnchor, xOffset, yOffset);
}
public enum Anchor {
TOP_LEFT(new Point(0, 0), new Point(-2, 2)),
TOP_CENTER(new Point(1, 0), new Point(-1, 2)),
TOP_RIGHT(new Point(2, 0), new Point(0, 2)),
CENTER_LEFT(new Point(0, -1), new Point(-2, 1)),
CENTER(new Point(1, -1), new Point(-1, 1)),
CENTER_RIGHT(new Point(1, -2), new Point(0, 1)),
BOTTOM_LEFT(new Point(0, -2), new Point(-2, 0)),
BOTTOM_CENTER(new Point(1, -2), new Point(-1, 0)),
BOTTOM_RIGHT(new Point(2, -2), new Point(0, 0));
public Point referencePoint;
public Point movablePoint;
Anchor(Point movablePoint, Point referencePoint) {
this.referencePoint = referencePoint;
this.movablePoint = movablePoint;
}
}
public enum Side {
TOP,
BOTTOM,
RIGHT,
LEFT
}
}

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());

Resources