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