{"id":"gui-events-listeners","slug":"reacting-to-typing","title":"Reacting to typing","text":"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.\n\n### The text as a property\n\nA 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:\n\n```java\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        TextField word = new TextField();\n        word.textProperty().addListener((observable, oldValue, newValue) -> {\n            System.out.println(\"\\\"\" + oldValue + \"\\\" to \\\"\" + newValue + \"\\\"\");\n        });\n\n        stage.setScene(new Scene(new VBox(word)));\n        stage.setTitle(\"Listening\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n```\n\n```window\n{\"events\":[\"type TextField 1 \\\"cat\\\"\",\"set TextField 1 \\\"dog\\\"\"],\"window\":{\"windows\":[{\"title\":\"Listening\",\"scene\":{\"root\":{\"n\":1,\"type\":\"VBox\",\"children\":[{\"n\":2,\"type\":\"TextField\",\"text\":\"dog\"}]}}}],\"problems\":[]},\"outline\":\"Window \\\"Listening\\\"\\n  VBox\\n    TextField \\\"dog\\\"\"}\n```\n\n```output\n\"\" to \"c\"\n\"c\" to \"ca\"\n\"ca\" to \"cat\"\n\"cat\" to \"dog\"\n```\n\n- Typing changes the text one character at a time, so typing `cat` calls the listener three times.\n- The last event replaces the whole text at once, as pasting `dog` over `cat` would: that's one change, and one call.\n- The listener hears every change of the text, whoever makes it: the user's typing, and the program's own `setText`, `appendText` and `clear`.\n- It hears only real changes. `setText` with the text that's already there changes nothing, and the listener isn't called.\n- 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, since `changed` has three parameters.\n\n### A sign that counts its characters\n\nA sign maker prints shop signs of up to 20 characters. The window shows how many are left while the customer types:\n\n```java\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        TextField text = new TextField();\n        text.setPromptText(\"Text of the sign\");\n        Label left = new Label(\"20 characters left\");\n        text.textProperty().addListener((observable, oldValue, newValue) -> {\n            left.setText((20 - newValue.length()) + \" characters left\");\n        });\n\n        stage.setScene(new Scene(new VBox(text, left)));\n        stage.setTitle(\"Sign maker\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n```\n\n```window\n{\"events\":[\"type TextField 1 \\\"FRESH BREAD\\\"\"],\"window\":{\"windows\":[{\"title\":\"Sign maker\",\"scene\":{\"root\":{\"n\":1,\"type\":\"VBox\",\"children\":[{\"n\":2,\"type\":\"TextField\",\"text\":\"FRESH BREAD\",\"promptText\":\"Text of the sign\"},{\"n\":3,\"type\":\"Label\",\"text\":\"9 characters left\"}]}}}],\"problems\":[]},\"outline\":\"Window \\\"Sign maker\\\"\\n  VBox\\n    TextField \\\"FRESH BREAD\\\", prompt \\\"Text of the sign\\\"\\n    Label \\\"9 characters left\\\"\"}\n```\n\nThe 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.\n\n### Switching a button on and off\n\n`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:\n\n```java\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        TextField text = new TextField();\n        text.setPromptText(\"Text of the sign\");\n        Label left = new Label(\"20 characters left\");\n        Button order = new Button(\"Order\");\n        order.setDisable(true);\n        text.textProperty().addListener((observable, oldValue, newValue) -> {\n            left.setText((20 - newValue.length()) + \" characters left\");\n            order.setDisable(newValue.isEmpty() || newValue.length() > 20);\n        });\n        order.setOnAction(event -> System.out.println(\"Ordered a sign: \" + text.getText()));\n\n        stage.setScene(new Scene(new VBox(text, left, order)));\n        stage.setTitle(\"Sign maker\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n```\n\n```window\n{\"events\":[\"click Button \\\"Order\\\"\",\"type TextField 1 \\\"OPEN LATE ON FRIDAYS!\\\"\",\"click Button \\\"Order\\\"\",\"set TextField 1 \\\"OPEN LATE FRIDAYS\\\"\",\"click Button \\\"Order\\\"\",\"set TextField 1 \\\"\\\"\"],\"window\":{\"windows\":[{\"title\":\"Sign maker\",\"scene\":{\"root\":{\"n\":1,\"type\":\"VBox\",\"children\":[{\"n\":2,\"type\":\"TextField\",\"text\":\"\",\"promptText\":\"Text of the sign\"},{\"n\":3,\"type\":\"Label\",\"text\":\"20 characters left\"},{\"n\":4,\"type\":\"Button\",\"text\":\"Order\",\"disable\":true,\"onAction\":true}]}}}],\"problems\":[]},\"outline\":\"Window \\\"Sign maker\\\"\\n  VBox\\n    TextField \\\"\\\", prompt \\\"Text of the sign\\\"\\n    Label \\\"20 characters left\\\"\\n    Button \\\"Order\\\" (disabled)\"}\n```\n\n```output\nOrdered a sign: OPEN LATE FRIDAYS\n```\n\nThe 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)`.\n\n`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.\n","task":"A name badge printer shows a preview while the name is typed. Fill in the three blanks, so that the label shows `Badge: ` and the name in capital letters after every change of the text. The task card shows the window after typing `Ada Lovelace`.\n","kind":"fill","seed":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        TextField name = new TextField();\n        name.setPromptText(\"Name\");\n        Label preview = new Label(\"Badge: \");\n        name.[[textProperty]]().[[addListener]]((observable, oldValue, newValue) -> {\n            preview.setText(\"Badge: \" + [[newValue‖name.getText()]].toUpperCase());\n        });\n\n        stage.setScene(new Scene(new VBox(name, preview)));\n        stage.setTitle(\"Name badge\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","solution":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        TextField name = new TextField();\n        name.setPromptText(\"Name\");\n        Label preview = new Label(\"Badge: \");\n        name.textProperty().addListener((observable, oldValue, newValue) -> {\n            preview.setText(\"Badge: \" + newValue.toUpperCase());\n        });\n\n        stage.setScene(new Scene(new VBox(name, preview)));\n        stage.setTitle(\"Name badge\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","hints":["The text field's text is a property, returned by `textProperty()`, and a listener is added to it with `addListener`.","The last blank is the text after the change: the listener's third parameter, `newValue`."],"tests":[{"name":"Test 1","stdin":"","expect":"","hidden":false,"events":["type TextField 1 \"Ada Lovelace\""],"window":{"windows":[{"title":"Name badge","scene":{"root":{"n":1,"type":"VBox","children":[{"n":2,"type":"TextField","text":"Ada Lovelace","promptText":"Name"},{"n":3,"type":"Label","text":"Badge: ADA LOVELACE"}]}}}],"problems":[]},"outline":"Window \"Name badge\"\n  VBox\n    TextField \"Ada Lovelace\", prompt \"Name\"\n    Label \"Badge: ADA LOVELACE\""},{"name":"Test 2","stdin":"","expect":"","hidden":true,"events":["type TextField 1 \"grace\"","set TextField 1 \"Linus Torvalds\""],"window":{"windows":[{"title":"Name badge","scene":{"root":{"n":1,"type":"VBox","children":[{"n":2,"type":"TextField","text":"Linus Torvalds","promptText":"Name"},{"n":3,"type":"Label","text":"Badge: LINUS TORVALDS"}]}}}],"problems":[]},"outline":"Window \"Name badge\"\n  VBox\n    TextField \"Linus Torvalds\", prompt \"Name\"\n    Label \"Badge: LINUS TORVALDS\""},{"name":"Test 3","stdin":"","expect":"","hidden":true,"events":["type TextField 1 \"Ka\"","set TextField 1 \"\"","type TextField 1 \"Ju\""],"window":{"windows":[{"title":"Name badge","scene":{"root":{"n":1,"type":"VBox","children":[{"n":2,"type":"TextField","text":"Ju","promptText":"Name"},{"n":3,"type":"Label","text":"Badge: JU"}]}}}],"problems":[]},"outline":"Window \"Name badge\"\n  VBox\n    TextField \"Ju\", prompt \"Name\"\n    Label \"Badge: JU\""}],"require":[],"forbid":[],"more":[{"task":"A shop's checkout has a field for a discount code, with a listener that prints the length of the text before and after each change. `appendText` adds a whole text with one change, and `clear()` empties the field. What does the program print?\n","kind":"predict","seed":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        TextField code = new TextField();\n        code.textProperty().addListener((observable, oldValue, newValue) -> {\n            System.out.println(oldValue.length() + \" to \" + newValue.length());\n        });\n        code.setText(\"SPRING\");\n        code.appendText(\"25\");\n        code.setText(\"SPRING25\");\n        code.clear();\n        code.clear();\n        System.out.println(\"Done: \" + code.getText().length());\n\n        stage.setScene(new Scene(new VBox(code)));\n        stage.setTitle(\"Checkout\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","solution":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        TextField code = new TextField();\n        code.textProperty().addListener((observable, oldValue, newValue) -> {\n            System.out.println(oldValue.length() + \" to \" + newValue.length());\n        });\n        code.setText(\"SPRING\");\n        code.appendText(\"25\");\n        code.setText(\"SPRING25\");\n        code.clear();\n        code.clear();\n        System.out.println(\"Done: \" + code.getText().length());\n\n        stage.setScene(new Scene(new VBox(code)));\n        stage.setTitle(\"Checkout\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","hints":["The listener runs only when the text really changes: `setText(\"SPRING25\")` when the text already is `SPRING25` changes nothing, and neither does clearing an empty field.","`appendText(\"25\")` is a single change from 6 characters to 8, unlike typing, which adds one character at a time."],"tests":[{"name":"Output","stdin":"","expect":"0 to 6\n6 to 8\n8 to 0\nDone: 0","hidden":false}],"require":[],"forbid":[],"lines":["0 to 6","6 to 8","8 to 0","Done: 0"]},{"task":"A website's invite page asks for a friend's email address. The window is built already: a text field with the prompt text `Email address`, the label `Not an email address yet`, and the disabled button `Send invite`. Make it work:\n\n- After every change of the text, if it contains `@`, the label says `Ready to send` and the button is enabled; if not, the label says `Not an email address yet` and the button is disabled.\n- **Send invite** sets the label to `Invite sent to ` and the address, like `Invite sent to sam@example.com`.\n\nThe task card shows the window after typing `sam@example.com` and clicking Send invite.\n","kind":"code","seed":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        TextField email = new TextField();\n        email.setPromptText(\"Email address\");\n        Label status = new Label(\"Not an email address yet\");\n        Button send = new Button(\"Send invite\");\n        send.setDisable(true);\n\n        stage.setScene(new Scene(new VBox(email, status, send)));\n        stage.setTitle(\"Invite a friend\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","solution":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        TextField email = new TextField();\n        email.setPromptText(\"Email address\");\n        Label status = new Label(\"Not an email address yet\");\n        Button send = new Button(\"Send invite\");\n        send.setDisable(true);\n        email.textProperty().addListener((observable, oldValue, newValue) -> {\n            if (newValue.contains(\"@\")) {\n                status.setText(\"Ready to send\");\n                send.setDisable(false);\n            } else {\n                status.setText(\"Not an email address yet\");\n                send.setDisable(true);\n            }\n        });\n        send.setOnAction(event -> status.setText(\"Invite sent to \" + email.getText()));\n\n        stage.setScene(new Scene(new VBox(email, status, send)));\n        stage.setTitle(\"Invite a friend\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","hints":["Add a listener to `email.textProperty()`. In it, `newValue.contains(\"@\")` tells which of the two cases it is; each case sets the label's text and calls `send.setDisable(...)`.","The button's handler is one line: `send.setOnAction(event -> status.setText(\"Invite sent to \" + email.getText()));`."],"tests":[{"name":"Test 1","stdin":"","expect":"","hidden":false,"events":["type TextField 1 \"sam@example.com\"","click Button \"Send invite\""],"window":{"windows":[{"title":"Invite a friend","scene":{"root":{"n":1,"type":"VBox","children":[{"n":2,"type":"TextField","text":"sam@example.com","promptText":"Email address"},{"n":3,"type":"Label","text":"Invite sent to sam@example.com"},{"n":4,"type":"Button","text":"Send invite","onAction":true}]}}}],"problems":[]},"outline":"Window \"Invite a friend\"\n  VBox\n    TextField \"sam@example.com\", prompt \"Email address\"\n    Label \"Invite sent to sam@example.com\"\n    Button \"Send invite\""},{"name":"Test 2","stdin":"","expect":"","hidden":true,"events":["type TextField 1 \"kim\""],"window":{"windows":[{"title":"Invite a friend","scene":{"root":{"n":1,"type":"VBox","children":[{"n":2,"type":"TextField","text":"kim","promptText":"Email address"},{"n":3,"type":"Label","text":"Not an email address yet"},{"n":4,"type":"Button","text":"Send invite","disable":true,"onAction":true}]}}}],"problems":[]},"outline":"Window \"Invite a friend\"\n  VBox\n    TextField \"kim\", prompt \"Email address\"\n    Label \"Not an email address yet\"\n    Button \"Send invite\" (disabled)"},{"name":"Test 3","stdin":"","expect":"","hidden":true,"events":["type TextField 1 \"lee@\"","set TextField 1 \"lee\"","click Button \"Send invite\""],"window":{"windows":[{"title":"Invite a friend","scene":{"root":{"n":1,"type":"VBox","children":[{"n":2,"type":"TextField","text":"lee","promptText":"Email address"},{"n":3,"type":"Label","text":"Not an email address yet"},{"n":4,"type":"Button","text":"Send invite","disable":true,"onAction":true}]}}}],"problems":[]},"outline":"Window \"Invite a friend\"\n  VBox\n    TextField \"lee\", prompt \"Email address\"\n    Label \"Not an email address yet\"\n    Button \"Send invite\" (disabled)"},{"name":"Test 4","stdin":"","expect":"","hidden":true,"events":["set TextField 1 \"max@mail.org\"","click Button \"Send invite\"","set TextField 1 \"nope\""],"window":{"windows":[{"title":"Invite a friend","scene":{"root":{"n":1,"type":"VBox","children":[{"n":2,"type":"TextField","text":"nope","promptText":"Email address"},{"n":3,"type":"Label","text":"Not an email address yet"},{"n":4,"type":"Button","text":"Send invite","disable":true,"onAction":true}]}}}],"problems":[]},"outline":"Window \"Invite a friend\"\n  VBox\n    TextField \"nope\", prompt \"Email address\"\n    Label \"Not an email address yet\"\n    Button \"Send invite\" (disabled)"}],"require":[],"forbid":[]}]}