Toni Epple works as a consultant for Eppleton (http://www.eppleton.de) in Munich, Germany. In his spare time he's an active member of the Open Source community as a community leader for JavaTools community (http://community.java.net/javatools/), moderator of the XING NetBeans User Group (http://www.xing.com/group-20148.82db20), founder of the NetBeans User Group Munich (http://tinyurl.com/5b8tuu), member of the NetBeans Dream Team (http://wiki.netbeans.org/NBDTCurrentMembers) and blogger (http://www.eppleton.de/blog). Toni is a DZone MVB and is not an employee of DZone and has posted 51 posts at DZone. You can read more from them at their website. View Full User Profile

Drag & Drop with the NetBeans Nodes API

02.13.2012
| 6620 views |
  • submit to reddit

With the Nodes API, the NetBeans Platform provides a presentation layer for your domain objects. You can easily separate UI-specific features like an internationalizable display name, actions, an icon, etc, for your domain object.

This is done via wrapping your domain objects with Nodes. Nodes are then displayed in specialized "explorer views", which are typically Swing components, but could be anything else, such as JavaFX components. Building your Node hierarchy is easy and it gives you a lot of features out of the box.

There are many tutorials out there showing you how you can create your own Node hierarchies, add actions to them, etc.

One thing that is not straightforward, though, is how to enable drag and drop functionality. A typical use case is that you have some kind of "Folder" Nodes that accept drops and some kind of "Leaf" Nodes that can be dragged from one folder to another.

In this article, you'll learn how to do that via an example.

Let's assume you've got a domain object like this:

public class Customer  {

    static int ids = 0;
    String name;
    int id;

    public Customer(String name) {
        this.name = name;
        id = ids++;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

Let's create a DataFlavor that our accepting Node can later check to test if it's OK to accept the drop:

public class CustomerFlavor extends DataFlavor{

    public static final DataFlavor CUSTOMER_FLAVOR = new CustomerFlavor();

    public CustomerFlavor() {
         super(Customer.class, "Customer");
    }

} 

Now we need some Nodes. So we first create a ChildFactory that can produce CustomerNodes:

import java.awt.datatransfer.Transferable;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.ChildFactory;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.util.Lookup;
import org.openide.util.datatransfer.ExTransferable;
import org.openide.util.lookup.Lookups;

class CustomerFactory extends ChildFactory {

    List customers;

    public CustomerFactory(List customerNames) {
        this.customers = customerNames;
    }

    public void addCustomer(Customer c) {
        customers.add(c);
        refresh(true);
    }

    private void removeChild(Customer c) {
        customers.remove(c);
        refresh(true);
    }

    @Override
    protected boolean createKeys(List list) {
        list.addAll(customers);
        return true;
    }

    @Override
    protected Node createNodeForKey(Customer name) {
        Node node = new CustomerNode(Children.LEAF, Lookups.fixed(name));
        return node;
    }

    public void reorder(int[] perm) {
        Customer[] reordered = new Customer[customers.size()];
        for (int i = 0; i < perm.length; i++) {
            int j = perm[i];

            Customer c = customers.get(i);
            reordered[j] = c;
        }
        customers.clear();
        customers.addAll(Arrays.asList(reordered));
        refresh(true);

    }

    private class CustomerNode extends AbstractNode
    {

        public CustomerNode(Children children, Lookup lookup) {
            super(children, lookup);
        }

        @Override
        public boolean canCut() {
            return true;
        }

        @Override
        public boolean canCopy() {
            return true;
        }

        @Override
        public String getDisplayName() {
            return getLookup().lookup(Customer.class).getName();
        }

        @Override
        public boolean canDestroy() {
            return true;
        }

        @Override
        public void destroy() throws IOException {
            removeChild(getLookup().lookup(Customer.class));
            refresh(true);
        }

        @Override
        public Transferable clipboardCut() throws IOException {
            Transferable deflt = super.clipboardCut();
            ExTransferable added = ExTransferable.create(deflt);
            added.put(new ExTransferable.Single(CustomerFlavor.CUSTOMER_FLAVOR) {
                protected Customer getData() {
                    return getLookup().lookup(Customer.class);
                }
            });
            return added;
        }
    }
}

The CustomerFactory manages an initial List of Customers. It provides additional methods for adding, removing and reordering Customers. Those will come in very handy later. Other than that it's very simple.

The CustomerNode overrides canCopy() and canCut() to return true, to indicate that we can remove the Node from its parent. When a user selects our Node, the cut Action will be invoked and AbstractNode.clipboardCut() will be called. By default, it creates a transferable supporting only one flavor, so we'll override it to add our own Flavor. We can use that Flavor later to get hold of the actual Customer Object.

When we Drop our Node in its new Folder, we want the original Node to be destroyed. So we need to return true from the canDestroy() method and our Node needs to know how to destroy itself.

This is where our removeChild() method comes in handy. Inside this method, the Customer is removed from the list and Childfactory.refresh(true) is called, telling our Children wrapper and the view to update and call Childfactory.createKeys() again.

That's all we need to do in order for our Nodes to be drag enabled.

Now let's have a look at our container Nodes:

import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Index;
import org.openide.nodes.Node;
import org.openide.nodes.NodeTransfer;
import org.openide.util.Exceptions;
import org.openide.util.datatransfer.PasteType;

/**
 *
 * @author eppleton
 */
public class DNDContainerNode extends AbstractNode {

    CustomerFactory customers;

    public DNDContainerNode(final CustomerFactory customers) {
        super(Children.create(customers, true));
        this.customers = customers;
        getCookieSet().add(new Index.Support() {

            @Override
            public Node[] getNodes() {
                return getChildren().getNodes();
            }

            @Override
            public int getNodesCount() {
                return getNodes().length;
            }

            @Override
            public void reorder(int[] perm) {
                customers.reorder(perm);
            }
        });
    }

    @Override
    public PasteType getDropType(final Transferable t, int arg1, int arg2) {

        if (t.isDataFlavorSupported(CustomerFlavor.CUSTOMER_FLAVOR)) {

            return new PasteType() {

                @Override
                public Transferable paste() throws IOException {
                    try {
                        customers.addCustomer((Customer) t.getTransferData(CustomerFlavor.CUSTOMER_FLAVOR));
                        final Node node = NodeTransfer.node(t, NodeTransfer.DND_MOVE + NodeTransfer.CLIPBOARD_CUT);
                        if (node != null) {
                            node.destroy();
                        }
                    } catch (UnsupportedFlavorException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                    return null;
                }
            };
        } else {
            return null;
        }
    }
}

The important part is the "getDropType" method. Here we can check for our DataFlavor. In case it's supported, we'll return a new PasteType. The NodeTransfer class is the central part will give us the Node that is dragged with no further effort. We'll only use it here, because we want to cut the Node from it's original. We could also use it's Lookup to check if it has a Customer instead of the DataFlavor to accept the drop. That would simplify the code a bit, but I slightly prefer to use the Flavor to check this and get hold of the Customer (even though it's not typesafe), because other Nodes not intended to be dragged and dropped might have a Customer Object too.

The other important part of this class is the Index.Support in the CookieSet. This allows us to drop the Node at a specific spot. Remove it, and you'll see the difference. The implementation delegates to CustomerFactory.reorder in order to do the job.

Now the only thing to do is to test our code in a new TopComponent. To the Constructor add this:

        setLayout(new BorderLayout());
        add(new BeanTreeView(), BorderLayout.CENTER);

        ArrayList names = new ArrayList();
        names.add(new Customer("Tom"));
        names.add(new Customer("Dick"));
        names.add(new Customer("Harry"));
        AbstractNode root1 = new DNDContainerNode(new CustomerFactory(names));
        root1.setDisplayName("D&D 1");

        ArrayList names2 = new ArrayList();
        names2.add(new Customer("Thomas"));
        names2.add(new Customer("Richard"));
        names2.add(new Customer("Harald"));
        AbstractNode root2 = new DNDContainerNode(new CustomerFactory(names2));
        root2.setDisplayName("D&D 2");

        AbstractNode root = new AbstractNode(new Children.Array());
        root.setDisplayName("Root");
        root.getChildren().add(new Node[]{root1, root2});
        em.setRootContext(root); 

That's it, you can now freely reorder your nodes.

 

Published at DZone with permission of Toni Epple, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)