Java tutorial
/* * ShellWidget.java * * Copyright (C) 2009-12 by RStudio, Inc. * * Unless you have received this program directly from RStudio pursuant * to the terms of a commercial license agreement with RStudio, then * this program is licensed to you under the terms of version 3 of the * GNU Affero General Public License. This program is distributed WITHOUT * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. * */ package org.rstudio.studio.client.common.shell; import java.util.Map; import java.util.TreeMap; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.RepeatingCommand; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.SpanElement; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Text; import com.google.gwt.event.dom.client.*; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.ui.*; import org.rstudio.core.client.ElementIds; import org.rstudio.core.client.FilePosition; import org.rstudio.core.client.StringUtil; import org.rstudio.core.client.TimeBufferedCommand; import org.rstudio.core.client.VirtualConsole; import org.rstudio.core.client.dom.DomUtils; import org.rstudio.core.client.files.FileSystemItem; import org.rstudio.core.client.jsonrpc.RpcObjectList; import org.rstudio.core.client.widget.BottomScrollPanel; import org.rstudio.core.client.widget.FontSizer; import org.rstudio.core.client.widget.PreWidget; import org.rstudio.studio.client.RStudioGinjector; import org.rstudio.studio.client.application.Desktop; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.common.debugging.model.ErrorFrame; import org.rstudio.studio.client.common.debugging.model.UnhandledError; import org.rstudio.studio.client.common.debugging.ui.ConsoleError; import org.rstudio.studio.client.common.filetypes.FileTypeRegistry; import org.rstudio.studio.client.common.filetypes.events.OpenSourceFileEvent; import org.rstudio.studio.client.common.filetypes.events.OpenSourceFileEvent.NavigationMethod; import org.rstudio.studio.client.workbench.model.ConsoleAction; import org.rstudio.studio.client.workbench.views.console.ConsoleResources; import org.rstudio.studio.client.workbench.views.console.events.RunCommandWithDebugEvent; import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorDisplay; import org.rstudio.studio.client.workbench.views.source.editors.text.AceEditor; import org.rstudio.studio.client.workbench.views.source.editors.text.AceEditor.NewLineMode; import org.rstudio.studio.client.workbench.views.source.editors.text.events.CursorChangedEvent; import org.rstudio.studio.client.workbench.views.source.editors.text.events.CursorChangedHandler; import org.rstudio.studio.client.workbench.views.source.editors.text.events.PasteEvent; public class ShellWidget extends Composite implements ShellDisplay, RequiresResize, ConsoleError.Observer { public ShellWidget(AceEditor editor, EventBus events) { styles_ = ConsoleResources.INSTANCE.consoleStyles(); events_ = events; SelectInputClickHandler secondaryInputHandler = new SelectInputClickHandler(); output_ = new PreWidget(); output_.setStylePrimaryName(styles_.output()); output_.addClickHandler(secondaryInputHandler); ElementIds.assignElementId(output_.getElement(), ElementIds.CONSOLE_OUTPUT); output_.addPasteHandler(secondaryInputHandler); pendingInput_ = new PreWidget(); pendingInput_.setStyleName(styles_.output()); pendingInput_.addClickHandler(secondaryInputHandler); prompt_ = new HTML(); prompt_.setStylePrimaryName(styles_.prompt()); prompt_.addStyleName(KEYWORD_CLASS_NAME); input_ = editor; input_.setShowLineNumbers(false); input_.setShowPrintMargin(false); if (!Desktop.isDesktop()) input_.setNewLineMode(NewLineMode.Unix); input_.setUseWrapMode(true); input_.setPadding(0); input_.autoHeight(); final Widget inputWidget = input_.asWidget(); ElementIds.assignElementId(inputWidget.getElement(), ElementIds.CONSOLE_INPUT); input_.addClickHandler(secondaryInputHandler); inputWidget.addStyleName(styles_.input()); input_.addCursorChangedHandler(new CursorChangedHandler() { public void onCursorChanged(CursorChangedEvent event) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { input_.scrollToCursor(scrollPanel_, 8, 60); } }); } }); input_.addCapturingKeyDownHandler(new KeyDownHandler() { @Override public void onKeyDown(KeyDownEvent event) { // If the user hits Page-Up from inside the console input, we need // to simulate pageup because focus is not contained in the scroll // panel (it's in the hidden textarea that Ace uses under the // covers). int keyCode = event.getNativeKeyCode(); switch (keyCode) { case KeyCodes.KEY_PAGEUP: event.stopPropagation(); event.preventDefault(); // Can't scroll any further up. Return before we change focus. if (scrollPanel_.getVerticalScrollPosition() == 0) return; scrollPanel_.focus(); int newScrollTop = scrollPanel_.getVerticalScrollPosition() - scrollPanel_.getOffsetHeight() + 40; scrollPanel_.setVerticalScrollPosition(Math.max(0, newScrollTop)); break; } } }); inputLine_ = new DockPanel(); inputLine_.setHorizontalAlignment(DockPanel.ALIGN_LEFT); inputLine_.setVerticalAlignment(DockPanel.ALIGN_TOP); inputLine_.add(prompt_, DockPanel.WEST); inputLine_.setCellWidth(prompt_, "1"); inputLine_.add(input_.asWidget(), DockPanel.CENTER); inputLine_.setCellWidth(input_.asWidget(), "100%"); inputLine_.setWidth("100%"); verticalPanel_ = new VerticalPanel(); verticalPanel_.setStylePrimaryName(styles_.console()); verticalPanel_.addStyleName("ace_text-layer"); verticalPanel_.addStyleName("ace_line"); FontSizer.applyNormalFontSize(verticalPanel_); verticalPanel_.add(output_); verticalPanel_.add(pendingInput_); verticalPanel_.add(inputLine_); verticalPanel_.setWidth("100%"); scrollPanel_ = new ClickableScrollPanel(); scrollPanel_.setWidget(verticalPanel_); scrollPanel_.addStyleName("ace_editor"); scrollPanel_.addStyleName("ace_scroller"); scrollPanel_.addClickHandler(secondaryInputHandler); scrollPanel_.addKeyDownHandler(secondaryInputHandler); secondaryInputHandler.setInput(editor); scrollToBottomCommand_ = new TimeBufferedCommand(5) { @Override protected void performAction(boolean shouldSchedulePassive) { if (!DomUtils.selectionExists()) scrollPanel_.scrollToBottom(); } }; initWidget(scrollPanel_); addCopyHook(getElement()); } private native void addCopyHook(Element element) /*-{ if ($wnd.desktop) { var clean = function() { setTimeout(function() { $wnd.desktop.cleanClipboard(true); }, 100) }; element.addEventListener("copy", clean, true); element.addEventListener("cut", clean, true); } }-*/; public void scrollToBottom() { scrollPanel_.scrollToBottom(); } private boolean initialized_ = false; @Override protected void onLoad() { super.onLoad(); if (!initialized_) { initialized_ = true; Scheduler.get().scheduleDeferred(new ScheduledCommand() { public void execute() { doOnLoad(); scrollPanel_.scrollToBottom(); } }); } ElementIds.assignElementId(this.getElement(), ElementIds.SHELL_WIDGET); } protected void doOnLoad() { input_.autoHeight(); // Console scroll pos jumps on first typing without this, because the // textarea is in the upper left corner of the screen and when focus // moves to it scrolling ensues. input_.forceCursorChange(); } public void setSuppressPendingInput(boolean suppressPendingInput) { suppressPendingInput_ = suppressPendingInput; } public void consoleWriteError(final String error) { clearPendingInput(); output(error, getErrorClass(), false); // Pick up the last element emitted to the console. If we get extended // information for this error, we'll need to swap out the simple error // element for the extended error element. Element outputElement = output_.getElement(); Node errorNode = outputElement.getChild(outputElement.getChildCount() - 1); if (clearErrors_) { errorNodes_.clear(); clearErrors_ = false; } errorNodes_.put(error, errorNode); } public void consoleWriteExtendedError(final String error, UnhandledError traceInfo, boolean expand, String command) { if (errorNodes_.containsKey(error)) { Node errorNode = errorNodes_.get(error); clearPendingInput(); ConsoleError errorWidget = new ConsoleError(traceInfo, getErrorClass(), this, command); if (expand) errorWidget.setTracebackVisible(true); // The widget must be added to the root panel to have its event handlers // wired properly, but this isn't an ideal structure; consider showing // console output as cell widgets in a virtualized scrolling CellTable // so we can easily add arbitrary controls. RootPanel.get().add(errorWidget); output_.getElement().replaceChild(errorWidget.getElement(), errorNode); scrollPanel_.onContentSizeChanged(); errorNodes_.remove(error); } } @Override public void showSourceForFrame(ErrorFrame frame) { if (events_ == null) return; FileSystemItem sourceFile = FileSystemItem.createFile(frame.getFileName()); events_.fireEvent(new OpenSourceFileEvent(sourceFile, FilePosition.create(frame.getLineNumber(), frame.getCharacterNumber()), FileTypeRegistry.R, NavigationMethod.HighlightLine)); } @Override public void runCommandWithDebug(String command) { events_.fireEvent(new RunCommandWithDebugEvent(command)); } public void consoleWriteOutput(final String output) { clearPendingInput(); output(output, styles_.output(), false); } public void consoleWriteInput(final String input) { clearPendingInput(); output(input, styles_.command() + KEYWORD_CLASS_NAME, false); } private void clearPendingInput() { pendingInput_.setText(""); pendingInput_.setVisible(false); } public void consoleWritePrompt(final String prompt) { output(prompt, styles_.prompt() + KEYWORD_CLASS_NAME, false); clearErrors_ = true; } public void consolePrompt(String prompt, boolean showInput) { if (prompt != null) prompt = VirtualConsole.consolify(prompt); prompt_.getElement().setInnerText(prompt); //input_.clear() ; ensureInputVisible(); // Deal gracefully with multi-line prompts int promptLines = StringUtil.notNull(prompt).split("\\n").length; input_.asWidget().getElement().getStyle().setPaddingTop((promptLines - 1) * 15, Unit.PX); input_.setPasswordMode(!showInput); clearErrors_ = true; } public void ensureInputVisible() { scrollPanel_.scrollToBottom(); } private String getErrorClass() { return styles_.error() + " " + RStudioGinjector.INSTANCE.getUIPrefs().getThemeErrorClass(); } private boolean output(String text, String className, boolean addToTop) { if (text.indexOf('\f') >= 0) clearOutput(); Node node; boolean isOutput = StringUtil.isNullOrEmpty(className) || className.equals(styles_.output()); if (isOutput && !addToTop && trailingOutput_ != null) { // Short-circuit the case where we're appending output to the // bottom, and there's already some output there. We need to // treat this differently in case the new output uses control // characters to pound over parts of the previous output. int oldLineCount = DomUtils.countLines(trailingOutput_, true); trailingOutputConsole_.submit(text); trailingOutput_.setNodeValue(ensureNewLine(trailingOutputConsole_.toString())); int newLineCount = DomUtils.countLines(trailingOutput_, true); lines_ += newLineCount - oldLineCount; } else { Element outEl = output_.getElement(); text = VirtualConsole.consolify(text); if (isOutput) { VirtualConsole console = new VirtualConsole(); console.submit(text); String consoleSnapshot = console.toString(); // We use ensureNewLine to make sure that even if output // doesn't end with \n, a prompt will appear on its own line. // However, if we call ensureNewLine indiscriminantly (i.e. // on an output that's going to be followed by another output) // we can end up inserting newlines where they don't belong. // // It's safe to add a newline when we're appending output to // the end of the console, because if the next append is also // output, we'll use the contents of VirtualConsole and the // newline we add here will be plowed over. // // If we're prepending output to the top of the console, then // it's safe to add a newline if the next chunk (which is already // there) is something besides output. if (!addToTop || (!outEl.hasChildNodes() || outEl.getFirstChild().getNodeType() != Node.TEXT_NODE)) { consoleSnapshot = ensureNewLine(consoleSnapshot); } node = Document.get().createTextNode(consoleSnapshot); if (!addToTop) { trailingOutput_ = (Text) node; trailingOutputConsole_ = console; } } else { SpanElement span = Document.get().createSpanElement(); span.setClassName(className); span.setInnerText(text); node = span; if (!addToTop) { trailingOutput_ = null; trailingOutputConsole_ = null; } } if (addToTop) outEl.insertFirst(node); else outEl.appendChild(node); lines_ += DomUtils.countLines(node, true); } boolean result = !trimExcess(); scrollPanel_.onContentSizeChanged(); if (scrollPanel_.isScrolledToBottom()) scrollToBottomCommand_.nudge(); return result; } private String ensureNewLine(String s) { if (s.length() == 0 || s.charAt(s.length() - 1) == '\n') return s; else return s + '\n'; } private boolean trimExcess() { if (maxLines_ <= 0) return false; // No limit in effect int linesToTrim = lines_ - maxLines_; if (linesToTrim > 0) { lines_ -= DomUtils.trimLines(output_.getElement(), lines_ - maxLines_); return true; } return false; } public void playbackActions(final RpcObjectList<ConsoleAction> actions) { Scheduler.get().scheduleIncremental(new RepeatingCommand() { private int i = actions.length() - 1; private int chunksize = 1000; public boolean execute() { int end = i - chunksize; chunksize = 10; for (; i > end && i >= 0; i--) { // User hit Ctrl+L at some point--we're done. if (cleared_) return false; boolean canContinue = false; ConsoleAction action = actions.get(i); switch (action.getType()) { case ConsoleAction.INPUT: canContinue = output(action.getData() + "\n", styles_.command() + " " + KEYWORD_CLASS_NAME, true); break; case ConsoleAction.OUTPUT: canContinue = output(action.getData(), styles_.output(), true); break; case ConsoleAction.ERROR: canContinue = output(action.getData(), styles_.error(), true); break; case ConsoleAction.PROMPT: canContinue = output(action.getData(), styles_.prompt() + " " + KEYWORD_CLASS_NAME, true); break; } if (!canContinue) return false; } if (!DomUtils.selectionExists()) scrollPanel_.scrollToBottom(); return i >= 0; } }); } public void focus() { input_.setFocus(true); } /** * Directs focus/selection to the input box when a (different) widget * is clicked. */ private class SelectInputClickHandler implements ClickHandler, KeyDownHandler, PasteEvent.Handler { public void onClick(ClickEvent event) { // If clicking on the input panel already, stop propagation. if (event.getSource() == input_) { event.stopPropagation(); return; } // Don't drive focus to the input unless there is no selection. // Otherwise it would interfere with the ability to select stuff // from the output buffer for copying to the clipboard. if (!DomUtils.selectionExists() && isInputOnscreen()) input_.setFocus(true); } public void onKeyDown(KeyDownEvent event) { if (event.getSource() == input_) return; // Filter out some keystrokes you might reasonably expect to keep // focus inside the output pane switch (event.getNativeKeyCode()) { case KeyCodes.KEY_PAGEDOWN: case KeyCodes.KEY_PAGEUP: case KeyCodes.KEY_HOME: case KeyCodes.KEY_END: case KeyCodes.KEY_CTRL: case KeyCodes.KEY_ALT: case KeyCodes.KEY_SHIFT: case 224: // META (Command) on Firefox/Mac return; case 91: case 93: // Left/Right META (Command), but also [ and ], on Safari if (event.isMetaKeyDown()) return; break; case 'C': if (event.isControlKeyDown() || event.isMetaKeyDown()) return; break; } input_.setFocus(true); delegateEvent(input_.asWidget(), event); } public void onPaste(PasteEvent event) { // When pasting, focus the input so it'll receive the pasted text input_.setFocus(true); } public void setInput(AceEditor input) { input_ = input; } private AceEditor input_; } private boolean isInputOnscreen() { return DomUtils.isVisibleVert(scrollPanel_.getElement(), inputLine_.getElement()); } protected class ClickableScrollPanel extends BottomScrollPanel { private ClickableScrollPanel() { super(); getElement().setTabIndex(-1); } public HandlerRegistration addClickHandler(ClickHandler handler) { return addDomHandler(handler, ClickEvent.getType()); } public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { return addDomHandler(handler, KeyDownEvent.getType()); } public void focus() { getElement().focus(); } } public void clearOutput() { output_.setText(""); lines_ = 0; cleared_ = true; trailingOutput_ = null; trailingOutputConsole_ = null; } public InputEditorDisplay getInputEditorDisplay() { return input_; } public String processCommandEntry() { // parse out the command text String promptText = prompt_.getElement().getInnerText(); String commandText = input_.getCode(); input_.setText(""); // Force render to avoid subtle command movement in the console, caused // by the prompt disappearing before the input line does input_.forceImmediateRender(); prompt_.setHTML(""); SpanElement pendingPrompt = Document.get().createSpanElement(); pendingPrompt.setInnerText(promptText); pendingPrompt.setClassName(styles_.prompt() + " " + KEYWORD_CLASS_NAME); if (!suppressPendingInput_ && !input_.isPasswordMode()) { SpanElement pendingInput = Document.get().createSpanElement(); String[] lines = StringUtil.notNull(commandText).split("\n"); String firstLine = lines.length > 0 ? lines[0] : ""; pendingInput.setInnerText(firstLine + "\n"); pendingInput.setClassName(styles_.command() + " " + KEYWORD_CLASS_NAME); pendingInput_.getElement().appendChild(pendingPrompt); pendingInput_.getElement().appendChild(pendingInput); pendingInput_.setVisible(true); } ensureInputVisible(); return commandText; } public HandlerRegistration addCapturingKeyDownHandler(KeyDownHandler handler) { return input_.addCapturingKeyDownHandler(handler); } public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { return input_.addKeyPressHandler(handler); } public int getCharacterWidth() { // create width checker label and add it to the root panel Label widthChecker = new Label(); widthChecker.setStylePrimaryName(styles_.console()); FontSizer.applyNormalFontSize(widthChecker); RootPanel.get().add(widthChecker, -1000, -1000); // put the text into the label, measure it, and remove it String text = new String("abcdefghijklmnopqrstuvwzyz0123456789"); widthChecker.setText(text); int labelWidth = widthChecker.getOffsetWidth(); RootPanel.get().remove(widthChecker); // compute the points per character int pointsPerCharacter = labelWidth / text.length(); // compute client width int clientWidth = getElement().getClientWidth(); int offsetWidth = getOffsetWidth(); if (clientWidth == offsetWidth) { // if the two widths are the same then there are no scrollbars. // however, we know there will eventually be a scrollbar so we // should offset by an estimated amount // (is there a more accurate way to estimate this?) final int ESTIMATED_SCROLLBAR_WIDTH = 19; clientWidth -= ESTIMATED_SCROLLBAR_WIDTH; } // compute character width (add pad so characters aren't flush to right) final int RIGHT_CHARACTER_PAD = 2; int width = (clientWidth / pointsPerCharacter) - RIGHT_CHARACTER_PAD; // enforce a minimum width final int MINIMUM_WIDTH = 30; return Math.max(width, MINIMUM_WIDTH); } public boolean isPromptEmpty() { return StringUtil.isNullOrEmpty(prompt_.getText()); } public String getPromptText() { return StringUtil.notNull(prompt_.getText()); } public void setReadOnly(boolean readOnly) { input_.setReadOnly(readOnly); } public int getMaxOutputLines() { return maxLines_; } public void setMaxOutputLines(int maxLines) { maxLines_ = maxLines; trimExcess(); } @Override public Widget getShellWidget() { return this; } public void onResize() { if (getWidget() instanceof RequiresResize) ((RequiresResize) getWidget()).onResize(); } @Override public void onErrorBoxResize() { scrollPanel_.onContentSizeChanged(); } private int lines_ = 0; private int maxLines_ = -1; private boolean cleared_ = false; private final PreWidget output_; private PreWidget pendingInput_; // Save a reference to the most recent output text node in case the // next bit of output contains \b or \r control characters private Text trailingOutput_; private VirtualConsole trailingOutputConsole_; private final HTML prompt_; protected final AceEditor input_; private final DockPanel inputLine_; private final VerticalPanel verticalPanel_; protected final ClickableScrollPanel scrollPanel_; private ConsoleResources.ConsoleStyles styles_; private final TimeBufferedCommand scrollToBottomCommand_; private boolean suppressPendingInput_; private final EventBus events_; // A list of errors that have occurred between console prompts. private Map<String, Node> errorNodes_ = new TreeMap<String, Node>(); private boolean clearErrors_ = false; private static final String KEYWORD_CLASS_NAME = ConsoleResources.KEYWORD_CLASS_NAME; }