001    /* ========================================================================
002     * JCommon : a free general purpose class library for the Java(tm) platform
003     * ========================================================================
004     *
005     * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
006     * 
007     * Project Info:  http://www.jfree.org/jcommon/index.html
008     *
009     * This library is free software; you can redistribute it and/or modify it 
010     * under the terms of the GNU Lesser General Public License as published by 
011     * the Free Software Foundation; either version 2.1 of the License, or 
012     * (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but 
015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017     * License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this library; if not, write to the Free Software
021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022     * USA.  
023     *
024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025     * in the United States and other countries.]
026     * 
027     * ---------------------
028     * DateChooserPanel.java
029     * ---------------------
030     * (C) Copyright 2000-2004, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   -;
034     *
035     * $Id: DateChooserPanel.java,v 1.11 2007/11/02 17:50:36 taqua Exp $
036     *
037     * Changes (from 26-Oct-2001)
038     * --------------------------
039     * 26-Oct-2001 : Changed package to com.jrefinery.ui.* (DG);
040     * 08-Dec-2001 : Dropped the getMonths() method (DG);
041     * 13-Oct-2002 : Fixed errors reported by Checkstyle (DG);
042     * 02-Nov-2005 : Fixed a bug where the current day-of-the-month is past
043     *               the end of the newly selected month when the month or year
044     *               combo boxes are changed - see bug id 1344319 (DG);
045     *
046     */
047    
048    package org.jfree.ui;
049    
050    import java.awt.BorderLayout;
051    import java.awt.Color;
052    import java.awt.Font;
053    import java.awt.GridLayout;
054    import java.awt.Insets;
055    import java.awt.event.ActionEvent;
056    import java.awt.event.ActionListener;
057    import java.text.DateFormatSymbols;
058    import java.util.Calendar;
059    import java.util.Date;
060    import javax.swing.BorderFactory;
061    import javax.swing.JButton;
062    import javax.swing.JComboBox;
063    import javax.swing.JLabel;
064    import javax.swing.JPanel;
065    import javax.swing.SwingConstants;
066    import javax.swing.UIManager;
067    
068    import org.jfree.date.SerialDate;
069    
070    /**
071     * A panel that allows the user to select a date.
072     *
073     * @author David Gilbert
074     */
075    public class DateChooserPanel extends JPanel implements ActionListener {
076    
077        /**
078         * The date selected in the panel.
079         */
080        private Calendar chosenDate;
081    
082        /**
083         * The color for the selected date.
084         */
085        private Color chosenDateButtonColor;
086    
087        /**
088         * The color for dates in the current month.
089         */
090        private Color chosenMonthButtonColor;
091    
092        /**
093         * The color for dates that are visible, but not in the current month.
094         */
095        private Color chosenOtherButtonColor;
096    
097        /**
098         * The first day-of-the-week.
099         */
100        private int firstDayOfWeek;
101    
102        /**
103         * The range used for selecting years.
104         */
105        private int yearSelectionRange = 20;
106    
107        /**
108         * The font used to display the date.
109         */
110        private Font dateFont = new Font("SansSerif", Font.PLAIN, 10);
111    
112        /**
113         * A combo for selecting the month.
114         */
115        private JComboBox monthSelector;
116    
117        /**
118         * A combo for selecting the year.
119         */
120        private JComboBox yearSelector;
121    
122        /**
123         * A button for selecting today's date.
124         */
125        private JButton todayButton;
126    
127        /**
128         * An array of buttons used to display the days-of-the-month.
129         */
130        private JButton[] buttons;
131    
132        /**
133         * A flag that indicates whether or not we are currently refreshing the 
134         * buttons.
135         */
136        private boolean refreshing = false;
137    
138        /**
139         * The ordered set of all seven days of a week,
140         * beginning with the 'firstDayOfWeek'.
141         */
142        private int[] WEEK_DAYS;
143    
144        /**
145         * Constructs a new date chooser panel, using today's date as the initial 
146         * selection.
147         */
148        public DateChooserPanel() {
149            this(Calendar.getInstance(), false);
150        }
151    
152        /**
153         * Constructs a new date chooser panel.
154         *
155         * @param calendar     the calendar controlling the date.
156         * @param controlPanel a flag that indicates whether or not the 'today' 
157         *                     button should appear on the panel.
158         */
159        public DateChooserPanel(final Calendar calendar, 
160                                final boolean controlPanel) {
161    
162            super(new BorderLayout());
163    
164            this.chosenDateButtonColor = UIManager.getColor("textHighlight");
165            this.chosenMonthButtonColor = UIManager.getColor("control");
166            this.chosenOtherButtonColor = UIManager.getColor("controlShadow");
167    
168            // the default date is today...
169            this.chosenDate = calendar;
170            this.firstDayOfWeek = calendar.getFirstDayOfWeek();
171            this.WEEK_DAYS = new int[7];
172            for (int i = 0; i < 7; i++) {
173                this.WEEK_DAYS[i] = ((this.firstDayOfWeek + i - 1) % 7) + 1;
174            }
175    
176            add(constructSelectionPanel(), BorderLayout.NORTH);
177            add(getCalendarPanel(), BorderLayout.CENTER);
178            if (controlPanel) {
179                add(constructControlPanel(), BorderLayout.SOUTH);
180            }
181            setDate(calendar.getTime());
182        }
183    
184        /**
185         * Sets the date chosen in the panel.
186         *
187         * @param theDate the new date.
188         */
189        public void setDate(final Date theDate) {
190    
191            this.chosenDate.setTime(theDate);
192            this.monthSelector.setSelectedIndex(this.chosenDate.get(
193                    Calendar.MONTH));
194            refreshYearSelector();
195            refreshButtons();
196    
197        }
198    
199        /**
200         * Returns the date selected in the panel.
201         *
202         * @return the selected date.
203         */
204        public Date getDate() {
205            return this.chosenDate.getTime();
206        }
207    
208        /**
209         * Handles action-events from the date panel.
210         *
211         * @param e information about the event that occurred.
212         */
213        public void actionPerformed(final ActionEvent e) {
214    
215            if (e.getActionCommand().equals("monthSelectionChanged")) {
216                final JComboBox c = (JComboBox) e.getSource();
217                
218                // In most cases, changing the month will not change the selected
219                // day.  But if the selected day is 29, 30 or 31 and the newly
220                // selected month doesn't have that many days, we revert to the 
221                // last day of the newly selected month...
222                int dayOfMonth = this.chosenDate.get(Calendar.DAY_OF_MONTH);
223                this.chosenDate.set(Calendar.DAY_OF_MONTH, 1);
224                this.chosenDate.set(Calendar.MONTH, c.getSelectedIndex());
225                int maxDayOfMonth = this.chosenDate.getActualMaximum(
226                        Calendar.DAY_OF_MONTH);
227                this.chosenDate.set(Calendar.DAY_OF_MONTH, Math.min(dayOfMonth, 
228                        maxDayOfMonth));
229                refreshButtons();
230            }
231            else if (e.getActionCommand().equals("yearSelectionChanged")) {
232                if (!this.refreshing) {
233                    final JComboBox c = (JComboBox) e.getSource();
234                    final Integer y = (Integer) c.getSelectedItem();
235                    
236                    // in most cases, changing the year will not change the 
237                    // selected day.  But if the selected day is Feb 29, and the
238                    // newly selected year is not a leap year, we revert to 
239                    // Feb 28...
240                    int dayOfMonth = this.chosenDate.get(Calendar.DAY_OF_MONTH);
241                    this.chosenDate.set(Calendar.DAY_OF_MONTH, 1);
242                    this.chosenDate.set(Calendar.YEAR, y.intValue());
243                    int maxDayOfMonth = this.chosenDate.getActualMaximum(
244                        Calendar.DAY_OF_MONTH);
245                    this.chosenDate.set(Calendar.DAY_OF_MONTH, Math.min(dayOfMonth, 
246                        maxDayOfMonth));
247                    refreshYearSelector();
248                    refreshButtons();
249                }
250            }
251            else if (e.getActionCommand().equals("todayButtonClicked")) {
252                setDate(new Date());
253            }
254            else if (e.getActionCommand().equals("dateButtonClicked")) {
255                final JButton b = (JButton) e.getSource();
256                final int i = Integer.parseInt(b.getName());
257                final Calendar cal = getFirstVisibleDate();
258                cal.add(Calendar.DATE, i);
259                setDate(cal.getTime());
260            }
261        }
262    
263        /**
264         * Returns a panel of buttons, each button representing a day in the month.
265         * This is a sub-component of the DatePanel.
266         *
267         * @return the panel.
268         */
269        private JPanel getCalendarPanel() {
270    
271            final JPanel p = new JPanel(new GridLayout(7, 7));
272            final DateFormatSymbols dateFormatSymbols = new DateFormatSymbols();
273            final String[] weekDays = dateFormatSymbols.getShortWeekdays();
274    
275            for (int i = 0; i < this.WEEK_DAYS.length; i++) {
276                p.add(new JLabel(weekDays[this.WEEK_DAYS[i]], 
277                        SwingConstants.CENTER));
278            }
279    
280            this.buttons = new JButton[42];
281            for (int i = 0; i < 42; i++) {
282                final JButton b = new JButton("");
283                b.setMargin(new Insets(1, 1, 1, 1));
284                b.setName(Integer.toString(i));
285                b.setFont(this.dateFont);
286                b.setFocusPainted(false);
287                b.setActionCommand("dateButtonClicked");
288                b.addActionListener(this);
289                this.buttons[i] = b;
290                p.add(b);
291            }
292            return p;
293    
294        }
295    
296        /**
297         * Returns the button color according to the specified date.
298         *
299         * @param theDate the date.
300         * @return the color.
301         */
302        private Color getButtonColor(final Calendar theDate) {
303            if (equalDates(theDate, this.chosenDate)) {
304                return this.chosenDateButtonColor;
305            }
306            else if (theDate.get(Calendar.MONTH) == this.chosenDate.get(
307                    Calendar.MONTH)) {
308                return this.chosenMonthButtonColor;
309            }
310            else {
311                return this.chosenOtherButtonColor;
312            }
313        }
314    
315        /**
316         * Returns true if the two dates are equal (time of day is ignored).
317         *
318         * @param c1 the first date.
319         * @param c2 the second date.
320         * @return boolean.
321         */
322        private boolean equalDates(final Calendar c1, final Calendar c2) {
323            if ((c1.get(Calendar.DATE) == c2.get(Calendar.DATE))
324                && (c1.get(Calendar.MONTH) == c2.get(Calendar.MONTH))
325                && (c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR))) {
326                return true;
327            }
328            else {
329                return false;
330            }
331        }
332    
333        /**
334         * Returns the first date that is visible in the grid.  This should always 
335         * be in the month preceding the month of the selected date.
336         *
337         * @return the date.
338         */
339        private Calendar getFirstVisibleDate() {
340            final Calendar c = Calendar.getInstance();
341            c.set(this.chosenDate.get(Calendar.YEAR), this.chosenDate.get(
342                    Calendar.MONTH), 1);
343            c.add(Calendar.DATE, -1);
344            while (c.get(Calendar.DAY_OF_WEEK) != getFirstDayOfWeek()) {
345                c.add(Calendar.DATE, -1);
346            }
347            return c;
348        }
349    
350        /**
351         * Returns the first day of the week (controls the labels in the date 
352         * panel).
353         *
354         * @return the first day of the week.
355         */
356        private int getFirstDayOfWeek() {
357            return this.firstDayOfWeek;
358        }
359    
360        /**
361         * Update the button labels and colors to reflect date selection.
362         */
363        private void refreshButtons() {
364            final Calendar c = getFirstVisibleDate();
365            for (int i = 0; i < 42; i++) {
366                final JButton b = this.buttons[i];
367                b.setText(Integer.toString(c.get(Calendar.DATE)));
368                b.setBackground(getButtonColor(c));
369                c.add(Calendar.DATE, 1);
370            }
371        }
372    
373        /**
374         * Changes the contents of the year selection JComboBox to reflect the 
375         * chosen date and the year range.
376         */
377        private void refreshYearSelector() {
378            if (!this.refreshing) {
379                this.refreshing = true;
380                this.yearSelector.removeAllItems();
381                final Integer[] years = getYears(this.chosenDate.get(
382                        Calendar.YEAR));
383                for (int i = 0; i < years.length; i++) {
384                    this.yearSelector.addItem(years[i]);
385                }
386                this.yearSelector.setSelectedItem(new Integer(this.chosenDate.get(
387                        Calendar.YEAR)));
388                this.refreshing = false;
389            }
390        }
391    
392        /**
393         * Returns a vector of years preceding and following the specified year.  
394         * The number of years preceding and following is determined by the 
395         * yearSelectionRange attribute.
396         *
397         * @param chosenYear the selected year.
398         * @return a vector of years.
399         */
400        private Integer[] getYears(final int chosenYear) {
401            final int size = this.yearSelectionRange * 2 + 1;
402            final int start = chosenYear - this.yearSelectionRange;
403    
404            final Integer[] years = new Integer[size];
405            for (int i = 0; i < size; i++) {
406                years[i] = new Integer(i + start);
407            }
408            return years;
409        }
410    
411        /**
412         * Constructs a panel containing two JComboBoxes (for the month and year) 
413         * and a button (to reset the date to TODAY).
414         *
415         * @return the panel.
416         */
417        private JPanel constructSelectionPanel() {
418            final JPanel p = new JPanel();
419    
420            final int minMonth = this.chosenDate.getMinimum(Calendar.MONTH);
421            final int maxMonth = this.chosenDate.getMaximum(Calendar.MONTH);
422            final String[] months = new String[maxMonth - minMonth + 1];
423            System.arraycopy(SerialDate.getMonths(), minMonth, months, 0, 
424                    months.length);
425    
426            this.monthSelector = new JComboBox(months);
427            this.monthSelector.addActionListener(this);
428            this.monthSelector.setActionCommand("monthSelectionChanged");
429            p.add(this.monthSelector);
430    
431            this.yearSelector = new JComboBox(getYears(0));
432            this.yearSelector.addActionListener(this);
433            this.yearSelector.setActionCommand("yearSelectionChanged");
434            p.add(this.yearSelector);
435    
436            return p;
437        }
438    
439        /**
440         * Returns a panel that appears at the bottom of the calendar panel - 
441         * contains a button for selecting today's date.
442         *
443         * @return the panel.
444         */
445        private JPanel constructControlPanel() {
446    
447            final JPanel p = new JPanel();
448            p.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5));
449            this.todayButton = new JButton("Today");
450            this.todayButton.addActionListener(this);
451            this.todayButton.setActionCommand("todayButtonClicked");
452            p.add(this.todayButton);
453            return p;
454    
455        }
456    
457        /**
458         * Returns the color for the currently selected date.
459         *
460         * @return a color.
461         */
462        public Color getChosenDateButtonColor() {
463            return this.chosenDateButtonColor;
464        }
465    
466        /**
467         * Redefines the color for the currently selected date.
468         *
469         * @param chosenDateButtonColor the new color
470         */
471        public void setChosenDateButtonColor(final Color chosenDateButtonColor) {
472            if (chosenDateButtonColor == null) {
473                throw new NullPointerException("UIColor must not be null.");
474            }
475            final Color oldValue = this.chosenDateButtonColor;
476            this.chosenDateButtonColor = chosenDateButtonColor;
477            refreshButtons();
478            firePropertyChange("chosenDateButtonColor", oldValue, 
479                    chosenDateButtonColor);
480        }
481    
482        /**
483         * Returns the color for the buttons representing the current month.
484         *
485         * @return the color for the current month.
486         */
487        public Color getChosenMonthButtonColor() {
488            return this.chosenMonthButtonColor;
489        }
490    
491        /**
492         * Defines the color for the buttons representing the current month.
493         *
494         * @param chosenMonthButtonColor the color for the current month.
495         */
496        public void setChosenMonthButtonColor(final Color chosenMonthButtonColor) {
497            if (chosenMonthButtonColor == null) {
498                throw new NullPointerException("UIColor must not be null.");
499            }
500            final Color oldValue = this.chosenMonthButtonColor;
501            this.chosenMonthButtonColor = chosenMonthButtonColor;
502            refreshButtons();
503            firePropertyChange("chosenMonthButtonColor", oldValue, 
504                    chosenMonthButtonColor);
505        }
506    
507        /**
508         * Returns the color for the buttons representing the other months.
509         *
510         * @return a color.
511         */
512        public Color getChosenOtherButtonColor() {
513            return this.chosenOtherButtonColor;
514        }
515    
516        /**
517         * Redefines the color for the buttons representing the other months.
518         *
519         * @param chosenOtherButtonColor a color.
520         */
521        public void setChosenOtherButtonColor(final Color chosenOtherButtonColor) {
522            if (chosenOtherButtonColor == null) {
523                throw new NullPointerException("UIColor must not be null.");
524            }
525            final Color oldValue = this.chosenOtherButtonColor;
526            this.chosenOtherButtonColor = chosenOtherButtonColor;
527            refreshButtons();
528            firePropertyChange("chosenOtherButtonColor", oldValue, 
529                    chosenOtherButtonColor);
530        }
531    
532        /**
533         * Returns the range of years available for selection (defaults to 20).
534         * 
535         * @return The range.
536         */
537        public int getYearSelectionRange() {
538            return this.yearSelectionRange;
539        }
540    
541        /**
542         * Sets the range of years available for selection.
543         * 
544         * @param yearSelectionRange  the range.
545         */
546        public void setYearSelectionRange(final int yearSelectionRange) {
547            final int oldYearSelectionRange = this.yearSelectionRange;
548            this.yearSelectionRange = yearSelectionRange;
549            refreshYearSelector();
550            firePropertyChange("yearSelectionRange", oldYearSelectionRange, 
551                    yearSelectionRange);
552        }
553    }