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