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.
A list the user rearranges
Section titled “A list the user rearranges”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.
Two lists that trade rows
Section titled “Two lists that trade rows”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:


/** 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.
An outline that rearranges
Section titled “An outline that rearranges”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.
A picture that leaves its view
Section titled “A picture that leaves its view”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.
Columns the user arranges
Section titled “Columns the user arranges”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.
By keyboard, and what a reader hears
Section titled “By keyboard, and what a reader hears”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.
Your own widget as a source
Section titled “Your own widget as a source”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,IMAGEandFILESare 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.
When the place to drop is out of sight
Section titled “When the place to drop is out of sight”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.
Your own widget as a target
Section titled “Your own widget as a target”/** 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 dropped on your window
Section titled “Files dropped on your window”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) { }}The clipboard
Section titled “The clipboard”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.
The edges
Section titled “The edges”- 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.