001package org.jdesktop.swingx.plaf.basic; 002 003import java.awt.Color; 004import java.awt.Dimension; 005import java.awt.Font; 006import java.beans.PropertyChangeEvent; 007import java.beans.PropertyChangeListener; 008import java.text.DateFormat; 009import java.text.SimpleDateFormat; 010import java.util.Calendar; 011import java.util.logging.Logger; 012 013import javax.swing.AbstractButton; 014import javax.swing.AbstractSpinnerModel; 015import javax.swing.Action; 016import javax.swing.BorderFactory; 017import javax.swing.Box; 018import javax.swing.BoxLayout; 019import javax.swing.JLabel; 020import javax.swing.JSpinner; 021import javax.swing.SpinnerModel; 022import javax.swing.UIManager; 023import javax.swing.JSpinner.DefaultEditor; 024import javax.swing.JSpinner.NumberEditor; 025 026import org.jdesktop.swingx.JXHyperlink; 027import org.jdesktop.swingx.JXMonthView; 028import org.jdesktop.swingx.JXPanel; 029import org.jdesktop.swingx.renderer.FormatStringValue; 030 031/** 032 * Custom CalendarHeaderHandler which supports year-wise navigation. 033 * <p> 034 * 035 * The custom component used as header component of this implementation contains 036 * month-navigation buttons, a label with localized month text and a spinner for 037 * .. well ... spinning the years. There is minimal configuration control via 038 * the UIManager: 039 * 040 * <ul> 041 * <li>control the position of the nextMonth button: the default is at the 042 * trailing edge of the header. Option is to insert it directly after the month 043 * text, to enable set a Boolean.TRUE as value for key 044 * <code>ARROWS_SURROUNDS_MONTH</code>. 045 * <li>control the focusability of the spinner's text field: the default is 046 * false. To enable set a Boolean.TRUE as value for key 047 * <code>FOCUSABLE_SPINNER_TEXT</code>. 048 * </ul> 049 * 050 * <b>Note</b>: this header is <b>not</b> used by default. To make it the 051 * per-application default register it with the UIManager, like 052 * 053 * <pre><code> 054 * UIManager.put(CalendarHeaderHandler.uiControllerID, 055 * "org.jdesktop.swingx.plaf.basic.SpinningCalendarHeaderHandler"); 056 * </code> 057 * </pre> 058 * 059 * PENDING JW: implement and bind actions for keyboard navigation. These are 060 * potentially different from navigation by mouse: need to move the selection 061 * along with the scrolling? 062 * 063 */ 064public class SpinningCalendarHeaderHandler extends CalendarHeaderHandler { 065 066 /** 067 * Key for use in UIManager to control the position of the nextMonth arrow. 068 */ 069 public static final String ARROWS_SURROUND_MONTH = "SpinningCalendarHeader.arrowsSurroundMonth"; 070 071 /** 072 * Key for use in UIManager to control the focusable property of the year 073 * spinner. 074 */ 075 public static final String FOCUSABLE_SPINNER_TEXT = "SpinningCalendarHeader.focusableSpinnerText"; 076 077 @SuppressWarnings("unused") 078 private static final Logger LOG = Logger 079 .getLogger(SpinningCalendarHeaderHandler.class.getName()); 080 081 /** the spinner model for year-wise navigation. */ 082 private SpinnerModel yearSpinnerModel; 083 084 /** listener for property changes of the JXMonthView. */ 085 private PropertyChangeListener monthPropertyListener; 086 087 /** converter for month text. */ 088 private FormatStringValue monthStringValue; 089 090 // ----------------- public/protected overrides to manage custom 091 // creation/config 092 093 /** 094 * {@inheritDoc} 095 * <p> 096 * 097 * Overridden to configure header specifics component after calling super. 098 */ 099 @Override 100 public void install(JXMonthView monthView) { 101 super.install(monthView); 102 getHeaderComponent().setActions( 103 monthView.getActionMap().get("previousMonth"), 104 monthView.getActionMap().get("nextMonth"), 105 getYearSpinnerModel()); 106 componentOrientationChanged(); 107 monthStringBackgroundChanged(); 108 fontChanged(); 109 localeChanged(); 110 } 111 112 /** 113 * {@inheritDoc} 114 * <p> 115 * 116 * Overridden to cleanup the specifics before calling super. 117 */ 118 @Override 119 public void uninstall(JXMonthView monthView) { 120 getHeaderComponent().setActions(null, null, null); 121 getHeaderComponent().setMonthText(""); 122 super.uninstall(monthView); 123 } 124 125 /** 126 * {@inheritDoc} 127 * <p> 128 * 129 * Convenience override to the type created. 130 */ 131 @Override 132 public SpinningCalendarHeader getHeaderComponent() { 133 return (SpinningCalendarHeader) super.getHeaderComponent(); 134 } 135 136 /** 137 * {@inheritDoc} 138 * <p> 139 * 140 * Implemented to create and configure the custom header component. 141 */ 142 @Override 143 protected SpinningCalendarHeader createCalendarHeader() { 144 SpinningCalendarHeader header = new SpinningCalendarHeader(); 145 if (Boolean.TRUE.equals(UIManager.getBoolean(FOCUSABLE_SPINNER_TEXT))) { 146 header.setSpinnerFocusable(true); 147 } 148 if (Boolean.TRUE.equals(UIManager.getBoolean(ARROWS_SURROUND_MONTH))) { 149 header.setArrowsSurroundMonth(true); 150 } 151 return header; 152 } 153 154 /** 155 * {@inheritDoc} 156 * <p> 157 */ 158 @Override 159 protected void installListeners() { 160 super.installListeners(); 161 monthView.addPropertyChangeListener(getPropertyChangeListener()); 162 } 163 164 /** 165 * {@inheritDoc} 166 * <p> 167 */ 168 @Override 169 protected void uninstallListeners() { 170 monthView.removePropertyChangeListener(getPropertyChangeListener()); 171 super.uninstallListeners(); 172 } 173 174 // ---------------- listening/update triggered by changes of the JXMonthView 175 176 /** 177 * Updates the formatter of the month text to the JXMonthView's Locale. 178 */ 179 protected void updateFormatters() { 180 SimpleDateFormat monthNameFormat = (SimpleDateFormat) DateFormat 181 .getDateInstance(DateFormat.SHORT, monthView.getLocale()); 182 monthNameFormat.applyPattern("MMMM"); 183 monthStringValue = new FormatStringValue(monthNameFormat); 184 } 185 186 /** 187 * Updates internal state to monthView's firstDisplayedDay. 188 */ 189 protected void firstDisplayedDayChanged() { 190 ((YearSpinnerModel) getYearSpinnerModel()).fireStateChanged(); 191 getHeaderComponent().setMonthText( 192 monthStringValue.getString(monthView.getFirstDisplayedDay())); 193 } 194 195 /** 196 * Updates internal state to monthView's locale. 197 */ 198 protected void localeChanged() { 199 updateFormatters(); 200 firstDisplayedDayChanged(); 201 } 202 203 /** 204 * Returns the property change listener for use on the monthView. This is 205 * lazyly created if not yet done. This implementation listens to changes of 206 * firstDisplayedDay and locale property and updates internal state 207 * accordingly. 208 * 209 * @return the property change listener for the monthView, never null. 210 */ 211 private PropertyChangeListener getPropertyChangeListener() { 212 if (monthPropertyListener == null) { 213 monthPropertyListener = new PropertyChangeListener() { 214 215 @Override 216 public void propertyChange(PropertyChangeEvent evt) { 217 if ("firstDisplayedDay".equals(evt.getPropertyName())) { 218 firstDisplayedDayChanged(); 219 } else if ("locale".equals(evt.getPropertyName())) { 220 localeChanged(); 221 } 222 223 } 224 225 }; 226 } 227 return monthPropertyListener; 228 } 229 230 // ---------------------- methods to back to Spinner model 231 232 /** 233 * Returns the current year of the monthView. Callback for spinner model. 234 * 235 * return the current year of the monthView. 236 */ 237 private int getYear() { 238 Calendar cal = monthView.getCalendar(); 239 return cal.get(Calendar.YEAR); 240 } 241 242 /** 243 * Returns the previous year of the monthView. Callback for spinner model. 244 * <p> 245 * 246 * PENDING JW: check against lower bound. 247 * 248 * return the previous year of the monthView. 249 */ 250 private int getPreviousYear() { 251 Calendar cal = monthView.getCalendar(); 252 cal.add(Calendar.YEAR, -1); 253 return cal.get(Calendar.YEAR); 254 } 255 256 /** 257 * Returns the next year of the monthView. Callback for spinner model. 258 * <p> 259 * 260 * PENDING JW: check against upper bound. 261 * 262 * return the next year of the monthView. 263 */ 264 private int getNextYear() { 265 Calendar cal = monthView.getCalendar(); 266 cal.add(Calendar.YEAR, 1); 267 return cal.get(Calendar.YEAR); 268 } 269 270 /** 271 * Sets the current year of the monthView to the given value. Callback for 272 * spinner model. 273 * 274 * @param value the new value of the year. 275 * @return a boolean indicating if a change actually happened. 276 */ 277 private boolean setYear(Object value) { 278 int year = ((Integer) value).intValue(); 279 Calendar cal = monthView.getCalendar(); 280 if (cal.get(Calendar.YEAR) == year) 281 return false; 282 cal.set(Calendar.YEAR, year); 283 monthView.setFirstDisplayedDay(cal.getTime()); 284 return true; 285 } 286 287 /** 288 * Thin-layer implementation of a SpinnerModel which is actually backed by 289 * this controller. 290 */ 291 private class YearSpinnerModel extends AbstractSpinnerModel { 292 293 @Override 294 public Object getNextValue() { 295 return getNextYear(); 296 } 297 298 @Override 299 public Object getPreviousValue() { 300 return getPreviousYear(); 301 } 302 303 @Override 304 public Object getValue() { 305 return getYear(); 306 } 307 308 @Override 309 public void setValue(Object value) { 310 if (setYear(value)) { 311 fireStateChanged(); 312 } 313 } 314 315 @Override 316 public void fireStateChanged() { 317 super.fireStateChanged(); 318 } 319 320 } 321 322 private SpinnerModel getYearSpinnerModel() { 323 if (yearSpinnerModel == null) { 324 yearSpinnerModel = new YearSpinnerModel(); 325 } 326 return yearSpinnerModel; 327 } 328 329 /** 330 * The custom header component controlled and configured by this handler. 331 * 332 */ 333 protected static class SpinningCalendarHeader extends JXPanel { 334 private AbstractButton prevButton; 335 336 private AbstractButton nextButton; 337 338 private JLabel monthText; 339 340 private JSpinner yearSpinner; 341 342 private boolean surroundMonth; 343 344 public SpinningCalendarHeader() { 345 initComponents(); 346 } 347 348 /** 349 * Installs the actions and models to be used by this component. 350 * 351 * @param prev the action to use for the previous button 352 * @param next the action to use for the next button 353 * @param model the spinner model to use for the spinner. 354 */ 355 public void setActions(Action prev, Action next, SpinnerModel model) { 356 prevButton.setAction(prev); 357 nextButton.setAction(next); 358 uninstallZoomAction(); 359 installZoomAction(model); 360 } 361 362 /** 363 * Sets the focusable property of the spinner's editor's text field. 364 * 365 * The default value is false. 366 * 367 * @param focusable the focusable property of the spinner's editor. 368 */ 369 public void setSpinnerFocusable(boolean focusable) { 370 ((DefaultEditor) yearSpinner.getEditor()).getTextField() 371 .setFocusable(focusable); 372 } 373 374 /** 375 * The default value is false. 376 * 377 * @param surroundMonth 378 */ 379 public void setArrowsSurroundMonth(boolean surroundMonth) { 380 if (this.surroundMonth == surroundMonth) 381 return; 382 this.surroundMonth = surroundMonth; 383 removeAll(); 384 addComponents(); 385 } 386 387 /** 388 * Sets the text to use for the month label. 389 * 390 * @param text the text to use for the month label. 391 */ 392 public void setMonthText(String text) { 393 monthText.setText(text); 394 } 395 396 /** 397 * {@inheritDoc} 398 * <p> 399 * 400 * Overridden to set the font of its child components. 401 */ 402 @Override 403 public void setFont(Font font) { 404 super.setFont(font); 405 if (monthText != null) { 406 monthText.setFont(font); 407 yearSpinner.setFont(font); 408 yearSpinner.getEditor().setFont(font); 409 ((DefaultEditor) yearSpinner.getEditor()).getTextField() 410 .setFont(font); 411 } 412 } 413 414 /** 415 * {@inheritDoc} 416 * <p> 417 * 418 * Overridden to set the background of its child compenents. 419 */ 420 @Override 421 public void setBackground(Color bg) { 422 super.setBackground(bg); 423 for (int i = 0; i < getComponentCount(); i++) { 424 getComponent(i).setBackground(bg); 425 } 426 if (yearSpinner != null) { 427 yearSpinner.setBackground(bg); 428 yearSpinner.getEditor().setBackground(bg); 429 ((DefaultEditor) yearSpinner.getEditor()).getTextField() 430 .setBackground(bg); 431 } 432 } 433 434 private void installZoomAction(SpinnerModel model) { 435 if (model == null) 436 return; 437 yearSpinner.setModel(model); 438 } 439 440 private void uninstallZoomAction() { 441 } 442 443 private void initComponents() { 444 createComponents(); 445 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS)); 446 setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 4)); 447 addComponents(); 448 } 449 450 /** 451 * 452 */ 453 private void addComponents() { 454 if (surroundMonth) { 455 add(prevButton); 456 add(monthText); 457 add(nextButton); 458 add(Box.createHorizontalStrut(5)); 459 add(yearSpinner); 460 } else { 461 add(prevButton); 462 add(Box.createHorizontalGlue()); 463 add(monthText); 464 add(Box.createHorizontalStrut(5)); 465 add(yearSpinner); 466 add(Box.createHorizontalGlue()); 467 add(nextButton); 468 } 469 } 470 471 /** 472 * 473 */ 474 private void createComponents() { 475 prevButton = createNavigationButton(); 476 nextButton = createNavigationButton(); 477 monthText = createMonthText(); 478 yearSpinner = createSpinner(); 479 } 480 481 private JLabel createMonthText() { 482 JLabel comp = new JLabel() { 483 484 @Override 485 public Dimension getMaximumSize() { 486 Dimension dim = super.getMaximumSize(); 487 dim.width = Integer.MAX_VALUE; 488 dim.height = Integer.MAX_VALUE; 489 return dim; 490 } 491 492 }; 493 comp.setHorizontalAlignment(JLabel.CENTER); 494 return comp; 495 } 496 497 /** 498 * Creates and returns the JSpinner used for year navigation. 499 * 500 * @return 501 */ 502 private JSpinner createSpinner() { 503 JSpinner spinner = new JSpinner(); 504 spinner.setFocusable(false); 505 spinner.setBorder(BorderFactory.createEmptyBorder()); 506 NumberEditor editor = new NumberEditor(spinner); 507 editor.getFormat().setGroupingUsed(false); 508 editor.getTextField().setFocusable(false); 509 spinner.setEditor(editor); 510 return spinner; 511 } 512 513 private AbstractButton createNavigationButton() { 514 JXHyperlink b = new JXHyperlink(); 515 b.setContentAreaFilled(false); 516 b.setBorder(BorderFactory.createEmptyBorder()); 517 b.setRolloverEnabled(true); 518 b.setFocusable(false); 519 return b; 520 } 521 522 } 523 524}