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 }