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