001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2016 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * https://www.ssec.wisc.edu/mcidas 009 * 010 * All Rights Reserved 011 * 012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 013 * some McIDAS-V source code is based on IDV and VisAD source code. 014 * 015 * McIDAS-V is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU Lesser Public License as published by 017 * the Free Software Foundation; either version 3 of the License, or 018 * (at your option) any later version. 019 * 020 * McIDAS-V is distributed in the hope that it will be useful, 021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 023 * GNU Lesser Public License for more details. 024 * 025 * You should have received a copy of the GNU Lesser Public License 026 * along with this program. If not, see http://www.gnu.org/licenses. 027 */ 028 029package edu.wisc.ssec.mcidasv.ui; 030 031import static java.text.DateFormat.getDateInstance; 032import static javax.swing.UIManager.getColor; 033 034import com.toedter.calendar.DateUtil; 035import com.toedter.calendar.IDateEditor; 036import org.joda.time.LocalDate; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040import java.awt.Color; 041import java.awt.Dimension; 042import java.awt.event.ActionEvent; 043import java.awt.event.ActionListener; 044import java.awt.event.FocusEvent; 045import java.awt.event.FocusListener; 046 047import javax.swing.JComponent; 048import javax.swing.JFormattedTextField; 049import javax.swing.JTextField; 050import javax.swing.event.CaretEvent; 051import javax.swing.event.CaretListener; 052import javax.swing.text.MaskFormatter; 053 054import java.text.DateFormat; 055import java.text.ParseException; 056import java.text.SimpleDateFormat; 057 058import java.util.Calendar; 059import java.util.Date; 060import java.util.Locale; 061import java.util.regex.Pattern; 062 063/** 064 * This class is just a {@link com.toedter.calendar.JTextFieldDateEditor} that 065 * allows the user to enter either the day within (current) year or a 066 * McIDAS-X style {@literal "julian day"} ({@code YYYYDDD} or {@code YYDDD}), 067 * in addition to the formatting allowed by {@code JTextFieldDateEditor}. 068 */ 069public class JCalendarDateEditor extends JFormattedTextField implements 070 IDateEditor, CaretListener, FocusListener, ActionListener 071{ 072 /** Match day of year. */ 073 private static final Pattern dayOnly = Pattern.compile("\\d{1,3}"); 074 075 /** Match {@code YYYYDDD}. */ 076 private static final Pattern yearDay = Pattern.compile("\\d{7}"); 077 078 /** Match {@code YYDDD} dates. */ 079 private static final Pattern badYearDay = Pattern.compile("\\d{5}"); 080 081 private static final Logger logger = 082 LoggerFactory.getLogger(JCalendarDateEditor.class); 083 084 protected Date date; 085 086 protected SimpleDateFormat dateFormatter; 087 088 /** Parse {@code DDD} dates (even if they are one or two digits). */ 089 private final SimpleDateFormat dayOfYear; 090 091 /** Parse {@code YYYYDDD} dates. */ 092 private final SimpleDateFormat yearAndDay; 093 094 /** Parse {@code YYDDD} dates. */ 095 private final SimpleDateFormat badYearAndDay; 096 097 protected MaskFormatter maskFormatter; 098 099 protected String datePattern; 100 101 protected String maskPattern; 102 103 protected char placeholder; 104 105 protected Color darkGreen; 106 107 protected DateUtil dateUtil; 108 109 private boolean isMaskVisible; 110 111 private boolean ignoreDatePatternChange; 112 113 private int hours; 114 115 private int minutes; 116 117 private int seconds; 118 119 private int millis; 120 121 private Calendar calendar; 122 123 public JCalendarDateEditor() { 124 this(false, null, null, ' '); 125 } 126 127 public JCalendarDateEditor(String datePattern, String maskPattern, 128 char placeholder) 129 { 130 this(true, datePattern, maskPattern, placeholder); 131 } 132 133 public JCalendarDateEditor(boolean showMask, String datePattern, 134 String maskPattern, char placeholder) 135 { 136 dateFormatter = (SimpleDateFormat)getDateInstance(DateFormat.MEDIUM); 137 dayOfYear = new SimpleDateFormat("DDD"); 138 yearAndDay = new SimpleDateFormat("yyyyDDD"); 139 badYearAndDay = new SimpleDateFormat("yyDDD"); 140 dateFormatter.setLenient(false); 141 dayOfYear.setLenient(false); 142 yearAndDay.setLenient(false); 143 144 setDateFormatString(datePattern); 145 if (datePattern != null) { 146 ignoreDatePatternChange = true; 147 } 148 149 this.placeholder = placeholder; 150 151 if (maskPattern == null) { 152 this.maskPattern = createMaskFromDatePattern(this.datePattern); 153 } else { 154 this.maskPattern = maskPattern; 155 } 156 157 setToolTipText(this.datePattern); 158 setMaskVisible(showMask); 159 160 addCaretListener(this); 161 addFocusListener(this); 162 addActionListener(this); 163 darkGreen = new Color(0, 150, 0); 164 165 calendar = Calendar.getInstance(); 166 167 dateUtil = new DateUtil(); 168 } 169 170 /* 171 * (non-Javadoc) 172 * 173 * @see com.toedter.calendar.IDateEditor#getDate() 174 */ 175 @Override public Date getDate() { 176 try { 177 calendar.setTime(dateFormatter.parse(getText())); 178 calendar.set(Calendar.HOUR_OF_DAY, hours); 179 calendar.set(Calendar.MINUTE, minutes); 180 calendar.set(Calendar.SECOND, seconds); 181 calendar.set(Calendar.MILLISECOND, millis); 182 date = calendar.getTime(); 183 } catch (ParseException e) { 184 date = null; 185 } 186 return date; 187 } 188 189 /* 190 * (non-Javadoc) 191 * 192 * @see com.toedter.calendar.IDateEditor#setDate(java.util.Date) 193 */ 194 @Override public void setDate(Date date) { 195 setDate(date, true); 196 } 197 198 /** 199 * Sets the date. 200 * 201 * @param date the date 202 * @param firePropertyChange true, if the date property should be fired. 203 */ 204 protected void setDate(Date date, boolean firePropertyChange) { 205 Date oldDate = this.date; 206 this.date = date; 207 208 if (date == null) { 209 setText(""); 210 } else { 211 calendar.setTime(date); 212 hours = calendar.get(Calendar.HOUR_OF_DAY); 213 minutes = calendar.get(Calendar.MINUTE); 214 seconds = calendar.get(Calendar.SECOND); 215 millis = calendar.get(Calendar.MILLISECOND); 216 217 String formattedDate = dateFormatter.format(date); 218 try { 219 setText(formattedDate); 220 } catch (RuntimeException e) { 221 logger.debug("could not set text: {}", e); 222 } 223 } 224 if ((date != null) && dateUtil.checkDate(date)) { 225 setForeground(Color.BLACK); 226 } 227 228 if (firePropertyChange) { 229 firePropertyChange("date", oldDate, date); 230 } 231 } 232 233 /* 234 * (non-Javadoc) 235 * 236 * @see com.toedter.calendar.IDateEditor#setDateFormatString(java.lang.String) 237 */ 238 @Override public void setDateFormatString(String dateFormatString) { 239 if (!ignoreDatePatternChange) { 240 try { 241 dateFormatter.applyPattern(dateFormatString); 242 } catch (RuntimeException e) { 243 dateFormatter = 244 (SimpleDateFormat)getDateInstance(DateFormat.MEDIUM); 245 dateFormatter.setLenient(false); 246 } 247 this.datePattern = dateFormatter.toPattern(); 248 setToolTipText(this.datePattern); 249 setDate(date, false); 250 } 251 } 252 253 /* 254 * (non-Javadoc) 255 * 256 * @see com.toedter.calendar.IDateEditor#getDateFormatString() 257 */ 258 @Override public String getDateFormatString() { 259 return datePattern; 260 } 261 262 /* 263 * (non-Javadoc) 264 * 265 * @see com.toedter.calendar.IDateEditor#getUiComponent() 266 */ 267 @Override public JComponent getUiComponent() { 268 return this; 269 } 270 271 private Date attemptParsing(String text) { 272 Date result = null; 273 try { 274 if (dayOnly.matcher(text).matches()) { 275 String full = LocalDate.now().getYear() + text; 276 result = yearAndDay.parse(full); 277 } else if (yearDay.matcher(text).matches()) { 278 result = yearAndDay.parse(text); 279 } else if (badYearDay.matcher(text).matches()) { 280 result = badYearAndDay.parse(text); 281 } else { 282 result = dateFormatter.parse(text); 283 } 284 } catch (Exception e) { 285 logger.trace("failed to parse '{}'", text); 286 } 287 return result; 288 } 289 290 /** 291 * After any user input, the value of the textfield is proofed. Depending on 292 * being a valid date, the value is colored green or red. 293 * 294 * @param event Caret event. 295 */ 296 @Override public void caretUpdate(CaretEvent event) { 297 String text = getText().trim(); 298 String emptyMask = maskPattern.replace('#', placeholder); 299 300 if (text.isEmpty() || text.equals(emptyMask)) { 301 setForeground(Color.BLACK); 302 return; 303 } 304 305 Date parsed = attemptParsing(this.getText()); 306 if ((parsed != null) && dateUtil.checkDate(parsed)) { 307 this.setForeground(this.darkGreen); 308 } else { 309 this.setForeground(Color.RED); 310 } 311 } 312 313 /* 314 * (non-Javadoc) 315 * 316 * @see java.awt.event.FocusListener#focusLost(java.awt.event.FocusEvent) 317 */ 318 @Override public void focusLost(FocusEvent focusEvent) { 319 checkText(); 320 } 321 322 private void checkText() { 323 Date parsedDate = attemptParsing(this.getText()); 324 if (parsedDate != null) { 325 this.setDate(parsedDate, true); 326 } 327 } 328 329 /* 330 * (non-Javadoc) 331 * 332 * @see java.awt.event.FocusListener#focusGained(java.awt.event.FocusEvent) 333 */ 334 @Override public void focusGained(FocusEvent e) { 335 } 336 337 /* 338 * (non-Javadoc) 339 * 340 * @see java.awt.Component#setLocale(java.util.Locale) 341 */ 342 @Override public void setLocale(Locale locale) { 343 if (!locale.equals(getLocale()) || ignoreDatePatternChange) { 344 super.setLocale(locale); 345 dateFormatter = 346 (SimpleDateFormat) getDateInstance(DateFormat.MEDIUM, locale); 347 setToolTipText(dateFormatter.toPattern()); 348 setDate(date, false); 349 doLayout(); 350 } 351 } 352 353 /** 354 * Creates a mask from a date pattern. This is a very simple (and 355 * incomplete) implementation thet works only with numbers. A date pattern 356 * of {@literal "MM/dd/yy"} will result in the mask "##/##/##". Probably 357 * you want to override this method if it does not fit your needs. 358 * 359 * @param datePattern Date pattern. 360 * @return the mask 361 */ 362 public String createMaskFromDatePattern(String datePattern) { 363 String symbols = "GyMdkHmsSEDFwWahKzZ"; 364 StringBuilder maskBuffer = new StringBuilder(datePattern.length() * 2); 365 for (int i = 0; i < datePattern.length(); i++) { 366 char ch = datePattern.charAt(i); 367 boolean symbolFound = false; 368 for (int n = 0; n < symbols.length(); n++) { 369 if (symbols.charAt(n) == ch) { 370 maskBuffer.append('#'); 371 symbolFound = true; 372 break; 373 } 374 } 375 if (!symbolFound) { 376 maskBuffer.append(ch); 377 } 378 } 379 return maskBuffer.toString(); 380 } 381 382 /** 383 * Returns {@code true}, if the mask is visible. 384 * 385 * @return {@code true}, if the mask is visible. 386 */ 387 public boolean isMaskVisible() { 388 return isMaskVisible; 389 } 390 391 /** 392 * Sets the mask visible. 393 * 394 * @param isMaskVisible Whether or not the mask should be visible. 395 */ 396 public void setMaskVisible(boolean isMaskVisible) { 397 this.isMaskVisible = isMaskVisible; 398 if (isMaskVisible) { 399 if (maskFormatter == null) { 400 try { 401 String mask = createMaskFromDatePattern(datePattern); 402 maskFormatter = new MaskFormatter(mask); 403 maskFormatter.setPlaceholderCharacter(this.placeholder); 404 maskFormatter.install(this); 405 } catch (ParseException e) { 406 logger.debug("parsing error: {}", e); 407 } 408 } 409 } 410 } 411 412 /** 413 * Returns the preferred size. If a date pattern is set, it is the size the 414 * date pattern would take. 415 */ 416 @Override public Dimension getPreferredSize() { 417 return (datePattern != null) 418 ? new JTextField(datePattern).getPreferredSize() 419 : super.getPreferredSize(); 420 } 421 422 /** 423 * Validates the typed date and sets it (only if it is valid). 424 */ 425 @Override public void actionPerformed(ActionEvent e) { 426 checkText(); 427 } 428 429 /** 430 * Enables and disabled the compoment. It also fixes the background bug 431 * 4991597 and sets the background explicitely to a 432 * TextField.inactiveBackground. 433 */ 434 @Override public void setEnabled(boolean b) { 435 super.setEnabled(b); 436 if (!b) { 437 super.setBackground(getColor("TextField.inactiveBackground")); 438 } 439 } 440 441 /* 442 * (non-Javadoc) 443 * 444 * @see com.toedter.calendar.IDateEditor#getMaxSelectableDate() 445 */ 446 @Override public Date getMaxSelectableDate() { 447 return dateUtil.getMaxSelectableDate(); 448 } 449 450 /* 451 * (non-Javadoc) 452 * 453 * @see com.toedter.calendar.IDateEditor#getMinSelectableDate() 454 */ 455 @Override public Date getMinSelectableDate() { 456 return dateUtil.getMinSelectableDate(); 457 } 458 459 /* 460 * (non-Javadoc) 461 * 462 * @see com.toedter.calendar.IDateEditor#setMaxSelectableDate(java.util.Date) 463 */ 464 @Override public void setMaxSelectableDate(Date max) { 465 dateUtil.setMaxSelectableDate(max); 466 checkText(); 467 } 468 469 /* 470 * (non-Javadoc) 471 * 472 * @see com.toedter.calendar.IDateEditor#setMinSelectableDate(java.util.Date) 473 */ 474 @Override public void setMinSelectableDate(Date min) { 475 dateUtil.setMinSelectableDate(min); 476 checkText(); 477 } 478 479 /* 480 * (non-Javadoc) 481 * 482 * @see com.toedter.calendar.IDateEditor#setSelectableDateRange(java.util.Date,java.util.Date) 483 */ 484 @Override public void setSelectableDateRange(Date min, Date max) { 485 dateUtil.setSelectableDateRange(min, max); 486 checkText(); 487 } 488}