Home >
This is the first in a series of several articles that explore some of the new features in Apache Pivot 1.1. Pivot is a Java-based RIA toolkit that is currently undergoing incubation at the Apache Software Foundation. This article discusses Pivot's support for drag and drop.
One of the biggest pain points in web application development is file upload. HTML's file <input> tag is functional but awkward, and it doesn't work well for more than one or two files.
Most users are comfortable using the desktop drag and drop metaphor to manage files. It is an efficient and intuitive way to interact with the local file system. Pivot 1.1 brings this capability to the web.
The Drag and Drop API
Pivot 1.1 introduces a new API for drag and drop operations. This API supports drag and drop within an application as well as with the native OS. The API relies primarily on the following interfaces and classes, defined in the pivot.wtk package:
DragSource - interface representing the source of a drag/drop operation
DropTarget - interface representing the destination of a drag/drop operation
Manifest - abstract base class for drag content
LocalManifest - concrete manifest representing local content (from the local JVM)
RemoteManifest - concrete manifest representing remote content (from a remote application)
This article walks through the implemtation of a simulated file upload tool that provides a drop target for files dragged in from the native file system. As a result, it only demonstrates the use of the DropTarget interface and Manifest class. DragSource and LocalManifest may be discussed in a future article.
The Sample Application
The sample application is designed as an introduction to drag and drop in Pivot. It highlights the steps that a typical developer would need to perform in order to integrate drag and drop into a Pivot application.

A screen shot of the sample application is shown above, and a runnable example is available in the Demos section of the Pivot Wiki. The application does the following:
Loads the WTKX source for the user interface, which contains a TableView for displaying the files and a button for simulating an upload operation.
Attaches a DropTarget to the table view. This is what allows the Pivot runtime environment to recognize the table view as a target for drop operations.
Attaches a list listener to the table view's data so the "Upload" button can be enabled/disabled as appropriate.
Attaches a keyboard listener to the table view so the user can delete entries from the list.
Attaches a button press listener to the "Upload" button to simulate an upload.
Each of these steps is discussed in more detail below. The full source code for the application is available here.
The WTKX Source
The WTKX source for the application is as follows:
<TablePane xmlns:wtkx="http://incubator.apache.org/pivot/wtkx/1.1"
xmlns:dnd="pivot.demos.dnd"
xmlns="pivot.wtk">
<columns>
<TablePane.Column width="1*"/>
</columns>
<rows>
<TablePane.Row height="1*">
<Border styles="{color:10, padding:0}">
<content>
<ScrollPane horizontalScrollBarPolicy="fillToCapacity"
verticalScrollBarPolicy="fillToCapacity">
<view>
<TableView wtkx:id="fileTableView" selectMode="multi"
styles="{showHorizontalGridLines:false}">
<columns>
<TableView.Column name="name" width="180" headerData="File">
<cellRenderer>
<dnd:FileCellRenderer/>
</cellRenderer>
</TableView.Column>
<TableView.Column name="size" width="60" headerData="Size">
<cellRenderer>
<dnd:FileCellRenderer/>
</cellRenderer>
</TableView.Column>
<TableView.Column name="lastModified" width="120" headerData="Modified">
<cellRenderer>
<dnd:FileCellRenderer/>
</cellRenderer>
</TableView.Column>
</columns>
</TableView>
</view>
<columnHeader>
<TableViewHeader tableView="$fileTableView" styles="{headersPressable:false}"/>
</columnHeader>
</ScrollPane>
</content>
</Border>
</TablePane.Row>
<TablePane.Row height="-1">
<FlowPane styles="{padding:6, horizontalAlignment:'right', verticalAlignment:'center'}">
<PushButton wtkx:id="uploadButton" buttonData="Upload"
enabled="false" styles="{preferredAspectRatio:3}"/>
</FlowPane>
</TablePane.Row>
</rows>
</TablePane>
The root element is a table pane containing one column and two rows. The first row contains the file table view, and the second the upload button. The table view is placed in a scroll pane to support long file lists.
Note that the table view uses a custom cell renderer to represent information about the files. This allows the application to use instances of java.io.File as row data for the table view. It is defined as follows:
package pivot.demos.dnd;
import java.awt.Color;
import java.awt.Font;
import java.io.File;
import java.text.DateFormat;
import java.util.Date;
import pivot.wtk.HorizontalAlignment;
import pivot.wtk.Insets;
import pivot.wtk.Label;
import pivot.wtk.TableView;
import pivot.wtk.VerticalAlignment;
public class FileCellRenderer extends Label implements TableView.CellRenderer {
public static final String NAME_KEY = "name";
public static final String SIZE_KEY = "size";
public static final String LAST_MODIFIED_KEY = "lastModified";
public FileCellRenderer() {
getStyles().put("verticalAlignment", VerticalAlignment.CENTER);
getStyles().put("padding", new Insets(2));
}
@SuppressWarnings("unchecked")
public void render(Object value, TableView tableView, TableView.Column column,
boolean rowSelected, boolean rowHighlighted, boolean rowDisabled) {
File file = (File)value;
String columnName = column.getName();
String text;
if (columnName.equals(NAME_KEY)) {
text = file.getName();
getStyles().put("horizontalAlignment", HorizontalAlignment.LEFT);
} else if (columnName.equals(SIZE_KEY)) {
long length = file.length();
text = Long.toString(length);
getStyles().put("horizontalAlignment", HorizontalAlignment.RIGHT);
} else if (columnName.equals(LAST_MODIFIED_KEY)) {
long lastModified = file.lastModified();
Date lastModifiedDate = new Date(lastModified);
DateFormat dateFormat = DateFormat.getDateInstance();
text = dateFormat.format(lastModifiedDate);
getStyles().put("horizontalAlignment", HorizontalAlignment.RIGHT);
} else {
text = null;
}
setText(text);
// Update the styles
Object font = tableView.getStyles().get("font");
if (font instanceof Font) {
getStyles().put("font", font);
}
Object color = null;
if (tableView.isEnabled() && !rowDisabled) {
if (rowSelected) {
if (tableView.isFocused()) {
color = tableView.getStyles().get("selectionColor");
} else {
color = tableView.getStyles().get("inactiveSelectionColor");
}
} else {
color = tableView.getStyles().get("color");
}
} else {
color = tableView.getStyles().get("disabledColor");
}
if (color instanceof Color) {
getStyles().put("color", color);
}
}
}
The application uses an instance of pivot.io.FileList as the actual table data. This class is an implementation of the pivot.collections.List class with the following properties that make it useful to the sample application:
It is one of the data types that is natively supported by Pivot's drag/drop API. This is discussed in more detail below.
Unlike a generic list (such as pivot.collections.ArrayList), entries in a FileList are unique; i.e. a single file can't be added to the list more than once.
File list contents are automatically sorted alphabetically by path.
The DropTarget Interface
After loading the UI from the WTKX file, the application attaches a drop target to the table view. This is what allows the application to actually respond to a drag/drop operation: as the mouse is moved during a drag, the framework walks up the component hierarchy looking for instances of attached DropTargets. When it finds one, it uses the methods defined by the interface to determine how to handle the prospective drop:
public interface DropTarget {
public DropAction dragEnter(Component component, Manifest dragContent,
int supportedDropActions, DropAction userDropAction);
public void dragExit(Component component);
public DropAction dragMove(Component component, Manifest dragContent,
int supportedDropActions, int x, int y, DropAction userDropAction);
public DropAction userDropActionChange(Component component,
Manifest dragContent, int supportedDropActions, int x, int y,
DropAction userDropAction);
public DropAction drop(Component component, Manifest dragContent,
int supportedDropActions, int x, int y, DropAction userDropAction);
}
The following describes the purpose and behavior of each method:
dragEnter() is called when the mouse first enters a drop target during a drag operation. It returns an instance of pivot.wtk.DropAction representing the drop action that would result if the user dropped the item at this location, or null if the target cannot accept the drop. DropAction is an enum containing the values COPY, MOVE, and LINK.
dragExit() is called when the mouse leaves a drop target during a drag operation. It does not return a value.
dragMove() is called when the mouse is moved while positioned over a drop target during a drag operation. Like dragEnter(), dragMove() also returns the drop action that would result if the user dropped the item at this location. This allows the drop target to customize the drop behavior based on the mouse's location over the component.
userDropActionChange() is called when the user drop action changes while the mouse is positioned over a drop target during a drag operation. This method also returns the drop action that would result if the user dropped the item at this location, allowing the drop target to respond to changes in user drop action. For example, the user may press the CTRL key to change the drop action from "move" to "copy"; if the drop target does not support "copy", it can reject the drop.
drop() is called to drop the drag content. This method returns the drop action used to perform the drop, or null if the target rejected the drop.
The Manifest
Most of DropTarget's methods take a Manifest as an argument. The manifest contains the actual data for the drag/drop operation. It is an instance of pivot.wtk.Manifest, which is defined as follows:
public interface Manifest {
public String getText() throws IOException;
public boolean containsText();
public Image getImage() throws IOException;
public boolean containsImage();
public FileList getFileList() throws IOException;
public boolean containsFileList();
public URL getURL() throws IOException;
public boolean containsURL();
public Object getValue(String key) throws IOException;
public boolean containsValue(String key);
}
A Pivot manifest may contain the following types of data, which correspond directly to the methods shown in the interface:
Plain text
Image data
A file list
A URL
Application-specific key/value pairs
The sample application supports drag content containing the file list data type.
The Sample Drop Target
The drop target for the sample application is implemented as follows:
fileTableView.setDropTarget(new DropTarget() {
public DropAction dragEnter(Component component, Manifest dragContent,
int supportedDropActions, DropAction userDropAction) {
DropAction dropAction = null;
if (dragContent.containsFileList()
&& DropAction.COPY.isSelected(supportedDropActions)) {
dropAction = DropAction.COPY;
}
return dropAction;
}
public void dragExit(Component component) {
}
public DropAction dragMove(Component component, Manifest dragContent,
int supportedDropActions, int x, int y, DropAction userDropAction) {
return (dragContent.containsFileList() ? DropAction.COPY : null);
}
public DropAction userDropActionChange(Component component, Manifest dragContent,
int supportedDropActions, int x, int y, DropAction userDropAction) {
return (dragContent.containsFileList() ? DropAction.COPY : null);
}
public DropAction drop(Component component, Manifest dragContent,
int supportedDropActions, int x, int y, DropAction userDropAction) {
DropAction dropAction = null;
if (dragContent.containsFileList()) {
try {
FileList tableData = (FileList)fileTableView.getTableData();
FileList fileList = dragContent.getFileList();
for (File file : fileList) {
if (file instanceof Folder) {
tableData.add((Folder)file);
} else {
tableData.add(file);
}
}
dropAction = DropAction.COPY;
} catch(IOException exception) {
System.err.println(exception);
}
}
dragExit(component);
return dropAction;
}
});
In the query methods, the drop target returns a drop action of COPY if the drag content contains a file list and the COPY drop action is supported by the drag source. In the drop() method, if the drag content contains a file list (which it does when files are dragged in from the native file system), the contents of the file list are added to the table data. If a file in the list refers to a folder, it's contents are recursively added to the list.
The Event Listeners
The demo defines a couple of event listeners simply to make the application behave more realistically. It attaches a list listener to the table data so the Upload button can be enabled/disabled as appropriate:
fileList.getListListeners().add(new ListListener<File>() {
public void itemInserted(List<File> list, int index) {
uploadButton.setEnabled(list.getLength() > 0);
}
public void itemsRemoved(List<File> list, int index, Sequence<File> files) {
uploadButton.setEnabled(list.getLength() > 0);
if (fileTableView.isFocused()
&& index < list.getLength()) {
fileTableView.setSelectedIndex(index);
}
}
public void itemUpdated(List<File> list, int index, File previousFile) {
// No-op
}
public void comparatorChanged(List<File> fileList, Comparator<File> previousComparator) {
// No-op
}
});
It also attaches a key handler so users can delete entries from the list:
fileTableView.getComponentKeyListeners().add(new ComponentKeyListener() {
public boolean keyTyped(Component component, char character) {
return false;
}
public boolean keyPressed(Component component, int keyCode, Keyboard.KeyLocation keyLocation) {
if (keyCode == Keyboard.KeyCode.DELETE
|| keyCode == Keyboard.KeyCode.BACKSPACE) {
Sequence<Span> selectedRanges = fileTableView.getSelectedRanges();
for (int i = selectedRanges.getLength() - 1; i >= 0; i--) {
Span range = selectedRanges.get(i);
int index = range.getStart();
int count = range.getEnd() - index + 1;
fileList.remove(index, count);
}
}
return false;
}
public boolean keyReleased(Component component, int keyCode, Keyboard.KeyLocation keyLocation) {
return false;
}
});
Finally, it attaches a button press listener to the "Upload" button to simulate an upload. The event handler for this button simply displays a prompt containing the text "Pretending to upload...":
uploadButton.getButtonPressListeners().add(new ButtonPressListener() {
public void buttonPressed(Button button) {
Prompt.prompt(MessageType.INFO, "Pretending to upload...", window);
}
});
A real file upload application would most likely start a background task at this point to send the files to the server.
The Clipboard
Though it isn't discussed in this article, it is worth noting that Pivot 1.1 also includes a related new API for interacting with the system clipboard. The pivot.wtk.Clipboard class also uses the Manifest classes to represent data and provides the following methods for clipboard access:
public static Manifest getContent()
public static void setContent(LocalManifest content)
getContent() retrieves the current clipboard contents, and setContent() allows a caller to place new content on the clipboard. If the application is trusted, these methods interact directly with the system clipboard. Otherwise, a local private clipboard is used (for intra-application clipboard access only).
Conclusion
Drag and drop is a common metaphor in user interface design, and is a feature users have come to expect from desktop applications. The new drag/drop and clipboard features of Pivot 1.1 now allow developers to bring the same level of interaction to web applications built with Pivot.
For more information, visit http://incubator.apache.org/pivot.




Facebook Application Development
Comments
Leave a comment