All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.guise.framework.component.DateTimeFieldsControl Maven / Gradle / Ivy

There is a newer version: 0.5.3
Show newest version
/*
 * Copyright © 2005-2008 GlobalMentor, Inc. 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.guise.framework.component;

import java.beans.PropertyVetoException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Pattern;

import com.globalmentor.beans.*;

import io.guise.framework.GuiseSession;
import io.guise.framework.component.Table.CellRepresentationStrategy;
import io.guise.framework.component.layout.*;
import io.guise.framework.converter.*;
import io.guise.framework.model.*;
import io.guise.framework.validator.*;

import static com.globalmentor.java.Classes.*;
import static com.globalmentor.java.Objects.*;
import static com.globalmentor.time.TimeZones.*;

/**
 * Control that allows selection of a date and/or a time, providing separate inputs for date/time fields with the option of a calendar popup.
 * @author Garret Wilson
 */
public class DateTimeFieldsControl extends AbstractLayoutValueControl { //TODO finish or delete class

	/** The visible date bound property. */
	//TODO del	public static final String DATE_PROPERTY=getPropertyName(DateTimeControl.class, "date");

	/** The list control containing the months. */
	private final ListControl monthListControl;

	/** @return The list control containing the months. */
	protected ListControl getMonthListControl() {
		return monthListControl;
	}

	/** The control containing the year; this control can change dynamically based upon the current model range. */
	private ValueControl yearControl = null;

	/** @return The control containing the year. */
	protected ValueControl getYearControl() {
		return yearControl;
	}

	/** The control containing the year. */
	//TODO del	private final TextControl yearControl;

	/** @return The control containing the year. */
	//TODO del		protected TextControl getYearControl() {return yearControl;}

	/** The control containing the month. */
	//TODO del	private final TextControl monthControl;

	/** @return The control containing the month. */
	//TODO del		protected TextControl getMonthControl() {return monthControl;}

	/** The control containing the day. */
	private final TextControl dayControl;

	/** @return The control containing the day. */
	protected TextControl getDayControl() {
		return dayControl;
	}

	/** The control containing the hour. */
	private final TextControl hourControl;

	/** @return The control containing the hour. */
	protected TextControl getHourControl() {
		return hourControl;
	}

	/** The control containing the minutes. */
	private final TextControl minuteControl;

	/** @return The control containing the minutes. */
	protected TextControl getMinuteControl() {
		return minuteControl;
	}

	/** The control containing the seconds. */
	private final TextControl secondControl;

	/** @return The control containing the day. */
	protected TextControl getSecondControl() {
		return secondControl;
	}

	/** The control containing the milliseconds. */
	private final TextControl millisecondControl;

	/** @return The control containing the milliseconds. */
	protected TextControl getMillisecondControl() {
		return millisecondControl;
	}

	/** The date being viewed, not necessarily chosen, or null if no date is being viewed. */
	//TODO del	private Date date;

	/** @return The date being viewed, not necessarily chosen, or null if no date is being viewed. */
	//TODO del		public Date getDate() {return date!=null ? (Date)date.clone() : null;}

	/**
	 * Sets the date being viewed. A copy will be made of the date before it is stored. This is a bound property.
	 * @param newDate The date to be viewed, not necessarily chosen.
	 * @throws NullPointerException if the given date is null.
	 * @see #DATE_PROPERTY
	 */
	/*TODO del
			public void setDate(final Date newDate)
			{
				if(!date.equals(requireNonNull(newDate, "Date cannot be null."))) {	//if the value is really changing
					final Date oldDate=date;	//get the old value
					date=(Date)newDate.clone();	//clone the new date and actually change the value
					updateDateControls();	//update the date controls based upon the new value
					firePropertyChange(DATE_PROPERTY, oldDate, newDate);	//indicate that the value changed
				}
			}
	*/

	/** The property change listener that updates the visible dates if the year is different than the last one. */
	protected final GenericPropertyChangeListener yearPropertyChangeListener;

	/** Default constructor with a default data model. */
	public DateTimeFieldsControl() {
		this(new DefaultValueModel(Date.class)); //construct the class with a default value model
	}

	/** The property change listener that updates the date controls when a property changes. */
	protected final GenericPropertyChangeListener updateDateControlsPropertyChangeListener;

	/** The property change listener that updates the visible dates if the year is different than the last one. */
	//TODO fix or del	protected final GenericPropertyChangeListener yearPropertyChangeListener;

	/**
	 * Value model constructor.
	 * @param valueModel The component value model.
	 * @throws NullPointerException if the given value model is null.
	 */
	public DateTimeFieldsControl(final ValueModel valueModel) {
		super(new FlowLayout(Flow.LINE), valueModel); //construct the parent class flowing along the line
		final Date date = valueModel.getValue(); //get the selected date
		//TODO del		date=selectedDate!=null ? selectedDate : new Date();	//set the currently visible date to the selected date, or the current date if no date is selected
		//TODO del		date=selectedDate;	//set the currently visible date to the selected date
		/*TODO del
					//YYYY
				yearControl=new TextControl(Integer.class);	//create the year control
				yearControl.setColumnCount(4);	//provide for sufficient characters for the year
				addComponent(yearControl);	//add the year control
					//MM
				monthControl=new TextControl(Integer.class);	//create the month control
				//TODO set the range
				monthControl.setColumnCount(2);	//provide for sufficient characters for the month
				addComponent(monthControl);	//add the month control
		*/
		addComponent(new Label("-"));
		monthListControl = new ListControl(Date.class, new SingleListSelectionPolicy()); //create a list control allowing only single selections of a month
		//TODO fix		monthListControl.setLabel("Month");	//set the month control label TODO get from resources
		//TODO fix		monthListControl.setValidator(new ValueRequiredValidator());	//require a locale to be selected in the list control
		monthListControl.setRowCount(1); //make this a drop-down list
		final Converter monthConverter = new DateStringLiteralConverter(DateStringLiteralStyle.MONTH_OF_YEAR); //get a converter to display the month of the year
		monthListControl.setValueRepresentationStrategy(new ListControl.DefaultValueRepresentationStrategy(monthConverter)); //install a month representation strategy
		addComponent(monthListControl); //add the month control
		addComponent(new Label("-"));
		//DD
		dayControl = new TextControl(Integer.class); //create the day control
		//TODO set the range
		dayControl.setColumnCount(2); //provide for sufficient characters for the day
		addComponent(dayControl); //add the day control
		//HH
		hourControl = new TextControl(Integer.class); //create the hour control
		//TODO set the range
		hourControl.setColumnCount(2); //provide for sufficient characters for the hours
		addComponent(hourControl); //add the day control
		addComponent(new Label(":"));
		//MM
		minuteControl = new TextControl(Integer.class); //create the minute control
		//TODO set the range
		minuteControl.setColumnCount(2); //provide for sufficient characters for the minutes
		addComponent(minuteControl); //add the minutes control
		addComponent(new Label(":"));
		//SS
		secondControl = new TextControl(Integer.class); //create the second control
		//TODO set the range
		secondControl.setColumnCount(2); //provide for sufficient characters for the seconds
		addComponent(secondControl); //add the second control
		addComponent(new Label("."));
		//sss
		millisecondControl = new TextControl(Integer.class); //create the millisecond control
		//TODO set the range
		millisecondControl.setColumnCount(3); //provide for sufficient characters for the milliseconds
		addComponent(millisecondControl); //add the millisecond control

		//TODO fix; default date for testing purposes only
		/*TODO del
				dateControl=new TextControl(Date.class, new Date());	//create a list control allowing only single selections of a month
				dateControl.setLabel("Date");	//set the date control label TODO get from resources
				final Converter dateConverter=new DateStringLiteralConverter(DateStringLiteralStyle.SHORT, TimeStringLiteralStyle.SHORT);	//get a converter to display the date in a numeric representation
				dateControl.setConverter(dateConverter);	//
				dateControl.setValidator(new ValueRequiredValidator());	//require a date
				dateControl.setColumnCount(10);	//provide for sufficient characters for the most common date format
		*/
		//TODO del		addComponent(dateControl);	//add the date control
		//create a year property change listener before we update the year control
		yearPropertyChangeListener = new AbstractGenericPropertyChangeListener() { //create a property change listener to listen for the year changing

			public void propertyChange(final GenericPropertyChangeEvent propertyChangeEvent) { //if the selected year changed
				/*TODO fix
										final Integer newYear=propertyChangeEvent.getNewValue();	//get the new selected year
										if(newYear!=null) {	//if a new year was selected (a null value can be sent when the model is cleared)
											final Calendar calendar=Calendar.getInstance(getSession().getLocale());	//create a new calendar
											calendar.setTime(getDate());	//set the calendar date to our currently displayed date
											if(calendar.get(Calendar.YEAR)!=newYear) {	//if the currently visible date is in another year
												calendar.set(Calendar.YEAR, newYear);	//change to the given year
												setDate(calendar.getTime());	//change the date to the given month, which will update the calenders TODO make sure that going from a 31-day month, for example, to a 28-day month will be OK, if the day is day 31
											}
										}
				*/
			}
		};
		updateYearControl(); //create and install an appropriate year control
		updateDateControls(); //update the date controls
		updateDateControlsPropertyChangeListener = new AbstractGenericPropertyChangeListener() { //create a property change listener to update the calendars

			public void propertyChange(final GenericPropertyChangeEvent propertyChangeEvent) { //if the model value value changed
				updateDateControls(); //update the date controls based upon the new selected date
			}
		};
		addPropertyChangeListener(VALUE_PROPERTY, updateDateControlsPropertyChangeListener); //update the calendars if the selected date changes
		addPropertyChangeListener(VALIDATOR_PROPERTY, new AbstractGenericPropertyChangeListener>() { //create a property change listener to listen for our validator changing, so that we can update the date control if needed

			@Override
			public void propertyChange(final GenericPropertyChangeEvent> propertyChangeEvent) { //if the model's validator changed
				updateYearControl(); //update the year control (e.g. a drop-down list) to match the new validator (e.g. a range validator), if any
			}

		});
	}

	/**
	 * Updates the year control by removing any old year control from the component and adding a new year control. If the model used by the calendar control uses
	 * a {@link RangeValidator} with a date range of less than 100 years, a drop-down list will be used for the year control. Otherwise, a text input will be used
	 * for year selection.
	 */
	protected void updateYearControl() {
		final ValueControl oldYearControl = yearControl; //get the old year control, which we'll replace
		if(oldYearControl != null) { //if there is a year control already in use
			removeComponent(yearControl); //remove our year control TODO later use controlContainer.replace() when that method is available
			oldYearControl.removePropertyChangeListener(ValueModel.VALUE_PROPERTY, yearPropertyChangeListener); //stop listening for the year changing
			yearControl = null; //for completeness, indicate that we don't currently have a year control
		}
		final Locale locale = getSession().getLocale(); //get the current locale
		//see if there is a minimum and maximum date specified; this will determine what sort of control to use for the date input
		int minYear = -1; //we'll determine if there is a minimum and/or maximum year restriction
		int maxYear = -1;
		final Validator validator = getValidator(); //get our validator
		if(validator instanceof RangeValidator) { //if there is a range validator installed
			final RangeValidator rangeValidator = (RangeValidator)validator; //get the validator as a range validator
			final Calendar calendar = Calendar.getInstance(locale); //create a new calendar for determining the year of the restricted dates
			final Date minDate = rangeValidator.getMinimum(); //get the minimum date
			if(minDate != null) { //if there is a minimum date specified
				calendar.setTime(minDate); //set the calendar date to the minimum date
				minYear = calendar.get(Calendar.YEAR); //get the minimum year to use
			}
			final Date maxDate = rangeValidator.getMaximum(); //get the maximum date
			if(maxDate != null) { //if there is a maximum date specified
				calendar.setTime(maxDate); //set the calendar date to the maximum date
				maxYear = calendar.get(Calendar.YEAR); //get the maximum year to use
			}
		}
		if(minYear >= 0 && maxYear >= 0 && maxYear - minYear < 100) { //if there is a minimum year and maximum year specified, use a drop-down control
			final ListControl yearListControl = new ListControl(Integer.class, new SingleListSelectionPolicy()); //create a list control allowing only single selections
			yearListControl.setRowCount(1); //make the list control a drop-down list
			for(int year = minYear; year <= maxYear; ++year) { //for each valid year
				yearListControl.add(Integer.valueOf(year)); //add this year to the choices
			}
			//TODO del			yearListControl.setValidator(new ValueRequiredValidator());	//require a value in the year drop-down
			yearControl = yearListControl; //use the year list control for the year control
		} else { //if minimum and maximum years are not specified, use a standard text control TODO update to use a spinner control as well, and auto-update the value once four characters are entered 
			final TextControl yearTextControl = new TextControl(Integer.class); //create a text control to select the year
			yearTextControl.setMaximumLength(4); //TODO testing
			yearTextControl.setColumnCount(4); //TODO testing
			//TODO fix or del			yearTextControl.setValidator(new IntegerRangeValidator(new Integer(1800), new Integer(2100), new Integer(1), true));	//restrict the range of the year TODO improve; don't arbitrarily restrict the range
			yearTextControl.setValidator(new IntegerRangeValidator(new Integer(1800), new Integer(2100), new Integer(1))); //restrict the range of the year TODO improve; don't arbitrarily restrict the range
			yearTextControl.setAutoCommitPattern(Pattern.compile("\\d{4}")); //automatically commit the year when four digits are entered
			yearControl = yearTextControl; //use the year text control for the year control
		}
		assert yearControl != null : "Failed to create a year control";
		//TODO fix if needed		yearControl.setStyleID("year");	//TODO use a constant
		//TODO del if not wanted		yearControl.setLabel("Year");	//set the year control label TODO get from resources

		yearControl.setLabel(getLabel()); //TODO improve
		yearControl.setLabelContentType(getLabelContentType()); //TODO improve

		//TODO del when works		final Date date=getDate();	//get the current date
		final Date date = getValue(); //get the current date
		if(date != null) { //if there is a date currently displayed
			final Calendar calendar = Calendar.getInstance(locale); //create a new calendar for setting the year
			calendar.setTime(date); //set the calendar date to our displayed date
			final int year = calendar.get(Calendar.YEAR); //get the current year
			try {
				yearControl.setValue(Integer.valueOf(year)); //show the selected year in the text box
			} catch(final PropertyVetoException propertyVetoException) { //we should never have a problem selecting a year or a month
				throw new AssertionError(propertyVetoException);
			}
		} else { //if there is no date displayed
			yearControl.clearValue(); //clear the year value
		}
		yearControl.addPropertyChangeListener(ValueModel.VALUE_PROPERTY, yearPropertyChangeListener); //listen for the year changing
		addComponent(0, yearControl); //add the year text control TODO use replaceComponent when available
	}

	/** The locale used the last time the date controls were updated, or null if no locale was known. */
	private Locale oldLocale = null;

	/** The month calendar used the last time the calendars were updated, or null if no calendar was known. */
	//TODO del	private Calendar oldCalendar=null;

	/** The month calendar used the last time the calendars were updated, or null if no calendar was known. */
	private Calendar calendar = null;

	/** The last year used for updating months, or -1 if no year was known. */
	//TODO del	private int oldMonthYear=-1;

	/** Whether we're currently updating the date controls, to avoid reentry from control events. */
	private boolean updatingDateControls = false;

	/**
	 * Updates the controls representing the date. This implementation updates the calendars on the calendar panel.
	 */
	protected synchronized void updateDateControls() {
		if(!updatingDateControls) { //if we're not already updating the calendars
			updatingDateControls = true; //show that we're updating the calendars
			try {

				//TODO del		Log.trace("*** Updating calendars");
				final GuiseSession session = getSession(); //get the Guise session
				final Integer oldMonthYearInteger = calendar != null ? Integer.valueOf(calendar.get(Calendar.YEAR)) : null; //get the year last used for calculating months
				final Locale locale = session.getLocale(); //get the current locale
				final boolean localeChanged = !locale.equals(oldLocale); //see if the locale changed
				if(calendar == null || localeChanged) { //if we don't have a month calendar, or the locale changed
					calendar = Calendar.getInstance(locale); //create a new calendar
					calendar.setTimeZone(TimeZone.getTimeZone(GMT_ID)); //change to GMT; we'll do the date offset ourselves
					//TODO del					monthCalendar.setTime(date!=null ? date : new Date());	//set the calendar date to our displayed date
				}

				//TODO fix				final Calendar calendar;	//determine which calendar to use

				//TODO del when works				final Date date=getDate();	//get the visible date
				final Date date = getValue(); //get the current date
				/*TODO fix
								final boolean dateChanged=oldCalendar==null || (date!=null && !oldCalendar.getTime().equals(date));	//we'll have to calculate all new dates if there was no calendar before or the dates diverged		
								if(localeChanged || dateChanged) {	//if the locale changed or the date changed
									calendar=Calendar.getInstance(locale);	//create a new calendar
									calendar.setTimeZone(TimeZone.getTimeZone(GMT_ID));	//change to GMT; we'll do the date offset ourselves
									calendar.setTime(date!=null ? date : new Date());	//set the calendar date to our displayed date
								}
								else {	//if we can keep the old calendar
									calendar=oldCalendar;	//keep the calendar we had before
								}
				*/
				//TODO fix				final long utcOffsetMilliseconds=session.getUTCOffset()/1000;	//get the UTC offset in milliseconds
				final Date monthDate = date != null ? date : new Date(); //get the date to use for determining months
				//TODO fix				calendar.setTime(new Date(monthDate.getTime()+utcOffsetMilliseconds));	//set the calendar date to the correct date, compensating for this session's offset
				final int year = calendar.get(Calendar.YEAR); //get the current year
				final boolean yearChanged = localeChanged || oldMonthYearInteger == null || oldMonthYearInteger.intValue() != year; //the year should be updated if the locale changed, there was no previous year, or the year changed
				final int month = calendar.get(Calendar.MONTH); //get the current month
				//TODO del if not needed				final boolean monthChanged=yearChanged || oldCalendar.get(Calendar.MONTH)!=month;	//the month should be updated if the the year or month changed
				try {
					yearControl.setValue(date != null ? Integer.valueOf(year) : null); //show the selected year in the text box
					if(yearChanged) { //if the year changed (different years can have different months with some calendars)
						monthListControl.clear(); //clear the values in the month list control
						final Calendar monthNameCalendar = (Calendar)calendar.clone(); //clone the month calendar as we step through the months
						final int minMonth = monthNameCalendar.getActualMinimum(Calendar.MONTH); //get the minimum month
						final int maxMonth = monthNameCalendar.getActualMaximum(Calendar.MONTH); //get the maximum month
						int namedMonthIndex = -1; //keep track of the named month index in the list
						for(int namedMonth = minMonth; namedMonth <= maxMonth; ++namedMonth) { //for each month
							++namedMonthIndex; //keep track of the list index
							monthNameCalendar.set(Calendar.MONTH, namedMonth); //switch to the given month
							monthListControl.add(monthNameCalendar.getTime()); //add this month date
							if(date != null && namedMonth == month) { //if this is the selected month
								monthListControl.setSelectedIndexes(namedMonthIndex); //select this month
							}
						}
					} else { //if the year didn't change, we still need to update the month
						monthListControl.setSelectedIndexes(date != null ? month - 1 : -1); //select this month
					}
					if(date != null) { //if there is a date, set the values
						final int day = calendar.get(Calendar.DAY_OF_MONTH); //get the current day
						dayControl.setValue(day); //set the day
						final int hour = calendar.get(Calendar.HOUR_OF_DAY); //get the current hour
						hourControl.setValue(hour); //set the hour
						final int minute = calendar.get(Calendar.MINUTE); //get the current minute
						minuteControl.setValue(minute); //set the minute
						final int second = calendar.get(Calendar.SECOND); //get the current second
						secondControl.setValue(second); //set the second
						final int millisecond = calendar.get(Calendar.MILLISECOND); //get the current milliseconds
						millisecondControl.setValue(minute); //set the millisecond
					} else { //if there is no date, clear the values
						dayControl.clearValue();
						hourControl.clearValue();
						minuteControl.clearValue();
						secondControl.clearValue();
						millisecondControl.clearValue();
					}
				} catch(final PropertyVetoException propertyVetoException) { //we should never have a problem selecting a year or a month
					throw new AssertionError(propertyVetoException);
				}
				/*TODO del if not needed
								if(monthChanged) {	//if the month needs updating
									final Calendar monthCalendar=(Calendar)calendar.clone();	//clone the calendar for stepping through the months
									final Container calendarContainer=getCalendarContainer();	//get the calendar container
									calendarContainer.clear();	//remove all calendars from the container
									final CellRepresentationStrategy dayRepresentationStrategy=createDayRepresentationStrategy();	//create a strategy for representing the days in the month calendar cells
									for(int monthIndex=0; monthIndex