/******************************************************************************* * Copyright (c) 2005 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.bpel.common.ui.calendar; import java.text.DateFormatSymbols; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import org.eclipse.bpel.common.ui.Messages; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Region; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; /** * Creates a control that allows the user to select a date from a visual * calendar control. The control displays a month in a given year as well as * a list of days for that month. Users can select a date by clicking on a day * or navigating around the calendar with the arrow keys. * * The text graphic below outlines the key features of the calendar control. * * <pre> * |-----------------------| * | <| Month Year |> | * |-----------------------| * | S M T W T F S | * | |------------------| | * | | | | * | | | | * | | Days | | * | | | | * | | | | * | |------------------| | * |-----------------------| * </pre> * * These features include: * <ul> * <li> arrow buttons to move to the previous/next month * <li> a title showing the month and year that is displayed in the calendar * <li> a list of the names of each day of the week * <li> a day matrix allowing the user to select days in the current month * or the last/first days of the next/previous month. * </ul> * * The length of the day names can be set to a long name (3 characters long) or a * short name (1 character long). The setLongDayNames provides this functionality. */ public class CalendarControl extends Canvas { /** The dayMargin size when long day names are used */ private static final int LONG_DAY_MARGIN = 1; /** The dayMargin size when short day names are used */ private static final int SHORT_DAY_MARGIN = 5; /** The default number of rows to display in the calendar*/ private static final int ROW_COUNT = 6; /** The number of pixels off the edge of the title the next/previous * arrows will be drawn*/ private static final int ARROW_OFFSET = 5; /** The number of pixels between rows in the day matrix */ private static final int ROW_SPACING = 5; /** The width of the line separating each column in the day matrix */ private static final int LINE_WIDTH = 1; /** The margin size around the days of the week */ private static final int DAY_MARGIN = 3; /** The margin size around the month name*/ private static final int MONTH_MARGIN = 4; Calendar calendar = Calendar.getInstance(); private ArrayList<SelectionListener> selectionListeners = new ArrayList<SelectionListener>(); private CalendarPainter painter; private CalendarMouseAdapter mouseListener; int cellWidth; int cellHeight; int titleHeight; int width; int height; int lineStart; int lineEnd; int dayOfMonthMin; int dayOfMonthMax; int dayOfWeekMax; int startDayOfWeek; int lastDayOfPreviousMonth; int[] previousMonth; int[] nextMonth; String title; String[] days; private String[] months; /** The width used to separate day names */ private int dayMargin; /** The selected day of the month */ int day; public CalendarControl(Composite parent) { super(parent, SWT.NO_BACKGROUND); initializeControlContents(); hookControlListener(); updateVisuals(); } public void setTimeZone(String tz) { calendar.setTimeZone(TimeZone.getTimeZone(tz)); } private void initializeControlContents() { dayOfWeekMax = calendar.getActualMaximum(Calendar.DAY_OF_WEEK); DateFormatSymbols symbols = new DateFormatSymbols(); months = symbols.getMonths(); setLongDayNames(true); GC gc = new GC(this); Point dayMax = findMaximumSize(gc, days); Point monthMax = findMaximumSize(gc, months); gc.dispose(); cellWidth = dayMax.x + (dayMargin * 2) + LINE_WIDTH; cellHeight = dayMax.y + ROW_SPACING; titleHeight = monthMax.y + (MONTH_MARGIN * 2); width = dayOfWeekMax * cellWidth; height = (MONTH_MARGIN * 2) + monthMax.y + (DAY_MARGIN * 2) + dayMax.y + ROW_COUNT * (dayMax.y + ROW_SPACING); lineStart = titleHeight + dayMax.y + (DAY_MARGIN * 2); lineEnd = lineStart + ((dayMax.y + ROW_SPACING) * 6); // Calculate the length of the lines that make up // the triangle button. int lineLength = titleHeight - (ARROW_OFFSET * 2); // Calculate points of previous button polygon previousMonth = new int[6]; previousMonth[0] = ARROW_OFFSET; previousMonth[1] = titleHeight / 2; previousMonth[2] = ARROW_OFFSET + (lineLength / 2); previousMonth[3] = ARROW_OFFSET; previousMonth[4] = ARROW_OFFSET + (lineLength / 2); previousMonth[5] = ARROW_OFFSET + lineLength; // Calculate points of next button polygon nextMonth = new int[6]; nextMonth[0] = width - ARROW_OFFSET; nextMonth[1] = titleHeight / 2; nextMonth[2] = width - ARROW_OFFSET - (lineLength / 2); nextMonth[3] = ARROW_OFFSET; nextMonth[4] = width - ARROW_OFFSET - (lineLength / 2); nextMonth[5] = ARROW_OFFSET + lineLength; } private void hookControlListener() { addKeyListener(new CalendarKeyAdapter()); mouseListener = new CalendarMouseAdapter(); addMouseListener(mouseListener); painter = new CalendarPainter(getDisplay()); addPaintListener(painter); } private Point findMaximumSize(GC gc, String[] displayStrings) { Point max = new Point(0, 0); for (int i = 0; i < displayStrings.length; i++) { Point size = gc.stringExtent(displayStrings[i]); if (size.x > max.x) { max.x = size.x; } if (size.y > max.y) { max.y = size.y; } } return max; } int getMatrixIndex(int x, int y) { if (lineStart <= y && lineEnd >= y && x >= 0 && x <= width) { int row = (y - lineStart) / cellHeight; int col = x / cellWidth; return (row * dayOfWeekMax) + col + 1; } return -1; } void changeMonth(boolean next) { if (day > dayOfMonthMax) { day = dayOfMonthMax; } else if (day < dayOfMonthMin) { day = dayOfMonthMin; } if (next) { calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) + 1); } else { calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) - 1); } updateVisuals(); } void updateVisuals() { // Set calendar to first day of month so no wrapping occurs // whem the month is set calendar.set(Calendar.DAY_OF_MONTH, dayOfMonthMin); // Find first and last days of month dayOfMonthMin = calendar.getActualMinimum(Calendar.DAY_OF_MONTH); dayOfMonthMax = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); // Find the day which the first of the month falls on calendar.set(Calendar.DAY_OF_MONTH, dayOfMonthMin); startDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); // Find the last day of the previous month calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) - 1); lastDayOfPreviousMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) + 1); // Reset the day of the month. If the day is greater // than the last day in the month (i.e. current day is 31 // and switched to a month with 30 days), set day to the // last day of the month. if (day > dayOfMonthMax) { day = dayOfMonthMax; } // Cache new title and title width title = NLS.bind(Messages.CalendarControl_title, (new Object[] { months[calendar.get(Calendar.MONTH)], String.valueOf(calendar.get(Calendar.YEAR)) })); // Redraw the calendar CalendarControl.this.redraw(); } void fireSelectionChanged() { Event e = new Event(); e.widget = this; e.data = getSelectedDate(); for (int i = 0; i < selectionListeners.size(); i++) { SelectionListener listener = selectionListeners.get(i); SelectionEvent se = new SelectionEvent(e); listener.widgetSelected(se); if (!se.doit) break; } } /** * Adds the listener to the collection of listeners who will be notified when the * receiver's selection changes, by sending it one of the messages defined in the * SelectionListener interface. * * @param listener the listener which should be notified */ public void addSelectionListener(SelectionListener listener) { if (listener != null && !selectionListeners.contains(listener)) { selectionListeners.add(listener); } } /** * Removes the listener from the collection of listeners who will be notified when * the receiver's selection changes. * * @param listener the listener which should no longer be notified */ public void removeSelectionListener(SelectionListener listener) { if (listener != null) { selectionListeners.remove(listener); } } /** * Returns the date selected in the calendar control. * * @return the date selected in the calendar control. */ public Date getSelectedDate() { calendar.set(Calendar.DAY_OF_MONTH, day); Date date = calendar.getTime(); calendar.set(Calendar.DAY_OF_MONTH, dayOfMonthMin); return date; } /** * Sets the date that is selected in the calendar control. If the date is null * the current date will be used as the selected date. * * @param the new selected date for the calendar. */ public void setSelectedDate(Date selectedDate) { if (selectedDate == null) { selectedDate = new Date(); } calendar.setTime(selectedDate); day = calendar.get(Calendar.DAY_OF_MONTH); updateVisuals(); } /** * Determines whether the calendar is set to use short or long * day names. * * A short day name is the first character of a day name. For example, * the short day name for Saturday would be S. Long day names are a compressed * form of the full day name. For example, the long day name for Saturday would * be Sat. * * @return Returns true if the calendar is displaying long names or false if it * is displaying short names. */ public boolean isLongDayNames() { return dayMargin == LONG_DAY_MARGIN; } /** * Configures the calendar to use either short or long day names. * * @param useLongDayNames If useLongDayNames is true, the calendar will display * long day names, else it will display short day names. */ public void setLongDayNames(boolean useLongDayNames) { DateFormatSymbols symbols = new DateFormatSymbols(); days = symbols.getShortWeekdays(); if (useLongDayNames) { dayMargin = LONG_DAY_MARGIN; } else { dayMargin = SHORT_DAY_MARGIN; for (int i = 0; i < days.length; i++) { if (days[i].length() != 0) { days[i] = days[i].substring(0, 1); } } } } /* (non-Javadoc) * @see org.eclipse.swt.widgets.Widget#dispose() */ @Override public void dispose() { painter.dispose(); mouseListener.dispose(); super.dispose(); } /* (non-Javadoc) * @see org.eclipse.swt.widgets.Control#computeSize(int, int, boolean) */ @Override public Point computeSize(int wHint, int hHint, boolean changed) { return new Point(width, height); } /* (non-Javadoc) * @see org.eclipse.swt.widgets.Control#computeSize(int, int) */ @Override public Point computeSize(int wHint, int hHint) { return new Point(width, height); } /** * Responsible for handling all keyboard events of the calendar. The following * are a list of commands the key adapter listens for and their associated actions. * * <ul> * <li>Shift+Left - Change the calendar month to the previous month. * <li>Shift+Right - Change the calendar month to the next month. * <li>Up/Down - Move the day selection in the calendar up or down. * <li>Left/Right - Move the day selection in the calendar left or right. * </ul> * * If the new day that is selected is in the next/previous month, the calendar will * change its display to show that month. */ private class CalendarKeyAdapter extends KeyAdapter { @Override public void keyPressed(KeyEvent e) { int dayChange = 0; int monthChange = 0; switch (e.keyCode) { case SWT.ARROW_UP: if (day - dayOfWeekMax >= dayOfMonthMin) { dayChange -= dayOfWeekMax; } else { calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) - 1); int lastDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) + 1); day = (day - dayOfWeekMax) + lastDay; monthChange = -1; } break; case SWT.ARROW_DOWN: if (day + dayOfWeekMax <= dayOfMonthMax) { dayChange += dayOfWeekMax; } else { day = (day + dayOfWeekMax) - dayOfMonthMax; monthChange = 1; } break; case SWT.ARROW_LEFT: if ((e.stateMask & SWT.SHIFT) != 0) { changeMonth(false); } else { if (day != dayOfMonthMin) { dayChange--; } else { calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) - 1); day = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) + 1); monthChange = -1; } } break; case SWT.ARROW_RIGHT: if ((e.stateMask & SWT.SHIFT) != 0) { changeMonth(true); } else { if (day != dayOfMonthMax) { dayChange++; } else { day = dayOfMonthMin; monthChange = 1; } } break; } if (dayChange != 0) { day += dayChange; CalendarControl.this.redraw(); } if (monthChange != 0) { calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) + monthChange); updateVisuals(); } if (dayChange != 0 || monthChange != 0) { fireSelectionChanged(); } } } /** * Responsible for handling all mouse events of the calendar. The mouse adapter * implements the following behaviour: * * <ul> * <li>On mouse down on the left/right arrows in the title, the month displayed * on the calendar will shift to the previous/next month. * <li>On mouse down on a day, the day that is clicked is selected. * </ul> * * Note: dispose must be called on this object to release operating system * resources. */ private class CalendarMouseAdapter extends MouseAdapter { private Region previousRegion; private Region nextRegion; public CalendarMouseAdapter() { previousRegion = new Region(); previousRegion.add(previousMonth); nextRegion = new Region(); nextRegion.add(nextMonth); } /* (non-Javadoc) * @see org.eclipse.swt.events.MouseListener#mouseDown(org.eclipse.swt.events.MouseEvent) */ @Override public void mouseDown(MouseEvent e) { boolean selectionChanged = false; if (e.y > lineStart && e.y < lineEnd) { int offset = (startDayOfWeek == calendar.getFirstDayOfWeek()) ? dayOfWeekMax : 0; int selectedDay = getMatrixIndex(e.x, e.y) - startDayOfWeek + 1 - offset; if (selectedDay > dayOfMonthMax) { day = selectedDay - dayOfMonthMax; changeMonth(true); } else if (selectedDay < dayOfMonthMin) { calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) - 1); day = calendar.getActualMaximum(Calendar.DAY_OF_MONTH) + selectedDay; calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) + 1); changeMonth(false); } else { day = selectedDay; } selectionChanged = true; CalendarControl.this.redraw(); } else if (nextRegion.contains(e.x, e.y)) { calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) + 1); updateVisuals(); selectionChanged = true; } else if (previousRegion.contains(e.x, e.y)) { calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) - 1); updateVisuals(); selectionChanged = true; } if (selectionChanged) { fireSelectionChanged(); } } public void dispose() { if (previousRegion != null) { previousRegion.dispose(); previousRegion = null; } if (nextRegion != null) { nextRegion.dispose(); nextRegion = null; } } } /** * The paint listener for the control. It is responsible for drawing the * calendar control on the canvas. It is also responsible for creating and * disposing of the resources (i.e. colours, fonts, images) that are required * for drawing the control. * * Note: dispose must be called on this object to release operating system * resources. */ private class CalendarPainter implements PaintListener { private int currentDay; private int currentMonth; private int currentYear; private Color background; private Color foreground; private Color titleBackground; private Color titleForeground; private Color lineForeground; private Color disabledForeground; private Font currentDayFont; private Image buffer; public CalendarPainter(Display display) { currentDay = calendar.get(Calendar.DAY_OF_MONTH); currentMonth = calendar.get(Calendar.MONTH); currentYear = calendar.get(Calendar.YEAR); foreground = display.getSystemColor(SWT.COLOR_LIST_FOREGROUND); background = display.getSystemColor(SWT.COLOR_LIST_BACKGROUND); titleBackground = display.getSystemColor(SWT.COLOR_LIST_SELECTION); titleForeground = display.getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT); // FIXME The SWT.COLOR_GRAY should be replaced with the disabled color for text. // Bug 72207 requests this color constant (https://bugs.eclipse.org/bugs/show_bug.cgi?id=72207) lineForeground = display.getSystemColor(SWT.COLOR_GRAY); disabledForeground = display.getSystemColor(SWT.COLOR_GRAY); currentDayFont = createBoldFont(getFont()); buffer = new Image(display, width, height); } /* (non-Javadoc) * @see org.eclipse.swt.events.PaintListener#paintControl(org.eclipse.swt.events.PaintEvent) */ public void paintControl(PaintEvent e) { GC gc = new GC(buffer); gc.setBackground(background); gc.setForeground(foreground); gc.fillRectangle(0, 0, width, height); drawMonthName(gc); drawDayNames(gc); int predayCount = (startDayOfWeek == calendar.getFirstDayOfWeek()) ? dayOfWeekMax : startDayOfWeek - 1; drawDayRange(gc, 0, lastDayOfPreviousMonth - predayCount + 1, predayCount, true); drawDayRange(gc, predayCount, dayOfMonthMin, dayOfMonthMax, false); drawDayRange(gc, dayOfMonthMax + predayCount, 1, (ROW_COUNT * dayOfWeekMax) - predayCount - dayOfMonthMax, true); drawDayLines(gc); e.gc.drawImage(buffer, 0, 0); gc.dispose(); } public void dispose() { if (currentDayFont != null) { currentDayFont.dispose(); currentDayFont = null; } if (buffer != null) { buffer.dispose(); buffer = null; } } private void drawMonthName(GC gc) { Color gcBackground = gc.getBackground(); Color gcForeground = gc.getForeground(); // Draw title rectangle and month name gc.setBackground(titleBackground); gc.setForeground(titleForeground); gc.fillRectangle(0, 0, width, titleHeight); gc.drawString(title, (width - gc.stringExtent(title).x) / 2, MONTH_MARGIN); gc.setBackground(gcBackground); gc.setForeground(gcForeground); // Draw next/previous month arrows gc.fillPolygon(previousMonth); gc.fillPolygon(nextMonth); } private void drawDayNames(GC gc) { int x = 0; int y = titleHeight + DAY_MARGIN; int i = calendar.getFirstDayOfWeek(); do { if (days[i] != null && days[i].length() != 0) { Point size = gc.stringExtent(days[i]); gc.drawString(days[i], ((cellWidth - size.x) / 2) + 1 + x, y); x += cellWidth; } i = (i + 1) % days.length; } while (i != calendar.getFirstDayOfWeek()); } private void drawDayRange(GC gc, int index, int startDay, int length, boolean disabled) { int x = (index % dayOfWeekMax) * cellWidth; int y = (index / dayOfWeekMax) * cellHeight + lineStart; int dayOfWeek = (index % dayOfWeekMax) + 1; Color gcBackground = gc.getBackground(); Color gcForeground = gc.getForeground(); Font gcFont = gc.getFont(); if (disabled) { gc.setForeground(disabledForeground); } for (int i = startDay; i < startDay + length; i++) { String displayString = String.valueOf(i); if (!disabled && i == day) { gc.setBackground(titleBackground); gc.setForeground(titleForeground); } if (!disabled && currentDay == i && currentMonth == calendar.get(Calendar.MONTH) && currentYear == calendar.get(Calendar.YEAR)) { gc.setFont(currentDayFont); } Point size = gc.stringExtent(displayString); gc.drawString(displayString, ((cellWidth - size.x) / 2) + 1 + x, y); if (!disabled && i == day) { gc.setBackground(gcBackground); gc.setForeground(gcForeground); } if (!disabled && currentDay == i && currentMonth == calendar.get(Calendar.MONTH) && currentYear == calendar.get(Calendar.YEAR)) { gc.setFont(gcFont); } if (dayOfWeek % dayOfWeekMax == 0 && i != dayOfMonthMax) { y += cellHeight; x = 0; } else { x += cellWidth; } dayOfWeek = (dayOfWeek + 1) % dayOfWeekMax; } if (disabled) { gc.setForeground(gcForeground); } } private void drawDayLines(GC gc) { int x = cellWidth; Color gcForeground = gc.getForeground(); gc.setForeground(lineForeground); for (int i = 1; i < dayOfWeekMax; i++) { gc.drawLine(x, lineStart, x, lineEnd); x += cellWidth; } gc.setForeground(gcForeground); } private Font createBoldFont(Font font) { FontData[] data = font.getFontData(); for (int i = 0; i < data.length; i++) { data[i].setStyle(data[i].getStyle() | SWT.BOLD); } return new Font(Display.getCurrent(), data); } } }