b1b7ac5b376899b440da412940eb51809caaab75
[jalview.git] / src / jalview / io / cache / JvCacheableInputBox.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.io.cache;
22
23 import jalview.bin.Cache;
24 import jalview.bin.Jalview;
25 import jalview.util.MessageManager;
26
27 import java.awt.event.ActionEvent;
28 import java.awt.event.ActionListener;
29 import java.awt.event.FocusListener;
30 import java.awt.event.KeyAdapter;
31 import java.awt.event.KeyEvent;
32 import java.awt.event.KeyListener;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collections;
36 import java.util.LinkedHashSet;
37 import java.util.List;
38 import java.util.Set;
39
40 import javax.swing.JComboBox;
41 import javax.swing.JComponent;
42 import javax.swing.JMenuItem;
43 import javax.swing.JPopupMenu;
44 import javax.swing.JTextField;
45 import javax.swing.SwingUtilities;
46 import javax.swing.event.CaretListener;
47 import javax.swing.event.DocumentListener;
48 import javax.swing.text.JTextComponent;
49
50 /**
51  * A class that provides an editable combobox with a memory of previous entries
52  * that may be persisted
53  * 
54  * @author tcofoegbu
55  *
56  * @param <E>
57  */
58 /*
59  * (temporary?) patches to wrap a JTextField instead when running as Javascript
60  */
61 public class JvCacheableInputBox<E>
62 {
63   private JComboBox<String> comboBox; // used for Jalview
64
65   private JTextField textField; // used for JalviewJS
66
67   private static final long serialVersionUID = 5774610435079326695L;
68
69   private String cacheKey;
70
71   private AppCache appCache;
72
73   private JPopupMenu popup = new JPopupMenu();
74
75   private JMenuItem menuItemClearCache = new JMenuItem();
76
77   volatile boolean enterWasPressed = false;
78
79   /**
80    * @return flag indicating if the most recent keypress was enter
81    */
82   public boolean wasEnterPressed()
83   {
84     return enterWasPressed;
85   }
86
87   /**
88    * Constructor
89    * 
90    * @param newCacheKey
91    */
92   public JvCacheableInputBox(String newCacheKey)
93   {
94     super();
95     if (Jalview.isJS())
96     {
97       textField = new JTextField();
98       return;
99     }
100
101     this.cacheKey = newCacheKey;
102     comboBox = new JComboBox<String>();
103     comboBox.setEditable(true);
104     comboBox.addKeyListener(new KeyAdapter()
105     {
106       @Override
107       public void keyTyped(KeyEvent e)
108       {
109         enterWasPressed = false;
110         if (e.getKeyCode() == KeyEvent.VK_ENTER)
111         {
112           enterWasPressed = true;
113         }
114         // let event bubble up
115       }
116     });
117     comboBox.setPrototypeDisplayValue(
118             "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
119     appCache = AppCache.getInstance();
120     initCachePopupMenu();
121     initCache(newCacheKey);
122     updateCache();
123   }
124
125   /**
126    * Method for initialising cache items for a given cache key and populating the
127    * in-memory cache with persisted cache items
128    * 
129    * @param cacheKey
130    */
131   private void initCache(String cacheKey)
132   {
133     // obtain persisted cache items from properties file as a delimited string
134     String delimitedCacheStr = Cache.getProperty(cacheKey);
135     if (delimitedCacheStr == null || delimitedCacheStr.isEmpty())
136     {
137       return;
138     }
139     // convert delimited cache items to a list of strings
140     List<String> persistedCacheItems = Arrays
141             .asList(delimitedCacheStr.split(AppCache.CACHE_DELIMITER));
142
143     LinkedHashSet<String> foundCacheItems = appCache
144             .getAllCachedItemsFor(cacheKey);
145     if (foundCacheItems == null)
146     {
147       foundCacheItems = new LinkedHashSet<>();
148     }
149     // populate memory cache
150     for (String cacheItem : persistedCacheItems)
151     {
152       foundCacheItems.add(cacheItem);
153     }
154     appCache.putCache(cacheKey, foundCacheItems);
155   }
156
157   /**
158    * Initialise this cache's pop-up menu
159    */
160   private void initCachePopupMenu()
161   {
162     menuItemClearCache.setFont(new java.awt.Font("Verdana", 0, 12));
163     menuItemClearCache
164             .setText(MessageManager.getString("action.clear_cached_items"));
165     menuItemClearCache.addActionListener(new ActionListener()
166     {
167       @Override
168       public void actionPerformed(ActionEvent e)
169       {
170         // System.out.println(">>>>> Clear cache items");
171         setSelectedItem("");
172         appCache.deleteCacheItems(cacheKey);
173         updateCache();
174       }
175     });
176
177     popup.add(menuItemClearCache);
178     comboBox.setComponentPopupMenu(popup);
179     comboBox.add(popup);
180   }
181
182   /**
183    * Answers true if input text is an integer
184    * 
185    * @param text
186    * @return
187    */
188   static boolean isInteger(String text)
189   {
190     try
191     {
192       Integer.parseInt(text);
193       return true;
194     } catch (NumberFormatException e)
195     {
196       return false;
197     }
198   }
199
200   /**
201    * Method called to update the cache with the last user input
202    */
203   public void updateCache()
204   {
205     if (Jalview.isJS())
206     {
207       return;
208     }
209     SwingUtilities.invokeLater(new Runnable()
210     {
211       @Override
212       public void run()
213       {
214         int cacheLimit = Integer.parseInt(appCache.getCacheLimit(cacheKey));
215         String userInput = getUserInput();
216         if (userInput != null && !userInput.isEmpty())
217         {
218           LinkedHashSet<String> foundCache = appCache
219                   .getAllCachedItemsFor(cacheKey);
220           // remove old cache item so as to place current input at the top of
221           // the result
222           foundCache.remove(userInput);
223           foundCache.add(userInput);
224           appCache.putCache(cacheKey, foundCache);
225         }
226
227         String lastSearch = userInput;
228         if (comboBox.getItemCount() > 0)
229         {
230           comboBox.removeAllItems();
231         }
232         Set<String> cacheItems = appCache.getAllCachedItemsFor(cacheKey);
233         List<String> reversedCacheItems = new ArrayList<>();
234         reversedCacheItems.addAll(cacheItems);
235         cacheItems = null;
236         Collections.reverse(reversedCacheItems);
237         if (lastSearch.isEmpty())
238         {
239           comboBox.addItem("");
240         }
241
242         if (reversedCacheItems != null && !reversedCacheItems.isEmpty())
243         {
244           LinkedHashSet<String> foundCache = appCache
245                   .getAllCachedItemsFor(cacheKey);
246           boolean prune = reversedCacheItems.size() > cacheLimit;
247           int count = 1;
248           boolean limitExceeded = false;
249           for (String cacheItem : reversedCacheItems)
250           {
251             limitExceeded = (count++ > cacheLimit);
252             if (prune)
253             {
254               if (limitExceeded)
255               {
256                 foundCache.remove(cacheItem);
257               }
258               else
259               {
260                 comboBox.addItem(cacheItem);
261               }
262             }
263             else
264             {
265               comboBox.addItem(cacheItem);
266             }
267           }
268           appCache.putCache(cacheKey, foundCache);
269         }
270         setSelectedItem(lastSearch.isEmpty() ? "" : lastSearch);
271       }
272     });
273   }
274
275   /**
276    * This method should be called to persist the in-memory cache when this
277    * components parent frame is closed / exited
278    */
279   public void persistCache()
280   {
281     if (!Jalview.isJS())
282     {
283       appCache.persistCache(cacheKey);
284     }
285   }
286
287   /**
288    * Returns the trimmed text in the input field
289    * 
290    * @return
291    */
292   public String getUserInput()
293   {
294     if (Jalview.isJS())
295     {
296       return textField.getText().trim();
297     }
298     Object item = comboBox.getEditor().getItem();
299     return item == null ? "" : item.toString().trim();
300   }
301
302   public JComponent getComponent()
303   {
304     return Jalview.isJS() ? textField : comboBox;
305   }
306
307   public void addActionListener(ActionListener actionListener)
308   {
309     if (Jalview.isJS())
310     {
311       textField.addActionListener(actionListener);
312     }
313     else
314     {
315       comboBox.addActionListener(actionListener);
316     }
317   }
318
319   public void addDocumentListener(DocumentListener listener)
320   {
321     if (!Jalview.isJS())
322     {
323       ((JTextComponent) comboBox.getEditor().getEditorComponent())
324               .getDocument().addDocumentListener(listener);
325     }
326   }
327
328   public void addFocusListener(FocusListener focusListener)
329   {
330     if (Jalview.isJS())
331     {
332       textField.addFocusListener(focusListener);
333     }
334     else
335     {
336       comboBox.addFocusListener(focusListener);
337     }
338   }
339
340   public void addKeyListener(KeyListener kl)
341   {
342     if (!Jalview.isJS())
343     {
344       comboBox.getEditor().getEditorComponent().addKeyListener(kl);
345     }
346   }
347
348   public void setEditable(boolean b)
349   {
350     if (!Jalview.isJS())
351     {
352       comboBox.setEditable(b);
353     }
354   }
355
356   public void setPrototypeDisplayValue(String string)
357   {
358     if (!Jalview.isJS())
359     {
360       comboBox.setPrototypeDisplayValue(string);
361     }
362   }
363
364   public void setSelectedItem(String userInput)
365   {
366     if (!Jalview.isJS())
367     {
368       comboBox.setSelectedItem(userInput);
369     }
370   }
371
372   public boolean isPopupVisible()
373   {
374     if (!Jalview.isJS())
375     {
376       return comboBox.isPopupVisible();
377     }
378     return false;
379   }
380
381   public void addCaretListener(CaretListener caretListener)
382   {
383     if (!Jalview.isJS())
384     {
385       ((JTextComponent) comboBox.getEditor().getEditorComponent())
386               .addCaretListener(caretListener);
387     }
388   }
389
390   public void addItem(String item)
391   {
392     if (!Jalview.isJS())
393     {
394       comboBox.addItem(item);
395     }
396   }
397
398 }