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