Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
JvCacheableInputBox | 63 | 112 | 51 |
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 | 0 | public boolean wasEnterPressed() |
87 | { | |
88 | 0 | 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 | 8 | public JvCacheableInputBox(String newCacheKey, int length) |
99 | { | |
100 | // super(); | |
101 | 8 | cacheKey = newCacheKey; |
102 | 8 | prototypeDisplayValue = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; |
103 | 8 | if (length > 0) |
104 | { | |
105 | 8 | StringBuilder sb = new StringBuilder(); |
106 | 224 | for (int i = 0; i < length; i++) |
107 | { | |
108 | 216 | sb.append("X"); |
109 | } | |
110 | 8 | setPrototypeDisplayValue(sb.toString()); |
111 | } | |
112 | 8 | boolean useTextField = Platform.isJS(); |
113 | // BH 2019.03 only switch for JavaScript here | |
114 | // SwingJS TODO implement editable combo box | |
115 | 8 | if (useTextField) |
116 | { | |
117 | 0 | appCache = null; |
118 | 0 | textComponent = textField = new JTextField(); |
119 | 0 | FontMetrics fm = textField.getFontMetrics(textField.getFont()); |
120 | 0 | 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 | 8 | appCache = AppCache.getInstance(); |
135 | 8 | comboBox = new JComboBox<>(); |
136 | 8 | textComponent = (JTextComponent) comboBox.getEditor() |
137 | .getEditorComponent(); | |
138 | 8 | comboBox.setEditable(true); |
139 | 8 | comboBox.addKeyListener(new KeyAdapter() |
140 | { | |
141 | 0 | @Override |
142 | public void keyTyped(KeyEvent e) | |
143 | { | |
144 | 0 | enterWasPressed = false; |
145 | 0 | if (e.getKeyCode() == KeyEvent.VK_ENTER) |
146 | { | |
147 | 0 | enterWasPressed = true; |
148 | } | |
149 | // let event bubble up | |
150 | } | |
151 | }); | |
152 | 8 | comboBox.setPrototypeDisplayValue(prototypeDisplayValue); |
153 | 8 | initCachePopupMenu(); |
154 | 8 | initCache(newCacheKey); |
155 | 8 | 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 | 8 | private void initCache(String cacheKey) |
166 | { | |
167 | 8 | if (appCache == null) |
168 | { | |
169 | 0 | return; |
170 | } | |
171 | // obtain persisted cache items from properties file as a delimited string | |
172 | 8 | String delimitedCacheStr = Cache.getProperty(cacheKey); |
173 | 8 | if (delimitedCacheStr == null || delimitedCacheStr.isEmpty()) |
174 | { | |
175 | 8 | return; |
176 | } | |
177 | // convert delimited cache items to a list of strings | |
178 | 0 | List<String> persistedCacheItems = Arrays |
179 | .asList(delimitedCacheStr.split(AppCache.CACHE_DELIMITER)); | |
180 | ||
181 | 0 | LinkedHashSet<String> foundCacheItems = appCache |
182 | .getAllCachedItemsFor(cacheKey); | |
183 | 0 | if (foundCacheItems == null) |
184 | { | |
185 | 0 | foundCacheItems = new LinkedHashSet<>(); |
186 | } | |
187 | // populate memory cache | |
188 | 0 | for (String cacheItem : persistedCacheItems) |
189 | { | |
190 | 0 | foundCacheItems.add(cacheItem); |
191 | } | |
192 | 0 | appCache.putCache(cacheKey, foundCacheItems); |
193 | } | |
194 | ||
195 | /** | |
196 | * Initialise this cache's pop-up menu | |
197 | */ | |
198 | 8 | private void initCachePopupMenu() |
199 | { | |
200 | 8 | if (appCache == null) |
201 | { | |
202 | 0 | return; |
203 | } | |
204 | 8 | menuItemClearCache.setFont(new java.awt.Font("Verdana", 0, 12)); |
205 | 8 | menuItemClearCache |
206 | .setText(MessageManager.getString("action.clear_cached_items")); | |
207 | 8 | menuItemClearCache.addActionListener(new ActionListener() |
208 | { | |
209 | 0 | @Override |
210 | public void actionPerformed(ActionEvent e) | |
211 | { | |
212 | // jalview.bin.Console.outPrintln(">>>>> Clear cache items"); | |
213 | 0 | setSelectedItem(""); |
214 | 0 | appCache.deleteCacheItems(cacheKey); |
215 | 0 | updateCache(); |
216 | } | |
217 | }); | |
218 | ||
219 | 8 | popup.add(menuItemClearCache); |
220 | 8 | comboBox.setComponentPopupMenu(popup); |
221 | 8 | comboBox.add(popup); |
222 | } | |
223 | ||
224 | /** | |
225 | * Answers true if input text is an integer | |
226 | * | |
227 | * @param text | |
228 | * @return | |
229 | */ | |
230 | 0 | static boolean isInteger(String text) |
231 | { | |
232 | 0 | try |
233 | { | |
234 | 0 | Integer.parseInt(text); |
235 | 0 | return true; |
236 | } catch (NumberFormatException e) | |
237 | { | |
238 | 0 | return false; |
239 | } | |
240 | } | |
241 | ||
242 | /** | |
243 | * Method called to update the cache with the last user input | |
244 | */ | |
245 | 11 | public void updateCache() |
246 | { | |
247 | 11 | if (appCache == null) |
248 | { | |
249 | 0 | return; |
250 | } | |
251 | 11 | SwingUtilities.invokeLater(new Runnable() |
252 | { | |
253 | 11 | @Override |
254 | public void run() | |
255 | { | |
256 | 11 | int cacheLimit = Integer.parseInt(appCache.getCacheLimit(cacheKey)); |
257 | 11 | String userInput = getUserInput(); |
258 | 11 | if (userInput != null && !userInput.isEmpty()) |
259 | { | |
260 | 1 | 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 | 1 | foundCache.remove(userInput); |
265 | 1 | foundCache.add(userInput); |
266 | 1 | appCache.putCache(cacheKey, foundCache); |
267 | } | |
268 | ||
269 | 11 | String lastSearch = userInput; |
270 | 11 | if (comboBox.getItemCount() > 0) |
271 | { | |
272 | 3 | comboBox.removeAllItems(); |
273 | } | |
274 | 11 | Set<String> cacheItems = appCache.getAllCachedItemsFor(cacheKey); |
275 | 11 | List<String> reversedCacheItems = new ArrayList<>(); |
276 | 11 | reversedCacheItems.addAll(cacheItems); |
277 | 11 | cacheItems = null; |
278 | 11 | Collections.reverse(reversedCacheItems); |
279 | 11 | if (lastSearch.isEmpty()) |
280 | { | |
281 | 10 | comboBox.addItem(""); |
282 | } | |
283 | ||
284 | 11 | if (reversedCacheItems != null && !reversedCacheItems.isEmpty()) |
285 | { | |
286 | 1 | LinkedHashSet<String> foundCache = appCache |
287 | .getAllCachedItemsFor(cacheKey); | |
288 | 1 | boolean prune = reversedCacheItems.size() > cacheLimit; |
289 | 1 | int count = 1; |
290 | 1 | boolean limitExceeded = false; |
291 | 1 | for (String cacheItem : reversedCacheItems) |
292 | { | |
293 | 1 | limitExceeded = (count++ > cacheLimit); |
294 | 1 | if (prune) |
295 | { | |
296 | 0 | if (limitExceeded) |
297 | { | |
298 | 0 | foundCache.remove(cacheItem); |
299 | } | |
300 | else | |
301 | { | |
302 | 0 | comboBox.addItem(cacheItem); |
303 | } | |
304 | } | |
305 | else | |
306 | { | |
307 | 1 | comboBox.addItem(cacheItem); |
308 | } | |
309 | } | |
310 | 1 | appCache.putCache(cacheKey, foundCache); |
311 | } | |
312 | 11 | 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 | 2 | public void persistCache() |
322 | { | |
323 | 2 | if (appCache == null) |
324 | { | |
325 | 0 | return; |
326 | } | |
327 | 2 | appCache.persistCache(cacheKey); |
328 | } | |
329 | ||
330 | /** | |
331 | * Returns the trimmed text in the input field | |
332 | * | |
333 | * @return | |
334 | */ | |
335 | 15 | public String getUserInput() |
336 | { | |
337 | 15 | if (comboBox == null) |
338 | { | |
339 | 0 | return textField.getText().trim(); |
340 | } | |
341 | 15 | Object item = comboBox.getEditor().getItem(); |
342 | 15 | return item == null ? "" : item.toString().trim(); |
343 | } | |
344 | ||
345 | 10 | public JComponent getComponent() |
346 | { | |
347 | 10 | return (comboBox == null ? textField : comboBox); |
348 | } | |
349 | ||
350 | 2 | public void addActionListener(ActionListener actionListener) |
351 | { | |
352 | 2 | if (comboBox == null) |
353 | { | |
354 | 0 | textField.addActionListener(actionListener); |
355 | } | |
356 | else | |
357 | { | |
358 | 2 | comboBox.addActionListener(actionListener); |
359 | } | |
360 | } | |
361 | ||
362 | 2 | public void addDocumentListener(DocumentListener listener) |
363 | { | |
364 | 2 | textComponent.getDocument().addDocumentListener(listener); |
365 | } | |
366 | ||
367 | 4 | public void addFocusListener(FocusListener focusListener) |
368 | { | |
369 | 4 | textComponent.addFocusListener(focusListener); |
370 | } | |
371 | ||
372 | 4 | public void addKeyListener(KeyListener kl) |
373 | { | |
374 | 4 | textComponent.addKeyListener(kl); |
375 | } | |
376 | ||
377 | 0 | public void addCaretListener(CaretListener caretListener) |
378 | { | |
379 | 0 | textComponent.addCaretListener(caretListener); |
380 | } | |
381 | ||
382 | 0 | public void setEditable(boolean b) |
383 | { | |
384 | 0 | if (comboBox != null) |
385 | { | |
386 | 0 | comboBox.setEditable(b); |
387 | } | |
388 | } | |
389 | ||
390 | 8 | public void setPrototypeDisplayValue(String string) |
391 | { | |
392 | 8 | prototypeDisplayValue = string; |
393 | 8 | if (comboBox != null) |
394 | { | |
395 | 0 | comboBox.setPrototypeDisplayValue(string); |
396 | } | |
397 | } | |
398 | ||
399 | 13 | public void setSelectedItem(String userInput) |
400 | { | |
401 | 13 | if (comboBox != null) |
402 | { | |
403 | 13 | comboBox.setSelectedItem(userInput); |
404 | } | |
405 | } | |
406 | ||
407 | 0 | public boolean isPopupVisible() |
408 | { | |
409 | 0 | return (comboBox != null && comboBox.isPopupVisible()); |
410 | } | |
411 | ||
412 | 2 | public void addItem(String item) |
413 | { | |
414 | 2 | if (comboBox != null) |
415 | { | |
416 | 2 | comboBox.addItem(item); |
417 | } | |
418 | } | |
419 | ||
420 | } |