001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
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}