diff --git a/app/src/processing/app/CommandHistory.java b/app/src/processing/app/CommandHistory.java new file mode 100644 index 00000000000..cae3c2fc498 --- /dev/null +++ b/app/src/processing/app/CommandHistory.java @@ -0,0 +1,167 @@ +package processing.app; + +import java.util.LinkedList; +import java.util.ListIterator; + +/** + * Keeps track of command history in console-like applications. + * @author P.J.S. Kools + */ +public class CommandHistory { + + private final LinkedList commandHistory = new LinkedList(); + private final int maxHistorySize; + private ListIterator iterator = null; + private boolean iteratorAsc; + + /** + * Create a new {@link CommandHistory}. + * @param maxHistorySize - The max command history size. + */ + public CommandHistory(int maxHistorySize) { + this.maxHistorySize = (maxHistorySize < 0 ? 0 : maxHistorySize); + this.commandHistory.addLast(""); // Current command placeholder. + } + + /** + * Adds the given command to the history and resets the history traversal + * position to the latest command. If the latest command in the history is + * equal to the given command, it will not be added to the history. + * 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) { + if (this.maxHistorySize == 0) { + return; + } + + // Remove 'current' command. + this.commandHistory.removeLast(); + + // Add new command if it differs from the latest command. + if (this.commandHistory.isEmpty() + || !this.commandHistory.getLast().equals(command)) { + + // Remove oldest command if max history size is exceeded. + if (this.commandHistory.size() >= this.maxHistorySize) { + this.commandHistory.removeFirst(); + } + + // Add new command and reset 'current' command. + this.commandHistory.addLast(command); + } + + // Re-add 'current' command and reset command iterator. + this.commandHistory.addLast(""); // Current command placeholder. + this.iterator = null; + } + + /** + * 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() { + if (this.iterator == null) { + return false; + } + if (!this.iteratorAsc) { + this.iterator.next(); // Current command, ascending. + this.iteratorAsc = true; + } + return this.iterator.hasNext(); + } + + /** + * 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 null if there is no next command available. + if (!this.hasNextCommand()) { + return null; + } + + // Get next command. + String next = this.iterator.next(); + + // Reset 'current' command when at the end of the list. + if (this.iterator.nextIndex() == this.commandHistory.size()) { + this.iterator.set(""); // Reset 'current' command. + } + return next; + } + + /** + * 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() { + if (this.iterator == null) { + return this.commandHistory.size() > 1; + } + if (this.iteratorAsc) { + this.iterator.previous(); // Current command, descending. + this.iteratorAsc = false; + } + return this.iterator.hasPrevious(); + } + + /** + * 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 once. 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 and create iterator if not traversing. + if (this.iterator == null) { + this.iterator = + this.commandHistory.listIterator(this.commandHistory.size()); + this.iterator.previous(); // Last element, descending. + this.iteratorAsc = false; + } + + // Store current unexecuted command if on 'current' index. + if (this.iterator.nextIndex() == this.commandHistory.size() - 1) { + this.iterator.set(currentCommand == null ? "" : currentCommand); + } + + // Return the previous command. + return this.iterator.previous(); + } + + /** + * 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.iterator = null; + return this.commandHistory.set(this.commandHistory.size() - 1, ""); + } + + /** + * Clears the command history. + */ + public void clear() { + this.iterator = null; + this.commandHistory.clear(); + this.commandHistory.addLast(""); // Current command placeholder. + } +} diff --git a/app/src/processing/app/SerialMonitor.java b/app/src/processing/app/SerialMonitor.java index d4f59019eae..1f9a8d5b139 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(Base base, BoardPort port) { super(base, port); @@ -54,11 +61,42 @@ public SerialMonitor(Base base, 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) {