1 /* 2 * ExportPlotSizeEditor.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.exportplot; 16 17 import org.rstudio.core.client.Size; 18 import org.rstudio.core.client.dom.IFrameElementEx; 19 20 import com.google.gwt.dom.client.Style.Overflow; 21 import com.google.gwt.dom.client.Style.Unit; 22 import com.google.gwt.event.dom.client.ChangeEvent; 23 import com.google.gwt.event.dom.client.ChangeHandler; 24 import com.google.gwt.event.dom.client.ClickEvent; 25 import com.google.gwt.event.dom.client.ClickHandler; 26 import com.google.gwt.user.client.Command; 27 import com.google.gwt.user.client.Timer; 28 import com.google.gwt.user.client.Window; 29 import com.google.gwt.user.client.ui.CellPanel; 30 import com.google.gwt.user.client.ui.CheckBox; 31 import com.google.gwt.user.client.ui.Composite; 32 import com.google.gwt.user.client.ui.Focusable; 33 import com.google.gwt.user.client.ui.HTML; 34 import com.google.gwt.user.client.ui.HasHorizontalAlignment; 35 import com.google.gwt.user.client.ui.HasVerticalAlignment; 36 import com.google.gwt.user.client.ui.HorizontalPanel; 37 import com.google.gwt.user.client.ui.LayoutPanel; 38 import com.google.gwt.user.client.ui.TextBox; 39 import com.google.gwt.user.client.ui.VerticalPanel; 40 import com.google.gwt.user.client.ui.Widget; 41 import org.rstudio.core.client.widget.FocusHelper; 42 import org.rstudio.core.client.widget.FormLabel; 43 import org.rstudio.core.client.widget.GlassPanel; 44 import org.rstudio.core.client.widget.ResizeGripper; 45 import org.rstudio.core.client.widget.ThemedButton; 46 47 public class ExportPlotSizeEditor extends Composite 48 { 49 public interface Observer 50 { onResized(boolean withMouse)51 void onResized(boolean withMouse); 52 } 53 ExportPlotSizeEditor(int initialWidth, int initialHeight, boolean keepRatio, ExportPlotPreviewer previewer, Observer observer)54 public ExportPlotSizeEditor(int initialWidth, 55 int initialHeight, 56 boolean keepRatio, 57 ExportPlotPreviewer previewer, 58 Observer observer) 59 { 60 this(initialWidth, initialHeight, keepRatio, null, previewer, observer); 61 } 62 ExportPlotSizeEditor(int initialWidth, int initialHeight, boolean keepRatio, Widget extraWidget, ExportPlotPreviewer previewer, final Observer observer)63 public ExportPlotSizeEditor(int initialWidth, 64 int initialHeight, 65 boolean keepRatio, 66 Widget extraWidget, 67 ExportPlotPreviewer previewer, 68 final Observer observer) 69 { 70 // alias objects and resources 71 previewer_ = previewer; 72 observer_ = observer; 73 ExportPlotResources resources = ExportPlotResources.INSTANCE; 74 75 // main widget 76 VerticalPanel verticalPanel = new VerticalPanel(); 77 78 // if we have an extra widget then enclose it within a horizontal 79 // panel with it on the left and the options on the right 80 HorizontalPanel topPanel = new HorizontalPanel(); 81 CellPanel optionsPanel = null; 82 HorizontalPanel widthAndHeightPanel = null; 83 if (extraWidget != null) 84 { 85 topPanel.setWidth("100%"); 86 87 topPanel.add(extraWidget); 88 topPanel.setCellHorizontalAlignment(extraWidget, 89 HasHorizontalAlignment.ALIGN_LEFT); 90 91 optionsPanel = new VerticalPanel(); 92 optionsPanel.setStylePrimaryName( 93 resources.styles().verticalSizeOptions()); 94 optionsPanel.setSpacing(0); 95 topPanel.add(optionsPanel); 96 topPanel.setCellHorizontalAlignment( 97 optionsPanel, 98 HasHorizontalAlignment.ALIGN_RIGHT); 99 100 widthAndHeightPanel = new HorizontalPanel(); 101 widthAndHeightPanel.setStylePrimaryName( 102 resources.styles().widthAndHeightEntry()); 103 configureHorizontalOptionsPanel(widthAndHeightPanel); 104 optionsPanel.add(widthAndHeightPanel); 105 } 106 else 107 { 108 optionsPanel = topPanel; 109 optionsPanel.setStylePrimaryName( 110 resources.styles().horizontalSizeOptions()); 111 widthAndHeightPanel = topPanel; 112 configureHorizontalOptionsPanel(topPanel); 113 } 114 115 // image width 116 widthTextBox_ = createImageSizeTextBox(); 117 FormLabel widthLabel = createImageOptionLabel("Width:", widthTextBox_); 118 widthAndHeightPanel.add(widthLabel); 119 widthTextBox_.addChangeHandler(new ChangeHandler() { 120 @Override 121 public void onChange(ChangeEvent event) 122 { 123 // screen out programmatic sets 124 if (settingDimenensionInProgress_) 125 return; 126 127 // enforce min size 128 int width = constrainWidth(getImageWidth()); 129 130 // preserve aspect ratio if requested 131 if (getKeepRatio()) 132 { 133 double ratio = (double)lastHeight_ / (double)lastWidth_; 134 int height = constrainHeight((int) (ratio * (double)width)); 135 setHeightTextBox(height); 136 } 137 138 // set width 139 setWidthTextBox(width); 140 } 141 142 }); 143 widthAndHeightPanel.add(widthTextBox_); 144 145 // image height 146 widthAndHeightPanel.add(new HTML(" ")); 147 heightTextBox_ = createImageSizeTextBox(); 148 FormLabel heightLabel = createImageOptionLabel("Height:", heightTextBox_); 149 widthAndHeightPanel.add(heightLabel); 150 heightTextBox_.addChangeHandler(new ChangeHandler() { 151 @Override 152 public void onChange(ChangeEvent event) 153 { 154 // screen out programmatic sets 155 if (settingDimenensionInProgress_) 156 return; 157 158 // enforce min size 159 int height = constrainHeight(getImageHeight()); 160 161 // preserve aspect ratio if requested 162 if (getKeepRatio()) 163 { 164 double ratio = (double)lastWidth_ / (double)lastHeight_; 165 int width = constrainWidth((int) (ratio * (double)height)); 166 setWidthTextBox(width); 167 } 168 169 // always set height 170 setHeightTextBox(height); 171 } 172 173 }); 174 widthAndHeightPanel.add(heightTextBox_); 175 176 // add width and height panel to options panel container if necessary 177 if (widthAndHeightPanel != optionsPanel) 178 optionsPanel.add(widthAndHeightPanel); 179 180 // lock ratio check box 181 keepRatioCheckBox_ = new CheckBox(); 182 keepRatioCheckBox_.addStyleName(resources.styles().maintainAspectRatioCheckBox()); 183 keepRatioCheckBox_.setValue(keepRatio); 184 keepRatioCheckBox_.setText("Maintain aspect ratio"); 185 optionsPanel.add(keepRatioCheckBox_); 186 187 // image and sizer in layout panel (create now so we can call 188 // setSize in update button click handler) 189 previewPanel_ = new LayoutPanel(); 190 191 192 // update button 193 ThemedButton updateButton = new ThemedButton("Update Preview", 194 new ClickHandler(){ 195 public void onClick(ClickEvent event) 196 { 197 updatePreview(); 198 } 199 }); 200 updateButton.setStylePrimaryName( 201 resources.styles().updateImageSizeButton()); 202 optionsPanel.add(updateButton); 203 204 // add top panel 205 verticalPanel.add(topPanel); 206 207 // previewer 208 Widget previewWidget = previewer_.getWidget(); 209 210 // Stops mouse events from being routed to the iframe, which would 211 // interfere with resizing 212 final GlassPanel glassPanel = new GlassPanel(previewWidget); 213 glassPanel.getChildContainerElement().getStyle().setOverflow( 214 Overflow.VISIBLE); 215 glassPanel.setSize("100%", "100%"); 216 217 218 219 previewPanel_.add(glassPanel); 220 previewPanel_.setWidgetLeftRight(glassPanel, 221 0, Unit.PX, 222 IMAGE_INSET, Unit.PX); 223 previewPanel_.setWidgetTopBottom(glassPanel, 224 0, Unit.PX, 225 IMAGE_INSET, Unit.PX); 226 previewPanel_.getWidgetContainerElement( 227 glassPanel).getStyle().setOverflow(Overflow.VISIBLE); 228 229 // resize gripper 230 gripper_ = new ResizeGripper(new ResizeGripper.Observer() 231 { 232 @Override 233 public void onResizingStarted() 234 { 235 int startWidth = getImageWidth(); 236 int startHeight = getImageHeight(); 237 238 widthAspectRatio_ = (double)startWidth / (double)startHeight; 239 heightAspectRatio_ = (double)startHeight / (double)startWidth; 240 241 glassPanel.setGlass(true); 242 } 243 244 @Override 245 public void onResizing(int xDelta, int yDelta) 246 { 247 // get start width and height 248 int startWidth = getImageWidth(); 249 int startHeight = getImageHeight(); 250 251 // calculate new height and width 252 int newWidth = constrainWidth(startWidth + xDelta); 253 int newHeight = constrainHeight(startHeight + yDelta); 254 255 // preserve aspect ratio if requested 256 if (getKeepRatio()) 257 { 258 if (Math.abs(xDelta) > Math.abs(yDelta)) 259 newHeight = (int) (heightAspectRatio_ * (double)newWidth); 260 else 261 newWidth = (int) (widthAspectRatio_ * (double)newHeight); 262 } 263 264 // set text boxes 265 setWidthTextBox(newWidth); 266 setHeightTextBox(newHeight); 267 268 // set image preview size 269 setPreviewPanelSize(newWidth, newHeight); 270 } 271 272 @Override 273 public void onResizingCompleted() 274 { 275 glassPanel.setGlass(false); 276 previewer_.updatePreview(getImageWidth(), getImageHeight()); 277 observer.onResized(true); 278 } 279 280 private double widthAspectRatio_ = 1.0; 281 private double heightAspectRatio_ = 1.0; 282 }); 283 284 // layout gripper 285 previewPanel_.add(gripper_); 286 previewPanel_.setWidgetRightWidth(gripper_, 287 0, Unit.PX, 288 gripper_.getImageWidth(), Unit.PX); 289 previewPanel_.setWidgetBottomHeight(gripper_, 290 0, Unit.PX, 291 gripper_.getImageHeight(), Unit.PX); 292 293 // constrain dimensions 294 initialWidth = constrainWidth(initialWidth); 295 initialHeight = constrainHeight(initialHeight); 296 297 // initialize text boxes 298 setWidthTextBox(initialWidth); 299 setHeightTextBox(initialHeight); 300 301 // initialize preview 302 setPreviewPanelSize(initialWidth, initialHeight); 303 304 verticalPanel.add(previewPanel_); 305 306 // set initial focus widget 307 if (extraWidget == null) 308 initialFocusWidget_ = widthTextBox_; 309 else 310 initialFocusWidget_ = null; 311 312 initWidget(verticalPanel); 313 314 } 315 onSizerShown()316 public void onSizerShown() 317 { 318 previewer_.updatePreview(getImageWidth(), getImageHeight()); 319 320 if (initialFocusWidget_ != null) 321 FocusHelper.setFocusDeferred(initialFocusWidget_); 322 } 323 324 getImageWidth()325 public int getImageWidth() 326 { 327 try 328 { 329 return Integer.parseInt(widthTextBox_.getText().trim()); 330 } 331 catch(NumberFormatException ex) 332 { 333 setWidthTextBox(lastWidth_); 334 return lastWidth_; 335 } 336 } 337 getImageHeight()338 public int getImageHeight() 339 { 340 try 341 { 342 return Integer.parseInt(heightTextBox_.getText().trim()); 343 } 344 catch(NumberFormatException ex) 345 { 346 setHeightTextBox(lastHeight_); 347 return lastHeight_; 348 } 349 } 350 getKeepRatio()351 public boolean getKeepRatio() 352 { 353 return keepRatioCheckBox_.getValue(); 354 } 355 prepareForExport(final Command onReady)356 public void prepareForExport(final Command onReady) 357 { 358 if (getPreviewRequiresUpdate()) 359 { 360 updatePreview(); 361 new Timer() { 362 @Override 363 public void run() 364 { 365 onReady.execute(); 366 } 367 }.schedule(1000); 368 } 369 else 370 { 371 onReady.execute(); 372 } 373 } 374 getPreviewIFrame()375 public IFrameElementEx getPreviewIFrame() 376 { 377 return previewer_.getPreviewIFrame(); 378 } 379 setGripperVisible(boolean visible)380 public void setGripperVisible(boolean visible) 381 { 382 gripper_.setVisible(visible); 383 } 384 updatePreview()385 private void updatePreview() 386 { 387 setPreviewPanelSize(getImageWidth(), getImageHeight()); 388 previewer_.updatePreview(getImageWidth(), getImageHeight()); 389 observer_.onResized(false); 390 } 391 getPreviewRequiresUpdate()392 private boolean getPreviewRequiresUpdate() 393 { 394 IFrameElementEx iframe = previewer_.getPreviewIFrame(); 395 return (getImageWidth() != iframe.getClientWidth() || 396 getImageHeight() != iframe.getClientHeight()); 397 } 398 setWidthTextBox(int width)399 private void setWidthTextBox(int width) 400 { 401 settingDimenensionInProgress_ = true; 402 lastWidth_ = width; 403 widthTextBox_.setText(Integer.toString(width)); 404 settingDimenensionInProgress_ = false; 405 } 406 407 setHeightTextBox(int height)408 private void setHeightTextBox(int height) 409 { 410 settingDimenensionInProgress_ = true; 411 lastHeight_ = height; 412 heightTextBox_.setText(Integer.toString(height)); 413 settingDimenensionInProgress_ = false; 414 } 415 constrainWidth(int width)416 private int constrainWidth(int width) 417 { 418 if (width < MIN_SIZE) 419 { 420 keepRatioCheckBox_.setValue(false); 421 return MIN_SIZE; 422 } 423 else if (previewer_.getLimitToScreen() && (width > getMaxSize().width)) 424 { 425 keepRatioCheckBox_.setValue(false); 426 return getMaxSize().width; 427 } 428 else 429 { 430 return width; 431 } 432 } 433 constrainHeight(int height)434 private int constrainHeight(int height) 435 { 436 if (height < MIN_SIZE) 437 { 438 keepRatioCheckBox_.setValue(false); 439 return MIN_SIZE; 440 } 441 else if (previewer_.getLimitToScreen() && (height > getMaxSize().height)) 442 { 443 keepRatioCheckBox_.setValue(false); 444 return getMaxSize().height; 445 } 446 else 447 { 448 return height; 449 } 450 } 451 setPreviewPanelSize(int width, int height)452 private void setPreviewPanelSize(int width, int height) 453 { 454 Size maxSize = getMaxSize(); 455 456 if (width <= maxSize.width && height <= maxSize.height) 457 { 458 previewPanel_.setVisible(true); 459 previewPanel_.setSize((width + IMAGE_INSET) + "px", 460 (height + IMAGE_INSET) + "px"); 461 } 462 else 463 { 464 previewPanel_.setVisible(false); 465 } 466 } 467 getMaxSize()468 private Size getMaxSize() 469 { 470 return new Size(Window.getClientWidth() - 100, 471 Window.getClientHeight() - 200); 472 } 473 createImageOptionLabel(String text, Widget w)474 private FormLabel createImageOptionLabel(String text, Widget w) 475 { 476 FormLabel label = new FormLabel(text, w); 477 label.setStylePrimaryName( 478 ExportPlotResources.INSTANCE.styles().imageOptionLabel()); 479 return label; 480 } 481 createImageSizeTextBox()482 private TextBox createImageSizeTextBox() 483 { 484 TextBox textBox = new TextBox(); 485 textBox.setStylePrimaryName( 486 ExportPlotResources.INSTANCE.styles().imageSizeTextBox()); 487 return textBox; 488 } 489 490 configureHorizontalOptionsPanel(HorizontalPanel panel)491 private void configureHorizontalOptionsPanel(HorizontalPanel panel) 492 { 493 panel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); 494 panel.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_LEFT); 495 } 496 497 @Override onAttach()498 protected void onAttach() 499 { 500 super.onAttach(); 501 502 // ensure image preview is updated after dialog is shown 503 updatePreview(); 504 } 505 506 private static final int IMAGE_INSET = 6; 507 508 private final Observer observer_; 509 510 private final ExportPlotPreviewer previewer_; 511 private final ResizeGripper gripper_; 512 private final TextBox widthTextBox_; 513 private final TextBox heightTextBox_; 514 private final CheckBox keepRatioCheckBox_; 515 516 private final Focusable initialFocusWidget_; 517 518 private int lastWidth_; 519 private int lastHeight_; 520 521 private boolean settingDimenensionInProgress_ = false; 522 523 private final int MIN_SIZE = 100; 524 private LayoutPanel previewPanel_; 525 } 526