diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index 7cfb254650a..638569c684d 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -21,6 +21,8 @@ public class Messages { public static final String MESSAGE_SHOWING_HELP = "Opened help window."; public static final String MESSAGE_EXITING = "Exiting Address Book as requested ..."; + public static final String MESSAGE_COPIED = "Copied details to clipboard: %1$s"; + public static final int MESSAGE_COPIED_LEN = "Copied details to clipboard:".length(); /** * Returns an error message indicating the duplicate prefixes. diff --git a/src/main/java/seedu/address/logic/commands/CommandType.java b/src/main/java/seedu/address/logic/commands/CommandType.java index a33cf045042..69c943ec9d3 100644 --- a/src/main/java/seedu/address/logic/commands/CommandType.java +++ b/src/main/java/seedu/address/logic/commands/CommandType.java @@ -11,6 +11,12 @@ public Command createCommand(String arguments) throws IllegalArgumentException { return AddCommand.of(arguments); } }, + COPY { + @Override + public Command createCommand(String arguments) throws IllegalArgumentException { + return CopyCommand.of(arguments); + } + }, EDIT { @Override public Command createCommand(String arguments) throws IllegalArgumentException { diff --git a/src/main/java/seedu/address/logic/commands/CopyCommand.java b/src/main/java/seedu/address/logic/commands/CopyCommand.java new file mode 100644 index 00000000000..5f19656b41e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CopyCommand.java @@ -0,0 +1,180 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.model.person.fields.Address.PREFIX_ADDRESS; +import static seedu.address.model.person.fields.Assets.PREFIX_ASSET; +import static seedu.address.model.person.fields.Email.PREFIX_EMAIL; +import static seedu.address.model.person.fields.Name.PREFIX_NAME; +import static seedu.address.model.person.fields.Phone.PREFIX_PHONE; +import static seedu.address.model.person.fields.Tags.PREFIX_TAG; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.util.ArgumentMultimap; +import seedu.address.logic.util.ArgumentTokenizer; +import seedu.address.logic.util.ParserUtil; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Copies the details of a person in the address book. + */ +public class CopyCommand extends Command { + + public static final String COMMAND_WORD = "copy"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Copies one detail of the prefix identified\n" + + "Parameters: " + + "[" + PREFIX_NAME + "] " + + "[" + PREFIX_PHONE + "] " + + "[" + PREFIX_EMAIL + "] " + + "[" + PREFIX_ADDRESS + "] " + + "[" + PREFIX_TAG + "] " + + "[" + PREFIX_ASSET + "]\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_NAME + " "; + + public static final String MESSAGE_NO_PARAM = "One field to copy must be provided."; + public static final String MESSAGE_EXTRA_PARAM = "Only one field can be copied."; + + private final Index index; + private final boolean[] info; + + /** + * @param index of the person in the filtered person list to copy + * @param info list of details to copy to clipboard + */ + public CopyCommand(Index index, boolean[] info) { + requireNonNull(index); + + this.index = index; + this.info = info; + } + + @Override + public String execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToCopy = lastShownList.get(index.getZeroBased()); + String personDetails = copyToClipboard(personToCopy, info); + + return String.format(Messages.MESSAGE_COPIED, personDetails); + } + + /** + * Copies the details of {@code personToCopy} to clipboard + */ + private static String copyToClipboard(Person personToCopy, boolean[] info) { + requireNonNull(personToCopy); + + if (info[0]) { + return personToCopy.getName().toString(); + } + if (info[1]) { + return personToCopy.getPhone().toString(); + } + if (info[2]) { + return personToCopy.getEmail().toString(); + } + if (info[3]) { + return personToCopy.getAddress().toString(); + } + if (info[4]) { + return personToCopy.getTags().toString(); + } + if (info[5]) { + return personToCopy.getAssets().toString(); + } + + throw new IllegalArgumentException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE)); + } + + /** + * Parses the given {@code String} of arguments in the context of the CopyCommand + * and returns an CopyCommand object for execution. + * @throws IllegalArgumentException if the user input does not conform the expected format + */ + public static CopyCommand of(String args) throws IllegalArgumentException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_TAG, PREFIX_ASSET); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (IllegalArgumentException ie) { + throw new IllegalArgumentException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE), ie); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + + boolean[] info = new boolean[6]; + int totalParams = 0; + + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + info[0] = true; + totalParams++; + } + if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { + info[1] = true; + totalParams++; + } + if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { + info[2] = true; + totalParams++; + } + if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { + info[3] = true; + totalParams++; + } + if (argMultimap.getValue(PREFIX_TAG).isPresent()) { + info[4] = true; + totalParams++; + } + if (argMultimap.getValue(PREFIX_ASSET).isPresent()) { + info[5] = true; + totalParams++; + } + + if (totalParams == 0) { + throw new IllegalArgumentException(MESSAGE_NO_PARAM); + } else if (totalParams > 1) { + throw new IllegalArgumentException(MESSAGE_EXTRA_PARAM); + } + + return new CopyCommand(index, info); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof CopyCommand)) { + return false; + } + + CopyCommand otherCopyCommand = (CopyCommand) other; + return index.equals(otherCopyCommand.index); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .toString(); + } +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 4dbe9f59520..6bbbbd4403c 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -6,6 +6,8 @@ import javafx.fxml.FXML; import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputControl; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; @@ -161,6 +163,17 @@ private void handleExit() { primaryStage.hide(); } + /** + * Copies string to the clipboard + */ + @FXML + private void handleCopy(String detailsToCopy) { + Clipboard clipboard = Clipboard.getSystemClipboard(); + ClipboardContent content = new ClipboardContent(); + content.putString(detailsToCopy); + clipboard.setContent(content); + } + /** * Executes the command and returns the result. * @@ -178,12 +191,17 @@ private String executeCommand(String commandText) throws CommandException, Parse throw e; } + // == used to prevent an edge case where a command may somehow return this exact string, + // but is not actually a help or exit command. if (commandResult == Messages.MESSAGE_SHOWING_HELP) { handleHelp(); } if (commandResult == Messages.MESSAGE_EXITING) { handleExit(); } + if (commandResult.startsWith(Messages.MESSAGE_COPIED.substring(0, Messages.MESSAGE_COPIED_LEN + 1))) { + handleCopy(commandResult.substring(Messages.MESSAGE_COPIED_LEN).trim()); + } return commandResult; } diff --git a/src/test/java/seedu/address/logic/commands/CopyCommandTest.java b/src/test/java/seedu/address/logic/commands/CopyCommandTest.java new file mode 100644 index 00000000000..651d2d3274c --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/CopyCommandTest.java @@ -0,0 +1,211 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.assertParseFailure; +import static seedu.address.logic.commands.CopyCommand.MESSAGE_USAGE; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.PersonBuilder.DEFAULT_ADDRESS; +import static seedu.address.testutil.PersonBuilder.DEFAULT_ASSETS; +import static seedu.address.testutil.PersonBuilder.DEFAULT_EMAIL; +import static seedu.address.testutil.PersonBuilder.DEFAULT_NAME; +import static seedu.address.testutil.PersonBuilder.DEFAULT_PHONE; +import static seedu.address.testutil.PersonBuilder.DEFAULT_TAGS; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.person.Person; +import seedu.address.model.person.fields.Assets; +import seedu.address.model.person.fields.Tags; +import seedu.address.testutil.PersonBuilder; + +/** + * Contains integration tests (interaction with the Model) and unit tests for CopyCommand. + */ +public class CopyCommandTest { + + private static final String VALID_INDEX = "1"; + private static final String INVALID_INDEX = "a"; + private final Model model = new ModelManager(); + + @Test + public void execute_validCopy1_success() throws CommandException { + Person personToCopy = new PersonBuilder().build(); + model.addPerson(personToCopy); + Index index = Index.fromOneBased(1); + boolean[] info = {true, false, false, false, false, false}; + CopyCommand copyCommand = new CopyCommand(index, info); + + String expectedMessage = String.format(Messages.MESSAGE_COPIED, DEFAULT_NAME); + assertEquals(expectedMessage, copyCommand.execute(model)); + } + + @Test + public void execute_validCopy2_success() throws CommandException { + Person personToCopy = new PersonBuilder().build(); + model.addPerson(personToCopy); + Index index = Index.fromOneBased(1); + boolean[] info = {false, false, false, false, false, true}; + CopyCommand copyCommand = new CopyCommand(index, info); + + + String expectedMessage = String.format(Messages.MESSAGE_COPIED, new Assets(DEFAULT_ASSETS)); + assertEquals(expectedMessage, copyCommand.execute(model)); + } + + @Test + public void execute_validCopy3_success() throws CommandException { + Person personToCopy = new PersonBuilder().build(); + model.addPerson(personToCopy); + Index index = Index.fromOneBased(1); + boolean[] info = {false, true, false, false, false, false}; + CopyCommand copyCommand = new CopyCommand(index, info); + + + String expectedMessage = String.format(Messages.MESSAGE_COPIED, DEFAULT_PHONE); + assertEquals(expectedMessage, copyCommand.execute(model)); + } + + @Test + public void execute_validCopy4_success() throws CommandException { + Person personToCopy = new PersonBuilder().build(); + model.addPerson(personToCopy); + Index index = Index.fromOneBased(1); + boolean[] info = {false, false, true, false, false, false}; + CopyCommand copyCommand = new CopyCommand(index, info); + + + String expectedMessage = String.format(Messages.MESSAGE_COPIED, DEFAULT_EMAIL); + assertEquals(expectedMessage, copyCommand.execute(model)); + } + + @Test + public void execute_validCopy5_success() throws CommandException { + Person personToCopy = new PersonBuilder().build(); + model.addPerson(personToCopy); + Index index = Index.fromOneBased(1); + boolean[] info = {false, false, false, true, false, false}; + CopyCommand copyCommand = new CopyCommand(index, info); + + + String expectedMessage = String.format(Messages.MESSAGE_COPIED, DEFAULT_ADDRESS); + assertEquals(expectedMessage, copyCommand.execute(model)); + } + + @Test + public void execute_validCopy6_success() throws CommandException { + Person personToCopy = new PersonBuilder().build(); + model.addPerson(personToCopy); + Index index = Index.fromOneBased(1); + boolean[] info = {false, false, false, false, true, false}; + CopyCommand copyCommand = new CopyCommand(index, info); + + + String expectedMessage = String.format(Messages.MESSAGE_COPIED, new Tags(DEFAULT_TAGS)); + assertEquals(expectedMessage, copyCommand.execute(model)); + } + + @Test + public void execute_invalidIndex_throwsCommandException() { + Index index = Index.fromOneBased(2); + boolean[] info = {true, true, true, true, true, true}; + CopyCommand copyCommand = new CopyCommand(index, info); + assertThrows(CommandException.class, () -> copyCommand.execute(model)); + } + + @Test + public void execute_noParam_throwsIllegalArgumentException() { + Index index = Index.fromOneBased(1); + boolean[] info = {false, false, false, false, false, false}; + CopyCommand copyCommand = new CopyCommand(index, info); + assertThrows(CommandException.class, () -> copyCommand.execute(model)); + } + + @Test + public void execute_extraParam_throwsIllegalArgumentException() { + Index index = Index.fromOneBased(1); + boolean[] info = {false, true, true, false, false, false}; + CopyCommand copyCommand = new CopyCommand(index, info); + assertThrows(CommandException.class, () -> copyCommand.execute(model)); + } + + @Test + public void of_invalidInput_failure() { + assertParseFailure(CopyCommand::of, "Invalid", String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE)); + assertParseFailure(CopyCommand::of, "", String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE)); + assertThrows(IllegalArgumentException.class, () -> CopyCommand.of("1")); + assertThrows(IllegalArgumentException.class, () -> CopyCommand.of(INVALID_INDEX)); + assertThrows(IllegalArgumentException.class, () -> CopyCommand.of(VALID_INDEX + " ")); + assertThrows(IllegalArgumentException.class, () -> CopyCommand.of(INVALID_INDEX + " n/ a/")); + assertThrows(IllegalArgumentException.class, () -> CopyCommand.of(VALID_INDEX + " A/ t/ p/")); + } + + @Test + public void of_validInput_success() { + assertDoesNotThrow(() -> CopyCommand.of(VALID_INDEX + " n/")); + assertDoesNotThrow(() -> CopyCommand.of(VALID_INDEX + " A/")); + assertDoesNotThrow(() -> CopyCommand.of(VALID_INDEX + " a/")); + } + + @Test + public void equals_sameObject_returnsTrue() { + Index index = Index.fromOneBased(1); + boolean[] info = {true, true, true, true, true, true}; + CopyCommand copyCommand = new CopyCommand(index, info); + + assertEquals(copyCommand, copyCommand); + } + + @Test + public void equals_nullObject_returnsFalse() { + Index index = Index.fromOneBased(1); + boolean[] info = {true, true, true, true, true, true}; + CopyCommand copyCommand = new CopyCommand(index, info); + + assertNotEquals(copyCommand, null); + } + + @Test + public void equals_differentClass_returnsFalse() { + Index index = Index.fromOneBased(1); + boolean[] info = {true, true, true, true, true, true}; + CopyCommand copyCommand = new CopyCommand(index, info); + + assertNotEquals(copyCommand, "Not a CopyCommand object"); + } + + @Test + public void equals_sameValues_returnsTrue() { + Index index = Index.fromOneBased(1); + boolean[] info = {true, true, true, true, true, true}; + CopyCommand copyCommand1 = new CopyCommand(index, info); + CopyCommand copyCommand2 = new CopyCommand(index, info); + assertEquals(copyCommand1, copyCommand2); + } + + @Test + public void equals_differentValues_returnsFalse() { + Index index1 = Index.fromOneBased(1); + Index index2 = Index.fromOneBased(2); + boolean[] info1 = {true, true, true, true, true, true}; + boolean[] info2 = {true, true, true, true, true, false}; + CopyCommand copyCommand1 = new CopyCommand(index1, info1); + CopyCommand copyCommand2 = new CopyCommand(index2, info2); + assertNotEquals(copyCommand1, copyCommand2); + } + + @Test + public void equals_differentObject_returnsFalse() { + Index index = Index.fromOneBased(1); + boolean[] info = {true, true, true, true, true, true}; + CopyCommand copyCommand = new CopyCommand(index, info); + assertNotEquals(copyCommand, new Object()); + } +} diff --git a/src/test/java/seedu/address/logic/util/AddressBookParserTest.java b/src/test/java/seedu/address/logic/util/AddressBookParserTest.java index 195aafe9893..78de30f61da 100644 --- a/src/test/java/seedu/address/logic/util/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/util/AddressBookParserTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; +import static seedu.address.model.person.fields.Name.PREFIX_NAME; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; @@ -15,6 +16,7 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.CopyCommand; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; @@ -86,6 +88,12 @@ public void parseCommand_list() throws Exception { assertTrue(AddressBookParser.parseCommand(ListCommand.COMMAND_WORD + " 3") instanceof ListCommand); } + @Test + public void parseCommand_copy() throws Exception { + assertTrue(AddressBookParser.parseCommand(CopyCommand.COMMAND_WORD + " 1 " + + PREFIX_NAME) instanceof CopyCommand); + } + @Test public void parseCommand_unrecognisedInput_throwsParseException() { assertThrows(ParseException.class, String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE), ()