Skip to content
Theme

Drag and drop, and the clipboard

A drag and a copy are the same thing seen twice: something leaves a widget in several forms at once, and whoever takes it picks the form it understands. Limn has one type for that, a Transfer, and it is what a drag carries and what the clipboard holds. This page starts with the widgets that drag without any of it, since most screens need nothing else.

Turn dragging on and say how a move is made:

static ListView<Field> reorderable(List<Field> fields) {
ListView<Field> list = new ListView<>(
field -> new Padding(Insets.symmetric(7, 10), new Label(field.name())));
list.setItems(fields);
list.setDragEnabled(true);
// The list shows the move; the handler makes it, in the application's own list. The
// index is counted among the rows that stay, so removing first is always right.
list.onReorder((rows, index) -> {
fields.removeAll(rows);
fields.addAll(index, rows);
});
return list;
}

That is the whole of it. The user presses a row and drags: the row lifts out of the list and follows the pointer, the rows around open room where it would land, and a line marks the place. The list scrolls when the pointer rests near its edge, Escape puts everything back, and with several rows selected they all go together. The same handler serves the keyboard: Alt+Shift+Up and Alt+Shift+Down move the selected rows one place.

Your handler moves the items in your own list and nothing more; the list then shows them, keeps them selected and tells a screen reader where they went. The index you are given is counted among the rows that stay, so “remove them, then insert at the index” is right wherever they came from.

A row’s own controls keep their gestures: a button in a row is still pressed, a field still selects its text.

The picker every business application has, the columns available on one side and the ones shown on the other, is two such lists that also let rows leave and arrive:

Two lists over two lists of the application's own. Rows are dragged within each and from one to the other.Two lists over two lists of the application's own. Rows are dragged within each and from one to the other.
Two lists over two lists of the application's own. Rows are dragged within each and from one to the other.
/** One side of a picker. Two of these, each over its own list, are the whole of it. */
static ListView<Field> pickerSide(List<Field> fields) {
ListView<Field> list = reorderable(fields);
list.setSelectionMode(SelectionMode.MULTI); // several rows go together
// Rows that were moved somewhere else leave this list.
list.onMoveOut(rows -> {
fields.removeAll(rows);
list.refresh();
});
// Rows of another list arrive between two of this one's.
list.setDropFormats(ListView.ITEMS);
list.onDrop((event, index) -> event.offer().read(ListView.ITEMS).thenAccept(rows -> {
List<Field> arriving = new ArrayList<>();
for (Object row : rows) {
arriving.add((Field) row);
}
fields.addAll(Math.min(index, fields.size()), arriving);
list.refresh();
}));
return list;
}

onMoveOut is told that rows dragged out of this list were moved somewhere else, and removes them. Setting it changes what a drag proposes: a list without it offers a copy of its rows, and one with it offers a move, unless the user holds the key that asks for a copy (Ctrl on Windows and Linux, Option on macOS).

setDropFormats names what the list takes and onDrop takes it, with the place between two rows it was dropped at. ListView.ITEMS is the rows themselves, the very objects, and it only ever travels inside your program. event.action() says whether the user asked for a copy or a move, which matters when a copy has to be a new object and not the same one twice.

A list that only ever copies what it is given, or only ever takes it over, says so with setDropAction(DropAction.COPY) or MOVE: that is then what a drop does wherever the source allows it, whatever the source would rather. And rows that mean nothing outside their window stay in it with setDragWithinWindow(true): no other program is offered them.

A list can take what is not a row at all. Name DataFormat.TEXT among the formats and a paragraph dragged out of an editor arrives as text, for you to make rows of; name DataFormat.FILES and it takes files.

What the rows carry to another program is their text, one line a row. To send something else, a file for each row, a picture, a format of your own, give the list a function with setDragContent.

A Tree does the same with one more dimension:

static Tree<Entry> outline(Entry root) {
Tree<Entry> tree = new Tree<>(new Tree.Model<Entry>() {
@Override
public List<Entry> roots() {
return root.children;
}
@Override
public List<Entry> children(Entry entry) {
return entry.children;
}
@Override
public boolean isLeaf(Entry entry) {
return !entry.folder; // an empty folder still takes a drop
}
@Override
public Widget<?> cellFor(Entry entry) {
return new Padding(Insets.symmetric(6, 6), new Label(entry.name));
}
});
tree.setDragEnabled(true);
// A null parent is the top level. As in a list, the index is counted without the nodes
// moved, so they are taken out of wherever they are before they are put in.
tree.onReorder((entries, parent, index) -> {
entries.forEach(root::take);
(parent == null ? root : parent).children.addAll(index, entries);
});
return tree;
}

Dropped on a node, what is dragged goes into it; near the node’s upper or lower edge, before or after it; and never into itself. A closed folder the pointer rests on opens after a moment, so a drop can go as deep as it needs to. The handler is given the new parent, null for the top level, and the place among its children.

By keyboard, Alt+Shift+Up and Down move the selected node among its siblings, Alt+Shift+Left takes it out one level, and Alt+Shift+Right puts it into the sibling before it. onMoveOut, onDrop, setDropFormats, setDropAction, setDragContent and setDragWithinWindow are the list’s, with the parent added.

static ImageView draggable(Image picture) {
ImageView view = new ImageView(picture).setDragEnabled(true);
view.setAccessibleName("Logo");
view.setFocusable(true); // so that the keyboard can pick it up too
// Not on by default, since it writes to disk: a PNG in the temporary directory, for
// the targets that take a picture only as a file. The desktop is one, a folder another.
view.setDragFileName("logo.png");
return view;
}

An ImageView with dragging on offers its picture, with its own transparency, to whatever will take one: a drop area of your program, an image editor, a document. What follows the pointer is the picture as the view shows it.

Some targets take a picture only as a file: the desktop and a folder in a file manager are the common ones. setDragFileName offers the picture that way too, as a PNG written to the temporary directory and deleted when your program ends. It is off unless you ask, because it writes to disk: on Windows and Linux the file is written when a target asks for it, and on macOS, where the system asks as the drag begins, at every drag.

The view offers a copy and allows a move. It keeps its picture whatever the target says it did; the move is allowed because some programs on Windows take a picture or a file only from a source that allows one. If you build such a drag yourself, allow both. setDragWithinWindow(true) keeps the picture from being offered to any other program.

A table’s columns are moved by dragging a header, or with Alt+Shift+Left and Right, which move the column the keyboard is on. Nothing turns that on, and Tables has what there is to say about saving the layout and keeping the first columns in place.

Every drag on this page can be made without a pointer. Ctrl+Shift+D (Cmd+Shift+D on macOS) picks up what the focused widget would drag; Tab and Shift+Tab walk the widgets that would take it, and only those; the arrow keys choose the place inside one; Enter or Space drops, and Escape calls it off. scene.setDragKey changes the key, for an application that already uses that one.

A screen reader is told each step, in the language in effect: what was picked up and where it stands, each place as the arrows move, and where it went. None of it is yours to write for the ready-made widgets. For a widget of your own it is two calls, both below.

Alt+Shift with an arrow is the shorter road for the common case, a row moved within its own list, and needs no picking up.

A widget is asked onDragStart when a press on it has travelled a few points, or when the drag key is pressed while it has the focus. Answering a Drag starts one; answering null leaves the gesture to the ordinary mouse events.

/** The object itself, for the widgets of this program; no other program is offered it. */
static final DataFormat<Order> ORDER =
DataFormat.inProcess("application/x-example-order", Order.class);
/** A card that is dragged: it answers what it carries and hears what was done with it. */
static final class OrderCard extends Widget<OrderCard> {
private final Order order;
private final Consumer<Order> remove;
OrderCard(Order order, Consumer<Order> remove) {
this.order = order;
this.remove = remove;
setFocusable(true); // the drag key picks up what has the focus
setAccessibleName("Order " + order.number());
}
@Override
protected Drag onDragStart(DragStartEvent event) {
// The order for a widget of this program, and a line of text for any other.
Transfer content = Transfer.of(ORDER, order)
.with(DataFormat.TEXT, "Order " + order.number() + ", " + order.customer());
return Drag.of(content)
.allow(DropAction.MOVE, DropAction.COPY) // the first is what is proposed
.describedAs("Order " + order.number())
.onEnd(action -> {
if (action == DropAction.MOVE) {
remove.accept(order); // null: cancelled, or nothing took it
}
});
}
@Override
protected Size onMeasure(Constraints constraints) {
return constraints.constrain(220, 56);
}
@Override
protected void onPaint(Canvas canvas) {
Theme theme = Theme.current();
canvas.fillRoundRectWithBorder(0, 0, width(), height(),
theme.tokensFor(this).radiusMedium(), theme.surfaceRaised(), 1, theme.outline());
canvas.drawText("Order " + order.number(), 12, 24, theme.body(), theme.text());
canvas.drawText(order.customer(), 12, 44, theme.body(), theme.textMuted());
}
}

A Transfer holds the same thing in several formats, and a good source offers more than one: its own for the widgets that know what an order is, and text for everything else, so the same card dropped on an editor becomes a line of text.

  • DataFormat.TEXT, HTML, IMAGE and FILES are understood by other programs.
  • DataFormat.inProcess(name, type) carries the object itself and never leaves your program.
  • DataFormat.of(name, encode, decode) names a format of your own and says how it becomes bytes, so that two copies of your application, or two of your applications, can exchange it.

allow says what a target may do with it, the action you would rather have first; a drag that says nothing allows a copy. describedAs is what a screen reader says was picked up. onEnd runs once, with what a target did, or null when the drag was cancelled or nothing took it: remove the original there when the answer is a move, and nowhere else.

What follows the pointer is the widget as it is painted. When that is wrong, a row of a long list, a card that should keep its rounded corners over another program, preview(x, y, width, height) shows a part of the widget and preview(width, height, holdX, holdY, painter) has you paint the dragged thing alone on a transparent surface. withinWindow() keeps a drag from ever being offered to another program, which is right for something that means nothing outside its window.

Whatever is dragged with the pointer, a scroll view, a list, a table, a tree or a text area under the pointer scrolls when the pointer comes near one of its edges, toward that edge: drag toward the bottom of a long page and the page brings up what is below, a drop area included. It starts gently and goes faster the nearer the edge. A drag that keeps to its window (withinWindow()) is followed past the window’s edge too, faster the farther, up to a bound. A list inside a page scrolls first, and when it has reached its end the page around it takes over, if the pointer is near the page’s edge too; a list the pointer has left for the page around it stays where it was. It works the same reading right to left, and for a view that is turned, scaled or tilted under a perspective, at each of its edges on screen. Files dragged in from another program scroll the window in the same way.

Nothing turns it on. A widget of your own that scrolls through a ScrollBar scrolls under a drag like the toolkit’s do, a few points a frame through its model’s scrollBy. Where your widget’s offset is an estimate, as with rows of different heights measured only as they are built, override scrollBy with a move from where the content is, as the wheel makes: the default goes through setOffset, and each small step would land by the estimate.

/** A lane that takes orders: it says yes while one hovers, shows it, and takes the drop. */
static final class Lane extends Widget<Lane> {
private final String name;
private final List<Order> orders;
private boolean over;
Lane(String name, List<Order> orders) {
this.name = name;
this.orders = orders;
setAccessibleName(name);
}
@Override
protected void onDragOver(DragEvent event) {
if (!event.offer().has(ORDER)) {
return; // says nothing: the parent is asked next
}
event.accept();
if (event.isKeyboard()) {
event.describePlace("At the end of " + name);
}
mark(true);
}
@Override
protected void onDragExit(DragEvent event) {
mark(false);
}
@Override
protected void onDrop(DropEvent event) {
mark(false); // no exit follows a drop
event.offer().read(ORDER).thenAccept(order -> {
orders.add(order);
invalidate();
});
}
private void mark(boolean now) {
if (over != now) {
over = now;
invalidate();
}
}
@Override
protected Size onMeasure(Constraints constraints) {
return constraints.constrain(240, 160);
}
@Override
protected void onPaint(Canvas canvas) {
Theme theme = Theme.current();
canvas.fillRoundRectWithBorder(0, 0, width(), height(),
theme.tokensFor(this).radiusMedium(), theme.surface(),
over ? 2 : 1, over ? theme.focusRing() : theme.outline());
float y = 24;
for (Order order : orders) {
canvas.drawText("Order " + order.number(), 12, y, theme.body(), theme.text());
y += 22;
}
}
}

onDragOver is asked again on every move, so the answer can depend on the place: accept over the upper half and not the lower, if that is what the widget means. A widget that says nothing passes the question to its parent, which is how a whole panel takes a drop that none of its children wants. Mark where the drop would land, and unmark it in both onDragExit and onDrop, since no exit follows a drop.

A widget that shows it refuses, a red edge on a column that takes nothing from that list, clears the sign in onDragExit too. Every widget asked hears it once the drag is no longer over it, whether it accepted or not: the pointer left it, a child under the pointer answered first, the drag was called off or left the window, it was dropped somewhere else, or it was let go over this widget with nothing taking it. A panel around children that say nothing is asked wherever the pointer is over them, so it keeps its sign while the pointer crosses them. In a keyboard drag a widget that refuses is told at once, as Tab walks on past it.

event.accept() takes what the user is asking for; event.accept(DropAction.COPY) insists on one action among those the source allows, for a target that only ever copies.

Reading is asynchronous, because what is dragged may belong to another program that has not produced it yet: read answers a CompletionStage that completes on the UI thread. event.reject() refuses a drop after all, and the source is then told nothing was done.

In a keyboard drag there is no pointer. event.isKeyboard() is true, each arrow key arrives as event.stepX() or stepY() for the widget to move a place of its own by, and event.describePlace("Before Order 42") is what a screen reader says of it.

Files dragged from the desktop or a file manager are a drop like any other, carrying DataFormat.FILES:

/** A widget that opens the files dropped on it, from a file manager or the desktop. */
static final class FileArea extends Widget<FileArea> {
private final Consumer<java.nio.file.Path> open;
FileArea(Consumer<java.nio.file.Path> open) {
this.open = open;
}
@Override
protected void onDragOver(DragEvent event) {
if (event.offer().has(DataFormat.FILES)) {
event.accept(DropAction.COPY); // the files stay where they are
}
}
@Override
protected void onDrop(DropEvent event) {
event.offer().read(DataFormat.FILES).thenAccept(files -> files.forEach(open));
}
@Override
protected Size onMeasure(Constraints constraints) {
return constraints.constrain(240, 120);
}
@Override
protected void onPaint(Canvas canvas) {
}
}

scene.clipboard(), or clipboard() inside a widget, is the system’s clipboard. Text has two direct calls, text() and setText(…), which is all a text control uses. Anything richer is a Transfer going in:

/** A copy that a spreadsheet pastes as cells, an editor as text, and a paint program as a picture. */
static void copy(Clipboard clipboard, String title, String html, Image chart) {
clipboard.set(Transfer.of(DataFormat.TEXT, title)
.with(DataFormat.HTML, html)
.with(DataFormat.IMAGE, chart));
}

and an offer coming out, which lists its formats at once, so a menu can grey its Paste item without waiting, and reads a value when asked:

static void paste(Clipboard clipboard, Consumer<Image> attach, Consumer<String> insert) {
Offer there = clipboard.content(); // the formats are known at once
if (there.has(DataFormat.IMAGE)) {
there.read(DataFormat.IMAGE).thenAccept(attach); // the value arrives on the UI thread
} else if (there.has(DataFormat.TEXT)) {
there.read(DataFormat.TEXT).thenAccept(insert);
}
}

A value that is expensive to make can wait until somebody wants it:

/** The picture is rendered when somebody pastes it, and not at all if nobody does. */
static void copyLazily(Clipboard clipboard, String title, java.util.function.Supplier<Image> render) {
clipboard.set(Transfer.of(DataFormat.TEXT, title).withLazy(DataFormat.IMAGE, render));
}

onChange hears the clipboard change, whoever changed it, where the system tells.

All of it reaches other programs, and comes from them, on Windows, macOS, X11 and Wayland: a picture from a screenshot tool, files copied in a file manager, a table pasted into a spreadsheet as HTML. On Windows and macOS what you copied stays on the clipboard after your application closes; on Linux a clipboard lives in the program that owns it, and what survives depends on the desktop’s clipboard manager.

  • Under Wayland linking is not offered, the protocol having copy and move only, and with no key held it is the target’s preference that decides between those two, not the source’s. Shift and Control still ask for a move and a copy.
  • On X11, a program built on GTK 4 speaks to your window in a way that goes round the channel drags are answered on. Files dragged from one, the desktop’s file manager being the usual case, are followed by the pointer instead: your widget is asked as they hover, offered them as files alone and as a copy alone, and they arrive at the drop. Anything else such a program drags, text for one, does not arrive. Under Wayland, and from programs built on anything else, a drag is answered in full.
  • At a fractional display scale under Wayland, the picture that follows the pointer is rounded to a whole scale and may look slightly soft.
  • In a test, the headless window has a clipboard of its own and a drag stays inside its scene, so a press, a few moves and a release from the test driver exercise both without a display.