/*
* ChunkOutputGallery.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 org.rstudio.core.client.ColorUtil;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.js.JsArrayEx;
import org.rstudio.studio.client.common.debugging.model.UnhandledError;
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 com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.resources.client.CssResource;
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.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;
public class ChunkOutputGallery extends Composite
implements ChunkOutputPresenter
{
private static ChunkOutputGalleryUiBinder uiBinder = GWT
.create(ChunkOutputGalleryUiBinder.class);
interface ChunkOutputGalleryUiBinder
extends UiBinder<Widget, ChunkOutputGallery>
{
}
public interface GalleryStyle extends CssResource
{
String thumbnail();
String selected();
String expand();
String content();
}
// Public methods ----------------------------------------------------------
public ChunkOutputGallery(
ChunkOutputPresenter.Host host,
ChunkOutputSize chunkOutputSize)
{
pages_ = new ArrayList<ChunkOutputPage>();
host_ = host;
chunkOutputSize_ = chunkOutputSize;
initWidget(uiBinder.createAndBindUi(this));
content_ = new SimplePanel();
viewer_.add(content_);
if (chunkOutputSize_ == ChunkOutputSize.Full)
{
addStyleName(style.expand());
content_.addStyleName(style.content());
}
}
@Override
public void showConsoleText(String text)
{
ensureConsole();
console_.showConsoleText(text);
}
@Override
public void showConsoleError(String error)
{
ensureConsole();
console_.showConsoleError(error);
}
@Override
public void showConsoleOutput(JsArray<JsArrayEx> output)
{
// ignore if no actual text needs to be emitted (this prevents us from
// creating a spurious console page)
int i = 0;
for (; i < output.length(); i++)
{
if (!StringUtil.isNullOrEmpty(output.get(i).getString(1)))
break;
}
if (i == output.length())
return;
ensureConsole();
console_.showConsoleOutput(output);
}
@Override
public void showPlotOutput(String url, NotebookPlotMetadata metadata,
int ordinal, Command onRenderComplete)
{
addPage(new ChunkPlotPage(url, metadata, ordinal, onRenderComplete, chunkOutputSize_));
}
@Override
public void showHtmlOutput(String url, NotebookHtmlMetadata metadata,
int ordinal, Command onRenderComplete)
{
addPage(new ChunkHtmlPage(url, metadata, ordinal, onRenderComplete, chunkOutputSize_));
}
@Override
public void showErrorOutput(UnhandledError error)
{
ensureConsole();
console_.showErrorOutput(error);
// switch back to the console so the user can see the error
for (int i = 0; i < pages_.size(); i++)
{
if (pages_.get(i) == console_)
{
setActivePage(i);
break;
}
}
}
@Override
public void showOrdinalOutput(int ordinal)
{
addPage(new ChunkOrdinalPage(ordinal));
}
@Override
public void showDataOutput(JavaScriptObject data,
NotebookFrameMetadata metadata, int ordinal)
{
addPage(new ChunkDataPage(data, metadata, ordinal, chunkOutputSize_));
}
@Override
public void setPlotPending(boolean pending, String pendingStyle)
{
for (ChunkOutputPage page: pages_)
{
if (page instanceof ChunkPlotPage)
{
ChunkPlotPage plot = (ChunkPlotPage)page;
if (pending)
plot.contentWidget().addStyleName(pendingStyle);
else
plot.contentWidget().removeStyleName(pendingStyle);
}
}
}
@Override
public void updatePlot(String plotUrl, String pendingStyle)
{
for (ChunkOutputPage page: pages_)
{
if (page instanceof ChunkPlotPage)
{
ChunkPlotPage plot = (ChunkPlotPage)page;
plot.updateImageUrl(plotUrl, pendingStyle);
}
}
}
@Override
public void clearOutput()
{
content_.clear();
pages_.clear();
filmstrip_.clear();
}
@Override
public void completeOutput()
{
if (console_ != null)
console_.completeOutput();
}
@Override
public boolean hasOutput()
{
return pages_.size() > 0;
}
@Override
public boolean hasPlots()
{
for (ChunkOutputPage page: pages_)
{
if (page instanceof ChunkPlotPage)
return true;
}
return false;
}
@Override
public boolean hasErrors()
{
if (console_ != null)
return console_.hasErrors();
return false;
}
@Override
public void onEditorThemeChanged(EditorThemeListener.Colors colors)
{
for (Widget thumbnail: filmstrip_)
{
syncThumbnailColor(thumbnail, colors);
}
for (ChunkOutputPage page: pages_)
{
if (page instanceof EditorThemeListener)
((EditorThemeListener)page).onEditorThemeChanged(colors);
}
}
public void addPage(ChunkOutputPage page)
{
int idx = pages_.size();
// look for ordinal replacements
for (int i = 0; i < pages_.size(); i++)
{
if (page.ordinal() == pages_.get(i).ordinal())
{
// replace at this index
idx = i;
// assert that we're actually removing an ordinal page
assert(pages_.get(i) instanceof ChunkOrdinalPage);
// remove the existing placeholder
pages_.remove(i);
filmstrip_.remove(i);
break;
}
}
pages_.add(idx, page);
addThumbnail(idx, page);
// show this page if it's the first one, or if we don't have any errors
// and we're adding a last page
if (!(page instanceof ChunkOrdinalPage) &&
(idx == 0 || ((idx == pages_.size() - 1) &&
!hasErrors())))
setActivePage(idx);
host_.notifyHeightChanged();
}
@Override
public void onResize()
{
for (ChunkOutputPage page: pages_)
{
if (page instanceof ChunkDataPage)
{
((ChunkDataPage)page).onResize();
}
}
}
// Private methods ---------------------------------------------------------
private void navigateActivePage(int delta)
{
// add with wraparound
int idx = activePage_ + delta;
if (idx >= pages_.size())
idx = 0;
if (idx < 0)
idx = pages_.size() - 1;
setActivePage(idx);
}
private void setActivePage(int idx)
{
// ignore if out of bounds or no-op
if (idx >= pages_.size())
return;
if (idx == activePage_)
return;
content_.clear();
content_.add(pages_.get(idx).contentWidget());
// remove the selection styling from the previously active page (if any)
// and add it to this page
if (activePage_ >= 0)
pages_.get(activePage_).thumbnailWidget().removeStyleName(
style.selected());
pages_.get(idx).thumbnailWidget().addStyleName(style.selected());
pages_.get(idx).onSelected();
activePage_ = idx;
// this page may have a different height than its predecessor
host_.notifyHeightChanged();
if (pages_.get(idx) instanceof ChunkDataPage)
{
((ChunkDataPage)pages_.get(idx)).onResize();
}
}
private void ensureConsole()
{
if (console_ == null)
{
console_ = new ChunkConsolePage(0, chunkOutputSize_);
addPage(console_);
}
}
private static void syncThumbnailColor(Widget thumbnail,
EditorThemeListener.Colors colors)
{
// might happen if we aren't initialized yet
if (colors == null || thumbnail == null)
return;
// create a border color by making the foreground color slightly
// translucent
ColorUtil.RGBColor fore = ColorUtil.RGBColor.fromCss(colors.foreground);
ColorUtil.RGBColor border = new ColorUtil.RGBColor(
fore.red(), fore.green(), fore.blue(), 0.5);
// apply border color from editor
thumbnail.getElement().getStyle().setBorderColor(border.asRgb());
if (thumbnail instanceof EditorThemeListener)
{
((EditorThemeListener)thumbnail).onEditorThemeChanged(colors);
}
}
private void addThumbnail(int idx, ChunkOutputPage page)
{
final Widget thumbnail = page.thumbnailWidget();
thumbnail.getElement().setTabIndex(0);
thumbnail.addStyleName(style.thumbnail());
// apply editor color to thumbnail before
syncThumbnailColor(thumbnail, ChunkOutputWidget.getEditorColors());
filmstrip_.insert(thumbnail, idx);
// lock to this console if we don't have one already
if (page instanceof ChunkConsolePage && console_ == null)
console_ = (ChunkConsolePage)page;
final int ordinal = page.ordinal();
DOM.sinkEvents(thumbnail.getElement(), Event.ONCLICK | Event.ONKEYDOWN);
DOM.setEventListener(thumbnail.getElement(), new EventListener()
{
@Override
public void onBrowserEvent(Event evt)
{
switch(DOM.eventGetType(evt))
{
case Event.ONCLICK:
// convert ordinal back to index (index can change with
// out-of-order insertions)
for (int i = 0; i < pages_.size(); i++)
{
if (pages_.get(i).ordinal() == ordinal)
{
setActivePage(i);
break;
}
}
break;
case Event.ONKEYDOWN:
int dir = 0;
if (evt.getKeyCode() == KeyCodes.KEY_LEFT)
dir = -1;
if (evt.getKeyCode() == KeyCodes.KEY_RIGHT)
dir = 1;
if (dir != 0)
navigateActivePage(dir);
break;
};
}
});
}
private final ArrayList<ChunkOutputPage> pages_;
private final ChunkOutputPresenter.Host host_;
private final ChunkOutputSize chunkOutputSize_;
private ChunkConsolePage console_;
private SimplePanel content_;
private int activePage_ = -1;
@UiField GalleryStyle style;
@UiField FlowPanel filmstrip_;
@UiField HTMLPanel viewer_;
}