1 /* 2 * UserPrefs.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.model; 16 17 import com.google.gwt.core.client.JsArray; 18 import com.google.gwt.core.client.Scheduler; 19 import com.google.inject.Inject; 20 import com.google.inject.Singleton; 21 22 import org.rstudio.core.client.CommandWithArg; 23 import org.rstudio.core.client.Debug; 24 import org.rstudio.core.client.StringUtil; 25 import org.rstudio.core.client.command.CommandBinder; 26 import org.rstudio.core.client.command.Handler; 27 import org.rstudio.core.client.dom.WindowEx; 28 import org.rstudio.studio.client.RStudioGinjector; 29 import org.rstudio.studio.client.application.ApplicationQuit; 30 import org.rstudio.studio.client.application.AriaLiveService; 31 import org.rstudio.studio.client.application.Desktop; 32 import org.rstudio.studio.client.application.events.AriaLiveStatusEvent.Severity; 33 import org.rstudio.studio.client.application.events.AriaLiveStatusEvent.Timing; 34 import org.rstudio.studio.client.application.events.DeferredInitCompletedEvent; 35 import org.rstudio.studio.client.application.events.EventBus; 36 import org.rstudio.studio.client.application.events.ReloadEvent; 37 import org.rstudio.studio.client.common.Timers; 38 import org.rstudio.studio.client.common.satellite.Satellite; 39 import org.rstudio.studio.client.common.satellite.SatelliteManager; 40 import org.rstudio.studio.client.server.ServerError; 41 import org.rstudio.studio.client.server.ServerRequestCallback; 42 import org.rstudio.studio.client.server.Void; 43 import org.rstudio.studio.client.server.VoidServerRequestCallback; 44 import org.rstudio.studio.client.workbench.commands.Commands; 45 import org.rstudio.studio.client.workbench.model.Session; 46 import org.rstudio.studio.client.workbench.prefs.events.UserPrefsChangedEvent; 47 import org.rstudio.studio.client.common.GlobalDisplay; 48 49 @Singleton 50 public class UserPrefs extends UserPrefsComputed 51 implements UserPrefsChangedEvent.Handler, DeferredInitCompletedEvent.Handler 52 { 53 public interface Binder 54 extends CommandBinder<Commands, UserPrefs> {} 55 56 @Inject UserPrefs(Session session, EventBus eventBus, PrefsServerOperations server, SatelliteManager satelliteManager, Commands commands, Binder binder, GlobalDisplay display, AriaLiveService ariaLive, ApplicationQuit quit)57 public UserPrefs(Session session, 58 EventBus eventBus, 59 PrefsServerOperations server, 60 SatelliteManager satelliteManager, 61 Commands commands, 62 Binder binder, 63 GlobalDisplay display, 64 AriaLiveService ariaLive, 65 ApplicationQuit quit) 66 { 67 super(session.getSessionInfo(), 68 (session.getSessionInfo() == null ? 69 JsArray.createArray().cast() : 70 session.getSessionInfo().getPrefs())); 71 72 session_ = session; 73 eventBus_ = eventBus; 74 server_ = server; 75 satelliteManager_ = satelliteManager; 76 display_ = display; 77 commands_ = commands; 78 reloadAfterInit_ = false; 79 ariaLive_ = ariaLive; 80 quit_ = quit; 81 82 binder.bind(commands_, this); 83 84 eventBus.addHandler(UserPrefsChangedEvent.TYPE, this); 85 eventBus.addHandler(DeferredInitCompletedEvent.TYPE, this); 86 Scheduler.get().scheduleDeferred(() -> 87 { 88 origScreenReaderLabel_ = commands_.toggleScreenReaderSupport().getMenuLabel(false); 89 announceScreenReaderState(); 90 syncToggleTabKeyMovesFocusState(); 91 }); 92 } 93 writeUserPrefs()94 public void writeUserPrefs() 95 { 96 writeUserPrefs(null); 97 } 98 writeUserPrefs(CommandWithArg<Boolean> onCompleted)99 public void writeUserPrefs(CommandWithArg<Boolean> onCompleted) 100 { 101 updatePrefs(session_.getSessionInfo().getPrefs()); 102 server_.setUserPrefs( 103 session_.getSessionInfo().getUserPrefs(), 104 new ServerRequestCallback<Void>() 105 { 106 @Override 107 public void onResponseReceived(Void v) 108 { 109 UserPrefsChangedEvent event = new UserPrefsChangedEvent( 110 session_.getSessionInfo().getUserPrefLayer()); 111 112 if (Satellite.isCurrentWindowSatellite()) 113 { 114 RStudioGinjector.INSTANCE.getEventBus() 115 .fireEventToMainWindow(event); 116 } 117 else 118 { 119 // let satellites know prefs have changed 120 satelliteManager_.dispatchCrossWindowEvent(event); 121 } 122 123 if (onCompleted != null) 124 { 125 onCompleted.execute(true); 126 } 127 } 128 @Override 129 public void onError(ServerError error) 130 { 131 if (onCompleted != null) 132 { 133 onCompleted.execute(false); 134 } 135 Debug.logError(error); 136 } 137 }); 138 } 139 140 /** 141 * Indicates whether autosave is enabled, via any pref that turns it on. 142 * 143 * @return Whether auto save is enabled. 144 */ autoSaveEnabled()145 public boolean autoSaveEnabled() 146 { 147 return autoSaveOnBlur().getValue() || 148 StringUtil.equals(autoSaveOnIdle().getValue(), AUTO_SAVE_ON_IDLE_COMMIT); 149 } 150 151 /** 152 * Indicates the number of milliseconds after which to autosave. Won't return 153 * a value less than 500 since autosaves typically require a network call and 154 * other synchronization. 155 * 156 * @return The number of milliseconds. 157 */ autoSaveMs()158 public int autoSaveMs() 159 { 160 Integer ms = autoSaveIdleMs().getValue(); 161 if (ms < 500) 162 return 500; 163 return ms; 164 } 165 166 @Override onUserPrefsChanged(UserPrefsChangedEvent e)167 public void onUserPrefsChanged(UserPrefsChangedEvent e) 168 { 169 syncPrefs(e.getName(), e.getValues()); 170 } 171 172 @Handler onEditUserPrefs()173 public void onEditUserPrefs() 174 { 175 server_.editPreferences(new VoidServerRequestCallback()); 176 } 177 178 @Handler onClearUserPrefs()179 public void onClearUserPrefs() 180 { 181 display_.showYesNoMessage(GlobalDisplay.MSG_QUESTION, 182 "Confirm Clear Preferences", 183 "Are you sure you want to clear your preferences? All RStudio settings " + 184 "will be restored to their defaults, and your R session will be " + 185 "restarted.", 186 false, 187 (indicator) -> 188 { 189 server_.clearPreferences(new ServerRequestCallback<String>() 190 { 191 public void onResponseReceived(String path) 192 { 193 indicator.onCompleted(); 194 display_.showMessage( 195 GlobalDisplay.MSG_INFO, 196 "Preferences Cleared", 197 "Your preferences have been cleared, and your R session " + 198 "will now be restarted. A backup copy of your preferences " + 199 "can be found at: \n\n" + path, 200 () -> 201 { 202 // Restart R, then reload the UI when done 203 reloadAfterInit_ = true; 204 commands_.restartR().execute(); 205 }, 206 "Restart R", 207 false); 208 } 209 210 @Override 211 public void onError(ServerError error) 212 { 213 indicator.onError(error.getMessage()); 214 } 215 }); 216 }, 217 null, 218 "Clear Preferences", 219 "Cancel", 220 false); 221 } 222 223 @Override onDeferredInitCompleted(DeferredInitCompletedEvent event)224 public void onDeferredInitCompleted(DeferredInitCompletedEvent event) 225 { 226 // Called when R is finished initializing; if we have just cleared prefs, 227 // we also reload the UI when R's done restarting 228 if (reloadAfterInit_) 229 { 230 reloadAfterInit_ = false; 231 WindowEx.get().reload(); 232 } 233 } 234 235 @Handler onViewAllPrefs()236 public void onViewAllPrefs() 237 { 238 server_.viewPreferences(new VoidServerRequestCallback()); 239 } 240 setScreenReaderMenuState(boolean checked)241 private void setScreenReaderMenuState(boolean checked) 242 { 243 commands_.toggleScreenReaderSupport().setChecked(checked); 244 commands_.toggleScreenReaderSupport().setMenuLabel(checked ? 245 origScreenReaderLabel_ + " (enabled)" : 246 origScreenReaderLabel_ + " (disabled)"); 247 } 248 announceScreenReaderState()249 private void announceScreenReaderState() 250 { 251 // announce if screen reader is not enabled; most things work without enabling it, but for 252 // best experience user should turn it on 253 if (!enableScreenReader().getValue()) 254 { 255 Timers.singleShot(AriaLiveService.STARTUP_ANNOUNCEMENT_DELAY, () -> 256 { 257 String shortcut = commands_.toggleScreenReaderSupport().getShortcutRaw(); 258 ariaLive_.announce(AriaLiveService.SCREEN_READER_NOT_ENABLED, 259 "Warning: screen reader mode not enabled. Turn on using shortcut " + shortcut + ".", 260 Timing.IMMEDIATE, Severity.ALERT); 261 }); 262 } 263 setScreenReaderMenuState(enableScreenReader().getValue()); 264 } 265 setScreenReaderEnabled(boolean enabled)266 public void setScreenReaderEnabled(boolean enabled) 267 { 268 if (Desktop.hasDesktopFrame()) 269 Desktop.getFrame().setEnableAccessibility(enabled); 270 enableScreenReader().setGlobalValue(enabled); 271 272 // When screen-reader is enabled, reduce UI animations as they serve no purpose 273 // other than to potentially confuse the screen reader; turn animations back 274 // on when screen-reader support is disabled as that is the normal default and most 275 // users will never touch it. 276 RStudioGinjector.INSTANCE.getUserPrefs().reducedMotion().setGlobalValue(enabled); 277 278 // Disable virtual scrolling when screen reader is enabled 279 if (enabled) 280 RStudioGinjector.INSTANCE.getUserPrefs().limitVisibleConsole().setGlobalValue(false); 281 } 282 283 @Handler onToggleScreenReaderSupport()284 void onToggleScreenReaderSupport() 285 { 286 display_.showYesNoMessage(GlobalDisplay.MSG_QUESTION, 287 "Confirm Toggle Screen Reader Support", 288 "Are you sure you want to " + (enableScreenReader().getValue() ? "disable" : "enable") + " " + 289 "screen reader support? The application will reload to apply the change.", 290 false, 291 () -> 292 { 293 setScreenReaderEnabled(!enableScreenReader().getValue()); 294 writeUserPrefs(succeeded -> { 295 if (succeeded) 296 { 297 if (Desktop.isDesktop()) 298 quit_.doRestart(session_); 299 else 300 eventBus_.fireEvent(new ReloadEvent()); 301 } 302 else 303 { 304 display_.showErrorMessage("Error Changing Setting", 305 "The screen reader support setting could not be changed."); 306 } 307 }); 308 }, 309 () -> { 310 setScreenReaderMenuState(enableScreenReader().getValue()); 311 }, 312 false); 313 } 314 syncToggleTabKeyMovesFocusState(boolean checked)315 public void syncToggleTabKeyMovesFocusState(boolean checked) 316 { 317 commands_.toggleTabKeyMovesFocus().setChecked(checked); 318 } 319 syncToggleTabKeyMovesFocusState()320 private void syncToggleTabKeyMovesFocusState() 321 { 322 syncToggleTabKeyMovesFocusState( 323 RStudioGinjector.INSTANCE.getUserPrefs().tabKeyMoveFocus().getValue()); 324 } 325 326 @Handler onToggleTabKeyMovesFocus()327 void onToggleTabKeyMovesFocus() 328 { 329 boolean newMode = !RStudioGinjector.INSTANCE.getUserPrefs().tabKeyMoveFocus().getValue(); 330 RStudioGinjector.INSTANCE.getUserPrefs().tabKeyMoveFocus().setGlobalValue(newMode); 331 writeUserPrefs(succeeded -> 332 { 333 if (succeeded) 334 { 335 syncToggleTabKeyMovesFocusState(); 336 ariaLive_.announce(AriaLiveService.TAB_KEY_MODE, 337 newMode ? "Tab key always moves focus on" : "Tab key always moves focus off", 338 Timing.IMMEDIATE, Severity.STATUS); 339 } 340 else 341 { 342 display_.showErrorMessage("Error Changing Setting", 343 "The tab key moves focus setting could not be updated."); 344 } 345 }); 346 } 347 348 public static final int LAYER_DEFAULT = 0; 349 public static final int LAYER_SYSTEM = 1; 350 public static final int LAYER_COMPUTED = 2; 351 public static final int LAYER_USER = 3; 352 public static final int LAYER_PROJECT = 4; 353 354 public static final int MAX_TAB_WIDTH = 64; 355 public static final int MAX_WRAP_COLUMN = 256; 356 public static final int MAX_SCREEN_READER_CONSOLE_OUTPUT = 999; 357 358 private final Session session_; 359 private final PrefsServerOperations server_; 360 private final SatelliteManager satelliteManager_; 361 private final GlobalDisplay display_; 362 private final Commands commands_; 363 private final EventBus eventBus_; 364 private final AriaLiveService ariaLive_; 365 private final ApplicationQuit quit_; 366 367 private boolean reloadAfterInit_; 368 private String origScreenReaderLabel_; 369 } 370