Trying to use standard JTree implementation, but the output is really messed up

74 views Asked by At

I'm trying to use JTree. Here is my example test class:

package inspector;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;


/**
 *
 * @author Alex
 */
public class TreeTest {

    private static ValueMap prepareTree() {
        ValueMap obj = new ValueMap();
        obj.set("x", new Value(1));
        obj.set("y", new Value(1));

        ValueArray users = new ValueArray();
        obj.set("users", users);

        ValueMap user = new ValueMap();
        user.set("login", new Value("Alex654"));
        user.set("password", new Value("123456"));

        ValueMap info = new ValueMap();
        info.set("city", new Value("Moscow"));
        info.set("job", new Value("Developer"));
        info.set("firstName", new Value("Alex"));
        info.set("lastName", new Value("Popov"));
        user.set("bio", info);

        users.push(user);

        ValueMap user2 = new ValueMap();
        user2.set("login", new Value("admin"));
        user2.set("password", new Value("test"));
        user2.set("bio", new Value(null));

        users.push(user2);

        return obj;
    }

    static class Value {

        Value() {
            value = null;
        }

        Value(Object val) {
            this.value = val;
        }

        @Override
        public String toString() {
            if (value instanceof Integer || value instanceof Double) {
                return value + "";
            } else {
                return "\"" + value.toString() + "\"";
            }
        }

        Object value;
    }

    static class ValueArray extends Value {

        ValueArray() {
            items = new ArrayList<Value>();
        }

        ValueArray(Value val) {
            items = new ArrayList<Value>();
            items.add(val);
        }

        void push(Value val) {
            items.add(val);
        }

        ArrayList<Value> items;
    }

    static class ValueMap extends Value {

        ValueMap() {
            items = new HashMap<String, Value>();
        }

        Value get(String key) {
            return items.get(key);
        }

        void put(String key, Value val) {
            items.put(key, val);
        }

        void set(String key, Value val) {
            items.put(key, val);
        }

        HashMap<String, Value> items;
    }

    public static class ValueWrapper {

        ValueWrapper(String label, Value val) {
            this.label = label;
            this.val = val;
        }

        String getLabel() {
            return label;
        }

        Value getValue() {
            return val;
        }

        @Override
        public String toString() {
            String result = label;
            if (val instanceof ValueArray) {
                result += ": Array";
            }
            else if (val instanceof ValueMap) {
                result += ": { Object }";
            }
            else result += ": " + val.toString();

            return result;
        }

        private String label;
        private Value val;
    }

    public static void processChildren(DefaultMutableTreeNode node) {
        Value val = ((ValueWrapper)node.getUserObject()).getValue();
        if (val instanceof ValueArray) {
            ArrayList<Value> items = ((ValueArray)val).items;
            for (int i = 0; i < items.size(); i++) {
                DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(new ValueWrapper(i + "", items.get(i)));
                node.add(newNode);
                processChildren(newNode);
            }
        } else if (val instanceof ValueMap) {
            HashMap<String, Value> props = ((ValueMap)val).items;
            Set<String> keys = props.keySet();
            for (String key: keys) {
                DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(new ValueWrapper(key, props.get(key)));
                node.add(newNode);
                if (!key.equals("__proto__")) {
                    processChildren(newNode);
                }
            }
        }
    }


    public static void main(String[] args) {

        try {
            UIManager.setLookAndFeel(
                UIManager.getSystemLookAndFeelClassName());
        } catch (Exception e) {}

        final ValueMap root = prepareTree();
        if (root == null) return;

        final JFrame frame = new JFrame("Object Inspector");
        JPanel cp = new JPanel();
        cp.setBorder(BorderFactory.createEmptyBorder(9, 10, 9, 10));
        frame.setContentPane(cp);
        cp.setLayout(new BoxLayout(cp, BoxLayout.PAGE_AXIS));

        DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(new ValueWrapper("obj", root));
        processChildren(rootNode);

        DefaultTreeModel treeModel = new DefaultTreeModel(rootNode, true);
        final JTree tree = new JTree(treeModel);

        final JPanel contentpane = new JPanel();
        contentpane.setBackground(Color.WHITE);
        contentpane.setOpaque(true);
        contentpane.setLayout(new BorderLayout());
        contentpane.add(tree);

        //contentpane.setBounds(0, 0, 490, 380);
        final int width = 490, height = 418;
        cp.setPreferredSize(new Dimension(width, height));

        final JScrollPane scrollpane = new JScrollPane(contentpane);
        scrollpane.setOpaque(false);
        scrollpane.getInsets();

        cp.add(scrollpane);
        scrollpane.setBackground(Color.WHITE);
        scrollpane.setOpaque(true);
        //setSize(518, 420);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

I expect to get a nice looking tree, like in NetBeans IDE that I use everyday.

But however I get this:

tree display

What the heck is wrong with it? Also should note that when my labels were longer (without the "wrapper" inner class) the output labels in leaves was 2-3 times wider, but the labels were also extremely cut down in width.

UPDATE:

Unfortunalely, the result after suggested solution is not ideal. See the screenshot below:

fixed display

  1. The symbols are cut by 1-2 pixels from the bottom (look at the square brackets). Maybe it is a windows bug (I use Windows 7 with 150% zoom), but is there a way to fix that? To increase the font size of the labels or the vertical gap between them (so called "line height")?
  2. I don't like that leaves have "+" on them until I click them. I solved it adding else { node.setAllowsChildren(false); } to the end of my processChildren() method. But now I would like to have a single custom icon for all of my nodes, whether they have children or not.
1

There are 1 answers

1
Alex654 On

Seems that all the methods I needed were available either in JTree itself or in DefaultTreeCellRenderer class. Here is the final working code:

    private static JPanel createObjectTree(JSObject obj) {
        DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(new JSValueWrapper("obj", obj));
        loadNodeDirectChildren(rootNode, false);

        DefaultTreeModel treeModel = new DefaultTreeModel(rootNode, true);
        final JTree tree = new JTree(treeModel);
        TreeExpandListener expansionListener = new TreeExpandListener();
        tree.addTreeWillExpandListener(expansionListener);

        DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
        renderer.setFont(new Font("Consolas", Font.PLAIN, 16));
        renderer.setLeafIcon(null);
        renderer.setIcon(null);
        renderer.setClosedIcon(null);
        renderer.setOpenIcon(null);
        renderer.setMinimumSize(new Dimension(0, 20));
        tree.setCellRenderer(renderer);
        tree.setRowHeight(23);

        final JPanel contentpane = new JPanel();
        contentpane.setBackground(Color.WHITE);
        contentpane.setOpaque(true);
        contentpane.setLayout(new BorderLayout());
        contentpane.add(tree);

        return contentpane;
    }

    private static void processNodeChildren(DefaultMutableTreeNode node, boolean show_prototypes) {
        JSValue val = ((JSValueWrapper)node.getUserObject()).getValue();
        if (val instanceof JSArray) {
            Vector<JSValue> items = ((JSArray)val).getItems();
            for (int i = 0; i < items.size(); i++) {
                DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(new JSValueWrapper("[" + i + "]", items.get(i)));
                node.add(newNode);
                processNodeChildren(newNode, show_prototypes);
            }
        } else if (!(val instanceof Function) && !val.getType().matches("Boolean|Integer|Float|Number|String|null|undefined") && val instanceof JSObject) {
            HashMap<String, JSValue> props = ((JSObject)val).getProperties();
            Set<String> keys = props.keySet();
            for (String key: keys) {
                if (!show_prototypes && key.equals("__proto__")) continue;
                DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(new JSValueWrapper(key, props.get(key)));
                node.add(newNode);
                if (!key.equals("__proto__")) {
                    processNodeChildren(newNode, show_prototypes);
                } else {
                    newNode.setAllowsChildren(false);
                }
            }
        } else {
            node.setAllowsChildren(false);
        }
    }

    private static void loadNodeDirectChildren(DefaultMutableTreeNode node, boolean show_prototypes) {
        JSValue val = ((JSValueWrapper)node.getUserObject()).getValue();
        if (val instanceof JSArray) {
            Vector<JSValue> items = ((JSArray)val).getItems();
            for (int i = 0; i < items.size(); i++) {
                DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(new JSValueWrapper("[" + i + "]", items.get(i)));
                node.add(newNode);
            }
        } else if (!(val instanceof Function) && !val.getType().matches("Boolean|Integer|Float|Number|String|null|undefined") && val instanceof JSObject) {
            HashMap<String, JSValue> props = ((JSObject)val).getProperties();
            Set<String> keys = props.keySet();
            for (String key: keys) {
                if (!show_prototypes && key.equals("__proto__")) continue;
                DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(new JSValueWrapper(key, props.get(key)));
                node.add(newNode);
                if (props.get(key) instanceof Function || props.get(key).getType().matches("Boolean|Integer|Float|Number|String|null|undefined")) {
                    newNode.setAllowsChildren(false);
                }
                if (key.equals("__proto__")) {
                    newNode.setAllowsChildren(false);
                }
            }
        } else {
            node.setAllowsChildren(false);
        }
    }

    static class TreeExpandListener implements TreeWillExpandListener {

        @Override
        public void treeWillExpand(final TreeExpansionEvent e) throws ExpandVetoException {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.getPath().getLastPathComponent();
            if (node.getChildCount() == 0) {
                loadNodeDirectChildren(node, false);
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        ((JTree)e.getSource()).invalidate();
                        ((JTree)e.getSource()).getParent().validate();
                        recalculateContentHeight();
                    }
                });
            }
        }

        @Override
        public void treeWillCollapse(TreeExpansionEvent e) {}

        public void treeExpanded(TreeExpansionEvent e) {}

        public void treeCollapsed(TreeExpansionEvent e) {}
    }

Note that if the tree contains "loops", e.g. in HTML document each child has a link to its parent, and every two adjacent siblings link to each other, then using simple approach of initializing all the node's children will lead to stackoverflow error and the program will crash.

The solution is to use lazy loading of children, and load only one level at a time, until the user clicks one of the existing "non-leaf" nodes.