org.fife.ui.rtextarea.RTATextTransferHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.fife.ui.rtextarea.RTATextTransferHandler.java

Source

/*
 * 07/29/2004
 *
 * RTATextTransferHandler.java - Handles the transfer of data to/from an
 * RTextArea via drag-and-drop.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package org.fife.ui.rtextarea;

import java.awt.datatransfer.*;
import java.awt.im.InputContext;
import java.io.InputStream;
import java.io.IOException;
import java.io.Reader;
import java.io.StringBufferInputStream;
import java.io.StringReader;

import javax.swing.*;
import javax.swing.text.*;

import org.apache.commons.io.IOUtils;

/**
 * Handles the transfer of data to/from an <code>RTextArea</code> via
 * drag-and-drop.  This class is pretty much ripped off from a subclass of
 * <code>BasicTextUI</code>.  In the future, it will include the ability to
 * drag-and-drop files into <code>RTextArea</code>s (i.e., the text will be
 * inserted into the text area).<p>
 * 
 * The main reason this class is kept around is so we can subclass it.
 *
 * @author Robert Futrell
 * @version 0.1
 */
public class RTATextTransferHandler extends TransferHandler {

    private JTextComponent exportComp;
    private boolean shouldRemove;
    private int p0;
    private int p1;
    private boolean withinSameComponent;

    /**
     * Try to find a flavor that can be used to import a Transferable to a
     * specified text component.  
     * The set of usable flavors are tried in the following order:
     * <ol>
     *     <li>First, an attempt is made to find a flavor matching the content
     *         tyep of the EditorKit for the component.
     *     <li>Second, an attempt to find a text/plain flavor is made.
     *     <li>Third, an attempt to find a flavor representing a String
     *         reference in the same VM is made.
     *     <li>Lastly, DataFlavor.stringFlavor is searched for.
     * </ol>
     *
     * @param flavors The flavors to check if c will accept them.
     * @param c The text component to see whether it will accept any of the
     *        specified data flavors as input.
     */
    protected DataFlavor getImportFlavor(DataFlavor[] flavors, JTextComponent c) {

        DataFlavor refFlavor = null;
        DataFlavor stringFlavor = null;

        for (int i = 0; i < flavors.length; i++) {

            String mime = flavors[i].getMimeType();
            if (mime.startsWith("text/plain")) {
                return flavors[i];
            } else if (refFlavor == null && mime.startsWith("application/x-java-jvm-local-objectref")
                    && flavors[i].getRepresentationClass() == String.class) {
                refFlavor = flavors[i];
            } else if (stringFlavor == null && flavors[i].equals(DataFlavor.stringFlavor)) {
                stringFlavor = flavors[i];
            }

        }

        if (refFlavor != null)
            return refFlavor;
        else if (stringFlavor != null)
            return stringFlavor;

        return null;

    }

    /**
     * Import the given stream data into the text component.
     */
    protected void handleReaderImport(Reader in, JTextComponent c) throws BadLocationException, IOException {
        if (withinSameComponent) {
            ((RTextArea) c).beginAtomicEdit();
        }

        c.replaceSelection(IOUtils.toString(in));
    }

    /**
     * This is the type of transfer actions supported by the source.  Some
     * models are not mutable, so a transfer operation of COPY only should
     * be advertised in that case.
     * 
     * @param c  The component holding the data to be transfered.  This
     *  argument is provided to enable sharing of TransferHandlers by
     *  multiple components.
     * @return If the text component is editable, COPY_OR_MOVE is returned,
     *         otherwise just COPY is allowed.
     */
    @Override
    public int getSourceActions(JComponent c) {
        if (((JTextComponent) c).isEditable())
            return COPY_OR_MOVE;
        else
            return COPY;
    }

    /**
     * Create a Transferable to use as the source for a data transfer.
     *
     * @param comp  The component holding the data to be transfered.  This
     *  argument is provided to enable sharing of TransferHandlers by
     *  multiple components.
     * @return  The representation of the data to be transfered. 
     *  
     */
    @Override
    protected Transferable createTransferable(JComponent comp) {
        exportComp = (JTextComponent) comp;
        shouldRemove = true;
        p0 = exportComp.getSelectionStart();
        p1 = exportComp.getSelectionEnd();
        return (p0 != p1) ? (new TextTransferable(exportComp, p0, p1)) : null;
    }

    /**
     * This method is called after data has been exported.  This method should
     * remove the data that was transfered if the action was MOVE.
     *
     * @param source The component that was the source of the data.
     * @param data   The data that was transferred or possibly null
      *               if the action is <code>NONE</code>.
     * @param action The actual action that was performed.  
     */
    @Override
    protected void exportDone(JComponent source, Transferable data, int action) {
        // only remove the text if shouldRemove has not been set to
        // false by importData and only if the action is a move
        if (shouldRemove && action == MOVE) {
            TextTransferable t = (TextTransferable) data;
            t.removeText();
            if (withinSameComponent) {
                ((RTextArea) source).endAtomicEdit();
                withinSameComponent = false;
            }
        }
        exportComp = null;
        if (data instanceof TextTransferable) {
            ClipboardHistory.get().add(((TextTransferable) data).getPlainData());
        }
    }

    /**
     * This method causes a transfer to a component from a clipboard or a 
     * DND drop operation.  The Transferable represents the data to be
     * imported into the component.  
     *
     * @param comp  The component to receive the transfer.  This
     *  argument is provided to enable sharing of TransferHandlers by
     *  multiple components.
     * @param t The data to import
     * @return <code>true</code> iff the data was inserted into the component.
     */
    @Override
    public boolean importData(JComponent comp, Transferable t) {

        JTextComponent c = (JTextComponent) comp;
        withinSameComponent = c == exportComp;

        // if we are importing to the same component that we exported from
        // then don't actually do anything if the drop location is inside
        // the drag location and set shouldRemove to false so that exportDone
        // knows not to remove any data
        if (withinSameComponent && c.getCaretPosition() >= p0 && c.getCaretPosition() <= p1) {
            shouldRemove = false;
            return true;
        }

        boolean imported = false;
        DataFlavor importFlavor = getImportFlavor(t.getTransferDataFlavors(), c);
        if (importFlavor != null) {
            try {
                InputContext ic = c.getInputContext();
                if (ic != null)
                    ic.endComposition();
                Reader r = importFlavor.getReaderForText(t);
                handleReaderImport(r, c);
                imported = true;
            } catch (UnsupportedFlavorException ufe) {
                ufe.printStackTrace();
            } catch (BadLocationException ble) {
                ble.printStackTrace();
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }

        return imported;

    }

    /**
     * This method indicates if a component would accept an import of the
     * given set of data flavors prior to actually attempting to import it. 
     *
     * @param comp The component to receive the transfer.  This argument is
     *        provided to enable sharing of TransferHandlers by multiple
     *        components.
     * @param flavors The data formats available.
     * @return <code>true</code> iff the data can be inserted.
     */
    @Override
    public boolean canImport(JComponent comp, DataFlavor[] flavors) {
        JTextComponent c = (JTextComponent) comp;
        if (!(c.isEditable() && c.isEnabled()))
            return false;
        return (getImportFlavor(flavors, c) != null);
    }

    /**
     * A possible implementation of the Transferable interface for RTextAreas.
     */
    static class TextTransferable implements Transferable {

        Position p0;
        Position p1;
        JTextComponent c;

        protected String plainData;

        private static DataFlavor[] stringFlavors;
        private static DataFlavor[] plainFlavors;

        TextTransferable(JTextComponent c, int start, int end) {
            this.c = c;
            Document doc = c.getDocument();
            try {
                p0 = doc.createPosition(start);
                p1 = doc.createPosition(end);
                plainData = c.getSelectedText();
            } catch (BadLocationException ble) {
            }
        }

        /**
         * Fetch the data in a text/plain format.
         */
        protected String getPlainData() {
            return plainData;
        }

        /**
         * Returns an object which represents the data to be transferred.  The class 
         * of the object returned is defined by the representation class of the flavor.
         *
         * @param flavor the requested flavor for the data
         * @see DataFlavor#getRepresentationClass
         * @exception IOException                if the data is no longer available
         *              in the requested flavor.
         * @exception UnsupportedFlavorException if the requested data flavor is
         *              not supported.
         */
        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
            if (isPlainFlavor(flavor)) {
                String data = getPlainData();
                data = (data == null) ? "" : data;
                if (String.class.equals(flavor.getRepresentationClass())) {
                    return data;
                } else if (Reader.class.equals(flavor.getRepresentationClass())) {
                    return new StringReader(data);
                } else if (InputStream.class.equals(flavor.getRepresentationClass())) {
                    return new StringBufferInputStream(data);
                }
                // fall through to unsupported
            } else if (isStringFlavor(flavor)) {
                String data = getPlainData();
                data = (data == null) ? "" : data;
                return data;
            }
            throw new UnsupportedFlavorException(flavor);
        }

        /**
         * Returns an array of DataFlavor objects indicating the flavors the data 
         * can be provided in.  The array should be ordered according to preference
         * for providing the data (from most richly descriptive to least descriptive).
         *
         * @return an array of data flavors in which this data can be transferred
         */
        public DataFlavor[] getTransferDataFlavors() {

            int plainCount = (isPlainSupported()) ? plainFlavors.length : 0;
            int stringCount = (isPlainSupported()) ? stringFlavors.length : 0;
            int totalCount = plainCount + stringCount;
            DataFlavor[] flavors = new DataFlavor[totalCount];

            // fill in the array
            int pos = 0;
            if (plainCount > 0) {
                System.arraycopy(plainFlavors, 0, flavors, pos, plainCount);
                pos += plainCount;
            }
            if (stringCount > 0) {
                System.arraycopy(stringFlavors, 0, flavors, pos, stringCount);
                //pos += stringCount;
            }

            return flavors;

        }

        /**
         * Returns whether or not the specified data flavor is supported for
         * this object.
         * @param flavor the requested flavor for the data
         * @return boolean indicating whether or not the data flavor is supported
         */
        public boolean isDataFlavorSupported(DataFlavor flavor) {
            DataFlavor[] flavors = getTransferDataFlavors();
            for (int i = 0; i < flavors.length; i++) {
                if (flavors[i].equals(flavor))
                    return true;
            }
            return false;
        }

        /**
         * Returns whether or not the specified data flavor is an plain flavor that
         * is supported.
         * @param flavor the requested flavor for the data
         * @return boolean indicating whether or not the data flavor is supported
         */
        protected boolean isPlainFlavor(DataFlavor flavor) {
            DataFlavor[] flavors = plainFlavors;
            for (int i = 0; i < flavors.length; i++) {
                if (flavors[i].equals(flavor))
                    return true;
            }
            return false;
        }

        /**
         * Should the plain text flavors be offered?  If so, the method
         * getPlainData should be implemented to provide something reasonable.
         */
        protected boolean isPlainSupported() {
            return plainData != null;
        }

        /**
         * Returns whether or not the specified data flavor is a String flavor that
         * is supported.
         * @param flavor the requested flavor for the data
         * @return boolean indicating whether or not the data flavor is supported
         */
        protected boolean isStringFlavor(DataFlavor flavor) {
            DataFlavor[] flavors = stringFlavors;
            for (int i = 0; i < flavors.length; i++) {
                if (flavors[i].equals(flavor))
                    return true;
            }
            return false;
        }

        void removeText() {
            if ((p0 != null) && (p1 != null) && (p0.getOffset() != p1.getOffset())) {
                try {
                    Document doc = c.getDocument();
                    doc.remove(p0.getOffset(), p1.getOffset() - p0.getOffset());
                } catch (BadLocationException e) {
                }
            }
        }

        // Initialization of supported flavors.
        static {
            try {

                plainFlavors = new DataFlavor[3];
                plainFlavors[0] = new DataFlavor("text/plain;class=java.lang.String");
                plainFlavors[1] = new DataFlavor("text/plain;class=java.io.Reader");
                plainFlavors[2] = new DataFlavor("text/plain;charset=unicode;class=java.io.InputStream");

                stringFlavors = new DataFlavor[2];
                stringFlavors[0] = new DataFlavor(
                        DataFlavor.javaJVMLocalObjectMimeType + ";class=java.lang.String");
                stringFlavors[1] = DataFlavor.stringFlavor;

            } catch (ClassNotFoundException cle) {
                System.err.println("Error initializing org.fife.ui.RTATextTransferHandler");
            }
        }

    }

}