Java Tip 93: Add a file finder accessory to JFileChooser

Enhance JFileChooser by implementing your own accessories

This tip describes how to extend the functionality of one of the most common user interface components -- the standard file open dialog -- with a threaded file search accessory.

When you attempt to open a file but can't locate it immediately, simply enter your search criteria into the accessory's search fields, hit the Start button, and wait for a list of found files to appear. That search accessory is integrated into the open file dialog and the file search is threaded so you can continue to browse the file system while the search is running.

Adding functionality to Swing's standard file dialog is easy once you understand how to integrate a component into JFileChooser's dialog box, how to make the component responsive to JFileChooser events, and how to control the JFileChooser's file display and selections. I will provide an example accessory with this article. The complete source code for the FindAccessory class is included in Resources. Refer to Jon Sharpe's Java Tip 85 for a review of JFileChooser basics.

Accessorizing JFileChooser

Customizing JFileChooser is easy. Rather than reinvent the standard file dialog to include special functionality, you can implement your custom functionality as a JComponent and integrate it into JFileChooser with a single method call.

    JFileChooser chooser = new JFileChooser();
    chooser.setAccessory(new FindAccessory());

These two lines of code are deceptively simple. On the surface, a FindAccessory component is attached to a standard file open dialog, as illustrated in Figure 1. At a deeper level, FindAccessory is modifying the behavior of JFileChooser. The details of integration are hidden inside the accessory's implementation.

Figure 1. FindAccessory component attached to JFileChooser as it appears with Swing's Metal look and feel

To fully appreciate the power of accessories and the flexibility of JFileChooser, you'll need to understand JFileChooser's properties, events, and control methods. But first, you should know how an accessory component is displayed within the JFileChooser dialog.

Controlling accessory layout

It is especially important to understand how specific layout managers work when implementing complex JFileChooser accessories. Some layout managers, like GridLayout, disregard a component's preferred size. In Java 1.2.2, JFileChooser is too eager to shrink its scrolling list of files to accommodate an accessory. Without some dimensional limitations, a complex accessory can expand to crowd out JFileChooser's file display list and control buttons.

To make layout matters even worse, some components such as text fields and lists, tend to expand to accommodate the width of their content. The rules for sizing JTextFields are particularly complex. Java Swing by Robert Eckstein, Marc Loy, and Dave Wood provides a thorough explanation of text field sizing (see Resources).

In early trials with GridLayout manager, FindAccessory's width would expand during a search to accommodate the widest item in its results list. That expansion often scrunched JFileChooser's file display list to a ridiculously narrow width.

To work around layout and expansion problems, FindAccessory uses the BorderLayout manager, which respects a component's preferred size. In addition, the results pane fixes the preferred and maximum dimensions of its scrolling results list just prior to the start of a search.

Dimension dim = resultsScroller.getSize();
resultsScroller.setMaximumSize(dim);
resultsScroller.setPreferredSize(dim);

Fixing the preferred and maximum dimensions late or just prior to a search lets FindAccessory panels display nicely when JFileChooser displays its dialog but prevents runaway expansion as the results list fills up.

Swing can emulate the look and feel of various GUI platforms through its Pluggable Look-and-Feel (PLAF) architecture. Swing 1.2.2 includes support for three themes: Windows, Motif, and Metal. Accessory appearance will vary, depending on which PLAF is active. You should test your accessory layout with each PLAF.

Responding to JFileChooser events

Attaching an accessory to JFileChooser is easy, but integrating an accessory into JFileChooser requires an understanding of event and property change listeners. An accessory can monitor its parent's property changes and action events to respond to the user's browsing and file selection activities. Complex accessories may need to terminate threads or close temporary files when the user clicks the Open, Save, or Cancel buttons.

PropertyChangeListener

Property change listeners are familiar to JavaBeans developers as the mechanism an object uses to notify other objects when a bound property value changes. Swing makes it easy for objects to receive PropertyChangeEvents from any JComponent. Just implement the java.beans.PropertyChangeListener interface and register your object with the component's addPropertyChangeListener() method.

Accessories implementing the java.beans.PropertyChangeListener interface can register with JFileChooser to receive notification of directory changes, selection changes, file filter changes, and more. See the JDK documentation for a complete list.

FindAccessory displays the absolute path of the root folder for your search. This display freezes when running a search. When a search is not running FindAccessory updates the search path display in response to a JFileChooser.DIRECTORY_CHANGED_PROPERTY event. In other words, FindAccessory tracks your movement through the file system with a PropertyChangeEvent from JFileChooser.

The code is very simple:

public void propertyChange (PropertyChangeEvent e)
{
    String prop = e.getPropertyName();
    if (prop.equals(JFileChooser.DIRECTORY_CHANGED_PROPERTY))
    {
        updateSearchDirectory();
    }
}

ActionListener

Accessories implementing the java.awt.event.ActionListener interface can receive notification when you click the Open, Save, or Cancel buttons.

FindAccessory stops a search when you click the Open or Cancel buttons. The ActionListener method is simple:

public void actionPerformed (ActionEvent e)
{
    String command = e.getActionCommand();
    if (command == null) return; // Can this happen? Probably not. Call me paranoid.
    if (command.equals(JFileChooser.APPROVE_SELECTION))
        quit();
    else if (command.equals(JFileChooser.CANCEL_SELECTION))
        quit();
}

Controlling JFileChooser

An accessory can be more than a slave to JFileChooser properties and events. It can exert as much control over JFileChooser as a user with a keyboard and mouse.

When you double-click an item in FindAccessory's search result list, JFileChooser displays and selects that item. FindAccessory uses JFileChooser methods to set the current directory, to set the current selection, and to change the type of files displayed.

Below is the code for FindAccessory's goTo() method that commands JFileChooser to display and select a file when you double-click an item in the search results list. The process is a bit more complicated than invoking JFileChooser.setSelectedFile(). First, you set JFileChooser's current file display filter to allow your file to be displayed. Second, you set JFileChooser's current directory to the folder containing the specified file. Finally, you invoke JFileChooser.setSelectedFile().

Step 2 is only necessary if you're running a version prior to Java 1.2.2. A bug in JFileChooser.setSelectedFile() didn't always change the current directory.

/**
    Set parent's current directory to the parent folder of the specified
    file and select the specified file. That method is invoked when the
    user double-clicks on an item in the results list.
    @param f File to select in parent JFileChooser
 */
public void goTo (File f)
{
    if (f == null) return;
    if (!f.exists()) return;
    if (chooser == null) return;
    // Make sure that files and directories
    // can be displayed
    chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
    // Make sure that parent file chooser will
    // show the type of file specified
    javax.swing.filechooser.FileFilter filter = chooser.getFileFilter();
    if (filter != null)
    {
        if (!filter.accept(f))
        {
            // The current filter will not
            // display the specified file.
            // Set the file filter to the
            // built-in accept-all filter (*.*)
            javax.swing.filechooser.FileFilter all = 
                chooser.getAcceptAllFileFilter();
            chooser.setFileFilter(all);
        }
    }
    // Tell parent file chooser to display contents of parentFolder.
    // Prior to Java 1.2.2 setSelectedFile() did not set the current
    // directory the folder containing the file to be selected.
    File parentFolder = f.getParentFile();
    if (parentFolder != null) chooser.setCurrentDirectory(parentFolder);
    // Nullify the current selection if any.
    // Why is this necessary?
    // JFileChooser gets sticky (i.e., it does not
    // always relinquish the current selection).
    // Nullifying the current selection seems to yield better results.
    chooser.setSelectedFile(null);
    // Select the file
    chooser.setSelectedFile(f);
    // Refresh file chooser display.
    // Is this really necessary? Testing on a variety of systems with
    // Java 1.2.2 suggests that helps. Sometimes it doesn't work,
    // but it doesn't do any harm.
    chooser.invalidate();
    chooser.repaint();
}

Caveats

JavaSoft's Bug Database contains 260 bug reports for JFileChooser. Of those 260 reports, 12 are related to JFileChooser.setSelectedFile(), but 10 have been fixed for JDK 1.2.2. You should make sure to run the most recent release of Java. FindAccessory has been tested with JDK 1.2.2 on Windows NT/98/95. The only known problem is JFileChooser's reluctance to display the selection when you double-click a file in the Found list. JFileChooser.setSelectedFile() selects the specified file, but the selection is not always displayed in the scrolling file list. You will see the file name displayed correctly, but the file list does not highlight it. The Open button works. Double clicking the item a second time displays the selection correctly. That bug appears to be cosmetic.

FindAccessory implementation details

FindAccessory extends JPanel and implements a threaded utility for finding files by name, date of modification, and content. FindAccessory is comprised of three components: a controller, a user interface, and a search engine. For code simplicity, the controller and the search engine are implemented within the FindAccessory class.

Search options are specified in three tab panes labeled Name, Date, and Content. Results are displayed in a fourth tab pane labeled Found. Searching is recursive from the current location (the path is displayed above the search tabbed panes). The search function is threaded so you can continue to browse the file system while a search is running. You may change the search criteria without affecting a running search.

Search results are displayed dynamically in a scrolling JList within the Found tab pane. You can double-click an entry in the results list to force JFileChooser to show and select the entry in its main scrolling view.

Search progress is displayed as a text label in the lower right corner of the accessory as number of items found/number of items searched.

FindAccessory user interface

Accessory layout varies depending on which Pluggable Look-and-Feel (PLAF) is active. Windows and Metal PLAF render JFileChooser with similar layouts and allocate comparable space for your accessory. By contrast, Motif PLAF allocates much less space for an accessory, so your components may appear scrunched. You could customize your layout for each PLAF. FindAccessory uses a 10-point Helvetica font and arranges components to use minimal space. Test your accessory with each PLAF to make sure it looks right.

FindAccessory tab panes

In addition to the find-by-name tab illustrated in Figure 1,

FindAccessory

contains find-by-date, find-by-content, and found-items tabs, as shown by Figures 2 through 4.

Figure 2. FindAccessory finds files by date of modification
Figure 3. FindAccessory finds files by text content
Figure 4. The absolute path of each found file appends to a scrolling list as the search progresses

Finding the right files

Implementing the selection functions for a search engine can be complicated. Ideally, the search controller and the search engine should know nothing about the implementation of a search function's selection algorithm. The FindAccessory class implements a recursive search engine that uses an array of FindFilter objects to test a file's acceptance. Each FindAccessory tab pane is responsible for implementing a user interface for the search as well as a FindFilter object. The search engine and the file selection functions enjoy separate responsibilities.

Each of FindAccessory's search tabs implements a FindFilterFactory interface. When a search begins, the FindAccessory controller loops through the tab panes and invokes newSearch() on each instance of FindFilterFactory to retrieve a FindFilter. The controller initializes the search engine with the array of FindFilters. Each FindFilter implements an accept() method so the selection algorithms are completely hidden from the search engine.

Extending FindAccessory with a new search category is an easy three-step process:

1 2 Page 1
Page 1 of 2