001: //The contents of this file are subject to the Mozilla Public License Version 1.1
002: //(the "License"); you may not use this file except in compliance with the
003: //License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
004: //
005: //Software distributed under the License is distributed on an "AS IS" basis,
006: //WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
007: //for the specific language governing rights and
008: //limitations under the License.
009: //
010: //The Original Code is "The Columba Project"
011: //
012: //The Initial Developers of the Original Code are Frederik Dietz and Timo Stich.
013: //Portions created by Frederik Dietz and Timo Stich are Copyright (C) 2003.
014: //
015: //All Rights Reserved.
016: package org.columba.core.gui.base;
017:
018: import java.awt.BorderLayout;
019: import java.awt.Color;
020: import java.awt.Dimension;
021: import java.awt.FontMetrics;
022: import java.awt.Graphics;
023: import java.awt.Insets;
024: import java.awt.event.ActionEvent;
025: import java.awt.event.ActionListener;
026: import java.awt.event.MouseAdapter;
027: import java.awt.event.MouseEvent;
028: import java.text.DateFormatSymbols;
029: import java.text.SimpleDateFormat;
030: import java.util.Calendar;
031:
032: import javax.swing.BorderFactory;
033: import javax.swing.JButton;
034: import javax.swing.JComponent;
035: import javax.swing.JLabel;
036: import javax.swing.JPanel;
037: import javax.swing.UIManager;
038:
039: /**
040: * taken some code from the Kiwi Toolkit: http://www.dystance.net/ping/kiwi/
041: * author: Mark Lindner
042: *
043: */
044:
045: public class DateChooser extends JPanel implements ActionListener {
046: private static final int cellSize = 22;
047:
048: private static final int[] daysInMonth = { 31, 28, 31, 30, 31, 30,
049: 31, 31, 30, 31, 30, 31 };
050:
051: private static final int[] daysInMonthLeap = { 31, 29, 31, 30, 31,
052: 30, 31, 31, 30, 31, 30, 31 };
053:
054: private static final Color weekendColor = Color.red.darker();
055:
056: // private ActionSupport asupport;
057:
058: /** <i>Date changed</i> event command. */
059: public static final String DATE_CHANGE_CMD = "dateChanged";
060:
061: /** <i>Month changed</i> event command. */
062: public static final String MONTH_CHANGE_CMD = "monthChanged";
063:
064: /** <i>Year changed</i> event command. */
065: public static final String YEAR_CHANGE_CMD = "yearChanged";
066:
067: CalendarPane calendarPane;
068:
069: // private JLabel l_date, l_year, l_month;
070: private JLabel l_date;
071:
072: // private JLabel l_date, l_year, l_month;
073: private JLabel l_month;
074:
075: // private JButton b_lyear, b_ryear, b_lmonth, b_rmonth;
076: private JButton b_lmonth;
077:
078: // private JButton b_lyear, b_ryear, b_lmonth, b_rmonth;
079: private JButton b_rmonth;
080:
081: private SimpleDateFormat datefmt = new SimpleDateFormat(
082: "E d MMM yyyy");
083:
084: private Calendar selectedDate = null;
085:
086: private Calendar minDate = null;
087:
088: private Calendar maxDate = null;
089:
090: private int selectedDay;
091:
092: private int firstDay;
093:
094: private int minDay = -1;
095:
096: private int maxDay = -1;
097:
098: private String[] months;
099:
100: private String[] labels = new String[7];
101:
102: private Color highlightColor;
103:
104: private Color disabledColor;
105:
106: private boolean clipMin = false;
107:
108: private boolean clipMax = false;
109:
110: private boolean clipAllMin = false;
111:
112: private boolean clipAllMax = false;
113:
114: private int[] weekendCols = { 0, 0 };
115:
116: /**
117: * Construct a new <code>DateChooser</code>. The selection will be
118: * initialized to the current date.
119: */
120: public DateChooser() {
121: this (Calendar.getInstance());
122: }
123:
124: /**
125: * Construct a new <code>DateChooser</code> with the specified selected
126: * date.
127: *
128: * @param <code>date</code> The date for the selection.
129: */
130: public DateChooser(Calendar date) {
131: // asupport = new ActionSupport(this);
132: DateFormatSymbols sym = new DateFormatSymbols();
133:
134: months = sym.getShortMonths();
135:
136: String[] wkd = sym.getShortWeekdays();
137:
138: for (int i = 0; i < 7; i++) {
139: int l = Math.min(wkd[i + 1].length(), 2);
140: labels[i] = wkd[i + 1].substring(0, l);
141: }
142:
143: // Let's at least make a half-assed attempt at conforming to the Metal
144: // PLAF colors.
145: highlightColor = UIManager.getColor("List.selectionBackground");
146: disabledColor = Color.red;
147:
148: setBorder(BorderFactory.createEtchedBorder());
149:
150: setLayout(new BorderLayout(5, 5));
151:
152: JPanel top = new JPanel();
153: top.setLayout(new BorderLayout(0, 0));
154: top.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
155:
156: // top.setBorder( BorderFactory.createEtchedBorder() );
157: JPanel p1 = new JPanel();
158: p1.setLayout(new BorderLayout());
159: top.add(p1, BorderLayout.CENTER);
160:
161: b_lmonth = new JButton("<");
162: b_lmonth.addActionListener(this );
163: b_lmonth.setMargin(new Insets(0, 0, 0, 0));
164:
165: // b_lmonth.setFocusPainted(false);
166: // b_lmonth.setOpaque(false);
167: // b_lmonth.addActionListener(this);
168: p1.add(b_lmonth, BorderLayout.WEST);
169:
170: l_month = new JLabel();
171:
172: // p1.add(l_month);
173: /*
174: * l_year = new JLabel(); p1.add(l_year);
175: */
176: l_date = new JLabel("Date");
177: l_date.setAlignmentX(0);
178: p1.add(l_date, BorderLayout.CENTER);
179:
180: b_rmonth = new JButton(">");
181: b_rmonth.addActionListener(this );
182: b_rmonth.setMargin(new Insets(0, 0, 0, 0));
183:
184: // b_rmonth.setFocusPainted(false);
185: // b_rmonth.setOpaque(false);
186: // b_rmonth.addActionListener(this);
187: p1.add(b_rmonth, BorderLayout.EAST);
188:
189: /*
190: * JPanel p2 = new JPanel(); p2.setLayout(new
191: * FlowLayout(FlowLayout.LEFT)); top.add("East", p2);
192: *
193: * b_lyear = new JButton("<"); b_lyear.addActionListener( this );
194: * //b_lyear.setMargin(KiwiUtils.emptyInsets);
195: * b_lyear.setFocusPainted(false); b_lyear.setOpaque(false);
196: * //b_lyear.addActionListener(this); p2.add(b_lyear);
197: *
198: * l_year = new JLabel(); p2.add(l_year);
199: *
200: * b_ryear = new JButton(">"); b_ryear.addActionListener( this );
201: * //b_ryear.setMargin(KiwiUtils.emptyInsets);
202: * b_ryear.setFocusPainted(false); b_ryear.setOpaque(false);
203: * //b_ryear.addActionListener(this); p2.add(b_ryear);
204: */
205: add("North", top);
206:
207: calendarPane = new CalendarPane();
208: calendarPane.setOpaque(false);
209: add("Center", calendarPane);
210:
211: /*
212: * Font f = getFont(); setFont(new Font(f.getName(), Font.BOLD,
213: * f.getSize()));
214: */
215: int fd = date.getFirstDayOfWeek();
216: weekendCols[0] = (Calendar.SUNDAY - fd + 7) % 7;
217: weekendCols[1] = (Calendar.SATURDAY - fd + 7) % 7;
218:
219: setSelectedDate(date);
220: }
221:
222: public static boolean isLeapYear(int year) {
223: return ((((year % 4) == 0) && ((year % 100) != 0)) || ((year % 400) == 0));
224: }
225:
226: /* Copy the relevant portions of a date. */
227: private Calendar copyDate(Calendar source, Calendar dest) {
228: if (dest == null) {
229: dest = Calendar.getInstance();
230: }
231:
232: dest.set(Calendar.YEAR, source.get(Calendar.YEAR));
233: dest.set(Calendar.MONTH, source.get(Calendar.MONTH));
234: dest.set(Calendar.DATE, source.get(Calendar.DATE));
235:
236: return (dest);
237: }
238:
239: /**
240: * Add a <code>ActionListener</code> to this component's list of
241: * listeners.
242: *
243: * @param listener
244: * The listener to add.
245: */
246: public void addActionListener(ActionListener listener) {
247: // asupport.addActionListener(listener);
248: }
249:
250: /**
251: * Remove a <code>ActionListener</code> from this component's list of
252: * listeners.
253: *
254: * @param listener
255: * The listener to remove.
256: */
257: public void removeActionListener(ActionListener listener) {
258: // asupport.removeActionListener(listener);
259: }
260:
261: /**
262: * Set the highlight color for this component.
263: *
264: * @param color
265: * The new highlight color.
266: */
267: public void setHighlightColor(Color color) {
268: highlightColor = color;
269: }
270:
271: /**
272: * Get the highlight color for this component.
273: *
274: * @return The current highlight color.
275: */
276: public Color getHighlightColor() {
277: return (highlightColor);
278: }
279:
280: /**
281: * Get a copy of the <code>Calendar</code> object that represents the
282: * currently selected date.
283: *
284: * @return The currently selected date.
285: */
286: public Calendar getSelectedDate() {
287: return ((Calendar) selectedDate.clone());
288: }
289:
290: /**
291: * Set the selected date for the chooser.
292: *
293: * @param date
294: * The date to select.
295: */
296: public void setSelectedDate(Calendar date) {
297: selectedDate = copyDate(date, selectedDate);
298: selectedDay = selectedDate.get(Calendar.DAY_OF_MONTH);
299:
300: _refresh();
301: }
302:
303: /**
304: * Set the earliest selectable date for the chooser.
305: *
306: * @param date
307: * The (possibly <code>null</code>) minimum selectable date.
308: */
309: public void setMinimumDate(Calendar date) {
310: minDate = ((date == null) ? null : copyDate(date, minDate));
311: minDay = ((date == null) ? (-1) : minDate.get(Calendar.DATE));
312:
313: _refresh();
314: }
315:
316: /**
317: * Get the earliest selectable date for the chooser.
318: *
319: * @return The minimum selectable date, or <code>null</code> if there is
320: * no minimum date currently set.
321: */
322: public Calendar getMinimumDate() {
323: return (minDate);
324: }
325:
326: /**
327: * Set the latest selectable date for the chooser.
328: *
329: * @param date
330: * The (possibly <code>null</code>) maximum selectable date.
331: */
332: public void setMaximumDate(Calendar date) {
333: maxDate = ((date == null) ? null : copyDate(date, maxDate));
334: maxDay = ((date == null) ? (-1) : maxDate.get(Calendar.DATE));
335:
336: _refresh();
337: }
338:
339: /**
340: * Get the latest selectable date for the chooser.
341: *
342: * @return The maximum selectable date, or <code>null</code> if there is
343: * no maximum date currently set.
344: */
345: public Calendar getMaximumDate() {
346: return (maxDate);
347: }
348:
349: /**
350: * Set the format for the textual date display at the bottom of the
351: * component.
352: *
353: * @param <code>format</code> The new date format to use.
354: */
355: public void setDateFormat(SimpleDateFormat format) {
356: datefmt = format;
357:
358: _refresh();
359: }
360:
361: /** Handle events. This method is public as an implementation side-effect. */
362: public void actionPerformed(ActionEvent evt) {
363: Object o = evt.getSource();
364:
365: if (o == b_lmonth) {
366: selectedDate.add(Calendar.MONTH, -1);
367: } else if (o == b_rmonth) {
368: selectedDate.add(Calendar.MONTH, 1);
369: }
370:
371: /*
372: * else if (o == b_lyear) { selectedDate.add(Calendar.YEAR, -1); if
373: * (minDate != null) { int m = minDate.get(Calendar.MONTH); if
374: * (selectedDate.get(Calendar.MONTH) < m)
375: * selectedDate.set(Calendar.MONTH, m); } }
376: *
377: * else if (o == b_ryear) { selectedDate.add(Calendar.YEAR, 1); if
378: * (maxDate != null) { int m = maxDate.get(Calendar.MONTH); if
379: * (selectedDate.get(Calendar.MONTH) > m)
380: * selectedDate.set(Calendar.MONTH, m); } }
381: */
382: selectedDay = 1;
383: selectedDate.set(Calendar.DATE, selectedDay);
384:
385: _refresh();
386:
387: /*
388: * asupport.fireActionEvent(((o == b_lmonth) || (o == b_rmonth)) ?
389: * MONTH_CHANGE_CMD : YEAR_CHANGE_CMD);
390: */
391: }
392:
393: /*
394: * Determine what day of week the first day of the month falls on. It's too
395: * bad we have to resort to this hack; the Java API provides no means of
396: * doing this any other way.
397: */
398: private void _computeFirstDay() {
399: int d = selectedDate.get(Calendar.DAY_OF_MONTH);
400: selectedDate.set(Calendar.DAY_OF_MONTH, 1);
401: firstDay = selectedDate.get(Calendar.DAY_OF_WEEK);
402: selectedDate.set(Calendar.DAY_OF_MONTH, d);
403: }
404:
405: /*
406: * This method is called whenever the month or year changes. It's job is to
407: * repaint the labels and determine whether any selection range limits have
408: * been reached.
409: */
410: private void _refresh() {
411: l_date.setText(datefmt.format(selectedDate.getTime()));
412:
413: // l_year.setText(String.valueOf(selectedDate.get(Calendar.YEAR)));
414: l_month.setText(months[selectedDate.get(Calendar.MONTH)]);
415:
416: _computeFirstDay();
417: clipMin = clipMax = clipAllMin = clipAllMax = false;
418:
419: // b_lyear.setEnabled(true);
420: // b_ryear.setEnabled(true);
421: b_lmonth.setEnabled(true);
422: b_rmonth.setEnabled(true);
423:
424: // Disable anything that would cause the date to go out of range. This
425: // logic is extremely sensitive so be very careful when making changes.
426: // Every condition test in here is necessary, so don't remove anything.
427: if (minDate != null) {
428: int y = selectedDate.get(Calendar.YEAR);
429: int y0 = minDate.get(Calendar.YEAR);
430: int m = selectedDate.get(Calendar.MONTH);
431: int m0 = minDate.get(Calendar.MONTH);
432:
433: // b_lyear.setEnabled(y > y0);
434: if (y == y0) {
435: b_lmonth.setEnabled(m > m0);
436:
437: if (m == m0) {
438: clipMin = true;
439:
440: int d0 = minDate.get(Calendar.DATE);
441:
442: if (selectedDay < d0) {
443: selectedDate.set(Calendar.DATE,
444: selectedDay = d0);
445: }
446:
447: // allow out-of-range selection
448: // selectedDate.set(Calendar.DATE, selectedDay);
449: }
450: }
451:
452: clipAllMin = ((m < m0) || (y < y0));
453: }
454:
455: if (maxDate != null) {
456: int y = selectedDate.get(Calendar.YEAR);
457: int y1 = maxDate.get(Calendar.YEAR);
458: int m = selectedDate.get(Calendar.MONTH);
459: int m1 = maxDate.get(Calendar.MONTH);
460:
461: // b_ryear.setEnabled(y < y1);
462: if (y == y1) {
463: b_rmonth.setEnabled(m < m1);
464:
465: if (m == m1) {
466: clipMax = true;
467:
468: int d1 = maxDate.get(Calendar.DATE);
469:
470: if (selectedDay > d1) {
471: selectedDate.set(Calendar.DATE,
472: selectedDay = d1);
473: }
474:
475: // allow out-of-range selection
476: // selectedDate.set(Calendar.DATE, selectedDay);
477: }
478: }
479:
480: clipAllMax = ((m > m1) || (y > y1));
481: }
482:
483: // repaint the calendar pane
484: calendarPane.repaint();
485: }
486:
487: private class CalendarPane extends JComponent {
488:
489: private int dp = 0;
490:
491: private int x0 = 0;
492:
493: private int y0 = 0;
494:
495: /** Construct a new <code>CalendarView</code>. */
496: CalendarPane() {
497: addMouseListener(new _MouseListener2());
498: }
499:
500: /** Paint the component. */
501: public void paint(Graphics gc) {
502: gc.setFont(UIManager.getFont("Label.font"));
503:
504: FontMetrics fm = gc.getFontMetrics();
505: Insets ins = getInsets();
506: int h = fm.getMaxAscent();
507:
508: gc.setColor(Color.white);
509: gc.fillRect(0, 0, getSize().width, getSize().height);
510:
511: // figure out how many blank spaces there are before first day of
512: // month,
513: // and calculate coordinates of first drawn cell
514: dp = ((firstDay - selectedDate.getFirstDayOfWeek() + 7) % 7);
515:
516: int x = dp;
517: int y = 0;
518: y0 = ((getSize().height - getPreferredSize().height) / 2);
519:
520: int yp = y0;
521: x0 = ((getSize().width - getPreferredSize().width) / 2);
522:
523: int xp = x0;
524:
525: // paint the border
526: paintBorder(gc);
527:
528: // set the clip rect to exclude the border & insets
529: gc.setColor(Color.black);
530: gc.clipRect(ins.left, ins.top,
531: (getSize().width - ins.left - ins.right),
532: (getSize().height - ins.top - ins.bottom));
533: gc.translate(ins.left, ins.top);
534:
535: // draw the weekday headings
536: for (int i = 0, ii = selectedDate.getFirstDayOfWeek() - 1; i < 7; i++) {
537: gc.drawString(labels[ii],
538: xp + 5 + (i * (cellSize + 2)), yp + h);
539:
540: if (++ii == 7) {
541: ii = 0;
542: }
543: }
544:
545: yp += 20;
546: xp += (dp * (cellSize + 2));
547:
548: // find out how many days there are in the current month
549: int month = DateChooser.this .selectedDate
550: .get(Calendar.MONTH);
551: int dmax = (isLeapYear(DateChooser.this .selectedDate
552: .get(Calendar.YEAR)) ? daysInMonthLeap[month]
553: : daysInMonth[month]);
554:
555: // draw all the day cells
556: for (int d = 1; d <= dmax; d++) {
557: // draw the outline of the cell
558: // gc.setColor(MetalLookAndFeel.getPrimaryControlShadow());
559: gc.setColor(Color.gray);
560:
561: // gc.draw3DRect(xp, yp, cellSize, cellSize, true);
562: // if the cell is selected, fill it with the highlight color
563: if (d == selectedDay) {
564: gc.setColor(highlightColor);
565: gc.fillRect(xp + 1, yp + 1, cellSize - 2,
566: cellSize - 2);
567: }
568:
569: // set the pen color depending on weekday or weekend, and paint
570: // the
571: // day number in the cell
572: if ((clipMin && (d < minDay))
573: || (clipMax && (d > maxDay)) || clipAllMin
574: || clipAllMax) {
575: gc.setColor(disabledColor);
576: } else {
577: gc
578: .setColor(((weekendCols[0] == x) || (weekendCols[1] == x)) ? weekendColor
579: : Color.black);
580: }
581:
582: String ss = String.valueOf(d);
583: int sw = fm.stringWidth(ss);
584:
585: if (d == selectedDay) {
586: gc.setColor(UIManager
587: .getColor("List.selectionForeground"));
588: }
589:
590: gc.drawString(ss, xp - 3 + (cellSize - sw), yp + 3 + h);
591:
592: // advance to the next cell position
593: if (++x == 7) {
594: x = 0;
595: xp = x0;
596: y++;
597: yp += (cellSize + 2);
598: } else {
599: xp += (cellSize + 2);
600: }
601: }
602: }
603:
604: /* Get the preferred size of the component. */
605: public Dimension getPreferredSize() {
606: Insets ins = getInsets();
607:
608: return (new Dimension(
609: (((cellSize + 2) * 7) + ins.left + ins.right),
610: (((cellSize + 2) * 6) + 20) + ins.top + ins.bottom));
611: }
612:
613: /* Get the minimum size of the component. */
614: public Dimension getMinimumSize() {
615: return (getPreferredSize());
616: }
617:
618: /* Figure out which day the mouse click is on. */
619: private int getDay(MouseEvent evt) {
620: Insets ins = getInsets();
621:
622: int x = evt.getX() - ins.left - x0;
623: int y = evt.getY() - ins.top - 20 - y0;
624: int maxw = (cellSize + 2) * 7;
625: int maxh = (cellSize + 2) * 6;
626:
627: // check if totally out of range.
628: if ((x < 0) || (x > maxw) || (y < 0) || (y > maxh)) {
629: return (-1);
630: }
631:
632: y /= (cellSize + 2);
633: x /= (cellSize + 2);
634:
635: int d = ((7 * y) + x) - (dp - 1);
636:
637: if ((d < 1)
638: || (d > selectedDate
639: .getMaximum(Calendar.DAY_OF_MONTH))) {
640: return (-1);
641: }
642:
643: if ((clipMin && (d < minDay)) || (clipMax && (d > maxDay))) {
644: return (-1);
645: }
646:
647: return (d);
648: }
649:
650: /* mouse listener */
651: private class _MouseListener2 extends MouseAdapter {
652: public void mouseReleased(MouseEvent evt) {
653: int d = getDay(evt);
654:
655: if (d < 0) {
656: return;
657: }
658:
659: selectedDay = d;
660:
661: selectedDate.set(Calendar.DAY_OF_MONTH, selectedDay);
662: _refresh();
663:
664: // asupport.fireActionEvent(DATE_CHANGE_CMD);
665: }
666: }
667: }
668: }
|