diff --git a/collated/main/A0092382A.md b/collated/main/A0092382A.md new file mode 100644 index 000000000000..d8e30e1ea2ef --- /dev/null +++ b/collated/main/A0092382A.md @@ -0,0 +1,380 @@ +# A0092382A +###### \java\seedu\todo\commons\core\TaskViewFilter.java +``` java +public class TaskViewFilter { + private static final Comparator CHRONOLOGICAL = (a, b) -> ComparisonChain.start() + .compare(a.getEndTime().orElse(null), b.getEndTime().orElse(null), Ordering.natural().nullsLast()) + .result(); + + private static final Comparator LAST_UPDATED = (a, b) -> + b.getCreatedAt().compareTo(a.getCreatedAt()); + + public static final TaskViewFilter DEFAULT = new TaskViewFilter("all", + null, LAST_UPDATED); + + public static final TaskViewFilter INCOMPLETE = new TaskViewFilter("incomplete", + task -> !task.isCompleted(), CHRONOLOGICAL); + + public static final TaskViewFilter DUE_SOON = new TaskViewFilter("due soon", + task -> !task.isCompleted() && !task.isEvent() && task.getEndTime().isPresent(), CHRONOLOGICAL); + + public static final TaskViewFilter EVENTS = new TaskViewFilter("events", + ImmutableTask::isEvent, CHRONOLOGICAL); + + public static final TaskViewFilter COMPLETED = new TaskViewFilter("completed", + ImmutableTask::isCompleted, LAST_UPDATED); + + public final String name; + + public final Predicate filter; + + public final Comparator sort; + + public final int shortcutCharPosition; + + public TaskViewFilter(String name, Predicate filter, Comparator sort) { + this(name, filter, sort, 0); + } + + public TaskViewFilter(String name, Predicate filter, Comparator sort, int underlineCharPosition) { + this.name = name; + this.filter = filter; + this.sort = sort; + this.shortcutCharPosition = underlineCharPosition; + } + + public static TaskViewFilter[] all() { + return new TaskViewFilter[]{ + DEFAULT, COMPLETED, INCOMPLETE, EVENTS, DUE_SOON, + }; + } + + @Override + public String toString() { + return name; + } +} +``` +###### \java\seedu\todo\commons\util\TimeUtil.java +``` java + public boolean isOngoing(LocalDateTime startTime, LocalDateTime endTime) { + if (endTime == null) { + logger.log(Level.WARNING, "endTime in isOngoing(..., ...) is null."); + return false; + } + + if (startTime == null) { + logger.log(Level.WARNING, "startTime in isOngoing(..., ...) is null"); + return false; + } + + return LocalDateTime.now(clock).isAfter(startTime) && LocalDateTime.now(clock).isBefore(endTime); + } + +``` +###### \java\seedu\todo\logic\commands\CompleteCommand.java +``` java +public class CompleteCommand extends BaseCommand { + private static final String VERB_COMPLETE = "marked complete"; + private static final String VERB_INCOMPLETE = "marked incomplete"; + + private Argument index = new IntArgument("index"); + + private Argument updateAllFlag = new StringArgument("all").flag("all"); + + @Override + protected Parameter[] getArguments() { + return new Parameter[] { index, updateAllFlag }; + } + + @Override + public String getCommandName() { + return "complete"; + } + + @Override + protected void validateArguments() { + if (updateAllFlag.hasBoundValue() && index.hasBoundValue()) { + errors.put("You must either specify an index or an /all flag, not both!"); + } else if (!index.hasBoundValue() && !updateAllFlag.hasBoundValue()) { + errors.put("You must specify an index or a /all flag. You have specified none!"); + } + } + + @Override + public List getCommandSummary() { + return ImmutableList.of(new CommandSummary("Mark task as completed", getCommandName(), getArgumentSummary())); + } + + @Override + public CommandResult execute() throws ValidationException { + if (index.hasBoundValue()) { + ImmutableTask task = this.model.update(index.getValue(), t -> t.setCompleted(!t.isCompleted())); + eventBus.post(new HighlightTaskEvent(task)); + String feedback = task.isCompleted() ? CompleteCommand.VERB_COMPLETE : CompleteCommand.VERB_INCOMPLETE; + return taskSuccessfulResult(task.getTitle(), feedback); + } else { + this.model.updateAll(t -> t.setCompleted(true)); + return new CommandResult("All tasks marked as completed"); + } + } + +} +``` +###### \java\seedu\todo\logic\commands\EditCommand.java +``` java +public class EditCommand extends BaseCommand { + private static final String VERB = "edited"; + + // These parameters will be sorted out manually by overriding setPositionalArgument + private Argument index = new IntArgument("index").required(); + private Argument title = new StringArgument("title"); + + private Argument description = new StringArgument("description") + .flag("m"); + + private Argument pin = new FlagArgument("pin") + .flag("p"); + + private Argument location = new StringArgument("location") + .flag("l"); + + private Argument date = new DateRangeArgument("date") + .flag("d"); + + @Override + protected Parameter[] getArguments() { + return new Parameter[] { index, title, date, description, pin, location }; + } + + @Override + public String getCommandName() { + return "edit"; + } + + @Override + public List getCommandSummary() { + return ImmutableList.of(new CommandSummary("Edit task", getCommandName(), + getArgumentSummary())); + + } + + @Override + protected void setPositionalArgument(String argument) { + String[] tokens = argument.trim().split("\\s+", 2); + Parameter[] positionals = new Parameter[]{ index, title }; + + for (int i = 0; i < tokens.length; i++) { + try { + positionals[i].setValue(tokens[i].trim()); + } catch (IllegalValueException e) { + errors.put(positionals[i].getName(), e.getMessage()); + } + } + } + + @Override + public CommandResult execute() throws ValidationException { + ImmutableTask editedTask = this.model.update(index.getValue(), task -> { + if (title.hasBoundValue()) { + task.setTitle(title.getValue()); + } + + if (description.hasBoundValue()) { + task.setDescription(description.getValue()); + } + + if (pin.hasBoundValue()) { + task.setPinned(pin.getValue()); + } + + if (location.hasBoundValue()) { + task.setLocation(location.getValue()); + } + + if (date.hasBoundValue()) { + task.setStartTime(date.getValue().getStartTime()); + task.setEndTime(date.getValue().getEndTime()); + } + }); + eventBus.post(new HighlightTaskEvent(editedTask)); + if (description.hasBoundValue()) { + eventBus.post(new ExpandCollapseTaskEvent(editedTask)); + } + return taskSuccessfulResult(editedTask.getTitle(), EditCommand.VERB); + } + +} +``` +###### \java\seedu\todo\logic\commands\PinCommand.java +``` java +public class PinCommand extends BaseCommand { + static private final String PIN = "pinned"; + static private final String UNPIN = "unpinned"; + + private Argument index = new IntArgument("index").required(); + + @Override + protected Parameter[] getArguments() { + return new Parameter[]{ index }; + } + + @Override + public String getCommandName() { + return "pin"; + } + + @Override + public List getCommandSummary() { + return ImmutableList.of(new CommandSummary("Pin task to top of list", getCommandName(), + getArgumentSummary())); + } + + @Override + public CommandResult execute() throws ValidationException { + ImmutableTask task = this.model.update(index.getValue(), t -> t.setPinned(!t.isPinned())); + String verb = task.isPinned() ? PinCommand.PIN : PinCommand.UNPIN; + eventBus.post(new HighlightTaskEvent(task)); + return taskSuccessfulResult(task.getTitle(), verb); + } + +} +``` +###### \java\seedu\todo\logic\commands\ViewCommand.java +``` java +public class ViewCommand extends BaseCommand { + private static final String FEEDBACK_FORMAT = "Displaying %s view"; + + private Argument view = new StringArgument("view").required(); + + private TaskViewFilter viewSpecified; + + @Override + protected Parameter[] getArguments() { + return new Parameter[]{ view }; + } + + @Override + public String getCommandName() { + return "view"; + } + + @Override + public List getCommandSummary() { + return ImmutableList.of(new CommandSummary("Switch tabs", getCommandName(), getArgumentSummary())); + } + + @Override + protected void validateArguments(){ + TaskViewFilter[] viewArray = TaskViewFilter.all(); + String viewSpecified = view.getValue().trim().toLowerCase(); + + for (TaskViewFilter filter : viewArray) { + String viewName = filter.name; + char shortcut = viewName.charAt(filter.shortcutCharPosition); + boolean matchesShortcut = viewSpecified.length() == 1 && viewSpecified.charAt(0) == shortcut; + + if (viewName.contentEquals(viewSpecified) || matchesShortcut) { + this.viewSpecified = filter; + return; + } + } + + String error = String.format("The view %s does not exist", view.getValue()); + errors.put("view", error); + } + + @Override + public CommandResult execute() throws ValidationException { + model.view(viewSpecified); + String feedback = String.format(ViewCommand.FEEDBACK_FORMAT, viewSpecified); + return new CommandResult(feedback); + } + +} +``` +###### \java\seedu\todo\model\TodoList.java +``` java + @Override + public void updateAll(List indexes, Consumer update) throws ValidationException { + for (Integer x: indexes) { + MutableTask task = tasks.get(x); + ValidationTask validationTask = new ValidationTask(task); + update.accept(validationTask); + validationTask.validate(); + } + + for (Integer i : indexes) { + MutableTask task = tasks.get(i); + update.accept(task); + } + + saveTodoList(); + + } + +``` +###### \java\seedu\todo\model\TodoListModel.java +``` java + /** + * Carries out the specified update in the fields of all visible tasks. Mutation of all {@link Task} + * objects should only be done in the update lambda. The lambda takes in a single parameter, + * a {@link MutableTask}, and does not expect any return value, as per the {@link update} command. Note that + * the 'All' in this case refers to all the indices specified by the accompanying list of indices. + * + *
todo.updateAll (List tasks, t -> {
+     *     t.setEndTime(t.getEndTime.get().plusHours(2)); // Push deadline of all specified tasks back by 2h
+     *     t.setPin(true); // Pin all tasks specified
+     * });
+ * + * @throws ValidationException if any updates on any of the task objects are considered invalid + */ + void updateAll(List indexes, Consumer update) throws ValidationException; + +``` +###### \java\seedu\todo\model\TodoModel.java +``` java + @Override + public void updateAll(Consumer update) throws ValidationException { + saveUndoState(); + Map uuidMap = new HashMap<>(); + for (int i = 0; i < tasks.size(); i++) { + uuidMap.put(tasks.get(i).getUUID(), i); + } + List indexes = new ArrayList<>(); + for (ImmutableTask task : getObservableList()) { + indexes.add(uuidMap.get(task.getUUID())); + } + todoList.updateAll(indexes, update); + } + +``` +###### \resources\style\DefaultStyle.css +``` css +/*Ongoing*/ +.ongoing { + -fx-background-color: #388E3C; +} + +.ongoing .label { + -fx-text-fill: #FFFFFF; +} + +.ongoing .roundLabel { + -fx-background-color: #FFFFFF; + -fx-text-fill: #388E3C; +} + +.ongoing .pinImage { + -fx-image: url("../images/star_white.png"); +} + +.ongoing .dateImage { + -fx-image: url("../images/clock_white.png"); +} + +.ongoing .locationImage { + -fx-image: url("../images/location_white.png"); +} + +``` diff --git a/collated/main/A0135805H.md b/collated/main/A0135805H.md new file mode 100644 index 000000000000..116aab6d4c67 --- /dev/null +++ b/collated/main/A0135805H.md @@ -0,0 +1,2256 @@ +# A0135805H +###### \java\seedu\todo\commons\events\ui\ExpandCollapseTaskEvent.java +``` java +/** + * An event to tell the Ui to collapse or expand a given task in the to-do list. + */ +public class ExpandCollapseTaskEvent extends BaseEvent{ + + public final ImmutableTask task; + + /** + * Construct an event that tells the Ui to collapse or expend a given task in the to-do list. + * @param task a single index of the task that is matching to the Ui (index 1 to num of tasks, inclusive) + */ + public ExpandCollapseTaskEvent(ImmutableTask task) { + this.task = task; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } +} +``` +###### \java\seedu\todo\commons\util\StringUtil.java +``` java + /** + * Partitions the string into three parts: + * string[0 .. position - 1], string[position], string[position + 1 .. length - 1], all index inclusive + * @param string to be partitioned + * @param position location where the string should be partitioned + * @return a String array containing the three elements stated above, + * where each element must not be null, but can have empty string. + */ + public static String[] partitionStringAtPosition(String string, int position) { + String[] stringArray = new String[3]; + if (string == null || string.isEmpty() || position < 0 || position >= string.length()) { + stringArray[0] = ""; + stringArray[1] = ""; + stringArray[2] = ""; + } else { + stringArray[0] = string.substring(0, position); + stringArray[1] = string.substring(position, position + 1); + stringArray[2] = string.substring(position + 1, string.length()); + } + return stringArray; + } + + /** + * Splits string at only space and comma. + * @return Returns a String array with all the split components of the string. + */ + public static String[] splitString(String string) { + if (string == null || string.isEmpty()) { + return new String[0]; + } else { + return string.trim().split("([, ])+"); + } + } + + /** + * Given a string list, gets the text from the list in the following manner: + * apple, pear, pineapple + */ + public static String convertListToString(String[] stringList) { + if (stringList == null || stringList.length == 0) { + return ""; + } + StringJoiner stringJoiner = new StringJoiner(", "); + for (String string : stringList) { + stringJoiner.add(string); + } + return stringJoiner.toString(); + } + +``` +###### \java\seedu\todo\commons\util\TimeUtil.java +``` java +/** + * Utility methods that deals with time. + */ +public class TimeUtil { + + /* Constants */ + private static final Logger logger = LogsCenter.getLogger(TimeUtil.class); + + private static final String WORD_IN = "in"; + private static final String WORD_BY = "by"; + private static final String WORD_SINCE = "since"; + private static final String WORD_AGO = "ago"; + private static final String WORD_FROM = "from"; + private static final String WORD_TO = "to"; + private static final String WORD_TOMORROW = "tomorrow"; + private static final String WORD_YESTERDAY = "yesterday"; + private static final String WORD_TODAY = "today"; + private static final String WORD_TONIGHT = "tonight"; + private static final String WORD_COMMA = ","; + private static final String WORD_SPACE = " "; + + private static final String DUE_NOW = "due now"; + private static final String DUE_LESS_THAN_A_MINUTE = "in less than a minute"; + + private static final String UNIT_MINUTES = "minutes"; + private static final String VALUE_ONE_MINUTE = "1 minute"; + + private static final String FORMAT_DATE_WITH_YEAR = "d MMMM yyyy"; + private static final String FORMAT_DATE_NO_YEAR = "d MMMM"; + private static final String FORMAT_TIME = "h:mm a"; + + private static final Pattern DATE_REGEX = Pattern.compile("\\b([0123]?\\d)([/-])([01]?\\d)(?=\\2\\d{2,4}|\\s|$)"); + + /* Variables */ + protected Clock clock = Clock.systemDefaultZone(); + + /** + * Gets the task deadline expression for the UI. + * @param endTime ending time + * @return a formatted deadline String + */ + public String getTaskDeadlineText(LocalDateTime endTime) { + if (endTime == null) { + logger.log(Level.WARNING, "endTime in getTaskDeadlineText(...) is missing."); + return ""; + } + + LocalDateTime currentTime = LocalDateTime.now(clock); + if (endTime.isAfter(currentTime)) { + return getDeadlineNotOverdueText(currentTime, endTime); + } else { + return getDeadlineOverdueText(currentTime, endTime); + } + } + + /** + * Helper method of {@link #getTaskDeadlineText(LocalDateTime)} to get deadline text + * when it is still not overdue (currentTime < endTime). + * @param currentTime the time now + * @param endTime the due date and time + * @return a formatted deadline string + */ + private String getDeadlineNotOverdueText(LocalDateTime currentTime, LocalDateTime endTime) { + Duration durationCurrentToEnd = Duration.between(currentTime, endTime); + long minutesToDeadline = durationCurrentToEnd.toMinutes(); + long secondsToDeadline = durationCurrentToEnd.getSeconds(); + + StringJoiner stringJoiner = new StringJoiner(WORD_SPACE); + + if (secondsToDeadline <= 59) { + return DUE_LESS_THAN_A_MINUTE; + } else if (minutesToDeadline <= 59) { + stringJoiner.add(WORD_IN).add(getMinutesText(currentTime, endTime)); + } else { + stringJoiner.add(WORD_BY).add(getDateText(currentTime, endTime) + WORD_COMMA) + .add(getTimeText(endTime)); + } + return stringJoiner.toString(); + } + + /** + * Helper method of {@link #getTaskDeadlineText(LocalDateTime)} to get deadline text + * when it is overdue (currentTime > endTime). + * @param currentTime the time now + * @param endTime the due date and time + * @return a formatted deadline string + */ + private String getDeadlineOverdueText(LocalDateTime currentTime, LocalDateTime endTime) { + Duration durationCurrentToEnd = Duration.between(currentTime, endTime); + long minutesToDeadline = durationCurrentToEnd.toMinutes(); + long secondsToDeadline = durationCurrentToEnd.getSeconds(); + + StringJoiner stringJoiner = new StringJoiner(WORD_SPACE); + + if (secondsToDeadline >= -59) { + return DUE_NOW; + } else if (minutesToDeadline >= -59) { + stringJoiner.add(getMinutesText(currentTime, endTime)).add(WORD_AGO); + } else { + stringJoiner.add(WORD_SINCE).add(getDateText(currentTime, endTime) + WORD_COMMA) + .add(getTimeText(endTime)); + } + return stringJoiner.toString(); + } + + /** + * Gets the event date and time text for the UI + * @param startTime of the event + * @param endTime of the event + * @return a formatted event duration string + */ + public String getEventTimeText(LocalDateTime startTime, LocalDateTime endTime) { + if (startTime == null || endTime == null) { + logger.log(Level.WARNING, "Either startTime or endTime is missing in getEventTimeText(...)"); + return ""; + } else if (startTime.isAfter(endTime)) { + logger.log(Level.WARNING, "Start time is after end time in getEventTimeText(...)"); + return ""; + } + + LocalDateTime currentTime = LocalDateTime.now(clock); + StringJoiner joiner = new StringJoiner(WORD_SPACE); + if (isSameDay(startTime, endTime)) { + joiner.add(getDateText(currentTime, startTime) + WORD_COMMA) + .add(WORD_FROM).add(getTimeText(startTime)) + .add(WORD_TO).add(getTimeText(endTime)); + } else { + joiner.add(WORD_FROM).add(getDateText(currentTime, startTime) + WORD_COMMA).add(getTimeText(startTime)) + .add(WORD_TO).add(getDateText(currentTime, endTime) + WORD_COMMA).add(getTimeText(endTime)); + } + return joiner.toString(); + } + + + /** + * Gives a formatted text of the dateTime based on the current system time, and then returns one of the following: + * "Yesterday", "Today", "Tonight", "Tomorrow", + * full date without year if this year, + * full date with year if other year. + * + * @param dateTime to format the date with + * @return a formatted date text described above + */ + private String getDateText(LocalDateTime currentTime, LocalDateTime dateTime) { + if (isYesterday(currentTime, dateTime)) { + return WORD_YESTERDAY; + } else if (isTonight(currentTime, dateTime)) { + return WORD_TONIGHT; + } else if (isToday(currentTime, dateTime)) { + return WORD_TODAY; + } else if (isTomorrow(currentTime, dateTime)) { + return WORD_TOMORROW; + } else if (isSameYear(currentTime, dateTime)) { + return dateTime.format(DateTimeFormatter.ofPattern(FORMAT_DATE_NO_YEAR)); + } else { + return dateTime.format(DateTimeFormatter.ofPattern(FORMAT_DATE_WITH_YEAR)); + } + } + + /** + * Returns a formatted string of the time component of dateTime + * @param dateTime to format the time with + * @return a formatted time text (HH:MM A/PM) + */ + private String getTimeText(LocalDateTime dateTime) { + return dateTime.format(DateTimeFormatter.ofPattern(FORMAT_TIME)); + } + + /** + * Counts the number of minutes between the two dateTimes and prints out either: + * "1 minute" or "X minutes", for X != 1, X >= 0. + * @param dateTime1 the first time instance + * @param dateTime2 the other time instance + * @return a formatted string to tell number of minutes left (as above) + */ + private String getMinutesText(LocalDateTime dateTime1, LocalDateTime dateTime2) { + Duration duration = Duration.between(dateTime1, dateTime2); + long minutesToDeadline = Math.abs(duration.toMinutes()); + + if (minutesToDeadline == 1){ + return VALUE_ONE_MINUTE; + } else { + return minutesToDeadline + WORD_SPACE + UNIT_MINUTES; + } + } + + public boolean isTomorrow(LocalDateTime dateTimeToday, LocalDateTime dateTimeTomorrow) { + LocalDate dayBefore = dateTimeToday.toLocalDate(); + LocalDate dayAfter = dateTimeTomorrow.toLocalDate(); + return dayBefore.plusDays(1).equals(dayAfter); + } + + public boolean isToday(LocalDateTime dateTime1, LocalDateTime dateTime2) { + LocalDate date1 = dateTime1.toLocalDate(); + LocalDate date2 = dateTime2.toLocalDate(); + return date1.equals(date2); + } + + private boolean isTonight(LocalDateTime dateTimeToday, LocalDateTime dateTimeTonight) { + return isToday(dateTimeToday, dateTimeTonight) + && dateTimeTonight.toLocalTime().isAfter(LocalTime.of(17, 59, 59)); + } + + private boolean isYesterday(LocalDateTime dateTimeToday, LocalDateTime dateTimeYesterday) { + return isTomorrow(dateTimeYesterday, dateTimeToday); + } + + private boolean isSameDay(LocalDateTime dateTime1, LocalDateTime dateTime2) { + return dateTime1.toLocalDate().equals(dateTime2.toLocalDate()); + } + + private boolean isSameYear(LocalDateTime dateTime1, LocalDateTime dateTime2) { + return dateTime1.getYear() == dateTime2.getYear(); + } + + public boolean isOverdue(LocalDateTime endTime) { + if (endTime == null) { + logger.log(Level.WARNING, "endTime in isOverdue(...) is null."); + return false; + } + return endTime.isBefore(LocalDateTime.now(clock)); + } + +``` +###### \java\seedu\todo\logic\commands\TagCommand.java +``` java +/** + * This class handles all tagging command + */ +public class TagCommand extends BaseCommand { + /* Constants */ + private static final String VERB = "tagged"; + + private static final String ERROR_INCOMPLETE_PARAMETERS = "You have not supplied sufficient parameters to run a Tag command."; + private static final String ERROR_INPUT_INDEX_REQUIRED = "A task index is required."; + private static final String ERROR_INPUT_ADD_TAGS_REQUIRED = "A list of tags \"tag1, tag2, ...\" to add is required."; + private static final String ERROR_INPUT_DELETE_TAGS_REQUIRED = "A list of tags \"tag1, tag2, ...\" to delete is required."; + private static final String ERROR_TAGS_DUPLICATED = "You might have keyed in duplicated tag names."; + private static final String ERROR_TAGS_ILLEGAL_CHAR = "Tags may only include alphanumeric characters, including dashes and underscores."; + + private static final String SUCCESS_ADD_TAGS = " tags have been added successfully."; + private static final String SUCCESS_DELETE_TAGS = " tags have been removed successfully."; + + private static final Pattern TAG_VALIDATION_REGEX = Pattern.compile("^[\\w\\d_-]+$"); + + /* Variables */ + private Argument index = new IntArgument("index"); + + private Argument addTags = new StringArgument("/a") + .flag("a"); + + private Argument deleteTags = new StringArgument("/d") + .flag("d"); + + /* Constructor */ + /** + * Empty constructor + */ + public TagCommand() {} + + @Override + public Parameter[] getArguments() { + return new Parameter[] { + index, deleteTags, addTags + }; + } + + @Override + protected void setPositionalArgument(String argument) { + String[] tokens = argument.trim().split(" ", 2); + boolean isFirstArgNumber = StringUtil.isUnsignedInteger(tokens[0]); + + if (isFirstArgNumber) { + try { + index.setValue(tokens[0]); + } catch (IllegalValueException e) { + errors.put(index.getName(), e.getMessage()); + } + } + } + + @Override + public String getCommandName() { + return "tag"; + } + + @Override + public List getCommandSummary() { + String addNewTagsFromTaskArgument = index.getName() + " /a tag1 [, tag2, ...]"; + String deleteTagsFromTaskArgument = "[" + index.getName() + "] /d tag1 [, tag2, ...]"; + + return ImmutableList.of( + new CommandSummary("Add tags to a task", getCommandName(), addNewTagsFromTaskArgument), + new CommandSummary("Delete tags from tasks", getCommandName(), deleteTagsFromTaskArgument) + ); + } + + @Override + protected void validateArguments() { + //Check if we have enough input arguments + if (!isInputParametersAvailable()) { + handleUnavailableInputParameters(); + } + + //Check arguments for add tags case + if (isAddTagsToTask()) { + String[] tagsToAdd = StringUtil.splitString(addTags.getValue()); + checkForIllegalCharInTagNames(addTags.getName(), tagsToAdd); + checkForDuplicatedTagNames(addTags.getName(), tagsToAdd); + } + + //Check arguments for delete tags case + if (isDeleteTagsFromTask()) { + String[] tagsToDelete = StringUtil.splitString(deleteTags.getValue()); + checkForDuplicatedTagNames(deleteTags.getName(), tagsToDelete); + } + super.validateArguments(); + } + + @Override + public CommandResult execute() throws ValidationException { + //Obtain values for manipulation + Integer displayedIndex = index.getValue(); + String[] tagsToAdd = StringUtil.splitString(addTags.getValue()); + String[] tagsToDelete = StringUtil.splitString(deleteTags.getValue()); + + //Performs the actual execution with the data + if (isAddTagsToTask()) { + model.addTagsToTask(displayedIndex, tagsToAdd); + return new CommandResult(StringUtil.convertListToString(tagsToAdd) + SUCCESS_ADD_TAGS); + + } else if (isDeleteTagsFromTask()) { + model.deleteTagsFromTask(displayedIndex, tagsToDelete); + return new CommandResult(StringUtil.convertListToString(tagsToDelete) + SUCCESS_DELETE_TAGS); + + } else { + //Invalid case, should not happen, as we have checked it validateArguments. + //However, for completeness, a command result is returned. + throw new ValidationException(ERROR_INCOMPLETE_PARAMETERS); + } + } + + /* Input Parameters Validation */ + /** + * Returns true if the command matches the action of adding tag(s) to a task. + */ + private boolean isAddTagsToTask() { + return index.hasBoundValue() && addTags.hasBoundValue(); + } + + /** + * Returns true if the command matches the action of deleting tag(s) from a task. + */ + private boolean isDeleteTagsFromTask() { + return index.hasBoundValue() && deleteTags.hasBoundValue(); + } + + /** + * Returns true if the command matches the action of deleting tag(s) from all tasks. + */ + private boolean isDeleteTagsFromAllTasks() { + return !index.hasBoundValue() && deleteTags.hasBoundValue(); + } + + /** + * Returns true if the correct input parameters are available. + * This method do not check validity of each input. + */ + private boolean isInputParametersAvailable() { + boolean isAddTagsToTask = isAddTagsToTask(); + boolean isDeleteTagsFromTask = isDeleteTagsFromTask(); + boolean isDeleteTagsFromAll = isDeleteTagsFromAllTasks(); + return BooleanUtils.xor(new boolean[]{isAddTagsToTask, isDeleteTagsFromTask, isDeleteTagsFromAll}); + } + + /** + * Sets error messages for insufficient input parameters, dependent on input parameters supplied. + */ + private void handleUnavailableInputParameters() { + boolean hasIndex = index.getValue() != null; + boolean hasAddTags = addTags.getValue() != null; + boolean hasDeleteTags = deleteTags.getValue() != null; + + //Validation for all inputs. + if (!hasIndex && !hasAddTags && !hasDeleteTags) { + errors.put(index.getName(), ERROR_INPUT_INDEX_REQUIRED); + errors.put(addTags.getName(), ERROR_INPUT_ADD_TAGS_REQUIRED); + errors.put(deleteTags.getName(), ERROR_INPUT_DELETE_TAGS_REQUIRED); + + } else if (!hasIndex && hasAddTags) { + errors.put(index.getName(), ERROR_INPUT_INDEX_REQUIRED); + + } else if (hasIndex && !hasAddTags && !hasDeleteTags) { + errors.put(addTags.getName(), ERROR_INPUT_ADD_TAGS_REQUIRED); + errors.put(deleteTags.getName(), ERROR_INPUT_DELETE_TAGS_REQUIRED); + } + } + + /** + * Checks if the given tag names have duplicated entries. + */ + private void checkForDuplicatedTagNames(String argumentName, String[] tagNames) { + if (!CollectionUtil.elementsAreUnique(Arrays.asList(tagNames))) { + errors.put(argumentName, ERROR_TAGS_DUPLICATED); + } + } + + /** + * Check if the given tag names are alphanumeric, which also can contain dashes and underscores. + */ + private void checkForIllegalCharInTagNames(String argumentName, String[] tagNames) { + for (String tagName : tagNames) { + if (!isValidTagName(tagName)) { + errors.put(argumentName, ERROR_TAGS_ILLEGAL_CHAR); + } + } + } + + /* Helper Methods */ + /** + * Returns true if a given string is a valid tag name (alphanumeric, can contain dashes and underscores) + * Originated from {@link Tag} + */ + private static boolean isValidTagName(String test) { + return TAG_VALIDATION_REGEX.matcher(test).matches(); + } +} +``` +###### \java\seedu\todo\model\Model.java +``` java + /** + * Adds the supplied list of tags (as tag names) to the specified task. + * + * @param index The task displayed index. + * @param tagNames The list of tag names to be added. + * @throws ValidationException when the given index is invalid, or the given tagNames contain illegal characters. + */ + void addTagsToTask(int index, String[] tagNames) throws ValidationException; + + /** + * Deletes a list of tags from the specified task. + * + * @param index The task displayed index. + * @param tagNames The list of tag names to be deleted. + * @throws ValidationException when the given index is invalid, or when there is duplicates. + */ + void deleteTagsFromTask(int index, String[] tagNames) throws ValidationException; +} +``` +###### \java\seedu\todo\model\tag\Tag.java +``` java +/** + * Represents a Tag in a task. + * + * Guarantees: immutable, only {@link UniqueTagCollection} can modify + * the package private {@link Tag#rename(String)}. + * + * However, since alphanumeric name is not critical to {@link Tag}, + * the validation is done at {@link seedu.todo.model.TodoModel} + */ +public class Tag { + /* Variables */ + //Stores a unique tag name, that is alphanumeric, and contains dashes and underscores. + private String tagName; + + /* Default Constructor */ + public Tag() {} + + /** + * Constructs a new tag with the given tag name. + * This class is intentional to be package private, so only {@link UniqueTagCollection} can construct new tags. + */ + public Tag(String name) { + this.tagName = name; + } + + /* Methods */ + /** + * Renames the tag with a {@code newName}. + * This class is intentional to be package private, so only {@link UniqueTagCollection} can rename tags. + */ + void rename(String newName) { + this.tagName = newName; + } + + /* Override Methods */ + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Tag && this.tagName.equals(((Tag) other).tagName)) // if is tag + || (other instanceof String && this.tagName.equals(other)); // if is string + //Enables string comparison for hashing. + } + + @Override + public int hashCode() { + return tagName.hashCode(); + } + + /** + * Format state as text for viewing. + */ + public String toString() { + return '[' + tagName + ']'; + } + + /* Getters */ + public String getTagName() { + return tagName; + } +} +``` +###### \java\seedu\todo\model\tag\UniqueTagCollection.java +``` java +/** + * A list of tags that enforces no nulls and uniqueness between its elements. + * Also supports minimal set of list operations for the app's features. + * + * Note: This class will disallow external access to {@link #uniqueTagsToTasksMap} so to + * maintain uniqueness of the tag names. + */ +public class UniqueTagCollection implements Iterable, UniqueTagCollectionModel { + /* Constants */ + private static final String ERROR_DATA_INTEGRITY = "Data Integrity Issue: A tag is missing from the collection."; + + /* Variables */ + private final Logger logger = Logger.getLogger(UniqueTagCollection.class.getName()); + + /* + Stores a list of tags with unique tag names. + TODO: ImmutableTask does not have consistent hashing. + TODO: So, duplicated ImmutableTask may be found in the set of each Tag. + */ + private final Map> uniqueTagsToTasksMap = new HashMap<>(); + + /** + * Constructs empty TagList. + */ + public UniqueTagCollection() {} + + /* Interfacing Methods */ + @Override + public void initialise(ObservableList globalTaskList) { + uniqueTagsToTasksMap.clear(); + globalTaskList.forEach(task -> task.getTags().forEach(tag -> associateTaskToTag(task, tag))); + } + + @Override + public Tag registerTagWithTask(ImmutableTask task, String tagName) { + Tag tag = getTagWithName(tagName, false); + associateTaskToTag(task, tag); + return tag; + } + + @Override + public Tag unregisterTagWithTask(ImmutableTask task, String tagName) { + Tag tag = getTagWithName(tagName, true); + dissociateTaskFromTag(task, tag); + return tag; + } + + @Override + public void notifyTaskDeleted(ImmutableTask task) { + task.getTags().forEach(tag -> dissociateTaskFromTag(task, tag)); + } + + @Override + public void renameTag(String originalName, String newName) { + Tag tag = getTagWithName(originalName, true); + Set setOfTasks = uniqueTagsToTasksMap.remove(tag); + tag.rename(newName); + uniqueTagsToTasksMap.put(tag, setOfTasks); + } + + /* Helper Methods */ + /** + * Links a {@code task} to the {@code tag} in the {@link #uniqueTagsToTasksMap}. + */ + private void associateTaskToTag(ImmutableTask task, Tag tag) { + Set setOfTasks = uniqueTagsToTasksMap.get(tag); + if (setOfTasks == null) { + setOfTasks = new HashSet<>(); + uniqueTagsToTasksMap.put(tag, setOfTasks); + } + setOfTasks.add(task); + } + + /** + * Removes the association between the {@code task} from the {@code tag} in + * the {@link #uniqueTagsToTasksMap}. + */ + private void dissociateTaskFromTag(ImmutableTask task, Tag tag) { + Set setOfTasks = uniqueTagsToTasksMap.get(tag); + if (setOfTasks != null) { + setOfTasks.remove(task); + } + } + + /** + * Obtains an instance of {@link Tag} with the supplied {@code tagName} from the + * {@link #uniqueTagsToTasksMap}. + * + * Note: If such an instance is not found, a new {@link Tag} instance will \be added to the + * {@link #uniqueTagsToTasksMap}. + * Note: If {@code expectAvailable} is true, logger will log an error when when we can't find the + * tag with the tag name. + * + * @param tagName The name of the {@link Tag}. + * @param expectAvailable True implies that the {@link Tag} object must be found. + * @return A {@link Tag} object that has the name {@code tagName}. + * TODO: Allow this method to have less responsibility. + */ + private Tag getTagWithName(String tagName, boolean expectAvailable) { + Optional possibleTag = uniqueTagsToTasksMap.keySet().stream() + .filter(tag -> tag.getTagName().equals(tagName)).findAny(); + + if (!possibleTag.isPresent() && expectAvailable) { + logger.warning(ERROR_DATA_INTEGRITY); + } + + Tag targetTag; + if (possibleTag.isPresent()) { + targetTag = possibleTag.get(); + } else { + targetTag = new Tag(tagName); + uniqueTagsToTasksMap.put(targetTag, new HashSet<>()); + } + return targetTag; + } + + /** + * Simply finds a tag with the tag name. + */ + private Optional findTagWithName(String tagName) { + return uniqueTagsToTasksMap.keySet().stream() + .filter(tag -> tag.getTagName().equals(tagName)).findAny(); + } + + /* Interfacing Getters */ + @Override + public List getUniqueTagList() { + return new ArrayList<>(uniqueTagsToTasksMap.keySet()); + } + + @Override + public List getTasksLinkedToTag(String tagName) { + Optional possibleTag = findTagWithName(tagName); + if (possibleTag.isPresent()) { + Set tasks = uniqueTagsToTasksMap.get(possibleTag.get()); + return new ArrayList<>(tasks); + } else { + return new ArrayList<>(); + } + } + + /* Other Override Methods */ + @Override + public Iterator iterator() { + return uniqueTagsToTasksMap.keySet().iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueTagCollection // instanceof handles nulls + && this.uniqueTagsToTasksMap.equals( + ((UniqueTagCollection) other).uniqueTagsToTasksMap)); + } + + @Override + public int hashCode() { + return uniqueTagsToTasksMap.hashCode(); + } +} +``` +###### \java\seedu\todo\model\tag\UniqueTagCollectionModel.java +``` java +/** + * An interface that spells out the available methods for maintaining a unique tag list. + */ +public interface UniqueTagCollectionModel { + + /* Model Interfacing Methods*/ + /** + * Instantiate the {@link UniqueTagCollectionModel} by extracting all the {@link Tag}s + * from the global to-do list {@code globalTaskList}. + * @param globalTaskList To extract the unique list of {@link Tag}s from. + */ + void initialise(ObservableList globalTaskList); + + /** + * Registers the given {@code task} to a {@link Tag} in the {@link UniqueTagCollectionModel}. + * + * @param task The task to be attached under the {@link Tag}. + * @param tagName The name of the {@link Tag}. + * @return Returns a {@link Tag} object with the {@code tagName} so that this tag can be added to the {@code task}. + */ + Tag registerTagWithTask(ImmutableTask task, String tagName); + + /** + * Notifies the {@link UniqueTagCollectionModel} that the given {@code task} is deleted, + * so that the {@link UniqueTagCollectionModel} can update the relations accordingly. + */ + void notifyTaskDeleted(ImmutableTask task); + + /* Tag Command Interfacing Methods */ + /** + * Unregistere the given {@code task} from the {@link Tag} in the {@link UniqueTagCollectionModel}. + * TODO: Tags not found may throw an exception. This will be implemented next time. + * + * @param task The task to be detached from the {@link Tag}. + * @param tagName The name of the {@link Tag}. + * @return Returns a {@link Tag} object with the {@code tagName} so that this tag can be removed from the {@code task}. + */ + Tag unregisterTagWithTask(ImmutableTask task, String tagName); + + /** + * Renames a {@link Tag} with the given {@code originalName} with the {@code newName} + */ + void renameTag(String originalName, String newName) throws ValidationException; + + /** + * Gets a copy of the list of tags. + */ + List getUniqueTagList(); + + /** + * Gets a copy of list of task associated with the {@link Tag} with the name {@code tagName} + */ + List getTasksLinkedToTag(String tagName); +} +``` +###### \java\seedu\todo\model\TodoModel.java +``` java + @Override + public void addTagsToTask(int index, String[] tagNames) throws ValidationException { + saveUndoState(); + update(index, mutableTask -> { + Set tagsFromTask = new HashSet<>(mutableTask.getTags()); + for (String tagName : tagNames) { + Tag newTag = uniqueTagCollection.registerTagWithTask(mutableTask, tagName); + tagsFromTask.add(newTag); + } + mutableTask.setTags(tagsFromTask); + }); + } + + @Override + public void deleteTagsFromTask(int index, String[] tagNames) throws ValidationException { + saveUndoState(); + update(index, mutableTask -> { + Set tagsFromTask = new HashSet<>(mutableTask.getTags()); + for (String tagName : tagNames) { + Tag deletedTag = uniqueTagCollection.unregisterTagWithTask(mutableTask, tagName); + tagsFromTask.remove(deletedTag); + } + mutableTask.setTags(tagsFromTask); + }); + } +} +``` +###### \java\seedu\todo\ui\controller\CommandController.java +``` java +/** + * Processes the input command from {@link CommandInputView}, pass it to {@link seedu.todo.logic.Logic} + * and hands the {@link seedu.todo.logic.commands.CommandResult} + * to {@link CommandFeedbackView} and {@link CommandErrorView} + */ +public class CommandController { + + private Logic logic; + private CommandInputView inputView; + private CommandPreviewView previewView; + private CommandFeedbackView feedbackView; + private CommandErrorView errorView; + + /** + * Defines a default constructor + */ + private CommandController() {} + + /** + * Constructs a link between the classes defined in the parameters. + */ + public static CommandController constructLink(Logic logic, + CommandInputView inputView, CommandPreviewView previewView, + CommandFeedbackView feedbackView, CommandErrorView errorView) { + CommandController controller = new CommandController(); + controller.logic = logic; + controller.inputView = inputView; + controller.previewView = previewView; + controller.feedbackView = feedbackView; + controller.errorView = errorView; + controller.start(); + return controller; + } + + /** + * Asks {@link #inputView} to start listening for a new key strokes. + * Once the callback returns a command, {@link #handleInput(KeyCode, String)} will process the input. + */ + private void start() { + inputView.listenToInput(this::handleInput); + } + +``` +###### \java\seedu\todo\ui\util\FxViewUtil.java +``` java + /** + * Hides a specified UI element, and ensures that it does not occupy any space. + */ + public static void setCollapsed(Node node, boolean isCollapsed) { + node.setVisible(!isCollapsed); + node.setManaged(!isCollapsed); + } + + /** + * Set the text to UI element when available, collapse the UI element when not. + */ + public static void displayTextWhenAvailable(Label labelToDisplay, Node nodeToHide, Optional optionalString) { + if (optionalString.isPresent()) { + labelToDisplay.setText(optionalString.get()); + } else { + labelToDisplay.setText(""); + setCollapsed(nodeToHide, true); + } + } + + /** + * Sets a recurring task on the UI specified in handler to repeat every specified seconds. + * Does not start until the user executes .play() + * @param seconds duration between each repeats + * @param handler method to run is specified here + * @return {@link Timeline} object to run. + */ + public static Timeline setRecurringUiTask(int seconds, EventHandler handler) { + Timeline recurringTask = new Timeline(new KeyFrame(Duration.seconds(seconds), handler)); + recurringTask.setCycleCount(Timeline.INDEFINITE); + return recurringTask; + } + + /** + * Converts an index from a list to the index that is displayed to the user via the Ui + */ + public static int convertToListIndex(int uiIndex) { + return uiIndex - 1; + } + + /** + * Converts an index displayed on the Ui to the user, to the index used on the list + */ + public static int convertToUiIndex(int listIndex) { + return listIndex + 1; + } +} +``` +###### \java\seedu\todo\ui\util\UiPartLoaderUtil.java +``` java + /** + * Attaches only one children view element to a specified placeholder. + * However, if either one of the params is null, it will result in no-op. + * Also, if there are any other children in the placeholder, they will be cleared first. + * + * @param placeholder to add the childrenView to, no-op if null + * @param childrenView to be attached to the placeholder, no-op if null + */ + private static void attachToPlaceholder(AnchorPane placeholder, Node childrenView) { + if (placeholder != null && childrenView != null) { + ObservableList placeholderChildren = placeholder.getChildren(); + placeholderChildren.clear(); + placeholderChildren.add(childrenView); + } + } +} +``` +###### \java\seedu\todo\ui\util\ViewGeneratorUtil.java +``` java +/** + * A utility class that generates commonly used UI elements, such as Labels. + */ +public class ViewGeneratorUtil { + + /** + * Generates a {@link Text} object with the class style applied onto the object. + * @param string to be wrapped in the {@link Text} object + * @param classStyle css style to be applied to the label + * @return a {@link Text} object + */ + public static Text constructText(String string, String classStyle) { + Text text = new Text(string); + ViewStyleUtil.addClassStyles(text, classStyle); + return text; + } + + /** + * Constructs a label view with a dark grey rounded background. + */ + public static Label constructRoundedText(String string) { + Label label = constructLabel(string, "roundLabel"); + label.setPadding(new Insets(0, 8, 0, 8)); + return label; + } + + /** + * Generates a {@link Label} object with the class style applied onto the object. + * @param string to be wrapped in the {@link Label} object + * @param classStyle css style to be applied to the label + * @return a {@link Label} object + */ + public static Label constructLabel(String string, String classStyle) { + Label label = new Label(string); + ViewStyleUtil.addClassStyles(label, classStyle); + return label; + } + + /** + * Place all the specified texts into a {@link TextFlow} object. + */ + public static TextFlow placeIntoTextFlow(Text... texts) { + return new TextFlow(texts); + } +} +``` +###### \java\seedu\todo\ui\util\ViewStyleUtil.java +``` java +/** + * Deals with the CSS styling of View elements + */ +public class ViewStyleUtil { + + /* Style Classes Constants */ + public static final String STYLE_COLLAPSED = "collapsed"; + public static final String STYLE_COMPLETED = "completed"; + public static final String STYLE_OVERDUE = "overdue"; + public static final String STYLE_SELECTED = "selected"; + public static final String STYLE_TEXT_4 = "text4"; + public static final String STYLE_ERROR = "error"; + public static final String STYLE_CODE = "code"; + public static final String STYLE_BOLDER = "bolder"; + public static final String STYLE_UNDERLINE = "underline"; + public static final String STYLE_ONGOING = "ongoing"; + + /*Static Helper Methods*/ + /** + * Adds only one instance of all the class styles to the node object + * @param node view object to add the class styles to + * @param classStyles all the class styles that is to be added to the node + */ + public static void addClassStyles(Node node, String... classStyles) { + for (String classStyle : classStyles) { + addClassStyle(node, classStyle); + } + } + + /** + * Remove all instance of all the class styles to the node object + * @param node view object to add the class styles to + * @param classStyles all the class styles that is to be removed from the node + */ + public static void removeClassStyles(Node node, String... classStyles) { + for (String classStyle : classStyles) { + removeClassStyle(node, classStyle); + } + } + + /** + * Adds or removes class style based on a boolean parameter + * @param isAdding true to add, false to remove + * @param node view object to add the class styles to + * @param classStyles all the class styles that is to be added to/removed from the node + */ + public static void addRemoveClassStyles(boolean isAdding, Node node, String... classStyles) { + for (String classStyle : classStyles) { + if (isAdding) { + addClassStyles(node, classStyle); + } else { + removeClassStyles(node, classStyle); + } + } + } + + /** + * Toggles one style class to the node: + * If supplied style class is available, remove it. + * Else, add one instance of it. + * @return true if toggled from OFF -> ON + */ + public static boolean toggleClassStyle(Node node, String classStyle) { + boolean wasPreviouslyOff = !node.getStyleClass().contains(classStyle); + if (wasPreviouslyOff) { + addClassStyles(node, classStyle); + } else { + removeClassStyles(node, classStyle); + } + return wasPreviouslyOff; + } + + /* Private Helper Methods */ + /** + * Adds only one instance of a single class style to the node object + */ + private static void addClassStyle(Node node, String classStyle) { + if (!node.getStyleClass().contains(classStyle)) { + node.getStyleClass().add(classStyle); + } + } + + /** + * Removes all instances of a single class style from the node object + */ + private static void removeClassStyle(Node node, String classStyle) { + while (node.getStyleClass().contains(classStyle)) { + node.getStyleClass().remove(classStyle); + } + } +} +``` +###### \java\seedu\todo\ui\view\CommandErrorView.java +``` java +/** + * A view class that displays specific command errors in greater detail. + */ +public class CommandErrorView extends UiPart { + + private final Logger logger = LogsCenter.getLogger(CommandFeedbackView.class); + private static final String FXML = "CommandErrorView.fxml"; + + private AnchorPane placeholder; + private VBox errorViewBox; + @FXML private VBox nonFieldErrorBox; + @FXML private VBox fieldErrorBox; + @FXML private GridPane nonFieldErrorGrid; + @FXML private GridPane fieldErrorGrid; + + /** + * Loads and initialise the feedback view element to the placeHolder + * @param primaryStage of the application + * @param placeHolder where the view element {@link #errorViewBox} should be placed + * @return an instance of this class + */ + public static CommandErrorView load(Stage primaryStage, AnchorPane placeHolder) { + CommandErrorView errorView = UiPartLoaderUtil.loadUiPart(primaryStage, placeHolder, new CommandErrorView()); + errorView.configureLayout(); + errorView.hideCommandErrorView(); + return errorView; + } + + /** + * Configure the UI layout of {@link CommandErrorView} + */ + private void configureLayout() { + FxViewUtil.applyAnchorBoundaryParameters(errorViewBox, 0.0, 0.0, 0.0, 0.0); + FxViewUtil.applyAnchorBoundaryParameters(nonFieldErrorBox, 0.0, 0.0, 0.0, 0.0); + FxViewUtil.applyAnchorBoundaryParameters(fieldErrorBox, 0.0, 0.0, 0.0, 0.0); + FxViewUtil.applyAnchorBoundaryParameters(fieldErrorGrid, 0.0, 0.0, 0.0, 0.0); + FxViewUtil.applyAnchorBoundaryParameters(nonFieldErrorGrid, 0.0, 0.0, 0.0, 0.0); + } + + /** + * Displays both field and non-field errors to the user + * @param errorBag that contains both field and non-field errors + */ + public void displayErrors(ErrorBag errorBag) { + showCommandErrorView(); + clearOldErrorsFromViews(); + displayFieldErrors(errorBag.getFieldErrors()); + displayNonFieldErrors(errorBag.getNonFieldErrors()); + } + + /** + * Feeds non field errors to the {@link #nonFieldErrorGrid}. + * If there are no non-field errors, then {@link #nonFieldErrorBox} will be hidden. + * @param nonFieldErrors that stores a list of non-field errors + */ + private void displayNonFieldErrors(List nonFieldErrors) { + if (nonFieldErrors.isEmpty()) { + hideErrorBox(nonFieldErrorBox); + } else { + int rowCounter = 0; + for (String error : nonFieldErrors) { + addRowToGrid(nonFieldErrorGrid, rowCounter++, rowCounter + ".", error); + } + } + } + + /** + * Feeds field errors to the {@link #fieldErrorGrid}. + * If there are no field errors, then {@link #fieldErrorBox} will be hidden. + * @param fieldErrors that stores the field errors + */ + private void displayFieldErrors(Map fieldErrors) { + if (fieldErrors.isEmpty()) { + hideErrorBox(fieldErrorBox); + } else { + int rowCounter = 0; + for (Map.Entry fieldError : fieldErrors.entrySet()) { + addRowToGrid(fieldErrorGrid, rowCounter++, fieldError.getKey(), fieldError.getValue()); + } + } + } + + /* Override Methods */ + @Override + public void setPlaceholder(AnchorPane placeholder) { + this.placeholder = placeholder; + } + + @Override + public void setNode(Node node) { + this.errorViewBox = (VBox) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } + + /*Helper Methods*/ + /** + * Adds a row of text to the targetGrid + * @param targetGrid to add a row of text on + * @param rowIndex which row to add this row of text + * @param leftText text for the first column + * @param rightText text for the second column + */ + private void addRowToGrid(GridPane targetGrid, int rowIndex, String leftText, String rightText) { + Label leftLabel = ViewGeneratorUtil.constructLabel(leftText, ViewStyleUtil.STYLE_TEXT_4); + Label rightLabel = ViewGeneratorUtil.constructLabel(rightText, ViewStyleUtil.STYLE_TEXT_4); + targetGrid.addRow(rowIndex, leftLabel, rightLabel); + } + + /** + * Clears all elements in the given grid. + */ + private void clearGrid(GridPane gridPane) { + gridPane.getChildren().clear(); + } + + /** + * Hides a field or non-field error box. + * @param vBox can be either {@link #fieldErrorBox} or {@link #nonFieldErrorBox} + */ + private void hideErrorBox(VBox vBox) { + FxViewUtil.setCollapsed(vBox, true); + } + + /** + * Shows a field or non-field error box. + * @param vBox can be either {@link #fieldErrorBox} or {@link #nonFieldErrorBox} + */ + private void showErrorBox(VBox vBox) { + FxViewUtil.setCollapsed(vBox, false); + } + + /** + * Hides the entire {@link CommandErrorView} + */ + public void hideCommandErrorView() { + FxViewUtil.setCollapsed(placeholder, true); + } + + /** + * Displays the entire {@link CommandErrorView} + */ + private void showCommandErrorView() { + FxViewUtil.setCollapsed(placeholder, false); + } + + /** + * Clears previous errors from the grid, and then unhide all the error boxes. + */ + private void clearOldErrorsFromViews() { + clearGrid(nonFieldErrorGrid); + clearGrid(fieldErrorGrid); + showErrorBox(nonFieldErrorBox); + showErrorBox(fieldErrorBox); + } +} +``` +###### \java\seedu\todo\ui\view\CommandFeedbackView.java +``` java +/** + * Display textual feedback to command input via this view with {@link #displayMessage(String)}. + */ +public class CommandFeedbackView extends UiPart { + /* Constants */ + private static final String FXML = "CommandFeedbackView.fxml"; + + /* Variables */ + private final Logger logger = LogsCenter.getLogger(CommandFeedbackView.class); + + /* Layout Elements */ + @FXML private Label commandFeedbackLabel; + private AnchorPane textContainer; + + /** + * Loads and initialise the feedback view element to the placeholder. + * + * @param primaryStage The main stage of the application. + * @param placeholder The place where the view element {@link #textContainer} should be placed. + * @return An instance of this class. + */ + public static CommandFeedbackView load(Stage primaryStage, AnchorPane placeholder) { + CommandFeedbackView feedbackView = UiPartLoaderUtil.loadUiPart(primaryStage, placeholder, new CommandFeedbackView()); + feedbackView.configureLayout(); + return feedbackView; + } + + /** + * Configure the UI layout of {@link CommandFeedbackView}. + */ + private void configureLayout() { + FxViewUtil.applyAnchorBoundaryParameters(textContainer, 0.0, 0.0, 0.0, 0.0); + FxViewUtil.applyAnchorBoundaryParameters(commandFeedbackLabel, 0.0, 0.0, 0.0, 0.0); + } + + /* Interfacing Methods */ + /** + * Displays a message onto the {@link #commandFeedbackLabel}. + * @param message The feedback message to be shown to the user. + */ + public void displayMessage(String message) { + commandFeedbackLabel.setText(message); + } + + /** + * Clears any message in {@link #commandFeedbackLabel}. + */ + public void clearMessage() { + commandFeedbackLabel.setText(""); + } + + /** + * Indicate an error visually on the {@link #commandFeedbackLabel}. + */ + public void flagError() { + ViewStyleUtil.addClassStyles(commandFeedbackLabel, ViewStyleUtil.STYLE_ERROR); + } + + /** + * Remove the error flag visually on the {@link #commandFeedbackLabel}. + */ + public void unFlagError() { + ViewStyleUtil.removeClassStyles(commandFeedbackLabel, ViewStyleUtil.STYLE_ERROR); + } + + /* Override Methods */ + @Override + public void setNode(Node node) { + this.textContainer = (AnchorPane) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } +} +``` +###### \java\seedu\todo\ui\view\CommandInputView.java +``` java +/** + * A view class that handles the Input text box directly. + */ +public class CommandInputView extends UiPart { + private final Logger logger = LogsCenter.getLogger(CommandInputView.class); + private static final String FXML = "CommandInputView.fxml"; + + private AnchorPane placeHolder; + private AnchorPane commandInputPane; + + @FXML + private TextArea commandTextField; + + /** + * Loads and initialise the input view element to the placeHolder + * @param primaryStage of the application + * @param placeHolder where the view element {@link #commandInputPane} should be placed + * @return an instance of this class + */ + public static CommandInputView load(Stage primaryStage, AnchorPane placeHolder) { + CommandInputView commandInputView = UiPartLoaderUtil.loadUiPart(primaryStage, placeHolder, new CommandInputView()); + commandInputView.configureLayout(); + commandInputView.configureProperties(); + return commandInputView; + } + + /** + * Configure the UI layout of {@link CommandInputView} + */ + private void configureLayout() { + FxViewUtil.applyAnchorBoundaryParameters(commandInputPane, 0.0, 0.0, 0.0, 0.0); + FxViewUtil.applyAnchorBoundaryParameters(commandTextField, 0.0, 0.0, 0.0, 0.0); + } + + /** + * Configure the UI properties of {@link CommandInputView} + */ + private void configureProperties() { + setCommandInputHeightAutoResizeable(); + unflagErrorWhileTyping(); + listenAndRaiseEnterEvent(); + } + +``` +###### \java\seedu\todo\ui\view\FilterBarView.java +``` java +/** + * Shows a row of filter categories via {@link TaskViewFilter} + * to filter the tasks in {@link TodoListView} + */ +public class FilterBarView extends UiPart { + /* Constants */ + private final Logger logger = LogsCenter.getLogger(FilterBarView.class); + private static final String FXML = "FilterBarView.fxml"; + + /* Layout Views */ + private AnchorPane placeholder; + private FlowPane filterViewPane; + + /* Variables */ + private Map taskFilterBoxesMap = new HashMap<>(); + + /* Layout Initialisation */ + /** + * Loads and initialise the {@link #filterViewPane} to the {@link seedu.todo.ui.MainWindow} + * @param primaryStage of the application + * @param placeholder where the view element {@link #filterViewPane} should be placed + * @return an instance of this class + */ + public static FilterBarView load(Stage primaryStage, AnchorPane placeholder, ObservableValue filter) { + FilterBarView filterView = UiPartLoaderUtil.loadUiPart(primaryStage, placeholder, new FilterBarView()); + filterView.configureLayout(); + filterView.configureProperties(); + filterView.bindListener(filter); + return filterView; + } + + /** + * Configure the UI layout of {@link FilterBarView} + */ + private void configureLayout() { + FxViewUtil.applyAnchorBoundaryParameters(filterViewPane, 0.0, 0.0, 0.0, 0.0); + } + + /** + * Initialise and configure the UI properties of {@link FilterBarView} + */ + private void configureProperties() { + initialiseAllViewFilters(); + selectOneViewFilter(TaskViewFilter.DEFAULT); + } + + /** + * Display all the {@link TaskViewFilter} on the {@link #filterViewPane} + */ + private void initialiseAllViewFilters() { + for (TaskViewFilter filter : TaskViewFilter.all()) { + appendEachViewFilter(filter); + } + } + + /** + * Add one {@link TaskViewFilter} on the {@link #filterViewPane} + * and save an instance to the {@link #taskFilterBoxesMap} + * @param filter to add onto the pane + */ + private void appendEachViewFilter(TaskViewFilter filter) { + HBox textContainer = constructViewFilterBox(filter); + taskFilterBoxesMap.put(filter, textContainer); + filterViewPane.getChildren().add(textContainer); + } + + /** + * Given a filter, construct a view element to be displayed on the {@link #filterViewPane} + * @param filter to be displayed + * @return a view element + */ + private HBox constructViewFilterBox(TaskViewFilter filter) { + String filterName = WordUtils.capitalize(filter.name); + String[] partitionedText = StringUtil.partitionStringAtPosition(filterName, filter.shortcutCharPosition); + + Label leftText = new Label(partitionedText[0]); + Label centreText = new Label(partitionedText[1]); + Label rightText = new Label(partitionedText[2]); + ViewStyleUtil.addClassStyles(centreText, ViewStyleUtil.STYLE_UNDERLINE); + + HBox textContainer = new HBox(); + textContainer.getChildren().add(leftText); + textContainer.getChildren().add(centreText); + textContainer.getChildren().add(rightText); + return textContainer; + } + + /** + * Binds this component with the {@link TaskViewFilter} property it listens to + */ + private void bindListener(ObservableValue filter) { + filter.addListener((observable, oldValue, newValue) -> selectOneViewFilter(newValue)); + } + + /** + * Select exactly one filter from {@link #filterViewPane} + */ + public void selectOneViewFilter(TaskViewFilter filter) { + clearAllViewFiltersSelection(); + selectViewFilter(filter); + } + + /* Helper Methods */ + /** + * Clears all selection from the {@link #filterViewPane} + */ + private void clearAllViewFiltersSelection() { + for (HBox filterBox : taskFilterBoxesMap.values()) { + ViewStyleUtil.removeClassStyles(filterBox, ViewStyleUtil.STYLE_SELECTED); + } + } + + /** + * Mark the filter as selected on {@link #filterViewPane} + * However, if filter is null, nothing is done. + */ + private void selectViewFilter(TaskViewFilter filter) { + if (filter != null) { + HBox filterBox = taskFilterBoxesMap.get(filter); + ViewStyleUtil.addClassStyles(filterBox, ViewStyleUtil.STYLE_SELECTED); + } + } + + /* Override Methods */ + @Override + public void setPlaceholder(AnchorPane placeholder) { + this.placeholder = placeholder; + } + + @Override + public void setNode(Node node) { + this.filterViewPane = (FlowPane) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } +} +``` +###### \java\seedu\todo\ui\view\HelpView.java +``` java +/** + * A view that displays all the help commands in a single view. + */ +public class HelpView extends UiPart { + + private final Logger logger = LogsCenter.getLogger(HelpView.class); + private static final String FXML = "HelpView.fxml"; + + /*Layouts*/ + private AnchorPane placeholder; + private VBox helpPanelView; + + @FXML + private GridPane helpGrid; + + /** + * Loads and initialise the feedback view element to the placeHolder + * @param primaryStage of the application + * @param placeholder where the view element {@link #helpPanelView} should be placed + * @return an instance of this class + */ + public static HelpView load(Stage primaryStage, AnchorPane placeholder) { + HelpView helpView = UiPartLoaderUtil.loadUiPart(primaryStage, placeholder, new HelpView()); + helpView.configureLayout(); + helpView.hideHelpPanel(); + return helpView; + } + + /** + * Configure the UI layout of {@link CommandErrorView} + */ + private void configureLayout() { + FxViewUtil.applyAnchorBoundaryParameters(helpPanelView, 0.0, 0.0, 0.0, 0.0); + FxViewUtil.applyAnchorBoundaryParameters(helpGrid, 0.0, 0.0, 0.0, 0.0); + } + + /** + * Displays a list of commands into the helpPanelView + */ + public void displayCommandSummaries(List commandSummaries) { + this.showHelpPanel(); + helpGrid.getChildren().clear(); + int rowIndex = 0; + for (CommandSummary commandSummary : commandSummaries) { + appendCommandSummary(rowIndex++, commandSummary); + } + } + + /** + * Add a command summary to each row of the helpGrid + * @param rowIndex the row number to which the command summary should append to + * @param commandSummary to be displayed + */ + private void appendCommandSummary(int rowIndex, CommandSummary commandSummary) { + Text commandScenario = ViewGeneratorUtil.constructText(commandSummary.scenario, ViewStyleUtil.STYLE_TEXT_4); + Text commandName = ViewGeneratorUtil.constructText(commandSummary.command, ViewStyleUtil.STYLE_TEXT_4); + Text commandArgument = ViewGeneratorUtil.constructText(" " + commandSummary.arguments, ViewStyleUtil.STYLE_TEXT_4); + + ViewStyleUtil.addClassStyles(commandArgument, ViewStyleUtil.STYLE_CODE); + ViewStyleUtil.addClassStyles(commandName, ViewStyleUtil.STYLE_CODE, ViewStyleUtil.STYLE_BOLDER); + + TextFlow combinedCommand = ViewGeneratorUtil.placeIntoTextFlow(commandName, commandArgument); + helpGrid.addRow(rowIndex, commandScenario, combinedCommand); + } + + /* Ui Methods */ + public void hideHelpPanel() { + FxViewUtil.setCollapsed(helpPanelView, true); + } + + private void showHelpPanel() { + FxViewUtil.setCollapsed(helpPanelView, false); + } + + + /* Override Methods */ + @Override + public void setPlaceholder(AnchorPane placeholder) { + this.placeholder = placeholder; + } + + @Override + public void setNode(Node node) { + this.helpPanelView = (VBox) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } +} +``` +###### \java\seedu\todo\ui\view\TaskCardView.java +``` java +/** + * This class links up with TaskCardView.fxml layout to display details of a given ReadOnlyTask to users via the TaskListPanel.fxml. + */ +public class TaskCardView extends UiPart { + /*Constants*/ + private static final String FXML = "TaskCardView.fxml"; + + private static final String TASK_TYPE = "Task"; + private static final String EVENT_TYPE = "Event"; + + /*Static Field*/ + /* + Provides a global reference between an ImmutableTask to the wrapper TaskCardView class, + since we have no direct access of TaskCardView from the ListView object. + */ + private static final Map taskCardMap = new HashMap<>(); + + /*Layout Declarations*/ + @FXML + private VBox taskCard; + @FXML + private ImageView pinImage; + @FXML + private Label titleLabel; + @FXML + private Label typeLabel, moreInfoLabel; + @FXML + private Label descriptionLabel, dateLabel, locationLabel; + @FXML + private HBox descriptionBox, dateBox, locationBox; + @FXML + private FlowPane tagsBox; + + /* Variables */ + private ImmutableTask task; + private int displayedIndex; + private TimeUtil timeUtil = new TimeUtil(); + + /* Default Constructor */ + private TaskCardView(){ + } + + /* Initialisation Methods */ + /** + * Loads and initialise one cell of the task in the to-do list ListView. + * @param task to be displayed on the cell + * @param displayedIndex index to be displayed on the card itself to the user + * @return an instance of this class + */ + public static TaskCardView load(ImmutableTask task, int displayedIndex){ + TaskCardView taskListCard = new TaskCardView(); + taskListCard.task = task; + taskListCard.displayedIndex = displayedIndex; + taskCardMap.put(task, taskListCard); + return UiPartLoaderUtil.loadUiPart(taskListCard); + } + + /** + * Initialise all the view elements in a task card. + */ + @FXML + public void initialize() { + displayEverythingElse(); + displayTags(); + displayTimings(); + setStyle(); + setTimingAutoUpdate(); + initialiseCollapsibleView(); + } + + /** + * Displays all other view elements, including title, type label, pin image, description and location texts. + */ + private void displayEverythingElse() { + titleLabel.setText(String.valueOf(displayedIndex) + ". " + task.getTitle()); + pinImage.setVisible(task.isPinned()); + typeLabel.setText(task.isEvent() ? EVENT_TYPE : TASK_TYPE); + FxViewUtil.displayTextWhenAvailable(descriptionLabel, descriptionBox, task.getDescription()); + FxViewUtil.displayTextWhenAvailable(locationLabel, locationBox, task.getLocation()); + } + + /** + * Displays the tags in lexicographical order, ignoring case. + */ + private void displayTags(){ + List tagList = new ArrayList<>(task.getTags()); + if (tagList.isEmpty()) { + FxViewUtil.setCollapsed(tagsBox, true); + } else { + tagList.sort((o1, o2) -> o1.toString().compareToIgnoreCase(o2.toString())); + for (Tag tag : tagList) { + Label tagLabel = ViewGeneratorUtil.constructRoundedText(tag.getTagName()); + tagsBox.getChildren().add(tagLabel); + } + } + } + + /** + * Sets style according to the status (e.g. completed, overdue, etc) of the task. + */ + private void setStyle() { + boolean isCompleted = task.isCompleted(); + boolean isOverdue = task.getEndTime().isPresent() && timeUtil.isOverdue(task.getEndTime().get()) && !task.isEvent(); + boolean isOngoing = task.isEvent() && + timeUtil.isOngoing(task.getStartTime().get(), task.getEndTime().get()); + + if (isCompleted) { + ViewStyleUtil.addClassStyles(taskCard, ViewStyleUtil.STYLE_COMPLETED); + } else if (isOverdue) { + ViewStyleUtil.addClassStyles(taskCard, ViewStyleUtil.STYLE_OVERDUE); + } else if (isOngoing){ + ViewStyleUtil.addClassStyles(taskCard, ViewStyleUtil.STYLE_ONGOING); + } + } + + /** + * Initialise the view to show collapsed state if it can be collapsed, + * else hide the {@link #moreInfoLabel} otherwise. + */ + private void initialiseCollapsibleView() { + ViewStyleUtil.addRemoveClassStyles(true, taskCard, ViewStyleUtil.STYLE_COLLAPSED); + FxViewUtil.setCollapsed(moreInfoLabel, !isTaskCollapsible()); + FxViewUtil.setCollapsed(tagsBox, isTaskCollapsible()); + } + + /** + * Displays formatted task or event timings in the time field. + */ + private void displayTimings() { + String displayTimingOutput; + Optional startTime = task.getStartTime(); + Optional endTime = task.getEndTime(); + boolean isEventWithTime = task.isEvent() && startTime.isPresent() && endTime.isPresent(); + boolean isTaskWithTime = !task.isEvent() && endTime.isPresent(); + + if (isEventWithTime) { + displayTimingOutput = timeUtil.getEventTimeText(startTime.get(), endTime.get()); + } else if (isTaskWithTime) { + displayTimingOutput = timeUtil.getTaskDeadlineText(endTime.get()); + } else { + FxViewUtil.setCollapsed(dateBox, true); + return; + } + dateLabel.setText(displayTimingOutput); + } + + /** + * Allows timing, and deadline highlight style to be updated automatically. + */ + private void setTimingAutoUpdate() { + Timeline timeline = FxViewUtil.setRecurringUiTask(30, event -> { + displayTimings(); + setStyle(); + }); + timeline.play(); + } + + /* Methods interfacing with UiManager*/ + /** + * Toggles the task card's collapsed or expanded state, only if this card is collapsible. + */ + public void toggleCardCollapsing() { + if (isTaskCollapsible()) { + //Sets both the collapsed style of the card, and mark the visibility of the "more" label. + boolean isCollapsing = ViewStyleUtil.toggleClassStyle(taskCard, ViewStyleUtil.STYLE_COLLAPSED); + FxViewUtil.setCollapsed(moreInfoLabel, !isCollapsing); + FxViewUtil.setCollapsed(tagsBox, isCollapsing); + } + } + + /** + * Displays in the Ui whether this card is selected + * @param isSelected true when the card is selected + */ + public void markAsSelected(boolean isSelected) { + ViewStyleUtil.addRemoveClassStyles(isSelected, taskCard, ViewStyleUtil.STYLE_SELECTED); + } + + /* Helper Methods */ + /** + * Returns true if this task card can be collapsed, based on the information given from the {@link ImmutableTask} + */ + private boolean isTaskCollapsible() { + boolean hasDescription = task.getDescription().isPresent(); + boolean hasTags = !task.getTags().isEmpty(); + return hasDescription || hasTags; + } + + /* Getters */ + /** + * Gets the mapped {@link TaskCardView} object from an {@link ImmutableTask} object + * @param task that is being wrapped by the {@link TaskCardView} object + * @return a {@link TaskCardView} object that contains this task (can be null if not available) + */ + public static TaskCardView getTaskCard(ImmutableTask task) { + return taskCardMap.get(task); + } + + public int getDisplayedIndex() { + return displayedIndex; + } + + public VBox getLayout() { + return taskCard; + } + + /* Override Methods */ + @Override + public void setNode(Node node) { + taskCard = (VBox) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } +} +``` +###### \java\seedu\todo\ui\view\TodoListView.java +``` java +/** + * A panel that holds all the tasks inflated from TaskCardView. + */ +public class TodoListView extends UiPart { + /*Constants*/ + private static final String FXML = "TodoListView.fxml"; + + /*Variables*/ + private final Logger logger = LogsCenter.getLogger(TodoListView.class); + private VBox panel; + private AnchorPane placeHolderPane; + + /*Layout Declarations*/ + @FXML + private ListView todoListView; + + /** + * Default Constructor for {@link TodoListView} + */ + public TodoListView() { + super(); + } + + /** + * Loads and initialise the {@link TodoListView} to the placeHolder. + * + * @param primaryStage of the application + * @param placeHolder where the view element {@link #todoListView} should be placed + * @return an instance of this class + */ + public static TodoListView load(Stage primaryStage, AnchorPane placeHolder, + ObservableList todoList) { + + TodoListView todoListView = + UiPartLoaderUtil.loadUiPart(primaryStage, placeHolder, new TodoListView()); + todoListView.configure(todoList); + return todoListView; + } + + /** + * Configures the {@link TodoListView} + * + * @param todoList A list of {@link ImmutableTask} to be displayed on this {@link #todoListView}. + */ + private void configure(ObservableList todoList) { + setConnections(todoList); + } + + /** + * Links the list of {@link ImmutableTask} to the todoListView. + * + * @param todoList A list of {@link ImmutableTask} to be displayed on this {@link #todoListView}. + */ + private void setConnections(ObservableList todoList) { + todoListView.setItems(todoList); + todoListView.setCellFactory(param -> new TodoListViewCell()); + } + + /* Ui Methods */ + /** + * Toggles the expanded/collapsed view of a task card. + * + * @param task The specific to be expanded or collapsed from view. + */ + public void toggleExpandCollapsed(ImmutableTask task) { + TaskCardView taskCardView = TaskCardView.getTaskCard(task); + if (taskCardView != null) { + taskCardView.toggleCardCollapsing(); + } + } + + /** + * Scrolls the {@link #todoListView} to the particular task card at the listIndex. + */ + public void scrollAndSelect(int listIndex) { + Platform.runLater(() -> { + todoListView.scrollTo(listIndex); + todoListView.getSelectionModel().clearAndSelect(listIndex); + }); + } + + /** + * Scrolls the {@link #todoListView} to the particular task card. + * + * @param task for the list to scroll to. + */ + public void scrollAndSelect(ImmutableTask task) { + TaskCardView taskCardView = TaskCardView.getTaskCard(task); + int listIndex = FxViewUtil.convertToListIndex(taskCardView.getDisplayedIndex()); + scrollAndSelect(listIndex); + } + + /* Override Methods */ + @Override + public void setNode(Node node) { + panel = (VBox) node; + } + + @Override + public String getFxmlPath() { + return FXML; + } + + @Override + public void setPlaceholder(AnchorPane pane) { + this.placeHolderPane = pane; + } + + /** + * Models a Task Card as a single ListCell of the ListView + */ + private class TodoListViewCell extends ListCell { + + /* Override Methods */ + @Override + protected void updateItem(ImmutableTask task, boolean empty) { + super.updateItem(task, empty); + + if (empty || task == null) { + setGraphic(null); + setText(null); + } else { + TaskCardView taskCardView = TaskCardView.load(task, FxViewUtil.convertToUiIndex(getIndex())); + setGraphic(taskCardView.getLayout()); + setTaskCardStyleProperties(taskCardView); + } + } + + /** + * Sets the style properties of a cell on the to-do list, that cannot be done in any other places. + */ + private void setTaskCardStyleProperties(TaskCardView taskCardView) { + this.setPadding(Insets.EMPTY); + this.selectedProperty().addListener((observable, oldValue, newValue) -> taskCardView.markAsSelected(newValue)); + } + } + +} +``` +###### \resources\style\DefaultStyle.css +``` css +.main { + -fx-background-color: #2D2D2D; + -fx-border-color: #00A4FF; + -fx-border-width: 1px; + -fx-padding: 12; + -fx-spacing: 4; +} + +.text1 { + -fx-text-fill: #FFFFFF; + -fx-fill: #ffffff; + -fx-font-family: "Segoe UI Black"; + -fx-font-size: 17pt; +} + +.text2 { + -fx-text-fill: #FFFFFF; + -fx-fill: #ffffff; + -fx-font-family: "Segoe UI Semibold"; + -fx-font-size: 15pt; +} + +.text3 { + -fx-text-fill: #FFFFFF; + -fx-fill: #ffffff; + -fx-font-family: "Segoe UI Semibold"; + -fx-font-size: 13pt; +} + +.text4 { + -fx-text-fill: #FFFFFF; + -fx-fill: #ffffff; + -fx-font-family: "Segoe UI"; + -fx-font-size: 12pt; +} + +.code { + -fx-font-family: "Consolas"; +} + +.bolder { + -fx-font-weight: bolder; +} + +.underline { + -fx-underline: true; +} + +.gridPanel { + -fx-hgap: 16pt; + -fx-vgap: 2pt; + -fx-background-color: #3D3D3D; + -fx-padding: 8 8 8 8; +} + +.spacingBig { + -fx-spacing: 16px; + -fx-hgap: 16px; + -fx-vgap: 16px; +} + +.spacing { + -fx-spacing: 8px; + -fx-hgap: 8px; + -fx-vgap: 8px; +} + +.spacingSmall { + -fx-spacing: 2px; + -fx-hgap: 2px; + -fx-vgap: 2px; +} + +.subheadingPadding { + -fx-padding: 0 0 0 2; +} + + +.commandFeedback{ + -fx-text-fill: #FFFFFF; + -fx-font-family: "Segoe UI"; + -fx-font-size: 12pt; +} + +.commandFeedback.error { + -fx-text-fill: #FF6464; +} + +.commandInput { + -fx-font-family: "Consolas"; + -fx-font-size: 20px; +} + +.commandInput.error { + -fx-border-color: #FF6464; + -fx-border-width: 2px; + -fx-text-fill: #FF6464; +} + +.commandError { + -fx-text-fill: #FFFFFF; + -fx-font-family: "Segoe UI"; + -fx-font-size: 12pt; + -fx-wrap-text: true; +} + +.roundLabel { + -fx-background-radius: 10; + -fx-text-fill: #ffffff; + -fx-background-color: #2D2D2D; + -fx-font-size: 10pt; + -fx-font-family: "Segoe UI Semilight"; +} + +/***View Filter Styles Start***/ +.viewFilter .label{ + -fx-font-family: "Segoe UI"; + -fx-font-size: 14pt; + -fx-text-fill: #FFFFFF; +} + +.viewFilter .selected { + -fx-background-color: #FFFFFF; + -fx-background-radius: 100; + -fx-padding: 0 8 0 8; +} + +.viewFilter .selected .label { + -fx-text-fill: #2D2D2D; + -fx-font-family: "Segoe UI Semibold"; +} + +``` +###### \resources\style\DefaultStyle.css +``` css +/***TaskCardView Styles Start***/ +/*Default and Base*/ +.taskCard .label { + -fx-font-smoothing-type: lcd; +} + +.taskCard .titleLabel { + -fx-font-size: 16pt; + -fx-font-family: "Segoe UI Semibold"; +} + +.taskCard .descriptionLabel { + -fx-font-size: 12pt; + -fx-font-family: "Segoe UI"; +} + +.taskCard .footnoteLabel { + -fx-font-size: 10pt; + -fx-font-family: "Segoe UI Semilight"; + -fx-font-style: italic; +} + +.taskCard .highlightedBackground { + -fx-background-color: #00A4FF; +} + +.taskCard .lightBackground { + -fx-background-color: #d2d2d2; +} + +.taskCard .pinImage { + -fx-image: url("../images/star_gold.png"); + -fx-fit-to-width: 30px; + -fx-fit-to-height: 30px; +} + +.taskCard .dateImage { + -fx-image: url("../images/clock_black.png"); + -fx-fit-to-width: 20px; + -fx-fit-to-height: 20px; +} + +.taskCard .locationImage { + -fx-image: url("../images/location_black.png"); + -fx-fit-to-width: 20px; + -fx-fit-to-height: 20px; +} + +/*Completed*/ +.completed .label{ + -fx-text-fill: #7F7F7F; +} + +.completed .titleLabel .text { + -fx-strikethrough: true; +} + +.completed .roundLabel { + -fx-background-color: #7F7F7F; + -fx-text-fill: #ffffff; +} + +.completed .pinImage { + -fx-image: url("../images/star_grey.png"); +} + +.completed .dateImage { + -fx-image: url("../images/clock_grey.png"); +} + +.completed .locationImage { + -fx-image: url("../images/location_grey.png"); +} + +/*Overdue*/ +.overdue { + -fx-background-color: #FF6464; +} + +.overdue .label { + -fx-text-fill: #FFFFFF; +} + +.overdue .roundLabel { + -fx-background-color: #FFFFFF; + -fx-text-fill: #FF6464; +} + +.overdue .pinImage { + -fx-image: url("../images/star_white.png"); +} + +.overdue .dateImage { + -fx-image: url("../images/clock_white.png"); +} + +.overdue .locationImage { + -fx-image: url("../images/location_white.png"); +} + + +``` +###### \resources\style\DefaultStyle.css +``` css +/*Selected*/ +.selected { + -fx-background-color: #00A4FF; +} + +.selected .label { + -fx-text-fill: #FFFFFF; +} + +.selected .roundLabel { + -fx-background-color: #FFFFFF; + -fx-text-fill: #00A4FF; +} + +.selected .pinImage { + -fx-image: url("../images/star_white.png"); +} + +.selected .dateImage { + -fx-image: url("../images/clock_white.png"); +} + +.selected .locationImage { + -fx-image: url("../images/location_white.png"); +} + +/*Collapse*/ +.collapsed .collapsible { + visibility: collapse; + -fx-pref-height: 0px; + -fx-min-height: 0px; +} + +/***TaskCardView Styles End***/ + +``` diff --git a/collated/main/A0135817B.md b/collated/main/A0135817B.md new file mode 100644 index 000000000000..b4d7029c3a49 --- /dev/null +++ b/collated/main/A0135817B.md @@ -0,0 +1,1635 @@ +# A0135817B +###### \java\seedu\todo\commons\util\StringUtil.java +``` java + /** + * Returns true if the string is null, of length zero, or contains only whitespace + */ + public static boolean isEmpty(String s) { + return s == null || s.length() == 0 || CharMatcher.whitespace().matchesAllOf(s); + } + +``` +###### \java\seedu\todo\commons\util\TimeUtil.java +``` java + /** + * Translates input string from International date format (DD/MM/YYYY) to American + * date format (MM/DD/YYYY), because Natty only recognizes the later + */ + public static String toAmericanDateFormat(String input) { + return DATE_REGEX.matcher(input).replaceAll("$3$2$1"); + } + +``` +###### \java\seedu\todo\logic\arguments\Argument.java +``` java +abstract public class Argument implements Parameter { + private static final String REQUIRED_ERROR_FORMAT = "The %s parameter is required"; + private static final String TYPE_ERROR_FORMAT = "The %s should be a %s. You gave '%s'."; + + protected static final String OPTIONAL_ARGUMENT_FORMAT = "[%s]"; + protected static final String FLAG_ARGUMENT_FORMAT = "%s%s %s"; + + private String name; + private String description; + private String flag; + private boolean optional = true; + private boolean boundValue = false; + + protected T value; + + private String requiredErrorMessage; + + private static final Logger logger = LogsCenter.getLogger(Argument.class); + + public Argument(String name) { + this.name = name; + } + + public Argument(String name, T defaultValue) { + this.name = name; + this.value = defaultValue; + } + + /** + * Binds a value to this parameter. Implementing classes MUST override AND + * call the parent class function so that the dirty bit is set for required + * parameter validation to work + */ + @Override + public void setValue(String input) throws IllegalValueException { + boundValue = true; + } + + public T getValue() { + return value; + } + + @Override + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Argument description(String description) { + this.description = description; + return this; + } + + public String getFlag() { + return flag; + } + + public Argument flag(String flag) { + this.flag = flag.trim().toLowerCase(); + + if (!this.flag.equals(flag)) { + logger.warning("Flag argument has uppercase or whitespace characters. These have been ignored."); + } + + return this; + } + + public boolean isOptional() { + return optional; + } + + public boolean hasBoundValue() { + return boundValue; + } + + /** + * Sets the field as required + */ + public Argument required() { + this.optional = false; + return this; + } + + /** + * Sets the field as required and specify an error message to show if it is not provided + * @param errorMessage shown to the user when the parameter is not provided + */ + public Argument required(String errorMessage) { + requiredErrorMessage = errorMessage; + this.optional = false; + return this; + } + + @Override + public boolean isPositional() { + return flag == null; + } + + @Override + public void checkRequired() throws IllegalValueException { + if (!isOptional() && !hasBoundValue()) { + String error = requiredErrorMessage == null ? + String.format(Argument.REQUIRED_ERROR_FORMAT, name) : requiredErrorMessage; + throw new IllegalValueException(error); + } + } + + /** + * Throws an IllegalValueException for a type mismatch between user input and what + * the argument expect + * @param field name of the argument + * @param expected the expected type for the argument + * @param actual what the user actually gave + */ + protected void typeError(String field, String expected, String actual) throws IllegalValueException { + throw new IllegalValueException(String.format(Argument.TYPE_ERROR_FORMAT, field, expected, actual)); + } + + @Override + public String toString() { + return toString(name); + } + + public String toString(String name) { + if (!isPositional()) { + name = String.format(FLAG_ARGUMENT_FORMAT, TodoParser.FLAG_TOKEN, flag, name); + } + + if (isOptional()) { + name = String.format(OPTIONAL_ARGUMENT_FORMAT, name); + } + + return name; + } +} +``` +###### \java\seedu\todo\logic\arguments\DateRange.java +``` java +/** + * Utility container class for the output from DateRangeArgument + */ +public class DateRange { + private final LocalDateTime endTime; + private LocalDateTime startTime; + + public DateRange(LocalDateTime endTime) { + this.endTime = endTime; + } + + public DateRange(LocalDateTime startTime, LocalDateTime endTime) { + this(endTime); + this.startTime = startTime; + } + + public LocalDateTime getEndTime() { + return endTime; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public boolean isRange() { + return startTime != null; + } +} +``` +###### \java\seedu\todo\logic\arguments\DateRangeArgument.java +``` java +public class DateRangeArgument extends Argument { + private static final PrettyTimeParser parser = new PrettyTimeParser(); + private static final TimeUtil timeUtil = new TimeUtil(); + + // TODO: Review all error messages to check for user friendliness + private static final String TOO_MANY_DATES_FORMAT = "You specified too many time - we found: %s"; + private static final String NO_DATE_FOUND_FORMAT = "%s does not seem to contain a date"; + + public DateRangeArgument(String name) { + // Makes sure that there is a default value, so that callers won't get null when they getValue() + super(name, new DateRange(null)); + } + + public DateRangeArgument(String name, DateRange defaultValue) { + super(name, defaultValue); + } + + @Override + public void setValue(String input) throws IllegalValueException { + super.setValue(input); + + if (StringUtil.isEmpty(input)) { + return; + } + + input = TimeUtil.toAmericanDateFormat(input); + List dateGroups = parser.parse(input); + + List dates = dateGroups.stream() + .map(TimeUtil::asLocalDateTime) + .sorted() + .collect(Collectors.toList()); + + if (dates.size() > 2) { + tooManyDatesError(dates); + } else if (dates.size() == 0) { + throw new IllegalValueException(String.format(DateRangeArgument.NO_DATE_FOUND_FORMAT, input)); + } + + if (dates.size() == 1) { + value = new DateRange(dates.get(0)); + } else { + value = new DateRange(dates.get(0), dates.get(1)); + } + } + + private void tooManyDatesError(List dates) throws IllegalValueException { + StringJoiner sj = new StringJoiner(", "); + for (LocalDateTime d : dates) { + sj.add(timeUtil.getTaskDeadlineText(d)); + } + String message = String.format(DateRangeArgument.TOO_MANY_DATES_FORMAT, sj.toString()); + throw new IllegalValueException(message); + } + +} +``` +###### \java\seedu\todo\logic\arguments\FlagArgument.java +``` java +public class FlagArgument extends Argument { + + public FlagArgument(String name) { + super(name); + flag(name.substring(0, 1).toLowerCase()); + this.value = false; + } + + public FlagArgument(String name, boolean defaultValue) { + super(name, defaultValue); + flag(name.substring(0, 1).toLowerCase()); + } + + @Override + public void setValue(String input) throws IllegalValueException { + this.value = true; + super.setValue(input); + } + + @Override + public String toString(String name) { + String flag = TodoParser.FLAG_TOKEN + getFlag(); + return isOptional() ? String.format(Argument.OPTIONAL_ARGUMENT_FORMAT, flag) : flag; + } +} +``` +###### \java\seedu\todo\logic\arguments\IntArgument.java +``` java +public class IntArgument extends Argument { + + public IntArgument(String name) { + super(name); + } + + public IntArgument(String name, int defaultValue) { + super(name, defaultValue); + } + + @Override + public void setValue(String input) throws IllegalValueException { + try { + value = Integer.parseInt(input); + super.setValue(input); + } catch (NumberFormatException e) { + typeError(this.getName(), "integer", input); + } + } + +} +``` +###### \java\seedu\todo\logic\arguments\Parameter.java +``` java +/** + * Represents a single command parameter that the parser will try to feed the user + * input into. The Parameter interface is needed because the Argument base class is + * typed, so this interface contains all of the non-typed methods that are common to + * all argument subclasses + */ +public interface Parameter { + void setValue(String input) throws IllegalValueException; + + boolean isPositional(); + + boolean hasBoundValue(); + + boolean isOptional(); + + String getFlag(); + + String getName(); + + String getDescription(); + + void checkRequired() throws IllegalValueException; +} +``` +###### \java\seedu\todo\logic\arguments\StringArgument.java +``` java +public class StringArgument extends Argument { + + public StringArgument(String name) { + super(name); + } + + public StringArgument(String name, String defaultValue) { + super(name, defaultValue); + } + + @Override + public void setValue(String input) throws IllegalValueException { + input = input.trim(); + + // Ignore empty strings + if (input.length() > 0) { + this.value = input; + } + + super.setValue(input); + } + +} +``` +###### \java\seedu\todo\logic\commands\AddCommand.java +``` java +public class AddCommand extends BaseCommand { + private static final String VERB = "added"; + + private Argument title = new StringArgument("title").required(); + + private Argument description = new StringArgument("description") + .flag("m"); + + private Argument pin = new FlagArgument("pin") + .flag("p"); + + private Argument location = new StringArgument("location") + .flag("l"); + + private Argument date = new DateRangeArgument("deadline") + .flag("d"); + + @Override + public Parameter[] getArguments() { + return new Parameter[] { + title, date, description, location, pin, + }; + } + + @Override + public String getCommandName() { + return "add"; + } + + @Override + public List getCommandSummary() { + String eventArguments = Joiner.on(" ").join(title, "/d start and end time", description, location, pin); + + return ImmutableList.of( + new CommandSummary("Add task", getCommandName(), getArgumentSummary()), + new CommandSummary("Add event", getCommandName(), eventArguments)); + } + + @Override + public CommandResult execute() throws ValidationException { + ImmutableTask addedTask = this.model.add(title.getValue(), task -> { + task.setDescription(description.getValue()); + task.setPinned(pin.getValue()); + task.setLocation(location.getValue()); + task.setStartTime(date.getValue().getStartTime()); + task.setEndTime(date.getValue().getEndTime()); + }); + if(!model.getObservableList().contains(addedTask)) { + model.view(TaskViewFilter.DEFAULT); + } + eventBus.post(new HighlightTaskEvent(addedTask)); + eventBus.post(new ExpandCollapseTaskEvent(addedTask)); + return taskSuccessfulResult(title.getValue(), AddCommand.VERB); + } + +} +``` +###### \java\seedu\todo\logic\commands\BaseCommand.java +``` java +/** + * The base class for commands. All commands need to implement an execute function + * and a getArguments function that collects the command arguments for the use of + * the help command. + * + * To perform additional validation on incoming arguments, override the validateArguments + * function. + */ +public abstract class BaseCommand { + /** + * The default message that accompanies argument errors + */ + private static final String DEFAULT_ARGUMENT_ERROR_MESSAGE = ""; + + private static final String TASK_MODIFIED_SUCCESS_MESSAGE = "'%s' successfully %s!"; + + protected static final EventsCenter eventBus = EventsCenter.getInstance(); + + protected Model model; + + protected ErrorBag errors = new ErrorBag(); + + abstract protected Parameter[] getArguments(); + + /** + * Return the name of the command, which is used to call it + */ + abstract public String getCommandName(); + + /** + * Returns a list of command summaries for the command. This function returns a + * list because commands may (rarely) be responsible for more than one thing, + * like the add command. + */ + abstract public List getCommandSummary(); + + abstract public CommandResult execute() throws ValidationException; + + /** + * Binds the data model to the command object + */ + public void setModel(Model model) { + this.model = model; + } + + /** + * Binds the both positional and named command arguments from the parse results + * to the command object itself + * + * @throws ValidationException if the arguments are invalid + */ + public void setArguments(ParseResult arguments) throws ValidationException { + if (arguments.getPositionalArgument().isPresent()) { + setPositionalArgument(arguments.getPositionalArgument().get()); + } + + for (Entry e : arguments.getNamedArguments().entrySet()) { + setNameArgument(e.getKey(), e.getValue()); + } + + checkRequiredArguments(); + validateArguments(); + + errors.validate(getArgumentErrorMessage()); + } + + /** + * Hook allowing subclasses to implement their own validation logic for arguments + * Subclasses should add additional errors to the errors ErrorBag + */ + protected void validateArguments() { + // Does no additional validation by default + } + + protected void setPositionalArgument(String argument) { + for (Parameter p : getArguments()) { + if (p.isPositional()) { + try { + p.setValue(argument); + } catch (IllegalValueException e) { + errors.put(e.getMessage()); + } + } + } + } + + protected void setNameArgument(String flag, String argument) { + for (Parameter p : getArguments()) { + if (flag.equals(p.getFlag())) { + try { + p.setValue(argument); + } catch (IllegalValueException e) { + errors.put(p.getName(), e.getMessage()); + } + + return; + } + } + + // TODO: Do something for unrecognized argument? + } + + private void checkRequiredArguments() { + for (Parameter p : getArguments()) { + try { + p.checkRequired(); + } catch (IllegalValueException e) { + errors.put(p.getName(), e.getMessage()); + } + } + } + + /** + * Override this function if the command should return some other error + * message on argument validation error + */ + protected String getArgumentErrorMessage() { + return BaseCommand.DEFAULT_ARGUMENT_ERROR_MESSAGE; + } + + /** + * Returns a generic CommandResult with a "{task} successfully {verbed}" success message. + * + * @param title the title of the task that was verbed on + * @param verb the action that was performed on the task, in past tense + */ + protected CommandResult taskSuccessfulResult(String title, String verb) { + return new CommandResult(String.format(BaseCommand.TASK_MODIFIED_SUCCESS_MESSAGE, title, verb)); + } + + /** + * Turns the arguments into a string summary using their toString function + */ + protected String getArgumentSummary() { + StringJoiner sj = new StringJoiner(" "); + for (Parameter p : getArguments()) { + sj.add(p.toString()); + } + return sj.toString(); + } +} +``` +###### \java\seedu\todo\logic\commands\CommandMap.java +``` java +public class CommandMap { + // List of command classes. Remember to register new commands here so that the + // dispatcher can recognize them + public static List> commandClasses = ImmutableList.of( + AddCommand.class, + CompleteCommand.class, + DeleteCommand.class, + EditCommand.class, + ExitCommand.class, + HelpCommand.class, + PinCommand.class, + UndoCommand.class, + RedoCommand.class, + SaveCommand.class, + LoadCommand.class, + ShowCommand.class, + FindCommand.class, + ViewCommand.class, + TagCommand.class + ); + + private static Map> commandMap; + + private static void buildCommandMap() { + commandMap = new LinkedHashMap<>(); + + for (Class command : CommandMap.commandClasses) { + String commandName = getCommand(command).getCommandName().toLowerCase(); + commandMap.put(commandName, command); + } + } + + public static Map> getCommandMap() { + if (commandMap == null) { + buildCommandMap(); + } + + return commandMap; + } + + public static BaseCommand getCommand(String key) { + return getCommand(getCommandMap().get(key)); + } + + public static BaseCommand getCommand(Class command) { + try { + return command.newInstance(); + } catch (InstantiationException|IllegalAccessException e) { + e.printStackTrace(); + return null; // This shouldn't happen + } + } +} +``` +###### \java\seedu\todo\logic\commands\CommandResult.java +``` java +/** + * Represents the result of a command execution. + */ +public class CommandResult { + private final String feedback; + private final ErrorBag errors; + + public CommandResult() { + this.feedback = ""; + this.errors = null; + } + + public CommandResult(String feedback) { + this.feedback = feedback; + this.errors = null; + } + + public CommandResult(String feedback, ErrorBag errors) { + this.feedback = feedback; + this.errors = errors; + } + + public String getFeedback() { + return feedback; + } + + public ErrorBag getErrors() { + return errors; + } + + public boolean isSuccessful() { + return errors == null; + } +} +``` +###### \java\seedu\todo\logic\commands\CommandSummary.java +``` java +public class CommandSummary { + /** + * The scenario the summary is aiming to describe, eg. add event, delete task, etc. + * Keep it short but descriptive. + */ + public final String scenario; + + /** + * The command to accomplish the scenario, eg. add, delete + */ + public final String command; + + /** + * The parameters for the command + */ + public final String arguments; + + public CommandSummary(String scenario, String command) { + this(scenario, command, ""); + } + + public CommandSummary(String scenario, String command, String arguments) { + this.scenario = scenario.trim(); + this.command = command.toLowerCase().trim(); + this.arguments = arguments.trim(); + } +} +``` +###### \java\seedu\todo\logic\commands\DeleteCommand.java +``` java +public class DeleteCommand extends BaseCommand { + private static final String VERB = "deleted"; + + private Argument index = new IntArgument("index").required(); + + @Override + protected Parameter[] getArguments() { + return new Parameter[]{ index }; + } + + @Override + public String getCommandName() { + return "delete"; + } + + @Override + public List getCommandSummary() { + return ImmutableList.of(new CommandSummary("Delete task", getCommandName(), + getArgumentSummary())); + } + + @Override + public CommandResult execute() throws ValidationException { + ImmutableTask deletedTask = this.model.delete(index.getValue()); + return taskSuccessfulResult(deletedTask.getTitle(), DeleteCommand.VERB); + } +} +``` +###### \java\seedu\todo\logic\commands\ExitCommand.java +``` java +/** + * Terminates the program. + */ +public class ExitCommand extends BaseCommand { + private final static String EXIT_MESSAGE = "Goodbye!"; + + @Override + public CommandResult execute() throws ValidationException { + EventsCenter.getInstance().post(new ExitAppRequestEvent()); + return new CommandResult(ExitCommand.EXIT_MESSAGE); + } + + @Override + protected Parameter[] getArguments() { + return new Parameter[]{}; + } + + @Override + public String getCommandName() { + return "exit"; + } + + @Override + public List getCommandSummary() { + return ImmutableList.of(new CommandSummary("Close this app :(", getCommandName())); + } + +} +``` +###### \java\seedu\todo\logic\commands\HelpCommand.java +``` java +/** + * Shows the help panel + */ +public class HelpCommand extends BaseCommand { + private final static String HELP_MESSAGE = "Showing help..."; + + private static List commandSummaries; + + @Override + public CommandResult execute() throws ValidationException { + if (commandSummaries == null) { + commandSummaries = collectCommandSummaries(); + } + + EventsCenter.getInstance().post(new ShowHelpEvent(commandSummaries)); + return new CommandResult(HelpCommand.HELP_MESSAGE); + } + + @Override + protected Parameter[] getArguments() { + return new Parameter[]{}; + } + + @Override + public String getCommandName() { + return "help"; + } + + @Override + public List getCommandSummary() { + return ImmutableList.of(new CommandSummary("Show help", getCommandName())); + } + + private List collectCommandSummaries() { + List summaries = new ArrayList<>(); + for (String key : CommandMap.getCommandMap().keySet()) { + summaries.addAll(CommandMap.getCommand(key).getCommandSummary()); + } + return summaries; + } +} +``` +###### \java\seedu\todo\logic\commands\LoadCommand.java +``` java +public class LoadCommand extends BaseCommand { + private Argument location = new StringArgument("location").required(); + + @Override + protected Parameter[] getArguments() { + return new Parameter[]{ location }; + } + + @Override + public String getCommandName() { + return "load"; + } + + @Override + public List getCommandSummary() { + return ImmutableList.of(new CommandSummary("Load todo list from file", getCommandName(), + getArgumentSummary())); + } + + @Override + public CommandResult execute() throws ValidationException { + String path = location.getValue(); + model.load(path); + return new CommandResult(String.format("File loaded from %s", path)); + } +} +``` +###### \java\seedu\todo\logic\commands\RedoCommand.java +``` java +public class RedoCommand extends BaseCommand { + @Override + protected Parameter[] getArguments() { + return new Parameter[0]; + } + + @Override + public String getCommandName() { + return "redo"; + } + + @Override + public List getCommandSummary() { + return ImmutableList.of(new CommandSummary("Redo", getCommandName())); + } + + @Override + public CommandResult execute() throws ValidationException { + model.redo(); + return new CommandResult("Redid last action"); + } +} +``` +###### \java\seedu\todo\logic\commands\SaveCommand.java +``` java +/** + * Saves the save file to a different location + */ +public class SaveCommand extends BaseCommand { + private Argument location = new StringArgument("location"); + + @Override + protected Parameter[] getArguments() { + return new Parameter[]{ location }; + } + + @Override + public String getCommandName() { + return "save"; + } + + @Override + public List getCommandSummary() { + return ImmutableList.of( + new CommandSummary("Save to a different file", getCommandName(), "location"), + new CommandSummary("Find my todo list file", getCommandName())); + } + + @Override + public CommandResult execute() throws ValidationException { + if (location.hasBoundValue()) { + model.save(location.getValue()); + + return new CommandResult(String.format("Todo list saved successfully to %s", location)); + } else { + return new CommandResult(String.format("Save location: %s", model.getStorageLocation())); + } + } +} +``` +###### \java\seedu\todo\logic\commands\UndoCommand.java +``` java +public class UndoCommand extends BaseCommand { + @Override + protected Parameter[] getArguments() { + return new Parameter[0]; + } + + @Override + public String getCommandName() { + return "undo"; + } + + @Override + public List getCommandSummary() { + return ImmutableList.of(new CommandSummary("Undo last edit", getCommandName())); + } + + @Override + public CommandResult execute() throws ValidationException { + model.undo(); + return new CommandResult("Undid last action"); + } +} +``` +###### \java\seedu\todo\logic\Dispatcher.java +``` java +public interface Dispatcher { + BaseCommand dispatch(String command) throws IllegalValueException; +} +``` +###### \java\seedu\todo\logic\Logic.java +``` java +/** + * API of the Logic component + */ +public interface Logic { + /** + * Executes the command and returns the result. + * @param input The command as entered by the user. + */ + CommandResult execute(String input); + +``` +###### \java\seedu\todo\logic\TodoDispatcher.java +``` java +/** + * Selects the correct command based on the parser results + */ +public class TodoDispatcher implements Dispatcher { + private final static String COMMAND_NOT_FOUND_FORMAT = "'%s' doesn't look like any command we know."; + private final static String AMBIGUOUS_COMMAND_FORMAT = "Do you mean %s?"; + + public BaseCommand dispatch(String input) throws IllegalValueException { + // Implements character by character matching of input to the list of command names + // Since this eliminates non-matches at every character, it is fast even though + // it is theoretically O(n^2) in the worst case. + Set commands = new HashSet<>(CommandMap.getCommandMap().keySet()); + + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + + Iterator s = commands.iterator(); + while (s.hasNext()) { + String commandName = s.next(); + if (input.length() > commandName.length() || commandName.charAt(i) != c) { + s.remove(); + } + } + + // Return immediately when there's one match left. This allow the user to + // type as little as possible, and is also good for autocomplete if that's + // on the radar + if (commands.size() == 1) { + String key = commands.iterator().next(); + return CommandMap.getCommand(key); + } else if (commands.isEmpty()) { + throw new IllegalValueException(String.format(TodoDispatcher.COMMAND_NOT_FOUND_FORMAT, input)); + } + } + + String ambiguousCommands = Joiner.on(" or ").join(commands); + throw new IllegalValueException(String.format(TodoDispatcher.AMBIGUOUS_COMMAND_FORMAT, ambiguousCommands)); + } +} +``` +###### \java\seedu\todo\logic\TodoLogic.java +``` java +/** + * Central controller for the application, abstracting application logic from the UI + */ +public class TodoLogic implements Logic { + private final Parser parser; + private final Model model; + private final Dispatcher dispatcher; + + private static final Logger logger = LogsCenter.getLogger(TodoLogic.class); + + public TodoLogic(Parser parser, Model model, Dispatcher dispatcher) { + assert parser != null; + assert model != null; + assert dispatcher != null; + + this.parser = parser; + this.model = model; + this.dispatcher = dispatcher; + } + + public CommandResult execute(String input) { + // Sanity check - input should not be empty + if (StringUtil.isEmpty(input)) { + return new CommandResult(); + } + + ParseResult parseResult = parser.parse(input); + BaseCommand command; + logger.fine("Parsed command: " + parseResult.toString()); + + try { + command = dispatcher.dispatch(parseResult.getCommand()); + } catch (IllegalValueException e) { + return new CommandResult(e.getMessage(), new ErrorBag()); + } + + try { + command.setArguments(parseResult); + command.setModel(model); + return command.execute(); + } catch (ValidationException e) { + logger.info(e.getMessage()); + return new CommandResult(e.getMessage(), e.getErrors()); + } + } + +``` +###### \java\seedu\todo\model\ErrorBag.java +``` java +public class ErrorBag { + private List nonFieldErrors = new ArrayList<>(); + private Map fieldErrors = new HashMap<>(); + + /** + * Add an error that is not related to any specific field + * + * @param nonFieldError the error message + */ + public void put(String nonFieldError) { + nonFieldErrors.add(nonFieldError); + } + + /** + * Add an error that is related to a specific field + * + * @param field the error is for + * @param error the error message + */ + public void put(String field, String error) { + fieldErrors.put(field, error); + } + + public List getNonFieldErrors() { + return nonFieldErrors; + } + + public Map getFieldErrors() { + return fieldErrors; + } + + public int size() { + return nonFieldErrors.size() + fieldErrors.size(); + } + + /** + * Throws a validation exception if the bag contains errors + * + * @param message a short message about why the validation failed + * @throws ValidationException if the ErrorBag is not empty + */ + public void validate(String message) throws ValidationException { + if (size() > 0) { + throw new ValidationException(message, this); + } + } +} +``` +###### \java\seedu\todo\model\ImmutableTodoList.java +``` java +public interface ImmutableTodoList { + /** + * Get an immutable list of tasks + */ + List getTasks(); +} +``` +###### \java\seedu\todo\model\Model.java +``` java +public interface Model { + /** + * Adds a new task or event with title only to the todo list. + * + * @param title the title of the task + * @return the task that was just created + * @throws IllegalValueException if the values set in the update predicate is invalid + */ + ImmutableTask add(String title) throws IllegalValueException; + + /** + * Adds a new task or event with title and other fields to the todo list. + * + * @param title the title of the task + * @param update a {@link MutableTask} is passed into this lambda. All other fields + * should be set from inside this lambda. + * @return the task that was just created + * @throws ValidationException if the fields in the task to be updated are not valid + */ + ImmutableTask add(String title, Consumer update) throws ValidationException; + + /** + * Deletes the given task from the todo list. This change is also propagated to the + * underlying persistence layer. + * + * @param index the 1-indexed position of the task that needs to be deleted + * @return the task that was just deleted + * @throws ValidationException if the task does not exist + */ + ImmutableTask delete(int index) throws ValidationException; + + /** + * Replaces certain fields in the task. Mutation of the {@link Task} object should + * only be done in the update lambda. The lambda takes in one parameter, + * a {@link MutableTask}, and does not expect any return value. For example: + * + *
todo.update(task, t -> {
+     *     t.setEndTime(t.getEndTime.get().plusHours(2)); // Push deadline back by 2h
+     *     t.setPin(true); // Pin this task
+     * });
+ * + * @return the task that was just updated + * + * @throws ValidationException if the task does not exist or if the fields in the + * task to be updated are not valid + */ + ImmutableTask update(int index, Consumer update) throws ValidationException; + + /** + * Carries out the specified update in the fields of all visible tasks. Mutation of all {@link Task} + * objects should only be done in the update lambda. The lambda takes in a single parameter, + * a {@link MutableTask}, and does not expect any return value, as per the {@link update} command. Since + * this represents the observable layer, the changes required to be done to the underlying layer TodoList + * is set via getting a list of their indices in the underlying layer using a UUID map. + * + *
todo.updateAll (t -> {
+     *     t.setEndTime(t.getEndTime.get().plusHours(2)); // Push deadline of all Observable tasks back by 2h
+     *     t.setPin(true); // Pin all tasks in Observable view
+     * });
+ * + * @throws ValidationException if any updates on any of the task objects are considered invalid + */ + void updateAll(Consumer update) throws ValidationException; + + /** + * Sets the model to the provided TaskViewFilter object. TaskViewFilters represents the + * filter and sorting needed by each intelligent view + */ + void view(TaskViewFilter view); + + /** + * Filters the list of tasks by this predicate. This is filtering is ran + * after the view predicate. No information about the search is shown to the user. + * Setting predicate to null will reset the search. + */ + void find(Predicate predicate); + + /** + * Filters the list of tasks by this predicate. This is filtering is ran + * after the view predicate. A list of search terms is also shown to the user. + */ + void find(Predicate predicate, List terms); + + /** + * Undoes the last operation that modifies the todolist + * @throws ValidationException if there are no more changes to undo + */ + void undo() throws ValidationException; + + /** + * Redoes the last operation that was undone + * @throws ValidationException if there are no more changes to redo + */ + void redo() throws ValidationException; + + /** + * Changes the save path of the TodoList storage + * @throws ValidationException if the path is not valid + */ + void save(String location) throws ValidationException; + + /** + * Loads a TodoList from the path. + * @throws ValidationException if the path or file is invalid + */ + void load(String location) throws ValidationException; + + /** + * Obtains the current storage methods + */ + String getStorageLocation(); + + /** + * Get an observable list of tasks. Used mainly by the JavaFX UI. + */ + UnmodifiableObservableList getObservableList(); + + /** + * Get the current view filter used on the model. Used mainly by the JavaFx UI. + */ + ObjectProperty getViewFilter(); + + /** + * Get the current status of the search used on the model. + */ + ObjectProperty getSearchStatus(); + +``` +###### \java\seedu\todo\model\TodoList.java +``` java +/** + * Represents the todolist inside memory. While Model works as the external + * interface for handling data and application state, this class is internal + * to Model and represents only CRUD operations to the todolist. + */ +public class TodoList implements TodoListModel { + private static final String INCORRECT_FILE_FORMAT_FORMAT = "%s doesn't seem to be in the correct format."; + private static final String FILE_NOT_FOUND_FORMAT = "%s does not seem to exist."; + private static final String FILE_SAVE_ERROR_FORMAT = "Couldn't save file: %s"; + + private ObservableList tasks = FXCollections.observableArrayList(Task::getObservableProperties); + + private MovableStorage storage; + + private static final Logger logger = LogsCenter.getLogger(TodoList.class); + private static final EventsCenter events = EventsCenter.getInstance(); + + public TodoList(MovableStorage storage) { + this.storage = storage; + + try { + setTasks(storage.read().getTasks(), false); + } catch (FileNotFoundException | DataConversionException e) { + logger.info("Data file not found. Will be starting with an empty TodoList"); + } + + // Update event status + new Timer().scheduleAtFixedRate(new UpdateEventTask(), 0, 60 * 1000); + } + + private void updateEventStatus() { + LocalDateTime now = LocalDateTime.now(); + boolean todoListModified = false; + + for (Task task : tasks) { + boolean isIncompleteEvent = !task.isCompleted() && task.isEvent(); + if (isIncompleteEvent && now.isAfter(task.getEndTime().get())) { + task.setCompleted(true); + todoListModified = true; + } + } + if (todoListModified) { + saveTodoList(); + } + } + + private void raiseStorageEvent(String message, Exception e) { + // TODO: Have this raise an event + } + + private void saveTodoList() { + try { + storage.save(this); + } catch (IOException e) { + events.post(new DataSavingExceptionEvent(e)); + } + } + +``` +###### \java\seedu\todo\model\TodoList.java +``` java + @Override + public void save(String location) throws ValidationException { + try { + storage.save(this, location); + } catch (IOException e) { + String message = String.format(TodoList.FILE_SAVE_ERROR_FORMAT, e.getMessage()); + throw new ValidationException(message); + } + } + + @Override + public void load(String location) throws ValidationException { + try { + setTasks(storage.read(location).getTasks()); + } catch (DataConversionException e) { + throw new ValidationException(TodoList.INCORRECT_FILE_FORMAT_FORMAT); + } catch (FileNotFoundException e) { + String message = String.format(TodoList.FILE_NOT_FOUND_FORMAT, location); + throw new ValidationException(message); + } + } + + @Override + public void setTasks(List todoList) { + setTasks(todoList, true); + } + + /** + * We have a private version of setTasks because we also need to setTask during initialization, + * but we don't want the list to be save during init (where we presumably got the data from) + */ + private void setTasks(List todoList, boolean persistToStorage) { + this.tasks.clear(); + this.tasks.addAll(todoList.stream().map(Task::new).collect(Collectors.toList())); + + if (persistToStorage) { + saveTodoList(); + } + } + + @Override + public ObservableList getObservableList() { + return new UnmodifiableObservableList<>(tasks); + } + + @Override + public List getTasks() { + return Collections.unmodifiableList(tasks); + } + + private class UpdateEventTask extends TimerTask { + @Override + public void run() { + updateEventStatus(); + } + } + +} +``` +###### \java\seedu\todo\model\TodoListModel.java +``` java +public interface TodoListModel extends ImmutableTodoList { + /** + * Adds a new task or event with title only to the todo list. + * + * @param title the title of the task + * @return the task that was just created + * @throws IllegalValueException if the values set in the update predicate is invalid + */ + ImmutableTask add(String title) throws IllegalValueException; + + /** + * Adds a new task or event with title and other fields to the todo list. + * + * @param title the title of the task + * @param update a {@link MutableTask} is passed into this lambda. All other fields + * should be set from inside this lambda. + * @return the task that was just created + * @throws ValidationException if the fields in the task to be updated are not valid + */ + ImmutableTask add(String title, Consumer update) throws ValidationException; + + /** + * Deletes the given task from the todo list. This change is also propagated to the + * underlying persistence layer. + * + * @param index the 1-indexed position of the task that needs to be deleted + * @return the task that was just deleted + * @throws ValidationException if the task does not exist + */ + ImmutableTask delete(int index) throws ValidationException; + + /** + * Replaces certain fields in the task. Mutation of the {@link Task} object should + * only be done in the update lambda. The lambda takes in one parameter, + * a {@link MutableTask}, and does not expect any return value. For example: + * + *
todo.update(task, t -> {
+     *     t.setEndTime(t.getEndTime.get().plusHours(2)); // Push deadline back by 2h
+     *     t.setPin(true); // Pin this task
+     * });
+ * + * @return the task that was just updated + * + * @throws ValidationException if the task does not exist or if the fields in the + * task to be updated are not valid + */ + ImmutableTask update(int index, Consumer update) throws ValidationException; + +``` +###### \java\seedu\todo\model\TodoListModel.java +``` java + /** + * Changes the save path of the TodoList storage + * @throws ValidationException if the path is not valid + */ + void save(String location) throws ValidationException; + + /** + * Loads a TodoList from the path. + * @throws ValidationException if the path or file is invalid + */ + void load(String location) throws ValidationException; + + /** + * Replaces the tasks in list with the one in the + */ + void setTasks(List todoList); + + /** + * Get an observable list of tasks. Used mainly by the JavaFX UI. + */ + ObservableList getObservableList(); + +} + +``` +###### \java\seedu\todo\model\TodoModel.java +``` java +/** + * Represents the data layer of the application. The TodoModel handles any + * interaction with the application state that are not persisted, such as the + * view (sort and filtering), undo and redo. Since this layer handles + * sorting and filtering, task ID must be passed through {@link #getTaskIndex} + * to transform them into the index {@link TodoList} methods can use. + */ +public class TodoModel implements Model { + // Constants + private static final int UNDO_LIMIT = 10; + private static final String INDEX_OUT_OF_BOUND_FORMAT = "There is no task no. %d"; + private static final String NO_MORE_UNDO_REDO_FORMAT = "There are no more steps to %s"; + + // Dependencies + private TodoListModel todoList; + private UniqueTagCollectionModel uniqueTagCollection = new UniqueTagCollection(); + private MovableStorage storage; + + // Stack of transformation that the tasks go through before being displayed to the user + private ObservableList tasks; + private FilteredList viewFilteredTasks; + private FilteredList findFilteredTasks; + private SortedList sortedTasks; + + // State stacks for managing un/redo + private Deque> undoStack = new ArrayDeque<>(); + private Deque> redoStack = new ArrayDeque<>(); + + /** + * Contains the current view tab the user has selected. + * {@link #getViewFilter()} is the getter and {@link #view(TaskViewFilter)} is the setter + */ + private ObjectProperty view = new SimpleObjectProperty<>(); + + private ObjectProperty search = new SimpleObjectProperty<>(); + + public TodoModel(Config config) { + this(new TodoListStorage(config.getTodoListFilePath())); + } + + public TodoModel(MovableStorage storage) { + this(new TodoList(storage), storage); + } + + public TodoModel(TodoListModel todoList, MovableStorage storage) { + this.storage = storage; + this.todoList = todoList; + + tasks = todoList.getObservableList(); + viewFilteredTasks = new FilteredList<>(tasks); + findFilteredTasks = new FilteredList<>(viewFilteredTasks); + sortedTasks = new SortedList<>(findFilteredTasks); + + // Sets the default view + view(TaskViewFilter.DEFAULT); + } + + /** + * Because the model does filtering and sorting on the tasks, the incoming index needs to be + * translated into it's index in the underlying todoList. The code below is not particularly + * clean, but it works well enough. + * + * @throws ValidationException if the index is invalid + */ + private int getTaskIndex(int index) throws ValidationException { + int taskIndex; + + try { + ImmutableTask task = getObservableList().get(index - 1); + taskIndex = tasks.indexOf(task); + } catch (IndexOutOfBoundsException e) { + taskIndex = -1; + } + + if (taskIndex == -1) { + String message = String.format(TodoModel.INDEX_OUT_OF_BOUND_FORMAT, index); + throw new ValidationException(message); + } + + return taskIndex; + } + + private void saveState(Deque> stack) { + List tasks = todoList.getTasks().stream() + .map(Task::new).collect(Collectors.toList()); + + stack.addFirst(tasks); + while (stack.size() > TodoModel.UNDO_LIMIT) { + stack.removeLast(); + } + } + + private void saveUndoState() { + saveState(undoStack); + redoStack.clear(); + } + + @Override + public ImmutableTask add(String title) throws IllegalValueException { + saveUndoState(); + return todoList.add(title); + } + + @Override + public ImmutableTask add(String title, Consumer update) throws ValidationException { + saveUndoState(); + return todoList.add(title, update); + } + + @Override + public ImmutableTask delete(int index) throws ValidationException { + saveUndoState(); + ImmutableTask taskToDelete = getObservableList().get(getTaskIndex(index)); + uniqueTagCollection.notifyTaskDeleted(taskToDelete); + return todoList.delete(getTaskIndex(index)); + } + + @Override + public ImmutableTask update(int index, Consumer update) throws ValidationException { + saveUndoState(); + return todoList.update(getTaskIndex(index), update); + } + +``` +###### \java\seedu\todo\model\TodoModel.java +``` java + @Override + public void view(TaskViewFilter view) { + viewFilteredTasks.setPredicate(view.filter); + + sortedTasks.setComparator((a, b) -> { + int pin = Boolean.compare(b.isPinned(), a.isPinned()); + return pin != 0 || view.sort == null ? pin : view.sort.compare(a, b); + }); + + this.view.setValue(view); + } + + @Override + public void find(Predicate predicate) { + findFilteredTasks.setPredicate(predicate); + search.setValue(null); + } + + @Override + public void find(Predicate predicate, List terms) { + findFilteredTasks.setPredicate(predicate); + search.setValue(new SearchStatus(terms, findFilteredTasks.size(), tasks.size())); + } + + @Override + public void undo() throws ValidationException { + if (undoStack.isEmpty()) { + String message = String.format(TodoModel.NO_MORE_UNDO_REDO_FORMAT, "undo"); + throw new ValidationException(message); + } + + List tasks = undoStack.removeFirst(); + uniqueTagCollection.initialise(todoList.getObservableList()); + saveState(redoStack); + todoList.setTasks(tasks); + } + + @Override + public void redo() throws ValidationException { + if (redoStack.isEmpty()) { + String message = String.format(TodoModel.NO_MORE_UNDO_REDO_FORMAT, "redo"); + throw new ValidationException(message); + } + + List tasks = redoStack.removeFirst(); + uniqueTagCollection.initialise(todoList.getObservableList()); + saveState(undoStack); + todoList.setTasks(tasks); + } + + @Override + public void save(String location) throws ValidationException { + todoList.save(location); + } + + @Override + public void load(String location) throws ValidationException { + todoList.load(location); + uniqueTagCollection.initialise(todoList.getObservableList()); + } + + @Override + public String getStorageLocation() { + return storage.getLocation(); + } + + @Override + public UnmodifiableObservableList getObservableList() { + return new UnmodifiableObservableList<>(sortedTasks); + } + + @Override + public ObjectProperty getViewFilter() { + return view; + } + + @Override + public ObjectProperty getSearchStatus() { + return search; + } + +``` +###### \resources\style\DefaultStyle.css +``` css +.searchStatus { + -fx-padding: 4px; +} + +.searchStatus Text { + -fx-font-size: 14px; + -fx-fill: #fff; +} + +.searchStatus .searchLabel { +} + +.searchStatus .searchTerm { + -fx-font-weight: bold; +} + +.searchStatus .searchCount { + -fx-text-alignment: right; +} +``` diff --git a/collated/main/A0135817Breuse.md b/collated/main/A0135817Breuse.md new file mode 100644 index 000000000000..f280ec675d98 --- /dev/null +++ b/collated/main/A0135817Breuse.md @@ -0,0 +1,46 @@ +# A0135817Breuse +###### \java\seedu\todo\commons\util\TimeUtil.java +``` java + // From http://stackoverflow.com/a/27378709/313758 + /** + * Calls {@link #asLocalDate(Date, ZoneId)} with the system default time zone. + */ + public static LocalDate asLocalDate(Date date) { + return asLocalDate(date, ZoneId.systemDefault()); + } + + /** + * Creates {@link LocalDate} from {@code java.util.Date} or it's subclasses. Null-safe. + */ + public static LocalDate asLocalDate(Date date, ZoneId zone) { + if (date == null) + return null; + + if (date instanceof java.sql.Date) + return ((java.sql.Date) date).toLocalDate(); + else + return Instant.ofEpochMilli(date.getTime()).atZone(zone).toLocalDate(); + } + + /** + * Calls {@link #asLocalDateTime(Date, ZoneId)} with the system default time zone. + */ + public static LocalDateTime asLocalDateTime(Date date) { + return asLocalDateTime(date, ZoneId.systemDefault()); + } + + /** + * Creates {@link LocalDateTime} from {@code java.util.Date} or it's subclasses. Null-safe. + */ + public static LocalDateTime asLocalDateTime(Date date, ZoneId zone) { + if (date == null) + return null; + + if (date instanceof java.sql.Timestamp) + return ((java.sql.Timestamp) date).toLocalDateTime(); + else + return Instant.ofEpochMilli(date.getTime()).atZone(zone).toLocalDateTime(); + } + +} +``` diff --git a/collated/main/A0139021U.md b/collated/main/A0139021U.md new file mode 100644 index 000000000000..e6a959d739af --- /dev/null +++ b/collated/main/A0139021U.md @@ -0,0 +1,417 @@ +# A0139021U +###### \java\seedu\todo\commons\events\ui\ShowPreviewEvent.java +``` java +/** + * An event requesting to view the help page. + */ +public class ShowPreviewEvent extends BaseEvent { + private List commandSummaries; + + public ShowPreviewEvent(List commandSummaries) { + this.commandSummaries = commandSummaries; + } + + public List getPreviewInfo() { + return commandSummaries; + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + +} +``` +###### \java\seedu\todo\commons\util\StringUtil.java +``` java + /** + * Calculates the levenstein distance between the two strings and returns + * their closeness in a percentage score. + * @param s1 The first string + * @param s2 The second string + * @return The percentage score of their closeness + */ + public static double calculateClosenessScore(String s1, String s2) { + // empty string, not close at all + if (isEmpty(s1) || isEmpty(s2)) { + return 0d; + } + + s1 = s1.toLowerCase(); + s2 = s2.toLowerCase(); + int distance = StringUtils.getLevenshteinDistance(s1, s2); + double ratio = ((double) distance) / (Math.max(s1.length(), s2.length())); + return 100 - ratio * 100; + } +} +``` +###### \java\seedu\todo\logic\commands\CommandPreview.java +``` java + +/** + * Represents all relevant commands that will be used to show to the user. + */ +public class CommandPreview { + private static final int COMMAND_INDEX = 0; + private static final double CLOSENESS_THRESHOLD = 50d; + private List commandSummaries; + + public CommandPreview(String userInput) { + commandSummaries = filterCommandSummaries(userInput); + } + + public List getPreview() { + return commandSummaries; + } + + private List filterCommandSummaries(String input) { + List summaries = new ArrayList<>(); + + if (StringUtil.isEmpty(input)) { + return summaries; + } + + List inputList = Lists.newArrayList(Splitter.on(" ") + .trimResults() + .omitEmptyStrings() + .split(input.toLowerCase())); + + String command = inputList.get(COMMAND_INDEX); + + CommandMap.getCommandMap().keySet().parallelStream().filter(key -> + StringUtil.calculateClosenessScore(key, command) > CLOSENESS_THRESHOLD || key.startsWith(command)) + .forEach(key -> summaries.addAll(CommandMap.getCommand(key).getCommandSummary())); + + return summaries; + } +} +``` +###### \java\seedu\todo\logic\Logic.java +``` java + /** + * Receives the intermediate product of the command and sends a ShowPreviewEvent. + * @param input The intermediate input as entered by the user. + */ + void preview(String input); +} +``` +###### \java\seedu\todo\logic\TodoLogic.java +``` java + @Override + public void preview(String input) { + List listOfCommands = new CommandPreview(input).getPreview(); + EventsCenter.getInstance().post(new ShowPreviewEvent(listOfCommands)); + } +} +``` +###### \java\seedu\todo\model\task\ValidationTask.java +``` java +public class ValidationTask implements MutableTask { + private static final String END_TIME = "endTime"; + private static final String TITLE = "title"; + private static final String ONLY_START_TIME_ERROR_MESSAGE = "You must define an ending time."; + private static final String TITLE_EMPTY_ERROR_MESSAGE = "Your title should not be empty."; + private static final String VALIDATION_ERROR_MESSAGE = "Your task is not in the correct format."; + private static final String START_AFTER_END_ERROR_MESSAGE = "No time travelling allowed! You've finished before you even start."; + + private ErrorBag errors = new ErrorBag(); + + private String title; + private String description; + private String location; + + private boolean pinned; + private boolean completed; + + private LocalDateTime startTime; + private LocalDateTime endTime; + + private Set tags = new HashSet<>(); + private LocalDateTime lastUpdated; + private UUID uuid; + + public ValidationTask(String title) { + this.setTitle(title); + this.setCreatedAt(); + this.uuid = UUID.randomUUID(); + } + + /** + * Constructs a ValidationTask from an ImmutableTask + */ + public ValidationTask(ImmutableTask task) { + this.setTitle(task.getTitle()); + this.setDescription(task.getDescription().orElse(null)); + this.setLocation(task.getLocation().orElse(null)); + this.setStartTime(task.getStartTime().orElse(null)); + this.setEndTime(task.getEndTime().orElse(null)); + this.setCompleted(task.isCompleted()); + this.setPinned(task.isPinned()); + this.setCreatedAt(); + this.uuid = task.getUUID(); + } + + /** + * Validates the task by checking the individual fields are valid. + */ + public void validate() throws ValidationException { + isValidTime(); + isValidTitle(); + errors.validate(VALIDATION_ERROR_MESSAGE); + } + + private void isValidTitle() { + if (StringUtil.isEmpty(title)) { + errors.put(TITLE, TITLE_EMPTY_ERROR_MESSAGE); + } + } + + /** + * Validates time. Only valid when + * 1) both time fields are not declared + * 2) end time is present + * 3) start time is before end time + */ + private void isValidTime() { + if (startTime == null && endTime == null) { + return; + } else if (endTime == null) { + errors.put(END_TIME, ONLY_START_TIME_ERROR_MESSAGE); + } else if (startTime != null && startTime.isAfter(endTime)) { + errors.put(END_TIME, START_AFTER_END_ERROR_MESSAGE); + } + } + + /** + * Converts the validation task into an actual task for consumption. + * + * @return A task with observable properties + */ + public Task convertToTask() throws ValidationException { + validate(); + return new Task(this); + } + + @Override + public String getTitle() { + return title; + } + + @Override + public Optional getDescription() { + return Optional.ofNullable(description); + } + + @Override + public Optional getLocation() { + return Optional.ofNullable(location); + } + + @Override + public Optional getStartTime() { + return Optional.ofNullable(startTime); + } + + @Override + public Optional getEndTime() { + return Optional.ofNullable(endTime); + } + + @Override + public boolean isPinned() { + return pinned; + } + + @Override + public boolean isCompleted() { + return completed; + } + + @Override + public Set getTags() { + return Collections.unmodifiableSet(tags); + } + + public LocalDateTime getCreatedAt() { return lastUpdated; } + + @Override + public UUID getUUID() { + return uuid; + } + + @Override + public void setTitle(String title) { + this.title = title; + } + + @Override + public void setDescription(String description) { + this.description = description; + } + + @Override + public void setLocation(String location) { + this.location = location; + } + + @Override + public void setPinned(boolean pinned) { + this.pinned = pinned; + } + + @Override + public void setCompleted(boolean completed) { + this.completed = completed; + } + + @Override + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + @Override + public void setEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + @Override + public void setTags(Set tags) { + this.tags = tags; + } + + public void setCreatedAt() { this.lastUpdated = LocalDateTime.now(); } + +} +``` +###### \java\seedu\todo\model\TodoList.java +``` java + @Override + public ImmutableTask add(String title) { + Task task = new Task(title); + tasks.add(task); + + saveTodoList(); + return task; + } + + @Override + public ImmutableTask add(String title, Consumer update) throws ValidationException { + ValidationTask validationTask = new ValidationTask(title); + update.accept(validationTask); + Task task = validationTask.convertToTask(); + tasks.add(task); + + saveTodoList(); + return task; + } +``` +###### \java\seedu\todo\model\TodoList.java +``` java + @Override + public ImmutableTask update(int index, Consumer update) throws ValidationException { + Task task = tasks.get(index); + ValidationTask validationTask = new ValidationTask(task); + update.accept(validationTask); + validationTask.validate(); + + // changes are validated and accepted + update.accept(task); + saveTodoList(); + return task; + } +``` +###### \java\seedu\todo\storage\LocalDateTimeAdapter.java +``` java +public class LocalDateTimeAdapter extends XmlAdapter { + + @Override + public LocalDateTime unmarshal(String v) throws Exception { + return LocalDateTime.parse(v); + } + + @Override + public String marshal(LocalDateTime v) throws Exception { + return v.toString(); + } +} +``` +###### \java\seedu\todo\ui\controller\CommandController.java +``` java + /** + * Handles a key stroke from input and sends it to logic. Once logic sends back a preview, it will be + * processed by {@link #handleCommandResult(CommandResult)} + * @param keyCode key pressed by user + * @param userInput text as shown in input view + */ + private void handleInput(KeyCode keyCode, String userInput) { + System.out.println("USER TYPED: " + userInput); + switch (keyCode) { + case ENTER : // Submitting command + //Note: Do not execute an empty command. TODO: This check should be done in the parser class. + if (!StringUtil.isEmpty(userInput)) { + CommandResult result = logic.execute(userInput); + handleCommandResult(result); + } + break; + default : // Typing command, show preview + logic.preview(userInput); + errorView.hideCommandErrorView(); // Don't show error when previewing + break; + } + } + + /** + * Handles a CommandResult object, and updates the user interface to reflect the result. + * @param result produced by {@link Logic} + */ + private void handleCommandResult(CommandResult result) { + previewView.hidePreviewPanel(); + displayMessage(result.getFeedback()); + if (result.isSuccessful()) { + viewDisplaySuccess(); + } else { + viewDisplayError(result.getErrors()); + } + } +``` +###### \java\seedu\todo\ui\UiManager.java +``` java + @Subscribe + private void handleShowHelpEvent(ShowHelpEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + mainWindow.getHelpView().displayCommandSummaries(event.getCommandSummaries()); + } +``` +###### \java\seedu\todo\ui\view\CommandInputView.java +``` java + /** + * Sets {@link #commandTextField} to listen out for keystrokes. + * Once a keystroke is received, calls {@link KeyStrokeCallback} interface to process this command. + */ + public void listenToInput(KeyStrokeCallback listener) { + this.commandTextField.addEventHandler(KeyEvent.KEY_RELEASED, event -> { + KeyCode keyCode = event.getCode(); + String textInput = commandTextField.getText(); + + boolean isNonEssential = keyCode.isNavigationKey() || + keyCode.isFunctionKey() || + keyCode.isMediaKey() || + keyCode.isModifierKey(); + + if (!isNonEssential) { + listener.onKeyStroke(keyCode, textInput); + } + }); + } +``` +###### \java\seedu\todo\ui\view\CommandInputView.java +``` java + /*Interface Declarations*/ + /** + * Defines an interface for controller class to receive a key stroke from this view class, and process it. + */ + public interface KeyStrokeCallback { + void onKeyStroke(KeyCode keyCode, String text); + } +} +``` diff --git a/collated/main/A0139021Ureused.md b/collated/main/A0139021Ureused.md new file mode 100644 index 000000000000..3b9cd1ef10dc --- /dev/null +++ b/collated/main/A0139021Ureused.md @@ -0,0 +1,307 @@ +# A0139021Ureused +###### \java\seedu\todo\model\task\Task.java +``` java +/** + * Represents a single task + */ +public class Task implements MutableTask { + private StringProperty title = new SimpleStringProperty(); + private StringProperty description = new SimpleStringProperty(); + private StringProperty location = new SimpleStringProperty(); + + private BooleanProperty pinned = new SimpleBooleanProperty(); + private BooleanProperty completed = new SimpleBooleanProperty(); + + private ObjectProperty startTime = new SimpleObjectProperty<>(); + private ObjectProperty endTime = new SimpleObjectProperty<>(); + + private ObjectProperty> tags = new SimpleObjectProperty<>(new HashSet()); + private LocalDateTime createdAt = LocalDateTime.now(); + private UUID uuid; + + /** + * Creates a new task + */ + public Task(String title) { + this.setTitle(title); + this.uuid = UUID.randomUUID(); + } + + /** + * Constructs a Task from a ReadOnlyTask + */ + public Task(ImmutableTask task) { + this.setTitle(task.getTitle()); + this.setDescription(task.getDescription().orElse(null)); + this.setLocation(task.getLocation().orElse(null)); + this.setStartTime(task.getStartTime().orElse(null)); + this.setEndTime(task.getEndTime().orElse(null)); + this.setCompleted(task.isCompleted()); + this.setPinned(task.isPinned()); + this.setCreatedAt(task.getCreatedAt()); + this.uuid = task.getUUID(); + this.setTags(task.getTags()); + } + + @Override + public String getTitle() { + return title.get(); + } + + @Override + public Optional getDescription() { + return Optional.ofNullable(description.get()); + } + + @Override + public Optional getLocation() { + return Optional.ofNullable(location.get()); + } + + @Override + public Optional getStartTime() { + return Optional.ofNullable(startTime.get()); + } + + @Override + public Optional getEndTime() { + return Optional.ofNullable(endTime.get()); + } + + @Override + public boolean isPinned() { + return pinned.get(); + } + + @Override + public boolean isCompleted() { + return completed.get(); + } + + @Override + public Set getTags() { + return Collections.unmodifiableSet(tags.get()); + } + + @Override + public LocalDateTime getCreatedAt() { return createdAt; } + + @Override + public void setTitle(String title) { + this.title.set(title); + } + + @Override + public void setPinned(boolean pinned) { + this.pinned.set(pinned); + } + + @Override + public void setCompleted(boolean completed) { + this.completed.set(completed); + } + + @Override + public void setDescription(String description) { + this.description.set(description); + } + + @Override + public void setLocation(String location) { + this.location.set(location); + } + + @Override + public void setStartTime(LocalDateTime startTime) { + this.startTime.set(startTime); + } + + @Override + public void setEndTime(LocalDateTime endTime) { + this.endTime.set(endTime); + } + + @Override + public void setTags(Set tags) { + this.tags.set(tags); + } + + public void setCreatedAt(LocalDateTime createdAt) { + if (createdAt == null) { + createdAt = LocalDateTime.now(); + } + + this.createdAt = createdAt; + } + + public Observable[] getObservableProperties() { + return new Observable[] { + title, description, location, startTime, endTime, tags, completed, pinned, + }; + } + + @Override + public UUID getUUID() { + return uuid; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof ImmutableTask)) { + return false; + } + + return uuid.equals(((ImmutableTask) o).getUUID()); + } + + @Override + public int hashCode() { + return uuid.hashCode(); + } + + @Override + public String toString() { + return title.get(); + } +} +``` +###### \java\seedu\todo\storage\XmlAdaptedTask.java +``` java +/** + * JAXB-friendly version of the task. + */ +public class XmlAdaptedTask { + + @XmlElement(required = true) + private String title; + @XmlElement + private String description; + @XmlElement + private String location; + + @XmlElement(required = true) + private boolean pinned; + @XmlElement(required = true) + private boolean completed; + + @XmlJavaTypeAdapter(LocalDateTimeAdapter.class) + private LocalDateTime startTime; + @XmlJavaTypeAdapter(LocalDateTimeAdapter.class) + private LocalDateTime endTime; + + @XmlElement(required = true) + @XmlJavaTypeAdapter(LocalDateTimeAdapter.class) + private LocalDateTime lastUpdated; + @XmlElement(required = true) + private UUID uuid; + + @XmlElement + private Set tags = new HashSet<>(); + + /** + * No-arg constructor for JAXB use. + */ + public XmlAdaptedTask() { + } + + /** + * Converts a given Task into this class for JAXB use. + * + * @param source + * future changes to this will not affect the created + * XmlAdaptedPerson + */ + public XmlAdaptedTask(ImmutableTask source) { + title = source.getTitle(); + description = source.getDescription().orElse(null); + location = source.getLocation().orElse(null); + + pinned = source.isPinned(); + completed = source.isCompleted(); + + startTime = source.getStartTime().orElse(null); + endTime = source.getEndTime().orElse(null); + + for (Tag tag : source.getTags()) { + tags.add(new XmlAdaptedTag(tag)); + } + + lastUpdated = source.getCreatedAt(); + uuid = source.getUUID(); + } + + /** + * Converts this jaxb-friendly adapted task object into the model's Task + * object. + * + * @throws IllegalValueException + * if there were any data constraints violated in the adapted + * person + */ + public Task toModelType() throws IllegalValueException { + Task task = new Task(title); + task.setDescription(description); + task.setLocation(location); + + task.setPinned(pinned); + task.setCompleted(completed); + + task.setStartTime(startTime); + task.setEndTime(endTime); + + Set setOfTags = new HashSet<>(); + for (XmlAdaptedTag tag : tags) { + setOfTags.add(tag.toModelType()); + } + task.setTags(setOfTags); + + task.setCreatedAt(lastUpdated); + return task; + } +} +``` +###### \java\seedu\todo\storage\XmlSerializableTodoList.java +``` java +/** + * An Immutable TodoList that is serializable to XML format + */ +@XmlRootElement(name = "todolist") +public class XmlSerializableTodoList implements ImmutableTodoList { + + @XmlElement + private List tasks; + + { + tasks = new ArrayList<>(); + } + + /** + * Empty constructor required for marshalling + */ + public XmlSerializableTodoList() {} + + /** + * Conversion + */ + public XmlSerializableTodoList(ImmutableTodoList src) { + tasks.addAll(src.getTasks().stream().map(XmlAdaptedTask::new).collect(Collectors.toList())); + } + + @Override + public List getTasks() { + return tasks.stream().map(p -> { + try { + return p.toModelType(); + } catch (IllegalValueException e) { + e.printStackTrace(); + //TODO: better error handling + return null; + } + }).collect(Collectors.toCollection(ArrayList::new)); + } +} +``` diff --git a/collated/main/reused.md b/collated/main/reused.md new file mode 100644 index 000000000000..3eb45dbb1e10 --- /dev/null +++ b/collated/main/reused.md @@ -0,0 +1,100 @@ +# reused +###### \resources\style\DefaultStyle.css +``` css +/**Modern UI***/ +/* + * Metro style Push Button + * Author: Pedro Duque Vieira + * http://pixelduke.wordpress.com/2012/10/23/jmetro-windows-8-controls-on-java/ + */ +.button { + -fx-padding: 5 22 5 22; + -fx-border-color: #e2e2e2; + -fx-border-width: 2; + -fx-background-radius: 0; + -fx-background-color: #1d1d1d; + -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; + -fx-font-size: 11pt; + -fx-text-fill: #d8d8d8; + -fx-background-insets: 0 0 0 0, 0, 1, 2; +} + +.button:hover { + -fx-background-color: #3a3a3a; +} + +.button:pressed, .button:default:hover:pressed { + -fx-background-color: white; + -fx-text-fill: #1d1d1d; +} + +.button:focused { + -fx-border-color: white, white; + -fx-border-width: 1, 1; + -fx-border-style: solid, segments(1, 1); + -fx-border-radius: 0, 0; + -fx-border-insets: 1 1 1 1, 0; +} + +.button:disabled, .button:default:disabled { + -fx-opacity: 0.4; + -fx-background-color: #1d1d1d; + -fx-text-fill: white; +} + +.button:default { + -fx-background-color: -fx-focus-color; + -fx-text-fill: #ffffff; +} + +.button:default:hover { + -fx-background-color: derive(-fx-focus-color, 30%); +} + +.dialog-pane { + -fx-background-color: #1d1d1d; +} + +.dialog-pane > *.button-bar > *.container { + -fx-background-color: #1d1d1d; +} + +.dialog-pane > *.label.content { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: white; +} + +.dialog-pane:header *.header-panel { + -fx-background-color: derive(#1d1d1d, 25%); +} + +.dialog-pane:header *.header-panel *.label { + -fx-font-size: 18px; + -fx-font-style: italic; + -fx-fill: white; + -fx-text-fill: white; +} + +.scroll-bar .thumb { + -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-insets: 3; +} + +.scroll-bar .increment-button, .scroll-bar .decrement-button { + -fx-background-color: transparent; + -fx-padding: 0 0 0 0; +} + +.scroll-bar .increment-arrow, .scroll-bar .decrement-arrow { + -fx-shape: " "; +} + +.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow { + -fx-padding: 1 8 1 8; +} + +.scroll-bar:horizontal .increment-arrow, .scroll-bar:horizontal .decrement-arrow { + -fx-padding: 8 1 8 1; +} +``` diff --git a/collated/test/A0092382A.md b/collated/test/A0092382A.md new file mode 100644 index 000000000000..dafc60039636 --- /dev/null +++ b/collated/test/A0092382A.md @@ -0,0 +1,251 @@ +# A0092382A +###### \java\seedu\todo\logic\commands\AddCommandTest.java +``` java +public class AddCommandTest extends CommandTest { + @Override + protected BaseCommand commandUnderTest() { + return new AddCommand(); + } + + @Test + public void testAddTask() throws Exception { + setParameter("Hello World"); + EventsCollector eventsCollector = new EventsCollector(); + execute(true); + ImmutableTask addedTask = getTaskAt(1); + assertEquals("Hello World", addedTask.getTitle()); + assertFalse(addedTask.isPinned()); + assertFalse(addedTask.getDescription().isPresent()); + assertFalse(addedTask.getLocation().isPresent()); + assertThat(eventsCollector.get(0), instanceOf(HighlightTaskEvent.class)); + assertThat(eventsCollector.get(1), instanceOf(ExpandCollapseTaskEvent.class)); + } + + @Test + public void testAddTaskWithLocation() throws Exception { + setParameter("Hello NUS"); + setParameter("l", "NUS"); + execute(true); + + ImmutableTask taskWithLocation = getTaskAt(1); + + assertEquals("Hello NUS", taskWithLocation.getTitle()); + assertFalse(taskWithLocation.isPinned()); + assertFalse(taskWithLocation.getDescription().isPresent()); + assertEquals("NUS", taskWithLocation.getLocation().get()); + } + + @Test + public void testAddTaskWithDescription() throws Exception { + setParameter("Destroy World"); + setParameter("m", "Remember to get Dynamites on sale!"); + execute(true); + + ImmutableTask taskWithDescription = getTaskAt(1); + + assertEquals("Destroy World", taskWithDescription.getTitle()); + assertEquals("Remember to get Dynamites on sale!", taskWithDescription.getDescription().get()); + assertFalse(taskWithDescription.isPinned()); + assertFalse(taskWithDescription.getLocation().isPresent()); + } + + @Test + public void testAddPinnedTask() throws Exception { + setParameter("Li Kai's Presentation"); + setParameter("p", null); + execute(true); + + ImmutableTask pinnedAddedTask = getTaskAt(1); + + assertEquals("Li Kai's Presentation", pinnedAddedTask.getTitle()); + assertTrue(pinnedAddedTask.isPinned()); + assertFalse(pinnedAddedTask.getDescription().isPresent()); + assertFalse(pinnedAddedTask.getLocation().isPresent()); + } + + @Test + public void testAddSingleDate() throws Exception { + setParameter("Test Task"); + setParameter("d", "tomorrow 9am"); + execute(true); + + ImmutableTask task = getTaskAt(1); + assertFalse(task.isEvent()); + assertEquals(TimeUtil.tomorrow().withHour(9), task.getEndTime().get()); + } + + @Test + public void testAddDateRange() throws Exception { + setParameter("Test Event"); + setParameter("d", "tomorrow 6 to 8pm"); + execute(true); + + ImmutableTask task = getTaskAt(1); + assertTrue(task.isEvent()); + assertEquals(TimeUtil.tomorrow().withHour(18), task.getStartTime().get()); + assertEquals(TimeUtil.tomorrow().withHour(20), task.getEndTime().get()); + } + + @Test + public void testAddMultipleParameters() throws Exception { + setParameter("Task 1"); + setParameter("p", null); + setParameter("l", "COM1"); + setParameter("m", "Useless task"); + execute(true); + + ImmutableTask taskWithParams = getTaskAt(1); + + assertEquals("Task 1", taskWithParams.getTitle()); + assertTrue(taskWithParams.isPinned()); + assertEquals("COM1", taskWithParams.getLocation().get()); + assertEquals("Useless task", taskWithParams.getDescription().get()); + } + + @Test + public void testAdd_switchViewsNecessary() throws Exception { + model.view(TaskViewFilter.COMPLETED); + assertTotalTaskCount(0); + setParameter("Task 1"); + setParameter("p", null); + setParameter("l", "COM1"); + setParameter("m", "Useless task"); + execute(true); + assertEquals(model.getViewFilter().get(), TaskViewFilter.DEFAULT); + assertTotalTaskCount(1); + assertVisibleTaskCount(1); + } + + @Test + public void testAdd_switchViewsUnnecessary() throws Exception { + model.view(TaskViewFilter.INCOMPLETE); + assertTotalTaskCount(0); + setParameter("Task 1"); + setParameter("p", null); + setParameter("l", "COM1"); + setParameter("m", "Useless task"); + execute(true); + assertEquals(model.getViewFilter().get(), TaskViewFilter.INCOMPLETE); + assertTotalTaskCount(1); + assertVisibleTaskCount(1); + } + + +} +``` +###### \java\seedu\todo\logic\commands\FindCommandTest.java +``` java +public class FindCommandTest extends CommandTest { + + @Override + protected BaseCommand commandUnderTest() { + return new FindCommand(); + } + + @Before + public void setUp() throws Exception { + model.add("CS2101 Project Task"); + model.add("CS2103T project"); + model.add("Unrelated task"); + model.add("Unrelated CS2101 that expands"); + } + + @Test + public void testFindSuccessful() throws ValidationException { + assertNull(model.getSearchStatus().getValue()); + assertVisibleTaskCount(4); + setParameter("CS2101"); + execute(true); + assertVisibleTaskCount(2); + assertNotNull(model.getSearchStatus().getValue()); + } + + @Test + public void testCaseInsensitive() throws ValidationException { + setParameter("project"); + execute(true); + assertVisibleTaskCount(2); + } + + @Test + public void testMultipleParameters() throws ValidationException { + setParameter("task expands"); + execute(true); + assertVisibleTaskCount(3); + } + + @Test + public void testUnsuccessfulFind() throws ValidationException { + setParameter("team"); + execute(true); + assertVisibleTaskCount(0); + } + +``` +###### \java\seedu\todo\logic\commands\PinCommandTest.java +``` java +public class PinCommandTest extends CommandTest { + + @Override + protected BaseCommand commandUnderTest() { + return new PinCommand(); + } + + @Before + public void setUp() throws Exception { + model.add("Task 3"); + model.add("Task 2"); + model.add("Task 1", task -> task.setPinned(true)); + } + + private long getPinnedCount() { + return model.getObservableList().stream().filter(ImmutableTask::isPinned).count(); + } + + @Test + public void testPinFirst() throws Exception { + setParameter("3"); + EventsCollector eventsCollector = new EventsCollector(); + execute(true); + + assertEquals(2, getPinnedCount()); + assertThat(eventsCollector.get(0), instanceOf(HighlightTaskEvent.class)); + } + + @Test + public void testUnpinFirst() throws Exception { + setParameter("1"); + execute(true); + + assertEquals(0, getPinnedCount()); + } +} +``` +###### \java\seedu\todo\logic\commands\ShowCommandTest.java +``` java +public class ShowCommandTest extends CommandTest { + + @Override + protected BaseCommand commandUnderTest() { + return new ShowCommand(); + } + + @Before + public void setUp() throws Exception { + model.add("Task 1"); + model.add("Task 2"); + model.add("Task 3"); + + } + + @Test + public void test() throws ValidationException { + EventsCollector eventCollector = new EventsCollector(); + setParameter("2"); + execute(true); + assertThat(eventCollector.get(0), instanceOf(ExpandCollapseTaskEvent.class)); + assertEquals("Task 2", ((ExpandCollapseTaskEvent) eventCollector.get(0)).task.getTitle()); + } + +} +``` diff --git a/collated/test/A0135805H.md b/collated/test/A0135805H.md new file mode 100644 index 000000000000..8a8b28174656 --- /dev/null +++ b/collated/test/A0135805H.md @@ -0,0 +1,1284 @@ +# A0135805H +###### \java\guitests\AddCommandTest.java +``` java +/** + * Test the add command via GUI. + * Note: + * Order-ness of the tasks is not tested. + * Invalid command input is not tested. + */ +public class AddCommandTest extends TodoListGuiTest { + + @Test + public void add_initialData() { + //Test if the data has correctly loaded the data into view. + assertTrue(todoListView.isDisplayedCorrectly()); + } + + @Test + public void add_addTasks() { + //Add a task + ImmutableTask task1 = TaskFactory.task(); + executeAddTestHelper(task1); + + //Add another task + ImmutableTask task2 = TaskFactory.task(); + executeAddTestHelper(task2); + + //Add duplicated task + executeAddTestHelper(task2); + } + + @Test + public void add_addEvents() { + //Add an event + ImmutableTask event1 = TaskFactory.event(); + executeAddTestHelper(event1); + + //Add another event + ImmutableTask event2 = TaskFactory.event(); + executeAddTestHelper(event2); + + //Add duplicated task + executeAddTestHelper(event1); + } + + @Test + public void add_addManyRandom() { + //Add a long list of random task, which in the end spans at least 2 pages. + List randomTaskList = TaskFactory.list(15, 25); + randomTaskList.forEach(this::executeAddTestHelper); + } + + /* Helper Methods */ + /** + * Gets the index of the newly added task. + */ + private int getNewlyAddedTaskIndex() { + return TestUtil.compareAndGetIndex(previousTasksFromView, todoListView.getImmutableTaskList()); + } + + /** + * A helper method to run the entire add command process and testing. + */ + private void executeAddTestHelper(ImmutableTask task) { + updatePreviousTaskListFromView(); + executeAddCommand(task); + assertCorrectnessHelper(task); + } + + /** + * Executes an add command given a {@code task} + */ + private void executeAddCommand(ImmutableTask task) { + String commandText = CommandGeneratorUtil.generateAddCommand(task); + runCommand(commandText); + } + + private void assertCorrectnessHelper(ImmutableTask newTask) { + int addedIndex = getNewlyAddedTaskIndex(); + TaskCardViewHandle taskCardHandle = todoListView.getTaskCardViewHandle(addedIndex); + + assertAddSuccess(taskCardHandle, newTask, addedIndex); + assertCorrectFeedbackDisplayed(newTask); + assertCollapsed(taskCardHandle); + } + + /** + * Check the two following areas: + * 1. If the {@code task} added to the view is reflected correctly in it's own task card view. + * 2. If the remaining task cards are present and still displayed correctly in their own + * respective card views. + */ + private void assertAddSuccess(TaskCardViewHandle newTaskHandle, ImmutableTask newTask, int addedListIndex) { + int expectedDisplayedIndex = UiTestUtil.convertToUiIndex(addedListIndex); + + //Test for the newly added task. + assertTrue(newTaskHandle.isDisplayedCorrectly(expectedDisplayedIndex, newTask)); + + //Test for remaining tasks. + assertTrue(todoListView.isDisplayedCorrectly()); + } + + /** + * Check if the correct feedback message for adding has been displayed to the user. + */ + private void assertCorrectFeedbackDisplayed(ImmutableTask task) { + assertFeedbackMessage("\'" + task.getTitle() + "\' successfully added!"); + } + + /** + * Tasks should be collapsed when newly added. + * Checks the case where if the task is collapsible, then the task is in the collapsed state when newly added. + */ + private void assertCollapsed(TaskCardViewHandle newTask) { + if (newTask.isTaskCollapsible()) { + assertTrue(newTask.isTaskCardCollapsed()); + } + } +} +``` +###### \java\guitests\DeleteCommandTest.java +``` java +/** + * Test the delete command via GUI. + * Note: + * Invalid indices are not tested. + */ +public class DeleteCommandTest extends TodoListGuiTest { + + @Override + protected TodoList getInitialData() { + return getInitialDataHelper(10, 20); + } + + @Test + public void delete_correctBoundary() { + //delete the last item in the list + executeDeleteHelper(initialTaskData.size()); + + //delete the first in the list (note that we initialised the size to be > 2. + executeDeleteHelper(1); + + } + + @Test + public void delete_allTasks() { + //delete all the elements, until you have none left. + Random random = new Random(); + int remainingTasks = initialTaskData.size(); + while (remainingTasks > 0) { + int randomChoice = random.nextInt(remainingTasks--) + 1; + executeDeleteHelper(randomChoice); + } + } + + /** + * A helper method to run the entire delete command process and testing. + */ + private void executeDeleteHelper(int displayedIndex) { + ImmutableTask deletedTask = executeDeleteCommand(displayedIndex); + assertDeleteSuccess(deletedTask); + assertCorrectFeedbackDisplayed(deletedTask); + } + + /** + * Deletes a task from the to-do list view, and returns the deleted task for verification. + */ + private ImmutableTask executeDeleteCommand(int displayedIndex) { + int listIndex = UiTestUtil.convertToListIndex(displayedIndex); + String commandText = CommandGeneratorUtil.generateDeleteCommand(displayedIndex); + ImmutableTask deletedTask = todoListView.getTask(listIndex); + + runCommand(commandText); + return deletedTask; + } + + /** + * Check if the {@code task} deleted from the view is reflected correctly (i.e. it's no longer there) + * and check if the remaining tasks are displayed correctly. + */ + private void assertDeleteSuccess(ImmutableTask deletedTask) { + //Check if the task is really deleted. + assertFalse(todoListView.getImmutableTaskList().contains(deletedTask)); + + //Test if the remaining list is correct. + assertTrue(todoListView.isDisplayedCorrectly()); + } + + /** + * Check if the correct feedback message for deleting has been displayed to the user. + */ + private void assertCorrectFeedbackDisplayed(ImmutableTask task) { + assertFeedbackMessage("\'" + task.getTitle() + "\' successfully deleted!"); + } + +} +``` +###### \java\guitests\guihandles\CommandFeedbackViewHandle.java +``` java +/** + * A handler for retrieving feedback to user via {@link CommandFeedbackView} + */ +public class CommandFeedbackViewHandle extends GuiHandle { + /* Constants */ + public static final String FEEDBACK_VIEW_LABEL_ID = "#commandFeedbackLabel"; + + /** + * Constructs a handle to the {@link CommandFeedbackViewHandle} + * + * @param guiRobot {@link GuiRobot} for the current GUI test. + * @param primaryStage The stage where the views for this handle is located. + */ + public CommandFeedbackViewHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + /** + * Get the feedback {@link Label} object. + */ + private Label getFeedbackLabel() { + return (Label) getNode(FEEDBACK_VIEW_LABEL_ID); + } + + /** + * Get the text that is displayed on this {@link #getFeedbackLabel()} object. + */ + public String getText() { + return getFeedbackLabel().getText(); + } + + /** + * Returns true if the {@code feedbackMessage} matches the displayed message in the view. + */ + public boolean doesFeedbackMessageMatch(String feedbackMessage) { + return this.getText().equals(feedbackMessage); + } + + /** + * Returns true if this feedback view has error style applied. + */ + public boolean isErrorStyleApplied() { + return UiTestUtil.containsStyleClass(getFeedbackLabel(), "error"); + } +} +``` +###### \java\guitests\guihandles\CommandInputViewHandle.java +``` java +/** + * A handle to the {@link CommandInputView}'s + * command text box in the GUI. + */ +public class CommandInputViewHandle extends GuiHandle { + /* Constants */ + private static final String COMMAND_INPUT_FIELD_ID = "#commandTextField"; + + /** + * Constructs a handle to the {@link CommandInputView} + * + * @param guiRobot {@link GuiRobot} for the current GUI test. + * @param primaryStage The stage where the views for this handle is located. + */ + public CommandInputViewHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + /* Interfacing Methods */ + /** + * Types in the supplied command into the text box in {@link CommandInputView}. + * Note: any existing text in the text box will be replaced. + * Note: this method will not execute the command. See {@link #runCommand(String)}. + * @param command Command text to be supplied into the text box. + */ + public void enterCommand(String command) { + setTextAreaText(COMMAND_INPUT_FIELD_ID, command); + } + + /** + * Gets the command text that is inside the text box in {@link CommandInputView}. + * @return A command text. + */ + public String getCommandInput() { + return getTextAreaText(COMMAND_INPUT_FIELD_ID); + } + + /** + * Enters the given command in the Command Box and presses enter. + * @param command Command text to be executed. + */ + public void runCommand(String command) { + enterCommand(command); + pressEnter(); + guiRobot.sleep(GUI_SLEEP_DURATION); //Give time for the command to take effect + } + + /* Text View Helper Methods */ + /** + * Gets the text stored in a text area given the id to the text area + * + * @param textFieldId ID of the text area. + * @return Returns the text that is contained in the text area. + */ + private String getTextAreaText(String textFieldId) { + return ((TextArea) getNode(textFieldId)).getText(); + } + + /** + * Keys in the given {@code newText} to the specified text area given its ID. + * + * @param textFieldId ID for the text area. + * @param newText Text to be keyed in to the text area. + */ + private void setTextAreaText(String textFieldId, String newText) { + guiRobot.clickOn(textFieldId); + TextArea textArea = (TextArea)guiRobot.lookup(textFieldId).tryQuery().get(); + Platform.runLater(() -> textArea.setText(newText)); + guiRobot.sleep(GUI_SLEEP_DURATION); // so that the texts stays visible on the GUI for a short period + } +} +``` +###### \java\guitests\guihandles\TaskCardViewHandle.java +``` java +/** + * Provides a handle to a {@link TaskCardView} + * that exists in {@link TodoListView} + */ +public class TaskCardViewHandle extends GuiHandle { + + /* Constants */ + private static final String TITLE_LABEL_ID = "#titleLabel"; + private static final String DESCRIPTION_LABEL_ID = "#descriptionLabel"; + private static final String DATE_LABEL_ID = "#dateLabel"; + private static final String LOCATION_LABEL_ID = "#locationLabel"; + + private static final String DESCRIPTION_BOX_ID = "#descriptionBox"; + private static final String DATE_BOX_ID = "#dateBox"; + private static final String LOCATION_BOX_ID = "#locationBox"; + + private static final String PIN_IMAGE_ID = "#pinImage"; + + private static final String TYPE_LABEL_ID = "#typeLabel"; + private static final String MOREINFO_LABEL_ID = "#moreInfoLabel"; + + /* Variables */ + private Node rootNode; + + /** + * Constructs a handle for {@link TaskCardView}. + * + * @param guiRobot The GUI test robot. + * @param primaryStage The main stage that is executed from the application's UI. + * @param rootNode Node that houses the contents of this Task Card. + */ + public TaskCardViewHandle(GuiRobot guiRobot, Stage primaryStage, Node rootNode){ + super(guiRobot, primaryStage, null); + this.rootNode = rootNode; + } + + /* Task Property Getters */ + public String getDisplayedTitle() { + return getTextFromLabel(TITLE_LABEL_ID); + } + + public String getDisplayedDescription() { + return getTextFromLabel(DESCRIPTION_LABEL_ID); + } + + public String getDisplayedDateText() { + return getTextFromLabel(DATE_LABEL_ID); + } + + public String getDisplayedLocation() { + return getTextFromLabel(LOCATION_LABEL_ID); + } + + public String getDisplayedTypeLabel() { + return getTextFromLabel(TYPE_LABEL_ID); + } + + public boolean getMoreInfoLabelVisibility() { + Node moreInfoLabel = getNode(MOREINFO_LABEL_ID); + return UiTestUtil.isDisplayed(moreInfoLabel); + } + + public boolean getDescriptionBoxVisibility() { + Node descriptionBox = getNode(DESCRIPTION_BOX_ID); + return UiTestUtil.isDisplayed(descriptionBox); + } + + public boolean getDateBoxVisibility() { + Node dateBox = getNode(DATE_BOX_ID); + return UiTestUtil.isDisplayed(dateBox); + } + + public boolean getLocationBoxVisibility() { + Node locationBox = getNode(LOCATION_BOX_ID); + return UiTestUtil.isDisplayed(locationBox); + } + + public boolean getPinImageVisibility() { + Node pinImage = getNode(PIN_IMAGE_ID); + return UiTestUtil.isDisplayed(pinImage); + } + + public boolean isSelectedStyleApplied() { + return UiTestUtil.containsStyleClass(rootNode, "selected"); + } + + public boolean isCompletedStyleApplied() { + return UiTestUtil.containsStyleClass(rootNode, "completed"); + } + + public boolean isOverdueStyleApplied() { + return UiTestUtil.containsStyleClass(rootNode, "overdue"); + } + + public boolean isTaskCardCollapsed() { + return UiTestUtil.containsStyleClass(rootNode, "collapsed"); + } + + public boolean isTaskCollapsible() { + return !getDisplayedDescription().isEmpty(); + } + + /* General Methods */ + /** + * Checks if the supplied index matches to what this node displays. + * To be used for nodes filtering. + */ + public boolean matchesTask(int displayedIndex) { + return getDisplayedTitle().startsWith(displayedIndex + ". "); + } + + /** + * Given an {@link ImmutableTask} and a displayed index, check if this view is displayed correctly. + * @param displayedIndex Index displayed in the view. + * @param task Task displayed in the view. + * @return Returns true only if and only if the elements in this view is displayed correctly. + * TODO: Maybe we do not need this at all. + */ + public boolean isDisplayedCorrectly(int displayedIndex, ImmutableTask task) { + //JUnit Assertion Test: To know which test are failing in detail + assertTrue(isTitleCorrect(displayedIndex, task)); + assertTrue(isDescriptionCorrect(task)); + assertTrue(isTaskCardCollapsedStateCorrect()); + assertTrue(isCompletedDisplayCorrect(task)); + assertTrue(isDateTextCorrect(task)); + assertTrue(isLocationCorrect(task)); + assertTrue(isTypeDisplayCorrect(task)); + assertTrue(isPinDisplayCorrect(task)); + assertTrue(isOverdueDisplayCorrect(task)); + return true; + } + + private boolean isTitleCorrect(int displayedIndex, ImmutableTask task) { + String expected = convertToDisplayedTitle(displayedIndex, task.getTitle()); + String actual = getDisplayedTitle(); + return expected.equals(actual); + } + + private boolean isDescriptionCorrect(ImmutableTask task) { + java.util.Optional description = task.getDescription(); + + if (description.isPresent()) { + //If there is a task description, it should match with the displayed description. + String expected = description.get(); + String actual = getDisplayedDescription(); + return expected.equals(actual); + } else { + //Description should be hidden when there is no description. + boolean expected = false; + boolean actual = getDescriptionBoxVisibility(); + return expected == actual; + } + } + + private boolean isDateTextCorrect(ImmutableTask task) { + java.util.Optional startTime = task.getStartTime(); + java.util.Optional endTime = task.getEndTime(); + + TimeUtil timeUtil = new TimeUtil(); + + String displayedDateText = getDisplayedDateText(); + String expectedDateText; + + if (!startTime.isPresent() && !endTime.isPresent()) { + //Date box should be hidden when there is no start and end time. + return !getDateBoxVisibility(); + } else if (startTime.isPresent() && endTime.isPresent()) { + //When start and end date are available, expect event format + expectedDateText = timeUtil.getEventTimeText(startTime.get(), endTime.get()); + } else if (endTime.isPresent()) { + //When only end time is present, expect deadline format + expectedDateText = timeUtil.getTaskDeadlineText(endTime.get()); + } else { + //Otherwise, illegal date state. + throw new IllegalStateException("Start time is present, but end time is not."); + } + return expectedDateText.equals(displayedDateText); + } + + private boolean isLocationCorrect(ImmutableTask task) { + java.util.Optional location = task.getLocation(); + + if (location.isPresent()) { + //If there is a location, it should match with the displayed location. + String expected = location.get(); + String actual = getDisplayedLocation(); + return expected.equals(actual); + } else { + //Description should be hidden when there is no description. + boolean expected = false; + boolean actual = getLocationBoxVisibility(); + return expected == actual; + } + } + + private boolean isPinDisplayCorrect(ImmutableTask task) { + boolean expected = task.isPinned(); + boolean actual = getPinImageVisibility(); + return expected == actual; + } + + private boolean isCompletedDisplayCorrect(ImmutableTask task) { + boolean expected = task.isCompleted(); + boolean actual = isCompletedStyleApplied(); + return expected == actual; + } + + private boolean isOverdueDisplayCorrect(ImmutableTask task) { + java.util.Optional endTime = task.getEndTime(); + boolean actual = isOverdueStyleApplied(); + boolean expected; + + if (endTime.isPresent() && !task.isEvent()) { + expected = seedu.todo.testutil.TimeUtil.isOverdue(endTime.get()); + } else { + expected = false; + } + return expected == actual; + } + + private boolean isTypeDisplayCorrect(ImmutableTask task) { + String actual = getDisplayedTypeLabel(); + String expected; + + if (task.isEvent()) { + expected = "Event"; + } else { + expected = "Task"; + } + + return expected.equals(actual); + } + + public boolean isTaskCardCollapsedStateCorrect() { + boolean collapsedStyleApplied = UiTestUtil.containsStyleClass(rootNode, "collapsed"); + boolean moreInfoLabelDisplayed = UiTestUtil.isDisplayed(getNode(MOREINFO_LABEL_ID)); + + if (isTaskCollapsible()) { + return collapsedStyleApplied == moreInfoLabelDisplayed; + } else { + return !moreInfoLabelDisplayed; + } + } + + /* View Elements Helper Methods */ + /** + * Search and returns exactly one matching node. + * + * @param fieldId Field ID to search inside the parent node. + * @return Returns one appropriate node that matches the {@code fieldId}. + * @throws NullPointerException when no node with {@code fieldId} can be found, intentionally breaking the tests. + */ + @Override + protected Node getNode(String fieldId) throws NullPointerException { + Optional node = guiRobot.from(rootNode).lookup(fieldId).tryQuery(); + if (node.isPresent()) { + return node.get(); + } else { + throw new NullPointerException("Node " + fieldId + " is not found."); + } + } + + /** + * Gets a text from the node with {@code fieldId}. + * + * @param fieldId To get the node's text from. + * @return Returns the text presented in the node. + */ + private String getTextFromLabel(String fieldId) { + return ((Label) getNode(fieldId)).getText(); + } + + /** + * Converts {@code actualTitle} to a displayed title with the relevant {@code displayedIndex}. + * @param displayedIndex The index that is shown on the title displayed to the user. + * @param actualTitle The actual title of the task. + * @return Returns a title text that is actually displayed to the user. + */ + private String convertToDisplayedTitle(int displayedIndex, String actualTitle) { + return displayedIndex + ". " + actualTitle; + } + + /* Override Methods */ + @Override + public boolean equals(Object obj) { + if(obj instanceof TaskCardViewHandle) { + TaskCardViewHandle handle = (TaskCardViewHandle) obj; + + boolean hasEqualTitle = this.getDisplayedTitle() + .equals(handle.getDisplayedTitle()); + boolean hasEqualDescription = this.getDisplayedDescription() + .equals(handle.getDisplayedDescription()); + boolean hasEqualDateText = this.getDisplayedDateText() + .equals(handle.getDisplayedDateText()); + boolean hasEqualLocation = this.getDisplayedLocation() + .equals(handle.getDisplayedLocation()); + boolean hasEqualType = this.getDisplayedTypeLabel() + .equals(handle.getDisplayedTypeLabel()); + boolean hasEqualMoreInfoVisibility = this.getMoreInfoLabelVisibility() + == handle.getMoreInfoLabelVisibility(); + boolean hasEqualDescriptionBoxVisibility = this.getDescriptionBoxVisibility() + == handle.getDescriptionBoxVisibility(); + boolean hasEqualDateBoxVisibility = this.getDateBoxVisibility() + == handle.getDateBoxVisibility(); + boolean hasEqualLocationBoxVisibility = this.getLocationBoxVisibility() + == handle.getLocationBoxVisibility(); + boolean hasEqualPinImageVisibility = this.getPinImageVisibility() + == handle.getPinImageVisibility(); + boolean hasEqualSelectedStyleApplied = this.isSelectedStyleApplied() + == handle.isSelectedStyleApplied(); + boolean hasEqualCompletedStyleApplied = this.isCompletedStyleApplied() + == handle.isCompletedStyleApplied(); + boolean hasEqualOverdueStyleApplied = this.isOverdueStyleApplied() + == handle.isOverdueStyleApplied(); + + return hasEqualTitle && hasEqualDescription && hasEqualDateText + && hasEqualLocation && hasEqualType && hasEqualMoreInfoVisibility + && hasEqualDescriptionBoxVisibility && hasEqualDateBoxVisibility + && hasEqualLocationBoxVisibility && hasEqualPinImageVisibility + && hasEqualSelectedStyleApplied && hasEqualCompletedStyleApplied + && hasEqualOverdueStyleApplied; + } + return super.equals(obj); + } + + @Override + public String toString() { + return getDisplayedTitle() + " " + getDisplayedDescription(); + } +} +``` +###### \java\guitests\guihandles\TodoListViewHandle.java +``` java +/** + * Provides a handle for the {@link TodoListView} + * containing a list of tasks. + */ +public class TodoListViewHandle extends GuiHandle { + + /* Constants */ + public static final int NOT_FOUND = -1; + private static final String TASK_CARD_ID = "#taskCard"; + private static final String TODO_LIST_VIEW_ID = "#todoListView"; + + /** + * Constructs a handle for {@link TodoListView}. + * + * @param guiRobot The GUI test robot. + * @param primaryStage The main stage that is executed from the application's UI. + */ + public TodoListViewHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + /* View Element Helper Methods */ + /** + * Gets an instance of {@link ListView} of {@link TodoListView} + */ + public ListView getTodoListView() { + return (ListView) getNode(TODO_LIST_VIEW_ID); + } + + /** + * Gets a list of {@link ImmutableTask} + */ + public List getImmutableTaskList() { + return getTodoListView().getItems(); + } + + /** + * Gets a set of task card nodes in this to-do list. + */ + public Set getAllTaskCardNodes() { + return guiRobot.lookup(TASK_CARD_ID).queryAll(); + } + + /** + * Gets a specific task from the list of tasks stored in the view by the list index. + */ + public ImmutableTask getTask(int listIndex) { + return getImmutableTaskList().get(listIndex); + } + + /** + * Gets the first occurring element from {@link #getImmutableTaskList()} + * where the detail matches the {@code task} param. + * + * @return Returns a value bounded from 0 to length - 1. + * Also returns {@code NOT_FOUND} (value of -1) if not found in the list. + */ + public int getFirstTaskIndex(ImmutableTask task) { + List tasks = getImmutableTaskList(); + for (int listIndex = 0; listIndex < tasks.size(); listIndex++) { + ImmutableTask taskInList = tasks.get(listIndex); + if (TestUtil.isShallowEqual(task, taskInList)) { + return listIndex; + } + } + return NOT_FOUND; + } + + /** + * Gets a {@link TaskCardViewHandle} object with the position {@code listIndex} located in the to-do list view. + * Guaranteed unique result for identical tasks. + * Inherits the behaviour from {@link #getTaskCardViewNode(int)}. + * + * @param listIndex Index of the {@link #getImmutableTaskList()}. + * @return An instance of the handle. + */ + public TaskCardViewHandle getTaskCardViewHandle(int listIndex) { + Node taskCardNode = getTaskCardViewNode(listIndex); + if (taskCardNode != null) { + return new TaskCardViewHandle(guiRobot, primaryStage, taskCardNode); + } else { + return null; + } + } + + /** + * Gets a {@link Node} object with the position {@code listIndex} located in the to-do list view. + * Guarantees: + * - Unique result for identical tasks, because we are referencing from the index displayed + * in the UI. + * - The object returned will never be null. + * This means if I can't find the object, an exception will be thrown. + * Behaviour: + * Because a task card node is only drawn when the task card is shown on the screen, + * this method will automatically scroll the to-do list view to the position where the + * node is drawn. + * + * @param listIndex Index of the {@link #getImmutableTaskList()} + * (must be a valid value from 0 to length of task list - 1, + * else {@link RuntimeException} is thrown. + * @return An instance of the node. Guarantees non-null. + * @throws NodeFinderException if we can't find the node after finite attempts. + */ + private Node getTaskCardViewNode(int listIndex) throws NodeFinderException { + int displayedIndex = UiTestUtil.convertToUiIndex(listIndex); + Optional possibleNode; + int attemptCounter = 0; + + do { + Platform.runLater(() -> getTodoListView().scrollTo(listIndex)); + guiRobot.sleep(50 * attemptCounter++); //Allow the new nodes to be loaded from scrolling. + + Set taskCardNodes = getAllTaskCardNodes(); + possibleNode = taskCardNodes.stream().filter(node -> { + TaskCardViewHandle taskCardView = new TaskCardViewHandle(guiRobot, primaryStage, node); + return taskCardView.matchesTask(displayedIndex); + }).findFirst(); + } while (!possibleNode.isPresent() && attemptCounter < 50); + + if (possibleNode.isPresent()) { + return possibleNode.get(); + } else { + String errorMessage = "Either the node fails to draw on the screen, or you provided an invalid index " + + listIndex + " where the number of nodes is " + getAllTaskCardNodes().size(); + throw new NodeFinderException(errorMessage, NodeFinderException.ErrorType.NO_NODES_FOUND); + } + } + + /** + * Checks if all the tasks stored inside the to-do list are displayed correctly. + * Note: This does not check the sorted-ness of the list. + * @return True if all the items are correctly displayed. + */ + public boolean isDisplayedCorrectly() { + return doesTodoListMatch(); + } + + /** + * Given a list of tasks, check if all the tasks in the list are displayed correctly in the + * {@link TodoListView}. + * Note: this does not check the sorted-ness of the list. + * + * @return True if all the tasks in the {@code tasks} are displayed correctly. + */ + public boolean doesTodoListMatch() { + boolean outcome = true; + List tasks = getImmutableTaskList(); + for (int listIndex = 0; listIndex < tasks.size(); listIndex++) { + ImmutableTask task = tasks.get(listIndex); + TaskCardViewHandle handle = getTaskCardViewHandle(listIndex); + int displayedIndex = UiTestUtil.convertToUiIndex(listIndex); + outcome &= handle.isDisplayedCorrectly(displayedIndex, task); + } + return outcome; + } + + /** + * Clicks on the ListView. + */ + public void clickOnListView() { + Point2D point= TestUtil.getScreenMidPoint(getTodoListView()); + guiRobot.clickOn(point.getX(), point.getY()); + } + + /** + * Returns true if the {@code tasks} appear as the sub list (in that order) at position {@code startPosition}. + */ + public boolean containsInOrder(int startPosition, ImmutableTask... tasks) { + List taskList = getImmutableTaskList(); + + // Return false if the list in panel is too short to contain the given list + if (startPosition + tasks.length > taskList.size()){ + return false; + } + + // Return false if any of the task doesn't match + for (int i = 0; i < tasks.length; i++) { + ImmutableTask taskInView = taskList.get(i); + ImmutableTask taskInList = tasks[i]; + if (!taskInView.equals(taskInList)) { + return false; + } + } + + return true; + } + + /** + * Navigates the {@link TodoListView} to display and select the task. + * @param listIndex Index of the list that the list view should navigate to. + * @return Handle of the selected task. + */ + public TaskCardViewHandle navigateToTask(int listIndex) { + guiRobot.interact(() -> { + getTodoListView().scrollTo(listIndex); + guiRobot.sleep(150); + getTodoListView().getSelectionModel().select(listIndex); + }); + guiRobot.sleep(100); + return getTaskCardViewHandle(listIndex); + } +} +``` +###### \java\seedu\todo\commons\util\StringUtilTest.java +``` java + @Test + public void partitionStringAtPosition_emptyString() { + String[] expected = {"", "", ""}; + + //Tests null string + testPartitionStringAtPositionHelper(null, 0, expected); + + //Test empty String + testPartitionStringAtPositionHelper("", 0, expected); + } + + @Test + public void partitionStringAtPosition_positionOutOfBounds() { + String input = "I have a Pikachu"; + String[] expected = {"", "", ""}; + + //Tests position too low + testPartitionStringAtPositionHelper(input, -1, expected); + + //Tests position too high + testPartitionStringAtPositionHelper(input, 16, expected); + } + + @Test + public void partitionStringAtPosition_partitionCorrectly() { + String input = "I have a Pikachu"; + + //Test lower bound + testPartitionStringAtPositionHelper(input, 0, new String[] {"", "I", " have a Pikachu"}); + + //Test upper bound + testPartitionStringAtPositionHelper(input, 15, new String[] {"I have a Pikach", "u", ""}); + + //Test normal partition + testPartitionStringAtPositionHelper(input, 5, new String[] {"I hav", "e", " a Pikachu"}); + } + + /** + * Helper method to test partitionStringAtPosition(...). + * @param input String to be partitioned. + * @param position Position where partition should take place. + * @param expected Expected output as String array. + */ + private void testPartitionStringAtPositionHelper(String input, int position, String[] expected) { + String[] outcome = StringUtil.partitionStringAtPosition(input, position); + assertArrayEquals(expected, outcome); + } + + @Test + public void splitString_emptyInput() { + String[] expected = new String[0]; + + //Test null input. + testSplitStringHelper(null, expected); + + //Test empty input. + testSplitStringHelper("", expected); + + //Test only space and commas. + testSplitStringHelper(" , , ,,, ,,,, , , ,,, , ,", expected); + } + + @Test + public void splitString_validInput() { + //Input does not include space and comma + testSplitStringHelper("!@(*&$!R#@%", new String[]{"!@(*&$!R#@%"}); + + //Test one element + testSplitStringHelper("Pichu-Pikachu_RAICHU's", new String[] {"Pichu-Pikachu_RAICHU's"}); + + //Test multiple element split by space and comma + testSplitStringHelper("an apple a, day, keeps , , doctor ,,, away", new String[] {"an", "apple", "a", "day", "keeps", "doctor", "away"}); + } + + /** + * Helper method to test splitString(...). + * @param input String to be split. + * @param expected Expected output as String array. + */ + private void testSplitStringHelper(String input, String[] expected) { + String[] outcome = StringUtil.splitString(input); + assertArrayEquals(expected, outcome); + } + + @Test + public void testConvertListToString_emptyList() { + String expected = ""; + + //Test null list + testConvertListToStringHelper(null, expected); + + //Test empty list + testConvertListToStringHelper(new String[0], expected); + } + + @Test + public void testConvertListToString_validInput() { + //Test one element + testConvertListToStringHelper(new String[]{"applepie123!"}, "applepie123!"); + + //Test several elements + testConvertListToStringHelper(new String[]{"this", "is", "apple", "pen"}, "this, is, apple, pen"); + } + + /** + * Helper method to test splitString(...). + * @param input String to be split. + * @param expected Expected output as String array. + */ + private void testConvertListToStringHelper(String[] input, String expected) { + String outcome = StringUtil.convertListToString(input); + assertEquals(expected, outcome); + } +``` +###### \java\seedu\todo\commons\util\TimeUtilTest.java +``` java + /** + * A subclass of TimeUtil that provides the ability to override the current system time, + * so that time sensitive components can be conveniently tested. + */ + private class ModifiedTimeUtil extends TimeUtil { + + /** + * Construct a ModifiedTimeUtil object overriding the current time with Clock object. + * Is only used for dependency injection in testing time sensitive components. + */ + private ModifiedTimeUtil(Clock clock) { + this.clock = clock; + } + + /** + * Construct a ModifiedTimeUtil object overriding the current time with LocalDateTime object. + * Is only used for dependency injection in testing time sensitive components. + */ + public ModifiedTimeUtil(LocalDateTime pseudoCurrentTime) { + this(Clock.fixed(pseudoCurrentTime.toInstant( + ZoneId.systemDefault().getRules().getOffset(pseudoCurrentTime)), ZoneId.systemDefault())); + } + } + + /** + * Aids to test taskDeadlineText with a current time and due time, against an expected output. + */ + private void testTaskDeadlineTextHelper(String expectedOutput, LocalDateTime currentTime, LocalDateTime dueTime) { + TimeUtil timeUtil = new ModifiedTimeUtil(currentTime); + String generatedOutput = timeUtil.getTaskDeadlineText(dueTime); + assertEquals(expectedOutput, generatedOutput); + } + + /** + * Aids to test eventTimeText with a current time, startTime and endTime, against an expected output. + */ + private void testEventTimeTextHelper(String expectedOutput, LocalDateTime currentTime, + LocalDateTime startTime, LocalDateTime endTime) { + TimeUtil timeUtil = new ModifiedTimeUtil(currentTime); + String generatedOutput = timeUtil.getEventTimeText(startTime, endTime); + assertEquals(expectedOutput, generatedOutput); + } + + @Test + public void getTaskDeadlineString_nullEndTime() { + testTaskDeadlineTextHelper("", LocalDateTime.now(), null); + } + + @Test + public void getTaskDeadlineText_dueNow() { + String expectedOutput = "due now"; + LocalDateTime dueTime = LocalDateTime.of(2016, Month.MARCH, 20, 12, 0, 0); + + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 12, 0, 0), dueTime); + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 12, 0, 59), dueTime); + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 12, 0, 30), dueTime); + } + + @Test + public void getTaskDeadlineText_dueLessThanAMinute() { + String expectedOutput = "in less than a minute"; + LocalDateTime dueTime = LocalDateTime.of(2016, Month.MARCH, 20, 12, 0, 0); + + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 11, 59, 1), dueTime); + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 11, 59, 59), dueTime); + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 11, 59, 30), dueTime); + } + + @Test + public void getTaskDeadlineText_aMinuteBeforeDeadline() { + String expectedOutput = "in 1 minute"; + LocalDateTime dueTime = LocalDateTime.of(2016, Month.MARCH, 20, 12, 0, 0); + + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 11, 59, 0), dueTime); + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 11, 58, 30), dueTime); + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 11, 58, 1), dueTime); + } + + @Test + public void getTaskDeadlineText_aMinuteAfterDeadline() { + String expectedOutput = "1 minute ago"; + LocalDateTime dueTime = LocalDateTime.of(2016, Month.MARCH, 20, 12, 0, 0); + + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 12, 1, 0), dueTime); + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 12, 1, 30), dueTime); + testTaskDeadlineTextHelper(expectedOutput, LocalDateTime.of(2016, Month.MARCH, 20, 12, 1, 59), dueTime); + } + + @Test + public void getTaskDeadlineText_minutesBeforeDeadline() { + LocalDateTime dueTime = LocalDateTime.of(2016, Month.MARCH, 20, 12, 0, 0); + + for (int minutesLeft = 2; minutesLeft <= 59; minutesLeft++) { + String expectedOutput = "in " + minutesLeft + " minutes"; + + testTaskDeadlineTextHelper(expectedOutput, dueTime.minusMinutes(minutesLeft), dueTime); + testTaskDeadlineTextHelper(expectedOutput, dueTime.minusMinutes(minutesLeft).minusSeconds(30), dueTime); + testTaskDeadlineTextHelper(expectedOutput, dueTime.minusMinutes(minutesLeft).minusSeconds(59), dueTime); + } + } + + @Test + public void getTaskDeadlineText_minutesAfterDeadline() { + LocalDateTime dueTime = LocalDateTime.of(2016, Month.MARCH, 20, 12, 0, 0); + + for (int minutesLater = 2; minutesLater <= 59; minutesLater++) { + String expectedOutput = minutesLater + " minutes ago"; + + testTaskDeadlineTextHelper(expectedOutput, dueTime.plusMinutes(minutesLater), dueTime); + testTaskDeadlineTextHelper(expectedOutput, dueTime.plusMinutes(minutesLater).plusSeconds(30), dueTime); + testTaskDeadlineTextHelper(expectedOutput, dueTime.plusMinutes(minutesLater).plusSeconds(59), dueTime); + } + } + + @Test + public void getTaskDeadlineText_todayBeforeDeadline() { + testTaskDeadlineTextHelper("by today, 5:59 PM", + LocalDateTime.of(2016, Month.MARCH, 20, 10, 45), LocalDateTime.of(2016, Month.MARCH, 20, 17, 59)); + testTaskDeadlineTextHelper("by tonight, 6:00 PM", + LocalDateTime.of(2016, Month.MARCH, 20, 11, 58), LocalDateTime.of(2016, Month.MARCH, 20, 18, 0)); + testTaskDeadlineTextHelper("in 30 minutes", + LocalDateTime.of(2016, Month.MARCH, 20, 0, 0), LocalDateTime.of(2016, Month.MARCH, 20, 0, 30)); + } + + @Test + public void getTaskDeadlineText_todayAfterDeadline() { + testTaskDeadlineTextHelper("since today, 12:00 PM", + LocalDateTime.of(2016, Month.MARCH, 20, 18, 45), LocalDateTime.of(2016, Month.MARCH, 20, 12, 0)); + testTaskDeadlineTextHelper("since tonight, 6:50 PM", + LocalDateTime.of(2016, Month.MARCH, 20, 23, 58), LocalDateTime.of(2016, Month.MARCH, 20, 18, 50)); + testTaskDeadlineTextHelper("30 minutes ago", + LocalDateTime.of(2016, Month.MARCH, 20, 0, 30), LocalDateTime.of(2016, Month.MARCH, 20, 0, 0)); + } + + @Test + public void getTaskDeadlineText_tomorrowBeforeDeadline() { + testTaskDeadlineTextHelper("by tomorrow, 12:00 PM", + LocalDateTime.of(2016, Month.MARCH, 20, 12, 0), LocalDateTime.of(2016, Month.MARCH, 21, 12, 0)); + testTaskDeadlineTextHelper("by tomorrow, 12:51 AM", + LocalDateTime.of(2016, Month.MARCH, 20, 23, 50), LocalDateTime.of(2016, Month.MARCH, 21, 0, 51)); + testTaskDeadlineTextHelper("in 20 minutes", + LocalDateTime.of(2016, Month.MARCH, 20, 23, 50), LocalDateTime.of(2016, Month.MARCH, 21, 0, 10)); + } + + @Test + public void getTaskDeadlineText_yesterdayAfterDeadline() { + testTaskDeadlineTextHelper("since yesterday, 12:00 PM", + LocalDateTime.of(2016, Month.MARCH, 21, 12, 0), LocalDateTime.of(2016, Month.MARCH, 20, 12, 0)); + testTaskDeadlineTextHelper("since yesterday, 12:51 AM", + LocalDateTime.of(2016, Month.MARCH, 21, 23, 50), LocalDateTime.of(2016, Month.MARCH, 20, 0, 51)); + testTaskDeadlineTextHelper("20 minutes ago", + LocalDateTime.of(2016, Month.MARCH, 21, 0, 10), LocalDateTime.of(2016, Month.MARCH, 20, 23, 50)); + } + + @Test + public void getTaskDeadlineText_thisYearBeforeDeadline() { + testTaskDeadlineTextHelper("by 12 August, 12:55 PM", + LocalDateTime.of(2016, Month.JANUARY, 21, 12, 0), LocalDateTime.of(2016, Month.AUGUST, 12, 12, 55)); + testTaskDeadlineTextHelper("by 15 September, 12:00 AM", + LocalDateTime.of(2016, Month.SEPTEMBER, 13, 23, 59), LocalDateTime.of(2016, Month.SEPTEMBER, 15, 0, 0)); + } + + @Test + public void getTaskDeadlineText_thisYearAfterDeadline() { + testTaskDeadlineTextHelper("since 21 January, 8:47 PM", + LocalDateTime.of(2016, Month.AUGUST, 12, 12, 55), LocalDateTime.of(2016, Month.JANUARY, 21, 20, 47)); + testTaskDeadlineTextHelper("since 13 September, 11:59 PM", + LocalDateTime.of(2016, Month.SEPTEMBER, 15, 0, 0), LocalDateTime.of(2016, Month.SEPTEMBER, 13, 23, 59)); + } + + @Test + public void getTaskDeadlineText_differentYearBeforeDeadline() { + testTaskDeadlineTextHelper("in 1 minute", + LocalDateTime.of(2016, Month.DECEMBER, 31, 23, 59), LocalDateTime.of(2017, Month.JANUARY, 1, 0, 0)); + testTaskDeadlineTextHelper("by tomorrow, 1:15 AM", + LocalDateTime.of(2016, Month.DECEMBER, 31, 23, 0), LocalDateTime.of(2017, Month.JANUARY, 1, 1, 15)); + testTaskDeadlineTextHelper("by 31 January 2017, 1:05 AM", + LocalDateTime.of(2016, Month.JUNE, 30, 22, 0), LocalDateTime.of(2017, Month.JANUARY, 31, 1, 5)); + testTaskDeadlineTextHelper("by 31 August 2020, 12:35 PM", + LocalDateTime.of(2016, Month.FEBRUARY, 13, 13, 0), LocalDateTime.of(2020, Month.AUGUST, 31, 12, 35)); + } + + @Test + public void getTaskDeadlineText_differentYearAfterDeadline() { + testTaskDeadlineTextHelper("1 minute ago", + LocalDateTime.of(2017, Month.JANUARY, 1, 0, 0), LocalDateTime.of(2016, Month.DECEMBER, 31, 23, 59)); + testTaskDeadlineTextHelper("since yesterday, 12:00 AM", + LocalDateTime.of(2017, Month.JANUARY, 1, 0, 0), LocalDateTime.of(2016, Month.DECEMBER, 31, 0, 0)); + testTaskDeadlineTextHelper("since 30 June 2016, 10:00 PM", + LocalDateTime.of(2017, Month.JANUARY, 31, 1, 5), LocalDateTime.of(2016, Month.JUNE, 30, 22, 0)); + testTaskDeadlineTextHelper("since 13 February 2016, 1:00 PM", + LocalDateTime.of(2020, Month.AUGUST, 31, 12, 35), LocalDateTime.of(2016, Month.FEBRUARY, 13, 13, 0)); + } + + @Test + public void getEventTimeText_nullStartTime() { + testEventTimeTextHelper("", LocalDateTime.now(), null, LocalDateTime.now().plusMinutes(1)); + } + + @Test + public void getEventTimeText_nullEndTime() { + testEventTimeTextHelper("", LocalDateTime.now(), LocalDateTime.now().plusMinutes(1), null); + } + + @Test + public void getEventTimeText_sameDay() { + LocalDateTime currentTime = LocalDateTime.of(2016, 10, 20, 12, 00); + testEventTimeTextHelper("yesterday, from 4:50 PM to 8:30 PM", currentTime, + LocalDateTime.of(2016, 10, 19, 16, 50), LocalDateTime.of(2016, 10, 19, 20, 30)); + testEventTimeTextHelper("today, from 4:50 PM to 8:30 PM", currentTime, + LocalDateTime.of(2016, 10, 20, 16, 50), LocalDateTime.of(2016, 10, 20, 20, 30)); + testEventTimeTextHelper("tonight, from 6:00 PM to 8:30 PM", currentTime, + LocalDateTime.of(2016, 10, 20, 18, 00), LocalDateTime.of(2016, 10, 20, 20, 30)); + testEventTimeTextHelper("tomorrow, from 4:50 PM to 8:30 PM", currentTime, + LocalDateTime.of(2016, 10, 21, 16, 50), LocalDateTime.of(2016, 10, 21, 20, 30)); + testEventTimeTextHelper("21 November, from 4:50 PM to 8:30 PM", currentTime, + LocalDateTime.of(2016, 11, 21, 16, 50), LocalDateTime.of(2016, 11, 21, 20, 30)); + testEventTimeTextHelper("21 November 2017, from 4:50 PM to 8:30 PM", currentTime, + LocalDateTime.of(2017, 11, 21, 16, 50), LocalDateTime.of(2017, 11, 21, 20, 30)); + } + + @Test + public void getEventTimeText_differentDay() { + LocalDateTime currentTime = LocalDateTime.of(2016, 10, 20, 12, 00); + testEventTimeTextHelper("from yesterday, 4:50 PM to today, 2:30 PM", currentTime, + LocalDateTime.of(2016, 10, 19, 16, 50), LocalDateTime.of(2016, 10, 20, 14, 30)); + testEventTimeTextHelper("from yesterday, 4:50 PM to tonight, 8:30 PM", currentTime, + LocalDateTime.of(2016, 10, 19, 16, 50), LocalDateTime.of(2016, 10, 20, 20, 30)); + testEventTimeTextHelper("from today, 4:50 PM to tomorrow, 8:30 PM", currentTime, + LocalDateTime.of(2016, 10, 20, 16, 50), LocalDateTime.of(2016, 10, 21, 20, 30)); + testEventTimeTextHelper("from tonight, 6:50 PM to tomorrow, 8:30 PM", currentTime, + LocalDateTime.of(2016, 10, 20, 18, 50), LocalDateTime.of(2016, 10, 21, 20, 30)); + testEventTimeTextHelper("from tomorrow, 6:50 PM to 22 October, 8:30 PM", currentTime, + LocalDateTime.of(2016, 10, 21, 18, 50), LocalDateTime.of(2016, 10, 22, 20, 30)); + testEventTimeTextHelper("from 18 October, 6:50 PM to 22 October, 8:30 PM", currentTime, + LocalDateTime.of(2016, 10, 18, 18, 50), LocalDateTime.of(2016, 10, 22, 20, 30)); + } + + @Test + public void getEventTimeText_differentYear() { + LocalDateTime currentTime = LocalDateTime.of(2016, 12, 31, 12, 00); + testEventTimeTextHelper("from 19 October, 4:50 PM to 3 January 2017, 2:30 PM", currentTime, + LocalDateTime.of(2016, 10, 19, 16, 50), LocalDateTime.of(2017, 1, 3, 14, 30)); + testEventTimeTextHelper("from 19 October, 4:50 PM to tomorrow, 2:30 PM", currentTime, + LocalDateTime.of(2016, 10, 19, 16, 50), LocalDateTime.of(2017, 1, 1, 14, 30)); + testEventTimeTextHelper("from today, 4:50 PM to 4 January 2017, 2:30 PM", currentTime, + LocalDateTime.of(2016, 12, 31, 16, 50), LocalDateTime.of(2017, 1, 4, 14, 30)); + } + + @Test + public void isOverdue_nullEndTime() { + TimeUtil timeUtil = new TimeUtil(); + assertFalse(timeUtil.isOverdue(null)); + } + + @Test + public void isOverdue_endTimeAfterNow() { + TimeUtil timeUtil = new ModifiedTimeUtil(LocalDateTime.of(2016, Month.DECEMBER, 12, 12, 34)); + LocalDateTime laterEndTime = LocalDateTime.of(2016, Month.DECEMBER, 12, 12, 35); + assertFalse(timeUtil.isOverdue(laterEndTime)); + } + + @Test + public void isOverdue_endTimeBeforeNow() { + TimeUtil timeUtil = new ModifiedTimeUtil(LocalDateTime.of(2016, Month.DECEMBER, 12, 12, 36)); + LocalDateTime laterEndTime = LocalDateTime.of(2016, Month.DECEMBER, 12, 12, 35); + assertTrue(timeUtil.isOverdue(laterEndTime)); + } + +``` +###### \java\seedu\todo\testutil\TaskFactory.java +``` java + /** + * Generates a list of random tasks with a random size between {@code lowerBound} + * and {@code upperBound} inclusive. + * + * @return Returns a randomly generated list. + */ + public static List list(int lowerBound, int upperBound) { + int size = lowerBound + random.nextInt(upperBound - lowerBound + 1); + ArrayList tasks = new ArrayList<>(); + for (int i = 0; i < size; i++) { + tasks.add(random()); + } + return tasks; + } +} +``` +###### \java\seedu\todo\testutil\TimeUtil.java +``` java + /** + * Checks against system time if the provided dueTime is before system time. + * + * @param dueTime The due time to check against with the current system time. + * @return Returns true if the provided dueTime is before system time. + */ + public static boolean isOverdue(LocalDateTime dueTime) { + return dueTime.isBefore(LocalDateTime.now()); + } + + /** + * Gets the complete date time text in the following format: + * 12 August 2015, 12:34 PM + */ + public static String getDateTimeText(LocalDateTime dateTime) { + return dateTime.format(DateTimeFormatter.ofPattern(FORMAT_FULL_DATE)); + } +} +``` diff --git a/collated/test/A0135817B.md b/collated/test/A0135817B.md new file mode 100644 index 000000000000..acc3ea9cbb99 --- /dev/null +++ b/collated/test/A0135817B.md @@ -0,0 +1,991 @@ +# A0135817B +###### \java\seedu\todo\commons\core\TaskViewFilterTest.java +``` java +public class TaskViewFilterTest { + @Test + public void testNoOverlappingShortcut() { + Set shortcuts = new HashSet<>(); + + for (TaskViewFilter filter : TaskViewFilter.all()) { + char shortcut = filter.name.charAt(filter.shortcutCharPosition); + assertFalse(shortcuts.contains(shortcut)); + shortcuts.add(shortcut); + } + } +} +``` +###### \java\seedu\todo\logic\arguments\ArgumentTest.java +``` java +public class ArgumentTest { + private Argument arg = new TestArgument(); + + @Test + public void testRequiredErrorMessage() { + arg.required("Hello world"); + + try { + arg.checkRequired(); + } catch (IllegalValueException e) { + assertEquals("Hello world", e.getMessage()); + } + } + + @Test(expected=IllegalValueException.class) + public void testRequired() throws IllegalValueException { + arg.required(); + arg.checkRequired(); + } + + @Test + public void testIsOptional() { + assertTrue(arg.isOptional()); + arg.required(); + assertFalse(arg.isOptional()); + } + + @Test + public void testIsPositional() { + assertTrue(arg.isPositional()); + arg.flag("t"); + assertFalse(arg.isPositional()); + } + + @Test + public void testFlag() { + assertNull(arg.getFlag()); + arg.flag("h"); + assertEquals("h", arg.getFlag()); + arg.flag(" H "); + assertEquals("h", arg.getFlag()); + } + + @Test + public void testDescription() { + assertNull(arg.getDescription()); + arg.description("Hello World"); + assertEquals("Hello World", arg.getDescription()); + } + + @Test + public void testToString() { + assertEquals("[Test]", arg.toString()); + assertEquals("[Something]", arg.toString("Something")); + + arg.flag("t"); + assertEquals("[/t Test]", arg.toString()); + assertEquals("[/t Something]", arg.toString("Something")); + + arg.required(); + assertEquals("/t Test", arg.toString()); + assertEquals("/t Something", arg.toString("Something")); + } + + private class TestArgument extends Argument { + public TestArgument() { + super("Test"); + } + } +} +``` +###### \java\seedu\todo\logic\arguments\DateRangeArgumentTest.java +``` java +public class DateRangeArgumentTest { + private final Argument arg = new DateRangeArgument("Test"); + private final LocalDateTime tomorrow = LocalDateTime.of(LocalDate.now().plusDays(1), LocalTime.of(0, 0)); + + @Test + public void testDefaultValue() throws Exception { + assertNull(arg.getValue().getStartTime()); + assertNull(arg.getValue().getEndTime()); + assertFalse(arg.hasBoundValue()); + } + + @Test + public void testEmptyInput() throws Exception { + arg.setValue(""); + assertNull(arg.getValue().getStartTime()); + assertNull(arg.getValue().getEndTime()); + assertTrue(arg.hasBoundValue()); + } + + @Test + public void testInternationalDate() throws Exception { + arg.setValue("6/12/16"); + assertEquals(LocalDate.of(2016, 12, 6), arg.getValue().getEndTime().toLocalDate()); + assertFalse(arg.getValue().isRange()); + } + + @Test + public void testIsoDate() throws Exception { + arg.setValue("06-12-2016"); + assertEquals(LocalDate.of(2016, 12, 6), arg.getValue().getEndTime().toLocalDate()); + assertFalse(arg.getValue().isRange()); + } + + @Test + public void testInternationalDateTime() throws Exception { + arg.setValue("6/12/16 12:45pm"); + assertEquals(LocalDateTime.of(2016, 12, 6, 12, 45), arg.getValue().getEndTime()); + assertFalse(arg.getValue().isRange()); + } + + @Test + public void testNaturalLanguageDateTime() throws Exception { + arg.setValue("12 Oct 2014 6pm"); + assertEquals(LocalDateTime.of(2014, 10, 12, 18, 0), arg.getValue().getEndTime()); + assertFalse(arg.getValue().isRange()); + } + + @Test + public void testRelativeDate() throws Exception { + arg.setValue("tomorrow"); + assertEquals(tomorrow.toLocalDate(), arg.getValue().getEndTime().toLocalDate()); + assertFalse(arg.getValue().isRange()); + } + + @Test + public void testRelativeDateTime() throws Exception { + arg.setValue("tomorrow 6pm"); + assertEquals(tomorrow.withHour(18), arg.getValue().getEndTime()); + assertFalse(arg.getValue().isRange()); + } + + @Test + public void testRelativeDateRange() throws Exception { + arg.setValue("tomorrow 6 to 8pm"); + assertEquals(tomorrow.withHour(20), arg.getValue().getEndTime()); + assertEquals(tomorrow.withHour(18), arg.getValue().getStartTime()); + } + + @Test + public void testFormalDateTimeRange() throws Exception { + arg.setValue("18-12-16 1800hrs to 2000hrs"); + LocalDateTime date = LocalDateTime.of(2016, 12, 18, 0, 0); + assertEquals(date.withHour(20), arg.getValue().getEndTime()); + assertEquals(date.withHour(18), arg.getValue().getStartTime()); + + arg.setValue("18-12-16 1800hrs to 19-12-16 2000hrs"); + assertEquals(LocalDateTime.of(2016, 12, 18, 18, 0), arg.getValue().getStartTime()); + assertEquals(LocalDateTime.of(2016, 12, 19, 20, 0), arg.getValue().getEndTime()); + } + + @Test(expected=IllegalValueException.class) + public void testNoDate() throws Exception { + arg.setValue("no date here"); + } + + @Test(expected=IllegalValueException.class) + public void testTooManyDates() throws Exception { + arg.setValue("yesterday, today, tomorrow"); + } + +} +``` +###### \java\seedu\todo\logic\arguments\DateRangeTest.java +``` java +public class DateRangeTest { + + @Test + public void testDateRangeLocalDateTime() { + LocalDateTime start = LocalDateTime.now(); + DateRange range = new DateRange(start); + assertNull(range.getStartTime()); + assertFalse(range.isRange()); + } + + @Test + public void testDateRangeLocalDateTimeLocalDateTime() { + LocalDateTime start = LocalDateTime.now(); + LocalDateTime end = start.plusHours(2); + DateRange range = new DateRange(start, end); + + assertTrue(range.isRange()); + } + +} +``` +###### \java\seedu\todo\logic\arguments\FlagArgumentTest.java +``` java +public class FlagArgumentTest { + + private Argument argument; + + @Before + public void setUp() throws Exception { + argument = new FlagArgument("test"); + } + + @Test + public void testDefaultValue() { + assertEquals("t", argument.getFlag()); + assertFalse(argument.getValue()); + + argument = new FlagArgument("Pin", true); + assertTrue(argument.getValue()); + assertEquals("p", argument.getFlag()); + } + + @Test + public void testSetEmptyValue() throws IllegalValueException { + argument.setValue(""); + assertTrue(argument.getValue()); + } + + @Test + public void testSetStringValue() throws IllegalValueException { + argument.setValue("Hello World"); + assertTrue(argument.getValue()); + } + + @Test + public void testToString() { + argument.flag("t"); + assertEquals("[/t]", argument.toString()); + + argument.required(); + assertEquals("/t", argument.toString()); + } + +} +``` +###### \java\seedu\todo\logic\arguments\IntArgumentTest.java +``` java +public class IntArgumentTest { + private Argument argument; + + private int setInput(String input) throws IllegalValueException { + argument.setValue(input); + return argument.getValue(); + } + + @Before + public void setUp() { + argument = new IntArgument("test"); + } + + @Test + public void testParse() throws IllegalValueException { + assertEquals(123, setInput("123")); + assertEquals(-345, setInput("-345")); + } + + @Test(expected=IllegalValueException.class) + public void testFloatArgument() throws IllegalValueException { + setInput("12.34"); + } + + @Test(expected=IllegalValueException.class) + public void testStringArgument() throws IllegalValueException { + setInput("random stuff"); + } +} +``` +###### \java\seedu\todo\logic\arguments\StringArgumentTest.java +``` java +public class StringArgumentTest { + + private Argument arg = new StringArgument("test"); + + @Test + public void testSetValue() throws IllegalValueException { + arg.setValue("Hello world"); + assertTrue(arg.hasBoundValue()); + assertEquals("Hello world", arg.getValue()); + } + + @Test + public void testTrimValue() throws IllegalValueException { + arg.setValue(" Hello world "); + assertTrue(arg.hasBoundValue()); + assertEquals("Hello world", arg.getValue()); + } + + @Test + public void testEmptyValue() throws IllegalValueException { + arg.setValue(" "); + assertTrue(arg.hasBoundValue()); + assertEquals(arg.getValue(), null); + } +} +``` +###### \java\seedu\todo\logic\commands\BaseCommandTest.java +``` java +public class BaseCommandTest extends CommandTest { + private Argument requiredArgument = mock(StringArgument.class); + private Argument flagArgument = mock(FlagArgument.class); + private Argument intArgument = mock(IntArgument.class); + private Argument stringArgument = mock(StringArgument.class); + + private StubCommand stubCommand; + + @Override + protected BaseCommand commandUnderTest() { + stubCommand = new StubCommand(); + return stubCommand; + } + + @Before + public void setUp() throws Exception { + when(requiredArgument.isPositional()).thenReturn(true); + when(requiredArgument.toString()).thenReturn("required"); + + when(flagArgument.isOptional()).thenReturn(true); + when(flagArgument.getFlag()).thenReturn("f"); + when(flagArgument.toString()).thenReturn("flag"); + + when(intArgument.isOptional()).thenReturn(true); + when(intArgument.getFlag()).thenReturn("i"); + when(intArgument.toString()).thenReturn("int"); + + when(stringArgument.isOptional()).thenReturn(true); + when(stringArgument.getFlag()).thenReturn("s"); + when(stringArgument.toString()).thenReturn("string"); + } + + @Test + public void testSetParameter() throws Exception { + this.setParameter("required") + .setParameter("f", "") + .setParameter("i", "20") + .setParameter("s", "Hello World"); + + execute(true); + + verify(requiredArgument).setValue("required"); + verify(flagArgument).setValue(""); + verify(intArgument).setValue("20"); + verify(stringArgument).setValue("Hello World"); + } + + @Test + public void testCustomArgumentError() throws Exception { + command = new CommandWithOverrideMethods(); + + try { + execute(false); + fail(); + } catch (ValidationException e) { + assertEquals("Test error message", e.getMessage()); + assertTrue(e.getErrors().getNonFieldErrors().contains("Test error")); + } + } + + @Test + public void getArgumentSummary() { + assertEquals("required flag int string", stubCommand.getArgumentSummaryResult()); + } + + @Test(expected=ValidationException.class) + public void testMissingRequiredArgument() throws Exception { + IllegalValueException e = mock(IllegalValueException.class); + doThrow(e).when(requiredArgument).checkRequired(); + + execute(false); + } + + private class StubCommand extends BaseCommand { + @Override + protected Parameter[] getArguments() { + return new Parameter[]{ requiredArgument, flagArgument, intArgument, stringArgument }; + } + + @Override + public String getCommandName() { + return "stub"; + } + + @Override + public List getCommandSummary() { + return ImmutableList.of(mock(CommandSummary.class)); + } + + @Override + public CommandResult execute() throws ValidationException { + // Does nothing + return new CommandResult("Great Success!"); + } + + public String getArgumentSummaryResult() { + return getArgumentSummary(); + } + } + + private class CommandWithOverrideMethods extends StubCommand { + @Override + protected String getArgumentErrorMessage() { + return "Test error message"; + } + + @Override + protected void validateArguments() { + errors.put("Test error"); + } + } +} +``` +###### \java\seedu\todo\logic\commands\CommandSummaryTest.java +``` java +public class CommandSummaryTest { + @Test + public void testConstructor() { + CommandSummary summary = new CommandSummary(" Hello ", "World"); + // Check trim + assertEquals("Hello", summary.scenario); + // Check command is lowercase + assertEquals("world", summary.command); + // Check constructor without third argument + assertEquals("", summary.arguments); + } +} +``` +###### \java\seedu\todo\logic\commands\CommandTest.java +``` java +/** + * Base test case for testing commands. All command tests should extend this class. + * Provides a simple interface for setting up command testing as well as a number + * of assertions to inspect the model. + */ +public abstract class CommandTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + protected Model model; + protected TodoList todolist; + @Mock protected MovableStorage storage; + @Mock protected ImmutableTodoList storageData; + protected BaseCommand command; + protected StubParseResult params; + protected CommandResult result; + + abstract protected BaseCommand commandUnderTest(); + + @Before + public void setUpCommand() throws Exception { + when(storage.read()).thenReturn(storageData); + when(storageData.getTasks()).thenReturn(Collections.emptyList()); + + todolist = new TodoList(storage); + model = new TodoModel(todolist, storage); + params = new StubParseResult(); + command = commandUnderTest(); + } + + /** + * Returns the task visible in the model at 1-indexed position, mimicking user input + */ + protected ImmutableTask getTaskAt(int index) { + return model.getObservableList().get(index - 1); + } + + /** + * Asserts that the model has this number of tasks stored in internal storage (visible and not visible) + */ + + protected void assertTotalTaskCount(int size) { + assertEquals(size, todolist.getTasks().size()); + } + + /** + * Asserts that the model has this number of tasks visible + */ + protected void assertVisibleTaskCount(int size) { + assertEquals(size, model.getObservableList().size()); + } + + /** + * Asserts that the task exists in memory + */ + protected void assertTaskExist(ImmutableTask task) { + if (!todolist.getTasks().contains(task)) { + throw new AssertionError("Task not found in model"); + } + } + + + /** + * Asserts that the task does not exist in memory + */ + protected void assertTaskNotExist(ImmutableTask task) { + if (todolist.getTasks().contains(task)) { + throw new AssertionError("Task found in model"); + } + } + + /** + * Asserts that the task is visible to the user through the model + */ + protected void assertTaskVisible(ImmutableTask task) { + if (!model.getObservableList().contains(task)) { + throw new AssertionError("Task is not visible"); + } + } + + /** + * Asserts that the task is visible to the user through the model. + * This can also mean the task is simply not in memory. Use {@link #assertTaskHidden} + * to assert that the task exists, but is not visible + */ + protected void assertTaskNotVisible(ImmutableTask task) { + if (model.getObservableList().contains(task)) { + throw new AssertionError("Task is visible"); + } + } + + /** + * Asserts that the task exists, but is not visible to the user through + * the model + */ + protected void assertTaskHidden(ImmutableTask task) { + assertTaskExist(task); + assertTaskNotVisible(task); + } + + /** + * Sets the positional parameter for command execution. Can be chained. + */ + protected CommandTest setParameter(String positional) { + params.positional = positional; + return this; + } + + /** + * Sets the named argument for command execution. Can be chained. + */ + protected CommandTest setParameter(String flag, String value) { + params.named.put(flag, value); + return this; + } + + @Test + public void testCommonProperties() { + assertNotNull(command.getArguments()); + assertThat(command.getCommandName(), not(containsString(" "))); + assertThat(command.getCommandSummary().size(), greaterThan(0)); + } + + /** + * Executes the command + */ + protected void execute(boolean expectSuccess) throws ValidationException { + command.setArguments(params); + command.setModel(model); + result = command.execute(); + + assertEquals(expectSuccess, result.isSuccessful()); + + // Resets the command object for re-execution + command = commandUnderTest(); + params = new StubParseResult(); + } + + private class StubParseResult implements ParseResult { + public String command; + public String positional; + public Map named = new HashMap<>(); + + @Override + public String getCommand() { + return command; + } + + @Override + public Optional getPositionalArgument() { + return Optional.ofNullable(positional); + } + + @Override + public Map getNamedArguments() { + return named; + } + } +} +``` +###### \java\seedu\todo\logic\commands\ExitCommandTest.java +``` java +public class ExitCommandTest extends CommandTest { + @Override + protected BaseCommand commandUnderTest() { + return new ExitCommand(); + } + + @Test + public void testExecute() throws IllegalValueException, ValidationException { + EventsCollector eventCollector = new EventsCollector(); + execute(true); + assertThat(eventCollector.get(0), instanceOf(ExitAppRequestEvent.class)); + } + +} +``` +###### \java\seedu\todo\logic\commands\FindCommandTest.java +``` java + @Test + public void testFindWithFilter() throws ValidationException { + TaskViewFilter filter = new TaskViewFilter("test", t -> t.getTitle().contains("CS2101"), null); + model.view(filter); + + setParameter("Task"); + execute(true); + assertVisibleTaskCount(1); + } + + @Test + public void testDismissFind() throws ValidationException { + setParameter("project"); + execute(true); + assertVisibleTaskCount(2); + assertNotNull(model.getSearchStatus().getValue()); + + setParameter(""); + execute(true); + assertVisibleTaskCount(4); + assertNull(model.getSearchStatus().getValue()); + } +} +``` +###### \java\seedu\todo\logic\commands\HelpCommandTest.java +``` java +public class HelpCommandTest extends CommandTest { + @Override + protected BaseCommand commandUnderTest() { + return new HelpCommand(); + } + + @Test + public void testExecute() throws Exception { + EventsCollector eventsCollector = new EventsCollector(); + execute(true); + assertThat(eventsCollector.get(0), instanceOf(ShowHelpEvent.class)); + } + +} +``` +###### \java\seedu\todo\logic\commands\LoadCommandTest.java +``` java +/** + * This is an integration test for the {@code load} command. For tests on the + * load functionality itself, see {@link seedu.todo.storage.TodoListStorageTest} + */ +public class LoadCommandTest extends CommandTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock private ImmutableTodoList tasks; + + @Override + protected BaseCommand commandUnderTest() { + return new LoadCommand(); + } + + @Test + public void testSaveLocation() throws Exception { + setParameter("new file"); + when(storage.read("new file")).thenReturn(tasks); + when(tasks.getTasks()).thenReturn(ImmutableList.of(new Task("Hello world"))); + + execute(true); + assertEquals("Hello world", getTaskAt(1).getTitle()); + } + + @Test(expected = ValidationException.class) + public void testHandleFileError() throws Exception { + setParameter("new file"); + doThrow(new FileNotFoundException()).when(storage).read("new file"); + execute(false); + } + +} +``` +###### \java\seedu\todo\logic\commands\RedoCommandTest.java +``` java +public class RedoCommandTest extends CommandTest { + @Override + protected BaseCommand commandUnderTest() { + return new RedoCommand(); + } + + /** + * This is an integration test for the redo command. For a more detailed test on the model itself + * {@link seedu.todo.model.TodoModelTest#testRedo} and other related tests + */ + @Test + public void testExecute() throws Exception { + model.add("Test task"); + model.undo(); + execute(true); + assertEquals("Test task", getTaskAt(1).getTitle()); + } + + @Test(expected = ValidationException.class) + public void testIncorrectExecute() throws Exception { + execute(false); + } +} +``` +###### \java\seedu\todo\logic\commands\SaveCommandTest.java +``` java +public class SaveCommandTest extends CommandTest { + @Test + public void testGetStorageLocation() throws Exception { + when(storage.getLocation()).thenReturn("test location"); + execute(true); + + assertThat(result.getFeedback(), containsString("test location")); + verify(storage, never()).save(eq(todolist), anyString()); + } + + @Test + public void testSaveLocation() throws Exception { + setParameter("new file"); + execute(true); + verify(storage).save(todolist, "new file"); + } + + @Test(expected = ValidationException.class) + public void testHandleFileError() throws Exception { + setParameter("new file"); + doThrow(new IOException()).when(storage).save(todolist, "new file"); + execute(false); + } + + @Override + protected BaseCommand commandUnderTest() { + return new SaveCommand(); + } +} +``` +###### \java\seedu\todo\logic\commands\UndoCommandTest.java +``` java +public class UndoCommandTest extends CommandTest { + + @Override + protected BaseCommand commandUnderTest() { + return new UndoCommand(); + } + + /** + * This is an integration test for the redo command. For a more detailed test on the model itself + * {@link TodoModelTest#testUndo} and other related tests + */ + @Test + public void testUndo() throws Exception { + model.add("Test task"); + execute(true); + } + + @Test(expected = ValidationException.class) + public void testEmptyUndo() throws Exception { + execute(false); + } +} +``` +###### \java\seedu\todo\logic\parser\TodoParserTest.java +``` java +public class TodoParserTest { + private Parser parser = new TodoParser(); + + @Test + public void testParse() { + ParseResult p; + + p = parser.parse("hello"); + assertEquals("hello", p.getCommand()); + + p = parser.parse("HeLLo"); + assertEquals("hello", p.getCommand()); + + p = parser.parse("HELLO"); + assertEquals("hello", p.getCommand()); + + p = parser.parse("hello world"); + assertEquals("hello", p.getCommand()); + + p = parser.parse(" hello "); + assertEquals("hello", p.getCommand()); + } + + @Test + public void testPositionalArgument() { + ParseResult p; + + p = parser.parse("hello world"); + assertEquals("world", p.getPositionalArgument().get()); + + p = parser.parse("hello one two three"); + assertEquals("one two three", p.getPositionalArgument().get()); + + p = parser.parse("hello one two three "); + assertEquals("one two three", p.getPositionalArgument().get()); + } + + @Test + public void testNamedArguments() { + ParseResult p; + + p = parser.parse("hello /f"); + assertEquals(1, p.getNamedArguments().size()); + assertTrue(p.getNamedArguments().containsKey("f")); + + p = parser.parse("hello /f Hello"); + assertEquals(1, p.getNamedArguments().size()); + assertEquals("Hello", p.getNamedArguments().get("f")); + + p = parser.parse("hello /f Hello "); + assertEquals(1, p.getNamedArguments().size()); + assertEquals("Hello", p.getNamedArguments().get("f")); + + p = parser.parse("hello /all Hello"); + assertEquals(1, p.getNamedArguments().size()); + assertEquals("Hello", p.getNamedArguments().get("all")); + + p = parser.parse("hello /f Hello /p /all"); + assertEquals(3, p.getNamedArguments().size()); + assertEquals("Hello", p.getNamedArguments().get("f")); + assertTrue(p.getNamedArguments().containsKey("p")); + assertTrue(p.getNamedArguments().containsKey("all")); + } + + @Test + public void testInvalidFlags() { + ParseResult p; + + p = parser.parse("hello /"); + assertTrue(p.getPositionalArgument().isPresent()); + assertEquals(0, p.getNamedArguments().size()); + } + +} +``` +###### \java\seedu\todo\logic\TodoDispatcherTest.java +``` java +public class TodoDispatcherTest { + private Dispatcher d = new TodoDispatcher(); + + @Test + public void testFullCommand() throws Exception { + assertThat(d.dispatch("add"), instanceOf(AddCommand.class)); + assertThat(d.dispatch("exit"), instanceOf(ExitCommand.class)); + } + + @Test + public void testPartialCommand() throws Exception { + assertThat(d.dispatch("ed"), instanceOf(EditCommand.class)); + assertThat(d.dispatch("a"), instanceOf(AddCommand.class)); + } + + @Test(expected = IllegalValueException.class) + public void testAmbiguousCommand() throws Exception { + d.dispatch("e"); + } + + @Test(expected = IllegalValueException.class) + public void testNonExistentCommand() throws Exception { + d.dispatch("applejack"); + } +} +``` +###### \java\seedu\todo\logic\TodoLogicTest.java +``` java +public class TodoLogicTest { + private static final String INPUT = "input"; + private static final String COMMAND = "command"; + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock private Parser parser; + @Mock private Dispatcher dispatcher; + @Mock private ParseResult parseResult; + @Mock private Model model; + @Mock private BaseCommand command; + + private Logic logic; + + @Before + public void setUp() throws Exception { + // Wire up some default behavior + when(parser.parse(TodoLogicTest.INPUT)) + .thenReturn(parseResult); + when(parseResult.getCommand()) + .thenReturn(TodoLogicTest.COMMAND); + when(dispatcher.dispatch(TodoLogicTest.COMMAND)) + .thenReturn(command); + + logic = new TodoLogic(parser, model, dispatcher); + } + + private CommandResult execute() throws Exception { + CommandResult r = logic.execute(TodoLogicTest.INPUT); + + verify(parser).parse(TodoLogicTest.INPUT); + verify(dispatcher).dispatch(TodoLogicTest.COMMAND); + + return r; + } + + @Test + public void testExecute() throws Exception { + execute(); + + verify(command).setModel(model); + verify(command).setArguments(parseResult); + verify(command).execute(); + + // Logic should not touch model directly + verifyZeroInteractions(model); + } + + @Test + public void testArgumentError() throws Exception { + // Create a stub exception for setArguments to throw + ValidationException e = mock(ValidationException.class); + ErrorBag errors = mock(ErrorBag.class); + + when(e.getErrors()).thenReturn(errors); + doThrow(e).when(command).setArguments(parseResult); + + CommandResult r = execute(); + + assertFalse(r.isSuccessful()); + // Make sure the command is never executed + verify(command, never()).execute(); + } + + @Test + public void testExecuteError() throws Exception { + // Create a stub exception for execute to throw + ValidationException e = mock(ValidationException.class); + ErrorBag errors = mock(ErrorBag.class); + + when(e.getErrors()).thenReturn(errors); + doThrow(e).when(command).execute(); + + CommandResult r = execute(); + + assertFalse(r.isSuccessful()); + assertEquals(errors, r.getErrors()); + } + + @Test + public void testDispatchError() throws Exception { + // Create a stub exception for execute to throw + IllegalValueException e = mock(IllegalValueException.class); + when(e.getMessage()).thenReturn("Test message"); + doThrow(e).when(dispatcher).dispatch(TodoLogicTest.COMMAND); + + CommandResult r = execute(); + + assertFalse(r.isSuccessful()); + assertEquals("Test message", r.getFeedback()); + } + + @Test + public void testEmptyInput() throws Exception { + CommandResult r = logic.execute(""); + + assertNotNull(r); + assertNotNull(r.getFeedback()); + verifyZeroInteractions(parser); + } +} +``` diff --git a/collated/test/A0139021U.md b/collated/test/A0139021U.md new file mode 100644 index 000000000000..9f585ce6554f --- /dev/null +++ b/collated/test/A0139021U.md @@ -0,0 +1,411 @@ +# A0139021U +###### \java\guitests\CommandPreviewViewTest.java +``` java + +/** + * Test the preview function through the GUI. + * Note: + * Order-ness of the tasks is not tested. + * Invalid preview output is not tested. + */ +public class CommandPreviewViewTest extends TodoListGuiTest { + + @Test + public void testPreviewEmptyString() { + int expected = 0; + int actual = commandPreviewView.getRowsDisplayed(); + + assertEquals(expected, actual); + } + + @Test + public void testPreviewAddCommand() throws InterruptedException { + //Add a task + ImmutableTask task = TaskFactory.task(); + enterCommand(CommandGeneratorUtil.generateAddCommand(task)); + + int expected = 2; + int actual = commandPreviewView.getRowsDisplayed(); + + assertEquals(expected, actual); + } +} +``` +###### \java\guitests\guihandles\CommandPreviewViewHandle.java +``` java +public class CommandPreviewViewHandle extends GuiHandle { + /* Constants */ + public static final String PREVIEW_VIEW_GRID_ID = "#previewGrid"; + + /** + * Constructs a handle to the {@link CommandPreviewViewHandle} + * + * @param guiRobot {@link GuiRobot} for the current GUI test. + * @param primaryStage The stage where the views for this handle is located. + */ + public CommandPreviewViewHandle(GuiRobot guiRobot, Stage primaryStage) { + super(guiRobot, primaryStage, TestApp.APP_TITLE); + } + + /** + * Get the preview {@link GridPane} object. + */ + private GridPane getPreviewGrid() { + return (GridPane) getNode(PREVIEW_VIEW_GRID_ID); + } + + /** + * Get the number of rows that is displayed on this {@link #getPreviewGrid()} object. + */ + public int getRowsDisplayed() { + return getPreviewGrid().getChildren().size() / 2; + } +} +``` +###### \java\seedu\todo\commons\util\StringUtilTest.java +``` java + @Test + public void calculateClosenessScoreNull() { + double expected = 0d; + double outcome = StringUtil.calculateClosenessScore(null, null); + assertEquals(expected, outcome, 0d); + } + + @Test + public void calculateClosenessScoreEmptyString() { + double expected = 0d; + double outcome = StringUtil.calculateClosenessScore("", ""); + assertEquals(expected, outcome, 0d); + } + + @Test + public void calculateClosenessScoreSameString() { + double expected = 100d; + double outcome = StringUtil.calculateClosenessScore("test", "test"); + assertEquals(expected, outcome, 0d); + } + + @Test + public void calculateClosenessScoreDifferentString() { + double expected = 0d; + double outcome = StringUtil.calculateClosenessScore("test", "ioio"); + assertEquals(expected, outcome, 0d); + } + + @Test + public void calculateClosenessScoreSomewhatCloseAdd() { + double expected = 50d; + double outcome = StringUtil.calculateClosenessScore("add", "a"); + assertEquals(expected, outcome, 20d); + } + + @Test + public void calculateClosenessScoreSomewhatCloseComplete() { + double expected = 50d; + double outcome = StringUtil.calculateClosenessScore("complete", "Com"); + assertEquals(expected, outcome, 20d); + } +} +``` +###### \java\seedu\todo\logic\commands\CommandPreviewTest.java +``` java +public class CommandPreviewTest { + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Before + public void setUpPreview() throws Exception { + Set mockCommands = Sets.newHashSet("add", "delete"); + } + + @Test + public void testFilterAdd() throws Exception { + // TODO: find way to mock static methods + List expected = CommandMap.getCommand("add").getCommandSummary(); + List actual = new CommandPreview("add").getPreview(); + assertTrue(isShallowCompareCommandSummaries(expected, actual)); + } + + @Test + public void testFilterEmptyString() throws Exception { + List expected = new ArrayList<>(); + List actual = new CommandPreview("").getPreview(); + assertEquals(expected, actual); + } + + private boolean isShallowCompareCommandSummaries(List list, List otherList) { + if (list.size() != otherList.size()) { + return false; + } + for (int i = 0; i < list.size(); i++) { + CommandSummary summary = list.get(i); + CommandSummary otherSummary = list.get(i); + + boolean isEqual = summary.arguments.equals(otherSummary.arguments) && + summary.command.equals(otherSummary.command) && + summary.scenario.equals(otherSummary.scenario); + + if (!isEqual) { + return false; + } + } + return true; + } +} +``` +###### \java\seedu\todo\model\task\ValidationTaskTest.java +``` java +public class ValidationTaskTest { + private ValidationTask task; + + @Before + public void setUp() throws Exception { + task = new ValidationTask("Test Task"); + } + + @Test + public void testTaskString() { + assertEquals("Test Task", task.getTitle()); + } + + @Test + public void testValidateTaskNoTime() throws ValidationException { + task.validate(); + } + + @Test(expected = ValidationException.class) + public void testValidateEmptyStringTitle() throws ValidationException { + task.setTitle(""); + task.validate(); + } + + @Test + public void testValidateTitle() throws ValidationException { + String testTitle = "test"; + task.setTitle(testTitle); + task.validate(); + assertEquals(task.getTitle(), testTitle); + } + + @Test + public void testValidateTaskTime() throws ValidationException { + LocalDateTime startTime = LocalDateTime.of(1, 1, 1, 1, 1); + LocalDateTime endTime = LocalDateTime.of(1, 1, 1, 1, 2); + + task.setStartTime(startTime); + task.setEndTime(endTime); + + task.validate(); + } + + @Test(expected = ValidationException.class) + public void testValidateTaskOnlyStartTime() throws ValidationException { + LocalDateTime startTime = LocalDateTime.of(1, 1, 1, 1, 1); + task.setStartTime(startTime); + task.validate(); + } + + @Test + public void testValidateTaskOnlyEndTime() throws ValidationException { + LocalDateTime endTime = LocalDateTime.of(1, 1, 1, 1, 1); + task.setEndTime(endTime); + task.validate(); + } + + @Test(expected = ValidationException.class) + public void testValidateTaskStartTimeBeforeEnd() throws ValidationException { + LocalDateTime startTime = LocalDateTime.of(1, 1, 1, 1, 2); + LocalDateTime endTime = LocalDateTime.of(1, 1, 1, 1, 1); + + task.setStartTime(startTime); + task.setEndTime(endTime); + + task.validate(); + } + + @Test + public void testConvertToTask() throws ValidationException { + LocalDateTime startTime = LocalDateTime.of(1, 1, 1, 1, 1); + LocalDateTime endTime = LocalDateTime.of(1, 1, 1, 1, 2); + + task.setStartTime(startTime); + task.setEndTime(endTime); + + assertAllPropertiesEqual(task, task.convertToTask()); + } + + @Test(expected = AssertionError.class) + public void testConvertDifferentTask() throws ValidationException { + Task convertedTask = task.convertToTask(); + task.setPinned(true); + // task.setDescription("test"); + assertAllPropertiesEqual(task, convertedTask); + } + + @Test + public void testTaskImmutableTask() { + ValidationTask original = new ValidationTask("Mock Task"); + assertAllPropertiesEqual(original, new ValidationTask(original)); + + original = new ValidationTask("Mock Task"); + original.setStartTime(LocalDateTime.now()); + original.setEndTime(LocalDateTime.now().plusHours(2)); + assertAllPropertiesEqual(original, new Task(original)); + + original = new ValidationTask("Mock Task"); + original.setDescription("A Test Description"); + original.setLocation("Test Location"); + assertAllPropertiesEqual(original, new ValidationTask(original)); + } + + @Test + public void testTitle() { + task.setTitle("New Title"); + assertEquals("New Title", task.getTitle()); + } + + @Test + public void testDescription() { + assertFalse(task.getDescription().isPresent()); + + task.setDescription("A short description"); + assertEquals("A short description", task.getDescription().get()); + } + + @Test + public void testLocation() { + assertFalse(task.getLocation().isPresent()); + + task.setLocation("Some Test Location"); + assertEquals("Some Test Location", task.getLocation().get()); + } + + @Test + public void testPinned() { + assertFalse(task.isPinned()); + + task.setPinned(true); + assertTrue(task.isPinned()); + } + + @Test + public void testCompleted() { + assertFalse(task.isCompleted()); + + task.setCompleted(true); + assertTrue(task.isCompleted()); + } + + @Test + public void testLastUpdated() { + assertNotNull(task.getCreatedAt()); + task.setCreatedAt(); + assertEquals(LocalDateTime.now(), task.getCreatedAt()); + } + + @Test + public void testTags() throws ValidationException { + assertEquals(0, task.getTags().size()); + + Set tags = new HashSet<>(); + tags.add(new Tag("Hello")); + tags.add(new Tag("World")); + task.setTags(tags); + + assertEquals(2, task.getTags().size()); + // TODO: This should do more when we finalize how tags can be edited + } + + @Test + public void testGetUUID() { + assertNotNull(task.getUUID()); + } +} +``` +###### \java\seedu\todo\testutil\TaskBuilder.java +``` java +/** + * Builds a task for testing purposes. + */ +public class TaskBuilder { + + private Task task; + private boolean defaultTime = true; + + private static LocalDateTime now = LocalDateTime.now(); + + private TaskBuilder(String name) { + task = new Task(name); + } + + public static TaskBuilder name(String name) { + return new TaskBuilder(name); + } + + public TaskBuilder description(String description) { + task.setDescription(description); + return this; + } + + public TaskBuilder location(String location) { + task.setLocation(location); + return this; + } + + public TaskBuilder createdAt(LocalDateTime lastUpdated) { + defaultTime = false; + task.setCreatedAt(lastUpdated); + return this; + } + + public TaskBuilder completed() { + task.setCompleted(true); + return this; + } + + public TaskBuilder pinned() { + task.setPinned(true); + return this; + } + + public TaskBuilder due() { + return due(TimeUtil.tomorrow().plusHours(12)); + } + + public TaskBuilder due(LocalDateTime due) { + task.setEndTime(due); + return this; + } + + public TaskBuilder event() { + return event(TimeUtil.tomorrow().plusHours(12), TimeUtil.tomorrow().plusHours(14)); + } + + public TaskBuilder event(LocalDateTime start, LocalDateTime end) { + task.setStartTime(start); + task.setEndTime(end); + return this; + } + + public TaskBuilder tagged(String ... tags) throws ValidationException { + Set setOfTags = new HashSet<>(); + for (String tag: tags) { + setOfTags.add(new Tag(tag)); + } + task.setTags(setOfTags); + return this; + } + + public Task build() { + // Push the time up by 1s to avoid colliding with previously created tasks + if (defaultTime) { + now = now.plusSeconds(1); + task.setCreatedAt(now); + } + + return task; + } + +} +``` diff --git a/docs/DeveloperGuide.html b/docs/DeveloperGuide.html index 903df5a56207..dd97053e4eb6 100644 --- a/docs/DeveloperGuide.html +++ b/docs/DeveloperGuide.html @@ -123,15 +123,14 @@

Developer Guide

-

1 Introduction#

Welcome to the project! This guide will get you up to speed with how to set up your development environment, -the basic architecture of the application, how to perform some common development tasks as well as -who to contact when you're lost.

+

1 Introduction#

Welcome to the Uncle Jim's Discount To-do App!

+

This guide will teach you how to set up your development environment, explain the basic architecture of the application, teach you how to perform some common development tasks, as well as provide contact information for the times when you require additional help.

1.1 Tooling#

This project uses

    -
  • git - Version control
  • -
  • Eclipse - IDE
  • +
  • Git - Version control
  • +
  • Eclipse - IDE
  • Gradle - Build automation
  • Travis, Coveralls and Codacy - Continuous integration and quality control
  • GitHub - Source code hosting and issue tracking
  • @@ -139,62 +138,69 @@

    Developer Guide

    2 Setting up#

    2.1 Prerequisites#

      -
    1. A git client. If you're on Linux you should already have one installed on your command line. For Windows -and OS X you can use SourceTree if you are more comfortable with using GUI
    2. -
    3. JDK 1.8.0_60 or later. Please use Oracle's because it comes with JavaFX, which is needed for -developing the application's UI.
    4. -
    5. Eclipse IDE
    6. -
    7. e(fx)clipse plugin for Eclipse (Do the steps 2 onwards given in - this page)
    8. -
    9. Buildship Gradle Integration plugin from the Eclipse Marketplace
    10. +
    11. Git client + If you are using Linux, you should already have one installed on your command line. If you are using Windows or OS X you can use SourceTree if you are more comfortable with using a GUI.
    12. +
    13. JDK 1.8.0_60 or later + Please use Oracle's jdk because it comes with JavaFX, which is needed for developing the application's UI.
    14. +
    15. Eclipse IDE
    16. +
    17. e(fx)clipse plugin for Eclipse + Perform steps 2 onwards as listed in this page to install the plugin.
    18. +
    19. Buildship Gradle Integration plugin from the Eclipse Marketplace + You can find Eclipse Marketplace from Eclipse's Help toolbar.
    -

    2.1.1 Importing the project into Eclipse#

      -
    1. Fork this repo, and clone the fork to your computer
    2. -
    3. Open Eclipse (Note: Ensure you have installed the e(fx)clipse and buildship plugins as given - in the prerequisites above)
    4. +

      2.1.1 Importing the project into Eclipse#

      +

      Note

      +

      Ensure that you have installed the e(fx)clipse and buildship plugins as listed in the prerequisites above.

      +
      + +
      1. Click File > Import
      2. Click Gradle > Gradle Project > Next > Next
      3. Click Browse, then locate the project's directory
      4. Click Finish
      5. -
    - +

    Note

    • 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 +
    • Depending on your connection speed and server load, this step may take up to 30 minutes 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.
    • +
    • If Eclipse has changed any settings files during the import process, you can discard those changes.
    -

    2.2 Contributing#

    We use the feature branch git workflow. When working on a task please remember to assign the relevant issue to yourself on the issue tracker and branch off from master. When the task is complete remember to push the branch to GitHub and create a new pull request so that the integrator can review the code. For large features that impact multiple parts of the code it is best to open a new issue on issue tracker so that the design of the code can be discussed first.

    +

    2.2 Contributing#

    We use the feature branch git workflow. Thus when you are working on a task, please remember to assign the relevant issue to yourself on the issue tracker and branch off from master. When the task is completed, do remember to push the branch to GitHub and create a new pull request so that the integrator can review the code. For large features that impact multiple parts of the code it is best to open a new issue on the issue tracker so that the design of the code can be discussed first.

    -

    Test driven development is encouraged but not required. All incoming code should have 100% accompanying tests if possible - Coveralls will fail any incoming pull request which causes coverage to fall.

    +

    Test driven development is encouraged but not required. If possible, all of your incoming code should have 100% accompanying tests - Coveralls will fail any incoming pull request which causes coverage to fall.

    2.3 Coding Style#

    We use the Java coding standard found at https://oss-generic.github.io/process/codingstandards/coding-standards-java.html.

    3 Design#

    3.1 Architecture#

    +

    Now let us explore the architecture of Uncle Jim's Discount To-do App to help you understand how it works.

    Figure 1. Simplistic overview of the application
    -

    The Architecture Diagram above explains the high-level design of the App. Here is a quick overview of each component.

    -

    Main has only one class called MainApp. It is responsible for,

    +

    The architecture diagram above explains the high-level design of the application. Here is a quick overview of each component:

      -
    • At app launch: Bootstrapping the application by initializing the components in the correct sequence and injecting the dependencies needed for each component.
    • -
    • At shut down: Shuts down the components and invoke cleanup method where necessary.
    • +
    • +

      Main has only one class called MainApp. It is responsible for:

      +
        +
      • Bootstrapping the application at app launch by initializing the components in the correct sequence and injecting the dependencies needed for each component.
      • +
      • Shutting down the components and invoke cleanup method where necessary during shut down.
      +
    • +
    • Commons represents a collection of modules used by multiple other components.

      -
        +
      • UI: The user facing elements of tha App, representing the view layer.
      • Logic: The parser and command executer, representing the controller
      • Model: Data manipulation and storage, representing the model and data layer
      -

      Each of the three components defines its API in an interface with the same name and are bootstrapped at launch by MainApp.

      -

      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 TodoLogic.java class.

      +

      The UI, Logic and Model components each define their API in an interface with the same name and is bootstrapped at launch by MainApp.

      +

      For example, the Logic component (see the class diagram given below) defines its API in the Logic.java interface and exposes its functionality using the TodoLogic.java class.

      Figure 2. Example of a Logic class diagram exposing its API to other components
      -

      The Sequence Diagram below shows how the components interact when the user issues a generic command.

      Figure 3. The interaction of major components in the application through a sequence diagram
      +

      The sequence diagram above shows how the components interact with each other when the user issues a generic command.

      The diagram below shows how the EventsCenter reacts to a help command event, where the UI does not know or contain any business side logic.

      Figure 4. A sequence diagram showing how EventsCenter work
      @@ -204,7 +210,7 @@

      Developer Guide

      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.

      +

      The sections below will provide you with more details for each component.

      3.2 UI component#

      Figure 5. The relation between the UI subcomponents
      diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 3432ab853eee..f39cb0f3cce6 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,15 +2,15 @@ ## Introduction -Welcome to the project! This guide will get you up to speed with how to set up your development environment, -the basic architecture of the application, how to perform some common development tasks as well as -who to contact when you're lost. +Welcome to the Uncle Jim's Discount To-do App! + +This guide will teach you how to set up your development environment, explain the basic architecture of the application, teach you how to perform some common development tasks, as well as provide contact information for the times when you require additional help. ### Tooling This project uses -- **git** - Version control +- **Git** - Version control - **[Eclipse][eclipse]** - IDE - **Gradle** - Build automation - **[Travis][travis], [Coveralls][coveralls] and [Codacy][codacy]** - Continuous integration and quality control @@ -20,21 +20,24 @@ This project uses ### Prerequisites -1. **A git client**. If you're on Linux you should already have one installed on your command line. For Windows -and OS X you can use [SourceTree][sourcetree] if you are more comfortable with using GUI -2. [**JDK 1.8.0_60**][jdk] or later. Please use Oracle's because it comes with JavaFX, which is needed for -developing the application's UI. +1. **Git client** + If you are using Linux, you should already have one installed on your command line. If you are using Windows or OS X you can use [SourceTree][sourcetree] if you are more comfortable with using a GUI. +2. [**JDK 1.8.0_60**][jdk] or later + Please use Oracle's jdk because it comes with JavaFX, which is needed for developing the application's UI. 3. **Eclipse** IDE -4. **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. **e(fx)clipse** plugin for Eclipse + Perform steps 2 onwards as listed in [this page](http://www.eclipse.org/efxclipse/install.html#for-the-ambitious){: .print-url } to install the plugin. 5. **Buildship Gradle Integration** plugin from the Eclipse Marketplace - + You can find Eclipse Marketplace from Eclipse's `Help` toolbar. #### 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) +0. Fork this repository, and clone the fork to your computer with Git. +1. Open Eclipse +!!! note + + Ensure that you have installed the **e(fx)clipse** and **buildship** plugins as listed in the prerequisites above. + 2. Click `File` > `Import` 3. Click `Gradle` > `Gradle Project` > `Next` > `Next` 4. Click `Browse`, then locate the project's directory @@ -43,15 +46,15 @@ developing the application's UI. !!! note * 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 + * Depending on your connection speed and server load, this step may take up to 30 minutes 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. + * If Eclipse has changed any settings files during the import process, you can discard those changes. ### Contributing -We use the [feature branch git workflow][workflow]. When working on a task please remember to assign the relevant issue to yourself [on the issue tracker][issues] and branch off from `master`. When the task is complete remember to push the branch to GitHub and [create a new pull request][pr] so that the integrator can review the code. For large features that impact multiple parts of the code it is best to open a new issue on issue tracker so that the design of the code can be discussed first. +We use the [feature branch git workflow][workflow]. Thus when you are working on a task, please remember to assign the relevant issue to yourself [on the issue tracker][issues] and branch off from `master`. When the task is completed, do remember to push the branch to GitHub and [create a new pull request][pr] so that the integrator can review the code. For large features that impact multiple parts of the code it is best to open a new issue on the issue tracker so that the design of the code can be discussed first. -[Test driven development][tdd] is encouraged but not required. All incoming code should have 100% accompanying tests if possible - Coveralls will fail any incoming pull request which causes coverage to fall. +[Test driven development][tdd] is encouraged but not required. If possible, all of your incoming code should have 100% accompanying tests - Coveralls will fail any incoming pull request which causes coverage to fall. ### Coding Style @@ -62,37 +65,39 @@ We use the Java coding standard found at
      Simplistic overview of the application
      -The Architecture Diagram above explains the high-level design of the App. Here is a quick overview of each component. - -`Main` has only one class called [`MainApp`](../src/main/java/seedu/todo/MainApp.java). It is responsible for, +The architecture diagram above explains the high-level design of the application. Here is a quick overview of each component: -* At app launch: Bootstrapping the application by initializing the components in the correct sequence and injecting the dependencies needed for each component. -* At shut down: Shuts down the components and invoke cleanup method where necessary. +* `Main` has only one class called [`MainApp`](../src/main/java/seedu/todo/MainApp.java). It is responsible for: -[**`Commons`**](#common-modules) represents a collection of modules used by multiple other components. + * Bootstrapping the application at app launch by initializing the components in the correct sequence and injecting the dependencies needed for each component. + * Shutting down the components and invoke cleanup method where necessary during shut down. +* [**`Commons`**](#common-modules) represents a collection of modules used by multiple other components. * [**`UI`**](#ui-component): The user facing elements of tha App, representing the view layer. * [**`Logic`**](#logic-component): The parser and command executer, representing the controller * [**`Model`**](#model-component): Data manipulation and storage, representing the model and data layer -Each of the three components defines its API in an `interface` with the same name and are bootstrapped at launch by `MainApp`. +The UI, Logic and Model components each define their API in an `interface` with the same name and is bootstrapped at launch by `MainApp`. -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 `TodoLogic.java` class. +For example, the `Logic` component (see the class diagram given below) defines its API in the `Logic.java` interface and exposes its functionality using the `TodoLogic.java` class.
      Example of a Logic class diagram exposing its API to other components
      -The Sequence Diagram below shows how the components interact when the user issues a generic command. -
      The interaction of major components in the application through a sequence diagram
      + +The sequence diagram above shows how the components interact with each other when the user issues a generic command. + The diagram below shows how the `EventsCenter` reacts to a `help` command event, where the UI does not know or contain any business side logic. @@ -105,7 +110,7 @@ The diagram below shows how the `EventsCenter` reacts to a `help` command event, 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. +The sections below will provide you with more details for each component. ### UI component @@ -955,6 +960,7 @@ See `build.gradle` > `allprojects` > `dependencies` > `testCompile` for the list *[CRUD]: Create, Retrieve, Update, Delete *[GUI]: Graphical User Interface *[UI]: User interface +*[IDE]: Integrated Development Environment [repo]: https://github.com/CS2103AUG2016-W10-C4/main/ diff --git a/src/test/java/seedu/todo/testutil/TestUtil.java b/src/test/java/seedu/todo/testutil/TestUtil.java index 91a0883afd1b..fb1c95e47e1f 100644 --- a/src/test/java/seedu/todo/testutil/TestUtil.java +++ b/src/test/java/seedu/todo/testutil/TestUtil.java @@ -13,11 +13,13 @@ import org.loadui.testfx.GuiTest; import org.testfx.api.FxToolkit; import seedu.todo.TestApp; +import seedu.todo.commons.exceptions.IllegalValueException; import seedu.todo.commons.util.FileUtil; import seedu.todo.commons.util.XmlUtil; import seedu.todo.model.AddressBook; import seedu.todo.model.TodoList; import seedu.todo.model.person.ReadOnlyPerson; +import seedu.todo.model.tag.Tag; import seedu.todo.model.task.ImmutableTask; import seedu.todo.storage.TodoListStorage; import seedu.todo.storage.XmlSerializableAddressBook; @@ -60,34 +62,6 @@ public static void assertThrows(Class expected, Runnable ex */ public static String SANDBOX_FOLDER = FileUtil.getPath("./src/test/data/sandbox/"); - public static void assertAllPropertiesEqual(ImmutableTask a, ImmutableTask b) { - assertEquals(a.getTitle(), b.getTitle()); - assertEquals(a.getDescription(), b.getDescription()); - assertEquals(a.getLocation(), b.getLocation()); - assertEquals(a.getStartTime(), b.getStartTime()); - assertEquals(a.getEndTime(), b.getEndTime()); - assertEquals(a.isPinned(), b.isPinned()); - assertEquals(a.isCompleted(), b.isCompleted()); - assertEquals(a.getTags(), b.getTags()); - assertEquals(a.getUUID(), b.getUUID()); - } - - public static final Tag[] sampleTagData = getSampleTagData(); - - private static Tag[] getSampleTagData() { - try { - return new Tag[]{ - new Tag("relatives"), - new Tag("friends") - }; - } catch (IllegalValueException e) { - assert false; - return null; - //not possible - } - } - - /** * Appends the file name to the sandbox folder path. * Creates the sandbox folder if it doesn't exist.