Skip to content

How to use WebAsyncTree

Mikle edited this page Jan 30, 2020 · 15 revisions

Available since WebLaF v1.17 release, updated for WebLaF v1.2.12 release
Requires weblaf-ui module and Java 6 update 30 or any later to run


What is it for?

Asynchronous tree (or WebAsyncTree) is an extension for JTree component that allows you to load tree data asynchronously in a separate non-EDT thread, which means it doesn't block the UI while something is loading unlike the standard JTree. This tree is useful for cases when you cannot load tree data right away and data loading methods might hang for a while because of the size of the data, slow connection or any other reason.

As a brief example - WebFileTree is based on WebAsyncTree and doesn't block the UI if file system doesn't respond fast enough. It simply displays a loader icon on the tree node while childs are being loaded. Though usually that icon will display only for a few milliseconds while files list is being loaded.


What should I know about it?

WebAsyncTree uses specific nodes and model and you should know how to use them to avoid any unexpected exceptions from WebAsyncTree or its model. Actually, you might never have to work with the tree model (AsyncTreeModel) itself, but you will certainly have to use AsyncTreeDataProvider for your tree.

AsyncUniqueNode

WebAsyncTree can work only with nodes assignable to AsyncUniqueNode. Each node of this type has an unique ID, busy state variable and special unique animated loader icon. Unique icons are required to properly update visual tree state.

Busy state is updated by the tree itself when appropriate node starts or finishes loading its childs. Loader icon can be used within your own renderer to display busy state (if you want to display it of course) or can simply be ignored.

There is also an option to provide feedback when childs load has failed (this is a pretty common situation in many kinds of applications). To provide small failed state icon under the node icon simply use this inside any tree renderer that extends WebAsyncTreeCellRenderer:

setIcon ( node.isFailed () ? getFailedStateIcon ( icon ) : icon );

Where node is rendered cell node (assignable to AsyncUniqueNode) and icon is the icon you provide for this cell rendering (for example a folder icon).

AsyncTreeDataProvider

This is the heart of the WebAsyncTree - this interface provide methods which you should implement to enable tree root and node childs retrieval. It also has method to provide node-dependant filter and comparator to filter-out and sort node childs.

List of AsyncTreeDataProvider available methods:
(E is a simple AsyncUniqueNode type reference here)

  • @NotNull public String getThreadGroupId ()
    Returns identifier of a ThreadGroup registered within TaskManager.
    It is used by AsyncTreeModel to perform asynchronous nodes loading.

  • @NotNull public N getRoot ()
    Returns root AsyncUniqueNode.
    This operation is always performed on EDT and should not take excessive amounts of time.
    Check Event Dispatch Thread wiki article for more information.

  • public void loadChildren ( @NotNull N parent, @NotNull NodesLoadCallback<N> listener )
    Starts loading child AsyncUniqueNodes for the specified parent AsyncUniqueNode.
    When children loading is finished or failed you must inform the NodesLoadCallback about result.
    This operation is executed outside of EDT and can take as much time as it needs to complete.

  • public boolean isLeaf ( @NotNull N node )
    Returns whether or not specified AsyncUniqueNode doesn't have any children.
    If you are not sure if the node is leaf or not - simply return false, that will allow the tree to expand this node.
    This method is created to avoid meaningless children requests for nodes which you are sure will never have children.
    This operation is always performed on EDT and should not take excessive amounts of time.
    Check Event Dispatch Thread wiki article for more information.

  • @Nullable public Filter<N> getChildrenFilter ( @NotNull N parent, @NotNull List<N> children )
    Returns Filter that will be used for the specified AsyncUniqueNode children.
    Specific List of child AsyncUniqueNodes is given for every separate filter operation.
    No filtering applied to children in case null is returned.
    This operation is always performed on EDT and should not take excessive amounts of time.
    Check Event Dispatch Thread wiki article for more information.

  • @Nullable public Comparator<N> getChildrenComparator ( @NotNull N parent, @NotNull List<N> children )
    Returns Comparator that will be used for the specified AsyncUniqueNode children.
    Specific List of child AsyncUniqueNodes is given for every separate comparison operation.
    No sorting applied to children in case null is returned.
    This operation is always performed on EDT and should not take excessive amounts of time.
    Check Event Dispatch Thread wiki article for more information.

In case you are extending AbstractAsyncTreeDataProvider - implementing getThreadGroupId (), getRoot () and loadChildren ( ... ) methods is sufficient.

You can check example data providers based on AbstractAsyncTreeDataProvider here:


How to use it?

You already know the basics required to use WebAsyncTree, let's try it out!
And don't panic! There is a lot of comments in the code below, but the code itself is really small.

First of all, let's make a custom node for your new tree:

public class SampleNode extends AsyncUniqueNode
{
    /**
     * Node name to display.
     */
    @NotNull
    protected String name;

    /**
     * Node type.
     */
    @NotNull
    protected SampleNodeType type;

    /**
     * Constructs sample node.
     *
     * @param name node name
     * @param type node type
     */
    public SampleNode ( @NotNull final String name, @NotNull final SampleNodeType type )
    {
        super ();
        this.name = name;
        this.type = type;
    }

    /**
     * Returns node name.
     *
     * @return node name
     */
    @NotNull
    public String getName ()
    {
        return name;
    }

    /**
     * Changes node name.
     *
     * @param name new node name
     */
    public void setName ( @NotNull final String name )
    {
        this.name = name;
    }

    /**
     * Returns node type.
     *
     * @return node type
     */
    @NotNull
    public SampleNodeType getType ()
    {
        return type;
    }

    /**
     * Changes node type.
     *
     * @param type new node type
     */
    public void setType ( @NotNull final SampleNodeType type )
    {
        this.type = type;
    }

    @Nullable
    @Override
    public Icon getNodeIcon ( @NotNull final TreeNodeParameters parameters )
    {
        final Icon icon;
        if ( parameters.tree ().getModel ().getRoot () == this )
        {
            icon = parameters.isExpanded () ? Icons.rootOpen : Icons.root;
        }
        else if ( !parameters.isLeaf () )
        {
            icon = parameters.isExpanded () ? Icons.folderOpen : Icons.folder;
        }
        else
        {
            icon = Icons.leaf;
        }
        return icon;
    }
}
public enum SampleNodeType
{
    /**
     * Root element type.
     */
    root,

    /**
     * Folder element type.
     */
    folder,

    /**
     * Leaf element type.
     */
    leaf
}

Now let's create an AsyncTreeDataProvider for your tree:

public class SampleDataProvider extends AbstractAsyncTreeDataProvider<SampleNode>
{
    @NotNull
    @Override
    public String getThreadGroupId ()
    {
        return TaskManager.REMOTE_REQUEST;
    }

    @NotNull
    @Override
    public SampleNode getRoot ()
    {
        return new SampleNode ( "Root", SampleNodeType.root );
    }

    @Override
    public void loadChildren ( @NotNull final SampleNode parent, @NotNull final NodesLoadCallback<SampleNode> listener )
    {
        // Sample loading delay to see the loader in progress
        ThreadUtils.sleepSafely ( MathUtils.random ( 100, 2000 ) );

        if ( parent.getName ().toLowerCase ().contains ( "fail" ) )
        {
            // Sample load fail
            listener.failed ( new RuntimeException ( "Sample exception cause" ) );
        }
        else
        {
            // Sample childs
            switch ( parent.getType () )
            {
                case root:
                {
                    // Folder type childs
                    final SampleNode folder1 = new SampleNode ( "Folder 1", SampleNodeType.folder );
                    final SampleNode folder2 = new SampleNode ( "Folder 2", SampleNodeType.folder );
                    final SampleNode folder3 = new SampleNode ( "Folder 3", SampleNodeType.folder );
                    final SampleNode folder4 = new SampleNode ( "Fail folder", SampleNodeType.folder );
                    listener.completed ( CollectionUtils.asList ( folder1, folder2, folder3, folder4 ) );
                    break;
                }
                case folder:
                {
                    // Leaf type childs
                    final SampleNode leaf1 = new SampleNode ( "Leaf 1", SampleNodeType.leaf );
                    final SampleNode leaf2 = new SampleNode ( "Leaf 2", SampleNodeType.leaf );
                    final SampleNode leaf3 = new SampleNode ( "Leaf 3", SampleNodeType.leaf );
                    listener.completed ( CollectionUtils.asList ( leaf1, leaf2, leaf3 ) );
                    break;
                }
            }
        }
    }

    /**
     * Returns whether the specified sample node is leaf or not.
     * Simply checks the node type to determine if it is leaf or not.
     *
     * @param node node
     * @return true if the specified node is leaf, false otherwise
     */
    @Override
    public boolean isLeaf ( @NotNull final SampleNode node )
    {
        return node.getType ().equals ( SampleNodeType.leaf );
    }
}

This example uses predefined TaskManager.REMOTE_REQUEST as TaskGroup identifier, but you can use your own one if you have any custom TaskGroups registered in TaskManager.

Also, I didn't mention it before, but you might have to change tree cell renderer and editor to fit your data. There is nothing special about it, simply extend WebTreeCellRenderer and WebTreeCellEditor and change whatever you need to. Just remember that when you use AsyncUniqueNodes - node Icon will always come from the node implementation by default.

Here is a sample WebTreeCellRenderer extension that uses our SampleNode type and provides a custom display text:

public class SampleTreeCellRenderer extends WebTreeCellRenderer<SampleNode, WebAsyncTree<SampleNode>, TreeNodeParameters<SampleNode, WebAsyncTree<SampleNode>>>
{
    @Override
    @Nullable
    protected String textForValue ( @NotNull final TreeNodeParameters<SampleNode, WebAsyncTree<SampleNode>> parameters )
    {
        return parameters.node ().getName ();
    }
}

And a custom editor for our nodes:

public class SampleTreeCellEditor extends WebTreeCellEditor
{
    /**
     * Last edited node.
     */
    protected SampleNode sampleNode;

    /**
     * Returns custom tree cell editor component.
     *
     * @param tree       tree
     * @param value      cell value
     * @param isSelected whether cell is selected or not
     * @param expanded   whether cell is expanded or not
     * @param leaf       whether cell is leaf or not
     * @param row        cell row index
     * @return cell editor component
     */
    @Override
    public Component getTreeCellEditorComponent ( final JTree tree, final Object value, final boolean isSelected, final boolean expanded,
                                                  final boolean leaf, final int row )
    {
        this.sampleNode = ( SampleNode ) value;
        final WebTextField editor = ( WebTextField ) super.getTreeCellEditorComponent ( tree, value, isSelected, expanded, leaf, row );
        editor.setText ( sampleNode.getName () );
        return editor;
    }

    /**
     * Returns current editor's value.
     *
     * @return current editor's value
     */
    @Override
    public Object getCellEditorValue ()
    {
        sampleNode.setName ( delegate.getCellEditorValue ().toString () );
        return sampleNode;
    }
}

Now you are ready to create a working asynchronous tree:

public class AsyncTreeExample
{
    public static void main ( final String[] args )
    {
        SwingUtilities.invokeLater ( new Runnable ()
        {
            @Override
            public void run ()
            {
                WebLookAndFeel.install ();

                // Create data provider
                final SampleDataProvider dataProvider = new SampleDataProvider ();

                // Create a tree based on your data provider
                final WebAsyncTree<SampleNode> asyncTree = new WebAsyncTree<SampleNode> ( dataProvider );
                asyncTree.setVisibleRowCount ( 8 );
                asyncTree.setEditable ( true );

                // Setup cell renderer and editor
                asyncTree.setCellRenderer ( new SampleTreeCellRenderer () );
                asyncTree.setCellEditor ( new SampleTreeCellEditor () );

                // Show an example frame
                TestFrame.show ( new WebScrollPane ( StyleId.scrollpaneTransparentHovering, asyncTree ) );
            }
        } );
    }
}

Here is what you will see after running this small app:
Example

This is just a basic example of using WebAsyncTree though, you are free to modify the tree, provider, model, renderer and editor to make it look and work the way you need it to.