/* * 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-2003 Sun * Microsystems, Inc. All Rights Reserved. */ package org.openide.explorer.view; import java.awt.Component; import java.util.*; import java.awt.event.*; import java.awt.dnd.*; import java.awt.Point; import java.awt.Rectangle; import java.awt.datatransfer.*; import java.awt.geom.Line2D; import javax.swing.JTree; import javax.swing.tree.TreePath; import javax.swing.tree.TreeCellEditor; import javax.swing.SwingUtilities; import javax.swing.Timer; import org.openide.ErrorManager; import org.openide.nodes.Children; import org.openide.nodes.Index; import org.openide.nodes.Node; import org.openide.util.datatransfer.PasteType; /** Implementation of drop support for asociated Tree View. * * @author Dafe Simonek, Jiri Rechtacek */ final class TreeViewDropSupport implements DropTargetListener, Runnable { // Attributes /** true if support is active, false otherwise */ boolean active = false; boolean dropTargetPopupAllowed; /** Drop target asociated with the tree */ DropTarget dropTarget; /** Node area which we were during * DnD operation. */ Rectangle lastNodeArea; private int upperNodeIdx = -1; private int lowerNodeIdx = -1; /** Swing Timer for expand node's parent with delay time. */ Timer timer; /** Glass pane for JTree which is associate with this class. */ DropGlassPane dropPane; final static protected int FUSSY_POINTING = 3; final static private int DELAY_TIME_FOR_EXPAND = 1000; final static private int SHIFT_DOWN = -1; final static private int SHIFT_RIGHT = 10; final static private int SHIFT_LEFT = 15; private int pointAt = DragDropUtilities.NODE_CENTRAL; // Associations /** View manager. */ protected TreeView view; /** The component we are supporting with drop support */ protected JTree tree; // Operations /** Creates new TreeViewDropSupport */ public TreeViewDropSupport (TreeView view, JTree tree, boolean dropTargetPopupAllowed) { this.view = view; this.tree = tree; this.dropTargetPopupAllowed = dropTargetPopupAllowed; } public void setDropTargetPopupAllowed(boolean value) { dropTargetPopupAllowed=value; } public boolean isDropTargetPopupAllowed() { return dropTargetPopupAllowed; } /** User is starting to drag over us */ public void dragEnter (DropTargetDragEvent dtde) { // remember current glass pane to set back at end of dragging over this compoment if (!DropGlassPane.isOriginalPaneStored ()) { Component comp = tree.getRootPane ().getGlassPane (); DropGlassPane.setOriginalPane (tree, comp, comp.isVisible ()); // set glass pane for paint selection line dropPane = DropGlassPane.getDefault (tree); tree.getRootPane ().setGlassPane (dropPane); dropPane.setOpaque (false); dropPane.revalidate(); dropPane.setVisible (true); } // set a status and cursor of dnd action doDragOver (dtde); } /** User drags over us */ public void dragOver (DropTargetDragEvent dtde) { // set a status and cursor of dnd action doDragOver (dtde); } /** Process events dragEnter or dragOver. */ private void doDragOver (DropTargetDragEvent dtde) { // 1. test if I'm over any node TreePath tp = getTreePath(dtde); if (tp == null) { dtde.rejectDrag(); removeDropLine (); return ; } // 2. find node for drop Point p = dtde.getLocation (); Node dropNode = getNodeForDrop (p); // if I haven't any node for drop then reject drop if (dropNode==null) { dtde.rejectDrag (); removeDropLine (); return ; } Rectangle nodeArea = tree.getPathBounds (tp); int endPointX = nodeArea.x + nodeArea.width; int row = tree.getRowForPath (tp); if (nodeArea!=null) { pointAt = DragDropUtilities.NODE_CENTRAL; if (p.y <= nodeArea.y+FUSSY_POINTING) { // don't get line above root if (row != 0) { // point above node pointAt = DragDropUtilities.NODE_UP; TreePath upPath = tree.getPathForRow (row-1); if (upPath!=null && !upPath.equals (tp)) { endPointX = Math.max (nodeArea.x + nodeArea.width, tree.getPathBounds (upPath).x + tree.getPathBounds (upPath).width); } // drop candidate is parent if (dropNode.getParentNode ()!=null) { dropNode = dropNode.getParentNode (); tp = null; } } } else if (p.y >= (nodeArea.y+nodeArea.height-FUSSY_POINTING)) { // exclude expanded folder if (!view.isExpanded (dropNode)) { // point bellow node pointAt = DragDropUtilities.NODE_DOWN; TreePath downPath = tree.getPathForRow (row+1); if (downPath!=null && !downPath.equals (tp)) { endPointX = Math.max (nodeArea.x + nodeArea.width, tree.getPathBounds (downPath).x + tree.getPathBounds (downPath).width); } // drop candidate is parent if (dropNode.getParentNode ()!=null) { dropNode = dropNode.getParentNode (); tp = null; } } } } endPointX = endPointX + SHIFT_RIGHT; // 2.b. check index cookie Index indexCookie = (Index)dropNode.getCookie (Index.class); if (indexCookie!=null) { if (pointAt==DragDropUtilities.NODE_UP) { lowerNodeIdx = indexCookie.indexOf (getNodeForDrop (p)); upperNodeIdx = lowerNodeIdx - 1; } else if (pointAt==DragDropUtilities.NODE_DOWN) { upperNodeIdx = indexCookie.indexOf (getNodeForDrop (p)); lowerNodeIdx = upperNodeIdx + 1; } } // 3. expand with a delay if ((timer==null || !timer.isRunning ()) && dropNode!=null && !dropNode.isLeaf() && !view.isExpanded (dropNode)) { // ok, let's expand in a while // node is candidate for expand final Node cn = dropNode; // remove old timer removeTimer (); // create new timer timer = new Timer (DELAY_TIME_FOR_EXPAND, new ActionListener () { final public void actionPerformed (ActionEvent e) { view.expandNode (cn); } }); timer.setRepeats (false); timer.start (); } // 4. present node for drop // prepare selection or line if (pointAt==DragDropUtilities.NODE_CENTRAL) { // no line dropPane.setDropLine (null); } else { // line and selection of parent if any if (pointAt==DragDropUtilities.NODE_UP) { Line2D line = new Line2D.Double (nodeArea.x-SHIFT_LEFT, nodeArea.y+SHIFT_DOWN, endPointX, nodeArea.y+SHIFT_DOWN); convertBoundsAndSetDropLine (line); // enlagre node area with area for line Rectangle lineArea = new Rectangle (nodeArea.x-SHIFT_LEFT, nodeArea.y+SHIFT_DOWN-3, endPointX-nodeArea.x+SHIFT_LEFT, 5); nodeArea = (Rectangle)nodeArea.createUnion (lineArea); } else { Line2D line = new Line2D.Double (nodeArea.x-SHIFT_LEFT, nodeArea.y+nodeArea.height+SHIFT_DOWN, endPointX, nodeArea.y+nodeArea.height+SHIFT_DOWN); convertBoundsAndSetDropLine (line); // enlagre node area with area for line Rectangle lineArea = new Rectangle (nodeArea.x-SHIFT_LEFT, nodeArea.y+nodeArea.height, endPointX-nodeArea.x+SHIFT_LEFT, SHIFT_DOWN+3); nodeArea = (Rectangle)nodeArea.createUnion (lineArea); //System.out.println("OLD + LINE: "+lineArea+" = AREA: "+nodeArea); } // the parent node won't be selected /*// select parent and enlarge paint area if (tp.getParentPath ()!=null) { tp = tp.getParentPath (); } nodeArea = (Rectangle)nodeArea.createUnion (tree.getPathBounds (tp));*/ } // back normal view w/o any selecetion nor line if ((lastNodeArea != null) && (!lastNodeArea.equals (nodeArea))) { NodeRenderer.dragExit (); repaint (lastNodeArea); } // paint new state if (!nodeArea.equals (lastNodeArea)) { if (tp!=null) NodeRenderer.dragEnter (tp.getLastPathComponent ()); repaint (nodeArea); lastNodeArea = nodeArea; removeTimer (); } // 5 show to cursor belong to state if (canDrop (dropNode, dtde.getDropAction ())) { // ok, can accept dtde.acceptDrag (dtde.getDropAction ()); } else { // can only reorder? if (canReorder (dropNode, ExplorerDnDManager.getDefault ().getDraggedNodes ())) { // ok, can accept only reoder dtde.acceptDrag (dtde.getDropAction ()); } else { dtde.rejectDrag (); } } } /** Repaints TreeView, the given rectangle is enlarged for 5 pixels * because some parts was not repainted correctly. * @param Rectangle r rectangle which will be repainted.*/ private void repaint (Rectangle r) { tree.repaint (r.x-5, r.y-5, r.width+10, r.height+10); } /** Converts line's bounds by the bounds of the root pane. Drop glass pane * is over this root pane. After covert a given line is set to drop glass pane. * @param line line for show in drop glass pane */ private void convertBoundsAndSetDropLine (final Line2D line) { int x1 = (int)line.getX1 (), x2 = (int)line.getX2 (); int y1 = (int)line.getY1 (), y2 = (int)line.getY2 (); Point p1 = SwingUtilities.convertPoint (tree, x1, y1, tree.getRootPane ()); Point p2 = SwingUtilities.convertPoint (tree, x2, y2, tree.getRootPane ()); line.setLine (p1, p2); dropPane.setDropLine (line); } /** Removes timer and all listeners. */ private void removeTimer () { if (timer!=null) { ActionListener[] l = (ActionListener[])timer.getListeners (ActionListener.class); for (int i=0; i<l.length; i++) { timer.removeActionListener (l[i]); } timer.stop (); timer = null; } } public void dropActionChanged (DropTargetDragEvent dtde) { // check if the nodes are willing to do selected action Node[] nodes = ExplorerDnDManager.getDefault ().getDraggedNodes (); int dropAction = dtde.getDropAction (); for (int i = 0; i < nodes.length; i++) { if (!DragDropUtilities.checkNodeForAction (nodes[i], dropAction)) { // this action is not supported dtde.rejectDrag (); return ; } } return ; } /** User exits the dragging */ public void dragExit (DropTargetEvent dte) { stopDragging (); } private void removeDropLine () { dropPane.setDropLine (null); if (lastNodeArea != null) { NodeRenderer.dragExit(); repaint (lastNodeArea); lastNodeArea = null; } } private void stopDragging() { removeDropLine (); removeTimer (); // set back the remembered glass pane if (DropGlassPane.isOriginalPaneStored()) { DropGlassPane.putBackOriginal (); } } /** Get a node on given point or null if there none*/ private Node getNodeForDrop (Point p) { if (p!=null) { TreePath tp = tree.getPathForLocation (p.x, p.y); if (tp!=null) { return DragDropUtilities.secureFindNode (tp.getLastPathComponent ()); } } return null; } private boolean canReorder (Node folder, Node[] dragNodes) { if (ExplorerDnDManager.getDefault ().getAllowedDragActions ()!=DnDConstants.ACTION_MOVE) { return false; } if (folder==null||dragNodes.length==0) { return false; } // has folder a index cookie? Index ic = (Index)folder.getCookie (Index.class); if (ic==null) { return false; } // folder has index cookie // check if all dragNodes are from same folder for (int i=0; i<dragNodes.length; i++) { // bugfix #23988, check if dragNodes[i] isn't null if (dragNodes[i]==null) { return false; } if (dragNodes[i].getParentNode ()==null) return false; if (!dragNodes[i].getParentNode ().equals (folder)) return false; } return true; } private void performReorder (final Node folder, Node[] dragNodes, int lNode, int uNode) { try { Index indexCookie = (Index) folder.getCookie (Index.class); if (indexCookie != null) { int perm [] = new int [indexCookie.getNodesCount()]; int indexes [] = new int [dragNodes.length]; int indexesLength = 0; for (int i = 0; i < dragNodes.length; i++) { int idx = indexCookie.indexOf(dragNodes[i]); if (idx >= 0 && idx < perm.length) { indexes[indexesLength++] = idx; } } // XXX: normally indexes of dragged nodes should be in ascending order, but // it seems that Tree.getSelectionPaths doesn't keep this order Arrays.sort(indexes); if (lNode < 0 || uNode >= perm.length || indexesLength == 0) { return; } int k = 0; for (int i = 0; i < perm.length; i++) { if (i <= uNode) { if (!containsNumber(indexes, indexesLength, i)) { perm[i] = k++; } if (i == uNode) { for (int j = 0; j < indexesLength; j++) { if (indexes[j] <= uNode) { perm[indexes[j]] = k++; } } } } else { if (i == lNode) { for (int j = 0; j < indexesLength; j++) { if (indexes[j] >= lNode) { perm[indexes[j]] = k++; } } } if (!containsNumber(indexes, indexesLength, i)) { perm[i] = k++; } } } // check for identity permutation for (int i = 0; i < perm.length; i++) { if (perm[i] != i) { indexCookie.reorder(perm); break; } } } } catch (Exception e) { // Pending: add annotation or remove try/catch block ErrorManager.getDefault ().notify (ErrorManager.INFORMATIONAL, e); } } private boolean containsNumber(int [] arr, int arrLength, int n) { for (int i = 0; i < arrLength; i++) { if (arr[i] == n) { return true; } } return false; } private Node[] findDropedNodes (Node folder, Node[] dragNodes) { if (folder==null||dragNodes.length==0) { return null; } Node[] dropNodes = new Node[dragNodes.length]; Children children = folder.getChildren (); for (int i=0; i<dragNodes.length; i++) { dropNodes[i] = children.findChild (dragNodes[i].getName ()); } return dropNodes; } /** Can node recieve given drop action? */ // XXX canditate for more general support private boolean canDrop (Node n, int dropAction) { if (n == null) { return false; } // test if a parent of the dragged nodes isn't the node over // only for MOVE action if (DnDConstants.ACTION_MOVE==dropAction) { Node[] nodes = ExplorerDnDManager.getDefault ().getDraggedNodes(); if (nodes == null) return false; for (int i=0; i<nodes.length; i++) { if (n.equals(nodes[i].getParentNode ())) return false; } } Transferable trans = ExplorerDnDManager.getDefault ().getDraggedTransferable (DnDConstants.ACTION_MOVE==dropAction); if (trans==null) { return false; } // get paste types for given transferred transferable PasteType[] pt = DragDropUtilities.getPasteTypes(n, trans); return ((pt!=null) && (pt.length!=0)); } /** Performs the drop action, if we are dropping on * right node and target node agrees. */ public void drop (DropTargetDropEvent dtde) { stopDragging (); // find node for the drop perform Node dropNode = getNodeForDrop (dtde.getLocation ()); Node [] dragNodes = ExplorerDnDManager.getDefault ().getDraggedNodes (); TreePath tp = tree.getPathForLocation (dtde.getLocation ().x, dtde.getLocation ().y); if (pointAt!=DragDropUtilities.NODE_CENTRAL) { dropNode = dropNode.getParentNode(); } if (!canDrop (dropNode, dtde.getDropAction ())) { if (canReorder (dropNode, dragNodes)) { performReorder (dropNode, dragNodes, lowerNodeIdx, upperNodeIdx); dtde.acceptDrop (dtde.getDropAction ()); } else { dtde.rejectDrop (); } dtde.dropComplete (true); return; } // finally perform the drop int dropAction = dtde.getDropAction (); dtde.acceptDrop (dropAction); if (DnDConstants.ACTION_LINK == dropAction) { // construct all paste types PasteType[] ptCut = new PasteType[] {}, ptCopy = new PasteType[] {}; // do not try get paste types for move if MOVE is not allowed if (ExplorerDnDManager.getDefault ().getAllowedDragActions ()==DnDConstants.ACTION_MOVE) { ptCut = DragDropUtilities.getPasteTypes (dropNode, ExplorerDnDManager.getDefault ().getDraggedTransferable (true)); } // do not try get paste types for copy if COPY is not allowed if (ExplorerDnDManager.getDefault ().getAllowedDragActions ()>=DnDConstants.ACTION_COPY) { ptCopy = DragDropUtilities.getPasteTypes (dropNode, ExplorerDnDManager.getDefault ().getDraggedTransferable (false)); } TreeSet setPasteTypes = new TreeSet (new Comparator () { public int compare (Object obj1, Object obj2) { return ((PasteType)obj1).getName ().compareTo (((PasteType)obj2).getName ()); // have to fix: the different actions can have same name!!! /*int res = ((PasteType)obj1).getName ().compareTo (((PasteType)obj2).getName ()); System.out.println("res1: "+res); if (res == 0) { res = System.identityHashCode(obj1)-System.identityHashCode(obj2); } System.out.println("res2: "+res); return res;*/ }} ); for (int i=0; i<ptCut.length; i++) { //System.out.println(ptCut[i].getName()+", "+System.identityHashCode(ptCut[i])); setPasteTypes.add (ptCut[i]); } for (int i=0; i<ptCopy.length; i++) { //System.out.println(ptCopy[i].getName()+", "+System.identityHashCode(ptCopy[i])); setPasteTypes.add (ptCopy[i]); } DragDropUtilities.createDropFinishPopup (setPasteTypes).show(tree,Math.max(dtde.getLocation ().x-5,0),Math.max(dtde.getLocation ().y-5,0)); // reorder have to be perform if (canReorder (dropNode, dragNodes)) { final Node tempDropNode = dropNode; final int tmpUpper = upperNodeIdx; final int tmpLower = lowerNodeIdx; final Node[] tempDragNodes = dragNodes; DragDropUtilities.setPostDropRun (new Runnable () { public void run () { performReorder (tempDropNode, findDropedNodes (tempDropNode, tempDragNodes), tmpLower, tmpUpper); } }); } } else { // get correct paste type PasteType[] pt = DragDropUtilities.getPasteTypes (dropNode, ExplorerDnDManager.getDefault ().getDraggedTransferable (DnDConstants.ACTION_MOVE==dropAction)); /*// help loop for all paste actions System.out.println("PASTE TYPES FOR "+dropAction); for (int i=0; i<pt.length; i++) { System.out.println(i+". "+pt [i].getName ()); }*/ DragDropUtilities.performDrop (pt[0]); // check canReorder or optionally perform it // before find new nodes in dropNode if (canReorder (dropNode, findDropedNodes (dropNode, dragNodes))) { performReorder (dropNode, findDropedNodes (dropNode, dragNodes), lowerNodeIdx, upperNodeIdx); } } TreeCellEditor tce = tree.getCellEditor (); if (tce instanceof TreeViewCellEditor) ((TreeViewCellEditor)tce).setDnDActive (false); // finished dtde.dropComplete (true); } /** Activates or deactivates Drag support on asociated JTree * component * @param active true if the support should be active, false * otherwise */ public void activate (boolean active) { if (this.active == active) return; this.active = active; getDropTarget().setActive(active); } /** Implementation of the runnable interface. * Notifies user in AWT thread. */ public void run () { if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater (this); return; } DragDropUtilities.dropNotSuccesfull(); } /** @return The tree path to the node the cursor is above now or * null if no such node currently exists or if conditions were not * satisfied to continue with DnD operation. */ TreePath getTreePath (DropTargetDragEvent dtde) { int dropAction = dtde.getDropAction(); // check location Point location = dtde.getLocation(); TreePath tp = tree.getPathForLocation(location.x, location.y); return tp != null && DragDropUtilities.secureFindNode(tp.getLastPathComponent())!=null ? tp : null; } /** Safe accessor to the drop target which is asociated * with the tree */ DropTarget getDropTarget () { if (dropTarget == null) { dropTarget = new DropTarget(tree, view.getAllowedDropActions(), this, false); } return dropTarget; } } /* end class TreeViewDropSupport */