001    package org.jdesktop.swingx.plaf.basic;
002    
003    import org.jdesktop.swingx.calendar.DateSpan;
004    import org.jdesktop.swingx.calendar.DateUtils;
005    import org.jdesktop.swingx.calendar.JXMonthView;
006    import org.jdesktop.swingx.plaf.MonthViewUI;
007    
008    import javax.swing.*;
009    import javax.swing.plaf.ComponentUI;
010    import java.awt.*;
011    import java.awt.event.*;
012    import java.beans.PropertyChangeEvent;
013    import java.beans.PropertyChangeListener;
014    import java.text.DateFormatSymbols;
015    import java.text.SimpleDateFormat;
016    import java.util.Calendar;
017    
018    public class BasicMonthViewUI extends MonthViewUI {
019        private static final int CALENDAR_SPACING = 10;
020    
021        /** Formatter used to format the day of the week to a numerical value. */
022        protected static final SimpleDateFormat dayOfMonthFormatter = new SimpleDateFormat("d");
023    
024        private static String[] monthsOfTheYear;
025    
026        protected JXMonthView monthView;
027        protected long firstDisplayedDate;
028        protected int firstDisplayedMonth;
029        protected int firstDisplayedYear;
030        protected long lastDisplayedDate;
031        protected long today;
032        protected DateSpan selection;
033    
034        private boolean usingKeyboard = false;
035        private boolean ltr;
036        private boolean showingWeekNumber;
037        private int arrowPaddingX = 3;
038        private int arrowPaddingY = 3;
039        private int boxPaddingX;
040        private int boxPaddingY;
041        private int fullMonthBoxHeight;
042        private int fullBoxWidth;
043        private int fullBoxHeight;
044        private int startX;
045        private int startY;
046        private Dimension dim = new Dimension();
047        private PropertyChangeListener propertyChangeListener;
048        private MouseListener mouseListener;
049        private MouseMotionListener mouseMotionListener;
050        private Handler handler;
051        private ImageIcon monthUpImage;
052        private ImageIcon monthDownImage;
053        private Rectangle dirtyRect = new Rectangle();
054        private Rectangle bounds = new Rectangle();
055        private Font derivedFont;
056    
057        /**
058         * Date span used by the keyboard actions to track the original selection.
059         */
060        private DateSpan originalDateSpan = null;
061        private int calendarWidth;
062        private int monthBoxHeight;
063        private int boxWidth;
064        private int boxHeight;
065        private int calendarHeight;
066        /** The number of calendars able to be displayed horizontally. */
067        private int numCalRows = 1;
068        /** The number of calendars able to be displayed vertically. */
069        private int numCalCols = 1;
070    
071    
072        @SuppressWarnings({"UNUSED_SYMBOL"})
073        public static ComponentUI createUI(JComponent c) {
074            return new BasicMonthViewUI();
075        }
076    
077        public void installUI(JComponent c) {
078            monthView = (JXMonthView)c;
079            monthView.setLayout(createLayoutManager());
080            ltr = monthView.getComponentOrientation().isLeftToRight();
081            LookAndFeel.installProperty(monthView, "opaque", Boolean.TRUE);
082    
083            // Get string representation of the months of the year.
084            monthsOfTheYear = new DateFormatSymbols().getMonths();
085    
086            installComponents();
087            installDefaults();
088            installKeyboardActions();
089            installListeners();
090        }
091    
092        public void uninstallUI(JComponent c) {
093            uninstallListeners();
094            uninstallKeyboardActions();
095            uninstallDefaults();
096            uninstallComponents();
097            monthView.setLayout(null);
098            monthView = null;
099        }
100    
101        protected void installComponents() {}
102    
103        protected void uninstallComponents() {}
104    
105        protected void installDefaults() {
106            String[] daysOfTheWeek =
107                    (String[])UIManager.get("JXMonthView.daysOfTheWeek");
108            if (daysOfTheWeek == null) {
109                String[] dateFormatSymbols =
110                    new DateFormatSymbols().getShortWeekdays();
111                daysOfTheWeek = new String[JXMonthView.DAYS_IN_WEEK];
112                for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
113                    daysOfTheWeek[i - 1] = dateFormatSymbols[i];
114                }
115            }
116            monthView.setDaysOfTheWeek(daysOfTheWeek);
117            monthView.setBoxPaddingX((Integer)UIManager.get("JXMonthView.boxPaddingX"));
118            monthView.setBoxPaddingY((Integer)UIManager.get("JXMonthView.boxPaddingY"));
119            monthView.setMonthStringBackground(UIManager.getColor("JXMonthView.monthStringBackground"));
120            monthView.setMonthStringForeground(UIManager.getColor("JXMonthView.monthStringForeground"));
121            monthView.setDaysOfTheWeekForeground(UIManager.getColor("JXMonthView.daysOfTheWeekForeground"));
122            monthView.setSelectedBackground(UIManager.getColor("JXMonthView.selectedBackground"));
123            monthView.setFlaggedDayForeground(UIManager.getColor("JXMonthView.flaggedDayForeground"));
124            monthView.setFont(UIManager.getFont("JXMonthView.font"));
125            monthDownImage = new ImageIcon(
126                    JXMonthView.class.getResource(UIManager.getString("JXMonthView.monthDownFileName")));
127            monthUpImage = new ImageIcon(
128                    JXMonthView.class.getResource(UIManager.getString("JXMonthView.monthUpFileName")));
129        }
130    
131        protected void uninstallDefaults() {}
132    
133        protected void installKeyboardActions() {
134            // Setup the keyboard handler.
135            InputMap inputMap = monthView.getInputMap(JComponent.WHEN_FOCUSED);
136            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "acceptSelection");
137            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "cancelSelection");
138    
139            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "selectPreviousDay");
140            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "selectNextDay");
141            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "selectDayInPreviousWeek");
142            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "selectDayInNextWeek");
143    
144            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_MASK, false), "addPreviousDay");
145            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_MASK, false), "addNextDay");
146            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_MASK, false), "addToPreviousWeek");
147            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_MASK, false), "addToNextWeek");
148    
149            // Needed to allow for keyboard control in popups.
150            inputMap = monthView.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
151            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "acceptSelection");
152            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "cancelSelection");
153    
154            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "selectPreviousDay");
155            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "selectNextDay");
156            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "selectDayInPreviousWeek");
157            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "selectDayInNextWeek");
158    
159            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_MASK, false), "addPreviousDay");
160            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_MASK, false), "addNextDay");
161            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_MASK, false), "addToPreviousWeek");
162            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_MASK, false), "addToNextWeek");
163    
164            ActionMap actionMap = monthView.getActionMap();
165            actionMap.put("acceptSelection", new KeyboardAction(KeyboardAction.ACCEPT_SELECTION));
166            actionMap.put("cancelSelection", new KeyboardAction(KeyboardAction.CANCEL_SELECTION));
167    
168            actionMap.put("selectPreviousDay", new KeyboardAction(KeyboardAction.SELECT_PREVIOUS_DAY));
169            actionMap.put("selectNextDay", new KeyboardAction(KeyboardAction.SELECT_NEXT_DAY));
170            actionMap.put("selectDayInPreviousWeek", new KeyboardAction(KeyboardAction.SELECT_DAY_PREVIOUS_WEEK));
171            actionMap.put("selectDayInNextWeek", new KeyboardAction(KeyboardAction.SELECT_DAY_NEXT_WEEK));
172    
173            actionMap.put("addPreviousDay", new KeyboardAction(KeyboardAction.ADD_PREVIOUS_DAY));
174            actionMap.put("addNextDay", new KeyboardAction(KeyboardAction.ADD_NEXT_DAY));
175            actionMap.put("addToPreviousWeek", new KeyboardAction(KeyboardAction.ADD_TO_PREVIOUS_WEEK));
176            actionMap.put("addToNextWeek", new KeyboardAction(KeyboardAction.ADD_TO_NEXT_WEEK));
177        }
178    
179        protected void uninstallKeyboardActions() {}
180    
181        protected void installListeners() {
182            propertyChangeListener = createPropertyChangeListener();
183            mouseListener = createMouseListener();
184            mouseMotionListener = createMouseMotionListener();
185    
186            monthView.addPropertyChangeListener(propertyChangeListener);
187            monthView.addMouseListener(mouseListener);
188            monthView.addMouseMotionListener(mouseMotionListener);
189        }
190    
191        protected void uninstallListeners() {
192            monthView.removeMouseMotionListener(mouseMotionListener);
193            monthView.removeMouseListener(mouseListener);
194            monthView.removePropertyChangeListener(propertyChangeListener);
195    
196            mouseMotionListener = null;
197            mouseListener = null;
198            propertyChangeListener = null;
199        }
200    
201        protected PropertyChangeListener createPropertyChangeListener() {
202            return getHandler();
203        }
204    
205        protected LayoutManager createLayoutManager() {
206            return getHandler();
207        }
208    
209        protected MouseListener createMouseListener() {
210            return getHandler();
211        }
212    
213        protected MouseMotionListener createMouseMotionListener() {
214            return getHandler();
215        }
216    
217        private Handler getHandler() {
218            if (handler == null) {
219                handler = new Handler();
220            }
221    
222            return handler;
223        }
224    
225        public boolean isUsingKeyboard() {
226            return usingKeyboard;
227        }
228    
229        public void setUsingKeyboard(boolean val) {
230            usingKeyboard = val;
231        }
232    
233    
234        /**
235         * Returns true if the date passed in is the same as today.
236         *
237         * @param date long representing the date you want to compare to today.
238         * @return true if the date passed is the same as today.
239         */
240        protected boolean isToday(long date) {
241            return date == today;
242        }
243    
244        public long getDayAt(int x, int y) {
245               if (ltr ? (startX > x) : (startX < x) || startY > y) {
246                return -1;
247            }
248    
249            // Determine which column of calendars we're in.
250            int calCol = (ltr ? (x - startX) : (startX - x)) /
251                    (calendarWidth + CALENDAR_SPACING);
252    
253            // Determine which row of calendars we're in.
254            int calRow = (y - startY) / (calendarHeight + CALENDAR_SPACING);
255    
256            if (calRow > numCalRows - 1 || calCol > numCalCols - 1) {
257                return -1;
258            }
259    
260            // Determine what row (week) in the selected month we're in.
261            int row = 1;
262            int boxPaddingX = monthView.getBoxPaddingX();
263            int boxPaddingY = monthView.getBoxPaddingY();
264            row += (((y - startY) -
265                    (calRow * (calendarHeight + CALENDAR_SPACING))) -
266                    (boxPaddingY + monthBoxHeight + boxPaddingY)) /
267                    (boxPaddingY + boxHeight + boxPaddingY);
268            // The first two lines in the calendar are the month and the days
269            // of the week.  Ignore them.
270            row -= 2;
271    
272            if (row < 0 || row > 5) {
273                return -1;
274            }
275    
276            // Determine which column in the selected month we're in.
277            int col = ((ltr ? (x - startX) : (startX - x)) -
278                    (calCol * (calendarWidth + CALENDAR_SPACING))) /
279                    (boxPaddingX + boxWidth + boxPaddingX);
280    
281            // If we're showing week numbers we need to reduce the selected
282            // col index by one.
283            if (showingWeekNumber) {
284                col--;
285            }
286    
287            // Make sure the selected column matches up with a day of the week.
288            if (col < 0 || col > JXMonthView.DAYS_IN_WEEK - 1) {
289                return -1;
290            }
291    
292            // Use the first day of the month as a key point for determining the
293            // date of our click.
294            // The week index of the first day will always be 0.
295            Calendar cal = monthView.getCalendar();
296            cal.setTimeInMillis(firstDisplayedDate);
297            //_cal.set(Calendar.DAY_OF_MONTH, 1);
298            cal.add(Calendar.MONTH, calCol + (calRow * numCalCols));
299    
300            int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
301            int firstDayIndex = dayOfWeek - monthView.getFirstDayOfWeek();
302            if (firstDayIndex < 0) {
303                firstDayIndex += JXMonthView.DAYS_IN_WEEK;
304            }
305    
306            int daysToAdd = (row * JXMonthView.DAYS_IN_WEEK) + (col - firstDayIndex);
307            if (daysToAdd < 0 || daysToAdd >
308                    (cal.getActualMaximum(Calendar.DAY_OF_MONTH) - 1)) {
309                return -1;
310            }
311    
312            cal.add(Calendar.DAY_OF_MONTH, daysToAdd);
313    
314            long selected = cal.getTimeInMillis();
315    
316            // Restore the time.
317            cal.setTimeInMillis(firstDisplayedDate);
318    
319            return selected;
320        }
321    
322    
323        /**
324         * Convenience method so subclasses can get the currently painted day's day of the
325         * week. It is assumed the calendar, _cal, is already set to the correct day.
326         *
327         * @see java.util.Calendar
328         * @return day of the week (Calendar.SATURDAY, Calendar.SUNDAY, ...)
329         */
330        protected int getDayOfTheWeek() {
331            return monthView.getCalendar().get(Calendar.DAY_OF_WEEK);
332        }
333    
334        /**
335         * Returns an index defining which, if any, of the buttons for
336         * traversing the month was pressed.  This method should only be
337         * called when <code>setTraversable</code> is set to true.
338         *
339         * @param x x position of the pointer
340         * @param y y position of the pointer
341         * @return MONTH_UP, MONTH_DOWN or -1 when no button is selected.
342         */
343        protected int getTraversableButtonAt(int x, int y) {
344            if (ltr ? (startX > x) : (startX < x) || startY > y) {
345                return -1;
346            }
347    
348            // Determine which column of calendars we're in.
349            int calCol = (ltr ? (x - startX) : (startX - x)) /
350                    (calendarWidth + CALENDAR_SPACING);
351    
352            // Determine which row of calendars we're in.
353            int calRow = (y - startY) / (calendarHeight + CALENDAR_SPACING);
354    
355            if (calRow > numCalRows - 1 || calCol > numCalCols - 1) {
356                return -1;
357            }
358    
359            // See if we're in the month string area.
360            y = ((y - startY) -
361                (calRow * (calendarHeight + CALENDAR_SPACING))) - monthView.getBoxPaddingY();
362            if (y < arrowPaddingY || y > (monthBoxHeight - arrowPaddingY)) {
363                return -1;
364            }
365    
366            x = ((ltr ? (x - startX) : (startX - x)) -
367                (calCol * (calendarWidth + CALENDAR_SPACING)));
368    
369            if (x > arrowPaddingX && x < (arrowPaddingX +
370                    monthDownImage.getIconWidth() + arrowPaddingX)) {
371                return JXMonthView.MONTH_DOWN;
372            }
373    
374            if (x > (calendarWidth - arrowPaddingX * 2 -
375                    monthUpImage.getIconWidth()) &&
376                    x < (calendarWidth - arrowPaddingX)) {
377                return JXMonthView.MONTH_UP;
378            }
379            return -1;
380        }
381    
382        /**
383         * Calculates the startX/startY position for centering the calendars
384         * within the available space.
385         */
386        private void calculateStartPosition() {
387            // Calculate offset in x-axis for centering calendars.
388            int width = monthView.getWidth();
389            startX = (width - ((calendarWidth * numCalCols) +
390                    (CALENDAR_SPACING * (numCalCols - 1)))) / 2;
391            if (!ltr) {
392                startX = width - startX;
393            }
394    
395            // Calculate offset in y-axis for centering calendars.
396            startY = (monthView.getHeight() - ((calendarHeight * numCalRows) +
397                    (CALENDAR_SPACING * (numCalRows - 1 )))) / 2;
398        }
399    
400        /**
401         * Calculates the numCalCols/numCalRows that determine the number of
402         * calendars that can be displayed.
403         */
404        private void calculateNumDisplayedCals() {
405            int oldNumCalCols = numCalCols;
406            int oldNumCalRows = numCalRows;
407    
408            // Determine how many columns of calendars we want to paint.
409            numCalCols = 1;
410            numCalCols += (monthView.getWidth() - calendarWidth) /
411                    (calendarWidth + CALENDAR_SPACING);
412    
413            // Determine how many rows of calendars we want to paint.
414            numCalRows = 1;
415            numCalRows += (monthView.getHeight() - calendarHeight) /
416                    (calendarHeight + CALENDAR_SPACING);
417    
418            if (oldNumCalCols != numCalCols ||
419                    oldNumCalRows != numCalRows) {
420                calculateLastDisplayedDate();
421            }
422        }
423    
424    
425        public long calculateLastDisplayedDate() {
426            Calendar cal = monthView.getCalendar();
427            cal.setTimeInMillis(firstDisplayedDate);
428    
429            // Figure out the last displayed date.
430            cal.add(Calendar.MONTH, ((numCalCols * numCalRows) - 1));
431            cal.set(Calendar.DAY_OF_MONTH,
432                    cal.getActualMaximum(Calendar.DAY_OF_MONTH));
433            cal.set(Calendar.HOUR_OF_DAY, 23);
434            cal.set(Calendar.MINUTE, 59);
435            cal.set(Calendar.SECOND, 59);
436    
437            lastDisplayedDate = cal.getTimeInMillis();
438    
439            return lastDisplayedDate;
440        }
441    
442        private void calculateDirtyRectForSelection() {
443            if (selection == null) {
444                dirtyRect.x = 0;
445                dirtyRect.y = 0;
446                dirtyRect.width = 0;
447                dirtyRect.height = 0;
448            } else {
449                Calendar cal = monthView.getCalendar();
450                cal.setTimeInMillis(selection.getStart());
451                calculateBoundsForDay(dirtyRect);
452                cal.add(Calendar.DAY_OF_MONTH, 1);
453    
454                Rectangle tmpRect;
455                while (cal.getTimeInMillis() <= selection.getEnd()) {
456                    calculateBoundsForDay(bounds);
457                    tmpRect = dirtyRect.union(bounds);
458                    dirtyRect.x = tmpRect.x;
459                    dirtyRect.y = tmpRect.y;
460                    dirtyRect.width = tmpRect.width;
461                    dirtyRect.height = tmpRect.height;
462                    cal.add(Calendar.DAY_OF_MONTH, 1);
463                }
464    
465                // Restore the time.
466                cal.setTimeInMillis(firstDisplayedDate);
467            }
468        }
469    
470        /**
471         * Calculate the bounding box for drawing a date.  It is assumed that the
472         * calendar, _cal, is already set to the date you want to find the offset
473         * for.
474         *
475         * @param bounds Bounds of the date to draw in.
476         */
477        private void calculateBoundsForDay(Rectangle bounds) {
478            Calendar cal = monthView.getCalendar();
479            int year = cal.get(Calendar.YEAR);
480            int month = cal.get(Calendar.MONTH);
481            int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
482            int weekOfMonth = cal.get(Calendar.WEEK_OF_MONTH);
483    
484            // Determine what row/column we are in.
485            int diffMonths = month - firstDisplayedMonth +
486                    ((year - firstDisplayedYear) * JXMonthView.MONTHS_IN_YEAR);
487            int calRowIndex = diffMonths / numCalCols;
488            int calColIndex = diffMonths - (calRowIndex * numCalCols);
489    
490            // Modify the index relative to the first day of the week.
491            bounds.x = dayOfWeek - monthView.getFirstDayOfWeek();
492            if (bounds.x < 0) {
493                bounds.x += JXMonthView.DAYS_IN_WEEK;
494            }
495    
496            // Offset for location of the day in the week.
497            int boxPaddingX = monthView.getBoxPaddingX();
498            int boxPaddingY = monthView.getBoxPaddingY();
499    
500            // If we're showing week numbers then increase the bounds.x
501            // by one more boxPaddingX boxWidth boxPaddingX.
502            if (showingWeekNumber) {
503                bounds.x++;
504            }
505    
506            // Calculate the x location.
507            bounds.x = ltr ?
508                    bounds.x * (boxPaddingX + boxWidth + boxPaddingX) :
509                    (bounds.x + 1) * (boxPaddingX + boxWidth + boxPaddingX);
510    
511            // Offset for the column the calendar is displayed in.
512            bounds.x += calColIndex * (calendarWidth + CALENDAR_SPACING);
513    
514            // Adjust by centering value.
515            bounds.x = ltr ? startX + bounds.x : startX - bounds.x;
516    
517            // Initial offset for Month and Days of the Week display.
518            bounds.y = boxPaddingY + monthBoxHeight + boxPaddingY +
519                + boxPaddingY + boxHeight + boxPaddingY;
520    
521            // Offset for centering and row the calendar is displayed in.
522            bounds.y += startY + calRowIndex *
523                    (calendarHeight + CALENDAR_SPACING);
524    
525            // Offset for Week of the Month.
526            bounds.y += (weekOfMonth - 1) *
527                    (boxPaddingY + boxHeight + boxPaddingY);
528    
529            bounds.width = boxPaddingX + boxWidth + boxPaddingX;
530            bounds.height = boxPaddingY + boxHeight + boxPaddingY;
531        }
532    
533        @Override
534        public void paint(Graphics g, JComponent c) {
535            super.paint(g, c);
536    
537            Object oldAAValue = null;
538            Graphics2D g2 = (g instanceof Graphics2D) ? (Graphics2D)g : null;
539            if (g2 != null && monthView.isAntialiased()) {
540                oldAAValue = g2.getRenderingHint(
541                    RenderingHints.KEY_TEXT_ANTIALIASING);
542                g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
543                                    RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
544            }
545    
546            Rectangle clip = g.getClipBounds();
547    
548            if (monthView.isOpaque()) {
549                g.setColor(monthView.getBackground());
550                g.fillRect(clip.x, clip.y, clip.width, clip.height);
551            }
552            g.setColor(monthView.getForeground());
553    
554            // Reset the calendar.
555            Calendar cal = monthView.getCalendar();
556            cal.setTimeInMillis(firstDisplayedDate);
557    
558            // Center the calendars horizontally/vertically in the available space.
559            for (int row = 0; row < numCalRows; row++) {
560                // Check if this row falls in the clip region.
561                bounds.x = 0;
562                bounds.y = startY +
563                        row * (calendarHeight + CALENDAR_SPACING);
564                bounds.width = monthView.getWidth();
565                bounds.height = calendarHeight;
566    
567                if (!bounds.intersects(clip)) {
568                    cal.add(Calendar.MONTH, numCalCols);
569                    continue;
570                }
571    
572                for (int column = 0; column < numCalCols; column++) {
573                    // Check if the month to paint falls in the clip.
574                    bounds.x = startX +
575                            (ltr ?
576                                column * (calendarWidth + CALENDAR_SPACING) :
577                                -(column * (calendarWidth + CALENDAR_SPACING) +
578                                        calendarWidth));
579                    bounds.y = startY +
580                            row * (calendarHeight + CALENDAR_SPACING);
581                    bounds.width = calendarWidth;
582                    bounds.height = calendarHeight;
583    
584                    // Paint the month if it intersects the clip.  If we don't move
585                    // the calendar forward a month as it would have if paintMonth
586                    // was called.
587                    if (bounds.intersects(clip)) {
588                        paintMonth(g, bounds.x, bounds.y, bounds.width, bounds.height);
589                    } else {
590                        cal.add(Calendar.MONTH, 1);
591                    }
592                }
593            }
594    
595            // Restore the calendar.
596            cal.setTimeInMillis(firstDisplayedDate);
597            if (g2 != null && monthView.isAntialiased()) {
598                g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
599                                    oldAAValue);
600            }
601        }
602    
603        /**
604         * Paints a month.  It is assumed the calendar, <code>monthView.getCalendar()</code>, is already set to the
605         * first day of the month to be painted.
606         *
607         * @param g Graphics object.
608         * @param x
609         * @param y
610         * @param width
611         * @param height
612         */
613        @SuppressWarnings({"UNUSED_SYMBOL"})
614        private void paintMonth(Graphics g, int x, int y, int width, int height) {
615            Calendar cal = monthView.getCalendar();
616            int days = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
617            Rectangle clip = g.getClipBounds();
618            long day;
619            int oldWeek = -1;
620    
621            // Paint month name background.
622            paintMonthStringBackground(g, x, y,
623                    width, boxPaddingY + monthBoxHeight + boxPaddingY);
624    
625            // Paint arrow buttons for traversing months if enabled.
626            if (monthView.isTraversable()) {
627                g.drawImage(monthDownImage.getImage(),
628                        x + arrowPaddingX, y + ((fullMonthBoxHeight - monthDownImage.getIconHeight()) / 2), null);
629                g.drawImage(monthUpImage.getImage(), x + width - arrowPaddingX - monthUpImage.getIconWidth(),
630                        y + ((fullMonthBoxHeight - monthDownImage.getIconHeight()) / 2), null);
631            }
632    
633            // Paint month name.
634            Font oldFont = monthView.getFont();
635            g.setFont(derivedFont);
636            FontMetrics fm = monthView.getFontMetrics(derivedFont);
637            String monthName = monthsOfTheYear[cal.get(Calendar.MONTH)];
638            monthName = monthName + " " + cal.get(Calendar.YEAR);
639    
640            g.setColor(monthView.getMonthStringForeground());
641            int tmpX =
642                    x + (calendarWidth / 2) -
643                            (fm.stringWidth(monthName) / 2);
644            int tmpY = y + boxPaddingY + ((monthBoxHeight - boxHeight) / 2) +
645                    fm.getAscent();
646            g.drawString(monthName, tmpX, tmpY);
647            g.setFont(oldFont);
648    
649            // Paint background of the short names for the days of the week.
650            tmpX = ltr ? x + (showingWeekNumber ? fullBoxWidth : 0) : x;
651            tmpY = y + fullMonthBoxHeight;
652            int tmpWidth = width - (showingWeekNumber ? fullBoxWidth : 0);
653            paintDayOfTheWeekBackground(g, tmpX, tmpY, tmpWidth, fullBoxHeight);
654    
655            // Paint short representation of day of the week.
656            int dayIndex = monthView.getFirstDayOfWeek() - 1;
657            g.setFont(derivedFont);
658            g.setColor(monthView.getDaysOfTheWeekForeground());
659            fm = monthView.getFontMetrics(derivedFont);
660            String[] daysOfTheWeek = monthView.getDaysOfTheWeek();
661            for (int i = 0; i < JXMonthView.DAYS_IN_WEEK; i++) {
662                tmpX = ltr ?
663                        x + (i * fullBoxWidth) + boxPaddingX +
664                                (boxWidth / 2) -
665                                (fm.stringWidth(daysOfTheWeek[dayIndex]) /
666                                        2) :
667                        x + width - (i * fullBoxWidth) - boxPaddingX -
668                                (boxWidth / 2) -
669                                (fm.stringWidth(daysOfTheWeek[dayIndex]) /
670                                        2);
671                if (showingWeekNumber) {
672                    tmpX += ltr ? fullBoxWidth : -fullBoxWidth;
673                }
674                tmpY = y + fullMonthBoxHeight + boxPaddingY + fm.getAscent();
675                g.drawString(daysOfTheWeek[dayIndex], tmpX, tmpY);
676                dayIndex++;
677                if (dayIndex == JXMonthView.DAYS_IN_WEEK) {
678                    dayIndex = 0;
679                }
680            }
681            g.setFont(oldFont);
682    
683    
684            if (showingWeekNumber) {
685                tmpX = ltr ? x : x + width - fullBoxWidth;
686                paintWeekOfYearBackground(g, tmpX, y + fullMonthBoxHeight + fullBoxHeight, fullBoxWidth,
687                        calendarHeight - (fullMonthBoxHeight + fullBoxHeight));
688            }
689    
690            int oldY = -1;
691            for (int i = 0; i < days; i++) {
692                calculateBoundsForDay(bounds);
693                // Paint the week numbers if we're displaying them.
694                if (showingWeekNumber && oldY != bounds.y) {
695                    oldY = bounds.y;
696                    int weekOfYear = cal.get(Calendar.WEEK_OF_YEAR);
697                    if (weekOfYear != oldWeek) {
698                        tmpX = ltr ? x : x + width - fullBoxWidth;
699                        paintWeekOfYear(g, tmpX, bounds.y, fullBoxWidth, fullBoxHeight, weekOfYear);
700                        oldWeek = weekOfYear;
701                    }
702                }
703    
704                if (bounds.intersects(clip)) {
705                    day = cal.getTimeInMillis();
706    
707                    // Paint bounding box around any date that falls within the
708                    // selection.
709                    if (monthView.isSelectedDate(day)) {
710                        // Keep track of the rectangle for the currently
711                        // selected date so we don't have to recalculate it
712                        // later when it becomes unselected.  This is only
713                        // useful for SINGLE_SELECTION mode.
714                        if (monthView.getSelectionMode() == JXMonthView.SINGLE_SELECTION) {
715                            dirtyRect.x = bounds.x;
716                            dirtyRect.y = bounds.y;
717                            dirtyRect.width = bounds.width;
718                            dirtyRect.height = bounds.height;
719                        }
720                    }
721    
722                    if (monthView.isFlaggedDate(day)) {
723                        paintFlaggedDayBackground(g, bounds.x, bounds.y,
724                                bounds.width, bounds.height, day);
725                        paintFlaggedDayForeground(g, bounds.x, bounds.y,
726                                bounds.width, bounds.height, day);
727                    } else {
728                        paintDayBackground(g, bounds.x, bounds.y,
729                                bounds.width, bounds.height, day);
730                        paintDayForeground(g, bounds.x, bounds.y,
731                                bounds.width, bounds.height, day);
732                    }
733                }
734                cal.add(Calendar.DAY_OF_MONTH, 1);
735            }
736        }
737    
738        private void paintDayOfTheWeekBackground(Graphics g, int x, int y, int width, int height) {
739            g.drawLine(x + boxPaddingX, y + height - 1, x + width - boxPaddingX, y + height - 1);
740        }
741    
742        private void paintWeekOfYearBackground(Graphics g, int x, int y, int width, int height) {
743            x = ltr ? x + width - 1 : x;
744            g.drawLine(x, y + boxPaddingY, x, y + height - boxPaddingY);
745        }
746    
747        /**
748         * Paints the week of the year
749         *
750         * @param g Graphics object
751         * @param x x-coordinate of upper left corner.
752         * @param y y-coordinate of upper left corner.
753         * @param width width of bounding box
754         * @param height height of bounding box
755         * @param weekOfYear week of the year
756         */
757        @SuppressWarnings({"UNUSED_SYMBOL"})
758        private void paintWeekOfYear(Graphics g, int x, int y, int width, int height, int weekOfYear) {
759            String str = Integer.toString(weekOfYear);
760            FontMetrics fm;
761    
762            g.setColor(monthView.getDayForeground(getDayOfTheWeek()));
763    
764            int boxPaddingX = monthView.getBoxPaddingX();
765            int boxPaddingY = monthView.getBoxPaddingY();
766    
767            fm = g.getFontMetrics();
768            g.drawString(str,
769                    ltr ?
770                            x + boxPaddingX +
771                                    boxWidth - fm.stringWidth(str) :
772                            x + boxPaddingX +
773                                    boxWidth - fm.stringWidth(str) - 1,
774                    y + boxPaddingY + fm.getAscent());
775        }
776    
777        /**
778         * Paints the background of the month string.  The bounding box for this
779         * background can be modified by setting its insets via
780         * setMonthStringInsets.  The color of the background can be set via
781         * setMonthStringBackground.
782         *
783         * @see org.jdesktop.swingx.calendar.JXMonthView#setMonthStringBackground
784         * @see org.jdesktop.swingx.calendar.JXMonthView#setMonthStringInsets
785         * @param g Graphics object to paint to.
786         * @param x x-coordinate of upper left corner.
787         * @param y y-coordinate of upper left corner.
788         * @param width width of the bounding box.
789         * @param height height of the bounding box.
790         */
791        protected void paintMonthStringBackground(Graphics g, int x, int y,
792                                                  int width, int height) {
793            // Modify bounds by the month string insets.
794            Insets monthStringInsets = monthView.getMonthStringInsets();
795            x = ltr ? x + monthStringInsets.left : x + monthStringInsets.right;
796            y = y + monthStringInsets.top;
797            width = width - monthStringInsets.left - monthStringInsets.right;
798            height = height - monthStringInsets.top - monthStringInsets.bottom;
799    
800            Graphics2D g2 = (Graphics2D)g;
801            GradientPaint gp = new GradientPaint(x, y + height, new Color(238, 238, 238), x, y, new Color(204, 204, 204));
802            g2.setPaint(gp);
803            g2.fillRect(x, y, width - 1, height - 1);
804            g2.setPaint(new Color(153, 153, 153));
805            g2.drawRect(x, y, width - 1, height - 1);
806        }
807    
808        /**
809         * Paint the background for the specified day.
810         *
811         * @param g Graphics object to paint to
812         * @param x x-coordinate of upper left corner
813         * @param y y-coordinate of upper left corner
814         * @param width width of bounding box for the day
815         * @param height height of bounding box for the day
816         * @param date long value representing the day being painted
817         * @see  org.jdesktop.swingx.calendar.JXMonthView#isSelectedDate
818         * @see  #isToday
819         */
820        protected void paintDayBackground(Graphics g, int x, int y, int width, int height,
821                                          long date) {
822            if (monthView.isSelectedDate(date)) {
823                g.setColor(monthView.getSelectedBackground());
824                g.fillRect(x, y, width, height);
825            }
826    
827            // If the date is today make sure we draw it's background over the selected
828            // background.
829            if (isToday(date)) {
830                // Paint the gradiented border
831                GradientPaint gp = new GradientPaint(x, y, new Color(91, 123, 145), x, y + height, new Color(68, 86, 98));
832                Graphics2D g2 = (Graphics2D)g;
833                g2.setPaint(gp);
834                g2.drawRect(x, y, width - 1, height - 1);
835            }
836        }
837    
838        /**
839         * Paint the foreground for the specified day.
840         *
841         * @param g Graphics object to paint to
842         * @param x x-coordinate of upper left corner
843         * @param y y-coordinate of upper left corner
844         * @param width width of bounding box for the day
845         * @param height height of bounding box for the day
846         * @param date long value representing the day being painted
847         */
848        protected void paintDayForeground(Graphics g, int x, int y, int width, int height,
849                                          long date) {
850            String numericDay = dayOfMonthFormatter.format(date);
851            FontMetrics fm;
852    
853            g.setColor(monthView.getDayForeground(getDayOfTheWeek()));
854    
855            int boxPaddingX = monthView.getBoxPaddingX();
856            int boxPaddingY = monthView.getBoxPaddingY();
857    
858            fm = g.getFontMetrics();
859            g.drawString(numericDay,
860                    ltr ?
861                            x + boxPaddingX +
862                                    boxWidth - fm.stringWidth(numericDay) :
863                            x + boxPaddingX +
864                                    boxWidth - fm.stringWidth(numericDay) - 1,
865                    y + boxPaddingY + fm.getAscent());
866        }
867    
868        /**
869         * Paint the background for the specified flagged day.  The default implementation just
870         * calls <code>paintDayBackground</code>.
871         *
872         * @param g Graphics object to paint to
873         * @param x x-coordinate of upper left corner
874         * @param y y-coordinate of upper left corner
875         * @param width width of bounding box for the day
876         * @param height height of bounding box for the day
877         * @param date long value representing the flagged day being painted
878         */
879        protected void paintFlaggedDayBackground(Graphics g, int x, int y, int width, int height, long date) {
880            paintDayBackground(g, x, y, width, height, date);
881        }
882    
883        /**
884         * Paint the foreground for the specified flagged day.
885         *
886         * @param g Graphics object to paint to
887         * @param x x-coordinate of upper left corner
888         * @param y y-coordinate of upper left corner
889         * @param width width of bounding box for the day
890         * @param height height of bounding box for the day
891         * @param date long value representing the flagged day being painted
892         */
893        protected void paintFlaggedDayForeground(Graphics g, int x, int y, int width, int height, long date) {
894            String numericDay = dayOfMonthFormatter.format(date);
895            FontMetrics fm;
896    
897            int boxPaddingX = monthView.getBoxPaddingX();
898            int boxPaddingY = monthView.getBoxPaddingY();
899    
900            Font oldFont = monthView.getFont();
901            g.setColor(monthView.getFlaggedDayForeground());
902            g.setFont(derivedFont);
903            fm = monthView.getFontMetrics(derivedFont);
904            g.drawString(numericDay,
905                    ltr ?
906                            x + boxPaddingX +
907                                    boxWidth - fm.stringWidth(numericDay):
908                            x + boxPaddingX +
909                                    boxWidth - fm.stringWidth(numericDay) - 1,
910                    y + boxPaddingY + fm.getAscent());
911            g.setFont(oldFont);
912        }
913    
914        private class Handler implements ComponentListener, MouseListener, MouseMotionListener, LayoutManager, PropertyChangeListener {
915            private boolean asKirkWouldSay_FIRE;
916            private long startDate;
917            private long endDate;
918    
919            /** For multiple selection we need to record the date we pivot around. */
920            private long pivotDate = -1;
921    
922            public void mouseClicked(MouseEvent e) {}
923    
924            public void mousePressed(MouseEvent e) {
925                // If we were using the keyboard we aren't anymore.
926                setUsingKeyboard(false);
927    
928                if (!monthView.isEnabled()) {
929                    return;
930                }
931    
932                if (!monthView.hasFocus() && monthView.isFocusable()) {
933                    monthView.requestFocusInWindow();
934                }
935    
936                // Check if one of the month traverse buttons was pushed.
937                if (monthView.isTraversable()) {
938                    int arrowType = getTraversableButtonAt(e.getX(), e.getY());
939                    if (arrowType == JXMonthView.MONTH_DOWN) {
940                        monthView.setFirstDisplayedDate(
941                                DateUtils.getPreviousMonth(monthView.getFirstDisplayedDate()));
942                        return;
943                    } else if (arrowType == JXMonthView.MONTH_UP) {
944                        monthView.setFirstDisplayedDate(
945                                DateUtils.getNextMonth(monthView.getFirstDisplayedDate()));
946                        return;
947                    }
948                }
949    
950                int selectionMode = monthView.getSelectionMode();
951                if (selectionMode == JXMonthView.NO_SELECTION) {
952                    return;
953                }
954    
955                long selected = monthView.getDayAt(e.getX(), e.getY());
956                if (selected == -1) {
957                    return;
958                }
959    
960                // Update the selected dates.
961                startDate = selected;
962                endDate = selected;
963    
964                if (selectionMode == JXMonthView.SINGLE_INTERVAL_SELECTION ||
965                        selectionMode == JXMonthView.WEEK_INTERVAL_SELECTION) {
966                    pivotDate = selected;
967                }
968    
969                monthView.setSelectedDateSpan(new DateSpan(startDate, endDate));
970    
971                // Arm so we fire action performed on mouse release.
972                asKirkWouldSay_FIRE = true;
973            }
974    
975            public void mouseReleased(MouseEvent e) {
976                // If we were using the keyboard we aren't anymore.
977                setUsingKeyboard(false);
978    
979                if (!monthView.isEnabled()) {
980                    return;
981                }
982    
983                if (!monthView.hasFocus() && monthView.isFocusable()) {
984                    monthView.requestFocusInWindow();
985                }
986    
987                if (asKirkWouldSay_FIRE) {
988                    monthView.postActionEvent();
989                }
990                asKirkWouldSay_FIRE = false;
991            }
992    
993            public void mouseEntered(MouseEvent e) {}
994    
995            public void mouseExited(MouseEvent e) {}
996    
997            public void mouseDragged(MouseEvent e) {
998                // If we were using the keyboard we aren't anymore.
999                setUsingKeyboard(false);
1000                int selectionMode = monthView.getSelectionMode();
1001    
1002                if (!monthView.isEnabled() || selectionMode == JXMonthView.NO_SELECTION) {
1003                    return;
1004                }
1005    
1006                int x = e.getX();
1007                int y = e.getY();
1008                long selected = monthView.getDayAt(x, y);
1009    
1010                if (selected == -1) {
1011                    return;
1012                }
1013    
1014                long oldStart = startDate;
1015                long oldEnd = endDate;
1016    
1017                if (selectionMode == JXMonthView.SINGLE_SELECTION) {
1018                    if (selected == oldStart) {
1019                        return;
1020                    }
1021                    startDate = selected;
1022                    endDate = selected;
1023                } else {
1024                    if (selected <= pivotDate) {
1025                        startDate = selected;
1026                        endDate = pivotDate;
1027                    } else if (selected > pivotDate) {
1028                        startDate = pivotDate;
1029                        endDate = selected;
1030                    }
1031                }
1032    
1033                if (selectionMode == JXMonthView.WEEK_INTERVAL_SELECTION) {
1034                    // Do we span a week.
1035                    long start = (selected > pivotDate) ? pivotDate : selected;
1036                    long end = (selected > pivotDate) ? selected : pivotDate;
1037    
1038                    Calendar cal = monthView.getCalendar();
1039                    cal.setTimeInMillis(start);
1040                    int count = 1;
1041                    while (cal.getTimeInMillis() < end) {
1042                        cal.add(Calendar.DAY_OF_MONTH, 1);
1043                        count++;
1044                    }
1045    
1046                    if (count > JXMonthView.DAYS_IN_WEEK) {
1047                        // Move the start date to the first day of the week.
1048                        cal.setTimeInMillis(start);
1049                        int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
1050                        int firstDayOfWeek = monthView.getFirstDayOfWeek();
1051                        int daysFromStart = dayOfWeek - firstDayOfWeek;
1052                        if (daysFromStart < 0) {
1053                            daysFromStart += JXMonthView.DAYS_IN_WEEK;
1054                        }
1055                        cal.add(Calendar.DAY_OF_MONTH, -daysFromStart);
1056    
1057                        startDate = cal.getTimeInMillis();
1058    
1059                        // Move the end date to the last day of the week.
1060                        cal.setTimeInMillis(end);
1061                        dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
1062                        int lastDayOfWeek = firstDayOfWeek - 1;
1063                        if (lastDayOfWeek == 0) {
1064                            lastDayOfWeek = Calendar.SATURDAY;
1065                        }
1066                        int daysTillEnd = lastDayOfWeek - dayOfWeek;
1067                        if (daysTillEnd < 0) {
1068                            daysTillEnd += JXMonthView.DAYS_IN_WEEK;
1069                        }
1070                        cal.add(Calendar.DAY_OF_MONTH, daysTillEnd);
1071                        endDate = cal.getTimeInMillis();
1072                    }
1073                }
1074    
1075                if (oldStart == startDate && oldEnd == endDate) {
1076                    return;
1077                }
1078    
1079                monthView.setSelectedDateSpan(new DateSpan(startDate, endDate));
1080    
1081                // Set trigger.
1082                asKirkWouldSay_FIRE = true;
1083            }
1084    
1085            public void mouseMoved(MouseEvent e) {}
1086    
1087            public void addLayoutComponent(String name, Component comp) {}
1088    
1089            public void removeLayoutComponent(Component comp) {}
1090    
1091            public Dimension preferredLayoutSize(Container parent) {
1092                layoutContainer(parent);
1093                return new Dimension(dim);
1094            }
1095    
1096            public Dimension minimumLayoutSize(Container parent) {
1097                return preferredLayoutSize(parent);
1098            }
1099    
1100            public void layoutContainer(Container parent) {
1101                // Loop through year and get largest representation of the month.
1102                // Keep track of the longest month so we can loop through it to
1103                // determine the width of a date box.
1104                int currDays;
1105                int longestMonth = 0;
1106                int daysInLongestMonth = 0;
1107    
1108                int currWidth;
1109                int longestMonthWidth = 0;
1110    
1111                // We use a bold font for figuring out size constraints since
1112                // it's larger and flaggedDates will be noted in this style.
1113                derivedFont = monthView.getFont().deriveFont(Font.BOLD);
1114                FontMetrics fm = monthView.getFontMetrics(derivedFont);
1115    
1116                Calendar cal = monthView.getCalendar();
1117                cal.set(Calendar.MONTH, cal.getMinimum(Calendar.MONTH));
1118                cal.set(Calendar.DAY_OF_MONTH,
1119                        cal.getActualMinimum(Calendar.DAY_OF_MONTH));
1120                for (int i = 0; i < cal.getMaximum(Calendar.MONTH); i++) {
1121                    currWidth = fm.stringWidth(monthsOfTheYear[i]);
1122                    if (currWidth > longestMonthWidth) {
1123                        longestMonthWidth = currWidth;
1124                    }
1125                    currDays = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
1126                    if (currDays > daysInLongestMonth) {
1127                        longestMonth = cal.get(Calendar.MONTH);
1128                        daysInLongestMonth = currDays;
1129                    }
1130                    cal.add(Calendar.MONTH, 1);
1131                }
1132    
1133                // Loop through the days of the week and adjust the box width
1134                // accordingly.
1135                boxHeight = fm.getHeight();
1136                String[] daysOfTheWeek = monthView.getDaysOfTheWeek();
1137                for (String dayOfTheWeek : daysOfTheWeek) {
1138                    currWidth = fm.stringWidth(dayOfTheWeek);
1139                    if (currWidth > boxWidth) {
1140                        boxWidth = currWidth;
1141                    }
1142                }
1143    
1144                // Loop through longest month and get largest representation of the day
1145                // of the month.
1146                cal.set(Calendar.MONTH, longestMonth);
1147                cal.set(Calendar.DAY_OF_MONTH,
1148                        cal.getActualMinimum(Calendar.DAY_OF_MONTH));
1149                for (int i = 0; i < daysInLongestMonth; i++) {
1150                    currWidth = fm.stringWidth(
1151                            dayOfMonthFormatter.format(cal.getTime()));
1152                    if (currWidth > boxWidth) {
1153                        boxWidth = currWidth;
1154                    }
1155                    cal.add(Calendar.DAY_OF_MONTH, 1);
1156                }
1157    
1158                // If we are displaying week numbers find the largest displayed week number.
1159                if (showingWeekNumber) {
1160                    int val = cal.getActualMaximum(Calendar.WEEK_OF_YEAR);
1161                    currWidth = fm.stringWidth(Integer.toString(val));
1162                    if (currWidth > boxWidth) {
1163                        boxWidth = currWidth;
1164                    }
1165                }
1166    
1167                // If the calendar is traversable, check the icon heights and
1168                // adjust the month box height accordingly.
1169                monthBoxHeight = boxHeight;
1170                if (monthView.isTraversable()) {
1171                    int newHeight = monthDownImage.getIconHeight() +
1172                            arrowPaddingY + arrowPaddingY;
1173                    if (newHeight > monthBoxHeight) {
1174                        monthBoxHeight = newHeight;
1175                    }
1176                }
1177    
1178                // Modify boxWidth if month string is longer
1179                int boxPaddingX = monthView.getBoxPaddingX();
1180                int boxPaddingY = monthView.getBoxPaddingY();
1181                dim.width = (boxWidth + (2 * boxPaddingX)) * JXMonthView.DAYS_IN_WEEK;
1182                if (dim.width < longestMonthWidth) {
1183                    double diff = longestMonthWidth - dim.width;
1184                    if (monthView.isTraversable()) {
1185                        diff += monthDownImage.getIconWidth() +
1186                                monthUpImage.getIconWidth() + (arrowPaddingX * 4);
1187                    }
1188                    boxWidth += Math.ceil(diff / (double)JXMonthView.DAYS_IN_WEEK);
1189                }
1190    
1191    
1192                // Keep track of a full box height/width and full month box height
1193                fullBoxWidth = boxWidth + boxPaddingX + boxPaddingX;
1194                fullBoxHeight = boxHeight + boxPaddingY + boxPaddingY;
1195                fullMonthBoxHeight = monthBoxHeight + boxPaddingY + boxPaddingY;
1196    
1197                // Keep track of calendar width and height for use later.
1198                calendarWidth = fullBoxWidth * JXMonthView.DAYS_IN_WEEK;
1199                if (showingWeekNumber) {
1200                    calendarWidth += fullBoxWidth;
1201                }
1202    
1203                calendarHeight = (fullBoxHeight * 7) + fullMonthBoxHeight;
1204    
1205                // Calculate minimum width/height for the component.
1206                int prefRows = monthView.getPreferredRows();
1207                dim.height = (calendarHeight * prefRows) +
1208                        (CALENDAR_SPACING * (prefRows - 1));
1209    
1210                int prefCols = monthView.getPreferredCols();
1211                dim.width = (calendarWidth * prefCols) +
1212                        (CALENDAR_SPACING * (prefCols - 1));
1213    
1214                // Add insets to the dimensions.
1215                Insets insets = monthView.getInsets();
1216                dim.width += insets.left + insets.right;
1217                dim.height += insets.top + insets.bottom;
1218    
1219                // Restore calendar.
1220                cal.setTimeInMillis(firstDisplayedDate);
1221    
1222                calculateNumDisplayedCals();
1223                calculateStartPosition();
1224    
1225                if (selection != null) {
1226                    long startDate = selection.getStart();
1227                    if (startDate > lastDisplayedDate ||
1228                            startDate < firstDisplayedDate) {
1229                        // Already does the recalculation for the dirty rect.
1230                        monthView.ensureDateVisible(startDate);
1231                    } else {
1232                        calculateDirtyRectForSelection();
1233                    }
1234                }
1235            }
1236    
1237    
1238            public void propertyChange(PropertyChangeEvent evt) {
1239                String property = evt.getPropertyName();
1240    
1241                if ("componentOrientation".equals(property)) {
1242                    ltr = monthView.getComponentOrientation().isLeftToRight();
1243                    monthView.revalidate();
1244                    calculateStartPosition();
1245                    calculateDirtyRectForSelection();
1246                } else if ("selectedDates".equals(property)) {
1247                    selection = (DateSpan)evt.getNewValue();
1248                    // repaint old dirty region
1249                    monthView.repaint(dirtyRect);
1250                    // calculate new dirty region based on selection
1251                    calculateDirtyRectForSelection();
1252                    // repaint new selection
1253                    monthView.repaint(dirtyRect);
1254                } else if ("ensureDateVisibility".equals(property)) {
1255                    calculateDirtyRectForSelection();
1256                } else if ("firstDisplayedDate".equals(property)) {
1257                    firstDisplayedDate = (Long)evt.getNewValue();
1258                } else if ("firstDisplayedMonth".equals(property)) {
1259                    firstDisplayedMonth = (Integer)evt.getNewValue();
1260                } else if ("firstDisplayedYear".equals(property)) {
1261                    firstDisplayedYear = (Integer)evt.getNewValue();
1262                } else if ("today".equals(property)) {
1263                    today = (Long)evt.getNewValue();
1264                } else if ("boxPaddingX".equals(property) || "boxPaddingY".equals(property) ||
1265                        "traversable".equals(property) || "daysOfTheWeek".equals(property) ||
1266                        "border".equals(property) || "font".equals(property) || "weekNumber".equals(property)) {
1267                    boxPaddingX = monthView.getBoxPaddingX();
1268                    boxPaddingY = monthView.getBoxPaddingY();
1269                    showingWeekNumber = monthView.isShowingWeekNumber();
1270                    monthView.revalidate();
1271                }
1272            }
1273    
1274            public void componentResized(ComponentEvent e) {
1275                monthView.revalidate();
1276                monthView.repaint();
1277            }
1278    
1279            public void componentMoved(ComponentEvent e) {}
1280    
1281            public void componentShown(ComponentEvent e) {}
1282    
1283            public void componentHidden(ComponentEvent e) {}
1284        }
1285    
1286        /**
1287         * Class that supports keyboard traversal of the JXMonthView component.
1288         */
1289        private class KeyboardAction extends AbstractAction {
1290            public static final int ACCEPT_SELECTION = 0;
1291            public static final int CANCEL_SELECTION = 1;
1292            public static final int SELECT_PREVIOUS_DAY = 2;
1293            public static final int SELECT_NEXT_DAY = 3;
1294            public static final int SELECT_DAY_PREVIOUS_WEEK = 4;
1295            public static final int SELECT_DAY_NEXT_WEEK = 5;
1296            public static final int ADD_PREVIOUS_DAY = 6;
1297            public static final int ADD_NEXT_DAY = 7;
1298            public static final int ADD_TO_PREVIOUS_WEEK = 8;
1299            public static final int ADD_TO_NEXT_WEEK = 9;
1300    
1301            private int action;
1302    
1303            public KeyboardAction(int action) {
1304                this.action = action;
1305            }
1306    
1307            public void actionPerformed(ActionEvent ev) {
1308                int selectionMode = monthView.getSelectionMode();
1309    
1310                // TODO: Modify this to allow keyboard selection even if we don't have a previous selection.
1311                if (selection != null && selectionMode != JXMonthView.NO_SELECTION) {
1312                    if (!isUsingKeyboard()) {
1313                        originalDateSpan = monthView.getSelectedDateSpan();
1314                    }
1315    
1316                    if (action >= ACCEPT_SELECTION && action <= CANCEL_SELECTION && isUsingKeyboard()) {
1317                        if (action == CANCEL_SELECTION) {
1318                            // Restore the original selection.
1319                            monthView.setSelectedDateSpan(originalDateSpan);
1320                            monthView.postActionEvent();
1321                        } else {
1322                            // Accept the keyboard selection.
1323                            monthView.setSelectedDateSpan(monthView.getSelectedDateSpan());
1324                            monthView.postActionEvent();
1325                        }
1326                        setUsingKeyboard(false);
1327                    } else if (action >= SELECT_PREVIOUS_DAY && action <= SELECT_DAY_NEXT_WEEK) {
1328                        setUsingKeyboard(true);
1329                        traverse(action);
1330                    } else if (selectionMode >= JXMonthView.SINGLE_INTERVAL_SELECTION &&
1331                            action >= ADD_PREVIOUS_DAY && action <= ADD_TO_NEXT_WEEK) {
1332                        setUsingKeyboard(true);
1333                        addToSelection(action);
1334                    }
1335                }
1336            }
1337    
1338            private void traverse(int action) {
1339                long oldStart = selection.getStart();
1340                Calendar cal = monthView.getCalendar();
1341                cal.setTimeInMillis(oldStart);
1342                switch (action) {
1343                    case SELECT_PREVIOUS_DAY:
1344                        cal.add(Calendar.DAY_OF_MONTH, -1);
1345                        break;
1346                    case SELECT_NEXT_DAY:
1347                        cal.add(Calendar.DAY_OF_MONTH, 1);
1348                        break;
1349                    case SELECT_DAY_PREVIOUS_WEEK:
1350                        cal.add(Calendar.DAY_OF_MONTH, -JXMonthView.DAYS_IN_WEEK);
1351                        break;
1352                    case SELECT_DAY_NEXT_WEEK:
1353                        cal.add(Calendar.DAY_OF_MONTH, JXMonthView.DAYS_IN_WEEK);
1354                        break;
1355                }
1356    
1357                long newStartDate = cal.getTimeInMillis();
1358                if (newStartDate != oldStart) {
1359                    monthView.setSelectedDateSpan(new DateSpan(newStartDate, newStartDate));
1360                    monthView.ensureDateVisible(newStartDate);
1361                }
1362                // Restore the original time value.
1363                cal.setTimeInMillis(firstDisplayedDate);
1364            }
1365    
1366            /**
1367             * If we are in a mode that allows for range selection this method
1368             * will extend the currently selected range.
1369             *
1370             * NOTE: This may not be the expected behavior for the keyboard controls
1371             * and we ay need to update this code to act in a way that people expect.
1372             */
1373            private void addToSelection(int action) {
1374                long newStartDate = -1;
1375                long newEndDate = -1;
1376                long selectionStart = -1;
1377                long selectionEnd = -1;
1378    
1379                if (selection != null) {
1380                    newStartDate = selectionStart = selection.getStart();
1381                    newEndDate = selectionEnd = selection.getEnd();
1382                }
1383    
1384                boolean isForward = true;
1385    
1386                Calendar cal = monthView.getCalendar();
1387                switch (action) {
1388                    case ADD_PREVIOUS_DAY:
1389                        cal.setTimeInMillis(selectionStart);
1390                        cal.add(Calendar.DAY_OF_MONTH, -1);
1391                        newStartDate = cal.getTimeInMillis();
1392                        isForward = false;
1393                        break;
1394                    case ADD_NEXT_DAY:
1395                        cal.setTimeInMillis(selectionEnd);
1396                        cal.add(Calendar.DAY_OF_MONTH, 1);
1397                        newEndDate = cal.getTimeInMillis();
1398                        break;
1399                    case ADD_TO_PREVIOUS_WEEK:
1400                        cal.setTimeInMillis(selectionStart);
1401                        cal.add(Calendar.DAY_OF_MONTH, -JXMonthView.DAYS_IN_WEEK);
1402                        newStartDate = cal.getTimeInMillis();
1403                        isForward = false;
1404                        break;
1405                    case ADD_TO_NEXT_WEEK:
1406                        cal.setTimeInMillis(selectionEnd);
1407                        cal.add(Calendar.DAY_OF_MONTH, JXMonthView.DAYS_IN_WEEK);
1408                        newEndDate = cal.getTimeInMillis();
1409                        break;
1410                }
1411                if (newStartDate != selectionStart || newEndDate != selectionEnd) {
1412                    monthView.setSelectedDateSpan(new DateSpan(newStartDate, newEndDate));
1413                    monthView.ensureDateVisible(isForward ? newEndDate : newStartDate);
1414                }
1415    
1416                // Restore the original time value.
1417                cal.setTimeInMillis(firstDisplayedDate);
1418            }
1419        }
1420    }