{"id":"gui-views-logic-and-views","slug":"logic-and-views","title":"Logic apart from the views","text":"Step 4's ToursView kept the number of free places itself, in an instance variable of the view. That's fine while one view is all that needs it. It gets awkward when a second view needs the same data, or when you want to check the rules of the application without clicking through a window.\n\nModule 49 moved the work of a window into a plain class that the handlers call, with no JavaFX in it. With several views, that class becomes more: it's where the application's data and rules, its **logic**, live, and every view gets the same object of it. The views only **show** what the logic says, and **forward** what the user does to it. Module 25 split text user interfaces the same way: the logic in one class, the reading and printing in another.\n\n### The logic first\n\nA savings jar for a goal, such as a bike for 120 euros:\n\n```java\npublic class Main {\n    public static void main(String[] args) {\n        SavingsJar jar = new SavingsJar(\"a bike\", 120);\n        System.out.println(jar.putIn(50));\n        System.out.println(jar.putIn(-5));\n        System.out.println(jar);\n        System.out.println(jar.missing());\n        jar.putIn(80);\n        System.out.println(jar);\n    }\n}\n// ==== SavingsJar.java ====\npublic class SavingsJar {\n    private String goal;\n    private int target;\n    private int saved;\n\n    public SavingsJar(String goal, int target) {\n        this.goal = goal;\n        this.target = target;\n    }\n\n    public boolean putIn(int amount) {\n        if (amount < 1) {\n            return false;\n        }\n        this.saved += amount;\n        return true;\n    }\n\n    public int getSaved() {\n        return this.saved;\n    }\n\n    public int missing() {\n        return Math.max(0, this.target - this.saved);\n    }\n\n    @Override\n    public String toString() {\n        if (this.missing() == 0) {\n            return \"Goal reached: \" + this.goal + \"!\";\n        }\n        return \"Saved \" + this.saved + \" of \" + this.target + \" for \" + this.goal;\n    }\n}\n```\n\n```output\ntrue\nfalse\nSaved 50 of 120 for a bike\n70\nGoal reached: a bike!\n```\n\nThis program has no window at all, and it can try every rule: a negative amount is refused, the missing amount never goes below 0, and the text changes when the goal is reached. The challenges of this step check logic classes the same way, by calling their methods.\n\n### Two views, one jar\n\nNow the window. The views get the jar through their constructors, and both get the **same** jar object:\n\n```java\nimport javafx.application.Application;\nimport javafx.geometry.Insets;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Button;\nimport javafx.scene.layout.BorderPane;\nimport javafx.scene.layout.HBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        SavingsJar jar = new SavingsJar(\"a bike\", 120);\n        DepositView deposit = new DepositView(jar);\n        ProgressView progress = new ProgressView(jar);\n\n        Button toDeposit = new Button(\"Deposit\");\n        Button toProgress = new Button(\"Progress\");\n        HBox menu = new HBox(10, toDeposit, toProgress);\n        menu.setPadding(new Insets(10));\n\n        BorderPane frame = new BorderPane();\n        frame.setTop(menu);\n        frame.setCenter(deposit.getView());\n        toDeposit.setOnAction(event -> frame.setCenter(deposit.getView()));\n        toProgress.setOnAction(event -> frame.setCenter(progress.getView()));\n\n        stage.setScene(new Scene(frame));\n        stage.setTitle(\"Savings jar\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n// ==== DepositView.java ====\nimport javafx.geometry.Insets;\nimport javafx.scene.Parent;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\n\npublic class DepositView {\n    private SavingsJar jar;\n    private VBox view;\n\n    public DepositView(SavingsJar jar) {\n        this.jar = jar;\n        TextField amount = new TextField();\n        amount.setPromptText(\"Euros\");\n        Button putIn = new Button(\"Put in\");\n        Label message = new Label(\"\");\n        putIn.setOnAction(event -> {\n            try {\n                if (this.jar.putIn(Integer.valueOf(amount.getText()))) {\n                    message.setText(\"Thanks! \" + this.jar.getSaved() + \" euros saved so far.\");\n                    amount.clear();\n                } else {\n                    message.setText(\"Put in at least 1 euro.\");\n                }\n            } catch (NumberFormatException e) {\n                message.setText(\"Type a whole number of euros.\");\n            }\n        });\n        this.view = new VBox(8, amount, putIn, message);\n        this.view.setPadding(new Insets(10));\n    }\n\n    public Parent getView() {\n        return this.view;\n    }\n}\n// ==== ProgressView.java ====\nimport javafx.geometry.Insets;\nimport javafx.scene.Parent;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.VBox;\n\npublic class ProgressView {\n    private SavingsJar jar;\n\n    public ProgressView(SavingsJar jar) {\n        this.jar = jar;\n    }\n\n    public Parent getView() {\n        VBox view = new VBox(8, new Label(this.jar.toString()), new Label(\"Still missing: \" + this.jar.missing() + \" euros\"));\n        view.setPadding(new Insets(10));\n        return view;\n    }\n}\n// ==== SavingsJar.java ====\npublic class SavingsJar {\n    private String goal;\n    private int target;\n    private int saved;\n\n    public SavingsJar(String goal, int target) {\n        this.goal = goal;\n        this.target = target;\n    }\n\n    public boolean putIn(int amount) {\n        if (amount < 1) {\n            return false;\n        }\n        this.saved += amount;\n        return true;\n    }\n\n    public int getSaved() {\n        return this.saved;\n    }\n\n    public int missing() {\n        return Math.max(0, this.target - this.saved);\n    }\n\n    @Override\n    public String toString() {\n        if (this.missing() == 0) {\n            return \"Goal reached: \" + this.goal + \"!\";\n        }\n        return \"Saved \" + this.saved + \" of \" + this.target + \" for \" + this.goal;\n    }\n}\n```\n\n```window\n{\"events\":[\"type TextField 1 \\\"50\\\"\",\"click Button \\\"Put in\\\"\",\"type TextField 1 \\\"15\\\"\",\"click Button \\\"Put in\\\"\",\"click Button \\\"Progress\\\"\"],\"window\":{\"windows\":[{\"title\":\"Savings jar\",\"scene\":{\"root\":{\"n\":1,\"type\":\"BorderPane\",\"top\":{\"n\":2,\"type\":\"HBox\",\"padding\":[10,10,10,10],\"spacing\":10,\"children\":[{\"n\":3,\"type\":\"Button\",\"text\":\"Deposit\",\"onAction\":true},{\"n\":4,\"type\":\"Button\",\"text\":\"Progress\",\"onAction\":true}]},\"center\":{\"n\":5,\"type\":\"VBox\",\"padding\":[10,10,10,10],\"spacing\":8,\"children\":[{\"n\":6,\"type\":\"Label\",\"text\":\"Saved 65 of 120 for a bike\"},{\"n\":7,\"type\":\"Label\",\"text\":\"Still missing: 55 euros\"}]}}}}],\"problems\":[]},\"outline\":\"Window \\\"Savings jar\\\"\\n  BorderPane\\n    top: HBox spacing 10, padding 10\\n      Button \\\"Deposit\\\"\\n      Button \\\"Progress\\\"\\n    center: VBox spacing 8, padding 10\\n      Label \\\"Saved 65 of 120 for a bike\\\"\\n      Label \\\"Still missing: 55 euros\\\"\"}\n```\n\nHere's who does what:\n\n\n<figure class=\"uml\"><div class=\"uml-scroll\" tabindex=\"0\" role=\"group\" aria-label=\"Class diagram of Main, DepositView, ProgressView and SavingsJar\">\n<svg class=\"uml-svg\" viewBox=\"0 0 385 416\" width=\"385\" height=\"416\" style=\"width:24.07rem;min-width:22.06rem;aspect-ratio:385/416\" font-size=\"12\" aria-hidden=\"true\" focusable=\"false\"><path class=\"u-edge\" fill=\"none\" d=\"M67 30L67 100L6 100L6 314L127.5 314L127.5 338\"/><path class=\"u-edge\" fill=\"none\" d=\"M99 18L147 18\"/><path class=\"u-edge\" fill=\"none\" d=\"M202.5 338L202.5 314L165 314L165 292\"/><path class=\"u-edge\" fill=\"none\" d=\"M214 78L214 124\"/><path class=\"u-edge\" fill=\"none\" d=\"M122 328L127.5 338L133 328\"/><path class=\"u-edge\" fill=\"none\" d=\"M137 23.5L147 18L137 12.5\"/><path class=\"u-edge\" fill=\"none\" d=\"M170.5 302L165 292L159.5 302\"/><path class=\"u-edge\" fill=\"none\" d=\"M208.5 114L214 124L219.5 114\"/><rect class=\"u-bg\" fill=\"none\" x=\"35\" y=\"6\" width=\"64\" height=\"24\"/><rect class=\"u-head\" fill=\"none\" x=\"35\" y=\"6\" width=\"64\" height=\"24\"/><text x=\"67\" y=\"22\" text-anchor=\"middle\" class=\"u-name\">Main</text><rect class=\"u-frame\" fill=\"none\" x=\"35\" y=\"6\" width=\"64\" height=\"24\"/><rect class=\"u-bg\" fill=\"none\" x=\"52.5\" y=\"338\" width=\"225\" height=\"72\"/><rect class=\"u-head\" fill=\"none\" x=\"52.5\" y=\"338\" width=\"225\" height=\"24\"/><text x=\"165\" y=\"354\" text-anchor=\"middle\" class=\"u-name\">DepositView</text><text x=\"60.5\" y=\"386\">+DepositView(jar: SavingsJar)</text><text x=\"60.5\" y=\"402\">+getView(): Parent</text><path class=\"u-sep\" fill=\"none\" d=\"M52.5 362h225M52.5 370h225\"/><rect class=\"u-frame\" fill=\"none\" x=\"52.5\" y=\"338\" width=\"225\" height=\"72\"/><rect class=\"u-bg\" fill=\"none\" x=\"147\" y=\"6\" width=\"232\" height=\"72\"/><rect class=\"u-head\" fill=\"none\" x=\"147\" y=\"6\" width=\"232\" height=\"24\"/><text x=\"263\" y=\"22\" text-anchor=\"middle\" class=\"u-name\">ProgressView</text><text x=\"155\" y=\"54\">+ProgressView(jar: SavingsJar)</text><text x=\"155\" y=\"70\">+getView(): Parent</text><path class=\"u-sep\" fill=\"none\" d=\"M147 30h232M147 38h232\"/><rect class=\"u-frame\" fill=\"none\" x=\"147\" y=\"6\" width=\"232\" height=\"72\"/><rect class=\"u-bg\" fill=\"none\" x=\"20\" y=\"124\" width=\"290\" height=\"168\"/><rect class=\"u-head\" fill=\"none\" x=\"20\" y=\"124\" width=\"290\" height=\"24\"/><text x=\"165\" y=\"140\" text-anchor=\"middle\" class=\"u-name\">SavingsJar</text><text x=\"28\" y=\"164\">-goal: String</text><text x=\"28\" y=\"180\">-target: int</text><text x=\"28\" y=\"196\">-saved: int</text><text x=\"28\" y=\"220\">+SavingsJar(goal: String, target: int)</text><text x=\"28\" y=\"236\">+putIn(amount: int): boolean</text><text x=\"28\" y=\"252\">+getSaved(): int</text><text x=\"28\" y=\"268\">+missing(): int</text><text x=\"28\" y=\"284\">+toString(): String</text><path class=\"u-sep\" fill=\"none\" d=\"M20 148h290M20 204h290\"/><rect class=\"u-frame\" fill=\"none\" x=\"20\" y=\"124\" width=\"290\" height=\"168\"/></svg>\n</div><details class=\"uml-text\"><summary>The diagram as text</summary><ul><li><strong>Main</strong>, a class</li><li><strong>DepositView</strong>, a class<ul><li>public constructor <code>DepositView(jar: SavingsJar)</code></li><li>public method <code>getView(): Parent</code></li></ul></li><li><strong>ProgressView</strong>, a class<ul><li>public constructor <code>ProgressView(jar: SavingsJar)</code></li><li>public method <code>getView(): Parent</code></li></ul></li><li><strong>SavingsJar</strong>, a class<ul><li>private field <code>goal: String</code></li><li>private field <code>target: int</code></li><li>private field <code>saved: int</code></li><li>public constructor <code>SavingsJar(goal: String, target: int)</code></li><li>public method <code>putIn(amount: int): boolean</code></li><li>public method <code>getSaved(): int</code></li><li>public method <code>missing(): int</code></li><li>public method <code>toString(): String</code></li></ul></li></ul><p>Connections:</p><ul><li>Each Main has one DepositView object.</li><li>Each Main has one ProgressView object.</li><li>Each DepositView has one SavingsJar object.</li><li>Each ProgressView has one SavingsJar object.</li></ul></details></figure>\n\n\n- **SavingsJar** holds the data and the rules: how much is saved, that an amount must be at least 1, when the goal is reached, and how the progress reads.\n- **DepositView** turns what the user typed into a number (a question of the user interface: a text field holds text), forwards it to `putIn`, and shows a message from the answer. It doesn't decide which amounts are allowed: the jar does.\n- **ProgressView** only shows what the jar says. It builds a new VBox at each call of `getView()`, so each visit reads the jar again and shows the latest amounts. That's step 4's first way to build a view, and here it's the right one: the data that lasts is in the jar, not in the view.\n- **Main** creates the jar and the views, gives each view the jar, and switches between them.\n\nSince both views refer to the same SavingsJar object, a deposit made in one view is what the other one shows. Had each view made a jar of its own, Progress would always say 0.\n\nThe split pays off in other ways too. A rule changes in one place: if the jar should refuse amounts over 500, only `putIn` changes, and every view follows. A new view, such as a list of deposits, needs no change in the views that exist. And the rules can be checked without a window, as the program at the top of the step did.\n","task":"A travel agency books seats on its excursions. Write the logic, the class `Excursion` in `Excursion.java` (no JavaFX in it):\n\n- a constructor `Excursion(String name, int seats)`: at first every seat is free\n- `book(int count)`: when `count` is at least 1 and at most the number of free seats, books that many seats and returns `true`; otherwise books nothing and returns `false`\n- `free()`: returns the number of free seats\n- `toString()`: returns `Island tour: 37 of 40 seats free` (with the name, the free seats and all seats), or `Island tour: sold out` when no seat is free\n\n`Main.java` is a test drive in the console. The check runs:\n\n```java\nExcursion trip = new Excursion(\"Island tour\", 40);\nSystem.out.println(trip.book(3));\nSystem.out.println(trip.book(0));\nSystem.out.println(trip);\n```\n\nand expects:\n\n```\ntrue\nfalse\nIsland tour: 37 of 40 seats free\n```\n","kind":"code","seed":"// ==== Excursion.java ====\npublic class Excursion {\n\n}\n\n// ==== Main.java ====\npublic class Main {\n    public static void main(String[] args) {\n        Excursion trip = new Excursion(\"Island tour\", 40);\n        System.out.println(trip.book(3));\n        System.out.println(trip.book(0));\n        System.out.println(trip);\n    }\n}\n","solution":"// ==== Excursion.java ====\npublic class Excursion {\n    private String name;\n    private int seats;\n    private int booked;\n\n    public Excursion(String name, int seats) {\n        this.name = name;\n        this.seats = seats;\n    }\n\n    public boolean book(int count) {\n        if (count < 1 || count > this.free()) {\n            return false;\n        }\n        this.booked += count;\n        return true;\n    }\n\n    public int free() {\n        return this.seats - this.booked;\n    }\n\n    @Override\n    public String toString() {\n        if (this.free() == 0) {\n            return this.name + \": sold out\";\n        }\n        return this.name + \": \" + this.free() + \" of \" + this.seats + \" seats free\";\n    }\n}\n\n// ==== Main.java ====\npublic class Main {\n    public static void main(String[] args) {\n        Excursion trip = new Excursion(\"Island tour\", 40);\n        System.out.println(trip.book(3));\n        System.out.println(trip.book(0));\n        System.out.println(trip);\n    }\n}\n","hints":["Keep the name, the number of seats and the number booked in instance variables. `free()` is then `this.seats - this.booked`.","`book` first checks `count < 1 || count > this.free()` and returns `false` for those; otherwise it adds `count` to the booked seats and returns `true`. `toString` checks `this.free() == 0` for the sold-out text."],"tests":[{"name":"Test 1","stdin":"","call":"Excursion trip = new Excursion(\"Island tour\", 40);\nSystem.out.println(trip.book(3));\nSystem.out.println(trip.book(0));\nSystem.out.println(trip);\n","expect":"true\nfalse\nIsland tour: 37 of 40 seats free","hidden":false},{"name":"Test 2","stdin":"","call":"Excursion trip = new Excursion(\"Lake cruise\", 10);\nSystem.out.println(trip.free());\nSystem.out.println(trip.book(11));\nSystem.out.println(trip.book(-2));\nSystem.out.println(trip.book(4));\nSystem.out.println(trip.book(6));\nSystem.out.println(trip.free());\nSystem.out.println(trip);\nSystem.out.println(trip.book(1));\n","expect":"10\nfalse\nfalse\ntrue\ntrue\n0\nLake cruise: sold out\nfalse","hidden":true},{"name":"Test 3","stdin":"","call":"Excursion trip = new Excursion(\"Bird walk\", 3);\nSystem.out.println(trip);\nSystem.out.println(trip.book(1));\nSystem.out.println(trip.book(3));\nSystem.out.println(trip);\n","expect":"Bird walk: 3 of 3 seats free\ntrue\nfalse\nBird walk: 2 of 3 seats free","hidden":true}],"require":[],"forbid":[{"pattern":"\\bjavafx\\b","message":"Keep the logic class free of JavaFX: Excursion holds the data and the rules, and the views come later."}],"more":[{"task":"The agency's booking view. `Excursion` (the class from the first task) and `Main` are ready. Write the class `BookingView` in `BookingView.java`:\n\n- a constructor `BookingView(Excursion excursion)`\n- `getView()`, which returns a Parent: a VBox with the spacing 8 and no padding, holding a label with the excursion's `toString()`, a text field with the prompt text `Seats`, the button `Book` and an empty label for messages. Make them once, in the constructor.\n\n**Book** reads the number of seats from the field, and:\n\n- when the text isn't a whole number, the message is `Type the number of seats in digits.`\n- when `book` returns `true`, the message is `Seats booked: 3.` (with the number), and the first label shows the excursion's new `toString()`\n- when `book` returns `false`, the message is `Can't book that. Free seats: 37.` (with the number of free seats)\n\nBook never clears the field: it keeps the text that was typed, whatever the outcome.\n\nThe rules about which numbers can be booked are in Excursion: the view only forwards the number. The task card shows the window after typing `3` and clicking Book.\n","kind":"code","seed":"// ==== BookingView.java ====\npublic class BookingView {\n\n}\n\n// ==== Main.java ====\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        Excursion excursion = new Excursion(\"Island tour\", 40);\n        BookingView booking = new BookingView(excursion);\n\n        stage.setScene(new Scene(booking.getView()));\n        stage.setTitle(\"Excursions\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n\n// ==== Excursion.java ====\npublic class Excursion {\n    private String name;\n    private int seats;\n    private int booked;\n\n    public Excursion(String name, int seats) {\n        this.name = name;\n        this.seats = seats;\n    }\n\n    public boolean book(int count) {\n        if (count < 1 || count > this.free()) {\n            return false;\n        }\n        this.booked += count;\n        return true;\n    }\n\n    public int free() {\n        return this.seats - this.booked;\n    }\n\n    @Override\n    public String toString() {\n        if (this.free() == 0) {\n            return this.name + \": sold out\";\n        }\n        return this.name + \": \" + this.free() + \" of \" + this.seats + \" seats free\";\n    }\n}\n","solution":"// ==== BookingView.java ====\nimport javafx.scene.Parent;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\n\npublic class BookingView {\n    private Excursion excursion;\n    private VBox view;\n\n    public BookingView(Excursion excursion) {\n        this.excursion = excursion;\n        Label info = new Label(excursion.toString());\n        TextField seats = new TextField();\n        seats.setPromptText(\"Seats\");\n        Button book = new Button(\"Book\");\n        Label message = new Label(\"\");\n        book.setOnAction(event -> {\n            try {\n                int count = Integer.valueOf(seats.getText());\n                if (this.excursion.book(count)) {\n                    message.setText(\"Seats booked: \" + count + \".\");\n                    info.setText(this.excursion.toString());\n                } else {\n                    message.setText(\"Can't book that. Free seats: \" + this.excursion.free() + \".\");\n                }\n            } catch (NumberFormatException e) {\n                message.setText(\"Type the number of seats in digits.\");\n            }\n        });\n        this.view = new VBox(8, info, seats, book, message);\n    }\n\n    public Parent getView() {\n        return this.view;\n    }\n}\n\n// ==== Main.java ====\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        Excursion excursion = new Excursion(\"Island tour\", 40);\n        BookingView booking = new BookingView(excursion);\n\n        stage.setScene(new Scene(booking.getView()));\n        stage.setTitle(\"Excursions\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n\n// ==== Excursion.java ====\npublic class Excursion {\n    private String name;\n    private int seats;\n    private int booked;\n\n    public Excursion(String name, int seats) {\n        this.name = name;\n        this.seats = seats;\n    }\n\n    public boolean book(int count) {\n        if (count < 1 || count > this.free()) {\n            return false;\n        }\n        this.booked += count;\n        return true;\n    }\n\n    public int free() {\n        return this.seats - this.booked;\n    }\n\n    @Override\n    public String toString() {\n        if (this.free() == 0) {\n            return this.name + \": sold out\";\n        }\n        return this.name + \": \" + this.free() + \" of \" + this.seats + \" seats free\";\n    }\n}\n","hints":["Keep the excursion in an instance variable and make the four nodes in the constructor. The first label starts with `excursion.toString()`.","In Book's handler, `int count = Integer.valueOf(seats.getText());` goes in a `try`, with `catch (NumberFormatException e)` for the first message. Then `if (this.excursion.book(count))` decides between the other two messages."],"tests":[{"name":"Test 1","stdin":"","expect":"","hidden":false,"events":["type TextField 1 \"3\"","click Button \"Book\""],"window":{"windows":[{"title":"Excursions","scene":{"root":{"n":1,"type":"VBox","spacing":8,"children":[{"n":2,"type":"Label","text":"Island tour: 37 of 40 seats free"},{"n":3,"type":"TextField","text":"3","promptText":"Seats"},{"n":4,"type":"Button","text":"Book","onAction":true},{"n":5,"type":"Label","text":"Seats booked: 3."}]}}}],"problems":[]},"outline":"Window \"Excursions\"\n  VBox spacing 8\n    Label \"Island tour: 37 of 40 seats free\"\n    TextField \"3\", prompt \"Seats\"\n    Button \"Book\"\n    Label \"Seats booked: 3.\""},{"name":"Test 2","stdin":"","expect":"","hidden":true,"events":[],"window":{"windows":[{"title":"Excursions","scene":{"root":{"n":1,"type":"VBox","spacing":8,"children":[{"n":2,"type":"Label","text":"Island tour: 40 of 40 seats free"},{"n":3,"type":"TextField","text":"","promptText":"Seats"},{"n":4,"type":"Button","text":"Book","onAction":true},{"n":5,"type":"Label","text":""}]}}}],"problems":[]},"outline":"Window \"Excursions\"\n  VBox spacing 8\n    Label \"Island tour: 40 of 40 seats free\"\n    TextField \"\", prompt \"Seats\"\n    Button \"Book\"\n    Label \"\""},{"name":"Test 3","stdin":"","expect":"","hidden":true,"events":["type TextField 1 \"three\"","click Button \"Book\""],"window":{"windows":[{"title":"Excursions","scene":{"root":{"n":1,"type":"VBox","spacing":8,"children":[{"n":2,"type":"Label","text":"Island tour: 40 of 40 seats free"},{"n":3,"type":"TextField","text":"three","promptText":"Seats"},{"n":4,"type":"Button","text":"Book","onAction":true},{"n":5,"type":"Label","text":"Type the number of seats in digits."}]}}}],"problems":[]},"outline":"Window \"Excursions\"\n  VBox spacing 8\n    Label \"Island tour: 40 of 40 seats free\"\n    TextField \"three\", prompt \"Seats\"\n    Button \"Book\"\n    Label \"Type the number of seats in digits.\""},{"name":"Test 4","stdin":"","expect":"","hidden":true,"events":["type TextField 1 \"38\"","click Button \"Book\"","set TextField 1 \"36\"","click Button \"Book\"","set TextField 1 \"5\"","click Button \"Book\""],"window":{"windows":[{"title":"Excursions","scene":{"root":{"n":1,"type":"VBox","spacing":8,"children":[{"n":2,"type":"Label","text":"Island tour: 2 of 40 seats free"},{"n":3,"type":"TextField","text":"5","promptText":"Seats"},{"n":4,"type":"Button","text":"Book","onAction":true},{"n":5,"type":"Label","text":"Can't book that. Free seats: 2."}]}}}],"problems":[]},"outline":"Window \"Excursions\"\n  VBox spacing 8\n    Label \"Island tour: 2 of 40 seats free\"\n    TextField \"5\", prompt \"Seats\"\n    Button \"Book\"\n    Label \"Can't book that. Free seats: 2.\""},{"name":"Test 5","stdin":"","expect":"","hidden":true,"events":["type TextField 1 \"0\"","click Button \"Book\"","set TextField 1 \"40\"","click Button \"Book\""],"window":{"windows":[{"title":"Excursions","scene":{"root":{"n":1,"type":"VBox","spacing":8,"children":[{"n":2,"type":"Label","text":"Island tour: sold out"},{"n":3,"type":"TextField","text":"40","promptText":"Seats"},{"n":4,"type":"Button","text":"Book","onAction":true},{"n":5,"type":"Label","text":"Seats booked: 40."}]}}}],"problems":[]},"outline":"Window \"Excursions\"\n  VBox spacing 8\n    Label \"Island tour: sold out\"\n    TextField \"40\", prompt \"Seats\"\n    Button \"Book\"\n    Label \"Seats booked: 40.\""}],"require":[],"forbid":[]},{"task":"The agency runs two excursions. `Excursion` and `BookingView` (from the tasks before) are ready. Write `start` in `Main`:\n\n- two Excursion objects: `Island tour` with 40 seats and `Lake cruise` with 25 seats, and a BookingView for each, made once\n- a BorderPane whose top is an HBox with the spacing 10 and the padding 10 on every side, holding the buttons `Island tour` and `Lake cruise`\n- each button puts the BookingView of its excursion in the center; at the start, the center shows the Island tour's\n- the window's title is `Excursions`\n\nThe task card shows the window after booking 2 seats for the Lake cruise.\n","kind":"code","seed":"// ==== Main.java ====\nimport javafx.application.Application;\nimport javafx.geometry.Insets;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Button;\nimport javafx.scene.layout.BorderPane;\nimport javafx.scene.layout.HBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n\n// ==== BookingView.java ====\nimport javafx.scene.Parent;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\n\npublic class BookingView {\n    private Excursion excursion;\n    private VBox view;\n\n    public BookingView(Excursion excursion) {\n        this.excursion = excursion;\n        Label info = new Label(excursion.toString());\n        TextField seats = new TextField();\n        seats.setPromptText(\"Seats\");\n        Button book = new Button(\"Book\");\n        Label message = new Label(\"\");\n        book.setOnAction(event -> {\n            try {\n                int count = Integer.valueOf(seats.getText());\n                if (this.excursion.book(count)) {\n                    message.setText(\"Seats booked: \" + count + \".\");\n                    info.setText(this.excursion.toString());\n                } else {\n                    message.setText(\"Can't book that. Free seats: \" + this.excursion.free() + \".\");\n                }\n            } catch (NumberFormatException e) {\n                message.setText(\"Type the number of seats in digits.\");\n            }\n        });\n        this.view = new VBox(8, info, seats, book, message);\n    }\n\n    public Parent getView() {\n        return this.view;\n    }\n}\n\n// ==== Excursion.java ====\npublic class Excursion {\n    private String name;\n    private int seats;\n    private int booked;\n\n    public Excursion(String name, int seats) {\n        this.name = name;\n        this.seats = seats;\n    }\n\n    public boolean book(int count) {\n        if (count < 1 || count > this.free()) {\n            return false;\n        }\n        this.booked += count;\n        return true;\n    }\n\n    public int free() {\n        return this.seats - this.booked;\n    }\n\n    @Override\n    public String toString() {\n        if (this.free() == 0) {\n            return this.name + \": sold out\";\n        }\n        return this.name + \": \" + this.free() + \" of \" + this.seats + \" seats free\";\n    }\n}\n","solution":"// ==== Main.java ====\nimport javafx.application.Application;\nimport javafx.geometry.Insets;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Button;\nimport javafx.scene.layout.BorderPane;\nimport javafx.scene.layout.HBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        BookingView island = new BookingView(new Excursion(\"Island tour\", 40));\n        BookingView lake = new BookingView(new Excursion(\"Lake cruise\", 25));\n\n        Button toIsland = new Button(\"Island tour\");\n        Button toLake = new Button(\"Lake cruise\");\n        HBox menu = new HBox(10, toIsland, toLake);\n        menu.setPadding(new Insets(10));\n\n        BorderPane frame = new BorderPane();\n        frame.setTop(menu);\n        frame.setCenter(island.getView());\n        toIsland.setOnAction(event -> frame.setCenter(island.getView()));\n        toLake.setOnAction(event -> frame.setCenter(lake.getView()));\n\n        stage.setScene(new Scene(frame));\n        stage.setTitle(\"Excursions\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n\n// ==== BookingView.java ====\nimport javafx.scene.Parent;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.Label;\nimport javafx.scene.control.TextField;\nimport javafx.scene.layout.VBox;\n\npublic class BookingView {\n    private Excursion excursion;\n    private VBox view;\n\n    public BookingView(Excursion excursion) {\n        this.excursion = excursion;\n        Label info = new Label(excursion.toString());\n        TextField seats = new TextField();\n        seats.setPromptText(\"Seats\");\n        Button book = new Button(\"Book\");\n        Label message = new Label(\"\");\n        book.setOnAction(event -> {\n            try {\n                int count = Integer.valueOf(seats.getText());\n                if (this.excursion.book(count)) {\n                    message.setText(\"Seats booked: \" + count + \".\");\n                    info.setText(this.excursion.toString());\n                } else {\n                    message.setText(\"Can't book that. Free seats: \" + this.excursion.free() + \".\");\n                }\n            } catch (NumberFormatException e) {\n                message.setText(\"Type the number of seats in digits.\");\n            }\n        });\n        this.view = new VBox(8, info, seats, book, message);\n    }\n\n    public Parent getView() {\n        return this.view;\n    }\n}\n\n// ==== Excursion.java ====\npublic class Excursion {\n    private String name;\n    private int seats;\n    private int booked;\n\n    public Excursion(String name, int seats) {\n        this.name = name;\n        this.seats = seats;\n    }\n\n    public boolean book(int count) {\n        if (count < 1 || count > this.free()) {\n            return false;\n        }\n        this.booked += count;\n        return true;\n    }\n\n    public int free() {\n        return this.seats - this.booked;\n    }\n\n    @Override\n    public String toString() {\n        if (this.free() == 0) {\n            return this.name + \": sold out\";\n        }\n        return this.name + \": \" + this.free() + \" of \" + this.seats + \" seats free\";\n    }\n}\n","hints":["Each excursion needs an Excursion object of its own, and each BookingView gets its excursion's: `BookingView island = new BookingView(new Excursion(\"Island tour\", 40));`, and the same for the Lake cruise.","The rest is step 3's frame: an HBox menu with `setPadding(new Insets(10))` in the top region, `frame.setCenter(island.getView())` at the start, and a handler on each button that puts its view in the center."],"tests":[{"name":"Test 1","stdin":"","expect":"","hidden":false,"events":["click Button \"Lake cruise\"","type TextField 1 \"2\"","click Button \"Book\""],"window":{"windows":[{"title":"Excursions","scene":{"root":{"n":1,"type":"BorderPane","top":{"n":2,"type":"HBox","padding":[10,10,10,10],"spacing":10,"children":[{"n":3,"type":"Button","text":"Island tour","onAction":true},{"n":4,"type":"Button","text":"Lake cruise","onAction":true}]},"center":{"n":5,"type":"VBox","spacing":8,"children":[{"n":6,"type":"Label","text":"Lake cruise: 23 of 25 seats free"},{"n":7,"type":"TextField","text":"2","promptText":"Seats"},{"n":8,"type":"Button","text":"Book","onAction":true},{"n":9,"type":"Label","text":"Seats booked: 2."}]}}}}],"problems":[]},"outline":"Window \"Excursions\"\n  BorderPane\n    top: HBox spacing 10, padding 10\n      Button \"Island tour\"\n      Button \"Lake cruise\"\n    center: VBox spacing 8\n      Label \"Lake cruise: 23 of 25 seats free\"\n      TextField \"2\", prompt \"Seats\"\n      Button \"Book\"\n      Label \"Seats booked: 2.\""},{"name":"Test 2","stdin":"","expect":"","hidden":true,"events":[],"window":{"windows":[{"title":"Excursions","scene":{"root":{"n":1,"type":"BorderPane","top":{"n":2,"type":"HBox","padding":[10,10,10,10],"spacing":10,"children":[{"n":3,"type":"Button","text":"Island tour","onAction":true},{"n":4,"type":"Button","text":"Lake cruise","onAction":true}]},"center":{"n":5,"type":"VBox","spacing":8,"children":[{"n":6,"type":"Label","text":"Island tour: 40 of 40 seats free"},{"n":7,"type":"TextField","text":"","promptText":"Seats"},{"n":8,"type":"Button","text":"Book","onAction":true},{"n":9,"type":"Label","text":""}]}}}}],"problems":[]},"outline":"Window \"Excursions\"\n  BorderPane\n    top: HBox spacing 10, padding 10\n      Button \"Island tour\"\n      Button \"Lake cruise\"\n    center: VBox spacing 8\n      Label \"Island tour: 40 of 40 seats free\"\n      TextField \"\", prompt \"Seats\"\n      Button \"Book\"\n      Label \"\""},{"name":"Test 3","stdin":"","expect":"","hidden":true,"events":["type TextField 1 \"4\"","click Button \"Book\"","click Button \"Lake cruise\"","type TextField 1 \"25\"","click Button \"Book\"","click Button \"Island tour\""],"window":{"windows":[{"title":"Excursions","scene":{"root":{"n":1,"type":"BorderPane","top":{"n":2,"type":"HBox","padding":[10,10,10,10],"spacing":10,"children":[{"n":3,"type":"Button","text":"Island tour","onAction":true},{"n":4,"type":"Button","text":"Lake cruise","onAction":true}]},"center":{"n":5,"type":"VBox","spacing":8,"children":[{"n":6,"type":"Label","text":"Island tour: 36 of 40 seats free"},{"n":7,"type":"TextField","text":"4","promptText":"Seats"},{"n":8,"type":"Button","text":"Book","onAction":true},{"n":9,"type":"Label","text":"Seats booked: 4."}]}}}}],"problems":[]},"outline":"Window \"Excursions\"\n  BorderPane\n    top: HBox spacing 10, padding 10\n      Button \"Island tour\"\n      Button \"Lake cruise\"\n    center: VBox spacing 8\n      Label \"Island tour: 36 of 40 seats free\"\n      TextField \"4\", prompt \"Seats\"\n      Button \"Book\"\n      Label \"Seats booked: 4.\""},{"name":"Test 4","stdin":"","expect":"","hidden":true,"events":["click Button \"Lake cruise\"","type TextField 1 \"26\"","click Button \"Book\"","click Button \"Island tour\"","click Button \"Lake cruise\""],"window":{"windows":[{"title":"Excursions","scene":{"root":{"n":1,"type":"BorderPane","top":{"n":2,"type":"HBox","padding":[10,10,10,10],"spacing":10,"children":[{"n":3,"type":"Button","text":"Island tour","onAction":true},{"n":4,"type":"Button","text":"Lake cruise","onAction":true}]},"center":{"n":5,"type":"VBox","spacing":8,"children":[{"n":6,"type":"Label","text":"Lake cruise: 25 of 25 seats free"},{"n":7,"type":"TextField","text":"26","promptText":"Seats"},{"n":8,"type":"Button","text":"Book","onAction":true},{"n":9,"type":"Label","text":"Can't book that. Free seats: 25."}]}}}}],"problems":[]},"outline":"Window \"Excursions\"\n  BorderPane\n    top: HBox spacing 10, padding 10\n      Button \"Island tour\"\n      Button \"Lake cruise\"\n    center: VBox spacing 8\n      Label \"Lake cruise: 25 of 25 seats free\"\n      TextField \"26\", prompt \"Seats\"\n      Button \"Book\"\n      Label \"Can't book that. Free seats: 25.\""}],"require":[],"forbid":[]}]}