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}