Drag-and-Drop in JavaFX (Draggable Icons), Part 4

To this point, we’ve created our skeleton application with a SplitPane view populated with a vertical list of colored icons at left. These icons are draggable from the left-hand pane to the right-hand pane, but that’s it.  In this post, we’ll complete the first drag-and-drop function as well as implement a custom drag-and-drop data structure for passing data with a drag-and drop operation.

You can also get the complete code for this application which demonstrates the work of these first four parts of the series.

To get started, we’ll need to review the way JavaFX transfers information as a part of the drag-and-drop operation.  There are three key components to a drag-and-drop operation in JavaFX:

  • The event DragBoard
  • A ClipboardContent object
  • The content to transfer

Note that if you try to perform drag-and-drop in JavaFX without providing at least some dummy information, it’s likely you’ll run into strange errors.

The object hierarchy is straightforward.  The event Dragboard is the means by which information is transmitted through the drag and drop operation.  The Dragboard is available through the event object at every trappable DragEvent, from DragDetected through DragDone.

The DragBoard maintains and accrues information using the ClipboardContent object.  The ClipboardContent class extends HashMap <DataFormat, Object>, where DataFormat is a static “key” for each type of drag data (often mapping directly to a MIME type) and Object represents the data itself.  We’ll come back to that.

Note that the data that gets passed must be serializable.  So, if you intend to pass a custom class, it’ll need to implement the Serializable interface.

So, before we fully implement drag-and-drop data transfer, let’s finish what we’ve started and implement a very simple, straightforward version.

In the RootLayout class, we added a private method, addDragDetection().  In it, we included the following code:

// get a reference to the clicked DragIcon object
DragIcon icn = (DragIcon) event.getSource();

//begin drag ops
mDragOverIcon.setType(icn.getType());
mDragOverIcon.relocateToPoint(new Point2D (event.getSceneX(), event.getSceneY()));

ClipboardContent content = new ClipboardContent();
content.putString(mDragOverIcon.getType().toString());
mDragOverIcon.startDragAndDrop (TransferMode.ANY).setContent(content);

A new ClipboardContent object is created to store the data we’re passing, namely, the “type” of icon we’re dragging.  This is merely the String representation of the chosen DragIcon’s DragIconType.

Once the drag and drop operation has begun, the data that we passed to the event Dragboard will persist to the end of the operation.  To prove that, try inserting the following code to the DragDone event handler:

System.out.println ((String) event.getDragboard().getContent(DataFormat.PLAIN_TEXT));

Run the application and drag and drop an icon from the left pane into the right pane of the SplitPane.

You should see the name of the icon’s color appear in your console.

Custom Drag And Drop Data

In the previous example, we simply accessed the event’s Dragboard and retrieved the content using the default text/plain MIME type.

But suppose we want to handle multiple kinds of drag and drop operations?  After all, by the time this example application is finished, we’ll have handled three distinct drag-and-drop operations.  How can we differentiate between them if they all have the same kind of data type?  Or worse, what if they all have a unique, custom data type?

The answer to that is to create a custom container class we can use to store information and specify the different types of drag operations we may perform.

Create a new class and call it DragContainer.

Copy and paste the code below into the class:

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javafx.scene.input.DataFormat;
import javafx.util.Pair;

public class DragContainer implements Serializable{
    
    /**
     * 
     */
    private static final long serialVersionUID = -1458406119115196098L;

    private final List <Pair<String, String> > mDataPairs = new ArrayList <Pair<String, String> > ();
    
    public static final DataFormat Binding = 
            new DataFormat("com.buddyware.treefrog.filesystem.view.FileSystemBinding");
    
    public static final DataFormat Node =
            new DataFormat("com.buddyware.treefrog.filesystem.view.FileSystemNode");
    
    public DragContainer () {
    }
    
    public void addData (String key, String value) {
        mDataPairs.add(new Pair<String, String>(key, value));        
    }
    
    public  T getValue (String key) {
 
        for (Pair<String, Object> data: mDataPairs) {
 
        if (data.getKey().equals(key))
            return (T) data.getValue();
 
         }
 
        return null;
    }
    
    public List <Pair<String, String> > getData () { return mDataPairs; }
}

The DragContainer class has two key aspects:

  • It inherits Serializable (required for custom Dragboard content)
  • It acts as a data model, storing data and providing get/set accessor methods

In this particular case, we’re using an ArrayList to store key/value pairs using a Pair <String, Object> object.  We provide a way to get the total list of data using getData() or accessing one particular value by passing it’s key to getValue().  Finally, we can add new key/value pairs using addData().

Now, let’s use our new DragContainer class as the content for the drag and drop operation.

First, find RootLayout.addDragDetection(). In the handle() method of the anonymous EventHandler class, replace the following two lines of code,

ClipboardContent content = new ClipboardContent();
content.putString(mDragOverIcon.getType().toString());

with

ClipboardContent content = new ClipboardContent();
DragContainer container = new DragContainer();

container.addData ("type", mDragOverIcon.getType().toString());
content.put(DragContainer.AddNode, container)

Then in RootLayout.buildDragHandlers(), find the call to setOnDragDone().  Replace it with the code below.

this.setOnDragDone (new EventHandler  (){
            
    @Override
    public void handle (DragEvent event) {
                
        right_pane.removeEventHandler(DragEvent.DRAG_OVER, mIconDragOverRightPane);
        right_pane.removeEventHandler(DragEvent.DRAG_DROPPED, mIconDragDropped);
        base_pane.removeEventHandler(DragEvent.DRAG_OVER, mIconDragOverRoot);
                                
        mDragOverIcon.setVisible(false);
                
        DragContainer container = 
            (DragContainer) event.getDragboard().getContent(DragContainer.AddNode);

        System.out.println (container.getData().toString());

        event.consume();
    }
});

Once you’ve added this code, run your application and drag and drop an icon.

You should see your console print out the same information as before, but in a key/value pair form.  That is, rather than see the color name by itself, you’ll see “[type=red]”.

If you only intend to use one type of drag-drop operation, and your data is simple, this custom container solution would be overkill.  However, with this small degree of added complexity we can save much effort where multiple kinds of data are passed for a variety of operations.  We will rely heavily on this data structure when implementing the remaining drag-and-drop features in our application.

Adding Nodes with Drag-And-Drop

Now that we’ve got our basic drag and drop data transfer mechanism in place, let’s use it to finish what we’ve begun and create a new floating node in the right-hand pane.

First, we need to add functionality to our DragContainer class to support this new function.  Basically, we’ll add a new DataFormat object called AddNode to the top of the class:

    public static final DataFormat AddNode = 
            new DataFormat("application.DragIcon.add");


Next, we need to modify two handlers to make this happen:  the right pane’s DragDropped handler and the RootLayout’s DragDone handler.

Go to RootLayout.buildDragHandlers() to update both handlers.

First, find the assignment to mIconDragDropped and update the code as follows:

mIconDragDropped = new EventHandler  () {

    @Override
    public void handle(DragEvent event) {

        DragContainer container =
            (DragContainer) event.getDragboard().getContent(DragContainer.AddNode);

        container.addData("scene_coords",
            new Point2D(event.getSceneX(), event.getSceneY()));

        ClipboardContent content = new ClipboardContent();
        content.put(DragContainer.AddNode, container);

        event.getDragboard().setContent(content);
        event.setDropCompleted(true);
    }
};

Notice what takes place:

  1. The DragContainer created in the DragDetected handler is retrieved from the event’s Dragboard.
  2. The container is updated with the scene coordinates of the mouse cursor.  These coordinates are stored in a Point2D object and keyed to the value “scene_coords”
  3. A new ClipboardContent object is created and used to store the DragContainer.  Notice that, once again, the container is keyed to the DataFormat object, DragContainer.AddNode.
  4. The event DragBoard’s content is replaced with the new ClipboardContent object containing the updated DragContainer data.

Finally, make sure the DragDone assignment in RootLayout.buildDragHandlers() looks like this:

this.setOnDragDone (new EventHandler  (){

    @Override
    public void handle (DragEvent event) {

    right_pane.removeEventHandler(DragEvent.DRAG_OVER, mIconDragOverRightPane);
    right_pane.removeEventHandler(DragEvent.DRAG_DROPPED, mIconDragDropped);
    base_pane.removeEventHandler(DragEvent.DRAG_OVER, mIconDragOverRoot);

    mDragOverIcon.setVisible(false);

    DragContainer container =
        (DragContainer) event.getDragboard().getContent(DragContainer.AddNode);

        if (container != null) {
            if (container.getValue("scene_coords") != null) {
                    
                DragIcon droppedIcon = new DragIcon();
                        
                droppedIcon.setType(DragIconType.valueOf(container.getValue("type")));
                right_pane.getChildren().add(droppedIcon);

        Point2D cursorPoint = container.getValue("scene_coords");

        droppedIcon.relocateToPoint(
                    new Point2D(cursorPoint.getX() - 32, cursorPoint.getY() - 32)
        );
            }
        }
    event.consume();
    }
});

We’re essentially setting DragDone up as a delegator.  That is, we’re using DragDone as a “clearinghouse” to vet whatever drag-and-drop operations occur in our application, verifying first that they are valid drag-and-drop operations, and second, that they completed successfully.  From there, the DragDone executes the appropriate code for that operation.

In the code above, we first try to retrieve the AddNode drag data by calling Dragboard.content() and passing it the DataFormat object named AddNode, which is defined in the DragContainer class.

If our DragContainer reference returns valid (i.e., the drag operation was an “AddNode” operation), then we check to see if the “scene_coords” value exists in the container.  If it returns null, we know that the user dropped the operation outside the limits of the right pane, because the “scene_coords” data is defined only in the right pane’s DragDropped event handler.

Having validated a successful drag-and-drop operation to add a new node, the remaining code does the following:

  • Create a new DragIcon to act as our new node
  • Set the icon’s type using the “type” value assigned to the DragContainer in the DragDetected event handler
  • Add the new DragIcon as a child of the right pane
  • Relocate the new DragIcon to center on the mouse cursor position

That’s it!  Run the application and try it out!

part_4

Next time, we’ll work on making the added nodes draggable.

Drag-and-Drop in JavaFX (Draggable Icons), Part 4

15 thoughts on “Drag-and-Drop in JavaFX (Draggable Icons), Part 4

  1. pacepiro says:

    Hi,
    First of all thank you very much for this guide. Your result is exactly the one we would like to achieve, but we are having some troubles.

    We are stuck at this point:
    ” You should see your console print out the same information as before, but in a key/value pair form. That is, rather than see the color name by itself, you’ll see “[type=red]”. ”

    The error presented is: java.lang.IllegalArgumentException: Could not serialize the data, referred to this line “mDragOverIcon.startDragAndDrop (TransferMode.ANY).setContent(content);” in handle of RootLayout.addDragDetection.
    We think that it should refer to the container variable of the DragContainer class that the compailer can’t read as serializable for some reason, but actually we don’t have a clue.

    Moreover, we are “debugging” this guide quite thoroughly, as soon as we reach the end of it we will try to give you our feedback!

    Like

    1. I would suggest downloading the code linked at the end of my post and testing that. If it works, then you’ve got the full code to review yours against.

      I’m sure the problem is in your drag detection event handler code (unless DragDropContainer doesn’t implement Serializable?). Comparing your code against what I’ve provided is your best bet.

      Anyway, thanks for taking the time to go through this. I’ve tried to make this as complete as possible, but since I’m busy with a bunch of other things, I’ve no doubt missed a thing or two, here…

      Like

      1. pacepiro says:

        Hi again,
        we managed to reach the end of your articles. To solve this particular issue, we had to make the classes used by DragContainer (Point2D and Pair) Serializable (for example we made another class called MyPoint2D which extends Point2D and at the same time implements Serializable).

        Almost everything works fine until now but we would be very happy to know the end of the story 🙂 Have you planned to write the last article “Linking nodes”? If not, can you please give us some hints to go on, at least?

        Thank you again for your great job!

        Like

      2. Hmmm. I’ll have to look into that Serializable issue a bit more.

        Just a few questions:

        1. Did you try the code I posted?
        2. Are you putting the data into a DragContainer object and adding the DragContainer to the ClipboardContent object?
        3. If so, does the DragContainer itself implement Serializable?
        4. Finally, what version of JavaFX / Java are you using?

        I know for a fact that I didn’t have to create a custom class for the different data types like you apparently have needed to. Doing a little research, however, I’ve found that this is how some have addressed this problem. Admittedly, it makes sense that all classes should be serializable, which is why I was surprised to see it work when only DragContainer was serializable…

        So far as the last part is concerned, I’m working on it. I was prepared to release the first post, but ran into a snag… A bit busy, but hopefully will start the last series yet this next week!

        Like

      3. pacepiro says:

        Actually we did everything as is described in the guide, the only difference (and we know it’s not irrelevant) is the version of Java. We thought that this specific problem is not dependent on the version. We are using Java 7, by the way.

        As we said, we found some things to be fixed in your code, and without this little fixes, it doesn’t work (but again, some of these things are due to the version).

        Thank you again and we’ll wait for the following posts 🙂

        PS: we hope to provide you our running version of the code, to help people with our same settings.

        Like

      4. I’m pretty certain the issue has to do with running an older version of JavaFX. I can’t guarantee compatibility with anything lower than JavaFX 8. I would have to assume it’s due to the difference in versions, as I’ve run the code without issue as I’ve posted it on the blog.

        Nevertheless, if you get it to work well, I’ll happily link it with the appropriate disclaimer.

        Like

  2. Metaltech duos says:

    i would like to thank you very much for this very precise guide , it was tremendous help
    i have a project where i have to integrate pig scripts in a DnD ui , produce something like talend
    but i don’t know how to make nodes return objects to other nodes or how to “run” the job at the end
    any help would be appreciable

    Like

    1. I’m not exactly sure what you mean, but if you’re looking for a way to pass a reference to an object as a result of a drag-and-drop operation, you can’t. You can pass strings (hint: keys) in the container that’s used in the drag-and-drop operation, then look up the object associated with the passed key in whatever object receives the drop operation, but that’s about it.

      Like

  3. Metaltech duos says:

    thanks a lot for the quick reply !
    your tutorial here showed me at least a glimpse of hope , and i intent to build on it .
    i m still a beginner and we have this university project where we need to make an graphic ETL and embed hadoop pig in it , basically the user opens a csv file( drags it into the container ) , drags and drop the operation he needs to make in the container and at the end pushes a “run” button and the pig job gets executed and the job result dumped into a data warehouse .
    any ideas /recommendations / tip would be very precious
    thank you !

    Like

    1. Sounds like the sort of stuff the tutorial covers. The simplest arrangement you can do is to create a view which acts as your container, then drag / drop icons from other views into it. Again, the drag / drop mechanism only passes string data, not object references, so you’ll have to find some way to pass keys that you can look up to get at the original objects when the drag / drop operation is complete.

      I can’t really advise you better than that. If you need to drag / drop from an external source (like a CSV file from a file explorer window), that’s possible as well, but it’s beyond the scope of what I’m doing here.

      Good luck!

      Like

  4. DREX says:

    I must be missing something, but in the RootLayout class in the onDragDone handle, the line Point2D cursorPoint = container.getValue(“scene_coords”); and container.getValue returns a string, not a point2D, and eclipse is throwing an error because Point2D != String. Have I missed something?

    also ‘AddNode’ doesnt exist in the DragContainer as you write in your code above, but ‘Node’ does, So I am guessing that is what was intended.

    Like

    1. DREX says:

      I created a work around for the Point problem by manipulating the String return value into two double values (x and y) and creating a new Point2D, and passing that into the relocate

      Like

  5. Ok. Sorry it took so long to get back to you. Good catch on the code problem. If you look at the final “NodeLink” code on the github repo, it works the way I intended. What was missing was the fact that I changed the getValue function to return it’s value cast to the data type of the variable it’s being assigned to (declared as public T getValue).

    Also, I forgot to mention that the DataFormat object, AddNode needed to be included at the top of the DragContainer class.

    I have made changes to this post to reflect that. It should make it run the way it’s supposed to – no need to use Object as the container value type or forcing the coordinates to return as a string.

    Thanks for catching that!

    Like

Leave a comment