001package org.jdesktop.swingx.plaf.basic;
002
003import java.awt.Color;
004import java.awt.Component;
005import java.awt.Font;
006import java.text.DateFormat;
007import java.text.DateFormatSymbols;
008import java.text.SimpleDateFormat;
009import java.util.Calendar;
010import java.util.HashMap;
011import java.util.Locale;
012import java.util.Map;
013
014import javax.swing.JComponent;
015import javax.swing.JLabel;
016
017import org.jdesktop.swingx.JXMonthView;
018import org.jdesktop.swingx.decorator.AbstractHighlighter;
019import org.jdesktop.swingx.decorator.ComponentAdapter;
020import org.jdesktop.swingx.decorator.CompoundHighlighter;
021import org.jdesktop.swingx.decorator.HighlightPredicate;
022import org.jdesktop.swingx.decorator.Highlighter;
023import org.jdesktop.swingx.decorator.PainterHighlighter;
024import org.jdesktop.swingx.plaf.UIManagerExt;
025import org.jdesktop.swingx.renderer.CellContext;
026import org.jdesktop.swingx.renderer.ComponentProvider;
027import org.jdesktop.swingx.renderer.FormatStringValue;
028import org.jdesktop.swingx.renderer.LabelProvider;
029import org.jdesktop.swingx.renderer.StringValue;
030import org.jdesktop.swingx.renderer.StringValues;
031
032/**
033 * The RenderingHandler responsible for text rendering. It provides 
034 * and configures a rendering component for the given cell of
035 * a JXMonthView. <p>
036 * 
037 * Note: exposing the createXXStringValue methods is an emergency workaround for
038 * Issue #1062-swingx (core doesn't use arabic digits where appropriate) to allow
039 * subclasses to do better than core. So beware of future changes!
040 * 
041 */
042class BasicCalendarRenderingHandler implements CalendarRenderingHandler {
043    /** The CellContext for content and default visual config. */
044    private CalendarCellContext cellContext;
045    /** The providers to use per DayState. */
046    private Map<CalendarState, ComponentProvider<?>> providers;
047    //-------- Highlight properties
048    /** The Painter used for highlighting unselectable dates. */
049    private TextCrossingPainter<?> textCross;
050    /** The foreground color for unselectable date highlight. */
051    private Color unselectableDayForeground;
052
053    /**
054     * Instantiates a RenderingHandler and installs default state.
055     */
056    public BasicCalendarRenderingHandler() {
057        install();
058    }
059    
060    private void install() {
061        unselectableDayForeground = UIManagerExt.getColor("JXMonthView.unselectableDayForeground");
062        textCross = new TextCrossingPainter<JLabel>();
063        cellContext = new CalendarCellContext();
064        installProviders();
065    }
066
067    /**
068     * Creates and stores ComponentProviders for all DayStates.
069     */
070    private void installProviders() {
071        providers = new HashMap<CalendarState, ComponentProvider<?>>();
072
073        StringValue sv = createDayStringValue(null);
074        ComponentProvider<?> provider = new LabelProvider(sv, JLabel.RIGHT);
075        providers.put(CalendarState.IN_MONTH, provider);
076        providers.put(CalendarState.TODAY, provider);
077        providers.put(CalendarState.TRAILING, provider);
078        providers.put(CalendarState.LEADING, provider);
079
080        StringValue wsv = createWeekOfYearStringValue(null);
081        ComponentProvider<?> weekOfYearProvider = new LabelProvider(wsv,
082                JLabel.RIGHT);
083        providers.put(CalendarState.WEEK_OF_YEAR, weekOfYearProvider);
084
085        ComponentProvider<?> dayOfWeekProvider = new LabelProvider(JLabel.CENTER) {
086
087            @Override
088            protected String getValueAsString(CellContext context) {
089                Object value = context.getValue();
090                // PENDING JW: this is breaking provider's contract in its
091                // role as StringValue! Don't in the general case.
092                if (value instanceof Calendar) {
093                    int day = ((Calendar) value).get(Calendar.DAY_OF_WEEK);
094                    return ((JXMonthView) context.getComponent()).getDayOfTheWeek(day);
095                }
096                return super.getValueAsString(context);
097            }
098            
099        };
100        providers.put(CalendarState.DAY_OF_WEEK, dayOfWeekProvider);
101
102        StringValue tsv = createMonthHeaderStringValue(null);
103        ComponentProvider<?> titleProvider = new LabelProvider(tsv,
104                JLabel.CENTER);
105        providers.put(CalendarState.TITLE, titleProvider);
106    }
107
108    /**
109     * Creates and returns a StringValue used for rendering the title of a month box.
110     * The input they are assumed to handle is a Calendar configured to a day of
111     * the month to render.
112     * 
113     * @param locale the Locale to use, might be null to indicate usage of the default
114     *   Locale
115     * @return a StringValue appropriate for rendering month title.
116     */
117    protected StringValue createMonthHeaderStringValue(Locale locale) {
118        if (locale == null) {
119            locale = Locale.getDefault();
120        }
121        final String[] monthNames = DateFormatSymbols.getInstance(locale).getMonths();
122        StringValue tsv = new StringValue() {
123
124            @Override
125            public String getString(Object value) {
126                if (value instanceof Calendar) {
127                    String month = monthNames[((Calendar) value)
128                            .get(Calendar.MONTH)];
129                    return month + " "
130                            + ((Calendar) value).get(Calendar.YEAR); 
131                }
132                return StringValues.TO_STRING.getString(value);
133            }
134
135        };
136        return tsv;
137    }
138
139    /**
140     * Creates and returns a StringValue used for rendering the week of year.
141     * The input they are assumed to handle is a Calendar configured to a day of
142     * the week to render.
143     * 
144     * @param locale the Locale to use, might be null to indicate usage of the default
145     *   Locale
146     * @return a StringValue appropriate for rendering week of year.
147     */
148    protected StringValue createWeekOfYearStringValue(Locale locale) {
149        StringValue wsv = new StringValue() {
150
151            @Override
152            public String getString(Object value) {
153                if (value instanceof Calendar) {
154                    value = ((Calendar) value).get(Calendar.WEEK_OF_YEAR);
155                }
156                return StringValues.TO_STRING.getString(value);
157            }
158
159        };
160        return wsv;
161    }
162
163    /**
164     * Creates and returns a StringValue used for rendering days in a month.
165     * The input they are assumed to handle is a Calendar configured to the day.
166     * 
167     * @param locale the Locale to use, might be null to indicate usage of the default
168     *   Locale
169     * @return a StringValue appropriate for rendering days in a month
170     */
171    protected StringValue createDayStringValue(Locale locale) {
172        if (locale == null) {
173            locale = Locale.getDefault();
174        }
175        FormatStringValue sv = new FormatStringValue(new SimpleDateFormat("d", locale)) {
176
177            @Override
178            public String getString(Object value) {
179                if (value instanceof Calendar) {
180                    ((DateFormat) getFormat()).setTimeZone(((Calendar) value).getTimeZone());
181                    value = ((Calendar) value).getTime();
182                }
183                return super.getString(value);
184            }
185
186        };
187        return sv;
188    }
189    
190
191    /**
192     * Updates internal state to the given Locale.
193     * 
194     * @param locale the new Locale.
195     */
196    @Override
197    public void setLocale(Locale locale) {
198        StringValue dayValue = createDayStringValue(locale);
199        providers.get(CalendarState.IN_MONTH).setStringValue(dayValue);
200        providers.get(CalendarState.TODAY).setStringValue(dayValue);
201        providers.get(CalendarState.TRAILING).setStringValue(dayValue);
202        providers.get(CalendarState.LEADING).setStringValue(dayValue);
203        
204        providers.get(CalendarState.WEEK_OF_YEAR).setStringValue(createWeekOfYearStringValue(locale));
205        providers.get(CalendarState.TITLE).setStringValue(createMonthHeaderStringValue(locale));
206    }
207
208    /**
209     * Configures and returns a component for rendering of the given monthView cell.
210     * 
211     * @param monthView the JXMonthView to render onto
212     * @param calendar the cell value
213     * @param dayState the DayState of the cell
214     * @return a component configured for rendering the given cell
215     */
216    @Override
217    public JComponent prepareRenderingComponent(JXMonthView monthView, Calendar calendar, CalendarState dayState) {
218        cellContext.installContext(monthView, calendar, 
219                isSelected(monthView, calendar, dayState), 
220                isFocused(monthView, calendar, dayState),
221                dayState);
222        JComponent comp = providers.get(dayState).getRendererComponent(cellContext);
223        return highlight(comp, monthView, calendar, dayState);
224    }
225
226
227    /**
228     * 
229     * NOTE: it's the responsibility of the CalendarCellContext to detangle
230     * all "default" (that is: which could be queried from the comp and/or UIManager)
231     * foreground/background colors based on the given state! Moved out off here.
232     * <p>
233     * PENDING JW: replace hard-coded logic by giving over to highlighters.
234     * 
235     * @param monthView the JXMonthView to render onto
236     * @param calendar the cell value
237     * @param dayState the DayState of the cell
238     * @param dayState
239     */
240    private JComponent highlight(JComponent comp, JXMonthView monthView,
241            Calendar calendar, CalendarState dayState) {
242        CalendarAdapter adapter = getCalendarAdapter(monthView, calendar, dayState);
243        return (JComponent) getHighlighter().highlight(comp, adapter);
244    }
245
246    /**
247     * @return
248     */
249    private Highlighter getHighlighter() {
250        if (highlighter == null) {
251            highlighter = new CompoundHighlighter();
252            installHighlighters();
253        }
254        return highlighter;
255    }
256
257    /**
258     * 
259     */
260    private void installHighlighters() {
261        HighlightPredicate boldPredicate = new HighlightPredicate() {
262
263            @Override
264            public boolean isHighlighted(Component renderer,
265                    ComponentAdapter adapter) {
266                if (!(adapter instanceof CalendarAdapter))
267                    return false;
268                CalendarAdapter ca = (CalendarAdapter) adapter;
269                return CalendarState.DAY_OF_WEEK == ca.getCalendarState() || 
270                    CalendarState.TITLE == ca.getCalendarState();
271            }
272            
273        };
274        Highlighter font = new AbstractHighlighter(boldPredicate) {
275
276            @Override
277            protected Component doHighlight(Component component,
278                    ComponentAdapter adapter) {
279                component.setFont(getDerivedFont(component.getFont()));
280                return component;
281            }
282            
283        };
284        highlighter.addHighlighter(font);
285        
286        HighlightPredicate unselectable = new HighlightPredicate() {
287
288            @Override
289            public boolean isHighlighted(Component renderer,
290                    ComponentAdapter adapter) {
291                if (!(adapter instanceof CalendarAdapter)) 
292                    return false;
293                return ((CalendarAdapter) adapter).isUnselectable();
294            }
295            
296        };
297        textCross.setForeground(unselectableDayForeground);
298        Highlighter painterHL = new PainterHighlighter(unselectable, textCross);
299        highlighter.addHighlighter(painterHL);
300        
301    }
302
303    /**
304     * @param monthView
305     * @param calendar
306     * @param dayState
307     * @return
308     */
309    private CalendarAdapter getCalendarAdapter(JXMonthView monthView,
310            Calendar calendar, CalendarState dayState) {
311        if (calendarAdapter == null) {
312            calendarAdapter = new CalendarAdapter(monthView);
313        }
314        return calendarAdapter.install(calendar, dayState);
315    }
316
317    private CalendarAdapter calendarAdapter;
318    private CompoundHighlighter highlighter;
319    
320    /**
321     * @param font
322     * @return
323     */
324    private Font getDerivedFont(Font font) {
325        return font.deriveFont(Font.BOLD);
326    }
327
328    /**
329     * @param monthView
330     * @param calendar
331     * @param dayState
332     * @return
333     */
334    private boolean isFocused(JXMonthView monthView, Calendar calendar,
335            CalendarState dayState) {
336        return false;
337    }
338    
339    /**
340     * @param monthView the JXMonthView to render onto
341     * @param calendar the cell value
342     * @param dayState the DayState of the cell
343     * @return
344     */
345    private boolean isSelected(JXMonthView monthView, Calendar calendar,
346            CalendarState dayState) {
347        if (!isSelectable(dayState)) return false;
348        return monthView.isSelected(calendar.getTime());
349    }
350
351    
352    /**
353     * @param dayState
354     * @return
355     */
356    private boolean isSelectable(CalendarState dayState) {
357        return (CalendarState.IN_MONTH == dayState) || (CalendarState.TODAY == dayState);
358    }
359
360}