001package jmri.util.swing;
002
003import java.awt.event.FocusEvent;
004import java.awt.event.FocusListener;
005import javax.swing.text.JTextComponent;
006
007/**
008 * Extends JTextField to provide a data validation function and a colorization
009 * function.
010 * <p>
011 * Supports two types of validated field: a generic text fields with length
012 * and/or character set limited by a Java regular expression or an integral
013 * numeric field with minimum and maximum allowed values.
014 *
015 * @author B. Milhaupt Copyright 2010, 2011
016 */
017public class ValidatedTextField extends javax.swing.JTextField {
018
019    ValidatedTextField thisone;
020
021    /**
022     * Provides a validated text field, where the validation mechanism requires
023     * a String value which passes the matching defined in validationRegExpr .
024     * <p>
025     * Validation occurs as part of the process of focus leaving the field. When
026     * validation fails, the focus remains within the field, and the field
027     * foreground and background colors are changed.
028     * <p>
029     * When focus leaves the field and the field value is valid, the value will
030     * be checked against the "Last Queried Value". If the current field value
031     * matches the "Last Queried Value", the field is colored using the default
032     * field foreground and background colors. If instead the current field
033     * value does not match the "Last Queried Value", the field background color
034     * is changed to reflect that the value is not yet saved. Use the
035     * {@link #setLastQueriedValue(String)} method to set the value for this
036     * comparison.
037     *
038     * @param len                    defines the width of the text field entry
039     *                               box, in characters
040     * @param forceUppercase         determines if all alphabetic characters are
041     *                               forced to uppercase
042     * @param validationRegExpr      defines a java regular expression which is
043     *                               used when validating the text input. A
044     *                               string such as "^[0-9]{2}[a-zA-Z]{3,4}$"
045     *                               would require the text field to be a 5 or 6
046     *                               character string which starts with exactly
047     *                               two digits and followed by either 3 or 4
048     *                               upper-case or lower-case letters
049     * @param validationErrorMessage is passed as an argument to the property
050     *                               change listener for the instantiating class
051     *
052     */
053    public ValidatedTextField(Integer len,
054            boolean forceUppercase,
055            String validationRegExpr,
056            String validationErrorMessage) {
057        super("0", len);
058        fieldType = FieldType.TEXT;
059        validateRegExpr = validationRegExpr;
060        validationErrorText = "ERROR:" + validationErrorMessage;
061        minAllowedValue = 0;
062        forceUpper = forceUppercase;
063        maxAllowedValue = 0;
064        allow0Length = false;
065
066        verifier = new MyVerifier();
067
068        // set default background color for invalid field data
069        setInvalidBackgroundColor(COLOR_BG_ERROR);
070
071        thisone = this;
072        thisone.setInputVerifier(verifier);
073        thisone.addFocusListener(new FocusListener() {
074            @Override
075            public void focusGained(FocusEvent e) {
076                setEditable(true);
077                setEnabled(true);
078            }
079
080            @Override
081            public void focusLost(FocusEvent e) {
082                exitFieldColorizer();
083                setEditable(true);
084            }
085        });
086    }
087
088    /**
089     * Provides a validated text field, where the validation mechanism requires
090     * a String value which passes the matching defined in validationRegExpr,
091     * and where the string begins with a number which must be within a
092     * specified integral range.
093     * <p>
094     * Validation occurs as part of the process of focus leaving the field. When
095     * validation fails, the focus remains within the field, and the field
096     * foreground and background colors are changed.
097     * <p>
098     * When focus leaves the field and the field value is valid, the value will
099     * be checked against the "Last Queried Value". If the current field value
100     * matches the "Last Queried Value", the field is colorized using the
101     * default field foreground and background colors. If instead the current
102     * field value does not match the "Last Queried Value", the field background
103     * color is changed to reflect that the value is not yet saved. Use the
104     * setLastQueriedValue() method to set the value for this comparison.
105     *
106     * @param len                    defines the width of the text field entry
107     *                               box, in characters.
108     *
109     * @param allow0LengthValue      determines if a value of 0 characters is
110     *                               allowed as a valid value.
111     *
112     * @param forceUppercase         determines if all alphabetic characters are
113     *                               forced to uppercase.
114     *
115     * @param minValue               is the smallest allowed value.
116     *
117     * @param maxValue               is the largest allowed value.
118     *
119     * @param validationRegExpr      defines a java regular expression which is
120     *                               used when validating the text input. A
121     *                               string such as "^[0-9]{2}[a-zA-Z]{3,4}$"
122     *                               would require the text field to be a 5 or 6
123     *                               character string which starts with exactly
124     *                               two digits and followed by either 3 or 4
125     *                               upper-case or lower-case letters.
126     *
127     * @param validationErrorMessage is passed as an argument to the property
128     *                               change listener for the instantiating
129     *                               class.
130     *
131     */
132    public ValidatedTextField(
133            Integer len,
134            boolean allow0LengthValue,
135            boolean forceUppercase,
136            Integer minValue,
137            Integer maxValue,
138            String validationRegExpr,
139            String validationErrorMessage) {
140        super("0", len);
141        fieldType = FieldType.INTEGRALNUMERICPLUSSTRING;
142        validateRegExpr = validationRegExpr;
143        validationErrorText = "ERROR:" + validationErrorMessage;
144        minAllowedValue = minValue;
145        forceUpper = forceUppercase;
146        maxAllowedValue = maxValue;
147        allow0Length = allow0LengthValue;
148
149        verifier = new MyVerifier();
150
151        thisone = this;
152        thisone.setInputVerifier(verifier);
153        thisone.addFocusListener(new FocusListener() {
154            @Override
155            public void focusGained(FocusEvent e) {
156                setEditable(true);
157                setEnabled(true);
158            }
159
160            @Override
161            public void focusLost(FocusEvent e) {
162                exitFieldColorizer();
163                setEditable(true);
164            }
165        });
166    }
167
168    /**
169     * Provides a validated text field for integral values, where the validation
170     * mechanism requires a numeric value between a minimum and maximum value.
171     * <p>
172     * Validation occurs as part of the process of focus leaving the field. When
173     * validation fails, the focus remains within the field, and the field
174     * foreground and background colors are changed.
175     * <p>
176     * When focus leaves the field and the field value is valid, the value will
177     * be checked against the "Last Queried Value". If the current field value
178     * matches the "Last Queried Value", the field is colored using the default
179     * field foreground and background colors. If instead the current field
180     * value does not match the "Last Queried Value", the field background color
181     * is changed to reflect that the value is not yet saved. Use the
182     * setLastQueriedValue() method to set the value for this comparison.
183     *
184     * @param len                    defines the width of the text field entry
185     *                               box, in characters
186     * @param allow0LengthValue      determines if a value of 0 characters is
187     *                               allowed as a valid value
188     * @param minValue               is the smallest allowed value
189     * @param maxValue               is the largest allowed value
190     * @param validationErrorMessage is passed as an argument to the property
191     *                               change listener for the instantiating
192     *                               class.
193     */
194    public ValidatedTextField(
195            Integer len,
196            boolean allow0LengthValue,
197            Integer minValue,
198            Integer maxValue,
199            String validationErrorMessage) {
200        super("0", len);
201        validateRegExpr = null;
202        validationErrorText = "ERROR:" + validationErrorMessage;
203        fieldType = FieldType.INTEGRALNUMERIC;
204        minAllowedValue = minValue;
205        maxAllowedValue = maxValue;
206        forceUpper = false;
207        allow0Length = allow0LengthValue;
208
209        verifier = new MyVerifier();
210
211        thisone = this;
212        thisone.setInputVerifier(verifier);
213        thisone.addFocusListener(new FocusListener() {
214            @Override
215            public void focusGained(FocusEvent e) {
216                setEditable(true);
217                setEnabled(true);
218            }
219
220            @Override
221            public void focusLost(FocusEvent e) {
222                exitFieldColorizer();
223                setEditable(true);
224            }
225        });
226    }
227
228    /**
229     * Provide a validated text field, where the validation mechanism requires a
230     * Numeric value which is a hexadecimal value which is valid and within a
231     * given numeric range.
232     * <p>
233     * Validation occurs as part of the process of focus leaving the field. When
234     * validation fails, the focus remains within the field, and the field
235     * foreground and background colors are changed.
236     * <p>
237     * When focus leaves the field and the field value is valid, the value will
238     * be checked against the "Last Queried Value". If the current field value
239     * matches the "Last Queried Value", the field is colored using the default
240     * field foreground and background colors. If instead the current field
241     * value does not match the "Last Queried Value", the field background color
242     * is changed to reflect that the value is not yet saved. Use the
243     * {@link #setLastQueriedValue(String)} method to set the value for this
244     * comparison.
245     *
246     * @param len                    the length of the field
247     * @param minAcceptableVal       defines the lowest acceptable value
248     * @param maxAcceptableVal       defines the lowest acceptable value
249     * @param validationErrorMessage is passed as an argument to the property
250     *                               change listener for the instantiating class
251     *
252     */
253    public ValidatedTextField(Integer len,
254            int minAcceptableVal,
255            int maxAcceptableVal,
256            String validationErrorMessage) {
257        super("0", len);
258        fieldType = FieldType.LIMITEDHEX;
259        validateRegExpr = null;
260        validationErrorText = "ERROR:" + validationErrorMessage;
261        minAllowedValue = minAcceptableVal;
262        forceUpper = true;
263        maxAllowedValue = maxAcceptableVal;
264        allow0Length = false;
265
266        verifier = new MyVerifier();
267
268        // set default background color for invalid field data
269        setInvalidBackgroundColor(COLOR_BG_ERROR);
270
271        thisone = this;
272        thisone.setInputVerifier(verifier);
273        thisone.addFocusListener(new FocusListener() {
274            @Override
275            public void focusGained(FocusEvent e) {
276                setEditable(true);
277                setEnabled(true);
278            }
279
280            @Override
281            public void focusLost(FocusEvent e) {
282                exitFieldColorizer();
283                setEditable(true);
284            }
285        });
286    }
287
288    private String lastQueryValue;            // used for GUI field colorization
289    private String validateRegExpr;           // used for validation of TEXT ValidatedTextField objects
290    private final Integer minAllowedValue;    // used for validation of INTEGRALNUMERIC ValidatedTextField objects
291    private final Integer maxAllowedValue;    // used for validation of INTEGRALNUMERIC ValidatedTextField objects
292    private final boolean allow0Length;       // used for validation
293
294    private final String validationErrorText; // text used when validation fails
295    private final FieldType fieldType;        // used to distinguish between INTEGRALNUMERIC-only and TEXT ValidatedTextField objects
296    private final boolean forceUpper;         // used for forcing all input to upper-case for TEXT ValidatedTextField objects
297    private final MyVerifier verifier;        // internal mechanism used for verifying field data before focus is lost
298
299    /**
300     * Method to colorize enabled field based on comparison with the last
301     * queried value. If the field is disabled, no colorization occurs.
302     */
303    private void exitFieldColorizer() {
304        // colorize the text field entry box based on comparison with last queried value
305
306        if (thisone.isEnabled()) {
307            thisone.setForeground(COLOR_OK);
308            thisone.firePropertyChange(VTF_PC_STAT_LN_UPDATE, "_", " ");
309
310            if ((getText() == null) || (getText().length() == 0)) {
311                // handle 0-length current value; 0-length is allowed
312                if (allow0Length) {
313                    if ((lastQueryValue == null) || (lastQueryValue.length() == 0)) {
314                        setBackground(COLOR_BG_UNEDITED);
315                    } else {
316                        setBackground(COLOR_BG_EDITED);
317                    }
318                }
319                // 0-length current value; 0-length is not allowed
320                // (validator should prevent from getting to this case)
321                return;
322            }
323
324            if ((lastQueryValue == null) || (lastQueryValue.length() == 0)) {
325                // handle 0-length last qurey value
326                // (already know current value is not 0-length)
327                setBackground(COLOR_BG_EDITED);
328                return;
329            }
330            if (!lastQueryValue.equals(thisone.getText())) {
331                // mismatch between last queried value and current field value
332                thisone.setBackground(COLOR_BG_EDITED);
333            } else {
334                // match between last queried value and current field value
335                thisone.setBackground(COLOR_BG_UNEDITED);
336            }
337        //} else {
338            // don't change background color of disabled field
339        }
340    }
341
342    /**
343     * Validate the field information. Does not make any GUI changes. A field
344     * value that is zero-length is considered invalid.
345     *
346     * @return true if current field information is valid; otherwise false
347     */
348    @Override
349    public boolean isValid() {
350        String value;
351        if (thisone == null) {
352            return false;
353        }
354        value = getText();
355        if (null == fieldType) {
356            // unknown validation field type
357            return false;
358        } else {
359            switch (fieldType) {
360                case TEXT:
361                    if ((value.length() < 1) && (!allow0Length)) {
362                        return false;
363                    } else {
364                        return ((allow0Length) && (value.length() == 0))
365                                || (value.matches(validateRegExpr));
366                    }
367                case INTEGRALNUMERIC:
368                    try {
369                        if ((allow0Length) && (value.length() == 0)) {
370                            return true;
371                        } else if (value.length() == 0) {
372                            return false;
373                        } else {
374                            return (Integer.parseInt(value) >= minAllowedValue)
375                                    && (Integer.parseInt(value) <= maxAllowedValue);
376                        }
377                    } catch (NumberFormatException e) {
378                        return false;
379                    }
380                case INTEGRALNUMERICPLUSSTRING:
381                    Integer findLocation = -1;
382                    Integer location;
383                    if ((allow0Length) && (value.length() == 0)) {
384                        return true;
385                    } else if (value.length() == 0) {
386                        return false;
387                    }
388
389                    location = value.indexOf('c');
390                    if ((location != -1) && (location < findLocation)) {
391                        findLocation = location;
392                    }
393                    location = value.indexOf('C');
394                    if ((location != -1) && (location < findLocation)) {
395                        findLocation = location;
396                    }
397                    location = value.indexOf('t');
398                    if ((location != -1) && (location < findLocation)) {
399                        findLocation = location;
400                    }
401                    location = value.indexOf('T');
402                    if ((location != -1) && (location < findLocation)) {
403                        findLocation = location;
404                    }
405                    if (findLocation == -1) {
406                        return false;
407                    }
408
409                    try {
410                        int address = Integer.parseInt(value.substring(0, findLocation));
411                        return (address >= minAllowedValue
412                                && address <= maxAllowedValue
413                                && value.length() >= 2
414                                && value.matches(validateRegExpr));
415                    } catch (NumberFormatException e) {
416                        return false;
417                    }
418                case LIMITEDHEX:
419                    try {
420                        if (value.isEmpty()) {
421                            return false;
422                        } else {
423                            return Integer.parseInt(value, 16) >= minAllowedValue
424                                    && Integer.parseInt(value, 16) <= maxAllowedValue;
425                        }
426                    } catch (NumberFormatException e) {
427                        return false;
428                    }
429                default:
430                    // unknown validation field type
431                    return false;
432            }
433        }
434    }
435
436    /**
437     * Set the "Last Queried Value". This value is used by the colorization
438     * process when focus is exiting the field.
439     *
440     * @see #getLastQueriedValue()
441     *
442     * @param lastQueriedValue the last value verified
443     */
444    public void setLastQueriedValue(String lastQueriedValue) {
445        lastQueryValue = lastQueriedValue;
446        exitFieldColorizer();
447    }
448
449    /**
450     * Retrieve the current value of the "Last Queried Value".
451     *
452     * @see #setLastQueriedValue(String)
453     *
454     * @return the last value verified
455     */
456    public String getLastQueriedValue() {
457        return lastQueryValue;
458    }
459
460    /**
461     * Set the "validationRegExp".
462     *
463     * @see #getValidateRegExp()
464     *
465     * @param validationRegExpr new validation pattern
466     */
467    public void setValidateRegExp(String validationRegExpr) {
468        validateRegExpr = validationRegExpr;
469    }
470
471    /**
472     * Retrieve the current "validationRegExp". Used in eg. Add Turnout to
473     * attach a manager-specific pattern without redrawing the pane
474     *
475     * @see #setValidateRegExp(String)
476     *
477     * @return the current validation pattern
478     */
479    public String getValidateRegExp() {
480        return validateRegExpr;
481    }
482
483    /**
484     * Enumeration type which differentiates the supported data types. Each
485     * different type requires special-case coding within the methods defined
486     * within this class.
487     */
488    private enum FieldType {
489
490        TEXT, INTEGRALNUMERIC, INTEGRALNUMERICPLUSSTRING, LIMITEDHEX
491    }
492
493    /**
494     * Private class used in conjunction with the basic GUI JTextField to
495     * provide the mechanisms required to validate the text field data upon loss
496     * of focus, and colorize the text field in case of validation failure.
497     */
498    private class MyVerifier extends javax.swing.InputVerifier implements java.awt.event.ActionListener {
499
500        @Override
501        public boolean shouldYieldFocus(javax.swing.JComponent input, javax.swing.JComponent target) {
502            if (input instanceof ValidatedTextField) {
503
504                if (((ValidatedTextField) input).forceUpper) {
505                    ((ValidatedTextField) input).setText(((ValidatedTextField) input).getText().toUpperCase());
506                }
507
508                boolean inputOK = verify(input);
509                if (inputOK) {
510                    input.setForeground(COLOR_OK);
511                    input.setBackground(COLOR_BG_OK);
512                    return true;
513                } else {
514                    // if there was a good way to make a beep sound here, this would be a good place to do so.
515                    //java.awt.Toolkit.getDefaultToolkit().beep();  // this didn't work under WinXP for unknown reasons.
516
517                    input.setForeground(COLOR_ERROR_VAL);
518                    input.setBackground(invalidBackgroundColor);
519                    ((JTextComponent) input).selectAll();
520                    thisone.firePropertyChange(VTF_PC_STAT_LN_UPDATE, " _ ", validationErrorText);
521                    return false;
522                }
523
524            } else {
525                return false;
526            }
527        }
528
529        @Override
530        public boolean verify(javax.swing.JComponent input) {
531            if (input instanceof ValidatedTextField) {
532                return input.isValid();
533            } else {
534                return false;
535            }
536        }
537
538        @Override
539        public void actionPerformed(java.awt.event.ActionEvent e) {
540            javax.swing.JTextField source = (javax.swing.JTextField) e.getSource();
541            shouldYieldFocus(source, null); //ignore return value
542            source.selectAll();
543        }
544    }
545
546    private java.awt.Color invalidBackgroundColor = null;
547
548    /**
549     * Set the color used for the field background when the field value is
550     * invalid.
551     *
552     * @param c background Color to be used when the value is invalid
553     */
554    public void setInvalidBackgroundColor(java.awt.Color c) {
555        invalidBackgroundColor = c;
556    }
557
558    public static final String VTF_PC_STAT_LN_UPDATE = "VTFPCK_STAT_LN_UPDATE";
559
560    // defines for colorizing the user input GUI elements and status line
561    public final static java.awt.Color COLOR_BG_EDITED = java.awt.Color.orange; // use default color for the component
562    public final static java.awt.Color COLOR_ERROR_VAL = java.awt.Color.black;
563    public final static java.awt.Color COLOR_OK = java.awt.Color.black;
564    public final static java.awt.Color COLOR_BG_OK = java.awt.Color.white;
565    public final static java.awt.Color COLOR_BG_UNEDITED = COLOR_BG_OK;
566    public final static java.awt.Color COLOR_BG_ERROR = java.awt.Color.red;
567
568}