1 /* 2 * SavePlotAsPdfDialog.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.workbench.views.plots.ui.export; 16 17 import java.util.ArrayList; 18 import java.util.List; 19 20 import com.google.gwt.aria.client.Roles; 21 import com.google.gwt.safehtml.shared.SafeHtmlUtils; 22 import org.rstudio.core.client.BrowseCap; 23 import org.rstudio.core.client.ElementIds; 24 import org.rstudio.core.client.files.FileSystemContext; 25 import org.rstudio.core.client.files.FileSystemItem; 26 import org.rstudio.core.client.widget.FieldSetWrapperPanel; 27 import org.rstudio.core.client.widget.FormLabel; 28 import org.rstudio.core.client.widget.LayoutGrid; 29 import org.rstudio.core.client.widget.ModalDialogBase; 30 import org.rstudio.core.client.widget.Operation; 31 import org.rstudio.core.client.widget.OperationWithInput; 32 import org.rstudio.core.client.widget.ProgressIndicator; 33 import org.rstudio.core.client.widget.ProgressOperationWithInput; 34 import org.rstudio.core.client.widget.ThemedButton; 35 import org.rstudio.studio.client.RStudioGinjector; 36 import org.rstudio.studio.client.common.FileDialogs; 37 import org.rstudio.studio.client.common.GlobalDisplay; 38 import org.rstudio.studio.client.server.Bool; 39 import org.rstudio.studio.client.server.ServerRequestCallback; 40 import org.rstudio.studio.client.workbench.exportplot.ExportPlotResources; 41 import org.rstudio.studio.client.workbench.exportplot.ExportPlotUtils; 42 import org.rstudio.studio.client.workbench.model.SessionInfo; 43 import org.rstudio.studio.client.workbench.views.plots.model.PlotsServerOperations; 44 import org.rstudio.studio.client.workbench.views.plots.model.SavePlotAsPdfOptions; 45 46 import com.google.gwt.dom.client.Style.Unit; 47 import com.google.gwt.event.dom.client.ChangeEvent; 48 import com.google.gwt.event.dom.client.ChangeHandler; 49 import com.google.gwt.event.dom.client.ClickEvent; 50 import com.google.gwt.event.dom.client.ClickHandler; 51 import com.google.gwt.i18n.client.NumberFormat; 52 import com.google.gwt.user.client.ui.CheckBox; 53 import com.google.gwt.user.client.ui.Composite; 54 import com.google.gwt.user.client.ui.HTML; 55 import com.google.gwt.user.client.ui.HasVerticalAlignment; 56 import com.google.gwt.user.client.ui.HorizontalPanel; 57 import com.google.gwt.user.client.ui.Label; 58 import com.google.gwt.user.client.ui.ListBox; 59 import com.google.gwt.user.client.ui.RadioButton; 60 import com.google.gwt.user.client.ui.TextBox; 61 import com.google.gwt.user.client.ui.VerticalPanel; 62 import com.google.gwt.user.client.ui.Widget; 63 64 public class SavePlotAsPdfDialog extends ModalDialogBase 65 { SavePlotAsPdfDialog(GlobalDisplay globalDisplay, PlotsServerOperations server, final SessionInfo sessionInfo, FileSystemItem defaultDirectory, String defaultPlotName, final SavePlotAsPdfOptions options, double plotWidth, double plotHeight, final OperationWithInput<SavePlotAsPdfOptions> onClose)66 public SavePlotAsPdfDialog(GlobalDisplay globalDisplay, 67 PlotsServerOperations server, 68 final SessionInfo sessionInfo, 69 FileSystemItem defaultDirectory, 70 String defaultPlotName, 71 final SavePlotAsPdfOptions options, 72 double plotWidth, 73 double plotHeight, 74 final OperationWithInput<SavePlotAsPdfOptions> onClose) 75 { 76 super(Roles.getDialogRole()); 77 setText("Save Plot as PDF"); 78 79 globalDisplay_ = globalDisplay; 80 sessionInfo_ = sessionInfo; 81 server_ = server; 82 defaultDirectory_ = defaultDirectory; 83 defaultPlotName_ = defaultPlotName; 84 options_ = options; 85 plotWidth_ = plotWidth; 86 plotHeight_ = plotHeight; 87 88 progressIndicator_ = addProgressIndicator(); 89 90 ThemedButton saveButton = new ThemedButton("Save", 91 new ClickHandler() { 92 public void onClick(ClickEvent event) 93 { 94 attemptSavePdf(false, new Operation() { 95 @Override 96 public void execute() 97 { 98 // get options to send back to caller for persistence 99 PaperSize paperSize = paperSizeEditor_.selectedPaperSize(); 100 SavePlotAsPdfOptions pdfOptions = SavePlotAsPdfOptions.create( 101 paperSize.getWidth(), 102 paperSize.getHeight(), 103 isPortraitOrientation(), 104 useCairoPdf(), 105 viewAfterSaveCheckBox_.getValue()); 106 107 onClose.execute(pdfOptions); 108 109 closeDialog(); 110 } 111 }); 112 } 113 }); 114 addOkButton(saveButton); 115 addCancelButton(); 116 117 118 ThemedButton previewButton = new ThemedButton("Preview", 119 new ClickHandler() { 120 @Override 121 public void onClick(ClickEvent event) 122 { 123 // get temp file for preview 124 FileSystemItem tempDir = 125 FileSystemItem.createDir(sessionInfo.getTempDir()); 126 FileSystemItem previewPath = 127 FileSystemItem.createFile(tempDir.completePath("preview.pdf")); 128 129 // invoke handler 130 SavePlotAsHandler handler = createSavePlotAsHandler(); 131 handler.attemptSave(previewPath, true, true, null); 132 } 133 }); 134 addLeftButton(previewButton, ElementIds.PREVIEW_BUTTON); 135 } 136 137 @Override focusInitialControl()138 protected void focusInitialControl() 139 { 140 fileNameTextBox_.setFocus(true); 141 fileNameTextBox_.selectAll(); 142 } 143 144 @Override createMainWidget()145 protected Widget createMainWidget() 146 { 147 ExportPlotResources.Styles styles = ExportPlotResources.INSTANCE.styles(); 148 149 LayoutGrid grid = new LayoutGrid(7, 2); 150 grid.setStylePrimaryName(styles.savePdfMainWidget()); 151 152 // paper size 153 Label sizeLabel = new Label("PDF Size:"); 154 grid.setWidget(0, 0, sizeLabel); 155 156 // paper size label 157 paperSizeEditor_ = new PaperSizeEditor(sizeLabel); 158 grid.setWidget(0, 1, paperSizeEditor_); 159 160 // orientation 161 Label orientationLabel = new Label("Orientation:"); 162 grid.setWidget(1, 0, orientationLabel); 163 HorizontalPanel orientationPanel = new HorizontalPanel(); 164 orientationPanel.setSpacing(kComponentSpacing); 165 VerticalPanel orientationGroupPanel = new VerticalPanel(); 166 FieldSetWrapperPanel<VerticalPanel> orientationButtons = 167 new FieldSetWrapperPanel<>(orientationGroupPanel, orientationLabel); 168 final String kOrientationGroup = "Orientation"; 169 portraitRadioButton_ = new RadioButton(kOrientationGroup, "Portrait"); 170 orientationGroupPanel.add(portraitRadioButton_); 171 landscapeRadioButton_ = new RadioButton(kOrientationGroup, "Landscape"); 172 orientationGroupPanel.add(landscapeRadioButton_); 173 orientationPanel.add(orientationButtons); 174 grid.setWidget(1, 1, orientationPanel); 175 176 boolean haveCairoPdf = sessionInfo_.isCairoPdfAvailable(); 177 if (haveCairoPdf) 178 grid.setWidget(2, 0, new Label("Options:")); 179 HorizontalPanel cairoPdfPanel = new HorizontalPanel(); 180 String label = "Use cairo_pdf device"; 181 if (BrowseCap.isMacintoshDesktop()) 182 label = label + " (requires X11)"; 183 chkCairoPdf_ = new CheckBox(label); 184 chkCairoPdf_.getElement().getStyle().setMarginLeft(kComponentSpacing, 185 Unit.PX); 186 cairoPdfPanel.add(chkCairoPdf_); 187 chkCairoPdf_.setValue(haveCairoPdf && options_.getCairoPdf()); 188 if (haveCairoPdf) 189 grid.setWidget(2, 1, cairoPdfPanel); 190 191 grid.setWidget(3, 0, new HTML(" ")); 192 193 ThemedButton directoryButton = new ThemedButton("Directory..."); 194 directoryButton.setStylePrimaryName(styles.directoryButton()); 195 directoryButton.getElement().getStyle().setMarginLeft(-2, Unit.PX); 196 grid.setWidget(4, 0, directoryButton); 197 directoryButton.addClickHandler(new ClickHandler() { 198 @Override 199 public void onClick(ClickEvent event) 200 { 201 fileDialogs_.chooseFolder( 202 "Choose Directory", 203 fileSystemContext_, 204 FileSystemItem.createDir(directoryTextBox_.getText().trim()), 205 new ProgressOperationWithInput<FileSystemItem>() { 206 207 public void execute(FileSystemItem input, 208 ProgressIndicator indicator) 209 { 210 if (input == null) 211 return; 212 213 indicator.onCompleted(); 214 215 // update default 216 ExportPlotUtils.setDefaultSaveDirectory(input); 217 218 // set display 219 setDirectory(input); 220 } 221 }); 222 } 223 }); 224 225 226 directoryTextBox_ = new TextBox(); 227 directoryTextBox_.setReadOnly(true); 228 Roles.getTextboxRole().setAriaLabelProperty(directoryTextBox_.getElement(), "Selected Directory"); 229 setDirectory(defaultDirectory_); 230 directoryTextBox_.setStylePrimaryName(styles.savePdfDirectoryTextBox()); 231 grid.setWidget(4, 1, directoryTextBox_); 232 233 fileNameTextBox_ = new TextBox(); 234 fileNameTextBox_.setText(defaultPlotName_); 235 fileNameTextBox_.setStylePrimaryName(styles.savePdfFileNameTextBox()); 236 FormLabel fileNameLabel = new FormLabel("File name:", fileNameTextBox_); 237 fileNameLabel.setStylePrimaryName(styles.savePdfFileNameLabel()); 238 grid.setWidget(5, 0, fileNameLabel); 239 grid.setWidget(5, 1, fileNameTextBox_); 240 241 242 // view after size 243 viewAfterSaveCheckBox_ = new CheckBox("View plot after saving"); 244 viewAfterSaveCheckBox_.addStyleName(styles.savePdfViewAfterCheckbox()); 245 viewAfterSaveCheckBox_.setValue(options_.getViewAfterSave()); 246 grid.setWidget(6, 1, viewAfterSaveCheckBox_); 247 248 // set default value 249 if (options_.getPortrait()) 250 portraitRadioButton_.setValue(true); 251 else 252 landscapeRadioButton_.setValue(true); 253 254 // return the widget 255 return grid; 256 } 257 attemptSavePdf(boolean overwrite, final Operation onCompleted)258 private void attemptSavePdf(boolean overwrite, 259 final Operation onCompleted) 260 { 261 // validate file name 262 FileSystemItem targetPath = getTargetPath(); 263 if (targetPath == null) 264 { 265 globalDisplay_.showErrorMessage( 266 "File Name Required", 267 "You must provide a file name for the plot pdf.", 268 fileNameTextBox_); 269 return; 270 } 271 272 // invoke handler 273 SavePlotAsHandler handler = createSavePlotAsHandler(); 274 handler.attemptSave(targetPath, 275 overwrite, 276 viewAfterSaveCheckBox_.getValue(), 277 onCompleted); 278 } 279 280 281 getTargetPath()282 private FileSystemItem getTargetPath() 283 { 284 return ExportPlotUtils.composeTargetPath(".pdf", fileNameTextBox_, directory_); 285 } 286 setDirectory(FileSystemItem directory)287 private void setDirectory(FileSystemItem directory) 288 { 289 // set directory 290 directory_ = directory; 291 292 // set label 293 String dirLabel = ExportPlotUtils.shortDirectoryName(directory, 250); 294 directoryTextBox_.setText(dirLabel); 295 } 296 297 298 299 isPortraitOrientation()300 private boolean isPortraitOrientation() 301 { 302 return portraitRadioButton_.getValue(); 303 } 304 useCairoPdf()305 private boolean useCairoPdf() 306 { 307 return chkCairoPdf_.getValue(); 308 } 309 310 private class PaperSize 311 { PaperSize(String name, double width, double height)312 public PaperSize(String name, double width, double height) 313 { 314 name_ = name; 315 width_ = width; 316 height_ = height; 317 } 318 getName()319 public String getName() { return name_; } getWidth()320 public double getWidth() { return width_; } getHeight()321 public double getHeight() { return height_; } 322 323 private final String name_; 324 private final double width_; 325 private final double height_; 326 } 327 createSavePlotAsHandler()328 private SavePlotAsHandler createSavePlotAsHandler() 329 { 330 return new SavePlotAsHandler( 331 globalDisplay_, 332 progressIndicator_, 333 new SavePlotAsHandler.ServerOperations() 334 { 335 @Override 336 public void savePlot( 337 FileSystemItem targetPath, 338 boolean overwrite, 339 ServerRequestCallback<Bool> requestCallback) 340 { 341 PaperSize paperSize = paperSizeEditor_.selectedPaperSize(); 342 double width = paperSize.getWidth(); 343 double height = paperSize.getHeight(); 344 if (!isPortraitOrientation()) 345 { 346 width = paperSize.getHeight(); 347 height = paperSize.getWidth(); 348 } 349 350 server_.savePlotAsPdf(targetPath, 351 width, 352 height, 353 chkCairoPdf_.getValue(), 354 overwrite, 355 requestCallback); 356 } 357 358 @Override 359 public String getFileUrl(FileSystemItem path) 360 { 361 return server_.getFileUrl(path); 362 } 363 }); 364 } 365 366 private class PaperSizeEditor extends Composite 367 { 368 public PaperSizeEditor(Label visibleLabel) 369 { 370 ExportPlotResources.Styles styles = 371 ExportPlotResources.INSTANCE.styles(); 372 373 paperSizes_.add(new PaperSize("US Letter", 8.5, 11)); 374 paperSizes_.add(new PaperSize("US Legal", 8.5, 14)); 375 paperSizes_.add(new PaperSize("A4", 8.27, 11.69)); 376 paperSizes_.add(new PaperSize("A5", 5.83, 8.27)); 377 paperSizes_.add(new PaperSize("A6", 4.13, 5.83)); 378 paperSizes_.add(new PaperSize("4 x 6 in.", 4, 6)); 379 paperSizes_.add(new PaperSize("5 x 7 in.", 5, 7)); 380 paperSizes_.add(new PaperSize("6 x 8 in.", 6, 8)); 381 382 FieldSetWrapperPanel<HorizontalPanel> panel = new FieldSetWrapperPanel<>( 383 new HorizontalPanel(), visibleLabel); 384 panel.getPanel().setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); 385 panel.getPanel().setSpacing(kComponentSpacing); 386 387 // paper size list box 388 int selectedPaperSize = -1; 389 paperSizeListBox_ = new ListBox(); 390 paperSizeListBox_.setStylePrimaryName(styles.savePdfSizeListBox()); 391 Roles.getListboxRole().setAriaLabelProperty(paperSizeListBox_.getElement(), "Size Preset"); 392 for (int i = 0; i < paperSizes_.size(); i++) 393 { 394 PaperSize paperSize = paperSizes_.get(i); 395 paperSizeListBox_.addItem(paperSize.getName()); 396 if (paperSize.getWidth() == options_.getWidth() && 397 paperSize.getHeight() == options_.getHeight()) 398 { 399 selectedPaperSize = i; 400 } 401 } 402 PaperSize customPaperSize = new PaperSize("(Device Size)", 403 plotWidth_, 404 plotHeight_); 405 paperSizes_.add(customPaperSize); 406 paperSizeListBox_.addItem(customPaperSize.getName()); 407 408 if (selectedPaperSize == -1) 409 { 410 setCustomPaperSize(plotWidth_, plotHeight_); 411 selectedPaperSize = paperSizes_.size() - 1; 412 } 413 414 paperSizeListBox_.addChangeHandler(new ChangeHandler() { 415 public void onChange(ChangeEvent event) 416 { 417 updateSizeDescription(); 418 } 419 }); 420 panel.add(paperSizeListBox_); 421 422 HorizontalPanel editPanel = new HorizontalPanel(); 423 widthTextBox_ = new TextBox(); 424 widthTextBox_.setStylePrimaryName(styles.savePdfPaperSizeTextBox()); 425 Roles.getTextboxRole().setAriaLabelProperty(widthTextBox_.getElement(), "Width"); 426 widthTextBox_.addChangeHandler(sizeTextBoxChangeHandler_); 427 editPanel.add(widthTextBox_); 428 429 Label label = new Label(); 430 label.getElement().setInnerSafeHtml(SafeHtmlUtils.fromSafeConstant("×")); 431 label.setStylePrimaryName(styles.savePdfPaperSizeX()); 432 editPanel.add(label); 433 434 heightTextBox_ = new TextBox(); 435 heightTextBox_.setStylePrimaryName(styles.savePdfPaperSizeTextBox()); 436 Roles.getTextboxRole().setAriaLabelProperty(heightTextBox_.getElement(), "Height"); 437 heightTextBox_.addChangeHandler(sizeTextBoxChangeHandler_); 438 editPanel.add(heightTextBox_); 439 panel.add(editPanel); 440 441 Label inchesLabel = new Label("inches"); 442 inchesLabel.setStylePrimaryName(styles.savePdfPaperSizeX()); 443 editPanel.add(inchesLabel); 444 445 paperSizeListBox_.setSelectedIndex(selectedPaperSize); 446 updateSizeDescription(); 447 448 initWidget(panel); 449 } 450 451 public PaperSize selectedPaperSize() 452 { 453 int selectedSize = paperSizeListBox_.getSelectedIndex(); 454 return paperSizes_.get(selectedSize); 455 } 456 457 private void updateSizeDescription() 458 { 459 setPaperSize(selectedPaperSize()); 460 } 461 462 private void setPaperSize(PaperSize paperSize) 463 { 464 widthTextBox_.setText(sizeFormat_.format(paperSize.getWidth())); 465 heightTextBox_.setText(sizeFormat_.format(paperSize.getHeight())); 466 } 467 468 private void setCustomPaperSize(double width, double height) 469 { 470 paperSizes_.remove(paperSizes_.size() - 1); 471 paperSizes_.add(new PaperSize("(Custom)", width, height)); 472 } 473 474 private ChangeHandler sizeTextBoxChangeHandler_ = new ChangeHandler() { 475 @Override 476 public void onChange(ChangeEvent event) 477 { 478 // read width and height 479 PaperSize defaultSize = selectedPaperSize(); 480 double width = readSizeEntry(widthTextBox_, defaultSize.getWidth()); 481 double height = readSizeEntry(heightTextBox_, 482 defaultSize.getHeight()); 483 484 // see if it matches an existing size 485 int sizeIndex = -1; 486 for (int i=0; i<paperSizes_.size(); i++) 487 { 488 PaperSize paperSize = paperSizes_.get(i); 489 if (paperSize.getWidth() == width && 490 paperSize.getHeight() == height) 491 { 492 sizeIndex = i; 493 break; 494 } 495 } 496 497 // if it doesn't then update custom 498 if (sizeIndex == -1) 499 { 500 setCustomPaperSize(width, height); 501 sizeIndex = paperSizes_.size() - 1; 502 } 503 504 // select 505 paperSizeListBox_.setSelectedIndex(sizeIndex); 506 } 507 }; 508 509 private double readSizeEntry(TextBox textBox, double defaultValue) 510 { 511 double size = defaultValue; 512 try 513 { 514 size = Double.parseDouble(textBox.getText().trim()); 515 516 if (size < kMimimumSize) 517 size = defaultValue; 518 else if (size > kMaximumSize) 519 size = defaultValue; 520 } 521 catch(NumberFormatException e) 522 { 523 } 524 textBox.setText(sizeFormat_.format(size)); 525 return size; 526 } 527 528 529 private ListBox paperSizeListBox_; 530 private final TextBox widthTextBox_; 531 private final TextBox heightTextBox_; 532 private final List<PaperSize> paperSizes_ = new ArrayList<>(); 533 private final NumberFormat sizeFormat_ = NumberFormat.getFormat("##0.00"); 534 535 private final double kMimimumSize = 3.0; 536 private final double kMaximumSize = 100.0; 537 } 538 539 540 541 private final GlobalDisplay globalDisplay_; 542 private final SessionInfo sessionInfo_; 543 private final PlotsServerOperations server_; 544 private final SavePlotAsPdfOptions options_; 545 private final double plotWidth_; 546 private final double plotHeight_; 547 private final FileSystemItem defaultDirectory_; 548 private final String defaultPlotName_; 549 private final ProgressIndicator progressIndicator_; 550 551 private TextBox fileNameTextBox_; 552 private FileSystemItem directory_; 553 private TextBox directoryTextBox_; 554 private PaperSizeEditor paperSizeEditor_; 555 556 private RadioButton portraitRadioButton_; 557 private RadioButton landscapeRadioButton_; 558 559 private CheckBox chkCairoPdf_; 560 private CheckBox viewAfterSaveCheckBox_; 561 562 final int kComponentSpacing = 7; 563 564 private final FileSystemContext fileSystemContext_ = 565 RStudioGinjector.INSTANCE.getRemoteFileSystemContext(); 566 567 private final FileDialogs fileDialogs_ = 568 RStudioGinjector.INSTANCE.getFileDialogs(); 569 570 } 571