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}