com.alee.extended.tree.AbstractTreeTransferHandler Maven / Gradle / Ivy
/*
* 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.api.annotations.NotNull;
import com.alee.api.annotations.Nullable;
import com.alee.laf.tree.*;
import com.alee.utils.CollectionUtils;
import com.alee.utils.CoreSwingUtils;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.util.*;
/**
* Most common transfer handler implementation that handles tree nodes transfer.
*
* @param nodes type
* @param tree type
* @param tree model type
* @author Mikle Garin
*/
public abstract class AbstractTreeTransferHandler, M extends WebTreeModel>
extends TransferHandler
{
/**
* todo 1. Add setting "reduceDropConfirmsRate" which will cull extra "canBeDropped" method calls with the same arguments
*/
/**
* {@link NodesAcceptPolicy} that defines a way to filter dragged nodes.
*/
@NotNull
protected NodesAcceptPolicy nodesAcceptPolicy;
/**
* Whether or not should expand single dragged node when it is dropped onto the tree.
*/
protected boolean expandSingleNode;
/**
* Whether or not should expand multiple dragged nodes when they are dropped onto the tree.
*/
protected boolean expandMultipleNodes;
/**
* Transferred data handlers.
*/
@NotNull
protected List extends TreeDropHandler> dropHandlers;
/**
* Array of dragged nodes.
*/
@Nullable
protected List draggedNodes;
/**
* Map of 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.
*/
@Nullable
protected Map> draggedNodeIndices;
/**
* Constructs new tree transfer handler.
*/
public AbstractTreeTransferHandler ()
{
super ();
nodesAcceptPolicy = NodesAcceptPolicy.ancestors;
expandSingleNode = false;
expandMultipleNodes = false;
dropHandlers = createDropHandlers ();
}
/**
* Returns supported drop handlers.
* These handlers are requested only once on transfer handler initialization.
* They will be used to check and process drop operations on the tree that uses this transfer handler.
*
* @return supported drop handlers
*/
@NotNull
protected List extends TreeDropHandler> createDropHandlers ()
{
return CollectionUtils.asList ( new NodesDropHandler () );
}
/**
* Return {@link NodesAcceptPolicy} that defines a way to filter dragged nodes.
*
* @return {@link NodesAcceptPolicy} that defines a way to filter dragged nodes
*/
@NotNull
public NodesAcceptPolicy getNodesAcceptPolicy ()
{
return nodesAcceptPolicy;
}
/**
* Sets {@link NodesAcceptPolicy} that defines a way to filter dragged nodes.
*
* @param policy {@link NodesAcceptPolicy} that defines a way to filter dragged nodes
*/
public void setNodesAcceptPolicy ( @NotNull final NodesAcceptPolicy policy )
{
this.nodesAcceptPolicy = policy;
}
/**
* Returns whether should expand single dragged node when it is dropped onto the tree or not.
*
* @return true if should expand single dragged node when it is dropped onto the tree, false otherwise
*/
public boolean isExpandSingleNode ()
{
return expandSingleNode;
}
/**
* Sets whether should expand single dragged node when it is dropped onto the tree or not.
*
* @param expand whether should expand single dragged node when it is dropped onto the tree or not
*/
public void setExpandSingleNode ( final boolean expand )
{
this.expandSingleNode = expand;
}
/**
* Returns whether or not should expand multiple dragged nodes when they are dropped onto the tree.
*
* @return {@code true} if should expand multiple dragged nodes when they are dropped onto the tree, {@code false} otherwise
*/
public boolean isExpandMultipleNodes ()
{
return expandMultipleNodes;
}
/**
* Sets whether or not should expand multiple dragged nodes when they are dropped onto the tree.
*
* @param expand whether or not should expand multiple dragged nodes when they are dropped onto the tree
*/
public void setExpandMultipleNodes ( final boolean expand )
{
this.expandMultipleNodes = expand;
}
/**
* 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 abstract int getSourceActions ( @NotNull JComponent c );
/**
* Returns whether action is MOVE or not.
*
* @param action drag action
* @return true if action is MOVE, false otherwise
*/
protected boolean isMoveAction ( final int action )
{
return ( action & MOVE ) == MOVE;
}
/**
* Returns whether action is COPY or not.
*
* @param action drag action
* @return true if action is COPY, false otherwise
*/
protected boolean isCopyAction ( final int action )
{
return ( action & COPY ) == COPY;
}
/**
* Creates a {@link 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
*/
@Nullable
@Override
protected Transferable createTransferable ( @NotNull final JComponent c )
{
Transferable transferable = null;
final T tree = ( T ) c;
final List nodes = tree.getSelectedNodes ();
if ( !nodes.isEmpty () )
{
// Do not allow root node export
if ( !nodes.contains ( tree.getRootNode () ) )
{
// Filtering nodes if needed
getNodesAcceptPolicy ().filter ( tree, nodes );
// Sorting nodes according to their position in the tree
CollectionUtils.sort ( nodes, new NodesRowComparator ( tree ) );
// Checking whether or not can drag specified nodes
final M model = ( M ) tree.getModel ();
if ( model != null && canBeDragged ( tree, model, nodes ) )
{
// Creating nodes copy
final List copies = new ArrayList ();
for ( final N node : nodes )
{
copies.add ( copy ( tree, model, node ) );
}
// Saving list of dragged nodes
// These nodes might be removed later if this is a MOVE operation
draggedNodes = nodes;
// Collecting node indices under their parent nodes
draggedNodeIndices = new HashMap> ( 1 );
for ( final N node : draggedNodes )
{
final N parent = ( N ) node.getParent ();
if ( parent != null )
{
List indices = draggedNodeIndices.get ( parent.getId () );
if ( indices == null )
{
indices = new ArrayList ( 1 );
draggedNodeIndices.put ( parent.getId (), indices );
}
indices.add ( parent.getIndex ( node ) );
}
}
// Returning new nodes transferable
transferable = createTransferable ( tree, model, copies );
}
}
}
return transferable;
}
/**
* Returns whether the specified nodes drag can be started or not.
*
* @param tree source tree
* @param model tree model
* @param nodes nodes to drag
* @return true if the specified nodes drag can be started, false otherwise
*/
protected abstract boolean canBeDragged ( @NotNull T tree, @NotNull M model, @NotNull List nodes );
/**
* 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 tree source tree
* @param model tree model
* @param node node to copy
* @return node copy
*/
@NotNull
protected abstract N copy ( @NotNull T tree, @NotNull M model, @NotNull N node );
/**
* Returns new transferable based on dragged nodes.
*
* @param tree source tree
* @param model tree model
* @param nodes dragged nodes
* @return new transferable based on dragged nodes
*/
@NotNull
protected Transferable createTransferable ( @NotNull final T tree, @NotNull final M model, @NotNull final List nodes )
{
return new NodesTransferable ( nodes );
}
@Override
public boolean canImport ( @NotNull final TransferSupport support )
{
boolean canImport = false;
if ( support.isDrop () )
{
// Do not allow drop onto null path
// Also check whether actual TransferHandler accepts drop to this location
final JTree.DropLocation dl = ( JTree.DropLocation ) support.getDropLocation ();
final T tree = ( T ) support.getComponent ();
final M model = ( M ) tree.getModel ();
final N destination = tree.getNodeForPath ( dl.getPath () );
if ( model != null && destination != null && canDropTo ( support, tree, model, destination ) )
{
// Do not allow drop inside one of dragged elements if this is a MOVE operation
// Will not work when dragged to another tree, but it doesn't matter in that case
boolean validMove = true;
if ( isMoveAction ( support.getDropAction () ) )
{
if ( draggedNodes != null )
{
for ( final N node : draggedNodes )
{
if ( node == destination || node.isNodeDescendant ( destination ) )
{
validMove = false;
break;
}
}
}
}
if ( validMove )
{
// Perform actual drop check
final TreeDropHandler dropHandler = getDropHandler ( support, tree, model, destination );
final boolean canBeDropped = dropHandler != null && dropHandler.canDrop ( support, tree, model, destination );
// Displaying drop location
support.setShowDropLocation ( canBeDropped );
canImport = canBeDropped;
}
}
}
return canImport;
}
/**
* Returns whether or not specified destination is acceptable for drop.
* This check is performed before another check for nodes drop possibility.
*
* @param support transfer support data
* @param tree destination tree
* @param model tree model
* @param destination node onto which drop was performed
* @return {@code true} if the specified destination is acceptable for drop, {@code false} otherwise
*/
protected boolean canDropTo ( @NotNull final TransferSupport support, @NotNull final T tree, @NotNull final M model,
@NotNull final N destination )
{
return true;
}
/**
* Returns drop handler supporting this drop operation.
*
* @param support transfer support data
* @param tree destination tree
* @param model tree model
* @param destination node onto which drop was performed
* @return drop handler supporting this drop operation
*/
@Nullable
protected TreeDropHandler getDropHandler ( @NotNull final TransferSupport support, @NotNull final T tree,
@NotNull final M model, @NotNull final N destination )
{
TreeDropHandler dropHandler = null;
for ( final TreeDropHandler handler : dropHandlers )
{
final List flavors = handler.getSupportedFlavors ();
if ( CollectionUtils.notEmpty ( flavors ) )
{
for ( final DataFlavor flavor : flavors )
{
if ( support.isDataFlavorSupported ( flavor ) )
{
dropHandler = handler;
break;
}
}
if ( dropHandler != null )
{
break;
}
}
}
return dropHandler;
}
@Override
public boolean importData ( @NotNull final TransferHandler.TransferSupport support )
{
// Getting drop location info
final JTree.DropLocation dl = ( JTree.DropLocation ) support.getDropLocation ();
final T tree = ( T ) support.getComponent ();
final M model = ( M ) tree.getModel ();
final N destination = tree.getNodeForPath ( dl.getPath () );
final int dropIndex = dl.getChildIndex ();
// Prepare drop operation
return model != null && destination != null && prepareDropOperation (
support,
tree,
model,
destination,
dropIndex
);
}
/**
* Performs all preparations required to perform drop operation and calls for actual drop when ready.
*
* @param support transfer support data
* @param tree tree to drop nodes onto
* @param model tree model
* @param destination parent node to drop nodes into
* @param dropIndex preliminary nodes drop index
* @return {@code true} if drop operation was successfully completed, {@code false} otherwise
*/
protected boolean prepareDropOperation ( @NotNull final TransferSupport support, @NotNull final T tree, @NotNull final M model,
@NotNull final N destination, final int dropIndex )
{
// Expanding parent first
if ( !tree.isExpanded ( destination ) )
{
tree.expandNode ( destination );
}
// Adjust drop index after we ensure parent is expanded
final int adjustedDropIndex = getAdjustedDropIndex ( support, tree, model, destination, dropIndex );
// Now we can perform drop
return performDropOperation ( support, tree, model, destination, adjustedDropIndex );
}
/**
* Returns properly adjusted nodes drop index.
*
* @param support transfer support data
* @param tree tree to drop nodes onto
* @param model tree model
* @param destination parent node to drop nodes into
* @param dropIndex drop index if dropped between nodes under dropLocation node or -1 if dropped directly onto dropLocation node
* @return properly adjusted nodes drop index
*/
protected int getAdjustedDropIndex ( @NotNull final TransferSupport support, @NotNull final T tree, @NotNull final M model,
@NotNull final N destination, final int dropIndex )
{
// Fixing drop index for case when dropped to non-leaf element
int adjustedDropIndex = dropIndex == -1 ? destination.getChildCount () : dropIndex;
// Adjusting drop index for MOVE operation
if ( isMoveAction ( support.getDropAction () ) &&
draggedNodeIndices != null && draggedNodeIndices.containsKey ( destination.getId () ) )
{
final int initialIndex = adjustedDropIndex;
for ( final Integer index : draggedNodeIndices.get ( destination.getId () ) )
{
if ( index < initialIndex )
{
// 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 support transfer support data
* @param tree tree to drop nodes onto
* @param model tree model
* @param destination parent node to drop nodes into
* @param index nodes drop index
* @return {@code true} if drop operation was successfully completed, {@code false} otherwise
*/
protected boolean performDropOperation ( @NotNull final TransferSupport support, @NotNull final T tree, @NotNull final M model,
@NotNull final N destination, final int index )
{
boolean dropApproved = false;
// Retrieving drop handler that will perform actual drop
final TreeDropHandler handler = getDropHandler ( support, tree, model, destination );
if ( handler != null )
{
// Preparing drop operation
// Some heavy checks might still be running here
dropApproved = handler.prepareDrop ( support, tree, model, destination, index );
// Proceeding to drop which might happen asynchronously in the handler
// At this point we have done all we can to provide synchronous drop visual feedback
if ( dropApproved )
{
// Retrieving callback responsible for the drop and performing the actual drop
final NodesDropCallback callback = createNodesDropCallback ( support, tree, model, destination, index );
handler.performDrop ( support, tree, model, destination, index, callback );
}
}
return dropApproved;
}
/**
* Returns actual nodes drop operation callback for drop completion.
* All node inserts should be performed later in EDT 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 would cause issues 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.
*
* @param support transfer support data
* @param tree destination tree
* @param model tree model
* @param destination node onto which drop was performed
* @param index nodes drop index
* @return actual nodes drop operation callback for drop completion
*/
@NotNull
protected NodesDropCallback createNodesDropCallback ( @NotNull final TransferSupport support, @NotNull final T tree,
@NotNull final M model, @NotNull final N destination, final int index )
{
return new NodesDropCallback ()
{
/**
* Dropped nodes collected while drop callback is used.
*/
private final List dropped = new ArrayList ();
@Override
public void dropped ( @NotNull final N... nodes )
{
// Insert dropped nodes in EDT
CoreSwingUtils.invokeLater ( new Runnable ()
{
@Override
public void run ()
{
// Saving added nodes
Collections.addAll ( dropped, nodes );
// Adding dropped nodes into model
model.insertNodesInto ( nodes, destination, index );
}
} );
}
@Override
public void dropped ( @NotNull final List nodes )
{
// Insert dropped nodes in EDT
CoreSwingUtils.invokeLater ( new Runnable ()
{
@Override
public void run ()
{
// Saving added nodes
dropped.addAll ( nodes );
// Adding dropped nodes into model
model.insertNodesInto ( nodes, destination, index );
}
} );
}
@Override
public void completed ()
{
dropCompleted ( support, tree, model, destination, index, dropped );
}
@Override
public void failed ( @NotNull final Throwable cause )
{
dropFailed ( support, tree, model, destination, index, dropped, cause );
}
};
}
/**
* Called upon drop completion.
*
* @param support transfer support data
* @param tree destination tree
* @param model tree model
* @param destination node onto which drop was performed
* @param index nodes drop index
* @param dropped dropped nodes collected while drop callback was used
*/
protected void dropCompleted ( @NotNull final TransferSupport support, @NotNull final T tree, @NotNull final M model,
@NotNull final N destination, final int index, @NotNull final List dropped )
{
// Simply finish drop operation
finishDrop ( support, tree, model, destination, index, dropped );
}
/**
* Called upon drop fail.
*
* @param support transfer support data
* @param tree destination tree
* @param model tree model
* @param destination node onto which drop was performed
* @param index nodes drop index
* @param dropped dropped nodes collected while drop callback was used
* @param cause drop failure cause
*/
protected void dropFailed ( @NotNull final TransferSupport support, @NotNull final T tree, @NotNull final M model,
@NotNull final N destination, final int index, @NotNull final List dropped,
@NotNull final Throwable cause )
{
// todo Think of a good way to process drop failure
// Logging drop operation issues that have occurred
LoggerFactory.getLogger ( AbstractTreeTransferHandler.class ).error ( "Unable to perform drop operation", cause );
// Finish drop operation after logging cause
finishDrop ( support, tree, model, destination, index, dropped );
}
/**
* Drop operation completion.
* This will be called even if only partial data has been dropped.
*
* @param support transfer support data
* @param tree destination tree
* @param model tree model
* @param destination node onto which drop was performed
* @param index nodes drop index
* @param dropped dropped nodes collected while drop callback was used
*/
protected void finishDrop ( @NotNull final TransferSupport support, @NotNull final T tree, @NotNull final M model,
@NotNull final N destination, final int index, @NotNull final List dropped )
{
CoreSwingUtils.invokeLater ( new Runnable ()
{
@Override
public void run ()
{
if ( dropped.size () > 0 )
{
// Expanding nodes after drop operation
if ( expandSingleNode && dropped.size () == 1 )
{
// Expand single dropped node
tree.expandNode ( dropped.get ( 0 ) );
}
else if ( expandMultipleNodes )
{
// Expand all dropped nodes
for ( final N node : dropped )
{
tree.expandNode ( node );
}
}
// Selecting inserted nodes
tree.setSelectedNodes ( dropped );
}
}
} );
}
/**
* Invoked after data has been exported.
* This method should remove the data that was transferred if the action was MOVE.
* This method is invoked from EDT so it is safe to perform tree operations.
*
* @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 ( @NotNull final JComponent source, @NotNull final Transferable data, final int action )
{
if ( draggedNodes != null && isMoveAction ( action ) )
{
// Removing nodes saved in draggedNodes in createTransferable
final T tree = ( T ) source;
removeTreeNodes ( tree, draggedNodes );
}
// Cleaning up
draggedNodeIndices = null;
draggedNodes = null;
}
/**
* Asks tree to remove nodes after drag move operation has completed.
*
* @param tree tree to remove nodes from
* @param nodesToRemove nodes that should be removed
*/
protected void removeTreeNodes ( @NotNull final T tree, @NotNull final List nodesToRemove )
{
final M model = ( M ) tree.getModel ();
if ( model != null )
{
model.removeNodesFromParent ( nodesToRemove );
}
}
/**
* Returns user objects extracted from specified nodes.
*
* @param nodes list of nodes to extract user objects from
* @param user object type
* @return user objects extracted from specified nodes
*/
@NotNull
protected List extract ( @NotNull final List nodes )
{
final List objects = new ArrayList ( nodes.size () );
for ( final N node : nodes )
{
objects.add ( ( O ) node.getUserObject () );
}
return objects;
}
@NotNull
@Override
public String toString ()
{
return getClass ().getName ();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy