1 /*
2  * The contents of this file is dual-licensed under 2
3  * alternative Open Source/Free licenses: LGPL 2.1 or later and
4  * Apache License 2.0. (starting with JNA version 4.0.0).
5  *
6  * You can freely decide which license you want to apply to
7  * the project.
8  *
9  * You may obtain a copy of the LGPL License at:
10  *
11  * http://www.gnu.org/licenses/licenses.html
12  *
13  * A copy is also included in the downloadable source code package
14  * containing JNA, in file "LGPL2.1".
15  *
16  * You may obtain a copy of the Apache License at:
17  *
18  * http://www.apache.org/licenses/
19  *
20  * A copy is also included in the downloadable source code package
21  * containing JNA, in file "AL2.0".
22  */
23 package com.sun.jna.contrib.demo;
24 
25 import java.awt.BorderLayout;
26 import java.awt.Color;
27 import java.awt.event.FocusAdapter;
28 import java.awt.event.FocusEvent;
29 import java.util.ArrayList;
30 
31 import javax.swing.BorderFactory;
32 import javax.swing.JFrame;
33 import javax.swing.JLabel;
34 import javax.swing.JPanel;
35 import javax.swing.JTextField;
36 import javax.swing.Popup;
37 import javax.swing.UIManager;
38 import javax.swing.border.Border;
39 import javax.swing.event.DocumentEvent;
40 import javax.swing.event.DocumentListener;
41 import javax.swing.text.AttributeSet;
42 import javax.swing.text.BadLocationException;
43 import javax.swing.text.Document;
44 import javax.swing.text.PlainDocument;
45 
46 /**
47  * The FilteredTextField class is a JTextField that only allows specified
48  * characters to be entered into it.  The allowed characters can be added to
49  * the text field, and entry validation is performed as each character is
50  * typed.  In addition, complete string validation is tested against a
51  * configurable regular expression when leaving the field.  If the string is
52  * invalid the text field is bordered with a red line, and the user is notified
53  * of the error upon returning to the text field.  The text field can also be
54  * configured to accept a limited number of characters.
55  */
56 @SuppressWarnings("serial")
57 public class FilteredTextField extends JTextField {
58 
59     public static final Character[] UPPERCASE_CHARS = {'A', 'B', 'C', 'D', 'E',
60         'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
61         'U', 'V', 'W', 'X', 'Y', 'Z'};
62     public static final Character[] LOWERCASE_CHARS = {'a', 'b', 'c', 'd', 'e',
63         'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
64         'u', 'v', 'w', 'x', 'y', 'z'};
65     public static final Character[] NUMERIC_CHARS = {'1', '2', '3', '4', '5',
66         '6', '7', '8', '9', '0'};
67 
68     private static final Integer ENTRY_BALLOON = 0;
69     private static final Integer VALID_BALLOON = 1;
70     private static final Integer LENGTH_BALLOON = 2;
71     private static final Border RED_BORDER
72         = BorderFactory.createLineBorder(Color.RED, 2);
73 
74     private ArrayList<Character> allowable = new ArrayList<Character>();
75     private int maximumLength = String.valueOf(Long.MAX_VALUE).length();
76 
77     private Border defaultBorder = null;
78     private boolean isValid = true;
79 
80     private Popup balloon = null;
81     private String entryError = null;
82     private String validRegex = null;
83     private String validError = null;
84     private Color balloonBorderColor = null;
85     private Color balloonBackgroundColor = null;
86     private Color balloonTextColor = null;
87     private Integer balloonDuration = null;
88 
89     private Integer balloonType = null;
90 
91     /**
92      * Create a FilteredTextField.
93      */
FilteredTextField()94     public FilteredTextField() {
95         super();
96         init();
97     }
98 
99     /**
100      * Create a FilteredTextField.
101      *
102      * @param columns the number of columns to use to calculate the preferred
103      *                width
104      */
FilteredTextField(int columns)105     public FilteredTextField(int columns) {
106         super(columns);
107         init();
108     }
109 
110     /*
111      * Initialize the FilteredTextField.
112      */
init()113     private void init() {
114         defaultBorder = getBorder();
115         entryError = "";
116         validRegex = "";
117         validError = "";
118         balloonBorderColor = BalloonTipManager.DEFAULT_BORDER_COLOR;
119         balloonBackgroundColor = BalloonTipManager.DEFAULT_BACKGROUND_COLOR;
120         balloonTextColor = BalloonTipManager.DEFAULT_TEXT_COLOR;
121         balloonDuration = 10000;
122         balloonType = ENTRY_BALLOON;
123         addFocusListener(new ValidationEar());
124     }
125 
126     /**
127      * Sets the allowable character used for entry validation.
128      *
129      * @param characters the allowable characters
130      */
setCharacters(Character[] characters)131     public void setCharacters(Character[] characters) {
132         clearCharacters();
133         for (int i = 0; i < characters.length; i++) {
134             addCharacter(characters[i]);
135         }
136     }
137 
138     /**
139      * Adds the character array to the list used for entry validation.
140      *
141      * @param characters the character array
142      */
addCharacters(Character[] characters)143     public void addCharacters(Character[] characters) {
144         for (int i = 0; i < characters.length; i++) {
145             addCharacter(characters[i]);
146         }
147     }
148 
149     /**
150      * Adds the character to the list used for entry validation.
151      *
152      * @param characters the character
153      */
addCharacter(Character character)154     public void addCharacter(Character character) {
155         if (!allowable.contains(character)) {
156             allowable.add(character);
157         }
158     }
159 
160     /**
161      * Clears the list of allowable characters for entry validation.
162      */
clearCharacters()163     public void clearCharacters() {
164         allowable.clear();
165     }
166 
167     /**
168      * Removes the character array from the list used for entry validation.
169      *
170      * @param characters the character array
171      */
removeCharacters(Character[] characters)172     public void removeCharacters(Character[] characters) {
173         for (int i = 0; i < characters.length; i++) {
174             removeCharacter(characters[i]);
175         }
176     }
177 
178     /**
179      * Removes the character from the list used for entry validation.
180      *
181      * @param character the character
182      */
removeCharacter(Character character)183     public void removeCharacter(Character character) {
184         if (allowable.contains(character)) {
185             allowable.remove(character);
186         }
187     }
188 
189     /**
190      * Sets the maximum number of characters for the length of the entry string.
191      *
192      * @param maximumLength the number of characters
193      */
setMaximumLength(int maximumLength)194     public void setMaximumLength(int maximumLength) {
195         this.maximumLength = maximumLength;
196     }
197 
198     /**
199      * Sets the message that is displayed when there is an entry error.
200      *
201      * @param entryError the entry error message
202      */
setEntryError(String entryError)203     public void setEntryError(String entryError) {
204         this.entryError = entryError;
205     }
206 
207     /**
208      * Sets the regular expression that is used for string validation. String
209      * validation is checked when exiting the text field.
210      *
211      * @param validRegex the validation regular expression
212      */
setValidRegex(String validRegex)213     public void setValidRegex(String validRegex) {
214         this.validRegex = validRegex;
215     }
216 
217     /**
218      * Sets the message that is displayed when there is a validation error.
219      *
220      * @param validError the validation error message
221      */
setValidError(String validError)222     public void setValidError(String validError) {
223         this.validError = validError;
224     }
225 
226     /**
227      * Sets the color to use for the balloon border.
228      *
229      * @param borderColor the balloon border color
230      */
setBalloonBorderColor(Color borderColor)231     public void setBalloonBorderColor(Color borderColor) {
232         balloonBorderColor = borderColor;
233     }
234 
235     /**
236      * Sets the color to use for the balloon background.
237      *
238      * @param backgroundColor the balloon background color
239      */
setBalloonBackgroundColor(Color backgroundColor)240     public void setBalloonBackgroundColor(Color backgroundColor) {
241         balloonBackgroundColor = backgroundColor;
242     }
243 
244     /**
245      * Sets the color to use for the balloon text.
246      *
247      * @param textColor the balloon text color
248      */
setBalloonTextColor(Color textColor)249     public void setBalloonTextColor(Color textColor) {
250         balloonTextColor = textColor;
251     }
252 
253     /**
254      * Sets the time in milliseconds that the balloon is visible before
255      * disappearing. This is the maximum time that the balloon will be visible,
256      * as other events can also make the balloon disappear.
257      *
258      * @param duration the time in milliseconds
259      */
setBalloonDuration(Integer duration)260     public void setBalloonDuration(Integer duration) {
261         balloonDuration = duration;
262     }
263 
264     /*
265    * (non-Javadoc)
266    * @see javax.swing.JTextField#createDefaultModel()
267      */
createDefaultModel()268     protected Document createDefaultModel() {
269         return new FilteredTextFieldDocument();
270     }
271 
272     /*
273    * This class defines the document used for the FilteredTextField.
274      */
275     private class FilteredTextFieldDocument extends PlainDocument {
276 
277         /*
278      * Create a FilteredTextFieldDocument.
279          */
FilteredTextFieldDocument()280         public FilteredTextFieldDocument() {
281             addDocumentListener(new FilteredTextFieldEar());
282         }
283 
284         /*
285          * (non-Javadoc)
286          * @see javax.swing.text.PlainDocument#insertString(
287          * int, java.lang.String, javax.swing.text.AttributeSet)
288          */
insertString(int offset, String str, AttributeSet a)289         public void insertString(int offset, String str, AttributeSet a)
290             throws BadLocationException {
291             if (balloon != null && BalloonTipManager.isShowing()) {
292                 if (balloonType == VALID_BALLOON) {
293                     balloon.hide();
294                 }
295             }
296             StringBuilder buffer
297                 = new StringBuilder(FilteredTextField.this.getText());
298             if (offset >= 0 && offset <= buffer.length()) {
299                 buffer.insert(offset, str);
300                 String strBuf = buffer.toString();
301 
302                 if (buffer.length() > maximumLength) {
303                     if (balloon != null && BalloonTipManager.isShowing()) {
304                         if (balloonType == LENGTH_BALLOON) {
305                             BalloonTipManager.restartTimer();
306                             return;
307                         } else {
308                             balloon.hide();
309                         }
310                     }
311                     balloon = BalloonTipManager.getBalloonTip(FilteredTextField.this,
312                         "The number of characters must be less than or equal to "
313                         + maximumLength, 0, 0, balloonDuration, balloonBorderColor,
314                         balloonBackgroundColor, balloonTextColor);
315                     balloon.show();
316                     balloonType = LENGTH_BALLOON;
317                     return;
318                 }
319 
320                 if (strBuf == null || strBuf.equals("")) {
321                     remove(0, getLength());
322                     super.insertString(0, "", null);
323                     if (balloon != null && BalloonTipManager.isShowing()) {
324                         balloon.hide();
325                     }
326                     return;
327                 }
328 
329                 if (allowable.contains(str.charAt(0))) {
330                     super.insertString(offset, str, a);
331                     if (balloon != null && BalloonTipManager.isShowing()) {
332                         balloon.hide();
333                     }
334                 } else {
335                     if (balloon != null && BalloonTipManager.isShowing()) {
336                         if (balloonType == ENTRY_BALLOON) {
337                             BalloonTipManager.restartTimer();
338                             return;
339                         } else {
340                             balloon.hide();
341                         }
342                     }
343                     balloon = BalloonTipManager.getBalloonTip(FilteredTextField.this,
344                         entryError, 0, 0, balloonDuration, balloonBorderColor,
345                         balloonBackgroundColor, balloonTextColor);
346                     balloon.show();
347                     balloonType = ENTRY_BALLOON;
348                 }
349             }
350         }
351 
352         /*
353          * This listener class is needed to catch character removal events.
354          */
355         private class FilteredTextFieldEar implements DocumentListener {
356 
357             /*
358              * (non-Javadoc)
359              * @see javax.swing.event.DocumentListener#insertUpdate(
360              * javax.swing.event.DocumentEvent)
361              */
insertUpdate(DocumentEvent e)362             public void insertUpdate(DocumentEvent e) {/* N/A */
363             }
364 
365             /*
366              * (non-Javadoc)
367              * @see javax.swing.event.DocumentListener#removeUpdate(
368              * javax.swing.event.DocumentEvent)
369              */
removeUpdate(DocumentEvent e)370             public void removeUpdate(DocumentEvent e) {
371                 if (balloon != null && BalloonTipManager.isShowing()) {
372                     balloon.hide();
373                 }
374             }
375 
376             /*
377              * (non-Javadoc)
378              * @see javax.swing.event.DocumentListener#changedUpdate(
379              * javax.swing.event.DocumentEvent)
380              */
changedUpdate(DocumentEvent e)381             public void changedUpdate(DocumentEvent e) {/* N/A */
382             }
383         }
384     }
385 
386     /*
387      * This listener class is used to determine whether the string is valid based
388      * on a regular expression.  The validation is tested when leaving the text
389      * field, and notification is performed when returning to the text field.
390      */
391     private class ValidationEar extends FocusAdapter {
392 
393         /*
394          * (non-Javadoc)
395          * @see java.awt.event.FocusAdapter#focusLost(java.awt.event.FocusEvent)
396          */
focusLost(FocusEvent e)397         public void focusLost(FocusEvent e) {
398             String entered = getText().trim();
399             if (!entered.matches(validRegex)) {
400                 if (balloon != null) {
401                     balloon.hide();
402                 }
403                 setBorder(
404                     BorderFactory.createCompoundBorder(RED_BORDER, defaultBorder));
405                 isValid = false;
406             } else {
407                 setBorder(defaultBorder);
408                 isValid = true;
409             }
410         }
411 
412         /*
413          * (non-Javadoc)
414          * @see java.awt.event.FocusAdapter#focusGained(java.awt.event.FocusEvent)
415          */
focusGained(FocusEvent e)416         public void focusGained(FocusEvent e) {
417             if (!isValid) {
418                 balloon = BalloonTipManager.getBalloonTip(FilteredTextField.this,
419                     validError, 0, 0, balloonDuration, balloonBorderColor,
420                     balloonBackgroundColor, balloonTextColor);
421                 balloon.show();
422                 balloonType = VALID_BALLOON;
423             }
424         }
425     }
426 
427     /**
428      * A main entry point to test the FilteredTextField.
429      *
430      * @param args application arguments
431      */
main(String[] args)432     public static void main(String[] args) {
433         try {
434             UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
435         } catch (Exception e) {
436             e.printStackTrace();
437         }
438         JFrame jframe = new JFrame("Balloon Tips on FilteredTextField");
439         jframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
440         jframe.setSize(400, 75);
441         jframe.setLocation(400, 400);
442         JPanel jpanel = new JPanel();
443         jpanel.setLayout(new BorderLayout());
444         FilteredTextField ftfield = new FilteredTextField(10);
445         ftfield.setCharacters(LOWERCASE_CHARS);
446         ftfield.addCharacter('-');
447         ftfield.addCharacter('_');
448         ftfield.addCharacter(' ');
449         ftfield.setMaximumLength(10);
450         ftfield.setEntryError(
451             "Only lower case letters, hyphens, underscores, and spaces allowed.");
452         ftfield.setValidRegex("^a+[a-z-_ ]*");
453         ftfield.setValidError("The string must begin with the letter 'a'.");
454         jpanel.add(new JLabel("Type some text into either field"), BorderLayout.NORTH);
455         jpanel.add(ftfield, BorderLayout.CENTER);
456         jpanel.add(new FilteredTextField(10), BorderLayout.SOUTH);
457         jframe.getContentPane().add(jpanel);
458         jframe.setVisible(true);
459     }
460 }
461