001/*
002 * $Id: McVTextField.java,v 1.12 2011/03/24 16:06:35 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
007 * Space Science and Engineering Center (SSEC)
008 * University of Wisconsin - Madison
009 * 1225 W. Dayton Street, Madison, WI 53706, USA
010 * https://www.ssec.wisc.edu/mcidas
011 * 
012 * All Rights Reserved
013 * 
014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015 * some McIDAS-V source code is based on IDV and VisAD source code.  
016 * 
017 * McIDAS-V is free software; you can redistribute it and/or modify
018 * it under the terms of the GNU Lesser Public License as published by
019 * the Free Software Foundation; either version 3 of the License, or
020 * (at your option) any later version.
021 * 
022 * McIDAS-V is distributed in the hope that it will be useful,
023 * but WITHOUT ANY WARRANTY; without even the implied warranty of
024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025 * GNU Lesser Public License for more details.
026 * 
027 * You should have received a copy of the GNU Lesser Public License
028 * along with this program.  If not, see http://www.gnu.org/licenses.
029 */
030
031package edu.wisc.ssec.mcidasv.util;
032
033import java.awt.BorderLayout;
034import java.awt.Color;
035import java.awt.event.FocusEvent;
036import java.awt.event.FocusListener;
037import java.util.regex.Matcher;
038import java.util.regex.Pattern;
039import java.util.regex.PatternSyntaxException;
040
041import javax.swing.InputVerifier;
042import javax.swing.JComponent;
043import javax.swing.JLabel;
044import javax.swing.JTextField;
045import javax.swing.SwingConstants;
046import javax.swing.border.EmptyBorder;
047import javax.swing.event.DocumentEvent;
048import javax.swing.event.DocumentListener;
049import javax.swing.text.AttributeSet;
050import javax.swing.text.BadLocationException;
051import javax.swing.text.Document;
052import javax.swing.text.JTextComponent;
053import javax.swing.text.PlainDocument;
054
055/**
056 * Extend JTextField to add niceties such as uppercase,
057 * length limits, and allow/deny character sets
058 */
059public class McVTextField extends JTextField {
060        
061        public static char[] mcidasDeny = new char[] { '/', '.', ' ', '[', ']', '%' };
062        
063        public static Pattern ipAddress = Pattern.compile("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}");
064
065        private McVTextFieldDocument document = new McVTextFieldDocument();
066        
067        private Pattern validPattern;
068        private String[] validStrings;
069        
070        public McVTextField() {
071                this("", 0, false);
072        }
073        
074        public McVTextField(String defaultString) {
075                this(defaultString, 0, false);
076        }
077        
078        public McVTextField(String defaultString, int limit) {
079                this(defaultString, limit, false);
080        }
081        
082        public McVTextField(String defaultString, boolean upper) {
083                this(defaultString, 0, upper);
084        }
085        
086        // All other constructors call this one
087        public McVTextField(String defaultString, int limit, boolean upper) {
088                super(limit);
089                this.document = new McVTextFieldDocument(limit, upper);
090                super.setDocument(document);
091                this.setText(defaultString);
092        }
093        
094        public McVTextField(String defaultString, int limit, boolean upper, String allow, String deny) {
095                this(defaultString, limit, upper);
096                setAllow(makePattern(allow));
097                setDeny(makePattern(deny));
098        }
099        
100        public McVTextField(String defaultString, int limit, boolean upper, char[] allow, char[] deny) {
101                this(defaultString, limit, upper);
102                setAllow(makePattern(allow));
103                setDeny(makePattern(deny));
104        }
105        
106        public McVTextField(String defaultString, int limit, boolean upper, Pattern allow, Pattern deny) {
107                this(defaultString, limit, upper);
108                setAllow(allow);
109                setDeny(deny);
110        }
111
112        public int getLimit() {
113                return this.document.getLimit();
114        }
115        
116        public void setLimit(int limit) {
117                this.document.setLimit(limit);
118                super.setDocument(document);
119        }
120        
121        public boolean getUppercase() {
122                return this.document.getUppercase();
123        }
124        
125        public void setUppercase(boolean uppercase) {
126                this.document.setUppercase(uppercase);
127                super.setDocument(document);
128        }
129                
130        public void setAllow(String string) {
131                this.document.setAllow(makePattern(string));
132                super.setDocument(document);
133        }
134        
135        public void setDeny(String string) {
136                this.document.setDeny(makePattern(string));
137                super.setDocument(document);
138        }
139                
140        public void setAllow(char[] characters) {
141                this.document.setAllow(makePattern(characters));
142                super.setDocument(document);
143        }
144        
145        public void setDeny(char[] characters) {
146                this.document.setDeny(makePattern(characters));
147                super.setDocument(document);
148        }
149        
150        public void setAllow(Pattern newPattern) {
151                this.document.setAllow(newPattern);
152                super.setDocument(document);
153        }
154        
155        public void setDeny(Pattern newPattern) {
156                this.document.setDeny(newPattern);
157                super.setDocument(document);
158        }
159        
160        // Take a string and turn it into a pattern
161        private Pattern makePattern(String string) {
162                if (string == null) return null;
163                try {
164                        return Pattern.compile(string);
165                }
166                catch (PatternSyntaxException e) {
167                        return null;
168                }
169        }
170        
171        // Take a character array and turn it into a [abc] class pattern
172        private Pattern makePattern(char[] characters) {
173                if (characters == null) return null;
174                String string = ".*";
175                if (characters.length > 0) {
176                        string = "[";
177                        for (char c : characters) {
178                                if (c == '[') string += "\\[";
179                                else if (c == ']') string += "\\]";
180                                else if (c == '\\') string += "\\\\";
181                                else string += c;
182                        }
183                        string += "]";
184                }
185                try {
186                        return Pattern.compile(string);
187                }
188                catch (PatternSyntaxException e) {
189                        return null;
190                }
191        }
192        
193        // Add an InputVerifier if we want to validate a particular pattern
194        public void setValidPattern(String string) {
195                if (string == null) return;
196                try {
197                        Pattern newPattern = Pattern.compile(string);
198                        setValidPattern(newPattern);
199                }
200                catch (PatternSyntaxException e) {
201                        return;
202                }
203        }
204        
205        // Add an InputVerifier if we want to validate a particular pattern
206        public void setValidPattern(Pattern pattern) {
207                if (pattern == null) {
208                        this.validPattern = null;
209                        if (this.validStrings == null) removeInputVerifier();
210                }
211                else {
212                        this.validPattern = pattern;
213                        addInputVerifier();
214                }
215        }
216        
217        // Add an InputVerifier if we want to validate a particular set of strings
218        public void setValidStrings(String[] strings) {
219                if (strings == null) {
220                        this.validStrings = null;
221                        if (this.validPattern == null) removeInputVerifier();
222                }
223                else {
224                        this.validStrings = strings;
225                        addInputVerifier();
226                }
227        }
228        
229        private void addInputVerifier() {
230                this.setInputVerifier(new InputVerifier() {
231                        public boolean verify(JComponent comp) {
232                                return verifyInput();
233                        }
234                        public boolean shouldYieldFocus(JComponent comp) {
235                                boolean valid = verify(comp);
236                                if (!valid) {
237                                        getToolkit().beep();
238                                }
239                                return valid;
240                        }
241                });
242                verifyInput();
243        }
244        
245        private void removeInputVerifier() {
246                this.setInputVerifier(null);
247        }
248        
249        private boolean verifyInput() {
250                boolean isValid = false;
251                String checkValue = this.getText();
252                if (checkValue=="") return true;
253                
254                if (this.validStrings != null) {
255                        for (String string : validStrings) {
256                                if (checkValue.equals(string)) isValid = true;
257                        }
258                }
259                
260                if (this.validPattern != null) {
261                        Matcher validMatch = this.validPattern.matcher(checkValue);
262                        isValid = isValid || validMatch.matches();
263                }
264                
265                if (!isValid) {
266                        this.selectAll();
267                }
268                return isValid;
269        }
270        
271        /**
272         * Extend PlainDocument to get the character validation features we require
273         */
274        private class McVTextFieldDocument extends PlainDocument {
275                private int limit;
276                private boolean toUppercase = false;            
277                private boolean hasPatterns = false;
278                private Pattern allow = Pattern.compile(".*");
279                private Pattern deny = null;
280                                                                                
281                public McVTextFieldDocument() {
282                        super();
283                }
284                                
285                public McVTextFieldDocument(int limit, boolean upper) {
286                        super();
287                        setLimit(limit);
288                        setUppercase(upper);
289                }
290
291                public void insertString(int offset, String str, AttributeSet attr) throws BadLocationException {
292                        if (str == null) return;
293                        if (toUppercase) str = str.toUpperCase();
294                                                
295                        // Only allow certain patterns, and only check if we think we have patterns
296                        if (hasPatterns) {
297                                char[] characters = str.toCharArray();
298                                String okString = "";
299                                for (char c : characters) {
300                                        String s = "" + c;
301                                        if (deny != null) {
302                                                Matcher denyMatch = deny.matcher(s);
303                                                if (denyMatch.matches()) continue;
304                                        }
305                                        if (allow != null) {
306                                                Matcher allowMatch = allow.matcher(s);
307                                                if (allowMatch.matches()) okString += s;
308                                        }
309                                }
310                                str = okString;
311                        }
312                        if (str.equals("")) return;
313
314                        if ((getLength() + str.length()) <= limit || limit <= 0) {
315                                super.insertString(offset, str, attr);
316                        }
317                }
318                
319                public int getLimit() {
320                        return this.limit;
321                }
322                
323                public void setLimit(int limit) {
324                        this.limit = limit;
325                }
326                
327                public boolean getUppercase() {
328                        return this.toUppercase;
329                }
330                
331                public void setUppercase(boolean uppercase) {
332                        this.toUppercase = uppercase;
333                }
334                                        
335                public void setAllow(Pattern newPattern) {
336                        if (newPattern==null) return;
337                        this.allow = newPattern;
338                        hasPatterns = true;
339                }
340                
341                public void setDeny(Pattern newPattern) {
342                        if (newPattern==null) return;
343                        this.deny = newPattern;
344                        hasPatterns = true;
345                }
346                
347        }
348
349        public static class Prompt extends JLabel implements FocusListener, DocumentListener {
350
351            public enum FocusBehavior { ALWAYS, FOCUS_GAINED, FOCUS_LOST };
352
353            private final JTextComponent component;
354
355            private final Document document;
356
357            private FocusBehavior focus;
358
359            private boolean showPromptOnce;
360
361            private int focusLost;
362
363            public Prompt(final JTextComponent component, final String text) {
364                this(component, FocusBehavior.FOCUS_LOST, text);
365            }
366
367            public Prompt(final JTextComponent component, final FocusBehavior focusBehavior, final String text) {
368                this.component = component;
369                setFocusBehavior(focusBehavior);
370
371                document = component.getDocument();
372
373                setText(text);
374                setFont(component.getFont());
375                setForeground(component.getForeground());
376                setHorizontalAlignment(JLabel.LEADING);
377                setEnabled(false);
378
379                component.addFocusListener(this);
380                document.addDocumentListener(this);
381
382                component.setLayout(new BorderLayout());
383                component.add(this);
384                checkForPrompt();
385            }
386
387            public FocusBehavior getFocusBehavior() {
388                return focus;
389            }
390
391            public void setFocusBehavior(final FocusBehavior focus) {
392                this.focus = focus;
393            }
394
395            public boolean getShowPromptOnce() {
396                return showPromptOnce;
397            }
398
399            public void setShowPromptOnce(final boolean showPromptOnce) {
400                this.showPromptOnce = showPromptOnce;
401            }
402
403            /**
404             * Check whether the prompt should be visible or not. The visibility
405             * will change on updates to the Document and on focus changes.
406             */
407            private void checkForPrompt() {
408                // text has been entered, remove the prompt
409                if (document.getLength() > 0) {
410                    setVisible(false);
411                    return;
412                }
413
414                // prompt has already been shown once, remove it
415                if (showPromptOnce && focusLost > 0) {
416                    setVisible(false);
417                    return;
418                }
419
420                // check the behavior property and component focus to determine if the
421                // prompt should be displayed.
422                if (component.hasFocus()) {
423                    if (focus == FocusBehavior.ALWAYS || focus == FocusBehavior.FOCUS_GAINED) {
424                        setVisible(true);
425                    } else {
426                        setVisible(false);
427                    }
428                } else {
429                    if (focus == FocusBehavior.ALWAYS || focus == FocusBehavior.FOCUS_LOST) {
430                        setVisible( true);
431                    } else {
432                        setVisible(false);
433                    }
434                }
435            }
436
437            // from FocusListener
438            public void focusGained(FocusEvent e) {
439                checkForPrompt();
440            }
441
442            public void focusLost(FocusEvent e) {
443                focusLost++;
444                checkForPrompt();
445            }
446
447            // from DocumentListener
448            public void insertUpdate(DocumentEvent e) {
449                checkForPrompt();
450            }
451
452            public void removeUpdate(DocumentEvent e) {
453                checkForPrompt();
454            }
455
456            public void changedUpdate(DocumentEvent e) {}
457        }
458}