
package uk.co.wingpath.gui;

import javax.swing.*;
import javax.swing.table.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import java.awt.event.*;

/**
* Transfer handler for drag & drop of table rows.
* <p>Adapted from
*  http://stackoverflow.com/questions/638807/how-do-i-drag-and-drop-a-row-in-a-jtable
* with the addition of keyboard support (ctrl-X and ctrl-V) for moving,
* moving of multiple rows, and a couple of bug fixes.
*/
public class TableRowTransferHandler
    extends TransferHandler
{
    private final JTable table;
    private final DataFlavor dataFlavor;
    private boolean validClip;

    private static class RowRange
    {
        int first;
        int last;

        RowRange (int first, int last)
        {
            this.first = first;
            this.last = last;
        }
    }

    private class RowRangeTransferable
        implements Transferable
    {
        private RowRange rowRange;

        public RowRangeTransferable (RowRange rowRange)
        {
            this.rowRange = rowRange;
        }

        @Override
        public Object getTransferData (DataFlavor flavor)
            throws UnsupportedFlavorException
        {
            if (flavor != dataFlavor)
                throw new UnsupportedFlavorException (flavor);
            return rowRange;
        }

        @Override
        public DataFlavor [] getTransferDataFlavors ()
        {
            DataFlavor [] flavors = new DataFlavor [1];
            flavors [0] = dataFlavor;
            return flavors;
        }

        @Override
        public boolean isDataFlavorSupported (DataFlavor flavor)
        {
            return flavor == dataFlavor;
        }
    }

    /**
    * Constructs a transfer handler for the supplied table.
    * <p>The table model must implement the {@link Reorderable} interface
    * to do the actual moving of table rows.
    * @param table the table
    */
    private TableRowTransferHandler (JTable table)
    {
        TableModel model = table.getModel ();
        if (!(model instanceof Reorderable))
        {
            throw new IllegalArgumentException (
                "Table must implement Reorderable");
        }
        this.table = table;
        dataFlavor = new DataFlavor (RowRange.class,
            "Integer Row Indices");
        validClip = false;

        // Set up Ctrl-X and Ctrl-V to perform cut and paste actions
        // respectively. The actions are already in the ActionMap (provided
        // by Swing).
        InputMap inputMap =
            table.getInputMap (JComponent.WHEN_IN_FOCUSED_WINDOW);
        inputMap.put (
            KeyStroke.getKeyStroke (KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK),
            "cut");
        inputMap.put (
            KeyStroke.getKeyStroke (KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK),
            "paste");

        // Enable drag & drop.
        table.setDragEnabled (true);
        table.setDropMode (DropMode.INSERT_ROWS);

        // Listen for row insertions & deletions so that we can invalidate
        // any clip made using Ctrl-X.
        // Note that this includes any moves done via this transfer handler,
        // so Ctrl-V can only be used once for each use of Ctrl-X.
        model.addTableModelListener (
            new TableModelListener ()
            {
                public void tableChanged (TableModelEvent e)
                {
                    validClip = false;
                }
            });
    }

    /**
    * Sets a transfer handler on the specified table.
    * <p>The table model must implement the {@link Reorderable} interface
    * to do the actual moving of table rows.
    * @param table table.
    */
    public static void setHandler (JTable table)
    {
        table.setTransferHandler (new TableRowTransferHandler (table));
    }

    //--------- Export methods ---------------

    @Override
    public int getSourceActions (JComponent c)
    {
        // Only moves are supported.
        return TransferHandler.MOVE;
    }

    @Override
    protected Transferable createTransferable (JComponent c)
    {
        assert c == table;

        // Since we are only supporting moves of rows within the table, the
        // only data we need to export are the row indices.
        int [] rows = table.getSelectedRows ();
        if (rows.length == 0)
            return null;

        // Check that the selected rows are contiguous.
        for (int i = 1 ; i < rows.length ; i++)
        {
            if (rows [i] != rows [i - 1] + 1)
                return null;
        }

        validClip = true;
        RowRange range = new RowRange (rows [0], rows [rows.length - 1]);
        return new RowRangeTransferable (range);
    }

    @Override
    protected void exportDone (JComponent c, Transferable t, int act)
    {
        // The actual move of the row is done by the importData method, so
        // the only cleanup required is resetting the cursor.
        if (act == TransferHandler.MOVE || act == TransferHandler.NONE)
        {
            table.setCursor (
                Cursor.getPredefinedCursor (Cursor.DEFAULT_CURSOR));
        }
    }

    //--------- Import methods ---------------

    @Override
    public boolean canImport (TransferHandler.TransferSupport info)
    {
        assert (JTable) info.getComponent () == table;
        // According to the Java API, this method is only called during
        // dragging (and not before pasting). So 'info.isDrop ()' will
        // presumably always be true - but test it anyway, just in case.
        boolean b = validClip && info.isDrop () &&
            info.isDataFlavorSupported (dataFlavor);
        if (b)
        {
            JTable.DropLocation dl =
                (JTable.DropLocation) info.getDropLocation ();
            int toRow = dl.getRow ();
            try
            {
                Transferable trans = info.getTransferable ();
                RowRange range = (RowRange) trans.getTransferData (dataFlavor);
                if (toRow >= range.first && toRow <= range.last)
                    b = false;
            }
            catch (Exception e)
            {
                b = false;
            }
        }
        // Change the cursor even if we are returning false. We want to display
        // the "no drop" cursor if the user tries to drag something
        // inappropriate onto the table.
        table.setCursor (b ? DragSource.DefaultMoveDrop :
            DragSource.DefaultMoveNoDrop);
        return b;
    }

    @Override
    public boolean importData (TransferHandler.TransferSupport info)
    {
        assert (JTable) info.getComponent () == table;
        if (!(validClip && info.isDataFlavorSupported (dataFlavor)))
            return false;
        int toRow;
        if (info.isDrop ())
        {
            // Drop action.
            JTable.DropLocation dl =
                (JTable.DropLocation) info.getDropLocation ();
            toRow = dl.getRow ();
        }
        else
        {
            // Paste action.
            toRow = table.getSelectedRow () + 1;
        }
        int max = table.getModel ().getRowCount();
        if (toRow < 0 || toRow > max)
            toRow = max;
        table.setCursor (Cursor.getPredefinedCursor (Cursor.DEFAULT_CURSOR));
        try
        {
            Transferable trans = info.getTransferable ();
            RowRange range = (RowRange) trans.getTransferData (dataFlavor);
            if (toRow >= range.first && toRow <= range.last)
                return false;
            ((Reorderable) table.getModel ()).reorder (range.first, range.last,
                toRow);
            // Update the range so that it still refers to the same data, in
            // case the user pastes it again.
            if (toRow > range.first)
                toRow -= range.last - range.first + 1;
            range.last = toRow + (range.last - range.first);
            range.first = toRow;
            // Select the rows in their new position.
            table.getSelectionModel ().setSelectionInterval (range.first,
                range.last);
            return true;
        }
        catch (Exception e)
        {
            e.printStackTrace ();
        }
        return false;
    }
}

