Search code examples
javaswingjtreejcheckbox

JTree - TreeCellEditor Implemetation changing the Node Object Class


I'm trying to build a JTree with TestObjectCheckBoxNode leafs where each leaf holds a TestObject, everything appears to work except when I check the box in the leaf it is changing the node from a TestObjectCheckBoxNode to the CheckBoxNode superclass. I know this is happening in the TreeCellEditor implementation, specifically CheckBoxNodeEditor:getCellEditorValue(), because it is creating the updated TreeCellRenderer from the UI of that node.

CheckBoxNode checkBoxNode = 
  new CheckBoxNode(checkBoxPanel.label.getText(),
                   checkBoxPanel.checkBox.isSelected());
return checkBoxNode;

I'm at a complete loss for how would I do this in a way where I have access to the TestObject for the selected node in CheckBoxNodeEditor, so I could do something like this:

TestObjectCheckBoxNode testObjectCheckBoxNode = 
  new TestObjectCheckBoxNode(testObject,
                             checkBoxPanel.checkBox.isSelected());
return testObjectCheckBoxNode;

Here is the complete code below:

import java.awt.BorderLayout;
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.awt.font.TextAttribute;
import java.util.Collections;
import java.util.Enumeration;
import java.util.EventObject;
import java.util.Vector;

import javax.swing.AbstractCellEditor;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;

public class JCheckBoxTreeTest 
{

  private TestObjectCheckBoxTree tree;

  public static void main(String... s)
  {
    new JCheckBoxTreeTest();
  }

  public JCheckBoxTreeTest() 
  {
    JFrame frame = new JFrame("JCheckBoxTreeTest Tree");


    Vector rootVector = new Category("Root", new Object[]
        {              
            new Category("POI", 
                new TestObjectCheckBoxNode[] {
                    new TestObjectCheckBoxNode(new TestObject("TestObject 1"),true),
                    new TestObjectCheckBoxNode(new TestObject("TestObject 2"),true),
            }),
        });




    tree = new TestObjectCheckBoxTree(rootVector);

    tree.addTreeSelectionListener(new TreeSelectionListener() 
    {
      public void valueChanged(TreeSelectionEvent e) 
      {
        DefaultMutableTreeNode node = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();

        if (node == null) return;

        Object userObject = ((DefaultMutableTreeNode) node).getUserObject();

        System.err.println("node: " + node.toString());
        System.err.println("userObject: " + userObject.toString());

      }
    });

    tree.expandAll();

    JScrollPane scrollPane = new JScrollPane(tree);
    frame.getContentPane().add(scrollPane, BorderLayout.NORTH);
    frame.setSize(360, 600);
    frame.setVisible(true);
  }



  class TestObject
  {
    String name;  
    public TestObject(String inStr)
    {
      name = inStr;
    }
  }

  public class CheckBoxNode 
  {
    String text;

    boolean selected;

    public CheckBoxNode(String text, boolean selected) 
    {
      this.text = text;
      this.selected = selected;
    }

    public boolean isSelected() { return selected; }

    public void setSelected(boolean newValue) { selected = newValue; }

    public String getText() { return text; }

    public void setText(String newValue) { text = newValue; }

    public String toString() { return getClass().getName() + "[" + text + "/" + selected + "]"; }
  }

  public class TestObjectCheckBoxNode extends CheckBoxNode
  {
    TestObject testObject;

    public TestObjectCheckBoxNode(TestObject testObject, boolean selected) 
    {
      super(testObject.name, selected);
      this.testObject = testObject;
    }  
  }




  public class CheckBoxTree extends JTree
  {    
    public CheckBoxTree(Vector rootVector)
    {     
      super(rootVector);      

      CheckBoxNodeRenderer renderer = new CheckBoxNodeRenderer();
      setCellRenderer(renderer);
      setCellEditor(new CheckBoxNodeEditor());
      setEditable(true);
    }   

    public void expandAll()
    {      
      expandAll(this, new TreePath(((DefaultMutableTreeNode)this.treeModel.getRoot()).getPath()), true);
    }


    private void expandAll(JTree tree, TreePath path, boolean expand) {
      TreeNode node = (TreeNode) path.getLastPathComponent();

      if (node.getChildCount() >= 0) {
        Enumeration<? extends TreeNode> enumeration = node.children();
        while (enumeration.hasMoreElements()) {
          TreeNode treeNode = enumeration.nextElement();
          TreePath treePath = path.pathByAddingChild(treeNode);

          expandAll(tree, treePath, expand);
        }
      }

      if (expand) {
        tree.expandPath(path);
      } else {
        tree.collapsePath(path);
      }
    }
  }

  class CheckBoxPanel extends JPanel
  {
    public JCheckBox checkBox;
    public JLabel label;

    public CheckBoxPanel() 
    { 
      super();

      checkBox = new JCheckBox();
      label = new JLabel();

      checkBox.setBorder(new EmptyBorder(0, 0, 0, 0));

      add(checkBox);
      add(label);
    }
  }

  class CheckBoxNodeRenderer implements TreeCellRenderer 
  {
    private CheckBoxPanel leafRenderer = new CheckBoxPanel();

    private DefaultTreeCellRenderer nonLeafRenderer = new DefaultTreeCellRenderer();

    Color selectionBorderColor, selectionForeground, selectionBackground,
    textForeground, textBackground;

    protected CheckBoxPanel getLeafRenderer() {
      return leafRenderer;
    }

    public CheckBoxNodeRenderer() {
      Font fontValue;
      fontValue = UIManager.getFont("Tree.font");
      if (fontValue != null)
      {
        leafRenderer.checkBox.setFont(fontValue);
        leafRenderer.label.setFont(fontValue);

        //set the nonLeaf text to bold
        nonLeafRenderer.setFont(fontValue.deriveFont(Collections.singletonMap(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD)));
      }


      Boolean booleanValue = (Boolean) UIManager.get("Tree.drawsFocusBorderAroundIcon");
      leafRenderer.checkBox.setFocusPainted((booleanValue != null) && (booleanValue.booleanValue()));

      selectionBorderColor = UIManager.getColor("Tree.selectionBorderColor");
      selectionForeground = UIManager.getColor("Tree.selectionForeground");
      selectionBackground = UIManager.getColor("Tree.selectionBackground");
      textForeground = UIManager.getColor("Tree.textForeground");
      textBackground = UIManager.getColor("Tree.textBackground");
    }

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

      Component returnValue;
      if (leaf) 
      {

        String stringValue = tree.convertValueToText(value, selected,
            expanded, leaf, row, false);

        leafRenderer.checkBox.setSelected(false);
        leafRenderer.label.setText(stringValue);

        leafRenderer.setEnabled(tree.isEnabled());

        if (selected) {
          leafRenderer.setForeground(selectionForeground);
          leafRenderer.setBackground(selectionBackground);
        } else {
          leafRenderer.setForeground(textForeground);
          leafRenderer.setBackground(textBackground);
        }

        if ((value != null) && (value instanceof DefaultMutableTreeNode)) {
          Object userObject = ((DefaultMutableTreeNode) value)
              .getUserObject();
          if (userObject instanceof CheckBoxNode) {
            CheckBoxNode node = (CheckBoxNode) userObject;                                        
            leafRenderer.checkBox.setSelected(node.isSelected());
            leafRenderer.label.setText(node.getText());
          }
        }
        returnValue = leafRenderer;
      } 
      else 
      {
        returnValue = nonLeafRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
      }
      return returnValue;
    }
  }

  class CheckBoxNodeEditor extends AbstractCellEditor implements TreeCellEditor {

    CheckBoxNodeRenderer renderer = new CheckBoxNodeRenderer();
      TestObject testObject;
      
    ChangeEvent changeEvent = null;

    public Object getCellEditorValue() 
    {
      CheckBoxPanel checkBoxPanel = renderer.getLeafRenderer();
      
          if (testObject != null) 
          { 
            return new TestObjectCheckBoxNode(testObject, checkBoxPanel.checkBox.isSelected());
          }
          else 
          { 
            return new CheckBoxNode(checkBoxPanel.label.getText(), checkBoxPanel.checkBox.isSelected()); 
          }
    }

    public boolean isCellEditable(EventObject event) {
      boolean returnValue = false;

      if (event instanceof MouseEvent) {
        MouseEvent mouseEvent = (MouseEvent) event;

        JTree tree = (JTree)event.getSource();

        TreePath path = tree.getPathForLocation(mouseEvent.getX(), mouseEvent.getY());
        if (path != null) {
          Object node = path.getLastPathComponent();
          if ((node != null) && (node instanceof DefaultMutableTreeNode)) {
            DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) node;
            Object userObject = treeNode.getUserObject();
            returnValue = ((treeNode.isLeaf()) && (userObject instanceof CheckBoxNode));
          }
        }
      }
      return returnValue;
    }

    public Component getTreeCellEditorComponent(JTree tree, Object value,
        boolean selected, boolean expanded, boolean leaf, int row) {
      
      Object userObject = ((DefaultMutableTreeNode)value).getUserObject();
                 
          if (userObject instanceof TestObjectCheckBoxNode) 
          {            
              testObject = ((TestObjectCheckBoxNode)userObject).testObject;             
          }
          else 
          {
              testObject = null;
          }
          
      Component editor = renderer.getTreeCellRendererComponent(tree, value,
          true, expanded, leaf, row, true);

      // editor always selected / focused
      ItemListener itemListener = new ItemListener() {
        public void itemStateChanged(ItemEvent itemEvent) {
          if (stopCellEditing()) {
            fireEditingStopped();
          }
        }
      };

      if (editor instanceof CheckBoxPanel) 
      {
        ((CheckBoxPanel) editor).checkBox.addItemListener(itemListener);
      }

      return editor;
    }
  }



  class Category extends Vector 
  {
    String name;

    public Category(String name) 
    {
      this.name = name;
    }

    public Category(String name, Object elements[]) 
    {
      this.name = name;
      for (int i = 0, n = elements.length; i < n; i++) 
      {
        add(elements[i]);
      }
    }

    public String toString() 
    {
      return "[" + name + "]";
    }
  }



  class TestObjectCheckBoxTree extends CheckBoxTree
  {  
    public TestObjectCheckBoxTree(Vector rootVector)
    {     
      super(rootVector);
    }    
  }  
}

Solution

  • When editing the tree node Swing calls CheckBoxNodeEditor.getTreeCellEditorComponent() with the current node as the value parameter. Later when you stop editing the node it calls CheckBoxNodeEditor.getCellEditorValue().

    What you need to do: if the value in getTreeCellEditorComponent() is a TestObjectCheckBoxNode then store its testObject field into an instance field of the CheckBoxNodeEditor.

    Later when getCellEditorValue() is called you can either return a TestObjectCheckBoxNode if the testObject was stored, otherwise return a ChechBoxNode.

    The code could look like this (shortened) example:

    class CheckBoxNodeEditor extends AbstractCellEditor implements TreeCellEditor {
        CheckBoxNodeRenderer renderer = new CheckBoxNodeRenderer();
        TestObject testObject;
    
        public Object getCellEditorValue() {
            CheckBoxPanel checkBoxPanel = renderer.getLeafRenderer();
            if (testObject != null) {
                return new TestObjectCheckBoxNode(testObject, checkBoxPanel.checkBox.isSelected());
            } else {
                return new CheckBoxNode(checkBoxPanel.label.getText(), checkBoxPanel.checkBox.isSelected());
            }
        }
    
        public Component getTreeCellEditorComponent(
            JTree tree, Object value,
            boolean selected, boolean expanded, boolean leaf, int row
        ) {
            Object realValue = ((DefaultMutableTreeNode) value).getUserObject();
            if (realValue instanceof TestObjectCheckBoxNode n) {
                testObject = n.testObject;
            } else {
                testObject = null;
            }
            // remaining code from your getTreeCellEditorComponent method
        }
        // other code removed for brevity
    }
    

    If you can't use the new pattern matching instanceof operator (because you don't use Java 16 or later) the if statement at the beginning of getTreeCellEditorComponent would need to be written as

        public Component getTreeCellEditorComponent(
            JTree tree, Object value,
            boolean selected, boolean expanded, boolean leaf, int row
        ) {
            Object realValue = ((DefaultMutableTreeNode) value).getUserObject();
            if (realValue instanceof TestObjectCheckBoxNode) {
                TestObjectCheckBoxNode n = (TestObjectCheckBoxNode) value;
                testObject = n.testObject;
            } else {
                testObject = null;
            }
            // remaining code from your getTreeCellEditorComponent method
        }
    

    Why is the value in getTreeCellEditorComponent a DefaultMutableTreeNode?

    The explanation is that JTree exclusively works with TreeNode instances. If you create a JTree with a Vector, a Hashtable or an Object[] it wraps the values that these contain in JTree.DynamicUtilTreeNode instances and sets your objects as userObject property on them.

    The JTree.DynamicUtilTreeNode extends from DefaultMutableTreeNode.