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