예제 #1
0
@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();
}
예제 #2
0
/** 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();
}