JavaFX make Node partially mouseTransparent - javafx

If I have two Nodes stacked on top of each other and overlapping, how can I make the Node on top mouseTransparent (so that the bottom Node can react to MouseEvents) but also have the Node on top react to some MouseEvents like onMouseEntered?
For example, consider two (let's say rectangular) Panes inside a StackPane, with the bottom one smaller and completely underneath the top one:
<StackPane>
<Pane onMouseEntered="#printA" onMouseClicked="#printB" />
<Pane onMouseEntered="#printC" />
</StackPane>
If the user moves his mouse over the top Pane then C should be printed in the console. If he also moves his mouse over the bottom Pane then A should be printed too. If he clicks with mouse over the bottom Pane then B should be printed. Clicking over the top Pane but not over the bottom Pane should do nothing.
Why do I want to do something like this? I want to detect when the mouse moves near the center of a Pane so that I can change the Pane's center content (basically from display mode to edit mode) and let the user interact with the new content. I want the detection area to be larger than the center itself and thus it will overlap with some other things inside the Pane. So the Pane center can't be the detector, it has to be something transparent stacked on top. The detector also has to remain there so it can detect when the mouse moves away again.
There are lots of questions on Stackoverflow that appear similar, but almost all of them are solved by setMouseTransparent(true) or setPickOnBounds(true). setMouseTransparent doesn't work here since then the top Pane won't print C. setPickOnBounds makes the Pane mouseTransparent everywhere the Pane is alpha/visually transparent, but then the transparent parts won't print C and the opaque parts prevent the lower Pane from printing A or B. So even if the top Pane is completely transparent or completely opaque it doesn't solve my issue. Setting the visibility to false for the top Pane also won't work since then the top Pane cannot print C.

One way I can think, is to observe the mouse movements on the parent node, to see if the mouse pointer falls on the detection zone of the desired node. This way you don't need a dummy (transparent)node for detection.
So the idea is as follows:
<StackPane id="parent">
<Pane onMouseEntered="#printA" onMouseClicked="#printB" />
// Other nodes in the parent
</StackPane>
As usual, you will have handlers on the center node.
Add a mouseMoved handler on the parent node to detect for mouse entering in detection zone.
Please check the below working demo :
import javafx.application.Application;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class MouseEventsDemo extends Application {
double detectionSize = 30;
#Override
public void start(Stage primaryStage) throws Exception {
Pane center = getBlock("red");
center.setOnMouseEntered(e -> System.out.println("Entered on center pane..."));
center.setOnMouseClicked(e -> System.out.println("Clicked on center pane..."));
// Simulating that the 'center' is surrounded by other nodes
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
grid.addRow(0, getBlock("yellow"), getBlock("pink"), getBlock("orange"));
grid.addRow(1, getBlock("orange"), center, getBlock("yellow"));
grid.addRow(2, getBlock("yellow"), getBlock("pink"), getBlock("orange"));
// Adding rectangle only for zone visual purpose
Rectangle zone = new Rectangle(center.getPrefWidth() + 2 * detectionSize, center.getPrefHeight() + 2 * detectionSize);
zone.setStyle("-fx-stroke:blue;-fx-stroke-width:1px;-fx-fill:transparent;-fx-opacity:.4");
zone.setMouseTransparent(true);
StackPane parent = new StackPane(grid, zone);
VBox root = new VBox(parent);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(20));
root.setSpacing(20);
Scene scene = new Scene(root, 500, 500);
primaryStage.setScene(scene);
primaryStage.show();
addDetectionHandler(parent, center);
}
private Pane getBlock(String color) {
Pane block = new Pane();
block.setStyle("-fx-background-color:" + color);
block.setMaxSize(100, 100);
block.setPrefSize(100, 100);
return block;
}
private void addDetectionHandler(StackPane parent, Pane node) {
final String key = "onDetectionZone";
parent.setOnMouseMoved(e -> {
boolean mouseEntered = (boolean) node.getProperties().computeIfAbsent(key, p -> false);
if (!mouseEntered && isOnDetectionZone(e, node)) {
node.getProperties().put(key, true);
// Perform your mouse enter operations on detection zone,.. like changing to edit mode.. or what ever
System.out.println("Entered on center pane detection zone...");
node.setStyle("-fx-background-color:green");
} else if (mouseEntered && !isOnDetectionZone(e, node)) {
node.getProperties().put(key, false);
// Perform your mouse exit operations from detection zone,.. like change back to default state from edit mode
System.out.println("Exiting from center pane detection zone...");
node.setStyle("-fx-background-color:red");
}
});
}
private boolean isOnDetectionZone(MouseEvent e, Pane node) {
Bounds b = node.localToScene(node.getBoundsInLocal());
double d = detectionSize;
Bounds detectionBounds = new BoundingBox(b.getMinX() - d, b.getMinY() - d, b.getWidth() + 2 * d, b.getHeight() + 2 * d);
return detectionBounds.contains(e.getSceneX(), e.getSceneY());
}
}
Note: You can go with a more better approach for checking if the mouse pointer falls in detection zone of the desired node :)
UPDATE : Approach#2
Looks like what you are seeking is a node which resembles a picture frame that has a hole through it :). If that is the case, the approach i can think of is to build a shape (like a frame) and place it over the desired node.
And then you can simply add mouse handlers to the detection zone and the center nodes.
Please check the below working demo:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Polyline;
import javafx.stage.Stage;
public class MouseEventsDemo2 extends Application {
double detectionSize = 30;
#Override
public void start(Stage primaryStage) throws Exception {
Pane center = getBlock("red");
center.getChildren().add(new Label("Hello"));
center.setOnMouseEntered(e -> {
System.out.println("Entered on center pane...");
center.setStyle("-fx-background-color:green");
});
center.setOnMouseClicked(e -> System.out.println("Clicked on center pane..."));
// Simulating that the 'center' is surrounded by other nodes
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
grid.addRow(0, getBlock("yellow"), getBlock("pink"), getBlock("orange"));
grid.addRow(1, getBlock("orange"), center, getBlock("yellow"));
grid.addRow(2, getBlock("yellow"), getBlock("pink"), getBlock("orange"));
StackPane parent = new StackPane(grid);
VBox root = new VBox(parent);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(20));
root.setSpacing(20);
Scene scene = new Scene(root, 500, 500);
primaryStage.setScene(scene);
primaryStage.show();
// Building the frame using Polyline node
Polyline zone = new Polyline();
zone.setStyle("-fx-fill:grey;-fx-opacity:.5;-fx-stroke-width:0px;");
zone.setOnMouseEntered(e -> {
System.out.println("Entered on detection zone...");
center.setStyle("-fx-background-color:green");
});
zone.setOnMouseExited(e -> {
System.out.println("Exited on detection zone...");
center.setStyle("-fx-background-color:red");
});
zone.setOnMouseClicked(e -> System.out.println("Clicked on detection zone..."));
parent.getChildren().add(zone);
parent.layoutBoundsProperty().addListener(p -> updatePolylineZone(center, zone));
center.layoutBoundsProperty().addListener(p -> updatePolylineZone(center, zone));
updatePolylineZone(center, zone);
}
/**
* Update the poly line shape to build a frame around the center node if the parent or center bounds changed.
*/
private void updatePolylineZone(Pane center, Polyline zone) {
zone.getPoints().clear();
Bounds b = center.localToParent(center.getBoundsInLocal());
double s = detectionSize;
ObservableList<Double> pts = FXCollections.observableArrayList();
// A-------------------------B
// | |
// | a---------------b |
// | | | |
// | | | |
// | | | |
// | d---------------c |
// | |
// D-------------------------C
// Outer square
pts.addAll(b.getMinX() - s, b.getMinY() - s); // A
pts.addAll(b.getMaxX() + s, b.getMinY() - s); // B
pts.addAll(b.getMaxX() + s, b.getMaxY() + s); // C
pts.addAll(b.getMinX() - s, b.getMaxY() + s); // D
// Inner Square
pts.addAll(b.getMinX() + 1, b.getMaxY() - 1); // d
pts.addAll(b.getMaxX() - 1, b.getMaxY() - 1); // c
pts.addAll(b.getMaxX() - 1, b.getMinY() + 1); // b
pts.addAll(b.getMinX() + 1, b.getMinY() + 1); // a
// Closing the loop
pts.addAll(b.getMinX() - s, b.getMinY() - s); // A
pts.addAll(b.getMinX() - s, b.getMaxY() + s); // D
pts.addAll(b.getMinX() + 1, b.getMaxY() - 1); // d
pts.addAll(b.getMinX() + 1, b.getMinY() + 1); // a
pts.addAll(b.getMinX() - s, b.getMinY() - s); // A
zone.getPoints().addAll(pts);
}
private Pane getBlock(String color) {
Pane block = new Pane();
block.setStyle("-fx-background-color:" + color);
block.setMaxSize(100, 100);
block.setPrefSize(100, 100);
return block;
}
}

Here is a potential strategy which might be used.
Create a node which is the shape of the area you want to detect interaction with.
In the example below it is a circle and in this description I term it the "detection node".
Make the detection node mouse transparent so that it doesn't consume or interact with any of the standard mouse events.
Add an event listener to the scene (using an event handler or event filter as appropriate).
When the scene event listener recognizes an event which should also be routed to the detection node, copy the event and fire it specifically at the detection node.
The detection node can then respond to the duplicated event.
The original event is also processed unaware of the detection node, so it can interact with other nodes in the scene as though the detection node was never there.
The effect will be that two events occur and can be separately handled by the target node and by the detection node.
import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.util.concurrent.atomic.AtomicBoolean;
public class LayersFX extends Application {
private final ListView<String> logViewer = new ListView<>();
// set to true if you with to see move events in the log.
private final boolean LOG_MOVES = false;
#Override
public void start(Stage stage) {
Rectangle square = new Rectangle(200, 200, Color.LIGHTSKYBLUE);
Circle circle = new Circle(80, Color.LEMONCHIFFON);
StackPane stack = new StackPane(square, circle);
addEventHandlers(square);
addEventHandlers(circle);
VBox layout = new VBox(10, stack, logViewer);
layout.setPadding(new Insets(10));
logViewer.setPrefSize(200, 200);
Scene scene = new Scene(layout);
routeMouseEventsToNode(scene, circle);
stage.setScene(scene);
stage.show();
}
/**
* Intercepts mouse events from the scene and created duplicate events which
* are routed to the node when appropriate.
*/
private void routeMouseEventsToNode(Scene scene, Node node) {
// make the node transparent to standard mouse events.
// it will only receive mouse events we specifically create and send to it.
node.setMouseTransparent(true);
// consume all events for the target node so that we don't
// accidentally let a duplicated event bubble back up to the scene
// and inadvertently cause an infinite loop.
node.addEventHandler(EventType.ROOT, Event::consume);
// Atomic isn't used here for concurrency, it is just
// a trick to make the boolean value effectively final
// so that it can be used in the lambda.
AtomicBoolean inNode = new AtomicBoolean(false);
scene.setOnMouseMoved(
event -> {
boolean wasInNode = inNode.get();
boolean nowInNode = node.contains(
node.sceneToLocal(
event.getSceneX(),
event.getSceneY()
)
);
inNode.set(nowInNode);
if (nowInNode) {
node.fireEvent(
event.copyFor(
node,
node,
MouseEvent.MOUSE_MOVED
)
);
}
if (!wasInNode && nowInNode) {
node.fireEvent(
event.copyFor(
node,
node,
MouseEvent.MOUSE_ENTERED_TARGET
)
);
}
if (wasInNode && !nowInNode) {
node.fireEvent(
event.copyFor(
node,
node,
MouseEvent.MOUSE_EXITED_TARGET
)
);
}
}
);
}
private void addEventHandlers(Node node) {
String nodeName = node.getClass().getSimpleName();
node.setOnMouseEntered(
event -> log("Entered " + nodeName)
);
node.setOnMouseExited(
event -> log("Exited " + nodeName)
);
node.setOnMouseClicked(
event -> log("Clicked " + nodeName)
);
node.setOnMouseMoved(event -> {
if (LOG_MOVES) {
log(
"Moved in " + nodeName +
" (" + Math.floor(event.getX()) + "," + Math.floor(event.getY()) + ")"
);
}
});
}
private void log(String msg) {
logViewer.getItems().add(msg);
logViewer.scrollTo(logViewer.getItems().size() - 1);
}
public static void main(String[] args) {
launch(args);
}
}
There is probably some better way of doing this using a custom event dispatch chain, but the logic above is what I came up with. The logic appears to do what you asked in your question, though it may not have the full functionality that you need for your actual application.

Related

Delegate mouse events to all children in a JavaFX StackPane

I'm trying to come up with a solution to allow multiple Pane nodes handle mouse events independently when assembled into a StackPane
StackPane
Pane 1
Pane 2
Pane 3
I'd like to be able to handle mouse events in each child, and the first child calling consume() stops the event going to the next child.
I'm also aware of setPickOnBounds(false), but this does not solve all cases as some of the overlays will be pixel based with Canvas, i.e. not involving the scene graph.
I've tried various experiments with Node.fireEvent(). However these always lead to recursion ending in stack overflow. This is because the event is propagated from the root scene and triggers the same handler again.
What I'm looking for is some method to trigger the event handlers on the child panes individually without the event travelling through its normal path.
My best workaround so far is to capture the event with a filter and manually invoke the handler. I'd need to repeat this for MouseMoved etc
parent.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
for (Node each : parent.getChildren()) {
if (!event.isConsumed()) {
each.getOnMouseClicked().handle(event);
}
}
event.consume();
});
However this only triggers listeners added with setOnMouseClicked, not addEventHandler, and only on that node, not child nodes.
Another sort of solution is just to accept JavaFX doesn't work like this, and restructure the panes like this, this will allow normal event propagation to take place.
Pane 1
Pane 2
Pane 3
Example
import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class EventsInStackPane extends Application {
public static void main(String[] args) {
launch(args);
}
private static class DebugPane extends Pane {
public DebugPane(Color color, String name) {
setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY)));
setOnMouseClicked(event -> {
System.out.println("setOnMouseClicked " + name + " " + event);
});
addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventHandler " + name + " " + event);
});
addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventFilter " + name + " " + event);
});
}
}
#Override
public void start(Stage primaryStage) throws Exception {
DebugPane red = new DebugPane(Color.RED, "red");
DebugPane green = new DebugPane(Color.GREEN, "green");
DebugPane blue = new DebugPane(Color.BLUE, "blue");
setBounds(red, 0, 0, 400, 400);
setBounds(green, 25, 25, 350, 350);
setBounds(blue, 50, 50, 300, 300);
StackPane parent = new StackPane(red, green, blue);
eventHandling(parent);
primaryStage.setScene(new Scene(parent));
primaryStage.show();
}
private void eventHandling(StackPane parent) {
parent.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (!event.isConsumed()) {
for (Node each : parent.getChildren()) {
Event copy = event.copyFor(event.getSource(), each);
parent.fireEvent(copy);
if (copy.isConsumed()) {
break;
}
}
}
event.consume();
});
}
private void setBounds(DebugPane panel, int x, int y, int width, int height) {
panel.setLayoutX(x);
panel.setLayoutY(y);
panel.setPrefWidth(width);
panel.setPrefHeight(height);
}
}
Using the hint from #jewelsea I was able to use a custom chain. I've done this from a "catcher" Pane which is added to the front of the StackPane. This then builds a chain using all the children, in reverse order, excluding itself.
private void eventHandling(StackPane parent) {
Pane catcher = new Pane() {
#Override
public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
EventDispatchChain chain = super.buildEventDispatchChain(tail);
for (int i = parent.getChildren().size() - 1; i >= 0; i--) {
Node child = parent.getChildren().get(i);
if (child != this) {
chain = chain.prepend(child.getEventDispatcher());
}
}
return chain;
}
};
parent.getChildren().add(catcher);
}

How do I set fxyz3d shape materials so they work?

How do I specify a material for an fxyz3d shape? When I add to a JavaFX Group of 3D objects the fxyz3d node
Cone cone = new Cone(coneFacets, coneRadius, coneHeight);
cone.setMaterial(Materials.redMaterial());
it turns every shape in that group solid black, not just the cone, regardless of what any of the specified materials are. If I comment out the above two lines and the one that adds the cone to the group, all the displays of the other shapes return to their specified appearances.
I am using javafx-sdk-17.0.1, fxyz3d-0.5.4.jar, JavaSE-16, Windows 10. Is Javadoc available for fxyz3d anywhere? Or is it necessary to download source and build it locally?
The redMaterial is defined as
final PhongMaterial material = new PhongMaterial();
material.setDiffuseColor(Color.INDIANRED);
material.setSpecularColor(Color.RED);
The following code will reproduce this. As is, both cone and cylinder display black. Comment out the four lines that create and add the Cone, and the cylinder will display red as specified by the material. (Don't otherwise use this as a starting-point example, as there are also issues with automatic scaling as the user adjusts the stage window size yet to be addressed.)
package org.javafxtests;
import org.fxyz3d.shapes.Cone;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SubScene;
import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Cylinder;
import javafx.stage.Stage;
public class JxyzConeMaterials extends Application {
// https://www.tutorialspoint.com/javafx/index.htm
// https://www.javatpoint.com/javafx-tutorial
// https://openjfx.io/javadoc/11/
/**
* The application initialization method.
*/
#Override
public void init() throws Exception {
super.init();
}
/**
* Main entry point for all JavaFX applications. The start method is called
* after the init method has returned and the JavaFX framework and hosting
* system are ready to start the application.
*/
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("Test JXzy Cone materials");
double sceneWidth = 750.0d;
double sceneHeight = 500.0d;
// The scene structure is constructed from the inside-out (bottom-up).
// A tool bar goes along the top
final FlowPane toolbar = new FlowPane();
toolbar.setPrefWidth(Double.MAX_VALUE);
toolbar.getChildren().addAll(new Label("Files"));
// A TreeView goes down the left side
final TreeView<String> treeView = new TreeView<String>();
treeView.setPrefHeight(Double.MAX_VALUE);
TreeItem<String> treeRoot = new TreeItem<String>("<empty>");
treeView.setRoot(treeRoot);
// A SubScene for viewing 3D objects goes to the right of the TreeView
final SubScene canvasScene = new SubScene(new AnchorPane(), 0, 0);
final AnchorPane canvasRootPane = (AnchorPane) canvasScene.getRoot();
canvasRootPane.setPrefWidth(Double.MAX_VALUE);
canvasRootPane.setPrefHeight(Double.MAX_VALUE);
canvasScene.setWidth(0.75 * sceneWidth); // No setPref methods
canvasScene.setHeight(sceneHeight);
// Create a controllable camera for the 3D SubScene
final PerspectiveCamera canvasCamera = new PerspectiveCamera(true);
final Group cameraTruck = new Group();
final Group cameraGimbal = new Group();
canvasCamera.setFarClip(6000);
canvasCamera.setNearClip(0.01);
cameraGimbal.getChildren().add(canvasCamera);
cameraTruck.getChildren().add(cameraGimbal);
cameraTruck.setTranslateZ(-500.0d);
canvasScene.setCamera(canvasCamera);
canvasRootPane.getChildren().add(cameraTruck);
// Create an HBox at the bottom of the scene,
// TreeView on the left and 3D canvas on the right.
HBox treeAnd3dViews = new HBox(treeView, canvasScene);
treeAnd3dViews.setFillHeight(true);
HBox.setHgrow(canvasScene, Priority.ALWAYS);
treeAnd3dViews.setMaxHeight(Double.MAX_VALUE);
treeAnd3dViews.setMaxWidth(Double.MAX_VALUE);
// Create a VBox to stack the tool bar over the above.
VBox toolbarOverViews = new VBox(toolbar, treeAnd3dViews);
toolbarOverViews.setMaxWidth(Double.MAX_VALUE);
toolbarOverViews.setMaxHeight(Double.MAX_VALUE);
VBox.setVgrow(treeAnd3dViews, Priority.ALWAYS);
AnchorPane.setTopAnchor(toolbarOverViews, 0.0);
AnchorPane.setBottomAnchor(toolbarOverViews, 0.0);
AnchorPane.setLeftAnchor(toolbarOverViews, 0.0);
AnchorPane.setRightAnchor(toolbarOverViews, 0.0);
final Scene scene = new Scene(new AnchorPane(), sceneWidth, sceneHeight);
final AnchorPane sceneRootPane = (AnchorPane) scene.getRoot();
sceneRootPane.getChildren().add(toolbarOverViews);
// Draw an arrow consisting of a cylinder with a cone on top.
double lineRadius = 1.0d;
double lineLength = 25.0d;
int coneFacets = 6;
double coneRadius = 3.0d;
double coneHeight = 6.0d;
final PhongMaterial material = new PhongMaterial();
material.setDiffuseColor(Color.INDIANRED);
material.setSpecularColor(Color.RED);
Cylinder cylinder = new Cylinder(lineRadius, lineLength);
cylinder.setMaterial(material);
Cone cone = new Cone(coneFacets, coneRadius, coneHeight);
cone.setMaterial(material);
// The cone points in the negative Y direction
cone.setTranslateY(-(lineLength / 2.0d) - coneHeight );
canvasRootPane.getChildren().add(cylinder);
canvasRootPane.getChildren().add(cone);
// Show
primaryStage.setScene(scene);
primaryStage.show();
}
#Override
public void stop() throws Exception {
super.stop();
}
/**
* Main method to launch the application with parameters if needed.
* This may or may not be called, depending on how this application
* is launched.
*
* #param args specifies arguments to {#linkplain Application#launch)}.
*/
public static void main(String[] args) {
launch(args);
}
}

JavaFx Overlapping mouse events

I’m trying to build a board game interface where the user can switch between multiple eras, each one with its own board. To do so, I’m creating 4 different board, each within its own pane, and I’m toggling the nodes Visibility and disabling the nodes that aren’t being used. The problem I have is the mouse event handlers I’m using to see where the user is clicking only work on the top layer, the last one that was rendered. The event Handlers underneath don’t work even if they are enabled.
Here’s what I wrote:
static EventHandler<MouseEvent> eventMouseClickRoad = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent e) {
final Shape innerShape = (Shape) (e.getTarget());
System.out.println("click");
Color color = (Color) innerShape.getFill();
if(color.getOpacity() != 1)
{
innerShape.setFill(Color.RED);
//and do the data treatment
}
}
};
public void boardControler(Vector2DList sideList,PointList hexEdge,Pane groupPane,float scaleX, float scaleY, float buttonSize)
{
//set road button
for(Vector2D v : sideList.getVectorList()){
Path mypath = new Path(new MoveTo(v.getP1().getX(),v.getP1().getY()),new LineTo(v.getP2().getX(),v.getP2().getY()));
groupPane.getChildren().add(mypath);
}
for(Vector2D v : sideList.getVectorList()){
float midX=(v.getP1().getX()+v.getP2().getX())/2;
float diffY=v.getP1().getY()-v.getP2().getY();
float diffX=v.getP1().getX()-v.getP2().getX();
Rectangle rectangle = new Rectangle(midX-buttonSize/2,midY-Math.abs(diffY)+buttonSize+(Math.abs(diffY)-scaleY/4),buttonSize,(scaleY/2)-(buttonSize*2));
rectangle.setRotate(Math.toDegrees(Math.atan(diffY/diffX))+90);
rectangle.setFill(Color.TRANSPARENT);
rectangle.addEventFilter(MouseEvent.MOUSE_ENTERED, Event.eventMouseEntered);
rectangle.addEventFilter(MouseEvent.MOUSE_EXITED, Event.eventMouseExit);
rectangle.addEventFilter(MouseEvent.MOUSE_CLICKED, Event.eventMouseClickRoad);
groupPane.getChildren().add(rectangle);
}
}
And this is what i use to toggle the board that's being used:
to disable
for(Node n : groupPane2.getChildren())
{
n.setDisable(true);
n.setManaged(false);
n.setVisible(false);
}
to enable
for(Node n : groupPane2.getChildren())
{
n.setDisable(false);
n.setManaged(true);
n.setVisible(true);
}
Perhaps using a StackPane would be the solution here. Your question doesn't include much code to show all of your context, but the MCVE below may help to demonstrate the idea.
Basically, we create a StackPane as our root display container for all of your boards. Your "boards" can be anything, a Pane, another StackPane, or a VBox like in my example. This should allow you to continue using whatever layout system you currently are.
One thing to note, it appears that each board will need to have a background set, or the lower boards will show through and may accept mouse events.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class StackPaneSample extends Application {
public static void main(String[] args) {
launch(args);
}
private static StackPane stackPane = new StackPane();
#Override
public void start(Stage primaryStage) {
// Simple interface
VBox root = new VBox(5);
root.setPadding(new Insets(10));
root.setAlignment(Pos.CENTER);
// Create our StackPane
stackPane.setStyle("-fx-border-color: black");
VBox.setVgrow(stackPane, Priority.ALWAYS);
// Let's create 3 "boards" for our StackPane. A background color seems necessary to hide layers below the top one
VBox board1 = new VBox() {{
setStyle("-fx-background-color: whitesmoke");
setAlignment(Pos.CENTER);
setUserData("Board #1");
getChildren().add(new Label((String) getUserData()));
}};
VBox board2 = new VBox() {{
setStyle("-fx-background-color: whitesmoke");
setAlignment(Pos.CENTER);
setUserData("Board #2");
getChildren().add(new Label((String) getUserData()));
}};
VBox board3 = new VBox() {{
setStyle("-fx-background-color: whitesmoke");
setAlignment(Pos.CENTER);
setUserData("Board #3");
getChildren().add(new Label((String) getUserData()));
}};
stackPane.getChildren().add(board1);
stackPane.getChildren().add(board2);
stackPane.getChildren().add(board3);
// Create three buttons that will switch between the boards
Button btnBoard1 = new Button("Board #1");
Button btnBoard2 = new Button("Board #2");
Button btnBoard3 = new Button("Board #3");
HBox hbButtons = new HBox(20) {{
setAlignment(Pos.CENTER);
setPadding(new Insets(5));
getChildren().addAll(btnBoard1, btnBoard2, btnBoard3);
}};
// Finish out layout
root.getChildren().addAll(
stackPane,
new Separator(Orientation.HORIZONTAL),
hbButtons
);
// ** Now let's add our functionality **
// Print out which board has been clicked upon
// We need to first cast our List to VBox
for (Node vbox : stackPane.getChildren()) {
vbox.setOnMouseClicked(event -> System.out.println("Clicked on " + vbox.getUserData()));
}
// Set the buttons to set the top board
btnBoard1.setOnAction(event -> selectBoard(board1));
btnBoard2.setOnAction(event -> selectBoard(board2));
btnBoard3.setOnAction(event -> selectBoard(board3));
// Show the Stage
primaryStage.setWidth(400);
primaryStage.setHeight(300);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
// Method to remove the board and readd it, placing it on top of all others.
private static void selectBoard(VBox board) {
stackPane.getChildren().remove(board);
stackPane.getChildren().add(board);
}
}
The Result:
I am, admittedly, not familiar with the Cartesian coordinates you mention in your comment, so perhaps this won't work for you. Adding more code/context to your question might help us narrow down the issue better.

SplitPane dividers synchronization JavaFX

I want to synchronize dividers in SplitPane, when divider(0) moves, I also want to make the same move by divider(1). I guess I have to bind the positionProperty of divider(0) with something.
How can I achieve this?
You need to add listeners to the positions of each divider, and update the "linked" divider when it changes. It's important to make sure you don't end up in an infinite recursive loop; the simplest way to do this is to set a flag indicating your updating, and not propagate the update if it's set.
Here's a proof-of-concept example that binds two dividers so the portion between them is always 1/3 of the split pane:
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.SplitPane;
import javafx.scene.control.SplitPane.Divider;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
public class SplitPaneDemo extends Application {
// helper class that binds two divider positions so the portion between them
// is always 1/3 of the split pane
private static class DividerPositionBinder {
private static final double ONE_THIRD = 1.0/3.0;
private boolean updating ;
DividerPositionBinder(List<Divider> dividers) {
dividers.get(0).positionProperty().addListener((obs, oldPos, newPos) -> {
// don't propagate update if already in an update:
if (updating) return ;
// special handling for right edge of split pane:
if (newPos.doubleValue() > 1.0 - ONE_THIRD) {
dividers.get(0).setPosition(1.0 - ONE_THIRD);
dividers.get(1).setPosition(1.0);
return ;
}
// make right divider the new value + 1/3:
updating = true ;
dividers.get(1).setPosition(newPos.doubleValue() + ONE_THIRD);
updating = false ;
});
dividers.get(1).positionProperty().addListener((obs, oldPos, newPos) -> {
// don't propagate update if already in an update:
if (updating) return ;
// special handling for left edge of split pane:
if (newPos.doubleValue() < ONE_THIRD) {
dividers.get(1).setPosition(ONE_THIRD);
dividers.get(0).setPosition(0.0);
return ;
}
// make left divider the new value - 1/3:
updating = true ;
dividers.get(0).setPosition(newPos.doubleValue() - ONE_THIRD);
updating = false ;
});
}
}
#Override
public void start(Stage primaryStage) {
Region left = new Pane();
left.setStyle("-fx-background-color: coral; ");
Region middle = new Pane();
middle.setStyle("-fx-background-color: aquamarine ;");
Region right = new Pane();
right.setStyle("-fx-background-color: cornflowerblue ;");
SplitPane splitPane = new SplitPane(left, middle, right);
new DividerPositionBinder(splitPane.getDividers());
Scene scene = new Scene(splitPane, 800, 800);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Getting a MouseEvent to target ImageViews inside a TilePane

I have many ImageViews inside a TilePane, that is inside a StackPane and then a ScrollPane. There is no border, padding, or margin between the children of the TilePane so there is no chance that I'm not clicking on an ImageView. When I click on an image, I want the target of the MouseEvent to be the ImageViews, but instead it is the TilePane.
How can I get the event chain to end on an ImageView instead of ending early on the TilePane?
Otherwise, is there a way I can get the ImageView using other information? Perhaps using the coordinates of the event?
The usual way I do this is just to register the mouse listener with the node in which I am interested; in your case this means register a mouse listener with each ImageView. It's easy then to have each mouse listener have a reference to the particular image view with which it's registered, or to other data (e.g. a filename) if you need.
One thing that might be happening: if your images have transparent pixels, then mouse clicks on that part of the image will by default "drop through" to the node below. You can change this behavior by calling imageView.setPickOnBounds(true); on the image views.
Some test code. If you run this you'll see some numbered images with different colored backgrounds. About 1 in 4 have transparent backgrounds (they appear white). If you click on these (but not on the actual text of the number), you'll see the mouse handlers registered with the scroll pane and stack pane have the tile pane as the target, and the handler registered with the ImageView is not even invoked. For those without the transparent background, the target is always the ImageView. If you select the check box, so pickOnBounds is true for all the ImageViews, both transparent and opaque images behave as you want.
import java.util.Random;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.TilePane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class ImageViewClickTest extends Application {
private static final Random RNG = new Random();
#Override
public void start(Stage primaryStage) {
TilePane tilePane = new TilePane();
CheckBox pickOnBounds = new CheckBox("Pick on bounds");
pickOnBounds.setPadding(new Insets(16));
for (int i=1; i<=200; i++) {
ImageView imageView = createImageView(i);
imageView.pickOnBoundsProperty().bind(pickOnBounds.selectedProperty());
// mouse handler directly on image view:
// can access image-view specific data...
String message = "Clicked on Image "+i ;
imageView.setOnMouseClicked(e ->
System.out.println("From handler on ImageView: "+message));
tilePane.getChildren().add(imageView);
}
StackPane stack = new StackPane(tilePane);
stack.setOnMouseClicked(e -> {
// source will be the stack pane
// target will be the top-most node
// (i.e. the ImageView, in most cases)
System.out.println("From handler on stack pane: Source: "+e.getSource());
System.out.println("From handler on stack pane: Target: "+e.getTarget());
});
ScrollPane scroller = new ScrollPane(stack);
scroller.setFitToWidth(true);
scroller.setOnMouseClicked(e -> {
// source will be the scroll pane
// target will be the top-most node
// (i.e. the ImageView, in most cases)
System.out.println("From handler on scroller: Source: "+e.getSource());
System.out.println("From handler on scroller: Target: "+e.getTarget());
});
BorderPane root = new BorderPane(scroller, pickOnBounds, null, null, null);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private ImageView createImageView(int index) {
Label label = new Label(Integer.toString(index));
label.setAlignment(Pos.CENTER);
label.setMinSize(48, 48);
label.setStyle(randomStyle());
Image image = new Scene(label, Color.TRANSPARENT).snapshot(null);
ImageView imageView = new ImageView(image);
return imageView ;
}
private String randomStyle() {
StringBuilder style = new StringBuilder();
style.append("-fx-background-color: -fx-background;");
style.append("-fx-background: ");
if (RNG.nextDouble() < 0.25) {
style.append( "transparent;");
style.append(" -fx-text-fill: black;") ;
} else {
String bg = String.format("#%02x%02x%02x;",
RNG.nextInt(256), RNG.nextInt(256), RNG.nextInt(256));
style.append(bg);
}
return style.toString();
}
public static void main(String[] args) {
launch(args);
}
}

Resources