package org.freehep.swing; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Cursor; import java.awt.FlowLayout; import java.awt.Frame; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.util.Arrays; import java.util.Comparator; import java.util.Vector; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFileChooser; 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.UIManager; import javax.swing.event.EventListenerList; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.filechooser.FileFilter; import javax.swing.filechooser.FileSystemView; import javax.swing.filechooser.FileView; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; /** * A class which allows a user to select directories or files, similar to JFileChooser, * except that it display files as a tree, and is better suited to selecting directories * than the current file chooser (see java bug id 4239219). * * TODO: Understand issue with moving mouse between double clicks * (seems like MOUSE_PRESSED and MOUSE_RELEASED events are generated, but not MOUSE_CLICKED) * See Bug ID 4218549 * * @author Tony Johnson (tony_johnson@slac.stanford.edu) * @version $Id: JDirectoryChooser.java 8584 2006-08-10 23:06:37Z duns $ */ public class JDirectoryChooser extends JComponent { /** * Create a JDirectoryChooser with the default FileSystemView */ public JDirectoryChooser() { this(FileSystemView.getFileSystemView()); } /** * Create a JDirectoryChooser with the default FileSystemView * @param currentDirectory The directory to which the tree is initially set */ public JDirectoryChooser(File currentDirectory) { this(); setCurrentDirectory(currentDirectory); } /** * Create a JDirectoryChooser with the default FileSystemView * @param currentDirectory The directory to which the tree is initially set */ public JDirectoryChooser(String currentDirectory) { this(); if (currentDirectory != null) setCurrentDirectory(new File(currentDirectory)); } /** * Create a JDirectoryChooser * @param currentDirectory The directory to which the tree is initially set * @param view The FileSystemView to use */ public JDirectoryChooser(File currentDirectory, FileSystemView view) { this(view); setCurrentDirectory(currentDirectory); } /** * Create a JDirectoryChooser * @param currentDirectory The directory to which the tree is initially set * @param view The FileSystemView to use */ public JDirectoryChooser(String currentDirectory, FileSystemView view) { this(view); setCurrentDirectory(new File(currentDirectory)); } /** * Create a JDirectoryChooser * @param view The FileSystemView to use */ public JDirectoryChooser(FileSystemView view) { setup(view); ButtonListener al = new ButtonListener(); tree = new JTree(); tree.setRootVisible(false); tree.setShowsRootHandles(true); tree.setCellRenderer(new FileRenderer()); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.addTreeSelectionListener(al); approve.addActionListener(al); cancel.addActionListener(al); approve.setEnabled(false); setLayout(new BorderLayout()); add(new JScrollPane(tree)); JPanel p = new JPanel(new FlowLayout()); p.add(approve); p.add(cancel); add(p,BorderLayout.SOUTH); } public void addNotify() { tree.setModel(model); makeCurrentDirectoryVisible(); super.addNotify(); } private void makeCurrentDirectoryVisible() { if (currentDirectory != null) { File dir = currentDirectory; FileSystemView view = foo.getFileSystemView(); Vector v = new Vector(); while (true) { if (dir == null) return; // Something wrong, ignore dir v.addElement(dir); if (view.isRoot(dir)) break; dir = view.getParentDirectory(dir); } Object[] files = new Object[v.size()+1]; Object node = model.getRoot(); files[0] = node; for (int i=1; i<files.length; i++) { File here = (File) v.elementAt(files.length - i -1); int index = model.getIndexOfChild(node,here); if (index < 0) return; files[i] = node = model.getChild(node,index); } TreePath path = new TreePath(files); tree.setSelectionPath(path); tree.scrollPathToVisible(path); } } /** * Set the directory to which the tree is to open */ public void setCurrentDirectory(File dir) { currentDirectory = dir; } /** * Set a filter to control which files are displayed in the tree */ public void setFileFilter(FileFilter fileFilter) { foo.setFileFilter(fileFilter); model.changed(); } /** * Get the current file filter * @return The current FileFilter or null if none set */ public FileFilter getFileFilter() { return foo.getFileFilter(); } /** * Select whether to show "hidden" files in the tree * @param hide True if hidden files should not be shown */ public void setFileHidingEnabled(boolean hide) { foo.setFileHidingEnabled(hide); model.changed(); } /** * Test if file hiding is enabled * @return true if file hiding is enabled * @see #setFileHidingEnabled */ public boolean isFileHidingEnabled() { return foo.isFileHidingEnabled(); } /** * Set the file selection mode. Valid modes are * @param mode either DIRECTORIES_ONLY or FILES_AND_DIRECTORIES (the default) * @see javax.swing.JFileChooser#setFileSelectionMode */ public void setFileSelectionMode(int mode) { foo.setFileSelectionMode(mode); model.changed(); } /** * Test the file selection mode * @return The current file selection mode * @see #setFileSelectionMode */ public int getFileSelectionMode() { return foo.getFileSelectionMode(); } /** * Sets the filechooser to allow multiple file selections. */ public void setMultiSelectionEnabled(boolean enable) { foo.setMultiSelectionEnabled(enable); tree.getSelectionModel().setSelectionMode(enable ? TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION : TreeSelectionModel.SINGLE_TREE_SELECTION); } /** * Returns true if multi-file selection is enabled. */ public boolean isMultiSelectionEnabled() { return foo.isMultiSelectionEnabled(); } /** * Returns the currently selected file (or the first selected file if multiple files are selected) */ public File getSelectedFile() { return (File) tree.getLastSelectedPathComponent(); } /** * Returns a list of selected files if the filechooser is set to allow multi-selection. */ public File[] getSelectedFiles() { TreePath[] paths = tree.getSelectionPaths(); File[] result = new File[paths.length]; for (int i=0; i<paths.length; i++) result[i] = (File) paths[i].getLastPathComponent(); return result; } protected void setup(FileSystemView view) { foo.setFileSystemView(view); } public void updateUI() { super.updateUI(); foo.updateUI(); } /** * Popup up a modal dialog containing the JDirectoryChooser * @param parent The parent of the dialog box * @return either CANCEL_OPTION or JAPPROVE_OPTION * @see javax.swing.JFileChooser#showDialog */ public int showDialog(Component parent) { Frame frame = parent instanceof Frame ? (Frame) parent : (Frame)SwingUtilities.getAncestorOfClass(Frame.class, parent); String title = null; title = foo.getDialogTitle(); if (title == null) foo.getUI().getDialogTitle(foo); returnValue = JFileChooser.CANCEL_OPTION; dialog = new JDialog(frame, title, true); dialog.getContentPane().add(this,BorderLayout.CENTER); dialog.pack(); dialog.setLocationRelativeTo(parent); dialog.setVisible(true); return returnValue; } /** * Sets the string that goes in the FileChooser window's title bar */ public void setDialogTitle(String dialogTitle) { foo.setDialogTitle(dialogTitle); } /** * Gets the string that goes in the FileChooser's titlebar */ public String getDialogTitle() { return foo.getDialogTitle(); } /** * Sets the file view to used to retrieve UI information, such as the icon that * represents a file or the type description of a file. */ public void setFileView(FileView fileView) { foo.setFileView(fileView); } /** * Returns the current file view. */ public FileView getFileView() { return foo.getFileView(); } private FileTreeModel model = new FileTreeModel(); private JFileChooser foo = new JFileChooser(); private JButton approve = new JButton("Select"); private JButton cancel = new JButton("Cancel"); private File currentDirectory = null; private JTree tree; private JDialog dialog; private int returnValue; public static final int APPROVE_OPTION = JFileChooser.APPROVE_OPTION; public static final int CANCEL_OPTION = JFileChooser.CANCEL_OPTION; public static final int DIRECTORIES_ONLY = JFileChooser.DIRECTORIES_ONLY; public static final int FILES_AND_DIRECTORIES = JFileChooser.FILES_AND_DIRECTORIES; private static final boolean DIRS_FIRST = true; private static final Comparator comparator = new FileComparator(); private static final Object root = new Object(); private class ButtonListener implements ActionListener, TreeSelectionListener { public void actionPerformed(ActionEvent e) { if (e.getSource() == approve) returnValue = JFileChooser.APPROVE_OPTION; dialog.setVisible(false); dialog = null; } public void valueChanged(TreeSelectionEvent e) { approve.setEnabled(tree.getSelectionCount() > 0); } } private class FileRenderer extends DefaultTreeCellRenderer { public Component getTreeCellRendererComponent(JTree tree, Object node, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { Component comp = super.getTreeCellRendererComponent(tree,node,selected,expanded,leaf,row,hasFocus); if (comp instanceof JLabel && node instanceof File) { JLabel label = (JLabel) comp; File f = (File) node; label.setText(foo.getName(f)); label.setIcon(foo.getIcon(f)); } return comp; } } private class FileTreeModel implements TreeModel { public Object getChild(Object node, int index) { return children(node)[index]; } public int getChildCount(Object node) { return children(node).length; } public int getIndexOfChild(Object node, Object child) { File[] children = children(node); for (int i=0; i<children.length; i++) if (children[i].equals(child)) return i; return -1; } public Object getRoot() { return root; } public boolean isLeaf(Object node) { //return children(node).length == 0; return !(node == root || ((File) node).isDirectory()); } public void valueForPathChanged(TreePath p1, Object p2) { // We dont allow model changes } /** * Adds a listener for the TreeModelEvent posted after the tree changes. * * @see #removeTreeModelListener * @param l the listener to add */ public void addTreeModelListener(TreeModelListener l) { listenerList.add(TreeModelListener.class, l); } /** * Removes a listener previously added with <B>addTreeModelListener()</B>. * * @see #addTreeModelListener * @param l the listener to remove */ public void removeTreeModelListener(TreeModelListener l) { listenerList.remove(TreeModelListener.class, l); } private File[] children(Object node) { for (int i=0; i<CACHE_SIZE; i++) { if (node == cachedNode[i]) return cachedChildren[i]; } Window w = null; Cursor oldCursor = null; if (tree != null && tree.isVisible()) { w = (Window) SwingUtilities.getAncestorOfClass(Window.class,tree); if (w != null) { oldCursor = w.getCursor(); w.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); } } try { FileSystemView view = foo.getFileSystemView(); boolean isRoot = (node == root); File[] children = isRoot ? view.getRoots() : view.getFiles((File) node,foo.isFileHidingEnabled()); if (!isRoot) { boolean dirOnly = foo.getFileSelectionMode() == JFileChooser.DIRECTORIES_ONLY; if (foo.getFileFilter() != null || dirOnly) { Vector v = new Vector(); for (int i=0; i<children.length; i++) { File f = children[i]; if (dirOnly && !f.isDirectory()) continue; if (foo.accept(f)) v.addElement(children[i]); } if (v.size() != children.length) { children = new File[v.size()]; v.copyInto(children); } } } Arrays.sort(children,comparator); cachedNode[nextCache] = node; cachedChildren[nextCache] = children; nextCache = (nextCache + 1) % CACHE_SIZE; return children; } finally { if (w != null) w.setCursor(oldCursor); } } void changed() { cachedNode = new Object[CACHE_SIZE]; cachedChildren = new File[CACHE_SIZE][]; fireTreeStructureChanged(new TreeModelEvent(this, new TreePath(root))); } protected void fireTreeStructureChanged(TreeModelEvent e) { Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==TreeModelListener.class) { ((TreeModelListener)listeners[i+1]).treeStructureChanged(e); } } } private EventListenerList listenerList = new EventListenerList(); private final int CACHE_SIZE = 10; private Object[] cachedNode = new Object[CACHE_SIZE]; private File[][] cachedChildren = new File[CACHE_SIZE][]; private int nextCache = 0; } private static class FileComparator implements Comparator { public int compare(Object o1, Object o2) { File f1 = (File) o1; File f2 = (File) o2; if (DIRS_FIRST) { boolean d1 = f1.isDirectory(); boolean d2 = f2.isDirectory(); if (d1 && !d2) return -1; if (!d1 && d2) return 1; } return f1.getName().compareTo(f2.getName()); } } // Test code public static void main(String argv[]) throws Exception { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); final JTextField tf = new JTextField(40); JButton b = new JButton("Show Browser") { public void fireActionPerformed(ActionEvent e) { JDirectoryChooser dlg = new JDirectoryChooser(tf.getText()); dlg.setFileHidingEnabled(false); dlg.showDialog(this); if (dlg.getSelectedFile() != null) tf.setText(dlg.getSelectedFile().toString()); } }; JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(b,BorderLayout.CENTER); frame.getContentPane().add(tf,BorderLayout.SOUTH); frame.pack(); frame.setVisible(true); } }