I want to use a custom TreeCellRenderer containing three text fields only when a node is SELECTED, while use the default renderer when the node is NOT SELECTED. The problem is that although I've set an appropriate preferred and minimum size for the panel, JTree does not update the edited row height. On the contrary when I use the same panel as an editor, it is rendered correctly.
Can someone explain why this is happening?
Is there a recommended way to achieve a rendering resizing behavior similar to that of editing?
Is there a method provided by JTree to set it directly or is it necessary to extend JTree or (worse) L&F?
NOTE:
After digging into the BasicTreeUI.startEditing(TreePath path, MouseEvent event)
method, I noticed the following lines of code.
They seem to be responsible for the edit resizing:
if(editorSize.width != nodeBounds.width ||
editorSize.height != nodeBounds.height) {
// Editor wants different width or height, invalidate
// treeState and relayout.
editorHasDifferentSize = true;
treeState.invalidatePathBounds(path);
updateSize();
// To make sure x/y are updated correctly, fetch
// the bounds again.
nodeBounds = getPathBounds(tree, path);
}
else
editorHasDifferentSize = false;
tree.add(editingComponent);
editingComponent.setBounds(nodeBounds.x, nodeBounds.y,
nodeBounds.width,
nodeBounds.height);
Here is a SSCCE showing the different editing and rendering behavior.
As you'll see, the panel is rendered correctly only during edit.
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.MouseEvent;
import java.util.EventObject;
import javax.swing.AbstractCellEditor;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
public class TestResizeTreeRowsFrame {
public static void main(String[] args){
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run() {
MyTreeNode root = createRoot();
TestFrame f = new TestFrame(root);
f.setVisible(true);
}
});
}
private static MyTreeNode createRoot(){
MyTreeNode root = new MyTreeNode("RootColumnName", "RootTableName", "Root");
MyTreeNode aNode = new MyTreeNode("AcolumnName", "AtableName", "A");
MyTreeNode bNode = new MyTreeNode("BcolumnName", "BtableName", "B");
MyTreeNode cNode = new MyTreeNode("CcolumnName", "CtableName", "C");
root.add(aNode);
root.add(bNode);
root.add(cNode);
return root;
}
public static class MyTreeNode extends DefaultMutableTreeNode{
/**
*
*/
private static final long serialVersionUID = 1L;
String columnName, tableName, value;
public MyTreeNode(String columnName, String tableName, String value){
this.columnName = columnName;
this.tableName = tableName;
this.value = value;
}
public String getColumnName() {
return columnName;
}
public String getTableName() {
return tableName;
}
public String getValue() {
return value;
}
public String toString(){
return value;
}
}
public static class TestFrame extends JFrame {
/**
*
*/
private static final long serialVersionUID = 1L;
private JPanel contentPane;
private JTree tree;
/**
* Create the frame.
*/
public TestFrame(MyTreeNode root) {
this.setTitle("RECORD Frame");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(100, 100, 450, 300);
contentPane = new JPanel();
contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
contentPane.setLayout(new BorderLayout(0, 0));
setContentPane(contentPane);
JScrollPane scrollPane = new JScrollPane();
contentPane.add(scrollPane, BorderLayout.CENTER);
tree = new JTree(root);
scrollPane.setViewportView(tree);
tree.setEditable(true);
tree.setInvokesStopCellEditing(true);
tree.setCellRenderer(new NodeRenderer());
tree.setCellEditor(new PanelRenderer());
}
private static class Renderer_Panel extends JPanel{
/**
*
*/
private static final long serialVersionUID = 1L;
private JTextField propertyTextField;
private JTextField prototypeTextField;
private JTextField valueTextField;
/**
* Create the panel.
*/
public Renderer_Panel() {
setPreferredSize(new Dimension(480, 97));
setMinimumSize(new Dimension(480, 97));
setLayout(new BorderLayout(0, 0));
JPanel panel = new JPanel();
panel.setMinimumSize(new Dimension(480, 97));
panel.setPreferredSize(new Dimension(480, 97));
add(panel, BorderLayout.CENTER);
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
Component verticalGlue_1 = Box.createVerticalGlue();
panel.add(verticalGlue_1);
JScrollPane scrollPane = new JScrollPane();
scrollPane.setBorder(null);
scrollPane.setPreferredSize(new Dimension(20, 60));
JPanel nodePropertiesPanel = new JPanel();
nodePropertiesPanel.setBorder(new EmptyBorder(0, 3, 0, 0));
nodePropertiesPanel.setPreferredSize(new Dimension(200, 30));
nodePropertiesPanel.setMinimumSize(new Dimension(0, 0));
scrollPane.setViewportView(nodePropertiesPanel);
GridBagLayout gbl_panel = new GridBagLayout();
gbl_panel.columnWidths = new int[]{0, 0, 0};
gbl_panel.rowHeights = new int[]{0, 0, 0, 0};
gbl_panel.columnWeights = new double[]{0.0, 1.0, Double.MIN_VALUE};
gbl_panel.rowWeights = new double[]{0.0, 0.0, 0.0, Double.MIN_VALUE};
nodePropertiesPanel.setLayout(gbl_panel);
JLabel lblProperty = new JLabel("Column:");
GridBagConstraints gbc_lblProperty = new GridBagConstraints();
gbc_lblProperty.insets = new Insets(0, 0, 5, 5);
gbc_lblProperty.anchor = GridBagConstraints.WEST;
gbc_lblProperty.gridx = 0;
gbc_lblProperty.gridy = 0;
nodePropertiesPanel.add(lblProperty, gbc_lblProperty);
propertyTextField = new JTextField();
GridBagConstraints gbc_propertyTextField = new GridBagConstraints();
gbc_propertyTextField.insets = new Insets(0, 0, 5, 0);
gbc_propertyTextField.fill = GridBagConstraints.HORIZONTAL;
gbc_propertyTextField.gridx = 1;
gbc_propertyTextField.gridy = 0;
nodePropertiesPanel.add(propertyTextField, gbc_propertyTextField);
propertyTextField.setColumns(10);
JLabel lblPrototype = new JLabel("Table:");
GridBagConstraints gbc_lblPrototype = new GridBagConstraints();
gbc_lblPrototype.anchor = GridBagConstraints.WEST;
gbc_lblPrototype.insets = new Insets(0, 0, 5, 5);
gbc_lblPrototype.gridx = 0;
gbc_lblPrototype.gridy = 1;
nodePropertiesPanel.add(lblPrototype, gbc_lblPrototype);
prototypeTextField = new JTextField();
GridBagConstraints gbc_prototypeTextField = new GridBagConstraints();
gbc_prototypeTextField.insets = new Insets(0, 0, 5, 0);
gbc_prototypeTextField.fill = GridBagConstraints.HORIZONTAL;
gbc_prototypeTextField.gridx = 1;
gbc_prototypeTextField.gridy = 1;
nodePropertiesPanel.add(prototypeTextField, gbc_prototypeTextField);
prototypeTextField.setColumns(10);
JLabel lblNewLabel = new JLabel("Value:");
GridBagConstraints gbc_lblNewLabel = new GridBagConstraints();
gbc_lblNewLabel.anchor = GridBagConstraints.WEST;
gbc_lblNewLabel.insets = new Insets(0, 0, 0, 5);
gbc_lblNewLabel.gridx = 0;
gbc_lblNewLabel.gridy = 2;
nodePropertiesPanel.add(lblNewLabel, gbc_lblNewLabel);
valueTextField = new JTextField();
GridBagConstraints gbc_valueTextField = new GridBagConstraints();
gbc_valueTextField.fill = GridBagConstraints.HORIZONTAL;
gbc_valueTextField.gridx = 1;
gbc_valueTextField.gridy = 2;
nodePropertiesPanel.add(valueTextField, gbc_valueTextField);
valueTextField.setColumns(10);
panel.add(scrollPane);
Component verticalGlue = Box.createVerticalGlue();
panel.add(verticalGlue);
}
public void setProperty(String property){
this.propertyTextField.setText(property);
}
public void setPrototype(String prototype){
this.prototypeTextField.setText(prototype);
}
public void setValue(String value){
this.valueTextField.setText(value);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(480, 97);
}
@Override
public Dimension getMinimumSize() {
return new Dimension(480, 97);
}
}
private class PanelRenderer extends AbstractCellEditor implements TreeCellEditor, TreeCellRenderer{
/**
*
*/
private static final long serialVersionUID = 1L;
Renderer_Panel component = new Renderer_Panel();
MyTreeNode value;
@Override
public Component getTreeCellEditorComponent(JTree tree,
Object value, boolean isSelected, boolean expanded,
boolean leaf, int row) {
MyTreeNode myNode = ((MyTreeNode)value);
String nodeValue = null;
String prototype = null;
String property = null;
nodeValue = myNode.getValue();
prototype = myNode.getTableName();
property = myNode.getColumnName();
component.setProperty(property);
component.setPrototype(prototype);
component.setValue(nodeValue);
this.value = myNode;
return component;
}
@Override
public Object getCellEditorValue() {
return this.value.getValue();
}
@Override
public boolean isCellEditable(EventObject anEvent) {
if(anEvent instanceof MouseEvent){
MouseEvent mouseEvent = (MouseEvent)anEvent;
if(mouseEvent.getClickCount() == 2){
return true;
}else{
return false;
}
}else{
return false;
}
}
@Override
public Component getTreeCellRendererComponent(JTree tree,
Object value, boolean selected, boolean expanded,
boolean leaf, int row, boolean hasFocus) {
return getTreeCellEditorComponent(tree, value, selected, expanded, leaf, row);
}
}
private class LabelNodeRenderer extends DefaultTreeCellRenderer {
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
MyTreeNode myNode = ((MyTreeNode)value);
this.setText(myNode.getValue());
return this;
}
}
private class NodeRenderer implements TreeCellRenderer{
/**
*
*/
private static final long serialVersionUID = 1L;
private LabelNodeRenderer labelRenderer = new LabelNodeRenderer();
private PanelRenderer panelRenderer = new PanelRenderer();
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
Component returnedComponent = null;
if(selected){
returnedComponent = panelRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
}else{
returnedComponent = labelRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
}
returnedComponent.setSize(returnedComponent.getPreferredSize());
return returnedComponent;
}
}
}
}
Also, forgive me if this is not the proper place, but I'm grabbing the chance to ask if there is a good book that recommends best practices for Swing?
Did the architects of Swing documented somewhere the recommended solutions upon which they based their design of Swing?(I know I'm asking too much)
Is there at least a cookbook that contains advice like Kleopatra's comment found in JTree TreeCellRenderer raising issue on showing the selection color:
a) extending a component is dirty design b) mixing calls to super and this is calling for pain (f.i. the infamous color memory in the default table cell renderer)
or explain design decisions such as making the CellEditorListener listen only for editingCanceled and editingStopped, but not for editingStarted (which would be useful in case I want to resize a JTable's cells without having to override the JTable.editCellAt).
Thanks in advance!
Some facts:
On the whole, there is no way to implement your requirement without going dirty. Basically, you have to listen to selection changes - because the renderer has different size requirement in selected vs. not selected - and then do your best to invalidate the ui's internal cache. Basically two options:
Below is a snippet for the first (couldn't make the second work on a quick test, but faintly remember that I did it ...)
protected TreeSelectionListener createReflectiveSelectionListener() {
TreeSelectionListener l = new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
invalidateLayoutCache();
}
protected void invalidateLayoutCache() {
BasicTreeUI ui = (BasicTreeUI) tree.getUI();
try {
Method method = BasicTreeUI.class.getDeclaredMethod("configureLayoutCache");
method.setAccessible(true);
method.invoke(ui);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e1) {
e1.printStackTrace();
}
}
};
return l;
}
Just found the second - similar dirtyness-level as the first - option:
protected TreeSelectionListener createFakeDataEventSelectionListener() {
TreeSelectionListener l = new TreeSelectionListener() {
@Override
public void valueChanged(final TreeSelectionEvent e) {
fireDataChanged(e.getOldLeadSelectionPath());
fireDataChanged(e.getNewLeadSelectionPath());
}
private void fireDataChanged(TreePath lead) {
if (lead == null) return;
DefaultTreeModel model = (DefaultTreeModel) tree.getModel();
TreeNode last = (TreeNode) lead.getLastPathComponent();
model.nodeChanged(last);
}
};
return l;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With