diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java index 5d313334..7c9e42aa 100644 --- a/src/main/java/Duke.java +++ b/src/main/java/Duke.java @@ -1,10 +1,61 @@ +import duke.command.Command; +import duke.DukeException; +import duke.Storage; +import duke.command.CommandResult; +import duke.command.Exit; +import duke.parser.Parser; +import duke.task.TaskList; +import duke.Ui.UserInterface; + + public class Duke { + UserInterface ui; + TaskList taskList; + Storage storage; + + public Duke() { + this.ui = new UserInterface(); + this.taskList = new TaskList(); + this.storage = new Storage(); + } + + public void run() { + ui.logo(); + ui.showWelcomeMessage(); + this.userCommand(); + ui.showGoodbyeMessage(); + System.exit(0); + } + + public void userCommand() { + boolean isExit = false; + while (!isExit) { + String fullCommand = ui.getInput(); + Command c = (new Parser()).parseCommand(fullCommand); + CommandResult result = execute(c); + ui.printRespond(result); + isExit = Exit.isExitCommand(c); + } + } + + + public CommandResult execute (Command command) { + command.inputData(taskList); + storage.save(taskList); + return command.execute(); + } + + + public String getResponse(String userInput) { + Command command = (new Parser()).parseCommand(userInput); + CommandResult result = execute(command); + return ui.getResponse(result); + } + public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); + Duke duke = new Duke(); + duke.run(); + } + } diff --git a/src/main/java/duke/DukeException.java b/src/main/java/duke/DukeException.java new file mode 100644 index 00000000..e9307dcf --- /dev/null +++ b/src/main/java/duke/DukeException.java @@ -0,0 +1,11 @@ +package duke; + +/** + * Represents exception class + */ + +public class DukeException extends Exception { + public DukeException(String e){ + super(e); + } +} diff --git a/src/main/java/duke/Storage.java b/src/main/java/duke/Storage.java new file mode 100644 index 00000000..28c378f5 --- /dev/null +++ b/src/main/java/duke/Storage.java @@ -0,0 +1,109 @@ +package duke; + +import duke.task.*; + +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * Stores the TaskList + */ + +public class Storage { + Path filePath; + Path folderPath; + + public Storage() { + String home = System.getProperty("user.home"); + filePath = Paths.get(home, "dukeData", "duke.txt"); + folderPath = Paths.get(home, "dukeData"); + + } + public void fileLocation() throws IOException { + if (Files.notExists(folderPath)) { + Files.createDirectory(folderPath); + } + if (Files.notExists(filePath)) { + Files.createFile(filePath); + } + } + /** + * Read the task list + * @return task list + */ + + public TaskList read() { + TaskList taskList = null; + try { + fileLocation(); + List tasks = Files.readAllLines(filePath, Charset.defaultCharset()); + taskList = this.TaskListDecoder(tasks); + } catch (IOException e) { + e.printStackTrace(); + } + return taskList; + } + + /** + * Stores a task list in a filePath + * @param taskList to store task list + */ + + public void save(TaskList taskList) { + try { + List encodedTasks = this.TaskListEncoder(taskList); + Files.write(filePath, encodedTasks); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Returns an encoded String task list to store. + * @return a String task list + */ + + public List TaskListEncoder (TaskList taskList) { + List TaskEncoder = new ArrayList<>(); + for (Task t : taskList.getList()) { + TaskEncoder.add(t.taskEncode()); + } + return TaskEncoder; + } + + /** + * Returns a decoded task list to use + * If the text is an empty String, an empty ArrayList will be returned as expected. + * @param TaskEncoder the encoded task list + * @return encoded task list + */ + + + public TaskList TaskListDecoder(List TaskEncoder) { + List TaskDecoder = new ArrayList<>(); + for (String s : TaskEncoder) { + switch(s.charAt(0)) { + case ('T'): + TaskDecoder.add(Todo.decode(s)); + break; + case ('D'): + TaskDecoder.add(Deadlines.decode(s)); + break; + case ('E'): + TaskDecoder.add(Event.decode(s)); + break; + default: + break; + } + } + return new TaskList(TaskDecoder); + } + +} + + diff --git a/src/main/java/duke/Ui/UserInterface.java b/src/main/java/duke/Ui/UserInterface.java new file mode 100644 index 00000000..6e1e9123 --- /dev/null +++ b/src/main/java/duke/Ui/UserInterface.java @@ -0,0 +1,50 @@ +package duke.Ui; + +import duke.command.CommandResult; + +import java.util.Scanner; + +public class UserInterface { + + Scanner s; + public UserInterface() { + this.s = new Scanner(System.in); + } + + public String getInput() { + return s.nextLine(); + } + + + public String getResponse(CommandResult result) { + return result.getRespondToUser(); + } + + public void printRespond(CommandResult result) { + System.out.println(result.getRespondToUser()); + } + + public void goodBye() { + System.out.println("Bye"); + } + + + public void showWelcomeMessage() { + System.out.println("Hello! I'm Duke\n" + + "What can I do for you?"); + } + + public void logo() { + String logo = " ____ _ \n" + + "| _ \\ _ _| | _____ \n" + + "| | | | | | | |/ / _ \\\n" + + "| |_| | |_| | < __/\n" + + "|____/ \\__,_|_|\\_\\___|\n"; + System.out.println("Hello from\n" + logo); + } + + public void showGoodbyeMessage() { + System.out.println("Bye :)"); + } + +} diff --git a/src/main/java/duke/command/Command.java b/src/main/java/duke/command/Command.java new file mode 100644 index 00000000..0e042b47 --- /dev/null +++ b/src/main/java/duke/command/Command.java @@ -0,0 +1,37 @@ +package duke.command; + +import duke.task.Task; +import duke.task.TaskList; +/** + * Represents a command + */ +public abstract class Command { + public TaskList taskList; + /** + * Executes the command and returns the result. + */ + + public abstract CommandResult execute(); + + /** + * Input data so command can work + * @param taskList the single taskList used in Duke + */ + + public void inputData(TaskList taskList) { + this.taskList = taskList; + } + + /** + * Return the successful message + * @param task + * @return answers to the user + */ + + public String addTaskSuccess(Task task) { + assert(taskList != null); + return "Got it. I have added the task!\n" + task + + "Now you have " + taskList.getSize() + " tasks in the list."; + + } +} diff --git a/src/main/java/duke/command/CommandResult.java b/src/main/java/duke/command/CommandResult.java new file mode 100644 index 00000000..777e9321 --- /dev/null +++ b/src/main/java/duke/command/CommandResult.java @@ -0,0 +1,22 @@ +package duke.command; + +import duke.task.TaskList; +/** + * Represents a command result + */ +public class CommandResult { + String respondToUser; + TaskList taskList; + + public CommandResult(String respondToUser){ + this.respondToUser = respondToUser; + } + + public String getRespondToUser() { + return respondToUser; + } + + public TaskList getTaskList() { + return taskList; + } +} diff --git a/src/main/java/duke/command/DeadlinesCommand.java b/src/main/java/duke/command/DeadlinesCommand.java new file mode 100644 index 00000000..0ab5fa2d --- /dev/null +++ b/src/main/java/duke/command/DeadlinesCommand.java @@ -0,0 +1,41 @@ +package duke.command; + +import duke.task.Deadlines; +import duke.DukeException; +import duke.task.Task; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +/** + * Adds a deadline to the taskList + */ + +public class DeadlinesCommand extends Command { + public static final String COMMAND_WORD = "deadline"; + String taskName; + LocalDate on; + + /** + * creates a DeadlinesCommand + * @param Text input by the user + * @throws DukeException if the command is empty + */ + + public DeadlinesCommand(String Text) throws DukeException, DateTimeParseException { + String[] deadParts = Text.split(" /by "); + if (deadParts[0].equals("") || deadParts[1].equals("")) { + throw new DukeException("The description of an deadline cannot be empty"); + } else { + this.taskName = deadParts[0]; + this.on = LocalDate.parse(deadParts[1]); + } + } + + @Override + public CommandResult execute() { + Task newDeadlines = new Deadlines(taskName, false, on); + taskList.addTasks(newDeadlines); + return new CommandResult(addTaskSuccess(newDeadlines)); + } +} diff --git a/src/main/java/duke/command/Delete.java b/src/main/java/duke/command/Delete.java new file mode 100644 index 00000000..288fafd8 --- /dev/null +++ b/src/main/java/duke/command/Delete.java @@ -0,0 +1,26 @@ +package duke.command; +import duke.task.Task; + +/** + * Delete tasks from the taskList + */ + +public class Delete extends Command { + public static final String COMMAND_WORD = "delete"; + int taskNumToDelete; + + + public Delete(String[] s) { + this.taskNumToDelete = Integer.parseInt(s[1]); + } + @Override + public CommandResult execute() { + Task removeTask = taskList.deleteTasks(taskNumToDelete); + return new CommandResult(Message(removeTask)); + } + public String Message(Task removeTask) { + return "Noted! I've removed this task:\n" + + removeTask + "\n" + + "Now you have " + taskList.getSize() + " tasks in the list."; + } +} diff --git a/src/main/java/duke/command/DoneCommand.java b/src/main/java/duke/command/DoneCommand.java new file mode 100644 index 00000000..ad90ea46 --- /dev/null +++ b/src/main/java/duke/command/DoneCommand.java @@ -0,0 +1,32 @@ +package duke.command; + +import duke.task.Task; + +/** + * Done Command for a task from taskList + */ + +public class DoneCommand extends Command { + public static final String COMMAND_WORD = "done"; + int numToComplete; + + public DoneCommand(String[] words) { + this.numToComplete = Integer.parseInt(words[1]); + } + + /** + * complete the task at the location of numToComplete + * @return CommandResult with specific message + */ + @Override + public CommandResult execute() { + Task completed = taskList.setDone(numToComplete); + return new CommandResult(getDoneSuccessful(completed)); + } + + public String getDoneSuccessful(Task completed) { + return "Marvelous! I've marked this task as done:\n" + + completed + "\n" + + "Now you have " + taskList.getSize() + " tasks in the list."; + } +} diff --git a/src/main/java/duke/command/EventCommand.java b/src/main/java/duke/command/EventCommand.java new file mode 100644 index 00000000..e3f587d0 --- /dev/null +++ b/src/main/java/duke/command/EventCommand.java @@ -0,0 +1,41 @@ +package duke.command; + +import duke.DukeException; +import duke.task.Event; +import duke.task.Task; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +/** + * Adds an event to the taskList + */ + +public class EventCommand extends Command { + public static final String COMMAND_WORD = "event"; + String taskName; + LocalDate on; + + /** + * Creates an EventCommand + * @param Text input by the user + * @throws DukeException if the command is empty + */ + + public EventCommand(String Text) throws DukeException, DateTimeParseException { + String[] eventParts = Text.split(" /on "); + if (eventParts[0].equals("") || eventParts[1].equals("")) { + throw new DukeException("The description of an event cannot be empty"); + } else { + this.taskName = eventParts[0]; + this.on = LocalDate.parse(eventParts[1]); + } + } + + @Override + public CommandResult execute() { + Task newEvent = new Event(taskName, false, on); + taskList.addTasks(newEvent); + return new CommandResult(addTaskSuccess(newEvent)); + } +} diff --git a/src/main/java/duke/command/Exit.java b/src/main/java/duke/command/Exit.java new file mode 100644 index 00000000..63df5080 --- /dev/null +++ b/src/main/java/duke/command/Exit.java @@ -0,0 +1,21 @@ +package duke.command; + +/** + * Exit from the chat + */ + +public class Exit extends Command { + public static final String COMMAND_WORD = "bye"; + public static final String BYE_MESSAGE = "Bye, hope to see you again"; + + @Override + public CommandResult execute() { + return new CommandResult(BYE_MESSAGE); + } + + public static boolean isExitCommand(Command c) { + return c instanceof Exit; + } +} + + diff --git a/src/main/java/duke/command/Find.java b/src/main/java/duke/command/Find.java new file mode 100644 index 00000000..65bc1b6d --- /dev/null +++ b/src/main/java/duke/command/Find.java @@ -0,0 +1,34 @@ +package duke.command; + +import duke.DukeException; +import duke.task.TaskList; +/** + * Find the task according to keyword from the taskList + */ + +public class Find extends Command { + public static final String COMMAND_WORD = "find"; + String keyword; + + /** + * Creates a Find Command + * @param Text input by the user + * @throws DukeException if the Text is empty + */ + public Find(String Text) throws DukeException { + if (Text.equals("")) { + throw new DukeException("The find command need to follow the specific standard."); + } else { + this.keyword = Text; + } + } + + /** + * Find tasks that matches the user input keyword + * @return CommandResult with the list of matching tasks + */ + @Override + public CommandResult execute() { + return new CommandResult(TaskList.findTask(keyword)); + } +} diff --git a/src/main/java/duke/command/IncorrectCommand.java b/src/main/java/duke/command/IncorrectCommand.java new file mode 100644 index 00000000..eca0d5e6 --- /dev/null +++ b/src/main/java/duke/command/IncorrectCommand.java @@ -0,0 +1,18 @@ +package duke.command; + +/** + * Pop put incorrect message + */ + +public class IncorrectCommand extends Command{ + String errorMessage; + + public IncorrectCommand(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public CommandResult execute() { + return new CommandResult(errorMessage); + } +} diff --git a/src/main/java/duke/command/List.java b/src/main/java/duke/command/List.java new file mode 100644 index 00000000..65efed61 --- /dev/null +++ b/src/main/java/duke/command/List.java @@ -0,0 +1,18 @@ +package duke.command; + +/** + * Command to list tasks + */ + +public class List extends Command { + public static final String COMMAND_WORD = "list"; + + /** + * list all the tasks in taskList + * @return taskList + */ + @Override + public CommandResult execute() { + return new CommandResult(taskList.getAllTasks()); + } +} diff --git a/src/main/java/duke/command/TodoCommand.java b/src/main/java/duke/command/TodoCommand.java new file mode 100644 index 00000000..ba8530a7 --- /dev/null +++ b/src/main/java/duke/command/TodoCommand.java @@ -0,0 +1,36 @@ +package duke.command; + +import duke.DukeException; +import duke.task.Task; +import duke.task.Todo; + + +public class TodoCommand extends Command { + public static final String COMMAND_WORD = "todo"; + String taskName; + + /** + * Creates a TodoCommand + * @param Text input by the user + * @throws DukeException if the Text is empty + */ + public TodoCommand(String Text) throws DukeException { + if (Text.equals("")) { + throw new DukeException("The description of a todo cannot be empty."); + } else { + this.taskName = Text; + } + } + + /** + * Creates a to-do and adds it to the taskList + * @return CommandResult with the success message + */ + @Override + public CommandResult execute() { + Task newTodo = new Todo(taskName, false); + taskList.addTasks(newTodo); + return new CommandResult(addTaskSuccess(newTodo)); + } + +} \ No newline at end of file diff --git a/src/main/java/duke/parser/Parser.java b/src/main/java/duke/parser/Parser.java new file mode 100644 index 00000000..78247e3e --- /dev/null +++ b/src/main/java/duke/parser/Parser.java @@ -0,0 +1,57 @@ +package duke.parser; + +import duke.DukeException; +import duke.command.*; + + +import java.time.format.DateTimeParseException; + +/** + * Parses user input. + */ +public class Parser { + + public Command parseCommand(String userCommandText) { + String[] words = userCommandText.split(" "); + String commandWord = words[0]; + String Text = words.length == 1 ? "" : Parser.getArguments(words); + + try { + switch (commandWord) { + case List.COMMAND_WORD: + return new List(); + case DoneCommand.COMMAND_WORD: + return new DoneCommand(words); + case Delete.COMMAND_WORD: + return new Delete(words); + case TodoCommand.COMMAND_WORD: + return new TodoCommand(Text); + case DeadlinesCommand.COMMAND_WORD: + return new DeadlinesCommand(Text); + case EventCommand.COMMAND_WORD: + return new EventCommand(Text); + case Exit.COMMAND_WORD: + return new Exit(); + case Find.COMMAND_WORD: + return new Find(Text); + default: + throw new DukeException("Sorry, I do not understand what you mean."); + + } + + } catch (DukeException | DateTimeParseException e) { + return new IncorrectCommand(e.getMessage()); + } + } + + /** + * Combines a String array from index 1 to (length - 1) + */ + public static String getArguments(String[] parts) { + String result = parts[1]; + for (int i = 2; i < parts.length; i++) { + result += " " + parts[i]; + } + return result; + } +} diff --git a/src/main/java/duke/task/Deadlines.java b/src/main/java/duke/task/Deadlines.java new file mode 100644 index 00000000..d577860a --- /dev/null +++ b/src/main/java/duke/task/Deadlines.java @@ -0,0 +1,53 @@ +package duke.task; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Represents a deadline task + */ + +public class Deadlines extends Task { + protected LocalDate by; + + public Deadlines(String description, boolean isDone, LocalDate by) { + super(description, isDone); + this.description = description; + this.isDone = isDone; + this.by = by; + } + + public String getDate() { + return this.by.format(DateTimeFormatter.ofPattern("MMM dd yyyy")); + } + + public static Deadlines decode(String s) { + String[] sentence = s.split(","); + boolean decodeIsDone = sentence[1].equals("1"); + LocalDate decodeDate = LocalDate.parse(sentence[3], DateTimeFormatter.ofPattern("MMM dd yyyy")); + return new Deadlines(sentence[2], decodeIsDone, decodeDate); + } + + @Override + public String toString() { + return "[D]" + super.toString() + " (by: " + by + ")"; + } + + public Task complete() { + return new Deadlines(this.getName(),true,by); + } + + @Override + public String taskEncode() { + int encodeIsDone = this.getIsDone() ? 1 : 0; + return "D," + encodeIsDone + "," + description + "," + this.getDate(); + } + + + @Override + public Task setName(String newName) { + return new Deadlines(newName, isDone,by); + } +} + + diff --git a/src/main/java/duke/task/Event.java b/src/main/java/duke/task/Event.java new file mode 100644 index 00000000..4b5e1733 --- /dev/null +++ b/src/main/java/duke/task/Event.java @@ -0,0 +1,48 @@ +package duke.task; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +public class Event extends Task { + LocalDate on; + + public Event(String description, boolean isDone, LocalDate on) { + super(description,isDone); + this.on = on; + } + + public static Event decode(String s) { + String[] sentence = s.split(","); + boolean decodeIsDone = sentence[1].equals("1"); + LocalDate decodeDate = LocalDate.parse(sentence[3], DateTimeFormatter.ofPattern("MMM dd yyyy")); + return new Event(sentence[2], decodeIsDone, decodeDate); + } + @Override + public String taskEncode () { + int encodeIsDone = this.getIsDone() ? 1:0; + return "E" + encodeIsDone + "," + this.getName() + "," + this.getDate(); + } + + + public String getDate() { + return this.on.format(DateTimeFormatter.ofPattern("MMM dd yyyy")); + } + @Override + public Task complete() { + return new Event(this.getName(), true, this.on); + } + + + + @Override + public Task setName(String New) { + return new Event(New, isDone, this.on); + } + @Override + public String toString() { + String taskString = super.toString(); + String result = "[E]" + taskString + " (on: " + this.on + ")"; + return result; + } + + +} diff --git a/src/main/java/duke/task/Task.java b/src/main/java/duke/task/Task.java new file mode 100644 index 00000000..81ef63c1 --- /dev/null +++ b/src/main/java/duke/task/Task.java @@ -0,0 +1,44 @@ +package duke.task; +/** + * Represents a task + */ +public abstract class Task { + + public Task(String description, boolean isDone) { + this.description = description; + this.isDone = isDone; + } + + protected boolean isDone; + protected String description; + + public boolean getIsDone() { + return isDone; + } + + public String getName() { + return description; + } + + public void markAsDone() { + System.out.println("Nice! I've marked this task as done: "); + System.out.println(getStatusIcon()); + } + + public String getStatusIcon() { + + return ("[" + (this.getIsDone() ? "X" : " ") + "]") + " " + this.getName(); // mark done task with X + } + + public abstract Task setName(String name); + public abstract Task complete(); + + public abstract String taskEncode(); + + @Override + public String toString() { + String checkbox = "[" + (this.getIsDone() ? "\u2713" : "\u2718") + "]"; + String result = checkbox + " " + this.getName(); + return result; + } +} diff --git a/src/main/java/duke/task/TaskList.java b/src/main/java/duke/task/TaskList.java new file mode 100644 index 00000000..c72f6112 --- /dev/null +++ b/src/main/java/duke/task/TaskList.java @@ -0,0 +1,70 @@ +package duke.task; + +import duke.task.Task; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a Task list + */ + +public class TaskList { + private static List tasks; + + public TaskList() { + tasks = new ArrayList<>(); + } + + public TaskList(List tasks) { + this.tasks = tasks; + } + + public int getSize() { + return tasks.size(); + } + + public void addTasks(Task newTask) { + tasks.add(newTask); + } + + public Task deleteTasks(int num) { + int index = num - 1; + Task removeTask = tasks.remove(index); + return removeTask; + } + public Task setDone(int num) { + int i = num - 1; + Task current = tasks.get(i); + Task completed = current.complete(); + tasks.set(i, completed); + return completed; + } + + public List getList() { + return tasks; + } + public String getAllTasks() { + String result = "Here are the tasks in your list:"; + for (int i = 0; i < tasks.size(); i++) { + result += "\n"; + Task currentTask = tasks.get(i); + int taskNumber = i + 1; + result += taskNumber + ". " + currentTask; + } + return result; + } + public static String findTask(String keyword) { + String result = "Here are the matching tasks in your list:"; + for (int i = 0; i < tasks.size(); i++) { + Task currentTask = tasks.get(i); + if (currentTask.toString().contains(keyword)) { + result += "\n"; + int taskNumber = i + 1; + result += taskNumber + ". " + currentTask; + } + } + return result; + } + +} diff --git a/src/main/java/duke/task/Todo.java b/src/main/java/duke/task/Todo.java new file mode 100644 index 00000000..46558514 --- /dev/null +++ b/src/main/java/duke/task/Todo.java @@ -0,0 +1,41 @@ +package duke.task; + +/** + * Represents a To do task + */ + +public class Todo extends Task { + public Todo(String name, boolean isDone) { + super(name, isDone); + } + + public static Todo decode(String s) { + String[] sentence = s.split(","); + boolean decodeIsDone = sentence[1].equals("1"); + return new Todo(sentence[2], decodeIsDone); + } + + @Override + public Task complete() { + return new Todo(this.getName(),true); + } + + @Override + public String taskEncode() { + int encodeIsDone = this.getIsDone() ? 1 : 0; + return "T," + encodeIsDone + "," + description; + } + + + @Override + public Task setName(String newName) { + return new Todo(newName, isDone); + } + + @Override + public String toString() { + String taskString = super.toString(); + String result = "[T]" + taskString; + return result; + } +} diff --git a/src/main/resources/view/ChatBox b/src/main/resources/view/ChatBox new file mode 100644 index 00000000..e69de29b diff --git a/tutorials/assets/DaDuke.png b/tutorials/assets/DaDuke.png new file mode 100644 index 00000000..d8936587 Binary files /dev/null and b/tutorials/assets/DaDuke.png differ diff --git a/tutorials/assets/DaUser.png b/tutorials/assets/DaUser.png new file mode 100644 index 00000000..3c82f454 Binary files /dev/null and b/tutorials/assets/DaUser.png differ diff --git a/tutorials/assets/DialogBoxController.png b/tutorials/assets/DialogBoxController.png new file mode 100644 index 00000000..1870dcbc 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 00000000..31e8f39e 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 00000000..aea148d6 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 00000000..adf8ba42 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 00000000..17bfe0d4 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 00000000..ea793725 --- /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 00000000..b56395ce 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 00000000..5e2ec75d 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 00000000..61b0ad4d 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 00000000..3066d4a2 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 00000000..fca5c48d 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 00000000..87228d5c 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 00000000..77c8c001 --- /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 00000000..54f48f12 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 00000000..352fb86e 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 00000000..e580196e 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 00000000..0980c3c5 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 00000000..a29de75a 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 00000000..a8cfded3 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 00000000..94e076c2 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 00000000..02183af4 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 00000000..106484f5 Binary files /dev/null and b/tutorials/assets/SceneBuilder.png differ diff --git a/tutorials/gradleTutorial.md b/tutorials/gradleTutorial.md new file mode 100644 index 00000000..08292b11 --- /dev/null +++ b/tutorials/gradleTutorial.md @@ -0,0 +1,170 @@ +# Gradle Tutorial + +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_. + +* [Introduction](#introduction) +* [Adding Gradle Support to Your Project](#adding-gradle-support-to-your-project) +* [Adding Plugins](#adding-plugins) + * [CheckStyle](#checkstyle) + * [Shadow](#shadow) +* [Adding Dependencies](#adding-dependencies) + * [JUnit](#junit) +* [Further Reading](#further-reading) + +## Introduction + +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. For example, 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. + +* **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_](https://en.wikipedia.org/wiki/Convention_over_configuration), there is not much to you need to configure if you follow the recommended directory structure. + +## Adding Gradle Support to Your Project + +1. Pull the branch named `gradle`. Merge it to the `master` branch. This will add the Gradle wrapper to your project. + ``` + git checkout --track origin/gradle + git checkout master + git merge gradle + ``` +1. Open the `build.gradle` file in an editor. Update the following code block to point to the main class (i.e., the one containing the `main` method) of your application. The code below assumes your main class is `seedu.duke.Duke` + ```groovy + application { + mainClassName = "seedu.duke.Duke" + } + ``` +1. To check if Gradle has been added to the project correctly, open a terminal window, navigate to the root directory of your project and run the command `gradlew run`. This should result in Gradle running the main method of your project. + +:bulb: Simply run the command `gradlew {taskName}` in the terminal and Gradle will run the task! Here are some example commands: +* `gradlew tasks` (or `gradlew tasks --all`): shows a list of tasks available +* `gradlew run`: runs the main class of your project + +:bulb: Some plugins may add more helpful tasks so be sure to check their documentation! + +#### Using Gradle from within Intellij + +1. After adding support for Gradle, Intellij might automatically ask you (via a pop-up at the bottom right corner of the Window) whether to import the project as a Gradle project. In that case, go ahead and say yes. + +1. If the above didn't happen, import the Gradle project by `Help > Find Action > Import Gradle Project`. + ![Import Gradle](assets/ImportGradle.png) + +1. If the above didn't work either, close Intellij, delete the Intellij project files (i.e., `.idea` folder and `*.iml` files), and set up the project again, but instead of choosing `Create project from existing sources`, choose `Import project from external model` -> `Gradle`. + +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. + +You can click on the Gradle icon in the Gradle toolbar and create a new run configuration for running Gradle tasks without needing to type a `gradlew` command. + +![Gradle icon](assets/GradleIcon.png) + +## 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 a set of style rules), which comes as 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' +} + +checkstyle { + toolVersion = '8.23' +} + +// ... +``` + +Checkstyle expects configuration files for checkstyle to be in `./config/checkstyle/` by convention. +A sample checkstyle rule configuration is provided in the branch. + +The plugin adds a few _tasks_ to your project. Run `gradlew checkstyleMain checkstyleTest` to verify that you have set up Checkstyle properly (the command will check your main code and test code against the style rules). + +**Resources**: +* [Gradle documentation for CheckStyle plugin](https://docs.gradle.org/current/userguide/checkstyle_plugin.html) + +### Shadow + +Shadow is a plugin that packages an application into an executable jar file. To use it, first add the following line to your Gradle build file: +```groovy +plugins { + //... + 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 +shadowJar { + archiveBaseName = "duke" + archiveVersion = "0.1.3" + archiveClassifier = null + archiveAppendix = null +} +``` + +Now you can run the command `gradlew shadowJar`.It publishes an executable jar to `./build/libs/`. You should be able to able to execute the created jar file by double-clicking it or using the command `java -jar {jarName}`? + +**Resources**: +* [Gradle documentation for Shadow plugin](https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow) +* [Gradle documentation for JUnit](https://docs.gradle.org/current/userguide/java_testing.html#using_junit5) +* [More about the Shadow plugin](https://imperceptiblethoughts.com/shadow/introduction/) + +## Adding Dependencies + +Gradle can automate the management of dependencies to third-party libraries. Given below are some examples. + +### JUnit + +JUnit is a testing framework for Java. It allows developers to write tests and run them. To manage JUnit dependency via Gradle, add the following 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, java tests belong in `src/test/java` folder. Create a new `test/java` folder in under `src`. +``` +src +├─main +│ └─java +│ └─seedu/duke/Duke.java +└─test + └─java + └─seedu/duke/DukeTest.java +``` + +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 (e.g., `test/java/seedu/duke/DukeTest.java`) and run it with `gradlew test`. + +**Resources**: +* [Gradle documentation for JUnit](https://docs.gradle.org/current/userguide/java_testing.html#using_junit5) + +## 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) + +---------------------------------------------------------------------------------------- +**Authors:** +* Initial Version: Jeffry Lum diff --git a/tutorials/javaFxTutorialPart1.md b/tutorials/javaFxTutorialPart1.md new file mode 100644 index 00000000..6400dfa1 --- /dev/null +++ b/tutorials/javaFxTutorialPart1.md @@ -0,0 +1,117 @@ +# JavaFX Tutorial Part 1 – Introduction + +## JavaFX lifecycle of an application + +Imagine yourself as a director of 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. + +![Hierarchy of Objects in JavaFX](assets/JavaFxHierarchy.png) + +A JavaFX application is like a play you are directing. Instead of creating props, you create `Nodes` (`Nodes` are the fundamental building blocks of a JavaFX application), and place them onto a `Scene` (a scene is a graph of `Node`s). 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. + +## Setting up Java FX + +### If you are not using Gradle + +1. Download the [JavaFX 11 SDK](https://gluonhq.com/products/javafx/) and unzip it. + +1. Import the `libs` folder from the SDK into your project (note: `JAVAFX_HOME` is the directory in which you have unzipped the JavaFX SDK). + + `File` > `Project Structure` > `Libraries` > `+` > `Java` > `{JAVAFX_HOME}/lib` + +1. From `Run` > `Edit Configurations`, add the following line into your `VM options` for each of the `main` classes. + + `--module-path {JAVAFX_HOME}/lib --add-modules javafx.controls,javafx.fxml`
+ e.g., `--module-path C:/javafx-sdk-11.0.2/lib --add-modules javafx.controls,javafx.fxml` + +### If you are using Gradle + +Update your `build.gradle` to include the following lines: +```groovy +repositories { + mavenCentral() +} + +dependencies { + String javaFxVersion = '11' + + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' +} +``` + +## Writing your first program + +As customary, let’s start off with a simple “Hello World” program. Modify your `Duke` class to 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 +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.stage.Stage; + +public class Duke extends Application { + + // ... + + @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. + } +} +``` + +Note how we have created a `Label` to contain the text that we want to show. We then create the `Scene` and set its content. Finally, we set the stage and show it. + +Next, we create another Java class, `Launcher`, as an entry point to our application. +The `Launcher` class is reproduced below in its entirety. + +```java +import javafx.application.Application; + +/** + * A launcher class to workaround classpath issues. + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(Duke.class, args); + } +} +``` + +Run `Launcher` 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? + +-------------------------------------------------------------------------------- +**Authors:** +* Initial Version: Jeffry Lum diff --git a/tutorials/javaFxTutorialPart2.md b/tutorials/javaFxTutorialPart2.md new file mode 100644 index 00000000..f24a0cd6 --- /dev/null +++ b/tutorials/javaFxTutorialPart2.md @@ -0,0 +1,158 @@ +# JavaFX Tutorial Part 2 - Creating a GUI for Duke + +In this tutorial, we will be creating a GUI for Duke from scratch based on the following mockup. + +![Mockup for Duke](assets/DukeMockup.png) + +## 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 +![Label](assets/MockupLabel.png) | Label +![Button](assets/MockupButton.png) | Button +![TextField](assets/MockupTextField.png) | TextField +![ScrollPane](assets/MockupScrollPane.png) | ScrollPane + +## Designing the Layout + +Now that we know what controls we need to implement our UI, let’s start programming! We quickly run into a problem: how do we show all of them on the screen at once? + +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. + +But how do we get the exact layout we want in the UI? 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. + +:bulb: 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) + +To get that layout, 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 +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + + +public class Duke extends Application { + + private ScrollPane scrollPane; + private VBox dialogContainer; + private TextField userInput; + private Button sendButton; + private Scene scene; + + public static void main(String[] args) { + // ... + } + + @Override + public void start(Stage stage) { + //Step 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(); + + // more code to be added here later + } +} +``` + +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? + +## Styling the Controls + +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: + +Add the following code to the bottom of the `start` method. You'll have to add `import javafx.scene.layout.Region;` to the imports too. + +```java + @Override + public void start(Stage stage) { + //Step 1. Formatting the window to look as expected. + + //... + + //Step 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); + + // You will need to import `javafx.scene.layout.Region` for this. + 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); + + // more code to be added here later + } +``` + +Run the application again. It should now look like this: + +![Duke's Final layout](assets/FinalLayout.png) + +## 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? + +-------------------------------------------------------------------------------- +**Authors:** +* Initial Version: Jeffry Lum diff --git a/tutorials/javaFxTutorialPart3.md b/tutorials/javaFxTutorialPart3.md new file mode 100644 index 00000000..a9e1bddd --- /dev/null +++ b/tutorials/javaFxTutorialPart3.md @@ -0,0 +1,243 @@ +# 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. + +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`. Update the `Main` class as follows. You'll need to add an `import javafx.scene.control.Label;` too. +```java +@Override +public void start(Stage stage) { + // Step 1 code here + + //Step 2 code here + + //Step 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) { + // You will need to import `javafx.scene.control.Label`. + 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. + +Update the `start` method as shown below. + +```java +public void start(Stage stage) { + // current code ... + + //Scroll down to the end every time dialogContainer's height changes. + 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. + +Let’s create our custom control `DialogBox`: +```java +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; + +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. Here are the steps to update the code to use the custom control in `Main.java`. + +First, add these imports: +```java +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +``` + +Next, add two images to the `main/resources/images` folder. +For this tutorial, we have two images `DaUser.png` and `DaDuke.png` to represent the user avatar and Duke's avatar respectively but you can use any image you want. + +Image|Filename +---|--- +![DaDuke](assets/DaUser.png) | `DaUser.png` +![DaUser](assets/DaDuke.png) | `DaDuke.png` + + +```java +public class Duke extends Application { + // ... + private Image user = new Image(this.getClass().getResourceAsStream("/images/DaUser.png")); + private Image duke = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png")); + // ... +} +``` + +Add a new method to handle user input: +```java +/** + * 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(getResponse(userInput.getText())); + dialogContainer.getChildren().addAll( + new DialogBox(userText, new ImageView(user)), + new DialogBox(dukeText, new ImageView(duke)) + ); + userInput.clear(); +} + +/** + * You should have your own function to generate a response to user input. + * Replace this stub with your completed method. + */ +private String getResponse(String input) { + return "Duke heard: " + input; +} +``` + +Update the event handler code in the `start` method to use the new `handleUserInput` method: +```java + +@Override +public void start(Stage stage) { + //... + + //Part 3. Add functionality to handle user input. + sendButton.setOnMouseClicked((event) -> { + handleUserInput(); + }); + + userInput.setOnAction((event) -> { + handleUserInput(); + }); +} +``` + +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 +/** + * 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()); + FXCollections.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; +} +``` + +You'll need to update the imports as follows: +```java +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +``` + +Now, we can go back to the `Main` class and change the event handler to use our new `DialogBox`. + +```java +private void handleUserInput() { + Label userText = new Label(userInput.getText()); + Label dukeText = new Label(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? + +-------------------------------------------------------------------------------- +**Authors:** +* Initial Version: Jeffry Lum diff --git a/tutorials/javaFxTutorialPart4.md b/tutorials/javaFxTutorialPart4.md new file mode 100644 index 00000000..0e0ab280 --- /dev/null +++ b/tutorials/javaFxTutorialPart4.md @@ -0,0 +1,294 @@ +# JavaFX Tutorial 4 – FXML + +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. + +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 the Scene using FXML + +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. + +Create the following files in `src/main/resources/view`: + +**MainWindow.fxml** +```xml + + + + + + + + + + + +