Eclipse Corner Article |
Summary
In this article, we discuss the drag and drop facilities provided by JFace and the Eclipse platform UI. After reading this, you will know how to add drag and drop support to your own Eclipse views, and how that support will interact with the standard views in the Eclipse platform. Along the way, we'll also discuss that keyboard relative of drag and drop: cut and paste. You'll learn that putting your own custom objects on the clipboard is easy once you've figured out the basics of drag and drop. This article is intended to be read as a companion to the SWT drag and drop article.
By John Arthorne, IBM OTI Labs
August 25, 2003
In keeping with the general philosophy of JFace, the JFace drag and drop adds a layer of functionality on top of the SWT drag and drop support. This layer allows the developer to deal directly with domain objects (such as resources, tasks, etc), without having to worry too much about the underlying window controls. Rather than concealing or replacing the drag and drop support in SWT, the JFace drag and drop support works as an extension to the same concepts found in SWT drag and drop.
org.eclipse.swt.dnd.Transfer
. These subclasses implement the marshaling
behavior that converts between objects and bytes, allowing drag and drop
transfers between applications. The following table summarizes the transfer types
provided by the basic Eclipse platform, along with the object they are capable of transferring:
Transfer class | Object it transfers |
org.eclipse.swt.dnd.FileTransfer |
java.lang.String[] (list of absolute file paths) |
org.eclipse.swt.dnd.RTFTransfer |
java.lang.String (may contain RTF formatting characters) |
org.eclipse.swt.dnd.TextTransfer |
java.lang.String |
org.eclipse.ui.part.MarkerTransfer |
org.eclipse.core.resources.IMarker[] |
org.eclipse.ui.part.ResourceTransfer |
org.eclipse.core.resources.IResource[] |
org.eclipse.ui.part.EditorInputTransfer |
org.eclipse.ui.part.EditorInputTransfer.EditorInputData[] |
org.eclipse.ui.part.PluginTransfer |
org.eclipse.ui.part.PluginTransferData |
The set of transfer types is open ended, because third party
tool writers can implement their own transfer types for their domain objects.
To implement your own transfer type, it is recommended that you subclass
org.eclipse.swt.dnd.ByteArrayTransfer
.
See the SWT drag and drop article for more information
on defining your own transfer types.
An important point about transfer types is that they don't necessarily need to store
the entire object as serialized bytes. In most cases it is simpler and more
efficient to just store enough information about where the object is found. For
example, FileTransfer
simply encodes a string which represents the
absolute path of the file in the file system. It does not store the entire file in the
transfer object.
Many of the basic views you see in Eclipse already support various transfer types. It is important to understand what transfer types are supported by each view, because this dictates how the drag and drop support in your view will interact with other basic views found in the Eclipse platform UI.
The Navigator view supports dragging and dropping files (FileTransfer
),
and resources (ResourceTransfer
). For example, you can drag a file
from the Navigator view into Windows Explorer or the Windows Desktop. Similarly,
you can import resources into Eclipse simply by dragging them from Windows into
the Navigator view of your workbench. You can also drag files between two instances
of Eclipse, or drag within a single Navigator to copy and move files within
your workspace. If your view supports either FileTransfer
or
ResourceTransfer
, then users will be able to transfer resources between
your view and the Navigator view.
The Tasks and Bookmarks views support dragging of markers (MarkerTransfer
).
Dragging a selection of tasks from the Tasks view into an application such as MS
Word will generate a textual marker report (TextTransfer
). You can also drag markers
out of the Tasks and Bookmarks views into other parts of the workbench, such as
the editor area. Dragging a marker to the editor area will open the associated
resource in the editor and jump to that marker location in the editor.
Finally, the editor area supports dropping of editor inputs (EditorInputTransfer
),
resources, or markers. Dragging these objects to the editor will cause it to locate and
open an appropriate editor for the given resource, editor input or marker. In the case
of markers, it will also jump to that marker location in the editor.
For the remainder of this article, we'll make use of a running example. This example
is an Eclipse plug-in for manipulating a simple object model of gadgets. Gadgets
are simply named objects that can be formed into trees. Each gadget knows its parent
and its children. The example includes two views, one containing a table viewer, and
the other containing a tree viewer. Drag and drop can be used to copy or move
gadgets between these views, and to rearrange the order or hierarchy of gadgets
within a view. There is a GadgetTransfer
class for encoding an array
of gadgets to and from a byte array. Complete source code for the example is found
here.
org.eclipse.jface.viewers.StructuredViewer
using the addDragSupport(int, Transfer[], DragSourceListener)
method. From our gadget example, here is the code for associating a drag listener
with a tree viewer:
TreeViewer gadgetViewer = new TreeViewer(...); int ops = DND.DROP_COPY | DND.DROP_MOVE; Transfer[] transfers = new Transfer[] { GadgetTransfer.getInstance()}; viewer.addDragSupport(ops, transfers, new GadgetDragListener(viewer));
GadgetDragListener
is an implementation of the SWT interface
org.eclipse.swt.dnd.DragSourceListener
. There is nothing specific
to JFace about the implementation of DragSourceListener
, so you
can learn more about its implementation in the SWT drag and drop article.
Drop support can be added to viewers using the StructuredViewer.addDropSupport(int,
Transfer[], DropTargetListener)
method. This allows your viewer to
be the target of a drop operation. The code for adding drop support is almost the
same as for adding drag support:
TreeViewer gadgetViewer = new TreeViewer(...);
int ops = DND.DROP_COPY | DND.DROP_MOVE;
Transfer[] transfers = new Transfer[] { GadgetTransfer.getInstance()};
viewer.addDropSupport(ops, transfers, new GadgetTreeDropAdapter(viewer));
JFace provides a standard implementation of DropTargetListener
called org.eclipse.jface.viewers.ViewerDropAdapter
.
This adapter makes it easy to add drop support for simple cases. If you have more
complex requirements, you can always override the SWT DropTargetListener
interface directly for ultimate flexibility.
When sub-classing ViewerDropAdapter
, simply implement its two
abstract methods: validateDrop(Object target, int operation, TransferData transferType)
,
and performDrop(Object data)
.
validateDrop
is called whenever the user moves over a new item
in your viewer, or changes the drop type with one of the modifier keys.
The method provides the current drop target, operation, and transfer type.
The return value of this method indicates whether a drop at the current location is
valid or not. A return value of false will change the drag icon to indicate
to the user that it is illegal to drop what they are dragging at the current
location. In our gadget example, it is always legal to drop a gadget, so the
validateDrop
implementation simply looks like this:
public boolean validateDrop(Object target, int op, TransferData type) {
return GadgetTransfer.getInstance().isSupportedType(type);
}
This code just makes sure that the user is indeed dropping a gadget, and not some other object such as a resource or marker. If you have more complex validation requirements based on the target object, you can do that here. For example, in a file navigator, you may want to allow dropping on top of directories, but not on top of files.
performDrop
is called when the user lets go of the mouse button,
indicating that they want the drop to occur. Your implementation should
accordingly perform the expected behavior for that drop. Context for the
drop is provided by the methods getCurrentTarget
, getCurrentOperation
,
and getCurrentLocation
on ViewerDropAdapter
. Most
importantly, at the time when performDrop
is called, getCurrentTarget
will provide the object in your viewer that is currently under the mouse. Here is the
performDrop
method from our gadget example:
public boolean performDrop(Object data) {
1 Gadget target = (Gadget)getCurrentTarget();
2 if (target == null)
3 target = (Gadget)getViewer().getInput();
4 Gadget[] toDrop = (Gadget[])data;
5 TreeViewer viewer = (TreeViewer)getViewer();
6 //cannot drop a gadget onto itself or a child
7 for (int i = 0; i < toDrop.length; i++)
8 if (toDrop[i].equals(target) || target.hasParent(toDrop[i]))
9 return false;
10 for (int i = 0; i < toDrop.length; i++) {
11 toDrop[i].setParent(target);
12 viewer.add(target, toDrop[i]);
13 viewer.reveal(toDrop[i]);
14 }
15 return true;
}
In lines 1-3, it is determining what the target gadget is. The target is the item that
is currently under the mouse when the drop occurs. If there is no item under the mouse, it
takes the viewer's input element as the target. On lines 7-9, it is making sure
that the user is not dropping an item onto itself, or onto a child of itself. You may
be wondering why this validation was not done in the validateDrop
method. In SWT, the transfer of data from the source to the target is done lazily
when the drop is initiated. So, as the user is dragging, the destination has no
way of finding out what source object is being dragged until the drop is performed.
Returning false
from the performDrop
method will
cancel the drop. On lines 10-13, it is performing the actual drop. This involves updating
the gadget with its new parent (line 11), and then adding and revealing the new item
in the viewer (lines 12-13). Finally, the method returns true on line 15 to indicate
that the drop was successful.
ViewerDropAdapter
has two more interesting methods for controlling
drag feedback effects. The method setFeedbackEnabled
is used to turn
insertion feedback on or off. If insertion feedback is on, the cursor will change when
the mouse is hovering between two items to indicate where the insertion will occur.
Using this effect, you can allow the user to sort items in a tree using drag and drop.
In your drop adapter, use the method getCurrentLocation
to determine
if the cursor is currently directly on, before, or after the current target object. Here
is how the gadget drop example can be updated to make use of this location
information:
public boolean performDrop(Object data) {
//set the target gadget according to current cursor location
Gadget target = (Gadget)getCurrentTarget();
if (target != null) {
int loc = getCurrentLocation();
if (loc == LOCATION_BEFORE || loc == LOCATION_AFTER)
target = target.getParent();
}
if (target == null)
target = (Gadget)getViewer().getInput();
Gadget[] toDrop = (Gadget[])data;
TreeViewer viewer = (TreeViewer)getViewer();
// ... remainder of method is the same as previous example ...
In this snippet, it looks at the cursor location to see if it is before or after an item in the tree. When a gadget is dropped between other gadgets, it sets the parent to be the parent of the neighboring gadget. Said another way, the dropped gadget will become a sibling of the gadget it is dropped next to.
The method ViewerDropAdapter.setScrollExpandEnabled
is used to
turn scroll and expansion effects on or off. When this is turned on, hovering
near the bottom of a tree or table will cause the widget to automatically scroll in
that direction. Hovering over a collapsed tree item for a sufficient amount of time
will cause the item to be expanded. In most cases you should leave these effects
on, otherwise users will not able to use drag and drop effectively when your tree
or table contains many items.
Due to the UI layering imposed by the plug-in mechanism, viewers are often
not aware of the content and nature of other viewers. This can make drag
and drop operations between plug-ins difficult. For example, our gadget
plug-in may want to allow the user to drop gadget objects into the Navigator
view. Since the Navigator view doesn't know anything about gadget objects
(the Navigator only displays org.eclipse.core.resources.IResource
objects), it would not be able to support this. Similarly, another plug-in may want
to drop some other kind of objects into the views from the gadget example.
To address this problem, a plug-in drop support mechanism
is provided by the workbench. This mechanism essentially delegates the
drop behavior back to the originator of the drag operation. In the gadget example,
we use this extension point to drop gadgets into the Navigator view, and create
files containing descriptions of the gadgets that were dropped. Here are the
steps required to add drag and drop behavior using this mechanism:
Step 1) In your plugin.xml, define an extension on the "org.eclipse.ui.dropActions" extension point. Here is an example XML declaration:
<extension
id="gadgetDrop"
name="Gadget Resource Drop"
point="org.eclipse.ui.dropActions">
<action
class="org.eclipse.ui.examples.gdt.dnd.GadgetPluginDropAdapter"
id="org.eclipse.ui.examples.gdt.gadgetDrop">
</action>
</extension>
Step 2) Implement the code that will perform the drop. This work is done by
the class defined in the extension markup above, which must implement
org.eclipse.ui.part.IDropActionDelegate
.
This interface defines a single run() method that gets called when the
drop occurs. The run method is supplied with the object being dragged, as well as the
object under the cursor when the drop occurs. Here is an example implementation of
the drop delegate from the gadget example:
public boolean run(Object source, Object target) {
1 if (target instanceof IContainer) {
2 GadgetTransfer transfer = GadgetTransfer.getInstance();
3 Gadget[] gadgets = transfer.fromByteArray((byte[])source);
4 IContainer parent = (IContainer)target;
5 for (int i = 0; i < gadgets.length; i++) {
6 writeGadgetFile(parent, gadgets[i]);
7 }
8 return true;
9 }
10 //drop was not successful so return false
11 return false;
}
On line 1, it ensures that the gadget is being dropped on some kind of container.
In practice, your drop delegate may support being dropped on several different
types of objects, with different behavior for each type. On lines 2-3, it makes use
of a convenience method to extract the transferred gadgets from the byte array in the
transfer data. Since the plug-in transfer data contains a byte array, this is the exact
same code that typically exists in your custom Transfer
subclass.
It then iterates over the transferred gadgets, and creates a file in the target container
for that gadget. Finally, it returns true if the drop was successful (line 8), or false if the drop
occurred on a target object that was not supported (line 11).
Step 3) In the viewer that will be the source of the drag and drop,
add drag support using the StructuredViewer.addDragSupport()
method
described earlier. In the array of supported transfer types, include the
singleton instance of org.eclipse.ui.part.PluginTransfer
. In your
implementation of DragSourceListener
, the dragSetData
method must set the data to be an instance of org.eclipse.ui.part.PluginTransferData
.
This object consists of the id of your drop action,
along with the data being transferred. Here is the code in the drag listener from the
gadget example:
public void dragSetData(DragSourceEvent event) {
1 IStructuredSelection selection = (IStructuredSelection)viewer.getSelection();
2 Gadget[] gadgets = (Gadget[])selection.toList().toArray(new Gadget[selection.size()]);
3 if (GadgetTransfer.getInstance().isSupportedType(event.dataType)) {
4 event.data = gadgets;
5 } else if (PluginTransfer.getInstance().isSupportedType(event.dataType)) {
6 byte[] data = GadgetTransfer.getInstance().toByteArray(gadgets);
7 event.data = new PluginTransferData("org.eclipse.ui.examples.gdt.gadgetDrop", data);
8 }
9 }
}
Lines 1-2 are the familiar code for creating an array of gadgets from the viewer's
selection. Line 4 handles the old case of a gadget transfer by simply setting the
event data to be the correct type for GadgetTransfer
. Lines 6-7
are the interesting new code for handling a plug-in drag event. First, it uses a convenience
method on the GadgetTransfer
class for converting the array of gadgets
into a byte array. Next, it creates a PluginTransferData
object, passing
in the id of the plug-in drop action that was declared in the plugin.xml in step 1), along
with the serialized gadget array.
Step 4) In the viewer that will receive the drop, the drop listener
for that viewer must subclass org.eclipse.ui.part.PluginDropAdapter
,
which in turn subclasses ViewerDropAdapter
as described earlier.
Be sure to invoke the super methods for validateDrop and performDrop in
cases where your adapter does not understand the transfer type. Following
from our earlier example, the viewer’s declaration would look
like this:
TreeViewer gadgetViewer = new TreeViewer(...);
int ops = DND.DROP_COPY | DND.DROP_MOVE;
Transfer[] transfers = new Transfer[] {
GadgetTransfer.getInstance(), PluginTransfer.getInstance()};
viewer.addDropSupport(ops, transfers, new GadgetTreeDropAdapter(viewer));
The basic Workbench views such as the Navigator view
already have this support added. It is recommended that anyone defining
their own views should add the plug-in support, in anticipation of future
third party plug-ins wanting to drop content into their views. For more information
about this advanced drag and drop mechanism, refer to the documentation
for the org.eclipse.ui.dropActions
extension point.
Cut and paste can be thought of as the keyboard equivalent of drag and drop. Once you've
mastered drag and drop support, you'll find that cut and paste is a snap. Once
again, the gadgets example provides a complete implementation of cut and paste
support. Here is code for adding cut and paste support from within an Eclipse view
(this code goes in the view's createPartControl
method):
public void createPartControl(Composite parent) {
viewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
//... initialize viewer's content and label providers ...
clipboard = new Clipboard(getSite().getShell().getDisplay());
IActionBars bars = getViewSite().getActionBars();
bars.setGlobalActionHandler(
IWorkbenchActionConstants.CUT,
new CutGadgetAction(viewer, clipboard));
bars.setGlobalActionHandler(
IWorkbenchActionConstants.COPY,
new CopyGadgetAction(viewer, clipboard));
bars.setGlobalActionHandler(
IWorkbenchActionConstants.PASTE,
new PasteTreeGadgetAction(viewer, clipboard));
}
This code simply creates a new SWT clipboard, and then defines global actions
for cut, copy, and paste using that clipboard. The IActionBars
interface is used for hooking into global actions. Note: SWT clipboard
objects are operating system resources that must be disposed when no longer
needed. In the gadget example, we dispose()
the clipboard
when the view is disposed. Disposing of an SWT clipboard instance does not remove
the data from the operating system's clipboard.
The actions that are provided as global action handlers should be subclasses of the
org.eclipse.jface.action.Action
class. The code for these actions is
similar to the drag and drop code, except that they use the clipboard as the transfer
mechanism, rather than the drag and drop event handlers. Here is the code for the
run method of the cut action:
public void run() {
1 IStructuredSelection selection = (IStructuredSelection)viewer.getSelection();
2 Gadget[] gadgets = (Gadget[])selection.toList().toArray(new Gadget[selection.size()]);
3 clipboard.setContents(
4 new Object[] { gadgets },
5 new Transfer[] { GadgetTransfer.getInstance()});
6 for (int i = 0; i < gadgets.length; i++) {
7 gadgets[i].setParent(null);
8 }
9 viewer.refresh();
}
On lines 3-5, the gadgets are placed on the clipboard, along with the
Transfer
object that will be used for serializing them. Lines 6-8 remove
the gadgets from the source view (since they are being cut, not copied), and line 9
refreshes the view to update it with the new contents. The copy action is almost
identical, except lines 6-9 are removed, since we don't want to remove the gadgets
from the source view on copy. The paste action for pasting into a tree looks like this:
public void run() {
1 IStructuredSelection sel = (IStructuredSelection)viewer.getSelection();
2 Gadget parent = (Gadget)sel.getFirstElement();
3 if (parent == null)
4 parent = (Gadget)viewer.getInput();
5 Gadget[] gadgets = (Gadget[])clipboard.getContents(GadgetTransfer.getInstance());
6 if (gadgets == null)
7 return;
8 //cannot drop a gadget onto itself or a child
9 for (int i = 0; i < gadgets.length; i++)
10 if (gadgets[i].equals(parent) || parent.hasParent(gadgets[i]))
11 return;
12 for (int i = 0; i < gadgets.length; i++) {
13 gadgets[i].setParent(parent);
14 }
15 viewer.refresh();
}
Lines 1-4 compute the parent gadget for the gadgets that are about to be pasted. If there is a gadget currently selected, it will become the new parent, otherwise the root gadget is used. Line 5 is the crucial step that takes the gadget objects off the clipboard. You should always check that the returned value is not null, since there may not be an object of the requested type on the clipboard. Lines 9-15 are the familiar code that we had in the drop event handler, first ensuring that we're not dropping a gadget onto itself or a child of itself, and then setting the parent elements for the dropped gadgets.
In your implementation, you will probably want to factor out the paste and drop
code into a common place, since they both do the same thing. Likewise, the code for
the cut action is similar to the code in dragFinished
in the drag action
handler. For clarity, the gadget example duplicates the code in both places, but you'll
make your code easier to maintain if you keep it all in one place.
You now have all the information you need for adding drag/drop and cut/paste support in your own JFace viewers. You should also know all about the standard transfer types supplied by the platform, and what transfers are supported by the standard views in the platform.
For more information, browse through the complete source from the
gadgets example. The UI readme
example, available from the eclipse.org
downloads page,
also implements some drag and drop support. For more in depth examples, you
can look at the drag and drop support implemented within the UI plug-in itself. For
example, see org.eclipse.ui.views.navigator.ResourceNavigator
to
see how the Navigator view implements its drag and drop behavior. The Navigator
uses two helper classes, NavigatorDragAdapter
and
NavigatorDropAdapter
, which are also instructive to look at.
Thanks to Knut Radloff from the Eclipse Platform UI Team and Jim des Rivières at OTI Labs for proof reading and providing feedback for this article.
Java and all Java-based trademarks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States, other countries, or both.