Change JTree row height resizing behavior when rendering
Asked Answered
M

1

7

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.

  • When a node is not selected, the default renderer is used.
  • By clicking once on a node, the node is selected and a panel renderer is used.
  • By clicking twice on a node, editing begins and a panel editor is used.

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!

Michalmichalak answered 11/3, 2014 at 16:15 Comment(2)
did you test if row height isn't 1 or negative number, reset this valueSpoliation
I tried to set the row height initially using JTree.setRowHeight(int), but then all the rows were updated, while I want only the selected ones. Then I tried to set the row height in the getTreeCellRendererComponent by distinguishing if(selected) but the result was unfortunately non-deterministic. Thanks, anyway for the quick reply.Michalmichalak
H
6

Some facts:

  • BasicTreeUI keeps a cache of node sizes
  • there's no public api to force it to revalidate that cache
  • node size requirements are assumed to depend exclusively on data, not on visual state, that is change of selection state will not trigger any internal update
  • an aside: setting size inside the renderer/editor has no effect: whatever you do, the ui will change it as it deems appropriate

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:

  • use reflection to access protected methods of the ui
  • fake model events that would lead to an internal re-calculation of the cache

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;
}
Henn answered 12/3, 2014 at 10:38 Comment(3)
Clear and precise as always. Thank you!...Second one is dirty, yet elegant... just the way I like it :) Keep sharing knowledge!Michalmichalak
@Michalmichalak both are dirty, if that's what you are after :-) The second's additional drawback is that it will work only for models of type DefaultTableModel. A clean solution would be to hook-in a custom ui-delegate (f.i. with SwingX's LAF hook mechanism) which exposes the relayout. Some work involved, though. We did something along those latter lines for JXList (needed a custom delegate anyway, because we support sorting of the list)Henn
I've been using this solution for years until I discovered that since Java 9 it's no longer possible so simply change the accessibility by means of reflection. But there's a nice work-around for this: Start the JVM with option "--add-opens java.desktop/javax.swing.plaf.basic=ALL-UNNAMED"Bandaid

© 2022 - 2024 — McMap. All rights reserved.