/* * Copyright (c) 2008, SQL Power Group Inc. * * This file is part of Power*Architect. * * Power*Architect is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * Power*Architect is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.sqlpower.swingui; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.prefs.PreferenceChangeEvent; import java.util.prefs.PreferenceChangeListener; import java.util.prefs.Preferences; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.SwingUtilities; import org.apache.log4j.Logger; /** * Maintain a "Recent Items" menu component. The caller must override a "template method" * named loadFile, and all open operations must be channeled through the RecentMenu's * openFile method, which calls loadFile and then updates the Recent Items. * <p> * Recent items are persisted across runs of the program, using java.util.prefs.Prefences under a * UserNode with the class passed into the constructor, which may be a Class descriptor (from within a static main * method using MyClassName.class) or simply passing main program (its getClass method will be called). * <p> * Will most commonly be used to maintain a list of recent files. * Typical usage might be as follows: * <pre> * // Assume this code is in MyMainClass constructor * JMenuItem open = new JMenuItem("Open"); * fileMenu.add(open); * final RecentMenu recent; * recent = new RecentMenu(this.getClass()) { * @Override * public void loadFile(String fileName) throws IOException { * myModel.openFile(fileName); * } * * }; * open.addActionListener(new ActionListener() { * public void actionPerformed(ActionEvent evt) { * try { * // maybe get "someFileName" from a JFileChooser... * recent.openFile(someFileName); * } catch (IOException e) { * myErrorPopup("Could not open file" + fileName, e); * } * } * * }); * fileMenu.add(recent); * JMenuItem clearItem = new JMenuItem("Clear Recent Files"); * clearItem.addActionListener(new ActionListener() { * public void actionPerformed(ActionEvent e) { * recentFilesMenu.clear(); * } * }); * </pre> * @author Ian Darwin */ public abstract class RecentMenu extends JMenu { private static final Logger logger = Logger.getLogger(RecentMenu.class); public final static int DEFAULT_MAX_RECENT_FILES = 5; private final int maxRecentFiles; private static final String PREFS_KEY = "recentFile"; //$NON-NLS-1$ /** The List of recent files */ private List<String> recentFileNames = new ArrayList<String>(); final Preferences prefs; /** Construct a RecentMenu with a given class and the number of items to hold * The class is used to for UserPrefsNode to determine where to save the list. * @param mainClass * @param max */ public RecentMenu(Class <?> mainClass, int max) { super(Messages.getString("RecentMenu.recentItems")); //$NON-NLS-1$ prefs = getUserPrefsNode(mainClass); prefs.addPreferenceChangeListener(recentListener); maxRecentFiles = max; logger.debug("Called from Recent menu constructor"); //$NON-NLS-1$ loadRecentMenu(); } /** * @param mainClass */ public static Preferences getUserPrefsNode(Class <?> mainClass) { return Preferences.userNodeForPackage(mainClass); } /** Construct a RecentMenu with a given class. * The class is used to for UserPrefsNode to determine where to save the list. * @param mainClass */ public RecentMenu(Class <?> mainClass) { this(mainClass, DEFAULT_MAX_RECENT_FILES); } /** * Call back to the main program to load the file, and and update * the recent files list. */ public void openFile(String fileName) throws IOException { loadFile(fileName); putRecentFileName(fileName); } /** * This is a template method that the caller must provide to * actually open a file. * @param fileName */ public abstract void loadFile(String fileName) throws IOException; /** * ActionListener that is used by all the Menuitems in the Recent Menu; * just opens the file named by the MenuItem's text. */ private ActionListener recentOpener = new ActionListener() { public void actionPerformed(ActionEvent e) { JMenuItem mi = (JMenuItem) e.getSource(); try { openFile(mi.getText()); } catch (IOException e1) { SPSUtils.showExceptionDialogNoReport(Messages.getString("RecentMenu.couldNotOpenFile"), e1); //$NON-NLS-1$ } } }; /** * PreferenceChangeListener that is used by all the sessions; * updates the recent file lists of each session if one is changed. */ private PreferenceChangeListener recentListener = new PreferenceChangeListener() { public void preferenceChange(PreferenceChangeEvent evt) { logger.debug("Called from pref change"); //$NON-NLS-1$ //Invoke later is used because the preference change events //are called on a separate thread (at least in Windows) SwingUtilities.invokeLater(new Runnable() { public void run() { loadRecentMenu(); } }); } }; /** * Add the given filename to the top of the recent list in Prefs and in menu. * It is generally <b>not</b> necessary for user code to call this method! */ public void putRecentFileName(String f) { recentFileNames.clear(); for (int i = 0; i < maxRecentFiles; i++) { String file = prefs.get(PREFS_KEY + i, null); if (file == null) { // Stop on first missing break; } if (file.equals("Clear Recent Items")) { //$NON-NLS-1$ break; } if (new File(file).exists()) { if (recentFileNames.contains(file)) { recentFileNames.remove(file); } recentFileNames.add(file); } } // Move filename to front: Remove if present, add at front. if (recentFileNames.contains(f)) { recentFileNames.remove(f); } // Trim from back end if too long while (recentFileNames.size() > maxRecentFiles - 1) { recentFileNames.remove(recentFileNames.size()-1); } recentFileNames.add(0, f); // Now save from List into Prefs for (int i = 0; i < recentFileNames.size(); i++) { String t = recentFileNames.get(i); logger.debug("put " + t); //$NON-NLS-1$ prefs.put(PREFS_KEY + i, t); } logger.debug("Called from putFileName"); //$NON-NLS-1$ // Finally, load menu again. loadRecentMenu(); } /** * Load or re-load the recentFileMenu */ public void loadRecentMenu() { recentFileNames.clear(); setEnabled(false); removeAll(); // Copy from Prefs into Menu JMenuItem mi; for (int i = 0; i < maxRecentFiles; i++) { String f = prefs.get(PREFS_KEY + i, null); if (f == null) { // Stop on first missing break; } //stops the loop if it passes the end if (f.equals("Clear Recent Items")) { //$NON-NLS-1$ break; } // Drop from list if file has been deleted. if (new File(f).exists()) { // If at least one item, enable menu setEnabled(true); // And add to Menu this.add(mi = new JMenuItem(f)); mi.addActionListener(recentOpener); //add to the list for to be used by //getMostRecentFile() recentFileNames.add(f); } } //Add in the clear item to the menu this.addSeparator(); JMenuItem clearItem = new JMenuItem(Messages.getString("RecentMenu.clearRecentFiles")); //$NON-NLS-1$ clearItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { clear(); } }); recentFileNames.add("Clear Recent Items"); //$NON-NLS-1$ this.add(clearItem); } /** * Clear all saved Recent Items from Preferences, from memory, and from the Menu. * There is NO UNDO for this so call with care. */ public void clear() { for (int i = 0; i < maxRecentFiles; i++) { prefs.remove(PREFS_KEY + i); } loadRecentMenu(); } /** * Returns the most recent path in this recent menu as a File object. * If there are no recent files in this menu, returns null. */ public File getMostRecentFile() { if (recentFileNames.size() > 0) { String mostRecentPath = recentFileNames.get(0); return new File(mostRecentPath); } else { return null; } } }