/*
* ViewerPresenter.java
*
* Copyright (C) 2009-12 by RStudio, Inc.
*
* 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.viewer;
import com.google.gwt.user.client.Command;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.Size;
import org.rstudio.core.client.command.AppCommand;
import org.rstudio.core.client.command.CommandBinder;
import org.rstudio.core.client.command.EnabledChangedHandler;
import org.rstudio.core.client.command.Handler;
import org.rstudio.core.client.dom.WindowEx;
import org.rstudio.core.client.files.FileSystemItem;
import org.rstudio.core.client.widget.OperationWithInput;
import org.rstudio.core.client.widget.ProgressIndicator;
import org.rstudio.core.client.widget.ProgressOperation;
import org.rstudio.core.client.widget.ProgressOperationWithInput;
import org.rstudio.studio.client.application.Desktop;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.FileDialogs;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.SimpleRequestCallback;
import org.rstudio.studio.client.common.GlobalDisplay.NewWindowOptions;
import org.rstudio.studio.client.common.dependencies.DependencyManager;
import org.rstudio.studio.client.common.rpubs.RPubsHtmlGenerator;
import org.rstudio.studio.client.common.rpubs.RPubsPresenter;
import org.rstudio.studio.client.common.rpubs.ui.RPubsUploadDialog;
import org.rstudio.studio.client.common.zoom.ZoomUtils;
import org.rstudio.studio.client.rmarkdown.model.RmdPreviewParams;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.VoidServerRequestCallback;
import org.rstudio.studio.client.shiny.events.ShinyApplicationStatusEvent;
import org.rstudio.studio.client.shiny.model.ShinyApplicationParams;
import org.rstudio.studio.client.shiny.model.ShinyViewerType;
import org.rstudio.studio.client.workbench.WorkbenchContext;
import org.rstudio.studio.client.workbench.WorkbenchView;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.exportplot.ExportPlotUtils;
import org.rstudio.studio.client.workbench.exportplot.model.ExportPlotOptions;
import org.rstudio.studio.client.workbench.exportplot.model.SavePlotAsImageContext;
import org.rstudio.studio.client.workbench.model.RemoteFileSystemContext;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.views.BasePresenter;
import org.rstudio.studio.client.workbench.views.source.SourceShim;
import org.rstudio.studio.client.workbench.views.viewer.events.ViewerClearedEvent;
import org.rstudio.studio.client.workbench.views.viewer.events.ViewerNavigateEvent;
import org.rstudio.studio.client.workbench.views.viewer.events.ViewerPreviewRmdEvent;
import org.rstudio.studio.client.workbench.views.viewer.export.CopyViewerPlotToClipboardDesktopDialog;
import org.rstudio.studio.client.workbench.views.viewer.export.SaveViewerPlotAsImageDesktopDialog;
import org.rstudio.studio.client.workbench.views.viewer.model.ViewerServerOperations;
public class ViewerPresenter extends BasePresenter
implements ViewerNavigateEvent.Handler,
ViewerPreviewRmdEvent.Handler,
ShinyApplicationStatusEvent.Handler,
RPubsPresenter.Context
{
public interface Binder extends CommandBinder<Commands, ViewerPresenter> {}
public interface Display extends WorkbenchView
{
void navigate(String url);
void setExportEnabled(boolean exportEnabled);
void previewRmd(RmdPreviewParams params);
String getUrl();
String getTitle();
void popout();
void refresh();
Size getViewerFrameSize();
}
@Inject
public ViewerPresenter(Display display,
EventBus eventBus,
GlobalDisplay globalDisplay,
WorkbenchContext workbenchContext,
DependencyManager dependencyManager,
FileDialogs fileDialogs,
RemoteFileSystemContext fileSystemContext,
Commands commands,
Binder binder,
ViewerServerOperations server,
SourceShim sourceShim,
RPubsPresenter rpubsPresenter,
Provider<UIPrefs> pUIPrefs)
{
super(display);
display_ = display;
workbenchContext_ = workbenchContext;
dependencyManager_ = dependencyManager;
fileDialogs_ = fileDialogs;
fileSystemContext_ = fileSystemContext;
commands_ = commands;
server_ = server;
events_ = eventBus;
globalDisplay_ = globalDisplay;
sourceShim_ = sourceShim;
pUIPrefs_ = pUIPrefs;
rpubsPresenter.setContext(this);
binder.bind(commands, this);
manageCommands(false);
// show a stop button when the console is busy (the stop and
// clear commands are mutually exclusive)
commands_.viewerStop().setVisible(commands_.interruptR().isEnabled());
commands_.viewerClear().setVisible(!commands_.viewerStop().isVisible());
commands_.interruptR().addEnabledChangedHandler(
new EnabledChangedHandler() {
@Override
public void onEnabledChanged(AppCommand command)
{
commands_.viewerStop().setVisible(command.isEnabled());
commands_.viewerClear().setVisible(!command.isEnabled());
commands_.viewerClearAll().setVisible(!command.isEnabled());
}
});
eventBus.addHandler(ShinyApplicationStatusEvent.TYPE, this);
initializeEvents();
}
@Override
public void onViewerNavigate(ViewerNavigateEvent event)
{
if (event.getURL().length() > 0)
{
manageCommands(true, event);
if (event.getBringToFront())
display_.bringToFront();
// respect height request
int ensureHeight = event.getHeight();
if (ensureHeight < 0)
display_.maximize();
else if (ensureHeight > 0)
display_.ensureHeight(ensureHeight);
navigate(event.getURL());
if (event.isHTMLWidget())
updateZoomWindow(display_.getUrl());
}
else
{
manageCommands(false);
navigate(ViewerPane.ABOUT_BLANK);
}
}
@Override
public void onViewerPreviewRmd(ViewerPreviewRmdEvent event)
{
manageCommands(true);
display_.bringToFront();
if (!event.isRefresh())
display_.maximize();
rmdPreviewParams_ = event.getParams();
if (Desktop.isDesktop())
Desktop.getFrame().setViewerUrl(event.getParams().getOutputUrl());
display_.previewRmd(event.getParams());
}
@Override
public void onShinyApplicationStatus(ShinyApplicationStatusEvent event)
{
if (event.getParams().getViewerType() ==
ShinyViewerType.SHINY_VIEWER_PANE &&
event.getParams().getState() ==
ShinyApplicationParams.STATE_STARTED)
{
manageCommands(true);
display_.bringToFront();
navigate(event.getParams().getUrl());
runningShinyAppParams_ = event.getParams();
}
}
@Handler
public void onViewerPopout() { display_.popout(); }
@Handler
public void onViewerRefresh() { display_.refresh(); }
@Handler
public void onViewerBack()
{
server_.viewerBack(new VoidServerRequestCallback());
}
@Handler
public void onViewerForward()
{
server_.viewerForward(new VoidServerRequestCallback());
}
@Handler
public void onViewerZoom()
{
Size windowSize = ZoomUtils.getZoomWindowSize(
display_.getViewerFrameSize(), zoomWindowDefaultSize_);
// open and activate window
NewWindowOptions options = new NewWindowOptions();
options.setName("_rstudio_viewer_zoom");
options.setFocus(true);
options.setCallback(new OperationWithInput<WindowEx>() {
@Override
public void execute(WindowEx input)
{
zoomWindow_ = input;
}
});
globalDisplay_.openMinimalWindow(display_.getUrl(),
false,
windowSize.width,
windowSize.height,
options);
}
@Handler
public void onViewerSaveAsImage()
{
display_.bringToFront();
final ProgressIndicator indicator =
globalDisplay_.getProgressIndicator("Error");
indicator.onProgress("Preparing to export plot...");
// get the default directory
FileSystemItem defaultDir = ExportPlotUtils.getDefaultSaveDirectory(
workbenchContext_.getCurrentWorkingDir());
// get context
server_.getViewerExportContext(
defaultDir.getPath(),
new SimpleRequestCallback<SavePlotAsImageContext>() {
@Override
public void onResponseReceived(SavePlotAsImageContext context)
{
indicator.onCompleted();
new SaveViewerPlotAsImageDesktopDialog(
globalDisplay_,
display_.getUrl(),
context,
ExportPlotOptions.adaptToSize(
pUIPrefs_.get().exportViewerOptions().getValue(),
display_.getViewerFrameSize()),
saveExportOptionsOperation_
).showModal();
}
@Override
public void onError(ServerError error)
{
indicator.onError(error.getUserMessage());
}
});
}
@Handler
public void onViewerSaveAsWebPage()
{
display_.bringToFront();
if (saveAsWebPageDefaultPath_ == null)
saveAsWebPageDefaultPath_ = workbenchContext_.getCurrentWorkingDir();
dependencyManager_.withRMarkdown("Saving standalone web pages",
new Command() {
@Override
public void execute()
{
fileDialogs_.saveFile(
"Save As Web Page",
fileSystemContext_,
saveAsWebPageDefaultPath_,
".html",
false,
new ProgressOperationWithInput<FileSystemItem>(){
@Override
public void execute(final FileSystemItem targetFile,
ProgressIndicator indicator)
{
if (targetFile == null)
{
indicator.onCompleted();
return;
}
indicator.onProgress("Saving as web page...");
server_.viewerSaveAsWebPage(
targetFile.getPath(),
new VoidServerRequestCallback(indicator) {
@Override
public void onSuccess()
{
saveAsWebPageDefaultPath_ =
targetFile.getParentPath();
}
});
}
});
}
});
}
@Handler
public void onViewerCopyToClipboard()
{
new CopyViewerPlotToClipboardDesktopDialog(
display_.getUrl(),
ExportPlotOptions.adaptToSize(
pUIPrefs_.get().exportViewerOptions().getValue(),
display_.getViewerFrameSize()),
saveExportOptionsOperation_
).showModal();;
}
@Handler
public void onViewerPublishToRPubs()
{
dependencyManager_.withRMarkdown("Publishing to RPubs",
new Command() {
@Override
public void execute()
{
RPubsUploadDialog dlg = new RPubsUploadDialog(
"Viewer",
"",
new RPubsHtmlGenerator() {
@Override
public void generateRPubsHtml(
String title,
String comment,
final CommandWithArg<String> onCompleted)
{
server_.viewerCreateRPubsHtml(
title, comment, new SimpleRequestCallback<String>(){
@Override
public void onResponseReceived(String rpubsHtmlFile)
{
onCompleted.execute(rpubsHtmlFile);
}
});
}
},
false);
dlg.showModal();
}
});
}
@Handler
public void onViewerSaveAllAndRefresh()
{
sourceShim_.handleUnsavedChangesBeforeExit(
sourceShim_.getUnsavedChanges(),
new Command() {
@Override
public void execute()
{
display_.refresh();
}
});
}
@Handler
public void onViewerClear()
{
stop(false);
}
@Handler
public void onViewerClearAll()
{
// confirm
globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_QUESTION,
"Clear Viewer",
"Are you sure you want to clear all of the items in the history?",
new ProgressOperation() {
public void execute(final ProgressIndicator indicator)
{
indicator.onProgress("Clearing viewer...");
stop(false, true, indicator);
}
},
true
);
}
@Handler
public void onViewerStop()
{
stop(true);
}
@Override
public String getContextId()
{
return "RMarkdownPreview";
}
@Override
public String getTitle()
{
String title = display_.getTitle();
if (title != null && !title.isEmpty())
return title;
String htmlFile = null;
if (rmdPreviewParams_ != null)
htmlFile = rmdPreviewParams_.getOutputFile();
if (htmlFile != null)
{
FileSystemItem fsi = FileSystemItem.createFile(htmlFile);
return fsi.getStem();
}
else
{
return "(Untitled)";
}
}
@Override
public String getHtmlFile()
{
if (rmdPreviewParams_ != null)
return rmdPreviewParams_.getOutputFile();
else
return "";
}
@Override
public boolean isPublished()
{
if (rmdPreviewParams_ != null)
return rmdPreviewParams_.getResult().getRpubsPublished();
else
return false;
}
private void navigate(String url)
{
if (Desktop.isDesktop())
Desktop.getFrame().setViewerUrl(url);
display_.navigate(url);
}
private void updateZoomWindow(String url)
{
if (Desktop.isDesktop())
Desktop.getFrame().reloadViewerZoomWindow(url);
else if ((zoomWindow_ != null) && !zoomWindow_.isClosed())
zoomWindow_.setLocationHref(url);
}
private void stop(boolean interruptR)
{
stop(interruptR, false, null);
}
private void stop(boolean interruptR,
boolean clearAll,
ProgressIndicator indicator)
{
// check whether this was a static widget (determine what we do
// visa-vi widget history clearing/restoration)
boolean wasStaticWidget = commands_.viewerZoom().isEnabled();
manageCommands(false);
navigate(ViewerPane.ABOUT_BLANK);
if (interruptR)
commands_.interruptR().execute();
server_.viewerStopped(new VoidServerRequestCallback());
// If we were viewing a Shiny application, let the rest of the app know
// that the application has been stopped
if (runningShinyAppParams_ != null)
{
runningShinyAppParams_.setState(ShinyApplicationParams.STATE_STOPPED);
events_.fireEvent(new ShinyApplicationStatusEvent(
runningShinyAppParams_));
}
runningShinyAppParams_ = null;
events_.fireEvent(new ViewerClearedEvent());
// if this was a static widget then clear the current widget
if (clearAll)
server_.viewerClearAll(new VoidServerRequestCallback(indicator));
else if (wasStaticWidget)
server_.viewerClearCurrent(new VoidServerRequestCallback(indicator));
// otherwise restore the last static widget
else
server_.viewerCurrent(new VoidServerRequestCallback());
}
private void manageCommands(boolean enable)
{
manageCommands(enable, false, false, false);
}
private void manageCommands(boolean enable, ViewerNavigateEvent event)
{
manageCommands(enable,
event.isHTMLWidget(),
event.getHasNext(),
event.getHasPrevious());
}
private void manageCommands(boolean enable,
boolean isHTMLWidget,
boolean hasNext,
boolean hasPrevious)
{
commands_.viewerPopout().setEnabled(enable);
commands_.viewerRefresh().setEnabled(enable);
commands_.viewerClear().setEnabled(enable);
commands_.viewerClearAll().setEnabled(enable);
commands_.viewerBack().setEnabled(hasPrevious);
commands_.viewerBack().setVisible(isHTMLWidget);
commands_.viewerForward().setEnabled(hasNext);
commands_.viewerForward().setVisible(isHTMLWidget);
commands_.viewerZoom().setEnabled(enable);
commands_.viewerZoom().setVisible(isHTMLWidget);
boolean canSnapshot = Desktop.isDesktop();
commands_.viewerSaveAsImage().setEnabled(enable && canSnapshot);
commands_.viewerSaveAsImage().setVisible(isHTMLWidget && canSnapshot);
commands_.viewerCopyToClipboard().setEnabled(enable && canSnapshot);
commands_.viewerCopyToClipboard().setVisible(isHTMLWidget && canSnapshot);
commands_.viewerSaveAsWebPage().setEnabled(enable);
commands_.viewerSaveAsWebPage().setVisible(isHTMLWidget);
commands_.viewerPublishToRPubs().setEnabled(enable);
commands_.viewerPublishToRPubs().setVisible(isHTMLWidget);
display_.setExportEnabled(isHTMLWidget);
}
private OperationWithInput<ExportPlotOptions> saveExportOptionsOperation_ =
new OperationWithInput<ExportPlotOptions>()
{
public void execute(ExportPlotOptions options)
{
UIPrefs uiPrefs = pUIPrefs_.get();
if (!ExportPlotOptions.areEqual(
options,
uiPrefs.exportViewerOptions().getValue()))
{
uiPrefs.exportViewerOptions().setGlobalValue(options);
uiPrefs.writeUIPrefs();
}
}
};
private native void initializeEvents() /*-{
var thiz = this;
$wnd.addEventListener(
"message",
$entry(function(e) {
thiz.@org.rstudio.studio.client.workbench.views.viewer.ViewerPresenter::onMessage(Ljava/lang/String;Ljava/lang/String;)(e.data, e.origin);
}),
true);
}-*/;
private void onMessage(String data, String origin)
{
if ("disconnected".equals(data))
{
// ensure the frame url starts with the specified origin
if (display_.getUrl().startsWith(origin))
onViewerClear();
}
}
private String normalizeUrl(String url)
{
if (url.endsWith("/"))
return url.substring(0, url.length()-1);
else
return url;
}
private final Display display_ ;
private final Commands commands_;
private final GlobalDisplay globalDisplay_;
private final WorkbenchContext workbenchContext_;
private final ViewerServerOperations server_;
private final EventBus events_;
private final DependencyManager dependencyManager_;
private final FileDialogs fileDialogs_;
private final RemoteFileSystemContext fileSystemContext_;
private final Provider<UIPrefs> pUIPrefs_;
private final SourceShim sourceShim_;
private FileSystemItem saveAsWebPageDefaultPath_ = null;
private ShinyApplicationParams runningShinyAppParams_;
private RmdPreviewParams rmdPreviewParams_;
private WindowEx zoomWindow_ = null;
private Size zoomWindowDefaultSize_ = null;
}