1 /***
2 * JCompletionEnabledTextField.java
3 *
4 * This file is part of the creme library.
5 * The creme library intends to ease the development effort of large
6 * applications by providing easy to use support classes.
7 *
8 * Copyright (C) 2002 Denis Bregeon
9 *
10 * This library is free software; you can redistribute it and/or
11 * modify it under the terms of the GNU Lesser General Public
12 * License as published by the Free Software Foundation; either
13 * version 2.1 of the License, or (at your option) any later version.
14 *
15 * This library is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 * Lesser General Public License for more details.
19 *
20 * You should have received a copy of the GNU Lesser General Public
21 * License along with this library; if not, write to the Free Software
22 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 *
24 * contact information: dbregeon@sourceforge.net
25 */
26 package org.jcreme.swing;
27
28 import java.awt.Component;
29 import java.awt.event.ActionEvent;
30 import java.awt.event.ActionListener;
31 import java.awt.event.FocusEvent;
32 import java.awt.event.KeyEvent;
33 import java.awt.event.KeyListener;
34 import java.text.ParseException;
35 import java.util.Collection;
36 import java.util.Iterator;
37
38 import javax.swing.DefaultListCellRenderer;
39 import javax.swing.DefaultListModel;
40 import javax.swing.JFormattedTextField;
41 import javax.swing.JList;
42 import javax.swing.JPopupMenu;
43 import javax.swing.ListSelectionModel;
44 import javax.swing.Timer;
45 import javax.swing.event.DocumentEvent;
46 import javax.swing.event.DocumentListener;
47 import javax.swing.event.ListSelectionEvent;
48 import javax.swing.event.ListSelectionListener;
49 import javax.swing.event.PopupMenuEvent;
50 import javax.swing.event.PopupMenuListener;
51 import javax.swing.text.BadLocationException;
52 import javax.swing.text.Document;
53
54 /***
55 * This GUI Component provides a Completion list in a window after a given delay
56 * expires. This Component still basically behaves like a JFormattedTextField.
57 *
58 * @author $Author: dbregeon $
59 * @version $Revision: 1.1 $
60 */
61 public class JCompletionEnabledTextField extends JFormattedTextField {
62 /***
63 * The Default delay before the completion window appears.
64 */
65 public static final int DEFAULT_DELAY = 600;
66
67 /***
68 * The CompletionModel used to provide a completion list.
69 */
70 private CompletionModel completionModel = null;
71
72 /***
73 * This member stores the seed used to perform the last completion so that
74 * the completion characters are selected.
75 */
76 private volatile String prefix = null;
77
78 /***
79 * The window that contains the completion list.
80 */
81 private final JPopupMenu popupWindow = new JPopupMenu();
82
83 /***
84 * The JList that presents the possible completions.
85 */
86 private final JList popupList = new JList();
87
88 /***
89 * This listener is used for both the timer that triggers the display of the
90 * completion window and this component to hide that same window.
91 */
92 private final ActionListener timerListener;
93
94 /***
95 * The Timer that triggers the display of the completion window.
96 */
97 private final Timer popupTimer;
98
99 /***
100 * This listener enables to update the text in this component, based on the
101 * completion selected in the completion list.
102 */
103 private final ListSelectionListener completionListener = new ListSelectionListener() {
104 public void valueChanged(ListSelectionEvent e) {
105 if (getPopupWindow().isVisible()) {
106 updateValue();
107 }
108 }
109 };
110
111 /***
112 * This listener updates the popupList when the document is modified.
113 */
114 private final DocumentListener documentListener = new DocumentListener() {
115 public void insertUpdate(DocumentEvent e) {
116 new Thread() {
117 public void run() {
118 updatePopupList();
119 }
120 }.start();
121 }
122
123 public void removeUpdate(DocumentEvent e) {
124 setPopupVisible(false);
125 }
126
127 public void changedUpdate(DocumentEvent e) {
128 }
129 };
130
131 protected final JPopupMenu getPopupWindow() {
132 return this.popupWindow;
133 }
134
135 protected final Timer getPopupTimer() {
136 return this.popupTimer;
137 }
138
139 /***
140 * @see javax.swing.JComponent#processKeyEvent(java.awt.event.KeyEvent)
141 */
142 protected void processKeyEvent(KeyEvent e) {
143 if (((e.getKeyCode() == KeyEvent.VK_LEFT) || (e.getKeyCode() == KeyEvent.VK_RIGHT))
144 && (e.getID() == KeyEvent.KEY_PRESSED)) {
145 setPopupVisible(false);
146 }
147 if ((e.getKeyCode() == KeyEvent.VK_ESCAPE)
148 && (e.getID() == KeyEvent.KEY_PRESSED)) {
149 cancelEdition();
150 } else {
151 super.processKeyEvent(e);
152 }
153 if (e.getID() == KeyEvent.KEY_PRESSED) {
154 if (this.popupWindow.isVisible()) {
155 if (e.getKeyCode() == KeyEvent.VK_UP) {
156 if (this.popupList.getSelectedIndex() > 0) {
157 this.popupList.setSelectedIndex(this.popupList
158 .getSelectedIndex() - 1);
159 } else {
160 this.popupList.setSelectedIndex(this.popupList
161 .getModel().getSize() - 1);
162 }
163 } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
164 if (this.popupList.getSelectedIndex() < this.popupList
165 .getModel().getSize() - 1) {
166 this.popupList.setSelectedIndex(this.popupList
167 .getSelectedIndex() + 1);
168 } else {
169 this.popupList.setSelectedIndex(0);
170 }
171 }
172 }
173 }
174 }
175
176 /***
177 * @see JFormattedTextField#processFocusEvent(java.awt.event.FocusEvent)
178 */
179 protected void processFocusEvent(FocusEvent e) {
180 getDocument().removeDocumentListener(this.documentListener);
181 super.processFocusEvent(e);
182 getDocument().addDocumentListener(this.documentListener);
183 }
184
185 /***
186 * Creates a JCompletionEnabledTextField.
187 */
188 public JCompletionEnabledTextField() {
189 this(null);
190 }
191
192 /***
193 * Creates a JCompletionEnabledTextField.
194 *
195 * @param value
196 * the initial value in this TextField.
197 */
198 public JCompletionEnabledTextField(Object value) {
199 super(value);
200 this.timerListener = new ActionListener() {
201 public void actionPerformed(ActionEvent evt) {
202 setPopupVisible((evt.getSource() == getPopupTimer()));
203 }
204 };
205 this.popupTimer = new Timer(DEFAULT_DELAY, this.timerListener);
206 initComponent();
207 }
208
209 /***
210 * This method creates all the elements that do not exist in the parent
211 * class. It also takes care of associated the event listeners the necessary
212 * components.
213 */
214 protected final void initComponent() {
215 this.popupTimer.setRepeats(false);
216 this.popupWindow.setInvoker(this);
217 addActionListener(this.timerListener);
218 this.popupList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
219 this.popupList.setCellRenderer(new DefaultListCellRenderer() {
220 public Component getListCellRendererComponent(JList list,
221 Object value, int index, boolean isSelected,
222 boolean cellHasFocus) {
223 Object newValue = value;
224 if ((getCompletionModel() != null)
225 && (getCompletionModel()
226 .getCompletionListElementFormat() != null)) {
227 newValue = getCompletionModel()
228 .getCompletionListElementFormat().format(value);
229 }
230 return super.getListCellRendererComponent(list, newValue,
231 index, isSelected, cellHasFocus);
232 }
233 });
234 this.popupList.addListSelectionListener(this.completionListener);
235 this.popupWindow.add(this.popupList);
236 this.popupList.addKeyListener(new KeyListener() {
237 public void keyTyped(KeyEvent e) {
238 processKeyEvent(e);
239 }
240
241 public void keyPressed(KeyEvent e) {
242 processKeyEvent(e);
243 }
244
245 public void keyReleased(KeyEvent e) {
246 processKeyEvent(e);
247 }
248 });
249 this.popupWindow.addKeyListener(new KeyListener() {
250 public void keyTyped(KeyEvent e) {
251 processKeyEvent(e);
252 }
253
254 public void keyPressed(KeyEvent e) {
255 processKeyEvent(e);
256 }
257
258 public void keyReleased(KeyEvent e) {
259 processKeyEvent(e);
260 }
261 });
262 setFormatterFactory(new AbstractFormatterFactory() {
263 protected AbstractFormatter formatter = null;
264
265 /***
266 * @see AbstractFormatterFactory#getFormatter(javax.swing.JFormattedTextField)
267 */
268 public AbstractFormatter getFormatter(JFormattedTextField field) {
269 if (this.formatter == null) {
270 this.formatter = new AbstractFormatter() {
271 /***
272 * @see AbstractFormatter#stringToValue(java.lang.String)
273 */
274 public Object stringToValue(String arg0)
275 throws ParseException {
276 Object result = arg0;
277 if ((getCompletionModel() != null)
278 && (getCompletionModel()
279 .getCompletionListElementFormat() != null)) {
280 result = getCompletionModel()
281 .getCompletionListElementFormat()
282 .parseObject(arg0);
283 }
284 return result;
285 }
286
287 /***
288 * @see AbstractFormatter#valueToString(java.lang.Object)
289 */
290 public String valueToString(Object arg0)
291 throws ParseException {
292 String result = null;
293 if ((getCompletionModel() != null)
294 && (getCompletionModel()
295 .getCompletionListElementFormat() != null)) {
296 result = getCompletionModel()
297 .getCompletionListElementFormat()
298 .format(arg0);
299 } else if (arg0 != null) {
300 result = arg0.toString();
301 }
302 return result;
303 }
304 };
305 }
306 return this.formatter;
307 }
308 });
309 getDocument().addDocumentListener(this.documentListener);
310 setRequestFocusEnabled(true);
311 this.popupWindow.addPopupMenuListener(new PopupMenuListener() {
312 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
313
314 }
315
316 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
317 getPopupTimer().stop();
318 JCompletionEnabledTextField.this.requestFocus();
319 }
320
321 public void popupMenuCanceled(PopupMenuEvent e) {
322
323 }
324 });
325 }
326
327 /***
328 * This method enables to change the model used to provide the possible
329 * completion displayed in the window.
330 *
331 * @param model
332 * the model that will provide completions.
333 */
334 public void setCompletionModel(CompletionModel model) {
335 this.completionModel = model;
336 }
337
338 /***
339 * Gives access to the CompletionModel in use in this Component.
340 *
341 * @return the current CompletionModel.
342 */
343 public CompletionModel getCompletionModel() {
344 return this.completionModel;
345 }
346
347 /***
348 * This method enables to change the delay after which the completion window
349 * is displayed (when the user finished typing).
350 *
351 * @param delay
352 * the new display delay.
353 */
354 public void setPopupDelay(int delay) {
355 this.popupTimer.setDelay(delay);
356 }
357
358 /***
359 * Gives access to the delay used to trigger the completion list.
360 *
361 * @return the display delay.
362 */
363 public int getPopupDelay() {
364 return this.popupTimer.getDelay();
365 }
366
367 /***
368 * This helper method enables to show or hide the completion window as
369 * needed.
370 *
371 * @param b
372 * true if the completion window needs to be displayed. false
373 * otherwise.
374 */
375 protected void setPopupVisible(boolean b) {
376 int size = this.popupList.getModel().getSize();
377 boolean display = b;
378 display = display && (size > 0);
379 display = display
380 && (!((size == 1) && (this.popupList.getModel().getElementAt(0)
381 .equals(getValue()))));
382 if (display) {
383 this.popupWindow.show(this, 0, getHeight());
384 this.popupList.requestFocus();
385 if (this.popupList.getModel().getSize() > 0) {
386 this.popupList.setSelectedIndex(0);
387 }
388 } else {
389 this.popupWindow.setVisible(false);
390
391 this.prefix = null;
392 this.popupTimer.stop();
393 requestFocus();
394 }
395 }
396
397 /***
398 * This helper method updates the completion list that is to be displayed in
399 * the completion window.
400 */
401 protected void updatePopupList() {
402 DefaultListModel model = new DefaultListModel();
403 try {
404 this.prefix = getDocument().getText(0, getDocument().getLength());
405 if ((this.completionModel != null) && (!"".equals(this.prefix))) {
406 Collection c = this.completionModel.getCompletions(this.prefix);
407 if (c != null) {
408 Iterator iter = c.iterator();
409 while (iter.hasNext()) {
410 model.addElement(iter.next());
411 }
412 }
413 }
414 this.popupList.setModel(model);
415 this.popupList.setSelectedValue(null, false);
416 this.popupWindow.pack();
417 } catch (BadLocationException e) {
418 e.printStackTrace();
419 }
420 if (isEditValid()) {
421 try {
422 commitEdit();
423 } catch (ParseException e) {
424 e.printStackTrace();
425 }
426 }
427 if ((!this.popupWindow.isVisible()) && (!model.isEmpty())) {
428 this.popupTimer.restart();
429 }
430 }
431
432 /***
433 * This helper method updates the text displayed in this component so that
434 * it is consistent with the completion currently selected in the completion
435 * list.
436 */
437 protected void updateValue() {
438 Object selectedValue = this.popupList.getSelectedValue();
439 if (selectedValue != null) {
440 int index = (this.prefix != null ? this.prefix.length() : 0);
441 getDocument().removeDocumentListener(this.documentListener);
442 setValue(selectedValue);
443
444
445 setSelectionStart(index);
446 setSelectionEnd(getDocument().getLength());
447 getDocument().addDocumentListener(this.documentListener);
448 }
449 }
450
451 /***
452 * @see JFormattedTextField#setDocument(javax.swing.text.Document)
453 */
454 public void setDocument(Document d) {
455 if (getDocument() != null) {
456 getDocument().removeDocumentListener(this.documentListener);
457 }
458 super.setDocument(d);
459 if (getDocument() != null) {
460 getDocument().addDocumentListener(this.documentListener);
461 }
462 }
463
464 /***
465 * This method returns the edition to its status before the proposed
466 * completion.
467 *
468 */
469 protected void cancelEdition() {
470 String text = this.prefix;
471 getDocument().removeDocumentListener(this.documentListener);
472 try {
473 getDocument().remove(0, getDocument().getLength());
474 getDocument().insertString(0, text, null);
475 } catch (BadLocationException ex) {
476 ex.printStackTrace();
477 }
478 getDocument().addDocumentListener(this.documentListener);
479 }
480 }