diff --git a/src/main/java/tuteez/logic/commands/CommandHistory.java b/src/main/java/tuteez/logic/commands/CommandHistory.java new file mode 100644 index 00000000000..59ab4f66362 --- /dev/null +++ b/src/main/java/tuteez/logic/commands/CommandHistory.java @@ -0,0 +1,58 @@ +package tuteez.logic.commands; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the history of commands entered by the user. + */ +public class CommandHistory { + private final List history; + private int currentIndex; + + /** + * Creates a CommandHistory class to remember the history of commands the user entered. + * The current index is initially set to -1 to indicate that no commands have been accessed + * or selected from the history yet. + */ + public CommandHistory() { + this.history = new ArrayList<>(); + this.currentIndex = -1; + } + + /** + * Adds a new command to the history and resets the current index. + */ + public void add(String command) { + assert command != null; + history.add(command); + // Reset index to the end of the list. + currentIndex = history.size(); + } + + /** + * Returns the previous command in history, or null if no previous command exists. + */ + public String getPreviousCommand() { + assert currentIndex >= -1 && currentIndex <= history.size(); + if (currentIndex > 0) { + currentIndex--; + return history.get(currentIndex); + } + return null; // No previous command available + } + + /** + * Returns the next command in history, or an empty string if no next command exists. + */ + public String getNextCommand() { + assert currentIndex >= -1 && currentIndex <= history.size(); + if (currentIndex < history.size() - 1) { + currentIndex++; + return history.get(currentIndex); + } + currentIndex = history.size(); + return ""; + } + +} diff --git a/src/main/java/tuteez/ui/CommandBox.java b/src/main/java/tuteez/ui/CommandBox.java index af385b6b090..4c3cc3a0096 100644 --- a/src/main/java/tuteez/ui/CommandBox.java +++ b/src/main/java/tuteez/ui/CommandBox.java @@ -3,7 +3,10 @@ import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; +import tuteez.logic.commands.CommandHistory; import tuteez.logic.commands.CommandResult; import tuteez.logic.commands.exceptions.CommandException; import tuteez.logic.parser.exceptions.ParseException; @@ -18,6 +21,8 @@ public class CommandBox extends UiPart { private final CommandExecutor commandExecutor; + private final CommandHistory commandHistory; + @FXML private TextField commandTextField; @@ -27,8 +32,11 @@ public class CommandBox extends UiPart { public CommandBox(CommandExecutor commandExecutor) { super(FXML); this.commandExecutor = commandExecutor; + this.commandHistory = new CommandHistory(); // calls #setStyleToDefault() whenever there is a change to the text of the command box. commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); + + commandTextField.addEventFilter(KeyEvent.KEY_PRESSED, this::handleKeyPressed); } /** @@ -43,12 +51,40 @@ private void handleCommandEntered() { try { commandExecutor.execute(commandText); + commandHistory.add(commandText); commandTextField.setText(""); } catch (CommandException | ParseException e) { setStyleToIndicateCommandFailure(); } } + /** + * Handles key press events for navigating the command history. + */ + private void handleKeyPressed(KeyEvent event) { + if (event.getCode() == KeyCode.UP || event.getCode() == KeyCode.DOWN) { + String command = (event.getCode() == KeyCode.UP) + ? commandHistory.getPreviousCommand() + : commandHistory.getNextCommand(); + + if (command != null) { + updateCommandField(command); + } + + event.consume(); + } + } + + /** + * Updates the command text field and sets the caret position to the end. + * + * @param command The command to set in the text field. + */ + private void updateCommandField(String command) { + commandTextField.setText(command); + commandTextField.positionCaret(command.length()); + } + /** * Sets the command box style to use the default style. */ diff --git a/src/main/java/tuteez/ui/MainWindow.java b/src/main/java/tuteez/ui/MainWindow.java index 5151d9fadf0..688a88d7196 100644 --- a/src/main/java/tuteez/ui/MainWindow.java +++ b/src/main/java/tuteez/ui/MainWindow.java @@ -4,10 +4,12 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import tuteez.commons.core.GuiSettings; @@ -38,6 +40,9 @@ public class MainWindow extends UiPart { @FXML private StackPane commandBoxPlaceholder; + @FXML + private MenuBar menuBar; + @FXML private MenuItem helpMenuItem; @@ -66,6 +71,8 @@ public MainWindow(Stage primaryStage, Logic logic) { setAccelerators(); helpWindow = new HelpWindow(); + + setupMenuBarListener(); } public Stage getPrimaryStage() { @@ -135,6 +142,22 @@ private void setWindowDefaultSize(GuiSettings guiSettings) { } } + /** + * Sets up the event listeners for the menu bar. + */ + private void setupMenuBarListener() { + menuBar.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> handleMenuBarClick()); + } + + /** + * Handles the click event for the menu bar to transfer focus away from command box to resultDisplay. + * This method is necessary to ensure that when the up and down arrow keys are recorded, + * the menu bar does not interfere with the command box's input. + */ + private void handleMenuBarClick() { + resultDisplayPlaceholder.requestFocus(); + } + /** * Opens the help window or focuses on it if it's already opened. */ diff --git a/src/test/java/tuteez/logic/commands/CommandHistoryTest.java b/src/test/java/tuteez/logic/commands/CommandHistoryTest.java new file mode 100644 index 00000000000..ddf5f8fdb80 --- /dev/null +++ b/src/test/java/tuteez/logic/commands/CommandHistoryTest.java @@ -0,0 +1,89 @@ +package tuteez.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.lang.reflect.Field; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +public class CommandHistoryTest { + private CommandHistory commandHistory; + + @BeforeEach + public void setUp() { + commandHistory = new CommandHistory(); + } + + @Test + public void add_nullCommand_throwsException() { + // Check that adding a null command throws an exception + assertThrows(AssertionError.class, () -> commandHistory.add(null)); + } + + @Test + public void add_correctlyAddsCommands() { + commandHistory.add("command1"); + commandHistory.add("command2"); + + // Check if the commands have been added correctly + assertEquals("command2", commandHistory.getPreviousCommand()); + assertEquals("command1", commandHistory.getPreviousCommand()); + + // Ensure currentIndex is reset to the end of the list after adding commands + commandHistory.add("command3"); + assertEquals("command3", commandHistory.getPreviousCommand()); // Should return command3 + } + @Test + public void getPreviousCommand_correctlyGetsPreviousCommands() { + commandHistory.add("command1"); + commandHistory.add("command2"); + commandHistory.add("command3"); + + // Check if the last command can be retrieved + assertEquals("command3", commandHistory.getPreviousCommand()); + assertEquals("command2", commandHistory.getPreviousCommand()); + assertEquals("command1", commandHistory.getPreviousCommand()); + assertNull(commandHistory.getPreviousCommand()); + } + + @Test + public void getNextCommand_correctlyGetsNextCommands() { + commandHistory.add("command1"); + commandHistory.add("command2"); + assertEquals("command2", commandHistory.getPreviousCommand()); + assertEquals("command1", commandHistory.getPreviousCommand()); + assertEquals("command2", commandHistory.getNextCommand()); + assertEquals("", commandHistory.getNextCommand()); // No more next commands + } + + @Test + public void getNextCommand_invalidCurrentIndex_throwsAssertionError() throws Exception { + commandHistory.add("command1"); + commandHistory.add("command2"); + + // Use reflection to set currentIndex to an invalid value (e.g., larger than history.size()) + Field field = CommandHistory.class.getDeclaredField("currentIndex"); + field.setAccessible(true); + field.set(commandHistory, 999); // Invalid index + + // Expect an AssertionError when calling getNextCommand() + assertThrows(AssertionError.class, () -> commandHistory.getNextCommand()); + } + + @Test + public void getPreviousCommand_invalidCurrentIndex_throwsAssertionError() throws Exception { + commandHistory.add("command1"); + commandHistory.add("command2"); + + // Use reflection to set currentIndex to an invalid value (e.g., negative beyond -1) + Field field = CommandHistory.class.getDeclaredField("currentIndex"); + field.setAccessible(true); + field.set(commandHistory, -999); // Invalid index + + // Expect an AssertionError when calling getPreviousCommand() + assertThrows(AssertionError.class, () -> commandHistory.getPreviousCommand()); + } + +}