Hi, I have a JTree with custom model. I test it with the following structure:
+root
----+category
---------+file
When the app starts,
root and
category are visible,
file is hidden. When I try to expand the
category node, the whole JTree disappears. Any further clicks in the place previously occupied by the JTree result in a
java.lang.NullPointerException at javax.swing.plaf.basic.BasicTreeUI$Handler.handleSelection().
The nodes I use represent a simple tree, where every node has its value and a LinkedList of its children. The model simply translates this to the "language" of TreeModel.
It is essential for me to correctly define equals() and hashCode() for the nodes. I found out that if the nodes' hashCode() doesn't rely on its list of children, everything works well. But I want it to rely on the children!
I use JRE 1.6.0_22 on Win7.
Where do I do something wrong, please?
Thanks very much for any help ;)
PS: I managed to workaround this - in a TreeSelectionListener I call tree.setModel(tree.getModel()) and then I restore the expanded and selected nodes. This works, but of course I'd rather have a cleaner solution. Moreover, this seems as a bug in JRE for me.
SSCCE included:
import java.awt.Dimension;
import java.util.LinkedList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
public class JTreeTest
{
public static void main(String[] args)
{
// setup the tree
final File list = new File();
list.setValue("root");
File category = new File();
list.getFiles().add(category);
category.setValue("category");
File file = new File();
category.getFiles().add(file);
file.setValue("file");
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run()
{
JFrame frame = new JFrame();
// setup the JTree
JTree tree = new JTree(new MyModel(list));
frame.add(tree);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(new Dimension(200, 100));
frame.setVisible(true);
}
});
}
// this model is very very basic and I don't see much space here for errors...
static class MyModel implements TreeModel
{
protected File root;
protected List<TreeModelListener> treeModelListeners = new LinkedList<TreeModelListener>();
public MyModel(File root)
{
this.root = root;
}
@Override
public File getRoot()
{
return root;
}
@Override
public Object getChild(Object parent, int index)
{
if (index < 0)
return null;
if (!(parent instanceof File))
return null;
File fParent = (File) parent;
try {
return fParent.getFiles().get(index);
} catch (ArrayIndexOutOfBoundsException e) {
return null;
}
}
@Override
public int getChildCount(Object parent)
{
if (!(parent instanceof File))
return 0;
File fParent = (File) parent;
return fParent.getFiles().size();
}
@Override
public boolean isLeaf(Object node)
{
return getChildCount(node) == 0;
}
@Override
public void valueForPathChanged(TreePath path, Object newValue)
{
if (!(newValue instanceof File))
return;
if (path.getParentPath() == null) {
fireTreeNodesChanged(new TreeModelEvent(this, path, null, null));
} else {
fireTreeNodesChanged(new TreeModelEvent(this, path.getParentPath(), new int[] { getIndexOfChild(path
.getParentPath().getLastPathComponent(), newValue) }, new Object[] { newValue }));
}
}
@Override
public int getIndexOfChild(Object parent, Object child)
{
if (parent == null || child == null)
return -1;
if (!(parent instanceof File) || !(child instanceof File))
return -1;
File fParent = (File) parent;
return fParent.getFiles().indexOf(child);
}
@Override
public void addTreeModelListener(TreeModelListener l)
{
treeModelListeners.add(l);
}
@Override
public void removeTreeModelListener(TreeModelListener l)
{
treeModelListeners.remove(l);
}
public void fireTreeNodesChanged(TreeModelEvent e)
{
for (TreeModelListener listener : treeModelListeners) {
listener.treeNodesChanged(e);
}
}
}
static class File
{
static int fileId = 0;
int id;
List<File> files = null;
String value = null;
public File()
{
// ensure each item will have a really unique identifier, so no equals() collisions should occur
id = fileId++;
}
public List<File> getFiles()
{
if (files == null)
files = new LinkedList<File>();
return files;
}
public String getValue()
{
return value;
}
public void setValue(String value)
{
this.value = value;
}
// generated by Eclipse code helpers
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((value == null) ? 0 : value.hashCode());
// ///////HERE IT IS////////
// if you uncomment the following line, the list will get empty when you expand the second category and
// any following clicks in any place that should be occupied by an item will result in a
// NullPointerException
//
result = prime * result + ((files == null) ? 0 : files.hashCode());
//
result = prime * result + id;
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
File other = (File) obj;
if (value == null) {
if (other.value != null)
return false;
} else if (!value.equals(other.value))
return false;
if (files == null) {
if (other.files != null)
return false;
} else if (!files.equals(other.files))
return false;
if (id != other.id)
return false;
return true;
}
@Override
public String toString()
{
return "File [value=" + value + "]";
}
}
}