e4e2d99e3e8d4dbcc8442eab96c229c7cb7d89d1
[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 given the key to cached values, and the (approximate) length in
91    * characters of the input field
92    * 
93    * @param newCacheKey
94    * @param length
95    */
96   public JvCacheableInputBox(String newCacheKey, int length)
97   {
98     // super();
99     cacheKey = newCacheKey;
100     prototypeDisplayValue = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
101     if (length > 0)
102     {
103       StringBuilder sb = new StringBuilder();
104       for (int i = 0; i < length; i++)
105       {
106         sb.append("X");
107       }
108       setPrototypeDisplayValue(sb.toString());
109     }
110     boolean useTextField = Platform.isJS();
111     // BH 2019.03 only switch for JavaScript here
112     // SwingJS TODO implement editable combo box
113     if (useTextField)
114     {
115       appCache = null;
116       textComponent = textField = new JTextField();
117       // {
118       // @Override
119       // public Dimension getPreferredSize() {
120       // return super.getPreferredSize();
121       //// FontMetrics fm = getFontMetrics(getFont());
122       //// return new Dimension(fm.stringWidth(prototypeDisplayValue),
123       // fm.getHeight());
124       // }
125       // };
126     }
127     else
128     {
129       appCache = AppCache.getInstance();
130       comboBox = new JComboBox<>();
131       textComponent = (JTextComponent) comboBox.getEditor()
132               .getEditorComponent();
133       comboBox.setEditable(true);
134       comboBox.addKeyListener(new KeyAdapter()
135       {
136         @Override
137         public void keyTyped(KeyEvent e)
138         {
139           enterWasPressed = false;
140           if (e.getKeyCode() == KeyEvent.VK_ENTER)
141           {
142             enterWasPressed = true;
143           }
144           // let event bubble up
145         }
146       });
147       comboBox.setPrototypeDisplayValue(prototypeDisplayValue);
148       initCachePopupMenu();
149       initCache(newCacheKey);
150       updateCache();
151     }
152   }
153
154   /**
155    * Method for initialising cache items for a given cache key and populating
156    * the in-memory cache with persisted cache items
157    * 
158    * @param cacheKey
159    */
160   private void initCache(String cacheKey)
161   {
162     if (appCache == null)
163     {
164       return;
165     }
166     // obtain persisted cache items from properties file as a delimited string
167     String delimitedCacheStr = Cache.getProperty(cacheKey);
168     if (delimitedCacheStr == null || delimitedCacheStr.isEmpty())
169     {
170       return;
171     }
172     // convert delimited cache items to a list of strings
173     List<String> persistedCacheItems = Arrays
174             .asList(delimitedCacheStr.split(AppCache.CACHE_DELIMITER));
175
176     LinkedHashSet<String> foundCacheItems = appCache
177             .getAllCachedItemsFor(cacheKey);
178     if (foundCacheItems == null)
179     {
180       foundCacheItems = new LinkedHashSet<>();
181     }
182     // populate memory cache
183     for (String cacheItem : persistedCacheItems)
184     {
185       foundCacheItems.add(cacheItem);
186     }
187     appCache.putCache(cacheKey, foundCacheItems);
188   }
189
190   /**
191    * Initialise this cache's pop-up menu
192    */
193   private void initCachePopupMenu()
194   {
195     if (appCache == null)
196     {
197       return;
198     }
199     menuItemClearCache.setFont(new java.awt.Font("Verdana", 0, 12));
200     menuItemClearCache
201             .setText(MessageManager.getString("action.clear_cached_items"));
202     menuItemClearCache.addActionListener(new ActionListener()
203     {
204       @Override
205       public void actionPerformed(ActionEvent e)
206       {
207         // jalview.bin.Console.outPrintln(">>>>> Clear cache items");
208         setSelectedItem("");
209         appCache.deleteCacheItems(cacheKey);
210         updateCache();
211       }
212     });
213
214     popup.add(menuItemClearCache);
215     comboBox.setComponentPopupMenu(popup);
216     comboBox.add(popup);
217   }
218
219   /**
220    * Answers true if input text is an integer
221    * 
222    * @param text
223    * @return
224    */
225   static boolean isInteger(String text)
226   {
227     try
228     {
229       Integer.parseInt(text);
230       return true;
231     } catch (NumberFormatException e)
232     {
233       return false;
234     }
235   }
236
237   /**
238    * Method called to update the cache with the last user input
239    */
240   public void updateCache()
241   {
242     if (appCache == null)
243     {
244       return;
245     }
246     SwingUtilities.invokeLater(new Runnable()
247     {
248       @Override
249       public void run()
250       {
251         int cacheLimit = Integer.parseInt(appCache.getCacheLimit(cacheKey));
252         String userInput = getUserInput();
253         if (userInput != null && !userInput.isEmpty())
254         {
255           LinkedHashSet<String> foundCache = appCache
256                   .getAllCachedItemsFor(cacheKey);
257           // remove old cache item so as to place current input at the top of
258           // the result
259           foundCache.remove(userInput);
260           foundCache.add(userInput);
261           appCache.putCache(cacheKey, foundCache);
262         }
263
264         String lastSearch = userInput;
265         if (comboBox.getItemCount() > 0)
266         {
267           comboBox.removeAllItems();
268         }
269         Set<String> cacheItems = appCache.getAllCachedItemsFor(cacheKey);
270         List<String> reversedCacheItems = new ArrayList<>();
271         reversedCacheItems.addAll(cacheItems);
272         cacheItems = null;
273         Collections.reverse(reversedCacheItems);
274         if (lastSearch.isEmpty())
275         {
276           comboBox.addItem("");
277         }
278
279         if (reversedCacheItems != null && !reversedCacheItems.isEmpty())
280         {
281           LinkedHashSet<String> foundCache = appCache
282                   .getAllCachedItemsFor(cacheKey);
283           boolean prune = reversedCacheItems.size() > cacheLimit;
284           int count = 1;
285           boolean limitExceeded = false;
286           for (String cacheItem : reversedCacheItems)
287           {
288             limitExceeded = (count++ > cacheLimit);
289             if (prune)
290             {
291               if (limitExceeded)
292               {
293                 foundCache.remove(cacheItem);
294               }
295               else
296               {
297                 comboBox.addItem(cacheItem);
298               }
299             }
300             else
301             {
302               comboBox.addItem(cacheItem);
303             }
304           }
305           appCache.putCache(cacheKey, foundCache);
306         }
307         setSelectedItem(lastSearch.isEmpty() ? "" : lastSearch);
308       }
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 }