001    /*
002     * $Id: JXMonthView.java,v 1.21 2006/05/14 02:36:12 dmouse Exp $
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.calendar;
022    
023    import org.jdesktop.swingx.plaf.JXMonthViewAddon;
024    import org.jdesktop.swingx.plaf.LookAndFeelAddons;
025    import org.jdesktop.swingx.plaf.MonthViewUI;
026    
027    import javax.swing.*;
028    import javax.swing.Timer;
029    import java.awt.*;
030    import java.awt.event.ActionEvent;
031    import java.awt.event.ActionListener;
032    import java.util.*;
033    
034    
035    /**
036     * Component that displays a month calendar which can be used to select a day
037     * or range of days.  By default the <code>JXMonthView</code> will display a
038     * single calendar using the current month and year, using
039     * <code>Calendar.SUNDAY</code> as the first day of the week.
040     * <p>
041     * The <code>JXMonthView</code> can be configured to display more than one
042     * calendar at a time by calling
043     * <code>setPreferredCalCols</code>/<code>setPreferredCalRows</code>.  These
044     * methods will set the preferred number of calendars to use in each
045     * column/row.  As these values change, the <code>Dimension</code> returned
046     * from <code>getMinimumSize</code> and <code>getPreferredSize</code> will
047     * be updated.  The following example shows how to create a 2x2 view which is
048     * contained within a <code>JFrame</code>:
049     * <pre>
050     *     JXMonthView monthView = new JXMonthView();
051     *     monthView.setPreferredCols(2);
052     *     monthView.setPreferredRows(2);
053     *
054     *     JFrame frame = new JFrame();
055     *     frame.getContentPane().add(monthView);
056     *     frame.pack();
057     *     frame.setVisible(true);
058     * </pre>
059     * <p>
060     * <code>JXMonthView</code> can be further configured to allow any day of the
061     * week to be considered the first day of the week.  Character
062     * representation of those days may also be set by providing an array of
063     * strings.
064     * <pre>
065     *    monthView.setFirstDayOfWeek(Calendar.MONDAY);
066     *    monthView.setDaysOfTheWeek(
067     *            new String[]{"S", "M", "T", "W", "Th", "F", "S"});
068     * </pre>
069     * <p>
070     * This component supports flagging days.  These flagged days are displayed
071     * in a bold font.  This can be used to inform the user of such things as
072     * scheduled appointment.
073     * <pre>
074     *    // Create some dates that we want to flag as being important.
075     *    Calendar cal1 = Calendar.getInstance();
076     *    cal1.set(2004, 1, 1);
077     *    Calendar cal2 = Calendar.getInstance();
078     *    cal2.set(2004, 1, 5);
079     *
080     *    long[] flaggedDates = new long[] {
081     *        cal1.getTimeInMillis(),
082     *        cal2.getTimeInMillis(),
083     *        System.currentTimeMillis()
084     *    };
085     *
086     *    monthView.setFlaggedDates(flaggedDates);
087     * </pre>
088     * Applications may have the need to allow users to select different ranges of
089     * dates.  There are four modes of selection that are supported, single,
090     * multiple, week and no selection.  Once a selection is made an action is
091     * fired, with exception of the no selection mode, to inform listeners that
092     * selection has changed.
093     * <pre>
094     *    // Change the selection mode to select full weeks.
095     *    monthView.setSelectionMode(JXMonthView.WEEK_INTERVAL_SELECTION);
096     *
097     *    // Add an action listener that will be notified when the user
098     *    // changes selection via the mouse.
099     *    monthView.addActionListener(new ActionListener() {
100     *        public void actionPerformed(ActionEvent e) {
101     *            System.out.println(
102     *                ((JXMonthView)e.getSource()).getSelectedDateSpan());
103     *        }
104     *    });
105     * </pre>
106     *
107     * @author Joshua Outwater
108     * @version  $Revision: 1.21 $
109     */
110    public class JXMonthView extends JComponent {
111        /** Mode that disallows selection of days from the calendar. */
112        public static final int NO_SELECTION = 0;
113        /** Mode that allows for selection of a single day. */
114        public static final int SINGLE_SELECTION = 1;
115        /** Mode that allows for selecting of multiple consecutive days. */
116        public static final int SINGLE_INTERVAL_SELECTION = 2;
117    // TODO: Add multiple interval selection.
118    //    /** Mode that allows for selecting disjoint days. */
119    //    public static final int MULTIPLE_INTERVAL_SELECTION = 3;
120        /**
121         * Mode where selections consisting of more than 7 days will
122         * snap to a full week.
123         */
124        public static final int WEEK_INTERVAL_SELECTION = 4;
125    
126        /** Return value used to identify when the month down button is pressed. */
127        public static final int MONTH_DOWN = 1;
128        /** Return value used to identify when the month up button is pressed. */
129        public static final int MONTH_UP = 2;
130    
131        /**
132         * Insets used in determining the rectangle for the month string
133         * background.
134         */
135        protected Insets _monthStringInsets = new Insets(0,0,0,0);
136    
137        @SuppressWarnings({"UNUSED_SYMBOL"})
138        private static final int MONTH_TRAVERSABLE = 1;
139        @SuppressWarnings({"UNUSED_SYMBOL"})
140        private static final int YEAR_TRAVERSABLE = 2;
141    
142        static {
143          LookAndFeelAddons.contribute(new JXMonthViewAddon());
144        }
145    
146        /**
147         * UI Class ID
148         */
149        public static final String uiClassID = "MonthViewUI";
150    
151        private int _boxPaddingX;
152        private int _boxPaddingY;
153        public static final int DAYS_IN_WEEK = 7;
154        public static final int MONTHS_IN_YEAR = 12;
155    
156        /**
157         * Keeps track of the first date we are displaying.  We use this as a
158         * restore point for the calendar.
159         */
160        private long _firstDisplayedDate;
161        private int _firstDisplayedMonth;
162        private int _firstDisplayedYear;
163    
164        private long _lastDisplayedDate;
165    
166        /** Beginning date of selection.  -1 if no date is selected. */
167        private long _startSelectedDate = -1;
168    
169        /** End date of selection.  -1 if no date is selected. */
170        private long _endSelectedDate = -1;
171    
172        private int _minCalCols = 1;
173        private int _minCalRows = 1;
174        private long _today;
175        private HashSet<Long> _flaggedDates;
176        private int _selectionMode = SINGLE_SELECTION;
177        private int _firstDayOfWeek = Calendar.SUNDAY;
178        private boolean _antiAlias = false;
179        private boolean _traversable = false;
180        private Calendar _cal;
181        private String[] _daysOfTheWeek;
182        private Color _todayBackgroundColor;
183        private Color _monthStringBackground;
184        private Color _monthStringForeground;
185        private Color _daysOfTheWeekForeground;
186        private Color _selectedBackground;
187        private String _actionCommand = "selectionChanged";
188        private Timer _todayTimer = null;
189        private Hashtable<Integer, Color> _dayToColorTable = new Hashtable<Integer, Color>();
190        private Color _flaggedDayForeground;
191        private boolean _showWeekNumber;
192    
193        /**
194         * Create a new instance of the <code>JXMonthView</code> class using the
195         * month and year of the current day as the first date to display.
196         */
197        public JXMonthView() {
198            this(new Date().getTime());
199        }
200    
201        /**
202         * Create a new instance of the <code>JXMonthView</code> class using the
203         * month and year from <code>initialTime</code> as the first date to
204         * display.
205         *
206         * @param initialTime The first month to display.
207         */
208        public JXMonthView(long initialTime) {
209            updateUI();
210    
211            // Set up calendar instance.
212            _cal = Calendar.getInstance(getLocale());
213            _cal.setFirstDayOfWeek(_firstDayOfWeek);
214            _cal.setMinimalDaysInFirstWeek(1);
215    
216            // Keep track of today.
217            _cal.set(Calendar.HOUR_OF_DAY, 0);
218            _cal.set(Calendar.MINUTE, 0);
219            _cal.set(Calendar.SECOND, 0);
220            _cal.set(Calendar.MILLISECOND, 0);
221    
222            setToday(_cal.getTimeInMillis());
223            _cal.setTimeInMillis(initialTime);
224            setFirstDisplayedDate(_cal.getTimeInMillis());
225    
226            setBackground(Color.WHITE);
227            setFocusable(true);
228            _todayBackgroundColor = getForeground();
229    
230            // Restore original time value.
231            _cal.setTimeInMillis(_firstDisplayedDate);
232        }
233    
234        /**
235         * @inheritDoc
236         */
237        public MonthViewUI getUI() {
238            return (MonthViewUI)ui;
239        }
240    
241        /**
242         * Sets the L&F object that renders this component.
243         *
244         * @param ui
245         */
246        public void setUI(MonthViewUI ui) {
247            super.setUI(ui);
248        }
249    
250        /**
251         * Resets the UI property with the value from the current look and feel.
252         *
253         * @see UIManager#getUI
254         */
255        public void updateUI() {
256            setUI((MonthViewUI)UIManager.getUI(this));
257            invalidate();
258        }
259    
260        /**
261         * @inheritDoc
262         */
263        @Override
264        public String getUIClassID() {
265            return uiClassID;
266        }
267    
268        /**
269         * Returns the first displayed date.
270         *
271         * @return long The first displayed date.
272         */
273        public long getFirstDisplayedDate() {
274            return _firstDisplayedDate;
275        }
276    
277        /**
278         * Set the first displayed date.  We only use the month and year of
279         * this date.  The <code>Calendar.DAY_OF_MONTH</code> field is reset to
280         * 1 and all other fields, with exception of the year and month ,
281         * are reset to 0.
282         *
283         * @param date The first displayed date.
284         */
285        public void setFirstDisplayedDate(long date) {
286            long oldFirstDisplayedDate = _firstDisplayedDate;
287            int oldFirstDisplayedMonth = _firstDisplayedMonth;
288            int oldFirstDisplayedYear = _firstDisplayedYear;
289    
290            _cal.setTimeInMillis(date);
291            _cal.set(Calendar.DAY_OF_MONTH, 1);
292            _cal.set(Calendar.HOUR_OF_DAY, 0);
293            _cal.set(Calendar.MINUTE, 0);
294            _cal.set(Calendar.SECOND, 0);
295            _cal.set(Calendar.MILLISECOND, 0);
296    
297            _firstDisplayedDate = _cal.getTimeInMillis();
298            _firstDisplayedMonth = _cal.get(Calendar.MONTH);
299            _firstDisplayedYear = _cal.get(Calendar.YEAR);
300    
301            firePropertyChange("firstDisplayedDate", oldFirstDisplayedDate, _firstDisplayedDate);
302            firePropertyChange("firstDisplayedMonth", oldFirstDisplayedMonth, _firstDisplayedMonth);
303            firePropertyChange("firstDisplayedYear", oldFirstDisplayedYear, _firstDisplayedYear);
304    
305            calculateLastDisplayedDate();
306    
307            repaint();
308        }
309    
310        /**
311         * Returns the last date able to be displayed.  For example, if the last
312         * visible month was April the time returned would be April 30, 23:59:59.
313         *
314         * @return long The last displayed date.
315         */
316        public long getLastDisplayedDate() {
317            return _lastDisplayedDate;
318        }
319    
320        private void calculateLastDisplayedDate() {
321            _lastDisplayedDate = getUI().calculateLastDisplayedDate();
322        }
323    
324        /**
325         * Moves the <code>date</code> into the visible region of the calendar.
326         * If the date is greater than the last visible date it will become the
327         * last visible date.  While if it is less than the first visible date
328         * it will become the first visible date.
329         *
330         * @param date Date to make visible.
331         */
332        public void ensureDateVisible(long date) {
333            if (date < _firstDisplayedDate) {
334                setFirstDisplayedDate(date);
335            } else if (date > _lastDisplayedDate) {
336                _cal.setTimeInMillis(date);
337                int month = _cal.get(Calendar.MONTH);
338                int year = _cal.get(Calendar.YEAR);
339    
340                _cal.setTimeInMillis(_lastDisplayedDate);
341                int lastMonth = _cal.get(Calendar.MONTH);
342                int lastYear = _cal.get(Calendar.YEAR);
343    
344                int diffMonths = month - lastMonth +
345                        ((year - lastYear) * MONTHS_IN_YEAR);
346    
347                _cal.setTimeInMillis(_firstDisplayedDate);
348                _cal.add(Calendar.MONTH, diffMonths);
349                setFirstDisplayedDate(_cal.getTimeInMillis());
350            }
351    
352            firePropertyChange("ensureDateVisibility", null, date);
353        }
354    
355        /**
356         * Returns a date span of the selected dates.  The result will be null if
357         * no dates are selected.
358         */
359        public DateSpan getSelectedDateSpan() {
360            DateSpan result = null;
361            if (_startSelectedDate != -1) {
362                result = new DateSpan(new Date(_startSelectedDate),
363                    new Date(_endSelectedDate));
364            }
365            return result;
366        }
367    
368        /**
369         * Selects the dates in the DateSpan.  This method will not change the
370         * initial date displayed so the caller must update this if necessary.
371         * If we are in SINGLE_SELECTION mode only the start time from the DateSpan
372         * will be used.  If we are in WEEK_INTERVAL_SELECTION mode the span will be
373         * modified to be valid if necessary.
374         *
375         * @param dateSpan DateSpan defining the selected dates.  Passing
376         * <code>null</code> will clear the selection.
377         */
378        public void setSelectedDateSpan(DateSpan dateSpan) {
379            DateSpan oldSpan = null;
380            DateSpan newSpan = null;
381    
382            if (_startSelectedDate != -1 && _endSelectedDate != -1) {
383                oldSpan = new DateSpan(_startSelectedDate, _endSelectedDate);
384            }
385    
386            if (dateSpan == null) {
387                _startSelectedDate = -1;
388                _endSelectedDate = -1;
389            } else {
390                _cal.setTimeInMillis(dateSpan.getStart());
391                _cal.set(Calendar.HOUR_OF_DAY, 0);
392                _cal.set(Calendar.MINUTE, 0);
393                _cal.set(Calendar.SECOND, 0);
394                _cal.set(Calendar.MILLISECOND, 0);
395                _startSelectedDate = _cal.getTimeInMillis();
396    
397                if (_selectionMode == SINGLE_SELECTION) {
398                    _endSelectedDate = _startSelectedDate;
399                } else {
400                    _cal.setTimeInMillis(dateSpan.getEnd());
401                    _cal.set(Calendar.HOUR_OF_DAY, 0);
402                    _cal.set(Calendar.MINUTE, 0);
403                    _cal.set(Calendar.SECOND, 0);
404                    _cal.set(Calendar.MILLISECOND, 0);
405                    _endSelectedDate = _cal.getTimeInMillis();
406    
407                    if (_selectionMode == WEEK_INTERVAL_SELECTION) {
408                        // Make sure if we are over 7 days we span full weeks.
409                        _cal.setTimeInMillis(_startSelectedDate);
410                        int count = 1;
411                        while (_cal.getTimeInMillis() < _endSelectedDate) {
412                            _cal.add(Calendar.DAY_OF_MONTH, 1);
413                            count++;
414                        }
415                        if (count > DAYS_IN_WEEK) {
416                            // Make sure start date is on the beginning of the
417                            // week.
418                            _cal.setTimeInMillis(_startSelectedDate);
419                            int dayOfWeek = _cal.get(Calendar.DAY_OF_WEEK);
420                            if (dayOfWeek != _firstDayOfWeek) {
421                                // Move the start date back to the first day of the
422                                // week.
423                                int daysFromStart = dayOfWeek - _firstDayOfWeek;
424                                if (daysFromStart < 0) {
425                                    daysFromStart += DAYS_IN_WEEK;
426                                }
427                                _cal.add(Calendar.DAY_OF_MONTH, -daysFromStart);
428                                count += daysFromStart;
429                                _startSelectedDate = _cal.getTimeInMillis();
430                            }
431    
432                            // Make sure we have full weeks.  Otherwise modify the
433                            // end date.
434                            int remainder = count % DAYS_IN_WEEK;
435                            if (remainder != 0) {
436                                _cal.setTimeInMillis(_endSelectedDate);
437                                _cal.add(Calendar.DAY_OF_MONTH, (DAYS_IN_WEEK - remainder));
438                                _endSelectedDate = _cal.getTimeInMillis();
439                            }
440                        }
441                    }
442                }
443                // Restore original time value.
444                _cal.setTimeInMillis(_firstDisplayedDate);
445                newSpan = new DateSpan(_startSelectedDate, _endSelectedDate);
446            }
447    
448            // Fire property change.
449            firePropertyChange("selectedDates", oldSpan, newSpan);
450        }
451    
452        /**
453         * Returns the current selection mode for this JXMonthView.
454         *
455         * @return int Selection mode.
456         */
457        public int getSelectionMode() {
458            return _selectionMode;
459        }
460    
461        /**
462         * Set the selection mode for this JXMonthView.
463         *
464         * @throws IllegalArgumentException
465         */
466        public void setSelectionMode(int mode) throws IllegalArgumentException {
467            if (mode != SINGLE_SELECTION && mode != SINGLE_INTERVAL_SELECTION &&
468                    mode != WEEK_INTERVAL_SELECTION && mode != NO_SELECTION) {
469                throw new IllegalArgumentException(mode +
470                        " is not a valid selection mode");
471            }
472            _selectionMode = mode;
473        }
474    
475    
476        /**
477         * Returns true if the specified date falls within the _startSelectedDate
478         * and _endSelectedDate range.
479         */
480        public boolean isSelectedDate(long date) {
481            return date >= _startSelectedDate && date <= _endSelectedDate;
482        }
483    
484        /**
485         * Identifies whether or not the date passed is a flagged date.
486         *
487         * @param date date which to test for flagged status
488         * @return true if the date is flagged, false otherwise
489         */
490        public boolean isFlaggedDate(long date) {
491            boolean result = false;
492            if (_flaggedDates != null) {
493                result = _flaggedDates.contains(date);
494            }
495            return result;
496        }
497    
498        /**
499         * An array of longs defining days that should be flagged.
500         *
501         * @param flaggedDates the dates to be flagged
502         */
503        public void setFlaggedDates(long[] flaggedDates) {
504            if (flaggedDates == null) {
505                _flaggedDates = null;
506            } else {
507                _flaggedDates = new HashSet<Long>();
508    
509                // Loop through the flaggedDates and set the hour, minute, seconds and
510                // milliseconds to 0 so we can compare times later.
511                for (long flaggedDate : flaggedDates) {
512                    _cal.setTimeInMillis(flaggedDate);
513    
514                    // We only want to compare the day, month and year
515                    // so reset all other values to 0.
516                    _cal.set(Calendar.HOUR_OF_DAY, 0);
517                    _cal.set(Calendar.MINUTE, 0);
518                    _cal.set(Calendar.SECOND, 0);
519                    _cal.set(Calendar.MILLISECOND, 0);
520    
521                    _flaggedDates.add(_cal.getTimeInMillis());
522                }
523    
524                // Restore the time.
525                _cal.setTimeInMillis(_firstDisplayedDate);
526            }
527    
528            repaint();
529        }
530    
531        /**
532         * Returns the padding used between days in the calendar.
533         */
534        public int getBoxPaddingX() {
535            return _boxPaddingX;
536        }
537    
538        /**
539         * Sets the number of pixels used to pad the left and right side of a day.
540         * The padding is applied to both sides of the days.  Therefore, if you
541         * used the padding value of 3, the number of pixels between any two days
542         * would be 6.
543         */
544        public void setBoxPaddingX(int boxPaddingX) {
545            int oldBoxPadding = _boxPaddingX;
546            _boxPaddingX = boxPaddingX;
547            firePropertyChange("boxPaddingX", oldBoxPadding, _boxPaddingX);
548        }
549    
550        /**
551         * Returns the padding used above and below days in the calendar.
552         */
553        public int getBoxPaddingY() {
554            return _boxPaddingY;
555        }
556    
557        /**
558         * Sets the number of pixels used to pad the top and bottom of a day.
559         * The padding is applied to both the top and bottom of a day.  Therefore,
560         * if you used the padding value of 3, the number of pixels between any
561         * two days would be 6.
562         */
563        public void setBoxPaddingY(int boxPaddingY) {
564            int oldBoxPadding = _boxPaddingY;
565            _boxPaddingY = boxPaddingY;
566            firePropertyChange("boxPaddingY", oldBoxPadding, _boxPaddingY);
567        }
568    
569        /**
570         * Returns whether or not the month view supports traversing months.
571         *
572         * @return <code>true</code> if month traversing is enabled.
573         */
574        public boolean isTraversable() {
575            return _traversable;
576        }
577    
578        /**
579         * Set whether or not the month view will display buttons to allow the
580         * user to traverse to previous or next months.
581         *
582         * @param traversable set to true to enable month traversing,
583         *        false otherwise.
584         */
585        public void setTraversable(boolean traversable) {
586            if (traversable != _traversable) {
587                _traversable = traversable;
588                firePropertyChange("traversable", !_traversable, _traversable);
589                repaint();
590            }
591        }
592    
593        /**
594         * Returns whether or not this <code>JXMonthView</code> should display
595         * week number.
596         *
597         * @return <code>true</code> if week numbers should be displayed
598         */
599        public boolean isShowingWeekNumber() {
600            return _showWeekNumber;
601        }
602    
603        /**
604         * Set whether or not this <code>JXMonthView</code> will display week
605         * numbers or not.
606         *
607         * @param showWeekNumber true if week numbers should be displayed,
608         *        false otherwise
609         */
610        public void setShowingWeekNumber(boolean showWeekNumber) {
611            if (_showWeekNumber != showWeekNumber) {
612                _showWeekNumber = showWeekNumber;
613                firePropertyChange("weekNumber", !_showWeekNumber, showWeekNumber);
614                repaint();            
615            }
616        }
617        /**
618         * Sets the single character representation for each day of the
619         * week.  For this method the first days of the week days[0] is assumed to
620         * be <code>Calendar.SUNDAY</code>.
621         *
622         * @throws IllegalArgumentException if <code>days.length</code> != DAYS_IN_WEEK
623         * @throws NullPointerException if <code>days</code> == null
624         */
625        public void setDaysOfTheWeek(String[] days)
626                throws IllegalArgumentException, NullPointerException {
627            if (days == null) {
628                throw new NullPointerException("Array of days is null.");
629            } else if (days.length != DAYS_IN_WEEK) {
630                throw new IllegalArgumentException(
631                        "Array of days is not of length " + DAYS_IN_WEEK + " as expected.");
632            }
633    
634            String[] oldValue = _daysOfTheWeek;
635            _daysOfTheWeek = days;
636            firePropertyChange("daysOfTheWeek", oldValue, _daysOfTheWeek);
637            repaint();
638        }
639    
640        /**
641         * Returns the single character representation for each day of the
642         * week.
643         *
644         * @return Single character representation for the days of the week
645         */
646        public String[] getDaysOfTheWeek() {
647            String[] days = new String[DAYS_IN_WEEK];
648            System.arraycopy(_daysOfTheWeek, 0, days, 0, DAYS_IN_WEEK);
649            return days;
650        }
651    
652        /**
653         * Gets what the first day of the week is; e.g.,
654         * <code>Calendar.SUNDAY</code> in the U.S., <code>Calendar.MONDAY</code>
655         * in France.
656         *
657         * @return int The first day of the week.
658         */
659        public int getFirstDayOfWeek() {
660            return _firstDayOfWeek;
661        }
662    
663        /**
664         * Sets what the first day of the week is; e.g.,
665         * <code>Calendar.SUNDAY</code> in US, <code>Calendar.MONDAY</code>
666         * in France.
667         *
668         * @param firstDayOfWeek The first day of the week.
669         *
670         * @see java.util.Calendar
671         */
672        public void setFirstDayOfWeek(int firstDayOfWeek) {
673            if (firstDayOfWeek == _firstDayOfWeek) {
674                return;
675            }
676    
677            int oldFirstDayOfWeek = _firstDayOfWeek;
678    
679            _firstDayOfWeek = firstDayOfWeek;
680            _cal.setFirstDayOfWeek(_firstDayOfWeek);
681    
682            firePropertyChange("firstDayOfWeek", oldFirstDayOfWeek, _firstDayOfWeek);
683    
684            repaint();
685        }
686    
687        /**
688         * Gets the time zone.
689         *
690         * @return The <code>TimeZone</code> used by the <code>JXMonthView</code>.
691         */
692        public TimeZone getTimeZone() {
693            return _cal.getTimeZone();
694        }
695    
696        /**
697         * Sets the time zone with the given time zone value.
698         *
699         * @param tz The <code>TimeZone</code>.
700         */
701        public void setTimeZone(TimeZone tz) {
702            _cal.setTimeZone(tz);
703        }
704    
705        /**
706         * Returns true if anti-aliased text is enabled for this component, false
707         * otherwise.
708         *
709         * @return boolean <code>true</code> if anti-aliased text is enabled,
710         * <code>false</code> otherwise.
711         */
712        public boolean isAntialiased() {
713            return _antiAlias;
714        }
715    
716        /**
717         * Turns on/off anti-aliased text for this component.
718         *
719         * @param antiAlias <code>true</code> for anti-aliased text,
720         * <code>false</code> to turn it off.
721         */
722        public void setAntialiased(boolean antiAlias) {
723            if (_antiAlias == antiAlias) {
724                return;
725            }
726            _antiAlias = antiAlias;
727            firePropertyChange("antialiased", !_antiAlias, _antiAlias);
728            repaint();
729        }
730    
731        /**
732        public void setDropShadowMask(int mask) {
733            _dropShadowMask = mask;
734            repaint();
735        }
736        */
737    
738        /**
739         * Returns the selected background color.
740         *
741         * @return the selected background color.
742         */
743        public Color getSelectedBackground() {
744            return _selectedBackground;
745        }
746    
747        /**
748         * Sets the selected background color to <code>c</code>.  The default color
749         * is <code>138, 173, 209 (Blue-ish)</code>
750         *
751         * @param c Selected background.
752         */
753        public void setSelectedBackground(Color c) {
754            _selectedBackground = c;
755        }
756    
757        /**
758         * Returns the color used when painting the today background.
759         *
760         * @return Color Color
761         */
762        public Color getTodayBackground() {
763            return _todayBackgroundColor;
764        }
765    
766        /**
767         * Sets the color used to draw the bounding box around today.  The default
768         * is the background of the <code>JXMonthView</code> component.
769         *
770         * @param c color to set
771         */
772        public void setTodayBackground(Color c) {
773            _todayBackgroundColor = c;
774            repaint();
775        }
776    
777        /**
778         * Returns the color used to paint the month string background.
779         *
780         * @return Color Color.
781         */
782        public Color getMonthStringBackground() {
783            return _monthStringBackground;
784        }
785    
786        /**
787         * Sets the color used to draw the background of the month string.  The
788         * default is <code>138, 173, 209 (Blue-ish)</code>.
789         *
790         * @param c color to set
791         */
792        public void setMonthStringBackground(Color c) {
793            _monthStringBackground = c;
794            repaint();
795        }
796    
797        /**
798         * Returns the color used to paint the month string foreground.
799         *
800         * @return Color Color.
801         */
802        public Color getMonthStringForeground() {
803            return _monthStringForeground;
804        }
805    
806        /**
807         * Sets the color used to draw the foreground of the month string.  The
808         * default is <code>Color.WHITE</code>.
809         *
810         * @param c color to set
811         */
812        public void setMonthStringForeground(Color c) {
813            _monthStringForeground = c;
814            repaint();
815        }
816    
817        /**
818         * Sets the color used to draw the foreground of each day of the week. These
819         * are the titles
820         *
821         * @param c color to set
822         */
823        public void setDaysOfTheWeekForeground(Color c) {
824            _daysOfTheWeekForeground = c;
825            repaint();
826        }
827    
828        /**
829         * @return Color Color
830         */
831        public Color getDaysOfTheWeekForeground() {
832            return _daysOfTheWeekForeground;
833        }
834    
835        /**
836         * Set the color to be used for painting the specified day of the week.
837         * Acceptable values are Calendar.SUNDAY - Calendar.SATURDAY.
838         *
839         * @param dayOfWeek constant value defining the day of the week.
840         * @param c The color to be used for painting the numeric day of the week.
841         */
842        public void setDayForeground(int dayOfWeek, Color c) {
843            _dayToColorTable.put(dayOfWeek, c);
844        }
845    
846        /**
847         * Return the color that should be used for painting the numerical day of the week.
848         *
849         * @param dayOfWeek The day of week to get the color for.
850         * @return The color to be used for painting the numeric day of the week.
851         *    If this was no color has yet been defined the component foreground color
852         *    will be returned.
853         */
854        public Color getDayForeground(int dayOfWeek) {
855            Color c;
856            c = _dayToColorTable.get(dayOfWeek);
857            if (c == null) {
858                c = getForeground();
859            }
860            return c;
861        }
862    
863        /**
864         * Set the color to be used for painting the foregroudn of a flagged day.
865         *
866         * @param c The color to be used for painting.
867         */
868        public void setFlaggedDayForeground(Color c) {
869            _flaggedDayForeground = c;
870        }
871    
872        /**
873         * Return the color that should be used for painting the foreground of the flagged day.
874         *
875         * @return The color to be used for painting
876         */
877        public Color getFlaggedDayForeground() {
878            return _flaggedDayForeground;
879        }
880    
881        /**
882         * Returns a copy of the insets used to paint the month string background.
883         *
884         * @return Insets Month string insets.
885         */
886        public Insets getMonthStringInsets() {
887            return (Insets)_monthStringInsets.clone();
888        }
889    
890        /**
891         * Insets used to modify the width/height when painting the background
892         * of the month string area.
893         *
894         * @param insets Insets
895         */
896        public void setMonthStringInsets(Insets insets) {
897            if (insets == null) {
898                _monthStringInsets.top = 0;
899                _monthStringInsets.left = 0;
900                _monthStringInsets.bottom = 0;
901                _monthStringInsets.right = 0;
902            } else {
903                _monthStringInsets.top = insets.top;
904                _monthStringInsets.left = insets.left;
905                _monthStringInsets.bottom = insets.bottom;
906                _monthStringInsets.right = insets.right;
907            }
908            repaint();
909        }
910    
911        /**
912         * Returns the preferred number of columns to paint calendars in.
913         *
914         * @return int Columns of calendars.
915         */
916        public int getPreferredCols() {
917            return _minCalCols;
918        }
919    
920        /**
921         * The preferred number of columns to paint calendars.
922         *
923         * @param cols The number of columns of calendars.
924         */
925        public void setPreferredCols(int cols) {
926            if (cols <= 0) {
927                return;
928            }
929            _minCalCols = cols;
930            revalidate();
931            repaint();
932        }
933    
934        /**
935         * Returns the preferred number of rows to paint calendars in.
936         *
937         * @return int Rows of calendars.
938         */
939        public int getPreferredRows() {
940            return _minCalRows;
941        }
942    
943        /**
944         * Sets the preferred number of rows to paint calendars.
945         *
946         * @param rows The number of rows of calendars.
947         */
948        public void setPreferredRows(int rows) {
949            if (rows <= 0) {
950                return;
951            }
952            _minCalRows = rows;
953            revalidate();
954            repaint();
955        }
956    
957    
958        private void updateToday() {
959            // Update _today.
960            _cal.setTimeInMillis(_today);
961            _cal.add(Calendar.DAY_OF_MONTH, 1);
962            setToday(_cal.getTimeInMillis());
963    
964            // Restore calendar.
965            _cal.setTimeInMillis(_firstDisplayedDate);
966            repaint();
967        }
968    
969        private void setToday(long today) {
970            long oldToday = _today;
971            _today = today;
972            firePropertyChange("today", oldToday, _today);
973        }
974    
975    //    /**
976    //     * Sets the border of this component. The Border object is responsible
977    //     * for defining the insets for the component (overriding any insets set
978    //     * directly on the component) and for optionally rendering any border
979    //     * decorations within the bounds of those insets. Borders should be used
980    //     * (rather than insets) for creating both decorative and non-decorative
981    //     * (such as margins and padding) regions for a swing component. Compound
982    //     * borders can be used to nest multiple borders within a single component.
983    //     * <p>
984    //     * As the border may modify the bounds of the component, setting the border
985    //     * may result in a reduced number of displayed calendars.
986    //     *
987    //     * @param border Border.
988    //     */
989    //    @Override
990    //    public void setBorder(Border border) {
991    //        super.setBorder(border);
992    //    }
993    //
994    //    /**
995    //     * Moves and resizes this component. The new location of the top-left
996    //     * corner is specified by x and y, and the new size is specified by
997    //     * width and height.
998    //     *
999    //     * @param x The new x-coordinate of this component
1000    //     * @param y The new y-coordinate of this component
1001    //     * @param width The new width of this component
1002    //     * @param height The new height of this component
1003    //     */
1004    //    @Override
1005    //    public void setBounds(int x, int y, int width, int height) {
1006    //        super.setBounds(x, y, width, height);
1007    //    }
1008    
1009        /**
1010         * Moves and resizes this component to conform to the new bounding
1011         * rectangle r. This component's new position is specified by r.x and
1012         * r.y, and its new size is specified by r.width and r.height
1013         *
1014         * @param r The new bounding rectangle for this component
1015         */
1016        @Override
1017        public void setBounds(Rectangle r) {
1018            setBounds(r.x, r.y, r.width, r.height);
1019        }
1020    
1021        /**
1022         * Sets the font of this component.
1023         *
1024         * @param font The font to become this component's font; if this parameter
1025         * is null then this component will inherit the font of its parent.
1026         */
1027        @Override
1028        public void setFont(Font font) {
1029            Font old = getFont();
1030            super.setFont(font);
1031            firePropertyChange("font", old, font);
1032        }
1033    
1034        /**
1035         * {@inheritDoc}
1036         */
1037        @Override
1038        public void removeNotify() {
1039            _todayTimer.stop();
1040            super.removeNotify();
1041        }
1042    
1043        /**
1044         * {@inheritDoc}
1045         */
1046        @Override
1047        public void addNotify() {
1048            super.addNotify();
1049    
1050            // Setup timer to update the value of _today.
1051            int secondsTillTomorrow = 86400;
1052    
1053            if (_todayTimer == null) {
1054                _todayTimer = new Timer(secondsTillTomorrow * 1000,
1055                    new ActionListener() {
1056                        public void actionPerformed(ActionEvent e) {
1057                            updateToday();
1058                        }
1059                    });
1060            }
1061    
1062            // Modify the initial delay by the current time.
1063            _cal.setTimeInMillis(System.currentTimeMillis());
1064            secondsTillTomorrow = secondsTillTomorrow -
1065                (_cal.get(Calendar.HOUR_OF_DAY) * 3600) -
1066                (_cal.get(Calendar.MINUTE) * 60) -
1067                _cal.get(Calendar.SECOND);
1068            _todayTimer.setInitialDelay(secondsTillTomorrow * 1000);
1069            _todayTimer.start();
1070    
1071            // Restore calendar
1072            _cal.setTimeInMillis(_firstDisplayedDate);
1073        }
1074    
1075        public Calendar getCalendar() {
1076            return _cal;
1077        }
1078    
1079        /**
1080         * Return a long representing the date at the specified x/y position.
1081         * The date returned will have a valid day, month and year.  Other fields
1082         * such as hour, minute, second and milli-second will be set to 0.
1083         *
1084         * @param x X position
1085         * @param y Y position
1086         * @return long The date, -1 if position does not contain a date.
1087         */
1088        public long getDayAt(int x, int y) {
1089            return getUI().getDayAt(x, y);
1090        }
1091    
1092        /**
1093         * Returns the string currently used to identiy fired ActionEvents.
1094         *
1095         * @return String The string used for identifying ActionEvents.
1096         */
1097        public String getActionCommand() {
1098            return _actionCommand;
1099        }
1100    
1101        /**
1102         * Sets the string used to identify fired ActionEvents.
1103         *
1104         * @param actionCommand The string used for identifying ActionEvents.
1105         */
1106        public void setActionCommand(String actionCommand) {
1107            _actionCommand = actionCommand;
1108        }
1109    
1110        /**
1111         * Adds an ActionListener.
1112         * <p>
1113         * The ActionListener will receive an ActionEvent when a selection has
1114         * been made.
1115         *
1116         * @param l The ActionListener that is to be notified
1117         */
1118        public void addActionListener(ActionListener l) {
1119            listenerList.add(ActionListener.class, l);
1120        }
1121    
1122        /**
1123         * Removes an ActionListener.
1124         *
1125         * @param l The action listener to remove.
1126         */
1127        public void removeActionListener(ActionListener l) {
1128            listenerList.remove(ActionListener.class, l);
1129        }
1130    
1131        /**
1132         * Fires an ActionEvent to all listeners.
1133         */
1134        protected void fireActionPerformed() {
1135            Object[] listeners = listenerList.getListenerList();
1136            ActionEvent e = null;
1137            for (int i = listeners.length - 2; i >= 0; i -=2) {
1138                if (listeners[i] == ActionListener.class) {
1139                    if (e == null) {
1140                        e = new ActionEvent(JXMonthView.this,
1141                                ActionEvent.ACTION_PERFORMED,
1142                                _actionCommand);
1143                    }
1144                    ((ActionListener)listeners[i + 1]).actionPerformed(e);
1145                }
1146            }
1147        }
1148    
1149        public void postActionEvent() {
1150            fireActionPerformed();
1151        }
1152    
1153    
1154        public static void main(String args[]) {
1155            SwingUtilities.invokeLater(new Runnable() {
1156                public void run() {
1157                    JFrame frame = new JFrame();
1158                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
1159                    JXMonthView mv = new JXMonthView();
1160                    mv.setShowingWeekNumber(true);
1161                    mv.setTraversable(true);
1162                    Calendar cal = Calendar.getInstance();
1163                    cal.set(2006, 5, 20);
1164                    mv.setFlaggedDates(new long[] { cal.getTimeInMillis() });
1165                    mv.setPreferredRows(2);
1166                    mv.setSelectionMode(JXMonthView.SINGLE_SELECTION);
1167                    mv.addActionListener(new ActionListener() {
1168                        public void actionPerformed(ActionEvent e) {
1169                            System.out.println(
1170                                    ((JXMonthView)e.getSource()).getSelectedDateSpan());
1171                        }
1172                    });
1173                    frame.getContentPane().add(mv);
1174                    frame.pack();
1175                    frame.setVisible(true);
1176                }
1177            });
1178        }
1179    }