@Singleton public class ApplicationQuit implements SaveActionChangedHandler, HandleUnsavedChangesHandler, SuspendAndRestartHandler { public interface Binder extends CommandBinder<Commands, ApplicationQuit> {} @Inject public ApplicationQuit( ApplicationServerOperations server, GlobalDisplay globalDisplay, EventBus eventBus, WorkbenchContext workbenchContext, SourceShim sourceShim, Provider<UIPrefs> pUiPrefs, Commands commands, Binder binder) { // save references server_ = server; globalDisplay_ = globalDisplay; eventBus_ = eventBus; workbenchContext_ = workbenchContext; sourceShim_ = sourceShim; pUiPrefs_ = pUiPrefs; // bind to commands binder.bind(commands, this); // subscribe to events eventBus.addHandler(SaveActionChangedEvent.TYPE, this); eventBus.addHandler(HandleUnsavedChangesEvent.TYPE, this); eventBus.addHandler(SuspendAndRestartEvent.TYPE, this); } // notification that we are ready to quit public interface QuitContext { void onReadyToQuit(boolean saveChanges); } public void prepareForQuit(final String caption, final QuitContext quitContext) { if (workbenchContext_.isServerBusy()) { globalDisplay_.showYesNoMessage( MessageDialog.QUESTION, caption, "The R session is currently busy. Are you sure you want to quit?", new Operation() { @Override public void execute() { handleUnsavedChanges(caption, quitContext); } }, true); } else { // if we aren't restoring source documents then close them all now if (!pUiPrefs_.get().restoreSourceDocuments().getValue()) { sourceShim_.closeAllSourceDocs( caption, new Command() { @Override public void execute() { handleUnsavedChanges(caption, quitContext); } }); } else { handleUnsavedChanges(caption, quitContext); } } } public static boolean isQuitSession() { return ("1".equals(Window.Location.getParameter("quit"))); } private void handleUnsavedChanges(String caption, QuitContext quitContext) { handleUnsavedChanges( saveAction_.getAction(), caption, sourceShim_, workbenchContext_, globalEnvTarget_, quitContext); } public static void handleUnsavedChanges( final int saveAction, String caption, final SourceShim sourceShim, final WorkbenchContext workbenchContext, final UnsavedChangesTarget globalEnvTarget, final QuitContext quitContext) { // see what the unsaved changes situation is and prompt accordingly ArrayList<UnsavedChangesTarget> unsavedSourceDocs = sourceShim.getUnsavedChanges(); // no unsaved changes at all if (saveAction != SaveAction.SAVEASK && unsavedSourceDocs.size() == 0) { // define quit operation final Operation quitOperation = new Operation() { public void execute() { quitContext.onReadyToQuit(saveAction == SaveAction.SAVE); } }; // if this is a quit session then we always prompt if (isQuitSession()) { RStudioGinjector.INSTANCE .getGlobalDisplay() .showYesNoMessage( MessageDialog.QUESTION, caption, "Are you sure you want to quit the R session?", quitOperation, true); } else { quitOperation.execute(); } return; } // just an unsaved environment if (unsavedSourceDocs.size() == 0 && workbenchContext != null) { // confirm quit and do it String prompt = "Save workspace image to " + workbenchContext.getREnvironmentPath() + "?"; RStudioGinjector.INSTANCE .getGlobalDisplay() .showYesNoMessage( GlobalDisplay.MSG_QUESTION, caption, prompt, true, new Operation() { public void execute() { quitContext.onReadyToQuit(true); } }, new Operation() { public void execute() { quitContext.onReadyToQuit(false); } }, new Operation() { public void execute() {} }, "Save", "Don't Save", true); } // a single unsaved document (can be any document in desktop mode, but // must be from the main window in web mode) else if (saveAction != SaveAction.SAVEASK && unsavedSourceDocs.size() == 1 && (Desktop.isDesktop() || !(unsavedSourceDocs.get(0) instanceof UnsavedChangesItem))) { sourceShim.saveWithPrompt( unsavedSourceDocs.get(0), sourceShim.revertUnsavedChangesBeforeExitCommand( new Command() { @Override public void execute() { quitContext.onReadyToQuit(saveAction == SaveAction.SAVE); } }), null); } // multiple save targets else { ArrayList<UnsavedChangesTarget> unsaved = new ArrayList<UnsavedChangesTarget>(); if (saveAction == SaveAction.SAVEASK && globalEnvTarget != null) unsaved.add(globalEnvTarget); unsaved.addAll(unsavedSourceDocs); new UnsavedChangesDialog( caption, unsaved, new OperationWithInput<UnsavedChangesDialog.Result>() { @Override public void execute(Result result) { ArrayList<UnsavedChangesTarget> saveTargets = result.getSaveTargets(); // remote global env target from list (if specified) and // compute the saveChanges value boolean saveGlobalEnv = saveAction == SaveAction.SAVE; if (saveAction == SaveAction.SAVEASK && globalEnvTarget != null) saveGlobalEnv = saveTargets.remove(globalEnvTarget); final boolean saveChanges = saveGlobalEnv; // save specified documents and then quit sourceShim.handleUnsavedChangesBeforeExit( saveTargets, new Command() { @Override public void execute() { quitContext.onReadyToQuit(saveChanges); } }); } }, // no cancel operation null) .showModal(); } } public void performQuit(boolean saveChanges) { performQuit(saveChanges, null, null); } public void performQuit(boolean saveChanges, String switchToProject) { performQuit(saveChanges, switchToProject, null); } public void performQuit( boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion) { performQuit(null, saveChanges, switchToProject, switchToRVersion); } public void performQuit( String progressMessage, boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion) { performQuit(progressMessage, saveChanges, switchToProject, switchToRVersion, null); } public void performQuit( String progressMessage, boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion, Command onQuitAcknowledged) { new QuitCommand( progressMessage, saveChanges, switchToProject, switchToRVersion, onQuitAcknowledged) .execute(); } @Override public void onSaveActionChanged(SaveActionChangedEvent event) { saveAction_ = event.getAction(); } @Override public void onHandleUnsavedChanges(HandleUnsavedChangesEvent event) { // command which will be used to callback the server class HandleUnsavedCommand implements Command { public HandleUnsavedCommand(boolean handled) { handled_ = handled; } @Override public void execute() { // this codepath is for when the user quits R using the q() // function -- in this case our standard client quit codepath // isn't invoked, and as a result the desktop is not notified // that there is a pending quit (so thinks R has crashed when // the process exits). since this codepath is only for the quit // case (and not the restart or restart and reload cases) // we can set the pending quit bit here if (Desktop.isDesktop()) { Desktop.getFrame().setPendingQuit(DesktopFrame.PENDING_QUIT_AND_EXIT); } server_.handleUnsavedChangesCompleted(handled_, new VoidServerRequestCallback()); } private final boolean handled_; }; // get unsaved source docs ArrayList<UnsavedChangesTarget> unsavedSourceDocs = sourceShim_.getUnsavedChanges(); if (unsavedSourceDocs.size() == 1) { sourceShim_.saveWithPrompt( unsavedSourceDocs.get(0), sourceShim_.revertUnsavedChangesBeforeExitCommand(new HandleUnsavedCommand(true)), new HandleUnsavedCommand(false)); } else if (unsavedSourceDocs.size() > 1) { new UnsavedChangesDialog( "Quit R Session", unsavedSourceDocs, new OperationWithInput<UnsavedChangesDialog.Result>() { @Override public void execute(Result result) { // save specified documents and then quit sourceShim_.handleUnsavedChangesBeforeExit( result.getSaveTargets(), new HandleUnsavedCommand(true)); } }, new HandleUnsavedCommand(false)) .showModal(); } else { new HandleUnsavedCommand(true).execute(); } } @Handler public void onRestartR() { boolean saveChanges = saveAction_.getAction() != SaveAction.NOSAVE; eventBus_.fireEvent( new SuspendAndRestartEvent(SuspendOptions.createSaveMinimal(saveChanges), null)); } @Override public void onSuspendAndRestart(final SuspendAndRestartEvent event) { // set restart pending for desktop setPendinqQuit(DesktopFrame.PENDING_QUIT_AND_RESTART); ProgressIndicator progress = new GlobalProgressDelayer(globalDisplay_, 200, "Restarting R...").getIndicator(); // perform the suspend and restart eventBus_.fireEvent(new RestartStatusEvent(RestartStatusEvent.RESTART_INITIATED)); server_.suspendForRestart( event.getSuspendOptions(), new VoidServerRequestCallback(progress) { @Override protected void onSuccess() { // send pings until the server restarts sendPing( event.getAfterRestartCommand(), 200, 25, new Command() { @Override public void execute() { eventBus_.fireEvent( new RestartStatusEvent(RestartStatusEvent.RESTART_COMPLETED)); } }); } @Override protected void onFailure() { eventBus_.fireEvent(new RestartStatusEvent(RestartStatusEvent.RESTART_COMPLETED)); setPendinqQuit(DesktopFrame.PENDING_QUIT_NONE); } }); } private void setPendinqQuit(int pendingQuit) { if (Desktop.isDesktop()) Desktop.getFrame().setPendingQuit(pendingQuit); } private void sendPing( final String afterRestartCommand, int delayMs, final int maxRetries, final Command onCompleted) { Scheduler.get() .scheduleFixedDelay( new RepeatingCommand() { private int retries_ = 0; private boolean pingDelivered_ = false; private boolean pingInFlight_ = false; @Override public boolean execute() { // if we've already delivered the ping or our retry count // is exhausted then return false if (pingDelivered_ || (++retries_ > maxRetries)) return false; if (!pingInFlight_) { pingInFlight_ = true; server_.ping( new VoidServerRequestCallback() { @Override protected void onSuccess() { pingInFlight_ = false; if (!pingDelivered_) { pingDelivered_ = true; // issue after restart command if (!StringUtil.isNullOrEmpty(afterRestartCommand)) { eventBus_.fireEvent( new SendToConsoleEvent(afterRestartCommand, true, true)); } // otherwise make sure the console knows we // restarted (ensure prompt and set focus) else { eventBus_.fireEvent(new ConsoleRestartRCompletedEvent()); } } if (onCompleted != null) onCompleted.execute(); } @Override protected void onFailure() { pingInFlight_ = false; if (onCompleted != null) onCompleted.execute(); } }); } // keep trying until the ping is delivered return true; } }, delayMs); } @Handler public void onQuitSession() { prepareForQuit( "Quit R Session", new QuitContext() { public void onReadyToQuit(boolean saveChanges) { performQuit(saveChanges); } }); } private UnsavedChangesTarget globalEnvTarget_ = new UnsavedChangesTarget() { @Override public String getId() { return "F59C8727-3C63-41F4-989C-B1E1D47760E3"; } @Override public ImageResource getIcon() { return FileIconResources.INSTANCE.iconRdata(); } @Override public String getTitle() { return "Workspace image (.RData)"; } @Override public String getPath() { return workbenchContext_.getREnvironmentPath(); } }; private String buildSwitchMessage(String switchToProject) { String msg = !switchToProject.equals("none") ? "Switching to project " + FileSystemItem.createFile(switchToProject).getParentPathString() : "Closing project"; return msg + "..."; } private class QuitCommand implements Command { public QuitCommand( String progressMessage, boolean saveChanges, String switchToProject, RVersionSpec switchToRVersion, Command onQuitAcknowledged) { progressMessage_ = progressMessage; saveChanges_ = saveChanges; switchToProject_ = switchToProject; switchToRVersion_ = switchToRVersion; onQuitAcknowledged_ = onQuitAcknowledged; } public void execute() { // show delayed progress String msg = progressMessage_; if (msg == null) { msg = switchToProject_ != null ? buildSwitchMessage(switchToProject_) : "Quitting R Session..."; } final GlobalProgressDelayer progress = new GlobalProgressDelayer(globalDisplay_, 250, msg); // Use a barrier and LastChanceSaveEvent to allow source documents // and client state to be synchronized before quitting. Barrier barrier = new Barrier(); barrier.addBarrierReleasedHandler( new BarrierReleasedHandler() { public void onBarrierReleased(BarrierReleasedEvent event) { // All last chance save operations have completed (or possibly // failed). Now do the real quit. // notify the desktop frame that we are about to quit String switchToProject = new String(switchToProject_); if (Desktop.isDesktop()) { if (Desktop.getFrame().isCocoa() && switchToProject_ != null) { // on Cocoa there's an ugly intermittent crash that occurs // when we reload, so exit this instance and start a new // one when switching projects Desktop.getFrame().setPendingProject(switchToProject_); // Since we're going to be starting a new process we don't // want to pass a switchToProject argument to quitSession switchToProject = null; } else { Desktop.getFrame() .setPendingQuit( switchToProject_ != null ? DesktopFrame.PENDING_QUIT_RESTART_AND_RELOAD : DesktopFrame.PENDING_QUIT_AND_EXIT); } } server_.quitSession( saveChanges_, switchToProject, switchToRVersion_, GWT.getHostPageBaseURL(), new ServerRequestCallback<Boolean>() { @Override public void onResponseReceived(Boolean response) { if (response) { // clear progress only if we aren't switching projects // (otherwise we want to leave progress up until // the app reloads) if (switchToProject_ == null) progress.dismiss(); // fire onQuitAcknowledged if (onQuitAcknowledged_ != null) onQuitAcknowledged_.execute(); } else { onFailedToQuit(); } } @Override public void onError(ServerError error) { onFailedToQuit(); } private void onFailedToQuit() { progress.dismiss(); if (Desktop.isDesktop()) { Desktop.getFrame().setPendingQuit(DesktopFrame.PENDING_QUIT_NONE); } } }); } }); // We acquire a token to make sure that the barrier doesn't fire before // all the LastChanceSaveEvent listeners get a chance to acquire their // own tokens. Token token = barrier.acquire(); try { eventBus_.fireEvent(new LastChanceSaveEvent(barrier)); } finally { token.release(); } } private final boolean saveChanges_; private final String switchToProject_; private final RVersionSpec switchToRVersion_; private final String progressMessage_; private final Command onQuitAcknowledged_; }; private final ApplicationServerOperations server_; private final GlobalDisplay globalDisplay_; private final Provider<UIPrefs> pUiPrefs_; private final EventBus eventBus_; private final WorkbenchContext workbenchContext_; private final SourceShim sourceShim_; private SaveAction saveAction_ = SaveAction.saveAsk(); }
/** Any methods on this class are automatically made available to the Qt frame code. */ public class DesktopHooks { @BaseExpression("$wnd.desktopHooks") interface DesktopHooksInjector extends JsObjectInjector<DesktopHooks> {} private static final DesktopHooksInjector injector = GWT.create(DesktopHooksInjector.class); /** Delay showing progress until DELAY_MILLIS have elapsed. */ private class ProgressDelayer { private ProgressDelayer(final String progressMessage) { final int DELAY_MILLIS = 250; timer_ = new Timer() { @Override public void run() { dismiss_ = globalDisplay_.showProgress(progressMessage); } }; timer_.schedule(DELAY_MILLIS); } public void dismiss() { timer_.cancel(); if (dismiss_ != null) dismiss_.execute(); } private final Timer timer_; private Command dismiss_; } @Inject public DesktopHooks( Commands commands, EventBus events, GlobalDisplay globalDisplay, Server server, FileTypeRegistry fileTypeRegistry, WorkbenchContext workbenchContext) { commands_ = commands; events_ = events; globalDisplay_ = globalDisplay; server_ = server; fileTypeRegistry_ = fileTypeRegistry; workbenchContext_ = workbenchContext; events_.addHandler( SaveActionChangedEvent.TYPE, new SaveActionChangedHandler() { public void onSaveActionChanged(SaveActionChangedEvent event) { saveAction_ = event.getAction(); } }); injector.injectObject(this); addCopyHook(); } private native void addCopyHook() /*-{ var clean = function() { setTimeout(function() { $wnd.desktop.cleanClipboard(false); }, 100) }; $wnd.addEventListener("copy", clean, true); $wnd.addEventListener("cut", clean, true); }-*/; void quitR(final boolean saveChanges) { final ProgressDelayer progress = new ProgressDelayer("Quitting R Session..."); // Use a barrier and LastChanceSaveEvent to allow source documents // and client state to be synchronized before quitting. Barrier barrier = new Barrier(); barrier.addBarrierReleasedHandler( new BarrierReleasedHandler() { public void onBarrierReleased(BarrierReleasedEvent event) { // All last chance save operations have completed (or possibly // failed). Now do the real quit. server_.quitSession( saveChanges, new VoidServerRequestCallback( globalDisplay_.getProgressIndicator("Error Quitting R")) { @Override public void onResponseReceived(org.rstudio.studio.client.server.Void response) { progress.dismiss(); super.onResponseReceived(response); } @Override public void onError(ServerError error) { progress.dismiss(); super.onError(error); } }); } }); // We acquire a token to make sure that the barrier doesn't fire before // all the LastChanceSaveEvent listeners get a chance to acquire their // own tokens. Token token = barrier.acquire(); try { events_.fireEvent(new LastChanceSaveEvent(barrier)); } finally { token.release(); } } void invokeCommand(String cmdId) { commands_.getCommandById(cmdId).execute(); } boolean isCommandVisible(String commandId) { AppCommand command = commands_.getCommandById(commandId); return command != null && command.isVisible(); } boolean isCommandEnabled(String commandId) { AppCommand command = commands_.getCommandById(commandId); return command != null && command.isEnabled(); } String getCommandLabel(String commandId) { AppCommand command = commands_.getCommandById(commandId); return command != null ? command.getMenuLabel(true) : ""; } void openFile(String filePath) { // get the file system item FileSystemItem file = FileSystemItem.createFile(filePath); // don't open directories (these can sneak in if the user // passes a directory on the command line) if (!file.isDirectory()) { // open the file. pass false for second param to prevent // the default handler (the browser) from taking it fileTypeRegistry_.openFile(file, false); } } int getSaveAction() { return saveAction_.getAction(); } String getREnvironmentPath() { return workbenchContext_.getREnvironmentPath(); } private final Commands commands_; private final EventBus events_; private final GlobalDisplay globalDisplay_; private final Server server_; private final FileTypeRegistry fileTypeRegistry_; private final WorkbenchContext workbenchContext_; private SaveAction saveAction_ = SaveAction.saveAsk(); }