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