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:
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.
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