Wednesday, June 3, 2015

Generic drag and drop in eclipse

The default drag and drop from SWT relies on the fact that source and target both agree on certain transfer types. The source adds the drop data, the target has to know how to deal with that data.

A drop target in eclipse therefore cannot deal with unknown objects by default. Eg you cannot drop your plugin local objects on a Navigator view. This article describes how you can drop your own objects into existing eclipse views and how to enrich your local drop target with generic drop support.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

Step 1: Simple drag support

Lets start with default SWT drag support. Create a new Plug-in Project named com.codeandme.draganddrop. Add a new view with a Text element to it. Name the text element txtInput. I expect you are familiar with that procedure.

Now we add TextTransfer support:
  int operations = DND.DROP_MOVE | DND.DROP_COPY;
  DragSource source = new DragSource(txtInput, operations);

  Transfer[] types = new Transfer[] { TextTransfer.getInstance() };
  source.setTransfer(types);

  source.addDragListener(new DragSourceListener() {
   @Override
   public void dragStart(DragSourceEvent event) {
    if (txtInput.getText().length() == 0)
     event.doit = false;
   }

   @Override
   public void dragSetData(DragSourceEvent event) {
    // for text drag to editors
    if (TextTransfer.getInstance().isSupportedType(event.dataType))
     event.data = txtInput.getSelectionText();
   }

   @Override
   public void dragFinished(DragSourceEvent event) {
   }
  });
This is default SWT dnd code, nothing special so far.

Step 1: Add drag support for generic objects

By default the action to be performed on a drop is implemented by the DropListener of the target. Fortunately eclipse provides a special transfer type that delegates the drop action to a dedicated class which we may provide in our plugin.

Switch to your plugin.xml and add a new Extension of type org.eclipse.ui.dropActions. Add an action element to it, set the id to com.codeandme.draganddrop.dropText and implement a class TextDropActionDelegate:
package com.codeandme.draganddrop;

import java.io.ByteArrayInputStream;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.part.IDropActionDelegate;

public class TextDropActionDelegate implements IDropActionDelegate {

 public static final String ID = "com.codeandme.draganddrop.dropText";

 @Override
 public boolean run(Object source, Object target) {
  if (source instanceof byte[]) {
   if (target instanceof IContainer) {
    IContainer parent = (IContainer) target;
    IFile file = parent.getFile(new Path("dropped text.txt"));
    if (!file.exists()) {
     try {
      file.create(new ByteArrayInputStream((byte[]) source), true, new NullProgressMonitor());
     } catch (CoreException e) {
      e.printStackTrace();
     }
    }

    return true;

   } else if (target instanceof Text)
    ((Text) target).setText(new String((byte[]) source));
  }

  return false;
 }
}
The run() method gets called when the drop action is to be performed. The target is automatically set to the object under the mouse when we perform the drop, eg a folder in a Navigator view. The provided code is a bit simplified, eg we do not try to adapt the target in case it is not an IContainer.

Now we need to add our drop delegate to our source object. Switch back to your view and modify the code:
  Transfer[] types = new Transfer[] { TextTransfer.getInstance(), PluginTransfer.getInstance() };
add a new transfer type: PluginTransfer.
   public void dragSetData(DragSourceEvent event) {
    // for text drag to editors
    if (TextTransfer.getInstance().isSupportedType(event.dataType))
     event.data = txtInput.getSelectionText();

    // for plugin transfer drags to navigator views
    if (PluginTransfer.getInstance().isSupportedType(event.dataType))
     event.data = new PluginTransferData(TextDropActionDelegate.ID, txtInput.getSelectionText().getBytes());
   }
When a plugin transfer is accepted by the target we need to set a PluginTransferData object that links to our action delegate and provides the source data.

Everything is in place, so give it a try: type and select some text in your view and drop it on a folder in the Project Explorer view.

Step 3: Add generic drop support to your own views

Generic drop support does not come entirely for free. If you want to add it to a JFace tree or table it is dead simple as that:

  int operations = DND.DROP_MOVE | DND.DROP_COPY;
  DropTarget target = new DropTarget(viewer.getControl(), operations);

  Transfer[] types = new Transfer[] { PluginTransfer.getInstance() };
  target.setTransfer(types);

  target.addDropListener(new PluginDropAdapter(viewer));
We may also reuse the PluginDropAdapter on simple Controls like text boxes. Therefore we need to overwrite some methods:
  target.addDropListener(new PluginDropAdapter(null) {
   @Override
   protected int determineLocation(DropTargetEvent event) {
    return LOCATION_ON;
   }

   @Override
   protected Object getCurrentTarget() {
    return txtTarget;
   }
  });
If you provide drop support in general it is advised to add the plugin transfer type to allow other plugins to use your drop support.