diff --git a/collated/docs/A0116603R.md b/collated/docs/A0116603R.md new file mode 100644 index 000000000000..a7ccb0f41a18 --- /dev/null +++ b/collated/docs/A0116603R.md @@ -0,0 +1,173 @@ +# A0116603R +###### /DeveloperGuide.md +``` md +### Continuous Integration + +We use [Travis CI](https://travis-ci.org/) to perform _Continuous Integration_ on our projects. +See [UsingTravis.md](UsingTravis.md) for more details. + +### Making a Release + +Here are the steps to create a new release. + + 1. Generate a JAR file [using Gradle](UsingGradle.md#creating-the-jar-file). + 2. Tag the repo with the version number. e.g. `v0.1` + 2. [Crete a new release using GitHub](https://help.github.com/articles/creating-releases/) + and upload the JAR file your created. + +### Managing Dependencies + +A project often depends on third-party libraries. For example, Address Book depends on the +[Jackson library](http://wiki.fasterxml.com/JacksonHome) for XML parsing. Managing these _dependencies_ +can be automated using Gradle. For example, Gradle can download the dependencies automatically, which +is better than these alternatives.
+a. Include those libraries in the repo (this bloats the repo size)
+b. Require developers to download those libraries manually (this creates extra work for developers)
+ +## Appendix A : User Stories + +| User Stories | Likely/Not Likely | +| ------ | ----------- | +|As a user I can add an entry by specifying a entry title only, so that I can record entrys that need to be done ‘some day’.|Likely +|As a user I can add entries that have a specific start and end time, so that I can record events with a specific duration.|Likely| +|As a user I can add descriptions to my entries, so that I can add detail to my tasks.|Likely| +|As a user I can edit an entry, so that I can make changes to entries as needed.|Likely| +|As a user I can delete an entry, so that I can get rid of entries that I no longer care to track.|Likely| +|As a user I can list all entries that are due, so that I can get an overview of all my entries.|Likely| +|As a user I can find upcoming entries, so that I can decide what needs to be done soon.|Likely| +|As a user, I can set a entry as completed, so that I can keep track of completed/uncompleted entries.|Likely| +|As a user, I can add tags to the entries so that I can categorize entries together|Likely| +|As a user I can search for an entry by different entry attributes, so that I can find specific entries.|Likely| +|As a user I can undo a certain number of actions so that I can correct any mistaken actions.|Likely| +|As a new user I can view more information about a particular command, so that I can learn how to use various commands.|Likely| +|As an advanced user I can use shorter versions of a command, so that type a command faster.|Likely| +|As a user, I can schedule a entry to be automatically added with a given frequency so that I don’t have to manually add entries every time.|Likely| +|As an advanced user, I can specify a specific folder as the data storage location, so that I can choose to store the data file in a local folder controlled by a cloud syncing service, allowing me to access entry data from multiple computers.|Likely| +|As a user, I can choose different views of all my tasks, e.g. “Due Today”, “Due This Week”, so that I can focus on what is important to me at that moment.|Likely| +|As a user, I can use autocompletion of entry description so that I can quickly add entries that are similar to what I have added before|Not Likely| +|As a user, I can personalize my app by changing the color scheme so that it fits my aesthetic requirements.|Not Likely| +|As an advanced user, I can rebind default commands into my own keyboard shortcuts, so that I can use shortcuts that I am comfortable with.|Not Likely| +|As a user, I can type in commands in a more ‘natural’ manner, so that I do not need to learn the command format.|Not Likely| + +## Appendix B : Use Cases + +(For all use cases below, the **System** is the `TaskManager` and the **Actor** is the `user`, unless specified otherwise) + +#### Use case: Delete person + +**Use case: Create a floating task** + +Actors: User + +MSS + +1. User request to create a new entry without specifying the start and/or end date and deadline +2. TodoList add that particular floating task into Storage and save it + + Use Case Ends + +*Extensions* + +1a. Entry is entered with invalid properties e.g. title +> 1a1. TodoList warns user that the entry is invalid and does not add it into Storage +> +> Use Case Ends + +1b. Entry already exists +> 1b1. TodoList warns user that the entry already exists and does not add it into Storage +> +> Use Case Ends + +**Use case: Create an event** + +Actors: User + +MSS + +1. User request to create a new entry while specifying the start and/or end date but not deadline +2. TodoList add that particular event into Storage and save it + + Use Case Ends + +**Use Case: Create a deadline** + +Actors: User + +MSS + +1. User request to create a new entry while specifying the deadline but no start and/or end date +2. TodoList add that particular deadline into Storage and save it + + Use Case Ends + +**Use Case: Delete an entry** + +Actors: User + +MSS + +1. User requests to delete an entry with a specified id +2. TodoList deletes the entry + + Use Case Ends + +*Extensions* + +1a. Entry with the specified id does not exist +> 1a1. TodoList warns the user that the entry with the specified id does not exist +> +> Use Case Ends + +``` +###### /UserGuide.md +``` md +
+ +Our to-do application primarily takes in input using a command line interface. It supports the basic creation, reading, updating and deletion of entries. You may add entries with or without deadlines. These are known as tasks. Entries with a start and/or end time are known as events. + +To start, try adding a new task: + +``` +add Buy groceries +``` + + +You can edit a task with the following command: + +``` +edit 0 end/tomorrow 5pm +``` +Yep, natural language is parsed as well! + + + +Undo is quite easily done: + +``` +$ undo + +$ list +[ ] Get groceries +``` + +Finally, try deleting the task: + +``` +delete 1 +``` + + + +``` +###### /UserGuide.md +``` md +#### Help +``` +help [] +``` + +Show available commands and how to use them + +Help is also shown if you enter an incorrect command e.g. abcd + +``` diff --git a/collated/docs/A0121501E.md b/collated/docs/A0121501E.md new file mode 100644 index 000000000000..024afd9e9850 --- /dev/null +++ b/collated/docs/A0121501E.md @@ -0,0 +1,162 @@ +# A0121501E +###### /DeveloperGuide.md +``` md +**Use Case: List all entries** + +Actors: User + +MSS + +1. User requests to list all entries +2. TodoList shows a list of entries, sorted by date (oldest first) + + Use Case Ends + +**Use Case: List entries with filters** + +Actors: User + +MSS + +1. User requests to list entries with some filters +2. TodoList shows a list of entries that satisfies the filters, sorted by date (oldest first) + + Use Case Ends + +**Use Case: Add tags to a list of entries** + +Actors: User + +MSS + +1. User requests to list a filtered list of entries +2. User requests to add tags to the listed entries + + Use Case Ends + +*Extensions* + +2a. User attempts to add an invalid tag +>2a1. TodoList tells the user that the tag name is invalid +> +>Use Case Ends + +## Appendix C : Non Functional Requirements + +1. User should navigate the application primarily using the command line +2. Should be easy to navigate through large numbers of tasks and events +3. Should not have a response time more than 100ms +4. Should have minimal data loss after an unexpected crash +5. Should have consistent data +6. Should be able to accommodate more than 1000 tasks and events +7. Should be able to work offline +8. Should not exceed 100MB of RAM when in use +9. Should store data as files instead of in some relational database +10. Storage file should be human-editable and human-readable + + +## Appendix D : Glossary + +##### Mainstream OS + +> Windows, Linux, Unix, OS-X + + +## Appendix E : Product Survey + +### Trello: + +Strengths: + +- Geared towards task management: focus on only 1 thing +- Covers various aspects of task management: deadline, assignees, descriptions, discussions +- Designed to be collaborative +- Unique UI, which gives a great overview of the tasks +- Cross-platform + +Weaknesses: + +- Missing ability to schedule recurring tasks +- Poor integration with calendar +- A bit cluttered for personal use, since it’s designed for collaborative work + +### Evernote: + +Strengths: + +- All-in-one swiss army knife: Not only task management, can keep notes as well +- Good in capturing tasks that need to be done before a specific date/time, or after a specific date/time, and items without specific times. +- Can access tasks online and offline (if Jim is willing to pay for Evernote premium) + +Weaknesses: + +- Needs a few clicks/keyboard shortcuts to add a new task +- No way to “block” some time out except by explicitly writing it in a note. + +### Google Keep: + +Strengths: + +- Compact UI that allows for many tasks to be viewed at once. +- Colored tasks as visual aid. +- Checkbox to indicate task completion. Alternatively, tasks can be archived to indicate completion. +- Supports dynamic content such as links, images and drawings. +- Sync through multiple gadget. + +Weakness: + +- No options for viewing the list of tasks in different orders (most recent, deadline etc) +- No calendar view. + +``` +###### /UserGuide.md +``` md +#### Tagging +``` +tag # [#...] +``` + +Add tag(s) to a particular entry with a specified id + +Examples: + +- `tag 123 #CS2103T #rocks` + +Delete tag(s) from a particular entry with a specified id using `untag` + +- `untag 123 #rocks` + +Duplicated tags will only be added once + +``` +###### /UserGuide.md +``` md +#### Deleting +``` +delete +``` +Delete the task with a particular entry id + +`list` should be executed before this command to obtain a entry id. + +Examples: + +- `Delete 42` + + +#### Marking +``` +mark +``` + +Check (or uncheck, for `unmark`) a entry as completed. + +`list` should be executed before this command to obtain a entry id. + +Examples: + +- `mark 42` + +- `unmark 42` + +``` diff --git a/collated/docs/A0126539Y.md b/collated/docs/A0126539Y.md new file mode 100644 index 000000000000..8696fa827339 --- /dev/null +++ b/collated/docs/A0126539Y.md @@ -0,0 +1,167 @@ +# A0126539Y +###### /DeveloperGuide.md +``` md +### Logic component + +
+ +**API** : [`Logic.java`](../src/main/java/seedu/address/logic/Logic.java) + +1. `Logic` uses the `Parser` class to parse the user command. +2. This results in a `Command` object which is executed by the `LogicManager`. +3. The command execution can affect the `Model` (e.g. adding a person) and/or raise events. +4. The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. + +Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` + API call.
+
+ +### Model component + +
+ +**API** : [`Model.java`](../src/main/java/seedu/address/model/Model.java) + +The `Model`, +* stores a `UserPref` object that represents the user's preferences. +* stores the Address Book data. +* exposes a `UnmodifiableObservableList` that can be 'observed' e.g. the UI can be bound to this list + so that the UI automatically updates when the data in the list change. +* does not depend on any of the other three components. + +### Storage component + +
+ +**API** : [`Storage.java`](../src/main/java/seedu/address/storage/Storage.java) + +The `Storage` component, +* can save `UserPref` objects in json format and read it back. +* can save the Address Book data in xml format and read it back. + +### Common classes + +Classes used by multiple components are in the `seedu.addressbook.commons` package. + +## Implementation + +### Logging + +We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels +and logging destinations. + +* The logging level can be controlled using the `logLevel` setting in the configuration file + (See [Configuration](#configuration)) +* The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to + the specified logging level +* Currently log messages are output through: `Console` and to a `.log` file. + +**Logging Levels** + +* `SEVERE` : Critical problem detected which may possibly cause the termination of the application +* `WARNING` : Can continue, but with caution +* `INFO` : Information showing the noteworthy actions by the App +* `FINE` : Details that is not usually noteworthy but may be useful in debugging + e.g. print the actual list instead of just its size + +### Configuration + +Certain properties of the application can be controlled (e.g App name, logging level) through the configuration file +(default: `config.json`): + + +## Testing + +Tests can be found in the `./src/test/java` folder. + +**In Eclipse**: +> If you are not using a recent Eclipse version (i.e. _Neon_ or later), enable assertions in JUnit tests + as described [here](http://stackoverflow.com/questions/2522897/eclipse-junit-ea-vm-option). + +* To run all tests, right-click on the `src/test/java` folder and choose + `Run as` > `JUnit Test` +* To run a subset of tests, you can right-click on a test package, test class, or a test and choose + to run as a JUnit test. + +**Using Gradle**: +* See [UsingGradle.md](UsingGradle.md) for how to run tests using Gradle. + +We have two types of tests: + +1. **GUI Tests** - These are _System Tests_ that test the entire App by simulating user actions on the GUI. + These are in the `guitests` package. + +2. **Non-GUI Tests** - These are tests not involving the GUI. They include, + 1. _Unit tests_ targeting the lowest level methods/classes.
+ e.g. `seedu.address.commons.UrlUtilTest` + 2. _Integration tests_ that are checking the integration of multiple code units + (those code units are assumed to be working).
+ e.g. `seedu.address.storage.StorageManagerTest` + 3. Hybrids of unit and integration tests. These test are checking multiple code units as well as + how the are connected together.
+ e.g. `seedu.address.logic.LogicManagerTest` + +**Headless GUI Testing** : +Thanks to the [TestFX](https://github.com/TestFX/TestFX) library we use, + our GUI tests can be run in the _headless_ mode. + In the headless mode, GUI tests do not show up on the screen. + That means the developer can do other things on the Computer while the tests are running.
+ See [UsingGradle.md](UsingGradle.md#running-tests) to learn how to run tests in headless mode. + +## Dev Ops + +### Build Automation + +See [UsingGradle.md](UsingGradle.md) to learn how to use Gradle for build automation. + +``` +###### /UserGuide.md +``` md +## Commands + +#### Adding +``` +add [start/ end/] [# ...] [r/] [desc/] +``` + +Add event or deadline + +Examples: + + +- `add CS2103T Lecture start/2016-10-10 10:00 end/2016-10-10 12:00 r/weekly #rocks` + +- `add CS2105 Assignment 1 end/2016-10-10 10:00` + +``` +###### /UserGuide.md +``` md +#### Editing +``` +edit [title/new title] [start/ end/] [#...] [r/ ] [desc/] +``` + + Edit the entry with the specified entry id. + + `list` should be executed before this command to obtain a entry id. + +Examples: + +- `edit 3 school` + +- `edit 13 #yearly` + +``` +###### /UserGuide.md +``` md +#### option +``` +option [/ ...] +``` +Configure user settings: name, file path to data file + +Examples: + +- `config save/data/MyNewLocation.xml` + +``` diff --git a/collated/docs/A0127828W.md b/collated/docs/A0127828W.md new file mode 100644 index 000000000000..2021dcd21b51 --- /dev/null +++ b/collated/docs/A0127828W.md @@ -0,0 +1,186 @@ +# A0127828W +###### /DeveloperGuide.md +``` md +# Developer Guide + +* [Setting Up](#setting-up) +* [Design](#design) +* [Implementation](#implementation) +* [Testing](#testing) +* [Dev Ops](#dev-ops) +* [Appendix A: User Stories](#appendix-a--user-stories) +* [Appendix B: Use Cases](#appendix-b--use-cases) +* [Appendix C: Non Functional Requirements](#appendix-c--non-functional-requirements) +* [Appendix D: Glossary](#appendix-d--glossary) +* [Appendix E : Product Survey](#appendix-e-product-survey) + + +## Setting up + +#### Prerequisites + +1. **JDK `1.8.0_60`** or later
+ + > Having any Java 8 version is not enough.
+ This app will not work with earlier versions of Java 8. + +2. **Eclipse** IDE +3. **e(fx)clipse** plugin for Eclipse (Do the steps 2 onwards given in + [this page](http://www.eclipse.org/efxclipse/install.html#for-the-ambitious)) +4. **Buildship Gradle Integration** plugin from the Eclipse Marketplace + + +#### Importing the project into Eclipse + +0. Fork this repo, and clone the fork to your computer +1. Open Eclipse (Note: Ensure you have installed the **e(fx)clipse** and **buildship** plugins as given + in the prerequisites above) +2. Click `File` > `Import` +3. Click `Gradle` > `Gradle Project` > `Next` > `Next` +4. Click `Browse`, then locate the project's directory +5. Click `Finish` + + > * If you are asked whether to 'keep' or 'overwrite' config files, choose to 'keep'. + > * Depending on your connection speed and server load, it can even take up to 30 minutes for the set up to finish + (This is because Gradle downloads library files from servers during the project set up process) + > * If Eclipse auto-changed any settings files during the import process, you can discard those changes. + +## Design + +### 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. +Two of those classes play important roles at the architecture level. +* `EventsCentre` : 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. + +The rest of the App consists four components. +* [**`UI`**](#ui-component) : The UI of tha App. +* [**`Logic`**](#logic-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 four components +* Defines its _API_ in an `interface` with the same name as the Component. +* Exposes its functionality using a `{Component Name}Manager` class. + +For example, the `Logic` component (see the class diagram given below) defines it's API in the `Logic.java` +interface and exposes its functionality using the `LogicManager.java` class.
+
+ +The _Sequence Diagram_ below shows how the components interact for the scenario where the user issues the +command `delete 3`. + + + +>Note how the `Model` simply raises a `AddressBookChangedEvent` when the Address Book data are changed, + instead of asking the `Storage` to save the updates to the hard disk. + +The diagram below shows how the `EventsCenter` reacts to that event, which eventually results in the updates +being saved to the hard disk and the status bar of the UI being updated to reflect the 'Last Updated' time.
+ + +> Note how the event is propagated through the `EventsCenter` to the `Storage` and `UI` without `Model` having + to be coupled to either of them. This is an example of how this Event Driven approach helps us reduce direct + coupling between components. + +The sections below give more details of each component. + +### UI component + +
+ +**API** : [`Ui.java`](../src/main/java/seedu/address/ui/Ui.java) + +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, +`StatusBarFooter`, `BrowserPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class +and they can be loaded using the `UiPartLoader`. + +The `UI` component uses JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files + that are in the `src/main/resources/view` folder.
+ For example, the layout of the [`MainWindow`](../src/main/java/seedu/address/ui/MainWindow.java) is specified in + [`MainWindow.fxml`](../src/main/resources/view/MainWindow.fxml) + +The `UI` component, +* Executes user commands using the `Logic` component. +* Binds itself to some data in the `Model` so that the UI can auto-update when data in the `Model` change. +* Responds to events raised from various parts of the App and updates the UI accordingly. + +``` +###### /UserGuide.md +``` md + +## Command Summary + +| Command |Format | +| --- | --- | +|add|`add [start/ end/] [# ...] [r/] [desc/]`| +|list|`list [[keywords] [[after/] [before/] | [on/]][# ...] [recurrence=] [desc=]]`| [type/{entry, task}] +|tag|`tag # [# ...]`| +|untag|`untag # [# ...]`| +|edit|`edit [title/new title] [start/ end/] [#...] [r/ ] [desc/]`| +|delete|`delete `| +|mark|`mark `| +|unmark|`unmark `| +|undo|`undo`| +|redo|`redo`| +|help|`help []`| +|option|`option [/ ...]`| + +``` +###### /UserGuide.md +``` md +#### Listing +``` +list [[keywords] [[after/] [before/] | [on/]] [# ...] [recurrence=] [desc=]] [type/{entry, task}] +``` + +List all entries, or entries that satisfy the given search criteria. For the best user experience, completed entries are _automatically excluded_. + +Examples: + +- `list` + +- `list after/2016-10-10` + +- `list buy banana #groceries` + +If you want to include completed entries in your search, replace `list` with `list-all` + +- `list-all buy banana` + +``` +###### /UserGuide.md +``` md +#### Undo + +``` +undo +``` + +Undo the latest change to the todo list. Handles every changes, including `clear`. + +#### Redo + +``` +redo +``` + +Redo the latest command reverted with `undo`. + +``` +###### /UserGuide.md +``` md +## Misc + +You can use UP and DOWN keys to browse through your past commands in the session. +``` diff --git a/collated/main/A0116603R.md b/collated/main/A0116603R.md index 1cd10cc56d91..0fdc81f35fb7 100644 --- a/collated/main/A0116603R.md +++ b/collated/main/A0116603R.md @@ -1,6 +1,12 @@ # A0116603R ###### /java/seedu/address/commons/events/ui/DidMarkTaskEvent.java ``` java +/** + * An event indicating that the Logic component has completed + * executing the mark/unmark command after a user clicks on + * a checkbox in the GUI. This event is used to propagate feedback + * to the user. + */ public class DidMarkTaskEvent extends BaseEvent{ private CommandResult cmdResult; @@ -21,6 +27,10 @@ public class DidMarkTaskEvent extends BaseEvent{ ``` ###### /java/seedu/address/commons/events/ui/MarkTaskEvent.java ``` java +/** + * An event indicating that the user has clicked on a checkbox for a particular + * task in the GUI. + */ public class MarkTaskEvent extends BaseEvent { private final static String CHECKED = "checked"; @@ -50,8 +60,34 @@ public class MarkTaskEvent extends BaseEvent { } ``` +###### /java/seedu/address/commons/events/ui/WindowResizeEvent.java +``` java +/** + * An event indicating that the user has finished resizing the + * application window. + */ +public class WindowResizeEvent extends BaseEvent { + private Double newWidth; + + public WindowResizeEvent(Number newWidth) { + this.newWidth = (Double)newWidth; + } + + public Double getNewWidth() { + return newWidth; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } +} +``` ###### /java/seedu/address/model/task/Entry.java ``` java + /** + * Formats the Entry as text, showing all contact details. + */ public String getAsText() { final StringBuilder builder = new StringBuilder(); builder.append(getTitle()); @@ -82,10 +118,11 @@ public class MarkTaskEvent extends BaseEvent { return buffer.substring(0, buffer.length() - DELIMITER.length()); } } -} + ``` ###### /java/seedu/address/model/task/Event.java ``` java + @Override public String getAsText() { final StringBuilder builder = new StringBuilder(); builder.append(super.getAsText()); @@ -105,6 +142,7 @@ public class MarkTaskEvent extends BaseEvent { ``` ###### /java/seedu/address/model/task/Task.java ``` java + @Override public String getAsText() { final StringBuilder builder = new StringBuilder(); builder.append(super.getAsText()); @@ -120,7 +158,11 @@ public class MarkTaskEvent extends BaseEvent { ``` ###### /java/seedu/address/ui/AppViewController.java ``` java +/** + * Interaction point between Main Controller and rest of the app view(s) + */ public class AppViewController { + private static final Logger logger = LogsCenter.getLogger(AppViewController.class); private Logic logic; @@ -136,6 +178,7 @@ public class AppViewController { // ################ void init() { initChildViews(); + EventsCenter.getInstance().registerHandler(this); } private void initChildViews() { @@ -155,10 +198,50 @@ public class AppViewController { this.logic = logic; } + // ################# + // # EVENT HANDLER # + // ################# + @Subscribe + private void handleWindowResizeEvent(WindowResizeEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + if (event.getNewWidth().compareTo(LARGE_DISPLAY_WIDTH) <= 0) { + removeLargeStyleClass(); + } else { + addLargeStyleClass(); + } + } + + // ################## + // # HELPER METHODS # + // ################## + + /** + * Adds the LARGE_STYLE_CLASS to the root layout if the style class is not + * already added + */ + private void removeLargeStyleClass() { + if (rootLayout.getStyleClass().contains(LARGE_STYLE_CLASS)) { + Platform.runLater(() -> rootLayout.getStyleClass().remove(LARGE_STYLE_CLASS)); + + } + } + + /** + * Removes the LARGE_STYLE_CLASS from the root layout if the style class is + * present + */ + private void addLargeStyleClass() { + if (!rootLayout.getStyleClass().contains(LARGE_STYLE_CLASS)) { + Platform.runLater(() -> rootLayout.getStyleClass().add(LARGE_STYLE_CLASS)); + } + } } ``` ###### /java/seedu/address/ui/CommandArea.java ``` java +/** + * Represents the Command Area + */ public class CommandArea extends VBox { // #################### @@ -177,6 +260,8 @@ public class CommandArea extends VBox { private CommandResult mostRecentResult; + private CommandHistory commandHistoryManager; + // ######## // # FXML # // ######## @@ -194,6 +279,7 @@ public class CommandArea extends VBox { this.logic = logic; EventsCenter.getInstance().registerHandler(this); + commandHistoryManager = this.logic.getCommandHistoryManager(); } @FXML @@ -202,9 +288,51 @@ public class CommandArea extends VBox { statusLine.setText(Messages.STATUS_LINE_WELCOME); } +``` +###### /java/seedu/address/ui/CommandArea.java +``` java + @Subscribe + private void handleDidMarkTaskEvent(DidMarkTaskEvent event) { + statusLine.setText(event.getCommandResult().feedbackToUser); + } + + @Subscribe + private void handleFocusCommandLineEvent(FocusCommandLineEvent event) { + if (!cmdLine.isFocused()) { + cmdLine.requestFocus(); + } + } + + /** + * Sets the command box style to indicate a correct command. + */ + private void setStyleToIndicateCorrectCommand() { + cmdLine.getStyleClass().remove("error"); + cmdLine.setText(""); + } + + /** + * Sets the command box style to indicate an error + */ + private void setStyleToIndicateIncorrectCommand() { + cmdLine.getStyleClass().add("error"); + } + + /** + * Restores the command box text to the previously entered command + */ + private void restoreCommandText() { + cmdLine.setText(previousCommand); + } + +} ``` ###### /java/seedu/address/ui/Controller.java ``` java +/** + * Abstract class for all app view controllers. + * Forces controllers to implement an init method. + */ abstract class Controller { BorderPane appView; @@ -223,6 +351,7 @@ abstract class Controller { void show() { assert appView != null; + appView.setVisible(true); appView.toFront(); getFadeTransition(DEFAULT_FADE_DURATION, OPAQUE).play(); } @@ -231,6 +360,7 @@ abstract class Controller { assert appView != null; getFadeTransition(DEFAULT_FADE_DURATION, TRANSPARENT).play(); appView.toBack(); + appView.setVisible(false); } private FadeTransition getFadeTransition(double duration, double newValue) { @@ -242,6 +372,9 @@ abstract class Controller { ``` ###### /java/seedu/address/ui/EntryViewCell.java ``` java +/** + * Generic cell interface for displaying different types of Entries + */ public interface EntryViewCell { // Default factory method for generating a ListCell static Callback, ListCell> getFactory() { @@ -251,6 +384,9 @@ public interface EntryViewCell { ``` ###### /java/seedu/address/ui/HelpCard.java ``` java +/** + * Represents a help item which can be displayed. + */ public class HelpCard extends HBox { private static final String FXML = "HelpCard.fxml"; @@ -279,27 +415,34 @@ public class HelpCard extends HBox { ``` ###### /java/seedu/address/ui/HelpList.java ``` java +/** + * Represents a list of help cards to be displayed + */ public class HelpList extends ListView { private static final String FXML = "HelpList.fxml"; - private static final String ADD_HELP = "ADD"; - private static final String ADD_HELP_TEXT = "add [st/ end/ | dl/] [# ...] [desc/]"; - private static final String EDIT_HELP = "EDIT"; - private static final String EDIT_HELP_TEXT = "edit [title/new title] [st/ end/ | deadline/] [#...] [desc/]"; - private static final String ESCAPE_HELP = "CLOSE HELP"; - private static final String ESCAPE_HELP_TEXT = ""; - private static final String LIST_HELP = "LIST"; - private static final String LIST_HELP_TEXT = "list [[keywords] [[after/] [before/] | [on/]][# ...] [recurrence=] [desc=]]"; - private static final String TAG_HELP = "TAG"; - private static final String TAG_HELP_TEXT = "tag # [# ...]"; - private static final String UNTAG_HELP = "UNTAG"; - private static final String UNTAG_HELP_TEXT = "untag # [# ...]"; - private static final String DELETE_HELP = "DELETE"; - private static final String DELETE_HELP_TEXT = "delete "; - private static final String MARK_HELP = "MARK"; - private static final String MARK_HELP_TEXT = "mark "; - private static final String UNMARK_HELP = "UNMARK"; - private static final String UNMARK_HELP_TEXT = "unmark "; + private static final String[] COMMANDS = new String[]{ + "CLOSE HELP", + "ADD", "EDIT", + "DELETE", "LIST", + "LIST-ALL", + "TAG", "UNTAG", + "MARK", "UNMARK", + "UNDO", "OPTION"}; + + private static final String[] HELP_TEXT = new String[]{ + "", + "add [start/ end/] [# ...] [r/] [desc/]", + "edit [title/new title] [start/ end/] [#...] [r/ ] [desc/]", + "delete ", + "list [[keywords] [[after/] [before/] | [on/]][# ...] [recurrence=] [desc=] [type/{task, event}]]", + "list-all [[keywords] [[after/] [before/] | [on/]][# ...] [recurrence=] [desc=]]", + "tag # [# ...]", + "untag # [# ...]", + "mark ", + "unmark ", + "undo", + "option [/ ...]"}; private ObservableList data; @@ -319,15 +462,11 @@ public class HelpList extends ListView { private void initHelpItems() { data = FXCollections.observableArrayList(); - data.add(new HelpItem(ESCAPE_HELP, ESCAPE_HELP_TEXT)); - data.add(new HelpItem(ADD_HELP, ADD_HELP_TEXT)); - data.add(new HelpItem(EDIT_HELP, EDIT_HELP_TEXT)); - data.add(new HelpItem(LIST_HELP, LIST_HELP_TEXT)); - data.add(new HelpItem(TAG_HELP, TAG_HELP_TEXT)); - data.add(new HelpItem(UNTAG_HELP, UNTAG_HELP_TEXT)); - data.add(new HelpItem(DELETE_HELP, DELETE_HELP_TEXT)); - data.add(new HelpItem(MARK_HELP, MARK_HELP_TEXT)); - data.add(new HelpItem(UNMARK_HELP, UNMARK_HELP_TEXT)); + + assert (COMMANDS.length == HELP_TEXT.length); + for (int i=0; i { ``` ###### /java/seedu/address/ui/HelpViewCell.java ``` java +/** + * Custom ListCell which is displayed using a HelpCard + */ public class HelpViewCell extends ListCell implements EntryViewCell { static Callback, ListCell> getFactory() { @@ -374,6 +516,9 @@ public class HelpViewCell extends ListCell implements EntryVi ``` ###### /java/seedu/address/ui/HelpViewController.java ``` java +/** + * Controller which displays the help page + */ public class HelpViewController extends Controller { private static final Logger logger = LogsCenter.getLogger(HelpViewController.class); @@ -410,6 +555,10 @@ public class HelpViewController extends Controller { ``` ###### /java/seedu/address/ui/MainController.java ``` java +/** + * The Main Controller, which is in charge of the root layout and + * enables other UI elements to be placed within the application. + */ public class MainController extends UiPart { // ############# @@ -417,7 +566,7 @@ public class MainController extends UiPart { // ############# private static final String FXML = "RootLayout.fxml"; private static final String CSS_SOURCE = "/view/PriorityQTheme.css"; - private static final int MIN_HEIGHT = 760; + private static final int MIN_HEIGHT = 520; private static final int MIN_WIDTH = 520; private static final Logger logger = LogsCenter.getLogger(MainController.class); @@ -473,7 +622,6 @@ public class MainController extends UiPart { assert rootLayout != null; logger.info("Initialising Scene..."); scene = new Scene(rootLayout); - configureScene(); } @@ -482,7 +630,8 @@ public class MainController extends UiPart { URL url = this.getClass().getResource(CSS_SOURCE); String css = url.toExternalForm(); scene.getStylesheets().add(css); - addEscapeHandlerForScene(); + addEscapeFilterForScene(); + scene.widthProperty().addListener(getWindowResizeEventListener()); } /** @@ -490,7 +639,7 @@ public class MainController extends UiPart { * A filter is used instead of a handler since the ListView or a particular * JavaFX node consumes the event only in the case of an `ESCAPE` key. */ - private void addEscapeHandlerForScene() { + private void addEscapeFilterForScene() { assert scene != null; scene.addEventFilter(KeyEvent.KEY_RELEASED, new EventHandler() { @Override @@ -560,12 +709,14 @@ public class MainController extends UiPart { return new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), (int) primaryStage.getX(), (int) primaryStage.getY()); } - } ``` ###### /java/seedu/address/ui/TaskCard.java ``` java -public class TaskCard extends HBox { +/** + * Represents Tasks and Events in the TaskList + */ +public class TaskCard extends VBox { private static final String FXML = "TaskCard.fxml"; // ######## @@ -619,7 +770,6 @@ public class TaskCard extends HBox { this.entry = entry; this.index = index; this.listener = listener; - this.checkBox.selectedProperty().bindBidirectional(entry.isMarkedProperty()); initData(); } @@ -641,8 +791,16 @@ public class TaskCard extends HBox { private void initCommonElements() { title.setText(entry.getTitle().fullTitle); id.setText(Integer.toString(index)); - tags.setText(entry.tagsString()); - description.setText(entry.getDescription()); + if (entry.tagsString().isEmpty()) { + hide(tags); + } else { + tags.setText(entry.tagsString()); + } + if (entry.getDescription() == null || entry.getDescription().isEmpty()) { + hide(description); + } else { + description.setText(entry.getDescription()); + } } private void initTaskSpecificElements(Task task) { @@ -650,19 +808,40 @@ public class TaskCard extends HBox { if (task.getDeadline() == null) { deadline.setOpacity(TRANSPARENT); + styleTask(getTaskStyling(task.isMarked())); return; } deadline.setText(task.getDeadlineDisplay().toUpperCase()); - String additionalStyleClass = getDeadlineStyling(task.getDeadline()); - if (!additionalStyleClass.isEmpty()) { - deadline.getStyleClass().add(additionalStyleClass); - } + styleDeadline(getDeadlineStyling(task.isMarked(), task.getDeadline())); } private void initCheckbox(Task task) { - checkBox.selectedProperty().addListener(listener); checkBox.setSelected(task.isMarked()); + checkBox.selectedProperty().addListener(listener); + } + + private void styleTask(String styleClass) { + if (!styleClass.isEmpty()) { + addStyleClass(styleClass, getTaskElements()); + } + } + + private void addStyleClass(String styleClass, Styleable... nodes) { + for (Styleable node : nodes) { + node.getStyleClass().add(styleClass); + } + } + + private Styleable[] getTaskElements() { + return new Styleable[]{id, title, description}; + } + + private void styleDeadline(String styleClass) { + if (!styleClass.isEmpty()) { + styleTask(styleClass); + addStyleClass(styleClass, deadline); + } } private void hideEventSpecificElements() { @@ -675,11 +854,13 @@ public class TaskCard extends HBox { separator.setText(EVENT_DATE_SEPARATOR); endTime.setText(event.getEndTimeDisplay().toUpperCase()); - String additionalStyleClass = getEventStyling(event.getStartTime(), event.getEndTime()); - if (!additionalStyleClass.isEmpty()) { - startTime.getStyleClass().add(additionalStyleClass); - separator.getStyleClass().add(additionalStyleClass); - endTime.getStyleClass().add(additionalStyleClass); + styleEvent(getEventStyling(event.getStartTime(), event.getEndTime())); + } + + private void styleEvent(String styleClass) { + addStyleClass(EVENT_DESCRIPTION_STYLE_CLASS, description); + if (!styleClass.isEmpty()) { + addStyleClass(styleClass, startTime, separator, endTime, id, title, description); } } @@ -688,21 +869,25 @@ public class TaskCard extends HBox { hide(deadline, checkBox); } - private void hide(Node... nodes) { - for (Node node : nodes) { - node.setOpacity(TRANSPARENT); - } - } - private void setEmptyText(Label... labels) { for (Label label : labels) { label.setText(""); } } + + private void hide(Node... nodes) { + for (Node node : nodes) { + node.managedProperty().bind(node.visibleProperty()); + node.setVisible(false); + } + } } ``` ###### /java/seedu/address/ui/TaskList.java ``` java +/** + * Represents a list of tasks to be displayed + */ public class TaskList extends ListView { private static String FXML = "TaskList.fxml"; @@ -725,6 +910,9 @@ public class TaskList extends ListView { ``` ###### /java/seedu/address/ui/TaskViewCell.java ``` java +/** + * Custom ListCell which is displayed using a TaskCard + */ public class TaskViewCell extends ListCell implements EntryViewCell { static Callback, ListCell> getFactory() { @@ -750,6 +938,10 @@ public class TaskViewCell extends ListCell implements EntryViewCell { ``` ###### /java/seedu/address/ui/TaskViewController.java ``` java +/** + * Controller which initializes the default Task View -- this consists of + * a TaskList and a CommandArea + */ public class TaskViewController extends Controller { private static final Logger logger = LogsCenter.getLogger(TaskViewController.class); @@ -777,6 +969,7 @@ public class TaskViewController extends Controller { appView.setCenter(taskList); appView.setBottom(new CommandArea(logic)); appView.toFront(); + addEnterHandlerForScene(); } // ################### @@ -788,6 +981,25 @@ public class TaskViewController extends Controller { return logic.getFilteredPersonList(); } + /** + * Register an event handler to enable focusing on the command + * line if the user presses the key. + */ + private void addEnterHandlerForScene() { + assert appView != null; + appView.addEventHandler(KeyEvent.KEY_RELEASED, new EventHandler() { + @Override + public void handle(KeyEvent event) { + if (event.getCode() == KeyCode.ENTER) { + EventsCenter.getInstance().post(new FocusCommandLineEvent()); + } + } + }); + } + + // ################## + // # EVENT HANDLERS # + // ################## @Subscribe private void handleShowHelpEvent(ShowHelpListEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); @@ -804,6 +1016,9 @@ public class TaskViewController extends Controller { ``` ###### /java/seedu/address/ui/util/GuiUtil.java ``` java +/** + * Helper class which returns event handlers for user-initiated events + */ public class GuiUtil { public static double OPAQUE = 1.0; @@ -811,36 +1026,21 @@ public class GuiUtil { public static double DEFAULT_FADE_DURATION = 400; + public static Double LARGE_DISPLAY_WIDTH = Double.valueOf(768); + public static String LARGE_STYLE_CLASS = "large"; + public static final String EVENT_DATE_SEPARATOR = " - "; + public static final String EVENT_DESCRIPTION_STYLE_CLASS = "event"; + private static String PAST_STYLE_CLASS = "past"; private static String ACTIVE_STYLE_CLASS = "present"; + private static String OVERDUE_STYLE_CLASS = "overdue"; public static ChangeListener getCheckBoxEventListener(int idx) { return (ov, old_val, new_val) -> EventsCenter.getInstance().post(new MarkTaskEvent(idx, new_val)); } - public static String getDeadlineStyling(LocalDateTime dateTime) { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime midnight = LocalDateTime.of(now.toLocalDate().plusDays(1), LocalTime.MIDNIGHT); - if (dateTime.isBefore(now)) { - return PAST_STYLE_CLASS; - } else if (dateTime.isAfter(now) && dateTime.isBefore(midnight)) { - return ACTIVE_STYLE_CLASS; - } - return ""; - } - - public static String getEventStyling(LocalDateTime startTime, LocalDateTime endTime) { - LocalDateTime now = LocalDateTime.now(); - if (endTime.isBefore(now)) { - return PAST_STYLE_CLASS; - } else if (now.isAfter(startTime) && now.isBefore(endTime)) { - return ACTIVE_STYLE_CLASS; - } - return ""; - } -} ``` ###### /resources/view/PriorityQTheme.css ``` css @@ -873,6 +1073,10 @@ public class GuiUtil { -fx-padding: 10; } +.large .label { + -fx-font-size: 12pt; +} + .list-view { -fx-border-width: 0; -fx-background-insets: 0; @@ -909,10 +1113,30 @@ public class GuiUtil { -fx-padding: 0 0 4 4; } +.large .taskcard #tags { + -fx-font-size: 11pt; +} + .taskcard #description { -fx-font-size: 9pt; -fx-text-fill: DarkGrey; - -fx-padding: 0 0 0 40; + -fx-padding: -6 0 0 26; +} + +.large .taskcard #description { + -fx-font-size: 11pt; +} + +.taskcard #description.event { + -fx-padding: -6 0 0 0; +} + +.taskcard .label.past, #description.past { + -fx-text-fill: LightGrey; +} + +.taskcard .label.overdue, #description.overdue { + -fx-text-fill: OrangeRed; } .taskcard #deadline { @@ -928,11 +1152,19 @@ public class GuiUtil { -fx-text-fill: DarkTurquoise; } +.large .taskcard .date { + -fx-font-size: 9pt; +} + .taskcard .date.present#deadline { -fx-background-color: YellowGreen; } .taskcard .date.past#deadline { + -fx-background-color: LightGrey; +} + +.taskcard .date.overdue#deadline { -fx-background-color: OrangeRed; } diff --git a/collated/main/A0116603Rreused.md b/collated/main/A0116603Rreused.md new file mode 100644 index 000000000000..891934643361 --- /dev/null +++ b/collated/main/A0116603Rreused.md @@ -0,0 +1,85 @@ +# A0116603Rreused +###### /java/seedu/address/ui/util/GuiUtil.java +``` java + public static ChangeListener getWindowResizeEventListener() { + return new ChangeListener() { + final Timer timer = new Timer(); + TimerTask task = null; + final long delayTime = 200; + + @Override + public void changed(ObservableValue observable, Number oldValue, final Number newValue) { + if (task != null) task.cancel(); + task = new TimerTask() { + @Override + public void run() { + EventsCenter.getInstance().post(new WindowResizeEvent(newValue)); + } + }; + timer.schedule(task, delayTime); + } + }; + } + + /** + * Get the style class for floating task elements such as id, title and + * description. + * @param isMarked true, if the current task is marked as done + * @return a string, the style class for the floating task + */ + public static String getTaskStyling(boolean isMarked) { + return getDeadlineStyling(isMarked, null); + } + + /** + * Get the style class for deadlines. The style differs depending on whether the + * task is marked as done. If it is not done, it differs depending on whether it + * is overdue. If it is not overdue, it differs depending on whether the deadline + * is due by the end of the current day. + * @param isMarked true, if the current task is marked as done + * @param deadline a datetime which will be compared to the current time to + * determine the style class + * @return a string, the style class for the deadline + */ + public static String getDeadlineStyling(boolean isMarked, LocalDateTime deadline) { + if (isMarked) { + return PAST_STYLE_CLASS; + } + + if (deadline == null) { + return ""; + } + + LocalDateTime now = LocalDateTime.now(); + LocalDateTime midnight = LocalDateTime.of(now.toLocalDate().plusDays(1), LocalTime.MIDNIGHT); + + if (deadline.isBefore(now)) { + return OVERDUE_STYLE_CLASS; + } + + if (deadline.isAfter(now) && deadline.isBefore(midnight)) { + return ACTIVE_STYLE_CLASS; + } + return ""; + } + + /** + * Get the style class for events. The style differs depending on whether the + * event is over. If it is not over, but currently ongoing, a different style + * class is also applied. + * @param startTime a datetime, the start of the event + * @param endTime a datetime, the end of the event + * @return a string, the style class for the event + */ + public static String getEventStyling(LocalDateTime startTime, LocalDateTime endTime) { + assert (startTime != null && endTime != null); + LocalDateTime now = LocalDateTime.now(); + if (endTime.isBefore(now)) { + return PAST_STYLE_CLASS; + } else if (now.isAfter(startTime) && now.isBefore(endTime)) { + return ACTIVE_STYLE_CLASS; + } + return ""; + } +} +``` diff --git a/collated/main/A0121501E.md b/collated/main/A0121501E.md index 5444ad607084..ed0e49bec2d9 100644 --- a/collated/main/A0121501E.md +++ b/collated/main/A0121501E.md @@ -3,7 +3,7 @@ ``` java @Override public CommandResult unexecute() { - if (!executionIsSuccessful){ + if (getCommandState()!=CommandState.UNDOABLE){ return new CommandResult(MESSAGE_UNDO_FAIL); }; assert toAdd != null; @@ -12,20 +12,21 @@ } catch (EntryNotFoundException enfe) { assert false : "The target entry cannot be missing"; } + setRedoable(); return new CommandResult(String.format(MESSAGE_UNDO_SUCCESS, toAdd)); } - } ``` ###### /java/seedu/address/logic/commands/ClearCommand.java ``` java @Override public CommandResult unexecute() { - if (!executionIsSuccessful){ + if (getCommandState() != CommandState.UNDOABLE){ return new CommandResult(MESSAGE_UNDO_FAIL); } assert model != null; model.resetData(originalTaskManager); + setRedoable(); return new CommandResult(MESSAGE_UNDO_SUCCESS); } } @@ -33,25 +34,25 @@ ###### /java/seedu/address/logic/commands/DeleteCommand.java ``` java public CommandResult unexecute() { - if (!executionIsSuccessful){ + if (getCommandState()!=CommandState.UNDOABLE){ return new CommandResult(MESSAGE_UNDO_FAIL); }; assert model != null; assert entryToDelete != null; try { model.addTask(entryToDelete); + setRedoable(); return new CommandResult(String.format(MESSAGE_UNDO_DELETE_PERSON_SUCCESS, entryToDelete)); } catch (DuplicateTaskException e) { return new CommandResult(MESSAGE_DUPLICATE_ENTRY); } } - } ``` ###### /java/seedu/address/logic/commands/EditCommand.java ``` java public CommandResult unexecute() { - if (!executionIsSuccessful){ + if (getCommandState()!=CommandState.UNDOABLE){ return new CommandResult(MESSAGE_UNDO_FAIL); }; assert model != null; @@ -59,6 +60,7 @@ try { model.editTask(reverseUpdate); + model.updateLastModifiedTime(taskToEdit, originalLastModifiedTime); } catch (EntryNotFoundException e) { assert false : "The target entry cannot be missing"; } catch (DuplicateTaskException e) { @@ -66,13 +68,16 @@ } catch (EntryConversionException e) { assert false: "Undo shouldn't convert Task to Event and vice versa"; } + setRedoable(); return new CommandResult(String.format(MESSAGE_UNDO_SUCCESS, taskToEdit)); } - } ``` ###### /java/seedu/address/logic/commands/MarkCommand.java ``` java +/** + * Marks an entry as completed. + */ public class MarkCommand extends UndoableCommand { public static final String COMMAND_WORD = "mark"; @@ -87,6 +92,7 @@ public class MarkCommand extends UndoableCommand { private final int targetIndex; private Entry entryToMark; + private LocalDateTime originalLastModifiedTime; private boolean originalIsMarked; public MarkCommand(int targetIndex) { @@ -96,39 +102,43 @@ public class MarkCommand extends UndoableCommand { @Override public CommandResult execute() { assert model != null; - UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); - - if (lastShownList.size() < targetIndex) { - indicateAttemptToExecuteIncorrectCommand(); - return new CommandResult(Messages.MESSAGE_INVALID_ENTRY_DISPLAYED_INDEX); + if (getCommandState()==CommandState.PRE_EXECUTION) { + UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); + + if (lastShownList.size() < targetIndex) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_ENTRY_DISPLAYED_INDEX); + } + + entryToMark = lastShownList.get(targetIndex - 1); + originalLastModifiedTime = entryToMark.getLastModifiedTime(); + originalIsMarked = entryToMark.isMarked(); } - - entryToMark = lastShownList.get(targetIndex - 1); - originalIsMarked = entryToMark.isMarked(); + try { model.markTask(entryToMark); } catch (EntryNotFoundException pnfe) { assert false : "The target entry cannot be missing"; } - setExecutionIsSuccessful(); + setUndoable(); return new CommandResult(String.format(MESSAGE_SUCCESS, entryToMark)); } @Override public CommandResult unexecute() { - if (!executionIsSuccessful){ + if (getCommandState() != CommandState.UNDOABLE){ return new CommandResult(MESSAGE_UNDO_FAIL); - }; + } assert model != null; assert entryToMark != null; try { - if (originalIsMarked) { - model.markTask(entryToMark); - } else { + if (!originalIsMarked) { model.unmarkTask(entryToMark); } + model.updateLastModifiedTime(entryToMark, originalLastModifiedTime); + setRedoable(); } catch (EntryNotFoundException enfe) { assert false : "The target entry cannot be missing"; } @@ -137,8 +147,44 @@ public class MarkCommand extends UndoableCommand { } } ``` +###### /java/seedu/address/logic/commands/RedoCommand.java +``` java +/** + * Redo the previous undo command. + */ +public class RedoCommand extends Command { + + public static final String COMMAND_WORD = "redo"; + public static final String MESSAGE_FAILURE = "No redoable commands found!"; + private UndoableCommandHistory undoableCommandHistory; + + public RedoCommand() {} + + public void setData(Model model, UndoableCommandHistory undoableCommandQueue) { + this.model = model; + this.undoableCommandHistory = undoableCommandQueue; + } + + @Override + public CommandResult execute() { + try { + UndoableCommand undoableCommand = undoableCommandHistory.getFromRedoStack(); + CommandResult undoableCommandResult = undoableCommand.reExecute(); + if (undoableCommand.getCommandState() == CommandState.UNDOABLE) { + undoableCommandHistory.pushToUndoStack(undoableCommand); + } + return undoableCommandResult; + } catch (UndoableCommandNotFoundException e) { + return new CommandResult(MESSAGE_FAILURE); + } + } +} +``` ###### /java/seedu/address/logic/commands/TagCommand.java ``` java +/* + * Add tags to an entry. Does not overwrite existing tags. + */ public class TagCommand extends UndoableCommand { public static final String COMMAND_WORD = "tag"; @@ -146,15 +192,17 @@ public class TagCommand extends UndoableCommand { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds tags to task. " + "Parameters: TASK_ID TAG[,...] " - + "Example: " + COMMAND_WORD + " 2 shopping, food"; + + "Example: " + COMMAND_WORD + " 2 #shopping #food"; - public static final String MESSAGE_SUCCESS = "Tagged entry: %1$s"; - public static final String MESSAGE_UNDO_SUCCESS = "Undo tag entry: %1$s"; + public static final String MESSAGE_SUCCESS = "Add %1$s to entry: %2$s"; + public static final String MESSAGE_UNDO_SUCCESS = "Undo add %1$s to entry: %2$s"; + public static final String MESSAGE_ALREADY_EXISTS = "All specified tags already exist on entry: %1$s"; private final int targetIndex; private final UniqueTagList tagsToAdd; private Entry taskToTag; + private LocalDateTime originalLastModifiedTime; public TagCommand(int targetIndex, Set tags) throws IllegalValueException { this.targetIndex = targetIndex; @@ -169,81 +217,138 @@ public class TagCommand extends UndoableCommand { @Override public CommandResult execute() { assert model != null; - UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); - - if (lastShownList.size() < targetIndex) { + assert !tagsToAdd.isEmpty(); //should be handled in the parser + if (getCommandState()==CommandState.PRE_EXECUTION) { + UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); + + if (lastShownList.size() < targetIndex) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_ENTRY_DISPLAYED_INDEX); + } + + taskToTag = lastShownList.get(targetIndex - 1); + originalLastModifiedTime = taskToTag.getLastModifiedTime(); + tagsToAdd.removeFrom(taskToTag.getTags()); + } + + if (tagsToAdd.isEmpty()){ indicateAttemptToExecuteIncorrectCommand(); - return new CommandResult(Messages.MESSAGE_INVALID_ENTRY_DISPLAYED_INDEX); + return new CommandResult(String.format(MESSAGE_ALREADY_EXISTS, taskToTag)); } - - taskToTag = lastShownList.get(targetIndex - 1); - tagsToAdd.removeFrom(taskToTag.getTags()); - try { model.tagTask(taskToTag, tagsToAdd); } catch (EntryNotFoundException e) { assert false : "The target entry cannot be missing"; } - setExecutionIsSuccessful(); - return new CommandResult(String.format(MESSAGE_SUCCESS, taskToTag)); + setUndoable(); + return new CommandResult(String.format(MESSAGE_SUCCESS, tagsToAdd, taskToTag)); } @Override public CommandResult unexecute() { - if (!executionIsSuccessful){ + if (getCommandState() != CommandState.UNDOABLE){ return new CommandResult(MESSAGE_UNDO_FAIL); - }; + } assert model != null; assert taskToTag != null; assert tagsToAdd != null; try { model.untagTask(taskToTag, tagsToAdd); + model.updateLastModifiedTime(taskToTag, originalLastModifiedTime); } catch (EntryNotFoundException enfe) { assert false : "The target entry cannot be missing"; } - return new CommandResult(String.format(MESSAGE_UNDO_SUCCESS, taskToTag)); + setRedoable(); + return new CommandResult(String.format(MESSAGE_UNDO_SUCCESS, tagsToAdd, taskToTag)); } } ``` ###### /java/seedu/address/logic/commands/UndoableCommand.java ``` java +/** + * Represents a command which changes to the model can be undone using the undo command + * and redone using the redo command + */ public abstract class UndoableCommand extends Command { - public String MESSAGE_UNDO_FAIL = "Command cannot be undone before it is successfully executed!"; - protected boolean executionIsSuccessful=false; + public enum CommandState {UNDOABLE, REDOABLE, PRE_EXECUTION}; + + public String MESSAGE_UNDO_FAIL = "Command cannot be undone before it is successfully executed!"; + public String MESSAGE_REDO_FAIL = "Command cannot be redone before it is successfully unexecuted!"; + public CommandState commandState = CommandState.PRE_EXECUTION; public abstract CommandResult unexecute(); - public void setExecutionIsSuccessful() { - executionIsSuccessful=true; + /** + * Re-executes the command which was previously undone. + */ + public CommandResult reExecute() { + if (getCommandState()!=CommandState.REDOABLE){ + return new CommandResult(MESSAGE_UNDO_FAIL); + }; + return execute(); + } + + public void setUndoable() { + commandState = CommandState.UNDOABLE; } - public boolean getExecutionIsSuccessful() { - return executionIsSuccessful; + public void setRedoable() { + commandState = CommandState.REDOABLE; + } + + public CommandState getCommandState() { + return commandState; } } ``` ###### /java/seedu/address/logic/commands/UndoableCommandHistory.java ``` java +/** + * Stack of successfully executed undoable commands. + */ public class UndoableCommandHistory { - Deque commandInternalQueue = new ArrayDeque(); + Deque commandInternalUndoQueue = new ArrayDeque(); + Deque commandInternalRedoQueue = new ArrayDeque(); public static class UndoableCommandNotFoundException extends Exception {}; - public void push(UndoableCommand undoableCommand) { - commandInternalQueue.addFirst(undoableCommand); + /** Push a new Undoable command to history. + * Resets the redo queue since the redo commands are no longer in sync. + */ + public void pushToHistory(UndoableCommand undoableCommand) { + commandInternalRedoQueue = new ArrayDeque(); + commandInternalUndoQueue.addFirst(undoableCommand); + } + + public void pushToRedoStack(UndoableCommand undoableCommand) { + commandInternalRedoQueue.addFirst(undoableCommand); + } + + public void pushToUndoStack(UndoableCommand undoableCommand) { + commandInternalUndoQueue.addFirst(undoableCommand); + } + + public UndoableCommand getFromUndoStack() throws UndoableCommandNotFoundException { + if (commandInternalUndoQueue.isEmpty()){ + throw new UndoableCommandNotFoundException(); + } + return commandInternalUndoQueue.poll(); } - public UndoableCommand getMostRecentUndoableCommand() throws UndoableCommandNotFoundException { - if (commandInternalQueue.isEmpty()){ + public UndoableCommand getFromRedoStack() throws UndoableCommandNotFoundException { + if (commandInternalRedoQueue.isEmpty()){ throw new UndoableCommandNotFoundException(); } - return commandInternalQueue.poll(); + return commandInternalRedoQueue.poll(); } } ``` ###### /java/seedu/address/logic/commands/UndoCommand.java ``` java +/** + * Undo the previous undoable command. + */ public class UndoCommand extends Command { public static final String COMMAND_WORD = "undo"; @@ -260,8 +365,12 @@ public class UndoCommand extends Command { @Override public CommandResult execute() { try { - UndoableCommand undoableCommand = undoableCommandHistory.getMostRecentUndoableCommand(); - return undoableCommand.unexecute(); + UndoableCommand undoableCommand = undoableCommandHistory.getFromUndoStack(); + CommandResult undoableCommandResult = undoableCommand.unexecute(); + if (undoableCommand.getCommandState() == CommandState.REDOABLE) { + undoableCommandHistory.pushToRedoStack(undoableCommand); + } + return undoableCommandResult; } catch (UndoableCommandNotFoundException e) { return new CommandResult(MESSAGE_FAILURE); } @@ -270,11 +379,14 @@ public class UndoCommand extends Command { ``` ###### /java/seedu/address/logic/commands/UnmarkCommand.java ``` java +/** + * Unmarks an entry. + */ public class UnmarkCommand extends UndoableCommand { public static final String COMMAND_WORD = "unmark"; public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Unmarks the entry as completed. " + + ": Unmarks the entry. " + "Identified by the index number used in the last entry listing.\n" + "Parameters: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; @@ -285,39 +397,42 @@ public class UnmarkCommand extends UndoableCommand { private final int targetIndex; private Entry entryToUnmark; private boolean originalIsMarked; + private LocalDateTime originalLastModifiedTime; public UnmarkCommand(int targetIndex) { this.targetIndex = targetIndex; } - @Override public CommandResult execute() { - - UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); - - if (lastShownList.size() < targetIndex) { - indicateAttemptToExecuteIncorrectCommand(); - return new CommandResult(Messages.MESSAGE_INVALID_ENTRY_DISPLAYED_INDEX); + assert model != null; + if (getCommandState()==CommandState.PRE_EXECUTION) { + UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); + + if (lastShownList.size() < targetIndex) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_ENTRY_DISPLAYED_INDEX); + } + + entryToUnmark = lastShownList.get(targetIndex - 1); + originalLastModifiedTime = entryToUnmark.getLastModifiedTime(); + originalIsMarked= entryToUnmark.isMarked(); } - entryToUnmark = lastShownList.get(targetIndex - 1); - originalIsMarked= entryToUnmark.isMarked(); - try { model.unmarkTask(entryToUnmark); } catch (EntryNotFoundException pnfe) { assert false : "The target entry cannot be missing"; } - setExecutionIsSuccessful(); + setUndoable(); return new CommandResult(String.format(MESSAGE_SUCCESS, entryToUnmark)); } @Override public CommandResult unexecute() { - if (!executionIsSuccessful){ + if (getCommandState() != CommandState.UNDOABLE){ return new CommandResult(MESSAGE_UNDO_FAIL); - }; + } assert model != null; assert entryToUnmark != null; @@ -327,16 +442,20 @@ public class UnmarkCommand extends UndoableCommand { } else { model.unmarkTask(entryToUnmark); } + model.updateLastModifiedTime(entryToUnmark, originalLastModifiedTime); } catch (EntryNotFoundException enfe) { assert false : "The target entry cannot be missing"; } - + setRedoable(); return new CommandResult(String.format(MESSAGE_UNDO_SUCCESS, entryToUnmark)); } } ``` ###### /java/seedu/address/logic/commands/UntagCommand.java ``` java +/* + * Remove tags from an entry. + */ public class UntagCommand extends UndoableCommand { public static final String COMMAND_WORD = "untag"; @@ -344,13 +463,15 @@ public class UntagCommand extends UndoableCommand { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Removes tags from task. " + "Parameters: TASK_ID TAG[,...] " - + "Example: " + COMMAND_WORD + " 2 shopping, food"; + + "Example: " + COMMAND_WORD + " 2 #shopping #food"; - public static final String MESSAGE_SUCCESS = "Removed tags from entry: %1$s"; - public static final String MESSAGE_UNDO_SUCCESS = "Undo remove tags from entry: %1$s"; + public static final String MESSAGE_SUCCESS = "Removed %1$s from entry: %2$s"; + public static final String MESSAGE_UNDO_SUCCESS = "Undo remove %1$s from entry: %2$s"; + public static final String MESSAGE_NON_EXISTENT = "None of the specified tags exist in the entry: %1$s"; private final int targetIndex; private Entry taskToUntag; + private LocalDateTime originalLastModifiedTime; private final UniqueTagList tagsToRemove; @@ -365,41 +486,52 @@ public class UntagCommand extends UndoableCommand { } @Override - public CommandResult execute() { - UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); - - if (lastShownList.size() < targetIndex) { - indicateAttemptToExecuteIncorrectCommand(); - return new CommandResult(Messages.MESSAGE_INVALID_ENTRY_DISPLAYED_INDEX); + public CommandResult execute() { + assert model != null; + assert !tagsToRemove.isEmpty(); //should be handled in the parser + if (getCommandState()==CommandState.PRE_EXECUTION) { + UnmodifiableObservableList lastShownList = model.getFilteredPersonList(); + + if (lastShownList.size() < targetIndex) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(Messages.MESSAGE_INVALID_ENTRY_DISPLAYED_INDEX); + } + + taskToUntag = lastShownList.get(targetIndex - 1); + originalLastModifiedTime = taskToUntag.getLastModifiedTime(); + tagsToRemove.retainAll(taskToUntag.getTags()); } - - taskToUntag = lastShownList.get(targetIndex - 1); - tagsToRemove.retainAll(taskToUntag.getTags()); + if (tagsToRemove.isEmpty()){ + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(String.format(MESSAGE_NON_EXISTENT, taskToUntag)); + } try { model.untagTask(taskToUntag, tagsToRemove); } catch (EntryNotFoundException e) { assert false : "The target entry cannot be missing"; } - setExecutionIsSuccessful(); - return new CommandResult(String.format(MESSAGE_SUCCESS, taskToUntag)); + setUndoable(); + return new CommandResult(String.format(MESSAGE_SUCCESS, tagsToRemove, taskToUntag)); } @Override public CommandResult unexecute() { - if (!executionIsSuccessful){ + if (getCommandState() != CommandState.UNDOABLE){ return new CommandResult(MESSAGE_UNDO_FAIL); - }; + } assert model != null; assert taskToUntag != null; assert tagsToRemove != null; try { model.tagTask(taskToUntag, tagsToRemove); + model.updateLastModifiedTime(taskToUntag, originalLastModifiedTime); } catch (EntryNotFoundException enfe) { assert false : "The target entry cannot be missing"; } - return new CommandResult(String.format(MESSAGE_UNDO_SUCCESS, taskToUntag)); + setRedoable(); + return new CommandResult(String.format(MESSAGE_UNDO_SUCCESS, tagsToRemove, taskToUntag)); } } @@ -409,13 +541,58 @@ public class UntagCommand extends UndoableCommand { else if (command instanceof UndoCommand) { ((UndoCommand) command).setData(model, undoableCommandHistory); } + else if (command instanceof RedoCommand) { + ((RedoCommand) command).setData(model, undoableCommandHistory); + } else { command.setData(model); } CommandResult commandResult = command.execute(); if (command instanceof UndoableCommand && - ((UndoableCommand) command).getExecutionIsSuccessful()) { - undoableCommandHistory.push((UndoableCommand) command); + ((UndoableCommand) command).getCommandState() == CommandState.UNDOABLE) { + undoableCommandHistory.pushToHistory((UndoableCommand) command); } + + commandHistoryManager.appendCommand(commandText); + commandHistoryManager.resetPosition(); + +``` +###### /java/seedu/address/model/task/EntryViewComparator.java +``` java +/** + * Comparator for the list view. + */ +public class EntryViewComparator implements Comparator{ + @Override + public int compare(Entry o1, Entry o2) { + LocalDateTime thisDateTime = o1.getComparableTime(); + LocalDateTime oDateTime = o2.getComparableTime(); + int compareValue = thisDateTime.compareTo(oDateTime); + if (compareValue==0) { + return o1.getTitle().toString().compareTo(o2.getTitle().toString()); + } + return thisDateTime.compareTo(oDateTime); + } + +} +``` +###### /java/seedu/address/model/task/Event.java +``` java + @Override + public LocalDateTime getComparableTime() { + return startTime.get(); + } +``` +###### /java/seedu/address/model/task/Task.java +``` java + @Override + public LocalDateTime getComparableTime() { + if (isFloatingTask()) { + return lastModifiedTime.get(); + } + return deadline.get(); + } + +} ``` diff --git a/collated/main/A0126539Y.md b/collated/main/A0126539Y.md index f874a60d7856..f06be20eaa69 100644 --- a/collated/main/A0126539Y.md +++ b/collated/main/A0126539Y.md @@ -42,7 +42,7 @@ * @throws IllegalValueException if any of the raw values are invalid */ public AddCommand(String title, LocalDateTime startTime, LocalDateTime endTime, Set tags, String description) - throws IllegalValueException { + throws IllegalValueException, IllegalArgumentException { final Set tagSet = new HashSet<>(); for (String tagName : tags) { tagSet.add(new Tag(tagName)); @@ -55,7 +55,8 @@ endTime, new UniqueTagList(tagSet), false, - description + description, + LocalDateTime.now() ); } else if (startTime == null){ @@ -64,7 +65,8 @@ endTime, new UniqueTagList(tagSet), false, - description + description, + LocalDateTime.now() ); } else if (startTime != null && endTime == null) { @@ -163,6 +165,19 @@ public class OptionCommand extends Command{ } ``` ###### /java/seedu/address/model/PredicateBuilder.java +``` java + private Predicate addTypePredicateIfExist(Predicate pred, String entryType) throws IllegalValueException { + Predicate result = pred; + if (entryType != null && !entryType.isEmpty()) { + if (!entryType.equalsIgnoreCase(TypeQualifier.EVENT_TYPE_STRING) && !entryType.equalsIgnoreCase(TypeQualifier.TASK_TYPE_STRING)) { + throw new IllegalValueException(TypeQualifier.INVALID_TYPE_MESSAGE); + } + result = result.and(buildTypePredicate(entryType)); + } + return result; + } +``` +###### /java/seedu/address/model/PredicateBuilder.java ``` java private class DateAfterQualifier implements Qualifier { private LocalDateTime startDate; @@ -224,6 +239,37 @@ public class OptionCommand extends Command{ } } +``` +###### /java/seedu/address/model/PredicateBuilder.java +``` java + private class TypeQualifier implements Qualifier { + private String type; + private static final String TASK_TYPE_STRING = "task"; + private static final String EVENT_TYPE_STRING = "event"; + private static final String INVALID_TYPE_MESSAGE = "Invalid type filtering. Please use either 'task' or 'event' as value."; + + TypeQualifier(String type) { + this.type = type; + } + + @Override + public boolean run(Entry entry) { + switch (type) { + case TASK_TYPE_STRING: + return entry instanceof Task; + case EVENT_TYPE_STRING: + return entry instanceof Event; + default: + return false; + } + } + + @Override + public String toString() { + return "Type of: " + type; + } + } + ``` ###### /java/seedu/address/model/task/Event.java ``` java @@ -231,19 +277,26 @@ public final class Event extends Entry{ protected ObjectProperty startTime; protected ObjectProperty endTime; static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEE, MMM d 'at' HH:mm"); + private static final String INVALID_START_END_TIME = "Invalid start and end time. i.e: start time is after end time."; - public Event(Title title, LocalDateTime startTime, LocalDateTime endTime, UniqueTagList tags, boolean isMarked, String description) { - assert !CollectionUtil.isAnyNull(title, tags, description, startTime, endTime); + public Event(Title title, LocalDateTime startTime, LocalDateTime endTime, UniqueTagList tags, boolean isMarked, String description, + LocalDateTime lastModifiedTime) throws IllegalArgumentException{ + assert !CollectionUtil.isAnyNull(title, tags, description, startTime, endTime, lastModifiedTime); + if(startTime.isAfter(endTime)) { + throw new IllegalArgumentException(INVALID_START_END_TIME); + } this.title = new SimpleObjectProperty<>(Title.copy(title)); this.tags = new SimpleObjectProperty<>(new UniqueTagList(tags)); this.isMarked = new SimpleBooleanProperty(Boolean.valueOf(isMarked)); this.description = new SimpleStringProperty(description); this.startTime = new SimpleObjectProperty<>(startTime); this.endTime = new SimpleObjectProperty<>(endTime); + this.lastModifiedTime = new SimpleObjectProperty<>(lastModifiedTime); } - public Event(Entry entry) { - this(entry.getTitle(), ((Event)entry).getStartTime(), ((Event)entry).getEndTime(), entry.getTags(), entry.isMarked(), entry.getDescription()); + public Event(Entry entry) throws IllegalArgumentException{ + this(entry.getTitle(), ((Event)entry).getStartTime(), ((Event)entry).getEndTime(), entry.getTags(), entry.isMarked(), + entry.getDescription(), entry.getLastModifiedTime()); } public LocalDateTime getStartTime() { @@ -303,7 +356,6 @@ public final class Event extends Entry{ && this.isSameStateAs((Event) other)); } - @Override ``` ###### /java/seedu/address/model/task/Task.java ``` java @@ -312,7 +364,7 @@ public class Task extends Entry { protected ObjectProperty deadline; - public Task(Title title, LocalDateTime deadline, UniqueTagList tags, boolean isMarked, String description) { + public Task(Title title, LocalDateTime deadline, UniqueTagList tags, boolean isMarked, String description, LocalDateTime lastModifiedTime) { assert !CollectionUtil.isAnyNull(title, tags, description); this.title = new SimpleObjectProperty<>(Title.copy(title)); this.tags = new SimpleObjectProperty<>(new UniqueTagList(tags)); @@ -323,13 +375,14 @@ public class Task extends Entry { } else { this.deadline = new SimpleObjectProperty<>(); } + this.lastModifiedTime = new SimpleObjectProperty<>(lastModifiedTime); } /** * Every field must be present and not null. */ public Task(Title title, UniqueTagList tags) { - this(title, null, tags, false, ""); + this(title, null, tags, false, "", LocalDateTime.MIN); } @@ -337,7 +390,7 @@ public class Task extends Entry { * Copy constructor. */ public Task(Entry source) { - this(source.getTitle(), null, source.getTags(), source.isMarked(), source.getDescription()); + this(source.getTitle(), null, source.getTags(), source.isMarked(), source.getDescription(), source.getLastModifiedTime()); if (source instanceof Task) { setDeadline(((Task)source).getDeadline()) ; } @@ -383,8 +436,11 @@ public class Task extends Entry { && other.getAsText().equals(this.getAsText()) && other.isMarked() == this.isMarked()); } + + public boolean isFloatingTask() { + return deadline.get() == null; + } - @Override ``` ###### /java/seedu/address/model/task/Update.java ``` java diff --git a/collated/main/A0126539Yreused.md b/collated/main/A0126539Yreused.md new file mode 100644 index 000000000000..ecb262726a0e --- /dev/null +++ b/collated/main/A0126539Yreused.md @@ -0,0 +1,7 @@ +# A0126539Yreused +###### /java/seedu/address/model/PredicateBuilder.java +``` java + private Predicate buildTypePredicate(String entryType) { + return new PredicateExpression(new TypeQualifier(entryType))::satisfies; + } +``` diff --git a/collated/main/A0127828W.md b/collated/main/A0127828W.md index caf61a821449..ad2d9f5eab66 100644 --- a/collated/main/A0127828W.md +++ b/collated/main/A0127828W.md @@ -1,4 +1,81 @@ # A0127828W +###### /java/seedu/address/logic/commands/CommandHistory.java +``` java + * + * Maintains an internal data structure to keep track of input commands + * Use a pointer to indicate which command should be returned when getter methods are called + */ +public class CommandHistory { + private ArrayList commandHistory; + private Integer commandHistoryIndex; + + /** + * Initialize command history + * A commandHistoryIndex of -1 indicates that it has reached the limit + */ + public CommandHistory() { + commandHistory = new ArrayList<>(); + commandHistoryIndex = -1; + } + + /** + * Append a new command, represented by a string, to an internal storage + * @param string + */ + public void appendCommand(String string) { + if (string == null || string.isEmpty()) { + return; + } + commandHistory.add(string); + commandHistoryIndex++; + } + + /** + * Return the previous command in the command history + * @return + */ + public String getPreviousCommand() { + if (isAtOldestCommand()) { + return null; + } + commandHistoryIndex--; + String returnCommand = commandHistory.get(commandHistoryIndex); + + return returnCommand; + } + + /** + * Return the next command in the command history + * @return + */ + public String getNextCommand() { + if (isAtNewestCommand()) { + return ""; + } + commandHistoryIndex++; + String returnCommand = commandHistory.get(commandHistoryIndex); + + return returnCommand; + } + + /** + * Reset the position of pointer to the latest command + */ + public void resetPosition() { + commandHistoryIndex = commandHistory.size(); + } + + private boolean isAtOldestCommand() { + return commandHistory.isEmpty() + || commandHistoryIndex == 0; + } + + private boolean isAtNewestCommand() { + return commandHistory.isEmpty() + || commandHistoryIndex == commandHistory.size() - 1; + } +} +``` ###### /java/seedu/address/logic/commands/ListCommand.java ``` java */ @@ -6,6 +83,8 @@ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; + public static final String LIST_ALL_COMMAND_WORD = "list-all"; + public static final String MESSAGE_SUCCESS = "Listed all entries"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": List all entries whose titles contain any of " @@ -20,16 +99,22 @@ public class ListCommand extends Command { public static final String AFTER_FLAG = "after/"; public static final String BEFORE_FLAG = "before/"; public static final String ON_FLAG = "on/"; + public static final String TYPE_FLAG = "type/"; private Set keywords; private Set tags; private LocalDateTime startDate; private LocalDateTime endDate; private LocalDateTime onDate; + private final boolean includeCompleted; + private final String entryType; private final PredicateBuilder predicateBuilder = new PredicateBuilder(); - public ListCommand() {} + public ListCommand(boolean includeCompleted, String entryType) { + this.includeCompleted = includeCompleted; + this.entryType = entryType; + } public void setKeywords(Set keywords) { this.keywords = keywords; @@ -56,10 +141,15 @@ public class ListCommand extends Command { if (isListAll()) { return showAll(); } else { - Predicate predicate = predicateBuilder.buildPredicate(keywords, tags, startDate, endDate, onDate); - model.updateFilteredEntryListPredicate(predicate); + try { + Predicate predicate = predicateBuilder.buildPredicate(keywords, tags, startDate, endDate, onDate, includeCompleted, entryType); + model.updateFilteredEntryListPredicate(predicate); + return new CommandResult(getMessageForPersonListShownSummary(model.getFilteredPersonList().size())); + } catch (IllegalValueException e) { + indicateAttemptToExecuteIncorrectCommand(); + return new CommandResult(e.getMessage()); + } - return new CommandResult(getMessageForPersonListShownSummary(model.getFilteredPersonList().size())); } } @@ -77,7 +167,8 @@ public class ListCommand extends Command { && (tags == null || tags.isEmpty()) && startDate == null && endDate == null - && onDate == null; + && onDate == null + && includeCompleted; } } ``` @@ -118,58 +209,38 @@ public class ListCommand extends Command { * full command args string * @return the prepared command */ - private Command prepareList(String args) { + private Command prepareList(String args, boolean includeCompleted) { // Guard statement if (args.isEmpty()) { - return new ListCommand(); + return new ListCommand(includeCompleted, ""); } - final ArgumentTokenizer argsTokenizer = new ArgumentTokenizer(startDatePrefix, endDatePrefix, onDatePrefix, tagPrefix); + final ArgumentTokenizer argsTokenizer = new ArgumentTokenizer(startDatePrefix, endDatePrefix, onDatePrefix, tagPrefix, typePrefix); argsTokenizer.tokenize(args); try { - ListCommand listCommand = new ListCommand(); + String typeString = unwrapOptionalStringOrEmpty(argsTokenizer.getValue(typePrefix)); + ListCommand listCommand = new ListCommand(includeCompleted, typeString); // keywords delimited by whitespace - Optional keywordsString = argsTokenizer.getPreamble(); - if (keywordsString.isPresent()) { - String[] keywords = keywordsString.get().split("\\s+"); - - Set keywordSet = new HashSet<>(Arrays.asList(keywords)); - keywordSet.removeIf(s -> "".equals(s)); - listCommand.setKeywords(keywordSet); - } + setKeywords(argsTokenizer, listCommand); String onDateString = unwrapOptionalStringOrEmpty(argsTokenizer.getValue(onDatePrefix)); String startDateString = unwrapOptionalStringOrEmpty(argsTokenizer.getValue(startDatePrefix)); String endDateString = unwrapOptionalStringOrEmpty(argsTokenizer.getValue(endDatePrefix)); Set tags = getTagsFromArgs(argsTokenizer); - if (!tags.isEmpty()) { - listCommand.setTags(tags); - } + setTags(listCommand, tags); // Ranged search and specific-day search should be mutually exclusive - if (!onDateString.isEmpty() && (!startDateString.isEmpty() || !endDateString.isEmpty())) { + if (isNotMutuallyExclusiveDates(onDateString, startDateString, endDateString)) { return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListCommand.MESSAGE_MUTUALLY_EXCLUSIVE_OPTIONS)); } - if (onDateString.isEmpty()) { - final LocalDateTime startDate = getLocalDateTimeFromArgument(startDateString); - LocalDateTime endDate = getLocalDateTimeFromArgument(endDateString); - if (endDate != null) { - endDate = endDate.plusDays(1).minusSeconds(1); - } - - if (startDate != null && endDate != null && startDate.isAfter(endDate)) { - return new IncorrectCommand(ListCommand.MESSAGE_INVALID_DATE); - } - - listCommand.setStartDate(startDate); - listCommand.setEndDate(endDate); + if (onDateString.isEmpty() && !setValidDateRange(listCommand, startDateString, endDateString)) { + return new IncorrectCommand(ListCommand.MESSAGE_INVALID_DATE); } else { final LocalDateTime onDate = getLocalDateTimeFromArgument(onDateString); - listCommand.setOnDate(onDate); } @@ -179,6 +250,49 @@ public class ListCommand extends Command { } } + private void setKeywords(ArgumentTokenizer argsTokenizer, ListCommand listCommand) { + Optional keywordsString = argsTokenizer.getPreamble(); + if (keywordsString.isPresent()) { + String[] keywords = keywordsString.get().split("\\s+"); + + Set keywordSet = new HashSet<>(Arrays.asList(keywords)); + keywordSet.removeIf(s -> "".equals(s)); + listCommand.setKeywords(keywordSet); + } + } + + private boolean isNotMutuallyExclusiveDates(String onDateString, String startDateString, String endDateString) { + return !onDateString.isEmpty() && (!startDateString.isEmpty() || !endDateString.isEmpty()); + } + + /** + * Return false if the provided dates are invalid + * Return true and set date attributes to listCommand otherwise + * + * @throws IllegalValueException + */ + private boolean setValidDateRange(ListCommand listCommand, String startDateString, String endDateString) throws IllegalValueException { + final LocalDateTime startDate = getLocalDateTimeFromArgument(startDateString); + LocalDateTime endDate = getLocalDateTimeFromArgument(endDateString); + if (endDate != null) { + endDate = endDate.plusDays(1).minusSeconds(1); + } + + if (startDate != null && endDate != null && startDate.isAfter(endDate)) { + return false; + } + + listCommand.setStartDate(startDate); + listCommand.setEndDate(endDate); + return true; + } + + private void setTags(ListCommand listCommand, Set tags) { + if (!tags.isEmpty()) { + listCommand.setTags(tags); + } + } + private static String unwrapOptionalStringOrEmpty(Optional optional) { if (optional.isPresent()) { return optional.get(); @@ -205,141 +319,66 @@ public class PredicateBuilder { /** * Return a chained Predicate from all the conditions indicated in params * @param keywords + * @param tags * @param startDate * @param endDate * @param onDate - * @param tags + * @param entryType TODO * @return pred the chained Predicate + * @throws IllegalValueException TODO */ - public Predicate buildPredicate(Set keywords, Set tags, LocalDateTime startDate, LocalDateTime endDate, LocalDateTime onDate) { + public Predicate buildPredicate(Set keywords, Set tags, LocalDateTime startDate, LocalDateTime endDate, LocalDateTime onDate, boolean includeCompleted, String entryType) throws IllegalValueException { // Initial predicate Predicate pred = e -> true; - if (keywords != null && !keywords.isEmpty()) { - pred = pred.and(buildKeywordsPredicate(keywords)); - } - - if (tags != null && !tags.isEmpty()) { - pred = pred.and(buildTagsPredicate(tags)); - } - - if (onDate != null) { - pred = pred.and(buildOnPredicate(onDate)); - } else { - if (startDate != null) { - pred = pred.and(buildAfterPredicate(startDate)); - } - if (endDate != null) { - pred = pred.and(buildBeforePredicate(endDate)); - } - } + pred = addCompletedPredicateIfExist(pred, includeCompleted); + pred = addKeywordsPredicateIfExist(pred, keywords); + pred = addTagPredicateIfExist(pred, tags); + pred = addDatePredicateIfExist(pred, onDate, startDate, endDate); + pred = addTypePredicateIfExist(pred, entryType); return pred; } - - private Predicate buildKeywordsPredicate(Set keywords) { - return new PredicateExpression(new TitleQualifier(keywords))::satisfies; - } - - private Predicate buildTagsPredicate(Set tags) { - return new PredicateExpression(new TagsQualifier(tags))::satisfies; - } - - private Predicate buildBeforePredicate(LocalDateTime endDate) { - return new PredicateExpression(new DateBeforeQualifier(endDate))::satisfies; - } - - private Predicate buildAfterPredicate(LocalDateTime startDate) { - return new PredicateExpression(new DateAfterQualifier(startDate))::satisfies; - } - - private Predicate buildOnPredicate(LocalDateTime onDate) { - return new PredicateExpression(new DateOnQualifier(onDate))::satisfies; - } - //@author A0127828W-reused - //========== Inner classes/interfaces used for filtering ================================================== - - interface Expression { - boolean satisfies(Entry entry); - String toString(); - } - - private class PredicateExpression implements Expression { - - private final Qualifier qualifier; - - PredicateExpression(Qualifier qualifier) { - this.qualifier = qualifier; - } - - @Override - public boolean satisfies(Entry entry) { - return qualifier.run(entry); - } - - @Override - public String toString() { - return qualifier.toString(); + + private Predicate addCompletedPredicateIfExist(Predicate pred, boolean includeCompleted) { + Predicate result = pred; + if (!includeCompleted) { + result = result.and(buildCompletedPredicate(includeCompleted)); } + return result; } - - interface Qualifier { - boolean run(Entry entry); - String toString(); - } - - //@author A0127828W - private class TitleQualifier implements Qualifier { - private Set titleKeyWords; - - TitleQualifier(Set nameKeyWords) { - this.titleKeyWords = nameKeyWords; - } - - @Override - public boolean run(Entry entry) { - return titleKeyWords.stream() - .filter(keyword -> StringUtil.containsIgnoreCase(entry.getTitle().fullTitle, keyword)) - .findAny() - .isPresent(); - } - - @Override - public String toString() { - return "name=" + String.join(", ", titleKeyWords); + + private Predicate addKeywordsPredicateIfExist(Predicate pred, Set keywords) { + Predicate result = pred; + if (keywords != null && !keywords.isEmpty()) { + result = result.and(buildKeywordsPredicate(keywords)); } + return result; } - - private class TagsQualifier implements Qualifier { - private Set tags; - - TagsQualifier(Set tags) { - this.tags = tags; + + private Predicate addTagPredicateIfExist(Predicate pred, Set tags) { + Predicate result = pred; + if (tags != null && !tags.isEmpty()) { + result = result.and(buildTagsPredicate(tags)); } - - @Override - public boolean run(Entry entry) { - Set tags = new HashSet<>(); - try { - for (String t: this.tags) { - tags.add(new Tag(t)); - } - } catch (IllegalValueException ive) { - System.err.println(MESSAGE_TAG_CONSTRAINTS); - return false; + return result; + } + + private Predicate addDatePredicateIfExist(Predicate pred, LocalDateTime onDate, LocalDateTime startDate, LocalDateTime endDate) { + Predicate result = pred; + if (onDate != null) { + result = result.and(buildOnPredicate(onDate)); + } else { + if (startDate != null) { + result = result.and(buildAfterPredicate(startDate)); + } + if (endDate != null) { + result = result.and(buildBeforePredicate(endDate)); } - - return tags.stream() - .filter(tag -> entry.getTags().contains(tag)) - .findAny() - .isPresent(); - } - - @Override - public String toString() { - return "tags=" + String.join(", ", tags); } + return result; } + ``` ###### /java/seedu/address/model/PredicateBuilder.java ``` java @@ -376,6 +415,31 @@ public class PredicateBuilder { return "Due on: " + onDate.toString(); } } + +``` +###### /java/seedu/address/model/PredicateBuilder.java +``` java + private class CompletedQualifier implements Qualifier { + private boolean includeCompleted; + + CompletedQualifier(boolean includeCompleted) { + this.includeCompleted = includeCompleted; + } + + @Override + public boolean run(Entry entry) { + if (includeCompleted) { + return true; + } else { + return !entry.isMarked(); + } + } + + @Override + public String toString() { + return "Include completed: " + includeCompleted; + } + } } ``` ###### /java/seedu/address/model/task/Event.java @@ -388,7 +452,7 @@ public class PredicateBuilder { Date interpreted = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant()); return dateTime.format(DATE_TIME_FORMATTER); } -} + ``` ###### /java/seedu/address/model/task/Task.java ``` java @@ -399,5 +463,32 @@ public class PredicateBuilder { Date interpreted = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant()); return prettyTime.format(interpreted); } -} + +``` +###### /java/seedu/address/ui/CommandArea.java +``` java + @FXML + private void handleKeyPressedCommandArea(KeyEvent event) { + switch (event.getCode()) { + case UP: + fillUpCommandLine(commandHistoryManager.getPreviousCommand()); + logger.info("UP key entered"); + break; + case DOWN: + fillUpCommandLine(commandHistoryManager.getNextCommand()); + logger.info("DOWN key entered"); + break; + } + } + + /** + * Fill the command line with an input string + */ + private void fillUpCommandLine(String command) { + if (command == null) { + logger.info("Reached command history limit."); + return; + } + cmdLine.setText(command); + } ``` diff --git a/collated/test/A0121501E.md b/collated/test/A0121501E.md new file mode 100644 index 000000000000..6b58c17cde03 --- /dev/null +++ b/collated/test/A0121501E.md @@ -0,0 +1,678 @@ +# A0121501E +###### \java\seedu\address\logic\LogicManagerTest.java +``` java + @Test + public void execute_tag_withoutTagnameArgs_errorMessageShown() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task t1 = helper.generateTask(1); + helper.addToModel(model, helper.generateEntryList(t1)); + + Task t1Copy = helper.generateTask(1); + List expectedList = helper.generateSortedEntryList(t1Copy); + TaskManager expectedAB = helper.generateTodoList(expectedList); + String expectedMessage = String.format(String.format(MESSAGE_INVALID_COMMAND_FORMAT, TagCommand.MESSAGE_USAGE)); + + assertCommandBehavior("tag 1", expectedMessage, expectedAB, expectedList); + } + + @Test + public void execute_tag_allowAlphanumericSpaceUnderscore() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task t1 = helper.generateTask(1); + helper.addToModel(model, helper.generateEntryList(t1)); + + + Tag tag1 = new Tag("tag10"); + Tag tag2 = new Tag("tag_10"); + Tag tag3 = new Tag("tag 10"); + UniqueTagList tags = new UniqueTagList(tag1, tag2, tag3); + + Task t1Copy = helper.generateTask(1); + t1Copy.addTags(tags); + + List expectedList = helper.generateSortedEntryList(t1Copy); + TaskManager expectedAB = helper.generateTodoList(expectedList); + String expectedMessage = String.format(TagCommand.MESSAGE_SUCCESS, tags, t1Copy); + assertCommandBehavior(helper.generateTagCommand(tags, 1), + expectedMessage, expectedAB, expectedList); + } + + @Test + public void execute_tag_allowAddExistingTags() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task t1 = helper.generateTask(1); + helper.addToModel(model, helper.generateEntryList(t1)); + + Tag tag1 = new Tag("tag1"); + Tag tag2 = new Tag("tag2"); + Tag tag3 = new Tag("tag3"); + UniqueTagList tags = new UniqueTagList(tag1, tag2, tag3); + + Task t1Copy = helper.generateTask(1); + t1Copy.addTags(tags); + + List expectedList = helper.generateSortedEntryList(t1Copy); + TaskManager expectedAB = helper.generateTodoList(expectedList); + String expectedMessage = String.format(TagCommand.MESSAGE_SUCCESS, new UniqueTagList(tag3), t1Copy); + assertCommandBehavior(helper.generateTagCommand(tags, 1), + expectedMessage, expectedAB, expectedList); + } + + @Test + public void execute_tagOnlyExistingTags_errorMessageShown() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task t1 = helper.generateTask(1); + helper.addToModel(model, helper.generateEntryList(t1)); + + Tag tag1 = new Tag("tag1"); + Tag tag2 = new Tag("tag2"); + UniqueTagList tags = new UniqueTagList(tag1, tag2); + + Task t1Copy = helper.generateTask(1); + t1Copy.addTags(tags); + + List expectedList = helper.generateSortedEntryList(t1Copy); + TaskManager expectedAB = helper.generateTodoList(expectedList); + String expectedMessage = String.format(TagCommand.MESSAGE_ALREADY_EXISTS, t1Copy); + assertCommandBehavior(helper.generateTagCommand(tags, 1), + expectedMessage, expectedAB, expectedList); + } + + @Test + public void execute_untagInvalidArgsFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, UntagCommand.MESSAGE_USAGE); + assertIncorrectIndexFormatBehaviorForCommand("untag", expectedMessage); + } + + @Test + public void execute_untagInvalidTagFormat_errorMessageShown() throws Exception { + String expectedMessage = String.format(Tag.MESSAGE_TAG_CONSTRAINTS); + + TestDataHelper helper = new TestDataHelper(); + Task t1 = helper.generateTask(1); + helper.addToModel(model, helper.generateEntryList(t1)); + + List expectedList = helper.generateSortedEntryList(t1); + TaskManager expectedAB = helper.generateTodoList(expectedList); + + assertCommandBehavior("untag 1 " + TAG_FLAG + "**", expectedMessage, expectedAB, expectedList); + } + + @Test + public void execute_untagWithoutTagnameArgs_errorMessageShown() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task t1 = helper.generateTask(1); + helper.addToModel(model, helper.generateEntryList(t1)); + + Task t1Copy = helper.generateTask(1); + List expectedList = helper.generateSortedEntryList(t1Copy); + TaskManager expectedAB = helper.generateTodoList(expectedList); + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, UntagCommand.MESSAGE_USAGE); + + assertCommandBehavior("untag 1", expectedMessage, expectedAB, expectedList); + } + + @Test + public void execute_untag_allowAlphanumericSpaceUnderscore() throws Exception { + TestDataHelper helper = new TestDataHelper(); + + Tag tag1 = new Tag("tag1"); + Tag tag2 = new Tag("tag_1"); + Tag tag3 = new Tag("tag 1"); + UniqueTagList tags = new UniqueTagList(tag1, tag2, tag3); + + Task t1 = helper.generateTask(1); + helper.addToModel(model, helper.generateEntryList(t1)); + + Task t1Copy = helper.generateTask(1); + t1Copy.removeTags(tags); + + List expectedList = helper.generateSortedEntryList(t1Copy); + TaskManager expectedAB = helper.generateTodoList(expectedList); + helper.addToAddressBook(expectedAB, new UniqueTagList(tag1)); + + String expectedMessage = String.format(UntagCommand.MESSAGE_SUCCESS, new UniqueTagList(tag1), t1Copy); + assertCommandBehavior(helper.generateUntagCommand(tags, 1), + expectedMessage, expectedAB, expectedList); + } + + @Test + public void execute_untagNonExistentTags_NonExistentMessageShown() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task t1 = helper.generateTask(1); + helper.addToModel(model, helper.generateEntryList(t1)); + + Tag tag3 = new Tag("tag3"); // Does not exist + UniqueTagList tags = new UniqueTagList(tag3); + + Task t1Copy = helper.generateTask(1); + + List expectedList = helper.generateSortedEntryList(t1Copy); + TaskManager expectedAB = helper.generateTodoList(expectedList); + String expectedMessage = String.format(UntagCommand.MESSAGE_NON_EXISTENT, t1Copy); + assertCommandBehavior(helper.generateUntagCommand(tags, 1), + expectedMessage, expectedAB, expectedList); + } + + @Test + public void execute_mark_successful() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task toBeMarked = helper.generateTask(1); + Task toBeMarkedCopy = helper.generateTask(1); + + TaskManager expectedAB = new TaskManager(); + toBeMarkedCopy.mark(); + expectedAB.addTask(toBeMarkedCopy); + + model.addTask(toBeMarked); + assertCommandBehavior("mark 1", + String.format(MarkCommand.MESSAGE_SUCCESS, toBeMarked), + expectedAB, + new ArrayList<>()); + } + + @Test + public void execute_mark_alreadyMarked() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task alreadyMarked = helper.generateTask(1); + alreadyMarked.mark(); + Task toBeMarkedCopy = helper.generateTask(1); + + TaskManager expectedAB = new TaskManager(); + toBeMarkedCopy.mark(); + expectedAB.addTask(toBeMarkedCopy); + + model.addTask(alreadyMarked); + logic.execute(ListCommand.LIST_ALL_COMMAND_WORD); + + assertCommandBehavior("mark 1", + String.format(MarkCommand.MESSAGE_SUCCESS, alreadyMarked), + expectedAB, + new ArrayList<>()); + } + + @Test + public void execute_unmark_successful() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task toBeUnmarked = helper.generateTask(1); + Task toBeUnmarkedCopy = helper.generateTask(1); + + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(toBeUnmarkedCopy); + + model.addTask(toBeUnmarked); + assertCommandBehavior("unmark 1", + String.format(UnmarkCommand.MESSAGE_SUCCESS, toBeUnmarked), + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_unmark_alreadyUnmarked() throws Exception { + TestDataHelper helper = new TestDataHelper(); + Task alreadyUnmarked = helper.generateTask(1); + Task alreadyUnmarkedCopy = helper.generateTask(1); + + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(alreadyUnmarkedCopy); + + model.addTask(alreadyUnmarked); + assertCommandBehavior("unmark 1", + String.format(UnmarkCommand.MESSAGE_SUCCESS, alreadyUnmarked), + expectedAB, + expectedAB.getTaskList()); + } + //################### + //# Undo test cases # + //################### + @Test + public void execute_undoWithNoUndoableCommandsInHistory_errorMessageShown() throws Exception { + // setup expectations + TaskManager expectedAB = new TaskManager(); + + // command before undo + logic.execute("list"); + // execute command and verify result + assertCommandBehavior("undo", + UndoCommand.MESSAGE_FAILURE, + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_undoAdd_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeAdded = helper.taskWithTags(); + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(toBeAdded); + expectedAB.removeEntry(toBeAdded); + + // commands before undo + logic.execute(helper.generateAddCommand(toBeAdded)); + // execute command and verify result + assertCommandBehavior("undo", + String.format(AddCommand.MESSAGE_UNDO_SUCCESS, toBeAdded), + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_undoDelete_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task task1 = helper.generateTask(1); + Task task2 = helper.generateTask(2); + Task task3 = helper.generateTask(3); + + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(task1); + expectedAB.addTask(task2); + expectedAB.addTask(task3); + expectedAB.removeEntry(task2); + expectedAB.addTask(task2); + + model.addTask(task1); + model.addTask(task2); + model.addTask(task3); + // command to undo + logic.execute("delete 2"); + // execute command and verify result + assertCommandBehavior("undo", + String.format(DeleteCommand.MESSAGE_UNDO_DELETE_PERSON_SUCCESS, task2), + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_undoEdit_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toEdit = helper.taskWithTags(); + Task toEditCopy = helper.taskWithTags(); + + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(toEditCopy); + + model.addTask(toEdit); + // command to undo + logic.execute(helper.generateEditCommand(toEdit, 1, "New Title")); + + // execute command and verify result + assertCommandBehavior("undo", + String.format(EditCommand.MESSAGE_UNDO_SUCCESS, toEditCopy), + expectedAB, expectedAB.getTaskList()); + } + + @Test + public void execute_undoMark_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeMarked = helper.taskWithTags(); + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(toBeMarked); + + model.addTask(toBeMarked); + // command to undo + logic.execute("mark 1"); + // execute command and verify result + assertCommandBehavior("undo", + String.format(MarkCommand.MESSAGE_UNDO_SUCCESS, toBeMarked), + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_undoUnmark_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeUnmarked = helper.taskWithTags(); + toBeUnmarked.mark(); + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(toBeUnmarked); + + model.addTask(toBeUnmarked); + // command to undo + logic.execute(ListCommand.LIST_ALL_COMMAND_WORD); + logic.execute("unmark 1"); + // execute command and verify result + assertCommandBehavior("undo", + String.format(UnmarkCommand.MESSAGE_UNDO_SUCCESS, toBeUnmarked), + expectedAB, + new ArrayList<>()); + } + + @Test + public void execute_undoTag_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeTagged = helper.taskWithTags(); + Task toBeTaggedCopy = helper.taskWithTags(); + + Tag tag1 = new Tag("tag1"); + Tag tag2 = new Tag("newTag"); + UniqueTagList tags = new UniqueTagList(tag1, tag2); + + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(toBeTagged); + expectedAB.addTag(tag2); + + model.addTask(toBeTagged); + // command to undo + logic.execute(helper.generateTagCommand(tags, 1)); + // execute command and verify result + assertCommandBehavior("undo", + String.format(TagCommand.MESSAGE_UNDO_SUCCESS, new UniqueTagList(tag2), toBeTaggedCopy), + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_undoUntag_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeUntagged = helper.taskWithTags(); + Task toBeUntaggedCopy = helper.taskWithTags(); + + Tag tag1 = new Tag("tag1"); + Tag tag2 = new Tag("newTag"); + UniqueTagList tags = new UniqueTagList(tag1, tag2); + + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(toBeUntagged); + + model.addTask(toBeUntagged); + // command to undo + logic.execute(helper.generateUntagCommand(tags, 1)); + // execute command and verify result + assertCommandBehavior("undo", + String.format(UntagCommand.MESSAGE_UNDO_SUCCESS, new UniqueTagList(tag1), toBeUntaggedCopy), + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_undoClear_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task task1 = helper.generateTask(1); + Task task2 = helper.generateTask(2); + Task task3 = helper.generateTask(3); + + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(task1); + expectedAB.addTask(task2); + expectedAB.addTask(task3); + + model.addTask(task1); + model.addTask(task2); + model.addTask(task3); + // command to undo + logic.execute("clear"); + // execute command and verify result + assertCommandBehavior("undo", + ClearCommand.MESSAGE_UNDO_SUCCESS, + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_undoMultipleCommands() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task task1 = helper.generateTask(1); + Task task2 = helper.generateTask(2); // To be marked + Task task2Copy = helper.generateTask(2); + Task task3 = helper.generateTask(3); + + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(task1); + expectedAB.addTask(task2); + expectedAB.addTask(task3); + expectedAB.removeEntry(task2); + expectedAB.addTask(task2); + + model.addTask(task1); + model.addTask(task2); + + // command to undo + logic.execute(helper.generateAddCommand(task3)); + logic.execute(ListCommand.LIST_ALL_COMMAND_WORD); //non-undoable command. + logic.execute("mark 2"); //changes the lastModifiedDate which changes the order + logic.execute(ListCommand.LIST_ALL_COMMAND_WORD); //non-undoable command. + logic.execute("delete 3"); + + // Undo "delete 2" + assertCommandBehavior("undo", + String.format(DeleteCommand.MESSAGE_UNDO_DELETE_PERSON_SUCCESS, task2), + expectedAB, + Arrays.asList(task1, task3)); + + // Undo "mark 2" + assertCommandBehavior("undo", + String.format(MarkCommand.MESSAGE_UNDO_SUCCESS, task2Copy), + expectedAB, + expectedAB.getTaskList()); + + // Undo add task + expectedAB.removeEntry(task3); + assertCommandBehavior("undo", + String.format(AddCommand.MESSAGE_UNDO_SUCCESS, task3), + expectedAB, + expectedAB.getTaskList()); + } + + + //################### + //# Redo test cases # + //################### + @Test + public void execute_redoWithNoUndoableCommandsInHistory_errorMessageShown() throws Exception { + // setup expectations + TaskManager expectedAB = new TaskManager(); + + // execute command and verify result + assertCommandBehavior("redo", + RedoCommand.MESSAGE_FAILURE, + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_redoAdd_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeAdded = helper.taskWithTags(); + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(toBeAdded); + + // commands before undo + logic.execute(helper.generateAddCommand(toBeAdded)); + logic.execute("undo"); + // execute command and verify result + assertCommandBehavior("redo", + String.format(AddCommand.MESSAGE_SUCCESS, toBeAdded), + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_redoDelete_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task task1 = helper.generateTask(1); + Task task2 = helper.generateTask(2); + Task task3 = helper.generateTask(3); + + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(task1); + expectedAB.addTask(task2); + expectedAB.addTask(task3); + expectedAB.removeEntry(task2); + + model.addTask(task1); + model.addTask(task2); + model.addTask(task3); + // command to undo + logic.execute("delete 2"); + logic.execute("undo"); + // execute command and verify result + assertCommandBehavior("redo", + String.format(DeleteCommand.MESSAGE_DELETE_PERSON_SUCCESS, task2), + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_redoEdit_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toEdit = helper.taskWithTags(); + Task toEditCopy = helper.taskWithTags(); + + TaskManager expectedAB = new TaskManager(); + toEditCopy.setTitle(new Title("New Title")); + expectedAB.addTask(toEditCopy); + + model.addTask(toEdit); + // command to undo + logic.execute(helper.generateEditCommand(toEdit, 1, "New Title")); + logic.execute("undo"); + + // execute command and verify result + assertCommandBehavior("redo", + String.format(EditCommand.MESSAGE_SUCCESS, toEditCopy), + expectedAB, expectedAB.getTaskList()); + } + + @Test + public void execute_redoMark_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeMarked = helper.taskWithTags(); + Task toBeMarkedCopy = helper.taskWithTags(); + TaskManager expectedAB = new TaskManager(); + toBeMarkedCopy.mark(); + expectedAB.addTask(toBeMarkedCopy); + + model.addTask(toBeMarked); + // command to undo + logic.execute("mark 1"); + logic.execute("undo"); + // execute command and verify result + assertCommandBehavior("redo", + String.format(MarkCommand.MESSAGE_SUCCESS, toBeMarked), + expectedAB, + new ArrayList<>()); + } + + @Test + public void execute_redoUnmark_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeUnmarked = helper.taskWithTags(); + Task toBeUnmarkedCopy = helper.taskWithTags(); + TaskManager expectedAB = new TaskManager(); + toBeUnmarked.mark(); + expectedAB.addTask(toBeUnmarkedCopy); + + model.addTask(toBeUnmarked); + // command to undo + logic.execute(ListCommand.LIST_ALL_COMMAND_WORD); + logic.execute("unmark 1"); + logic.execute("undo"); + // execute command and verify result + assertCommandBehavior("redo", + String.format(UnmarkCommand.MESSAGE_SUCCESS, toBeUnmarked), + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_redoTag_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeTagged = helper.taskWithTags(); + Task toBeTaggedCopy = helper.taskWithTags(); + + Tag tag1 = new Tag("tag1"); + Tag tag2 = new Tag("newTag"); + UniqueTagList tags = new UniqueTagList(tag1, tag2); + toBeTaggedCopy.addTags(tags); + + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(toBeTaggedCopy); + + model.addTask(toBeTagged); + // command to undo + logic.execute(helper.generateTagCommand(tags, 1)); + logic.execute("undo"); + // execute command and verify result + assertCommandBehavior("redo", + String.format(TagCommand.MESSAGE_SUCCESS, new UniqueTagList(tag2), toBeTaggedCopy), + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_redoUntag_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task toBeUntagged = helper.taskWithTags(); + Task toBeUntaggedCopy = helper.taskWithTags(); + Tag tag1 = new Tag("tag1"); + Tag tag2 = new Tag("newTag"); + UniqueTagList tags = new UniqueTagList(tag1, tag2); + + TaskManager expectedAB = new TaskManager(); + expectedAB.addTask(toBeUntaggedCopy); + toBeUntaggedCopy.removeTags(tags); + + model.addTask(toBeUntagged); + // command to undo + logic.execute(helper.generateUntagCommand(tags, 1)); + logic.execute("undo"); + // execute command and verify result + assertCommandBehavior("redo", + String.format(UntagCommand.MESSAGE_SUCCESS, new UniqueTagList(tag1), toBeUntaggedCopy), + expectedAB, + expectedAB.getTaskList()); + } + + @Test + public void execute_redoClear_successful() throws Exception { + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task task1 = helper.generateTask(1); + Task task2 = helper.generateTask(2); + Task task3 = helper.generateTask(3); + + TaskManager expectedAB = new TaskManager(); + + model.addTask(task1); + model.addTask(task2); + model.addTask(task3); + // command to undo + logic.execute("clear"); + logic.execute("undo"); + // execute command and verify result + assertCommandBehavior("redo", + ClearCommand.MESSAGE_SUCCESS, + expectedAB, + expectedAB.getTaskList()); + } +``` +###### \java\seedu\address\testutil\TestUtil.java +``` java + /** + * Adds persons to the array of sorted persons. + * @param persons A array of persons. + * @param personsToAdd The persons that are to be added to the sorted list. + * @return The modified array of sorted persons. + */ + public static TestEntry[] addPersonsToSortedList(final TestEntry[] persons, TestEntry... personsToAdd) { + TestEntry[] testEntry = addPersonsToList(persons, personsToAdd); + Arrays.sort(testEntry, new EntryViewComparator()); + return testEntry; + } +``` diff --git a/collated/test/A0127828W.md b/collated/test/A0127828W.md index e97c54f7edab..4d34880f278b 100644 --- a/collated/test/A0127828W.md +++ b/collated/test/A0127828W.md @@ -7,6 +7,7 @@ public class EventTest { private Title title; private LocalDateTime startTime; private LocalDateTime endTime; + private LocalDateTime lastModifiedTime; private UniqueTagList tags; private String description; private boolean isMarked; @@ -20,7 +21,8 @@ public class EventTest { tags = new UniqueTagList(new Tag("tag1")); isMarked = false; description = "Description"; - testEvent = new Event(title, startTime, endTime, tags, isMarked, description); + lastModifiedTime = LocalDateTime.parse("2016-10-10T10:00:00"); + testEvent = new Event(title, startTime, endTime, tags, isMarked, description, lastModifiedTime); } catch (IllegalValueException ive) { ive.printStackTrace(); } @@ -33,10 +35,10 @@ public class EventTest { @Test(expected = ClassCastException.class) public void createEventFromEntry() { - Entry eventEntry = new Event(title, startTime, endTime, tags, isMarked, description); + Entry eventEntry = new Event(title, startTime, endTime, tags, isMarked, description, lastModifiedTime); assertTrue(new Event(eventEntry) instanceof Event); - Entry taskEntry = new Task(title, endTime, tags, isMarked, description); + Entry taskEntry = new Task(title, endTime, tags, isMarked, description, lastModifiedTime); // Should throw exception new Event(taskEntry); } @@ -71,7 +73,7 @@ public class EventTest { @Test public void isSameStateAs() { - Event otherEvent = new Event(title, startTime, endTime, tags, isMarked, description); + Event otherEvent = new Event(title, startTime, endTime, tags, isMarked, description, lastModifiedTime); assertTrue(testEvent.isSameStateAs(otherEvent)); } diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 51bb275bf404..97f9d4dbf119 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,3 +1,4 @@ + # Developer Guide * [Setting Up](#setting-up) @@ -112,6 +113,7 @@ The `UI` component, * Binds itself to some data in the `Model` so that the UI can auto-update when data in the `Model` change. * Responds to events raised from various parts of the App and updates the UI accordingly. + ### Logic component
@@ -225,6 +227,7 @@ Thanks to the [TestFX](https://github.com/TestFX/TestFX) library we use, See [UsingGradle.md](UsingGradle.md) to learn how to use Gradle for build automation. + ### Continuous Integration We use [Travis CI](https://travis-ci.org/) to perform _Continuous Integration_ on our projects. @@ -289,17 +292,17 @@ MSS 2. TodoList add that particular floating task into Storage and save it Use Case Ends - + *Extensions* 1a. Entry is entered with invalid properties e.g. title > 1a1. TodoList warns user that the entry is invalid and does not add it into Storage -> +> > Use Case Ends 1b. Entry already exists > 1b1. TodoList warns user that the entry already exists and does not add it into Storage -> +> > Use Case Ends **Use case: Create an event** @@ -342,6 +345,7 @@ MSS > > Use Case Ends + **Use Case: List all entries** Actors: User diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 573e15a8be83..d692490120af 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -2,7 +2,7 @@ User guide =================== ## Getting Started - +
Our to-do application primarily takes in input using a command line interface. It supports the basic creation, reading, updating and deletion of entries. You may add entries with or without deadlines. These are known as tasks. Entries with a start and/or end time are known as events. @@ -40,6 +40,7 @@ delete 1 + ## Command Summary @@ -53,9 +54,12 @@ delete 1 |delete|`delete `| |mark|`mark `| |unmark|`unmark `| +|undo|`undo`| +|redo|`redo`| |help|`help []`| |option|`option [/ ...]`| + ## Commands #### Adding @@ -72,6 +76,7 @@ Examples: - `add CS2105 Assignment 1 end/2016-10-10 10:00` + #### Listing ``` list [[keywords] [[after/] [before/] | [on/]] [# ...] [recurrence=] [desc=]] [type/{entry, task}] @@ -91,6 +96,7 @@ If you want to include completed entries in your search, replace `list` with `li - `list-all buy banana` + #### Tagging ``` tag # [#...] @@ -108,6 +114,7 @@ Delete tag(s) from a particular entry with a specified id using `untag` Duplicated tags will only be added once + #### Editing ``` edit [title/new title] [start/ end/] [#...] [r/ ] [desc/] @@ -123,7 +130,7 @@ Examples: - `edit 13 #yearly` - + #### Deleting ``` delete @@ -136,6 +143,7 @@ Examples: - `Delete 42` + #### Marking ``` mark @@ -151,6 +159,24 @@ Examples: - `unmark 42` + +#### Undo + +``` +undo +``` + +Undo the latest change to the todo list. Handles every changes, including `clear`. + +#### Redo + +``` +redo +``` + +Redo the latest command reverted with `undo`. + + #### Help ``` help [] @@ -160,6 +186,7 @@ Show available commands and how to use them Help is also shown if you enter an incorrect command e.g. abcd + #### option ``` option [/ ...] @@ -170,6 +197,7 @@ Examples: - `config save/data/MyNewLocation.xml` -#### Misc + +## Misc You can use UP and DOWN keys to browse through your past commands in the session. diff --git a/src/main/java/seedu/address/logic/commands/MarkCommand.java b/src/main/java/seedu/address/logic/commands/MarkCommand.java index 1216f5171ff8..10b1cdbf1d04 100644 --- a/src/main/java/seedu/address/logic/commands/MarkCommand.java +++ b/src/main/java/seedu/address/logic/commands/MarkCommand.java @@ -9,6 +9,9 @@ import seedu.address.model.task.UniqueTaskList.EntryNotFoundException; //@@author A0121501E +/** + * Marks an entry as completed. + */ public class MarkCommand extends UndoableCommand { public static final String COMMAND_WORD = "mark"; diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java index aa364d892daa..f39af67bae6b 100644 --- a/src/main/java/seedu/address/logic/commands/RedoCommand.java +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -4,10 +4,10 @@ import seedu.address.logic.commands.UndoableCommandHistory.UndoableCommandNotFoundException; import seedu.address.model.Model; +//@@author A0121501E /** * Redo the previous undo command. */ -//@@author A0121501E public class RedoCommand extends Command { public static final String COMMAND_WORD = "redo"; diff --git a/src/main/java/seedu/address/logic/commands/TagCommand.java b/src/main/java/seedu/address/logic/commands/TagCommand.java index 80848eab5e61..90ce9da9e9df 100644 --- a/src/main/java/seedu/address/logic/commands/TagCommand.java +++ b/src/main/java/seedu/address/logic/commands/TagCommand.java @@ -13,10 +13,10 @@ import seedu.address.model.tag.Tag; import seedu.address.model.tag.UniqueTagList; +//@@author A0121501E /* - * Add tags to an entry. + * Add tags to an entry. Does not overwrite existing tags. */ -//@@author A0121501E public class TagCommand extends UndoableCommand { public static final String COMMAND_WORD = "tag"; diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java index c68f31258aae..deb652825294 100644 --- a/src/main/java/seedu/address/logic/commands/UndoCommand.java +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -4,10 +4,10 @@ import seedu.address.logic.commands.UndoableCommandHistory.UndoableCommandNotFoundException; import seedu.address.model.Model; +//@@author A0121501E /** * Undo the previous undoable command. */ -//@@author A0121501E public class UndoCommand extends Command { public static final String COMMAND_WORD = "undo"; diff --git a/src/main/java/seedu/address/logic/commands/UndoableCommand.java b/src/main/java/seedu/address/logic/commands/UndoableCommand.java index 74a8457764cf..84f353d9ea9a 100644 --- a/src/main/java/seedu/address/logic/commands/UndoableCommand.java +++ b/src/main/java/seedu/address/logic/commands/UndoableCommand.java @@ -1,10 +1,10 @@ package seedu.address.logic.commands; +//@@author A0121501E /** * Represents a command which changes to the model can be undone using the undo command * and redone using the redo command */ -//@@author A0121501E public abstract class UndoableCommand extends Command { public enum CommandState {UNDOABLE, REDOABLE, PRE_EXECUTION}; @@ -14,6 +14,9 @@ public enum CommandState {UNDOABLE, REDOABLE, PRE_EXECUTION}; public CommandState commandState = CommandState.PRE_EXECUTION; public abstract CommandResult unexecute(); + /** + * Re-executes the command which was previously undone. + */ public CommandResult reExecute() { if (getCommandState()!=CommandState.REDOABLE){ return new CommandResult(MESSAGE_UNDO_FAIL); diff --git a/src/main/java/seedu/address/logic/commands/UndoableCommandHistory.java b/src/main/java/seedu/address/logic/commands/UndoableCommandHistory.java index a18dae506798..8d3a99d30ac3 100644 --- a/src/main/java/seedu/address/logic/commands/UndoableCommandHistory.java +++ b/src/main/java/seedu/address/logic/commands/UndoableCommandHistory.java @@ -3,10 +3,10 @@ import java.util.ArrayDeque; import java.util.Deque; +//@@author A0121501E /** * Stack of successfully executed undoable commands. */ -//@@author A0121501E public class UndoableCommandHistory { Deque commandInternalUndoQueue = new ArrayDeque(); Deque commandInternalRedoQueue = new ArrayDeque(); diff --git a/src/main/java/seedu/address/logic/commands/UnmarkCommand.java b/src/main/java/seedu/address/logic/commands/UnmarkCommand.java index 41ee4a52cb21..2dd0f11ef3e9 100644 --- a/src/main/java/seedu/address/logic/commands/UnmarkCommand.java +++ b/src/main/java/seedu/address/logic/commands/UnmarkCommand.java @@ -9,11 +9,14 @@ import seedu.address.model.task.UniqueTaskList.EntryNotFoundException; //@@author A0121501E +/** + * Unmarks an entry. + */ public class UnmarkCommand extends UndoableCommand { public static final String COMMAND_WORD = "unmark"; public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Unmarks the entry as completed. " + + ": Unmarks the entry. " + "Identified by the index number used in the last entry listing.\n" + "Parameters: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; diff --git a/src/main/java/seedu/address/logic/commands/UntagCommand.java b/src/main/java/seedu/address/logic/commands/UntagCommand.java index 07307b9732f2..43767852b477 100644 --- a/src/main/java/seedu/address/logic/commands/UntagCommand.java +++ b/src/main/java/seedu/address/logic/commands/UntagCommand.java @@ -13,10 +13,10 @@ import seedu.address.model.tag.Tag; import seedu.address.model.tag.UniqueTagList; +//@@author A0121501E /* * Remove tags from an entry. */ -//@@author A0121501E public class UntagCommand extends UndoableCommand { public static final String COMMAND_WORD = "untag"; diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java index 7720f38f488d..f37a6ed5342b 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/seedu/address/logic/LogicManagerTest.java @@ -440,7 +440,7 @@ public void execute_tagInvalidTagFormat_errorMessageShown() throws Exception { assertCommandBehavior("tag 1 " + TAG_FLAG + "**", expectedMessage, expectedAB, expectedList); } - + //@@author A0121501E @Test public void execute_tag_withoutTagnameArgs_errorMessageShown() throws Exception { TestDataHelper helper = new TestDataHelper(); @@ -1101,7 +1101,7 @@ public void execute_redoClear_successful() throws Exception { expectedAB, expectedAB.getTaskList()); } - + //@@author /** * A utility class to generate test data. */