001/*
002 * $Id: JDateChooser.java,v 1.2 2011/03/24 16:06:33 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
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 */
030package edu.wisc.ssec.mcidasv.data.dateChooser;
031
032import java.awt.BorderLayout;
033import java.awt.Font;
034import java.awt.Insets;
035import java.awt.event.ActionEvent;
036import java.awt.event.ActionListener;
037import java.awt.event.KeyEvent;
038import java.beans.PropertyChangeEvent;
039import java.beans.PropertyChangeListener;
040import java.net.URL;
041import java.util.Calendar;
042import java.util.Date;
043import java.util.Locale;
044
045import javax.swing.ImageIcon;
046import javax.swing.JButton;
047import javax.swing.JFrame;
048import javax.swing.JPanel;
049import javax.swing.JPopupMenu;
050import javax.swing.MenuElement;
051import javax.swing.MenuSelectionManager;
052import javax.swing.SwingUtilities;
053import javax.swing.event.ChangeEvent;
054import 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 */
065public 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}