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