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

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

The newest version!
/**
 * Copyright (c) 2011-2021, 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 JFXTRAS 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 javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
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.KeyEvent;
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 jfxtras.scene.control.CalendarTimePicker;
import jfxtras.scene.control.CalendarTimeTextField;
import jfxtras.scene.control.ImageViewButton;
import jfxtras.util.NodeUtil;

/**
 * Allows editing the time in a text field.
 * Format is hardcoded to HH:MM:SS.mmm, but  
 * @author Tom Eugelink
 */
public class CalendarTimeTextFieldSkin extends SkinBase implements TextFieldSkin
{
	// ==================================================================================================================
	// CONSTRUCTOR
	
	/**
	 * 
	 */
	public CalendarTimeTextFieldSkin(CalendarTimeTextField control)
	{
		super(control);//, new CalendarTimeTextFieldBehavior(control));
		construct();
		// show where the skin is loaded from (for debugging in Ensemble) System.out.println("!!! " + this.getClass().getProtectionDomain().getCodeSource().getLocation());
	}

	/*
	 * 
	 */
	private void construct()
	{
		// setup component
		createNodes();

		// Bind
        textField.editableProperty().bindBidirectional(getSkinnable().editableProperty());

        // The enter event must be forwarded to support the DefaultButton
		textField.addEventHandler(KeyEvent.ANY, keyEvent -> {
			if (keyEvent.getCode() == KeyCode.ENTER) {
				getSkinnable().fireEvent(keyEvent);
			}
		});
		
		// react to value changes in the model
		getSkinnable().calendarProperty().addListener( (observable) -> {
			refreshValue();
		});
		getSkinnable().dateFormatProperty().addListener( (observable) -> {			
			refreshValue();
		});
		refreshValue();
		
		// focus
		initFocusSimulation();
            
        /**
         * If the user is triggering the property, we must show the popup.
         * We cannot bind the property directly to the popup because the
         * popup property is read only. MoreOver, we want to show the popup
         * next to the TextField, that's why we need to call showPopup();
         */
        getSkinnable().pickerShowingProperty().addListener((ObservableValue observable, Boolean oldValue, Boolean showing) -> {
            if (showing) {
                showPopup();
            } else if(popup != null){
                popup.hide();
            }
        });
	}

	/*
	 * 
	 */
	private void refreshValue()
	{
		// write out to textfield
		Calendar c = getSkinnable().getCalendar();
		String s = (c == null ? "" : getSkinnable().getDateFormat().format(c.getTime()));
		textField.setText( s );
	}
	
	/**
	 * When the control is focus, forward the focus to the textfield
	 */
    private void initFocusSimulation() {
        getSkinnable().focusedProperty().addListener((observable, wasFocused, isFocused) -> {
            if (isFocused) {
                textField.requestFocus();
            }
        });
    }
	
    @Override
	public void selectAll(){
            textField.selectAll();
        }

	// ==================================================================================================================
	// DRAW
	
	/**
	 * construct the nodes
	 */
	private void createNodes()
	{
		// the main textField
		textField = new TextField();
		textField.focusedProperty().addListener(new InvalidationListener()
		{			
			@Override
			public void invalidated(Observable arg0)
			{
				if (textField.isFocused() == false) 
				{
					parse();
				}
			}
		});
		textField.setOnAction(new EventHandler()
		{
			@Override
			public void handle(ActionEvent evt)
			{
				parse();
			}
		});
		textField.setOnKeyPressed(new EventHandler()
		{
			@Override
			public void handle(KeyEvent keyEvent)
			{
				if (keyEvent.getCode() == KeyCode.UP || keyEvent.getCode() == KeyCode.DOWN)
				{
					// parse the content
					parse();
					
					// get the calendar to modify
					Calendar lCalendar = (Calendar)getSkinnable().getCalendar().clone();
					
					// modify
					if (keyEvent.isControlDown()) lCalendar.add(Calendar.HOUR_OF_DAY, keyEvent.getCode() == KeyCode.UP ? 1 : -1);
					else lCalendar.add(Calendar.MINUTE, keyEvent.getCode() == KeyCode.UP ? getSkinnable().getMinuteStep() : -1 * getSkinnable().getMinuteStep());
					
					// set it
					getSkinnable().setCalendar( CalendarTimePickerSkin.blockMinutesToStep(lCalendar, getSkinnable().getMinuteStep()) );
				}
			}
		});
		// 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 time or use # for now, or +/-[h|m] for delta's (for example: -3m for minus 3 minutes)\nUse cursor up and down plus optional ctrl (hour) for quick keyboard changes."));
		}
        textField.promptTextProperty().bind(getSkinnable().promptTextProperty());

		// the icon
		imageView = new ImageViewButton();
		imageView.getStyleClass().add("icon");
		imageView.setPickOnBounds(true);
		imageView.setOnMouseClicked(new EventHandler()
		{
			@Override public void handle(MouseEvent evt)
			{
	            // no popup if the textfield is not editable (disabled is handled by the node)
				if (!textField.isEditable()) {
					return;
				}
				if (textField.focusedProperty().get() == true) 
				{
					parse();
				}
				getSkinnable().setPickerShowing(true);
			}
		});

		// 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(20, 100, 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()
	{
		try
		{
			// get the text to parse
			String lText = textField.getText();
			lText = lText.trim();
			if (lText.length() == 0)
			{
				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 hour, minute
				// TODO: internationalize?
				int lUnit = Calendar.DATE;
				if (lText.toLowerCase().endsWith("m")) { lText = lText.substring(0, lText.length() - 1); lUnit = Calendar.MINUTE; }
				if (lText.toLowerCase().endsWith("h")) { lText = lText.substring(0, lText.length() - 1); lUnit = Calendar.HOUR_OF_DAY; }

				// parse the delta
				int lDelta = Integer.parseInt(lText);
				Calendar lCalendar = (Calendar)getSkinnable().getCalendar().clone();
				lCalendar.add(lUnit, lDelta);

				// set the value
				getSkinnable().setCalendar( CalendarTimePickerSkin.blockMinutesToStep(lCalendar, getSkinnable().getMinuteStep()) );
			}
			else if (lText.equals("#"))
			{
				// set the value
				getSkinnable().setCalendar( CalendarTimePickerSkin.blockMinutesToStep(Calendar.getInstance(getSkinnable().getLocale()), getSkinnable().getMinuteStep()) );
			}
			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 e2) {} // we can safely ignore this, since we will fall back to the default formatter in the end
					}
					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 setupPopup() {
        popup = new Popup();
        popup.setAutoFix(true);
        popup.setAutoHide(true);
        popup.setHideOnEscape(true);

        // setup CSS
        BorderPane lBorderPane = new BorderPane() {
            // As of 1.8.0_40 CSS files are added in the scope of a control, the popup does not fall under the control, so the stylesheet must be reapplied 
            // When JFxtras is based on 1.8.0_40+: @Override 
            public String getUserAgentStylesheet() {
                return getSkinnable().getUserAgentStylesheet();
            }
        };
        lBorderPane.getStyleClass().add(this.getClass().getSimpleName() + "_popup");

		// prep the picker
		calendarTimePicker = new CalendarTimePicker();
		// bind our properties to the picker's
		//Bindings.bindBidirectional(calendarTimePicker.calendarProperty(), getSkinnable().calendarProperty()); // order is important, because the value of the first field is overwritten initially with the value of the last field
		Bindings.bindBidirectional(calendarTimePicker.minuteStepProperty(), getSkinnable().minuteStepProperty()); // order is important, because the value of the first field is overwritten initially with the value of the last field
		Bindings.bindBidirectional(calendarTimePicker.secondStepProperty(), getSkinnable().secondStepProperty()); // order is important, because the value of the first field is overwritten initially with the value of the last field
		// support immediate mode
		calendarTimePicker.calendarProperty().addListener( (observable) -> {
			if (getSkinnable().isImmediate()) {
				getSkinnable().calendarProperty().set(calendarTimePicker.calendarProperty().get());
			}
		});
        lBorderPane.setCenter(calendarTimePicker);

        // add a close button
        ImageView lImageView = new ImageViewButton();
        lImageView.getStyleClass().addAll("close-icon");
        lImageView.setPickOnBounds(true);
        lImageView.setOnMouseClicked(new EventHandler() {
            @Override
            public void handle(MouseEvent evt) {
                popup.hide();
                popup = null;
                getSkinnable().calendarProperty().set(calendarTimePicker.calendarProperty().get());
            }
        });
        lBorderPane.rightProperty().set(lImageView);

        // add pane
        popup.getContent().add(lBorderPane);
        popup.setOnShown((event) -> {
            ((CalendarTimePickerSkin) calendarTimePicker.getSkin()).labelDateFormatProperty().set(getSkinnable().getDateFormat());
        });

        /**
         * If the popup is showing/hiding, we must notify the property of the
         * CalendarTextField so that they are always in sync.
         */
        popup.showingProperty().addListener(new ChangeListener() {

            public void changed(ObservableValue observable, Boolean oldValue, Boolean showing) {
                getSkinnable().setPickerShowing(showing);
                if (!showing) {
                    popup.showingProperty().removeListener(this);
                    popup = null;
                }
            }
        });
    }
    private Popup popup = null;
	private CalendarTimePicker calendarTimePicker = null;

    /*
     * 
     */
    private void showPopup() {
        if(popup == null){
            setupPopup();
        }
        // show it just below the textfield
        calendarTimePicker.calendarProperty().set(getSkinnable().calendarProperty().get());
        double height = textField.getHeight() == 0.0 ? textField.prefHeight(-1): textField.getHeight();
        popup.show(textField, NodeUtil.screenX(getSkinnable()), NodeUtil.screenY(getSkinnable()) + height);

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy