/*
* Application.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.application;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.RunAsyncCallback;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootLayoutPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.rstudio.core.client.Barrier;
import org.rstudio.core.client.BrowseCap;
import org.rstudio.core.client.Debug;
import org.rstudio.core.client.Barrier.Token;
import org.rstudio.core.client.command.CommandBinder;
import org.rstudio.core.client.command.Handler;
import org.rstudio.core.client.dom.DomUtils;
import org.rstudio.core.client.events.BarrierReleasedEvent;
import org.rstudio.core.client.events.BarrierReleasedHandler;
import org.rstudio.core.client.widget.Operation;
import org.rstudio.studio.client.application.events.*;
import org.rstudio.studio.client.application.model.ProductInfo;
import org.rstudio.studio.client.application.model.SessionSerializationAction;
import org.rstudio.studio.client.application.ui.AboutDialog;
import org.rstudio.studio.client.application.ui.RequestLogVisualization;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.SimpleRequestCallback;
import org.rstudio.studio.client.common.SuperDevMode;
import org.rstudio.studio.client.common.satellite.SatelliteManager;
import org.rstudio.studio.client.projects.Projects;
import org.rstudio.studio.client.server.*;
import org.rstudio.studio.client.workbench.ClientStateUpdater;
import org.rstudio.studio.client.workbench.Workbench;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.events.LastChanceSaveEvent;
import org.rstudio.studio.client.workbench.events.SessionInitEvent;
import org.rstudio.studio.client.workbench.model.Agreement;
import org.rstudio.studio.client.workbench.model.Session;
import org.rstudio.studio.client.workbench.model.SessionInfo;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.views.source.editors.text.themes.AceThemes;
@Singleton
public class Application implements ApplicationEventHandlers
{
public interface Binder extends CommandBinder<Commands, Application> {}
@Inject
public Application(ApplicationView view,
GlobalDisplay globalDisplay,
EventBus events,
Binder binder,
Commands commands,
Server server,
Session session,
Projects projects,
SatelliteManager satelliteManager,
ApplicationUncaughtExceptionHandler uncaughtExHandler,
Provider<UIPrefs> uiPrefs,
Provider<Workbench> workbench,
Provider<EventBus> eventBusProvider,
Provider<ClientStateUpdater> clientStateUpdater,
Provider<ApplicationClientInit> pClientInit,
Provider<ApplicationQuit> pApplicationQuit,
Provider<ApplicationInterrupt> pApplicationInterrupt,
Provider<AceThemes> pAceThemes)
{
// save references
view_ = view ;
globalDisplay_ = globalDisplay;
events_ = events;
session_ = session;
commands_ = commands;
satelliteManager_ = satelliteManager;
clientStateUpdater_ = clientStateUpdater;
server_ = server;
uiPrefs_ = uiPrefs;
workbench_ = workbench;
eventBusProvider_ = eventBusProvider;
pClientInit_ = pClientInit;
pApplicationQuit_ = pApplicationQuit;
pApplicationInterrupt_ = pApplicationInterrupt;
pAceThemes_ = pAceThemes;
// bind to commands
binder.bind(commands_, this);
// register as main window
satelliteManager.initialize();
// subscribe to events
events.addHandler(LogoutRequestedEvent.TYPE, this);
events.addHandler(UnauthorizedEvent.TYPE, this);
events.addHandler(ReloadEvent.TYPE, this);
events.addHandler(QuitEvent.TYPE, this);
events.addHandler(SuicideEvent.TYPE, this);
events.addHandler(SessionAbendWarningEvent.TYPE, this);
events.addHandler(SessionSerializationEvent.TYPE, this);
events.addHandler(ServerUnavailableEvent.TYPE, this);
events.addHandler(InvalidClientVersionEvent.TYPE, this);
events.addHandler(ServerOfflineEvent.TYPE, this);
// register for uncaught exceptions
uncaughtExHandler.register();
}
public void go(final RootLayoutPanel rootPanel,
final Command dismissLoadingProgress)
{
Widget w = view_.getWidget();
rootPanel.add(w);
rootPanel.setWidgetTopBottom(w, 0, Style.Unit.PX, 0, Style.Unit.PX);
rootPanel.setWidgetLeftRight(w, 0, Style.Unit.PX, 0, Style.Unit.PX);
// attempt init
pClientInit_.get().execute(
new ServerRequestCallback<SessionInfo>() {
public void onResponseReceived(final SessionInfo sessionInfo)
{
// initialize workbench after verifying agreement
verifyAgreement(sessionInfo, new Operation() {
public void execute()
{
dismissLoadingProgress.execute();
session_.setSessionInfo(sessionInfo);
// hide the workbench if we have a project parameter
// (since we are going to redirect anyway)
if (haveProjectParameter())
hideWorkbench(rootPanel);
// initialize workbench
initializeWorkbench();
// reload application if we have a project parameter
if (haveProjectParameter())
reloadApplication(sessionInfo.getSwitchToProject());
}
});
}
public void onError(ServerError error)
{
Debug.logError(error);
dismissLoadingProgress.execute();
globalDisplay_.showErrorMessage("RStudio Initialization Error",
error.getUserMessage());
}
}) ;
}
private void reloadApplication(final String switchToProject)
{
// use a last chance save barrier since we typically call this very
// early in the lifetime of the application before client/server
// sync has occurred
Barrier barrier = new Barrier();
barrier.addBarrierReleasedHandler(new BarrierReleasedHandler() {
@Override
public void onBarrierReleased(BarrierReleasedEvent event)
{
if (switchToProject.length() > 0)
pApplicationQuit_.get().forceSwitchProject(switchToProject);
else
reloadWindowWithDelay(true);
}
});
Token token = barrier.acquire();
try
{
events_.fireEvent(new LastChanceSaveEvent(barrier));
}
finally
{
token.release();
}
}
@Handler
public void onShowToolbar()
{
setToolbarPref(true);
}
@Handler
public void onHideToolbar()
{
setToolbarPref(false);
}
@Handler
void onShowAboutDialog()
{
server_.getProductInfo(new ServerRequestCallback<ProductInfo>()
{
@Override
public void onResponseReceived(ProductInfo info)
{
AboutDialog about = new AboutDialog(info);
about.showModal();
}
@Override
public void onError(ServerError error)
{
}
});
}
@Handler
public void onGoToFileFunction()
{
view_.performGoToFunction();
}
public void onUnauthorized(UnauthorizedEvent event)
{
navigateToSignIn();
}
public void onServerOffline(ServerOfflineEvent event)
{
cleanupWorkbench();
view_.showApplicationOffline();
}
public void onLogoutRequested(LogoutRequestedEvent event)
{
navigateWindowTo("auth-sign-out");
}
@Handler
public void onHelpUsingRStudio()
{
String customDocsURL = session_.getSessionInfo().docsURL();
if (customDocsURL.length() > 0)
globalDisplay_.openWindow(customDocsURL);
else
globalDisplay_.openRStudioLink("docs");
}
private void showAgreement()
{
globalDisplay_.openWindow(server_.getApplicationURL("agreement"));
}
@Handler
public void onRstudioSupport()
{
globalDisplay_.openRStudioLink("support");
}
@Handler
public void onRstudioAgreement()
{
showAgreement();
}
@Handler
public void onUpdateCredentials()
{
server_.updateCredentials();
}
@Handler
public void onRaiseException() {
throw new RuntimeException("foo");
}
@Handler
public final native void onRaiseException2() /*-{
$wnd.welfkjweg();
}-*/;
@Handler
public void onShowRequestLog()
{
GWT.runAsync(new RunAsyncCallback()
{
public void onFailure(Throwable reason)
{
Window.alert(reason.toString());
}
public void onSuccess()
{
final RequestLogVisualization viz = new RequestLogVisualization();
final RootLayoutPanel root = RootLayoutPanel.get();
root.add(viz);
root.setWidgetTopBottom(viz, 10, Unit.PX, 10, Unit.PX);
root.setWidgetLeftRight(viz, 10, Unit.PX, 10, Unit.PX);
viz.addCloseHandler(new CloseHandler<RequestLogVisualization>()
{
public void onClose(CloseEvent<RequestLogVisualization> event)
{
root.remove(viz);
}
});
}
});
}
@Handler
public void onLogFocusedElement()
{
Element el = DomUtils.getActiveElement();
DomUtils.dump(el, "Focused Element: ");
}
@Handler
public void onRefreshSuperDevMode()
{
SuperDevMode.reload();
}
@Handler
public void onZoomActualSize()
{
// only supported in cocoa desktop
if (BrowseCap.isCocoaDesktop())
Desktop.getFrame().macZoomActualSize();
}
@Handler
public void onZoomIn()
{
// pass on to cocoa desktop (qt desktop intercepts)
if (BrowseCap.isCocoaDesktop())
Desktop.getFrame().macZoomIn();
}
@Handler
public void onZoomOut()
{
// pass on to cocoa desktop (qt desktop intercepts)
if (BrowseCap.isCocoaDesktop())
Desktop.getFrame().macZoomOut();
}
public void onSessionSerialization(SessionSerializationEvent event)
{
switch(event.getAction().getType())
{
case SessionSerializationAction.LOAD_DEFAULT_WORKSPACE:
view_.showSerializationProgress(
"Loading workspace" + getSuffix(event),
false, // non-modal, appears to user as std latency
500, // willing to show progress earlier since
// this will always be at workbench startup
0); // no timeout
break;
case SessionSerializationAction.SAVE_DEFAULT_WORKSPACE:
view_.showSerializationProgress(
"Saving workspace image" + getSuffix(event),
true, // modal, inputs will fall dead anyway
0, // show immediately
0); // no timeout
break;
case SessionSerializationAction.SUSPEND_SESSION:
view_.showSerializationProgress(
"Backing up R session...",
true, // modal, inputs will fall dead anyway
0, // show immediately
60000); // timeout after 60 seconds. this is done
// in case the user suspends or loses
// connectivity during the backup (in which
// case the 'completed' event dies with
// server and is never received by the client
break;
case SessionSerializationAction.RESUME_SESSION:
view_.showSerializationProgress(
"Resuming R session...",
false, // non-modal, appears to user as std latency
2000, // don't show this for reasonable restore time
// (happens inline while using a running
// workbench so be more conservative)
0); // no timeout
break;
case SessionSerializationAction.COMPLETED:
view_.hideSerializationProgress();
break;
}
}
private String getSuffix(SessionSerializationEvent event)
{
SessionSerializationAction action = event.getAction();
String targetPath = action.getTargetPath();
if (targetPath != null)
{
String verb = " from ";
if (action.getType() == SessionSerializationAction.SAVE_DEFAULT_WORKSPACE)
verb = " to ";
return verb + targetPath + "...";
}
else
{
return "...";
}
}
public void onServerUnavailable(ServerUnavailableEvent event)
{
view_.hideSerializationProgress();
}
public void onReload(ReloadEvent event)
{
cleanupWorkbench();
reloadWindowWithDelay(false);
}
public void onQuit(QuitEvent event)
{
cleanupWorkbench();
// only show the quit state in server mode (desktop mode has its
// own handling triggered to process exit)
if (!Desktop.isDesktop())
{
// if we are switching projects then reload after a delay (to allow
// the R session to fully exit on the server)
if (event.getSwitchProjects())
{
reloadWindowWithDelay(true);
}
else
{
view_.showApplicationQuit();
}
}
}
private void reloadWindowWithDelay(final boolean baseUrlOnly)
{
new Timer() {
@Override
public void run()
{
if (baseUrlOnly)
Window.Location.replace(GWT.getHostPageBaseURL());
else
Window.Location.reload();
}
}.schedule(100);
}
public void onSuicide(SuicideEvent event)
{
cleanupWorkbench();
view_.showApplicationSuicide(event.getMessage());
}
public void onClientDisconnected(ClientDisconnectedEvent event)
{
cleanupWorkbench();
view_.showApplicationDisconnected();
}
public void onInvalidClientVersion(InvalidClientVersionEvent event)
{
cleanupWorkbench();
view_.showApplicationUpdateRequired();
}
public void onSessionAbendWarning(SessionAbendWarningEvent event)
{
view_.showSessionAbendWarning();
}
private void verifyAgreement(SessionInfo sessionInfo,
final Operation verifiedOperation)
{
// get the agreement (if any)
final Agreement agreement = sessionInfo.pendingAgreement();
// if there is an agreement then prompt user for agreement (otherwise just
// execute the verifiedOperation immediately)
if (agreement != null)
{
// append updated to the title if necessary
String title = agreement.getTitle();
if (agreement.getUpdated())
title += " (Updated)";
view_.showApplicationAgreement(
// title and contents
title,
agreement.getContents(),
// bail to sign in page if the user doesn't confirm
new Operation()
{
public void execute()
{
if (Desktop.isDesktop())
{
Desktop.getFrame().setPendingQuit(
DesktopFrame.PENDING_QUIT_AND_EXIT);
server_.quitSession(false,
null,
new SimpleRequestCallback<Boolean>());
}
else
navigateToSignIn();
}
},
// user confirmed
new Operation() {
public void execute()
{
// call verified operation
verifiedOperation.execute();
// record agreement on server
server_.acceptAgreement(agreement,
new VoidServerRequestCallback());
}
}
);
}
else
{
// no agreement pending
verifiedOperation.execute();
}
}
private void navigateWindowTo(String relativeUrl)
{
cleanupWorkbench();
String url = GWT.getHostPageBaseURL() + relativeUrl;
Window.Location.replace(url);
}
private void initializeWorkbench()
{
pAceThemes_.get();
// subscribe to ClientDisconnected event (wait to do this until here
// because there were spurious ClientDisconnected events occuring
// after a session interrupt sequence. we couldn't figure out why,
// and since this is a temporary hack why not add another temporary
// hack to go with it here :-)
// TOOD: move this back tot he constructor after we revise the
// interrupt hack(s)
events_.addHandler(ClientDisconnectedEvent.TYPE, this);
// create workbench
Workbench wb = workbench_.get();
eventBusProvider_.get().fireEvent(new SessionInitEvent()) ;
// disable commands
SessionInfo sessionInfo = session_.getSessionInfo();
if (!sessionInfo.getAllowShell())
{
commands_.showShellDialog().remove();
}
if (!sessionInfo.getAllowPackageInstallation())
{
commands_.installPackage().remove();
commands_.updatePackages().remove();
}
if (!sessionInfo.getAllowVcs())
{
commands_.versionControlProjectSetup().remove();
}
if (!sessionInfo.getAllowFileDownloads())
{
commands_.exportFiles().remove();
}
// disable rpubs if requested
if (!sessionInfo.getAllowRpubsPublish())
{
commands_.publishHTML().remove();
commands_.publishPlotToRPubs().remove();
commands_.presentationPublishToRpubs().remove();
commands_.viewerPublishToRPubs().remove();
}
// hide the agreement menu item if we don't have one
if (!session_.getSessionInfo().hasAgreement())
commands_.rstudioAgreement().setVisible(false);
// show workbench
view_.showWorkbenchView(wb.getMainView().asWidget());
// hide zoom actual size everywhere but cocoa desktop
if (!BrowseCap.isCocoaDesktop())
{
commands_.zoomActualSize().remove();
}
// hide zoom in and zoom out in web mode
if (!Desktop.isDesktop())
{
commands_.zoomIn().remove();
commands_.zoomOut().remove();
}
// toolbar (must be after call to showWorkbenchView because
// showing the toolbar repositions the workbench view widget)
showToolbar( uiPrefs_.get().toolbarVisible().getValue());
// sync to changes in the toolbar visibility state
uiPrefs_.get().toolbarVisible().addValueChangeHandler(
new ValueChangeHandler<Boolean>() {
@Override
public void onValueChange(ValueChangeEvent<Boolean> event)
{
showToolbar(event.getValue());
}
});
clientStateUpdaterInstance_ = clientStateUpdater_.get();
}
private void setToolbarPref(boolean showToolbar)
{
uiPrefs_.get().toolbarVisible().setGlobalValue(showToolbar);
uiPrefs_.get().writeUIPrefs();
}
private void showToolbar(boolean showToolbar)
{
// show or hide the toolbar
view_.showToolbar(showToolbar);
// manage commands
commands_.showToolbar().setVisible(!showToolbar);
commands_.hideToolbar().setVisible(showToolbar);
}
private void hideWorkbench(final RootLayoutPanel rootPanel)
{
final Label w = new Label();
w.getElement().getStyle().setBackgroundColor("#e1e2e5");
rootPanel.add(w);
rootPanel.setWidgetTopBottom(w, 0, Style.Unit.PX,
0, Style.Unit.PX);
rootPanel.setWidgetLeftRight(w, 0, Style.Unit.PX,
0, Style.Unit.PX);
}
private void cleanupWorkbench()
{
server_.disconnect();
satelliteManager_.closeAllSatellites();
if (clientStateUpdaterInstance_ != null)
{
clientStateUpdaterInstance_.suspend();
clientStateUpdaterInstance_ = null;
}
}
private void navigateToSignIn()
{
navigateWindowTo("auth-sign-in");
}
private boolean haveProjectParameter()
{
return Window.Location.getParameter("project") != null;
}
private final ApplicationView view_ ;
private final GlobalDisplay globalDisplay_ ;
private final EventBus events_;
private final Session session_;
private final Commands commands_;
private final SatelliteManager satelliteManager_;
private final Provider<ClientStateUpdater> clientStateUpdater_;
private final Server server_;
private final Provider<UIPrefs> uiPrefs_;
private final Provider<Workbench> workbench_;
private final Provider<EventBus> eventBusProvider_;
private final Provider<ApplicationClientInit> pClientInit_;
private final Provider<ApplicationQuit> pApplicationQuit_;
private final Provider<ApplicationInterrupt> pApplicationInterrupt_;
private final Provider<AceThemes> pAceThemes_;
private ClientStateUpdater clientStateUpdaterInstance_;
}