Index: openide/src/org/openide/explorer/view/ExplorerDragSupport.java @@ -213,4 +213,18 @@ */ abstract Node[] obtainNodes (DragGestureEvent dge); + + // <> + // We need to let some top components listen to the drags so + // they can for example clear explorer selection. + // See bug 5048028. + public void addDragSourceListener(DragSourceListener dsl) { + getDefaultGestureRecognizer().getDragSource().addDragSourceListener(dsl); + } + public void removeDragSourceListener(DragSourceListener dsl) { + getDefaultGestureRecognizer().getDragSource().removeDragSourceListener(dsl); + } + // + + } /* end class ExplorerDragSupport */ Index: openide/src/org/openide/explorer/view/TreeView.java @@ -16,6 +16,9 @@ import java.awt.*; import java.awt.dnd.DnDConstants; +// <> +import java.awt.dnd.DragSourceListener; +// import java.awt.dnd.Autoscroll; import java.awt.event.*; import java.beans.PropertyChangeEvent; @@ -350,6 +353,34 @@ } } + // <> + // We need to let some top components listen to the drags so + // they can for example clear explorer selection. + // See bug 5048028. + /** + * Add a drag source listener. + * @param dsl The drag source listener to be added + */ + public void addDragSourceListener(DragSourceListener dsl) { + if (!dragActive) { + setDragSource(true); + } + dragSupport.addDragSourceListener(dsl); + } + /** + * Remove a drag source listener. + * @param dsl The drag source listener to be removed + */ + public void removeDragSourceListener(DragSourceListener dsl) { + if (dragSupport != null) { + dragSupport.removeDragSourceListener(dsl); + } + } + // + + + + /** Drop support is enabled by default. * @return true if dropping to the view is enabled, false * otherwise
Index: openide/src/org/openide/text/CloneableEditor.java @@ -198,6 +198,21 @@ pane.setEditorKit (support.kit ()); pane.setDocument (doc); +// <> Temporary put back (to fix a bug), +// when is figured out another method to hack this (at least from editor module). +// Modified for by Tor: install drag & drop handlers on the editor + // Enable drag & drop +// pane.setDragEnabled(true); // seems not necessary + pane.setTransferHandler(new TextTransferHandler()); + if (pane.getDropTarget() != null) { + TextDropTargetListener dropTargetListener = new TextDropTargetListener(); + try { + pane.getDropTarget().addDropTargetListener(dropTargetListener); + } catch(java.util.TooManyListenersException tmle) { + ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, tmle); + } + } +// if (doc instanceof NbDocument.CustomEditor) { NbDocument.CustomEditor ce = (NbDocument.CustomEditor)doc; customComponent = ce.createEditor(pane); Index: openide/src/org/openide/text/TextDropTargetListener.java @@ -0,0 +1,326 @@ +/* + * Sun Public License Notice + * + * The contents of this file are subject to the Sun Public License + * Version 1.0 (the "License"). You may not use this file except in + * compliance with the License. A copy of the License is available at + * http://www.sun.com/ + * + * The Original Code is NetBeans. The Initial Developer of the Original + * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun + * Microsystems, Inc. All Rights Reserved. + */ + +package org.openide.text; + +import java.awt.*; +import java.awt.event.*; +import java.awt.font.*; +import java.awt.datatransfer.*; +import java.awt.dnd.*; +import java.beans.*; +import java.io.*; +import java.net.*; +import javax.swing.*; +import javax.swing.plaf.*; +import javax.swing.text.*; +import javax.swing.event.*; +import javax.swing.border.Border; +import javax.swing.plaf.UIResource; +import javax.swing.Timer; + +/* A drop listener for text components. This seems necessary because + * the NetBeans editor doesn't inherit from the Swing plaf.basic package, + * so it's missing a bunch of drag & drop behavior. + * In particular, this class is responsible for moving the caret + * to the nearest drag location in the document, as well as autoscrolling + * the view when necesary. + *

+ * This code is basically a merged version of text-related code in + * javax.swing.plaf.basic: BasicTextUI's TextDropTargetListener and + * its parent class, BasickDropTargetListener. + *

+ * I had to copy it since it has package protected access in + * javax.swing.plaf.basic. + * + *

+ * @author Tor Norbye + */ +class TextDropTargetListener implements DropTargetListener, + UIResource, ActionListener { + + // The first code is the general BasicDropTargetListener code; at the + // end you'll find the TextDropTargetListener code + + + /** + * construct a DropTargetAutoScroller + *

+ * @param c the Component + * @param p the Point + */ + protected TextDropTargetListener() { + } + + + /** + * Update the geometry of the autoscroll region. The geometry is + * maintained as a pair of rectangles. The region can cause + * a scroll if the pointer sits inside it for the duration of the + * timer. The region that causes the timer countdown is the area + * between the two rectangles. + *

+ * This is implemented to use the visible area of the component + * as the outer rectangle and the insets are based upon the + * Scrollable information (if any). If the Scrollable is + * scrollable along an axis, the step increment is used as + * the autoscroll inset. If the component is not scrollable, + * the insets will be zero (i.e. autoscroll will not happen). + */ + void updateAutoscrollRegion(JComponent c) { + // compute the outer + Rectangle visible = c.getVisibleRect(); + outer.reshape(visible.x, visible.y, visible.width, visible.height); + + // compute the insets + // TBD - the thing with the scrollable + Insets i = new Insets(0, 0, 0, 0); + if (c instanceof Scrollable) { + Scrollable s = (Scrollable) c; + i.left = s.getScrollableUnitIncrement(visible, SwingConstants.HORIZONTAL, 1); + i.top = s.getScrollableUnitIncrement(visible, SwingConstants.VERTICAL, 1); + i.right = s.getScrollableUnitIncrement(visible, SwingConstants.HORIZONTAL, -1); + i.bottom = s.getScrollableUnitIncrement(visible, SwingConstants.VERTICAL, -1); + } + + // set the inner from the insets + inner.reshape(visible.x + i.left, + visible.y + i.top, + visible.width - (i.left + i.right), + visible.height - (i.top + i.bottom)); + } + + /** + * Perform an autoscroll operation. This is implemented to scroll by the + * unit increment of the Scrollable using scrollRectToVisible. If the + * cursor is in a corner of the autoscroll region, more than one axis will + * scroll. + */ + void autoscroll(JComponent c, Point pos) { + if (c instanceof Scrollable) { + Scrollable s = (Scrollable) c; + if (pos.y < inner.y) { + // scroll top downward + int dy = s.getScrollableUnitIncrement(outer, SwingConstants.VERTICAL, 1); + Rectangle r = new Rectangle(inner.x, outer.y - dy, inner.width, dy); + c.scrollRectToVisible(r); + } else if (pos.y > (inner.y + inner.height)) { + // scroll bottom upward + int dy = s.getScrollableUnitIncrement(outer, SwingConstants.VERTICAL, -1); + Rectangle r = new Rectangle(inner.x, outer.y + outer.height, inner.width, dy); + c.scrollRectToVisible(r); + } + + if (pos.x < inner.x) { + // scroll left side to the right + int dx = s.getScrollableUnitIncrement(outer, SwingConstants.HORIZONTAL, 1); + Rectangle r = new Rectangle(outer.x - dx, inner.y, dx, inner.height); + c.scrollRectToVisible(r); + } else if (pos.x > (inner.x + inner.width)) { + // scroll right side to the left + int dx = s.getScrollableUnitIncrement(outer, SwingConstants.HORIZONTAL, -1); + Rectangle r = new Rectangle(outer.x + outer.width, inner.y, dx, inner.height); + c.scrollRectToVisible(r); + } + } + } + + /** + * Initializes the internal properties if they haven't been already + * inited. This is done lazily to avoid loading of desktop properties. + */ + private void initPropertiesIfNecessary() { + if (timer == null) { + Toolkit t = Toolkit.getDefaultToolkit(); + Integer initial = new Integer(100); + Integer interval = new Integer(100); + + try { + initial = (Integer)t.getDesktopProperty( + "DnD.Autoscroll.initialDelay"); + } catch (Exception e) { + // ignore + } + try { + interval = (Integer)t.getDesktopProperty( + "DnD.Autoscroll.interval"); + } catch (Exception e) { + // ignore + } + timer = new Timer(interval.intValue(), this); + + timer.setCoalesce(true); + timer.setInitialDelay(initial.intValue()); + + try { + hysteresis = ((Integer)t.getDesktopProperty( + "DnD.Autoscroll.cursorHysteresis")).intValue(); + } catch (Exception e) { + // ignore + } + } + } + + static JComponent getComponent(DropTargetEvent e) { + DropTargetContext context = e.getDropTargetContext(); + return (JComponent) context.getComponent(); + } + + // --- ActionListener methods -------------------------------------- + + /** + * The timer fired, perform autoscroll if the pointer is within the + * autoscroll region. + *

+ * @param e the ActionEvent + */ + public synchronized void actionPerformed(ActionEvent e) { + updateAutoscrollRegion(component); + if (outer.contains(lastPosition) && !inner.contains(lastPosition)) { + autoscroll(component, lastPosition); + } + } + + // --- DropTargetListener methods ----------------------------------- + + public void dragEnter(DropTargetDragEvent e) { + component = getComponent(e); + TransferHandler th = component.getTransferHandler(); + canImport = th.canImport(component, e.getCurrentDataFlavors()); + if (canImport) { + saveComponentState(component); + lastPosition = e.getLocation(); + updateAutoscrollRegion(component); + initPropertiesIfNecessary(); + } + } + + public void dragOver(DropTargetDragEvent e) { + if (canImport) { + Point p = e.getLocation(); + updateInsertionLocation(component, p); + + + // check autoscroll + synchronized(this) { + if (Math.abs(p.x - lastPosition.x) > hysteresis || + Math.abs(p.y - lastPosition.y) > hysteresis) { + // no autoscroll + if (timer.isRunning()) timer.stop(); + } else { + if (!timer.isRunning()) timer.start(); + } + lastPosition = p; + } + } + } + + public void dragExit(DropTargetEvent e) { + if (canImport) { + restoreComponentState(component); + } + cleanup(); + } + + public void drop(DropTargetDropEvent e) { + if (canImport) { + restoreComponentStateForDrop(component); + } + cleanup(); + } + + public void dropActionChanged(DropTargetDragEvent e) { + } + + /** + * Cleans up internal state after the drop has finished (either succeeded + * or failed). + */ + private void cleanup() { + if (timer != null) { + timer.stop(); + } + component = null; + lastPosition = null; + } + + // --- fields -------------------------------------------------- + + private Timer timer; + private Point lastPosition; + private Rectangle outer = new Rectangle(); + private Rectangle inner = new Rectangle(); + private int hysteresis = 10; + private boolean canImport; + + /** + * The current component. The value is cached from the drop events and used + * by the timer. When a drag exits or a drop occurs, this value is cleared. + */ + private JComponent component; + + + + + // TEXT DROP LISTENER SPECIFIC STUFF - from BasicTextUI's + // TextDropListener + + + /** + * called to save the state of a component in case it needs to + * be restored because a drop is not performed. + */ + protected void saveComponentState(JComponent comp) { + JTextComponent c = (JTextComponent) comp; + Caret caret = c.getCaret(); + dot = caret.getDot(); + mark = caret.getMark(); + visible = caret.isVisible(); + caret.setVisible(true); + } + + /** + * called to restore the state of a component + * because a drop was not performed. + */ + protected void restoreComponentState(JComponent comp) { + JTextComponent c = (JTextComponent) comp; + Caret caret = c.getCaret(); + caret.setDot(mark); + caret.moveDot(dot); + caret.setVisible(visible); + } + + /** + * called to restore the state of a component + * because a drop was performed. + */ + protected void restoreComponentStateForDrop(JComponent comp) { + JTextComponent c = (JTextComponent) comp; + Caret caret = c.getCaret(); + caret.setVisible(visible); + } + + /** + * called to set the insertion location to match the current + * mouse pointer coordinates. + */ + protected void updateInsertionLocation(JComponent comp, Point p) { + JTextComponent c = (JTextComponent) comp; + c.setCaretPosition(c.viewToModel(p)); + } + + int dot; + int mark; + boolean visible; +} Index: openide/src/org/openide/text/TextTransferHandler.java @@ -0,0 +1,748 @@ +/* + * Sun Public License Notice + * + * The contents of this file are subject to the Sun Public License + * Version 1.0 (the "License"). You may not use this file except in + * compliance with the License. A copy of the License is available at + * http://www.sun.com/ + * + * The Original Code is NetBeans. The Initial Developer of the Original + * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun + * Microsystems, Inc. All Rights Reserved. + */ + +package org.openide.text; + +import java.awt.*; +import java.awt.event.*; +import java.awt.font.*; +import java.awt.datatransfer.*; +import java.awt.dnd.*; +import java.beans.*; +import java.io.*; +import java.net.*; +import javax.swing.*; +import javax.swing.plaf.*; +import javax.swing.text.*; +import javax.swing.event.*; +import javax.swing.border.Border; +import javax.swing.plaf.UIResource; +import javax.swing.Timer; + +import org.openide.windows.TopComponent; + + +/* A transfer handler for the editor component. This seems necessary because + * the NetBeans editor doesn't inherit from the Swing plaf.basic package, + * so it's missing a bunch of drag & drop behavior. + *

+ * This code is basically a merged version of text-related code in + * javax.swing.plaf.basic: BasicTextUI, BasicTransferable, ... + * I had to copy it since it has package protected access in + * javax.swing.plaf.basic. + *

+ * There is one important difference. In order to allow the DESTINATION + * to decide if the transferable should be moved or copied (e.g. if the + * destination is the same document, move, if it's the clipboard palette, + * copy), there's a global flag that can be set which basically turns off + * moving when a drag is in progress. Yup, this is a bit of a hack, but + * I couldn't find a better way. With Swing, the copy-vs-move decision is + * made when the drag is -started-, and at that point we don't know yet + * where you're going to drop. The docs for TransferHandler exportAsDrag says: + *

action - the transfer action initially requested; this should + * be a value of either COPY or MOVE; + * the value may be changed during the course of the drag operation + *
. + * However, it does not say HOW you can change the action, and from looking + * at the code, I suspect it cannot be done, since the action is passed + * to a gesture listener that has private access. + *

+ * @author Tor Norbye + */ + +public class TextTransferHandler extends TransferHandler implements UIResource { + + /** Flag which is only defined during a drag & drop operation. + * Clients (typically drop zones) can set it to true to indicate + * that the data being dragged should be copied, not moved. + * For example, the clipboard viewer sets this when it handles + * the import. That way, the exportDone method knows not to remove + * the text being placed on the clipboard from the document, since + * text dragging (without modified keys) defaults to moving, not + * copying. And we don't want to disallow copying in getSourceActions, + * since dragging text from one place in the document to another + * SHOULD be moved, not copied. */ + public static boolean dontRemove = false; + + + private JTextComponent exportComp; + private boolean shouldRemove; + private int p0; + private int p1; + + /** + * Try to find a flavor that can be used to import a Transferable. + * The set of usable flavors are tried in the following order: + *

    + *
  1. First, an attempt is made to find a flavor matching the content type + * of the EditorKit for the component. + *
  2. Second, an attempt to find a text/plain flavor is made. + *
  3. Third, an attempt to find a flavor representing a String reference + * in the same VM is made. + *
  4. Lastly, DataFlavor.stringFlavor is searched for. + *
+ */ + protected DataFlavor getImportFlavor(DataFlavor[] flavors, JTextComponent c) { + DataFlavor plainFlavor = null; + DataFlavor refFlavor = null; + DataFlavor stringFlavor = null; + if (c instanceof JEditorPane) { + for (int i = 0; i < flavors.length; i++) { + String mime = flavors[i].getMimeType(); + if (mime.startsWith(((JEditorPane)c).getEditorKit().getContentType())) { + return flavors[i]; + } else if (plainFlavor == null && mime.startsWith("text/plain")) { + plainFlavor = flavors[i]; + } else if (refFlavor == null && mime.startsWith("application/x-java-jvm-local-objectref") + && flavors[i].getRepresentationClass() == java.lang.String.class) { + refFlavor = flavors[i]; + } else if (stringFlavor == null && flavors[i].equals(DataFlavor.stringFlavor)) { + stringFlavor = flavors[i]; + } + } + if (plainFlavor != null) { + return plainFlavor; + } else if (refFlavor != null) { + return refFlavor; + } else if (stringFlavor != null) { + return stringFlavor; + } + return 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() == java.lang.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, boolean useRead) + throws BadLocationException, IOException { + if (useRead) { + int startPosition = c.getSelectionStart(); + int endPosition = c.getSelectionEnd(); + int length = endPosition - startPosition; + EditorKit kit = c.getUI().getEditorKit(c); + Document doc = c.getDocument(); + if (length > 0) { + doc.remove(startPosition, length); + } + kit.read(in, doc, startPosition); + } else { + char[] buff = new char[1024]; + int nch; + boolean lastWasCR = false; + int last; + StringBuffer 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 StringBuffer(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; + } + } + if (last < nch) { + if (lastWasCR) { + if (last < (nch - 1)) { + sbuff.append(buff, last, nch - last - 1); + } + } else { + sbuff.append(buff, last, nch - last); + } + } + } + if (lastWasCR) { + sbuff.append('\n'); + } + c.replaceSelection(sbuff != null ? sbuff.toString() : ""); + } + } + + // --- TransferHandler methods ------------------------------------ + + /** + * 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 This is implemented to return NONE if the component is a JPasswordField + * since exporting data via user gestures is not allowed. If the text component is + * editable, COPY_OR_MOVE is returned, otherwise just COPY is allowed. + */ + public int getSourceActions(JComponent c) { + int actions = NONE; + if (! (c instanceof JPasswordField)) { + if (((JTextComponent)c).isEditable()) { + actions = COPY_OR_MOVE; + } else { + actions = COPY; + } + } + return actions; + } + + /** + * 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. + * + */ + protected Transferable createTransferable(JComponent comp) { + exportComp = (JTextComponent)comp; + shouldRemove = true; + dontRemove = false; + 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 NONE. + * @param action The actual action that was performed. + */ + 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; + if (!dontRemove) { + t.removeText(); + } + } + + exportComp = null; + } + + /** + * 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 true if the data was inserted into the component, false otherwise. + */ + public boolean importData(JComponent comp, Transferable t) { + JTextComponent c = (JTextComponent)comp; + + // 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 (c == exportComp && c.getCaretPosition() >= p0 && c.getCaretPosition() <= p1) { + shouldRemove = false; + return true; + } + + boolean imported = false; + DataFlavor importFlavor = getImportFlavor(t.getTransferDataFlavors(), c); + if (importFlavor != null) { + try { + boolean useRead = false; + if (comp instanceof JEditorPane) { + JEditorPane ep = (JEditorPane)comp; + if (!ep.getContentType().startsWith("text/plain") && + importFlavor.getMimeType().startsWith(ep.getContentType())) { + useRead = true; + + } + + try { + // XXX Hacking the code clips transfer. + Runnable r = (Runnable)t.getTransferData(CodeClipTransferData.CODE_CLIP_DATA_FLAVOR); + if(r != null) { + r.run(); + } + } catch(Exception e) { +// e.printStackTrace(); + } + } + Reader r = importFlavor.getReaderForText(t); + handleReaderImport(r, c, useRead); + imported = true; + + // #4946925 Trying to put the activation to the drop target. + TopComponent tc = (TopComponent)SwingUtilities.getAncestorOfClass(TopComponent.class, c); + if(tc != null) { + tc.requestActive(); + } + } catch (UnsupportedFlavorException ufe) { + } catch (BadLocationException ble) { + } catch (IOException ioe) { + } + } + 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 true if the data can be inserted into the component, false otherwise. + */ + 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 text components. For a JEditorPane with a rich set + * of EditorKit implementations, conversions could be made + * giving a wider set of formats. This is implemented to + * offer up only the active content type and text/plain + * (if that is not the active format) since that can be + * extracted from other formats. + */ + static class TextTransferable implements Transferable, UIResource { + + // begin copied from BasicTransferable + protected String plainData = null; + protected String htmlData = null; + + private static DataFlavor[] htmlFlavors; + private static DataFlavor[] stringFlavors; + private static DataFlavor[] plainFlavors; + + static { + try { + htmlFlavors = new DataFlavor[3]; + htmlFlavors[0] = new DataFlavor("text/html;class=java.lang.String"); + htmlFlavors[1] = new DataFlavor("text/html;class=java.io.Reader"); + htmlFlavors[2] = new DataFlavor("text/html;charset=unicode;class=java.io.InputStream"); + + 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 javax.swing.plaf.basic.BasicTranserable"); + } + } + + /** + * 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() { + DataFlavor[] richerFlavors = getRicherFlavors(); + int nRicher = (richerFlavors != null) ? richerFlavors.length : 0; + int nHTML = (isHTMLSupported()) ? htmlFlavors.length : 0; + int nPlain = (isPlainSupported()) ? plainFlavors.length: 0; + int nString = (isPlainSupported()) ? stringFlavors.length : 0; + int nFlavors = nRicher + nHTML + nPlain + nString; + DataFlavor[] flavors = new DataFlavor[nFlavors]; + + // fill in the array + int nDone = 0; + if (nRicher > 0) { + System.arraycopy(richerFlavors, 0, flavors, nDone, nRicher); + nDone += nRicher; + } + if (nHTML > 0) { + System.arraycopy(htmlFlavors, 0, flavors, nDone, nHTML); + nDone += nHTML; + } + if (nPlain > 0) { + System.arraycopy(plainFlavors, 0, flavors, nDone, nPlain); + nDone += nPlain; + } + if (nString > 0) { + System.arraycopy(stringFlavors, 0, flavors, nDone, nString); + nDone += nString; + } + 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 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 { + DataFlavor[] richerFlavors = getRicherFlavors(); + if (isRicherFlavor(flavor)) { + return getRicherData(flavor); + } else if (isHTMLFlavor(flavor)) { + String data = getHTMLData(); + 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 (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); + } + + // --- richer subclass flavors ---------------------------------------------- + + protected boolean isRicherFlavor(DataFlavor flavor) { + DataFlavor[] richerFlavors = getRicherFlavors(); + int nFlavors = (richerFlavors != null) ? richerFlavors.length : 0; + for (int i = 0; i < nFlavors; i++) { + if (richerFlavors[i].equals(flavor)) { + return true; + } + } + return false; + } + + // --- html flavors ---------------------------------------------------------- + + /** + * Returns whether or not the specified data flavor is an HTML 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 isHTMLFlavor(DataFlavor flavor) { + DataFlavor[] flavors = htmlFlavors; + for (int i = 0; i < flavors.length; i++) { + if (flavors[i].equals(flavor)) { + return true; + } + } + return false; + } + + /** + * Should the HTML flavors be offered? If so, the method + * getHTMLData should be implemented to provide something reasonable. + */ + protected boolean isHTMLSupported() { + return htmlData != null; + } + + /** + * Fetch the data in a text/html format + */ + protected String getHTMLData() { + return htmlData; + } + + // --- plain text flavors ---------------------------------------------------- + + /** + * 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; + } + + /** + * Fetch the data in a text/plain format. + */ + protected String getPlainData() { + return plainData; + } + + // --- string flavorss -------------------------------------------------------- + + /** + * 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; + } + + // end copied from BasicTransferable + + + 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(); + + if (c instanceof JEditorPane) { + JEditorPane ep = (JEditorPane)c; + + mimeType = ep.getContentType(); + + if (mimeType.startsWith("text/plain")) { + return; + } + + StringWriter sw = new StringWriter(p1.getOffset() - p0.getOffset()); + ep.getEditorKit().write(sw, doc, p0.getOffset(), p1.getOffset() - p0.getOffset()); + + if (mimeType.startsWith("text/html")) { + htmlData = sw.toString(); + } else { + richText = sw.toString(); + } + } + } catch (BadLocationException ble) { + } catch (IOException ioe) { + } + } + + 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) { + } + } + } + + // ---- EditorKit other than plain or HTML text ----------------------- + + /** + * If the EditorKit is not for text/plain or text/html, that format + * is supported through the "richer flavors" part of BasicTransferable. + */ + protected DataFlavor[] getRicherFlavors() { + if (richText == null) { + return null; + } + + try { + DataFlavor[] flavors = new DataFlavor[3]; + flavors[0] = new DataFlavor(mimeType + ";class=java.lang.String"); + flavors[1] = new DataFlavor(mimeType + ";class=java.io.Reader"); + flavors[2] = new DataFlavor(mimeType + ";class=java.io.InputStream;charset=unicode"); + return flavors; + } catch (ClassNotFoundException cle) { + // fall through to unsupported (should not happen) + } + + return null; + } + + /** + * The only richer format supported is the file list flavor + */ + protected Object getRicherData(DataFlavor flavor) throws UnsupportedFlavorException { + if (richText == null) { + return null; + } + + if (String.class.equals(flavor.getRepresentationClass())) { + return richText; + } else if (Reader.class.equals(flavor.getRepresentationClass())) { + return new StringReader(richText); + } else if (InputStream.class.equals(flavor.getRepresentationClass())) { + return new StringBufferInputStream(richText); + } + throw new UnsupportedFlavorException(flavor); + } + + Position p0; + Position p1; + String mimeType; + String richText; + JTextComponent c; + } + + // XXX Hack to enable poping up a dialog when DnD of code clip. + public static class CodeClipTransferData extends StringSelection { + + // XXX Fake DataFlavor.. for cheating to retrieve the callback. + public static final DataFlavor CODE_CLIP_DATA_FLAVOR + = new DataFlavor(CodeClipTransferData.class, CodeClipTransferData.class.getName()); // TEMP + + // XXX Callback to provide the popup. + private Runnable callback; + // XXX We need to manipulate the data in this class (the superclass is private). + private String data; + + + public CodeClipTransferData(String data) { + super(data); // Just fake. + this.data = data; + } + + + public void setCallback(Runnable callback) { + this.callback = callback; + } + + public void resetData(String data) { + this.data = data; + } + + // XXX Overriden to manipulate with our data field and provide the hacking callback. + public Object getTransferData(DataFlavor flavor) + throws UnsupportedFlavorException, IOException { + if(flavor == CODE_CLIP_DATA_FLAVOR) { + return callback; + } + + // JCK Test StringSelection0007: if 'flavor' is null, throw NPE + if (flavor.equals(DataFlavor.stringFlavor)) { + return (Object)data; + } else if (flavor.equals(DataFlavor.plainTextFlavor)) { // deprecated + return new StringReader(data); + } else { + throw new UnsupportedFlavorException(flavor); + } + } + + } +} Index: editor/libsrc/org/netbeans/editor/BaseCaret.java @@ -35,9 +35,9 @@ import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; import java.io.IOException; -import javax.swing.Action; -import javax.swing.Timer; -import javax.swing.SwingUtilities; +// <> +import javax.swing.*; +// import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.JTextComponent; @@ -49,6 +49,9 @@ import javax.swing.event.EventListenerList; import javax.swing.text.AbstractDocument; import javax.swing.text.Position; +// <> +import sun.awt.dnd.SunDragSourceContextPeer; +// import org.netbeans.api.editor.fold.Fold; import org.netbeans.api.editor.fold.FoldHierarchy; import org.netbeans.api.editor.fold.FoldHierarchyEvent; @@ -58,8 +61,25 @@ /** * Caret implementation * -* @author Miloslav Metelka -* @version 1.00 +*

+* Modified for by Tor: added drag & drop handling in the mouse +* listeners: specifically, recognize drag gestures and initiate drags +* when recognized. Did this by merging in code from +* BasicDragGestureRecognizer.java and BaseTextUI.java's +* TextDragGestureRecognizer and modifiying the existing +* various mouse-event methods to check for drags before proceeding +* with their previous (selection-swiping etc.) stuff. +*

+* There was one problem with that - if you click inside the +* selection (to unselect the text), the mouse click is ignored - and +* the selection not unselected. This was especially troublesome if +* you've Selected All so it's hard to go outside the selection. +* So I modified it such that if you release the mouse, when you've +* ignored a mouse press (because a drag was possible), the selection +* is cleared and the caret moved. +*

+* +* @author Miloslav Metelka, Tor Norbye */ public class BaseCaret extends Rectangle implements Caret, FocusListener, @@ -1112,7 +1132,27 @@ } } + // <> + // Add support for dragging within the source editor. You can select + // text, then drag this text around to a different location in the + // editor buffer, or even to other windows. In , this lets you + // drag source text to the palette to create a code clip. + // The code works by setting up the mouse listeners to recognize a + // drag gesture then uses the standard Swing mechanism to export + // a transferable. public void mousePressed(MouseEvent evt) { + dndArmedEvent = null; + + MouseEvent e = evt; + if (isDragPossible(e) && mapDragOperationFromModifiers(e) != TransferHandler.NONE) { + dndArmedEvent = e; + e.consume(); + return; + } + setPosition(evt); + } + + private void setPosition(MouseEvent evt) { JTextComponent c = component; if (c != null) { Utilities.getEditorUI(c).getWordMatch().clear(); // [PENDING] should be done cleanly @@ -1139,16 +1179,49 @@ } public void mouseReleased(MouseEvent evt) { + // If you click on a selection, but don't drag it, we should + // treat it as a regular mouse click in the buffer -> move the + // dot to this point + if (dndArmedEvent != null) { + setPosition(dndArmedEvent); + dndArmedEvent = null; + } } public void mouseEntered(MouseEvent evt) { + //dndArmedEvent = null; } public void mouseExited(MouseEvent evt) { + //if (dndArmedEvent != null && mapDragOperationFromModifiers(e) == TransferHandler.NONE) { + // dndArmedEvent = null; + //} } // MouseMotionListener methods public void mouseDragged(MouseEvent evt) { + if (dndArmedEvent != null) { + MouseEvent e = evt; + e.consume(); + + int action = mapDragOperationFromModifiers(e); + if (action == TransferHandler.NONE) { + return; + } + + int dx = Math.abs(e.getX() - dndArmedEvent.getX()); + int dy = Math.abs(e.getY() - dndArmedEvent.getY()); + if ((dx > getMotionThreshold()) || (dy > getMotionThreshold())) { + // start transfer... shouldn't be a click at this point + JComponent c = getComponent(e); + TransferHandler th = c.getTransferHandler(); + //th.exportAsDrag(c, dndArmedEvent, action); + th.exportAsDrag(c, dndArmedEvent, TransferHandler.COPY); + dndArmedEvent = null; + } + return; + } + JTextComponent c = component; if (SwingUtilities.isLeftMouseButton(evt)) { if (c != null) { @@ -1167,6 +1240,116 @@ public void mouseMoved(MouseEvent evt) { } + + + // Drag Gesture related stuff; borrowed from BasicDragGestureRecognizer.java + // in javax.swing.plaf.basic. The mouse method stuff is merged into the + // above mouse handlers, so consult the source. + + private MouseEvent dndArmedEvent = null; + + private static int motionThreshold; + + private static boolean checkedMotionThreshold = false; + + private static int getMotionThreshold() { + if (checkedMotionThreshold) { + return motionThreshold; + } else { + checkedMotionThreshold = true; + try { + motionThreshold = ((Integer)Toolkit.getDefaultToolkit().getDesktopProperty("DnD.gestureMotionThreshold")).intValue(); + } catch (Exception e) { + motionThreshold = 5; + } + } + return motionThreshold; + } + + protected int mapDragOperationFromModifiers(MouseEvent e) { + int mods = e.getModifiersEx(); + + if ((mods & InputEvent.BUTTON1_DOWN_MASK) != InputEvent.BUTTON1_DOWN_MASK) { + return TransferHandler.NONE; + } + + JComponent c = getComponent(e); + TransferHandler th = c.getTransferHandler(); + return SunDragSourceContextPeer.convertModifiersToDropAction(mods, th.getSourceActions(c)); + } + + private TransferHandler getTransferHandler(MouseEvent e) { + JComponent c = getComponent(e); + return c == null ? null : c.getTransferHandler(); + } + + + protected JComponent getComponent(MouseEvent e) { + Object src = e.getSource(); + if (src instanceof JComponent) { + JComponent c = (JComponent) src; + return c; + } + return null; + } + + // Copied from BaseTextUI's TextDragGestureRecognizer + /** + * Determines if the following are true: + *

+ *

+ * This is implemented to check for a TransferHandler. + * Subclasses should perform the remaining conditions. + */ + protected boolean isDragPossible(MouseEvent e) { + JTextComponent c = (JTextComponent) this.getComponent(e); + + // Check if the associated text component has drag enabled: + //boolean dragEnabled = c.getDragEnabled(); + // However, we'd like the default to be "true" in the whole + // IDE, since that's how most IDEs work. However, that means + // that all places in the IDE which create an editor + // (new JEditorPane()) we'd have to go and call + // pane.setDraggable(true). Instead, there's probably no good + // reason to have inconsistent behavior for different editors + // in the IDE, so we globally enforce editor draggability. + // (An alternative would be for the install() method in + // the UIDelegate to go and set draggable true, giving + // clients a chance to turn it off after install, but this + // seems like overkill for something clients ought not to + // do.) + boolean dragEnabled = true; + + if (dragEnabled) { + Caret caret = c.getCaret(); + int dot = caret.getDot(); + int mark = caret.getMark(); + if (dot != mark) { + Point p = new Point(e.getX(), e.getY()); + int pos = c.viewToModel(p); + + int p0 = Math.min(dot, mark); + int p1 = Math.max(dot, mark); + if ((pos >= p0) && (pos < p1)) { + return true; + } + } + } + return false; + } + + // End stuff copied from BasicDragGestureRecognizer + + // + + + + + // PropertyChangeListener methods public void propertyChange(PropertyChangeEvent evt) { String propName = evt.getPropertyName();