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