1 /*
2  * TextEditingTargetPresentationHelper.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 
16 package org.rstudio.studio.client.workbench.views.source.editors.text;
17 
18 import org.rstudio.core.client.CommandWithArg;
19 import org.rstudio.core.client.StringUtil;
20 import org.rstudio.core.client.files.FileSystemItem;
21 import org.rstudio.core.client.regex.Pattern;
22 import org.rstudio.studio.client.RStudioGinjector;
23 import org.rstudio.studio.client.common.SimpleRequestCallback;
24 import org.rstudio.studio.client.common.presentation.model.SlideNavigation;
25 import org.rstudio.studio.client.common.presentation.model.SlideNavigationItem;
26 import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorPosition;
27 import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorSelection;
28 import org.rstudio.studio.client.workbench.views.presentation.model.PresentationServerOperations;
29 import org.rstudio.studio.client.workbench.views.source.editors.EditingTarget;
30 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position;
31 import org.rstudio.studio.client.workbench.views.source.editors.text.status.StatusBarPopupMenu;
32 import org.rstudio.studio.client.workbench.views.source.editors.text.status.StatusBarPopupRequest;
33 import org.rstudio.studio.client.workbench.views.source.model.SourcePosition;
34 
35 import com.google.gwt.core.client.Scheduler;
36 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
37 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
38 import com.google.gwt.user.client.Command;
39 import com.google.gwt.user.client.ui.MenuItem;
40 import com.google.inject.Inject;
41 
42 public class TextEditingTargetPresentationHelper
43 {
44    public static interface SlideNavigator
45    {
navigateToSlide(int index)46       void navigateToSlide(int index);
47    }
48 
TextEditingTargetPresentationHelper(DocDisplay docDisplay)49    public TextEditingTargetPresentationHelper(DocDisplay docDisplay)
50    {
51       docDisplay_ = docDisplay;
52       RStudioGinjector.INSTANCE.injectMembers(this);
53    }
54 
55    @Inject
initialize(PresentationServerOperations server)56    void initialize(PresentationServerOperations server)
57    {
58       server_ = server;
59    }
60 
getCurrentSlide()61    public String getCurrentSlide()
62    {
63       // search starting two lines ahead
64       Position cursorPos = docDisplay_.getCursorPosition();
65       Position searchPos = Position.create(cursorPos.getRow()+2, 0);
66       InputEditorSelection sel = docDisplay_.search(SLIDE_REGEX,
67                                                 true,
68                                                 false,
69                                                 false,
70                                                 false,
71                                                 searchPos,
72                                                 null,
73                                                 true);
74 
75 
76       if (sel != null)
77       {
78          InputEditorPosition titlePos = sel.getStart().moveToPreviousLine();
79          String title = docDisplay_.getLine(
80                            docDisplay_.selectionToPosition(titlePos).getRow());
81          title = title.trim();
82          if (title.length() > 0 && SLIDE_PATTERN.match(title, 0) == null)
83             return title;
84          else
85             return "(Untitled Slide)";
86       }
87       else
88          return "(No Slides)";
89    }
90 
buildSlideMenu( final String path, boolean isDirty, final EditingTarget editor, final CommandWithArg<StatusBarPopupRequest> onCompleted)91    public void buildSlideMenu(
92                         final String path,
93                         boolean isDirty,
94                         final EditingTarget editor,
95                         final CommandWithArg<StatusBarPopupRequest> onCompleted)
96    {
97       // rpc response handler
98       SimpleRequestCallback<SlideNavigation> requestCallback =
99                         new SimpleRequestCallback<SlideNavigation>() {
100 
101          @Override
102          public void onResponseReceived(SlideNavigation slideNavigation)
103          {
104             // create the menu and make sure we have some slides to return
105             StatusBarPopupMenu menu =  new StatusBarPopupMenu();
106             if (slideNavigation.getTotalSlides() == 0)
107             {
108                onCompleted.execute(new StatusBarPopupRequest(menu, null));
109                return;
110             }
111 
112             MenuItem defaultMenuItem = null;
113             int length = slideNavigation.getItems().length();
114             for (int i=0; i<length; i++)
115             {
116                SlideNavigationItem item = slideNavigation.getItems().get(i);
117                String title = item.getTitle();
118                if (StringUtil.isNullOrEmpty(title))
119                   title = "(Untitled Slide)";
120 
121                StringBuilder indentBuilder = new StringBuilder();
122                for (int level=0; level<item.getIndent(); level++)
123                   indentBuilder.append("&nbsp;&nbsp;");
124 
125                SafeHtmlBuilder labelBuilder = new SafeHtmlBuilder();
126                labelBuilder.appendHtmlConstant(indentBuilder.toString());
127                labelBuilder.appendEscaped(title);
128 
129                final int targetSlide = i;
130                final MenuItem menuItem = new MenuItem(
131                   labelBuilder.toSafeHtml(),
132                   new Command()
133                   {
134                      public void execute()
135                      {
136                         navigateToSlide(editor, targetSlide);
137                      }
138                   });
139                menu.addItem(menuItem);
140 
141                // see if this is the default menu item
142                if (defaultMenuItem == null &&
143                    item.getLine() >= (docDisplay_.getSelectionStart().getRow()))
144                {
145                   defaultMenuItem = menuItem;
146                }
147             }
148 
149             StatusBarPopupRequest request = new StatusBarPopupRequest(
150                                                               menu,
151                                                               defaultMenuItem);
152             onCompleted.execute(request);
153          }
154       };
155 
156       // send code over the wire if we are dirty
157       if (isDirty)
158       {
159          server_.getSlideNavigationForCode(
160                      docDisplay_.getCode(),
161                      FileSystemItem.createFile(path).getParentPathString(),
162                      requestCallback);
163       }
164       else
165       {
166          server_.getSlideNavigationForFile(path, requestCallback);
167       }
168    }
169 
navigateToSlide(final EditingTarget editor, int slideIndex)170    public static void navigateToSlide(final EditingTarget editor,
171                                       int slideIndex)
172    {
173       // scan for the specified slide
174       int currentSlide = 0;
175       Position navPos = null;
176       Position pos = Position.create(0, 0);
177       while ((pos = editor.search(pos, "^\\={3,}\\s*$")) != null)
178       {
179          if (currentSlide++ == slideIndex)
180          {
181             navPos = Position.create(pos.getRow() - 1, 0);
182             break;
183          }
184 
185          pos = Position.create(pos.getRow() + 1, 0);
186       }
187 
188       // navigate to the slide
189       if (navPos != null)
190       {
191          final Position navPosAlias = navPos;
192          Scheduler.get().scheduleDeferred(new ScheduledCommand() {
193 
194             @Override
195             public void execute()
196             {
197                editor.navigateToPosition(
198                  SourcePosition.create(navPosAlias.getRow(), 0),
199                  false);
200 
201             }
202          });
203       }
204    }
205 
206 
207    private final DocDisplay docDisplay_;
208    private PresentationServerOperations server_;
209 
210    private static final String SLIDE_REGEX = "^\\={3,}\\s*$";
211    private static final Pattern SLIDE_PATTERN = Pattern.create(SLIDE_REGEX);
212 }
213