Java Tip 100: Add a history mechanism to JFileChooser

Enhance JFileChooser by implementing a directory history and file previewer accessory

Good user interface design tries to minimize the needed user input. For example, some Unix system shells and editors provide auto completion for command input. The Windows desktop maintains a list of documents previously opened. And Netscape Navigator and Microsoft Internet Explorer have a URL history combo box or a list to track visited Websites. In the Java project I’m currently working on, my team and I built a test-and-monitoring tool to observe and analyze distributed systems. The user organizes his input data — for example, trace files — in different directories. Inspired by the history mechanisms in other systems, I had the idea to implement a history feature for directories as a reusable Java component.

I also decided to provide a file previewer with the directory navigator, for two reasons: First, I thought the mechanisms complemented one another, and second, the JFileChooser accessory layout looked much better with another panel below the navigator combo box widget. So, this Java tip discusses how to enhance the standard Swing JFileChooser by providing a directory history and a file previewer.

Adding accessories to JFileChooser is easy, and has been described previously in Java Tip 93 for the File Finder accessory. For convenience I will combine the File Finder with my navigator/previewer in one accessory that is controlled by a JTabbedPane. As shown in Java Tip 93, the major work is the accessory implementation, and not the linking to the JFileChooser component. Now I’ll take a closer look at how your users will benefit from this implementation and how you can achieve it.

Figure 1 shows the accessory in action: I have selected the file Menu_help_texts.txt from the E:JavaSourceExamplesSwing directory. Its first 500 bytes are shown in the preview pane below the navigator combo box. In Figure 2 the combo list is expanded so that the user can select one of three directories. The last directory name is larger than the available display area in the combo box; you have a tooltip connected to that item to show the user the complete path name.

Figure 1. The TextPreviewer in action. Click on thumbnail to view full image (12 KB)
Figure 2. The directory history combo box with tooltips. Click on thumbnail to view full image (12 KB)

Using EFileChooser as a wrapper

EFileChooser is a class that enhances JFileChooser by subclassing it. EFileChooser provides all the functionality necessary to add the accessory components and to connect them to the different file chooser events. First I’ll cover the implementation of the navigator combo box, which is basically a JComboBox that has a vector list with previously visited directories as its internal model. Every item in the list is a String that represents the directory path name. Because you will make the combo box remember its state after you restart the application, you will implement an object serialization mechanism that writes and reads the internal model — that is, the vector object.

Note that the directory list is not implemented as a ring buffer. It will eventually grow depending on how many different directories you are using. You can improve the code by allowing it to remember only the last 10 or so entries (the length of the ring buffer should be customizable by the user).

You use a configuration file called _DIRECTORY_HISTORY.cfg in the user’s home directory to store the combo box state between sessions. The application name differentiates between two or more Java applications that use the EFileChooser class. The name is provided in the constructor. If the application is started and the EFileChooser class is loaded, you examine the configuration file in order to build up the combo box model. Similarly, if the application is closed, you must provide a call to the static public EFileChooser.saveDirectoryEntries() method in order to store the current state. That call typically resides in the windowClosing() method of the application’s frame WindowListener. I have used the Java serialization mechanism here, but you could also use a text file to store the entries in a human-readable and -editable format.

The PreviewerAndHistoryPanel

The File Chooser accessory for your new features is placed in a PreviewAndHistoryPanel that subclasses JPanel. You register a ComboItemListener that watches for item state changes. Additionally you register keyboard actions for Delete and Shift-Delete. If the user has selected a directory in the combo box and presses the Delete key, that item is removed from the history list. Further, if the user presses the Shift-Delete keys, all history items are removed. To handle long directory path names, you implement your own combo-box renderer and replace the default one. The DeleteKeyListenener is straightforward: You use an action command to differentiate between the two states “Delete Selected Item” and “Delete All Items”. Of course, you can also write two different listeners, but the code will essentially be the same.

private final class DeleteKeyListener implements ActionListener {
  String action_;
  DeleteKeyListener(String action) {
    action_ = action;
  }
  public void actionPerformed(ActionEvent e) {
    if (action_.equals("ONE")) {
      combo.removeItemAt(combo.getSelectedIndex());
    }
    else {
      combo.removeAllItems();
    }
  }
}

The ItemListener watches for directory selections in the combo box list and sets the JFileChooser‘s current directory according to that selection. You use an ItemListener instead of an ActionListener because an action event is also generated if the Delete key or the Shift-Delete keys are pressed to delete an item.

An important detail is setting the tooltip text for the combo box item if the full directory name is too large to be displayed completely. To display tooltips, you must extend BasicComboBoxRenderer as shown in the code’s inner class ComboBoxRendererWithToolTips. Note that when JComboBox is located near the border of a frame, the tooltip doesn’t display outside the frame because of current Swing limitations.

private final class ComboItemListener implements ItemListener {
   String dir;
   public void itemStateChanged(ItemEvent e) {
      if (e.getStateChange() == ItemEvent.SELECTED) {
         selectNewDirectory();
      }
   }
   private void selectNewDirectory() {
      dir = (String)combo.getSelectedItem();
      EFileChooser.this.setCurrentDirectory(new File(dir));
      JLabel label = new JLabel(dir);
      label.setFont(combo.getFont());
      if (label.getPreferredSize().width > combo.getSize().width){
         combo.setToolTipText(dir);
      }
      else {
         combo.setToolTipText(null);
     }
   }
}

To review the actions completed so far, look at the PreviewAndHistoryPanel constructor to see how things are connected:

PreviewAndHistoryPanel() {
   setPreferredSize(new Dimension(250,250));
   setBorder(BorderFactory.createEtchedBorder());
   setLayout(new BorderLayout());
   combo = new MyComboBox(comboModel);
   comboItemListener = new ComboItemListener();
   combo.addItemListener(comboItemListener);
   combo.registerKeyboardAction(new DeleteKeyListener("ONE"),
      KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0, false),
      JComponent.WHEN_IN_FOCUSED_WINDOW);
   combo.registerKeyboardAction(new DeleteKeyListener("ALL"),
      KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,
      InputEvent.SHIFT_MASK, false),
      JComponent.WHEN_IN_FOCUSED_WINDOW);
   combo.setRenderer(new ComboBoxRendererWithTooltips());
   add(combo, BorderLayout.NORTH);
   previewer = new TextPreviewer();
   add(previewer, BorderLayout.CENTER);
}

The last component added is the TextPreviewer. There is nothing special about that section of coding; basically, the TextPreviewer maintains a 500-character buffer to read the first part of the selected file and to display it in a JTextArea. The previewer has a initTextArea() method that handles the connection to the JFileChooser. I will go over this point in the next section. As TextPreviewer‘s name suggests, currently only text files can display. Selecting other file types will result in confusing output. The user can overwrite the protected method EFileChooser.showFileContent() to display images or other file types — depending on the file extension — with specialized viewers.

Making connections

The EFileChooser constructor is responsible for establishing the event-triggered connections to the three accessory components. As mentioned earlier, I added the File Finder accessory described in Java Tip 93 to my directory and file preview component. Thus, you must connect three different accessory parts to EFileChooser: Directory History, Text Previewer, and File Finder. Here’s how that works:

public EFileChooser(String applicationName) {
   ...
   previewAndHistoryPanel = new PreviewAndHistoryPanel();
   findPanel = new FindAccessory(this, null);
   JPanel choicePanel = new JPanel(new BorderLayout());
   JTabbedPane choicePane = new JTabbedPane();
   choicePane.addTab("Navigation", previewAndHistoryPanel);
   choicePane.addTab("Find Files", findPanel);
   choicePanel.add(choicePane, BorderLayout.CENTER);
   setAccessory(choicePanel);
   addPropertyChangeListener(new PropertyChangeListener() {
      public void propertyChange(PropertyChangeEvent e) {
        if (e.getPropertyName().equals(
          JFileChooser.DIRECTORY_CHANGED_PROPERTY)) {
          findPanel.updateFindDirectory();
          previewer.clear();
          File dir = (File)e.getNewValue();
          if (dir == null) {
            return;
          }
        if (dir.getName().equals("")) {
          return;
        }
        String pathname = dir.getAbsolutePath();
        int i;
        boolean found = false;
        for (i=0; i < comboModel.size(); i++) {
          String dirname = (String)comboModel.elementAt(i);
          if (dirname.equals(pathname)) {
            found = true;
            break;
          }
        } // end for
      if (found) {
        combo.setSelectedIndex(i);
      }
    }
    if (e.getPropertyName().equals(
        JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) {(i);ectory
        File f = (File)e.getNewValue();
        showFileContents(f);
        insertDirectory(f);
    }
   } // end propertyChange
  }); // end addPropertyChangeListener
} // end constructor

The TextPreviewer is connected to the JFileChooser.SELECTED_FILE_CHANGED_PROPERTY. That’s where you refresh your display text area. The FileFinder is connected to the JFileChooser.DIRECTORY_CHANGED_PROPERTY via its updateFindDirectory() method. The directory history combo box is connected to both events. For the DIRECTORY_CHANGED_PROPERTY, the code tries to determine if the selected directory is already in the history list. If so, the user will select it in the combo box. If the user has switched to a new directory and opened a file, its name is added to the list in the SELECTED_FILE_CHANGED_PROPERTY part via insertDirectory(). That’s all!

To run and test the example, I have provided an EFileChooser main() method that starts a demo application. You’ll find that the code for FindAccessory from Java Tip 93 has been included in order for it to compile properly, but you can also modify the code and separate the File Finder from the Previewer/Directory History part. I have tested the application on Windows NT and on Windows 2000, running JDK 1.2.2.

One last trifle

As long as the ComboItemListener gets an itemStateChanged event, everything works fine and the new directory will be set in JFileChooser. But if the user presses the JFileChooser‘s Up button to move a step up in the directory hierarchy, the selection of a new directory by means of the combo box may not work.

Consider the following scenario: The selected directory in the combo box is called C:dirscurrentSelection. The user has switched to C:dirs, which is not in the history list. Therefore, the combo box still has C:dirscurrentSelection selected, but the user cannot switch back to that directory by using the combo box. Why not? Because no itemStateChanged event will be generated. Remember, the combo box will try to catch up with selected directories by moving the selected directory in the first list place as the “selected item”. However, that is only possible if it can be found in the list. Otherwise, the last selection in the combo box will still remain, and the user cannot switch to that item. To overcome such limitations, you must provide a new InvocationMouseHandler for your combo box. Doing so is tricky; the major step is to set a new user interface for the combo box. For further details, refer to the source code provided in Resources.

Basically, you must provide a new ComboBoxUI. Because the setUI() method is not public but protected, you must do that by subclassing ComboBox. I have called the resulting class MyComboBox. You extend javax.swing.plaf.basic.BasicComboBoxUI and pay special attention to BasicComboPopup. To make things work, you must provide new MouseListeners. Those listeners will activate your combo box directory change mechanism even in cases such as those described above.

There are two other situations that need different support: The MouseInvocationHandler is responsible for situations in which the left mouse button is released when the mouse is over the item to be selected. On the other hand, the ListMouseHandler comes into action if the user presses the left mouse button to expand the combo box list and then releases the mouse button immediately. The new combo box item is selected later on by a new mouse click while the combo box list is still expanded.

Conclusion

Applications that will often need files from different directories can profit from a JFileChooser enhancement that allows for easy directory changes. The history mechanism presented here remembers visited directory path names by means of a JComboBox. This tip has introduced such a directory history combo box as a JFileChooser accessory. In addition, it has added a text previewer that lets the user look at the first lines of a selected file. You can easily combine these two features with the File Finder accessory that has been described in Java Tip 93.

Klaus Berg has studied electrical engineering
and informatics at the University of Karlsruhe in Germany, and
works on system monitoring and performance engineering. Currently
he is an architect and implementer in a team at Siemens that is
building a Java tool to test and monitor distributed systems. His
research interests are Java GUI development and professional print
and export support for Java applications.

Source: www.infoworld.com