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 // McIDAS Inquiry #3121-3141 345 // This might be the culprit! 346 347 // this.selectAll(); 348 349 // It's hard to be sure because the bug is not easy to reproduce 350 // but it seems like the most likely source of the issue 351 } 352 353 return isValid; 354 } 355 356 /** 357 * Extend PlainDocument to get the character validation features we require 358 */ 359 private class McVTextFieldDocument extends PlainDocument { 360 private int limit; 361 private boolean toUppercase = false; 362 private boolean hasPatterns = false; 363 private boolean useComplete = false; 364 private Pattern allow = Pattern.compile(".*"); 365 private Pattern deny = null; 366 367 public McVTextFieldDocument() { 368 super(); 369 } 370 371 public McVTextFieldDocument(int limit, boolean upper) { 372 super(); 373 setLimit(limit); 374 setUppercase(upper); 375 } 376 377 /** 378 * Apply the given {@code update} to the {@code offset} within the 379 * {@code original} string. 380 * 381 * @param original Text field contents before update. 382 * @param offset Offset within {@code original}. 383 * @param update Update to apply. 384 * 385 * @return String that represents text field contents after a 386 * {@link JTextField} change. 387 */ 388 private String makeComplete(String original, int offset, String update) 389 { 390 StringBuilder sb = 391 new StringBuilder(original.length() + update.length()); 392 // TODO(jon): probably a smarter way to do this... 393 if (offset >= original.length()) { 394 sb.append(original).append(update); 395 } else { 396 for (int i = 0; i < original.length(); i++) { 397 if (i == offset) { 398 sb.append(update); 399 } 400 sb.append(original.charAt(i)); 401 } 402 } 403 return sb.toString(); 404 } 405 406 public void insertString(int offset, String str, AttributeSet attr) 407 throws BadLocationException 408 { 409 if (str == null) { 410 return; 411 } 412 if (toUppercase) { 413 str = str.toUpperCase(); 414 } 415 416 String update = str; 417 if (useComplete) { 418 str = makeComplete(getText(0, getLength()), offset, str); 419 } 420 421 // Only allow certain patterns, and only check if we think we 422 // have patterns 423 if (hasPatterns) { 424 char[] characters = str.toCharArray(); 425 StringBuilder okString = new StringBuilder(characters.length); 426 for (char c : characters) { 427 String s = String.valueOf(c); 428 if (deny != null) { 429 Matcher denyMatch = deny.matcher(s); 430 if (denyMatch.matches()) { 431 continue; 432 } 433 } 434 if (allow != null) { 435 Matcher allowMatch = allow.matcher(s); 436 if (allowMatch.matches()) { 437 okString.append(s); 438 } 439 } 440 } 441 str = okString.toString(); 442 } 443 444 if (useComplete) { 445 str = update; 446 } 447 448 if (str.isEmpty()) { 449 return; 450 } 451 452 if ((getLength() + str.length()) <= limit || limit <= 0) { 453 super.insertString(offset, str, attr); 454 } 455 } 456 457 public int getLimit() { 458 return this.limit; 459 } 460 461 public void setLimit(int limit) { 462 this.limit = limit; 463 } 464 465 public boolean getUppercase() { 466 return this.toUppercase; 467 } 468 469 public void setUppercase(boolean uppercase) { 470 this.toUppercase = uppercase; 471 } 472 473 public void setAllow(Pattern newPattern) { 474 if (newPattern == null) { 475 return; 476 } 477 this.allow = newPattern; 478 hasPatterns = true; 479 } 480 481 public void setDeny(Pattern newPattern) { 482 if (newPattern == null) { 483 return; 484 } 485 this.deny = newPattern; 486 hasPatterns = true; 487 } 488 489 public void setUseComplete(boolean useComplete) { 490 this.useComplete = useComplete; 491 } 492 } 493 494 public static class Prompt extends JLabel implements FocusListener, 495 DocumentListener 496 { 497 498 public enum FocusBehavior { ALWAYS, FOCUS_GAINED, FOCUS_LOST } 499 500 private final JTextComponent component; 501 502 private final Document document; 503 504 private FocusBehavior focus; 505 506 private boolean showPromptOnce; 507 508 private int focusLost; 509 510 public Prompt(final JTextComponent component, final String text) { 511 this(component, FocusBehavior.FOCUS_LOST, text); 512 } 513 514 public Prompt(final JTextComponent component, 515 final FocusBehavior focusBehavior, final String text) 516 { 517 this.component = component; 518 setFocusBehavior(focusBehavior); 519 520 document = component.getDocument(); 521 522 setText(text); 523 setFont(component.getFont()); 524 setForeground(component.getForeground()); 525 setHorizontalAlignment(JLabel.LEADING); 526 setEnabled(false); 527 528 component.addFocusListener(this); 529 document.addDocumentListener(this); 530 531 component.setLayout(new BorderLayout()); 532 component.add(this); 533 checkForPrompt(); 534 } 535 536 public FocusBehavior getFocusBehavior() { 537 return focus; 538 } 539 540 public void setFocusBehavior(final FocusBehavior focus) { 541 this.focus = focus; 542 } 543 544 public boolean getShowPromptOnce() { 545 return showPromptOnce; 546 } 547 548 public void setShowPromptOnce(final boolean showPromptOnce) { 549 this.showPromptOnce = showPromptOnce; 550 } 551 552 /** 553 * Check whether the prompt should be visible or not. The visibility 554 * will change on updates to the Document and on focus changes. 555 */ 556 private void checkForPrompt() { 557 // text has been entered, remove the prompt 558 if (document.getLength() > 0) { 559 setVisible(false); 560 return; 561 } 562 563 // prompt has already been shown once, remove it 564 if (showPromptOnce && focusLost > 0) { 565 setVisible(false); 566 return; 567 } 568 569 // check the behavior property and component focus to determine if the 570 // prompt should be displayed. 571 if (component.hasFocus()) { 572 if ((focus == FocusBehavior.ALWAYS) || 573 (focus == FocusBehavior.FOCUS_GAINED)) 574 { 575 setVisible(true); 576 } else { 577 setVisible(false); 578 } 579 } else { 580 if ((focus == FocusBehavior.ALWAYS) || 581 (focus == FocusBehavior.FOCUS_LOST)) 582 { 583 setVisible(true); 584 } else { 585 setVisible(false); 586 } 587 } 588 } 589 590 // from FocusListener 591 @Override public void focusGained(FocusEvent e) { 592 checkForPrompt(); 593 } 594 595 @Override public void focusLost(FocusEvent e) { 596 focusLost++; 597 checkForPrompt(); 598 } 599 600 // from DocumentListener 601 @Override public void insertUpdate(DocumentEvent e) { 602 checkForPrompt(); 603 } 604 605 @Override public void removeUpdate(DocumentEvent e) { 606 checkForPrompt(); 607 } 608 609 @Override public void changedUpdate(DocumentEvent e) {} 610 } 611}