1 /* 2 * RmdTemplateOptionsWidget.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.ui; 16 17 import java.util.ArrayList; 18 import java.util.HashMap; 19 import java.util.List; 20 import java.util.Map; 21 22 import com.google.gwt.aria.client.Id; 23 import com.google.gwt.aria.client.Roles; 24 import com.google.gwt.dom.client.Style; 25 import com.google.gwt.user.client.DOM; 26 import org.rstudio.core.client.JsArrayUtil; 27 import org.rstudio.core.client.files.FileSystemItem; 28 import org.rstudio.core.client.theme.res.ThemeResources; 29 import org.rstudio.core.client.theme.res.ThemeStyles; 30 import org.rstudio.studio.client.common.FilePathUtils; 31 import org.rstudio.studio.client.rmarkdown.model.RmdFrontMatter; 32 import org.rstudio.studio.client.rmarkdown.model.RmdFrontMatterOutputOptions; 33 import org.rstudio.studio.client.rmarkdown.model.RmdTemplate; 34 import org.rstudio.studio.client.rmarkdown.model.RmdTemplateFormat; 35 import org.rstudio.studio.client.rmarkdown.model.RmdTemplateFormatOption; 36 37 import com.google.gwt.core.client.GWT; 38 import com.google.gwt.core.client.JavaScriptObject; 39 import com.google.gwt.core.client.JsArray; 40 import com.google.gwt.core.client.JsArrayString; 41 import com.google.gwt.dom.client.Element; 42 import com.google.gwt.dom.client.Style.Overflow; 43 import com.google.gwt.event.dom.client.ChangeEvent; 44 import com.google.gwt.event.dom.client.ChangeHandler; 45 import com.google.gwt.resources.client.CssResource; 46 import com.google.gwt.uibinder.client.UiBinder; 47 import com.google.gwt.uibinder.client.UiField; 48 import com.google.gwt.user.client.ui.Composite; 49 import com.google.gwt.user.client.ui.FlowPanel; 50 import com.google.gwt.user.client.ui.Label; 51 import com.google.gwt.user.client.ui.ListBox; 52 import com.google.gwt.user.client.ui.ScrollPanel; 53 import com.google.gwt.user.client.ui.TabLayoutPanel; 54 import com.google.gwt.user.client.ui.Widget; 55 56 public class RmdTemplateOptionsWidget extends Composite 57 { 58 59 private static RmdTemplateOptionsWidgetUiBinder uiBinder = GWT 60 .create(RmdTemplateOptionsWidgetUiBinder.class); 61 62 interface RmdTemplateOptionsWidgetUiBinder extends 63 UiBinder<Widget, RmdTemplateOptionsWidget> 64 { 65 } 66 67 interface OptionsStyle extends CssResource 68 { optionWidget()69 String optionWidget(); 70 } 71 RmdTemplateOptionsWidget(boolean allowFormatChange)72 public RmdTemplateOptionsWidget(boolean allowFormatChange) 73 { 74 optionsTabs_ = new TabLayoutPanel(30, Style.Unit.PX, "R Markdown Options"); 75 initWidget(uiBinder.createAndBindUi(this)); 76 style.ensureInjected(); 77 allowFormatChange_ = allowFormatChange; 78 if (allowFormatChange) 79 { 80 listFormats_.addChangeHandler(new ChangeHandler() 81 { 82 @Override 83 public void onChange(ChangeEvent event) 84 { 85 updateFormatOptions(getSelectedFormat()); 86 } 87 }); 88 } 89 else 90 { 91 listFormats_.setVisible(false); 92 listFormats_.setEnabled(false); 93 labelFormatNotes_.setVisible(false); 94 labelFormatName_.setVisible(true); 95 } 96 97 ThemeStyles styles = ThemeResources.INSTANCE.themeStyles(); 98 optionsTabs_.addStyleName(styles.dialogTabPanel()); 99 } 100 setTemplate(RmdTemplate template, boolean forCreate)101 public void setTemplate(RmdTemplate template, boolean forCreate) 102 { 103 setTemplate(template, forCreate, null); 104 } 105 setTemplate(RmdTemplate template, boolean forCreate, RmdFrontMatter frontMatter)106 public void setTemplate(RmdTemplate template, boolean forCreate, 107 RmdFrontMatter frontMatter) 108 { 109 template_ = template; 110 formats_ = template.getFormats(); 111 options_ = template.getOptions(); 112 if (frontMatter != null) 113 applyFrontMatter(frontMatter); 114 listFormats_.clear(); 115 for (int i = 0; i < formats_.length(); i++) 116 { 117 listFormats_.addItem(formats_.get(i).getUiName(), 118 formats_.get(i).getName()); 119 } 120 updateFormatOptions(getSelectedFormat()); 121 } 122 setDocument(FileSystemItem document)123 public void setDocument(FileSystemItem document) 124 { 125 document_ = document; 126 } 127 getSelectedFormat()128 public String getSelectedFormat() 129 { 130 return listFormats_.getValue(listFormats_.getSelectedIndex()); 131 } 132 getOutputOptions()133 public RmdFrontMatterOutputOptions getOutputOptions() 134 { 135 return RmdFormatOptionsHelper.optionsListToJson( 136 optionWidgets_, 137 document_, 138 frontMatter_.getOutputOption(getSelectedFormat())).cast(); 139 } 140 setSelectedFormat(String format)141 public void setSelectedFormat(String format) 142 { 143 if (allowFormatChange_) 144 { 145 for (int i = 0; i < listFormats_.getItemCount(); i++) 146 { 147 if (listFormats_.getValue(i) == format) 148 { 149 listFormats_.setSelectedIndex(i); 150 updateFormatOptions(format); 151 } 152 } 153 } 154 else 155 { 156 RmdTemplateFormat selFormat = template_.getFormat(format); 157 if (selFormat != null) 158 labelFormatName_.setText("Shiny " + selFormat.getUiName()); 159 } 160 } 161 getOptionsJSON()162 public JavaScriptObject getOptionsJSON() 163 { 164 return RmdFormatOptionsHelper.optionsListToJson( 165 optionWidgets_, 166 document_, 167 frontMatter_ == null ? 168 null : frontMatter_.getOutputOption(getSelectedFormat())); 169 } 170 updateFormatOptions(String format)171 private void updateFormatOptions(String format) 172 { 173 tabs_ = new HashMap<>(); 174 optionsTabs_.clear(); 175 for (int i = 0; i < formats_.length(); i++) 176 { 177 if (formats_.get(i).getName() == format) 178 { 179 addFormatOptions(formats_.get(i)); 180 break; 181 } 182 } 183 } 184 addFormatOptions(RmdTemplateFormat format)185 private void addFormatOptions(RmdTemplateFormat format) 186 { 187 if (format.getNotes().length() > 0 && allowFormatChange_) 188 { 189 labelFormatNotes_.setText(format.getNotes()); 190 labelFormatNotes_.setVisible(true); 191 } 192 else 193 { 194 labelFormatNotes_.setVisible(false); 195 } 196 optionWidgets_ = new ArrayList<>(); 197 JsArrayString options = format.getOptions(); 198 for (int i = 0; i < options.length(); i++) 199 { 200 RmdFormatOption optionWidget; 201 RmdTemplateFormatOption option = findOption(format.getName(), 202 options.get(i)); 203 if (option == null) 204 continue; 205 206 String initialValue = option.getDefaultValue(); 207 208 // check to see whether a value for this format and option were 209 // specified in the front matter 210 String frontMatterValue = getFrontMatterDefault( 211 format.getName(), option.getName()); 212 if (frontMatterValue != null) 213 initialValue = frontMatterValue; 214 215 optionWidget = createWidgetForOption(option, initialValue); 216 if (optionWidget == null) 217 continue; 218 219 optionWidget.asWidget().addStyleName(style.optionWidget()); 220 221 FlowPanel panel = null; 222 String category = option.getCategory(); 223 if (tabs_.containsKey(category)) 224 { 225 panel = tabs_.get(category); 226 } 227 else 228 { 229 ScrollPanel scrollPanel = new ScrollPanel(); 230 panel = new FlowPanel(); 231 scrollPanel.add(panel); 232 optionsTabs_.add(scrollPanel, new Label(category)); 233 234 // associate tabpanel widget with controlling tab 235 Roles.getTabpanelRole().set(scrollPanel.getElement()); 236 String tabId = DOM.createUniqueId(); 237 optionsTabs_.setTabId(scrollPanel, tabId); 238 Roles.getTabpanelRole().setAriaLabelledbyProperty(scrollPanel.getElement(), Id.of(tabId)); 239 240 tabs_.put(category, panel); 241 } 242 243 panel.add(optionWidget); 244 optionWidgets_.add(optionWidget); 245 } 246 247 // we need to center the tabs and overlay them on the top edge of the 248 // content; to do this, it is necessary to nuke a couple of the inline 249 // styles used by the default GWT tab panel. 250 Element tabOuter = (Element) optionsTabs_.getElement().getChild(1); 251 tabOuter.getStyle().setOverflow(Overflow.VISIBLE); 252 Element tabInner = (Element) tabOuter.getFirstChild(); 253 tabInner.getStyle().clearWidth(); 254 } 255 createWidgetForOption(RmdTemplateFormatOption option, String initialValue)256 private RmdFormatOption createWidgetForOption(RmdTemplateFormatOption option, 257 String initialValue) 258 { 259 RmdFormatOption optionWidget = null; 260 if (option.getType() == RmdTemplateFormatOption.TYPE_BOOLEAN) 261 { 262 optionWidget = new RmdBooleanOption(option, initialValue); 263 } 264 else if (option.getType() == RmdTemplateFormatOption.TYPE_CHOICE) 265 { 266 optionWidget = new RmdChoiceOption(option, initialValue); 267 } 268 else if (option.getType() == RmdTemplateFormatOption.TYPE_STRING) 269 { 270 optionWidget = new RmdStringOption(option, initialValue); 271 } 272 else if (option.getType() == RmdTemplateFormatOption.TYPE_FLOAT || 273 option.getType() == RmdTemplateFormatOption.TYPE_INTEGER) 274 { 275 optionWidget = new RmdFloatOption(option, initialValue); 276 } 277 else if (option.getType() == RmdTemplateFormatOption.TYPE_FILE) 278 { 279 // if we have a document and a relative path, resolve the path 280 // relative to the document 281 if (document_ != null && initialValue != "null" && 282 FilePathUtils.pathIsRelative(initialValue)) 283 { 284 initialValue = 285 document_.getParentPath().completePath(initialValue); 286 } 287 optionWidget = new RmdFileOption(option, initialValue); 288 } 289 return optionWidget; 290 } 291 findOption(String formatName, String optionName)292 private RmdTemplateFormatOption findOption(String formatName, 293 String optionName) 294 { 295 RmdTemplateFormatOption result = null; 296 for (int i = 0; i < options_.length(); i++) 297 { 298 RmdTemplateFormatOption option = options_.get(i); 299 300 // Not the option we're looking for 301 if (option.getName() != optionName) 302 continue; 303 304 String optionFormatName = option.getFormatName(); 305 if (optionFormatName.length() > 0) 306 { 307 // A format-specific option: if it's for this format we're done, 308 // otherwise keep looking 309 if (optionFormatName.equals(formatName)) 310 return option; 311 else 312 continue; 313 } 314 315 result = option; 316 } 317 return result; 318 } 319 applyFrontMatter(RmdFrontMatter frontMatter)320 private void applyFrontMatter(RmdFrontMatter frontMatter) 321 { 322 frontMatter_ = frontMatter; 323 frontMatterCache_ = new HashMap<>(); 324 ensureOptionsCache(); 325 JsArrayString formats = frontMatter.getFormatList(); 326 for (int i = 0; i < formats.length(); i++) 327 { 328 String format = formats.get(i); 329 RmdFrontMatterOutputOptions options = 330 frontMatter.getOutputOption(format); 331 JsArrayString optionList = options.getOptionList(); 332 for (int j = 0; j < optionList.length(); j++) 333 { 334 String option = optionList.get(j); 335 String value = options.getOptionValue(option); 336 frontMatterCache_.put(format + ":" + option, value); 337 if (optionCache_.containsKey(option)) 338 { 339 // If the option is specifically labeled as transferable 340 // between formats, add a generic key to be applied to other 341 // formats 342 RmdTemplateFormatOption formatOption = optionCache_.get(option); 343 if (formatOption.isTransferable()) 344 { 345 frontMatterCache_.put(option, value); 346 } 347 } 348 } 349 } 350 } 351 getFrontMatterDefault(String formatName, String optionName)352 private String getFrontMatterDefault(String formatName, String optionName) 353 { 354 // if we have no front matter, we have no default 355 if (frontMatterCache_ == null) 356 return null; 357 358 // is this value defined in the front matter? 359 String key = formatName + ":" + optionName; 360 if (frontMatterCache_.containsKey(key)) 361 return frontMatterCache_.get(key); 362 else 363 { 364 // is this value transferable from a format defined in the front 365 // matter? (don't transfer options into formats explicitly defined 366 // in the front matter) 367 JsArrayString frontMatterFormats = frontMatter_.getFormatList(); 368 if ((!JsArrayUtil.jsArrayStringContains(frontMatterFormats, formatName)) 369 && 370 frontMatterCache_.containsKey(optionName)) 371 { 372 return frontMatterCache_.get(optionName); 373 } 374 } 375 return null; 376 } 377 ensureOptionsCache()378 private void ensureOptionsCache() 379 { 380 if (optionCache_ != null) 381 return; 382 optionCache_ = new HashMap<>(); 383 for (int i = 0; i < options_.length(); i++) 384 { 385 RmdTemplateFormatOption option = options_.get(i); 386 if (option.getFormatName().length() > 0) 387 continue; 388 optionCache_.put(option.getName(), option); 389 } 390 } 391 392 private RmdTemplate template_; 393 private JsArray<RmdTemplateFormat> formats_; 394 private JsArray<RmdTemplateFormatOption> options_; 395 private List<RmdFormatOption> optionWidgets_; 396 private RmdFrontMatter frontMatter_; 397 private FileSystemItem document_; 398 private boolean allowFormatChange_; 399 400 // Cache of options present in the template (ignores those options that 401 // are specifically marked for a format) 402 private Map<String, RmdTemplateFormatOption> optionCache_; 403 404 // Cache of values set in the front matter, e.g.: 405 // "html_document:fig_width" => "7.5" 406 // In the case of options that are marked as transferable, maps directly 407 // from an option name to its default, e.g. 408 // "toc" => "true" 409 private Map<String, String> frontMatterCache_; 410 411 private Map<String, FlowPanel> tabs_; 412 413 @UiField ListBox listFormats_; 414 @UiField Label labelFormatNotes_; 415 @UiField Label labelFormatName_; 416 @UiField(provided=true) TabLayoutPanel optionsTabs_; 417 @UiField OptionsStyle style; 418 } 419