All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.alee.extended.tree.AsyncTreeTransferHandler Maven / Gradle / Ivy

There is a newer version: 1.2.14
Show newest version
/*
 * This file is part of WebLookAndFeel library.
 *
 * WebLookAndFeel library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * WebLookAndFeel library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with WebLookAndFeel library.  If not, see .
 */

package com.alee.extended.tree;

import com.alee.laf.tree.TreeUtils;
import com.alee.log.Log;

import javax.swing.*;
import javax.swing.tree.TreePath;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Custom TransferHandler for WebAsyncTree that provides a quick and convenient way to implement nodes DnD.
 *
 * @author Mikle Garin
 */

public abstract class AsyncTreeTransferHandler> extends TransferHandler
{
    /**
     * Whether should allow dropping nodes onto not-yet-loaded node or not.
     * Be aware that if this set to true and your tree might fail loading childs - old nodes will still get removed on drop.
     * If set to false tree will try to load child nodes first and then perform the drop operation.
     */
    protected boolean allowUncheckedDrop = false;

    /**
     * Nodes flavor.
     */
    protected DataFlavor nodesFlavor;

    /**
     * Nodes flavor array.
     */
    protected DataFlavor[] flavors = new DataFlavor[ 1 ];

    /**
     * Array of dragged nodes that should be removed at the end of DnD operation.
     */
    protected List nodesToRemove;

    /**
     * Map of removed node indices lists under their parent node IDs.
     * This map is used to properly adjust drop index in the destination node if D&D performed within one tree.
     */
    protected Map> removedUnder;

    /**
     * Constructs new async tree transfer handler.
     */
    public AsyncTreeTransferHandler ()
    {
        super ();
        try
        {
            nodesFlavor = new DataFlavor ( DataFlavor.javaJVMLocalObjectMimeType + ";class=\"" + List.class.getName () + "\"" );
            flavors[ 0 ] = nodesFlavor;
        }
        catch ( final ClassNotFoundException e )
        {
            e.printStackTrace ();
        }
    }

    /**
     * Returns whether should allow dropping nodes onto not-yet-loaded node or not.
     *
     * @return true if should allow dropping nodes onto not-yet-loaded node, false otherwise
     */
    public boolean isAllowUncheckedDrop ()
    {
        return allowUncheckedDrop;
    }

    /**
     * Sets whether should allow dropping nodes onto not-yet-loaded node or not
     *
     * @param allowUncheckedDrop whether should allow dropping nodes onto not-yet-loaded node or not
     */
    public void setAllowUncheckedDrop ( final boolean allowUncheckedDrop )
    {
        this.allowUncheckedDrop = allowUncheckedDrop;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean canImport ( final TransferHandler.TransferSupport support )
    {
        if ( !support.isDrop () )
        {
            return false;
        }

        // Do not allow drop if flavor is not supported
        if ( !support.isDataFlavorSupported ( nodesFlavor ) )
        {
            return false;
        }

        // Do not allow a drop on busy node as that might break tree model
        // Do not allow a drop on a failed node as it is already messed
        final JTree.DropLocation dl = ( JTree.DropLocation ) support.getDropLocation ();
        final TreePath path = dl.getPath ();
        if ( path == null )
        {
            return false;
        }
        final N target = ( N ) path.getLastPathComponent ();
        if ( target.isLoading () || target.isFailed () )
        {
            return false;
        }

        try
        {
            // Do not allow drop inside one of dragged elements
            // Will not work when dragged to another tree, but it doesn't matter in that case
            if ( nodesToRemove != null )
            {
                for ( final N node : nodesToRemove )
                {
                    if ( target == node || target.isNodeAncestor ( node ) )
                    {
                        return false;
                    }
                }
            }

            // Perform the actual drop check
            final List nodes = ( List ) support.getTransferable ().getTransferData ( nodesFlavor );
            final boolean canBeDropped = canBeDropped ( nodes, target, dl.getChildIndex () );

            // Displaying drop location
            support.setShowDropLocation ( canBeDropped );

            return canBeDropped;
        }
        catch ( final UnsupportedFlavorException ufe )
        {
            Log.warn ( this, "UnsupportedFlavor: " + ufe.getMessage () );
            return false;
        }
        catch ( final IOException ioe )
        {
            Log.error ( this, "I/O error: " + ioe.getMessage () );
            return false;
        }
    }

    /**
     * Creates a Transferable to use as the source for a data transfer.
     * Returns the representation of the data to be transferred, or null if the component's property is null
     *
     * @param c the component holding the data to be transferred, provided to enable sharing of TransferHandlers
     * @return the representation of the data to be transferred, or null if the property associated with component is null
     */
    @Override
    protected Transferable createTransferable ( final JComponent c )
    {
        final T tree = ( T ) c;
        final List nodes = tree.getSelectedNodes ();
        if ( !nodes.isEmpty () )
        {
            // Do not allow root node export
            if ( nodes.contains ( tree.getRootNode () ) )
            {
                return null;
            }

            // Optimizing dragged nodes
            TreeUtils.optimizeNodes ( nodes );

            if ( !canBeDragged ( nodes ) )
            {
                return null;
            }

            // Creating copies
            final List copies = new ArrayList ();
            for ( final N node : nodes )
            {
                copies.add ( copy ( node ) );
            }

            // Saving list of nodes to be deleted if operation succeed
            nodesToRemove = nodes;

            // Collecting removed node indices under their parent nodes
            removedUnder = new HashMap> ( 1 );
            for ( final N node : nodesToRemove )
            {
                final AsyncUniqueNode parent = node.getParent ();
                List indices = removedUnder.get ( parent.getId () );
                if ( indices == null )
                {
                    indices = new ArrayList ( 1 );
                    removedUnder.put ( parent.getId (), indices );
                }
                indices.add ( parent.getIndex ( node ) );
            }

            // Returning new nodes transferable
            return new NodesTransferable ( copies );
        }
        return null;
    }

    /**
     * Invoked after data has been exported.
     * This method should remove the data that was transferred 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
     */
    @Override
    protected void exportDone ( final JComponent source, final Transferable data, final int action )
    {
        if ( ( action & MOVE ) == MOVE )
        {
            // Removing nodes saved in nodesToRemove in createTransferable
            final T tree = ( T ) source;
            tree.removeNodes ( nodesToRemove );
            nodesToRemove = null;
        }
    }

    /**
     * Returns the type of transfer actions supported by the source.
     * Any bitwise-OR combination of {@code COPY}, {@code MOVE} and {@code LINK}.
     * Returning {@code NONE} disables transfers from the component.
     *
     * @param c the component holding the data to be transferred
     * @return type of transfer actions supported by the source
     */
    @Override
    public int getSourceActions ( final JComponent c )
    {
        return MOVE;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean importData ( final TransferHandler.TransferSupport support )
    {
        // Checking whether we can perform the import or not
        if ( !canImport ( support ) )
        {
            return false;
        }

        // Extracting transfer data.
        final List nodes;
        try
        {
            nodes = ( List ) support.getTransferable ().getTransferData ( nodesFlavor );
        }
        catch ( final UnsupportedFlavorException ufe )
        {
            Log.warn ( this, "UnsupportedFlavor: " + ufe.getMessage () );
            return false;
        }
        catch ( final java.io.IOException ioe )
        {
            Log.error ( this, "I/O error: " + ioe.getMessage () );
            return false;
        }

        // Getting drop location info
        final JTree.DropLocation dl = ( JTree.DropLocation ) support.getDropLocation ();
        final int dropIndex = dl.getChildIndex ();
        final TreePath dest = dl.getPath ();
        final N parent = ( N ) dest.getLastPathComponent ();
        final T tree = ( T ) support.getComponent ();
        final AsyncTreeModel model = ( AsyncTreeModel ) tree.getModel ();

        if ( allowUncheckedDrop )
        {
            // Acting differently in case parent node childs are not yet loaded
            // We don't want to modify model (insert childs) before existing childs are actually loaded
            if ( parent.isLoaded () )
            {
                // Adding data to model
                performDropOperation ( nodes, parent, tree, model, getAdjustedDropIndex ( dropIndex, parent ) );
            }
            else
            {
                // Loading childs first
                tree.addAsyncTreeListener ( new AsyncTreeAdapter ()
                {
                    @Override
                    public void childsLoadCompleted ( final AsyncUniqueNode loadedFor, final List childs )
                    {
                        if ( loadedFor == parent )
                        {
                            // Adding data to model
                            performDropOperation ( nodes, parent, tree, model, parent.getChildCount () );

                            // Removing listener
                            tree.removeAsyncTreeListener ( this );
                        }
                    }

                    @Override
                    public void childsLoadFailed ( final AsyncUniqueNode loadedFor, final Throwable cause )
                    {
                        if ( loadedFor == parent )
                        {
                            // Removing listener
                            tree.removeAsyncTreeListener ( this );
                        }
                    }
                } );
                tree.reloadNode ( parent );
            }
            return true;
        }
        else
        {
            // We have to load childs synchronously, otherwise we cannot say for sure if drop succeed or not
            if ( !parent.isLoaded () )
            {
                tree.reloadNodeSync ( parent );
            }

            // If childs were loaded right away with our attempt - perform the drop
            if ( parent.isLoaded () )
            {
                // Adding data to model
                performDropOperation ( nodes, parent, tree, model, getAdjustedDropIndex ( dropIndex, parent ) );
                return true;
            }
            else
            {
                return false;
            }
        }
    }

    /**
     * Returns properly adjusted nodes drop index.
     *
     * @param dropIndex drop index if dropped between nodes under dropLocation node or -1 if dropped directly onto dropLocation node
     * @param parent    parent node to drop nodes into
     * @return properly adjusted nodes drop index
     */
    protected int getAdjustedDropIndex ( final int dropIndex, final N parent )
    {
        // Adjusting drop index
        int adjustedDropIndex = dropIndex == -1 ? parent.getChildCount () : dropIndex;
        if ( removedUnder.containsKey ( parent.getId () ) )
        {
            for ( final Integer index : removedUnder.get ( parent.getId () ) )
            {
                if ( index < adjustedDropIndex )
                {
                    // We simply decrement inserted index in case some node which was higher than this one was deleted
                    // That allows us to have index that is correct when dragged nodes are already removed from the tree
                    adjustedDropIndex--;
                }
            }
        }
        return adjustedDropIndex;
    }

    /**
     * Performs actual nodes drop operation.
     *
     * @param nodes  list of nodes to drop
     * @param parent parent node to drop nodes into
     * @param tree   tree to drop nodes onto
     * @param model  tree model
     * @param index  nodes drop index
     */
    protected void performDropOperation ( final List nodes, final N parent, final T tree, final AsyncTreeModel model,
                                          final int index )
    {
        // This operation should be performed in EDT later to allow drop operation get completed in source TransferHandler first
        // Otherwise new nodes will be added into the tree before old ones are removed which is bad if it is the same tree
        // This is meaningful for D&D opearation within one tree, for other situations its meaningless but doesn't cause any problems
        SwingUtilities.invokeLater ( new Runnable ()
        {
            @Override
            public void run ()
            {
                // Adding data to model
                model.insertNodesInto ( nodes, parent, index );

                // Selecting inserted nodes
                tree.setSelectedNodes ( nodes );

                // Informing about drop in a separate thread
                AsyncTreeQueue.execute ( tree, new Runnable ()
                {
                    @Override
                    public void run ()
                    {
                        nodesDropped ( nodes, parent, tree, model, index );
                    }
                } );
            }
        } );
    }

    /**
     * Returns whether the specified nodes drag can be started or not.
     *
     * @param nodes nodes to drag
     * @return true if the specified nodes drag can be started, false otherwise
     */
    protected abstract boolean canBeDragged ( List nodes );

    /**
     * Returns whether nodes can be dropped to the specified location and index or not.
     *
     * @param nodes        list of nodes to drop
     * @param dropLocation node onto which drop was performed
     * @param dropIndex    drop index if dropped between nodes under dropLocation node or -1 if dropped directly onto dropLocation node
     * @return true if nodes can be dropped to the specified location and index, false otherwise
     */
    protected abstract boolean canBeDropped ( List nodes, N dropLocation, int dropIndex );

    /**
     * Returns node copy used in createTransferable.
     * Used each time when node is moved within tree or into another tree.
     * Node copy should have the same ID and content but must be another instance of node type class.
     *
     * @param node node to copy
     * @return node copy
     */
    protected abstract N copy ( final N node );

    /**
     * Informs about nodes drop operation completition in a separate tree thread.
     * This method should be used to perform actual data move operation.
     * You are allowed to perform slow operations here as it is executed in a special async tree thread.
     *
     * @param nodes  list of nodes to drop
     * @param parent parent node to drop nodes into
     * @param tree   tree to drop nodes onto
     * @param model  tree model
     * @param index  nodes drop index
     */
    public abstract void nodesDropped ( final List nodes, final N parent, final T tree, final AsyncTreeModel model, final int index );

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString ()
    {
        return getClass ().getName ();
    }

    /**
     * Custom nodes transferable used for D&D operation.
     */
    public class NodesTransferable implements Transferable, Serializable
    {
        /**
         * Transferred nodes.
         */
        protected final List nodes;

        /**
         * Constructs new nodes transferable with the specified nodes as data.
         *
         * @param nodes transferred nodes
         */
        public NodesTransferable ( final List nodes )
        {
            super ();
            this.nodes = nodes;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Object getTransferData ( final DataFlavor flavor ) throws UnsupportedFlavorException
        {
            if ( !isDataFlavorSupported ( flavor ) )
            {
                throw new UnsupportedFlavorException ( flavor );
            }
            return nodes;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public DataFlavor[] getTransferDataFlavors ()
        {
            return flavors;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isDataFlavorSupported ( final DataFlavor flavor )
        {
            return nodesFlavor.equals ( flavor );
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy