/* * ChunkOutputWidget.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.Collections; import java.util.Comparator; import java.util.List; import org.rstudio.core.client.ColorUtil; import org.rstudio.core.client.CommandWithArg; import org.rstudio.core.client.Size; import org.rstudio.core.client.dom.DomUtils; import org.rstudio.core.client.widget.ProgressSpinner; import org.rstudio.studio.client.RStudioGinjector; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.application.events.InterruptStatusEvent; import org.rstudio.studio.client.application.events.RestartStatusEvent; import org.rstudio.studio.client.common.Value; 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.NotebookQueueUnit; import org.rstudio.studio.client.rmarkdown.model.RmdChunkOptions; import org.rstudio.studio.client.rmarkdown.model.RmdChunkOutput; import org.rstudio.studio.client.rmarkdown.model.RmdChunkOutputUnit; import org.rstudio.studio.client.server.ServerError; import org.rstudio.studio.client.workbench.views.console.events.ConsoleWriteErrorEvent; import org.rstudio.studio.client.workbench.views.console.events.ConsoleWriteErrorHandler; import org.rstudio.studio.client.workbench.views.console.events.ConsoleWriteOutputEvent; import org.rstudio.studio.client.workbench.views.console.events.ConsoleWriteOutputHandler; import org.rstudio.studio.client.workbench.views.source.editors.text.rmd.ChunkOutputHost; import org.rstudio.studio.client.workbench.views.source.editors.text.rmd.ChunkOutputUi; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.JsArrayString; 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.Unit; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.EventListener; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; public class ChunkOutputWidget extends Composite implements ConsoleWriteOutputHandler, ConsoleWriteErrorHandler, RestartStatusEvent.Handler, InterruptStatusEvent.Handler, ChunkOutputPresenter.Host { private static ChunkOutputWidgetUiBinder uiBinder = GWT .create(ChunkOutputWidgetUiBinder.class); interface ChunkOutputWidgetUiBinder extends UiBinder<Widget, ChunkOutputWidget> { } public interface Resources extends ClientBundle { @Source("ExpandChunkIcon_2x.png") ImageResource expandChunkIcon2x(); @Source("CollapseChunkIcon_2x.png") ImageResource collapseChunkIcon2x(); @Source("RemoveChunkIcon_2x.png") ImageResource removeChunkIcon2x(); @Source("PopoutChunkIcon_2x.png") ImageResource popoutIcon2x(); } public interface ChunkStyle extends CssResource { String overflowY(); String collapsed(); String spinner(); String pendingResize(); String fullsize(); String baresize(); String noclear(); } public ChunkOutputWidget(String documentId, String chunkId, RmdChunkOptions options, int expansionState, boolean canClose, ChunkOutputHost host, ChunkOutputSize chunkOutputSize) { documentId_ = documentId; chunkId_ = chunkId; host_ = host; options_ = options; chunkOutputSize_ = chunkOutputSize; initWidget(uiBinder.createAndBindUi(this)); expansionState_ = new Value<Integer>(expansionState); applyCachedEditorStyle(); if (expansionState_.getValue() == COLLAPSED) setCollapsedStyles(); ChunkDataWidget.injectPagedTableResources(); if (chunkOutputSize_ == ChunkOutputSize.Default) { frame_.getElement().getStyle().setHeight( expansionState_.getValue() == COLLAPSED ? ChunkOutputUi.CHUNK_COLLAPSED_HEIGHT : ChunkOutputUi.MIN_CHUNK_HEIGHT, Unit.PX); } else if (chunkOutputSize_ == ChunkOutputSize.Full) { addStyleName(style.fullsize()); } else if (chunkOutputSize_ == ChunkOutputSize.Bare) { addStyleName(style.baresize()); } if (!canClose) addStyleName(style.noclear()); // create the initial output stream and attach it to the frame attachPresenter(new ChunkOutputStream(this, chunkOutputSize_)); DOM.sinkEvents(clear_.getElement(), Event.ONCLICK); DOM.setEventListener(clear_.getElement(), new EventListener() { @Override public void onBrowserEvent(Event evt) { switch(DOM.eventGetType(evt)) { case Event.ONCLICK: host_.onOutputRemoved(ChunkOutputWidget.this); break; }; } }); EventListener toggleExpansion = new EventListener() { @Override public void onBrowserEvent(Event evt) { switch(DOM.eventGetType(evt)) { case Event.ONCLICK: toggleExpansionState(true); break; }; } }; EventListener popoutChunkEvent = new EventListener() { @Override public void onBrowserEvent(Event evt) { switch(DOM.eventGetType(evt)) { case Event.ONCLICK: popoutChunk(); break; }; } }; DOM.sinkEvents(expander_.getElement(), Event.ONCLICK); DOM.setEventListener(expander_.getElement(), toggleExpansion); DOM.sinkEvents(expand_.getElement(), Event.ONCLICK); DOM.setEventListener(expand_.getElement(), toggleExpansion); DOM.sinkEvents(popout_.getElement(), Event.ONCLICK); DOM.setEventListener(popout_.getElement(), popoutChunkEvent); EventBus events = RStudioGinjector.INSTANCE.getEventBus(); events.addHandler(RestartStatusEvent.TYPE, this); events.addHandler(InterruptStatusEvent.TYPE, this); chunkWindowManager_ = RStudioGinjector.INSTANCE.getChunkWindowManager(); } // Public methods ---------------------------------------------------------- public int getExpansionState() { return expansionState_.getValue(); } public void setExpansionState(int state) { setExpansionState(state, null); } public void setExpansionState(int state, CommandWithArg<Boolean> onTransitionCompleted) { if (state == expansionState_.getValue()) { if (onTransitionCompleted != null) onTransitionCompleted.execute(false); return; } toggleExpansionState(false, onTransitionCompleted); } public int getState() { return state_; } public void setOptions(RmdChunkOptions options) { boolean needsSync = options_.include() != options.include(); options_ = options; if (needsSync) syncHeight(false, false); } public HandlerRegistration addExpansionStateChangeHandler( ValueChangeHandler<Integer> handler) { return expansionState_.addValueChangeHandler(handler); } public void showChunkOutput(RmdChunkOutput output, int mode, int scope, boolean complete, boolean ensureVisible) { if (output.getType() == RmdChunkOutput.TYPE_MULTIPLE_UNIT) { JsArray<RmdChunkOutputUnit> units = output.getUnits(); // prepare chunk for output on replay if (output.isReplay() && state_ == CHUNK_EMPTY && units.length() > 0) state_ = CHUNK_PRE_OUTPUT; // loop over the output units and emit the appropriate contents for // each for (int i = 0; i < units.length(); i++) { showChunkOutputUnit(units.get(i), mode, output.isReplay(), ensureVisible); } // if complete, wrap everything up; if not (could happen for partial // replay) just sync up the height if (complete) onOutputFinished(ensureVisible, scope); else syncHeight(true, ensureVisible); } else if (output.getType() == RmdChunkOutput.TYPE_SINGLE_UNIT) { showChunkOutputUnit(output.getUnit(), mode, output.isReplay(), ensureVisible); } } @Override public void notifyHeightChanged() { syncHeight(true, false); } public void syncHeight(final boolean scrollToBottom, final boolean ensureVisible) { // special behavior for chunks which don't have output included by // default: hide unless chunk includes errors or is not being run as // a unit if (!options_.include() && !hasErrors_ && execScope_ == NotebookQueueUnit.EXEC_SCOPE_CHUNK) { if (isVisible()) { setVisible(false); host_.onOutputHeightChanged(this, 0, ensureVisible); } return; } // don't sync if not visible and no output yet if (!isVisible() && (state_ == CHUNK_EMPTY || state_ == CHUNK_PRE_OUTPUT)) return; setVisible(true); // clamp chunk height to min/max (the +19 is the sum of the vertical // padding on the element) int height = ChunkOutputUi.CHUNK_COLLAPSED_HEIGHT; if (expansionState_.getValue() == EXPANDED) { int contentHeight = root_.getElement().getOffsetHeight() + 19; height = Math.max(ChunkOutputUi.MIN_CHUNK_HEIGHT, contentHeight); // if we have renders pending, don't shrink until they're loaded if (pendingRenders_ > 0 && height < renderedHeight_) return; } // don't report height if it hasn't changed (unless we also need to ensure // visibility) if (height == renderedHeight_ && !ensureVisible) return; // cache last reported render size renderedHeight_ = height; if (scrollToBottom) root_.getElement().setScrollTop(root_.getElement().getScrollHeight()); if (chunkOutputSize_ != ChunkOutputSize.Full) frame_.getElement().getStyle().setHeight(height, Unit.PX); // allocate some extra space so the cursor doesn't touch the output frame host_.onOutputHeightChanged(this, height + 7, ensureVisible); } public static boolean isEditorStyleCached() { return s_colors != null; } public static EditorThemeListener.Colors getEditorColors() { return s_colors; } public void onOutputFinished(boolean ensureVisible, int execScope) { presenter_.completeOutput(); if (state_ != CHUNK_PRE_OUTPUT) { // if we got some output, synchronize the chunk's height to accommodate // it syncHeight(true, ensureVisible); } else if (execScope == NotebookQueueUnit.EXEC_SCOPE_CHUNK) { // if executing the whole chunk but no output was received, clean up // any prior output and hide the output presenter_.clearOutput(); renderedHeight_ = 0; setVisible(false); host_.onOutputHeightChanged(this, 0, ensureVisible); } state_ = presenter_.hasOutput() ? CHUNK_READY : CHUNK_EMPTY; setOverflowStyle(); showReadyState(); unregisterConsoleEvents(); } public void setCodeExecuting(int mode, int scope) { // expand if currently collapsed if (expansionState_.getValue() == COLLAPSED) toggleExpansionState(false); // do nothing if code is already executing if (state_ == CHUNK_PRE_OUTPUT || state_ == CHUNK_POST_OUTPUT) { return; } // clean error state hasErrors_ = false; // if we already had output, clear it if (state_ == CHUNK_READY) { presenter_.clearOutput(); attachPresenter(new ChunkOutputStream(this, chunkOutputSize_)); } registerConsoleEvents(); state_ = CHUNK_PRE_OUTPUT; execScope_ = scope; showBusyState(); } public static void cacheEditorStyle( String foregroundColor, String backgroundColor, String aceEditorColor) { // use a muted version of the text color for the outline ColorUtil.RGBColor text = ColorUtil.RGBColor.fromCss(aceEditorColor); // dark themes require a slightly more pronounced color ColorUtil.RGBColor outline = new ColorUtil.RGBColor( text.red(), text.green(), text.blue(), text.isDark() ? 0.12: 0.18); String border = outline.asRgb(); // highlight color used in data chunks ColorUtil.RGBColor highlight = new ColorUtil.RGBColor( text.red(), text.green(), text.blue(), 0.02); // synthesize a surface color by blending the keyword color with the // background JsArrayString classes = JsArrayString.createArray().cast(); classes.push("ace_editor"); classes.push("ace_keyword"); ColorUtil.RGBColor surface = ColorUtil.RGBColor.fromCss( DomUtils.extractCssValue(classes, "color")); surface = surface.mixedWith( ColorUtil.RGBColor.fromCss(backgroundColor), 0.02, 1); s_colors = new EditorThemeListener.Colors(foregroundColor, backgroundColor, border, highlight.asRgb(), surface.asRgb()); } public void showServerError(ServerError error) { // consider: less obtrusive error message RStudioGinjector.INSTANCE.getGlobalDisplay().showErrorMessage( "Chunk Execution Error", error.getMessage()); // treat as an interrupt (don't clear output) completeInterrupt(); } public void applyCachedEditorStyle() { if (!isEditorStyleCached()) return; Style frameStyle = frame_.getElement().getStyle(); frameStyle.setBorderColor(s_colors.border); getElement().getStyle().setBackgroundColor(s_colors.background); frame_.getElement().getStyle().setBackgroundColor(s_colors.background); if (presenter_ != null) presenter_.onEditorThemeChanged(s_colors); } public boolean hasErrors() { return hasErrors_; } public boolean hasPlots() { return presenter_.hasPlots(); } public void updatePlot(String url) { presenter_.updatePlot(url, style.pendingResize()); } public void setPlotPending(boolean pending) { presenter_.setPlotPending(pending, style.pendingResize()); } public void setHost(ChunkOutputHost host) { host_ = host; } public void onResize() { presenter_.onResize(); } // Event handlers ---------------------------------------------------------- @Override public void onConsoleWriteOutput(ConsoleWriteOutputEvent event) { if (event.getConsole() != chunkId_) return; initializeOutput(RmdChunkOutputUnit.TYPE_TEXT); presenter_.showConsoleText(event.getOutput()); } @Override public void onConsoleWriteError(ConsoleWriteErrorEvent event) { if (event.getConsole() != chunkId_) return; initializeOutput(RmdChunkOutputUnit.TYPE_TEXT); presenter_.showConsoleError(event.getError()); } @Override public void onRestartStatus(RestartStatusEvent event) { if (event.getStatus() != RestartStatusEvent.RESTART_COMPLETED) return; // when R is restarted, we're not going to get any more output, so act // as though the server told us it's done if (state_ != CHUNK_READY) { onOutputFinished(false, NotebookQueueUnit.EXEC_SCOPE_PARTIAL); } } @Override public void onInterruptStatus(InterruptStatusEvent event) { if (event.getStatus() != InterruptStatusEvent.INTERRUPT_COMPLETED) return; completeInterrupt(); } public void setRootWidget(Widget widget) { root_.setWidget(widget); } public void hideSatellitePopup() { popout_.setVisible(false); hideSatellitePopup_ = true; } public HTMLPanel getFrame() { return frame_; } // Private methods --------------------------------------------------------- private void showChunkOutputUnit(RmdChunkOutputUnit unit, int mode, boolean replay, boolean ensureVisible) { // no-op for empty console objects (avoid initializing output when we have // nothing to show) if (unit.getType() == RmdChunkOutputUnit.TYPE_TEXT && unit.getArray().length() < 1) return; // prepare for rendering this output type (except for ordinals, which // are not visible) if (unit.getType() != RmdChunkOutputUnit.TYPE_ORDINAL) initializeOutput(unit.getType()); switch(unit.getType()) { case RmdChunkOutputUnit.TYPE_TEXT: presenter_.showConsoleOutput(unit.getArray()); break; case RmdChunkOutputUnit.TYPE_HTML: final RenderTimer widgetTimer = new RenderTimer(); presenter_.showHtmlOutput(unit.getString(), (NotebookHtmlMetadata)unit.getMetadata().cast(), unit.getOrdinal(), new Command() { @Override public void execute() { widgetTimer.cancel(); } }); break; case RmdChunkOutputUnit.TYPE_PLOT: final RenderTimer plotTimer = new RenderTimer(); presenter_.showPlotOutput(unit.getString(), (NotebookPlotMetadata)unit.getMetadata().cast(), unit.getOrdinal(), new Command() { @Override public void execute() { plotTimer.cancel(); } }); break; case RmdChunkOutputUnit.TYPE_ERROR: // override visibility flag when there's an error in batch mode if (!replay && !options_.error() && mode == NotebookQueueUnit.EXEC_MODE_BATCH) ensureVisible = true; hasErrors_ = true; presenter_.showErrorOutput(unit.getUnhandledError()); break; case RmdChunkOutputUnit.TYPE_ORDINAL: // used to reserve a plot placeholder presenter_.showOrdinalOutput(unit.getOrdinal()); break; case RmdChunkOutputUnit.TYPE_DATA: presenter_.showDataOutput(unit.getOuputObject(), (NotebookFrameMetadata)unit.getMetadata().cast(), unit.getOrdinal()); break; } } private class RenderTimer extends Timer { public RenderTimer() { pendingRenders_++; // ensure we decrement the counter eventually even if content never // renders schedule(15000); } @Override public void cancel() { if (isRunning()) pendingRenders_--; super.cancel(); } @Override public void run() { pendingRenders_--; } }; private void registerConsoleEvents() { EventBus events = RStudioGinjector.INSTANCE.getEventBus(); events.addHandler(ConsoleWriteOutputEvent.TYPE, this); events.addHandler(ConsoleWriteErrorEvent.TYPE, this); } private void unregisterConsoleEvents() { EventBus events = RStudioGinjector.INSTANCE.getEventBus(); events.removeHandler(ConsoleWriteOutputEvent.TYPE, this); events.removeHandler(ConsoleWriteErrorEvent.TYPE, this); } private void showBusyState() { if (spinner_ != null) { spinner_.removeFromParent(); spinner_.detach(); spinner_ = null; } // create a black or white spinner as appropriate ColorUtil.RGBColor bgColor = ColorUtil.RGBColor.fromCss(s_colors.background); spinner_ = new ProgressSpinner( bgColor.isDark() ? ProgressSpinner.COLOR_WHITE : ProgressSpinner.COLOR_BLACK); spinner_.getElement().addClassName(style.spinner()); frame_.add(spinner_); spinner_.getElement().getStyle().setOpacity(1); root_.getElement().getStyle().setOpacity(0.2); clear_.setVisible(false); expand_.setVisible(false); popout_.setVisible(false); } private void showReadyState() { if (getElement() != null && getElement().getStyle() != null && s_colors != null) { getElement().getStyle().setBackgroundColor(s_colors.background); } if (spinner_ != null) { spinner_.removeFromParent(); spinner_.detach(); spinner_ = null; } if (expansionState_.getValue() == EXPANDED) root_.getElement().getStyle().setOpacity(1); if (chunkOutputSize_ != ChunkOutputSize.Full && !hideSatellitePopup_) { clear_.setVisible(true); expand_.setVisible(true); popout_.setVisible(true); } } private void setOverflowStyle() { Element ele = root_.getElement(); boolean hasOverflow = ele.getScrollHeight() > ele.getOffsetHeight(); if (hasOverflow && !root_.getElement().hasClassName(style.overflowY())) { frame_.getElement().addClassName(style.overflowY()); } else if (!hasOverflow && root_.getElement().hasClassName(style.overflowY())) { frame_.getElement().removeClassName(style.overflowY()); } } private void completeInterrupt() { if (state_ == CHUNK_PRE_OUTPUT || state_ == CHUNK_POST_OUTPUT) { state_ = CHUNK_READY; } else { return; } showReadyState(); } private void popoutChunk() { chunkWindowManager_.openChunkWindow( documentId_, chunkId_, new Size(getElement().getOffsetWidth(), getElement().getOffsetHeight()) ); } private void toggleExpansionState(final boolean ensureVisible) { toggleExpansionState(ensureVisible, null); } private void toggleExpansionState(final boolean ensureVisible, final CommandWithArg<Boolean> onTransitionCompleted) { // don't permit toggling state while we're animating a new state // (no simple way to gracefully reverse direction) if (collapseTimer_ != null && collapseTimer_.isRunning()) return; if (expansionState_.getValue() == EXPANDED) { // remove scrollbars frame_.getElement().getStyle().setProperty("transition", "height " + ANIMATION_DUR + "ms ease"); setCollapsedStyles(); collapseTimer_ = new Timer() { @Override public void run() { renderedHeight_ = ChunkOutputUi.CHUNK_COLLAPSED_HEIGHT; host_.onOutputHeightChanged(ChunkOutputWidget.this, renderedHeight_, ensureVisible); if (onTransitionCompleted != null) onTransitionCompleted.execute(true); } }; expansionState_.setValue(COLLAPSED, true); } else { clearCollapsedStyles(); expansionState_.setValue(EXPANDED, true); syncHeight(true, ensureVisible); collapseTimer_ = new Timer() { @Override public void run() { syncHeight(true, ensureVisible); frame_.getElement().getStyle().clearProperty("transition"); if (onTransitionCompleted != null) onTransitionCompleted.execute(true); } }; } collapseTimer_.schedule(ANIMATION_DUR); } private void setCollapsedStyles() { getElement().addClassName(style.collapsed()); root_.getElement().getStyle().setOverflow(Overflow.HIDDEN); root_.getElement().getStyle().setOpacity(0); frame_.getElement().getStyle().setHeight( ChunkOutputUi.CHUNK_COLLAPSED_HEIGHT, Unit.PX); } private void clearCollapsedStyles() { getElement().removeClassName(style.collapsed()); root_.getElement().getStyle().clearOverflow(); root_.getElement().getStyle().clearOpacity(); } private void attachPresenter(ChunkOutputPresenter presenter) { if (root_.getWidget() != null) root_.remove(root_.getWidget()); presenter_ = presenter; root_.add(presenter.asWidget()); } private void initializeOutput(int type) { if (state_ == CHUNK_PRE_OUTPUT) { hasErrors_ = false; state_ = CHUNK_POST_OUTPUT; } else if (state_ == CHUNK_POST_OUTPUT && presenter_ instanceof ChunkOutputStream && (isBlockType(type) || isBlockType(type) != isBlockType(lastOutputType_))) { // we switch to gallery mode when we have either two block-type // outputs (e.g. two plots), or a block-type output combined with // non-block output (e.g. a plot and some text) final ChunkOutputStream stream = (ChunkOutputStream)presenter_; final ChunkOutputGallery gallery = new ChunkOutputGallery(this, chunkOutputSize_); attachPresenter(gallery); // extract all the pages from the stream and populate the gallery List<ChunkOutputPage> pages = stream.extractPages(); int ordinal = stream.getContentOrdinal(); if (ordinal > 0) { // add the stream itself if there's still anything left in it pages.add(new ChunkConsolePage(ordinal, stream, chunkOutputSize_)); } // ensure page ordering is correct Collections.sort(pages, new Comparator<ChunkOutputPage>() { @Override public int compare(ChunkOutputPage o1, ChunkOutputPage o2) { return o1.ordinal() - o2.ordinal(); } }); for (ChunkOutputPage page: pages) { gallery.addPage(page); } syncHeight(false, false); } lastOutputType_ = type; } private boolean isBlockType(int type) { switch (type) { case RmdChunkOutputUnit.TYPE_PLOT: case RmdChunkOutputUnit.TYPE_DATA: case RmdChunkOutputUnit.TYPE_HTML: return true; case RmdChunkOutputUnit.TYPE_TEXT: case RmdChunkOutputUnit.TYPE_ERROR: case RmdChunkOutputUnit.TYPE_ORDINAL: return false; } return true; } @UiField Image clear_; @UiField Image expand_; @UiField Image popout_; @UiField SimplePanel root_; @UiField ChunkStyle style; @UiField HTMLPanel frame_; @UiField HTMLPanel expander_; private ProgressSpinner spinner_; private RmdChunkOptions options_; private ChunkOutputHost host_; private ChunkOutputPresenter presenter_; private ChunkWindowManager chunkWindowManager_; private ChunkOutputSize chunkOutputSize_; private int state_ = CHUNK_EMPTY; private int execScope_ = NotebookQueueUnit.EXEC_SCOPE_CHUNK; private int renderedHeight_ = 0; private int pendingRenders_ = 0; private int lastOutputType_ = RmdChunkOutputUnit.TYPE_NONE; private boolean hasErrors_ = false; private boolean hideSatellitePopup_ = false; private Timer collapseTimer_ = null; private final String documentId_; private final String chunkId_; private final Value<Integer> expansionState_; private static EditorThemeListener.Colors s_colors; public final static int EXPANDED = 0; public final static int COLLAPSED = 1; private final static int ANIMATION_DUR = 400; public final static int CHUNK_EMPTY = 1; public final static int CHUNK_READY = 2; public final static int CHUNK_PRE_OUTPUT = 3; public final static int CHUNK_POST_OUTPUT = 4; }