diff --git a/.classpath b/.classpath
index 3f05f311a90b..83c7ddd991c7 100644
--- a/.classpath
+++ b/.classpath
@@ -15,7 +15,11 @@
-
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
index 45a20de82e87..1c2133bf6e65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,32 @@ preferences.json
classes/
/data/
/bin/
+
+# Generated class files
+*.class
+
+# Default data file
+addressbook.txt
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# Idea files
+.idea/
+*.iml
+out/
+test/data/
+/bin/
+data/
+publish.sh
+
+# Junit logs
+junit/*.xml
+
+# Eclipse settings
+.settings/*
+
+# DB file
+database.json
diff --git a/.project b/.project
index 1c9339c5f927..e1d86e83cc5f 100644
--- a/.project
+++ b/.project
@@ -1,7 +1,7 @@
- addressbook-level4
- Project addressbook-level4 created by Buildship.
+ GetShitDone
+ Get your shit done!
@@ -20,4 +20,24 @@
org.eclipse.buildship.core.gradleprojectnature
org.eclipse.jdt.core.javanature
+
+
+ 1475919188342
+
+ 26
+
+ org.eclipse.ui.ide.multiFilter
+ 1.0-projectRelativePath-matches-false-false-build
+
+
+
+ 1475919188352
+
+ 26
+
+ org.eclipse.ui.ide.multiFilter
+ 1.0-projectRelativePath-matches-false-false-.gradle
+
+
+
diff --git a/README.md b/README.md
index 249a00b3899c..2e5278f186a6 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,9 @@
-[![Build Status](https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master)](https://travis-ci.org/se-edu/addressbook-level4)
-[![Coverage Status](https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master)](https://coveralls.io/github/se-edu/addressbook-level4?branch=master)
+# GetShitDone
+[![Build Status](https://travis-ci.org/CS2103AUG2016-F11-C1/main.svg?branch=master)](https://travis-ci.org/CS2103AUG2016-F11-C1/main)
+[![Coverage Status](https://coveralls.io/repos/github/CS2103AUG2016-F11-C1/main/badge.svg?branch=master)](https://coveralls.io/github/CS2103AUG2016-F11-C1/main?branch=master)
+[![Codacy Badge](https://api.codacy.com/project/badge/Grade/bb54debec79f4383924b89c9131865fc)](https://www.codacy.com/app/CS2103AUG2016-F11-C1/main?utm_source=github.com&utm_medium=referral&utm_content=CS2103AUG2016-F11-C1/main&utm_campaign=Badge_Grade)
-# Address Book (Level 4)
-
-
-
-* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using
- a CLI (Command Line Interface).
-* It is a Java sample application intended for students learning Software Engineering while using Java as
- the main programming language.
-* It is **written in OOP fashion**. It provides a **reasonably well-written** code example that is
- **significantly bigger** (around 6 KLoC)than what students usually write in beginner-level SE modules.
-* What's different from [level 3](https://github.com/se-edu/addressbook-level3):
- * A more sophisticated GUI that includes a list panel and an in-built Browser.
- * More test cases, including automated GUI testing.
- * Support for *Build Automation* using Gradle and for *Continuous Integration* using Travis CI.
+
#### Site Map
@@ -27,8 +16,15 @@
#### Acknowledgements
-* Some parts of this sample application were inspired by the excellent
- [Java FX tutorial](http://code.makery.ch/library/javafx-8-tutorial/) by *Marco Jakob*.
+* Built on top of SE.edu's sample [Address Book project](https://github.com/se-edu/addressbook-level4).
+
+**Resources used**
+
+* [Emoji One](http://emojione.com/)
+* [jbootx Java Bootstrap CSS](https://github.com/dicolar/jbootx)
+* [Natty](http://natty.joestelmach.com/)
+* [Google Diff-Match-Patch](https://bitbucket.org/cowwoc/google-diff-match-patch/overview)
+#### License
-#### Licence : [MIT](LICENSE)
+[MIT](LICENSE)
diff --git a/build.gradle b/build.gradle
index 46b06c1e42ec..0f7818a4d557 100644
--- a/build.gradle
+++ b/build.gradle
@@ -52,6 +52,12 @@ allprojects {
compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonDataTypeVersion"
compile "com.google.guava:guava:$guavaVersion"
+
+ // https://mvnrepository.com/artifact/com.joestelmach/natty
+ compile group: 'com.joestelmach', name: 'natty', version: '0.12'
+
+ // https://mvnrepository.com/artifact/org.bitbucket.cowwoc/diff-match-patch
+ compile group: 'org.bitbucket.cowwoc', name: 'diff-match-patch', version: '1.1'
testCompile "junit:junit:$junitVersion"
testCompile "org.testfx:testfx-core:$testFxVersion"
diff --git a/collated/docs/A0093907W.md b/collated/docs/A0093907W.md
new file mode 100644
index 000000000000..23c12f5bb0a3
--- /dev/null
+++ b/collated/docs/A0093907W.md
@@ -0,0 +1,305 @@
+# A0093907W
+###### \DeveloperGuide.md
+``` md
+
+### Controller component
+
+
+
+The Controllers are responsible for most of the back-end logic responsible for processing the user's input. They take in the full input command, parse, process, and construct the response messages which are handed over to the Renderer to be rendered on the View.
+
+**API** : [`Controller.java`](../src/main/java/seedu/todo/logic/Logic.java)
+
+1. `Controller`s have a `process()` method which processes the command passed in by `InputHandler`.
+2. The command execution can affect the `Model` (e.g. adding a person), raise events, and/or have other auxilliary effects like updating the config or modifying the UI directly.
+3. After doing the required processing, the `Controller` calls the `Renderer` concern with appropriate parameters to be rendered on the user window. This is regardless of whether the command was successful (if not, then an error message or disambiguation prompt is rendered).
+
+#### Controller concerns
+
+Controller concerns are intended to contain helper methods to be shared across Controllers, in the spirit of code reuse. These are methods that are not generic enough to be considered utility functions (in `commons.utils`), but are at the same time not specific to a single Controller.
+
+A brief description of each concern:
+
+* **`CalendarItemFilter`** extracts out the parsing and filtering logic that is used by `ListController`, `ClearController` and to a small extent, `FindController`. These controllers depend on being able to filter out CalendarItems before doing some processing on it. Extracting this out into a concern allows us to maintain a consistent filtering syntax for the user.
+* **`Disambiguator`** contains the disambiguation helper methods to be used by Controllers which rely heavily on CalendarItemFilter. Since the token parsing is extracted out into a common concern, so should the code for populating disambiguation fields.
+* **`DateParser`** extracts out the parsing methods for single and paired dates. Virtually all Controllers need some support for converting a natural date input to a LocalDateTime object.
+* **`Renderer`** contains the bulk of the code required for renderering a success or failure message, as well as disambiguation prompts. We want disambiguation prompts from all Controllers to be more or less consistent in their wording, hence it makes sense to extract this out allow each Controller to provide a more detailed explanation that will be rendered together with the generic message.
+* **`Tokenizer`** contains the heavy logic that parses an input into its component token keys and values, while respecting the presence of quotes. All but the simplest of Controllers need to use this for parsing user input. Each Controller defines its own tokenDefinitions which the `Tokenizer` uses to parse the raw user input.
+
+### Model component
+
+**API** : [`CalendarItem.java`](../src/main/java/seedu/todo/models/CalendarItem.java)
+
+A Model represents a single database record that is part of the persistent state of the TodoList app.
+
+`CalendarItem`
+* is subclassed by two record types, namely `Event` and `Task`
+* Both subclasses contain setters and getters to be used to manipulate records
+* Both subclasses implement dynamic predicate constructors to be chained together for use in a `.where()` query
+* Has **NO** support for dirty records. In the spirit of Java's LBYL (and against my personal preferences...), all Controllers doing database operations are expected to validate parameters before updating a record. Once a record field is changed, if a validation fails, the only way to rollback the change is by reloading from disk or calling `undo`.
+
+`TodoListDB`
+* is a class that holds the entire persistent database for the TodoList app
+* is a singleton class. For obvious reasons, the TodoList app should not be working with multiple DB instances simultaneously
+* is recursively serialized to disk - hence object-to-object dynamic references should not be expected to survive serialization/deserialization
+
+### Storage component
+
+
+
+**API** : [`Storage.java`](../src/main/java/seedu/todo/storage/Storage.java)
+
+The `Storage` module should be considered to be a black box which provides read/write functionality and a few bonus features to the TodoListDB. This can be compared to a MySQL database implementation - no one needs to know how this is implemented, and in actual fact our implementation does little more than wrap around a serializer / deserializer in order to provide undo/redo functionality.
+
+The `Storage` component,
+* holds the logic for saving and loading the TodoListDB from disk
+* maintains the required information to undo/redo the state of the TodoListDB in steps. One step represents the changes made in a single atomic transaction
+* will discard all redo information the moment a new operation (i.e. not `redo`) is committed
+
+*Some notes on the `JsonStorage` implementation of `Storage`*:
+* The undo/redo information is stored using a stack of memory-efficient diffs containing the required patches to the data. When we undo, we construct a diff in the opposite direction so that we can redo.
+* Average case time complexity for an undo/redo operation is constant with undo/redo history, linear with DB size.
+* The space complexity of the undo/redo operation is constant with the DB size (this is the reason we are able to support up to 1000 undo/redos even though Jim likely isn't that much of a keyboard warrior).
+```
+###### \DeveloperGuide.md
+``` md
+
+#### Use case : UC12 - Undo command
+
+**MSS**
+
+1. User requests to undo a specified number of commands which defaults to 1.
+2. Application undoes this number of commands.
+Use case ends.
+
+**Extensions**
+
+1a. The given number exceeds the total number of possible undo states.
+> Application shows an error message.
+Use case ends.
+
+#### Use case : UC13 - Redo command
+
+**MSS**
+
+1. User requests to redo a specified number of commands which defaults to 1.
+2. Application redoes this number of commands.
+Use case ends.
+
+**Extensions**
+
+1a. The given number exceeds the total number of possible redo states.
+> Application shows an error message.
+Use case ends.
+
+```
+###### \DeveloperGuide.md
+``` md
+#### Use case : UC18 - Config
+
+**MSS**
+
+1. User requests to set a config variable.
+2. Application sets the config variable.
+Use case ends.
+
+**Extensions**
+
+1a. The config variable key does not exist.
+> 1a1. Application shows an error message.
+Use case ends.
+
+1b. The config variable value is invalid.
+> 1b1. Application shows an error message.
+Use case ends.
+```
+###### \DeveloperGuide.md
+``` md
+#### Use case: UC21 - Clear all tasks and events
+
+**MSS**
+
+1. User requests to clear all tasks and events
+2. Application clears all tasks and events
+Use case ends
+
+#### Use case: UC22 - Clear by type
+
+**MSS**
+
+1. User requests to clear by task/event.
+2. Application clears all tasks/events.
+Use case ends.
+
+**Extensions**
+
+1a. User specifies an invalid type.
+> 1a1. Application shows an error message.
+Use case ends.
+
+#### Use case: UC23 - Clear by date
+
+**MSS**
+
+1. User requests to clear by date range.
+2. Application clears all records in the date range.
+Use case ends.
+
+**Extensions**
+
+1a. User enters unparseable invalid date.
+> 1a1. Application prompts for disambiguation.
+> 1a2. User enters parseable date.
+> 1a3. Application clears all records in the date range.
+Use case ends.
+
+1b. There are no records in the date range.
+> 1b1. Application shows error message.
+Use case ends.
+
+#### Use case: UC24 - Clear by tags
+
+**MSS**
+
+1. User requests to clear by tag.
+2. Application clears all records with the specified tag.
+Use case ends.
+
+**Extensions**
+
+2a. There are no records matching the tag.
+> 2a1. Application shows error message.
+Use case ends.
+
+```
+###### \UserGuide.md
+``` md
+
+# User Guide
+
+Please refer to the [Setting up](DeveloperGuide.md#setting-up) section to learn how to set up the project.
+
+
+
+## Contents
+
+* [Features](#features)
+* [FAQ](#faq)
+* [Command Summary](#command-summary)
+
+## Features
+
+> **Command Format**
+> * Words in `UPPER_CASE` are the parameters.
+> * Items in `SQUARE_BRACKETS` are optional.
+> * Items with `...` after them can have multiple instances.
+> * The order of optional parameters are flexible.
+
+#### Viewing help : `help`
+
+Shows a list of all commands in GetShitDone.
+
+Format: `help`
+
+Examples:
+
+* `help`
+ Shows all available commands and examples
+
+#### Adding a task: `add` or `add task`
+
+Adds a task to GetShitDone
+
+Format: `add [task] NAME [(by|on|at|before|time) DEADLINE]`
+
+> * Tasks can have a deadline, or can do without one as well.
+> * Tasks added without specifying a deadline will be displayed under "No Deadline".
+> * Date formats can be flexible. The application is able to parse commonly-used human-readable date formats.
+> * e.g. `1 Oct`, `Monday`, `next wed`, `tomorrow`, `5 days ago`, etc.
+> * Dates can include time as well.
+> * If only time is specified, it will default to today's date.
+> * If time is not specified, it will default to the current time of the particular date.
+> * Time formats are flexible as well. The application supports 24 hour format and AM/PM format.
+> * e.g. `Monday 3pm`, `today 1930`, `5:30pm`, `10.00 am`
+> * Tasks can have any number of tags up to 20. (including 0).
+> * Using the `add` command without specifying `task` will interpret the command as `add task`.
+
+Examples:
+
+ * `add CS2103 Project`
+ * `add CS2103 V0.3 by next Friday`
+ * `add task Buy milk by tmr`
+
+#### Adding an event: `add event`
+
+Adds an event to GetShitDone
+
+Format: `add event NAME from STARTDATETIME to ENDDATETIME`
+
+> * Events must have both start and end date/time specified.
+> * If there is no start or end date, you have to rectify your command, since it wasn't clear what should be added.
+> * If only time is given, the date is interpreted as today's date.
+> * If only date is given, the time is interpreted as the time now.
+
+Examples:
+
+ * `add event Orientation Camp from Monday 8am to Friday 9pm`
+ * `add event CS2103 Workshop from Sat 10am to 4pm`
+
+```
+###### \UserGuide.md
+``` md
+
+#### Finding all tasks/events containing any keyword in their name & tag: `find`
+
+Finds tasks whose name contains any of the given keywords.
+
+Format: `find KEYWORD [MORE_KEYWORDS]...`
+
+> The search is not case sensitive, the order of the keywords does not matter, only the item name is searched, and tasks/events matching at least one keyword will be returned (i.e. `OR` search).
+> Searching follows wildcard search, i.e. a search term of `pr` will return both `Print notes` and `Make PR to GitHub`.
+
+Examples:
+
+* `find assignment`
+Returns tasks and events which contain words starting with `assignment`.
+
+* `find assignment cs`
+Returns tasks and events which contain words starting with `assignment` or `cs`.
+
+#### Editing a task : `update`
+
+Edits the specified task from GetShitDone.
+
+Format: `update INDEX [name NAME] [( (by|on|at|before) DATE] | from STARTDATE to ENDDATE )]`
+
+> Edits the task at the specified `INDEX`. The index refers to the index number shown in the most recent listing.
+
+Examples:
+
+* `update 2 name Presentation`
+ Update the 1st task's/event's name to CS2107 Project.
+
+* `update 1 name CS2107 Project by saturday`
+ Update the 1st task's name to CS2107 Project.
+ Change the task's deadline to Saturday.
+
+#### Deleting a task : `destroy`
+
+Deletes the specified task from GetShitDone.
+
+Format: `destroy INDEX`
+
+> Deletes the task at the specified `INDEX`.
+ The index refers to the index number shown in the most recent listing.
+
+Examples:
+
+* `destroy 3`
+ Deletes the 3rd task/event in GetShitDone.
+
+* `find assignment2`
+ `destroy 1`
+ Deletes the 1st task/event in the results of the `find` command.
+
+```
diff --git a/collated/docs/A0139812A.md b/collated/docs/A0139812A.md
new file mode 100644
index 000000000000..4723ac8f554c
--- /dev/null
+++ b/collated/docs/A0139812A.md
@@ -0,0 +1,409 @@
+# A0139812A
+###### \DeveloperGuide.md
+``` md
+
+### Architecture
+
+
+The **_Architecture Diagram_** given above explains the high-level design of the App.
+Given below is a quick overview of each component.
+
+`Main` has only one class called [`MainApp`](../src/main/java/seedu/address/MainApp.java). It is responsible for,
+* At app launch: Initializes the components in the correct sequence, and connect them up with each other.
+* At shut down: Shuts down the components and invoke cleanup method where necessary.
+
+[**`Commons`**](#common-classes) represents a collection of classes used by multiple other components.
+Three of those classes play important roles at the architecture level.
+
+* `EventsCenter` : This class (written using [Google's Event Bus library](https://github.com/google/guava/wiki/EventBusExplained))
+ is used by components to communicate with other components using events (i.e. a form of _Event Driven_ design)
+* `LogsCenter` : Used by many classes to write log messages to the App's log file.
+* `ConfigCenter` : Used by many classes to access and save the config
+* `EphemeralDB` : Used by the UI as well as the Controller, so that the Controller is able to refer to items in the UI level. One example would be for the controller to get the index each item was listed, since the ordering of items is only determined at the UI level.
+
+The rest of the App consists of the following.
+
+* [**`UI`**](#ui-component) : The UI of tha App.
+* [**`InputHandler`**](#inputhandler-component) : The command receiver.
+* [**`Controller`**](#controller-component) : The command executor.
+* [**`Model`**](#model-component) : Holds the data of the App in-memory.
+* [**`Storage`**](#storage-component) : Reads data from, and writes data to, the hard disk.
+
+Each of the components defines an _API_ in an `interface` with the same name as the component.
+
+The sections below give more details of each component.
+
+### UI component
+
+
+
+**API** : [`Ui.java`](../src/main/java/seedu/todo/ui/Ui.java)
+
+The `UI` uses the JavaFX UI framework. The UI consists of a `MainWindow`, which contains the application shell components such as the `Header` and the `Console`, and a currently displayed `View`, denoted by `currentView`. Each `View` will define the layout and subcomponents that will be rendered within the `View`.
+
+#### Components
+
+The `UI` is predicated on the concept of a **Component**. A Component is a single sub-unit of the UI, and should preferably only be responsible for a single item or functionality in the UI. For example, a task item in the UI is a single Component, as it is responsible for purely displaying the task information. A task list is also a Component, as it contains multiple task items, and it is responsible just for rendering each task item.
+
+Hence, a Component has the following properties:
+
+- Associated with FXML files
+- Loaded with `load`
+- Able to accept **props**
+- Rendered in placeholder panes
+- Can load sub-Components
+
+*Note: The concept of Components and their associated behaviours came from [React](https://facebook.github.io/react/), a modern JavaScript library for the web.*
+
+##### Associated with FXML files
+
+Each Component is associated with a matching `.fxml` file in the `src/main/resources/ui` folder. For example, the layout of the [`TaskList`](../src/main/java/seedu/todo/ui/components/TaskList.java) Component is specified in [`TaskList.fxml`](../src/main/resources/ui/components/TaskList.fxml).
+
+To learn more about FXML, check out this [tutorial](http://docs.oracle.com/javafx/2/get_started/fxml_tutorial.htm).
+
+##### Loaded with `load`
+
+To load a Component from FXML, use the `load` method found on Component, which calls `UiPartLoader` to read the FXML file, loads a JavaFX Node onto the Stage, and returns the Component which can control the Node on the Stage.
+
+Example usage:
+
+``` java
+TaskList taskList = load(primaryStage, placeholderPane, TaskList.class);
+```
+
+##### Able to accept props
+
+Components should define a set of public fields or **props** so that dynamic values can be passed into the Component and displayed. These props can be displayed in the UI at the `componentDidMount` phase, which will be explained more below.
+
+For example, to pass in tasks to a TaskList component, simply set the value of `taskList.tasks`, or however it is defined in `TaskList`.
+
+##### Rendered in placeholder panes
+
+After props have been passed, the loaded node can be rendered into a placeholder `Pane`. Typically, these panes are `AnchorPane`s defined in the layout in the FXML file.
+
+##### Can load sub-Components
+
+Every Component has a method called `componentDidMount`, which is run after `render` is called. Hence, there are a few uses of `componentDidMount`:
+
+- Control UI-specific properties which cannot be done in FXML
+- Set UI component values (e.g. using `setText` on an FXML `Text` object)
+- Load sub-Components and propogate the chain
+
+Hence, a Component can contain further sub-Components, where each Component is not aware of its parent and only renders what it is told to (via props).
+
+Example usage:
+
+``` java
+public void componentDidMount() {
+ // Set Text field value
+ textField.setText(textProp);
+
+ // Load and render sub-components
+ SubComponent sub = load(primaryStage, placeholderPane, SubComponent.class);
+ sub.value = subTextValue;
+ sub.render();
+}
+```
+
+#### Views
+
+A `View` is essentially a special type of Component, with no implementation differences at the moment. However, a `View` is the grouping of Components to form the whole UI experience. In the case of this app, the `View` corresponds with the portion between the Header and the Console. Different `View`s can be loaded depending on the context.
+
+#### MultiComponents
+
+A `MultiComponent` is also a special type of Component, except that the `render` method behaves differently. Successive calls to `render()` would cause the node to the rendered to the placeholder multiple times, instead of replacing the old node. This is especially useful for rendering lists of variable items, using a loop.
+
+To clear the placeholder of previously rendered items, use `MultiComponent.reset(placeholder)`.
+
+Example usage:
+
+``` java
+public void componentDidMount() {
+ // Reset items
+ TaskItem.reset(placeholder);
+
+ // Load multiple components
+ for (Task task : tasks) {
+ TaskItem item = load(primaryStage, placeholder, TaskItem.class);
+ item.value = task.value;
+ item.render();
+ }
+}
+```
+
+### InputHandler component
+
+
+
+The InputHandler is the bridge facilitating the handoff from the View to the Controller when the user enters an input.
+
+**API** : [`InputHandler.java`](../src/main/java/seedu/todo/ui/components/InputHandler.java)
+
+1. The console input field will find a `Controller` which matches the command keyword (defined to be the first space-delimited word in the command).
+2. The matched `Controller` selected will process the commands accordingly.
+3. The InputHandler also maps aliased commands back to their original command keyword.
+3. If no Controllers were matched, the console would display an error, to indicate an invalid command.
+
+```
+###### \DeveloperGuide.md
+``` md
+#### Use case : UC19 - Add alias
+
+**MSS**
+
+1. User requests to set an alias mapping.
+2. Application commits the alias mapping.
+Use case ends.
+
+**Extensions**
+
+1a. The specified alias key is already set.
+> 1a1. Application shows an error message.
+Use case ends.
+
+1b. The command syntax is invalid.
+>1b1. Application shows an error message.
+Use case ends.
+
+#### Use Case: UC20 - Remove alias
+
+**MSS**
+
+1. User requests for a list of existing alias mappings.
+2. Application shows the list of existing alias mappings.
+3. User requests to remove an existing alias mapping.
+4. Application removes the alias mapping.
+Use case ends.
+
+**Extensions**
+
+1a. The list is empty.
+> Use case ends.
+
+3a. The specified alias key does not exist.
+> 3a1. Application will show an error message.
+Use case ends.
+
+3b. The commmand syntax is invalid.
+> 3b1. Application will show an error message.
+Use case ends.
+```
+###### \UserGuide.md
+``` md
+
+#### Listing all tasks and events : `list`
+
+Shows a list of all tasks and events in GetShitDone. Able to filter by type of task (task/event), or based on status of task/event.
+
+Format: `list [PARAMS]...`
+
+> Valid parameters:
+> * Item type: `events` / `event`/ `tasks` / `task`
+> * Task status: `complete` / `completed` / `incomplete` / `incompleted`
+> * Event status: `over` / `past` / `future`
+> * Task deadline: `(by|on|at|before) DATE`
+> * Event date: `from STARTDATE to ENDDATE`
+> * Tag: `tag TAGNAME`
+>
+> The command accepts any combination of the above, with the exception of:
+> * Task status cannot be defined for events
+> * Event status cannot be defined for tasks
+> In the event of such ambiguity, the command will display an error for the user to rectify it.
+
+Examples:
+
+* `list`
+ Lists all tasks and events.
+
+* `list events`
+ Lists all events.
+
+* `list completed tasks`
+ Lists all completed tasks
+
+* `list by today`
+ Lists all tasks whose deadline are today or before, and events which end before today
+
+* `list from monday to friday`
+ Lists all tasks due within the coming Monday to Friday, and events which start after the coming Monday and end before Friday
+
+```
+###### \UserGuide.md
+``` md
+
+#### Clearing the Database : `clear`
+
+Clear tasks/events by specific instruction from GetShitDone.
+
+Format: `clear [PARAMS]...`
+
+> Valid parameters:
+> * Item type: `events` / `event`/ `tasks` / `task`
+> * Task status: `complete` / `completed` / `incomplete` / `incompleted`
+> * Event status: `over` / `past` / `future`
+> * Task deadline: `(by|on|at|before) DATE`
+> * Event date: `from STARTDATE to ENDDATE`
+> * Tag: `tag TAGNAME`
+>
+> The command accepts any combination of the above, with the exception of:
+> * Task status cannot be defined for events
+> * Event status cannot be defined for tasks
+> In the event of such ambiguity, the command will display an error for the user to rectify it.
+
+Examples:
+
+* `clear task`
+ Clear all tasks in GetShitDone.
+
+* `clear event to yesterday`
+ Clear all events up to yesterday [inclusive].
+
+```
+###### \UserGuide.md
+``` md
+
+#### Aliasing: `alias`
+
+Adds aliases for existing commands. *For advanced users.*
+
+Format: `alias [NEW_ALIAS EXISTING_COMMAND]`
+
+Examples:
+* `alias`
+ Lists all current aliases.
+
+* `alias ls list`
+ `ls`
+ Aliases `find` to `f`, and subsequently `ls` will list all tasks and events.
+
+* `alias f find`
+ `f Irvin`
+ Aliases `find` to `f`, and subsequently `f` can be used to `find` tasks and events.
+
+#### Unaliasing: `unalias`
+
+Removes existing aliases. *For advanced users.*
+
+Format: `unalias ALIAS`
+
+Examples:
+* `unalias f`
+ Removes the alias for `f`.
+
+#### Undo tasks : `undo`
+
+Undo commands in the application.
+
+Format: `undo [COUNT]`
+
+> Performs undo repeatedly based on the specified `COUNT`. If `COUNT` is not specified, it defaults to 1.
+
+Examples:
+
+* `undo`
+ Performs undo.
+
+* `undo 2`
+ Performs undo twice.
+
+#### Redo tasks : `redo`
+
+Redo commands in GetShitDone.
+
+Format: `redo [COUNT]`
+
+> Performs redos based on the specified `COUNT`. If `COUNT` is not specified, it defaults to 1.
+
+Examples:
+
+* `redo`
+ Performs redo.
+
+* `redo 2`
+ Performs redo twice.
+
+#### Changing the app title : `config appTitle`
+
+Format: `config appTitle FILEPATH`
+
+Examples:
+
+* `config appTitle Jim's Todo List`
+Changes the app title to `Jim's Todo List`.
+
+#### Changing the save location : `config databaseFilePath`
+
+The application data is saved in a file called `database.json`, which is saved in the same directory as the application by default.
+
+Format: `config databaseFilePath FILEPATH`
+
+> The file name of the database file must end in `.json`.
+
+Examples:
+
+* `config databaseFilePath movedDatabase.json`
+ Moves the existing database file to `movedDatabase.json`.
+
+* `config databaseFilePath /absolute/path/to/database.json`
+Moves the existing database file to `/absolute/path/to/database.json`.
+
+#### Exiting the program : `exit`
+
+Exits the program.
+
+Format: `exit`
+
+#### Saving of data
+The application data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually.
+
+
+## FAQ
+
+**Q**: How do I transfer my data to another computer?
+**A**: Install the app in the other computer, and replace `database.json` from the root of the application directory.
+
+## Command Summary
+
+**Standard Actions**
+
+Command | Format
+-------- | :--------
+Add Task | `add [task] NAME [(by|on|at|before|time) DEADLINE] `
+Add Event | `add event NAME from STARTDATETIME to ENDDATETIME`
+Complete | `complete INDEX`
+Uncomplete | `uncomplete INDEX`
+Help | `help`
+
+**Viewing**
+
+Command | Format
+-------- | :--------
+List | `list [PARAMS]...`
+Find | `find KEYWORD [MORE_KEYWORDS]...`
+
+**Editing**
+
+Command | Format
+-------- | :--------
+Update | `update INDEX [name NAME] [( (by|on|at|before) DATE] | from STARTDATE to ENDDATE )]`
+Delete | `destroy INDEX`
+Clear | `clear [PARAMS]...`
+Add Tag | `tag INDEX TAG_NAME`
+Untag | `untag INDEX TAG_NAME`
+Undo | `undo [COUNT]`
+Redo | `redo [COUNT]`
+
+**App Actions**
+
+Command | Format
+-------- | :--------
+Change App Title | `config appTitle APPTITLE`
+Change Database File Path | `config databaseFilePath FILEPATH`
+
+**Advanced Actions**
+
+Command | Format
+-------- | :--------
+Add alias | `alias`
+Remove alias | `unalias`
+```
diff --git a/collated/docs/A0139922Y.md b/collated/docs/A0139922Y.md
new file mode 100644
index 000000000000..06529bd41aab
--- /dev/null
+++ b/collated/docs/A0139922Y.md
@@ -0,0 +1,378 @@
+# A0139922Y
+###### \DeveloperGuide.md
+``` md
+#### Use case : UC03 - Add Event
+
+**MSS**
+
+1. User requests to add new events with its details.
+2. Application adds the events with specified details.
+3. Application displays successful message.
+Use case ends.
+
+**Extensions**
+
+1a. User specifies a start date and end date.
+
+> 1a1. User specifies an invalid date format.
+Application shows an error message.
+Use case ends.
+
+1b. User specifies a deadline.
+
+> 1b1. User specifies an invalid date deadline format.
+Application shows an error message.
+Use case ends.
+
+
+
+#### Use case : UC04 - Find by keyword
+
+**MSS**
+
+1. User requests to find records with specified keyword(s)
+2. Application shows the list of records with the specified keyword(s)
+Use case ends.
+
+**Extensions**
+
+2a. The list is empty.
+> 2a1. Application shows an error message.
+Use case ends.
+```
+###### \DeveloperGuide.md
+``` md
+
+#### Use case : UC06 - List by date
+
+**MSS**
+
+1. User requests to list all the tasks and events by date.
+2. Application shows the list of tasks and events by the date with respective details.
+Use case ends.
+
+**Extensions**
+
+2a. The list is empty.
+
+> 2a1. Application shows error message.
+Use case ends.
+
+2b. User did not provide any date.
+> Use case ends.
+
+2c. User provides a single date.
+> 2c1. User specifies an invalid date format.
+> Application shows error message.
+> Use case ends.
+
+2d. User provide a start date and end date.
+> 2c1. User specifies invalid date format for either start or end date.
+> Application shows error message.
+Use case ends.
+
+#### Use case : UC07 - List by status
+
+**MSS**
+
+1. User requests to list all the tasks and events by status.
+2. Application shows the list of tasks or events by status with respective details.
+Use case ends.
+
+**Extensions**
+
+2a. The list is empty.
+
+> 2a1. Application shows error message.
+Use case ends.
+
+2b. User specifies status
+>2b1. User specifies an invalid task/event status.
+Use case ends.
+
+#### Use case: UC08 - Delete task/event
+
+**MSS**
+
+1. Application shows a list of tasks and events.
+2. User requests to delete a specific task or event in the list by its respective index.
+3. Application deletes the task or event.
+4. Application shows a updated list of task and events.
+Use case ends.
+
+**Extensions**
+
+2a. The list is empty.
+> Use case ends.
+
+3a. The given index is invalid.
+> 3a1. Application shows an error message.
+Use case ends.
+
+#### Use case: UC10 - Update task
+
+**MSS**
+
+1. Application shows a list of tasks and events.
+2. User requests to update a specific task in the list by respective index.
+3. Application edits the task.
+4. Application shows a updated list of tasks and events.
+Use case ends.
+
+**Extensions**
+
+2a. The list is empty.
+> Use case ends.
+
+3a. The given index is invalid.
+> 3a1. Application shows an error message.
+Use case ends.
+
+3b. The given details are invalid.
+> 3b1. User specifies an invalid date format.
+> Application shows an error message.
+Use case ends.
+
+> 3b2. User specifies more than one date.
+> Application shows an error message.
+Use case ends.
+
+#### Use case: UC11 - Update Events
+
+**MSS**
+
+1. Application shows a list of tasks and events.
+2. User requests to update a specific event in the list by respective index.
+3. Application edits the event.
+4. Application shows a updated list of tasks and events.
+Use case ends.
+
+**Extensions**
+
+2a. The list is empty.
+> Use case ends.
+
+3a. The given index is invalid.
+> 3a1. Application shows an error message.
+Use case ends.
+
+3b. The given details are invalid.
+> 3b1. User specifies an invalid date format.
+> Application shows an error message.
+Use case ends.
+
+```
+###### \DeveloperGuide.md
+``` md
+
+#### Use case: UC14 - Complete task
+
+**MSS**
+
+1. Application shows a list of tasks and events.
+2. User requests to complete a specific task in the list by respective index.
+3. Application completes the task.
+4. Application shows a updated list of tasks and events.
+Use case ends.
+
+**Extensions**
+
+2a. The list is empty.
+> Use case ends.
+
+2a. The given index is invalid.
+
+> 2a1. The given index is out of range.
+> Application shows an error message.
+Use case ends.
+
+>2a2. The given index belongs to an event.
+> Application shows an error message.
+Use case ends.
+
+2b. Index is not specified.
+
+> 2b1. Application shows an error message.
+Use case ends.
+
+#### Use case: UC15 - Uncomplete task
+
+**MSS**
+
+1. User requests a list of completed tasks or find completed task with keyword.
+2. Application shows a list of completed tasks.
+3. User requests to uncomplete a task by respective index.
+4. Application uncompletes the task.
+5. Application shows the updated list of tasks and events.
+Use case ends.
+
+**Extensions**
+
+2a. The list is empty.
+> Use case ends.
+
+3a. The given index is invalid.
+
+> 3a1. The given index is out of range.
+> Application shows an error message.
+Use case ends.
+
+3b. Index is not specified.
+
+> 3b1. Application shows an error message.
+Use case ends.
+
+#### Use case: UC16 - Add tag to a task/event
+
+**MSS**
+
+1. Application shows a list of tasks and events.
+2. User requests to tag a specific task/event in the list by respective index.
+3. Application adds the tag and associated it with the task/event.
+4. Application shows a updated list of tasks and events.
+Use case ends.
+
+**Extensions**
+
+2a. The list is empty.
+> Use case ends
+
+2a. The given index is invalid.
+
+> 2a1. The given index is out of range.
+> Application shows an error message.
+Use case ends.
+
+2b. Index is not specified.
+
+> 2b1. Application shows an error message.
+Use case ends.
+
+2c. Invalid tag name
+> 2c1. Tag name is not specified
+> Application shows an error message.
+Use case ends.
+
+> 2c2. Tag name specified is already associated to the task/event.
+> Application shows an error message.
+Use case ends.
+
+2d. Tag list size is full
+> 2d1. Application shows an error message.
+Use case ends.
+
+#### Use case: UC17 - Untag tag from a task/event
+
+**MSS**
+
+1. Application shows a list of tasks and events.
+2. User requests to untag the tag of a specific task/event in the list by respective index.
+3. Application deletes the tag that is associated to the task/event.
+4. Application shows a updated list of tasks and events.
+Use case ends.
+
+**Extensions**
+
+2a. The list is empty.
+> Use case ends
+
+2a. The given index is invalid.
+
+> 2a1. The given index is out of range.
+> Application shows an error message.
+Use case ends.
+
+2b. Index is not specified.
+
+> 2b1. Application shows an error message.
+Use case ends.
+
+2c. Invalid tag name
+> 2c1. Tag name is not specified.
+> Application shows an error message.
+Use case ends.
+
+>2c2. Tag name specified does not belong to the task/event.
+> Application shows and error message.
+Use case ends.
+
+```
+###### \DeveloperGuide.md
+``` md
+5. Should be able to respond to any command within 3 seconds.
+6. User-friendly interface
+```
+###### \DeveloperGuide.md
+``` md
+#### Todoist: Strength and Weaknesses
+
+> Todoist is a task management application, with access to over ten different platforms and the ability to collaborate on tasks. The application is straightforward and quick in providing the user with easy access to the important details of the to-do item. It also encourages people to keep up the habit of clearing existing tasks with its Karma Mode.
+
+> Moreover, its ease of use and its integration with other services are its true strength. It can integrate with the latest technologies such as Trello and Amazon Echo to keep every to-do item in a single place.
+
+> However, one flaw with Todoist is that it does not possess any capabilities of having subproject hierarchy. Hence, it would make complex projects' task to be split among the team in an orderly fashion.
+```
+###### \UserGuide.md
+``` md
+
+#### Tagging an item : `tag`
+
+Adds a tag to the task.
+
+Format: `tag INDEX TAG_NAME`
+
+> Adds the tag for the task at the specified `INDEX`.
+ The index refers to the index number shown in the most recent listing.
+
+#### Untagging an item : `untag`
+
+Removes the specified tag of the task.
+
+Format: `untag INDEX TAG_NAME`
+
+> Removes the tag for the task at the specified `INDEX`.
+ The index refers to the index number shown in the most recent listing.
+
+Examples:
+
+* `untag 2 CS2103`
+ Untag the 2nd task/event of the tag name `CS2103` in GetShitDone.
+
+* `untag 1 CS2103`
+ Untag the 1st task/event of the tag name `CS2103` in GetShitDone.
+
+#### Completing a task : `complete`
+
+Completes the specified task from GetShitDone.
+
+Format: `complete INDEX`
+
+> completes the task at the specified `INDEX`.
+ The index refers to the index number shown in the most recent listing.
+
+Examples:
+
+* `complete 2`
+ Completes the 2nd task/event in GetShitDone.
+
+* `complete 1`
+ Completes the 1st task/event in GetShitDone.
+
+#### Uncompleting a task : `uncomplete`
+
+Uncompletes the specified task from GetShitDone.
+
+Format: `uncomplete INDEX`
+
+> uncompletes the task at the specified `INDEX`.
+ The index refers to the index number shown in the most recent listing.
+
+Examples:
+
+* `uncomplete 2`
+ Uncomplete the 2nd task in GetShitDone.
+
+* `uncomplete 1`
+ Uncomplete the 1st task in GetShitDone.
+
+```
diff --git a/collated/main/A0093907W.md b/collated/main/A0093907W.md
new file mode 100644
index 000000000000..42a2c9a0a714
--- /dev/null
+++ b/collated/main/A0093907W.md
@@ -0,0 +1,2785 @@
+# A0093907W
+###### \java\seedu\todo\commons\core\AliasDefinition.java
+``` java
+/**
+ * Container class to store and retrieve a pair of values for an alias key -> value pair.
+ */
+public class AliasDefinition {
+ private String aliasKey;
+ private String aliasValue;
+
+ public AliasDefinition(String aliasKey, String aliasValue) {
+ this.aliasKey = aliasKey;
+ this.aliasValue = aliasValue;
+ }
+
+ public String getAliasKey() {
+ return aliasKey;
+ }
+
+ public String getAliasValue() {
+ return aliasValue;
+ }
+
+}
+```
+###### \java\seedu\todo\commons\EphemeralDB.java
+``` java
+/**
+ * A bit like Redis, essentially a data store for things that should not be
+ * persisted to disk, but should be shared between all modules.
+ *
+ * All variables should be public. In-place modifications of variables are
+ * encouraged.
+ */
+public class EphemeralDB {
+
+ private static EphemeralDB instance = null;
+
+ // Stores
+ public List displayedCalendarItems = new ArrayList<>();
+
+
+ protected EphemeralDB() {
+ // Prevent instantiation.
+ }
+
+ /**
+ * Gets the singleton instance of the EphemeralDB.
+ *
+ * @return EphemeralDB
+ */
+ public static EphemeralDB getInstance() {
+ if (instance == null) {
+ instance = new EphemeralDB();
+ }
+ return instance;
+ }
+
+
+ /** ======== DISPLAYED CALENDAR ITEMS ======== **/
+
+ /**
+ * Returns a CalendarItem from all displayedCalendarItems according to their displayed ID.
+ * Note that displayedCalendarItems stores the indexes of the last displayed list of CalendarItems.
+ * Their displayed ID is simply their index in the ArrayList + 1 (due to 0-indexing of ArrayLists).
+ *
+ * @param id Display ID of task. Bounded between 1 and the size of the ArrayList.
+ * @return Returns the Task at the specified display index.
+ */
+ public CalendarItem getCalendarItemsByDisplayedId(int id) {
+ if (id <= 0 || id > displayedCalendarItems.size()) {
+ return null;
+ } else {
+ return displayedCalendarItems.get(id - 1);
+ }
+ }
+
+ /**
+ * Adds a CalendarItem to displayedCalendarItems in EphemeralDB.
+ * Returns the 1-indexed index of the added item.
+ *
+ * @param item CalendarItem to add to displayedCalendarItems.
+ * @return List index (1-index) of the added item.
+ */
+ public int addToDisplayedCalendarItems(CalendarItem item) {
+ displayedCalendarItems.add(item);
+ return displayedCalendarItems.size();
+ }
+
+ /**
+ * Clears displayedCalendarItems.
+ */
+ public void clearDisplayedCalendarItems() {
+ displayedCalendarItems.clear();
+ }
+
+}
+```
+###### \java\seedu\todo\controllers\AddController.java
+``` java
+/**
+ * Controller to add an event or task.
+ */
+public class AddController extends Controller {
+
+ private static final String NAME = "Add";
+ private static final String DESCRIPTION = "Adds a task / event to the to-do list.\n"
+ + "Accepts natural date formats (e.g. \"Today 5pm\" is allowed).";
+ private static final String COMMAND_SYNTAX = "add by || add from to ";
+ private static final String COMMAND_KEYWORD = "add";
+
+ private static final String MESSAGE_ADD_SUCCESS = "Item successfully added!";
+ private static final String STRING_WHITESPACE = "";
+ private static final String ADD_EVENT_TEMPLATE = "add event \"%s\" from \"%s\" to \"%s\"";
+ private static final String ADD_TASK_TEMPLATE = "add task \"%s\" by \"%s\"";
+ private static final String START_TIME_FIELD = "";
+ private static final String END_TIME_FIELD = "";
+ private static final String DEADLINE_FIELD = "";
+ private static final String NAME_FIELD = "";
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ /**
+ * Get the token definitions for use with tokenizer
.
+ * This method exists primarily because Java does not support HashMap
+ * literals...
+ *
+ * @return tokenDefinitions
+ */
+ private static Map getTokenDefinitions() {
+ Map tokenDefinitions = new HashMap();
+ tokenDefinitions.put("default", new String[] {"add"});
+ tokenDefinitions.put("eventType", new String[] { "event", "task" });
+ tokenDefinitions.put("time", new String[] { "at", "by", "on", "before", "time" });
+ tokenDefinitions.put("timeFrom", new String[] { "from" });
+ tokenDefinitions.put("timeTo", new String[] { "to" });
+ return tokenDefinitions;
+ }
+
+ @Override
+ public void process(String input) throws ParseException {
+ Map parsedResult;
+ parsedResult = Tokenizer.tokenize(getTokenDefinitions(), input);
+
+ // Task or event?
+ boolean isTask = parseIsTask(parsedResult);
+
+ // Name
+ String name = parseName(parsedResult);
+
+ // Time
+ String[] naturalDates = DateParser.extractDatePair(parsedResult);
+ String naturalFrom = naturalDates[0];
+ String naturalTo = naturalDates[1];
+
+ // Parse natural date using Natty.
+ LocalDateTime dateFrom;
+ LocalDateTime dateTo;
+ try {
+ dateFrom = naturalFrom == null ? null : DateParser.parseNatural(naturalFrom);
+ dateTo = naturalTo == null ? null : DateParser.parseNatural(naturalTo);
+ } catch (InvalidNaturalDateException e) {
+ renderDisambiguation(isTask, name, naturalFrom, naturalTo);
+ return;
+ }
+
+ // Validate isTask, name and times.
+ if (!validateParams(isTask, name, dateFrom, dateTo)) {
+ renderDisambiguation(isTask, name, naturalFrom, naturalTo);
+ return;
+ }
+
+ // Create and persist task / event.
+ TodoListDB db = TodoListDB.getInstance();
+ createCalendarItem(db, isTask, name, dateFrom, dateTo);
+
+ // Re-render
+ Renderer.renderIndex(db, MESSAGE_ADD_SUCCESS);
+ }
+
+ /**
+ * Creates and persists a CalendarItem to the DB.
+ *
+ * @param db
+ * TodoListDB object
+ * @param isTask
+ * true if CalendarItem should be a Task, false if Event
+ * @param name
+ * Display name of CalendarItem object
+ * @param dateFrom
+ * Due date for Task or start date for Event
+ * @param dateTo
+ * End date for Event
+ */
+ private void createCalendarItem(TodoListDB db,
+ boolean isTask, String name, LocalDateTime dateFrom, LocalDateTime dateTo) {
+ if (isTask) {
+ Task newTask = db.createTask();
+ newTask.setName(name);
+ newTask.setDueDate(dateFrom);
+ } else {
+ Event newEvent = db.createEvent();
+ newEvent.setName(name);
+ newEvent.setStartDate(dateFrom);
+ newEvent.setEndDate(dateTo);
+ }
+ db.save();
+ }
+
+
+ /**
+ * Validates the parsed parameters.
+ *
+ *
+ * - Fail if name is invalid
+ *
+ *
+ * Tasks:
+ *
+ * - Fail if task has a dateTo
+ *
+ *
+ * Events:
+ *
+ * - Fail if event does not have both dateFrom and dateTo
+ * - Fail if event has a dateTo that is before dateFrom
+ *
+ *
+ * @param isTask
+ * true if CalendarItem should be a Task, false if Event
+ * @param name
+ * Display name of CalendarItem object
+ * @param dateFrom
+ * Due date for Task or start date for Event
+ * @param dateTo
+ * End date for Event
+ * @return true if validation passed, false otherwise
+ */
+ private boolean validateParams(boolean isTask, String name, LocalDateTime dateFrom, LocalDateTime dateTo) {
+ return (!(name == null || name.length() == 0 // Invalid name
+ || (isTask && dateTo != null) // Task with dateTo
+ || (!isTask && dateFrom == null) // Event without dateFrom
+ || (!isTask && dateTo == null) // Event without dateTo
+ || (!isTask && dateTo.isBefore(dateFrom)))); // Event with dateTo before dateFrom
+ }
+
+ /**
+ * Extracts the display name of the CalendarItem from parsedResult.
+ *
+ * @param parsedResult
+ * @return name
+ */
+ private String parseName(Map parsedResult) {
+ String name = null;
+ if (parsedResult.get("default") != null && parsedResult.get("default")[1] != null) {
+ name = parsedResult.get("default")[1];
+ }
+ if (parsedResult.get("eventType") != null && parsedResult.get("eventType")[1] != null) {
+ name = parsedResult.get("eventType")[1];
+ }
+ return name;
+ }
+
+ /**
+ * Extracts the intended CalendarItem type from parsedResult.
+ *
+ * @param parsedResult
+ * @return true if Task, false if Event
+ */
+ private boolean parseIsTask(Map parsedResult) {
+ boolean isTask = true;
+ if (parsedResult.get("eventType") != null && parsedResult.get("eventType")[0].equals("event")) {
+ isTask = false;
+ }
+ return isTask;
+ }
+
+ /**
+ * Disambiguate an ambiguous input by auto-populating a templated command on
+ * a best-effort basis.
+ *
+ * @param isTask
+ * @param name
+ * @param naturalFrom
+ * @param naturalTo
+ */
+ private void renderDisambiguation(boolean isTask, String name, String naturalFrom, String naturalTo) {
+ name = StringUtil.replaceEmpty(name, NAME_FIELD);
+
+ String disambiguationString;
+ String errorMessage = STRING_WHITESPACE;
+
+ if (isTask) {
+ naturalFrom = StringUtil.replaceEmpty(naturalFrom, DEADLINE_FIELD);
+ disambiguationString = String.format(ADD_TASK_TEMPLATE, name, naturalFrom);
+ } else {
+ naturalFrom = StringUtil.replaceEmpty(naturalFrom, START_TIME_FIELD);
+ naturalTo = StringUtil.replaceEmpty(naturalTo, END_TIME_FIELD);
+ disambiguationString = String.format(ADD_EVENT_TEMPLATE, name, naturalFrom, naturalTo);
+ }
+
+ // Show an error in the console
+ Renderer.renderDisambiguation(disambiguationString, errorMessage);
+ }
+
+}
+```
+###### \java\seedu\todo\controllers\AliasController.java
+``` java
+/**
+ * Controller to declare aliases
+ */
+public class AliasController extends Controller {
+
+ private static final String NAME = "Alias";
+ private static final String DESCRIPTION = "Shows current aliases or updates them.";
+ private static final String COMMAND_SYNTAX = "alias [ ]";
+ private static final String COMMAND_KEYWORD = "alias";
+
+ private static final String SPACE = " ";
+ private static final int ARGS_LENGTH = 2;
+ private static final String MESSAGE_SHOWING = "Showing all aliases.";
+ private static final String MESSAGE_SAVE_SUCCESS = "Successfully saved alias!";
+ private static final String MESSAGE_INVALID_NUM_PARAMS = "Seems like you have provided an invalid number of parameters!";
+ private static final String MESSAGE_INVALID_INPUT = "Invalid alias parameters! Alias inputs must consist solely "
+ + "of alphabetical characters.";
+ private static final String SAVE_ERROR = "There was an error saving your aliases. Please try again.";
+ private static final String ALIAS_TEMPLATE = "alias %s %s";
+ private static final String ALIAS_VALUE_FIELD = "";
+ private static final String ALIAS_KEY_FIELD = "";
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ @Override
+ public void process(String input) {
+ String params = input.replaceFirst("alias", "").trim();
+
+ if (params.length() <= 0) {
+ Renderer.renderAlias(MESSAGE_SHOWING);
+ return;
+ }
+
+ String[] args = params.split(SPACE, ARGS_LENGTH);
+
+ String aliasKey = null;
+ String aliasValue = null;
+
+ // Best-effort matching, disambiguate if wrong.
+ validate: {
+ switch (args.length) {
+ case 0:
+ break;
+ case 1:
+ aliasKey = args[0];
+ break;
+ case 2: // All good!
+ aliasKey = args[0];
+ aliasValue = args[1];
+ break validate;
+ default:
+ aliasKey = args[0];
+ aliasValue = args[0];
+ break;
+ }
+ renderDisambiguation(aliasKey, aliasValue, MESSAGE_INVALID_NUM_PARAMS);
+ return;
+ }
+
+ if (!validateAlias(aliasKey) || !validateAlias(aliasValue)) {
+ renderDisambiguation(aliasKey, aliasValue, MESSAGE_INVALID_INPUT);
+ return;
+ }
+
+ // Persist alias mapping
+ try {
+ saveAlias(aliasKey, aliasValue);
+ } catch (IOException e) {
+ Renderer.renderAlias(SAVE_ERROR);
+ return;
+ }
+
+ Renderer.renderAlias(MESSAGE_SAVE_SUCCESS);
+ }
+
+ /**
+ * Persists an alias mapping to the database.
+ *
+ * @param db TodoListDB singleton
+ * @param aliasKey
+ * @param aliasValue
+ * @throws IOException
+ */
+ private static void saveAlias(String aliasKey, String aliasValue) throws IOException {
+ Config config = ConfigCenter.getInstance().getConfig();
+ Map aliases = config.getAliases();
+ aliases.put(aliasKey, aliasValue);
+ ConfigCenter.getInstance().saveConfig(config);
+
+ }
+
+ /**
+ * Validates that string is sanitized and safe for aliasing.
+ *
+ * @param alias string to check
+ * @return true if string is sanitized, false otherwise
+ */
+ private static boolean validateAlias(String alias) {
+ return alias.chars().allMatch(Character::isLetter);
+ }
+
+ /**
+ * Disambiguate an ambiguous input by auto-populating a templated command on
+ * a best-effort basis.
+ *
+ * @param aliasKey
+ * @param aliasValue
+ * @param message
+ */
+ private static void renderDisambiguation(String aliasKey, String aliasValue, String message) {
+ String sanitizedAliasKey = StringUtil.sanitize(aliasKey);
+ sanitizedAliasKey = StringUtil.replaceEmpty(sanitizedAliasKey, ALIAS_KEY_FIELD);
+ String sanitizedAliasValue = StringUtil.sanitize(aliasValue);
+ sanitizedAliasValue = StringUtil.replaceEmpty(sanitizedAliasValue, ALIAS_VALUE_FIELD);
+ Renderer.renderDisambiguation(String.format(ALIAS_TEMPLATE,
+ sanitizedAliasKey, sanitizedAliasValue), message);
+ }
+
+}
+```
+###### \java\seedu\todo\controllers\ClearController.java
+``` java
+/**
+ * Controller to clear task/event by type or status
+ */
+public class ClearController extends Controller {
+
+ private static final String NAME = "Clear";
+ private static final String DESCRIPTION = "Clear all tasks/events or by specify date.";
+ private static final String COMMAND_SYNTAX = "clear [task/event] [on date]";
+ private static final String COMMAND_KEYWORD = "clear";
+
+ private static final String MESSAGE_CLEAR_NO_ITEMS_FOUND = "No items matched your query!";
+ private static final String MESSAGE_CLEAR_SUCCESS = "A total of %s %s and %s %s deleted!\n" + "To undo, type \"undo\".";
+ public static final String MESSAGE_UNKNOWN_TOKENS = "Could not parse your query as it contained unknown tokens: %s";
+ public static final String MESSAGE_AMBIGUOUS_TYPE = "We could not tell if you wanted to clear events or tasks. \n"
+ + "Note that only tasks can be \"complete\"/\"incomplete\", "
+ + "while only events can be \"past\", \"over\" or \"future\".";
+ public static final String MESSAGE_INVALID_DATE = "We could not parse the date in your query, please try again.";
+
+ private static final String CLEAR_TEMPLATE = "clear [name \"%s\"] [from \"%s\"] [to \"%s\"] [tag \"%s\"]";
+ private static final String CLEAR_TASKS_TEMPLATE = "clear tasks [name \"%s\"] [\"%s\"] [from \"%s\"] [to \"%s\"] [tag \"%s\"]";
+ private static final String CLEAR_EVENTS_TEMPLATE = "clear events [name \"%s\"] [\"%s\"] [from \"%s\"] [to \"%s\"] [tag \"%s\"]";
+
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ @Override
+ public void process(String input) throws ParseException {
+ // Tokenize input
+ Map parsedResult =
+ Tokenizer.tokenize(CalendarItemFilter.getFilterTokenDefinitions(), input);
+
+ // Check if there are any unknown tokens.
+ if (Disambiguator.getUnknownTokenString(parsedResult) != null) {
+ String errorMessage = String.format(MESSAGE_UNKNOWN_TOKENS, Disambiguator.getUnknownTokenString(parsedResult));
+ renderDisambiguation(parsedResult, true, true, errorMessage);
+ return;
+ }
+
+ // Decide if task/event/both
+ boolean[] isTaskEvent = null;
+ try {
+ isTaskEvent = CalendarItemFilter.parseIsTaskEvent(parsedResult);
+ } catch (AmbiguousEventTypeException e) {
+ renderDisambiguation(parsedResult, true, true, MESSAGE_AMBIGUOUS_TYPE);
+ return;
+ }
+
+ boolean filterTask = isTaskEvent[0];
+ boolean filterEvent = isTaskEvent[1];
+
+ List clearTasks = new ArrayList();
+ List clearEvents = new ArrayList();
+ try {
+ if (filterTask) {
+ clearTasks = CalendarItemFilter.filterTasks(parsedResult);
+ }
+ if (filterEvent) {
+ clearEvents = CalendarItemFilter.filterEvents(parsedResult);
+ }
+ } catch (InvalidNaturalDateException e) {
+ renderDisambiguation(parsedResult, filterTask, filterEvent, MESSAGE_INVALID_DATE);
+ return;
+ }
+
+ // Clear them all!
+ TodoListDB db = TodoListDB.getInstance();
+
+ if (clearTasks.size() == 0 && clearEvents.size() == 0) {
+ Renderer.renderIndex(db, MESSAGE_CLEAR_NO_ITEMS_FOUND);
+ return;
+ }
+
+ db.destroyTasks(clearTasks);
+ db.destroyEvents(clearEvents);
+ db.save();
+
+ String consoleMessage = String.format(MESSAGE_CLEAR_SUCCESS,
+ clearTasks.size(), StringUtil.pluralizer(clearTasks.size(), "task", "tasks"),
+ clearEvents.size(), StringUtil.pluralizer(clearEvents.size(), "event", "events"));
+
+ Renderer.renderIndex(db, consoleMessage);
+ }
+
+
+ /**
+ * Disambiguate an ambiguous input by auto-populating a templated command on
+ * a best-effort basis.
+ *
+ * @param parsedResult
+ * @param filterTask
+ * @param filterEvent
+ * @param errorMessage
+ */
+ private void renderDisambiguation(Map parsedResult, boolean filterTask, boolean filterEvent, String errorMessage) {
+ Map extractedTokens = Disambiguator.extractParsedTokens(parsedResult);
+ String consoleCommand;
+
+ if ((filterTask && filterEvent) || (!filterTask && !filterEvent)) {
+ consoleCommand = String.format(CLEAR_TEMPLATE, extractedTokens.get("name"), extractedTokens.get("startTime"),
+ extractedTokens.get("endTime"), extractedTokens.get("tag"));
+ } else if (filterTask) {
+ consoleCommand = String.format(CLEAR_TASKS_TEMPLATE, extractedTokens.get("name"), extractedTokens.get("taskStatus"),
+ extractedTokens.get("startTime"), extractedTokens.get("endTime"), extractedTokens.get("tag"));
+ } else {
+ consoleCommand = String.format(CLEAR_EVENTS_TEMPLATE, extractedTokens.get("name"), extractedTokens.get("eventStatus"),
+ extractedTokens.get("startTime"), extractedTokens.get("endTime"), extractedTokens.get("tag"));
+ }
+
+ Renderer.renderDisambiguation(consoleCommand, errorMessage);
+ }
+}
+```
+###### \java\seedu\todo\controllers\concerns\CalendarItemFilter.java
+``` java
+/**
+ * Class to store CalendarItem filtering methods to be shared across controllers.
+ */
+public class CalendarItemFilter {
+
+ /**
+ * Get the token definitions for use with tokenizer
.
+ * This method exists primarily because Java does not support HashMap
+ * literals...
+ *
+ * @return tokenDefinitions
+ */
+ public static Map getFilterTokenDefinitions() {
+ Map tokenDefinitions = new HashMap();
+ tokenDefinitions.put("default", new String[] { "clear", "list" });
+ tokenDefinitions.put("eventType", new String[] { "event", "events", "task", "tasks" });
+ tokenDefinitions.put("name", new String[] { "name" });
+ tokenDefinitions.put("taskStatus", new String[] { "complete" , "completed", "incomplete", "incompleted" });
+ tokenDefinitions.put("eventStatus", new String[] { "over" , "past", "future" });
+ tokenDefinitions.put("timeFrom", new String[] { "from", "after" });
+ tokenDefinitions.put("timeTo", new String[] { "to", "before", "until", "by" });
+ tokenDefinitions.put("tag", new String[] { "tag" });
+ return tokenDefinitions;
+ }
+
+ /**
+ * Returns a boolean array of {isTask, isEvent} which specifies if we should
+ * filter tasks, events or both.
+ *
+ * If there is no eventType specified, we will filter both.
+ *
+ *
+ * - If no "task"/"event" token, and no eventStatus/taskStatus token, then filter both
+ * - If no "task"/"event" token, and exactly one of eventStatus/taskStatus present, then use it to guess
+ * - If "task" token found, then assert no eventStatus token
+ * - If "event" token found, then assert no taskStatus token
+ * - Assert that eventStatus and taskStatus tokens cannot both be present
+ *
+ *
+ * @param parsedResult
+ * @return {isTask, isEvent}
+ */
+ public static boolean[] parseIsTaskEvent(Map parsedResult) throws AmbiguousEventTypeException {
+ // Extract relevant params
+ String eventType = null;
+ if (parsedResult.get("eventType") != null) {
+ eventType = parsedResult.get("eventType")[0];
+ // Singularize
+ eventType = "events".equals(eventType) ? "event" : eventType;
+ eventType = "tasks".equals(eventType) ? "task" : eventType;
+ }
+ boolean taskStatusPresent = parsedResult.get("taskStatus") != null;
+ boolean eventStatusPresent = parsedResult.get("eventStatus") != null;
+
+ if (eventType == null) {
+ if (!taskStatusPresent && !eventStatusPresent) {
+ // Condition 1
+ return new boolean[] { true, true };
+ } else if (eventStatusPresent && !taskStatusPresent) {
+ // Condition 2 - Task
+ return new boolean[] { false, true };
+ } else if (taskStatusPresent && !eventStatusPresent) {
+ // Condition 2 - Event
+ return new boolean[] { true, false };
+ }
+ } else {
+ if ("task".equals(eventType) && !eventStatusPresent) {
+ // Condition 3
+ return new boolean[] { true, false };
+ } else if ("event".equals(eventType) && !taskStatusPresent) {
+ // Condition 4
+ return new boolean[] { false, true };
+ }
+ }
+
+ // If we made it here, then at least one assertion was violated.
+ throw new AmbiguousEventTypeException("Couldn't determine if task or event!");
+ }
+
+
+ public static List filterTasks(Map parsedResult) throws InvalidNaturalDateException {
+ List> taskPredicates = new ArrayList>();
+
+ // Filter by name
+ if (parsedResult.get("name") != null) {
+ taskPredicates.add(Task.predByName(parsedResult.get("name")[1]));
+ }
+
+ // Filter by taskStatus
+ if (parsedResult.get("taskStatus") != null) {
+ String taskStatus = parsedResult.get("taskStatus")[0];
+ if ("complete".equals(taskStatus) || "completed".equals(taskStatus)) {
+ taskPredicates.add(Task.predCompleted(true));
+ } else {
+ taskPredicates.add(Task.predCompleted(false));
+ }
+ }
+
+ // Filter by dueDate
+ String[] datePair = DateParser.extractDatePair(parsedResult);
+ String timeStartNatural = datePair[0];
+ String timeEndNatural = datePair[1];
+ if (timeStartNatural != null) {
+ LocalDateTime timeStart = DateParser.parseNatural(timeStartNatural);
+ taskPredicates.add(Task.predAfterDueDate(timeStart));
+ }
+ if (timeEndNatural != null) {
+ LocalDateTime timeEnd = DateParser.parseNatural(timeEndNatural);
+ taskPredicates.add(Task.predBeforeDueDate(timeEnd));
+ }
+
+ // Filter by tag
+ if (parsedResult.get("tag") != null && parsedResult.get("tag")[1] != null) {
+ taskPredicates.add(Task.predTag(parsedResult.get("tag")[1]));
+ }
+
+ return Task.where(taskPredicates);
+ }
+
+ public static List filterEvents(Map parsedResult) throws InvalidNaturalDateException {
+ List> eventPredicates = new ArrayList>();
+
+ // Filter by name
+ if (parsedResult.get("name") != null) {
+ eventPredicates.add(Event.predByName(parsedResult.get("name")[1]));
+ }
+
+ // Filter by eventStatus
+ if (parsedResult.get("eventStatus") != null && parsedResult.get("eventStatus")[0] != null) {
+ String eventStatus = parsedResult.get("eventStatus")[0];
+ LocalDateTime now = LocalDateTime.now();
+ if ("over".equals(eventStatus) || "past".equals(eventStatus)) {
+ eventPredicates.add(Event.predEndBefore(now));
+ } else if ("future".equals(eventStatus)) {
+ eventPredicates.add(Event.predStartAfter(now));
+ }
+ }
+
+ // Filter by time
+ String[] datePair = DateParser.extractDatePair(parsedResult);
+ String timeStartNatural = datePair[0];
+ String timeEndNatural = datePair[1];
+ if (timeStartNatural != null) {
+ LocalDateTime timeStart = DateParser.parseNatural(timeStartNatural);
+ eventPredicates.add(Event.predStartAfter(timeStart));
+ }
+ if (timeEndNatural != null) {
+ LocalDateTime timeEnd = DateParser.parseNatural(timeEndNatural);
+ eventPredicates.add(Event.predEndBefore(timeEnd));
+ }
+
+ // Filter by tag
+ if (parsedResult.get("tag") != null && parsedResult.get("tag")[1] != null) {
+ eventPredicates.add(Event.predTag(parsedResult.get("tag")[1]));
+ }
+
+ return Event.where(eventPredicates);
+ }
+
+}
+```
+###### \java\seedu\todo\controllers\concerns\DateParser.java
+``` java
+/**
+ * Class to store date parsing methods to be shared across controllers.
+ */
+public class DateParser {
+
+ /**
+ * Extracts the natural dates from parsedResult.
+ *
+ * @param parsedResult
+ * @return { naturalFrom, naturalTo }
+ */
+ public static String[] extractDatePair(Map parsedResult) {
+ String naturalFrom = null;
+ String naturalTo = null;
+ setTime: {
+ if (parsedResult.get("time") != null && parsedResult.get("time")[1] != null) {
+ naturalFrom = parsedResult.get("time")[1];
+ break setTime;
+ }
+ if (parsedResult.get("timeFrom") != null && parsedResult.get("timeFrom")[1] != null) {
+ naturalFrom = parsedResult.get("timeFrom")[1];
+ }
+ if (parsedResult.get("timeTo") != null && parsedResult.get("timeTo")[1] != null) {
+ naturalTo = parsedResult.get("timeTo")[1];
+ }
+ }
+ return new String[] { naturalFrom, naturalTo };
+ }
+
+ /**
+ * Parse a natural date into a LocalDateTime object.
+ *
+ * @param natural
+ * @return LocalDateTime object
+ * @throws InvalidNaturalDateException
+ */
+ public static LocalDateTime parseNatural(String natural) throws InvalidNaturalDateException {
+ Parser parser = new Parser();
+ List groups = parser.parse(natural);
+ Date date = null;
+ try {
+ date = groups.get(0).getDates().get(0);
+ } catch (IndexOutOfBoundsException e) {
+ throw new InvalidNaturalDateException(natural);
+ }
+ LocalDateTime ldt = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
+ return ldt;
+ }
+
+}
+```
+###### \java\seedu\todo\controllers\concerns\Renderer.java
+``` java
+ *
+ * Class to store rendering methods to be shared across controllers.
+ *
+ */
+public class Renderer {
+
+ public static final String MESSAGE_DISAMBIGUATE = "Your last command wasn't clear, please fix your command and try again.";
+
+ /**
+ * Renders an error message in both the console and the input field, leave null or empty string if not needed.
+ * @param replacedCommand Value to display in the input field
+ * @param detailedError Message to be rendered in the console
+ */
+ public static void renderDisambiguation(String replacedCommand, String detailedError) {
+ // Update console input field
+ if (replacedCommand != null && replacedCommand.length() > 0) {
+ UiManager.updateConsoleInputValue(replacedCommand);
+ }
+
+ // Update console message
+ if (detailedError != null && detailedError.length() > 0) {
+ UiManager.updateConsoleMessage(String.format("%s\n\n%s", MESSAGE_DISAMBIGUATE, detailedError));
+ } else {
+ UiManager.updateConsoleMessage(String.format("%s", MESSAGE_DISAMBIGUATE));
+ }
+ }
+
+ /**
+ * Renders the indexView.
+ *
+ * @param db
+ * @param consoleMessage to be rendered in console, leave null if not needed
+ */
+ public static void renderSelected(TodoListDB db, String consoleMessage, List tasks, List events) {
+ IndexView view = UiManager.loadView(IndexView.class);
+
+ if (tasks != null) {
+ view.tasks = tasks;
+ }
+
+ if (events != null) {
+ view.events = events;
+ }
+ view.tags = db.getTagList();
+ UiManager.renderView(view);
+
+ if (consoleMessage != null) {
+ UiManager.updateConsoleMessage(consoleMessage);
+ }
+ }
+
+ /**
+ * Renders the indexView.
+ *
+ * @param db
+ * @param consoleMessage to be rendered in console, leave null if not needed
+ */
+ public static void renderIndex(TodoListDB db, String consoleMessage) {
+ IndexView view = UiManager.loadView(IndexView.class);
+ view.tasks = db.getIncompleteTasksAndTaskFromTodayDate();
+ view.events = db.getAllCurrentEvents();
+ view.tags = db.getTagList();
+ UiManager.renderView(view);
+
+ if (consoleMessage != null) {
+ UiManager.updateConsoleMessage(consoleMessage);
+ }
+ }
+
+ /**
+ * Renders the ConfigView.
+ *
+ * @param consoleMessage to be rendered in console, leave null if not needed
+ */
+ public static void renderConfig(String consoleMessage) {
+ ConfigView view = UiManager.loadView(ConfigView.class);
+ UiManager.renderView(view);
+
+ if (consoleMessage != null) {
+ UiManager.updateConsoleMessage(consoleMessage);
+ }
+ }
+
+ public static void renderAlias(String consoleMessage) {
+ AliasView view = UiManager.loadView(AliasView.class);
+ UiManager.renderView(view);
+
+ if (consoleMessage != null) {
+ UiManager.updateConsoleMessage(consoleMessage);
+ }
+ }
+
+}
+```
+###### \java\seedu\todo\controllers\concerns\Tokenizer.java
+``` java
+/**
+ * Class to store the static method tokenizer
.
+ */
+public class Tokenizer {
+
+ private static final String MESSAGE_UNMATCHED_QUOTES = "Unmatched double-quotes detected.";
+ private final static String QUOTE = "\"";
+
+ /**
+ * A private class to tag a string as a token or a quote.
+ */
+ private static class TokenizedString {
+ public String string;
+ public boolean isToken;
+ public boolean isQuote;
+
+ TokenizedString(String string, boolean isToken, boolean isQuote) {
+ this.string = string;
+ this.isToken = isToken;
+ this.isQuote = isQuote;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("TokenizedString(%s, %s, %s)", this.string, isToken, isQuote);
+ }
+ }
+
+ /**
+ * Parses and tokenizes a user-input string into a mapping of tokenType -> tokenField.
+ *
+ * - Quoted chunks are kept as a whole and never matched to a token.
+ * - If there are multiple token matches, only the first one will be registered.
+ * - If there are multiple tokenType matches, only one match will be returned.
+ *
+ * @param tokenDefinitions Mapping of tokenType -> list of token strings to match
+ * @param inputCommand User input to tokenize
+ * @return Mapping of tokenType -> { matchedToken, tokenField }
+ * @throws UnmatchedQuotesException If there is an odd number of quotes
+ */
+ public static Map tokenize(Map tokenDefinitions, String inputCommand)
+ throws UnmatchedQuotesException {
+
+ if (inputCommand.length() == 0) {
+ return null;
+ }
+
+ if (StringUtils.countMatches(inputCommand, QUOTE) % 2 == 1) {
+ throw new UnmatchedQuotesException(MESSAGE_UNMATCHED_QUOTES);
+ }
+
+ // Generate token -> tokenType mapping and list of tokens
+ List tokens = new ArrayList();
+ HashMap getTokenType = new HashMap();
+ for (Map.Entry tokenDefinition : tokenDefinitions.entrySet()) {
+ String tokenType = tokenDefinition.getKey();
+ for (String token : tokenDefinition.getValue()) {
+ tokens.add(token);
+ getTokenType.put(token, tokenType);
+ }
+ }
+
+ // Split inputCommand into arraylist of chunks
+ // --- Split by quotes
+ List tokenizedSplitString = tokenizeQuotes(inputCommand);
+
+ // --- Split by tokens
+ Map tokenIndices = splitByTokens(tokens, getTokenType, tokenizedSplitString);
+
+ // Get arraylist of indices
+ // Get dictionary of tokenType -> index
+ // Return dictionary of tokenType -> {token, tokenField}
+ return constructParsedResult(tokenizedSplitString, tokenIndices);
+ }
+
+ /**
+ * Given a string, extract quoted substrings and flag them as quotes.
+ *
+ * @param inputCommand
+ * @return List of TokenizedString
+ */
+ private static List tokenizeQuotes(String inputCommand) {
+ String[] splitString = inputCommand.split(QUOTE);
+
+ // If first char is QUOTE, then first element is a quoted string.
+ List tokenizedSplitString = new ArrayList();
+ for (int i = 0; i < splitString.length; i++) {
+ tokenizedSplitString.add(new TokenizedString(splitString[i].trim(), false, (i % 2 == 1)));
+ }
+ return tokenizedSplitString;
+ }
+
+ /**
+ * Constructs the parsedResult from user input that has been delimited and tokenized.
+ * @param tokenizedSplitString
+ * @param tokenIndices
+ * @return parsedResult
+ */
+ private static Map constructParsedResult(List tokenizedSplitString,
+ Map tokenIndices) {
+ Map parsedResult = new HashMap();
+ for (Map.Entry tokenIndex : tokenIndices.entrySet()) {
+ String tokenType = tokenIndex.getKey();
+ String token = tokenizedSplitString.get(tokenIndex.getValue()).string;
+ String tokenField = null;
+ // Should just EAFP instead of LBYL, but oh well.
+ if (tokenIndexPresent(tokenIndex, tokenizedSplitString)) {
+ tokenField = tokenizedSplitString.get(tokenIndex.getValue() + 1).string;
+ }
+ parsedResult.put(tokenType, new String[] { token, tokenField });
+ }
+ return parsedResult;
+ }
+
+ /**
+ * Checks if an identified tokenIndex contains any data immediately after the token.
+ *
+ * @param tokenIndex
+ * @param tokenizedSplitString
+ * @return true if data is present, false otherwise
+ */
+ private static boolean tokenIndexPresent(Map.Entry tokenIndex, List tokenizedSplitString) {
+ return tokenIndex.getValue() + 1 < tokenizedSplitString.size()
+ && !tokenizedSplitString.get(tokenIndex.getValue() + 1).isToken;
+ }
+
+ /**
+ * Re-implementation from scratch of
+ * tokens.split("token1|token2|token3|...")
with the constraint
+ * that quoted strings are kept intact and unmatched.
+ *
+ * @param tokens
+ * List of tokens to match
+ * @param getTokenType
+ * Mapping of token -> tokenType
+ * @param tokenizedSplitString
+ * User input with quoted strings tagged. This method will modify
+ * tokenizedSplitString in-place.
+ * @return Indexes of matched tokens
+ */
+ private static Map splitByTokens(List tokens, HashMap getTokenType,
+ List tokenizedSplitString) {
+ Map tokenIndices = new HashMap();
+ for (int i = 0; i < tokenizedSplitString.size(); i++) { // Java doesn't eager-evaluate the terminating condition
+ TokenizedString currString = tokenizedSplitString.get(i);
+ if (currString.isQuote) {
+ continue;
+ }
+
+ // Record token.
+ if (currString.isToken) {
+ tokenIndices.put(getTokenType.get(currString.string), i);
+ tokens.remove(currString.string);
+ continue;
+ }
+
+ // Try to match all the tokens
+ for (String token : tokens) {
+ Matcher m = Pattern.compile(String.format("\\b%s\\b", token), Pattern.CASE_INSENSITIVE)
+ .matcher(currString.string);
+ if (!m.find()) {
+ continue;
+ }
+
+ // Found. Replace current element with split elements.
+ String preString = currString.string.substring(0, m.start()).trim();
+ String postString = currString.string.substring(m.end(), currString.string.length()).trim();
+
+ tokenizedSplitString.remove(i);
+ List replacedSplitStrings = new ArrayList();
+ if (!preString.isEmpty()) {
+ replacedSplitStrings.add(new TokenizedString(preString, false, false));
+ }
+ replacedSplitStrings.add(new TokenizedString(token, true, false));
+ if (!postString.isEmpty()) {
+ replacedSplitStrings.add(new TokenizedString(postString, false, false));
+ }
+ tokenizedSplitString.addAll(i, replacedSplitStrings);
+
+ // Restart outer loop at current index.
+ i--;
+ break;
+ }
+ }
+ return tokenIndices;
+ }
+
+}
+
+```
+###### \java\seedu\todo\controllers\Controller.java
+``` java
+/**
+ * Contains the logic required to appropriately interpret and process user input
+ * from the views.
+ */
+public abstract class Controller {
+
+ public abstract CommandDefinition getCommandDefinition();
+
+ /**
+ * Given a command keyword, performs a case-insensitive match.
+ *
+ * @param keyword Keyword to match
+ * @return confidence True if the command keyword matches.
+ */
+ public boolean matchCommandKeyword(String keyword) {
+ return getCommandDefinition().getCommandKeyword().equalsIgnoreCase(keyword);
+ }
+
+ /**
+ * Processes the user input.
+ *
+ * @param input
+ * User input
+ */
+ public abstract void process(String input) throws ParseException;
+
+}
+```
+###### \java\seedu\todo\controllers\DestroyController.java
+``` java
+/**
+ * Controller to destroy a CalendarItem.
+ */
+public class DestroyController extends Controller {
+
+ private static final String DESTROY_TEMPLATE = "destroy %d";
+ private static final String NAME = "Destroy";
+ private static final String DESCRIPTION = "Destroys a task/event by listed index";
+ private static final String COMMAND_SYNTAX = "destroy ";
+ private static final String COMMAND_KEYWORD = "destroy";
+
+ public static final String MESSAGE_DELETE_SUCCESS = "Item deleted successfully!\n" + "To undo, type \"undo\".";
+ public static final String MESSAGE_INDEX_OUT_OF_RANGE = "Could not delete task/event: Invalid index provided!";
+ public static final String MESSAGE_MISSING_INDEX = "Please specify the index of the item to delete.";
+ public static final String MESSAGE_INDEX_NOT_NUMBER = "Index has to be a number!";
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ @Override
+ public void process(String args) {
+ // Extract param
+ String param = args.replaceFirst("destroy", "").trim();
+
+ if (param.length() <= 0) {
+ Renderer.renderDisambiguation(COMMAND_SYNTAX, MESSAGE_MISSING_INDEX);
+ return;
+ }
+
+ assert param.length() > 0;
+
+ // Get index.
+ int index = 0;
+ try {
+ index = Integer.decode(param);
+ } catch (NumberFormatException e) {
+ Renderer.renderDisambiguation(COMMAND_SYNTAX, MESSAGE_INDEX_NOT_NUMBER);
+ return;
+ }
+
+ // Get record
+ EphemeralDB edb = EphemeralDB.getInstance();
+ CalendarItem calendarItem = edb.getCalendarItemsByDisplayedId(index);
+ TodoListDB db = TodoListDB.getInstance();
+
+ if (calendarItem == null) {
+ Renderer.renderDisambiguation(String.format(DESTROY_TEMPLATE, index), MESSAGE_INDEX_OUT_OF_RANGE);
+ return;
+ }
+
+ assert calendarItem != null;
+
+ if (calendarItem instanceof Task) {
+ db.destroyTask((Task) calendarItem);
+ } else {
+ db.destroyEvent((Event) calendarItem);
+ }
+
+ // Re-render
+ Renderer.renderIndex(db, MESSAGE_DELETE_SUCCESS);
+ }
+
+}
+```
+###### \java\seedu\todo\controllers\FindController.java
+``` java
+/**
+ * Controller to find task/event by keyword
+ */
+public class FindController extends Controller {
+
+ private static final String NAME = "Find";
+ private static final String DESCRIPTION = "Find tasks and events based on the provided keyword.";
+ private static final String COMMAND_SYNTAX = "find [name]";
+ private static final String COMMAND_KEYWORD = "find";
+
+ public static final String MESSAGE_LISTING_SUCCESS = "A total of %s %s and %s %s found!";
+ public static final String MESSAGE_LISTING_FAILURE = "No tasks or events found!";
+ public static final String STRING_SPACE = " ";
+
+ private static CommandDefinition commandDefinition = new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX,
+ COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ @Override
+ public void process(String input) throws ParseException {
+ input = input.replaceFirst(COMMAND_KEYWORD, "").trim();
+ List namesToFind = Arrays.asList(input.split(STRING_SPACE));
+
+ List> taskPredicates = new ArrayList>();
+ taskPredicates.add(Task.predByNameAny(namesToFind));
+ List tasks = Task.where(taskPredicates);
+
+ List> eventPredicates = new ArrayList>();
+ eventPredicates.add(Event.predByNameAny(namesToFind));
+ List events = Event.where(eventPredicates);
+
+ if (tasks.size() == 0 && events.size() == 0) {
+ Renderer.renderIndex(TodoListDB.getInstance(), MESSAGE_LISTING_FAILURE);
+ } else {
+ String consoleMessage = String.format(MESSAGE_LISTING_SUCCESS, tasks.size(),
+ StringUtil.pluralizer(tasks.size(), "task", "tasks"), events.size(),
+ StringUtil.pluralizer(events.size(), "event", "events"));
+ Renderer.renderSelected(TodoListDB.getInstance(), consoleMessage, tasks, events);
+ }
+ }
+}
+```
+###### \java\seedu\todo\controllers\RedoController.java
+``` java
+/**
+ * Controller to redo a database commit.
+ */
+public class RedoController extends Controller {
+
+ private static final String NAME = "Redo";
+ private static final String DESCRIPTION = "Redo your last undo(s).";
+ private static final String COMMAND_SYNTAX = "redo ";
+ private static final String COMMAND_KEYWORD = "redo";
+ private static final String REDO_TEMPLATE = "redo %s";
+ private static final String INDEX_FIELD = "";
+
+ private static final String MESSAGE_SUCCESS = "Successfully redid %s %s!\nTo undo, type \"undo\".";
+ private static final String MESSAGE_MULTIPLE_FAILURE = "We cannot redo %s %s! At most, you can redo %s %s.";
+ private static final String MESSAGE_FAILURE = "There is no command to redo!";
+ private static final String MESSAGE_INDEX_NOT_NUMBER = "Index has to be a number!";
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ /**
+ * Get the token definitions for use with tokenizer
.
+ * This method exists primarily because Java does not support HashMap
+ * literals...
+ *
+ * @return tokenDefinitions
+ */
+ private static Map getTokenDefinitions() {
+ Map tokenDefinitions = new HashMap();
+ tokenDefinitions.put("default", new String[] {"redo"});
+ return tokenDefinitions;
+ }
+
+ @Override
+ public void process(String input) throws ParseException {
+
+ Map parsedResult;
+ parsedResult = Tokenizer.tokenize(getTokenDefinitions(), input);
+
+ int numRedo = 1;
+ if (parsedResult.get("default")[1] != null) {
+ try {
+ numRedo = Integer.decode(parsedResult.get("default")[1]);
+ } catch (NumberFormatException e) {
+ Renderer.renderDisambiguation(String.format(REDO_TEMPLATE, INDEX_FIELD), MESSAGE_INDEX_NOT_NUMBER);
+ return;
+ }
+ }
+
+ // We don't really have a nice way to support SQL transactions, so yeah >_<
+ TodoListDB db = TodoListDB.getInstance();
+
+ // Attempt to redo DB. If fail, exit method.
+ if (!attemptRedo(numRedo, db)) {
+ return;
+ }
+
+ // Reload DB
+ db = TodoListDB.getInstance();
+
+ // Render
+ Renderer.renderIndex(db, String.format(MESSAGE_SUCCESS, numRedo,
+ StringUtil.pluralizer(numRedo, "command", "commands")));
+ }
+
+ /**
+ * Attempt to redo the DB by numRedo
steps. If this fails, this
+ * method will update the console message with appropriate error messages.
+ *
+ * @param numRedo Number of steps to redo
+ * @param db TodoListDB instance
+ * @return true if redo attempt successful, false otherwise
+ */
+ private boolean attemptRedo(int numRedo, TodoListDB db) {
+ if (numRedo <= 0 || db.redoSize() <= 0) {
+ UiManager.updateConsoleMessage(MESSAGE_FAILURE);
+ return false;
+ }
+ if (db.redoSize() < numRedo) {
+ UiManager.updateConsoleMessage(String.format(MESSAGE_MULTIPLE_FAILURE,
+ numRedo, StringUtil.pluralizer(numRedo, "command", "commands"),
+ db.redoSize(), StringUtil.pluralizer(db.redoSize(), "command", "commands")));
+ return false;
+ }
+ for (int i = 0; i < numRedo; i++) {
+ if (!db.redo()) {
+ UiManager.updateConsoleMessage(MESSAGE_FAILURE);
+ return false;
+ }
+ }
+ return true;
+ }
+}
+```
+###### \java\seedu\todo\controllers\UnaliasController.java
+``` java
+/**
+ * Controller to unalias an existing alias.
+ */
+public class UnaliasController extends Controller {
+
+ private static final String NAME = "Unalias";
+ private static final String DESCRIPTION = "Deletes an existing alias pair.";
+ private static final String COMMAND_SYNTAX = "unalias ";
+ private static final String COMMAND_KEYWORD = "unalias";
+
+ private static final String MESSAGE_DESTROY_SUCCESS = "Successfully destroyed alias!";
+ private static final String MESSAGE_INVALID_INPUT = "Invalid alias parameters! Alias inputs must consist solely "
+ + "of alphabetical characters.";
+ private static final String MESSAGE_ALIAS_NOT_EXISTS = "Specified alias key doesn't exist!";
+ private static final String SAVE_ERROR = "There was an error saving your aliases. Please try again.";
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ @Override
+ public void process(String input) throws ParseException {
+ String aliasKey = input.replaceFirst("unalias", "").trim();
+
+ if (aliasKey.isEmpty() || !validateAlias(aliasKey)) {
+ renderDisambiguation(aliasKey, MESSAGE_INVALID_INPUT);
+ return;
+ }
+
+ // Persist alias mapping
+ try {
+ if (destroyAlias(aliasKey)) {
+ Renderer.renderAlias(MESSAGE_DESTROY_SUCCESS);
+ } else {
+ renderDisambiguation(aliasKey, MESSAGE_ALIAS_NOT_EXISTS);
+ }
+ } catch (IOException e) {
+ Renderer.renderAlias(SAVE_ERROR);
+ return;
+ }
+ }
+
+ private static boolean destroyAlias(String aliasKey) throws IOException {
+ Config config = ConfigCenter.getInstance().getConfig();
+ Map aliases = config.getAliases();
+ if (aliases.remove(aliasKey) == null) {
+ return false;
+ }
+ ConfigCenter.getInstance().saveConfig(config);
+ return true;
+ }
+
+ /**
+ * Validates that string is sanitized and safe for aliasing.
+ *
+ * @param alias string to check
+ * @return true if string is sanitized, false otherwise
+ */
+ private static boolean validateAlias(String alias) {
+ return alias.chars().allMatch(Character::isLetter);
+ }
+
+ private static void renderDisambiguation(String aliasKey, String message) {
+ String sanitizedAliasKey = StringUtil.sanitize(aliasKey);
+ sanitizedAliasKey = StringUtil.replaceEmpty(sanitizedAliasKey, "");
+ Renderer.renderDisambiguation(String.format("unalias \"%s\"", sanitizedAliasKey), message);
+ }
+
+}
+```
+###### \java\seedu\todo\controllers\UndoController.java
+``` java
+/**
+ * Controller to undo a database commit.
+ */
+public class UndoController extends Controller {
+
+ private static final String NAME = "Undo";
+ private static final String DESCRIPTION = "Undo your last action(s) to the list of tasks/events.";
+ private static final String COMMAND_SYNTAX = "undo ";
+ private static final String COMMAND_KEYWORD = "undo";
+ private static final String UNDO_TEMPLATE = "undo %s";
+ private static final String INDEX_FIELD = "";
+
+ private static final String MESSAGE_SUCCESS = "Successfully undid %s %s!\nTo redo, type \"redo\".";
+ private static final String MESSAGE_MULTIPLE_FAILURE = "We cannot undo %s %s! At most, you can undo %s %s.";
+ private static final String MESSAGE_FAILURE = "There is no command to undo!";
+ private static final String MESSAGE_INDEX_NOT_NUMBER = "Index has to be a number!";
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ /**
+ * Get the token definitions for use with tokenizer
.
+ * This method exists primarily because Java does not support HashMap
+ * literals...
+ *
+ * @return tokenDefinitions
+ */
+ private static Map getTokenDefinitions() {
+ Map tokenDefinitions = new HashMap();
+ tokenDefinitions.put("default", new String[] {"undo"});
+ return tokenDefinitions;
+ }
+
+ @Override
+ public void process(String input) throws ParseException {
+
+ Map parsedResult;
+ parsedResult = Tokenizer.tokenize(getTokenDefinitions(), input);
+
+ int numUndo = 1;
+ if (parsedResult.get("default")[1] != null) {
+ try {
+ numUndo = Integer.decode(parsedResult.get("default")[1]);
+ } catch (NumberFormatException e) {
+ Renderer.renderDisambiguation(String.format(UNDO_TEMPLATE, INDEX_FIELD), MESSAGE_INDEX_NOT_NUMBER);
+ return;
+ }
+ }
+
+ // We don't really have a nice way to support SQL transactions, so yeah >_<
+ TodoListDB db = TodoListDB.getInstance();
+
+ // Attempt to undo DB. If fail, exit method.
+ if (!attemptUndo(numUndo, db)) {
+ return;
+ }
+
+ // Reload DB
+ db = TodoListDB.getInstance();
+
+ // Render
+ Renderer.renderIndex(db, String.format(MESSAGE_SUCCESS, numUndo,
+ StringUtil.pluralizer(numUndo, "command", "commands")));
+ }
+
+ /**
+ * Attempt to undo the DB by numUndo
steps. If this fails, this
+ * method will update the console message with appropriate error messages.
+ *
+ * @param numUndo Number of steps to undo
+ * @param db TodoListDB instance
+ * @return true if undo attempt successful, false otherwise
+ */
+ private boolean attemptUndo(int numUndo, TodoListDB db) {
+ if (numUndo <= 0 || db.undoSize() <= 0) {
+ UiManager.updateConsoleMessage(MESSAGE_FAILURE);
+ return false;
+ }
+ if (db.undoSize() < numUndo) {
+ UiManager.updateConsoleMessage(String.format(MESSAGE_MULTIPLE_FAILURE,
+ numUndo, StringUtil.pluralizer(numUndo, "command", "commands"),
+ db.undoSize(), StringUtil.pluralizer(db.undoSize(), "command", "commands")));
+ return false;
+ }
+ for (int i = 0; i < numUndo; i++) {
+ if (!db.undo()) {
+ UiManager.updateConsoleMessage(MESSAGE_FAILURE);
+ return false;
+ }
+ }
+ return true;
+ }
+}
+```
+###### \java\seedu\todo\controllers\UpdateController.java
+``` java
+/**
+ * Controller to update a CalendarItem.
+ */
+public class UpdateController extends Controller {
+
+ private static final String NAME = "Update";
+ private static final String DESCRIPTION = "Updates a task by listed index.";
+ private static final String COMMAND_SYNTAX = "update by ";
+ private static final String COMMAND_KEYWORD = "update";
+
+ public static final String MESSAGE_UPDATE_SUCCESS = "Item successfully updated!";
+ public static final String MESSAGE_INVALID_ITEM_OR_PARAM = "Please specify a valid index and parameter to update!";
+ public static final String MESSAGE_CANNOT_PARSE_DATE = "We could not parse the date in your previous command, please correct it.";
+
+ public static final String STRING_NULL = "null";
+ public static final String UPDATE_EVENT_TEMPLATE = "update %s [name \"%s\"] [from \"%s\" to \"%s\"]";
+ public static final String UPDATE_TASK_TEMPLATE = "update %s [name \"%s\"] [by \"%s\"]";
+ public static final String START_TIME_FIELD = "";
+ public static final String END_TIME_FIELD = "";
+ public static final String DEADLINE_FIELD = "";
+ public static final String NAME_FIELD = "";
+ public static final String INDEX_FIELD = "";
+
+ private static CommandDefinition commandDefinition = new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ /**
+ * Get the token definitions for use with tokenizer
.
+ * This method exists primarily because Java does not support HashMap
+ * literals...
+ *
+ * @return tokenDefinitions
+ */
+ private static Map getTokenDefinitions() {
+ Map tokenDefinitions = new HashMap();
+ tokenDefinitions.put("default", new String[] { "update" });
+ tokenDefinitions.put("name", new String[] { "name" });
+ tokenDefinitions.put("time", new String[] { "at", "by", "on", "before", "time" });
+ tokenDefinitions.put("timeFrom", new String[] { "from" });
+ tokenDefinitions.put("timeTo", new String[] { "to" });
+ return tokenDefinitions;
+ }
+
+ @Override
+ public void process(String input) throws ParseException {
+ Map parsedResult;
+ parsedResult = Tokenizer.tokenize(getTokenDefinitions(), input);
+
+ // Name
+ String name = parseName(parsedResult);
+
+ // Time
+ String[] naturalDates = DateParser.extractDatePair(parsedResult);
+ String naturalFrom = naturalDates[0];
+ String naturalTo = naturalDates[1];
+
+ // Record index
+ Integer recordIndex = parseIndex(parsedResult);
+
+ // Retrieve record and check if task or event
+ EphemeralDB edb = EphemeralDB.getInstance();
+ CalendarItem calendarItem = null;
+ boolean isTask;
+ try {
+ calendarItem = edb.getCalendarItemsByDisplayedId(recordIndex);
+ isTask = calendarItem.getClass() == Task.class;
+ } catch (NullPointerException e) {
+ // Assume task for disambiguation purposes since we can't tell
+ renderDisambiguation(true, recordIndex, name, naturalFrom, naturalTo, MESSAGE_INVALID_ITEM_OR_PARAM);
+ return;
+ }
+
+ // Parse natural date using Natty.
+ LocalDateTime dateFrom = null;
+ LocalDateTime dateTo = null;
+ try {
+ // Allow exception for "null"
+ dateFrom = (naturalFrom == null || STRING_NULL.equals(naturalFrom.trim()))
+ ? null : DateParser.parseNatural(naturalFrom);
+ dateTo = naturalTo == null ? null : DateParser.parseNatural(naturalTo);
+ } catch (InvalidNaturalDateException e) {
+ renderDisambiguation(isTask, recordIndex, name, naturalFrom, naturalTo, MESSAGE_CANNOT_PARSE_DATE);
+ return;
+ }
+
+ // Validate isTask, name and times.
+ if (!validateParams(isTask, calendarItem, name, dateFrom, dateTo, naturalFrom)) {
+ renderDisambiguation(isTask, (int) recordIndex, name, naturalFrom, naturalTo, null);
+ return;
+ }
+
+ // Update and persist task / event.
+ TodoListDB db = TodoListDB.getInstance();
+ updateCalendarItem(db, calendarItem, isTask, name, dateFrom, dateTo, naturalFrom);
+
+ // Re-render
+ Renderer.renderIndex(db, MESSAGE_UPDATE_SUCCESS);
+ }
+
+ /**
+ * Extracts the record index from parsedResult.
+ *
+ * @param parsedResult
+ * @return Integer index if parse was successful, null otherwise.
+ */
+ private Integer parseIndex(Map parsedResult) {
+ String indexStr = null;
+ try {
+ if (parsedResult.get("default") != null && parsedResult.get("default")[1] != null) {
+ indexStr = parsedResult.get("default")[1].trim();
+ return Integer.decode(indexStr);
+ }
+ } catch (NumberFormatException e) {
+ // Everything is fine, just return null if cannot.
+ }
+ return null;
+ }
+
+ /**
+ * Extracts the name to be updated from parsedResult.
+ *
+ * @param parsedResult
+ * @return String name if found, null otherwise.
+ */
+ private String parseName(Map parsedResult) {
+ if (parsedResult.get("name") != null && parsedResult.get("name")[1] != null) {
+ return parsedResult.get("name")[1];
+ }
+ return null;
+ }
+
+ /**
+ * Updates and persists a CalendarItem to the DB.
+ *
+ * @param db
+ * TodoListDB instance
+ * @param record
+ * Record to update
+ * @param isTask
+ * true if CalendarItem is a Task, false if Event
+ * @param name
+ * Display name of CalendarItem object
+ * @param dateFrom
+ * Due date for Task or start date for Event
+ * @param dateTo
+ * End date for Event
+ */
+ private void updateCalendarItem(TodoListDB db, CalendarItem record, boolean isTask, String name,
+ LocalDateTime dateFrom, LocalDateTime dateTo, String naturalFrom) {
+ // Update name if not null
+ if (name != null) {
+ record.setName(name);
+ }
+
+ // Update time
+ if (isTask) {
+ Task task = (Task) record;
+ if (dateFrom != null) {
+ task.setDueDate(dateFrom);
+ } else if (naturalFrom != null && naturalFrom.trim().equals(STRING_NULL)) {
+ task.setDueDate(null);
+ }
+ } else {
+ Event event = (Event) record;
+ if (dateFrom != null) {
+ event.setStartDate(dateFrom);
+ }
+ if (dateTo != null) {
+ event.setEndDate(dateTo);
+ }
+ }
+
+ // Persist
+ db.save();
+ }
+
+ /**
+ * Validate that applying the update changes to the record will not result
+ * in an inconsistency.
+ *
+ *
+ * - Fail if name is invalid
+ * - Fail if no update changes
+ *
+ *
+ * Tasks:
+ *
+ * - Fail if task has a dateTo
+ *
+ *
+ * Events:
+ *
+ * - Fail if event does not have both dateFrom and dateTo
+ * - Fail if event has a dateTo that is before dateFrom
+ *
+ *
+ * @param isTask
+ * @param name
+ * @param dateFrom
+ * @param dateTo
+ * @return
+ */
+ private boolean validateParams(boolean isTask, CalendarItem record, String name, LocalDateTime dateFrom,
+ LocalDateTime dateTo, String naturalFrom) {
+ // We really need proper ActiveRecord validation and rollback, sigh...
+
+ if (!validateNameDateChange(name, dateFrom, dateTo, naturalFrom)) {
+ return false;
+ }
+
+ if (isTask) {
+ // Fail if task has a dateTo
+ if (dateTo != null) {
+ return false;
+ }
+ } else {
+ Event event = (Event) record;
+ if (!validateUpdatedEventDates(event, dateFrom, dateTo)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks that there is at least one update param specified.
+ *
+ * @param name
+ * @param dateFrom
+ * @param dateTo
+ * @param naturalFrom
+ * @return
+ */
+ private boolean validateNameDateChange(String name, LocalDateTime dateFrom, LocalDateTime dateTo, String naturalFrom) {
+ return !(name == null && dateFrom == null && dateTo == null
+ && !STRING_NULL.equals(naturalFrom));
+ }
+
+ /**
+ * Validates an event to be updated with dateFrom and dateTo.
+ *
+ * @param oldEvent
+ * @param dateFrom
+ * @param dateTo
+ * @return
+ */
+ private boolean validateUpdatedEventDates(Event oldEvent, LocalDateTime dateFrom, LocalDateTime dateTo) {
+ // Take union of existing fields and update params
+ LocalDateTime newDateFrom = (dateFrom == null) ? oldEvent.getStartDate() : dateFrom;
+ LocalDateTime newDateTo = (dateTo == null) ? oldEvent.getEndDate() : dateTo;
+
+ return (newDateFrom != null && newDateTo != null && !newDateTo.isBefore(newDateFrom));
+ }
+
+ /**
+ * Disambiguate an ambiguous input by auto-populating a templated command on
+ * a best-effort basis.
+ *
+ * @param isTask
+ * @param name
+ * @param naturalFrom
+ * @param naturalTo
+ * @param errorMessage
+ */
+ private void renderDisambiguation(boolean isTask, Integer recordIndex, String name, String naturalFrom,
+ String naturalTo, String errorMessage) {
+ name = StringUtil.replaceEmpty(name, NAME_FIELD);
+
+ String disambiguationString;
+ String indexStr = (recordIndex == null) ? INDEX_FIELD : recordIndex.toString();
+
+ if (isTask) {
+ naturalFrom = StringUtil.replaceEmpty(naturalFrom, DEADLINE_FIELD);
+ disambiguationString = String.format(UPDATE_TASK_TEMPLATE, indexStr, name, naturalFrom);
+ } else {
+ naturalFrom = StringUtil.replaceEmpty(naturalFrom, START_TIME_FIELD);
+ naturalTo = StringUtil.replaceEmpty(naturalTo, END_TIME_FIELD);
+ disambiguationString = String.format(UPDATE_EVENT_TEMPLATE, indexStr, name, naturalFrom, naturalTo);
+ }
+
+ // Show an error in the console
+ Renderer.renderDisambiguation(disambiguationString, errorMessage);
+ }
+}
+```
+###### \java\seedu\todo\models\CalendarItem.java
+``` java
+/**
+ * CalendarItem interface
+ */
+public interface CalendarItem {
+
+ /**
+ * Get the display name of the calendar item.
+ * @return name
+ */
+ public String getName();
+
+ /**
+ * Set the display name of the calendar item.
+ * @param name
+ */
+ public void setName(String name);
+
+ /**
+ * Get the calendar date of the calendar item. This is mostly for display
+ * and sorting purposes.
+ *
+ * @return datetime
+ */
+ public LocalDateTime getCalendarDateTime();
+
+ /**
+ * Set the calendar date of the calendar item. The behavior of this will
+ * depend on the implementation, but it is guaranteed that the variable
+ * being set by this method is the variable that is returned by
+ * getCalendarDT()
.
+ *
+ * It is unclear why one would ever set a calendar item's datetime using
+ * this method, but it is here for completeness.
+ *
+ * @param datetime
+ */
+ public void setCalendarDateTime(LocalDateTime datetime);
+
+ /**
+ * Returns true if the calendar item has passed, false otherwise.
+ *
+ * @return isOver
+ */
+ public boolean isOver();
+
+ /**
+ * Returns the current tag list that belong to the CalendarItem, mainly for displaying purpose
+ *
+ * @return ArrayList tags
+```
+###### \java\seedu\todo\models\Event.java
+``` java
+/**
+ * Event model
+ */
+public class Event implements CalendarItem {
+
+ private String name;
+ private LocalDateTime startDate;
+ private LocalDateTime endDate;
+ private ArrayList tagList = new ArrayList();
+
+ public static final int MAX_TAG_LIST_SIZE = 20;
+
+ /**
+ * Get the start date of an Event.
+ * @return startDate
+ */
+ public LocalDateTime getStartDate() {
+ return startDate;
+ }
+
+ /**
+ * Set the start date of an Event.
+ * @param startDate
+ */
+ public void setStartDate(LocalDateTime startDate) {
+ this.startDate = startDate;
+ }
+
+ /**
+ * Get the end date of an Event.
+ * @return endDate
+ */
+ public LocalDateTime getEndDate() {
+ return endDate;
+ }
+
+ /**
+ * Set the end date of an Event.
+ * @param endDate
+ */
+ public void setEndDate(LocalDateTime endDate) {
+ this.endDate = endDate;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public LocalDateTime getCalendarDateTime() {
+ return getStartDate();
+ }
+
+ @Override
+ public void setCalendarDateTime(LocalDateTime datetime) {
+ setStartDate(datetime);
+ }
+
+ @Override
+ public boolean isOver() {
+ if (endDate == null) {
+ return false;
+ } else {
+ return endDate.isBefore(LocalDateTime.now());
+ }
+ }
+
+ @Override
+```
+###### \java\seedu\todo\models\Event.java
+``` java
+ *
+ * Filtering methods intended to replace hacky one-filter-method-per-permutation from Yaocong.
+ * Seriously, why??!!
+ */
+ public static List where(List> predicates) {
+ List result = TodoListDB.getInstance().getAllEvents();
+ for (Predicate predicate : predicates) {
+ filter(predicate, result);
+ }
+ return result;
+ }
+
+ public static Predicate predByName(String name) {
+ return (Event event) -> Pattern.compile(String.format("\\b%s", name), Pattern.CASE_INSENSITIVE)
+ .matcher(event.getName()).find();
+ }
+
+ public static Predicate predByNameAny(List names) {
+ return (Event event) -> {
+ for (String name : names) {
+ if (predByName(name).test(event)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ }
+
+ public static Predicate predStartBefore(LocalDateTime date) {
+ return (Event event) -> event.getStartDate().isBefore(date);
+ }
+
+ public static Predicate predStartAfter(LocalDateTime date) {
+ return (Event event) -> event.getStartDate().isAfter(date);
+ }
+
+ public static Predicate predEndBefore(LocalDateTime date) {
+ return (Event event) -> event.getEndDate().isBefore(date);
+ }
+
+ public static Predicate predEndAfter(LocalDateTime date) {
+ return (Event event) -> event.getEndDate().isAfter(date);
+ }
+
+ public static Predicate predTag(String tag) {
+ return (Event event) -> {
+ for (String currTag : event.getTagList()) {
+ if (currTag.toLowerCase().equals(tag.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ };
+ }
+
+ public static void filter(Predicate predicate, List eventList) {
+ for (int i = eventList.size() - 1; i >= 0; i--) {
+ if (!predicate.test(eventList.get(i))) {
+ eventList.remove(i);
+ }
+ }
+ }
+
+}
+```
+###### \java\seedu\todo\models\Task.java
+``` java
+/**
+ * Task model
+ */
+public class Task implements CalendarItem {
+
+ private String name;
+ private LocalDateTime dueDate;
+ private boolean isCompleted = false;
+ private ArrayList tagList = new ArrayList();
+
+ public static final int MAX_TAG_LIST_SIZE = 20;
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Get the due date of a Task.
+ * @return dueDate
+ */
+ public LocalDateTime getDueDate() {
+ return dueDate;
+ }
+
+ /**
+ * Set the due date of a Task.
+ * @param dueDate
+ */
+ public void setDueDate(LocalDateTime dueDate) {
+ this.dueDate = dueDate;
+ }
+
+ @Override
+ public LocalDateTime getCalendarDateTime() {
+ return getDueDate();
+ }
+
+ @Override
+ public void setCalendarDateTime(LocalDateTime date) {
+ setDueDate(date);
+ }
+
+ @Override
+ public boolean isOver() {
+ if (dueDate == null) {
+ return false;
+ } else {
+ return dueDate.isBefore(LocalDateTime.now());
+ }
+ }
+
+ /**
+ * Returns true if the Task is completed, false otherwise.
+ * @return isCompleted
+ */
+ public boolean isCompleted() {
+ return isCompleted;
+ }
+
+ /**
+ * Marks a Task as completed.
+ */
+ public void setCompleted() {
+ this.isCompleted = true;
+ }
+
+ /**
+ * Marks a Task as incomplete.
+ */
+ public void setIncomplete() {
+ this.isCompleted = false;
+ }
+
+ @Override
+```
+###### \java\seedu\todo\models\Task.java
+``` java
+ *
+ * Filtering methods intended to replace hacky one-filter-method-per-permutation from Yaocong.
+ * Seriously, why??!!
+ */
+ public static List where(List> predicates) {
+ List result = TodoListDB.getInstance().getAllTasks();
+ for (Predicate predicate : predicates) {
+ filter(predicate, result);
+ }
+ return result;
+ }
+
+ public static Predicate predByName(String name) {
+ return (Task task) -> Pattern.compile(String.format("\\b%s", name), Pattern.CASE_INSENSITIVE)
+ .matcher(task.getName()).find();
+ }
+
+ public static Predicate predByNameAny(List names) {
+ return (Task task) -> {
+ for (String name : names) {
+ if (predByName(name).test(task)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ }
+
+ public static Predicate predBeforeDueDate(LocalDateTime date) {
+ return (Task task) -> task.getDueDate() != null && task.getDueDate().isBefore(date);
+ }
+
+ public static Predicate predAfterDueDate(LocalDateTime date) {
+ return (Task task) -> task.getDueDate() != null && task.getDueDate().isAfter(date);
+ }
+
+ public static Predicate predCompleted(boolean completed) {
+ return (Task task) -> task.isCompleted() == completed;
+ }
+
+ public static Predicate predTag(String tag) {
+ return (Task task) -> {
+ for (String currTag : task.getTagList()) {
+ if (currTag.toLowerCase().equals(tag.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ };
+ }
+
+ public static void filter(Predicate predicate, List taskList) {
+ for (int i = taskList.size() - 1; i >= 0; i--) {
+ if (!predicate.test(taskList.get(i))) {
+ taskList.remove(i);
+ }
+ }
+ }
+
+}
+```
+###### \java\seedu\todo\models\TodoListDB.java
+``` java
+/**
+ * This class holds the entire persistent database for the TodoList app.
+ *
+ * - This is a singleton class. For obvious reasons, the TodoList app should
+ * not be working with multiple DB instances simultaneously.
+ * - Object to object dynamic references should not be expected to survive
+ * serialization.
+ *
+ */
+public class TodoListDB {
+
+ private static TodoListDB instance = null;
+ private static Storage storage = new JsonStorage();
+
+ private Set tasks = new LinkedHashSet();
+ private Set events = new LinkedHashSet();
+ private Map aliases = new HashMap();
+ private Set tagList = new LinkedHashSet();
+
+ protected TodoListDB() {
+ // Prevent instantiation.
+ }
+
+ /**
+ * Gets the singleton instance of the TodoListDB.
+ *
+ * @return TodoListDB
+ */
+ public static TodoListDB getInstance() {
+ if (instance == null) {
+ instance = new TodoListDB();
+ }
+ return instance;
+ }
+
+ public void setStorage(Storage storageToSet) {
+ storage = storageToSet;
+ }
+
+ /**
+ * Update the overall Tags that exist in the DB.
+ *
+ */
+ public void updateTagList(String tagName) {
+ tagList.add(tagName);
+ }
+
+ /**
+ * Get a list of Tags in the DB.
+ *
+ * @return tagList
+ */
+ public List getTagList() {
+ return new ArrayList(tagList);
+ }
+
+ /**
+ * Count tags which are already inserted into the db
+ *
+ * @return Number of tags
+ */
+ public int countTagList() {
+ return tagList.size();
+ }
+
+ public Map getAliases() {
+ return aliases;
+ }
+
+ /**
+ * Get a list of Tasks in the DB.
+ *
+ * @return tasks
+ */
+ public List getAllTasks() {
+ return new ArrayList(tasks);
+ }
+
+
+ /**
+ * Count tasks which are not marked as complete, where {@code isComplete} is false.
+ *
+ * @return Number of incomplete tasks
+ */
+ public int countIncompleteTasks() {
+ int count = 0;
+ for (Task task : tasks) {
+ if (!task.isCompleted()) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Count tasks which are overdue, where {@code dueDate} is before the time now.
+ *
+ * @return Number of overdue tasks
+ */
+ public int countOverdueTasks() {
+ LocalDateTime now = LocalDateTime.now();
+ int count = 0;
+ for (Task task : tasks) {
+ LocalDateTime dueDate = task.getDueDate();
+ if (!task.isCompleted() && dueDate != null && dueDate.compareTo(now) < 0) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Get a list of Events in the DB.
+ *
+ * @return events
+ */
+ public List getAllEvents() {
+ return new ArrayList(events);
+ }
+
+ /**
+ * Count events which are in the future, where {@code startDate} is after the time now.
+ *
+ * @return Number of future events
+ */
+ public int countFutureEvents() {
+ LocalDateTime now = LocalDateTime.now();
+ int count = 0;
+ for (Event event : events) {
+ LocalDateTime startDate = event.getStartDate();
+ if (startDate != null && startDate.compareTo(now) >= 0) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Create a new Task in the DB and return it.
+ * The new record is not persisted until save
is explicitly
+ * called.
+ *
+ * @return task
+ */
+ public Task createTask() {
+ Task task = new Task();
+ tasks.add(task);
+ return task;
+ }
+
+ /**
+ * Destroys a Task in the DB and persists the commit.
+ *
+ * @param task
+ * @return true if the save was successful, false otherwise
+ */
+ public boolean destroyTask(Task task) {
+ tasks.remove(task);
+ return save();
+ }
+
+ /**
+ * Destroys all `tasks` from the DB.
+ *
+ * @param tasks Tasks to remove
+ */
+ public void destroyTasks(List clearTasks) {
+ tasks.removeAll(clearTasks);
+ }
+
+ /**
+```
+###### \java\seedu\todo\models\TodoListDB.java
+``` java
+ *
+ * Create a new Event in the DB and return it.
+ * The new record is not persisted until save
is explicitly
+ * called.
+ *
+ * @return event
+ */
+ public Event createEvent() {
+ Event event = new Event();
+ events.add(event);
+ return event;
+ }
+
+ /**
+ * Destroys an Event in the DB and persists the commit.
+ *
+ * @param event
+ * @return true if the save was successful, false otherwise
+ */
+ public boolean destroyEvent(Event event) {
+ events.remove(event);
+ return save();
+ }
+
+ /**
+ * Destroys all `events` from the DB.
+ *
+ * @param tasks Tasks to remove
+ */
+ public void destroyEvents(List clearEvents) {
+ events.removeAll(clearEvents);
+ }
+
+ /**
+```
+###### \java\seedu\todo\models\TodoListDB.java
+``` java
+ *
+ * Explicitly persists the database to disk.
+ *
+ * @return true if the save was successful, false otherwise
+ */
+ public boolean save() {
+ try {
+ storage.save(this);
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Explicitly reloads the database from disk.
+ *
+ * @return true if the load was successful, false otherwise
+ */
+ public boolean load() {
+ try {
+ instance = storage.load();
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ public void move(String newPath) throws IOException {
+ storage.move(newPath);
+ }
+
+ /**
+ * Returns the maximum possible number of undos.
+ *
+ * @return undoSize
+ */
+ public int undoSize() {
+ return storage.undoSize();
+ }
+
+ /**
+ * Rolls back the DB by one commit.
+ *
+ * @return true if the rollback was successful, false otherwise
+ */
+ public boolean undo() {
+ try {
+ instance = storage.undo();
+ return true;
+ } catch (CannotUndoException | IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the maximum possible number of redos.
+ *
+ * @return redoSize
+ */
+ public int redoSize() {
+ return storage.redoSize();
+ }
+
+ /**
+ * Rolls forward the DB by one undo commit.
+ *
+ * @return true if the redo was successful, false otherwise
+ */
+ public boolean redo() {
+ try {
+ instance = storage.redo();
+ return true;
+ } catch (CannotRedoException | IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Get a list of events that are not over based on today date from the DB.
+ *
+ * @return events
+```
+###### \java\seedu\todo\storage\JsonStorage.java
+``` java
+/**
+ * JSON Storage for persisting and loading from disk.
+ */
+public class JsonStorage implements Storage {
+
+ // Ideally this would be a single circular-queue, but there is no such built-in
+ // mechanism, and we would really really like to keep this operation O(1).
+ private Deque> historyPatch = new ArrayDeque>();
+ private Deque> futurePatch = new ArrayDeque>();
+ private String currJson;
+ private final static int HISTORY_SIZE = 1000;
+ private final DiffMatchPatch dmp = new DiffMatchPatch();
+
+ private File getStorageFile() {
+ String filePath = ConfigCenter.getInstance().getConfig().getDatabaseFilePath();
+ return new File(filePath);
+ }
+
+ /**
+ * Internal function to prune the history patches stored to ensure it does
+ * not exit HISTORY_SIZE.
+ */
+ private void pruneHistory() {
+ // Don't need to worry about future because it cannot exceed limit.
+ while (historyPatch.size() > HISTORY_SIZE) {
+ historyPatch.removeFirst();
+ }
+ }
+
+ @Override
+ public void move(String newPath) throws IOException {
+ boolean hasMoved = false;
+
+ try {
+ FileUtil.createParentDirsOfFile(new File(newPath));
+ } catch (IOException e) {
+ throw e;
+ }
+
+ try {
+ hasMoved = getStorageFile().renameTo(new File(newPath));
+ } catch (SecurityException e) {
+ throw new IOException(e.getMessage(), e.getCause());
+ }
+
+ if (!hasMoved) {
+ throw new IOException(String.format("Could not move file to \"%s\".", newPath));
+ }
+ }
+
+ @Override
+ public void save(TodoListDB db) throws JsonProcessingException, IOException {
+ String newJson = JsonUtil.toJsonString(db);
+
+ // Store the undo patch.
+ if (this.currJson != null) {
+ historyPatch.addLast(dmp.patchMake(newJson, this.currJson));
+ pruneHistory();
+ futurePatch.clear(); // A forward move nullifies all future patches.
+ }
+
+ // Update currJson and persist to disk.
+ this.currJson = newJson;
+ FileUtil.writeToFile(getStorageFile(), this.currJson);
+ }
+
+ @Override
+ public TodoListDB load() throws IOException {
+ this.currJson = FileUtil.readFromFile(getStorageFile());
+ historyPatch.clear(); // It does not make sense to preserve history on load.
+ return JsonUtil.fromJsonString(this.currJson, TodoListDB.class);
+ }
+
+ @Override
+ public TodoListDB undo() throws CannotUndoException, IOException {
+ // Get undo
+ LinkedList undoPatch;
+ try {
+ undoPatch = historyPatch.removeLast();
+ } catch (NoSuchElementException e) {
+ throw new CannotUndoException(e);
+ }
+
+ String newJson = (String)dmp.patchApply(undoPatch, this.currJson)[0];
+
+ // Create redo
+ LinkedList redoPatch = dmp.patchMake(newJson, this.currJson);
+ futurePatch.addLast(redoPatch);
+
+ // Apply undo
+ this.currJson = newJson;
+ FileUtil.writeToFile(getStorageFile(), this.currJson);
+ return JsonUtil.fromJsonString(this.currJson, TodoListDB.class);
+ }
+
+ @Override
+ public int undoSize() {
+ return historyPatch.size();
+ }
+
+ @Override
+ public TodoListDB redo() throws CannotRedoException, IOException {
+ // Get redo
+ LinkedList redoPatch;
+ try {
+ redoPatch = futurePatch.removeLast();
+ } catch (NoSuchElementException e) {
+ throw new CannotRedoException(e);
+ }
+
+ String newJson = (String)dmp.patchApply(redoPatch, this.currJson)[0];
+
+ // Create undo
+ LinkedList undoPatch = dmp.patchMake(newJson, this.currJson);
+ historyPatch.addLast(undoPatch);
+
+ // Apply redo
+ this.currJson = newJson;
+ FileUtil.writeToFile(getStorageFile(), this.currJson);
+ return JsonUtil.fromJsonString(this.currJson, TodoListDB.class);
+ }
+
+ @Override
+ public int redoSize() {
+ return futurePatch.size();
+ }
+
+}
+```
+###### \java\seedu\todo\storage\Storage.java
+``` java
+/**
+ * Storage interface for persisting and loading from disk.
+ */
+public interface Storage {
+
+ /**
+ * Persists a TodoListDB object to disk.
+ * @param db TodoListDB object
+ * @throws IOException If there is an error writing to disk.
+ */
+ public void save(TodoListDB db) throws IOException;
+
+ /**
+ * Loads a TodoListDB object from disk
+ * @return TodoListDB object
+ * @throws IOException If there is an error reading from disk.
+ */
+ public TodoListDB load() throws IOException;
+
+ public void move(String newPath) throws IOException;
+
+ /**
+ * Rolls back the DB by one commit, persists the DB, and returns a
+ * TodoListDB object.
+ *
+ * Undo information is on a per-session basis, and should not be persisted
+ * to disk.
+ *
+ * @return TodoListDB object
+ * @throws CannotUndoException
+ * If there is nothing to undo.
+ * @throws IOException
+ * If there is an error writing to disk.
+ */
+ public TodoListDB undo() throws CannotUndoException, IOException;
+
+ /**
+ * Rolls forward the DB by one undo commit, persists the DB, and returns a
+ * TodoListDB object.
+ *
+ * @return TodoListDB object
+ * @throws CannotRedoException
+ * If there is nothing to redo.
+ * @throws IOException
+ * If there is an error writing to disk.
+ */
+ public TodoListDB redo() throws CannotRedoException, IOException;
+
+ /**
+ * Returns the maximum possible number of undos.
+ * @return undoSize
+ */
+ public int undoSize();
+
+ /**
+ * Returns the maximum possible number of redos.
+ * @return
+ */
+ public int redoSize();
+
+}
+```
+###### \java\seedu\todo\ui\components\AliasItem.java
+``` java
+public class AliasItem extends MultiComponent {
+
+ private static final String FXML_PATH = "components/AliasItem.fxml";
+
+ // Props
+ public AliasDefinition aliasDefinition;
+
+ // FXML
+ @FXML
+ private Text aliasKey;
+ @FXML
+ private Text aliasValue;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ if (aliasDefinition != null) {
+ aliasKey.setText(aliasDefinition.getAliasKey());
+ aliasValue.setText(aliasDefinition.getAliasValue());
+ }
+ }
+
+}
+```
+###### \java\seedu\todo\ui\views\AliasView.java
+``` java
+public class AliasView extends View {
+
+ private static final String FXML_PATH = "views/AliasView.fxml";
+
+ private static final String ICON_PATH = "/images/icon-settings.png";
+ private static final String TEXT_INSTRUCTIONS = "Aliases make your life easier by allowing you to customize shortcuts!\n"
+ + "To set an alias, use the following command:\n alias ";
+
+ // FXML
+ @FXML
+ private Text aliasInstructionsText;
+ @FXML
+ private ImageView aliasImageView;
+ @FXML
+ private Pane aliasesPlaceholder;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ // Makes the Component full width wrt parent container.
+ FxViewUtil.makeFullWidth(this.mainNode);
+
+ // Set instructions
+ aliasInstructionsText.setText(TEXT_INSTRUCTIONS);
+
+ // Load image
+ aliasImageView.setImage(new Image(ICON_PATH));
+
+ // Get definitions
+ Map aliasMap = ConfigCenter.getInstance().getConfig().getAliases();
+ List> aliasDefinitions =
+ new ArrayList>(aliasMap.entrySet());
+
+ Comparator> aliasComparator = new Comparator>() {
+ @Override
+ public int compare(Entry o1, Entry o2) {
+ return o1.getKey().compareTo(o2.getKey());
+ }
+ };
+ Collections.sort(aliasDefinitions, aliasComparator);
+
+ // Clear items
+ AliasItem.reset(aliasesPlaceholder);
+
+ // Load items
+ loadAliases(aliasDefinitions);
+ }
+
+ /**
+ * Load aliases into view.
+ *
+ * @param aliasDefinitions List of aliasDefinitions
+ */
+ private void loadAliases(List> aliasDefinitions) {
+ for (Map.Entry aliasPair : aliasDefinitions) {
+ AliasItem item = load(primaryStage, aliasesPlaceholder, AliasItem.class);
+ item.aliasDefinition = new AliasDefinition(aliasPair.getKey(), aliasPair.getValue());
+ item.render();
+ }
+ }
+
+}
+```
+###### \resources\ui\components\AliasItem.fxml
+``` fxml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+###### \resources\ui\views\AliasView.fxml
+``` fxml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
diff --git a/collated/main/A0139812A.md b/collated/main/A0139812A.md
new file mode 100644
index 000000000000..2cad2be0ef3a
--- /dev/null
+++ b/collated/main/A0139812A.md
@@ -0,0 +1,3305 @@
+# A0139812A
+###### \java\seedu\todo\commons\core\CommandDefinition.java
+``` java
+/**
+ * A CommandDefinition encapsulates the definition of a
+ * command that is handled by a Controller.
+ */
+public class CommandDefinition {
+ private String commandName;
+ private String commandDescription;
+ private String commandSyntax;
+ private String commandKeyword;
+
+ public CommandDefinition(String name, String desc, String syntax, String keyword) {
+ commandName = name;
+ commandDescription = desc;
+ commandSyntax = syntax;
+ setCommandKeyword(keyword);
+ }
+
+ public String getCommandName() {
+ return commandName;
+ }
+
+ public void setCommandName(String commandName) {
+ this.commandName = commandName;
+ }
+
+ public String getCommandDescription() {
+ return commandDescription;
+ }
+
+ public void setCommandDescription(String commandDescription) {
+ this.commandDescription = commandDescription;
+ }
+
+ public String getCommandSyntax() {
+ return commandSyntax;
+ }
+
+ public void setCommandSyntax(String commandSyntax) {
+ this.commandSyntax = commandSyntax;
+ }
+
+ public String getCommandKeyword() {
+ return commandKeyword;
+ }
+
+ public void setCommandKeyword(String commandKeyword) {
+ this.commandKeyword = commandKeyword;
+ }
+
+}
+```
+###### \java\seedu\todo\commons\core\Config.java
+``` java
+/**
+ * Container class to contain config values used by the app.
+ */
+public class Config {
+
+ public static final String DEFAULT_CONFIG_FILE = "config.json";
+
+ // Config values customizable through config file
+ private String appTitle = "GetShitDone";
+ private Level logLevel = Level.INFO;
+ private String databaseFilePath = "database.json";
+ private Map aliases = new HashMap();
+
+ public Config() {
+ }
+
+ public String getAppTitle() {
+ return appTitle;
+ }
+
+ public void setAppTitle(String appTitle) {
+ this.appTitle = appTitle;
+ }
+
+ public Level getLogLevel() {
+ return logLevel;
+ }
+
+ public void setLogLevel(Level logLevel) {
+ this.logLevel = logLevel;
+ }
+
+ public String getDatabaseFilePath() {
+ return databaseFilePath;
+ }
+
+ public void setDatabaseFilePath(String databaseFilePath) {
+ this.databaseFilePath = databaseFilePath;
+ }
+
+ public Map getAliases() {
+ return aliases;
+ }
+
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this){
+ return true;
+ }
+ if (!(other instanceof Config)){ //this handles null as well.
+ return false;
+ }
+
+ Config o = (Config)other;
+
+ return Objects.equals(appTitle, o.appTitle)
+ && Objects.equals(logLevel, o.logLevel)
+ && Objects.equals(databaseFilePath, o.databaseFilePath);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(appTitle, logLevel, databaseFilePath);
+ }
+
+ @Override
+ public String toString(){
+ StringBuilder sb = new StringBuilder();
+ sb.append("App title : " + appTitle);
+ sb.append("\nCurrent log level : " + logLevel);
+ sb.append("\nLocal data file location : " + databaseFilePath);
+ return sb.toString();
+ }
+
+ public List getDefinitions() {
+ ConfigDefinition configAppTitle = new ConfigDefinition("appTitle", "App Title", appTitle);
+ ConfigDefinition configDatabaseFilePath = new ConfigDefinition("databaseFilePath", "Database File Path", databaseFilePath);
+
+ return Arrays.asList(configAppTitle, configDatabaseFilePath);
+ }
+
+ public List getDefinitionsNames() {
+ List definitions = getDefinitions();
+ List names = new ArrayList<>();
+
+ for (ConfigDefinition definition : definitions) {
+ names.add(definition.getConfigName());
+ }
+
+ return names;
+ }
+
+}
+```
+###### \java\seedu\todo\commons\core\ConfigCenter.java
+``` java
+/**
+ * Singleton to store the current Config used by the app.
+ * This is especially necessary in a testing environment,
+ * and needs to be decoupled with the MainApp instance,
+ * but rather instantiated independently.
+ */
+public class ConfigCenter {
+ private static final Logger logger = LogsCenter.getLogger(MainApp.class);
+
+ private static ConfigCenter instance;
+
+ private Config config;
+ private String configFilePath;
+
+ public static ConfigCenter getInstance() {
+ if (instance == null) {
+ instance = new ConfigCenter();
+ }
+
+ return instance;
+ }
+
+ public void setConfigFilePath(String path) {
+ configFilePath = path;
+ }
+
+ public Config getConfig() {
+ if (config == null) {
+ Optional configOptional;
+
+ try {
+ configOptional = ConfigUtil.readConfig(configFilePath);
+ config = configOptional.orElse(new Config());
+ } catch (DataConversionException e) {
+ logger.warning("Config file at " + configFilePath + " is not in the correct format. " +
+ "Using default config properties");
+ }
+ }
+
+ return config;
+ }
+
+ public void saveConfig(Config config) throws IOException {
+ ConfigUtil.saveConfig(config, configFilePath);
+ this.config = config;
+ }
+}
+```
+###### \java\seedu\todo\commons\core\ConfigDefinition.java
+``` java
+/**
+ * Container class to store and retrieve config value properties.
+ * Each ConfigDefinition refers to a single config value (e.g. {@code appTitle})
+ * and stores the name, description and the current value.
+ */
+public class ConfigDefinition {
+ private String configName;
+ private String configDescription;
+ private String configValue;
+
+ public ConfigDefinition(String configName, String configDescription, String configValue) {
+ this.configName = configName;
+ this.configDescription = configDescription;
+ this.configValue = configValue;
+ }
+
+ public String getConfigName() {
+ return configName;
+ }
+
+ public String getConfigDescription() {
+ return configDescription;
+ }
+
+ public String getConfigValue() {
+ return configValue;
+ }
+}
+```
+###### \java\seedu\todo\commons\util\DateUtil.java
+``` java
+/**
+ * A utility class for Dates and LocalDateTimes
+ */
+public class DateUtil {
+
+ public static final LocalDateTime NO_DATETIME_VALUE = LocalDateTime.MIN;
+ public static final LocalDate NO_DATE_VALUE = NO_DATETIME_VALUE.toLocalDate();
+
+ private static final String FROM_NOW = "later";
+ private static final String TILL_NOW = "ago";
+ private static final String TODAY = "Today";
+ private static final String TOMORROW = "Tomorrow";
+ private static final String DAY = "day";
+ private static final String DAYS = "days";
+
+ /**
+ * Converts a LocalDateTime object to a legacy java.util.Date object.
+ *
+ * @param dateTime LocalDateTime object.
+ * @return Date object.
+ */
+ public static Date toDate(LocalDateTime dateTime) {
+ return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
+ }
+
+ /**
+ * Performs a "floor" operation on a LocalDateTime, and returns a new LocalDateTime
+ * with time set to 00:00.
+ *
+ * @param dateTime LocalDateTime for operation to be performed on.
+ * @return "Floored" LocalDateTime.
+ */
+ public static LocalDateTime floorDate(LocalDateTime dateTime) {
+ if (dateTime == null) {
+ return null;
+ }
+
+ return dateTime.toLocalDate().atTime(0, 0);
+ }
+
+ /**
+ * Formats a LocalDateTime to a relative date.
+ * Prefers DayOfWeek format, for dates up to 6 days from today.
+ * Otherwise, returns a relative time (e.g. 13 days from now).
+ *
+ * @param dateTime LocalDateTime to format.
+ * @return Formatted relative day.
+ */
+ public static String formatDay(LocalDateTime dateTime) {
+ if (dateTime == null) {
+ return null;
+ }
+
+ LocalDate date = dateTime.toLocalDate();
+ long daysDifference = LocalDate.now().until(date, ChronoUnit.DAYS);
+
+ // Consider today's date.
+ if (date.isEqual(LocalDate.now())) {
+ return TODAY;
+ }
+
+ if (daysDifference == 1) {
+ return TOMORROW;
+ }
+
+ // Consider dates up to 6 days from today.
+ if (daysDifference > 1 && daysDifference <= 6) {
+ return date.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.US);
+ }
+
+ // Otherwise, dates should be a relative days ago/from now format.
+ return String.format("%d %s %s", Math.abs(daysDifference),
+ StringUtil.pluralizer((int) Math.abs(daysDifference), DAY, DAYS),
+ daysDifference > 0 ? FROM_NOW : TILL_NOW);
+ }
+
+ /**
+ * Formats a LocalDateTime to a formatted date, following the dd MMM yyyy format.
+ *
+ * @param dateTime LocalDateTime to format.
+ * @return Formatted date in dd MMM yyyy format.
+ */
+ public static String formatDate(LocalDateTime dateTime) {
+ return dateTime.format(DateTimeFormatter.ofPattern("dd MMM yyyy"));
+ }
+
+ /**
+ * Formats a LocalDateTime to a ISO formatted date, following the ISO yyyy-MM-dd format.
+ *
+ * @param dateTime LocalDateTime to format.
+ * @return Formatted date in ISO format.
+ */
+ public static String formatIsoDate(LocalDateTime dateTime) {
+ return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+ }
+
+ /**
+ * Formats a LocalDateTime to a short date. Excludes the day of week only if
+ * the date is within 2-6 days from now.
+ *
+ * @param dateTime LocalDateTime to format.
+ * @return Formatted shorten day.
+ */
+ public static String formatShortDate(LocalDateTime dateTime) {
+ if (dateTime == null) {
+ return null;
+ }
+
+ LocalDate date = dateTime.toLocalDate();
+ long daysDifference = LocalDate.now().until(date, ChronoUnit.DAYS);
+ String dateFormat;
+
+ // Don't show dayOfWeek for days d, such that d = {n+2,...,n+6}, where n = date now
+ if (daysDifference >= 2 && daysDifference <= 6) {
+ dateFormat = "dd MMM";
+ } else {
+ dateFormat = "E dd MMM";
+ }
+
+ return date.format(DateTimeFormatter.ofPattern(dateFormat));
+ }
+
+ /**
+ * Parses a short date (as defined in {@link formatShortDate}) back to a LocalDateTime.
+ * We ignore the day of week portion for simplicity, since the shortDate can optionally omit it.
+ *
+ * @param shortDateToParse Date string to format.
+ * @return Parsed LocalDateTime.
+ */
+ public static LocalDate parseShortDate(String shortDateToParse) {
+ String[] dateParts = shortDateToParse.split(" ");
+ String dateString;
+
+ // Get the current year to add to the parsing since we cannot parse without a year...
+ int currentYear = Calendar.getInstance().get(Calendar.YEAR);
+
+ if (dateParts.length < 2 || dateParts.length > 3) {
+ return null;
+ }
+
+ if (dateParts.length == 3) {
+ dateString = String.format("%s %s %d", dateParts[1], dateParts[2], currentYear);
+ } else {
+ dateString = String.format("%s %s %d", dateParts[0], dateParts[1], currentYear);
+ }
+
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd MMM yyyy");
+ return LocalDate.parse(dateString, formatter);
+ }
+
+ /**
+ * Formats a LocalDateTime to a 24-hour time.
+ *
+ * @param dateTime LocalDateTime to format.
+ * @return 24-hour time formatted string.
+ */
+ public static String formatTime(LocalDateTime dateTime) {
+ if (dateTime == null) {
+ return null;
+ }
+
+ return dateTime.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm"));
+ }
+
+ /**
+ * Parses a ISO-format time string ({@code HH:mm}) into a LocalTime.
+ */
+ public static LocalTime parseTime(String timeString) {
+ return LocalTime.parse(timeString, DateTimeFormatter.ofPattern("HH:mm"));
+ }
+
+ /**
+ * Formats a LocalDateTime into short date + time format.
+ * @param dateTime LocalDateTime to format.
+ * @return Short date + time formatted string.
+ */
+ public static String formatDateTime(LocalDateTime dateTime) {
+ return String.format("%s %s", formatShortDate(dateTime), formatTime(dateTime));
+ }
+
+ /**
+ * Formats a start date and end date to a date range, which will display only as much info as necessary.
+ * @param dateFrom LocalDateTime from.
+ * @param dateTo LocalDateTime to.
+ * @return Formatted string.
+ */
+ public static String formatDateFromTo(LocalDateTime dateFrom, LocalDateTime dateTo) {
+ if (dateFrom == null && dateTo == null) {
+ return "";
+ } else if (dateTo == null) {
+ // No endDate
+ return formatTime(dateFrom);
+ } else if (dateFrom.isAfter(dateTo)) {
+ // Unhandled error, just ignore endDate and assume it has no endDate
+ return formatTime(dateFrom);
+ } else if (dateFrom.toLocalDate().equals(dateTo.toLocalDate())) {
+ return String.format("%s - %s", formatTime(dateFrom), formatTime(dateTo));
+ } else {
+ return String.format("%s - %s", formatDateTime(dateFrom), formatDateTime(dateTo));
+ }
+ }
+
+ /**
+ * Parses a dateTime string with the standard ISO format {@code yyyy-MM-dd HH:mm:ss}.
+ *
+ * @param dateTimeString
+ * @return
+ */
+ public static LocalDateTime parseDateTime(String dateTimeString) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+ return LocalDateTime.parse(dateTimeString, formatter);
+ }
+
+}
+```
+###### \java\seedu\todo\commons\util\FxViewUtil.java
+``` java
+/**
+ * Contains utility methods for JavaFX views
+ */
+public class FxViewUtil {
+
+ public static void applyAnchorBoundaryParameters(Node node, double left, double right, double top, double bottom) {
+ AnchorPane.setBottomAnchor(node, bottom);
+ AnchorPane.setLeftAnchor(node, left);
+ AnchorPane.setRightAnchor(node, right);
+ AnchorPane.setTopAnchor(node, top);
+ }
+
+ public static void makeFullWidth(Node node) {
+ applyAnchorBoundaryParameters(node, 0.0, 0.0, 0.0, 0.0);
+ }
+}
+```
+###### \java\seedu\todo\commons\util\ListUtil.java
+``` java
+public class ListUtil {
+
+ /**
+ * Checks if two lists are equal, without regard for order.
+ */
+ public static boolean unorderedListEquals(List list1, List list2) {
+ final Set set1 = new HashSet<>(list1);
+ final Set set2 = new HashSet<>(list2);
+
+ return set1.equals(set2);
+ }
+
+}
+```
+###### \java\seedu\todo\controllers\CompleteTaskController.java
+``` java
+/**
+ * Controller to mark a task as completed.
+ */
+public class CompleteTaskController extends Controller {
+
+ private static final String NAME = "Complete Task";
+ private static final String DESCRIPTION = "Marks a task as completed, by listed index";
+ private static final String COMMAND_SYNTAX = "complete ";
+ private static final String COMMAND_KEYWORD = "complete";
+
+ public static final String MESSAGE_SUCCESS = "Task marked as complete!";
+ public static final String MESSAGE_MISSING_INDEX = "Please specify the index of the item to delete.";
+ public static final String MESSAGE_INDEX_NOT_NUMBER = "Index has to be a number!";
+ public static final String MESSAGE_INVALID_ITEM = "Could not mark task as complete: Invalid index provided!";
+ public static final String MESSAGE_CANNOT_COMPLETE_EVENT = "An event cannot be marked as complete!";
+ public static final String MESSAGE_ALREADY_COMPLETED = "Could not mark task as complete: Task is already complete!";
+ public static final String MESSAGE_COULD_NOT_SAVE = "Could not mark task as complete: An error occured while saving the database file.";
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ @Override
+ public void process(String args) {
+ // Get index.
+ String param = args.replaceFirst(COMMAND_KEYWORD, "").trim();
+
+ if (param.length() <= 0) {
+ Renderer.renderDisambiguation(COMMAND_SYNTAX, MESSAGE_MISSING_INDEX);
+ return;
+ }
+
+ assert param.length() > 0;
+
+ // Get index.
+ int index = 0;
+ try {
+ index = Integer.decode(param);
+ } catch (NumberFormatException e) {
+ Renderer.renderDisambiguation(COMMAND_SYNTAX, MESSAGE_INDEX_NOT_NUMBER);
+ return;
+ }
+
+ // Get record
+ EphemeralDB edb = EphemeralDB.getInstance();
+ CalendarItem calendarItem = edb.getCalendarItemsByDisplayedId(index);
+ TodoListDB db = TodoListDB.getInstance();
+
+ if (calendarItem == null) {
+ Renderer.renderIndex(db, MESSAGE_INVALID_ITEM);
+ return;
+ }
+
+ if (!(calendarItem instanceof Task)) {
+ Renderer.renderIndex(db, MESSAGE_CANNOT_COMPLETE_EVENT);
+ return;
+ }
+
+ Task task = (Task) calendarItem;
+
+ if (task.isCompleted()) {
+ Renderer.renderIndex(db, MESSAGE_ALREADY_COMPLETED);
+ return;
+ }
+
+ // Set task as completed
+ task.setCompleted();
+ boolean hadSaved = db.save();
+
+ if (!hadSaved) {
+ task.setIncomplete();
+ Renderer.renderIndex(db, MESSAGE_COULD_NOT_SAVE);
+ return;
+ }
+
+ // Show success
+ Renderer.renderIndex(db, MESSAGE_SUCCESS);
+ }
+
+}
+```
+###### \java\seedu\todo\controllers\concerns\Disambiguator.java
+``` java
+/**
+ * Concern to generate disambiguation strings that will be returned from a Controller
+ * back to the UI, for the user to correct an invalid/ambiguous command.
+ */
+public class Disambiguator {
+
+ private static final String TOKEN_NAME = "name";
+ public static final String PLACEHOLDER_NAME = "";
+ private static final String TOKEN_TASKSTATUS = "taskStatus";
+ public static final String PLACEHOLDER_TASKSTATUS = "";
+ private static final String TOKEN_EVENTSTATUS = "eventStatus";
+ public static final String PLACEHOLDER_EVENTSTATUS = "";
+ private static final String TOKEN_TAG = "tag";
+ public static final String PLACEHOLDER_TAG = "";
+ private static final String TOKEN_STARTTIME = "startTime";
+ public static final String PLACEHOLDER_STARTTIME = "";
+ private static final int INDEX_STARTTIME = 0;
+ private static final String TOKEN_ENDTIME = "endTime";
+ public static final String PLACEHOLDER_ENDTIME = "";
+ private static final int INDEX_ENDTIME = 1;
+
+ /**
+ * Extracts the first token from each type of token.
+ * If the token doesn't exist, it will be replaced with a default placeholder.
+ * This is for the purpose of rendering disambiguation strings.
+ */
+ public static Map extractParsedTokens(Map parsedResult) {
+ Map tokens = new HashMap<>();
+
+ // Extract tokens
+ tokens.put(TOKEN_NAME, extractKeyOrValue(parsedResult, true, TOKEN_NAME, PLACEHOLDER_NAME));
+ tokens.put(TOKEN_TASKSTATUS, extractKeyOrValue(parsedResult, false, TOKEN_TASKSTATUS, PLACEHOLDER_TASKSTATUS));
+ tokens.put(TOKEN_EVENTSTATUS, extractKeyOrValue(parsedResult, false, TOKEN_EVENTSTATUS, PLACEHOLDER_EVENTSTATUS));
+ tokens.put(TOKEN_TAG, extractKeyOrValue(parsedResult, true, TOKEN_TAG, PLACEHOLDER_TAG));
+
+ // Time start/end
+ String[] datePair = DateParser.extractDatePair(parsedResult);
+ tokens.put(TOKEN_STARTTIME, StringUtil.replaceEmpty(datePair[INDEX_STARTTIME], PLACEHOLDER_STARTTIME));
+ tokens.put(TOKEN_ENDTIME, StringUtil.replaceEmpty(datePair[INDEX_ENDTIME], PLACEHOLDER_ENDTIME));
+
+ return tokens;
+ }
+
+ /**
+ * Extracts the key or value of the token value array, and returns it.
+ * Accepts a placeholder string in the event that the key or value doesn't exist.
+ */
+ private static String extractKeyOrValue(Map parsedResult, boolean extractValue, String key, String placeholder) {
+ int n = extractValue ? 1 : 0;
+
+ // Extracts the key or value depending on extractKey.
+ String extracted = null;
+ if (parsedResult.get(key) != null && parsedResult.get(key).length > n) {
+ extracted = parsedResult.get(key)[n];
+ }
+
+ // Replaces with placeholder if empty.
+ extracted = StringUtil.replaceEmpty(extracted, placeholder);
+
+ return extracted;
+ }
+
+ /**
+ * Extracts any unknown token strings from the parsed result.
+ */
+ public static String getUnknownTokenString(Map parsedResult) {
+ String[] defaultToken = parsedResult.get("default");
+
+ if (defaultToken != null && defaultToken[1] != null && defaultToken[1].length() > 0) {
+ return defaultToken[1];
+ } else {
+ return null;
+ }
+ }
+}
+```
+###### \java\seedu\todo\controllers\ConfigController.java
+``` java
+/**
+ * Controller to configure app settings.
+ * Has side effects, since it has to perform
+ * updates on the UI or file sources on update.
+ */
+public class ConfigController extends Controller {
+
+ private static final String NAME = "Configure";
+ private static final String DESCRIPTION = "Shows current configuration settings or updates them.";
+ private static final String COMMAND_SYNTAX = "config [ ]";
+ private static final String COMMAND_KEYWORD = "config";
+
+ private static final String MESSAGE_SHOWING = "Showing all settings.";
+ private static final String MESSAGE_SUCCESS = "Successfully updated %s.";
+ public static final String MESSAGE_FAILURE = "Could not update settings: %s";
+ private static final String MESSAGE_INVALID_INPUT = "Invalid config setting provided!";
+ public static final String MESSAGE_WRONG_EXTENSION = "Could not change storage path: File must end with %s";
+ public static final String TEMPLATE_SET_CONFIG = "config ";
+
+ private static final String STRING_SPACE = " ";
+ private static final int ARGS_LENGTH = 2;
+ public static final String DB_FILE_EXTENSION = ".json";
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ @Override
+ public void process(String input) {
+ String params = input.replaceFirst("config", "").trim();
+
+ // Check for basic command.
+ if (params.length() <= 0) {
+ Renderer.renderConfig(MESSAGE_SHOWING);
+ return;
+ }
+
+ // Check args length
+ String[] args = params.split(STRING_SPACE, ARGS_LENGTH);
+ if (args.length != ARGS_LENGTH) {
+ Renderer.renderDisambiguation(TEMPLATE_SET_CONFIG, MESSAGE_INVALID_INPUT);
+ return;
+ }
+
+ String configName = args[0];
+ String configValue = args[1];
+ Config config = ConfigCenter.getInstance().getConfig();
+
+ // Check if configName is a valid name.
+ if (!config.getDefinitionsNames().contains(configName)) {
+ Renderer.renderDisambiguation(TEMPLATE_SET_CONFIG, MESSAGE_INVALID_INPUT);
+ return;
+ }
+
+ try {
+ // Update config value
+ config = updateConfigByName(config, configName, configValue);
+
+ // Save config to file
+ ConfigCenter.getInstance().saveConfig(config);
+ } catch (CannotConfigureException | IOException e) {
+ Renderer.renderConfig(String.format(MESSAGE_FAILURE, e.getMessage()));
+ return;
+ }
+
+ // Update console for success
+ Renderer.renderConfig(String.format(MESSAGE_SUCCESS, configName));
+ }
+
+ /**
+ * Updates a config value and performs the necessary actions for the configuration.
+ * Throws a {@code CannotConfigureException} if an error was encountered while performing configuration actions.
+ *
+ * @param config Config object which will be updated.
+ * @param configName Config setting name to update.
+ * @param configValue New value to set for the config setting.
+ * @return Config object after setting values.
+ * @throws CannotConfigureException if an error was encountered during configuration.
+ */
+ private Config updateConfigByName(Config config, String configName, String configValue) throws CannotConfigureException {
+ switch (configName) {
+ case "appTitle" :
+ // Updates MainWindow title
+ UiManager.getInstance().getMainWindow().setTitle(configValue);
+
+ // Update config
+ config.setAppTitle(configValue);
+
+ break;
+
+ case "databaseFilePath" :
+ // Move the DB file to the new location
+ moveDatabaseFile(configValue);
+
+ // Update config
+ config.setDatabaseFilePath(configValue);
+
+ break;
+
+ default :
+ break;
+ }
+
+ return config;
+ }
+
+ /**
+ * Moves the database file to the new location.
+ * Throws an exception if the new path does not exist, or if it has the wrong extension.
+ */
+ private void moveDatabaseFile(String newPath) throws CannotConfigureException {
+ // Make sure the new path has a .json extension
+ if (!newPath.endsWith(DB_FILE_EXTENSION)) {
+ throw new CannotConfigureException(String.format(MESSAGE_WRONG_EXTENSION, DB_FILE_EXTENSION));
+ }
+
+ try {
+ TodoListDB.getInstance().move(newPath);
+ } catch (IOException e) {
+ throw new CannotConfigureException(e.getMessage());
+ }
+ }
+
+}
+```
+###### \java\seedu\todo\controllers\HelpController.java
+``` java
+/**
+ * Controller to show commands help.
+ */
+public class HelpController extends Controller {
+
+ private static final String NAME = "Help";
+ private static final String DESCRIPTION = "Shows documentation for all valid commands.";
+ private static final String COMMAND_SYNTAX = "help";
+ private static final String COMMAND_KEYWORD = "help";
+
+ private static final String MESSAGE_HELP_SUCCESS = "Showing all commands.";
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ @Override
+ public void process(String input) {
+ HelpView view = UiManager.loadView(HelpView.class);
+ view.commandDefinitions = Arrays.asList(getAllCommandDefinitions());
+ UiManager.renderView(view);
+
+ UiManager.updateConsoleMessage(MESSAGE_HELP_SUCCESS);
+ }
+
+ public CommandDefinition[] getAllCommandDefinitions() {
+ return new CommandDefinition[] { new HelpController().getCommandDefinition(),
+ new AddController().getCommandDefinition(),
+ new ListController().getCommandDefinition(),
+ new UpdateController().getCommandDefinition(),
+ new CompleteTaskController().getCommandDefinition(),
+ new UncompleteTaskController().getCommandDefinition(),
+ new DestroyController().getCommandDefinition(),
+ new ConfigController().getCommandDefinition(),
+ new DestroyController().getCommandDefinition(),
+ new ClearController().getCommandDefinition(),
+ new FindController().getCommandDefinition(),
+ new TagController().getCommandDefinition(),
+ new UntagController().getCommandDefinition(),
+ new ExitController().getCommandDefinition() };
+ }
+}
+```
+###### \java\seedu\todo\controllers\ListController.java
+``` java
+/**
+ * Controller to list CalendarItems.
+ */
+public class ListController extends Controller {
+
+ private static final String NAME = "List";
+ private static final String DESCRIPTION = "Lists all tasks and events.";
+ private static final String COMMAND_SYNTAX = "list [task/event] [complete/incomplete] [on date] or [from date to date]";
+ private static final String COMMAND_KEYWORD = "list";
+
+ private static final String MESSAGE_LISTING_ALL = "Showing all tasks and events.\n\n"
+ + "You have a total of %d incomplete tasks, %d overdue tasks, "
+ + "and %d upcoming events.";
+ private static final String MESSAGE_LISTING_FILTERED = "Showing %s %s and %s %s.\n\nYour query: %s";
+ private static final String MESSAGE_UNKNOWN_TOKENS = "Could not parse your query as it contained unknown tokens: %s";
+ private static final String MESSAGE_AMBIGUOUS_TYPE = "We could not tell if you wanted to clear events or tasks. \n"
+ + "Note that only tasks can be \"complete\"/\"incomplete\", "
+ + "while only events can be \"past\", \"over\" or \"future\".";
+ private static final String MESSAGE_INVALID_DATE = "We could not parse the date in your query, please try again.";
+
+ public static final String TEMPLATE_LIST = "list [from \"%s\"] [to \"%s\"] [tag \"%s\"]";
+ public static final String TEMPLATE_LIST_TASKS = "list tasks [\"%s\"] [from \"%s\"] [to \"%s\"] [tag \"%s\"]";
+ public static final String TEMPLATE_LIST_EVENTS = "list events [\"%s\"] [from \"%s\"] [to \"%s\"] [tag \"%s\"]";
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ @Override
+ public void process(String input) throws ParseException {
+
+ TodoListDB db = TodoListDB.getInstance();
+
+ // First, we check if it's a basic command, then don't bother filtering.
+ if (input.toLowerCase().trim().equals(COMMAND_KEYWORD)) {
+ String consoleMessage = String.format(MESSAGE_LISTING_ALL, db.countIncompleteTasks(),
+ db.countOverdueTasks(), db.countFutureEvents());
+ Renderer.renderIndex(db, consoleMessage);
+ return;
+ }
+
+ List filteredTasks = new ArrayList<>();
+ List filteredEvents = new ArrayList<>();
+
+ // Parse the input with Tokenizer.
+ Map parsedResult = Tokenizer.tokenize(CalendarItemFilter.getFilterTokenDefinitions(), input);
+
+ // Check if there are any unknown tokens.
+ if (Disambiguator.getUnknownTokenString(parsedResult) != null) {
+ String errorMessage = String.format(MESSAGE_UNKNOWN_TOKENS, Disambiguator.getUnknownTokenString(parsedResult));
+ renderDisambiguation(parsedResult, true, true, errorMessage);
+ return;
+ }
+
+ // Determine if command should return tasks/events/both.
+ boolean[] tasksOrEventsBools;
+ try {
+ tasksOrEventsBools = CalendarItemFilter.parseIsTaskEvent(parsedResult);
+ } catch (AmbiguousEventTypeException e) {
+ renderDisambiguation(parsedResult, true, true, MESSAGE_AMBIGUOUS_TYPE);
+ return;
+ }
+
+ boolean isTask = tasksOrEventsBools[0];
+ boolean isEvent = tasksOrEventsBools[1];
+
+ // Filter tasks and events.
+ try {
+ if (isTask) {
+ filteredTasks = CalendarItemFilter.filterTasks(parsedResult);
+ }
+ if (isEvent) {
+ filteredEvents = CalendarItemFilter.filterEvents(parsedResult);
+ }
+ } catch (InvalidNaturalDateException e) {
+ renderDisambiguation(parsedResult, isTask, isEvent, MESSAGE_INVALID_DATE);
+ return;
+ }
+
+ // Render the new view with filtered tasks.
+ String consoleMessage = String.format(MESSAGE_LISTING_FILTERED,
+ filteredTasks.size(), StringUtil.pluralizer(filteredTasks.size(), "task", "tasks"),
+ filteredEvents.size(), StringUtil.pluralizer(filteredEvents.size(), "event", "events"), input);
+ Renderer.renderSelected(TodoListDB.getInstance(), consoleMessage, filteredTasks, filteredEvents);
+ }
+
+ /**
+ * Disambiguate an ambiguous input by auto-populating a templated command on
+ * a best-effort basis.
+ *
+ * @param parsedResult
+ * @param isTask
+ * @param isEvent
+ * @param errorMessage
+ */
+ private void renderDisambiguation(Map parsedResult, boolean isTask, boolean isEvent, String errorMessage) {
+ Map extractedTokens = Disambiguator.extractParsedTokens(parsedResult);
+ String consoleCommand;
+
+ if ((isTask && isEvent) || (!isTask && !isEvent)) {
+ consoleCommand = String.format(TEMPLATE_LIST, extractedTokens.get("startTime"),
+ extractedTokens.get("endTime"), extractedTokens.get("tag"));
+ } else if (isTask) {
+ consoleCommand = String.format(TEMPLATE_LIST_TASKS, extractedTokens.get("taskStatus"),
+ extractedTokens.get("startTime"), extractedTokens.get("endTime"), extractedTokens.get("tag"));
+ } else {
+ consoleCommand = String.format(TEMPLATE_LIST_EVENTS, extractedTokens.get("eventStatus"),
+ extractedTokens.get("startTime"), extractedTokens.get("endTime"), extractedTokens.get("tag"));
+ }
+
+ Renderer.renderDisambiguation(consoleCommand, errorMessage);
+ }
+}
+```
+###### \java\seedu\todo\controllers\UncompleteTaskController.java
+``` java
+/**
+ * Controller to mark a task as uncompleted.
+ */
+public class UncompleteTaskController extends Controller {
+
+ private static final String NAME = "Uncomplete Task";
+ private static final String DESCRIPTION = "Marks a task as incomplete, by listed index";
+ private static final String COMMAND_SYNTAX = "uncomplete ";
+ private static final String COMMAND_KEYWORD = "uncomplete";
+
+ public static final String MESSAGE_SUCCESS = "Task marked as incomplete!";
+ public static final String MESSAGE_MISSING_INDEX = "Please specify the index of the item to delete.";
+ public static final String MESSAGE_INDEX_NOT_NUMBER = "Index has to be a number!";
+ public static final String MESSAGE_INVALID_ITEM = "Could not mark task as incomplete: Invalid index provided!";
+ public static final String MESSAGE_CANNOT_UNCOMPLETE_EVENT = "An event cannot be marked as incomplete!";
+ public static final String MESSAGE_ALREADY_INCOMPLETE = "Could not mark task as incomplete: Task is not completed!";
+ public static final String MESSAGE_COULD_NOT_SAVE = "Could not mark task as incomplete: An error occured while saving the database file.";
+
+
+ private static CommandDefinition commandDefinition =
+ new CommandDefinition(NAME, DESCRIPTION, COMMAND_SYNTAX, COMMAND_KEYWORD);
+
+ @Override
+ public CommandDefinition getCommandDefinition() {
+ return commandDefinition;
+ }
+
+ @Override
+ public void process(String args) {
+ // Get index.
+ String param = args.replaceFirst(COMMAND_KEYWORD, "").trim();
+
+ if (param.length() <= 0) {
+ Renderer.renderDisambiguation(COMMAND_SYNTAX, MESSAGE_MISSING_INDEX);
+ return;
+ }
+
+ assert param.length() > 0;
+
+ // Get index.
+ int index = 0;
+ try {
+ index = Integer.decode(param);
+ } catch (NumberFormatException e) {
+ Renderer.renderDisambiguation(COMMAND_SYNTAX, MESSAGE_INDEX_NOT_NUMBER);
+ return;
+ }
+
+ // Get record
+ EphemeralDB edb = EphemeralDB.getInstance();
+ CalendarItem calendarItem = edb.getCalendarItemsByDisplayedId(index);
+ TodoListDB db = TodoListDB.getInstance();
+
+ if (calendarItem == null) {
+ Renderer.renderIndex(db, MESSAGE_INVALID_ITEM);
+ return;
+ }
+
+ if (!(calendarItem instanceof Task)) {
+ Renderer.renderIndex(db, MESSAGE_CANNOT_UNCOMPLETE_EVENT);
+ return;
+ }
+
+ Task task = (Task) calendarItem;
+
+ if (!task.isCompleted()) {
+ Renderer.renderIndex(db, MESSAGE_ALREADY_INCOMPLETE);
+ return;
+ }
+
+ // Set task as completed
+ task.setIncomplete();
+ boolean hadSaved = db.save();
+
+ if (!hadSaved) {
+ task.setCompleted();
+ Renderer.renderIndex(db, MESSAGE_COULD_NOT_SAVE);
+ return;
+ }
+
+ // Show success message
+ Renderer.renderIndex(db, MESSAGE_SUCCESS);
+ }
+
+}
+```
+###### \java\seedu\todo\MainApp.java
+``` java
+/**
+ * The main entry point to the application.
+ */
+public class MainApp extends Application {
+ private static final Logger logger = LogsCenter.getLogger(MainApp.class);
+
+ public static final Version VERSION = new Version(1, 0, 0, true);
+
+ private static final String MESSAGE_WELCOME = "Welcome! What would like to get done today?";
+
+ private static final ConfigCenter configCenter = ConfigCenter.getInstance();
+ private String configFilePath;
+ protected Config config;
+
+ protected UiManager ui;
+
+ public MainApp() {}
+
+ @Override
+ public void init() throws Exception {
+ super.init();
+
+ // Read app param
+ configFilePath = getApplicationParameter("config");
+
+ // Initialize config from config file, or create a new one.
+ initConfig();
+
+ // Initialize logging
+ initLogging(configCenter.getConfig());
+
+ // Initialize events center
+ initEventsCenter();
+
+ // Initialize UI config
+ UiManager.initialize(configCenter.getConfig());
+ ui = UiManager.getInstance();
+
+ // Load DB
+ if (!TodoListDB.getInstance().load()) {
+ TodoListDB.getInstance().save();
+ }
+ }
+
+ @Override
+ public void start(Stage primaryStage) {
+ ui.start(primaryStage);
+
+ IndexView view = UiManager.loadView(IndexView.class);
+ view.tasks = TodoListDB.getInstance().getIncompleteTasksAndTaskFromTodayDate();
+ view.events = TodoListDB.getInstance().getAllCurrentEvents();
+ view.tags = TodoListDB.getInstance().getTagList();
+ UiManager.renderView(view);
+
+ // Show welcome message
+ UiManager.updateConsoleMessage(MESSAGE_WELCOME);
+ }
+
+ @Override
+ public void stop() {
+ ui.stop();
+ Platform.exit();
+ System.exit(0);
+ }
+
+ /** ================== UTILS ====================== **/
+
+ /**
+ * Gets command-line parameter by name.
+ *
+ * @param parameterName Name of parameter
+ * @return Value of parameter
+ */
+ private String getApplicationParameter(String parameterName){
+ Map applicationParameters = getParameters().getNamed();
+ return applicationParameters.get(parameterName);
+ }
+
+ /** ================== INITIALIZATION ====================== **/
+
+ private void initLogging(Config config) {
+ LogsCenter.init(config);
+ }
+
+ protected Config initConfig() {
+ String configFilePathUsed;
+
+ configFilePathUsed = Config.DEFAULT_CONFIG_FILE;
+
+ if (configFilePath != null) {
+ logger.info("Custom Config file specified " + configFilePath);
+ configFilePathUsed = configFilePath;
+ }
+
+ configFilePath = configFilePathUsed;
+
+ logger.info("Using config file : " + configFilePathUsed);
+
+ return loadConfigFromFile(configFilePathUsed);
+ }
+
+ protected Config loadConfigFromFile(String configFilePathUsed) {
+ configCenter.setConfigFilePath(configFilePathUsed);
+ config = configCenter.getConfig();
+
+ // Update config file in case it was missing to begin with or there are new/unused fields
+ try {
+ configCenter.saveConfig(config);
+ } catch (IOException e) {
+ logger.warning("Failed to save config file : " + StringUtil.getDetails(e));
+ }
+
+ return config;
+ }
+
+ private void initEventsCenter() {
+ EventsCenter.getInstance().registerHandler(this);
+ }
+
+ /** ================== SUBSCRIPTIONS ====================== **/
+
+ @Subscribe
+ public void handleExitAppRequestEvent(ExitAppRequestEvent event) {
+ logger.info(LogsCenter.getEventHandlingLogMessage(event));
+ this.stop();
+ }
+
+ /** ================== MAIN METHOD ====================== **/
+
+ public static void main(String[] args) {
+ launch(args);
+ }
+}
+```
+###### \java\seedu\todo\ui\components\Component.java
+``` java
+/**
+ * The UI is predicated on the concept of a {@code Component}.
+ * A {@code Component} is a single sub-unit of the UI, and should preferably
+ * only be responsible for a single item or functionality in the UI.
+ *
+ * For example, a task item in the UI is a single {@code Component}, as it is
+ * responsible for purely displaying the task information. A task list
+ * is also a {@code Component}, as it contains multiple task items, and it is
+ * responsible just for rendering each task item.
+ *
+ * A {@code Component} has the following properties:
+ *
+ *
+ * - Associated with FXML files
+ * - Loaded with load
+ * - Able to accept props
+ * - Rendered in placeholder panes
+ * - Can load sub-{@code Component}s
+ *
+ *
+ * Note: The concept of {@code Component}s and their associated behaviours came
+ * from React, a modern JavaScript library for the web.
+ */
+public abstract class Component extends UiPart {
+
+ private static final Logger logger = LogsCenter.getLogger(View.class);
+
+ protected Pane placeHolderPane;
+ protected Pane mainNode;
+
+ /**
+ * Loads a component into a placeholder.
+ *
+ *
+ * - Gets the FXML file specified in the {@link Component}, and loads the JavaFX node.
+ * - Loads a view controller that controls the node, and returns the view controller (ie. {@link Component}).
+ *
+ *
+ * @param primaryStage Stage to load the component on. Typically, a Component rendering other Components should pass in primaryStage.
+ * @param placeholder Placeholder {@code Pane} to render the component in.
+ * @param componentClass Class of the Component to render.
+ * @return The Component, that controls the rendered JavaFX node.
+ */
+ public T load(Stage primaryStage, Pane placeholder, Class componentClass) {
+ return UiPartLoader.loadUiPart(primaryStage, placeholder, componentClass);
+ }
+
+ /**
+ * This method renders the View in the specified placeholder, if provided.
+ *
+ * Note that all as specified in the FXML file, in the placeholder pane.
+ * After loading the FXML file, the execution is then passed onto setNode,
+ * which will replace the placeholder contents accordingly.
+ *
+ * @param primaryStage The primary stage that contains the main application window.
+ * @param placeholder The placeholder pane where this View should reside.
+ */
+ public void render() {
+ // If the View is not loaded from the FXML file, we have no node to render.
+ if (mainNode == null) {
+ return;
+ }
+
+ assert mainNode != null;
+
+ if (placeHolderPane != null) {
+ // Replace placeholder children with node.
+ placeHolderPane.getChildren().setAll(mainNode);
+ } else {
+ logger.warning(this.getClass().getName() + " has no placeholder.");
+ }
+
+ // Callback once view is rendered.
+ componentDidMount();
+ }
+
+ public Node getNode() {
+ return mainNode;
+ }
+
+ /**
+ * Runs once the {@code render()} is called. Used to perform any of the following actions:
+ * Modify JavaFX components
+ * Set the state of JavaFX components (such as value)
+ * Load and render any children components
+ *
+ * Declaration is optional, and will default to not doing anything if it is not overridden in child components.
+ */
+ public void componentDidMount() {
+ // Does nothing by default.
+ }
+
+ @Override
+ public void setPlaceholder(Pane pane) {
+ this.placeHolderPane = pane;
+ }
+
+ @Override
+ public void setNode(Node node) {
+ mainNode = (Pane) node;
+ }
+
+}
+```
+###### \java\seedu\todo\ui\components\ConfigItem.java
+``` java
+public class ConfigItem extends MultiComponent {
+
+ private static final String FXML_PATH = "components/ConfigItem.fxml";
+
+ // Props
+ public ConfigDefinition configDefinition;
+
+ // FXML
+ @FXML
+ private Text configDescription;
+ @FXML
+ private Text configName;
+ @FXML
+ private Text configValue;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ if (configDefinition != null) {
+ configDescription.setText(configDefinition.getConfigDescription());
+ configName.setText(configDefinition.getConfigName());
+ configValue.setText(configDefinition.getConfigValue());
+ }
+ }
+
+}
+```
+###### \java\seedu\todo\ui\components\Console.java
+``` java
+public class Console extends Component {
+
+ private static final String FXML_PATH = "components/Console.fxml";
+ private static final String INVALID_COMMAND_RESPONSE = "Invalid command!";
+ private static final String INVALID_COMMAND_STYLECLASS = "invalid";
+
+ // Props
+ public String consoleOutput;
+ public String consoleInputValue;
+ private String lastCommandEntered;
+
+ // Input handler
+ private InputHandler inputHandler = InputHandler.getInstance();
+
+ // FXML
+ @FXML
+ private TextField consoleInputTextField;
+ @FXML
+ private TextArea consoleTextArea;
+
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ // Makes ConsoleInput full width wrt parent container.
+ FxViewUtil.makeFullWidth(this.mainNode);
+
+ // Set text in ConsoleInput box if provided
+ if (consoleInputValue.length() > 0) {
+ consoleInputTextField.setText(consoleInputValue);
+
+ // Add invalid field css
+ consoleInputTextField.getStyleClass().add(INVALID_COMMAND_STYLECLASS);
+ } else {
+ // Remove invalid field css
+ consoleInputTextField.getStyleClass().remove(INVALID_COMMAND_STYLECLASS);
+ }
+
+ // Load ConsoleDisplay text.
+ consoleTextArea.setText(consoleOutput);
+ }
+
+ /** ================ ACTION HANDLERS ================== **/
+ @FXML
+ public void handleConsoleInputKeyPress(KeyEvent event) {
+ if (event.getCode() == KeyCode.UP) {
+ String command = inputHandler.getPreviousCommandFromHistory();
+ consoleInputTextField.setText(command);
+ } else if (event.getCode() == KeyCode.DOWN) {
+ String command = inputHandler.getNextCommandFromHistory();
+ consoleInputTextField.setText(command);
+ } else if (!event.getCode().isModifierKey()) {
+ // Handle key accelerators in main scene if defined
+ Runnable r = UiManager.getInstance().getMainWindow().getScene()
+ .getAccelerators().get(new KeyCodeCombination(event.getCode()));
+
+ if (r != null) {
+ r.run();
+ }
+ }
+ }
+
+ @FXML
+ public void handleConsoleInputChanged() {
+ lastCommandEntered = consoleInputTextField.getText();
+
+ // Don't change anything if our command is empty.
+ if (lastCommandEntered.length() <= 0) {
+ return;
+ }
+
+ assert lastCommandEntered.length() > 0;
+
+ boolean isValidCommand = inputHandler.processInput(lastCommandEntered);
+
+ if (!isValidCommand) {
+ // Show invalid response in Console
+ consoleTextArea.setText(INVALID_COMMAND_RESPONSE);
+
+ // Set CSS
+ consoleInputTextField.getStyleClass().add(INVALID_COMMAND_STYLECLASS);
+ } else {
+ // Remove console output
+ consoleTextArea.setText("");
+
+ // Remove CSS
+ consoleInputTextField.getStyleClass().remove(INVALID_COMMAND_STYLECLASS);
+
+ // Clear input text
+ consoleInputTextField.clear();
+ }
+ }
+}
+```
+###### \java\seedu\todo\ui\components\Header.java
+``` java
+public class Header extends Component {
+
+ private static final String LOGO_IMAGE_PATH = "/images/logo-64x64.png";
+ private static final String FXML_PATH = "components/Header.fxml";
+ private static final String VERSION_TEXT = "version ";
+
+ // Props
+ public String versionString;
+ public String appTitle;
+
+ // FXML
+ @FXML
+ private Text headerAppTitle;
+ @FXML
+ private Text headerVersionText;
+ @FXML
+ private ImageView headerLogoImageView;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ // Makes the Component full width wrt parent container.
+ FxViewUtil.makeFullWidth(this.mainNode);
+
+ // Set text.
+ headerAppTitle.setText(appTitle);
+ headerVersionText.setText(VERSION_TEXT + versionString);
+
+ // Set logo image.
+ Image image = new Image(LOGO_IMAGE_PATH);
+ headerLogoImageView.setImage(image);
+ }
+
+}
+```
+###### \java\seedu\todo\ui\components\HelpCommandItem.java
+``` java
+public class HelpCommandItem extends MultiComponent {
+
+ private static final String FXML_PATH = "components/HelpCommandItem.fxml";
+
+ // Props
+ public String commandName;
+ public String commandDescription;
+ public String commandSyntax;
+
+ // FXML
+ @FXML
+ private Text commandNameText;
+ @FXML
+ private Text commandDescriptionText;
+ @FXML
+ private Text commandSyntaxText;
+
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ commandNameText.setText(commandName);
+ commandDescriptionText.setText(commandDescription);
+ commandSyntaxText.setText(commandSyntax);
+ }
+
+}
+```
+###### \java\seedu\todo\ui\components\MultiComponent.java
+``` java
+/**
+ * A MultiComponent is a special type of {@link Component}, except that
+ * the render method behaves differently. Successive calls to {@code render()}
+ * would cause the node to the rendered to the placeholder multiple times,
+ * instead of replacing the old node. This is especially useful for
+ * rendering lists of variable items, using a loop.
+ */
+public abstract class MultiComponent extends Component {
+
+ @Override
+ public void render() {
+ if (placeHolderPane != null) {
+ // Replace placeholder children with node.
+ placeHolderPane.getChildren().add(mainNode);
+ }
+
+ // Callback once view is loaded.
+ componentDidMount();
+ }
+
+ /**
+ * Resets the items in the specified {@code placeholder}.
+ *
+ * @param placeholder Placeholder pane whose children items are to be cleared.
+ */
+ public static void reset(Pane placeholder) {
+ placeholder.getChildren().clear();
+ }
+
+}
+```
+###### \java\seedu\todo\ui\components\Sidebar.java
+``` java
+public class Sidebar extends Component {
+
+ private static final String FXML_PATH = "components/Sidebar.fxml";
+
+ // Links
+ private static final String TASKS_LABEL = "Tasks";
+ private static final String TASKS_ICON_PATH = "/images/icon-pin.png";
+ private static final String OVERDUE_LABEL = "Overdue Tasks";
+ private static final String OVERDUE_ICON_PATH = "/images/icon-siren.png";
+ private static final String EVENTS_LABEL = "Events";
+ private static final String EVENTS_ICON_PATH = "/images/icon-calendar.png";
+
+ private static final String TAG_LABEL = "Tags";
+
+ // Props
+ public List tags = new ArrayList<>();
+
+ // FXML
+ @FXML
+ private Text titleText;
+ @FXML
+ private VBox sidebarCountersPlaceholder;
+ @FXML
+ private VBox sidebarTagsPlaceholder;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ titleText.setText(formatTagSize(tags.size()));
+
+ // Load Counters
+ loadCounters();
+
+ // Load Tags
+ loadTags();
+ }
+
+ private String formatTagSize(int size) {
+ return String.format("%s (%s)",TAG_LABEL, size);
+ }
+
+ private void loadCounters() {
+ TodoListDB db = TodoListDB.getInstance();
+
+ // Clear items.
+ SidebarCounter.reset(sidebarCountersPlaceholder);
+
+ String[] linkLabels = { formatLink(TASKS_LABEL, db.countIncompleteTasks()),
+ formatLink(OVERDUE_LABEL, db.countOverdueTasks()),
+ formatLink(EVENTS_LABEL , db.countFutureEvents()) };
+ String[] linkIconPaths = { TASKS_ICON_PATH, OVERDUE_ICON_PATH, EVENTS_ICON_PATH };
+
+ for (int i = 0; i < linkLabels.length; i++) {
+ SidebarCounter counter = load(primaryStage, sidebarCountersPlaceholder, SidebarCounter.class);
+ counter.label = linkLabels[i];
+ counter.iconPath = linkIconPaths[i];
+ counter.render();
+ }
+ }
+
+ private void loadTags() {
+ TagListItem.reset(sidebarTagsPlaceholder);
+
+ for (String tag : tags) {
+ TagListItem item = load(primaryStage, sidebarTagsPlaceholder, TagListItem.class);
+ item.tag = tag;
+ item.render();
+ }
+ }
+
+ private String formatLink(String label, int total) {
+ return String.format("%s (%d)", label, total);
+ }
+
+}
+```
+###### \java\seedu\todo\ui\components\SidebarCounter.java
+``` java
+public class SidebarCounter extends MultiComponent {
+
+ private static final String FXML_PATH = "components/SidebarCounter.fxml";
+
+ // Props
+ public String iconPath;
+ public String label;
+
+ // FXML
+ @FXML
+ private ImageView imageView;
+ @FXML
+ private Text labelText;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ imageView.setImage(new Image(iconPath));
+ labelText.setText(label);
+ }
+
+}
+```
+###### \java\seedu\todo\ui\components\TagListItem.java
+``` java
+public class TagListItem extends MultiComponent {
+
+ private static final String FXML_PATH = "components/TagListItem.fxml";
+ private static final Color BULLET_COLOR = Color.rgb(0, 0, 0, 0.3);
+
+ // Props
+ public String tag;
+
+ // FXML
+ @FXML
+ private Text labelText;
+ @FXML
+ private Circle labelBullet;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ labelText.setText(tag);
+ labelBullet.setFill(BULLET_COLOR);
+ }
+
+}
+```
+###### \java\seedu\todo\ui\components\TaskList.java
+``` java
+public class TaskList extends Component {
+
+ public static final LocalDateTime NO_DATE_VALUE = LocalDateTime.MIN;
+
+ private static final String FXML_PATH = "components/TaskList.fxml";
+ private static EphemeralDB ephemeralDb = EphemeralDB.getInstance();
+
+ // Props
+ public List tasks = new ArrayList<>();
+ public List events = new ArrayList<>();
+
+ // FXML
+ @FXML
+ private VBox taskListDateItemsPlaceholder;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ loadTasks();
+ }
+
+ private void loadTasks() {
+ TaskListDateItem.reset(taskListDateItemsPlaceholder);
+
+ // Clears displayedCalendarItems in EphemeralDB.
+ ephemeralDb.clearDisplayedCalendarItems();
+
+ // Get a list of tasks mapped to each date
+ Map> tasksByDate = getItemsByDate(tasks);
+ Map> eventsByDate = getItemsByDate(events);
+
+ // Get unique task/event dates
+ Set uniqueDateSet = new HashSet<>();
+ uniqueDateSet.addAll(tasksByDate.keySet());
+ uniqueDateSet.addAll(eventsByDate.keySet());
+
+ // Sort the dates
+ List sortedUniqueDates = new ArrayList<>();
+ sortedUniqueDates.addAll(uniqueDateSet);
+ java.util.Collections.sort(sortedUniqueDates);
+
+ // For each dateTime, individually render a single TaskListDateItem.
+ for (LocalDateTime dateTime : sortedUniqueDates) {
+ List tasksForDate = tasksByDate.get(dateTime);
+ List eventsForDate = eventsByDate.get(dateTime);
+
+ TaskListDateItem item = load(primaryStage, taskListDateItemsPlaceholder, TaskListDateItem.class);
+ item.dateTime = dateTime;
+
+ if (tasksForDate != null) {
+ item.tasks = tasksForDate;
+ }
+
+ if (eventsForDate != null) {
+ item.events = eventsForDate;
+ }
+
+ item.render();
+ }
+ }
+
+ private Map> getItemsByDate(List calendarItems) {
+ Map> itemsByDate = new HashMap<>();
+
+ for (T item : calendarItems) {
+ LocalDateTime itemDate = DateUtil.floorDate(item.getCalendarDateTime());
+
+ // Handle tasks without a date
+ if (itemDate == null) {
+ itemDate = NO_DATE_VALUE;
+ }
+
+ // Creates ArrayList if not already exists.
+ if (!itemsByDate.containsKey(itemDate)) {
+ itemsByDate.put(itemDate, new ArrayList());
+ }
+
+ // Adds to the ArrayList.
+ itemsByDate.get(itemDate).add(item);
+ }
+
+ return itemsByDate;
+ }
+
+}
+```
+###### \java\seedu\todo\ui\components\TaskListDateItem.java
+``` java
+public class TaskListDateItem extends MultiComponent {
+
+ private static final String FXML_PATH = "components/TaskListDateItem.fxml";
+ private static EphemeralDB ephemeralDb = EphemeralDB.getInstance();
+ private static final String NO_DATE_STRING = "No Deadline";
+
+ // Props
+ public LocalDateTime dateTime;
+ public List tasks = new ArrayList<>();
+ public List events = new ArrayList<>();
+
+ // FXML
+ @FXML
+ private Text dateHeader;
+ @FXML
+ private Text dateLabel;
+ @FXML
+ private VBox dateCalendarItemsPlaceholder;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+
+ // Set header for DateItem using the "x days ago" format
+ String dateHeaderString;
+ if (dateTime == TaskList.NO_DATE_VALUE) {
+ dateHeaderString = NO_DATE_STRING;
+ } else {
+ dateHeaderString = DateUtil.formatDay(dateTime);
+ }
+
+ dateHeader.setText(dateHeaderString);
+
+ // Set date label using the short date format (e.g. Fri 14 Oct)
+ if (dateTime != TaskList.NO_DATE_VALUE) {
+ String dateLabelString = DateUtil.formatShortDate(dateTime);
+ dateLabel.setText(dateLabelString);
+ }
+
+ // Clear the TaskList of its items
+ TaskListTaskItem.reset(dateCalendarItemsPlaceholder);
+
+ // Load task and event items
+ loadEventItems();
+ loadTaskItems();
+ }
+
+ private void loadTaskItems() {
+ for (Task task : tasks) {
+ TaskListTaskItem item = load(primaryStage, dateCalendarItemsPlaceholder, TaskListTaskItem.class);
+
+ // Add to EphemeralDB and get the index.
+ int displayIndex = ephemeralDb.addToDisplayedCalendarItems(task);
+
+ // Set the props and render the TaskListTaskItem.
+ item.task = task;
+ item.displayIndex = displayIndex;
+ item.render();
+ }
+ }
+
+ private void loadEventItems() {
+ for (Event event : events) {
+ TaskListEventItem item = load(primaryStage, dateCalendarItemsPlaceholder, TaskListEventItem.class);
+
+ // Add to EphemeralDB and get the index.
+ int displayIndex = ephemeralDb.addToDisplayedCalendarItems(event);
+
+ // Set the props and render the TaskListTaskItem.
+ item.event = event;
+ item.displayIndex = displayIndex;
+ item.render();
+ }
+ }
+
+}
+```
+###### \java\seedu\todo\ui\components\TaskListEventItem.java
+``` java
+public class TaskListEventItem extends MultiComponent {
+
+ private static final String FXML_PATH = "components/TaskListEventItem.fxml";
+ private static final String ICON_PATH = "/images/icon-calendar.png";
+
+ // Props
+ public Event event;
+ public Integer displayIndex;
+
+ // FXML
+ @FXML
+ private Text eventText;
+ @FXML
+ private Text eventTime;
+ @FXML
+ private Text rowIndex;
+ @FXML
+ private ImageView rowIconImageView;
+ @FXML
+ private Text eventTagListText;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ eventText.setText(event.getName());
+ eventTime.setText(DateUtil.formatDateFromTo(event.getStartDate(), event.getEndDate()));
+ rowIndex.setText(displayIndex.toString());
+ eventTagListText.setText(StringUtil.checkEmptyList(event.getTagList())); //TODO : Change FXML file to support TagList
+
+ // Set image
+ rowIconImageView.setImage(new Image(ICON_PATH));
+
+ // If over, set style
+ if (event.isOver()) {
+ eventText.getStyleClass().add("completed");
+ }
+ }
+
+}
+```
+###### \java\seedu\todo\ui\components\TaskListTaskItem.java
+``` java
+public class TaskListTaskItem extends MultiComponent {
+
+ private static final String COMPLETED_ICON_PATH = "/images/icon-tick.png";
+ private static final String FXML_PATH = "components/TaskListTaskItem.fxml";
+
+ // Props
+ public Task task;
+ public Integer displayIndex;
+
+ // FXML
+ @FXML
+ private Text taskText;
+ @FXML
+ private Text taskTime;
+ @FXML
+ private Text rowIndex;
+ @FXML
+ private Circle taskCheckMarkCircle;
+ @FXML
+ private ImageView taskCheckMarkImage;
+ @FXML
+ private Text taskTagListText;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ rowIndex.setText(displayIndex.toString());
+ taskText.setText(task.getName());
+ taskTagListText.setText(StringUtil.checkEmptyList(task.getTagList()));
+
+ LocalDateTime dateTime = task.getCalendarDateTime();
+ if (dateTime != null) {
+ taskTime.setText(DateUtil.formatTime(dateTime));
+ }
+
+ if (task.isCompleted()) {
+ showCompleted();
+ } else {
+ showIncomplete();
+ }
+ }
+
+ private void showCompleted() {
+ taskCheckMarkImage.setImage(new Image(COMPLETED_ICON_PATH));
+ taskCheckMarkCircle.setRadius(0);
+ taskText.getStyleClass().add("completed");
+ }
+
+ private void showIncomplete() {
+ taskCheckMarkImage.setFitWidth(0);
+ }
+
+}
+```
+###### \java\seedu\todo\ui\InputHandler.java
+``` java
+/**
+ * InputHandler is the bridge between the UI, or more
+ * specifically the Console, and the Controllers.
+ *
+ * It will match the correct Controller to handle the command
+ * based on the command words.
+ *
+ * It will also maintain a command history (similar to that of a
+ * terminal console) where the user can interact with using
+ * up and down arrow keys.
+ */
+public class InputHandler {
+
+ private static InputHandler instance;
+
+ private static final int MAX_HISTORY_SIZE = 20;
+ private static LinkedList commandHistory = new LinkedList();
+ private static ListIterator commandHistoryIterator = commandHistory.listIterator();
+
+ private static final String CHAR_SPACE = " ";
+
+ protected InputHandler() {
+ // Prevent instantiation.
+ }
+
+ /**
+ * Gets the current input handler instance.
+ */
+ public static InputHandler getInstance() {
+ if (instance == null) {
+ instance = new InputHandler();
+ }
+
+ return instance;
+ }
+
+ /**
+ * Pushes a command to the end of a LinkedList.
+ * Commands are stored like a queue, where the oldest items
+ * are at the start of the List and will be popped off first.
+ *
+ * @param command Command string
+ */
+ private void pushCommand(String command) {
+ // Adds to the end of the LinkedList.
+ commandHistory.addLast(command);
+
+ // Truncates the list when it gets too big.
+ if (commandHistory.size() > MAX_HISTORY_SIZE) {
+ commandHistory.removeFirst();
+ }
+
+ // Create a new iterator, initialize position to point right at the end.
+ commandHistoryIterator = commandHistory.listIterator(commandHistory.size());
+ }
+
+ /**
+ * Gets the previous command from the command history. Successive calls will return commands earlier in history.
+ *
+ * @return The input command earlier than what was previously retrieved
+ */
+ public String getPreviousCommandFromHistory() {
+ if (!commandHistoryIterator.hasPrevious()) {
+ return "";
+ }
+
+ return commandHistoryIterator.previous();
+ }
+
+ /**
+ * Gets the next command from the command history. Successive calls will return commands later in history.
+ *
+ * @return The input command later than what was previously retrieved
+ */
+ public String getNextCommandFromHistory() {
+ if (!commandHistoryIterator.hasNext()) {
+ return "";
+ }
+
+ return commandHistoryIterator.next();
+ }
+
+ /**
+ * Processes the command. Returns true if the command was intercepted by a controller, false if otherwise.
+ * If the command was not intercepted by a controller, it means that the command was not recognized.
+ */
+ public boolean processInput(String input) {
+
+ Map aliases = ConfigCenter.getInstance().getConfig().getAliases();
+ String aliasedInput = StringUtil.replaceAliases(input, aliases);
+
+ Controller[] controllers = instantiateAllControllers();
+
+ // Extract keyword.
+ String keyword = extractKeyword(aliasedInput);
+
+ // Get controller which has the maximum confidence.
+ Controller matchingController = getMatchingController(keyword, controllers);
+
+ // If command keyword did not match any controllers, console will show invalid command.
+ if (matchingController == null) {
+ return false;
+ }
+
+ // Patch input commands.
+ input = patchCommandKeyword(input);
+ aliasedInput = patchCommandKeyword(aliasedInput);
+
+ // Process using matched controller.
+ boolean isProcessSuccess = processWithController(input, aliasedInput, matchingController);
+
+ // Catch commands which throw errors here.
+ if (!isProcessSuccess) {
+ return false;
+ }
+
+ // Since command is not invalid, we push it to history
+ pushCommand(aliasedInput);
+
+ return true;
+ }
+
+ /**
+ * Process an input/aliasedInput with a selected controller.
+ *
+ * Note that for proper functioning, alias
and
+ * unalias
will receive the input
instead of
+ * aliasedInput
for proper functioning.
+ *
+ * @param input Raw user input
+ * @param aliasedInput Input with aliases replaced
+ * @param selectedController Controller to process input
+ * @return true if processing was successful, false otherwise
+ */
+ private boolean processWithController(String input, String aliasedInput, Controller selectedController) {
+ try {
+ // Alias and unalias should not receive an aliasedInput for proper functioning.
+ if (selectedController.getClass() == AliasController.class ||
+ selectedController.getClass() == UnaliasController.class) {
+ selectedController.process(input);
+ } else {
+ selectedController.process(aliasedInput);
+ }
+ return true;
+ } catch (ParseException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Get controller which matches the command keyword.
+ *
+ * @param aliasedCommandKeyword Input with aliases replaced appropriately
+ * @param controllers Array of instantiated controllers to test
+ * @return Matching controller.
+ */
+ private Controller getMatchingController(String aliasedCommandKeyword, Controller[] controllers) {
+ for (Controller controller : controllers) {
+ if (controller.matchCommandKeyword(aliasedCommandKeyword)) {
+ return controller;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Extracts the command keyword from the input command.
+ */
+ private String extractKeyword(String inputCommand) {
+ String[] commandWords = inputCommand.trim().split(CHAR_SPACE);
+
+ if (commandWords.length < 1) {
+ return "";
+ }
+
+ return commandWords[0].toLowerCase();
+ }
+
+ /**
+ * Replaces the first word in a command input string to prepare it for parsing and/or disambiguating.
+ */
+ private String patchCommandKeyword(String inputCommand) {
+ String[] commandWords = inputCommand.trim().split(CHAR_SPACE);
+
+ if (commandWords.length < 1) {
+ return "";
+ } else {
+ commandWords[0] = extractKeyword(inputCommand);
+ }
+
+ return String.join(CHAR_SPACE, commandWords);
+ }
+
+ private static Controller[] instantiateAllControllers() {
+ return new Controller[] { new AliasController(),
+ new UnaliasController(),
+ new HelpController(),
+ new AddController(),
+ new ListController(),
+ new DestroyController(),
+ new CompleteTaskController(),
+ new UncompleteTaskController(),
+ new UpdateController(),
+ new UndoController(),
+ new RedoController(),
+ new ConfigController(),
+ new ClearController(),
+ new FindController(),
+ new TagController(),
+ new UntagController(),
+ new ExitController() };
+ }
+
+}
+```
+###### \java\seedu\todo\ui\MainWindow.java
+``` java
+/**
+ * The Main Window. Provides the basic application layout containing
+ * a menu bar and space where other JavaFX elements can be placed.
+ */
+public class MainWindow extends Component {
+
+ private static final String FXML_PATH = "MainWindow.fxml";
+ private static final String ICON_PATH = "/images/logo-512x512.png";
+ public static final int MIN_HEIGHT = 600;
+ public static final int MIN_WIDTH = 600;
+
+ private static final String COMMAND_HELP = "help";
+ private static final String COMMAND_LIST = "list";
+ private static final String COMMAND_CONFIG = "config";
+ private static final String COMMAND_ALIAS = "alias";
+
+ private static final String KEY_OPEN_HELP = "F1";
+ private static final String KEY_OPEN_LIST = "F5";
+ private static final String KEY_OPEN_CONFIG = "F12";
+
+ // Handles to elements of this Ui container
+ private VBox rootLayout;
+ private Scene scene;
+
+ // FXML Components
+ @FXML
+ private AnchorPane childrenPlaceholder;
+ @FXML
+ private AnchorPane consoleInputPlaceholder;
+ @FXML
+ private AnchorPane headerPlaceholder;
+ @FXML
+ private MenuItem homeMenuItem;
+ @FXML
+ private MenuItem configMenuItem;
+ @FXML
+ private MenuItem helpMenuItem;
+
+ public void configure(Config config) {
+ String appTitle = config.getAppTitle();
+
+ // Configure the UI
+ setTitle(appTitle);
+ setIcon(ICON_PATH);
+ setWindowMinSize();
+ scene = new Scene(rootLayout);
+ primaryStage.setScene(scene);
+
+ // Bind accelerators
+ setAccelerators();
+
+ // Load other components.
+ loadComponents();
+ }
+
+ protected void loadComponents() {
+ // Load Header
+ Header header = UiPartLoader.loadUiPart(primaryStage, getHeaderPlaceholder(), Header.class);
+ header.appTitle = ConfigCenter.getInstance().getConfig().getAppTitle();
+ header.versionString = MainApp.VERSION.toString();
+ header.render();
+
+ // Load ConsoleInput
+ Console console = UiPartLoader.loadUiPart(primaryStage, getConsoleInputPlaceholder(), Console.class);
+ console.consoleOutput = UiManager.getConsoleMessage();
+ console.consoleInputValue = UiManager.getConsoleInputValue();
+ console.render();
+ }
+
+ @Override
+ public void setNode(Node node) {
+ rootLayout = (VBox) node;
+ }
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ public void show() {
+ primaryStage.show();
+ }
+
+ public void hide() {
+ primaryStage.hide();
+ }
+
+ public void setTitle(String appTitle) {
+ primaryStage.setTitle(appTitle);
+ }
+
+ public void setWindowMinSize() {
+ primaryStage.setMinHeight(MIN_HEIGHT);
+ primaryStage.setMinWidth(MIN_WIDTH);
+ }
+
+ protected T loadView(Class viewClass) {
+ return load(primaryStage, getChildrenPlaceholder(), viewClass);
+ }
+
+ public Scene getScene() {
+ return scene;
+ }
+
+ /** ================ FXML COMPONENTS ================== **/
+
+ public AnchorPane getChildrenPlaceholder() {
+ return childrenPlaceholder;
+ }
+
+ public AnchorPane getConsoleInputPlaceholder() {
+ return consoleInputPlaceholder;
+ }
+
+ public AnchorPane getHeaderPlaceholder() {
+ return headerPlaceholder;
+ }
+
+ /** ================ ACCELERATORS ================== **/
+
+ private void setAccelerators() {
+ helpMenuItem.setAccelerator(KeyCombination.valueOf(KEY_OPEN_HELP));
+ homeMenuItem.setAccelerator(KeyCombination.valueOf(KEY_OPEN_LIST));
+ configMenuItem.setAccelerator(KeyCombination.valueOf(KEY_OPEN_CONFIG));
+ }
+
+ /** ================ ACTION HANDLERS ================== **/
+
+ @FXML
+ public void handleHelp() {
+ // Pass directly to HelpController.
+ new HelpController().process(COMMAND_HELP);
+ }
+
+ @FXML
+ public void handleHome() {
+ // Pass directly to ListController.
+ try {
+ new ListController().process(COMMAND_LIST);
+ } catch (ParseException e) {
+ return;
+ }
+ }
+
+ @FXML
+ public void handleConfig() {
+ // Pass directly to HelpController.
+ new ConfigController().process(COMMAND_CONFIG);
+ }
+
+ @FXML
+ public void handleAlias() {
+ // Pass directly to HelpController.
+ new AliasController().process(COMMAND_ALIAS);
+ }
+
+ @FXML
+ private void handleExit() {
+ raise(new ExitAppRequestEvent());
+ }
+}
+```
+###### \java\seedu\todo\ui\UiManager.java
+``` java
+/**
+ * The manager of the UI component.
+ *
+ * Singleton class for other modules to interact
+ * with the UI. Provides two methods {@code loadView} and
+ * {@code renderView}, which generate a view controller
+ * and subsequently rendering it after passing/binding
+ * relevant properties.
+ */
+public class UiManager extends ComponentManager implements Ui {
+ private static final Logger logger = LogsCenter.getLogger(UiManager.class);
+
+ // Only one instance of UiManager should be present.
+ private static UiManager instance = null;
+
+ // Only one currentView.
+ public static View currentView;
+
+ private static String currentConsoleMessage = "";
+ private static String currentConsoleInputValue = "";
+
+ private Config config;
+ private MainWindow mainWindow;
+
+ private static final String FATAL_ERROR_DIALOG = "Fatal error during initializing";
+ private static final String LOAD_VIEW_ERROR = "Cannot loadView: UiManager not instantiated.";
+
+ protected UiManager() {
+ // Prevent instantiation.
+ }
+
+ public static UiManager getInstance() {
+ return instance;
+ }
+
+ public static void initialize(Config config) {
+ if (instance == null) {
+ instance = new UiManager();
+ }
+
+ instance.config = config;
+ }
+
+ @Override
+ public void start(Stage primaryStage) {
+ logger.info("Starting UI...");
+
+ // Show main window.
+ try {
+ mainWindow = UiPartLoader.loadUiPart(primaryStage, null, MainWindow.class);
+ mainWindow.configure(config);
+ mainWindow.render();
+ mainWindow.show();
+ } catch (Throwable e) {
+ logger.severe(StringUtil.getDetails(e));
+ showFatalErrorDialogAndShutdown(FATAL_ERROR_DIALOG, e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ mainWindow.hide();
+ }
+
+ public MainWindow getMainWindow() {
+ return mainWindow;
+ }
+
+ /**
+ * Helper function to load view into the MainWindow.
+ */
+ public static T loadView(Class viewClass) {
+ if (instance == null) {
+ logger.warning(LOAD_VIEW_ERROR);
+ return null;
+ }
+
+ return instance.mainWindow.loadView(viewClass);
+ }
+
+ /**
+ * Updates the currentView and renders it.
+ *
+ * @param view View to render.
+ */
+ public static void renderView(View view) {
+ if (view != null && view.getNode() != null) {
+ currentView = view;
+
+ // Clear console values first
+ currentConsoleInputValue = "";
+ currentConsoleMessage = "";
+
+ // Render view
+ view.render();
+ }
+ }
+
+ public static String getConsoleMessage() {
+ return currentConsoleMessage;
+ }
+
+ public static String getConsoleInputValue() {
+ return currentConsoleInputValue;
+ }
+
+ /**
+ * Sets the message shown in the console and reloads the console box.
+ * Does not do anything if no views have been loaded yet.
+ *
+ * @param consoleMessage Message to display in the console.
+ */
+ public static void updateConsoleMessage(String consoleMessage) {
+ if (currentView != null) {
+ currentConsoleMessage = consoleMessage;
+ instance.mainWindow.loadComponents();
+ }
+ }
+
+ /**
+ * Sets the message shown in the console input box and reloads the console box.
+ * Does not do anything if no views have been loaded yet.
+ *
+ * @param consoleInputValue Message to display in the console input box.
+ */
+ public static void updateConsoleInputValue(String consoleInputValue) {
+ if (currentView != null) {
+ currentConsoleInputValue = consoleInputValue;
+ instance.mainWindow.loadComponents();
+ }
+ }
+
+
+ /** ================ DISPLAY ERRORS ================== **/
+
+ private void showAlertDialogAndWait(Alert.AlertType type, String title, String headerText, String contentText) {
+ showAlertDialogAndWait(mainWindow.getPrimaryStage(), type, title, headerText, contentText);
+ }
+
+ private static void showAlertDialogAndWait(Stage owner, AlertType type, String title, String headerText,
+ String contentText) {
+ final Alert alert = new Alert(type);
+ alert.initOwner(owner);
+ alert.setTitle(title);
+ alert.setHeaderText(headerText);
+ alert.setContentText(contentText);
+
+ alert.showAndWait();
+ }
+
+ private void showFatalErrorDialogAndShutdown(String title, Throwable e) {
+ logger.severe(title + " " + e.getMessage() + StringUtil.getDetails(e));
+ showAlertDialogAndWait(Alert.AlertType.ERROR, title, e.getMessage(), e.toString());
+ Platform.exit();
+ System.exit(1);
+ }
+
+}
+```
+###### \java\seedu\todo\ui\UiPartLoader.java
+``` java
+/**
+ * A utility class to load UiParts from FXML files.
+ * Modified from original codebase to support two-step
+ * loading and rendering (see {@link UiManager}).
+ */
+public class UiPartLoader {
+ private final static String FXML_FILE_FOLDER = "/ui/";
+ private final static String FXML_ERROR_MESSAGE = "FXML Load Error for ";
+ private final static String INSTANTION_EXCEPTION_ERROR_MESSAGE = "Could not instantiate ";
+
+ /**
+ * Loads the UiPart and returns the view controller.
+ *
+ * @param primaryStage The primary stage for the view.
+ * @param placeholder The placeholder where the loaded Ui Part is added.
+ * @param uiPartClass The UiPart class to load.
+ * @param The type of the UiPart
+ */
+ public static T loadUiPart(Stage primaryStage, Pane placeholder, Class uiPartClass) {
+ FXMLLoader loader = new FXMLLoader();
+
+ // Get FXML path
+ T instance = null;
+ try {
+ instance = uiPartClass.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ String errorMessage = INSTANTION_EXCEPTION_ERROR_MESSAGE + uiPartClass.getName();
+ throw new RuntimeException(errorMessage, e);
+ }
+ String fxmlPath = instance.getFxmlPath();
+
+ // Continue with loading
+ loader.setLocation(getFXMLResource(fxmlPath));
+ Node mainNode = loadLoader(loader, fxmlPath);
+
+ T controller = loader.getController();
+ controller.setStage(primaryStage);
+ controller.setPlaceholder(placeholder);
+ controller.setNode(mainNode);
+ return controller;
+ }
+
+
+ private static Node loadLoader(FXMLLoader loader, String fxmlFileName) {
+ try {
+ return loader.load();
+ } catch (Exception e) {
+ String errorMessage = FXML_ERROR_MESSAGE + fxmlFileName;
+ throw new RuntimeException(errorMessage, e);
+ }
+ }
+
+ private static URL getFXMLResource(String fxmlPath) {
+ return MainApp.class.getResource(FXML_FILE_FOLDER + fxmlPath);
+ }
+
+}
+```
+###### \java\seedu\todo\ui\views\ConfigView.java
+``` java
+/**
+ * Config View, which shows the list of settings that can be configured.
+ */
+public class ConfigView extends View {
+
+ private static final String FXML_PATH = "views/ConfigView.fxml";
+
+ private static final String ICON_PATH = "/images/icon-settings.png";
+ private static final String TEXT_INSTRUCTIONS = "To change a setting, use the following command:\n config SETTING VALUE";
+
+ // FXML
+ @FXML
+ private Text configInstructionsText;
+ @FXML
+ private ImageView configImageView;
+ @FXML
+ private Pane configsPlaceholder;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ // Makes the Component full width wrt parent container.
+ FxViewUtil.makeFullWidth(this.mainNode);
+
+ // Set instructions
+ configInstructionsText.setText(TEXT_INSTRUCTIONS);
+
+ // Load image
+ configImageView.setImage(new Image(ICON_PATH));
+
+ // Get definitions
+ List configDefinitions = ConfigCenter.getInstance().getConfig().getDefinitions();
+
+ // Clear items
+ ConfigItem.reset(configsPlaceholder);
+
+ // Load items
+ for (ConfigDefinition definition : configDefinitions) {
+ ConfigItem item = load(primaryStage, configsPlaceholder, ConfigItem.class);
+ item.configDefinition = definition;
+ item.render();
+ }
+ }
+
+}
+```
+###### \java\seedu\todo\ui\views\HelpView.java
+``` java
+/**
+ * Help View, which shows all the commands available.
+ */
+public class HelpView extends View {
+
+ private static final String FXML_PATH = "views/HelpView.fxml";
+
+ // Props
+ public List commandDefinitions = new ArrayList();
+
+ // FXML
+ @FXML
+ private Pane helpCommandsPlaceholder;
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ // Makes the Component full width wrt parent container.
+ FxViewUtil.makeFullWidth(this.mainNode);
+
+ // Clear help commands
+ HelpCommandItem.reset(helpCommandsPlaceholder);
+
+ // Load help commands
+ for (CommandDefinition command : commandDefinitions) {
+ HelpCommandItem item = load(primaryStage, helpCommandsPlaceholder, HelpCommandItem.class);
+ item.commandName = command.getCommandName();
+ item.commandDescription = command.getCommandDescription();
+ item.commandSyntax = command.getCommandSyntax();
+ item.render();
+ }
+ }
+
+
+}
+```
+###### \java\seedu\todo\ui\views\IndexView.java
+``` java
+/**
+ * Index View, which shows the list of tasks and tags in a two-column format.
+ */
+public class IndexView extends View {
+
+ private static final String FXML_PATH = "views/IndexView.fxml";
+
+ // FXML
+ @FXML
+ private Pane tagsPane;
+ @FXML
+ private Pane tasksPane;
+
+ // Props
+ public List events = new ArrayList<>();
+ public List tasks = new ArrayList<>();
+ public List tags = new ArrayList<>();
+
+ @Override
+ public String getFxmlPath() {
+ return FXML_PATH;
+ }
+
+ @Override
+ public void componentDidMount() {
+ // Makes full width wrt parent container.
+ FxViewUtil.makeFullWidth(this.mainNode);
+
+ // Load sub components
+ loadComponents();
+ }
+
+ private void loadComponents() {
+ // Render TagList
+ Sidebar tagList = load(primaryStage, tagsPane, Sidebar.class);
+ tagList.tags = tags;
+ tagList.render();
+
+ // Render TaskList
+ TaskList taskList = load(primaryStage, tasksPane, TaskList.class);
+ taskList.tasks = tasks;
+ taskList.events = events;
+ taskList.render();
+ }
+
+}
+```
+###### \java\seedu\todo\ui\views\View.java
+``` java
+/**
+ * A {@code View} is essentially a special type of Component, with no implementation
+ * differences at the moment. However, a {@code View} is the grouping of Components
+ * to form the whole UI experience. In the case of this app, the {@code View} corresponds
+ * with the portion between the Header and the Console.
+ *
+ * Different {@code View}s can be loaded depending on the context.
+ */
+public abstract class View extends Component {
+
+}
+```
+###### \resources\ui\components\ConfigItem.fxml
+``` fxml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+###### \resources\ui\components\Console.fxml
+``` fxml
+
+
+
+
+
+
+
+
+
+
+```
+###### \resources\ui\components\Header.fxml
+``` fxml
+
+
+
+
+
+
+
+