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 }