001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2023
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                
204                // always fire"day" property even if the user selects
205                // the already selected day again
206                // jcalendar.getDayChooser().setAlwaysFireDayProperty(true);
207                
208                // TJJ Jun 2015 - no, this was causing infinite loops of prop events
209                // between the two date pickers
210                jcalendar.getDayChooser().setAlwaysFireDayProperty(false);
211
212                setDateFormatString(dateFormatString);
213                setDate(date);
214
215                // Display a calendar button with an icon
216                URL iconURL = getClass().getResource(
217                                "/com/toedter/calendar/images/JDateChooserIcon.gif");
218                ImageIcon icon = new ImageIcon(iconURL);
219
220                calendarButton = new JButton(icon) {
221                        private static final long serialVersionUID = -1913767779079949668L;
222
223                        public boolean isFocusable() {
224                                return false;
225                        }
226                };
227                calendarButton.setMargin(new Insets(0, 0, 0, 0));
228                calendarButton.addActionListener(this);
229
230                // Alt + 'C' selects the calendar.
231                calendarButton.setMnemonic(KeyEvent.VK_C);
232
233                add(calendarButton, BorderLayout.EAST);
234                add(this.dateEditor.getUiComponent(), BorderLayout.CENTER);
235
236                calendarButton.setMargin(new Insets(0, 0, 0, 0));
237                // calendarButton.addFocusListener(this);
238
239                popup = new JPopupMenu() {
240                        private static final long serialVersionUID = -6078272560337577761L;
241
242                        public void setVisible(boolean b) {
243                                Boolean isCanceled = (Boolean) getClientProperty("JPopupMenu.firePopupMenuCanceled");
244                                if (b
245                                                || (!b && dateSelected)
246                                                || ((isCanceled != null) && !b && isCanceled
247                                                                .booleanValue())) {
248                                        super.setVisible(b);
249                                }
250                        }
251                };
252
253                popup.setLightWeightPopupEnabled(true);
254
255                popup.add(jcalendar);
256
257                lastSelectedDate = date;
258
259                // Corrects a problem that occured when the JMonthChooser's combobox is
260                // displayed, and a click outside the popup does not close it.
261
262                // The following idea was originally provided by forum user
263                // podiatanapraia:
264                changeListener = new ChangeListener() {
265                        boolean hasListened = false;
266
267                        public void stateChanged(ChangeEvent e) {
268                                if (hasListened) {
269                                        hasListened = false;
270                                        return;
271                                }
272                                if (popup.isVisible()
273                                                && JDateChooser.this.jcalendar.monthChooser
274                                                                .getComboBox().hasFocus()) {
275                                        MenuElement[] me = MenuSelectionManager.defaultManager()
276                                                        .getSelectedPath();
277                                        MenuElement[] newMe = new MenuElement[me.length + 1];
278                                        newMe[0] = popup;
279                                        for (int i = 0; i < me.length; i++) {
280                                                newMe[i + 1] = me[i];
281                                        }
282                                        hasListened = true;
283                                        MenuSelectionManager.defaultManager()
284                                                        .setSelectedPath(newMe);
285                                }
286                        }
287                };
288                MenuSelectionManager.defaultManager().addChangeListener(changeListener);
289                // end of code provided by forum user podiatanapraia
290
291                isInitialized = true;
292        }
293
294        /**
295         * Called when the jalendar button was pressed.
296         * 
297         * @param e
298         *            the action event
299         */
300        public void actionPerformed(ActionEvent e) {
301                int x = calendarButton.getWidth()
302                                - (int) popup.getPreferredSize().getWidth();
303                int y = calendarButton.getY() + calendarButton.getHeight();
304
305                Calendar calendar = Calendar.getInstance();
306                Date date = dateEditor.getDate();
307                if (date != null) {
308                        calendar.setTime(date);
309                }
310                jcalendar.setCalendar(calendar);
311                popup.show(calendarButton, x, y);
312                dateSelected = false;
313        }
314
315        /**
316         * Listens for a "date" property change or a "day" property change event
317         * from the JCalendar. Updates the date editor and closes the popup.
318         * 
319         * @param evt
320         *            the event
321         */
322        public void propertyChange(PropertyChangeEvent evt) {
323                
324                if (evt.getPropertyName().equals("day")) {
325                        if (popup.isVisible()) {
326                                dateSelected = true;
327                                popup.setVisible(false);
328                                setDate(jcalendar.getCalendar().getTime());
329                        }
330                } else if (evt.getPropertyName().equals("date")) {
331                        if (evt.getSource() == dateEditor) {
332                                firePropertyChange("date", evt.getOldValue(), evt.getNewValue());
333                        } else {
334                                setDate((Date) evt.getNewValue());
335                        }
336                }
337                
338                // if event source was a day chooser, may need to check start/end day bounds
339                if (evt.getSource() instanceof JDayChooser) {
340
341                        JDayChooser jdcSrc = (JDayChooser) evt.getSource();
342                        
343                        String srcName = jdcSrc.getName();
344                        String dstName = jcalendar.getDayChooser().getName();
345                        if ((srcName != null) && (dstName != null)) {
346                                
347                                // property event from start day chooser to end day chooser
348                                if (srcName.equals(JDayChooser.BEG_DAY) && (dstName.equals(JDayChooser.END_DAY))) {
349                                        JDayChooser jdcDst = jcalendar.getDayChooser();
350                                        Calendar srcCal = Calendar.getInstance();
351                                        srcCal.set(Calendar.YEAR, jdcSrc.getYear());
352                                        srcCal.set(Calendar.MONTH, jdcSrc.getMonth());
353                                        srcCal.set(Calendar.DAY_OF_MONTH, jdcSrc.getDay());
354                                        Calendar dstCal = Calendar.getInstance();
355                                        dstCal.set(Calendar.YEAR, jdcDst.getYear());
356                                        dstCal.set(Calendar.MONTH, jdcDst.getMonth());
357                                        dstCal.set(Calendar.DAY_OF_MONTH, jdcDst.getDay());
358                                        if (srcCal.after(dstCal)) {
359                                                logger.debug("Adjusting: Src date exceeds Dst date...");
360                                                jdcDst.setDay(srcCal.get(Calendar.DAY_OF_MONTH));
361                                                jdcDst.setMonth(srcCal.get(Calendar.MONTH));
362                                                jdcDst.setYear(srcCal.get(Calendar.YEAR));
363                                                dateEditor.setDate(srcCal.getTime());
364                                                if (getParent() != null) {
365                                                        getParent().invalidate();
366                                                }
367                                        }
368                                }
369                                
370                                // property event from end day chooser to start day chooser
371                                if (srcName.equals(JDayChooser.END_DAY) && (dstName.equals(JDayChooser.BEG_DAY))) {
372                                        JDayChooser jdcDst = jcalendar.getDayChooser();
373                                        Calendar srcCal = Calendar.getInstance();
374                                        srcCal.set(Calendar.YEAR, jdcSrc.getYear());
375                                        srcCal.set(Calendar.MONTH, jdcSrc.getMonth());
376                                        srcCal.set(Calendar.DAY_OF_MONTH, jdcSrc.getDay());
377                                        Calendar dstCal = Calendar.getInstance();
378                                        dstCal.set(Calendar.YEAR, jdcDst.getYear());
379                                        dstCal.set(Calendar.MONTH, jdcDst.getMonth());
380                                        dstCal.set(Calendar.DAY_OF_MONTH, jdcDst.getDay());
381                                        if (srcCal.before(dstCal)) {
382                                                logger.debug("Adjusting: End date preceeds Src date...");
383                                                jdcDst.setDay(srcCal.get(Calendar.DAY_OF_MONTH));
384                                                jdcDst.setMonth(srcCal.get(Calendar.MONTH));
385                                                jdcDst.setYear(srcCal.get(Calendar.YEAR));
386                                                dateEditor.setDate(srcCal.getTime());
387                                                if (getParent() != null) {
388                                                        getParent().invalidate();
389                                                }
390                                        }
391                                }
392                                
393                        }
394                }
395                
396        }
397
398        /**
399         * Updates the UI of itself and the popup.
400         */
401        public void updateUI() {
402                super.updateUI();
403                setEnabled(isEnabled());
404
405                if (jcalendar != null) {
406                        SwingUtilities.updateComponentTreeUI(popup);
407                }
408        }
409
410        /**
411         * Sets the locale.
412         * 
413         * @param l
414         *            The new locale value
415         */
416        public void setLocale(Locale l) {
417                super.setLocale(l);
418                dateEditor.setLocale(l);
419                jcalendar.setLocale(l);
420        }
421
422        /**
423         * @return the jcalendar
424         */
425        public JCalendar getJcalendar() {
426                return jcalendar;
427        }
428
429        /**
430         * Gets the date format string.
431         * 
432         * @return Returns the dateFormatString.
433         */
434        public String getDateFormatString() {
435                return dateEditor.getDateFormatString();
436        }
437
438        /**
439         * Sets the date format string. E.g "MMMMM d, yyyy" will result in "July 21,
440         * 2004" if this is the selected date and locale is English.
441         * 
442         * @param dfString
443         *            The dateFormatString to set.
444         */
445        public void setDateFormatString(String dfString) {
446                dateEditor.setDateFormatString(dfString);
447                invalidate();
448        }
449
450        /**
451         * Returns the date. If the JDateChooser is started with a null date and no
452         * date was set by the user, null is returned.
453         * 
454         * @return the current date
455         */
456        public Date getDate() {
457                return dateEditor.getDate();
458        }
459
460        /**
461         * Sets the date. Fires the property change "date" if date != null.
462         * 
463         * @param date
464         *            the new date.
465         */
466        public void setDate(Date date) {
467                dateEditor.setDate(date);
468                if (getParent() != null) {
469                        getParent().invalidate();
470                }
471        }
472
473        /**
474         * Returns the calendar. If the JDateChooser is started with a null date (or
475         * null calendar) and no date was set by the user, null is returned.
476         * 
477         * @return the current calendar
478         */
479        public Calendar getCalendar() {
480                Date date = getDate();
481                if (date == null) {
482                        return null;
483                }
484                Calendar calendar = Calendar.getInstance();
485                calendar.setTime(date);
486                return calendar;
487        }
488
489        /**
490         * Sets the calendar. Value null will set the null date on the date editor.
491         * 
492         * @param calendar
493         *            the calendar.
494         */
495        public void setCalendar(Calendar calendar) {
496                if (calendar == null) {
497                        dateEditor.setDate(null);
498                } else {
499                        dateEditor.setDate(calendar.getTime());
500                }
501        }
502
503        /**
504         * Enable or disable the JDateChooser.
505         * 
506         * @param enabled
507         *            the new enabled value
508         */
509        public void setEnabled(boolean enabled) {
510                super.setEnabled(enabled);
511                if (dateEditor != null) {
512                        dateEditor.setEnabled(enabled);
513                        calendarButton.setEnabled(enabled);
514                }
515        }
516
517        /**
518         * Returns true, if enabled.
519         * 
520         * @return true, if enabled.
521         */
522        public boolean isEnabled() {
523                return super.isEnabled();
524        }
525
526        /**
527         * Sets the icon of the buuton.
528         * 
529         * @param icon
530         *            The new icon
531         */
532        public void setIcon(ImageIcon icon) {
533                calendarButton.setIcon(icon);
534        }
535
536        /**
537         * Sets the font of all subcomponents.
538         * 
539         * @param font
540         *            the new font
541         */
542        public void setFont(Font font) {
543                if (isInitialized) {
544                        dateEditor.getUiComponent().setFont(font);
545                        jcalendar.setFont(font);
546                }
547                super.setFont(font);
548        }
549
550        /**
551         * Returns the JCalendar component. THis is usefull if you want to set some
552         * properties.
553         * 
554         * @return the JCalendar
555         */
556        public JCalendar getJCalendar() {
557                return jcalendar;
558        }
559
560        /**
561         * Returns the calendar button.
562         * 
563         * @return the calendar button
564         */
565        public JButton getCalendarButton() {
566                return calendarButton;
567        }
568
569        /**
570         * Returns the date editor.
571         * 
572         * @return the date editor
573         */
574        public IDateEditor getDateEditor() {
575                return dateEditor;
576        }
577
578        /**
579         * Sets a valid date range for selectable dates. If max is before min, the
580         * default range with no limitation is set.
581         * 
582         * @param min
583         *            the minimum selectable date or null (then the minimum date is
584         *            set to 01\01\0001)
585         * @param max
586         *            the maximum selectable date or null (then the maximum date is
587         *            set to 01\01\9999)
588         */
589        public void setSelectableDateRange(Date min, Date max) {
590                jcalendar.setSelectableDateRange(min, max);
591                dateEditor.setSelectableDateRange(jcalendar.getMinSelectableDate(),
592                                jcalendar.getMaxSelectableDate());
593        }
594
595        public void setMaxSelectableDate(Date max) {
596                jcalendar.setMaxSelectableDate(max);
597                dateEditor.setMaxSelectableDate(max);
598        }
599
600        public void setMinSelectableDate(Date min) {
601                jcalendar.setMinSelectableDate(min);
602                dateEditor.setMinSelectableDate(min);
603        }
604
605        /**
606         * Gets the maximum selectable date.
607         * 
608         * @return the maximum selectable date
609         */
610        public Date getMaxSelectableDate() {
611                return jcalendar.getMaxSelectableDate();
612        }
613
614        /**
615         * Gets the minimum selectable date.
616         * 
617         * @return the minimum selectable date
618         */
619        public Date getMinSelectableDate() {
620                return jcalendar.getMinSelectableDate();
621        }
622
623        /**
624         * Should only be invoked if the JDateChooser is not used anymore. Due to popup
625         * handling it had to register a change listener to the default menu
626         * selection manager which will be unregistered here. Use this method to
627         * cleanup possible memory leaks.
628         */
629        public void cleanup() {
630                MenuSelectionManager.defaultManager().removeChangeListener(changeListener);
631                changeListener = null;
632        }
633
634        /**
635         * Creates a JFrame with a JDateChooser inside and can be used for testing.
636         * 
637         * @param s
638         *            The command line arguments
639         */
640        public static void main(String[] s) {
641                JFrame frame = new JFrame("JDateChooser");
642                JDateChooser dateChooser = new JDateChooser();
643                // JDateChooser dateChooser = new JDateChooser(null, new Date(), null,
644                // null);
645                // dateChooser.setLocale(new Locale("de"));
646                // dateChooser.setDateFormatString("dd. MMMM yyyy");
647
648                // dateChooser.setPreferredSize(new Dimension(130, 20));
649                // dateChooser.setFont(new Font("Verdana", Font.PLAIN, 10));
650                // dateChooser.setDateFormatString("yyyy-MM-dd HH:mm");
651
652                // URL iconURL = dateChooser.getClass().getResource(
653                // "/com/toedter/calendar/images/JMonthChooserColor32.gif");
654                // ImageIcon icon = new ImageIcon(iconURL);
655                // dateChooser.setIcon(icon);
656
657                frame.getContentPane().add(dateChooser);
658                frame.pack();
659                frame.setVisible(true);
660        }
661
662}