001/*
002 * $Id: BasicMonthViewUI.java 3927 2011-02-22 16:34:11Z kleopatra $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 * 
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 * 
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021package org.jdesktop.swingx.plaf.basic;
022
023import java.awt.Component;
024import java.awt.Container;
025import java.awt.Dimension;
026import java.awt.Graphics;
027import java.awt.Insets;
028import java.awt.LayoutManager;
029import java.awt.Point;
030import java.awt.Rectangle;
031import java.awt.event.ActionEvent;
032import java.awt.event.InputEvent;
033import java.awt.event.KeyEvent;
034import java.awt.event.MouseEvent;
035import java.awt.event.MouseListener;
036import java.awt.event.MouseMotionListener;
037import java.beans.PropertyChangeEvent;
038import java.beans.PropertyChangeListener;
039import java.lang.reflect.Constructor;
040import java.lang.reflect.InvocationTargetException;
041import java.text.DateFormatSymbols;
042import java.util.Calendar;
043import java.util.Date;
044import java.util.Locale;
045import java.util.SortedSet;
046import java.util.logging.Logger;
047
048import javax.swing.AbstractAction;
049import javax.swing.ActionMap;
050import javax.swing.CellRendererPane;
051import javax.swing.Icon;
052import javax.swing.InputMap;
053import javax.swing.JComponent;
054import javax.swing.KeyStroke;
055import javax.swing.LookAndFeel;
056import javax.swing.UIManager;
057import javax.swing.plaf.ComponentUI;
058
059import org.jdesktop.swingx.JXMonthView;
060import org.jdesktop.swingx.SwingXUtilities;
061import org.jdesktop.swingx.action.AbstractActionExt;
062import org.jdesktop.swingx.calendar.CalendarUtils;
063import org.jdesktop.swingx.calendar.DateSelectionModel;
064import org.jdesktop.swingx.calendar.DateSelectionModel.SelectionMode;
065import org.jdesktop.swingx.event.DateSelectionEvent;
066import org.jdesktop.swingx.event.DateSelectionListener;
067import org.jdesktop.swingx.plaf.MonthViewUI;
068import org.jdesktop.swingx.plaf.UIManagerExt;
069
070/**
071 * Base implementation of the <code>JXMonthView</code> UI.<p>
072 *
073 * <b>Note</b>: The api changed considerably between releases 0.9.4 and 0.9.5.  
074 * <p>
075 * 
076 * The general drift of the change was to delegate all text rendering to a dedicated
077 * rendering controller (currently named RenderingHandler), similar to 
078 * the collection view rendering. The UI itself keeps layout and positioning of
079 * the rendering components. Plus updating on property changes received from the 
080 * monthView. <p>
081 * 
082 * 
083 * <p>   
084 * Painting: coordinate systems.
085 * 
086 * <ul>
087 * <li> Screen coordinates of months/days, accessible via the getXXBounds() methods. These
088 * coordinates are absolute in the system of the monthView. 
089 * <li> The grid of visible months with logical row/column coordinates. The logical 
090 * coordinates are adjusted to ComponentOrientation. 
091 * <li> The grid of days in a month with logical row/column coordinates. The logical 
092 * coordinates are adjusted to ComponentOrientation. The columns 
093 * are the (optional) week header and the days of the week. The rows are the day header  
094 * and the weeks in a month. The day header shows  the localized names of the days and 
095 * has the row coordinate DAY_HEADER_ROW. It is shown always.
096 * The row header shows the week number in the year and has the column coordinate WEEK_HEADER_COLUMN. It
097 * is shown only if the showingWeekNumber property is true.  
098 * </ul>
099 * 
100 * On the road to "zoomable" date range views (Vista-style).<p>
101 * 
102 * Added support (doesn't do anything yet, zoom-logic must yet be defined) 
103 * by way of an active calendar header which is added to the monthView if zoomable. 
104 * It is disabled by default. In this mode, the view is always
105 * traversable and shows exactly one calendar. It is orthogonal to the classic 
106 * mode, that is client code should not be effected in any way as long as the mode 
107 * is not explicitly enabled. <p>
108 * 
109 * NOTE to LAF implementors: the active calendar header is very, very, very raw and 
110 * sure to change without much notice. Better not yet to support it right now.
111 * 
112 * @author dmouse
113 * @author rbair
114 * @author rah003
115 * @author Jeanette Winzenburg
116 */
117public class BasicMonthViewUI extends MonthViewUI {
118    @SuppressWarnings("all")
119    private static final Logger LOG = Logger.getLogger(BasicMonthViewUI.class
120            .getName());
121    
122    private static final int CALENDAR_SPACING = 10;
123    
124    /** Return value used to identify when the month down button is pressed. */
125    public static final int MONTH_DOWN = 1;
126    /** Return value used to identify when the month up button is pressed. */
127    public static final int MONTH_UP = 2;
128
129    // constants for day columns
130    protected static final int WEEK_HEADER_COLUMN = 0;
131    protected static final int DAYS_IN_WEEK = 7;
132    protected static final int FIRST_DAY_COLUMN = WEEK_HEADER_COLUMN + 1;
133    protected static final int LAST_DAY_COLUMN = FIRST_DAY_COLUMN + DAYS_IN_WEEK -1;
134
135    // constants for day rows (aka: weeks)
136    protected static final int DAY_HEADER_ROW = 0;
137    protected static final int WEEKS_IN_MONTH = 6;
138    protected static final int FIRST_WEEK_ROW = DAY_HEADER_ROW + 1;
139    protected static final int LAST_WEEK_ROW = FIRST_WEEK_ROW + WEEKS_IN_MONTH - 1;
140
141
142    /** the component we are installed for. */
143    protected JXMonthView monthView;
144    // listeners
145    private PropertyChangeListener propertyChangeListener;
146    private MouseListener mouseListener;
147    private MouseMotionListener mouseMotionListener;
148    private Handler handler;
149
150    // fields related to visible date range
151    /** end of day of the last visible month. */
152    private Date lastDisplayedDate;
153    /** 
154    
155    //---------- fields related to selection/navigation
156
157
158    /** flag indicating keyboard navigation. */
159    private boolean usingKeyboard = false;
160    /** For interval selections we need to record the date we pivot around. */
161    private Date pivotDate = null;
162    /**
163     * Date span used by the keyboard actions to track the original selection.
164     */
165    private SortedSet<Date> originalDateSpan;
166
167    //------------------ visuals
168
169    protected boolean isLeftToRight;
170    protected Icon monthUpImage;
171    protected Icon monthDownImage;
172    
173
174    /**
175     * The padding for month traversal icons.
176     * PENDING JW: decouple rendering and hit-detection.
177     */
178    private int arrowPaddingX = 3;
179    private int arrowPaddingY = 3;
180    
181    
182    /** height of month header including the monthView's box padding. */
183    private int fullMonthBoxHeight;
184    /** 
185     * width of a "day" box including the monthView's box padding
186     * this is the same for days-of-the-week, weeks-of-the-year and days
187     */
188    private int fullBoxWidth;
189    /** 
190     * height of a "day" box including the monthView's box padding
191     * this is the same for days-of-the-week, weeks-of-the-year and days
192     */
193    private int fullBoxHeight;
194    /** the width of a single month display. */
195    private int calendarWidth;
196    /** the height of a single month display. */
197    private int calendarHeight;
198    /** the height of a single month grid cell, including padding. */
199    private int fullCalendarHeight;
200    /** the width of a single month grid cell, including padding. */
201    private int fullCalendarWidth;
202    /** The number of calendars displayed vertically. */
203    private int calendarRowCount = 1;
204    /** The number of calendars displayed horizontally. */
205    private int calendarColumnCount = 1;
206    
207    /**
208     * The bounding box of the grid of visible months. 
209     */
210    protected Rectangle calendarGrid = new Rectangle();
211    
212    /**
213     * The Strings used for the day headers. This is the fall-back for
214     * the monthView if no custom strings are set. 
215     * PENDING JW: delegate to RenderingHandler?
216     */
217    private String[] daysOfTheWeek;
218
219    /**
220     * Provider of configured components for text rendering.
221     */
222    private CalendarRenderingHandler renderingHandler;
223    /**
224     * The CellRendererPane for stamping rendering comps.
225     */
226    private CellRendererPane rendererPane;
227
228    /**
229     * The CalendarHeaderHandler which provides the header component if zoomable.
230     */
231    private CalendarHeaderHandler calendarHeaderHandler;
232    
233
234    @SuppressWarnings({"UnusedDeclaration"})
235    public static ComponentUI createUI(JComponent c) {
236        return new BasicMonthViewUI();
237    }
238
239    /**
240     * Installs the component as appropriate for the current lf.
241     * 
242     * PENDING JW: clarify sequence of installXX methods. 
243     */
244    @Override
245    public void installUI(JComponent c) {
246        monthView = (JXMonthView)c;
247        monthView.setLayout(createLayoutManager());
248        
249        // PENDING JW: move to installDefaults or installComponents?
250        installRenderingHandler();
251        
252        installDefaults();
253        installDelegate();
254        installKeyboardActions();
255        installComponents();
256        updateLocale(false);
257        updateZoomable();
258        installListeners();
259    }
260
261
262    @Override
263    public void uninstallUI(JComponent c) {
264        uninstallRenderingHandler();
265        uninstallListeners();
266        uninstallKeyboardActions();
267        uninstallDefaults();
268        uninstallComponents();
269        monthView.setLayout(null);
270        monthView = null;
271    }
272
273    /**
274     * Creates and installs the calendar header handler. 
275     */
276    protected void installComponents() {
277        setCalendarHeaderHandler(createCalendarHeaderHandler());
278        getCalendarHeaderHandler().install(monthView);
279    }
280
281    /**
282     * Uninstalls the calendar header handler.
283     */
284    protected void uninstallComponents() {
285        getCalendarHeaderHandler().uninstall(monthView);
286        setCalendarHeaderHandler(null);
287    }
288
289    /**
290     * Installs default values. <p>
291     * 
292     * This is refactored to only install default properties on the monthView.
293     * Extracted install of this delegate's properties into installDelegate. 
294     *  
295     */
296    protected void installDefaults() {
297        LookAndFeel.installProperty(monthView, "opaque", Boolean.TRUE);
298        
299       // @KEEP JW: do not use the core install methods (might have classloader probs)
300        // instead access all properties via the UIManagerExt ..
301        //        BasicLookAndFeel.installColorsAndFont(monthView, 
302//                "JXMonthView.background", "JXMonthView.foreground", "JXMonthView.font");
303        
304        if (SwingXUtilities.isUIInstallable(monthView.getBackground())) {
305            monthView.setBackground(UIManagerExt.getColor("JXMonthView.background"));
306        }
307        if (SwingXUtilities.isUIInstallable(monthView.getForeground())) {
308            monthView.setForeground(UIManagerExt.getColor("JXMonthView.foreground"));
309        }
310        if (SwingXUtilities.isUIInstallable(monthView.getFont())) {
311            // PENDING JW: missing in managerExt? Or not applicable anyway?
312            monthView.setFont(UIManager.getFont("JXMonthView.font"));
313        }
314        if (SwingXUtilities.isUIInstallable(monthView.getMonthStringBackground())) {
315            monthView.setMonthStringBackground(UIManagerExt.getColor("JXMonthView.monthStringBackground"));
316        }
317        if (SwingXUtilities.isUIInstallable(monthView.getMonthStringForeground())) {
318            monthView.setMonthStringForeground(UIManagerExt.getColor("JXMonthView.monthStringForeground"));
319        }
320        if (SwingXUtilities.isUIInstallable(monthView.getDaysOfTheWeekForeground())) {
321            monthView.setDaysOfTheWeekForeground(UIManagerExt.getColor("JXMonthView.daysOfTheWeekForeground"));
322        }
323        if (SwingXUtilities.isUIInstallable(monthView.getSelectionBackground())) {
324            monthView.setSelectionBackground(UIManagerExt.getColor("JXMonthView.selectedBackground"));
325        }
326        if (SwingXUtilities.isUIInstallable(monthView.getSelectionForeground())) {
327            monthView.setSelectionForeground(UIManagerExt.getColor("JXMonthView.selectedForeground"));
328        }
329        if (SwingXUtilities.isUIInstallable(monthView.getFlaggedDayForeground())) {
330            monthView.setFlaggedDayForeground(UIManagerExt.getColor("JXMonthView.flaggedDayForeground"));
331        }
332        
333        monthView.setBoxPaddingX(UIManagerExt.getInt("JXMonthView.boxPaddingX"));
334        monthView.setBoxPaddingY(UIManagerExt.getInt("JXMonthView.boxPaddingY"));
335    }
336
337    /**
338     * Installs this ui delegate's properties.
339     */
340    protected void installDelegate() {
341        isLeftToRight = monthView.getComponentOrientation().isLeftToRight();
342        // PENDING JW: remove here if rendererHandler takes over control completely
343        // as is, some properties are duplicated
344        monthDownImage = UIManager.getIcon("JXMonthView.monthDownFileName");
345        monthUpImage = UIManager.getIcon("JXMonthView.monthUpFileName");
346        // install date related state
347        setFirstDisplayedDay(monthView.getFirstDisplayedDay());
348    }
349
350    
351    protected void uninstallDefaults() {}
352
353    protected void installKeyboardActions() {
354        // Setup the keyboard handler.
355        // JW: changed (0.9.6) to when-ancestor just to be on the safe side
356        // if the title contain active comps
357        installKeyBindings(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
358        // JW: removed the automatic keybindings in WHEN_IN_FOCUSED
359        // which caused #555-swingx (binding active if not focused)
360        ActionMap actionMap = monthView.getActionMap();
361        KeyboardAction acceptAction = new KeyboardAction(KeyboardAction.ACCEPT_SELECTION);
362        actionMap.put("acceptSelection", acceptAction);
363        KeyboardAction cancelAction = new KeyboardAction(KeyboardAction.CANCEL_SELECTION);
364        actionMap.put("cancelSelection", cancelAction);
365
366        actionMap.put("selectPreviousDay", new KeyboardAction(KeyboardAction.SELECT_PREVIOUS_DAY));
367        actionMap.put("selectNextDay", new KeyboardAction(KeyboardAction.SELECT_NEXT_DAY));
368        actionMap.put("selectDayInPreviousWeek", new KeyboardAction(KeyboardAction.SELECT_DAY_PREVIOUS_WEEK));
369        actionMap.put("selectDayInNextWeek", new KeyboardAction(KeyboardAction.SELECT_DAY_NEXT_WEEK));
370
371        actionMap.put("adjustSelectionPreviousDay", new KeyboardAction(KeyboardAction.ADJUST_SELECTION_PREVIOUS_DAY));
372        actionMap.put("adjustSelectionNextDay", new KeyboardAction(KeyboardAction.ADJUST_SELECTION_NEXT_DAY));
373        actionMap.put("adjustSelectionPreviousWeek", new KeyboardAction(KeyboardAction.ADJUST_SELECTION_PREVIOUS_WEEK));
374        actionMap.put("adjustSelectionNextWeek", new KeyboardAction(KeyboardAction.ADJUST_SELECTION_NEXT_WEEK));
375
376        
377        actionMap.put(JXMonthView.COMMIT_KEY, acceptAction);
378        actionMap.put(JXMonthView.CANCEL_KEY, cancelAction);
379        
380        // PENDING JW: complete (year-, decade-, ?? ) and consolidate with KeyboardAction
381        // additional navigation actions
382        AbstractActionExt prev = new AbstractActionExt() {
383
384            @Override
385            public void actionPerformed(ActionEvent e) {
386                previousMonth();
387            }
388            
389        };
390        monthView.getActionMap().put("scrollToPreviousMonth", prev);
391        AbstractActionExt next = new AbstractActionExt() {
392
393            @Override
394            public void actionPerformed(ActionEvent e) {
395                nextMonth();
396            }
397            
398        };
399        monthView.getActionMap().put("scrollToNextMonth", next);
400        
401    }
402
403    
404    /**
405     * @param inputMap
406     */
407    private void installKeyBindings(int type) {
408        InputMap inputMap = monthView.getInputMap(type);
409        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "acceptSelection");
410        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "cancelSelection");
411
412        // @KEEP quickly check #606-swingx: keybindings not working in internalframe
413        // eaten somewhere
414//        inputMap.put(KeyStroke.getKeyStroke("F1"), "selectPreviousDay");
415
416        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "selectPreviousDay");
417        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "selectNextDay");
418        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "selectDayInPreviousWeek");
419        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "selectDayInNextWeek");
420
421        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_MASK, false), "adjustSelectionPreviousDay");
422        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_MASK, false), "adjustSelectionNextDay");
423        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_MASK, false), "adjustSelectionPreviousWeek");
424        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_MASK, false), "adjustSelectionNextWeek");
425    }
426
427    /**
428     * @param inputMap
429     */
430    private void uninstallKeyBindings(int type) {
431        InputMap inputMap = monthView.getInputMap(type);
432        inputMap.clear();
433    }
434
435    protected void uninstallKeyboardActions() {}
436
437    protected void installListeners() {
438        propertyChangeListener = createPropertyChangeListener();
439        mouseListener = createMouseListener();
440        mouseMotionListener = createMouseMotionListener();
441        
442        monthView.addPropertyChangeListener(propertyChangeListener);
443        monthView.addMouseListener(mouseListener);
444        monthView.addMouseMotionListener(mouseMotionListener);
445
446        monthView.getSelectionModel().addDateSelectionListener(getHandler());
447    }
448
449    protected void uninstallListeners() {
450        monthView.getSelectionModel().removeDateSelectionListener(getHandler());
451        monthView.removeMouseMotionListener(mouseMotionListener);
452        monthView.removeMouseListener(mouseListener);
453        monthView.removePropertyChangeListener(propertyChangeListener);
454
455        mouseMotionListener = null;
456        mouseListener = null;
457        propertyChangeListener = null;
458    }
459
460    /**
461     * Creates and installs the renderingHandler and infrastructure to use it.
462     */
463    protected void installRenderingHandler() {
464        setRenderingHandler(createRenderingHandler());
465        if (getRenderingHandler() != null) {
466            rendererPane = new CellRendererPane();
467            monthView.add(rendererPane);
468        }
469    }
470    
471    /**
472     * Uninstalls the renderingHandler and infrastructure that used it.
473     */
474    protected void uninstallRenderingHandler() {
475        if (getRenderingHandler() == null) return;
476        monthView.remove(rendererPane);
477        rendererPane = null;
478        setRenderingHandler(null);
479    }
480
481    /**
482     * Returns the <code>CalendarRenderingHandler</code> to use. Subclasses may override to 
483     * plug-in custom implementations. <p>
484     * 
485     * This implementation returns an instance of RenderingHandler.
486     * 
487     * @return the endering handler to use for painting, must not be null
488     */
489    protected CalendarRenderingHandler createRenderingHandler() {
490        return new RenderingHandler();
491    }
492    
493    /**
494     * @param renderingHandler the renderingHandler to set
495     */
496    protected void setRenderingHandler(CalendarRenderingHandler renderingHandler) {
497        this.renderingHandler = renderingHandler;
498    }
499
500    /**
501     * @return the renderingHandler
502     */
503    protected CalendarRenderingHandler getRenderingHandler() {
504        return renderingHandler;
505    }
506
507    /**
508     * 
509     * Empty subclass for backward compatibility. The original implementation was 
510     * extracted as standalone class and renamed to BasicCalendarRenderingHandler. <p>
511     * 
512     * This will be available for extension by LAF providers until all collaborators 
513     * in the new rendering pipeline are ready for public exposure.
514     */
515    protected static class RenderingHandler extends BasicCalendarRenderingHandler {
516        
517    }
518    /**
519     * Binds/clears the keystrokes in the component input map, 
520     * based on the monthView's componentInputMap enabled property.
521     * 
522     * @see org.jdesktop.swingx.JXMonthView#isComponentInputMapEnabled()
523     */
524    protected void updateComponentInputMap() {
525        if (monthView.isComponentInputMapEnabled()) {
526            installKeyBindings(JComponent.WHEN_IN_FOCUSED_WINDOW);
527        } else {
528            uninstallKeyBindings(JComponent.WHEN_IN_FOCUSED_WINDOW);
529        }
530    }
531
532
533
534    /**
535     * Updates internal state according to monthView's locale. Revalidates the
536     * monthView if the boolean parameter is true.
537     * 
538     * @param revalidate a boolean indicating whether the monthView should be 
539     * revalidated after the change.
540     */
541    protected void updateLocale(boolean revalidate) {
542        Locale locale = monthView.getLocale();
543        if (getRenderingHandler() != null) {
544            getRenderingHandler().setLocale(locale);
545        }
546
547        // fixed JW: respect property in UIManager if available
548        // PENDING JW: what to do if weekdays had been set
549        // with JXMonthView method? how to detect?
550        daysOfTheWeek = (String[]) UIManager.get("JXMonthView.daysOfTheWeek");
551
552        if (daysOfTheWeek == null) {
553            daysOfTheWeek = new String[7];
554            String[] dateFormatSymbols = DateFormatSymbols.getInstance(locale)
555                    .getShortWeekdays();
556            daysOfTheWeek = new String[JXMonthView.DAYS_IN_WEEK];
557            for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
558                daysOfTheWeek[i - 1] = dateFormatSymbols[i];
559            }
560        }
561        if (revalidate) {
562            monthView.invalidate();
563            monthView.validate();
564        }
565    }
566
567   @Override
568   public String[] getDaysOfTheWeek() {
569       String[] days = new String[daysOfTheWeek.length];
570       System.arraycopy(daysOfTheWeek, 0, days, 0, days.length);
571       return days;
572   }
573   
574
575//---------------------- listener creation    
576    protected PropertyChangeListener createPropertyChangeListener() {
577        return getHandler();
578    }
579
580    protected LayoutManager createLayoutManager() {
581        return getHandler();
582    }
583
584    protected MouseListener createMouseListener() {
585        return getHandler();
586    }
587
588    protected MouseMotionListener createMouseMotionListener() {
589        return getHandler();
590    }
591
592    private Handler getHandler() {
593        if (handler == null) {
594            handler = new Handler();
595        }
596
597        return handler;
598    }
599
600    public boolean isUsingKeyboard() {
601        return usingKeyboard;
602    }
603
604    public void setUsingKeyboard(boolean val) {
605        usingKeyboard = val;
606    }
607
608
609
610    // ----------------------- mapping day coordinates
611
612    /**
613     * Returns the bounds of the day in the grid of days which contains the
614     * given location. The bounds are in monthView screen coordinate system.
615     * <p>
616     * 
617     * Note: this is a pure geometric mapping. The returned rectangle need not
618     * necessarily map to a date in the month which contains the location, it
619     * can represent a week-number/column header or a leading/trailing date.
620     * 
621     * @param x the x position of the location in pixel
622     * @param y the y position of the location in pixel
623     * @return the bounds of the day which contains the location, or null if
624     *         outside
625     */
626    protected Rectangle getDayBoundsAtLocation(int x, int y) {
627        Rectangle monthDetails = getMonthDetailsBoundsAtLocation(x, y);
628        if ((monthDetails == null) || (!monthDetails.contains(x, y)))
629            return null;
630        // calculate row/column in absolute grid coordinates
631        int row = (y - monthDetails.y) / fullBoxHeight;
632        int column = (x - monthDetails.x) / fullBoxWidth;
633        return new Rectangle(monthDetails.x + column * fullBoxWidth, monthDetails.y
634                + row * fullBoxHeight, fullBoxWidth, fullBoxHeight);
635    }
636
637    /**
638     * Returns the bounds of the day box at logical coordinates in the given month.
639     * The row's range is from DAY_HEADER_ROW to LAST_WEEK_ROW. Column's range is from
640     * WEEK_HEADER_COLUMN to LAST_DAY_COLUMN.
641     * 
642     * @param month the month containing the day box  
643     * @param row the logical row (== week) coordinate in the day grid 
644     * @param column the logical column (== day) coordinate in the day grid
645     * @return the bounds of the daybox or null if not showing
646     * @throws IllegalArgumentException if row or column are out off range.
647     * 
648     * @see #getDayGridPositionAtLocation(int, int)
649     */
650    protected Rectangle getDayBoundsInMonth(Date month, int row, final int column) {
651        checkValidRow(row, column);
652        if ((WEEK_HEADER_COLUMN == column) && !monthView.isShowingWeekNumber()) return null;
653        Rectangle monthBounds = getMonthBounds(month);
654        if (monthBounds == null) return null;
655        // dayOfWeek header is shown always
656        monthBounds.y += getMonthHeaderHeight() + (row - DAY_HEADER_ROW) * fullBoxHeight;
657        // PENDING JW: still looks fishy ... 
658        int absoluteColumn = column - FIRST_DAY_COLUMN;
659        if (monthView.isShowingWeekNumber()) {
660            absoluteColumn++;
661        }
662        if (isLeftToRight) {
663           monthBounds.x += absoluteColumn * fullBoxWidth; 
664        } else {
665            int leading = monthBounds.x + monthBounds.width - fullBoxWidth; 
666            monthBounds.x = leading - absoluteColumn * fullBoxWidth;
667        }
668        monthBounds.width = fullBoxWidth;
669        monthBounds.height = fullBoxHeight;
670        return monthBounds;
671    }
672    
673
674    /**
675     * Returns the logical coordinates of the day which contains the given
676     * location. The p.x of the returned value represents the week header or the 
677     * day of week, ranging from WEEK_HEADER_COLUMN to LAST_DAY_COLUMN. The
678     * p.y represents the day header or week of the month, ranging from DAY_HEADER_ROW
679     * to LAST_WEEK_ROW. The transformation takes care of
680     * ComponentOrientation.
681     * <p>
682     * 
683     * Note: The returned grid position need not
684     * necessarily map to a date in the month which contains the location, it
685     * can represent a week-number/column header or a leading/trailing date.
686     * 
687     * @param x the x position of the location in pixel
688     * @param y the y position of the location in pixel
689     * @return the logical coordinates of the day in the grid of days in a month
690     *         or null if outside.
691     *         
692     * @see #getDayBoundsInMonth(Date, int, int)     
693     */
694    protected Point getDayGridPositionAtLocation(int x, int y) {
695        Rectangle monthDetailsBounds = getMonthDetailsBoundsAtLocation(x, y);
696        if ((monthDetailsBounds == null) ||(!monthDetailsBounds.contains(x, y))) return null;
697        int calendarRow = (y - monthDetailsBounds.y) / fullBoxHeight + DAY_HEADER_ROW; 
698        int absoluteColumn = (x - monthDetailsBounds.x) / fullBoxWidth;
699        int calendarColumn = absoluteColumn + FIRST_DAY_COLUMN;
700        if (!isLeftToRight) {
701            int leading = monthDetailsBounds.x + monthDetailsBounds.width;
702            calendarColumn = (leading - x) / fullBoxWidth + FIRST_DAY_COLUMN;
703        }
704        if (monthView.isShowingWeekNumber()) {
705            calendarColumn -= 1;
706        }
707        return new Point(calendarColumn, calendarRow);
708    }
709
710    /**
711     * Returns the Date defined by the logical 
712     * grid coordinates relative to the given month. May be null if the
713     * logical coordinates represent a header in the day grid or is outside of the
714     * given month.
715     * 
716     * Mapping logical day grid coordinates to Date.<p>
717     * 
718     * PENDING JW: relax the startOfMonth pre? Why did I require it?
719     * 
720     * @param month a calendar representing the first day of the month, must not
721     *   be null.
722     * @param row the logical row index in the day grid of the month
723     * @param column the logical column index in the day grid of the month
724     * @return the day at the logical grid coordinates in the given month or null
725     *    if the coordinates are day/week header or leading/trailing dates 
726     * @throws IllegalStateException if the month is not the start of the month. 
727     * 
728     * @see #getDayGridPosition(Date)  
729     */
730    protected Date getDayInMonth(Date month, int row, int column) {
731        if ((row == DAY_HEADER_ROW) || (column == WEEK_HEADER_COLUMN)) return null;
732        Calendar calendar = getCalendar(month);
733        int monthField = calendar.get(Calendar.MONTH);
734        if (!CalendarUtils.isStartOfMonth(calendar))
735            throw new IllegalStateException("calendar must be start of month but was: " + month.getTime());
736        CalendarUtils.startOfWeek(calendar);
737        // PENDING JW: correctly mapped now?
738        calendar.add(Calendar.DAY_OF_MONTH, 
739                (row - FIRST_WEEK_ROW) * DAYS_IN_WEEK + (column - FIRST_DAY_COLUMN));
740        if (calendar.get(Calendar.MONTH) == monthField) {
741            return calendar.getTime();
742        } 
743        return null;
744        
745    }
746    
747    /**
748     * Returns the given date's position in the grid of the month it is contained in.
749     * 
750     * @param date the Date to get the logical position for, must not be null.
751     * @return the logical coordinates of the day in the grid of days in a
752     *   month or null if the Date is not visible. 
753     *   
754     *  @see #getDayInMonth(Date, int, int)  
755     */
756    protected Point getDayGridPosition(Date date) {
757        if (!isVisible(date)) return null;
758        Calendar calendar = getCalendar(date);
759        Date startOfDay = CalendarUtils.startOfDay(calendar, date);
760        // there must be a less ugly way?
761        // columns
762        CalendarUtils.startOfWeek(calendar);
763        int column = FIRST_DAY_COLUMN;
764        while (calendar.getTime().before(startOfDay)) {
765            column++;
766            calendar.add(Calendar.DAY_OF_MONTH, 1);
767        }
768        
769        Date startOfWeek = CalendarUtils.startOfWeek(calendar, date);
770        calendar.setTime(date);
771        CalendarUtils.startOfMonth(calendar);
772        int row = FIRST_WEEK_ROW;
773        while (calendar.getTime().before(startOfWeek)) {
774            row++;
775            calendar.add(Calendar.WEEK_OF_YEAR, 1);
776        }
777        return new Point(column, row);
778    }
779    
780
781    /**
782     * Returns the Date at the given location. May be null if the
783     * coordinates don't map to a day in the month which contains the 
784     * coordinates. Specifically: hitting leading/trailing dates returns null.
785     * 
786     * Mapping pixel to calendar day.
787     *
788     * @param x the x position of the location in pixel
789     * @param y the y position of the location in pixel
790     * @return the day at the given location or null if the location
791     *   doesn't map to a day in the month which contains the coordinates.
792     *   
793     * @see #getDayBounds(Date)  
794     */ 
795    @Override
796    public Date getDayAtLocation(int x, int y) {
797        Point dayInGrid = getDayGridPositionAtLocation(x, y);
798        if ((dayInGrid == null) 
799                || (dayInGrid.x == WEEK_HEADER_COLUMN) || (dayInGrid.y == DAY_HEADER_ROW)) return null;
800        Date month = getMonthAtLocation(x, y);
801        return getDayInMonth(month, dayInGrid.y, dayInGrid.x);
802    }
803    
804    /**
805     * Returns the bounds of the given day.
806     * The bounds are in monthView coordinate system.<p>
807     * 
808     * PENDING JW: this most probably should be public as it is the logical
809     * reverse of getDayAtLocation <p>
810     * 
811     * @param date the Date to return the bounds for. Must not be null.
812     * @return the bounds of the given date or null if not visible.
813     * 
814     * @see #getDayAtLocation(int, int)
815     */
816    protected Rectangle getDayBounds(Date date) {
817        if (!isVisible(date)) return null;
818        Point position = getDayGridPosition(date);
819        Rectangle monthBounds = getMonthBounds(date);
820        monthBounds.y += getMonthHeaderHeight() + (position.y - DAY_HEADER_ROW) * fullBoxHeight;
821        if (monthView.isShowingWeekNumber()) {
822            position.x++;
823        }
824        position.x -= FIRST_DAY_COLUMN;
825        if (isLeftToRight) {
826           monthBounds.x += position.x * fullBoxWidth; 
827        } else {
828            int start = monthBounds.x + monthBounds.width - fullBoxWidth; 
829            monthBounds.x = start - position.x * fullBoxWidth;
830        }
831        monthBounds.width = fullBoxWidth;
832        monthBounds.height = fullBoxHeight;
833        return monthBounds;
834    }
835    
836    /**
837     * @param row
838     */
839    private void checkValidRow(int row, int column) {
840        if ((column < WEEK_HEADER_COLUMN) || (column > LAST_DAY_COLUMN)) 
841            throw new IllegalArgumentException("illegal column in day grid " + column);
842        if ((row < DAY_HEADER_ROW) || (row > LAST_WEEK_ROW)) 
843            throw new IllegalArgumentException("illegal row in day grid" + row);
844    }
845
846    /**
847     * Returns a boolean indicating if the given Date is visible. Trailing/leading
848     * dates of the last/first displayed month are considered to be invisible.
849     * 
850     * @param date the Date to check for visibility. Must not be null.
851     * @return true if the date is visible, false otherwise.
852     */
853    private boolean isVisible(Date date) {
854        if (getFirstDisplayedDay().after(date) || getLastDisplayedDay().before(date)) return false;
855        return true;
856    }
857
858    
859    // ------------------- mapping month parts 
860 
861
862    /**
863     * Mapping pixel to bounds.<p>
864     * 
865     * PENDING JW: define the "action grid". Currently this replaces the old
866     * version to remove all internal usage of deprecated methods.
867     *  
868     * @param x the x position of the location in pixel
869     * @param y the y position of the location in pixel
870     * @return the bounds of the active header area in containing the location
871     *   or null if outside.
872     */
873    protected int getTraversableGridPositionAtLocation(int x, int y) {
874        Rectangle headerBounds = getMonthHeaderBoundsAtLocation(x, y);
875        if (headerBounds == null) return -1;
876        if (y < headerBounds.y + arrowPaddingY) return -1;
877        if (y > headerBounds.y + headerBounds.height - arrowPaddingY) return -1;
878        headerBounds.setBounds(headerBounds.x + arrowPaddingX, y, 
879                headerBounds.width - 2 * arrowPaddingX, headerBounds.height);
880        if (!headerBounds.contains(x, y)) return -1;
881        Rectangle hitArea = new Rectangle(headerBounds.x, headerBounds.y, monthUpImage.getIconWidth(), monthUpImage.getIconHeight());
882        if (hitArea.contains(x, y)) {
883            return isLeftToRight ? MONTH_DOWN : MONTH_UP;
884        }
885        hitArea.translate(headerBounds.width - monthUpImage.getIconWidth(), 0);
886        if (hitArea.contains(x, y)) {
887            return isLeftToRight ? MONTH_UP : MONTH_DOWN;
888        } 
889        return -1;
890    }
891    
892    /**
893     * Returns the bounds of the month header which contains the 
894     * given location. The bounds are in monthView coordinate system.
895     * 
896     * <p>
897     * 
898     * @param x the x position of the location in pixel
899     * @param y the y position of the location in pixel
900     * @return the bounds of the month which contains the location, 
901     *   or null if outside
902     */
903    protected Rectangle getMonthHeaderBoundsAtLocation(int x, int y) {
904        Rectangle header = getMonthBoundsAtLocation(x, y);
905        if (header == null) return null;
906        header.height = getMonthHeaderHeight();
907        return header;
908    }
909    
910    /**
911     * Returns the bounds of the month details which contains the 
912     * given location. The bounds are in monthView coordinate system.
913     * 
914     * @param x the x position of the location in pixel
915     * @param y the y position of the location in pixel
916     * @return the bounds of the details grid in the month at
917     *   location or null if outside.
918     */
919    protected Rectangle getMonthDetailsBoundsAtLocation(int x, int y) {
920        Rectangle month = getMonthBoundsAtLocation(x, y);
921        if (month == null) return null;
922        int startOfDaysY = month.y + getMonthHeaderHeight();
923        if (y < startOfDaysY) return null;
924        month.y = startOfDaysY;
925        month.height = month.height - getMonthHeaderHeight();
926        return month;
927    }
928
929    
930    // ---------------------- mapping month coordinates    
931
932    /**
933      * Returns the bounds of the month which contains the 
934     * given location. The bounds are in monthView coordinate system.
935     * 
936     * <p>
937     * 
938     * Mapping pixel to bounds.
939     * 
940     * @param x the x position of the location in pixel
941     * @param y the y position of the location in pixel
942     * @return the bounds of the month which contains the location, 
943     *   or null if outside
944     */
945    protected Rectangle getMonthBoundsAtLocation(int x, int y) {
946        if (!calendarGrid.contains(x, y)) return null;
947        int calendarRow = (y - calendarGrid.y) / fullCalendarHeight;
948        int calendarColumn = (x - calendarGrid.x) / fullCalendarWidth;
949        return new Rectangle( 
950                calendarGrid.x + calendarColumn * fullCalendarWidth,
951                calendarGrid.y + calendarRow * fullCalendarHeight,
952                calendarWidth, calendarHeight);
953    }
954    
955    
956    /**
957     * 
958     * Returns the logical coordinates of the month which contains
959     * the given location. The p.x of the returned value represents the column, the
960     * p.y represents the row the month is shown in. The transformation takes
961     * care of ComponentOrientation. <p>
962     * 
963     * Mapping pixel to logical grid coordinates.
964     * 
965     * @param x the x position of the location in pixel
966     * @param y the y position of the location in pixel
967     * @return the logical coordinates of the month in the grid of month shown by
968     *   this monthView or null if outside. 
969     */
970    protected Point getMonthGridPositionAtLocation(int x, int y) {
971        if (!calendarGrid.contains(x, y)) return null;
972        int calendarRow = (y - calendarGrid.y) / fullCalendarHeight;
973        int calendarColumn = (x - calendarGrid.x) / fullCalendarWidth;
974        if (!isLeftToRight) {
975            int start = calendarGrid.x + calendarGrid.width;
976            calendarColumn = (start - x) / fullCalendarWidth;
977              
978        }
979        return new Point(calendarColumn, calendarRow);
980    }
981
982    /**
983     * Returns the Date representing the start of the month which 
984     * contains the given location.<p>
985     * 
986     * Mapping pixel to calendar day.
987     *
988     * @param x the x position of the location in pixel
989     * @param y the y position of the location in pixel
990     * @return the start of the month which contains the given location or 
991     *    null if the location is outside the grid of months.
992     */
993    protected Date getMonthAtLocation(int x, int y) {
994        Point month = getMonthGridPositionAtLocation(x, y);
995        if (month ==  null) return null;
996        return getMonth(month.y, month.x);
997    }
998    
999    /**
1000     * Returns the Date representing the start of the month at the given 
1001     * logical position in the grid of months. <p>
1002     * 
1003     * Mapping logical grid coordinates to Calendar.
1004     * 
1005     * @param row the rowIndex in the grid of months.
1006     * @param column the columnIndex in the grid months.
1007     * @return a Date representing the start of the month at the given
1008     *   logical coordinates.
1009     *   
1010     * @see #getMonthGridPosition(Date)  
1011     */
1012    protected Date getMonth(int row, int column) {
1013        Calendar calendar = getCalendar();
1014        calendar.add(Calendar.MONTH, 
1015                row * calendarColumnCount + column);
1016        return calendar.getTime();
1017        
1018    }
1019
1020    /**
1021     * Returns the logical grid position of the month containing the given date.
1022     * The Point's x value is the column in the grid of months, the y value
1023     * is the row in the grid of months.
1024     * 
1025     * Mapping Date to logical grid position, this is the reverse of getMonth(int, int).
1026     * 
1027     * @param date the Date to return the bounds for. Must not be null.
1028     * @return the postion of the month that contains the given date or null if not visible.
1029     * 
1030     * @see #getMonth(int, int)
1031     * @see #getMonthBounds(int, int)
1032     */
1033    protected Point getMonthGridPosition(Date date) {
1034        if (!isVisible(date)) return null;
1035        // start of grid
1036        Calendar calendar = getCalendar();
1037        int firstMonth = calendar.get(Calendar.MONTH);
1038        int firstYear = calendar.get(Calendar.YEAR);
1039        
1040        // 
1041        calendar.setTime(date);
1042        int month = calendar.get(Calendar.MONTH);
1043        int year = calendar.get(Calendar.YEAR);
1044        
1045        int diffMonths = month - firstMonth
1046            + ((year - firstYear) * JXMonthView.MONTHS_IN_YEAR);
1047        
1048        int row = diffMonths / calendarColumnCount;
1049        int column = diffMonths % calendarColumnCount;
1050
1051        return new Point(column, row);
1052    }
1053
1054    /**
1055     * Returns the bounds of the month at the given logical coordinates
1056     * in the grid of visible months.<p>
1057     * 
1058     * Mapping logical grip position to pixel.
1059     * 
1060     * @param row the rowIndex in the grid of months.
1061     * @param column the columnIndex in the grid months.
1062     * @return the bounds of the month at the given logical logical position.
1063     * 
1064     * @see #getMonthGridPositionAtLocation(int, int)
1065     * @see #getMonthBoundsAtLocation(int, int)
1066     */
1067    protected Rectangle getMonthBounds(int row, int column) {
1068        int startY = calendarGrid.y + row * fullCalendarHeight;
1069        int startX = calendarGrid.x + column * fullCalendarWidth;
1070        if (!isLeftToRight) {
1071            startX = calendarGrid.x + (calendarColumnCount - 1 - column) * fullCalendarWidth;
1072        }
1073        return new Rectangle(startX, startY, calendarWidth, calendarHeight);
1074    }
1075
1076    /**
1077     * Returns the bounds of the month containing the given date.
1078     * The bounds are in monthView coordinate system.<p>
1079     * 
1080     * Mapping Date to pixel.
1081     * 
1082     * @param date the Date to return the bounds for. Must not be null.
1083     * @return the bounds of the month that contains the given date or null if not visible.
1084     * 
1085     * @see #getMonthAtLocation(int, int)
1086     */
1087    protected Rectangle getMonthBounds(Date date) {
1088        Point position = getMonthGridPosition(date);
1089        return position != null ? getMonthBounds(position.y, position.x) : null;
1090    }
1091    
1092    /**
1093     * Returns the bounds of the month containing the given date.
1094     * The bounds are in monthView coordinate system.<p>
1095     * 
1096     * Mapping Date to pixel.
1097     * 
1098     * @param date the Date to return the bounds for. Must not be null.
1099     * @return the bounds of the month that contains the given date or null if not visible.
1100     * 
1101     * @see #getMonthAtLocation(int, int)
1102     */
1103    protected Rectangle getMonthHeaderBounds(Date date, boolean includeInsets) {
1104        Point position = getMonthGridPosition(date);
1105        if (position == null) return null;
1106        Rectangle bounds = getMonthBounds(position.y, position.x);
1107        bounds.height = getMonthHeaderHeight();
1108        if (!includeInsets) {
1109            
1110        }
1111        return bounds;
1112    }
1113
1114
1115    //---------------- accessors for sizes
1116    
1117    /**
1118     * Returns the size of a month.
1119     * @return the size of a month.
1120     */
1121    protected Dimension getMonthSize() {
1122        return new Dimension(calendarWidth, calendarHeight);
1123    }
1124    
1125    /**
1126     * Returns the size of a day including the padding.
1127     * @return the size of a month.
1128     */
1129    protected Dimension getDaySize() {
1130        return new Dimension(fullBoxWidth, fullBoxHeight);
1131    }
1132    /**
1133     * Returns the height of the month header.
1134     * 
1135     * @return the height of the month header.
1136     */
1137    protected int getMonthHeaderHeight() {
1138        return fullMonthBoxHeight;
1139    }
1140
1141    
1142
1143    //-------------------  layout    
1144    
1145    /**
1146     * Called from layout: calculates properties
1147     * of grid of months.
1148     */
1149    private void calculateMonthGridLayoutProperties() {
1150        calculateMonthGridRowColumnCount();
1151        calculateMonthGridBounds();
1152    }
1153    
1154    /**
1155     * Calculates the bounds of the grid of months. 
1156     * 
1157     * CalendarRow/ColumnCount and calendarWidth/Height must be
1158     * initialized before calling this. 
1159     */
1160    private void calculateMonthGridBounds() {
1161        calendarGrid.setBounds(calculateCalendarGridX(), 
1162                calculateCalendarGridY(), 
1163                calculateCalendarGridWidth(), 
1164                calculateCalendarGridHeight());
1165    }
1166
1167
1168    private int calculateCalendarGridY() {
1169        return (monthView.getHeight() - calculateCalendarGridHeight()) / 2;
1170    }
1171
1172    private int calculateCalendarGridX() {
1173        return (monthView.getWidth() - calculateCalendarGridWidth()) / 2; 
1174    }
1175    
1176    private int calculateCalendarGridHeight() {
1177        return ((calendarHeight * calendarRowCount) +
1178                (CALENDAR_SPACING * (calendarRowCount - 1 )));
1179    }
1180
1181    private int calculateCalendarGridWidth() {
1182        return ((calendarWidth * calendarColumnCount) +
1183                (CALENDAR_SPACING * (calendarColumnCount - 1)));
1184    }
1185
1186    /**
1187     * Calculates and updates the numCalCols/numCalRows that determine the
1188     * number of calendars that can be displayed. Updates the last displayed
1189     * date if appropriate.
1190     * 
1191     */
1192    private void calculateMonthGridRowColumnCount() {
1193        int oldNumCalCols = calendarColumnCount;
1194        int oldNumCalRows = calendarRowCount;
1195
1196        calendarRowCount = 1;
1197        calendarColumnCount = 1;
1198        if (!isZoomable()) {
1199            // Determine how many columns of calendars we want to paint.
1200            int addColumns = (monthView.getWidth() - calendarWidth)
1201                    / (calendarWidth + CALENDAR_SPACING);
1202            // happens if used as renderer in a tree.. don't know yet why
1203            if (addColumns > 0) {
1204                calendarColumnCount += addColumns;
1205            }
1206
1207            // Determine how many rows of calendars we want to paint.
1208            int addRows = (monthView.getHeight() - calendarHeight)
1209                    / (calendarHeight + CALENDAR_SPACING);
1210            if (addRows > 0) {
1211                calendarRowCount += addRows;
1212            }
1213        }
1214        if (oldNumCalCols != calendarColumnCount
1215                || oldNumCalRows != calendarRowCount) {
1216            updateLastDisplayedDay(getFirstDisplayedDay());
1217        }
1218    }
1219
1220    /**
1221     * @return true if the month view can be zoomed, false otherwise
1222     */
1223    protected boolean isZoomable() {
1224        return monthView.isZoomable();
1225    }
1226
1227
1228    
1229
1230//-------------------- painting
1231
1232    
1233    /**
1234     * Overridden to extract the background painting for ease-of-use of 
1235     * subclasses.
1236     */
1237    @Override
1238    public void update(Graphics g, JComponent c) {
1239        paintBackground(g);
1240        paint(g, c);
1241    }
1242
1243    /**
1244     * Paints the background of the component. This implementation fill the
1245     * monthView's area with its background color if opaque, does nothing
1246     * if not opaque. Subclasses can override but must comply to opaqueness
1247     * contract.
1248     * 
1249     * @param g the Graphics to fill.
1250     * 
1251     */
1252    protected void paintBackground(Graphics g) {
1253        if (monthView.isOpaque()) {
1254            g.setColor(monthView.getBackground());
1255            g.fillRect(0, 0, monthView.getWidth(), monthView.getHeight());
1256        }
1257    }
1258
1259    /**
1260     * {@inheritDoc}
1261     */
1262    @Override
1263    public void paint(Graphics g, JComponent c) {
1264        Rectangle clip = g.getClipBounds();
1265        // Get a calender set to the first displayed date
1266        Calendar cal = getCalendar();
1267        // loop through grid of months
1268        for (int row = 0; row < calendarRowCount; row++) {
1269            for (int column = 0; column < calendarColumnCount; column++) {
1270                // get the bounds of the current month.
1271                Rectangle bounds = getMonthBounds(row, column);
1272                // Check if this row falls in the clip region.
1273                if (bounds.intersects(clip)) {
1274                    paintMonth(g, cal);
1275                }
1276                cal.add(Calendar.MONTH, 1);
1277            }
1278        }
1279
1280    }
1281
1282    /**
1283     * Paints the month represented by the given Calendar.
1284     * 
1285     * Note: the given calendar must not be changed.
1286     * @param g the graphics to paint into
1287     * @param month the calendar specifying the first day of the month to
1288     *        paint, must not be null
1289     */
1290    protected void paintMonth(Graphics g, Calendar month) {
1291        paintMonthHeader(g, month);
1292        paintDayHeader(g, month);
1293        paintWeekHeader(g, month);
1294        paintDays(g, month);
1295    }
1296
1297    /**
1298     * Paints the header of a month.
1299     * 
1300     * Note: the given calendar must not be changed.
1301     * @param g the graphics to paint into
1302     * @param month the calendar specifying the first day of the month to
1303     *        paint, must not be null
1304     */
1305    protected void paintMonthHeader(Graphics g, Calendar month) {
1306        Rectangle page = getMonthHeaderBounds(month.getTime(), false);
1307        paintDayOfMonth(g, page, month, CalendarState.TITLE);
1308    }
1309
1310    /**
1311     * Paints the day column header.
1312     * 
1313     * Note: the given calendar must not be changed.
1314     * @param g the graphics to paint into
1315     * @param month the calendar specifying the first day of the month to
1316     *        paint, must not be null
1317     */
1318    protected void paintDayHeader(Graphics g, Calendar month) {
1319        paintDaysOfWeekSeparator(g, month);
1320        Calendar cal = (Calendar) month.clone();
1321        CalendarUtils.startOfWeek(cal);
1322        for (int i = FIRST_DAY_COLUMN; i <= LAST_DAY_COLUMN; i++) {
1323            Rectangle dayBox = getDayBoundsInMonth(month.getTime(), DAY_HEADER_ROW, i);
1324            paintDayOfMonth(g, dayBox, cal, CalendarState.DAY_OF_WEEK);
1325            cal.add(Calendar.DATE, 1);
1326        }
1327    }
1328
1329    /**
1330     * Paints the day column header.
1331     * 
1332     * Note: the given calendar must not be changed.
1333     * @param g the graphics to paint into
1334     * @param month the calendar specifying the first day of the month to
1335     *        paint, must not be null
1336     */
1337    protected void paintWeekHeader(Graphics g, Calendar month) {
1338        if (!monthView.isShowingWeekNumber())
1339            return;
1340        paintWeekOfYearSeparator(g, month);
1341    
1342        int weeks = getWeeks(month);
1343        // the calendar passed to the renderers
1344        Calendar weekCalendar = (Calendar) month.clone();
1345        // we loop by logical row (== week in month) coordinates 
1346        for (int week = FIRST_WEEK_ROW; week < FIRST_WEEK_ROW + weeks; week++) {
1347            // get the day bounds based on logical row/column coordinates
1348            Rectangle dayBox = getDayBoundsInMonth(month.getTime(), week, WEEK_HEADER_COLUMN);
1349            // NOTE: this can be set to any day in the week to render the weeknumber of
1350            // categorized by CalendarState
1351            paintDayOfMonth(g, dayBox, weekCalendar, CalendarState.WEEK_OF_YEAR);
1352            weekCalendar.add(Calendar.WEEK_OF_YEAR, 1);
1353        }
1354    }
1355
1356    /**
1357     * Paints the days of the given month.
1358     * 
1359     * Note: the given calendar must not be changed.
1360     * @param g the graphics to paint into
1361     * @param month the calendar specifying the first day of the month to
1362     *        paint, must not be null
1363     */
1364    protected void paintDays(Graphics g, Calendar month) {
1365        Calendar clonedCal = (Calendar) month.clone();
1366        CalendarUtils.startOfMonth(clonedCal);
1367        Date startOfMonth = clonedCal.getTime();
1368        CalendarUtils.endOfMonth(clonedCal);
1369        Date endOfMonth = clonedCal.getTime();
1370        // reset the clone
1371        clonedCal.setTime(month.getTime());
1372        // adjust to start of week
1373        clonedCal.setTime(month.getTime());
1374        CalendarUtils.startOfWeek(clonedCal);
1375        for (int week = FIRST_WEEK_ROW; week <= LAST_WEEK_ROW; week++) {
1376            for (int day = FIRST_DAY_COLUMN; day <= LAST_DAY_COLUMN; day++) {
1377                CalendarState state = null;
1378                if (clonedCal.getTime().before(startOfMonth)) {
1379                    if (monthView.isShowingLeadingDays()) {
1380                        state = CalendarState.LEADING;
1381                    }
1382                } else if (clonedCal.getTime().after(endOfMonth)) {
1383                    if (monthView.isShowingTrailingDays()) {
1384                        state = CalendarState.TRAILING;
1385                    }
1386
1387                } else {
1388                    state = isToday(clonedCal.getTime()) ? CalendarState.TODAY : CalendarState.IN_MONTH;
1389                }
1390                if (state != null) {
1391                    Rectangle bounds = getDayBoundsInMonth(startOfMonth, week, day);
1392                    paintDayOfMonth(g, bounds, clonedCal, state);
1393                }
1394                clonedCal.add(Calendar.DAY_OF_MONTH, 1);
1395            }
1396        }
1397    }
1398
1399
1400    /**
1401     * Paints a day which is of the current month with the given state.<p>
1402     * 
1403     * PENDING JW: mis-nomer - this is in fact called for rendering any day-related
1404     * state (including weekOfYear, dayOfWeek headers) and for rendering
1405     * the month header as well, that is from everywhere.
1406     *  Rename to paintSomethingGeneral. Think about impact for subclasses 
1407     *  (what do they really need? feedback please!)
1408     * 
1409     * @param g the graphics to paint into.
1410     * @param bounds the rectangle to paint the day into
1411     * @param calendar the calendar representing the day to paint
1412     * @param state the calendar state
1413     */
1414    protected void paintDayOfMonth(Graphics g, Rectangle bounds, Calendar calendar, CalendarState state) {
1415        JComponent comp = getRenderingHandler().prepareRenderingComponent(monthView, calendar, 
1416                state);
1417        rendererPane.paintComponent(g, comp, monthView, bounds.x, bounds.y,
1418                bounds.width, bounds.height, true);
1419    }
1420
1421    /**
1422     * Paints the separator between row header (weeks of year) and days.
1423     * 
1424     * Note: the given calendar must not be changed.
1425     * @param g the graphics to paint into
1426     * @param month the calendar specifying the first day of the month to
1427     *        paint, must not be null
1428     */
1429    protected void paintWeekOfYearSeparator(Graphics g, Calendar month) {
1430        Rectangle r = getSeparatorBounds(month, FIRST_WEEK_ROW, WEEK_HEADER_COLUMN);
1431        if (r == null) return;
1432        g.setColor(monthView.getForeground());
1433        g.drawLine(r.x, r.y, r.x, r.y + r.height);
1434    }
1435
1436    /**
1437     * Paints the separator between column header (days of week) and days.
1438     * 
1439     * Note: the given calendar must not be changed.
1440     * @param g the graphics to paint into
1441     * @param month the calendar specifying the the first day of the month to
1442     *        paint, must not be null
1443     */
1444    protected void paintDaysOfWeekSeparator(Graphics g, Calendar month) {
1445        Rectangle r = getSeparatorBounds(month, DAY_HEADER_ROW, FIRST_DAY_COLUMN);
1446        if (r == null) return;
1447        g.setColor(monthView.getForeground());
1448        g.drawLine(r.x, r.y, r.x + r.width, r.y);
1449    }
1450    
1451    /**
1452     * @param month
1453     * @param row
1454     * @param column
1455     * @return
1456     */
1457    private Rectangle getSeparatorBounds(Calendar month, int row, int column) {
1458        Rectangle separator = getDayBoundsInMonth(month.getTime(), row, column);
1459        if (separator == null) return null;
1460        if (column == WEEK_HEADER_COLUMN) {
1461            separator.height *= WEEKS_IN_MONTH;
1462            if (isLeftToRight) {
1463                separator.x += separator.width - 1;
1464            }
1465            separator.width = 1;
1466        } else if (row == DAY_HEADER_ROW) {
1467            int oldWidth = separator.width;
1468            separator.width *= DAYS_IN_WEEK;
1469            if (!isLeftToRight) {
1470                separator.x -= separator.width - oldWidth;
1471            }
1472            separator.y += separator.height - 1;
1473            separator.height = 1;
1474        }
1475        return separator;
1476    }
1477
1478    /**
1479     * Returns the number of weeks to paint in the current month, as represented
1480     * by the given calendar. The calendar is expected to be set to the first
1481     * of the month. 
1482     * 
1483     * Note: the given calendar must not be changed.
1484     * 
1485     * @param month the calendar specifying the the first day of the month to
1486     *        paint, must not be null
1487     * @return the number of weeks of this month.
1488     */
1489    protected int getWeeks(Calendar month) {
1490        Calendar cloned = (Calendar) month.clone();
1491        // the calendar is set to the first of month, get date for last
1492        CalendarUtils.endOfMonth(cloned);
1493        // marker for end
1494        Date last = cloned.getTime();
1495        // start again
1496        cloned.setTime(month.getTime());
1497        CalendarUtils.startOfWeek(cloned);
1498        int weeks = 0;
1499        while (last.after(cloned.getTime())) {
1500            weeks++;
1501            cloned.add(Calendar.WEEK_OF_MONTH, 1);
1502        }
1503        return weeks;
1504    }
1505
1506
1507
1508    private void traverseMonth(int arrowType) {
1509        if (arrowType == MONTH_DOWN) {
1510            previousMonth();
1511        } else if (arrowType == MONTH_UP) {
1512            nextMonth();
1513        }
1514    }
1515
1516    private void nextMonth() {
1517        Date upperBound = monthView.getUpperBound();
1518        if (upperBound == null
1519                || upperBound.after(getLastDisplayedDay()) ){
1520            Calendar cal = getCalendar();
1521            cal.add(Calendar.MONTH, 1);
1522            monthView.setFirstDisplayedDay(cal.getTime());
1523        }
1524    }
1525
1526    private void previousMonth() {
1527        Date lowerBound = monthView.getLowerBound();
1528        if (lowerBound == null
1529                || lowerBound.before(getFirstDisplayedDay())){
1530            Calendar cal = getCalendar();
1531            cal.add(Calendar.MONTH, -1);
1532            monthView.setFirstDisplayedDay(cal.getTime());
1533        }
1534    }
1535
1536//--------------------------- displayed dates, calendar
1537
1538    
1539    /**
1540     * Returns the monthViews calendar configured to the firstDisplayedDate.
1541     * 
1542     * NOTE: it's safe to change the calendar state without resetting because
1543     * it's JXMonthView's responsibility to protect itself.
1544     * 
1545     * @return the monthView's calendar, configured with the firstDisplayedDate.
1546     */
1547    protected Calendar getCalendar() {
1548        return getCalendar(getFirstDisplayedDay());
1549    }
1550    
1551    /**
1552     * Returns the monthViews calendar configured to the given time.
1553     * 
1554     * NOTE: it's safe to change the calendar state without resetting because
1555     * it's JXMonthView's responsibility to protect itself.
1556     * 
1557     * @param date the date to configure the calendar with
1558     * @return the monthView's calendar, configured with the given date.
1559     */
1560    protected Calendar getCalendar(Date date) {
1561        Calendar calendar = monthView.getCalendar();
1562        calendar.setTime(date);
1563        return calendar;
1564    }
1565
1566    
1567
1568    /**
1569     * Updates the lastDisplayedDate property based on the given first and 
1570     * visible # of months.
1571     * 
1572     * @param first the date of the first visible day.
1573     */
1574    private void updateLastDisplayedDay(Date first) {
1575        Calendar cal = getCalendar(first);
1576        cal.add(Calendar.MONTH, ((calendarColumnCount * calendarRowCount) - 1));
1577        CalendarUtils.endOfMonth(cal);
1578        lastDisplayedDate = cal.getTime();
1579    }
1580
1581
1582    /**
1583     * {@inheritDoc}
1584     */
1585    @Override
1586    public Date getLastDisplayedDay() {
1587        return lastDisplayedDate;
1588    }
1589
1590    /*-------------- refactored: encapsulate aliased fields
1591     */
1592
1593    /**
1594     * Updates internal state that depends on the MonthView's firstDisplayedDay
1595     * property. <p>
1596     * 
1597     * Here: updates lastDisplayedDay.
1598     * <p>
1599     * 
1600     * 
1601     * @param firstDisplayedDay the firstDisplayedDate to set
1602     */
1603    protected void setFirstDisplayedDay(Date firstDisplayedDay) {
1604        updateLastDisplayedDay(firstDisplayedDay);
1605    }
1606    
1607    /**
1608     * Returns the first displayed day. Convenience delegate to 
1609     * 
1610     * @return the firstDisplayed
1611     */
1612    protected Date getFirstDisplayedDay() {
1613        return monthView.getFirstDisplayedDay();
1614    }
1615
1616    /**
1617     * @return the firstDisplayedMonth
1618     */
1619    protected int getFirstDisplayedMonth() {
1620        return getCalendar().get(Calendar.MONTH);
1621    }
1622
1623
1624    /**
1625     * @return the firstDisplayedYear
1626     */
1627    protected int getFirstDisplayedYear() {
1628        return getCalendar().get(Calendar.YEAR);
1629    }
1630
1631
1632    /**
1633     * @return the selection
1634     */
1635    protected SortedSet<Date> getSelection() {
1636        return monthView.getSelection();
1637    }
1638    
1639    
1640    /**
1641     * @return the start of today.
1642     */
1643    protected Date getToday() {
1644        return monthView.getToday();
1645    }
1646
1647    /**
1648     * Returns true if the date passed in is the same as today.
1649     *
1650     * PENDING JW: really want the exact test?
1651     * 
1652     * @param date long representing the date you want to compare to today.
1653     * @return true if the date passed is the same as today.
1654     */
1655    protected boolean isToday(Date date) {
1656        return date.equals(getToday());
1657    }
1658    
1659
1660//-----------------------end encapsulation
1661 
1662    
1663//------------------ Handler implementation 
1664//  
1665    /**
1666     * temporary: removed SelectionMode.NO_SELECTION, replaced
1667     * all access by this method to enable easy re-adding, if we want it.
1668     * If not - remove.
1669     */
1670    private boolean canSelectByMode() {
1671        return true;
1672    }
1673    
1674
1675    private class Handler implements  
1676        MouseListener, MouseMotionListener, LayoutManager,
1677            PropertyChangeListener, DateSelectionListener {
1678        private boolean armed;
1679        private Date startDate;
1680        private Date endDate;
1681
1682        @Override
1683        public void mouseClicked(MouseEvent e) {}
1684
1685        @Override
1686        public void mousePressed(MouseEvent e) {
1687            // If we were using the keyboard we aren't anymore.
1688            setUsingKeyboard(false);
1689
1690            if (!monthView.isEnabled()) {
1691                return;
1692            }
1693
1694            if (!monthView.hasFocus() && monthView.isFocusable()) {
1695                monthView.requestFocusInWindow();
1696            }
1697
1698            // Check if one of the month traverse buttons was pushed.
1699            if (monthView.isTraversable()) {
1700                int arrowType = getTraversableGridPositionAtLocation(e.getX(), e.getY());
1701                if (arrowType != -1) {
1702                    traverseMonth(arrowType);
1703                    return;
1704                }
1705            }
1706
1707            if (!canSelectByMode()) {
1708                return;
1709            }
1710
1711            Date cal = getDayAtLocation(e.getX(), e.getY());
1712            if (cal == null) {
1713                return;
1714            }
1715
1716            // Update the selected dates.
1717            startDate = cal;
1718            endDate = cal;
1719
1720            if (monthView.getSelectionMode() == SelectionMode.SINGLE_INTERVAL_SELECTION ||
1721//                    selectionMode == SelectionMode.WEEK_INTERVAL_SELECTION ||
1722                    monthView.getSelectionMode() == SelectionMode.MULTIPLE_INTERVAL_SELECTION) {
1723                pivotDate = startDate;
1724            }
1725
1726            monthView.getSelectionModel().setAdjusting(true);
1727            
1728            if (monthView.getSelectionMode() == SelectionMode.MULTIPLE_INTERVAL_SELECTION && e.isControlDown()) {
1729                monthView.addSelectionInterval(startDate, endDate);
1730            } else {
1731                monthView.setSelectionInterval(startDate, endDate);
1732            }
1733
1734            // Arm so we fire action performed on mouse release.
1735            armed = true;
1736        }
1737
1738        
1739        @Override
1740        public void mouseReleased(MouseEvent e) {
1741            // If we were using the keyboard we aren't anymore.
1742            setUsingKeyboard(false);
1743
1744            if (!monthView.isEnabled()) {
1745                return;
1746            }
1747
1748            if (!monthView.hasFocus() && monthView.isFocusable()) {
1749                monthView.requestFocusInWindow();
1750            }
1751            
1752            if (armed) {
1753                monthView.commitSelection();
1754            }
1755            armed = false;
1756        }
1757
1758        @Override
1759        public void mouseEntered(MouseEvent e) {}
1760
1761        @Override
1762        public void mouseExited(MouseEvent e) {}
1763
1764        @Override
1765        public void mouseDragged(MouseEvent e) {
1766            // If we were using the keyboard we aren't anymore.
1767            setUsingKeyboard(false);
1768            if (!monthView.isEnabled() || !canSelectByMode()) {
1769                return;
1770            }
1771            Date cal = getDayAtLocation(e.getX(), e.getY());
1772            if (cal == null) {
1773                return;
1774            }
1775
1776            Date selected = cal;
1777            Date oldStart = startDate;
1778            Date oldEnd = endDate;
1779
1780            if (monthView.getSelectionMode() == SelectionMode.SINGLE_SELECTION) {
1781                if (selected.equals(oldStart)) {
1782                    return;
1783                }
1784                startDate = selected;
1785                endDate = selected;
1786            } else  if (pivotDate != null){
1787                if (selected.before(pivotDate)) {
1788                    startDate = selected;
1789                    endDate = pivotDate;
1790                } else if (selected.after(pivotDate)) {
1791                    startDate = pivotDate;
1792                    endDate = selected;
1793                }
1794            } else { // pivotDate had not yet been initialiased
1795                // might happen on first click into leading/trailing dates
1796                // JW: fix of #996-swingx: NPE when dragging 
1797                startDate = selected;
1798                endDate = selected;
1799                pivotDate = selected;
1800            }
1801
1802            if (startDate.equals(oldStart) && endDate.equals(oldEnd)) {
1803                return;
1804            }
1805
1806            if (monthView.getSelectionMode() == SelectionMode.MULTIPLE_INTERVAL_SELECTION && e.isControlDown()) {
1807                monthView.addSelectionInterval(startDate, endDate);
1808            } else {
1809                monthView.setSelectionInterval(startDate, endDate);
1810            }
1811
1812            // Set trigger.
1813            armed = true;
1814        }
1815
1816        @Override
1817        public void mouseMoved(MouseEvent e) {}
1818
1819//------------------------ layout
1820        
1821        
1822        private Dimension preferredSize = new Dimension();
1823
1824        @Override
1825        public void addLayoutComponent(String name, Component comp) {}
1826
1827        @Override
1828        public void removeLayoutComponent(Component comp) {}
1829
1830        @Override
1831        public Dimension preferredLayoutSize(Container parent) {
1832            layoutContainer(parent);
1833            return new Dimension(preferredSize);
1834        }
1835
1836        @Override
1837        public Dimension minimumLayoutSize(Container parent) {
1838            return preferredLayoutSize(parent);
1839        }
1840
1841        @Override
1842        public void layoutContainer(Container parent) {
1843
1844            int maxMonthWidth = 0;
1845            int maxMonthHeight = 0;
1846            Calendar calendar = getCalendar();
1847            for (int i = calendar.getMinimum(Calendar.MONTH); i <= calendar.getMaximum(Calendar.MONTH); i++) {
1848                calendar.set(Calendar.MONTH, i);
1849                CalendarUtils.startOfMonth(calendar);
1850                JComponent comp = getRenderingHandler().prepareRenderingComponent(monthView, calendar, CalendarState.TITLE);
1851                Dimension pref = comp.getPreferredSize();
1852                maxMonthWidth = Math.max(maxMonthWidth, pref.width);
1853                maxMonthHeight = Math.max(maxMonthHeight, pref.height);
1854            }
1855            
1856            int maxBoxWidth = 0;
1857            int maxBoxHeight = 0;
1858            calendar = getCalendar();
1859            CalendarUtils.startOfWeek(calendar);
1860            for (int i = 0; i < JXMonthView.DAYS_IN_WEEK; i++) {
1861                JComponent comp = getRenderingHandler().prepareRenderingComponent(monthView, calendar, CalendarState.DAY_OF_WEEK);
1862                Dimension pref = comp.getPreferredSize();
1863                maxBoxWidth = Math.max(maxBoxWidth, pref.width);
1864                maxBoxHeight = Math.max(maxBoxHeight, pref.height);
1865                calendar.add(Calendar.DATE, 1);
1866            }
1867            
1868            calendar = getCalendar();
1869            for (int i = 0; i < calendar.getMaximum(Calendar.DAY_OF_MONTH); i++) {
1870                JComponent comp = getRenderingHandler().prepareRenderingComponent(monthView, calendar, CalendarState.IN_MONTH);
1871                Dimension pref = comp.getPreferredSize();
1872                maxBoxWidth = Math.max(maxBoxWidth, pref.width);
1873                maxBoxHeight = Math.max(maxBoxHeight, pref.height);
1874                calendar.add(Calendar.DATE, 1);
1875            }
1876            
1877            int dayColumns = JXMonthView.DAYS_IN_WEEK;
1878            if (monthView.isShowingWeekNumber()) {
1879                dayColumns++;
1880            }
1881            
1882            if (maxMonthWidth > maxBoxWidth * dayColumns) {
1883                //  monthHeader pref > sum of box widths
1884                // handle here: increase day box width accordingly
1885                double diff = maxMonthWidth - (maxBoxWidth * dayColumns);
1886                maxBoxWidth += Math.ceil(diff/(double) dayColumns);
1887                
1888            }
1889            
1890            fullBoxWidth = maxBoxWidth;
1891            fullBoxHeight = maxBoxHeight;
1892            // PENDING JW: huuh? what we doing here?
1893            int boxHeight = maxBoxHeight - 2 * monthView.getBoxPaddingY();
1894            fullMonthBoxHeight = Math.max(boxHeight, maxMonthHeight) ; 
1895
1896            // Keep track of calendar width and height for use later.
1897            calendarWidth = fullBoxWidth * JXMonthView.DAYS_IN_WEEK;
1898            if (monthView.isShowingWeekNumber()) {
1899                calendarWidth += fullBoxWidth;
1900            }
1901            fullCalendarWidth = calendarWidth + CALENDAR_SPACING;
1902            
1903            calendarHeight = (fullBoxHeight * 7) + fullMonthBoxHeight;
1904            fullCalendarHeight = calendarHeight + CALENDAR_SPACING;
1905            // Calculate minimum width/height for the component.
1906            int prefRows = getPreferredRows();
1907            preferredSize.height = (calendarHeight * prefRows) +
1908                    (CALENDAR_SPACING * (prefRows - 1));
1909
1910            int prefCols = getPreferredColumns();
1911            preferredSize.width = (calendarWidth * prefCols) +
1912                    (CALENDAR_SPACING * (prefCols - 1));
1913
1914            // Add insets to the dimensions.
1915            Insets insets = monthView.getInsets();
1916            preferredSize.width += insets.left + insets.right;
1917            preferredSize.height += insets.top + insets.bottom;
1918           
1919            calculateMonthGridLayoutProperties();
1920            
1921            if (isZoomable()) {
1922                getCalendarHeaderHandler().getHeaderComponent().setBounds(getMonthHeaderBounds(monthView.getFirstDisplayedDay(), false));
1923            }
1924        }
1925
1926        /**
1927         * @return
1928         */
1929        private int getPreferredColumns() {
1930            return isZoomable() ? 1 : monthView.getPreferredColumnCount();
1931        }
1932
1933        /**
1934         * @return
1935         */
1936        private int getPreferredRows() {
1937            return isZoomable() ? 1 : monthView.getPreferredRowCount();
1938        }
1939
1940
1941
1942        @Override
1943        public void propertyChange(PropertyChangeEvent evt) {
1944            String property = evt.getPropertyName();
1945
1946            if ("componentOrientation".equals(property)) {
1947                isLeftToRight = monthView.getComponentOrientation().isLeftToRight();
1948                monthView.revalidate();
1949                monthView.repaint();
1950            } else if (JXMonthView.SELECTION_MODEL.equals(property)) {
1951                DateSelectionModel selectionModel = (DateSelectionModel) evt.getOldValue();
1952                selectionModel.removeDateSelectionListener(getHandler());
1953                selectionModel = (DateSelectionModel) evt.getNewValue();
1954                selectionModel.addDateSelectionListener(getHandler());
1955            } else if ("firstDisplayedDay".equals(property)) {
1956                setFirstDisplayedDay(((Date) evt.getNewValue()));
1957                monthView.repaint();
1958            } else if (JXMonthView.BOX_PADDING_X.equals(property) 
1959                    || JXMonthView.BOX_PADDING_Y.equals(property) 
1960                    || JXMonthView.TRAVERSABLE.equals(property) 
1961                    || JXMonthView.DAYS_OF_THE_WEEK.equals(property) 
1962                    || "border".equals(property) 
1963                    || "showingWeekNumber".equals(property)
1964                    || "traversable".equals(property) 
1965                   
1966                    ) {
1967                monthView.revalidate();
1968                monthView.repaint();
1969            } else if ("zoomable".equals(property)) {
1970                updateZoomable();
1971//            } else if ("font".equals(property)) {
1972//                calendarHeaderHandler.getHeaderComponent().setFont(getAsNotUIResource(createDerivedFont()));
1973//                monthView.revalidate();
1974            } else if ("componentInputMapEnabled".equals(property)) {
1975                updateComponentInputMap();
1976            } else if ("locale".equals(property)) { // "locale" is bound property
1977                updateLocale(true);
1978            } else {
1979                monthView.repaint();
1980//                LOG.info("got propertyChange:" + property);
1981            }
1982        }
1983
1984        @Override
1985        public void valueChanged(DateSelectionEvent ev) {
1986            monthView.repaint();
1987        }
1988
1989
1990    }
1991
1992    /**
1993     * Class that supports keyboard traversal of the JXMonthView component.
1994     */
1995    private class KeyboardAction extends AbstractAction {
1996        public static final int ACCEPT_SELECTION = 0;
1997        public static final int CANCEL_SELECTION = 1;
1998        public static final int SELECT_PREVIOUS_DAY = 2;
1999        public static final int SELECT_NEXT_DAY = 3;
2000        public static final int SELECT_DAY_PREVIOUS_WEEK = 4;
2001        public static final int SELECT_DAY_NEXT_WEEK = 5;
2002        public static final int ADJUST_SELECTION_PREVIOUS_DAY = 6;
2003        public static final int ADJUST_SELECTION_NEXT_DAY = 7;
2004        public static final int ADJUST_SELECTION_PREVIOUS_WEEK = 8;
2005        public static final int ADJUST_SELECTION_NEXT_WEEK = 9;
2006
2007        private int action;
2008
2009        public KeyboardAction(int action) {
2010            this.action = action;
2011        }
2012
2013        @Override
2014        public void actionPerformed(ActionEvent ev) {
2015            if (!canSelectByMode())
2016                return;
2017            if (!isUsingKeyboard()) {
2018                originalDateSpan = getSelection();
2019            }
2020            // JW: removed the isUsingKeyboard from the condition
2021            // need to fire always.
2022            if (action >= ACCEPT_SELECTION && action <= CANCEL_SELECTION) { 
2023                // refactor the logic ...
2024                if (action == CANCEL_SELECTION) {
2025                    // Restore the original selection.
2026                    if ((originalDateSpan != null)
2027                            && !originalDateSpan.isEmpty()) {
2028                        monthView.setSelectionInterval(
2029                                originalDateSpan.first(), originalDateSpan
2030                                        .last());
2031                    } else {
2032                        monthView.clearSelection();
2033                    }
2034                    monthView.cancelSelection();
2035                } else {
2036                    // Accept the keyboard selection.
2037                    monthView.commitSelection();
2038                }
2039                setUsingKeyboard(false);
2040            } else if (action >= SELECT_PREVIOUS_DAY
2041                    && action <= SELECT_DAY_NEXT_WEEK) {
2042                setUsingKeyboard(true);
2043                monthView.getSelectionModel().setAdjusting(true);
2044                pivotDate = null;
2045                traverse(action);
2046            } else if (isIntervalMode()
2047                    && action >= ADJUST_SELECTION_PREVIOUS_DAY
2048                    && action <= ADJUST_SELECTION_NEXT_WEEK) {
2049                setUsingKeyboard(true);
2050                monthView.getSelectionModel().setAdjusting(true);
2051                addToSelection(action);
2052            }
2053        }
2054
2055
2056        /**
2057         * @return
2058         */
2059        private boolean isIntervalMode() {
2060            return !(monthView.getSelectionMode() == SelectionMode.SINGLE_SELECTION);
2061        }
2062
2063        private void traverse(int action) {
2064            Date oldStart = monthView.isSelectionEmpty() ? 
2065                    monthView.getToday() : monthView.getFirstSelectionDate();
2066            Calendar cal = getCalendar(oldStart);
2067            switch (action) {
2068                case SELECT_PREVIOUS_DAY:
2069                    cal.add(Calendar.DAY_OF_MONTH, -1);
2070                    break;
2071                case SELECT_NEXT_DAY:
2072                    cal.add(Calendar.DAY_OF_MONTH, 1);
2073                    break;
2074                case SELECT_DAY_PREVIOUS_WEEK:
2075                    cal.add(Calendar.DAY_OF_MONTH, -JXMonthView.DAYS_IN_WEEK);
2076                    break;
2077                case SELECT_DAY_NEXT_WEEK:
2078                    cal.add(Calendar.DAY_OF_MONTH, JXMonthView.DAYS_IN_WEEK);
2079                    break;
2080            }
2081
2082            Date newStartDate = cal.getTime();
2083            if (!newStartDate.equals(oldStart)) {
2084                monthView.setSelectionInterval(newStartDate, newStartDate);
2085                monthView.ensureDateVisible(newStartDate);
2086            }
2087        }
2088
2089        /**
2090         * If we are in a mode that allows for range selection this method
2091         * will extend the currently selected range.
2092         *
2093         * NOTE: This may not be the expected behavior for the keyboard controls
2094         * and we ay need to update this code to act in a way that people expect.
2095         *
2096         * @param action action for adjusting selection
2097         */
2098        private void addToSelection(int action) {
2099            Date newStartDate;
2100            Date newEndDate;
2101            Date selectionStart;
2102            Date selectionEnd;
2103            if (!monthView.isSelectionEmpty()) {
2104                newStartDate = selectionStart = monthView.getFirstSelectionDate();
2105                newEndDate = selectionEnd = monthView.getLastSelectionDate();
2106            } else {
2107                newStartDate = selectionStart = monthView.getToday();
2108                newEndDate = selectionEnd = newStartDate;
2109            }
2110
2111            if (pivotDate == null) {
2112                pivotDate = newStartDate;
2113            }
2114
2115            // want a copy to play with - each branch sets and reads the time
2116            // actually don't care about the pre-set time.
2117            Calendar cal = getCalendar();
2118            boolean isStartMoved;
2119            switch (action) {
2120            case ADJUST_SELECTION_PREVIOUS_DAY:
2121                if (newEndDate.after(pivotDate)) {
2122                    newEndDate = previousDay(cal, newEndDate);
2123                    isStartMoved = false;
2124                } else {
2125                    newStartDate = previousDay(cal, newStartDate);
2126                    newEndDate = pivotDate;
2127                    isStartMoved = true;
2128                }
2129                break;
2130            case ADJUST_SELECTION_NEXT_DAY:
2131                if (newStartDate.before(pivotDate)) {
2132                    newStartDate = nextDay(cal, newStartDate);
2133                    isStartMoved = true;
2134                } else {
2135                    newEndDate = nextDay(cal, newEndDate);
2136                    isStartMoved = false;
2137                    newStartDate = pivotDate;
2138                }
2139                break;
2140            case ADJUST_SELECTION_PREVIOUS_WEEK:
2141                if (newEndDate.after(pivotDate)) {
2142                    Date newTime = previousWeek(cal, newEndDate);
2143                    if (newTime.after(pivotDate)) {
2144                        newEndDate = newTime;
2145                        isStartMoved = false;
2146                    } else {
2147                        newStartDate = newTime;
2148                        newEndDate = pivotDate;
2149                        isStartMoved = true;
2150                    }
2151                } else {
2152                    newStartDate = previousWeek(cal, newStartDate);
2153                    isStartMoved = true;
2154                }
2155                break;
2156            case ADJUST_SELECTION_NEXT_WEEK:
2157                if (newStartDate.before(pivotDate)) {
2158                    Date newTime = nextWeek(cal, newStartDate);
2159                    if (newTime.before(pivotDate)) {
2160                        newStartDate = newTime;
2161                        isStartMoved = true;
2162                    } else {
2163                        newStartDate = pivotDate;
2164                        newEndDate = newTime;
2165                        isStartMoved = false;
2166                    }
2167                } else {
2168                    newEndDate = nextWeek(cal, newEndDate);
2169                    isStartMoved = false;
2170                }
2171                break;
2172            default : throw new IllegalArgumentException("invalid adjustment action: " + action);
2173            }
2174            
2175            if (!newStartDate.equals(selectionStart) || !newEndDate.equals(selectionEnd)) {
2176                monthView.setSelectionInterval(newStartDate, newEndDate);
2177                monthView.ensureDateVisible(isStartMoved ? newStartDate  : newEndDate);
2178            }
2179
2180        }
2181
2182        /**
2183         * @param cal
2184         * @param date
2185         * @return
2186         */
2187        private Date nextWeek(Calendar cal, Date date) {
2188            cal.setTime(date);
2189            cal.add(Calendar.DAY_OF_MONTH, JXMonthView.DAYS_IN_WEEK);
2190            return cal.getTime();
2191        }
2192
2193        /**
2194         * @param cal
2195         * @param date
2196         * @return
2197         */
2198        private Date previousWeek(Calendar cal, Date date) {
2199            cal.setTime(date);
2200            cal.add(Calendar.DAY_OF_MONTH, -JXMonthView.DAYS_IN_WEEK);
2201            return cal.getTime();
2202        }
2203
2204        /**
2205         * @param cal
2206         * @param date
2207         * @return
2208         */
2209        private Date nextDay(Calendar cal, Date date) {
2210            cal.setTime(date);
2211            cal.add(Calendar.DAY_OF_MONTH, 1);
2212            return cal.getTime();
2213        }
2214
2215        /**
2216         * @param cal
2217         * @param date
2218         * @return
2219         */
2220        private Date previousDay(Calendar cal, Date date) {
2221            cal.setTime(date);
2222            cal.add(Calendar.DAY_OF_MONTH, -1);
2223            return cal.getTime();
2224        }
2225        
2226
2227    }
2228
2229//--------------------- zoomable    
2230
2231    /**
2232     * Updates state after the monthView's zoomable property has been changed.
2233     * This implementation adds/removes the header component if zoomable is true/false
2234     * respectively.
2235     */
2236    protected void updateZoomable() {
2237        if (monthView.isZoomable()) {
2238            monthView.add(getCalendarHeaderHandler().getHeaderComponent());
2239        } else {
2240            monthView.remove(getCalendarHeaderHandler().getHeaderComponent());
2241        }
2242        monthView.revalidate();
2243        monthView.repaint();
2244    }
2245
2246    /**
2247     * Creates and returns a calendar header handler which provides and configures
2248     * a component for use in a zoomable monthView. Subclasses may override to return
2249     * a custom handler.<p>
2250     * 
2251     * This implementation first queries the UIManager for class to use and returns 
2252     * that if available, returns a BasicCalendarHeaderHandler if not.
2253     * 
2254     * @return a calendar header handler providing a component for use in zoomable
2255     *   monthView.
2256     * 
2257     * @see #getHeaderFromUIManager()  
2258     * @see CalendarHeaderHandler
2259     * @see BasicCalendarHeaderHandler  
2260     */
2261    protected CalendarHeaderHandler createCalendarHeaderHandler() {
2262        CalendarHeaderHandler handler = getHeaderFromUIManager();
2263        return handler != null ? handler : new BasicCalendarHeaderHandler();
2264    }
2265
2266    
2267    /**
2268     * Returns a CalendarHeaderHandler looked up in the UIManager. This implementation 
2269     * looks for a String registered with a key of CalendarHeaderHandler.uiControllerID. If
2270     * found it assumes that the value is the class name of the handler and tries 
2271     * to instantiate the handler. 
2272     * 
2273     * @return a CalendarHeaderHandler from the UIManager or null if none 
2274     *   available or instantiation failed.
2275     */
2276    protected CalendarHeaderHandler getHeaderFromUIManager() {
2277        Object handlerClass = UIManager.get(CalendarHeaderHandler.uiControllerID);
2278        if (handlerClass instanceof String) {
2279            return instantiateClass((String) handlerClass);
2280        }
2281        return null;
2282    }
2283
2284    /**
2285     * @param handlerClassName
2286     * @return
2287     */
2288    private CalendarHeaderHandler instantiateClass(String handlerClassName) {
2289        Class<?> handler = null;
2290        try {
2291            handler = Class.forName(handlerClassName);
2292            return instantiateClass(handler);
2293        } catch (ClassNotFoundException e) {
2294            // TODO Auto-generated catch block
2295            e.printStackTrace();
2296        }
2297         return null;
2298    }
2299
2300    /**
2301     * @param handlerClass
2302     * @return
2303     */
2304    private CalendarHeaderHandler instantiateClass(Class<?> handlerClass) {
2305        Constructor<?> constructor = null; 
2306        try {
2307            constructor = handlerClass.getConstructor();
2308        } catch (SecurityException e) {
2309            LOG.finer("cant instantiate CalendarHeaderHandler (security) " + handlerClass);
2310        } catch (NoSuchMethodException e) {
2311            LOG.finer("cant instantiate CalendarHeaderHandler (missing parameterless constructo?)" + handlerClass);
2312        }
2313        if (constructor != null) {
2314            try {
2315                return (CalendarHeaderHandler) constructor.newInstance();
2316            } catch (IllegalArgumentException e) {
2317                LOG.finer("cant instantiate CalendarHeaderHandler (missing parameterless constructo?)" + handlerClass);
2318            } catch (InstantiationException e) {
2319                LOG.finer("cant instantiate CalendarHeaderHandler (not instantiable) " + handlerClass);
2320            } catch (IllegalAccessException e) {
2321                LOG.finer("cant instantiate CalendarHeaderHandler (constructor not public) " + handlerClass);
2322            } catch (InvocationTargetException e) {
2323                LOG.finer("cant instantiate CalendarHeaderHandler (Invocation target)" + handlerClass);
2324            }
2325        }
2326        return null;
2327    }
2328
2329    /**
2330     * @param calendarHeaderHandler the calendarHeaderHandler to set
2331     */
2332    protected void setCalendarHeaderHandler(CalendarHeaderHandler calendarHeaderHandler) {
2333        this.calendarHeaderHandler = calendarHeaderHandler;
2334    }
2335    
2336    /**
2337     * @return the calendarHeaderHandler
2338     */
2339    protected CalendarHeaderHandler getCalendarHeaderHandler() {
2340        return calendarHeaderHandler;
2341    }
2342    
2343
2344}