Reacting to typing
A button's handler runs when the button is clicked. Often a window should react while the user types: show how many characters are left, say whether a name is free, switch a button on once a field is filled in. For that, a program listens to the text itself.
The text as a property
A text field keeps its text in a property: an object that holds a value and tells others when the value changes. field.textProperty() returns it. Its method addListener takes a ChangeListener, an interface with one method, changed, which has three parameters: the property that changed, the old value and the new value. A lambda with three parameters needs parentheses around them, as module 37's lambdas with two did:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage stage) {
TextField word = new TextField();
word.textProperty().addListener((observable, oldValue, newValue) -> {
System.out.println("\"" + oldValue + "\" to \"" + newValue + "\"");
});
stage.setScene(new Scene(new VBox(word)));
stage.setTitle("Listening");
stage.show();
}
public static void main(String[] args) {
launch(Main.class);
}
}
After: Type "cat" in text field 1, then replace the text in text field 1 with "dog"
This window is wider than the screen: scroll it sideways, or read it as text below.
The window as text
Window "Listening"
VBox
TextField "dog""" to "c"
"c" to "ca"
"ca" to "cat"
"cat" to "dog"- Typing changes the text one character at a time, so typing
catcalls the listener three times. - The last event replaces the whole text at once, as pasting
dogovercatwould: that's one change, and one call. - The listener hears every change of the text, whoever makes it: the user's typing, and the program's own
setText,appendTextandclear. - It hears only real changes.
setTextwith the text that's already there changes nothing, and the listener isn't called. - The first parameter, here called
observable, is the property itself. The listeners in this module don't need it, but the lambda must have it, sincechangedhas three parameters.
A sign that counts its characters
A sign maker prints shop signs of up to 20 characters. The window shows how many are left while the customer types:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage stage) {
TextField text = new TextField();
text.setPromptText("Text of the sign");
Label left = new Label("20 characters left");
text.textProperty().addListener((observable, oldValue, newValue) -> {
left.setText((20 - newValue.length()) + " characters left");
});
stage.setScene(new Scene(new VBox(text, left)));
stage.setTitle("Sign maker");
stage.show();
}
public static void main(String[] args) {
launch(Main.class);
}
}
After: Type "FRESH BREAD" in text field 1
This window is wider than the screen: scroll it sideways, or read it as text below.
The window as text
Window "Sign maker"
VBox
TextField "FRESH BREAD", prompt "Text of the sign"
Label "9 characters left"The label is set from newValue, the text after the change. text.getText() would give the same text inside the listener, since the change has already happened when the listener runs.
Switching a button on and off
setDisable(true) disables a button: it's drawn pale, and clicking it does nothing. setDisable(false) enables it again. The sign maker's Order button should work only when there's a text, and one that fits. The button starts disabled, and the listener switches it on and off as the text changes:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage stage) {
TextField text = new TextField();
text.setPromptText("Text of the sign");
Label left = new Label("20 characters left");
Button order = new Button("Order");
order.setDisable(true);
text.textProperty().addListener((observable, oldValue, newValue) -> {
left.setText((20 - newValue.length()) + " characters left");
order.setDisable(newValue.isEmpty() || newValue.length() > 20);
});
order.setOnAction(event -> System.out.println("Ordered a sign: " + text.getText()));
stage.setScene(new Scene(new VBox(text, left, order)));
stage.setTitle("Sign maker");
stage.show();
}
public static void main(String[] args) {
launch(Main.class);
}
}
After: Click "Order", type "OPEN LATE ON FRIDAYS!" in text field 1, click "Order", replace the text in text field 1 with "OPEN LATE FRIDAYS", click "Order", then replace the text in text field 1 with ""
This window is wider than the screen: scroll it sideways, or read it as text below.
The window as text
Window "Sign maker"
VBox
TextField "", prompt "Text of the sign"
Label "20 characters left"
Button "Order" (disabled)Ordered a sign: OPEN LATE FRIDAYSThe first click comes before any typing, while the button is disabled, so nothing happens. The 21 characters of the first text are one too many, so the second click does nothing either. After the shorter text, the button is enabled, and the third click orders the sign. The last event clears the text, so the button is disabled again: in "The window as text", it's marked (disabled).
setDisable works on any part of a window: a disabled text field can't be typed in, and disabling a pane (a VBox, say) disables everything in it.