001package org.jdesktop.swingx.search; 002 003import java.awt.event.ActionEvent; 004import java.awt.event.ActionListener; 005import java.security.AccessControlException; 006import java.util.ArrayList; 007import java.util.Arrays; 008import java.util.List; 009import java.util.logging.Logger; 010import java.util.prefs.BackingStoreException; 011import java.util.prefs.Preferences; 012 013import javax.swing.JMenuItem; 014import javax.swing.JPopupMenu; 015import javax.swing.JTextField; 016import javax.swing.event.ChangeEvent; 017import javax.swing.event.ChangeListener; 018 019import org.jdesktop.swingx.JXSearchField; 020import org.jdesktop.swingx.plaf.UIManagerExt; 021 022/** 023 * Maintains a list of recent searches and persists this list automatically 024 * using {@link Preferences}. A recent searches popup menu can be installed on 025 * a {@link JXSearchField} using {@link #install(JXSearchField)}. 026 * 027 * @author Peter Weishapl <petw@gmx.net> 028 * 029 */ 030public class RecentSearches implements ActionListener { 031 private Preferences prefsNode; 032 033 private int maxRecents = 5; 034 035 private List<String> recentSearches = new ArrayList<String>(); 036 037 private List<ChangeListener> listeners = new ArrayList<ChangeListener>(); 038 039 /** 040 * Creates a list of recent searches and uses <code>saveName</code> to 041 * persist this list under the {@link Preferences} user root node. Existing 042 * entries will be loaded automatically. 043 * 044 * @param saveName 045 * a unique name for saving this list of recent searches 046 */ 047 public RecentSearches(String saveName) { 048 this(null, saveName); 049 } 050 051 /** 052 * Creates a list of recent searches and uses <code>saveName</code> to 053 * persist this list under the <code>prefs</code> node. Existing entries 054 * will be loaded automatically. 055 * 056 * @param prefsNode 057 * the preferences node under which this list will be persisted. 058 * If prefsNode is <code>null</code> the preferences node will 059 * be set to the user root node 060 * @param saveName 061 * a unique name for saving this list of recent searches. If 062 * saveName is <code>null</code>, the list will not be 063 * persisted 064 */ 065 public RecentSearches(Preferences prefs, String saveName) { 066 if (prefs == null) { 067 try { 068 prefs = Preferences.userRoot(); 069 } catch (AccessControlException ace) { 070 // disable persistency, if we aren't allowed to access 071 // preferences. 072 Logger.getLogger(getClass().getName()).warning("cannot acces preferences. persistency disabled."); 073 } 074 } 075 076 if (prefs != null && saveName != null) { 077 this.prefsNode = prefs.node(saveName); 078 load(); 079 } 080 } 081 082 private void load() { 083 // load persisted entries 084 try { 085 String[] recent = new String[prefsNode.keys().length]; 086 for (String key : prefsNode.keys()) { 087 recent[prefsNode.getInt(key, -1)] = key; 088 } 089 recentSearches.addAll(Arrays.asList(recent)); 090 } catch (Exception ex) { 091 // ignore 092 } 093 } 094 095 private void save() { 096 if (prefsNode == null) { 097 return; 098 } 099 100 try { 101 prefsNode.clear(); 102 } catch (BackingStoreException e) { 103 // ignore 104 } 105 106 int i = 0; 107 for (String search : recentSearches) { 108 prefsNode.putInt(search, i++); 109 } 110 } 111 112 /** 113 * Add a search string as the first element. If the search string is 114 * <code>null</code> or empty nothing will be added. If the search string 115 * already exists, the old element will be removed. The modified list will 116 * automatically be persisted. 117 * 118 * If the number of elements exceeds the maximum number of entries, the last 119 * entry will be removed. 120 * 121 * @see #getMaxRecents() 122 * @param searchString 123 * the search string to add 124 */ 125 public void put(String searchString) { 126 if (searchString == null || searchString.trim().length() == 0) { 127 return; 128 } 129 130 int lastIndex = recentSearches.indexOf(searchString); 131 if (lastIndex != -1) { 132 recentSearches.remove(lastIndex); 133 } 134 recentSearches.add(0, searchString); 135 if (getLength() > getMaxRecents()) { 136 recentSearches.remove(recentSearches.size() - 1); 137 } 138 save(); 139 fireChangeEvent(); 140 } 141 142 /** 143 * Returns all recent searches in this list. 144 * 145 * @return the recent searches 146 */ 147 public String[] getRecentSearches() { 148 return recentSearches.toArray(new String[] {}); 149 } 150 151 /** 152 * The number of recent searches. 153 * 154 * @return number of recent searches 155 */ 156 public int getLength() { 157 return recentSearches.size(); 158 } 159 160 /** 161 * Remove all recent searches. 162 */ 163 public void removeAll() { 164 recentSearches.clear(); 165 save(); 166 fireChangeEvent(); 167 } 168 169 /** 170 * Returns the maximum number of recent searches. 171 * 172 * @see #put(String) 173 * @return the maximum number of recent searches 174 */ 175 public int getMaxRecents() { 176 return maxRecents; 177 } 178 179 /** 180 * Set the maximum number of recent searches. 181 * 182 * @see #put(String) 183 * @param maxRecents 184 * maximum number of recent searches 185 */ 186 public void setMaxRecents(int maxRecents) { 187 this.maxRecents = maxRecents; 188 } 189 190 /** 191 * Add a change listener. A {@link ChangeEvent} will be fired whenever a 192 * search is added or removed. 193 * 194 * @param l 195 * the {@link ChangeListener} 196 */ 197 public void addChangeListener(ChangeListener l) { 198 listeners.add(l); 199 } 200 201 /** 202 * Remove a change listener. 203 * 204 * @param l 205 * a registered {@link ChangeListener} 206 */ 207 public void removeChangeListener(ChangeListener l) { 208 listeners.remove(l); 209 } 210 211 /** 212 * Returns all registered {@link ChangeListener}s. 213 * 214 * @return all registered {@link ChangeListener}s 215 */ 216 public ChangeListener[] getChangeListeners() { 217 return listeners.toArray(new ChangeListener[] {}); 218 } 219 220 private void fireChangeEvent() { 221 ChangeEvent e = new ChangeEvent(this); 222 223 for (ChangeListener l : listeners) { 224 l.stateChanged(e); 225 } 226 } 227 228 /** 229 * Creates the recent searches popup menu which will be used by 230 * {@link #install(JXSearchField)} to set a search popup menu on 231 * <code>searchField</code>. 232 * 233 * Override to return a custom popup menu. 234 * 235 * @param searchField 236 * the search field the returned popup menu will be installed on 237 * @return the recent searches popup menu 238 */ 239 protected JPopupMenu createPopupMenu(JTextField searchField) { 240 return new RecentSearchesPopup(this, searchField); 241 } 242 243 /** 244 * Install a recent the searches popup menu returned by 245 * {@link #createPopupMenu(JXSearchField)} on <code>searchField</code>. 246 * Also registers an {@link ActionListener} on <code>searchField</code> 247 * and adds the search string to the list of recent searches whenever a 248 * {@link ActionEvent} is received. 249 * 250 * Uses {@link NativeSearchFieldSupport} to achieve compatibility with the native 251 * search field support provided by the Mac Look And Feel since Mac OS 10.5. 252 * 253 * @param searchField 254 * the search field to install a recent searches popup menu on 255 */ 256 public void install(JTextField searchField) { 257 searchField.addActionListener(this); 258 NativeSearchFieldSupport.setFindPopupMenu(searchField, createPopupMenu(searchField)); 259 } 260 261 /** 262 * Remove the recent searches popup from <code>searchField</code> when 263 * installed and stop listening for {@link ActionEvent}s fired by the 264 * search field. 265 * 266 * @param searchField 267 * uninstall recent searches popup menu 268 */ 269 public void uninstall(JXSearchField searchField) { 270 searchField.removeActionListener(this); 271 if (searchField.getFindPopupMenu() instanceof RecentSearchesPopup) { 272 removeChangeListener((ChangeListener) searchField.getFindPopupMenu()); 273 searchField.setFindPopupMenu(null); 274 } 275 } 276 277 /** 278 * Calls {@link #put(String)} with the {@link ActionEvent}s action command 279 * as the search string. 280 */ 281 @Override 282 public void actionPerformed(ActionEvent e) { 283 put(e.getActionCommand()); 284 } 285 286 /** 287 * The popup menu returned by 288 * {@link RecentSearches#createPopupMenu(JXSearchField)}. 289 */ 290 public static class RecentSearchesPopup extends JPopupMenu implements ActionListener, ChangeListener { 291 private RecentSearches recentSearches; 292 293 private JTextField searchField; 294 295 private JMenuItem clear; 296 297 /** 298 * Creates a new popup menu based on the given {@link RecentSearches} 299 * and {@link JXSearchField}. 300 * 301 * @param recentSearches 302 * @param searchField 303 */ 304 public RecentSearchesPopup(RecentSearches recentSearches, JTextField searchField) { 305 this.searchField = searchField; 306 this.recentSearches = recentSearches; 307 308 recentSearches.addChangeListener(this); 309 buildMenu(); 310 } 311 312 /** 313 * Rebuilds the menu according to the recent searches. 314 */ 315 private void buildMenu() { 316 setVisible(false); 317 removeAll(); 318 319 if (recentSearches.getLength() == 0) { 320 JMenuItem noRecent = new JMenuItem(UIManagerExt.getString("SearchField.noRecentsText")); 321 noRecent.setEnabled(false); 322 add(noRecent); 323 } else { 324 JMenuItem recent = new JMenuItem(UIManagerExt.getString("SearchField.recentsMenuTitle")); 325 recent.setEnabled(false); 326 add(recent); 327 328 for (String searchString : recentSearches.getRecentSearches()) { 329 JMenuItem mi = new JMenuItem(searchString); 330 mi.addActionListener(this); 331 add(mi); 332 } 333 334 addSeparator(); 335 clear = new JMenuItem(UIManagerExt.getString("SearchField.clearRecentsText")); 336 clear.addActionListener(this); 337 add(clear); 338 } 339 } 340 341 /** 342 * Sets {@link #searchField}s text to the {@link ActionEvent}s action 343 * command and call {@link JXSearchField#postActionEvent()} to fire an 344 * {@link ActionEvent}, if <code>e</code>s source is not the clear 345 * menu item. If the source is the clear menu item, all recent searches 346 * will be removed. 347 */ 348 @Override 349 public void actionPerformed(ActionEvent e) { 350 if (e.getSource() == clear) { 351 recentSearches.removeAll(); 352 } else { 353 searchField.setText(e.getActionCommand()); 354 searchField.postActionEvent(); 355 } 356 } 357 358 /** 359 * Every time the recent searches fires a {@link ChangeEvent} call 360 * {@link #buildMenu()} to rebuild the whole menu. 361 */ 362 @Override 363 public void stateChanged(ChangeEvent e) { 364 buildMenu(); 365 } 366 } 367}