001/*
002 * $Id: JXMonthView.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.Insets;
025import java.awt.event.ActionEvent;
026import java.awt.event.ActionListener;
027import java.util.Calendar;
028import java.util.Date;
029import java.util.EventListener;
030import java.util.Hashtable;
031import java.util.Locale;
032import java.util.SortedSet;
033import java.util.TimeZone;
034import java.util.TreeSet;
035import java.util.logging.Logger;
036
037import javax.swing.JComponent;
038import javax.swing.Timer;
039import javax.swing.UIManager;
040
041import org.jdesktop.beans.JavaBean;
042import org.jdesktop.swingx.calendar.CalendarUtils;
043import org.jdesktop.swingx.calendar.DateSelectionModel;
044import org.jdesktop.swingx.calendar.DaySelectionModel;
045import org.jdesktop.swingx.calendar.DateSelectionModel.SelectionMode;
046import org.jdesktop.swingx.event.DateSelectionEvent;
047import org.jdesktop.swingx.event.DateSelectionListener;
048import org.jdesktop.swingx.event.EventListenerMap;
049import org.jdesktop.swingx.event.DateSelectionEvent.EventType;
050import org.jdesktop.swingx.plaf.LookAndFeelAddons;
051import org.jdesktop.swingx.plaf.MonthViewAddon;
052import org.jdesktop.swingx.plaf.MonthViewUI;
053import org.jdesktop.swingx.util.Contract;
054
055
056/**
057 * Component that displays a month calendar which can be used to select a day
058 * or range of days.  By default the <code>JXMonthView</code> will display a
059 * single calendar using the current month and year, using
060 * <code>Calendar.SUNDAY</code> as the first day of the week.
061 * <p>
062 * The <code>JXMonthView</code> can be configured to display more than one
063 * calendar at a time by calling
064 * <code>setPreferredCalCols</code>/<code>setPreferredCalRows</code>.  These
065 * methods will set the preferred number of calendars to use in each
066 * column/row.  As these values change, the <code>Dimension</code> returned
067 * from <code>getMinimumSize</code> and <code>getPreferredSize</code> will
068 * be updated.  The following example shows how to create a 2x2 view which is
069 * contained within a <code>JFrame</code>:
070 * <pre>
071 *     JXMonthView monthView = new JXMonthView();
072 *     monthView.setPreferredCols(2);
073 *     monthView.setPreferredRows(2);
074 *
075 *     JFrame frame = new JFrame();
076 *     frame.getContentPane().add(monthView);
077 *     frame.pack();
078 *     frame.setVisible(true);
079 * </pre>
080 * <p>
081 * <code>JXMonthView</code> can be further configured to allow any day of the
082 * week to be considered the first day of the week.  Character
083 * representation of those days may also be set by providing an array of
084 * strings.
085 * <pre>
086 *    monthView.setFirstDayOfWeek(Calendar.MONDAY);
087 *    monthView.setDaysOfTheWeek(
088 *            new String[]{"S", "M", "T", "W", "Th", "F", "S"});
089 * </pre>
090 * <p>
091 * This component supports flagging days.  These flagged days are displayed
092 * in a bold font.  This can be used to inform the user of such things as
093 * scheduled appointment.
094 * <pre><code>
095 *    // Create some dates that we want to flag as being important.
096 *    Calendar cal1 = Calendar.getInstance();
097 *    cal1.set(2004, 1, 1);
098 *    Calendar cal2 = Calendar.getInstance();
099 *    cal2.set(2004, 1, 5);
100 *
101 *    monthView.setFlaggedDates(cal1.getTime(), cal2.getTime(), new Date());
102 * </code></pre>
103 * Applications may have the need to allow users to select different ranges of
104 * dates.  There are three modes of selection that are supported, single, single interval
105 * and multiple interval selection.  Once a selection is made a DateSelectionEvent is
106 * fired to inform listeners of the change.
107 * <pre>
108 *    // Change the selection mode to select full weeks.
109 *    monthView.setSelectionMode(SelectionMode.SINGLE_INTERVAL_SELECTION);
110 *
111 *    // Register a date selection listener to get notified about
112 *    // any changes in the date selection model.
113 *    monthView.getSelectionModel().addDateSelectionListener(new DateSelectionListener {
114 *        public void valueChanged(DateSelectionEvent e) {
115 *            log.info(e.getSelection());
116 *        }
117 *    });
118 * </pre>
119 * 
120 * NOTE (for users of earlier versions): as of version 1.19 control about selection 
121 * dates is moved completely into the model. The default model used is of type 
122 * DaySelectionModel, which handles dates in the same way the JXMonthView did earlier
123 * (that is, normalize all to the start of the day, which means zeroing all time
124 * fields).<p>
125 * 
126 * @author Joshua Outwater
127 * @author Jeanette Winzenburg
128 * @version  $Revision: 4147 $
129 */
130@JavaBean
131public class JXMonthView extends JComponent {
132    @SuppressWarnings("unused")
133    private static final Logger LOG = Logger.getLogger(JXMonthView.class
134            .getName());
135    /*
136     * moved from package calendar to swingx at version 1.51
137     */
138
139    /** action command used for commit actionEvent. */
140    public static final String COMMIT_KEY = "monthViewCommit";
141    /** action command used for cancel actionEvent. */
142    public static final String CANCEL_KEY = "monthViewCancel";
143
144    public static final String BOX_PADDING_X = "boxPaddingX";
145    public static final String BOX_PADDING_Y = "boxPaddingY";
146    public static final String DAYS_OF_THE_WEEK = "daysOfTheWeek";
147    public static final String SELECTION_MODEL = "selectionModel";
148    public static final String TRAVERSABLE = "traversable";
149    public static final String FLAGGED_DATES = "flaggedDates";
150
151    static {
152        LookAndFeelAddons.contribute(new MonthViewAddon());
153    }
154
155     /**
156     * UI Class ID
157     */
158    public static final String uiClassID = "MonthViewUI";
159
160    public static final int DAYS_IN_WEEK = 7;
161    public static final int MONTHS_IN_YEAR = 12;
162
163
164    /**
165     * Keeps track of the first date we are displaying.  We use this as a
166     * restore point for the calendar. This is normalized to the start of the
167     * first day of the month given in setFirstDisplayedDate.
168     */
169    private Date firstDisplayedDay;
170    /** 
171     * the calendar to base all selections, flagging upon. 
172     * NOTE: the time of this calendar is undefined - before using, internal
173     * code must explicitly set it.
174     * PENDING JW: as of version 1.26 all calendar/properties are controlled by the model.
175     * We keep a clone of the model's calendar here for notification reasons: 
176     * model fires DateSelectionEvent of type CALENDAR_CHANGED which neiter carry the
177     * oldvalue nor the property name needed to map into propertyChange notification.
178     */
179    private Calendar cal;
180    /** calendar to store the real input of firstDisplayedDate. */
181    private Calendar anchor;
182    /** 
183     * Start of the day which contains System.millis() in the current calendar.
184     * Kept in synch via a timer started in addNotify.
185     */
186    private Date today;
187    /**
188     * The timer used to keep today in synch with system time.
189     */
190    private Timer todayTimer;
191    // PENDING JW: why kept apart from cal? Why writable? - shouldn't the calendar have complete
192    // control?
193    private int firstDayOfWeek;
194    //-------------- selection/flagging
195    /** 
196     * The DateSelectionModel driving this component. This model's calendar
197     * is the reference for all dates.
198     */
199    private DateSelectionModel model;
200    /**
201     * Listener registered with the current model to keep Calendar dependent
202     * state synched.
203     */
204    private DateSelectionListener modelListener;
205    /** 
206     * The manager of the flagged dates. Note
207     * that the type of this is an implementation detail.  
208     */
209    private DaySelectionModel flaggedDates;
210    /**
211     * Storage of actionListeners registered with the monthView.
212     */
213    private EventListenerMap listenerMap;
214    
215    private boolean traversable;
216    private boolean leadingDays;
217    private boolean trailingDays;
218    private boolean showWeekNumber;
219    private boolean componentInputMapEnabled;
220    
221    //-------------------
222    // PENDING JW: ??
223    @SuppressWarnings({"FieldCanBeLocal"})
224    protected Date modifiedStartDate;
225    @SuppressWarnings({"FieldCanBeLocal"})
226    protected Date modifiedEndDate;
227    
228    //------------- visuals
229    
230    /**
231     * Localizable day column headers. Default typically installed by the uidelegate.
232     */
233    private String[] _daysOfTheWeek;
234
235    protected Insets _monthStringInsets = new Insets(0, 0, 0, 0);
236    private int boxPaddingX;
237    private int boxPaddingY;
238    private int minCalCols = 1;
239    private int minCalRows = 1;
240    private Color todayBackgroundColor;
241    private Color monthStringBackground;
242    private Color monthStringForeground;
243    private Color daysOfTheWeekForeground;
244    private Color selectedBackground;
245    private Hashtable<Integer, Color> dayToColorTable = new Hashtable<Integer, Color>();
246    private Color flaggedDayForeground;
247
248    private Color selectedForeground;
249    private boolean zoomable;
250
251    /**
252     * Create a new instance of the <code>JXMonthView</code> class using the
253     * default Locale and the current system time as the first date to 
254     * display.
255     */
256    public JXMonthView() {
257        this(null, null, null);
258    }
259
260    /**
261     * Create a new instance of the <code>JXMonthView</code> class using the 
262     * default Locale and the current system time as the first date to 
263     * display.
264     * 
265     * @param locale desired locale, if null the system default locale is used
266     */
267    public JXMonthView(final Locale locale) {
268        this(null, null, locale);
269    }
270
271    /**
272     * Create a new instance of the <code>JXMonthView</code> class using the
273     * default Locale and the given time as the first date to 
274     * display.
275     *
276     * @param firstDisplayedDay a day of the first month to display; if null, the current
277     *   System time is used.
278     */
279    public JXMonthView(Date firstDisplayedDay) {
280        this(firstDisplayedDay, null, null);
281    }
282
283    /**
284     * Create a new instance of the <code>JXMonthView</code> class using the
285     * default Locale, the given time as the first date to 
286     * display and the given selection model. 
287     * 
288     * @param firstDisplayedDay a day of the first month to display; if null, the current
289     *   System time is used.
290     * @param model the selection model to use, if null a <code>DefaultSelectionModel</code> is
291     *   created.
292     */
293    public JXMonthView(Date firstDisplayedDay, final DateSelectionModel model) {
294        this(firstDisplayedDay, model, null);
295    }
296
297
298    /**
299     * Create a new instance of the <code>JXMonthView</code> class using the
300     * given Locale, the given time as the first date to 
301     * display and the given selection model. 
302     * 
303     * @param firstDisplayedDay a day of the first month to display; if null, the current
304     *   System time is used.
305     * @param model the selection model to use, if null a <code>DefaultSelectionModel</code> is
306     *   created.
307     * @param locale desired locale, if null the system default locale is used
308     */
309    public JXMonthView(Date firstDisplayedDay, final DateSelectionModel model, final Locale locale) {
310        super();
311        listenerMap = new EventListenerMap();
312
313        initModel(model, locale);
314        superSetLocale(locale);
315        setFirstDisplayedDay(firstDisplayedDay != null ? firstDisplayedDay : getCurrentDate());
316        // Keep track of today
317        updateTodayFromCurrentTime();
318
319        // install the controller
320        updateUI();
321
322        setFocusable(true);
323        todayBackgroundColor = getForeground();
324
325    }
326
327    
328//------------------ Calendar related properties
329    
330    /**
331     * Sets locale and resets text and format used to display months and days. 
332     * Also resets firstDayOfWeek. <p>
333     * 
334     * PENDING JW: the following warning should be obsolete (installCalendar
335     * should take care) - check if it really is!
336     * 
337     * <p>
338     * <b>Warning:</b> Since this resets any string labels that are cached in UI
339     * (month and day names) and firstDayofWeek, use <code>setDaysOfTheWeek</code> and/or
340     * setFirstDayOfWeek after (re)setting locale.
341     * </p>
342     * 
343     * @param   locale new Locale to be used for formatting
344     * @see     #setDaysOfTheWeek(String[])
345     * @see     #setFirstDayOfWeek(int)
346     */
347    @Override
348    public void setLocale(Locale locale) {
349        model.setLocale(locale);
350    }
351
352    /**
353     * 
354     * @param locale
355     */
356    private void superSetLocale(Locale locale) {
357        // PENDING JW: formally, a null value is allowed and must be passed on to super
358        // I suspect this is not done here to keep the logic out off the constructor?
359        // 
360        if (locale != null) {
361            super.setLocale(locale);
362            repaint();
363       }
364    }
365    
366    /**
367     * Returns a clone of the internal calendar, with it's time set to firstDisplayedDate.
368     * 
369     * PENDING: firstDisplayed useful as reference time? It's timezone dependent anyway. 
370     * Think: could return with monthView's today instead? 
371     * 
372     * @return a clone of internal calendar, configured to the current firstDisplayedDate
373     * @throws IllegalStateException if called before instantitation is completed
374     */
375    public Calendar getCalendar() {
376        // JW: this is to guard against a regression of not-fully understood 
377        // problems in constructor (UI used to call back into this before we were ready)
378        if (cal == null) throw 
379            new IllegalStateException("must not be called before instantiation is complete");
380        Calendar calendar = (Calendar) cal.clone();
381        calendar.setTime(firstDisplayedDay);
382        return calendar;
383    }
384
385    /**
386     * Gets the time zone.
387     *
388     * @return The <code>TimeZone</code> used by the <code>JXMonthView</code>.
389     */
390    public TimeZone getTimeZone() {
391        // PENDING JW: looks fishy (left-over?) .. why not ask the model?
392        return cal.getTimeZone();
393    }
394
395    /**
396     * Sets the time zone with the given time zone value.
397     * 
398     * This is a bound property. 
399     * 
400     * @param tz The <code>TimeZone</code>.
401     */
402    public void setTimeZone(TimeZone tz) {
403        model.setTimeZone(tz);
404    }
405
406    /**
407     * Gets what the first day of the week is; e.g.,
408     * <code>Calendar.SUNDAY</code> in the U.S., <code>Calendar.MONDAY</code>
409     * in France.
410     *
411     * @return int The first day of the week.
412     */
413    public int getFirstDayOfWeek() {
414        return firstDayOfWeek;
415    }
416
417    /**
418     * Sets what the first day of the week is; e.g.,
419     * <code>Calendar.SUNDAY</code> in US, <code>Calendar.MONDAY</code>
420     * in France.
421     *
422     * @param firstDayOfWeek The first day of the week.
423     * @see java.util.Calendar
424     */
425    public void setFirstDayOfWeek(int firstDayOfWeek) {
426        getSelectionModel().setFirstDayOfWeek(firstDayOfWeek);
427    }
428
429
430
431//---------------------- synch to model's calendar    
432
433    /**
434     * Initializes selection model related internals. If the Locale is
435     * null, it falls back to JComponent.defaultLocale. If the model
436     * is null it creates a default model with the locale.
437     * 
438     * PENDING JW: leave default locale fallback to model?
439     * 
440     * @param model the DateSelectionModel which should drive the monthView. 
441     *    If null, a default model is created and initialized with the given locale.
442     * @param locale the Locale to use with the selectionModel. If null,
443     *   JComponent.getDefaultLocale is used.
444     */
445    private void initModel(DateSelectionModel model, Locale locale) {
446        if (locale == null) {
447            locale = JComponent.getDefaultLocale();
448        }
449        if (model == null) {
450            model = new DaySelectionModel(locale);
451        }
452        this.model = model;
453        // PENDING JW: do better to synchronize Calendar related 
454        // properties of flaggedDates to those of the selection model.
455        // plus: should use the same normalization?
456        this.flaggedDates = new DaySelectionModel(locale);
457        flaggedDates.setSelectionMode(SelectionMode.MULTIPLE_INTERVAL_SELECTION);
458        
459        installCalendar();
460        model.addDateSelectionListener(getDateSelectionListener());
461    }
462
463    /**
464     * Lazily creates and returns the DateSelectionListener which listens
465     * for model's calendar properties.
466     * 
467     * @return a DateSelectionListener for model's CALENDAR_CHANGED notification.
468     */
469    private DateSelectionListener getDateSelectionListener() {
470        if (modelListener == null) {
471            modelListener = new DateSelectionListener() {
472
473                @Override
474                public void valueChanged(DateSelectionEvent ev) {
475                    if (EventType.CALENDAR_CHANGED.equals(ev.getEventType())) {
476                        updateCalendar();
477                    }
478                    
479                }
480                
481            };
482        }
483        return modelListener;
484    }
485
486    /**
487     * Installs the internal calendars from the selection model.<p>
488     * 
489     * PENDING JW: in fixing #11433, added update of firstDisplayedDay and
490     * today here - check if correct place to do so. 
491     * 
492     */
493    private void installCalendar() {
494        cal = model.getCalendar();
495        firstDayOfWeek = cal.getFirstDayOfWeek();
496        Date anchorDate = getAnchorDate();
497        anchor = (Calendar) cal.clone();
498        if (anchorDate != null) {
499            setFirstDisplayedDay(anchorDate);
500        }
501        updateTodayFromCurrentTime();
502    }
503
504    /**
505     * Returns the anchor date. Currently, this is the "uncleaned" input date 
506     * of setFirstDisplayedDate. This is a quick hack for Issue #618-swingx, to
507     * have some invariant for testing. Do not use in client code, may change
508     * without notice!
509     * 
510     * @return the "uncleaned" first display date or null if the firstDisplayedDay
511     *   is not yet set.
512     */
513    protected Date getAnchorDate() {
514        return anchor != null ? anchor.getTime() : null;
515    }
516
517    /**
518     * Callback from selection model calendar changes.
519     */
520    private void updateCalendar() {
521       if (!getLocale().equals(model.getLocale())) {
522           installCalendar();
523           superSetLocale(model.getLocale());
524       } else {
525           if (!model.getTimeZone().equals(getTimeZone())) {
526               updateTimeZone();
527           }
528           if (cal.getMinimalDaysInFirstWeek() != model.getMinimalDaysInFirstWeek()) {
529               updateMinimalDaysOfFirstWeek();
530           }
531           if (cal.getFirstDayOfWeek() != model.getFirstDayOfWeek()) {
532              updateFirstDayOfWeek(); 
533           }
534       }
535    }
536
537
538    /**
539     * Callback from changing timezone in model.
540     */
541    private void updateTimeZone() {
542        TimeZone old = getTimeZone();
543        TimeZone tz = model.getTimeZone();
544        cal.setTimeZone(tz);
545        anchor.setTimeZone(tz);
546        setFirstDisplayedDay(anchor.getTime());
547        updateTodayFromCurrentTime();
548        updateDatesAfterTimeZoneChange(old);
549        firePropertyChange("timeZone", old, getTimeZone());
550    }
551    
552    /**
553     * All dates are "cleaned" relative to the timezone they had been set.
554     * After changing the timezone, they need to be updated to the new.
555     * 
556     * Here: clear everything. 
557     * 
558     * @param oldTimeZone the timezone before the change
559     */
560    protected void updateDatesAfterTimeZoneChange(TimeZone oldTimeZone) {
561        SortedSet<Date> flagged = getFlaggedDates();
562        flaggedDates.setTimeZone(getTimeZone());
563        firePropertyChange("flaggedDates", flagged, getFlaggedDates());
564     }
565    
566    /**
567     * Call back from listening to model firstDayOfWeek change.
568     */
569    private void updateFirstDayOfWeek() {
570        int oldFirstDayOfWeek = this.firstDayOfWeek;
571
572        firstDayOfWeek = getSelectionModel().getFirstDayOfWeek();
573        cal.setFirstDayOfWeek(firstDayOfWeek);
574        anchor.setFirstDayOfWeek(firstDayOfWeek);
575        firePropertyChange("firstDayOfWeek", oldFirstDayOfWeek, firstDayOfWeek);
576    }
577
578    /**
579     * Call back from listening to model minimalDaysOfFirstWeek change.
580     * <p>
581     * NOTE: this is not a property as we have no public api to change
582     * it on JXMonthView.
583     */
584    private void updateMinimalDaysOfFirstWeek() {
585        cal.setMinimalDaysInFirstWeek(model.getMinimalDaysInFirstWeek());
586        anchor.setMinimalDaysInFirstWeek(model.getMinimalDaysInFirstWeek());
587    }
588
589
590    
591//-------------------- scrolling
592    /**
593     * Returns the last date able to be displayed.  For example, if the last
594     * visible month was April the time returned would be April 30, 23:59:59.
595     *
596     * @return long The last displayed date.
597     */
598    public Date getLastDisplayedDay() {
599        return getUI().getLastDisplayedDay();
600    }
601
602    
603    /**
604     * Returns the first displayed date.
605     *
606     * @return long The first displayed date.
607     */
608    public Date getFirstDisplayedDay() {
609        return firstDisplayedDay;
610    }
611
612    
613    /**
614     * Set the first displayed date.  We only use the month and year of
615     * this date.  The <code>Calendar.DAY_OF_MONTH</code> field is reset to
616     * 1 and all other fields, with exception of the year and month,
617     * are reset to 0.
618     *
619     * @param date The first displayed date.
620     */
621    public void setFirstDisplayedDay(Date date) {
622        anchor.setTime(date);
623        Date oldDate = getFirstDisplayedDay();
624
625        cal.setTime(anchor.getTime());
626        CalendarUtils.startOfMonth(cal);
627        firstDisplayedDay = cal.getTime();
628        firePropertyChange("firstDisplayedDay", oldDate, getFirstDisplayedDay() );
629    }
630
631    /**
632     * Moves the <code>date</code> into the visible region of the calendar. If
633     * the date is greater than the last visible date it will become the last
634     * visible date. While if it is less than the first visible date it will
635     * become the first visible date. <p>
636     * 
637     * NOTE: this is the recommended method to scroll to a particular date, the
638     * functionally equivalent method taking a long as parameter will most 
639     * probably be deprecated.
640     * 
641     * @param date Date to make visible, must not be null.
642     * @see #ensureDateVisible(Date)
643     */
644    public void ensureDateVisible(Date date) {
645        if (date.before(firstDisplayedDay)) {
646            setFirstDisplayedDay(date);
647        } else {
648            Date lastDisplayedDate = getLastDisplayedDay();
649            if (date.after(lastDisplayedDate)) {
650                // extract to CalendarUtils!
651                cal.setTime(date);
652                int month = cal.get(Calendar.MONTH);
653                int year = cal.get(Calendar.YEAR);
654
655                cal.setTime(lastDisplayedDate);
656                int lastMonth = cal.get(Calendar.MONTH);
657                int lastYear = cal.get(Calendar.YEAR);
658
659                int diffMonths = month - lastMonth
660                        + ((year - lastYear) * MONTHS_IN_YEAR);
661
662                cal.setTime(firstDisplayedDay);
663                cal.add(Calendar.MONTH, diffMonths);
664                setFirstDisplayedDay(cal.getTime());
665            }
666        }
667    }
668
669
670    /**
671     * Returns the Date at the given location. May be null if the
672     * coordinates don't map to a day in the month which contains the 
673     * coordinates. Specifically: hitting leading/trailing dates returns null.
674     * 
675     * Mapping pixel to calendar day.
676     *
677     * @param x the x position of the location in pixel
678     * @param y the y position of the location in pixel
679     * @return the day at the given location or null if the location
680     *   doesn't map to a day in the month which contains the coordinates.
681     */ 
682    public Date getDayAtLocation(int x, int y) {
683        return getUI().getDayAtLocation(x, y);
684    }
685   
686//------------------ today
687
688    /**
689     * Returns the current Date (whateverthatmeans). Internally always invoked when
690     * the current default is needed. Introduced mainly for testing, don't override!
691     * 
692     * This implementation returns a Date instantiated with <code>System.currentTimeInMillis</code>.
693     * 
694     * @return the date deemed as current. 
695     */
696    Date getCurrentDate() {
697        return new Date(System.currentTimeMillis());
698    }
699
700
701    /**
702     * Sets today from the current system time. 
703     * 
704     * temporary widened access for testing.
705     */
706    protected void updateTodayFromCurrentTime() {
707        setToday(getCurrentDate());
708    }
709
710    /**
711     * Increments today. This is used by the timer.
712     * 
713     * PENDING: is it safe? doesn't check if we are really tomorrow?
714     * temporary widened access for testing.
715     */
716    protected void incrementToday() {
717        cal.setTime(getToday());
718        cal.add(Calendar.DAY_OF_MONTH, 1);
719        setToday(cal.getTime());
720    }
721
722    /**
723     * Sets the date which represents today. Internally 
724     * modified to the start of the day which contains the
725     * given date in this monthView's calendar coordinates.
726     *  
727     * temporary widened access for testing.
728     * 
729     * @param date the date which should be used as today.
730     */
731    protected void setToday(Date date) {
732        Date oldToday = getToday();
733        // PENDING JW: do we really want the start of today? 
734        this.today = startOfDay(date);
735        firePropertyChange("today", oldToday, getToday());
736    }
737
738    /**
739     * Returns the start of today in this monthviews calendar coordinates.
740     * 
741     * @return the start of today as Date.
742     */
743    public Date getToday() {
744        // null only happens in the very first time ... 
745        return today != null ? (Date) today.clone() : null;
746    }
747
748    
749    
750//----   internal date manipulation ("cleanup" == start of day in monthView's calendar)
751    
752
753    /**
754     * Returns the start of the day as Date.
755     * 
756     * @param date the Date.
757     * @return start of the given day as Date, relative to this
758     *    monthView's calendar.
759     *    
760     */
761    private Date startOfDay(Date date) {
762        return CalendarUtils.startOfDay(cal, date);
763    }
764
765//------------------- ui delegate    
766    /**
767     * @inheritDoc
768     */
769    public MonthViewUI getUI() {
770        return (MonthViewUI)ui;
771    }
772
773    /**
774     * Sets the L&F object that renders this component.
775     *
776     * @param ui UI to use for this {@code JXMonthView}
777     */
778    public void setUI(MonthViewUI ui) {
779        super.setUI(ui);
780    }
781
782    /**
783     * Resets the UI property with the value from the current look and feel.
784     *
785     * @see UIManager#getUI(JComponent)
786     */
787    @Override
788    public void updateUI() {
789        setUI((MonthViewUI)LookAndFeelAddons.getUI(this, MonthViewUI.class));
790        invalidate();
791    }
792
793    /**
794     * @inheritDoc
795     */
796    @Override
797    public String getUIClassID() {
798        return uiClassID;
799    }
800
801    
802//---------------- DateSelectionModel
803
804    /**
805     * Returns the date selection model which drives this
806     * JXMonthView.
807     * 
808     * @return the date selection model
809     */
810    public DateSelectionModel getSelectionModel() {
811        return model;
812    }
813
814    /**
815     * Sets the date selection model to drive this monthView.
816     * 
817     * @param model the selection model to use, must not be null.
818     * @throws NullPointerException if model is null
819     */
820    public void setSelectionModel(DateSelectionModel model) {
821        Contract.asNotNull(model, "date selection model must not be null");
822        DateSelectionModel oldModel = getSelectionModel();
823        model.removeDateSelectionListener(getDateSelectionListener());
824        this.model = model;
825        installCalendar();
826        if (!model.getLocale().equals(getLocale())) {
827            super.setLocale(model.getLocale());
828        }
829        model.addDateSelectionListener(getDateSelectionListener());
830        firePropertyChange(SELECTION_MODEL, oldModel, getSelectionModel());
831    }
832
833//-------------------- delegates to model
834    
835    /**
836     * Clear any selection from the selection model
837     */
838    public void clearSelection() {
839        getSelectionModel().clearSelection();
840    }
841
842    /**
843     * Return true if the selection is empty, false otherwise
844     *
845     * @return true if the selection is empty, false otherwise
846     */
847    public boolean isSelectionEmpty() {
848        return getSelectionModel().isSelectionEmpty();
849    }
850
851    /**
852     * Get the current selection
853     *
854     * @return sorted set of selected dates
855     */
856   public SortedSet<Date> getSelection() {
857        return getSelectionModel().getSelection();
858    }
859
860    /**
861     * Adds the selection interval to the selection model. 
862     * 
863     * @param startDate Start of date range to add to the selection
864     * @param endDate End of date range to add to the selection
865     */
866    public void addSelectionInterval(Date startDate, Date endDate) {
867            getSelectionModel().addSelectionInterval(startDate, endDate);
868    }
869
870    /**
871     * Sets the selection interval to the selection model.  
872     *
873     * @param startDate Start of date range to set the selection to
874     * @param endDate End of date range to set the selection to
875     */
876    public void setSelectionInterval(final Date startDate, final Date endDate) {
877            getSelectionModel().setSelectionInterval(startDate, endDate);
878    }
879
880    /**
881     * Removes the selection interval from the selection model.  
882     * 
883     * @param startDate Start of the date range to remove from the selection
884     * @param endDate End of the date range to remove from the selection
885     */
886    public void removeSelectionInterval(final Date startDate, final Date endDate) {
887        getSelectionModel().removeSelectionInterval(startDate, endDate);
888    }
889
890    /**
891     * Returns the current selection mode for this JXMonthView.
892     *
893     * @return int Selection mode.
894     */
895    public SelectionMode getSelectionMode() {
896        return getSelectionModel().getSelectionMode();
897    }
898
899    /**
900     * Set the selection mode for this JXMonthView.
901
902     * @param selectionMode The selection mode to use for this {@code JXMonthView}
903     */
904    public void setSelectionMode(final SelectionMode selectionMode) {
905        getSelectionModel().setSelectionMode(selectionMode);
906    }
907
908    /**
909     * Returns the earliest selected date. 
910     * 
911     *   
912     * @return the first Date in the selection or null if empty.
913     */
914    public Date getFirstSelectionDate() {
915        return getSelectionModel().getFirstSelectionDate();    
916     }
917   
918
919    /**
920     * Returns the earliest selected date. 
921     * 
922     * @return the first Date in the selection or null if empty.
923     */
924    public Date getLastSelectionDate() {
925        return getSelectionModel().getLastSelectionDate();    
926     }
927
928    /**
929     * Returns the earliest selected date. 
930     * 
931     * PENDING JW: keep this? it was introduced before the first/last 
932     *   in model. When delegating everything, we duplicate here.
933     *   
934     * @return the first Date in the selection or null if empty.
935     */
936    public Date getSelectionDate() {
937        return getFirstSelectionDate();    
938    }
939
940    /**
941     * Sets the model's selection to the given date or clears the selection if
942     * null.
943     * 
944     * @param newDate the selection date to set
945     */
946    public void setSelectionDate(Date newDate) {
947        if (newDate == null) {
948            clearSelection();
949        } else {
950            setSelectionInterval(newDate, newDate);
951        }
952    }
953
954    /**
955     * Returns true if the specified date falls within the _startSelectedDate
956     * and _endSelectedDate range.  
957     *
958     * @param date The date to check
959     * @return true if the date is selected, false otherwise
960     */
961    public boolean isSelected(Date date) {
962        return getSelectionModel().isSelected(date);
963    }
964
965
966    /**
967     * Set the lower bound date that is allowed to be selected. <p>
968     * 
969     * 
970     * @param lowerBound the lower bound, null means none.
971     */
972    public void setLowerBound(Date lowerBound) {
973        getSelectionModel().setLowerBound(lowerBound);
974    }
975
976    /**
977     * Set the upper bound date that is allowed to be selected. <p>
978     * 
979     * @param upperBound the upper bound, null means none.
980     */
981    public void setUpperBound(Date upperBound) {
982        getSelectionModel().setUpperBound(upperBound);
983    }
984
985
986    /**
987     * Return the lower bound date that is allowed to be selected for this
988     * model.
989     *
990     * @return lower bound date or null if not set
991     */
992    public Date getLowerBound() {
993        return getSelectionModel().getLowerBound();
994    }
995
996    /**
997     * Return the upper bound date that is allowed to be selected for this
998     * model.
999     *
1000     * @return upper bound date or null if not set
1001     */
1002    public Date getUpperBound() {
1003        return getSelectionModel().getUpperBound();
1004    }
1005
1006    /**
1007     * Identifies whether or not the date passed is an unselectable date.
1008     * <p>
1009     * 
1010     * @param date date which to test for unselectable status
1011     * @return true if the date is unselectable, false otherwise
1012     */
1013    public boolean isUnselectableDate(Date date) {
1014        return getSelectionModel().isUnselectableDate(date);
1015    }
1016
1017    /**
1018     * Sets the dates that should be unselectable. This will replace the model's
1019     * current set of unselectable dates. The implication is that calling with
1020     * zero dates will remove all unselectable dates.
1021     * <p>
1022     * 
1023     * NOTE: neither the given array nor any of its elements must be null.
1024     * 
1025     * @param unselectableDates zero or more not-null dates that should be
1026     *        unselectable.
1027     * @throws NullPointerException if either the array or any of the elements
1028     *         are null
1029     */
1030    public void setUnselectableDates(Date... unselectableDates) {
1031        Contract.asNotNull(unselectableDates,
1032                "unselectable dates must not be null");
1033        SortedSet<Date> unselectableSet = new TreeSet<Date>();
1034        for (Date unselectableDate : unselectableDates) {
1035            unselectableSet.add(unselectableDate);
1036        }
1037        getSelectionModel().setUnselectableDates(unselectableSet);
1038        // PENDING JW: check that ui does the repaint!
1039        repaint();
1040    }
1041
1042    // --------------------- flagged dates
1043    /**
1044     * Identifies whether or not the date passed is a flagged date. 
1045     * 
1046     * @param date date which to test for flagged status
1047     * @return true if the date is flagged, false otherwise
1048     */
1049    public boolean isFlaggedDate(Date date) {
1050        if (date == null)
1051            return false;
1052        return flaggedDates.isSelected(date);
1053    }
1054    
1055    /**
1056     * Replace all flags with the given dates.<p>
1057     * 
1058     * NOTE: neither the given array nor any of its elements should be null.
1059     * Currently, a null array will be tolerated to ease migration. A null
1060     * has the same effect as clearFlaggedDates.
1061     * 
1062     *
1063     * @param flagged the dates to be flagged
1064     */
1065    public void setFlaggedDates(Date... flagged) {
1066//        Contract.asNotNull(flagged, "must not be null");
1067        SortedSet<Date> oldFlagged = getFlaggedDates();
1068        flaggedDates.clearSelection();
1069        if (flagged != null) {
1070            for (Date date : flagged) {
1071                flaggedDates.addSelectionInterval(date, date);
1072            }
1073        }
1074        firePropertyChange("flaggedDates", oldFlagged, getFlaggedDates());
1075   }
1076    /**
1077     * Adds the dates to the flags. 
1078     * 
1079     * NOTE: neither the given array nor any of its elements should be null.
1080     * Currently, a null array will be tolerated to ease migration. A null
1081     * does nothing.
1082     *
1083     * @param flagged the dates to be flagged
1084     */
1085    public void addFlaggedDates(Date... flagged) {
1086//        Contract.asNotNull(flagged, "must not be null");
1087        SortedSet<Date> oldFlagged = flaggedDates.getSelection();
1088        if (flagged != null) {
1089            for (Date date : flagged) {
1090                flaggedDates.addSelectionInterval(date, date);
1091            }
1092        }
1093        firePropertyChange("flaggedDates", oldFlagged, flaggedDates.getSelection());
1094    }
1095    
1096    /**
1097     * Unflags the given dates.
1098     * 
1099     * NOTE: neither the given array nor any of its elements should be null.
1100     * Currently, a null array will be tolerated to ease migration. 
1101     *
1102     * @param flagged the dates to be unflagged
1103     */
1104    public void removeFlaggedDates(Date... flagged) {
1105//        Contract.asNotNull(flagged, "must not be null");
1106        SortedSet<Date> oldFlagged = flaggedDates.getSelection();
1107        if (flagged != null) {
1108            for (Date date : flagged) {
1109                flaggedDates.removeSelectionInterval(date, date);
1110            }
1111        }
1112        firePropertyChange("flaggedDates", oldFlagged, flaggedDates.getSelection());
1113    }
1114    /**
1115     * Clears all flagged dates.
1116     * 
1117     */
1118    public void clearFlaggedDates() {
1119        SortedSet<Date> oldFlagged = flaggedDates.getSelection();
1120        flaggedDates.clearSelection();
1121        firePropertyChange("flaggedDates", oldFlagged, flaggedDates.getSelection());
1122    }
1123    
1124    /**
1125     * Returns a sorted set of flagged Dates. The returned set is guaranteed to
1126     * be not null, but may be empty.
1127     * 
1128     * @return a sorted set of flagged dates.
1129     */
1130    public SortedSet<Date> getFlaggedDates() {
1131        return flaggedDates.getSelection();
1132    }
1133
1134    /**
1135     * Returns a boolean indicating if this monthView has flagged dates.
1136     * 
1137     * @return a boolean indicating if this monthView has flagged dates.
1138     */
1139    public boolean hasFlaggedDates() {
1140        return !flaggedDates.isSelectionEmpty();
1141    }
1142
1143
1144//------------------- visual properties    
1145    /**
1146     * Sets a boolean property indicating whether or not to show leading dates
1147     * for a months displayed by this component.<p>
1148     * 
1149     * The default value is false.
1150     * 
1151     * @param value true if leading dates should be displayed, false otherwise.
1152     */
1153    public void setShowingLeadingDays(boolean value) {
1154        boolean old = isShowingLeadingDays();
1155        leadingDays = value;
1156        firePropertyChange("showingLeadingDays", old, isShowingLeadingDays());
1157    }
1158
1159    /**
1160     * Returns a boolean indicating whether or not we're showing leading dates.
1161     *
1162     * @return true if leading dates are shown, false otherwise.
1163     */
1164    public boolean isShowingLeadingDays() {
1165        return leadingDays;
1166    }
1167
1168    /**
1169     * Sets a boolean property indicating whether or not to show 
1170     * trailing dates for the months displayed by this component.<p>
1171     * 
1172     * The default value is false.
1173     *
1174     * @param value true if trailing dates should be displayed, false otherwise.
1175     */
1176    public void setShowingTrailingDays(boolean value) {
1177        boolean old = isShowingTrailingDays();
1178        trailingDays = value;
1179        firePropertyChange("showingTrailingDays", old, isShowingTrailingDays());
1180    }
1181
1182    /**
1183     * Returns a boolean indicating whether or not we're showing trailing dates.
1184     *
1185     * @return true if trailing dates are shown, false otherwise.
1186     */
1187    public boolean isShowingTrailingDays() {
1188        return trailingDays;
1189    }
1190    
1191    /**
1192     * Returns whether or not the month view supports traversing months.
1193     * If zoomable is enabled, traversable is enabled as well. Otherwise
1194     * returns the traversable property as set by client code.
1195     * 
1196     * @return <code>true</code> if month traversing is enabled.
1197     * @see #setZoomable(boolean)
1198     */
1199    public boolean isTraversable() {
1200        if (isZoomable()) return true;
1201        return traversable;
1202    }
1203
1204    /**
1205     * Set whether or not the month view will display buttons to allow the user
1206     * to traverse to previous or next months. <p>
1207     * 
1208     * The default value is false. <p>
1209     * 
1210     * PENDING JW: fire the "real" property or the compound with zoomable?
1211     * 
1212     * @param traversable set to true to enable month traversing, false
1213     *        otherwise.
1214     * @see #isTraversable()       
1215     * @see #setZoomable(boolean)
1216     */
1217    public void setTraversable(boolean traversable) {
1218        boolean old = isTraversable();
1219        this.traversable = traversable;
1220        firePropertyChange(TRAVERSABLE, old, isTraversable());
1221    }
1222
1223    /**
1224     * Returns true if zoomable (through date ranges).
1225     * 
1226     * @return true if zoomable is enabled.
1227     * @see #setZoomable(boolean)
1228     */
1229    public boolean isZoomable() {
1230        return zoomable;
1231    }
1232
1233    /**
1234     * Sets the zoomable property. If true, the calendar's date range can
1235     * be zoomed. This state implies that the calendar is traversable and
1236     * showing exactly one calendar box, effectively ignoring the properties.<p>
1237     * 
1238     * <b>Note</b>: The actual zoomable behaviour is not yet implemented.
1239     * 
1240     * @param zoomable a boolean indicating whether or not zooming date
1241     *    ranges is enabled.
1242     *    
1243     * @see #setTraversable(boolean)
1244     */
1245    public void setZoomable(boolean zoomable) {
1246        boolean old = isZoomable();
1247        this.zoomable = zoomable;
1248        firePropertyChange("zoomable", old, isZoomable());
1249    }
1250
1251    /**
1252     * Returns whether or not this <code>JXMonthView</code> should display
1253     * week number.
1254     *
1255     * @return <code>true</code> if week numbers should be displayed
1256     */
1257    public boolean isShowingWeekNumber() {
1258        return showWeekNumber;
1259    }
1260
1261    /**
1262     * Set whether or not this <code>JXMonthView</code> will display week
1263     * numbers or not.
1264     *
1265     * @param showWeekNumber true if week numbers should be displayed,
1266     *        false otherwise
1267     */
1268    public void setShowingWeekNumber(boolean showWeekNumber) {
1269        boolean old = isShowingWeekNumber();
1270        this.showWeekNumber = showWeekNumber;
1271        firePropertyChange("showingWeekNumber", old, isShowingWeekNumber());
1272    }
1273
1274    /**
1275     * Sets the String representation for each day of the week as used
1276     * in the header of the day's grid. For
1277     * this method the first days of the week days[0] is assumed to be
1278     * <code>Calendar.SUNDAY</code>. If null, the representation provided
1279     * by the MonthViewUI is used.
1280     * 
1281     * The default value is the representation as 
1282     * returned from the MonthViewUI.
1283     * 
1284     * @param days Array of characters that represents each day
1285     * @throws IllegalArgumentException if not null and <code>days.length</code> !=
1286     *         DAYS_IN_WEEK
1287     */
1288    public void setDaysOfTheWeek(String[] days) {
1289        if ((days != null) && (days.length != DAYS_IN_WEEK)) {
1290            throw new IllegalArgumentException(
1291                    "Array of days is not of length " + DAYS_IN_WEEK
1292                            + " as expected.");
1293        }
1294
1295        String[] oldValue = getDaysOfTheWeek();
1296        _daysOfTheWeek = days;
1297        firePropertyChange(DAYS_OF_THE_WEEK, oldValue, days);
1298    }
1299
1300    /**
1301     * Returns the String representation for each day of the
1302     * week. 
1303     *
1304     * @return String representation for the days of the week, guaranteed to
1305     *   never be null.
1306     *   
1307     * @see #setDaysOfTheWeek(String[])
1308     * @see MonthViewUI  
1309     */
1310    public String[] getDaysOfTheWeek() {
1311        if (_daysOfTheWeek != null) {
1312            String[] days = new String[DAYS_IN_WEEK];
1313            System.arraycopy(_daysOfTheWeek, 0, days, 0, DAYS_IN_WEEK);
1314            return days;
1315        } 
1316        return getUI().getDaysOfTheWeek();
1317    }
1318
1319    /**
1320     * 
1321     * @param dayOfWeek
1322     * @return String representation of day of week.
1323     */
1324    public String getDayOfTheWeek(int dayOfWeek) {
1325        return getDaysOfTheWeek()[dayOfWeek - 1];
1326    }
1327    
1328
1329    /**
1330     * Returns the padding used between days in the calendar.
1331     *
1332     * @return Padding used between days in the calendar
1333     */
1334    public int getBoxPaddingX() {
1335        return boxPaddingX;
1336    }
1337
1338    /**
1339     * Sets the number of pixels used to pad the left and right side of a day.
1340     * The padding is applied to both sides of the days.  Therefore, if you
1341     * used the padding value of 3, the number of pixels between any two days
1342     * would be 6.
1343     *
1344     * @param boxPaddingX Number of pixels applied to both sides of a day
1345     */
1346    public void setBoxPaddingX(int boxPaddingX) {
1347        int oldBoxPadding = getBoxPaddingX();
1348        this.boxPaddingX = boxPaddingX;
1349        firePropertyChange(BOX_PADDING_X, oldBoxPadding, getBoxPaddingX());
1350    }
1351
1352    /**
1353     * Returns the padding used above and below days in the calendar.
1354     *
1355     * @return Padding used between dats in the calendar
1356     */
1357    public int getBoxPaddingY() {
1358        return boxPaddingY;
1359    }
1360
1361    /**
1362     * Sets the number of pixels used to pad the top and bottom of a day.
1363     * The padding is applied to both the top and bottom of a day.  Therefore,
1364     * if you used the padding value of 3, the number of pixels between any
1365     * two days would be 6.
1366     *
1367     * @param boxPaddingY Number of pixels applied to top and bottom of a day
1368     */
1369    public void setBoxPaddingY(int boxPaddingY) {
1370        int oldBoxPadding = getBoxPaddingY();
1371        this.boxPaddingY = boxPaddingY;
1372        firePropertyChange(BOX_PADDING_Y, oldBoxPadding, getBoxPaddingY());
1373    }
1374
1375
1376    /**
1377     * Returns the selected background color.
1378     *
1379     * @return the selected background color.
1380     */
1381    public Color getSelectionBackground() {
1382        return selectedBackground;
1383    }
1384
1385    /**
1386     * Sets the selected background color to <code>c</code>.  The default color
1387     * is installed by the ui.
1388     *
1389     * @param c Selected background.
1390     */
1391    public void setSelectionBackground(Color c) {
1392        Color old = getSelectionBackground();
1393        selectedBackground = c;
1394        firePropertyChange("selectionBackground", old, getSelectionBackground());
1395    }
1396
1397    /**
1398     * Returns the selected foreground color.
1399     *
1400     * @return the selected foreground color.
1401     */
1402    public Color getSelectionForeground() {
1403        return selectedForeground;
1404    }
1405
1406    /**
1407     * Sets the selected foreground color to <code>c</code>.  The default color
1408     * is installed by the ui.
1409     *
1410     * @param c Selected foreground.
1411     */
1412    public void setSelectionForeground(Color c) {
1413        Color old = getSelectionForeground();
1414        selectedForeground = c;
1415        firePropertyChange("selectionForeground", old, getSelectionForeground());
1416    }
1417
1418
1419    /**
1420     * Returns the color used when painting the today background.
1421     *
1422     * @return Color Color
1423     */
1424    public Color getTodayBackground() {
1425        return todayBackgroundColor;
1426    }
1427
1428    /**
1429     * Sets the color used to draw the bounding box around today.  The default
1430     * is the background of the <code>JXMonthView</code> component.
1431     *
1432     * @param c color to set
1433     */
1434    public void setTodayBackground(Color c) {
1435        Color oldValue = getTodayBackground();
1436        todayBackgroundColor = c;
1437        firePropertyChange("todayBackground", oldValue, getTodayBackground());
1438        // PENDING JW: remove repaint, ui must take care of it
1439        repaint();
1440    }
1441
1442    /**
1443     * Returns the color used to paint the month string background.
1444     *
1445     * @return Color Color.
1446     */
1447    public Color getMonthStringBackground() {
1448        return monthStringBackground;
1449    }
1450
1451    /**
1452     * Sets the color used to draw the background of the month string.  The
1453     * default is <code>138, 173, 209 (Blue-ish)</code>.
1454     *
1455     * @param c color to set
1456     */
1457    public void setMonthStringBackground(Color c) {
1458        Color old = getMonthStringBackground();
1459        monthStringBackground = c;
1460        firePropertyChange("monthStringBackground", old, getMonthStringBackground());
1461        // PENDING JW: remove repaint, ui must take care of it
1462        repaint();
1463    }
1464
1465    /**
1466     * Returns the color used to paint the month string foreground.
1467     *
1468     * @return Color Color.
1469     */
1470    public Color getMonthStringForeground() {
1471        return monthStringForeground;
1472    }
1473
1474    /**
1475     * Sets the color used to draw the foreground of the month string.  The
1476     * default is <code>Color.WHITE</code>.
1477     *
1478     * @param c color to set
1479     */
1480    public void setMonthStringForeground(Color c) {
1481        Color old = getMonthStringForeground();
1482        monthStringForeground = c;
1483        firePropertyChange("monthStringForeground", old, getMonthStringForeground());
1484        // PENDING JW: remove repaint, ui must take care of it
1485        repaint();
1486    }
1487
1488    /**
1489     * Sets the color used to draw the foreground of each day of the week. These
1490     * are the titles
1491     *
1492     * @param c color to set
1493     */
1494    public void setDaysOfTheWeekForeground(Color c) {
1495        Color old = getDaysOfTheWeekForeground();
1496        daysOfTheWeekForeground = c;
1497        firePropertyChange("daysOfTheWeekForeground", old, getDaysOfTheWeekForeground());
1498    }
1499
1500    /**
1501     * @return Color Color
1502     */
1503    public Color getDaysOfTheWeekForeground() {
1504        return daysOfTheWeekForeground;
1505    }
1506
1507    /**
1508     * Set the color to be used for painting the specified day of the week.
1509     * Acceptable values are Calendar.SUNDAY - Calendar.SATURDAY. <p>
1510     * 
1511     * PENDING JW: this is not a property - should it be and 
1512     * fire a change notification? If so, how?
1513     * 
1514     *
1515     * @param dayOfWeek constant value defining the day of the week.
1516     * @param c         The color to be used for painting the numeric day of the week.
1517     */
1518    public void setDayForeground(int dayOfWeek, Color c) {
1519        if ((dayOfWeek < Calendar.SUNDAY) || (dayOfWeek > Calendar.SATURDAY)) {
1520            throw new IllegalArgumentException("dayOfWeek must be in [Calendar.SUNDAY ... " +
1521                        "Calendar.SATURDAY] but was " + dayOfWeek);
1522        }
1523        dayToColorTable.put(dayOfWeek, c);
1524        repaint();
1525    }
1526
1527    /**
1528     * Return the color that should be used for painting the numerical day of the week.
1529     *
1530     * @param dayOfWeek The day of week to get the color for.
1531     * @return The color to be used for painting the numeric day of the week.
1532     *         If this was no color has yet been defined the component foreground color
1533     *         will be returned.
1534     */
1535    public Color getDayForeground(int dayOfWeek) {
1536        Color c;
1537        c = dayToColorTable.get(dayOfWeek);
1538        if (c == null) {
1539            c = getForeground();
1540        }
1541        return c;
1542    }
1543
1544    /**
1545     * Return the color that should be used for painting the numerical day of the week.
1546     *
1547     * @param dayOfWeek The day of week to get the color for.
1548     * @return The color to be used for painting the numeric day of the week or null
1549     *         If no color has yet been defined.
1550     */
1551    public Color getPerDayOfWeekForeground(int dayOfWeek) {
1552        return dayToColorTable.get(dayOfWeek);
1553    }
1554    /**
1555     * Set the color to be used for painting the foreground of a flagged day.
1556     *
1557     * @param c The color to be used for painting.
1558     */
1559    public void setFlaggedDayForeground(Color c) {
1560        Color old = getFlaggedDayForeground();
1561        flaggedDayForeground = c;
1562        firePropertyChange("flaggedDayForeground", old, getFlaggedDayForeground());
1563    }
1564
1565    /**
1566     * Return the color that should be used for painting the foreground of the flagged day.
1567     *
1568     * @return The color to be used for painting
1569     */
1570    public Color getFlaggedDayForeground() {
1571        return flaggedDayForeground;
1572    }
1573
1574    /**
1575     * Returns a copy of the insets used to paint the month string background.
1576     *
1577     * @return Insets Month string insets.
1578     */
1579    public Insets getMonthStringInsets() {
1580        return (Insets) _monthStringInsets.clone();
1581    }
1582
1583    /**
1584     * Insets used to modify the width/height when painting the background
1585     * of the month string area.
1586     *
1587     * @param insets Insets
1588     */
1589    public void setMonthStringInsets(Insets insets) {
1590        Insets old = getMonthStringInsets();
1591        if (insets == null) {
1592            _monthStringInsets.top = 0;
1593            _monthStringInsets.left = 0;
1594            _monthStringInsets.bottom = 0;
1595            _monthStringInsets.right = 0;
1596        } else {
1597            _monthStringInsets.top = insets.top;
1598            _monthStringInsets.left = insets.left;
1599            _monthStringInsets.bottom = insets.bottom;
1600            _monthStringInsets.right = insets.right;
1601        }
1602        firePropertyChange("monthStringInsets", old, getMonthStringInsets());
1603        // PENDING JW: remove repaint, ui must take care of it
1604        repaint();
1605    }
1606
1607    /**
1608     * Returns the preferred number of columns to paint calendars in. 
1609     * <p>
1610     * @return int preferred number of columns of calendars.
1611     * 
1612     * @see #setPreferredColumnCount(int)
1613     */
1614    public int getPreferredColumnCount() {
1615        return minCalCols;
1616    }
1617
1618    /**
1619     * Sets the preferred number of columns of calendars. Does nothing if cols
1620     * <= 0. The default value is 1.
1621     * <p>
1622     * @param cols The number of columns of calendars.
1623     * 
1624     * @see #getPreferredColumnCount()
1625     */
1626    public void setPreferredColumnCount(int cols) {
1627        if (cols <= 0) {
1628            return;
1629        }
1630        int old = getPreferredColumnCount();
1631        minCalCols = cols;
1632        firePropertyChange("preferredColumnCount", old, getPreferredColumnCount());
1633        // PENDING JW: remove revalidate/repaint, ui must take care of it
1634        revalidate();
1635        repaint();
1636    }
1637    
1638
1639    /**
1640     * Returns the preferred number of rows to paint calendars in.
1641     * <p>
1642     * @return int Rows of calendars.
1643     * 
1644     * @see #setPreferredRowCount(int)
1645     */
1646    public int getPreferredRowCount() {
1647        return minCalRows;
1648    }
1649
1650    /**
1651     * Sets the preferred number of rows to paint calendars.Does nothing if rows
1652     * <= 0. The default value is 1.
1653     * <p>
1654     *
1655     * @param rows The number of rows of calendars.
1656     * 
1657     * @see #getPreferredRowCount()
1658     */
1659    public void setPreferredRowCount(int rows) {
1660        if (rows <= 0) {
1661            return;
1662        }
1663        int old = getPreferredRowCount();
1664        minCalRows = rows;
1665        firePropertyChange("preferredRowCount", old, getPreferredRowCount());
1666        // PENDING JW: remove revalidate/repaint, ui must take care of it
1667        revalidate();
1668        repaint();
1669    }
1670
1671
1672    /**
1673     * {@inheritDoc}
1674     */
1675    @Override
1676    public void removeNotify() {
1677        if (todayTimer != null) {
1678            todayTimer.stop();
1679        }
1680        super.removeNotify();
1681    }
1682
1683    /**
1684     * {@inheritDoc}
1685     */
1686    @Override
1687    public void addNotify() {
1688        super.addNotify();
1689        // partial fix for #1125: today updated in addNotify
1690        // partial, because still not in synch if not shown
1691        updateTodayFromCurrentTime();
1692        // Setup timer to update the value of today.
1693        int secondsTillTomorrow = 86400;
1694
1695        if (todayTimer == null) {
1696            todayTimer = new Timer(secondsTillTomorrow * 1000,
1697                    new ActionListener() {
1698                        @Override
1699                        public void actionPerformed(ActionEvent e) {
1700                            incrementToday();
1701                        }
1702                    });
1703        }
1704
1705        // Modify the initial delay by the current time.
1706//        cal.setTimeInMillis(System.currentTimeMillis());
1707        cal.setTime(getCurrentDate());
1708        secondsTillTomorrow = secondsTillTomorrow -
1709                (cal.get(Calendar.HOUR_OF_DAY) * 3600) -
1710                (cal.get(Calendar.MINUTE) * 60) -
1711                cal.get(Calendar.SECOND);
1712        todayTimer.setInitialDelay(secondsTillTomorrow * 1000);
1713        todayTimer.start();
1714    }
1715
1716//-------------------- action and listener
1717    
1718
1719    /**
1720     * Commits the current selection. <p>
1721     * 
1722     * Resets the model's adjusting property to false
1723     * and fires an ActionEvent
1724     * with the COMMIT_KEY action command.
1725     * 
1726     * 
1727     * @see #cancelSelection()
1728     * @see org.jdesktop.swingx.calendar.DateSelectionModel#setAdjusting(boolean)
1729     */
1730    public void commitSelection() {
1731        getSelectionModel().setAdjusting(false);
1732        fireActionPerformed(COMMIT_KEY);
1733    }
1734
1735    /**
1736     * Cancels the selection. <p>
1737     * 
1738     * Resets the model's adjusting property to 
1739     * false and fires an ActionEvent with the CANCEL_KEY action command.
1740     * 
1741     * @see #commitSelection
1742     * @see org.jdesktop.swingx.calendar.DateSelectionModel#setAdjusting(boolean)
1743     */
1744    public void cancelSelection() {
1745        getSelectionModel().setAdjusting(false);
1746        fireActionPerformed(CANCEL_KEY);
1747    }
1748
1749    /**
1750     * Sets the component input map enablement property.<p>
1751     * 
1752     * If enabled, the keybinding for WHEN_IN_FOCUSED_WINDOW are
1753     * installed, otherwise not. Changing this property will
1754     * install/clear the corresponding key bindings. Typically, clients 
1755     * which want to use the monthview in a popup, should enable these.<p>
1756     * 
1757     * The default value is false.
1758     * 
1759     * @param enabled boolean to indicate whether the component
1760     *   input map should be enabled.
1761     * @see #isComponentInputMapEnabled()  
1762     */
1763    public void setComponentInputMapEnabled(boolean enabled) {
1764        boolean old = isComponentInputMapEnabled();
1765        this.componentInputMapEnabled = enabled;
1766        firePropertyChange("componentInputMapEnabled", old, isComponentInputMapEnabled());
1767    }
1768
1769    /**
1770     * Returns the componentInputMapEnabled property.
1771     * 
1772     * @return a boolean indicating whether the component input map is 
1773     *   enabled.
1774     * @see #setComponentInputMapEnabled(boolean)  
1775     *   
1776     */
1777    public boolean isComponentInputMapEnabled() {
1778        return componentInputMapEnabled;
1779    }
1780
1781    /**
1782     * Adds an ActionListener.
1783     * <p/>
1784     * The ActionListener will receive an ActionEvent with its actionCommand
1785     * set to COMMIT_KEY or CANCEL_KEY after the selection has been committed
1786     * or canceled, respectively.
1787     * <p>
1788     * 
1789     * Note that actionEvents are typically fired after a dedicated user gesture 
1790     * to end an ongoing selectin (like ENTER, ESCAPE) or after explicit programmatic
1791     * commits/cancels. It is usually not fired after each change to the selection state.
1792     * Client code which wants to be notified about all selection changes should 
1793     * register a DateSelectionListener to the DateSelectionModel.
1794     * 
1795     * @param l The ActionListener that is to be notified
1796     * 
1797     * @see #commitSelection()
1798     * @see #cancelSelection()
1799     * @see #getSelectionModel()
1800     */
1801    public void addActionListener(ActionListener l) {
1802        listenerMap.add(ActionListener.class, l);
1803    }
1804
1805    /**
1806     * Removes an ActionListener.
1807     *
1808     * @param l The ActionListener to remove.
1809     */
1810    public void removeActionListener(ActionListener l) {
1811        listenerMap.remove(ActionListener.class, l);
1812    }
1813
1814    @Override
1815    @SuppressWarnings("unchecked")
1816    public <T extends EventListener> T[] getListeners(Class<T> listenerType) {
1817        java.util.List<T> listeners = listenerMap.getListeners(listenerType);
1818        T[] result;
1819        if (!listeners.isEmpty()) {
1820            //noinspection unchecked
1821            result = (T[]) java.lang.reflect.Array.newInstance(listenerType, listeners.size());
1822            result = listeners.toArray(result);
1823        } else {
1824            result = super.getListeners(listenerType);
1825        }
1826        return result;
1827    }
1828
1829    /**
1830     * Creates and fires an ActionEvent with the given action 
1831     * command to all listeners.
1832     * 
1833     * @param actionCommand the command for the created.
1834     */
1835    protected void fireActionPerformed(String actionCommand) {
1836        ActionListener[] listeners = getListeners(ActionListener.class);
1837        ActionEvent e = null;
1838
1839        for (ActionListener listener : listeners) {
1840            if (e == null) {
1841                e = new ActionEvent(JXMonthView.this,
1842                        ActionEvent.ACTION_PERFORMED,
1843                        actionCommand);
1844            }
1845            listener.actionPerformed(e);
1846        }
1847    }
1848
1849
1850//--- deprecated code - NOTE: these methods will be removed soon! 
1851
1852    /**
1853     * @deprecated pre-0.9.5 - this is kept as a reminder only, <b>don't
1854     *             use</b>! we can make this private or comment it out after
1855     *             next version
1856     */
1857     @Deprecated
1858    protected void cleanupWeekSelectionDates(Date startDate, Date endDate) {
1859        int count = 1;
1860        cal.setTime(startDate);
1861        while (cal.getTimeInMillis() < endDate.getTime()) {
1862            cal.add(Calendar.DAY_OF_MONTH, 1);
1863            count++;
1864        }
1865
1866        if (count > JXMonthView.DAYS_IN_WEEK) {
1867            // Move the start date to the first day of the week.
1868            cal.setTime(startDate);
1869            int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
1870            int firstDayOfWeek = getFirstDayOfWeek();
1871            int daysFromStart = dayOfWeek - firstDayOfWeek;
1872            if (daysFromStart < 0) {
1873                daysFromStart += JXMonthView.DAYS_IN_WEEK;
1874            }
1875            cal.add(Calendar.DAY_OF_MONTH, -daysFromStart);
1876
1877            modifiedStartDate = cal.getTime();
1878
1879            // Move the end date to the last day of the week.
1880            cal.setTime(endDate);
1881            dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
1882            int lastDayOfWeek = firstDayOfWeek - 1;
1883            if (lastDayOfWeek == 0) {
1884                lastDayOfWeek = Calendar.SATURDAY;
1885            }
1886            int daysTillEnd = lastDayOfWeek - dayOfWeek;
1887            if (daysTillEnd < 0) {
1888                daysTillEnd += JXMonthView.DAYS_IN_WEEK;
1889            }
1890            cal.add(Calendar.DAY_OF_MONTH, daysTillEnd);
1891            modifiedEndDate = cal.getTime();
1892        }
1893    }
1894
1895
1896
1897
1898
1899}