e02a396cf19ebdaa8914a09fd4131a23b7f3ee83
[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.util.MessageManager;
25 import jalview.util.Platform;
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   protected JComboBox<String> comboBox; // used for Jalview
64
65   protected JTextField textField; // used for JalviewJS
66
67   protected JTextComponent textComponent; // used for both
68
69   protected String cacheKey;
70
71   protected AppCache appCache;
72
73   private JPopupMenu popup = new JPopupMenu();
74
75   private JMenuItem menuItemClearCache = new JMenuItem();
76
77   volatile boolean enterWasPressed = false;
78
79   private String prototypeDisplayValue;
80
81   /**
82    * @return flag indicating if the most recent keypress was enter
83    */
84   public boolean wasEnterPressed()
85   {
86     return enterWasPressed;
87   }
88
89   /**
90    * Constructor
91    * 
92    * @param newCacheKey
93    */
94   public JvCacheableInputBox(String newCacheKey)
95   {
96     // super();
97     cacheKey = newCacheKey;
98     prototypeDisplayValue = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
99     boolean useTextField = Platform.isJS();
100     // BH 2019.03 only switch for JavaScript here
101     // SwingJS TODO implement editable combo box
102     if (useTextField)
103     {
104       appCache = null;
105       textComponent = textField = new JTextField();
106       // {
107       // @Override
108       // public Dimension getPreferredSize() {
109       // return super.getPreferredSize();
110       //// FontMetrics fm = getFontMetrics(getFont());
111       //// return new Dimension(fm.stringWidth(prototypeDisplayValue),
112       // fm.getHeight());
113       // }
114       // };
115     }
116     else
117     {
118       appCache = AppCache.getInstance();
119       comboBox = new JComboBox<>();
120       textComponent = (JTextComponent) comboBox.getEditor()
121               .getEditorComponent();
122       comboBox.setEditable(true);
123       comboBox.addKeyListener(new KeyAdapter()
124       {
125         @Override
126         public void keyTyped(KeyEvent e)
127         {
128           enterWasPressed = false;
129           if (e.getKeyCode() == KeyEvent.VK_ENTER)
130           {
131             enterWasPressed = true;
132           }
133           // let event bubble up
134         }
135       });
136       comboBox.setPrototypeDisplayValue(prototypeDisplayValue);
137     }
138     initCachePopupMenu();
139     initCache(newCacheKey);
140     updateCache();
141   }
142
143   /**
144    * Method for initialising cache items for a given cache key and populating
145    * the in-memory cache with persisted cache items
146    * 
147    * @param cacheKey
148    */
149   private void initCache(String cacheKey)
150   {
151     if (appCache == null)
152     {
153       return;
154     }
155     // obtain persisted cache items from properties file as a delimited string
156     String delimitedCacheStr = Cache.getProperty(cacheKey);
157     if (delimitedCacheStr == null || delimitedCacheStr.isEmpty())
158     {
159       return;
160     }
161     // convert delimited cache items to a list of strings
162     List<String> persistedCacheItems = Arrays
163             .asList(delimitedCacheStr.split(AppCache.CACHE_DELIMITER));
164
165     LinkedHashSet<String> foundCacheItems = appCache
166             .getAllCachedItemsFor(cacheKey);
167     if (foundCacheItems == null)
168     {
169       foundCacheItems = new LinkedHashSet<>();
170     }
171     // populate memory cache
172     for (String cacheItem : persistedCacheItems)
173     {
174       foundCacheItems.add(cacheItem);
175     }
176     appCache.putCache(cacheKey, foundCacheItems);
177   }
178
179   /**
180    * Initialise this cache's pop-up menu
181    */
182   private void initCachePopupMenu()
183   {
184     if (appCache == null)
185     {
186       return;
187     }
188     menuItemClearCache.setFont(new java.awt.Font("Verdana", 0, 12));
189     menuItemClearCache
190             .setText(MessageManager.getString("action.clear_cached_items"));
191     menuItemClearCache.addActionListener(new ActionListener()
192     {
193       @Override
194       public void actionPerformed(ActionEvent e)
195       {
196         // System.out.println(">>>>> Clear cache items");
197         setSelectedItem("");
198         appCache.deleteCacheItems(cacheKey);
199         updateCache();
200       }
201     });
202
203     popup.add(menuItemClearCache);
204     comboBox.setComponentPopupMenu(popup);
205     comboBox.add(popup);
206   }
207
208   /**
209    * Answers true if input text is an integer
210    * 
211    * @param text
212    * @return
213    */
214   static boolean isInteger(String text)
215   {
216     try
217     {
218       Integer.parseInt(text);
219       return true;
220     } catch (NumberFormatException e)
221     {
222       return false;
223     }
224   }
225
226   /**
227    * Method called to update the cache with the last user input
228    */
229   public void updateCache()
230   {
231     if (appCache == null)
232     {
233       return;
234     }
235     SwingUtilities.invokeLater(new Runnable()
236     {
237       @Override
238       public void run()
239       {
240         updateCacheNow();
241       }
242     });
243   }
244
245   /**
246    * For TestNG
247    * 
248    * @author Bob Hanson 2019.08.28
249    */
250   public void updateCacheNow()
251   {
252     int cacheLimit = Integer.parseInt(appCache.getCacheLimit(cacheKey));
253     String userInput = getUserInput();
254     if (userInput != null && !userInput.isEmpty())
255     {
256       LinkedHashSet<String> foundCache = appCache
257               .getAllCachedItemsFor(cacheKey);
258       // remove old cache item so as to place current input at the top of
259       // the result
260       foundCache.remove(userInput);
261       foundCache.add(userInput);
262       appCache.putCache(cacheKey, foundCache);
263     }
264
265     String lastSearch = userInput;
266     if (comboBox.getItemCount() > 0)
267     {
268       comboBox.removeAllItems();
269     }
270     Set<String> cacheItems = appCache.getAllCachedItemsFor(cacheKey);
271     List<String> reversedCacheItems = new ArrayList<>();
272     reversedCacheItems.addAll(cacheItems);
273     cacheItems = null;
274     Collections.reverse(reversedCacheItems);
275     if (lastSearch.isEmpty())
276     {
277       comboBox.addItem("");
278     }
279
280     if (reversedCacheItems != null && !reversedCacheItems.isEmpty())
281     {
282       LinkedHashSet<String> foundCache = appCache
283               .getAllCachedItemsFor(cacheKey);
284       boolean prune = reversedCacheItems.size() > cacheLimit;
285       int count = 1;
286       boolean limitExceeded = false;
287       for (String cacheItem : reversedCacheItems)
288       {
289         limitExceeded = (count++ > cacheLimit);
290         if (prune)
291         {
292           if (limitExceeded)
293           {
294             foundCache.remove(cacheItem);
295           }
296           else
297           {
298             comboBox.addItem(cacheItem);
299           }
300         }
301         else
302         {
303           comboBox.addItem(cacheItem);
304         }
305       }
306       appCache.putCache(cacheKey, foundCache);
307     }
308     setSelectedItem(lastSearch.isEmpty() ? "" : lastSearch);
309
310   }
311
312   /**
313    * This method should be called to persist the in-memory cache when this
314    * components parent frame is closed / exited
315    */
316   public void persistCache()
317   {
318     if (appCache == null)
319     {
320       return;
321     }
322     appCache.persistCache(cacheKey);
323   }
324
325   /**
326    * Returns the trimmed text in the input field
327    * 
328    * @return
329    */
330   public String getUserInput()
331   {
332     if (comboBox == null)
333     {
334       return textField.getText().trim();
335     }
336     Object item = comboBox.getEditor().getItem();
337     return item == null ? "" : item.toString().trim();
338   }
339
340   public JComponent getComponent()
341   {
342     return (comboBox == null ? textField : comboBox);
343   }
344
345   public void addActionListener(ActionListener actionListener)
346   {
347     if (comboBox == null)
348     {
349       textField.addActionListener(actionListener);
350     }
351     else
352     {
353       comboBox.addActionListener(actionListener);
354     }
355   }
356
357   public void addDocumentListener(DocumentListener listener)
358   {
359     textComponent.getDocument().addDocumentListener(listener);
360   }
361
362   public void addFocusListener(FocusListener focusListener)
363   {
364     textComponent.addFocusListener(focusListener);
365   }
366
367   public void addKeyListener(KeyListener kl)
368   {
369     textComponent.addKeyListener(kl);
370   }
371
372   public void addCaretListener(CaretListener caretListener)
373   {
374     textComponent.addCaretListener(caretListener);
375   }
376
377   public void setEditable(boolean b)
378   {
379     if (comboBox != null)
380     {
381       comboBox.setEditable(b);
382     }
383   }
384
385   public void setPrototypeDisplayValue(String string)
386   {
387     prototypeDisplayValue = string;
388     if (comboBox != null)
389     {
390       comboBox.setPrototypeDisplayValue(string);
391     }
392   }
393
394   public void setSelectedItem(String userInput)
395   {
396     if (comboBox != null)
397     {
398       comboBox.setSelectedItem(userInput);
399     }
400   }
401
402   public boolean isPopupVisible()
403   {
404     return (comboBox != null && comboBox.isPopupVisible());
405   }
406
407   public void addItem(String item)
408   {
409     if (comboBox != null)
410     {
411       comboBox.addItem(item);
412     }
413   }
414
415 }