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 }