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

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

There is a newer version: 17-r1
Show newest version
/**
 * CalendarTextFieldSkin.java
 *
 * Copyright (c) 2011-2014, 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.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import javafx.application.Platform;
import javafx.scene.control.SkinBase;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Popup;
import javafx.util.Callback;
import jfxtras.scene.control.CalendarPicker;
import jfxtras.scene.control.CalendarPicker.CalendarRange;
import jfxtras.scene.control.CalendarTextField;
import jfxtras.scene.control.ImageViewButton;
import jfxtras.scene.control.LocalDatePicker;
import jfxtras.scene.control.LocalDateTextField;
import jfxtras.scene.control.LocalDateTimePicker;
import jfxtras.scene.control.LocalDateTimeTextField;
import jfxtras.scene.layout.VBox;
import jfxtras.util.NodeUtil;

/**
 * 
 * @author Tom Eugelink
 * 
 * Possible extension: drop down list or grid for quick selection
 */
public class CalendarTextFieldSkin extends SkinBase
{
	// ==================================================================================================================
	// CONSTRUCTOR
	
	/**
	 * 
	 */
	public CalendarTextFieldSkin(CalendarTextField control)
	{
		super(control);
		construct();
	}

	/*
	 * 
	 */
	private void construct()
	{
		// setup component
		createNodes();
		
		// react to value changes in the model
		getSkinnable().calendarProperty().addListener( (observableValue, oldValue, newValue) -> { refreshValue(); });
        getSkinnable().dateFormatProperty().addListener( (observableValue, oldValue, newValue) -> { refreshValue(); });
        getSkinnable().textProperty().addListener( (observable) -> {
        	parse(getSkinnable().getText());
        });
		refreshValue();
		
		// focus
		initFocusSimulation();
	}
	
	/*
	 * 
	 */
	private void refreshValue()
	{
		// write out to textfield
		Calendar c = getSkinnable().getCalendar();
		String s = c == null ? "" : getSkinnable().getDateFormat().format( c.getTime() );
		textField.setText(s);
		if (s.equals(getSkinnable().getText()) == false) {
			getSkinnable().setText(s);
		}
	}
	
	/**
	 * When the control is focus, forward the focus to the textfield
	 */
    private void initFocusSimulation() 
    {
    	getSkinnable().focusedProperty().addListener( (observableValue, wasFocused, isFocused) -> {
			if (isFocused) {
            	Platform.runLater( () -> {
					textField.requestFocus();
				});
			}
		});
    }
	

	// ==================================================================================================================
	// DRAW
	
	/**
	 * construct the nodes
	 */
	private void createNodes()
	{
		// the main textField
		textField = new TextField();
		textField.setPrefColumnCount(20);
		textField.focusedProperty().addListener( (observable) -> {
			if (textField.isFocused() == false) {
				parse();
			}
		});
		textField.setOnAction( (actionEvent) ->  {
			parse();
		});
		textField.setOnKeyPressed( (keyEvent) ->  {
			if (keyEvent.getCode() == KeyCode.UP || keyEvent.getCode() == KeyCode.DOWN) {
				// parse the content
				parse();
				
				// get the calendar to modify
				Calendar lCalendar = getSkinnable().getCalendar();
				if (lCalendar == null) return;
				lCalendar = (Calendar)lCalendar.clone();
				
				// modify
				int lField = Calendar.DATE;
				if (keyEvent.isShiftDown() == false && keyEvent.isControlDown()) lField = Calendar.MONTH;
				if (keyEvent.isShiftDown() == false && keyEvent.isAltDown()) lField = Calendar.YEAR;
				if (keyEvent.isShiftDown() == true && keyEvent.isControlDown() &&  getSkinnable().getShowTime()) lField = Calendar.HOUR_OF_DAY;
				if (keyEvent.isShiftDown() == true && keyEvent.isAltDown() &&  getSkinnable().getShowTime()) lField = Calendar.MINUTE;
				lCalendar.add(lField, keyEvent.getCode() == KeyCode.UP ? 1 : -1);
				
				// set it
				getSkinnable().setCalendar(lCalendar);
			}
		});
		// bind the textField's tooltip to our (so it will show up) and give it a default value describing the mutation features
		textField.tooltipProperty().bindBidirectional(getSkinnable().tooltipProperty()); 
		if (getSkinnable().getTooltip() == null)
		{
			// TODO: internationalize the tooltip
			getSkinnable().setTooltip(new Tooltip("Type a date or use # for today, or +/-[d|w|m|y] for delta's (for example: -3m for minus 3 months)\nUse cursor up and down plus optional shift (week), ctrl (month) or alt (year) for quick keyboard changes."));
		}
        textField.promptTextProperty().bind(getSkinnable().promptTextProperty());

		// the icon
        imageView = new ImageViewButton();
		imageView.getStyleClass().add("icon");
		imageView.setPickOnBounds(true);
		imageView.setOnMouseClicked( (evt) -> {
			if (textField.focusedProperty().get() == true) {
				parse();
			}
			showPopup(evt);
		});
		
		// construct a gridpane: one row, two columns
		gridPane = new GridPane();
		gridPane.setHgap(3);
		gridPane.add(textField, 0, 0);
		gridPane.add(imageView, 1, 0);
		ColumnConstraints column0 = new ColumnConstraints(100, 10, Double.MAX_VALUE);
		column0.setHgrow(Priority.ALWAYS);
		gridPane.getColumnConstraints().addAll(column0); // first column gets any extra width
		
		// 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
		getChildren().add(gridPane);
	}
	private TextField textField = null;
	private ImageView imageView = null;
	private GridPane gridPane = null;

	/**
	 * parse the contents that was typed in the textfield
	 */
	private void parse() {
		parse(textField.getText());
	}
	
	/**
	 * parse the text
	 */
	private void parse(String lText)
	{
		try
		{
			// process the text 			
			lText = lText.trim();
			if (lText.length() == 0) 
			{
				if (getSkinnable().getAllowNull() == false) {
					throw new IllegalArgumentException("Empty string would result in null and that is not allowed");
				}
				getSkinnable().setCalendar(null);
				return;
			}
			
			// starts with - means substract days
			if (lText.startsWith("-") || lText.startsWith("+"))
			{
				// + has problems 
				if (lText.startsWith("+")) lText = lText.substring(1);
				
				// special units Day, Week, Month, Year
				// TODO: internationalize?
				int lUnit = Calendar.DATE;
				if (lText.toLowerCase().endsWith("d")) { lText = lText.substring(0, lText.length() - 1); lUnit = Calendar.DATE; }
				if (lText.toLowerCase().endsWith("w")) { lText = lText.substring(0, lText.length() - 1); lUnit = Calendar.WEEK_OF_YEAR; }
				if (lText.toLowerCase().endsWith("m")) { lText = lText.substring(0, lText.length() - 1); lUnit = Calendar.MONTH; }
				if (lText.toLowerCase().endsWith("y")) { lText = lText.substring(0, lText.length() - 1); lUnit = Calendar.YEAR; }
				
				// parse the delta
				int lDelta = Integer.parseInt(lText);
				Calendar lCalendar = (Calendar)getSkinnable().getCalendar().clone();
				lCalendar.add(lUnit, lDelta);
				
				// set the value
				getSkinnable().setCalendar(lCalendar);
			}
			else if (lText.equals("#"))
			{
				// set the value
				getSkinnable().setCalendar(Calendar.getInstance(getSkinnable().getLocale()));
			}
			else
			{
				try
				{
					Calendar lCalendar = getSkinnable().getCalendar();
					Date lDate = null;
					// First we're going to try the parsers in the list.
					// The user is free to decide to sequence here, if we would try the default first, that would not be the case.
					for (DateFormat lDateFormat : getSkinnable().getDateFormats())
					{
						try
						{
							// parse using the formatter
							lDate = lDateFormat.parse( lText );
							break; // exit the for loop
						}
						catch (java.text.ParseException | java.time.format.DateTimeParseException e2) {
							// we can safely ignore this, since we will fall back to the default formatter in the end
							// TODO: do we want a way to show these error messages? System.out.println(e2.getMessage());
						} 
					}
					if (lDate == null) 
					{
						// parse using the default formatter
						lDate = getSkinnable().getDateFormat().parse( lText );
					}
					
					// set the value (the parse with the default formatter either succeeded or threw an exception, skipping this code)
					lCalendar = Calendar.getInstance(getSkinnable().getLocale());
					lCalendar.setTime(lDate);
					getSkinnable().setCalendar(lCalendar);
				}
				finally
				{
					// always refresh
					refreshValue();
				}
			}
		}
		catch (Throwable t) 
		{ 
			// handle the exception
			// TODO: implement a default handler (show in popup / validation icon) 
			if (getSkinnable().getParseErrorCallback() != null) {
				getSkinnable().getParseErrorCallback().call(t);
			}
			else {
				t.printStackTrace();
			}
		} 
	}
	
	/*
	 * 
	 */
	private void showPopup(MouseEvent evt)
	{
		// create a picker
		CalendarPicker calendarPicker = new CalendarPicker();
		calendarPicker.setMode(CalendarPicker.Mode.SINGLE);
		calendarPicker.localeProperty().set(getSkinnable().localeProperty().get());
		calendarPicker.allowNullProperty().set(getSkinnable().allowNullProperty().get());
		calendarPicker.calendarProperty().set(getSkinnable().calendarProperty().get());
		calendarPicker.disabledCalendars().addAll(getSkinnable().disabledCalendars());
		calendarPicker.highlightedCalendars().addAll(getSkinnable().highlightedCalendars());
		calendarPicker.setCalendarRangeCallback(new Callback() {
			@Override
			public Void call(CalendarRange calendarRange) {
				Callback lCallback = getSkinnable().getCalendarRangeCallback();
				if (lCallback == null) {
					return null;
				}
				return lCallback.call(calendarRange);
			}
		});
		
		// TODO: replace with PopupControl because that is styleable (see C:/Users/user/Documents/openjfx/8.0rt/modules/controls/src/main/java/com/sun/javafx/scene/control/skin/ComboBoxPopupControl.java)
//			popup = new PopupControl() {
//
//	            @Override public Styleable getStyleableParent() {
//	                return CalendarTextFieldSkin.this.getSkinnable();
//	            }
//	            {
//	                setSkin(new Skin() {
//	                    @Override public Skinnable getSkinnable() { return CalendarTextFieldSkin.this.getSkinnable(); }
//	                    @Override public Node getNode() { return lBorderPane; }
//	                    @Override public void dispose() { }
//	                });
//	                getScene().getRoot().impl_processCSS(true);
//	            }
//	        };
		Popup lPopup = new Popup();
		lPopup.setAutoFix(true);
		lPopup.setAutoHide(true);
		lPopup.setHideOnEscape(true);
		BorderPane lBorderPane = new BorderPane();
		lBorderPane.getStyleClass().add(this.getClass().getSimpleName() + "_popup");
		lBorderPane.setCenter(calendarPicker);
		calendarPicker.showTimeProperty().set( getSkinnable().getShowTime() );
		
		// because the Java 8 DateTime classes use the CalendarPicker, we need to add some specific CSS classes here to support seamless CSS
		if (getSkinnable().getStyleClass().contains(LocalDateTextField.class.getSimpleName())) {
			calendarPicker.getStyleClass().addAll(LocalDatePicker.class.getSimpleName());
		}
		if (getSkinnable().getStyleClass().contains(LocalDateTimeTextField.class.getSimpleName())) {
			calendarPicker.getStyleClass().addAll(LocalDateTimePicker.class.getSimpleName());
		}
		
		// add a close and accept button if we're showing time
		if ( getSkinnable().getShowTime())
		{
			VBox lVBox = new VBox(); 
			lBorderPane.rightProperty().set(lVBox);
			
			ImageView lAcceptIconImageView = new ImageViewButton();
			lAcceptIconImageView.getStyleClass().addAll("accept-icon");
			lAcceptIconImageView.setPickOnBounds(true);
			lAcceptIconImageView.setOnMouseClicked( (mouseEvent) ->  {
				getSkinnable().calendarProperty().set(calendarPicker.calendarProperty().get());
				lPopup.hide(); 
			});
			lVBox.add(lAcceptIconImageView);
			
			ImageView lCloseIconImageView = new ImageViewButton();
			lCloseIconImageView.getStyleClass().addAll("close-icon");
			lCloseIconImageView.setPickOnBounds(true);
			lCloseIconImageView.setOnMouseClicked( (mouseEvent) ->  {
				lPopup.hide(); 
			});
			lVBox.add(lCloseIconImageView);
		}
		
		// if a value is selected in date mode, immediately close the popup
		calendarPicker.calendarProperty().addListener( (observable) -> {
			if (lPopup != null &&  getSkinnable().getShowTime() == false && lPopup.isShowing()) {
				lPopup.hide(); 
			}
		});

		// when the popup is hidden 
		lPopup.setOnHiding( (windowEvent) -> {
			// and time is not shown, the value must be set into the textfield
			if ( getSkinnable().getShowTime() == false) {
				getSkinnable().calendarProperty().set(calendarPicker.calendarProperty().get());
			}
			// but at least the textfield must be enabled again
			textField.setDisable(false);
		});
		
		// add to popup
		lPopup.getContent().add(lBorderPane);
		
		// show it just below the textfield
		textField.setDisable(true);
		lPopup.show(textField, NodeUtil.screenX(getSkinnable()), NodeUtil.screenY(getSkinnable()) + textField.getHeight());

		// move the focus over		
		calendarPicker.requestFocus(); // TODO: not working
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy