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    }