Skip to content

Instantly share code, notes, and snippets.

@hastebrot
Forked from jewelsea/BoundsPlayground.java
Created February 28, 2014 22:26
Show Gist options
  • Save hastebrot/9281300 to your computer and use it in GitHub Desktop.
Save hastebrot/9281300 to your computer and use it in GitHub Desktop.
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.WindowEvent;
/** Demo for understanding JavaFX Layout Bounds */
public class BoundsPlayground extends Application {
final ObservableList<Shape> shapes = FXCollections.observableArrayList();
final ObservableList<ShapePair> intersections = FXCollections.observableArrayList();
ObjectProperty<BoundsType> selectedBoundsType = new SimpleObjectProperty<BoundsType>(BoundsType.LAYOUT_BOUNDS);
public static void main(String[] args) { launch(args); }
@Override public void start(Stage stage) {
stage.setTitle("Bounds Playground");
// define some objects to manipulate on the scene.
Circle greenCircle = new Circle(100, 100, 50, Color.FORESTGREEN); greenCircle.setId("Green Circle");
Circle redCircle = new Circle(300, 200, 50, Color.FIREBRICK); redCircle.setId("Red Circle");
Line line = new Line(25, 300, 375, 200); line.setId("Line");
line.setStrokeLineCap(StrokeLineCap.ROUND);
line.setStroke(Color.MIDNIGHTBLUE);
line.setStrokeWidth(5);
final Anchor anchor1 = new Anchor("Anchor 1", line.startXProperty(), line.startYProperty());
final Anchor anchor2 = new Anchor("Anchor 2", line.endXProperty(), line.endYProperty());
final Group group = new Group(greenCircle, redCircle, line, anchor1, anchor2);
// monitor intersections of shapes in the scene.
for (Node node : group.getChildrenUnmodifiable()) {
if (node instanceof Shape) {
shapes.add((Shape) node);
}
}
testIntersections();
// enable dragging for the scene objects.
Circle[] circles = { greenCircle, redCircle, anchor1, anchor2 };
for (Circle circle : circles) {
enableDrag(circle);
circle.centerXProperty().addListener(new ChangeListener<Number>() {
@Override public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
testIntersections();
}
});
circle.centerYProperty().addListener(new ChangeListener<Number>() {
@Override public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
testIntersections();
}
});
}
// define an overlay to show the layout bounds of the scene's shapes.
Group layoutBoundsOverlay = new Group();
layoutBoundsOverlay.setMouseTransparent(true);
for (Shape shape : shapes) {
if (!(shape instanceof Anchor)) {
layoutBoundsOverlay.getChildren().add(new BoundsDisplay(shape));
}
}
// layout the scene.
final StackPane background = new StackPane();
background.setStyle("-fx-background-color: cornsilk;");
final Scene scene = new Scene(new Group(background, group, layoutBoundsOverlay), 600, 500);
background.prefHeightProperty().bind(scene.heightProperty());
background.prefWidthProperty().bind(scene.widthProperty());
stage.setScene(scene);
stage.show();
createUtilityWindow(stage, layoutBoundsOverlay, new Shape[] { greenCircle, redCircle });
}
// update the list of intersections.
private void testIntersections() {
intersections.clear();
// for each shape test it's intersection with all other shapes.
for (Shape src : shapes) {
for (Shape dest : shapes) {
ShapePair pair = new ShapePair(src, dest);
if ((!(pair.a instanceof Anchor) && !(pair.b instanceof Anchor))
&& !intersections.contains(pair)
&& pair.intersects(selectedBoundsType.get())) {
intersections.add(pair);
}
}
}
}
// make a node movable by dragging it around with the mouse.
private void enableDrag(final Circle circle) {
final Delta dragDelta = new Delta();
circle.setOnMousePressed(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
// record a delta distance for the drag and drop operation.
dragDelta.x = circle.getCenterX() - mouseEvent.getX();
dragDelta.y = circle.getCenterY() - mouseEvent.getY();
circle.getScene().setCursor(Cursor.MOVE);
}
});
circle.setOnMouseReleased(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
circle.getScene().setCursor(Cursor.HAND);
}
});
circle.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
circle.setCenterX(mouseEvent.getX() + dragDelta.x);
circle.setCenterY(mouseEvent.getY() + dragDelta.y);
}
});
circle.setOnMouseEntered(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
circle.getScene().setCursor(Cursor.HAND);
}
}
});
circle.setOnMouseExited(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
circle.getScene().setCursor(Cursor.DEFAULT);
}
}
});
}
// a helper enumeration of the various types of bounds we can work with.
enum BoundsType { LAYOUT_BOUNDS, BOUNDS_IN_LOCAL, BOUNDS_IN_PARENT }
// a translucent overlay display rectangle to show the bounds of a Shape.
class BoundsDisplay extends Rectangle {
// the shape to which the bounds display has been type.
final Shape monitoredShape;
private ChangeListener<Bounds> boundsChangeListener;
BoundsDisplay(final Shape shape) {
setFill(Color.LIGHTGRAY.deriveColor(1, 1, 1, 0.35));
setStroke(Color.LIGHTGRAY.deriveColor(1, 1, 1, 0.5));
setStrokeType(StrokeType.INSIDE);
setStrokeWidth(3);
monitoredShape = shape;
monitorBounds(BoundsType.LAYOUT_BOUNDS);
}
// set the type of the shape's bounds to monitor for the bounds display.
void monitorBounds(final BoundsType boundsType) {
// remove the shape's previous boundsType.
if (boundsChangeListener != null) {
final ReadOnlyObjectProperty<Bounds> oldBounds;
switch (selectedBoundsType.get()) {
case LAYOUT_BOUNDS: oldBounds = monitoredShape.layoutBoundsProperty(); break;
case BOUNDS_IN_LOCAL: oldBounds = monitoredShape.boundsInLocalProperty(); break;
case BOUNDS_IN_PARENT: oldBounds = monitoredShape.boundsInParentProperty(); break;
default: oldBounds = null;
}
if (oldBounds != null) {
oldBounds.removeListener(boundsChangeListener);
}
}
// determine the shape's bounds for the given boundsType.
final ReadOnlyObjectProperty<Bounds> bounds;
switch (boundsType) {
case LAYOUT_BOUNDS: bounds = monitoredShape.layoutBoundsProperty(); break;
case BOUNDS_IN_LOCAL: bounds = monitoredShape.boundsInLocalProperty(); break;
case BOUNDS_IN_PARENT: bounds = monitoredShape.boundsInParentProperty(); break;
default: bounds = null;
}
// set the visual bounds display based upon the new bounds and keep it in sync.
updateBoundsDisplay(bounds.get());
// keep the visual bounds display based upon the new bounds and keep it in sync.
boundsChangeListener = new ChangeListener<Bounds>() {
@Override public void changed(ObservableValue<? extends Bounds> observableValue, Bounds oldBounds, Bounds newBounds) {
updateBoundsDisplay(newBounds);
}
};
bounds.addListener(boundsChangeListener);
}
// update this bounds display to match a new set of bounds.
private void updateBoundsDisplay(Bounds newBounds) {
setX(newBounds.getMinX());
setY(newBounds.getMinY());
setWidth(newBounds.getWidth());
setHeight(newBounds.getHeight());
}
}
// an anchor displayed around a point.
class Anchor extends Circle {
Anchor(String id, DoubleProperty x, DoubleProperty y) {
super(x.get(), y.get(), 10);
setId(id);
setFill(Color.GOLD.deriveColor(1, 1, 1, 0.5));
setStroke(Color.GOLD);
setStrokeWidth(2);
setStrokeType(StrokeType.OUTSIDE);
x.bind(centerXProperty());
y.bind(centerYProperty());
}
}
// records relative x and y co-ordinates.
class Delta { double x, y; }
// records a pair of (possibly) intersecting shapes.
class ShapePair {
private Shape a, b;
public ShapePair(Shape src, Shape dest) {
this.a = src; this.b = dest;
}
public boolean intersects(BoundsType boundsType) {
if (a == b) return false;
a.intersects(b.getBoundsInLocal());
switch (boundsType) {
case LAYOUT_BOUNDS: return a.getLayoutBounds().intersects(b.getLayoutBounds());
case BOUNDS_IN_LOCAL: return a.getBoundsInLocal().intersects(b.getBoundsInLocal());
case BOUNDS_IN_PARENT: return a.getBoundsInParent().intersects(b.getBoundsInParent());
default: return false;
}
}
@Override public String toString() {
return a.getId() + " : " + b.getId();
}
@Override
public boolean equals(Object other) {
ShapePair o = (ShapePair) other;
return o != null && ((a == o.a && b == o.b) || (a == o.b) && (b == o.a));
}
@Override
public int hashCode() {
int result = a != null ? a.hashCode() : 0;
result = 31 * result + (b != null ? b.hashCode() : 0);
return result;
}
}
// define a utility stage for reporting intersections.
private void createUtilityWindow(Stage stage, final Group boundsOverlay, final Shape[] transformableShapes) {
final Stage reportingStage = new Stage();
reportingStage.setTitle("Control Panel");
reportingStage.initStyle(StageStyle.UTILITY);
reportingStage.setX(stage.getX() + stage.getWidth());
reportingStage.setY(stage.getY());
// define content for the intersections utility panel.
final ListView<ShapePair> intersectionView = new ListView<ShapePair>(intersections);
final Label instructions = new Label(
"Click on any circle in the scene to the left to drag it around."
);
instructions.setMinSize(Control.USE_PREF_SIZE, Control.USE_PREF_SIZE);
instructions.setStyle("-fx-font-weight: bold; -fx-text-fill: darkgreen;");
final Label intersectionInstructions = new Label(
"Any intersecting bounds in the scene will be reported below."
);
instructions.setMinSize(Control.USE_PREF_SIZE, Control.USE_PREF_SIZE);
// add the ability to set a translate value for the circles.
final CheckBox translateNodes = new CheckBox("Translate circles");
translateNodes.selectedProperty().addListener(new ChangeListener<Boolean>() {
@Override public void changed(ObservableValue<? extends Boolean> observableValue, Boolean oldValue, Boolean doTranslate) {
if (doTranslate) {
for (Shape shape : transformableShapes) {
shape.setTranslateY(100);
testIntersections();
}
} else {
for (Shape shape : transformableShapes) {
shape.setTranslateY(0);
testIntersections();
}
}
}
});
translateNodes.selectedProperty().set(false);
// add the ability to add an effect to the circles.
final Label modifyInstructions = new Label(
"Modify visual display aspects."
);
modifyInstructions.setStyle("-fx-font-weight: bold;");
modifyInstructions.setMinSize(Control.USE_PREF_SIZE, Control.USE_PREF_SIZE);
final CheckBox effectNodes = new CheckBox("Add an effect to circles");
effectNodes.selectedProperty().addListener(new ChangeListener<Boolean>() {
@Override public void changed(ObservableValue<? extends Boolean> observableValue, Boolean oldValue, Boolean doTranslate) {
if (doTranslate) {
for (Shape shape : transformableShapes) {
shape.setEffect(new DropShadow());
testIntersections();
}
} else {
for (Shape shape : transformableShapes) {
shape.setEffect(null);
testIntersections();
}
}
}
});
effectNodes.selectedProperty().set(true);
// add the ability to add a stroke to the circles.
final CheckBox strokeNodes = new CheckBox("Add outside strokes to circles");
strokeNodes.selectedProperty().addListener(new ChangeListener<Boolean>() {
@Override public void changed(ObservableValue<? extends Boolean> observableValue, Boolean oldValue, Boolean doTranslate) {
if (doTranslate) {
for (Shape shape : transformableShapes) {
shape.setStroke(Color.LIGHTSEAGREEN);
shape.setStrokeWidth(10);
testIntersections();
}
} else {
for (Shape shape : transformableShapes) {
shape.setStrokeWidth(0);
testIntersections();
}
}
}
});
strokeNodes.selectedProperty().set(true);
// add the ability to show or hide the layout bounds overlay.
final Label showBoundsInstructions = new Label(
"The gray squares represent layout bounds."
);
showBoundsInstructions.setStyle("-fx-font-weight: bold;");
showBoundsInstructions.setMinSize(Control.USE_PREF_SIZE, Control.USE_PREF_SIZE);
final CheckBox showBounds = new CheckBox("Show Bounds");
boundsOverlay.visibleProperty().bind(showBounds.selectedProperty());
showBounds.selectedProperty().set(true);
// create a container for the display control checkboxes.
VBox displayChecks = new VBox(10);
displayChecks.getChildren().addAll(modifyInstructions, translateNodes, effectNodes, strokeNodes, showBoundsInstructions, showBounds);
// create a toggle group for the bounds type to use.
ToggleGroup boundsToggleGroup = new ToggleGroup();
final RadioButton useLayoutBounds = new RadioButton("Use Layout Bounds");
final RadioButton useBoundsInLocal = new RadioButton("Use Bounds in Local");
final RadioButton useBoundsInParent = new RadioButton("Use Bounds in Parent");
useLayoutBounds.setToggleGroup(boundsToggleGroup);
useBoundsInLocal.setToggleGroup(boundsToggleGroup);
useBoundsInParent.setToggleGroup(boundsToggleGroup);
VBox boundsToggles = new VBox(10);
boundsToggles.getChildren().addAll(useLayoutBounds, useBoundsInLocal, useBoundsInParent);
// change the layout bounds display depending on which bounds type has been selected.
useLayoutBounds.selectedProperty().addListener(new ChangeListener<Boolean>() {
@Override public void changed(ObservableValue<? extends Boolean> observableValue, Boolean aBoolean, Boolean isSelected) {
if (isSelected) {
for (Node overlay : boundsOverlay.getChildren()) {
((BoundsDisplay) overlay).monitorBounds(BoundsType.LAYOUT_BOUNDS);
}
selectedBoundsType.set(BoundsType.LAYOUT_BOUNDS);
testIntersections();
}
}
});
useBoundsInLocal.selectedProperty().addListener(new ChangeListener<Boolean>() {
@Override public void changed(ObservableValue<? extends Boolean> observableValue, Boolean aBoolean, Boolean isSelected) {
if (isSelected) {
for (Node overlay : boundsOverlay.getChildren()) {
((BoundsDisplay) overlay).monitorBounds(BoundsType.BOUNDS_IN_LOCAL);
}
selectedBoundsType.set(BoundsType.BOUNDS_IN_LOCAL);
testIntersections();
}
}
});
useBoundsInParent.selectedProperty().addListener(new ChangeListener<Boolean>() {
@Override public void changed(ObservableValue<? extends Boolean> observableValue, Boolean aBoolean, Boolean isSelected) {
if (isSelected) {
for (Node overlay : boundsOverlay.getChildren()) {
((BoundsDisplay) overlay).monitorBounds(BoundsType.BOUNDS_IN_PARENT);
}
selectedBoundsType.set(BoundsType.BOUNDS_IN_PARENT);
testIntersections();
}
}
});
useLayoutBounds.selectedProperty().set(true);
WebView boundsExplanation = new WebView();
boundsExplanation.getEngine().loadContent(
"<html><body bgcolor='darkseagreen' fgcolor='lightgrey' style='font-size:12px'><dl>" +
"<dt><b>Layout Bounds</b></dt><dd>The boundary of the shape.</dd><br/>" +
"<dt><b>Bounds in Local</b></dt><dd>The boundary of the shape and effect.</dd><br/>" +
"<dt><b>Bounds in Parent</b></dt><dd>The boundary of the shape, effect and transforms.<br/>The co-ordinates of what you see.</dd>" +
"</dl></body></html>"
);
boundsExplanation.setPrefWidth(100);
boundsExplanation.setMinHeight(130);
boundsExplanation.setMaxHeight(130);
boundsExplanation.setStyle("-fx-background-color: transparent");
// layout the utility pane.
VBox utilityLayout = new VBox(10);
utilityLayout.setStyle("-fx-padding:10; -fx-background-color: linear-gradient(to bottom, lightblue, derive(lightblue, 20%));");
utilityLayout.getChildren().addAll(instructions, intersectionInstructions, intersectionView, displayChecks, boundsToggles, boundsExplanation);
utilityLayout.setPrefHeight(530);
reportingStage.setScene(new Scene(utilityLayout));
reportingStage.show();
// ensure the utility window closes when the main app window closes.
stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
@Override public void handle(WindowEvent windowEvent) {
reportingStage.close();
}
});
}
}
@daveyostcom
Copy link

Thank you so much for this. By making a small change to your code that broke intersections, I was able to prove that intermediate levels of groups were causing the problem with my code. Here's the change:

    final Group greenGroup = new Group(greenCircle);
    final Group group = new Group(greenGroup, redCircle, line, anchor1, anchor2);

The fix is to use node.localToScreen(node.getBoundsInLocal(). Modifying your code to demonstrate this fact would require major changes to your code, so I left it at that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment