/*
* Plots.java
*
* Copyright (C) 2009-12 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.plots;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.HasResizeHandlers;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Panel;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.rstudio.core.client.BrowseCap;
import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.Point;
import org.rstudio.core.client.Size;
import org.rstudio.core.client.dom.WindowEx;
import org.rstudio.core.client.files.FileSystemItem;
import org.rstudio.core.client.widget.HasCustomizableToolbar;
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.application.events.DeferredInitCompletedEvent;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.application.Desktop;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.SimpleRequestCallback;
import org.rstudio.studio.client.common.dependencies.DependencyManager;
import org.rstudio.studio.client.common.rpubs.RPubsHtmlGenerator;
import org.rstudio.studio.client.common.rpubs.ui.RPubsUploadDialog;
import org.rstudio.studio.client.common.zoom.ZoomUtils;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.server.Void;
import org.rstudio.studio.client.server.VoidServerRequestCallback;
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.Session;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.views.BasePresenter;
import org.rstudio.studio.client.workbench.views.console.events.ConsolePromptEvent;
import org.rstudio.studio.client.workbench.views.console.events.ConsolePromptHandler;
import org.rstudio.studio.client.workbench.views.plots.events.LocatorEvent;
import org.rstudio.studio.client.workbench.views.plots.events.LocatorHandler;
import org.rstudio.studio.client.workbench.views.plots.events.PlotsChangedEvent;
import org.rstudio.studio.client.workbench.views.plots.events.PlotsChangedHandler;
import org.rstudio.studio.client.workbench.views.plots.events.PlotsZoomSizeChangedEvent;
import org.rstudio.studio.client.workbench.views.plots.model.PlotsServerOperations;
import org.rstudio.studio.client.workbench.views.plots.model.PlotsState;
import org.rstudio.studio.client.workbench.views.plots.model.SavePlotAsPdfOptions;
import org.rstudio.studio.client.workbench.views.plots.ui.export.ExportPlot;
import org.rstudio.studio.client.workbench.views.plots.ui.manipulator.ManipulatorChangedHandler;
import org.rstudio.studio.client.workbench.views.plots.ui.manipulator.ManipulatorManager;
public class Plots extends BasePresenter implements PlotsChangedHandler,
LocatorHandler,
ConsolePromptHandler,
DeferredInitCompletedEvent.Handler,
PlotsZoomSizeChangedEvent.Handler
{
public interface Parent extends HasWidgets, HasCustomizableToolbar
{
}
public interface Display extends WorkbenchView, HasResizeHandlers
{
void showEmptyPlot();
void showPlot(String plotUrl);
String getPlotUrl();
void refresh();
Panel getPlotsSurface();
Parent getPlotsParent();
Size getPlotFrameSize();
}
@Inject
public Plots(final Display view,
GlobalDisplay globalDisplay,
WorkbenchContext workbenchContext,
Provider<UIPrefs> uiPrefs,
Commands commands,
EventBus events,
DependencyManager dependencyManager,
final PlotsServerOperations server,
Session session)
{
super(view);
view_ = view;
globalDisplay_ = globalDisplay;
workbenchContext_ = workbenchContext;
uiPrefs_ = uiPrefs;
server_ = server;
session_ = session;
dependencyManager_ = dependencyManager;
exportPlot_ = GWT.create(ExportPlot.class);
zoomWindow_ = null;
zoomWindowDefaultSize_ = null;
locator_ = new Locator(view.getPlotsParent());
locator_.addSelectionHandler(new SelectionHandler<Point>()
{
public void onSelection(SelectionEvent<Point> e)
{
org.rstudio.studio.client.workbench.views.plots.model.Point p = null;
if (e.getSelectedItem() != null)
p = org.rstudio.studio.client.workbench.views.plots.model.Point.create(
e.getSelectedItem().getX(),
e.getSelectedItem().getY()
);
server.locatorCompleted(p, new SimpleRequestCallback<Void>());
}
});
// manipulator
manipulatorManager_ = new ManipulatorManager(
view_.getPlotsSurface(),
commands,
new ManipulatorChangedHandler()
{
@Override
public void onManipulatorChanged(JSONObject values)
{
server_.setManipulatorValues(values,
new ManipulatorRequestCallback());
}
},
new ClickHandler()
{
@Override
public void onClick(ClickEvent event)
{
server_.manipulatorPlotClicked(new Double(event.getX()).intValue(),
new Double(event.getY()).intValue(),
new ManipulatorRequestCallback());
}
}
);
events.addHandler(DeferredInitCompletedEvent.TYPE, this);
events.addHandler(PlotsZoomSizeChangedEvent.TYPE, this);
}
public void onPlotsChanged(PlotsChangedEvent event)
{
// get the event
PlotsState plotsState = event.getPlotsState();
// clear progress
view_.setProgress(false);
manipulatorManager_.setProgress(false);
// if this is the empty plot then clear the display
// NOTE: we currently return a zero byte PNG as our "empty.png" from
// the server. this is shown as a blank pane by Webkit, however
// firefox shows the full URI of the empty.png rather than a blank
// pane. therefore, we put in this workaround.
if (plotsState.getFilename().startsWith("empty."))
{
view_.showEmptyPlot();
}
else
{
String url = server_.getGraphicsUrl(plotsState.getFilename());
view_.showPlot(url);
}
// activate the plots tab if requested
if (plotsState.getActivatePlots())
view_.bringToFront();
// update plot size
plotSize_ = new Size(plotsState.getWidth(), plotsState.getHeight());
// manipulator
manipulatorManager_.setManipulator(plotsState.getManipulator(),
plotsState.getShowManipulator());
// locator
if (locator_.isActive())
locate();
// reload zoom window if we have one
if (Desktop.isDesktop())
Desktop.getFrame().reloadZoomWindow();
else if ((zoomWindow_ != null) && !zoomWindow_.isClosed())
zoomWindow_.reload();
}
void onNextPlot()
{
view_.bringToFront();
setChangePlotProgress();
server_.nextPlot(new PlotRequestCallback());
}
void onPreviousPlot()
{
view_.bringToFront();
setChangePlotProgress();
server_.previousPlot(new PlotRequestCallback());
}
void onRemovePlot()
{
// delete plot gesture indicates we are done with locator
safeClearLocator();
// confirm
globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_QUESTION,
"Remove Plot",
"Are you sure you want to remove the current plot?",
new ProgressOperation() {
public void execute(final ProgressIndicator indicator)
{
indicator.onProgress("Removing plot...");
server_.removePlot(new VoidServerRequestCallback(indicator));
}
},
true
);
view_.bringToFront();
}
void onClearPlots()
{
// clear plots gesture indicates we are done with locator
safeClearLocator();
// confirm
globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_QUESTION,
"Clear Plots",
"Are you sure you want to clear all of the plots in the history?",
new ProgressOperation() {
public void execute(final ProgressIndicator indicator)
{
indicator.onProgress("Clearing plots...");
server_.clearPlots(new VoidServerRequestCallback(indicator));
}
},
true
);
}
void onSavePlotAsImage()
{
view_.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_.getSavePlotContext(
defaultDir.getPath(),
new SimpleRequestCallback<SavePlotAsImageContext>() {
@Override
public void onResponseReceived(SavePlotAsImageContext context)
{
indicator.onCompleted();
exportPlot_.savePlotAsImage(globalDisplay_,
server_,
context,
ExportPlotOptions.adaptToSize(
uiPrefs_.get().exportPlotOptions().getValue(),
getPlotSize()),
saveExportOptionsOperation_);
}
@Override
public void onError(ServerError error)
{
indicator.onError(error.getUserMessage());
}
});
}
void onSavePlotAsPdf()
{
view_.bringToFront();
final ProgressIndicator indicator =
globalDisplay_.getProgressIndicator("Error");
indicator.onProgress("Preparing to export plot...");
// get the default directory
final FileSystemItem defaultDir = ExportPlotUtils.getDefaultSaveDirectory(
workbenchContext_.getCurrentWorkingDir());
// get context
server_.getUniqueSavePlotStem(
defaultDir.getPath(),
new SimpleRequestCallback<String>() {
@Override
public void onResponseReceived(String stem)
{
indicator.onCompleted();
Size size = getPlotSize();
final SavePlotAsPdfOptions currentOptions =
SavePlotAsPdfOptions.adaptToSize(
uiPrefs_.get().savePlotAsPdfOptions().getValue(),
pixelsToInches(size.width),
pixelsToInches(size.height));
exportPlot_.savePlotAsPdf(
globalDisplay_,
server_,
session_.getSessionInfo(),
defaultDir,
stem,
currentOptions,
new OperationWithInput<SavePlotAsPdfOptions>() {
@Override
public void execute(SavePlotAsPdfOptions options)
{
if (!SavePlotAsPdfOptions.areEqual(
options,
currentOptions))
{
UIPrefs prefs = uiPrefs_.get();
prefs.savePlotAsPdfOptions().setGlobalValue(options);
prefs.writeUIPrefs();
}
}
}) ;
}
@Override
public void onError(ServerError error)
{
indicator.onError(error.getUserMessage());
}
});
}
void onCopyPlotToClipboard()
{
view_.bringToFront();
exportPlot_.copyPlotToClipboard(
server_,
ExportPlotOptions.adaptToSize(
uiPrefs_.get().exportPlotOptions().getValue(),
getPlotSize()),
saveExportOptionsOperation_);
}
void onPublishPlotToRPubs()
{
dependencyManager_.withRMarkdown("Publishing to RPubs",
new Command() {
@Override
public void execute()
{
// determine the size (re-use the zoom window logic for this)
final Size size = ZoomUtils.getZoomedSize(view_.getPlotFrameSize(),
new Size(400, 350),
new Size(750, 600));
// show the dialog
RPubsUploadDialog dlg = new RPubsUploadDialog(
"Plots",
"",
new RPubsHtmlGenerator() {
@Override
public void generateRPubsHtml(
String title,
String comment,
final CommandWithArg<String> onCompleted)
{
server_.plotsCreateRPubsHtml(
title,
comment,
size.width,
size.height,
new SimpleRequestCallback<String>() {
@Override
public void onResponseReceived(String rpubsHtmlFile)
{
onCompleted.execute(rpubsHtmlFile);
}
});
}
},
false);
dlg.showModal();
}
});
}
private double pixelsToInches(int pixels)
{
return (double)pixels / 96.0;
}
private OperationWithInput<ExportPlotOptions> saveExportOptionsOperation_ =
new OperationWithInput<ExportPlotOptions>()
{
public void execute(ExportPlotOptions options)
{
UIPrefs uiPrefs = uiPrefs_.get();
if (!ExportPlotOptions.areEqual(
options,
uiPrefs.exportPlotOptions().getValue()))
{
uiPrefs.exportPlotOptions().setGlobalValue(options);
uiPrefs.writeUIPrefs();
}
}
};
void onZoomPlot()
{
Size windowSize = ZoomUtils.getZoomWindowSize(
view_.getPlotFrameSize(), zoomWindowDefaultSize_);
// determine whether we should scale (see comment in ImageFrame.onLoad
// for why we wouldn't want to scale)
int scale = 1;
if (Desktop.isDesktop() && BrowseCap.isMacintosh())
scale = 0;
// compose url string
String url = server_.getGraphicsUrl("plot_zoom?" +
"width=" + windowSize.width + "&" +
"height=" + windowSize.height + "&" +
"scale=" + scale);
// open the window
ZoomUtils.openZoomWindow(
"_rstudio_zoom",
url,
windowSize,
new OperationWithInput<WindowEx>() {
@Override
public void execute(WindowEx input)
{
zoomWindow_ = input;
}
}
);
}
void onRefreshPlot()
{
view_.bringToFront();
view_.setProgress(true);
server_.refreshPlot(new PlotRequestCallback());
}
@Override
public void onDeferredInitCompleted(DeferredInitCompletedEvent event)
{
boolean showErrors = !workbenchContext_.isRestartInProgress();
server_.refreshPlot(new PlotRequestCallback(showErrors));
}
void onShowManipulator()
{
manipulatorManager_.showManipulator();
}
public Display getView()
{
return view_;
}
private void safeClearLocator()
{
if (locator_.isActive())
{
server_.locatorCompleted(null, new SimpleRequestCallback<Void>() {
@Override
public void onError(ServerError error)
{
// ignore errors (this method is meant to be used "quietly"
// so that if the server has a problem with clearing
// locator state (e.g. because it has already exited the
// locator state) we don't bother the user with it. worst
// case if this fails then the user will see that the console
// is still pending the locator command and the Done and Esc
// gestures will still be available to clear the Locator
}
});
}
}
private void setChangePlotProgress()
{
if (!Desktop.isDesktop())
view_.setProgress(true);
}
private class PlotRequestCallback extends ServerRequestCallback<Void>
{
public PlotRequestCallback()
{
this(true);
}
public PlotRequestCallback(boolean showErrors)
{
showErrors_ = showErrors;
}
@Override
public void onResponseReceived(Void response)
{
// we don't clear the progress until the GraphicsOutput
// event is received (enables us to wait for rendering
// to complete before clearing progress)
}
@Override
public void onError(ServerError error)
{
view_.setProgress(false);
if (showErrors_)
{
globalDisplay_.showErrorMessage("Server Error",
error.getUserMessage());
}
}
private final boolean showErrors_;
}
public void onLocator(LocatorEvent event)
{
view_.bringToFront();
locate();
}
private void locate()
{
locator_.locate(view_.getPlotUrl(), getPlotSize());
}
public void onConsolePrompt(ConsolePromptEvent event)
{
locator_.clearDisplay();
}
@Override
public void onPlotsZoomSizeChanged(PlotsZoomSizeChangedEvent event)
{
zoomWindowDefaultSize_ = new Size(event.getWidth(), event.getHeight());
}
private Size getPlotSize()
{
// NOTE: the reason we capture the plotSize_ from the PlotChangedEvent
// is that the server can actually change the size of the plot
// (e.g. for CairoSVG the width and height must be multiples of 4)
// in order for locator to work properly we need to use this size
// rather than size of our current plot frame
if (plotSize_ != null) // first try to use the last size reported
return plotSize_ ;
else // then fallback to frame size
return view_.getPlotFrameSize();
}
private class ManipulatorRequestCallback extends ServerRequestCallback<Void>
{
public ManipulatorRequestCallback()
{
manipulatorManager_.setProgress(true);
}
@Override
public void onResponseReceived(Void response)
{
// we don't clear the progress until the GraphicsOutput
// event is received (enables us to wait for rendering
// to complete before clearing progress)
}
@Override
public void onError(ServerError error)
{
manipulatorManager_.setProgress(false);
globalDisplay_.showErrorMessage("Server Error",
error.getUserMessage());
}
}
private final Display view_;
private final GlobalDisplay globalDisplay_;
private final PlotsServerOperations server_;
private final WorkbenchContext workbenchContext_;
private final DependencyManager dependencyManager_;
private final Session session_;
private final Provider<UIPrefs> uiPrefs_;
private final Locator locator_;
private final ManipulatorManager manipulatorManager_;
private WindowEx zoomWindow_;
private Size zoomWindowDefaultSize_;
// export plot impl
private final ExportPlot exportPlot_ ;
// size of most recently rendered plot
Size plotSize_ = null;
}