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

jfxtras.internal.scene.control.skin.CalendarPickerControlSkin Maven / Gradle / Ivy

There is a newer version: 17-r1
Show newest version
/**
 * CalendarPickerControlSkin.java
 *
 * Copyright (c) 2011-2016, JFXtras
 * 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 the organization 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  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.
 */

package jfxtras.internal.scene.control.skin;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.converter.EnumConverter;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.control.ToggleButton;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.util.Callback;
import jfxtras.css.CssMetaDataForSkinProperty;
import jfxtras.css.converters.SimpleDateFormatConverter;
import jfxtras.scene.control.CalendarPicker;
import jfxtras.scene.control.CalendarTimePicker;
import jfxtras.scene.control.ListSpinner;
import jfxtras.scene.control.ListSpinnerIntegerList;
import jfxtras.scene.layout.GridPane;
import jfxtras.util.NodeUtil;

/**
 * This skin uses regular JavaFX controls
 * @author Tom Eugelink
 *
 */
public class CalendarPickerControlSkin extends CalendarPickerMonthlySkinAbstract
{
	// ==================================================================================================================
	// CONSTRUCTOR
	
	/**
	 * 
	 */
	public CalendarPickerControlSkin(CalendarPicker control)
	{
		super(control);
		construct();
	}

	/*
	 * construct the component
	 */
	private void construct()
	{
		// setup component
		createNodes();
		layoutNodes();
		
		// start listening to changes
		// if the calendar changes, the display calendar will jump to show that
		getSkinnable().calendarProperty().addListener( (InvalidationListener) observable -> {
			Calendar calendar = getSkinnable().getCalendar();
			Calendar displayedCalendar = getSkinnable().getDisplayedCalendar();
			if (calendar != null && (displayedCalendar == null || calendar.get(Calendar.YEAR) != displayedCalendar.get(Calendar.YEAR) || calendar.get(Calendar.MONTH) != displayedCalendar.get(Calendar.MONTH)) ) {
				getSkinnable().setDisplayedCalendar(calendar);
			}
		});
		if (getSkinnable().getCalendar() != null) {
			getSkinnable().setDisplayedCalendar(getSkinnable().getCalendar());
		}
		
		// if the calendars change, the selection must be refreshed
		getSkinnable().calendars().addListener( (InvalidationListener) observable -> {
			refreshDayButtonToggleState();
		});
		
        // react to changes in the locale
        getSkinnable().localeProperty().addListener( (InvalidationListener) observable -> {
			monthListSpinner.setItems(FXCollections.observableArrayList(getMonthLabels()));

			// force change the locale in the displayed calendar
			getSkinnable().displayedCalendar().set( (Calendar)getSkinnable().getDisplayedCalendar().clone() );
			refresh();
        });

        // react to changes in the locale
        getSkinnable().showTimeProperty().addListener( (InvalidationListener) observable -> {
            layoutNodes();
        });

        // react to changes in the disabled calendars
        getSkinnable().disabledCalendars().addListener( new ListChangeListener(){
			@Override
			public void onChanged(javafx.collections.ListChangeListener.Change change) {
				refreshDayButtonsVisibilityAndLabel();
			}
	    });
        
        // react to changes in the highlighted calendars
        getSkinnable().highlightedCalendars().addListener(new ListChangeListener(){
			@Override
			public void onChanged(javafx.collections.ListChangeListener.Change arg0) {
				refreshDayButtonsVisibilityAndLabel();
			}
   	     });

		// react to changes in the displayed calendar
		getSkinnable().displayedCalendar().addListener( (InvalidationListener) observable -> {
			refresh();
		});

		// update the data
		refresh();
	}
	
	// ==================================================================================================================
	// PROPERTIES

	/**
	 * This skin has the displayed date always pointing to the first of the month
	 * @param displayedCalendar
	 * @return
	 */
	private Calendar deriveDisplayedCalendar(Calendar displayedCalendar)
	{
		// always the 1st of the month, without time
		Calendar lCalendar = Calendar.getInstance(getSkinnable().getLocale());
		lCalendar.set(Calendar.DATE, 1);
		lCalendar.set(Calendar.MONTH, displayedCalendar.get(Calendar.MONTH));
		lCalendar.set(Calendar.YEAR, displayedCalendar.get(Calendar.YEAR));
		lCalendar.set(Calendar.HOUR_OF_DAY, 0);
		lCalendar.set(Calendar.MINUTE, 0);
		lCalendar.set(Calendar.SECOND, 0);
		lCalendar.set(Calendar.MILLISECOND, 0);
		return lCalendar;
	}

	// ==================================================================================================================
	// StyleableProperties
	
	/** ShowWeeknumbers: */
    public final ObjectProperty showWeeknumbersProperty() { return showWeeknumbers; }
    private ObjectProperty showWeeknumbers = new SimpleStyleableObjectProperty(StyleableProperties.SHOW_WEEKNUMBERS, this, "showWeeknumbers", StyleableProperties.SHOW_WEEKNUMBERS.getInitialValue(null)) {
    	{ // anonymous constructor
			addListener( (invalidationEvent) -> {
                layoutNodes();
			});
		}
    };
    public final void setShowWeeknumbers(ShowWeeknumbers value) { showWeeknumbersProperty().set(value); }
    public final ShowWeeknumbers getShowWeeknumbers() { return showWeeknumbers.get(); }
    public final CalendarPickerControlSkin withShowWeeknumbers(ShowWeeknumbers value) { setShowWeeknumbers(value); return this; }
    public enum ShowWeeknumbers {YES, NO}
    
	/** LabelDateFormat: */
    public final ObjectProperty labelDateFormatProperty() { return labelDateFormat; }
    private ObjectProperty labelDateFormat = new SimpleStyleableObjectProperty(StyleableProperties.LABEL_DATEFORMAT, this, "labelDateFormat", StyleableProperties.LABEL_DATEFORMAT.getInitialValue(null)) {
    	{ // anonymous constructor
			addListener( (invalidationEvent) -> {
                refreshDayButtonsVisibilityAndLabel();
			});
		}
    };
    public final void setLabelDateFormat(DateFormat value) { labelDateFormatProperty().set(value); }
    public final DateFormat getLabelDateFormat() { return labelDateFormat.get(); }
    public final CalendarPickerControlSkin withLabelDateFormat(DateFormat value) { setLabelDateFormat(value); return this; }
    static private final SimpleDateFormat ID_DATEFORMAT = new SimpleDateFormat("yyyy-MM-dd");
    
    // ----------------------------
    // communicate the styleables

    private static class StyleableProperties {
    	
        private static final CssMetaData SHOW_WEEKNUMBERS = new CssMetaDataForSkinProperty("-fxx-show-weeknumbers", new EnumConverter(ShowWeeknumbers.class), ShowWeeknumbers.YES ) {
        	@Override 
        	protected ObjectProperty getProperty(CalendarPickerControlSkin s) {
            	return s.showWeeknumbersProperty();
            }
        };

        private static final CssMetaData LABEL_DATEFORMAT = new CssMetaDataForSkinProperty("-fxx-label-dateformat", new SimpleDateFormatConverter(), new SimpleDateFormat("d") ) {
        	@Override 
        	protected ObjectProperty getProperty(CalendarPickerControlSkin s) {
            	return s.labelDateFormatProperty();
            }
        };

        private static final List> STYLEABLES;
        static {
            final List> styleables = new ArrayList>(SkinBase.getClassCssMetaData());
            styleables.add(SHOW_WEEKNUMBERS);
            styleables.add(LABEL_DATEFORMAT);
            STYLEABLES = Collections.unmodifiableList(styleables);
        }
    }

    /**
     * @return The CssMetaData associated with this class, which may include the
     * CssMetaData of its super classes.
     */
    public static List> getClassCssMetaData() {
        return StyleableProperties.STYLEABLES;
    }

    /**
     * This method should delegate to {@link javafx.scene.Node#getClassCssMetaData()} so that
     * a Node's CssMetaData can be accessed without the need for reflection.
     * @return The CssMetaData associated with this node, which may include the
     * CssMetaData of its super classes.
     */
    public List> getCssMetaData() {
        return getClassCssMetaData();
    }


    // ==================================================================================================================
	// DRAW
	
	/**
	 * construct the nodes
	 */
	private void createNodes()
	{
		// setup the grid so all weekday togglebuttons will grow, but the weeknumbers do not
		ColumnConstraints lColumnConstraintsAlwaysGrow = new ColumnConstraints();
		lColumnConstraintsAlwaysGrow.setHgrow(Priority.ALWAYS);
		ColumnConstraints lColumnConstraintsNeverGrow = new ColumnConstraints();
		lColumnConstraintsNeverGrow.setHgrow(Priority.NEVER);

		// month spinner
		List lMonthLabels = getMonthLabels();
		monthListSpinner = new ListSpinner(lMonthLabels).withIndex(Calendar.getInstance().get(Calendar.MONTH)).withCyclic(Boolean.TRUE);
		monthListSpinner.setId("monthListSpinner");
		// on cycle overflow to year
		monthListSpinner.setOnCycle( (cycleEvent) -> {
			// if we've cycled down
			if (cycleEvent.cycledDown()) 
			{
				yearListSpinner.increment();
			}
			else 
			{
				yearListSpinner.decrement();				
			}
		});
		// if the value changed, update the displayed calendar
		monthListSpinner.valueProperty().addListener( (ObservableValue observable, String oldValue, String newValue) -> {
			setDisplayedCalendarFromSpinners();
		});
		
		// year spinner
		yearListSpinner = new ListSpinner(new ListSpinnerIntegerList()).withValue(Calendar.getInstance().get(Calendar.YEAR));
		yearListSpinner.setId("yearListSpinner");
		// if the value changed, update the displayed calendar
		yearListSpinner.valueProperty().addListener( (ObservableValue observable, Integer oldValue, Integer newValue) -> {
			setDisplayedCalendarFromSpinners();
		});
		
		// double click here to show today
		todayButton = new Button("   ");
        todayButton.getStyleClass().add("today-button");
        todayButton.setMinSize(16, 16);
		todayButton.setOnAction((ActionEvent event) -> {
            setToToday();
        });
		
		// weekday labels
		for (int i = 0; i < 7; i++)
		{
			// create buttons
			Label lLabel = new Label("" + i);
			// style class is set together with the label
			lLabel.getStyleClass().add("weekday-label"); 
			lLabel.setMaxWidth(Integer.MAX_VALUE); // this is one of those times; why the @#$@#$@#$ do I need to specify this in order to make the damn label centered?
			
			// remember the column it is associated with
			lLabel.setUserData(Integer.valueOf(i));
			lLabel.onMouseClickedProperty().set(weekdayLabelMouseClickedPropertyEventHandler);

			// remember it
			weekdayLabels.add(lLabel);
		}
		
		// weeknumber labels
		for (int i = 0; i < 6; i++)
		{
			// create buttons
			Label lLabel = new Label("" + i);
			lLabel.getStyleClass().add("weeknumber");
			lLabel.setAlignment(Pos.BASELINE_RIGHT);
			
			// remember it
			weeknumberLabels.add(lLabel);
			
			// remember the row it is associated with
			lLabel.setUserData(Integer.valueOf(i));
			lLabel.onMouseClickedProperty().set(weeknumerLabelMouseClickedPropertyEventHandler);
		}
		
		// setup: 6 rows of 7 days per week (which is the maximum number of buttons required in the worst case layout)
		for (int i = 0; i < 6 * 7; i++)
		{
			// create buttons
			ToggleButton lToggleButton = new ToggleButton("" + i);
			lToggleButton.setId("day" + i);
			lToggleButton.getStyleClass().add("day-button");
			lToggleButton.onMouseReleasedProperty().set(toggleButtonMouseReleasedPropertyEventHandler); // for minimal memory usage, use a single listener
			lToggleButton.onKeyReleasedProperty().set(toggleButtonKeyReleasedPropertyEventHandler); // for minimal memory usage, use a single listener
			
			// remember which button belongs to this property
			booleanPropertyToDayToggleButtonMap.put(lToggleButton.selectedProperty(), lToggleButton);
			
			// add it
			lToggleButton.setMaxWidth(Double.MAX_VALUE); // make the button grow to fill a GridPane's cell
			lToggleButton.setAlignment(Pos.BASELINE_CENTER);
			
			// remember it
			dayButtons.add(lToggleButton);
		}

		// add timepicker
		Bindings.bindBidirectional(timePicker.calendarProperty(), getSkinnable().calendarProperty());
		Bindings.bindBidirectional(timePicker.valueValidationCallbackProperty(), getSkinnable().valueValidationCallbackProperty());
		Bindings.bindBidirectional(timePicker.localeProperty(), getSkinnable().localeProperty());

		// add to self
        getSkinnable().getStyleClass().add(this.getClass().getSimpleName()); // always add self as style class, because CSS should relate to the skin not the control
	}
	// the result
	private ListSpinner monthListSpinner = null;
	private ListSpinner yearListSpinner = null;
	private Button todayButton = new Button("   ");
	final private List




© 2015 - 2024 Weber Informatics LLC | Privacy Policy