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}