diff --git a/app/src/processing/app/CommandHistory.java b/app/src/processing/app/CommandHistory.java new file mode 100644 index 00000000000..25ded6f1fb9 --- /dev/null +++ b/app/src/processing/app/CommandHistory.java @@ -0,0 +1,118 @@ +package processing.app; + +import java.util.ArrayList; +import java.util.List; + +/** + * Keeps track of command history in console-like applications. + * @author P.J.S. Kools + */ +public class CommandHistory { + + private List commandHistory = new ArrayList(); + private int selectedCommandIndex = 0; + private final int maxHistorySize; + + /** + * Create a new {@link CommandHistory}. + * @param maxHistorySize - The max command history size. + */ + public CommandHistory(int maxHistorySize) { + this.maxHistorySize = (maxHistorySize < 0 ? 0 : maxHistorySize); + this.commandHistory.add(""); // Current command placeholder. + } + + /** + * Adds the given command to the history and resets the history traversal + * position to the latest command. If the max history size is exceeded, + * the oldest command will be removed from the history. + * @param command - The command to add. + */ + public void addCommand(String command) { + + // Remove the oldest command if the max history size is exceeded. + if(this.commandHistory.size() >= this.maxHistorySize + 1) { + this.commandHistory.remove(0); + } + + // Add the new command, reset the 'current' command and reset the index. + this.commandHistory.set(this.commandHistory.size() - 1, command); + this.commandHistory.add(""); // Current command placeholder. + this.selectedCommandIndex = this.commandHistory.size() - 1; + } + + /** + * Gets whether a next (more recent) command is available in the history. + * @return {@code true} if a next command is available, + * returns {@code false} otherwise. + */ + public boolean hasNextCommand() { + return this.selectedCommandIndex + 1 < this.commandHistory.size(); + } + + /** + * Gets the next (more recent) command from the history. + * @return The next command or {@code null} if no next command is available. + */ + public String getNextCommand() { + return this.hasNextCommand() + ? this.commandHistory.get(++this.selectedCommandIndex) : null; + } + + /** + * Gets whether a previous (older) command is available in the history. + * @return {@code true} if a previous command is available, + * returns {@code false} otherwise. + */ + public boolean hasPreviousCommand() { + return this.selectedCommandIndex > 0; + } + + /** + * Gets the previous (older) command from the history. + * When this method is called while the most recent command in the history is + * selected, this will store the current command as temporary latest command + * so that {@link #getNextCommand()} will return it. This temporary latest + * command gets reset when this case occurs again or when + * {@link #addCommand(String)} is invoked. + * @param currentCommand - The current unexecuted command. + * @return The previous command or {@code null} if no previous command is + * available. + */ + public String getPreviousCommand(String currentCommand) { + + // Return null if there is no previous command available. + if (!this.hasPreviousCommand()) { + return null; + } + + // Store current unexecuted command if not traversing already. + if (this.selectedCommandIndex == this.commandHistory.size() - 1) { + this.commandHistory.set(this.commandHistory.size() - 1, + (currentCommand == null ? "" : currentCommand)); + } + + // Return the previous command. + return this.commandHistory.get(--this.selectedCommandIndex); + } + + /** + * Resets the history location to the most recent command. + * @returns The latest unexecuted command as stored by + * {@link #getPreviousCommand(String)} or an empty string if no such command + * was set. + */ + public String resetHistoryLocation() { + this.selectedCommandIndex = this.commandHistory.size() - 1; + return this.commandHistory.set(this.commandHistory.size() - 1, ""); + } + + /** + * Clears the command history. + */ + public void clear() { + this.commandHistory.clear(); + this.commandHistory.add(""); // Current command placeholder. + this.selectedCommandIndex = 0; + } +} diff --git a/app/src/processing/app/SerialMonitor.java b/app/src/processing/app/SerialMonitor.java index d69b5ae27db..435f19956c3 100644 --- a/app/src/processing/app/SerialMonitor.java +++ b/app/src/processing/app/SerialMonitor.java @@ -23,6 +23,9 @@ import java.awt.Color; import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; import static processing.app.I18n.tr; @@ -32,6 +35,10 @@ public class SerialMonitor extends AbstractTextMonitor { private Serial serial; private int serialRate; + private static final int COMMAND_HISTORY_SIZE = 100; + private final CommandHistory commandHistory = + new CommandHistory(COMMAND_HISTORY_SIZE); + public SerialMonitor(BoardPort port) { super(port); @@ -54,11 +61,42 @@ public SerialMonitor(BoardPort port) { }); onSendCommand((ActionEvent event) -> { - send(textField.getText()); + String command = textField.getText(); + send(command); + commandHistory.addCommand(command); textField.setText(""); }); - + onClearCommand((ActionEvent event) -> textArea.setText("")); + + // Add key listener to UP, DOWN, ESC keys for command history traversal. + textField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + switch (e.getKeyCode()) { + + // Select previous command. + case KeyEvent.VK_UP: + if (commandHistory.hasPreviousCommand()) { + textField.setText( + commandHistory.getPreviousCommand(textField.getText())); + } + break; + + // Select next command. + case KeyEvent.VK_DOWN: + if (commandHistory.hasNextCommand()) { + textField.setText(commandHistory.getNextCommand()); + } + break; + + // Reset history location, restoring the last unexecuted command. + case KeyEvent.VK_ESCAPE: + textField.setText(commandHistory.resetHistoryLocation()); + break; + } + } + }); } private void send(String s) {