jfxtras.internal.scene.control.skin.ListSpinnerSkin Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jfxtras-controls Show documentation
Show all versions of jfxtras-controls Show documentation
Miscellaneous components for JavaFX
/**
* 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.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.control.TextField;
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.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.RowConstraints;
import javafx.util.Callback;
import javafx.util.Duration;
import jfxtras.animation.Timer;
import jfxtras.css.CssMetaDataForSkinProperty;
import jfxtras.scene.control.ListSpinner;
import jfxtras.scene.layout.HBox;
import jfxtras.scene.layout.VBox;
import jfxtras.util.NodeUtil;
import javafx.css.converter.EnumConverter;
/**
*
* @author Tom Eugelink
*
* Possible extension: drop down list or grid for quick selection
*/
public class ListSpinnerSkin extends SkinBase>
{
// TODO: vertical centering
// ==================================================================================================================
// CONSTRUCTOR
/**
*
*/
public ListSpinnerSkin(ListSpinner control)
{
super(control);
construct();
}
/*
*
*/
private void construct()
{
// setup component
createNodes();
// react to value changes in the model
getSkinnable().editableProperty().addListener( (ObservableValue extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
replaceValueNode();
});
replaceValueNode();
// react to value changes in the model
getSkinnable().valueProperty().addListener( (ObservableValue extends T> observable, T oldValue, T newValue) -> {
refreshValue();
});
refreshValue();
// react to value changes in the model
setArrowCSS();
layout();
// react to value changes in the model
alignValue();
}
/*
*
*/
private void refreshValue()
{
// if editable
if (getSkinnable().isEditable() == true)
{
// update textfield
T lValue = getSkinnable().getValue();
textField.setText( getSkinnable().getPrefix() + getSkinnable().getStringConverter().toString(lValue) + getSkinnable().getPostfix() );
}
else
{
// get node for this value
getSkinnable().getCellFactory().call( getSkinnable() );
}
}
// ==================================================================================================================
// StyleableProperties
/**
* arrowPosition
*/
public final ObjectProperty arrowPositionProperty() { return arrowPosition; }
private ObjectProperty arrowPosition = new SimpleStyleableObjectProperty(StyleableProperties.ARROW_POSITION, this, "arrowPosition", StyleableProperties.ARROW_POSITION.getInitialValue(null)) {
{ // anonymous constructor
addListener( (invalidationEvent) -> {
setArrowCSS();
layout();
});
}
};
public final void setArrowPosition(ArrowPosition value) { arrowPositionProperty().set(value); }
public final ArrowPosition getArrowPosition() { return arrowPosition.get(); }
public final ListSpinnerSkin withArrowPosition(ArrowPosition value) { setArrowPosition(value); return this; }
public enum ArrowPosition {LEADING, TRAILING, SPLIT}
/**
* arrowDirection
*/
public final ObjectProperty arrowDirectionProperty() { return arrowDirection; }
private ObjectProperty arrowDirection = new SimpleStyleableObjectProperty(StyleableProperties.ARROW_DIRECTION, this, "arrowDirection", StyleableProperties.ARROW_DIRECTION.getInitialValue(null)) {
{ // anonymous constructor
addListener( (invalidationEvent) -> {
setArrowCSS();
layout();
});
}
};
public final void setArrowDirection(ArrowDirection value) { arrowDirectionProperty().set(value); }
public final ArrowDirection getArrowDirection() { return arrowDirection.get(); }
public final ListSpinnerSkin withArrowDirection(ArrowDirection value) { setArrowDirection(value); return this; }
public enum ArrowDirection {VERTICAL, HORIZONTAL}
/**
* valueAlignment
*/
public final ObjectProperty valueAlignmentProperty() { return valueAlignment; }
private ObjectProperty valueAlignment = new SimpleStyleableObjectProperty(StyleableProperties.VALUE_ALIGNMENT, this, "valueAlignment", StyleableProperties.VALUE_ALIGNMENT.getInitialValue(null)) {
{ // anonymous constructor
addListener( (invalidationEvent) -> {
alignValue();
});
}
};
public final void setValueAlignment(Pos value) { valueAlignmentProperty().set(value); }
public final Pos getValueAlignment() { return valueAlignment.get(); }
public final ListSpinnerSkin withValueAlignment(Pos value) { setValueAlignment(value); return this; }
// -------------------------
private static class StyleableProperties
{
private static final CssMetaData, ArrowPosition> ARROW_POSITION = new CssMetaDataForSkinProperty, ListSpinnerSkin>, ArrowPosition>("-fxx-arrow-position", new EnumConverter(ArrowPosition.class), ArrowPosition.TRAILING ) {
@Override
protected ObjectProperty getProperty(ListSpinnerSkin> s) {
return s.arrowPositionProperty();
}
};
private static final CssMetaData, ArrowDirection> ARROW_DIRECTION = new CssMetaDataForSkinProperty, ListSpinnerSkin>, ArrowDirection>("-fxx-arrow-direction", new EnumConverter(ArrowDirection.class), ArrowDirection.HORIZONTAL ) {
@Override
protected ObjectProperty getProperty(ListSpinnerSkin> s) {
return s.arrowDirectionProperty();
}
};
private static final CssMetaData, Pos> VALUE_ALIGNMENT = new CssMetaDataForSkinProperty, ListSpinnerSkin>, Pos>("-fxx-value-alignment", new EnumConverter(Pos.class), Pos.CENTER_LEFT ) {
@Override
protected ObjectProperty getProperty(ListSpinnerSkin> s) {
return s.valueAlignmentProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables = new ArrayList>(SkinBase.getClassCssMetaData());
styleables.add(ARROW_POSITION);
styleables.add(ARROW_DIRECTION);
styleables.add(VALUE_ALIGNMENT);
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 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.
* Spinner uses a GridPane where the arrows and the node for the value are laid out according to the arrows direction and location.
* A place holder in inserted into the GridPane to hold the value node, so the spinner can alternate between editable or readonly mode, without having to recreate the GridPane.
*/
private void createNodes()
{
// left arrow
decrementArrow = new Region();
decrementArrow.getStyleClass().add("idle");
// place holder for showing the value
valueHolderNode = new BorderPane();
valueHolderNode.getStyleClass().add("valuePane");
//valueHolderNode.setStyle("-fx-border-color: white;");
// right arrow
incrementArrow = new Region();
incrementArrow.getStyleClass().add("idle");
// construct a placeholder node
skinNode = new BorderPane();
skinNode.setCenter(valueHolderNode);
// we're not catching the mouse events on the individual children, but let it bubble up to the parent and handle it there, this makes our life much more simple
// process mouse clicks
skinNode.setOnMouseClicked( (mouseEvent) -> {
// if click was the in the greater vicinity of the decrement arrow
if (mouseEventOverArrow(mouseEvent, decrementArrow))
{
// left
NodeUtil.addStyleClass(decrementArrow, "clicked");
NodeUtil.removeStyleClass(incrementArrow, "clicked");
getSkinnable().decrement();
unclickTimer.restart();
return;
}
// if click was the in the greater vicinity of the increment arrow
if (mouseEventOverArrow(mouseEvent, incrementArrow))
{
// right
NodeUtil.removeStyleClass(decrementArrow, "clicked");
NodeUtil.addStyleClass(incrementArrow, "clicked");
getSkinnable().increment();
unclickTimer.restart();
return;
}
});
// process mouse holds
skinNode.setOnMousePressed( evt -> {
// if click was the in the greater vicinity of the decrement arrow
if (mouseEventOverArrow(evt, decrementArrow))
{
// left
decrementArrow.getStyleClass().add("clicked");
repeatDecrementClickTimer.restart();
}
// if click was the in the greater vicinity of the increment arrow
else if (mouseEventOverArrow(evt, incrementArrow))
{
// right
incrementArrow.getStyleClass().add("clicked");
repeatIncrementClickTimer.restart();
return;
}
// if a control does not have the focus, request focus
ListSpinner lControl = getSkinnable();
if (!lControl.isFocused() && lControl.isFocusTraversable()) {
lControl.requestFocus();
}
});
skinNode.setOnMouseReleased( evt -> {
unclickArrows();
repeatDecrementClickTimer.stop();
repeatIncrementClickTimer.stop();
});
skinNode.setOnMouseExited( evt -> {
unclickArrows();
repeatDecrementClickTimer.stop();
repeatIncrementClickTimer.stop();
});
// mouse wheel
skinNode.setOnScroll( evt -> {
// if click was the in the greater vicinity of the decrement arrow
if (evt.getDeltaY() < 0 || evt.getDeltaX() < 0)
{
// left
NodeUtil.addStyleClass(decrementArrow, "clicked");
NodeUtil.removeStyleClass(incrementArrow, "clicked");
getSkinnable().decrement();
unclickTimer.restart();
return;
}
// if click was the in the greater vicinity of the increment arrow
if (evt.getDeltaY() > 0 || evt.getDeltaX() > 0)
{
// right
NodeUtil.removeStyleClass(decrementArrow, "clicked");
NodeUtil.addStyleClass(incrementArrow, "clicked");
getSkinnable().increment();
unclickTimer.restart();
return;
}
});
// key events
getSkinnable().onKeyTypedProperty().set( keyEvent -> {
KeyCode lKeyCode = keyEvent.getCode();
if ( KeyCode.MINUS.equals(lKeyCode)
|| KeyCode.SUBTRACT.equals(lKeyCode)
|| KeyCode.DOWN.equals(lKeyCode)
|| KeyCode.LEFT.equals(lKeyCode)
) {
getSkinnable().decrement();
}
if ( KeyCode.PLUS.equals(lKeyCode)
|| KeyCode.ADD.equals(lKeyCode)
|| KeyCode.UP.equals(lKeyCode)
|| KeyCode.RIGHT.equals(lKeyCode)
) {
getSkinnable().increment();
}
});
// 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(skinNode);
}
private Region decrementArrow = null;
private Region incrementArrow = null;
private BorderPane skinNode = null;
private BorderPane valueHolderNode;
// timer to remove the click styling on the arrows after a certain delay
final private Timer unclickTimer = new Timer( () -> {
unclickArrows();
}).withDelay(Duration.millis(100)).withRepeats(false);
// timer to handle the holding of the decrement button
final private Timer repeatDecrementClickTimer = new Timer( () -> {
getSkinnable().decrement();
}).withDelay(Duration.millis(500)).withCycleDuration(Duration.millis(50));
// timer to handle the holding of the increment button
final private Timer repeatIncrementClickTimer = new Timer( () -> {
getSkinnable().increment();
}).withDelay(Duration.millis(500)).withCycleDuration(Duration.millis(50));
/**
* Check if the mouse event is considered to have happened over the arrow
* @param evt
* @param region
* @return
*/
private boolean mouseEventOverArrow(MouseEvent evt, Region region)
{
// if click was the in the greater vicinity of the arrow
Point2D lClickInRelationToArrow = region.sceneToLocal(evt.getSceneX(), evt.getSceneY());
if ( lClickInRelationToArrow.getX() >= 0.0 && lClickInRelationToArrow.getX() <= region.getWidth()
&& lClickInRelationToArrow.getY() >= 0.0 && lClickInRelationToArrow.getY() <= region.getHeight()
)
{
return true;
}
return false;
}
/**
* Remove clicked CSS styling from the arrows
*/
private void unclickArrows()
{
decrementArrow.getStyleClass().remove("clicked");
incrementArrow.getStyleClass().remove("clicked");
}
/**
* Put the correct node for the value's place holder:
* - either the TextField when in editable mode,
* - or a node generated by the cell factory when in readonly mode.
*/
private void replaceValueNode()
{
// clear
valueHolderNode.getChildren().clear();
// if not editable
if (getSkinnable().isEditable() == false)
{
// use the cell factory
Node lNode = getSkinnable().getCellFactory().call(getSkinnable());
//lNode.setStyle("-fx-border-color: blue;");
valueHolderNode.setCenter(lNode);
if (lNode.getStyleClass().contains("value") == false) lNode.getStyleClass().add("value");
if (lNode.getStyleClass().contains("readonly") == false) lNode.getStyleClass().add("readonly");
}
else
{
// use the textfield
if (textField == null)
{
textField = new TextField();
textField.getStyleClass().add("value");
textField.getStyleClass().add("editable");
// process text entry
textField.focusedProperty().addListener(new InvalidationListener()
{
@Override
public void invalidated(Observable arg0)
{
if (textField.isFocused() == false)
{
parse(textField);
}
}
});
textField.setOnAction( (actionEvent) -> {
parse(textField);
});
textField.setOnKeyPressed( (keyEvent) -> {
if (keyEvent.getCode() == KeyCode.ESCAPE)
{
// refresh
refreshValue();
}
});
// alignment
textField.alignmentProperty().bind(valueAlignmentProperty());
}
valueHolderNode.setCenter(textField);
//textField.setStyle("-fx-border-color: blue;");
}
// align
alignValue();
}
private TextField textField = null;
/**
* align the value inside the plave holder
*/
private void alignValue()
{
// valueHolderNode always only holds one child (the value)
BorderPane.setAlignment(valueHolderNode.getChildren().get(0), valueAlignmentProperty().getValue());
}
// ==================================================================================================================
// EDITABLE
/**
* Parse the contents of the textfield
* @param textField
*/
protected void parse(TextField textField)
{
// get the text to parse
String lText = textField.getText();
// process it
parse(lText);
// refresh
refreshValue();
return;
}
/**
* Lays out the spinner, depending on the location and direction of the arrows.
*/
private void layout()
{
// get the things we decide on
ArrowDirection lArrowDirection = getArrowDirection();
ArrowPosition lArrowPosition = getArrowPosition();
// get helper values
ColumnConstraints lColumnValue = new ColumnConstraints(valueHolderNode.getMinWidth(), valueHolderNode.getPrefWidth(), Double.MAX_VALUE);
lColumnValue.setHgrow(Priority.ALWAYS);
// get helper values
RowConstraints lRowValue = new RowConstraints(valueHolderNode.getMinHeight(), valueHolderNode.getPrefHeight(), Double.MAX_VALUE);
lRowValue.setVgrow(Priority.ALWAYS);
// create the grid
skinNode.getChildren().clear();
skinNode.setCenter(valueHolderNode);
if (lArrowDirection == ArrowDirection.HORIZONTAL)
{
if (lArrowPosition == ArrowPosition.LEADING)
{
HBox lHBox = new HBox(0);
lHBox.add(decrementArrow, new HBox.C().hgrow(Priority.ALWAYS));
lHBox.add(incrementArrow, new HBox.C().hgrow(Priority.ALWAYS));
skinNode.setLeft(lHBox);
BorderPane.setAlignment(lHBox, Pos.CENTER_LEFT);
//lHBox.setStyle("-fx-border-color: blue;");
}
if (lArrowPosition == ArrowPosition.TRAILING)
{
HBox lHBox = new HBox(0);
lHBox.add(decrementArrow, new HBox.C().hgrow(Priority.ALWAYS));
lHBox.add(incrementArrow, new HBox.C().hgrow(Priority.ALWAYS));
skinNode.setRight(lHBox);
BorderPane.setAlignment(lHBox, Pos.CENTER_RIGHT);
//lHBox.setStyle("-fx-border-color: blue;");
}
if (lArrowPosition == ArrowPosition.SPLIT)
{
skinNode.setLeft(decrementArrow);
skinNode.setRight(incrementArrow);
BorderPane.setAlignment(decrementArrow, Pos.CENTER_LEFT);
BorderPane.setAlignment(incrementArrow, Pos.CENTER_RIGHT);
}
}
if (lArrowDirection == ArrowDirection.VERTICAL)
{
if (lArrowPosition == ArrowPosition.LEADING)
{
VBox lVBox = new VBox(0);
lVBox.add(incrementArrow, new VBox.C().vgrow(Priority.ALWAYS));
lVBox.add(decrementArrow, new VBox.C().vgrow(Priority.ALWAYS));
skinNode.setLeft(lVBox);
BorderPane.setAlignment(lVBox, Pos.CENTER_LEFT);
//lVBox.setStyle("-fx-border-color: blue;");
}
if (lArrowPosition == ArrowPosition.TRAILING)
{
VBox lVBox = new VBox(0);
lVBox.add(incrementArrow, new VBox.C().vgrow(Priority.ALWAYS));
lVBox.add(decrementArrow, new VBox.C().vgrow(Priority.ALWAYS));
skinNode.setRight(lVBox);
BorderPane.setAlignment(lVBox, Pos.CENTER_RIGHT);
//lVBox.setStyle("-fx-border-color: blue;");
}
if (lArrowPosition == ArrowPosition.SPLIT)
{
skinNode.setTop(incrementArrow);
skinNode.setBottom(decrementArrow);
BorderPane.setAlignment(incrementArrow, Pos.TOP_CENTER);
BorderPane.setAlignment(decrementArrow, Pos.BOTTOM_CENTER);
}
}
}
/**
* Set the CSS according to the direction of the arrows, so the correct arrows are shown
*/
private void setArrowCSS()
{
decrementArrow.getStyleClass().remove("down-arrow");
decrementArrow.getStyleClass().remove("left-arrow");
incrementArrow.getStyleClass().remove("up-arrow");
incrementArrow.getStyleClass().remove("right-arrow");
if (getArrowDirection().equals(ArrowDirection.HORIZONTAL))
{
decrementArrow.getStyleClass().add("left-arrow");
incrementArrow.getStyleClass().add("right-arrow");
}
else
{
decrementArrow.getStyleClass().add("down-arrow");
incrementArrow.getStyleClass().add("up-arrow");
}
}
// ==================================================================================================================
// EDITABLE
/**
* Parse the value (which usually comes from the TextField in the skin).
* If the value exists in the current items, select it.
* If not and a callback is defined, call the callback to have it handle it.
* Otherwise do nothing (leave it to the skin).
*/
public void parse(String text)
{
// strip
String lText = text;
String lPostfix = getSkinnable().getPostfix();
if (lPostfix.length() > 0 && lText.endsWith(lPostfix)) {
lText = lText.substring(0, lText.length() - lPostfix.length());
}
String lPrefix = getSkinnable().getPrefix();
if (lPrefix.length() > 0 && lText.startsWith(lPrefix)) {
lText = lText.substring(lPrefix.length());
}
// convert from string to value
T lValue = getSkinnable().getStringConverter().fromString(lText);
// if the value does exists in the domain
int lItemIndex = getSkinnable().getItems().indexOf(lValue);
if (lItemIndex >= 0)
{
// accept value and bail out
getSkinnable().setValue(lValue);
return;
}
// check to see if we have a addCallback
Callback lAddCallback = getSkinnable().getAddCallback();
if (lAddCallback != null)
{
// call the callback
Integer lIndex = lAddCallback.call(lValue);
// if the callback reports that it has processed the value by returning the index where it has added the item. (Or at least the index it wants to show now.)
if (lIndex != null)
{
// accept value and bail out
getSkinnable().setIndex(lIndex);
return;
}
}
}
}