Java Arena
Step 7 of 8

A practice app, part 2: the views

Step 6 wrote the capitals trainer's logic: a CardBox that stores the cards, and a PracticeRound that runs the training and remembers the missed countries. Neither knows that a window exists. This step puts views on top of them.

The plan for the window

  • A BorderPane, with a menu in the top region: an HBox with the buttons Add cards and Train. The center shows one of two views, each a class with a getView() method (step 4).
  • NewCardView has a field for the country, one for the capital, the button Add card and a label for messages. It's made once, in its constructor, so that what the user typed stays when they visit the training and come back.
  • TrainingView shows a country, a field for the answer, the button Answer, a label that says whether the answer was right, and the score. It builds its nodes at each call of getView(), since the question depends on the cards, and the user may have added some since the last visit.
  • Main makes one CardBox and one PracticeRound, and gives both views what they need of them.
The diagram as text
  • Main, a class
  • NewCardView, a class
    • public constructor NewCardView(box: CardBox)
    • public method getView(): Parent
  • TrainingView, a class
    • public constructor TrainingView(box: CardBox, round: PracticeRound)
    • public method getView(): Parent
  • CardBox, a class
    • public method add(country: String, capital: String): boolean
    • public method capitalOf(country: String): String
    • public method size(): int
    • more members, not shown
  • PracticeRound, a class
    • public method question(): String
    • public method answer(text: String): boolean
    • public method getRight(): int
    • public method getWrong(): int
    • more members, not shown

Connections:

  • Each Main has one NewCardView object.
  • Each Main has one TrainingView object.
  • Each NewCardView has one CardBox object.
  • Each TrainingView has one CardBox object.
  • Each TrainingView has one PracticeRound object.
  • Each PracticeRound has one CardBox object.

A first version, with a stand-in

A larger program is easier to build in pieces that each run. The first version has the real NewCardView and a stand-in for TrainingView: a class with the right constructor and getView(), which only shows a label for now. Everything compiles, the menu works, and the adding of cards can be tried before the training view exists:

Main.java
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        CardBox box = new CardBox();
        PracticeRound round = new PracticeRound(box);
        NewCardView newCards = new NewCardView(box);
        TrainingView training = new TrainingView(box, round);

        Button toNewCards = new Button("Add cards");
        Button toTraining = new Button("Train");
        HBox menu = new HBox(10, toNewCards, toTraining);
        menu.setPadding(new Insets(10));

        BorderPane frame = new BorderPane();
        frame.setTop(menu);
        frame.setCenter(newCards.getView());
        toNewCards.setOnAction(event -> frame.setCenter(newCards.getView()));
        toTraining.setOnAction(event -> frame.setCenter(training.getView()));

        stage.setScene(new Scene(frame));
        stage.setTitle("Capitals");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
NewCardView.java
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;

public class NewCardView {
    private CardBox box;
    private VBox view;

    public NewCardView(CardBox box) {
        this.box = box;
        TextField country = new TextField();
        country.setPromptText("Country");
        TextField capital = new TextField();
        capital.setPromptText("Capital");
        Button add = new Button("Add card");
        Label message = new Label("");
        add.setOnAction(event -> {
            if (this.box.add(country.getText(), capital.getText())) {
                message.setText("Added. Cards in the box: " + this.box.size());
                country.clear();
                capital.clear();
            } else {
                message.setText("Fill in both fields, and add each country once.");
            }
        });
        this.view = new VBox(8, country, capital, add, message);
        this.view.setPadding(new Insets(10));
    }

    public Parent getView() {
        return this.view;
    }
}
TrainingView.java
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;

public class TrainingView {
    private CardBox box;
    private PracticeRound round;

    public TrainingView(CardBox box, PracticeRound round) {
        this.box = box;
        this.round = round;
    }

    public Parent getView() {
        VBox view = new VBox(8, new Label("Training comes in the next version."));
        view.setPadding(new Insets(10));
        return view;
    }
}
CardBox.java
import java.util.ArrayList;

public class CardBox {
    private ArrayList<Card> cards;

    public CardBox() {
        this.cards = new ArrayList<>();
    }

    public boolean add(String country, String capital) {
        String name = country.trim();
        String city = capital.trim();
        if (name.isEmpty() || city.isEmpty() || this.capitalOf(name) != null) {
            return false;
        }
        this.cards.add(new Card(name, city));
        return true;
    }

    public String capitalOf(String country) {
        for (Card card : this.cards) {
            if (card.getCountry().equals(country)) {
                return card.getCapital();
            }
        }
        return null;
    }

    public String countryAt(int index) {
        return this.cards.get(index).getCountry();
    }

    public int size() {
        return this.cards.size();
    }
}
Card.java
public class Card {
    private String country;
    private String capital;

    public Card(String country, String capital) {
        this.country = country;
        this.capital = capital;
    }

    public String getCountry() {
        return this.country;
    }

    public String getCapital() {
        return this.capital;
    }
}
PracticeRound.java
import java.util.ArrayList;

public class PracticeRound {
    private CardBox box;
    private int next;
    private int right;
    private int wrong;
    private ArrayList<String> missed;

    public PracticeRound(CardBox box) {
        this.box = box;
        this.missed = new ArrayList<>();
    }

    public String question() {
        if (this.box.size() == 0) {
            return null;
        }
        return this.box.countryAt(this.next);
    }

    public boolean answer(String text) {
        String country = this.question();
        if (country == null) {
            return false;
        }
        boolean correct = text.trim().equalsIgnoreCase(this.box.capitalOf(country));
        if (correct) {
            this.right++;
        } else {
            this.wrong++;
            if (!this.missed.contains(country)) {
                this.missed.add(country);
            }
        }
        this.next++;
        if (this.next >= this.box.size()) {
            this.next = 0;
        }
        return correct;
    }

    public int getRight() {
        return this.right;
    }

    public int getWrong() {
        return this.wrong;
    }

    public ArrayList<String> missed() {
        return this.missed;
    }
}
The window

After: Type "Peru" in text field 1, type "Lima" in text field 2, click "Add card", type "Kenya" in text field 1, then click "Add card"

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Capitals"
  BorderPane
    top: HBox spacing 10, padding 10
      Button "Add cards"
      Button "Train"
    center: VBox spacing 8, padding 10
      TextField "Kenya", prompt "Country"
      TextField "", prompt "Capital"
      Button "Add card"
      Label "Fill in both fields, and add each country once."
  • NewCardView's handler forwards both texts to box.add and lets the box decide. On true it says how many cards there are and empties the fields for the next card; on false it explains what the box refuses. The rule itself stays in CardBox.
  • Kenya wasn't added, since its capital was missing, and the field still holds Kenya, so the user only needs to type the capital.
  • The stand-in TrainingView already takes the box and the round, as the real one will, so Main won't need to change when it's replaced.

The training view

TrainingView's getView() builds a new VBox at each call. When the box is empty, there is nothing to train, and the view holds only a label that says so. Otherwise it holds the question, the answer field, the Answer button, the feedback label and the score.

Answer's handler must be careful about order. round.answer(...) moves the turn on to the next card, so the country that was asked, and its capital for the feedback, have to be read before calling it. After it, the question label shows the next country, the field is emptied for the next answer, and the score is updated.

Your task

Challenge 1 of 3

Replace the stand-in. Write TrainingView in TrainingView.java; everything else is ready, and PracticeRound is step 6's, with missed(). getView() returns a VBox with the spacing 8 and the padding 10 on every side:

  • when the box is empty, it holds only the label Add some cards first.
  • otherwise it holds, in order: the label Capital of Peru? (with the country of round.question()), a text field with the prompt text Capital, the button Answer, an empty label for feedback, and the label Right 0, wrong 0 (with the round's counts)

Answer answers with the field's text, and then:

  • the feedback label says Yes, Lima. for a right answer, or No, it's Lima. for a wrong one (with the capital of the country that was asked)
  • the question label shows the next question, the field is emptied, and the score label shows the new counts

The task card shows the window after adding Peru and Kenya, clicking Train and answering lima.

Challenge 2

The trainer gets a third view: the countries to practice again. Write the class MissedView in MissedView.java, and add it to the menu in Main:

  • a constructor MissedView(CardBox box, PracticeRound round)
  • getView() builds a new VBox at each call, with the spacing 4 and the padding 10 on every side. When round.missed() is empty, it holds only the label Nothing missed so far. Otherwise it holds the label Practice these again: and then a label for each missed country, in the order of missed(), like Peru: Lima (the country and its capital).
  • in Main, a third button Missed, after Train in the menu, puts the missed view in the center

The other classes are ready. The task card shows the window after adding Peru and Kenya, answering Cusco and Nairobi, and clicking Missed.

Challenge 3

A text field has an action too: setOnAction on a TextField gives it a handler that runs when the user presses Enter in it. Typing an answer and then reaching for the mouse is slow, so make Enter work:

  • in NewCardView, Enter in the capital field adds the card, exactly as a click on Add card does
  • in TrainingView, Enter in the answer field answers, exactly as a click on Answer does

A button's fire() runs its handler, so neither handler needs to be written twice. Enter in the country field still does nothing. Nothing else changes. The task card shows the window after adding Peru and Kenya with Enter, clicking Train, and answering Lima with Enter.