diff --git a/fx/docs/integer-spinners.md b/fx/docs/integer-spinners.md new file mode 100644 index 0000000..aaf6389 --- /dev/null +++ b/fx/docs/integer-spinners.md @@ -0,0 +1,14 @@ +# Bounded integer inputs + +`de.neitzel.fx.control.IntegerSpinners.nonNegative(min, max, value)` creates an +editable JavaFX spinner with inclusive nonnegative bounds. Construct and use it +on the JavaFX application thread. + +Every valid typed number immediately updates the value property, so a form can +save its model without waiting for Enter or a focus change. Non-digit input and +values above the maximum are rejected. Empty input and intermediate numbers +below the minimum retain the last committed value and revert when focus leaves +the editor. Increment/decrement use the normal JavaFX spinner behavior. + +The factory rejects negative/inverted bounds and out-of-range initial values. +Applications own their field labels, units, and domain-specific bounds. diff --git a/fx/src/main/java/de/neitzel/fx/control/IntegerSpinners.java b/fx/src/main/java/de/neitzel/fx/control/IntegerSpinners.java new file mode 100644 index 0000000..95637ad --- /dev/null +++ b/fx/src/main/java/de/neitzel/fx/control/IntegerSpinners.java @@ -0,0 +1,51 @@ +package de.neitzel.fx.control; + +import javafx.scene.control.Spinner; +import javafx.scene.control.TextFormatter; + +/** Factories for bounded integer inputs that commit valid typing immediately. */ +public final class IntegerSpinners { + private IntegerSpinners() { + } + + /** + * Creates a nonnegative bounded input. Valid typing immediately updates its value; + * incomplete values below the minimum revert to the committed value on focus loss. + * + * @param min inclusive nonnegative minimum + * @param max inclusive maximum + * @param value initial value + * @return an editable spinner + */ + public static Spinner nonNegative(int min, int max, int value) { + if (min < 0 || max < min || value < min || value > max) { + throw new IllegalArgumentException("Invalid nonnegative spinner bounds or value"); + } + Spinner spinner = new Spinner<>(min, max, value); + spinner.setEditable(true); + spinner.getEditor().setTextFormatter(new TextFormatter(change -> { + String text = change.getControlNewText(); + if (!text.matches("\\d{0,10}")) { + return null; + } + if (!text.isEmpty() && Long.parseLong(text) > max) { + return null; + } + return change; + })); + spinner.getEditor().textProperty().addListener((observable, old, text) -> { + if (!text.isEmpty()) { + int parsed = Integer.parseInt(text); + if (parsed >= min) { + spinner.getValueFactory().setValue(parsed); + } + } + }); + spinner.getEditor().focusedProperty().addListener((observable, old, focused) -> { + if (!focused) { + spinner.getEditor().setText(spinner.getValue().toString()); + } + }); + return spinner; + } +}