0

I tried to fix this issue for the past week but somehow I cant seem to find a solution. There is not a lot of information about this topic so its hard to find examples or code to look at.

What I have here is a JList which uses a custom TransferHandler that creates a custom Transferable, for reference here's the code of the involved classes:

Transferable:

package org.dinhware.swing.special;

import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;

/**
 * Created by: Niklas
 * Date: 20.10.2017
 * Alias: Dinh
 * Time: 20:03
 */

public class GenericTransferable<T> implements Transferable {
    static DataFlavor FLAVOR;
    private T object;

    GenericTransferable(T object) {
        GenericTransferable.FLAVOR = new DataFlavor(object.getClass(), object.getClass().getCanonicalName());
        this.object = object;
    }

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

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

    @Override
    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
        return object;
    }
}

TransferHandler:

package org.dinhware.swing.special;

import javax.swing.*;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;

/**
 * Created by: Niklas
 * Date: 19.10.2017
 * Alias: Dinh
 * Time: 18:54
 */

@SuppressWarnings("unchecked")
public class HListItemTransferHandler<T> extends TransferHandler {

    @Override
    protected Transferable createTransferable(JComponent component) {
        JList<T> list = (JList<T>) component;
        index = list.getSelectedIndex();
        T transferredObject = list.getSelectedValue();
        return new GenericTransferable<>(transferredObject);
    }

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

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

    @Override
    public boolean importData(TransferSupport info) {
        if (!canImport(info)) {
            return false;
        }

        JList<Object> target = (JList<Object>) info.getComponent();
        JList.DropLocation dl = (JList.DropLocation) info.getDropLocation();
        DefaultListModel<Object> listModel = (DefaultListModel<Object>) target.getModel();
        int index = dl.getIndex();
        int max = listModel.getSize();

        if (index < 0 || index > max)
            index = max;

        addIndex = index;

        try {
            Object object = info.getTransferable().getTransferData(GenericTransferable.FLAVOR);
            listModel.add(index, object);
            target.addSelectionInterval(index, index);
            return moveAllowed = true;
        } catch (UnsupportedFlavorException | IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    protected void exportDone(JComponent c, Transferable data, int action) {
        if (moveAllowed)
            cleanup(c, action == MOVE, false);

    }

    private void cleanup(JComponent component, boolean remove, boolean bin) {
        if (remove && index != -1) {
            JList<T> source = (JList<T>) component;
            DefaultListModel<T> model = (DefaultListModel<T>) source.getModel();
            int removeAt = index > addIndex ? index + 1 : index;
            model.remove(bin ? removeAt - 1 : removeAt);
        }

        index = -1;
        addIndex = -1;
        moveAllowed = false;
    }

    private int index = -1;
    private int addIndex = -1;
    private boolean moveAllowed = false;
}

HBin

package org.dinhware.swing.child;

import org.dinhware.swing.special.HListItemTransferHandler;

import javax.swing.*;

/**
 * Created by: Niklas
 * Date: 20.10.2017
 * Alias: Dinh
 * Time: 19:57
 */

public class HBin<T> extends HImageLabel {
    public HBin(String text, Icon image, int distance) {
        super(text, image, distance);
        setTransferHandler(new HListItemTransferHandler<T>());
    }
}

And a visualization of how it should work, sadly the container always disappears even when not dragged onto the HBin Container. I thought it was working the entire time until I accidentally moved it outside of my Frame and it still disappeared. The Code above only allows dragging/dropping inside of the List which is intended.

enter image description here

My Question is how to add the functionality to properly make a Container only disappear when dragged onto the HBin Container

The first part of Code I used was this

@Override
protected void exportDone(JComponent c, Transferable data, int action) {
    if (moveAllowed) cleanup(c, action == MOVE, false);
    else try {
        if (data.getTransferData(GenericTransferable.FLAVOR) instanceof RewardItem) {
            cleanup(c, true, true);
        }
    } catch (UnsupportedFlavorException | IOException e) {
        e.printStackTrace();
    }
}

My logic behind it was that both the List and HBin shared the same Type (RewardItem) which I could compare, later I realized (after making a more generic version of that method) that data will always be of the type RewardItem and will always result in a cleanup call. This results in the bug I am currently still facing.

The approach I took earlier today really made me question my mind and also made me do this post. I added a boolean to the TransferHandler called bin which was false by default. After the canImport check in importData I added bin = info.getComponent() instanceof HBin which I thought should work. But this field always stood false. I went ahead and added a log for it

System.out.println("IMPORT");
if (info.getComponent() instanceof HBin) {
    System.out.println("bin");
    return bin = true;
}

It ended up printing IMPORT followed by bin. After importData exportData is called in which I then logged the value of bin, which for whatever reason was now false again. Meanwhile the moveAllowed field seems to change.

This was my full modified TransferHandler

package org.dinhware.swing.special;

import org.dinhware.swing.child.HBin;

import javax.swing.*;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;

/**
 * Created by: Niklas Date: 19.10.2017 Alias: Dinh Time: 18:54
 */

@SuppressWarnings("unchecked")
public class HListItemTransferHandler<T> extends TransferHandler {

    @Override
    protected Transferable createTransferable(JComponent component) {
        System.out.println("CREATE");
        JList<T> list = (JList<T>) component;
        index = list.getSelectedIndex();
        T transferredObject = list.getSelectedValue();
        return new GenericTransferable<>(transferredObject);
    }

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

    @Override
    public int getSourceActions(JComponent c) {
        System.out.println("ACTION");
        return MOVE;
    }

    @Override
    public boolean importData(TransferSupport info) {
        System.out.println("IMPORT");
        if (!canImport(info)) {
            return false;
        }
        if (info.getComponent() instanceof HBin) {
            System.out.println("bin");
            return bin = true;
        }

        JList<Object> target = (JList<Object>) info.getComponent();
        JList.DropLocation dl = (JList.DropLocation) info.getDropLocation();
        DefaultListModel<Object> listModel = (DefaultListModel<Object>) target.getModel();
        int index = dl.getIndex();
        int max = listModel.getSize();

        if (index < 0 || index > max)
            index = max;

        addIndex = index;

        try {
            Object object = info.getTransferable().getTransferData(GenericTransferable.FLAVOR);
            listModel.add(index, object);
            target.addSelectionInterval(index, index);
            return moveAllowed = true;
        } catch (UnsupportedFlavorException | IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    protected void exportDone(JComponent c, Transferable data, int action) {
        System.out.println("EXPORT " + moveAllowed + "/" + bin);
        if (moveAllowed)
            cleanup(c, action == MOVE, false);
        else
            cleanup(c, true, true);

    }

    private void cleanup(JComponent component, boolean remove, boolean bin) {
        System.out.println("CLEAN");
        if (remove && index != -1) {
            JList<T> source = (JList<T>) component;
            DefaultListModel<T> model = (DefaultListModel<T>) source.getModel();
            int removeAt = index > addIndex ? index + 1 : index;
            model.remove(bin ? removeAt - 1 : removeAt);
        }

        index = -1;
        addIndex = -1;
        moveAllowed = false;
    }

    private int index = -1;
    private int addIndex = -1;
    private boolean moveAllowed = false, bin = false;
}   

When moving inside of the List everything works fine (prints)

ACTION
CREATE
IMPORT
EXPORT true/false
CLEAN

But when dropping onto the HBin Container I cant explain whats going on (prints)

ACTION
CREATE
IMPORT
bin
EXPORT false/false

I am farily sure that it should be false/true

Now I am stuck, not able to make the Container only disappear when dropped onto HBin whilst also being confused about the field value not changing when it clearly logs that it has been set to true.

Please.. help...

Andrey Belykh
  • 2,578
  • 4
  • 32
  • 46
Dinh
  • 759
  • 5
  • 16
  • As you've noted, Drag'n'Drop is a complicated beast, a [Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve) would go along way to helping us help you – MadProgrammer Nov 12 '17 at 21:05
  • Generally speaking, you shouldn't be maintaining information in the transferable about the state of the operation (i.e. the index of the item for example), this information should be extrapolated from the transferable information – MadProgrammer Nov 12 '17 at 21:10
  • `static DataFlavor FLAVOR;` doesn't make sense since you creating a new instance each time you create a new instance of the transferable and each instance is different – MadProgrammer Nov 12 '17 at 21:11
  • Thanks for your time, Ill try to work on a minimal version of my problem thats compilable! – Dinh Nov 12 '17 at 21:22
  • Drag'n'Drop is complicated - I've been staring at the code for half an hour, trust me, a runnable example would make a HUGE difference – MadProgrammer Nov 12 '17 at 21:34
  • Do I just edit the Thread witha bunch more of code – Dinh Nov 12 '17 at 21:56
  • Well, I'd first read the link to understand what it is we're looking for and the update the question to include primarily the example and maybe the transfer handler – MadProgrammer Nov 12 '17 at 22:01
  • One question I still have above the Code I posted here is why the boolean was not true, the log appeared and it was clearly set. I still have no clue how that is possible – Dinh Nov 12 '17 at 22:11
  • There's not much context to go by, but if you're using difference instances of the `TransferHandler` that would explain the probable cause. This is why it's important not to rely on instance properties, but rely on the information been passed to the methods. It's possible to write a transfer handler that is generic enough to be supported by multiple components – MadProgrammer Nov 12 '17 at 22:23

1 Answers1

1

Drag'n'Drop is complicated and it's not helped by the fact that there are at least two ways to do it.

D'n'D revolves around the idea of "wrapping" an object up in a "transferable" package, which can be "imported" via a number of different means (i.e. DataFlavors)

So, in this example, I've focused only on removing items from the JList, to do this, I created a Trash object which actually maintains a reference to the item to be removed (I also created a ListTrash object to demonstrate at least one way you could pass more information)

This object is then wrapped in a TrashTransferable when a drag occurs on the JList

The main reason for having a Trash object, it it allows the DataFlavor to be standardised. The "trash can" only cares about Trash objects, nothing else. This is especially important if you have more TransferHandlers doing more operations

One other thing I did was create two TransferHandlers. One for the "trash can" and one for the JList, the main reason for this is it isolates the functionality each handler wants to perform and reduces the complexity, as you're not also trying to determine which object is trying to perform which operation.

The example also has another component which is not doing much at all, so it can reject the drop operation.

If you have another components using TransferHandlers, then those need to reject the TrashTransferable.FLAVOR

import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.io.IOException;
import javax.swing.DefaultListModel;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.TransferHandler.TransferSupport;

public class Test {

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

    public Test() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame("Test");
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new GridBagLayout());
            DefaultListModel<String> model = new DefaultListModel<>();
            model.addElement("Cooks_Assistant");
            model.addElement("Romeo_and_Juliet");
            model.addElement("Sheep_Shearer");

            JList list = new JList(model);
            list.setTransferHandler(new HListItemTransferHandler());
            list.setDragEnabled(true);

            JLabel noDrop = new JLabel("No drop here", JLabel.CENTER);
            JLabel trash = new JLabel("All your trash belong to us", JLabel.CENTER);
            trash.setTransferHandler(new BinTransferHandler());

            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridx = 0;
            gbc.gridy = 0;
            gbc.weightx = 0.5;
            gbc.weighty = 1;
            gbc.fill = GridBagConstraints.BOTH;
            gbc.insets = new Insets(4, 4, 4, 4);

            add(new JScrollPane(list), gbc);

            gbc.gridx++;
            add(noDrop, gbc);

            gbc.gridx = 0;
            gbc.gridy++;
            gbc.gridwidth = GridBagConstraints.REMAINDER;
            add(trash, gbc);

        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(300, 300);
        }

    }

    public class BinTransferHandler extends TransferHandler {

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

        @Override
        public int getSourceActions(JComponent c) {
            System.out.println("ACTION");
            return DnDConstants.ACTION_MOVE;
        }

        @Override
        public boolean importData(TransferSupport support) {
            if (!canImport(support)) {
                return false;
            }
            // Check target component
            Transferable transferable = support.getTransferable();
            try {
                Trash trash = (Trash) transferable.getTransferData(TrashTransferable.FLAVOR);
                Object item = trash.getItem();
                System.out.println(">> Trash " + item);

                return true;
            } catch (UnsupportedFlavorException | IOException ex) {
                ex.printStackTrace();
            }
            return false;
        }

    }

    public class HListItemTransferHandler<T> extends TransferHandler {

        @Override
        protected Transferable createTransferable(JComponent component) {
            System.out.println("createTransferable");
            JList<T> list = (JList<T>) component;
            int index = list.getSelectedIndex();
            T transferredObject = list.getSelectedValue();
            return new TrashTransferable(new ListTrash<>(list, index, transferredObject));
        }

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

        @Override
        public int getSourceActions(JComponent c) {
            return DnDConstants.ACTION_MOVE;
        }

        @Override
        public boolean importData(TransferSupport info) {
            JList<Object> target = (JList<Object>) info.getComponent();
            JList.DropLocation dl = (JList.DropLocation) info.getDropLocation();
            DefaultListModel<Object> listModel = (DefaultListModel<Object>) target.getModel();
            int index = dl.getIndex();
            int max = listModel.getSize();

            if (index < 0 || index > max) {
                index = max;
            }

            try {
                Object object = info.getTransferable().getTransferData(DataFlavor.stringFlavor);
                listModel.add(index, object);
                target.addSelectionInterval(index, index);
                return true;
            } catch (UnsupportedFlavorException | IOException e) {
                e.printStackTrace();
            }
            return false;
        }

        @Override
        protected void exportDone(JComponent c, Transferable data, int action) {
            System.out.println("Export data");
            try {
                if (action != MOVE) {
                    return;
                }
                if (!(c instanceof JList)) {
                    return;
                }
                JList list = (JList) c;
                if (!(list.getModel() instanceof DefaultListModel)) {
                    return;
                }
                DefaultListModel model = (DefaultListModel) list.getModel();
                if (!(data instanceof TrashTransferable)) {
                    return;
                }
                Object transferData = data.getTransferData(TrashTransferable.FLAVOR);
                if (transferData == null || !(transferData instanceof Trash)) {
                    return;
                }
                Trash trash = (Trash) transferData;
                Object item = trash.item;
                int index = model.indexOf(item);
                if (index == -1) {
                    return;
                }
                model.remove(index);
            } catch (UnsupportedFlavorException | IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    public static class ListTrash<T> extends Trash<T> {

        private JList list;
        private int index;

        public ListTrash(JList list, int index, T item) {
            super(item);
            this.list = list;
            this.index = index;
        }

        public JList getList() {
            return list;
        }

        public int getIndex() {
            return index;
        }

    }

    public static class Trash<T> {

        private T item;

        public Trash(T item) {
            this.item = item;
        }

        public T getItem() {
            return item;
        }

    }

    public static class TrashTransferable<T> implements Transferable {

        public static final DataFlavor FLAVOR = new DataFlavor(Trash.class, "Trash");

        private Trash<T> trash;

        TrashTransferable(Trash<T> object) {
            trash = object;
        }

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

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

        @Override
        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
            return trash;
        }
    }
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thanks, that is really helpfull! Looks like I did not correctly understand the use of a Transferhandler I thought only one is required. This totally makes sense now seeing how you have done it. I really appreciate the time and effort you have put into this! I had another really weird Swing issue 9 days ago which was marked as not asked accurately enough by you and some others this was mainly why I wrote a wall of text this time. I still could not figure out what caused that Issue but thanks nevertheless! – Dinh Nov 12 '17 at 22:10