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