Ollie
Ollie

Reputation: 77

Mouse drag gesture

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

Answers (2)

jewelsea
jewelsea

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

circles

As a result of the above challenges, the approach I took was:

  1. Put something in the dragboard when a drag is detected.
  2. Remove the mouse event handlers and only use drag event handlers.
  3. 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.
  4. 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.

Upvotes: 0

Roland
Roland

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

Related Questions