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