0

I got a JTree with a custom TreeCellRenderer.
This renderer is a panel containing a checkbox and a label.
While the text of the label is fixed for each node (specified in the UserObject of the DefaultMutableTreeNode), this text may or may not be bold. This depends on the status of the checkbox.
When the checkbox is de-selected, the label text is no longer bold but its width remains unchanged (too broad).
Similar, when selecting the checkbox, the text is reported bold but the label is not enlarged.
This causes the text to be truncated.

The real-life situation is a little more complicated but here below is a full example.
In order to reproduce the problem:

  • Select the first node by clicking on the text
  • Press the space bar to de-select the checkbox. The text is not bold but the label is too large.
  • Click the expand button on the first node. The label width is adjusted
  • Press the space bar to select the combobox. The text becomes bold but is truncated.

I tried to insert several calls to invalidate, repaint, etc. but nothing solves the problem.
The problem occurs both in the default look-and-feel and the system (Windows) look-and-feel.

import java.awt.*;
import javax.swing.*;

@SuppressWarnings("serial")
public class TestFrame extends JFrame
{
  public TestFrame()
  {
    getContentPane().setLayout(new GridBagLayout());
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setTitle("Test TreeCellRenderer");

    JScrollPane tree_pane;
    tree_pane = new JScrollPane();
    tree_pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    tree_pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
    tree_pane.setPreferredSize(new Dimension(300, 200));

    TestTree tree;
    tree = new TestTree();
    tree_pane.getViewport().add(tree, null);

    GridBagConstraints constraints;
    constraints = new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.NORTH,
                                         GridBagConstraints.BOTH, new Insets(8, 8, 8, 8), 0, 0);
    getContentPane().add(tree_pane, constraints);

    pack();
    setMinimumSize(getPreferredSize());

  }

  public static void main(String[] args)
  {
    try
    {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
      TestFrame frame;
      frame = new TestFrame();
      frame.setVisible(true);
    }
    catch (Exception exception)
    {
      exception.printStackTrace();
    }

  } // main

} // class TestFrame

This class implements my tree:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.*;

@SuppressWarnings("serial")
public class TestTree extends JTree
{
  // The width of the checkbox within the renderer
  // We need it when a node is clicked in order to check what part is exactly underneath the mouse
  public int checkboxWidth;

  public TestTree()
  {
    // Initialize object
    super(getNodes());
    setRootVisible(false);
    setShowsRootHandles(true);
    setCellRenderer(new MyCellRenderer());
    addMouseListener(new TreeMouseManager());
    addKeyListener(new TreeKeyManager());

  } // constructor

  private void toggleCheckBox(TreePath treePath)
  {
    // Determine node being toggled
    Object[]               path;
    DefaultMutableTreeNode node;
    NodeInfo            info;
    path = treePath.getPath();
    node = (DefaultMutableTreeNode)path[path.length - 1];
    info = (NodeInfo)node.getUserObject();

    // Toggle selection
    info.checked = !info.checked;
    repaint();

  } // toggleCheckBox

  private class TreeMouseManager extends MouseAdapter
  {
    @Override
    public void mouseClicked(MouseEvent event)
    {
      // Determine node corresponding to location
      TreePath treePath;
      treePath = getPathForLocation(event.getX(), event.getY());
      if (treePath == null)
        return;

      // Manage only single click with left button
      if ((event.getClickCount() != 1) || (event.getButton() != MouseEvent.BUTTON1))
        return;

      // Determine horizontal position of checkbox
      BasicTreeUI ui;
      int         depth;
      int         leftIndent;
      int         rightIndent;
      int         checkboxLeft;
      int         checkboxRight;
      ui = (BasicTreeUI)getUI();
      depth = treePath.getPathCount();
      leftIndent = ui.getLeftChildIndent();
      rightIndent = ui.getRightChildIndent();
      checkboxLeft = (depth - 1) * (leftIndent + rightIndent);
      checkboxRight = checkboxLeft + checkboxWidth - 1;

      // Ignore if not clicked on checkbox
      int x;
      x = event.getX();
      if ((x < checkboxLeft) || (x > checkboxRight))
        return;

      // Toggle checkbox
      toggleCheckBox(treePath);

    } // mouseClicked

  } // class TreeMouseManager

  private class TreeKeyManager extends KeyAdapter
  {
    @Override
    public void keyPressed(KeyEvent event)
    {
      // Determine selected element
      TreePath treePath;
      treePath = getSelectionPath();
      if (treePath == null)
        return;

      // Manage event for this element
      if (event.getKeyCode() == KeyEvent.VK_SPACE)
        toggleCheckBox(treePath);

    } // keyPressed

  } // class TreeKeyManager

  private class MyCellRenderer extends JPanel implements TreeCellRenderer
  {
    public MyCellRenderer()
    {
      // Create components
      checkbox = new JCheckBox();
      checkbox.setBorder(null);
      checkbox.setOpaque(false);
      label = new JLabel();
      label.setBorder(new EmptyBorder(new Insets(0, 2, 0, 2)));

      // Initialize panel
      GridBagConstraints constraints;
      setLayout(new GridBagLayout());
      setOpaque(false);
      constraints = new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,
                                           GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
      add(checkbox, constraints);
      constraints = new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST,
                                           GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);
      add(label, constraints);

      // Save the width of the checkbox
      // We need it when the mouse is clicked on a node
      checkboxWidth = (int)checkbox.getPreferredSize().getWidth();

    } // constructor

    @Override
    public Component getTreeCellRendererComponent(JTree   tree,
                                                  Object  value,
                                                  boolean selected,
                                                  boolean expanded,
                                                  boolean leaf,
                                                  int     row,
                                                  boolean hasFocus)
    {
      // Make data accessible
      // Ignore if it's the root node
      DefaultMutableTreeNode node;
      NodeInfo            info;
      node = (DefaultMutableTreeNode)value;
      if (node.getUserObject() instanceof NodeInfo)
        info = (NodeInfo)node.getUserObject();
      else
        return (this);

      // Determine font
      Font font;
      font = label.getFont();
      if (info.checked)
        font = font.deriveFont(font.getStyle() | Font.BOLD);
      else
        font = font.deriveFont(font.getStyle() & ~Font.BOLD);

      // Configure components
      checkbox.setSelected(info.checked);
      label.setText(info.name);
      label.setOpaque(selected);
      label.setFont(font);
      if (selected)
      {
        label.setBackground(SystemColor.textHighlight);
        label.setForeground(SystemColor.textHighlightText);
      }
      else
      {
        label.setBackground(SystemColor.text);
        label.setForeground(SystemColor.textText);
      }

      // Make sure everything is painted correctly
      label.invalidate();
      checkbox.invalidate();
      invalidate();

      // Done
      return (this);

    } // getTreeCellRendererComponent

    private JCheckBox checkbox;
    private JLabel label;

  } // class MyCellRenderer

  private static DefaultMutableTreeNode getNodes()
  {
    // Create root
    DefaultMutableTreeNode root;
    root = new DefaultMutableTreeNode("root");

    // Create first level children
    DefaultMutableTreeNode first;
    DefaultMutableTreeNode second;
    DefaultMutableTreeNode third;
    NodeInfo               info;
    info = new NodeInfo();
    info.name = "This is the first node";
    info.checked = true;
    first = new DefaultMutableTreeNode(info);
    info = new NodeInfo();
    info.name = "And this is the second";
    info.checked = false;
    second = new DefaultMutableTreeNode(info);
    info = new NodeInfo();
    info.name = "Finally, the third";
    info.checked = false;
    third = new DefaultMutableTreeNode(info);
    root.add(first);
    root.add(second);
    root.add(third);

    // Add second level children
    info = new NodeInfo();
    info.name = "Second level node";
    info.checked = true;
    first.add(new DefaultMutableTreeNode(info));
    info = new NodeInfo();
    info.name = "This is another one";
    info.checked = false;
    first.add(new DefaultMutableTreeNode(info));
    info = new NodeInfo();
    info.name = "And this is the last one";
    info.checked = true;
    first.add(new DefaultMutableTreeNode(info));

    // Done
    return (root);

  } // getNodes

  private static class NodeInfo
  {
    public String name;
    public boolean checked;
  }

} // class TestTree

UPDATE
Within getTreeCellRendererComponent, I tried getting the preferred size.
They seem OK. When selecting the checkbox, the preferred size of both the label and the panel itself increase. When de-selecting the checkbox, they decrease.

Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
Robert Kock
  • 5,795
  • 1
  • 12
  • 20
  • Doesn't a JCheckBox already have a label as part of that component? You can set its text. – TT. Nov 19 '19 at 16:39
  • This is a simplied example. The real-life situation has an icon between checkbox and label. Furthermore, I don't want the checkbox to be triggered when the user presses the label (for reasons that go beyond this example). – Robert Kock Nov 19 '19 at 16:42
  • Anyway, I tried removing the label and setting the text and style of the checkbox. The results remains the same. – Robert Kock Nov 19 '19 at 16:59
  • 1
    Would be great to have the program that shows the problem in a [Short, Self Contained, Correct (Compilable), Example](http://sscce.org). – TT. Nov 19 '19 at 17:04
  • @TT. The code a posted should do the job. – Robert Kock Nov 19 '19 at 17:05

1 Answers1

0

Thanks to the answer to this question Change JTree row height resizing behavior when rendering, I managed to resolve the problem myself:

  private void toggleCheckBox(TreePath treePath)
  {
    // Determine node being toggled
    Object[]               path;
    DefaultMutableTreeNode node;
    NodeInfo               info;
    path = treePath.getPath();
    node = (DefaultMutableTreeNode)path[path.length - 1];
    info = (NodeInfo)node.getUserObject();

    // Toggle selection
    info.checked = !info.checked;

    // Make sure tree recalculates width of the nodes
    BasicTreeUI ui = (BasicTreeUI)getUI();
    try
    {
      Method method = BasicTreeUI.class.getDeclaredMethod("configureLayoutCache");
      method.setAccessible(true);
      method.invoke(ui);
    }
    catch (Exception e1)
    {
     e1.printStackTrace();
    }

  } // toggleCheckBox
Robert Kock
  • 5,795
  • 1
  • 12
  • 20