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.data.dateChooser;
030
031import java.awt.Color;
032import java.awt.Dimension;
033import java.awt.event.ActionEvent;
034import java.awt.event.ActionListener;
035import java.awt.event.FocusEvent;
036import java.awt.event.FocusListener;
037import java.text.DateFormat;
038import java.text.ParseException;
039import java.text.SimpleDateFormat;
040import java.util.Calendar;
041import java.util.Date;
042import java.util.Locale;
043
044import javax.swing.JComponent;
045import javax.swing.JFormattedTextField;
046import javax.swing.JFrame;
047import javax.swing.JTextField;
048import javax.swing.UIManager;
049import javax.swing.event.CaretEvent;
050import javax.swing.event.CaretListener;
051import javax.swing.text.MaskFormatter;
052
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055
056/**
057 * JTextFieldDateEditor is the default editor used by JDateChooser. It is a
058 * formatted text field, that colors valid dates green/black and invalid dates
059 * red. The date format pattern and mask can be set manually. If not set, the
060 * MEDIUM pattern of a SimpleDateFormat with regards to the actual locale is
061 * used.
062 * 
063 * @author Kai Toedter
064 * @version $LastChangedRevision: 97 $
065 * @version $LastChangedDate: 2006-05-24 17:30:41 +0200 (Mi, 24 Mai 2006) $
066 */
067
068public class JTextFieldDateEditor extends JFormattedTextField implements IDateEditor,
069                CaretListener, FocusListener, ActionListener {
070
071        private static final Logger logger =
072                LoggerFactory.getLogger(JTextFieldDateEditor.class);
073        
074        private static final long serialVersionUID = -8901842591101625304L;
075
076        protected Date date;
077
078        protected SimpleDateFormat dateFormatter;
079
080        protected MaskFormatter maskFormatter;
081
082        protected String datePattern;
083
084        protected String maskPattern;
085
086        protected char placeholder;
087
088        protected Color darkGreen;
089
090        protected DateUtil dateUtil;
091
092        private boolean isMaskVisible;
093
094        private boolean ignoreDatePatternChange;
095
096        private int hours;
097
098        private int minutes;
099
100        private int seconds;
101
102        private int millis;
103
104        private Calendar calendar;
105
106        public JTextFieldDateEditor() {
107                this(false, null, null, ' ');
108        }
109
110        public JTextFieldDateEditor(String datePattern, String maskPattern, char placeholder) {
111                this(true, datePattern, maskPattern, placeholder);
112        }
113
114        public JTextFieldDateEditor(boolean showMask, String datePattern, String maskPattern,
115                        char placeholder) {
116                dateFormatter = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.MEDIUM);
117                dateFormatter.setLenient(false);
118
119                setDateFormatString(datePattern);
120                if (datePattern != null) {
121                        ignoreDatePatternChange = true;
122                }
123
124                this.placeholder = placeholder;
125
126                if (maskPattern == null) {
127                        this.maskPattern = createMaskFromDatePattern(this.datePattern);
128                } else {
129                        this.maskPattern = maskPattern;
130                }
131
132                setToolTipText(this.datePattern);
133                setEditable(false);
134                setMaskVisible(showMask);
135
136                addCaretListener(this);
137                addFocusListener(this);
138                addActionListener(this);
139                darkGreen = new Color(0, 150, 0);
140
141                calendar = Calendar.getInstance();
142
143                dateUtil = new DateUtil();
144        }
145
146        /*
147         * (non-Javadoc)
148         * 
149         * @see com.toedter.calendar.IDateEditor#getDate()
150         */
151        public Date getDate() {
152                try {
153                        calendar.setTime(dateFormatter.parse(getText()));
154                        calendar.set(Calendar.HOUR_OF_DAY, hours);
155                        calendar.set(Calendar.MINUTE, minutes);
156                        calendar.set(Calendar.SECOND, seconds);
157                        calendar.set(Calendar.MILLISECOND, millis);
158                        date = calendar.getTime();
159                } catch (ParseException e) {
160                        date = null;
161                }
162                return date;
163        }
164
165        /*
166         * (non-Javadoc)
167         * 
168         * @see com.toedter.calendar.IDateEditor#setDate(java.util.Date)
169         */
170        public void setDate(Date date) {
171                setDate(date, true);
172        }
173
174        /**
175         * Sets the date.
176         * 
177         * @param date
178         *            the date
179         * @param firePropertyChange
180         *            true, if the date property should be fired.
181         */
182        
183        protected void setDate(Date date, boolean firePropertyChange) {
184                Date oldDate = this.date;
185                this.date = date;
186
187                if (date == null) {
188                        setText("");
189                } else {
190                        calendar.setTime(date);
191                        hours = calendar.get(Calendar.HOUR_OF_DAY);
192                        minutes = calendar.get(Calendar.MINUTE);
193                        seconds = calendar.get(Calendar.SECOND);
194                        millis = calendar.get(Calendar.MILLISECOND);
195
196                        String formattedDate = dateFormatter.format(date);
197                        try {
198                                setText(formattedDate);
199                        } catch (RuntimeException e) {
200                                String msg = String.format("Problem setting text to '%s'", formattedDate);
201                                logger.error(msg, e);
202                        }
203                }
204                if (date != null && dateUtil.checkDate(date)) {
205                        setForeground(Color.BLACK);
206
207                }
208
209                if (firePropertyChange) {
210                        firePropertyChange("date", oldDate, date);
211                }
212        }
213
214        /*
215         * (non-Javadoc)
216         * 
217         * @see com.toedter.calendar.IDateEditor#setDateFormatString(java.lang.String)
218         */
219        
220        public void setDateFormatString(String dateFormatString) {
221                if (ignoreDatePatternChange) {
222                        return;
223                }
224
225                try {
226                        dateFormatter.applyPattern(dateFormatString);
227                } catch (RuntimeException e) {
228                        dateFormatter = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.MEDIUM);
229                        dateFormatter.setLenient(false);
230                }
231                this.datePattern = dateFormatter.toPattern();
232                setToolTipText(this.datePattern);
233                setDate(date, false);
234        }
235
236        /*
237         * (non-Javadoc)
238         * 
239         * @see com.toedter.calendar.IDateEditor#getDateFormatString()
240         */
241        
242        public String getDateFormatString() {
243                return datePattern;
244        }
245
246        /*
247         * (non-Javadoc)
248         * 
249         * @see com.toedter.calendar.IDateEditor#getUiComponent()
250         */
251        
252        public JComponent getUiComponent() {
253                return this;
254        }
255
256        /**
257         * After any user input, the value of the textfield is proofed. Depending on
258         * being a valid date, the value is colored green or red.
259         * 
260         * @param event
261         *            the caret event
262         */
263        
264        public void caretUpdate(CaretEvent event) {
265                String text = getText().trim();
266                String emptyMask = maskPattern.replace('#', placeholder);
267
268                if (text.length() == 0 || text.equals(emptyMask)) {
269                        setForeground(Color.BLACK);
270                        return;
271                }
272
273                try {
274                        Date date = dateFormatter.parse(getText());
275                        if (dateUtil.checkDate(date)) {
276                                setForeground(darkGreen);
277                        } else {
278                                setForeground(Color.RED);
279                        }
280                } catch (Exception e) {
281                        setForeground(Color.RED);
282                }
283        }
284
285        /*
286         * (non-Javadoc)
287         * 
288         * @see java.awt.event.FocusListener#focusLost(java.awt.event.FocusEvent)
289         */
290        
291        public void focusLost(FocusEvent focusEvent) {
292                checkText();
293        }
294
295        private void checkText() {
296                try {
297                        Date date = dateFormatter.parse(getText());
298                        setDate(date, true);
299                } catch (Exception e) {
300                        // ignore
301                }
302        }
303
304        /*
305         * (non-Javadoc)
306         * 
307         * @see java.awt.event.FocusListener#focusGained(java.awt.event.FocusEvent)
308         */
309        
310        public void focusGained(FocusEvent e) {
311        }
312
313        /*
314         * (non-Javadoc)
315         * 
316         * @see java.awt.Component#setLocale(java.util.Locale)
317         */
318        
319        public void setLocale(Locale locale) {
320                if (locale == getLocale() || ignoreDatePatternChange) {
321                        return;
322                }
323
324                super.setLocale(locale);
325                dateFormatter = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
326                setToolTipText(dateFormatter.toPattern());
327
328                setDate(date, false);
329                doLayout();
330        }
331
332        /**
333         * Creates a mask from a date pattern. This is a very simple (and
334         * incomplete) implementation that works only with numbers. A date pattern
335         * of "MM/dd/yy" will result in the mask "##/##/##". Probably you want to
336         * override this method if it does not fit your needs.
337         * 
338         * @param datePattern
339         *            the date pattern
340         * @return the mask
341         */
342        
343        public String createMaskFromDatePattern(String datePattern) {
344                String symbols = "GyMdkHmsSEDFwWahKzZ";
345                String mask = "";
346                for (int i = 0; i < datePattern.length(); i++) {
347                        char ch = datePattern.charAt(i);
348                        boolean symbolFound = false;
349                        for (int n = 0; n < symbols.length(); n++) {
350                                if (symbols.charAt(n) == ch) {
351                                        mask += "#";
352                                        symbolFound = true;
353                                        break;
354                                }
355                        }
356                        if (!symbolFound) {
357                                mask += ch;
358                        }
359                }
360                return mask;
361        }
362
363        /**
364         * Returns true, if the mask is visible.
365         * 
366         * @return true, if the mask is visible
367         */
368        
369        public boolean isMaskVisible() {
370                return isMaskVisible;
371        }
372
373        /**
374         * Sets the mask visible.
375         * 
376         * @param isMaskVisible
377         *            true, if the mask should be visible
378         */
379        
380        public void setMaskVisible(boolean isMaskVisible) {
381                this.isMaskVisible = isMaskVisible;
382                if (isMaskVisible) {
383                        if (maskFormatter == null) {
384                                try {
385                                        maskFormatter = new MaskFormatter(createMaskFromDatePattern(datePattern));
386                                        maskFormatter.setPlaceholderCharacter(this.placeholder);
387                                        maskFormatter.install(this);
388                                } catch (ParseException e) {
389                                        logger.trace("Could not parse", e);
390                                }
391                        }
392                }
393        }
394
395        /**
396         * Returns the preferred size. If a date pattern is set, it is the size the
397         * date pattern would take.
398         */
399        
400        public Dimension getPreferredSize() {
401
402                Dimension ret = null;
403                if (datePattern != null) {
404                        ret = (new JTextField(datePattern)).getPreferredSize();
405                        // return new JTextField(datePattern).getPreferredSize();
406                } else {
407                        ret = super.getPreferredSize();
408                        // return super.getPreferredSize();
409                }
410                ret.setSize(90, 19);
411                return ret;
412                
413        }
414
415        /**
416         * Validates the typed date and sets it (only if it is valid).
417         */
418        public void actionPerformed(ActionEvent e) {
419                checkText();
420        }
421
422        /**
423         * Enables and disabled the component. It also fixes the background bug
424         * 4991597 and sets the background explicitly to a
425         * TextField.inactiveBackground.
426         */
427        
428        public void setEnabled(boolean b) {
429                super.setEnabled(b);
430                if (!b) {
431                        super.setBackground(UIManager.getColor("TextField.inactiveBackground"));
432                }
433        }
434
435        /*
436         * (non-Javadoc)
437         * 
438         * @see com.toedter.calendar.IDateEditor#getMaxSelectableDate()
439         */
440        public Date getMaxSelectableDate() {
441                return dateUtil.getMaxSelectableDate();
442        }
443
444        /*
445         * (non-Javadoc)
446         * 
447         * @see com.toedter.calendar.IDateEditor#getMinSelectableDate()
448         */
449        public Date getMinSelectableDate() {
450                return dateUtil.getMinSelectableDate();
451        }
452
453        /*
454         * (non-Javadoc)
455         * 
456         * @see com.toedter.calendar.IDateEditor#setMaxSelectableDate(java.util.Date)
457         */
458        public void setMaxSelectableDate(Date max) {
459                dateUtil.setMaxSelectableDate(max);
460                checkText();
461        }
462
463        /*
464         * (non-Javadoc)
465         * 
466         * @see com.toedter.calendar.IDateEditor#setMinSelectableDate(java.util.Date)
467         */
468        public void setMinSelectableDate(Date min) {
469                dateUtil.setMinSelectableDate(min);
470                checkText();
471        }
472
473        /*
474         * (non-Javadoc)
475         * 
476         * @see com.toedter.calendar.IDateEditor#setSelectableDateRange(java.util.Date,
477         *      java.util.Date)
478         */
479        public void setSelectableDateRange(Date min, Date max) {
480                dateUtil.setSelectableDateRange(min, max);
481                checkText();
482        }
483
484        /**
485         * Creates a JFrame with a JCalendar inside and can be used for testing.
486         * 
487         * @param s
488         *            The command line arguments
489         */
490        public static void main(String[] s) {
491                JFrame frame = new JFrame("JTextFieldDateEditor");
492                JTextFieldDateEditor jTextFieldDateEditor = new JTextFieldDateEditor();
493                jTextFieldDateEditor.setDate(new Date());
494                frame.getContentPane().add(jTextFieldDateEditor);
495                frame.pack();
496                frame.setVisible(true);
497        }
498}