Java Recent File Menu

Background

While working on some editors for a Swing project I thought "gee, it would be nice to add a recent file menu like every other program on the planet has". It seemed like a simple enough idea but turned out to be a tad more time-consuming than I first estimated. Maybe I went about it totally wrong, you be the judge.

RecentFileMenu class

The logical route to go was to create an abstract class called RecentFileMenu that extended JMenu. It was abstract because it contained a method used to notify the parent class that a selection was made. Making this class abstract ensured the method would be overridden. The design for the class ended up looking like this:

RecentFileMenu object model

There are a few private properties that store the list of recent file entries, the maximum number of entries , the default text (used to hide unset entries), and the path to where the contents of the menu are saved. While reviewing this I had a couple of thoughts.. instead of using a defaultText constant I could just as easily have used an empty string, there'd be a minor performance gain from doing that. The recent file entries are stored in a file called "[name].recent", I guess the Properties class could have been used instead but that seems overly complicated. A more elegant solution would be to have RecentFileMenu plug into the configuration of the entire application. I wanted something quick and easy to implement from a parent application so the fancy design patterns will have to wait for next time.

So how does all this work? In the constructor the list of entries are initialized to the default text, the name of the saved file is determined, and the saved entries are loaded. If no saved entries are found then the menu is visible but disabled.


/**
* Create a new instance of RecentFileMenu.
* @param name The name of this menu, not displayed but used to store the
    list of recently used file names.
    * @param count The number of recent files to store.
*/
public RecentFileMenu(String name,int count){
    super();
    this.setText("Recent");
    this.setMnemonic('R');
    this.itemCount=count;
   		//initialize default entries
    this.recentEntries=new String[count];
    for(int index=0;index<this.itemCount;index++){
        this.recentEntries[index]=defaultText;
    }
    //figure out the name of the recent file
    this.pathToSavedFile=System.getProperty("user.dir");
    if((this.pathToSavedFile==null)||(this.pathToSavedFile.length()<=0)){
        this.pathToSavedFile=new String(name+".recent"); //probably unreachable
    } else if(this.pathToSavedFile.endsWith(File.separator)){
        this.pathToSavedFile=this.pathToSavedFile+name+".recent";
    } else{
        this.pathToSavedFile=this.pathToSavedFile+File.separator+name+".recent";
    }
    //load the recent entries if they exist
    File recentFile=new File(this.pathToSavedFile);
    if(recentFile.exists()){
        try{
            LineNumberReader reader=new LineNumberReader(new FileReader(this.pathToSavedFile));
            while(reader.ready()){
               
    this.addEntry(reader.readLine(),false);
            }
        } catch(Exception x){
            x.printStackTrace();
        } 
    } else{ //disable
        this.setEnabled(false);
    }
}

The public addEntry method just calls the private addEntry, passing true in the updateFile argument. The private add entry method moves all the menu items down one position. If filePath already exists in the list of entries it gets moved to the top, it shouldn't be possible to have the same entry appear twice. If updateFile is true it saves the entries in the .recent file.


/**
* Adds a new entry to the menu. Moves everything "down" one row.
* @param filePath The new path to add.
* @param updateFile Whether to update the saved file, only false when called from constructor.
*/
private void addEntry(String filePath,boolean updateFile){
    //check if this is disabled 
    if(!this.isEnabled()){
        this.setEnabled(true);
    }
    //clear the existing items
    this.removeAll();
    //move everything down one slot
    int count=this.itemCount-1;
    for(int index=count;index>0;index--){
        //check for duplicate entry
        if(!this.recentEntries[index-1].equalsIgnoreCase(filePath)){
            this.recentEntries[index]=new String(this.recentEntries[index-1]);
        }
    }
    //add the new item, check if it's not alredy the first item
    if(!this.recentEntries[0].equalsIgnoreCase(filePath)){
        this.recentEntries[0]=new String(filePath);
    }
    //add items back to the menu
    for(int index=0;index<this.itemCount;index++){
        JMenuItem menuItem=new JMenuItem();
        menuItem.setText(this.recentEntries[index]);
        if(this.recentEntries[index].equals(defaultText)){
            menuItem.setVisible(false);
        } else{
            menuItem.setVisible(true);
            menuItem.setToolTipText(this.recentEntries[index]);
            menuItem.setActionCommand(this.recentEntries[index]);
            menuItem.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent actionEvent){
               
    onSelectFile(actionEvent.getActionCommand());
            }
            });
        }
        this.add(menuItem);
    }
    //update the file
    if(updateFile){
        try{
            FileWriter writer=new FileWriter(new File(this.pathToSavedFile));
            int topIndex=this.itemCount-1;
            for(int index=topIndex;index>=0;index--){
               
    if(!this.recentEntries[index].equals(defaultText)){
                   
    writer.write(this.recentEntries[index]);
                   
    writer.write("\n");
               
    }
            }
            writer.flush();
            writer.close();
        } catch(Exception x){
            x.printStackTrace();
        }
    }
}

Using RecentFileMenu

Creating RecentFileMenu was a minor inconvenience, luckily using it is really easy. Here is all the coding that's necessary:


//class properties
private RecentFileMenu recentMenu;
[....]
//code to setup the file menu
JMenu menuFile=new JMenu();
menuFile.setText("File");
menuFile.setMnemonic('F');
this.recentMenu=new RecentFileMenu("RecentFileMenu_Test",10){
public void onSelectFile(String filePath){
        onRecentFile(filePath);
    }
};
menuFile.add(recentMenu);
[....]
//code to handle onRecentFile
public void onRecentFile(String filePath){
    call-your-open-file-routine(filePath);
}
[....]
//code to add a recent file to the menu
public void addRecentEntry(){
    recentMenu.addEntry(browsePanel.getFilePath());
}

Download

Source code for RecentFileMenu & RecentFileMenu_Test (6kb)

Related