The purpose of this short program is to:
- Detect all accessible local drives and USB drives
- List all the accessible directories for each drive which do not require administrator privileges or are hidden by the system
- Display all the data in a friendly UI that is simple to use and nice to look at
I have noticed once the list starts to become longer the performance of the list scrolling and animations decreases. Therefore I would particularly be interested in your thoughts on the efficiency of the program and if any improvements can be made to it. I would also like to know if you think the program does what it is meant to do properly.
Dropbox Folder with source code and icons
Edit
I know there is a lot of code for what seems like a simple component so I will add the performance issues that I hinted at earlier on in my question and what I think might be causing them.
As the list grows:
- The scrolling becomes jumpy and sometimes lags. This is while there are only a few rows on the screen is ~250 on my system.
- The expand control fade animation becomes slow with ~30 rows on my system. I believe this is being caused by the way the timers work in
TreeHandleUI.fadeHandles(boolean show, int msDur)
- The "rollover" tracking when the mouse one of the rows or expand controls for a row can have a significant delay of almost half a second if the number of rows is ~200 on my system.
Another performance issue that I noted is when a directory has a large amount of subdirectories in it and files it can take a while to open. An example in Windows would be C:\Program Files
. This is because the method ComputerDirectoryTreeModel.listDirectories(File path)
has to check every file to see if it is a directory. Then for paint controls and to determine what icons should be used there is also ComputerDirectoryTreeModel.hasDirectory(File path)
and FileNameRenderer.hasDirectory(File path)
. This is a method with improved efficiency over listDirectories
as it stops as soon as it finds one directory in the directory it is searching. The problem is if the directory is large and does not actually contain a directory or the directory is a far way down the list then this can still take a while to run.
JFileTreeIE.java - Implementation of program
package tree;
import java.awt.EventQueue;
import java.awt.GridLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import tree.design.DesignTree;
import tree.model.ComputerDirectoryTreeModel;
/*
* Short implementation class to show the program
*/
@SuppressWarnings("serial")
public class JFileTreeIE extends JFrame {
private JFileTreeIE() {
super("Directory Explorer");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new GridLayout());
createPanel();
setSize(500, 800);
setVisible(true);
}
private void createPanel() {
DesignTree tree = new DesignTree(new ComputerDirectoryTreeModel());
JScrollPane scroll = new JScrollPane(tree);
JPanel panel = new JPanel(new GridLayout(1, 1));
panel.add(scroll);
getContentPane().add(panel);
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> new JFileTreeIE());
}
}
DesignTree.java - The component class
package tree.design;
import javax.swing.JTree;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeSelectionModel;
import tree.listeners.MouseHandler;
import tree.render.FileNameRenderer;
import tree.ui.TreeHandleUI;
/*
* Class to set a range of custom UIs, renderers, and handlers
*/
@SuppressWarnings("serial")
public class DesignTree extends JTree {
public DesignTree(TreeModel model) {
super(model);
setUI(new TreeHandleUI(this));
setCellRenderer(new FileNameRenderer());
setRootVisible(false); //Allows for each drive to act as a root
setShowsRootHandles(true); //Shows handles for each individual drive
getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
addMouseListener(new MouseHandler(this));
}
}
AlphaImageIcon.java - Slightly edited version of code from this post - Used for icon animations
package tree.icon;
import java.awt.AlphaComposite;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.ImageObserver;
import javax.swing.Icon;
import javax.swing.ImageIcon;
/**
* An Icon wrapper that paints the contained icon with a specified transparency.
* <P>
* This class is suitable for wrapping an <CODE>ImageIcon</CODE>
* that holds an animated image. To show a non-animated Icon with transparency,
* the companion class {@link AlphaIcon} is a lighter alternative.
*
* @version 1.0 08/16/10
* @author Darryl
*
* https://tips4java.wordpress.com/2010/08/22/alpha-icons/
*/
@SuppressWarnings("serial")
public class AlphaImageIcon extends ImageIcon {
private Icon icon;
private Image image;
private float alpha;
public AlphaImageIcon(Icon icon, float alpha) {
this.icon = icon;
this.alpha = alpha;
}
@Override
public Image getImage() {
return image;
}
@Override
public void setImage(Image image) {
if (icon instanceof ImageIcon)
((ImageIcon) icon).setImage(image);
}
@Override
public int getImageLoadStatus() {
if (icon instanceof ImageIcon)
return ((ImageIcon) icon).getImageLoadStatus();
return 0;
}
@Override
public ImageObserver getImageObserver() {
if (icon instanceof ImageIcon)
return ((ImageIcon) icon).getImageObserver();
return null;
}
@Override
public void setImageObserver(ImageObserver observer) {
if (icon instanceof ImageIcon)
((ImageIcon) icon).setImageObserver(observer);
}
public float getAlpha() {
return alpha;
}
public Icon getIcon() {
return icon;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
if (icon instanceof ImageIcon)
image = ((ImageIcon) icon).getImage();
else
image = null;
Graphics2D g2 = (Graphics2D) g.create();
g2.setComposite(AlphaComposite.SrcAtop.derive(alpha));
icon.paintIcon(c, g2, x, y);
g2.dispose();
}
@Override
public int getIconWidth() {
return icon.getIconWidth();
}
@Override
public int getIconHeight() {
return icon.getIconHeight();
}
}
MouseHandler.java - Main MouseListener
for the tree
package tree.listeners;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import tree.ui.TreeHandleUI;
/*
* Class which controls expand / collapse handle animations
* and other mouse related functions
*/
public class MouseHandler extends MouseAdapter {
JTree t = null;
TreeHandleUI tUI = null;
public MouseHandler(JTree tree) {
t = tree;
tUI = (TreeHandleUI) tree.getUI();
}
@Override
public void mouseEntered(MouseEvent e) {
tUI.fadeHandles(true, 300); //Fades handles in
}
@Override
public void mouseExited(MouseEvent e) {
tUI.fadeHandles(false, 300); //Fades handles out
}
@Override
public void mousePressed(MouseEvent e) {
//Clears selection when mouse is pressed outside of on of the nodes
if((SwingUtilities.isLeftMouseButton(e) || SwingUtilities.isRightMouseButton(e))
&& t.getRowForLocation(e.getX(), e.getY()) == -1)
t.clearSelection();
}
}
ComputerDirectoryTreeModel.java - The TreeModel
that detects the drives and automatically generates the list of directories for each drive
package tree.model;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import javax.swing.filechooser.FileSystemView;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
/*
* Class that automatically generates the tree
* so that it contains all directories accessible drives.
*
* Does not access hidden directories or directories
* that require administrator privileges.
*/
public class ComputerDirectoryTreeModel implements TreeModel {
private DefaultMutableTreeNode root = new DefaultMutableTreeNode();
public ComputerDirectoryTreeModel() {
createRootLayout();
}
private void createRootLayout() {
for(File path : File.listRoots()) { //Loops through the "root" directories on a computer - Determined by system
if(path.exists())
if(addable(path))
if(listDirectories(path).length != 0)
root.add(new DefaultMutableTreeNode(path));
}
}
//Checks whether the drive can be added
//Only adds local drives or USBs
private boolean addable(File path) {
FileSystemView fsv = FileSystemView.getFileSystemView();
String type = fsv.getSystemTypeDescription(path);
//System.out.println(type); //For Debugging Purposes
if(type != null) {
if(type.equalsIgnoreCase("Local Disk"))
return true;
if(type.equalsIgnoreCase("USB Drive"))
return true;
return true; //Temporary fix for users who don't use windows
}
return false;
}
//Not used
@Override
public void addTreeModelListener(javax.swing.event.TreeModelListener l) {}
//Overrides the original getChild so that the method returns the correct directory
@Override
public Object getChild(Object parent, int index) {
if(parent != root && parent instanceof DefaultMutableTreeNode) {
File f = (File) (((DefaultMutableTreeNode) parent).getUserObject());
return listDirectories(f)[index];
} else if(parent != root) {
File f = (File) parent;
return listDirectories(f)[index];
}
return root.getChildAt(index);
}
//Overrides the original getChildCount so that
//the method returns the correct value
@Override
public int getChildCount(Object parent) {
if(parent != root && parent instanceof DefaultMutableTreeNode) {
File f = (File) (((DefaultMutableTreeNode) parent).getUserObject());
if (!f.isDirectory())
return 0;
else
return listDirectories(f).length;
} else if(parent != root) {
File f = (File) parent;
if (!f.isDirectory())
return 0;
else
return listDirectories(f).length;
}
return root.getChildCount();
}
//Overrides the original hasChildren so that
//the method returns the correct value
public boolean hasChildren(Object parent) {
if(parent != root && parent instanceof DefaultMutableTreeNode) {
File f = (File) (((DefaultMutableTreeNode) parent).getUserObject());
if (!f.isDirectory())
return false;
else
return hasDirectory(f);
} else if(parent != root) {
File f = (File) parent;
if (!f.isDirectory())
return false;
else
return hasDirectory(f);
}
return root.getChildCount() != 0 ? true : false;
}
//Overrides the original getIndexOfChild so that
//the method returns the correct value
@Override
public int getIndexOfChild(Object parent, Object child) {
if(parent != root && parent instanceof DefaultMutableTreeNode) {
File par = (File) (((DefaultMutableTreeNode) parent).getUserObject());
File ch = (File) child;
return Arrays.asList(listDirectories(par)).indexOf(ch);
} else if(parent != root) {
File par = (File) parent;
File ch = (File) child;
return Arrays.asList(listDirectories(par)).indexOf(ch);
}
return root.getIndex((TreeNode) child);
}
@Override
public Object getRoot() {
return root;
}
//There should technically be no leaves as every directory
//could potentially have a sub directory in it.
@Override
public boolean isLeaf(Object node) {
return false;
}
//Not used
@Override
public void removeTreeModelListener(javax.swing.event.TreeModelListener l) {}
//Not used
@Override
public void valueForPathChanged(javax.swing.tree.TreePath path, Object newValue) {}
//Lists all the sub directories of the given directory
//if it is accessible. This will not work for folders
//that require administrator privilages to view.
private File[] listDirectories(File path) {
ArrayList<File> arrayList = new ArrayList<File>();
for(File temp : path.listFiles()) {
if(temp.isDirectory() && !temp.isHidden())
if(temp.listFiles() != null)
arrayList.add(temp);
}
return arrayList.toArray(new File[0]);
}
//Written to improve performance of listDirectories(path) != 0
private boolean hasDirectory(File path) {
for(File temp : path.listFiles()) {
if(temp.isDirectory() && !temp.isHidden())
if(temp.listFiles() != null)
return true;
}
return false;
}
}
FileNameRenderer.java - Makes sure that the rows are rendered correctly
package tree.render;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.io.File;
import java.util.Arrays;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
/*
* This class handles the rendering of the tree nodes.
* This includes handling rollover functions, and naming conventions.
*/
@SuppressWarnings("serial")
public class FileNameRenderer extends DefaultTreeCellRenderer {
public FileNameRenderer() {
setBackgroundSelectionColor(new Color(205, 232, 255));
setHoverSelectionColor(new Color(229, 243, 255));
}
private boolean mOver = false;
private boolean selected = false;
private Color rolloverColor = null;
public void setHoverSelectionColor(Color rollover) {
rolloverColor = rollover;
}
public Color getHoverSelectionColor() {
return rolloverColor;
}
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
return getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus, false);
}
//Replaces the node text from the path of the directory to just the directory name
//Also update the what node is selected and whether the mouse is over it for other
//functions in this class
//Sets some of the icons of the nodes on the tree. Ie if the directory is expanded
//the icon will change
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus, boolean mouseOver) {
File holder = null;
if(value instanceof File) {
holder = (File) value;
if(!Arrays.asList(File.listRoots()).contains((File) value)) {
value = ((File) value).getName();
}
}
JComponent c = (JComponent) super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
selected = sel;
mOver = mouseOver;
if(c instanceof JLabel)
((JLabel) c).setText("<html><p style=\"padding: 3px 0;\">" + value + "<p/>");
if(value instanceof DefaultMutableTreeNode)
setIcon(new ImageIcon("resources/root.png"));
else if(expanded && holder != null && hasDirectory(holder))
setIcon(new ImageIcon("resources/open-folder.png"));
else
setIcon(new ImageIcon("resources/folder.png"));
return c;
}
//Checks whether the directory has any sub directories.
//This determines if a handle will be painted next to
//the node
private boolean hasDirectory(File path) {
for(File temp : path.listFiles()) {
if(temp.isDirectory() && !temp.isHidden())
if(temp.listFiles() != null)
return true;
}
return false;
}
//Paints the node
//Changes background colour of the node if the mouse is over it
//or if it is selected
//Offsets paint by so much so that the node works with the icons
@Override
public void paintComponent(Graphics g) {
Color bColor;
if (selected)
bColor = getBackgroundSelectionColor();
else if(mOver)
bColor = getHoverSelectionColor();
else {
bColor = getBackgroundNonSelectionColor();
if (bColor == null)
bColor = getBackground();
}
int imageOffset = -1;
if (bColor != null) {
imageOffset = getLabelStart();
g.setColor(bColor);
if(getComponentOrientation().isLeftToRight())
g.fillRect(imageOffset, 0, getWidth() - imageOffset, getHeight());
else
g.fillRect(0, 0, getWidth() - imageOffset, getHeight());
}
super.paintComponent(g);
}
//Used for getting the image offset for the paintComponent function
private int getLabelStart() {
Icon currentI = getIcon();
if(currentI != null && getText() != null)
return currentI.getIconWidth() + Math.max(0, getIconTextGap() - 1);
return 0;
}
}
TreeHandleUI.java - Look and feel of the tree
package tree.ui;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JTree;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.TreePath;
import tree.icon.AlphaImageIcon;
import tree.model.ComputerDirectoryTreeModel;
import tree.render.FileNameRenderer;
/*
* This mainly handles the look and feel of the JTree
* It effects areas such as the expand controls of nodes,
* the way the nodes look, and how lines are drawn on the
* tree if they are drawn at all
*/
public class TreeHandleUI extends BasicTreeUI {
///Variables
private JTree t = null;
private boolean lines = false;
private boolean lineTypeDashed = true;
private Icon rolloverIcon = null;
private boolean iconRolloverEnabled = false;
private ArrayList<Timer> timers = new ArrayList<Timer>(); //For preventing any nasty glitching if fade method is still running
private UpdateHandler uH = null;
private ComputerDirectoryTreeModel tM; //Used so that we can quickly check if the file contains any children
private boolean isLeftToRight(Component c) {
return c.getComponentOrientation().isLeftToRight();
}
private int count = 0;
private float val = 0F;
///Constructors
//Sets most of the icons for the JTree and changes the default mouse handler
public TreeHandleUI(JTree tree) {
t = tree;
uH = new UpdateHandler(t);
t.addMouseMotionListener(uH);
EventQueue.invokeLater(() -> tM = (ComputerDirectoryTreeModel) treeModel); //Is like this so that tM does not return an NPE
EventQueue.invokeLater(() -> setCollapsedIcon(new AlphaImageIcon(new ImageIcon("resources/closed.png"), 0F)));
EventQueue.invokeLater(() -> setExpandedIcon(new AlphaImageIcon(new ImageIcon("resources/open.png"), 0F)));
setRolloverIcon(new AlphaImageIcon(new ImageIcon("resources/rollover.png") , 1F));
removeLines(true); //Removes all the visible lines on the JTree
}
///Methods
//Methods used for rollover icon
public void setRolloverIcon(Icon rolloverG) {
Icon oldValue = rolloverIcon;
rolloverIcon = rolloverG;
setIconRolloverEnabled(true);
if (rolloverG != oldValue) {
t.repaint();
}
}
private void setIconRolloverEnabled(boolean handleRolloverEnabled) {
boolean oldValue = iconRolloverEnabled;
iconRolloverEnabled = handleRolloverEnabled;
if (handleRolloverEnabled != oldValue) {
t.repaint();
}
}
//Paints the correct icon for the expand control
//Ie when the mouse is over the collapsed icon it
//changes to the rollover icon
@Override
protected void paintExpandControl(Graphics g,
Rectangle clipBounds, Insets insets,
Rectangle bounds, TreePath path,
int row, boolean isExpanded,
boolean hasBeenExpanded,
boolean isLeaf) {
Object value = path.getLastPathComponent();
if(tM.hasChildren(value)) {
int middleXOfKnob;
if(isLeftToRight(t))
middleXOfKnob = bounds.x - getRightChildIndent() + 1;
else
middleXOfKnob = bounds.x + bounds.width + getRightChildIndent() - 1;
int middleYOfKnob = bounds.y + (bounds.height / 2);
if(isExpanded) {
Icon expandedIcon = getExpandedIcon();
if(expandedIcon != null)
drawCentered(tree, g, expandedIcon, middleXOfKnob, middleYOfKnob );
} else if(isLocationInExpandControl(path, uH.getXPos(), uH.getYPos())
& !isExpanded && iconRolloverEnabled) {
if(row == uH.getRowHandle()) {
if(rolloverIcon != null)
drawCentered(tree, g, rolloverIcon, middleXOfKnob, middleYOfKnob);
} else {
Icon collapsedIcon = getCollapsedIcon();
if(collapsedIcon != null)
drawCentered(tree, g, collapsedIcon, middleXOfKnob, middleYOfKnob);
}
} else {
Icon collapsedIcon = getCollapsedIcon();
if(collapsedIcon != null)
drawCentered(tree, g, collapsedIcon, middleXOfKnob, middleYOfKnob);
}
}
}
//Paints the row, updates background if mouse over row
@Override
protected void paintRow(Graphics g, Rectangle clipBounds,
Insets insets, Rectangle bounds, TreePath path,
int row, boolean isExpanded,
boolean hasBeenExpanded, boolean isLeaf) {
// Don't paint the renderer if editing this row.
if(editingComponent != null && editingRow == row)
return;
int leadIndex;
if(tree.hasFocus())
leadIndex = getLeadSelectionRow();
else
leadIndex = -1;
Component component = ((FileNameRenderer) t.getCellRenderer()).getTreeCellRendererComponent
(tree, path.getLastPathComponent(),
tree.isRowSelected(row), isExpanded, isLeaf, row,
(leadIndex == row), (row == uH.getRow()));
rendererPane.paintComponent(g, component, tree, bounds.x, bounds.y,
bounds.width, bounds.height, true);
}
//Small utility class used for retrieving information so that
//tasks such as adding a rollover icon or rollover background
//can be completed
private class UpdateHandler extends BasicTreeUI.MouseHandler {
private JTree t = null;
private int xPos = 0;
private int yPos = 0;
public UpdateHandler(JTree tree) {
t = tree;
}
@Override
public void mouseMoved(MouseEvent e) {
xPos = e.getX();
yPos = e.getY();
t.repaint();
}
public int getXPos() {
return xPos;
}
public int getYPos() {
return yPos;
}
public int getRow() {
return t.getRowForLocation(xPos, yPos);
}
public int getRowHandle() {
return getRowForPath(t, getClosestPathForLocation(t, xPos, yPos));
}
}
//Method used for fade handles in or out
public void fadeHandles(boolean show, int msDur) {
Timer time = new Timer((int) Math.floor(msDur / 25), new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if(show)
count++;
else
count--;
val = count * 0.04F;
setCollapsedIcon(new AlphaImageIcon(new ImageIcon("resources/closed.png") , val));
setExpandedIcon(new AlphaImageIcon(new ImageIcon("resources/open.png") , val));
setRolloverIcon(new AlphaImageIcon(new ImageIcon("resources/rollover.png") , val));
if(count == 0 || count == 25)
((javax.swing.Timer) e.getSource()).stop();
t.repaint();
}
});
timers.add(time);
for(javax.swing.Timer timer : timers) {
timer.stop();
}
time.start();
}
//Methods used for remove lines
public void removeLines(boolean ShowLines) {
lines = ShowLines;
}
public void removeLines(boolean ShowLines, boolean dashedLines) {
lines = ShowLines;
lineTypeDashed = dashedLines;
}
@Override
protected void paintDropLine(Graphics g) {
if(!lines) {
JTree.DropLocation loc = tree.getDropLocation();
if (!isDropLine(loc))
return;
Color c = UIManager.getColor("Tree.dropLineColor");
if (c != null) {
g.setColor(c);
Rectangle rect = getDropLineRect(loc);
g.fillRect(rect.x, rect.y, rect.width, rect.height);
}
}
}
@Override
protected void paintHorizontalLine(Graphics g, JComponent c, int y, int left, int right) {
if(!lines) {
if (lineTypeDashed)
drawDashedHorizontalLine(g, y, left, right);
else
g.drawLine(left, y, right, y);
}
}
@Override
protected void paintVerticalLine(Graphics g, JComponent c, int x, int top, int bottom) {
if(!lines) {
if (lineTypeDashed)
drawDashedVerticalLine(g, x, top, bottom);
else
g.drawLine(x, top, x, bottom);
}
}
}
Any other comments, tips or suggestions are welcomed and appreciated.
ComputerDirectoryTreeModel.java
-private boolean addable(File path)
where the descriptions decide whether a drive is added. In Windows it isLocal Drive
andUSB Drive
. I don't know what it is in Mac \$\endgroup\$ArrayList<Timer> timers = new ArrayList<Timer>();
I suggest you useList
on the left hand side. What's important is thattimers
will have list functionality, not its implementation. \$\endgroup\$