Search code examples
javaswingjtree

JTree entry rendering issue


In my project's GUI, where I use a JTree to display a file system, I encounter a render-specific problem: it seems that some entries in said JTree are able to render properly as well as render themselves over the previously selected entry. It's hard to explain, so I think including a screenshot would be easiest to understand.

JTree with multiple entries duplicated

To achieve this, I moved quickly from top to bottom with the arrow keys.

I don't quite know the source of this issue but I suspect it might be an issue with either my TreeModel or the CellRenderer, which are both included below.

Any help that can help me fix this issue is welcome!

import java.awt.Component;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Vector;

import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.WindowConstants;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

public class MCVE {
    public static void main(String[] args) throws Throwable {
        JFrame frame = new JFrame();
        frame.getContentPane().add(new FileSystemTree(new File(System.getProperty("user.home"))));
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}

class FileSystemTree extends JPanel {
    private File                root;
    private FileSystemTreeModel model;
    private final JTree         fileSystem;

    public FileSystemTree(File root) throws IOException {
        this.root = root;
        model = new FileSystemTreeModel(root);
        fileSystem = new JTree(model);
        fileSystem.setEditable(false);
        fileSystem.setCellRenderer(new FileSystemTreeCellRenderer());
        fileSystem.putClientProperty("JTree.lineStyle", "None");

        add(new JScrollPane(fileSystem));
    }
}

class FileSystemTreeCellRenderer extends DefaultTreeCellRenderer {
    private final Icon  folderSpecial;
    private final Icon  folderOther;
    private final Icon  fileSpecial;
    private final Icon  fileOther;

    public FileSystemTreeCellRenderer() throws IOException {
        InputStream stream;
        BufferedImage image;

        stream = new FileInputStream("folder-special.png");
        image = ImageIO.read(stream);
        folderSpecial = new ImageIcon(image);
        stream.close();

        stream = new FileInputStream("folder-other.png");
        image = ImageIO.read(stream);
        folderOther = new ImageIcon(image);
        stream.close();

        stream = new FileInputStream("file-special.png");
        image = ImageIO.read(stream);
        fileSpecial = new ImageIcon(image);
        stream.close();

        stream = new FileInputStream("file-other.png");
        image = ImageIO.read(stream);
        fileOther = new ImageIcon(image);
        stream.close();
    }

    @Override
    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
        super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);

        if (value instanceof File) {
            Icon icon = null;
            File file = (File) value;

            if (file.isDirectory()) {
                if (file.getName().endsWith("-special")) {
                    icon = folderSpecial;
                } else {
                    icon = folderOther;
                }
            } else if (file.isFile()) {
                if (file.getName().endsWith(".special")) {
                    icon = fileSpecial;
                } else {
                    icon = fileOther;
                }
            }

            setIcon(icon);
        }

        return this;
    }
}

class FileSystemTreeModel implements TreeModel, Comparator<File> {
    private final File                      root;
    private final Vector<TreeModelListener> listeners   = new Vector<>();

    public FileSystemTreeModel(File root) {
        if (!root.isDirectory()) throw new IllegalArgumentException();
        this.root = root;
    }

    @Override
    public int compare(File a, File b) {
        if (a.isDirectory() && !b.isDirectory()) return -1;
        if (!a.isDirectory() && b.isDirectory()) return 1;
        return a.getName().compareToIgnoreCase(b.getName());
    }

    @Override
    public File getRoot() {
        return root;
    }

    @Override
    public File getChild(Object parent, int index) {
        File dir = (File) parent;
        File[] files = dir.listFiles();
        String[] children = new String[files.length];
        Arrays.sort(files, this::compare);

        for (int i = 0; i < files.length; i++) {
            children[i] = files[i].getName();
        }

        return new TreeFile(dir, children[index]);
    }

    @Override
    public int getChildCount(Object parent) {
        File file = (File) parent;
        if (file.isDirectory()) {
            String[] fileList = file.list();
            if (fileList != null) return fileList.length;
        }
        return 0;
    }

    @Override
    public boolean isLeaf(Object node) {
        File file = (File) node;
        return file.isFile();
    }

    @Override
    public int getIndexOfChild(Object parent, Object child) {
        File dir = (File) parent;
        File file = (File) child;
        File[] children = dir.listFiles();
        Arrays.sort(children, this::compare);

        for (int i = 0; i < children.length; i++) {
            if (file.equals(children[i])) return i;
        }

        return -1;
    }

    @Override
    public void valueForPathChanged(TreePath path, Object value) {}

    @Override
    public void addTreeModelListener(TreeModelListener listener) {
        listeners.add(listener);
    }

    @Override
    public void removeTreeModelListener(TreeModelListener listener) {
        listeners.remove(listener);
    }

    private static class TreeFile extends File {
        public TreeFile(File parent, String path) {
            super(parent, path);
        }

        @Override
        public String toString() {
            return getName();
        }
    }
}

Solution

  • I was able to fix this rendering issue by making the custom TreeCellRenderer implement TreeCellRenderer instead of extending DefaultTreeCellRenderer. It seems like the issue is connected to DefaultTreeCellRenderer extending JLabel and editing its own values in order to display entries. By using a HashMap which maps values to JLabels, memory consumption goes up by quite a bit but fixes the issue.

    public class FileSystemTreeCellRenderer implements TreeCellRenderer {
        protected Color foregroundColor = null;
        protected Color backgroundColor = null;
        protected Color selectionForegroundColor = null;
        protected Color selectionBackgroundColor = null;
        protected Map<Object, JLabel> labels = new HashMap<>();
        protected final Icon  folderSpecial;
        protected final Icon  folderOther;
        protected final Icon  fileSpecial;
        protected final Icon  fileOther;
    
        public FileSystemTreeCellRenderer() throws IOException {
            InputStream stream;
            BufferedImage image;
    
            stream = new FileInputStream("folder-special.png");
            image = ImageIO.read(stream);
            folderSpecial = new ImageIcon(image);
            stream.close();
    
            stream = new FileInputStream("folder-other.png");
            image = ImageIO.read(stream);
            folderOther = new ImageIcon(image);
            stream.close();
    
            stream = new FileInputStream("file-special.png");
            image = ImageIO.read(stream);
            fileSpecial = new ImageIcon(image);
            stream.close();
    
            stream = new FileInputStream("file-other.png");
            image = ImageIO.read(stream);
            fileOther = new ImageIcon(image);
            stream.close();
        }
    
        protected JLabel getLabelFor(Object object) {
            JLabel label = labels.get(object);
            if(label == null) {
                label = new JLabel();
                labels.put(object, label);
            }
            return label;
        }
    
        public Color getForegroundColor() {
            if (foregroundColor == null) return UIManager.getColor("Tree.textForeground");
            return foregroundColor;
        }
    
        public Color getBackgroundColor() {
            if (backgroundColor == null) return UIManager.getColor("Tree.textBackground");
            return backgroundColor;
        }
    
        public Color getSelectionForegroundColor() {
            if (selectionForegroundColor == null) return UIManager.getColor("Tree.selectionForeground");
            return selectionForegroundColor;
        }
    
        public Color getSelectionBackgroundColor() {
            if (selectionBackgroundColor == null) return UIManager.getColor("Tree.selectionBackground");
            return selectionBackgroundColor;
        }
    
        @Override
        public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
            JLabel label = getLabelFor(value);
            label.setText(Objects.toString(value, ""));
            label.setOpaque(true);
            label.setBackground(selected ? getSelectionBackgroundColor() : getBackgroundColor());
            label.setForeground(selected ? getSelectionForegroundColor() : getForegroundColor());
            label.setEnabled(tree.isEnabled());
            label.setComponentOrientation(tree.getComponentOrientation());
    
            if (value instanceof File) {
                Icon icon = null;
                File file = (File) value;
    
                if (file.isDirectory()) {
                    if (file.getName().endsWith("-special")) {
                        icon = folderSpecial;
                    } else {
                        icon = folderOther;
                    }
                } else if (file.isFile()) {
                    if (file.getName().endsWith(".special")) {
                        icon = fileSpecial;
                    } else {
                        icon = fileOther;
                    }
                }
    
                label.setIcon(icon);
            }
    
            return label;
        }
    }
    

    If anyone has a better, less memory consumptive way to do this, feel free to post another answer!