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

jfxtras.scene.control.ListSpinner Maven / Gradle / Ivy

The newest version!
/**
 * ListSpinner.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.scene.control;

import java.util.Arrays;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventTarget;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.util.Callback;
import javafx.util.StringConverter;

/**
 * This is a spinner, showing one value at a time from a list.
 * This value is set and retrieved through the value property.
 * Basically a spinner shows a list of values and can do a "next" or "previous".
 * 
 * A spinner can be editable, the user can then type a value instead of selecting it.
 * If the value exists in the list, the spinner will simply jump to it. 
 * If the value does not exist, the AddCallback is called if defined.
 * 
 * - If the AddCallback returns null, spinner will only refresh the current index.
 * - If the AddCallback returns an Integer, spinner will jump to that index (usually the index where the new value was added to the list).   
 *
 * 
 * '''
 * 
 * In the default skin you can style the text in the control using CSS like so:
 * [source,css]
 * --
 * .ListSpinner .value { 
 *     -fx-font-weight: bold;
 * }
 * --
 * 
 * The "value" class applies to the text in both readonly and editable spinners, use the "readonly" or "editable" class to style either mode specifically.
 * There is a left-arrow, right-arrow, up-arrow and down-arrow class that uses a SVG path to draw the arrow, this can be overridden with another SVG to draw a different shape.
 * 
 * The default skin has a number of styleable properies which use the text representation of an enum for their value:
 * 
 * [source,css]
 * --
 * .ListSpinner { 
 *     -fxx-arrow-position: {LEADING, TRAILING, SPLIT}
 *     -fxx-arrow-direction: {VERTICAL, HORIZONTAL}
 *     -fxx-value-alignment: see javafx.geometry.Pos (https://docs.oracle.com/javase/8/javafx/api/javafx/geometry/Pos.html) 
 * }
 * --
 * 
 * @author Tom Eugelink
 */
public class ListSpinner extends Control
{
	// TODO: implement SelectionModel?
	// ==================================================================================================================
	// CONSTRUCTOR

	/**
	 */
	public ListSpinner()
	{
		construct();
	}

	// ------------
	// model
	
	/**
	 * @param items The item list used to populate the spinner.
	 */
	public ListSpinner(ObservableList items)
	{
		construct();
		setItems(items);
		first();
	}

	/**
	 * @param items The item list used to populate the spinner.
	 * @param startValue The initial value of the spinner (one of the items).
	 */
	public ListSpinner(ObservableList items, T startValue)
	{
		construct();
		setItems(items);
		setValue(startValue);
	}

	// ------------
	// convenience
	
	/**
	 * @param list
	 */
	public ListSpinner(final java.util.List list)
	{
		this( FXCollections.observableList(list) );
	}

	/**
	 * @param list
	 */
	public ListSpinner(T... list)
	{
		this( Arrays.asList(list) );
	}

	/**
	 * @param from
	 * @param to
	 */
	public ListSpinner(int from, int to)
	{
		this( (java.util.List) new ListSpinnerIntegerList(from, to) );
	}

	/**
	 * @param from
	 * @param to
	 * @param step
	 */
	public ListSpinner(int from, int to, int step)
	{
		this( (java.util.List) new ListSpinnerIntegerList(from, to, step) );
	}

	// ------------
	
	/*
	 * 
	 */
	private void construct()
	{
		// setup the CSS
		// the -fx-skin attribute in the CSS sets which Skin class is used
		this.getStyleClass().add(ListSpinner.class.getSimpleName());
		
		// react to changes of the value
		this.valueObjectProperty.addListener(new ChangeListener()
		{
			@Override
			public void changed(ObservableValue property, T oldValue, T newValue)
			{
				// get the value of the new index
				int lIdx = getItems().indexOf(newValue);
				
				// set the value
				if (ListSpinner.equals(indexObjectProperty.getValue(), lIdx) == false)
				{
					indexObjectProperty.setValue(lIdx);
				}
			}
		});
		
		// react to changes of the index
		this.indexObjectProperty.addListener(new ChangeListener()
		{
			@Override
			public void changed(ObservableValue property, Integer oldIndex, Integer newIndex)
			{
				// get the value of the new index
				T lValue = newIndex < 0 ? null : getItems().get(newIndex);
				
				// set the value
				if (ListSpinner.equals(valueObjectProperty.getValue(), lValue) == false)
				{
					valueObjectProperty.setValue(lValue);
				}
			}
		});
		
		// react to changes of the items
		this.itemsObjectProperty.addListener(new ChangeListener>()
		{
			@Override
			public void changed(ObservableValue> property, ObservableList oldList, ObservableList newList)
			{
				if (oldList != null) oldList.removeListener(listChangeListener);
				if (newList != null) newList.addListener(listChangeListener);
			}
		});
	}
	
	/*
	 * react to observable list changes
	 * TODO: what is sticky, index or value? Now: index
	 */
	private ListChangeListener listChangeListener = new ListChangeListener()
	{
		@Override
		public void onChanged(javafx.collections.ListChangeListener.Change change)
		{
			// get current index
			int lIndex = getIndex();
			
			// is it still valid?
			if (lIndex >= getItems().size()) 
			{
				lIndex = getItems().size() - 1;
				setIndex(lIndex);
				return;
			}
			
			// (re)set the value of the index
			valueObjectProperty.setValue( getItems().get(lIndex) );
		}
	};

	/**
	 * Return the path to the CSS file so things are setup right
	 */
	@Override public String getUserAgentStylesheet()
	{
		return ListSpinner.class.getResource("/jfxtras/internal/scene/control/" + ListSpinner.class.getSimpleName() + ".css").toExternalForm();
	}

	@Override public Skin createDefaultSkin() {
		return new jfxtras.internal.scene.control.skin.ListSpinnerSkin(this); 
	}

	// ==================================================================================================================
	// PROPERTIES

	/** Id */
	public ListSpinner withId(String value) { setId(value); return this; }

	/** Value: the currently show value of the list. */
	public ObjectProperty valueProperty() { return this.valueObjectProperty; }
	final private ObjectProperty valueObjectProperty = new SimpleObjectProperty(this, "value", null)
	{
		public void set(T value)
		{
			if (getItems().indexOf(value) < 0) throw new IllegalArgumentException("Value does not exist in the list: " + value); 
			super.set(value);
		}
	};
	// java bean API
	public T getValue() { return this.valueObjectProperty.getValue(); }
	public void setValue(T value) { this.valueObjectProperty.setValue(value); }
	public ListSpinner withValue(T value) { setValue(value); return this; }
	
	/** Index: the currently show index in the list. */
	public ObjectProperty indexProperty() { return this.indexObjectProperty; }
	final private ObjectProperty indexObjectProperty = new SimpleObjectProperty(this, "index", null)
	{
		public void set(Integer value)
		{
			if (value == null) throw new NullPointerException("Null not allowed as the value for index");
			if (value >= getItems().size()) throw new IllegalArgumentException("Index out of bounds: " + value + ", valid values are 0-" + (getItems().size() - 1)); 
			super.set(value);
		}
	};
	public Integer getIndex() { return this.indexObjectProperty.getValue(); }
	public void setIndex(Integer value) { this.indexObjectProperty.setValue(value); }
	public ListSpinner withIndex(Integer value) { setIndex(value); return this; }
	
	/** Cyclic: what happens at the beginning or end of the list, stop or cycle to the other end. */
	public ObjectProperty cyclicProperty() { return this.cyclicObjectProperty; }
	final private ObjectProperty cyclicObjectProperty = new SimpleObjectProperty(this, "cyclic", false)
	{
		public void set(Boolean value)
		{
			if (value == null) throw new NullPointerException("Null not allowed as the value for cyclic");
			super.set(value);
		}
	};
	public Boolean isCyclic() { return this.cyclicObjectProperty.getValue(); }
	public void setCyclic(Boolean value) { this.cyclicObjectProperty.setValue(value); }
	public ListSpinner withCyclic(Boolean value) { setCyclic(value); return this; }

	/** Editable: is the listspinner editable. It allows the user to type a value instead of only navigating to it, and if the AddCallback is defined, possibly also adding values. */
	public ObjectProperty editableProperty() { return this.editableObjectProperty; }
	final private ObjectProperty editableObjectProperty = new SimpleObjectProperty(this, "editable", false)
	{
		public void set(Boolean value)
		{
			if (value == null) throw new NullPointerException("Null not allowed as the value for editable");
			super.set(value);
		}
	};
	public Boolean isEditable() { return this.editableObjectProperty.getValue(); }
	public void setEditable(Boolean value) { this.editableObjectProperty.setValue(value); }
	public ListSpinner withEditable(Boolean value) { setEditable(value); return this; }

	/** Postfix: a string to be placed after the value, this can for example be a unit like "kg" */
	public ObjectProperty postfixProperty() { return this.postfixObjectProperty; }
	final private ObjectProperty postfixObjectProperty = new SimpleObjectProperty(this, "postfix", "");
	public String getPostfix() { return this.postfixObjectProperty.getValue(); }
	public void setPostfix(String value) { this.postfixObjectProperty.setValue(value); }
	public ListSpinner withPostfix(String value) { setPostfix(value); return this; }

	/** Prefix: a string to be placed before the list value, this can for example be a currency */
	public ObjectProperty prefixProperty() { return this.prefixObjectProperty; }
	final private ObjectProperty prefixObjectProperty = new SimpleObjectProperty(this, "prefix", "");
	public String getPrefix() { return this.prefixObjectProperty.getValue(); }
	public void setPrefix(String value) { this.prefixObjectProperty.setValue(value); }
	public ListSpinner withPrefix(String value) { setPrefix(value); return this; }

	/** Items: the list. */
	public ObjectProperty> itemsProperty() { return this.itemsObjectProperty; }
	final private ObjectProperty> itemsObjectProperty = new SimpleObjectProperty>(this, "items", null)
	{
		public void set(ObservableList value)
		{
			if (value == null) throw new NullPointerException("Null not allowed as the value for items");
			super.set(value);
		}
	};
	public ObservableList getItems() { return this.itemsObjectProperty.getValue(); }
	public void setItems(ObservableList value) { this.itemsObjectProperty.setValue(value); }
	public ListSpinner withItems(ObservableList value) { setItems(value); return this; }

	/** CellFactory: generate the cell to render a value */
	public ObjectProperty, Node>> cellFactoryProperty() { return this.cellFactoryObjectProperty; }
	final private ObjectProperty, Node>> cellFactoryObjectProperty = new SimpleObjectProperty, Node>>(this, "cellFactory", new DefaultCellFactory());
	public Callback, Node> getCellFactory() { return this.cellFactoryObjectProperty.getValue(); }
	public void setCellFactory(Callback, Node> value) { this.cellFactoryObjectProperty.setValue(value); }
	public ListSpinner withCellFactory(Callback, Node> value) { setCellFactory(value); return this; }

	/** StringConverter<T>: convert a value in the list to its string representation and (when in edit mode) vice versa. */
	public ObjectProperty> stringConverterProperty() { return this.stringConverterObjectProperty; }
	final private ObjectProperty> stringConverterObjectProperty = new SimpleObjectProperty>(this, "stringConverter", new DefaultStringConverter());
	public StringConverter getStringConverter() { return this.stringConverterObjectProperty.getValue(); }
	public void setStringConverter(StringConverter value) { this.stringConverterObjectProperty.setValue(value); }
	public ListSpinner withStringConverter(StringConverter value) { setStringConverter(value); return this; }

	/** AddCallback: this callback is called in editable mode when a value is entered that is not found in the list. 
	 *  It is up to the coder to added it to the list or not.
	 *  @return the index where of the position the ListSpinner must show or null (do nothing expect refresh the currently show index)
	 */
	public ObjectProperty> addCallbackProperty() { return this.addCallbackObjectProperty; }
	final private ObjectProperty> addCallbackObjectProperty = new SimpleObjectProperty>(this, "addCallback", null);
	public Callback getAddCallback() { return this.addCallbackObjectProperty.getValue(); }
	public void setAddCallback(Callback value) { this.addCallbackObjectProperty.setValue(value); }
	public ListSpinner withAddCallback(Callback value) { setAddCallback(value); return this; }

	// ==================================================================================================================
	// StringConverter
	
	/**
	 * A string converter that does a simple toString, but cannot convert to an object
	 * @see org.jfxextras.util.StringConverterFactory 
	 */
	class DefaultStringConverter extends StringConverter
	{
		@Override
		public T fromString(String string)
		{
			throw new IllegalStateException("No StringConverter is set. An editable Spinner must have a StringConverter to be able to render and parse the value.");
		}

		@Override
		public String toString(T value)
		{
			return value == null ? "" : value.toString();
		}
	}
	
	// ==================================================================================================================
	// CellFactory
	
	/**
	 * Default cell factory
	 */
	class DefaultCellFactory implements Callback, Node>
	{
		private Label label = null;
		
		@Override
		public Node call(ListSpinner spinner)
		{
			// get value
			T lValue = spinner.getValue();
			
			// label not yet created
			if (this.label == null) 
			{
				this.label = new Label();
			}
			this.label.setText( lValue == null ? "" : spinner.getPrefix() + getStringConverter().toString(lValue) + spinner.getPostfix() );
			return this.label;
		}
	};
	
	// ==================================================================================================================
	// EVENTS
	
	/** OnCycle: callback for when the list cycles to the other end in cyclic mode (for example to increase a year when a month ListSpinner skips from December to January) */
	public ObjectProperty> onCycleProperty() { return iOnCycleObjectProperty; }
	final private ObjectProperty> iOnCycleObjectProperty = new SimpleObjectProperty>(null);
	// java bean API
	public EventHandler getOnCycle() { return iOnCycleObjectProperty.getValue(); }
	public void setOnCycle(EventHandler value) { iOnCycleObjectProperty.setValue(value); }
	public ListSpinner withOnCycle(EventHandler value) { setOnCycle(value); return this; }
	final static public String ONCYCLE_PROPERTY_ID = "onCycle";
	
	/**
	 * CycleEvent 
	 */
	static public class CycleEvent extends Event
	{
		/**
		 * The only valid EventType for the CycleEvent.
		 */
		public static final EventType CYCLE = new EventType(Event.ANY, "CYCLE");

		/**
		 * 
		 */
		public CycleEvent()
		{
			super(CYCLE);
		}

		/**
		 * 
		 * @param source
		 * @param target
		 */
		public CycleEvent(Object source, EventTarget target)
		{
			super(source, target, new EventType());
		}
		
		public Object getOldIdx() { return this.oldIdx; }
		private Object oldIdx;
		
		public Object getNewIdx() { return this.newIdx; }
		private Object newIdx;
		
		
		public boolean cycledDown() { return cycleDirection == CycleDirection.TOP_TO_BOTTOM; }
		public boolean cycledUp() { return cycleDirection == CycleDirection.BOTTOM_TO_TOP; }
		CycleDirection cycleDirection;
	}
	
	/**
	 * we're cycling, fire the event
	 */
	public void fireCycleEvent(CycleDirection cycleDirection)
	{
		EventHandler lCycleEventHandler = getOnCycle();
		if (lCycleEventHandler != null)
		{
			CycleEvent lCycleEvent = new CycleEvent();
			lCycleEvent.cycleDirection = cycleDirection;
			lCycleEventHandler.handle(lCycleEvent);
		}
	}
	static public enum CycleDirection { TOP_TO_BOTTOM, BOTTOM_TO_TOP }
	
	
	// ==================================================================================================================
	// BEHAVIOR

	/**
	 * 
	 */
	public void first()
	{
		// nothing to do
		if (getItems() == null || getItems().size() == 0) return;
		
		// set the new index (this will update the value)
		indexObjectProperty.setValue(0);
	}
	
	/**
	 * 
	 */
	public void decrement()
	{
		// nothing to do
		if (getItems() == null || getItems().size() == 0) return;
		
		// get the current index
		int lOldIdx = this.indexObjectProperty.getValue();
					
		// get the previous index (usually current - 1)
		int lIdx = lOldIdx - 1;
		
		// if end
		if (lIdx < 0)
		{
			// if we're not cyclic
			if (isCyclic() != null && isCyclic().booleanValue() == false)
			{
				// do nothing
				return;
			}
			
			// cycle to the other end: get the last value
			lIdx = getItems().size() - 1;
			
			// notify listener that we've cycled
			fireCycleEvent(CycleDirection.BOTTOM_TO_TOP);
		}

		// set the new index (this will update the value)
		indexObjectProperty.setValue(lIdx);
	}
	
	/**
	 * 
	 */
	public void increment()
	{
		// nothing to do
		if (getItems() == null || getItems().size() == 0) return;
		
		// get the current index
		int lOldIdx = this.indexObjectProperty.getValue();
		
		// get the next index (usually current + 1)
		int lIdx = lOldIdx + 1;
		
		// if null is return, there is no next index (usually current + 1)
		if (lIdx >= getItems().size())
		{
			// if we're not cyclic
			if (isCyclic() != null && isCyclic().booleanValue() == false)
			{
				// do nothing
				return;
			}
			
			// cycle to the other end: get the first value
			lIdx = 0;
			
			// notify listener that we've cycled
			fireCycleEvent(CycleDirection.TOP_TO_BOTTOM);
		}
		
		// set the new index (this will update the value)
		indexObjectProperty.setValue(lIdx);
	}

	/**
	 * Get the last index; if the data provide is endless, this method mail fail!
	 */
	public void last()
	{
		// nothing to do
		if (getItems() == null || getItems().size() == 0) return;
		
		// set the new index (this will update the value)
		indexObjectProperty.setValue(getItems().size() - 1);
	}

	/**
	 * Does a o1.equals(o2) but also checks if o1 or o2 are null.
	 * @param o1
	 * @param o2
	 * @return True if the two values are equal, false otherwise.
	 */
	static public boolean equals(Object o1, Object o2)
	{
		if ( o1 == null && o2 == null ) return true;
		if ( o1 != null && o2 == null ) return false;
		if ( o1 == null && o2 != null ) return false;
		// TODO: compare arrays if (o1.getClass().isArray() && o2.getClass().isArray()) return Arrays.equals( (Object[])o1, (Object[])o2 );		
		return o1.equals(o2);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy