5

I have a JTree in a JScrollPane. The JTree is rather long so it takes a while to drag a node from the top of the tree to the bottom. When dragging a node the JScrollPane scrolls, but not nearly as fast as it scrolls using the mouse wheel. Implementing setUnitIncrement as suggested in the selected answer here makes the mouse wheel scroll speed even faster, but does not change the speed of dragging a node. The same is true when implementing setBlockIncrement. The speed of the scroll when dragging a node is about the same as if I were to hold the up or down arrow and traverse the JTree in that manner.

How can I speed up the speed of dragging a node?

UPDATE 1:

Here is an SSCCE as requested. Most of this code I ripped off from here because it is good at illustrating the problem I am having. Just drag a node to a part of the tree that isn't visible in the scroll pane and you will see how slow scrolling is. This is what I want to speed up.

package example;

import java.awt.datatransfer.*;
import java.util.*;
import javax.swing.*;
import javax.swing.tree.*;

public class Example {

    private JScrollPane getContent() {
        ArrayList<String> arrayList = new ArrayList<String>();
        for( int i = 0; i < 3000; i++ ) {
            arrayList.add( String.format( "Node %d", i ) );
        }
        DefaultMutableTreeNode root = new DefaultMutableTreeNode( "Root" );
        for( String s : arrayList ) {
            root.add( new DefaultMutableTreeNode( s ) );
        }
        JTree tree = new JTree( root );
        tree.setDragEnabled( true );
        tree.setDropMode( DropMode.ON_OR_INSERT );
        tree.setTransferHandler( new TreeTransferHandler() );
        tree.getSelectionModel().setSelectionMode( TreeSelectionModel.CONTIGUOUS_TREE_SELECTION );
        expandTree( tree );
        return new JScrollPane( tree );
    }

    private void expandTree( JTree tree ) {
        DefaultMutableTreeNode root = (DefaultMutableTreeNode)tree.getModel().getRoot();
        Enumeration e = root.breadthFirstEnumeration();
        while( e.hasMoreElements() ) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode)e.nextElement();
            if( node.isLeaf() ) {
                continue;
            }
            int row = tree.getRowForPath( new TreePath( node.getPath() ) );
            tree.expandRow( row );
        }
    }

    public static void main( String[] args ) {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        f.add( new Example().getContent() );
        f.setSize( 400, 400 );
        f.setLocation( 200, 200 );
        f.setVisible( true );
    }
}

class TreeTransferHandler extends TransferHandler {

    DataFlavor nodesFlavor;
    DataFlavor[] flavors = new DataFlavor[1];
    DefaultMutableTreeNode[] nodesToRemove;

    public TreeTransferHandler() {
        try {
            String mimeType = DataFlavor.javaJVMLocalObjectMimeType
                    + ";class=\""
                    + javax.swing.tree.DefaultMutableTreeNode[].class.getName()
                    + "\"";
            nodesFlavor = new DataFlavor( mimeType );
            flavors[0] = nodesFlavor;
        } catch( ClassNotFoundException e ) {
            System.out.println( "ClassNotFound: " + e.getMessage() );
        }
    }

    public boolean canImport( TransferHandler.TransferSupport support ) {
        if( !support.isDrop() ) {
            return false;
        }
        support.setShowDropLocation( true );
        if( !support.isDataFlavorSupported( nodesFlavor ) ) {
            return false;
        }
        // Do not allow a drop on the drag source selections.
        JTree.DropLocation dl = (JTree.DropLocation)support.getDropLocation();
        JTree tree = (JTree)support.getComponent();
        int dropRow = tree.getRowForPath( dl.getPath() );
        int[] selRows = tree.getSelectionRows();
        for( int i = 0; i < selRows.length; i++ ) {
            if( selRows[i] == dropRow ) {
                return false;
            }
        }
        // Do not allow MOVE-action drops if a non-leaf node is
        // selected unless all of its children are also selected.
        int action = support.getDropAction();
        if( action == MOVE ) {
            return haveCompleteNode( tree );
        }
        // Do not allow a non-leaf node to be copied to a level
        // which is less than its source level.
        TreePath dest = dl.getPath();
        DefaultMutableTreeNode target = (DefaultMutableTreeNode)dest.getLastPathComponent();
        TreePath path = tree.getPathForRow( selRows[0] );
        DefaultMutableTreeNode firstNode = (DefaultMutableTreeNode)path.getLastPathComponent();
        if( firstNode.getChildCount() > 0 && target.getLevel() < firstNode.getLevel() ) {
            return false;
        }
        return true;
    }

    private boolean haveCompleteNode( JTree tree ) {
        int[] selRows = tree.getSelectionRows();
        TreePath path = tree.getPathForRow( selRows[0] );
        DefaultMutableTreeNode first = (DefaultMutableTreeNode)path.getLastPathComponent();
        int childCount = first.getChildCount();
        // first has children and no children are selected.
        if( childCount > 0 && selRows.length == 1 ) {
            return false;
        }
        // first may have children.
        for( int i = 1; i < selRows.length; i++ ) {
            path = tree.getPathForRow( selRows[i] );
            DefaultMutableTreeNode next = (DefaultMutableTreeNode)path.getLastPathComponent();
            if( first.isNodeChild( next ) ) {
                // Found a child of first.
                if( childCount > selRows.length - 1 ) {
                    // Not all children of first are selected.
                    return false;
                }
            }
        }
        return true;
    }

    protected Transferable createTransferable( JComponent c ) {
        JTree tree = (JTree)c;
        TreePath[] paths = tree.getSelectionPaths();
        if( paths != null ) {
            // Make up a node array of copies for transfer and
            // another for/of the nodes that will be removed in
            // exportDone after a successful drop.
            List<DefaultMutableTreeNode> copies = new ArrayList<DefaultMutableTreeNode>();
            List<DefaultMutableTreeNode> toRemove = new ArrayList<DefaultMutableTreeNode>();
            DefaultMutableTreeNode node = (DefaultMutableTreeNode)paths[0].getLastPathComponent();
            DefaultMutableTreeNode copy = copy( node );
            copies.add( copy );
            toRemove.add( node );
            for( int i = 1; i < paths.length; i++ ) {
                DefaultMutableTreeNode next = (DefaultMutableTreeNode)paths[i].getLastPathComponent();
                // Do not allow higher level nodes to be added to list.
                if( next.getLevel() < node.getLevel() ) {
                    break;
                } else if( next.getLevel() > node.getLevel() ) {  // child node
                    copy.add( copy( next ) );
                    // node already contains child
                } else {                                        // sibling
                    copies.add( copy( next ) );
                    toRemove.add( next );
                }
            }
            DefaultMutableTreeNode[] nodes = copies.toArray( new DefaultMutableTreeNode[copies.size()] );
            nodesToRemove = toRemove.toArray( new DefaultMutableTreeNode[toRemove.size()] );
            return new NodesTransferable( nodes );
        }
        return null;
    }

    /**
     * Defensive copy used in createTransferable.
     */
    private DefaultMutableTreeNode copy( TreeNode node ) {
        return new DefaultMutableTreeNode( node );
    }

    protected void exportDone( JComponent source, Transferable data, int action ) {
        if( ( action & MOVE ) == MOVE ) {
            JTree tree = (JTree)source;
            DefaultTreeModel model = (DefaultTreeModel)tree.getModel();
            // Remove nodes saved in nodesToRemove in createTransferable.
            for( int i = 0; i < nodesToRemove.length; i++ ) {
                model.removeNodeFromParent( nodesToRemove[i] );
            }
        }
    }

    public int getSourceActions( JComponent c ) {
        return COPY_OR_MOVE;
    }

    public boolean importData( TransferHandler.TransferSupport support ) {
        if( !canImport( support ) ) {
            return false;
        }
        // Extract transfer data.
        DefaultMutableTreeNode[] nodes = null;
        try {
            Transferable t = support.getTransferable();
            nodes = (DefaultMutableTreeNode[])t.getTransferData( nodesFlavor );
        } catch( UnsupportedFlavorException ufe ) {
            System.out.println( "UnsupportedFlavor: " + ufe.getMessage() );
        } catch( java.io.IOException ioe ) {
            System.out.println( "I/O error: " + ioe.getMessage() );
        }
        // Get drop location info.
        JTree.DropLocation dl = (JTree.DropLocation)support.getDropLocation();
        int childIndex = dl.getChildIndex();
        TreePath dest = dl.getPath();
        DefaultMutableTreeNode parent = (DefaultMutableTreeNode)dest.getLastPathComponent();
        JTree tree = (JTree)support.getComponent();
        DefaultTreeModel model = (DefaultTreeModel)tree.getModel();
        // Configure for drop mode.
        int index = childIndex;    // DropMode.INSERT
        if( childIndex == -1 ) {     // DropMode.ON
            index = parent.getChildCount();
        }
        // Add data to model.
        for( int i = 0; i < nodes.length; i++ ) {
            model.insertNodeInto( nodes[i], parent, index++ );
        }
        return true;
    }

    public String toString() {
        return getClass().getName();
    }

    public class NodesTransferable implements Transferable {

        DefaultMutableTreeNode[] nodes;

        public NodesTransferable( DefaultMutableTreeNode[] nodes ) {
            this.nodes = nodes;
        }

        public Object getTransferData( DataFlavor flavor )
                throws UnsupportedFlavorException {
            if( !isDataFlavorSupported( flavor ) ) {
                throw new UnsupportedFlavorException( flavor );
            }
            return nodes;
        }

        public DataFlavor[] getTransferDataFlavors() {
            return flavors;
        }

        public boolean isDataFlavorSupported( DataFlavor flavor ) {
            return nodesFlavor.equals( flavor );
        }
    }
}

SUCCESS!

I was able finally able to speed up the scrolling when dragging. In fact I was also able to make the scroll speed variable based on how close the cursor is to the top or bottom of the tree. Ended up being a lot simpler than I was making it. Didn't even need a listener. Just diff these two code examples to see what I added.

package example;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.datatransfer.*;
import java.util.*;
import javax.swing.*;
import javax.swing.tree.*;

public class Example {

    private JScrollPane getContent() {
        ArrayList<String> arrayList = new ArrayList<String>();
        for( int i = 0; i < 3000; i++ ) {
            arrayList.add( String.format( "Node %d", i ) );
        }
        DefaultMutableTreeNode root = new DefaultMutableTreeNode( "Root" );
        for( String s : arrayList ) {
            root.add( new DefaultMutableTreeNode( s ) );
        }
        JTree tree = new JTree( root );
        tree.setDragEnabled( true );
        tree.setDropMode( DropMode.ON_OR_INSERT );
        tree.setTransferHandler( new TreeTransferHandler() );
        tree.getSelectionModel().setSelectionMode( TreeSelectionModel.CONTIGUOUS_TREE_SELECTION );
        expandTree( tree );
        return new JScrollPane( tree );
    }

    private void expandTree( JTree tree ) {
        DefaultMutableTreeNode root = (DefaultMutableTreeNode)tree.getModel().getRoot();
        Enumeration e = root.breadthFirstEnumeration();
        while( e.hasMoreElements() ) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode)e.nextElement();
            if( node.isLeaf() ) {
                continue;
            }
            int row = tree.getRowForPath( new TreePath( node.getPath() ) );
            tree.expandRow( row );
        }
    }

    public static void main( String[] args ) {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        f.add( new Example().getContent() );
        f.setSize( 400, 400 );
        f.setLocation( 200, 200 );
        f.setVisible( true );
    }
}

class TreeTransferHandler extends TransferHandler {

    DataFlavor nodesFlavor;
    DataFlavor[] flavors = new DataFlavor[1];
    DefaultMutableTreeNode[] nodesToRemove;

    public TreeTransferHandler() {
        try {
            String mimeType = DataFlavor.javaJVMLocalObjectMimeType
                    + ";class=\""
                    + javax.swing.tree.DefaultMutableTreeNode[].class.getName()
                    + "\"";
            nodesFlavor = new DataFlavor( mimeType );
            flavors[0] = nodesFlavor;
        } catch( ClassNotFoundException e ) {
            System.out.println( "ClassNotFound: " + e.getMessage() );
        }
    }

    public boolean canImport( TransferHandler.TransferSupport support ) {
        if( !support.isDrop() ) {
            return false;
        }

        boolean isScrolling = false;
        JTree tree = (JTree)support.getComponent();
        JViewport vp = (JViewport)tree.getParent();
        Point vpMousePosition = vp.getMousePosition();
        Rectangle treeVisibleRectangle = tree.getVisibleRect();

        // Don't attempt scroll if mouse isn't over tree
        if( vpMousePosition != null ) {
            Integer newY = null;

            // Make sure we aren't already scrolled all the way down
            if( tree.getHeight() - treeVisibleRectangle.y != vp.getHeight() ) {
                /*
                 * Get Y coordinate for scrolling down
                 */
                if( vp.getHeight() - vpMousePosition.y < 10 ) {
                    newY = treeVisibleRectangle.y + 500;
                } else if( vp.getHeight() - vpMousePosition.y < 20 ) {
                    newY = treeVisibleRectangle.y + 400;
                } else if( vp.getHeight() - vpMousePosition.y < 30 ) {
                    newY = treeVisibleRectangle.y + 300;
                } else if( vp.getHeight() - vpMousePosition.y < 40 ) {
                    newY = treeVisibleRectangle.y + 200;
                } else if( vp.getHeight() - vpMousePosition.y < 50 ) {
                    newY = treeVisibleRectangle.y + 100;
                } else if( vp.getHeight() - vpMousePosition.y < 60 ) {
                    newY = treeVisibleRectangle.y + 50;
                } else if( vp.getHeight() - vpMousePosition.y < 70 ) {
                    newY = treeVisibleRectangle.y + 25;
                } else if( vp.getHeight() - vpMousePosition.y < 80 ) {
                    newY = treeVisibleRectangle.y + 10;
                }
            }

            // Make sure we aren't already scrolled all the way up
            if( newY == null && treeVisibleRectangle.y != 0 ) {
                /*
                 * Get Y coordinate for scrolling up
                 */
                if( 10 > vpMousePosition.y ) {
                    newY = treeVisibleRectangle.y - 500;
                } else if( 20 > vpMousePosition.y ) {
                    newY = treeVisibleRectangle.y - 400;
                } else if( 30 > vpMousePosition.y ) {
                    newY = treeVisibleRectangle.y - 300;
                } else if( 40 > vpMousePosition.y ) {
                    newY = treeVisibleRectangle.y - 200;
                } else if( 50 > vpMousePosition.y ) {
                    newY = treeVisibleRectangle.y - 100;
                } else if( 60 > vpMousePosition.y ) {
                    newY = treeVisibleRectangle.y - 50;
                } else if( 70 > vpMousePosition.y ) {
                    newY = treeVisibleRectangle.y - 25;
                } else if( 80 > vpMousePosition.y ) {
                    newY = treeVisibleRectangle.y - 10;
                }
            }

            // Do the scroll
            if( newY != null  ) {
                Rectangle treeNewVisibleRectangle = new Rectangle( treeVisibleRectangle.x, newY, treeVisibleRectangle.width, treeVisibleRectangle.height );
                tree.scrollRectToVisible( treeNewVisibleRectangle );
                isScrolling = true;
            }
        }

        if( isScrolling ) {
            return false;
        }

        support.setShowDropLocation( true );
        if( !support.isDataFlavorSupported( nodesFlavor ) ) {
            return false;
        }
        // Do not allow a drop on the drag source selections.
        JTree.DropLocation dl = (JTree.DropLocation)support.getDropLocation();
        int dropRow = tree.getRowForPath( dl.getPath() );
        int[] selRows = tree.getSelectionRows();
        for( int i = 0; i < selRows.length; i++ ) {
            if( selRows[i] == dropRow ) {
                return false;
            }
        }
        // Do not allow MOVE-action drops if a non-leaf node is
        // selected unless all of its children are also selected.
        int action = support.getDropAction();
        if( action == MOVE ) {
            return haveCompleteNode( tree );
        }
        // Do not allow a non-leaf node to be copied to a level
        // which is less than its source level.
        TreePath dest = dl.getPath();
        DefaultMutableTreeNode target = (DefaultMutableTreeNode)dest.getLastPathComponent();
        TreePath path = tree.getPathForRow( selRows[0] );
        DefaultMutableTreeNode firstNode = (DefaultMutableTreeNode)path.getLastPathComponent();
        if( firstNode.getChildCount() > 0 && target.getLevel() < firstNode.getLevel() ) {
            return false;
        }
        return true;
    }

    private boolean haveCompleteNode( JTree tree ) {
        int[] selRows = tree.getSelectionRows();
        TreePath path = tree.getPathForRow( selRows[0] );
        DefaultMutableTreeNode first = (DefaultMutableTreeNode)path.getLastPathComponent();
        int childCount = first.getChildCount();
        // first has children and no children are selected.
        if( childCount > 0 && selRows.length == 1 ) {
            return false;
        }
        // first may have children.
        for( int i = 1; i < selRows.length; i++ ) {
            path = tree.getPathForRow( selRows[i] );
            DefaultMutableTreeNode next = (DefaultMutableTreeNode)path.getLastPathComponent();
            if( first.isNodeChild( next ) ) {
                // Found a child of first.
                if( childCount > selRows.length - 1 ) {
                    // Not all children of first are selected.
                    return false;
                }
            }
        }
        return true;
    }

    protected Transferable createTransferable( JComponent c ) {
        JTree tree = (JTree)c;
        TreePath[] paths = tree.getSelectionPaths();
        if( paths != null ) {
            // Make up a node array of copies for transfer and
            // another for/of the nodes that will be removed in
            // exportDone after a successful drop.
            List<DefaultMutableTreeNode> copies = new ArrayList<DefaultMutableTreeNode>();
            List<DefaultMutableTreeNode> toRemove = new ArrayList<DefaultMutableTreeNode>();
            DefaultMutableTreeNode node = (DefaultMutableTreeNode)paths[0].getLastPathComponent();
            DefaultMutableTreeNode copy = copy( node );
            copies.add( copy );
            toRemove.add( node );
            for( int i = 1; i < paths.length; i++ ) {
                DefaultMutableTreeNode next = (DefaultMutableTreeNode)paths[i].getLastPathComponent();
                // Do not allow higher level nodes to be added to list.
                if( next.getLevel() < node.getLevel() ) {
                    break;
                } else if( next.getLevel() > node.getLevel() ) {  // child node
                    copy.add( copy( next ) );
                    // node already contains child
                } else {                                        // sibling
                    copies.add( copy( next ) );
                    toRemove.add( next );
                }
            }
            DefaultMutableTreeNode[] nodes = copies.toArray( new DefaultMutableTreeNode[copies.size()] );
            nodesToRemove = toRemove.toArray( new DefaultMutableTreeNode[toRemove.size()] );
            return new NodesTransferable( nodes );
        }
        return null;
    }

    /**
     * Defensive copy used in createTransferable.
     */
    private DefaultMutableTreeNode copy( TreeNode node ) {
        return new DefaultMutableTreeNode( node );
    }

    protected void exportDone( JComponent source, Transferable data, int action ) {
        if( ( action & MOVE ) == MOVE ) {
            JTree tree = (JTree)source;
            DefaultTreeModel model = (DefaultTreeModel)tree.getModel();
            // Remove nodes saved in nodesToRemove in createTransferable.
            for( int i = 0; i < nodesToRemove.length; i++ ) {
                model.removeNodeFromParent( nodesToRemove[i] );
            }
        }
    }

    public int getSourceActions( JComponent c ) {
        return COPY_OR_MOVE;
    }

    public boolean importData( TransferHandler.TransferSupport support ) {
        if( !canImport( support ) ) {
            return false;
        }
        // Extract transfer data.
        DefaultMutableTreeNode[] nodes = null;
        try {
            Transferable t = support.getTransferable();
            nodes = (DefaultMutableTreeNode[])t.getTransferData( nodesFlavor );
        } catch( UnsupportedFlavorException ufe ) {
            System.out.println( "UnsupportedFlavor: " + ufe.getMessage() );
        } catch( java.io.IOException ioe ) {
            System.out.println( "I/O error: " + ioe.getMessage() );
        }
        // Get drop location info.
        JTree.DropLocation dl = (JTree.DropLocation)support.getDropLocation();
        int childIndex = dl.getChildIndex();
        TreePath dest = dl.getPath();
        DefaultMutableTreeNode parent = (DefaultMutableTreeNode)dest.getLastPathComponent();
        JTree tree = (JTree)support.getComponent();
        DefaultTreeModel model = (DefaultTreeModel)tree.getModel();
        // Configure for drop mode.
        int index = childIndex;    // DropMode.INSERT
        if( childIndex == -1 ) {     // DropMode.ON
            index = parent.getChildCount();
        }
        // Add data to model.
        for( int i = 0; i < nodes.length; i++ ) {
            model.insertNodeInto( nodes[i], parent, index++ );
        }
        return true;
    }

    public String toString() {
        return getClass().getName();
    }

    public class NodesTransferable implements Transferable {

        DefaultMutableTreeNode[] nodes;

        public NodesTransferable( DefaultMutableTreeNode[] nodes ) {
            this.nodes = nodes;
        }

        public Object getTransferData( DataFlavor flavor )
                throws UnsupportedFlavorException {
            if( !isDataFlavorSupported( flavor ) ) {
                throw new UnsupportedFlavorException( flavor );
            }
            return nodes;
        }

        public DataFlavor[] getTransferDataFlavors() {
            return flavors;
        }

        public boolean isDataFlavorSupported( DataFlavor flavor ) {
            return nodesFlavor.equals( flavor );
        }
    }
}
Community
  • 1
  • 1
ubiquibacon
  • 10,451
  • 28
  • 109
  • 179
  • Please edit your question to include an [sscce](http://sscce.org/) that clarifies the notion of _rather long_. – trashgod Jan 23 '13 at 18:45
  • @trashgod added SSCCE as requested. – ubiquibacon Jan 24 '13 at 21:27
  • +1 Your [sscce](http://sscce.org/) makes it clear that scrolling frequently stutters to a halt as a new drop target auto-scrolls into view. Sorry, I don't know a work-around. Is cut and paste a reasonable alternative? – trashgod Jan 25 '13 at 02:30
  • Yeah, cut and paste is probably the best option at this point. I was just trying to avoid that because our Transfer Handler is already very complex, and adding cut and paste will likely take some time and make it even more complex. – ubiquibacon Jan 25 '13 at 13:35
  • Also consider a context menu that provides `Move To` any of a list of recently used sub-trees, including `Other`. The feature is common in email clients. – trashgod Jan 25 '13 at 13:41

1 Answers1

2

When it isn't working nicely with SDK, than I am starting my hackish methods, in this case it is a kind of that: listen the mouse. When the mouse moved down 10 pixel, than make an event as the mouse moved 100 pixel on the same direction. In Windows , in Excel if you drag under the task bar the mouse it will seed up the scrolling. Something similar should be here, it worth a try.

  • 1
    I took your suggestion, but I was unable to get things to work correctly using listeners. Java's mouse listeners don't play nice with the DnD methods. For instance, I was unable to detect a mouse up event while a DnD operation was taking place. The code I added to my question does scroll down the tree based on the mouse location as you suggested though, so I'll give this one to ya :) – ubiquibacon Jan 30 '13 at 22:15