001 package org.jdesktop.swingx.plaf.basic; 002 003 import java.awt.Color; 004 import java.awt.Dimension; 005 import java.awt.Font; 006 import java.beans.PropertyChangeEvent; 007 import java.beans.PropertyChangeListener; 008 import java.text.DateFormat; 009 import java.text.SimpleDateFormat; 010 import java.util.Calendar; 011 import java.util.logging.Logger; 012 013 import javax.swing.AbstractButton; 014 import javax.swing.AbstractSpinnerModel; 015 import javax.swing.Action; 016 import javax.swing.BorderFactory; 017 import javax.swing.Box; 018 import javax.swing.BoxLayout; 019 import javax.swing.JLabel; 020 import javax.swing.JSpinner; 021 import javax.swing.SpinnerModel; 022 import javax.swing.UIManager; 023 import javax.swing.JSpinner.DefaultEditor; 024 import javax.swing.JSpinner.NumberEditor; 025 026 import org.jdesktop.swingx.JXHyperlink; 027 import org.jdesktop.swingx.JXMonthView; 028 import org.jdesktop.swingx.JXPanel; 029 import 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 */ 064 public 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 public void propertyChange(PropertyChangeEvent evt) { 216 if ("firstDisplayedDay".equals(evt.getPropertyName())) { 217 firstDisplayedDayChanged(); 218 } else if ("locale".equals(evt.getPropertyName())) { 219 localeChanged(); 220 } 221 222 } 223 224 }; 225 } 226 return monthPropertyListener; 227 } 228 229 // ---------------------- methods to back to Spinner model 230 231 /** 232 * Returns the current year of the monthView. Callback for spinner model. 233 * 234 * return the current year of the monthView. 235 */ 236 private int getYear() { 237 Calendar cal = monthView.getCalendar(); 238 return cal.get(Calendar.YEAR); 239 } 240 241 /** 242 * Returns the previous year of the monthView. Callback for spinner model. 243 * <p> 244 * 245 * PENDING JW: check against lower bound. 246 * 247 * return the previous year of the monthView. 248 */ 249 private int getPreviousYear() { 250 Calendar cal = monthView.getCalendar(); 251 cal.add(Calendar.YEAR, -1); 252 return cal.get(Calendar.YEAR); 253 } 254 255 /** 256 * Returns the next year of the monthView. Callback for spinner model. 257 * <p> 258 * 259 * PENDING JW: check against upper bound. 260 * 261 * return the next year of the monthView. 262 */ 263 private int getNextYear() { 264 Calendar cal = monthView.getCalendar(); 265 cal.add(Calendar.YEAR, 1); 266 return cal.get(Calendar.YEAR); 267 } 268 269 /** 270 * Sets the current year of the monthView to the given value. Callback for 271 * spinner model. 272 * 273 * @param value the new value of the year. 274 * @return a boolean indicating if a change actually happened. 275 */ 276 private boolean setYear(Object value) { 277 int year = ((Integer) value).intValue(); 278 Calendar cal = monthView.getCalendar(); 279 if (cal.get(Calendar.YEAR) == year) 280 return false; 281 cal.set(Calendar.YEAR, year); 282 monthView.setFirstDisplayedDay(cal.getTime()); 283 return true; 284 } 285 286 /** 287 * Thin-layer implementation of a SpinnerModel which is actually backed by 288 * this controller. 289 */ 290 private class YearSpinnerModel extends AbstractSpinnerModel { 291 292 public Object getNextValue() { 293 return getNextYear(); 294 } 295 296 public Object getPreviousValue() { 297 return getPreviousYear(); 298 } 299 300 public Object getValue() { 301 return getYear(); 302 } 303 304 public void setValue(Object value) { 305 if (setYear(value)) { 306 fireStateChanged(); 307 } 308 } 309 310 @Override 311 public void fireStateChanged() { 312 super.fireStateChanged(); 313 } 314 315 } 316 317 private SpinnerModel getYearSpinnerModel() { 318 if (yearSpinnerModel == null) { 319 yearSpinnerModel = new YearSpinnerModel(); 320 } 321 return yearSpinnerModel; 322 } 323 324 /** 325 * The custom header component controlled and configured by this handler. 326 * 327 */ 328 protected static class SpinningCalendarHeader extends JXPanel { 329 private AbstractButton prevButton; 330 331 private AbstractButton nextButton; 332 333 private JLabel monthText; 334 335 private JSpinner yearSpinner; 336 337 private boolean surroundMonth; 338 339 public SpinningCalendarHeader() { 340 initComponents(); 341 } 342 343 /** 344 * Installs the actions and models to be used by this component. 345 * 346 * @param prev the action to use for the previous button 347 * @param next the action to use for the next button 348 * @param model the spinner model to use for the spinner. 349 */ 350 public void setActions(Action prev, Action next, SpinnerModel model) { 351 prevButton.setAction(prev); 352 nextButton.setAction(next); 353 uninstallZoomAction(); 354 installZoomAction(model); 355 } 356 357 /** 358 * Sets the focusable property of the spinner's editor's text field. 359 * 360 * The default value is false. 361 * 362 * @param focusable the focusable property of the spinner's editor. 363 */ 364 public void setSpinnerFocusable(boolean focusable) { 365 ((DefaultEditor) yearSpinner.getEditor()).getTextField() 366 .setFocusable(focusable); 367 } 368 369 /** 370 * The default value is false. 371 * 372 * @param surroundMonth 373 */ 374 public void setArrowsSurroundMonth(boolean surroundMonth) { 375 if (this.surroundMonth == surroundMonth) 376 return; 377 this.surroundMonth = surroundMonth; 378 removeAll(); 379 addComponents(); 380 } 381 382 /** 383 * Sets the text to use for the month label. 384 * 385 * @param text the text to use for the month label. 386 */ 387 public void setMonthText(String text) { 388 monthText.setText(text); 389 } 390 391 /** 392 * {@inheritDoc} 393 * <p> 394 * 395 * Overridden to set the font of its child components. 396 */ 397 @Override 398 public void setFont(Font font) { 399 super.setFont(font); 400 if (monthText != null) { 401 monthText.setFont(font); 402 yearSpinner.setFont(font); 403 yearSpinner.getEditor().setFont(font); 404 ((DefaultEditor) yearSpinner.getEditor()).getTextField() 405 .setFont(font); 406 } 407 } 408 409 /** 410 * {@inheritDoc} 411 * <p> 412 * 413 * Overridden to set the background of its child compenents. 414 */ 415 @Override 416 public void setBackground(Color bg) { 417 super.setBackground(bg); 418 for (int i = 0; i < getComponentCount(); i++) { 419 getComponent(i).setBackground(bg); 420 } 421 if (yearSpinner != null) { 422 yearSpinner.setBackground(bg); 423 yearSpinner.getEditor().setBackground(bg); 424 ((DefaultEditor) yearSpinner.getEditor()).getTextField() 425 .setBackground(bg); 426 } 427 } 428 429 private void installZoomAction(SpinnerModel model) { 430 if (model == null) 431 return; 432 yearSpinner.setModel(model); 433 } 434 435 private void uninstallZoomAction() { 436 } 437 438 private void initComponents() { 439 createComponents(); 440 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS)); 441 setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 4)); 442 addComponents(); 443 } 444 445 /** 446 * 447 */ 448 private void addComponents() { 449 if (surroundMonth) { 450 add(prevButton); 451 add(monthText); 452 add(nextButton); 453 add(Box.createHorizontalStrut(5)); 454 add(yearSpinner); 455 } else { 456 add(prevButton); 457 add(Box.createHorizontalGlue()); 458 add(monthText); 459 add(Box.createHorizontalStrut(5)); 460 add(yearSpinner); 461 add(Box.createHorizontalGlue()); 462 add(nextButton); 463 } 464 } 465 466 /** 467 * 468 */ 469 private void createComponents() { 470 prevButton = createNavigationButton(); 471 nextButton = createNavigationButton(); 472 monthText = createMonthText(); 473 yearSpinner = createSpinner(); 474 } 475 476 private JLabel createMonthText() { 477 JLabel comp = new JLabel() { 478 479 @Override 480 public Dimension getMaximumSize() { 481 Dimension dim = super.getMaximumSize(); 482 dim.width = Integer.MAX_VALUE; 483 dim.height = Integer.MAX_VALUE; 484 return dim; 485 } 486 487 }; 488 comp.setHorizontalAlignment(JLabel.CENTER); 489 return comp; 490 } 491 492 /** 493 * Creates and returns the JSpinner used for year navigation. 494 * 495 * @return 496 */ 497 private JSpinner createSpinner() { 498 JSpinner spinner = new JSpinner(); 499 spinner.setFocusable(false); 500 spinner.setBorder(BorderFactory.createEmptyBorder()); 501 NumberEditor editor = new NumberEditor(spinner); 502 editor.getFormat().setGroupingUsed(false); 503 editor.getTextField().setFocusable(false); 504 spinner.setEditor(editor); 505 return spinner; 506 } 507 508 private AbstractButton createNavigationButton() { 509 JXHyperlink b = new JXHyperlink(); 510 b.setContentAreaFilled(false); 511 b.setBorder(BorderFactory.createEmptyBorder()); 512 b.setRolloverEnabled(true); 513 b.setFocusable(false); 514 return b; 515 } 516 517 } 518 519 }