JAL-3490 revised layout and search algorithm (more tests to be added)
[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.event.ActionEvent;
27 import java.awt.event.ActionListener;
28 import java.awt.event.KeyAdapter;
29 import java.awt.event.KeyEvent;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Collections;
33 import java.util.LinkedHashSet;
34 import java.util.List;
35 import java.util.Set;
36
37 import javax.swing.JComboBox;
38 import javax.swing.JMenuItem;
39 import javax.swing.JPopupMenu;
40 import javax.swing.SwingUtilities;
41
42 public class JvCacheableInputBox<E> extends JComboBox<String>
43 {
44
45   private static final long serialVersionUID = 5774610435079326695L;
46
47   private static final int LEFT_BOARDER_WIDTH = 16;
48
49   private String cacheKey;
50
51   private AppCache appCache;
52
53   private JPopupMenu popup = new JPopupMenu();
54
55   private JMenuItem menuItemClearCache = new JMenuItem();
56
57   volatile boolean enterWasPressed = false;
58
59   /**
60    * @return flag indicating if the most recent keypress was enter
61    */
62   public boolean wasEnterPressed()
63   {
64     return enterWasPressed;
65   }
66
67   /**
68    * Constructor given the key to cached values, and the (approximate) length in
69    * characters of the input field
70    * 
71    * @param newCacheKey
72    * @param length
73    */
74   public JvCacheableInputBox(String newCacheKey, int length)
75   {
76     super();
77     this.cacheKey = newCacheKey;
78     setEditable(true);
79     addKeyListener(new KeyAdapter()
80     {
81
82       @Override
83       public void keyTyped(KeyEvent e)
84       {
85         enterWasPressed = false;
86         if (e.getKeyCode() == KeyEvent.VK_ENTER)
87         {
88           enterWasPressed = true;
89         }
90         // let event bubble up
91       }
92     });
93     if (length > 0)
94     {
95       StringBuilder sb = new StringBuilder();
96       for (int i = 0; i < length; i++)
97       {
98         sb.append("X");
99       }
100       setPrototypeDisplayValue(sb.toString());
101     }
102     appCache = AppCache.getInstance();
103     initCachePopupMenu();
104     initCache(newCacheKey);
105     updateCache();
106   }
107
108   /**
109    * Method for initialising cache items for a given cache key and populating the
110    * in-memory cache with persisted cache items
111    * 
112    * @param cacheKey
113    */
114   private void initCache(String cacheKey)
115   {
116     // obtain persisted cache items from properties file as a delimited string
117     String delimitedCacheStr = Cache.getProperty(cacheKey);
118     if (delimitedCacheStr == null || delimitedCacheStr.isEmpty())
119     {
120       return;
121     }
122     // convert delimited cache items to a list of strings
123     List<String> persistedCacheItems = Arrays
124             .asList(delimitedCacheStr.split(AppCache.CACHE_DELIMITER));
125
126     LinkedHashSet<String> foundCacheItems = appCache
127             .getAllCachedItemsFor(cacheKey);
128     if (foundCacheItems == null)
129     {
130       foundCacheItems = new LinkedHashSet<>();
131     }
132     // populate memory cache
133     for (String cacheItem : persistedCacheItems)
134     {
135       foundCacheItems.add(cacheItem);
136     }
137     appCache.putCache(cacheKey, foundCacheItems);
138   }
139
140   /**
141    * Initialise this cache's pop-up menu
142    */
143   private void initCachePopupMenu()
144   {
145     menuItemClearCache.setFont(new java.awt.Font("Verdana", 0, 12));
146     menuItemClearCache
147             .setText(MessageManager.getString("action.clear_cached_items"));
148     menuItemClearCache.addActionListener(new ActionListener()
149     {
150       @Override
151       public void actionPerformed(ActionEvent e)
152       {
153         // System.out.println(">>>>> Clear cache items");
154         setSelectedItem("");
155         appCache.deleteCacheItems(cacheKey);
156         updateCache();
157       }
158     });
159
160     popup.add(menuItemClearCache);
161     setComponentPopupMenu(popup);
162     add(popup);
163   }
164
165   /**
166    * Answers true if input text is an integer
167    * 
168    * @param text
169    * @return
170    */
171   static boolean isInteger(String text)
172   {
173     try
174     {
175       Integer.parseInt(text);
176       return true;
177     } catch (NumberFormatException e)
178     {
179       return false;
180     }
181   }
182
183   /**
184    * Method called to update the cache with the last user input
185    */
186   public void updateCache()
187   {
188     SwingUtilities.invokeLater(new Runnable()
189     {
190       @Override
191       public void run()
192       {
193         int cacheLimit = Integer.parseInt(appCache.getCacheLimit(cacheKey));
194         String userInput = getUserInput();
195         if (userInput != null && !userInput.isEmpty())
196         {
197           LinkedHashSet<String> foundCache = appCache
198                   .getAllCachedItemsFor(cacheKey);
199           // remove old cache item so as to place current input at the top of
200           // the result
201           foundCache.remove(userInput);
202           foundCache.add(userInput);
203           appCache.putCache(cacheKey, foundCache);
204         }
205
206         String lastSearch = userInput;
207         if (getItemCount() > 0)
208         {
209           removeAllItems();
210         }
211         Set<String> cacheItems = appCache.getAllCachedItemsFor(cacheKey);
212         List<String> reversedCacheItems = new ArrayList<>();
213         reversedCacheItems.addAll(cacheItems);
214         cacheItems = null;
215         Collections.reverse(reversedCacheItems);
216         if (lastSearch.isEmpty())
217         {
218           addItem("");
219         }
220
221         if (reversedCacheItems != null && !reversedCacheItems.isEmpty())
222         {
223           LinkedHashSet<String> foundCache = appCache
224                   .getAllCachedItemsFor(cacheKey);
225           boolean prune = reversedCacheItems.size() > cacheLimit;
226           int count = 1;
227           boolean limitExceeded = false;
228           for (String cacheItem : reversedCacheItems)
229           {
230             limitExceeded = (count++ > cacheLimit);
231             if (prune)
232             {
233               if (limitExceeded)
234               {
235                 foundCache.remove(cacheItem);
236               }
237               else
238               {
239                 addItem(cacheItem);
240               }
241             }
242             else
243             {
244               addItem(cacheItem);
245             }
246           }
247           appCache.putCache(cacheKey, foundCache);
248         }
249         setSelectedItem(lastSearch.isEmpty() ? "" : lastSearch);
250       }
251     });
252   }
253
254   /**
255    * This method should be called to persist the in-memory cache when this
256    * components parent frame is closed / exited
257    */
258   public void persistCache()
259   {
260     appCache.persistCache(cacheKey);
261   }
262
263   /**
264    * Method to obtain input text from the cache box
265    * 
266    * @return
267    */
268   public String getUserInput()
269   {
270     return getEditor().getItem() == null ? ""
271             : getEditor().getItem().toString().trim();
272   }
273
274 }