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