View Javadoc

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                 // Do Nothing
314             }
315 
316             public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
317                 getPopupTimer().stop();
318                 JCompletionEnabledTextField.this.requestFocus();
319             }
320 
321             public void popupMenuCanceled(PopupMenuEvent e) {
322                 // Do Nothing
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             // Text has been validated, the prefix is not valid anymore.
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             // Ensure that the completion is selected so that it is easy to
444             // erase.
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 }