Mouse drag gesture - java

I'm trying to drag nodes about and drop them onto each other. This is a simple class that I expected would react to drag gestures but it doesn't
public class CircleDrag extends Circle
{
double x, y;
String name;
int count = 0;
public CircleDrag(double centerX, double centerY, String name)
{
super(centerX, centerY, 10);
this.name = name;
setOnDragDetected(e ->
{
startFullDrag();
startDragAndDrop(TransferMode.ANY); // tried with and without this line.
logIt(e);
});
setOnDragEntered(e ->
{
logIt(e);
});
setOnDragDone(e ->
{
logIt(e);
});
setOnDragOver(e ->
{
logIt(e);
});
setOnMousePressed(e ->
{
logIt(e);
setMouseTransparent(true);
x = getLayoutX() - e.getSceneX();
y = getLayoutY() - e.getSceneY();
});
setOnMouseReleased(e ->
{
logIt(e);
setMouseTransparent(false);
});
setOnMouseDragged(e ->
{
logIt(e);
setLayoutX(e.getSceneX() + x);
setLayoutY(e.getSceneY() + y);
});
}
private void logIt(Event e)
{
System.out.printf("%05d %s: %s\n", count++, name, e.getEventType().getName());
}
}
I was expecting to add a bunch of CircleDrags to a pane and when dragging one onto another the other would fire an onDrag* event. But it doesn't.
What is it I don't understand about this gesture?
Thanks
Ollie.

Here's how you could do it in general:
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.event.EventHandler;
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.Circle;
import javafx.stage.Stage;
public class PhysicsTest extends Application {
public static List<Circle> circles = new ArrayList<Circle>();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
Group root = new Group();
Circle circle1 = new Circle( 50);
circle1.setStroke(Color.GREEN);
circle1.setFill(Color.GREEN.deriveColor(1, 1, 1, 0.3));
circle1.relocate(100, 100);
Circle circle2 = new Circle( 50);
circle2.setStroke(Color.BLUE);
circle2.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.3));
circle2.relocate(200, 200);
MouseGestures mg = new MouseGestures();
mg.makeDraggable( circle1);
mg.makeDraggable( circle2);
circles.add( circle1);
circles.add( circle2);
root.getChildren().addAll(circle1, circle2);
primaryStage.setScene(new Scene(root, 1600, 900));
primaryStage.show();
}
public static class MouseGestures {
double orgSceneX, orgSceneY;
double orgTranslateX, orgTranslateY;
public void makeDraggable( Node node) {
node.setOnMousePressed(circleOnMousePressedEventHandler);
node.setOnMouseDragged(circleOnMouseDraggedEventHandler);
}
EventHandler<MouseEvent> circleOnMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
orgSceneX = t.getSceneX();
orgSceneY = t.getSceneY();
Circle p = ((Circle) (t.getSource()));
orgTranslateX = p.getCenterX();
orgTranslateY = p.getCenterY();
}
};
EventHandler<MouseEvent> circleOnMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
double offsetX = t.getSceneX() - orgSceneX;
double offsetY = t.getSceneY() - orgSceneY;
double newTranslateX = orgTranslateX + offsetX;
double newTranslateY = orgTranslateY + offsetY;
Circle p = ((Circle) (t.getSource()));
p.setCenterX(newTranslateX);
p.setCenterY(newTranslateY);
for( Circle c: circles) {
if( c == p)
continue;
if( c.getBoundsInParent().intersects(p.getBoundsInParent())) {
System.out.println( "Overlapping!");
}
}
}
};
}
}
Please note that this solution uses the bounds in the parent, ie in the end a rectangle is used for overlap check. If you want to use eg a circle check, you could use the radius and check the distance between the circles. Depends on your requirement.

Challenges with your current solution
You need to put some content in the dragboard
If you don't put anything in the dragboard when the drag is initially detected, there is nothing to drag, so subsequent drag related events such as dragEntered, dragDone and dragOver will never be fired.
Conflating both "dragging the node using a mouse" with "drag and drop content processing" is hard
I couldn't get it to work exactly as you have it with the drag handled by mouse drag events as well as having a drag and drop operation in effect because as soon as the drag and drop operation took effect, the node stopped receiving mouse drag events.
Sample Solution
As a result of the above challenges, the approach I took was:
Put something in the dragboard when a drag is detected.
Remove the mouse event handlers and only use drag event handlers.
Simulate dragging the node around by taking a snapshot of the node to an image, then hiding the node and making use of the DragView with the node image.
When the drag process completes, detect the current location of the mouse cursor then relocate the node to that location.
Unfortunately, JavaFX drag events are unlike mouse events. The drag events don't seem to include full location information (e.g. x,y or sceneX,sceneY). This means you need a way to determine this information independent of the event. I don't know of an API in JavaFX to detect the current location of the mouse cursor, so I had to resort to the awt MouseInfo class to determine the current mouse location.
In the process, I lost a little bit of the accuracy in initial and final node location calculation. For small circles, that doesn't not seem to matter. For other apps, you could probably modify my logic slightly to make the drag and drop transitions 100% accurate and smooth.
I used Java 8 for the sample solution (DragView is not available in Java 7). CircleDrag is an updated version of your draggable node with drag and drop handling. The CircleDragApp is just a JavaFX application test harness for the CircleDrag nodes.
CircleDrag.java
import javafx.event.Event;
import javafx.scene.SnapshotParameters;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import java.awt.Point;
import java.awt.MouseInfo;
public class CircleDrag extends Circle {
private final String name;
private int count = 0;
public CircleDrag(double centerX, double centerY, String name) {
super(centerX, centerY, 10);
this.name = name;
setOnDragDetected(e -> {
ClipboardContent content = new ClipboardContent();
content.putString(name);
Dragboard dragboard = startDragAndDrop(TransferMode.ANY);
dragboard.setContent(content);
SnapshotParameters params = new SnapshotParameters();
params.setFill(Color.TRANSPARENT);
dragboard.setDragView(snapshot(params, null));
dragboard.setDragViewOffsetX(dragboard.getDragView().getWidth() / 2);
dragboard.setDragViewOffsetY(dragboard.getDragView().getHeight() / 2);
setVisible(false);
e.consume();
logIt(e);
});
setOnDragEntered(this::logIt);
setOnDragDone(e ->
{
Point p = MouseInfo.getPointerInfo().getLocation();
relocate(
p.x - getScene().getWindow().getX() - getScene().getX() - getRadius(),
p.y - getScene().getWindow().getY() - getScene().getY() - getRadius()
);
setVisible(true);
logIt(e);
});
setOnDragDropped(e -> {
Dragboard db = e.getDragboard();
System.out.println("Dropped: " + db.getString() + " on " + name);
e.setDropCompleted(true);
e.consume();
logIt(e);
});
setOnDragOver(e -> {
if (e.getGestureSource() != this) {
e.acceptTransferModes(TransferMode.ANY);
logIt(e);
}
e.consume();
});
}
private void logIt(Event e) {
System.out.printf("%05d %s: %s\n", count++, name, e.getEventType().getName());
}
}
CircleDragApp.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Random;
public class CircleDragApp extends Application {
private static final int W = 320;
private static final int H = 200;
private static final int R = 5;
private Random random = new Random(42);
public void start(Stage stage) throws Exception {
Pane pane = new Pane();
pane.setPrefSize(W, H);
for (int i = 0; i < 10; i++) {
CircleDrag circle = new CircleDrag(
random.nextInt(W - R) + R,
random.nextInt(H - R) + R,
i + ""
);
circle.setFill(Color.rgb(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
pane.getChildren().add(circle);
}
stage.setScene(new Scene(pane));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Parting Thoughts
Lorand's solution which does not make use of the drag event handlers looks pretty good in comparison to what I have and may have a less quirks. Study both and choose the solution which appears best for your situation.
My general recommendation is that if you are going to be doing data transfer handling, then the drag and drop APIs might be a good approach. If you are not doing data transfer handling, then sticking with plain mouse events might be the best approach.

Related

JavaFX How can I use one instance of scene in 2 windows? (Mirroring)

I made a chess game that is supposed to run in 2 windows (stages). When a player moves a piece, in the second window the same piece moves accordingly. But the second window is rotated by 180 degrees to simulate a 2 player experience.
To realize this I thought it would be the easiest to use the 1 scene in 2 Windows. Basically a mirror of the first scene.
Problem: Figure doesn't move in the second window but the game knows that it has been moved in the first window because the player can't move the same figures again but the other color.
There is also a Main Menu, which has a play button, that starts the 2 windows.
#FXML
public void play_game(ActionEvent event) throws IOException {
Parent root = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("game_board.fxml")));
Parent second_screen = FXMLLoader.load(Objects.requireNonNull(getClass().getResource("game_board.fxml")));
stage = (Stage)((Node)event.getSource()).getScene().getWindow();
scene = new Scene(root);
stage.setScene(scene);
stage.show();
Stage second_stage = new Stage();
second_stage.setScene(new Scene(second_screen));
second_screen.rotateProperty().set(180);
second_stage.show();
}
The **controller class ** for the first window:
public class board_controller {
#FXML
GridPane chess_board;
#FXML
GridPane second_board;
#FXML
public void initialize(){
game_logic game_logic = new game_logic(chess_board);
}
The controller class for the second window:
public class second_board_controller extends board_controller {
#FXML
public void initialize(){
game_logic new_round = new game_logic(chess_board);
}
}
My question is: How can I use the same exact instance of the scene but only rotated in the second window (Basically a Mirror of the first window)?
Game Example
Thank you!
I tried making the gridpane in the first_board controller static using it in the second board controller with the hopes of them updating automatically but with no results. Setting the main scene in first and second stages but my IDE said it's not allowed. I'm out of ideas...
Share the Model
Since you want to mirror between two windows in the same process, the general idea is to create two instances of your view but share only one instance of the model. The model should be observable in some way so that the view/controller can react to changes in the model by updating the view. With the model being shared and observed, updating it from one window will be seen by the other window.
Models
Note a model should not know about the view. In your code, you do:
game_logic game_logic = new game_logic(chess_board);
From the name of the class, this indicates you're passing a GridPane (the view) to your model. It would be better if your model only modelled a chess game. The controller/view is responsible for translating that state into a visual representation.
Rotating the Second View
The simplest approach to this would be to add state/a method to your controller, and then only on the second instance of the controller configure it to rotate the view. It is at least somewhat justifiable to put this logic in the controller/view because it is only a view thing (it does not affect the game state).
Though instead of rotating the board, you might want to consider "inverting" the location of the pieces (vertically). In other words, for the second view, have it so that a white piece in the bottom-left corner of the board is actually displayed in the top-left corner (and the opposite for black pieces). That way the chess piece images are not rotated along with the rest of the board.
Example
Here's a proof-of-concept for mirroring a draggable rectangle (much simpler than a chess game). Note it only demonstrates the mirroring, it does not show how to e.g., rotate the view in one window but not the other.
RectangleModel.java:
package sample;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
public class RectangleModel {
private final List<Consumer<? super Dimensions>> listeners = new CopyOnWriteArrayList<>();
private Dimensions dimensions;
public RectangleModel(Dimensions dimensions) {
this.dimensions = Objects.requireNonNull(dimensions);
}
public RectangleModel(double x, double y, double width, double height) {
this(new Dimensions(x, y, width, height));
}
public void move(double deltaX, double deltaY) {
if (deltaX != 0.0 || deltaY != 0.0) {
double x = dimensions.x() + deltaX;
double y = dimensions.y() + deltaY;
double w = dimensions.width();
double h = dimensions.height();
dimensions = new Dimensions(x, y, w, h);
notifyListeners();
}
}
public Dimensions getDimensions() {
return dimensions;
}
public void addDimensionsListener(Consumer<? super Dimensions> listener) {
listeners.add(Objects.requireNonNull(listener));
}
public void removeDimensionsListener(Consumer<? super Dimensions> listener) {
listeners.remove(Objects.requireNonNull(listener));
}
private void notifyListeners() {
for (var listener : listeners) {
listener.accept(dimensions);
}
}
public record Dimensions(double x, double y, double width, double height) {}
}
RectangleController.java:
package sample;
import java.util.function.Consumer;
import javafx.fxml.FXML;
import javafx.geometry.Point2D;
import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Rectangle;
public class RectangleController {
private final Consumer<RectangleModel.Dimensions> listener = this::updateRectangle;
#FXML
private Rectangle rectangle;
private Point2D offset;
private RectangleModel model;
public void setModel(RectangleModel model) {
if (this.model != null) {
this.model.removeDimensionsListener(listener);
}
this.model = model;
if (model != null) {
model.addDimensionsListener(listener);
updateRectangle(model.getDimensions());
} else {
updateRectangle(null);
}
}
private void updateRectangle(RectangleModel.Dimensions dims) {
if (dims != null) {
rectangle.setX(dims.x());
rectangle.setY(dims.y());
rectangle.setWidth(dims.width());
rectangle.setHeight(dims.height());
} else {
rectangle.setX(0);
rectangle.setY(0);
rectangle.setWidth(0);
rectangle.setHeight(0);
}
}
#FXML
private void handleMousePressed(MouseEvent event) {
event.consume();
offset = new Point2D(event.getX(), event.getY());
}
#FXML
private void handleMouseDragged(MouseEvent event) {
event.consume();
double deltaX = event.getX() - offset.getX();
double deltaY = event.getY() - offset.getY();
model.move(deltaX, deltaY);
offset = new Point2D(event.getX(), event.getY());
}
#FXML
private void handleMouseReleased(MouseEvent event) {
event.consume();
offset = null;
}
}
RectangleView.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.shape.Rectangle?>
<Pane xmlns="http://javafx.com/javafx/" xmlns:fx="http://javafx.com/fxml/"
fx:controller="sample.RectangleController">
<Rectangle fx:id="rectangle" onMousePressed="#handleMousePressed" onMouseDragged="#handleMouseDragged"
onMouseReleased="#handleMouseReleased"/>
</Pane>
Main.java:
package sample;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
var model = new RectangleModel(0, 0, 100, 50);
primaryStage.setScene(createScene(model));
primaryStage.setTitle("Primary Stage");
primaryStage.show();
var secondStage = new Stage();
secondStage.setScene(createScene(model));
secondStage.setTitle("Second Stage");
secondStage.show();
primaryStage.setX(primaryStage.getX() - primaryStage.getWidth() / 2);
secondStage.setX(primaryStage.getX() + primaryStage.getWidth());
secondStage.setY(primaryStage.getY());
primaryStage.setOnCloseRequest(e -> secondStage.close());
secondStage.setOnCloseRequest(e -> primaryStage.close());
}
private Scene createScene(RectangleModel model) throws IOException {
var loader = new FXMLLoader(Main.class.getResource("RectangleView.fxml"));
var root = loader.<Parent>load();
var controller = loader.<RectangleController>getController();
var scene = new Scene(root, 600, 400);
controller.setModel(model);
return scene;
}
}
Naming Conventions
You should follow the standard naming conventions of Java (or whatever language you're using) when posting on a public forum.
Classes and interfaces use PascalCase.
Methods, fields, parameters, and local variables use camelCase.
Static constants use UPPER_SNAKE_CASE.

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();
}
}

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.

JavaFX Container Draggable

i have a draggable container in JavaFX. This Container is implemented in a PopUp. I can drag the Container, but if i drag it, the mouse-event hasn't a constant coordinate. There switchs the mouse position very fast between 2 fix Positions.
Thats my code:
container.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent me) {
// TODO Auto-generated method stub
if(dragAct==true){
//Set the Position of the PopUp to the Position of the Mouse
setX(me.getX());
setY(me.getY());
}
}
});
The containe is a VBox. The Main-Class is a extended Version of the PopUp-Class.
JavaFX Container Draggable
The setX and setY methods you call set the position of the Popup in screen coordinates. The calls to me.getX() and me.getY() give you the coordinates of the mouse relative to the container. When you move the popup, the container also moves, so the position of the mouse has changed relative to the container. So your calculations are not going to be consistent from one dragging event to the next.
The fix is to compute the positions relative to something that is fixed. Since you are moving the popup, which is a window, the fixed coordinate system is the screen coordinate system. MouseEvent has getScreenX and getScreenY methods you can use to easily get these.
I like to implement dragging by saving the last mouse location and then computing the distance moved on drag. There are other (possibly less verbose) ways to do this but to me this is clearest:
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Popup;
import javafx.stage.Stage;
public class DraggingPopup extends Application {
#Override
public void start(Stage primaryStage) {
Button button = new Button("Show popup");
button.setOnAction(event -> showDraggablePopup(primaryStage));
StackPane root = new StackPane(button);
Scene scene = new Scene(root, 250, 75);
primaryStage.setScene(scene);
primaryStage.show();
}
private void showDraggablePopup(Stage owner) {
Popup popup = new Popup();
Button closeButton = new Button("Close");
closeButton.setOnAction(event -> popup.hide());
StackPane container = new StackPane(closeButton);
container.setStyle("-fx-background-color: steelblue;");
container.setMinWidth(300);
container.setMinHeight(125);
// Dragging implementation:
ObjectProperty<Point2D> mouseLocation = new SimpleObjectProperty<>();
container.setOnMousePressed(event ->
mouseLocation.set(new Point2D(event.getScreenX(), event.getScreenY())));
container.setOnMouseDragged(event -> {
if (mouseLocation.get() != null) {
double x = event.getScreenX();
double deltaX = x - mouseLocation.get().getX() ;
double y = event.getScreenY();
double deltaY = y - mouseLocation.get().getY() ;
//in case of 2 or more computer screens this help me to avoid get stuck on 1 screen
if(Math.abs(popup.getX()-x)>popup.getWidth()){
popup.setX(x);
popup.setY(y);
}else {
popup.setX(popup.getX() + deltaX);
popup.setY(popup.getY() + deltaY);
}
mouseLocation.set(new Point2D(x, y));
}
});
container.setOnMouseReleased(event -> mouseLocation.set(null));
popup.getScene().setRoot(container);
popup.show(owner);
}
public static void main(String[] args) {
launch(args);
}
}

dragging nodes when their parent has a transformation results in nodes disappearing

Very weird problem, I finally managed to distill it into a small piece of code which demonstrates the problem. I have a pane, which contains 1 group, that groups contains a group which contains some ellipses. The top group has a rotate transform applied to it. The ellipses are made draggable.
Try the below example, drag some ellipses downwards (outside the group's bounds), you'll see them disappearing. If you maximize the window, they appear again but you can't drag them anymore, they don't receive any events anymore.
Now for the really strange part, there are three ways I can make the problem go away:
don't apply the transform
remove one ellipse (!?) (I experimented to get to this number, 11)
start ScenicView alongside and select the group containing the ellipses so you can see the bounds of the group
I'm at a total loss here, completely stupefied. Please, does anyone have any idea why this problem is occuring and how to solve it?
Code (JavaFX 2.2.3 and java 1.7.0_09 64bit Windows 7):
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.GroupBuilder;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SceneBuilder;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.EllipseBuilder;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.RotateBuilder;
import javafx.stage.Stage;
public class DragProblem extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
DrawingPane drawingPane = new DrawingPane();
drawingPane.setStyle("-fx-background-color: darkgrey;");
Scene scene = SceneBuilder.create().root(drawingPane).width(1280d).height(1024d).build();
primaryStage.setScene(scene);
primaryStage.show();
}
public class DrawingPane extends Pane {
private Group transformedGroup;
private Group splinePoints;
public DrawingPane() {
transformedGroup = GroupBuilder.create().id("transformedGroup").build();
getChildren().add(transformedGroup);
addPoints();
makePointsDraggable();
}
public void addPoints() {
double[] coords = new double[] {
// comment any one the below x,y coordinates and the problem doesn't occur..
239.28353881835938, 488.2192687988281,
245.04466247558594, 505.30169677734375,
258.56671142578125, 539.49462890625,
267.2294006347656, 563.618408203125,
282.89141845703125, 587.84033203125,
309.6925048828125, 602.2174072265625,
327.4945068359375, 616.4683227539062,
345.25445556640625, 633.718994140625,
371.0416259765625, 649.0819702148438,
393.78704833984375, 667.402587890625,
442.67010498046875, 676.0886840820312 };
splinePoints = GroupBuilder.create().build();
for (int i = 0; i < coords.length; i += 2) {
Ellipse ellipse = EllipseBuilder.create().radiusX(3).radiusY(3).centerX(coords[i]).centerY(coords[i + 1]).build();
splinePoints.getChildren().add(ellipse);
}
transformedGroup.getChildren().add(splinePoints);
Rotate rotateTransform = RotateBuilder.create().build();
rotateTransform.setPivotX(224);
rotateTransform.setPivotY(437);
rotateTransform.setAngle(15);
// ..or comment this line to prevent the problem occuring
transformedGroup.getTransforms().add(rotateTransform);
}
public void makePointsDraggable() {
for (final Node n : splinePoints.getChildren()) {
Ellipse e = (Ellipse) n;
final NodeDragHandler ellipseDragHandler = new NodeDragHandler(e, transformedGroup);
e.setOnMousePressed(ellipseDragHandler);
e.setOnMouseDragged(ellipseDragHandler);
}
}
}
public class NodeDragHandler implements EventHandler<MouseEvent> {
protected final Ellipse node;
private final Node transformedGroup;
private double initialX;
private double initialY;
private Point2D initial;
private boolean dragStarted = false;
public NodeDragHandler(Ellipse node, Group transformedGroup) {
this.node = node;
this.transformedGroup = transformedGroup;
}
#Override
public void handle(MouseEvent event) {
if (!dragStarted) {
initialX = event.getScreenX();
initialY = event.getScreenY();
initial = transformedGroup.localToParent(new Point2D(node.getCenterX(), node.getCenterY()));
dragStarted = true;
} else {
double xDragged = event.getScreenX() - initialX;
double yDragged = event.getScreenY() - initialY;
Point2D newPos = new Point2D(initial.getX() + xDragged, initial.getY() + yDragged);
Point2D p = transformedGroup.parentToLocal(newPos.getX(), newPos.getY());
node.setCenterX(p.getX());
node.setCenterY(p.getY());
}
}
}
}
It's been acknowledged as a bug in JavaFX and will be solved in 2.2.6, see here. I've tested it with the early access release and I can confirm it has been solved.

Categories