JAL-3626 from JAL-3253-applet web page embedding
[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         int cacheLimit = Integer.parseInt(appCache.getCacheLimit(cacheKey));
241         String userInput = getUserInput();
242         if (userInput != null && !userInput.isEmpty())
243         {
244           LinkedHashSet<String> foundCache = appCache
245                   .getAllCachedItemsFor(cacheKey);
246           // remove old cache item so as to place current input at the top of
247           // the result
248           foundCache.remove(userInput);
249           foundCache.add(userInput);
250           appCache.putCache(cacheKey, foundCache);
251         }
252
253         String lastSearch = userInput;
254         if (comboBox.getItemCount() > 0)
255         {
256           comboBox.removeAllItems();
257         }
258         Set<String> cacheItems = appCache.getAllCachedItemsFor(cacheKey);
259         List<String> reversedCacheItems = new ArrayList<>();
260         reversedCacheItems.addAll(cacheItems);
261         cacheItems = null;
262         Collections.reverse(reversedCacheItems);
263         if (lastSearch.isEmpty())
264         {
265           comboBox.addItem("");
266         }
267
268         if (reversedCacheItems != null && !reversedCacheItems.isEmpty())
269         {
270           LinkedHashSet<String> foundCache = appCache
271                   .getAllCachedItemsFor(cacheKey);
272           boolean prune = reversedCacheItems.size() > cacheLimit;
273           int count = 1;
274           boolean limitExceeded = false;
275           for (String cacheItem : reversedCacheItems)
276           {
277             limitExceeded = (count++ > cacheLimit);
278             if (prune)
279             {
280               if (limitExceeded)
281               {
282                 foundCache.remove(cacheItem);
283               }
284               else
285               {
286                 comboBox.addItem(cacheItem);
287               }
288             }
289             else
290             {
291               comboBox.addItem(cacheItem);
292             }
293           }
294           appCache.putCache(cacheKey, foundCache);
295         }
296         setSelectedItem(lastSearch.isEmpty() ? "" : lastSearch);
297       }
298     });
299   }
300
301   /**
302    * This method should be called to persist the in-memory cache when this
303    * components parent frame is closed / exited
304    */
305   public void persistCache()
306   {
307     if (appCache == null)
308     {
309       return;
310     }
311     appCache.persistCache(cacheKey);
312   }
313
314   /**
315    * Returns the trimmed text in the input field
316    * 
317    * @return
318    */
319   public String getUserInput()
320   {
321     if (comboBox == null)
322     {
323       return textField.getText().trim();
324     }
325     Object item = comboBox.getEditor().getItem();
326     return item == null ? "" : item.toString().trim();
327   }
328
329   public JComponent getComponent()
330   {
331     return (comboBox == null ? textField : comboBox);
332   }
333
334   public void addActionListener(ActionListener actionListener)
335   {
336     if (comboBox == null)
337     {
338       textField.addActionListener(actionListener);
339     }
340     else
341     {
342       comboBox.addActionListener(actionListener);
343     }
344   }
345
346   public void addDocumentListener(DocumentListener listener)
347   {
348     textComponent.getDocument().addDocumentListener(listener);
349   }
350
351   public void addFocusListener(FocusListener focusListener)
352   {
353     textComponent.addFocusListener(focusListener);
354   }
355
356   public void addKeyListener(KeyListener kl)
357   {
358     textComponent.addKeyListener(kl);
359   }
360
361   public void addCaretListener(CaretListener caretListener)
362   {
363     textComponent.addCaretListener(caretListener);
364   }
365
366   public void setEditable(boolean b)
367   {
368     if (comboBox != null)
369     {
370       comboBox.setEditable(b);
371     }
372   }
373
374   public void setPrototypeDisplayValue(String string)
375   {
376     prototypeDisplayValue = string;
377     if (comboBox != null)
378     {
379       comboBox.setPrototypeDisplayValue(string);
380     }
381   }
382
383   public void setSelectedItem(String userInput)
384   {
385     if (comboBox != null)
386     {
387       comboBox.setSelectedItem(userInput);
388     }
389   }
390
391   public boolean isPopupVisible()
392   {
393     return (comboBox != null && comboBox.isPopupVisible());
394   }
395
396   public void addItem(String item)
397   {
398     if (comboBox != null)
399     {
400       comboBox.addItem(item);
401     }
402   }
403
404 }