001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * https://www.ssec.wisc.edu/mcidas/ 009 * 010 * All Rights Reserved 011 * 012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 013 * some McIDAS-V source code is based on IDV and VisAD source code. 014 * 015 * McIDAS-V is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU Lesser Public License as published by 017 * the Free Software Foundation; either version 3 of the License, or 018 * (at your option) any later version. 019 * 020 * McIDAS-V is distributed in the hope that it will be useful, 021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 023 * GNU Lesser Public License for more details. 024 * 025 * You should have received a copy of the GNU Lesser Public License 026 * along with this program. If not, see https://www.gnu.org/licenses/. 027 */ 028 029package edu.wisc.ssec.mcidasv.util; 030 031import java.awt.BorderLayout; 032 033import java.awt.event.FocusEvent; 034import java.awt.event.FocusListener; 035 036import java.util.regex.Matcher; 037import java.util.regex.Pattern; 038import java.util.regex.PatternSyntaxException; 039 040import javax.swing.InputVerifier; 041import javax.swing.JComponent; 042import javax.swing.JLabel; 043import javax.swing.JTextField; 044 045import javax.swing.event.DocumentEvent; 046import javax.swing.event.DocumentListener; 047 048import javax.swing.text.AttributeSet; 049import javax.swing.text.BadLocationException; 050import javax.swing.text.Document; 051import javax.swing.text.JTextComponent; 052import javax.swing.text.PlainDocument; 053 054/** 055 * Extend JTextField to add niceties such as uppercase, 056 * length limits, and allow/deny character sets 057 */ 058public class McVTextField extends JTextField { 059 060 public static char[] mcidasDeny = 061 new char[] { '/', '.', ' ', '[', ']', '%' }; 062 063 public static Pattern ipAddress = 064 Pattern.compile("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"); 065 066 private McVTextFieldDocument document = new McVTextFieldDocument(); 067 068 private Pattern validPattern; 069 070 private String[] validStrings; 071 072 public McVTextField() { 073 this("", 0, false); 074 } 075 076 public McVTextField(String defaultString) { 077 this(defaultString, 0, false); 078 } 079 080 public McVTextField(String defaultString, int limit) { 081 this(defaultString, limit, false); 082 } 083 084 public McVTextField(String defaultString, boolean upper) { 085 this(defaultString, 0, upper); 086 } 087 088 // All other constructors call this one 089 public McVTextField(String defaultString, int limit, boolean upper) { 090 super(limit); 091 this.document = new McVTextFieldDocument(limit, upper); 092 super.setDocument(document); 093 this.setText(defaultString); 094 } 095 096 public McVTextField(String defaultString, int limit, boolean upper, 097 String allow, String deny) 098 { 099 this(defaultString, limit, upper); 100 setAllow(makePattern(allow)); 101 setDeny(makePattern(deny)); 102 } 103 104 public McVTextField(String defaultString, int limit, boolean upper, 105 char[] allow, char[] deny) 106 { 107 this(defaultString, limit, upper); 108 setAllow(makePattern(allow)); 109 setDeny(makePattern(deny)); 110 } 111 112 public McVTextField(String defaultString, int limit, boolean upper, 113 Pattern allow, Pattern deny) 114 { 115 this(defaultString, limit, upper); 116 setAllow(allow); 117 setDeny(deny); 118 } 119 120 public int getLimit() { 121 return this.document.getLimit(); 122 } 123 124 public void setLimit(int limit) { 125 this.document.setLimit(limit); 126 super.setDocument(document); 127 } 128 129 public boolean getUppercase() { 130 return this.document.getUppercase(); 131 } 132 133 public void setUppercase(boolean uppercase) { 134 this.document.setUppercase(uppercase); 135 super.setDocument(document); 136 } 137 138 /** @see #setAllow(Pattern, boolean) */ 139 public void setAllow(char... characters) { 140 setAllow(makePattern(characters), false); 141 } 142 143 /** @see #setAllow(Pattern, boolean) */ 144 public void setAllow(String string) { 145 setAllow(makePattern(string), false); 146 } 147 148 /** @see #setAllow(Pattern, boolean) */ 149 public void setAllow(Pattern newPattern) { 150 setAllow(newPattern, false); 151 } 152 153 /** @see #setAllow(Pattern, boolean) */ 154 public void setAllow(String string, boolean useComplete) { 155 setAllow(makePattern(string), useComplete); 156 } 157 158 /** @see #setAllow(Pattern, boolean) */ 159 public void setAllow(char[] characters, boolean useComplete) { 160 setAllow(makePattern(characters), useComplete); 161 } 162 163 /** @see #setDeny(Pattern, boolean) */ 164 public void setDeny(char... characters) { 165 setDeny(characters, false); 166 } 167 168 /** @see #setDeny(Pattern, boolean) */ 169 public void setDeny(String string) { 170 setDeny(makePattern(string), false); 171 } 172 173 /** @see #setDeny(Pattern, boolean) */ 174 public void setDeny(Pattern newPattern) { 175 setDeny(newPattern, false); 176 } 177 178 /** @see #setDeny(Pattern, boolean) */ 179 public void setDeny(String string, boolean useComplete) { 180 setDeny(makePattern(string), useComplete); 181 } 182 183 /** @see #setDeny(Pattern, boolean) */ 184 public void setDeny(char[] characters, boolean useComplete) { 185 setDeny(makePattern(characters), useComplete); 186 } 187 188 /** 189 * Change the regular expression used to match allowed strings. 190 * 191 * <p>Note: if set to {@code true}, {@code useComplete} parameter will allow 192 * you to match {@code newPattern} against the complete text of this text 193 * field, including the tentative updates. If set to {@code false}, 194 * {@code newPattern} will be used against the <i>only</i> the updated 195 * characters.</p> 196 * 197 * @param newPattern New regular expression. Cannot be {@code null}. 198 * @param useComplete Whether or not the complete contents of the text field 199 * should be used. 200 */ 201 public void setAllow(Pattern newPattern, boolean useComplete) { 202 this.document.setAllow(newPattern); 203 this.document.setUseComplete(useComplete); 204 super.setDocument(document); 205 } 206 207 /** 208 * Change the regular expression used to match denied strings. 209 * 210 * <p>Note: if set to {@code true}, {@code useComplete} parameter will allow 211 * you to match {@code newPattern} against the complete text of this text 212 * field, including the tentative updates. If set to {@code false}, 213 * {@code newPattern} will be used against the <i>only</i> the updated 214 * characters.</p> 215 * 216 * @param newPattern New regular expression. Cannot be {@code null}. 217 * @param useComplete Whether or not the complete contents of the text field 218 * should be used. 219 */ 220 public void setDeny(Pattern newPattern, boolean useComplete) { 221 this.document.setDeny(newPattern); 222 this.document.setUseComplete(useComplete); 223 super.setDocument(document); 224 } 225 226 // Take a string and turn it into a pattern 227 private Pattern makePattern(String string) { 228 if (string == null) { 229 return null; 230 } 231 try { 232 return Pattern.compile(string); 233 } catch (PatternSyntaxException e) { 234 return null; 235 } 236 } 237 238 // Take a character array and turn it into a [abc] class pattern 239 private Pattern makePattern(char... characters) { 240 if (characters == null) { 241 return null; 242 } 243 StringBuilder string = new StringBuilder(".*"); 244 if (characters.length > 0) { 245 string = new StringBuilder("["); 246 for (char c : characters) { 247 if (c == '[') { 248 string.append("\\["); 249 } else if (c == ']') { 250 string.append("\\]"); 251 } else if (c == '\\') { 252 string.append("\\\\"); 253 } else { 254 string.append(c); 255 } 256 } 257 string.append("]"); 258 } 259 try { 260 return Pattern.compile(string.toString()); 261 } catch (PatternSyntaxException e) { 262 return null; 263 } 264 } 265 266 // Add an InputVerifier if we want to validate a particular pattern 267 public void setValidPattern(String string) { 268 if (string == null) { 269 return; 270 } 271 try { 272 Pattern newPattern = Pattern.compile(string); 273 setValidPattern(newPattern); 274 } catch (PatternSyntaxException e) { 275 } 276 } 277 278 // Add an InputVerifier if we want to validate a particular pattern 279 public void setValidPattern(Pattern pattern) { 280 if (pattern == null) { 281 this.validPattern = null; 282 if (this.validStrings == null) { 283 removeInputVerifier(); 284 } 285 } else { 286 this.validPattern = pattern; 287 addInputVerifier(); 288 } 289 } 290 291 // Add an InputVerifier if we want to validate a particular set of strings 292 public void setValidStrings(String... strings) { 293 if (strings == null) { 294 this.validStrings = null; 295 if (this.validPattern == null) { 296 removeInputVerifier(); 297 } 298 } else { 299 this.validStrings = strings; 300 addInputVerifier(); 301 } 302 } 303 304 private void addInputVerifier() { 305 this.setInputVerifier(new InputVerifier() { 306 @Override public boolean verify(JComponent comp) { 307 return verifyInput(); 308 } 309 310 @Override public boolean shouldYieldFocus(JComponent comp) { 311 boolean valid = verify(comp); 312 if (!valid) { 313 getToolkit().beep(); 314 } 315 return valid; 316 } 317 }); 318 verifyInput(); 319 } 320 321 private void removeInputVerifier() { 322 this.setInputVerifier(null); 323 } 324 325 private boolean verifyInput() { 326 boolean isValid = false; 327 String checkValue = this.getText(); 328 if (checkValue.isEmpty()) return true; 329 330 if (this.validStrings != null) { 331 for (String string : validStrings) { 332 if (checkValue.equals(string)) { 333 isValid = true; 334 } 335 } 336 } 337 338 if (this.validPattern != null) { 339 Matcher validMatch = this.validPattern.matcher(checkValue); 340 isValid = isValid || validMatch.matches(); 341 } 342 343 if (!isValid) { 344 this.selectAll(); 345 } 346 347 return isValid; 348 } 349 350 /** 351 * Extend PlainDocument to get the character validation features we require 352 */ 353 private class McVTextFieldDocument extends PlainDocument { 354 private int limit; 355 private boolean toUppercase = false; 356 private boolean hasPatterns = false; 357 private boolean useComplete = false; 358 private Pattern allow = Pattern.compile(".*"); 359 private Pattern deny = null; 360 361 public McVTextFieldDocument() { 362 super(); 363 } 364 365 public McVTextFieldDocument(int limit, boolean upper) { 366 super(); 367 setLimit(limit); 368 setUppercase(upper); 369 } 370 371 /** 372 * Apply the given {@code update} to the {@code offset} within the 373 * {@code original} string. 374 * 375 * @param original Text field contents before update. 376 * @param offset Offset within {@code original}. 377 * @param update Update to apply. 378 * 379 * @return String that represents text field contents after a 380 * {@link JTextField} change. 381 */ 382 private String makeComplete(String original, int offset, String update) 383 { 384 StringBuilder sb = 385 new StringBuilder(original.length() + update.length()); 386 // TODO(jon): probably a smarter way to do this... 387 if (offset >= original.length()) { 388 sb.append(original).append(update); 389 } else { 390 for (int i = 0; i < original.length(); i++) { 391 if (i == offset) { 392 sb.append(update); 393 } 394 sb.append(original.charAt(i)); 395 } 396 } 397 return sb.toString(); 398 } 399 400 public void insertString(int offset, String str, AttributeSet attr) 401 throws BadLocationException 402 { 403 if (str == null) { 404 return; 405 } 406 if (toUppercase) { 407 str = str.toUpperCase(); 408 } 409 410 String update = str; 411 if (useComplete) { 412 str = makeComplete(getText(0, getLength()), offset, str); 413 } 414 415 // Only allow certain patterns, and only check if we think we 416 // have patterns 417 if (hasPatterns) { 418 char[] characters = str.toCharArray(); 419 StringBuilder okString = new StringBuilder(characters.length); 420 for (char c : characters) { 421 String s = String.valueOf(c); 422 if (deny != null) { 423 Matcher denyMatch = deny.matcher(s); 424 if (denyMatch.matches()) { 425 continue; 426 } 427 } 428 if (allow != null) { 429 Matcher allowMatch = allow.matcher(s); 430 if (allowMatch.matches()) { 431 okString.append(s); 432 } 433 } 434 } 435 str = okString.toString(); 436 } 437 438 if (useComplete) { 439 str = update; 440 } 441 442 if (str.isEmpty()) { 443 return; 444 } 445 446 if ((getLength() + str.length()) <= limit || limit <= 0) { 447 super.insertString(offset, str, attr); 448 } 449 } 450 451 public int getLimit() { 452 return this.limit; 453 } 454 455 public void setLimit(int limit) { 456 this.limit = limit; 457 } 458 459 public boolean getUppercase() { 460 return this.toUppercase; 461 } 462 463 public void setUppercase(boolean uppercase) { 464 this.toUppercase = uppercase; 465 } 466 467 public void setAllow(Pattern newPattern) { 468 if (newPattern == null) { 469 return; 470 } 471 this.allow = newPattern; 472 hasPatterns = true; 473 } 474 475 public void setDeny(Pattern newPattern) { 476 if (newPattern == null) { 477 return; 478 } 479 this.deny = newPattern; 480 hasPatterns = true; 481 } 482 483 public void setUseComplete(boolean useComplete) { 484 this.useComplete = useComplete; 485 } 486 } 487 488 public static class Prompt extends JLabel implements FocusListener, 489 DocumentListener 490 { 491 492 public enum FocusBehavior { ALWAYS, FOCUS_GAINED, FOCUS_LOST } 493 494 private final JTextComponent component; 495 496 private final Document document; 497 498 private FocusBehavior focus; 499 500 private boolean showPromptOnce; 501 502 private int focusLost; 503 504 public Prompt(final JTextComponent component, final String text) { 505 this(component, FocusBehavior.FOCUS_LOST, text); 506 } 507 508 public Prompt(final JTextComponent component, 509 final FocusBehavior focusBehavior, final String text) 510 { 511 this.component = component; 512 setFocusBehavior(focusBehavior); 513 514 document = component.getDocument(); 515 516 setText(text); 517 setFont(component.getFont()); 518 setForeground(component.getForeground()); 519 setHorizontalAlignment(JLabel.LEADING); 520 setEnabled(false); 521 522 component.addFocusListener(this); 523 document.addDocumentListener(this); 524 525 component.setLayout(new BorderLayout()); 526 component.add(this); 527 checkForPrompt(); 528 } 529 530 public FocusBehavior getFocusBehavior() { 531 return focus; 532 } 533 534 public void setFocusBehavior(final FocusBehavior focus) { 535 this.focus = focus; 536 } 537 538 public boolean getShowPromptOnce() { 539 return showPromptOnce; 540 } 541 542 public void setShowPromptOnce(final boolean showPromptOnce) { 543 this.showPromptOnce = showPromptOnce; 544 } 545 546 /** 547 * Check whether the prompt should be visible or not. The visibility 548 * will change on updates to the Document and on focus changes. 549 */ 550 private void checkForPrompt() { 551 // text has been entered, remove the prompt 552 if (document.getLength() > 0) { 553 setVisible(false); 554 return; 555 } 556 557 // prompt has already been shown once, remove it 558 if (showPromptOnce && focusLost > 0) { 559 setVisible(false); 560 return; 561 } 562 563 // check the behavior property and component focus to determine if the 564 // prompt should be displayed. 565 if (component.hasFocus()) { 566 if ((focus == FocusBehavior.ALWAYS) || 567 (focus == FocusBehavior.FOCUS_GAINED)) 568 { 569 setVisible(true); 570 } else { 571 setVisible(false); 572 } 573 } else { 574 if ((focus == FocusBehavior.ALWAYS) || 575 (focus == FocusBehavior.FOCUS_LOST)) 576 { 577 setVisible(true); 578 } else { 579 setVisible(false); 580 } 581 } 582 } 583 584 // from FocusListener 585 @Override public void focusGained(FocusEvent e) { 586 checkForPrompt(); 587 } 588 589 @Override public void focusLost(FocusEvent e) { 590 focusLost++; 591 checkForPrompt(); 592 } 593 594 // from DocumentListener 595 @Override public void insertUpdate(DocumentEvent e) { 596 checkForPrompt(); 597 } 598 599 @Override public void removeUpdate(DocumentEvent e) { 600 checkForPrompt(); 601 } 602 603 @Override public void changedUpdate(DocumentEvent e) {} 604 } 605}