/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.gui.command; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.custom.VerifyKeyListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Menu; import org.eclipse.ui.IActionBars; import org.eclipse.ui.part.ViewPart; import de.rcenvironment.core.command.api.CommandExecutionResult; import de.rcenvironment.core.command.api.CommandExecutionService; import de.rcenvironment.core.command.spi.AbstractInteractiveCommandConsole; import de.rcenvironment.core.gui.resources.api.FontManager; import de.rcenvironment.core.gui.resources.api.ImageManager; import de.rcenvironment.core.gui.resources.api.StandardFonts; import de.rcenvironment.core.gui.resources.api.StandardImages; import de.rcenvironment.core.gui.utils.common.ClipboardHelper; import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils; import de.rcenvironment.core.utils.incubator.ServiceRegistry; import de.rcenvironment.core.utils.incubator.ServiceRegistryAccess; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; /** * A visual command console for executing RCE commands. * * @author Marc Stammerjohann * @author Doreen Seider (command history tweaks) * @author Robert Mischke (fixed #10886) * @author Sascha Zur */ public class CommandConsoleViewer extends ViewPart { private static final String RCEPROMPT = "rce>"; private static final int RCEPROMPTLENGTH = RCEPROMPT.length(); private static final Color PROMPT_COLOR_BLUE = Display.getCurrent().getSystemColor(SWT.COLOR_BLUE); private static final int MAXIMUM_PASTE_LENGTH = 10000; /** Used to insert text in new line. It is not allowed to insert text behind this position. */ private int caretLinePosition; /** Used to extract text from styled text. */ private int currentLine; private Action clearConsoleAction; private Action copyAction; private Action pasteAction; private StyledText styledtext; private CommandConsoleOutputAdapter textOutputReceiver; private CommandExecutionService commandExecutionService; // replacing the line break with the platform-independent one results in errors (invalid // argument) in #setSelection(int start); // investigate if "\n" causes issues private final String lineBreak = "\n"; private final String platformIndependentLineBreak = System.lineSeparator(); /** starts at -1. */ private int commandPosition = 0 - 1; private final Log log = LogFactory.getLog(getClass()); private CommandHandler commandService; private List<String> usedCommands; /** * An {@link Action} to clear the displayed text in the console. */ private final class ClearConsoleAction extends Action { ClearConsoleAction(String clearConsoleActionContextMenuLabel) { super(clearConsoleActionContextMenuLabel); } @Override public void run() { currentLine = 0; styledtext.selectAll(); insertText(""); insertRCEPrompt(); setSelection(caretLinePosition); clearConsoleAction.setEnabled(false); } } /** * An {@link Action} to paste text in the console. */ private final class PasteAction extends Action { PasteAction(String text) { super(text); } @Override public void run() { // if multiple lines are in the clipboard, extract the first one and put it back to the // clipboard so that only this line will be String content = ClipboardHelper.getContentAsStringOrNull(); if (content == null) { return; } final int contentLength = content.length(); if (contentLength < MAXIMUM_PASTE_LENGTH) { if (content.contains(platformIndependentLineBreak)) { content = content.replaceAll(platformIndependentLineBreak, " "); } ClipboardHelper.setContent(content); styledtext.paste(); String line = getLineWithoutRCEPROMPT(currentLine); setStyledRange(caretLinePosition, line.length()); if (!line.isEmpty()) { clearConsoleAction.setEnabled(true); } } else { Display.getCurrent().asyncExec(new Runnable() { @Override public void run() { String warningMessage = "The text could not be pasted because it is too long. " + "Its length is " + contentLength + " characters but the maximum allowed length is " + MAXIMUM_PASTE_LENGTH + " characters."; MessageDialog.open(MessageDialog.ERROR, null, "Warning", warningMessage, SWT.NONE); } }); } } } /** * An {@link Action} to copy text of the console. */ private final class CopyAction extends Action { protected CopyAction(String text) { super(text); } @Override public void run() { styledtext.copy(); } } @Override public void createPartControl(Composite parent) { /* Layout Section */ GridLayout gridLayout = new GridLayout(1, false); parent.setLayout(gridLayout); GridData gridDataLabel = new GridData(GridData.FILL_HORIZONTAL); GridData gridDataStyledText = new GridData(GridData.FILL_BOTH); /* Label Section */ Label label = new Label(parent, SWT.BORDER); label.setText(Messages.defaultLabelText); label.setLayoutData(gridDataLabel); /* Text Section */ styledtext = new StyledText(parent, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL); styledtext.setLayoutData(gridDataStyledText); /* Listener Section */ styledtext.addKeyListener(new CommandKeyListener()); styledtext.addTraverseListener(new CommandTraverseListener()); styledtext.addMouseListener(new CommandMouseListener()); styledtext.addVerifyKeyListener(new CommandVerifyKeyListener()); styledtext.setFont(FontManager.getInstance().getFont(StandardFonts.CONSOLE_TEXT_FONT)); styledtext.addModifyListener(new CommandModifyListener()); insertRCEPrompt(); makeActions(); hookContextMenu(); contributeToActionBars(); registerServices(); commandService = new CommandHandler(); usedCommands = commandService.getUsedCommands(); textOutputReceiver = new CommandConsoleOutputAdapter(); } /** Register {@link CommandExecutionService}. */ private void registerServices() { ServiceRegistryAccess serviceRegistryAccess = ServiceRegistry.createAccessFor(this); commandExecutionService = serviceRegistryAccess.getService(CommandExecutionService.class); } private void hookContextMenu() { MenuManager menuMgr = new MenuManager("#PopupMenu"); menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(new IMenuListener() { @Override public void menuAboutToShow(IMenuManager manager) { CommandConsoleViewer.this.fillContextMenu(manager); } }); Menu menu = menuMgr.createContextMenu(styledtext); styledtext.setMenu(menu); } private void contributeToActionBars() { IActionBars bars = getViewSite().getActionBars(); fillLocalToolBar(bars.getToolBarManager()); } private void fillLocalToolBar(IToolBarManager manager) { manager.add(clearConsoleAction); } private void fillContextMenu(IMenuManager manager) { manager.add(copyAction); manager.add(pasteAction); manager.add(new Separator()); manager.add(clearConsoleAction); } private void makeActions() { clearConsoleAction = new ClearConsoleAction(Messages.clearConsoleActionContextMenuLabel); clearConsoleAction.setImageDescriptor(ImageManager.getInstance().getImageDescriptor(StandardImages.CLEARCONSOLE_16)); clearConsoleAction.setEnabled(false); copyAction = new CopyAction(Messages.copyActionContextMenuLabel); copyAction.setImageDescriptor(ImageManager.getInstance().getImageDescriptor(StandardImages.COPY_16)); pasteAction = new PasteAction(Messages.pasteActionContextMenuLabel); pasteAction.setImageDescriptor(ImageManager.getInstance().getImageDescriptor(StandardImages.PASTE_16)); } @Override public void setFocus() { styledtext.setFocus(); } /** Changes output text color to black. */ private void setStyledRange(int styledstart, int styledlength) { StyleRange styleRange = new StyleRange(); styleRange.start = styledstart; styleRange.length = styledlength; styleRange.foreground = PROMPT_COLOR_BLUE; styledtext.setStyleRange(styleRange); } /** Display all used commands. */ private void displayUsedCommands() { // reverse order to display commands chronologically List<String> usedCommandsToDisplay = new ArrayList<>(usedCommands); Collections.reverse(usedCommandsToDisplay); for (String command : usedCommandsToDisplay) { insertTextWithLineBreak(command); selectNewLine(command.length()); } insertRCEPrompt(); increaseCurrentLine(); } /** Insert RCE Prompt in the current line. */ private void insertRCEPrompt() { insertText(RCEPROMPT); setCaretLinePosition(getCurrentCaretLocation() + RCEPROMPTLENGTH); setSelection(caretLinePosition); setStyledRange(caretLinePosition - RCEPROMPTLENGTH, RCEPROMPTLENGTH); } /** * Display a command from a given String without line break. * * @param command to be displayed */ private void displayCommand(String command) { String line = getLineWithoutRCEPROMPT(currentLine); if (line.isEmpty()) { setSelection(caretLinePosition); } else { setSelectionRange(caretLinePosition, line.length()); } insertText(command); setStyledRange(caretLinePosition, command.length()); setSelection(caretLinePosition + command.length()); } /** * Display a messages from a given String with a line break. * * @param message to be displayed */ private void displayMessage(String message) { int selected = message.length(); String line = getLine(currentLine); if (line.isEmpty()) { insertTextWithLineBreak(message); selectNewLine(selected); } else { int caretOffsetLine = getCurrentCaretLocation(); setSelectionRange(caretLinePosition - RCEPROMPTLENGTH, line.length()); insertTextWithLineBreak(message); int currentCaretLocation = getCurrentCaretLocation(); insertText(line); setStyledRange(currentCaretLocation, line.length()); selectLineAndUpdateCaretPosition(currentCaretLocation + RCEPROMPTLENGTH); // select same caret location as it was before setSelection(caretOffsetLine + selected + 1); } } /** displays commands, when arrow up is pressed. */ private void displayCommands() { if (commandPosition + 1 < usedCommands.size()) { commandPosition++; displayCommand(usedCommands.get(commandPosition)); } } /** displays commands (reverse), when arrow down is pressed. */ private void displayCommandsReverse() { if (commandPosition < usedCommands.size() && commandPosition > 0) { commandPosition--; displayCommand(usedCommands.get(commandPosition)); } } /** Reset command position to -1. */ private void resetCommandPosition() { this.commandPosition = 0 - 1; } private void setSelection(int start) { styledtext.setSelection(start); } private void setSelectionRange(int start, int length) { styledtext.setSelectionRange(start, length); } private void insertText(String text) { styledtext.insert(text); } private void insertTextWithLineBreak(String text) { styledtext.insert(text + lineBreak); increaseCurrentLine(); } private void selectNewLine(int selection) { setSelection(getCurrentCaretLocation() + selection + 1); } private void selectLineAndUpdateCaretPosition(int selection) { setSelection(selection); // change caretLinePosition to the new line setCaretLinePosition(getCurrentCaretLocation()); } private void setCaretLinePosition(int caretLinePosition) { this.caretLinePosition = caretLinePosition; } private void increaseCurrentLine() { currentLine++; } private int getCurrentCaretLocation() { return styledtext.getCaretOffset(); } private String getLine(int line) { try { return styledtext.getLine(line); } catch (IllegalArgumentException e) { log.error("Invalid line " + line + " requested; line count is " + styledtext.getLineCount(), e); return ""; } } private String getLineWithoutRCEPROMPT(int line) { return getLine(line).replaceFirst(RCEPROMPT, ""); } /** * If a key (between 'a' and 'z', '0' and '9') is pressed, caret selects the command line (where the latest RCEPROMPT is). If command * line contains text, caret is set at the end. */ private void moveCaretToCommandLine() { String line = getLineWithoutRCEPROMPT(currentLine); setSelection(caretLinePosition + line.length()); } /** * As soon as the text is modified this listener ensures that the clearConsoleAction is enabled, if the text contains anything else than * the command prompt. */ private class CommandModifyListener implements ModifyListener { private boolean containsContent() { for (int i = 0; i <= currentLine; i++) { if (!getLineWithoutRCEPROMPT(i).isEmpty()) { return true; } } return false; } @Override public void modifyText(ModifyEvent arg0) { if (clearConsoleAction != null && !clearConsoleAction.isEnabled() && containsContent()) { clearConsoleAction.setEnabled(true); } } } /** A {@link KeyListener} to react on pressed or released key's. */ private class CommandKeyListener implements KeyListener { @Override public void keyReleased(KeyEvent keyEvent) { // disable clearConsoleAction, if after Backspace/Delete the first line is empty if (keyEvent.keyCode == SWT.BS || keyEvent.keyCode == SWT.DEL) { if (getLineWithoutRCEPROMPT(0).isEmpty()) { clearConsoleAction.setEnabled(false); } } if (keyEvent.keyCode == SWT.ESC) { styledtext.setSelection(styledtext.getCharCount() - getLineWithoutRCEPROMPT(currentLine).length(), styledtext.getCharCount()); insertText(""); setSelection(caretLinePosition); } } @Override public void keyPressed(KeyEvent keyEvent) { String command = getLineWithoutRCEPROMPT(currentLine); // It can happen that keyEvent.keyCode == SWT.CR but keyEvent.character != '\r'. This is for example the case if you press <^> // once on your keyboard and <RETURN> directly after that. In this case two KeyEvents are passed to this method. Both KeyEvents // have keyEvent.keyCode == SWT.CR but one will also have keyEvent.character == '^'. This is due to the construction of some // special characters which require two key presses, e.g. diacritical versions of characters like the circumflex on top of the // letter e. if ((keyEvent.keyCode == SWT.CR || keyEvent.keyCode == SWT.KEYPAD_CR) && keyEvent.character == '\r') { if (command.isEmpty()) { // If the prompt is selected and the selection ends at the current caret position, there occured an exception. // To quick fix it, if this kind of selection is done, pressing return won't enter a new empty line. if (styledtext.getSelectionCount() == 0) { increaseCurrentLine(); insertRCEPrompt(); } } else { if (command.equals(Messages.historyUsedCommand)) { displayUsedCommands(); return; } else if (command.equals("clear")) { clearConsoleAction.run(); return; } else { setSelection(styledtext.getCaretOffset() + command.length()); resetCommandPosition(); // addUsedCommand(command); setCaretLinePosition(caretLinePosition + command.length()); increaseCurrentLine(); // TODO problematic insertRCEPrompt(); // run command in separate thread to keep the UI responsive ConcurrencyUtils.getAsyncTaskService().execute(new ExecuteCommand(command)); } } } else if (keyEvent.keyCode == SWT.HOME) { setSelection(caretLinePosition); // POS1/HOME moves caret behind prompt of last command line } else if (keyEvent.keyCode == SWT.END) { setSelection(caretLinePosition + command.length()); // END moves caret behind command of the last of command line } if (!command.isEmpty() && getCurrentCaretLocation() > caretLinePosition) { // set new character to prompt color setStyledRange(getCurrentCaretLocation() - 1, 1); } } } /** A {@link TraverseListener} to change the keys behavior. (Return, Arrow Up and Arrow Down) */ private class CommandTraverseListener implements TraverseListener { @Override public void keyTraversed(TraverseEvent event) { switch (event.detail) { case SWT.TRAVERSE_RETURN: executeReturn(event); break; case SWT.TRAVERSE_ARROW_PREVIOUS: if (event.keyCode == SWT.ARROW_UP) { disableEvent(event); displayCommands(); if (!getLineWithoutRCEPROMPT(currentLine).isEmpty()) { clearConsoleAction.setEnabled(true); } } break; case SWT.TRAVERSE_ARROW_NEXT: if (event.keyCode == SWT.ARROW_DOWN) { disableEvent(event); displayCommandsReverse(); } break; default: break; } } private void disableEvent(TraverseEvent event) { event.doit = true; event.detail = SWT.TRAVERSE_NONE; } /** * "Return" is enabled: if the selected line has text. <br> * "Return" is disabled: if the selected line is empty or the selected line is above the command * * @param event */ private void executeReturn(TraverseEvent event) { String line = getLineWithoutRCEPROMPT(currentLine); int currentCaretLocation = getCurrentCaretLocation(); if (currentCaretLocation < caretLinePosition) { disableEvent(event); } else { if (!line.isEmpty()) { // setSelection(styledtext.getCaretOffset() + (line.length() - // (styledtext.getCaretOffset() - // caretPosition))); setSelection(styledtext.getCaretOffset() + line.length()); } } } } /** * A {@link VerifyKeyListener} to change the keys behavior.<br> * Changes the behavior of the key, if the current caret position is lower than the {@link CommandConsoleViewer#caretLinePosition}. */ private class CommandVerifyKeyListener implements VerifyKeyListener { // keys are always enable, even above the caretLinePosition private final List<Integer> enabledEvents = new ArrayList<Integer>(); { enabledEvents.add(SWT.ARROW_LEFT); enabledEvents.add(SWT.ARROW_RIGHT); enabledEvents.add(SWT.PAGE_UP); enabledEvents.add(SWT.PAGE_DOWN); enabledEvents.add(SWT.HOME); enabledEvents.add(SWT.END); } @Override public void verifyKey(VerifyEvent event) { int currentCaretLocation = getCurrentCaretLocation(); int selectionRangeStart = styledtext.getSelectionRange().x; boolean emptyLine = getLine(currentLine).isEmpty(); // disable all key actions if caret location or selection range start is above caret // line position or if line is empty if (emptyLine || (caretLinePosition > currentCaretLocation || caretLinePosition > selectionRangeStart)) { if (!enabledEvents.contains(event.keyCode)) { if (event.stateMask == 0 && ((event.keyCode >= 'a' && event.keyCode <= 'z') || (event.keyCode >= '0' && event.keyCode <= '9'))) { moveCaretToCommandLine(); } else { event.doit = false; } } // enable a some key (combinations) // allow copy with CTRL + C if (event.stateMask == SWT.CTRL && event.keyCode == 'c') { event.doit = true; } } else { if (event.keyCode == SWT.BS) { checkBackspace(event, currentCaretLocation); } else if (event.keyCode == SWT.DEL) { checkDelete(event, currentCaretLocation); } else if ((event.stateMask == SWT.CTRL && event.keyCode == 'v') || (event.stateMask == SWT.SHIFT && event.keyCode == SWT.INSERT)) { // key event of 'paste' are using the 'pasteAction' event.doit = false; pasteAction.run(); } } } /** * "Backspace" is enabled: if the current command line has text. <br> * "Backspace" is disabled: if the caret is at the beginning of the line. * * @param event * @param caretOffset */ private void checkBackspace(VerifyEvent event, int caretOffset) { String line = getLineWithoutRCEPROMPT(currentLine); if (caretLinePosition < caretOffset && !line.isEmpty()) { event.doit = true; } else { event.doit = false; } } /** * "Delete" is enabled: if the current command line has text. <br> * * @param event * @param caretOffset */ private void checkDelete(VerifyEvent event, int caretOffset) { String line = getLineWithoutRCEPROMPT(currentLine); if (!line.isEmpty()) { event.doit = true; } else { event.doit = false; } } } /** * A {@link MouseListener} to set enable settings of Copy and Paste. */ private class CommandMouseListener implements MouseListener { @Override public void mouseDown(MouseEvent event) { int currentCaretLocation = getCurrentCaretLocation(); if (caretLinePosition > currentCaretLocation) { pasteAction.setEnabled(false); } else { pasteAction.setEnabled(true); } if (!styledtext.getSelectionText().isEmpty()) { copyAction.setEnabled(true); } else { copyAction.setEnabled(false); } } @Override public void mouseDoubleClick(MouseEvent event) {} @Override public void mouseUp(MouseEvent event) {} } /** Thread to execute the command. */ private class ExecuteCommand implements Runnable { private final String command; private final Display display = Display.getDefault(); private int line; private volatile boolean writing; ExecuteCommand(String command) { this.command = command; // this.line = ++currentLine; this.writing = true; } @Override @TaskDescription("Execute Command") public void run() { commandService.saveCommand(command); commandService.addUsedCommand(command); List<String> tokens = getTokens(); Future<CommandExecutionResult> asyncExecMultiCommand = commandExecutionService.asyncExecMultiCommand(tokens, textOutputReceiver, "command console"); // waitWithDelay(); // waitForExecToFinish(asyncExecMultiCommand); } /** * waiting for the execution to finish, while text is displayed user cannot enter new commands. */ private void waitWithDelay() { int sleepTime = 5; try { while (writing) { Thread.sleep(sleepTime); display.asyncExec(new Runnable() { @Override public void run() { // TODO this line can throw an "Index out of bounds" exception; check // before this code is being used again if (styledtext.isDisposed() || (writing && getLineWithoutRCEPROMPT(line++).isEmpty())) { writing = false; } } }); } display.asyncExec(new DisplayText(RCEPROMPT)); } catch (InterruptedException e) { log.error("Exception while waiting for console output", e); } } /** waiting until the execution of the command is finished. */ private void waitForExecToFinish(Future<CommandExecutionResult> asyncExecMultiCommand) { try { CommandExecutionResult commandExecutionResult = asyncExecMultiCommand.get(); if (commandExecutionResult == CommandExecutionResult.DEFAULT || commandExecutionResult == CommandExecutionResult.ERROR) { display.asyncExec(new DisplayText(RCEPROMPT)); } } catch (InterruptedException | ExecutionException e) { log.error("Exception while waiting for console command to complete", e); } } private List<String> getTokens() { final List<String> tokens = new LinkedList<String>(); String[] arguments = command.split(" "); for (String argument : arguments) { tokens.add(argument); } return tokens; } } /** Thread to interact with the swt styledtext. */ private class DisplayText implements Runnable { private final String text; DisplayText(String text) { this.text = text; } @Override @TaskDescription("Display Text") public void run() { if (styledtext.isDisposed()) { return; } if (text.equals(RCEPROMPT)) { insertRCEPrompt(); } else { displayMessage(text); } } } /** Class for handling command input and printing out the output. */ private class CommandConsoleOutputAdapter extends AbstractInteractiveCommandConsole { private final Display display = Display.getDefault(); CommandConsoleOutputAdapter() { super(commandExecutionService); } /** Adds an Output to the console. */ @Override public void addOutput(String line) { if (display.isDisposed()) { return; } if (line.contains("\n")) { while (line.contains("\n")) { // displays the string before the line break display.asyncExec(new DisplayText(line.substring(0, line.indexOf("\n")))); line = line.substring(line.indexOf("\n") + 1); } if (!line.equals("")) { display.asyncExec(new DisplayText(line)); } } else { display.asyncExec(new DisplayText(line)); } } } }