Java tutorial
/* ***************************************************************************** * Copyright (c) 2008-2009 Carl Masak <carl.masak@farmbio.uu.se> * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * <http://www.eclipse.org/legal/epl-v10.html> * * Contact: http://www.bioclipse.net/ ******************************************************************************/ package net.bioclipse.scripting.ui.views; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import net.bioclipse.scripting.ui.tabcompletion.TabCompleter; import org.eclipse.core.resources.IResource; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.util.LocalSelectionTransfer; import org.eclipse.jface.viewers.TreeSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.DropTarget; import org.eclipse.swt.dnd.DropTargetEvent; import org.eclipse.swt.dnd.DropTargetListener; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; 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.Text; import org.eclipse.ui.part.ViewPart; import org.eclipse.ui.texteditor.IWorkbenchActionDefinitionIds; /** * A general scripting console. * * @author masak * */ public abstract class ScriptingConsoleView extends ViewPart { /** A string representing the system's newline. */ public static String NEWLINE = System.getProperty("line.separator"); /** The preferred maximum length of a line of output. */ protected static final int MAX_OUTPUT_LINE_LENGTH = 79; private Text output; private Text input; /** List of all commands written. * * Don't be alarmed by the double braces after the constructor call; they * are amply explained at http://norvig.com/java-iaq.html under the section * "I have a class with six...". */ @SuppressWarnings("serial") private List<String> commandHistory = new ArrayList<String>() { { add(""); } }; /** * An index pointer into the command history. Between commands, it is reset * to point to the last (still not run) command, but changes when ARROW_UP * and ARROW_DOWN keys are used. */ private int currentHistoryLine = 0; private TabCompleter tabCompleter = new TabCompleter(); /** * Represents something to do when a specific key is pressed. Java 5 doesn't * have closures, so we use anonymous classes with a method in it instead, * which amounts to the same thing. */ protected static interface KeyAction { public void receiveKey(KeyEvent e); } /** * Essentially a switching table for handleKey. So, every time a keypress * is made that we intercept, a receiveKey method somewhere in actionTable * is called. * * Don't be alarmed by the double braces after the constructor call; they * are amply explained at http://norvig.com/java-iaq.html under the section * "I have a class with six...". */ @SuppressWarnings("serial") private Map<Integer, KeyAction> actionTable = new HashMap<Integer, KeyAction>() { { put(new Integer(SWT.CR), new KeyAction() { public void receiveKey(KeyEvent e) { carryOutCommand(input.getText().trim()); } }); put(new Integer(SWT.KEYPAD_CR), new KeyAction() { public void receiveKey(KeyEvent e) { carryOutCommand(input.getText().trim()); } }); put(new Integer(SWT.ARROW_UP), new KeyAction() { public void receiveKey(KeyEvent e) { if (currentHistoryLine == commandHistory.size() - 1) commandHistory.set(commandHistory.size() - 1, input.getText().trim()); if (currentHistoryLine > 0) { String previousCommand = commandHistory.get(--currentHistoryLine); input.setText(previousCommand); input.setSelection(previousCommand.length(), previousCommand.length()); } } }); put(new Integer(SWT.ARROW_DOWN), new KeyAction() { public void receiveKey(KeyEvent e) { if (currentHistoryLine < commandHistory.size() - 1) { String nextCommand = commandHistory.get(++currentHistoryLine); input.setText(nextCommand); input.setSelection(nextCommand.length(), nextCommand.length()); } } }); put(new Integer(32), new KeyAction() { // space public void receiveKey(KeyEvent e) { if (e.stateMask == SWT.CTRL) { tabComplete(); } else { e.doit = true; } } }); } }; /** * The constructor. Called by Eclipse reflection when a new console * is created. */ public ScriptingConsoleView() { } /** * This is a callback that will allow us to create the view and * initialize it. */ public void createPartControl(Composite parent) { GridLayout layout = new GridLayout(); layout.numColumns = 1; parent.setLayout(layout); output = new Text(parent, SWT.READ_ONLY | SWT.MULTI | SWT.V_SCROLL | SWT.BORDER | SWT.WRAP); output.setFont(JFaceResources.getTextFont()); GridData outputData = new GridData(GridData.FILL_BOTH); output.setBackground(new Color(parent.getDisplay(), 0xFF, 0xFF, 0xFF)); output.setLayoutData(outputData); output.addKeyListener(new KeyListener() { public void keyPressed(KeyEvent e) { if (e.character != '\0' && e.stateMask == 0) { e.doit = false; input.setText(input.getText() + e.character); input.setSelection(input.getText().length()); input.setFocus(); } else if (actionTable.containsKey(e.keyCode)) { input.setFocus(); handleKey(e); } // "Paste" forwarding. // SWT.MOD1 is Ctrl or Command as appropriate based on the // platform. That funny '&' is a bitop. See the JLS. else if ((Character.toLowerCase(e.character) == 'v' || e.keyCode == 'v') && (e.stateMask & SWT.MOD1) != 0) { input.setFocus(); input.paste(); } else if (Character.toLowerCase(e.character) == 'c' && (e.stateMask & SWT.MOD1) != 0) { // We'll want to let this one pass through, so that // the output can do copying as it should. // Added because of #1076, shk3++. } } public void keyReleased(KeyEvent _) { } }); input = new Text(parent, SWT.MULTI | SWT.BORDER | SWT.V_SCROLL); input.setFont(JFaceResources.getTextFont()); input.addKeyListener(new KeyListener() { public void keyPressed(KeyEvent e) { handleKey(e); if (e.keyCode == SWT.TAB) { e.doit = false; tabComplete(); } } public void keyReleased(KeyEvent _) { } }); input.addTraverseListener(new TraverseListener() { public void keyTraversed(TraverseEvent e) { if (e.detail == SWT.TRAVERSE_TAB_NEXT) { e.doit = false; } } }); GridData inputData = new GridData(GridData.FILL_HORIZONTAL); inputData.heightHint = 20; input.setLayoutData(inputData); hookContextMenu(); enableResourceDropSupport(); } protected void handleKey(KeyEvent e) { if (actionTable.containsKey(e.keyCode)) { e.doit = false; actionTable.get(e.keyCode).receiveKey(e); } } /** Sets up and installs the context menu for the console view. */ private void hookContextMenu() { final Action cutInputAction = new Action("Cut") { public void run() { input.cut(); } }, copyInputAction = new Action("Copy") { public void run() { input.copy(); } }, pasteInputAction = new Action("Paste") { public void run() { input.paste(); } }, copyOutputAction = new Action("Copy") { public void run() { output.copy(); } }; final Action clearAction = new Action("Clear") { public void run() { output.setText(""); } }; cutInputAction.setActionDefinitionId(IWorkbenchActionDefinitionIds.CUT); copyInputAction.setActionDefinitionId(IWorkbenchActionDefinitionIds.COPY); pasteInputAction.setActionDefinitionId(IWorkbenchActionDefinitionIds.PASTE); copyOutputAction.setActionDefinitionId(IWorkbenchActionDefinitionIds.COPY); final MenuManager inputMenuMgr = new MenuManager("#PopupMenu"); inputMenuMgr.setRemoveAllWhenShown(true); inputMenuMgr.addMenuListener(new IMenuListener() { public void menuAboutToShow(IMenuManager mgr) { inputMenuMgr.add(cutInputAction); inputMenuMgr.add(copyInputAction); inputMenuMgr.add(pasteInputAction); } }); input.setMenu(inputMenuMgr.createContextMenu(input)); final MenuManager outputMenuMgr = new MenuManager("#PopupMenu"); outputMenuMgr.setRemoveAllWhenShown(true); outputMenuMgr.addMenuListener(new IMenuListener() { public void menuAboutToShow(IMenuManager mgr) { outputMenuMgr.add(copyOutputAction); outputMenuMgr.add(clearAction); } }); output.setMenu(outputMenuMgr.createContextMenu(output)); } private void enableResourceDropSupport() { int ops = DND.DROP_MOVE; DropTargetListener dropListener = new DropTargetListener() { public void dragEnter(DropTargetEvent event) { }; public void dragOver(DropTargetEvent event) { }; public void dragLeave(DropTargetEvent event) { }; public void dragOperationChanged(DropTargetEvent event) { }; public void dropAccept(DropTargetEvent event) { } public void drop(DropTargetEvent event) { if (event.data == null) { event.detail = DND.DROP_NONE; return; } if (event.data instanceof TreeSelection) { TreeSelection selection = (TreeSelection) event.data; for (Object item : selection.toArray()) { String content = item instanceof IResource ? interceptDroppedString(((IResource) item).getFullPath().toOSString()) : "[O_o]"; // unrecognized content int pos = input.getCaretPosition(); String quote = "\"", beforeCursor = pos > 0 ? input.getText().substring(pos - 1, pos) : "", afterCursor = pos < input.getText().length() ? input.getText().substring(pos, pos + 1) : ""; if (!beforeCursor.equals(quote)) content = quote + content; if (!afterCursor.equals(quote)) content += quote; addAtCursor(content); setFocus(); } } } }; DropTarget inputTarget = new DropTarget(input, ops), outputTarget = new DropTarget(output, ops); inputTarget.setTransfer(new Transfer[] { LocalSelectionTransfer.getTransfer() }); outputTarget.setTransfer(new Transfer[] { LocalSelectionTransfer.getTransfer() }); inputTarget.addDropListener(dropListener); outputTarget.addDropListener(dropListener); } /** * Intercepts a string before it is dropped into the console. * Meant to be overridden by deriving classes. */ protected String interceptDroppedString(String s) { // Fix for Windows, because single backslashes are treated as meta- // characters in js strings. Note that the quadruple backslashes are // needed because '\' is a metacharacter in Java strings as well as // in the regex language. return s.replaceAll("\\\\", "/"); } public String currentCommand() { return input.getText(); } /** * Empties the console of contents. */ public void clearConsole() { synchronized (output) { output.setText(NEWLINE); } } /** * Prints a piece of text to the console. The text ends up before the * active command line. * * @param message the text to be printed */ public void printMessage(String message) { if (message == null) return; // Non-printable characters are removed because people have complained // of seeing them. See http://en.wikipedia.org/wiki/Robustness_Principle // for more information. Also, feel free to add other disturbing non- // printables here. message = message.replaceAll("\u0008", ""); // R has a tendency to output newlines as "\r\n". Bringing those in line // here. If you read the below code and think that it could be // shortened to one call, you're probably not taking R's chunking into // account. message = message.replaceAll("\r", ""); message = message.replaceAll("\n", NEWLINE); final String printme = message; Display.getDefault().asyncExec(new Runnable() { public void run() { synchronized (output) { if (output.isDisposed()) return; output.append(printme); output.setFont(JFaceResources.getTextFont()); output.redraw(); } } }); } /** Makes a system notification sound. */ protected void beep() { Display.getCurrent().beep(); } @Override public void setFocus() { input.setFocus(); } /** * Executes a command in the underlying scripting engine. * * @param command the command to be executed * @return the return value/error message (if any) from the command */ protected abstract String executeCommand(String command); /** * Returns all names of variables and methods contained in a certain * container object, or, if <code>""</code> or <code>null</code> is passed, * in the root container object. This method is meant to be overridden * by deriving classes. * * @param object The container object of interest * @return A list of all the variable names in the container object */ @SuppressWarnings("unchecked") protected List<String> allNamesIn(String object) { return Collections.EMPTY_LIST; } /** * Returns all special commands for this scripting console. A special * command is an out-of-band command given at the start of a command line. * This method is meant to be overridden by deriving classes. * * @param object The container object of interest * @return A list of all the variable names in the container object */ @SuppressWarnings("unchecked") protected List<String> allSpecialCommands() { return Collections.EMPTY_LIST; } /** * Automatically writes to the command line the rest of a variable or * method name. (Names are completed case-insensitively; the case of the * already-written parts doesn't matter.) Beeps if no unique such * completion exists. Gives a list of possible completions if called a * second time. * * This method is meant to implement generic tab-completion and should * generally not need to be overridden in deriving classes. Instead, * override <code>allNamesIn</code> and <code>allSpecialCommands</code>, * which returns the relevant things to tab-complete on. */ protected void tabComplete() { String command = input.getText(); int pos = input.getCaretPosition() - 1; String prefix = eatTermBackwards(command, pos); pos -= prefix.length(); int startOfCompletedWord = pos + 1; List<String> variables = new ArrayList<String>(); String parent = ""; if (pos > 0 && command.charAt(pos) == '.') { if (Character.isLetterOrDigit(command.charAt(pos - 1))) { parent = eatTermBackwards(command, pos - 1, "."); pos -= parent.length() + 1; } else { return; } } else if (pos == -1) { variables.addAll(allSpecialCommands()); } variables.addAll(allNamesIn(parent)); List<String> interestingVariables = tabCompleter.complete(prefix, variables); if (interestingVariables.isEmpty()) { beep(); } else if (interestingVariables.size() > 1 && tabCompleter.secondTime()) { printMessage(NEWLINE + tabCompleter.completions(interestingVariables) + NEWLINE); } else { deleteBackwards(input.getCaretPosition() - startOfCompletedWord); addAtCursor(tabCompleter.commonPrefix(interestingVariables)); if (interestingVariables.size() == 1) addAtCursor(tabCompletionHook(parent, interestingVariables.get(0))); else beep(); } } private String eatTermBackwards(String string, int pos) { return eatTermBackwards(string, pos, ""); } private String eatTermBackwards(String string, int pos, String okChars) { String accTerm = ""; for (char additionalCharacter; pos >= 0 && (Character.isLetterOrDigit(additionalCharacter = string.charAt(pos)) || okChars.contains("" + additionalCharacter)); --pos) accTerm = additionalCharacter + accTerm; return accTerm; } private void deleteBackwards(int length) { int oldPosition = input.getCaretPosition(), newPosition = oldPosition - length; String oldText = input.getText(), before = oldText.substring(0, newPosition), after = oldText.substring(oldPosition); input.setText(before + after); input.setSelection(newPosition); } /** * Outputs extra characters after the actual name of the completed thing. * For managers, this could be a period ("."), because that's what the * user will write herself anyway. For methods, it could be "(", or "()" * if the method has no parameters. * * @param object the thing written before the dot (if any) when completing * @param completedVariable the variable that was just tab-completed * @return any extra characters to be output after the completed name */ protected String tabCompletionHook(String parent, String completedName) { return ""; } /** * Inserts text at the cursor. * * @param newText the text to insert */ protected void addAtCursor(String newText) { int oldPosition = input.getSelection().x; String allText = input.getText(); String textWithNewTextAdded = allText.substring(0, input.getCaretPosition()) + newText + allText.substring(input.getCaretPosition()); input.setText(textWithNewTextAdded); input.setSelection(oldPosition + newText.length()); } /** * Prints the command to the console with an appropriate prefix, * and executes it. Doesn't do anything if the command string is empty. * * @param command the command to be printed and executed */ public void carryOutCommand(String command) { input.setText(""); if ("".equals(command)) return; commandHistory.remove(commandHistory.size() - 1); commandHistory.add(command); commandHistory.add(""); currentHistoryLine = commandHistory.size() - 1; executeCommand(command); } }