001/* ---------------------------------------------------------------------------- 002 The Kiwi Toolkit - A Java Class Library 003 Copyright (C) 1998-2004 Mark A. Lindner 004 005 This library is free software; you can redistribute it and/or 006 modify it under the terms of the GNU General Public License as 007 published by the Free Software Foundation; either version 2 of the 008 License, or (at your option) any later version. 009 010 This library is distributed in the hope that it will be useful, 011 but WITHOUT ANY WARRANTY; without even the implied warranty of 012 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013 General Public License for more details. 014 015 You should have received a copy of the GNU General Public License 016 along with this library; if not, write to the Free Software 017 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 018 02111-1307, USA. 019 020 The author may be contacted at: mark_a_lindner@yahoo.com 021 ---------------------------------------------------------------------------- 022 $Log: DateChooser.java,v $ 023 Revision 1.27 2004/06/28 02:41:32 markl 024 Applied Robert Heitzmann's patch for date clipping. 025 026 Revision 1.26 2004/05/31 07:26:23 markl 027 javadoc update 028 029 Revision 1.25 2004/05/12 19:14:50 markl 030 Rewritten to render calendar grid with toggle buttons, rather than 031 painting it directly. Added setCellSize() method. 032 033 Revision 1.24 2004/03/16 06:43:39 markl 034 LocaleManager method change 035 036 Revision 1.23 2004/01/23 00:03:58 markl 037 javadoc corrections 038 039 Revision 1.22 2003/02/06 07:40:31 markl 040 Removed references to Metal Look & Feel. 041 042 Revision 1.21 2003/01/19 09:50:04 markl 043 Modified mouse event handling so cell is highlighted on mousedown rather 044 than mouseup. 045 046 Revision 1.20 2001/06/26 06:17:35 markl 047 Updated with new LocaleManager API. 048 049 Revision 1.19 2001/03/12 09:56:54 markl 050 KLabel/KLabelArea changes. 051 052 Revision 1.18 2001/03/12 09:27:54 markl 053 Source code and Javadoc cleanup. 054 055 Revision 1.17 2000/10/11 10:46:37 markl 056 Fixed mouse event handler and added ActionEvent command strings. 057 058 Revision 1.16 2000/05/29 06:08:25 markl 059 Removed debug println. 060 061 Revision 1.15 1999/08/30 22:29:25 markl 062 Replaced RepeaterButton with KButton, due to synchronization bugs. 063 064 Revision 1.14 1999/08/18 06:13:18 markl 065 Fixed a coordinate calculation bug in the mouse event handler. 066 067 Revision 1.13 1999/08/13 07:11:17 markl 068 Bug fix to eliminate NullPointerException. 069 070 Revision 1.12 1999/08/05 14:47:17 markl 071 Added accessors for highlight color. 072 073 Revision 1.11 1999/08/05 14:37:14 markl 074 Allow setSelectedDay() to select a day that is out of the selection range. 075 076 Revision 1.10 1999/08/03 04:49:13 markl 077 Removed debug println. 078 079 Revision 1.9 1999/07/26 08:51:18 markl 080 Added ActionEvent support. 081 082 Revision 1.8 1999/07/25 13:57:11 markl 083 Bug fix. 084 085 Revision 1.7 1999/07/25 13:46:30 markl 086 Javadoc fixes. 087 088 Revision 1.6 1999/07/25 13:35:59 markl 089 Extensive changes, including fixes to the paint method, added support for 090 constrained date selection range, enhanced month/year wrap-around, and 091 internationalization enhancements. 092 093 Revision 1.5 1999/06/14 02:10:35 markl 094 Added range selection flag 095 096 Revision 1.4 1999/06/08 06:47:06 markl 097 Mouseup event bug fix. 098 099 Revision 1.3 1999/05/05 06:08:50 markl 100 Added logic for leap years. 101 102 Revision 1.2 1999/02/28 00:25:42 markl 103 Changes from David Croy. 104 ---------------------------------------------------------------------------- 105*/ 106 107package kiwi.ui; 108 109import java.awt.*; 110import java.awt.event.*; 111import java.text.*; 112import java.util.*; 113import javax.swing.*; 114import javax.swing.border.*; 115 116import kiwi.event.*; 117import kiwi.util.*; 118 119/** This class represents a date chooser. The chooser allows an arbitrary date 120 * to be selected by presenting a calendar with day, month and year selectors. 121 * The range of selectable dates may be constrained by supplying a minimum 122 * and/or maximum selectable date. The date chooser is fully locale-aware. 123 * 124 * <p><center> 125 * <img src="snapshot/DateChooser.gif"><br> 126 * <i>An example DateChooser.</i> 127 * </center> 128 * 129 * @author Mark Lindner 130 */ 131 132public class DateChooser extends KPanel implements ActionListener 133 { 134 private KLabel l_date, l_year, l_month; 135 private KButton b_lyear, b_ryear, b_lmonth, b_rmonth; 136 private SimpleDateFormat datefmt = new SimpleDateFormat("E d MMM yyyy"); 137 private Calendar selectedDate = null, minDate = null, maxDate = null; 138 private int selectedDay, firstDay, minDay = -1, maxDay = -1, firstCell = 0; 139 private JToggleButton b_days[]; 140 private KLabel l_days[]; 141 private static final int[] daysInMonth 142 = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; 143 private static final int[] daysInMonthLeap 144 = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; 145 private String months[], labels[] = new String[7]; 146 private static final Color weekendColor = Color.red.darker(); 147 private boolean clipMin = false, clipMax = false, clipAllMin = false, 148 clipAllMax = false; 149 private int weekendCols[] = { 0, 0 }; 150 private ActionSupport asupport; 151 private int fontHeight; 152 private ButtonGroup group; 153 154 /** The default cell size. */ 155 public static final int CELL_SIZE = 30; 156 /** <i>Date changed</i> event command. */ 157 public static final String DATE_CHANGE_CMD = "dateChanged"; 158 /** <i>Month changed</i> event command. */ 159 public static final String MONTH_CHANGE_CMD = "monthChanged"; 160 /** <i>Year changed</i> event command. */ 161 public static final String YEAR_CHANGE_CMD = "yearChanged"; 162 163 /** Construct a new <code>DateChooser</code>. The selection will be 164 * initialized to the current date. 165 */ 166 167 public DateChooser() 168 { 169 this(Calendar.getInstance()); 170 } 171 172 /** Construct a new <code>DateChooser</code> with the specified selected 173 * date. 174 * 175 * @param date The date for the selection. 176 */ 177 178 public DateChooser(Calendar date) 179 { 180 l_days = new KLabel[7]; 181 182 int fd = date.getFirstDayOfWeek(); 183 DateFormatSymbols sym = LocaleManager.getDefault().getDateFormatSymbols(); 184 months = sym.getShortMonths(); 185 String wkd[] = sym.getShortWeekdays(); 186 187 for(int i = 0, ii = fd; i < l_days.length; i++) 188 { 189 int l = Math.min(wkd[ii].length(), 2); 190 l_days[i] = new KLabel(wkd[ii].substring(0, l)); 191 l_days[i].setHorizontalAlignment(SwingConstants.CENTER); 192 l_days[i].setVerticalAlignment(SwingConstants.BOTTOM); 193 194 if((ii == Calendar.SATURDAY) || (ii == Calendar.SUNDAY)) 195 l_days[i].setForeground(weekendColor); 196 197 if(++ii > 7) 198 ii = 1; 199 } 200 201 group = new ButtonGroup(); 202 b_days = new JToggleButton[6 * 7]; 203 204 Insets cellMargin = new Insets(2, 2, 2, 2); 205 206 for(int i = 0, ii = fd; i < b_days.length; i++) 207 { 208 b_days[i] = new JToggleButton(); 209 b_days[i].setOpaque(false); 210 b_days[i].setMargin(cellMargin); 211 // b_days[i].setFocusPainted(false); 212 b_days[i].setHorizontalAlignment(SwingConstants.RIGHT); 213 b_days[i].setVerticalAlignment(SwingConstants.TOP); 214 if((ii == Calendar.SATURDAY) || (ii == Calendar.SUNDAY)) 215 b_days[i].setForeground(weekendColor); 216 217 Dimension dim = new Dimension(CELL_SIZE, CELL_SIZE); 218 b_days[i].setSize(dim); 219 b_days[i].setMinimumSize(dim); 220 b_days[i].setPreferredSize(dim); 221 222 b_days[i].addActionListener(this); 223 224 group.add(b_days[i]); 225 226 if(++ii > 7) 227 ii = 1; 228 } 229 230 asupport = new ActionSupport(this); 231 232 setLayout(new BorderLayout(5, 5)); 233 234 KPanel top = new KPanel(); 235 top.setLayout(new BorderLayout(0, 0)); 236 237 KPanel p1 = new KPanel(); 238 p1.setLayout(new FlowLayout(FlowLayout.LEFT)); 239 top.add("West", p1); 240 241 b_lmonth = new KButton(KiwiUtils.getResourceManager() 242 .getIcon("repeater-left.gif")); 243 b_lmonth.setMargin(KiwiUtils.emptyInsets); 244 b_lmonth.setFocusPainted(false); 245 b_lmonth.setOpaque(false); 246 b_lmonth.addActionListener(this); 247 p1.add(b_lmonth); 248 249 l_month = new KLabel(); 250 p1.add(l_month); 251 252 b_rmonth = new KButton(KiwiUtils.getResourceManager() 253 .getIcon("repeater-right.gif")); 254 b_rmonth.setMargin(KiwiUtils.emptyInsets); 255 b_rmonth.setFocusPainted(false); 256 b_rmonth.setOpaque(false); 257 b_rmonth.addActionListener(this); 258 p1.add(b_rmonth); 259 260 KPanel p2 = new KPanel(); 261 p2.setLayout(new FlowLayout(FlowLayout.LEFT)); 262 top.add("East" ,p2); 263 264 b_lyear = new KButton(KiwiUtils.getResourceManager() 265 .getIcon("repeater-left.gif")); 266 b_lyear.setMargin(KiwiUtils.emptyInsets); 267 b_lyear.setFocusPainted(false); 268 b_lyear.setOpaque(false); 269 b_lyear.addActionListener(this); 270 p2.add(b_lyear); 271 272 l_year = new KLabel(); 273 p2.add(l_year); 274 275 b_ryear = new KButton(KiwiUtils.getResourceManager() 276 .getIcon("repeater-right.gif")); 277 b_ryear.setMargin(KiwiUtils.emptyInsets); 278 b_ryear.setFocusPainted(false); 279 b_ryear.setOpaque(false); 280 b_ryear.addActionListener(this); 281 p2.add(b_ryear); 282 283 add("North", top); 284 285 KPanel p_grid = new KPanel(); 286 p_grid.setLayout(new BorderLayout(0, 0)); 287 288 KPanel p_headings = new KPanel(); 289 p_headings.setLayout(new GridLayout(0, 7, 1, 1)); 290 291 for(int i = 0; i < l_days.length; i++) 292 p_headings.add(l_days[i]); 293 294 p_grid.add("North", p_headings); 295 296 KPanel p_days = new KPanel(); 297 p_days.setLayout(new GridLayout(0, 7, 1, 1)); 298 299 for(int i = 0; i < b_days.length; i++) 300 p_days.add(b_days[i]); 301 302 p_grid.add("Center", p_days); 303 304 add("Center", p_grid); 305 306 l_date = new KLabel("Date", SwingConstants.CENTER); 307 add("South", l_date); 308 309 Font f = getFont(); 310 super.setFont(new Font(f.getName(), Font.BOLD, f.getSize())); 311 312 setSelectedDate(date); 313 } 314 315 /** Get a copy of the <code>Calendar</code> object that represents the 316 * currently selected date. 317 * 318 * @return The currently selected date. 319 */ 320 321 public Calendar getSelectedDate() 322 { 323 return((Calendar)selectedDate.clone()); 324 } 325 326 /** 327 * Set the selected date for the chooser. 328 * 329 * @param date The date to select. 330 */ 331 332 public void setSelectedDate(Calendar date) 333 { 334 selectedDate = copyDate(date, selectedDate); 335 selectedDay = selectedDate.get(Calendar.DAY_OF_MONTH); 336 337 _refresh(); 338 } 339 340 /** Set the earliest selectable date for the chooser. 341 * 342 * @param date The (possibly <code>null</code>) minimum selectable date. 343 */ 344 345 public void setMinimumDate(Calendar date) 346 { 347 minDate = ((date == null) ? null : copyDate(date, minDate)); 348 minDay = ((date == null) ? -1 : minDate.get(Calendar.DATE)); 349 350 _refresh(); 351 } 352 353 /** Get the earliest selectable date for the chooser. 354 * 355 * @return The minimum selectable date, or <code>null</code> if there is no 356 * minimum date currently set. 357 */ 358 359 public Calendar getMinimumDate() 360 { 361 return(minDate); 362 } 363 364 /** Set the latest selectable date for the chooser. 365 * 366 * @param date The (possibly <code>null</code>) maximum selectable date. 367 */ 368 369 public void setMaximumDate(Calendar date) 370 { 371 maxDate = ((date == null) ? null : copyDate(date, maxDate)); 372 maxDay = ((date == null) ? -1 : maxDate.get(Calendar.DATE)); 373 374 _refresh(); 375 } 376 377 /** Get the latest selectable date for the chooser. 378 * 379 * @return The maximum selectable date, or <code>null</code> if there is no 380 * maximum date currently set. 381 */ 382 383 public Calendar getMaximumDate() 384 { 385 return(maxDate); 386 } 387 388 /** 389 * Set the format for the textual date display at the bottom of the 390 * component. 391 * 392 * @param format The new date format to use. 393 */ 394 395 public void setDateFormat(SimpleDateFormat format) 396 { 397 datefmt = format; 398 399 _refreshDate(); 400 } 401 402 /** Handle events. This method is public as an implementation side-effect. */ 403 404 public void actionPerformed(ActionEvent evt) 405 { 406 Object o = evt.getSource(); 407 408 if(o instanceof JToggleButton) 409 { 410 for(int i = 0; i < b_days.length; i++) 411 { 412 if(b_days[i].isSelected()) 413 { 414 selectedDay = i - firstCell + 1; 415 selectedDate.set(Calendar.DAY_OF_MONTH, selectedDay); 416 asupport.fireActionEvent(DATE_CHANGE_CMD); 417 418 break; 419 } 420 } 421 422 _refreshDate(); 423 424 return; 425 } 426 427 if(o == b_lmonth) 428 selectedDate.add(Calendar.MONTH, -1); 429 430 else if(o == b_rmonth) 431 selectedDate.add(Calendar.MONTH, 1); 432 433 else if(o == b_lyear) 434 { 435 selectedDate.add(Calendar.YEAR, -1); 436 if(minDate != null) 437 { 438 int m = minDate.get(Calendar.MONTH); 439 if(selectedDate.get(Calendar.MONTH) < m) 440 selectedDate.set(Calendar.MONTH, m); 441 } 442 } 443 444 else if(o == b_ryear) 445 { 446 selectedDate.add(Calendar.YEAR, 1); 447 if(maxDate != null) 448 { 449 int m = maxDate.get(Calendar.MONTH); 450 if(selectedDate.get(Calendar.MONTH) > m) 451 selectedDate.set(Calendar.MONTH, m); 452 } 453 } 454 455 selectedDay = 1; 456 selectedDate.set(Calendar.DATE, selectedDay); 457 _refresh(); 458 b_days[selectedDay + firstCell - 1].setSelected(true); 459 460 asupport.fireActionEvent(((o == b_lmonth) || (o == b_rmonth)) 461 ? MONTH_CHANGE_CMD : YEAR_CHANGE_CMD); 462 } 463 464 /* Determine what day of week the first day of the month falls on. It's too 465 * bad we have to resort to this hack; the Java API provides no means of 466 * doing this any other way. 467 */ 468 469 private void _computeFirstDay() 470 { 471 int d = selectedDate.get(Calendar.DAY_OF_MONTH); 472 selectedDate.set(Calendar.DAY_OF_MONTH, 1); 473 firstDay = selectedDate.get(Calendar.DAY_OF_WEEK); 474 selectedDate.set(Calendar.DAY_OF_MONTH, d); 475 } 476 477 private void _refreshDate() 478 { 479 l_date.setText(datefmt.format(selectedDate.getTime())); 480 } 481 482 /* This method is called whenever the month or year changes. Its job 483 * is to reconfigure the grid and determine whether any selection 484 * range limits have been reached. 485 */ 486 487 private void _refresh() 488 { 489 l_year.setText(String.valueOf(selectedDate.get(Calendar.YEAR))); 490 l_month.setText(months[selectedDate.get(Calendar.MONTH)]); 491 492 _computeFirstDay(); 493 clipMin = clipMax = clipAllMin = clipAllMax = false; 494 495 b_lyear.setEnabled(true); 496 b_ryear.setEnabled(true); 497 b_lmonth.setEnabled(true); 498 b_rmonth.setEnabled(true); 499 500 // Disable anything that would cause the date to go out of range. This 501 // logic is extremely sensitive so be very careful when making changes. 502 // Every condition test in here is necessary, so don't remove anything. 503 504 if(minDate != null) 505 { 506 int y = selectedDate.get(Calendar.YEAR); 507 int y0 = minDate.get(Calendar.YEAR); 508 int m = selectedDate.get(Calendar.MONTH); 509 int m0 = minDate.get(Calendar.MONTH); 510 511 b_lyear.setEnabled(y > y0); 512 if(y == y0) 513 { 514 b_lmonth.setEnabled(m > m0); 515 516 if(m == m0) 517 { 518 clipMin = true; 519 int d0 = minDate.get(Calendar.DATE); 520 521 if(selectedDay < d0) 522 selectedDate.set(Calendar.DATE, selectedDay = d0); 523 524 // allow out-of-range selection 525 // selectedDate.set(Calendar.DATE, selectedDay); 526 } 527 } 528 529 clipAllMin = ((m < m0) || (y < y0)); 530 } 531 532 if(maxDate != null) 533 { 534 int y = selectedDate.get(Calendar.YEAR); 535 int y1 = maxDate.get(Calendar.YEAR); 536 int m = selectedDate.get(Calendar.MONTH); 537 int m1 = maxDate.get(Calendar.MONTH); 538 539 b_ryear.setEnabled(y < y1); 540 if(y == y1) 541 { 542 b_rmonth.setEnabled(m < m1); 543 if(m == m1) 544 { 545 clipMax = true; 546 int d1 = maxDate.get(Calendar.DATE); 547 if(selectedDay > d1) 548 selectedDate.set(Calendar.DATE, selectedDay = d1); 549 550 // allow out-of-range selection 551 // selectedDate.set(Calendar.DATE, selectedDay); 552 } 553 } 554 555 // clip if years same but month later, OR if year is later. 556 // fix submitted by Robert Heitzmann. 557 558 clipAllMax = (((y == y1) && (m > m1)) || (y > y1)); 559 } 560 561 // update the grid buttons 562 563 firstCell = ((firstDay - selectedDate.getFirstDayOfWeek() + 7) % 7); 564 565 // find out how many days there are in the current month 566 567 int month = DateChooser.this.selectedDate.get(Calendar.MONTH); 568 int dmax = (isLeapYear(DateChooser.this.selectedDate.get(Calendar.YEAR)) 569 ? daysInMonthLeap[month] : daysInMonth[month]); 570 571 for(int c = 0, ii = 0; c < b_days.length; c++) 572 { 573 if((c < firstCell) || (ii == dmax)) 574 { 575 b_days[c].setText(null); 576 b_days[c].setVisible(false); 577 } 578 else 579 { 580 b_days[c].setText(String.valueOf(++ii)); 581 b_days[c].setVisible(true); 582 583 int d = c - firstCell + 1; 584 585 boolean disabled = ((clipMin && (d < minDay)) 586 || (clipMax && (d > maxDay)) 587 || clipAllMin || clipAllMax); 588 589 b_days[c].setEnabled(! disabled); 590 } 591 } 592 593 _refreshDate(); 594 } 595 596 /** Determine if a year is a leap year. 597 * 598 * @param year The year to check. 599 * @return <code>true</code> if the year is a leap year, and 600 * <code>false</code> otherwise. 601 */ 602 603 public static boolean isLeapYear(int year) 604 { 605 return((((year % 4) == 0) && ((year % 100) != 0)) || ((year % 400) == 0)); 606 } 607 608 /* Copy the relevant portions of a date. */ 609 610 private Calendar copyDate(Calendar source, Calendar dest) 611 { 612 if(dest == null) 613 dest = Calendar.getInstance(); 614 615 dest.set(Calendar.YEAR, source.get(Calendar.YEAR)); 616 dest.set(Calendar.MONTH, source.get(Calendar.MONTH)); 617 dest.set(Calendar.DATE, source.get(Calendar.DATE)); 618 619 return(dest); 620 } 621 622 /** Add a <code>ActionListener</code> to this component's list of listeners. 623 * 624 * @param listener The listener to add. 625 */ 626 627 public void addActionListener(ActionListener listener) 628 { 629 asupport.addActionListener(listener); 630 } 631 632 /** Remove a <code>ActionListener</code> from this component's list of 633 * listeners. 634 * 635 * @param listener The listener to remove. 636 */ 637 638 public void removeActionListener(ActionListener listener) 639 { 640 asupport.removeActionListener(listener); 641 } 642 643 /** Set the size of date cells in the calendar pane. 644 * 645 * @param cellSize The width and height, in pixels, of a cell. 646 * 647 * @since Kiwi 2.0 648 */ 649 650 public void setCellSize(int cellSize) 651 { 652 Dimension dim = new Dimension(cellSize, cellSize); 653 654 for(int i = 0; i < b_days.length; i++) 655 { 656 b_days[i].setSize(dim); 657 b_days[i].setMinimumSize(dim); 658 b_days[i].setPreferredSize(dim); 659 } 660 } 661 662 } 663 664/* end of source file */