/*
* RmdOutput.java
*
* Copyright (C) 2009-14 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.rmarkdown;
import java.util.Map;
import java.util.HashMap;
import org.rstudio.core.client.command.CommandBinder;
import org.rstudio.core.client.dom.WindowEx;
import org.rstudio.core.client.files.FileSystemItem;
import org.rstudio.core.client.widget.Operation;
import org.rstudio.core.client.widget.OperationWithInput;
import org.rstudio.core.client.widget.ProgressIndicator;
import org.rstudio.core.client.widget.ProgressOperation;
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.application.events.RestartStatusEvent;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.SimpleRequestCallback;
import org.rstudio.studio.client.common.filetypes.FileTypeRegistry;
import org.rstudio.studio.client.common.viewfile.ViewFilePanel;
import org.rstudio.studio.client.pdfviewer.PDFViewer;
import org.rstudio.studio.client.rmarkdown.events.ConvertToShinyDocEvent;
import org.rstudio.studio.client.rmarkdown.events.RenderRmdEvent;
import org.rstudio.studio.client.rmarkdown.events.RenderRmdSourceEvent;
import org.rstudio.studio.client.rmarkdown.events.RmdRenderCompletedEvent;
import org.rstudio.studio.client.rmarkdown.events.RmdRenderStartedEvent;
import org.rstudio.studio.client.rmarkdown.events.RmdShinyDocStartedEvent;
import org.rstudio.studio.client.rmarkdown.model.RMarkdownServerOperations;
import org.rstudio.studio.client.rmarkdown.model.RmdOutputFormat;
import org.rstudio.studio.client.rmarkdown.model.RmdPreviewParams;
import org.rstudio.studio.client.rmarkdown.model.RmdRenderResult;
import org.rstudio.studio.client.rmarkdown.model.RmdShinyDocInfo;
import org.rstudio.studio.client.rmarkdown.ui.RmdOutputFrame;
import org.rstudio.studio.client.rmarkdown.ui.ShinyDocumentWarningDialog;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.server.VoidServerRequestCallback;
import org.rstudio.studio.client.server.Void;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.prefs.events.UiPrefsChangedEvent;
import org.rstudio.studio.client.workbench.prefs.events.UiPrefsChangedHandler;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@Singleton
public class RmdOutput implements RmdRenderStartedEvent.Handler,
RmdRenderCompletedEvent.Handler,
RmdShinyDocStartedEvent.Handler,
RenderRmdEvent.Handler,
RenderRmdSourceEvent.Handler,
RestartStatusEvent.Handler,
UiPrefsChangedHandler
{
public interface Binder
extends CommandBinder<Commands, RmdOutput> {}
@Inject
public RmdOutput(EventBus eventBus,
Commands commands,
GlobalDisplay globalDisplay,
FileTypeRegistry fileTypeRegistry,
Provider<ViewFilePanel> pViewFilePanel,
Binder binder,
UIPrefs prefs,
PDFViewer pdfViewer,
RMarkdownServerOperations server)
{
globalDisplay_ = globalDisplay;
fileTypeRegistry_ = fileTypeRegistry;
pViewFilePanel_ = pViewFilePanel;
prefs_ = prefs;
pdfViewer_ = pdfViewer;
server_ = server;
events_ = eventBus;
eventBus.addHandler(RmdRenderStartedEvent.TYPE, this);
eventBus.addHandler(RmdRenderCompletedEvent.TYPE, this);
eventBus.addHandler(RmdShinyDocStartedEvent.TYPE, this);
eventBus.addHandler(RenderRmdEvent.TYPE, this);
eventBus.addHandler(RenderRmdSourceEvent.TYPE, this);
eventBus.addHandler(RestartStatusEvent.TYPE, this);
eventBus.addHandler(UiPrefsChangedEvent.TYPE, this);
prefs_.rmdViewerType().addValueChangeHandler(new ValueChangeHandler<Integer>()
{
@Override
public void onValueChange(ValueChangeEvent<Integer> e)
{
onViewerTypeChanged(e.getValue());
}
});
binder.bind(commands, this);
exportRmdOutputClosedCallback();
}
@Override
public void onRmdRenderStarted(RmdRenderStartedEvent event)
{
// When a Word document starts rendering, tell the desktop frame
// (if it exists) to get ready; this generally involves closing the
// document in preparation for a refresh
if (event.getFormat().getFormatName()
.equals(RmdOutputFormat.OUTPUT_WORD_DOCUMENT) &&
Desktop.isDesktop())
{
Desktop.getFrame().prepareShowWordDoc();
}
}
@Override
public void onRmdRenderCompleted(RmdRenderCompletedEvent event)
{
// if there's a custom operation to be run when render completes, run
// that instead
if (onRenderCompleted_ != null)
{
onRenderCompleted_.execute();
onRenderCompleted_ = null;
return;
}
// ignore failures and completed Shiny docs (the latter are handled when
// the server starts rather than when the render process is finished)
final RmdRenderResult result = event.getResult();
if (result.isShinyDocument())
{
shinyDoc_ = null;
return;
}
if (result.hasShinyContent() && !result.isShinyDocument())
{
// If the result has Shiny content but wasn't rendered as a Shiny
// document, suggest rendering as a Shiny document instead
new ShinyDocumentWarningDialog(new OperationWithInput<Integer>()
{
@Override
public void execute(Integer input)
{
switch (input)
{
case ShinyDocumentWarningDialog.RENDER_SHINY_NO:
if (result.getSucceeded())
displayRenderResult(result);
break;
case ShinyDocumentWarningDialog.RENDER_SHINY_ONCE:
rerenderAsShiny(result);
break;
case ShinyDocumentWarningDialog.RENDER_SHINY_ALWAYS:
events_.fireEvent(new ConvertToShinyDocEvent
(result.getTargetFile()));
break;
}
}
}).showModal();
}
else if (result.getSucceeded())
{
displayRenderResult(event.getResult());
}
}
@Override
public void onRmdShinyDocStarted(RmdShinyDocStartedEvent event)
{
shinyDoc_ = event.getDocInfo();
RmdRenderResult result =
RmdRenderResult.createFromShinyDoc(shinyDoc_);
displayHTMLRenderResult(result);
}
@Override
public void onRenderRmd(final RenderRmdEvent event)
{
final Operation renderOperation = new Operation() {
@Override
public void execute()
{
server_.renderRmd(event.getSourceFile(),
event.getSourceLine(),
event.getFormat(),
event.getEncoding(),
event.asTempfile(),
event.asShiny(),
new SimpleRequestCallback<Boolean>());
}
};
// If there's a running shiny document for this file and it's not a
// presentation, we can do an in-place reload. Note that we don't
// currently support in-place reload for Shiny presentations since we
// would need to hook a client event at the end of the re-render that
// emitted updated slide navigation information and then plumbed that
// information back into the preview window.
if (shinyDoc_ != null &&
event.getSourceFile().equals(shinyDoc_.getFile()) &&
!shinyDoc_.getFormat().getFormatName().endsWith(
RmdOutputFormat.OUTPUT_PRESENTATION_SUFFIX))
{
final RmdRenderResult result =
RmdRenderResult.createFromShinyDoc(shinyDoc_);
displayHTMLRenderResult(result);
}
else
{
performRenderOperation(renderOperation);
}
}
@Override
public void onRenderRmdSource(final RenderRmdSourceEvent event)
{
performRenderOperation(new Operation() {
@Override
public void execute()
{
server_.renderRmdSource(event.getSource(),
new SimpleRequestCallback<Boolean>());
}
});
}
@Override
public void onRestartStatus(RestartStatusEvent event)
{
// preemptively close the satellite window when R restarts (so we don't
// wait around if the session doesn't get a chance to tell us about
// terminated renders)
if (event.getStatus() == RestartStatusEvent.RESTART_INITIATED)
{
if (outputFrame_ != null)
outputFrame_.closeOutputFrame(false);
restarting_ = true;
}
else
{
restarting_ = false;
}
}
@Override
public void onUiPrefsChanged(UiPrefsChangedEvent e)
{
onViewerTypeChanged(prefs_.rmdViewerType().getValue());
}
// Private methods ---------------------------------------------------------
private void onViewerTypeChanged(int newViewerType)
{
if (outputFrame_ != null &&
outputFrame_.getWindowObject() != null &&
newViewerType != outputFrame_.getViewerType())
{
// close the existing frame
RmdPreviewParams params = outputFrame_.getPreviewParams();
outputFrame_.closeOutputFrame(true);
// reset the scroll position (as it will vary with the document width,
// which will change)
params.setScrollPosition(0);
// open a new one with the same parameters
outputFrame_ = createOutputFrame(newViewerType);
outputFrame_.showRmdPreview(params);
}
else if (outputFrame_ != null &&
outputFrame_.getWindowObject() == null &&
outputFrame_.getViewerType() != newViewerType)
{
// output frame exists but doesn't have a loaded doc, clear it so we'll
// create the frame appropriate to this type on next render
outputFrame_ = null;
}
}
// perform the given render after terminating the currently running Shiny
// application if there is one
private void performRenderOperation(final Operation renderOperation)
{
if (shinyDoc_ != null)
{
// there is a Shiny doc running; we'll need to terminate it before
// we can render this document
outputFrame_.closeOutputFrame(false);
server_.terminateRenderRmd(true, new ServerRequestCallback<Void>()
{
@Override
public void onResponseReceived(Void v)
{
onRenderCompleted_ = renderOperation;
shinyDoc_ = null;
}
@Override
public void onError(ServerError error)
{
globalDisplay_.showErrorMessage("Shiny Terminate Failed",
"The Shiny document " + shinyDoc_.getFile() + " needs to " +
"be stopped before the document can be rendered.");
}
});
}
else
{
renderOperation.execute();
}
}
private void rerenderAsShiny(RmdRenderResult result)
{
events_.fireEvent(new RenderRmdEvent(
result.getTargetFile(), result.getTargetLine(),
null, result.getTargetEncoding(), false, true));
}
private void displayRenderResult(final RmdRenderResult result)
{
String extension = FileSystemItem.getExtensionFromPath(
result.getOutputFile());
if (".pdf".equals(extension))
{
String previewer = prefs_.getPdfPreviewValue();
if (previewer.equals(UIPrefs.PDF_PREVIEW_RSTUDIO))
{
pdfViewer_.viewPdfUrl(
result.getOutputUrl(),
result.getPreviewSlide() >= 0 ?
result.getPreviewSlide() : null);
}
else if (!previewer.equals(UIPrefs.PDF_PREVIEW_NONE))
{
if (Desktop.isDesktop())
Desktop.getFrame().showPDF(result.getOutputFile(),
result.getPreviewSlide());
else
globalDisplay_.showHtmlFile(result.getOutputFile());
}
}
else if (".docx".equals(extension))
{
if (Desktop.isDesktop())
globalDisplay_.showWordDoc(result.getOutputFile());
// it's not possible to show Word docs inline in a useful way from
// within the browser, so just offer to download the file.
else
{
globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_INFO,
"R Markdown Render Completed",
"R Markdown has finished rendering " +
result.getTargetFile() + " to " +
result.getOutputFile() + ".",
false,
new ProgressOperation()
{
@Override
public void execute(ProgressIndicator indicator)
{
globalDisplay_.showWordDoc(result.getOutputFile());
indicator.onCompleted();
}
},
null,
"Download File",
"OK",
false);
}
}
else if (".html".equals(extension))
{
displayHTMLRenderResult(result);
}
else if (".md".equalsIgnoreCase(extension) ||
extension.toLowerCase().startsWith(".markdown") ||
".tex".equalsIgnoreCase(extension))
{
ViewFilePanel viewFilePanel = pViewFilePanel_.get();
viewFilePanel.showFile(
FileSystemItem.createFile(result.getOutputFile()), "UTF-8");
}
else
{
if (Desktop.isDesktop())
Desktop.getFrame().showFile(result.getOutputFile());
else
globalDisplay_.openWindow(result.getOutputUrl());
}
}
private void displayHTMLRenderResult(RmdRenderResult result)
{
// find the last known position for this file
int scrollPosition = 0;
String anchor = "";
if (scrollPositions_.containsKey(keyFromResult(result)))
{
scrollPosition = scrollPositions_.get(keyFromResult(result));
}
if (anchors_.containsKey(keyFromResult(result)))
{
anchor = anchors_.get(keyFromResult(result));
}
final RmdPreviewParams params = RmdPreviewParams.create(
result, scrollPosition, anchor);
// don't host presentations in the viewer pane--ioslides doesn't scale
// slides well without help
final int newViewerType = result.isHtmlPresentation() ?
RMD_VIEWER_TYPE_WINDOW :
prefs_.rmdViewerType().getValue();
// get the window object if available
WindowEx win = null;
boolean needsReopen = false;
if (outputFrame_ != null)
{
win = outputFrame_.getWindowObject();
if (outputFrame_.getViewerType() != newViewerType)
needsReopen = true;
}
// if there's a window up but it's showing a different document type,
// close it so that we can create a new one better suited to this doc type
if (needsReopen ||
(win != null &&
result_ != null &&
!result_.getFormatName().equals(result.getFormatName())))
{
outputFrame_.closeOutputFrame(false);
outputFrame_ = null;
win = null;
// let window finish closing before continuing
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
@Override
public void execute()
{
displayRenderResult(null, newViewerType, params);
}
});
}
else
{
displayRenderResult(win, newViewerType, params);
}
}
private void displayRenderResult(WindowEx win, int viewerType,
RmdPreviewParams params)
{
RmdRenderResult result = params.getResult();
if (outputFrame_ == null)
outputFrame_ = createOutputFrame(viewerType);
// we're refreshing if the window is up and we're pulling the same
// output file as the last one
boolean isRefresh = win != null &&
result_ != null &&
result_.getOutputFile().equals(
result.getOutputFile());
// if this isn't a refresh but there's a window up, cache the scroll
// position of the old document before we replace it
if (!isRefresh && result_ != null && win != null)
{
cacheDocPosition(result_, outputFrame_.getScrollPosition(),
outputFrame_.getAnchor());
}
// if it is a refresh, use the doc's existing positions
if (isRefresh)
{
params.setScrollPosition(outputFrame_.getScrollPosition());
params.setAnchor(outputFrame_.getAnchor());
}
outputFrame_.showRmdPreview(params);
// save the result so we know if the next render is a re-render of the
// same document
result_ = result;
}
private final native void exportRmdOutputClosedCallback()/*-{
var registry = this;
$wnd.notifyRmdOutputClosed = $entry(
function(params) {
registry.@org.rstudio.studio.client.rmarkdown.RmdOutput::notifyRmdOutputClosed(Lcom/google/gwt/core/client/JavaScriptObject;)(params);
}
);
}-*/;
// when the window is closed, remember our position within it
private void notifyRmdOutputClosed(JavaScriptObject closeParams)
{
// save anchor location for presentations and scroll position for
// documents
RmdPreviewParams params = closeParams.cast();
cacheDocPosition(params.getResult(), params.getScrollPosition(),
params.getAnchor());
// if this is a Shiny document, stop the associated process
if (params.isShinyDocument() && !restarting_)
{
server_.terminateRenderRmd(true, new VoidServerRequestCallback());
}
shinyDoc_ = null;
}
private void cacheDocPosition(RmdRenderResult result, int scrollPosition,
String anchor)
{
if (result.isHtmlPresentation())
{
anchors_.put(keyFromResult(result), anchor);
}
else
{
scrollPositions_.put(keyFromResult(result), scrollPosition);
}
}
// Generates lookup keys from results; used to enforce caching scroll
// position and/or anchor by document name and type
private String keyFromResult(RmdRenderResult result)
{
if (result.isShinyDocument())
return result.getTargetFile();
else
return result.getOutputFile() + "-" + result.getFormatName();
}
private RmdOutputFrame createOutputFrame(int viewerType)
{
switch(viewerType)
{
case RMD_VIEWER_TYPE_WINDOW:
return RStudioGinjector.INSTANCE.getRmdOutputFrameSatellite();
case RMD_VIEWER_TYPE_PANE:
return RStudioGinjector.INSTANCE.getRmdOutputFramePane();
}
return null;
}
private final GlobalDisplay globalDisplay_;
private final FileTypeRegistry fileTypeRegistry_;
private final UIPrefs prefs_;
private final PDFViewer pdfViewer_;
private final Provider<ViewFilePanel> pViewFilePanel_;
private final RMarkdownServerOperations server_;
private final EventBus events_;
private boolean restarting_ = false;
// stores the last scroll position of each document we know about: map
// of path to position
private final Map<String, Integer> scrollPositions_ =
new HashMap<String, Integer>();
private final Map<String, String> anchors_ =
new HashMap<String, String>();
private RmdRenderResult result_;
private RmdShinyDocInfo shinyDoc_;
private Operation onRenderCompleted_;
private RmdOutputFrame outputFrame_;
public final static int RMD_VIEWER_TYPE_WINDOW = 0;
public final static int RMD_VIEWER_TYPE_PANE = 1;
}