/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * RelativeDateValueEditor.java * Creation date: September 22, 2006 * By: Neil Corkum */ package org.openquark.gems.client.valueentry; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.DateFormatSymbols; import java.util.ArrayList; import java.util.List; import java.util.Locale; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JFormattedTextField; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.JToggleButton; import javax.swing.SpinnerNumberModel; import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.text.DefaultFormatter; import org.openquark.cal.valuenode.RelativeDateValueNode; import org.openquark.cal.valuenode.ValueNode; import com.ibm.icu.util.Calendar; import com.ibm.icu.util.TimeZone; /** * A RelativeDateValueEditor displays a calendar-like window which allows the user * to select a date. * * @author Neil Corkum */ class RelativeDateValueEditor extends ValueEditor { private static final long serialVersionUID = 8737584309664754353L; /** * A custom value editor provider for the RelativeDateValueEditor. */ public static class RelativeDateValueEditorProvider extends ValueEditorProvider<RelativeDateValueEditor> { public RelativeDateValueEditorProvider(ValueEditorManager valueEditorManager) { super(valueEditorManager); } /** * {@inheritDoc} */ @Override public boolean canHandleValue(ValueNode valueNode, SupportInfo providerSupportInfo) { return valueNode instanceof RelativeDateValueNode; } /** * @see org.openquark.gems.client.valueentry.ValueEditorProvider#getEditorInstance(ValueEditorHierarchyManager, ValueNode) */ @Override public RelativeDateValueEditor getEditorInstance(ValueEditorHierarchyManager valueEditorHierarchyManager, ValueNode valueNode) { RelativeDateValueEditor editor = new RelativeDateValueEditor(valueEditorHierarchyManager); editor.setOwnerValueNode(valueNode); return editor; } /** * {@inheritDoc} */ @Override public boolean usableForOutput() { return false; } } /** * Key listener for the buttons in this value editor. * Allows use of keyboard arrows to move around the date portion of * the calendar. * Gives the enter key the same effect as the space bar (presses current button). * * * @author Neil Corkum */ private class RelativeDateValueEditorKeyListener extends ValueEditorKeyListener { @Override public void keyPressed(KeyEvent evt) { int keyCode = evt.getKeyCode(); // check for escape pressed if (keyCode == KeyEvent.VK_ESCAPE) { // close editor without saving changes, unless month selector // popup menu is open, in which case do nothing (the combo box // will handle closing the popup menu if (!monthSelector.isPopupVisible()) { handleCancelGesture(); evt.consume(); } return; } // if enter pressed exit and save changes if (keyCode == KeyEvent.VK_ENTER) { handleCommitGesture(); evt.consume(); return; } Component component = evt.getComponent(); if (component instanceof JToggleButton) { JToggleButton button = (JToggleButton)component; int deltaDays; // difference between currently focused day and new day // check for arrow keys pressed if (keyCode == KeyEvent.VK_DOWN) { deltaDays = DAYS_IN_WEEK; } else if (keyCode == KeyEvent.VK_UP) { deltaDays = -DAYS_IN_WEEK; } else if (keyCode == KeyEvent.VK_LEFT) { deltaDays = -1; } else if (keyCode == KeyEvent.VK_RIGHT) { deltaDays = 1; } else { // no relevant key pressed; do nothing return; } // We must get the currently focused button's date, then convert // to a Calendar and increment/decrement the Calendar by a certain // number of days. The new day is then obtained from the calendar. // Just using an integer and adding/subtracting will not work for // a month in which each day is not one greater than the day before // it (eg. October 1582, in the Gregorian calendar) int day = dayButtons.indexOf(button) + 1; Calendar calendar = getUtcCalendar(); calendar.set(getDisplayedYear(), getDisplayedMonth(), day); // Add the day delta. Do nothing if it changes the month. calendar.add(Calendar.DAY_OF_MONTH, deltaDays); if (calendar.get(Calendar.MONTH) == getDisplayedMonth()) { int newDay = calendar.get(Calendar.DAY_OF_MONTH); JToggleButton newFocus = dayButtons.get(newDay - 1); newFocus.doClick(0); } } } } /** * Listener for action events from the date buttons and the month selector * combo box. * * @author Neil Corkum */ private class RelativeDateValueEditorActionListener implements ActionListener { /** * {@inheritDoc} */ public void actionPerformed(ActionEvent e) { // check if month changed if (e.getActionCommand().equals(monthSelector.getActionCommand())) { // update selected date so that it is in the month selected // use roll instead of set because roll handles going from // May 31 to June 30, for example, while set would go to July 1 int oldMonth = selectedDate.get(Calendar.MONTH); selectedDate.roll(Calendar.MONTH, getDisplayedMonth() - oldMonth); updateCalendar(); } else { // date was changed // The action command from a button is a string representing // the date the button shows. // Get this date and use it to set the new date. try { int dateSelected = Integer.parseInt(e.getActionCommand()); Calendar newDate = getUtcCalendar(); newDate.set(getDisplayedYear(), getDisplayedMonth(), dateSelected); setCalendar(newDate); } catch (NumberFormatException exc) { } } } } /** * Listener for change events from the year selector combo box. * @author Neil Corkum */ private class RelativeDateValueEditorChangeListener implements ChangeListener { /** * {@inheritDoc} */ public void stateChanged(ChangeEvent e) { // update selected date so that it is in the new selected year // use add instead of set because add handles going from // Feb 29 one leap year to Feb 28 the next year, whereas set will // say March 1 int oldYear = selectedDate.get(Calendar.YEAR); selectedDate.add(Calendar.YEAR, getDisplayedYear() - oldYear); updateCalendar(); } } /** Minimum year displayable and selectable in editor. */ private static final int MINIMUM_YEAR = 1; /** Days in week */ private static final int DAYS_IN_WEEK = 7; /** Months in year */ private static final int MONTHS_IN_YEAR = 12; /** Combo box holding list of months. */ private JComboBox monthSelector; /** Spinner field displaying year. */ private JSpinner yearSelector; /** List of buttons usable in UI. */ private List <JToggleButton> dayButtons; /** Panel holding layout of buttons representing days in month. */ private JPanel calendarPanel; /** Date currently selected. */ private Calendar selectedDate; /** * Constructor for RelativeDateValueEditor. * @param valueEditorHierarchyManager */ protected RelativeDateValueEditor(ValueEditorHierarchyManager valueEditorHierarchyManager) { super(valueEditorHierarchyManager); initialize(); } /** * {@inheritDoc} */ @Override protected void commitValue() { // Update the value in the ValueNode. ValueNode returnVN = new RelativeDateValueNode(getCalendar().getTime(), getValueNode().getTypeExpr()); replaceValueNode(returnVN, false); notifyValueCommitted(); } /** * {@inheritDoc} */ @Override public Component getDefaultFocusComponent() { return monthSelector; } /** * {@inheritDoc} */ @Override public void setInitialValue() { Calendar calendar = ((RelativeDateValueNode)getValueNode()).getCalendarValue(); setCalendar(calendar); } /** * Function to initialize the editor. */ private void initialize() { // set locale // TODO: get locale from GemCutter instead of using default Locale locale = Locale.getDefault(); setLocale(locale); // create listeners RelativeDateValueEditorKeyListener keyListener = new RelativeDateValueEditorKeyListener(); RelativeDateValueEditorActionListener actionListener = new RelativeDateValueEditorActionListener(); RelativeDateValueEditorChangeListener changeListener = new RelativeDateValueEditorChangeListener(); selectedDate = getUtcCalendar(); // set to current date int maxDaysInMonth = selectedDate.getMaximum(Calendar.DAY_OF_MONTH); // set up year selector spinner field SpinnerNumberModel numberModel = new SpinnerNumberModel(); numberModel.setMinimum(Integer.valueOf(MINIMUM_YEAR)); numberModel.setMaximum(Integer.valueOf(selectedDate.getActualMaximum(Calendar.YEAR))); yearSelector = new JSpinner(numberModel); yearSelector.setLocale(locale); yearSelector.setEditor(new JSpinner.NumberEditor(yearSelector, "0")); // set up month selector field monthSelector = new JComboBox(getMonthStrings()); monthSelector.setLocale(locale); // initialize buttons for all possible days in month dayButtons = new ArrayList<JToggleButton>(maxDaysInMonth); for (int day = 1; day <= maxDaysInMonth; day++) { JToggleButton button = new JToggleButton(getDayString(day)); button.setLocale(locale); // set buttons' action command to a string containing the date they represent button.setActionCommand(String.valueOf(day)); button.addActionListener(actionListener); button.addKeyListener(keyListener); dayButtons.add(button); } // initialize panels JPanel monthPanel = new JPanel(); JPanel yearPanel = new JPanel(); JPanel weekdayLabelPanel = new JPanel(); calendarPanel = new JPanel(); // set up month panel monthPanel.setLayout(new BoxLayout(monthPanel, BoxLayout.X_AXIS)); monthPanel.add(monthSelector); // set up year panel yearPanel.setLayout(new BoxLayout(yearPanel, BoxLayout.X_AXIS)); yearPanel.add(yearSelector); // set up calendar panel GridLayout calendarLayout = new GridLayout(0, DAYS_IN_WEEK); calendarPanel.setLayout(calendarLayout); // set up weekday label panel weekdayLabelPanel.setLayout(new GridLayout(1, DAYS_IN_WEEK)); String[] weekdayStrings = getWeekdayStrings(); for (int i = 0; i < DAYS_IN_WEEK; i++) { JLabel weekday = new JLabel(weekdayStrings[i], SwingConstants.CENTER); weekday.setLocale(locale); weekdayLabelPanel.add(weekday); } // set up main panel GridBagLayout gridBag = new GridBagLayout(); GridBagConstraints constraints = new GridBagConstraints(); setLayout(gridBag); constraints.fill = GridBagConstraints.BOTH; constraints.weightx = 1.0; gridBag.addLayoutComponent(monthPanel, constraints); add(monthPanel); constraints.gridwidth = GridBagConstraints.REMAINDER; gridBag.addLayoutComponent(yearPanel, constraints); add(yearPanel); gridBag.addLayoutComponent(weekdayLabelPanel, constraints); add(weekdayLabelPanel); gridBag.addLayoutComponent(calendarPanel, constraints); add(calendarPanel); // set up calendar to display current date setDisplayedMonth(selectedDate.get(Calendar.MONTH)); setDisplayedYear(selectedDate.get(Calendar.YEAR)); updateCalendar(); // set up listeners monthSelector.addActionListener(actionListener); monthSelector.addKeyListener(keyListener); yearSelector.addChangeListener(changeListener); JComponent yearEditor = yearSelector.getEditor(); if (yearEditor instanceof JSpinner.DefaultEditor) { final JFormattedTextField yearTextField = ((JSpinner.DefaultEditor)yearEditor).getTextField(); // add listener to text field of year selector spinner yearTextField.addKeyListener(keyListener); // listener to select year text when spinner is "spun" PropertyChangeListener spinListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { yearTextField.selectAll(); } }; yearTextField.addPropertyChangeListener("value", spinListener); // make calendar update whenever an edit is made to the year selection JFormattedTextField.AbstractFormatter formatter = yearTextField.getFormatter(); if (formatter instanceof DefaultFormatter) { DefaultFormatter defaultFormatter = (DefaultFormatter)formatter; defaultFormatter.setCommitsOnValidEdit(true); defaultFormatter.setAllowsInvalid(false); } } // set visible size of this editor setSize(getPreferredSize()); } /** * Gets the names of all months in the locale. * @return array of strings representing each month */ private String[] getMonthStrings() { String[] months = new String[MONTHS_IN_YEAR]; String[] allMonths = new DateFormatSymbols(getLocale()).getMonths(); for (int month = 0; month < MONTHS_IN_YEAR; month++) { months[month] = allMonths[month]; } return months; } /** * Gets the names of all weekdays in the locale. * The names are ordered in the correct order for the locale. * @return array of weekday names */ private String[] getWeekdayStrings() { String[] orderedNames = new String[DAYS_IN_WEEK]; String[] names = new DateFormatSymbols(getLocale()).getShortWeekdays(); // set day names in proper order for locale int day = selectedDate.getFirstDayOfWeek(); for (int i = 0; i < DAYS_IN_WEEK; i++) { orderedNames[i] = names[day]; day = (day % DAYS_IN_WEEK) + 1; // day value is 1-based } return orderedNames; } /** * Gets a string representing a given day of the month * @param date int value of date * @return corresponding to date */ private String getDayString(int date) { return String.valueOf(date); } /** * Updates calendar display and buttons to match month and year displayed. */ private void updateCalendar() { calendarPanel.invalidate(); calendarPanel.removeAll(); Calendar firstDayOfMonth = getUtcCalendar(); int displayedMonth = getDisplayedMonth(); int displayedYear = getDisplayedYear(); firstDayOfMonth.set(displayedYear, displayedMonth, 1); int firstDayOfWeek = firstDayOfMonth.getFirstDayOfWeek(); int firstDayOfWeekOfThisMonth = firstDayOfMonth.get(Calendar.DAY_OF_WEEK); List<Integer> daysInMonth = getDaysInMonth(displayedYear, displayedMonth); // calculate the number of blank spaces to leave at start of grid that will contain calendar int blankStartDays = firstDayOfWeekOfThisMonth - firstDayOfWeek; if (blankStartDays < 0) { blankStartDays += DAYS_IN_WEEK; } // insert blank sections in grid for (int i = 0; i < blankStartDays; i++) { calendarPanel.add(getCalendarSpacerComponent()); } // Need calendar to validate its internal fields so that no dates past // the end of the "valid" range are accepted. // This is a grotesque hack // add method seems to make calendar validate itself selectedDate.add(Calendar.MILLISECOND, -1); selectedDate.add(Calendar.MILLISECOND, 1); // add buttons to grid for (final Integer dayInMonth : daysInMonth) { int date = dayInMonth.intValue(); int dateSelected = selectedDate.get(Calendar.DAY_OF_MONTH); JToggleButton button = (dayButtons.get(date - 1)); // check for selected date button // set only the selected button to be focusable, this makes using // the TAB key to change focus work more intuitively if (date == dateSelected) { button.setSelected(true); button.setFocusable(true); button.setText("<html><b><u>" + getDayString(date) + "</u></b></html>"); } else { button.setSelected(false); button.setFocusable(false); button.setText("<html>" + getDayString(date) + "</html>"); } calendarPanel.add(button); } // add extra blank sections to ensure that grid is always 6 rows of dates final int necessaryGridComponents = DAYS_IN_WEEK * 5 + 1; for (int componentsInGrid = blankStartDays + daysInMonth.size(); componentsInGrid < necessaryGridComponents; componentsInGrid++) { calendarPanel.add(getCalendarSpacerComponent()); } // redraw calendar panel calendarPanel.validate(); calendarPanel.repaint(); } /** * Function to set the selected date */ public void setCalendar(Calendar date) { selectedDate = date; setDisplayedMonth(date.get(Calendar.MONTH)); setDisplayedYear(date.get(Calendar.YEAR)); updateCalendar(); // give focus to selected date button int day = selectedDate.get(Calendar.DAY_OF_MONTH); JToggleButton button = dayButtons.get(day - 1); button.requestFocus(); } /** * Gets the month displayed * @return the month currently displayed in the window */ private int getDisplayedMonth() { return monthSelector.getSelectedIndex(); } /** * Gets the year displayed * @return the year currently displayed in the window */ private int getDisplayedYear() { return ((Integer)yearSelector.getValue()).intValue(); } /** * Sets the month displayed * @param month the month */ private void setDisplayedMonth(int month) { monthSelector.setSelectedIndex(month); } /** * Sets the year displayed * @param year the year */ private void setDisplayedYear(int year) { yearSelector.setValue(Integer.valueOf(year)); } /** * Gets a list of days in the selected month. * @param year the year * @param month the month * @return List of dates in month */ private List<Integer> getDaysInMonth(int year, int month) { // set up calendar to first day of month Calendar calendar = getUtcCalendar(); calendar.clear(); calendar.set(year, month, 1); ArrayList<Integer> dayList = new ArrayList<Integer>(31); // lastCalendar is used to check the situation where the calendar hits // its maximum value and can no longer be increased Calendar lastCalendar = (Calendar)calendar.clone(); lastCalendar.add(Calendar.DAY_OF_MONTH, -1); // increase date and add to list until the next month is reached while ((calendar.get(Calendar.MONTH) == month) && !calendar.equals(lastCalendar)) { dayList.add(Integer.valueOf(calendar.get(Calendar.DAY_OF_MONTH))); calendar.add(Calendar.DAY_OF_MONTH, 1); lastCalendar.add(Calendar.DAY_OF_MONTH, 1); } return dayList; } /** * Gets the value of the selected date * @return the selected value in the editor */ public Calendar getCalendar() { return selectedDate; } /** * Gets a Calendar instance with the current locale and UTC time zone. * @return Calendar instance */ private Calendar getUtcCalendar() { return Calendar.getInstance(TimeZone.getTimeZone("UTC"), getLocale()); } /** * Gets an instance of an invisible component that can be used as a spacer. * Used in the calendar grid to fill in the spaces before and after actual * days in the month. * @return Invisible spacer component */ private Component getCalendarSpacerComponent() { Component glue = Box.createGlue(); glue.setVisible(false); return glue; } }