Jens-Peter Haack
Jens-Peter Haack

Reputation: 1907

How to delay the Drag-Start to combine move and Drag-And-Drop dynamically

I want to provide an application that:

So the javafx DragDetected() would come too soon during the object move on the canvas area, I suppress the onDragDetected() handling and in the onMouseDragged() handler I tried to convert the MouseDrag event into a Drag event using

event.setDragDetect(true);

But the onDragDetected() comes never again..... what can I do?

The full sample application is:

package fx.samples;

import java.io.File;
import java.util.LinkedList;

import javax.imageio.ImageIO;

import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.WritableImage;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class DragRectangle extends Application {
    Point2D lastXY = null;

    public void start(Stage primaryStage) {
        Pane mainPane = new Pane(); 
        Scene scene = new Scene(mainPane, 500, 500);
        primaryStage.setScene(scene);
        primaryStage.show();

        Rectangle area = new Rectangle(0, 0, 500 , 500);

        Rectangle rect = new Rectangle(0, 0, 30, 30);
        rect.setFill(Color.RED);
        mainPane.getChildren().add(rect);

        rect.setOnMouseDragged(event -> {
            System.out.println("Move");
            Node on = (Node)event.getTarget();
            if (lastXY == null) {
                lastXY = new Point2D(event.getSceneX(), event.getSceneY());
            }
            double dx = event.getSceneX() - lastXY.getX();
            double dy = event.getSceneY() - lastXY.getY();
            on.setTranslateX(on.getTranslateX()+dx);
            on.setTranslateY(on.getTranslateY()+dy);
            lastXY = new Point2D(event.getSceneX(), event.getSceneY());
            if (!area.intersects(event.getSceneX(), event.getSceneY(), 1, 1)) {
                System.out.println("->Drag");
                event.setDragDetect(true);
            } else {
                event.consume();
            }
        });

        rect.setOnDragDetected(event -> {
            System.out.println("Drag:"+event);
            if (area.intersects(event.getSceneX(), event.getSceneY(), 1, 1)) { event.consume(); return; }
            Node on = (Node)event.getTarget();
            Dragboard db = on.startDragAndDrop(TransferMode.COPY);
            db.setContent(makeClipboardContent(event, on, null));
            event.consume();
        });

        rect.setOnMouseReleased(d -> lastXY = null);
    }

    public static ClipboardContent makeClipboardContent(MouseEvent event, Node child, String text) {
        ClipboardContent cb = new ClipboardContent();
        if (text != null) {
            cb.put(DataFormat.PLAIN_TEXT, text);
        }
        if (!event.isShiftDown()) {
            SnapshotParameters params = new SnapshotParameters();
            params.setFill(Color.TRANSPARENT);
            Bounds b = child.getBoundsInParent();
            double f = 10;
            params.setViewport(new Rectangle2D(b.getMinX()-f, b.getMinY()-f, b.getWidth()+f+f, b.getHeight()+f+f));

            WritableImage image = child.snapshot(params, null);
            cb.put(DataFormat.IMAGE, image);

            try {
                File tmpFile = File.createTempFile("snapshot", ".png");
                LinkedList<File> list = new LinkedList<File>();
                ImageIO.write(SwingFXUtils.fromFXImage(image, null),
                        "png", tmpFile);
                list.add(tmpFile);
                cb.put(DataFormat.FILES, list);
            } catch (Exception e) {

            }
        }
        return cb;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Upvotes: 0

Views: 3136

Answers (2)

Jens-Peter Haack
Jens-Peter Haack

Reputation: 1907

Ok, I spend some hours reading the JavaFX sources and playing arround with EventDispatcher etc... its finally easy:

In Short:

Suppress the system drag start proposal in the onMouseDragged() handler and set that flag on your behalf:

onMouseDragged(e -> {
    e.setDragDetect(false); // clear the system proposal
    if (...) e.setDragDetect(true); // trigger drag on your own decision
}

Long text:

The mechanism to start a DragDetected is consequently using the MouseEvent MOUSE_DRAGGED. The system Drag detection will apply some rules to determine if the current mouse-drag will be interpreted as a drag, here the original code:

        if (dragDetected != DragDetectedState.NOT_YET) {
            mouseEvent.setDragDetect(false);
            return;
        }

        if (mouseEvent.getEventType() == MouseEvent.MOUSE_PRESSED) {
            pressedX = mouseEvent.getSceneX();
            pressedY = mouseEvent.getSceneY();

            mouseEvent.setDragDetect(false);

        } else if (mouseEvent.getEventType() == MouseEvent.MOUSE_DRAGGED) {

            double deltaX = Math.abs(mouseEvent.getSceneX() - pressedX);
            double deltaY = Math.abs(mouseEvent.getSceneY() - pressedY);
            mouseEvent.setDragDetect(deltaX > hysteresisSizeX ||
                                     deltaY > hysteresisSizeY);

        }
    }

and it set

mouseEvent.setDragDetect(true) 

in the normal MOUSE_DRAG event. That event is passed down and is being processed by all 'down-chain' EventDispatchers... only if this events finally arrives for processing and if the isDragDetect flag is still true, a follow up DragDetected event will be generated.

So I am able to delay the DragDetected by clearing the isDragDetect flag on its way down using an EventDispatcher:

        mainPane.setEventDispatcher((event, chain) -> {
            switch (event.getEventType().getName()) {
                case "MOUSE_DRAGGED":
                    MouseEvent drag = (MouseEvent)event;

                    drag.setDragDetect(false);
                    if (!area.intersects(drag.getSceneX(), drag.getSceneY(), 1, 1)) {
                        System.out.println("->Drag down");
                        drag.setDragDetect(true);
                    }
                    break;
            }

            return chain.dispatchEvent(event);
        });

And if this code decides that a drag condition is reached, it simply sets the flag.

      drag.setDragDetect(true);

Now I am able to move precisely my objects and start the Drag if they are moved outside the application area.

And after some minutes of thinking: the EventDispatcher is not necessary, all can be done in the onMouseDragged handler...

Full code:

package fx.samples;

import java.io.File;
import java.util.LinkedList;

import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.WritableImage;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

import javax.imageio.ImageIO;


public class DragRectangle extends Application {
    Point2D lastXY = null;

    public void start(Stage primaryStage) {
        Pane mainPane = new Pane(); 
        Scene scene = new Scene(mainPane, 500, 500);
        primaryStage.setScene(scene);
        primaryStage.show();

        Rectangle area = new Rectangle(0, 0, 500 , 500);

        Rectangle rect = new Rectangle(0, 0, 30, 30);
        rect.setFill(Color.RED);
        mainPane.getChildren().add(rect);

        rect.setOnMouseDragged(event -> {
            System.out.println("Move");
            event.setDragDetect(false);
            Node on = (Node)event.getTarget();
            if (lastXY == null) {
                lastXY = new Point2D(event.getSceneX(), event.getSceneY());
            }
            double dx = event.getSceneX() - lastXY.getX();
            double dy = event.getSceneY() - lastXY.getY();
            on.setTranslateX(on.getTranslateX()+dx);
            on.setTranslateY(on.getTranslateY()+dy);
            lastXY = new Point2D(event.getSceneX(), event.getSceneY());
            if (!area.intersects(event.getSceneX(), event.getSceneY(), 1, 1)) event.setDragDetect(true);
            event.consume();
        });

        rect.setOnDragDetected(event -> {
            System.out.println("Drag:"+event);
            Node on = (Node)event.getTarget();
            Dragboard db = on.startDragAndDrop(TransferMode.COPY);
            db.setContent(makeClipboardContent(event, on, "red rectangle"));
            event.consume();
        });

        rect.setOnMouseReleased(d ->  lastXY = null);
    }

    public static ClipboardContent makeClipboardContent(MouseEvent event, Node child, String text) {
        ClipboardContent cb = new ClipboardContent();
        if (text != null) {
            cb.put(DataFormat.PLAIN_TEXT, text);
        }
        if (!event.isShiftDown()) {
            SnapshotParameters params = new SnapshotParameters();
            params.setFill(Color.TRANSPARENT);
            Bounds b = child.getBoundsInParent();
            double f = 10;
            params.setViewport(new Rectangle2D(b.getMinX()-f, b.getMinY()-f, b.getWidth()+f+f, b.getHeight()+f+f));

            WritableImage image = child.snapshot(params, null);
            cb.put(DataFormat.IMAGE, image);

            try {
                File tmpFile = File.createTempFile("snapshot", ".png");
                LinkedList<File> list = new LinkedList<File>();
                ImageIO.write(SwingFXUtils.fromFXImage(image, null),
                        "png", tmpFile);
                list.add(tmpFile);
                cb.put(DataFormat.FILES, list);
            } catch (Exception e) {

            }
        }
        return cb;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Upvotes: 1

Jos&#233; Pereda
Jos&#233; Pereda

Reputation: 45456

Based on this:

The default drag detection mechanism uses mouse movements with a pressed button in combination with hysteresis. This behavior can be augmented by the application. Each MOUSE_PRESSED and MOUSE_DRAGGED event has a dragDetect flag that determines whether a drag gesture has been detected. The default value of this flag depends on the default detection mechanism and can be modified by calling setDragDetect() inside of an event handler. When processing of one of these events ends with the dragDetect flag set to true, a DRAG_DETECTED MouseEvent is sent to the potential gesture source (the object on which a mouse button has been pressed). This event notifies about the gesture detection.

your assumption that just enabling setDragDetect(true) at some point after the mousedragged has started will fire the drag event, does not work.

The reason for that is there is some private DragDetectedState flag, that is used to set the drag state at the beginning of the gesture. And once it's already selected, it can't be changed.

So if the event it's not triggered, what you could do is manually trigger it yourself:

if (!area.intersects(event.getSceneX(), event.getSceneY(), 1, 1)) {
        System.out.println("->Drag");
        event.setDragDetect(true);
        Event.fireEvent(rect, new MouseEvent(MouseEvent.DRAG_DETECTED, 0, 0, 0, 0, MouseButton.PRIMARY, 0, false, false, false, false, true, false, false, true, true, true, null));
    }

This would effectively trigger a drag dectected event, and rect.setOnDragDetected() will be called.

But this is what you will get:

Exception in thread "JavaFX Application Thread" java.lang.IllegalStateException: 
     Cannot start drag and drop outside of DRAG_DETECTED event handler
at javafx.scene.Scene.startDragAndDrop(Scene.java:5731)
at javafx.scene.Node.startDragAndDrop(Node.java:2187)

Basically, you can't combine DragDetected with MouseDragged, since the startDragAndDrop method has to be called within a Drag_Detected event:

Confirms a potential drag and drop gesture that is recognized over this Node. Can be called only from a DRAG_DETECTED event handler.

So instead of trying to combine two different events, my suggestion is you just use drag detected event, allowing your scene or parts of it to accept the drop, and translating your node there, while at the same time, if the drag&drop leaves the scene, you could drop the file on the new target.

Something like this:

@Override
public void start(Stage primaryStage) {
    Pane mainPane = new Pane();

    Rectangle rect = new Rectangle(0, 0, 30, 30);
    rect.setFill(Color.RED);
    mainPane.getChildren().add(rect);

    Scene scene = new Scene(mainPane, 500, 500);
    primaryStage.setScene(scene);
    primaryStage.show();

    rect.setOnDragDetected(event -> {
        Node on = (Node)event.getSource();
        Dragboard db = on.startDragAndDrop(TransferMode.ANY);
        db.setContent(makeClipboardContent(event, on, null));
        event.consume();
    });

    mainPane.setOnDragOver(e->{
        e.acceptTransferModes(TransferMode.ANY);
    });

    mainPane.setOnDragExited(e->{
        rect.setLayoutX(e.getSceneX()-rect.getWidth()/2d);
        rect.setLayoutY(e.getSceneY()-rect.getHeight()/2d);
    });
}

Upvotes: 0

Related Questions