Reputation: 77
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.
Upvotes: 2
Views: 1320
Reputation: 159376
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:
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.
Upvotes: 0
Reputation: 18415
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.
Upvotes: 1