Saturday 28 April 2012

Filtered JTree

The solutions (1, 2) out there for filtering a JTree's nodes weren't suitable for me, so I rolled by own FilteredTreeModel class which wraps a JTree's underlying TreeModel and applies a string filter to the node names.

The tree model recurses over the tree nodes, from the root to the leaf nodes, checking if the nodes toString() value contains the filter value:

FilteredTreeModel.java

package org.adrianwalker.filteredjtree;

import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

public final class FilteredTreeModel implements TreeModel {

  private TreeModel treeModel;
  private String filter;

  public FilteredTreeModel(final TreeModel treeModel) {
    this.treeModel = treeModel;
    this.filter = "";
  }

  public TreeModel getTreeModel() {
    return treeModel;
  }

  public void setFilter(final String filter) {
    this.filter = filter;
  }

  private boolean recursiveMatch(final Object node, final String filter) {

    boolean matches = node.toString().contains(filter);

    int childCount = treeModel.getChildCount(node);
    for (int i = 0; i < childCount; i++) {
      Object child = treeModel.getChild(node, i);
      matches |= recursiveMatch(child, filter);
    }
    
    return matches;
  }

  @Override
  public Object getRoot() {
    return treeModel.getRoot();
  }

  @Override
  public Object getChild(final Object parent, final int index) {
    int count = 0;
    int childCount = treeModel.getChildCount(parent);
    for (int i = 0; i < childCount; i++) {
      Object child = treeModel.getChild(parent, i);
      if (recursiveMatch(child, filter)) {
        if (count == index) {
          return child;
        }
        count++;
      }
    }
    return null;
  }

  @Override
  public int getChildCount(final Object parent) {
    int count = 0;
    int childCount = treeModel.getChildCount(parent);
    for (int i = 0; i < childCount; i++) {
      Object child = treeModel.getChild(parent, i);
      if (recursiveMatch(child, filter)) {
        count++;
      }
    }
    return count;
  }

  @Override
  public boolean isLeaf(final Object node) {
    return treeModel.isLeaf(node);
  }

  @Override
  public void valueForPathChanged(final TreePath path, final Object newValue) {
    treeModel.valueForPathChanged(path, newValue);
  }

  @Override
  public int getIndexOfChild(final Object parent, final Object childToFind) {
    int childCount = treeModel.getChildCount(parent);
    for (int i = 0; i < childCount; i++) {
      Object child = treeModel.getChild(parent, i);
      if (recursiveMatch(child, filter)) {
        if (childToFind.equals(child)) {
          return i;
        }
      }
    }
    return -1;
  }

  @Override
  public void addTreeModelListener(final TreeModelListener l) {
    treeModel.addTreeModelListener(l);
  }

  @Override
  public void removeTreeModelListener(final TreeModelListener l) {
    treeModel.removeTreeModelListener(l);
  }
}

Example Application

JTree before and after filtering

Below is a simple example of how the FilteredTreeModel can be used. The tree is filtered using the value of the text field as characters are typed.

FilteredJTreeExample.java

package org.adrianwalker.filteredjtree;

import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import javax.swing.JFrame;
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.DefaultTreeModel;

public final class FilteredJTreeExample {

  public static void main(final String[] args) {
    javax.swing.SwingUtilities.invokeLater(new Runnable() {

      @Override
      public void run() {
        createAndShowGUI();
      }
    });
  }

  private static void createAndShowGUI() {
    JFrame frame = new JFrame("Filtered JTree Demo");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    addComponentsToPane(frame.getContentPane());

    frame.pack();
    frame.setVisible(true);
  }

  private static void addComponentsToPane(final Container pane) {
    pane.setLayout(new GridBagLayout());

    JTree tree = createTree(pane);
    JTextField filter = createFilterField(pane);

    filter.getDocument().addDocumentListener(createDocumentListener(tree, filter));
  }

  private static JTree createTree(final Container pane) {
    DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree");
    FilteredTreeModel model = new FilteredTreeModel(new DefaultTreeModel(root));
    JTree tree = new JTree(model);
    JScrollPane scrollPane = new JScrollPane(tree);
    GridBagConstraints c = new GridBagConstraints();
    c.weightx = 1;
    c.weighty = 1;
    c.fill = GridBagConstraints.BOTH;
    c.gridx = 0;
    c.gridy = 1;
    pane.add(scrollPane, c);
    createTreeNodes(root);
    expandTree(tree);

    return tree;
  }

  private static JTextField createFilterField(final Container pane) {
    JTextField filter = new JTextField();
    GridBagConstraints c = new GridBagConstraints();
    c.weightx = 0;
    c.weighty = 0;
    c.fill = GridBagConstraints.HORIZONTAL;
    c.gridx = 0;
    c.gridy = 0;
    pane.add(filter, c);

    return filter;
  }
  
  private static DocumentListener createDocumentListener(final JTree tree, final JTextField filter) {
    return new DocumentListener() {

      @Override
      public void insertUpdate(final DocumentEvent e) {
        applyFilter();
      }

      @Override
      public void removeUpdate(final DocumentEvent e) {
        applyFilter();
      }

      @Override
      public void changedUpdate(final DocumentEvent e) {
        applyFilter();
      }

      public void applyFilter() {
        FilteredTreeModel filteredModel = (FilteredTreeModel) tree.getModel();
        filteredModel.setFilter(filter.getText());
        
        DefaultTreeModel treeModel = (DefaultTreeModel) filteredModel.getTreeModel();
        treeModel.reload();
        
        expandTree(tree);
      }
    };
  }

  private static void expandTree(final JTree tree) {
    for (int i = 0; i < tree.getRowCount(); i++) {
      tree.expandRow(i);
    }
  }

  private static void createTreeNodes(final DefaultMutableTreeNode node) {
    DefaultMutableTreeNode ab = new DefaultMutableTreeNode("ab");
    DefaultMutableTreeNode cd = new DefaultMutableTreeNode("cd");
    DefaultMutableTreeNode ef = new DefaultMutableTreeNode("ef");
    DefaultMutableTreeNode gh = new DefaultMutableTreeNode("gh");
    DefaultMutableTreeNode ij = new DefaultMutableTreeNode("ij");
    DefaultMutableTreeNode kl = new DefaultMutableTreeNode("kl");
    DefaultMutableTreeNode mn = new DefaultMutableTreeNode("mn");
    DefaultMutableTreeNode op = new DefaultMutableTreeNode("op");
    DefaultMutableTreeNode qr = new DefaultMutableTreeNode("qr");
    DefaultMutableTreeNode st = new DefaultMutableTreeNode("st");
    DefaultMutableTreeNode uv = new DefaultMutableTreeNode("uv");
    DefaultMutableTreeNode wx = new DefaultMutableTreeNode("wx");
    DefaultMutableTreeNode yz = new DefaultMutableTreeNode("yz");

    node.add(ab);
    node.add(cd);
    ab.add(ef);
    ab.add(gh);
    cd.add(ij);
    cd.add(kl);
    ef.add(mn);
    ef.add(op);
    gh.add(qr);
    gh.add(st);
    ij.add(uv);
    ij.add(wx);

    node.add(yz);
  }
}

Source Code

Usage

Build and install the Filtered JTree project, using 'mvn clean install'.

Run the FilteredJTreeExample class, and enter characters in the filter text field.