JAL-3490 merged with 2.11.2 develop
[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     }
149     appCache = AppCache.getInstance();
150     initCachePopupMenu();
151     initCache(newCacheKey);
152     updateCache();
153   }
154
155   /**
156    * Method for initialising cache items for a given cache key and populating
157    * the in-memory cache with persisted cache items
158    * 
159    * @param cacheKey
160    */
161   private void initCache(String cacheKey)
162   {
163     if (appCache == null)
164     {
165       return;
166     }
167     // obtain persisted cache items from properties file as a delimited string
168     String delimitedCacheStr = Cache.getProperty(cacheKey);
169     if (delimitedCacheStr == null || delimitedCacheStr.isEmpty())
170     {
171       return;
172     }
173     // convert delimited cache items to a list of strings
174     List<String> persistedCacheItems = Arrays
175             .asList(delimitedCacheStr.split(AppCache.CACHE_DELIMITER));
176
177     LinkedHashSet<String> foundCacheItems = appCache
178             .getAllCachedItemsFor(cacheKey);
179     if (foundCacheItems == null)
180     {
181       foundCacheItems = new LinkedHashSet<>();
182     }
183     // populate memory cache
184     for (String cacheItem : persistedCacheItems)
185     {
186       foundCacheItems.add(cacheItem);
187     }
188     appCache.putCache(cacheKey, foundCacheItems);
189   }
190
191   /**
192    * Initialise this cache's pop-up menu
193    */
194   private void initCachePopupMenu()
195   {
196     if (appCache == null)
197     {
198       return;
199     }
200     menuItemClearCache.setFont(new java.awt.Font("Verdana", 0, 12));
201     menuItemClearCache
202             .setText(MessageManager.getString("action.clear_cached_items"));
203     menuItemClearCache.addActionListener(new ActionListener()
204     {
205       @Override
206       public void actionPerformed(ActionEvent e)
207       {
208         // System.out.println(">>>>> Clear cache items");
209         setSelectedItem("");
210         appCache.deleteCacheItems(cacheKey);
211         updateCache();
212       }
213     });
214
215     popup.add(menuItemClearCache);
216     comboBox.setComponentPopupMenu(popup);
217     comboBox.add(popup);
218   }
219
220   /**
221    * Answers true if input text is an integer
222    * 
223    * @param text
224    * @return
225    */
226   static boolean isInteger(String text)
227   {
228     try
229     {
230       Integer.parseInt(text);
231       return true;
232     } catch (NumberFormatException e)
233     {
234       return false;
235     }
236   }
237
238   /**
239    * Method called to update the cache with the last user input
240    */
241   public void updateCache()
242   {
243     if (appCache == null)
244     {
245       return;
246     }
247     SwingUtilities.invokeLater(new Runnable()
248     {
249       @Override
250       public void run()
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   /**
314    * This method should be called to persist the in-memory cache when this
315    * components parent frame is closed / exited
316    */
317   public void persistCache()
318   {
319     if (appCache == null)
320     {
321       return;
322     }
323     appCache.persistCache(cacheKey);
324   }
325
326   /**
327    * Returns the trimmed text in the input field
328    * 
329    * @return
330    */
331   public String getUserInput()
332   {
333     if (comboBox == null)
334     {
335       return textField.getText().trim();
336     }
337     Object item = comboBox.getEditor().getItem();
338     return item == null ? "" : item.toString().trim();
339   }
340
341   public JComponent getComponent()
342   {
343     return (comboBox == null ? textField : comboBox);
344   }
345
346   public void addActionListener(ActionListener actionListener)
347   {
348     if (comboBox == null)
349     {
350       textField.addActionListener(actionListener);
351     }
352     else
353     {
354       comboBox.addActionListener(actionListener);
355     }
356   }
357
358   public void addDocumentListener(DocumentListener listener)
359   {
360     textComponent.getDocument().addDocumentListener(listener);
361   }
362
363   public void addFocusListener(FocusListener focusListener)
364   {
365     textComponent.addFocusListener(focusListener);
366   }
367
368   public void addKeyListener(KeyListener kl)
369   {
370     textComponent.addKeyListener(kl);
371   }
372
373   public void addCaretListener(CaretListener caretListener)
374   {
375     textComponent.addCaretListener(caretListener);
376   }
377
378   public void setEditable(boolean b)
379   {
380     if (comboBox != null)
381     {
382       comboBox.setEditable(b);
383     }
384   }
385
386   public void setPrototypeDisplayValue(String string)
387   {
388     prototypeDisplayValue = string;
389     if (comboBox != null)
390     {
391       comboBox.setPrototypeDisplayValue(string);
392     }
393   }
394
395   public void setSelectedItem(String userInput)
396   {
397     if (comboBox != null)
398     {
399       comboBox.setSelectedItem(userInput);
400     }
401   }
402
403   public boolean isPopupVisible()
404   {
405     return (comboBox != null && comboBox.isPopupVisible());
406   }
407
408   public void addItem(String item)
409   {
410     if (comboBox != null)
411     {
412       comboBox.addItem(item);
413     }
414   }
415
416 }