Add a node in the middle of a line java fx - javafx

I have a line and I want a square with text inside of the square to be placed in the middle of this line.
I have created the square with text using a stack pane. This line is draggable so I want the square to stay in the middle of this line when it is being dragged.
I tried:
weightSquare.layoutXProperty().bind((line.startXProperty().add(line.endXProperty())).divide(2).add(line.translateXProperty()));
weightSquare.layoutYProperty().bind((line.startYProperty().add(line.endYProperty())).divide(2).add(line.translateXProperty()));
where weightSquare is a StackPane containing a rectangle and text.
Currently, the weightSquare is near the middle of the line but not perfectly in the middle. When the line moves around the weightSquare stays relatively near the middle of the line but sometimes goes off the line slightly.
I want something like this:
Example of what I want
Thank you.

Assuming no transformations have been applied to the line or the StackPane, you can calculate the position of the StackPane based on the line properties like this
stackPane.layoutX = (line.startX + line.endX - stackPane.width) / 2;
(Procede accordingly for y coordinates.)
transformX and transformY could simply be added, but general transforms would require you to
Listen to changes of the transforms
Use localToParent on the start/end coordinates of the line to get the location in the parent.
I recommend using Bindings.createDoubleBindings for complicate double bindings btw, since this makes the formula for calculating the values much easier to read.
Example
I use a Label, since this provides background/border functionality too.
#Override
public void start(Stage primaryStage) throws IOException {
Label label = new Label();
label.setStyle("-fx-background-color: white; -fx-border-color: black;");
label.setPadding(new Insets(2, 4, 2, 4));
Line line = new Line(300, 300, 300, 100);
label.layoutXProperty().bind(Bindings.createDoubleBinding(
() -> (line.getStartX() + line.getEndX() - label.getWidth()) / 2,
line.startXProperty(), line.endXProperty(), label.widthProperty()));
label.layoutYProperty().bind(Bindings.createDoubleBinding(
() -> (line.getStartY() + line.getEndY() - label.getHeight()) / 2,
line.startYProperty(), line.endYProperty(), label.heightProperty()));
DoubleProperty angle = new SimpleDoubleProperty();
line.endXProperty().bind(Bindings.createDoubleBinding(() -> 300 + 200 * Math.sin(angle.get()), angle));
line.endYProperty().bind(Bindings.createDoubleBinding(() -> 300 + 200 * Math.cos(angle.get()), angle));
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(angle, 0d, Interpolator.LINEAR)),
new KeyFrame(Duration.seconds(10), new KeyValue(angle, Math.PI * 2, Interpolator.LINEAR)));
timeline.setCycleCount(Animation.INDEFINITE);
label.textProperty().bind(timeline.currentTimeProperty().asString());
timeline.play();
Scene scene = new Scene(new Pane(line, label), 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}

Related

Region width not refreshing automatically after modifying Grid Pane to which the region width is bound - JavaFX

I'm making a simple Java GUI app using JavaFX that has a Border Pane as the root node.
In the top section of the Border Pane, there is a Grid Pane with three columns (top Grid Pane from now on).
In the first column of the top Grid Pane, there is a Home Button, in the second column, there is an empty Region that only serves as spacer between the first and third column of the top Grid Pane, and in the third column, there is another GridPane (right Grid Pane from now on).
The right Grid Pane contains one Button (Log In Button) on start. However, when a user successfully logs into the app, two other Buttons and a Label are added to the right Grid Pane as part of the Log In Button click event.
The spacer maxWidthProperty and minWidthProperty are bound to the top Grid Pane (tgp) widthProperty and the right Grid Pane(rgp) widthProperty like this:
spacer.minWidthProperty().bind(tgp.widthProperty().subtract(80).subtract(rgp.widthProperty()).subtract(3));
spacer.maxWidthProperty().bind(tgp.widthProperty().subtract(80).subtract(rgp.widthProperty()).subtract(3));
which makes the right Grid Pane move nicely with its buttons staying on the right side of the scene when a user resizes the main stage.
However, a problem occurs when the user logs in and additional buttons are added to the right Grid Pane. The spacer somehow misses this change and its width stays the same, which makes the additional Buttons appear outside of the current stage width. The only way to refresh the spacer width is to interact with the stage somehow, by clicking minimize/maximize/restore or by clicking any button on the scene.
Is there a way to automatically refresh Region width after the nodes to which its width is bound to are modified? Or, is there a better approach to making a top Grid Pane with one button on the left and modifiable number of buttons (nodes) on the right?
Edit: Here is a demonstration of the problem with several screenshots stacked on one another:
Minimal reproducible example:
BorderPane root = new BorderPane();
GridPane tgp = new GridPane();
tgp.minWidthProperty().bind(root.widthProperty());
tgp.maxWidthProperty().bind(root.widthProperty());
tgp.setStyle("-fx-background-color: WHITE; -fx-border-color: LIGHTGREY;");
tgp.setMinHeight(37);
tgp.setMaxHeight(37);
root.setTop(tgp);
Button homeButton = new Button("Home"));
homeButton.setMinHeight(35);
homeButton.setMaxHeight(35);
homeButton.setMinWidth(80);
homeButton.setMaxWidth(80);
tgp.add(homeButton, 0, 0);
GridPane rgp = new GridPane(); // Right Grid Pane - holds User related nodes
rgp.setHgap(5);
tgp.add(rgp, 2, 0);
Label unl = new Label("My Profile");
unl.setFont(new Font("Calibri", 15));
unl.setTextFill(Color.RED);
unl.setMinWidth(Region.USE_PREF_SIZE);
Button wlButton = new Button("Watchlist");
wlButton.setMinHeight(35);
wlButton.setMaxHeight(35);
wlButton.setMinWidth(80);
wlButton.setMaxWidth(80);
Button cartButton = new Button("Cart");
cartButton.setMinHeight(35);
cartButton.setMaxHeight(35);
cartButton.setMinWidth(60);
cartButton.setMaxWidth(60);
Button logInOutButton = new Button("Log In");
logInOutButton.setMinHeight(35);
logInOutButton.setMaxHeight(35);
logInOutButton.setMinWidth(60);
logInOutButton.setMaxWidth(60);
rgp.add(logInOutButton, 3, 0);
logInOutButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
if (logInOutButton.getText().equals("Log In")) {
LogInStage lis = new LogInStage();
lis.initStage();
if (lis.username != null) {
logInOutButton.setText("Log Out");
rgp.add(unl, 0, 0);
rgp.add(wlButton, 1, 0);
rgp.add(cartButton, 2, 0);
}
} else if (logInOutButton.getText().equals("Log Out")) {
logInOutButton.setText("Log In");
rgp.getChildren().remove(unl);
rgp.getChildren().remove(wlButton);
rgp.getChildren().remove(cartButton);
}
}
});
Region spacer = new Region();
spacer.minWidthProperty().bind(tgp.widthProperty().subtract(80).subtract(rgp.widthProperty()).subtract(3));
spacer.maxWidthProperty().bind(tgp.widthProperty().subtract(80).subtract(rgp.widthProperty()).subtract(3));
tgp.add(spacer, 1, 0)
It's always a bad idea to use bindings, if you can avoid it. Any changes to the size constraints can lead to a new layout pass being scheduled, but during the layout pass they are assumed to be constant. If you now introduce a binding the following sequence of events could happen:
A layout pass is requested for the GridPane, setting a flag to indicate layout is required
A layout pass happens. During the layout pass the children are resized. This triggers an update of the constraints of the children with the bindings.
The flag is cleared, but the changes to the contraints already happened. The layout won't reflect this. The GridPane gets another reason to do a layout.
I don't know, how your scene is set up in detail, but I recommend using column constraints: Set the grow priorities for the outer ones to SOMETIMES and the one for the center to ALWAYS. If you require some spacing around the children, you could use GridPane.setMargin (or the padding of the GridPane itself, if you require the a distance to the edges for all children).
#Override
public void start(Stage primaryStage) {
Button[] rightContent = new Button[3];
for (int i = 0; i < rightContent.length; i++) {
Button btn = new Button(Integer.toString(i));
GridPane.setColumnIndex(btn, i);
rightContent[i] = btn;
}
Button cycle = new Button("cycle");
GridPane rgp = new GridPane(); // I would usually use a HBox here
// don't grow larger than needed
rgp.setMaxWidth(Region.USE_PREF_SIZE);
// cycle though 0 to 3 buttons on the right
cycle.setOnAction(new EventHandler<ActionEvent>() {
int nextIndex = 0;
#Override
public void handle(ActionEvent event) {
if (nextIndex >= rightContent.length) {
rgp.getChildren().clear();
nextIndex = 0;
} else {
rgp.getChildren().add(rightContent[nextIndex]);
nextIndex++;
}
}
});
ColumnConstraints sideConstraints = new ColumnConstraints();
sideConstraints.setHgrow(Priority.SOMETIMES);
ColumnConstraints centerConstraints = new ColumnConstraints();
centerConstraints.setHgrow(Priority.ALWAYS);
//prefer to grow the center part of the GridPane
GridPane root = new GridPane();
root.getColumnConstraints().addAll(sideConstraints, centerConstraints, sideConstraints);
root.add(cycle, 0, 0);
root.add(rgp, 2, 0);
// add something to visualize the center part
// you could simply leave this part out
Region center = new Region();
center.setStyle("-fx-border-radius: 10;-fx-border-width: 1;-fx-border-color:black;");
root.add(center, 1, 0);
Scene scene = new Scene(root, 400, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
As mentioned in the comments, the center region is not actually needed.

JavaFX 3D PerspectiveCamera affects drag position of a node

I have been having issues trying to move nodes in JavaFX within a 3D Scene. The issue is that I want the mouse to stay at the position within the node I've clicked, i.e., center. With a PerspectiveCamera it will alter the position. I've tried a couple of different methods and haven't had any luck. 2D Rectangles, and 3D Boxes(without a camera) work perfectly, but once a PerspectiveCamera is added, regardless of true/false parameter, I have issues.
I am wondering if this a bug that should be reported, or if there is some way to get another the perspective affecting the moving of nodes
public class Move extends Application {
double x0,xDiff;
double y0,yDiff;
#Override
public void start(Stage primaryStage) {
Box b = new Box(100,100,1);
b.setLayoutX(0);
b.setLayoutY(0);
// b.setTranslateZ(20000);
Pane root = new Pane();
root.getChildren().add(b);
PhongMaterial p = new PhongMaterial();
p.setDiffuseColor(Color.RED);
b.setMaterial(p);
Scene scene = new Scene(root, 2000, 1250,true);
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setTranslateZ(-1000);
camera.setFarClip(2000);
scene.setCamera(camera);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
b.setOnMousePressed(event
->{
x0 = event.getSceneX();
y0 = event.getSceneY();
event.consume();
});
b.setOnMouseDragged(event
->{
xDiff = event.getSceneX() - x0;
yDiff = event.getSceneY() - y0;
b.setLayoutX(b.getLayoutX() + xDiff);
b.setLayoutY(b.getLayoutY() + yDiff);
x0 = event.getSceneX();
y0 = event.getSceneY();
});
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
I'm using Java 8 update 91 or 181 (it seems netbeans puts the default at 91, but I have 181 as well)
JavaFX Moving 3D objects with mouse on a virtual plane
I also found this post, and had tried the answer and it seemed like it also had some issues, but seemed much better(except it it was hard to test with the additional code of spinning the node when dragging, so if you drag off the sphere it rotates instead).
Thank you very much
EDIT: After trying to go back to how I originally dragged, I was able to get to a point where I could get the mouse cursor to stay in the middle, but I am trying to figure out how to get the exact position.
b.setLayoutX(b.getLayoutX() + (event.getX()));
b.setLayoutY(b.getLayoutY() + (event.getY()));
will give me the center of the node.
I originally used similar code to this for 2D, but was having issues with 3D, which I am assuming was due to the differences of 0,0 top-left corner, vs 0,0,0 in the center.
The code for 2D was something along the lines of
b.setLayoutX(b.getLayoutX() + (event.getX()-b.getMinX()));
b.setLayoutY(b.getLayoutY() + (event.getY()-b.getMinY()));
Essentially, from what I see when I set the original layout + the event position it just moves the center/top-left to the coordinates of the mouse event, so I would try to get the difference between the origin of the node and the mouseEvent, which is what event.getX() does, and try to figure out the difference to move, which is what the event.getSceneX() - x0 is for. I tried doing it without the Scene X/Y but it doesn't seem to work properly, but I'm not sure if using the SceneX/Y is what I should be doing.

JavaFX - Get Coordinates of Node Relative to its Parent

I am making a simple graphical interface for saving previously generated images. All images come to me square but I want to allow for some cropping functionality (more precisely cutting off equal parts from the bottom and top of the image). I want to do this by allowing the user to drag a shaded region over the image which will tell the user that this region will be cropped out. See the below image for details. To enable this drag functionality I have added small triangles that I want the user to drag which in turn will move the shaded regions about. However the coordinates for the triangles are all weird and seem nonsensical. Therefor I was wondering what the best way is to get the coordinates of the triangles in relation to the ImageView (or their first common parent node) in terms of ImageView-side-lengths. So if the triangle is in the center its coordinates are [0.5, 0.5] for instance.
The Image view will be moving around inside the scene and will also be changing size so it is vital that I can get the coordinates relative to not only the ImageView but also to the size of the ImageView.
Here is also the surrounding hierarchy of nodes if that helps. The Polygons are the triangles and the regions are the rectangles.
Thanks for all forms of help!
Node.getBoundsInParent returns the bounds of a node in it's parent coordinates. E.g. polygon.getBoundsInParent() would return the bounds in the VBox.
If you need to "go up" one additional step, you can use parent.localToParent to do this. vBox.localToParent(boundsInVbox) returns the bounds in the coordinate system of the AnchorPane.
To get values relative to the size of the image, you simply need to divide by it's size.
The following example only allows you to move the cover regions to in one direction and does not check, if the regions intersect, but it should be sufficient to demonstrate the approach.
The interesting part is the event handler of the button. It restricts the viewport of the second image to the part of the first image that isn't covered.
private static void setSideAnchors(Node node) {
AnchorPane.setLeftAnchor(node, 0d);
AnchorPane.setRightAnchor(node, 0d);
}
#Override
public void start(Stage primaryStage) {
// create covering area
Region topRegion = new Region();
topRegion.setStyle("-fx-background-color: white;");
Polygon topArrow = new Polygon(0, 0, 20, 0, 10, 20);
topArrow.setFill(Color.WHITE);
VBox top = new VBox(topRegion, topArrow);
top.setAlignment(Pos.TOP_CENTER);
topArrow.setOnMouseClicked(evt -> {
topRegion.setPrefHeight(topRegion.getPrefHeight() + 10);
});
// create bottom covering area
Region bottomRegion = new Region();
bottomRegion.setStyle("-fx-background-color: white;");
Polygon bottomArrow = new Polygon(0, 20, 20, 20, 10, 0);
bottomArrow.setFill(Color.WHITE);
VBox bottom = new VBox(bottomArrow, bottomRegion);
bottom.setAlignment(Pos.BOTTOM_CENTER);
bottomArrow.setOnMouseClicked(evt -> {
bottomRegion.setPrefHeight(bottomRegion.getPrefHeight() + 10);
});
Image image = new Image("https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/402px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg");
ImageView imageView = new ImageView(image);
setSideAnchors(top);
setSideAnchors(bottom);
setSideAnchors(imageView);
AnchorPane.setTopAnchor(top, 0d);
AnchorPane.setBottomAnchor(bottom, 0d);
AnchorPane.setTopAnchor(imageView, 0d);
AnchorPane.setBottomAnchor(imageView, 0d);
AnchorPane container = new AnchorPane(imageView, top, bottom);
ImageView imageViewRestricted = new ImageView(image);
Button button = new Button("restrict");
button.setOnAction(evt -> {
// determine bouns of Regions in AnchorPane
Bounds topBounds = top.localToParent(topRegion.getBoundsInParent());
Bounds bottomBounds = bottom.localToParent(bottomRegion.getBoundsInParent());
// set viewport accordingly
imageViewRestricted.setViewport(new Rectangle2D(
0,
topBounds.getMaxY(),
image.getWidth(),
bottomBounds.getMinY() - topBounds.getMaxY()));
});
HBox root = new HBox(container, button, imageViewRestricted);
root.setFillHeight(false);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}

Fill/Padding space between 2 Labels with dots in GridPane (JavaFX)

Starting point:
GridPane with 2 Columns
each Column has a Label
Like-to-have output:
space between the labels filled by dots
So far I have only come across String solutions in which the target length of the combined String is known. But this case doesn't do it for me because I need a solution which can also works when screen size changes and therefore the space between Labels do change dynamically. Could you guys please point me to the right direction?
You could put the 2 Labels in a HBox with a Region in between them, set hgrow for the labels and the Region to NEVER and ALWAYS respectively and use a linear gradient as background for the region that draws half of it's size black and the other half transparent.
Example
// 20 px wide horizontal gradient alternating between black and transparent with immediate color switch
private static final Paint FILL = new LinearGradient(
0, 0,
10, 0,
false,
CycleMethod.REPEAT,
new Stop(0, Color.BLACK),
new Stop(0.5, Color.BLACK),
new Stop(0.5, Color.TRANSPARENT)
);
// create background for regions
private static final Background BACKGROUND = new Background(new BackgroundFill(FILL, CornerRadii.EMPTY, Insets.EMPTY));
private static void addRow(Pane parent, String s1, String s2) {
// create labels
Label label1 = new Label(s2);
Label label2 = new Label('['+s2+']');
// create filler region with "stroke width" 2
Region filler = new Region();
filler.setPrefHeight(2);
filler.setBackground(BACKGROUND);
HBox hbox = new HBox(5, label1, filler, label2);
hbox.setAlignment(Pos.CENTER);
HBox.setHgrow(label1, Priority.NEVER);
HBox.setHgrow(label2, Priority.NEVER);
HBox.setHgrow(filler, Priority.ALWAYS);
hbox.setFillHeight(false);
parent.getChildren().add(hbox);
}
#Override
public void start(Stage primaryStage) {
VBox root = new VBox();
addRow(root, "JBoss", "DOWN");
addRow(root, "GlassFish", "UP");
addRow(root, "verylongprocessname", "UP");
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
You could also use a border (top only) on the Region instead of using a background and do not set prefHeight. This would allow you to actually use dots instead of strokes, but since your picture shows strokes, I posted the background approach instead...

JavaFX: Making BorderPane's Center's contents not strech

just a quick one for you since I'm generally inexperienced in designing apps. Any help would be greatly appreciated.
I've got the following application, and it's obvious that it uses a BorderPane for its layout. I've got a GridPane of 5 labels and 5 textFields in the Center region, and I wanted a subtle border for it.
As you can see, the GridPane utilizes the full space allocated to it in the Center region, and although the border gets drawn nicely, it goes all the way down to the bottom of the Center region (Red arrows).
What I want it to do is finish at the blue line.
I tried using grid.setPrefHeight(400);, for example, but it didn't work.
Are there any solutions other than the obvious one, to add a second container below the GridPane and squeeze the upper container enough?
EDIT: For reference, this is the code that creates the center area GridPane:
public GridPane addGridPaneCenter() {
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(0, 10, 0, 10));
sihuid = new Text("SIHU ID:");
sihuid.setFont(Font.font("Inconsolata", 16));
grid.add(sihuid, 0, 1); //spans from 0,1 to 1,1 (Column-Row)
sihuid_tf = new TextField();
sihuid_tf.setEditable(false);
grid.add(sihuid_tf, 1, 1);
mac = new Text("Plug MAC:");
mac.setFont(Font.font("Inconsolata", 16));
grid.add(mac, 0, 2);
mac_tf = new TextField();
mac_tf.setEditable(false);
grid.add(mac_tf, 1, 2);
loc = new Text("Location:");
loc.setFont(Font.font("Inconsolata", 16));
grid.add(loc, 0, 3);
loc_tf = new TextField();
loc_tf.setEditable(false);
grid.add(loc_tf, 1, 3);
appl = new Text("Appliance:");
appl.setFont(Font.font("Inconsolata", 16));
grid.add(appl, 0, 4);
appl_tf = new TextField();
appl_tf.setEditable(false);
grid.add(appl_tf, 1, 4);
type = new Text("Type:");
type.setFont(Font.font("Inconsolata", 16));
grid.add(type, 0, 5);
type_tf = new TextField();
type_tf.setEditable(false);
grid.add(type_tf, 1, 5);
grid.setPrefHeight(400);
grid.setStyle(
"-fx-border-color: #b8b8ba; -fx-border-width: 1;"
+ "-fx-border-radius: 4;"
/*+ "-fx-font: " + "Inconsolata" + ";" */
);
return grid;
}
After that, there's a simple
GridPane grid_center = addGridPaneCenter(); //CENTER Grid, contains info about plugs.
border_pane.setCenter(grid_center);
Scene scene = new Scene(border_pane, 900, 700);
scene.setFill(Color.GHOSTWHITE);
primaryStage.setTitle("PlugControl v0.1e");
primaryStage.setScene(scene);
primaryStage.show();
I'm afraid this is behaviour of BorderPane is by design (ref):
The top and bottom children will be resized to their preferred heights and extend the width of the borderpane. The left and right children will be resized to their preferred widths and extend the length between the top and bottom nodes. And the center node will be resized to fill the available space in the middle.
You should probably put the GridPane in an AnchorPane or VBox and put that (AnchorPane or VBox) in the center of the BorderPane.

Resources