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