Quickly access files and directories you use repeatedly

The JFileChooser shortcuts accessory organizes your documents

Developers have suggested many approaches for accelerating the selection of a directory or file in JFileChooser. TypeAheadSelector allows you to quickly select a specific file in the current directory by typing just the initial letters of its name (suitable for JFileChooser in J2SE (Java 2 Platform, Standard Edition) 1.3 and earlier versions—the chooser in J2SE 1.4 sports a built-in type-ahead feature). A history mechanism maintains a list of recently visited directories, allowing you to easily revisit the same directory. Some applications allow you to create and maintain a list of files that you can access from a menu. For instance, Microsoft Word XP allows you to add a Work menu to the Main menu and then add currently opened documents to the Work menu for quick access in the future.

In this article, I describe a JFileChooser shortcuts accessory that goes further than the Work menu. The accessory allows both files and directories in the list. In addition, you can define a short name (alias) for a given shortcut that proves easier to read than an absolute path. You can further improve your shortcuts’ readability by coding them in a specific color. Finally, the accessory also updates the chooser’s dialog title to show the path to the currently selected directory, which helps you navigate the file system.

Note: You can download this article’s source code from Resources.

The shortcuts accessory

The accessory consists of a list of shortcuts and, below that, a control panel, as shown in the figure below.

Shortcuts accessory

The list’s shortcuts are sorted alphabetically. A shortcut displays as either an absolute path to a directory/file or, for shortcuts with defined aliases, as its alias enclosed in brackets. For instance, four aliases are defined in the figure above: Flights Retriever, Java Source, Tomcat-Apps, and Tomcat-Work. When the mouse moves over an alias, a tool-tip text displays for that alias, showing the path to a file or directory represented by that alias.

To change the chooser’s current directory to the one represented by the shortcut, you click on a shortcut in the list. If the selected shortcut represents a file, the accessory selects that file in the chooser, and the file’s name appears in the File Name field. The accessory modifies the chooser’s dialog title to include the chooser’s current directory path in parentheses.

You can easily add/remove the list’s shortcuts and set/reset your aliases. Clicking the Add button adds the chooser’s current directory to the list or, if the accessory selects a file in the chooser, the selected file is added to the shortcuts list. The accessory ensures no duplicates appear in the list. Clicking the Delete button removes the currently selected shortcut from the list. To set a given shortcut’s alias, first select this shortcut, then type any text in the Alias field, and either press Enter on your keyboard or click the Set button. To change a shortcut’s color, add a color name and hash character (#) to an alias (for example, blue#Tomcat). When a shortcut with a nonempty alias is selected in the list, the alias also displays in the Alias field so you can easily rename or remove it (again, press Enter or click Set for confirmation). If you delete the shortcut’s alias in the Alias field, this shortcut again displays as its absolute path.

Shortcuts model

Let’s look closer at the implementation of the shortcuts list. Shortcuts are elements of JList, and each element is a Shortcut class instance. This class encapsulates three attributes of a shortcut, namely its alias (name), absolute path, and color for displaying on the screen. Shortcut contains accessor methods for those attributes, an important method getDisplayName() that defines how a shortcut displays in the list, and two helper methods for converting between Color and String.

DefaultListModel is the list’s model. The model is created in the createModel() method, where shortcut information is read from a file in your home directory. The shortcut file’s name is unique for a given application. An application’s name is passed as a parameter to the accessory and becomes part of the shortcut file’s name. Each line in the file represents one shortcut, except for lines beginning with two slashes (Java-style single-line comments), which are treated as comment lines and thus ignored. Each shortcut line contains a color name, hash character (#), alias (or empty string, if no alias is set), comma character, and shortcut’s absolute path. For instance: blue#Java,c:jdk1.3bin represents a shortcut whose alias is Java, path is c:jdk1.3bin, and which displays in blue. Shortcut recognizes many colors by their name (red, blue, and so on). You can specify other colors with a six-digit hexadecimal number representing a color’s RGB (red-green-blue) parameter. For instance, if you want to change the color of the shortcut defined above from blue to teal, you could replace blue with 66cc99: 66cc99#Java,c:jdk1.3bin.

The model’s shortcuts are sorted alphabetically by shortcut’s path or name as defined in Shortcut‘s getName() method. The insertShortcut() method implements this sorting. Initially, this method checks whether a new shortcut already appears in the list. If it does not, the method finds an index in the list where it can insert a new shortcut so the list’s sorted order is preserved.

All shortcuts are written back to the same file when you close the chooser. The accessory registers with the chooser as an ancestor listener (in the addListeners() method), and shortcuts are saved to a file when listener’s ancestorRemoved() method is called.

Render on the screen

The accessory displays shortcuts as either a path or an alias in a specified color. To implement a custom rendering of shortcuts on the screen, you create a new cell renderer for the list:

list.setCellRenderer(new ListCellRenderer() {
    public Component getListCellRendererComponent(JList list,
            Object value, int index, boolean isSelected,
            boolean cellHasFocus) {
        Shortcut shortcut = (Shortcut)value;
        String name = shortcut.getDisplayName();
        JLabel label = new JLabel(name);
        label.setBorder(new EmptyBorder(0,3,0,3));
        label.setOpaque(true);
        if (!isSelected) {
            label.setBackground(list.getBackground());
            label.setForeground(shortcut.getColor());
        } else {
            label.setBackground(list.getSelectionBackground());
            label.setForeground(list.getSelectionForeground());
        }
        return label;
    }
});

The renderer implements the only method defined in the ListCellRenderer interface, getListCellRendererComponent(). It creates a label component that draws the list’s cells. The label displays a shortcut’s name as defined in the getDisplayName() method. This method’s current implementation returns an alias enclosed in brackets or a shortcut’s path if the alias is empty. You can easily redefine getDisplayName() to render shortcuts differently. The renderer uses the default colors defined in JList to paint the currently selected cell. For all other cells, it sets the label’s foreground color to the shortcut’s color. You must set the label’s opaque attribute to true, otherwise the label’s text of the selected cell will not show up on the screen.

When you set an alias for a given shortcut, that alias replaces a shortcut’s path on the screen. In that case, a tool-tip displays a shortcut’s path when you position the mouse pointer over an alias in the list. A tool-tip also displays for the shortcuts with empty aliases whose paths are longer than the display area (horizontal scrolling of the accessory would be required).

To implement a smooth and quick display of tool-tips when you move the mouse from one shortcut to another, the accessory changes a tool-tip’s initial and dismiss-delay times as defined in ToolTipManager, so that tool-tips display quickly (after 0.3 seconds) and remain on the screen for a short time (2 seconds). When you close a chooser’s dialog, those delay times restore to their original values.

A tool-tip’s text changes as you move the mouse pointer from shortcut to shortcut. We achieve that functionality by extending JList and overriding JComponent‘s getToolTipText() method:

list = new JList(model) {
    public String getToolTipText(MouseEvent me) {
        if (model.size() == 0)
            return null;
            
        Point p = me.getPoint();
        Rectangle bounds = list.getCellBounds(model.size()-1, 
            model.size()-1);
        int lastElementBaseline = bounds.y + bounds.height;
        // Is the mouse pointer below the last element in the list?
        if (lastElementBaseline < p.y)
            return null;
        int index = list.locationToIndex(p);
        if (index == -1) // For compatibility with J2SE 1.3
            return null;
                    
        Shortcut shortcut = (Shortcut)model.get(index);
        String path = shortcut.getPath();
        if (shortcut.hasAlias())
            return path;
            
        FontMetrics fm = list.getFontMetrics(list.getFont());
        int textWidth = SwingUtilities.computeStringWidth(fm, path);
        if (textWidth <= listScrollPane.getSize().width)
            return null;
        return path;
    }       
};

The locationToIndex() method converts the mouse-click point to the list’s index. This index’s shortcut is inspected, and getToolTipText() returns the shortcut’s path if the shortcut has an alias set or when a shortcut’s path is longer than the display area. Computing a string’s width can be tricky; you should delegate this task to SwingUtilities‘s computeStringWidth() method.

The case when the list’s display area extends below its last element needs special consideration. We would like to display tool-tips for the list’s relevant shortcuts, but when the mouse pointer moves below the last shortcut, which represents an empty region of JList, no tool-tip should display. To add such behavior to the accessory, we must detect when the mouse pointer crosses the list’s last shortcut. It is a straightforward task in J2SE 1.3 and earlier versions since the JList‘s locationToIndex() method returns -1 as soon as the mouse pointer is positioned below the last element. But for the same situation in J2SE 1.4, locationToIndex() returns the last element’s index so we have no indication that the mouse is below the last element. In this case, the accessory compares the y coordinate of the mouse-click’s point with the y coordinate of the list’s last shortcut. The lastElementBaseline() variable in the code above represents the last shortcut’s y coordinate. If the last shortcut’s y coordinate is smaller than the mouse click’s y coordinate, the method returns null and no tool-tip is set.

Warning about customized tool-tips in J2SE 1.3

The ToolTipManager class manages tool-tips for Swing components. Only one instance of ToolTipManager is available for any given JVM and can be retrieved with the ToolTipManager.getSharedInstance() method. To set up a tool-tip for a component, you usually call the setToolTipText() method on that component. That method accepts a string argument that defines the text to display in the tool-tip. If this argument is null, no tool-tip will display for that component. To have different tool-tips display for different elements in a list, as in our case, it seems natural that you need only decide what text should display in each element’s tool-tip and then call setToolTipText() with either text or null—if no tool-tip should display—as an argument. Unfortunately, such an approach can result in serious side effects under some circumstances in J2SE 1.3 or earlier versions (The problems with ToolTipManager described here have been fixed in J2SE 1.4—this is another good reason to migrate to version 1.4!).

The innocent-looking setToolTipText() method does more than just set a tool-tip’s text. It causes the following chain of events in J2SE 1.3 or earlier versions:

  1. Calling a component’s setToolTipText() method with a nonnull argument causes the tool-tip manager to register itself as a mouse listener on the component
  2. ToolTipManager waits for the mouse pointer to enter the component
  3. When that happens, the manager registers itself as a mouse-motion listener on the component (in the mouseEntered() method)
  4. Now the manager has all information about mouse movements within the component and can orchestrate tool-tip display accordingly
  5. When the mouse pointer exits the component, the manager deregisters itself as a mouse-motion listener with the component in mouseExited()
  6. Calling a component’s setToolTipText() with a null argument causes the manager to deregister itself as a mouse listener with the component

What happens when setToolTipText() is called with a null argument just prior to the mouse pointer leaving the component? The manager is no longer a registered mouse listener on the component and is unable to remove itself as the component’s mouse-motion listener. As a result, we have an orphan mouse-motion listener that will stay registered with the component until the program exits. Every time the mouse pointer exits the list component through an element with no tool-tip set (setToolTipText() is called with a null argument), a new orphan listener is created.

The second side effect has more visible signs: if the mouse pointer leaves the list component through a null tool-tip element and then enters another component in the program’s GUI (graphical user interface) with a set tool-tip, on entering the list again through a null tool-tip element, no tool-tips will display in the entire list. Without going into too much detail, this side effect occurs because there is only one tool-tip manager for all Swing components, and other components in a program’s GUI can affect variables used in the manager. When you exit the second component with a tool-tip set, an insideComponent() variable in ToolTipManager sets to null. On entering the list again, ToolTipManager‘s showTipWindow() method will return prematurely without displaying a tool-tip, after detecting that insideComponent() is null.

If you are working with J2SE 1.3 or earlier versions, you should extend a component whose differing tool-tips will change as the mouse pointer rolls over it, and override the getToolTipText() method instead of using the setToolTipText() method.

How to use ShortcutsAccessory

To see how the accessory works, you must download this article’s source code. The ShortcutsAccessory class’s main() method contains the following lines that set the shortcuts accessory in JFileChooser:

JFileChooser chooser = new JFileChooser();
ShortcutsAccessory shortcuts = new ShortcutsAccessory(chooser, "demo");
chooser.setAccessory(shortcuts);
Dimension d = new Dimension(700, 400);
chooser.setMinimumSize(d);
chooser.setPreferredSize(d);

First, you create an accessory object by instantiating ShortcutsAccessory and passing two parameters to its constructor. The first parameter is the file chooser and the second is the name of your application that uses the chooser. This way you can maintain separate lists of shortcuts for each application. You invoke JFileChooser‘s setAccessory() method to set the accessory. If you plan to have many shortcuts in the accessory, you should increase the chooser’s size so less vertical scrolling is required when browsing the shortcut list.

Quick file access

The shortcuts accessory described in this article makes it easy to access files and directories you use repeatedly. Adding and removing shortcuts is so simple that within minutes you can build a whole list of shortcuts that will satisfy your current needs. Color-coding can further assist file/directory selection by letting you mark particular shortcuts with a specific color to easily focus on them. Since the accessory maintains a separate shortcuts list for each application, it allows you to implement a Work menu for maintaining a list of working documents/files/directories.

Slav
Boleslawski has been programming in Java since 1996 and is a
Sun Certified Java Programmer. His interests include GUI
development, database, and network programming. Currently he
designs database systems at an Australian law enforcement agency.

Source: www.infoworld.com