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}