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(" "); 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