1 /* 2 * AppearancePreferencesPane.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.prefs.views; 16 17 import com.google.gwt.core.client.JsArrayString; 18 import com.google.gwt.core.client.Scheduler; 19 import com.google.gwt.core.client.Scheduler.RepeatingCommand; 20 import com.google.gwt.dom.client.SelectElement; 21 import com.google.gwt.dom.client.Style; 22 import com.google.gwt.dom.client.Style.Unit; 23 import com.google.gwt.event.dom.client.ChangeEvent; 24 import com.google.gwt.event.dom.client.ChangeHandler; 25 import com.google.gwt.resources.client.ImageResource; 26 import com.google.gwt.user.client.ui.FlowPanel; 27 import com.google.gwt.user.client.ui.HorizontalPanel; 28 import com.google.gwt.user.client.ui.VerticalPanel; 29 import com.google.inject.Inject; 30 31 import org.rstudio.core.client.Debug; 32 import org.rstudio.core.client.StringUtil; 33 import org.rstudio.core.client.js.JsUtil; 34 import org.rstudio.core.client.prefs.RestartRequirement; 35 import org.rstudio.core.client.resources.ImageResource2x; 36 import org.rstudio.core.client.theme.ThemeFonts; 37 import org.rstudio.core.client.widget.FontDetector; 38 import org.rstudio.core.client.widget.Operation; 39 import org.rstudio.core.client.widget.SelectWidget; 40 import org.rstudio.core.client.widget.ThemedButton; 41 import org.rstudio.studio.client.RStudioGinjector; 42 import org.rstudio.studio.client.application.Desktop; 43 import org.rstudio.studio.client.application.DesktopInfo; 44 import org.rstudio.studio.client.common.FileDialogs; 45 import org.rstudio.studio.client.common.GlobalDisplay; 46 import org.rstudio.studio.client.common.dependencies.DependencyManager; 47 import org.rstudio.studio.client.server.ServerError; 48 import org.rstudio.studio.client.server.ServerRequestCallback; 49 import org.rstudio.studio.client.workbench.WorkbenchContext; 50 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs; 51 import org.rstudio.studio.client.workbench.prefs.model.UserState; 52 import org.rstudio.studio.client.workbench.views.source.editors.text.themes.AceTheme; 53 import org.rstudio.studio.client.workbench.views.source.editors.text.themes.AceThemes; 54 import org.rstudio.studio.client.workbench.views.source.editors.text.themes.model.ThemeServerOperations; 55 56 import java.util.HashMap; 57 import java.util.Set; 58 import java.util.TreeSet; 59 60 public class AppearancePreferencesPane extends PreferencesPane 61 { 62 63 @Inject AppearancePreferencesPane(PreferencesDialogResources res, UserPrefs userPrefs, UserState userState, final AceThemes themes, WorkbenchContext workbenchContext, GlobalDisplay globalDisplay, DependencyManager dependencyManager, FileDialogs fileDialogs, ThemeServerOperations server)64 public AppearancePreferencesPane(PreferencesDialogResources res, 65 UserPrefs userPrefs, 66 UserState userState, 67 final AceThemes themes, 68 WorkbenchContext workbenchContext, 69 GlobalDisplay globalDisplay, 70 DependencyManager dependencyManager, 71 FileDialogs fileDialogs, 72 ThemeServerOperations server) 73 { 74 res_ = res; 75 userPrefs_ = userPrefs; 76 userState_ = userState; 77 globalDisplay_ = globalDisplay; 78 dependencyManager_ = dependencyManager; 79 server_ = server; 80 81 VerticalPanel leftPanel = new VerticalPanel(); 82 83 relaunchRequired_ = false; 84 85 // dark-grey theme used to be derived from default, now also applies to sky 86 if (StringUtil.equals(userPrefs_.globalTheme().getValue(), "dark-grey")) 87 userPrefs_.globalTheme().setGlobalValue(UserPrefs.GLOBAL_THEME_DEFAULT); 88 89 final String originalTheme = userPrefs_.globalTheme().getValue(); 90 91 flatTheme_ = new SelectWidget("RStudio theme:", 92 new String[]{"Classic", "Modern", "Sky"}, 93 new String[]{ 94 UserPrefs.GLOBAL_THEME_CLASSIC, 95 UserPrefs.GLOBAL_THEME_DEFAULT, 96 UserPrefs.GLOBAL_THEME_ALTERNATE 97 }, 98 false); 99 flatTheme_.addStyleName(res.styles().themeChooser()); 100 flatTheme_.getListBox().setWidth("95%"); 101 flatTheme_.getListBox().addChangeHandler(event -> 102 relaunchRequired_ = (StringUtil.equals(originalTheme, "classic") && !StringUtil.equals(flatTheme_.getValue(), "classic")) || 103 (StringUtil.equals(flatTheme_.getValue(), "classic") && !StringUtil.equals(originalTheme, "classic"))); 104 105 String themeAlias = userPrefs_.globalTheme().getGlobalValue(); 106 flatTheme_.setValue(themeAlias); 107 108 leftPanel.add(flatTheme_); 109 110 if (Desktop.hasDesktopFrame()) 111 { 112 int initialIndex = -1; 113 int normalIndex = -1; 114 String[] zoomValues = new String[] { 115 "0.25", "0.50", "0.75", "0.80", "0.90", 116 "1.00", "1.10", "1.25", "1.50", "1.75", 117 "2.00", "2.50", "3.00", "4.00", "5.00" 118 }; 119 String[] zoomLabels = new String[zoomValues.length]; 120 double currentZoomLevel = DesktopInfo.getZoomLevel(); 121 for (int i = 0; i < zoomValues.length; i++) 122 { 123 double zoomValue = Double.parseDouble(zoomValues[i]); 124 125 if (zoomValue == 1.0) 126 normalIndex = i; 127 128 if (zoomValue == currentZoomLevel) 129 initialIndex = i; 130 131 zoomLabels[i] = StringUtil.formatPercent(zoomValue); 132 } 133 134 if (initialIndex == -1) 135 initialIndex = normalIndex; 136 137 zoomLevel_ = new SelectWidget("Zoom:", 138 zoomLabels, 139 zoomValues, 140 false); 141 zoomLevel_.getListBox().setSelectedIndex(initialIndex); 142 initialZoomLevel_ = zoomValues[initialIndex]; 143 144 leftPanel.add(zoomLevel_); 145 146 zoomLevel_.getListBox().addChangeHandler(event -> updatePreviewZoomLevel()); 147 } 148 149 String[] fonts = new String[] {}; 150 151 if (Desktop.isDesktop()) 152 { 153 // In desktop mode, get the list of installed fonts from Qt 154 String fontList = DesktopInfo.getFixedWidthFontList(); 155 156 if (fontList.isEmpty()) 157 registerFontListReadyCallback(); 158 else 159 fonts = fontList.split("\\n"); 160 } 161 else 162 { 163 // In server mode, get the installed set of fonts by querying the server 164 getInstalledFontList(); 165 } 166 167 String fontFaceLabel = fonts.length == 0 168 ? "Editor font (loading...):" 169 : "Editor font:"; 170 171 fontFace_ = new SelectWidget(fontFaceLabel, fonts, fonts, false, false, false); 172 fontFace_.getListBox().setWidth("95%"); 173 174 if (Desktop.isDesktop()) 175 { 176 // Get the fixed width font set in desktop mode 177 String value = DesktopInfo.getFixedWidthFont(); 178 String label = value.replaceAll("\\\"", ""); 179 if (!fontFace_.setValue(label)) 180 { 181 fontFace_.insertValue(0, label, value); 182 fontFace_.setValue(value); 183 } 184 } 185 else 186 { 187 // In server mode, there's always a Default option which uses a 188 // browser-specific font. 189 fontFace_.insertValue(0, DEFAULT_FONT_NAME, DEFAULT_FONT_VALUE); 190 } 191 192 initialFontFace_ = StringUtil.notNull(fontFace_.getValue()); 193 194 leftPanel.add(fontFace_); 195 fontFace_.addChangeHandler(new ChangeHandler() 196 { 197 @Override 198 public void onChange(ChangeEvent event) 199 { 200 String font = fontFace_.getValue(); 201 if (font == null || StringUtil.equals(font, DEFAULT_FONT_VALUE)) 202 { 203 preview_.setFont(ThemeFonts.getFixedWidthFont(), false); 204 } 205 else 206 { 207 preview_.setFont(font, !Desktop.hasDesktopFrame()); 208 } 209 } 210 }); 211 212 String[] labels = {"7", "8", "9", "10", "11", "12", "13", "14", "16", "18", "24", "36"}; 213 String[] values = new String[labels.length]; 214 for (int i = 0; i < labels.length; i++) 215 values[i] = Double.parseDouble(labels[i]) + ""; 216 217 fontSize_ = new SelectWidget("Editor font size:", 218 labels, 219 values, 220 false); 221 fontSize_.getListBox().setWidth("95%"); 222 if (!fontSize_.setValue(userPrefs.fontSizePoints().getGlobalValue() + "")) 223 fontSize_.getListBox().setSelectedIndex(3); 224 fontSize_.getListBox().addChangeHandler(new ChangeHandler() 225 { 226 public void onChange(ChangeEvent event) 227 { 228 preview_.setFontSize(Double.parseDouble(fontSize_.getValue())); 229 } 230 }); 231 232 theme_ = new SelectWidget("Editor theme:", 233 new String[0], 234 new String[0], 235 false); 236 theme_.getListBox().getElement().<SelectElement>cast().setSize(7); 237 theme_.getListBox().getElement().getStyle().setHeight(225, Unit.PX); 238 theme_.getListBox().addChangeHandler(new ChangeHandler() 239 { 240 @Override 241 public void onChange(ChangeEvent event) 242 { 243 AceTheme aceTheme = themeList_.get(theme_.getValue()); 244 preview_.setTheme(aceTheme.getUrl()); 245 removeThemeButton_.setEnabled(!aceTheme.isDefaultTheme()); 246 } 247 }); 248 theme_.addStyleName(res.styles().themeChooser()); 249 250 AceTheme currentTheme = userState_.theme().getGlobalValue().cast(); 251 addThemeButton_ = new ThemedButton("Add...", event -> 252 fileDialogs.openFile( 253 "Theme Files (*.tmTheme *.rstheme)", 254 RStudioGinjector.INSTANCE.getRemoteFileSystemContext(), 255 workbenchContext.getCurrentWorkingDir(), 256 "Theme Files (*.tmTheme *.rstheme)", 257 (input, indicator) -> 258 { 259 if (input == null) 260 return; 261 262 String inputStem = input.getStem(); 263 String inputPath = input.getPath(); 264 boolean isTmTheme = StringUtil.equalsIgnoreCase(".tmTheme", input.getExtension()); 265 boolean found = false; 266 for (AceTheme theme: themeList_.values()) 267 { 268 if (theme.isLocalCustomTheme() && 269 StringUtil.equalsIgnoreCase(theme.getFileStem(), inputStem)) 270 { 271 showThemeExistsDialog(inputStem, () -> addTheme(inputPath, themes, isTmTheme)); 272 found = true; 273 break; 274 } 275 } 276 277 if (!found) 278 { 279 addTheme(inputPath, themes, isTmTheme); 280 } 281 282 indicator.onCompleted(); 283 })); 284 addThemeButton_.setLeftAligned(true); 285 removeThemeButton_ = new ThemedButton( 286 "Remove", 287 event -> showRemoveThemeWarning( 288 theme_.getValue(), 289 () -> removeTheme(theme_.getValue(), themes))); 290 removeThemeButton_.setLeftAligned(true); 291 removeThemeButton_.setEnabled(!currentTheme.isDefaultTheme()); 292 293 HorizontalPanel buttonPanel = new HorizontalPanel(); 294 buttonPanel.add(addThemeButton_); 295 buttonPanel.add(removeThemeButton_); 296 297 leftPanel.add(fontSize_); 298 leftPanel.add(theme_); 299 leftPanel.add(buttonPanel); 300 301 FlowPanel previewPanel = new FlowPanel(); 302 303 previewPanel.setSize("100%", "100%"); 304 preview_ = new AceEditorPreview(CODE_SAMPLE); 305 preview_.setHeight(previewDefaultHeight_); 306 preview_.setWidth("278px"); 307 preview_.setFontSize(Double.parseDouble(fontSize_.getValue())); 308 preview_.setTheme(currentTheme.getUrl()); 309 updatePreviewZoomLevel(); 310 previewPanel.add(preview_); 311 312 HorizontalPanel hpanel = new HorizontalPanel(); 313 hpanel.setWidth("100%"); 314 hpanel.add(leftPanel); 315 hpanel.setCellWidth(leftPanel, "160px"); 316 hpanel.add(previewPanel); 317 318 add(hpanel); 319 320 // Themes are retrieved asynchronously, so we have to update the theme list and preview panel 321 // asynchronously too. We also need to wait until the next event cycle so that the progress 322 // indicator will be ready. 323 Scheduler.get().scheduleDeferred(() -> setThemes(themes)); 324 } 325 326 @Override setPaneVisible(boolean visible)327 protected void setPaneVisible(boolean visible) 328 { 329 super.setPaneVisible(visible); 330 if (visible) 331 { 332 // When making the pane visible in desktop mode, add or remove a 333 // meaningless transform to the iframe hosting the preview. This is 334 // gross but necessary to work around a QtWebEngine bug which causes 335 // the region to not paint at all (literally showing the previous 336 // contents of the screen buffer) until invalidated in some way. 337 // 338 // Known to be an issue with Qt 5.12.8/Chromium 69; could be removed if 339 // the bug is fixed in later releases. 340 // 341 // See https://github.com/rstudio/rstudio/issues/6268 342 343 Scheduler.get().scheduleDeferred(() -> 344 { 345 Style style = preview_.getElement().getStyle(); 346 String translate = "translate(0px, 0px)"; 347 String transform = style.getProperty("transform"); 348 style.setProperty("transform", 349 StringUtil.isNullOrEmpty(transform) || !StringUtil.equals(translate, transform) ? 350 translate : 351 ""); 352 }); 353 } 354 } 355 removeTheme(String themeName, AceThemes themes, Operation afterOperation)356 private void removeTheme(String themeName, AceThemes themes, Operation afterOperation) 357 { 358 AceTheme currentTheme = userState_.theme().getGlobalValue().cast(); 359 if (StringUtil.equalsIgnoreCase(currentTheme.getName(), themeName)) 360 { 361 showCantRemoveActiveThemeDialog(currentTheme.getName()); 362 } 363 else 364 { 365 themes.removeTheme( 366 themeName, 367 errorMessage -> showCantRemoveThemeDialog(themeName, errorMessage), 368 () -> 369 { 370 updateThemes(currentTheme.getName(), themes); 371 afterOperation.execute(); 372 }); 373 } 374 } 375 removeTheme(String themeName, AceThemes themes)376 private void removeTheme(String themeName, AceThemes themes) 377 { 378 // No after operation necessary. 379 removeTheme(themeName, themes, () -> {}); 380 } 381 doAddTheme(String inputPath, AceThemes themes, boolean isTmTheme)382 private void doAddTheme(String inputPath, AceThemes themes, boolean isTmTheme) 383 { 384 if (isTmTheme) 385 dependencyManager_.withThemes( 386 "Converting a tmTheme to an rstheme", 387 () -> themes.addTheme( 388 inputPath, 389 result -> updateThemes(result, themes), 390 error -> showCantAddThemeDialog(inputPath, error))); 391 else 392 themes.addTheme( 393 inputPath, 394 result -> updateThemes(result, themes), 395 error -> showCantAddThemeDialog(inputPath, error)); 396 397 } 398 addTheme(String inputPath, AceThemes themes, boolean isTmTheme)399 private void addTheme(String inputPath, AceThemes themes, boolean isTmTheme) 400 { 401 // Get the theme name and check if it's in the current list of themes. 402 themes.getThemeName( 403 inputPath, 404 name -> 405 { 406 if (themeList_.containsKey(name)) 407 { 408 if (themeList_.get(name).isLocalCustomTheme()) 409 { 410 showDuplicateThemeError( 411 name, 412 () -> removeTheme( 413 name, 414 themes, 415 () -> doAddTheme(inputPath, themes, isTmTheme))); 416 } 417 else 418 { 419 showDuplicateThemeWarning( 420 name, 421 () -> doAddTheme(inputPath, themes, isTmTheme)); 422 } 423 } 424 else 425 { 426 doAddTheme(inputPath, themes, isTmTheme); 427 } 428 }, 429 error -> showCantAddThemeDialog(inputPath, error)); 430 } 431 setThemes(AceThemes themes)432 private void setThemes(AceThemes themes) 433 { 434 themes.getThemes( 435 themeList -> 436 { 437 themeList_ = themeList; 438 439 // It's possible the current theme was removed outside the context of 440 // RStudio, so choose a default if it can't be found. 441 AceTheme currentTheme = userState_.theme().getGlobalValue().cast(); 442 if (!themeList_.containsKey(currentTheme.getName())) 443 { 444 StringBuilder warningMsg = new StringBuilder(); 445 warningMsg.append("The active theme \"") 446 .append(currentTheme.getName()) 447 .append("\" could not be found. It's possible it was removed outside the context of RStudio. Switching to the ") 448 .append(currentTheme.isDark() ? "dark " : "light ") 449 .append("default theme: \""); 450 451 currentTheme = AceTheme.createDefault(currentTheme.isDark()); 452 userState_.theme().setGlobalValue(currentTheme); 453 preview_.setTheme(currentTheme.getUrl()); 454 455 warningMsg.append(currentTheme.getName()) 456 .append("\"."); 457 Debug.logWarning(warningMsg.toString()); 458 } 459 460 theme_.setChoices(themeList_.keySet().toArray(new String[0])); 461 theme_.setValue(currentTheme.getName()); 462 removeThemeButton_.setEnabled(!currentTheme.isDefaultTheme()); 463 }, 464 getProgressIndicator()); 465 } 466 updateThemes(String focusedThemeName, AceThemes themes)467 private void updateThemes(String focusedThemeName, AceThemes themes) 468 { 469 themes.getThemes( 470 themeList-> 471 { 472 themeList_ = themeList; 473 474 String themeName = focusedThemeName; 475 if (!themeList.containsKey(themeName)) 476 { 477 Debug.logWarning("The theme \"" + focusedThemeName + "\" does not exist. It may have been manually deleted outside the context of RStudio."); 478 themeName = AceTheme.createDefault().getName(); 479 } 480 AceTheme focusedTheme = themeList.get(themeName); 481 482 theme_.setChoices(themeList_.keySet().toArray(new String[0])); 483 theme_.setValue(focusedTheme.getName()); 484 preview_.setTheme(focusedTheme.getUrl()); 485 removeThemeButton_.setEnabled(!focusedTheme.isDefaultTheme()); 486 }, 487 getProgressIndicator()); 488 } 489 updatePreviewZoomLevel()490 private void updatePreviewZoomLevel() 491 { 492 // no zoom preview on desktop 493 if (Desktop.hasDesktopFrame()) 494 { 495 preview_.setZoomLevel(Double.parseDouble(zoomLevel_.getValue()) / 496 DesktopInfo.getZoomLevel()); 497 } 498 } 499 showThemeExistsDialog(String inputFileName, Operation continueOperation)500 private void showThemeExistsDialog(String inputFileName, Operation continueOperation) 501 { 502 StringBuilder msg = new StringBuilder(); 503 msg.append("A theme file with the same name, '") 504 .append(inputFileName) 505 .append("', already exists. Adding the theme will cause the existing file to be ") 506 .append("overwritten. Would you like to add the theme anyway?"); 507 globalDisplay_.showYesNoMessage( 508 GlobalDisplay.MSG_WARNING, 509 "Theme File Already Exists", 510 msg.toString(), 511 continueOperation, 512 false); 513 } 514 showCantAddThemeDialog(String themePath, String errorMessage)515 private void showCantAddThemeDialog(String themePath, String errorMessage) 516 { 517 StringBuilder msg = new StringBuilder(); 518 msg.append("Unable to add the theme '") 519 .append(themePath) 520 .append("'. The following error occurred: ") 521 .append(errorMessage); 522 523 globalDisplay_.showErrorMessage("Failed to Add Theme", msg.toString()); 524 } 525 showCantRemoveThemeDialog(String themeName, String errorMessage)526 private void showCantRemoveThemeDialog(String themeName, String errorMessage) 527 { 528 StringBuilder msg = new StringBuilder(); 529 msg.append("Unable to remove the theme '") 530 .append(themeName) 531 .append("': ") 532 .append(errorMessage); 533 534 globalDisplay_.showErrorMessage("Failed to Remove Theme", msg.toString()); 535 } 536 showCantRemoveActiveThemeDialog(String themeName)537 private void showCantRemoveActiveThemeDialog(String themeName) 538 { 539 StringBuilder msg = new StringBuilder(); 540 msg.append("The theme \"") 541 .append(themeName) 542 .append("\" cannot be removed because it is currently in use. To delete this theme,") 543 .append(" please change the active theme and retry."); 544 545 globalDisplay_.showErrorMessage("Cannot Remove Active Theme", msg.toString()); 546 } 547 showRemoveThemeWarning(String themeName, Operation continueOperation)548 private void showRemoveThemeWarning(String themeName, Operation continueOperation) 549 { 550 StringBuilder msg = new StringBuilder(); 551 msg.append("Taking this action will delete the theme \"") 552 .append(themeName) 553 .append("\" and cannot be undone. Are you sure you wish to continue?"); 554 555 globalDisplay_.showYesNoMessage( 556 GlobalDisplay.MSG_WARNING, 557 "Remove Theme", 558 msg.toString(), 559 continueOperation, 560 false); 561 } 562 showDuplicateThemeError(String themeName, Operation continueOperation)563 private void showDuplicateThemeError(String themeName, Operation continueOperation) 564 { 565 StringBuilder msg = new StringBuilder(); 566 msg.append("There is an existing theme with the same name as the new theme in the current") 567 .append(" location. Would you like remove the existing theme, \"") 568 .append(themeName) 569 .append("\", and add the new theme?"); 570 571 globalDisplay_.showYesNoMessage( 572 GlobalDisplay.MSG_ERROR, 573 "Duplicate Theme In Same Location", 574 msg.toString(), 575 continueOperation, 576 false); 577 } 578 showDuplicateThemeWarning(String themeName, Operation continueOperation)579 private void showDuplicateThemeWarning(String themeName, Operation continueOperation) 580 { 581 StringBuilder msg = new StringBuilder(); 582 msg.append("There is an existing theme with the same name as the new theme, \"") 583 .append(themeName) 584 .append("\" in another location. The existing theme will be hidden but not removed.") 585 .append(" Removing the new theme later will un-hide the existing theme. Would you") 586 .append(" like to continue?"); 587 588 globalDisplay_.showYesNoMessage( 589 GlobalDisplay.MSG_WARNING, 590 "Duplicate Theme In Another Location", 591 msg.toString(), 592 continueOperation, 593 true); 594 } 595 596 @Override getIcon()597 public ImageResource getIcon() 598 { 599 return new ImageResource2x(res_.iconAppearance2x()); 600 } 601 602 @Override initialize(UserPrefs prefs)603 protected void initialize(UserPrefs prefs) 604 { 605 } 606 607 @Override onApply(UserPrefs rPrefs)608 public RestartRequirement onApply(UserPrefs rPrefs) 609 { 610 RestartRequirement restartRequirement = super.onApply(rPrefs); 611 612 if (relaunchRequired_) 613 restartRequirement.setUiReloadRequired(true); 614 615 String themeName = flatTheme_.getValue(); 616 if (!StringUtil.equals(themeName, userPrefs_.globalTheme().getGlobalValue())) 617 { 618 userPrefs_.globalTheme().setGlobalValue(themeName, false); 619 } 620 621 double fontSize = Double.parseDouble(fontSize_.getValue()); 622 userPrefs_.fontSizePoints().setGlobalValue(fontSize); 623 if (!StringUtil.equals(theme_.getValue(), userPrefs_.editorTheme().getGlobalValue())) 624 { 625 userState_.theme().setGlobalValue(themeList_.get(theme_.getValue())); 626 userPrefs_.editorTheme().setGlobalValue(theme_.getValue(), false); 627 } 628 629 if (!StringUtil.equals(initialFontFace_, fontFace_.getValue())) 630 { 631 String fontFace = fontFace_.getValue(); 632 initialFontFace_ = fontFace; 633 if (Desktop.hasDesktopFrame()) 634 { 635 // In desktop mode the font is stored in a per-machine file since 636 // the font list varies between machines. 637 Desktop.getFrame().setFixedWidthFont(fontFace); 638 } 639 else 640 { 641 if (StringUtil.equals(fontFace, DEFAULT_FONT_VALUE)) 642 { 643 // User has chosen the default font face 644 userPrefs_.serverEditorFontEnabled().setGlobalValue(false); 645 } 646 else 647 { 648 // User has chosen a specific font 649 userPrefs_.serverEditorFontEnabled().setGlobalValue(true); 650 userPrefs_.serverEditorFont().setGlobalValue(fontFace); 651 } 652 } 653 restartRequirement.setUiReloadRequired(true); 654 } 655 656 if (Desktop.hasDesktopFrame()) 657 { 658 if (!StringUtil.equals(initialZoomLevel_, zoomLevel_.getValue())) 659 { 660 double zoomLevel = Double.parseDouble(zoomLevel_.getValue()); 661 initialZoomLevel_ = zoomLevel_.getValue(); 662 Desktop.getFrame().setZoomLevel(zoomLevel); 663 } 664 } 665 666 return restartRequirement; 667 } 668 669 @Override getName()670 public String getName() 671 { 672 return "Appearance"; 673 } 674 registerFontListReadyCallback()675 private final native void registerFontListReadyCallback() 676 /*-{ 677 678 var self = this; 679 $wnd.onFontListReady = $entry(function() { 680 self.@org.rstudio.studio.client.workbench.prefs.views.AppearancePreferencesPane::onFontListReady()(); 681 }); 682 683 }-*/; 684 onFontListReady()685 private void onFontListReady() 686 { 687 // NOTE: we use a short poll as we might receive this notification 688 // just before the Qt webchannel has been able to synchronize with 689 // the front-end 690 Scheduler.get().scheduleFixedDelay(new RepeatingCommand() 691 { 692 private int retryCount_ = 0; 693 694 @Override 695 public boolean execute() 696 { 697 if (retryCount_++ > 20) 698 return false; 699 700 String fonts = DesktopInfo.getFixedWidthFontList(); 701 if (fonts.isEmpty()) 702 return true; 703 704 String[] fontList = fonts.split("\\n"); 705 populateFontList(fontList); 706 return false; 707 } 708 709 }, 100); 710 } 711 getInstalledFontList()712 private void getInstalledFontList() 713 { 714 // Search for installed fixed-width fonts on this web browser. 715 final Set<String> browserFonts = new TreeSet<>(); 716 JsArrayString candidates = userPrefs_.browserFixedWidthFonts().getGlobalValue(); 717 for (String candidate: JsUtil.asIterable(candidates)) 718 { 719 if (FontDetector.isFontSupported(candidate)) 720 { 721 browserFonts.add(candidate); 722 } 723 } 724 725 server_.getInstalledFonts(new ServerRequestCallback<JsArrayString>() 726 { 727 @Override 728 public void onResponseReceived(JsArrayString fonts) 729 { 730 browserFonts.addAll(JsUtil.toList(fonts)); 731 populateFontList(browserFonts.toArray(new String[browserFonts.size()])); 732 fontFace_.insertValue(0, DEFAULT_FONT_NAME, DEFAULT_FONT_VALUE); 733 734 String font = null; 735 if (userPrefs_.serverEditorFontEnabled().getValue()) 736 { 737 // Use the user's supplied font 738 font = userPrefs_.serverEditorFont().getValue(); 739 } 740 741 if (StringUtil.isNullOrEmpty(font)) 742 { 743 // No font selected 744 fontFace_.setValue(DEFAULT_FONT_VALUE); 745 } 746 else 747 { 748 // If there's a non-empty, enabled font, set it as the default 749 fontFace_.setValue(font); 750 preview_.setFont(font, true); 751 } 752 753 initialFontFace_ = StringUtil.notNull(fontFace_.getValue()); 754 } 755 756 @Override 757 public void onError(ServerError error) 758 { 759 // Change label so it doesn't load indefinitely 760 fontFace_.setLabel("Editor font:"); 761 762 Debug.logError(error); 763 } 764 }); 765 } 766 populateFontList(String[] fontList)767 private void populateFontList(String[] fontList) 768 { 769 String value = fontFace_.getValue(); 770 if (!StringUtil.isNullOrEmpty(value)) 771 value = value.replaceAll("\\\"", ""); 772 fontFace_.setLabel("Editor font:"); 773 fontFace_.setChoices(fontList, fontList); 774 fontFace_.setValue(value); 775 } 776 777 private final PreferencesDialogResources res_; 778 private final UserPrefs userPrefs_; 779 private final UserState userState_; 780 private SelectWidget helpFontSize_; 781 private SelectWidget fontSize_; 782 private SelectWidget theme_; 783 private ThemedButton addThemeButton_; 784 private ThemedButton removeThemeButton_; 785 private final AceEditorPreview preview_; 786 private SelectWidget fontFace_; 787 private String initialFontFace_; 788 private SelectWidget zoomLevel_; 789 private String initialZoomLevel_; 790 private final SelectWidget flatTheme_; 791 private Boolean relaunchRequired_; 792 private static String previewDefaultHeight_ = "533px"; 793 private HashMap<String, AceTheme> themeList_; 794 private final GlobalDisplay globalDisplay_; 795 private final DependencyManager dependencyManager_; 796 private final ThemeServerOperations server_; 797 private int renderPass_ = 1; 798 799 private final static String DEFAULT_FONT_NAME = "(Default)"; 800 private final static String DEFAULT_FONT_VALUE = "__default__"; 801 802 private static final String CODE_SAMPLE = 803 "# plotting of R objects\n" + 804 "plot <- function (x, y, ...)\n" + 805 "{\n" + 806 " if (is.function(x) && \n" + 807 " is.null(attr(x, \"class\")))\n" + 808 " {\n" + 809 " if (missing(y))\n" + 810 " y <- NULL\n" + 811 " \n" + 812 " # check for ylab argument\n" + 813 " hasylab <- function(...) \n" + 814 " !all(is.na(\n" + 815 " pmatch(names(list(...)),\n" + 816 " \"ylab\")))\n" + 817 " \n" + 818 " if (hasylab(...))\n" + 819 " plot.function(x, y, ...)\n" + 820 " \n" + 821 " else \n" + 822 " plot.function(\n" + 823 " x, y, \n" + 824 " ylab = paste(\n" + 825 " deparse(substitute(x)),\n" + 826 " \"(x)\"), \n" + 827 " ...)\n" + 828 " }\n" + 829 " else \n" + 830 " UseMethod(\"plot\")\n" + 831 "}\n"; 832 } 833