Swing Filtered Tree Just Like in Eclipse

Eclipse has a pretty nice widget. It’s basically a tree with a text field above it. As you type in the text field, the tree gets filtered.

Start typing some text :

As you can see, the tree shrinks and only nodes that match the search term are displayed. Also the parent nodes of any matched nodes are displayed. The matched nodes are highlighted in bold.

Here is my filtered tree in Swing :

..

As you can see it behaves almost the same as the filtered tree in Eclipse. The string matches are matched on the toString() of the UserObject in the tree nodes. The Eclipse filtered tree is a bit different in that it matches on strings inside the panel on the right. Mine does not do that however it would not be too hard to extend that to make the matches occur on some arbitrary object containing multiple strings. I am thinking that the super class of all panels that are displayed on the right could have some method like getPanelStrings().

Here is my code :

package filtertree;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Font;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Enumeration;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;

/**
 * Tree widget which allows the tree to be filtered on keystroke time. Only nodes who's
 * toString matches the search field will remain in the tree or its parents.
 *
 * Copyright (c) Oliver.Watkins
 */

public class FilteredTree extends JPanel{

	private String filteredText = "";
	private DefaultTreeModel originalTreeModel;
	private JScrollPane scrollpane = new JScrollPane();
	private JTree tree = new JTree();
	private DefaultMutableTreeNode originalRoot;

	public FilteredTree(DefaultMutableTreeNode originalRoot){
		this.originalRoot = originalRoot;
		guiLayout();
	}

	private void guiLayout() {
		tree.setCellRenderer(new Renderer());

		final JTextField field = new JTextField(10);
		field.addKeyListener(new KeyAdapter() {
			public void keyTyped(KeyEvent ke) {
				super.keyTyped(ke);
				filterTree(field.getText() + ke.getKeyChar());
			}
		});

		originalTreeModel = new DefaultTreeModel(originalRoot);

		tree.setModel(originalTreeModel);

		this.setLayout(new BorderLayout());

		add(field, BorderLayout.NORTH);
		add(scrollpane = new JScrollPane(tree), BorderLayout.CENTER);

		originalRoot = (DefaultMutableTreeNode) originalTreeModel.getRoot();

	}

	/**
	 *
	 * @param text
	 */

	private void filterTree(String text) {
		filteredText = text;
		//get a copy
		DefaultMutableTreeNode filteredRoot = copyNode(originalRoot);

		if (text.trim().toString().equals("")) {

			//reset with the original root
			originalTreeModel.setRoot(originalRoot);

			tree.setModel(originalTreeModel);
			tree.updateUI();
			scrollpane.getViewport().setView(tree);

			for (int i = 0; i < tree.getRowCount(); i++) {
				tree.expandRow(i);
			}

			return;
		} else {

			TreeNodeBuilder b = new TreeNodeBuilder(text);
			filteredRoot = b.prune((DefaultMutableTreeNode) filteredRoot.getRoot());

			originalTreeModel.setRoot(filteredRoot);

			tree.setModel(originalTreeModel);
			tree.updateUI();
			scrollpane.getViewport().setView(tree);
		}

		for (int i = 0; i < tree.getRowCount(); i++) {
			tree.expandRow(i);
		}
	}

	/**
	 * Clone/Copy a tree node. TreeNodes in Swing don't support deep cloning.
	 *
	 * @param orig to be cloned
	 * @return cloned copy
	 */
	private DefaultMutableTreeNode copyNode(DefaultMutableTreeNode orig) {

		DefaultMutableTreeNode newOne = new DefaultMutableTreeNode();
		newOne.setUserObject(orig.getUserObject());

		Enumeration enm = orig.children();

		while(enm.hasMoreElements()){

			DefaultMutableTreeNode child = enm.nextElement();
			newOne.add(copyNode(child));
		}
		return newOne;
	}

	/**
	 * Renders bold any tree nodes who's toString() value starts with the filtered text
	 * we are filtering on.
	 *
	 * @author Oliver.Watkins
	 */
	public class Renderer extends DefaultTreeCellRenderer{

		@Override
		public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded,
				boolean leaf, int row, boolean hasfocus) {

			Component c = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasfocus);

			if (c instanceof JLabel){

				if (!filteredText.equals("") && value.toString().startsWith(filteredText)){
					Font f = c.getFont();
					f = new Font("Dialog", Font.BOLD, f.getSize());
					c.setFont(f);
				}else{
					Font f = c.getFont();
					f = new Font("Dialog", Font.PLAIN , f.getSize());
					c.setFont(f);
				}
			}
			return c;
		}
	}

	public JTree getTree() {
		return tree;
	}

	/**
	 * Class that prunes off all leaves which do not match the search string.
	 *
	 * @author Oliver.Watkins
	 */

	public class TreeNodeBuilder {

		private String textToMatch;

		public TreeNodeBuilder(String textToMatch) {
			this.textToMatch = textToMatch;
		}

		public DefaultMutableTreeNode prune(DefaultMutableTreeNode root) {

			boolean badLeaves = true;

			//keep looping through until tree contains only leaves that match
			while (badLeaves){
				badLeaves = removeBadLeaves(root);
			}
			return root;
		}

		/**
		 *
		 * @param root
		 * @return boolean bad leaves were returned
		 */
		private boolean removeBadLeaves(DefaultMutableTreeNode root) {

			//no bad leaves yet
			boolean badLeaves = false;

			//reference first leaf
			DefaultMutableTreeNode leaf = root.getFirstLeaf();

			//if leaf is root then its the only node
			if (leaf.isRoot())
				return false;

			int leafCount = root.getLeafCount(); //this get method changes if in for loop so have to define outside of it
			for (int i = 0; i < leafCount; i++) {

				DefaultMutableTreeNode nextLeaf = leaf.getNextLeaf();

				//if it does not start with the text then snip it off its parent
				if (!leaf.getUserObject().toString().startsWith(textToMatch)) {
					DefaultMutableTreeNode parent = (DefaultMutableTreeNode) leaf.getParent();

					if (parent != null)
						parent.remove(leaf);

					badLeaves = true;
				}
				leaf = nextLeaf;
			}
			return badLeaves;
		}
	}
}

and here is a test class:

package filtertree;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;

public class Main extends JFrame {

	public static void main(String[] args) {
		Main m = new Main();
	}

	public Main() {

		DefaultMutableTreeNode n = new DefaultMutableTreeNode("animals");
		{
			DefaultMutableTreeNode n1 = new DefaultMutableTreeNode("bear");n.add(n1);
		}
		{
			DefaultMutableTreeNode n1 = new DefaultMutableTreeNode("cat");n.add(n1);
		}
		{
			DefaultMutableTreeNode n1 = new DefaultMutableTreeNode("boor");n.add(n1);
		}
		{
			DefaultMutableTreeNode n1 = new DefaultMutableTreeNode("dog");n.add(n1);
			{
				DefaultMutableTreeNode n2 = new DefaultMutableTreeNode("billy");n1.add(n2);
			}
			{
				DefaultMutableTreeNode n2 = new DefaultMutableTreeNode("cassie");n1.add(n2);
			}
		}
		{
			DefaultMutableTreeNode n1 = new DefaultMutableTreeNode("bat");n.add(n1);
		}

		{
			DefaultMutableTreeNode n1 = new DefaultMutableTreeNode("crow");n.add(n1);
		}
		{
			DefaultMutableTreeNode n1 = new DefaultMutableTreeNode("cow");n.add(n1);

			{
				DefaultMutableTreeNode n2 = new DefaultMutableTreeNode("carp");n1.add(n2);
			}
			{
				DefaultMutableTreeNode n2 = new DefaultMutableTreeNode("boa constrictor");n1.add(n2);

				{
					DefaultMutableTreeNode n3 = new DefaultMutableTreeNode("cockatoo");n2.add(n3);
				}
				{
					DefaultMutableTreeNode n3 = new DefaultMutableTreeNode("dragon");n2.add(n3);
				}
				{
					DefaultMutableTreeNode n3 = new DefaultMutableTreeNode("adder");n2.add(n3);
				}
			}
			{
				DefaultMutableTreeNode n2 = new DefaultMutableTreeNode("alligator");n1.add(n2);
			}
			{
				DefaultMutableTreeNode n2 = new DefaultMutableTreeNode("snake");n1.add(n2);
			}
			{
				DefaultMutableTreeNode n2 = new DefaultMutableTreeNode("spider");n1.add(n2);
			}
			{
				DefaultMutableTreeNode n2 = new DefaultMutableTreeNode("salamander");n1.add(n2);

				{
					DefaultMutableTreeNode n3 = new DefaultMutableTreeNode("mouse");n2.add(new DefaultMutableTreeNode("lala"));
				}
				{
					DefaultMutableTreeNode n3 = new DefaultMutableTreeNode("shark");n2.add(n3);
				}
				{
					DefaultMutableTreeNode n3 = new DefaultMutableTreeNode("llama");n2.add(n3);
				}
			}
			{
				DefaultMutableTreeNode n2 = new DefaultMutableTreeNode("ant");
				n1.add(n2);
			}
		}

		FilteredTree ftree = new FilteredTree(n);
		getContentPane().add(ftree);

		final JPanel rightPanel = new JPanel();
		rightPanel.setMinimumSize(new Dimension(300, 300));
		rightPanel.setPreferredSize(new Dimension(300, 300));

		getContentPane().add(rightPanel, BorderLayout.EAST);

		pack();
		setVisible(true);

		final JTree tree = ftree.getTree();

		MouseListener ml = new MouseAdapter() {
			public void mousePressed(MouseEvent e) {
				int selRow = tree.getRowForLocation(e.getX(), e.getY());
				TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
				if (selRow != -1) {
					if (e.getClickCount() == 1) {
						rightPanel.removeAll();
						rightPanel.add(new JLabel(""
								+ selPath.getLastPathComponent()));
						rightPanel.updateUI();

						System.out.println("" + selPath.getLastPathComponent());
					}
				}
			}
		};
		tree.addMouseListener(ml);
	}
}

enjoy! Comments appreciated!


Posted in Swing | Tagged , , , , , , , | 3 Comments

3 Responses to Swing Filtered Tree Just Like in Eclipse

  1. praveen says:

    very nice, very much helpful .thank u lot friend

  2. Michael Bartl says:

    The following improves upon your code :) It fixes a bug (keylistener is always late and doesn’t work with DEL key) and searches within the node and not only at the start.


    import java.awt.BorderLayout;
    import java.awt.Component;
    import java.awt.Font;
    import java.util.Enumeration;

    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.JScrollPane;
    import javax.swing.JTextField;
    import javax.swing.JTree;
    import javax.swing.event.DocumentEvent;
    import javax.swing.event.DocumentListener;
    import javax.swing.tree.DefaultMutableTreeNode;
    import javax.swing.tree.DefaultTreeCellRenderer;
    import javax.swing.tree.DefaultTreeModel;

    /**
    * Tree widget which allows the tree to be filtered on keystroke time. Only nodes who's
    * toString matches the search field will remain in the tree or its parents.
    *
    * Copyright (c) Oliver.Watkins
    */

    public class FilteredTree extends JPanel
    {

    private static final long serialVersionUID = 1L;

    private String filteredText = "";

    private DefaultTreeModel originalTreeModel;

    private JScrollPane scrollpane = new JScrollPane();

    private final JTree tree = new JTree();

    private DefaultMutableTreeNode originalRoot;

    public FilteredTree( final DefaultMutableTreeNode originalRoot )
    {
    this.originalRoot = originalRoot;
    guiLayout();
    }

    private void guiLayout()
    {
    this.tree.setCellRenderer( new Renderer() );

    final JTextField field = new JTextField( 10 );
    field.getDocument().addDocumentListener( new DocumentListener()
    {

    @Override
    public void removeUpdate( final DocumentEvent e )
    {
    filterTree( field.getText() );
    }

    @Override
    public void insertUpdate( final DocumentEvent e )
    {
    filterTree( field.getText() );
    }

    @Override
    public void changedUpdate( final DocumentEvent e )
    {
    filterTree( field.getText() );
    }
    } );

    this.originalTreeModel = new DefaultTreeModel( this.originalRoot );

    this.tree.setModel( this.originalTreeModel );

    setLayout( new BorderLayout() );

    add( field, BorderLayout.NORTH );
    add( this.scrollpane = new JScrollPane( this.tree ), BorderLayout.CENTER );

    this.originalRoot = ( DefaultMutableTreeNode ) this.originalTreeModel.getRoot();

    }

    /**
    *
    * @param text
    */

    private void filterTree( final String text )
    {
    this.filteredText = text;
    //get a copy
    DefaultMutableTreeNode filteredRoot = copyNode( this.originalRoot );

    if ( text.trim().toString().equals( "" ) )
    {

    //reset with the original root
    this.originalTreeModel.setRoot( this.originalRoot );

    this.tree.setModel( this.originalTreeModel );
    this.tree.updateUI();
    this.scrollpane.getViewport().setView( this.tree );

    for ( int i = 0; i < this.tree.getRowCount(); i++ )
    {
    this.tree.expandRow( i );
    }

    return;
    }
    else
    {

    final TreeNodeBuilder b = new TreeNodeBuilder( text );
    filteredRoot = b.prune( ( DefaultMutableTreeNode ) filteredRoot.getRoot() );

    this.originalTreeModel.setRoot( filteredRoot );

    this.tree.setModel( this.originalTreeModel );
    this.tree.updateUI();
    this.scrollpane.getViewport().setView( this.tree );
    }

    for ( int i = 0; i 0 )
    {
    Font f = c.getFont();
    f = new Font( "Dialog", Font.BOLD, f.getSize() );
    c.setFont( f );
    }
    else
    {
    Font f = c.getFont();
    f = new Font( "Dialog", Font.PLAIN, f.getSize() );
    c.setFont( f );
    }
    }
    return c;
    }
    }

    public JTree getTree()
    {
    return this.tree;
    }

    /**
    * Class that prunes off all leaves which do not match the search string.
    *
    * @author Oliver.Watkins
    */

    public class TreeNodeBuilder
    {

    private final String textToMatch;

    public TreeNodeBuilder( final String textToMatch )
    {
    this.textToMatch = textToMatch;
    }

    public DefaultMutableTreeNode prune( final DefaultMutableTreeNode root )
    {

    boolean badLeaves = true;

    //keep looping through until tree contains only leaves that match
    while ( badLeaves )
    {
    badLeaves = removeBadLeaves( root );
    }
    return root;
    }

    /**
    *
    * @param root
    * @return boolean bad leaves were returned
    */
    private boolean removeBadLeaves( final DefaultMutableTreeNode root )
    {

    //no bad leaves yet
    boolean badLeaves = false;

    //reference first leaf
    DefaultMutableTreeNode leaf = root.getFirstLeaf();

    //if leaf is root then its the only node
    if ( leaf.isRoot() )
    {
    return false;
    }

    final int leafCount = root.getLeafCount(); //this get method changes if in for loop so have to define outside of it
    for ( int i = 0; i < leafCount; i++ )
    {

    final DefaultMutableTreeNode nextLeaf = leaf.getNextLeaf();

    //if it does not start with the text then snip it off its parent
    if ( leaf.getUserObject().toString().indexOf( this.textToMatch ) == -1 )
    {
    final DefaultMutableTreeNode parent =
    ( DefaultMutableTreeNode ) leaf.getParent();

    if ( parent != null )
    {
    parent.remove( leaf );
    }

    badLeaves = true;
    }
    leaf = nextLeaf;
    }
    return badLeaves;
    }
    }
    }

  3. pratikch says:

    Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

THREE_COLUMN_PAGE