/* *------------------- * The JXMonthView.java is part of ASH Viewer *------------------- * * ASH Viewer is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ASH Viewer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ASH Viewer. If not, see <http://www.gnu.org/licenses/>. * * Copyright (c) 2009, Alex Kardapolov, All rights reserved. * */ package org.ash.history.period; import java.awt.*; import java.awt.event.*; import java.text.SimpleDateFormat; import java.text.DateFormatSymbols; import java.util.Calendar; import java.util.Date; import java.util.Hashtable; import java.util.TimeZone; import javax.swing.*; import javax.swing.border.Border; import org.ash.history.period.DateUtils; /** * Component that displays a month calendar which can be used to select a day * or range of days. By default the <code>JXMonthView</code> will display a * single calendar using the current month and year, using * <code>Calendar.SUNDAY</code> as the first day of the week. * <p> * The <code>JXMonthView</code> can be configured to display more than one * calendar at a time by calling * <code>setPreferredCalCols</code>/<code>setPreferredCalRows</code>. These * methods will set the preferred number of calendars to use in each * column/row. As these values change, the <code>Dimension</code> returned * from <code>getMinimumSize</code> and <code>getPreferredSize</code> will * be updated. The following example shows how to create a 2x2 view which is * contained within a <code>JFrame</code>: * <pre> * JXMonthView monthView = new JXMonthView(); * monthView.setPreferredCols(2); * monthView.setPreferredRows(2); * * JFrame frame = new JFrame(); * frame.getContentPane().add(monthView); * frame.pack(); * frame.setVisible(true); * </pre> * <p> * <code>JXMonthView</code> can be further configured to allow any day of the * week to be considered the first day of the week. Character * representation of those days may also be set by providing an array of * strings. * <pre> * monthView.setFirstDayOfWeek(Calendar.MONDAY); * monthView.setDaysOfTheWeek( * new String[]{"S", "M", "T", "W", "Th", "F", "S"}); * </pre> * <p> * This component supports flagging days. These flagged days, which must be * provided in sorted order, are displayed in a bold font. This can be used to * inform the user of such things as scheduled appointment. * <pre> * // Create some dates that we want to flag as being important. * Calendar cal1 = Calendar.getInstance(); * cal1.set(2004, 1, 1); * Calendar cal2 = Calendar.getInstance(); * cal2.set(2004, 1, 5); * * long[] flaggedDates = new long[] { * cal1.getTimeInMillis(), * cal2.getTimeInMillis(), * System.currentTimeMillis() * }; * * // Sort them in ascending order. * java.util.Arrays.sort(flaggedDates); * monthView.setFlaggedDates(flaggedDates); * </pre> * Applications may have the need to allow users to select different ranges of * dates. There are four modes of selection that are supported, single, * multiple, week and no selection. Once a selection is made an action is * fired, with exception of the no selection mode, to inform listeners that * selection has changed. * <pre> * // Change the selection mode to select full weeks. * monthView.setSelectionMode(JXMonthView.WEEK_SELECTION); * * // Add an action listener that will be notified when the user * // changes selection via the mouse. * monthView.addActionListener(new ActionListener() { * public void actionPerformed(ActionEvent e) { * System.out.println( * ((JXMonthView)e.getSource()).getSelectedDateSpan()); * } * }); * </pre> * * @author Joshua Outwater * @version $Revision: 1.12 $ */ public class JXMonthView extends JComponent { /** Mode that disallows selection of days from the calendar. */ public static final int NO_SELECTION = 0; /** Mode that allows for selection of a single day. */ public static final int SINGLE_SELECTION = 1; /** Mode that allows for selecting of multiple consecutive days. */ public static final int MULTIPLE_SELECTION = 2; /** * Mode where selections consisting of more than 7 days will * snap to a full week. */ public static final int WEEK_SELECTION = 3; /** Return value used to identify when the month down button is pressed. */ public static final int MONTH_DOWN = 1; /** Return value used to identify when the month up button is pressed. */ public static final int MONTH_UP = 2; /** * Insets used in determining the rectangle for the month string * background. */ protected Insets _monthStringInsets = new Insets(0,0,0,0); private static final int MONTH_DROP_SHADOW = 1; private static final int MONTH_LINE_DROP_SHADOW = 2; private static final int WEEK_DROP_SHADOW = 4; private int _boxPaddingX = 1; private int _boxPaddingY = 2; private int _arrowPaddingX = 1; private int _arrowPaddingY = 1; private static final int CALENDAR_SPACING = 1; private static final int DAYS_IN_WEEK = 7; private static final int MONTHS_IN_YEAR = 12; /** * Keeps track of the first date we are displaying. We use this as a * restore point for the calendar. */ private long _firstDisplayedDate; private int _firstDisplayedMonth; private int _firstDisplayedYear; private long _lastDisplayedDate; private Font _derivedFont; private Color _derivedColorSelection = Color.BLUE; /** Beginning date of selection. -1 if no date is selected. */ private long _startSelectedDate = -1; /** End date of selection. -1 if no date is selected. */ private long _endSelectedDate = -1; /** For multiple selection we need to record the date we pivot around. */ private long _pivotDate = -1; /** The number of calendars able to be displayed horizontally. */ private int _numCalCols = 1; /** The number of calendars able to be displayed vertically. */ private int _numCalRows = 1; private int _minCalCols = 1; private int _minCalRows = 1; private long _today; private long[] _flaggedDates; private int _selectionMode = SINGLE_SELECTION; private int _boxHeight; private int _boxWidth; private int _monthBoxHeight; private int _calendarWidth; private int _calendarHeight; private int _firstDayOfWeek = Calendar.SUNDAY; private int _startX; private int _startY; private int _dropShadowMask = 0; private boolean _dirty = false; private boolean _antiAlias = false; private boolean _ltr; private boolean _traversable = false; private boolean _usingKeyboard = false; private boolean _asKirkWouldSay_FIRE = false; private Calendar _cal; private String[] _daysOfTheWeek; private static String[] _monthsOfTheYear; private Dimension _dim = new Dimension(); private Rectangle _bounds = new Rectangle(); private Rectangle _dirtyRect = new Rectangle(); private Color _todayBackgroundColor; private Color _monthStringBackground; private Color _monthStringForeground; private Color _daysOfTheWeekForeground; private Color _selectedBackground; private SimpleDateFormat _dayOfMonthFormatter = new SimpleDateFormat("d"); private String _actionCommand = "selectionChanged"; private Timer _todayTimer = null; private ImageIcon _monthDownImage; private ImageIcon _monthUpImage; private Hashtable<Integer, Color> _dayToColorTable = new Hashtable<Integer, Color>(); /** * Date span used by the keyboard actions to track the original selection. */ private DateSpan _originalDateSpan = null; /** * Create a new instance of the <code>JXMonthView</code> class using the * month and year of the current day as the first date to display. */ public JXMonthView() { this(new Date().getTime()); } /** * Create a new instance of the <code>JXMonthView</code> class using the * month and year from <code>initialTime</code> as the first date to * display. * * @param initialTime The first month to display. */ public JXMonthView(long initialTime) { super(); _ltr = getComponentOrientation().isLeftToRight(); // Set up calendar instance. _cal = Calendar.getInstance(getLocale()); _cal.setFirstDayOfWeek(_firstDayOfWeek); _cal.setMinimalDaysInFirstWeek(1); // Keep track of today. _cal.set(Calendar.HOUR_OF_DAY, 0); _cal.set(Calendar.MINUTE, 0); _cal.set(Calendar.SECOND, 0); _cal.set(Calendar.MILLISECOND, 0); _today = _cal.getTimeInMillis(); _cal.setTimeInMillis(initialTime); setFirstDisplayedDate(_cal.getTimeInMillis()); // Get string representation of the months of the year. _monthsOfTheYear = new DateFormatSymbols().getMonths(); setOpaque(true); setBackground(Color.WHITE); setFocusable(true); _todayBackgroundColor = getForeground(); // Restore original time value. _cal.setTimeInMillis(_firstDisplayedDate); enableEvents(AWTEvent.MOUSE_EVENT_MASK); enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK); // Setup the keyboard handler. InputMap inputMap = getInputMap(JComponent.WHEN_FOCUSED); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "acceptSelection"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "cancelSelection"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "selectPreviousDay"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "selectNextDay"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "selectDayInPreviousWeek"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "selectDayInNextWeek"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_MASK, false), "addPreviousDay"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_MASK, false), "addNextDay"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_MASK, false), "addToPreviousWeek"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_MASK, false), "addToNextWeek"); // Needed to allow for keyboard control in popups. inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "acceptSelection"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "cancelSelection"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "selectPreviousDay"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "selectNextDay"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "selectDayInPreviousWeek"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "selectDayInNextWeek"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_MASK, false), "addPreviousDay"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_MASK, false), "addNextDay"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_MASK, false), "addToPreviousWeek"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_MASK, false), "addToNextWeek"); ActionMap actionMap = getActionMap(); actionMap.put("acceptSelection", new KeyboardAction(KeyboardAction.ACCEPT_SELECTION)); actionMap.put("cancelSelection", new KeyboardAction(KeyboardAction.CANCEL_SELECTION)); actionMap.put("selectPreviousDay", new KeyboardAction(KeyboardAction.SELECT_PREVIOUS_DAY)); actionMap.put("selectNextDay", new KeyboardAction(KeyboardAction.SELECT_NEXT_DAY)); actionMap.put("selectDayInPreviousWeek", new KeyboardAction(KeyboardAction.SELECT_DAY_PREVIOUS_WEEK)); actionMap.put("selectDayInNextWeek", new KeyboardAction(KeyboardAction.SELECT_DAY_NEXT_WEEK)); actionMap.put("addPreviousDay", new KeyboardAction(KeyboardAction.ADD_PREVIOUS_DAY)); actionMap.put("addNextDay", new KeyboardAction(KeyboardAction.ADD_NEXT_DAY)); actionMap.put("addToPreviousWeek", new KeyboardAction(KeyboardAction.ADD_TO_PREVIOUS_WEEK)); actionMap.put("addToNextWeek", new KeyboardAction(KeyboardAction.ADD_TO_NEXT_WEEK)); updateUI(); } /** * Resets the UI property to a value from the current look and feel. */ @Override public void updateUI() { super.updateUI(); String[] daysOfTheWeek = (String[])UIManager.get("JXMonthView.daysOfTheWeek"); if (daysOfTheWeek == null) { String[] dateFormatSymbols = new DateFormatSymbols().getShortWeekdays(); daysOfTheWeek = new String[DAYS_IN_WEEK]; for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { daysOfTheWeek[i - 1] = dateFormatSymbols[i]; } } setDaysOfTheWeek(daysOfTheWeek); Color color = UIManager.getColor("JXMonthView.monthStringBackground"); if (color == null) { color = new Color(138, 173, 209); } setMonthStringBackground(color); color = UIManager.getColor("JXMonthView.monthStringForeground"); if (color == null) { color = new Color(68, 68, 68); } setMonthStringForeground(color); color = UIManager.getColor("JXMonthView.daysOfTheWeekForeground"); if (color == null) { color = new Color(68, 68, 68); } setDaysOfTheWeekForeground(color); color = UIManager.getColor("JXMonthView.selectedBackground"); if (color == null) { color = new Color(197, 220, 240); } setSelectedBackground(color); Font font = UIManager.getFont("JXMonthView.font"); if (font == null) { font = UIManager.getFont("Button.font"); } setFont(font); /* String imageLocation = UIManager.getString("JXMonthView.monthDownFileName"); if (imageLocation == null) { imageLocation = "resources/month-down.png"; } _monthDownImage = new ImageIcon( JXMonthView.class.getResource(imageLocation)); imageLocation = UIManager.getString("JXMonthView.monthUpFileName"); if (imageLocation == null) { imageLocation = "resources/month-up.png"; } _monthUpImage = new ImageIcon( JXMonthView.class.getResource(imageLocation)); */ } /** * Returns the first displayed date. * * @return long The first displayed date. */ public long getFirstDisplayedDate() { return _firstDisplayedDate; } /** * Set the first displayed date. We only use the month and year of * this date. The <code>Calendar.DAY_OF_MONTH</code> field is reset to * 1 and all other fields, with exception of the year and month , * are reset to 0. * * @param date The first displayed date. */ public void setFirstDisplayedDate(long date) { long old = _firstDisplayedDate; _cal.setTimeInMillis(date); _cal.set(Calendar.DAY_OF_MONTH, 1); _cal.set(Calendar.HOUR_OF_DAY, 0); _cal.set(Calendar.MINUTE, 0); _cal.set(Calendar.SECOND, 0); _cal.set(Calendar.MILLISECOND, 0); _firstDisplayedDate = _cal.getTimeInMillis(); _firstDisplayedMonth = _cal.get(Calendar.MONTH); _firstDisplayedYear = _cal.get(Calendar.YEAR); calculateLastDisplayedDate(); firePropertyChange("firstDisplayedDate", old, _firstDisplayedDate); repaint(); } /** * Returns the last date able to be displayed. For example, if the last * visible month was April the time returned would be April 30, 23:59:59. * * @return long The last displayed date. */ public long getLastDisplayedDate() { return _lastDisplayedDate; } private void calculateLastDisplayedDate() { long old = _lastDisplayedDate; _cal.setTimeInMillis(_firstDisplayedDate); // Figure out the last displayed date. _cal.add(Calendar.MONTH, 3);//((_numCalCols * _numCalRows) - 1)); _cal.set(Calendar.DAY_OF_MONTH, _cal.getActualMaximum(Calendar.DAY_OF_MONTH)); _cal.set(Calendar.HOUR_OF_DAY, 23); _cal.set(Calendar.MINUTE, 59); _cal.set(Calendar.SECOND, 59); _lastDisplayedDate = _cal.getTimeInMillis(); firePropertyChange("lastDisplayedDate", old, _lastDisplayedDate); } /** * Moves the <code>date</code> into the visible region of the calendar. * If the date is greater than the last visible date it will become the * last visible date. While if it is less than the first visible date * it will become the first visible date. * * @param date Date to make visible. */ public void ensureDateVisible(long date) { if (date < _firstDisplayedDate) { setFirstDisplayedDate(date); } else if (date > _lastDisplayedDate) { _cal.setTimeInMillis(date); int month = _cal.get(Calendar.MONTH); int year = _cal.get(Calendar.YEAR); _cal.setTimeInMillis(_lastDisplayedDate); int lastMonth = _cal.get(Calendar.MONTH); int lastYear = _cal.get(Calendar.YEAR); int diffMonths = month - lastMonth + ((year - lastYear) * MONTHS_IN_YEAR); _cal.setTimeInMillis(_firstDisplayedDate); _cal.add(Calendar.MONTH, diffMonths); setFirstDisplayedDate(_cal.getTimeInMillis()); } if (_startSelectedDate != -1 || _endSelectedDate != -1) { calculateDirtyRectForSelection(); } } /** * Returns a date span of the selected dates. The result will be null if * no dates are selected. */ public DateSpan getSelectedDateSpan() { DateSpan result = null; if (_startSelectedDate != -1) { result = new DateSpan(new Date(_startSelectedDate), new Date(_endSelectedDate)); } return result; } /** * Selects the dates in the DateSpan. This method will not change the * initial date displayed so the caller must update this if necessary. * If we are in SINGLE_SELECTION mode only the start time from the DateSpan * will be used. If we are in WEEK_SELECTION mode the span will be * modified to be valid if necessary. * * @param dateSpan DateSpan defining the selected dates. Passing * <code>null</code> will clear the selection. */ public void setSelectedDateSpan(DateSpan dateSpan) { DateSpan oldSpan = null; if (_startSelectedDate != -1 && _endSelectedDate != -1) { oldSpan = new DateSpan(_startSelectedDate, _endSelectedDate); } if (dateSpan == null) { _startSelectedDate = -1; _endSelectedDate = -1; } else { _cal.setTimeInMillis(dateSpan.getStart()); _cal.set(Calendar.HOUR_OF_DAY, 0); _cal.set(Calendar.MINUTE, 0); _cal.set(Calendar.SECOND, 0); _cal.set(Calendar.MILLISECOND, 0); _startSelectedDate = _cal.getTimeInMillis(); if (_selectionMode == SINGLE_SELECTION) { _endSelectedDate = _startSelectedDate; } else { _cal.setTimeInMillis(dateSpan.getEnd()); _cal.set(Calendar.HOUR_OF_DAY, 0); _cal.set(Calendar.MINUTE, 0); _cal.set(Calendar.SECOND, 0); _cal.set(Calendar.MILLISECOND, 0); _endSelectedDate = _cal.getTimeInMillis(); if (_selectionMode == WEEK_SELECTION) { // Make sure if we are over 7 days we span full weeks. _cal.setTimeInMillis(_startSelectedDate); int count = 1; while (_cal.getTimeInMillis() < _endSelectedDate) { _cal.add(Calendar.DAY_OF_MONTH, 1); count++; } if (count > DAYS_IN_WEEK) { // Make sure start date is on the beginning of the // week. _cal.setTimeInMillis(_startSelectedDate); int dayOfWeek = _cal.get(Calendar.DAY_OF_WEEK); if (dayOfWeek != _firstDayOfWeek) { // Move the start date back to the first day of the // week. int daysFromStart = dayOfWeek - _firstDayOfWeek; if (daysFromStart < 0) { daysFromStart += DAYS_IN_WEEK; } _cal.add(Calendar.DAY_OF_MONTH, -daysFromStart); count += daysFromStart; _startSelectedDate = _cal.getTimeInMillis(); } // Make sure we have full weeks. Otherwise modify the // end date. int remainder = count % DAYS_IN_WEEK; if (remainder != 0) { _cal.setTimeInMillis(_endSelectedDate); _cal.add(Calendar.DAY_OF_MONTH, (DAYS_IN_WEEK - remainder)); _endSelectedDate = _cal.getTimeInMillis(); } } } } // Restore original time value. _cal.setTimeInMillis(_firstDisplayedDate); } repaint(_dirtyRect); calculateDirtyRectForSelection(); repaint(_dirtyRect); // Fire property change. firePropertyChange("selectedDates", oldSpan, dateSpan); } /** * Returns the current selection mode for this JXMonthView. * * @return int Selection mode. */ public int getSelectionMode() { return _selectionMode; } /** * Set the selection mode for this JXMonthView. * * @throws IllegalArgumentException */ public void setSelectionMode(int mode) throws IllegalArgumentException { if (mode != SINGLE_SELECTION && mode != MULTIPLE_SELECTION && mode != WEEK_SELECTION && mode != NO_SELECTION) { throw new IllegalArgumentException(mode + " is not a valid selection mode"); } _selectionMode = mode; } /** * An array of longs defining days that should be flagged. This array is * assumed to be in sorted order from least to greatest. */ public void setFlaggedDates(long[] flaggedDates) { _flaggedDates = flaggedDates; if (_flaggedDates == null) { repaint(); return; } // Loop through the flaggedDates and set the hour, minute, seconds and // milliseconds to 0 so we can compare times later. for (int i = 0; i < _flaggedDates.length; i++) { _cal.setTimeInMillis(_flaggedDates[i]); // We only want to compare the day, month and year // so reset all other values to 0. _cal.set(Calendar.HOUR_OF_DAY, 0); _cal.set(Calendar.MINUTE, 0); _cal.set(Calendar.SECOND, 0); _cal.set(Calendar.MILLISECOND, 0); _flaggedDates[i] = _cal.getTimeInMillis(); } // Restore the time. _cal.setTimeInMillis(_firstDisplayedDate); repaint(); } /** * Returns the padding used between days in the calendar. */ public int getBoxPaddingX() { return _boxPaddingX; } /** * Sets the number of pixels used to pad the left and right side of a day. * The padding is applied to both sides of the days. Therefore, if you * used the padding value of 3, the number of pixels between any two days * would be 6. */ public void setBoxPaddingX(int _boxPaddingX) { this._boxPaddingX = _boxPaddingX; _dirty = true; } /** * Returns the padding used above and below days in the calendar. */ public int getBoxPaddingY() { return _boxPaddingY; } /** * Sets the number of pixels used to pad the top and bottom of a day. * The padding is applied to both the top and bottom of a day. Therefore, * if you used the padding value of 3, the number of pixels between any * two days would be 6. */ public void setBoxPaddingY(int _boxPaddingY) { this._boxPaddingY = _boxPaddingY; _dirty = true; } /** * Returns whether or not the month view supports traversing months. * * @return <code>true</code> if month traversing is enabled. */ public boolean getTraversable() { return _traversable; } /** * Set whether or not the month view will display buttons to allow the * user to traverse to previous or next months. * * @param traversable set to true to enable month traversing, * false otherwise. */ public void setTraversable(boolean traversable) { _traversable = traversable; _dirty = true; repaint(); } /** * Sets the single character representation for each day of the * week. For this method the first days of the week days[0] is assumed to * be <code>Calendar.SUNDAY</code>. * * @throws IllegalArgumentException if <code>days.length</code> != DAYS_IN_WEEK * @throws NullPointerException if <code>days</code> == null */ public void setDaysOfTheWeek(String[] days) throws IllegalArgumentException, NullPointerException { if (days == null) { throw new NullPointerException("Array of days is null."); } else if (days.length != DAYS_IN_WEEK) { throw new IllegalArgumentException( "Array of days is not of length " + DAYS_IN_WEEK + " as expected."); } // TODO: This could throw off internal size information we should // call update and then recalculate displayed calendars and start // positions. _daysOfTheWeek = days; _dirty = true; repaint(); } /** * Returns the single character representation for each day of the * week. * * @return Single character representation for the days of the week */ public String[] getDaysOfTheWeek() { String[] days = new String[DAYS_IN_WEEK]; System.arraycopy(_daysOfTheWeek, 0, days, 0, DAYS_IN_WEEK); return days; } /** * Gets what the first day of the week is; e.g., * <code>Calendar.SUNDAY</code> in the U.S., <code>Calendar.MONDAY</code> * in France. * * @return int The first day of the week. */ public int getFirstDayOfWeek() { return _firstDayOfWeek; } /** * Sets what the first day of the week is; e.g., * <code>Calendar.SUNDAY</code> in US, <code>Calendar.MONDAY</code> * in France. * * @param firstDayOfWeek The first day of the week. * * @see java.util.Calendar */ public void setFirstDayOfWeek(int firstDayOfWeek) { if (firstDayOfWeek == _firstDayOfWeek) { return; } _firstDayOfWeek = firstDayOfWeek; _cal.setFirstDayOfWeek(_firstDayOfWeek); repaint(); } /** * Gets the time zone. * * @return The <code>TimeZone</code> used by the <code>JXMonthView</code>. */ public TimeZone getTimeZone() { return _cal.getTimeZone(); } /** * Sets the time zone with the given time zone value. * * @param tz The <code>TimeZone</code>. */ public void setTimeZone(TimeZone tz) { _cal.setTimeZone(tz); } /** * Returns true if anti-aliased text is enabled for this component, false * otherwise. * * @return boolean <code>true</code> if anti-aliased text is enabled, * <code>false</code> otherwise. */ public boolean getAntialiased() { return _antiAlias; } /** * Turns on/off anti-aliased text for this component. * * @param antiAlias <code>true</code> for anti-aliased text, * <code>false</code> to turn it off. */ public void setAntialiased(boolean antiAlias) { if (_antiAlias == antiAlias) { return; } _antiAlias = antiAlias; repaint(); } /** public void setDropShadowMask(int mask) { _dropShadowMask = mask; repaint(); } */ /** * Returns the selected background color. * * @return the selected background color. */ public Color getSelectedBackground() { return _selectedBackground; } /** * Sets the selected background color to <code>c</code>. The default color * is <code>138, 173, 209 (Blue-ish)</code> * * @param c Selected background. */ public void setSelectedBackground(Color c) { _selectedBackground = c; } /** * Returns the color used when painting the today background. * * @return Color Color */ public Color getTodayBackground() { return _todayBackgroundColor; } /** * Sets the color used to draw the bounding box around today. The default * is the background of the <code>JXMonthView</code> component. * * @param c color to set */ public void setTodayBackground(Color c) { _todayBackgroundColor = c; repaint(); } /** * Returns the color used to paint the month string background. * * @return Color Color. */ public Color getMonthStringBackground() { return _monthStringBackground; } /** * Sets the color used to draw the background of the month string. The * default is <code>138, 173, 209 (Blue-ish)</code>. * * @param c color to set */ public void setMonthStringBackground(Color c) { _monthStringBackground = c; repaint(); } /** * Returns the color used to paint the month string foreground. * * @return Color Color. */ public Color getMonthStringForeground() { return _monthStringForeground; } /** * Sets the color used to draw the foreground of the month string. The * default is <code>Color.WHITE</code>. * * @param c color to set */ public void setMonthStringForeground(Color c) { _monthStringForeground = c; repaint(); } /** * Sets the color used to draw the foreground of each day of the week. These * are the titles * * @param c color to set */ public void setDaysOfTheWeekForeground(Color c) { _daysOfTheWeekForeground = c; repaint(); } /** * @return Color Color */ public Color getDaysOfTheWeekForeground() { return _daysOfTheWeekForeground; } /** * Set the color to be used for painting the specified day of the week. * Acceptable values are Calendar.SUNDAY - Calendar.SATURDAY. * * @param dayOfWeek constant value defining the day of the week. * @param c The color to be used for painting the numeric day of the week. */ public void setDayForeground(int dayOfWeek, Color c) { _dayToColorTable.put(dayOfWeek, c); } /** * Return the color that should be used for painting the numerical day of the week. * * @param dayOfWeek The day of week to get the color for. * @return The color to be used for painting the numeric day of the week. * If this was no color has yet been defined the component foreground color * will be returned. */ public Color getDayForeground(int dayOfWeek) { Color c; c = _dayToColorTable.get(dayOfWeek); if (c == null) { c = getForeground(); } return c; } /** * Returns a copy of the insets used to paint the month string background. * * @return Insets Month string insets. */ public Insets getMonthStringInsets() { return (Insets)_monthStringInsets.clone(); } /** * Insets used to modify the width/height when painting the background * of the month string area. * * @param insets Insets */ public void setMonthStringInsets(Insets insets) { if (insets == null) { _monthStringInsets.top = 0; _monthStringInsets.left = 0; _monthStringInsets.bottom = 0; _monthStringInsets.right = 0; } else { _monthStringInsets.top = insets.top; _monthStringInsets.left = insets.left; _monthStringInsets.bottom = insets.bottom; _monthStringInsets.right = insets.right; } repaint(); } /** * Returns the preferred number of columns to paint calendars in. * * @return int Columns of calendars. */ public int getPreferredCols() { return _minCalCols; } /** * The preferred number of columns to paint calendars. * * @param cols The number of columns of calendars. */ public void setPreferredCols(int cols) { if (cols <= 0) { return; } _minCalCols = cols; _dirty = true; revalidate(); repaint(); } /** * Returns the preferred number of rows to paint calendars in. * * @return int Rows of calendars. */ public int getPreferredRows() { return _minCalRows; } /** * Sets the preferred number of rows to paint calendars. * * @param rows The number of rows of calendars. */ public void setPreferredRows(int rows) { if (rows <= 0) { return; } _minCalRows = rows; _dirty = true; revalidate(); repaint(); } private void updateIfNecessary() { if (_dirty) { update(); _dirty = false; } } /** * Calculates size information necessary for laying out the month view. */ private void update() { // Loop through year and get largest representation of the month. // Keep track of the longest month so we can loop through it to // determine the width of a date box. int currDays; int longestMonth = 0; int daysInLongestMonth = 0; int currWidth; int longestMonthWidth = 0; // We use a bold font for figuring out size constraints since // it's larger and flaggedDates will be noted in this style. _derivedFont = getFont().deriveFont(Font.BOLD); FontMetrics fm = getFontMetrics(_derivedFont); _cal.set(Calendar.MONTH, _cal.getMinimum(Calendar.MONTH)); _cal.set(Calendar.DAY_OF_MONTH, _cal.getActualMinimum(Calendar.DAY_OF_MONTH)); for (int i = 0; i < _cal.getMaximum(Calendar.MONTH); i++) { currWidth = fm.stringWidth(_monthsOfTheYear[i]); if (currWidth > longestMonthWidth) { longestMonthWidth = currWidth; } currDays = _cal.getActualMaximum(Calendar.DAY_OF_MONTH); if (currDays > daysInLongestMonth) { longestMonth = _cal.get(Calendar.MONTH); daysInLongestMonth = currDays; } _cal.add(Calendar.MONTH, 1); } // Loop through the days of the week and adjust the box width // accordingly. _boxHeight = fm.getHeight(); for (String dayOfTheWeek : _daysOfTheWeek) { currWidth = fm.stringWidth(dayOfTheWeek); if (currWidth > _boxWidth) { _boxWidth = currWidth; } } // Loop through longest month and get largest representation of the day // of the month. _cal.set(Calendar.MONTH, longestMonth); _cal.set(Calendar.DAY_OF_MONTH, _cal.getActualMinimum(Calendar.DAY_OF_MONTH)); for (int i = 0; i < daysInLongestMonth; i++) { currWidth = fm.stringWidth( _dayOfMonthFormatter.format(_cal.getTime())); if (currWidth > _boxWidth) { _boxWidth = currWidth; } _cal.add(Calendar.DAY_OF_MONTH, 1); } // If the calendar is traversable, check the icon heights and // adjust the month box height accordingly. _monthBoxHeight = _boxHeight; if (_traversable) { int newHeight = _monthDownImage.getIconHeight() + _arrowPaddingY + _arrowPaddingY; if (newHeight > _monthBoxHeight) { _monthBoxHeight = newHeight; } } // Modify _boxWidth if month string is longer _dim.width = (_boxWidth + (2 * _boxPaddingX)) * DAYS_IN_WEEK; if (_dim.width < longestMonthWidth) { double diff = longestMonthWidth - _dim.width; if (_traversable) { diff += _monthDownImage.getIconWidth() + _monthUpImage.getIconWidth() + (_arrowPaddingX * 4); } _boxWidth += Math.ceil(diff / DAYS_IN_WEEK); _dim.width = (_boxWidth + (2 * _boxPaddingX)) * DAYS_IN_WEEK; } // Keep track of calendar width and height for use later. _calendarWidth = (_boxWidth + (2 * _boxPaddingX)) * DAYS_IN_WEEK; _calendarHeight = ((_boxPaddingY + _boxHeight + _boxPaddingY) * 7) + (_boxPaddingY + _monthBoxHeight + _boxPaddingY); // Calculate minimum width/height for the component. _dim.height = (_calendarHeight * _minCalRows) + (CALENDAR_SPACING * (_minCalRows - 1)); _dim.width = (_calendarWidth * _minCalCols) + (CALENDAR_SPACING * (_minCalCols - 1)); // Add insets to the dimensions. Insets insets = getInsets(); _dim.width += insets.left + insets.right; _dim.height += insets.top + insets.bottom; // Restore calendar. _cal.setTimeInMillis(_firstDisplayedDate); calculateNumDisplayedCals(); calculateStartPosition(); if (_startSelectedDate != -1 || _endSelectedDate != -1) { if (_startSelectedDate > _lastDisplayedDate || _startSelectedDate < _firstDisplayedDate) { // Already does the recalculation for the dirty rect. ensureDateVisible(_startSelectedDate); } else { calculateDirtyRectForSelection(); } } } private void updateToday() { // Update _today. _cal.setTimeInMillis(_today); _cal.add(Calendar.DAY_OF_MONTH, 1); _today = _cal.getTimeInMillis(); // Restore calendar. _cal.setTimeInMillis(_firstDisplayedDate); repaint(); } /** * Returns the minimum size needed to display this component. * * @return Dimension Minimum size. */ @Override public Dimension getMinimumSize() { return getPreferredSize(); } /** * Returns the preferred size of this component. * * @return Dimension Preferred size. */ @Override public Dimension getPreferredSize() { updateIfNecessary(); return new Dimension(_dim); } /** * Returns the maximum size of this component. * * @return Dimension Maximum size. */ @Override public Dimension getMaximumSize() { return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); } /** * Sets the border of this component. The Border object is responsible * for defining the insets for the component (overriding any insets set * directly on the component) and for optionally rendering any border * decorations within the bounds of those insets. Borders should be used * (rather than insets) for creating both decorative and non-decorative * (such as margins and padding) regions for a swing component. Compound * borders can be used to nest multiple borders within a single component. * <p> * As the border may modify the bounds of the component, setting the border * may result in a reduced number of displayed calendars. * * @param border Border. */ @Override public void setBorder(Border border) { super.setBorder(border); _dirty = true; } /** * Moves and resizes this component. The new location of the top-left * corner is specified by x and y, and the new size is specified by * width and height. * * @param x The new x-coordinate of this component * @param y The new y-coordinate of this component * @param width The new width of this component * @param height The new height of this component */ @Override public void setBounds(int x, int y, int width, int height) { super.setBounds(x, y, width, height); _dirty = true; } /** * Moves and resizes this component to conform to the new bounding * rectangle r. This component's new position is specified by r.x and * r.y, and its new size is specified by r.width and r.height * * @param r The new bounding rectangle for this component */ @Override public void setBounds(Rectangle r) { setBounds(r.x, r.y, r.width, r.height); } /** * Sets the language-sensitive orientation that is to be used to order * the elements or text within this component. Language-sensitive * LayoutManager and Component subclasses will use this property to * determine how to lay out and draw components. * <p> * At construction time, a component's orientation is set to * ComponentOrientation.UNKNOWN, indicating that it has not been * specified explicitly. The UNKNOWN orientation behaves the same as * ComponentOrientation.LEFT_TO_RIGHT. * * @param o The component orientation. */ @Override public void setComponentOrientation(ComponentOrientation o) { super.setComponentOrientation(o); _ltr = o.isLeftToRight(); calculateStartPosition(); calculateDirtyRectForSelection(); } /** * Sets the font of this component. * * @param font The font to become this component's font; if this parameter * is null then this component will inherit the font of its parent. */ @Override public void setFont(Font font) { super.setFont(font); _dirty = true; } /** * {@inheritDoc} */ @Override public void removeNotify() { _todayTimer.stop(); super.removeNotify(); } /** * {@inheritDoc} */ @Override public void addNotify() { super.addNotify(); // Setup timer to update the value of _today. int secondsTillTomorrow = 86400; if (_todayTimer == null) { _todayTimer = new Timer(secondsTillTomorrow * 1000, new ActionListener() { public void actionPerformed(ActionEvent e) { updateToday(); } }); } // Modify the initial delay by the current time. _cal.setTimeInMillis(System.currentTimeMillis()); secondsTillTomorrow = secondsTillTomorrow - (_cal.get(Calendar.HOUR_OF_DAY) * 3600) - (_cal.get(Calendar.MINUTE) * 60) - _cal.get(Calendar.SECOND); _todayTimer.setInitialDelay(secondsTillTomorrow * 1000); _todayTimer.start(); // Restore calendar _cal.setTimeInMillis(_firstDisplayedDate); } /** * {@inheritDoc} */ @Override protected void paintComponent(Graphics g) { Object oldAAValue = null; Graphics2D g2 = (g instanceof Graphics2D) ? (Graphics2D)g : null; if (g2 != null && _antiAlias) { oldAAValue = g2.getRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING); g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } Rectangle clip = g.getClipBounds(); updateIfNecessary(); if (isOpaque()) { g.setColor(getBackground()); g.fillRect(clip.x, clip.y, clip.width, clip.height); } g.setColor(getForeground()); Color shadowColor = g.getColor(); shadowColor = new Color(shadowColor.getRed(), shadowColor.getGreen(), shadowColor.getBlue(), (int)(.20 * 255)); FontMetrics fm = g.getFontMetrics(); // Reset the calendar. _cal.setTimeInMillis(_firstDisplayedDate); // Center the calendars vertically in the available space. int y = _startY; for (int row = 0; row < _numCalRows; row++) { // Center the calendars horizontally in the available space. int x = _startX; int tmpX, tmpY; // Check if this row falls in the clip region. _bounds.x = 0; _bounds.y = _startY + row * (_calendarHeight + CALENDAR_SPACING); _bounds.width = getWidth(); _bounds.height = _calendarHeight; if (!_bounds.intersects(clip)) { _cal.add(Calendar.MONTH, _numCalCols); y += _calendarHeight + CALENDAR_SPACING; continue; } for (int column = 0; column < _numCalCols; column++) { String monthName = _monthsOfTheYear[_cal.get(Calendar.MONTH)]; monthName = monthName + " " + _cal.get(Calendar.YEAR); _bounds.x = (_ltr ? x : x - _calendarWidth);// + 4; //4px of padding on the left _bounds.y = y + _boxPaddingY;// + 4; //4px of padding on the top _bounds.width = _calendarWidth;// - 8; //4px of padding on both sides _bounds.height = _monthBoxHeight; //4px of padding on top if (_bounds.intersects(clip)) { // Paint month name background. paintMonthStringBackground(g, _bounds.x, _bounds.y, _bounds.width, _bounds.height); // Paint arrow buttons for traversing months if enabled. if (_traversable) { tmpX = _bounds.x + _arrowPaddingX; tmpY = _bounds.y + (_bounds.height - _monthDownImage.getIconHeight()) / 2; g.drawImage(_monthDownImage.getImage(), tmpX, tmpY, null); tmpX = _bounds.x + _bounds.width - _arrowPaddingX - _monthUpImage.getIconWidth(); g.drawImage(_monthUpImage.getImage(), tmpX, tmpY, null); } // Paint month name. Font oldFont = getFont(); FontMetrics oldFM = fm; g.setFont(_derivedFont); fm = getFontMetrics(_derivedFont); g.setColor(_monthStringForeground); tmpX = _ltr ? x + (_calendarWidth / 2) - (fm.stringWidth(monthName) / 2) : x - (_calendarWidth / 2) - (fm.stringWidth(monthName) / 2) - 1; tmpY = _bounds.y + ((_monthBoxHeight - _boxHeight) / 2) + fm.getAscent(); if ((_dropShadowMask & MONTH_DROP_SHADOW) != 0) { g.setColor(shadowColor); g.drawString(monthName, tmpX + 1, tmpY + 1); g.setColor(_monthStringForeground); } g.drawString(monthName, tmpX, tmpY); g.setFont(oldFont); fm = oldFM; } g.setColor(getDaysOfTheWeekForeground()); _bounds.x = _ltr ? x : x - _calendarWidth; _bounds.y = y + _boxPaddingY + _monthBoxHeight + _boxPaddingY + _boxPaddingY; _bounds.width = _calendarWidth; _bounds.height = _boxHeight; if (_bounds.intersects(clip)) { _cal.set(Calendar.DAY_OF_MONTH, _cal.getActualMinimum(Calendar.DAY_OF_MONTH)); // Paint short representation of day of the week. int dayIndex = _firstDayOfWeek - 1; Font oldFont = g.getFont(); FontMetrics oldFM = fm; g.setFont(_derivedFont); fm = getFontMetrics(_derivedFont); for (int i = 0; i < DAYS_IN_WEEK; i++) { tmpX = _ltr ? x + (i * (_boxPaddingX + _boxWidth + _boxPaddingX)) + _boxPaddingX + (_boxWidth / 2) - (fm.stringWidth(_daysOfTheWeek[dayIndex]) / 2) : x - (i * (_boxPaddingX + _boxWidth + _boxPaddingX)) - _boxPaddingX - (_boxWidth / 2) - (fm.stringWidth(_daysOfTheWeek[dayIndex]) / 2); tmpY = _bounds.y + fm.getAscent(); if ((_dropShadowMask & WEEK_DROP_SHADOW) != 0) { g.setColor(shadowColor); g.drawString(_daysOfTheWeek[dayIndex], tmpX + 1, tmpY + 1); g.setColor(getDaysOfTheWeekForeground()); } g.drawString(_daysOfTheWeek[dayIndex], tmpX, tmpY); dayIndex++; if (dayIndex == DAYS_IN_WEEK) { dayIndex = 0; } } g.setFont(oldFont); fm = oldFM; } // Check if the month to paint falls in the clip. _bounds.x = _startX + (_ltr ? column * (_calendarWidth + CALENDAR_SPACING) : -(column * (_calendarWidth + CALENDAR_SPACING) + _calendarWidth)); _bounds.y = _startY + row * (_calendarHeight + CALENDAR_SPACING); _bounds.width = _calendarWidth; _bounds.height = _calendarHeight; // Paint the month if it intersects the clip. If we don't move // the calendar forward a month as it would have if paintMonth // was called. if (_bounds.intersects(clip)) { paintMonth(g, column, row); } else { _cal.add(Calendar.MONTH, 1); } x += _ltr ? _calendarWidth + CALENDAR_SPACING : -(_calendarWidth + CALENDAR_SPACING); } y += _calendarHeight + CALENDAR_SPACING; } // Restore the calendar. _cal.setTimeInMillis(_firstDisplayedDate); if (g2 != null && _antiAlias) { g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, oldAAValue); } } /** * Paints a month. It is assumed the calendar, _cal, is already set to the * first day of the month to be painted. * * @param col X (column) the calendar is displayed in. * @param row Y (row) the calendar is displayed in. * @param g Graphics object. */ private void paintMonth(Graphics g, int col, int row) { String numericDay; int days = _cal.getActualMaximum(Calendar.DAY_OF_MONTH); FontMetrics fm = g.getFontMetrics(); Rectangle clip = g.getClipBounds(); long nextFlaggedDate = -1; int flaggedDateIndex = 0; if (_flaggedDates != null && _flaggedDates.length > 0) { nextFlaggedDate = _flaggedDates[flaggedDateIndex]; } for (int i = 0; i < days; i++) { calculateBoundsForDay(_bounds); if (_bounds.intersects(clip)) { numericDay = _dayOfMonthFormatter.format(_cal.getTime()); // Paint bounding box around any date that falls within the // selection. if (isSelectedDate(_cal.getTimeInMillis())) { // Keep track of the rectangle for the currently // selected date so we don't have to recalculate it // later when it becomes unselected. This is only // useful for SINGLE_SELECTION mode. if (_selectionMode == SINGLE_SELECTION) { _dirtyRect.x = _bounds.x; _dirtyRect.y = _bounds.y; _dirtyRect.width = _bounds.width; _dirtyRect.height = _bounds.height; } paintSelectedDayBackground(g, _bounds.x, _bounds.y, _bounds.width, _bounds.height); g.setColor(getForeground()); } // Paint bounding box around today. if (_cal.getTimeInMillis() == _today) { paintTodayBackground(g, _bounds.x, _bounds.y, _bounds.width, _bounds.height); g.setColor(getForeground()); } // If the appointment date is less than the current // calendar date increment to the next appointment. while (nextFlaggedDate != -1 && nextFlaggedDate < _cal.getTimeInMillis()) { flaggedDateIndex++; if (flaggedDateIndex < _flaggedDates.length) { nextFlaggedDate = _flaggedDates[flaggedDateIndex]; } else { nextFlaggedDate = -1; } } // Paint numeric day of the month. g.setColor(getDayForeground(_cal.get(Calendar.DAY_OF_WEEK))); if (nextFlaggedDate != -1 && _cal.getTimeInMillis() == nextFlaggedDate) { Font oldFont = getFont(); FontMetrics oldFM = fm; Color oldColor = g.getColor(); // akardapolov g.setColor(_derivedColorSelection); // akardapolov g.drawString(numericDay, _ltr ? _bounds.x + _boxPaddingX + _boxWidth - fm.stringWidth(numericDay): _bounds.x + _boxPaddingX + _boxWidth - fm.stringWidth(numericDay) - 1, _bounds.y + _boxPaddingY + fm.getAscent()); g.setFont(oldFont); g.setColor(oldColor); fm = oldFM; } else { g.drawString(numericDay, _ltr ? _bounds.x + _boxPaddingX + _boxWidth - fm.stringWidth(numericDay): _bounds.x + _boxPaddingX + _boxWidth - fm.stringWidth(numericDay) - 1, _bounds.y + _boxPaddingY + fm.getAscent()); } } _cal.add(Calendar.DAY_OF_MONTH, 1); } } /** * Paints the background of the month string. The bounding box for this * background can be modified by setting its insets via * setMonthStringInsets. The color of the background can be set via * setMonthStringBackground. * * @see #setMonthStringBackground * @see #setMonthStringInsets * @param g Graphics object to paint to. * @param x x-coordinate of upper left corner. * @param y y-coordinate of upper left corner. * @param width width of the bounding box. * @param height height of the bounding box. */ protected void paintMonthStringBackground(Graphics g, int x, int y, int width, int height) { // Modify bounds by the month string insets. x = _ltr ? x + _monthStringInsets.left : x + _monthStringInsets.right; y = y + _monthStringInsets.top; width = width - _monthStringInsets.left - _monthStringInsets.right; height = height - _monthStringInsets.top - _monthStringInsets.bottom; Graphics2D g2 = (Graphics2D)g; GradientPaint gp = new GradientPaint(x, y + height, new Color(238, 238, 238), x, y, new Color(204, 204, 204)); //paint the border // g.setColor(_monthStringBackground); g2.setPaint(gp); g2.fillRect(x, y, width - 1, height - 1); g2.setPaint(new Color(153, 153, 153)); g2.drawRect(x, y, width - 1, height - 1); //TODO The right side of the rect is being clipped } /** * Paints the background for today. The default is a rectangle drawn in * using the color set by <code>setTodayBackground</code> * * @see #setTodayBackground * @param g Graphics object to paint to. * @param x x-coordinate of upper left corner. * @param y y-coordinate of upper left corner. * @param width width of bounding box for the day. * @param height height of bounding box for the day. */ protected void paintTodayBackground(Graphics g, int x, int y, int width, int height) { // g.setColor(_todayBackgroundColor); // g.drawRect(x, y, width - 1, height - 1); //paint the gradiented border GradientPaint gp = new GradientPaint(x, y, new Color(91, 123, 145), x, y + height, new Color(68, 86, 98)); Graphics2D g2 = (Graphics2D)g; g2.setPaint(gp); g2.drawRect(x, y, width - 1, height - 1); } /** * Paint the background for a selected day. The default is a filled * rectangle in the in the component's background color. * * @param g Graphics object to paint to. * @param x x-coordinate of upper left corner. * @param y y-coordinate of upper left corner. * @param width width of bounding box for the day. * @param height height of bounding box for the day. */ protected void paintSelectedDayBackground(Graphics g, int x, int y, int width, int height) { g.setColor(getSelectedBackground()); g.fillRect(x, y, width, height); } /** * Returns true if the specified time falls within the _startSelectedDate * and _endSelectedDate range. */ private boolean isSelectedDate(long time) { return time >= _startSelectedDate && time <= _endSelectedDate; } /** * Calculates the _numCalCols/_numCalRows that determine the number of * calendars that can be displayed. */ private void calculateNumDisplayedCals() { int oldNumCalCols = _numCalCols; int oldNumCalRows = _numCalRows; // Determine how many columns of calendars we want to paint. _numCalCols = 1; _numCalCols += (getWidth() - _calendarWidth) / (_calendarWidth + CALENDAR_SPACING); // Determine how many rows of calendars we want to paint. _numCalRows = 1; _numCalRows += (getHeight() - _calendarHeight) / (_calendarHeight + CALENDAR_SPACING); if (oldNumCalCols != _numCalCols || oldNumCalRows != _numCalRows) { calculateLastDisplayedDate(); } } /** * Calculates the _startX/_startY position for centering the calendars * within the available space. */ private void calculateStartPosition() { // Calculate offset in x-axis for centering calendars. _startX = (getWidth() - ((_calendarWidth * _numCalCols) + (CALENDAR_SPACING * (_numCalCols - 1)))) / 2; if (!_ltr) { _startX = getWidth() - _startX; } // Calculate offset in y-axis for centering calendars. _startY = (getHeight() - ((_calendarHeight * _numCalRows) + (CALENDAR_SPACING * (_numCalRows - 1 )))) / 2; } /** * Calculate the bounding box for drawing a date. It is assumed that the * calendar, _cal, is already set to the date you want to find the offset * for. * * @param bounds Bounds of the date to draw in. */ private void calculateBoundsForDay(Rectangle bounds) { int year = _cal.get(Calendar.YEAR); int month = _cal.get(Calendar.MONTH); int dayOfWeek = _cal.get(Calendar.DAY_OF_WEEK); int weekOfMonth = _cal.get(Calendar.WEEK_OF_MONTH); // Determine what row/column we are in. int diffMonths = month - _firstDisplayedMonth + ((year - _firstDisplayedYear) * MONTHS_IN_YEAR); int calRowIndex = diffMonths / _numCalCols; int calColIndex = diffMonths - (calRowIndex * _numCalCols); // Modify the index relative to the first day of the week. bounds.x = dayOfWeek - _firstDayOfWeek; if (bounds.x < 0) { bounds.x += DAYS_IN_WEEK; } // Offset for location of the day in the week. bounds.x = _ltr ? bounds.x * (_boxPaddingX + _boxWidth + _boxPaddingX) : (bounds.x + 1) * (_boxPaddingX + _boxWidth + _boxPaddingX); // Offset for the column the calendar is displayed in. bounds.x += calColIndex * (_calendarWidth + CALENDAR_SPACING); // Adjust by centering value. bounds.x = _ltr ? _startX + bounds.x : _startX - bounds.x; // Initial offset for Month and Days of the Week display. bounds.y = _boxPaddingY + _monthBoxHeight + _boxPaddingY + + _boxPaddingY + _boxHeight + _boxPaddingY; // Offset for centering and row the calendar is displayed in. bounds.y += _startY + calRowIndex * (_calendarHeight + CALENDAR_SPACING); // Offset for Week of the Month. bounds.y += (weekOfMonth - 1) * (_boxPaddingY + _boxHeight + _boxPaddingY); bounds.width = _boxPaddingX + _boxWidth + _boxPaddingX; bounds.height = _boxPaddingY + _boxHeight + _boxPaddingY; } /** * Return a long representing the date at the specified x/y position. * The date returned will have a valid day, month and year. Other fields * such as hour, minute, second and milli-second will be set to 0. * * @param x X position * @param y Y position * @return long The date, -1 if position does not contain a date. */ public long getDayAt(int x, int y) { if (_ltr ? (_startX > x) : (_startX < x) || _startY > y) { return -1; } // Determine which column of calendars we're in. int calCol = (_ltr ? (x - _startX) : (_startX - x)) / (_calendarWidth + CALENDAR_SPACING); // Determine which row of calendars we're in. int calRow = (y - _startY) / (_calendarHeight + CALENDAR_SPACING); if (calRow > _numCalRows - 1 || calCol > _numCalCols - 1) { return -1; } // Determine what row (week) in the selected month we're in. int row = 1; row += (((y - _startY) - (calRow * (_calendarHeight + CALENDAR_SPACING))) - (_boxPaddingY + _monthBoxHeight + _boxPaddingY)) / (_boxPaddingY + _boxHeight + _boxPaddingY); // The first two lines in the calendar are the month and the days // of the week. Ignore them. row -= 2; if (row < 0 || row > 5) { return -1; } // Determine which column in the selected month we're in. int col = ((_ltr ? (x - _startX) : (_startX - x)) - (calCol * (_calendarWidth + CALENDAR_SPACING))) / (_boxPaddingX + _boxWidth + _boxPaddingX); if (col > DAYS_IN_WEEK - 1) { return -1; } // Use the first day of the month as a key point for determining the // date of our click. // The week index of the first day will always be 0. _cal.setTimeInMillis(_firstDisplayedDate); //_cal.set(Calendar.DAY_OF_MONTH, 1); _cal.add(Calendar.MONTH, calCol + (calRow * _numCalCols)); int dayOfWeek = _cal.get(Calendar.DAY_OF_WEEK); int firstDayIndex = dayOfWeek - _firstDayOfWeek; if (firstDayIndex < 0) { firstDayIndex += DAYS_IN_WEEK; } int daysToAdd = (row * DAYS_IN_WEEK) + (col - firstDayIndex); if (daysToAdd < 0 || daysToAdd > (_cal.getActualMaximum(Calendar.DAY_OF_MONTH) - 1)) { return -1; } _cal.add(Calendar.DAY_OF_MONTH, daysToAdd); long selected = _cal.getTimeInMillis(); // Restore the time. _cal.setTimeInMillis(_firstDisplayedDate); return selected; } /** * Returns an index defining which, if any, of the buttons for * traversing the month was pressed. This method should only be * called when <code>setTraversable</code> is set to true. * * @param x x position of the pointer * @param y y position of the pointer * @return MONTH_UP, MONTH_DOWN or -1 when no button is selected. */ protected int getTraversableButtonAt(int x, int y) { if (_ltr ? (_startX > x) : (_startX < x) || _startY > y) { return -1; } // Determine which column of calendars we're in. int calCol = (_ltr ? (x - _startX) : (_startX - x)) / (_calendarWidth + CALENDAR_SPACING); // Determine which row of calendars we're in. int calRow = (y - _startY) / (_calendarHeight + CALENDAR_SPACING); if (calRow > _numCalRows - 1 || calCol > _numCalCols - 1) { return -1; } // See if we're in the month string area. y = ((y - _startY) - (calRow * (_calendarHeight + CALENDAR_SPACING))) - _boxPaddingY; if (y < _arrowPaddingY || y > (_monthBoxHeight - _arrowPaddingY)) { return -1; } x = ((_ltr ? (x - _startX) : (_startX - x)) - (calCol * (_calendarWidth + CALENDAR_SPACING))); if (x > _arrowPaddingX && x < (_arrowPaddingX + _monthDownImage.getIconWidth() + _arrowPaddingX)) { return MONTH_DOWN; } if (x > (_calendarWidth - _arrowPaddingX * 2 - _monthUpImage.getIconWidth()) && x < (_calendarWidth - _arrowPaddingX)) { return MONTH_UP; } return -1; } private void calculateDirtyRectForSelection() { if (_startSelectedDate == -1 || _endSelectedDate == -1) { _dirtyRect.x = 0; _dirtyRect.y = 0; _dirtyRect.width = 0; _dirtyRect.height = 0; } else { _cal.setTimeInMillis(_startSelectedDate); calculateBoundsForDay(_dirtyRect); _cal.add(Calendar.DAY_OF_MONTH, 1); Rectangle tmpRect; while (_cal.getTimeInMillis() <= _endSelectedDate) { calculateBoundsForDay(_bounds); tmpRect = _dirtyRect.union(_bounds); _dirtyRect.x = tmpRect.x; _dirtyRect.y = tmpRect.y; _dirtyRect.width = tmpRect.width; _dirtyRect.height = tmpRect.height; _cal.add(Calendar.DAY_OF_MONTH, 1); } // Restore the time. _cal.setTimeInMillis(_firstDisplayedDate); } } /** * Returns the string currently used to identiy fired ActionEvents. * * @return String The string used for identifying ActionEvents. */ public String getActionCommand() { return _actionCommand; } /** * Sets the string used to identify fired ActionEvents. * * @param actionCommand The string used for identifying ActionEvents. */ public void setActionCommand(String actionCommand) { _actionCommand = actionCommand; } /** * Adds an ActionListener. * <p> * The ActionListener will receive an ActionEvent when a selection has * been made. * * @param l The ActionListener that is to be notified */ public void addActionListener(ActionListener l) { listenerList.add(ActionListener.class, l); } /** * Removes an ActionListener. * * @param l The action listener to remove. */ public void removeActionListener(ActionListener l) { listenerList.remove(ActionListener.class, l); } /** * Fires an ActionEvent to all listeners. */ protected void fireActionPerformed() { Object[] listeners = listenerList.getListenerList(); ActionEvent e = null; for (int i = listeners.length - 2; i >= 0; i -=2) { if (listeners[i] == ActionListener.class) { if (e == null) { e = new ActionEvent(JXMonthView.this, ActionEvent.ACTION_PERFORMED, _actionCommand); } ((ActionListener)listeners[i + 1]).actionPerformed(e); } } } /** * {@inheritDoc} */ @Override protected void processMouseEvent(MouseEvent e) { // If we were using the keyboard we aren't anymore. _usingKeyboard = false; if (!isEnabled()) { return; } if (!hasFocus() && isFocusable()) { requestFocusInWindow(); } int id = e.getID(); // Check if one of the month traverse buttons was pushed. if (id == MouseEvent.MOUSE_PRESSED && _traversable) { int arrowType = getTraversableButtonAt(e.getX(), e.getY()); if (arrowType == MONTH_DOWN) { setFirstDisplayedDate( DateUtils.getPreviousMonth(getFirstDisplayedDate())); return; } else if (arrowType == MONTH_UP) { setFirstDisplayedDate( DateUtils.getNextMonth(getFirstDisplayedDate())); return; } } if (_selectionMode == NO_SELECTION) { return; } if (id == MouseEvent.MOUSE_PRESSED) { long selected = getDayAt(e.getX(), e.getY()); if (selected == -1) { return; } // Update the selected dates. _startSelectedDate = selected; _endSelectedDate = selected; if (_selectionMode == MULTIPLE_SELECTION || _selectionMode == WEEK_SELECTION) { _pivotDate = selected; } // Determine the dirty rectangle of the new selected date so we // draw the bounding box around it. This dirty rect includes the // visual border of the selected date. _cal.setTimeInMillis(selected); calculateBoundsForDay(_bounds); _cal.setTimeInMillis(_firstDisplayedDate); // Repaint the old dirty area. repaint(_dirtyRect); // Repaint the new dirty area. repaint(_bounds); // Update the dirty area. _dirtyRect.x = _bounds.x; _dirtyRect.y = _bounds.y; _dirtyRect.width = _bounds.width; _dirtyRect.height = _bounds.height; // Arm so we fire action performed on mouse release. _asKirkWouldSay_FIRE = true; } else if (id == MouseEvent.MOUSE_RELEASED) { if (_asKirkWouldSay_FIRE) { fireActionPerformed(); } _asKirkWouldSay_FIRE = false; } super.processMouseEvent(e); } /** * {@inheritDoc} */ @Override protected void processMouseMotionEvent(MouseEvent e) { // If we were using the keyboard we aren't anymore. _usingKeyboard = false; if (!isEnabled() || _selectionMode == NO_SELECTION) { return; } int id = e.getID(); if (id == MouseEvent.MOUSE_DRAGGED) { int x = e.getX(); int y = e.getY(); long selected = getDayAt(x, y); if (selected == -1) { return; } long oldStart = _startSelectedDate; long oldEnd = _endSelectedDate; if (_selectionMode == SINGLE_SELECTION) { if (selected == oldStart) { return; } _startSelectedDate = selected; _endSelectedDate = selected; } else { if (selected <= _pivotDate) { _startSelectedDate = selected; _endSelectedDate = _pivotDate; } else if (selected > _pivotDate) { _startSelectedDate = _pivotDate; _endSelectedDate = selected; } } if (_selectionMode == WEEK_SELECTION) { // Do we span a week. long start = (selected > _pivotDate) ? _pivotDate : selected; long end = (selected > _pivotDate) ? selected : _pivotDate; _cal.setTimeInMillis(start); int count = 1; while (_cal.getTimeInMillis() < end) { _cal.add(Calendar.DAY_OF_MONTH, 1); count++; } if (count > DAYS_IN_WEEK) { // Move the start date to the first day of the week. _cal.setTimeInMillis(start); int dayOfWeek = _cal.get(Calendar.DAY_OF_WEEK); int daysFromStart = dayOfWeek - _firstDayOfWeek; if (daysFromStart < 0) { daysFromStart += DAYS_IN_WEEK; } _cal.add(Calendar.DAY_OF_MONTH, -daysFromStart); _startSelectedDate = _cal.getTimeInMillis(); // Move the end date to the last day of the week. _cal.setTimeInMillis(end); dayOfWeek = _cal.get(Calendar.DAY_OF_WEEK); int lastDayOfWeek = _firstDayOfWeek - 1; if (lastDayOfWeek == 0) { lastDayOfWeek = Calendar.SATURDAY; } int daysTillEnd = lastDayOfWeek - dayOfWeek; if (daysTillEnd < 0) { daysTillEnd += DAYS_IN_WEEK; } _cal.add(Calendar.DAY_OF_MONTH, daysTillEnd); _endSelectedDate = _cal.getTimeInMillis(); } } if (oldStart == _startSelectedDate && oldEnd == _endSelectedDate) { return; } // Repaint the old dirty area. repaint(_dirtyRect); // Repaint the new dirty area. calculateDirtyRectForSelection(); repaint(_dirtyRect); // Set trigger. _asKirkWouldSay_FIRE = true; } super.processMouseMotionEvent(e); } /** * Class that supports keyboard traversal of the JXMonthView component. */ private class KeyboardAction extends AbstractAction { public static final int ACCEPT_SELECTION = 0; public static final int CANCEL_SELECTION = 1; public static final int SELECT_PREVIOUS_DAY = 2; public static final int SELECT_NEXT_DAY = 3; public static final int SELECT_DAY_PREVIOUS_WEEK = 4; public static final int SELECT_DAY_NEXT_WEEK = 5; public static final int ADD_PREVIOUS_DAY = 6; public static final int ADD_NEXT_DAY = 7; public static final int ADD_TO_PREVIOUS_WEEK = 8; public static final int ADD_TO_NEXT_WEEK = 9; private int action; public KeyboardAction(int action) { this.action = action; } public void actionPerformed(ActionEvent ev) { int selectionMode = getSelectionMode(); // TODO: Modify this to allow keyboard selection even if we don't have a previous selection. if (_startSelectedDate != -1 && selectionMode != NO_SELECTION) { if (!_usingKeyboard) { _originalDateSpan = getSelectedDateSpan(); } if (action >= ACCEPT_SELECTION && action <= CANCEL_SELECTION && _usingKeyboard) { if (action == CANCEL_SELECTION) { // Restore the original selection. setSelectedDateSpan(_originalDateSpan); fireActionPerformed(); } else { // Accept the keyboard selection. setSelectedDateSpan(getSelectedDateSpan()); fireActionPerformed(); } _usingKeyboard = false; } else if (action >= SELECT_PREVIOUS_DAY && action <= SELECT_DAY_NEXT_WEEK) { _usingKeyboard = true; traverse(action); } else if (selectionMode >= MULTIPLE_SELECTION && action >= ADD_PREVIOUS_DAY && action <= ADD_TO_NEXT_WEEK) { _usingKeyboard = true; addToSelection(action); } } } private void traverse(int action) { _cal.setTimeInMillis(_startSelectedDate); switch (action) { case SELECT_PREVIOUS_DAY: _cal.add(Calendar.DAY_OF_MONTH, -1); break; case SELECT_NEXT_DAY: _cal.add(Calendar.DAY_OF_MONTH, 1); break; case SELECT_DAY_PREVIOUS_WEEK: _cal.add(Calendar.DAY_OF_MONTH, -DAYS_IN_WEEK); break; case SELECT_DAY_NEXT_WEEK: _cal.add(Calendar.DAY_OF_MONTH, DAYS_IN_WEEK); break; } long newStartDate = _cal.getTimeInMillis(); if (newStartDate != _startSelectedDate) { setSelectedDateSpan(new DateSpan(newStartDate, newStartDate)); ensureDateVisible(newStartDate); } // Restore the original time value. _cal.setTimeInMillis(_firstDisplayedDate); } /** * If we are in a mode that allows for range selection this method * will extend the currently selected range. * * NOTE: This may not be the expected behavior for the keyboard controls * and we ay need to update this code to act in a way that people expect. */ private void addToSelection(int action) { long newStartDate = _startSelectedDate; long newEndDate = _endSelectedDate; boolean isForward = true; switch (action) { case ADD_PREVIOUS_DAY: _cal.setTimeInMillis(_startSelectedDate); _cal.add(Calendar.DAY_OF_MONTH, -1); newStartDate = _cal.getTimeInMillis(); isForward = false; break; case ADD_NEXT_DAY: _cal.setTimeInMillis(_endSelectedDate); _cal.add(Calendar.DAY_OF_MONTH, 1); newEndDate = _cal.getTimeInMillis(); break; case ADD_TO_PREVIOUS_WEEK: _cal.setTimeInMillis(_startSelectedDate); _cal.add(Calendar.DAY_OF_MONTH, -DAYS_IN_WEEK); newStartDate = _cal.getTimeInMillis(); isForward = false; break; case ADD_TO_NEXT_WEEK: _cal.setTimeInMillis(_endSelectedDate); _cal.add(Calendar.DAY_OF_MONTH, DAYS_IN_WEEK); newEndDate = _cal.getTimeInMillis(); break; } if (newStartDate != _startSelectedDate || newEndDate != _endSelectedDate) { setSelectedDateSpan(new DateSpan(newStartDate, newEndDate)); ensureDateVisible(isForward ? newEndDate : newStartDate); } // Restore the original time value. _cal.setTimeInMillis(_firstDisplayedDate); } } }