001    /*
002     * $Id: JXDatePicker.java 3311 2009-03-26 11:55:17Z kleopatra $
003     *
004     * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005     * Santa Clara, California 95054, U.S.A. All rights reserved.
006     *
007     * This library is free software; you can redistribute it and/or
008     * modify it under the terms of the GNU Lesser General Public
009     * License as published by the Free Software Foundation; either
010     * version 2.1 of the License, or (at your option) any later version.
011     *
012     * This library is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this library; if not, write to the Free Software
019     * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020     */
021    package org.jdesktop.swingx;
022    
023    import java.awt.Color;
024    import java.awt.ComponentOrientation;
025    import java.awt.FlowLayout;
026    import java.awt.Font;
027    import java.awt.GradientPaint;
028    import java.awt.Graphics;
029    import java.awt.event.ActionEvent;
030    import java.awt.event.ActionListener;
031    import java.awt.event.MouseAdapter;
032    import java.awt.event.MouseEvent;
033    import java.awt.event.MouseListener;
034    import java.beans.PropertyChangeEvent;
035    import java.beans.PropertyChangeListener;
036    import java.beans.PropertyVetoException;
037    import java.text.DateFormat;
038    import java.text.Format;
039    import java.text.MessageFormat;
040    import java.text.ParseException;
041    import java.text.SimpleDateFormat;
042    import java.util.Calendar;
043    import java.util.Date;
044    import java.util.EventListener;
045    import java.util.Locale;
046    import java.util.TimeZone;
047    import java.util.logging.Logger;
048    
049    import javax.swing.AbstractAction;
050    import javax.swing.Action;
051    import javax.swing.JComponent;
052    import javax.swing.JFormattedTextField;
053    import javax.swing.JPanel;
054    import javax.swing.JPopupMenu;
055    import javax.swing.SwingUtilities;
056    import javax.swing.UIManager;
057    import javax.swing.JFormattedTextField.AbstractFormatter;
058    import javax.swing.JFormattedTextField.AbstractFormatterFactory;
059    import javax.swing.text.DefaultFormatterFactory;
060    
061    import org.jdesktop.swingx.calendar.DatePickerFormatter;
062    import org.jdesktop.swingx.event.EventListenerMap;
063    import org.jdesktop.swingx.painter.MattePainter;
064    import org.jdesktop.swingx.plaf.DatePickerAddon;
065    import org.jdesktop.swingx.plaf.DatePickerUI;
066    import org.jdesktop.swingx.plaf.LookAndFeelAddons;
067    import org.jdesktop.swingx.plaf.UIManagerExt;
068    import org.jdesktop.swingx.util.Contract;
069    
070    /**
071     * A component for entering dates with a user interaction similar to a
072     * JComboBox. The dates can be typed into a text field or selected from a
073     * JXMonthView which opens in a JXPopupMenu on user's request.
074     * <p>
075     * 
076     * The date selection is controlled by the JXMonthView's DateSelectionModel.
077     * This allows the use of all its functionality in the JXDatePicker as well.
078     * F.i. restrict the selection to a date in the current or next week:
079     * <p>
080     * 
081     * <pre><code>
082     * Appointment appointment = new Appointment(director,
083     *         &quot;Be sure to have polished shoes!&quot;);
084     * JXDatePicker picker = new JXDatePicker();
085     * Calendar calendar = picker.getMonthView().getCalendar();
086     * // starting today if we are in a hurry
087     * calendar.setTime(new Date());
088     * picker.getMonthView().setLowerBound(calendar.getTime());
089     * // end of next week
090     * CalendarUtils.endOfWeek(calendar);
091     * calendar.add(Calendar.WEEK_OF_YEAR);
092     * picker.getMonthView().setUpperBound(calendar.getTime());
093     * </code></pre>
094     * 
095     * Similar to a JXMonthView, the JXDatePicker fires an ActionEvent when the user
096     * actively commits or cancels a selection. Interested client code can add a
097     * ActionListener to be notified by the user action.
098     * 
099     * <pre><code>
100     * JXDatePicker picker = new JXDatePicker(new Date());
101     * ActionListener l = new ActionListener() {
102     *     public void actionPerformed(ActionEvent e) {
103     *         if (JXDatePicker.COMMIT_KEY.equals(e.getActionCommand)) {
104     *             saveDate(picker.getDate());
105     *         }
106     *     }
107     * };
108     * picker.addActionListener(l);
109     * </code></pre>
110     * 
111     * Note that  ActionListener will <b>not</b> be notified if the user 
112     * edits the date text without hitting the Enter key afterwards. To detect both kinds of
113     * date change, interested client code can add a PropertyChangeListener.
114     * 
115     * <pre><code>
116     * JXDatePicker picker = new JXDatePicker(new Date());
117     * PropertyChangeListener listener = new PropertyChangeListener() {
118     *     public void propertyChange(PropertyChangeEvent e) {
119     *         if ("date".equals(e.getPropertyName()) {
120     *              saveDate(picker.getDate());
121     *         }
122     *     }
123     * };
124     * picker.addPropertyChangeListener(listener);
125     * </code></pre>
126    
127     * 
128     * <p>
129     * The DateFormats used in the JXDatePicker's are initialized to the default
130     * formats of the DatePickerFormatter, as defined by the picker's resourceBundle
131     * DatePicker.properties. Application code can overwrite the picker's default
132     * 
133     * <pre><code>
134     * picker.setDateFormats(myCustomFormat, myAlternativeCustomFormat);
135     * </code></pre>
136     * 
137     * PENDING JW: explain what the alternatives are for (after understanding it
138     * myself ;-)
139     * <p>
140     * 
141     * The selected Date is a bound property of the JXDatePicker. This allows easy
142     * binding to a property of a custom bean when using a binding framework.
143     * <p>
144     * 
145     * Keybindings (as installed by the UI-Delegate)
146     * <ul>
147     * <li> ENTER commits the edited or selected value
148     * <li> ESCAPE reverts the edited or selected value
149     * <li> alt-DOWN opens the monthView popup
150     * <li> shift-F5 if monthView is visible, navigates the monthView to today 
151     *    (no effect otherwise)
152     * <li> F5 commits today 
153     * </ul>
154     * 
155     * PENDNG JW: support per-OS keybindings to be installed, currently they are 
156     * hardcoded in our (single) BasicDatePickerUI. 
157     * 
158     * @author Joshua Outwater
159     * @author Jeanette Winzenburg
160     * 
161     * @see JXMonthView
162     * @see org.jdesktop.swingx.calendar.DateSelectionModel
163     * @see DatePickerFormatter
164     * 
165     */
166    public class JXDatePicker extends JComponent {
167        @SuppressWarnings("unused")
168        private static final Logger LOG = Logger.getLogger(JXDatePicker.class
169                .getName());
170        static {
171            LookAndFeelAddons.contribute(new DatePickerAddon());
172        }
173    
174        /**
175         * UI Class ID
176         */
177        public static final String uiClassID = "DatePickerUI";
178    
179        public static final String EDITOR = "editor";
180        public static final String MONTH_VIEW = "monthView";
181    
182        public static final String LINK_PANEL = "linkPanel";
183    
184        /** action command used for commit actionEvent. */
185        public static final String COMMIT_KEY = "datePickerCommit";
186        /** action command used for cancel actionEvent. */
187        public static final String CANCEL_KEY = "datePickerCancel";
188        /** action key for navigate home action */
189        public static final String HOME_NAVIGATE_KEY = "navigateHome";
190        /** action key for commit home action */
191        public static final String HOME_COMMIT_KEY = "commitHome";
192    
193        private static final DateFormat[] EMPTY_DATE_FORMATS = new DateFormat[0];
194    
195        /**
196         * The editable date field that displays the date
197         */
198        private JFormattedTextField _dateField;
199    
200        /**
201         * Popup that displays the month view with controls for
202         * traversing/selecting dates.
203         */
204        private JPanel _linkPanel;
205        private MessageFormat _linkFormat;
206        private Date linkDate;
207        
208        private JXMonthView _monthView;
209        private boolean editable = true;
210        private EventListenerMap listenerMap;
211        protected boolean lightWeightPopupEnabled = JPopupMenu.getDefaultLightWeightPopupEnabled();
212    
213        private Date date;
214    
215        private PropertyChangeListener monthViewListener;
216    
217    
218        /**
219         * Intantiates a date picker with no selection and the default 
220         * <code>DatePickerFormatter</code>.
221         * <p/>
222         * The date picker is configured with the default time zone and locale
223         *
224         * @see #setTimeZone
225         * @see #getTimeZone
226         */
227        public JXDatePicker() {
228            this(null, null);
229        }
230    
231        
232    
233        /**
234         * Intantiates a date picker using the specified time as the initial
235         * selection and the default 
236         * <code>DatePickerFormatter</code>.
237         * <p/>
238         * The date picker is configured with the default time zone and locale
239         *
240         * @param selected the initially selected date
241         * @see #setTimeZone
242         * @see #getTimeZone
243         */
244        public JXDatePicker(Date selected) {
245            this(selected, null);
246        }
247        
248        /**
249         * Intantiates a date picker with no selection and the default 
250         * <code>DatePickerFormatter</code>.
251         * <p/>
252         * The date picker is configured with the default time zone and specified 
253         * locale
254         *
255         * @param locale    initial Locale
256         * @see #setTimeZone
257         * @see #getTimeZone
258         */
259        public JXDatePicker(Locale locale) {
260            this(null, locale);
261        }
262    
263        /**
264         * Intantiates a date picker using the specified time as the initial
265         * selection and the default 
266         * <code>DatePickerFormatter</code>.
267         * <p/>
268         * The date picker is configured with the default time zone and specified locale
269         *
270         * @param selection initially selected Date
271         * @param locale initial Locale
272         * @see #setTimeZone
273         * @see #getTimeZone
274         * 
275         */
276        public JXDatePicker(Date selection, Locale locale) {
277            init();
278            if (locale != null) {
279                setLocale(locale);
280            }
281            // install the controller before setting the date
282            updateUI();
283            setDate(selection);
284        }
285    
286        /**
287         * Sets the date property. <p>
288         * 
289         * Does nothing if the ui vetos the new date - as might happen if
290         * the code tries to set a date which is unselectable in the 
291         * monthView's context. The actual value of the new Date is controlled
292         * by the JXMonthView's DateSelectionModel. The default implementation
293         * normalizes the date to the start of the day in the model's calendar's 
294         * coordinates, that is all time fields are zeroed. To keep the time fields,
295         * configure the monthView with a SingleDaySelectionModel.
296         * <p>
297         * 
298         * At all "stable" (= not editing in date input field nor 
299         * in the monthView) times the date is the same in the 
300         * JXMonthView, this JXDatePicker and the editor. If a new Date
301         * is set, this invariant is enforced by the DatePickerUI.
302         * <p>
303         * 
304         * This is a bound property. 
305         * 
306         *  
307         * @param date the new date to set.
308         * @see #getDate()
309         * @see org.jdesktop.swingx.calendar.DateSelectionModel
310         * @see org.jdesktop.swingx.calendar.SingleDaySelectionModel
311         */
312        public void setDate(Date date) {
313            /*
314             * JW: 
315             * this is a poor woman's constraint property.
316             * Introduces explicit coupling to the ui. 
317             * Which is unusual at this place in code.
318             * 
319             * If needed the date can be made a formal
320             * constraint property and let the ui add a
321             * VetoablePropertyListener.
322             */
323            try {
324                date = getUI().getSelectableDate(date);
325            } catch (PropertyVetoException e) {
326                return;
327            }
328            Date old = getDate();
329            this.date = date;
330            firePropertyChange("date", old, getDate());
331        }
332    
333     
334    
335        /**
336         * Returns the currently selected date.
337         *
338         * @return Date
339         */
340        public Date getDate() {
341            return date; 
342        }
343        
344        /**
345         * 
346         */
347        private void init() {
348            listenerMap = new EventListenerMap();
349            initMonthView();
350    
351            updateLinkFormat();
352            linkDate = _monthView.getToday();
353            _linkPanel = new TodayPanel();
354        }
355    
356        private void initMonthView() {
357            _monthView = new JXMonthView();
358    //        _monthView.setSelectionModel(new SingleDaySelectionModel());
359            _monthView.setTraversable(true);
360            _monthView.addPropertyChangeListener(getMonthViewListener());
361        }
362    
363        /**
364         * Lazily creates and returns the PropertyChangeListener which listens
365         * for model's calendar properties.
366         * 
367         * @return a PropertyChangeListener for monthView's property change notification.
368         */
369        private PropertyChangeListener getMonthViewListener() {
370            if (monthViewListener == null) {
371                monthViewListener = new PropertyChangeListener() {
372    
373                    public void propertyChange(PropertyChangeEvent evt) {
374                        if ("timeZone".equals(evt.getPropertyName())) {
375                            updateTimeZone((TimeZone) evt.getOldValue(), (TimeZone) evt.getNewValue());
376                        }
377                        
378                    }
379                    
380                };
381            }
382            return monthViewListener;
383        }
384    
385        /**
386         * Callback from monthView timezone changes. <p>
387         * 
388         * NOTE: as timeZone is a bound property of this class we need to 
389         * guarantee the propertyChangeNotification. As this class doesn't 
390         * own this property it must listen to the owner (monthView) and 
391         * re-fire the change.
392         * 
393         * @param oldValue the old timezone.
394         * @param newValue the new timezone.
395         */
396        protected void updateTimeZone(TimeZone oldValue, TimeZone newValue) {
397            firePropertyChange("timeZone", oldValue, newValue);
398        }
399    
400        /**
401         * Returns the look and feel (L&F) object that renders this component.
402         *
403         * @return the DatePickerUI object that renders this component
404         */
405        public DatePickerUI getUI() {
406            return (DatePickerUI) ui;
407        }
408    
409        /**
410         * Sets the L&F object that renders this component.
411         *
412         * @param ui UI to use for this {@code JXDatePicker}
413         */
414        public void setUI(DatePickerUI ui) {
415            super.setUI(ui);
416        }
417    
418        /**
419         * Resets the UI property with the value from the current look and feel.
420         *
421         * @see UIManager#getUI
422         */
423        @Override
424        public void updateUI() {
425            setUI((DatePickerUI) LookAndFeelAddons.getUI(this, DatePickerUI.class));
426            // JW: quick hack around #706-swingx - monthView not updated
427            // is this complete? how about editor (if not uiResource), linkPanel?
428            SwingUtilities.updateComponentTreeUI(getMonthView());
429            invalidate();
430        }
431    
432        /**
433         * @inheritDoc
434         */
435        @Override
436        public String getUIClassID() {
437            return uiClassID;
438        }
439    
440        /**
441         * Replaces the currently installed formatter and factory used by the
442         * editor. These string formats are defined by the
443         * <code>java.text.SimpleDateFormat</code> class. <p>
444         * 
445         * Note: The given formats are internally synched to the picker's current
446         *    TimeZone. 
447         * 
448         * @param formats zero or more not null string formats to use. Note that a 
449         *    null array is allowed and resets the formatter to use the 
450         *    localized default formats.
451         * @throws NullPointerException any array element is null.
452         * @see java.text.SimpleDateFormat
453         */
454        public void setFormats(String... formats) {
455            DateFormat[] dateFormats = null;
456            if (formats !=  null) {
457                Contract.asNotNull(formats,
458                        "the array of format strings must not "
459                                + "must not contain null elements");
460                dateFormats = new DateFormat[formats.length];
461                for (int counter = formats.length - 1; counter >= 0; counter--) {
462                    dateFormats[counter] = new SimpleDateFormat(formats[counter], getLocale());
463                }
464            }
465            setFormats(dateFormats);
466        }
467    
468        /**
469         * Replaces the currently installed formatter and factory used by the
470         * editor.<p>
471         *
472         * Note: The given formats are internally synched to the picker's current
473         *    TimeZone. 
474         * 
475         * @param formats zero or more not null formats to use. Note that a 
476         *    null array is allowed and resets the formatter to use the 
477         *    localized default formats.
478         * @throws NullPointerException any of its elements is null.
479         */
480        public void setFormats(DateFormat... formats) {
481            if (formats != null) {
482                Contract.asNotNull(formats, "the array of formats " + "must not contain null elements");
483            }
484            
485            DateFormat[] old = getFormats();
486            _dateField.setFormatterFactory(new DefaultFormatterFactory(
487                    new DatePickerFormatter(formats, getLocale())));
488            firePropertyChange("formats", old, getFormats());
489        }
490    
491        /**
492         * Returns an array of the formats used by the installed formatter
493         * if it is a subclass of <code>JXDatePickerFormatter<code>.
494         * <code>javax.swing.JFormattedTextField.AbstractFormatter</code>
495         * and <code>javax.swing.text.DefaultFormatter</code> do not have
496         * support for accessing the formats used.
497         *
498         * @return array of formats guaranteed to be not null, but might be empty.
499         */
500        public DateFormat[] getFormats() {
501            // Dig this out from the factory, if possible, otherwise return null.
502            AbstractFormatterFactory factory = _dateField.getFormatterFactory();
503            if (factory != null) {
504                AbstractFormatter formatter = factory.getFormatter(_dateField);
505                if (formatter instanceof DatePickerFormatter) {
506                    return ((DatePickerFormatter) formatter).getFormats();
507                }
508            }
509            return EMPTY_DATE_FORMATS;
510        }
511    
512        /**
513         * Return the <code>JXMonthView</code> used in the popup to
514         * select dates from.
515         *
516         * @return the month view component
517         */
518        public JXMonthView getMonthView() {
519            return _monthView;
520        }
521    
522        /**
523         * Set the component to use the specified JXMonthView.  If the new JXMonthView
524         * is configured to a different time zone it will affect the time zone of this
525         * component.
526         *
527         * @param monthView month view comopnent.
528         * @throws NullPointerException if view component is null
529         * 
530         * @see #setTimeZone
531         * @see #getTimeZone
532         */
533        public void setMonthView(JXMonthView monthView) {
534            Contract.asNotNull(monthView, "monthView must not be null");
535            JXMonthView oldMonthView = getMonthView();
536            TimeZone oldTZ = getTimeZone();
537            oldMonthView.removePropertyChangeListener(getMonthViewListener());
538            _monthView = monthView;
539            getMonthView().addPropertyChangeListener(getMonthViewListener());
540            firePropertyChange(MONTH_VIEW, oldMonthView, getMonthView());
541            firePropertyChange("timeZone", oldTZ, getTimeZone());
542        }
543    
544        /**
545         * Gets the time zone.  This is a convenience method which returns the time zone
546         * of the JXMonthView being used.
547         *
548         * @return The <code>TimeZone</code> used by the <code>JXMonthView</code>.
549         */
550        public TimeZone getTimeZone() {
551            return _monthView.getTimeZone();
552        }
553    
554        /**
555         * Sets the time zone with the given time zone value.    This is a convenience
556         * method which returns the time zone of the JXMonthView being used.<p>
557         * 
558         * PENDING JW: currently this property is the only property of the monthView 
559         * which is exposed in this api. Not sure why it is here at all.
560         * It's asymetric (to the other properties) and as such should be either removed
561         * or the others which might be relevant to a datePicker exposed as well (probably 
562         * hiding the monthView itself as an implementation detail of the ui delegate). 
563         *
564         * @param tz The <code>TimeZone</code>.
565         */
566        public void setTimeZone(TimeZone tz) {
567            _monthView.setTimeZone(tz);
568        }
569    
570    
571        /**
572         * Returns the date shown in the LinkPanel.
573         * <p>
574         * PENDING JW: the property should be named linkDate - but that's held by the 
575         * deprecated long returning method. Maybe revisit if we actually remove the other.
576         * 
577         * @return the date shown in the LinkPanel.
578         */
579        public Date getLinkDay() {
580            return linkDate;
581        }
582    
583        /**
584         * Set the date the link will use and the string defining a MessageFormat
585         * to format the link.  If no valid date is in the editor when the popup
586         * is displayed the popup will focus on the month the linkDate is in.  Calling
587         * this method will replace the currently installed linkPanel and install
588         * a new one with the requested date and format.
589         * 
590         * 
591         * @param linkDay     the Date to set on the LinkPanel 
592         * @param linkFormatString String used to format the link
593         * @see java.text.MessageFormat
594         */
595        public void setLinkDay(Date linkDay, String linkFormatString) {
596            setLinkFormat(new MessageFormat(linkFormatString));
597            setLinkDay(linkDay);
598        }
599        
600        /**
601         * Sets the date shown in the TodayPanel. 
602         * 
603         * PENDING JW ... quick api hack for testing. Don't recreate the panel if
604         * it had been used 
605         * 
606         * @param linkDay the date used in the TodayPanel
607         */
608        public void setLinkDay(Date linkDay) {
609            this.linkDate = linkDay;
610            Format[] formats = getLinkFormat().getFormatsByArgumentIndex();
611            for (Format format : formats) {
612                if (format instanceof DateFormat) {
613                    ((DateFormat) format).setTimeZone(getTimeZone());
614                }
615            }
616            setLinkPanel(new TodayPanel());
617        }
618        
619    
620        /**
621         * @param _linkFormat the _linkFormat to set
622         */
623        protected void setLinkFormat(MessageFormat _linkFormat) {
624            this._linkFormat = _linkFormat;
625        }
626    
627        /**
628         * @return the _linkFormat
629         */
630        protected MessageFormat getLinkFormat() {
631            return _linkFormat;
632        }
633    
634        /**
635         * Update text on the link panel.
636         * 
637         */
638        private void updateLinkFormat() {
639            // PENDING JW: move to ui
640            String linkFormat = UIManagerExt.getString(
641                    "JXDatePicker.linkFormat", getLocale());
642            
643            if (linkFormat != null) {
644                setLinkFormat(new MessageFormat(linkFormat));
645            } else {
646                setLinkFormat(new MessageFormat("{0,date, dd MMMM yyyy}"));
647            }
648        }
649    
650        /**
651         * Return the panel that is used at the bottom of the popup.  The default
652         * implementation shows a link that displays the current month.
653         *
654         * @return The currently installed link panel
655         */
656        public JPanel getLinkPanel() {
657            return _linkPanel;
658        }
659    
660        /**
661         * Set the panel that will be used at the bottom of the popup.
662         * PENDING JW: why insist on JPanel? JComponent would be enough?
663         *  
664         * @param linkPanel The new panel to install in the popup
665         */
666        public void setLinkPanel(JPanel linkPanel) {
667            JPanel oldLinkPanel = _linkPanel;
668            _linkPanel = linkPanel;
669            firePropertyChange(LINK_PANEL, oldLinkPanel, _linkPanel);
670        }
671    
672        /**
673         * Returns the formatted text field used to edit the date selection.
674         * <p>
675         * Clients should NOT use this method. It is provided to temporarily support
676         * the PLAF code.
677         * 
678         * @return the formatted text field
679         */
680    //    @Deprecated
681        public JFormattedTextField getEditor() {
682            return _dateField;
683        }
684    
685        /**
686         * Sets the editor. <p>
687         * 
688         * The default is created and set by the UI delegate.
689         * <p>
690         * Clients should NOT use this method. It is provided to temporarily support
691         * the PLAF code.
692         * 
693         * @param editor the formatted input.
694         * @throws NullPointerException if editor is null.
695         * 
696         * @see #getEditor
697         */
698    //    @Deprecated
699        public void setEditor(JFormattedTextField editor) {
700            Contract.asNotNull(editor, "editor must not be null");
701            JFormattedTextField oldEditor = _dateField;
702            _dateField = editor;
703            firePropertyChange(EDITOR, oldEditor, _dateField);
704        }
705    
706        @Override
707        public void setComponentOrientation(ComponentOrientation orientation) {
708            super.setComponentOrientation(orientation);
709            _monthView.setComponentOrientation(orientation);
710        }
711    
712        /**
713         * Returns true if the current value being edited is valid.
714         *
715         * @return true if the current value being edited is valid.
716         */
717        public boolean isEditValid() {
718            return _dateField.isEditValid();
719        }
720    
721        /**
722         * Commits the editor's changes and notifies ActionListeners.
723         * 
724         * Forces the current value to be taken from the AbstractFormatter and
725         * set as the current value. This has no effect if there is no current
726         * AbstractFormatter installed.
727         *
728         * @throws java.text.ParseException Throws parse exception if the date
729         *                                  can not be parsed.
730         */
731        public void commitEdit() throws ParseException {
732            try {
733                _dateField.commitEdit();
734                fireActionPerformed(COMMIT_KEY);
735            } catch (ParseException e) {
736                // re-throw
737                throw e;
738            } 
739        }
740    
741        /**
742         * Cancels the editor's changes and notifies ActionListeners.
743         * 
744         */
745        public void cancelEdit() {
746            // hmmm... no direct api?
747             _dateField.setValue(_dateField.getValue());
748             fireActionPerformed(CANCEL_KEY);
749        }
750    
751        /**
752         * Sets the editable property. If false, ...?
753         * 
754         * The default value is true.
755         * 
756         * @param value
757         * @see #isEditable()
758         */
759        public void setEditable(boolean value) {
760            boolean oldEditable = isEditable();
761            editable = value;
762            firePropertyChange("editable", oldEditable, editable);
763            if (editable != oldEditable) {
764                repaint();
765            }
766        }
767    
768        /**
769         * Returns the editable property. 
770         * 
771         * @return {@code true} if the picker is editable; {@code false} otherwise
772         */
773        public boolean isEditable() {
774            return editable;
775        }
776    
777        /**
778         * Returns the font that is associated with the editor of this date picker.
779         */
780        @Override
781        public Font getFont() {
782            return getEditor().getFont();
783        }
784    
785        /**
786         * Set the font for the editor associated with this date picker.
787         */
788        @Override
789        public void setFont(final Font font) {
790            getEditor().setFont(font);
791        }
792    
793        /**
794         * Sets the <code>lightWeightPopupEnabled</code> property, which
795         * provides a hint as to whether or not a lightweight
796         * <code>Component</code> should be used to contain the
797         * <code>JXDatePicker</code>, versus a heavyweight
798         * <code>Component</code> such as a <code>Panel</code>
799         * or a <code>Window</code>.  The decision of lightweight
800         * versus heavyweight is ultimately up to the
801         * <code>JXDatePicker</code>.  Lightweight windows are more
802         * efficient than heavyweight windows, but lightweight
803         * and heavyweight components do not mix well in a GUI.
804         * If your application mixes lightweight and heavyweight
805         * components, you should disable lightweight popups.
806         * The default value for the <code>lightWeightPopupEnabled</code>
807         * property is <code>true</code>, unless otherwise specified
808         * by the look and feel.  Some look and feels always use
809         * heavyweight popups, no matter what the value of this property.
810         * <p/>
811         * See the article <a href="http://java.sun.com/products/jfc/tsc/articles/mixing/index.html">Mixing Heavy and Light Components</a>
812         * on <a href="http://java.sun.com/products/jfc/tsc">
813         * <em>The Swing Connection</em></a>
814         * This method fires a property changed event.
815         *
816         * @param aFlag if <code>true</code>, lightweight popups are desired
817         * @beaninfo bound: true
818         * expert: true
819         * description: Set to <code>false</code> to require heavyweight popups.
820         */
821        public void setLightWeightPopupEnabled(boolean aFlag) {
822            boolean oldFlag = lightWeightPopupEnabled;
823            lightWeightPopupEnabled = aFlag;
824            firePropertyChange("lightWeightPopupEnabled", oldFlag, lightWeightPopupEnabled);
825        }
826    
827        /**
828         * Gets the value of the <code>lightWeightPopupEnabled</code>
829         * property.
830         *
831         * @return the value of the <code>lightWeightPopupEnabled</code>
832         *         property
833         * @see #setLightWeightPopupEnabled
834         */
835        public boolean isLightWeightPopupEnabled() {
836            return lightWeightPopupEnabled;
837        }
838    
839        /**
840         * Get the baseline for the specified component, or a value less
841         * than 0 if the baseline can not be determined.  The baseline is measured
842         * from the top of the component.
843         *
844         * @param width  Width of the component to determine baseline for.
845         * @param height Height of the component to determine baseline for.
846         * @return baseline for the specified component
847         */
848        public int getBaseline(int width, int height) {
849            return ((DatePickerUI) ui).getBaseline(width, height);
850        }
851    
852    
853        /**
854         * Adds an ActionListener.
855         * <p/>
856         * The ActionListener will receive an ActionEvent when a selection has
857         * been made.
858         *
859         * @param l The ActionListener that is to be notified
860         */
861        public void addActionListener(ActionListener l) {
862            listenerMap.add(ActionListener.class, l);
863        }
864    
865        /**
866         * Removes an ActionListener.
867         *
868         * @param l The action listener to remove.
869         */
870        public void removeActionListener(ActionListener l) {
871            listenerMap.remove(ActionListener.class, l);
872        }
873    
874        @Override
875        @SuppressWarnings("unchecked")
876        public <T extends EventListener> T[] getListeners(Class<T> listenerType) {
877            java.util.List<T> listeners = listenerMap.getListeners(listenerType);
878            T[] result;
879            if (!listeners.isEmpty()) {
880                //noinspection unchecked
881                result = (T[]) java.lang.reflect.Array.newInstance(listenerType, listeners.size());
882                result = listeners.toArray(result);
883            } else {
884                result = super.getListeners(listenerType);
885            }
886            return result;
887        }
888    
889    
890        /**
891         * Fires an ActionEvent with the given actionCommand
892         * to all listeners.
893         */
894        protected void fireActionPerformed(String actionCommand) {
895            ActionListener[] listeners = getListeners(ActionListener.class);
896            ActionEvent e = null;
897    
898            for (ActionListener listener : listeners) {
899                if (e == null) {
900                    e = new ActionEvent(this,
901                            ActionEvent.ACTION_PERFORMED,
902                            actionCommand);
903                }
904                listener.actionPerformed(e);
905            }
906        }
907     
908        /**
909         * Pes: added setLocale method to refresh link text on locale changes
910         */
911        private final class TodayPanel extends JXPanel {
912            private TodayAction todayAction;
913            private JXHyperlink todayLink;
914    
915            TodayPanel() {
916                super(new FlowLayout());
917                setBackgroundPainter(new MattePainter(new GradientPaint(0, 0, new Color(238, 238, 238), 0, 1, Color.WHITE)));
918                todayAction = new TodayAction();
919                todayLink = new JXHyperlink(todayAction);
920                todayLink.addMouseListener(createDoubleClickListener());
921                Color textColor = new Color(16, 66, 104);
922                todayLink.setUnclickedColor(textColor);
923                todayLink.setClickedColor(textColor);
924                add(todayLink);
925            }
926    
927            /**
928             * @return
929             */
930            private MouseListener createDoubleClickListener() {
931                MouseAdapter adapter = new MouseAdapter() {
932    
933                    @Override
934                    public void mousePressed(MouseEvent e) {
935                        if (e.getClickCount() != 2) return;
936                        todayAction.select = true;
937                    }
938                    
939                };
940                return adapter;
941            }
942    
943            @Override
944            protected void paintComponent(Graphics g) {
945                super.paintComponent(g);
946                g.setColor(new Color(187, 187, 187));
947                g.drawLine(0, 0, getWidth(), 0);
948                g.setColor(new Color(221, 221, 221));
949                g.drawLine(0, 1, getWidth(), 1);
950            }
951    
952            /**
953             * {@inheritDoc} <p>
954             *  Overridden to update the link format and hyperlink text.
955             */
956            @Override
957            public void setLocale(Locale l) {
958                super.setLocale(l);
959                updateLinkFormat();
960                todayLink.setText(getLinkFormat().format(new Object[]{getLinkDay()}));
961            }
962            
963            private final class TodayAction extends AbstractAction {
964                boolean select;
965                TodayAction() {
966                    super(getLinkFormat().format(new Object[]{getLinkDay()}));
967                    Calendar cal = _monthView.getCalendar();
968                    cal.setTime(getLinkDay());
969                    putValue(NAME, getLinkFormat().format(new Object[] {cal.getTime()}));
970                }
971    
972                public void actionPerformed(ActionEvent ae) {
973                    String key = select ? JXDatePicker.HOME_COMMIT_KEY : JXDatePicker.HOME_NAVIGATE_KEY;
974                    select = false;
975                    Action delegate = getActionMap().get(key);
976                    /*
977                     * PatrykRy: Commit today date only when commit action is enabled.
978                     * Home navigate is always enabled.
979                     */
980                    if (delegate !=  null && delegate.isEnabled()) {  
981                        delegate.actionPerformed(null);
982                    }
983                    
984                }
985            }
986        }
987    
988    
989    }    
990