Hatefiend Hatefiend - 20 days ago 5
Java Question

JavaFX - Notify when user drags mouse from one Node to another?

I have the following basic GUI for demonstration:

enter image description here

I'm trying to achieve the following functionality but I've exhausted all avenues that I've attempted.


User can left click on any of the ImageView's and it will create an
arrow that follows the user's cursor around until the user let's go of
the mouse button. (arrow start x,y is where he clicked and end x,y is
where his mouse currently is) If the user clicked on the Red
ImageView
and dragged it over the Blue
ImageView
and then let go,
the program would print
User just clicked from R to B


If the user clicked on the Red
ImageView
and let go of the mouse but
was not over a different
ImageView
, the program would print
User
just clicked from R but did not target a different ImageView
.

Under all circumstances, the arrow will appear when the user clicks on
the
ImageView
and will disappear the second he lets go of the mouse.


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.shape.Line;
import javafx.stage.Stage;

import java.util.HashMap;

public class Test extends Application
{
public static int HEIGHT = 500, WIDTH = 600;

@Override
public void start(Stage primaryStage) throws Exception
{
ImageView blue = new ImageView(new Image("blue.png")),
red = new ImageView(new Image("red.png")),
dark = new ImageView(new Image("dark.png"));

// Final array as to bypass the `final` requirement of event handler inner classes.
final ImageView[] hoveredOver = new ImageView[1];
final Line[] linePtr = new Line[1];
linePtr[0] = new Line();
linePtr[0].setStrokeWidth(10);

HashMap<ImageView, Character> lookup = new HashMap<ImageView, Character>(3)
{{
put(blue, 'B');
put(red, 'R');
put(dark, 'D');
}};

for (ImageView i : new ImageView[] { blue, red, dark })
{
i.setFitWidth(150);
i.setFitHeight(150);

// Set the anchor points of the click and display the arrow.
i.setOnMousePressed(e -> {
linePtr[0].setStartX(e.getX());
linePtr[0].setStartY(e.getY());
linePtr[0].setVisible(true);
});
// Move the arrow as the mouse moves.
i.setOnMouseDragged(e -> {
linePtr[0].setEndX(e.getX());
linePtr[0].setEndY(e.getY());
});
i.setOnMouseReleased(e -> {
// Not null means that the user WAS actually just now hovering over an imageview.
if (hoveredOver[0] != null)
System.out.printf("The user clicked from %c to %c!\n", lookup.get(i), lookup.get(hoveredOver[0]));
// Null means the user is not over an ImageView.
else
System.out.printf("The user initially clicked %c but did not drag to another Imageview.\n", lookup.get(i));
linePtr[0].setVisible(false);
});
// If the user enters ANY of the ImageViews,
// Set a variable so that the drag release listener
// can know about it!
i.setOnMouseDragOver(e -> hoveredOver[0] = i);
i.setOnMouseDragExited(e -> hoveredOver[0] = null);
}

blue.setX(400);
blue.setY(250);

red.setY(300);
red.setX(50);

/*
In this example I'm using a Pane but in my real program
I might be using a VBOX HBOX etc where I cannot freely move stuff around as I'd like.
This makes things extremely difficult and without using a 'Pane'
I don't know how this can even be done. Suggestions?
*/
Pane pneRoot = new Pane(blue, red, dark, linePtr[0]);
Scene scene = new Scene(pneRoot, WIDTH, HEIGHT);
primaryStage.setScene(scene);
primaryStage.show();
}

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


This was my best attempt and it's not even close. It moves a line (not an arrow, and ideally I want my arrow to curve as it moves much like this example image from a popular video game) but does not suit my needs. It cannot detect however when I let go while 'dragging over' an ImageView.

Is there a better way to do this? I feel like I can't simply the code I have down any further but there MUST be another way.

Answer
  1. Java is an object-oriented language. The basic idea is that you create classes to represent the data you are modeling and then create objects from those classes. If you are tying things together with arbitrary maps to look things up, and arrays kicking around for no apparent reason, you are starting in the wrong place.
  2. JavaFX has a system of observable properties. These wrap objects in a mutable way and can be observed so you can respond to changes.
  3. Make sure you read and understand the documentation on MouseEvents and MouseDragEvents. There are three different modes for handling dragging. For events (mouse drag events) to be sent to nodes other than the one on which the drag was initiated during a mouse drag, you need to be in full "press-drag-release gesture" mode. You can activate this mode by calling startFullDrag() on the node when responding to a dragDetected event.

I would start with something like

public class NamedDragAwareImageView {

    private final ObjectProperty<NamedDragAwareImageView> source ;
    private final ObjectProperty<NamedDragAwareImageView> destination ;
    private final String name ;
    private final ImageView imageView ;

    public NamedDragAwareImageView(ObjectProperty<NamedDragAwareImageView> source,
        ObjectProperty<NamedDragAwareImageView> destination,
        String name, String resource) {

        this.source = source ;
        this.destination = destination ;
        this.name = name ;
        this.imageView = new ImageView(new Image(resource));

        imageView.setOnDragDetected(e -> {
            source.set(this);
            destination.set(null);
            imageView.startFullDrag();
        });

        imageView.setOnMouseDragReleased(e -> {
            if (source.get() != null && source.get() != this) {
                destination.set(this);
            }
        });

        // other image view config...

    }

    public ImageView getView() {
        return imageView ;
    }

    public String getName() {
        return name ;
    }

}

Then you can do things like:

// observable properties to represent start and end nodes for drag:
ObjectProperty<NamedDragAwareImageView> source = new SimpleObjectProperty<>();
ObjectProperty<NamedDragAwareImageView> destination = new SimpleObjectProperty<>();


Pane root = new Pane();
// create your named image views, referencing the source and destination
// and add their image views to root, e.g.
NamedDragAwareImageView red = new NamedDragAwareImageView(source, destination, "Red", "red.png");
root.getChildren().add(red.getView());

// recommend using SVG paths (i.e. javafx.scene.shape.Path) for the arrow
// easy to draw programmatically, easy to manipulate elements etc:
Path arrowHead = new Path();
MoveTo arrowHeadStart = new MoveTo();
arrowHead.getElements().add(arrowHeadStart);
arrowHead.getElements().addAll(/* draw an arrow head with relative path elements... */);
arrowHead.setVisible(false);
// avoid arrowHead interfering with dragging:
arrowHead.setMouseTransparent(true);

// this will contain a MoveTo and a bunch of LineTo to follow the mouse:
Path arrowLine = new Path();
arrowLine.setMouseTransparent(true);

root.getChildren().addAll(arrowHead, arrowLine);

// change listener for source. source is set when drag starts:
source.addListener((obs, oldSource, newSource) -> {
    if (newSource == null) return ;
    arrowHeadStart.setX(/* x coord based on newSource */);
    arrowHeadStart.setY(/* similarly */);
    arrowHead.setVisible(true);
});

// change listener for destination. destination is only set
// when drag complete:
destination.addListener((obs, oldDestination, newDestination) -> {
    if (newDestination != null) {
        System.out.println("User dragged from "+source.get().getName()+
            " to "+destination.get().getName());
    }
});

root.setOnMouseDragOver(e -> {
    if (source.get()==null && destination.get()!=null) {
        // update arrowStart position
        // add line element to arrowLine
    }
});

root.setOnMouseReleased(e -> {
    // clear arrow:
    arrowHead.setVisible(false);
    arrowLine.getElements().clear();
});