changeset 7477:9e26a5eaed82

RT-5747: Add Spinner Control
author jgiles
date Sat, 12 Jul 2014 16:03:15 +1200
parents e9e4c7cc25ea
children 18b6955caf3f
files apps/toys/Hello/src/main/java/hello/HelloSpinner.java modules/controls/src/main/java/com/sun/javafx/scene/control/behavior/SpinnerBehavior.java modules/controls/src/main/java/com/sun/javafx/scene/control/skin/SpinnerSkin.java modules/controls/src/main/java/javafx/scene/control/Spinner.java modules/controls/src/main/java/javafx/scene/control/SpinnerValueFactory.java modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css modules/controls/src/test/java/javafx/scene/control/SpinnerTest.java
diffstat 7 files changed, 3484 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/toys/Hello/src/main/java/hello/HelloSpinner.java	Sat Jul 12 16:03:15 2014 +1200
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package hello;
+
+import javafx.application.Application;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.geometry.Insets;
+import javafx.geometry.NodeOrientation;
+import javafx.scene.Scene;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.SpinnerValueFactory;
+import javafx.scene.layout.GridPane;
+import javafx.stage.Stage;
+
+public class HelloSpinner extends Application {
+
+    public static void main(String[] args) {
+        Application.launch(args);
+    }
+
+    @Override public void start(Stage stage) {
+        final Spinner spinner = new Spinner();
+
+        // debug output to console
+        spinner.valueProperty().addListener((o, oldValue, newValue) ->
+                System.out.println("value changed: '" + oldValue + "' -> '" + newValue + "'"));
+        spinner.getEditor().textProperty().addListener((o, oldValue, newValue) ->
+                System.out.println("text changed: '" + oldValue + "' -> '" + newValue + "'"));
+
+        // this lets us switch between the spinner value factories
+        ComboBox<String> spinnerValueFactoryOptions =
+                new ComboBox<>(FXCollections.observableArrayList("Integer", "Double", "List<String>", "Calendar"));
+        spinnerValueFactoryOptions.getSelectionModel().selectedItemProperty().addListener((o, oldValue, newValue) -> {
+            switch (newValue) {
+                case "Integer": {
+                    spinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(5, 10));
+                    break;
+                }
+
+                case "List<String>": {
+                    ObservableList<String> items = FXCollections.observableArrayList("Jonathan", "Julia", "Henry");
+                    spinner.setValueFactory(new SpinnerValueFactory.ListSpinnerValueFactory<>(items));
+                    break;
+                }
+
+                case "Calendar": {
+                    spinner.setValueFactory(new SpinnerValueFactory.LocalDateSpinnerValueFactory());
+                    break;
+                }
+
+                case "Double": {
+                    spinner.setValueFactory(new SpinnerValueFactory.DoubleSpinnerValueFactory(0.0, 1.0, 0.5, 0.05));
+                    break;
+                }
+            }
+        });
+        spinnerValueFactoryOptions.getSelectionModel().select(0);
+
+        ComboBox<String> spinnerStyleClassOptions =
+                new ComboBox<>(FXCollections.observableArrayList(
+                        "Default (Arrows on right (Vertical))",
+                        "Arrows on right (Horizontal)",
+                        "Arrows on left (Vertical)",
+                        "Arrows on left (Horizontal)",
+                        "Split (Vertical)",
+                        "Split (Horizontal)"));
+        spinnerStyleClassOptions.getSelectionModel().selectedItemProperty().addListener((o, oldValue, newValue) -> {
+            spinner.getStyleClass().removeAll(
+                    Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL,
+                    Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL,
+                    Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL,
+                    Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL,
+                    Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
+
+            switch (newValue) {
+                case "Default (Arrows on right (Vertical))": break;
+
+                case "Arrows on right (Horizontal)": {
+                    spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL);
+                    break;
+                }
+
+                case "Arrows on left (Vertical)": {
+                    spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL);
+                    break;
+                }
+
+                case "Arrows on left (Horizontal)": {
+                    spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL);
+                    break;
+                }
+
+                case "Split (Vertical)": {
+                    spinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL);
+                    break;
+                }
+
+                case "Split (Horizontal)": {
+                    spinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
+                    break;
+                }
+            }
+        });
+        spinnerStyleClassOptions.getSelectionModel().select(0);
+
+        final CheckBox wrapAroundCheckBox = new CheckBox();
+        wrapAroundCheckBox.selectedProperty().addListener((o, oldValue, newValue) ->
+                spinner.getValueFactory().setWrapAround(newValue));
+
+        final CheckBox editableCheckBox = new CheckBox();
+        spinner.editableProperty().bind(editableCheckBox.selectedProperty());
+
+        final CheckBox rtlCheckBox = new CheckBox();
+        rtlCheckBox.selectedProperty().addListener((o, oldValue, newValue) ->
+                spinner.setNodeOrientation(newValue ? NodeOrientation.RIGHT_TO_LEFT : NodeOrientation.INHERIT));
+
+
+
+        GridPane grid = new GridPane();
+        grid.setHgap(10);
+        grid.setVgap(10);
+        grid.setPadding(new Insets(10));
+
+        int row = 0;
+
+        grid.add(new Label("Value Factory:"), 0, row);
+        grid.add(spinnerValueFactoryOptions, 1, row++);
+
+        grid.add(new Label("Style Class:"), 0, row);
+        grid.add(spinnerStyleClassOptions, 1, row++);
+
+        grid.add(new Label("Wrap around:"), 0, row);
+        grid.add(wrapAroundCheckBox, 1, row++);
+
+        grid.add(new Label("Editable:"), 0, row);
+        grid.add(editableCheckBox, 1, row++);
+
+        grid.add(new Label("Right-to-left:"), 0, row);
+        grid.add(rtlCheckBox, 1, row++);
+
+        grid.add(new Label("Spinner:"), 0, row);
+        grid.add(spinner, 1, row);
+
+        Scene scene = new Scene(grid, 350, 300);
+
+        stage.setTitle("Hello Spinner");
+        stage.setScene(scene);
+        stage.show();
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/controls/src/main/java/com/sun/javafx/scene/control/behavior/SpinnerBehavior.java	Sat Jul 12 16:03:15 2014 +1200
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.sun.javafx.scene.control.behavior;
+
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.SpinnerValueFactory;
+import javafx.util.Duration;
+
+import java.util.Collections;
+
+public class SpinnerBehavior<T> extends BehaviorBase<Spinner<T>> {
+
+    // this specifies how long the mouse has to be pressed on a button
+    // before the value steps. As the mouse is held down longer, we begin
+    // to cut down the duration of subsequent steps (and also increase the
+    // step size)
+    private static final double INITIAL_DURATION_MS = 750;
+
+    private final int STEP_AMOUNT = 1;
+
+    private boolean isIncrementing = false;
+
+    private Timeline timeline;
+
+    final EventHandler<ActionEvent> spinningKeyFrameEventHandler = event -> {
+        final SpinnerValueFactory<T> valueFactory = getControl().getValueFactory();
+        if (valueFactory == null) {
+            return;
+        }
+
+        if (isIncrementing) {
+            increment(STEP_AMOUNT);
+        } else {
+            decrement(STEP_AMOUNT);
+        }
+    };
+
+
+    public SpinnerBehavior(Spinner<T> spinner) {
+        super(spinner, Collections.emptyList());
+    }
+
+    public void increment(int steps) {
+        getControl().increment(steps);
+    }
+
+    public void decrement(int steps) {
+        getControl().decrement(steps);
+    }
+
+    public void startSpinning(boolean increment) {
+        isIncrementing = increment;
+
+        if (timeline != null) {
+            timeline.stop();
+        }
+        timeline = new Timeline();
+        timeline.setCycleCount(Timeline.INDEFINITE);
+        final KeyFrame kf = new KeyFrame(Duration.millis(INITIAL_DURATION_MS), spinningKeyFrameEventHandler);
+        timeline.getKeyFrames().setAll(kf);
+        timeline.playFromStart();
+        timeline.play();
+        spinningKeyFrameEventHandler.handle(null);
+    }
+
+    public void stopSpinning() {
+        if (timeline != null) {
+            timeline.stop();
+        }
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/controls/src/main/java/com/sun/javafx/scene/control/skin/SpinnerSkin.java	Sat Jul 12 16:03:15 2014 +1200
@@ -0,0 +1,293 @@
+/*
+ * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.sun.javafx.scene.control.skin;
+
+import com.sun.javafx.scene.control.behavior.SpinnerBehavior;
+import javafx.collections.ListChangeListener;
+import javafx.css.PseudoClass;
+import javafx.geometry.HPos;
+import javafx.geometry.VPos;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.TextField;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+
+import java.util.List;
+
+public class SpinnerSkin<T> extends BehaviorSkinBase<Spinner<T>, SpinnerBehavior<T>> {
+
+    private TextField textField;
+
+    private Region incrementArrow;
+    private StackPane incrementArrowButton;
+
+    private Region decrementArrow;
+    private StackPane decrementArrowButton;
+
+    // rather than create an private enum, lets just use an int, here's the important details:
+    private static final int ARROWS_ON_RIGHT_VERTICAL   = 0;
+    private static final int ARROWS_ON_LEFT_VERTICAL    = 1;
+    private static final int ARROWS_ON_RIGHT_HORIZONTAL = 2;
+    private static final int ARROWS_ON_LEFT_HORIZONTAL  = 3;
+    private static final int SPLIT_ARROWS_VERTICAL      = 4;
+    private static final int SPLIT_ARROWS_HORIZONTAL    = 5;
+
+    private int layoutMode = 0;
+
+    public SpinnerSkin(Spinner<T> spinner) {
+        super(spinner, new SpinnerBehavior<T>(spinner));
+
+        textField = spinner.getEditor();
+        getChildren().add(textField);
+
+        updateStyleClass();
+        spinner.getStyleClass().addListener((ListChangeListener<String>) c -> updateStyleClass());
+
+        // increment / decrement arrows
+        incrementArrow = new Region();
+        incrementArrow.setFocusTraversable(false);
+        incrementArrow.getStyleClass().setAll("increment-arrow");
+        incrementArrow.setMaxWidth(Region.USE_PREF_SIZE);
+        incrementArrow.setMaxHeight(Region.USE_PREF_SIZE);
+        incrementArrow.setMouseTransparent(true);
+
+        incrementArrowButton = new StackPane();
+        incrementArrowButton.setFocusTraversable(false);
+        incrementArrowButton.getStyleClass().setAll("increment-arrow-button");
+        incrementArrowButton.getChildren().add(incrementArrow);
+        incrementArrowButton.setOnMousePressed(e -> {
+            getSkinnable().requestFocus();
+            getBehavior().startSpinning(true);
+        });
+        incrementArrowButton.setOnMouseReleased(e -> getBehavior().stopSpinning());
+
+        decrementArrow = new Region();
+        decrementArrow.setFocusTraversable(false);
+        decrementArrow.getStyleClass().setAll("decrement-arrow");
+        decrementArrow.setMaxWidth(Region.USE_PREF_SIZE);
+        decrementArrow.setMaxHeight(Region.USE_PREF_SIZE);
+        decrementArrow.setMouseTransparent(true);
+
+        decrementArrowButton = new StackPane();
+        decrementArrowButton.setFocusTraversable(false);
+        decrementArrowButton.getStyleClass().setAll("decrement-arrow-button");
+        decrementArrowButton.getChildren().add(decrementArrow);
+        decrementArrowButton.setOnMousePressed(e -> {
+            getSkinnable().requestFocus();
+            getBehavior().startSpinning(false);
+        });
+        decrementArrowButton.setOnMouseReleased(e -> getBehavior().stopSpinning());
+
+        getChildren().addAll(incrementArrowButton, decrementArrowButton);
+
+        // Fixes in the same vein as ComboBoxListViewSkin
+
+        // move fake focus in to the textfield if the spinner is editable
+        spinner.focusedProperty().addListener((ov, t, hasFocus) -> {
+            // Fix for the regression noted in a comment in RT-29885.
+            ((ComboBoxListViewSkin.FakeFocusTextField)textField).setFakeFocus(hasFocus);
+        });
+
+        spinner.addEventFilter(KeyEvent.ANY, ke -> {
+            if (spinner.isEditable()) {
+                // This prevents a stack overflow from our rebroadcasting of the
+                // event to the textfield that occurs in the final else statement
+                // of the conditions below.
+                if (ke.getTarget().equals(textField)) return;
+
+                // Fix for the regression noted in a comment in RT-29885.
+                // This forwards the event down into the TextField when
+                // the key event is actually received by the Spinner.
+                textField.fireEvent(ke.copyFor(textField, textField));
+                ke.consume();
+            }
+        });
+
+        textField.focusedProperty().addListener((ov, t, hasFocus) -> {
+            // Fix for RT-29885
+            spinner.getProperties().put("FOCUSED", hasFocus);
+            // --- end of RT-29885
+
+            // RT-21454 starts here
+            if (! hasFocus) {
+                pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, false);
+            } else {
+                pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, true);
+            }
+            // --- end of RT-21454
+        });
+
+        // end of comboBox-esque fixes
+
+        textField.focusTraversableProperty().bind(spinner.editableProperty());
+    }
+
+    private void updateStyleClass() {
+        final List<String> styleClass = getSkinnable().getStyleClass();
+
+        if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL)) {
+            layoutMode = ARROWS_ON_LEFT_VERTICAL;
+        } else if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL)) {
+            layoutMode = ARROWS_ON_LEFT_HORIZONTAL;
+        } else if (styleClass.contains(Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL)) {
+            layoutMode = ARROWS_ON_RIGHT_HORIZONTAL;
+        } else if (styleClass.contains(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL)) {
+            layoutMode = SPLIT_ARROWS_VERTICAL;
+        } else if (styleClass.contains(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL)) {
+            layoutMode = SPLIT_ARROWS_HORIZONTAL;
+        } else {
+            layoutMode = ARROWS_ON_RIGHT_VERTICAL;
+        }
+    }
+
+    @Override protected void layoutChildren(final double x, final double y,
+                                            final double w, final double h) {
+
+        final double incrementArrowButtonWidth = incrementArrowButton.snappedLeftInset() +
+                snapSize(incrementArrow.prefWidth(-1)) + incrementArrowButton.snappedRightInset();
+
+        final double decrementArrowButtonWidth = decrementArrowButton.snappedLeftInset() +
+                snapSize(decrementArrow.prefWidth(-1)) + decrementArrowButton.snappedRightInset();
+
+        final double widestArrowButton = Math.max(incrementArrowButtonWidth, decrementArrowButtonWidth);
+
+        // we need to decide on our layout approach, and this depends on
+        // the presence of style classes in the Spinner styleClass list.
+        // To be a bit more efficient, we observe the list for changes, so
+        // here in layoutChildren we can just react to a few booleans.
+        if (layoutMode == ARROWS_ON_RIGHT_VERTICAL || layoutMode == ARROWS_ON_LEFT_VERTICAL) {
+            final double textFieldStartX = layoutMode == ARROWS_ON_RIGHT_VERTICAL ? x : x + widestArrowButton;
+            final double buttonStartX = layoutMode == ARROWS_ON_RIGHT_VERTICAL ? x + w - widestArrowButton : x;
+            final double halfHeight = Math.floor(h / 2.0);
+
+            textField.resizeRelocate(textFieldStartX, y, w - widestArrowButton, h);
+
+            incrementArrowButton.resize(widestArrowButton, halfHeight);
+            positionInArea(incrementArrowButton, buttonStartX, y,
+                    widestArrowButton, halfHeight, 0, HPos.CENTER, VPos.CENTER);
+
+            decrementArrowButton.resize(widestArrowButton, halfHeight);
+            positionInArea(decrementArrowButton, buttonStartX, y + halfHeight,
+                    widestArrowButton, h - halfHeight, 0, HPos.CENTER, VPos.BOTTOM);
+        } else if (layoutMode == ARROWS_ON_RIGHT_HORIZONTAL || layoutMode == ARROWS_ON_LEFT_HORIZONTAL) {
+            final double totalButtonWidth = incrementArrowButtonWidth + decrementArrowButtonWidth;
+            final double textFieldStartX = layoutMode == ARROWS_ON_RIGHT_HORIZONTAL ? x : x + totalButtonWidth;
+            final double buttonStartX = layoutMode == ARROWS_ON_RIGHT_HORIZONTAL ? x + w - totalButtonWidth : x;
+
+            textField.resizeRelocate(textFieldStartX, y, w - totalButtonWidth, h);
+
+            // decrement is always on the left
+            decrementArrowButton.resize(decrementArrowButtonWidth, h);
+            positionInArea(decrementArrowButton, buttonStartX, y,
+                    decrementArrowButtonWidth, h, 0, HPos.CENTER, VPos.CENTER);
+
+            // ... and increment is always on the right
+            incrementArrowButton.resize(incrementArrowButtonWidth, h);
+            positionInArea(incrementArrowButton, buttonStartX + decrementArrowButtonWidth, y,
+                    incrementArrowButtonWidth, h, 0, HPos.CENTER, VPos.CENTER);
+        } else if (layoutMode == SPLIT_ARROWS_VERTICAL) {
+            final double incrementArrowButtonHeight = incrementArrowButton.snappedTopInset() +
+                    snapSize(incrementArrow.prefHeight(-1)) + incrementArrowButton.snappedBottomInset();
+
+            final double decrementArrowButtonHeight = decrementArrowButton.snappedTopInset() +
+                    snapSize(decrementArrow.prefHeight(-1)) + decrementArrowButton.snappedBottomInset();
+
+            final double tallestArrowButton = Math.max(incrementArrowButtonHeight, decrementArrowButtonHeight);
+
+            // increment is at the top
+            incrementArrowButton.resize(w, tallestArrowButton);
+            positionInArea(incrementArrowButton, x, y,
+                    w, tallestArrowButton, 0, HPos.CENTER, VPos.CENTER);
+
+            // textfield in the middle
+            textField.resizeRelocate(x, y + tallestArrowButton, w, h - (2*tallestArrowButton));
+
+            // decrement is at the bottom
+            decrementArrowButton.resize(w, tallestArrowButton);
+            positionInArea(decrementArrowButton, x, h - tallestArrowButton,
+                    w, tallestArrowButton, 0, HPos.CENTER, VPos.CENTER);
+        } else if (layoutMode == SPLIT_ARROWS_HORIZONTAL) {
+            // decrement is on the left-hand side
+            decrementArrowButton.resize(widestArrowButton, h);
+            positionInArea(decrementArrowButton, x, y,
+                    widestArrowButton, h, 0, HPos.CENTER, VPos.CENTER);
+
+            // textfield in the middle
+            textField.resizeRelocate(x + widestArrowButton, y, w - (2*widestArrowButton), h);
+
+            // increment is on the right-hand side
+            incrementArrowButton.resize(widestArrowButton, h);
+            positionInArea(incrementArrowButton, w - widestArrowButton, y,
+                    widestArrowButton, h, 0, HPos.CENTER, VPos.CENTER);
+        }
+    }
+
+    @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+    }
+
+    @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        final double textfieldWidth = textField.prefWidth(height);
+        return leftInset + textfieldWidth + rightInset;
+    }
+
+    @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        double ph;
+        double textFieldHeight = textField.prefHeight(width);
+
+        if (layoutMode == SPLIT_ARROWS_VERTICAL) {
+            ph = topInset + incrementArrowButton.prefHeight(width) +
+                    textFieldHeight + decrementArrowButton.prefHeight(width) + bottomInset;
+        } else {
+            ph = topInset + textFieldHeight + bottomInset;
+        }
+
+        return ph;
+    }
+
+    @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return getSkinnable().prefWidth(height);
+    }
+
+    @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return getSkinnable().prefHeight(width);
+    }
+
+    // Overridden so that we use the textfield as the baseline, rather than the arrow.
+    // See RT-30754 for more information.
+    @Override protected double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
+        return textField.getLayoutBounds().getMinY() + textField.getLayoutY() + textField.getBaselineOffset();
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Stylesheet Handling                                                     *
+     *                                                                         *
+     **************************************************************************/
+
+    private static PseudoClass CONTAINS_FOCUS_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("contains-focus");
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/controls/src/main/java/javafx/scene/control/Spinner.java	Sat Jul 12 16:03:15 2014 +1200
@@ -0,0 +1,579 @@
+/*
+ * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package javafx.scene.control;
+
+import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
+import com.sun.javafx.scene.control.skin.SpinnerSkin;
+import javafx.beans.NamedArg;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.MapChangeListener;
+import javafx.collections.ObservableList;
+import javafx.util.StringConverter;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.temporal.TemporalUnit;
+
+/**
+ * A single line text field that lets the user select a number or an object
+ * value from an ordered sequence. Spinners typically provide a pair of tiny
+ * arrow buttons for stepping through the elements of the sequence. The keyboard
+ * up/down arrow keys also cycle through the elements. The user may also be
+ * allowed to type a (legal) value directly into the spinner. Although combo
+ * boxes provide similar functionality, spinners are sometimes preferred because
+ * they don't require a drop down list that can obscure important data, and also
+ * because they allow for features such as
+ * {@link SpinnerValueFactory#wrapAroundProperty() wrapping}
+ * and simpler specification of 'infinite' data models (the
+ * {@link SpinnerValueFactory SpinnerValueFactory}, rather than using a
+ * {@link javafx.collections.ObservableList ObservableList} data model like many
+ * other JavaFX UI controls.
+ *
+ * <p>A Spinner's sequence value is defined by its
+ * {@link SpinnerValueFactory SpinnerValueFactory}. The value factory
+ * can be specified as a constructor argument and changed with the
+ * {@link #valueFactoryProperty() value factory property}. SpinnerValueFactory
+ * classes for some common types are provided with JavaFX, including:
+ *
+ * <br/>
+ *
+ * <ul>
+ *     <li>{@link SpinnerValueFactory.IntegerSpinnerValueFactory}</li>
+ *     <li>{@link SpinnerValueFactory.DoubleSpinnerValueFactory}</li>
+ *     <li>{@link SpinnerValueFactory.ListSpinnerValueFactory}</li>
+ *     <li>{@link SpinnerValueFactory.LocalDateSpinnerValueFactory}</li>
+ * </ul>
+ *
+ * <br/>
+ *
+ * <p>A Spinner has a TextField child component that is responsible for displaying
+ * and potentially changing the current {@link #valueProperty() value} of the
+ * Spinner, which is called the {@link #editorProperty() editor}. By default the
+ * Spinner is non-editable, but input can be accepted if the
+ * {@link #editableProperty() editable property} is set to true. The Spinner
+ * editor stays in sync with the value factory by listening for changes to the
+ * {@link SpinnerValueFactory#valueProperty() value property} of the value factory.
+ * If the user has changed the value displayed in the editor it is possible for
+ * the Spinner {@link #valueProperty() value} to differ from that of the editor.
+ * To make sure the model has the same value as the editor, the user must commit
+ * the edit using the Enter key.
+ *
+ * @see SpinnerValueFactory
+ * @param <T> The type of all values that can be iterated through in the Spinner.
+ *            Common types include Integer and String.
+ * @since JavaFX 8u40
+ */
+public class Spinner<T> extends Control {
+
+    // default style class, puts arrows on right, stacked vertically
+    private static final String DEFAULT_STYLE_CLASS = "spinner";
+
+    /** The arrows are placed on the right of the Spinner, pointing horizontally (i.e. left and right). */
+    public static final String STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL = "arrows-on-right-horizontal";
+
+    /** The arrows are placed on the left of the Spinner, pointing vertically (i.e. up and down). */
+    public static final String STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL = "arrows-on-left-vertical";
+
+    /** The arrows are placed on the left of the Spinner, pointing horizontally (i.e. left and right). */
+    public static final String STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL = "arrows-on-left-horizontal";
+
+    /** The arrows are placed above and beneath the spinner, stretching to take the entire width. */
+    public static final String STYLE_CLASS_SPLIT_ARROWS_VERTICAL = "split-arrows-vertical";
+
+    /** The decrement arrow is placed on the left of the Spinner, and the increment on the right. */
+    public static final String STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL = "split-arrows-horizontal";
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Constructs a default Spinner instance, with the default 'spinner' style
+     * class and a non-editable editor.
+     */
+    public Spinner() {
+        getStyleClass().add(DEFAULT_STYLE_CLASS);
+
+        getEditor().setOnAction(action -> {
+            String text = getEditor().getText();
+            SpinnerValueFactory<T> valueFactory = getValueFactory();
+            if (valueFactory != null) {
+                StringConverter<T> converter = valueFactory.getConverter();
+                if (converter != null) {
+                    T value = converter.fromString(text);
+                    valueFactory.setValue(value);
+                }
+            }
+        });
+
+        getEditor().editableProperty().bind(editableProperty());
+
+        value.addListener((o, oldValue, newValue) -> setText(newValue));
+
+        // Fix for RT-29885
+        getProperties().addListener((MapChangeListener<Object, Object>) change -> {
+            if (change.wasAdded()) {
+                if (change.getKey() == "FOCUSED") {
+                    setFocused((Boolean)change.getValueAdded());
+                    getProperties().remove("FOCUSED");
+                }
+            }
+        });
+        // End of fix for RT-29885
+    }
+
+    /**
+     * Creates a Spinner instance with the
+     * {@link #valueFactoryProperty() value factory} set to be an instance
+     * of {@link SpinnerValueFactory.IntegerSpinnerValueFactory}. Note that
+     * if this constructor is called, the only valid generic type for the
+     * Spinner instance is Integer, i.e. Spinner&lt;Integer&gt;.
+     *
+     * @param min The minimum allowed integer value for the Spinner.
+     * @param max The maximum allowed integer value for the Spinner.
+     * @param initialValue The value of the Spinner when first instantiated, must
+     *                     be within the bounds of the min and max arguments, or
+     *                     else the min value will be used.
+     */
+    public Spinner(@NamedArg("min") int min,
+                   @NamedArg("max") int max,
+                   @NamedArg("initialValue") int initialValue) {
+        // This only works if the Spinner is of type Integer
+        this((SpinnerValueFactory<T>)new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, initialValue));
+    }
+
+    /**
+     * Creates a Spinner instance with the
+     * {@link #valueFactoryProperty() value factory} set to be an instance
+     * of {@link SpinnerValueFactory.IntegerSpinnerValueFactory}. Note that
+     * if this constructor is called, the only valid generic type for the
+     * Spinner instance is Integer, i.e. Spinner&lt;Integer&gt;.
+     *
+     * @param min The minimum allowed integer value for the Spinner.
+     * @param max The maximum allowed integer value for the Spinner.
+     * @param initialValue The value of the Spinner when first instantiated, must
+     *                     be within the bounds of the min and max arguments, or
+     *                     else the min value will be used.
+     * @param amountToStepBy The amount to increment or decrement by, per step.
+     */
+    public Spinner(@NamedArg("min") int min,
+                   @NamedArg("max") int max,
+                   @NamedArg("initialValue") int initialValue,
+                   @NamedArg("amountToStepBy") int amountToStepBy) {
+        // This only works if the Spinner is of type Integer
+        this((SpinnerValueFactory<T>)new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, initialValue, amountToStepBy));
+    }
+
+    /**
+     * Creates a Spinner instance with the
+     * {@link #valueFactoryProperty() value factory} set to be an instance
+     * of {@link SpinnerValueFactory.DoubleSpinnerValueFactory}. Note that
+     * if this constructor is called, the only valid generic type for the
+     * Spinner instance is Double, i.e. Spinner&lt;Double&gt;.
+     *
+     * @param min The minimum allowed double value for the Spinner.
+     * @param max The maximum allowed double value for the Spinner.
+     * @param initialValue The value of the Spinner when first instantiated, must
+     *                     be within the bounds of the min and max arguments, or
+     *                     else the min value will be used.
+     */
+    public Spinner(@NamedArg("min") double min,
+                   @NamedArg("max") double max,
+                   @NamedArg("initialValue") double initialValue) {
+        // This only works if the Spinner is of type Double
+        this((SpinnerValueFactory<T>)new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initialValue));
+    }
+
+    /**
+     * Creates a Spinner instance with the
+     * {@link #valueFactoryProperty() value factory} set to be an instance
+     * of {@link SpinnerValueFactory.DoubleSpinnerValueFactory}. Note that
+     * if this constructor is called, the only valid generic type for the
+     * Spinner instance is Double, i.e. Spinner&lt;Double&gt;.
+     *
+     * @param min The minimum allowed double value for the Spinner.
+     * @param max The maximum allowed double value for the Spinner.
+     * @param initialValue The value of the Spinner when first instantiated, must
+     *                     be within the bounds of the min and max arguments, or
+     *                     else the min value will be used.
+     * @param amountToStepBy The amount to increment or decrement by, per step.
+     */
+    public Spinner(@NamedArg("min") double min,
+                   @NamedArg("max") double max,
+                   @NamedArg("initialValue") double initialValue,
+                   @NamedArg("amountToStepBy") double amountToStepBy) {
+        // This only works if the Spinner is of type Double
+        this((SpinnerValueFactory<T>)new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initialValue, amountToStepBy));
+    }
+
+    /**
+     * Creates a Spinner instance with the
+     * {@link #valueFactoryProperty() value factory} set to be an instance
+     * of {@link SpinnerValueFactory.LocalDateSpinnerValueFactory}. Note that
+     * if this constructor is called, the only valid generic type for the
+     * Spinner instance is LocalDate, i.e. Spinner&lt;LocalDate&gt;.
+     *
+     * @param min The minimum allowed LocalDate value for the Spinner.
+     * @param max The maximum allowed LocalDate value for the Spinner.
+     * @param initialValue The value of the Spinner when first instantiated, must
+     *                     be within the bounds of the min and max arguments, or
+     *                     else the min value will be used.
+     */
+    public Spinner(@NamedArg("min") LocalDate min,
+                   @NamedArg("max") LocalDate max,
+                   @NamedArg("initialValue") LocalDate initialValue) {
+        // This only works if the Spinner is of type LocalDate
+        this((SpinnerValueFactory<T>)new SpinnerValueFactory.LocalDateSpinnerValueFactory(min, max, initialValue));
+    }
+
+    /**
+     * Creates a Spinner instance with the
+     * {@link #valueFactoryProperty() value factory} set to be an instance
+     * of {@link SpinnerValueFactory.LocalDateSpinnerValueFactory}. Note that
+     * if this constructor is called, the only valid generic type for the
+     * Spinner instance is LocalDate, i.e. Spinner&lt;LocalDate&gt;.
+     *
+     * @param min The minimum allowed LocalDate value for the Spinner.
+     * @param max The maximum allowed LocalDate value for the Spinner.
+     * @param initialValue The value of the Spinner when first instantiated, must
+     *                     be within the bounds of the min and max arguments, or
+     *                     else the min value will be used.
+     * @param amountToStepBy The amount to increment or decrement by, per step.
+     * @param temporalUnit The size of each step (e.g. day, week, month, year, etc).
+     */
+    public Spinner(@NamedArg("min") LocalDate min,
+                   @NamedArg("max") LocalDate max,
+                   @NamedArg("initialValue") LocalDate initialValue,
+                   @NamedArg("amountToStepBy") long amountToStepBy,
+                   @NamedArg("temporalUnit") TemporalUnit temporalUnit) {
+        // This only works if the Spinner is of type LocalDate
+        this((SpinnerValueFactory<T>)new SpinnerValueFactory.LocalDateSpinnerValueFactory(min, max, initialValue, amountToStepBy, temporalUnit));
+    }
+
+    /**
+     * Creates a Spinner instance with the
+     * {@link #valueFactoryProperty() value factory} set to be an instance
+     * of {@link SpinnerValueFactory.ListSpinnerValueFactory}. The
+     * Spinner {@link #valueProperty() value property} will be set to the first
+     * element of the list, if an element exists, or null otherwise.
+     *
+     * @param items A list of items that will be stepped through in the Spinner.
+     */
+    public Spinner(@NamedArg("items") ObservableList<T> items) {
+        this(new SpinnerValueFactory.ListSpinnerValueFactory<T>(items));
+    }
+
+    /**
+     * Creates a Spinner instance with the given value factory set.
+     *
+     * @param valueFactory The {@link #valueFactoryProperty() value factory} to use.
+     */
+    public Spinner(@NamedArg("valueFactory") SpinnerValueFactory<T> valueFactory) {
+        this();
+
+        setValueFactory(valueFactory);
+    }
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Public API                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Attempts to increment the {@link #valueFactoryProperty() value factory}
+     * by one step, by calling the {@link SpinnerValueFactory#increment(int)}
+     * method with an argument of one. If the value factory is null, an
+     * IllegalStateException is thrown.
+     *
+     * @throws IllegalStateException if the value factory returned by
+     *      calling {@link #getValueFactory()} is null.
+     */
+    public void increment() {
+        increment(1);
+    }
+
+    /**
+     * Attempts to increment the {@link #valueFactoryProperty() value factory}
+     * by the given number of steps, by calling the
+     * {@link SpinnerValueFactory#increment(int)}
+     * method and forwarding the steps argument to it. If the value factory is
+     * null, an IllegalStateException is thrown.
+     *
+     * @param steps The number of increments that should be performed on the value.
+     * @throws IllegalStateException if the value factory returned by
+     *      calling {@link #getValueFactory()} is null.
+     */
+    public void increment(int steps) {
+        SpinnerValueFactory<T> valueFactory = getValueFactory();
+        if (valueFactory == null) {
+            throw new IllegalStateException("Can't increment Spinner with a null SpinnerValueFactory");
+        }
+        valueFactory.increment(steps);
+    }
+
+    /**
+     * Attempts to decrement the {@link #valueFactoryProperty() value factory}
+     * by one step, by calling the {@link SpinnerValueFactory#decrement(int)}
+     * method with an argument of one. If the value factory is null, an
+     * IllegalStateException is thrown.
+     *
+     * @throws IllegalStateException if the value factory returned by
+     *      calling {@link #getValueFactory()} is null.
+     */
+    public void decrement() {
+        decrement(1);
+    }
+
+    /**
+     * Attempts to decrement the {@link #valueFactoryProperty() value factory}
+     * by the given number of steps, by calling the
+     * {@link SpinnerValueFactory#decrement(int)}
+     * method and forwarding the steps argument to it. If the value factory is
+     * null, an IllegalStateException is thrown.
+     *
+     * @param steps The number of decrements that should be performed on the value.
+     * @throws IllegalStateException if the value factory returned by
+     *      calling {@link #getValueFactory()} is null.
+     */
+    public void decrement(int steps) {
+        SpinnerValueFactory<T> valueFactory = getValueFactory();
+        if (valueFactory == null) {
+            throw new IllegalStateException("Can't decrement Spinner with a null SpinnerValueFactory");
+        }
+        valueFactory.decrement(steps);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new SpinnerSkin<>(this);
+    }
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Properties                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    // --- value (a read only, bound property to the value factory value property)
+    /**
+     * The value property on Spinner is a read-only property, as it is bound to
+     * the SpinnerValueFactory
+     * {@link SpinnerValueFactory#valueProperty() value property}. Should the
+     * {@link #valueFactoryProperty() value factory} change, this value property
+     * will be unbound from the old value factory and bound to the new one.
+     *
+     * <p>If developers wish to modify the value property, they may do so with
+     * code in the following form:
+     *
+     * <pre>
+     * {@code
+     * Object newValue = ...;
+     * spinner.getValueFactory().setValue(newValue);
+     * }</pre>
+     */
+    private ReadOnlyObjectWrapper<T> value = new ReadOnlyObjectWrapper<T>(this, "value");
+    public final T getValue() {
+        return value.get();
+    }
+    public final ReadOnlyObjectProperty<T> valueProperty() {
+        return value;
+    }
+
+
+    // --- valueFactory
+    /**
+     * The value factory is the model behind the JavaFX Spinner control - without
+     * a value factory installed a Spinner is unusable. It is the role of the
+     * value factory to handle almost all aspects of the Spinner, including:
+     *
+     * <ul>
+     *     <li>Representing the current state of the {@link SpinnerValueFactory#valueProperty() value},</li>
+     *     <li>{@link SpinnerValueFactory#increment(int) Incrementing}
+     *         and {@link SpinnerValueFactory#decrement(int) decrementing} the
+     *         value, with one or more steps per call,</li>
+     *     <li>{@link SpinnerValueFactory#converterProperty() Converting} text input
+     *         from the user (via the Spinner {@link #editorProperty() editor},</li>
+     *     <li>Converting {@link SpinnerValueFactory#converterProperty() objects to user-readable strings}
+     *         for display on screen</li>
+     * </ul>
+     */
+    private ObjectProperty<SpinnerValueFactory<T>> valueFactory =
+            new SimpleObjectProperty<SpinnerValueFactory<T>>(this, "valueFactory") {
+                @Override protected void invalidated() {
+                    value.unbind();
+
+                    SpinnerValueFactory<T> newFactory = get();
+                    if (newFactory != null) {
+                        // this binding is what ensures the Spinner.valueProperty()
+                        // properly represents the value in the value factory
+                        value.bind(newFactory.valueProperty());
+                    }
+                }
+            };
+    public final void setValueFactory(SpinnerValueFactory<T> value) {
+        valueFactory.setValue(value);
+    }
+    public final SpinnerValueFactory<T> getValueFactory() {
+        return valueFactory.get();
+    }
+    public final ObjectProperty<SpinnerValueFactory<T>> valueFactoryProperty() {
+        return valueFactory;
+    }
+
+
+    // --- editable
+    /**
+     * The editable property is used to specify whether user input is able to
+     * be typed into the Spinner {@link #editorProperty() editor}. If editable
+     * is true, user input will be received once the user types and presses
+     * the Enter key. At this point the input is passed to the
+     * SpinnerValueFactory {@link SpinnerValueFactory#converterProperty() converter}
+     * {@link javafx.util.StringConverter#fromString(String)} method.
+     * The returned value from this call (of type T) is then sent to the
+     * {@link SpinnerValueFactory#setValue(Object)} method. If the value
+     * is valid, it will remain as the value. If it is invalid, the value factory
+     * will need to react accordingly and back out this change.
+     */
+    private BooleanProperty editable;
+    public final void setEditable(boolean value) {
+        editableProperty().set(value);
+    }
+    public final boolean isEditable() {
+        return editable == null ? true : editable.get();
+    }
+    public final BooleanProperty editableProperty() {
+        if (editable == null) {
+            editable = new SimpleBooleanProperty(this, "editable", false);
+        }
+        return editable;
+    }
+
+
+    // --- editor
+    /**
+     * The editor used by the Spinner control.
+     */
+    public final ReadOnlyObjectProperty<TextField> editorProperty() {
+        if (editor == null) {
+            editor = new ReadOnlyObjectWrapper<TextField>(this, "editor");
+            textField = new ComboBoxListViewSkin.FakeFocusTextField();
+            editor.set(textField);
+        }
+        return editor.getReadOnlyProperty();
+    }
+    private TextField textField;
+    private ReadOnlyObjectWrapper<TextField> editor;
+    public final TextField getEditor() {
+        return editorProperty().get();
+    }
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Implementation                                                          *
+     *                                                                         *
+     **************************************************************************/
+
+    /*
+     * Update the TextField based on the current value
+     */
+    private void setText(T value) {
+        String text = null;
+
+        SpinnerValueFactory<T> valueFactory = getValueFactory();
+        if (valueFactory != null) {
+            StringConverter<T> converter = valueFactory.getConverter();
+            if (converter != null) {
+                text = converter.toString(value);
+            }
+        }
+
+        if (text == null) {
+            if (value == null) {
+                getEditor().clear();
+                return;
+            } else {
+                text = value.toString();
+            }
+        }
+
+        getEditor().setText(text);
+    }
+
+    /*
+     * Convenience method to support wrapping values around their min / max
+     * constraints. Used by the SpinnerValueFactory implementations when
+     * the Spinner wrapAround property is true.
+     */
+    static int wrapValue(int value, int min, int max) {
+        if (max == 0) {
+            throw new RuntimeException();
+        }
+
+        int r = value % max;
+        if (r > min && max < min) {
+            r = r + max - min;
+        } else if (r < min && max > min) {
+            r = r + max - min;
+        }
+        return r;
+    }
+
+    /*
+     * Convenience method to support wrapping values around their min / max
+     * constraints. Used by the SpinnerValueFactory implementations when
+     * the Spinner wrapAround property is true.
+     */
+    static BigDecimal wrapValue(BigDecimal value, BigDecimal min, BigDecimal max) {
+        if (max.doubleValue() == 0) {
+            throw new RuntimeException();
+        }
+
+        // note that this wrap method differs from the others where we take the
+        // difference - in this approach we wrap to the min or max - it feels better
+        // to go from 1 to 0, rather than 1 to 0.05 (where max is 1 and step is 0.05).
+        if (value.compareTo(min) < 0) {
+            return max;
+        } else if (value.compareTo(max) > 0) {
+            return min;
+        }
+        return value;
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/controls/src/main/java/javafx/scene/control/SpinnerValueFactory.java	Sat Jul 12 16:03:15 2014 +1200
@@ -0,0 +1,1095 @@
+/*
+ * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package javafx.scene.control;
+
+import com.sun.javafx.scene.control.skin.ListViewSkin;
+import javafx.beans.NamedArg;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.LongProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleLongProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.WeakChangeListener;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.collections.WeakListChangeListener;
+import javafx.util.StringConverter;
+import javafx.util.converter.IntegerStringConverter;
+
+import java.lang.ref.WeakReference;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.Temporal;
+import java.time.temporal.TemporalUnit;
+import java.util.List;
+
+/**
+ * The SpinnerValueFactory is the model behind the JavaFX
+ * {@link Spinner Spinner control} - without a value factory installed a
+ * Spinner is unusable. It is the role of the value factory to handle almost all
+ * aspects of the Spinner, including:
+ *
+ * <ul>
+ *     <li>Representing the current state of the {@link javafx.scene.control.SpinnerValueFactory#valueProperty() value},</li>
+ *     <li>{@link SpinnerValueFactory#increment(int) Incrementing}
+ *         and {@link SpinnerValueFactory#decrement(int) decrementing} the
+ *         value, with one or more steps per call,</li>
+ *     <li>{@link javafx.scene.control.SpinnerValueFactory#converterProperty() Converting} text input
+ *         from the user (via the Spinner {@link Spinner#editorProperty() editor},</li>
+ *     <li>Converting {@link javafx.scene.control.SpinnerValueFactory#converterProperty() objects to user-readable strings}
+ *         for display on screen</li>
+ * </ul>
+ *
+ * <p>SpinnerValueFactory classes for some common types are provided with JavaFX, including:
+ *
+ * <br/>
+ *
+ * <ul>
+ *     <li>{@link SpinnerValueFactory.IntegerSpinnerValueFactory}</li>
+ *     <li>{@link SpinnerValueFactory.DoubleSpinnerValueFactory}</li>
+ *     <li>{@link SpinnerValueFactory.ListSpinnerValueFactory}</li>
+ *     <li>{@link SpinnerValueFactory.LocalDateSpinnerValueFactory}</li>
+ * </ul>
+ *
+ * @param <T> The type of the data this value factory deals with, which must
+ *            coincide with the type of the Spinner that the value factory is set on.
+ * @see Spinner
+ * @see SpinnerValueFactory.IntegerSpinnerValueFactory
+ * @see SpinnerValueFactory.DoubleSpinnerValueFactory
+ * @see SpinnerValueFactory.ListSpinnerValueFactory
+ * @see SpinnerValueFactory.LocalDateSpinnerValueFactory
+ */
+public abstract class SpinnerValueFactory<T> {
+
+    /***************************************************************************
+     *                                                                         *
+     * Private fields                                                          *
+     *                                                                         *
+     **************************************************************************/
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Abstract methods                                                        *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Attempts to decrement the {@link #valueProperty() value} by the given
+     * number of steps.
+     *
+     * @param steps The number of decrements that should be performed on the value.
+     */
+    public abstract void decrement(int steps);
+
+
+    /**
+     * Attempts to omcrement the {@link #valueProperty() value} by the given
+     * number of steps.
+     *
+     * @param steps The number of increments that should be performed on the value.
+     */
+    public abstract void increment(int steps);
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Properties                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    // --- value
+    /**
+     * Represents the current value of the SpinnerValueFactory, or null if no
+     * value has been set.
+     */
+    private ObjectProperty<T> value = new SimpleObjectProperty<>(this, "value");
+    public final T getValue() {
+        return value.get();
+    }
+    public final void setValue(T newValue) {
+        value.set(newValue);
+    }
+    public final ObjectProperty<T> valueProperty() {
+        return value;
+    }
+
+
+    // --- converter
+    /**
+     * Converts the user-typed input (when the Spinner is
+     * {@link Spinner#editableProperty() editable}) to an object of type T,
+     * such that the input may be retrieved via the  {@link #valueProperty() value}
+     * property.
+     */
+    private ObjectProperty<StringConverter<T>> converter = new SimpleObjectProperty<>(this, "converter");
+    public final StringConverter<T> getConverter() {
+        return converter.get();
+    }
+    public final void setConverter(StringConverter<T> newValue) {
+        converter.set(newValue);
+    }
+    public final ObjectProperty<StringConverter<T>> converterProperty() {
+        return converter;
+    }
+
+
+    // --- wrapAround
+    /**
+     * The wrapAround property is used to specify whether the value factory should
+     * be circular. For example, should an integer-based value model increment
+     * from the maximum value back to the minimum value (and vice versa).
+     */
+    private BooleanProperty wrapAround;
+    public final void setWrapAround(boolean value) {
+        wrapAroundProperty().set(value);
+    }
+    public final boolean isWrapAround() {
+        return wrapAround == null ? false : wrapAround.get();
+    }
+    public final BooleanProperty wrapAroundProperty() {
+        if (wrapAround == null) {
+            wrapAround = new SimpleBooleanProperty(this, "wrapAround", false);
+        }
+        return wrapAround;
+    }
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Subclasses of SpinnerValueFactory                                       *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * A {@link javafx.scene.control.SpinnerValueFactory} implementation designed to iterate through
+     * a list of values.
+     *
+     * <p>Note that the default {@link #converterProperty() converter} is implemented
+     * simply as shown below, which may be adequate in many cases, but it is important
+     * for users to ensure that this suits their needs (and adjust when necessary):
+     *
+     * <pre>
+     * setConverter(new StringConverter&lt;T&gt;() {
+     *     &#064;Override public String toString(T value) {
+     *         if (value == null) {
+     *             return "";
+     *         }
+     *         return value.toString();
+     *     }
+     *
+     *     &#064;Override public T fromString(String string) {
+     *         return (T) string;
+     *     }
+     * });</pre>
+     *
+     * @param <T> The type of the elements in the {@link java.util.List}.
+     */
+    public static class ListSpinnerValueFactory<T> extends SpinnerValueFactory<T> {
+
+        /***********************************************************************
+         *                                                                     *
+         * Private fields                                                      *
+         *                                                                     *
+         **********************************************************************/
+
+        private int currentIndex = 0;
+
+        private final ListChangeListener<T> itemsContentObserver = c -> {
+            // the items content has changed. We do not try to find the current
+            // item, instead we remain at the currentIndex, if possible, or else
+            // we go back to index 0, and if that fails, we go to null
+            updateCurrentIndex();
+        };
+
+        private WeakListChangeListener<T> weakItemsContentObserver =
+                new WeakListChangeListener<T>(itemsContentObserver);
+
+
+
+        /***********************************************************************
+         *                                                                     *
+         * Constructors                                                        *
+         *                                                                     *
+         **********************************************************************/
+
+        /**
+         * Creates a new instance of the ListSpinnerValueFactory with the given
+         * list used as the list to step through.
+         *
+         * @param items The list of items to step through with the Spinner.
+         */
+        public ListSpinnerValueFactory(@NamedArg("items") ObservableList<T> items) {
+            setItems(items);
+            setConverter(new StringConverter<T>() {
+                @Override public String toString(T value) {
+                    if (value == null) {
+                        return "";
+                    }
+                    return value.toString();
+                }
+
+                @Override public T fromString(String string) {
+                    return (T) string;
+                }
+            });
+
+            valueProperty().addListener((o, oldValue, newValue) -> {
+                // when the value is set, we need to react to ensure it is a
+                // valid value (and if not, blow up appropriately)
+                int newIndex = -1;
+                if (items.contains(newValue)) {
+                    newIndex = items.indexOf(newValue);
+                } else {
+                    // add newValue to list
+                    items.add(newValue);
+                    newIndex = items.indexOf(newValue);
+                }
+                currentIndex = newIndex;
+            });
+            setValue(_getValue(currentIndex));
+        }
+
+
+
+        /***********************************************************************
+         *                                                                     *
+         * Properties                                                          *
+         *                                                                     *
+         **********************************************************************/
+        // --- Items
+        private ObjectProperty<ObservableList<T>> items;
+
+        /**
+         * Sets the underlying data model for the ListSpinnerValueFactory. Note that it has a generic
+         * type that must match the type of the Spinner itself.
+         */
+        public final void setItems(ObservableList<T> value) {
+            itemsProperty().set(value);
+        }
+
+        /**
+         * Returns an {@link javafx.collections.ObservableList} that contains the items currently able
+         * to be iterated through by the user. This may be null if
+         * {@link #setItems(javafx.collections.ObservableList)} has previously been
+         * called, however, by default it is an empty ObservableList.
+         *
+         * @return An ObservableList containing the items to be shown to the user, or
+         *      null if the items have previously been set to null.
+         */
+        public final ObservableList<T> getItems() {
+            return items == null ? null : items.get();
+        }
+
+        /**
+         * The underlying data model for the ListView. Note that it has a generic
+         * type that must match the type of the ListView itself.
+         */
+        public final ObjectProperty<ObservableList<T>> itemsProperty() {
+            if (items == null) {
+                items = new SimpleObjectProperty<ObservableList<T>>(this, "items") {
+                    WeakReference<ObservableList<T>> oldItemsRef;
+
+                    @Override protected void invalidated() {
+                        ObservableList<T> oldItems = oldItemsRef == null ? null : oldItemsRef.get();
+                        ObservableList<T> newItems = getItems();
+
+                        // update listeners
+                        if (oldItems != null) {
+                            oldItems.removeListener(weakItemsContentObserver);
+                        }
+                        if (newItems != null) {
+                            newItems.addListener(weakItemsContentObserver);
+                        }
+
+                        // update the current value based on the index
+                        updateCurrentIndex();
+
+                        oldItemsRef = new WeakReference<>(getItems());
+                    }
+                };
+            }
+            return items;
+        }
+
+
+
+        /***********************************************************************
+         *                                                                     *
+         * Overridden methods                                                  *
+         *                                                                     *
+         **********************************************************************/
+
+        /** {@inheritDoc} */
+        @Override public void decrement(int steps) {
+            final int max = getItemsSize() - 1;
+            int newIndex = currentIndex - steps;
+            currentIndex = newIndex >= 0 ? newIndex : (isWrapAround() ? Spinner.wrapValue(newIndex, 0, max + 1) : 0);
+            setValue(_getValue(currentIndex));
+        }
+
+        /** {@inheritDoc} */
+        @Override public void increment(int steps) {
+            final int max = getItemsSize() - 1;
+            int newIndex = currentIndex + steps;
+            currentIndex = newIndex <= max ? newIndex : (isWrapAround() ? Spinner.wrapValue(newIndex, 0, max + 1) : max);
+            setValue(_getValue(currentIndex));
+        }
+
+
+
+        /***********************************************************************
+         *                                                                     *
+         * Private implementation                                              *
+         *                                                                     *
+         **********************************************************************/
+        private int getItemsSize() {
+            List<T> items = getItems();
+            return items == null ? 0 : items.size();
+        }
+
+        private void updateCurrentIndex() {
+            int itemsSize = getItemsSize();
+            if (currentIndex < 0 || currentIndex >= itemsSize) {
+                currentIndex = 0;
+            }
+            setValue(_getValue(currentIndex));
+        }
+
+        private T _getValue(int index) {
+            List<T> items = getItems();
+            return items == null ? null : (index >= 0 && index < items.size()) ? items.get(index) : null;
+        }
+    }
+
+
+
+    /**
+     * A {@link javafx.scene.control.SpinnerValueFactory} implementation designed to iterate through
+     * integer values.
+     *
+     * <p>Note that the default {@link #converterProperty() converter} is implemented
+     * as an {@link javafx.util.converter.IntegerStringConverter} instance.
+     */
+    public static class IntegerSpinnerValueFactory extends SpinnerValueFactory<Integer> {
+
+        /***********************************************************************
+         *                                                                     *
+         * Constructors                                                        *
+         *                                                                     *
+         **********************************************************************/
+
+        /**
+         * Constructs a new IntegerSpinnerValueFactory that sets the initial value
+         * to be equal to the min value, and a default {@code amountToStepBy} of one.
+         *
+         * @param min The minimum allowed integer value for the Spinner.
+         * @param max The maximum allowed integer value for the Spinner.
+         */
+        public IntegerSpinnerValueFactory(@NamedArg("min") int min,
+                                          @NamedArg("max") int max) {
+            this(min, max, min);
+        }
+
+        /**
+         * Constructs a new IntegerSpinnerValueFactory with a default
+         * {@code amountToStepBy} of one.
+         *
+         * @param min The minimum allowed integer value for the Spinner.
+         * @param max The maximum allowed integer value for the Spinner.
+         * @param initialValue The value of the Spinner when first instantiated, must
+         *                     be within the bounds of the min and max arguments, or
+         *                     else the min value will be used.
+         */
+        public IntegerSpinnerValueFactory(@NamedArg("min") int min,
+                                          @NamedArg("max") int max,
+                                          @NamedArg("initialValue") int initialValue) {
+            this(min, max, initialValue, 1);
+        }
+
+        /**
+         * Constructs a new IntegerSpinnerValueFactory.
+         *
+         * @param min The minimum allowed integer value for the Spinner.
+         * @param max The maximum allowed integer value for the Spinner.
+         * @param initialValue The value of the Spinner when first instantiated, must
+         *                     be within the bounds of the min and max arguments, or
+         *                     else the min value will be used.
+         * @param amountToStepBy The amount to increment or decrement by, per step.
+         */
+        public IntegerSpinnerValueFactory(@NamedArg("min") int min,
+                                          @NamedArg("max") int max,
+                                          @NamedArg("initialValue") int initialValue,
+                                          @NamedArg("amountToStepBy") int amountToStepBy) {
+            setMin(min);
+            setMax(max);
+            setAmountToStepBy(amountToStepBy);
+            setConverter(new IntegerStringConverter());
+
+            valueProperty().addListener((o, oldValue, newValue) -> {
+                // when the value is set, we need to react to ensure it is a
+                // valid value (and if not, blow up appropriately)
+                if (newValue < getMin()) {
+                    setValue(getMin());
+                } else if (newValue > getMax()) {
+                    setValue(getMax());
+                }
+            });
+            setValue(initialValue >= min && initialValue <= max ? initialValue : min);
+        }
+
+
+        /***********************************************************************
+         *                                                                     *
+         * Properties                                                          *
+         *                                                                     *
+         **********************************************************************/
+
+        // --- min
+        private IntegerProperty min = new SimpleIntegerProperty(this, "min") {
+            @Override protected void invalidated() {
+                Integer currentValue = IntegerSpinnerValueFactory.this.getValue();
+                if (currentValue == null) {
+                    return;
+                }
+
+                int newMin = get();
+                if (newMin > getMax()) {
+                    setMin(getMax());
+                    return;
+                }
+
+                if (currentValue < newMin) {
+                    IntegerSpinnerValueFactory.this.setValue(newMin);
+                }
+            }
+        };
+
+        public final void setMin(int value) {
+            min.set(value);
+        }
+        public final int getMin() {
+            return min.get();
+        }
+        /**
+         * Sets the minimum allowable value for this value factory
+         */
+        public final IntegerProperty minProperty() {
+            return min;
+        }
+
+        // --- max
+        private IntegerProperty max = new SimpleIntegerProperty(this, "max") {
+            @Override protected void invalidated() {
+                Integer currentValue = IntegerSpinnerValueFactory.this.getValue();
+                if (currentValue == null) {
+                    return;
+                }
+
+                int newMax = get();
+                if (newMax < getMin()) {
+                    setMax(getMin());
+                    return;
+                }
+
+                if (currentValue > newMax) {
+                    IntegerSpinnerValueFactory.this.setValue(newMax);
+                }
+            }
+        };
+
+        public final void setMax(int value) {
+            max.set(value);
+        }
+        public final int getMax() {
+            return max.get();
+        }
+        /**
+         * Sets the maximum allowable value for this value factory
+         */
+        public final IntegerProperty maxProperty() {
+            return max;
+        }
+
+        // --- amountToStepBy
+        private IntegerProperty amountToStepBy = new SimpleIntegerProperty(this, "amountToStepBy");
+        public final void setAmountToStepBy(int value) {
+            amountToStepBy.set(value);
+        }
+        public final int getAmountToStepBy() {
+            return amountToStepBy.get();
+        }
+        /**
+         * Sets the amount to increment or decrement by, per step.
+         */
+        public final IntegerProperty amountToStepByProperty() {
+            return amountToStepBy;
+        }
+
+
+
+        /***********************************************************************
+         *                                                                     *
+         * Overridden methods                                                  *
+         *                                                                     *
+         **********************************************************************/
+
+        /** {@inheritDoc} */
+        @Override public void decrement(int steps) {
+            final int min = getMin();
+            final int max = getMax();
+            final int newIndex = getValue() - steps * getAmountToStepBy();
+            setValue(newIndex >= min ? newIndex : (isWrapAround() ? Spinner.wrapValue(newIndex, min, max) + 1 : min));
+        }
+
+        /** {@inheritDoc} */
+        @Override public void increment(int steps) {
+            final int min = getMin();
+            final int max = getMax();
+            final int currentValue = getValue();
+            final int newIndex = currentValue + steps * getAmountToStepBy();
+            setValue(newIndex <= max ? newIndex : (isWrapAround() ? Spinner.wrapValue(newIndex, min, max) - 1 : max));
+        }
+    }
+
+
+
+    /**
+     * A {@link javafx.scene.control.SpinnerValueFactory} implementation designed to iterate through
+     * double values.
+     *
+     * <p>Note that the default {@link #converterProperty() converter} is implemented
+     * simply as shown below, which may be adequate in many cases, but it is important
+     * for users to ensure that this suits their needs (and adjust when necessary). The
+     * main point to note is that this {@link javafx.util.StringConverter} embeds
+     * within it a {@link java.text.DecimalFormat} instance that shows the Double
+     * to two decimal places. This is used for both the toString and fromString
+     * methods:
+     *
+     * <pre>
+     * setConverter(new StringConverter&lt;Double&gt;() {
+     *     private final DecimalFormat df = new DecimalFormat("#.##");
+     *
+     *     &#064;Override public String toString(Double value) {
+     *         // If the specified value is null, return a zero-length String
+     *         if (value == null) {
+     *             return "";
+     *         }
+     *
+     *         return df.format(value);
+     *     }
+     *
+     *     &#064;Override public Double fromString(String value) {
+     *         try {
+     *             // If the specified value is null or zero-length, return null
+     *             if (value == null) {
+     *                 return null;
+     *             }
+     *
+     *             value = value.trim();
+     *
+     *             if (value.length() &lt; 1) {
+     *                 return null;
+     *             }
+     *
+     *             // Perform the requested parsing
+     *             return df.parse(value).doubleValue();
+     *         } catch (ParseException ex) {
+     *             throw new RuntimeException(ex);
+     *         }
+     *     }
+     * });</pre>
+     */
+    public static class DoubleSpinnerValueFactory extends SpinnerValueFactory<Double> {
+
+        /**
+         * Constructs a new DoubleSpinnerValueFactory that sets the initial value
+         * to be equal to the min value, and a default {@code amountToStepBy} of
+         * one.
+         *
+         * @param min The minimum allowed double value for the Spinner.
+         * @param max The maximum allowed double value for the Spinner.
+         */
+        public DoubleSpinnerValueFactory(@NamedArg("min") double min,
+                                         @NamedArg("max") double max) {
+            this(min, max, min);
+        }
+
+        /**
+         * Constructs a new DoubleSpinnerValueFactory with a default
+         * {@code amountToStepBy} of one.
+         *
+         * @param min The minimum allowed double value for the Spinner.
+         * @param max The maximum allowed double value for the Spinner.
+         * @param initialValue The value of the Spinner when first instantiated, must
+         *                     be within the bounds of the min and max arguments, or
+         *                     else the min value will be used.
+         */
+        public DoubleSpinnerValueFactory(@NamedArg("min") double min,
+                                         @NamedArg("max") double max,
+                                         @NamedArg("initialValue") double initialValue) {
+            this(min, max, initialValue, 1);
+        }
+
+        /**
+         * Constructs a new DoubleSpinnerValueFactory.
+         *
+         * @param min The minimum allowed double value for the Spinner.
+         * @param max The maximum allowed double value for the Spinner.
+         * @param initialValue The value of the Spinner when first instantiated, must
+         *                     be within the bounds of the min and max arguments, or
+         *                     else the min value will be used.
+         * @param amountToStepBy The amount to increment or decrement by, per step.
+         */
+        public DoubleSpinnerValueFactory(@NamedArg("min") double min,
+                                         @NamedArg("max") double max,
+                                         @NamedArg("initialValue") double initialValue,
+                                         @NamedArg("amountToStepBy") double amountToStepBy) {
+            setMin(min);
+            setMax(max);
+            setAmountToStepBy(amountToStepBy);
+            setConverter(new StringConverter<Double>() {
+                private final DecimalFormat df = new DecimalFormat("#.##");
+
+                @Override public String toString(Double value) {
+                    // If the specified value is null, return a zero-length String
+                    if (value == null) {
+                        return "";
+                    }
+
+                    return df.format(value);
+                }
+
+                @Override public Double fromString(String value) {
+                    try {
+                        // If the specified value is null or zero-length, return null
+                        if (value == null) {
+                            return null;
+                        }
+
+                        value = value.trim();
+
+                        if (value.length() < 1) {
+                            return null;
+                        }
+
+                        // Perform the requested parsing
+                        return df.parse(value).doubleValue();
+                    } catch (ParseException ex) {
+                        throw new RuntimeException(ex);
+                    }
+                }
+            });
+
+            valueProperty().addListener((o, oldValue, newValue) -> {
+                // when the value is set, we need to react to ensure it is a
+                // valid value (and if not, blow up appropriately)
+                if (newValue < getMin()) {
+                    setValue(getMin());
+                } else if (newValue > getMax()) {
+                    setValue(getMax());
+                }
+            });
+            setValue(initialValue >= min && initialValue <= max ? initialValue : min);
+        }
+
+
+
+        /***********************************************************************
+         *                                                                     *
+         * Properties                                                          *
+         *                                                                     *
+         **********************************************************************/
+
+        // --- min
+        private DoubleProperty min = new SimpleDoubleProperty(this, "min") {
+            @Override protected void invalidated() {
+                Double currentValue = DoubleSpinnerValueFactory.this.getValue();
+                if (currentValue == null) {
+                    return;
+                }
+
+                final double newMin = get();
+                if (newMin > getMax()) {
+                    setMin(getMax());
+                    return;
+                }
+
+                if (currentValue < newMin) {
+                    DoubleSpinnerValueFactory.this.setValue(newMin);
+                }
+            }
+        };
+
+        public final void setMin(double value) {
+            min.set(value);
+        }
+        public final double getMin() {
+            return min.get();
+        }
+        /**
+         * Sets the minimum allowable value for this value factory
+         */
+        public final DoubleProperty minProperty() {
+            return min;
+        }
+
+        // --- max
+        private DoubleProperty max = new SimpleDoubleProperty(this, "max") {
+            @Override protected void invalidated() {
+                Double currentValue = DoubleSpinnerValueFactory.this.getValue();
+                if (currentValue == null) {
+                    return;
+                }
+
+                final double newMax = get();
+                if (newMax < getMin()) {
+                    setMax(getMin());
+                    return;
+                }
+
+                if (currentValue > newMax) {
+                    DoubleSpinnerValueFactory.this.setValue(newMax);
+                }
+            }
+        };
+
+        public final void setMax(double value) {
+            max.set(value);
+        }
+        public final double getMax() {
+            return max.get();
+        }
+        /**
+         * Sets the maximum allowable value for this value factory
+         */
+        public final DoubleProperty maxProperty() {
+            return max;
+        }
+
+        // --- amountToStepBy
+        private DoubleProperty amountToStepBy = new SimpleDoubleProperty(this, "amountToStepBy");
+        public final void setAmountToStepBy(double value) {
+            amountToStepBy.set(value);
+        }
+        public final double getAmountToStepBy() {
+            return amountToStepBy.get();
+        }
+        /**
+         * Sets the amount to increment or decrement by, per step.
+         */
+        public final DoubleProperty amountToStepByProperty() {
+            return amountToStepBy;
+        }
+
+
+
+        /** {@inheritDoc} */
+        @Override public void decrement(int steps) {
+            final BigDecimal currentValue = BigDecimal.valueOf(getValue());
+            final BigDecimal minBigDecimal = BigDecimal.valueOf(getMin());
+            final BigDecimal maxBigDecimal = BigDecimal.valueOf(getMax());
+            final BigDecimal amountToStepByBigDecimal = BigDecimal.valueOf(getAmountToStepBy());
+            BigDecimal newValue = currentValue.subtract(amountToStepByBigDecimal.multiply(BigDecimal.valueOf(steps)));
+            setValue(newValue.compareTo(minBigDecimal) >= 0 ? newValue.doubleValue() :
+                    (isWrapAround() ? Spinner.wrapValue(newValue, minBigDecimal, maxBigDecimal).doubleValue() : getMin()));
+        }
+
+        /** {@inheritDoc} */
+        @Override public void increment(int steps) {
+            final BigDecimal currentValue = BigDecimal.valueOf(getValue());
+            final BigDecimal minBigDecimal = BigDecimal.valueOf(getMin());
+            final BigDecimal maxBigDecimal = BigDecimal.valueOf(getMax());
+            final BigDecimal amountToStepByBigDecimal = BigDecimal.valueOf(getAmountToStepBy());
+            BigDecimal newValue = currentValue.add(amountToStepByBigDecimal.multiply(BigDecimal.valueOf(steps)));
+            setValue(newValue.compareTo(maxBigDecimal) <= 0 ? newValue.doubleValue() :
+                    (isWrapAround() ? Spinner.wrapValue(newValue, minBigDecimal, maxBigDecimal).doubleValue() : getMax()));
+        }
+    }
+
+    /**
+     * A {@link javafx.scene.control.SpinnerValueFactory} implementation designed to iterate through
+     * {@link java.time.LocalDate} values.
+     *
+     * <p>Note that the default {@link #converterProperty() converter} is implemented
+     * simply as shown below, which may be adequate in many cases, but it is important
+     * for users to ensure that this suits their needs (and adjust when necessary):
+     *
+     * <pre>
+     * setConverter(new StringConverter&lt;LocalDate&gt;() {
+     *     &#064;Override public String toString(LocalDate object) {
+     *         if (object == null) {
+     *             return "";
+     *         }
+     *         return object.toString();
+     *     }
+     *
+     *     &#064;Override public LocalDate fromString(String string) {
+     *         return LocalDate.parse(string);
+     *     }
+     * });</pre>
+     */
+    public static class LocalDateSpinnerValueFactory extends SpinnerValueFactory<LocalDate> {
+
+        /**
+         * Creates a new instance of the LocalDateSpinnerValueFactory, using the
+         * value returned by calling {@code LocalDate#now()} as the initial value,
+         * and using a stepping amount of one day.
+         */
+        public LocalDateSpinnerValueFactory() {
+            this(LocalDate.now());
+        }
+
+        /**
+         * Creates a new instance of the LocalDateSpinnerValueFactory, using the
+         * provided initial value, and a stepping amount of one day.
+         *
+         * @param initialValue The value of the Spinner when first instantiated.
+         */
+        public LocalDateSpinnerValueFactory(@NamedArg("initialValue") LocalDate initialValue) {
+            this(LocalDate.MIN, LocalDate.MAX, initialValue);
+        }
+
+        /**
+         * Creates a new instance of the LocalDateSpinnerValueFactory, using the
+         * provided initial value, and a stepping amount of one day.
+         *
+         * @param min The minimum allowed double value for the Spinner.
+         * @param max The maximum allowed double value for the Spinner.
+         * @param initialValue The value of the Spinner when first instantiated.
+         */
+        public LocalDateSpinnerValueFactory(@NamedArg("min") LocalDate min,
+                                            @NamedArg("min") LocalDate max,
+                                            @NamedArg("initialValue") LocalDate initialValue) {
+            this(min, max, initialValue, 1, ChronoUnit.DAYS);
+        }
+
+        /**
+         * Creates a new instance of the LocalDateSpinnerValueFactory, using the
+         * provided min, max, and initial values, as well as the amount to step
+         * by and {@link java.time.temporal.TemporalUnit}.
+         *
+         * <p>To better understand, here are a few examples:
+         *
+         * <ul>
+         *     <li><strong>To step by one day from today: </strong> {@code new LocalDateSpinnerValueFactory(LocalDate.MIN, LocalDate.MAX, LocalDate.now(), 1, ChronoUnit.DAYS)}</li>
+         *     <li><strong>To step by one month from today: </strong> {@code new LocalDateSpinnerValueFactory(LocalDate.MIN, LocalDate.MAX, LocalDate.now(), 1, ChronoUnit.MONTHS)}</li>
+         *     <li><strong>To step by one year from today: </strong> {@code new LocalDateSpinnerValueFactory(LocalDate.MIN, LocalDate.MAX, LocalDate.now(), 1, ChronoUnit.YEARS)}</li>
+         * </ul>
+         *
+         * @param min The minimum allowed double value for the Spinner.
+         * @param max The maximum allowed double value for the Spinner.
+         * @param initialValue The value of the Spinner when first instantiated.
+         * @param amountToStepBy The amount to increment or decrement by, per step.
+         * @param temporalUnit The size of each step (e.g. day, week, month, year, etc)
+         */
+        public LocalDateSpinnerValueFactory(@NamedArg("min") LocalDate min,
+                                            @NamedArg("min") LocalDate max,
+                                            @NamedArg("initialValue") LocalDate initialValue,
+                                            @NamedArg("amountToStepBy") long amountToStepBy,
+                                            @NamedArg("temporalUnit") TemporalUnit temporalUnit) {
+            setMin(min);
+            setMax(max);
+            setAmountToStepBy(amountToStepBy);
+            setTemporalUnit(temporalUnit);
+            setConverter(new StringConverter<LocalDate>() {
+                @Override public String toString(LocalDate object) {
+                    if (object == null) {
+                        return "";
+                    }
+                    return object.toString();
+                }
+
+                @Override public LocalDate fromString(String string) {
+                    return LocalDate.parse(string);
+                }
+            });
+
+            valueProperty().addListener((o, oldValue, newValue) -> {
+                // when the value is set, we need to react to ensure it is a
+                // valid value (and if not, blow up appropriately)
+                if (getMin() != null && newValue.isBefore(getMin())) {
+                    setValue(getMin());
+                } else if (getMax() != null && newValue.isAfter(getMax())) {
+                    setValue(getMax());
+                }
+            });
+            setValue(initialValue != null ? initialValue : LocalDate.now());
+        }
+
+
+
+        /***********************************************************************
+         *                                                                     *
+         * Properties                                                          *
+         *                                                                     *
+         **********************************************************************/
+
+        // --- min
+        private ObjectProperty<LocalDate> min = new SimpleObjectProperty<LocalDate>(this, "min") {
+            @Override protected void invalidated() {
+                LocalDate currentValue = LocalDateSpinnerValueFactory.this.getValue();
+                if (currentValue == null) {
+                    return;
+                }
+
+                final LocalDate newMin = get();
+                if (newMin.isAfter(getMax())) {
+                    setMin(getMax());
+                    return;
+                }
+
+                if (currentValue.isBefore(newMin)) {
+                    LocalDateSpinnerValueFactory.this.setValue(newMin);
+                }
+            }
+        };
+
+        public final void setMin(LocalDate value) {
+            min.set(value);
+        }
+        public final LocalDate getMin() {
+            return min.get();
+        }
+        /**
+         * Sets the minimum allowable value for this value factory
+         */
+        public final ObjectProperty<LocalDate> minProperty() {
+            return min;
+        }
+
+        // --- max
+        private ObjectProperty<LocalDate> max = new SimpleObjectProperty<LocalDate>(this, "max") {
+            @Override protected void invalidated() {
+                LocalDate currentValue = LocalDateSpinnerValueFactory.this.getValue();
+                if (currentValue == null) {
+                    return;
+                }
+
+                final LocalDate newMax = get();
+                if (newMax.isBefore(getMin())) {
+                    setMax(getMin());
+                    return;
+                }
+
+                if (currentValue.isAfter(newMax)) {
+                    LocalDateSpinnerValueFactory.this.setValue(newMax);
+                }
+            }
+        };
+
+        public final void setMax(LocalDate value) {
+            max.set(value);
+        }
+        public final LocalDate getMax() {
+            return max.get();
+        }
+        /**
+         * Sets the maximum allowable value for this value factory
+         */
+        public final ObjectProperty<LocalDate> maxProperty() {
+            return max;
+        }
+
+        // --- temporalUnit
+        private ObjectProperty<TemporalUnit> temporalUnit = new SimpleObjectProperty<>(this, "temporalUnit");
+        public final void setTemporalUnit(TemporalUnit value) {
+            temporalUnit.set(value);
+        }
+        public final TemporalUnit getTemporalUnit() {
+            return temporalUnit.get();
+        }
+        /**
+         * The size of each step (e.g. day, week, month, year, etc).
+         */
+        public final ObjectProperty<TemporalUnit> temporalUnitProperty() {
+            return temporalUnit;
+        }
+
+        // --- amountToStepBy
+        private LongProperty amountToStepBy = new SimpleLongProperty(this, "amountToStepBy");
+        public final void setAmountToStepBy(long value) {
+            amountToStepBy.set(value);
+        }
+        public final long getAmountToStepBy() {
+            return amountToStepBy.get();
+        }
+        /**
+         * Sets the amount to increment or decrement by, per step.
+         */
+        public final LongProperty amountToStepByProperty() {
+            return amountToStepBy;
+        }
+
+
+
+        /***********************************************************************
+         *                                                                     *
+         * Overridden methods                                                  *
+         *                                                                     *
+         **********************************************************************/
+
+        /** {@inheritDoc} */
+        @Override public void decrement(int steps) {
+            final LocalDate currentValue = getValue();
+            final LocalDate min = getMin();
+            LocalDate newValue = currentValue.minus(getAmountToStepBy() * steps, getTemporalUnit());
+
+            if (min != null && isWrapAround() && newValue.isBefore(min)) {
+                // we need to wrap around
+                newValue = getMax();
+            }
+
+            setValue(newValue);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void increment(int steps) {
+            final LocalDate currentValue = getValue();
+            final LocalDate max = getMax();
+            LocalDate newValue = currentValue.plus(getAmountToStepBy() * steps, getTemporalUnit());
+
+            if (max != null && isWrapAround() && newValue.isAfter(max)) {
+                // we need to wrap around
+                newValue = getMin();
+            }
+
+            setValue(newValue);
+        }
+    }
+}
\ No newline at end of file
--- a/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css	Fri Jul 11 18:50:36 2014 +0200
+++ b/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css	Sat Jul 12 16:03:15 2014 +1200
@@ -3147,3 +3147,184 @@
 .date-picker-popup > * > .next-month.today:hover {
     -fx-background-color: -fx-selection-bar-non-focused, derive(-fx-selection-bar-non-focused, -20%), -fx-selection-bar-non-focused;
 }
+
+
+/*******************************************************************************
+ *                                                                             *
+ * Spinner                                                                     *
+ *                                                                             *
+ ******************************************************************************/
+.spinner {
+    -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
+    -fx-background-insets: 0 0 -1 0, 0, 1, 2;
+    -fx-background-radius: 3px, 3px, 2px, 1px;
+
+    -fx-text-fill: -fx-text-base-color;
+    -fx-alignment: CENTER;
+    -fx-content-display: LEFT;
+}
+
+.spinner:focused,
+.spinner:contains-focus {
+    -fx-background-color: -fx-focus-color, -fx-inner-border, -fx-body-color, -fx-faint-focus-color, -fx-body-color;
+    -fx-background-insets: -0.2, 1, 2, -1.4, 2.6;
+    -fx-background-radius: 3, 2, 1, 4, 1;
+}
+
+.spinner > .text-field {
+    -fx-background-color: -fx-control-inner-background;
+    -fx-background-insets: 1 0 1 1;
+    -fx-background-radius: 2 0 0 2;
+}
+
+.spinner .increment-arrow-button {
+    -fx-background-color: -fx-outer-border, -fx-inner-border, -fx-body-color;
+    /* Change the two 0's here to -1 to get rid of the horizontal line */
+    -fx-background-insets: 0 0 -1 0, 1 1 0 1, 2 2 1 2;
+    -fx-background-radius: 0 3 0 0;
+
+    -fx-padding: 0.333335em 0.666667em 0.333335em 0.666667em; /* 5 8 3 8 */
+    -fx-text-fill: -fx-text-base-color;
+    -fx-alignment: CENTER;
+    -fx-content-display: LEFT;
+}
+
+.spinner .decrement-arrow-button {
+    -fx-background-color: -fx-outer-border, -fx-inner-border, -fx-body-color;
+    -fx-background-insets: 0, 0 1 1 1, 1 2 2 2;
+    -fx-background-radius: 0 0 3 0;
+
+    -fx-padding: 0.333335em 0.666667em 0.333335em 0.666667em; /* 3 8 5 8 */
+    -fx-text-fill: -fx-text-base-color;
+    -fx-alignment: CENTER;
+    -fx-content-display: LEFT;
+}
+
+.spinner:focused .increment-arrow-button,
+.spinner:contains-focus .increment-arrow-button,
+.spinner:focused .decrement-arrow-button,
+.spinner:contains-focus .decrement-arrow-button {
+    -fx-background-color: -fx-focus-color, -fx-body-color, -fx-faint-focus-color, -fx-body-color;
+}
+
+.spinner .increment-arrow-button:hover,
+.spinner .decrement-arrow-button:hover {
+    -fx-color: -fx-hover-base;
+}
+
+.spinner .increment-arrow-button:pressed,
+.spinner .decrement-arrow-button:pressed {
+    -fx-color: -fx-pressed-base;
+}
+
+.spinner .increment-arrow-button .increment-arrow {
+    -fx-background-color: -fx-mark-highlight-color, -fx-mark-color;
+    -fx-background-insets: 0 0 -1 0, 0;
+    -fx-padding: 0.166667em 0.333333em 0.166667em 0.333333em; /* 2 4 2 4 */
+    -fx-shape: "M 0 4 h 7 l -3.5 -4 z";
+}
+
+.spinner .decrement-arrow-button .decrement-arrow {
+    -fx-background-color: -fx-mark-highlight-color, -fx-mark-color;
+    -fx-background-insets: 0 0 -1 0, 0;
+    -fx-padding: 0.166667em 0.333333em 0.166667em 0.333333em; /* 2 4 2 4 */
+    -fx-shape: "M 0 0 h 7 l -3.5 4 z";
+}
+
+/* Spinner - STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL */
+.spinner.arrows-on-right-horizontal > .text-field {
+    -fx-background-color: -fx-control-inner-background;
+    -fx-background-insets: 1 0 1 1;
+    -fx-background-radius: 2 0 0 2
+}
+
+.spinner.arrows-on-right-horizontal .increment-arrow-button {
+    -fx-background-radius: 0 3 3 0;
+    -fx-background-insets: 0 0 0 -1, 1 1 1 0, 2 2 2 1;
+}
+
+.spinner.arrows-on-right-horizontal .decrement-arrow-button {
+    -fx-background-radius: 0;
+    -fx-background-insets: 0, 1, 2;
+}
+
+/* Spinner - STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL */
+.spinner.arrows-on-left-vertical > .text-field {
+    -fx-background-color: -fx-control-inner-background;
+    -fx-background-insets: 1 1 1 0;
+    -fx-background-radius: 0 2 2 0;
+}
+
+.spinner.arrows-on-left-vertical .increment-arrow-button {
+    -fx-background-radius: 3 0 0 0;
+}
+
+.spinner.arrows-on-left-vertical .decrement-arrow-button {
+    -fx-background-radius: 0 0 0 3;
+}
+
+/* Spinner - STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL */
+.spinner.arrows-on-left-horizontal > .text-field {
+    -fx-background-color: -fx-control-inner-background;
+    -fx-background-insets: 1 1 1 0;
+    -fx-background-radius: 0 2 2 0;
+}
+
+.spinner.arrows-on-left-horizontal .increment-arrow-button {
+    -fx-background-radius: 0;
+    -fx-background-insets: 0 0 0 -1, 1 1 1 0, 2 2 2 1;
+}
+
+.spinner.arrows-on-left-horizontal .decrement-arrow-button {
+    -fx-background-radius: 3 0 0 3;
+    -fx-background-insets: 0, 1, 2;
+}
+
+/* Spinner - STYLE_CLASS_SPLIT_ARROWS_VERTICAL */
+.spinner.split-arrows-vertical > .text-field {
+    -fx-background-color: -fx-control-inner-background;
+    -fx-background-insets: 0 1 0 1;
+    -fx-background-radius: 0;
+}
+
+.spinner.split-arrows-vertical .increment-arrow-button {
+    -fx-background-insets: 0, 1, 2;
+    -fx-background-radius: 3 3 0 0;
+}
+
+.spinner.split-arrows-horizontal .decrement-arrow-button {
+    -fx-background-insets: 0, 1, 2;
+    -fx-background-radius: 3 0 0 3;
+}
+
+.spinner.split-arrows-vertical .decrement-arrow-button {
+    -fx-background-insets: 0, 1, 2;
+    -fx-background-radius: 0 0 3 3;
+}
+
+/* Spinner - STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL */
+.spinner.split-arrows-horizontal .increment-arrow-button {
+    -fx-background-insets: 0, 1, 2;
+    -fx-background-radius: 0 3 3 0;
+}
+
+.spinner.split-arrows-horizontal .increment-arrow-button {
+    -fx-padding: 0.333333em 0.666667em 0.333333em 0.666667em; /* 4 8 4 8 */
+}
+.spinner.split-arrows-horizontal .decrement-arrow-button {
+    -fx-padding: 0.333333em 0.666667em 0.333333em 0.666667em; /* 4 8 4 8 */
+}
+
+/* Spinner - Horizontal arrows */
+.spinner.split-arrows-horizontal .increment-arrow-button .increment-arrow,
+.spinner.arrows-on-right-horizontal .increment-arrow-button .increment-arrow,
+.spinner.arrows-on-left-horizontal .increment-arrow-button .increment-arrow {
+    -fx-padding: 0.333333em 0.166667em 0.333333em 0.166667em; /* 4 2 4 2 */
+    -fx-shape: "M 0 0 v 7 l 3.5 -4 z";
+}
+.spinner.split-arrows-horizontal .decrement-arrow-button .decrement-arrow,
+.spinner.arrows-on-right-horizontal .decrement-arrow-button .decrement-arrow,
+.spinner.arrows-on-left-horizontal .decrement-arrow-button .decrement-arrow {
+    -fx-padding: 0.333333em 0.166667em 0.333333em 0.166667em; /* 4 2 4 2 */
+    -fx-shape: "M 4 0 v 7 l -4 -3.5 z";
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/controls/src/test/java/javafx/scene/control/SpinnerTest.java	Sat Jul 12 16:03:15 2014 +1200
@@ -0,0 +1,1063 @@
+/*
+ * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package javafx.scene.control;
+
+import static junit.framework.Assert.*;
+
+import com.sun.javafx.scene.control.skin.SpinnerSkin;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
+
+import static javafx.scene.control.SpinnerValueFactory.*;
+
+public class SpinnerTest {
+
+    private Spinner<?> spinner;
+
+    // --- int spinner
+    private Spinner<Integer> intSpinner;
+    private IntegerSpinnerValueFactory intValueFactory;
+
+    // --- double spinner
+    private Spinner<Double> dblSpinner;
+    private DoubleSpinnerValueFactory dblValueFactory;
+
+    // --- list spinner
+    private ObservableList<String> strings;
+    private Spinner<String> listSpinner;
+    private ListSpinnerValueFactory listValueFactory;
+
+    // --- LocalDate spinner
+    private Spinner<LocalDate> localDateSpinner;
+    private LocalDateSpinnerValueFactory localDateValueFactory;
+
+    // used in tests for counting events, reset to zero in setup()
+    private int eventCount;
+
+
+    @Before public void setup() {
+        eventCount = 0;
+        spinner = new Spinner();
+
+        intSpinner = new Spinner<>(0, 10, 5, 1);
+        intValueFactory = (IntegerSpinnerValueFactory) intSpinner.getValueFactory();
+
+        dblSpinner = new Spinner<>(0.0, 1.0, 0.5, 0.05);
+        dblValueFactory = (DoubleSpinnerValueFactory) dblSpinner.getValueFactory();
+
+        strings = FXCollections.observableArrayList("string1", "string2", "string3");
+        listSpinner = new Spinner<>(strings);
+        listValueFactory = (ListSpinnerValueFactory<String>) listSpinner.getValueFactory();
+
+        // minimum is today minus 10 days, maximum is today plus 10 days
+        localDateSpinner = new Spinner<>(nowPlusDays(-10), nowPlusDays(10), LocalDate.now(), 1, ChronoUnit.DAYS);
+        localDateValueFactory = (LocalDateSpinnerValueFactory) localDateSpinner.getValueFactory();
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Basic tests                                                             *
+     *                                                                         *
+     **************************************************************************/
+
+    @Test public void createDefaultSpinner_hasSpinnerStyleClass() {
+        assertEquals(1, spinner.getStyleClass().size());
+        assertTrue(spinner.getStyleClass().contains("spinner"));
+    }
+
+    @Test public void createDefaultSpinner_editorIsNotNull() {
+        assertNotNull(spinner.getEditor());
+    }
+
+    @Test public void createDefaultSpinner_valueFactoryIsNull() {
+        assertNull(spinner.getValueFactory());
+    }
+
+    @Test public void createDefaultSpinner_valueIsNull() {
+        assertNull(spinner.getValue());
+    }
+
+    @Test public void createDefaultSpinner_editableIsFalse() {
+        assertFalse(spinner.isEditable());
+    }
+
+//    @Test public void createDefaultSpinner_wrapAroundIsFalse() {
+//        assertFalse(spinner.isWrapAround());
+//    }
+
+    @Ignore("Waiting for StageLoader")
+    @Test public void createDefaultSpinner_defaultSkinIsInstalled() {
+        assertTrue(spinner.getSkin() instanceof SpinnerSkin);
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Alternative constructor tests                                           *
+     *                                                                         *
+     **************************************************************************/
+
+    @Test public void createIntSpinner_createValidValueFactory() {
+        Spinner<Integer> intSpinner = new Spinner<Integer>(0, 10, 5, 1);
+        assertTrue(intSpinner.getValueFactory() instanceof IntegerSpinnerValueFactory);
+        IntegerSpinnerValueFactory valueFactory = (IntegerSpinnerValueFactory) intSpinner.getValueFactory();
+        assertEquals(5, (int) valueFactory.getValue());
+    }
+
+    @Test public void createIntSpinner_setInitialValueOutsideMaxBounds() {
+        Spinner<Integer> intSpinner = new Spinner<Integer>(0, 10, 100, 1);
+        assertTrue(intSpinner.getValueFactory() instanceof IntegerSpinnerValueFactory);
+        IntegerSpinnerValueFactory valueFactory = (IntegerSpinnerValueFactory) intSpinner.getValueFactory();
+        assertEquals(0, (int) valueFactory.getValue());
+    }
+
+    @Test public void createIntSpinner_setInitialValueOutsideMinBounds() {
+        Spinner<Integer> intSpinner = new Spinner<Integer>(0, 10, -100, 1);
+        assertTrue(intSpinner.getValueFactory() instanceof IntegerSpinnerValueFactory);
+        IntegerSpinnerValueFactory valueFactory = (IntegerSpinnerValueFactory) intSpinner.getValueFactory();
+        assertEquals(0, (int) valueFactory.getValue());
+    }
+
+    @Test public void createListSpinner_createValidValueFactory() {
+        Spinner<String> stringSpinner = new Spinner<>(FXCollections.observableArrayList("item 1", "item 2"));
+        assertTrue(stringSpinner.getValueFactory() instanceof ListSpinnerValueFactory);
+        ListSpinnerValueFactory valueFactory = (ListSpinnerValueFactory) stringSpinner.getValueFactory();
+        assertEquals("item 1", valueFactory.getValue());
+    }
+
+    @Test public void createListSpinner_emptyListResultsInNullValue() {
+        Spinner<String> stringSpinner = new Spinner<String>(FXCollections.observableArrayList());
+        assertTrue(stringSpinner.getValueFactory() instanceof ListSpinnerValueFactory);
+        ListSpinnerValueFactory valueFactory = (ListSpinnerValueFactory) stringSpinner.getValueFactory();
+        assertNull(valueFactory.getValue());
+    }
+
+    @Test public void createListSpinner_nullListResultsInNullValue() {
+        Spinner<String> stringSpinner = new Spinner<>((ObservableList<String>)null);
+        assertTrue(stringSpinner.getValueFactory() instanceof ListSpinnerValueFactory);
+        ListSpinnerValueFactory valueFactory = (ListSpinnerValueFactory) stringSpinner.getValueFactory();
+        assertNull(valueFactory.getValue());
+    }
+
+    @Test public void createSpinner_customSpinnerValueFactory() {
+        SpinnerValueFactory<String> valueFactory = new ListSpinnerValueFactory<>(FXCollections.observableArrayList("item 1", "item 2"));
+        Spinner<String> stringSpinner = new Spinner<>(valueFactory);
+        assertEquals(valueFactory, stringSpinner.getValueFactory());
+        ListSpinnerValueFactory valueFactory1 = (ListSpinnerValueFactory) stringSpinner.getValueFactory();
+        assertEquals("item 1", valueFactory.getValue());
+        assertEquals("item 1", valueFactory1.getValue());
+    }
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * increment / decrement tests                                             *
+     * (we test the actual inc / dec in the value factory impl tests)          *
+     *                                                                         *
+     **************************************************************************/
+
+    @Test(expected = IllegalStateException.class)
+    public void expectExceptionWhenNoArgsIncrementCalled_noValueFactory() {
+        spinner.increment();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void expectExceptionWhenOneArgsIncrementCalled_noValueFactory() {
+        spinner.increment(2);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void expectExceptionWhenNoArgsDecrementCalled_noValueFactory() {
+        spinner.decrement();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void expectExceptionWhenOneArgsDecrementCalled_noValueFactory() {
+        spinner.decrement(2);
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * changing value factory tests                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    @Test public void valueFactory_valueIsNulledWhenValueFactoryisNull() {
+        assertEquals(5, (int) intSpinner.getValue());
+        intSpinner.setValueFactory(null);
+        assertNull(spinner.getValue());
+    }
+
+    @Test public void valueFactory_valueIsUpdatedWhenValueFactoryChanged() {
+        assertEquals(5, (int) intSpinner.getValue());
+        intSpinner.setValueFactory(new IntegerSpinnerValueFactory(0, 10, 8));
+        assertEquals(8, (int) intSpinner.getValue());
+    }
+
+//    @Test public void valueFactory_spinnerPropertyIsNullWhenRemovedFromSpinner() {
+//        SpinnerValueFactory initialValueFactory = intSpinner.getValueFactory();
+//        assertEquals(intSpinner, initialValueFactory.getSpinner());
+//
+//        intSpinner.setValueFactory(null);
+//        assertNull(intSpinner.getValueFactory());
+//    }
+//
+//    @Test public void valueFactory_spinnerPropertyIsSetOnNewSpinner() {
+//        SpinnerValueFactory initialValueFactory = intSpinner.getValueFactory();
+//        assertEquals(intSpinner, initialValueFactory.getSpinner());
+//
+//        SpinnerValueFactory newValueFactory = new IntSpinnerValueFactory(0, 10, 8);
+//        intSpinner.setValueFactory(newValueFactory);
+//
+//        assertNull(initialValueFactory.getSpinner());
+//        assertEquals(intSpinner, newValueFactory.getSpinner());
+//    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * value property events                                                   *
+     *                                                                         *                                                                         *
+     **************************************************************************/
+
+    @Test public void value_notifyWhenChanged_validValue() {
+        assertEquals(5, (int) intSpinner.getValue());
+        intSpinner.valueProperty().addListener(o -> eventCount++);
+        intSpinner.getValueFactory().setValue(3);
+        assertEquals(1, eventCount);
+    }
+
+    @Test public void value_notifyWhenChanged_invalidValue() {
+        assertEquals(5, (int) intSpinner.getValue());
+        intSpinner.valueProperty().addListener(o -> eventCount++);
+        intSpinner.getValueFactory().setValue(1000);
+
+        // we expect two events: firstly, one for the invalid value, and another
+        // for the valid value
+        assertEquals(2, eventCount);
+    }
+
+    @Test public void value_notifyWhenChanged_existingValue() {
+        assertEquals(5, (int) intSpinner.getValue());
+        intSpinner.valueProperty().addListener(o -> eventCount++);
+        intSpinner.getValueFactory().setValue(5);
+        assertEquals(0, eventCount);
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * editing tests                                                           *
+     *                                                                         *
+     **************************************************************************/
+
+    @Ignore("Need KeyboardEventFirer")
+    @Test public void editing_commitValidInput() {
+        intSpinner.valueProperty().addListener(o -> eventCount++);
+        intSpinner.getEditor().setText("3");
+        // TODO press enter
+
+        assertEquals(1, eventCount);
+        assertEquals(3, (int) intSpinner.getValue());
+        assertEquals("3", intSpinner.getEditor().getText());
+    }
+
+    @Ignore("Need KeyboardEventFirer")
+    @Test public void editing_commitInvalidInput() {
+        intSpinner.valueProperty().addListener(o -> eventCount++);
+        intSpinner.getEditor().setText("300");
+        // TODO press enter
+
+        assertEquals(2, eventCount);
+        assertEquals(5, (int) intSpinner.getValue());
+        assertEquals("5", intSpinner.getEditor().getText());
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * IntegerSpinnerValueFactory tests                                        *
+     *                                                                         *
+     **************************************************************************/
+
+    @Test public void intSpinner_testIncrement_oneStep() {
+        intValueFactory.increment(1);
+        assertEquals(6, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_testIncrement_twoSteps() {
+        intValueFactory.increment(2);
+        assertEquals(7, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_testIncrement_manyCalls() {
+        for (int i = 0; i < 100; i++) {
+            intValueFactory.increment(1);
+        }
+        assertEquals(10, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_testIncrement_bigStepPastMaximum() {
+        intValueFactory.increment(1000);
+        assertEquals(10, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_testDecrement_oneStep() {
+        intValueFactory.decrement(1);
+        assertEquals(4, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_testDecrement_twoSteps() {
+        intValueFactory.decrement(2);
+        assertEquals(3, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_testDecrement_manyCalls() {
+        for (int i = 0; i < 100; i++) {
+            intValueFactory.decrement(1);
+        }
+        assertEquals(0, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_testDecrement_bigStepPastMinimum() {
+        intValueFactory.decrement(1000);
+        assertEquals(0, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_testWrapAround_increment_oneStep() {
+        intValueFactory.setWrapAround(true);
+        intValueFactory.increment(1); // 6
+        intValueFactory.increment(1); // 7
+        intValueFactory.increment(1); // 8
+        intValueFactory.increment(1); // 9
+        intValueFactory.increment(1); // 10
+        intValueFactory.increment(1); // 0
+        intValueFactory.increment(1); // 1
+        assertEquals(1, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_testWrapAround_increment_twoSteps() {
+        intValueFactory.setWrapAround(true);
+        intValueFactory.increment(2); // 7
+        intValueFactory.increment(2); // 9
+        intValueFactory.increment(2); // 0
+        intValueFactory.increment(2); // 2
+        assertEquals(2, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_testWrapAround_decrement_oneStep() {
+        intValueFactory.setWrapAround(true);
+        intValueFactory.decrement(1); // 4
+        intValueFactory.decrement(1); // 3
+        intValueFactory.decrement(1); // 2
+        intValueFactory.decrement(1); // 1
+        intValueFactory.decrement(1); // 0
+        intValueFactory.decrement(1); // 10
+        intValueFactory.decrement(1); // 9
+        assertEquals(9, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_testWrapAround_decrement_twoSteps() {
+        intValueFactory.setWrapAround(true);
+        intValueFactory.decrement(2); // 3
+        intValueFactory.decrement(2); // 1
+        intValueFactory.decrement(2); // 10
+        intValueFactory.decrement(2); // 8
+        assertEquals(8, (int) intValueFactory.getValue());
+    }
+
+    @Test public void intSpinner_assertDefaultConverterIsNonNull() {
+        assertNotNull(intValueFactory.getConverter());
+    }
+
+    @Test public void intSpinner_testToString_valueInRange() {
+        assertEquals("3", intValueFactory.getConverter().toString(3));
+    }
+
+    @Test public void intSpinner_testToString_valueOutOfRange() {
+        assertEquals("300", intValueFactory.getConverter().toString(300));
+    }
+
+    @Test public void intSpinner_testFromString_valueInRange() {
+        assertEquals(3, (int) intValueFactory.getConverter().fromString("3"));
+    }
+
+    @Test public void intSpinner_testFromString_valueOutOfRange() {
+        assertEquals(300, (int) intValueFactory.getConverter().fromString("300"));
+    }
+
+    @Test public void intSpinner_testSetMin_doesNotChangeSpinnerValueWhenMinIsLessThanCurrentValue() {
+        intValueFactory.setValue(5);
+        assertEquals(5, (int) intSpinner.getValue());
+        intValueFactory.setMin(3);
+        assertEquals(5, (int) intSpinner.getValue());
+    }
+
+    @Test public void intSpinner_testSetMin_changesSpinnerValueWhenMinIsGreaterThanCurrentValue() {
+        intValueFactory.setValue(0);
+        assertEquals(0, (int) intSpinner.getValue());
+        intValueFactory.setMin(5);
+        assertEquals(5, (int) intSpinner.getValue());
+    }
+
+    @Test public void intSpinner_testSetMin_ensureThatMinCanNotExceedMax() {
+        assertEquals(0, intValueFactory.getMin());
+        assertEquals(10, intValueFactory.getMax());
+        intValueFactory.setMin(20);
+        assertEquals(10, intValueFactory.getMin());
+    }
+
+    @Test public void intSpinner_testSetMin_ensureThatMinCanEqualMax() {
+        assertEquals(0, intValueFactory.getMin());
+        assertEquals(10, intValueFactory.getMax());
+        intValueFactory.setMin(10);
+        assertEquals(10, intValueFactory.getMin());
+    }
+
+    @Test public void intSpinner_testSetMax_doesNotChangeSpinnerValueWhenMaxIsGreaterThanCurrentValue() {
+        intValueFactory.setValue(5);
+        assertEquals(5, (int) intSpinner.getValue());
+        intValueFactory.setMax(8);
+        assertEquals(5, (int) intSpinner.getValue());
+    }
+
+    @Test public void intSpinner_testSetMax_changesSpinnerValueWhenMaxIsGreaterThanCurrentValue() {
+        intValueFactory.setValue(5);
+        assertEquals(5, (int) intSpinner.getValue());
+        intValueFactory.setMax(3);
+        assertEquals(3, (int) intSpinner.getValue());
+    }
+
+    @Test public void intSpinner_testSetMax_ensureThatMaxCanNotGoLessThanMin() {
+        intValueFactory.setMin(5);
+        assertEquals(5, intValueFactory.getMin());
+        assertEquals(10, intValueFactory.getMax());
+        intValueFactory.setMax(3);
+        assertEquals(5, intValueFactory.getMin());
+    }
+
+    @Test public void intSpinner_testSetMax_ensureThatMaxCanEqualMin() {
+        intValueFactory.setMin(5);
+        assertEquals(5, intValueFactory.getMin());
+        assertEquals(10, intValueFactory.getMax());
+        intValueFactory.setMax(5);
+        assertEquals(5, intValueFactory.getMin());
+    }
+
+    @Test public void intSpinner_testSetValue_canNotExceedMax() {
+        assertEquals(0, intValueFactory.getMin());
+        assertEquals(10, intValueFactory.getMax());
+        intValueFactory.setValue(50);
+        assertEquals(10, (int) intSpinner.getValue());
+    }
+
+    @Test public void intSpinner_testSetValue_canNotExceedMin() {
+        assertEquals(0, intValueFactory.getMin());
+        assertEquals(10, intValueFactory.getMax());
+        intValueFactory.setValue(-50);
+        assertEquals(0, (int) intSpinner.getValue());
+    }
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * DoubleSpinnerValueFactory tests                                         *
+     *                                                                         *                                                                         *
+     **************************************************************************/
+
+    @Test public void dblSpinner_testIncrement_oneStep() {
+        dblValueFactory.increment(1);
+        assertEquals(0.55, dblValueFactory.getValue(), 0);
+    }
+
+    @Test public void dblSpinner_testIncrement_twoSteps() {
+        dblValueFactory.increment(2);
+        assertEquals(0.6, dblValueFactory.getValue(), 0);
+    }
+
+    @Test public void dblSpinner_testIncrement_manyCalls() {
+        for (int i = 0; i < 100; i++) {
+            dblValueFactory.increment(1);
+        }
+        assertEquals(1.0, dblValueFactory.getValue(), 0);
+    }
+
+    @Test public void dblSpinner_testIncrement_bigStepPastMaximum() {
+        dblValueFactory.increment(1000);
+        assertEquals(1.0, dblValueFactory.getValue(), 0);
+    }
+
+    @Test public void dblSpinner_testDecrement_oneStep() {
+        dblValueFactory.decrement(1);
+        assertEquals(0.45, dblValueFactory.getValue());
+    }
+
+    @Test public void dblSpinner_testDecrement_twoSteps() {
+        dblValueFactory.decrement(2);
+        assertEquals(0.4, dblValueFactory.getValue());
+    }
+
+    @Test public void dblSpinner_testDecrement_manyCalls() {
+        for (int i = 0; i < 100; i++) {
+            dblValueFactory.decrement(1);
+        }
+        assertEquals(0, dblValueFactory.getValue(), 0);
+    }
+
+    @Test public void dblSpinner_testDecrement_bigStepPastMinimum() {
+        dblValueFactory.decrement(1000);
+        assertEquals(0, dblValueFactory.getValue(), 0);
+    }
+
+    @Test public void dblSpinner_testWrapAround_increment_oneStep() {
+        dblValueFactory.setWrapAround(true);
+        dblValueFactory.setValue(0.80);
+        dblValueFactory.increment(1); // 0.85
+        dblValueFactory.increment(1); // 0.90
+        dblValueFactory.increment(1); // 0.95
+        dblValueFactory.increment(1); // 1.00
+        dblValueFactory.increment(1); // 0.00
+        dblValueFactory.increment(1); // 0.05
+        dblValueFactory.increment(1); // 0.10
+        assertEquals(0.10, dblValueFactory.getValue(), 0);
+    }
+
+    @Test public void dblSpinner_testWrapAround_increment_twoSteps() {
+        dblValueFactory.setWrapAround(true);
+        dblValueFactory.setValue(0.80);
+        dblValueFactory.increment(2); // 0.90
+        dblValueFactory.increment(2); // 1.00
+        dblValueFactory.increment(2); // 0.00
+        dblValueFactory.increment(2); // 0.10
+        assertEquals(0.10, dblValueFactory.getValue(), 0);
+    }
+
+    @Test public void dblSpinner_testWrapAround_decrement_oneStep() {
+        dblValueFactory.setWrapAround(true);
+        dblValueFactory.setValue(0.20);
+        dblValueFactory.decrement(1); // 0.15
+        dblValueFactory.decrement(1); // 0.10
+        dblValueFactory.decrement(1); // 0.05
+        dblValueFactory.decrement(1); // 0.00
+        dblValueFactory.decrement(1); // 1.00
+        dblValueFactory.decrement(1); // 0.95
+        dblValueFactory.decrement(1); // 0.90
+        assertEquals(0.90, dblValueFactory.getValue(), 0);
+    }
+
+    @Test public void dblSpinner_testWrapAround_decrement_twoSteps() {
+        dblValueFactory.setWrapAround(true);
+        dblValueFactory.setValue(0.20);
+        dblValueFactory.decrement(2); // 0.10
+        dblValueFactory.decrement(2); // 0.00
+        dblValueFactory.decrement(2); // 1.00
+        dblValueFactory.decrement(2); // 0.90
+        assertEquals(0.90, dblValueFactory.getValue());
+    }
+
+    @Test public void dblSpinner_assertDefaultConverterIsNonNull() {
+        assertNotNull(dblValueFactory.getConverter());
+    }
+
+    @Test public void dblSpinner_testToString_valueInRange() {
+        assertEquals("0.3", dblValueFactory.getConverter().toString(0.3));
+    }
+
+    @Test public void dblSpinner_testToString_valueOutOfRange() {
+        assertEquals("300", dblValueFactory.getConverter().toString(300D));
+    }
+
+    @Test public void dblSpinner_testFromString_valueInRange() {
+        assertEquals(0.3, dblValueFactory.getConverter().fromString("0.3"));
+    }
+
+    @Test public void dblSpinner_testFromString_valueOutOfRange() {
+        assertEquals(300.0, dblValueFactory.getConverter().fromString("300"), 0);
+    }
+
+    @Test public void dblSpinner_testSetMin_doesNotChangeSpinnerValueWhenMinIsLessThanCurrentValue() {
+        dblValueFactory.setValue(0.5);
+        assertEquals(0.5, dblSpinner.getValue());
+        dblValueFactory.setMin(0.3);
+        assertEquals(0.5, dblSpinner.getValue());
+    }
+
+    @Test public void dblSpinner_testSetMin_changesSpinnerValueWhenMinIsGreaterThanCurrentValue() {
+        dblValueFactory.setValue(0.0);
+        assertEquals(0.0, dblSpinner.getValue());
+        dblValueFactory.setMin(0.5);
+        assertEquals(0.5, dblSpinner.getValue());
+    }
+
+    @Test public void dblSpinner_testSetMin_ensureThatMinCanNotExceedMax() {
+        assertEquals(0, dblValueFactory.getMin(), 0);
+        assertEquals(1.0, dblValueFactory.getMax());
+        dblValueFactory.setMin(20);
+        assertEquals(1.0, dblValueFactory.getMin());
+    }
+
+    @Test public void dblSpinner_testSetMin_ensureThatMinCanEqualMax() {
+        assertEquals(0, dblValueFactory.getMin(), 0);
+        assertEquals(1.0, dblValueFactory.getMax());
+        dblValueFactory.setMin(1.0);
+        assertEquals(1.0, dblValueFactory.getMin());
+    }
+
+    @Test public void dblSpinner_testSetMax_doesNotChangeSpinnerValueWhenMaxIsGreaterThanCurrentValue() {
+        dblValueFactory.setValue(0.5);
+        assertEquals(0.5, dblSpinner.getValue());
+        dblValueFactory.setMax(0.8);
+        assertEquals(0.5, dblSpinner.getValue());
+    }
+
+    @Test public void dblSpinner_testSetMax_changesSpinnerValueWhenMaxIsGreaterThanCurrentValue() {
+        dblValueFactory.setValue(0.5);
+        assertEquals(0.5, dblSpinner.getValue());
+        dblValueFactory.setMax(0.3);
+        assertEquals(0.3, dblSpinner.getValue());
+    }
+
+    @Test public void dblSpinner_testSetMax_ensureThatMaxCanNotGoLessThanMin() {
+        dblValueFactory.setMin(0.5);
+        assertEquals(0.5, dblValueFactory.getMin());
+        assertEquals(1.0, dblValueFactory.getMax());
+        dblValueFactory.setMax(0.3);
+        assertEquals(0.5, dblValueFactory.getMin());
+    }
+
+    @Test public void dblSpinner_testSetMax_ensureThatMaxCanEqualMin() {
+        dblValueFactory.setMin(0.5);
+        assertEquals(0.5, dblValueFactory.getMin());
+        assertEquals(1.0, dblValueFactory.getMax());
+        dblValueFactory.setMax(0.5);
+        assertEquals(0.5, dblValueFactory.getMin());
+    }
+
+    @Test public void dblSpinner_testSetValue_canNotExceedMax() {
+        assertEquals(0, dblValueFactory.getMin(), 0);
+        assertEquals(1.0, dblValueFactory.getMax());
+        dblValueFactory.setValue(5.0);
+        assertEquals(1.0, dblSpinner.getValue());
+    }
+
+    @Test public void dblSpinner_testSetValue_canNotExceedMin() {
+        assertEquals(0, dblValueFactory.getMin(), 0);
+        assertEquals(1.0, dblValueFactory.getMax(), 0);
+        dblValueFactory.setValue(-5.0);
+        assertEquals(0, dblSpinner.getValue(), 0);
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * ListSpinnerValueFactory tests                                           *
+     *                                                                         *
+     **************************************************************************/
+
+    @Test public void listSpinner_testIncrement_oneStep() {
+        listValueFactory.increment(1);
+        assertEquals("string2", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_testIncrement_twoSteps() {
+        listValueFactory.increment(2);
+        assertEquals("string3", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_testIncrement_manyCalls() {
+        for (int i = 0; i < 100; i++) {
+            listValueFactory.increment(1);
+        }
+        assertEquals("string3", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_testIncrement_bigStepPastMaximum() {
+        listValueFactory.increment(1000);
+        assertEquals("string3", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_testDecrement_oneStep() {
+        listValueFactory.decrement(1);
+        assertEquals("string1", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_testDecrement_twoSteps() {
+        listValueFactory.decrement(2);
+        assertEquals("string1", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_testDecrement_manyCalls() {
+        for (int i = 0; i < 100; i++) {
+            listValueFactory.decrement(1);
+        }
+        assertEquals("string1", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_testDecrement_bigStepPastMinimum() {
+        listValueFactory.decrement(1000);
+        assertEquals("string1", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_testWrapAround_increment_oneStep() {
+        listValueFactory.setWrapAround(true);
+        listValueFactory.increment(1); // string2
+        listValueFactory.increment(1); // string3
+        listValueFactory.increment(1); // string1
+        listValueFactory.increment(1); // string2
+        listValueFactory.increment(1); // string3
+        listValueFactory.increment(1); // string1
+        listValueFactory.increment(1); // string2
+        assertEquals("string2", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_testWrapAround_increment_twoSteps() {
+        listValueFactory.setWrapAround(true);
+        listValueFactory.increment(2); // string1 -> string3
+        listValueFactory.increment(2); // string3 -> string2
+        listValueFactory.increment(2); // string2 -> string1
+        listValueFactory.increment(2); // string1 -> string3
+        assertEquals("string3", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_testWrapAround_decrement_oneStep() {
+        listValueFactory.setWrapAround(true);
+        listValueFactory.decrement(1); // string3
+        listValueFactory.decrement(1); // string2
+        listValueFactory.decrement(1); // string1
+        listValueFactory.decrement(1); // string3
+        listValueFactory.decrement(1); // string2
+        listValueFactory.decrement(1); // string1
+        listValueFactory.decrement(1); // string3
+        assertEquals("string3", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_testWrapAround_decrement_twoSteps() {
+        listValueFactory.setWrapAround(true);
+        listValueFactory.decrement(2); // string1 -> string2
+        listValueFactory.decrement(2); // string2 -> string3
+        listValueFactory.decrement(2); // string3 -> string1
+        listValueFactory.decrement(2); // string1 -> string2
+        assertEquals("string2", listValueFactory.getValue());
+    }
+
+    @Test public void listSpinner_assertDefaultConverterIsNonNull() {
+        assertNotNull(listValueFactory.getConverter());
+    }
+
+    @Test public void listSpinner_testToString_valueInRange() {
+        assertEquals("string2", listValueFactory.getConverter().toString("string2"));
+    }
+
+    @Test public void listSpinner_testToString_valueOutOfRange() {
+        assertEquals("string300", listValueFactory.getConverter().toString("string300"));
+    }
+
+    @Test public void listSpinner_testFromString_valueInRange() {
+        assertEquals("string3", listValueFactory.getConverter().fromString("string3"));
+    }
+
+    @Test public void listSpinner_testFromString_valueOutOfRange() {
+        assertEquals("string300", listValueFactory.getConverter().fromString("string300"));
+    }
+
+    @Test public void listSpinner_testListChange_changeNonSelectedItem() {
+        assertEquals("string1", listSpinner.getValue());
+
+        strings.set(1, "string200"); // change 'string2' to 'string200'
+
+        // there should be no change
+        assertEquals("string1", listSpinner.getValue());
+    }
+
+    @Test public void listSpinner_testListChange_changeSelectedItem() {
+        assertEquals("string1", listSpinner.getValue());
+
+        strings.set(0, "string100"); // change 'string1' to 'string100'
+
+        // the selected value should change
+        assertEquals("string100", listSpinner.getValue());
+    }
+
+    @Test public void listSpinner_testListChange_changeEntireList_directly() {
+        assertEquals("string1", listSpinner.getValue());
+
+        listValueFactory.getItems().setAll("newString1", "newString2", "newString3");
+
+        // the selected value should change
+        assertEquals("newString1", listSpinner.getValue());
+    }
+
+    @Test public void listSpinner_testListChange_changeEntireList_usingSetter() {
+        assertEquals("string1", listSpinner.getValue());
+
+        listValueFactory.setItems(FXCollections.observableArrayList("newString1", "newString2", "newString3"));
+        assertEquals("newString1", listSpinner.getValue());
+    }
+
+    @Test public void listSpinner_testListChange_setItemsToNull() {
+        assertEquals("string1", listSpinner.getValue());
+        listValueFactory.setItems(null);
+        assertNull(listSpinner.getValue());
+    }
+
+    @Test public void listSpinner_testListChange_setItemsToNonNull() {
+        assertEquals("string1", listSpinner.getValue());
+        listValueFactory.setItems(null);
+        assertNull(listSpinner.getValue());
+
+        listValueFactory.setItems(FXCollections.observableArrayList("newString1", "newString2", "newString3"));
+        assertEquals("newString1", listSpinner.getValue());
+    }
+
+    @Test public void listSpinner_testListChange_setNewEmptyListOverOldEmptyList() {
+        // this tests the issue where we replace an empty list with another. As
+        // both empty lists are equal, we are ensuring that the listeners update
+        // to the new list.
+        ObservableList<String> firstEmptyList = FXCollections.observableArrayList();
+        ObservableList<String> newEmptyList = FXCollections.observableArrayList();
+
+        ListSpinnerValueFactory valueFactory = new ListSpinnerValueFactory(firstEmptyList);
+        Spinner listSpinner = new Spinner(valueFactory);
+
+        valueFactory.setItems(newEmptyList);
+        assertNull(listSpinner.getValue());
+
+        newEmptyList.addAll("newString1", "newString2", "newString3");
+        assertEquals("newString1", listSpinner.getValue());
+    }
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * LocalDateSpinnerValueFactory tests                                      *
+     *                                                                         *
+     **************************************************************************/
+
+    private LocalDate nowPlusDays(int days) {
+        return LocalDate.now().plus(days, ChronoUnit.DAYS);
+    }
+
+    @Test public void localDateSpinner_testIncrement_oneStep() {
+        localDateValueFactory.increment(1);
+        assertEquals(nowPlusDays(1), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_testIncrement_twoSteps() {
+        localDateValueFactory.increment(2);
+        assertEquals(nowPlusDays(2), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_testIncrement_manyCalls() {
+        for (int i = 0; i < 100; i++) {
+            localDateValueFactory.increment(1);
+        }
+        assertEquals(nowPlusDays(10), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_testIncrement_bigStepPastMaximum() {
+        localDateValueFactory.increment(1000);
+        assertEquals(nowPlusDays(10), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_testDecrement_oneStep() {
+        localDateValueFactory.decrement(1);
+        assertEquals(nowPlusDays(-1), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_testDecrement_twoSteps() {
+        localDateValueFactory.decrement(2);
+        assertEquals(nowPlusDays(-2), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_testDecrement_manyCalls() {
+        for (int i = 0; i < 100; i++) {
+            localDateValueFactory.decrement(1);
+        }
+        assertEquals(nowPlusDays(-10), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_testDecrement_bigStepPastMinimum() {
+        localDateValueFactory.decrement(1000);
+        assertEquals(nowPlusDays(-10), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_testWrapAround_increment_oneStep() {
+        localDateValueFactory.setWrapAround(true);
+        localDateValueFactory.setValue(nowPlusDays(7));
+        localDateValueFactory.increment(1); // nowPlusDays(8)
+        localDateValueFactory.increment(1); // nowPlusDays(9)
+        localDateValueFactory.increment(1); // nowPlusDays(10)
+        localDateValueFactory.increment(1); // nowPlusDays(-10)
+        localDateValueFactory.increment(1); // nowPlusDays(-9)
+        localDateValueFactory.increment(1); // nowPlusDays(-8)
+        localDateValueFactory.increment(1); // nowPlusDays(-7)
+        assertEquals(nowPlusDays(-7), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_testWrapAround_increment_twoSteps() {
+        localDateValueFactory.setWrapAround(true);
+        localDateValueFactory.setValue(nowPlusDays(7));
+        localDateValueFactory.increment(2); // nowPlusDays(9)
+        localDateValueFactory.increment(2); // nowPlusDays(-10)
+        localDateValueFactory.increment(2); // nowPlusDays(-8)
+        localDateValueFactory.increment(2); // nowPlusDays(-6)
+        assertEquals(nowPlusDays(-6), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_testWrapAround_decrement_oneStep() {
+        localDateValueFactory.setWrapAround(true);
+        localDateValueFactory.setValue(nowPlusDays(-8));
+        localDateValueFactory.decrement(1); // nowPlusDays(-9)
+        localDateValueFactory.decrement(1); // nowPlusDays(-10)
+        localDateValueFactory.decrement(1); // nowPlusDays(10)
+        localDateValueFactory.decrement(1); // nowPlusDays(9)
+        localDateValueFactory.decrement(1); // nowPlusDays(8)
+        localDateValueFactory.decrement(1); // nowPlusDays(7)
+        localDateValueFactory.decrement(1); // nowPlusDays(6)
+        assertEquals(nowPlusDays(6), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_testWrapAround_decrement_twoSteps() {
+        localDateValueFactory.setWrapAround(true);
+        localDateValueFactory.setValue(nowPlusDays(-8));
+        localDateValueFactory.decrement(2); // nowPlusDays(-10)
+        localDateValueFactory.decrement(2); // nowPlusDays(9)
+        localDateValueFactory.decrement(2); // nowPlusDays(7)
+        localDateValueFactory.decrement(2); // nowPlusDays(6)
+        assertEquals(nowPlusDays(6), localDateValueFactory.getValue());
+    }
+
+    @Test public void localDateSpinner_assertDefaultConverterIsNonNull() {
+        assertNotNull(localDateValueFactory.getConverter());
+    }
+
+    @Test public void localDateSpinner_testToString_valueInRange() {
+        assertEquals("2014-06-27", localDateValueFactory.getConverter().toString(LocalDate.of(2014, 6, 27)));
+    }
+
+    @Test public void localDateSpinner_testToString_valueOutOfRange() {
+        assertEquals("2024-06-27", localDateValueFactory.getConverter().toString(LocalDate.of(2024, 6, 27)));
+    }
+
+    @Test public void localDateSpinner_testFromString_valueInRange() {
+        assertEquals(LocalDate.of(2014, 6, 27), localDateValueFactory.getConverter().fromString("2014-06-27"));
+    }
+
+    @Test public void localDateSpinner_testFromString_valueOutOfRange() {
+        assertEquals(LocalDate.of(2024, 6, 27), localDateValueFactory.getConverter().fromString("2024-06-27"));
+    }
+
+    @Test public void localDateSpinner_testSetMin_doesNotChangeSpinnerValueWhenMinIsLessThanCurrentValue() {
+        LocalDate newValue = LocalDate.now();
+        localDateValueFactory.setValue(newValue);
+        assertEquals(newValue, localDateSpinner.getValue());
+        localDateValueFactory.setMin(nowPlusDays(-3));
+        assertEquals(newValue, localDateSpinner.getValue());
+    }
+
+    @Test public void localDateSpinner_testSetMin_changesSpinnerValueWhenMinIsGreaterThanCurrentValue() {
+        LocalDate newValue = LocalDate.now();
+        localDateValueFactory.setValue(newValue);
+        assertEquals(newValue, localDateSpinner.getValue());
+
+        LocalDate twoDaysFromNow = nowPlusDays(2);
+        localDateValueFactory.setMin(twoDaysFromNow);
+        assertEquals(twoDaysFromNow, localDateSpinner.getValue());
+    }
+
+    @Test public void localDateSpinner_testSetMin_ensureThatMinCanNotExceedMax() {
+        assertEquals(nowPlusDays(-10), localDateValueFactory.getMin());
+        assertEquals(nowPlusDays(10), localDateValueFactory.getMax());
+        localDateValueFactory.setMin(nowPlusDays(20));
+        assertEquals(nowPlusDays(10), localDateValueFactory.getMin());
+    }
+
+    @Test public void localDateSpinner_testSetMin_ensureThatMinCanEqualMax() {
+        assertEquals(nowPlusDays(-10), localDateValueFactory.getMin());
+        assertEquals(nowPlusDays(10), localDateValueFactory.getMax());
+        localDateValueFactory.setMin(nowPlusDays(10));
+        assertEquals(nowPlusDays(10), localDateValueFactory.getMin());
+    }
+
+    @Test public void localDateSpinner_testSetMax_doesNotChangeSpinnerValueWhenMaxIsGreaterThanCurrentValue() {
+        LocalDate newValue = LocalDate.now();
+        localDateValueFactory.setValue(newValue);
+        assertEquals(newValue, localDateSpinner.getValue());
+        localDateValueFactory.setMax(nowPlusDays(2));
+        assertEquals(newValue, localDateSpinner.getValue());
+    }
+
+    @Test public void localDateSpinner_testSetMax_changesSpinnerValueWhenMaxIsLessThanCurrentValue() {
+        LocalDate newValue = nowPlusDays(4);
+        localDateValueFactory.setValue(newValue);
+        assertEquals(newValue, localDateSpinner.getValue());
+
+        LocalDate twoDays = nowPlusDays(2);
+        localDateValueFactory.setMax(twoDays);
+        assertEquals(twoDays, localDateSpinner.getValue());
+    }
+
+    @Test public void localDateSpinner_testSetMax_ensureThatMaxCanNotGoLessThanMin() {
+        localDateValueFactory.setMin(nowPlusDays(5));
+        assertEquals(nowPlusDays(5), localDateValueFactory.getMin());
+        assertEquals(nowPlusDays(10), localDateValueFactory.getMax());
+        localDateValueFactory.setMax(nowPlusDays(2));
+        assertEquals(nowPlusDays(5), localDateValueFactory.getMin());
+    }
+
+    @Test public void localDateSpinner_testSetMax_ensureThatMaxCanEqualMin() {
+        LocalDate twoDays = nowPlusDays(2);
+        localDateValueFactory.setMin(twoDays);
+        assertEquals(twoDays, localDateValueFactory.getMin());
+        assertEquals(nowPlusDays(10), localDateValueFactory.getMax());
+        localDateValueFactory.setMax(twoDays);
+        assertEquals(twoDays, localDateValueFactory.getMin());
+    }
+
+    @Test public void localDateSpinner_testSetValue_canNotExceedMax() {
+        assertEquals(nowPlusDays(-10), localDateValueFactory.getMin());
+        assertEquals(nowPlusDays(10), localDateValueFactory.getMax());
+        localDateValueFactory.setValue(nowPlusDays(50));
+        assertEquals(nowPlusDays(10), localDateSpinner.getValue());
+    }
+
+    @Test public void localDateSpinner_testSetValue_canNotExceedMin() {
+        assertEquals(nowPlusDays(-10), localDateValueFactory.getMin());
+        assertEquals(nowPlusDays(10), localDateValueFactory.getMax());
+        localDateValueFactory.setValue(nowPlusDays(-50));
+        assertEquals(nowPlusDays(-10), localDateSpinner.getValue());
+    }
+}
\ No newline at end of file