OK, I couldn't really find a decent example for a custom TreeModel in Swing that uses more than the basic features, so I wrote my own (code follows below) so I can ask questions about it, rather than about a more complicated application that's the one I really want to write when I understand how to write it. Apologies for the fact that there are multiple related questions here, it's hard to ask these Q's in a vacuum w/o an example to refer to, and I figure it's better to put up the example once in one post, than to separate out my questions. My real application is not infinite, just large (state stored in a database) so a custom TreeModel seems appropriate.
package com.example.test.gui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Map;
import javax.swing.AbstractCellEditor;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
/*
* GUI rendering of the ancestry of hailstone numbers
* (see http://mathworld.wolfram.com/CollatzProblem.html)
*
* This is an infinite tree model.
*
* each node in the tree is a Long number
* each node has 1 or 2 children:
* all nodes N have a child 2N
* any node N = 3k+1, where k > 0, has a second child k
*
* checkboxes are present just to see custom rendering
* - nodes N where N is divisible by 7 are editable, the rest are not
* - editable nodes override their default state (stored in a hashmap)
* - default state of a node N is checked if N is divisible by 5,
* unchecked otherwise
*/
class HailstoneTreeModel implements TreeModel {
final private Map<Long,Boolean> modifiedCheckState = new HashMap<Long,Boolean>();
@Override public Object getChild(Object parent, int index) {
if (!(parent instanceof Long))
return null;
if (index < 0 || index > 1)
return null;
final long l = ((Long)parent).longValue();
if (index == 0)
{
return (l*2);
}
else if ((l > 1) && (l-1)%3 == 0)
{
return (l-1)/3;
}
else
return null;
}
@Override public int getChildCount(Object parent) {
if (!(parent instanceof Long))
return 0;
final long l = ((Long)parent).longValue();
if ((l > 1) && (l-1) % 3 == 0)
return 2;
return 1;
}
@Override public int getIndexOfChild(Object parent, Object child) {
if (parent instanceof Long && child instanceof Long)
{
final long p = ((Long)parent).longValue();
final long c = ((Long)child).longValue();
if (p*2 == c)
return 0;
if (p == 3*c+1)
return 1;
}
return -1;
}
@Override public Object getRoot() {
return 1L;
}
@Override public boolean isLeaf(Object arg0) {
return false;
}
@Override
public void addTreeModelListener(TreeModelListener arg0) {
// TODO Auto-generated method stub
}
@Override
public void removeTreeModelListener(TreeModelListener arg0) {
// TODO Auto-generated method stub
}
@Override
public void valueForPathChanged(TreePath arg0, Object arg1) {
// !!! what is typically done here and when does this get called?
}
public boolean isEditable(TreePath path) {
if (path != null) {
Object node = path.getLastPathComponent();
// only the nodes divisible by 7 are editable
if (node instanceof Long)
{
return ((Long)node) % 7 == 0;
}
}
return false;
}
private void _setState(Long value, boolean selected)
{
this.modifiedCheckState.put(value, selected);
System.out.println(value+" -> "+selected);
}
public void setState(Object value, boolean selected) {
if (value instanceof Long)
{
_setState((Long)value, selected);
}
}
private boolean _getState(Long value)
{
Boolean b = this.modifiedCheckState.get(value);
if (b != null)
{
return b.booleanValue();
}
return (value.longValue() % 5 == 0);
}
public boolean getState(Object value)
{
if (value instanceof Long)
{
return _getState((Long) value);
}
return false;
}
public void toggleState(Object value) {
if (value instanceof Long)
{
_setState((Long)value, !_getState((Long)value));
}
}
}
// adapted from http://www.java2s.com/Code/Java/Swing-JFC/CheckBoxNodeTreeSample.htm
class CheckBoxNodeRenderer implements TreeCellRenderer {
final private JCheckBox nodeRenderer = new JCheckBox();
final private HailstoneTreeModel model;
private Long currentValue = null; // value currently being displayed/edited
final private Color selectionBorderColor, selectionForeground, selectionBackground,
textForeground, textBackground;
protected JCheckBox getNodeRenderer() {
return this.nodeRenderer;
}
public CheckBoxNodeRenderer(HailstoneTreeModel model) {
this.model=model;
Font fontValue;
fontValue = UIManager.getFont("Tree.font");
if (fontValue != null) {
this.nodeRenderer.setFont(fontValue);
}
Boolean booleanValue = (Boolean) UIManager
.get("Tree.drawsFocusBorderAroundIcon");
this.nodeRenderer.setFocusPainted((booleanValue != null)
&& (booleanValue.booleanValue()));
this.selectionBorderColor = UIManager.getColor("Tree.selectionBorderColor");
this.selectionForeground = UIManager.getColor("Tree.selectionForeground");
this.selectionBackground = UIManager.getColor("Tree.selectionBackground");
this.textForeground = UIManager.getColor("Tree.textForeground");
this.textBackground = UIManager.getColor("Tree.textBackground");
}
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
Component returnValue = this.nodeRenderer;
String stringValue = tree.convertValueToText(value, selected,
expanded, leaf, row, false);
this.nodeRenderer.setText(stringValue);
this.nodeRenderer.setSelected(false);
this.nodeRenderer.setEnabled(tree.isEnabled());
if (selected) {
this.nodeRenderer.setForeground(this.selectionForeground);
this.nodeRenderer.setBackground(this.selectionBackground);
} else {
this.nodeRenderer.setForeground(this.textForeground);
this.nodeRenderer.setBackground(this.textBackground);
}
if (value instanceof Long)
{
this.currentValue = (Long) value;
}
this.nodeRenderer.setSelected(this.model.getState(value));
returnValue = this.nodeRenderer;
return returnValue;
}
public Long getCurrentValue() { return this.currentValue; }
}
class CheckBoxNodeEditor extends AbstractCellEditor implements TreeCellEditor {
final CheckBoxNodeRenderer renderer;
final HailstoneTreeModel model;
public CheckBoxNodeEditor(HailstoneTreeModel model) {
this.model = model;
this.renderer = new CheckBoxNodeRenderer(model);
ItemListener itemListener = new ItemListener() {
public void itemStateChanged(ItemEvent itemEvent) {
Object cb = itemEvent.getItem();
if (cb instanceof JCheckBox && itemEvent.getStateChange() == ItemEvent.SELECTED)
{
Long v = CheckBoxNodeEditor.this.renderer.getCurrentValue();
CheckBoxNodeEditor.this.model.toggleState(v);
}
// !!! the following 3 lines are important because... ?
if (stopCellEditing()) {
fireEditingStopped();
}
}
};
this.renderer.getNodeRenderer().addItemListener(itemListener);
}
public Object getCellEditorValue() {
JCheckBox checkbox = this.renderer.getNodeRenderer();
return checkbox;
}
@Override public boolean isCellEditable(EventObject event) {
boolean returnValue = false;
Object source = event.getSource();
if (event instanceof MouseEvent && source instanceof JTree) {
MouseEvent mouseEvent = (MouseEvent) event;
TreePath path = ((JTree)source).getPathForLocation(mouseEvent.getX(),
mouseEvent.getY());
returnValue = this.model.isEditable(path);
}
return returnValue;
}
public Component getTreeCellEditorComponent(JTree tree, final Object value,
boolean selected, boolean expanded, boolean leaf, int row) {
Component editor = this.renderer.getTreeCellRendererComponent(tree, value,
true, expanded, leaf, row, true);
return editor;
}
}
public class VirtualTree1 {
public static void main(String[] args) {
HailstoneTreeModel model = new HailstoneTreeModel();
// Create a JTree and tell it to display our model
JTree tree = new JTree(model);
tree.setCellRenderer(new CheckBoxNodeRenderer(model));
tree.setCellEditor(new CheckBoxNodeEditor(model));
tree.setEditable(true);
// The JTree can get big, so allow it to scroll
JScrollPane scrollpane = new JScrollPane(tree);
// Display it all in a window and make the window appear
JFrame frame = new JFrame("Hailstone Tree Demo");
frame.getContentPane().add(scrollpane, "Center");
frame.setSize(400,600);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
See the first comment (should be visible above) for what this is showing. It's a custom TreeModel displaying an infinite tree, something that would not be possible in a "regular" tree where all nodes in the tree need to actually exist in memory, but is possible using custom TreeModels since the only parts that are displayed/instantiated are the ones the user clicks on, which is inherently finite. :-)
I have a few miscellaneous questions. An accepted answer for this post will be given for the best answer to my question #4.
1) TreeModel listeners -- am I correct in assuming that this is for classes that want to receive events for updates from my TreeModel? (whether I write them or someone else does) What are typical use cases?
2) TreeModel.valueForPathChanged()
-- when does this get called, and what would I typically do about it?
3) (this has to do with TreeCellEditor / TreeCellRenderer) -- the lines in the example I adapted had the following call:
if (stopCellEditing()) {
fireEditingStopped();
}
what's this for?
4) as far as class organization -- is there a better way to structure this sort of thing? I figure I should have a separation between the TreeModel (M = Model in MVC) and the TreeCellEditor / TreeCellRenderer (V = view, or C = controller, I'm not sure), but they vaguely need to know about each other, and I wasn't sure which should contain a reference to which. Right now I have the TreeModel as an independent object, and the editor/renderer has a reference to the TreeModel so it can query/mutate the model as necessary. Also I'm wondering if maybe the custom TreeCellEditor and TreeCellRenderer should really be one class that implements both interfaces. My itemStateChanged()
method in CheckBoxNodeEditor
seems kind of weird... I get an item listener event when the checkbox gets clicked, and then I kind of assume this event comes from the renderer, and toggle the appropriate value since I can't seem to figure out how to determine if the check box is presently checked or unchecked, selection in this checkbox object seems to be whether the mouse has been clicked or let go, rather than the checkbox state.
There's probably more than one way to restructure this so it seems like a better, more modularized approach, but right now I'm not sure how to do this so any suggestions are appreciated.
5) Directed Acyclic Graphs (DAG) displayed as a tree hierarchy -- If you run the application, then expand the nodes 1,2,4,8,16,32,64,128, you'll see a second "1" which is really the same node as the first "1" since node values in this example application are just Long
objects. If you expand this second "1" to 1,2,4,8,16,32,64,128, you will now see two "21" nodes. The "21" node can be checked/unchecked as necessary. But in an ideal world, both "21"s would update their checked state when I click on one of them. Is there any way to do this automatically? Or do I have to keep track of all the multiple paths for a single node that are currently displayed at the same time? (alternatively, all the paths for a single node that exist -- possible in a finite DAG, not possible in an infinite DAG) This is only an issue with DAGs where there are multiple paths to reach the same node... I have to deal with this in my application.