package cryodex.widget;
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 abstract class FilteredTree extends JPanel {
private static final long serialVersionUID = 1L;
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.getDocument().addDocumentListener(
new DocumentListener() {
@Override
public void changedUpdate(DocumentEvent e) {
filter();
}
@Override
public void removeUpdate(DocumentEvent e) {
filter();
}
@Override
public void insertUpdate(DocumentEvent e) {
filter();
}
private void filter() {
filterTree(field.getText());
}
});
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 = (DefaultMutableTreeNode) enm
.nextElement();
newOne.add(copyNode(child));
}
return newOne;
}
protected abstract boolean matches(DefaultMutableTreeNode node,
String testToMatch);
/**
* 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 {
private static final long serialVersionUID = 1L;
@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))
// {
if (!matches(leaf, textToMatch)) {
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) leaf
.getParent();
if (parent != null)
parent.remove(leaf);
badLeaves = true;
}
leaf = nextLeaf;
}
return badLeaves;
}
}
}