diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..4f420b5823 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# IDEA files +/.idea/ +/out/ +/*.iml + +# Gradle build files +/.gradle/ +/build/ +src/main/resources/docs/ + +# MacOS custom attributes files created by Finder +.DS_Store diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000000..0daa0030f5 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,9 @@ +# Contributors + +Display | Name | Github Profile | Homepage +---|:---:|:---:|:---: +![](https://avatars1.githubusercontent.com/u/22460123) | Jeffry Lum | [Github](https://github.com/j-lum/) | [Homepage](https://se.kasugano.moe) + +# I would like to join this list. How can I help the project + +For more information, please refer to our [contributor's guide](https://oss-generic.github.io/process/). diff --git a/README.md b/README.md new file mode 100644 index 0000000000..c82db455a4 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Duke + +Duke is a Personal Assistant Chatbot that helps a person to keep track of various things. + +# Tutorials + +Increments | Tutorial Name +---|--- +A-Gradle | [Gradle Tutorial 1 : Introduction to Gradle](tutorials/gradle-tutorial-1-introduction-to-gradle.md) +Level-10 | [JavaFX Tutorial 1: Introduction to JavaFX](tutorials/ui-tutorial-1-introduction-to-javafx.md) +Level-10 | [JavaFX Tutorial 2: Creating a GUI for Duke](tutorials/ui-tutorial-2-creating-a-gui-for-duke.md) +Level-10 | [JavaFX Tutorial 3: Interacting with the user](tutorials/ui-tutorial-3-interacting-with-the-user.md) +Level-10 | [JavaFX Tutorial 4: Introduction to FXML](tutorials/ui-tutorial-4-introduction-to-fxml.md) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..fd44069597 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,20 @@ +# User Guide + +## Features + +### Feature 1 +Description of feature. + +## Usage + +### `Keyword` - Describe action + +Describe action and its outcome. + +Example of usage: + +`keyword (optional arguments)` + +Expected outcome: + +`outcome` diff --git a/tutorials/assets/DialogBoxController.png b/tutorials/assets/DialogBoxController.png new file mode 100644 index 0000000000..1870dcbc0b Binary files /dev/null and b/tutorials/assets/DialogBoxController.png differ diff --git a/tutorials/assets/DialogBoxesIteration2.png b/tutorials/assets/DialogBoxesIteration2.png new file mode 100644 index 0000000000..2d72f1d1c7 Binary files /dev/null and b/tutorials/assets/DialogBoxesIteration2.png differ diff --git a/tutorials/assets/DialogBoxesIteration3.png b/tutorials/assets/DialogBoxesIteration3.png new file mode 100644 index 0000000000..aea148d668 Binary files /dev/null and b/tutorials/assets/DialogBoxesIteration3.png differ diff --git a/tutorials/assets/DukeMockup.png b/tutorials/assets/DukeMockup.png new file mode 100644 index 0000000000..adf8ba42ac Binary files /dev/null and b/tutorials/assets/DukeMockup.png differ diff --git a/tutorials/assets/DukeSceneGraph.png b/tutorials/assets/DukeSceneGraph.png new file mode 100644 index 0000000000..17bfe0d4f9 Binary files /dev/null and b/tutorials/assets/DukeSceneGraph.png differ diff --git a/tutorials/assets/DukeSceneGraph.puml b/tutorials/assets/DukeSceneGraph.puml new file mode 100644 index 0000000000..ea7937255a --- /dev/null +++ b/tutorials/assets/DukeSceneGraph.puml @@ -0,0 +1,24 @@ +@startuml +hide members +hide circle +skinparam shadowing false +skinparam ClassFontSize 16 +skinparam ClassFontName Arial + +class Stage +class AnchorPane +class ScrollPane +class VBox +class ImageView +class Label + +AnchorPane -up-> Stage +ScrollPane -up-> AnchorPane + +TextField -up-> AnchorPane +Button -up-> AnchorPane + +VBox -up-> ScrollPane +ImageView -up-> VBox +Label -up-> VBox +@enduml diff --git a/tutorials/assets/EchoNotScrolling.png b/tutorials/assets/EchoNotScrolling.png new file mode 100644 index 0000000000..b56395ce88 Binary files /dev/null and b/tutorials/assets/EchoNotScrolling.png differ diff --git a/tutorials/assets/FinalLayout.png b/tutorials/assets/FinalLayout.png new file mode 100644 index 0000000000..5e2ec75d02 Binary files /dev/null and b/tutorials/assets/FinalLayout.png differ diff --git a/tutorials/assets/GradleIcon.png b/tutorials/assets/GradleIcon.png new file mode 100644 index 0000000000..61b0ad4da2 Binary files /dev/null and b/tutorials/assets/GradleIcon.png differ diff --git a/tutorials/assets/HelloWorld.png b/tutorials/assets/HelloWorld.png new file mode 100644 index 0000000000..3066d4a2e3 Binary files /dev/null and b/tutorials/assets/HelloWorld.png differ diff --git a/tutorials/assets/ImportGradle.png b/tutorials/assets/ImportGradle.png new file mode 100644 index 0000000000..fca5c48df7 Binary files /dev/null and b/tutorials/assets/ImportGradle.png differ diff --git a/tutorials/assets/JavaFxHierarchy.png b/tutorials/assets/JavaFxHierarchy.png new file mode 100644 index 0000000000..87228d5c64 Binary files /dev/null and b/tutorials/assets/JavaFxHierarchy.png differ diff --git a/tutorials/assets/JavaFxHierarchy.puml b/tutorials/assets/JavaFxHierarchy.puml new file mode 100644 index 0000000000..77c8c00185 --- /dev/null +++ b/tutorials/assets/JavaFxHierarchy.puml @@ -0,0 +1,22 @@ +@startuml +hide members +hide circle +skinparam shadowing false +skinparam ClassFontSize 16 +skinparam ClassFontName Arial + +Class Stage +Class Scene +Class "Root Node" as RN +Class Node +Class "Node" as Node1 +Class "Node" as Node2 +Class "Node" as Node3 + +Scene -up-> Stage : > must have one +RN -up-> Scene : > must have one +Node -up-> RN : > can have zero or more +Node1 -up-> Node +Node2 -up-> Node +Node3 -up-> Node +@enduml diff --git a/tutorials/assets/MainWindowController.png b/tutorials/assets/MainWindowController.png new file mode 100644 index 0000000000..26d1a73639 Binary files /dev/null and b/tutorials/assets/MainWindowController.png differ diff --git a/tutorials/assets/MockupButton.png b/tutorials/assets/MockupButton.png new file mode 100644 index 0000000000..352fb86ef9 Binary files /dev/null and b/tutorials/assets/MockupButton.png differ diff --git a/tutorials/assets/MockupImageView.png b/tutorials/assets/MockupImageView.png new file mode 100644 index 0000000000..e580196ecf Binary files /dev/null and b/tutorials/assets/MockupImageView.png differ diff --git a/tutorials/assets/MockupLabel.png b/tutorials/assets/MockupLabel.png new file mode 100644 index 0000000000..0980c3c50c Binary files /dev/null and b/tutorials/assets/MockupLabel.png differ diff --git a/tutorials/assets/MockupScrollPane.png b/tutorials/assets/MockupScrollPane.png new file mode 100644 index 0000000000..a29de75abb Binary files /dev/null and b/tutorials/assets/MockupScrollPane.png differ diff --git a/tutorials/assets/MockupTextField.png b/tutorials/assets/MockupTextField.png new file mode 100644 index 0000000000..a8cfded3bb Binary files /dev/null and b/tutorials/assets/MockupTextField.png differ diff --git a/tutorials/assets/NewEmptyProject.png b/tutorials/assets/NewEmptyProject.png new file mode 100644 index 0000000000..94e076c206 Binary files /dev/null and b/tutorials/assets/NewEmptyProject.png differ diff --git a/tutorials/assets/RawLayout.png b/tutorials/assets/RawLayout.png new file mode 100644 index 0000000000..02183af4de Binary files /dev/null and b/tutorials/assets/RawLayout.png differ diff --git a/tutorials/assets/SceneBuilder.png b/tutorials/assets/SceneBuilder.png new file mode 100644 index 0000000000..106484f594 Binary files /dev/null and b/tutorials/assets/SceneBuilder.png differ diff --git a/tutorials/gradle-tutorial-1-introduction-to-gradle.md b/tutorials/gradle-tutorial-1-introduction-to-gradle.md new file mode 100644 index 0000000000..68ab003bcb --- /dev/null +++ b/tutorials/gradle-tutorial-1-introduction-to-gradle.md @@ -0,0 +1,163 @@ +# Gradle Tutorial 1 - Setting up Gradle + +Gradle is a _build automation tool_ used to automate build processes. +There are many ways of integrating Gradle into a project. +In this guide, we will be using the _Gradle wrapper_. + +## Primer to Gradle + +As a developer, you write a build file that describes the project. + +A build file mainly consists of _plugins_, _tasks_ and _properties_. + +Plugins extend the functionality of Gradle. The `java` plugin adds support for `Java` projects. + +Tasks are reusable blocks of logic. +For example, the task `clean` simply deletes the project build directory. +Tasks can be composed of other tasks or be dependent on another task. +You might be surprised to find that a simple `check` will invoke another eight tasks at the very least! + +Properties change the behavior of tasks. +For instance, `mainClassName` of the `application` plugin is a compulsory property which tells Gradle which class is the entrypoint to your application. +As Gradle favors `convention over configuration`, there is not much to you need to configure if you follow the recommended directory structure. + +## Getting started + +1. Merge [this branch](https://github.com/se-edu/duke/tree/gradle). This will add the Gradle wrapper to your project. + +2. Navigate to the root directory of your project and type `gradlew run`. + +For users of IntelliJ IDEA, you can import the Gradle project by `Help > Find Action > Import Gradle Project`. + +![Import Gradle](assets/ImportGradle.png) + +After this, IntelliJ IDEA will identify your project as a Gradle project and you will gain access to the `Gradle Toolbar`. +Through the toolbar, you run Gradle tasks and view your project's dependencies. + +## Using Gradle + +Simply type `gradlew {taskName}` into the terminal and Gradle will run the task! +For example, you can type `gradlew tasks` and Gradle will show you a list of tasks available for your project. +Some plugins may add more helpful tasks so be sure to check the documentation! + +If you're using IntelliJ IDEA, you can click on the Gradle icon in the Gradle toolbar and create a new run configuration. + +![Gradle icon](assets/GradleIcon.png) + +Having a run configuration will save you a few keypresses in the long run. + +## Adding plugins + +Gradle plugins are reusable units of build logic. +Most common build tasks are provided as core plugins by Gradle. +Given below are instructions on how to use some useful plugins: + +### Checkstyle +To add support for _Checkstyle_, a tool to check if your code complies with coding standards. +Since Checkstyle is a core plugin, simply add the line `id 'checkstyle` into the `plugins` block. + +Your build file should look something like this now + +```groovy +plugins { + id 'java' + id 'application' + id 'checkstyle' +} +// ... code omitted for brevity ... +``` + +Checkstyle expects configuration files to for checkstyle to be in `./config/checkstyle/` by convention. +You can find the configuration files used in later projects [here](https://github.com/se-edu/addressbook-level3/tree/master/config/checkstyle). + +The plugin adds a few _tasks_ to your project. +Run `gradlew checkstyleMain checkstyleTest` to verify that you have set up Checkstyle properly. + +To find out the full list of tasks available to you, you can run `gradlew tasks --all`. + +### Shadow + +Shadow is a plugin that helps you package your application into an executable jar file. +Add the following line to your Gradle build file: + +```groovy +plugin { + //... + id 'com.github.johnrengelman.shadow' version '5.1.0' + //... +} +``` + +The plugin can be configured by setting some properties. +Let's try to produce a jar file with the name in format of `{baseName}-{version}.jar`. + +Add the following block to your build file: + +```groovy +//Publishes an executable jar to ./build/libs/ +shadowJar { + archiveBaseName = "duke" + archiveVersion = "0.1.3" + archiveClassifier = null + archiveAppendix = null +} +``` + +Now you can run the task `shadowJar` with the command `gradlew shadowJar`. +Are you able to execute your jar file with `java -jar {jarName}`? + +## Adding dependencies + +### JUnit 5 + +JUnit is a testing framework for Java. +It allows developers to write tests and run them. + +Add the following dependency to your build file: + +```groovy +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.5.0' +} +``` + +Then, configure Gradle to use JUnit by adding the following block to your build file: + +```groovy + +test { + useJUnitPlatform() +} +``` +By convention, tests belong in `src/test` folder. +Create a new `test` folder in under `src`. + +``` +src +├─main +│ ├─java +│ │ └─seedu +│ │ └─duke +│ └─resources +│ └─view +└─test + └─java + └─seedu + └─duke +``` + +If you have imported your Gradle project into IntelliJ IDEA, you will notice that IDEA is able to mark the test +directory as the Test root (colored in green by default) automatically. + +You can now write a test and run it with `gradlew test`. + +## Further reading + +Now that you have a general idea of how to accomplish basic tasks with Gradle, here's a list of material you can read + to further your understanding. + +- [Official Gradle Documentation](https://docs.gradle.org/current/userguide/userguide.html) +- [Google's checkstyle file](https://github.com/checkstyle/checkstyle/blob/master/src/main/resources/google_checks.xml) +- [Shadow](https://imperceptiblethoughts.com/shadow/introduction/) +- [Official JUnit Documentation](https://junit.org/junit5/docs/current/user-guide/#writing-tests) +- [AddressBook Level-4's build file](https://github.com/se-edu/addressbook-level4/blob/master/build.gradle) \ No newline at end of file diff --git a/tutorials/ui-tutorial-1-introduction-to-javafx.md b/tutorials/ui-tutorial-1-introduction-to-javafx.md new file mode 100644 index 0000000000..32345b7403 --- /dev/null +++ b/tutorials/ui-tutorial-1-introduction-to-javafx.md @@ -0,0 +1,94 @@ +# JavaFX Tutorial 1 – Hello World + +# Setting up +Follow the guide in the README. + + +# Basics of JavaFX: + +## An analogy +In JavaFX, you are a director and each application is a play. +First you provision the props that you will feature in your play. +These can be hand props for your actors to interact with or even set dressings just to liven up the background. +You then decide where to layout the props for every scene. +With a proper script in hand, you can finally approach a theatre and request for a stage. +There on, it’s just a matter of pulling the curtains on your masterpiece. + +## JavaFX lifecycle of an application +Instead of creating props, you create `Nodes` instead. +`Nodes` are the fundamental building blocks of a JavaFX application. +You create `Nodes` and place them onto a `Scene`. +A scene can be thought of as a graph, each `Scene` must have a root `Node` and each `Node` can have zero or more children. + +![Hierarchy of Objects in JavaFX](assets/JavaFxHierarchy.png) + +Don’t worry too much about this for now, you’ll see exactly how to work with the scene graph in a later tutorial. +Then, you set your `Scene` on a `Stage` provided by JavaFX. +When you call `Stage#show()` method, JavaFX renders a window with your `Stage` on it. + +# Writing your first program +As customary, let’s start off with a simple “Hello World” program. +Create a new `HelloWorld.java` class in the `duke.seedu` package. +Have the class extend `javafx.application.Application`. +This requires you to override the `Application#start()` method and provide a concrete implementation. +Notice that the method signature for `Application#start()` has a parameter `Stage`. +This is the _primary stage_ that JavaFX provides. + +```java +public class HelloWorld extends Application { + + public static void main(String[] args) { + launch(); + } + + @Override + public void start(Stage stage) { + // Our code goes here + } +} +``` + +Let’s create a `Label` to contain the text that we want to show. +We then create the `Scene` and set its content. +Finally, we can set the stage and show it. + +```java +public class HelloWorld extends Application { + + public static void main(String[] args) { + launch(); + } + + @Override + public void start(Stage stage) { + Label helloWorld = new Label("Hello World!"); // Creating a new Label control + Scene scene = new Scene(helloWorld); // Setting the scene to be our Label + + stage.setScene(scene); // Setting the stage to show our screen + stage.show(); // Render the stage. + } +} +``` + +Run the program and you should see something like this: + +![Hello World](assets/HelloWorld.png) + +Congratulations! You have created your first GUI application! + +# Exercises +1. We mentioned that `Node`s are the fundamental building blocks of JavaFX and used a `Label` as our root node in the HelloWorld application. + + 1. What are some of the other types of `Node`s? + 1. How does JavaFX group them? + +1. `Node`s can be interacted with like Plain Old Java Objects (POJO). + + 1. What properties of a `Label` can you change programmatically? + 1. Try changing the `Label` to have a font of Arial at size 50. + +1. You’ve learnt that a `Stage` can be thought of as a window. + + 1. Can you have more than one `Stage` an application? + 1. Try creating another stage and showing it! What happens? + \ No newline at end of file diff --git a/tutorials/ui-tutorial-2-creating-a-gui-for-duke.md b/tutorials/ui-tutorial-2-creating-a-gui-for-duke.md new file mode 100644 index 0000000000..529971b8aa --- /dev/null +++ b/tutorials/ui-tutorial-2-creating-a-gui-for-duke.md @@ -0,0 +1,157 @@ +# JavaFX Tutorial 2 - Creating a GUI for Duke +We assume that you have finished the setting up process in the previous tutorial. +In this tutorial, we will be creating a graphical user interface for Duke from scratch based on the following mockup. + +![Mockup for Duke](assets/DukeMockup.png) + +# Introducing JavaFX controls +Controls are reusable UI elements. +Refer to the [JavaFX's official documentation](https://openjfx.io/javadoc/11/javafx.controls/javafx/scene/control/package-summary.html) for a list of controls available. +From the mockup above, can you identify the controls that we will need to use? + +Mockup | Control +--- | :---: | +![ImageView](assets/MockupImageView.png) | ImageView +![ImageView](assets/MockupLabel.png) | Label +![ImageView](assets/MockupButton.png) | Button +![ImageView](assets/MockupTextField.png) | TextField +![ImageView](assets/MockupScrollPane.png) | ScrollPane + +Now that we know what controls we need to implement our UI, let’s start programming! + +```java +public class Main extends Application { + + private ScrollPane scrollPane; + private TextField userInput; + private Button sendButton; + private Scene scene; + + public static void main(String[] args) { + launch(); + } + + @Override + public void start(Stage stage) { + //Part 1. Setting up required components + scrollPane = new ScrollPane(); + userInput = new TextField(); + sendButton = new Button("Send"); + + // What do we put into our scene as the root Node? + // scene = new Scene(???) + // stage.setScene(scene); + // stage.show(); + } +} +``` +We quickly run into a problem: how do we show all of them on the screen at once? + +# Introducing the Scenegraph API +Each scene is initialized with a root `Node`. +In the previous tutorial, our root `Node` was a `Label`. +What happens when we need to display more than one `Node` on the `Scene`? +For that, we need to understand the JavaFX hierarchy. +Recall from the previous tutorial: + +![Hierarchy of Objects in JavaFX](assets/JavaFxHierarchy.png) + +From the diagram, you see that the root `Node` can contain many other `Nodes` and similarly, each of those `Nodes` can contain many other `Nodes`. +This means that if we can find a _container_ to set as our root `Node`, we can place all our other `Nodes` in it. + +## Layout managers and containers +Fortunately for us, JavaFX provides that functionality in the form of layout panes in `javafx.scene.layouts`. +Each layout pane follows a layout policy to decide how to arrange its children. +For example, the `VBox` lays out its children in a single vertical column and its counterpart, the `HBox` lays out its children in a single horizontal row. +A comprehensive list of layouts and how they behave is available here from the [official documentation](https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/layout/package-summary.html). + +One way to obtain the layout in the mockup is as follows: + +![Duke's layout](assets/DukeSceneGraph.png) + +For the rest of the tutorial, let’s use this layout. + +## Putting it together +We create a new `AnchorPane` and add our controls to it. +Similarly, we create a new `VBox` to hold the contents of the `ScrollPane` +The code should look something like this: + +```java +@Override +public void start(Stage stage) { + //Part 1. Setting up required components + //The container for the content of the chat to scroll. + scrollPane = new ScrollPane(); + dialogContainer = new VBox(); + scrollPane.setContent(dialogContainer); + + userInput = new TextField(); + sendButton = new Button("Send"); + + AnchorPane mainLayout = new AnchorPane(); + mainLayout.getChildren().addAll(scrollPane, userInput, sendButton); + + scene = new Scene(mainLayout); + + stage.setScene(scene); + stage.show(); +} +``` +Run the application and you should see something like this: + +![Duke's raw layout](assets/RawLayout.png) + +That is not what we were expecting, what did we forget to do? + +# Introducing styling and properties +Almost every JavaFX object offer properties that you can set to customize its look and feel. +For example, the `Stage` allows you to set its preferred size and title. +Again, refer to the official JavaFX documentation for a comprehensive list of properties that you can modify. +Here’s how you can get the application to look like the mockup: + +```java +//Part 2. Formatting the window to look as expected. +stage.setTitle("Duke"); +stage.setResizable(false); +stage.setMinHeight(600.0); +stage.setMinWidth(400.0); + +mainLayout.setPrefSize(400.0, 600.0); + +scrollPane.setPrefSize(385, 535); +scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); +scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS); + +scrollPane.setVvalue(1.0); +scrollPane.setFitToWidth(true); + +dialogContainer.setPrefHeight(Region.USE_COMPUTED_SIZE); + +userInput.setPrefWidth(325.0); + +sendButton.setPrefWidth(55.0); + +AnchorPane.setTopAnchor(scrollPane, 1.0); + +AnchorPane.setBottomAnchor(sendButton, 1.0); +AnchorPane.setRightAnchor(sendButton, 1.0); + +AnchorPane.setLeftAnchor(userInput , 1.0); +AnchorPane.setBottomAnchor(userInput, 1.0); +``` + +Run the application again. + +![Duke's Final layout](assets/FinalLayout.png) + +Looking great! + +# Exercises +1. In the tutorial, we used an `AnchorPane` to achieve the desired layout. + + 1. Can you find other ways to obtain a similar layout? + 1. What are the advantages and disadvantages of your layout? + +1. Try interacting with the application + 1. What happens when you press the `Enter` key or left-click the send button? + 1. Why? diff --git a/tutorials/ui-tutorial-3-interacting-with-the-user.md b/tutorials/ui-tutorial-3-interacting-with-the-user.md new file mode 100644 index 0000000000..34a4ab7a48 --- /dev/null +++ b/tutorials/ui-tutorial-3-interacting-with-the-user.md @@ -0,0 +1,193 @@ +# JavaFX Tutorial 3 – Interacting with the user + +Picking up from where we left off last tutorial, we have successfully achieved the desired layout. +Now let’s make the application respond to user input. +You should substitute the provided `DukeStub` class with your completed Duke. + +Rather than to do everything in one try, let’s iterate and build up towards our final goal. + +## Iteration 1 – Echoing the user +JavaFX has an _event-driven architecture style_. +As such, we programmatically define handler methods to execute as a response to certain events. +When an event is detected, JavaFX will call the respective handlers. + +For Duke, there are two events that we want to respond to, namely the user pressing `Enter` in the `TextField` and left-clicking the `Button`. +These are the `onAction` event for the `TextField` and the `onMouseClicked` event for the `Button`. + +For now, let’s have the application add a new `Label` with the text from the `TextField`. +```java +@Override +public void start(Stage stage) { + // Code from part 1 and part 2 omitted for brevity + //Part 3. Add functionality to handle user input. + sendButton.setOnMouseClicked((event) -> { + dialogContainer.getChildren().add(getDialogLabel(userInput.getText())); + userInput.clear(); + }); + + userInput.setOnAction((event) -> { + dialogContainer.getChildren().add(getDialogLabel(userInput.getText())); + userInput.clear(); + }); +} + +/** + * Iteration 1: + * Creates a label with the specified text and adds it to the dialog container. + * @param text String containing text to add + * @return a label with the specified text that has word wrap enabled. + */ +private Label getDialogLabel(String text) { + Label textToAdd = new Label(text); + textToAdd.setWrapText(true); + + return textToAdd; +} +``` + +Run the program and give it a whirl! + +![Echo not scrolling as intended](assets/EchoNotScrolling.png) + +At first glance everything appears to work perfectly. +However, when the `VBox` stretches beyond the confines of the `ScrollPane`, the `ScrollPane` does not scroll down automatically as expected. +We can remedy this by attaching a handler on the `VBox` to react to its own size changing and scrolling the `ScrollPane` down. + +```java +public void start(Stage stage) { + // ... + //Scroll down to the end every time dialogContainer's height changes. + //v-value of 1.0 means 100% of the way down + dialogContainer.heightProperty().addListener((observable) -> scrollPane.setVvalue(1.0)); +} +``` + +Verify that the `ScrollPane` scrolls as intended. + +## Iteration 2 – Adding dialog boxes. +In the mockup of the UI, notice that the dialog boxes are composed of two different controls (`ImageView` and `Label`) and reused multiple times. +In situations like this, it is often beneficial to create our own custom control. +Doing this allows us to tightly couple the components together. + +Let’s create our custom control `DialogBox`: +```java +public class DialogBox extends HBox { + + private Label text; + private ImageView displayPicture; + + public DialogBox(Label l, ImageView iv) { + text = l; + displayPicture = iv; + + text.setWrapText(true); + displayPicture.setFitWidth(100.0); + displayPicture.setFitHeight(100.0); + + this.setAlignment(Pos.TOP_RIGHT); + this.getChildren().addAll(text, displayPicture); + } +} +``` + +We use the code in our main class just like any other control: + +```java +@Override +private Image user = new Image(this.getClass().getResourceAsStream("/images/DaUser.png")); +private Image duke = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png")); + +public void start(Stage stage) { + //Part 3. Add functionality to handle user input. + sendButton.setOnMouseClicked((event) -> { + dialogContainer.getChildren().add(getDialogLabel(userInput.getText())); + }); + + userInput.setOnAction((event) -> { + dialogContainer.getChildren().add(getDialogLabel(userInput.getText())); + }); +} + +/** + * Iteration 2: + * Creates two dialog boxes, one echoing user input and the other containing Duke's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ +private void handleUserInput() { + Label userText = new Label(userInput.getText()); + Label dukeText = new Label(DukeStub.getResponse(userInput.getText())); + dialogContainer.getChildren().addAll( + new DialogBox(userText, new ImageView(user)), + new DialogBox(dukeText, new ImageView(duke)) + ); + userInput.clear(); +} +``` + +Run the program and see how it works. + +![DialogBoxes Iteration 2](assets/DialogBoxesIteration2.png) + +## Iteration 3 – Adding custom behavior to DialogBox +One additional benefit of defining a custom control is that we can add behavior specific to our `DialogBox`. +Let’s add a method to flip a dialog box such that the image on the left to differentiate between user input and Duke’s output. + +```java +private DialogBox(Label l, ImageView iv) { + // ... + //Note how we changed the constructor to be private. +} +/** + * Iteration 3: + * Flips the dialog box such that the ImageView is on the left and text on the right. + */ +private void flip() { + this.setAlignment(Pos.TOP_LEFT); + ObservableList tmp = FXCollections.observableArrayList(this.getChildren()); + Collections.reverse(tmp); + this.getChildren().setAll(tmp); +} + +public static DialogBox getUserDialog(Label l, ImageView iv) { + return new DialogBox(l, iv); +} + +public static DialogBox getDukeDialog(Label l, ImageView iv) { + var db = new DialogBox(l, iv); + db.flip(); + return db; +} +``` + +Now, we can change the event handler to use our new `DialogBox`. + +```java +private void handleUserInput() { + Label userText = new Label(userInput.getText()); + Label dukeText = new Label(DukeStub.getResponse(userInput.getText())); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(userText, new ImageView(user)), + DialogBox.getDukeDialog(dukeText, new ImageView(duke)) + ); + userInput.clear(); +} +``` + +Run the application and play around with it. + +![DialogBoxes Iteration 3](assets/DialogBoxesIteration3.png) + +Congratulations! +You have successfully implemented a fully functional GUI for Duke! + +# Exercises +1. While the GUI looks similar to the mockup, there are still parts that need to be refined. +Try your hand at some of these tasks: + * Add padding between each DialogBox + * Add padding between each ImageView and its Label + * Clip the ImageView into a circle + * Add background color to each dialog box + +1. After attempting the changes, reflect critically on the following: + * What was the development workflow like? + * Is the code base well-organized? diff --git a/tutorials/ui-tutorial-4-introduction-to-fxml.md b/tutorials/ui-tutorial-4-introduction-to-fxml.md new file mode 100644 index 0000000000..828b78494e --- /dev/null +++ b/tutorials/ui-tutorial-4-introduction-to-fxml.md @@ -0,0 +1,175 @@ +# JavaFX Tutorial 4 – Introduction to FXML +This tutorial assumes that you have attempted the exercises at the end of tutorial 3. +You will need to clone the code sample for this tutorial. + +While we have produced a fully functional prototype, there are a few major problems with our application. + +1. The process of visually enhancing the GUI is long and painful: + > * Does the `TextField` need to be 330px or 325px wide? + > * How much padding is enough padding to look good? + + Every small change requires us to rebuild and run the application. + +1. Components are heavily dependent on each other: + > Why does `Main` need to know that `DialogBox` needs a `Label`? + > What happens if we change the `Label` to a custom `ColoredLabel` in the future? + + We need to minimize the amount of information each control needs to know about another. + Otherwise, making changes in the future will break existing features. + +1. The code is untidy and long: + > Why is all the code in one place? + + The `Main` class attempts to do it all. + Code for visual tweaks, listeners and even utility methods are all in one file. + This makes it difficult to find and make changes to existing code. + +# The solution +FXML is a XML-based language that allows us to define our user interface. +Properties of JavaFX objects can be defined in the FXML file. +For example: +```xml + +``` + +The FXML snippet define a TextField similar to the one that we programmatically defined previous in Tutorial 2. +Notice how concise FXML is compared to the plain Java version. + +Let's return to Duke and convert it to use FXML instead. + +# Rebuilding our scenes +Scene Builder is a tool developed by Oracle and currently maintained by Gluon. +It is a What-You-See-Is-What-You-Get GUI creation tool. +[Download](https://gluonhq.com/products/scene-builder/#download) the appropriate version for your OS and install it: + +Let’s explore the provided FXML files in Scene Builder. +Running the tool brings up the main screen. +Select `Open Project` > `src/main/resources/view/MainWindow.fxml`. +Inspect each control and its properties. + +![SceneBuilder opening MainWindow.fxml](assets/SceneBuilder.png) + +On the right accordion pane, you can modify the properties of the control that you have selected. +Try changing the various settings and see what they do! + +On the left accordion, you can see that we have set the controller class to `MainWindow`. +We will get to that later. + +![Controller for MainWindow](assets/MainWindowController.png) + +Let’s repeat the process for `DialogBox`. +The main difference here is that DialogBox checks `Use fx:root construct` and _does not define a controller class_. + + +![Settings for DialogBox](assets/DialogBoxController.png) + +# Structure of a Controller. +Let's take a closer look at the `MainWindow` class that we specified as the controller in the FXML file: + +```java +/** + * Controller for MainWindow. Provides the layout for the other controls. + */ +public class MainWindow extends AnchorPane { + @FXML + private ScrollPane scrollPane; + @FXML + private VBox dialogContainer; + @FXML + private TextField userInput; + @FXML + private Button sendButton; + + private Image user = new Image(this.getClass().getResourceAsStream("/images/DaUser.png")); + private Image duke = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png")); + + @FXML + public void initialize() { + scrollPane.vvalueProperty().bind(dialogContainer.heightProperty()); + } + + /** Creates two dialog boxes, one echoing user input and the other containing Duke's reply and then appends them to + * the dialog container. Clears the user input after processing. + */ + @FXML + private void handleUserInput() { + String input = userInput.getText(); + String response = DukeStub.getResponse(input); + dialogContainer.getChildren().addAll( + DialogBox.getUserDialog(input, user), + DialogBox.getDukeDialog(response, duke) + ); + userInput.clear(); + } + +} +``` + +As the controller, `MainWindow` is solely responsible for the logic behind the application. + +The `@FXML` annotation marks a `private` or `protected` member and makes it accessible to FXML despite its modifier. +Without the annotation, we will have to make everything `public` and expose our UI to unwanted changes. + +The `FXMLLoader` will map the a control with a `fx:id` defined in FXML to a variable with the same name in its controller. +Notice how in `MainWindow`, we can invoke `TextField#clear()` on `userInput` and access its content just as we did in the previous example. +Similarly, methods like private methods like `handleUserInput` can be used in FXML when annotated by `@FXML`. + +# Using FXML in our application + +We load a FXML file using a `FXMLLoader`. +Note how we use `getResource` instead of using `InputStreams` from `java.io` as our resources will no longer be accessible through file IO when we eventually package our application into an executable `jar`. + +```java +@Override +public void start(Stage stage) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml")); + AnchorPane ap = (AnchorPane) fxmlLoader.load(); + Scene scene = new Scene(ap); + stage.setScene(scene); + stage.show(); + } catch (IOException e) { + e.printStackTrace(); + } +} +``` + +Again, we can interact with the `AnchorPane` defined in the FXML as we would have if we created the `AnchorPane` ourselves. + +For our custom `DialogBox`, we did not define a controller. +Looking at the code for the `DialogBox` class: + +```java +public class DialogBox extends HBox { + @FXML + private Label dialog; + @FXML + private ImageView displayPicture; + + private DialogBox(String text, Image img) { + try { + FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("/view/DialogBox.fxml")); + fxmlLoader.setController(this); + fxmlLoader.setRoot(this); + fxmlLoader.load(); + } catch (IOException e) { + e.printStackTrace(); + } + + dialog.setText(text); + displayPicture.setImage(img); + } + // ... +} +``` + +When we create a new instance of `DialogBox`, we set both the controller and root Node to `DialogBox`. +From this point onwards we can interact with `DialogBox` as we have in the previous tutorials. + +[todo]: # (Discussion on the fx:root pattern.) + +# Exercise + +1. Convert `MainWindow` to use the `fx:root` construct. +1. Extend `MainWindow` to have a `Stage` as a root Node. +1. Customize the appearance of the application further with CSS. \ No newline at end of file