1 /*
2  * Presentation.java
3 
4  *
5  * Copyright (C) 2021 by RStudio, PBC
6  *
7  * Unless you have received this program directly from RStudio pursuant
8  * to the terms of a commercial license agreement with RStudio, then
9  * this program is licensed to you under the terms of version 3 of the
10  * GNU Affero General Public License. This program is distributed WITHOUT
11  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
12  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
13  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
14  *
15  */
16 package org.rstudio.studio.client.workbench.views.presentation;
17 
18 import java.util.Iterator;
19 
20 import com.google.gwt.core.client.JavaScriptObject;
21 import com.google.gwt.core.client.JsArray;
22 import com.google.gwt.event.shared.HandlerManager;
23 import com.google.gwt.event.shared.HandlerRegistration;
24 import com.google.gwt.json.client.JSONString;
25 import com.google.gwt.user.client.Command;
26 import com.google.gwt.user.client.Event;
27 import com.google.gwt.user.client.Timer;
28 import com.google.inject.Inject;
29 
30 import org.rstudio.core.client.StringUtil;
31 import org.rstudio.core.client.TimeBufferedCommand;
32 import org.rstudio.core.client.command.CommandBinder;
33 import org.rstudio.core.client.command.Handler;
34 import org.rstudio.core.client.files.FileSystemItem;
35 import org.rstudio.core.client.widget.MessageDialog;
36 import org.rstudio.core.client.widget.ProgressIndicator;
37 import org.rstudio.core.client.widget.ProgressOperation;
38 import org.rstudio.core.client.widget.ProgressOperationWithInput;
39 import org.rstudio.studio.client.application.Desktop;
40 import org.rstudio.studio.client.application.events.EventBus;
41 import org.rstudio.studio.client.application.events.ReloadWithLastChanceSaveEvent;
42 import org.rstudio.studio.client.common.FileDialogs;
43 import org.rstudio.studio.client.common.GlobalDisplay;
44 import org.rstudio.studio.client.common.GlobalProgressDelayer;
45 import org.rstudio.studio.client.common.SimpleRequestCallback;
46 import org.rstudio.studio.client.common.filetypes.FileTypeRegistry;
47 import org.rstudio.studio.client.common.presentation.SlideNavigationMenu;
48 import org.rstudio.studio.client.common.presentation.SlideNavigationPresenter;
49 import org.rstudio.studio.client.common.presentation.events.SlideIndexChangedEvent;
50 import org.rstudio.studio.client.common.presentation.events.SlideNavigationChangedEvent;
51 import org.rstudio.studio.client.common.presentation.model.SlideNavigation;
52 import org.rstudio.studio.client.server.Void;
53 import org.rstudio.studio.client.server.ServerError;
54 import org.rstudio.studio.client.server.ServerRequestCallback;
55 import org.rstudio.studio.client.server.VoidServerRequestCallback;
56 import org.rstudio.studio.client.workbench.WorkbenchView;
57 import org.rstudio.studio.client.workbench.commands.Commands;
58 import org.rstudio.studio.client.workbench.model.RemoteFileSystemContext;
59 import org.rstudio.studio.client.workbench.model.Session;
60 import org.rstudio.studio.client.workbench.views.BasePresenter;
61 import org.rstudio.studio.client.workbench.views.presentation.events.PresentationPaneRequestCompletedEvent;
62 import org.rstudio.studio.client.workbench.views.presentation.events.ShowPresentationPaneEvent;
63 import org.rstudio.studio.client.workbench.views.presentation.events.SourceFileSaveCompletedEvent;
64 import org.rstudio.studio.client.workbench.views.presentation.model.PresentationServerOperations;
65 import org.rstudio.studio.client.workbench.views.presentation.model.PresentationState;
66 import org.rstudio.studio.client.workbench.views.source.events.EditPresentationSourceEvent;
67 
68 public class Presentation extends BasePresenter
69                           implements SlideNavigationPresenter.Display
70 {
71    public interface Binder extends CommandBinder<Commands, Presentation> {}
72 
73    public interface Display extends WorkbenchView
74    {
load(String url, String sourceFile)75       void load(String url, String sourceFile);
zoom(String title, String url, Command onClosed)76       void zoom(String title, String url, Command onClosed);
clear()77       void clear();
hasSlides()78       boolean hasSlides();
79 
home()80       void home();
navigate(int index)81       void navigate(int index);
next()82       void next();
prev()83       void prev();
84 
getNavigationMenu()85       SlideNavigationMenu getNavigationMenu();
86 
pauseMedia()87       void pauseMedia();
88 
getPresentationTitle()89       String getPresentationTitle();
90 
showBusy()91       void showBusy();
hideBusy()92       void hideBusy();
93    }
94 
95    @Inject
Presentation(Display display, PresentationServerOperations server, GlobalDisplay globalDisplay, FileDialogs fileDialogs, RemoteFileSystemContext fileSystemContext, EventBus eventBus, FileTypeRegistry fileTypeRegistry, Session session, Binder binder, Commands commands, PresentationDispatcher dispatcher)96    public Presentation(Display display,
97                        PresentationServerOperations server,
98                        GlobalDisplay globalDisplay,
99                        FileDialogs fileDialogs,
100                        RemoteFileSystemContext fileSystemContext,
101                        EventBus eventBus,
102                        FileTypeRegistry fileTypeRegistry,
103                        Session session,
104                        Binder binder,
105                        Commands commands,
106                        PresentationDispatcher dispatcher)
107    {
108       super(display);
109       view_ = display;
110       server_ = server;
111       globalDisplay_ = globalDisplay;
112       fileDialogs_ = fileDialogs;
113       fileSystemContext_ = fileSystemContext;
114       eventBus_ = eventBus;
115       commands_ = commands;
116       fileTypeRegistry_ = fileTypeRegistry;
117       session_ = session;
118       dispatcher_ = dispatcher;
119       dispatcher_.setContext(new PresentationDispatcher.Context()
120       {
121          @Override
122          public void pauseMedia()
123          {
124             view_.pauseMedia();
125          }
126 
127          @Override
128          public String getPresentationFilePath()
129          {
130             return currentState_.getFilePath();
131          }
132       });
133       navigationPresenter_ = new SlideNavigationPresenter(this);
134 
135       binder.bind(commands, this);
136 
137       // auto-refresh for presentation files saved
138       eventBus.addHandler(SourceFileSaveCompletedEvent.TYPE,
139                          new SourceFileSaveCompletedEvent.Handler() {
140          @Override
141          public void onSourceFileSaveCompleted(SourceFileSaveCompletedEvent event)
142          {
143             if (currentState_ != null)
144             {
145                FileSystemItem file = event.getSourceFile();
146                if (file.getPath() == currentState_.getFilePath())
147                {
148                   int index = detectSlideIndex(event.getContents(),
149                                                event.getCursorPos().getRow());
150                   if (index != -1)
151                      currentState_.setSlideIndex(index);
152 
153                   refreshPresentation();
154                }
155                else if (file.getParentPathString() == getCurrentPresDir()
156                           &&
157                         file.getExtension().toLowerCase().equals(".css"))
158                {
159                   refreshPresentation();
160                }
161             }
162          }
163       });
164 
165       eventBus.addHandler(PresentationPaneRequestCompletedEvent.TYPE,
166                           new PresentationPaneRequestCompletedEvent.Handler()
167       {
168          @Override
169          public void onPresentationRequestCompleted(
170                PresentationPaneRequestCompletedEvent event)
171          {
172             view_.hideBusy();
173          }
174       });
175 
176       initPresentationCallbacks();
177    }
178 
initialize(PresentationState state)179    public void initialize(PresentationState state)
180    {
181       if ((state.getSlideIndex() == 0))
182          view_.bringToFront();
183 
184       init(state);
185    }
186 
onShowPresentationPane(ShowPresentationPaneEvent event)187    public void onShowPresentationPane(ShowPresentationPaneEvent event)
188    {
189       globalDisplay_.showProgress("Opening Presentation...");
190       reloadWorkbench();
191    }
192 
193    @Override
editCurrentSlide()194    public void editCurrentSlide()
195    {
196       eventBus_.fireEvent(new EditPresentationSourceEvent(
197             FileSystemItem.createFile(currentState_.getFilePath()),
198             currentState_.getSlideIndex()));
199    }
200 
201    @Handler
onPresentationNext()202    void onPresentationNext()
203    {
204       view_.next();
205    }
206 
207    @Handler
onPresentationPrev()208    void onPresentationPrev()
209    {
210       view_.prev();
211    }
212 
213    @Handler
onPresentationFullscreen()214    void onPresentationFullscreen()
215    {
216       // clear the internal iframe so there is no conflict over handling
217       // presentation events (we'll restore it on zoom close)
218       view_.clear();
219 
220       // show the zoomed version of the presentation. after it closes
221       // restore the inline version
222       view_.zoom(session_.getSessionInfo().getPresentationName(),
223                  buildPresentationUrl("zoom"),
224                  new Command() {
225          @Override
226          public void execute()
227          {
228             view_.load(buildPresentationUrl(), currentState_.getFilePath());
229          }
230       });
231    }
232 
233    @Handler
onPresentationViewInBrowser()234    void onPresentationViewInBrowser()
235    {
236       if (Desktop.isDesktop())
237       {
238          server_.createDesktopViewInBrowserPresentation(
239             new SimpleRequestCallback<String>() {
240                @Override
241                public void onResponseReceived(String path)
242                {
243                   Desktop.getFrame().showFile(StringUtil.notNull(path));
244                }
245             });
246       }
247       else
248       {
249          globalDisplay_.openWindow(
250                            server_.getApplicationURL("presentation/view"));
251       }
252    }
253 
254    @Handler
onPresentationSaveAsStandalone()255    void onPresentationSaveAsStandalone()
256    {
257       // determine the default file name
258       if (saveAsStandaloneDefaultPath_ == null)
259       {
260          FileSystemItem presFilePath = FileSystemItem.createFile(
261                                              currentState_.getFilePath());
262          saveAsStandaloneDefaultPath_ = FileSystemItem.createFile(
263                presFilePath.getParentPath().completePath(presFilePath.getStem()
264                                                   + ".html"));
265       }
266 
267       fileDialogs_.saveFile(
268          "Save Presentation As",
269           fileSystemContext_,
270           saveAsStandaloneDefaultPath_,
271           ".html",
272           false,
273           new ProgressOperationWithInput<FileSystemItem>(){
274 
275             @Override
276             public void execute(final FileSystemItem targetFile,
277                                 ProgressIndicator indicator)
278             {
279                if (targetFile == null)
280                {
281                   indicator.onCompleted();
282                   return;
283                }
284 
285                indicator.onProgress("Saving Presentation...");
286 
287                server_.createStandalonePresentation(
288                   targetFile.getPath(),
289                   new VoidServerRequestCallback(indicator) {
290                      @Override
291                      public void onSuccess()
292                      {
293                         saveAsStandaloneDefaultPath_ = targetFile;
294                      }
295                   });
296             }
297       });
298    }
299 
saveAsStandalone(String targetFile, final ProgressIndicator indicator, final Command onSuccess)300    private void saveAsStandalone(String targetFile,
301                                  final ProgressIndicator indicator,
302                                  final Command onSuccess)
303    {
304       server_.createStandalonePresentation(
305             targetFile, new VoidServerRequestCallback(indicator) {
306                @Override
307                public void onSuccess()
308                {
309                   onSuccess.execute();
310                }
311             });
312    }
313 
314    @Handler
onClearPresentationCache()315    void onClearPresentationCache()
316    {
317       globalDisplay_.showYesNoMessage(
318             MessageDialog.INFO,
319             "Clear Knitr Cache",
320             "Clearing the Knitr cache will discard previously cached " +
321             "output and re-run all of the R code chunks within the " +
322             "presentation.\n\n" +
323             "Are you sure you want to clear the cache now?",
324             false,
325             new ProgressOperation() {
326 
327                @Override
328                public void execute(final ProgressIndicator indicator)
329                {
330                   indicator.onProgress("Clearing Knitr Cache...");
331                   server_.clearPresentationCache(
332                         new ServerRequestCallback<Void>() {
333                            @Override
334                            public void onResponseReceived(Void response)
335                            {
336                               indicator.onCompleted();
337                               refreshPresentation();
338                            }
339 
340                            @Override
341                            public void onError(ServerError error)
342                            {
343                               indicator.onCompleted();
344                               globalDisplay_.showErrorMessage(
345                                                 "Error Clearing Cache",
346                                                  getErrorMessage(error));
347                            }
348                         });
349                }
350 
351             },
352             new ProgressOperation() {
353 
354                @Override
355                public void execute(ProgressIndicator indicator)
356                {
357                   indicator.onCompleted();
358                }
359             },
360             true);
361    }
362 
363 
364    @Handler
onRefreshPresentation()365    void onRefreshPresentation()
366    {
367       if (Event.getCurrentEvent().getShiftKey())
368          currentState_.setSlideIndex(0);
369 
370       refreshPresentation();
371    }
372 
refreshPresentation()373    private void refreshPresentation()
374    {
375       view_.showBusy();
376       view_.load(buildPresentationUrl(), currentState_.getFilePath());
377    }
378 
379    @Override
onSelected()380    public void onSelected()
381    {
382       super.onSelected();
383 
384       // after doing a pane reconfig the frame gets wiped (no idea why)
385       // workaround this by doing a check for an active state with
386       // no slides currently displayed
387       if (currentState_ != null &&
388           currentState_.isActive() &&
389           !view_.hasSlides())
390       {
391          init(currentState_);
392       }
393    }
394 
395 
confirmClose(Command onConfirmed)396    public void confirmClose(Command onConfirmed)
397    {
398       final ProgressIndicator progress = new GlobalProgressDelayer(
399             globalDisplay_,
400             0,
401             "Closing Presentation...").getIndicator();
402 
403       server_.closePresentationPane(new ServerRequestCallback<Void>(){
404          @Override
405          public void onResponseReceived(Void resp)
406          {
407             reloadWorkbench();
408          }
409 
410          @Override
411          public void onError(ServerError error)
412          {
413             progress.onError(error.getUserMessage());
414 
415          }
416       });
417    }
418 
419 
420    @Override
navigate(int index)421    public void navigate(int index)
422    {
423      view_.navigate(index);
424 
425    }
426 
427    @Override
getNavigationMenu()428    public SlideNavigationMenu getNavigationMenu()
429    {
430       return view_.getNavigationMenu();
431    }
432 
433    @Override
addSlideNavigationChangedHandler( SlideNavigationChangedEvent.Handler handler)434    public HandlerRegistration addSlideNavigationChangedHandler(
435                               SlideNavigationChangedEvent.Handler handler)
436    {
437       return handlerManager_.addHandler(SlideNavigationChangedEvent.TYPE,
438                                         handler);
439    }
440 
441    @Override
addSlideIndexChangedHandler( SlideIndexChangedEvent.Handler handler)442    public HandlerRegistration addSlideIndexChangedHandler(
443                               SlideIndexChangedEvent.Handler handler)
444    {
445       return handlerManager_.addHandler(SlideIndexChangedEvent.TYPE, handler);
446    }
447 
getErrorMessage(ServerError error)448    public static String getErrorMessage(ServerError error)
449    {
450       String message = error.getUserMessage();
451       JSONString userMessage = error.getClientInfo().isString();
452       if (userMessage != null)
453          message = userMessage.stringValue();
454       return message;
455    }
456 
reloadWorkbench()457    private void reloadWorkbench()
458    {
459       eventBus_.fireEvent(new ReloadWithLastChanceSaveEvent());
460    }
461 
462 
init(PresentationState state)463    private void init(PresentationState state)
464    {
465       currentState_ = state;
466       view_.load(buildPresentationUrl(), currentState_.getFilePath());
467    }
468 
buildPresentationUrl()469    private String buildPresentationUrl()
470    {
471       return buildPresentationUrl(null);
472    }
473 
buildPresentationUrl(String extraPath)474    private String buildPresentationUrl(String extraPath)
475    {
476       String url = server_.getApplicationURL("presentation/");
477       if (extraPath != null)
478          url = url + extraPath;
479       url = url + "#/" + currentState_.getSlideIndex();
480       return url;
481    }
482 
isPresentationActive()483    private boolean isPresentationActive()
484    {
485       return (currentState_ != null) &&
486              (currentState_.isActive())&&
487              view_.hasSlides();
488    }
489 
getCurrentPresDir()490    private String getCurrentPresDir()
491    {
492       if (currentState_ == null)
493          return "";
494 
495       FileSystemItem presFilePath = FileSystemItem.createFile(
496                                                currentState_.getFilePath());
497       return presFilePath.getParentPathString();
498    }
499 
onPresentationSlideChanged(final int index, final JavaScriptObject jsCmds)500    private void onPresentationSlideChanged(final int index,
501                                            final JavaScriptObject jsCmds)
502    {
503       // note the slide index and save it
504       currentState_.setSlideIndex(index);
505       indexPersister_.setIndex(index);
506 
507       handlerManager_.fireEvent(new SlideIndexChangedEvent(index));
508 
509       // execute commands if we stay on the slide for > 500ms
510       new Timer() {
511          @Override
512          public void run()
513          {
514             // execute commands if we're still on the same slide
515             if (index == currentState_.getSlideIndex())
516             {
517                JsArray<JavaScriptObject> cmds = jsCmds.cast();
518                for (int i=0; i<cmds.length(); i++)
519                   dispatchCommand(cmds.get(i));
520             }
521          }
522       }.schedule(500);
523    }
524 
dispatchCommand(JavaScriptObject jsCommand)525    private void dispatchCommand(JavaScriptObject jsCommand)
526    {
527       dispatcher_.dispatchCommand(jsCommand);
528    }
529 
initPresentationNavigator(JavaScriptObject jsNavigator)530    private void initPresentationNavigator(JavaScriptObject jsNavigator)
531    {
532       // record current slides
533       SlideNavigation navigation = jsNavigator.cast();
534       handlerManager_.fireEvent(
535                new SlideNavigationChangedEvent(navigation));
536    }
537 
recordPresentationQuizAnswer(int slideIndex, int answer, boolean correct)538    private void recordPresentationQuizAnswer(int slideIndex,
539                                              int answer,
540                                              boolean correct)
541    {
542       server_.tutorialQuizResponse(slideIndex,
543                                    answer,
544                                    correct,
545                                    new VoidServerRequestCallback());
546    }
547 
initPresentationCallbacks()548    private final native void initPresentationCallbacks() /*-{
549       var thiz = this;
550       $wnd.presentationSlideChanged = $entry(function(index, cmds) {
551          thiz.@org.rstudio.studio.client.workbench.views.presentation.Presentation::onPresentationSlideChanged(ILcom/google/gwt/core/client/JavaScriptObject;)(index, cmds);
552       });
553       $wnd.dispatchPresentationCommand = $entry(function(cmd) {
554          thiz.@org.rstudio.studio.client.workbench.views.presentation.Presentation::dispatchCommand(Lcom/google/gwt/core/client/JavaScriptObject;)(cmd);
555       });
556       $wnd.initPresentationNavigator = $entry(function(slides) {
557          thiz.@org.rstudio.studio.client.workbench.views.presentation.Presentation::initPresentationNavigator(Lcom/google/gwt/core/client/JavaScriptObject;)(slides);
558       });
559       $wnd.recordPresentationQuizAnswer = $entry(function(index, answer, correct) {
560          thiz.@org.rstudio.studio.client.workbench.views.presentation.Presentation::recordPresentationQuizAnswer(IIZ)(index, answer, correct);
561       });
562    }-*/;
563 
564    private class IndexPersister extends TimeBufferedCommand
565    {
IndexPersister()566       public IndexPersister()
567       {
568          super(500);
569       }
570 
setIndex(int index)571       public void setIndex(int index)
572       {
573          index_ = index;
574          nudge();
575       }
576 
577       @Override
performAction(boolean shouldSchedulePassive)578       protected void performAction(boolean shouldSchedulePassive)
579       {
580          server_.setPresentationSlideIndex(index_,
581                                            new VoidServerRequestCallback());
582       }
583 
584       private int index_ = 0;
585    }
586 
587    private IndexPersister indexPersister_ = new IndexPersister();
588 
589 
590 
591 
592 
detectSlideIndex(String contents, int cursorLine)593    private static int detectSlideIndex(String contents, int cursorLine)
594    {
595       int currentLine = 0;
596       int slideIndex = -1;
597       String slideRegex = "^\\={3,}\\s*$";
598 
599       Iterator<String> it = StringUtil.getLineIterator(contents).iterator();
600       while (it.hasNext())
601       {
602          String line = it.next();
603          if (line.matches(slideRegex))
604             slideIndex++;
605 
606          if (currentLine++ >= cursorLine)
607          {
608             // bump the slide index if the next line is a header
609             if (it.hasNext() && it.next().matches(slideRegex))
610                slideIndex++;
611 
612             return slideIndex;
613          }
614       }
615 
616 
617       return -1;
618    }
619 
620    private final Display view_;
621    private final PresentationServerOperations server_;
622    private final GlobalDisplay globalDisplay_;
623    private final EventBus eventBus_;
624    private final Commands commands_;
625    private final FileTypeRegistry fileTypeRegistry_;
626    private final FileDialogs fileDialogs_;
627    private final RemoteFileSystemContext fileSystemContext_;
628    private final Session session_;
629    private final PresentationDispatcher dispatcher_;
630    private final SlideNavigationPresenter navigationPresenter_;
631    private PresentationState currentState_ = null;
632 
633    private FileSystemItem saveAsStandaloneDefaultPath_ = null;
634 
635    private HandlerManager handlerManager_ = new HandlerManager(this);
636 }
637