-
-
Save hastebrot/9281300 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
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.