jfxtras.scene.control.ListSpinner Maven / Gradle / Ivy
/**
* ListSpinner.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.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;
import jfxtras.internal.scene.control.skin.LocalTimePickerSkin;
/**
* This is a spinner, showing one value at a time from a list.
* This value is set and retrieved from the value property.
* Basically a spinner shows a list of values and can do "next" or "previous" on this.
*
* 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, if defined the AddCallback is called.
* - 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).
*
* http://openjdk.java.net/projects/openjfx/ux/spinner/index.html
*
* 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 "readonly" or "editable" to style either mode specifically.
*
* @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 extends T> 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 extends Integer> 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 extends ObservableList> 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 extends T> 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 protected 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: */
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: */
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: */
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: */
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: */
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: */
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: */
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: */
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>: */
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: */
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: */
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