001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2015
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.data.dateChooser;
029
030import java.awt.BorderLayout;
031import java.awt.Font;
032import java.awt.Insets;
033import java.awt.event.ActionEvent;
034import java.awt.event.ActionListener;
035import java.awt.event.KeyEvent;
036import java.beans.PropertyChangeEvent;
037import java.beans.PropertyChangeListener;
038import java.net.URL;
039import java.util.Calendar;
040import java.util.Date;
041import java.util.Locale;
042
043import javax.swing.ImageIcon;
044import javax.swing.JButton;
045import javax.swing.JFrame;
046import javax.swing.JPanel;
047import javax.swing.JPopupMenu;
048import javax.swing.MenuElement;
049import javax.swing.MenuSelectionManager;
050import javax.swing.SwingUtilities;
051import javax.swing.event.ChangeEvent;
052import javax.swing.event.ChangeListener;
053
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056
057/**
058 * A date chooser containing a date editor and a button, that makes a JCalendar
059 * visible for choosing a date. If no date editor is specified, a
060 * JTextFieldDateEditor is used as default.
061 * 
062 * @author Kai Toedter
063 * @version $LastChangedRevision: 101 $
064 * @version $LastChangedDate: 2006-06-04 14:42:29 +0200 (So, 04 Jun 2006) $
065 */
066
067public class JDateChooser extends JPanel implements ActionListener,
068                PropertyChangeListener {
069
070        private static final Logger logger = LoggerFactory.getLogger(JDateChooser.class);
071        
072        private static final long serialVersionUID = -4306412745720670722L;
073
074        protected IDateEditor dateEditor;
075
076        protected JButton calendarButton;
077
078        protected JCalendar jcalendar;
079
080        protected JPopupMenu popup;
081
082        protected boolean isInitialized;
083
084        protected boolean dateSelected;
085
086        protected Date lastSelectedDate;
087
088        private ChangeListener changeListener;
089
090        /**
091         * Creates a new JDateChooser. By default, no date is set and the textfield
092         * is empty.
093         */
094        public JDateChooser() {
095                this(null, null, null, null);
096        }
097
098        /**
099         * Creates a new JDateChooser with given IDateEditor.
100         * 
101         * @param dateEditor
102         *            the dateEditor to be used used to display the date. if null, a
103         *            JTextFieldDateEditor is used.
104         */
105        public JDateChooser(IDateEditor dateEditor) {
106                this(null, null, null, dateEditor);
107        }
108
109        /**
110         * Creates a new JDateChooser.
111         * 
112         * @param date
113         *            the date or null
114         */
115        public JDateChooser(Date date) {
116                this(date, null);
117        }
118
119        /**
120         * Creates a new JDateChooser.
121         * 
122         * @param date
123         *            the date or null
124         * @param dateFormatString
125         *            the date format string or null (then MEDIUM SimpleDateFormat
126         *            format is used)
127         */
128        public JDateChooser(Date date, String dateFormatString) {
129                this(date, dateFormatString, null);
130        }
131
132        /**
133         * Creates a new JDateChooser.
134         * 
135         * @param date
136         *            the date or null
137         * @param dateFormatString
138         *            the date format string or null (then MEDIUM SimpleDateFormat
139         *            format is used)
140         * @param dateEditor
141         *            the dateEditor to be used used to display the date. if null, a
142         *            JTextFieldDateEditor is used.
143         */
144        public JDateChooser(Date date, String dateFormatString,
145                        IDateEditor dateEditor) {
146                this(null, date, dateFormatString, dateEditor);
147        }
148
149        /**
150         * Creates a new JDateChooser. If the JDateChooser is created with this
151         * constructor, the mask will be always visible in the date editor. Please
152         * note that the date pattern and the mask will not be changed if the locale
153         * of the JDateChooser is changed.
154         * 
155         * @param datePattern
156         *            the date pattern, e.g. "MM/dd/yy"
157         * @param maskPattern
158         *            the mask pattern, e.g. "##/##/##"
159         * @param placeholder
160         *            the placeholer charachter, e.g. '_'
161         */
162        public JDateChooser(String datePattern, String maskPattern, char placeholder) {
163                this(null, null, datePattern, new JTextFieldDateEditor(datePattern,
164                                maskPattern, placeholder));
165        }
166
167        /**
168         * Creates a new JDateChooser.
169         * 
170         * @param jcal
171         *            the JCalendar to be used
172         * @param date
173         *            the date or null
174         * @param dateFormatString
175         *            the date format string or null (then MEDIUM Date format is
176         *            used)
177         * @param dateEditor
178         *            the dateEditor to be used used to display the date. if null, a
179         *            JTextFieldDateEditor is used.
180         */
181        public JDateChooser(JCalendar jcal, Date date, String dateFormatString,
182                        IDateEditor dateEditor) {
183                setName("JDateChooser");
184
185                this.dateEditor = dateEditor;
186                if (this.dateEditor == null) {
187                        this.dateEditor = new JTextFieldDateEditor();
188                }
189                this.dateEditor.addPropertyChangeListener("date", this);
190
191                if (jcal == null) {
192                        jcalendar = new JCalendar(date);
193                } else {
194                        jcalendar = jcal;
195                        if (date != null) {
196                                jcalendar.setDate(date);
197                        }
198                }
199
200                setLayout(new BorderLayout());
201
202                jcalendar.getDayChooser().addPropertyChangeListener("day", this);
203                // always fire"day" property even if the user selects
204                // the already selected day again
205                jcalendar.getDayChooser().setAlwaysFireDayProperty(true);
206
207                setDateFormatString(dateFormatString);
208                setDate(date);
209
210                // Display a calendar button with an icon
211                URL iconURL = getClass().getResource(
212                                "/com/toedter/calendar/images/JDateChooserIcon.gif");
213                ImageIcon icon = new ImageIcon(iconURL);
214
215                calendarButton = new JButton(icon) {
216                        private static final long serialVersionUID = -1913767779079949668L;
217
218                        public boolean isFocusable() {
219                                return false;
220                        }
221                };
222                calendarButton.setMargin(new Insets(0, 0, 0, 0));
223                calendarButton.addActionListener(this);
224
225                // Alt + 'C' selects the calendar.
226                calendarButton.setMnemonic(KeyEvent.VK_C);
227
228                add(calendarButton, BorderLayout.EAST);
229                add(this.dateEditor.getUiComponent(), BorderLayout.CENTER);
230
231                calendarButton.setMargin(new Insets(0, 0, 0, 0));
232                // calendarButton.addFocusListener(this);
233
234                popup = new JPopupMenu() {
235                        private static final long serialVersionUID = -6078272560337577761L;
236
237                        public void setVisible(boolean b) {
238                                Boolean isCanceled = (Boolean) getClientProperty("JPopupMenu.firePopupMenuCanceled");
239                                if (b
240                                                || (!b && dateSelected)
241                                                || ((isCanceled != null) && !b && isCanceled
242                                                                .booleanValue())) {
243                                        super.setVisible(b);
244                                }
245                        }
246                };
247
248                popup.setLightWeightPopupEnabled(true);
249
250                popup.add(jcalendar);
251
252                lastSelectedDate = date;
253
254                // Corrects a problem that occured when the JMonthChooser's combobox is
255                // displayed, and a click outside the popup does not close it.
256
257                // The following idea was originally provided by forum user
258                // podiatanapraia:
259                changeListener = new ChangeListener() {
260                        boolean hasListened = false;
261
262                        public void stateChanged(ChangeEvent e) {
263                                if (hasListened) {
264                                        hasListened = false;
265                                        return;
266                                }
267                                if (popup.isVisible()
268                                                && JDateChooser.this.jcalendar.monthChooser
269                                                                .getComboBox().hasFocus()) {
270                                        MenuElement[] me = MenuSelectionManager.defaultManager()
271                                                        .getSelectedPath();
272                                        MenuElement[] newMe = new MenuElement[me.length + 1];
273                                        newMe[0] = popup;
274                                        for (int i = 0; i < me.length; i++) {
275                                                newMe[i + 1] = me[i];
276                                        }
277                                        hasListened = true;
278                                        MenuSelectionManager.defaultManager()
279                                                        .setSelectedPath(newMe);
280                                }
281                        }
282                };
283                MenuSelectionManager.defaultManager().addChangeListener(changeListener);
284                // end of code provided by forum user podiatanapraia
285
286                isInitialized = true;
287        }
288
289        /**
290         * Called when the jalendar button was pressed.
291         * 
292         * @param e
293         *            the action event
294         */
295        public void actionPerformed(ActionEvent e) {
296                int x = calendarButton.getWidth()
297                                - (int) popup.getPreferredSize().getWidth();
298                int y = calendarButton.getY() + calendarButton.getHeight();
299
300                Calendar calendar = Calendar.getInstance();
301                Date date = dateEditor.getDate();
302                if (date != null) {
303                        calendar.setTime(date);
304                }
305                jcalendar.setCalendar(calendar);
306                popup.show(calendarButton, x, y);
307                dateSelected = false;
308        }
309
310        /**
311         * Listens for a "date" property change or a "day" property change event
312         * from the JCalendar. Updates the date editor and closes the popup.
313         * 
314         * @param evt
315         *            the event
316         */
317        public void propertyChange(PropertyChangeEvent evt) {
318                
319                if (evt.getPropertyName().equals("day")) {
320                        if (popup.isVisible()) {
321                                dateSelected = true;
322                                popup.setVisible(false);
323                                setDate(jcalendar.getCalendar().getTime());
324                        }
325                } else if (evt.getPropertyName().equals("date")) {
326                        if (evt.getSource() == dateEditor) {
327                                firePropertyChange("date", evt.getOldValue(), evt.getNewValue());
328                        } else {
329                                setDate((Date) evt.getNewValue());
330                        }
331                }
332                
333                // if event source was a day chooser, may need to check start/end day bounds
334                if (evt.getSource() instanceof JDayChooser) {
335
336                        JDayChooser jdcSrc = (JDayChooser) evt.getSource();
337                        
338                        String srcName = jdcSrc.getName();
339                        String dstName = jcalendar.getDayChooser().getName();
340                        if ((srcName != null) && (dstName != null)) {
341                                
342                                // property event from start day chooser to end day chooser
343                                if (srcName.equals(JDayChooser.BEG_DAY) && (dstName.equals(JDayChooser.END_DAY))) {
344                                        JDayChooser jdcDst = jcalendar.getDayChooser();
345                                        Calendar srcCal = Calendar.getInstance();
346                                        srcCal.set(Calendar.YEAR, jdcSrc.getYear());
347                                        srcCal.set(Calendar.MONTH, jdcSrc.getMonth());
348                                        srcCal.set(Calendar.DAY_OF_MONTH, jdcSrc.getDay());
349                                        Calendar dstCal = Calendar.getInstance();
350                                        dstCal.set(Calendar.YEAR, jdcDst.getYear());
351                                        dstCal.set(Calendar.MONTH, jdcDst.getMonth());
352                                        dstCal.set(Calendar.DAY_OF_MONTH, jdcDst.getDay());
353                                        if (srcCal.after(dstCal)) {
354                                                logger.debug("Adjusting: Src date exceeds Dst date...");
355                                                jdcDst.setDay(srcCal.get(Calendar.DAY_OF_MONTH));
356                                                jdcDst.setMonth(srcCal.get(Calendar.MONTH));
357                                                jdcDst.setYear(srcCal.get(Calendar.YEAR) - 1900);
358                                                dateEditor.setDate(srcCal.getTime());
359                                                if (getParent() != null) {
360                                                        getParent().invalidate();
361                                                }
362                                        }
363                                }
364                                
365                                // property event from end day chooser to start day chooser
366                                if (srcName.equals(JDayChooser.END_DAY) && (dstName.equals(JDayChooser.BEG_DAY))) {
367                                        JDayChooser jdcDst = jcalendar.getDayChooser();
368                                        Calendar srcCal = Calendar.getInstance();
369                                        srcCal.set(Calendar.YEAR, jdcSrc.getYear());
370                                        srcCal.set(Calendar.MONTH, jdcSrc.getMonth());
371                                        srcCal.set(Calendar.DAY_OF_MONTH, jdcSrc.getDay());
372                                        Calendar dstCal = Calendar.getInstance();
373                                        dstCal.set(Calendar.YEAR, jdcDst.getYear());
374                                        dstCal.set(Calendar.MONTH, jdcDst.getMonth());
375                                        dstCal.set(Calendar.DAY_OF_MONTH, jdcDst.getDay());
376                                        if (srcCal.before(dstCal)) {
377                                                logger.debug("Adjusting: End date preceeds Src date...");
378                                                jdcDst.setDay(srcCal.get(Calendar.DAY_OF_MONTH));
379                                                jdcDst.setMonth(srcCal.get(Calendar.MONTH));
380                                                jdcDst.setYear(srcCal.get(Calendar.YEAR) - 1900);
381                                                dateEditor.setDate(srcCal.getTime());
382                                                if (getParent() != null) {
383                                                        getParent().invalidate();
384                                                }
385                                        }
386                                }
387                                
388                        }
389                }
390                
391        }
392
393        /**
394         * Updates the UI of itself and the popup.
395         */
396        public void updateUI() {
397                super.updateUI();
398                setEnabled(isEnabled());
399
400                if (jcalendar != null) {
401                        SwingUtilities.updateComponentTreeUI(popup);
402                }
403        }
404
405        /**
406         * Sets the locale.
407         * 
408         * @param l
409         *            The new locale value
410         */
411        public void setLocale(Locale l) {
412                super.setLocale(l);
413                dateEditor.setLocale(l);
414                jcalendar.setLocale(l);
415        }
416
417        /**
418         * @return the jcalendar
419         */
420        public JCalendar getJcalendar() {
421                return jcalendar;
422        }
423
424        /**
425         * Gets the date format string.
426         * 
427         * @return Returns the dateFormatString.
428         */
429        public String getDateFormatString() {
430                return dateEditor.getDateFormatString();
431        }
432
433        /**
434         * Sets the date format string. E.g "MMMMM d, yyyy" will result in "July 21,
435         * 2004" if this is the selected date and locale is English.
436         * 
437         * @param dfString
438         *            The dateFormatString to set.
439         */
440        public void setDateFormatString(String dfString) {
441                dateEditor.setDateFormatString(dfString);
442                invalidate();
443        }
444
445        /**
446         * Returns the date. If the JDateChooser is started with a null date and no
447         * date was set by the user, null is returned.
448         * 
449         * @return the current date
450         */
451        public Date getDate() {
452                return dateEditor.getDate();
453        }
454
455        /**
456         * Sets the date. Fires the property change "date" if date != null.
457         * 
458         * @param date
459         *            the new date.
460         */
461        public void setDate(Date date) {
462                dateEditor.setDate(date);
463                if (getParent() != null) {
464                        getParent().invalidate();
465                }
466        }
467
468        /**
469         * Returns the calendar. If the JDateChooser is started with a null date (or
470         * null calendar) and no date was set by the user, null is returned.
471         * 
472         * @return the current calendar
473         */
474        public Calendar getCalendar() {
475                Date date = getDate();
476                if (date == null) {
477                        return null;
478                }
479                Calendar calendar = Calendar.getInstance();
480                calendar.setTime(date);
481                return calendar;
482        }
483
484        /**
485         * Sets the calendar. Value null will set the null date on the date editor.
486         * 
487         * @param calendar
488         *            the calendar.
489         */
490        public void setCalendar(Calendar calendar) {
491                if (calendar == null) {
492                        dateEditor.setDate(null);
493                } else {
494                        dateEditor.setDate(calendar.getTime());
495                }
496        }
497
498        /**
499         * Enable or disable the JDateChooser.
500         * 
501         * @param enabled
502         *            the new enabled value
503         */
504        public void setEnabled(boolean enabled) {
505                super.setEnabled(enabled);
506                if (dateEditor != null) {
507                        dateEditor.setEnabled(enabled);
508                        calendarButton.setEnabled(enabled);
509                }
510        }
511
512        /**
513         * Returns true, if enabled.
514         * 
515         * @return true, if enabled.
516         */
517        public boolean isEnabled() {
518                return super.isEnabled();
519        }
520
521        /**
522         * Sets the icon of the buuton.
523         * 
524         * @param icon
525         *            The new icon
526         */
527        public void setIcon(ImageIcon icon) {
528                calendarButton.setIcon(icon);
529        }
530
531        /**
532         * Sets the font of all subcomponents.
533         * 
534         * @param font
535         *            the new font
536         */
537        public void setFont(Font font) {
538                if (isInitialized) {
539                        dateEditor.getUiComponent().setFont(font);
540                        jcalendar.setFont(font);
541                }
542                super.setFont(font);
543        }
544
545        /**
546         * Returns the JCalendar component. THis is usefull if you want to set some
547         * properties.
548         * 
549         * @return the JCalendar
550         */
551        public JCalendar getJCalendar() {
552                return jcalendar;
553        }
554
555        /**
556         * Returns the calendar button.
557         * 
558         * @return the calendar button
559         */
560        public JButton getCalendarButton() {
561                return calendarButton;
562        }
563
564        /**
565         * Returns the date editor.
566         * 
567         * @return the date editor
568         */
569        public IDateEditor getDateEditor() {
570                return dateEditor;
571        }
572
573        /**
574         * Sets a valid date range for selectable dates. If max is before min, the
575         * default range with no limitation is set.
576         * 
577         * @param min
578         *            the minimum selectable date or null (then the minimum date is
579         *            set to 01\01\0001)
580         * @param max
581         *            the maximum selectable date or null (then the maximum date is
582         *            set to 01\01\9999)
583         */
584        public void setSelectableDateRange(Date min, Date max) {
585                jcalendar.setSelectableDateRange(min, max);
586                dateEditor.setSelectableDateRange(jcalendar.getMinSelectableDate(),
587                                jcalendar.getMaxSelectableDate());
588        }
589
590        public void setMaxSelectableDate(Date max) {
591                jcalendar.setMaxSelectableDate(max);
592                dateEditor.setMaxSelectableDate(max);
593        }
594
595        public void setMinSelectableDate(Date min) {
596                jcalendar.setMinSelectableDate(min);
597                dateEditor.setMinSelectableDate(min);
598        }
599
600        /**
601         * Gets the maximum selectable date.
602         * 
603         * @return the maximum selectable date
604         */
605        public Date getMaxSelectableDate() {
606                return jcalendar.getMaxSelectableDate();
607        }
608
609        /**
610         * Gets the minimum selectable date.
611         * 
612         * @return the minimum selectable date
613         */
614        public Date getMinSelectableDate() {
615                return jcalendar.getMinSelectableDate();
616        }
617
618        /**
619         * Should only be invoked if the JDateChooser is not used anymore. Due to popup
620         * handling it had to register a change listener to the default menu
621         * selection manager which will be unregistered here. Use this method to
622         * cleanup possible memory leaks.
623         */
624        public void cleanup() {
625                MenuSelectionManager.defaultManager().removeChangeListener(changeListener);
626                changeListener = null;
627        }
628
629        /**
630         * Creates a JFrame with a JDateChooser inside and can be used for testing.
631         * 
632         * @param s
633         *            The command line arguments
634         */
635        public static void main(String[] s) {
636                JFrame frame = new JFrame("JDateChooser");
637                JDateChooser dateChooser = new JDateChooser();
638                // JDateChooser dateChooser = new JDateChooser(null, new Date(), null,
639                // null);
640                // dateChooser.setLocale(new Locale("de"));
641                // dateChooser.setDateFormatString("dd. MMMM yyyy");
642
643                // dateChooser.setPreferredSize(new Dimension(130, 20));
644                // dateChooser.setFont(new Font("Verdana", Font.PLAIN, 10));
645                // dateChooser.setDateFormatString("yyyy-MM-dd HH:mm");
646
647                // URL iconURL = dateChooser.getClass().getResource(
648                // "/com/toedter/calendar/images/JMonthChooserColor32.gif");
649                // ImageIcon icon = new ImageIcon(iconURL);
650                // dateChooser.setIcon(icon);
651
652                frame.getContentPane().add(dateChooser);
653                frame.pack();
654                frame.setVisible(true);
655        }
656
657}