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