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