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 */