1 /*
2  * RmdOutput.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.studio.client.rmarkdown;
16 
17 import java.util.Map;
18 import java.util.HashMap;
19 
20 import org.rstudio.core.client.CommandWithArg;
21 import org.rstudio.core.client.StringUtil;
22 import org.rstudio.core.client.command.CommandBinder;
23 import org.rstudio.core.client.dom.WindowEx;
24 import org.rstudio.core.client.files.FileSystemItem;
25 import org.rstudio.core.client.widget.Operation;
26 import org.rstudio.core.client.widget.OperationWithInput;
27 import org.rstudio.core.client.widget.ProgressIndicator;
28 import org.rstudio.core.client.widget.ProgressOperation;
29 import org.rstudio.studio.client.RStudioGinjector;
30 import org.rstudio.studio.client.application.Desktop;
31 import org.rstudio.studio.client.application.events.EventBus;
32 import org.rstudio.studio.client.application.events.QuitInitiatedEvent;
33 import org.rstudio.studio.client.application.events.RestartStatusEvent;
34 import org.rstudio.studio.client.common.GlobalDisplay;
35 import org.rstudio.studio.client.common.SimpleRequestCallback;
36 import org.rstudio.studio.client.common.filetypes.FileTypeRegistry;
37 import org.rstudio.studio.client.common.filetypes.TextFileType;
38 import org.rstudio.studio.client.common.viewfile.ViewFilePanel;
39 import org.rstudio.studio.client.pdfviewer.PDFViewer;
40 import org.rstudio.studio.client.rmarkdown.events.ConvertToShinyDocEvent;
41 import org.rstudio.studio.client.rmarkdown.events.PreviewRmdEvent;
42 import org.rstudio.studio.client.rmarkdown.events.RenderRmdEvent;
43 import org.rstudio.studio.client.rmarkdown.events.RenderRmdSourceEvent;
44 import org.rstudio.studio.client.rmarkdown.events.RmdRenderCompletedEvent;
45 import org.rstudio.studio.client.rmarkdown.events.RmdRenderStartedEvent;
46 import org.rstudio.studio.client.rmarkdown.events.RmdShinyDocStartedEvent;
47 import org.rstudio.studio.client.rmarkdown.events.RmdRenderPendingEvent;
48 import org.rstudio.studio.client.rmarkdown.events.WebsiteFileSavedEvent;
49 import org.rstudio.studio.client.rmarkdown.model.RMarkdownServerOperations;
50 import org.rstudio.studio.client.rmarkdown.model.RmdEditorOptions;
51 import org.rstudio.studio.client.rmarkdown.model.RmdOutputFormat;
52 import org.rstudio.studio.client.rmarkdown.model.RmdPreviewParams;
53 import org.rstudio.studio.client.rmarkdown.model.RmdRenderResult;
54 import org.rstudio.studio.client.rmarkdown.model.RmdShinyDocInfo;
55 import org.rstudio.studio.client.rmarkdown.ui.RmdOutputFrame;
56 import org.rstudio.studio.client.rmarkdown.ui.ShinyDocumentWarningDialog;
57 import org.rstudio.studio.client.rsconnect.ui.RSConnectPublishButton;
58 import org.rstudio.studio.client.server.ServerError;
59 import org.rstudio.studio.client.server.ServerRequestCallback;
60 import org.rstudio.studio.client.server.VoidServerRequestCallback;
61 import org.rstudio.studio.client.server.Void;
62 import org.rstudio.studio.client.workbench.WorkbenchContext;
63 import org.rstudio.studio.client.workbench.commands.Commands;
64 import org.rstudio.studio.client.workbench.model.Session;
65 import org.rstudio.studio.client.workbench.prefs.events.UserPrefsChangedEvent;
66 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
67 import org.rstudio.studio.client.workbench.views.source.events.NotebookRenderFinishedEvent;
68 
69 import com.google.gwt.core.client.JavaScriptObject;
70 import com.google.gwt.core.client.Scheduler;
71 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
72 import com.google.gwt.event.logical.shared.ValueChangeEvent;
73 import com.google.gwt.event.logical.shared.ValueChangeHandler;
74 import com.google.gwt.user.client.Command;
75 import com.google.inject.Inject;
76 import com.google.inject.Provider;
77 import com.google.inject.Singleton;
78 
79 @Singleton
80 public class RmdOutput implements RmdRenderStartedEvent.Handler,
81                                   RmdRenderCompletedEvent.Handler,
82                                   RmdShinyDocStartedEvent.Handler,
83                                   PreviewRmdEvent.Handler,
84                                   RenderRmdEvent.Handler,
85                                   RenderRmdSourceEvent.Handler,
86                                   RestartStatusEvent.Handler,
87                                   WebsiteFileSavedEvent.Handler,
88                                   NotebookRenderFinishedEvent.Handler,
89                                   RmdRenderPendingEvent.Handler,
90                                   QuitInitiatedEvent.Handler,
91                                   UserPrefsChangedEvent.Handler
92 {
93    public interface Binder
94    extends CommandBinder<Commands, RmdOutput> {}
95 
96    @Inject
RmdOutput(EventBus eventBus, Commands commands, Session session, GlobalDisplay globalDisplay, FileTypeRegistry fileTypeRegistry, WorkbenchContext workbenchContext, Provider<ViewFilePanel> pViewFilePanel, Binder binder, UserPrefs prefs, PDFViewer pdfViewer, RMarkdownServerOperations server)97    public RmdOutput(EventBus eventBus,
98                     Commands commands,
99                     Session session,
100                     GlobalDisplay globalDisplay,
101                     FileTypeRegistry fileTypeRegistry,
102                     WorkbenchContext workbenchContext,
103                     Provider<ViewFilePanel> pViewFilePanel,
104                     Binder binder,
105                     UserPrefs prefs,
106                     PDFViewer pdfViewer,
107                     RMarkdownServerOperations server)
108    {
109       globalDisplay_ = globalDisplay;
110       fileTypeRegistry_ = fileTypeRegistry;
111       workbenchContext_ = workbenchContext;
112       pViewFilePanel_ = pViewFilePanel;
113       prefs_ = prefs;
114       pdfViewer_ = pdfViewer;
115       server_ = server;
116       events_ = eventBus;
117       session_ = session;
118       commands_ = commands;
119 
120       eventBus.addHandler(RmdRenderStartedEvent.TYPE, this);
121       eventBus.addHandler(RmdRenderCompletedEvent.TYPE, this);
122       eventBus.addHandler(RmdShinyDocStartedEvent.TYPE, this);
123       eventBus.addHandler(PreviewRmdEvent.TYPE, this);
124       eventBus.addHandler(RenderRmdEvent.TYPE, this);
125       eventBus.addHandler(RenderRmdSourceEvent.TYPE, this);
126       eventBus.addHandler(RestartStatusEvent.TYPE, this);
127       eventBus.addHandler(UserPrefsChangedEvent.TYPE, this);
128       eventBus.addHandler(WebsiteFileSavedEvent.TYPE, this);
129       eventBus.addHandler(QuitInitiatedEvent.TYPE, this);
130       eventBus.addHandler(RmdRenderPendingEvent.TYPE, this);
131       eventBus.addHandler(NotebookRenderFinishedEvent.TYPE, this);
132 
133       prefs_.rmdViewerType().addValueChangeHandler(new ValueChangeHandler<String>()
134       {
135          @Override
136          public void onValueChange(ValueChangeEvent<String> e)
137          {
138             onViewerTypeChanged(e.getValue());
139          }
140       });
141 
142       binder.bind(commands, this);
143 
144       exportRmdOutputClosedCallback();
145    }
146 
147    @Override
onRmdRenderPending(RmdRenderPendingEvent event)148    public void onRmdRenderPending(RmdRenderPendingEvent event)
149    {
150       renderInProgress_ = true;
151    }
152 
153    @Override
onRmdRenderStarted(RmdRenderStartedEvent event)154    public void onRmdRenderStarted(RmdRenderStartedEvent event)
155    {
156       if (Desktop.isDesktop())
157       {
158          // When an Office document starts rendering, tell the desktop frame
159          // (if it exists) to get ready; this generally involves closing the
160          // document in preparation for a refresh
161          String format = event.getFormat().getFormatName();
162          if (StringUtil.equals(format, RmdOutputFormat.OUTPUT_WORD_DOCUMENT))
163             Desktop.getFrame().prepareShowWordDoc();
164          else if (StringUtil.equals(format, RmdOutputFormat.OUTPUT_PPT_PRESENTATION))
165             Desktop.getFrame().prepareShowPptPresentation();
166       }
167    }
168 
169    @Override
onRmdRenderCompleted(RmdRenderCompletedEvent event)170    public void onRmdRenderCompleted(RmdRenderCompletedEvent event)
171    {
172       renderInProgress_ = false;
173 
174       // if there's a custom operation to be run when render completes, run
175       // that instead
176       if (onRenderCompleted_ != null)
177       {
178          onRenderCompleted_.execute();
179          onRenderCompleted_ = null;
180          return;
181       }
182 
183       // ignore failures and completed Shiny docs (the latter are handled when
184       // the server starts rather than when the render process is finished)
185       final RmdRenderResult result = event.getResult();
186       if (result.isShinyDocument())
187       {
188          shinyDoc_ = null;
189          return;
190       }
191 
192       if (result.hasShinyContent() && !result.isShinyDocument())
193       {
194          // If the result has Shiny content but wasn't rendered as a Shiny
195          // document, suggest rendering as a Shiny document instead
196          new ShinyDocumentWarningDialog(new OperationWithInput<Integer>()
197          {
198             @Override
199             public void execute(Integer input)
200             {
201                switch (input)
202                {
203                case ShinyDocumentWarningDialog.RENDER_SHINY_NO:
204                   if (result.getSucceeded())
205                      displayRenderResult(result);
206                   break;
207                case ShinyDocumentWarningDialog.RENDER_SHINY_ONCE:
208                   rerenderAsShiny(result);
209                   break;
210                case ShinyDocumentWarningDialog.RENDER_SHINY_ALWAYS:
211                   events_.fireEvent(new ConvertToShinyDocEvent
212                         (result.getTargetFile()));
213                   break;
214                }
215             }
216          }).showModal();
217       }
218       else if (result.getSucceeded())
219       {
220          displayRenderResult(event.getResult());
221       }
222    }
223 
224    @Override
onRmdShinyDocStarted(RmdShinyDocStartedEvent event)225    public void onRmdShinyDocStarted(RmdShinyDocStartedEvent event)
226    {
227       shinyDoc_ = event.getDocInfo();
228       RmdRenderResult result =
229             RmdRenderResult.createFromShinyDoc(shinyDoc_);
230       displayHTMLRenderResult(result);
231    }
232 
233    @Override
onPreviewRmd(final PreviewRmdEvent event)234    public void onPreviewRmd(final PreviewRmdEvent event)
235    {
236       RenderRmdEvent renderEvent = new RenderRmdEvent(
237            event.getSourceFile(),
238            1,
239            null,
240            event.getEncoding(),
241            null,
242            false,
243            RmdOutput.TYPE_STATIC,
244            event.getOutputFile(),
245            null,
246            null);
247       events_.fireEvent(renderEvent);
248    }
249 
250    @Override
onRenderRmd(final RenderRmdEvent event)251    public void onRenderRmd(final RenderRmdEvent event)
252    {
253       quitInitiatedAfterLastRender_ = false;
254 
255       final Operation renderOperation = new Operation() {
256          @Override
257          public void execute()
258          {
259             renderInProgress_ = true;
260             server_.renderRmd(event.getSourceFile(),
261                               event.getSourceLine(),
262                               event.getFormat(),
263                               event.getEncoding(),
264                               event.getParamsFile(),
265                               event.asTempfile(),
266                               event.getType(),
267                               event.getExistingOutputFile(),
268                               event.getWorkingDir(),
269                               event.getViewerType(),
270                   new SimpleRequestCallback<Boolean>() {
271                        @Override
272                        public void onError(ServerError error)
273                        {
274                           renderInProgress_ = false;
275                        }
276                   });
277          }
278       };
279 
280       // If there's a running shiny document for this file and it's not a
281       // presentation, we can do an in-place reload. Note that we don't
282       // currently support in-place reload for Shiny presentations since we
283       // would need to hook a client event at the end of the re-render that
284       // emitted updated slide navigation information and then plumbed that
285       // information back into the preview window.
286       if (shinyDoc_ != null &&
287           event.getSourceFile() == shinyDoc_.getFile() &&
288           !shinyDoc_.getFormat().getFormatName().endsWith(
289                 RmdOutputFormat.OUTPUT_PRESENTATION_SUFFIX) &&
290           (result_ == null || "shiny".equals(result_.getRuntime())))
291       {
292          final RmdRenderResult result =
293                RmdRenderResult.createFromShinyDoc(shinyDoc_);
294          displayHTMLRenderResult(result);
295       }
296       else
297       {
298          performRenderOperation(renderOperation);
299       }
300    }
301 
302    @Override
onNotebookRenderFinished(NotebookRenderFinishedEvent event)303    public void onNotebookRenderFinished(NotebookRenderFinishedEvent event)
304    {
305       // ignore if no result, no output frame/closed output frame, or frame not
306       // associated with this document
307       if (result_ == null ||
308           outputFrame_ == null ||
309           outputFrame_.getWindowObject() == null ||
310           outputFrame_.getWindowObject().isClosed() ||
311           outputFrame_.getPreviewParams().getTargetFile() != event.getDocPath() ||
312           !outputFrame_.getPreviewParams().getOutputFile().endsWith(".nb.html"))
313         return;
314 
315       // redisplay the result
316       displayRenderResult(result_);
317    }
318 
319    @Override
onWebsiteFileSaved(WebsiteFileSavedEvent event)320    public void onWebsiteFileSaved(WebsiteFileSavedEvent event)
321    {
322       // auto reload/rerender on file saves (first apply various
323       // filters to not auto reload). note that before we even
324       // receive this event we know the file is one that is contained
325       // in the website directory
326 
327       // skip if there is a build in progress
328       if (workbenchContext_.isBuildInProgress())
329          return;
330 
331       // skip if there is a render in progress
332       if (renderInProgress_)
333          return;
334 
335       // skip if there was a quit initiated since the last render
336       if (quitInitiatedAfterLastRender_)
337          return;
338 
339       // is there an output frame?
340       if (outputFrame_ == null || outputFrame_.getWindowObject() == null)
341          return;
342 
343       // is it showing a page from the current site?
344       String websiteDir = session_.getSessionInfo().getBuildTargetDir();
345       final RmdPreviewParams params = outputFrame_.getPreviewParams();
346       if (!params.getTargetFile().startsWith(websiteDir))
347          return;
348 
349       // is the changed file one that should always produce a rebuild?
350       FileSystemItem file = event.getFileSystemItem();
351       TextFileType fileType = fileTypeRegistry_.getTextTypeForFile(file);
352       String typeId = fileType.getTypeId();
353       if (fileType.isR() ||
354           typeId == FileTypeRegistry.HTML.getTypeId() ||
355           typeId == FileTypeRegistry.YAML.getTypeId() ||
356           typeId == FileTypeRegistry.JSON.getTypeId())
357       {
358          reRenderPreview();
359       }
360 
361       // is the changed file a markdown document
362       else if (fileType.isMarkdown())
363       {
364          // included Rmd files always produce a rebuild of the current file
365          if (file.getStem().startsWith("_"))
366             reRenderPreview();
367 
368          // ...otherwise leave it alone (requires a knit)
369       }
370 
371       // see if this should result in a copy + refresh
372       else
373       {
374          server_.maybeCopyWebsiteAsset(file.getPath(),
375                new SimpleRequestCallback<Boolean>() {
376                   @Override
377                   public void onResponseReceived(Boolean copied)
378                   {
379                      if (copied)
380                         outputFrame_.showRmdPreview(params, true);
381                   }
382                 });
383       }
384    }
385 
reRenderPreview()386    private void reRenderPreview()
387    {
388       reRenderPreview(null);
389    }
390 
reRenderPreview(String targetFile)391    private void reRenderPreview(String targetFile)
392    {
393       if (outputFrame_ == null)
394          return;
395 
396       livePreviewRenderInProgress_ = true;
397 
398       RmdPreviewParams params = outputFrame_.getPreviewParams();
399       if (targetFile == null)
400          targetFile = params.getTargetFile();
401 
402       RenderRmdEvent renderEvent = new RenderRmdEvent(
403             targetFile,
404             1,
405             params.getResult().getFormatName(),
406             params.getResult().getTargetEncoding(),
407             null,
408             false,
409             RmdOutput.TYPE_STATIC,
410             null,
411             null,
412             null);
413        events_.fireEvent(renderEvent);
414    }
415 
416    @Override
onQuitInitiated(QuitInitiatedEvent event)417    public void onQuitInitiated(QuitInitiatedEvent event)
418    {
419       quitInitiatedAfterLastRender_ = true;
420    }
421 
422 
423    @Override
onRenderRmdSource(final RenderRmdSourceEvent event)424    public void onRenderRmdSource(final RenderRmdSourceEvent event)
425    {
426       quitInitiatedAfterLastRender_ = false;
427 
428       performRenderOperation(new Operation() {
429          @Override
430          public void execute()
431          {
432             server_.renderRmdSource(event.getSource(),
433                                     new SimpleRequestCallback<>());
434          }
435       });
436    }
437 
438    @Override
onRestartStatus(RestartStatusEvent event)439    public void onRestartStatus(RestartStatusEvent event)
440    {
441       // preemptively close the satellite window when R restarts (so we don't
442       // wait around if the session doesn't get a chance to tell us about
443       // terminated renders)
444       if (event.getStatus() == RestartStatusEvent.RESTART_INITIATED)
445       {
446          if (outputFrame_ != null)
447             outputFrame_.closeOutputFrame(false);
448          restarting_ = true;
449       }
450       else
451       {
452          restarting_ =  false;
453       }
454    }
455 
456    @Override
onUserPrefsChanged(UserPrefsChangedEvent e)457    public void onUserPrefsChanged(UserPrefsChangedEvent e)
458    {
459       onViewerTypeChanged(prefs_.rmdViewerType().getValue());
460    }
461 
462    // Private methods ---------------------------------------------------------
463 
onViewerTypeChanged(String newViewerType)464    private void onViewerTypeChanged(String newViewerType)
465    {
466       if (outputFrame_ != null &&
467           outputFrame_.getWindowObject() != null &&
468           newViewerType != outputFrame_.getViewerType())
469       {
470          // close the existing frame
471          RmdPreviewParams params = outputFrame_.getPreviewParams();
472          outputFrame_.closeOutputFrame(true);
473 
474          // reset the scroll position (as it will vary with the document width,
475          // which will change)
476          params.setScrollPosition(0);
477 
478          // open a new one with the same parameters
479          outputFrame_ = createOutputFrame(newViewerType);
480          if (outputFrame_ != null)
481             outputFrame_.showRmdPreview(params, true);
482       }
483       else if (outputFrame_ != null &&
484                outputFrame_.getWindowObject() == null &&
485                outputFrame_.getViewerType() != newViewerType)
486       {
487          // output frame exists but doesn't have a loaded doc, clear it so we'll
488          // create the frame appropriate to this type on next render
489          outputFrame_ = null;
490       }
491    }
492 
493    // perform the given render after terminating the currently running Shiny
494    // application if there is one
performRenderOperation(final Operation renderOperation)495    private void performRenderOperation(final Operation renderOperation)
496    {
497       if (shinyDoc_ != null)
498       {
499          // if we already have this up in the viewer pane, cache the scroll
500          // position (we don't need to do this for the satellite since it
501          // caches scroll position when it closes)
502          if (result_ != null &&
503              outputFrame_.getViewerType() == UserPrefs.RMD_VIEWER_TYPE_PANE)
504          {
505             cacheDocPosition(result_, outputFrame_.getScrollPosition(),
506                   outputFrame_.getAnchor());
507          }
508 
509          // there is a Shiny doc running; we'll need to terminate it before
510          // we can render this document
511          outputFrame_.closeOutputFrame(false);
512          server_.terminateRenderRmd(true, new ServerRequestCallback<Void>()
513          {
514             @Override
515             public void onResponseReceived(Void v)
516             {
517                onRenderCompleted_ = renderOperation;
518                shinyDoc_ = null;
519             }
520 
521             @Override
522             public void onError(ServerError error)
523             {
524                globalDisplay_.showErrorMessage("Shiny Terminate Failed",
525                      "The Shiny document " + shinyDoc_.getFile() + " needs to " +
526                      "be stopped before the document can be rendered.");
527             }
528          });
529       }
530       else
531       {
532          renderOperation.execute();
533       }
534    }
535 
rerenderAsShiny(RmdRenderResult result)536    private void rerenderAsShiny(RmdRenderResult result)
537    {
538       events_.fireEvent(new RenderRmdEvent(
539             result.getTargetFile(), result.getTargetLine(),
540             null, result.getTargetEncoding(), null, false,
541             RmdOutput.TYPE_SHINY, null, null, result.getViewerType()));
542    }
543 
displayOfficeDoc(final RmdRenderResult result, final CommandWithArg<String> displayResult)544    private void displayOfficeDoc(final RmdRenderResult result,
545                                  final CommandWithArg<String> displayResult)
546    {
547       // in desktop mode, the document can be displayed directly
548       if (Desktop.isDesktop())
549          displayResult.execute(result.getOutputFile());
550 
551       // it's not possible to show Office docs inline in a useful way from
552       // within the browser, so just offer to download the file.
553       else
554       {
555          showDownloadPreviewFileDialog(result, new Command() {
556             @Override
557             public void execute()
558             {
559                displayResult.execute(result.getOutputFile());
560             }
561          });
562       }
563    }
564 
displayRenderResult(final RmdRenderResult result)565    private void displayRenderResult(final RmdRenderResult result)
566    {
567       // don't display anything if user doesn't want to
568       if (prefs_.rmdViewerType().getValue() == UserPrefs.RMD_VIEWER_TYPE_NONE)
569          return;
570 
571       String extension = FileSystemItem.getExtensionFromPath(
572                                                 result.getOutputFile());
573       if (".pdf".equals(extension))
574       {
575          String previewer = prefs_.pdfPreviewer().getValue();
576          if (previewer == UserPrefs.PDF_PREVIEWER_RSTUDIO)
577          {
578             pdfViewer_.viewPdfUrl(
579                   result.getOutputUrl(),
580                   result.getPreviewSlide() >= 0 ?
581                         result.getPreviewSlide() : null);
582          }
583          else if (previewer != UserPrefs.PDF_PREVIEWER_NONE)
584          {
585             if (Desktop.isDesktop())
586                Desktop.getFrame().showPDF(StringUtil.notNull(result.getOutputFile()),
587                                           result.getPreviewSlide());
588             else
589                globalDisplay_.showHtmlFile(result.getOutputFile());
590          }
591       }
592       else if (".docx".equals(extension) ||
593                ".rtf".equals(extension) ||
594                ".odt".equals(extension))
595       {
596          displayOfficeDoc(result, (r) -> globalDisplay_.showWordDoc(r));
597       }
598       else if (".pptx".equals(extension))
599       {
600          displayOfficeDoc(result, (r) -> globalDisplay_.showPptPresentation(r));
601       }
602       else if (".html".equals(extension) ||
603                NOTEBOOK_EXT.equals(extension))
604       {
605          displayHTMLRenderResult(result);
606       }
607       else if (".md".equalsIgnoreCase(extension) ||
608                extension.toLowerCase().startsWith(".markdown") ||
609                ".tex".equalsIgnoreCase(extension))
610       {
611          ViewFilePanel viewFilePanel = pViewFilePanel_.get();
612          viewFilePanel.showFile(
613             FileSystemItem.createFile(result.getOutputFile()), "UTF-8");
614       }
615       else
616       {
617          if (Desktop.hasDesktopFrame())
618             Desktop.getFrame().showFile(StringUtil.notNull(result.getOutputFile()));
619          else
620          {
621             showDownloadPreviewFileDialog(result, new Command() {
622                @Override
623                public void execute()
624                {
625                   String url = server_.getFileUrl(
626                         FileSystemItem.createFile(result.getOutputFile()));
627                   globalDisplay_.openWindow(url);
628                }
629             });
630          }
631 
632       }
633    }
634 
showDownloadPreviewFileDialog( final RmdRenderResult result, final Command onDownload)635    private void showDownloadPreviewFileDialog(
636          final RmdRenderResult result, final Command onDownload)
637    {
638       globalDisplay_.showYesNoMessage(GlobalDisplay.MSG_INFO,
639             "R Markdown Render Completed",
640             "R Markdown has finished rendering " +
641             result.getTargetFile() + " to " +
642             result.getOutputFile() + ".",
643             false,
644             new ProgressOperation()
645             {
646                @Override
647                public void execute(ProgressIndicator indicator)
648                {
649                   onDownload.execute();
650                   indicator.onCompleted();
651                }
652             },
653             null,
654             "Download File",
655             "OK",
656             false);
657    }
658 
displayHTMLRenderResult(RmdRenderResult result)659    private void displayHTMLRenderResult(RmdRenderResult result)
660    {
661       // find the last known position for this file
662       int scrollPosition = 0;
663       String anchor = "";
664       if (scrollPositions_.containsKey(keyFromResult(result)))
665       {
666          scrollPosition = scrollPositions_.get(keyFromResult(result));
667       }
668       if (anchors_.containsKey(keyFromResult(result)))
669       {
670          anchor = anchors_.get(keyFromResult(result));
671       }
672       final RmdPreviewParams params = RmdPreviewParams.create(
673             result, scrollPosition, anchor);
674 
675       // get the default viewer type from prefs
676       String viewerType = prefs_.rmdViewerType().getValue();
677 
678       // apply override from result, if any
679       if (result.getViewerType() == RmdEditorOptions.PREVIEW_IN_VIEWER)
680          viewerType = UserPrefs.RMD_VIEWER_TYPE_PANE;
681       else if (result.getViewerType() == RmdEditorOptions.PREVIEW_IN_WINDOW)
682          viewerType = UserPrefs.RMD_VIEWER_TYPE_WINDOW;
683       else if (result.getViewerType() == RmdEditorOptions.PREVIEW_IN_NONE)
684          viewerType = UserPrefs.RMD_VIEWER_TYPE_NONE;
685 
686       // don't host presentations in the viewer pane--ioslides doesn't scale
687       // slides well without help
688       if (result.isHtmlPresentation() && viewerType == UserPrefs.RMD_VIEWER_TYPE_PANE)
689          viewerType = UserPrefs.RMD_VIEWER_TYPE_WINDOW;
690 
691       final String newViewerType = viewerType;
692 
693       // if we're about to pop open a window but one of the publish buttons
694       // is waiting for a render to complete, skip the preview entirely so
695       // we don't disturb the publish flow with a window popping up
696       if (newViewerType == UserPrefs.RMD_VIEWER_TYPE_WINDOW &&
697             RSConnectPublishButton.isAnyRmdRenderPending())
698       {
699          return;
700       }
701 
702       // get the window object if available
703       WindowEx win = null;
704       boolean needsReopen = false;
705       if (outputFrame_ != null)
706       {
707          win = outputFrame_.getWindowObject();
708          if (outputFrame_.getViewerType() != newViewerType)
709             needsReopen = true;
710       }
711 
712       // if there's a window up but it's showing a different document type,
713       // close it so that we can create a new one better suited to this doc type
714       if (needsReopen ||
715             (win != null &&
716              result_ != null &&
717              result_.getFormatName() != result.getFormatName()))
718       {
719          outputFrame_.closeOutputFrame(false);
720          outputFrame_ = null;
721          win = null;
722          // let window finish closing before continuing
723          Scheduler.get().scheduleDeferred(new ScheduledCommand()
724          {
725             @Override
726             public void execute()
727             {
728                displayRenderResult(null, newViewerType, params);
729             }
730          });
731       }
732       else
733       {
734          displayRenderResult(win, newViewerType, params);
735       }
736    }
737 
displayRenderResult(WindowEx win, String viewerType, RmdPreviewParams params)738    private void displayRenderResult(WindowEx win, String viewerType,
739                                     RmdPreviewParams params)
740    {
741       if (viewerType == UserPrefs.RMD_VIEWER_TYPE_NONE)
742          return;
743 
744       RmdRenderResult result = params.getResult();
745 
746       if (outputFrame_ == null)
747          outputFrame_ = createOutputFrame(viewerType);
748 
749       // we're refreshing if the window is up and we're pulling the same
750       // output file as the last one
751       boolean isRefresh = win != null &&
752                           result_ != null &&
753                           result_.getOutputFile() == result.getOutputFile();
754 
755       // if this isn't a refresh but there's a window up, cache the scroll
756       // position of the old document before we replace it
757       if (!isRefresh && result_ != null && win != null)
758       {
759          cacheDocPosition(result_, outputFrame_.getScrollPosition(),
760                           outputFrame_.getAnchor());
761       }
762 
763       // if it is a refresh, use the doc's existing positions
764       if (isRefresh)
765       {
766          params.setScrollPosition(outputFrame_.getScrollPosition());
767          params.setAnchor(outputFrame_.getAnchor());
768       }
769 
770       boolean isNotebook = result_ != null &&
771             FileSystemItem.getExtensionFromPath(result_.getOutputFile()) ==
772             NOTEBOOK_EXT;
773 
774       // show the preview; activate the window (but not for auto-refresh of
775       // notebook preview)
776       outputFrame_.showRmdPreview(params, !(isRefresh && isNotebook &&
777             result.viewed()));
778       result.setViewed(true);
779 
780       // reset live preview state
781       livePreviewRenderInProgress_ = false;
782 
783       // save the result so we know if the next render is a re-render of the
784       // same document
785       result_ = result;
786    }
787 
exportRmdOutputClosedCallback()788    private final native void exportRmdOutputClosedCallback()/*-{
789       var registry = this;
790       $wnd.notifyRmdOutputClosed = $entry(
791          function(params) {
792             registry.@org.rstudio.studio.client.rmarkdown.RmdOutput::notifyRmdOutputClosed(Lcom/google/gwt/core/client/JavaScriptObject;)(params);
793          }
794       );
795    }-*/;
796 
797    // when the window is closed, remember our position within it
notifyRmdOutputClosed(JavaScriptObject closeParams)798    private void notifyRmdOutputClosed(JavaScriptObject closeParams)
799    {
800       // save anchor location for presentations and scroll position for
801       // documents
802       RmdPreviewParams params = closeParams.cast();
803       cacheDocPosition(params.getResult(), params.getScrollPosition(),
804                        params.getAnchor());
805 
806       // if this is a Shiny document, stop the associated process
807       if (params.isShinyDocument() && !restarting_)
808       {
809          server_.terminateRenderRmd(true, new VoidServerRequestCallback());
810       }
811       shinyDoc_ = null;
812    }
813 
cacheDocPosition(RmdRenderResult result, int scrollPosition, String anchor)814    private void cacheDocPosition(RmdRenderResult result, int scrollPosition,
815                                  String anchor)
816    {
817       if (result.isHtmlPresentation())
818       {
819          anchors_.put(keyFromResult(result), anchor);
820       }
821       else
822       {
823          scrollPositions_.put(keyFromResult(result), scrollPosition);
824       }
825    }
826 
827    // Generates lookup keys from results; used to enforce caching scroll
828    // position and/or anchor by document name and type
keyFromResult(RmdRenderResult result)829    private String keyFromResult(RmdRenderResult result)
830    {
831       if (result.isShinyDocument())
832          return result.getTargetFile();
833       else
834          return result.getOutputFile() + "-" + result.getFormatName();
835    }
836 
createOutputFrame(String viewerType)837    private RmdOutputFrame createOutputFrame(String viewerType)
838    {
839       switch(viewerType)
840       {
841       case UserPrefs.RMD_VIEWER_TYPE_WINDOW:
842          return RStudioGinjector.INSTANCE.getRmdOutputFrameSatellite();
843       case UserPrefs.RMD_VIEWER_TYPE_PANE:
844          return RStudioGinjector.INSTANCE.getRmdOutputFramePane();
845       }
846       return null;
847    }
848 
849    private final GlobalDisplay globalDisplay_;
850    private final FileTypeRegistry fileTypeRegistry_;
851    private final UserPrefs prefs_;
852    private final PDFViewer pdfViewer_;
853    private final Provider<ViewFilePanel> pViewFilePanel_;
854    private final RMarkdownServerOperations server_;
855    private final Session session_;
856    private final EventBus events_;
857    private final Commands commands_;
858    private final WorkbenchContext workbenchContext_;
859    private boolean restarting_ = false;
860 
861    // stores the last scroll position of each document we know about: map
862    // of path to position
863    private final Map<String, Integer> scrollPositions_ = new HashMap<>();
864    private final Map<String, String> anchors_ = new HashMap<>();
865    private RmdRenderResult result_;
866    private RmdShinyDocInfo shinyDoc_;
867    private Operation onRenderCompleted_;
868    private RmdOutputFrame outputFrame_;
869    private boolean renderInProgress_ = false;
870    private boolean livePreviewRenderInProgress_ = false;
871    private boolean quitInitiatedAfterLastRender_ = false;
872 
873    public final static String NOTEBOOK_EXT = ".nb.html";
874 
875    public final static int TYPE_STATIC   = 0;
876    public final static int TYPE_SHINY    = 1;
877    public final static int TYPE_NOTEBOOK = 2;
878 }
879