/* * 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.*; /** * 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 */ @SuppressWarnings("deprecation") 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 { char[] buff = new char[1024]; int nch; boolean lastWasCR = false; int last; StringBuilder sbuff = null; // Read in a block at a time, mapping \r\n to \n, as well as single // \r to \n. while ((nch = in.read(buff, 0, buff.length)) != -1) { if (sbuff == null) { sbuff = new StringBuilder(nch); } last = 0; for (int counter = 0; counter < nch; counter++) { switch (buff[counter]) { case '\r': if (lastWasCR) { if (counter == 0) sbuff.append('\n'); else buff[counter - 1] = '\n'; } else lastWasCR = true; break; case '\n': if (lastWasCR) { if (counter > (last + 1)) sbuff.append(buff, last, counter - last - 1); // else nothing to do, can skip \r, next write will // write \n lastWasCR = false; last = counter; } break; default: if (lastWasCR) { if (counter == 0) sbuff.append('\n'); else buff[counter - 1] = '\n'; lastWasCR = false; } break; } // End fo switch (buff[counter]). } // End of for (int counter = 0; counter < nch; counter++). if (last < nch) { if (lastWasCR) { if (last < (nch - 1)) sbuff.append(buff, last, nch - last - 1); } else sbuff.append(buff, last, nch - last); } } // End of while ((nch = in.read(buff, 0, buff.length)) != -1). if (withinSameComponent) { ((RTextArea)c).beginAtomicEdit(); } if (lastWasCR) sbuff.append('\n'); c.replaceSelection(sbuff != null ? sbuff.toString() : ""); } /** * 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"); } } } }