/*
* ChunkOutputStream.java
*
* Copyright (C) 2009-16 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.workbench.views.source.editors.text;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.VirtualConsole;
import org.rstudio.core.client.js.JsArrayEx;
import org.rstudio.core.client.widget.FixedRatioWidget;
import org.rstudio.core.client.widget.PreWidget;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.common.debugging.model.UnhandledError;
import org.rstudio.studio.client.common.debugging.ui.ConsoleError;
import org.rstudio.studio.client.rmarkdown.model.NotebookFrameMetadata;
import org.rstudio.studio.client.rmarkdown.model.NotebookHtmlMetadata;
import org.rstudio.studio.client.rmarkdown.model.NotebookPlotMetadata;
import org.rstudio.studio.client.rmarkdown.model.RmdChunkOutputUnit;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.views.source.editors.text.rmd.ChunkOutputUi;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Widget;
public class ChunkOutputStream extends FlowPanel
implements ChunkOutputPresenter,
ConsoleError.Observer
{
public ChunkOutputStream(ChunkOutputPresenter.Host host)
{
this(host, ChunkOutputSize.Default);
}
public ChunkOutputStream(ChunkOutputPresenter.Host host, ChunkOutputSize chunkOutputSize)
{
host_ = host;
chunkOutputSize_ = chunkOutputSize;
metadata_ = new HashMap<Integer, JavaScriptObject>();
if (chunkOutputSize_ == ChunkOutputSize.Full) {
getElement().getStyle().setWidth(100, Unit.PCT);
getElement().getStyle().setProperty("display", "-ms-flexbox");
getElement().getStyle().setProperty("display", "-webkit-flex");
getElement().getStyle().setProperty("display", "flex");
getElement().getStyle().setProperty("msFlexDirection", "column");
getElement().getStyle().setProperty("webkitFlexDirection", "column");
getElement().getStyle().setProperty("flexDirection", "column");
getElement().getStyle().setOverflow(Overflow.AUTO);
}
}
@Override
public void showConsoleText(String text)
{
// flush any queued errors
flushQueuedErrors();
renderConsoleOutput(text, classOfOutput(ChunkConsolePage.CONSOLE_OUTPUT));
}
@Override
public void showConsoleError(String error)
{
// queue the error -- we don't emit errors right away since a more
// detailed error event may be forthcoming
queuedError_ += error;
}
@Override
public void showConsoleOutput(JsArray<JsArrayEx> output)
{
initializeOutput(RmdChunkOutputUnit.TYPE_TEXT);
for (int i = 0; i < output.length(); i++)
{
// the first element is the output, and the second is the text; if we
// don't have at least 2 elements, it's not a valid entry
if (output.get(i).length() < 2)
continue;
int outputType = output.get(i).getInt(0);
String outputText = output.get(i).getString(1);
// we don't currently render input as output
if (outputType == ChunkConsolePage.CONSOLE_INPUT)
continue;
if (outputType == ChunkConsolePage.CONSOLE_ERROR)
{
queuedError_ += outputText;
}
else
{
// release any queued errors
if (!queuedError_.isEmpty())
{
vconsole_.submit(queuedError_, classOfOutput(
ChunkConsolePage.CONSOLE_ERROR));
queuedError_ = "";
}
vconsole_.submit(outputText, classOfOutput(outputType));
}
}
}
@Override
public void showPlotOutput(String url, NotebookPlotMetadata metadata,
int ordinal, final Command onRenderComplete)
{
// flush any queued errors
initializeOutput(RmdChunkOutputUnit.TYPE_PLOT);
flushQueuedErrors();
// persist metadata
metadata_.put(ordinal, metadata);
final ChunkPlotWidget plot = new ChunkPlotWidget(url, metadata,
new Command()
{
@Override
public void execute()
{
onRenderComplete.execute();
onHeightChanged();
}
}, chunkOutputSize_);
// check to see if the given ordinal matches one of the existing
// placeholder elements
boolean placed = false;
for (int i = 0; i < getWidgetCount(); i++)
{
Widget w = getWidget(i);
int ordAttr = getOrdinal(w.getElement());
if (ordAttr == ordinal)
{
// insert the plot widget after the ordinal
plot.getElement().setAttribute(
ORDINAL_ATTRIBUTE, "" + ordinal);
if (i < getWidgetCount() - 1)
insert(plot, i + 1);
else
add(plot);
placed = true;
break;
}
}
// if we haven't placed the plot yet, add it at the end of the output
if (!placed)
addWithOrdinal(plot, ordinal);
}
@Override
public void showHtmlOutput(String url, NotebookHtmlMetadata metadata,
int ordinal, final Command onRenderComplete)
{
// flush any queued errors
initializeOutput(RmdChunkOutputUnit.TYPE_HTML);
flushQueuedErrors();
// persist metadata
metadata_.put(ordinal, metadata);
final boolean knitrFigure = metadata.getSizingPolicyKnitrFigure();
// amend the URL to cause any contained widget to use the RStudio viewer
// sizing policy
if (url.indexOf('?') > 0)
url += "&";
else
url += "?";
if (knitrFigure) {
url += "viewer_pane=1";
}
final ChunkOutputFrame frame = new ChunkOutputFrame();
if (chunkOutputSize_ == ChunkOutputSize.Default) {
if (knitrFigure) {
final FixedRatioWidget fixedFrame = new FixedRatioWidget(frame,
ChunkOutputUi.OUTPUT_ASPECT,
ChunkOutputUi.MAX_HTMLWIDGET_WIDTH);
addWithOrdinal(fixedFrame, ordinal);
}
else {
// reduce size of html widget as much as possible and add scroll,
// once it loads, we will adjust the height appropriately.
frame.getElement().getStyle().setHeight(25, Unit.PX);
frame.getElement().getStyle().setOverflow(Overflow.SCROLL);
frame.getElement().getStyle().setWidth(100, Unit.PCT);
addWithOrdinal(frame, ordinal);
}
}
else if (chunkOutputSize_ == ChunkOutputSize.Full) {
frame.getElement().getStyle().setPosition(Position.ABSOLUTE);
frame.getElement().getStyle().setWidth(100, Unit.PCT);
frame.getElement().getStyle().setHeight(100, Unit.PCT);
addWithOrdinal(frame, ordinal);
}
Element body = frame.getDocument().getBody();
Style bodyStyle = body.getStyle();
bodyStyle.setPadding(0, Unit.PX);
bodyStyle.setMargin(0, Unit.PX);
frame.loadUrlDelayed(url, 250, new Command()
{
@Override
public void execute()
{
onRenderComplete.execute();
if (!knitrFigure) {
int contentHeight = frame.getWindow().getDocument().getBody().getOffsetHeight();
frame.getElement().getStyle().setHeight(contentHeight, Unit.PX);
frame.getElement().getStyle().setOverflow(Overflow.HIDDEN);
frame.getWindow().getDocument().getBody().getStyle().setOverflow(Overflow.HIDDEN);
}
onHeightChanged();
};
});
themeColors_ = ChunkOutputWidget.getEditorColors();
afterRender_ = new Command()
{
@Override
public void execute()
{
if (themeColors_ != null) {
Element body = frame.getDocument().getBody();
Style bodyStyle = body.getStyle();
bodyStyle.setColor(themeColors_.foreground);
}
}
};
frame.runAfterRender(afterRender_);
}
@Override
public void showErrorOutput(UnhandledError err)
{
hasErrors_ = true;
// if there's only one error frame, it's not worth showing dedicated
// error UX
if (err.getErrorFrames() != null &&
err.getErrorFrames().length() < 2)
{
flushQueuedErrors();
return;
}
int idx = queuedError_.indexOf(err.getErrorMessage());
if (idx >= 0)
{
// emit any messages queued prior to the error
if (idx > 0)
{
renderConsoleOutput(queuedError_.substring(0, idx),
classOfOutput(ChunkConsolePage.CONSOLE_ERROR));
initializeOutput(RmdChunkOutputUnit.TYPE_ERROR);
}
// leave messages following the error in the queue
queuedError_ = queuedError_.substring(
idx + err.getErrorMessage().length());
}
else
{
// flush any irrelevant messages from the stream
flushQueuedErrors();
}
UIPrefs prefs = RStudioGinjector.INSTANCE.getUIPrefs();
ConsoleError error = new ConsoleError(err, prefs.getThemeErrorClass(),
this, null);
error.setTracebackVisible(prefs.autoExpandErrorTracebacks().getValue());
add(error);
flushQueuedErrors();
onHeightChanged();
}
@Override
public void showOrdinalOutput(int ordinal)
{
// ordinals are placeholder elements which can be replaced with content
// later
addWithOrdinal(new ChunkOrdinalWidget(), ordinal);
}
@Override
public void showDataOutput(JavaScriptObject data,
NotebookFrameMetadata metadata, int ordinal)
{
metadata_.put(ordinal, metadata);
addWithOrdinal(new ChunkDataWidget(data, chunkOutputSize_), ordinal);
}
@Override
public void onErrorBoxResize()
{
onHeightChanged();
}
@Override
public void runCommandWithDebug(String command)
{
// not implemented (this is is only useful in the console)
}
@Override
public void clearOutput()
{
clear();
if (vconsole_ != null)
vconsole_.clear();
lastOutputType_ = RmdChunkOutputUnit.TYPE_NONE;
}
@Override
public void completeOutput()
{
// flush any remaining queued errors
flushQueuedErrors();
lastOutputType_ = RmdChunkOutputUnit.TYPE_NONE;
}
@Override
public void setPlotPending(boolean pending, String pendingStyle)
{
for (Widget w: this)
{
if (w instanceof FixedRatioWidget &&
((FixedRatioWidget)w).getWidget() instanceof Image)
{
if (pending)
w.addStyleName(pendingStyle);
else
w.removeStyleName(pendingStyle);
}
}
}
@Override
public void updatePlot(String plotUrl, String pendingStyle)
{
for (Widget w: this)
{
if (w instanceof ChunkPlotWidget)
{
// ask the plot to sync this URL (it contains the logic for
// determining whether it matches the URL)
ChunkPlotWidget plot = (ChunkPlotWidget)w;
plot.updateImageUrl(plotUrl, pendingStyle);
}
}
}
@Override
public void onEditorThemeChanged(EditorThemeListener.Colors colors)
{
themeColors_ = colors;
// apply the style to any frames in the output
for (Widget w: this)
{
if (w instanceof ChunkOutputFrame)
{
ChunkOutputFrame frame = (ChunkOutputFrame)w;
frame.runAfterRender(afterRender_);
}
else if (w instanceof FixedRatioWidget)
{
FixedRatioWidget fixedRatioWidget = (FixedRatioWidget)w;
Widget innerWidget = fixedRatioWidget.getWidget();
if (innerWidget instanceof ChunkOutputFrame)
{
ChunkOutputFrame frame = (ChunkOutputFrame)innerWidget;
frame.runAfterRender(afterRender_);
}
}
else if (w instanceof EditorThemeListener)
{
((EditorThemeListener)w).onEditorThemeChanged(colors);
}
}
}
@Override
public boolean hasOutput()
{
return getWidgetCount() > 0;
}
@Override
public boolean hasPlots()
{
for (Widget w: this)
{
if (w instanceof ChunkPlotWidget)
{
return true;
}
}
return false;
}
@Override
public boolean hasErrors()
{
return hasErrors_;
}
@Override
public void onResize()
{
for (Widget w: this)
{
if (w instanceof ChunkDataWidget)
{
ChunkDataWidget widget = (ChunkDataWidget)w;
widget.onResize();
}
}
}
public List<ChunkOutputPage> extractPages()
{
// flush any errors so they are properly accounted for
flushQueuedErrors();
List<ChunkOutputPage> pages = new ArrayList<ChunkOutputPage>();
List<Widget> removed = new ArrayList<Widget>();
for (Widget w: this)
{
// extract ordinal and metadata
JavaScriptObject metadata = null;
String ord = w.getElement().getAttribute(ORDINAL_ATTRIBUTE);
int ordinal = 0;
if (!StringUtil.isNullOrEmpty(ord))
ordinal = StringUtil.parseInt(ord, 0);
if (metadata_.containsKey(ordinal))
metadata = metadata_.get(ordinal);
if (w instanceof ChunkDataWidget)
{
ChunkDataWidget widget = (ChunkDataWidget)w;
ChunkDataPage data = new ChunkDataPage(widget,
(NotebookFrameMetadata)metadata.cast(), ordinal);
pages.add(data);
removed.add(w);
continue;
}
else if (w instanceof ChunkOrdinalWidget)
{
pages.add(new ChunkOrdinalPage(ordinal));
removed.add(w);
continue;
}
// extract the inner element if this is a fixed-ratio widget (or just
// use raw if it's not)
Widget inner = w;
if (w instanceof FixedRatioWidget)
inner = ((FixedRatioWidget)w).getWidget();
if (inner instanceof ChunkPlotWidget)
{
ChunkPlotWidget plot = (ChunkPlotWidget)inner;
ChunkPlotPage page = new ChunkPlotPage(plot.plotUrl(),
plot.getMetadata(), ordinal, null, chunkOutputSize_);
pages.add(page);
removed.add(w);
}
else if (inner instanceof ChunkOutputFrame)
{
ChunkOutputFrame frame = (ChunkOutputFrame)inner;
ChunkHtmlPage html = new ChunkHtmlPage(frame.getUrl(),
(NotebookHtmlMetadata)metadata.cast(), ordinal, null, chunkOutputSize_);
// cancel any pending page load
frame.cancelPendingLoad();
pages.add(html);
removed.add(w);
}
}
for (Widget r: removed)
this.remove(r);
return pages;
}
/**
* Gets the ordinal of the content stream (determined by the first ordinal
* of output).
*
* @return The ordinal of the content stream, or 0 if no ordinal is known
*/
public int getContentOrdinal()
{
for (Widget w: this)
{
// ignore consoles with no content
if (w instanceof PreWidget && w.getElement().getChildCount() == 0)
continue;
// ignore ordinals
if (w instanceof ChunkOrdinalWidget)
continue;
if (w.isVisible() &&
w.getElement().getStyle().getDisplay() != "none")
{
int ord = getOrdinal(w.getElement());
if (ord > 0)
return ord;
return 1;
}
}
return 0;
}
public String getAllConsoleText()
{
String text = "";
for (Widget w: this)
{
if (w instanceof PreWidget)
text += w.getElement().getInnerText();
}
return text;
}
// Private methods ---------------------------------------------------------
private String classOfOutput(int type)
{
if (type == ChunkConsolePage.CONSOLE_ERROR)
return RStudioGinjector.INSTANCE.getUIPrefs().getThemeErrorClass();
else if (type == ChunkConsolePage.CONSOLE_INPUT)
return "ace_keyword";
return null;
}
private void flushQueuedErrors()
{
if (!queuedError_.isEmpty())
{
initializeOutput(RmdChunkOutputUnit.TYPE_TEXT);
renderConsoleOutput(queuedError_, classOfOutput(
ChunkConsolePage.CONSOLE_ERROR));
queuedError_ = "";
}
}
private void addWithOrdinal(Widget w, int ordinal)
{
w.getElement().setAttribute(ORDINAL_ATTRIBUTE, "" + ordinal);
// record max observed ordinal
if (ordinal > maxOrdinal_)
maxOrdinal_ = ordinal;
add(w);
}
private void initializeOutput(int outputType)
{
if (lastOutputType_ == outputType)
return;
if (outputType == RmdChunkOutputUnit.TYPE_TEXT)
{
// if switching to textual output, allocate a new virtual console
initConsole();
}
else if (lastOutputType_ == RmdChunkOutputUnit.TYPE_TEXT)
{
// if switching from textual input, clear the text accumulator
if (vconsole_ != null)
vconsole_.clear();
console_ = null;
}
lastOutputType_ = outputType;
}
private void initConsole()
{
if (console_ == null)
{
console_ = new PreWidget();
console_.getElement().removeAttribute("tabIndex");
console_.getElement().getStyle().setMarginTop(0, Unit.PX);
console_.getElement().getStyle().setProperty("whiteSpace", "pre-wrap");
}
else
{
console_.getElement().setInnerHTML("");
}
if (vconsole_ == null)
vconsole_ = new VirtualConsole(console_.getElement());
else
vconsole_.clear();
// attach the console
addWithOrdinal(console_, maxOrdinal_ + 1);
}
private void renderConsoleOutput(String text, String clazz)
{
initializeOutput(RmdChunkOutputUnit.TYPE_TEXT);
vconsole_.submit(text, clazz);
onHeightChanged();
}
private void onHeightChanged()
{
host_.notifyHeightChanged();
}
private int getOrdinal(Element ele)
{
String ord = ele.getAttribute(ORDINAL_ATTRIBUTE);
if (!StringUtil.isNullOrEmpty(ord))
{
try
{
int ordAttr = Integer.parseInt(ord);
return ordAttr;
}
catch(Exception e)
{
return 0;
}
}
return 0;
}
private final ChunkOutputPresenter.Host host_;
private final Map<Integer, JavaScriptObject> metadata_;
private PreWidget console_;
private String queuedError_ = "";
private VirtualConsole vconsole_;
private int lastOutputType_ = RmdChunkOutputUnit.TYPE_NONE;
private boolean hasErrors_ = false;
private ChunkOutputSize chunkOutputSize_;
private int maxOrdinal_ = 0;
private final static String ORDINAL_ATTRIBUTE = "data-ordinal";
private Command afterRender_;
private Colors themeColors_;
}