1

I'm working on a JavaFX application that should interact with an existing Swing application via drag-and-drop. The data exchange via drag-and-drop actually works, but we want to rework that part of the functionality to actually exchange custom Java objects instead of simple Strings with objects serialized to JSON. The problem is, that the Swing UI does not receive the dragged data, if custom MIME types are used instead of e.g. text/plain. Below you can find a minimal example for both the drag application (JavaFX) and the drop application (Swing).

FxDrag

public class FxDrag extends Application {

    private static final DataFormat format = new DataFormat("application/x-my-mime-type");

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        BorderPane root = new BorderPane();
        root.setOnDragDetected(event -> {
            Dragboard dragboard = root.startDragAndDrop(TransferMode.COPY);
            ClipboardContent content = new ClipboardContent();
            content.putString("Test");
            // content.put(format, "Test");
            dragboard.setContent(content);
            event.consume();
        });
        stage.setScene(new Scene(root, 300, 300));
        stage.setTitle("Drag");
        stage.show();
    }
}

SwingDrop

public class SwingDrop {

    public static void main(String[] args) {
        new SwingDrop().run();
    }

    private void run() {
        JPanel panel = new JPanel();
        panel.setTransferHandler(new TransferHandler() {

            @Override
            public boolean canImport(TransferSupport support) {
                return true;
            }

            @Override
            public boolean importData(TransferSupport support) {
                Stream.of(support.getDataFlavors()).forEach(flavor -> {
                    System.out.println(flavor.getMimeType());
                });
                return super.importData(support);
            }

        });
        JFrame frame = new JFrame();
        frame.setTitle("Drop");
        frame.add(panel);
        frame.setSize(300, 300);
        frame.setVisible(true);
    }
}

When putting a String via putString to the content in the JavaFX application, the Swing application receives the drag and provides the following flavors:

application/x-java-serialized-object; class=java.lang.String
text/plain; class=java.io.Reader; charset=Unicode
text/plain; class=java.lang.String; charset=Unicode
text/plain; class=java.nio.CharBuffer; charset=Unicode
text/plain; class="[C"; charset=Unicode
text/plain; class=java.io.InputStream; charset=unicode
text/plain; class=java.nio.ByteBuffer; charset=UTF-16
text/plain; class="[B"; charset=UTF-16
text/plain; class=java.io.InputStream; charset=UTF-8
text/plain; class=java.nio.ByteBuffer; charset=UTF-8
text/plain; class="[B"; charset=UTF-8
text/plain; class=java.io.InputStream; charset=UTF-16BE
text/plain; class=java.nio.ByteBuffer; charset=UTF-16BE
text/plain; class="[B"; charset=UTF-16BE
text/plain; class=java.io.InputStream; charset=UTF-16LE
text/plain; class=java.nio.ByteBuffer; charset=UTF-16LE
text/plain; class="[B"; charset=UTF-16LE
text/plain; class=java.io.InputStream; charset=ISO-8859-1
text/plain; class=java.nio.ByteBuffer; charset=ISO-8859-1
text/plain; class="[B"; charset=ISO-8859-1
text/plain; class=java.io.InputStream; charset=windows-1252
text/plain; class=java.io.InputStream
text/plain; class=java.nio.ByteBuffer; charset=windows-1252
text/plain; class="[B"; charset=windows-1252
text/plain; class=java.io.InputStream; charset=US-ASCII
text/plain; class=java.nio.ByteBuffer; charset=US-ASCII
text/plain; class="[B"; charset=US-ASCII

I can even drop different data from various applications like browsers etc. and the Swing application provides the respective data flavors in the drop (text, images etc.).

However, if I use my custom format, no flavors are listed at all. Does Swing filter the data flavors transfered via a drag-and-drop application?

Lukas Körfer
  • 13,515
  • 7
  • 46
  • 62
  • I am able to do transfer of Java objects (e.g., Product class) by using minimal info like a key (e.g., Product Id) which is a string or combination of delimiter separated strings. The drag and drop happens as a string and at the drop location the Java object is built using the key string value; the actual object is available in a collection. This worked within a Swing application between two windows. – prasad_ Aug 01 '18 at 12:32
  • Of course I could always somehow put the necessary information into a string and only transfer this string as `text/plain`, but it is ugly in my opinion, as the transfered data is not just a plain string. Also, this way I can drop the data nearly anywhere instead of just into the target application. – Lukas Körfer Aug 01 '18 at 12:36
  • Its an idea. But, _I can drop the data nearly anywhere instead of just into the target application..._. I think no; at least what I had tried doesn't allow the drop anywhere within the app or outside other than the target (one can restrict). – prasad_ Aug 01 '18 at 12:42
  • Using `text/plain` will allow a drop on anything that accepts a `text/plain`, e.g. editors, browsers, most applications text inputs. Even if the content is just a string, I would like to use a custom MIME type to prevent data transfers into these applications, of course not for security reasons, but for usability. – Lukas Körfer Aug 01 '18 at 12:53

2 Answers2

4

Old answer did not work between separate applications. New attempt below.


I managed to get this working between separate Swing and JavaFX applications in both directions. I uploaded a working example to a GitLab repository if you want to view it, but I will go over some of the basics here.

If you look at the repository you'll notice I have a Gradle subproject named model that contains the model class com.example.dnd.model.Doctor. This class is Serializable and contains three properties: firstName, lastName, and number. This project is shared between the JavaFX and Swing applications (i.e. they use the same model). In each application I have a table that displays a list of Doctors by those properties: TableView in JavaFX and JTable in Swing.

The applications allow you to drag one or more rows to the other application and append them to the end of the table. They do this by sending a list of the appropriate Doctors.

The example requires Java 10. GIF of example in action.


JavaFX

The JavaFX side I found much simpler to implement. Really, the only thing you need to work out is how to configure the appropriate DataFormat. The MIME type I used was,

application/x-my-mime-type; class=com.example.dnd.model.Doctor

The class= parameter is important on the Swing side; it is used for deserialization. After some trial-and-error I found out that when you try to drag data from Swing to JavaFX the given MIME type is prepended with JAVA_DATAFLAVOR:, making it:

JAVA_DATAFLAVOR:application/x-my-mime-type; class=com.example.dnd.model.Doctor

I had to add that to the DataFormat used in the onDragDetected handler otherwise Swing did not recognize the data format. I don't know why this is the case and I did not find documentation about this. When changing Java versions and/or platforms I'd be careful about this in case this is implementation-dependent behavior (unless you manage to find documentation).

In the end, my DataFormat was declared like so:

DataFormat format = new DataForamt(
    "JAVA_DATAFLAVOR:application/x-my-mime-type; class=com.example.dnd.model.Doctor",
    "application/x-my-mime-type; class=com.example.dnd.model.Doctor"
);

I added two identifiers, one with JAVA_DATAFLAVOR and one without, in an attempt to cover both cases (where it's needed and not). I don't know if this is necessary nor if it helps at all. I then stored this in some static final field for global access.

Then you just implement the onDragXXX handlers as you would expect.


Swing

The Swing side was a little more involved in my opinion; though that could just be because I'm more comfortable with JavaFX. I'd like to mention that the Oracle Tutorials were very useful here. There were three1 important classes related to DnD in Swing:

1 - There are other classes involved but these were the three I found most important in this case.

To get this working I had to create custom implementations of TransferHandler and Transferable.

TransferHandler

import com.example.dnd.model.Doctor;
import java.awt.datatransfer.Transferable;
import java.util.ArrayList;
import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.TransferHandler;

public class DoctorTransferHandler extends TransferHandler {

  @Override
  public boolean canImport(TransferSupport support) {
    return support.isDrop() && support.isDataFlavorSupported(DoctorTransferable.DOCTOR_FLAVOR);
  }

  @Override
  public boolean importData(TransferSupport support) {
    if (!canImport(support)) {
      return false;
    }
    JTable table = (JTable) support.getComponent();
    DoctorTableModel model = (DoctorTableModel) table.getModel();
    try {
      Transferable transferable = support.getTransferable();
      ArrayList<Doctor> list =
          (ArrayList<Doctor>) transferable.getTransferData(DoctorTransferable.DOCTOR_FLAVOR);
      model.addAll(list);
      return true;
    } catch (Exception ex) {
      ex.printStackTrace();
      return false;
    }
  }

  @Override
  public int getSourceActions(JComponent c) {
    return COPY_OR_MOVE;
  }

  @Override
  protected Transferable createTransferable(JComponent c) {
    JTable table = (JTable) c;
    DoctorTableModel model = (DoctorTableModel) table.getModel();
    return new DoctorTransferable(model.getAll(table.getSelectedRows()));
  }

  @Override
  protected void exportDone(JComponent source, Transferable data, int action) {
    if (action == MOVE) {
      JTable table = (JTable) source;
      DoctorTableModel model = (DoctorTableModel) table.getModel();
      model.removeAll(model.getAll(table.getSelectedRows()));
    }
  }

}

Transferable

import com.example.dnd.model.Doctor;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;

public class DoctorTransferable implements Transferable {

  public static final DataFlavor DOCTOR_FLAVOR;

  static {
    try {
      DOCTOR_FLAVOR = new DataFlavor("application/x-my-mime-type; class=java.util.ArrayList");
    } catch (ClassNotFoundException ex) {
      throw new RuntimeException(ex);
    }
  }

  private final ArrayList<Doctor> doctors;

  public DoctorTransferable(Collection<? extends Doctor> doctors) {
    this.doctors = new ArrayList<>(doctors);
  }

  @Override
  public DataFlavor[] getTransferDataFlavors() {
    return new DataFlavor[]{DOCTOR_FLAVOR};
  }

  @Override
  public boolean isDataFlavorSupported(DataFlavor flavor) {
    return DOCTOR_FLAVOR.equals(flavor);
  }

  @Override
  public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
    if (DOCTOR_FLAVOR.equals(flavor)) {
      return doctors;
    }
    throw new UnsupportedFlavorException(flavor);
  }

}

If you look in at the declaration of the DataFlavor, inside the Transferable, you'll see I'm using the same MIME type as in JavaFX minus the JAVA_DATAFLAVOR: bit.

I believe the most import part is creating your own Transferable that handles your custom object. This Transferable will be created in the protected TransferHandler#createTranserfable method. It wasn't until I realized I needed to do this that I managed to get this to work. It's the Transferable that's responsible for reporting the DataFlavor and how to retrieve the objects.

The next two important things you have to do is override canImport and importData. These methods handle whether or not the dragged over data can be successfully dropped and, if it is, how to add it to the Swing component. My example is quite simple and adds the data to the end of the JTable's model.

For exporting data you should also override exportDone. This method is responsible for performing any cleanup if the transfer involved moving data rather than just copying it.


I reached this solution through a decent amount of trial and error. As a result of that, combined with the fact I wanted to keep this as simple as I could, a lot of "standard" behavior is not implemented. For instance, data is always appended to the bottom of the table rather than inserted where dropped. On the JavaFX side the drag handlers are on the entire TableView rather than on each TableCell (which would make more sense, I think).

I hope this works for you. If not, please let me know.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • *so this should work between separate applications* - Sadly, it does not. I just started your application twice and when dragging from the one instance to the other, it throws an `IllegalArgumentException` with the reason `DataFormat 'application/x-my-mime-type' already exists.`. I also tried to circumvent this by calling the creation of the `DataFormat`/`DataFlavor` on startup, but it still fails on drags **over** the second application instance. – Lukas Körfer Aug 06 '18 at 22:58
  • Well, the exception above is only thrown on drops to an application instance without a previous successful drop within the instance. If a successful drop within the instance has been performed and a "cross-instance" drop is done, it throws an `IllegalArgumentException` with the reason `failed to parse:DragImageBits`. Also, this time the stacktrace does not mention the call to the `DataHolder` constructor from my own code, but from creating a `DataFlavor` in `javafx.embed.swing.DataFlavorUtils.getDataFlavors`. – Lukas Körfer Aug 06 '18 at 23:08
  • @lu.koerfer Interesting... just tried it and got the same exception. That's strange because the `IllegalArgumentException` doesn't make sense in the context of two application instances. I'll see if I can figure this out in a little bit. – Slaw Aug 06 '18 at 23:10
  • @lu.koerfer I updated my answer, let me know if it works for you or not. – Slaw Aug 07 '18 at 17:56
  • Wow, really good work. I could make my minimal example work with your approach, which definitely suits my needs. However, I still don't know how why this is necessary, maybe I will do some research later on. I already found the `SystemFlavorMap` class in JDK, which provides various methods to extend MIME types with the `JAVA_DATAFLAVOR:` prefix. – Lukas Körfer Aug 07 '18 at 22:58
  • For some unknown and strange reason, this does not work when embedding the JavaFX part into a Swing application (via `JFXPanel`). I created a new question for this scenario: [Custom object drag-and-drop from embedded FX (JFXPanel) to Swing](https://stackoverflow.com/questions/52599005/custom-object-drag-and-drop-from-embedded-fx-jfxpanel-to-swing). – Lukas Körfer Oct 01 '18 at 21:40
0

Just for convenience, I'll add a combination of the excellent solution by @Slaw and the minimal example from my question. To get a better insight, take a look at his answer, as it is way more detailed.


FxDrag

public class FxDrag extends Application {

    public static final DataFormat FORMAT = new DataFormat(
        "JAVA_DATAFLAVOR:application/x-my-mime-type; class=java.lang.String",
        "application/x-my-mime-type; class=java.lang.String");

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        BorderPane root = new BorderPane();
        root.setOnDragDetected(event -> {
            Dragboard dragboard = root.startDragAndDrop(TransferMode.COPY);
            ClipboardContent content = new ClipboardContent();
            content.put(FORMAT, "Test123");
            dragboard.setContent(content);
            event.consume();
        });

        stage.setScene(new Scene(root, 300, 300));
        stage.setTitle("Drag");
        stage.show();
    }

}

SwingDrop

public class SwingDrop {

    public static final DataFlavor FLAVOR;

    static {
        try {
            FLAVOR = new DataFlavor("application/x-my-mime-type; class=java.lang.String");
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static void main(String[] args) {
        new SwingDrop().run();
    }

    private void run() {
        JPanel panel = new JPanel();
        panel.setTransferHandler(new TransferHandler() {

            @Override
            public boolean canImport(TransferSupport support) {
                return support.isDataFlavorSupported(FLAVOR);
            }

            @Override
            public boolean importData(TransferSupport support) {
                if (!canImport(support)) return false;
                try {
                    String data = (String) support.getTransferable().getTransferData(FLAVOR);
                    System.out.println(data);
                    return true;
                } catch (UnsupportedFlavorException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return false;
            }

        });
        JFrame frame = new JFrame("Drop");
        frame.getContentPane().add(panel);
        frame.setSize(300, 300);
        frame.setVisible(true);
    }

}

DragAndDrop operations from the FX applications to the Swing application are possible with these example applications. It is not possible to drag to any other application, even if the transfered data is just a plain String. This works just as intended to improve the usability.

Lukas Körfer
  • 13,515
  • 7
  • 46
  • 62