JAL-2835 spike updated with latest
[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
26 import java.awt.Color;
27 import java.awt.Dimension;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ActionListener;
30 import java.awt.event.KeyEvent;
31 import java.awt.event.KeyListener;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Collections;
35 import java.util.LinkedHashSet;
36 import java.util.List;
37 import java.util.Set;
38
39 import javax.swing.BorderFactory;
40 import javax.swing.JComboBox;
41 import javax.swing.JLabel;
42 import javax.swing.JMenuItem;
43 import javax.swing.JPanel;
44 import javax.swing.JPopupMenu;
45 import javax.swing.JTextField;
46 import javax.swing.SwingUtilities;
47 import javax.swing.text.AttributeSet;
48 import javax.swing.text.BadLocationException;
49 import javax.swing.text.PlainDocument;
50
51 public class JvCacheableInputBox<E> extends JComboBox<String>
52 {
53
54   private static final long serialVersionUID = 5774610435079326695L;
55
56   private static final int INPUT_LIMIT = 2;
57
58   private static final int LEFT_BOARDER_WIDTH = 16;
59
60   private String cacheKey;
61
62   private AppCache appCache;
63
64   private JPanel pnlDefaultCache = new JPanel();
65
66   private JLabel lblDefaultCacheSize = new JLabel();
67
68   private JTextField txtDefaultCacheSize = new JTextField();
69
70   private JPopupMenu popup = new JPopupMenu();
71
72   private JMenuItem menuItemClearCache = new JMenuItem();
73
74   volatile boolean enterWasPressed = false;
75
76   /**
77    * @return flag indicating if the most recent keypress was enter
78    */
79   public boolean wasEnterPressed()
80   {
81     return enterWasPressed;
82   }
83
84   public JvCacheableInputBox(String newCacheKey)
85   {
86     super();
87     this.cacheKey = newCacheKey;
88     setEditable(true);
89     addKeyListener(new KeyListener()
90     {
91
92       @Override
93       public void keyTyped(KeyEvent e)
94       {
95         enterWasPressed = false;
96         if (e.getKeyCode() == KeyEvent.VK_ENTER)
97         {
98           enterWasPressed = true;
99         }
100         // let event bubble up
101       }
102
103       @Override
104       public void keyReleased(KeyEvent e)
105       {
106         // TODO Auto-generated method stub
107
108       }
109
110       @Override
111       public void keyPressed(KeyEvent e)
112       {
113         // TODO Auto-generated method stub
114
115       }
116     });
117     setPrototypeDisplayValue(
118             "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
119     appCache = AppCache.getInstance();
120     initCachePopupMenu();
121     initCache(newCacheKey);
122     updateCache();
123   }
124
125   /**
126    * Method for initialising cache items for a given cache key and populating
127    * the in-memory cache with persisted cache items
128    * 
129    * @param cacheKey
130    */
131   private void initCache(String cacheKey)
132   {
133     // obtain persisted cache items from properties file as a delimited string
134     String delimitedCacheStr = Cache.getProperty(cacheKey);
135     if (delimitedCacheStr == null || delimitedCacheStr.isEmpty())
136     {
137       return;
138     }
139     // convert delimited cache items to a list of strings
140     List<String> persistedCacheItems = Arrays
141             .asList(delimitedCacheStr.split(AppCache.CACHE_DELIMITER));
142
143     LinkedHashSet<String> foundCacheItems = appCache
144             .getAllCachedItemsFor(cacheKey);
145     if (foundCacheItems == null)
146     {
147       foundCacheItems = new LinkedHashSet<String>();
148     }
149     // populate memory cache
150     for (String cacheItem : persistedCacheItems)
151     {
152       foundCacheItems.add(cacheItem);
153     }
154     appCache.putCache(cacheKey, foundCacheItems);
155   }
156
157   /**
158    * Initialise this cache's pop-up menu
159    */
160   private void initCachePopupMenu()
161   {
162     pnlDefaultCache.setBackground(Color.WHITE);
163     // pad panel so as to align with other menu items
164     pnlDefaultCache.setBorder(
165             BorderFactory.createEmptyBorder(0, LEFT_BOARDER_WIDTH, 0, 0));
166     txtDefaultCacheSize.setPreferredSize(new Dimension(45, 20));
167     txtDefaultCacheSize.setFont(new java.awt.Font("Verdana", 0, 12));
168     lblDefaultCacheSize
169             .setText(MessageManager.getString("label.default_cache_size"));
170     lblDefaultCacheSize.setFont(new java.awt.Font("Verdana", 0, 12));
171     // Force input to accept only Integer entries up to length - INPUT_LIMIT
172     txtDefaultCacheSize.setDocument(new PlainDocument()
173     {
174       private static final long serialVersionUID = 1L;
175
176       @Override
177       public void insertString(int offs, String str, AttributeSet a)
178               throws BadLocationException
179       {
180         if (getLength() + str.length() <= INPUT_LIMIT && isInteger(str))
181         {
182           super.insertString(offs, str, a);
183         }
184       }
185     });
186     txtDefaultCacheSize.addKeyListener(new java.awt.event.KeyAdapter()
187     {
188       @Override
189       public void keyPressed(KeyEvent e)
190       {
191         if (e.getKeyCode() == KeyEvent.VK_ENTER)
192         {
193           e.consume();
194           updateCache();
195           closePopup();
196         }
197       }
198     });
199
200     txtDefaultCacheSize.setText(appCache.getCacheLimit(cacheKey));
201     pnlDefaultCache.add(lblDefaultCacheSize);
202     menuItemClearCache.setFont(new java.awt.Font("Verdana", 0, 12));
203     pnlDefaultCache.add(txtDefaultCacheSize);
204     menuItemClearCache
205             .setText(MessageManager.getString("action.clear_cached_items"));
206     menuItemClearCache.addActionListener(new ActionListener()
207     {
208       @Override
209       public void actionPerformed(ActionEvent e)
210       {
211         // System.out.println(">>>>> Clear cache items");
212         setSelectedItem("");
213         appCache.deleteCacheItems(cacheKey);
214         updateCache();
215       }
216     });
217
218     popup.insert(pnlDefaultCache, 0);
219     popup.add(menuItemClearCache);
220     setComponentPopupMenu(popup);
221     add(popup);
222   }
223
224   private void closePopup()
225   {
226     popup.setVisible(false);
227     popup.transferFocus();
228   }
229
230   /**
231    * Answers true if input text is an integer
232    * 
233    * @param text
234    * @return
235    */
236   static boolean isInteger(String text)
237   {
238     try
239     {
240       Integer.parseInt(text);
241       return true;
242     } catch (NumberFormatException e)
243     {
244       return false;
245     }
246   }
247
248   /**
249    * Method called to update the cache with the last user input
250    */
251   public void updateCache()
252   {
253     SwingUtilities.invokeLater(new Runnable()
254     {
255       @Override
256       public void run()
257       {
258         int userLimit = txtDefaultCacheSize.getText().trim().isEmpty()
259                 ? Integer.valueOf(AppCache.DEFAULT_LIMIT)
260                 : Integer.valueOf(txtDefaultCacheSize.getText());
261         int cacheLimit = appCache.updateCacheLimit(cacheKey, userLimit);
262         String userInput = getUserInput();
263         if (userInput != null && !userInput.isEmpty())
264         {
265           LinkedHashSet<String> foundCache = appCache
266                   .getAllCachedItemsFor(cacheKey);
267           // remove old cache item so as to place current input at the top of
268           // the result
269           foundCache.remove(userInput);
270           foundCache.add(userInput);
271           appCache.putCache(cacheKey, foundCache);
272         }
273
274         String lastSearch = userInput;
275         if (getItemCount() > 0)
276         {
277           removeAllItems();
278         }
279         Set<String> cacheItems = appCache.getAllCachedItemsFor(cacheKey);
280         List<String> reversedCacheItems = new ArrayList<String>();
281         reversedCacheItems.addAll(cacheItems);
282         cacheItems = null;
283         Collections.reverse(reversedCacheItems);
284         if (lastSearch.isEmpty())
285         {
286           addItem("");
287         }
288
289         if (reversedCacheItems != null && !reversedCacheItems.isEmpty())
290         {
291           LinkedHashSet<String> foundCache = appCache
292                   .getAllCachedItemsFor(cacheKey);
293           boolean prune = reversedCacheItems.size() > cacheLimit;
294           int count = 1;
295           boolean limitExceeded = false;
296           for (String cacheItem : reversedCacheItems)
297           {
298             limitExceeded = (count++ > cacheLimit);
299             if (prune)
300             {
301               if (limitExceeded)
302               {
303                 foundCache.remove(cacheItem);
304               }
305               else
306               {
307                 addItem(cacheItem);
308               }
309             }
310             else
311             {
312               addItem(cacheItem);
313             }
314           }
315           appCache.putCache(cacheKey, foundCache);
316         }
317         setSelectedItem(lastSearch.isEmpty() ? "" : lastSearch);
318       }
319     });
320   }
321
322   /**
323    * This method should be called to persist the in-memory cache when this
324    * components parent frame is closed / exited
325    */
326   public void persistCache()
327   {
328     appCache.persistCache(cacheKey);
329     int userLimit = txtDefaultCacheSize.getText().trim().isEmpty()
330             ? Integer.valueOf(AppCache.DEFAULT_LIMIT)
331             : Integer.valueOf(txtDefaultCacheSize.getText());
332     appCache.updateCacheLimit(cacheKey, userLimit);
333   }
334
335   /**
336    * Method to obtain input text from the cache box
337    * 
338    * @return
339    */
340   public String getUserInput()
341   {
342     return getEditor().getItem() == null ? ""
343             : getEditor().getItem().toString().trim();
344   }
345
346 }