001/*
002 * $Id: CalendarHeaderHandler.java 3927 2011-02-22 16:34:11Z kleopatra $
003 *
004 * Copyright 2007 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 * 
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 * 
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 *
021 */
022package org.jdesktop.swingx.plaf.basic;
023
024import java.awt.Color;
025import java.awt.Font;
026import java.awt.event.ActionEvent;
027import java.beans.PropertyChangeEvent;
028import java.beans.PropertyChangeListener;
029import java.util.logging.Logger;
030
031import javax.swing.Action;
032import javax.swing.Icon;
033import javax.swing.JComponent;
034import javax.swing.UIManager;
035import javax.swing.plaf.UIResource;
036
037import org.jdesktop.swingx.JXMonthView;
038import org.jdesktop.swingx.action.AbstractActionExt;
039
040/**
041 * Provides and wires a component appropriate as a calendar navigation header.
042 * The design idea is to support a pluggable header for a zoomable (PENDING JW:
043 * naming!) JXMonthView. Then custom implementations can be tailored to exactly
044 * fit their needs.
045 * <p>
046 * 
047 * To install a custom implementation, register the class name of the custom
048 * header handler with the key <code>CalendarHeaderHandler.uiControllerID</code>
049 * , example:
050 * 
051 * <pre>
052 * <code>
053 *  UIManager.put(CalendarHeaderHandler.uiControllerID, &quot;com.foo.bar.MagicHeaderHandler&quot;)
054 * </code>
055 * </pre>
056 * 
057 * Basic navigation action should (will) be defined by the ui delegate itself (PENDING
058 * JW: still incomplete in BasicMonthViewUI). This handler can modify/enhance
059 * them as appropriate for its context.
060 * <p>
061 * 
062 * PENDING JW: those icons ... who's responsible? Shouldn't we use any of the
063 * default arrows as defined in the laf anyway (are there any?)
064 * <p>
065 * 
066 * <b>Note</b>: this is work-in-progress, be prepared to change if subclassing
067 * for custom requirements!
068 * 
069 * @author Jeanette Winzenburg
070 */
071public abstract class CalendarHeaderHandler {
072
073    @SuppressWarnings("unused")
074    private static final Logger LOG = Logger
075            .getLogger(CalendarHeaderHandler.class.getName());
076
077    public static final String uiControllerID = "CalendarHeaderHandler";
078
079    protected JXMonthView monthView;
080
081    private JComponent calendarHeader;
082
083    protected Icon monthDownImage;
084
085    protected Icon monthUpImage;
086
087    private PropertyChangeListener monthViewPropertyChangeListener;
088
089    /**
090     * Installs this handler to the given month view.
091     * 
092     * @param monthView the target month view to install to.
093     */
094    public void install(JXMonthView monthView) {
095        this.monthView = monthView;
096        // PENDING JW: remove here if rendererHandler takes over control
097        // completely
098        // as is, some properties are duplicated
099        monthDownImage = UIManager.getIcon("JXMonthView.monthDownFileName");
100        monthUpImage = UIManager.getIcon("JXMonthView.monthUpFileName");
101        installNavigationActions();
102        installListeners();
103        componentOrientationChanged();
104        monthStringBackgroundChanged();
105        fontChanged();
106    }
107
108    /**
109     * Uninstalls this handler from the given target month view.
110     * 
111     * @param monthView the target month view to install from.
112     */
113    public void uninstall(JXMonthView monthView) {
114        this.monthView.remove(getHeaderComponent());
115        uninstallListeners();
116        this.monthView = null;
117    }
118
119    /**
120     * Returns a component to be used as header in a zoomable month view,
121     * guaranteed to be not null.
122     * 
123     * @return a component to be used as header in a zoomable JXMonthView
124     */
125    public JComponent getHeaderComponent() {
126        if (calendarHeader == null) {
127            calendarHeader = createCalendarHeader();
128        }
129        return calendarHeader;
130    }
131
132    /**
133     * Creates and registered listeners on the monthView as appropriate. This
134     * implementation registers a PropertyChangeListener which synchronizes
135     * internal state on changes of componentOrientation, font and
136     * monthStringBackground.
137     */
138    protected void installListeners() {
139        monthView
140                .addPropertyChangeListener(getMonthViewPropertyChangeListener());
141    }
142
143    /**
144     * Unregisters listeners which had been installed to the monthView.
145     */
146    protected void uninstallListeners() {
147        monthView.removePropertyChangeListener(monthViewPropertyChangeListener);
148    }
149
150    /**
151     * Returns the propertyChangelistener for the monthView. Lazily created.
152     * 
153     * @return the propertyChangeListener for the monthView.
154     */
155    private PropertyChangeListener getMonthViewPropertyChangeListener() {
156        if (monthViewPropertyChangeListener == null) {
157            monthViewPropertyChangeListener = new PropertyChangeListener() {
158
159                @Override
160                public void propertyChange(PropertyChangeEvent evt) {
161                    if ("componentOrientation".equals(evt.getPropertyName())) {
162                        componentOrientationChanged();
163                    } else if ("font".equals(evt.getPropertyName())) {
164                        fontChanged();
165                    } else if ("monthStringBackground".equals(evt
166                            .getPropertyName())) {
167                        monthStringBackgroundChanged();
168                    }
169
170                }
171            };
172        }
173        return monthViewPropertyChangeListener;
174    }
175
176    /**
177     * Synchronizes internal state which depends on the month view's
178     * monthStringBackground.
179     */
180    protected void monthStringBackgroundChanged() {
181        getHeaderComponent().setBackground(
182                getAsNotUIResource(monthView.getMonthStringBackground()));
183
184    }
185
186    /**
187     * Synchronizes internal state which depends on the month view's font.
188     */
189    protected void fontChanged() {
190        getHeaderComponent().setFont(getAsNotUIResource(createDerivedFont()));
191        monthView.revalidate();
192    }
193
194    /**
195     * Synchronizes internal state which depends on the month view's
196     * componentOrientation.
197     * 
198     * This implementation updates the month navigation icons and the header
199     * component's orientation.
200     */
201    protected void componentOrientationChanged() {
202        getHeaderComponent().applyComponentOrientation(
203                monthView.getComponentOrientation());
204        if (monthView.getComponentOrientation().isLeftToRight()) {
205            updateMonthNavigationIcons(monthDownImage, monthUpImage);
206        } else {
207            updateMonthNavigationIcons(monthUpImage, monthDownImage);
208        }
209    }
210
211    /**
212     * @param previous the icon to use in the previousMonth action
213     * @param next the icon to use on the nextMonth action
214     */
215    private void updateMonthNavigationIcons(Icon previous, Icon next) {
216        updateActionIcon("previousMonth", previous);
217        updateActionIcon("nextMonth", next);
218    }
219
220    /**
221     * @param previousKey
222     * @param previous
223     */
224    private void updateActionIcon(String previousKey, Icon previous) {
225        Action action = monthView.getActionMap().get(previousKey);
226        if (action != null) {
227            action.putValue(Action.SMALL_ICON, previous);
228        }
229    }
230
231    /**
232     * Creates and returns the component used as header in a zoomable monthView.
233     * 
234     * @return the component used as header in a zoomable monthView, guaranteed
235     *         to be not null.
236     */
237    protected abstract JComponent createCalendarHeader();
238
239    /**
240     * Installs and configures navigational actions.
241     * <p>
242     * 
243     * This implementation creates and installs wrappers around the
244     * scrollToPrevious/-NextMonth actions installed by the ui and configures
245     * them with the appropriate next/previous icons.
246     */
247    protected void installNavigationActions() {
248        installWrapper("scrollToPreviousMonth", "previousMonth", monthView
249                .getComponentOrientation().isLeftToRight() ? monthDownImage
250                : monthUpImage);
251        installWrapper("scrollToNextMonth", "nextMonth", monthView
252                .getComponentOrientation().isLeftToRight() ? monthUpImage
253                : monthDownImage);
254    }
255
256    /**
257     * Creates an life action wrapper around the action registered with
258     * actionKey, sets its SMALL_ICON property to the given icon and installs
259     * itself with the newActionKey.
260     * 
261     * @param actionKey the key of the action to wrap around
262     * @param newActionKey the key of the wrapper action
263     * @param icon the icon to use in the wrapper action
264     */
265    private void installWrapper(final String actionKey, String newActionKey,
266            Icon icon) {
267        AbstractActionExt wrapper = new AbstractActionExt(null, icon) {
268
269            @Override
270            public void actionPerformed(ActionEvent e) {
271                Action action = monthView.getActionMap().get(actionKey);
272                if (action != null) {
273                    action.actionPerformed(e);
274                }
275            }
276
277        };
278        monthView.getActionMap().put(newActionKey, wrapper);
279    }
280
281    /**
282     * Returns a Font based on the param which is not of type UIResource.
283     * 
284     * @param font the base font
285     * @return a font not of type UIResource, may be null.
286     */
287    private Font getAsNotUIResource(Font font) {
288        if (!(font instanceof UIResource))
289            return font;
290        // PENDING JW: correct way to create another font instance?
291        return font.deriveFont(font.getAttributes());
292    }
293
294    /**
295     * Returns a Color based on the param which is not of type UIResource.
296     * 
297     * @param color the base color
298     * @return a color not of type UIResource, may be null.
299     */
300    private Color getAsNotUIResource(Color color) {
301        if (!(color instanceof UIResource))
302            return color;
303        // PENDING JW: correct way to create another color instance?
304        float[] rgb = color.getRGBComponents(null);
305        return new Color(rgb[0], rgb[1], rgb[2], rgb[3]);
306    }
307
308    /**
309     * Create a derived font used to when painting various pieces of the month
310     * view component. This method will be called whenever the font on the
311     * component is set so a new derived font can be created.
312     */
313    protected Font createDerivedFont() {
314        return monthView.getFont().deriveFont(Font.BOLD);
315    }
316
317}