Handle dropped files - fakeivchenko/lwjwae GitHub Wiki

Users drag files onto applications: a photo into an editor, a PDF into a reader, a folder into a project window. A page can read the names and the bytes of the files that a drop gives it, but never their paths, and the path is what a desktop application needs to open, move, or watch a file. lwjwae reads the paths natively and tells Java and the page. In this guide, a page has a drop zone that lists what lands on it, and Java counts the files.

A drop zone before and after three files land on it, on Linux and on Windows 11

Hear a drop in Java

window.onFileDrop calls a listener with a FileDropEvent when the user drops files from the file manager onto the page:

public class Drops {
  public static void main(String[] args) {
    Application.launch(
        WindowParameters.of("Drop", 560, 400),
        window -> {
          window.onFileDrop(
              event ->
                  window.emit(
                      "heard",
                      "Java heard %d files at %d, %d"
                          .formatted(event.paths().size(), event.x(), event.y())));
          window.loadResource("drop/index.html");
        });
  }
}
Component What it is
paths() The files and folders, as absolute Paths, in the order that the file manager gave them. Never empty
window() The window that the files landed on
x(), y() Where the pointer was when the user let go, from the top left of the page

The listener runs on a virtual thread, so it may read the files, or wait for something, without blocking the window. onFileDrop returns an EventSubscription, and unlisten() stops it.

Hear a drop on the page

The page has no paths in the files of a drop event, so it asks lwjwae.files:

const zone = document.querySelector("#zone");
lwjwae.files.listen(({ paths, x, y }) => {
  document.querySelector("#list").replaceChildren(...paths.map((path) => {
    const item = document.createElement("li");
    item.textContent = path;
    return item;
  }));
  zone.textContent = paths.length + " files dropped";
});

listen resolves to the function that stops the listener. The payload has the same paths, x, and y as the Java event. The ordinary events of the page, dragenter, dragover, dragleave, and drop, still come, and are the place to light up the drop zone:

addEventListener("dragenter", () => zone.classList.add("over"));
addEventListener("dragleave", () => zone.classList.remove("over"));
addEventListener("drop", () => zone.classList.remove("over"));

What the window does with the file

Without help, an engine opens a file that lands on its page, and the application is gone from the window. lwjwae stops that: it takes a dragover or a drop of files that the page leaves alone, so the window stays where it is and the file goes to the listeners only.

  • A page that handles dragover and drop itself, and calls preventDefault(), keeps its way, and Java still hears the drop.
  • A text field, a text area, or an editable element keeps its own drop, so dragging a link into a field still inserts the text.
  • A drop of anything but files, such as selected text, is not a file drop and isn't reported.

Where it works

Platform How the paths come
Windows The script that lwjwae adds to every page posts the files of each drop to the host, and WebView2 gives the host their full paths. The runtime must be 1.0.1264 or later
macOS The web view reads the file URLs on the pasteboard of the drag as it takes the drop, and the place comes from the drop event of the page. Finder hands references to files, which arrive as their paths
Linux, GTK 3 The text/uri-list of the drop. A file that lives only in a remote location, which the file manager mounted, is left out
Linux, GTK 4 The window reads the files that the drag offers as it comes over the page, and the place comes from the drop event of the page. A file that isn't local is left out

Good to know

  • x and y are where the pointer was, from the top left of the page. On Windows, macOS, and GTK 4 they are the clientX and clientY of the drop event, so document.elementFromPoint(x, y) finds the element that got the files at any zoom. GTK 3 measures them in the pixels of the window, which are the pixels of the page only at a zoom of 1.
  • A folder arrives as one path. Walk it in Java, with Files.walk, to get its files.
  • Paths with spaces, #, %, and & arrive the way they are on the disk.