resizing a substraction shape - java

I would like to resize the below created shape. but cannot get it.
The project is to create a transparent rectangle to show only a part of the desktop, and hide the rest. The transparent zone is the result of a substraction, and I need to make it resizable by the user.
I tryed several ways, such as adapting from this : https://gist.github.com/jewelsea/1441960
But couldn't get it.
Here is my code :
#Override
public void start(Stage stage) {
Group group = new Group();
Rectangle rect = new Rectangle(0, 0, 350, 300);
Rectangle clip = new Rectangle(20, 20, 200, 200);
clip.setArcHeight(15);
clip.setArcWidth(15);
Shape shape = Shape.subtract(rect, clip);
shape.setFill(Color.GRAY);
group.getChildren().add(shape);
Scene scene = new Scene(group);
scene.setFill(Color.TRANSPARENT);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
stage.show();
}
Any link or help would be appreciated.

If you create a Shape by Shape.subtract(...), you don't have any mechanism to change the properties of it afterwards (in the sense of changing the bounds of the shapes that were used to create it). You would have to remove the shape from its parent, recompute the rect and clip, recompute the shape, and add the new shape back into the scene.
It might be better to use a Path here so that you can manipulate the coordinates without creating a new shape every time. Traverse one way (say clockwise) around the outside (filled portion), and then the other way (anti-clockwise) around the inner (transparent portion). The resulting shape will be the same as a subtraction of the inner portion from the outer portion. The initial setup will potentially require considerably more code, but you can then manipulate the coordinates as you need to.
I'm not sure exactly what functionality you were looking for, but the following allows you to drag the inner portion around by clicking and dragging on it, and allows you to move the whole window by clicking and dragging on the outer portion. It should be enough for you to figure out what you need. I didn't include the nice rounded corners you had in your example, but you can fairly easily implement those using ArcTo path elements.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class TransparentRectangle extends Application {
#Override
public void start(Stage stage) {
Pane root = new Pane();
PathElement start = new MoveTo(0, 0);
PathElement outerTopRight = createBoundLineTo(root.widthProperty(), 0);
PathElement outerBottomRight = createBoundLineTo(root.widthProperty(), root.heightProperty());
PathElement outerBottomLeft = createBoundLineTo(0, root.heightProperty());
PathElement outerTopLeft = new LineTo(0, 0);
DoubleProperty innerLeft = new SimpleDoubleProperty(20);
DoubleProperty innerTop = new SimpleDoubleProperty(20);
DoubleBinding innerRight = innerLeft.add(180);
DoubleBinding innerBottom = innerTop.add(180);
PathElement innerTopLeft = createBoundLineTo(innerLeft, innerTop);
PathElement innerTopRight = createBoundLineTo(innerRight, innerTop);
PathElement innerBottomRight = createBoundLineTo(innerRight, innerBottom);
PathElement innerBottomLeft = createBoundLineTo(innerLeft, innerBottom);
Path path = new Path(
start, outerTopRight,
outerBottomRight, outerBottomLeft,
outerTopLeft,
innerTopLeft, innerBottomLeft,
innerBottomRight, innerTopRight,
innerTopLeft, new ClosePath()
);
path.setFill(Color.GRAY);
path.setStroke(Color.TRANSPARENT);
root.getChildren().add(path);
class Wrapper<T> { T value ; }
Wrapper<Point2D> mouseLocation = new Wrapper<>();
// Drag on gray portion of path - move entire window:
path.setOnDragDetected(event -> {
mouseLocation.value = new Point2D(event.getScreenX(), event.getScreenY());
});
path.setOnMouseDragged(event -> {
if (mouseLocation.value != null) {
stage.setX(stage.getX() + event.getScreenX() - mouseLocation.value.getX());
stage.setY(stage.getY() + event.getScreenY() - mouseLocation.value.getY());
mouseLocation.value = new Point2D(event.getScreenX(), event.getScreenY());
}
});
path.setOnMouseReleased(event -> mouseLocation.value = null);
// Drag on scene (i.e not on path, i.e. on transparent part) - move transparent part
root.setOnDragDetected(event -> {
mouseLocation.value = new Point2D(event.getScreenX(), event.getScreenY());
});
root.setOnMouseDragged(event -> {
if (mouseLocation.value != null) {
innerLeft.set(innerLeft.get() + event.getScreenX() - mouseLocation.value.getX());
innerTop.set(innerTop.get() + event.getScreenY() - mouseLocation.value.getY());
mouseLocation.value = new Point2D(event.getScreenX(), event.getScreenY());
}
});
root.setOnMouseReleased(event -> mouseLocation.value = null);
// No close button on a transparent window, so exit on double click:
root.setOnMouseClicked(event -> {
if (event.getClickCount() == 2) Platform.exit();
event.consume();
});
Scene scene = new Scene(root, 800, 600);
scene.setFill(Color.TRANSPARENT);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
stage.show();
}
private PathElement createBoundLineTo(ObservableDoubleValue x, ObservableDoubleValue y) {
LineTo lineTo = new LineTo();
lineTo.xProperty().bind(x);
lineTo.yProperty().bind(y);
return lineTo ;
}
private PathElement createBoundLineTo(double fixedX, ObservableDoubleValue y) {
LineTo lineTo = new LineTo();
lineTo.setX(fixedX);
lineTo.yProperty().bind(y);
return lineTo ;
}
private PathElement createBoundLineTo(ObservableDoubleValue x, double fixedY) {
LineTo lineTo = new LineTo();
lineTo.setY(fixedY);
lineTo.xProperty().bind(x);
return lineTo ;
}
public static void main(String[] args) {
launch(args);
}
}

Related

Most simple rotate camera via mouse not working

Okay, this is driving me crazy. The documentation is pretty weak, the example application from Oracle is very weird, with a huge convoluted helper class, and even the questions about it on here have no answers!
I've largely followed and simplified this tutorial, but instead of rotating the object, I'm trying to rotate the camera, so when you drag the mouse, it should orbit the camera.
However, though I have confirmed via console logs and debugging that the event handlers are being called, and everything seems to have the right values, my rotates just never happen! What am I missing?
Furthermore, I can't manage to move the camera at all, even the (commented out) translateX and the like don't work either, so I am quite stumped, but can't get the axis to look like anywhere but the upper left corner!
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Camera;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.transform.Rotate;
public class RotateCameraExample extends Group {
private double anchorX, anchorY;
private double anchorAngleX = 0;
private double anchorAngleY = 0;
private DoubleProperty angleX = new SimpleDoubleProperty(0);
private DoubleProperty angleY = new SimpleDoubleProperty(0);
Camera camera;
Group axes;
public RotateCameraExample() {
axes = buildAxes();
getChildren().add(axes);
camera = new PerspectiveCamera(true);
camera.setFarClip(6000);
camera.setNearClip(0.01);
//camera.translateYProperty().set(300); // this doesn't do anything! why?
getChildren().add(camera);
initMouseControl();
}
private void initMouseControl() {
Rotate xRotate = new Rotate(0, Rotate.X_AXIS);
Rotate yRotate = new Rotate(0, Rotate.Y_AXIS);
camera.getTransforms().addAll(xRotate, yRotate);
xRotate.angleProperty().bind(angleX);
yRotate.angleProperty().bind(angleY);
setOnMousePressed(event -> {
anchorX = event.getSceneX();
anchorY = event.getSceneY();
anchorAngleX = angleX.get();
anchorAngleY = angleY.get();
});
setOnMouseDragged(event -> {
angleX.set(anchorAngleX - (anchorY - event.getSceneY()));
angleY.set(anchorAngleY + anchorX - event.getSceneX());
});
}
private Group buildAxes() {
final Box xAxis = new Box(1200, 10, 10);
final Box yAxis = new Box(10, 1200, 10);
final Box zAxis = new Box(10, 10, 1200);
xAxis.setMaterial(new PhongMaterial(Color.RED));
yAxis.setMaterial(new PhongMaterial(Color.GREEN));
zAxis.setMaterial(new PhongMaterial(Color.BLUE));
Group axisGroup = new Group();
axisGroup.getChildren().addAll(xAxis, yAxis, zAxis);
return axisGroup;
}
}
Here can see that the axis is visible in the upper left, and I want it to remain at (0, 0, 0) while moving the camera around it.
Here is the Application launch code, which is clearly not the issue:
public class TestApp extends Application {
#Override
public void start(Stage stage) throws IOException {
RotateCameraExample g = new RotateCameraExample();
Scene scene = new Scene(g, 800, 800, Color.BLACK);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Instead of adding the camera to the children of the Group,
getChildren().add(camera);
You should set the scene's camera.
scene.setCamera(g.camera);
You will immediately see the axes at the center of the screen. Similarly, mouse handler(s) should be applied to the scene. You can then update the group's transforms in your scene's handler(s).
As an example, the variation below alters the camera's rotation in response to mouse scroll events. Note how the vertical mouse scroll affects rotation about the X axis, while horizontal mouse scroll affects rotation about the Y axis. The same gestures also translate the group as a whole. An assortment of keyboard commands enable one to rotate the camera around the Z axis, dolly along the Z axis, and reset the scene.
You can translate and rotate about points on a circle, as illustrated here; in contrast, this related example animates the rotation of an object about a pivot.
import javafx.application.Application;
import javafx.scene.Camera;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
/**
* #see https://stackoverflow.com/a/69260181/230513
*/
public class RotateCameraExample extends Application {
private static class RotateCamera extends Group {
private final Camera camera;
private final Rotate xRotate = new Rotate(0, Rotate.X_AXIS);
private final Rotate yRotate = new Rotate(0, Rotate.Y_AXIS);
private final Rotate zRotate = new Rotate(0, Rotate.Z_AXIS);
public RotateCamera() {
buildAxes();
camera = new PerspectiveCamera(true);
camera.setFarClip(6000);
camera.setNearClip(0.01);
camera.setTranslateZ(-2000);
camera.getTransforms().addAll(xRotate, yRotate, zRotate);
}
private void buildAxes() {
final Box xAxis = new Box(1200, 10, 10);
final Box yAxis = new Box(10, 1200, 10);
final Box zAxis = new Box(10, 10, 1200);
xAxis.setMaterial(new PhongMaterial(Color.RED));
yAxis.setMaterial(new PhongMaterial(Color.GREEN));
zAxis.setMaterial(new PhongMaterial(Color.BLUE));
Group axisGroup = new Group();
axisGroup.getChildren().addAll(xAxis, yAxis, zAxis);
this.getChildren().add(axisGroup);
}
}
#Override
public void start(Stage stage) {
RotateCamera g = new RotateCamera();
Scene scene = new Scene(g, 800, 800, Color.BLACK);
scene.setCamera(g.camera);
stage.setScene(scene);
stage.show();
scene.setOnScroll((final ScrollEvent e) -> {
g.xRotate.setAngle(g.xRotate.getAngle() + e.getDeltaY() / 10);
g.yRotate.setAngle(g.yRotate.getAngle() - e.getDeltaX() / 10);
g.setTranslateX(g.getTranslateX() + e.getDeltaX());
g.setTranslateY(g.getTranslateY() + e.getDeltaY());
});
scene.setOnKeyPressed((KeyEvent e) -> {
KeyCode code = e.getCode();
switch (code) {
case LEFT:
g.zRotate.setAngle(g.zRotate.getAngle() + 10);
break;
case RIGHT:
g.zRotate.setAngle(g.zRotate.getAngle() - 10);
break;
case UP:
g.setTranslateZ(g.getTranslateZ() - 100);
break;
case DOWN:
g.setTranslateZ(g.getTranslateZ() + 100);
break;
case HOME:
g.xRotate.setAngle(0);
g.yRotate.setAngle(0);
g.zRotate.setAngle(0);
g.setTranslateX(0);
g.setTranslateY(0);
g.setTranslateZ(0);
break;
default:
break;
}
});
}
public static void main(String[] args) {
launch();
}
}

Why does the addition of a SubScene change the localToScreen function output in JavFX?

I am trying to develop a tool with JavaFX that has multiple viewports and I think the best way to do that is through the use of SubScenes. One requirement I have is the ability to know where on a given plane in the scene corresponds to the pixel clicked by the mouse. I thought that I could use the Node.localToScreen() and Node.screenToLocal() functions to do this, however when adding a SubScene I get different values, despite nothing else changing.
Below is an example, where running the code with withSubScene = false, the console shows:
Point2D [x = 996.0, y = 514.8333400189878]
Point2D [x = 117.98005476276654, y = 514.8333400189878]
And running with withSubScene = true, the console shows:
Point2D [x = 997.0, y = 529.3333400189878]
Point2D [x = 64.91937872905163, y = 529.3333400189878]
Why would these values be different, when the camera is in the same location and looking at the same object?
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class ScreenToLocalTest extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
// Change this variable to swap to adding a SubScene
boolean withSubScene = false;
// Set the stage to be the same size each time
primaryStage.setWidth(1000);
primaryStage.setHeight(500);
Group root = new Group();
Scene rootScene = new Scene(root);
// Create out camera
Camera camera = new PerspectiveCamera(true);
camera.setTranslateZ(-1000);
camera.setNearClip(1);
camera.setFarClip(10000);
Group groupToAddRectangle;
if(withSubScene) {
Group sceneRoot = new Group();
SubScene subScene = new SubScene(sceneRoot, primaryStage.getWidth(), primaryStage.getHeight());
root.getChildren().add(subScene);
subScene.setCamera(camera);
groupToAddRectangle = sceneRoot;
} else {
rootScene.setCamera(camera);
groupToAddRectangle = root;
}
Rectangle rectangle = new Rectangle(1000, 300, Color.ALICEBLUE);
groupToAddRectangle.getChildren().add(rectangle);
rectangle.setTranslateZ(1);
root.setOnMouseMoved(event-> {
System.out.println(rectangle.screenToLocal(event.getScreenX(), event.getScreenY()));
});
primaryStage.setScene(rootScene);
primaryStage.show();
System.out.println(rectangle.localToScreen(0, 0));
rectangle.translateXProperty().set(-1000);
System.out.println(rectangle.localToScreen(0, 0));
}
public static void main(String[] args) {
launch(args);
}
}

Transparent JavaFX stage capture scrolling events if there is another window behind

Mouse events and scroll events behave in different ways
Mouse Events:
The event is captured by mainStage
The event is captured by mainStage
The event is not captured
Scroll Events:
The event is captured by mainStage
The event is captured by secondStage
The event is not captured
Is there any way that transparent secondStage does not capture scroll events?
My code:
Pane mainPane = new Pane(new Label("Main Stage"));
mainPane.setPrefSize(300, 300);
mainStage.setScene(new Scene(mainPane));
Stage secondStage = new Stage();
Pane secondPane = new Pane(new Label("Second Stage"));
secondPane.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
secondPane.setBorder(new Border(
new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(2))));
secondPane.setPrefSize(300, 300);
secondStage.setScene(new Scene(secondPane, Color.TRANSPARENT));
secondStage.initStyle(StageStyle.TRANSPARENT);
mainStage.getScene().setOnScroll(event -> System.out.println("Scroll in main stage"));
secondStage.getScene().setOnScroll(event -> System.out.println("Scroll in second stage"));
mainStage.getScene().setOnMouseClicked(event -> System.out.println("Click in main stage"));
secondStage.getScene().setOnMouseClicked(event -> System.out.println("Click in second stage"));
mainStage.show();
secondStage.show();
Java version: 1.8.0_201 (64 bits), Windows 10
edit:
The example is a simplification with only two windows. Fire the event programmatically implies discovering which stage is immediately lower and that is another problem in itself.
It might be a great coincidence, that we also came with the same solution of transparent window because of not having the feature of managing z-index of stages. And We encountered the exact same issue as yours. ie, scroll events not propagating to underlying Stages. We used the below approach, not sure whether this can help you:
Firstly, We constructed a Singleton class that keeps a reference of Node that is currently hovered on.
Then, when we create any normal stage, we include the below handlers to the scene of that new stage. The key thing here is that, the mouse events are still able to pass through the transparent stage to the underlying window, keep track of node which sits under the mouse.
scene.addEventFilter(MouseEvent.MOUSE_EXITED_TARGET, e -> {
hoverNode.set(null);
});
scene.addEventFilter(MouseEvent.MOUSE_MOVED, e -> {
hoverNode.set(e.getTarget());
});
In the scene of the transparent window, we included the below handlers to delegate the scroll events to the underlying node.
scene.addEventFilter(ScrollEvent.SCROLL, e -> {
if (hoverNode.get() != null) {
Event.fireEvent(hoverNode.get(), e);
}
});
scene.addEventHandler(ScrollEvent.SCROLL, e -> {
if (hoverNode.get() != null) {
Event.fireEvent(hoverNode.get(), e);
}
});
I am pretty sure this is not the most desired way. But this addressed our issue. :)
Below is the quick demo code of what I mean.
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.Event;
import javafx.event.EventTarget;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.stream.IntStream;
public class ScrollThroughTransparentStage_Demo extends Application {
#Override
public void start(Stage stage) throws Exception {
stage.setTitle("Main Window");
VBox root = new VBox(buildScrollPane());
root.setStyle("-fx-background-color:#888888;");
root.setSpacing(10);
root.setPadding(new Insets(10));
Button normalStageBtn = new Button("Normal Stage");
normalStageBtn.setOnAction(e -> {
Stage normalStage = new Stage();
normalStage.initOwner(stage);
Scene normalScene = new Scene(buildScrollPane(), 300, 300);
addHandlers(normalScene);
normalStage.setScene(normalScene);
normalStage.show();
});
CheckBox allowScrollThrough = new CheckBox("Allow scroll through transparency");
allowScrollThrough.setSelected(true);
HBox buttons = new HBox(normalStageBtn);
buttons.setSpacing(20);
root.getChildren().addAll(allowScrollThrough,buttons);
Scene scene = new Scene(root, 600, 600);
addHandlers(scene);
stage.setScene(scene);
stage.show();
/* Transparent Stage */
Stage transparentStage = new Stage();
transparentStage.initOwner(stage);
transparentStage.initStyle(StageStyle.TRANSPARENT);
Pane mainRoot = new Pane();
Pane transparentRoot = new Pane(mainRoot);
transparentRoot.setStyle("-fx-background-color:transparent;");
Scene transparentScene = new Scene(transparentRoot, Color.TRANSPARENT);
transparentStage.setScene(transparentScene);
transparentScene.addEventFilter(ScrollEvent.SCROLL, e -> {
if (allowScrollThrough.isSelected() && HoverNodeSingleton.getInstance().getHoverNode() != null) {
Event.fireEvent(HoverNodeSingleton.getInstance().getHoverNode(), e);
}
});
transparentScene.addEventHandler(ScrollEvent.SCROLL, e -> {
if (allowScrollThrough.isSelected() && HoverNodeSingleton.getInstance().getHoverNode() != null) {
Event.fireEvent(HoverNodeSingleton.getInstance().getHoverNode(), e);
}
});
determineStageSize(transparentStage, mainRoot);
transparentStage.show();
Button transparentStageBtn = new Button("Transparent Stage");
transparentStageBtn.setOnAction(e -> {
MiniStage miniStage = new MiniStage(mainRoot);
ScrollPane scrollPane = buildScrollPane();
scrollPane.setPrefSize(300, 300);
miniStage.setContent(scrollPane);
miniStage.show();
});
buttons.getChildren().add(transparentStageBtn);
}
private static void determineStageSize(Stage stage, Node root) {
DoubleProperty width = new SimpleDoubleProperty();
DoubleProperty height = new SimpleDoubleProperty();
DoubleProperty shift = new SimpleDoubleProperty();
Screen.getScreens().forEach(screen -> {
Rectangle2D bounds = screen.getVisualBounds();
width.set(width.get() + bounds.getWidth());
if (bounds.getHeight() > height.get()) {
height.set(bounds.getHeight());
}
if (bounds.getMinX() < shift.get()) {
shift.set(bounds.getMinX());
}
});
stage.setX(shift.get());
stage.setY(0);
stage.setWidth(width.get());
stage.setHeight(height.get());
root.setTranslateX(-1 * shift.get());
}
private void addHandlers(Scene scene) {
scene.addEventFilter(MouseEvent.MOUSE_EXITED_TARGET, e -> {
HoverNodeSingleton.getInstance().setHoverNode(null);
});
scene.addEventFilter(MouseEvent.MOUSE_MOVED, e -> {
HoverNodeSingleton.getInstance().setHoverNode(e.getTarget());
});
}
private ScrollPane buildScrollPane() {
VBox vb = new VBox();
vb.setSpacing(10);
vb.setPadding(new Insets(15));
IntStream.rangeClosed(1, 100).forEach(i -> vb.getChildren().add(new Label(i + "")));
ScrollPane scrollPane = new ScrollPane(vb);
return scrollPane;
}
class MiniStage extends Group {
private Pane parent;
double sceneX, sceneY, layoutX, layoutY;
protected BorderPane windowPane;
private BorderPane windowTitleBar;
private Label labelTitle;
private Button buttonClose;
public MiniStage(Pane parent) {
this.parent = parent;
buildRootNode();
getChildren().add(windowPane);
addEventHandler(MouseEvent.MOUSE_PRESSED, e -> toFront());
}
#Override
public void toFront() {
parent.getChildren().remove(this);
parent.getChildren().add(this);
}
public void setContent(Node content) {
// Computing the bounds of the content before rendering
Group grp = new Group(content);
new Scene(grp);
grp.applyCss();
grp.requestLayout();
double width = grp.getLayoutBounds().getWidth();
double height = grp.getLayoutBounds().getHeight() + 30; // 30 title bar height
grp.getChildren().clear();
windowPane.setCenter(content);
// Centering the stage
Rectangle2D screenBounds = Screen.getPrimary().getBounds();
setX(screenBounds.getWidth() / 2 - width / 2);
setY(screenBounds.getHeight() / 2 - height / 2);
}
public Node getContent() {
return windowPane.getCenter();
}
public void setX(double x) {
setLayoutX(x);
}
public void setY(double y) {
setLayoutY(y);
}
public void show() {
if (!parent.getChildren().contains(this)) {
parent.getChildren().add(this);
}
}
public void hide() {
parent.getChildren().remove(this);
}
private void buildRootNode() {
windowPane = new BorderPane();
windowPane.setStyle("-fx-border-width:2px;-fx-border-color:#444444;");
labelTitle = new Label("Mini Stage");
labelTitle.setStyle("-fx-font-weight:bold;");
labelTitle.setMaxHeight(Double.MAX_VALUE);
buttonClose = new Button("X");
buttonClose.setFocusTraversable(false);
buttonClose.setStyle("-fx-background-color:red;-fx-background-radius:0;-fx-background-insets:0;");
buttonClose.setOnMouseClicked(evt -> hide());
windowTitleBar = new BorderPane();
windowTitleBar.setStyle("-fx-border-width: 0 0 2px 0;-fx-border-color:#444444;-fx-background-color:#BBBBBB");
windowTitleBar.setLeft(labelTitle);
windowTitleBar.setRight(buttonClose);
windowTitleBar.setPadding(new Insets(0, 0, 0, 10));
windowTitleBar.getStyleClass().add("nonfocus-title-bar");
windowPane.setTop(windowTitleBar);
assignTitleBarEvents();
}
private void assignTitleBarEvents() {
windowTitleBar.setOnMousePressed(this::recordWindowLocation);
windowTitleBar.setOnMouseDragged(this::moveWindow);
windowTitleBar.setOnMouseReleased(this::resetMousePointer);
}
private final void recordWindowLocation(final MouseEvent event) {
sceneX = event.getSceneX();
sceneY = event.getSceneY();
layoutX = getLayoutX();
layoutY = getLayoutY();
getScene().setCursor(Cursor.MOVE);
}
private final void resetMousePointer(final MouseEvent event) {
// Updating the new layout positions
setLayoutX(layoutX + getTranslateX());
setLayoutY(layoutY + getTranslateY());
// Resetting the translate positions
setTranslateX(0);
setTranslateY(0);
getScene().setCursor(Cursor.DEFAULT);
}
private final void moveWindow(final MouseEvent event) {
double offsetX = event.getSceneX() - sceneX;
double offsetY = event.getSceneY() - sceneY;
setTranslateX(offsetX);
setTranslateY(offsetY);
event.consume();
}
}
}
/**
* Singleton class.
*/
class HoverNodeSingleton {
private static HoverNodeSingleton INSTANCE = new HoverNodeSingleton();
private EventTarget hoverNode;
private HoverNodeSingleton() {
}
public static HoverNodeSingleton getInstance() {
return INSTANCE;
}
public EventTarget getHoverNode() {
return hoverNode;
}
public void setHoverNode(EventTarget hoverNode) {
this.hoverNode = hoverNode;
}
}
I don't know that's right or not, but you can bind properties:
secondStage.getScene().onScrollProperty().bind(mainStage.getScene().onScrollProperty());
You can create a custom event dispatcher that will ignore events you don't want:
public class CustomEventDispatcher extends BasicEventDispatcher {
#Override
public Event dispatchEvent(Event event, EventDispatchChain tail) {
if(event instanceof ScrollEvent) {
return null;
} else {
return super.dispatchEvent(event, tail);
}
}
}
Then set that on your stage:
secondStage.setEventDispatcher(new CustomEventDispatcher());
I don't know how this works in the context of stages but for simple shapes it makes a difference whether you set the fill color to Color.TRANSPARENT or just null. Using any Color catches events, whereas null does not.
You can do so by ignoring the event on the second stage using event dispatcher using this answer by #Slaw you can understand everything about EventDispatcher
https://stackoverflow.com/a/51015783/5303683
Then you can fire your own event using this answer by DVarga
https://stackoverflow.com/a/40042513/5303683
Sorry I don't have time to try and make a full example of it

How to drag a `Shape` that is below anther `shape` in JavaFX scene graph?

I'm writing an educational application using JavaFX in which the user can draw and manipulate Bezier curves Line, QuadCurve, and CubicCurve. These curves should have the capability to be dragged with mouse. I've got two options available:
1- Using classes Line, QuadCurve, and CubicCurve, and then filling them with transparent color, and stroke them with another color, say black. The problem that arises for this option is that the user wants to drag a curve, but sees that another curve is dragged. The reason for this is that the curve that user is going to drag, resides below another one in the scene graph. For example, in the following figure the smaller curve is not capable of dragging, since it is below the larger one in the scene graph.
2- Using class javafx.scene.shape.Path, in which case the problem is that manipulating such a path is a little bit more complicated, since it's composed of some PathElements, and simply manipulating the elements does not change the Path, unless we remove an element from its elements property, and add a new one. Therefore I prefer approach 1.
How can I Overcome the problem arising in the first approach?
Thank you in advance for your help. A simplified version of my program code is as follows.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.QuadCurve;
import javafx.stage.Stage;
public class SampleForStackOverflow extends Application
{
double lastMouseX;
double lastMouseY;
double lastTranslateX;
double lastTranslateY;
#Override
public void start(Stage window)
{
final double STROKE_WIDTH = 5;
QuadCurve quad = new QuadCurve(100, 200, 150, 50, 200, 200);
quad.setFill(Color.TRANSPARENT);
quad.setStroke(Color.BLACK);
quad.setStrokeWidth(STROKE_WIDTH);
quad.setOnMousePressed(e -> {
lastMouseX = e.getSceneX();
lastMouseY = e.getSceneY();
lastTranslateX = quad.getTranslateX();
lastTranslateY = quad.getTranslateY();
});
quad.setOnMouseDragged(e -> followMouse(quad, e));
CubicCurve cubic = new CubicCurve(0, 300, 100, 0, 300, 0, 300, 300);
cubic.setFill(Color.TRANSPARENT);
cubic.setStroke(Color.BLACK);
cubic.setStrokeWidth(STROKE_WIDTH);
cubic.setOnMousePressed(e -> {
lastMouseX = e.getSceneX();
lastMouseY = e.getSceneY();
lastTranslateX = cubic.getTranslateX();
lastTranslateY = cubic.getTranslateY();
});
cubic.setOnMouseDragged(e -> followMouse(cubic, e));
Group root = new Group(quad, cubic);
Scene scene = new Scene(root, 500, 500);
window.setScene(scene);
window.show();
}
private void followMouse(Node node, MouseEvent e)
{
double deltaX = e.getSceneX() - lastMouseX;
double deltaY = e.getSceneY() - lastMouseY;
node.setTranslateX(deltaX + lastTranslateX);
node.setTranslateY(deltaY + lastTranslateY);
}
public static void main(String[] args) {
launch(args);
}
}
Instead of using a transparent color in your first scenario, you should explicitly set the fill color of your curves to null and set pickOnBounds to false. A transparent color will still catch the mouse events but null will not and when pickOnBounds if false the mouse events will be caught only if you are exactly over the colored parts of your shape.
Creating the QuadCurve and the CubicCurve using Paths seems to work fine for me. Here is a complete example :
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.QuadCurveTo;
import javafx.stage.Stage;
public class SampleForStackOverflow extends Application {
private final double STROKE_WIDTH = 5;
private double lastMouseX;
private double lastMouseY;
private double lastTranslateX;
private double lastTranslateY;
#Override
public void start(Stage window) {
Path quad = initQuadCurve(100, 200, 200, 200, 150, 50);
Path cubic = initCubicCurve(0, 300, 300, 300, 100, 0, 300, 0);
Pane root = new Pane(cubic, quad);
Scene scene = new Scene(root, 500, 500);
window.setScene(scene);
window.show();
}
private Path initQuadCurve(int xStart, int yStart, int xEnd, int yEnd, int controlX, int controlY) {
Path curvePath = new Path();
curvePath.setStrokeWidth(STROKE_WIDTH);
MoveTo moveTo = new MoveTo(xStart, yStart);
QuadCurveTo quadTo = new QuadCurveTo();
quadTo.setControlX(controlX);
quadTo.setControlY(controlY);
quadTo.setX(xEnd);
quadTo.setY(yEnd);
curvePath.getElements().addAll(moveTo, quadTo);
curvePath.setOnMousePressed(e -> {
lastMouseX = e.getSceneX();
lastMouseY = e.getSceneY();
lastTranslateX = curvePath.getTranslateX();
lastTranslateY = curvePath.getTranslateY();
});
curvePath.setOnMouseDragged(e -> followMouse(curvePath, e));
return curvePath;
}
private Path initCubicCurve(int xStart, int yStart, int xEnd, int yEnd, int x1Control, int y1Control, int x2Control,
int y2Control) {
Path curvePath = new Path();
curvePath.setStrokeWidth(STROKE_WIDTH);
MoveTo moveTo = new MoveTo(xStart, yStart);
CubicCurveTo cubicTo = new CubicCurveTo();
cubicTo.setControlX1(x1Control);
cubicTo.setControlY1(y1Control);
cubicTo.setControlX2(x2Control);
cubicTo.setControlY2(y2Control);
cubicTo.setX(xEnd);
cubicTo.setY(yEnd);
curvePath.getElements().addAll(moveTo, cubicTo);
curvePath.setOnMousePressed(e -> {
lastMouseX = e.getSceneX();
lastMouseY = e.getSceneY();
lastTranslateX = curvePath.getTranslateX();
lastTranslateY = curvePath.getTranslateY();
});
curvePath.setOnMouseDragged(e -> followMouse(curvePath, e));
return curvePath;
}
private void followMouse(Node node, MouseEvent e) {
double deltaX = e.getSceneX() - lastMouseX;
double deltaY = e.getSceneY() - lastMouseY;
node.setTranslateX(deltaX + lastTranslateX);
node.setTranslateY(deltaY + lastTranslateY);
}
public static void main(String[] args) {
launch(args);
}
}
To be honest my first attempt was to call quad.setPickOnBounds(false); (and for the cubic as well ) as suggested on both post below :
Mouse Events get Ignored on the Underlying Layer
JavaFX Pass MouseEvents through Transparent Node to Children
But its not working or I miss something, but if creating the path by yourself works find its unnecessary to complicate thing more. In any case I recommend to have a look on the links if you want to follow the first approach. In case you are going to follow the second approach manipulating the paths is not going to be very difficult in my opinion.

How can I build map with region selection?

I trying to build map which consist of regions(states) and when mouse entered to some region, I need handle it. Have many png images which represent each region separately. I blend my images and got what I want, but I can't handle some region.
For instance:
It's a first region img
This is a second region img
As result I got:
Code:
#Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("view/MapView.fxml"));
Pane root = loader.load();
primaryStage.setTitle("Map");
primaryStage.setScene(new Scene(root, 700, 700));
primaryStage.show();
//First region
File file = new File("src/res/img/region1.png");
Canvas canvas = new Canvas(700, 700);
canvas.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, event -> System.out.println("Region 1"));
GraphicsContext graphicsContext = canvas.getGraphicsContext2D();
graphicsContext.drawImage(new Image(file.toURI().toString()), 0, 0);
root.getChildren().add(canvas);
//Second region
file = new File("src/res/img/region2.png");
canvas = new Canvas(700, 700);
canvas.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, event -> System.out.println("Region 2"));
graphicsContext = canvas.getGraphicsContext2D();
graphicsContext.drawImage(new Image(file.toURI().toString()), 0, 0);
root.getChildren().add(canvas);
}
In console I got always "Region 2".
Please give me tips for research. Thanks in advance!
You can use an ImageView and setPickOnBounds for that.
Example code:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class LayersWithMouseEvents extends Application {
#Override
public void start(Stage primaryStage) {
// root
StackPane root = new StackPane();
// create layers
Pane region1Layer = new Pane();
Pane region2Layer = new Pane();
// add layers
root.getChildren().addAll(region1Layer, region2Layer);
// load images
ImageView region1ImageView = new ImageView( new Image( getClass().getResource("region1.png").toExternalForm()));
ImageView region2ImageView = new ImageView( new Image( getClass().getResource("region2.png").toExternalForm()));
// add images
region1Layer.getChildren().add(region1ImageView);
region2Layer.getChildren().add(region2ImageView);
// mouse handler
region1Layer.setOnMousePressed(e -> System.out.println("Region 1: " + e));
region2Layer.setOnMousePressed(e -> System.out.println("Region 2: " + e));
// this is the magic that allows you to click on the separate layers, but ONLY(!) as long as the layer is transparent
region1Layer.setPickOnBounds(false);
region2Layer.setPickOnBounds(false);
primaryStage.setScene(new Scene(root, 800, 600));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
In the future please create an MCVE with focus on complete. Nobody wants to waste their time in order to make your incomplete example work.
To my opinion using a canvas here is not the right approach. If you would define your regions as polygons and add them to the scene graph you can attach a listener to each of these polygons and then react accordingly if the mouse is over some region. Maybe this is also possible with image views but I have never tried wether a transparent region of an image is also mouse transparent which seems to be necessary in that case. For a program of mine I used polygons and it works nicely.
The image data can be used to determine the color of the pixels under the mouse cursor. If the pixel is not fully transparent, then the cursor is on that region.
To retrieve this information you need to use a listener for the mouse moved event. For simplicity you can use a property with a listener attached to trigger the region enter/leave events:
The following example assumes you keep references to the images used for the regions named image1 and image2:
PixelReader reader1 = image1.getPixelReader();
PixelReader reader2 = image2.getPixelReader();
SimpleIntegerProperty region = new SimpleIntegerProperty(-1);
region.addListener((observable, oldValue, newValue) -> {
if (newValue.intValue() < 0) {
System.out.println("region left");
} else {
System.out.println("Region " + (newValue.intValue() + 1));
}
});
canvas.setOnMouseMoved(event -> {
int x = (int) event.getX();
int y = (int) event.getY();
if (x < image1.getWidth() && y < image1.getHeight() && reader1.getColor(x, y).getOpacity() != 0) {
region.set(0);
} else if (x < image2.getWidth() && y < image2.getHeight() && reader2.getColor(x, y).getOpacity() != 0) {
region.set(1);
} else {
region.set(-1);
}
});
Also there is no need to create multiple Canvas to paint the images.

Categories